To link two microcontrollers wirelessly via Bluetooth, you are essentially bridging two local UART buses over a 2.4 GHz RF link. For new Arduino Bluetooth Arduino builds in 2026, skip the legacy HC-05 Classic Bluetooth modules. The definitive choice is the ESP32-WROOM-32 running the ESP-NOW protocol. It requires no pairing handshake, delivers sub-millisecond latency, supports 250-byte payloads, and costs roughly $5 per node compared to $8+ for a genuine HC-05.
This primer breaks down the physical layer requirements, compares the three dominant wireless serial protocols, and provides a complete, copy-pasteable ESP-NOW master/slave implementation.
The Physical Layer: Wiring UART and Voltage Translation
Bluetooth modules do not speak 'Bluetooth' to your microcontroller; they speak UART (Universal Asynchronous Receiver-Transmitter). The RF module acts as a transparent serial bridge. Because UART is an asynchronous point-to-point bus, it relies on agreed-upon baud rates rather than a shared clock line.
Voltage Dividers and Logic Levels
The most common hardware failure in Arduino-to-Bluetooth wiring is frying the module's RX pin. Most legacy Arduino boards (Uno, Mega, Nano) operate at 5V logic. However, the HC-05, HM-10, and ESP32 all use 3.3V logic on their UART pins. While many HC-05 modules have a 3.3V onboard voltage regulator for the VCC pin (allowing you to power it from the Arduino 5V pin), the TX/RX data pins are strictly 3.3V tolerant.
If you are using a 5V Arduino, you must use a voltage divider on the Arduino TX to Bluetooth RX line:
- 1kΩ resistor in series from Arduino TX to Bluetooth RX.
- 2kΩ resistor from Bluetooth RX to GND.
This drops the 5V HIGH signal down to a safe ~3.33V. The Bluetooth TX to Arduino RX line can be wired directly, as the Arduino will reliably read 3.3V as a logic HIGH.
Pull-Up Requirements
Unlike I2C, UART does not require pull-up resistors on the data lines. However, the EN (Enable) or KEY pin on legacy modules like the HC-05 requires a 10kΩ pull-up to VCC. If left floating, ambient RF noise can trigger the enable pin, causing the module to randomly reboot mid-transmission and drop your serial packets.
Bus Mechanics: Classic SPP vs. BLE vs. ESP-NOW
Choosing the right protocol dictates your maximum distance, throughput, and whether you need to manage MAC address pairing. Here is how the three primary Arduino Bluetooth Arduino protocols compare at the bus level.
| Protocol / Module | MCU Wires | UART / RF Speed | Addressing / Pairing | Max Distance (LOS) | Node Count |
|---|---|---|---|---|---|
| Classic SPP (HC-05) | TX, RX, VCC, GND, KEY | 115.2k baud / 3 Mbps | MAC Pairing (PIN code) | ~10 meters | 1-to-1 (Point-to-Point) |
| BLE 4.0 (HM-10) | TX, RX, VCC, GND | 9600 baud / 1 Mbps | UUID / Service Discovery | ~30 meters | 1-to-Many (Central/Peripheral) |
| ESP-NOW (ESP32) | Native (No external module) | N/A / 802.11b (72 Mbps) | Peer MAC Registration | ~100 meters | 1-to-20 (Broadcast/Multicast) |
Distance and Speed Reality Check: While Classic SPP boasts a 3 Mbps RF link, the UART bottleneck on a standard Arduino Uno caps practical throughput at 115,200 baud (roughly 11.5 KB/s). ESP-NOW bypasses the TCP/IP stack entirely, allowing the ESP32 to push 250-byte payloads at over 1,000 packets per second in optimal conditions (Espressif ESP-NOW Documentation).
Decision Tree: Picking Your Arduino Bluetooth Arduino Link
Do not default to the module you have in your junk bin. Use this decision matrix to select the correct hardware for your specific constraints.
| Condition / Constraint | Recommended Protocol | Hardware Pick |
|---|---|---|
| Must interface with an iOS/Android phone app | BLE (GATT Server) | HM-10 or ESP32 BLE |
| Strictly limited to 5V Arduino Uno, simple serial string parsing | Classic SPP | HC-05 (with voltage divider) |
| Arduino-to-Arduino telemetry, low latency, >10m range | ESP-NOW | ESP32-WROOM-32 DevKit V1 |
Minimal Working Exchange: ESP-NOW Master and Slave
Below is a complete, compilable implementation for an ESP-NOW link. Because ESP-NOW operates outside standard WiFi infrastructure, you do not need a router or SSID. The boards communicate directly via their MAC addresses.
Wiring Table
Since we are using the ESP32 natively, there is no external Bluetooth module to wire. If you need to read a 5V sensor on the ESP32, use a bidirectional logic level shifter (like the BSS138-based modules) rather than a simple resistor divider.
Master Node (Transmitter) Code
This node reads an analog pin, packages it into a struct, and fires it to the Slave's MAC address. Note: You must replace the slaveMacAddress array with your actual Slave's MAC address, which you can find by running the Slave code once and checking the Serial Monitor.
#include <esp_now.h>
#include <WiFi.h>
// REPLACE WITH YOUR SLAVE's MAC ADDRESS
uint8_t slaveMacAddress[] = {0x24, 0x0A, 0xC4, 0x9A, 0x58, 0x1C};
typedef struct struct_message {
int sensorValue;
float voltage;
} struct_message;
struct_message myData;
esp_now_peer_info_t peerInfo;
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
Serial.print("Last Packet Send Status: ");
Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Success" : "Fail");
}
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA); // Must be in STA mode for ESP-NOW
if (esp_now_init() != ESP_OK) {
Serial.println("Error initializing ESP-NOW");
return;
}
esp_now_register_send_cb(OnDataSent);
memcpy(peerInfo.peer_addr, slaveMacAddress, 6);
peerInfo.channel = 0;
peerInfo.encrypt = false;
if (esp_now_add_peer(&peerInfo) != ESP_OK){
Serial.println("Failed to add peer");
return;
}
}
void loop() {
myData.sensorValue = analogRead(34);
myData.voltage = (myData.sensorValue * 3.3) / 4095.0;
esp_err_t result = esp_now_send(slaveMacAddress, (uint8_t *) &myData, sizeof(myData));
delay(100); // 10Hz update rate
}
Slave Node (Receiver) Code
#include <esp_now.h>
#include <WiFi.h>
typedef struct struct_message {
int sensorValue;
float voltage;
} struct_message;
struct_message incomingData;
void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) {
memcpy(&incomingData, incomingData, sizeof(incomingData));
Serial.print("Raw ADC: ");
Serial.println(incomingData.sensorValue);
Serial.print("Voltage: ");
Serial.println(incomingData.voltage);
}
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
// Print MAC address so you can paste it into the Master code
Serial.print("My MAC Address: ");
Serial.println(WiFi.macAddress());
if (esp_now_init() != ESP_OK) {
Serial.println("Error initializing ESP-NOW");
return;
}
esp_now_register_recv_cb(OnDataRecv);
}
void loop() {
// Slave just listens; no code needed here
}
Debugging the Bus: Classic Failures and Sniffing
When your serial monitor outputs garbage or goes completely silent, the issue is almost always at the physical or configuration layer. Here is how to diagnose the three most common failures.
1. The Baud Rate Mismatch (HC-05 Specific)
Symptom: You send AT commands to the HC-05, but get no response, or you get endless '???' characters in the serial monitor.
Cause: The HC-05 has two distinct baud rates. In transparent data mode, it defaults to 9600 baud. In AT command mode (entered by holding the KEY button high while applying power), it defaults to 38400 baud.
Fix: Ensure your Arduino Serial.begin() or USB-to-TTL adapter is set to 38400 when configuring the module, and 9600 when passing live data.
2. The Floating EN Pin Reset Loop
Symptom: The Bluetooth module pairs successfully, but drops the connection every 30-60 seconds, or the onboard LED flashes rapidly at random intervals.
Cause: The EN (Enable) pin is floating. High-impedance inputs act as antennas, picking up 60Hz mains hum or RF noise, which momentarily pulls the pin low and resets the module's internal state machine.
Fix: Solder or breadboard a 10kΩ pull-up resistor between the EN pin and the 3.3V/VCC line. Alternatively, wire it directly to VCC if you never need to programmatically disable the module.
3. ESP-NOW MAC Address Clash or Channel Drift
Symptom: ESP-NOW code compiles and uploads, but the OnDataSent callback returns 'Fail' and the receiver sees nothing.
Cause: You hardcoded the wrong MAC address, or the Master and Slave are defaulting to different WiFi channels. ESP-NOW defaults to channel 1, but if the ESP32 previously connected to a router on channel 6, it may retain that in NVRAM.
Fix: Explicitly set peerInfo.channel = 0; in the Master code (which tells it to use the current default channel) and run WiFi.disconnect(true, true); in setup() to wipe saved NVRAM credentials before initializing ESP-NOW.
How to Sniff the Bus
Because Bluetooth and ESP-NOW operate over the air, you cannot easily 'tap' the RF link with a standard multimeter. To debug the physical layer:
- UART Sniffing: Clip a logic analyzer (like a $10 Saleae clone running PulseView) to the TX and RX pins between the MCU and the BT module. Set the decoder to UART, 3.3V, and your target baud rate. This will instantly reveal if the MCU is actually pushing bytes, or if the BT module is ignoring them.
- RF Sniffing (ESP-NOW): Flash a third ESP32 with a 'promiscuous mode' sniffer sketch (available via the Espressif GitHub repositories). This allows the third board to capture raw 802.11 frames in the air, proving whether the Master is actually transmitting, even if the Slave's MAC filter is rejecting the packets.
By treating wireless serial not as magic, but as a standard UART bus with an RF physical layer, you eliminate the guesswork. Secure your logic levels, terminate your enable pins, and match your baud rates, and your Arduino Bluetooth Arduino link will run as reliably as a hardwired connection.






