
1. 项目缘起与整体设计思路STM32C5 是意法半导体新推的一条产品线定位在主流高性能与低功耗之间外设资源相比老的 F1/F4 系列有不少调整。我手上这块板子拿到之后第一件事就是想验证它的 SPI 外设到底好不好使于是翻出了一颗 IIS2ICLX——这是一颗工业级的双轴加速度计支持 SPI 和 I2C 两种数字接口量程可配输出数据率也能拉到比较高很适合拿来当 SPI 通信的试金石。为什么选 IIS2ICLX 而不是随便找颗常见的消费级加速度计原因有几个。第一它的寄存器映射清晰WHO_AM_I 这类身份寄存器固定读回来对不上就说明通信有问题调试起来有明确的判断依据。第二它支持 SPI 模式 0 和模式 3可以顺便验证 STM32C5 的 SPI 时钟极性和相位配置是否正确。第三它的数据输出是 16 位补码格式涉及高低字节拼接和符号扩展能把数据解析这一环也走通。这三点加起来基本覆盖了 SPI 驱动开发中最容易踩坑的地方。整个项目的目标很明确用 STM32C5 的硬件 SPI 接口把 IIS2ICLX 的加速度数据读出来并通过串口打印到上位机。听起来简单但中间涉及时钟配置、GPIO 复用、片选管理、寄存器读写时序、数据解析这几个环节任何一个环节出问题都会导致读回来的数据是乱的。我打算把这几个环节拆开来讲把每一步的意图和容易出错的地方都说清楚。适合谁来参考这篇内容如果你手上有 STM32C5 的板子或者正在用其他 STM32 系列驱动 SPI 传感器这篇东西都能直接拿去用。即使你用的是 I2C 接口的传感器寄存器读写的那套逻辑也是相通的。我会尽量把原理讲透同时给出可以直接复制的代码片段让你不用从头翻数据手册就能跑起来。在正式开始之前先说一下我用的开发环境STM32CubeMX 做初始化代码生成IDE 用的是 STM32CubeIDEHAL 库版本跟着 CubeMX 走。IIS2ICLX 这边我用的是一块现成的 breakout 板上面已经把必要的去耦电容和电平匹配做好了直接飞线到 STM32C5 的 SPI 引脚上就能用。如果你是自己画板子记得在传感器电源引脚旁边放 100nF 和 10uF 的电容不然读出来的数据会有随机跳变。2. 硬件连接与 SPI 引脚规划2.1 IIS2ICLX 的 SPI 引脚定义IIS2ICLX 是一颗 14 引脚的 LGA 封装器件SPI 相关的引脚有这么几个CS 是片选SCLK 是串行时钟SDI 是数据输入也就是 MOSISDO 是数据输出也就是 MISO。注意它的命名和 STM32 那边的 MOSI/MISO 是对应的SDI 接 STM32 的 MOSISDO 接 STM32 的 MISO别接反了。这里有一个容易忽略的点IIS2ICLX 的 SDO 引脚在 I2C 模式下还有别的功能但在 SPI 模式下它就是纯粹的数据输出。另外这颗芯片的 CS 引脚必须由主机控制不能一直拉低否则它不会进入 SPI 模式。每次读写操作之前把 CS 拉低操作完成之后拉高这是 SPI 通信的基本规矩。还有一点值得注意IIS2ICLX 在 SPI 模式下支持最高 10MHz 的时钟频率。我一开始想直接拉到 10MHz后来发现杜邦线飞线的情况下信号完整性撑不住读回来的数据偶尔会错位。所以实际调试阶段我把时钟降到了 1MHz 左右等确认通信稳定之后再往上提。如果你是自己画的 PCB走线短、阻抗控制得好可以直接上 10MHz。2.2 STM32C5 的 SPI 外设选择STM32C5 上面有好几个 SPI 接口我选的是 SPI1因为它挂在 APB2 总线上时钟源比较灵活而且引脚分布在我这块板子上比较方便接线。SPI1 的默认引脚是 PA5SCK、PA6MISO、PA7MOSI片选我用的是 PA4配置成普通 GPIO 输出软件控制。为什么片选要用软件控制而不是硬件 NSS硬件 NSS 在某些模式下会自动拉低但它的时序和传感器要求的片选建立时间、保持时间不一定匹配。软件控制片选的好处是我可以在拉低 CS 之后、发送第一个时钟之前插入一个短暂的延时确保传感器已经检测到了片选信号。这个延时通常几百纳秒就够了但如果没有有些传感器会漏掉第一个字节。STM32C5 的 SPI 外设和老的 F1 系列相比最大的变化是 FIFO 深度增加了支持 16 位数据帧的自动打包。这意味着在读取 16 位加速度数据的时候可以一次传输两个字节硬件自动处理字节顺序省去了手动拼接的麻烦。不过为了通用性我下面还是用 8 位数据帧的方式来写这样代码移植到其他系列上也不用改。2.3 硬件连接检查清单在通电之前我习惯把连接检查一遍避免因为接线错误烧掉传感器。下面是我用的检查清单检查项正确状态常见错误电源电压3.3V接到 5V 上会烧芯片地线连接共地不共地会导致通信乱码CS 引脚接 GPIO 输出悬空会导致传感器不响应SCK/MOSI/MISO对应连接MOSI 和 MISO 接反读不到数据上拉电阻SDO 不需要上拉I2C 模式下才需要上拉注意IIS2ICLX 的供电范围是 1.71V 到 3.6V虽然它标称支持 3.3V但如果你用的 STM32C5 板子是 5V 供电的一定要确认 SPI 引脚的电平是 3.3V不然会通过 IO 口的钳位二极管倒灌电流时间长了会损坏传感器。3. STM32CubeMX 配置与代码生成3.1 时钟树配置STM32C5 的时钟树比 F1 系列复杂一些但 CubeMX 把大部分参数都图形化了照着填就行。我的配置是这样的外部晶振用 8MHzPLL 倍频到 144MHz 作为系统时钟APB2 分频系数设为 2所以 SPI1 的时钟源是 72MHz。这个频率下SPI 的分频系数设为 64实际 SCK 频率就是 72MHz / 64 1.125MHz正好落在我想要的 1MHz 附近。为什么不用更高的 SPI 时钟前面说了飞线的情况下信号质量不行。如果你是自己画的板子可以把分频系数设小一点比如 8 或者 16这样 SCK 能到 9MHz 或者 4.5MHz读数据的速率会快很多。但要注意IIS2ICLX 的最高 SCK 频率是 10MHz别超了。3.2 SPI 参数配置在 CubeMX 的 SPI1 配置页面有几个关键参数需要设置ModeFull-Duplex Master。IIS2ICLX 是全双工器件读的时候会同时发一个空字节出去所以选全双工主机模式。Data Size8 Bits。虽然加速度数据是 16 位的但分两次读更通用。Clock Polarity (CPOL)Low。IIS2ICLX 在 SPI 模式 0 下空闲时 SCK 是低电平。Clock Phase (CPHA)1 Edge。第一个时钟边沿采样这也是模式 0 的特征。NSSSoftware。片选由软件控制。Baud Rate Prescaler64。对应 1.125MHz 的 SCK。First BitMSB First。IIS2ICLX 要求高位先出。CPOL 和 CPHA 的组合决定了 SPI 模式。CPOL0、CPHA0 是模式 0CPOL1、CPHA1 是模式 3。IIS2ICLX 两种都支持我选的是模式 0因为大部分 SPI 器件默认都是模式 0调试的时候不容易搞混。如果你发现读回来的数据全是 0xFF 或者 0x00八成是模式设错了换成模式 3 试试。3.3 GPIO 配置除了 SPI1 的四个引脚之外我还配了一个串口用来打印数据。串口用的是 USART2波特率 115200引脚是 PA2TX和 PA3RX。这个配置很常规没什么好说的。片选引脚 PA4 配置成 GPIO_Output初始电平设为 High也就是不选中传感器。输出速度设成 Very High这样拉低和拉高的边沿比较陡减少片选信号的建立时间。推挽输出不上拉也不下拉因为传感器那边已经有内部上拉了。3.4 生成代码后的初始化检查CubeMX 生成代码之后我习惯先看一眼MX_SPI1_Init()函数确认参数和我设的一致。有时候 CubeMX 的版本不同生成的代码会有细微差别比如hspi1.Init.CLKPolarity这个字段有的版本叫CLKPolarity有的叫CLKPhase别搞混了。另外HAL_SPI_Init()函数里面会自动调用HAL_SPI_MspInit()这个函数负责初始化 GPIO 和时钟。如果你发现 SPI 没反应先检查这个函数有没有被正确执行。我遇到过一种情况在main()里面手动调用了HAL_SPI_Init()结果MspInit被调了两次GPIO 被重新配置了一遍反而把之前的配置覆盖了。所以初始化只做一次就行别重复调。4. IIS2ICLX 寄存器读写与数据解析4.1 SPI 读写时序与寄存器地址规则IIS2ICLX 的 SPI 读写有一个特殊规则寄存器地址的最高位bit 7用来区分读和写。读操作的时候地址的最高位置 1写操作的时候最高位置 0。比如 WHO_AM_I 寄存器的地址是 0x0F读的时候要发 0x8F写的时候发 0x0F。这个规则和很多 SPI 传感器不一样有些传感器是用单独的读写位或者命令字节来区分。IIS2ICLX 这种设计的好处是省了一个字节坏处是容易忘。我一开始就没注意直接发 0x0F 去读结果读回来全是 0x00查了半天数据手册才发现这个问题。读操作的完整时序是这样的先把 CS 拉低然后发送地址字节最高位为 1接着发送一个空字节0x00同时接收传感器返回的数据。注意发送地址字节和接收数据之间有一个时钟周期的延迟这是 IIS2ICLX 的特性不是所有 SPI 器件都这样。如果你用逻辑分析仪抓波形会看到地址字节发完之后MISO 线上会先出现一个无效字节然后才是真正的数据。4.2 单寄存器读写的代码实现下面是我写的单寄存器读函数uint8_t IIS2ICLX_ReadReg(uint8_t reg) { uint8_t tx_buf[2]; uint8_t rx_buf[2]; tx_buf[0] reg | 0x80; // 最高位置1表示读操作 tx_buf[1] 0x00; // 空字节用来产生时钟 HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); HAL_SPI_TransmitReceive(hspi1, tx_buf, rx_buf, 2, HAL_MAX_DELAY); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); return rx_buf[1]; // 第二个字节才是有效数据 }写函数稍微简单一点因为不需要接收数据void IIS2ICLX_WriteReg(uint8_t reg, uint8_t data) { uint8_t tx_buf[2]; tx_buf[0] reg 0x7F; // 最高位清0表示写操作 tx_buf[1] data; HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(hspi1, tx_buf, 2, HAL_MAX_DELAY); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); }这两个函数看起来简单但有几个细节需要注意。第一HAL_SPI_TransmitReceive的第四个参数是数据长度单位是字节不是位。第二CS 拉低和拉高之间不能有其他的 SPI 操作否则传感器会认为你在连续传输把后面的数据当成同一个命令的一部分。第三如果 SPI 的时钟频率比较高CS 拉高之后最好加一个微秒级的延时给传感器时间完成内部状态机切换。4.3 连续读取加速度数据IIS2ICLX 的加速度数据存放在 OUTX_L_A、OUTX_H_A、OUTY_L_A、OUTY_H_A 这几个寄存器里地址是连续的。为了提高读取效率可以用连续读的方式一次把四个字节都读回来。IIS2ICLX 支持地址自动递增只要在第一个地址的最高位置 1后面的字节就会自动从下一个寄存器读。连续读的代码实现void IIS2ICLX_ReadAccel(int16_t *accel) { uint8_t tx_buf[5]; uint8_t rx_buf[5]; tx_buf[0] OUTX_L_A | 0x80; // 起始地址最高位置1 tx_buf[1] 0x00; tx_buf[2] 0x00; tx_buf[3] 0x00; tx_buf[4] 0x00; HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); HAL_SPI_TransmitReceive(hspi1, tx_buf, rx_buf, 5, HAL_MAX_DELAY); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); accel[0] (int16_t)((rx_buf[2] 8) | rx_buf[1]); // X轴 accel[1] (int16_t)((rx_buf[4] 8) | rx_buf[3]); // Y轴 }这里有一个容易搞错的地方IIS2ICLX 的输出寄存器是小端格式低字节在前高字节在后。所以拼接的时候要把先读到的字节放在低位后读到的字节放在高位。我一开始搞反了读出来的数据一直在跳后来用已知的静止状态去验证才发现是字节顺序的问题。另外rx_buf[0]是发送地址字节时收到的无效数据直接丢掉。rx_buf[1]是 OUTX_L_A 的值rx_buf[2]是 OUTX_H_A 的值以此类推。如果你用逻辑分析仪看波形会发现 MISO 线上第一个字节是随机的第二个字节开始才是有效数据。4.4 数据换算与单位转换IIS2ICLX 输出的原始数据是 16 位补码需要转换成实际的加速度值。转换公式是加速度值 原始数据 × 灵敏度灵敏度取决于你设置的全量程范围。IIS2ICLX 支持 ±2g、±4g、±8g、±16g 四个档位对应的灵敏度分别是 0.061 mg/LSB、0.122 mg/LSB、0.244 mg/LSB、0.488 mg/LSB。我一般用 ±2g 档因为分辨率最高适合测微小的振动。换算成 mg 单位的代码float IIS2ICLX_ConvertToMg(int16_t raw) { return raw * 0.061f; // ±2g档的灵敏度 }如果你要换算成 m/s²再乘以 0.00980665 就行了。不过在实际项目里我一般直接用 mg 作为单位因为传感器的数据手册和校准报告都是用 mg 给的省得来回换算。5. 初始化流程与 WHO_AM_I 验证5.1 上电后的初始化顺序IIS2ICLX 上电之后不能马上读数据需要先做几件事等待电源稳定、配置控制寄存器、设置输出数据率、使能轴。我一般按照下面的顺序来延时 10ms等电源稳定。读 WHO_AM_I 寄存器确认通信正常。写 CTRL1 寄存器设置输出数据率和轴使能。写 CTRL6 寄存器设置全量程范围。延时 100ms等传感器内部滤波器稳定。开始读数据。WHO_AM_I 寄存器的地址是 0x0FIIS2ICLX 的固定返回值是 0x6B。如果你读回来不是这个值说明 SPI 通信有问题后面的步骤都不用做了。我遇到过读回来是 0xFF 的情况一般是 MISO 线没接好或者传感器没供电。读回来是 0x00 的话可能是 CS 没拉低或者 SPI 模式设错了。5.2 控制寄存器的配置细节CTRL1 寄存器地址 0x10控制输出数据率和轴使能。bit 7 到 bit 4 是 ODR 设置bit 3 到 bit 0 是轴使能。IIS2ICLX 是双轴加速度计只有 X 轴和 Y 轴所以 bit 0 和 bit 1 分别对应 X 轴和 Y 轴。我一般设成 0x57意思是 ODR 设为 416HzX 轴和 Y 轴都使能。CTRL6 寄存器地址 0x15控制全量程范围。bit 6 到 bit 4 是 FS 设置000 对应 ±2g001 对应 ±4g010 对应 ±8g011 对应 ±16g。我设成 0x00也就是 ±2g 档。这里有一个坑IIS2ICLX 的寄存器在写入之后需要一定的时间才能生效特别是 ODR 设置。如果你写完 CTRL1 之后马上读数据读回来的可能是旧配置下的数据或者干脆是 0。我一般写完配置之后延时 100ms 再开始读这样比较稳妥。5.3 初始化代码的完整实现把上面的步骤串起来初始化函数大概长这样uint8_t IIS2ICLX_Init(void) { uint8_t who_am_i; HAL_Delay(10); who_am_i IIS2ICLX_ReadReg(WHO_AM_I); if (who_am_i ! 0x6B) { return 1; // 通信失败 } IIS2ICLX_WriteReg(CTRL1, 0x57); // 416Hz, XY轴使能 IIS2ICLX_WriteReg(CTRL6, 0x00); // ±2g HAL_Delay(100); return 0; // 初始化成功 }这个函数返回 0 表示成功返回 1 表示 WHO_AM_I 校验失败。在主函数里调用的时候如果返回值不是 0就打印一条错误信息然后停在死循环里方便调试。6. 常见问题排查与实操心得6.1 读回来的数据全是 0xFF 或 0x00这是最常见的问题原因通常有三个SPI 模式设错了、CS 没拉低、MISO 线没接好。排查顺序是这样的先用示波器或者逻辑分析仪看 SCK 和 MOSI 上有没有波形如果没有说明 SPI 外设没初始化成功如果有波形但 MISO 一直是高电平说明传感器没响应检查 CS 和电源如果 MISO 有波形但数据不对检查 CPOL 和 CPHA 设置。我遇到过一次特殊情况MISO 上有波形但数据一直是 0xFF。后来发现是传感器的 SDO 引脚在 I2C 模式下被配置成了地址选择引脚而我的 breakout 板上有一个上拉电阻把它拉高了。把那个上拉电阻去掉之后SPI 通信就正常了。所以如果你用的是现成的模块先看一眼原理图确认 SDO 上没有多余的上拉。6.2 数据偶尔跳变或者错位数据跳变一般是信号完整性问题。飞线的情况下SCK 频率太高会导致串扰和反射读回来的数据偶尔会错位。解决办法有两个降低 SCK 频率或者在 SCK 和 MISO 线上串一个 33 欧姆的电阻。我试过串电阻效果立竿见影1MHz 下基本不再跳变了。另一个可能的原因是电源噪声。IIS2ICLX 对电源噪声比较敏感如果电源上有开关电源的纹波加速度数据会有周期性的跳动。在传感器的电源引脚旁边并一个 10uF 的钽电容和一个 100nF 的陶瓷电容能明显改善。6.3 连续读的时候数据错位连续读的时候如果地址自动递增没生效读回来的数据会是同一个寄存器的值重复。IIS2ICLX 的地址自动递增是默认开启的但如果你在第一个地址的最高位没有置 1它就不会进入读模式自然也不会递增。另外连续读的字节数不能超过寄存器地址空间的末尾否则会绕回到开头读回来的数据就乱了。我一般会在连续读之后单独读一次第一个寄存器对比一下数据是否一致。如果一致说明连续读没问题如果不一致说明地址递增有问题需要检查地址字节的最高位。6.4 常见问题速查表现象可能原因解决办法WHO_AM_I 读回 0x00CS 没拉低检查 CS 引脚配置和接线WHO_AM_I 读回 0xFFMISO 没接或传感器没供电检查电源和 MISO 接线数据全是 0x00SPI 模式错误尝试切换 CPOL/CPHA数据偶尔跳变信号完整性差降低 SCK 频率或串电阻连续读数据重复地址递增未生效检查地址字节最高位数据符号错误字节拼接顺序反了低字节在前高字节在后提示如果你用的是 STM32C5 的 SPI FIFO 模式连续读的时候要注意 FIFO 的阈值设置。阈值设得太高数据会滞留在 FIFO 里读不出来设得太低会频繁触发中断影响实时性。我一般把阈值设成 1也就是每收到一个字节就触发一次这样最不容易出错。6.5 实操心得先用低速验证再逐步提速我调试 SPI 传感器的习惯是先把 SCK 频率降到 100kHz 左右确认通信正常、数据正确之后再逐步提高频率。每提高一次都重新验证一遍 WHO_AM_I 和数据稳定性。这样虽然多花几分钟但能避免在高速下遇到问题时分不清是代码问题还是信号问题。另外逻辑分析仪是调试 SPI 的利器。我用的是一款几十块钱的 8 通道逻辑分析仪配合开源软件能直接解码 SPI 波形看到地址、数据和时钟的对应关系。有了它排查问题的效率至少提高一倍。如果你经常调 SPI 器件建议入手一个。7. 从单轴到双轴数据融合的初步尝试IIS2ICLX 是双轴加速度计输出 X 轴和 Y 轴两个方向的数据。在静止状态下X 轴和 Y 轴的读数应该接近 0gZ 轴如果有的话应该接近 1g。但 IIS2ICLX 没有 Z 轴所以静止的时候两个轴的读数都应该在 0g 附近噪声范围内波动。如果你要做倾角计算可以用这两个轴的数据算出一个平面内的倾斜角度。公式是角度 atan2(Y轴数据, X轴数据) × 180 / π这个角度表示的是传感器在 XY 平面内的旋转角度不是相对于重力的倾角。因为 IIS2ICLX 没有 Z 轴无法感知重力方向在 Z 轴上的分量所以它只能测平面内的旋转不能测俯仰和横滚。如果你需要测三维姿态得换一颗三轴加速度计或者加一颗陀螺仪做数据融合。我在实际项目里用 IIS2ICLX 主要是测振动和平面倾斜这两个场景下双轴数据足够了。如果你要做姿态解算建议看看 IIS2ICLX 的兄弟型号比如 LSM6DSO 之类的六轴器件那个更适合做姿态融合。8. 性能优化与后续扩展方向8.1 用 DMA 提高读取效率目前我是用阻塞方式读数据的每次读四个字节CPU 要等 SPI 传输完成才能继续。如果数据率要求不高比如 100Hz 以下这种方式完全够用。但如果要把 ODR 拉到 416Hz 甚至更高阻塞方式会占用大量 CPU 时间影响其他任务的执行。用 DMA 可以解决这个问题。STM32C5 的 SPI 支持 DMA 请求配置好 DMA 通道之后SPI 传输数据的时候 CPU 可以去干别的事等传输完成中断来了再处理数据。CubeMX 里面可以直接勾选 DMA 选项生成代码之后调用HAL_SPI_TransmitReceive_DMA()就行了。不过 DMA 方式有一个坑DMA 传输完成中断和 SPI 传输完成中断的触发顺序不一定一致。如果你在 DMA 中断里直接读数据可能会读到不完整的数据。稳妥的做法是在 DMA 传输完成中断里设置一个标志位然后在主循环里检查这个标志位再处理数据。8.2 用 FIFO 模式减少通信次数IIS2ICLX 内部有一个 32 级的 FIFO可以把多次采样的数据缓存起来等 FIFO 快满的时候一次性读出来。这样可以减少 SPI 通信的次数降低功耗也能让数据的时间戳更准确。配置 FIFO 需要写几个寄存器FIFO_CTRL1 到 FIFO_CTRL5设置 FIFO 的模式、水印阈值和触发条件。我一般用水印模式设置一个阈值比如 16 个样本当 FIFO 里的数据达到 16 个的时候触发中断然后一次性把 16 个样本都读出来。这样 CPU 不用频繁响应中断效率高很多。8.3 后续可以扩展的方向这个项目目前只是把加速度数据读出来了后续可以做的事情还很多。比如加一个卡尔曼滤波器把加速度数据平滑一下减少噪声或者把数据通过串口传到上位机用 Python 画实时曲线再或者把数据存到 SD 卡里做离线分析。如果你手上有多个 IIS2ICLX还可以用同一条 SPI 总线通过不同的 CS 引脚来区分实现多传感器同步采集。STM32C5 的 SPI 外设支持多个片选引脚配置起来很方便。不过要注意多个传感器共用 SCK 和 MOSI 的时候MISO 线上会有多个器件同时驱动的问题需要确保同一时刻只有一个传感器的 CS 被拉低。我在实际使用中发现IIS2ICLX 的零偏稳定性不错常温下静止放置几个小时零偏漂移在 1mg 以内。如果你要做长时间监测这个指标是够用的。但如果温度变化比较大零偏会跟着变需要做温度补偿。IIS2ICLX 内部有温度传感器可以读出来做补偿曲线这个我还没试过后面有空再折腾。