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安全芯片,死活不响应。查了半天,发现是起始条件的保持时间不够,芯片根本没识别到起始信号。

避坑指南: 我曾经在批量生产时发现,同一批安全芯片,有的能通信有的不行。最后定位到是I2C上拉电阻的问题。安全芯片对上升沿时间很敏感,建议上拉电阻选4.7kΩ,总线电容大的话可以降到2.2kΩ。

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,意思是“我忙着呢,别打扰我”。这时候主控需要重新发送起始条件重试。

实战技巧: 跟安全芯片通信时,建议在每次写操作后加一个延时,比如5ms。因为安全芯片内部有EEPROM写入时间,你连续发指令它可能不响应。我一般会写一个重试函数,最多重试3次。

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去抖时间”。

避坑指南: 我曾经用GPIO模拟SPI,CS拉低后直接发数据,结果安全芯片偶尔不响应。后来用示波器一看,CS下降沿到第一个SCK上升沿只有200ns,远小于芯片要求的1μs。加了个微秒延时就解决了。

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读写。它需要先发一个“命令包”,再等芯片处理,最后读“响应包”。

比如读状态寄存器:

  1. 主控发写命令:0x03(读命令)+ 0x00(寄存器地址)
  2. 芯片处理,内部拉高忙标志
  3. 主控轮询状态,直到芯片就绪
  4. 主控发读命令,芯片返回寄存器值
核心要点: 安全芯片的寄存器操作,本质上是“命令-响应”模式。不要想着像操作普通EEPROM那样直接读地址。你得先发命令,再等结果。

4.5 实战:用逻辑分析仪抓包

最后,我强烈建议你准备一个逻辑分析仪。别靠猜,别靠printf。逻辑分析仪一抓,什么时序问题都现原形。

我一般会抓这几个关键点:

  • 起始条件/停止条件是否完整
  • 地址字节是否正确(尤其是读写位)
  • ACK/NACK是否正常
  • 数据字节的顺序和内容

嗯,这一章内容不少。你先把I2C和SPI的时序吃透,寄存器映射多看几遍数据手册。下一章我们会讲如何用这些协议去读写安全芯片的密钥存储区——那才是真正考验功底的地方。