news 2026/10/14 4:43:00

Linux 驱动开发学习笔记:I2C 子系统从入门到实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux 驱动开发学习笔记:I2C 子系统从入门到实践

1. 引言

I2C(Inter-Integrated Circuit)总线是一种由 Philips 公司(现 NXP)开发的两线式串行通信总线,广泛应用于嵌入式系统中连接低速外设,如传感器、EEPROM、RTC 时钟芯片、触摸控制器等。在 Linux 内核中,I2C 子系统提供了完整的驱动框架,屏蔽了底层硬件差异,让开发者可以专注于设备驱动的业务逻辑。本文记录笔者学习 Linux I2C 驱动开发的过程,从硬件协议基础到内核框架,再到实际驱动编写,逐步梳理关键知识点。

2. I2C 协议基础

在深入 Linux 驱动之前,先回顾 I2C 总线的硬件协议要点,这对理解内核框架的设计至关重要。

2.1 物理层特性

  • 两线制:SCL(串行时钟线)和 SDA(串行数据线),均为开漏输出,需外接上拉电阻。
  • 多主多从:总线上可挂载多个主机和从机,通过地址区分设备。
  • 7 位或 10 位地址:常用 7 位地址,一个总线上最多挂载 112 个设备(保留地址除外)。
  • 传输速率:标准模式 100 kbit/s,快速模式 400 kbit/s,高速模式 3.4 Mbit/s。

2.2 数据传输格式

一次完整的 I2C 传输包含起始条件(START)、地址帧、数据帧和停止条件(STOP)。读写操作的基本格式如下:

写操作:START + 从机地址(写) + ACK + 寄存器地址 + ACK + 数据 + ACK + STOP 读操作:START + 从机地址(写) + ACK + 寄存器地址 + ACK + START + 从机地址(读) + ACK + 数据 + NACK + STOP

理解这个时序是编写驱动的基础,因为 Linux 内核的 I2C 传输接口本质上就是对这些时序的封装。

3. Linux I2C 子系统架构

Linux 内核将 I2C 子系统划分为三个层次:控制器驱动(Adapter)、核心层(Core)和设备驱动(Client Driver)。理解这三层的关系是学习 I2C 驱动的关键。

3.1 三层结构总览

  • I2C Adapter(控制器驱动):直接操作硬件寄存器,负责在总线上产生时序,对应 SoC 上的 I2C 控制器外设。
  • I2C Core(核心层):提供统一的设备模型、总线注册机制、数据传输接口,以及设备与驱动的匹配逻辑,对上层屏蔽控制器差异。
  • I2C Client Driver(设备驱动):针对具体挂在总线上的设备(如某型号传感器)编写,通过核心层提供的 API 与设备通信,不直接操作硬件寄存器。

3.2 关键数据结构

I2C 子系统的核心数据结构主要有以下几个:

数据结构作用
struct i2c_adapter描述一个 I2C 控制器,包含总线号、算法、锁等。
struct i2c_algorithm定义控制器收发数据的主函数(master_xfer)。
struct i2c_client描述一个挂在总线上的设备,包含地址、驱动名等。
struct i2c_driver描述一个设备驱动,包含 probe、remove 回调及 id 表。
struct i2c_msg描述一次传输的消息,包含地址、标志、缓冲区等。

4. 设备驱动开发流程

编写一个 I2C 设备驱动通常遵循固定的步骤。下面以读取一个虚拟传感器为例,梳理完整流程。

4.1 驱动框架搭建

首先定义一个i2c_driver结构体,并实现 probe 和 remove 回调:

#include <linux/i2c.h> #include <linux/module.h> #include <linux/init.h> static int my_sensor_probe(struct i2c_client *client) { pr_info("my_sensor: probe success, addr=0x%02x\n", client->addr); return 0; } static void my_sensor_remove(struct i2c_client *client) { pr_info("my_sensor: remove\n"); } static const struct i2c_device_id my_sensor_id[] = { { "my-sensor", 0 }, { } }; MODULE_DEVICE_TABLE(i2c, my_sensor_id); static struct i2c_driver my_sensor_driver = { .driver = { .name = "my-sensor", }, .probe = my_sensor_probe, .remove = my_sensor_remove, .id_table = my_sensor_id, }; module_i2c_driver(my_sensor_driver); MODULE_LICENSE("GPL");

4.2 数据传输接口

在 probe 中获取设备后,就可以通过i2c_transfer或更便捷的i2c_smbus_read_byte_data等接口读写数据。以读取传感器寄存器为例:

static int my_sensor_read_reg(struct i2c_client *client, u8 reg, u8 *val) { int ret; struct i2c_msg msgs[2] = { { .addr = client->addr, .flags = 0, .len = 1, .buf = &reg, }, { .addr = client->addr, .flags = I2C_M_RD, .len = 1, .buf = val, }, }; ret = i2c_transfer(client-&gt;adapter, msgs, 2); if (ret != 2) { dev_err(&amp;client-&gt;dev, "read reg 0x%02x failed, ret=%d\n", reg, ret); return ret &lt; 0 ? ret : -EIO; } return 0; }

4.3 设备树匹配

在现代内核中,通常通过设备树描述硬件。在设备树中声明设备节点后,驱动通过of_match_table完成匹配:

static const struct of_device_id my_sensor_of_match[] = { { .compatible = "vendor,my-sensor" }, { } }; MODULE_DEVICE_TABLE(of, my_sensor_of_match); static struct i2c_driver my_sensor_driver = { .driver = { .name = "my-sensor", .of_match_table = my_sensor_of_match, }, .probe = my_sensor_probe, .remove = my_sensor_remove, };

对应的设备树节点示例:

&i2c1 { my_sensor@48 { compatible = "vendor,my-sensor"; reg = <0x48>; }; };

5. SMBus 接口与辅助函数

对于大多数简单设备,不必每次都构造i2c_msg数组。内核提供了 SMBus 风格的封装函数,使用更加简洁。

5.1 常用 SMBus API

函数功能
i2c_smbus_read_byte()读取一个字节,不带寄存器地址。
i2c_smbus_write_byte()写入一个字节,不带寄存器地址。
i2c_smbus_read_byte_data()读取指定寄存器的值。
i2c_smbus_write_byte_data()向指定寄存器写入值。
i2c_smbus_read_word_data()读取 16 位寄存器值。
i2c_smbus_write_word_data()写入 16 位寄存器值。

5.2 使用示例

使用 SMBus 接口后,前面的读寄存器函数可以简化为:

static int my_sensor_read_reg(struct i2c_client *client, u8 reg, u8 *val) { int ret; ret = i2c_smbus_read_byte_data(client, reg); if (ret &lt; 0) { dev_err(&amp;client-&gt;dev, "read reg 0x%02x failed, ret=%d\n", reg, ret); return ret; } *val = (u8)ret; return 0; }

6. 用户空间访问

除了编写内核驱动,Linux 还提供了用户空间访问 I2C 设备的接口,方便调试和快速验证。通过/dev/i2c-N设备节点,配合i2c-dev内核模块,可以在应用层直接读写设备。

6.1 使用 i2c-tools

在开发板上安装i2c-tools后,可以快速扫描总线和读写设备:

# 扫描总线上的设备 i2cdetect -y 1 读取 0x48 设备 0x00 寄存器的值 i2cget -y 1 0x48 0x00 向 0x48 设备 0x01 寄存器写入 0xff i2cset -y 1 0x48 0x01 0xff

6.2 应用层编程

在 C 程序中,通过ioctl系统调用与I2C_RDWR命令完成读写:

#include <stdio.h> #include <fcntl.h> #include <linux/i2c-dev.h> #include <sys/ioctl.h> int main(void) { int fd = open("/dev/i2c-1", O_RDWR); if (fd < 0) { perror("open"); return -1; } struct i2c_msg msgs[2]; unsigned char reg = 0x00; unsigned char val = 0; msgs[0].addr = 0x48; msgs[0].flags = 0; msgs[0].len = 1; msgs[0].buf = &amp;reg; msgs[1].addr = 0x48; msgs[1].flags = I2C_M_RD; msgs[1].len = 1; msgs[1].buf = &amp;val; struct i2c_rdwr_ioctl_data data = { .msgs = msgs, .nmsgs = 2, }; if (ioctl(fd, I2C_RDWR, &amp;data) &lt; 0) { perror("ioctl"); close(fd); return -1; } printf("reg 0x00 = 0x%02x\n", val); close(fd); return 0; }

7. 调试与常见问题

I2C 驱动开发中遇到问题,可以从以下几个方向排查。

7.1 常用调试手段

  • 开启内核调试日志:在drivers/i2c目录下开启CONFIG_I2C_DEBUG_CORE和CONFIG_I2C_DEBUG_ALGO,观察传输细节。
  • 逻辑分析仪:抓取 SCL/SDA 波形,确认时序是否符合协议。
  • i2cdetect 扫描:确认设备地址是否正确,设备是否真的挂在总线上。
  • 检查上拉电阻:SDA/SCL 必须有上拉电阻,否则通信不稳定。

7.2 常见错误码

错误码含义
-ENXIO无此设备,通常是没有 ACK 应答,地址错误或设备未上电。
-ETIMEDOUT传输超时,总线可能被拉死或时钟异常。
-EBUSY总线忙,可能被其他主机占用。
-EINVAL参数无效,检查消息长度和标志位。

8. 总结

Linux I2C 驱动开发的核心在于理解三层架构:控制器驱动负责时序,核心层负责统一抽象,设备驱动负责业务逻辑。实际开发中,大部分工作集中在编写i2c_driver和利用 SMBus 辅助函数读写寄存器。建议初学者先在用户空间用 i2c-tools 验证硬件通路,再编写内核驱动,这样能有效缩小问题排查范围。后续可以进一步学习 I2C 的 10 位地址、多主机仲裁、DMA 传输等进阶内容。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 4:43:00

微信小程序支付后台Java实现:从下单到回调的完整闭环

简介&#xff1a;这份资源是面向Java后端开发者与小程序开发者的微信小程序支付后台实现实例&#xff0c;聚焦于打通小程序端与微信支付接口的完整链路&#xff0c;适合已具备一定Java与网络通信基础、希望快速落地支付功能的中级开发者参考。资源包共1个文件&#xff0c;为1个…

作者头像 李华
网站建设 2026/10/14 4:43:00

MySQL复合查询实战:JOIN、子查询与HAVING的正确打开方式

做后端开发这些年&#xff0c;MySQL复合查询是躲不开的坎。报表需求、多条件筛选、多表统计&#xff0c;几乎天天都在写。很多人一开始习惯把逻辑拆成好几条SQL&#xff0c;在应用层用for循环做关联和过滤&#xff0c;短期能跑&#xff0c;数据量一上来就卡得让人挠头。复合查询…

作者头像 李华
网站建设 2026/10/14 4:42:09

手写体识别V2实战:CRNN+CTC模型、数据增强与工程化落地全解析

手写体识别这事儿&#xff0c;看着挺成熟&#xff0c;真要落地做一遍就知道坑有多深。我从V1那个勉强能跑通Demo的版本迭代到V2&#xff0c;中间踩过的坑、推翻的设计方案、重新梳理的数据流程&#xff0c;都在这儿了。这篇博文不聊那些虚的&#xff0c;直接说V2是怎么从模型选…

作者头像 李华
网站建设 2026/10/14 4:41:31

从n8n迁移到Juggle:国内职场自动化背后的工作流设计差异

最近有个做运营的朋友跟我吐槽&#xff0c;说公司用某开源自托管的工作流工具搭了一堆自动化&#xff0c;结果一次小版本升级&#xff0c;好几个流程同时出了诡异问题&#xff0c;表达式里一个函数的行为变了&#xff0c;数据同步错了整整两天没人发现。他说准备换个工具重做。…

作者头像 李华
网站建设 2026/10/14 4:41:06

多时间尺度灵活性资源优化:微电网日内滚动调度与MPC实现

干过微电网能量管理或者综合能源系统优化调度的人&#xff0c;大概率都经历过这个场景&#xff1a;系统刚投运的时候&#xff0c;一套日前调度方案跑得挺好&#xff0c;全天成本漂亮、曲线也平滑。结果一到天气多变的季节&#xff0c;光伏一阵一阵地抖&#xff0c;负荷预测偏差…

作者头像 李华
网站建设 2026/10/14 4:41:06

低照度图像增强全栈方案:从Retinex到轻量GAN蒸馏

简介&#xff1a;本资源是一份面向计算机视觉研究者与深度学习开发者的低照度图像增强技术实践合集&#xff0c;聚焦解决夜间监控、弱光安防、野外探测等场景下目标检测因RGB信息匮乏导致的特征提取难、定位精度低等核心问题。压缩包为1个10KB的docx文档&#xff0c;系统梳理了…

作者头像 李华