1. 拿到IIS3DWB这颗震动计,为什么我第一反应是走SPI而不是I2C
IIS3DWB 是意法半导体推出的一颗面向工业级振动监测的三轴数字加速度计,量程覆盖 ±2g 到 ±16g,带宽能拉到 6kHz 以上,内部还带一个 3KB 的 FIFO。它的定位很明确:机器状态监测、预测性维护、结构健康监测这类需要连续采振动波形的场景。我第一次拿到这块芯片的时候,评估板上的通信接口同时引出了 SPI 和 I2C,但我几乎没有犹豫就选了 SPI,原因很直接——IIS3DWB 在 I2C 模式下最高只能跑到 1MHz 左右的时钟,而 SPI 可以轻松跑到 10MHz 甚至更高。对于振动监测来说,采样率和数据吞吐量是命门,I2C 那点带宽根本喂不饱它。
STM32C5 是 ST 新出的 Cortex-M33 内核产品线,主频和总线架构相比老的 F103 系列提升明显,SPI 外设的时钟源也更灵活。我这次用的板子是 STM32C5 的 Nucleo 开发板,配合 IIS3DWB 的评估子板,通过飞线把 SPI 四根线接起来。整个链路是:STM32C5 做 SPI Master,IIS3DWB 做 SPI Slave,模式选 CPOL=1/CPHA=1(也就是 SPI Mode 3),这是 IIS3DWB 数据手册里明确要求的。很多人第一次调这颗芯片会卡在时序模式上,后面我会专门讲这个坑。
这篇文章适合谁看?如果你手上有 STM32C5 或者类似的 STM32 新系列芯片,想通过 SPI 把 IIS3DWB 的振动数据读出来,但不确定 CubeMX 里怎么配、寄存器怎么读写、FIFO 怎么用,那这篇内容基本能覆盖你从零到跑通的全过程。我会把 CubeMX 的配置逻辑、SPI 读写函数的封装、WHO_AM_I 验证、单次读取和 FIFO 批量读取都讲清楚,同时把我在调试过程中踩过的几个坑原原本本还原出来。
注意:IIS3DWB 的 SPI 接口不支持三线模式,必须用标准的四线 SPI(SCLK、MOSI、MISO、CS),CS 建议用硬件片选,软件片选在高速率下容易出问题。
2. STM32C5 的 SPI 外设配置:CubeMX 里那几个参数到底怎么选
2.1 时钟树与 SPI 时钟源的确定
打开 STM32CubeMX,选好 STM32C5 对应的芯片型号之后,第一件事是配时钟树。STM32C5 的 SPI1 挂在 APB2 总线上,SPI2/SPI3 挂在 APB1 上。我这次用的是 SPI1,因为它的时钟源上限更高。在 Clock Configuration 页面里,把 SYSCLK 拉到芯片允许的最高频率(具体取决于你用的具体型号,我手上这颗跑到了 144MHz),然后确认 APB2 的分频系数。假设 APB2 最终是 144MHz,SPI1 的波特率预分频器选 16,那 SCLK 就是 9MHz。这个速率对 IIS3DWB 来说完全在安全范围内,它的 SPI 时钟最高支持 10MHz。
这里有个细节值得说:STM32C5 的 SPI 外设支持可配置的 FIFO 阈值和 8/16 位数据宽度。IIS3DWB 的寄存器地址是 7 位,读写位在最高位,数据是 8 位。所以 SPI 数据宽度必须设成 8 位,不能设 16 位,否则地址和数据会错位。我在第一次配的时候手滑选了 16 位,结果读出来的 WHO_AM_I 永远是 0xFFFF,排查了半天才发现是这个原因。
2.2 SPI 模式与片选管理
在 SPI1 的 Parameter Settings 里,几个关键参数这样设:
- Frame Format:Motorola
- Data Size:8 Bits
- First Bit:MSB First
- Clock Polarity (CPOL):High
- Clock Phase (CPHA):2 Edge
- NSS:Hardware NSS Output(如果你用硬件片选)或者 Disable(用软件片选控制 GPIO)
CPOL=High、CPHA=2Edge 合起来就是 SPI Mode 3。IIS3DWB 数据手册里写的时序图明确是 Mode 3,SCLK 空闲高电平,数据在第二个边沿采样。如果你配成 Mode 0,读出来的数据会整体偏移一位,表现为数值乱跳或者固定错位。
关于片选,我强烈建议用硬件 NSS。STM32C5 的 SPI 外设支持硬件 NSS 输出,配置之后片选信号由硬件自动拉低和拉高,时序精准,不会因为软件干预导致 CS 提前拉高或延后拉低。如果你用软件片选,在 9MHz 的速率下,GPIO 翻转的延迟可能导致最后一个字节还没移完 CS 就抬起来了,数据就丢了。我实测过,软件片选在 1MHz 以下问题不大,但上了 5MHz 之后误码率明显上升。
2.3 DMA 通道的预留
虽然这一篇主要讲轮询方式读数据,但我在 CubeMX 里还是把 DMA 配上了。SPI1_RX 和 SPI1_TX 各分配一个 DMA 通道,模式选 Normal,数据宽度 Byte。为什么要提前配?因为 IIS3DWB 的 FIFO 一旦启用,一次要读几百个字节,轮询方式会占满 CPU,后面升级到 DMA 就是改几行代码的事。提前把 DMA 配好,后面切换的时候不用重新生成工程。
配置完成后生成代码,CubeMX 会自动生成MX_SPI1_Init()函数。我习惯在这个函数基础上再包一层自己的 SPI 读写函数,不直接调 HAL 的HAL_SPI_TransmitReceive,因为 IIS3DWB 的读写时序有特殊要求——地址字节的最高位是读写标志位,读操作要置 1,写操作要置 0。
3. IIS3DWB 的 SPI 读写时序:地址字节里藏着的读写位
3.1 单寄存器读写的底层逻辑
IIS3DWB 的 SPI 通信协议不复杂,但有一个地方容易搞错:它的寄存器地址是 7 位的,传输的时候要把地址放在一个字节的低 7 位,最高位(bit7)作为读写控制位。读操作时 bit7=1,写操作时 bit7=0。也就是说,如果你想读WHO_AM_I寄存器(地址 0x0F),实际发送的字节是0x0F | 0x80 = 0x8F。写操作则是0x0F & 0x7F = 0x0F。
SPI 的传输过程是这样的:CS 拉低,先发一个字节的地址(带读写位),然后紧接着发一个字节的 dummy(读操作时)或者要写的数据(写操作时)。读操作时,从机在第二个字节的时钟周期把数据放到 MISO 上,主机收回来就是寄存器的值。写操作时,主机在第二个字节把数据放到 MOSI 上,从机接收。
我封装了两个函数,一个读一个写:
uint8_t IIS3DWB_ReadReg(uint8_t reg) { uint8_t tx_buf[2]; uint8_t rx_buf[2]; tx_buf[0] = reg | 0x80; // 读操作,bit7置1 tx_buf[1] = 0x00; // dummy byte HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); HAL_SPI_TransmitReceive(&hspi1, tx_buf, rx_buf, 2, 100); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); return rx_buf[1]; } void IIS3DWB_WriteReg(uint8_t reg, uint8_t data) { uint8_t tx_buf[2]; tx_buf[0] = reg & 0x7F; // 写操作,bit7清0 tx_buf[1] = data; HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, tx_buf, 2, 100); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); }如果你用的是硬件 NSS,那HAL_GPIO_WritePin那两行可以去掉,硬件会自动控制。但我个人还是习惯手动控制 CS,因为调试的时候用逻辑分析仪抓波形更直观,能看到 CS 和 SCLK 的对应关系。
3.2 多字节连续读取的实现
IIS3DWB 支持地址自动递增,读多个连续寄存器的时候,只需要发一次地址,然后连续读多个字节就行。比如读三轴加速度的 6 个字节(X_L、X_H、Y_L、Y_H、Z_L、Z_H),地址从OUTX_L_A(0x28)开始,连续读 6 个字节。这里要注意,IIS3DWB 的加速度数据是 16 位有符号数,低字节在前,高字节在后,拼的时候要(int16_t)((high << 8) | low)。
void IIS3DWB_ReadAccel(int16_t *accel) { uint8_t tx_buf[7] = {0}; uint8_t rx_buf[7] = {0}; tx_buf[0] = 0x28 | 0x80; // OUTX_L_A 地址,读操作 HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); HAL_SPI_TransmitReceive(&hspi1, tx_buf, rx_buf, 7, 100); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); accel[0] = (int16_t)((rx_buf[2] << 8) | rx_buf[1]); accel[1] = (int16_t)((rx_buf[4] << 8) | rx_buf[3]); accel[2] = (int16_t)((rx_buf[6] << 8) | rx_buf[5]); }注意tx_buf的长度是 7,第一个字节是地址,后面 6 个是 dummy。rx_buf[0]是发地址时收回来的垃圾数据,从rx_buf[1]开始才是有效数据。这个偏移量很容易搞错,我第一次写的时候把rx_buf[0]当成了 X_L,结果读出来的值整体错了一位。
3.3 用逻辑分析仪验证时序
调 SPI 最怕的就是时序不对但代码看起来没问题。我的做法是拿一个逻辑分析仪(或者带 SPI 解码功能的示波器),抓 CS、SCLK、MOSI、MISO 四根线。重点看几个地方:CS 拉低之后第一个 SCLK 边沿是不是在半个周期之后出现,地址字节的 bit7 是不是正确的读写位,MISO 上的数据是不是在第二个边沿稳定。我抓过一次波形,发现 CS 拉低和第一个 SCLK 之间几乎没有延迟,导致从机还没准备好就收到了时钟,读出来的数据全是 0xFF。后来在 CS 拉低之后加了一个微秒级的延时,问题就解决了。虽然 IIS3DWB 手册上说 CS 建立时间只要 10ns,但实际板子上有走线电容,留点余量更稳。
4. 从 WHO_AM_I 到 FIFO:分阶段验证你的 SPI 链路
4.1 第一步永远是读 WHO_AM_I
不管调什么传感器,我第一步永远是读 WHO_AM_I 寄存器。IIS3DWB 的WHO_AM_I地址是 0x0F,返回值应该是 0x6B。如果你读出来不是 0x6B,那说明 SPI 链路有问题,后面的寄存器配置都不用看了。我遇到过几种情况:读出来是 0x00,说明 MISO 没接好或者从机没供电;读出来是 0xFF,说明 MISO 被拉高了,可能是 CS 没拉低或者 SPI 模式不对;读出来是 0x6B 但偶尔跳变,说明时序余量不够,需要降速或者加延时。
uint8_t whoami = IIS3DWB_ReadReg(0x0F); if (whoami == 0x6B) { printf("IIS3DWB detected!\n"); } else { printf("WHO_AM_I = 0x%02X, expected 0x6B\n", whoami); }这一步过了,心里就有底了。接下来配置CTRL1_XL寄存器,设置量程和输出数据率。IIS3DWB 的CTRL1_XL地址是 0x10,bit7-4 是 ODR 设置,bit3-2 是量程设置。我一般先用 1.6kHz 的 ODR 和 ±4g 的量程做验证,这个组合比较中庸,不容易出问题。
4.2 单次读取验证数据通路
配置完CTRL1_XL之后,等个几毫秒让传感器稳定,然后读STATUS_REG(地址 0x1E)看XLDA位是不是置 1。如果置 1,说明有新数据了,这时候读OUTX_L_A开始的 6 个字节就能拿到加速度值。我习惯把原始值转成 mg 来验证:±4g 量程下,灵敏度是 0.122 mg/LSB,所以accel_mg = raw * 0.122。把板子平放,Z 轴应该接近 1000mg,X 和 Y 接近 0。如果 Z 轴是负的,说明传感器贴反了,或者坐标轴定义和你的预期不一致。
这一步能过,说明 SPI 读写、寄存器配置、数据解析都是对的。但单次读取有个问题:它依赖STATUS_REG的轮询,在高速 ODR 下会丢数据。比如 ODR 设到 6.6kHz,你轮询的速度根本跟不上,读到的永远是旧数据。所以真正做振动监测的时候,必须用 FIFO。
4.3 FIFO 批量读取的配置与陷阱
IIS3DWB 内部有一个 3KB 的 FIFO,可以存 512 组三轴数据(每组 6 字节)。配置 FIFO 涉及几个寄存器:FIFO_CTRL1到FIFO_CTRL4,还有CTRL4_INT1之类的引脚配置。我一般用 Stream 模式或者 FIFO 模式,Watermark 设到 256 组,这样半满的时候触发中断,一次性读 256 组数据,CPU 占用率很低。
配置 FIFO 的时候有个坑:FIFO_CTRL4里的FIFO_MODE字段必须最后设置,因为一旦设成 FIFO 模式,FIFO 就开始工作了,这时候再改其他参数可能会丢数据。正确的顺序是:先设 Watermark,再设 ODR,最后设 FIFO_MODE。我一开始没注意顺序,先把 FIFO_MODE 设成了 Stream,然后去改 Watermark,结果 FIFO 里的数据全乱了。
读 FIFO 的时候,地址从FIFO_DATA_OUT_TAG(0x78)开始,每个数据组有一个 Tag 字节加 6 个数据字节,一共 7 个字节。Tag 字节的高 5 位是标签,低 3 位是计数。连续读的时候,每 7 个字节解析一组。这里要注意,SPI 读 FIFO 的时候,地址发完之后要连续读 N*7 个字节,中间不能断 CS,否则 FIFO 指针会复位。
#define FIFO_WATERMARK 256 uint8_t fifo_buf[FIFO_WATERMARK * 7 + 1]; fifo_buf[0] = 0x78 | 0x80; // FIFO_DATA_OUT_TAG HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); HAL_SPI_TransmitReceive(&hspi1, fifo_buf, fifo_buf, FIFO_WATERMARK * 7 + 1, 1000); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); // 解析:从 fifo_buf[1] 开始,每 7 个字节一组 for (int i = 0; i < FIFO_WATERMARK; i++) { uint8_t tag = fifo_buf[1 + i * 7]; int16_t x = (int16_t)((fifo_buf[2 + i * 7] << 8) | fifo_buf[1 + i * 7 + 1]); // ... 解析 Y 和 Z }提示:FIFO 读取的缓冲区大小要算准,
FIFO_WATERMARK * 7 + 1是必须的,那个 +1 是地址字节。少一个字节就会越界,我因为这个原因调试了一下午。
5. 调试过程中踩过的三个坑和对应的排查思路
5.1 坑一:SPI 模式配错导致数据整体偏移
第一次读WHO_AM_I的时候,返回值是 0xD6,正好是 0x6B 左移一位。这个现象很典型,就是 SPI 模式配错了。IIS3DWB 要求 Mode 3,我配成了 Mode 0,导致数据在错误的边沿被采样,整体偏移了一位。把 CPOL 改成 High、CPHA 改成 2Edge 之后,WHO_AM_I立刻变成了 0x6B。这个坑的排查思路是:如果读出来的值是预期值的移位版本,优先检查 SPI 模式。
5.2 坑二:CS 拉高太早导致最后一个字节丢失
用软件片选的时候,我在HAL_SPI_TransmitReceive返回之后立刻拉高了 CS。但在 9MHz 的速率下,HAL 函数的返回和最后一个 SCLK 边沿之间可能有几十纳秒的延迟,CS 提前拉高会让从机认为传输结束,最后一个字节就丢了。表现是读多字节数据的时候,最后一个字节永远是 0x00 或者 0xFF。解决办法有两个:一是改用硬件 NSS,二是拉高 CS 之前加一个__NOP()或者几微秒的延时。我最后改成了硬件 NSS,问题彻底消失。
5.3 坑三:FIFO 读取时 CS 中断导致指针复位
这个坑最隐蔽。我在读 FIFO 的时候,为了打印调试信息,在读取过程中插入了一个printf,结果printf耗时太长,CS 在传输过程中被其他中断打断,拉高了一下又拉低。IIS3DWB 的 FIFO 指针在 CS 拉高的时候会复位,所以第二次拉低之后读到的数据是从 FIFO 头部重新开始的,导致数据重复。排查这个问题花了我最久的时间,因为逻辑分析仪抓到的波形看起来只是 CS 上有一个毛刺,很容易忽略。后来我把 FIFO 读取放在了一个关中断的临界区里,问题就解决了。
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| WHO_AM_I 返回 0x00 | MISO 未连接或从机未供电 | 检查硬件连线,测量从机供电 | 重新焊接或更换板子 |
| WHO_AM_I 返回 0xFF | CS 未拉低或 SPI 模式错误 | 逻辑分析仪抓 CS 和 SCLK | 检查 CS 控制逻辑,确认 Mode 3 |
| 数据整体偏移一位 | SPI 模式配错 | 对比预期值和实际值的二进制 | 改为 CPOL=1, CPHA=1 |
| 多字节读取最后一字节丢失 | CS 拉高太早 | 抓 CS 和最后一个 SCLK 的时序 | 改用硬件 NSS 或加延时 |
| FIFO 数据重复 | 传输过程中 CS 被中断打断 | 抓 CS 波形看是否有毛刺 | 关中断或改用 DMA |
6. 从轮询到 DMA:下一步的优化方向
轮询方式读 FIFO 在 1.6kHz ODR 下勉强够用,但 ODR 拉到 6.6kHz 的时候,CPU 大部分时间都在等 SPI 传输完成,根本干不了别的事。我下一步的计划是改成 DMA 模式:SPI1_RX 用 DMA 通道,FIFO 的 Watermark 中断触发 DMA 传输,传输完成中断里再解析数据。这样 CPU 只在数据搬完之后介入一次,占用率能降到 5% 以下。
DMA 配置的时候要注意,SPI 的 DMA 请求要在HAL_SPI_TransmitReceive_DMA之前使能,而且 DMA 的传输长度要算准。IIS3DWB 的 FIFO 读取是单向的(只读),所以其实只需要 RX DMA,TX 那边发完地址字节之后就可以不管了。但 HAL 库的HAL_SPI_TransmitReceive_DMA要求 TX 和 RX 都配 DMA,所以我一般用HAL_SPI_Receive_DMA配合手动发地址的方式,或者干脆用 LL 库直接操作寄存器,更灵活。
另外,STM32C5 的 SPI 支持 FIFO 阈值中断,可以设成 RX FIFO 达到 8 字节就触发中断,这样配合 DMA 的循环模式,可以实现连续不断的采集。不过这是后话了,这一篇先把轮询方式跑通,把数据链路验证清楚,后面再折腾 DMA 就有底气了。
我在实际使用中发现,IIS3DWB 这颗芯片的 SPI 接口虽然标称支持 10MHz,但在长走线或者飞线的情况下,7-8MHz 更稳。如果你发现高速下数据偶尔出错,不妨先把 SCLK 降到 5MHz 试试,确认链路没问题之后再逐步往上提。振动监测这个场景,数据完整性比速度更重要,宁可降速也不能丢数据。