Firmware Overview¶
Source layout¶
firmware/
├── shared/
│ ├── packet_protocol.h # Packet sizes, offsets, CRC32 (C, shared by both MCUs)
│ └── packet_protocol.c # CRC32 implementation
├── stm32f405/
│ ├── platformio.ini # STM32duino, 168 MHz, Arduino framework
│ └── src/
│ ├── main.c # setup(), loop() — orchestrator
│ ├── bno055_driver.h # Self-contained Bosch-style driver with bus_ctx
│ ├── bno055_driver.c # Register read/write, data parsing
│ ├── imu_task.h/c # 4x BNO055 read cycle, fresh-cleared-each-cycle
│ ├── analog_task.h/c # 6ch ADC, 4x oversampled
│ ├── kb2040_uart.h/c # USART6 state machine, 0xBB 0x55 sync, CRC check
│ ├── packet_builder.h/c # 214-byte frame assembly
│ └── usb_tx.h/c # CDC TX with host-draining guard
└── kb2040/
├── platformio.ini # Arduino-Pico, 133 MHz
└── src/
└── main.cpp # 2x BNO055 (Adafruit lib), 70B frame, byte-wise put_f32
STM32F405 firmware¶
Module responsibilities¶
| Module | Role |
|---|---|
main.c |
100 Hz main loop, 10 ms timing guard |
imu_task.c |
Reads 4 BNO055 via bno055_driver. Each IMU carries its bus_ctx (a TwoWire*) so shared addresses on different buses route correctly. A failed read clears fresh — the packet carries zeros, never stale data. |
analog_task.c |
Reads A0–A5 (2 EMG + 4 FSR), 4× oversampled, normalized to 0.0–1.0. |
kb2040_uart.c |
USART6 state machine: sync on 0xBB 0x55 magic, read 70 bytes, validate CRC32. |
packet_builder.c |
Assembles 214-byte frame. BMP390 fields = 0.0 until sensor arrives. |
usb_tx.c |
Checks Serial.availableForWrite() before writing; drops frame if host is not draining (prevents blocking the 100 Hz loop). |
Critical fix: I2C bus routing¶
The v1 code probed all I²C buses for the BNO055 address, silently
misrouting IMUs #2/#3. v2 gives each BNO055 device a bus_ctx pointer:
// bno055_driver.h
typedef struct {
uint8_t addr;
void *bus_ctx; // TwoWire* — per-device, not global
...
} bno055_dev_t;
imu_task.c sets the correct bus_ctx per IMU, and the transport callbacks
cast ctx back to TwoWire* for the read/write. This guarantees each IMU
talks on its assigned bus regardless of address collisions.
Fresh-cleared-each-cycle¶
// imu_task.c — each read cycle
dev->fresh = 0; // start of read
dev->callback(dev); // populate data, set fresh = 1 on success
// if read fails, fresh stays 0 → packet gets zeros, not stale data
KB2040 firmware¶
Single-file (main.cpp). Uses the Adafruit BNO055 library.
Critical fix: byte-wise put_f32¶
The RP2040 (Cortex-M0+) does NOT support unaligned word stores. The v1 code
used memcpy(float_val, &pkt[offset]) at arbitrary offsets (10, 38, etc.)
which caused hard faults on the M0+. v2 uses byte-by-byte copy:
static inline void put_f32(uint8_t *b, uint16_t o, float v) {
memcpy(&b[o], &v, 4); // SAFE on M0+: byte-wise internally
}
This is safe because memcpy on unaligned pointers on M0+ compiles to byte-by-byte loads/stores (GCC knows the architecture).
Shared code¶
firmware/shared/packet_protocol.h and .c define:
- Packet sizes: PKT_KB2040_SIZE = 70, PKT_STM32_SIZE = 214
- Byte offsets for all fields
- crc32_calc() and crc32_check() (Ethernet polynomial)
Both MCU projects include this via a symlink or -I path in platformio.ini.