1.I2C总线知识
1.1 I2C总线物理拓扑结构

I2C总线的物理拓扑结构极为简洁,主要由SDA(串行数据线)、SCL(串行时钟线)以及上拉电阻(阻值范围4.7K-100K)构成。其电气特性是通过精确控制SCL与SDA信号线的高低电平时序序列,生成符合I2C总线协议规范的通信信号,从而完成数据传输任务。当总线处于闲置状态时,由于上拉电阻的作用,SDA与SCL信号线均被拉至高电平,维持总线的空闲状态。
1.2 I2C总线特征
-
I2C总线架构支持每个连接设备在不同场景下担任主控或从属角色。
-
每个设备均分配有特定的唯一标识符(具体数值可从对应I2C器件技术手册查询),主从设备间的通信目标正是通过此标识符确定。
-
在常规应用场景中,CPU内置的I2C控制器通常配置为主控设备,而连接至总线的其余外围设备则配置为从属设备。
-
I2C总线允许连接的设备总数受限于总线最大负载电容400pF;若连接的为同类型器件,则额外受限于器件地址编码的可配置范围。
-
I2C协议支持7位与10位两种地址格式,地址类型可分为固定硬件地址和软件可配置地址。同一条I2C总线上允许7位地址设备与10位地址设备共存。
-
I2C总线的数据传输速率在标准模式下最高达100kbit/s,快速模式下可达400kbit/s,高速模式下可提升至3.4Mbit/s。传输速率通常通过配置I2C控制器的可编程时钟分频器实现调节。
-
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设备驱动层。架构组成关系如图所示:

-
I2C子系统采用三层架构设计:设备驱动层,核心层,控制器驱动层
-
设备驱动层:负责特定I2C外设的功能实现(依据器件规格书实现通信时序及控制逻辑),此组件可进一步细分为:I2C外设描述层(类比平台设备模型)和I2C外设驱动层(类比平台驱动模型),分别通过i2c_client和i2c_driver数据结构实现。(这一层是驱动开发人员的核心工作区域)
-
核心层:承担中间桥梁职能,向上层应用提供统一的设备操作API,向下层硬件提供控制器驱动接口。此层实现完全硬件抽象,具备跨平台特性,通常无需开发人员干预。
-
控制器驱动层:内核通过i2c_adapter结构体表征I2C控制器实例,负责实现特定I2C控制器的硬件操作,包括标准I2C通信协议的时序控制及数据收发功能。此组件通常由芯片制造商提供支持。例如RK3399平台的控制器驱动由瑞芯微公司提供。
-
I2C子系统分层架构带来两大优势:
-
特定外设驱动可实现硬件平台无关性,显著提升代码可移植性;
-
内置总线仲裁机制,有效处理多设备对总线的并发访问冲突。
-
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
3265

被折叠的 条评论
为什么被折叠?



