资讯动态

STM32C5与LSM6DSOW陀螺仪轮询读取:从I2C配置到数据解析全攻略

发布时间:2026/9/9 6:11:18 来源:尧图企业网站定制
最近在调试一块基于STM32C5系列的新板子外挂了LSM6DSOW六轴传感器。作为系列文章的第一篇先把最基础、也是后续一切功能地基的“轮询读取陀螺仪数据”完整跑通。LSM6DSOW这颗传感器本身不算新但配合STM32C5这个新内核平台还是有一些值得记录的细节。这篇文章就从硬件接线、CubeMX工程配置、寄存器初始化、轮询逻辑、实测数据解读到常见坑位一条线捋清楚给后面用FIFO、中断、姿态解算的读者留一个可靠的起点。1. 硬件连接与模块选型注意点1.1 传感器模块怎么选LSM6DSOW市面上常见的模块有两种形态一种是ST官方的扩展板比如基于DIL24接口的板卡另一种是淘宝上很常见的国产小模块一般是I2C/SPI双接口引出。两种我都有用过如果是做快速验证我建议选国产小模块因为引脚直接排针引出接杜邦线就能调试。官方扩展板做工虽然好但接口是ST的专用排母在非ST官方主控板上反而不方便。不管选哪种有一点要清楚LSM6DSOW的数据输出和配置接口同时支持I2C和SPI但上电后默认是I2C模式。如果你同时把SPI的CS引脚拉低了才会切到SPI。所以做I2C调试时不论模块上有没有引出CS引脚都不要把它接到地。1.2 引脚接线与上拉电阻用I2C方式连接接线就四个信号VCC、GND、SCL、SDA。模块的VCC一般是1.8V到3.6V连STM32C5开发板的3.3V就行。这个不能接错接5V会烧传感器。SCL和SDA接STM32C5的I2C引脚。我用的板子I2C1在PB6和PB7这个后面在CubeMX里能直接看到引脚映射。关于上拉电阻有个容易忽略的点。LSM6DSOW模块上一般已经焊了2.2k或者4.7k的上拉电阻到VCC这时候就不需要在STM32C5这边再开内部上拉。但如果你的模块是那种非常精简的、板上没有上拉电阻的版本必须在外部加两个上拉电阻否则I2C波形会很难看通信时好时坏。注意MCU内部上拉通常比较弱比如40k对于高速I2C400kHz来说不够可靠最稳妥的做法是外部4.7k上拉。1.3 I2C地址和总线选择LSM6DSOW的7位I2C地址由SDO/SA0引脚决定SDO/SA0引脚电平7位I2C地址8位写地址8位读地址低电平GND0x6A0xD40xD5高电平VCC0x6B0xD60xD7绝大多数模块在板上把SDO引脚拉低了默认地址就是0x6A。但也不能排除部分模块拉高。所以在一开始写代码时可以通过读WHO_AM_I来验证实际地址这个后面详细说。STM32C5的I2C外设在CubeMX里配置的时候要记得使能同时确认引脚没有和其他外设冲突比如有些引脚同时是调试口或晶振引脚。我用的I2C1时钟挂载在APB1上配置成100kHz标准模式对于这种传感器通信完全够用。2. CubeMX工程搭建时钟、I2C、串口2.1 芯片选型和时钟配置STM32C5系列是基于Cortex-M33内核的新一代MCU自带FPU和DSP指令集性能比同价位的M0/M0强不少。在CubeMX里选芯片型号时根据自己的实际板子选。工程建好之后第一件事就是确认System Core里的RCC配置。时钟方面LSM6DSOW本身对MCU时钟没有苛刻要求I2C时钟、串口波特率都从这里派生。我直接用外部晶振HSE然后在Clock Configuration里把系统主频拉高一点。这里不需要太纠结关键是确认APB1总线上I2C的时钟不要超过36MHz具体值取决于你所选的STM32C5芯片否则I2C分频系数会怎么配都不对。如果偷懒直接用HSI内部16MHz时钟也可以跑但后面如果还要做定时器、串口高波特率通信建议一开始就把外部时钟配好。2.2 I2C外设参数设置在CubeMX左侧Categories里找到Connectivity - I2C1使能后参数设置如下参数设置值说明I2C Speed ModeStandard Mode100kHzI2C Clock Speed100000100kHzRising Time1000ns标准模式下默认即可Falling Time300ns标准模式下默认即可为什么要刻意选100kHz而不是400kHz快速模式我的理由是第一传感器模块的飞线如果比较长比如超过10cm的杜邦线400kHz下的波形边沿会变差偶尔出现应答超时很难排查第二轮询读取陀螺仪数据本身数据量很小一次6个字节100kHz完全够用没必要为了这点差距去冒毛刺风险。先把通信可靠性做扎实以后真要上400kHz再优化。串口这里用USART2配置成异步模式波特率1152008位数据无校验1位停止位。STM32C5的USART2一般在PA2TX、PA3RX。不管数据最终要不要回传串口打印是在传感器调试阶段最直接的观察手段。2.3 USART串口配置与printf重定向CubeMX生成代码后在main.c里加上printf重定向。用HAL库最简单的做法是重写fputc#include stdio.h int fputc(int ch, FILE *f) { HAL_UART_Transmit(huart2, (uint8_t *)ch, 1, 100); return ch; }在Keil MDK工程里还需要勾选MicroLib这样才能省去底层文件系统的重定向麻烦。在STM32CubeIDE里则不用额外操作直接就能用printf。这步做完以后可以用一个简单的循环打印一串字符验证串口通路。这一步看似基础但确能提前暴露printf重定向配置问题别等传感器数据都读回来了才发现打印不出来白白浪费调试时间。3. LSM6DSOW初始化这些寄存器决定了能不能出数3.1 WHO_AM_I先证明通信是通的读WHO_AM_I永远是接入一颗新传感器的第一步。WHO_AM_I寄存器地址是0x0F复位值对LSM6DSOW来说是0x6C。uint8_t value 0; HAL_I2C_Mem_Read(hi2c1, 0xD4, 0x0F, 1, value, 1, 100); if (value 0x6C) { printf(LSM6DSOW detected!\r\n); } else { printf(WHO_AM_I error: 0x%02X\r\n, value); }这里0xD4是8位写地址HAL库的Mem_Read函数内部会自动把最低位置1变成读地址所以调用时传0xD4没问题。如果你在别处看到有人直接传0x6A那是7位地址HAL库里不能用会被左移一位导致地址错误。如果读回的数值不是0x6C先别急着怀疑芯片坏了。大概率是地址没对上可以换0xD6试一下也就是SDO引脚拉高的那组地址。3.2 关键控制寄存器逐位说明LSM6DSOW的寄存器很多但初始化只要关心下面几个CTRL3_C地址0x12Bit名称值作用6BDU1数据块更新防止读取过程中高低字节不同步2IF_INC1寄存器地址自动递增允许连续多字节读0SW_RESET0软件复位初始化为0我最看重BDU位。如果不置BDU读取高字节和低字节的间隙传感器恰好更新了数据缓冲区就会出现高字节是这一采样时刻、低字节是下一采样时刻的错位情况算出来的数据会有一瞬间的毛刺。在轮询模式下这种毛刺概率不高但一旦系统跑到中断模式或FIFO模式它就是必踩的坑。所以一上来就把BDU置1养成习惯。CTRL1_XL地址0x10这个寄存器配置加速度计的采样率和量程。只读陀螺仪也需要把加速度计配起来原因后面说。Bit名称值作用7:4ODR_XL0101加速度计输出数据率104Hz3:2FS_XL00满量程±2g1:0LPF1_BW_SEL00滤波器带宽选择默认即可加速度计量程选±2g这是因为做姿态解算时精度最高。这里只读陀螺仪的话加速度计量程并不关键但把ODR配成104Hz可以和陀螺仪的ODR保持一致后续如果做6轴融合数据时间戳对齐会省事很多。CTRL2_G地址0x11这个寄存器配置陀螺仪是当前这篇的核心。Bit名称值作用7:4ODR_G0100陀螺仪输出数据率104Hz3:2FS_G00满量程±250dps1FS_1250是否选择±125dps量程这里量程的选择要稍微想一下±250dps意味着满量程输出对应每秒250度对于人手拿着板子转动这种场景已经覆盖绝大多数情况了。而且量程越小同一位数对应的物理分辨率越高。如果选±2000dps分辨率会降低8倍静态时的噪声看起来会很大。真正需要用到±2000dps的场景一般是电机轴、云台这类高速剧烈转动场合普通手持设备用不到。ODR方面104Hz是融合精度和功耗之间的一个常见取舍点。设为12.5Hz虽然更省电但人手的快速转动会被严重欠采样陀螺仪数据会“跟不上手”。设为1.66kHz性能是好但对MCU的轮询频率要求高很多而且系统功耗明显上升没必要。CTRL4_C地址0x13这个寄存器有一个关键位bit7是I2C_disable。如果这个位为1I2C就被关闭了。默认是0但如果你的初始化代码里曾经从别处抄过一段配置不小心写1后面I2C通信会直接失败。我一般会在初始化时显式地读一次这个寄存器确认bit7为0。CTRL7_G地址0x16这里注意bit7G_HM_MODE。0表示高性能模式1表示低功耗模式。LSM6DSOW上电默认是高性能我们保持默认。如果你从某个例程里抄了配置代码看到对这个寄存器有写入一定要确认没有把G_HM_MODE写成1否则陀螺仪噪声会明显变大。3.3 初始化代码与顺序寄存器配置顺序其实没有严格规定但我会遵循一个原则先做软件复位等待复位完成然后再配置CTRL3_CBDU、IF_INC最后配置ODR和量程。static void LSM6DSOW_Init(void) { uint8_t tmp 0; // 软件复位 tmp 0x01; HAL_I2C_Mem_Write(hi2c1, LSM6DSOW_ADDR, 0x12, 1, tmp, 1, 100); HAL_Delay(20); // 配置CTRL3_C: BDU1, IF_INC1 tmp 0x44; HAL_I2C_Mem_Write(hi2c1, LSM6DSOW_ADDR, 0x12, 1, tmp, 1, 100); // 配置CTRL1_XL: ODR104Hz, ±2g tmp 0x50; HAL_I2C_Mem_Write(hi2c1, LSM6DSOW_ADDR, 0x10, 1, tmp, 1, 100); // 配置CTRL2_G: ODR104Hz, ±250dps tmp 0x40; HAL_I2C_Mem_Write(hi2c1, LSM6DSOW_ADDR, 0x11, 1, tmp, 1, 100); printf(LSM6DSOW init done.\r\n); }初始化前后建议做个延时比如等50ms确保传感器内部启动流程走完。如果刚上电就立即读数据传感器有时还在自检状态STATUS_REG里的数据位还没拉高。4. 轮询读取与角速度换算4.1 为什么先用轮询以及STATUS_REG含义如果是从单片机裸机编程过来的朋友对“轮询”这个词应该不陌生。所谓轮询就是MCU主动、反复地去查询传感器的一个状态寄存器看数据准备好没有。准备好了就读取没准备好就继续查。LSM6DSOW的STATUS_REG寄存器地址是0x1E其中bit1就是GDAGyroscope Data Available这个位为1时表示陀螺仪的输出寄存器数据已经更新。#define LSM6DSOW_STATUS_REG 0x1E #define LSM6DSOW_G_DA (1 1)轮询的方式很简单uint8_t status 0; do { HAL_I2C_Mem_Read(hi2c1, LSM6DSOW_ADDR, LSM6DSOW_STATUS_REG, 1, status, 1, 100); } while (!(status LSM6DSOW_G_DA));为什么要先查GDA再读数据因为LSM6DSOW的输出寄存器是随时更新的。如果不去管数据准备好没有直接在任意时刻读可能读到旧数据可能读到新数据也可能是新旧数据的混合。在传感器静止场景下没有区别但一旦开始转动角度变化率很快你就会发现数据在时间轴上不对齐姿态解算时尤其致命。相比于中断方式轮询的代码简单直观还不需要额外占用一个外部中断引脚。缺点是MCU要一直耗在这个查询循环里。不过对于这一篇的目标——跑通数据链路——轮询是最不容易出错的方式。后面如果做低功耗或者需要节省CPU时间再换成EXTI中断或者FIFO批量读取逻辑都建立在今天这个基础上。4.2 读取六字节输出并合成int16陀螺仪的三轴输出寄存器从OUTX_L_G开始依次是寄存器地址寄存器名称含义0x22OUTX_L_GX轴低字节0x23OUTX_H_GX轴高字节0x24OUTY_L_GY轴低字节0x25OUTY_H_GY轴高字节0x26OUTZ_L_GZ轴低字节0x27OUTZ_H_GZ轴高字节因为初始化时已经把IF_INC置1地址自动递增是开启的所以可以一次把6个字节连续读出来uint8_t data[6]; HAL_I2C_Mem_Read(hi2c1, LSM6DSOW_ADDR, 0x22, 1, data, 6, 100);注意这里传的起始地址是0x22OUTX_L_G而不是0x22再偏移什么的直接从这里读6个字节依次就是X低、X高、Y低、Y高、Z低、Z高。有些例程会把START_ADDRESS写成0x22然后手动读两次其实没必要。合成int16时要小心符号扩展。C语言里最稳妥的写法是这样int16_t gyro_x (int16_t)((data[1] 8) | data[0]); int16_t gyro_y (int16_t)((data[3] 8) | data[2]); int16_t gyro_z (int16_t)((data[5] 8) | data[4]);这段代码为什么是对的因为data[1]是高字节左移8位后放到int16_t的高8位data[0]是低字节放在低8位。C语言里把16位无符号组合先移位再强制转为int16_t符号位会在转换中被正确解释。有些人的写法是先拼成uint16_t再转int16_t效果一样看个人习惯。4.3 从原始值到dps的换算公式LSM6DSOW的陀螺仪输出是一个16位有符号整数满量程对应的关系是-32768对应负的满量程32767对应正的满量程。换算公式角速度(dps) 原始值 × 满量程(dps) / 32768以我们配置的±250dps量程为例float gyro_x_dps (float)gyro_x * 250.0f / 32768.0f; float gyro_y_dps (float)gyro_y * 250.0f / 32768.0f; float gyro_z_dps (float)gyro_z * 250.0f / 32768.0f;这里250.0f如果写错了量程和初始化时配置的CTRL2_G对不上那所有计算出来的角速度都是线性偏差这是一个非常隐蔽的错误必须仔细核对。一个有点反直觉的细节当陀螺仪静止时输出不是0。传感器本身存在零偏bias好的传感器出厂校准后零偏可能只有正负几dps但廉价的模块往往不做出厂校准静止时输出可能在正负十几dps甚至更大。这个问题后面实测数据部分再细聊。5. 上机实测串口打印与数据解读5.1 实测静止数据零偏是正常的把初始化代码和轮询读取代码写好后在主循环里这样调用int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_I2C1_Init(); MX_USART2_UART_Init(); LSM6DSOW_Init(); while (1) { uint8_t status 0; uint8_t data[6] {0}; HAL_I2C_Mem_Read(hi2c1, LSM6DSOW_ADDR, LSM6DSOW_STATUS_REG, 1, status, 1, 100); if (status 0x02) { HAL_I2C_Mem_Read(hi2c1, LSM6DSOW_ADDR, 0x22, 1, data, 6, 100); int16_t gyro_x (int16_t)((data[1] 8) | data[0]); int16_t gyro_y (int16_t)((data[3] 8) | data[2]); int16_t gyro_z (int16_t)((data[5] 8) | data[4]); float gx (float)gyro_x * 250.0f / 32768.0f; float gy (float)gyro_y * 250.0f / 32768.0f; float gz (float)gyro_z * 250.0f / 32768.0f; printf(gx%.2f gy%.2f gz%.2f\r\n, gx, gy, gz); } } }我实际跑下来的静止数据大概是这样的gx0.85 gy-0.62 gz1.13 gx0.92 gy-0.71 gz1.24 gx0.80 gy-0.58 gz1.05可以看到数据在某个基线上小幅抖动这是完全正常的。这个基线比如gz大概在1.1dps附近就是零偏不是传感器坏了。这里我想多说一句在后续做姿态解算的时候必须对零偏做校准。最简单的办法是让板子保持静止几十秒然后把这段时间的数据平均得到的就是偏置以后每次读取的时候减去这个偏置。这个校准在步进电机云台、自平衡小车项目里是必须做的否则积分出来的角度会一直漂。5.2 转动数据怎么判断好坏把板子拿在手里绕Z轴逆时针转动从上往下看此时gz应该明显变大比如转到大概90度每秒时gz应该接近90dps左右。由于手转的速度不恒定数据会有波动但方向应该是一致的。同样绕X轴转动看gx绕Y轴转动看gy。一个用于验证量程和符号的简单表格转动方向预期输出说明绕Z轴正向俯视逆时针gz 0右手定则绕Z轴反向俯视顺时针gz 0右手定则静止三轴都接近0加零偏有波动属于噪声如果转动时发现符号反了不是传感器的错是传感器安装方向和你定义的坐标系不一致导致的。最简单的处理方式是在代码里对相应轴取反或者调整安装方向。我这个模块上面印了坐标轴方向安装时和板子对齐就省了很多事。5.3 简单一阶低通滤波原始数据直接打印会有不少高频噪声特别是在廉价模块上噪声幅度可能达到正负2到3dps。如果直接拿去做积分噪声会累积成明显的角度漂移。一个零成本的小优化是一阶低通滤波float gx_filtered 0.0f; float gy_filtered 0.0f; float gz_filtered 0.0f; // 主循环内每次读到新值后执行 #define ALPHA 0.8f gx_filtered ALPHA * gx_filtered (1.0f - ALPHA) * gx; gy_filtered ALPHA * gy_filtered (1.0f - ALPHA) * gy; gz_filtered ALPHA * gz_filtered (1.0f - ALPHA) * gz;ALPHA取值越大滤波越平滑但滞后越明显。0.8这个值在104Hz的ODR下大概保留了几十Hz以内的信号变化对手部动作来说足够同时能把大部分高频噪声削掉。注意这类滤波算法假设采样周期是均匀的主循环里的轮询并不是严格等间隔偶尔会因为其他任务卡顿导致一帧间隔变长。对陀螺仪这种应用来说影响不大但如果追求严格可以用定时器中断来触发采样。6. 踩坑记录与排查建议6.1 WHO_AM_I每次都读不对这是最常出现的开局问题我的排查顺序是这样的先确认模块供电用万用表量模块VCC引脚正常应该在3.3V左右。再确认SCL、SDA有没有接反。这个错误在杜邦线连接时特别容易犯因为有些模块上丝印不明显。确认模块SDO/SA0的电平如果模块上SDO没接用杜邦线直接接VCC或者GND两种情况对应的地址不同代码里地址换一下。最后用示波器或者逻辑分析仪抓一下I2C波形看主机有没有发地址、从机有没有回ACK。如果没有逻辑分析仪用HAL函数返回值也能判断返回HAL_OK之外的值就说明总线层出错。6.2 数据一直是零如果数据全是0.00但WHO_AM_I能读对那通信层是通的问题出在初始化配置。我遇到过的原因主要有两个一是CTRL2_G没有配置ODR位。记得有个型号的默认状态是省电模式ODR为0000陀螺仪不工作输出寄存器永远是0。你会发现STATUS_REG的GDA位也一直为0轮询循环根本跳不出来。二是CTRL3_C的SW_RESET位没有释放。如果你写了软件复位后立刻配置寄存器没有等复位完成后面的配置可能被复位操作覆盖掉了。解决办法就是在写复位之后加至少10到20ms的延时。6.3 数据跳变或者一轴符号对不上某个轴的数据偶尔跳一下十有八九是低字节和高字节读的时候发生了寄存器更新也就是说BDU没有生效。确认CTRL3_C里写入了0x44其中bit6是BDU必须为1。某轴符号和预期相反这是安装坐标问题不需要改寄存器在代码里对这个轴的原始值取反即可。但要注意如果同时在做加速度计融合符号一致性非常重要否则姿态解算会发散。6.4 STM32C5新平台的特别提醒最后单独说一下STM32C5这个平台。相比老一代F1系列它的HAL库版本比较新CubeMX生成代码的结构也有差异。有一个细节是HAL_I2C_Mem_Read这类函数的超时参数不要设成HAL_MAX_DELAY一旦I2C总线上有异常代码会卡死在等待里。调试期间统一用100ms超时通信异常时函数会返回错误你就知道问题出在I2C层而不是主循环逻辑。另外STM32C5的GPIO初始化在HAL_GPIO_Init里如果你在CubeMX里把I2C引脚重新映射了记得重新生成代码否则手动改代码时很容易漏掉AF复用功能配置。我自己就干过在main.c里手动改了半个多小时才发现引脚复用没使能代码生成功能能省这个时间就省吧。这些坑都绕过去之后LSM6DSOW的轮询陀螺仪数据读取基本就稳定了。这套基础跑通后面无论是切到SPI提高速率、用EXTI中断释放CPU、开FIFO做低功耗批量采集还是进一步融合加速度计做姿态解算都是在这个链路上加新组件的事。先把数据拿稳后续才有文章可做。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价