惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

aimingoo的专栏
aimingoo的专栏
O
OpenAI News
B
Blog
阮一峰的网络日志
阮一峰的网络日志
博客园_首页
罗磊的独立博客
Hugging Face - Blog
Hugging Face - Blog
Martin Fowler
Martin Fowler
博客园 - 司徒正美
美团技术团队
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Vercel News
Vercel News
Recent Announcements
Recent Announcements
MyScale Blog
MyScale Blog
D
Docker
S
SegmentFault 最新的问题
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
H
Hackread – Cybersecurity News, Data Breaches, AI and More
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
量子位
博客园 - 【当耐特】
云风的 BLOG
云风的 BLOG
Blog — PlanetScale
Blog — PlanetScale
V
V2EX
博客园 - Franky
宝玉的分享
宝玉的分享
C
CERT Recently Published Vulnerability Notes
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Simon Willison's Weblog
Simon Willison's Weblog
Recorded Future
Recorded Future
T
The Blog of Author Tim Ferriss
T
Tor Project blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
I
Intezer
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
雷峰网
雷峰网
T
Threatpost
Cisco Talos Blog
Cisco Talos Blog
L
LINUX DO - 热门话题
SecWiki News
SecWiki News
I
InfoQ
T
The Exploit Database - CXSecurity.com
Hacker News - Newest:
Hacker News - Newest: "LLM"
P
Privacy & Cybersecurity Law Blog
T
Troy Hunt's Blog
Latest news
Latest news
MongoDB | Blog
MongoDB | Blog
T
Tenable Blog
博客园 - 叶小钗
Attack and Defense Labs
Attack and Defense Labs

博客园 - 于光远

泛型编程 分区理论知识整理 SPI协议及驱动开发框架 使用qemu 加载linux-6.18.1内核 linux 内核 策略模式 单例模式 Linux性能分析:从监控到优化的完整工具链 C++模板元编程实现序列化与反序列化 引用 bootchart linux 脚本打印时间 接口编程 不定参数解析,结合tuple 独立于main运行的程序 类模板继承 std::ostream & operator<< Flash HIDL --HelloWorld
I2C协议及驱动开发框架
于光远 · 2026-01-08 · via 博客园 - 于光远

简介

I2C(Inter-Integrated Circuit)总线是由Philips公司开发的一种两线式串行总线,用于连接微处理器及其外围设备。以简单、高效特点著名,占用PCB空间很小,芯片引脚数量少,设计成本低。

总线即意味着可连接多个设备。控制数据传输和时钟频率的设备称为主控,其余为从设备。I2C设备支持多主控模式,但任意时刻只能有一个主控。

组成I2C总线的两个信号线分别为SCL(时钟)和SDA(数据线)。为避免总线信号混乱,从硬件设计角度出发,总线上会外接上拉电阻,默认使SCL和SDA均保持高电平,同时要求各设备连接到总线的输出端必须为OC门(CMOS开漏输出或TTL集电极开路输出),实现“线与”逻辑,即任意设备输出低电平时,都会拉低总线电平。

image

I2C设备上的串行数据线SDA的接口电路是双向的,输出电路用于向总线上发数据,输入电路用于接收总线上的数据。同样的,串行时钟线SCL也是双向的,对于主机设备而言,通过SCL输出电路发送时钟信号,并检测总线上的SCL电平以决定什么时候发送下一个时钟脉冲;作为从机设备,需要按总线上SCL的信号发送或者接收SDA上的信号,也可以向SCL线发出低电平信号以延长总线时钟信号周期。简而言之,I2C设备的通信频率是主机的SCL脉冲信号决定的,从机需要按照此频率来接收和发送SDA数据

那么,数据到底如何传输呢?

时序图

和串口协议一样,I2C也有自己的起始位与停止位。当SCL稳定在高电平时,SDA由高到低的变化将产生一个开始位,而由低到高的变化则产 生一个停止位,均由主设备产生。

image

I2C设备是以字节为单位进行传输的。当SCL脉冲电平恒为高时,SDA线电平即为传输的有效数据(高电平采样)。每个字节传完后,主设备会放开SDA线,相应的从设备需要拉低SDA线电平,表示收到数据,主设备会检测SDA线电平,以判断传输是否在正常进行(缺确认完成ACK)。

image

以数据读写为例:在正式开始传输数据之前,I2C主设备需要确定想要和哪个从设备通信(从设备编号),以及是去读数据还是写数据(读写标志),读/写到从设备哪里(寄存器地址),这些信息都需要在开始位后首先确定。

image

首字节为7位设备地址+1位读写标志,第二字节的8位为寄存器的地址。与写数据不同的是,读数据需要先发送一个写数据帧,从设备确认应答后,需要重新开始一次数据传输,这次就无需重新指定寄存器的地址了,因为寄存器地址已被I2C控制器保存(实际上是一个指针)。

i2c速率

i2c总线在不同模式下具有不同的传输速率上限:

  • 标准模式:最高100Kb/s
  • 快速模式:最高400Kb/s
  • 高速模式:最高3.4Mb/s
  • 超快速模式:部分芯片支持,速率可达5Mb/s(需要硬件支持)

驱动程序

I2C驱动体系结构

image

I2C系统的体系结构如图所示,可按5个层级划分,介绍如下:

  • 应用层:用户通过对open,write,read等系统调用对设备进行操作;
  • 设备驱动层:连接到I2C总线上的一些外围设备的驱动程序,如传感器、EEPROM等。设备驱动层包含i2c_driver和i2c_client数据结构,需要根据设备实现其中的成员函数。i2c_driver对应一套驱动方法,i2c_client对应于真实的物理设备,每个i2c设备都需要一个i2c_client来描述
  • i2c核心层:这部分由Linux内核源码提供,包括i2c总线驱动和设备驱动的注册、注销方法,i2c通信方法。i2c_transfer函数是在这里实现的。
  • i2c总线驱动层:由芯片的原厂程序员负责编写,也可以称为控制器驱动层。包含Algorithm、i2c_adapter核心结构体,实现与硬件相关的代码。Algorithm->master_xfer()会被核心层的i2c_transfer调用,完成i2c信号的传输。在这里控制i2c适配器以主控的方式产生开始位、停止位、读写周期,以及以从设备方式被读写,产生ACK等。i2c_adapter对应物理上的一个适配器,而i2c_algorithm对应一套通信方法
  • 硬件层:包括集成在CPU内部的i2c适配器以及挂载在适配器上的各类i2c设备。

其中,i2c驱动主要关注i2c核心、i2c总线、i2c设备驱动。

image

i2c驱动的软件框架与基本的注册流程如上图,说明如下:

  • 最右边部分为i2c设备驱动,包含i2c_driver和i2c_client两个核心结构体,在设备树中将某个设备挂在到某个i2c总线节点下,of_i2c_rigister_devices(adap)将该设备节点转换为i2c_client,添加到总线的设备列表中;i2c_driver在驱动加载时通过i2c_register_drive添加到总线的驱动列表中,通过i2c_device_match函数匹配compitible属性,若匹配成功,则执行i2c_driver下的probe函数,在这里实现对该i2c设备的初始化,以及open、write等操作方式,以供用户空间调用。
  • 最左边为i2c控制器驱动,其中设备树节点中的i2c控制器节点被转换为platform_device,添加到platform_bus_type的虚拟总线设备列表中,在驱动加载时,相应的platform_driver也会被加载到虚拟总线的驱动列表中,然后执行driver->i2x_imx_probe函数。
  • 中间是i2x_imx_probe函数里面所做的工作,首先是将构建一个i2c_adapter,然后调用了i2c_register_adapter函数,将该i2c_adapter注册到i2c_bus_type的总线上,最后调用of_i2c_register_devices(adap)将挂载在当前i2c总线上的设备添加到i2c_bus_type类型的设备列表中,查找有无匹配的驱动被注册,与设备驱动的注册流程完成闭环。

面对如此复杂的Linux i2c子系统,驱动工程师要完成哪些工作?

一方面,适配器驱动可能是Linux内核本身还不包含的;另一方面,挂接在适配器上的具体设备驱动可能也是Linux内核还不包含的。因此,工程师要实现的主要工作如下。

  • 提供I2C适配器的硬件驱动,探测、初始化I2C适配器(如申请I2C的I/O地址和中断号)、 驱动CPU控制的I2C适配器从硬件上产生各种信号以及处理I2C中断等。
  • 提供I2C 适配器的 algorithm,用具体适配器的 xxx_xfer()函数填充 i2c_algorithm 的 master_xfer 指针,并把i2c_algorithm 指针赋值给 i2c_adapter 的 algo 指针。
  • 实现I2C设备驱动中的i2c_driver接口,用具体设备yyy的yyy_probe()、yyy_remove()、 yyy_suspend()、yyy_resume()函数指针和 i2c_device_id 设备 ID 表赋值给 i2c_driver 的 probe、remove、suspend、resume 和 id_table 指针。
  • 实现I2C设备所对应类型的具体驱动,i2c_driver只是实现设备与总线的挂接,而挂接在 总线上的设备则是千差万别。例如,如果是字符设备,就实现文件操作接口,即实现具体设备yyy的yyy_read()、yyy_write()和 yyy_ioctl()函数等;如果是声卡,就实现 ALSA 驱动。

I2C-tools

i2ctool是一个非常具有实用性的工具,一般系统内都有集成,对于调试非常方便

image

使用过程中会用到以下参数:

-y:自动选择yes,跳过交互

-r:快速读指令

-q:快速写指令

-f:强制使用此设备地址

  • i2cdetect

i2cdetect -l 探测所有的i2c总线

image

i2cdetect -y -r <id> 检测某个总线上的i2c设备,注意:这里检测出的是7位地址,不包括读写位

image

表示0x1e处有设备,若显示UU,则表示该设备已被驱动占用

i2cdetect -F <id> 查看总线设备支持的功能

image

  • i2cdump

i2cdump -f -y <id> <device addr> 打印i2c设备所有寄存器的值

image

  • i2cget

i2cget -f -y <id> <device addr> <register addr> 读取某个设备某个寄存器的值

如:i2cget -f -y 1 0x62 0x00 意为读取1号总线上0x62处设备的0x00寄存器的值

  • i2cset

i2cset -f -y <id> <device addr> <register addr> <value> 写入某个设备某个寄存器的值

  • i2ctransfer

读取/写入多字节寄存器地址,是i2cget和i2cset的升级版,语法如下:

i2ctransfer [-f] [-y] [-v] [-a] <bus id> w-n@<device addr> data-0 data-1 ... data-n r-num

i2ctransfer [-f] [-y] [-v] [-a] <bus id> w-n@<device addr> data-0 data-1 ... data-n

参数说明:

-a:允许使用0x00~0x07和0x78~0x7f地址

w-n:写n个字节

data-0,data-1...,data-n:寄存器地址(读) / 寄存器地址和写的数据(写)

r-num:读n个字节

举例:

  • i2ctransfer -y -f 1 w4@0x1a 0x0 0x0 0xfe 0xf2 把0xfe和0xf2写入到0x1a芯片的0x0 0x0寄存器中。w4表示要写4个字节,即寄存器地址0x0 0x0和数据0xfe 0xf2。
  • i2ctransfer -y -f 1 w2@0x1a 0x0 0xf r16 从0x0 0xf寄存器开始,向后读16个字节。w2表示要写2个字节,即寄存器地址0x0 0xf