4、接口测试:模块间通信接口测试(SPI、I2C、UART),协议一致性验证
嵌入式系统里,模块之间怎么说话?说白了就是靠 SPI、I2C、UART 这三兄弟。我见过不少项目,功能逻辑写得天花乱坠,结果一联调,接口先崩了——要么波形对不上,要么时序差那么几个微秒,整个系统就卡死。
所以这一章,咱们就专门聊聊怎么给这三个接口做测试,以及怎么验证协议的一致性。嗯,这里说的“一致性”,不只是看波形对不对,还要看数据内容、时序参数、异常处理是不是都符合规格书。
4.1 三大接口的核心差异与测试关注点
先快速过一遍这三者的区别。你心里有个谱,后面测试才知道重点在哪。
| 特性 | UART | SPI | I2C |
|---|---|---|---|
| 信号线数 | 2(TX, RX) | 4(SCLK, MOSI, MISO, CS) | 2(SCL, SDA) |
| 通信方式 | 异步,需双方约定波特率 | 同步,主设备提供时钟 | 同步,主设备提供时钟 |
| 主从关系 | 对等(无严格主从) | 一主多从(片选区分) | 多主多从(地址区分) |
| 数据帧结构 | 起始位+数据位+校验位+停止位 | 无固定帧,靠时钟和片选 | 起始条件+地址+数据+应答+停止条件 |
| 测试核心 | 波特率误差、帧格式、溢出 | 时钟极性/相位、片选时序、数据对齐 | 地址仲裁、应答时序、时钟拉伸 |
这张表你最好存一下。每次做接口测试前,对着它列检查清单,基本不会漏项。
4.2 UART 接口测试:别小看波特率误差
UART 看起来最简单,两根线一接就能跑。但坑往往就藏在最简单的地方。
4.2.1 波特率一致性测试
我曾经在一个项目里,两边都配了 115200,结果跑起来总是丢字节。拿示波器一量,发送端实际波特率是 115400,接收端是 114800。差了 0.5% 左右,理论上在容差范围内,但连续传 1000 个字节,累积误差就把帧同步搞崩了。
测试方法其实不复杂:
- 用示波器或逻辑分析仪抓 TX 引脚上的一个字节波形。
- 测量起始位下降沿到第一个数据位上升沿的时间,这就是一个位的时间。
- 用 1 除以这个时间,得到实际波特率。
- 对比实际值与配置值,误差应小于 ±2%(工业级通常要求 ±1%)。
4.2.2 帧格式与校验验证
UART 的帧格式包括数据位长度(7/8/9)、校验位(无/奇/偶)、停止位(1/1.5/2)。测试时,我一般会构造一个测试矩阵:
| 测试用例 | 配置 | 发送数据 | 预期结果 |
|---|---|---|---|
| 正常帧 | 8N1 | 0x55, 0xAA | 接收端正确收到 |
| 奇校验 | 8O1 | 0x01(二进制 0000 0001) | 校验位应为 0(1 的个数为奇数) |
| 偶校验 | 8E1 | 0x01 | 校验位应为 1(1 的个数为偶数) |
| 错误帧注入 | 8N1 | 人为拉低停止位 | 接收端报帧错误 |
4.3 SPI 接口测试:时钟极性与相位是魔鬼
SPI 的测试,核心就四个字:CPOL 和 CPHA。CPOL 决定时钟空闲时的电平,CPHA 决定数据在哪个边沿采样。组合起来有四种模式(Mode 0~3)。
我遇到过最典型的坑:两个芯片手册上都写 Mode 0,结果一个把 CPOL=0、CPHA=0 叫 Mode 0,另一个把 CPOL=0、CPHA=0 叫 Mode 0 但内部实现反了。最后拿逻辑分析仪一抓,才发现数据是在错误的边沿被采样的。
4.3.1 时序参数测量
SPI 的时序参数很多,但测试时重点关注这几个:
- SCLK 频率: 实际频率与配置频率的偏差,一般要求 < 5%。
- CS 建立时间: CS 拉低到第一个 SCLK 边沿的时间,必须大于从设备的最小要求。
- CS 保持时间: 最后一个 SCLK 边沿到 CS 拉高的时间。
- 数据建立/保持时间: MOSI/MISO 数据在 SCLK 边沿前后的稳定时间。
实测案例: 某款 LCD 驱动芯片要求 CS 建立时间至少 100ns。我们的主控 SPI 时钟是 20MHz(周期 50ns),CS 和 SCLK 是同时产生的。结果 CS 建立时间只有 30ns,LCD 经常初始化失败。后来在 CS 线上加了一个 RC 延迟,才解决问题。
4.3.2 数据一致性测试
SPI 是全双工的,发一个字节的同时会收到一个字节。测试时,我习惯用“伪随机序列”做压力测试:
// SPI 回环测试示例(主控自测)
uint8_t tx_buffer[256];
uint8_t rx_buffer[256];
// 填充伪随机数据
for (int i = 0; i < 256; i++) {
tx_buffer[i] = (uint8_t)(i * 7 + 13);
}
// 发送并接收
HAL_SPI_TransmitReceive(&hspi1, tx_buffer, rx_buffer, 256, 1000);
// 比对
for (int i = 0; i < 256; i++) {
if (tx_buffer[i] != rx_buffer[i]) {
// 记录错误位置和期望值
printf("Mismatch at %d: tx=0x%02X, rx=0x%02X\n", i, tx_buffer[i], rx_buffer[i]);
}
}
如果回环测试通过,说明 SPI 的物理层和驱动层基本没问题。接下来再挂上真实从设备,做端到端的功能测试。
4.4 I2C 接口测试:地址、应答与仲裁
I2C 比 SPI 复杂,因为它有地址机制、应答机制,还支持多主仲裁。测试时,这三个点一个都不能漏。
4.4.1 设备地址验证
I2C 设备地址有 7 位和 10 位两种。测试时,我一般先发一个“空写”操作(只发地址+写位,不发数据),然后检查是否收到 ACK。
4.4.2 应答时序测试
I2C 协议规定:每个字节后,接收方必须在第 9 个 SCLK 周期拉低 SDA 表示 ACK。如果接收方没准备好(比如正在处理上一个数据),它可以拉低 SCLK(时钟拉伸)来让主设备等待。
测试时,用逻辑分析仪抓取以下场景:
- 正常应答: 第 9 个 SCLK 高电平期间,SDA 为低。
- 无应答(NACK): 访问不存在的地址,应看到 SDA 在第 9 个 SCLK 为高。
- 时钟拉伸: 从设备拉低 SCLK 超过 50μs,主设备是否超时处理。
4.4.3 多主仲裁测试
这个测试在单主系统里可以跳过,但如果是多主系统,必须验证仲裁逻辑。测试方法:
- 两个主设备同时向同一个从设备发送数据。
- 观察 SDA 线上是否出现仲裁(一个主设备拉低,另一个检测到电平不一致后释放总线)。
- 验证仲裁失败的设备是否正确退出并重试。
4.5 协议一致性验证的整体流程
上面讲的是单个接口的测试点。但实际项目中,我们需要一个系统化的验证流程。下面这张图是我自己总结的:
这个流程我用了好几年。每次做接口测试,都按这个顺序来:先确认物理层没问题,再抓时序,最后验证数据链路层。三层都过了,协议一致性才算验证通过。
4.6 测试工具与自动化
手工测接口太累了。我建议你至少准备这几样工具:
- 逻辑分析仪: 至少 8 通道,采样率 100MHz 以上。抓 SPI 和 I2C 时序必备。
- 示波器: 带宽 100MHz 以上,用于测量信号质量和时序参数。
- 串口调试助手: 带自定义帧发送和 CRC 校验功能的。
- 自动化测试脚本: 用 Python 的 pySerial 或 pylibi2c 库,写一个批量测试框架。
我的自动化测试框架结构:
test_interface/
├── test_uart.py # UART 测试用例
├── test_spi.py # SPI 测试用例
├── test_i2c.py # I2C 测试用例
├── test_common.py # 公共函数(日志、比对、报告)
├── config.yaml # 测试配置(波特率、地址、模式)
└── reports/ # 测试报告输出目录
每次跑完,自动生成 HTML 报告,包含每个测试项的通过/失败、波形截图、错误详情。这样不管是自己看还是给客户看,都一目了然。
4.7 本章小结
接口测试说难不难,说简单也不简单。关键是把每个接口的“脾气”摸清楚:UART 怕波特率漂移,SPI 怕时钟模式配错,I2C 怕地址冲突和应答超时。
我个人习惯是:每次新板子回来,先不跑应用逻辑,花半天时间把这三个接口的物理层和时序层测一遍。这半天花得值——它能让后面几周的集成调试少掉 80% 的坑。