4. 通信协议基础:掌握I2C、SPI、UART等与安全芯片通信的底层协议,理解时序图与寄存器映射
各位同学,今天我们来聊聊安全芯片的“语言”——通信协议。说白了,安全芯片再厉害,它也得跟主控MCU“说话”。我这些年调试过的安全芯片,十有八九的坑都出在通信这一块。你想想看,加密算法算得再快,数据传错了也是白搭。
这一章,我会把I2C、SPI、UART这三个最常用的协议掰开揉碎了讲。重点不是背时序图,而是理解它们跟安全芯片打交道时,哪些地方容易出幺蛾子。
4.1 I2C协议:两根线搞定一切?没那么简单
I2C是安全芯片最常用的接口。为什么?因为它只需要两根线:SCL(时钟)和SDA(数据)。对于引脚紧张的MCU来说,这简直是福音。
但我要泼盆冷水——I2C的坑,恰恰就藏在“简单”里。
4.1.1 时序要点:起始条件与停止条件
先看最基本的时序。I2C总线上,SCL高电平时,SDA从高变低,这叫起始条件。反过来,SCL高电平时,SDA从低变高,这叫停止条件。
// 伪代码:I2C起始条件
void i2c_start(void) {
SDA_HIGH();
SCL_HIGH();
delay_us(5); // 保持时间
SDA_LOW(); // SCL高时,SDA下降沿
delay_us(5);
SCL_LOW(); // 准备传输数据
}
嗯,这里要注意:很多新手工程师会忽略保持时间。我曾经遇到过一块ATECC608A安全芯片,死活不响应。查了半天,发现是起始条件的保持时间不够,芯片根本没识别到起始信号。
4.1.2 7位地址与读写位
起始条件之后,主控要发送7位从机地址加1位读写位。安全芯片的地址通常是固定的,比如ATECC608A的默认地址是0xC0(写)和0xC1(读)。
| 操作 | 地址字节 | 说明 |
|---|---|---|
| 写操作 | 0xC0 (1100 0000) | 7位地址0x60 + 写位0 |
| 读操作 | 0xC1 (1100 0001) | 7位地址0x60 + 读位1 |
我个人习惯是把地址定义成宏,免得每次写代码时换算。比如:
#define SEC_CHIP_ADDR_WRITE 0xC0
#define SEC_CHIP_ADDR_READ 0xC1
4.1.3 应答机制:ACK与NACK
I2C的应答机制是它的精髓。每发送一个字节,接收方必须拉低SDA表示ACK。如果接收方没拉低(保持高电平),那就是NACK,表示“我没收到”或“我不想收”。
安全芯片的应答跟普通传感器不太一样。我遇到过一种情况:安全芯片在处理内部加密运算时,会主动发出NACK,意思是“我忙着呢,别打扰我”。这时候主控需要重新发送起始条件重试。
4.2 SPI协议:速度至上,但小心片选
SPI比I2C快得多,全双工,四根线:SCK、MOSI、MISO、CS。安全芯片如果要做大量数据传输(比如固件加密升级),SPI是首选。
4.2.1 四种模式:CPOL和CPHA
SPI有四种模式,由时钟极性(CPOL)和时钟相位(CPHA)决定。安全芯片通常支持模式0(CPOL=0, CPHA=0)或模式3(CPOL=1, CPHA=1)。
我建议你直接看数据手册。别猜,别试。我曾经因为SPI模式配错,浪费了整整两天。安全芯片的数据手册里会明确写“SPI Mode 0”或“SPI Mode 3”。
// SPI模式0配置示例(STM32 HAL库)
SPI_HandleTypeDef hspi1;
hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; // CPOL=0
hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; // CPHA=0
4.2.2 片选信号:别让它乱跳
SPI的片选(CS)是硬伤。很多工程师以为CS就是拉低拉高那么简单。但安全芯片对CS的时序要求很严格。
举个例子:ATECC608A在SPI模式下,CS拉低后必须等待至少1μs才能开始发时钟。传输结束后,CS拉高后也要保持至少1μs才能再次拉低。这叫“CS去抖时间”。
4.2.3 数据格式:MSB还是LSB?
安全芯片通常使用MSB先行(最高位先发)。但有些芯片支持配置。我个人习惯是统一用MSB,这样跟大多数MCU的SPI外设默认配置一致。
4.3 UART协议:简单可靠,但注意波特率误差
UART在安全芯片中用得相对少一些,但有些老款芯片或者专用安全模块(比如SIM卡)还是用UART。UART只有两根线:TX和RX,异步通信,没有时钟线。
4.3.1 波特率:误差不能超过2%
UART的波特率误差是个大问题。安全芯片的UART接口通常要求波特率误差在±2%以内。如果MCU的时钟不准,或者分频系数算错了,通信就会丢数据。
我建议用示波器实测一下UART的波形,数一下一个位的宽度。比如115200bps,一个位应该是8.68μs。如果偏差超过0.17μs,就得调整分频系数了。
// 波特率计算示例(假设MCU时钟16MHz)
// 目标波特率:115200
// 分频系数 = 16000000 / 115200 = 138.89
// 取整为139,实际波特率 = 16000000 / 139 = 115107
// 误差 = (115107 - 115200) / 115200 = -0.08% ✅ 在允许范围内
4.3.2 数据格式:8N1是标配
安全芯片的UART通信格式通常是8位数据、无校验、1位停止位(8N1)。有些芯片支持奇偶校验,但我建议用8N1,兼容性最好。
4.4 寄存器映射:读懂芯片的“内存地图”
好了,协议讲完了,接下来是重点——寄存器映射。安全芯片的功能,说白了就是通过读写寄存器来实现的。你发一个指令,本质上是往某个寄存器写一个值。
4.4.1 寄存器地址与功能
每个安全芯片都有寄存器映射表。比如ATECC608A的配置区,地址从0x00到0x07,分别存储序列号、配置数据等。
| 寄存器地址 | 名称 | 功能 | 访问权限 |
|---|---|---|---|
| 0x00 | SN[0:3] | 序列号低4字节 | 只读 |
| 0x01 | RevNum | 芯片版本号 | 只读 |
| 0x02 | Config | 配置选项 | 读写(需认证) |
| 0x03 | Status | 状态寄存器 | 只读 |
我个人习惯是把寄存器地址定义成枚举,这样代码可读性高:
typedef enum {
REG_SN_LOW = 0x00,
REG_REV_NUM = 0x01,
REG_CONFIG = 0x02,
REG_STATUS = 0x03
} sec_chip_reg_t;
4.4.2 读写时序:组合指令
安全芯片的寄存器读写通常不是简单的I2C读写。它需要先发一个“命令包”,再等芯片处理,最后读“响应包”。
比如读状态寄存器:
- 主控发写命令:0x03(读命令)+ 0x00(寄存器地址)
- 芯片处理,内部拉高忙标志
- 主控轮询状态,直到芯片就绪
- 主控发读命令,芯片返回寄存器值
4.5 实战:用逻辑分析仪抓包
最后,我强烈建议你准备一个逻辑分析仪。别靠猜,别靠printf。逻辑分析仪一抓,什么时序问题都现原形。
我一般会抓这几个关键点:
- 起始条件/停止条件是否完整
- 地址字节是否正确(尤其是读写位)
- ACK/NACK是否正常
- 数据字节的顺序和内容
嗯,这一章内容不少。你先把I2C和SPI的时序吃透,寄存器映射多看几遍数据手册。下一章我们会讲如何用这些协议去读写安全芯片的密钥存储区——那才是真正考验功底的地方。