ARTICLE DETAIL

资讯详情

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

STM32C5驱动LSM6DSVE:I2C轮询读取陀螺仪数据实战

STM32C5驱动LSM6DSVE:I2C轮询读取陀螺仪数据实战 拿到STM32C5这颗新片子之后我第一个想到的就是把ST自家那颗LSM6DSVE六轴传感器接上去。做这件事的初衷很简单先把一条最基础的数据链路打通——用轮询方式读陀螺仪数据把原始寄存器值变成能用的角速度。很多刚接触这个组合的开发者容易被中断、FIFO、DMA这些高级玩法吸引其实轮询是最朴素也最不容易出错的起点把这条路径走通后面再加什么都顺理成章。这篇内容适合三类人正在用STM32C5开新项目的、从F1/F4往C5迁移的、第一次碰LSM6DSVE想快速出数据的。我不会把轮询说的多高大上就按我实际调试的步骤把该配的寄存器、该注意的信号完整说一遍。1. 为什么把STM32C5和LSM6DSVE放在一起1.1 STM32C5带来的变化STM32C5这个系列跟以前的F系列有一个明显区别内核换成了Arm Cortex-M33主频做到了250MHz。M33比M4多了TrustZone和低延迟中断处理功耗控制也更好定位是那种“要算力但又不想为功耗买单”的场景。我这次用的具体型号是C5系列里的评估板跑一个I2C传感器轮询对这颗MCU来说基本谈不上压力更多的精力反而要花在传感器这边。从开发习惯来说C5依然走STM32CubeMX加HAL库这套流程这也是我选它做驱动验证的原因。HAL库的I2C接口跟我们熟悉的F1/F4几乎没有差别HAL_I2C_Mem_Read、HAL_I2C_Mem_Write这些函数可以直接平移过来。所以你别看换了内核代码层面的迁移成本很低真正要重新研究的是C5的时钟树、电源域和引脚复用表这些在CubeMX里能省不少事。有一点想单独提一下C5的I2C外设跟老系列相比底层多了一些关于数字滤波和时序控制的寄存器默认配置下兼容性没问题。但如果你在做高速率外设通信建议花十分钟过一遍参考手册里的I2C timing配置别完全依赖默认值尤其是总线电容比较大的时候。1.2 LSM6DSVE的定位和接口选择LSM6DSVE是一颗六轴惯性测量单元内部同时集成了三轴陀螺仪和三轴加速度计。它比那些老款的LSM6DS3、LSM6DSO系列在噪声和功耗控制上有改进适合可穿戴、TWS耳机、智能家居这种需要长时间待机但又对姿态检测有要求的应用。这颗传感器支持I2C、SPI和MIPI I3C三种接口。我在这个项目里选I2C原因很实际接线少评估板上直接拉两根线就行而且对于轮询读取陀螺仪数据这种低频操作I2C 400kHz模式完全够用不需要SPI那种高速通道。后续如果要从FIFO里批量导数据或者ODR拉得很高再考虑换成SPI也不迟。这里提醒一句LSM6DSVE这个型号在市面上可能带不同后缀不同供货批次的Device ID定义有细微差别。你拿到芯片第一件事就是查对应数据手册的WHO_AM_I寄存器描述确认默认值再写进代码里做校验。我在下面示例代码里写的ID是常见值但你板子上那颗料到底返回什么必须以规格书为准。1.3 轮询方式到底合不合适轮询的本质是CPU主动去查传感器的状态寄存器看数据有没有准备好准备好了再读数据。它跟中断的区别在于中断是传感器主动喊你轮询是你隔一会儿就去敲一次门。敲门的次数少于传感器产生数据的次数就可能漏数据敲多了又浪费CPU。在LSM6DSVE跑104Hz ODR的情况下一个周期约9.6ms一次I2C读完整6字节数据大概也就一百多微秒占用比例不到2%。所以用轮询方式在这个频率下没有任何问题CPU大部分时间依然是空闲的。这也就回答了很多人纠结的问题轮询是不是太低效答案是看场景低速率的传感器数据获取轮询就是最简单可靠的方案。顺带说一句“轮询”这个词在工业控制里太常见了从PLC的MODBUS扫描到FPGA里的总线轮询仲裁本质都是主动去查询。MCU做传感器轮询时最怕的是在循环里忘记加超时保护一旦传感器掉线整个程序就卡死在等待里。这个细节后面我会在代码里体现。2. 硬件连接中容易忽略的几个点2.1 最小接线方案LSM6DSVE的硬件连接不复杂基本就是供电加I2C两根线。建议按下面这张表来接线别嫌简单就乱飞线后面很多诡异问题都出在连接细节上。传感器引脚连接到说明VDD3.3V供电范围一般在1.71V到3.6V具体看手册GNDGND共地必须可靠SCL主控I2C1_SCL接上拉电阻到VDDIOSDA主控I2C1_SDA接上拉电阻到VDDIOSA0GND决定I2C地址接GND地址为0x6ACSVDDIOI2C模式下必须拉高禁用SPIINT1/INT2暂不接后续中断模式再用上拉电阻要特别注意。I2C的SCL和SDA是开漏输出必须外部上拉。典型值4.7kΩ在400kHz下能用如果总线上设备多、走线长换成2.2kΩ会更稳。很多人把传感器模块插上发现通信时好时坏结果一量波形上升沿缓慢得像乌龟爬这就是上拉电阻选大了。我的实际经验是调试初期能不用杜邦线就不用能焊就焊或者直接做一个小转接板。杜邦线在I2C 400kHz下长距离传输带来的寄生电容很烦人会直接影响时序。如果你只能飞线把线控制在10厘米以内还不行就先把I2C时钟降到100kHz跑通逻辑再慢慢提升频率。2.2 I2C地址的确认方法LSM6DSVE的7位I2C地址由SA0引脚电平决定。SA0接GND时地址是0x6A接VDDIO时是0x6B。这里有个地方非常容易踩坑HAL库函数的DevAddress参数要的是左移一位后的8位地址也就是0x6A要左移一位变成0xD4。我见过不止一个人直接把0x6A填进去结果逻辑分析仪一看总线地址发的是0x6A配合读写位实际寻址到了0x35传感器当然不会应答。正确的做法是在代码里定义宏的时候直接左移#define LSM6DSVE_I2C_ADDR (0x6A 1)这样后面传给HAL_I2C_Mem_Read的都是0xD4读写方向由HAL库内部控制不会把方向搞混。如果你用逻辑分析仪抓I2C波形会看到主机先发出0xD4写方向指定寄存器地址再切到0xD5读方向取数据这个顺序是正常的。还有一颗引脚容易被忽略CS引脚。I2C模式下CS必须接高电平否则传感器可能进入SPI从机模式I2C总线就完全没有应答。这个问题在买现成模块时一般处理好了但如果你是自己在底板上焊芯片务必检查这一脚的电平。2.3 供电和去耦经验LSM6DSVE的数据手册里工作电压范围比较宽1.71V到3.6V都能跑。但我要说的是能稳定跑和跑得稳定是两码事。传感器内部有MEMS结构和模拟前端电源纹波会直接影响陀螺仪输出噪声。实测中我发现如果VDD引脚上只有一个大电容没有高频去耦静止状态下的数据跳动会比正常值大出不少。常规做法是VDD引脚旁边放一个100nF的MLCC尽量靠近芯片引脚走线要短。如果板上空间允许再并联一个1uF到10uF的电容放到稍远的位置用于吸收低频波动。传感器模块上一般会预置这些电容但如果你是自己画板子千万别省。另外就是供电地线的处理。传感器不要跟电机、继电器这类大电流负载共用一根电源走线开启瞬间的压降和反冲会把I2C通信直接搞挂。我调试时遇到过一次数据偶发跳变排查半天最后发现是旁边一个继电器动作时把VDD拉出了毛刺给传感器单独加了RC滤波才解决。3. 初始化寄存器时我在想什么3.1 WHO_AM_I是第一道防线LSM6DSVE上电之后第一步不是急着配置各种寄存器而是读WHO_AM_I寄存器它的地址是0x0F。这个寄存器保存着芯片的固定识别码只要通信链路正常读到的值就应该等于数据手册里给的Device ID。我习惯在初始化函数的第一步就做这个校验不通过就直接返回错误码避免后续所有操作都建立在一个不可信的通信链路上。uint8_t lsm6dsve_init(void) { uint8_t val 0; if (HAL_I2C_Mem_Read(hi2c1, LSM6DSVE_I2C_ADDR, LSM6DSVE_REG_WHO_AM_I, 1, val, 1, 100) ! HAL_OK) { return 1; } if (val ! LSM6DSVE_WHO_AM_I_VALUE) { return 1; } // 后续配置... return 0; }如果这一步返回失败不要急着改代码先按这个顺序排查第一量一下传感器VDD和GND之间有没有正常的电压第二确认SA0引脚电平是否跟代码里的地址一致第三示波器或逻辑分析仪挂上看SCL/SDA有没有波形有没有ACK应答第四检查CS引脚是不是被拉低了。我见过90%的通信失败都出现在这四类问题上而不是寄存器配错。3.2 CTRL3_C里的BDU和IF_INC必须开CTRL3_C寄存器地址是0x12这里有两个位对后面读取数据至关重要BDU和IF_INC。BDU是块数据更新英文是Block Data Update。陀螺仪数据在寄存器里是16位有符号数拆成高8位和低8位两个寄存器。如果不开启BDU当主机正在读低8位的时候恰好传感器产生新数据写入了高8位那么你读到的高低字节就不是同一时刻的样本组合出来的数据会是个“撕裂”的垃圾值。IF_INC是寄存器地址自动递增。开启之后当从OUTX_L_G开始连续读多个寄存器时I2C设备内部会自动把寄存器地址往后加主机只需要发起一次读事务就能把六个数据字节连续读回来。如果不开这个位你必须分别发起六次I2C读取每次指定一个寄存器地址时间和代码量都上去了。我把CTRL3_C写成0x44对应的二进制是0100 0100bit6置1表示开启BDUbit2置1表示开启IF_INC。代码里最好用宏或注释标明每个位的含义不要只留下一个魔术数字。uint8_t cfg 0x44; // bit6: BDU1, bit2: IF_INC1 HAL_I2C_Mem_Write(hi2c1, LSM6DSVE_I2C_ADDR, LSM6DSVE_REG_CTRL3_C, 1, cfg, 1, 100);很多人一开始只开了IF_INC而忘了BDU结果读出来的数据偶尔会跳一下特别像软件bug实际上就是数据撕裂。3.3 CTRL2_G选择合适的ODR和量程陀螺仪的配置主要在CTRL2_G寄存器里地址0x11。高四位控制输出数据速率ODR低三位控制满量程FS。我在这颗传感器上初始化的值选的是0x53。拆开来看高四位0101对应ODR为104Hz低三位011对应满量程正负2000dps。之所以选择104Hz是因为后面做姿态显示或者手势识别时这个采样率已经足够反映人的正常动作又不至于让I2C轮询太频繁。量程选正负2000dps是因为这个阶段还不知道最终应用场景先把量程拉满保证任何快速转动都不会削波等跑通了根据实际运动范围再缩小量程提升分辨率。CTRL2_G的ODR和FS配置我整理成了一张速查表注意不同系列传感器定义略有差异以LSM6DSVE数据手册为准。ODR配置值输出速率FS配置值满量程0x11.6Hz0x0125dps0x212.5Hz0x1250dps0x326Hz0x2500dps0x452Hz0x31000dps0x5104Hz0x42000dps写入代码如下cfg 0x53; // ODR104Hz, FS±2000dps HAL_I2C_Mem_Write(hi2c1, LSM6DSVE_I2C_ADDR, LSM6DSVE_REG_CTRL2_G, 1, cfg, 1, 100);这里有个很容易忽略的点陀螺仪在写完CTRL2_G且ODR非零之后才开始真正工作之前默认处于掉电模式。如果你忘记配这个寄存器状态寄存器的GDA位永远不会置1轮询就会一直超时。如果你只想用加速度计也可以把CTRL2_G保持默认值不主动启动陀螺仪这样还能省点功耗。3.4 初始化顺序的讲究初始化顺序看起来是小事但确实有讲究。我比较习惯先把CTRL3_C这种全局功能开关配好再去配ODR和量程。理由是让传感器先保持在一个确定的状态再让它跑起来避免它用默认参数先跑一拍后面你改配置时又产生一次不必要的状态跳变。如果你打算把加速度计也关掉省电可以在初始化末尾往CTRL1_XL写0x00让加速度计进入掉电模式。本文只读陀螺仪加速度计留着不用实际会多消耗一点电流写入0会成为一个更环保的配置。整个初始化函数写完大概是这个状态uint8_t lsm6dsve_init(void) { uint8_t val 0; uint8_t cfg 0; if (HAL_I2C_Mem_Read(hi2c1, LSM6DSVE_I2C_ADDR, LSM6DSVE_REG_WHO_AM_I, 1, val, 1, 100) ! HAL_OK) { return 1; } if (val ! LSM6DSVE_WHO_AM_I_VALUE) { return 1; } cfg 0x44; HAL_I2C_Mem_Write(hi2c1, LSM6DSVE_I2C_ADDR, LSM6DSVE_REG_CTRL3_C, 1, cfg, 1, 100); cfg 0x53; HAL_I2C_Mem_Write(hi2c1, LSM6DSVE_I2C_ADDR, LSM6DSVE_REG_CTRL2_G, 1, cfg, 1, 100); cfg 0x00; HAL_I2C_Mem_Write(hi2c1, LSM6DSVE_I2C_ADDR, LSM6DSVE_REG_CTRL1_XL, 1, cfg, 1, 100); return 0; }4. 轮询读取的核心实现4.1 先读状态位而不是直接读数据初始化完成之后进入主循环就面临一个选择是直接去读数据寄存器还是先看状态寄存器直接读数据的问题在于LSM6DSVE的ODR是104Hz相当于9.6ms才产生一个新样本而主循环可能几百微秒就转一圈。如果不去看状态位大部分时间读到的都是同一个还没更新的旧数据而且你没法判断数据有没有更新。更糟的是如果没有状态位做同步你甚至不知道读到的数据是哪一时刻的这在做姿态解算时会产生很大的相位误差。正确做法是循环读取STATUS_REG它的地址是0x1E。这个寄存器的bit1是GDA表示陀螺仪数据已准备好置1说明新的角速度样本已经写入输出寄存器。等到GDA为1再去读OUTX_L_G等一系列寄存器这样拿到的数据就是最新的。uint8_t lsm6dsve_read_gyro(int16_t *gx, int16_t *gy, int16_t *gz) { uint8_t status 0; uint8_t buf[6]; uint8_t timeout 10; do { if (HAL_I2C_Mem_Read(hi2c1, LSM6DSVE_I2C_ADDR, LSM6DSVE_REG_STATUS, 1, status, 1, 100) ! HAL_OK) { return 1; } if (status 0x02) { break; } HAL_Delay(1); } while (--timeout); if ((status 0x02) 0) { return 1; // 超时仍未等到数据 } // 继续读六字节数据... return 0; }timeout一定要加。因为轮询本质是等待如果传感器初始化失败或通信异常没有超时保护的程序会在这个循环里一直空转到天荒地老看门狗都救不回来。我这里给的超时是10ms级别远大于104Hz对应的9.6ms周期正常情况下足够等到新的GDA标志。4.2 连续读六字节的两种写法由于初始化时开启了IF_INC从OUTX_L_G寄存器开始连续读取六个字节是最高效的方式。陀螺仪三个轴各占16位分别是OUTX_L_G、OUTX_H_G、OUTY_L_G、OUTY_H_G、OUTZ_L_G、OUTZ_H_G起始地址0x22。写法一是不开IF_INC时的笨办法分别读六个寄存器每次都要发起一次I2C事务指定寄存器地址、等应答、读数据。这样不仅代码长I2C总线上的事务数量也多耗时是连续读的好几倍。写法二是利用IF_INC直接从0x22发起一次读事务HAL库的HAL_I2C_Mem_Read会连续读6字节内部寄存器地址自动递增。这是最推荐的做法。HAL_I2C_Mem_Read(hi2c1, LSM6DSVE_I2C_ADDR, LSM6DSVE_REG_OUTX_L_G, 1, buf, 6, 100);这段代码执行完之后buf[0]到buf[5]依次对应X低字节、X高字节、Y低字节、Y高字节、Z低字节、Z高字节。注意HAL库在连续读取时多字节读之间是有I2C继续发送时钟的这对传感器来说是标准操作不需要担心。如果你没有逻辑分析仪但怀疑IF_INC没有生效可以先单读一个寄存器试试。比如单独读OUTX_H_G看返回值是不是正常范围。如果单独读正常但连续读错位基本就是IF_INC的配置被覆盖了。4.3 数据组合的细节六字节拿到手之后要把高低字节拼成三个有符号16位整数。LSB系列传感器的输出是小端模式低字节在前高字节在后所以组合方式是左移八位再按位或。*gx (int16_t)((buf[1] 8) | buf[0]); *gy (int16_t)((buf[3] 8) | buf[2]); *gz (int16_t)((buf[5] 8) | buf[4]);这里必须强调用int16_t而不是uint16_t。陀螺仪输出是有符号数正方向转动和负方向转动分别对应正数和负数。如果定义成无符号负的半轴数据在转换时就会变成65536减原始值后面再除以灵敏度角速度就完全错了。如果你的主循环里既有轮询读取又有中断里访问同一组变量记得给变量加volatile修饰防止编译器把它们优化进寄存器导致读不到最新值。不过初版代码建议先只在主循环里用减少变量。5. 把原始值变成可用的角速度5.1 LSB与dps的换算关系读取到的原始数据是无量纲的ADC码值真正使用时需要转换成角速度单位是dps也就是每秒转过多少度。换算关系跟量程直接相关。LSM6DSVE内部是16位ADC满量程正负2000dps时对应的数字量范围是负32768到正32767接近4000dps的量程宽度。所以一个LSB代表的角速度约为4000除以65536约等于0.061dps。换算成工程上常用的灵敏度表示法就是每dps对应16.4个LSB。不同量程下的灵敏度我列在表格里方便后面改配置时直接查。满量程dps灵敏度LSB/dps每LSB对应角速度dps125262.40.0038250131.20.007650065.60.0153100032.80.0305200016.40.0610换算公式就是把原始值除以灵敏度float gx_dps gx_raw / 16.4f;如果你的代码为了省CPU开销不想用浮点也可以保留整数但要注意后续做积分时分辨率不够的偏差会积累建议至少用float。5.2 静止时验证零点传感器初始化和读取都做完之后第一件事把板子平放在桌面上静止不动打印陀螺仪数据。理想情况下三个轴都应该接近0dps但现实中会有一定偏差。我在实测中看到过X轴稳定输出20到30个LSB换算成角速度大约1.2到1.8dps的偏移。这个偏差来源很复杂有MEMS结构本身的零偏有焊接应力还有温度漂移。静止时看到这个现象不能说明传感器坏了关键在于判断它是固定偏移还是随机噪声。固定偏移可以通过软件校准消除上电后静止采集100个样本求平均得到一个零偏之后每次读取都减去这个值。随机噪声部分就要靠滤波或者更高级的姿态融合算法来处理了。这里有个经验不要一看到静止数据不是0就开始折腾滤波。先用足够长的数据观察零偏是否稳定如果是因为板子没放平或重心偏移导致的常值偏置滤波是滤不掉的得从安装上解决。5.3 轮询读取与滤波的边界滤波这个话题很容易跟轮询读取混在一起。我的建议是先把原始数据读出来打出来确认通信和转换逻辑没有问题了再考虑要不要滤波。很多人上来就上一堆滤波算法结果数据不对也分不清是传感器问题、通信问题还是滤波参数问题。如果要做最简单的数据平滑一阶低通滤波就够了float filtered 0; void filter_update(float raw) { filtered 0.8f * filtered 0.2f * raw; }系数0.2表示每次新数据的权重数值越小滤波效果越强但响应也越慢。具体选多少要根据你的应用场景调整。做姿态解算的话原始数据直接喂给姿态算法往往比自己乱滤更好因为很多算法内部对噪声有建模。6. 实测遇到的两个问题和定位思路6.1 WHO_AM_I读到全FF第一次上板调试我遇到的情况是WHO_AM_I读回来0xFF总线上看起来是传感器根本没回应。当时我第一反应是接线问题但量了电压正常SA0也接GNDCS也拉高了逻辑分析仪挂上去能看到主机的写地址和寄存器地址就是等不到ACK。后来排查发现是杜邦线太长I2C速率跑在400kHz时SDA线上的上升沿变得很平缓传感器那边根本识别不了有效电平。处理办法很粗暴把I2C时钟降到100kHz再读ACK就出来了。等到代码全部跑通之后我重新优化了接线把杜邦线换成短跳线才恢复400kHz。这里想强调一下排查思路。I2C通信失败先分清是“无应答”还是“有应答但数据错”。无应答代表设备不在总线上或者总线物理条件不满足数据错则更多是地址、寄存器、位宽配置问题。逻辑分析仪是排查这类问题最有效的工具别只会用万用表。6.2 数据偶发跳变的根源初始化成功、数据也能正常打印之后我又遇到一个问题静止时陀螺仪数据大部分时间很稳但每隔几秒会突然跳一下幅度能有几十个dps然后又恢复正常。开始我怀疑是BDU没开导致的高低字节撕裂但检查配置发现BDU确实置位了。于是怀疑是不是读取过程中被中断打断导致状态位和数据读取错位。排查后发现真正原因是电源问题。我的传感器模块用一个USB供电的测试板带动旁边接了一个继电器负载继电器吸合瞬间VDD上出现明显的电压跌落逻辑分析仪上能看到I2C时钟和数据线上有毛刺。给传感器供电单独加了100nF去耦电容和磁珠之后跳变就消失了。这也算是个典型教训IMU这类模拟敏感器件对电源质量的要求比数字芯片高。如果你也遇到数据随机跳变先别怀疑传感器芯片和软件先把示波器挂到VDD引脚上看纹波排除供电因素之后再往寄存器方向查。6.3 轮询方式的开销和下一步演进最后算一笔账。I2C通信在400kHz模式下每传输一个字节约需要9个SCL时钟加上起始位、寄存器地址、应答位等开销读6字节数据大概一百多微秒。LSM6DSVE在104Hz ODR下数据周期约9.6ms轮询带来的CPU占用不到2%。这个余量说明在中等采样率下轮询完全没有性能压力。什么时候需要换方案呢大致三个信号第一ODR跑到833Hz甚至更高轮询频繁且容易丢数据第二系统里还有大量其他任务CPU不能长时间等I2C第三要进入低功耗模式CPU大部分时间应该睡觉而不是周期醒来轮询。出现这些情况就需要用中断代替轮询或者直接读FIFO批量取数再进一步就是DMA搬运。我个人习惯的开发路径一直是先轮询跑通数据链路再加中断降低CPU占用最后才上FIFO和DMA。轮询虽然看起来原始但它能逼着你把每个寄存器的行为搞清楚后面加任何特性都是在一条真实可行的代码路径上做增量而不是在黑盒子里猜。下一次我打算在C5上把LSM6DSVE的中断和FIFO方式也走一遍到时候再把对比数据发出来。
返回列表