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.

**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.