To add wireless connectivity to your bench, you have two primary paths for a microcontroller with Bluetooth: use a native System-on-Chip (SoC) like the ESP32-WROOM-32 or nRF52840, or wire an external UART module (HC-05 for Classic, HM-10 for BLE) to a legacy host like an Arduino Uno. For new designs in 2026, the ESP32 is the undisputed winner due to its integrated RF stack, 3.3V logic, and zero external wiring requirements. However, if you are retrofitting an existing 5V Arduino project, a UART module is your bridge. Below is the exact physical layer, protocol mechanics, and code you need to get bytes moving over the air.

Physical Layer: Wiring UART Modules vs. Native SoCs

The most common point of failure when integrating Bluetooth is ignoring the physical bus limitations and logic-level mismatches. External modules communicate with your host MCU via a physical wired bus (usually UART or SPI), while the Bluetooth protocol itself acts as a logical wireless bus.

Bus Mechanics & Interface Comparison

Interface / Protocol Physical Wires Bus Speed Addressing Max Distance
UART (HC-05 / HM-10) 4 (VCC, GND, TX, RX) 9600 - 115200 bps None (Point-to-Point) < 1m (wire trace)
SPI (High-Speed Combos) 6 (MOSI, MISO, SCK, CS, IRQ, GND) Up to 80 MHz Chip Select (CS) < 0.5m (wire trace)
Native SoC (ESP32 / nRF52) 0 (Internal RF Transceiver) N/A (Internal Bus) MAC / BLE UUID N/A (Internal)
Bluetooth Classic (SPP) Wireless (RF 2.4 GHz) ~1-3 Mbps actual MAC Address / PIN 10m (Class 2 RF)
Bluetooth Low Energy (GATT) Wireless (RF 2.4 GHz) 1-2 Mbps (BLE 5.0) Service/Characteristic UUID 10-100m (RF + Antenna)

Physical Wiring and Pull-Up Requirements

If you are wiring an HM-10 or HC-05 to a 5V Arduino Uno, you must use a logic level shifter or voltage divider on the RX line. The module's VCC can accept 5V (via an onboard regulator), but its TX/RX pins are strictly 3.3V. Feeding 5V from the Arduino's TX into the module's RX will fry the module's silicon within seconds.

  • Arduino TX to Module RX: Use a voltage divider. Connect a 1kΩ resistor in series with the Arduino TX, and a 2kΩ resistor from the module RX to GND. This drops the 5V logic down to a safe ~3.3V.
  • Module TX to Arduino RX: Direct connection. The 3.3V output from the module is above the Arduino Uno's 2.0V high-level threshold (V_IH), so it reads as a logic HIGH reliably.
  • Pull-ups: Standard UART does not require I2C-style pull-up resistors. However, if using the module's STATE or KEY pins for AT-command mode toggling, a 10kΩ pull-up to 3.3V is recommended to prevent floating logic.

Protocol Mechanics: Classic SPP vs. BLE GATT

Choosing the right protocol depends entirely on your target mobile OS and power budget. According to the Bluetooth SIG Core Specification, the stack is divided into distinct profiles.

Bluetooth Classic (SPP - Serial Port Profile): Used by the HC-05. It emulates a physical RS-232 serial cable. It pairs via a PIN code and maintains a continuous, high-bandwidth connection. Verdict: Use only for legacy Android or Windows desktop apps. Apple's iOS strictly blocks SPP connections to non-MFi (Made for iPhone) certified hardware, meaning an HC-05 will not work with an iPhone.

Bluetooth Low Energy (BLE - GATT): Used by the HM-10 and native ESP32. It relies on a Central/Peripheral architecture. The peripheral advertises its presence; the central scans and connects. Data is exchanged via GATT (Generic Attribute Profile) using hierarchical UUIDs (Services and Characteristics). Verdict: Mandatory for iOS compatibility, battery-operated sensors, and modern IoT deployments. The trade-off is higher latency and lower raw throughput compared to Classic.

Minimal Working Exchange: ESP32 BLE UART Passthrough

Rather than fighting with external HM-10 AT commands, the most reliable approach in 2026 is using an ESP32-WROOM-32 as a native BLE peripheral. The code below creates a custom BLE UART service, allowing any smartphone serial terminal (like nRF Connect or Serial Bluetooth Terminal) to send and receive strings.

Wiring: No external Bluetooth wiring required. The ESP32 handles RF internally. Connect your sensors to standard GPIO pins.

#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>

// Custom UUIDs for our virtual UART service
#define SERVICE_UUID        "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
#define CHARACTERISTIC_UUID_RX "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
#define CHARACTERISTIC_UUID_TX "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"

BLECharacteristic *pTxCharacteristic;
bool deviceConnected = false;

class MyServerCallbacks: public BLEServerCallbacks {
  void onConnect(BLEServer* pServer) {
    deviceConnected = true;
    Serial.println("BLE Central Connected");
  }
  void onDisconnect(BLEServer* pServer) {
    deviceConnected = false;
    Serial.println("BLE Central Disconnected");
    pServer->startAdvertising(); // Restart advertising on disconnect
  }
};

class MyCallbacks: public BLECharacteristicCallbacks {
  void onWrite(BLECharacteristic *pCharacteristic) {
    std::string rxValue = pCharacteristic->getValue();
    if (rxValue.length() > 0) {
      Serial.print("Received via BLE: ");
      Serial.println(rxValue.c_str());
      // Echo back to the central device
      pTxCharacteristic->setValue(rxValue);
      pTxCharacteristic->notify();
    }
  }
};

void setup() {
  Serial.begin(115200);
  BLEDevice::init("ESP32_BLE_UART");
  BLEServer *pServer = BLEDevice::createServer();
  pServer->setCallbacks(new MyServerCallbacks());

  BLEService *pService = pServer->createService(SERVICE_UUID);
  
  pTxCharacteristic = pService->createCharacteristic(
    CHARACTERISTIC_UUID_TX, BLECharacteristic::PROPERTY_NOTIFY);
  pTxCharacteristic->addDescriptor(new BLE2902());

  BLECharacteristic *pRxCharacteristic = pService->createCharacteristic(
    CHARACTERISTIC_UUID_RX, BLECharacteristic::PROPERTY_WRITE);
  pRxCharacteristic->setCallbacks(new MyCallbacks());

  pService->start();
  pServer->getAdvertising()->start();
  Serial.println("Waiting for BLE Central connection...");
}

void loop() {
  if (deviceConnected) {
    // Example: Send hardware serial monitor input over BLE
    if (Serial.available()) {
      String msg = Serial.readStringUntil('\n');
      pTxCharacteristic->setValue(msg.c_str());
      pTxCharacteristic->notify();
    }
  }
  delay(10); // Prevent watchdog resets
}

Debugging the Bus: Sniffing and Classic Failures

When bytes stop flowing, the issue is almost always at the physical layer or the advertisement layer. Here is how to isolate the fault.

The Classic Failures

  1. Baud Rate Mismatch (UART Modules): The HC-05 defaults to 9600 baud in data mode, but requires 38400 baud to enter AT command mode. If your serial monitor is set to 115200, you will see garbage characters or nothing at all. Always verify the module's firmware default.
  2. Missing Voltage Divider: If the module's RX pin is dead but TX works, you likely fed 5V into the 3.3V RX pin, destroying the internal ESD protection diode. Replace the module and add the 1k/2k resistor divider.
  3. Address/Name Clash (BLE): If you flash five ESP32s with the exact same device name ("ESP32_BLE_UART") and UUIDs, the central device's BLE stack will cache the MAC address of the first one it connects to. Subsequent connections will fail silently. Always append a unique identifier (like the last 4 digits of the MAC address) to the BLE device name in production.

How to Sniff and Debug

Do not guess; measure. For the physical UART bus, clip a logic analyzer or oscilloscope to the TX and RX lines. Verify that the idle state is HIGH (3.3V) and that the start bit pulls LOW. If the physical bus is clean, the issue is in the RF stack.

To sniff the logical BLE bus, download the nRF Connect for Mobile app. It acts as a BLE Central device. Use the 'Scanner' tab to view raw advertising payloads, verify your ESP32's MAC address, and inspect the exact RSSI (signal strength). If nRF Connect sees the advertisement but fails to connect, your GATT service UUIDs in the code do not match what your mobile app is querying.

Frequently Asked Questions

Which microcontroller with Bluetooth is best for low-power coin-cell sensors?

If your project must run for months on a CR2032 coin cell, the ESP32 is the wrong choice; its WiFi/BLE stack and dual-core architecture draw too much quiescent current (typically 10-20mA even in light sleep). For ultra-low-power BLE, use a dedicated SoC like the Nordic nRF52840 or the nRF52810. These chips feature dedicated hardware accelerators for the BLE stack and can achieve average currents in the microamp (µA) range during advertising intervals, extending coin-cell life to over a year.

Why won't my iPhone connect to my Arduino microcontroller with Bluetooth Classic?

Apple restricts Bluetooth Classic (SPP profile) access on iOS to hardware that passes their proprietary MFi (Made for iPhone) certification program, which requires specialized auth chips and licensing. Standard modules like the HC-05 or HC-06 will never appear in iOS Bluetooth settings. To connect an Arduino to an iPhone, you must swap the Classic module for a BLE module (like the HM-10) or upgrade to an ESP32 running a BLE GATT service, as iOS fully supports standard BLE profiles.

How do I debug a microcontroller with Bluetooth when the serial monitor is silent?

If the ESP32 serial monitor is completely blank upon boot, the BLE stack initialization may be causing a brownout or memory allocation failure. The ESP32 BLE stack requires significant RAM. If you are using a board without PSRAM, ensure you have not allocated large buffers elsewhere in your code. Additionally, check your USB cable; cheap charge-only cables lack the D+/D- data lines required for serial communication, making it look like a code failure when it is actually a physical layer fault.

What is the maximum range for a microcontroller with Bluetooth 5.0?

While Bluetooth 5.0 introduces Long Range (LE Coded PHY) capable of exceeding 1km in open air with specialized high-gain antennas, standard microcontroller dev boards (like the ESP32-DevKitC with a PCB trace antenna) typically max out at 10 to 30 meters indoors. Walls, human bodies (which absorb 2.4GHz RF), and USB 3.0 cable interference severely degrade the link budget. For reliable indoor IoT deployments, plan for a maximum range of 15 meters per node, or use ESP-NOW / BLE Mesh to hop the signal.