资讯动态

ATSHA204A Linux驱动源码解析:I2C通信与内核驱动实战指南

发布时间:2026/9/29 7:28:17 来源:尧图企业网站定制
简介一套针对ATSHA204A加密芯片的Linux驱动源码面向嵌入式Linux和安卓底层开发人员围绕安全认证与数据加密场景解决内核与硬件之间的识别、通信和命令控制问题。压缩包体积仅32KB共12个文件其中6个头文件用于寄存器定义、接口声明和错误码枚举4个源文件实现I2C读写、SHA-256计算、应用程序接口封装以及设备驱动注册另有Makefile和Kconfig分别承担编译规则与内核功能开关。已有485人学习下载适合正在调试安全芯片驱动或计划为其增加系统级支持的技术人员。阅读这份源码可以直观理解一个完整Linux字符设备驱动的骨架从模块初始化、文件操作回调到用户态控制指令的收发同时还能学习硬件层与密码运算的分层设计借鉴其错误处理与缓冲区管理手法为后续扩展AES-128加密能力或移植到安卓硬件抽象层打下基础。对于深入掌握加密芯片驱动开发、参与安全启动或密钥管理项目的工程师而言这份紧凑的代码包具有直接的参考价值。1. 先解决一个实际问题ATSHA204A 在 Linux 下为什么难用做嵌入式产品的人对这颗芯片应该不陌生ATSHA204A 是 Microchip 的对称加密芯片靠 I2C 接口和主机通信常用于防抄板、密钥存储、固件完整性校验。但真把它接到 Linux 板子上很多人会卡在第一关——官方资料和例程几乎全围绕 8 位 MCULinux 下的驱动源码要么藏在厂商 SDK 里要么是社区零散维护的版本想直接抄作业都找不到完整的一份。这篇笔记就把我拆过的 ATSHA204A Linux 驱动源码讲透从 I2C 时序、命令帧格式到内核驱动框架和用户态测试最后是几处只有实机跑过才能踩到的坑。适合正在做产品选型、需要把加密芯片快速跑通的嵌入式 Linux 工程师也适合想搞清楚这颗芯片到底怎么和内核驱动配合的入门者。2. 看懂 ATSHA204A 的通信协议I2C 时序与命令帧格式2.1 I2C 地址与唤醒时序先解决设备不上电的问题ATSHA204A 的 I2C 从机地址默认是 0x647 位地址 0x32左移一位后为 0x64但这颗芯片有个特别之处它平时处于低功耗睡眠状态I2C 总线上的普通读写操作不会唤醒它。必须先执行一个唤醒时序芯片才会在 2ms 内准备好接收命令。唤醒时序有两种做法一是把 SDA 引脚拉低至少 60μs注意不是 I2C 起始条件是单纯的 SDA 低电平二是把 WAKE 引脚拉低至少 60μs如果硬件上引出了这个引脚。我一般习惯用 GPIO 控制 WAKE 引脚因为 SDA 拉低容易在总线还有其他设备时造成误触发。唤醒后需要延时 1.52ms 再发命令太急了芯片还没完成内部上电复位命令会直接丢失。另一个容易忽略的点是 I2C 工作频率。ATSHA204A 最高支持 1MHzFast Mode Plus但很多 Linux 板子的 I2C 控制器默认配置是 100kHz。低速没问题高速反而要注意上拉电阻的选型否则波形沿不陡芯片会偶发通信失败。我在 RK 平台和 i.MX 平台都跑过400kHz 是稳定性和兼容性最折中的档位。2.2 命令帧与响应帧从 CRC 到状态字ATSHA204A 的通信模型是主机发命令帧芯片处理完后返回一个状态字4 字节再通过读操作取回完整响应帧。命令帧的格式是固定的字段长度字节说明命令头4命令码 参数1 参数2 数据长度数据032具体命令的附加参数CRC2对前面所有字节做 CRC16CRC16 算法是标准的 CRC-16/CCITT多项式 0x1021初值 0xFFFF。这是新手最容易翻车的地方——如果 CRC 算错芯片会回一个 0x88 状态字表示校验失败命令不执行。状态字节的含义要记牢0x77 表示命令执行完成0x88 是 CRC 错误0x99 是接收错误0xEE 是芯片被锁定后尝试写操作。响应帧的格式和命令帧类似1 字节长度 数据 1 字节 CRC 校验位 2 字节 CRC。也就是说响应帧最后是 4 个字节数据 CRC 校验位 CRC。我第一次拆这个协议时被这个不对称的格式坑了半天后来直接看 Microchip 的 ATECC204A 数据手册的时序图才反应过来。2.3 常用命令速查随机数、MAC、锁定区驱动里真正高频用到的命令其实就那么几个命令码名称典型用途0x1BRandom生成随机数用于挑战应答0x08MAC基于密钥和数据生成消息认证码0x28CheckMAC验证对方提供的 MAC 是否匹配0x17Lock锁定配置区或数据区之后不可写0x02Read读取配置区或数据区内容0x15GenKey生成内部密钥或公钥MAC 命令是防抄板的核心主机发一串挑战数据随机数芯片用内部存储的密钥做 HMAC-SHA256 运算回传 MAC 值。主机再把同样的数据发给服务端服务端用同一把密钥算一次两边 MAC 一致就说明芯片是正品。这套流程里芯片私钥永远不出芯片主机厂商只导出密钥的副本用于服务端校验。设备出厂时必须先执行 Lock 命令把数据区锁定否则攻击者可以 Read 命令直接把密钥读出来。锁定是不可逆操作配置区和数据区分开锁配置区锁了数据区还能写反过来不行。我的习惯是先用 Read 命令把配置内容读出来备份确认无误后再锁。3. 先验证硬件把 i2c-tools 和用户态脚本用起来3.1 检查 I2C 总线与设备地址拿到驱动源码前先确认硬件链路是否通的。这一步用 i2c-tools 就够了别急着编译驱动模块。首先扫描总线上的设备地址注意不能用普通的 i2cdetect 暴力扫描因为 ATSHA204A 的睡眠状态会干扰扫描结果。正确做法是先用 i2cset 写一个字节触发唤醒再执行探测。# 查看系统里有哪些 I2C 总线 ls /dev/i2c-* # 唤醒 ATSHA204A假设挂载在 i2c-0 上 i2cset -y 0 0x64 0x00 0x00 sleep 0.1 # 扫描总线看 0x64 是否出现 i2cdetect -y 0注意i2cset 的参数是 8 位地址 0x64i2cdetect 扫描结果里显示的也是 0x64但内核驱动里设备树填写的是 7 位地址 0x32这个差异经常让人白排查半天。扫描完成后如果 0x64 附近出现设备说明芯片能被唤醒I2C 地址和硬件连线都没问题。常见做法是这一步先跑通再进驱动层。如果扫描不到优先检查供电和上拉电阻ATSHA204A 的 VCC 要高于 2.0V否则芯片完全不上电。3.2 用 Python 脚本完成一次 Random 命令i2c-tools 只验证了 I2C 地址还没验证命令链路。我一般会写一个短小的 Python 脚本直接通过 /dev/i2c 设备发一条 Random 命令确认 CRC、状态字和响应全链路都是通的。这一步不需要任何额外的库用系统自带的 fcntl 和 struct 就能操作 ioctl。import fcntl import struct import time I2C_SLAVE 0x0703 DEVICE /dev/i2c-0 ADDR 0x64 # 8位I2C地址 # CRC16-CCITT初值0xFFFF def crc16(data): crc 0xFFFF for byte in data: crc ^ byte 8 for _ in range(8): if crc 0x8000: crc ((crc 1) ^ 0x1021) 0xFFFF else: crc (crc 1) 0xFFFF return crc # 唤醒SDA拉低40us以上。这里用低速i2c写一个字节模拟 # 实际产品中建议用GPIO控制WAKE引脚 fd open(DEVICE, rb, buffering0) fcntl.ioctl(fd, I2C_SLAVE, ADDR) # 发送唤醒 fd.write(b\x00) time.sleep(0.01) # 构建 Random 命令命令码0x1B参数1模式0x01表示SEED更新 # 参数20数据长度0无附加数据 cmd bytes([0x1B, 0x01, 0x00, 0x00]) crc crc16(cmd) pkt cmd bytes([crc 8, crc 0xFF]) # 写命令帧 fd.write(pkt) time.sleep(0.02) # 读状态字4字节确认0x77表示完成 status fd.read(4) print(status:, status.hex()) # 读响应帧第1字节是长度 resp_len fd.read(1)[0] resp fd.read(resp_len 3) # 数据 校验位 CRC2字节 print(response:, resp.hex()) print(random:, resp[0:resp_len].hex())这段脚本的逻辑拆开看crc16 函数实现了芯片要求的 CCITT 变体注意逐字节处理时每次都先异或高字节再迭代 8 次这是标准做法。构造命令帧时Random 命令的模式参数 0x01 表示更新种子寄存器后再产生随机数如果只要随机数不更新种子可以传 0x00。写命令帧后要等芯片内部处理20ms 已经足够不放心的可以用 50ms 更稳妥。读响应时会发现一个细节先读了 4 字节状态字再读长度、数据、CRC。这是因为芯片在收到命令后会在总线上先回状态字状态字确认无误后再回响应帧。如果只发一次读操作可能只拿到部分数据。这个行为在数据手册里画得很清楚实机调试时也要按这个顺序来。3.3 参数说明地址换算、延时与重试脚本里最值得注意的参数有三个。地址换算i2cset 和 Python 脚本里用的是 8 位地址 0x64设备树里是 7 位地址 0x32两者是移位关系别混用。延时设置唤醒后的 10ms 延时覆盖了芯片手册要求的 1.52ms 上电时间留了余量命令后的 20ms 是芯片处理典型耗时MAC 命令这类需要内部计算的操作建议改到 100ms 更稳。重试策略一旦状态字是 0x88 或 0x99不要直接重发同样命令芯片可能处于异常状态要先发 Sleep 命令让芯片睡回去再唤醒重来。这个阶段的目的就是把「裸 I2C 通信」跑通证明芯片硬件没毛病。接下来才进驱动源码因为驱动里封装的正是这些操作的框架化版本。4. Linux 内核驱动源码结构从 platform_driver 到 procfs4.1 驱动框架与设备树配置ATSHA204A 在 Linux 内核里有一套比较完整的驱动参考实现源码在drivers/misc/atsha204-i2c.c配套文档在Documentation/misc-devices/atsha204.rst。这套驱动的设计思路值得学习它把密码学操作封装在 ioctl 接口里用户态程序不直接触碰 I2C 字节流。设备树配置如下示例i2c0 { status okay; atsha204a: atsha204a32 { compatible microchip,atsha204a; reg 0x32; /* 7位I2C地址 */ wake-gpios gpio4 21 GPIO_ACTIVE_LOW; clock-frequency 400000; }; };compatible 字符串必须与驱动里 of_match_table 声明一致内核才能正确匹配。reg 填的是 7 位地址 0x32这点前面反复强调了。wake-gpios 是可选的如果硬件上把 WAKE 引脚接到了 GPIO驱动可以直接操作它来唤醒芯片比 SDA 拉低的方式更可靠。clock-frequency 建议先按 400kHz 配跑稳了再往上调。驱动加载后会在 /sys/class/misc/ 下创建一个名为 atsha204a 的设备节点用户态通过 open、ioctl、read、write 访问它。这种设计的好处是上层应用不需要知道 I2C 细节也方便在驱动层统一做并发控制和重试。4.2 核心函数open、ioctl 与命令封装驱动内部的核心逻辑是几个静态函数atsha204_wake负责唤醒时序atsha204_sleep负责使芯片回到低功耗状态atsha204_send和atsha204_receive封装了命令帧和响应帧的收发。ioctl 接口按命令类型分发每个命令对应一个 case。static long atsha204_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { struct atsha204_command *user_cmd (struct atsha204_command *)arg; struct atsha204 *chip file-private_data; u8 buf[64]; int ret; switch (cmd) { case ATSHA204_CMD_RANDOM: ret atsha204_wait_wake(chip); /* 唤醒并等待就绪 */ if (ret) return ret; ret atsha204_random(chip, buf, sizeof(buf)); /* 生成随机数 */ if (ret) return ret; if (copy_to_user(user_cmd-data, buf, 32)) return -EFAULT; return 0; case ATSHA204_CMD_MAC: /* 从用户态拿挑战数据调用芯片计算MAC */ ret atsha204_wait_wake(chip); if (ret) return ret; ret atsha204_mac(chip, user_cmd-param, user_cmd-param_len, buf); if (ret) return ret; if (copy_to_user(user_cmd-data, buf, 32)) return -EFAULT; return 0; } return -ENOTTY; }这段代码的逻辑是用户态发来一个命令结构体内核驱动先调用 atsha204_wait_wake 确保芯片处于唤醒状态然后执行具体的密码学操作最后通过 copy_to_user 把结果返回到用户空间。atsha204_wait_wake 内部会先检查芯片是否已经在唤醒状态如果判断芯片可能睡着了就发唤醒时序再延时等待。这个检查很关键因为驱动可能在长时间空闲后收到用户态请求此时芯片已经自动进入睡眠。参数说明user_cmd 结构体里 param 和 param_len 是挑战数据比如 MAC 命令需要 32 字节随机数作为输入data 字段是输出缓冲区Random 命令产出 32 字节随机数MAC 命令产出 32 字节 MAC 值。所有指针都来自用户态必须用 copy_to_user 而不是直接 memcpy否则会触发内核地址校验错误。4.3 把驱动编进内核或做成模块两种编译路径驱动源码拿到手后编译方式有两种。第一种是直接编进内核镜像适合产品固件确定后不再改的场景第二种是编译成 .ko 模块调试阶段用这个改代码不用整个内核重编。# 方式一编进内核 cd linux-kernel make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- menuconfig # 进入 Device Drivers → Misc devices → 勾选 ATSHA204A support # 方式二编成模块 make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- Mdrivers/misc/ modules # 生成 drivers/misc/atsha204-i2c.ko模块方式调试时加载后先看 dmesg 确认驱动匹配到了设备树节点insmod atsha204-i2c.ko dmesg | tail -20 # 应该能看到类似 atsha204a 0-0032: ATSHA204 detected 的输出如果 dmesg 里没有匹配信息优先查设备树 compatible 字符串是否一致。另外注意模块的依赖如果内核开启了 module 签名机制还要处理签名问题。我遇到过一次 insmod 报Exec format error折腾半天发现是交叉编译工具链的内核头文件版本和当前内核不匹配重编内核头文件后解决。4.4 用户态测试程序ioctl 命令的封装与验证驱动层只是搬运工真正验证芯片功能要写用户态程序。常见做法是定义一组和驱动 ioctl 编号对应的宏然后封装成易用的函数库。下面是一个简化版的随机数测试程序#include stdio.h #include fcntl.h #include sys/ioctl.h #include unistd.h #define ATSHA204_CMD_RANDOM 0xC0045A01 #define ATSHA204_CMD_MAC 0xC0085A02 struct atsha204_command { unsigned char param[32]; int param_len; unsigned char data[32]; }; int main(void) { int fd open(/dev/atsha204a, O_RDWR); struct atsha204_command cmd; int ret; if (fd 0) { perror(open); return 1; } memset(cmd, 0, sizeof(cmd)); ret ioctl(fd, ATSHA204_CMD_RANDOM, cmd); if (ret 0) { perror(ioctl random); close(fd); return 1; } printf(random data: ); for (int i 0; i 32; i) printf(%02x, cmd.data[i]); printf(\n); close(fd); return 0; }做 MAC 验证时把预置的挑战数据填入 cmd.param然后 ioctl 调 ATSHA204_CMD_MAC输出的 32 字节 MAC 值和服务端计算结果对比。这里注意一个安全习惯别把密钥本身传给驱动驱动和芯片只接收挑战数据密钥永远留在芯片内部。看到有人把密钥当成参数传进 MAC 命令那说明对芯片的理解有偏差。5. 常见问题排查地址冲突、CRC 错误、复位与锁定5.1 现象i2cdetect 扫描不到 0x64 地址有时候 i2cdetect -y 0 扫完0x64 位置是空的但芯片明明焊在板子上。原因通常是芯片处于睡眠状态普通扫描时序不会唤醒它。解决方法是先发一个带数据的写操作强制唤醒再扫描或者直接用 GPIO 控制 WAKE 引脚拉低 60μs 以上再释放。还有一种情况是地址冲突总线上如果有另一颗设备地址也是 0x64两个芯片的 ACK 信号会互相干扰表现为扫描结果时有时无。排查时先断开其他 I2C 设备只剩 ATSHA204A 再试。5.2 现象能读到 ACK 但命令返回 0xFF唤醒成功、地址也正确但状态字读回来是 0xFF。这个值不是芯片定义的状态码通常是读取时机不对。ATSHA204A 在睡眠状态下不受 I2C 时钟如果命令写太快芯片还没来得及从睡眠中恢复总线上的电平全部是释放状态高电平读到的就是 0xFF。解决方法是拉长唤醒后的等待时间至少 2ms并确保命令帧发送完成后延时足够再读状态字。我一般会在写命令后固定 sleep 20ms宁可慢也不翻车。5.3 现象CRC 校验老是不通过状态字始终 0x88状态字 0x88 明确指示 CRC 错误但数据内容明明是照着手册算的。最常见的原因是把 CRC 的字节序搞反了。芯片命令帧里的 CRC 是高字节在前、低字节在后如果按低字节在前发送芯片必然报错。另一个容易踩的点是 CRC 的输入范围只看命令头 4 字节 数据区不要把长度字节自己加进去。写驱动时建议加一个自检函数把已知的测试向量丢进去验证 CRC 实现是否正确。5.4 现象上电第一次通信必失败要重启才能好硬件上电后第一次访问芯片总超时但只要重启应用或重新打开设备文件就好了。这通常是唤醒时序不完整导致的。芯片上电后默认进入睡眠如果驱动初始化时没有先执行完整的唤醒序列而是直接发命令芯片不会响应。检查驱动 probe 函数里是否调用了 atsha204_wake以及立刻跟踪 dmesg 里有没有对应的状态打印。还有一种情况是上电时 SDA 电平不稳芯片检测到意外的 I2C 起始条件进入错误状态。解法是在驱动初始化前给芯片一次完整的「电力重置」拉低 WAKE 引脚 10ms 再释放等 2ms 后再操作。5.5 现象GPIO 复用成 I2C 后系统卡死把某组 GPIO 配置为 I2C 功能后系统启动到一半就挂起或内核报 i2c 超时错误。这多半是引脚复用配置不对比如同一个引脚被两个设备占用或者 bank 的时钟没有使能。ATSHA204A 的 SDA/SCL 必须接到支持开漏输出的引脚内部上拉使能后还要外接 4.7kΩ 上拉电阻到 VCC。检查设备树里 pinctrl 节点确认引脚组的 mux 功能是否包含了 i2c 模式。我遇到过在设备树里把同一个 pin 同时配置成 GPIO 和 I2C导致内核死锁去掉冲突配置后正常。5.6 现象芯片被锁定后误写导致永久损坏Lock 命令执行后对应的区域永远不能再写。如果调试阶段不小心提前执行了 Lock再尝试 Write 命令会返回 0xEE 状态字且不可逆。这个不是驱动 bug是芯片的安全设计。避免的办法开发调试阶段不要执行 Lock 命令等量产固件里再启用如果非要测试 Lock 流程换一颗新芯片专门做测试。文档里如果包含 Lock 自动执行的逻辑建议做成单独工具不要混进产品主流程。6. 把驱动做成产品级多设备并发、重试策略与安全边界6.1 多芯片管理与并发访问一个板子上有时会挂多颗 ATSHA204A比如一个主加密芯片加一个备份芯片。每颗芯片有独立 I2C 地址设备树里分别声明节点驱动实例也会各自创建。但同一总线上并发访问时要注意驱动里的互斥锁是否覆盖了整个命令收发过程。如果用户在两个线程里同时打开设备节点发命令底层 I2C 传输交错芯片收到的命令帧会被拆散。解法是驱动内部用 mutex 串行化所有命令或者用户态自己加锁。我看过一套源码就是在 atsha204_ioctl 入口处加 mutex_lock整个命令过程独占总线代价是多颗芯片并发时会互相等待但安全性优先。6.2 重试与看门狗把通信失败率降到可接受范围I2C 通信受干扰偶尔会失败产品级的驱动必须要有重试策略。常见做法是三层重试唤醒阶段重试、命令发送阶段重试、状态字不合法时复位重试。我习惯把单条命令的重试上限设为 3 次超过就返回设备忙错误给用户态。注意重试前要先让芯片进入睡眠状态再唤醒否则芯片内部状态机可能还停留在上一次未完成的命令上直接重发会叠加错乱。static int atsha204_reset_and_retry(struct atsha204 *chip) { int ret 0; int retries 0; /* 强制进入睡眠清掉内部残留状态 */ ret atsha204_sleep(chip); if (ret) { dev_err(chip-dev, sleep failed during retry\n); return ret; } /* 重新唤醒等待芯片就绪 */ for (retries 0; retries 3; retries) { ret atsha204_wait_wake(chip); if (ret 0) break; msleep(50); /* 等待芯片上电稳定 */ } return ret; }这段代码的逻辑是 sleep 清状态再 wake 重来。sleep 失败说明芯片可能已经不在总线上了那就不必继续重试。唤醒循环里等待 50ms比正常唤醒的 2ms 长很多是为了覆盖芯片上电慢的场景。实际项目里应该在每次重试后打印内核日志方便现场排查干扰源。6.3 安全边界密钥不出芯片别把密文留到日志ATSHA204A 的核心价值是密钥物理隔离。驱动代码里严禁把密钥明文、甚至密钥的校验值打印到 dmesg。调试时可以把随机数和 MAC 值打出来比对但任何和密钥相关的中间量都不要输出。另外要注意 ioctl 接口的缓冲区长度校验用户态传入的 param_len 如果大于芯片支持的最大长度驱动要提前拦截防止内核栈溢出。用户态层面产品里不要让上层应用拿到裸的密钥或者能反推密钥的调试接口。6.4 验证建议逻辑分析仪抓波形与 MAC 回环测试驱动写完别急着量产先做一轮实机验证。接逻辑分析仪抓 SDA/SCL 波形确认唤醒低电平持续时间和命令帧的字节顺序。更直接的验证是回环测试用同一颗芯片执行两次 MAC 命令输入相同挑战数据两次结果必须一致再用另一个已知正确的服务端实现做交叉验证。我每做完一版驱动都会做这个回环测试基本能挡住大部分通信和 CRC 问题。6.5 一处值得保留的习惯把驱动源码做成可配置模块从那以后我每次接手这类加密芯片驱动都会强制走一遍同样的流程先裸 I2C 验证硬件再跑用户态脚本最后才碰内核驱动代码。驱动里所有延时、重试次数、I2C 地址都做成宏或设备树属性不去硬编码。这样做的好处是换板子、换总线时不用改代码只改设备树就能适配。如果你拿到的驱动源码里某个参数写死了先改成可配置再往下走能省掉后面大量排错时间。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑