4、TPM固件栈与驱动:TPM TIS接口、TPM命令协议栈、Linux内核TPM驱动框架

这一章我们聊聊TPM的软件栈。说白了,就是操作系统怎么跟TPM芯片打交道。

很多人以为TPM就是个黑盒子,发个命令进去,拿个结果出来。其实没那么简单。中间隔了好几层——硬件接口、传输协议、驱动框架、上层API。每一层都有坑。

我个人习惯,先把整个栈的层次画清楚,再逐层深入。这样出了问题,你能快速定位是硬件没响应,还是驱动没加载,还是命令格式错了。

核心观点: TPM固件栈的核心是三层——TIS接口(物理层)、TPM命令协议(传输层)、内核驱动框架(管理层)。三层各司其职,缺一不可。
TPM 固件栈三层架构 TPM 固件栈三层架构 用户空间 应用程序 / TSS (Trousers / tpm2-tss) / 安全服务 内核空间 Linux TPM 驱动框架 (drivers/char/tpm/) 硬件层 TPM 芯片 / TIS 接口 / SPI / LPC 总线 驱动框架 TIS 接口 💡 每一层都有独立的错误处理和超时机制,不能跨层假设 例如:驱动层不能假设 TIS 接口一定在 2ms 内响应

4.1 TIS接口——TPM的硬件门面

TIS,全称TPM Interface Specification。它定义了CPU怎么通过内存映射I/O去访问TPM寄存器。

说白了,TPM芯片在系统地址空间里占了一块区域。你往特定地址写数据,就是发命令;从特定地址读数据,就是收结果。

我记得第一次调TIS接口时,死活读不到状态寄存器的值。后来发现是ACPI表里没把TPM的MMIO区域正确暴露给操作系统。嗯,这种坑很常见。

TIS接口的核心寄存器

TIS规范定义了一组固定偏移的寄存器。我列几个关键的:

偏移地址 寄存器名称 作用
0x00 ACCESS 访问控制,判断TPM是否忙、是否可用
0x04 INT_ENABLE 中断使能,控制TPM是否触发中断
0x08 INT_VECTOR 中断向量号,告诉CPU中断线编号
0x0C INT_STATUS 中断状态,读清零,写置位
0x10 INTF_CAPABILITY 接口能力,支持FIFO还是CRB模式
0x18 STS 状态寄存器,核心!判断命令执行状态
0x24 DATA_FIFO 数据FIFO,读写命令和响应的地方
0x30 INTERFACE_ID 接口ID,标识TPM厂商和版本
我的经验: 调试TIS接口时,先读ACCESS寄存器确认TPM是否处于可用状态。如果ACCESS的activeLoc位没置起来,后面所有操作都是白费。我曾经在一个国产主板上折腾了两天,最后发现是BIOS没给TPM分配MMIO资源。

TIS的两种传输模式

TIS支持两种模式:FIFO和CRB。FIFO是传统模式,CRB是Command Response Buffer模式,性能更好。

  • FIFO模式: 数据通过DATA_FIFO寄存器逐字节或逐字传输。需要轮询STS寄存器的状态位来判断是否可写/可读。我建议新手先调通FIFO模式,逻辑简单,容易debug。
  • CRB模式: 使用一块连续的缓冲区,命令和响应都放在里面。通过控制寄存器触发传输。效率高,但实现复杂一点。

你想想看,如果TPM芯片同时支持两种模式,驱动怎么选?嗯,先读INTF_CAPABILITY寄存器,看bit0和bit1。bit0置位表示支持FIFO,bit1置位表示支持CRB。优先选CRB,不行再回退到FIFO。

4.2 TPM命令协议栈——数据怎么封装

TIS接口只负责传输原始字节。这些字节怎么组织成有意义的命令,是TPM命令协议栈的事。

TPM命令的格式其实很固定。我直接给你看结构:

// TPM命令头部结构(固定长度6字节)
struct tpm_cmd_header {
    __be16  tag;        // 标签,标识命令类型
    __be32  length;     // 整个命令的长度(包括头部)
    __be32  ordinal;    // 命令码,比如TPM_ORD_GETRANDOM
};

// 命令码示例
#define TPM_ORD_GETRANDOM    0x14
#define TPM_ORD_EXTEND       0x14  // 注意:不同版本可能重复
#define TPM2_CC_GETRANDOM    0x17B
#define TPM2_CC_EXTEND       0x132

这里有个容易混淆的地方:TPM 1.2和TPM 2.0的命令码完全不同。TPM 1.2用16位ordinal,TPM 2.0用32位command code。我见过有人把TPM 2.0的命令码发给TPM 1.2芯片,结果返回TPM_BADTAG。

命令执行流程

一个完整的TPM命令执行,分五步:

  1. 准备命令缓冲区: 填充tag、length、ordinal,后面跟参数。长度要算准,多一个字节或少一个字节都会导致TPM拒绝执行。
  2. 获取TPM访问权: 通过ACCESS寄存器的requestUse位请求访问。如果TPM正被别的软件占用(比如BIOS),会返回忙。
  3. 写入命令: 把命令缓冲区写入DATA_FIFO。每次写入前检查STS的tpmGo位是否清零,确保TPM空闲。
  4. 通知TPM执行: 写STS寄存器的tpmGo位为1,告诉TPM“命令写完了,开始干活”。
  5. 等待并读取响应: 轮询STS的dataAvail位,或者等中断。数据就绪后从DATA_FIFO读响应。
注意: 步骤4和5之间,TPM可能执行很长时间。比如生成RSA密钥对,可能要几十秒。驱动必须实现超时机制,不能死等。我曾经遇到过TPM卡死的情况,超时后复位TIS接口才恢复。

4.3 Linux内核TPM驱动框架

Linux内核的TPM驱动在 drivers/char/tpm/ 目录下。这个框架设计得挺巧妙,把硬件差异和上层协议分开了。

我直接说核心结构:

驱动框架的层次

  • tpm-core: 核心层,处理TPM命令的发送和接收,管理命令流水号,维护TPM状态。这部分跟具体硬件无关。
  • tpm-interface: 接口层,封装了TPM 1.2和TPM 2.0的命令差异。比如tpm_get_random(),内部会判断是1.2还是2.0,然后构造不同的命令。
  • tpm-dev: 字符设备层,提供/dev/tpm0这样的用户空间接口。应用程序可以通过read/write直接跟TPM交互。
  • tpm_tis: TIS接口驱动,负责初始化MMIO区域、注册中断、实现FIFO/CRB传输。这是跟硬件最近的一层。
  • tpm_crb: CRB模式驱动,用于支持CRB接口的TPM芯片。

我建议你看源码时,先看 tpm_tis_core.c 和 tpm_tis.c。前者是TIS核心逻辑,后者是具体平台适配。这两个文件加起来不到2000行,读起来不费劲。

关键数据结构

// TPM芯片的私有数据结构
struct tpm_chip {
    struct device dev;          // 设备模型
    int dev_num;                // 设备编号,对应/dev/tpm%d
    unsigned long timeout_a;    // 超时时间A
    unsigned long timeout_b;    // 超时时间B
    unsigned long timeout_c;    // 超时时间C
    unsigned long timeout_d;    // 超时时间D
    bool timeout_adjusted;      // 超时是否已调整
    struct tpm_class_ops *ops;  // 硬件操作函数指针
    // ... 省略其他字段
};

// 硬件操作函数集
struct tpm_class_ops {
    int (*send)(struct tpm_chip *chip, u8 *buf, size_t len);
    int (*recv)(struct tpm_chip *chip, u8 *buf, size_t len);
    void (*cancel)(struct tpm_chip *chip);
    u8 (*status)(struct tpm_chip *chip);
    bool (*req_complete)(struct tpm_chip *chip, u8 status);
    int (*go_idle)(struct tpm_chip *chip);
    int (*cmd_ready)(struct tpm_chip *chip);
};

你看这个 tpm_class_ops,其实就是硬件抽象层。不同的TPM芯片(比如Infineon SLB9670、Nuvoton NPCT75x)只要实现这几个函数,就能接入Linux TPM框架。我做过一个项目,需要把一款老旧的TPM 1.1芯片适配到Linux 5.10内核,就是靠实现这组ops搞定的。

驱动加载流程

Linux TPM驱动的加载顺序是这样的:

  1. ACPI表或设备树中声明TPM设备,匹配到tpm_tis或tpm_crb驱动。
  2. 驱动probe函数被调用,做三件事:请求MMIO内存区域、映射寄存器、注册中断。
  3. 读取TPM芯片的did_vid寄存器,确认厂商和型号。
  4. 发送TPM_ORD_GET_CAPABILITY命令,获取TPM版本(1.2还是2.0)和超时参数。
  5. 注册字符设备,创建/dev/tpm0节点。
  6. 启动内核线程,执行TPM自检(TPM_ORD_SELF_TEST)。
调试技巧: 如果TPM驱动加载失败,先看dmesg。常见的错误有:
- "TPM: cannot read TPM PCID":MMIO映射失败,检查ACPI表
- "TPM: tpm_tis: 0x0 is busy":TPM被其他软件占用,比如BIOS的TPM驱动
- "TPM: tpm_tis: timeout":TPM没有响应,可能是硬件故障或中断没配好

用户空间交互

驱动加载成功后,用户空间可以通过 /dev/tpm0 直接发命令。但我不建议你这么做。原因很简单:TPM命令的格式太底层了,容易出错。

更好的方式是使用TSS(TPM Software Stack)。Linux上有两个主流实现:

  • Trousers: 老牌TSS,支持TPM 1.2。现在基本没人用了。
  • tpm2-tss: 新一代TSS,支持TPM 2.0。SAPI、ESAPI、FAPI三层API,从底层到高层都有。

我个人推荐直接用tpm2-tss。它封装了命令构造、参数编解码、会话管理等复杂逻辑。你调用tpm2_getrandom(),它内部会构造TPM2_CC_GetRandom命令,通过/dev/tpm0发送,解析响应,最后返回随机数。省心很多。

一句话总结: TIS接口是TPM的硬件门面,负责字节传输;TPM命令协议栈定义数据格式;Linux内核驱动框架把这两层串起来,并提供统一的用户空间接口。三层协同,才能让TPM正常工作。