A Bluetooth module is not just a radio. Under the shield sits a real MCU with GPIO, I2C, SPI, UART, ADC, PWM, and timers. How you wire sensors and peripherals to that MCU – and how the driver software is structured – decides whether your product is reliable, low-power, and maintainable. This article is a practical field guide.
The buses, compared
| Bus | Wires | Speed | Topology | Addressing | Best for |
|---|---|---|---|---|---|
| GPIO | 1-2 | n/a | point | none | buttons, LEDs, enable |
| I2C | 2 (SDA/SCL) | 100 k-1 MHz | multi-drop | 7/10-bit addr | many sensors, config regs |
| SPI | 4 (MOSI/MISO/SCK/CS) | 1-32 MHz | point (CS per dev) | CS pin | high-rate, displays, flash |
| UART | 2 (TX/RX) | up to few Mbps | point | none | GNSS, debug, legacy |
The rule of thumb: use I2C for many slow sensors on few pins, SPI when throughput or latency matters, UART for talking to another chip’s serial console.
I2C details that bite
- Pull-ups: 4.7 kOhm is the classic starting value for 100 kHz with moderate bus capacitance. Compute R_min from max sink current and R_max from rise time: R_max <= t_r / (0.847 * C_bus). A long bus with 200 pF and 1 us rise needs R <= ~5.9 kOhm.
- Clock stretching: slaves may hold SCL low. Your master must support it or a slow sensor hangs the bus.
- Bus lock recovery: if a slave is reset mid-transfer it can freeze SCL low. Recovery toggles SCL (9 pulses) until the stuck device releases, then sends a STOP. Build this into the driver.
- Address conflicts: two sensors with the same fixed address need a mux (e.g. TCA9548A) or pin-strapped variant.
SPI details that bite
- CS timing: assert CS, respect setup before clock, hold after last bit. Many flashes need CS high between commands or they stay in a state.
- Mode (CPOL/CPHA): get it wrong and every byte is shifted. Document it per device.
- DMA: SPI at 8 MHz moves 1 MB/s; doing that with CPU-poked bytes burns both cores. Use DMA with a ring buffer for streaming (sensor fusion, audio).
- MISO contention: only the selected device may drive MISO; a mis-wired CS lets two devices fight.
UART details that bite
- Baud mismatch: 2% clock error is the usual tolerance; a module RC oscillator at 3% will corrupt at 115200. Use a crystal or tolerate lower baud.
- Flow control: enable RTS/CTS before trusting high throughput, or you drop bytes under load.
- Overrun: if the ISR cannot drain the FIFO fast enough, set a larger FIFO trigger or move to DMA.
Interrupt latency budget
From a pin change to your ISR running:
- SoC wake from low-power: 1-10 us (depends on retention state)
- ISR entry + prologue: < 1 us
- Critical sections: the killer. A long `disable_irq()` block delays every other interrupt. Keep critical sections to microseconds; defer work to a task.
On an nRF52840 at 64 MHz, a clean ISR fires in low single-digit microseconds. A 5 ms critical section in a log function can blow a 1 kHz sampling deadline.
Clock gating and peripheral power
Unused peripherals draw uA even when idle. A UART left enabled at 3 uA, an ADC reference at 10 uA, a spare timer at 2 uA – that is 15 uA of pure waste on a tag that should sip 5 uA. Disable peripherals you are not using, and gate the HF clock when the radio is idle.
Driver architecture
Prefer a layered, non-blocking design:
<h1>pseudo: non-blocking I2C read with DMA + timeout</h1>
def read_sensor(dev, reg, buf):
i2c.start_write(dev, [reg])
i2c.start_read_dma(dev, buf, done_cb)
return # do not block
def done_cb(buf):
queue.put(("sensor", buf)) # RTOS task processes later
- HAL layer: thin wrappers over registers, same API across chips.
- Driver layer: sensor-specific init, calibration, unit conversion.
- Service layer: an RTOS task that owns the bus, serializes access, and exposes a queue.
- Never block inside an interrupt; only signal.
This keeps the radio stack (which has hard real-time needs) from being starved by a slow sensor read.
Level shifting
A 1.8 V module talking to a 3.3 V sensor needs a level shifter on every line (or a 1.8 V-tolerant sensor). Forgets here cause intermittent corruption that disappears on a logic analyzer.
Common pitfalls table
| Symptom | Likely cause |
|---|---|
| Bus freezes randomly | I2C slave stuck low, no recovery |
| SPI reads shifted | Wrong CPOL/CPHA |
| UART drops bytes | No flow control / small FIFO |
| High idle current | Peripheral not gated off |
| Missed deadlines | Long critical section |
| Intermittent data | Missing level shifter |
Bottom line
A Bluetooth module succeeds or fails on the parts around the radio. Pick the bus by speed and pin count, compute I2C pull-ups, respect SPI CS timing, use DMA for throughput, keep critical sections tiny, and gate every unused peripheral. A clean driver layering – HAL, driver, service task, queue – keeps the radio stack real-time and your firmware debuggable.