3、SPI通信协议详解:CPOL/CPHA模式、片选时序、多字节读写、CRC校验
SPI 这玩意儿,搞嵌入式的基本都打过交道。但在 BMS 的 AFE 芯片驱动里,它跟普通传感器通信还真不太一样——时序要求更严,数据量更大,而且动不动就十几 MHz 的时钟。我刚开始调 AFE 驱动时,就因为在 SPI 模式上栽了个跟头,折腾了两天才发现是 CPOL/CPHA 配反了。
说白了,SPI 就是个主从同步串行接口。主设备(通常是 MCU)产生时钟,同时控制片选信号。从设备(AFE 芯片)被动响应。但这里面的门道,远不止“四根线”那么简单。
3.1 CPOL/CPHA 模式:时钟极性与时序相位
很多工程师把 CPOL 和 CPHA 当成“配置模板”来背——模式 0、模式 1、模式 2、模式 3。但如果你不理解背后的采样和移位逻辑,换一颗 AFE 芯片就可能翻车。
| SPI 模式 | CPOL(时钟极性) | CPHA(时钟相位) | 数据采样边沿 |
|---|---|---|---|
| 模式 0 | 0(空闲低) | 0 | 第一个跳变沿(上升沿) |
| 模式 1 | 0(空闲低) | 1 | 第二个跳变沿(下降沿) |
| 模式 2 | 1(空闲高) | 0 | 第一个跳变沿(下降沿) |
| 模式 3 | 1(空闲高) | 1 | 第二个跳变沿(上升沿) |
CPOL 决定时钟空闲时的电平。CPHA 决定数据是在第一个边沿采样,还是第二个边沿采样。我个人习惯先看 AFE 数据手册里的时序图——它会明确标出“数据采样点”的位置。如果手册写的是“在 SCLK 上升沿锁存数据”,那就要看空闲电平:空闲低就是模式 0,空闲高就是模式 3。
我曾经遇到一颗 AFE 芯片,手册上写的是“模式 1”,但实际抓波形发现它是在下降沿采样。后来发现是手册勘误——版本号没更新。从那以后,我再也不完全相信手册上的文字描述,只看时序图。
3.2 片选时序:CS 的建立时间与保持时间
片选(CS)在 SPI 里看似简单——拉低选中,拉高释放。但在 AFE 芯片驱动中,CS 的建立时间(tCSSU)和保持时间(tCSHOLD)非常关键。
为什么?因为 AFE 芯片内部有模拟电路和数字电路混合工作。CS 拉低后,芯片需要时间从“休眠”或“待机”状态切换到“通信”状态。如果 MCU 在 CS 拉低后立即发送时钟,AFE 可能还没准备好,第一个字节就丢了。
片选时序的关键参数:
- tCSSU(CS 建立时间):CS 拉低到第一个 SCLK 边沿的最小时间
- tCSHOLD(CS 保持时间):最后一个 SCLK 边沿到 CS 拉高的最小时间
- tCSHIGH(CS 高电平时间):两次通信之间 CS 保持高电平的最小时间
我建议在驱动初始化时,把这些参数配置成 AFE 手册最大值的 2 倍。虽然牺牲了一点通信速率,但换来的是全温度范围的可靠性。BMS 系统里,可靠性永远比速度重要。
3.3 多字节读写:连续传输与地址自增
AFE 芯片的寄存器通常不是单字节的。电压采集结果可能是 16 位,配置寄存器可能是 8 位或 16 位。多字节读写时,SPI 协议有两种实现方式:
- 固定长度传输:每次 CS 拉低只传输固定字节数(如 2 字节)
- 连续传输(地址自增):CS 保持低电平,连续发送多个字节,地址自动递增
大部分 AFE 芯片支持地址自增模式。比如读 6 个通道的电压,只需要发送一次读命令和起始地址,然后连续读取 12 个字节(每个通道 2 字节)。
// 多字节读示例:读取 6 个通道电压(地址自增模式)
uint8_t tx_buf[3] = {0x10, 0x00, 0x00}; // 读命令 + 起始地址高字节 + 起始地址低字节
uint8_t rx_buf[12]; // 6 通道 × 2 字节
CS_LOW();
// 发送读命令和地址(3 字节)
spi_transfer(tx_buf, 3);
// 连续读取 12 字节数据
spi_receive(rx_buf, 12);
CS_HIGH();
我个人习惯在多字节读之后,加一个“读回验证”步骤——再读一次起始地址,对比第一个字节是否一致。这能快速发现 CS 时序或时钟频率问题。
3.4 CRC 校验:数据完整性的最后防线
BMS 系统里,通信错误可能导致误判电池状态,严重时会触发错误的保护动作。SPI 本身没有内置的校验机制,所以 AFE 芯片通常会在协议层加入 CRC 校验。
常见的 CRC 算法:
- CRC-8:1 字节校验,常用于短数据包(如寄存器读写)
- CRC-16:2 字节校验,用于长数据包(如批量电压读取)
CRC 的计算范围通常包括:地址字节 + 数据字节(不包括 CRC 本身)。发送方计算 CRC 并附加在数据末尾,接收方重新计算并比对。
// CRC-8 计算示例(多项式 0x07,初始值 0x00)
uint8_t crc8_calc(uint8_t *data, uint8_t len) {
uint8_t crc = 0x00;
for (uint8_t i = 0; i < len; i++) {
crc ^= data[i];
for (uint8_t j = 0; j < 8; j++) {
if (crc & 0x80) {
crc = (crc << 1) ^ 0x07;
} else {
crc <<= 1;
}
}
}
return crc;
}
CRC 校验失败后的处理策略也很重要:
- 读操作失败:重试 3 次,如果仍然失败,上报错误并标记该通道数据无效
- 写操作失败:重试后读回验证,如果写入值与期望值不符,触发系统安全保护
我见过一些方案在 CRC 失败后直接丢弃数据,不做任何处理。这在 BMS 里是危险的——一个错误的电压值可能导致过充或过放保护误动作。宁可上报无效数据,也不要使用未经验证的数据。
SPI 通信看似简单,但在 BMS 的 AFE 驱动中,每一个细节都可能成为隐患。CPOL/CPHA 配错,数据全乱;片选时序不够,低温罢工;CRC 范围搞错,校验永远失败。嗯,这些坑我都踩过。记住一句话:看时序图,别只看文字;加余量,别卡极限;做验证,别靠运气。