深入ATSHA204A:手写Linux驱动实现I2C加密芯片认证 简介ATSHA204A是Microchip推出的安全认证加密芯片支持SHA-256与AES-128算法在Linux/Android设备中负责身份验证与数据保护。这套驱动源码包面向嵌入式驱动开发与系统安全集成人员聚焦内核与芯片的高效通信解决初始化、I2C读写、命令交互等关键问题。资源共12个文件含6个头文件、4个C源文件、1个Makefile和1个Kconfig分别承担接口定义、核心驱动逻辑、SHA辅助函数、编译规则与内核配置选项结构清晰便于按模块移植。压缩包仅32KB轻量精炼。已有485人学习下载具有较高实践参考价值。研读代码不仅能够掌握字符设备注册、ioctl接口设计、设备树匹配等Linux驱动核心技能还能理解ATSHA204A在安全启动、密钥管理等实际场景中的典型用法为自身项目提供可复用模板与排查思路。1. 为什么我选择自己写 ATSHA204A 的 Linux 驱动而不是直接调内核现成的某公司做了一批工业采集设备固件被同行抄走板卡也被复刻。后面在板上加了 ATSHA204A 加密芯片做绑定校验Linux 系统启动时读芯片序列号、算 HMAC验证不过就拒绝进业务系统。这个芯片本身是 I2C 接口的硬件安全模块内核源码树里其实已经有 atsha204a 的驱动但真正上手会发现它对应用层非常不友好——接口绑定在内核 crypto 框架里想用它做 license 绑定、防抄板的软件/插件方案还是得自己把 I2C 协议和命令封装搞清楚。这里有个反直觉的点驱动源码里最值钱的不是那几百行内核模块而是命令帧组装、CRC 计算、唤醒时序这些看不见的细节。把这些拆开讲透比直接拿现成驱动 insmod 有用得多。适合正在做嵌入式安全启动、设备认证、固件完整性校验的开发者也适合想理解硬件安全芯片怎么和 Linux 配合的人。下文按“芯片协议 → 驱动架构 → 跑通验证 → 踩坑 → 改造”这条线展开。2. 硬件与协议层一颗 I2C 加密芯片的通信细节2.1 芯片能干什么SHA-256、HMAC 与一次性密码本ATSHA204A 不是一个通用 MCU而是一颗专门做对称认证的加密协处理器。它内部集成 SHA-256 引擎支持 Nonce、HMAC、MAC、Random、DeriveKey、Lock 这些命令密钥存放在芯片内部的内存区里通过 I2C 总线无法直接读出。正因为密钥物理上不可读它才能承担设备认证和防伪场景里的“信任根”角色。实际项目中我常把它用在三件事上一是开机时算 HMAC 校验固件合法性二是和上位机做挑战应答上位机生成随机数发给设备设备用芯片里的密钥算 MAC 回传以此证明自己是真设备三是配合硬件随机数发生器做密钥种子下发。和纯软件方案比它最大的优势是密钥不存在 Flash 里抄板的人即使把整个 Flash dump 出来也拿不到密钥。芯片通过 I2C 接口连接主机默认 7 位地址是 0x64供电范围 2.0V 到 5.5VI2C 速率支持到 1MHz。需要注意的是芯片平时处于睡眠状态每次通信前要先发送唤醒脉冲否则命令直接丢进黑洞。2.2 内存模型与配置 Zone哪些数据必须烧死哪些能现场写这颗芯片内部不是一块简单 EEPROM而是分了好几个区配置区Config Zone88 字节、数据区Data Zone512 字节、OTP 区 64 字节。数据区进一步划分成 16 个 slot每个默认 32 字节密钥就存在 slot 里。每个 slot 有独立的读写权限配置比如可以配成“可写、不可读”“不可写、不可读”“加密读写”等。理解 zone 概念是驱动开发的前提。Config Zone 里存着 I2C 地址、slot 配置、锁定状态这些元数据Data Zone 里的 slot 是业务数据的主战场。出厂时芯片所有 zone 都是可写的一旦执行 Lock 命令把配置区或数据区锁死里面的内容就永久固定再写会直接返回错误。所以生产流程上必须先规划好哪几个 slot 放什么密钥再统一锁定锁定之后就是一次性烧录没有后悔药可吃。内存区域大小主要用途锁定方式Config Zone88 字节I2C 地址、slot 权限、锁状态LockConfig 命令Data Zone512 字节16 个 slot密钥、随机数种子、业务数据LockData 命令OTP Zone64 字节一次性可编程数据LockData 命令2.3 命令帧格式与 CRC-16 计算驱动和芯片通信的最小单位是命令帧。I2C 上传输的报文不是裸的命令字节而是带长度、CRC 的帧。发命令时第一字节固定是 0x03I/O 标志第二字节是包长度后面跟 opcode、参数和数据最后两个字节是 CRC-16 校验值。常用命令的 opcode 我用表格整理一下方便对照数据手册查参数命令名opcodeParam1 用途Param2 用途Nonce0x16随机数模式NumIn 字节数SHA0x47算法模式上下文字节数HMAC0x11slot 号0MAC0x08slot 号0Random0x1B00Read0x02zone/slot偏移量Write0x03zone/slot偏移量Lock0x17zone 类型0Info0x30参数模式0CRC 是这里最容易翻车的地方。ATSHA204A 的 CRC-16 是查表法实现多项式 0x8005计算范围是从“包长度”字节开始一直到数据区最后一个字节不包括开头的 0x03 标志。很多初次写驱动的人把 0x03 也算进去结果芯片一直回 0xEE 错误码。初始化值通常用 0x0000字节需要反转处理我一般直接抄数据手册附录的查表函数不做任何修改验证直接用。3. 驱动架构拆解内核态模块 用户态 ioctl 的两层设计这里直接按自研驱动的方案拆因为内核自带的 atsha204a 驱动更接近一个 crypto 加速器接口和业务认证场景不对齐。自己写的驱动采用两层结构底层内核模块负责 I2C 通信和命令收发暴露一个字符设备 /dev/atsha204a用户态库负责拼装业务参数通过 ioctl 下发命令。为什么这样设计内核态做 I2C 访问可以用 i2c-core 的完整能力而业务逻辑放在用户态调试起来不用动不动就重启机器。3.1 设备注册用 miscdevice 而不是 register_chrdev字符设备用 miscdevice 注册是嵌入式驱动里的常见做法。它自动分配主设备号不用手动 mknod模块加载后 /dev/atsha204a 就会出现在 /dev 目录下。代码不长但五脏俱全#include linux/miscdevice.h #include linux/fs.h static const struct file_operations atsha_fops { .owner THIS_MODULE, .open atsha_open, .release atsha_release, .unlocked_ioctl atsha_ioctl, #ifdef CONFIG_COMPAT .compat_ioctl atsha_ioctl, #endif }; static struct miscdevice atsha_dev { .minor MISC_DYNAMIC_MINOR, .name atsha204a, .fops atsha_fops, }; static int __init atsha_init(void) { return misc_register(atsha_dev); } module_init(atsha_init);这段代码把驱动的行为收敛到 fops 上挂载 ioctl 处理函数。open/release 里只做最简单的引用计数打开不申请内存、不初始化硬件真正的初始化放在第一次 ioctl 时做。选 miscdevice 而不是 register_chrdev 的好处是不用自己管设备号分配也不会和内核里其它驱动抢号对一颗独立外设芯片来说正好够用。compat_ioctl 这里必须和 unlocked_ioctl 一起写否则 32 位用户态程序在 64 位内核上跑 ioctl 会直接报 ENOTTY。这个坑我在早期版本里踩过表现是交叉编译的工具链是 32 位内核是 64 位测试程序一开就挂。3.2 命令组装与发送把协议细节放进一条 ioctlioctl 的入参是一个统一定义的结构体包含 opcode、参数和缓冲区。内核态拿到参数后拼命令帧通过 i2c_master_send 发出去。这里最关键的是把帧长度算对static int atsha_send_cmd(struct i2c_client *client, uint8_t opcode, uint8_t param1, uint16_t param2, uint8_t *data, size_t data_len) { uint8_t cmd[128]; uint8_t crc[2]; size_t idx 0; if (data_len 7 sizeof(cmd)) return -EINVAL; cmd[idx] 0x03; /* I/O 标志 */ cmd[idx] data_len 7; /* 包长度 opcode p1 p2 crc count 共 7 字节 data */ cmd[idx] opcode; cmd[idx] param1; cmd[idx] (param2 8) 0xFF; cmd[idx] param2 0xFF; if (data_len) { memcpy(cmd[idx], data, data_len); idx data_len; } atsha_crc16(cmd[1], idx - 1, crc); cmd[idx] crc[0]; cmd[idx] crc[1]; return i2c_master_send(client, cmd, idx); }逻辑说明第一字节 0x03 是协议固定的 I/O 标志第二字节是整个包从 count 字节开始到 CRC 结束的总长度所以是 data_len 7。CRC 的输入从 cmd[1] 开始长度为 idx - 1把开头的 0x03 排除在计算范围外。i2c_master_send 返回的是实际发送的字节数调用方必须校验返回值等于 idx否则说明帧没发完整。接收侧对应参数说明param2 按大端序拆成两个字节因为芯片内部是 16 位寄存器比如 Nonce 命令里 param2 的高 14 位表示外部随机数的字节数低 2 位是模式位。数据缓冲区的长度受限于芯片固件最大不超过 80 字节左右超过会返回错误码所以缓冲定义 128 字节足够覆盖所有命令场景。3.3 响应读取与忙等待0x77 状态字的处理芯片收到命令之后需要几十毫秒计算期间 I2C 总线上如果直接读会读到 0x77 状态字节表示“忙”。驱动需要轮询等待状态从 0x77 变成 0x00 后才能读出完整响应帧static int atsha_wait_response(struct i2c_client *client, uint8_t *resp, size_t max_len) { uint8_t status; int ret; int try 10; do { ret i2c_master_recv(client, status, 1); if (ret 1 status ! 0x77) break; msleep(10); } while (--try 0); if (try 0) return -ETIMEDOUT; if (status ! 0x00) return -EIO; /* 0xEE 表示 CRC 错误0x0F 表示命令不允许 */ ret i2c_master_recv(client, resp[1], resp[0] - 1); return atsha_crc16_verify(resp, resp[0] - 2) ? 0 : -EBADMSG; }逻辑说明每次读一个字节判断状态status 为 0x00 时说明响应已经就绪此时第一字节是响应包长度 count。再调用 i2c_master_recv 读取剩余 count - 1 个字节最后对整个响应帧做 CRC 校验。这里有个细节i2c_master_recv 在 I2C 协议上发的是读请求芯片忙时会自动返回 0x77 而不是拉低总线所以轮询不会阻塞总线。参数说明try 设 10 次超时上限每次 10ms。SHA 计算在芯片内大约耗时 10ms 级别Nonce 更快10 次轮询通常第一次或第二次就成功。如果频繁超时要检查是不是唤醒时序没做对而不是拉大 try 次数硬扛。3.4 抽象接口从 cmd 层到 SHA / MAC 语义层直接暴露 opcode 给用户态不够友好业务方不应该关心 0x47 是 SHA、0x11 是 HMAC。我在 ioctl 层做了一层语义映射用户态传一个结构体指定操作类型内核负责翻译成对应的 opcode 并填充参数。ioctl 命令入参结构体内核行为出参AT_IOC_SHA256req: data len发送 SHA 命令req.data 回填 32 字节摘要AT_IOC_HMACreq: slot data发送 HMAC 命令req.data 回填 32 字节 MACAT_IOC_RANDOMreq发送 Random 命令req.data 回填 32 字节随机数AT_IOC_READ_SLOTreq: slot offset发送 Read 命令req.data 回填 slot 内容AT_IOC_WRITE_SLOTreq: slot data发送 Write 命令无AT_IOC_LOCKreq: zone 类型发送 Lock 命令无这个设计把协议细节留在内核态用户态拿到的是一组“动词”。后续如果要换芯片型号比如从 ATSHA204A 换到 ATECC608A只需要改内核层实现用户态接口保持不动二次开发和升级都省事。4. 编译、加载与验证从 Makefile 到摘要对比跑通全流程4.1 交叉编译与 Makefile驱动要跟着目标板的内核版本编译不能直接拿发行版内核编出来的 .ko 到处用。Makefile 里指定内核源码树路径和交叉编译工具链obj-m : atsha204a.o KDIR : /opt/linux-6.1.28 CROSS_COMPILE : aarch64-none-linux-gnu- all: $(MAKE) ARCHarm64 CROSS_COMPILE$(CROSS_COMPILE) -C $(KDIR) M$(PWD) modules clean: $(MAKE) -C $(KDIR) M$(PWD) clean逻辑说明KDIR 指向目标板同版本的内核源码树编译系统会读取源码树里的 Module.symvers 和头文件来生成模块。如果你的内核已经单独编译过最好先确认源码树根目录下有 .config 文件否则 make modules 会提示找不到配置。交叉编译工具链前缀要和板子的 libc 匹配这里用的是 aarch64 路线。编译之前还有一步容易漏内核源码树需要先执行 make modules_prepare把生成的头文件准备好否则外置模块编译会报缺少 generated/autoconf.h。这一步做一次就够了后续改驱动代码只需要重新 make。4.2 设备树节点与模块加载编译出的 atsha204a.ko 拷贝到板子后先用 insmod 加载确认注册成功insmod atsha204a.ko dmesg | tail -n 10 ls -l /dev/atsha204a如果使用设备树描述硬件需要在 I2C 控制器节点下面加子节点i2c1 { status okay; clock-frequency 100000; atsha204a64 { compatible atmel,atsha204a; reg 0x64; }; };驱动里的 i2c_device_id 表需要声明 compatible 字符串static const struct i2c_device_id atsha_id[] { { atsha204a, 0 }, {} }; MODULE_DEVICE_TABLE(i2c, atsha_id);逻辑说明设备树里 reg 0x64 就是芯片的 I2C 地址。如果板子上 I2C 总线上同时挂了别的设备要先用 i2cdetect 扫一遍确认地址没有被占用。clock-frequency 这里我习惯先设 100kHz 验证跑通了再往上拉因为后面讲过有兼容芯片在高频下会翻车。insmod 之后 /dev/atsha204a 由 miscdevice 自动创建。如果没有出现多半是设备树节点没匹配上用 dmesg 看 i2c subsystem 的输出或者检查 /sys/bus/i2c/devices/ 下有没有对应目录。4.3 用户态测试程序与 openssl 对拍驱动跑通后第一个验证动作是算 SHA-256 摘要和 openssl 的结果对拍#include stdio.h #include string.h #include fcntl.h #include sys/ioctl.h #include atsha204a.h int main(void) { int fd open(/dev/atsha204a, O_RDWR); struct atsha_req req; int i; if (fd 0) { perror(open); return 1; } memset(req, 0, sizeof(req)); req.op AT_SHA256; req.data_len 11; memcpy(req.data, hello atsha, 11); if (ioctl(fd, AT_IOC_SHA256, req) 0) { perror(ioctl); return 1; } printf(sha256: ); for (i 0; i 32; i) printf(%02x, req.data[i]); printf(\n); close(fd); return 0; }逻辑说明测试程序把待哈希数据放进 req.data调用 ioctl 后摘要直接回填在同一块缓冲区。硬件 SHA 和软件 SHA 结果是同一个标准算法正常情况输出应该完全一致。如果驱动实现有错开局就会翻车。对拍用这条命令echo -n hello atsha | openssl dgst -sha256两边输出一致说明命令帧组织、CRC 和响应解析都正确接下来再测 HMAC 和随机数就有底气了。这里有个经验先跑 SHA 而不是先跑 HMAC因为 SHA 命令不依赖任何 slot 配置能隔离“驱动问题”和“密钥配置问题”。4.4 实用工具链配置 zone、烧写密钥、锁定驱动验证通过只是第一步真实产品要做密钥注入。常见做法是写一个小工具批量执行 Write 命令把密钥写进 slot然后执行 Lock 命令锁定。这个工具在生产烧录阶段跑只在管理机上存在不出现在目标设备里。我会把工具设计成四个子命令atsha_tool config --read atsha_tool write --slot 2 --key-file secret.bin atsha_tool lock --zone data atsha_tool verify --slot 2 --key-file secret.bin先读配置确认芯片处于未锁定状态再写密钥锁数据区最后用 verify 从芯片内部计算 HMAC 验证密钥是否生效。写进去的密钥如果发现写错了只要还没 Lock 就还能覆写所以 verify 放在 Lock 之前做锁定之后就真的不能再动了。5. 避坑手册ATSHA204A 驱动开发里的五类翻车现场5.1 芯片无应答i2c_master_send 返回 -ENXIO现象insmod 正常/dev/atsha204a 也出现了但一调用 ioctl 就报 I/O errordmesg 里看到 i2c 传输失败。原因芯片默认处于睡眠状态直接发命令帧它根本收不到。数据手册要求先产生一个唤醒脉冲SCL 保持高电平SDA 拉低至少 60 微秒再释放然后等待约 1.5ms 让芯片启动。解决在每次发送命令之前增加唤醒流程。如果主板把 SDA/SCL 接到了 GPIO可以用 gpiod 直接拉低 SDA 实现如果用 I2C 控制器可以通过 i2c_master_send 发送一个零字节包配合 usleep_range 来模拟。注意唤醒脉冲后要等一下别立刻发命令。5.2 CRC 校验失败芯片返回 0xEE现象命令发出后轮询读到的状态不是 0x00而是 0xEE这表示芯片认为收到的帧 CRC 或长度不合法直接拒绝处理。原因CRC 计算范围搞错。帧的第一个字节 0x03 是 I2C 传输的包裹标志不属于命令内容很多参考代码把长度、opcode、数据算完 CRC 后又把 0x03 单独甩在 CRC 外面顺序一乱就验不过。另一种可能是 count 字段忘了加 7导致芯片按错误长度解析帧。解决严格按数据手册的算法实现CRC 输入从 count 字节开始到 data 最后一字节不包括 0x03。写完驱动后用 5.3 节的方法先跑 SHA 命令验证能出摘要就说明 CRC 通路对了。5.3 内核态 msleep 在原子上下文直接 oops现象ioctl 里调用了 msleep 等待芯片计算常规调用没问题但某个业务线程在持有 spinlock 的场景下调用 ioctl内核直接报 scheduling while atomic然后系统卡死。原因msleep 会触发进程调度spinlock 临界区不允许调度。加密等待天然要阻塞拿自旋锁做同步是错误的设计。解决驱动里的互斥用 mutex 而不是 spinlock等待只用 msleep禁止在持锁状态下调用。如果上层业务确实需要在原子上下文访问就把请求丢给 workqueue 异步处理ioctl 只负责提交和等待完成。这个设计在驱动评审时也是必问项。5.4 Slot 写错后数据区已锁定只能换芯片现象生产时把密钥写进错的 slot执行了 LockData 之后才发现问题想重新写却返回 0x0F。原因Lock 命令是不可逆操作数据区锁定后 slot 的写权限就永久关闭。写脚本时没有把“可写校验”放在 Lock 前面。解决生产工具把流程组织成 write - verify - lock 三步verify 阶段用芯片内部计算 HMAC 和预期值比对比对通过才允许执行 lock。我在这上面翻过车一批板子锁完后发现密钥种子文件搞错了整批换芯片从那以后批量烧录前都会先用一块样板跑完整流程再上产线。5.5 I2C 频率调太高随机数读出全 0xFF现象驱动逻辑没变只是把设备树的 clock-frequency 从 100kHz 提到 400kHz之后 Random 命令读出的数据全是 0xFF。原因部分芯片或转接板在 400kHz 下的时序裕量不足芯片没完全唤醒或者响应帧采样出错。数据手册标注了 1MHz 上限但实际跑起来受 PCB 走线、上拉电阻、电平转换器影响很大。解决先在 100kHz 下把全流程验证通过再逐步提频率。如果生产环境必须跑 400kHz检查 I2C 总线上拉电阻是否在 2.2k 到 4.7k 之间并且用示波器看 SDA/SCL 的上升沿超过 300ns 就要调整。这属于玄学范畴但频率降一档往往就好了。6. 改造方向把驱动做成可复用的设备认证服务6.1 会话级认证流程设计基础 ioctl 就绪后下一步是把单条命令封装成会话级的认证服务。我在实际项目里用的是挑战应答模型设备启动时从芯片取 32 字节随机数作为 nonce把这个 nonce 发给上位机上位机用共享密钥计算 HMAC-SHA256(nonce) 返回设备再用芯片内部 slot 里的密钥算一次 HMAC比对两个结果。struct atsha_req req {0}; req.op AT_RANDOM; ioctl(fd, AT_IOC_RANDOM, req); /* 先取 nonce */ memcpy(hmac_in, req.data, 32); req.op AT_HMAC; req.slot 2; /* 共享密钥所在的 slot */ req.data_len 32; ioctl(fd, AT_IOC_HMAC, req); /* 芯片内部完成 HMAC 计算 */ if (memcmp(req.data, hmac_from_host, 32) 0) auth_pass(); else auth_fail();这样设计的好处是密钥永远不离开芯片即使上位机被逆向拿到的也只是某一次会话的 HMAC 结果无法反推密钥。把这套流程封装成一个库上层业务只需要调用 auth_once()不需要理解任何协议细节。6.2 密钥管理与验证手段密钥管理要提前规划 slot 占用表比如 slot 0 放 HMAC 主密钥slot 1 放固件加密密钥slot 2 留作现场更新。如果产品需要远程换密钥数据区不能锁定要接受密钥可能被覆写的安全降级如果追求最高安全性就锁定数据区配合云端证书体系做一次性绑定。验证阶段不能只看认证通过还要模拟三类攻击错误密钥、重放旧 nonce、篡改 HMAC 结果。这三类都必须返回认证失败且连续失败几次后触发退避策略避免暴力穷举。性能上整个认证过程在几十毫秒量级适合启动时做一次性校验不适合高频调用。从那以后我每设计一块带安全芯片的板子第一件事就是画 zone 配置表把每个 slot 的用途、读写权限、锁定时机全部标清楚再动手写驱动代码。驱动本身不难难的是把芯片的物理特性、内存规划和业务认证流程对齐这个顺序反过来就会在产线上吃苦头。希望帮到你。本文还有配套的精品资源点击获取