Bus reference for I2C, SPI, UART, CAN, and USB: addressing, pull-up sizing, bus recovery, SPI mode card, DMA cache rules, ring buffers, CAN bit timing and error states, USB descriptors, and STM32 HAL code for each bus. Use when writing or reviewing a bus driver or diagnosing a misbehaving bus.
Installs into .claude/skills of the current project.
Are you the author of Communication Buses?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/hermeticormus-communication-buses)
---
name: "communication-buses"
description: "Bus reference for I2C, SPI, UART, CAN, and USB: addressing, pull-up sizing, bus recovery, SPI mode card, DMA cache rules, ring buffers, CAN bit timing and error states, USB descriptors, and STM32 HAL code for each bus. Use when writing or reviewing a bus driver or diagnosing a misbehaving bus."
---
# Communication buses pattern library
Reference patterns for I2C, SPI, UART, CAN, and USB driver development. Use as a lookup when designing or reviewing drivers.
## I2C reference
### Address resolution
| Address space | Bits | Range |
|---|---|---|
| 7-bit | bits 7..1 | 0x08–0x77 (0x00-0x07 and 0x78-0x7F reserved) |
| 10-bit | bits 9..0 | 0x000–0x3FF |
Format on the wire (7-bit):
```
S | 7-bit addr | R/W | A | ... | P
```
The R/W bit is bit 0. Many HALs accept the address pre-shifted (`address << 1`); some accept it raw. Confirm against the HAL docs — common bug source.
### Pull-up sizing
For VCC = 3.3V:
| Speed | Recommended pull-up |
|---|---|
| Standard (100 kHz) | 4.7 kΩ |
| Fast (400 kHz) | 4.7 kΩ or 2.2 kΩ |
| Fast Plus (1 MHz) | 2.2 kΩ or 1 kΩ |
| High Speed (3.4 MHz) | 1 kΩ + external buffer |
Bus capacitance matters. Wire capacitance ~ 1 pF/cm. Total bus capacitance (wire + slaves + master) should be < 400 pF for standard, < 200 pF for fast modes.
### Clock stretching
Slave holds SCL low to gain processing time. Master must wait. STM32 hardware I2C handles this. Some software bit-bang implementations don't. If using bit-bang, sample SCL before assuming the master controls it.
### Bus recovery
When SDA is stuck low (slave hung mid-transfer):
```
1. Configure SDA + SCL as GPIO (out of AF)
2. Pulse SCL low → high, 9-16 times
3. Check if SDA released (high)
4. If yes: generate STOP (SCL high, SDA low → high)
5. Restore SDA + SCL to AF
6. Re-init the I2C peripheral
```
## SPI reference
### Mode card
| Mode | CPOL | CPHA | Idle clock | Data captured on |
|---|---|---|---|---|
| 0 | 0 | 0 | Low | Rising edge |
| 1 | 0 | 1 | Low | Falling edge |
| 2 | 1 | 0 | High | Falling edge |
| 3 | 1 | 1 | High | Rising edge |
Most sensors: mode 0 or 3. Always verify against datasheet — wrong mode silently corrupts every byte.
### CS line patterns
**Hardware NSS** (single slave):
- Peripheral toggles NSS automatically
- Faster setup/hold timing
- Limited to one slave
**GPIO CS** (multi-slave):
- Software writes GPIO before/after transfer
- More flexible
- Setup time: typically need a few microseconds CS-low before SCK starts
- Hold time: keep CS low until last bit clocked + a few extra cycles
### DMA buffer alignment
| MCU family | Requirement |
|---|---|
| Cortex-M0/M3/M4 (no cache) | No alignment requirement beyond AHB bus |
| Cortex-M7 with cache | Buffer in non-cacheable region OR explicit cache maintenance |
| ESP32 | DMA-capable memory region only (check linker) |
For Cortex-M7 cache maintenance:
```c
// Before DMA write (CPU → peripheral):
SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, len);
// Before DMA read (peripheral → CPU):
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, len);
```
### Daisy chain
For multiple SPI slaves on one CS:
```
Master MOSI → Slave 1 MOSI
Slave 1 MISO → Slave 2 MOSI
Slave 2 MISO → Master MISO
Common CS to all slaves
Common SCK to all slaves
```
Transfer N×M bytes for N slaves with M-byte registers. Each slave shifts out its previous-cycle data while shifting in new data. Useful for sensor arrays.
## UART reference
### Frame format
```
Start bit | Data bits (5-9) | Parity (optional) | Stop bits (1 / 1.5 / 2)
```
Common configurations:
- 8N1: 8 data, no parity, 1 stop — default for most modern UART
- 7E1: 7 data, even parity, 1 stop — legacy serial
- 8E2: 8 data, even parity, 2 stop — older industrial
### Baud rate selection
Common rates: 9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600, 1500000, 2000000, 3000000
Above 921600, accuracy of the MCU's UART clock matters. STM32 USART_BRR can hit any baud rate; calculate the actual rate and verify it's within ±2% of nominal.
### RX patterns
| Throughput | Pattern |
|---|---|
| < 1 kbps | Polled (rare, debug only) |
| 1 kbps – 100 kbps | Interrupt RX + ring buffer |
| 100 kbps – 1 Mbps | DMA RX + IDLE line detection |
| > 1 Mbps | DMA RX circular + IDLE line + flow control |
### Ring buffer pattern
```c
typedef struct {
uint8_t buf[256];
volatile uint16_t head;
uint16_t tail;
} ringbuf_t;
// ISR
void USART2_IRQHandler(void) {
if (USART2->SR & USART_SR_RXNE) {
uint8_t byte = USART2->DR;
uint16_t next = (rb.head + 1) & 0xFF;
if (next != rb.tail) { // Not full
rb.buf[rb.head] = byte;
rb.head = next;
}
// else: overflow — log it
}
}
// Task
ssize_t ringbuf_read(uint8_t *out, size_t maxlen) {
size_t count = 0;
while (rb.tail != rb.head && count < maxlen) {
out[count++] = rb.buf[rb.tail];
rb.tail = (rb.tail + 1) & 0xFF;
}
return count;
}
```
### Hardware flow control (RTS/CTS)
When the host can stall:
- MCU asserts RTS = "I have buffer space, send me data"
- MCU deasserts RTS = "stop, my buffer is full"
- MCU checks CTS = "host has buffer space, I can transmit"
For STM32 USART:
- `USART_CR3_CTSE` enables CTS
- `USART_CR3_RTSE` enables RTS
- Hardware handles both — no software signaling needed
## CAN reference
### Bit timing
```
Bit time = sync + propagation + phase1 + phase2
Sample point = (sync + propagation + phase1) / bit time
```
Recommended sample point: 75–87.5% of bit time. Higher sample point gives more margin for bus propagation delay but less margin for resync.
For 500 kbps on 42 MHz APB clock:
- 42 MHz / 0.5 MHz = 84 time quanta — too many, use prescaler
- Prescaler = 6 → 14 time quanta per bit
- 1 sync + 6 prop + 5 phase1 + 2 phase2 → sample point at (1+6+5)/14 = 86% ✓
- SJW = 2
### Filter banks
For STM32 bxCAN:
- 14 filter banks per CAN peripheral
- Each bank: 16-bit list mode (4 IDs) OR 16-bit mask mode (2 ID/mask pairs) OR 32-bit list (2 IDs) OR 32-bit mask (1 ID/mask)
Configure for the IDs you actually consume. Other IDs dropped at the controller (no CPU work).
### Error states
```
Active → Passive (TX error count > 127): sends recessive error frames only
Active → Bus-off (TX error count > 255): controller stops
Bus-off → Active (after 128 × 11 recessive bits): automatic recovery
```
Driver must detect bus-off and either wait for auto-recovery OR explicitly re-init. Auto-recovery is safer for production.
## USB reference
### Descriptor tree
```
Device descriptor
├─ Configuration descriptor (1+)
│ ├─ Interface descriptor (1+)
│ │ └─ Endpoint descriptor (0+)
│ └─ String descriptors (indexed)
└─ Vendor extensions (rare)
```
Total length in configuration descriptor must equal sum of all subordinate descriptor lengths. Off-by-one here causes enumeration to fail at the configuration-read step.
### Endpoint types
- **Control** (EP0) — required, bidirectional, 8/16/32/64 byte packets
- **Bulk** — high throughput, no latency guarantee, packet size 8-512
- **Interrupt** — low latency, periodic polling, packet size 8-1024
- **Isochronous** — guaranteed bandwidth, no error recovery, packet size up to 1024
### Common enumeration failures
| Symptom | Likely cause |
|---|---|
| Host doesn't see device at all | D+ pullup not raised; VBUS detection wrong |
| Host sees device but fails enumeration | Descriptor length mismatch; EP0 size wrong |
| Host enumerates but wrong driver loads | VID/PID conflict with existing driver |
| Enumerates fine but immediate disconnect | Power request > bus can provide |
| Sometimes works, sometimes not | VBUS noise; pullup raised before VBUS stable |
## Common mistakes catalog
### "I2C bus is stuck"
A slave is holding SDA low. Use bus recovery (pulse SCL 9-16 times, then STOP). Common causes:
- Power glitch on slave during transfer
- Master reset mid-transfer
- Slave with buggy firmware
### "SPI reads all 0xFFs"
Slave isn't driving MISO. Check:
- Slave VCC (at the chip, not the regulator)
- Slave reset state
- CS line actually going low during transfer
- Correct SPI mode
- Slave power-up time elapsed
### "UART loses bytes under load"
RX overrun. Either:
- Increase RX FIFO threshold to give software more time
- Switch from interrupt to DMA RX
- Add hardware flow control
### "CAN bus shows messages but receiver doesn't see them"
Filter bank misconfigured. Check the filter bank's ID + mask vs. the actual message ID being broadcast.
### "USB device works on one host, fails on another"
Descriptor issue. Some hosts are stricter than others. Run [USB-IF compliance tool](https://www.usb.org/usbet) on the descriptors.
## Code patterns (STM32 HAL)
Production communication bus patterns for embedded C. STM32 HAL and LL examples.
### I2C Register Read/Write for MEMS Sensors
Generic register-addressed I2C sensor driver (BME280, MPU-6050, etc.):
```c
#define I2C_TIMEOUT_MS 5U
HAL_StatusTypeDef sensor_write_reg(I2C_HandleTypeDef *hi2c,
uint8_t dev_addr, uint8_t reg, uint8_t val)
{
uint8_t buf[2] = { reg, val };
return HAL_I2C_Master_Transmit(hi2c, dev_addr << 1, buf, 2, I2C_TIMEOUT_MS);
}
HAL_StatusTypeDef sensor_read_reg(I2C_HandleTypeDef *hi2c,
uint8_t dev_addr, uint8_t reg,
uint8_t *out, uint16_t len)
{
HAL_StatusTypeDef s;
s = HAL_I2C_Master_Transmit(hi2c, dev_addr << 1, ®, 1, I2C_TIMEOUT_MS);
if (s != HAL_OK) { return s; }
return HAL_I2C_Master_Receive(hi2c, (dev_addr << 1) | 1,
out, len, I2C_TIMEOUT_MS);
}
/* I2C bus recovery: toggle SCL 9 times to release stuck SDA */
void i2c_bus_recover(GPIO_TypeDef *scl_port, uint16_t scl_pin,
GPIO_TypeDef *sda_port, uint16_t sda_pin)
{
for (int i = 0; i < 9; i++) {
HAL_GPIO_WritePin(scl_port, scl_pin, GPIO_PIN_SET);
HAL_Delay(1);
HAL_GPIO_WritePin(scl_port, scl_pin, GPIO_PIN_RESET);
HAL_Delay(1);
}
/* Generate STOP: SDA low→high while SCL high */
HAL_GPIO_WritePin(sda_port, sda_pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(scl_port, scl_pin, GPIO_PIN_SET);
HAL_Delay(1);
HAL_GPIO_WritePin(sda_port, sda_pin, GPIO_PIN_SET);
}
```
### SPI DMA Transfer with Semaphore
Non-blocking SPI using DMA and FreeRTOS binary semaphore for completion notification:
```c
static SemaphoreHandle_t s_spi_done;
void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi)
{
if (hspi == &hspi1) {
BaseType_t woken = pdFALSE;
xSemaphoreGiveFromISR(s_spi_done, &woken);
portYIELD_FROM_ISR(woken);
}
}
void spi_dma_init(void)
{
s_spi_done = xSemaphoreCreateBinary();
}
bool spi_transfer(const uint8_t *tx, uint8_t *rx, uint16_t len)
{
spi_cs_assert();
HAL_SPI_TransmitReceive_DMA(&hspi1, (uint8_t *)tx, rx, len);
/* Block task until DMA complete (max 10ms) */
bool ok = xSemaphoreTake(s_spi_done, pdMS_TO_TICKS(10)) == pdTRUE;
spi_cs_deassert();
return ok;
}
```
### UART DMA Circular Buffer with IDLE Detection
Most reliable pattern for variable-length UART frames at high baud:
```c
#define DMA_RX_BUF 256U
static uint8_t s_dma_rx[DMA_RX_BUF];
static uint32_t s_rx_wr = 0U; /* Written by IDLE ISR */
static uint32_t s_rx_rd = 0U; /* Read by application */
void uart_dma_init(UART_HandleTypeDef *hu)
{
/* Start DMA receive in circular mode — never needs restart */
HAL_UARTEx_ReceiveToIdle_DMA(hu, s_dma_rx, DMA_RX_BUF);
__HAL_DMA_DISABLE_IT(hu->hdmarx, DMA_IT_HT); /* Disable half-transfer */
}
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *hu, uint16_t size)
{
/* size = write position in s_dma_rx (bytes from buffer start), not a */
/* count since the last callback. size == DMA_RX_BUF means it wrapped. */
s_rx_wr = size % DMA_RX_BUF;
}
uint16_t uart_available(void) {
return (s_rx_wr - s_rx_rd + DMA_RX_BUF) % DMA_RX_BUF;
}
uint8_t uart_read_byte(void) {
uint8_t c = s_dma_rx[s_rx_rd];
s_rx_rd = (s_rx_rd + 1U) % DMA_RX_BUF;
return c;
}
```
### CAN Transmit and Receive
```c
/* Transmit CAN frame */
CAN_TxHeaderTypeDef tx_hdr = {
.StdId = 0x123U,
.IDE = CAN_ID_STD,
.RTR = CAN_RTR_DATA,
.DLC = 8U,
.TransmitGlobalTime = DISABLE,
};
void can_send(uint8_t *data)
{
uint32_t mailbox;
if (HAL_CAN_GetTxMailboxesFreeLevel(&hcan1) == 0) { return; }
HAL_CAN_AddTxMessage(&hcan1, &tx_hdr, data, &mailbox);
}
/* Receive: called from CAN RX FIFO0 interrupt */
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
CAN_RxHeaderTypeDef hdr;
uint8_t buf[8];
if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &hdr, buf) == HAL_OK) {
can_dispatch(hdr.StdId, buf, hdr.DLC);
}
}
```
### SPI CPOL/CPHA Mode Selection Reference
```c
/* STM32 HAL SPI mode to CPOL/CPHA mapping */
/* Mode 0: CPOL=0, CPHA=0 — idle low, sample rising */
hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;
/* Mode 3: CPOL=1, CPHA=1 — idle high, sample rising */
hspi1.Init.CLKPolarity = SPI_POLARITY_HIGH;
hspi1.Init.CLKPhase = SPI_PHASE_2EDGE;
/* Always verify against the sensor's timing diagram.
ICM-42688 (IMU): Mode 0 or Mode 3
W25Q128 (Flash): Mode 0 or Mode 3
MCP3204 (ADC): Mode 0 or Mode 3 */
```
## Anti-patterns
- **I2C HAL_OK return does not mean data is correct**: check sensor WHO_AM_I register before trusting any data.
- **SPI without explicit CS control**: HAL NSS software mode has glitches during multi-byte transfers. Manage CS manually.
- **CAN without filter configured**: all frames enter FIFO, FIFO overflows, data is lost.
- **UART polling in production**: blocks CPU on every byte. Use DMA+IDLE for any baud rate above 9600.
- **I2C without bus recovery**: a slave stuck holding SDA low after power glitch makes the bus permanently busy without recovery.
## References
- UM1905 (ST): Description of STM32F4 HAL and Low-Layer drivers
- I2C specification: NXP UM10204
- CAN spec: Bosch CAN 2.0 specification
- USB CDC class specification: USB.org CDC120.pdf
## Cross-references
- **debug-trace** plugin: when you need to see bus traffic without a logic analyzer (ITM-based UART sniffing, etc.)
- **memory-management** plugin: DMA buffer placement in cacheable vs. non-cacheable regions
- **iot-protocols** plugin: when the bus carries IoT protocol payloads (MQTT over UART, etc.)
- **rtos-patterns** plugin: when driver completion needs to wake a task (ISR-to-task hand-off)