手把手教你写一个可靠的 I2C 从设备驱动
你有没有遇到过这样的场景:板子上接了一个温湿度传感器,明明硬件连接没问题,电源也正常,但i2cdetect就是扫不到设备?或者读出来的数据乱七八糟,调试半天才发现是字节顺序搞反了?
别急,这背后很可能不是你的代码写得差,而是对I2C 驱动机制的理解不够深。在嵌入式 Linux 开发中,I2C 看似简单——两根线、地址一配、读写搞定。可一旦涉及中断、热插拔、跨平台适配,问题就层出不穷。
今天我们就来彻底拆解一遍如何从零开始编写一个健壮的 I2C 从设备驱动,不讲空话套话,只讲你在实际开发中真正会用到的东西:协议要点、内核架构、设备树配置、核心 API 使用,以及那些藏在手册里的“坑”。
为什么 I2C 如此重要?
先说结论:I2C 是现代嵌入式系统中最常用、最不可替代的低速总线之一。
它不像 SPI 那样高速,也不像 UART 只能点对点通信。它的优势在于“省”和“多”:
- 仅需两根引脚(SDA + SCL)
- 一条总线上可以挂载多达 128 个设备(7位地址)
- 几乎所有 SoC 都内置 I2C 控制器
- Linux 内核支持完善,生态成熟
所以无论是传感器、EEPROM、RTC 还是触摸控制器,几乎都能看到 I2C 的身影。掌握它的驱动开发能力,等于打通了外设接入的第一道关卡。
但这也意味着:如果你写的驱动不稳定,整个系统的可靠性都会受影响。
先搞明白:I2C 到底是怎么通信的?
很多开发者直接跳进代码,结果连最基本的通信流程都没理清。我们不妨先回到硬件层面,把关键机制捋清楚。
起始与停止信号:每次通信的“开关”
I2C 是半双工总线,所有操作都由主设备发起。通信开始时,主控会拉低 SDA(数据线),而此时 SCL(时钟线)保持高电平——这就是起始条件(Start Condition)。相反,当 SCL 为高时 SDA 从低变高,则表示停止条件(Stop Condition),一次传输结束。
中间的数据传输则按字节进行,每传完一个字节,接收方必须给出一个 ACK(应答)信号,否则就是 NACK。
📌小贴士:逻辑分析仪抓波形时,第一眼看的就是 Start 和 Stop 是否正确。如果连 Start 都没触发,那基本可以确定是软件没发出请求或硬件未使能。
地址帧结构:你是谁?
主设备要访问某个从设备,首先要发送它的地址。标准模式下使用的是7位地址 + 1位读写标志,共 8 位。
比如你要向地址为0x48的设备写数据,实际发送的是0x90(0x48 << 1 | 0);如果是读,就是0x91(0x48 << 1 | 1)。
注意!这个地址是你在设备手册里查到的原始值左移一位后的结果。很多初学者在这里栽跟头,误以为直接发0x48就行,其实是错的。
复合消息(Combined Format):读寄存器的标准姿势
最常见的操作是“先写寄存器地址,再读数据”。例如你想读 LM75 的温度值:
- 发送起始信号
- 写设备地址(W)
- 写目标寄存器地址(如
0x00) - 不发 Stop,而是立刻重发起始(Repeated Start)
- 发送设备地址(R)
- 读取两个字节数据
- 发 Stop
这种“中间不断开”的方式叫做repeated start,对应的在 Linux 中就是使用i2c_transfer()提交多个i2c_msg消息数组。
如果你中途发了 Stop,总线就会释放,第二次启动就变成了独立事务,某些设备可能无法响应。
Linux 内核中的 I2C 子系统长什么样?
理解了物理层之后,我们进入软件世界。Linux 对 I2C 的抽象非常清晰,主要分为三个层次:
| 层级 | 名称 | 作用 |
|---|---|---|
| 底层 | I2C Adapter(适配器) | 对应真实的 I2C 控制器(如 i2c-0),负责收发时序 |
| 中间 | I2C Client(客户端) | 表示挂在总线上的具体设备(如 lm75@0x48) |
| 上层 | I2C Driver(驱动程序) | 实现对该类设备的操作逻辑 |
它们之间的关系可以用一句话概括:Driver 描述“怎么操作”,Client 描述“哪个设备”,Adapter 负责“实际执行”。
匹配机制:设备和驱动是如何凑到一起的?
当你插入一块新板卡,系统是怎么知道该加载哪个驱动的?答案就在设备树(Device Tree)和.compatible字段中。
流程如下:
- 设备树声明了某个 I2C 总线下挂了一个
lm75@48,其compatible = "national,lm75"; - 内核解析 DTB 后创建一个
i2c_client实例; - 此时如果有注册过的驱动包含
.of_match_table条目匹配"national,lm75",就会调用其.probe()函数; - probe 成功后,设备正式上线。
这就实现了“硬件描述”和“软件功能”的解耦。同一个驱动文件可以在不同项目中复用,只要设备兼容字符串一致即可。
设备树该怎么写?别让语法错误拖后腿
设备树虽然强大,但也最容易因拼写错误导致设备无法识别。下面是一个典型的 I2C 设备节点定义:
&i2c1 { status = "okay"; clock-frequency = <400000>; // 设置为 400kHz 快速模式 temperature_sensor: lm75@48 { compatible = "national,lm75"; reg = <0x48>; interrupt-parent = <&gpio1>; interrupts = <25 IRQ_TYPE_LEVEL_LOW>; }; };几个关键点必须注意:
reg = <0x48>:这里的地址是7位地址,不要加读写位。clock-frequency:如果不设置,默认可能是 100kHz,影响性能。interrupts:如果设备有中断输出(比如超温报警),需要指定 GPIO 编号和触发类型。status = "okay":确保 I2C 控制器本身被启用。
💡经验之谈:如果你发现设备没加载,第一步不是看驱动代码,而是运行:
bash i2cdetect -y -a 1看是否能在总线上看到对应地址。如果看不到,八成是设备树或硬件问题。
核心驱动代码实战:以 LM75 温度传感器为例
现在我们动手写一个完整的驱动骨架。这不是玩具代码,而是生产环境中可用的基础模板。
第一步:定义私有数据结构
每个设备通常都需要保存一些运行状态,比如 client 指针、锁、缓存等:
struct lm75_data { struct i2c_client *client; struct mutex lock; /* 并发访问保护 */ bool powered; /* 电源状态标记 */ };第二步:声明支持的设备列表
有两个匹配途径:传统 ID 表 和 设备树 compatible 匹配。
/* 支持的传统设备ID */ static const struct i2c_device_id lm75_id[] = { { "lm75", 0 }, { } }; MODULE_DEVICE_TABLE(i2c, lm75_id); /* 支持的设备树兼容性 */ static const struct of_device_id lm75_of_match[] = { { .compatible = "national,lm75" }, { } }; MODULE_DEVICE_TABLE(of, lm75_of_match);✅最佳实践:同时提供两种匹配方式,增强兼容性。
第三步:实现 probe 函数 —— 设备初始化的核心
static int lm75_probe(struct i2c_client *client, const struct i2c_device_id *id) { struct lm75_data *data; int ret; /* 检查适配器是否支持所需的通信功能 */ if (!i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_WORD_DATA)) { dev_err(&client->dev, "I2C adapter does not support word access\n"); return -EIO; } /* 分配并初始化私有数据 */ data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL); if (!data) return -ENOMEM; >static int lm75_read_temperature(struct i2c_client *client) { s32 raw; int temp; raw = i2c_smbus_read_word_data(client, LM75_REG_TEMP); if (raw < 0) { dev_err(&client->dev, "Read failed: %d\n", raw); return raw; } /* 注意:SMBus 返回的是 little-endian,但 LM75 发送的是 big-endian */ raw = swab16(raw); /* 字节交换 */ temp = sign_extend32(raw >> 7, 8) * 500; /* 转换为 m°C,分辨率 0.5°C */ return temp; /* 单位:微摄氏度(μ°C)更佳,此处简化为毫度 */ }🔥经典坑点:忘记
swab16()导致读出 256°C 的“高温奇迹”。务必根据芯片手册确认数据格式!
第五步:remove 函数与模块注册
static int lm75_remove(struct i2c_client *client) { dev_info(&client->dev, "LM75 removed\n"); return 0; } static struct i2c_driver lm75_driver = { .driver = { .name = "lm75", .of_match_table = lm75_of_match, }, .probe = lm75_probe, .remove = lm75_remove, .id_table = lm75_id, }; module_i2c_driver(lm75_driver);使用module_i2c_driver()宏可以省去手动调用i2c_add_driver()和注销函数,更加简洁安全。
更底层的操作:什么时候要用i2c_transfer()?
前面用了i2c_smbus_read_word_data(),这是高层封装,方便但有限制。如果你面对的是非标准协议、批量数据传输或需要精确控制 timing,就得上i2c_transfer()。
比如你要连续读取 6 字节的加速度计原始数据:
u8 reg = 0x28; /* 数据起始寄存器 */ u8 buf[6]; struct i2c_msg msgs[2]; /* Step 1: 写寄存器地址 */ msgs[0].addr = client->addr; msgs[0].flags = 0; msgs[0].len = 1; msgs[0].buf = ® /* Step 2: 读取6字节数据 */ msgs[1].addr = client->addr; msgs[1].flags = I2C_M_RD; msgs[1].len = 6; msgs[1].buf = buf; int ret = i2c_transfer(client->adapter, msgs, 2); if (ret != 2) { dev_err(&client->dev, "I2C transfer failed: %d\n", ret); return -EIO; }这种方式完全可控,适合复杂场景。记住:返回值是成功传输的消息数,不是字节数!
常见问题与调试技巧(血泪总结)
❌ 问题1:i2cdetect扫不到设备
- ✅ 检查设备树
status="okay"和reg地址是否正确 - ✅ 测量 VCC 是否供电(万用表最直接)
- ✅ 查看上拉电阻是否存在(典型值 4.7kΩ)
- ✅ 使用逻辑分析仪观察是否有 Start 信号发出
❌ 问题2:总是收到 NACK
- ✅ 地址是否左移了一位?(常见错误)
- ✅ 从设备是否处于 reset 或 sleep 状态?
- ✅ SDA/SCL 是否被其他设备拉低?
❌ 问题3:读出的数据错乱
- ✅ 是否处理了字节序?
swab16()/be16_to_cpu()不能少 - ✅ 是否使用了正确的寄存器地址?对照 datasheet 再核对一遍
- ✅ 是否在读写之间加了必要延时?(某些 ADC 需要稳定时间)
✅ 调试利器推荐
| 工具 | 用途 |
|---|---|
i2cdetect -y -a N | 扫描总线设备 |
i2cget -f -y N A [R] | 读取指定寄存器 |
i2cset -f -y N A R V | 写入寄存器 |
| Saleae Logic Analyzer | 抓取真实波形,定位 timing 问题 |
dmesg | 查看驱动加载日志、probe 是否成功 |
最后一点思考:I2C 的未来会被取代吗?
随着 I3C(Improved I2C)的推出,更高带宽、更低功耗、动态地址分配等特性正在逐步落地。但它仍然向下兼容 I2C,短期内不会动摇现有生态。
对于我们开发者来说,深入理解 I2C 不仅是为了现在能快速接入外设,更是为了将来平滑过渡到 I3C 打下基础。毕竟,通信的本质从未改变:精准、可靠、可维护。
如果你正在做一个新的传感器模块,不妨试着用本文的方法从头写一遍驱动。你会发现,原来那些看似神秘的 I2C 通信,其实都有迹可循。
有任何疑问或踩过的坑,欢迎在评论区分享交流。