前一篇我们把IIS3DWB10IS接到STM32C5的评估板上,用最基础的点灯流程确认了复位和供电都没问题。这篇直接进正题,讲怎么用IIC(I2C)把震动计的数据真正读出来。IIS3DWB10IS不是手机里那种普通加速度计,它是意法半导体面向振动监测推出的高带宽三轴加速度计,带宽最大能到6kHz,噪声特性也比普通传感器好很多;STM32C5则是Cortex-M33内核的新一代MCU,I2C外设配置很现代,两者配合做状态监测非常顺手。这篇内容适合手里正好有C5样片、想把IIS3DWB跑起来的开发者,也适合从G4或其他系列迁I2C代码时参考,因为C5的I2C底层几乎可以平移过来用。
1. 为什么用STM32C5驱动IIS3DWB10IS
1.1 震动传感器的定位和选型逻辑
IIS3DWB10IS这类传感器,和消费级加速度计最大的区别就是带宽。普通加速度计,比如常见的ADXL345,带宽大概300到400Hz,LIS2DW12也差不多,做倾斜、计步、跌落检测够了,但拿来看旋转机械的振动远远不够。轴承故障产生的特征频率往往在几百赫兹到几千赫兹,齿轮啮合频率更是轻松到5kHz以上,如果传感器带宽不够,信号直接衰减,后面算法再猛也白搭。
IIS3DWB把带宽做到了6kHz,噪声密度标称值也压得比较低,这才有资格做设备健康监测。选型的时候还有一个因素:它同时提供I2C和SPI接口。使用I2C做配置和低速数据读取非常方便,但真要把6kHz带宽吃满,最终还是要走SPI加FIFO,这一点后面我会专门讲。不过对于“先把传感器调通、看看数据是否合理”这个阶段,I2C是成本最低、排查最快的方式。
另外一个选型逻辑是功耗和温度稳定性。工业现场的设备往往长期挂着传感器采集,I2C只比SPI多两根线,放在靠近轴承的位置时布线压力小。虽然高带宽传感器本身功耗不算特别低,但在电池供电的巡检设备上,降低主控唤醒频率、用好FIFO,整体还是能接受。
1.2 STM32C5的I2C外设:和G4对比差在哪
STM32C5是意法半导体后面推出的主流低功耗系列,内核是Cortex-M33,和STM32G4的M4内核不一样,但外设风格却很接近,尤其是I2C。G4上的I2C已经是很新的版本,支持多主机、SMBus、PMBus,时序寄存器TIMINGR由硬件自动计算,完全没有老F1系列那种需要不停查EV5、EV6标志的手工状态机。C5上的I2C模块基本延续了这套设计,因此从G4把I2C驱动代码迁到C5是比较流畅的,改的主要是时钟使能、引脚复用和中断向量名。
我实测的体感差异有三点。第一,C5的复位和时钟架构做了调整,I2C外设挂在不同总线域上,__HAL_RCC_I2C1_CLK_ENABLE()底层对应的寄存器位和G4不一样,但HAL封装之后代码相同,直接迁移没问题。第二,C5的低功耗模式更丰富,可以把I2C配置成从机唤醒源,这在低功耗巡检设备里比G4的普通停机模式更好用。第三,C5的主频和内部时钟树需要重新配置,I2C的TIMINGR计算依赖PCLK,如果直接用G4工程里的TIMINGR值,实际波特率会偏,务必用CubeMX重新生成。
| 对比项 | STM32C5 | STM32G4 |
|---|---|---|
| 内核 | Cortex-M33 | Cortex-M4 |
| I2C模块 | 新IP核,可编程时序 | 新IP核,可编程时序 |
| 迁移难度 | 低,主要是时钟/引脚 | 参照基准 |
| 低功耗能力 | 更强,支持I2C唤醒 | 侧重高性能模拟外设 |
所以,如果团队里已经有人在G4上写过IIS3DWB驱动,换到C5时没必要重写,只要对照CubeMX重新生成一遍I2C初始化,然后再确认GPIO复用号即可。真正会坑人的还是传感器那边的配置和I2C总线设计。
2. 硬件连接与IIC总线设计要点
2.1 引脚分配、上拉电阻与电平匹配
我用的STM32C5评估板I2C1默认放在PB8和PB9上。不同板子的默认引脚可能不一样,C5的GPIO复用表和G4不完全相同,直接搬之前G4工程里的GPIO_InitStruct.Alternate值要查一下C5的数据手册。最简单的办法是在CubeMX里直接勾选I2C1,它会自动给你匹配可用引脚。
传感器这边,IIS3DWB10IS的I2C地址由SDO引脚决定,SDO接GND时7位地址是0x18,接VDD时是0x19。我习惯把SDO拉低,固定用0x18,这样万一SDA和SCL接反了,判断起来更容易。
然后是上拉电阻。I2C总线本身是开漏结构,SCL和SDA必须有外部上拉才能输出高电平。上拉电阻取多大,很多人直接照抄4.7k,但这是分场合的。I2C快速模式400kHz要求上升时间小于300ns,而上升时间近似等于0.8473乘以电阻和总线电容的乘积。假设短线连接时总线电容大概50pF,4.7k对应上升时间约199ns,没问题;但如果线稍微长一点,分布式电容到了100pF,4.7k对应的上升时间就是398ns,已经超了。这种场景建议换成2.2k,100pF时上升时间约186ns,还能留有裕量。
低电平那边的约束也要提一句。上拉电阻太小,比如低于1k,总线灌电流过大,从设备的输出级不一定扛得住;太大会导致上升沿过缓,时序违规。我自己的习惯是:100kHz模式用4.7k,400kHz模式用2.2k,线缆超过20cm时优先选用2.2k甚至1.8k。内部上拉一般不开,GPIO的弱上拉有几十k欧,对外部上拉几乎没有帮助,还会增加计算的不确定性,不开更干净。
关于“IIC为什么不能用推挽”这个问题,顺便说清楚。推挽输出的低阻特性会让两个节点同时输出不同电平的时候产生短路电流,I2C多主机仲裁机制完全依赖开漏加“线与”逻辑:谁先拉低谁就赢了,高电平靠上拉电阻决定。单主机场景虽然不需要仲裁,但从设备会拉低SCL做时钟拉伸,也会拉低SDA做应答,所以主机的SCL和SDA引脚也必须配置成开漏。
供电电平方面也要注意。IIS3DWB的I2C电平取决于VDD_IO,如果传感器模块单独供电1.8V,而STM32C5这边是3.3V,那么I2C上拉电压必须按1.8V来,并且确认C5引脚的容忍电压。最简单的方案是让传感器VDD_IO和MCU都用3.3V,上拉电阻统一接到3.3V,省掉电平转换电路。高带宽加速度计对电源纹波比较敏感,我习惯在传感器的电源引脚前面串一个磁珠,再加一个10uF和一个100nF的电容,实测数据稳定很多。
2.2 时钟占空比与时序计算
I2C时钟占空比不是随便设的,也不是必须50%。400kHz快速模式对时序的要求是SCL高电平时长至少0.6us,低电平时长至少1.3us,加起来2.5us正好是400kHz的周期,所以理想占空比是24%比52%加边沿,而不是各一半。现代I2C外设不使用固定占空比发生器,而是通过TIMINGR寄存器里的SCLH和SCLL分别控制高低电平计数。CubeMX把PCLK频率和目标波特率填进去,会自动算出TIMINGR,不需要手工算。
但手工理解一遍有好处。假设PCLK是80MHz,周期12.5ns,快速模式高电平0.6us对应需要48个计数,低电平1.3us对应104个计数。SCLH和SCLL还得分别加上信号上升沿的补偿值,CubeMX在这里会根据你填的上升时间自动微调。所以同一个I2C波特率,在不同主频下TIMINGR完全不一样,这也是为什么从G4迁到C5不能直接复制过去的原因。
时序相关还有一个重要概念叫时钟拉伸。低速从设备没准备好时,会在SCL拉低期间继续把SCL拉低,让主机等待。C5的I2C硬件支持这个机制,但是如果从设备因为某种原因持续拉低SCL,主机一直等可能出现超时。因此HAL库的所有I2C函数都有超时参数,我统一设成100ms,看起来对I2C这种近距离通信很宽裕,实际处理总线异常时很有用。
3. 核心代码实现:IIC读取震动数据
3.1 底层IIC读写函数封装
先用HAL库实现最底层的I2C读写。ST的传感器驱动通常会回调platform_write和platform_read两个函数,我们可以在工程里把它们实现成基于HAL_I2C的封装。
#define IIS3DWB_I2C_ADDR 0x18U #define IIS3DWB_I2C_ADDR_HAL (IIS3DWB_I2C_ADDR << 1) /* HAL需要8位地址 */ int32_t platform_write(void *handle, uint8_t reg, const uint8_t *bufp, uint16_t len) { HAL_StatusTypeDef status = HAL_I2C_Mem_Write( (I2C_HandleTypeDef *)handle, IIS3DWB_I2C_ADDR_HAL, reg, I2C_MEMADD_SIZE_8BIT, (uint8_t *)bufp, len, 100); return (status == HAL_OK) ? 0 : -1; } int32_t platform_read(void *handle, uint8_t reg, uint8_t *bufp, uint16_t len) { HAL_StatusTypeDef status = HAL_I2C_Mem_Read( (I2C_HandleTypeDef *)handle, IIS3DWB_I2C_ADDR_HAL, reg, I2C_MEMADD_SIZE_8BIT, bufp, len, 100); return (status == HAL_OK) ? 0 : -1; }注意I2C_MEMADD_SIZE_8BIT必须写清楚,因为寄存器地址是8位。如果不指定,默认按16位寄存器地址发送两个字节地址,传感器会完全不响应。HAL库的HAL_I2C_Mem_Read会自己处理“先写寄存器地址、再发起始位、再读数据”的完整时序,不需要手动调用Transmit和Receive组合。
3.2 配置传感器和确认通信
连接建立后,第一步不是配置寄存器,而是读WHO_AM_I,确认I2C链路真的通。IIS3DWB的WHO_AM_I寄存器地址是0x0F,每个ST传感器都有一个固定ID,我这片样片读回来是0x44,不同批次以数据手册为准。把它做成宏,避免在初始化代码里写魔法数字。
#define IIS3DWB_WHO_AM_I 0x0FU #define IIS3DWB_EXPECTED_ID 0x44U uint8_t id = 0; platform_read(&hi2c1, IIS3DWB_WHO_AM_I, &id, 1); if (id != IIS3DWB_EXPECTED_ID) { // 通信异常,检查地址、上拉、供电 Error_Handler(); }WHO_AM_I正常之后,做传感器复位,再设置量程、输出数据速率和带宽。ST官方驱动包里已经把这些配置封装成函数,可读性比自己填寄存器好很多。下面代码是我在C5工程里的实际调用,驱动包名称是iis3dwb_reg.c,函数参数以SDK头文件里的枚举为准。
iis3dwb_reg_t dev = {0}; dev.write_reg = platform_write; dev.read_reg = platform_read; dev.handle = &hi2c1; uint8_t rst = 1; platform_write(&hi2c1, 0x20, &rst, 1); // CTRL1复位位,实际以手册为准 // 配置量程和ODR iis3dwb_fullscale_set(&dev, IIS3DWB_2g); iis3dwb_odr_set(&dev, IIS3DWB_ODR_1kHz); iis3dwb_xl_bw_set(&dev, IIS3DWB_BW_1kHz);这里我先把ODR配置成1kHz,而不是传感器最高的26.7kHz,原因是I2C在400kHz下面连续读三轴原始数据,最多也就跑到几千Hz采样率,把ODR拉太高没有意义。先用1kHz调通,后面真正做振动分析再上SPI加FIFO。
3.3 读取加速度数据并转换为物理量
IIS3DWB的数据寄存器从0x28开始,依次是X低字节、X高字节、Y低字节、Y高字节、Z低字节、Z高字节。读的时候利用寄存器地址自动递增,一次连续读6个字节,效率更高,也避免多次发起I2C事务造成数据不同步。
#define IIS3DWB_OUT_X_L 0x28U void read_accel_raw(int16_t *ax, int16_t *ay, int16_t *az) { uint8_t raw[6]; platform_read(&hi2c1, IIS3DWB_OUT_X_L, raw, 6); *ax = (int16_t)((uint16_t)raw[0] | ((uint16_t)raw[1] << 8)); *ay = (int16_t)((uint16_t)raw[2] | ((uint16_t)raw[3] << 8)); *az = (int16_t)((uint16_t)raw[4] | ((uint16_t)raw[5] << 8)); }转换为重力加速度g时,和量程对应。我用的是±2g,16位输出,参考灵敏度是0.061mg/LSB,换算公式就是原始值乘以0.061再除以1000。下面代码放在主循环里,每2ms读一次,对应500Hz的轮询频率,验证数据已经足够。
#define SENSITIVITY_MG 0.061f float ax_g, ay_g, az_g; int16_t ax_raw, ay_raw, az_raw; read_accel_raw(&ax_raw, &ay_raw, &az_raw); ax_g = (float)ax_raw * SENSITIVITY_MG / 1000.0f; ay_g = (float)ay_raw * SENSITIVITY_MG / 1000.0f; az_g = (float)az_raw * SENSITIVITY_MG / 1000.0f;如果初始化里选的量程不是±2g,灵敏度必须同步修改。比如±4g时灵敏度会变成0.122mg/LSB,不换的话算出来的加速度会差一倍。这类细节最容易踩坑,建议大家把量程和灵敏度定义成宏,放在同一个头文件里,避免修改时漏掉。
4. 实测过程与常见问题排查
4.1 总线卡死和时钟拉伸:最常见的两个坑
实际调试中我遇到最多的问题不是寄存器配置,而是I2C总线卡死。现象是第一次通信正常,第二次HAL_I2C_Mem_Read返回HAL_BUSY,用示波器看SDA被拉低不放。这种问题多半是上电时序或者从设备复位造成的。传感器还在复位时,主机就开始发数据,从设备没来得及处理,总线状态就拧巴了。
最简单有效的恢复方法是把SCL引脚临时配置成普通开漏输出,手动翻转9个时钟周期。9个时钟能把卡在中间状态的总线状态机复位掉。注意翻转时钟时SDA也要保持高电平,避免产生意外的起始或停止条件。
void recover_i2c_bus(void) { GPIO_InitTypeDef gpio = {0}; gpio.Pin = SCL_PIN; gpio.Mode = GPIO_MODE_OUTPUT_OD; gpio.Pull = GPIO_PULLUP; HAL_GPIO_Init(SCL_PORT, &gpio); for (int i = 0; i < 9; i++) { HAL_GPIO_WritePin(SCL_PORT, SCL_PIN, GPIO_PIN_RESET); HAL_Delay(1); HAL_GPIO_WritePin(SCL_PORT, SCL_PIN, GPIO_PIN_SET); HAL_Delay(1); } // 恢复成I2C复用功能 gpio.Mode = GPIO_MODE_AF_OD; gpio.Pull = GPIO_PULLUP; gpio.Alternate = GPIO_AF4_I2C1; HAL_GPIO_Init(SCL_PORT, &gpio); }时钟拉伸导致的超时更容易被忽略。C5的I2C硬件会实时监测SCL电平,从设备拉低SCL时,主设备内部的传输计数器暂停,所以慢速从设备不会造成数据错乱,只会让单次传输时间变长。如果你的代码在中断里做I2C读取,建议不要用阻塞式HAL API,改成中断模式,否则一个慢从设备会卡住整个中断响应。
还有一个经验是:I2C地址错了并不一定表现为NACK,也可能是读回来的数据全是0xFF或者0x00。因为错误地址可能恰好被总线上另一个设备响应。排查时先用最简单的方式扫描总线,向0x01到0x7F轮流发WHO_AM_I读请求,看哪个地址有ACK。
4.2 用示波器看时序:别猜,直接看
在I2C这种有明确电平状态的协议上,示波器是最好的排查工具。我调试时至少会抓两组波形:一组是上电后第一次通信的完整时序,另一组是出问题时的波形。
第一组波形要看的是START条件:SCL在高电平时,SDA从高跳低。然后是地址字节,我的传感器地址0x18左移一位后变成0x30,发送方向是写。第8个时钟后SDA在第9个时钟被从设备拉低,这是ACK。如果看不到ACK,说明地址、上拉或传感器供电有问题。读操作时,主机发送完寄存器地址后会再产生一个重复起始条件,然后主机把方向改成读,忽略一次应答,之后连续收6个数据字节,最后一个字节主机回NACK并产生STOP。
第二组波形一般是总线卡死时的。SDA一直为低,SCL还能翻转,这是典型的从设备应答后没释放SDA。此时手动翻转SCL时钟,同时盯着示波器,看到SDA在第几个时钟恢复高电平,就能大概判断是哪个状态机卡住了。
要注意示波器探头的等效电容。便宜的探头本身可能有10多pF电容,并联到总线上会让上升沿变慢,原本临界满足的400kHz时序可能就变成超限了。调试时可以临时把速率降到100kHz,排除探头负载的影响。
4.3 I2C读取的带宽局限:FIFO和中断怎么配合
这篇文章标题是IIC获取震动数据,但我要泼一盆冷水。IIS3DWB虽然支持6kHz带宽,但I2C在400kHz下连续读6字节,单次事务至少需要大约150到200us,换算下来最多也就5kSPS左右,吃不满传感器的高带宽。做振动监测时,采样率至少要达到信号最高频率的两倍以上,如果目标是6kHz带宽,I2C物理层就不够用。
合理的做法是开启传感器内置FIFO。IIS3DWB可以把多组采样数据暂存在FIFO里,等攒够一定数量,MCU再通过I2C一次性突发读取。这样I2C只需要在FIFO快满的时候介入一次,其余时间传感器自己按高ODR采集。配合中断引脚,FIFO水位到达阈值时触发MCU,MCU进入中断把整块FIFO搬走,能大幅降低总线占用和MCU唤醒次数。
使用FIFO时尤其要注意读出长度必须和FIFO水位匹配。如果读到一半,传感器还在往FIFO里写新数据,数据帧边界就容易错位。我建议配置成FIFO满或者接近满时停止写入,先让MCU把数据稳定搬走,再恢复采集。这样会丢一部分数据,但对连续监测场景来说,比错位数据更好处理。
4.4 避坑清单
不同HAL版本、不同驱动包,坑的位置其实很固定,我整理了一份自己反复对照的清单。
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 读WHO_AM_I返回全0xFF | SDA上拉丢失或电压不匹配 | 检查上拉电阻,VDD_IO和MCU电平是否一致 |
| 读WHO_AM_I返回全0x00 | 地址错误或传感器没供电 | 检查SDO引脚,扫描I2C总线 |
| 第一个字节正常,后面全部超时 | 持续读多字节时地址自增没生效 | 确认寄存器地址使用连续读,不要按单字节地址递增 |
| 400kHz下波形上升沿很平 | 总线电容过大或上拉电阻偏大 | 换成2.2k,缩短杜邦线 |
| 偶尔NACK,上位机数据跳变 | 连接接触不良或电源波动 | 改用焊接线,传感器电源加滤波电容 |
| 数值波动离谱,低频噪声大 | 传感器供电纹波或周围强磁场干扰 | 电源加磁珠,PCB布局远离电机驱动线 |
还有一个容易忽略的点:传感器中断引脚输出开漏,如果要接到MCU的EXTI输入,外部必须加上拉。我最早一次接中断引脚时,忘了这个细节,中断状态一直不对,查了半天才发现是引脚浮空。
最后说一个实际操作的体会
我踩得最多的坑不是寄存器配置,而是I2C的物理层。第一次用杜邦线飞线调试,总线电容太大,400kHz死活不稳,后来降到100kHz才通。等到换成短焊接线,再切回400kHz,整个链路就非常稳定了。如果你也打算用STM32C5来读IIS3DWB,建议开局直接检查三样东西:SDO地址、上拉电阻、传感器供电滤波。这三样没问题,剩下的就是寄存器配置和调试工具问题。目前这套I2C代码在样机上已经连续跑了两个星期,低速读取和FIFO读取都验证过了,下一步我准备把SPI那套加上,真正把高带宽吃满。