2. SPI驱动框架设计:Linux SPI子系统架构、SPI设备驱动模型、SPI消息队列与传输机制、驱动注册与注销流程

好,咱们进入第二章。这一章是 SPI 驱动开发的骨架,说白了就是搞清楚 Linux 内核到底怎么管 SPI 的。你如果只是照着别人的代码改改 pin 脚,那永远只能当个“调参侠”。想真正把安全元件(SE)的驱动写稳、写透,必须把 SPI 子系统的脉络摸清楚。

我早年刚接触内核驱动时,也犯过傻——以为 SPI 驱动就是填几个结构体,注册一下就完事了。结果有一次在项目联调时,安全元件死活不回数据,折腾了两天。最后发现是消息队列的传输顺序没搞对,数据包被拆成了两半,SE 直接丢弃了。嗯,从那以后,我再也不敢轻视 SPI 框架的设计了。

2.1 Linux SPI 子系统架构

先看整体。Linux SPI 子系统分三层,像三明治:

  • SPI 核心层:内核帮你写好的通用逻辑,管理总线、设备和驱动。
  • SPI 控制器驱动层:跟硬件寄存器打交道,比如你的 MCU 上的 SPI 外设。
  • SPI 设备驱动层:我们写安全元件驱动,主要就在这一层。

这三层之间通过标准接口通信。核心层提供 API,控制器驱动负责收发,设备驱动只管“我要发什么、收什么”。

关键点:设备驱动不直接操作寄存器。你只需要填充 spi_message 和 spi_transfer,剩下的交给下层。这是分层设计的好处——解耦。

下面这张图,我建议你多看两眼。它把 SPI 子系统的数据流和模块关系画清楚了。

Linux SPI 子系统架构图 Linux SPI 子系统架构 用户空间 (User Space) SPI 设备驱动 (SPI Device Driver) SPI 核心层 (SPI Core) SPI 控制器驱动 (SPI Controller Driver) 硬件 (SPI Controller + SE) 我们写驱动 主要在这里 芯片原厂提供 一般不改

2.2 SPI 设备驱动模型

内核里,SPI 设备驱动模型围绕三个核心结构体展开:

结构体 作用 谁负责填充
struct spi_device 描述一个 SPI 从设备(比如你的安全元件) 板级代码或设备树
struct spi_driver 描述驱动本身,包含 probe/remove 回调 驱动开发者(你)
struct spi_controller 描述 SPI 主控制器硬件 芯片厂商

我个人习惯,写安全元件驱动时,先关注 spi_driver。它的核心是 probe 函数——驱动和设备匹配成功后,内核会调用它。你在 probe 里做初始化、分配内存、注册中断、创建子设备(比如字符设备或杂项设备)。

小技巧:设备树里配 spi-max-frequency 时,别只看 SE 数据手册上的理论值。我遇到过 SE 在 20MHz 下通信不稳定,降到 10MHz 就一切正常。留点余量,别把频率卡太死。

2.3 SPI 消息队列与传输机制

这是 SPI 驱动最核心的部分,也是坑最多的地方。

SPI 传输不是直接调 read/write,而是通过 消息队列。你构造一个 spi_message,里面挂一个或多个 spi_transfer,然后提交给核心层。核心层会把它排到控制器驱动的队列里,按顺序执行。

为什么用消息队列?因为 SPI 是同步协议,但内核里可能有多个任务同时想访问同一个 SPI 设备。队列保证了原子性和顺序。

看一个典型的安全元件读写流程:

// 构造一个传输
struct spi_transfer t = {
    .tx_buf = tx_buffer,   // 要发送的数据
    .rx_buf = rx_buffer,   // 接收缓冲区
    .len    = 32,          // 传输长度
    .cs_change = 0,        // 传输完成后是否拉高片选
};

// 构造消息
struct spi_message msg;
spi_message_init(&msg);
spi_message_add_tail(&t, &msg);

// 同步传输
int ret = spi_sync(spi_device, &msg);
if (ret < 0) {
    dev_err(&spi_device->dev, "SPI transfer failed: %d\n", ret);
    return ret;
}

这里有个细节:cs_change。安全元件通常要求一次完整的 APDU 命令在片选持续拉低期间完成。如果你把一条命令拆成多个 transfer,且中间 cs_change 置了 1,片选会拉高再拉低——SE 会认为通信结束,丢掉之前的数据。

我曾经在调试一个 TPM 安全芯片时,就因为 cs_change 没处理好,导致每次发完命令头后片选被拉高,SE 直接复位了状态机。查了整整一天。记住:安全元件通信,片选控制要格外小心。

2.4 驱动注册与注销流程

流程其实不复杂,但顺序错了就会出问题。我把它总结成四步:

  1. 定义 spi_driver 结构体:填充 probe、remove、id_table 等字段。
  2. 注册驱动:调用 spi_register_driver(),一般在模块入口函数里做。
  3. 匹配设备:内核根据设备树或板级信息,自动匹配你的驱动,调用 probe。
  4. 注销驱动:调用 spi_unregister_driver(),一般在模块出口函数里做。

代码骨架长这样:

static int se_probe(struct spi_device *spi)
{
    // 1. 设置 SPI 模式、频率
    spi->mode = SPI_MODE_0;
    spi->max_speed_hz = 10 * 1000 * 1000; // 10MHz
    spi_setup(spi);

    // 2. 分配私有数据结构
    struct se_dev *dev = devm_kzalloc(&spi->dev, sizeof(*dev), GFP_KERNEL);
    if (!dev)
        return -ENOMEM;

    dev->spi = spi;
    spi_set_drvdata(spi, dev);

    // 3. 注册字符设备或 misc 设备
    // ... (后续章节展开)

    return 0;
}

static int se_remove(struct spi_device *spi)
{
    // 清理资源,注销设备
    return 0;
}

static const struct of_device_id se_of_match[] = {
    { .compatible = "vendor,secure-element" },
    { }
};
MODULE_DEVICE_TABLE(of, se_of_match);

static struct spi_driver se_spi_driver = {
    .driver = {
        .name = "secure_element",
        .of_match_table = se_of_match,
    },
    .probe  = se_probe,
    .remove = se_remove,
};

module_spi_driver(se_spi_driver);

注意 module_spi_driver 这个宏,它帮你自动展开 module_init 和 module_exit,省去手动调用注册/注销的麻烦。我建议新手直接用这个宏,少写代码少犯错。

避坑指南:probe 函数里如果失败了,一定要返回负的错误码。返回 0 但设备没初始化好,内核会认为驱动已就绪,后续操作直接崩溃。我见过有人 probe 里分配内存失败后返回 0,结果 open 设备时访问空指针——内核 panic。

嗯,这一章的内容就这些。SPI 驱动框架是基础,后面所有安全元件的通信逻辑都建立在这上面。你先把消息队列和注册流程吃透,后面写 APDU 传输、中断处理、电源管理时,才能游刃有余。