I2C总线架构 之 设备驱动
引言
I2C设备驱动是I2C框架中最接近应用层的,其上接应用层,下接I2C核心。也是驱动开发人员需要实现的代码,在此驱动中我们只需负责以下步骤(以ap3216c为例):
a. 添加硬件信息(设备树)
b. 搭建驱动框架
c. 构建i2c_driver,并注册到linux i2c中
d. 注册字符设备
e. 向应用层提供i2c设备操作接口
f. 注销i2c设备
本篇文章会按照以上六个阶段展开解析。
流程解析
a. 添加硬件信息设备树(设备树)
首先观察硬件i2c设备挂载到哪个i2c总线上,然后在设备树文件找到该总线的设备节点,在节点下创建子节点描述i2c设备硬件信息即可。
&i2c1 {
clock-frequency = <100000>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c1>;
status = "okay";
ap3216c@1e {
compatible = "100ask,ap3216c";
reg = <0x1e>;
};
};
b. 搭建驱动框架
所谓搭建驱动框架,无非就是字符驱动将驱动入口、出口、以及对应用层的接口实现。与其他字符驱动的搭建是一样的。
c. 构建i2c_driver,并注册到linux
首先先看需要构建的i2c_driver结构体原型:
struct i2c_driver {
unsigned int class;
int (*attach_adapter)(struct i2c_adapter *) __deprecated;
int (*probe)(struct i2c_client *, const struct i2c_device_id *);
int (*remove)(struct i2c_client *);
void (*shutdown)(struct i2c_client *);
void (*alert)(struct i2c_client *, enum i2c_alert_protocol protocol,
unsigned int data);
int (*command)(struct i2c_client *client, unsigned int cmd, void *arg);
struct device_driver driver;
const struct i2c_device_id *id_table;
int (*detect)(struct i2c_client *, struct i2c_board_info *);
const unsigned short *address_list;
struct list_head clients;
};
注: i2c_driver类似于platform_driver,在i2c_driver注册到内核且名称与设备树匹配一致就会进入probe中,在要卸载该驱动时会进入remove中。因此要填充i2c_driver的入口函数probe、出口函数(remove)和用于匹配的硬件信息driver。
static struct i2c_driver ap3216c_device_driver = {
.probe = ap3216c_probe,
.remove = ap3216c_remove,
.driver = {
.name = PLATFORM_NAME,
.owner = THIS_MODULE,
.of_match_table = ap3216c_table,
},
.id_table = ap3216c_id,
};
d. 注册i2c设备
static int __init ap3216c_init(void)
{
int ret = 0;
printk("%s:%d: Entry %s rn", __FILE__, __LINE__, __func__);
ret = i2c_add_driver(&ap3216c_device_driver);
return ret;
}
注册i2c设备很简单,只需要在初始化中调用Linux提供的宏i2c_add_driver 即可。但是i2c_add_driver具体如何实现,有必要了解一下:
首先,这个宏调用了i2c_register_driver:
#define i2c_add_driver(driver)
i2c_register_driver(THIS_MODULE, driver)
再大致了解i2c_register_driver函数流程:
--- drivers --- i2c_core.c --- i2c_register_driver( --- driver->driver.bus = &i2c_bus_type
| struct module *owner, |- INIT_LIST_HEAD(&driver->clients)
| struct i2c_driver *driver) |- driver_register(&driver->driver)
| |- i2c_for_each_dev(driver, __process_new_driver)
|- i2c_for_each_dev( --- bus_for_each_dev(&i2c_bus_type, NULL, data, fn)
| void *data,
| int (*fn)(struct device *, void *))
|- __process_new_driver( --- if (dev->type != &i2c_adapter_type)
struct device *dev, | return 0;
void *data) |- i2c_do_add_adapter(data, to_i2c_adapter(dev));
小结:
从上图流程来看,在将i2c结构体注册进内核时,调用了属于i2c核心的i2c_register_driver。进入i2c核心中,会将i2c结构体添加到i2c链表中,并实现i2c_client与i2c_driver的匹配,匹配成功会进入i2c_driver 结构体的probe函数中。(具体实现放在I2C核心文章分析)
e. 向应用层提供i2c设备操作接口
成功进入probe函数后,就说明i2c驱动配置基本成功。接下来在probe中需要实现字符驱动的注册,以及实现对外的读写接口。字符驱动的注册代码,与其他字符驱动是一致的,浏览代码实现即可。主要分析对外接口的读写i2c设备操作:
在单片机的程序中,实现对i2c设备的读写,需要手动实现读写i2c寄存器,或者通过GPIO模拟i2c时序与i2c设备通信。而在Linux中,如何与i2c设备的具体通信已经被封装成固定的API,在程序中填充这些API的数据参数调用即可,列举读写单个字节的实现:
static int ap3216c_read_regs(struct sap3216c_dev *dev, unsigned char reg,
void *val, int len)
{
int ret;
struct i2c_msg msg[2];
struct i2c_client *client = (struct i2c_client *)dev->private_data;
msg[0].addr = client->addr;
msg[0].flags = 0;
msg[0].buf = ®
msg[0].len = 1;
msg[1].addr = client->addr;
msg[1].flags = I2C_M_RD;
msg[1].buf = val;
msg[1].len = len;
ret = i2c_transfer(client->adapter, msg, 2);
if (ret ==2 ) {
ret = 0;
} else {
printk("%s %d: i2c transfer error!n ", __func__, __LINE__);
ret = -EREMOTEIO;
}
return ret;
}
static int ap3216c_write_regs(struct sap3216c_dev *dev, unsigned char reg,
unsigned char *buf, unsigned char len)
{
unsigned char temp_buf[256];
struct i2c_msg msg;
struct i2c_client *client = (struct i2c_client *)dev->private_data;
temp_buf[0] = reg;
memcpy(&temp_buf[1], buf, len);
msg.addr = client->addr;
msg.flags = 0;
msg.buf = temp_buf;
msg.len = len+1;
return i2c_transfer(client->adapter, &msg, 1);
}
小结:
由以上代码发现,在与i2c设备的读写通信中,都是通过调用i2c_transfer实现。
i2c_transfer三个参数意义 :
(1) client->adapter: 该i2c设备连接的i2c总线适配器;
(2) msg:需要发送的数据;
(3) 1:需要发送的msg个数。
通过以上读写的实现,与上一篇文章 《I2C总线架构 之 I2C协议》 读写时序是对应的:
(1) 写操作只需要一个msg结构体:
- 起始位 + 写操作(msg[0]) + 停止位。
(2) 读操作需要两个msg结构体 :
- 起始位+ 写操作(写入地址 msg[0])+ 起始位 + 读操作(存入msg[1]) + 停止位。
f. 注销i2c设备
注销操作:在字符驱动出口函数中,卸载掉注册的i2c设备。这里调用i2c_del_driver即可实现,与i2c_add_driver是对应的。
static void __exit ap3216c_exit(void)
{
printk("%s:%d: Entry %s rn", __FILE__, __LINE__, __func__);
i2c_del_driver(&ap3216c_device_driver);
}
总结
到这里本篇文章对i2c设备驱动的具体分析基本完成。本篇以ap3216c光敏传感器代码为例,从入口到出口代码走向展开分析。通读文章大致了解,会发现本篇i2c设备驱动与虚拟总线platform架构类似。不同的是platform是软件实现的虚拟总线,在soc上并不存在;而i2c总线,在soc上是实际存在的。相同的是两者实现将驱动分层为硬件参数和驱动抽象,在注册时遍历匹配,然后进入正文probe中!
由于Linux内部的实现较为复杂,本篇主要以设备驱动的角度来分析整个驱动的代码走向,涉及到内部API的实现,本篇只大概介绍其功能,剩余部分会放在i2c核心继续分析。
参考:
《Linux设备驱动开发详解》
《【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.4》
https://blog.csdn.net/Egean/article/details/81085077
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Elasticsearch:Elasticsearch 中的 refresh 和 flush 操作指南
- Flutter 和 iOS 之间的 Battle:手势交互谁才是老大?
- python 爬取B站原视频的实例代码
- flink实战-使用自定义聚合函数统计网站TP指标
- 详解flink中Look up维表的使用
- 聊聊flink 1.11 中的随机数据生成器-DataGen connector
- flink实战教程-集群的部署
- Flink实战教程-自定义函数之标量函数
- Flink实战教程-自定义函数之TableFunction
- Flink教程-flink 1.11 流式数据ORC格式写入file
- Flink教程-使用sql将流式数据写入文件系统
- flink教程-flink 1.11 集成zeppelin实现简易实时计算平台
- flink教程-详解flink 1.11 中的CDC (Change Data Capture)
- flink教程-基于flink 1.11 使 sql客户端支持执行sql文件
- flink教程-详解flink 1.11 中的JDBC Catalog