IIS3DWB这块芯片的卖点就一句话:它把普通加速度计100~400Hz的响应带宽直接拉到了6kHz。你从它口里拿到的不是“板子在晃”这种宏观信息,而是轴承磨损、齿轮啮合、电机转子这类真正能用于故障诊断的高频振动成分。上一篇文章我们把STM32C5的工程环境搭了起来,这篇接着写最核心的一步——用IIC(也就是I2C)把三轴震动数据从传感器里读回来,并且让读取过程可持续、可分析,而不是示波器上亮一下就完事。
这篇适合正在做状态监测、机械故障诊断,或者单纯想验证STM32C5外设到底好不好用的人。我会从IIC总线的物理层开始讲,然后落到STM32C5的I2C外设配置和IIS3DWB寄存器操作,最后把我在实板上踩过的几个坑完整复盘一遍。全程用HAL库,但关键位置会解释寄存器发生了什么,方便你换到LL库或者寄存器操作时心里有数。
1. 这块“6kHz振动计”为什么值得用IIC认真对待
1.1 IIS3DWB在加速度计家族里的定位
普通加速度计,比如LIS3DH、MMA8451这类用来测姿态、倾角的芯片,响应带宽一般只有几百Hz,说得直白一点,它们对“快”的振动不敏感。你拿LIS3DH去测电机轴承早期故障,高频分量还没进ADC就被传感器本身的机械结构衰减掉了,数据再漂亮也是空欢喜。
IIS3DWB专门干这个事。它把可用的平坦响应带宽做到了6kHz,输出三轴、16位数字量,供电范围1.71~3.6V,接口有IIC和SPI两种。拿它和常见的几款传感器放一起看,定位差异会很清楚:
| 传感器 | 类型 | 典型最高带宽/ODR能力 | 适合场景 |
|---|---|---|---|
| LIS3DH | 三轴加速度计 | 带宽有限,ODR最高5kHz | 姿态、倾角、低功耗唤醒 |
| LSM6DSO | 六轴惯性单元 | ODR最高6.66kHz | 手机、可穿戴、运动检测 |
| IIS3DWB | 三轴振动计 | 带宽6kHz,ODR可达26.7kHz | 振动监测、故障诊断、状态维护 |
所以选它做机械设备状态监测,不是因为它参数堆得高,而是这个带宽范围正好覆盖了绝大多数旋转机械的故障特征频率。高速轴、齿轮、滚动轴承的早期异常,往往就藏在1kHz到6kHz这一段。
1.2 为什么主控选了STM32C5而不是更老的F1/G4
STM32C5是Cortex-M33核心、带FPU的新系列,性能比F1这种老家伙强一个量级。做振动采集这件事,主控最怕两件事:一是IIC速率上不去,数据吞吐卡脖子;二是中断响应和DMA能力弱,高频采样时主循环被读取操作拖死。STM32C5的I2C外设带DMA支持,配合250MHz这个级别的主频,跑400kHz的IIC总线还有大量余量做计算。
我在做这个项目之前手上正好有一套G4的板子,所以刚开始是照着G4的工程改的。C5的I2C外设和G4属于同源IP,HAL库函数基本通用,但时钟树、GPIO复用映射这些细节有差异,千万不要把G4的初始化代码直接拖过来用。本章后面专门有一节讲迁移时的差异清单。
2. 硬件底子:IIC总线上拉电阻、开漏与时序的核心逻辑
2.1 IIS3DWB引脚接线与7位地址
IIS3DWB的IIC接线很常规:SCL、SDA各接一根线,另外有一个SA0引脚决定IIC从设备地址。典型接法是SA0接地,此时7位地址为0x18;如果SA0接高电平,地址变成0x19。这个引脚对应过来就是IIC地址最末位的不一样,别小看这一点,后面主机软件里地址没对上,所有寄存器都会读不回。
还有一个容易混淆的点:这个芯片同时也有SPI接口,所以SDI/SDO/CS的引脚在两种模式下功能完全不同。如果按SPI的方式接了SDI、SDO,再想在IIC模式下工作,信号逻辑会乱掉。我建议拿到模块先看原理图,确认SCL和SDA连的是芯片的SCL、SDA功能脚,而不是复用成SPI的引脚。
IIC总线本质是“开漏+上拉”结构,所以硬件上除了VDD和GND,必须给SCL、SDA各接一个上拉电阻到VDD。不少开发板把上拉电阻做在了板载传感器一侧,这时候主控这边就不用再加;如果传感器是独立模块,主控和模块之间只用杜邦线连,那上拉电阻就得自己加。
2.2 上拉电阻“取多大”的计算公式
这个值是IIC硬件设计里问得最多的问题。公式其实不复杂,两条边界:
电阻下限由灌电流决定,公式是:
Rp(min) = (VDD - VOL) / IOL其中VOL是低电平输出电压上限,一般取0.4V;IOL是器件在VOL下能吸收的灌电流,标准/快速模式典型按3mA算。3.3V系统算下来:
(3.3 - 0.4) / 0.003 = 966Ω所以3.3V下上拉电阻小于1kΩ是不合适的,再往下拉,器件低电平输出拉不住,总线低电平电压超标,逻辑判断会出错。
电阻上限由上升时间决定,I2C规范规定了不同模式的SCL上升时间上限,公式是:
Rp(max) = t_rise / (0.8473 × C_bus)C_bus是总线总电容,包括主控引脚电容、从设备引脚电容和PCB走线电容,经验估算每个器件约10pF,每10cm走线约3~5pF。400kHz快速模式下t_rise最大300ns,假设总电容110pF:
300ns / (0.8473 × 110pF) ≈ 3218Ω也就是说,400kHz下如果线很短、器件很少,3.3kΩ也能跑;但线长了、挂的设备多了,总线电容上去,电阻就必须降。
我实际用的原则很简单:3.3V、400kHz、两三个设备、短线连接,取2.2kΩ最稳;如果接了杜邦线并且超过20cm,直接降到1kΩ;5V系统则最小从1.8kΩ起步,不要一味学网上那些4.7kΩ的通用习惯,那是100kHz时代留下来的值。
| 总线电容 | 100kHz标准模式 | 400kHz快速模式 |
|---|---|---|
| 50pF | 上拉到23.6kΩ都行 | 上拉到7kΩ都行 |
| 110pF | 上拉到10.7kΩ都行 | 上拉到3.2kΩ都行 |
| 400pF(重负载) | 上拉到2.9kΩ | 上拉到884Ω,接近下限 |
要测总线电容最省事的办法是看波形:用示波器看SCL上升沿,如果上升沿显得很“软”、一直爬不上3.3V,那就是电阻偏大;如果低电平压不下去、始终高于0.4V,那就是电阻偏小。
2.3 开漏为什么是IIC的底线(推挽会闯祸)
网上经常刷到“IIC为什么不能用推挽”,这个问题其实一句话就能解释:IIC总线上允许挂多个设备,而总线协议任何一方都可能把SCL或SDA拉低。如果主机用推挽输出,主机输出高电平时会主动往高打,而从机想拉低,两个输出直接打架,轻则波形畸形,重则烧坏引脚内部的驱动管。
开漏结构就安全多了:任何设备想表达低电平,只需要把管子导通,把线拉向地;想表达高电平,谁都不用主动输出,靠上拉电阻慢慢把线拉上去。也就是大家常说的“线与”逻辑,多个设备可以同时在总线上拉低而不产生冲突。
STM32内部虽然自带弱的I2C上拉,但那个电阻通常几十kΩ,驱动能力和波形质量都撑不住400kHz,只适合临时验证。正规做法永远是外部2.2kΩ左右的上拉电阻,内部上拉只当保险,不要当主力。
2.4 SCL占空比不是越高越好,而是“别低于下限”
很多刚接触IIC的人以为SCL必须是50%占空比,这其实是个误区。I2C规范定义的是SCL高电平和低电平各自的最小持续时间,而不是两者必须相等。以400kHz快速模式为例,低电平最小1.3μs,高电平最小0.6μs,两者加起来小于2.5μs,剩下的是周期余量。
真正容易出问题的不是“占空比不标准”,而是高电平时间被上升沿吃掉。SCL高电平是靠上拉电阻给总线电容充电才建立起来的,如果电阻太大、电容又大,上升沿很缓,实际高电平窗口就被压缩。从机采样SCL高电平判读数据,窗口不够,就会出现地址发送时好时坏、寄存器读回来是0xFF这类问题。
STM32的I2C外设里有SCLH和SCLL两个参数,分别控制高电平和低电平时间,CubeMX会根据目标速率自动算。我遇到过的最典型错误是:某个老工程里手工填了奇怪的SCLL/SCLH,表面看频率对、占空比接近50%,但换了一块板子后总线电容变了,高电平建立不起来,IIS3DWB死活不回ACK。最后改成CubeMX自动计算,问题消失。所以这个参数宁可交给HAL自动生成,也不要自己拍脑袋填。
3. STM32C5的I2C外设配置:与G4对比后的迁移清单
3.1 C5与G4的I2C外设差异速览
我最初用G4的工程往C5上迁移,发现两边I2C的HAL层几乎没区别,但工程能编译通过和跑通是两回事。把差异列一下:
| 对比项 | STM32G4 | STM32C5 |
|---|---|---|
| 内核 | Cortex-M4,最高约170MHz | Cortex-M33,主频更高 |
| I2C IP | 带TIMINGR寄存器的第二代I2C | 同源IP,寄存器布局基本一致 |
| SCL/SDA所在GPIO复用 | 具体AF编号不同 | 具体AF编号不同,需查C5数据手册 |
| I2C时钟源 | PCLK1可选 | PCLK1,但PCLK1来源和G4不一定一样 |
| HAL初始化参数 | 同样通过Timing计算 | 同样通过Timing计算,CubeMX可直接生成 |
迁移的时候最坑的是GPIO复用表,C5的I2C1_SCL、I2C1_SDA可能分配在完全不同的GPIO和AF编号上。CubeMX里用图形化配置就绕开了这个坑,配置后生成的代码里AF编号是自动对齐的,这点强烈建议用CubeMX,不要手撸寄存器。
3.2 用HAL写通一条I2C通道
CubeMX配置步骤很简单:选择I2C1,Mode设为I2C,Speed设成400000,方向I2C,其余默认。生成的初始化代码大致长这样:
static void MX_I2C1_Init(void) { hi2c1.Instance = I2C1; hi2c1.Init.Timing = 0x10707DBC; // CubeMX根据PCLK1频率自动计算 hi2c1.Init.OwnAddress1 = 0; hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; hi2c1.Init.OwnAddress2 = 0; hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; if (HAL_I2C_Init(&hi2c1) != HAL_OK) { Error_Handler(); } }这里有个细节:不要手工去改Timing值。同一个400kHz,PCLK1是80MHz还是64MHz,算出来的Timing完全不同。你从别的工程复制过来的Timing值很可能恰好和你的时钟树不匹配,表现为总线波形看起来对,但从机就是通信失败。
初始化之后,还要确认GPIO初始化代码里把SCL/SDA引脚配成了开漏复用模式。STM32端I2C引脚配置成开漏是标配,和前面说的IIC开漏总线逻辑一致。如果引脚被误配成推挽复用,同样会出问题。
3.3 地址左移一位与“0x18还是0x30”的糊涂账
这是IIS3DWB用HAL库开发时翻车率最高的一个点。IIS3DWB的7位地址是0x18(SA0接地),但HAL函数里的DevAddress参数需要的是左移一位后的8位地址,也就是0x30。HAL_I2C_Mem_Read内部会把DevAddress直接写进I2C CR2寄存器,而I2C硬件在7位模式下发送的是Bits[7:1]作为地址位,Bit[0]当作读/写标志位,所以必须左移。
代码里我习惯这样定义:
#define IIS3DWB_I2C_ADDR (0x18U << 1) // 0x30,HAL用8位地址如果直接填0x18,硬件发出去的低7位就成了0x18右移一位的地址,从机地址对不上,结果是所有读操作NACK,寄存器读什么都是0xFF。这个问题示波器上看SCL、SDA波形都能看到地址字节不对,但波形解读需要经验,不如直接记住“7位地址必须左移一位再传给HAL”。
4. IIS3DWB寄存器驱动:真正的“震动计数据”是怎么读出来的
4.1 先做一次WHO_AM_I握手
新接一个传感器,第一步不是配置寄存器,而是读WHO_AM_I。IIS3DWB的WHO_AM_I寄存器地址是0x0F,固定值0x7B。这一步的作用是确认IIC地址、接线、供电统统正常,把芯片和板子上其他IIC设备区分开。
uint8_t who = 0; HAL_I2C_Mem_Read(&hi2c1, IIS3DWB_I2C_ADDR, 0x0F, I2C_MEMADD_SIZE_8BIT, &who, 1, 100); if (who != 0x7B) { // 不要继续向下初始化,先排查IIC链路 }我见到太多人跳过握手直接写寄存器,写完之后数据读出来是乱的,然后开始怀疑传感器坏了。其实WHO_AM_I不对,说明前面物理层或者地址层就有问题,后面一切都白搭。把这步做成一个强制check点,效率高得多。
4.2 CTRL1/CTRL3/CTRL6:三大控制寄存器一次讲清
IIS3DWB的寄存器比普通加速度计简单,核心就是三个控制寄存器。我参考的是ST官方开源的iis3dwb_reg驱动库,里面有完整的寄存器定义和读写函数,建议直接拿来用,改一行别自己从头造。
CTRL1(0x20)控制工作模式和输出数据率,包括连续测量模式、带宽选择(6kHz还是1.5kHz)、ODR档位。想拿数据,先从这个寄存器把芯片从掉电状态切到连续测量模式。CTRL3(0x22)里面有个IF_ADD_INC位很关键,它控制多字节读取时寄存器地址是否自动递增。读三轴加速度要连续读6字节,这个位如果不打开,6个字节读回来的全是同一个低字节,数据全错。CTRL6(0x25)里的BDU位控制数据块更新:BDU置1后,高字节和低字节会作为一个整体更新,避免读到一半时正好赶上新数据进来,导致高低字节拼出来的值是“混血”的。
我用一段典型的启动序列说明顺序:
uint8_t tmp; /* 1. 软件复位/掉电配置:先把CTRL1清零,让芯片回到受控状态 */ tmp = 0x00; HAL_I2C_Mem_Write(&hi2c1, IIS3DWB_I2C_ADDR, 0x20, I2C_MEMADD_SIZE_8BIT, &tmp, 1, 100); /* 2. CTRL3:打开地址自动递增 */ HAL_I2C_Mem_Read(&hi2c1, IIS3DWB_I2C_ADDR, 0x22, I2C_MEMADD_SIZE_8BIT, &tmp, 1, 100); tmp |= 0x04; // IF_ADD_INC: 具体掩码以数据手册为准 HAL_I2C_Mem_Write(&hi2c1, IIS3DWB_I2C_ADDR, 0x22, I2C_MEMADD_SIZE_8BIT, &tmp, 1, 100); /* 3. CTRL6:打开BDU */ HAL_I2C_Mem_Read(&hi2c1, IIS3DWB_I2C_ADDR, 0x25, I2C_MEMADD_SIZE_8BIT, &tmp, 1, 100); tmp |= 0x80; // BDU掩码 HAL_I2C_Mem_Write(&hi2c1, IIS3DWB_I2C_ADDR, 0x25, I2C_MEMADD_SIZE_8BIT, &tmp, 1, 100); /* 4. CTRL1:切到连续测量,6kHz带宽,ODR按需设置 */ tmp = 0x00; // 组合模式/带宽/ODR位,值参考DS的CTRL1表 HAL_I2C_Mem_Write(&hi2c1, IIS3DWB_I2C_ADDR, 0x20, I2C_MEMADD_SIZE_8BIT, &tmp, 1, 100);CTRL1的具体位组合值我不在这里硬列,因为你手里的数据手册版本可能和我手上这颗样片批次有细微差异,但寄存器地址和操作顺序是通用的。这也是为什么我建议寄存器宏定义直接从ST官方驱动里带,省得每颗料的数据手册一更新就要改一遍。
4.3 读X/Y/Z三轴加速度的标准套路
配置完成后,加速度数据可以从0x28开始的6个寄存器连续读出,排列是X_L、X_H、Y_L、Y_H、Z_L、Z_H。配好IF_ADD_INC之后,一条IIC命令就能全部读回:
uint8_t buf[6] = {0}; HAL_I2C_Mem_Read(&hi2c1, IIS3DWB_I2C_ADDR, 0x28, I2C_MEMADD_SIZE_8BIT, buf, 6, 100); int16_t x = (int16_t)((buf[1] << 8) | buf[0]); int16_t y = (int16_t)((buf[3] << 8) | buf[2]); int16_t z = (int16_t)((buf[5] << 8) | buf[4]);注意拼字节时高低顺序不要搞反。IIS3DWB输出的是16位有符号数,低字节在低地址,所以要先把L字节放到低8位,再把H字节左移8位拼成int16_t。拼反了的结果是数据看起来在跳动,或者正负反转,很多人会误判为传感器噪声大。
原始int16_t怎么换算成g,取决于传感器量程定义。IIS3DWB这类芯片通常是每满量程对应16位有效范围,例如±2g档位下约等于0.061mg/LSB。但换算系数必须看数据手册里FS的定义,不要想当然照搬其他ST传感器的系数。我一般先在静止状态下校准零点,再用手晃动传感器对比量级,确认换算系数没搞错,然后再进数据分析流程。
4.4 采样节奏与带宽的搭配:光“能读”还不行
有人读到三轴数据之后直接在主循环里循环读取,这就把传感器的能力废掉一半。说个具体数字:IIS3DWB最高ODR能到26.7kHz,此时每个采样间隔约37.5μs,读6字节加速度数据算上地址和ACK开销,在400kHz IIC下大约要50μs以上。这意味着阻塞式轮询根本跟不上最高ODR,轮询一圈还没读完,下一组数据又来了。
实际做振动监测,我建议按“监测带宽”而不是“芯片上限”来配置。如果关注的是电机轴承这类1kHz~6kHz高频故障,把带宽设置成6kHz,ODR按数据手册推荐对应档位;如果是普通设备状态粗筛,1.5kHz带宽配合较低ODR就够了,IIC 400kHz下用DMA读取完全能应付。不要一上来就顶满ODR,数据量大了处理不过来,反而把系统拖垮。
5. 实测踩坑记:三类让数据停摆的问题与完整排查链路
5.1 问题A:WHO_AM_I读回0xFF或者0x00
这是IIC开发最经典的问题,表象是读不回来。我排查的顺序是这样的:
首先用万用表量传感器VDD有没有电。IIS3DWB工作电压范围很宽,但模块板上常带稳压,如果板载LDO没启用,VDD脚电压为0,WHO_AM_I肯定不对。
然后用示波器或逻辑分析仪看SCL和SDA空闲电平。两者都应该是高电平,如果SDA或SCL被拉低,先检查上拉电阻,再看是不是IIC总线被某个设备锁死。接着确认SA0引脚接的位置,7位地址到底是0x18还是0x19,并且检查软件里传给HAL的地址有没有左移。
再用逻辑分析仪抓一次读操作。如果主机根本就没发时钟,问题在STM32一侧——I2C的PE位没使能、GPIO复用没配对、或者时钟源没打开,这些用HAL初始化时一般不会有问题,但如果你手改过初始化顺序就可能踩中。如果主机时钟正常发出了0x30地址,但没有ACK回,问题在从机或物理层,重点检查上拉电阻和传感器供电。
这个排查链路我用了很多次,基本能在五分钟内定位90%的WHO_AM_I问题。
5.2 问题B:SCL被拉死、总线超时
IIC总线被某个设备拉低卡死,是比地址错误更隐蔽的坑。我们遇到过一次:主循环每次读6字节,某次读到一半软件复位了,从机还在等待后续时钟传递数据,此时SDA正好被从机拉低,主机复位后又想发START,结果发现总线忙,一直卡死。
这种问题的恢复套路是“伪时钟”法:把SCL引脚临时配成普通GPIO输出,手动翻转9到16个脉冲,让从机把内部状态机推到位,释放SDA;然后再把引脚恢复成复用开漏模式。伪时钟恢复之后再读WHO_AM_I,通常就正常了。
// 伪时钟恢复示例:SCL手动翻转10次 for (int i = 0; i < 10; i++) { HAL_GPIO_WritePin(SCL_GPIO_Port, SCL_Pin, GPIO_PIN_RESET); delay_us(5); HAL_GPIO_WritePin(SCL_GPIO_Port, SCL_Pin, GPIO_PIN_SET); delay_us(5); }我后来在软件上做了层保险:每次IIC操作都设置超时,超时后先执行一次伪时钟再重新发起通信。这套机制加进去之后,现场跑了一周没再出现死锁。
5.3 问题C:数据偶发跳变,查到最后是时钟配置
数据能读出来,但偶发出现一个极大的值,这种“幽灵跳变”最容易误导人。最初我怀疑是传感器噪声,抓波形看数据也正常,后来发现是BDU没开,读到某一刻数据高低字节正好跨越了两次更新边界,拼出来的数就会错得离谱。把CTRL6的BDU位置1之后,跳变立刻消失。
还有一个案例是SCL高电平时间不够的隐性故障:某块板子用了我手工填的占空比参数,低速时正常,切到400kHz后偶发NACK,表现为10组数据里有一组全0xFF。示波器抓到SCL上升沿明显变缓,才意识到是总线电容和电阻导致高电平窗口被压缩。最后把I2C Timing参数改成CubeMX自动计算,并把上拉电阻从4.7kΩ换成2.2kΩ,问题彻底解决。
排查数据跳变时,我的顺序是:先看BDU,再看电源纹波,再看总线波形,最后才怀疑传感器本体,倒过来排查会走很多弯路。
5.4 关于STM32C5芯片采购的现状
不少人私信问STM32C5在哪能买到。目前C5系列还处于从工程样片到量产过渡的阶段,个人开发者想直接拿现货不容易,最省事的是买NUCLEO-C5评估板,板载的芯片够你把外设全部跑一遍。项目要量产的话,直接联系正规授权代理商确认供货节点和批次,比在网上到处找散片靠谱得多。你在评估板上验证好的代码,批量换用同型号芯片后基本不用改。
6. 进阶:DMA+数据就绪中断,让主循环喘口气
6.1 数据就绪中断与时间戳
做真正的振动分析时,我强烈建议不要在主循环里反复阻塞读IIC。传感器的INT1引脚可以在数据就绪时拉高,把它接到STM32C5的一个EXTI引脚,这样每次新数据到来都会触发一次中断,读取动作和数据处理解耦。
具体配置,在CubeMX里把INT1对应GPIO设为上升沿中断,中断回调里只做一件事:置一个“数据已就绪”标志,或者直接发起DMA读取。真正的时间戳不应该在中断里用HAL_GetTick这种毫秒级时钟,而要挂一个定时器计数器,把每次采样的时间精确到微秒级。振动数据做FFT时,采样间隔不均匀会让频谱出现谱泄漏,固定时间步长是后续分析质量的前提。
6.2 用DMA读6字节后的主循环结构
DMA读取三轴数据只需要一条HAL函数:
HAL_I2C_Mem_Read_DMA(&hi2c1, IIS3DWB_I2C_ADDR, 0x28, I2C_MEMADD_SIZE_8BIT, (uint8_t *)&axis_buf, 6);DMA传输完成时会触发HAL_I2C_MemRxCpltCallback,在这个回调里把读到的原始值换算成g,放进环形缓冲区,主循环只负责消费缓冲区数据做特征计算。这样IIC读取不再占用主循环时间,也不会被其他任务打断导致波形毛刺。
一个小提示:DMA读取期间不要同时用阻塞方式去操作同一个I2C外设,否则总线时序会乱。我在实际工程里把IIC访问做成一个简单的互斥状态机,DMA传输中任何阻塞调用一律等待,这样就不会出现同一时刻两个读操作抢占总线的情况。把这一层处理好之后,整个数据采集链路在400kHz下跑得很稳。
最后再说一个经验:不管你是要跑频谱分析还是一般的时域振动监测,先把“数据能连续不断流”这一步跑扎实,再往上层加算法。IIC这种看起来基础的通信方式,恰恰最容易在细节上埋雷,地址左移一位、上拉电阻取值、BDU开关、DMA互斥,这几件事处理干净,IIS3DWB在STM32C5上的数据获取就算真正过关了。