Here’s a practical, MCU-level playbook for reading IMU data with an [STM32](https://www.ampheo.com/search/STM32). It covers I²C and SPI wiring, initialization, polling vs. interrupt/DMA reads, unit conversion, and the common pitfalls—plus a concrete I²C example ([MPU-6050](https://www.ampheo.com/product/mpu-6050-26900976)) you can paste and run. ![34-1](https://hackmd.io/_uploads/ryOXIbitle.png) **1) Pick a bus & wire it** **I²C (simpler, fine ≤1 kHz ODR)** * SDA/SCL → IMU, both with pull-ups (typically 4.7–10 kΩ to 3.3 V). * Tie IMU ADDR/SA0 pin to select address (e.g., 0x68/0x69 for MPU-6050). * Optionally route INT/DRDY pin to a free EXTI line for data-ready interrupts. **SPI (higher throughput/less jitter)** * SCK/MOSI/MISO + CS per device. * Many IMUs need MSB=1 for read and an auto-increment bit for multi-byte bursts (datasheet-specific). * Route INT/DRDY to EXTI. Voltage: most IMUs are 3.3 V tolerant. If your board is 5 V, use level shifting. **2) CubeMX setup (quick)** * Enable I2C1 (Fast-mode 400 kHz) or SPI1 (e.g., 8–10 MHz). * Enable the GPIO for INT/DRDY as external interrupt (rising edge). * Generate code (HAL). Confirm the bus handles __weak MSP init functions. **3) Firmware flow (applies to [LSM6DSx](https://www.ampheo.com/search/LSM6DS), [ICM-20xxx](https://www.ampheo.com/search/ICM-20), [BMI160](https://www.ampheo.com/product/bmi160-26900559), MPU-6050, etc.)** 1. Probe: read WHO_AM_I; compare expected value. 2. Reset: write device reset if available; wait for ready bit. 3. Configure: * ODR (sample rate), full-scale (±2/4/8/16 g, ±250/500/1000/2000 dps), filters/FIFO. * Enable data-ready (DRDY) interrupt (latches a pin at each new sample). 4. Read data: * Polling: periodically burst-read accel/gyro registers (6 or 12 bytes). * Interrupt: on DRDY ISR, read a burst. * DMA: start a multi-byte read into a struct; handle in DMA complete callback. 5. Convert raw 16-bit two’s complement → g/°/s using the sensor’s sensitivity constants. 6. (Optional) Calibrate & fuse: * Remove static biases (offsets), low-pass filter noise. * Fuse accel+gyro (+mag if present) with Madgwick/Mahony for orientation. **4) Minimal I²C example (MPU-6050 @ 0x68)** Registers used: WHO_AM_I=0x75 (0x68), PWR_MGMT_1=0x6B, accel start at 0x3B, gyro start at 0x43. Paste into your Cube project (HAL): ``` #include "main.h" extern I2C_HandleTypeDef hi2c1; #define MPU_ADDR (0x68 << 1) // HAL uses 8-bit address #define WHO_AM_I 0x75 #define PWR_MGMT_1 0x6B #define ACCEL_XOUT_H 0x3B static HAL_StatusTypeDef mpu_write(uint8_t reg, uint8_t val) { return HAL_I2C_Mem_Write(&hi2c1, MPU_ADDR, reg, I2C_MEMADD_SIZE_8BIT, &val, 1, 100); } static HAL_StatusTypeDef mpu_read(uint8_t reg, uint8_t *buf, uint16_t len) { return HAL_I2C_Mem_Read(&hi2c1, MPU_ADDR, reg, I2C_MEMADD_SIZE_8BIT, buf, len, 100); } static int16_t be16(const uint8_t *p) { return (int16_t)((p[0] << 8) | p[1]); } int mpu_init(void) { uint8_t id = 0; if (mpu_read(WHO_AM_I, &id, 1) != HAL_OK || id != 0x68) return -1; // wake up; select PLL clock if (mpu_write(PWR_MGMT_1, 0x01) != HAL_OK) return -2; // (Optional) set full-scale ranges, filters, sample rate here… return 0; } typedef struct { float ax, ay, az, gx, gy, gz; } imu_t; int mpu_read_imu(imu_t *o) { uint8_t buf[14]; if (mpu_read(ACCEL_XOUT_H, buf, sizeof(buf)) != HAL_OK) return -1; int16_t ax = be16(&buf[0]); int16_t ay = be16(&buf[2]); int16_t az = be16(&buf[4]); /* int16_t tmp = be16(&buf[6]); */ // temperature if you want it int16_t gx = be16(&buf[8]); int16_t gy = be16(&buf[10]); int16_t gz = be16(&buf[12]); // Default sensitivities: ±2g → 16384 LSB/g, ±250 dps → 131 LSB/(°/s) o->ax = ax / 16384.0f; o->ay = ay / 16384.0f; o->az = az / 16384.0f; o->gx = gx / 131.0f; o->gy = gy / 131.0f; o->gz = gz / 131.0f; return 0; } ``` **Loop usage** ``` imu_t s; if (mpu_init() == 0) { while (1) { mpu_read_imu(&s); // use s.ax..s.gz HAL_Delay(5); // ~200 Hz. For precise timing, use a timer or DRDY interrupt. } } ``` Swap the register map/conversion constants for your device (e.g., [LSM6DS3](https://www.ampheo.com/search/LSM6DS3)/[LSM6DSL](https://www.ampheo.com/search/LSM6DSL), [ICM-20948](https://www.ampheo.com/product/icm-20948-26900336), BMI160). The pattern stays identical. **5) Using DRDY interrupt (smoothed timing)** * Connect IMU INT/DRDY → [STM32](https://www.ampheoelec.de/search/STM32) EXTI pin. * In CubeMX: enable EXTI line (rising edge), generate callback HAL_GPIO_EXTI_Callback(pin). * In the callback, set a flag. In while(1), if flag set → burst read 12 bytes (acc+gyro). This reduces jitter and keeps sampling aligned with the IMU’s internal ODR. **6) SPI + DMA burst template (multi-byte read)** (For IMUs where read = MSB=1, auto-inc enabled) ``` // Example for LSM6DSx-like parts uint8_t tx[1] = { (0x28 /* OUTX_L_G */) | 0x80 }; // 0x80 = read + auto-increment (chip-specific) uint8_t rx[12]; HAL_GPIO_WritePin(IMU_CS_GPIO_Port, IMU_CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, tx, 1, 10); HAL_SPI_Receive_DMA(&hspi1, rx, sizeof(rx)); // on complete: parse rx[0..11] ``` In HAL_SPI_RxCpltCallback, convert to engineering units. Remember to raise CS when the transfer completes. **7) Converting to physical units (generic)** * Treat accel/gyro outputs as signed 16-bit two’s complement. * Scale by the selected FSR: * Example LSM6DS3 accel sensitivities: ±2 g → 0.061 mg/LSB, ±4 g → 0.122 mg/LSB, ±8 g → 0.244 mg/LSB, ±16 g → 0.488 mg/LSB. * Gyro: ±245/500/1000/2000 dps with corresponding mdps/LSB. * Apply a simple offset calibration: average N samples at rest; subtract biases. **8) FIFO (optional, recommended)** Enable IMU FIFO to: * Burst-read many samples per transaction (less CPU/bus overhead). * Avoid sample loss at high ODR. * Timestamp alignment (some IMUs provide internal time tags). **9) Common pitfalls (checklist)** * Wrong address format (HAL wants 8-bit address; shift 7-bit left by 1). * No pull-ups or too-weak pull-ups on I²C → bus hangs. * Not clearing INT properly (some IMUs require a status read to de-latch DRDY). * Endian/auto-inc flags wrong in SPI reads. * Mixing units after changing full-scale mid-run. * Jitter from HAL_Delay—use timers/DRDY for constant dt in fusion [filters](https://www.onzuu.com/category/filters). **10) Next steps (fusion & calibration)** * Use Madgwick or Mahony filters (open-source) for quaternion/attitude from accel+gyro (+mag). * Do a 6-face accel calibration to solve scale/misalignment if you need <1% gravity accuracy. * For vibration environments, implement a low-pass or notch on gyro, and limit accel weight in the filter.