---
# System prepended metadata

title: How does STM32 read IMU data?
tags: [stm32]

---

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.