Skip to content

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.