目录
- 什么是I2C?
- I2C硬件基础:两根线搞定所有通信
- [Linux I2C子系统三层架构详解](#Linux I2C子系统三层架构详解)
- 实操:RK3399原生I2C控制器设备树解析
- 常用I2C核心API速览
- 核心总结+思维导图
1. 什么是I2C?
I2C(Inter-Integrated Circuit)是嵌入式系统中最常用的低速串行总线,用于连接各种低速外设,如触摸屏、传感器、EEPROM、RTC时钟等。
1.1 I2C总线核心概念
| 概念 | 说明 |
|---|---|
| I2C适配器(主设备) | 发起所有通信,控制整个总线的时钟与数据传输 |
| I2C从设备 | 被动响应主设备的请求,不能主动发起通信 |
| I2C总线 | 由SCL和SDA两根信号线构成的物理通信通道 |
| SCL时钟线 | 由主设备驱动,为总线提供同步时钟信号 |
| SDA数据线 | 双向数据线,主从设备通过它传输数据 |
| 从设备地址 | 每个从设备唯一的7位标识,用于总线寻址 |
1.2 I2C通信流程
I2C总线上的数据传输遵循严格的主从通信协议,具体流程如下:
- 起始条件(START Condition):主设备将SDA线从高电平拉低,同时SCL线保持高电平,标志一次通信的开始
- 发送从设备地址与读写位:主设备发送7位从设备地址,随后发送1位读写控制位(0表示写操作,1表示读操作)
- 从设备应答(ACK):地址匹配的从设备在第9个SCL周期将SDA线拉低,发送应答信号确认连接
- 数据传输:主从设备之间按字节传输数据,每传输一个字节后接收方发送ACK应答,直至数据发送完毕
- 停止条件(STOP Condition):主设备将SDA线从低电平拉高,同时SCL线保持高电平,标志通信结束,释放总线
1.3 I2C总线时序图
下面展示一次完整的I2C写操作时序,包含起始条件、从设备地址、读写位、应答、数据和停止条件:
从设备 I2C总线(SCL/SDA) 主设备(适配器) 从设备 I2C总线(SCL/SDA) 主设备(适配器) #mermaid-svg-70FUa5ha93g3YaaG{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-70FUa5ha93g3YaaG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-70FUa5ha93g3YaaG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-70FUa5ha93g3YaaG .error-icon{fill:#552222;}#mermaid-svg-70FUa5ha93g3YaaG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-70FUa5ha93g3YaaG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-70FUa5ha93g3YaaG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-70FUa5ha93g3YaaG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-70FUa5ha93g3YaaG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-70FUa5ha93g3YaaG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-70FUa5ha93g3YaaG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-70FUa5ha93g3YaaG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-70FUa5ha93g3YaaG .marker.cross{stroke:#333333;}#mermaid-svg-70FUa5ha93g3YaaG svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-70FUa5ha93g3YaaG p{margin:0;}#mermaid-svg-70FUa5ha93g3YaaG .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-70FUa5ha93g3YaaG text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-70FUa5ha93g3YaaG .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-70FUa5ha93g3YaaG .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-70FUa5ha93g3YaaG .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-70FUa5ha93g3YaaG .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-70FUa5ha93g3YaaG #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-70FUa5ha93g3YaaG .sequenceNumber{fill:white;}#mermaid-svg-70FUa5ha93g3YaaG #sequencenumber{fill:#333;}#mermaid-svg-70FUa5ha93g3YaaG #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-70FUa5ha93g3YaaG .messageText{fill:#333;stroke:none;}#mermaid-svg-70FUa5ha93g3YaaG .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-70FUa5ha93g3YaaG .labelText,#mermaid-svg-70FUa5ha93g3YaaG .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-70FUa5ha93g3YaaG .loopText,#mermaid-svg-70FUa5ha93g3YaaG .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-70FUa5ha93g3YaaG .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-70FUa5ha93g3YaaG .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-70FUa5ha93g3YaaG .noteText,#mermaid-svg-70FUa5ha93g3YaaG .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-70FUa5ha93g3YaaG .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-70FUa5ha93g3YaaG .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-70FUa5ha93g3YaaG .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-70FUa5ha93g3YaaG .actorPopupMenu{position:absolute;}#mermaid-svg-70FUa5ha93g3YaaG .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-70FUa5ha93g3YaaG .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-70FUa5ha93g3YaaG .actor-man circle,#mermaid-svg-70FUa5ha93g3YaaG line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-70FUa5ha93g3YaaG :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ① 起始条件:SDA先拉低,SCL保持高 ② 发送7位从设备地址 + 1位读写位 ③ 从设备应答(ACK) ④ 数据传输(每字节后跟ACK) ⑤ 停止条件:SDA先拉高,SCL保持高 SDA ↓(SCL高电平) 发送地址位 A6~A0 + R/W(0=写) 第9个SCL周期拉低SDA 发送数据字节 D7~D0 ACK应答 SDA ↑(SCL高电平)
时序说明:
- 起始条件:SCL为高电平时,SDA从高电平切换到低电平
- 地址发送:主设备在SCL每个时钟周期发送1位数据,先发送最高位(MSB)
- 应答位:每个字节传输完成后,接收方在第9个SCL周期将SDA拉低表示应答
- 停止条件:SCL为高电平时,SDA从低电平切换到高电平
- 整个通信过程中,SDA数据只能在SCL低电平时变化,SCL高电平时SDA必须保持稳定
2. I2C硬件基础:两根线搞定所有通信
I2C总线只需要两根线就能连接上百个设备,这是它最大的优势:
- SCL(Serial Clock):串行时钟线,由主设备控制,同步数据传输
- SDA(Serial Data):串行数据线,主从设备双向传输数据
核心特性
- 主从架构:同一时间只能有一个主设备,多个从设备共享总线
- 唯一地址:每个从设备有一个7位的唯一地址(部分设备支持10位地址)
- 半双工通信:同一时间只能单向传输数据
- 低速总线:标准模式100kbps,快速模式400kbps,高速模式3.4Mbps
- 总线仲裁:多个主设备同时发起通信时,通过仲裁机制决定谁先使用总线
3. Linux I2C子系统三层架构详解
Linux内核将I2C子系统设计为三层架构,实现了硬件与软件的解耦,极大提高了驱动的复用性和可移植性。
三层架构总览
| 层级 | 名称 | 定位 | 核心职责 |
|---|---|---|---|
| 第一层 | 适配器驱动层(Adapter Driver) | 最底层,直接操作硬件 | 初始化控制器、收发数据、处理中断 |
| 第二层 | 核心层(I2C Core) | 中间层,承上启下 | 提供通用API、管理总线与设备模型 |
| 第三层 | 从设备驱动层(Client Driver) | 最上层,面向外设 | 实现具体外设功能(触摸屏、传感器等) |
3.1 第一层:I2C适配器驱动层(Adapter Driver)
- 定位:最底层,直接和硬件打交道
- 职责:实现I2C控制器的硬件操作,包括初始化控制器、发送数据、接收数据、处理中断等
- 本质:一个 platform 驱动,因为I2C控制器是SoC平台自带的外设
- RK3399对应 :
drivers/i2c/busses/i2c-rk3x.c,这是瑞芯微所有平台通用的I2C控制器驱动
核心工作:将内核通用的I2C消息转换为具体控制器能识别的硬件操作,对上层屏蔽硬件差异。
3.2 第二层:I2C核心层(I2C Core)
- 定位:中间层,连接适配器驱动和从设备驱动的桥梁
- 职责 :
- 提供通用的I2C核心API,对上层屏蔽底层硬件差异
- 管理I2C总线和设备,负责总线的注册、注销和设备的枚举
- 实现I2C设备模型,将I2C设备和驱动关联起来
- 核心文件 :
drivers/i2c/i2c-core.c - 对驱动开发者的意义:我们写I2C从设备驱动时,只需要调用核心层提供的API,不需要关心底层控制器的具体实现
3.3 第三层:I2C从设备驱动层(Client Driver)
- 定位:最上层,和具体的外设打交道
- 职责:实现具体外设的功能,如触摸屏的坐标读取、传感器的数据采集等
- 本质 :一个
i2c_driver,通过I2C总线和从设备通信 - 常见例子 :
- FT5x06 触摸屏驱动
- MMA7660 加速度传感器驱动
- RX8010 RTC驱动
核心工作:调用核心层提供的API,向I2C从设备发送命令和读取数据,实现外设的具体功能。
小结:适配器与从设备的关系及分层优势
核心关系:一个I2C适配器对应一条I2C总线,一条总线上可挂多个从设备(最多127个),每个从设备有唯一7位地址。适配器是主设备,主动发起通信;从设备被动响应。
分层优势:
- 硬件与软件解耦:从设备驱动不关心底层SoC型号,更换控制器只需改适配器驱动
- 代码复用性高:适配器驱动被所有从设备共享,核心层API被所有I2C驱动复用
- 可移植性强:从设备驱动可轻松移植到任何有对应适配器驱动的平台
- 易于维护扩展:新增控制器只需写适配器驱动,新增外设只需写从设备驱动
4. 实操:RK3399原生I2C控制器设备树解析
现在结合飞凌OK3399-C设备树,解析RK3399原生I2C控制器的设备树节点。
4.1 定位RK3399 I2C节点
RK3399的I2C控制器定义在rk3399.dtsi中,板级DTS(rk3399-ok3399-c.dts)中根据实际硬件配置启用或禁用对应的I2C总线。
在设备树中搜索i2c,可以找到以下启用的I2C节点:
dts
&i2c0 {
status = "okay";
i2c-scl-rising-time-ns = <168>;
i2c-scl-falling-time-ns = <4>;
clock-frequency = <400000>;
rk808: pmic@1b {
compatible = "rockchip,rk808";
reg = <0x1b>;
// ... PMIC其他配置
};
};
&i2c1 {
status = "okay";
i2c-scl-rising-time-ns = <300>;
i2c-scl-falling-time-ns = <15>;
polytouch: edt-ft5x06@38{
compatible = "edt,edt-ft5406", "edt,edt-ft5x06";
reg = <0x38>;
// ... 触摸屏其他配置
};
gs_mma7660: gs-mma7660@4c{
compatible = "gs_mma7660";
reg = <0x4c>;
// ... 加速度传感器其他配置
};
rtc@32 {
compatible = "rx8010";
reg = <0x32>;
// ... RTC其他配置
};
};
&i2c2 {
status = "okay";
i2c-scl-rising-time-ns = <300>;
i2c-scl-falling-time-ns = <15>;
wm8960: wm8960@1a {
compatible = "wlf,wm8960";
reg = <0x1a>;
// ... 音频Codec其他配置
};
};
&i2c7 {
status = "okay";
i2c-scl-rising-time-ns = <300>;
i2c-scl-falling-time-ns = <15>;
fusb0: fusb30x@22 {
compatible = "fairchild,fusb302";
reg = <0x22>;
// ... USB PD芯片其他配置
};
};
4.2 逐行解析I2C控制器节点
以i2c1为例,逐行解析每个属性的含义:
dts
&i2c1 {
status = "okay"; // 启用该I2C总线
i2c-scl-rising-time-ns = <300>; // SCL时钟上升沿时间,单位纳秒
i2c-scl-falling-time-ns = <15>; // SCL时钟下降沿时间,单位纳秒
clock-frequency = <400000>; // 总线时钟频率,400kHz(快速模式)
// 挂在i2c1总线上的从设备节点
polytouch: edt-ft5x06@38{
compatible = "edt,edt-ft5406", "edt,edt-ft5x06"; // 驱动匹配字符串
reg = <0x38>; // 从设备地址:0x38(7位地址)
interrupt-parent = <&gpio1>; // 中断父控制器
interrupts = <RK_PC6 IRQ_TYPE_EDGE_FALLING>; // 中断配置
// ... 其他从设备特定属性
};
};
4.3 从设备节点核心属性
所有I2C从设备节点都必须包含以下两个核心属性:
compatible:与从设备驱动匹配的字符串reg:从设备的7位I2C地址(注意:不是8位地址,不需要左移)
4.4 如何添加自己的I2C从设备节点
如果需要在OK3399-C上添加一个新的I2C设备,只需要在对应的I2C总线节点下添加从设备节点即可。
示例:在i2c1总线上添加一个地址为0x50的EEPROM设备:
dts
&i2c1 {
eeprom@50 {
compatible = "atmel,24c02";
reg = <0x50>; // EEPROM的I2C地址
status = "okay";
};
};
5. 常用I2C核心API速览
编写I2C从设备驱动时,最常用的核心API有以下几个:
5.1 从设备驱动注册/注销
c
// 注册I2C从设备驱动
int i2c_add_driver(struct i2c_driver *driver);
// 注销I2C从设备驱动
void i2c_del_driver(struct i2c_driver *driver);
// 简化宏,自动生成module_init和module_exit
module_i2c_driver(driver);
5.2 数据读写API
c
// 从从设备的指定寄存器读取一个字节
u8 i2c_smbus_read_byte_data(const struct i2c_client *client, u8 command);
// 向从设备的指定寄存器写入一个字节
int i2c_smbus_write_byte_data(const struct i2c_client *client, u8 command, u8 value);
// 从从设备的指定寄存器读取多个字节
int i2c_smbus_read_i2c_block_data(const struct i2c_client *client, u8 command, u8 length, u8 *values);
// 向从设备的指定寄存器写入多个字节
int i2c_smbus_write_i2c_block_data(const struct i2c_client *client, u8 command, u8 length, const u8 *values);
5.3 通用传输API
c
// 通用I2C消息传输函数,可以发送任意数量的消息
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);
6. 核心总结+思维导图
6.1 核心总结
- I2C本质:主从架构的低速串行总线,只用两根线就能连接多个外设
- 三层架构 :
- 适配器驱动层:直接操作I2C控制器硬件
- 核心层:提供通用API,管理总线和设备
- 从设备驱动层:实现具体外设的功能
- 核心关系:一个适配器对应一条总线,一条总线上可以挂多个从设备,每个从设备有唯一地址
- 设备树规则 :
- I2C控制器节点在
rk3399.dtsi中定义 - 板级DTS中启用需要的总线并添加从设备节点
- 从设备节点必须包含
compatible和reg属性
- I2C控制器节点在
- 驱动开发:编写I2C从设备驱动时,只需要调用核心层提供的API,不需要关心底层控制器的实现
6.2 思维导图总结
