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 子系统的数据流和模块关系画清楚了。
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 驱动注册与注销流程
流程其实不复杂,但顺序错了就会出问题。我把它总结成四步:
- 定义 spi_driver 结构体:填充 probe、remove、id_table 等字段。
- 注册驱动:调用
spi_register_driver(),一般在模块入口函数里做。 - 匹配设备:内核根据设备树或板级信息,自动匹配你的驱动,调用 probe。
- 注销驱动:调用
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 传输、中断处理、电源管理时,才能游刃有余。