先把话放在前面:这篇文章是系列第一篇,主题是“用STM32C5通过SPI把IIS3DWB10IS这款宽带振动传感器的数据读回来”。做旋转机械振动监测、轴承故障诊断或者状态预测性维护的朋友,大概率会碰到类似需求——传感器选好了,MCU选好了,结果第一步SPI通信就卡了两天。这篇就把我实际调通这套组合的完整过程拆开讲,包括硬件接线、CubeMX配置、寄存器读写、数据解析和踩坑记录。不管你是刚接触STM32还是已经在做振动采集,照着下面这套流程走,能少走不少弯路。
我用的传感器是ST的IIS3DWB10IS,这颗料在振动监测里出镜率挺高。它属于超宽带三轴加速度计,输出数据速率最高能到26.7kHz,平坦带宽6.4kHz,专门为工业振动监测、声学分析这类场景设计。MCU这边选了STM32C5系列,Cortex-M33内核,主频和资源都够用,而且CubeMX支持很完善,直接生成HAL工程特别顺手。这套组合的好处是SPI时钟可以跑到10MHz,数据量大的时候不用担心瓶颈,I2C在这种带宽需求下基本不够看。
1. 项目整体设计与思路拆解
1.1 为什么是IIS3DWB10IS而不是普通六轴传感器
很多朋友看到“振动计”第一反应是拿MPU6050、LSM6DSO这类惯性传感器去测。但这里有个核心差异:普通消费级IMU的带宽大多在几百赫兹到2kHz左右,而工业设备振动信号的特征频率经常落在几kHz甚至更高。比如齿轮箱啮合频率、轴承外圈故障频率(BPFO),高频段才是故障的早期信号。IIS3DWB10IS的优势就在这,满量程支持±2g到±16g,SPI接口下ODR最高26.7kHz,带宽6.4kHz,这数据在同级别里相当能打。
另外这颗传感器在低噪声和温度稳定性上做了优化,长期监测场景下漂移更小。我之前用普通六轴做过一次轴承跑合实验,高频成分直接被带宽卡死,频谱上什么都看不出来,换成IIS3DWB之后同样工况下特征频率一清二楚。所以选型阶段就把带宽和噪声放在第一位,而不是只看分辨率位数。
1.2 SPI为什么比I2C更适合这个项目
热词里很多人搜“iic和spi的区别”,放到这个项目里其实很典型。I2C是半双工、开漏加上拉,速率上限在1MHz左右(实际稳定跑400kHz居多),而且每读一帧数据要发设备地址、寄存器地址,开销很大。IIS3DWB的最高ODR是26.7kHz,每个样本三轴共6字节,按满速率算光数据量就是160KB/s左右,如果再算上FIFO批量读取,I2C的压力非常大。
SPI是全双工,没有地址帧,CS拉低之后直接读写寄存器,时钟能跑到10MHz。同样一帧数据,SPI的耗时只有I2C的十分之一。再加上后续如果要做FFT频谱分析,数据是连续往上位机或者SD卡里灌的,SPI的带宽余量让整个链路没那么容易卡住。
1.3 整体系统架构
这套系统的硬件链路大致是:IIS3DWB10IS通过四线SPI接到STM32C5,传感器中断引脚(INT1)接到MCU的EXTI,用于数据就绪或FIFO阈值通知。MCU收到中断后通过SPI DMA读取FIFO数据,做简单校验后再通过UART/USB发送给上位机。为了省CPU,读取和传输都走DMA,主循环只做状态管理和数据帧拼装。
第一次做这个项目不建议一上来就上DMA+FIFO+中断的组合,先把裸SPI轮询读通,再逐步加复杂度。我后面代码部分也是这样安排的,先保证能读到WHO_AM_I,再谈性能优化。
2. 硬件接线与关键设计要点
2.1 引脚分配与连接对照
STM32C5的SPI引脚复用和F1/F4系列不完全一样,所以不要凭老经验直接对引脚号。我这边用CubeMX查了目标封装的具体AF映射,最终用的SPI1,接线如下表:
| 功能 | IIS3DWB10IS引脚 | STM32C5引脚 | 说明 |
|---|---|---|---|
| SCLK | SPC | PA5 (SPI1_SCK) | SPI时钟 |
| MOSI | SDI | PA7 (SPI1_MOSI) | 主发从收 |
| MISO | SDO | PA6 (SPI1_MISO) | 主收从发 |
| CS | CS | PA4 (GPIO输出) | 片选,手动控制 |
| INT1 | INT1 | PB0 (EXTI) | 数据就绪/FIFO中断 |
| VDD | 3.3V | 3.3V | 传感器供电 |
| GND | GND | GND | 共地 |
| VDD_IO | 3.3V | 3.3V | IO电平参考 |
注意SDO/SA0这个引脚,在SPI模式下它就是MISO数据输出,但如果是I2C模式,它是地址选择脚。用SPI时直接把SDO接到MCU的MISO就行,SA0功能可以忽略。我实际焊板子时把SDO和SA0搞混过一次,MISO上永远没数据,排查了半天才发现是封装上两个引脚位置看反了。
2.2 硬件设计上容易忽略的几个点
第一,电源去耦。IIS3DWB是精密模拟+数字混合器件,VDD和GND之间建议放一个1uF瓷片电容靠近引脚,有条件的再并联一个0.1uF去耦。板上如果电机驱动或开关电源离得近,还建议加一颗磁珠隔离。这个传感器对电源纹波不是特别敏感,但电源脏了直接体现在输出噪声上,FFT底噪会变差。
第二,电平匹配。IIS3DWB的VDD和VDD_IO不用强制同电压,VDD_IO决定数字引脚的逻辑电平。如果MCU是3.3V,就把VDD_IO也接3.3V。如果MCU那边是1.8V IO,VDD_IO接1.8V,避免逻辑电平不匹配导致读到的数据全是0xFF或者0x00。
第三,CS引脚上拉。CS建议在传感器端接一个10k上拉到VDD_IO,防止上电瞬间误触发片选。之前有一版板子没加上拉,MCU复位时SPI引脚处于高阻态,CS被干扰拉低,传感器产生了莫名其妙的中断。
2.3 中断引脚与MCU的匹配
INT1是推挽输出,直接连MCU的PB0没问题。配成上升沿触发或者电平触发都可以,我一般用上升沿。注意INT1高电平有效还是低电平有效是通过寄存器配置的,默认通常是高有效,别想当然,后面会讲到。
3. CubeMX工程配置与SPI参数计算
3.1 新建工程与时钟树设置
STM32C5在CubeMX里很好找,按具体型号选就行。时钟树方面,我直接用HSI+PLL把系统时钟拉到头(具体主频看型号,C5系列支持到250MHz左右)。SPI外设时钟来自APB总线,先确认APB1/APB2的时钟频率是多少,再算分频系数。
我的SPI1挂载在APB2上,APB2时钟默认是系统时钟除以2。假设系统时钟250MHz,APB2就是125MHz。要把SPI时钟控制在10MHz以内,125MHz除以16等于7.8125MHz,这个值是安全可用的。除以8是15.625MHz,超了IIS3DWB的10MHz上限,不能直接用。
3.2 SPI参数配置的具体选项
在CubeMX的SPI1配置界面里,我用的参数组合:
| 参数项 | 设置值 |
|---|---|
| Mode | Full-Duplex Master |
| Hardware NSS Signal | Disable(用软件片选) |
| Clock Polarity (CPOL) | Low |
| Clock Phase (CPHA) | 1Edge |
| Prescaler | 16(实际SPI时钟7.8125MHz) |
| Data Size | 8 bit |
| First Bit | MSB First |
| CRC Calculation | Disabled |
CPOL=Low、CPHA=1Edge对应SPI Mode 0,实测这套配置在IIS3DWB上是通的。如果读WHO_AM_I一直不对,可以改成CPOL=High、CPHA=2Edge(SPI Mode 3)。这颗传感器手册里写的是支持Mode 0和Mode 3,别的模式不用试。
这里多说一句为什么不用硬件NSS。STM32的硬件NSS行为比较繁琐,主模式下会自动拉低片选,但时序上和传感器要求的CS边沿配合不好,尤其是连续读FIFO时容易出现片选提前释放的问题。软件片选最直观:要通信就把CS拉低,通信完拉高,时序完全自己控制,排查问题也方便。
3.3 GPIO与DMA配置
SPI1的SCK、MOSI、MISO在CubeMX的Pinout视图里直接选AF5复用即可。CS(PA4)配成GPIO_Output,初始电平设为High。INT1(PB0)配成GPIO_EXTI,上升沿触发,使能对应的EXTI中断。
DMA这里建议先不配,等轮询读通之后再加。如果配DMA,在CubeMX里把SPI1_RX和SPI1_TX都加上,模式Normal,数据宽度Byte。我后续代码示例用的是HAL_UART_Transmit做调试输出,UART也开一下,波特率115200。
4. SPI寄存器读写与代码实现
4.1 理解IIS3DWB的寄存器访问协议
IIS3DWB的SPI访问方式和大多数ST传感器一致:每帧数据第一个字节是高7位寄存器地址加1位读写标志位(最高位为1表示读,0表示写)。CS低有效,SCLK时钟沿采样,MSB先行。
举个例子,读WHO_AM_I寄存器(地址0x0F)时,发送字节是0x8F。后面跟上1个字节的虚拟数据,接收到的第二个字节就是寄存器内容。写寄存器时,发送0x00|寄存器地址作为第一个字节,后面接要写入的数据。
为了连续读多个寄存器,CTRL3寄存器的IF_ADD_INC位要置1。这个位开启后,SPI读操作会自动递增寄存器地址,读FIFO或读三轴输出时一帧搞定,效率高很多。默认值我记得是1,但保险起见初始化时还是显式设置一次。
注意:不同批次的IIS3DWB寄存器地址可能有细微差异,以下代码中的寄存器定义以ST官方数据手册为准。我这边验证过的是常规量产版本,建议拿到芯片后先读一遍WHO_AM_I确认通信正常。
4.2 SPI底层读写函数模板
用HAL库做SPI读写非常直接,核心就是HAL_SPI_TransmitReceive。CS控制我用宏封装:
#define IIS3DWB_CS_LOW() HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET) #define IIS3DWB_CS_HIGH() HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET) uint8_t iis3dwb_spi_read_reg(uint8_t reg) { uint8_t tx_data[2]; uint8_t rx_data[2] = {0}; tx_data[0] = reg | 0x80; // 读标志 tx_data[1] = 0x00; // dummy byte IIS3DWB_CS_LOW(); HAL_SPI_TransmitReceive(&hspi1, tx_data, rx_data, 2, 100); IIS3DWB_CS_HIGH(); return rx_data[1]; } void iis3dwb_spi_write_reg(uint8_t reg, uint8_t value) { uint8_t tx_data[2]; tx_data[0] = reg & 0x7F; // 写标志 tx_data[1] = value; IIS3DWB_CS_LOW(); HAL_SPI_TransmitReceive(&hspi1, tx_data, rx_data, 2, 100); IIS3DWB_CS_HIGH(); }这里要注意HAL_SPI_TransmitReceive的TX和RX长度必须一致,我是直接传同一个长度参数两个数组,读寄存器也一样,保证时钟数是对的。
4.3 多字节连续读取函数
如果要读FIFO或者三轴六字节数据,建议单独写一个多字节读取函数:
void iis3dwb_spi_read_multi(uint8_t reg, uint8_t *buf, uint16_t len) { uint8_t tx_data[1]; uint8_t rx_data[1]; // 第一个字节的接收数据不用 IIS3DWB_CS_LOW(); tx_data[0] = reg | 0x80; HAL_SPI_Transmit(&hspi1, tx_data, 1, 100); // 之后的字节都是时钟驱动,MCU发0x00同时收数据 for (uint16_t i = 0; i < len; i++) { tx_data[0] = 0x00; HAL_SPI_TransmitReceive(&hspi1, tx_data, &rx_data[0], 1, 100); buf[i] = rx_data[0]; } IIS3DWB_CS_HIGH(); }这个函数在开启动态FIFO读取时非常关键,后面解析FIFO数据会用到。
4.4 芯片ID验证与初始化序列
拿到板子第一步永远是读WHO_AM_I。IIS3DWB10IS的WHO_AM_I寄存器地址是0x0F,正常应该返回0x7B。连上之后我习惯先做个基本检查:
uint8_t who_am_i = iis3dwb_spi_read_reg(0x0F); if (who_am_i == 0x7B) { // 通信正常 } else { // 打印错误,检查接线和SPI模式 }如果这一步不对,别急着往下调,大概率是SPI模式、引脚复用或者时序问题。我当时就卡在这,后来发现是CPOL/CPHA设成了Mode 1,改成Mode 0立刻好。
接下来是初始化序列。CTRL1寄存器(0x20)用来设置ODR和量程,CTRL3寄存器(0x22)里有个IF_ADD_INC位,记得确认是1。我为了调试方便,先设置了默认的量程±2g和低功耗模式,代码里大概这样:
uint8_t ctrl1 = 0x00; ctrl1 |= (0x0F << 4); // ODR配置,具体值查手册 ctrl1 |= (0x00 << 2); // 量程 ±2g iis3dwb_spi_write_reg(0x20, ctrl1); uint8_t ctrl3 = iis3dwb_spi_read_reg(0x22); ctrl3 |= 0x02; // IF_ADD_INC = 1 iis3dwb_spi_write_reg(0x22, ctrl3);关于CTRL1的ODR位域具体编码,不同版本手册有差异,最稳妥的做法是直接查你手里的数据手册对照表,不要凭记忆写死。我的经验是,如果ODR配错了,表现就是数据输出速率和你预期不一致,但通信本身正常,这个坑比较隐蔽。
4.5 中断引脚配置(可选)
如果打算用中断方式,把INT1配置成数据就绪输出。具体在CTRL2(0x21)或CTRL4里设置中断映射,把Data-Ready信号映射到INT1。我用的代码片段:
uint8_t ctrl2 = iis3dwb_spi_read_reg(0x21); ctrl2 |= 0x01; // 把DRDY映射到INT1,具体位以手册为准 iis3dwb_spi_write_reg(0x21, ctrl2);INT1分配好之后,在STM32C5的EXTI回调里置一个标志位,主循环检测到标志位再去读数据。这样CPU不用空转轮询,后面接FFT和上位机逻辑时更从容。
5. 振动数据读取、解析与验证
5.1 输出数据格式与换算
IIS3DWB的三轴原始输出是16位有符号补码,分别存在OUT_X_L(0x28)、OUT_X_H(0x29)、OUT_Y_L(0x2A)、OUT_Y_H(0x2B)、OUT_Z_L(0x2C)、OUT_Z_H(0x2D)。读取顺序是低字节在前,所以拼数据时要先把低字节读出来再拼高字节:
int16_t raw_x = (int16_t)((uint16_t)buf[1] << 8 | buf[0]); int16_t raw_y = (int16_t)((uint16_t)buf[3] << 8 | buf[2]); int16_t raw_z = (int16_t)((uint16_t)buf[5] << 8 | buf[4]);换算物理量时,要根据量程查灵敏度。IIS3DWB在±2g量程下灵敏度是0.061mg/LSB,±4g是0.122mg/LSB,±8g是0.244mg/LSB,±16g是0.488mg/LSB。换算公式为:
float acc_x_g = (float)raw_x * 0.061f / 1000.0f; // 单位 g我一般会在代码里把量程和灵敏度做成宏或者查表,避免硬编码。后期如果要切量程,只改初始化配置和灵敏度宏两处即可。
5.2 静态验证:看Z轴是不是1g
硬件接好、代码烧进MCU之后,第一步验证不是看波形,而是把传感器平放在桌面上,读出来的Z轴加速度应该非常接近1g,X轴和Y轴接近0g。如果有晃动或者数值明显不对,多半是量程配置、数据拼字节或者SPI读取字节顺序出了问题。
我当时调试时遇到过一种情况:Z轴读出是0.5g左右。后来发现是FIFO模式配置有误,读出来的数据有一部分是旧数据,平均值就拉低了。所以静态测试时不要只读一帧,可以连续读100帧求平均,这样能过滤掉单帧异常。
5.3 FIFO模式配置与批量读取
高频采样时如果每来一个数据就触发一次中断、读一次寄存器,CPU占用率很高。IIS3DWB内置FIFO,可以把数据先缓存,等缓冲区半满或者满时再一次性读走。这在FFT场景下特别合适,因为FFT本来就要攒够一帧数据才能算。
FIFO相关的寄存器包括FIFO_CTRL和FIFO_SRC,具体地址以手册为准。配置思路是:把FIFO设为Stream模式(连续模式),再设置FIFO阈值,只要堆到N个样本就触发中断。我用的阈值一般是FIFO容量的一半,比如容量是64,就设32,这样永远有时间处理,不会溢出。
读FIFO时用多字节读取函数,一次性读出当前FIFO里的所有数据。代码逻辑大概是:
#define FIFO_SAMPLE_COUNT 32 uint8_t fifo_buf[FIFO_SAMPLE_COUNT * 6]; // 从FIFO读取当前水位 uint8_t fifo_src = iis3dwb_spi_read_reg(FIFO_SRC_REG); uint8_t level = fifo_src & 0x1F; // 低5位是当前样本数 if (level > 0) { iis3dwb_spi_read_multi(OUT_X_L, fifo_buf, level * 6); // 解析 level 个样本 }FIFO模式下数据的字节顺序和直接读三轴输出寄存器是一致的,所以解析代码可以复用。这里要特别留意FIFO_SRC的低5位才是FIFO水位,高几位是状态标志,如果直接读整个字节当level用,会出现越界读的问题。
5.4 串口输出与上位机联调
调试阶段我习惯用串口把原始值和换算后的g值打出来,格式尽量简单,方便串口助手或者脚本解析。比如:
X=0.012g Y=-0.003g Z=0.998g这种格式可读性强,同时可以每100帧汇总一次最大值最小值,观察振动冲击事件。串口波特率115200,调试足够用。如果后面的数据量很大,可以把SPI读取和串口发送分开,用DMA+缓冲区,避免互相阻塞。
5.5 用真实振动源做验证
静态验证通过后,找一个可重复的振动源,比如小电机或者音叉,把传感器固定上去,观察输出是否有规律波动。更进阶的验证是用信号发生器驱动一个激振器,但一般实验室用手机振动马达也能凑合。
我这边用小马达做了个简易测试,转速大约3000rpm,对应基频50Hz。传感器粘在电机外壳上,FFT后能看到明显的50Hz峰,还能看到一些2倍频、3倍频的谐波,说明这套采集链路是能反映真实振动的。这一步做完,SPI获取振动数据这件事基本就闭环了。
6. 常见问题排查与调试建议
6.1 问题速查表
我整理了一份实际操作中遇到最多的问题和解决思路:
| 现象 | 可能原因 | 解决方向 |
|---|---|---|
| WHO_AM_I读出来全是0xFF | MISO没接好/SPI模式不对 | 检查SDO引脚、换SPI Mode 3试试 |
| WHO_AM_I读出来全是0x00 | MOSI或者CS时序问题 | 用逻辑分析仪抓CS和SCLK波形 |
| 读出的数据不变化 | ODR配置为0/传感器进入低功耗停机 | 检查CTRL1配置 |
| Z轴不是1g | 字节顺序反了/灵敏度用错 | 低字节在前拼接、按量程换灵敏度 |
| FIFO读出来数据乱跳 | FIFO水位判断错误 | 检查FIFO_SRC的低5位掩码 |
| 中断一直触发 | INT1极性配置错 | 确认中断是高有效还是低有效 |
| SPI速度上来后丢帧 | 没开DMA/CPU处理不过来 | 上DMA/FIFO批量读取 |
6.2 踩坑实录:最让我头疼的三个问题
第一个坑是SPI模式设置。开始我用的是Mode 1,结果WHO_AM_I读出来随机值,时好时坏。后来用逻辑分析仪抓时序,发现SCLK空闲是高电平,和传感器的采样沿对不上。改成Mode 0之后一切正常。这个案例说明遇到SPI问题,先抓时序图,别瞎猜。
第二个坑是CS片选时序。我一开始用了STM32的硬件NSS,CS自动控制,结果读FIFO时总是错位,一个样本的最后一个字节会跑到下一个样本的第一个字节。改成软件CS,通信前拉低、读完后拉高,问题立刻解决。软件片选虽然多两行代码,但可控性提升巨大。
第三个坑是FIFO水位解析。FIFO_SRC寄存器高几位有溢出标志和FIFO满标志,我早期直接把整个寄存器的值当成水位用,导致一次读多了数据,缓冲越界。后来按手册加上0x1F掩码才正常。任何寄存器都不要整字节直接信,该掩码就要掩码。
6.3 调试工具推荐
有条件的话,逻辑分析仪是SPI调试的神器。我用的24MHz采样率的入门级逻辑分析仪,抓SPI时序绰绰有余。重点观察CS低电平期间SCLK的沿和MOSI/MISO的数据对应关系。超过10MHz的SPI时钟逻辑分析仪可能吃力,但IIS3DWB的7.8MHz完全覆盖。
如果没有逻辑分析仪,可以先用GPIO翻转法,在代码里手动拉高拉低CS,然后用示波器看SCLK和MISO。示波器时间基准调小一点,抓几个帧,也能确认基本时序。不过论效率,还是逻辑分析仪省心。
6.4 性能优化的进阶方向
裸SPI读通以后,往高性能方向走有几条路:第一是用DMA方式搬数据,SPI读FIFO不占CPU。第二是用两个缓冲区交替使用,采集数据往缓冲A写的时候,CPU处理缓冲B的数据,实现流水线。第三是利用IIS3DWB的FIFO中断触发DMA传输,从传感器到内存全自动,CPU只在DMA传输完成中断里处理数据。这三步做完,即使跑26.7kHz的满速率采样,CPU占用率也能压得很低。
从我个人经验来看,这套“STM32C5 + IIS3DWB10IS + SPI”方案的调试门槛并不高,关键在于先慢后快、先轮询后DMA、先静态后动态,一步步验证。寄存器层面的细节确实多,但每踩一个坑都能让下一版更顺。下一篇我会接着写FFT频谱分析怎么做、如何在时域和频域之间切换视角。如果你正在做振动监测相关的项目,先把SPI基础通信打牢,后面所有分析才有可靠的数据源。