本节摘要:把 4.1 的通用总线模型套到 I2C 与 SPI 两条真实总线上。本节以一个 I2C 温度传感器驱动为主案例,走完 i2c 驱动注册、probe 取信息、总线传输读温度的完整闭环,再对照 SPI 驱动的差异,最后用一张表总结三类总线驱动的同与异。
4.2 节的设备树里,传感器节点 sensor@48 挂在 I2C 控制器节点之下——树只是声明了"总线上有个从地址 0x48 的设备",谁来操作它?答案分两层:I2C 控制器驱动管"怎么在总线上跑报文"(通常由芯片厂商提供),I2C 设备驱动管"跟这台设备说什么话"(本节要写的)。两层之间隔着内核 I2C 核心层,它把控制器抽象成传输接口——设备驱动只管发消息,不管电气与时序。
I2C 驱动的结构与平台驱动如出一辙,匹配表、probe、注册宏各就各位,只是类型换了名字:
#include <linux/i2c.h> static int tmp_probe(struct i2c_client *client) { int ret; u8 id; /* 探测设备身份:读芯片识别寄存器确认设备真实存在 */ ret = i2c_smbus_read_byte_data(client, 0xE0); if (ret < 0) return ret; id = (u8)ret; if (id != 0x54) return -ENODEV; /* 地址对上了但不是这颗芯片 */ pr_info("tmp-sensor: 芯片识别码 0x%02x,探测通过\n", id); return 0; } static void tmp_remove(struct i2c_client *client) { /* 托管资源自动回收 */ } static const struct of_device_id tmp_of_match[] = { { .compatible = "vendor,tmp-sensor" }, { } }; static struct i2c_driver tmp_driver = { .probe = tmp_probe, .remove = tmp_remove, .driver = { .name = "tmp-sensor", .of_match_table = tmp_of_match, }, }; module_i2c_driver(tmp_driver);
对比平台驱动,差异集中在三处。入口参数换了类型:probe 收到的是 i2c_client——内核替你把树里 sensor@48 节点解析成了设备对象,从地址、所在控制器、中断全在里面,不用再查资源。probe 里可以做真实探测:总线上能实际对话,读个识别寄存器验证"地址后面真的挂着这颗芯片"——这比平台设备的"盲信树"更进一步,总线给了你验货的机会。注册宏换成 module_i2c_driver:匹配与生命周期管理由 I2C 核心层照办。
💡 关键直觉:probe 里验货是总线设备驱动的黄金习惯。设备树的声明可能与现实不符(焊反了、没焊、型号不同),probe 是把"纸面硬件"变成"确认硬件"的最后一道关卡。
I2C 核心层给设备驱动两套传输接口,抽象层级不同。
系统管理总线风格接口适合单字节小操作——读寄存器、写配置,一函数一事务:
/* 读温度寄存器:高字节低字节两次读取 */ int mag_raw = i2c_smbus_read_word_swapped(client, 0x00); /* 写配置:关闭传感器自检位 */ i2c_smbus_write_byte_data(client, 0x01, 0x80);
消息接口适合一次跑多段事务(3.4 节那组"写寄存器号、重启、读数据"的组合拳):
static int tmp_read_temp(struct i2c_client *client, int *mdegc) { u8 reg = 0x00; /* 温度寄存器编号 */ u8 raw[2]; int ret; struct i2c_msg msgs[2] = { { .addr = client->addr, .len = 1, .buf = ®, /* 先写寄存器号 */ }, { .addr = client->addr, .len = 2, .buf = raw, .flags = I2C_M_RD }, /* 再读两字节 */ }; ret = i2c_transfer(client->adapter, msgs, 2); if (ret != 2) return ret < 0 ? ret : -EIO; /* 高位在前:拼成原始值再换算为毫摄氏度 */ *mdegc = ((raw[0] << 8) | raw[1]) * 5 / 8; return 0; }
消息结构里的 addr 与 flags 把 3.4 节逻辑分析仪上的那套"地址加方向"协议原封不动搬进了 C 结构——传输接口就是总线协议的软件镜像。i2c_transfer 返回完成的报文数,不等于请求数即算失败,这个判定不能省。
SPI 驱动的骨架与 I2C 完全同构:spi_driver 挂匹配表、probe 收 spi_device、module_spi_driver 完成注册。差异在传输模型——SPI 是全双工移位,消息接口把"片选周期内的一串双向字节"打包成一次传输:
#include <linux/spi/spi.h> static int flash_read_id(struct spi_device *spi, u8 *id) { u8 tx[4] = { 0x9f, 0, 0, 0 }; /* 读识别命令加三个填充字节 */ u8 rx[4] = { 0 }; int ret; struct spi_transfer xfer = { .tx_buf = tx, .rx_buf = rx, .len = 4, }; struct spi_message msg; spi_message_init(&msg); /* 组装消息 */ spi_message_add_tail(&xfer, &msg); ret = spi_sync(spi, &msg); /* 同步执行:发四字节收四字节 */ if (ret) return ret; id[0] = rx[1]; id[1] = rx[2]; id[2] = rx[3]; /* 跳过命令回波 */ return 0; }
注意 tx_buf 与 rx_buf 同时给出——这正是 3.4 节说的"移位寄存器对射":主机发的四个字节与收到的四个字节在时间上重叠,命令字节发出去的同时收回的是填充,真正的识别码在后面三个字节。此外 SPI 设备的速率、模式(时钟极性与相位)在树节点里声明,probe 时内核已按声明配置好控制器,驱动不必再碰时序参数。
至此,三类总线驱动的骨架都见过了,同异一表收拢:
| 维度 | 平台驱动 | I2C 设备驱动 | SPI 设备驱动 |
|---|---|---|---|
| 设备对象来源 | 树节点展开 | 树里总线子节点加从地址 | 树里总线子节点加片选 |
| probe 参数 | 平台设备 | i2c_client | spi_device |
| 硬件信息获取 | 资源接口按索引取 | client 内含地址与适配器 | device 内含速率模式片选 |
| 寄存器访问 | 映射后直接读写 | 总线报文(消息或字节接口) | 总线报文(消息接口) |
| 探测验证能力 | 无总线可对话,信树 | 可读识别寄存器验货 | 可读识别命令验货 |
| 传输并发控制 | 不涉及 | 核心层串行化 | 核心层串行化 |
骨架同构、传输各异——这正是总线模型的设计意图:驱动的结构问题解决一次,处处复用;总线的个性问题各管各的。第 5 章要见到的块设备与网络设备框架虽然复杂得多,但"框架做掉通用、驱动填差异"的思想一脉相承。
主线 LED 已成"正规军",通用方法论也集齐了。下一章走出字符设备的世界,看块设备与网络设备如何用完全不同的框架解决"数据吞吐"这道题。