4.3 I2C 与 SPI 驱动:总线模型下的 probe


4.3 I2C 与 SPI 驱动:总线模型下的 probe

本节摘要:把 4.1 的通用总线模型套到 I2C 与 SPI 两条真实总线上。本节以一个 I2C 温度传感器驱动为主案例,走完 i2c 驱动注册、probe 取信息、总线传输读温度的完整闭环,再对照 SPI 驱动的差异,最后用一张表总结三类总线驱动的同与异。

4.2 节的设备树里,传感器节点 sensor@48 挂在 I2C 控制器节点之下——树只是声明了"总线上有个从地址 0x48 的设备",谁来操作它?答案分两层:I2C 控制器驱动管"怎么在总线上跑报文"(通常由芯片厂商提供),I2C 设备驱动管"跟这台设备说什么话"(本节要写的)。两层之间隔着内核 I2C 核心层,它把控制器抽象成传输接口——设备驱动只管发消息,不管电气与时序。

i2c_driver:换汤不换药的匹配

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 = &reg, /* 先写寄存器号 */ }, { .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; }

消息结构里的 addrflags 把 3.4 节逻辑分析仪上的那套"地址加方向"协议原封不动搬进了 C 结构——传输接口就是总线协议的软件镜像i2c_transfer 返回完成的报文数,不等于请求数即算失败,这个判定不能省。

SPI 驱动:同构骨架,传输换形

SPI 驱动的骨架与 I2C 完全同构:spi_driver 挂匹配表、probe 收 spi_devicemodule_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_bufrx_buf 同时给出——这正是 3.4 节说的"移位寄存器对射":主机发的四个字节与收到的四个字节在时间上重叠,命令字节发出去的同时收回的是填充,真正的识别码在后面三个字节。此外 SPI 设备的速率、模式(时钟极性与相位)在树节点里声明,probe 时内核已按声明配置好控制器,驱动不必再碰时序参数。

三类总线驱动对照收束

至此,三类总线驱动的骨架都见过了,同异一表收拢:

维度 平台驱动 I2C 设备驱动 SPI 设备驱动
设备对象来源 树节点展开 树里总线子节点加从地址 树里总线子节点加片选
probe 参数 平台设备 i2c_client spi_device
硬件信息获取 资源接口按索引取 client 内含地址与适配器 device 内含速率模式片选
寄存器访问 映射后直接读写 总线报文(消息或字节接口) 总线报文(消息接口)
探测验证能力 无总线可对话,信树 可读识别寄存器验货 可读识别命令验货
传输并发控制 不涉及 核心层串行化 核心层串行化

骨架同构、传输各异——这正是总线模型的设计意图:驱动的结构问题解决一次,处处复用;总线的个性问题各管各的。第 5 章要见到的块设备与网络设备框架虽然复杂得多,但"框架做掉通用、驱动填差异"的思想一脉相承。

本节要点回顾

  • 两层驱动分工:控制器驱动管总线时序,设备驱动管设备对话,I2C 核心层居中抽象。
  • probe 验货:总线设备驱动应读识别码确认硬件真实存在,别盲信声明。
  • 消息接口是协议镜像:地址、方向、报文段落在结构体里一一对应。
  • 三类驱动同构:匹配表加 probe 加注册宏的骨架统一,差异只在传输形态。

主线 LED 已成"正规军",通用方法论也集齐了。下一章走出字符设备的世界,看块设备与网络设备如何用完全不同的框架解决"数据吞吐"这道题。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U