ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32C5读取LSM6D3TR-C陀螺仪:I2C轮询实战指南

STM32C5读取LSM6D3TR-C陀螺仪:I2C轮询实战指南 最近用 STM32C5 做了一套传感器数据采集的小板子主控选的是自家生态里货量比较足的型号传感器则用的是 LSM6D3TR-C。这颗芯片内部集成了三轴加速度计和三轴陀螺仪是典型的 6 轴 IMU主要面向可穿戴设备、智能家居、游戏手柄这类对成本和功耗都敏感的场景。我把“轮询获取陀螺仪数据”放在这篇系列文章的第一篇因为它最能说明 LSM6D3TR-C 的工作流先通过 I2C 去查状态寄存器等陀螺仪数据就绪位置 1 后再把 6 个字节的原始输出读回来。为了让刚入门的朋友也能直接抄作业我会把从 CubeMX 配置到主循环代码完整讲一遍同时把我踩过的坑一起写出来后面中断和 DMA 版本再继续更新。1. 项目背景与轮询方案选型1.1 LSM6D3TR-C 究竟是个什么传感器LSM6D3TR-C 是意法半导体 LSM6D 系列里的一个型号内部结构可以理解成“两个传感器挤在一个封装里”三轴加速度计加三轴陀螺仪。加速度计测的是物体受到的加速度陀螺仪测的是绕 X、Y、Z 三个轴的角速度。两者结合起来就可以在一定条件下估算物体姿态这也是消费级无人机、AR 眼镜、运动手表里最常见的传感器方案。和很多同系列芯片一样LSM6D3TR-C 对外提供 I2C 和 SPI 两种接口我这次选的是 I2C。它的寄存器地址是 8 位内部功能寄存器分成好几组比较核心的有控制寄存器、状态寄存器、数据输出寄存器、FIFO 相关寄存器。你不需要把所有寄存器都背下来但几个关键地址必须知道后面代码里全都会用到WHO_AM_I0x0F读出来的是芯片 ID用来确认 I2C 链路是不是通了。CTRL1_XL0x10控制加速度计的输出速率和量程。CTRL2_G0x11控制陀螺仪的输出速率和量程。CTRL3_C0x12控制接口、BDU 位和自动地址递增。STATUS_REG0x1E用来查询加速度计和陀螺仪是否准备好了新数据。OUTX_L_G0x22到 OUTZ_H_G0x27陀螺仪三轴原始数据每轴两个字节总共 6 字节。我说这个传感器的时候有人会拿 MPU6050 来对比。MPU6050 是老牌六轴器件很多入门的飞控项目都在用但从寄存器结构、低功耗表现和 FIFO 能力上看LSM6D3TR-C 这套方案更现代一些。使用方法上两者差别不大都是先初始化寄存器再读输出寄存器核心套路是一样的。1.2 为什么选 STM32C5 这颗主控STM32C5 是 ST 在 MCU 产品线里比较新的一代核心基于 ARM Cortex-M33带 FPU 和 DSP 指令集。先别听到 FPU 就觉得夸张我用它的主要原因是后面打算直接在芯片上做简易姿态解算浮点运算会比定点写起来省心很多代码也更容易扩展。实际测试下来裸机轮询读取 LSM6D3TR-C 的占用率其实很低CPU 大部分时间是空闲的所以完全有余力去跑滤波和解算。另一个原因是 STM32C5 的生态很顺。STM32CubeMX 直接支持这套芯片HAL 库生成代码很快I2C 外设初始化不需要自己翻寄存器手册。对做传感器验证来说关键是先把 MCU 和传感器之间的通道打通而不是去跟底层寄存器较劲。开发板上我用的是 3.3V 供电I2C 速度设置为 100kbps 的标准模式这样信号质量和兼容性都比较稳。如果换成 RK3588 接这颗传感器流程上其实是类似的只不过一个在裸机环境直接操作寄存器另一个在 Linux 用户态通过 I2C 设备节点读写。平台不同思路一致先把 I2C 读写函数验明白后面怎么接都跑不掉这套逻辑。1.3 轮询和中断、DMA 怎么选“轮询获取陀螺仪数据”里的“轮询”指的是 CPU 主动、反复去查传感器状态寄存器等数据就绪后再读数据。和中断、DMA 相比轮询最大的优点是逻辑简单代码里没有回调函数没有中断优先级配置也不需要考虑 DMA 缓冲区的生命周期。对刚上手传感器的开发者来说轮询是理解传感器时序最快的方式。但轮询也有它的代价如果传感器输出速率特别高比如 ODR 设成 6.6kHz那 CPU 会持续被状态查询占用没时间干别的事。所以我这次把陀螺仪 ODR 设置成 104Hz这个速率对姿态显示来讲足够CPU 绝大部分时间还是在主循环里空跑。这里顺带说一个容易被坑的点在实际项目里轮询不只是这一种形式。有人会把传感器的 INT 引脚接到 MCU 的 IO 口然后在主循环里查询这个引脚电平这也算“轮询状态”。甚至一些通信系统里也有类似问题比如有人用西门子 1200PLC 做 Modbus 轮询读取时发现读取频率如果设置不合理高频率请求会互相覆盖其他数据。本质原因和传感器轮询是一样的——在你读取旧数据之前新数据已经产生数据帧被覆盖了。所以设计轮询逻辑时必须把“数据就绪”作为一个判断条件不能盲目地定时去读。2. 硬件准备与工程初始化2.1 接线和地址设置拿到 LSM6D3TR-C 之后先别急着写代码把硬件连接确认清楚最重要。我这块板子上LSM6D3TR-C 的 VDD 和 VDDIO 都接 3.3VGND 公共接地。I2C 的 SCL 和 SDA 分别接到 STM32C5 的 I2C1 引脚上同时各加一个 4.7kΩ 上拉电阻到 VDDIO。很多新手会忽略上拉电阻导致 I2C 波形上沿太慢通信时好时坏尤其数据量大时特别容易出问题。LSM6D3TR-C 的 I2C 地址取决于 SDO/SA0 引脚的电平。SDO 接地时7 位地址是 0x6A左移一位的 8 位写地址是 0xD4SDO 接高电平时7 位地址变成 0x6B8 位地址是 0xD6。我这里把 SDO 拉低所以后边代码里定义设备地址用的 7 位 0x6A传给 HAL 函数时左移一位。接线表整理如下信号LSM6D3TR-CSTM32C5备注VDD3.3V3.3V传感器主电源VDDIO3.3V3.3VIO 电平参考GNDGNDGND共地SCLSCLPB6I2C1_SCLSDASDAPB7I2C1_SDASDOGNDGND设置 I2C 地址CS3.3V3.3V选择 I2C 模式CS 引脚在 I2C 模式下必须接高。这个是我刚上手时忽略的问题CS 悬空的时候 I2C 一直没反应后来查数据手册才意识到问题接上拉后立刻正常了。2.2 在 STM32CubeMX 里配置 I2C用 STM32CubeMX 新建 STM32C5 工程时我做了几步配置选择芯片型号或开发板后先把时钟树配好。系统时钟直接跑最高主频也行不过为了功耗考虑我用了 64MHz 的外部晶振倍频方案。打开 I2C1 外设Mode 选 I2CSpeed Mode 选 Standard ModeI2C Clock Speed 设置为 100000Hz。这里特意用标准模式一方面兼容 LSM6D3TR-C 的上电时序另一方面总线上如果还挂其他传感器100kHz 对走线的容错率更高。打开 UART2 用于调试后面打印陀螺仪数据要用人机对话。波特率设成 115200不对直接叫 115200。生成工程代码框架里会带上 MX_I2C1_Init() 和 MX_USART2_UART_Init()。CubeMX 生成的 I2C 初始化基本不用改只是我会把超时时间从 100ms 调大一点因为第一次上电时传感器可能需要时间来稳定如果是 10 万毫秒这种悬殊配置完全没必要。重点是确认 GPIO 复用正确别把 SCL 和 SDA 配成普通推挽输出口。2.3 传感器寄存器初始化顺序LSM6D3TR-C 上电后默认是低功耗模式输出数据还没准备好必须先写控制寄存器。我在初始化函数里按这个顺序操作void LSM6D3_Init(void) { uint8_t id 0; LSM6D3_ReadRegs(LSM6D3_WHO_AM_I, id, 1); if (id ! 0x6A) { while(1); } LSM6D3_WriteReg(LSM6D3_CTRL3_C, 0x44); LSM6D3_WriteReg(LSM6D3_CTRL2_G, 0x55); LSM6D3_WriteReg(LSM6D3_CTRL1_XL, 0x50); }第一行读 WHO_AM_I如果读回来不是 0x6A说明接线有问题、地址配置错了或者你的传感器批次 ID 不一样。这里我直接让程序死循环是为了调试时一眼看出初始化没过。写 CTRL3_C 时我设置了两个关键位BDUBlock Data Update位置 1。这个位的意思是“数据未读完之前寄存器内容不更新”。如果不设置它当 MCU 在读数据的间隙传感器更新了寄存器就可能出现高字节是旧数据、低字节是新数据的撕裂情况陀螺仪输出值会莫名其妙跳变。IF_INC 位置 1。这个位让 I2C 支持自动地址递增后面一次读 6 字节时才不需要逐个地址操作。CTRL2_G 写 0x55表示陀螺仪 ODR 为 104Hz满量程为 2000dps。CTRL1_XL 写 0x50表示加速度计 ODR 为 104Hz满量程为 4g。这个配置对我要做的静态和慢速姿态演示完全够用。3. 轮询读取陀螺仪的核心代码3.1 I2C 寄存器的读写封装STM32C5 的 HAL 库里I2C 读写用起来很直接。我封装了两个函数一个是写单个寄存器一个是连续读多个寄存器void LSM6D3_WriteReg(uint8_t reg, uint8_t data) { HAL_I2C_Mem_Write(hi2c1, LSM6D3_DEV_ADDR, reg, I2C_MEMADD_SIZE_8BIT, data, 1, 100); } void LSM6D3_ReadRegs(uint8_t reg, uint8_t *buf, uint16_t len) { HAL_I2C_Mem_Read(hi2c1, LSM6D3_DEV_ADDR, reg, I2C_MEMADD_SIZE_8BIT, buf, len, 100); }HAL_I2C_Mem_Write 和 HAL_I2C_Mem_Read 是专门针对寄存器型器件的封装函数内部会先发送寄存器地址再写或读数据。这里要注意设备地址参数HAL 库要求传入 8 位地址也就是 7 位地址左移一位所以我宏定义里写的是#define LSM6D3_DEV_ADDR (0x6A 1) #define LSM6D3_WHO_AM_I 0x0F #define LSM6D3_CTRL3_C 0x12 #define LSM6D3_CTRL2_G 0x11 #define LSM6D3_CTRL1_XL 0x10 #define LSM6D3_STATUS 0x1E #define LSM6D3_OUTX_L_G 0x22如果你用的是 SPI 模式地址就不用左移但 I2C 模式下这个细节最容易翻车。3.2 数据就绪位判断与超时保护轮询读取的核心逻辑不是直接读 OUTX_L_G而是先读 STATUS_REG判断陀螺仪数据是否已经准备好。LSM6D3TR-C 的 STATUS_REG 里bit0 是加速度计数据就绪位bit1 是陀螺仪数据就绪位。所以代码里要检查(status 0x02)。必须加超时保护。如果不加只要传感器一旦配置错误或者总线异常这个 while 循环就会永远卡死MCU 连看门狗的机会都没有。我一般给 1000 次查询作为上限查询时间大约几毫秒足够覆盖 104Hz 对应的 9.6ms 周期int LSM6D3_ReadGyro(int16_t *gyro) { uint8_t status 0; uint8_t buf[6]; uint32_t timeout 1000; do { LSM6D3_ReadRegs(LSM6D3_STATUS, status, 1); if (--timeout 0) { return -1; } } while ((status 0x02) 0); LSM6D3_ReadRegs(LSM6D3_OUTX_L_G, buf, 6); gyro[0] (int16_t)((buf[1] 8) | buf[0]); gyro[1] (int16_t)((buf[3] 8) | buf[2]); gyro[2] (int16_t)((buf[5] 8) | buf[4]); return 0; }有人会问为什么不能定时 10ms 读一次输出寄存器非要去查状态位因为在高速模式下如果你一直读传感器可能会覆盖数据在低速模式下提前读会拿到旧数据。状态位就是传感器在 SAY这次的数据已经准备好了你赶紧来拿。这也是“状态轮询”这个名字的由来。3.3 主循环里的完整调用流程主循环里我写得比较精简int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_I2C1_Init(); MX_USART2_UART_Init(); LSM6D3_Init(); int16_t gyro_raw[3]; float gyro_dps[3]; while (1) { if (LSM6D3_ReadGyro(gyro_raw) 0) { gyro_dps[0] gyro_raw[0] * 0.070f; gyro_dps[1] gyro_raw[1] * 0.070f; gyro_dps[2] gyro_raw[2] * 0.070f; printf(GX:%.2f GY:%.2f GZ:%.2f\r\n, gyro_dps[0], gyro_dps[1], gyro_dps[2]); } } }实际运行效果是串口助手以 104Hz 的频率不断输出三轴角速度。板子静止时三个值都会在 0 dps 附近小幅波动用手转动板子对应轴向的值会立刻变化。如果你看到数值不变化先回去查初始化顺序如果数值始终是一个固定的大值多半是数据拼接时高低字节顺序搞反了。4. 数据处理角速度换算与误差校正4.1 怎么把原始值换算成角速度LSM6D3TR-C 内部 ADC 是 16 位输出寄存器里存放的是补码形式的原始值。满量程设置为 2000dps 时-32768 对应 -2000dps32767 对应约 2000dps。换算公式很简单角速度(dps) 原始值 * 2000 / 32768实际测试中我更习惯用封装好的灵敏系数这颗传感器在 2000dps 量程下典型灵敏度是 70mdps/LSB。也就是说原始值每变化 1对应角速度变化 0.070 dps。代码里直接乘以 0.070f 就可以gyro_dps[0] gyro_raw[0] * 0.070f;如果换成了 250dps 量程那灵敏度就变成 8.75mdps/LSB系数就要改成 0.00875f。这个数值千万别想当然得去数据手册的“Sensitivity”表格里查不同量程对应的系数差距很大。4.2 零漂、Z轴补偿和姿态解算的关系陀螺仪不是完美器件静止时读数不会正好是 0。因为温度、机械应力和硅片工艺的原因输出值会有零漂。我做 Z 轴补偿的时候先把传感器水平静置连续采样 200 帧陀螺仪数据求平均把这个平均值作为零偏记录下来以后每次读到的角速度都把它减掉static float gyro_offset[3]; void Gyro_CalibZero(void) { int32_t sum[3] {0,0,0}; int16_t gyro[3]; for (int i 0; i 200; i) { if (LSM6D3_ReadGyro(gyro) 0) { sum[0] gyro[0]; sum[1] gyro[1]; sum[2] gyro[2]; } HAL_Delay(2); } gyro_offset[0] (float)sum[0] / 200.0f * 0.070f; gyro_offset[1] (float)sum[1] / 200.0f * 0.070f; gyro_offset[2] (float)sum[2] / 200.0f * 0.070f; }这里说的 Z 轴补偿其实包括两层意思一是校正绕 Z 轴的零偏二是保证 Z 轴和重力方向对齐让姿态解算初始角度为 0。如果只是做角速度测量补偿零偏就够了如果拿去做姿态解算还需要加速度计参与初始化。很多人一上来就想拿陀螺仪积分算角度结果越漂越厉害根因就是没有先做零偏校准。从这份原始角速度到最终姿态中间还差一个姿态解算的过程可以选欧拉角、方向余弦矩阵或者四元数。四元数在嵌入式里用得最多因为它没有万向锁问题计算量也可控。这篇文章暂时不会展开先把干净的角速度数据拿到手这是第一步也是最重要的一步。4.3 用串口和虚拟示波器观察数据数据拿到手后光看串口数字很难直观感受角度变化。我习惯把数据用文本协议发出去然后在 PC 上用虚拟示波器软件查看波形。串口打印代码我一般用重定向 printf如果你用的是 STM32CubeIDE直接在 usart.c 文件里加上int fputc(int ch, FILE *f) { HAL_UART_Transmit(huart2, (uint8_t *)ch, 1, 100); return ch; }发送格式保持一行一条字段用逗号或者空格分隔这样解析最简单GX:-0.05 GY:0.10 GZ:0.02把开发板水平放在桌面上然后快速转动板子你会看到对应轴的波形出现一个脉冲。静止不动时三个轴的波形都在零线附近小幅抖动。抖动幅度如果到了 ±2dps 以上就要留意是不是供电纹波太大或者传感器附近有振动源。5. 常见问题与排坑记录5.1 常见问题速查表我把这套开发过程中最容易遇到的问题整理成一个表方便你直接对照排查现象可能原因解决办法WHO_AM_I 读不对I2C 地址错误、SDO 电平不对、接线虚焊确认 SDO 接地地址是 0x6A检查 SCL/SDA 是否反WHO_AM_I 读不到值芯片供电异常、CS 悬空、I2C 未初始化给 CS 接 3.3V确认 VDD 和 VDDIO 有电GDA 位一直为 0陀螺仪没有配置 ODR确认写入了 CTRL2_G而不是只写了 CTRL1_XL数据能读但跳变得厉害寄存器撕裂、电源纹波太大打开 BDU 位增加 0.1uF 去耦电容数据像一个固定大数不变高低字节拼接顺序错误检查 buf[1]8 还是 buf[0]8And确认数据是补码上电立刻读的陀螺仪数值不稳芯片内部还在自校准初始化后丢弃前 10 帧或 HAL_Delay(50) 再读多次调用占 CPU 高轮询里每次循环都做 I2C 查询先查状态位再读避免高频空转这部分是我最想让你收藏的地方。因为这些坑尤其是 I2C 地址和 BDU 位不看经验分享的话真的会卡你一两天。5.2 我这轮开发踩过的坑第一个坑是 I2C 通信时的上拉电阻。我最初直接用开发板内部上拉其实部分 STM32 的引脚内部上拉只有几十千欧用作 I2C 外接总线时驱动能力不够。换了 4.7kΩ 外部上拉后通信一下子稳定了再也没出现“读十次失败一次”的问题。第二个坑是 BDU 位。我第一版代码没设置 BDU转动板子的速度比较快时偶尔会出现某个轴的角度值突然跳到 1800 然后又跳回来看起来像毛刺。后来仔细读了数据手册发现是数据撕裂问题设置 BDU 后毛刺明显消失。第三个坑和上电时序有关。一开始我在初始化后立刻开始轮询读到的第一个数经常是一个很大的异常值。这不是芯片坏了而是传感器刚切换 ODR 时内部数字滤波还没稳定。我在初始化结束后加了 50ms 延时并丢弃前 10 帧马上就干净了。还有一次比较典型的情况我为了调试方便把 ODR 设为 1.6kHz主循环轮询也能跑但我同时在串口打印导致打印耗时太长状态位已经被下一次采样覆盖。虽然逻辑上没错但每隔一段时间会丢失一帧。这种场景下就该考虑 FIFO 或者切换到 SPI轮询不是不能用而是要把打印这类阻塞任务从主循环里拿出来。5.3 轮询方式在真实项目中的边界最后说一句实在话轮询获取陀螺仪数据适合前期验证和低速率场景。我在这篇里设置的 104Hz ODR 属于比较保守的值轮询完全扛得住。可一旦 ODR 开始往 1kHz 以上走或者系统里还要跑 LVGL、无线协议栈这些重任务轮询就会成为瓶颈。到那时候你应该切换到中断模式或者直接把传感器数据接到 FIFO 和 DMA 上面让传感器先进 FIFO再由 DMA 定期搬运CPU 只负责消费结果。但不管你选轮询还是中断Sensor 初始化、寄存器配置、单位换算这些底层逻辑是一模一样的。所以把这一篇完整吃透后面再升级中断或者 DMA 版本就是单纯的工程技巧问题不用再回头啃数据手册。我在实际开发中还有个体会很多同学一上来就想实现姿态解算结果连原始角速度都没打出来就开始调四元数最后定位问题耗时特别久。正确的路径应该是先打印原始数据确认传感器通路再做零偏校准和单位换算最后才进入滤波和姿态融合。先把这层地基打牢后面再做 LSM6D3TR-C 的中断方式获取、陀螺仪温度补偿、FIFO 批量读取都会顺很多。
返回列表