Linux I2C子系统

编程达人挑战赛·第5期 10w+人浏览 401人参与

1.I2C总线知识

1.1 I2C总线物理拓扑结构

I2C总线的物理拓扑结构极为简洁,主要由SDA(串行数据线)、SCL(串行时钟线)以及上拉电阻(阻值范围4.7K-100K)构成。其电气特性是通过精确控制SCL与SDA信号线的高低电平时序序列,生成符合I2C总线协议规范的通信信号,从而完成数据传输任务。当总线处于闲置状态时,由于上拉电阻的作用,SDA与SCL信号线均被拉至高电平,维持总线的空闲状态。

1.2 I2C总线特征

  1. I2C总线架构支持每个连接设备在不同场景下担任主控或从属角色。

  2. 每个设备均分配有特定的唯一标识符(具体数值可从对应I2C器件技术手册查询),主从设备间的通信目标正是通过此标识符确定。

  3. 在常规应用场景中,CPU内置的I2C控制器通常配置为主控设备,而连接至总线的其余外围设备则配置为从属设备。

  4. I2C总线允许连接的设备总数受限于总线最大负载电容400pF;若连接的为同类型器件,则额外受限于器件地址编码的可配置范围。

  5. I2C协议支持7位与10位两种地址格式,地址类型可分为固定硬件地址和软件可配置地址。同一条I2C总线上允许7位地址设备与10位地址设备共存。

  6. I2C总线的数据传输速率在标准模式下最高达100kbit/s,快速模式下可达400kbit/s,高速模式下可提升至3.4Mbit/s。传输速率通常通过配置I2C控制器的可编程时钟分频器实现调节。

  7. I2C总线通信过程中,主控设备与从属设备之间以字节(8位数据)为基本单位进行双向数据交换。

1.3 I2C总线协议

1.3.1 I2C总线协议基本时序信号

空闲状态:SCL与SDA两条信号线均维持高电平状态。

起始条件:当总线处于空闲状态(SCL和SDA均为高电平)时,若SCL保持高电平而SDA出现由高至低的电平转变,则表明起始条件被触发。起始条件触发后,总线进入占用状态,由当前通信的主从设备对获得独占使用权,其他I2C设备无法介入总线操作。

停止条件:当SCL保持高电平而SDA出现由低至高的电平转变,表示停止条件被触发。

应答信号:在完成每个字节传输后的下一个时钟周期内,当SCL处于高电平期间,若SDA保持低电平状态,则表示接收到有效的应答信号。

非应答信号:在完成每个字节传输后的下一个时钟周期内,当SCL处于高电平期间,若SDA保持高电平状态,则表示接收到非应答信号。

注意:起始条件与停止条件的生成操作始终由主控设备负责执行。

基本时序如下图所示:

1.3.2 数据传输时序

当起始条件触发后,数据传送过程随即开始。在此阶段,主控设备在SCL线的每个时钟周期内,同步在SDA线上发送一位数据(地址信息与常规数据采用相同的传送机制),每完成一个字节的传输,后续会附加一个确认位。当主控设备决定终止数据交换时,会生成停止信号,此时总线控制权被释放,SCL与SDA信号线均恢复至高电平空闲状态。数据传送时序关系如下图所示:

1.3.3 I2C寻址方式

I2C总线网络中每个I2C器件均分配有特定的唯一标识符,主从设备间的通信过程依赖于该标识符建立连接。主控单元在发送有效数据前必须首先选定目标设备的标识符,此标识符传输流程与前述数据传输机制一致。通常情况下,多数从设备采用7位标识符格式(有的设备地址是10位的,发送地址要使用两个字节,这里仅以7位地址为例子)。根据协议规范,需在7位标识符后附加一个方向控制位,构成完整的8位地址字节:该位为0时表示主控单元向目标设备写入数据,为1时表示主控单元从目标设备读取数据。具体格式如图所示:

1.4 I2C总线操作

  • 主设备往从设备中写数据。数据传输格式如下:

  • 主设备从从设备中读数据。数据传输格式如下:

  • 主设备往从设备中写数据,然后重启起始条件,紧接着从从设备中读取数据;或者是主设备从从设备中读数据,然后重启起始条件,紧接着主设备往从设备中写数据。数据传输格式如下:

第三种操作在单个主设备系统中,重复的开启起始条件机制要比用STOP终止传输后又再次开启总线更有效率。

标准I2C时序不等于具体器件时序,具体器件时序才有内部地址,标准I2C协议中并没有内部地址。

AT24C02:是存储芯片,内部会有存储单元,给存储单元一个编号,读写它时需要指明内部地址。

示例:

2.I2C子系统体系结构框架

在Linux操作系统环境下使用I2C总线相较于裸机环境更为复杂。为充分展现Linux内核的架构设计理念,I2C总线的使用机制被设计为层次化组织结构。此架构由三个核心组件构成:I2C核心层、I2C总线控制器驱动层以及I2C设备驱动层。架构组成关系如图所示:

  1. I2C子系统采用三层架构设计:设备驱动层,核心层,控制器驱动层

  2. 设备驱动层:负责特定I2C外设的功能实现(依据器件规格书实现通信时序及控制逻辑),此组件可进一步细分为:I2C外设描述层(类比平台设备模型)和I2C外设驱动层(类比平台驱动模型),分别通过i2c_client和i2c_driver数据结构实现。(这一层是驱动开发人员的核心工作区域)

  3. 核心层:承担中间桥梁职能,向上层应用提供统一的设备操作API,向下层硬件提供控制器驱动接口。此层实现完全硬件抽象,具备跨平台特性,通常无需开发人员干预。

  4. 控制器驱动层:内核通过i2c_adapter结构体表征I2C控制器实例,负责实现特定I2C控制器的硬件操作,包括标准I2C通信协议的时序控制及数据收发功能。此组件通常由芯片制造商提供支持。例如RK3399平台的控制器驱动由瑞芯微公司提供。

  5. I2C子系统分层架构带来两大优势:

    1. 特定外设驱动可实现硬件平台无关性,显著提升代码可移植性;

    2. 内置总线仲裁机制,有效处理多设备对总线的并发访问冲突。

3.I2C相关的重要数据结构

3.1 struct i2c_driver

struct i2c_driver对应驱动方法,当总线上注册了对应的i2c从设备时,如果可以匹配成功,则调用probe函数初始化设备,注册字符设备,提供接口给应用程序。

struct i2c_driver {
	unsigned int class;

	/* Standard driver model interfaces */
	int (*probe)(struct i2c_client *, const struct i2c_device_id *);
	int (*remove)(struct i2c_client *);

	struct device_driver driver;
	const struct i2c_device_id *id_table;

	/* Device detection callback for automatic device creation */
	int (*detect)(struct i2c_client *, struct i2c_board_info *);
	const unsigned short *address_list;
	struct list_head clients;
};

重要成员:probe:设备探测函数,当i2c_client实例与i2c_driver成功绑定时触发执行,其功能定位与平台驱动模型中的probe函数等效

remove:设备移除函数,当已绑定的i2c_client与i2c_driver解除关联关系时(如卸载任一组件模块),此函数将被调用执行资源清理工作

driver:结构体中的name字段必须填充有效值,但该字段不参与设备匹配过程,此特性与平台驱动模型存在本质区别。

id_table:驱动支持设备列表,其工作机制与平台驱动中的id_table实现原理一致。

较少使用成员:

class,detect,address_list:这三个成员协同工作。class字段定义驱动支持的设备类别范围。detect回调函数负责探测总线上实际存在的设备(验证address_list中哪些设备地址有效)。

3.2 struct i2c_client

struct i2c_client {
	unsigned short flags;		    /* div., see below		*/
	unsigned short addr;		    /* chip address - NOTE: 7bit	*/
					                /* addresses are stored in the	*/
					                /* _LOWER_ 7 bits		*/
	char name[I2C_NAME_SIZE];
	struct i2c_adapter *adapter;	/* the adapter we sit on	*/
	struct device dev;		        /* the device structure		*/
	int irq;			            /* irq issued by device		*/
	struct list_head detected;
};
  • name:设备名,必须的,长度最大是19字节
  • flags:指示该I2C slave device一些特性,包括:

I2C_CLIENT_TEN indicates the device uses a ten bit chip address;

I2C_CLIENT_PEC indicates it uses SMBus Packet Error Checking

I2C_CLIENT_WAKE,该设备具备wakeup的能力。

  • addr:该设备的7-bit的slave地址。
  • adapter:该设备所在的I2C controller。
  • irq:irq number(可选)。
  • dev:其中平否数据成员platform_data可以存放i2c设备个性化信息

说明:与平台设备驱动模型的区别在于:platform_device设备结构体需要开发者自行构建,而struct i2c_client结构体并非由开发者直接实现,仅需提供构建该结构体所需的配置信息(通过i2c_board_info结构体提供),最终由内核负责动态实例化此结构体并将其注册到内核设备模型中。

3.3 struct i2c_adapter

该结构体表示一个i2c总线适配器,定义如下:

struct i2c_adapter {
	unsigned int class;		            /* classes to allow probing for */
	const struct i2c_algorithm *algo;   /* the algorithm to access the bus */
	void *algo_data;
	int timeout;			            /* in jiffies */
	int retries;
	struct device dev;		            /* the adapter device */
	int nr;
};

class:该I2C bus支持哪些类型的slave device,只有匹配的slave device才能和bus绑定。具体的类型包括(可参考include/linux/i2c.h中的定义和注释):

I2C_CLASS_HWMON:硬件监控类,如lm_sensors等;

I2C_CLASS_DDC:数字显示通道(Digital Display Channel),通常用于显示设备信息的获取;

I2C_CLASS_SPD:存储类的模组;

algo:I2C总线发送和接收数据的方法

retries,timeout:在传输失败的时候,可以选择重试。重试的逻辑由I2C core自行完成,但I2C controller driver需要设定重试的次数,这就是retries字段的意义。

nr:该I2C总线编号,一般和硬件物理上的编号相同。

3.4 struct i2c_algorithm

algorithm代表了当前I2C adapter的行为特征,必须能够描述adapter的所有传输行为。

struct i2c_algorithm {
    /*
     *master_xfer提供的是i2c_transfer实现部分。更多的I2C adapter工作于I2C总线主机模式。
     */
    int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);
    /*smbus_xfer提供i2c_smbus_xfer的实现部分。只有I2C adapter工作于SMBus模式,需要提供。
     *也就是说,I2C adapter必须指定I2C还是SMBus其中的一个。
     */
	int (*smbus_xfer) (struct i2c_adapter *adap, u16 addr,
			   unsigned short flags, char read_write,
			   u8 command, int size, union i2c_smbus_data *data);

	/* I2Cadapter的功能函数,用户自己定义*/
	u32 (*functionality) (struct i2c_adapter *);
};
  • functionality,通过一个bitmap,告诉调用者该I2Cadapter支持的功能,包括(具体可参考include/uapi/linux/i2c.h中的定义和注释):

I2C_FUNC_I2C,支持标准的I2C功能;

I2C_FUNC_10BIT_ADDR,支持10bit地址;

I2C_FUNC_PROTOCOL_MANGLING,支持非标准的协议行为;

I2C_FUNC_NOSTART,支持不需要发送START信号的I2C传输;

I2C_FUNC_SMBUS_XXX,SMBUS相关的功。

RK3399中适配器驱动:(i2c-rk3x.c)

  • master_xfef:标准I2C收发函数,输入参数是struct i2c_msg类型的数组(大小由num指定)。返回值是成功传输的msg的个数,如有错误返回负值。
  • smbus_xfer:SMBUS收发函数,如果为NULL,I2C核心层会尝试使用master_xfer模拟。

3.5 struct i2c_msg

i2c发送或者接收一次数据都以数据包struct i2c_msg封装,结构定义如下:

struct i2c_msg{
    __u16 addr;							//从机地址
    __u16 flags;						//标志
    #define I2C_M_TEN		0x0010		//十位地址标志
    #define I2C_M_RD		0x0001		//接收数据标志
    __u16 len;							//数据长度
    __u8 *buf;							//数据指针
}
  • addr,I2C slave device的地址。
  • flags,数据传输可携带的flag,包括(具体可参考include/uapi/linux/i2c.h中的定义和注释):

I2C_M_TEN,支持10-bit的slave地址;

I2C_M_RD,此次传输是读操作;

  • len,数据传输的长度,单位为byte。

3.6 struct i2c_board_info

该结构体描述一个I2C外设的配置信息,在系统启动初始化阶段,通过i2c_register_board_info函数将设备描述信息注册至全局链表__i2c_board_list;当I2C适配器注册时,内核会从__i2c_board_list中提取相应结构体信息,动态创建对应的I2C设备实例(struct i2c_client)。

注意:通过i2c_register_board_info函数实现的设备注册方式被定义为静态注册机制,此类注册方式必须直接编译进内核镜像,无法以内核模块形式(.ko文件)在系统运行时动态加载。

说明:I2C适配器驱动通常被编译进内核主体,在系统启动阶段即被加载执行。若在系统完全启动后才调用i2c_register_board_info进行注册,该操作仅会将设备信息加入内部链表,而不会触发总线重新探测流程以创建对应的i2c_client实例。内核已明确限制在可加载模块中调用此函数,因为未将此注册函数导出为内核符号:

未包含     EXPORT_SYMBOL(i2c_register_board_info);

或                 EXPORT_SYMBOL_GPL(i2c_register_board_info);        

补充:随着设备树机制在内核中的普及,i2c_register_board_info注册方式已逐渐被淘汰。采用设备树机制的内核实现已摒弃传统板级文件架构,而此类静态注册方式通常在板级支持文件中实现。

板级文件:指针对特定开发板编写的包含硬件初始化逻辑的专属源文件。

值得注意的是,尽管静态注册方式使用减少,但i2c_register_board_info函数使用的核心数据结构在动态设备注册机制中仍会被引用。

4.I2C子系统常用API

4.1 I2C适配器驱动层常用API

4.1.1 i2c_add_numbered_adapter

头文件

#include <linux/i2c.h>

原型

int i2c_add_numbered_adapter(struct i2c_adapter *adap)

参数

adap适配器结构指针

返回值

0:注册成功

负数:注册失败

功能

注册指定编号的I2C总线

4.1.2 i2c_del_adapter​​​​​​​​​​​​​​

头文件

#include <linux/i2c.h>

原型

int i2c_del_adapter(struct i2c_adapter *adap)

参数

adap适配器结构指针

返回值

0:注销成功

负数:注销失败

功能

移除已注册I2C总线

4.2 I2C设备层常用API

4.2.1 i2c_get_adapter

头文件

#include <linux/i2c.h>

原型

struct i2c_adapter *i2c_get_adapter(int nr)

参数

总线编号

返回值

NULL:没有找到指定总线编号的i2c_adapter结构;

非NULL:指定nr的适配器结构内存地址;

功能

通过i2c总线编号获得内核中的i2c_adapter结构地址,用户就可以使用这个结构地址给i2c_client结构使用,从而实现i2c_client进行总线绑定,同时还会增加适配器引用计数。

减少引用计数:当使用i2c_get_adapter后,需要使用该函数来减少引用计数。

4.2.2 i2c_put_adapter

头文件

#include <linux/i2c.h>

原型

void i2c_put_adapter(struct i2c_adapter *adap)

参数

adap适配器结构指针

功能

使用i2c_get_adapter获得适配器后会增加它的引用计数,当不使用适配器需要减少引用计数。

4.2.3 i2c_new_probed_device & i2c_new_device(动态创建i2c_client函数)

头文件

#include <linux/i2c.h>

原型

struct i2c_client *i2c_new_device(struct i2c_adapter *adap, struct i2c_board_info const *info);

struct i2c_client *i2c_new_probed_device(struct i2c_adapter *adap, struct i2c_board_info *info, unsigned short const *addr_list, int (*probe)(struct i2c_adapter *, unsigned short addr));

参数

adap适配器结构指针,需要调用传入,可以通过i2c_get_adapter函数获得

info除地址不需要指定以外设备的信息,地址是通过在探测成功后把成功的地址填入其中。

addrlist设备可能出现的所有地址列表,指向一个short数组,这个数组结束元素必须是I2C_CLIENT_END;如AT24C02设备可能出现的地址:101 0XYZ

probe回调函数,一般不需要传入,NULL

返回值

非NULL:成功创建了i2c_client结构地址

NULL:失败

功能

创建info,adap根据在addr_list地址探测一个存在的设备,然后为它创建i2c_client结构,并且注册。

4.2.4 i2c_unregister_device

头文件

#include <linux/i2c.h>

原型

void i2c_unregister_device(struct i2c_client *client)

参数

client:i2c设备结构指针

功能

注销I2C客户端设备

4.3 I2C驱动层API

4.3.1 i2c_add_driver

头文件

#include <linux/i2c.h>

原型

#define i2c_add_driver(driver) i2c_register_driver(THIS_MODULE, driver)

参数

已经初始化的i2c_driver结构指针

返回值

0:成功

负数:注册失败

功能

注册I2C设备驱动

4.3.2 i2c_del_driver

头文件

#include <linux/i2c.h>

原型

void i2c_del_driver(struct i2c_driver *driver)

参数

driver:指定的i2c_driver对象的地址

功能

注销指定的i2c_driver对象

4.3.3 i2c_master_send

头文件

#include <linux/i2c.h>

原型

int i2c_master_send(const struct i2c_client *client,const char *buf,int count);

参数

const struct i2c_client *client:代表从的i2c_client对象的地址

const char *buf:数据缓存的首地址

int count:待发送的数据的字节数(包含从设备的地址)

返回值

成功:>0,表示发送的字节数

失败:负数

功能

主机向从机发送指定字节的数据

示例:向AT24C02内部地址0x10开始写入8个数据1~8。


​​​​​

char buf[9]={0x10,1,2,3,4,5,6,7,8};
i2c_master_send(client,buf,9);

4.3.4 i2c_master_recv

头文件

#include <linux/i2c.h>

原型

int i2c_master_recv(const struct i2c_client *client,const char *buf,int count);

参数

const struct i2c_client *client:代表从的i2c_client对象的地址

const char *buf:数据缓存的首地址

int count:待读取的数据的字节数(包含从设备的地址)

返回值

成功:>0,表示接收的字节数

失败:负数

功能

主机向从机接收指定字节的数据

示例:向AT24C02内部地址0x10开始读取8个数据1~8。

上面时序看成IIC基本时序:由一个写单字节时序+N字节读时序构成。

核心代码片段:

int ret;
u8 subaddr = 0x10;					        //内部地址
char buf[100]={0};
ret = i2c_master_send(client,&subaddr,1);	//先发内部地址,在IIC看就是写了一个字节普通数据
if(ret << 0) {……}
ret = i2c_master_recv(client,buf,16);		//接收16字节数据
if(ret < 0) {……}

4.3.5 i2c_transfer

头文件

#include <linux/i2c.h>

原型

int i2c_transfer(struct i2c_adapter *adap,struct i2c_msg *msgs, int num);

参数

struct i2c_adapter *adap:从机挂载的适配器,表示:i2c_client对象所挂载的i2c_adapter对象的地址

struct i2c_msg * msgs:待传输的数据缓存的首地址

int num:指定了msgs数组中元素的个数,即要执行的消息数量

返回值

成功:>0,表示成功传输的msgs数组的个数

失败:负数

功能

主机向从机发送或接收指定字节数的数据

示例:向AT24C02内部地址0x10开始读取16个字节数据。

上面时序看成IIC基本时序:由一个写单字节时序+N字节读时序构成。

核心代码片段:

int ret;
char subaddr = 0x10;			//内部地址
struct i2c_msg msg[2];
char buf[100]={0};
msg[0].addr = client->addr;
msg[0].flags = 0;				//7位地址,写数据
msg[0].len = 1;				//只发送1字节数据,对AT24C02来说就是发送内部地址
msg[0].buf = (char *)&subaddr;	//要发送的内容地址,对AT24C02来说就是发送内部地址
msg[1].addr = client->addr;
msg[1].flags = I2C_M_RD;	//7位地址,写数据
msg[1].len = 16;				//要读的数据数量16字节
msg[1].buf = (char *)buf;		//存放结果

ret = i2c_transfer(client->adapter, msg, 2);
if(ret < 0) {……}

4.4 SMBus读写常用API

SMBus由Intel公司研发,可视为I2C总线协议的一个子协议集。在多数应用场景中,SMBus操作接口可用于控制标准I2C设备,二者具备良好的兼容性,Linux内核为SMBus实现了一系列完备的数据访问接口。

4.4.1 i2c_smbus_read_byte

头文件

#include <linux/i2c.h>

原型

s32 i2c_smbus_read_byte(const struct i2c_client*client)

参数

client:I2C设备结构指针

返回值

读取回来的值

功能

从当前位置读取数据,不设置内部地址

4.4.2 i2c_smbus_write_byte

头文件

#include <linux/i2c.h>

原型

s32 i2e_smbus_write_byte(const struct i2c_client *client, u8 value)

参数

client:I2C设备结构指针

value:要写入的数据

返回值

0:成功

负数:失败

功能

在当前位置写数据,不设置内部地址

​​​​​​​4.4.3 i2c_smbus_read_byte_data

头文件

#include <linux/i2c.h>

原型

s32 i2c_smbus_read_byte_data(const struct i2c_client *client, u8 command)

参数

client:I2C设备结构指针

command:要读的内部地址

返回值

读取回来的值

功能

读取指定位置的数据(1字节)

示例:向AT24C02内部地址0x10开始读取16个字节数据。

IIC基本时序:由一个写单字节时序+N字节读时序构成。

核心代码片段:​​​​​​​

s32 ret;
char buf[100];
for(i = 0;i < 16;i++){
	ret = i2c_smbus_read_byte_data(client, 0x10+i);
	if(ret < 0){
	
    }
    buf[i] = ret & 0xff;
}

4.4.4 i2c_smbus_write_byte_data

头文件

#include <linux/i2c.h>

原型

s32 i2c_smbus_write_byte_data(const struct i2c_client *client, u8 command, u8 value)

参数

client:I2C设备结构指针

command:要写的内部地址

value:要写入的内容

返回值

0:成功

负数:失败

功能

往指定位置写数据(1字节)

示例:向AT24C02内部地址0x10开始写入8个数据1~8。

char buf[9]={1,2,3,4,5,6,7,8};
for(i = 0;i < 8;i++){
	ret = i2c_smbus_write_byte_data(client,0x10+i;buf[i]);
	if(ret < 0){
	
    }
}

4.4.5 ​​​​​​​i2c_smbus_read_word_data

头文件

#include <linux/i2c.h>

原型

s32 i2c_smbus_read_word_data(const struct i2c_client *client, u8 command)

参数

client:I2C设备结构指针

command:要读的内部地址

返回值

读取回来的值

功能

读取指定位置的数据(2字节)

4.4.6 i2c_smbus_write_word_data

头文件

#include <linux/i2c.h>

原型

s32 i2c_smbus_write_word_data(const struct i2c_client *client, u8 command, u16value)

参数

client:I2C设备结构指针

command:要写的内部地址

value:要写入的内容

返回值

0:成功

负数:失败

功能

往指定位置写数据(2字节)

4.4.7 i2c_smbus_read_i2c_block_data

头文件

#include <linux/i2c.h>

原型

s32 i2c_smbus_read_i2c_block_data(const struct i2c_client *client, u8 command, u8 length, u8 *values);

参数

client:I2C设备结构指针

command:要读的内部地址

values:存放读取回来的数据的缓冲区首地址

length:要读取的字节数量

返回值

成功读取到的字节数量

功能

读取指定位置指定数量的数据(最大32字节)

示例:向AT24C02内部地址0x10开始读取16个字节数据。

IIC基本时序:由一个写单字节时序+N字节读时序构成。

核心代码片段:

s32 ret;
char buf[100];
ret = i2c_smbus_read_i2c_block_data(client, 0x10, 16, buf);
if(ret < 0){
	
}

4.4.8 i2c_smbus_write_2c_block_data

头文件

#include <linux/i2c.h>

原型

s32 i2c_smbus_write_i2c_block_data(const struct i2c_client *client, u8 command, u8 length, const u8 *values)

参数

client:I2C设备结构指针

command:要写的内部地址

values:数据的缓冲区首地址

length:要读取的字节数量

返回值

成功写入到的字节数量

功能

往指定位置指定数量的数据(最大32字节)

示例:向AT24C02内部地址0x10开始写入8个数据1~8。

char buf[9]={1,2,3,4,5,6,7,8};
i2c_smbus_write_i2c_block_data(client, 0x10, 8, buf);
if(ret < 0){
	
}

    5.I2C编程

      5.1 I2C驱动层框架

      i2c_driver层实现代码框架,代码如下所示:

      #include <linux/kernel.h>
      #include <linux/module.h>
      #include <linux/i2c.h>
      #include <linux/fs.h>
      #include <linux/uaccess.h>
      #include <linux/delay.h>
      #include <linux/miscdevice.h>
      #include <linux/slab.h>
      
      static struct i2c_client *clt;		//必须有,全局变量
      
      //定义探测设备函数---设备与驱动匹配成功后便会执行
      static int at24_probe(struct i2c_client *client, const struct i2c_device_id *id)
      {
      	int ret;
      
      	printk("line:%d, %s is call\r\n", __LINE__,__FUNCTION__);	
      	printk("name:%s,addr:0x%x\r\n",client->name,client->addr);
      	printk("id->name:%s,id->driver_data:%lu\r\n",id->name,id->driver_data);
      
      	//一定要做的事情,否则后面实现文件操作方法需要调用读写函数时就没有i2c_client	
      	clt = client;
      
      	//如果设备需要进行初始化,这里可以调用i2c子系统的读写API来对设备进行初始化
      	//......
      	
      	//如果设备使用到中断信号,这里可以注册中断(触摸屏)
      
      	//给用户空间提供访问设备入口:注册杂项设备,早期标准字符设备,Linux 2.6标准字符设备,input设备
      
      	return 0;
      
      error_misc_register:
      	return ret;
      }
      
      static int at24_remove(struct i2c_client * client)
      {
      	printk("line:%d, %s is call\r\n",__LINE__,__FUNCTION__);
      	printk("name:%s,addr:0x%x\r\n",client->name,client->addr);
      	
      	//如果probe函数中注册杂项设备、标准字符设备、input设备,这里进行注销
      	
      	//如果probe函数中注册中断,在这里注销中断
      	
      	//如果probe分配资源,在这里分配资源
      	return 0;
      }
      
      //驱动支持的设备列表
      static const struct i2c_device_id at24cxx_table[]={
      	[0]={
      		.name = "at24c04",
      		.driver_data = 512,//可以填充一个结构体变量地址,其中包含容量和页大小信息
      	},
      	
      	[1]={
      		.name = "at24c02",
      		.driver_data = 256,//可以填充一个结构体变量地址,其中包含容量和页大小信息
      	},
      	//......
      };
      
      static struct i2c_driver at24cxx_driver = {
      	.probe  = at24_probe,
      	.remove = at24_remove,
      	.driver = {
      		.name = "at24cxx"
      	},
      	.id_table = at24cxx_table
      };
      
      //驱动初始化函数
      static int at24c02_drv_init(void)
      {
      	int ret;
      	ret = i2c_add_driver(&at24cxx_driver);
      	if(ret < 0){
      		printk("error i2c_add_driver\r\n");
      		return ret;
      	}
      	printk("i2c_add_driver success\r\n");
      	return 0;
      }
      
      //驱动卸载函数
      static void at24c02_drv_exit(void)
      {
      	i2c_del_driver(&at24cxx_driver);
      }
      
      module_init(at24c02_drv_init);
      module_exit(at24c02_drv_exit);
      MODULE_LICENSE("GPL");
      MODULE_AUTHOR("LIU");
      

      上面只是一个代码框架,并没有注册具体的字符设备模型,需要根据设备的特点选择合适设备模型注册,如触摸屏,三轴加速传感器可以注册input设备,eeprom,光传感器,温度传感器....可以注册杂项设备,标准字符设备。

      5.2 I2C设备读写AT24C02示例

      at24cxx_clt.c

      #include <linux/module.h>
      #include <linux/kernel.h>
      #include <linux/i2c.h>
      
      static struct i2c_client *clt;
      static struct i2c_adapter *adap;
      
      static int at24c02_dev_init(void)
      {
      	struct i2c_board_info info = {
      		.type  = "at24c02",
      		.addr  = 0x50,			//纯地址,不带方向位:0xA0>>1,方法一需要实现
      		.flags = 0,
      	};
      
      	//获得适配器指针
      	adap = i2c_get_adapter(2);	//IIC2
      	if(adap == NULL){
      		printk("error i2c_get_adapter\r\n");
      		return -ENXIO;
      	}
      	
      	//创建设备并且注册
      #if 0
      	//方法一
      	clt = i2c_new_device(adap, &info);
      	if(clt == NULL){
      		printk("error i2c_new_device\r\n");
      		return -EINVAL;
      	}
      	printk("i2c_new_device suceess\r\n");
      #endif
      
      	//方法二
      	//可能的地址列表
      	unsigned short const addr_list[]={0x50,0x51,0x52,0x53,0x54,0x55,0x56,0x57,I2C_CLIENT_END};
      	clt = i2c_new_probed_device(adap,&info,addr_list,NULL);
      	if(clt == NULL){
      		printk("error i2c_new_probed_device\r\n");
      		return - EINVAL;
      	}
      	printk("addr:0x%x\r\n",info.addr);
      	printk("i2c_new_probed_device success\r\n");
      	return 0;
      }
      
      static void at24c02_dev_exit(void)
      {
      	i2c_unregister_device(clt);		//注销设备
      	i2c_put_adapter(adap);			//释放i2c适配器
      }
      
      module_init(at24c02_dev_init);
      module_exit(at24c02_dev_exit);
      MODULE_LICENSE("GPL");
      MODULE_AUTHOR("LIU");
      

      at24cxx_drv.c

      #include <linux/kernel.h>
      #include <linux/module.h>
      #include <linux/i2c.h>
      #include <linux/fs.h>
      #include <linux/uaccess.h>
      #include <linux/delay.h>
      #include <linux/miscdevice.h>
      #include <linux/slab.h>
      
      static struct i2c_client *clt;
      static u32 eeprom_size;
      
      //对应于系统调用 off_t lseek(int fd,off_t offset,int whence)函数
      static loff_t at24cxx_llseek(struct file *pfile, loff_t offset , int whence)
      {
      	loff_t new_pos;
      	printk("line:%d, %s is call\rIn", __LINE__,__FUNCTION__);
      	switch(whence){
      		case SEEK_SET:
      			new_pos = offset;
      			break;
      		case SEEK_CUR:
      			new_pos = pfile->f_pos + offset;
      			break;
      		case SEEK_END:
      			new_pos = eeprom_size + offset;
      			break;
      		default:
      			return -EINVAL;
      			break;
      	}
      
      	//检测最终于的文件偏移是否合法
      	if (new_pos < 0 || new_pos > eeprom_size)
      		return -EINVAL;
      	//修改文件偏移量
      	pfile->f_pos = new_pos;
      	return pfile->f_pos;
      }
      
      //对应于系统调用 ssize_t read(int fd, void *buf, size_t count); 函数
      static ssize_t at24cxx_read(struct file *pfile, char __user *buf, size_t count, loff_t *offset)
      {
      	int ret;
      	u8 *kbuf;
      	loff_t cur_pos = *offset;
      	char subaddr = cur_pos & 0xff;
      	struct i2c_msg msg[2];
      
      	printk("line:%d, %s is call\r\n",__LINE__,__FUNCTION__);
      
      	if(count + cur_pos > eeprom_size)	count = eeprom_size - cur_pos;
      	if(count == 0) 						return 0;
      
      	//根据用户空间传递的数据数量,分配缓冲区
      	kbuf = kzalloc(count,GFP_KERNEL);
      	if(buf == NULL){
      		printk("error kzalloc\r\n");
      		ret = -ENOMEM;
      		goto error_kzalloc;
      	}
      	
      	//从eeprom中读取数据到kbuf
      	msg[0].addr = clt->addr;		//设备地址
      	msg[0].flags = 0;				//7位地址,写数据
      	msg[0].len = 1; 				//只发送1字节数据,对AT24C02来说就是发送内部地址
      	msg[0].buf = (char *)&subaddr;	//要发送的内容地址,对AT24C02来说就是发送内部地址
      
      	msg[1].addr  = clt->addr;		//设备地址
      	msg[1].flags = I2C_M_RD;		//7位地址读操作
      	msg[1].len   = count;			//10字节数据
      	msg[1].buf   =(char *)kbuf;
      	
      	ret = i2c_transfer(clt->adapter, msg, 2);
      	if(ret < 0){
      		printk("error i2c_transfer\r\n");
      		goto error_i2c_transfer;
      	}
      
      	//把kbuf数据复制用户空间
      	ret = copy_to_user(buf,kbuf,count);
      	if(ret){
      		printk("error copy_to_user\r\n");
      		goto error_copy_to_user;
      	}
      	*offset += count;	//修改偏移量
      	kfree(kbuf);		//释放空间
      	msleep (5);
      	return count;
      error_copy_to_user:
      error_i2c_transfer:
      	kfree (kbuf);
      error_kzalloc:
      	return ret;
      }
      
      //当前位置使用offset表示
      //对应于系统调用ssize_t write(int fd,const void *buf,size_t count);函数
      static ssize_t at24cxx_write (struct file *pfile,const char __user *buf, size_t count, loff_t *offset)
      {
      	int i;
      	int ret ;
      	u8 *kbuf;
      	loff_t cur_pos = *offset;			//取得当前地址(当前内部地址
      
      	printk("line:%d,%s is call\r\n",__LINE__,__FUNCTION__);
      
      	if(count + cur_pos > eeprom_size)	count = eeprom_size - cur_pos;
      	if(count == 0)						return 0;
      
      	//根据用户空间传递的数据数量,分配缓冲区
      	kbuf = kzalloc(count,GFP_KERNEL);
      	if (buf == NULL){
      		printk("erro kzalloc\r\n");
      		ret = -ENOMEM;
      		goto error_kzalloc;
      	}
      
      	//把用户空间的数据复制内核空间
      	ret = copy_from_user (kbuf, buf,count);
      	if(ret){
      		printk("erro copy_from_user\r\n");
      		ret = -EFAULT;
      		goto error_copy_from_user;
      	}
      	
      	for(i = 0; i < count; i++){
      		ret = i2c_smbus_write_byte_data(clt,cur_pos+i, kbuf[i]);
      		if(ret < 0)
      			goto error_i2c_smbus_write_byte_data;
      		msleep(5);		//测试正确再把值修改为更小的时间延时
      	}
      	
      	*offset += count;	//修改偏移量
      	kfree(kbuf);		//释放空间
      
      	msleep(5);
      	return count;
      	
      error_i2c_smbus_write_byte_data:
      error_copy_from_user:
      	kfree(kbuf);
      error_kzalloc:
      	return ret;
      }
      
      //对应于系统调用的intopen(const char *pathname,int flags);
      static int at24cxx_open(struct inode *pinode, struct file *pfile)
      {
      	printk("line:%d,%s is call\r\n",__LINE__,__FUNCTION__);
      	return 0;
      }
      
      //对应于系统调用的intclose(intfd);
      static int at24cxx_release(struct inode *pinode, struct file *pfile)
      {
      	printk("line:%d,%s is call\r\n",__LINE__,__FUNCTION__);
      	return 0;
      }
      
      static struct file_operations at24cxx_ops = {
      	.owner   = THIS_MODULE,
      	.open    = at24cxx_open,
      	.release = at24cxx_release,
      	.read    = at24cxx_read,
      	.write   = at24cxx_write,
      	.llseek  = at24cxx_llseek,
      };
      
      static struct miscdevice at24cxx_dev = {
      	.minor = 255,			//自动分配一个可用次设备号
      	.name  = "at24cxx",		//设备名
      	.fops  = &at24cxx_ops,	//设备的操作方法
      };
      
      //定义探测设备函数---设备与驱动匹配成功后便会执行
      static int at24_probe(struct i2c_client *client, const struct i2c_device_id *id)
      {
      	int ret;
      
      	printk("line:%d, %s is call\r\n", __LINE__,__FUNCTION__);	
      	printk("name:%s,addr:0x%x\r\n",client->name,client->addr);
      	printk("id->name:%s,id->driver_data:%lu\r\n",id->name,id->driver_data);
      
      	//一定要做的事情,否则后面实现文件操作方法需要调用读写函数时就没有i2c_client	
      	clt = client;
      
      	//如果设备需要进行初始化,这里可以调用i2c子系统的读写API来对设备进行初始化
      	//AT24CXX不需要对硬件初始化
      	eeprom_size = id->driver_data;
      
      	//如果设备使用到中断信号,这里可以注册中断
      
      	//给用户空间提供访问设备入口:注册杂项设备,早期标准字符设备,Linux 2.6标准字符设备,input设备
      
      	ret = misc_register(&at24cxx_dev);
      	if(ret < 0){
      		printk("error misc_register\r\n");
      		goto error_misc_register;
      	}
      	printk("device name:/dev/%s\r\n",at24cxx_dev.name);
      
      	return 0;
      
      error_misc_register:
      	return ret;
      }
      
      static int at24_remove(struct i2c_client * client)
      {
      	printk("line:%d, %s is call\r\n",__LINE__,__FUNCTION__);
      	printk("name:%s,addr:0x%x\r\n",client->name,client->addr);
      	misc_deregister(&at24cxx_dev);
      	return 0;
      }
      
      //驱动支持的设备列表
      static const struct i2c_device_id at24cxx_table[]={
      	[0]={
      		.name = "at24c04",
      		.driver_data = 512,
      		//可以填充一个结构体变量地址,其中包含容量和页大小信息
      	},
      	
      	[1]={
      		.name = "at24c02",
      		.driver_data = 256,
      		//可以填充一个结构体变量地址,其中包含容量和页大小信息
      	},
      };
      
      static struct i2c_driver at24cxx_driver = {
      	.probe  = at24_probe,
      	.remove = at24_remove,
      	.driver = {
      		.name = "at24cxx"
      	},
      	.id_table = at24cxx_table
      };
      
      //驱动初始化函数
      static int at24c02_drv_init(void)
      {
      	int ret;
      	ret = i2c_add_driver(&at24cxx_driver);
      	if(ret < 0){
      		printk("error i2c_add_driver\r\n");
      		return ret;
      	}
      	printk("i2c_add_driver success\r\n");
      	return 0;
      }
      
      //驱动卸载函数
      static void at24c02_drv_exit(void)
      {
      	i2c_del_driver(&at24cxx_driver);
      }
      
      module_init(at24c02_drv_init);
      module_exit(at24c02_drv_exit);
      MODULE_LICENSE("GPL");
      MODULE_AUTHOR("LIU");
      

      app.c

      #include <stdio.h>
      #include <stdlib.h>
      #include <unistd.h>
      #include <fcntl.h>
      #include <time.h>
      #include <string.h>
      
      #define DEVNAME "/dev/at24cxx"  // 默认设备节点路径
      
      int main(int argc, char **argv)
      {
      	const char *path = DEVNAME; 	// 设备路径指针(初始化为默认路径)
          int fd,len;
          char buf[128] = {0};
          char read_buf[128] = {0};
          time_t t;
          struct tm *tm_info;
          
          if(argc == 1)
              path = DEVNAME;
          else if(argc == 2)
              path = argv[1];
          else {
              printf("Usage:%s [/dev/devname]\r\n", argv[0]);
              return 0;
          }
          // 打开设备
          fd = open(path, O_RDWR);  	// O_RDWR:以读写模式打开
          if (fd < 0) {
              perror("open");
              return -1;
          }
          
          // 获取当前时间
          time(&t);
          tm_info = localtime(&t);
          strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", tm_info);
          len = strlen(buf);
          
          printf("Writing: %s\r\n", buf);
          
          // 写入数据
          if (write(fd, buf, len) < 0) {
              perror("write");
              close(fd);
              return -1;
          }
          
          // 等待写入完成
          usleep(10000);  // 10ms
          
          // 重置文件指针到开始位置
          lseek(fd, 0, SEEK_SET);
          
          // 读取相同长度的数据
          if (read(fd, read_buf, len) < 0) {
              perror("read");
              close(fd);
              return -1;
          }
          read_buf[len] = '\0';  // 确保字符串结束
          
          printf("Stored time: %s\r\n", read_buf);
          printf("Current time: %s\r\n", buf);
          
          close(fd);
          return 0;
      }
      

      现象

      说明:

      如果时间不对可能是没选择时区,可以调整时区:

      cat /etc/localtime

      TZif2UTCTZif2▒UTC

      UTC0

      echo "Asia/Shanghai" > /etc/timezone
      cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
      ### Linux I2C 子系统的工作原理 在 Linux 内核中,I2C(Inter-Integrated Circuit)子系统用于管理硬件设备之间的串行通信协议。该子系统的算法详细描述了如何在物理层上操作 I2C 适配器,以执行诸如数据读写、起始和停止条件的发送等关键任务[^3]。 #### 主要组件 1. **驱动程序**:负责与特定的 I2C 设备交互。 2. **总线控制器**:控制 I2C 总线上所有的传输活动。 3. **客户端设备**:连接到 I2C 总线上的各个外设。 #### 数据流过程 当应用程序发起一次 I2C 操作时,请求会通过字符设备接口传递给相应的驱动模块。随后,驱动模块调用底层函数来启动实际的数据交换流程,在此期间可能会涉及到地址匹配、命令发送以及响应接收等一系列动作。 --- ### 配置方法 配置 Linux 下的 I2C 接口主要涉及以下几个方面: #### 加载必要的内核模块 确保已经加载了支持目标平台所需的 I2C 控制器驱动及其关联的设备驱动。可以通过 `modprobe` 或者直接编辑 `/etc/modules-load.d/` 文件夹下的配置文件来进行静态加载。 ```bash sudo modprobe i2c-dev ``` #### 修改 DTS (Device Tree Source) 对于 ARM 架构而言,还需要调整 DeviceTree 来声明新的 I2C 节点并指定参数属性。这一步骤通常由板级支持包提供预定义模板供开发者修改使用。 #### 编辑 udev 规则 为了让用户空间能够方便地访问新添加的 I2C 设备节点,建议创建自定义 UDev 规则以便自动分配合适的权限。 ```bash echo 'SUBSYSTEM=="i2c-dev", MODE="0666"' | sudo tee /etc/udev/rules.d/i2c.rules ``` --- ### 故障排除指南 遇到无法正常工作的 I2C 连接问题时,可以从以下几个角度入手解决: #### 日志分析 查看 dmesg 输出中的错误提示信息可以帮助定位具体哪一部分出现了异常状况;同时也可以利用 `journalctl -k` 命令获取更详细的日志记录。 #### 测试工具应用 借助于专门设计用来调试 I2C 的实用程序如 `i2cdetect`, 可以扫描整个总线寻找在线设备,并验证它们是否能被正确识别出来。 ```bash sudo apt-get install i2c-tools sudo i2cdetect -y 1 ``` #### 参数微调 有时适当调节某些高级选项比如时钟频率或者拉高电阻值也有可能改善通讯质量不佳的情况。这类改动往往需要深入研究具体的芯片手册才能做出合理的选择。
      评论
      添加红包

      请填写红包祝福语或标题

      红包个数最小为10个

      红包金额最低5元

      当前余额3.43前往充值 >
      需支付:10.00
      成就一亿技术人!
      领取后你会自动成为博主和红包主的粉丝 规则
      hope_wisdom
      发出的红包
      实付
      使用余额支付
      点击重新获取
      扫码支付
      钱包余额 0

      抵扣说明:

      1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
      2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

      余额充值