I2C协议及驱动开发框架

简介

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
 
 
 
posted @ 2026-01-08 15:45  于光远  阅读(3)  评论(0)    收藏  举报