十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

STM32C5驱动LSM6DSOW:基于I2C轮询读取陀螺仪数据详解

STM32C5驱动LSM6DSOW:基于I2C轮询读取陀螺仪数据详解 最近在评估STM32C5系列这批新片子正好手头有一颗LSM6DSOW六轴传感器就搭了个最小验证工程先把陀螺仪用轮询方式读出来。这篇是系列第一篇目标定得很低不碰中断、不碰FIFO、不碰融合算法只把I2C通信、WHO_AM_I校验、寄存器配置、STATUS_REG状态位轮询、最后拿到原始数据换算成dps这一整套流程跑通。适合刚会用STM32的HAL库、想正经调一颗IMU的开发者也适合以前调过LSM6DSO系列、现在想快速切到LSM6DSOW的人。轮询这个词在不同领域长得不太一样但本质都是“一遍遍主动问”。PLC做Modbus主站时上位机得一轮轮去读从站寄存器问一次拿一次FPGA里的轮询仲裁器也是挨个看谁在申请总线。传感器这边也是一样的思路我隔一会儿读一次STATUS_REG看GDA位有没有变成1变成1说明陀螺仪的输出寄存器已经刷新好了这时候把OUTX_L_G到OUTZ_H_G一口气读回来就是当前时刻的角速度原始值。相比中断和DMA轮询谈不上高效但胜在逻辑一眼看穿、出问题好排查作为驱动移植的第一步再合适不过。1. 项目概述与整体设计思路1.1 为什么选STM32C5和LSM6DSOW这个组合STM32C5是ST新一代主打性能与安全的MCU系列内核换成了Arm Cortex-M33主频最高可以拉到250MHz带TrustZone、DSP指令和硬件浮点单元。对传感器应用来说这些特性有两点实实在在的好处一是运算能力强后面想跑姿态解算、卡尔曼滤波这类算法时M33配合FPU会轻松很多二是低功耗表现和丰富的外设接口IRQ引脚、I2C、SPI、UART该有的都有很适合做可穿戴设备或工业传感节点这类场景。LSM6DSOW则属于ST iNEMO家族里的低功耗六轴IMU三轴陀螺仪加三轴加速度计I2C和SPI接口都支持工作电压范围1.71V到3.6V可以直接用3.3V供电。它最吸引人的地方是功耗控制低功耗模式下比老一代传感器省不少适合TWS耳机、门锁、运动手表这种电池供电的产品。寄存器兼容性也做得很好如果你之前摸过LSM6DSO、LSM6DSOX或者LSM6DSR会发现操作方式几乎一样代码迁移成本很低。这个组合放到一起性能上完全够用甚至可以说有点“大马拉小车”。但我的原则是先把最简单的路子走通所以不急着上高级外设直接用最基础的I2C接口加轮询方式把数据读回来。后面想升级到中断、DMA、FIFO也都是在这个基础上做增量不会推翻重来。1.2 为什么第一步用轮询而不是中断或DMA很多初学者一上来就琢磨DMA加中断觉得这才显得专业。我个人的建议是如果你还不确定传感器能不能正常出数千万别直接上复杂机制。轮询方式虽然笨但有一个天然优势它能直观地证明“传感器在工作、数据在更新”。你可以在主循环里一直读状态寄存器串口打印数据看到数值随着板子转动而变化整个链路就验证通过了。中断方式当然好但多引入了一个GPIO引脚和外部中断配置出现问题时分不清是数据没更新、中断没触发还是配置写错。DMA加中断的效率最高但调试时的复杂度直线上升一旦数据没来你得从DMA配置、中断优先级、数据缓冲区一层层排查。先把轮询跑通再切换到中断最后再上DMA这是一个非常平滑的学习路径。从数据实时性角度来说轮询的代价是CPU会被占住等待。如果ODR配成104Hz也就是每条数据间隔约9.6ms轮询等待的时间很短主循环依然有大量余量做其他事情。等以后ODR往1kHz以上提或者要同时处理多个传感器再考虑用中断和DMA去释放CPU不迟。2. 硬件接线与CubeMX工程配置2.1 开发板接线和关键引脚处理STM32C5的部分引脚和传感器之间接线形式上跟普通I2C设备一样但有几个细节很容易踩坑我一个个说。LSM6DSOW选择的通信接口跟CS引脚的电平有关。CS引脚拉低会进入SPI模式拉高才是I2C模式。很多人第一次调这个传感器程序怎么改都读不到WHO_AM_I最后发现CS引脚悬空了芯片在SPI模式下根本不响应I2C时序。所以接线时务必把CS引脚接到VDD而不是悬空。SA0引脚决定I2C从机地址。SA0接GND时7位地址是0x6ASA0接VDD时地址是0x6B。本文默认SA0接地所以地址用0x6A左移一位后的8位写地址是0xD4读地址是0xD5。SCL和SDA引脚上必须有上拉电阻。不少开发板的I2C引脚已经带了上拉但如果你用的是核心板加杜邦线方案建议外接两个4.7kΩ电阻分别到3.3V。没有上拉I2C可能出现通信不稳定、偶尔读错寄存器的问题。VDD旁边我习惯放一个0.1μF和一个10μF电容做去耦尤其是用手工面包板搭电路的时候供电纹波会直接影响陀螺仪零偏。我用的连接关系如下表供参考LSM6DSOW引脚STM32C5引脚说明VDD3.3V供电GNDGND共地SCLPB8 (I2C1_SCL)4.7kΩ上拉SDAPB9 (I2C1_SDA)4.7kΩ上拉SA0GND地址选择0x6ACS3.3V强制I2C模式INT1暂不接后续中断篇使用2.2 CubeMX里的外设配置打开STM32CubeMX选择对应的STM32C5型号先配置调试接口为Serial Wire方便烧录和调试。I2C的配置其实没多少玄学。选择I2C1Speed Mode选Fast Mode时钟频率设成400kHz其余保持默认。LSM6DSOW在I2C模式下最高支持1MHz400kHz是很稳妥的选择。I2C地址长度不需要在这里配置HAL库的Mem_Read和Mem_Write接口会自己处理8位寄存器地址。再配一个串口用于打印数据我习惯用USART1或USART2波特率1152008位数据位无校验1位停止位。CubeMX里把USART的一根发送脚复用配置配好即可。printf重定向这点容易遗漏需要在代码里实现fputc否则HAL库自带的printf没法打印到串口。时钟树上只要确保I2C外设时钟来源正确、APB总线时钟不超过外设上限即可。如果不太确定先让CubeMX的时钟自动解算方案生成一般不会出问题。CubeMX的代码生成设置里记得勾选Generate peripheral initialization as a pair of .c/.h files per peripheral这样生成的代码结构更清晰HAL句柄在对应外设源文件里定义方便我们在驱动文件里用extern引用。3. LSM6DSOW关键寄存器逐个拆3.1 WHO_AM_I通信是否正常的试金石所有I2C传感器调试的第一步永远是读WHO_AM_I寄存器。LSM6DSOW的WHO_AM_I地址是0x0F正常情况下读出来的值固定为0x6C。我习惯在初始化的第一步就做这件事并且做两层判断第一层检查I2C通信本身是否成功也就是HAL_I2C_Mem_Read的返回值是否为HAL_OK第二层检查读回来的值是否等于0x6C。如果第一层失败说明接线或地址有问题如果第二层失败说明通信链路通了但对方可能不是LSM6DSOW或者芯片进入了SPI模式。很多代码示例喜欢用死循环卡在ID校验那里我建议改成带失败返回的形式把错误信息传给上层。这样在串口调试时能明确知道是哪个环节挂了而不是整个程序卡死在初始化里。3.2 CTRL1_XL和CTRL2_G速率和量程怎么配加速度计和陀螺仪的控制寄存器分别为CTRL1_XL0x10和CTRL2_G0x11。两个寄存器的高4位都是输出数据速率ODR配置陀螺仪的量程位在CTRL2_G的bit3:2加速度计的量程位在CTRL1_XL的bit3:2。陀螺仪的ODR有如下常用配置ODR_G值输出速率0000关闭001126 Hz010052 Hz0101104 Hz0110208 Hz0111416 Hz1000833 Hz加速度计的ODR配置码与陀螺仪基本一致但低功耗模式下支持1.6Hz这个细节在后续做低功耗设计时会用到。本文的项目里我把加速度计和陀螺仪都配成104Hz既不会太快导致轮询跟不上也不会太慢导致手感发卡。陀螺仪量程选择我用的±2000dps对应CTRL2_G的FS_G两位设为11。这样做的原因是在调试初期你无法保证板子会不会被猛地翻转大范围量程可以避免数据饱和。大范围量程的代价是灵敏度变低±2000dps时一个LSB对应70mdps而±250dps时一个LSB只对应8.75mdps。后续做精细测量时再根据实际应用改小量程即可。FS_125这个位需要单独说明一下。在LSM6DSOW里把CTRL2_G的bit1置1量程会强制切换到±125dps此时FS_G两位的设置会被覆盖。我在配置时特意把FS_125保持为0避免意想不到的量程切换。3.3 STATUS_REG轮询标志与数据寄存器数据就绪标志位在STATUS_REG0x1E里。bit0是XLDA对应加速度计bit1是GDA对应陀螺仪bit2是TDA对应温度。每个ODR周期内对应数据刷新完成后标志位置1读取完输出寄存器后自动清零。轮询的核心逻辑就是循环读STATUS_REG判断bit1是不是1。如果GDA保持0说明陀螺仪数据还没有更新可能是ODR没配好也可能是芯片进入了掉电模式。陀螺仪数据放在6个连续寄存器里起始地址是OUTX_L_G0x22依次是OUTX_L_G、OUTX_H_G、OUTY_L_G、OUTY_H_G、OUTZ_L_G、OUTZ_H_G每个轴16位低字节在前。读取时如果IF_INC为1地址会自动递增可以一次性读6个字节不用分三次读。这里有个非常关键的细节高低字节是分两次进I2C的在读取这几微秒内传感器可能已经刷新了下一组数据。如果运气不好可能出现新数据的一半和旧数据的一半拼在一起产生毛刺。解决办法是把CTRL3_C的BDU位置1开启块数据更新让高低字节在数据更新期间保持不变等读完了再更新。驱动里我每次都把BDU和IF_INC一起配置就是为了避免这类问题。3.4 数据换算的数学原理寄存器读出来的是16位有符号整数补码格式范围从-32768到32767。它并不是最终的物理量要除以灵敏度或者乘以灵敏度系数才能得到度数每秒。陀螺仪各量程下的灵敏度如下量程灵敏度±125 dps4.375 mdps/LSB±250 dps8.75 mdps/LSB±500 dps17.5 mdps/LSB±1000 dps35 mdps/LSB±2000 dps70 mdps/LSB如果你配的是±2000dps那么角速度dps 原始值 × 0.070。举个例子读到的原始值是1000那么角速度就是70dps。这个0.070就是70mdps/LSB只是把单位从mdps换成了dps。有些人会觉得应该除以14.286也能得到一样的结果。不管是乘还是除关键在于数据手册的灵敏度表必须记牢。加速度计这边逻辑完全一致只是保留到小数点后更精细±2g时灵敏度是0.061mg/LSB我一般会在代码里用float类型做计算。4. 轮询获取陀螺仪数据的完整代码实现4.1 I2C读写寄存器函数封装驱动代码我习惯单独建一个lsm6dsow.c和lsm6dsow.h跟主逻辑分开。底层读写直接调用HAL库接口封装成两个静态函数上层只关心寄存器地址和要读写的字节。HAL_I2C_Mem_Read和HAL_I2C_Mem_Write的最后一个参数是超时时间我习惯设100ms。传感器不响应时HAL库会在超时后返回错误而不是永远挂死。这在调试阶段很重要能让你在串口看到“读失败”而不是整个程序卡住。static HAL_StatusTypeDef lsm6dsow_read_reg(uint8_t reg, uint8_t *buf, uint16_t len) { return HAL_I2C_Mem_Read(hi2c1, LSM6DSOW_I2C_ADDR, reg, I2C_MEMADD_SIZE_8BIT, buf, len, 100); } static HAL_StatusTypeDef lsm6dsow_write_reg(uint8_t reg, uint8_t data) { return HAL_I2C_Mem_Write(hi2c1, LSM6DSOW_I2C_ADDR, reg, I2C_MEMADD_SIZE_8BIT, data, 1, 100); }这里有一个细节I2C地址用的是8位地址格式所以0x6A要左移一位变成0xD4。如果你写成0x6AHAL库会认为你要访问地址0x35通信肯定失败。这种低级错误我犯过不止一次写驱动第一件事就是把地址宏定义好。4.2 初始化流程初始化函数按照“读ID、软复位、配BDU/IF_INC、配ODR和量程”的顺序执行。这个顺序是有讲究的软复位之后所有寄存器都会回到上电默认值所以必须先复位再配置。uint8_t LSM6DSOW_Init(void) { uint8_t id 0; if (lsm6dsow_read_reg(LSM6DSOW_REG_WHO_AM_I, id, 1) ! HAL_OK) { return 0; } if (id ! LSM6DSOW_WHO_AM_I_VALUE) { return 0; } /* 软件复位 */ lsm6dsow_write_reg(LSM6DSOW_REG_CTRL3_C, 0x01); HAL_Delay(50); /* BDU1IF_INC1 */ lsm6dsow_write_reg(LSM6DSOW_REG_CTRL3_C, 0x06); /* 陀螺仪104Hz±2000dps */ lsm6dsow_write_reg(LSM6DSOW_REG_CTRL2_G, (0x05 4) | (0x03 2)); /* 加速度计104Hz±2g */ lsm6dsow_write_reg(LSM6DSOW_REG_CTRL1_XL, (0x05 4) | (0x00 2)); return 1; }CTRL3_C写0x01触发软复位后我特意延时50ms给芯片内部重启留足时间。如果复位后立刻写其他寄存器有可能被复位操作覆盖导致配置丢失。这个延时从几十毫秒到一百毫秒都可以只要在后续读写前把等待留够。4.3 轮询读取陀螺仪数据读取函数的逻辑就是前面说的循环读STATUS_REG直到GDA为1然后把6个数据寄存器一口气读回来拼成三个int16_t。void LSM6DSOW_Read_Gyro(int16_t *gyro_x, int16_t *gyro_y, int16_t *gyro_z) { uint8_t buf[6]; uint8_t status 0; do { lsm6dsow_read_reg(LSM6DSOW_REG_STATUS_REG, status, 1); } while ((status 0x02) 0); lsm6dsow_read_reg(LSM6DSOW_REG_OUTX_L_G, buf, 6); *gyro_x (int16_t)((buf[1] 8) | buf[0]); *gyro_y (int16_t)((buf[3] 8) | buf[2]); *gyro_z (int16_t)((buf[5] 8) | buf[4]); }拼接高低字节时我用了位移再相或buf[1]是高字节buf[0]是低字节所以是(buf[1] 8) | buf[0]。强转成int16_t是因为符号位必须保留如果直接赋值给int16_t也行但写上更明确。轮询等待循环里没有加超时保护正常情况下GDA在几个微秒内就会置位。但如果ODR配置错了芯片完全不更新数据这个循环会变成死循环。我在调试阶段会临时加一个循环计数超过某个次数就跳出并打印错误确认没问题后再去掉。4.4 主循环里计算和打印数据读回来只是原始值我在主循环里换算成dps再通过串口输出。串口重定向后printf就可以直接用了。int16_t gx 0, gy 0, gz 0; while (1) { LSM6DSOW_Read_Gyro(gx, gy, gz); float dps_x (float)gx * 0.070f; float dps_y (float)gy * 0.070f; float dps_z (float)gz * 0.070f; printf(GX:%8.2f GY:%8.2f GZ:%8.2f dps\r\n, dps_x, dps_y, dps_z); HAL_Delay(10); }主循环的HAL_Delay(10)是为了把打印频率控制在100Hz左右。这里有个逻辑要理清LSM6DSOW在104Hz的ODR下大约9.6ms更新一次数据GDA置位后我立刻读取数据是新鲜的HAL_Delay(10)只是为了不让串口打印刷爆而不是为了等数据。如果把ODR改成1kHzHAL_Delay还保持10ms那你每次循环其实读到的都是最新一次的数据中间的数据会被丢掉这在轮询模式天然存在后续用FIFO才能解决。另外打印浮点数时串口端的软件比如串口助手会占用一定时间。调试阶段没关系但如果后续要做数据记录或者给上位机实时馈送建议把格式化成字符串或者直接发二进制原始值能省下不少带宽。5. 踩坑记录与排查思路5.1 WHO_AM_I读不到程序卡死在初始化这是我最常碰到的问题。排查方向按优先级排序先看CS引脚有没有拉高再看I2C地址是不是正确再看上拉电阻有没有接最后用示波器或逻辑分析仪抓SCL/SDA波形确认时序。CS和SA0出问题的概率最大。CS悬空会让芯片进入SPI模式I2C根本不响应SA0接错会把地址从0x6A变成0x6B程序读0x6A自然找不到设备。如果你用万用表量SDA上有没有波形只能判断有通信动作不能判断地址对不对最好用逻辑分析仪看I2C从机地址帧。HAL_I2C_Mem_Read返回HAL_ERROR而不是HAL_TIMEOUT时多半是地址或应答位问题。返回HAL_TIMEOUT则大概率是SCL或SDA被卡死比如某个引脚被意外配置成了其他功能。5.2 读到的数据永远不变代码能读到数据但不管怎么晃动板子数值纹丝不动。我遇到过一次原因是ODR没有配成功。CTRL2_G的ODR_G位如果写成了0陀螺仪保持在Power Down模式STATUS_REG的GDA位永远不会置位读取函数会卡死如果读出来的是某个固定值但GDA偶尔置位可能是量程和灵敏度不匹配数值变化被缩放得很小视觉上像没变。还有一种情况寄存器写时序错了数据实际写到了别的配置位置传感器完全没启动。处理办法是初始化完成后把CTRL2_G和CTRL1_XL再读回来打印确认写入值和回读值一致。这也是调试I2C设备的通用技巧先回读配置再读数据。5.3 静止时数值不是零而且跳得厉害陀螺仪静止时输出不为零这是物理特性叫零速率偏差不是传感器坏了。LSM6DSOW的零偏在几dps到几十dps之间量程越大时LSB对应的物理量越大观测到的原始值跳动会更明显。如果你发现静止时原始值就有几千甚至接近满量程那就要检查电源纹波和地线。面包板供电的纹波会直接耦合到传感器内部导致陀螺仪输出明显漂移。我调这类传感器时只要条件允许都会把传感器放在一个小转接板上用杜邦线连接时尽量缩短SCL和SDA长度电源线加电容滤波。另外BDU没配置时偶尔会出现某个轴的高字节和低字节来自不同采样周期产生瞬态跳变。这种现象会在静止时表现为偶尔冒出一个很大的尖峰。把BDU置1后尖峰基本消失。5.4 轮询等待和打印阻塞造成主循环卡顿有人会把轮询的等待循环和串口打印混在一起导致主循环实际频率低得可怜。在低ODR下感觉不明显但ODR提高到416Hz或833Hz后每次循环都要等数据、打印、再等数据打印时间长的话主循环根本跟不上传感器的刷新速度。我的建议是数据读取和打印解耦。读取函数只负责获取最新一帧数据打印放在另一个低频逻辑里比如每100ms打印一次。这样轮询等待的时间窗口被压缩得很短主循环剩下的时间可以用来执行其他任务也为后续往中断和DMA迁移打了基础。6. 数据验证和后续能玩的方向6.1 用串口数据快速验证传感器方向拿到轮询数据后第一件事不是急着看数值误差而是验证轴方向对不对。把板子平放Z轴应该能测到约9.8m/s²的重力加速度陀螺仪的X、Y、Z在一段时间内应该接近0。用手指拨动板子绕X轴旋转GX的读数会有明显变化方向跟你拨动的方向对应。我做这个验证时习惯编写一个简单的串口波形显示脚本把三轴数据画成三条曲线。看到曲线随板子运动而起伏整个链路才算真正打通。没有脚本的话直接在串口助手里看数值变化也能判断只是没那么直观。6.2 下一步中断、FIFO和六轴融合轮询这篇到这里第一版驱动已经可以用了。但轮询毕竟只在数据量小的时候合适只要你开始往外接第二个传感器、跑无线协议栈、或者提高ODRCPU就会被拖累。下一阶段我计划把INT1引脚配成数据就绪中断在回调里读取数据这样可以彻底释放主循环。再往后就是启用FIFO让传感器自己攒数据攒满一包再整包读出适合低功耗场景。陀螺仪和加速度计都有了之后姿态解算也提上了日程。最入门的是互补滤波和Mahony算法在STM32C5的M33内核上跑绰绰有余。到时候再把Yaw、Pitch、Roll算出来就能做一些实际应用了。我在实际把玩LSM6DSOW的过程中最大的体会是这颗芯片的寄存器设计其实比想象中简单难点往往不在芯片本身而在你是不是能每一步都踏踏实实地验证。读ID、回读配置、看原始值、再做换算只要每一步都不糊弄一次性点亮并不难。希望这篇轮询篇能帮你少走点弯路。
返回列表