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.