ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

atsha204a Linux驱动源码实战:命令帧、CRC与I2C时序避坑指南

atsha204a Linux驱动源码实战:命令帧、CRC与I2C时序避坑指南 简介加密芯片ATSHA204A的Linux驱动源码面向嵌入式Linux驱动开发者和安全相关项目人员。驱动负责内核与芯片间的通信实现设备初始化、I2C读写、认证命令封装及用户空间访问接口可支撑设备身份认证、数据加密、防抄板等安全场景。压缩包共12个文件整体仅32KB包含6个头文件、4个C源文件以及Makefile和Kconfig。头文件定义硬件接口、数据结构与错误码C源文件实现设备注册、命令处理、传输层逻辑Makefile和Kconfig用于内核编译配置。源码将通信层与API层分离便于移植到不同硬件平台同时给出清晰的层次划分方便阅读。已有485人学习下载。整体结构紧凑既可直接参考集成也能帮助理解Linux字符设备驱动与安全芯片的交互流程适合需要快速落地ATSHA204A或系统学习密码学硬件驱动的开发者。1. 能防抄板的 atsha204alinux 驱动源码到底在驱动什么我接过一个被抄板的产品固件可以从 Flash 里完整 dump 出来AES 密钥就埋在代码段里反编译十分钟就能定位。后来方案改成把密钥写进加密芯片 atsha204a主控只负责通过 linux 驱动源码去调用它——固件再被 dump 也没有密钥了。这就是这颗芯片在嵌入式 linux 产品里的典型位置密钥不落地、认证不裸奔。但真去写这套 linux 驱动源码时你会发现它不像驱动一个 EEPROM 那么简单。芯片有状态机、有命令帧、有 CRC、有唤醒时序甚至忙的时候连 ACK 都不给你。这篇文章不打算让你去抄一份现成代码而是从命令帧开始把最小可用的用户态驱动写出来再帮你把设备地址、CRC 多项式、忙等待、WAKE 这四个最容易翻车的地方一次说透。适合正在做防抄板、安全启动、IoT 设备认证的嵌入式工程师也适合被这颗芯片折腾过但没系统捋过时序的 Linux 驱动开发。2. 命令帧与 I2C 时序先把 atsha204a 驱动的基本盘搞清楚2.1 一次 I2C 会话在做什么命令、状态、响应三段式atsha204a 挂在 I2C 总线上但它不是普通从机。你写进去的不是寄存器地址而是一条「命令帧」读出来的也不是寄存器值而是这条命令的执行结果。一次完整会话固定是三个阶段第一阶段主机把命令帧写到芯片。芯片收到后开始内部运算比如生成随机数、计算 MAC、更新摘要。第二阶段主机不断去读芯片的 STATUS 寄存器判断运算是否完成。这个阶段芯片可能对总线上的访问不响应表现为主机读不到数据甚至收到 NACK。第三阶段运算完成后主机再从芯片读出响应数据包。很多刚接触的人只做第一步和第三步把第二步漏了结果就是命令发出后立刻去读读回来一堆空数据然后开始怀疑硬件。实际上芯片内部跑的是硬件 AES 引擎和 EEPROM 操作几十毫秒的等待是常态这一步在驱动源码里必须体现为「轮询」而不是「睡一觉再读」。另外atsha204a 的 I2C 会话通常采用 SMBus 兼容的传输方式单次读写的最大负载约 32 字节。也就是说一次会话能承载的数据量有限像 SHA 摘要这种长数据会按命令拆包但 I2C 层一次传输最大就是 32 字节量级。写驱动时不要按照通用 I2C 驱动那种「地址加数据」的思维来组织代码而要把「命令、状态、响应」三段式作为默认骨架。2.2 命令帧里最容易写错的三个字段Count、Opcode、CRCatsha204a 的所有命令都走同一个帧格式从上到下依次是字段字节数含义Count1整包长度包含 Count 自身Opcode1命令码决定芯片执行什么操作Param11参数一一般是命令模式Param22参数二一般是地址或选项Data0~N可选的输入数据CRC2从 Opcode 开始到 Data 末尾的 CRC-16先说 Count。这个字段经常被误解因为不同厂家的文档表达不一样。atsha204a 数据手册里写的是「包的总字节数」也就是把 Count 自己也算进去。以最小命令 Random 为例整包是 7 个字节Count 1 字节、Opcode 1 字节、Param1 1 字节、Param2 2 字节、CRC 2 字节所以 Count 填 0x07。如果你看到某份开源实现里 Count 填 0x06说明那份实现把 Count 定义为「后续字节数」两者都不影响 CRC 计算范围但混用时包就错位了。再说 CRC。atsha204a 的 CRC 是多项式 0x8005、初值 0x0000、无反射的 CRC-16计算范围是 Opcode 一直到 Data 的最后一个字节不含 Count也不含 CRC 本身。这个参数组合很容易和 CRC-16/CCITT 混淆我后面会在避坑章节单独展开这里只提醒一句不要用你手头库里默认的那个 CRC-16极大概率对不上。最后是 Opcode。常用命令就这么几个建议把这张表直接做成驱动里的命令分发数组命令Opcode典型用途Random0x0C生成随机数Nonce0x03注入随机数或临时数据MAC0x02计算消息认证码CheckMAC0x0A校验 MACGenDig0x15参与摘要运算Read0x50读配置区、数据区或密钥Write0x44写入配置区、数据区或密钥Lock0x1C锁定配置区或数据区Info0xAF读芯片版本、序列号等信息驱动里最核心的命令不是 Read 也不是 Write而是 Random。因为 Random 命令对芯片状态没有前置要求不需要配置锁、不需要密钥槽位、不需要权限校验是验证「命令帧 I2C 时序 CRC」这条链路是否跑通的最短路径。后面的驱动骨架我用的就是它。2.3 用户态还是内核态Linux 驱动源码的两种组织方式提到「linux 驱动源码」很多人的第一反应是要写一个内核 module注册为 i2c_driver挂到 device tree 上。但针对 atsha204a我见过的大多数量产项目走的是完全相反的路用户态守护进程加 i2c-dev或者直接套 Microchip 提供的用户态库 cryptoauthlib。原因很实际。atsha204a 的业务逻辑很重芯片本身有配置区、数据区、密钥槽、各种命令组合这些本质上是一套安全协议不是简单的寄存器读写。放在内核态里每改一个业务逻辑就要重新编译内核模块调试还不方便放在用户态里密钥管理和业务代码天然在同一层调用链短更新也灵活。内核态闭环更适合安全启动或 dm-crypt 这种「密钥绝不能经过用户态」的场景但那通常意味着还要自己实现一套完整的密钥生命周期工作量不是一个量级。实现方式优点典型代价用户态 i2c-dev开发快、易调试、可动态更新WAKE 时序需要外接 GPIO用户态 cryptoauthlib命令协议完整封装库比较大业务层要跟着设计内核态 i2c 驱动密钥不出内核安全边界强开发维护成本高协议实现复杂我的建议是先写用户态最小驱动把芯片跑通确认时序和 CRC 都没问题再评估有没有必要下沉到内核态。对多数嵌入式 linux 产品来说用户态方案已经够用而且调试效率直接决定项目进度。后面要写的驱动源码就是用户态 i2c-dev 这条路线。3. 用 C 和 i2c-dev 写最小用户态 atsha204a 驱动一条 Random 命令跑通3.1 先把总线打开地址、波特率和 /dev/i2c-N用户态访问 I2C 的前提是内核打开了 i2c-dev 支持。启动后检查一下是否存在 /dev/i2c-N这个 N 是总线编号一般在设备树上能查到。不知道编号时用 i2cdetect -l 列出来找到挂 atsha204a 的那条总线。这一步属于嵌入式 linux 的常规操作但经常有人在这卡住原因后面避坑章节会讲。打开设备后要做两件事设置从机地址设置超时与重试策略。atsha204a 的 I2C 地址是 0x64注意这是 7 位地址不是 8 位写地址 0xC8。linux 的 i2c-dev 接口在 ioctl 里用的就是 7 位地址所以传 0x64 而不是 0xC8。#include stdio.h #include stdlib.h #include string.h #include unistd.h #include fcntl.h #include errno.h #include sys/ioctl.h #include linux/i2c-dev.h #include linux/i2c.h #define ATSHA_I2C_ADDR 0x64 #define ATSHA_RANDOM 0x0C #define ATSHA_WAKE_GPIO 0 /* 是否启用 GPIO 唤醒后面讲 */ int atsha_open(const char *bus_path) { int fd open(bus_path, O_RDWR); if (fd 0) { perror(open i2c bus); return -1; } if (ioctl(fd, I2C_SLAVE_FORCE, ATSHA_I2C_ADDR) 0) { perror(I2C_SLAVE_FORCE); close(fd); return -1; } /* 这两项影响总线异常时的行为建议设小值 */ ioctl(fd, I2C_TIMEOUT, 1); ioctl(fd, I2C_RETRIES, 3); return fd; }这里用 I2C_SLAVE_FORCE 而不是 I2C_SLAVE是为了跳过设备树中的地址占用检查。如果总线上还有其他设备用普通 I2C_SLAVE 也足够但 FORCE 在调试阶段省事。I2C_TIMEOUT 的单位是 jiffies设成 1 表示在总线超时后尽快返回I2C_RETRIES 是内核在收到 NACK 后自动重试的次数设小一点更有利于我们控制轮询节奏。3.2 CRC-16 计算多项式 0x8005、初值 0别用 CCITT先把 CRC 函数写出来这是整份驱动源码里最需要较真的地方。atsha204a 的 CRC 参数是多项式 0x8005、初值 0x0000、不反射、不异或输出。对应标准实现就是左移版本static uint16_t atsha_crc16(const uint8_t *buf, size_t len) { uint16_t crc 0x0000; size_t i; int bit; for (i 0; i len; i) { crc ^ (uint16_t)buf[i] 8; for (bit 0; bit 8; bit) { if (crc 0x8000) crc (crc 1) ^ 0x8005; else crc 1; } } return crc; }逻辑说明每个字节先左移到 CRC 的高字节再逐位运算这是「左移非反射」的标准写法。如果你在别的库里看到右移版本多项式应当是 0x84080x8005 的镜像初值同样为 0。两种写法计算结果等价但把 0x8005 直接套进右移算法结果一定错。调用时注意范围。以 Random 命令为例参与 CRC 计算的是 Opcode、Param1、Param2 这 4 个字节不包含 Count 和 CRC 本身。所以构造命令帧时CRC 的输入长度是total_len - 3即整包长度减去 Count 和 CRC 两字节。这个细节在移植时要特别当心很多移植版本就是在长度边界上差了一个字节。3.3 唤醒、发命令、轮询状态、读响应一个最小驱动骨架驱动的主体是三个动作构造命令帧、发送命令帧、轮询并读取响应。这里用一个完整的示例把整条链路串起来命令选 Random返回 32 字节随机数static size_t atsha_build_random(uint8_t mode, uint8_t *tx) { uint16_t crc; size_t total 7; /* Count Opcode Param1 Param2(2) CRC(2) */ tx[0] total; tx[1] ATSHA_RANDOM; tx[2] mode; /* 0: 只生成随机数1: 生成并写入内部种子 */ tx[3] 0x00; tx[4] 0x00; crc atsha_crc16(tx[1], total - 3); tx[5] (crc 8) 0xff; tx[6] crc 0xff; return total; } static int atsha_transceive(int fd, const uint8_t *tx, size_t txlen, uint8_t *rx, size_t rxmax) { int ret, tries; uint8_t status; /* 如果芯片进入休眠这里需要先完成 WAKE 时序稍后细说 */ ret write(fd, tx, txlen); if (ret ! (int)txlen) { perror(write command); return -1; } /* 轮询 STATUSread 返回 1 表示芯片已就绪 */ for (tries 0; tries 200; tries) { usleep(1000); ret read(fd, status, 1); if (ret 1) break; /* EREMOTEIO / ENXIO 意味着芯片还在算或还没醒继续等 */ } if (tries 200) { fprintf(stderr, poll status timeout\n); return -1; } ret read(fd, rx, rxmax); if (ret 0) { perror(read response); return -1; } /* 响应首字节是整包长度按它截断避免多读的 0xFF 混进来 */ if (rx[0] (uint8_t)ret) return rx[0]; return ret; }参数说明write 发送的是完整命令帧内核会在一次 I2C 传输里完成 START、写数据、STOP。轮询部分用 read 读 1 字节对应芯片的 STATUS 寄存器芯片忙时会 NACK 地址read 返回 -1这是正常现象。轮询间隔 1ms最多 200 次整个命令的超时上限约 200ms。Random 命令实际执行很快但 SHA、MAC 这类命令更慢量产时建议把超时做成可配置参数。响应截断是个容易被忽略的细节。atsha204a 的响应包第一个字节是包长度但主机如果请求读 64 字节芯片在有效数据结束后会持续返回 0xFF。用 rx[0] 做一次截断后面校验 CRC 就不会被多余字节干扰。主函数调用起来很直接int main(int argc, char *argv[]) { uint8_t tx[70], rx[70]; size_t txlen; int fd, rlen, i; fd atsha_open(/dev/i2c-1); if (fd 0) return 1; txlen atsha_build_random(0, tx); rlen atsha_transceive(fd, tx, txlen, rx, sizeof(rx)); if (rlen 0) return 1; /* Random 响应Length 32 字节随机数 2 字节 CRC */ for (i 1; i rlen - 2; i) printf(%02x, rx[i]); printf(\n); close(fd); return 0; }这里把输出格式做成纯十六进制方便后续用命令行工具做批量验证。编译时只需要链接标准库和 i2c-dev 头文件没有额外的依赖适合直接丢进嵌入式 linux 的交叉编译环境。3.4 验证驱动先别把随机数拿去用做三件事驱动能打印出十六进制串只算第一步跑通命令闭环要确认三件事否则后面接业务逻辑时会分不清是驱动问题还是芯片问题。第一响应首字节必须是 0x23就是十进制的 35。这说明芯片返回的包长度正确Count 1 字节、随机数 32 字节、CRC 2 字节。第二用上面同一个 atsha_crc16 函数去算 rx[1] 到 rx[32] 的 CRC结果要等于 rx[33] 和 rx[34]。第三连着跑几次每次输出的 32 字节必须不相同这是真随机源的基本特征。如果首字节不是 0x23优先检查命令帧的 Count 是否把 Param2 的长度算错。如果 CRC 对不上回到 CRC 多项式。如果读响应卡死回到轮询节奏。这三条验证路径能覆盖驱动源码里绝大多数初级问题比拿着逻辑分析仪去抓波形更先一步。4. 避坑清单CRC、设备地址、忙等待是三个反复翻车点4.1 命令发出去没 ACK先核对 CRC 多项式而不是怀疑板子现象write 返回成功但 read 状态一直返回 EREMOTEIO或者偶尔能读到响应但校验 CRC 永远失败。原因atsha204a 的 CRC 参数是多项式 0x8005、初值 0x0000、无反射。很多通用库默认给的是 CRC-16/CCITT多项式 0x1021初值 0xFFFF还有的库默认右移反射但多项式写成 0x8005。这两种情况算出来的校验值都和芯片期望值对不上芯片收到 CRC 错误的命令帧会直接丢弃表现就是没有 ACK。解决不要相信库里的命名直接按参数表核对实现方式多项式初值反射/输出atsha204a 规范值0x80050x0000无反射不异或输出等价右移实现0x84080x0000反射算法结果相同常见 CCITT错误0x10210xFFFF无反射不异或输出我一般会在驱动里单独保留这个 CRC 函数不引第三方库就是怕移植时被库的默认参数带偏。这是这颗芯片驱动源码里最隐蔽、也最不值得翻车的点。4.2 i2cdetect 扫到 0x64驱动里却用 0xC8地址高低位的坑现象i2cdetect 明明在 0x64 位置扫到了芯片但驱动里 ioctl 设置地址后write 每次都返回 Remote I/O error。原因i2cdetect 显示的是 7 位地址0x64 就是芯片真正的从机地址。I2C 总线上的 8 位地址由 7 位地址左移一位得到读方向是 0xC9写方向是 0xC8。有些芯片文档会把写地址标成 0xC8让人误以为要写 0xC8但这个值在 linux 内核和 i2c-tools 的世界里是不认的。解决ioctl 和命令行全部统一用 0x64。调试时用 i2cdetect -y 1 看到哪个地址对应芯片驱动里就写哪个不要再做任何换算。4.3 忙等待轮询接收字节模式 NACK 不代表芯片挂了现象命令发送后马上调用 read 读响应返回 -1errno 是 ENXIO 或 EREMOTEIO。换板子、换总线、加延时重试问题依旧。原因atsha204a 在内部计算期间不会响应 I2C 主机这不是故障而是本分。驱动里如果只做「发一条命令、等一下、读一次」大概率读到的是芯片未就绪的状态。芯片计算快的命令几十毫秒慢的上百毫秒固定延时很难覆盖全部命令。解决把「等一下读一次」改成「循环读 STATUS」。每次读之前短睡 1ms连续 100 到 200 次期间只要 read 返回 1 字节就说明芯片就绪再发起响应读取。具体循环在上面的 atsha_transceive 里已经写过量产时把循环次数和 sleep 间隔做成宏方便按芯片型号微调。这个方法也适用于总线上挂着其他 SMBus 设备的场景不会误杀其他从机。4.4 WAKE 缺失第一次成功、之后就卡死的「玄学」现象上电后第一次发 Random 命令能正常返回 32 字节第二次开始全部超时。重新上电又好了再跑一次又卡死。很多人把这归为玄学。原因atsha204a 在总线空闲一段时间后会进入睡眠模式睡眠后不再响应 I2C 访问。上电后的第一次命令恰好赶在芯片醒着所以成功之后芯片睡了再次访问就没有任何响应。重新上电让芯片回到唤醒状态于是出现了「第一次总是成功」的假象。解决每次命令交易前补一个 WAKE 时序把 SDA 拉低至少 60 微秒然后释放等待约 1.5 毫秒让芯片完成唤醒再开始 I2C 传输。这里有个关键限制普通 i2c-dev 的用户态读写产生不了 60 微秒的低电平因为 I2C 控制器的 START 条件只把 SDA 拉低一个位的时间远达不到唤醒要求。常见做法是把 SDA 引脚复用为 GPIO用 GPIO 拉低 80 微秒再切回 I2C 功能产品上更稳妥的是在设备树里给驱动单独配置一个 GPIO 唤醒脚。这部分不是 I2C 驱动源码能单独解决的必须在硬件方案层面留出这个控制点。5. 进阶产品化三件事与量产前的验证方法5.1 把 WAKE 从方案变成标准动作验证阶段的驱动可以允许「上电后不睡觉」这种侥幸量产不行。我一般会把 WAKE 做成一个独立的函数封装在驱动的外层每次 transceive 之前无条件调用一次。具体实现取决于板子如果有 GPIO就请求 GPIO 并切换到 I2C SDA 复用如果是单独拉一组 I2C 给芯片可以直接用 bit-bang 模拟完整的 I2C 时序来发命令。前者更快后者更干净。不管哪种WAKE 一定要在驱动源码里占一个明确的位置而不是靠人工 reset。5.2 超时与重试参数参照表不同命令耗时差异大把这组参数做成可配置项比写死在代码里省心参数推荐值说明WAKE 低电平80us手册要求最小 60us留余量WAKE 后等待1.5ms芯片内部上电稳定时间STATUS 轮询间隔1ms太短空转太长拖慢命令命令总超时100ms~250msRandom 用 100msMAC/SHA 用 250msI2C 速率400kHz与芯片快速模式匹配这些参数在调试阶段打成 log观察每次命令的实际耗时分布比凭感觉调超时可靠得多。5.3 量产前验证随机数重复率与命令时序统计量产前我会写一个验证脚本循环跑 100 次 Random 命令每次输出 32 字节十六进制并求 md5最后统计重复次数。真随机源在 100 条样本里出现完全重复的概率几乎为零如果有了重复先查 WAKE 时序再查电源去耦而不是怀疑驱动算法。另一个指标是命令耗时直方图如果某条命令偶发接近超时上限说明轮询过程中有多次 NACK 在重试这时要回头确认总线上是不是有其他设备抢占了 SDA 电平。这是我在好几个项目里的固定收尾动作。驱动源码写出来只是开始把参数、时序、验证脚本一套留清楚后面量产才能睡个安稳觉。希望帮到你。本文还有配套的精品资源点击获取
返回列表