
陀螺仪数据能不能稳定、及时地拿到往往是 IMU 项目里最容易翻车的地方。最近我在 STM32C5 上调试 LSM6DSOW把传感器数据就绪DRDY中断接到 MCU 的外部中断上用中断方式读取陀螺仪数据。和简单的轮询相比这套方式不需要在主循环里反复查询状态寄存器CPU 占用低很多采样节拍也更准不会因为主循环被其他任务拖住而漏掉数据。这篇文章会把从硬件连接到寄存器配置、从 EXTI 设置到中断服务函数的完整过程记录下来过程中踩过的坑也一并说清楚。如果你正在做运动检测、姿态解算或者想优化 IMU 读取方式可以照着抄。1. 为什么采用中断方式读取LSM6DSOW陀螺仪数据1.1 轮询方式的痛点到底在哪很多人第一次点陀螺仪都会在主循环里写一个“定时读状态寄存器”的逻辑比如这样每 5ms 去读一下 STATUS_REG发现陀螺仪数据就绪位变成 1就把 OUTX_L_G、OUTX_H_G 这几个寄存器读回来。这套逻辑在最简单的 Demo 里跑得通但一旦主循环里有屏幕刷新、电机控制、通信协议处理事情就变了。某次循环多跑了 2ms下一次去读状态寄存器时数据就绪脉冲可能已经被传感器发完并清掉了。结果就是丢数姿态解算出来的角度就会出现跳变。轮询看起来“代码简单”但本质上是用 CPU 的时间和主循环的确定性去赌传感器不丢数据赌赢了没事赌输了很难查。中断方式解决的是“谁先主动”的问题。LSM6DSOW 一旦完成一次陀螺仪采样会通过 INT1 引脚主动告诉 MCU有新数据了你赶紧来拿。MCU 的 EXTI 模块收到这个脉冲后会触发中断服务函数。这样就算主循环很忙数据就绪事件也不会被遗漏只需要在中断里置一个标志等主循环空闲时再去读。1.2 LSM6DSOW的DRDY中断机制LSM6DSOW 内部其实有两套“中断系统”很多人容易搞混。第一套是传感器内部事件中断比如陀螺仪数据就绪、加速度计数据就绪、FIFO 水位达到阈值、唤醒事件检测等。这些事件会映射到芯片的两个物理引脚 INT1 和 INT2由寄存器 INT1_CTRL、INT2_CTRL 控制。第二套才是 MCU 侧的外部中断。STM32C5 的某个 GPIO 引脚配成 EXTI 模式后收到 INT1 引脚上的脉冲就会跳进自己家的中断服务函数。我这里做的事情就是把两颗芯片连接起来LSM6DSOW 的陀螺仪数据就绪事件映射到 INT1 引脚INT1 引脚再接到 STM32C5 的一个可外部中断的 GPIO 上。这样传感器每次采样完MCU 就会被“踢一脚”在中断回调里记录一下“该取数了”。LSM6DSOW 的数据就绪位在 STATUS_REG 的 bit1也就是 GDAGyro Data Available。当这个位为 1 时说明陀螺仪新的 X/Y/Z 三轴数据已经写入输出寄存器。把这个事件映射到 INT1 引脚需要把 INT1_CTRL 寄存器的 INT1_DRDY_G 位置 1对应写入值就是 0x02。1.3 什么时候不建议用中断虽然中断比轮询好但不是所有场景都必须上中断。如果你的 IMU 采样率很低比如每秒 10 次而且主循环空得很轮询完全够用。反过来如果你把陀螺仪 ODR 开到 3.3kHz 甚至 6.6kHz每个周期只有几百微秒中断函数本身在频繁触发CPU 大量时间都在进中断和出中断这时候单纯用 DRDY 中断反而拖累系统。我自己的经验是低频率采样用轮询中频率采样式 100Hz 到 1kHz 用 DRDY 中断高频率采样优先考虑 FIFO让传感器先把数据存起来MCU 每隔一段时间批量取一次。后面会有 FIFO 的方案说明。2. 硬件连接与CubeMX工程配置2.1 引脚规划与硬件连接表STM32C5 的 I2C 外设比较丰富我这次用 I2C1 和 LSM6DSOW 通信。在 CubeMX 里把 I2C1 的 SCL、SDA 分别分配到 PB8、PB9 两个引脚然后把 LSM6DSOW 的 INT1 接到 PA2PA2 同时启用了 EXTI2 外部中断当作传感数据就绪信号输入。LSM6DSOW 这边除了 I2C 通信线还要处理好电源和地址引脚。LSM6DSOW引脚接到 STM32C5 或外部说明VDD3.3V数字电源GNDGND地SCLPB8I2C 时钟SDAPB9I2C 数据INT1PA2中断输出接 MCU 外部中断SDO/SA0可选我接的 GND决定 I2C 地址CS接 3.3V不使用 SPI 时固定拉高SDO/SA0 引脚接 GND 时LSM6DSOW 的 7 位 I2C 地址是 0x6A接 VDD 时是 0x6B。具体到代码里写寄存器时要记得把 7 位地址左移一位变成 8 位地址也就是 HAL 库里的0x6A 1。很多人第一次 I2C 通信失败就是卡在这个地址换算上。I2C 的 SCL 和 SDA 需要外部上拉电阻一般 4.7kΩ 配 3.3V 供电比较稳。如果板子已经自带传感器模组通常不用再加大电阻。2.2 CubeMX 配置步骤与容易忽略的细节打开 STM32CubeMX选择 STM32C5 型号后第一件事是配 RCC 时钟源和调试下载口。如果用的是 SWD 调试SYS 里 Debug 要选 Serial Wire不然下载几次后可能把调试口占了。接着配置 I2C1打开对应引脚后把速度选到 400kHz。400kHz 是 LSM6DSOW 和 STM32C5 都支持的标准 fast mode读取 6 字节陀螺仪数据大概不到 150 微秒对中断处理来说足够快。配置 PA2 外部中断时GPIO 模式选择 External Interrupt Mode with Rising edge trigger detection也就是外部中断上升沿触发。这里要特别留意LSM6DSOW 的 DRDY 是短脉冲选上升沿才能在脉冲跳变的那一刻捕获到。如果选成下降沿那只能在脉冲回落时触发虽然偶尔也能用但时序上容易出问题。PA2 内部可以开启上拉电阻这样 INT1 在空闲状态下不会悬空。虽然 LSM6DSOW 的 INT1 默认是推挽输出但加上拉不会坏事反而能避免因为接线松动带来的电平抖动。最后在 NVIC 设置里确认 EXTI2 中断没有被禁止。CubeMX 默认会生成能打开的 IRQHandler但如果你手动关过就要去查一下。2.3 初始化LSM6DSOW的关键寄存器代码层面初始化传感器建议按这个顺序走读取 WHO_AM_I确认 I2C 通没通。软复位传感器让内部寄存器回到默认值。设置 BDU 和地址自增。配置陀螺仪 ODR 和满量程。把数据就绪中断映射到 INT1。WHO_AM_I 寄存器地址是 0x0FLSM6DSOW 的固定值一般是 0x6C。如果读出来不对后面就别继续了优先查接线和地址。软复位写 CTRL3_C 的 SW_RESET 位等待传感器内部完成复位保险起见加 20ms 延时然后再写 BDU 和 IF_INC。BDU 是 Block Data Update意思是高字节和低字节的输出寄存器在做切换时会被锁存到同一份采样值避免读取过程中高低字节来自不同时刻的数据。IF_INC 叫 Register Address Increment开启后可以连续读多个寄存器读 6 字节陀螺仪数据会非常方便。陀螺仪配置写到 CTRL2_G寄存器地址 0x11。我用的 ODR 是 104Hz满量程 2000dps配置值写成 0x46。这里的 0x46 含义是高四位 ODR_G 为 0x04 表示 104Hz中间的 FS_G 位为 0x03 并且放到 bit3 到 bit1 的位置最后 bit0 的 FS_125 不置位。如果是其它系列的 ST 传感器位定义可能有差异建议以 LSM6DSOW 数据手册的 CTRL2_G 位定义为准。中断映射寄存器 INT1_CTRL 地址是 0x0D写入 0x02把 INT1_DRDY_G 使能。这样每次陀螺仪有一个新采样准备好INT1 引脚就会输出一个脉冲。初始化代码uint8_t id 0; lsm6dsow_read_regs(0x0F, id, 1); if (id ! 0x6C) { // 通信失败或地址不对停在错误处理里 } lsm6dsow_write_reg(0x12, 0x02); // SW_RESET HAL_Delay(20); lsm6dsow_write_reg(0x12, 0x44); // BDU | IF_INC lsm6dsow_write_reg(0x11, 0x46); // 104Hz, 2000dps lsm6dsow_write_reg(0x0D, 0x02); // INT1_DRDY_G2.4 I2C读写的底层函数写寄存器用 HAL 的HAL_I2C_Mem_Write读寄存器用HAL_I2C_Mem_Read。这两个函数针对的是“带子地址的 I2C 设备”正好适用于 LSM6DSOW。#define LSM6DSOW_ADDR 0x6A #define LSM6DSOW_REG_WHO_AM_I 0x0F #define LSM6DSOW_REG_CTRL3_C 0x12 #define LSM6DSOW_REG_CTRL2_G 0x11 #define LSM6DSOW_REG_INT1_CTRL 0x0D #define LSM6DSOW_REG_STATUS 0x1E #define LSM6DSOW_REG_OUTX_L_G 0x22 static void lsm6dsow_write_reg(uint8_t reg, uint8_t val) { HAL_I2C_Mem_Write(hi2c1, LSM6DSOW_ADDR 1, reg, I2C_MEMADD_SIZE_8BIT, val, 1, 10); } static void lsm6dsow_read_regs(uint8_t reg, uint8_t *buf, uint16_t len) { HAL_I2C_Mem_Read(hi2c1, LSM6DSOW_ADDR 1, reg, I2C_MEMADD_SIZE_8BIT, buf, len, 10); }这一段是基础后面所有读取都建立在它上面。3. 中断读取实现的完整流程与代码解析3.1 中断回调里该放什么不该放什么STM32 的标准 HAL 库会在 EXTI2_IRQHandler 里调用HAL_GPIO_EXTI_IRQHandler最终再调用弱定义的HAL_GPIO_EXTI_Callback。我们只需要覆盖这个回调函数就行。一开始我图省事直接在回调函数里用阻塞式 I2C 去读陀螺仪也就是在 EXTI 中断里面调用HAL_I2C_Mem_Read。低 ODR 时好像没啥问题一旦 ODR 调到 416Hz 以上问题就来了I2C 阻塞传输本身要花时间如果中断优先级不够高还可能被其他中断打断。更严重的是如果主循环里正好还在用 I2C 读其他寄存器两边的传输会互相干扰出现 I2C 总线超时。所以中断回调里最稳的做法是只置一个标志位把真正耗时的读取操作放到主循环里。这也是中断处理的最基本原则中断服务函数要短不在中断里做耗时的外围设备访问。volatile uint8_t gyro_ready 0; void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin LSM_INT1_Pin) { gyro_ready 1; } }这个变量加上 volatile是因为它会在中断里被写入在主循环里被读取编译器如果不加 volatile可能会把主循环里的读取优化成只读一次导致标志变了主循环也不知道。3.2 主循环里面怎么读陀螺仪数据主循环里判断gyro_ready如果变成 1就清标志并调用读取函数。读取函数不是直接读输出寄存器而是先看看 STATUS_REG 的 bit1 是否为 1确认数据确实准备好了再一口气读 6 个字节。这里有个细节LSM6DSOW 的陀螺仪输出寄存器从 OUTX_L_G 开始依次是 X 轴低位、X 轴高位、Y 轴低位、Y 轴高位、Z 轴低位、Z 轴高位。因为前面开了 IF_INCI2C 多字节读可以自动地址递增一次读完 6 个字节。float gx_dps, gy_dps, gz_dps; void read_gyro_data(void) { uint8_t status 0; uint8_t buf[6]; int16_t raw_x, raw_y, raw_z; lsm6dsow_read_regs(0x1E, status, 1); if ((status 0x02) 0) { return; } lsm6dsow_read_regs(0x22, buf, 6); raw_x (int16_t)((buf[1] 8) | buf[0]); raw_y (int16_t)((buf[3] 8) | buf[2]); raw_z (int16_t)((buf[5] 8) | buf[4]); gx_dps raw_x * 70.0f / 1000.0f; gy_dps raw_y * 70.0f / 1000.0f; gz_dps raw_z * 70.0f / 1000.0f; }主循环里的逻辑就很简洁while (1) { if (gyro_ready) { gyro_ready 0; read_gyro_data(); printf(gx%.2f gy%.2f gz%.2f\r\n, gx_dps, gy_dps, gz_dps); } }3.3 原始值换算成角速度LSM6DSOW 陀螺仪输出是 16 位有符号整数不经过换算直接看原始值没有意义。换算成角速度需要一个灵敏度系数这个系数由满量程决定。满量程 (dps)灵敏度 (mdps/LSB)原始值 32768 对应的角速度1254.375约 143 dps2508.75约 286 dps50017.5约 573 dps100035约 1146 dps200070约 2294 dps上面的代码里用的是 2000dps 满量程所以灵敏度取 70 mdps/LSB原理解就是每个 LSB 代表 0.07 dps。把原始值乘以 70 再除以 1000就是实际的每秒旋转度数。如果你的满量程不一样灵敏度系数也要跟着换。这套换算在 125dps 档位下需要特别注意因为 4.375 mdps/LSB 意味着相同角速度下原始值更大精度更高但也更容易溢出。选择量程的原则是尽量选接近实际运动范围的最小量程反正代码改一个灵敏度系数就行。3.4 进阶优化用FIFO扛住高采样率当你把 ODR 拉到 833Hz 甚至 3.3kHz 时每次采样都触发一次 EXTI中断次数会非常多。以 3.3kHz 为例大概每 303 微秒就触发一次。主循环哪怕只是进中断再退出都会占用不少 CPU。LSM6DSOW 内部有 FIFO可以不断把陀螺仪数据存进去MCU 不用每个采样都去响应而是等 FIFO 里的数据攒够一批再一次性读出来。这样 EXTI 中断频率就从 3.3kHz 降到了比如 100Hz调用一次 I2C 批量读取就能取回几十组数据。FIFO 的使用逻辑大致是配置 FIFO 工作模式让它持续存储陀螺仪数据。设置 FIFO 阈值比如存到 32 组数据时触发一次中断。把 FIFO 阈值中断映射到 INT1。在中断回调里置标志主循环根据 FIFO_STATUS 寄存器读回样本数再批量读取。我自己在工程里用的就是这个思路ODR 设在 833HzFIFO 攒一部分数据再取CPU 占用比单纯 DRDY 中断低很多。不过 FIFO 涉及到的寄存器比较多建议先把单字节中断读通再迁移到 FIFO排错会更容易。4. 常见问题、排查技巧与方案升级4.1 INT1没有脉冲中断一直不触发这是最常见的问题。我建议排查时先用示波器或逻辑分析仪看 INT1 引脚手动缓慢转动板子观察是否有脉冲输出。如果没有脉冲问题基本在传感器侧如果有脉冲但 MCU 不进中断问题在 EXTI 配置侧。传感器侧先回读 INT1_CTRL确认写入的 0x02 是生效的很多情况下是因为 I2C 写顺序不对后面又被别的初始化覆盖了。另外检查传感器是否真的进入了正常工作状态如果 CTRL2_G 的 ODR_G 还是 0陀螺仪不采样自然不会有数据就绪中断。还有一个非常容易忽略的点GPIO 外部中断优先级。如果项目里其他外设中断比较多而且 NVIC 里 EXT2 的优先级被设置得非常低它可能一直被其他中断抢占。虽然标志位最后还是会置起来但延迟可能已经很大表现为数据总是“晚到”。4.2 I2C通信失败WHO_AM_I读回不对排查顺序如下确认 SDO/SA0 引脚电平地址是 0x6A 还是 0x6B。代码里有没有把 7 位地址左移一位传给 HAL 函数。I2C 总线上有没有上拉电阻上拉电阻是不是太大或者太小。4.7kΩ 是一个通用起点。尝试把 I2C 速度降到 100kHz排除时序问题。检查 LSM6DSOW 的供电VDD 是否稳定在 1.71V 到 3.6V 之间GND 是否共地。我在调试时遇到过一种情况SCL 和 SDA 两根线被模块板和主控板的长排线一接线长超过 20cm400kHz 下时不时读错。把速度降到 100kHz 后稳定了但最后还是缩短线距才彻底解决。4.3 陀螺仪数据偶尔突变静止时不为零这个现象通常不是中断问题而是读数本身不稳定。如果你没有开 BDU高字节和低字节可能在两次独立 I2C 传输之间被传感器更新读出个“缝合怪”数据看起来就是偶尔跳一下。这个用示波器很难抓但把 CTRL3_C 的 BDU 打开后跳变基本消失。静止时陀螺仪输出不为零非常正常这是零偏。每个芯片出厂都不一样温度变化也会影响。工程上要做零偏校准上电稳定后把板子放平静止连续采集 1000 组数据求平均值作为零偏。之后每次读到的数据减去这个零偏值再参与计算姿态角就不会漂得那么快了。电源噪声也会影响陀螺仪读数尤其是 VDD 纹波大时输出会有周期性波动。在 VDD 和 GND 之间加一个 100nF 陶瓷电容离传感器引脚越近越好能明显改善。4.4 不要在主循环里同时发起多个I2C传输从 STM32C5 的 HAL 库角度讲HAL_I2C_Mem_Read是阻塞式调用。如果主循环里有两个任务都在用同一个 I2C1它们必须串行执行。一旦有人在中断里插了一脚I2C 总线的起始条件就可能被打乱导致总线锁死或读回 0xFF。我踩过一次坑一个任务在读气压计另一个任务在读陀螺仪两个任务还不在同一个优先级里。结果 I2C 总线偶尔卡死必须复位传感器才能恢复。后来把所有 I2C 访问都放到同一个线程里另外加了互斥保护问题才消失。如果你必须在外围设备上做类似的事情建议用一个 I2C 访问锁或者统一走一个“外设读取队列”不要让两个任务同时碰 I2C。4.5 如何验证中断读取的数据真的准确验证分为三步。第一步静止测试。板子放平不动时打印出来的角速度应该在 0 附近小范围波动波动幅度和你设置的 ODR、量程有关。如果出现几百 dps 的数值检查是不是单位换算搞错了。第二步方向测试。绕 Z 轴转动板子观察 gz 的数值是否随转动方向和快慢变化x、y 轴应该接近 0。用手比划一个右手坐标系转哪个轴就看哪个轴的数据确保坐标轴方向和你预期一致。第三步与轮询对比。在同一个工程里先跑 5 分钟轮询采集再跑 5 分钟中断采集对比数据有没有明显丢帧。中断方式下gyro_ready置位的次数理论上应该等于传感器产生的采样点数如果差距很大说明中断回调里处理太慢或者 EXTI 丢失了脉冲。我个人在实际项目里的体会是中断读陀螺仪最大的收益不是“快”而是“稳”。它让采样节奏和传感器自己保持一致不用去猜主循环什么时候轮到 IMU 任务。如果你现在还在用轮询先改成 DRDY 中断试试感受会很直接等 ODR 上去了再考虑 FIFO。这个小改动能让后续的姿态解算少很多莫名其妙的跳变。