STM32C5 SPI驱动IIS3DWB10IS振动传感器:三轴数据读取实战
这篇是 STM32C5 驱动 IIS3DWB10IS 振动传感器的第一篇目标很单一通过 SPI 接口把三轴震动数据读出来。IIS3DWB10IS 是意法半导体一颗面向工业振动监测的 MEMS 加速度计带宽做到 10kHz 级别SPI 是它的主要通信接口适合长时间原始数据采集STM32C5 是最近两年主推的 Cortex-M33 内核 MCU外设丰富、性能足够拿来做数据采集和后续的频谱分析都很合适。这篇先把 SPI 通路打通WHO_AM_I 能读到、三轴原始寄存器能刷出来后面做 FIFO、DMA、FFT 才不会心里没底。读者如果是刚接触振动传感器的新手建议先把 CubeMX 的 SPI 配置和传感器寄存器初始化理解透如果你已经在做状态监测或故障诊断项目那这篇可以直接当参考代码来抄重点看 BDU、自动递增和片选控制这几个坑。1. 为什么是 IIS3DWB10IS以及 SPI 和 I2C 怎么选1.1 型号拆解与传感器定位IIS3DWB10IS 这个名字乍一看有点长拆开理解会清楚很多。前缀 IIS 是 ST 的工业级 MEMS 传感器系列3D 表示三轴加速度计WB 是 wide bandwidth 的缩写也就是宽带宽版本后边的 10IS 我理解是它的封装和功能变体比如带金属盖、集成了 IS2S 智能自检功能适合对安全性有要求的工业应用。这颗传感器最突出的指标是带宽3dB 截止频率在 10kHz 附近ODR 最高能到 26.667kHz也就是说它不只是用来测“有没有震动”而是真的能帮你分析到几千赫兹的轴承故障频率、齿轮啮合频率这是普通几百赫兹的加速度计做不到的。在旋转机械状态监测场景里震动信号频率往往覆盖几百 Hz 到几 kHz甚至更高。用带宽只有 1kHz 的传感器去测轴承早期故障等于拿着低通滤波器去采集高频冲击数据还没进算法就已经废了大半。IIS3DWB10IS 这类高带宽传感器就是为这种场景设计的。而且它内部带 3KB 的 FIFO估算下来能缓存大约 512 组三轴原始数据硬件上已经为“高速采样、打包上传”做好了准备。1.2 SPI 相比 I2C 的优势很多刚入门的同学会问这传感器不是同时支持 SPI 和 I2C 吗为什么大家都用 SPI核心原因是数据量。IIS3DWB10IS 最高 ODR 26.667kHz每次读一组三轴数据要读 6 个字节一秒就是 26.667k × 6B ≈ 160KB/s 的数据吞吐。I2C 标准模式 100kHz、快速模式 400kHz要在这条线上同时完成命令帧和连续 6 字节数据搬运几乎把总线占满还容易因为时钟拉伸和应答位把时序搞乱。SPI 没有应答位波特率可以轻松跑到 10MHz 甚至更高数据是连续流式搬出来的特别适合这种大吞吐场景。对比一下SPI 是主从式、四线制SCLK、MOSI、MISO、CS全双工主机提供时钟数据按位同步发送和接收I2C 是两线制SCL、SDA半双工有器件地址、寄存器地址、应答位等额外开销而且每次传输都要等 ACK。同样读 6 字节三轴数据SPI 发出 1 字节命令后时钟持续给6 字节就能流水回来I2C 要先发器件地址、寄存器地址再 restart再读数据还要处理 NACK 和 STOP。在高速振动采样的项目里SPI 就是更合理的选择。后面博客如果做 FFT 频谱分析你需要的是长时间连续采样不是偶尔读一次温湿度所以本文所有代码都基于 SPI 展开。2. 硬件连接与 CubeMX 工程配置2.1 引脚分配建议我这次用的是 STM32C5 最小开发板外接 IIS3DWB10IS 模块SPI 用的是 SPI1片选没有用硬件 NSS而是单独找了一个普通 GPIO 控制。接线如下传感器引脚功能连接 STM32C5说明VDD电源3.3V必须加 100nF 去耦电容靠近引脚GND地GND与 MCU 共地SCLKSPI 时钟PA5 / SPI1_SCK由主机输出SDISPI 数据输入MOSIPA7 / SPI1_MOSI主机发给传感器SDO/SA0SPI 数据输出MISOPA6 / SPI1_MISO传感器返回数据CS片选PA4 或任意 GPIO低电平有效需要注意的是这颗传感器没有独立的复位引脚上电后等一小段时间数据手册建议至少等系统电源稳定后再操作然后通过 SPI 命令复位寄存器即可。SDO 引脚在 I2C 模式下是地址引脚但在 SPI 模式下它就是 MISO别接错。2.2 STM32CubeMX 关键配置打开 STM32CubeMX选择你手上的 STM32C5 型号先配置时钟树。STM32C5 主频可以跑到 250MHz 甚至更高具体看型号。建议先把系统时钟配置到最高频率然后让 SPI1 挂到 APB2 总线上SPI 时钟源尽量用高的那个方便后续把波特率拉上去。SPI1 参数按下面配置ModeFull-Duplex MasterHardware NSS SignalDisableCS 用普通 GPIO 控制Data Size8 bitFirst BitMSB FirstPrescaler根据 APB 时钟算建议先配成 8MHz ~ 10MHz 左右CPOLLowCPHA1 Edge其他保持默认CPOLLow、CPHA1 Edge 就是 SPI Mode 0。大多数 ST 传感器默认支持 Mode 0 和 Mode 3我们选 Mode 0 就好。当然如果你在工程中发现读回来的数据错位可以试试把 CPHA 改成 2 Edge也就是 Mode 3不同的 PCB 走线长度、电平转换芯片可能会导致采样点偏差这属于正常调试范围。CS 引脚我选 PA4配置为 GPIO Output初始电平设置为 High。注意 CubeMX 生成代码后要手动把 CS 初始状态拉高不然传感器在上电瞬间可能被误选中。2.3 硬件片选和软件片选的选择这里把硬件片选和软件片选说透。STM32 的 SPI 外设里NSS 有两种工作模式硬件 NSS 和软件 NSS。硬件 NSS 模式下NSS 引脚由 SPI 外设自动控制主机发送数据时自动拉低发送完自动拉高看起来很方便软件 NSS 模式下NSS 引脚不受 SPI 外设控制而是要你在 GPIO 里手动操作。实际驱动 IIS3DWB10IS 这类传感器时我强烈建议用普通 GPIO 软件控制 CS而不是用硬件 NSS。原因有几个第一传感器的 SPI 帧要求“先发命令字节再连续读写数据”中间 CS 必须保持低电平硬件 NSS 在每次字节传输后可能会产生释放动作导致帧被切断第二硬件 NSS 的极性和时序是通过寄存器配置的调试时不够直观第三GPIO 控制 CS 可以随时用逻辑分析仪拉出来看排查问题非常快。所以本文全部示例代码都采用 GPIO 拉低拉高的方式管理片选这也是实际项目里最常见的做法。3. SPI 通信协议与寄存器操作3.1 命令帧结构与地址自动递增IIS3DWB10IS 的 SPI 帧格式很直接主机先拉低 CS发送 1 个字节的命令最高位是读写标志1 表示读0 表示写剩下 7 位是寄存器地址。然后根据命令是读还是写继续发数据或收数据最后拉高 CS。举个例子读取 WHO_AM_I 寄存器地址 0x0F主机需要发送的命令字节是 0xAF也就是 0x0F 加上最高位的读标志 1。传感器收到后从 MISO 线上返回 1 字节默认值应该是 0x7F。这是整个硬件链路是否打通的最快验证手段。写寄存器时命令字节就是寄存器地址本身比如往 CTRL1地址 0x10写数据命令字节就是 0x10后面跟要写入的值。连续读取三轴数据时可以一条命令读 6 个字节。这里有个必须关注的寄存器位CTRL3 里的 IF_INC 位也就是寄存器地址自动递增使能。把这个位置 1 后主机在读命令之后连续给时钟传感器会自动把下一个寄存器的值继续放到 MISO 上不需要主机再发一次命令。这样一次 SPI 事务就能读完 OUT_X_L、OUT_X_H、OUT_Y_L、OUT_Y_H、OUT_Z_L、OUT_Z_H 六个寄存器。如果 IF_INC0那只能一个一个寄存器读效率低而且数据一致性差强烈不建议。3.2 关键寄存器解读与初始化流程IIS3DWB10IS 的寄存器映射延续了 ST 传感器的风格这里只说本文用到的几个关键寄存器。WHO_AM_I 地址 0x0F默认值 0x7F。上电后第一件事就是读它确认 SPI 通信正常、器件地址正确。如果读不到后面所有寄存器操作都白搭。CTRL1 地址 0x10主要是操作模式、ODR 和量程配置。ODR 的低 4 位决定输出数据速率最高可配到 26.667kHz对应的寄存器值是 0b1000。量程位可以配 ±2g、±4g、±8g、±16g具体编码要对着数据手册查。振动采样建议先把量程设在 ±16g避免传感器在电机启动瞬间饱和削波等摸清信号幅值后再根据实际振动烈度往下调。CTRL3 地址 0x12重点配置两个位BDU块数据更新和 IF_INC地址自动递增。BDU 置 1 后传感器在主机读取六字节数据期间会锁存当前的输出保证高字节和低字节来自同一次采样不会出现“X 轴低字节是这次采样、高字节是上次采样”的错位。IF_INC 置 1 启用自动递增。建议把 CTRL3 写成 0x44差不多就是 BDU 和 IF_INC 同时置位。不同批次的数据手册如果位定义有差异请以官方最新版本为准。CTRL4 地址 0x13主要和中断、自检功能相关。IS2S 智能自检是 IIS3DWB10IS 的一个卖点可以把执行机构内部产生的 test pulse 加进去验证信号链路是否正常。本文第一步不展开自检后面单独写。3.3 封装 SPI 读写函数代码层面先封装两个最基础的函数写寄存器和读寄存器。用到的 SPI 句柄是 hspi1CS 引脚是 PA4。#define SPI_CS_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) #define SPI_CS_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET) int8_t SPI_WriteReg(uint8_t reg, uint8_t *data, uint16_t len) { uint8_t cmd reg 0x7F; // 写命令最高位为0 SPI_CS_LOW(); if (HAL_SPI_Transmit(hspi1, cmd, 1, 100) ! HAL_OK) { SPI_CS_HIGH(); return -1; } if (HAL_SPI_Transmit(hspi1, data, len, 100) ! HAL_OK) { SPI_CS_HIGH(); return -1; } SPI_CS_HIGH(); return 0; } int8_t SPI_ReadReg(uint8_t reg, uint8_t *data, uint16_t len) { uint8_t cmd reg | 0x80; // 读命令最高位置1 SPI_CS_LOW(); if (HAL_SPI_Transmit(hspi1, cmd, 1, 100) ! HAL_OK) { SPI_CS_HIGH(); return -1; } if (HAL_SPI_Receive(hspi1, data, len, 100) ! HAL_OK) { SPI_CS_HIGH(); return -1; } SPI_CS_HIGH(); return 0; }这里用阻塞式 HAL 函数是为了验证逻辑代码直观适合第一篇。后面如果跑 26.667kHz 连续采样必须换成中断或 DMA这个我会在 FIFO 和 DMA 的博文里专门写。要注意一个细节读多字节时CS 在整个事务期间必须一直保持低电平。有些初学者会在每读一个字节之前拉一次 CS这会导致传感器认为你重新开始了一个新的 SPI 帧自动递增逻辑全部失效读回来的数据全是同一个寄存器。正确做法就是上面代码这样先拉低命令和整个数据段都传完最后再拉高。4. 上电实测WHO_AM_I 与三轴数据读取4.1 验证 SPI 通路是否打通工程生成后先写一个最简单的测试读 WHO_AM_I。代码可以放在 main 函数的初始化之后uint8_t who_am_i 0; if (SPI_ReadReg(0x0F, who_am_i, 1) ! 0) { printf(SPI read error\r\n); Error_Handler(); } printf(WHO_AM_I 0x%02X\r\n, who_am_i); if (who_am_i ! 0x7F) { printf(device id mismatch\r\n); } else { printf(IIS3DWB10IS detected\r\n); }如果串口打印出 0x7F说明 SPI 物理链路、命令格式、从机应答都正常可以直接进入寄存器配置。如果读出来是 0x00、0xFF 或者随机值不要急着怀疑传感器坏了先检查接线、供电和 SPI Mode后面第 5 节我会列出排查顺序。我实测的时候第一次读 WHO_AM_I 卡了很久最后发现是 CubeMX 生成的代码默认把 CS 引脚配置成了推挽输出但初始电平没有被拉高上电瞬间 CS 处于低电平传感器一直认为被选中SPI 通信被自己的 CS 状态干扰。后来在 MX_GPIO_Init 里把 PA4 初始电平改成 High问题立刻消失。4.2 寄存器初始化与量程选择确认 WHO_AM_I 正确后开始配置传感器。我的初始化代码大致如下uint8_t ctrl1 0x00; uint8_t ctrl3 0x44; // 配置 ODR 26.667kHz, FS ±16g ctrl1 0x08 | (0x03 5); // 具体位定义对照数据手册 SPI_WriteReg(0x10, ctrl1, 1); // 使能 BDU 和 IF_INC SPI_WriteReg(0x12, ctrl3, 1);ODR 和量程的位编码我不在这里展开全部表格因为不同版本数据手册的表格排版不一样。你只要记住低 4 位是 ODR中间两位是量程配置时对着数据手册的 CTRL1 说明填即可。实测下来先用 ±16g 是最安全的特别是不确定现场振动强度的时候宁可量程大一点损失一些分辨率也不能让数据削波。为什么量程和分辨率是个取舍IIS3DWB10IS 在 ±2g 量程下灵敏度大约是 0.061mg/LSB±16g 量程下灵敏度大约降到 0.488mg/LSB。同样是 16bit ADC 的原始值±2g 下能分辨非常微弱的震动但在强振动场景容易溢出±16g 下动态范围大但小信号的分辨率会变差。工业现场如果只是做设备状态监测±16g 起步没毛病。4.3 连续读取三轴数据并转为加速度初始化完成后就可以循环读数据了。最简单的做法是直接读 0x30 起始的 6 个字节前提是 CTRL3 里的 IF_INC 已经置 1。uint8_t raw_data[6] {0}; int16_t x_raw, y_raw, z_raw; float x_g, y_g, z_g; while (1) { if (SPI_ReadReg(0x30, raw_data, 6) ! 0) { printf(SPI read error\r\n); continue; } x_raw (int16_t)((raw_data[1] 8) | raw_data[0]); y_raw (int16_t)((raw_data[3] 8) | raw_data[2]); z_raw (int16_t)((raw_data[5] 8) | raw_data[4]); // 如果配置为 ±16g灵敏度约为 0.488 mg/LSB x_g (float)x_raw * 0.488f / 1000.0f; y_g (float)y_raw * 0.488f / 1000.0f; z_g (float)z_raw * 0.488f / 1000.0f; printf(X%.3f g, Y%.3f g, Z%.3f g\r\n, x_g, y_g, z_g); HAL_Delay(10); }这里有个很容易踩的坑负数的正确符号扩展。原始数据是两个 8bit 字节拼成 16bit如果直接写uint16_t tmp (raw_data[1] 8) | raw_data[0]; int16_t x_raw (int16_t)tmp;在某些编译器上如果你先赋值给带符号的 int16_t再把 int16_t 直接和 0xFFFF 做位运算可能因为整型提升导致符号位被吃掉。上面代码里我直接用(int16_t)((raw_data[1] 8) | raw_data[0])先把括号里的 unsigned 16bit 算清楚再显式转换为 int16_t这样负值才能正确变成负数。静止放置传感器时Z 轴理论上应该读到 1g 左右X 和 Y 接近 0g。如果读到的三个轴都是 0或者 Z 轴没有重力分量大概率是拼接顺序反了把高低字节调换一下再试。4.4 高速场景下的后续规划到这里一个基本的 SPI 读取链路已经通了。但 26.667kHz 的 ODR 配合 HAL_Delay(10) 这种阻塞式循环是跑不满的因为 CPU 大部分时间花在等待 SPI 传输完成上而且每读一组 6 字节就要进一次 HAL 库的阻塞循环。真正做振动监测时我会建议分三步走第一步把传感器内部的 3KB FIFO 用起来让数据先缓存到传感器内部MCU 通过 FIFO 状态寄存器判断是否有足够数据再批量读取。这样可以减少 SPI 事务次数CPU 占用率会明显下降。第二步MCU 侧启用 SPI DMA配合定时器触发或空闲中断把数据从 SPI 外设直接搬到内存数组不占用 CPU 时间。第三步在内存里做滑动窗口积累足够样本后统一做 FFT 或其他信号处理。这三步是连续的高阶主题我会在后续博文里逐个拆开讲。第一篇先把 SPI 通信和原始数据读数跑通后面的优化才有基础。5. 常见问题与排查技巧实录5.1 读 WHO_AM_I 失败的排查顺序这是做板级驱动时最常遇到的问题我总结了一个固定的排查顺序每次都能快速定位现象可能原因排查方向读 WHO_AM_I 返回 0x00CS 没有拉到低或者 MISO 没接对用逻辑分析仪抓 CS 波形确认命令帧期间 CS 保持低电平读 WHO_AM_I 返回 0xFFMISO 线悬空或传感器供电异常检查 SDO/MISO 是否虚焊VDD 电压是否稳定数据错位读数像随机数SPI Mode 配错采样点不在数据稳定区把 CPHA 从 1 Edge 改为 2 Edge或者降低 SPI 波特率到 1MHz 以下再试读一次成功连续读失败CS 时序有问题或 IF_INC 没配置确认 CS 在每个帧结束后确实拉高检查 CTRL3 里的 IF_INC 位数据全是 0x00 但寄存器写返回正常传感器处于掉电或 sleep 状态检查 CTRL1 的 ODR 配置确保传感器不是 power-down 模式很多情况下问题不在代码而在硬件。拿万用表量一下传感器 VDD 引脚电压是否真的是 3.3V量一下 CS 在拉低前是不是高电平。不要小看这一步我和同事不止一次遇到模块上的排针虚焊代码调了一下午最后发现是 MISO 那根杜邦线接触不良。5.2 数据跳变和毛刺的处理如果你发现静止状态下读数有小幅跳变先别急着换传感器。常见原因有几个电源纹波大、SPI 线太长导致串扰、传感器附近有电机或开关电源。工业现场尤其容易踩到电源坑。IIS3DWB10IS 是宽带宽传感器它对电源噪声是天然敏感的带宽越宽噪声越容易被采样到。VDD 旁边除了 100nF 去耦电容最好再并一个 1uF 到 10uF 的电容PCB 上电容要尽量靠近传感器电源引脚。另外要注意SPI 时钟线不建议和传感器输出数据线靠得太近。如果 PCB 布线时 SCLK 和 MISO 在长距离内平行走线SCLK 的边沿可能耦合到 MISO 上导致读数毛刺。实测中把 SPI 波特率从 10MHz 降到 5MHz 也能缓解这个问题但最终解法还是改布局。5.3 BDU 不使能会引起什么后果BDU 这个位很多人容易忽略但在高速采样时非常关键。假设你正在以 26.667kHz 速率读数据MCU 发读命令那一刻传感器正好更新了输出寄存器那你可能读到这样一组数据X 轴低字节是新样本X 轴高字节还是旧样本拼出来的 16bit 数值完全是乱的。这种错位在低速采样时很难遇到但在高速采样时出现的概率大大增加。把 CTRL3 的 BDU 位置 1 后主机读取寄存器期间传感器会锁存输出直到你把 6 个字节全部读完才解锁。实测开 BDU 前后对比静止状态读数波动幅度明显下降。所以只要你有连续读三轴的需求BDU 必须开。5.4 三轴数据正负号与方向确认最后一个容易被忽略的点是坐标轴方向。IIS3DWB10IS 的数据手册里有封装坐标定义X、Y、Z 方向按照芯片丝印和封装引脚来确定。用万用板手焊模块时芯片方向一不留神就转了 90 度导致 X 轴和 Y 轴数据互换。我在一次项目里就遇到过这类问题传感器装上去之后震动方向怎么都对不上后来查原理图才发现芯片旋转了 90 度焊接软件里重新映射轴就解决了。拿到新模块建议先用重力方向做一次标定平放时 Z 轴约 1g倒放时 Z 轴约 -1g侧放时对应轴约 ±1g。这一步能同时验证正负号、量程和轴方向比直接上振动台简单多了。还有一个经验是传感器固定在被测物上时安装面的平整度会直接影响测量结果。MEMS 加速度计对安装扭矩和平面度非常敏感扭矩过大可能让内部应力影响零点输出。批量产品一定要用标准的安装扭矩开发阶段可以用少量硅胶或者双面胶先做验证但正式测试别图省事。这篇写到这核心的 SPI 获取震动计数据链路已经完整跑通CubeMX 配置、GPIO 片选、寄存器初始化、三轴数据解析、常见坑排查每一步都是实际调试中反复验证过的。接下来会是 FIFO 批量读取和 DMA 高速传输的专题等系列更新后再继续聊。