2.linux内核I2C驱动编程框架
面试题:谈谈对I2C总线的理解
2.1.回顾I2C总线协议
以MMA8653三轴加速度传感器为例
定义:七个字/SCL/SDA/串行/并行/总线/上拉电阻
提问题:
CPU如何找到外设:通过设备地址
CPU如何和外设进行数据传输呢:框框圈圈图,单字节寄存器写/读,多字节寄存器的写/读
SCL和SDA的配合:四个字:低放高取(大招)
2.2.问:linux内核如何编写一个I2C总线驱动程序呢?
答:很简单,只需将ARM裸板课上编写的I2C总线程序移植改造即可,只需将ARM裸板课上的物理地址
做个ioremap映射即可,然后封装read,write,ioctl接口即可访问
弊端:肯定是没有问题的,但是代码编写的工作量极大,又要操作I2C控制器和内部的寄存器,又要关心
I2C外设本身的各种寄存器信息,特别多的操作
2.3.linux内核I2C驱动编程框架(分层思想)
分层思想:就是将自己用ioremap实现的I2C驱动代码进行高度的封装,让开发者尽量避免查看
I2C控制器相关的操作,I2C控制器如何操作你不用关心,这些芯片厂家帮你完成!
问:如何体现分层思想呢?
答:linux内核将I2C驱动分成三部分:I2C总线驱动,I2C设备驱动,SMBUS接口层
1.I2C总线驱动特点
操作硬件:I2C总线驱动只操作I2C控制器本身(也就是操作各种I2C控制器内部的寄存器)
功能:既然操作I2C控制器和内部的寄存器,也就是最终发起I2C外设要求的各种时序
最终完成设备地址,片内寄存器地址和数据的传输
类似:卡车,只负责运输,不关心运输的货物(设备地址,片内寄存器地址和片内寄存器数据)
好消息:此驱动代码由芯片厂家提供
掌握其配置添加过程:
cd /opt/kernel
make menuconfig
Device Drivers->
<*>I2C supports->
I2C Hardware Bus support ---> //配置I2C总线驱动入口
<*> Slsiap I2C //S5P6818的I2C控制器的驱动,又称总线驱动
只负责发起时序,至于传输什么数据它不操心
2.I2C设备驱动:
操作硬件:只关心I2C外设相关的信息(设备地址,片内寄存器地址,片内寄存器数据)
I2C总线驱动发起硬件时序,时序里面传输的数据(设备地址,片内寄存器地址,片内寄存器数据)
都是由I2C设备驱动来提供的
并且还要给应用程序提供操作接口
类似:农民,提供货物(设备地址,片内寄存器地址,片内寄存器数据)
将来这些获取丢给I2C总线驱动来负责运输
好消息:此驱动由驱动工程师编写
3.SMBUS接口层
功能:连接I2C总线驱动和I2C设备驱动,起到了桥梁的作用,类似:农民和卡车司机中间的介绍人
I2C设备驱动将来调用SMBUS接口成将传输的数据(设备地址,片内寄存器地址,片内寄存器数据)丢给I2C
总线驱动,最终由I2C总线驱动操作I2C控制器发起硬件时序完成数据的传输
好消息:此代码由linux内核完成!
2.4.linux内核I2C驱动分层:
以CPU读取MMA8653的ID值为例,ID值为0x5A,并且对应的片内寄存器地址是0x0D
1.应用层(驱动工程师)
#define MMA_READ 0x100001 //读片内寄存器命令
#define MMA_WRITE 0x100002 //写片内寄存器命令
struct mma {
unsigned char addr; //指定访问的片内寄存器地址
unsigned char data; //指定访问的片内寄存器数据
};
struct mma mma; //分配用户缓冲区
mma.addr = 0x0D; //指定要访问的片内寄存器地址是0x0D
mma.data = ? //目标
ioctl(fd, MMA_READ, &mma);
printf("ioctl返回之后,ID=%#x\n", mma.data); //ID=0x5A
2.I2C设备驱动层(驱动工程师)
功能:负责提供操作接口,获取操作的数据信息,根据用户的需求(读或者写需求)调用内核提供的
SMBUS接口函数最终完成和I2C总线驱动的数据传输
参考代码:
//给用户提供的ioctl接口
long mma_ioctl(file, cmd, unsigned long buf) { //注意:buf=&mma=用户缓冲区的首地址
//分配内核缓冲区
struct mma kmma;
//拷贝用户缓冲区到内核缓冲区,获取用户要访问的片内寄存器地址或者数据
copy_from_user(&kmma, (struct mma *)buf, sizeof(kmma));
//此时:kmma.addr = 0x0D, kmma.data = ?
//调用内核提供的SMBUS接口函数将片内寄存器的地址丢给I2C总线驱动
并且让I2C总线驱动发起读单字节的时序获取ID=0x5A返回给I2C设备驱动
kmma.data = i2c_smbus_read...(..., kmma.addr/*发送片内寄存器地址*/, 0x1D/*顺便发个设备地址*/);
//将内核缓冲区的数据拷贝到用户缓冲区
copy_to_user((struct mma *)buf, &kmma, sizeof(kmma));
return 0;
}
3.SMBUS接口层(linux内核提供)
连接I2C设备驱动和I2C总线驱动
unsigned char i2c_smbus_read...(...., unsigned char addr, unsigned char dev_addr)
{
//大神调用I2C总线驱动提供的接口函数完成对I2C总线驱动的调用
发起硬件时序的操作
return I2C总线驱动read1字节(addr, dev_addr);
}
4.I2C总线驱动层(芯片厂家提供)
只负责操作I2C控制器,只负责根据不同的需求发起不同的时序
unsigned char I2C总线驱动read1字节(unsigned char addr, unsigned char dev_addr) {
//各种ioremap寄存器的物理地址,然后各种操作寄存器的相应的bit位
//最终发起读1字节的时序,并且获取ID值
ST->dev_addr<<1|0->(ACK)->addr->(ACK)->ST->dev_addr<<1|1->(ACK)
<-(0x5A)->NACK->SP
return 0x5A;
}
5.硬件层(硬件工程师)
S5P6818<-----SCL/SDA------->MMA8653
时序
总结:linux内核分层思想的终极目标就是尽可能的让开发人员少关注底层的硬件操作细节!降低开发的工作量!
2.5.问:如何开发一个I2C设备驱动呢?
答:采用设备dev-总线bus-驱动drv编程模型(分离思想)
platform也是采用这种编程模型,I2C设备驱动也是采用此编程模型
实现过程如下:
1.在linux内核中已经帮你定义好了一个虚拟总线叫i2c_bus_type(platform的虚拟总线叫platform_bus_type)在这个总线上(就是一个结构体变量)维护着两个链表:dev链表和drv链表
2.dev链表上每个节点描述的是I2C外设的纯硬件信息(设备地址啊,中断号啊等)对应的结构体叫struct i2c_client,每当向dev链表添加一个硬件节点,驱动工程师只需定义初始化一个struct i2c_client硬件节点然后注册到dev链表即可,内核会帮你遍历drv链表,取出drv链表上每个软件节点跟这个要注册的硬件节点进行匹配,匹配是内核调用总线提供的match函数,比i2c_client.name和软件节点i2c_driver.id_table.name,如果匹配成功,内核自动调用软件节点probe函数,并且把匹配成功的硬件节点的首地址传递给probe函数,如果匹配不成功,没关系,硬件节点静静等待着软件节点的到来
3.drv链表上每个节点描述的是I2C外设的纯软件信息(混杂设备,操作接口等)
对应的结构体叫struct i2c_driver,每当向drv链表添加一个软件节点,驱动工程师只需定义初始化一个
struct i2c_driver软件节点然后注册到drv链表即可,内核会帮你遍历dev链表,取出dev链表上每个硬件节点
跟这个要注册的软件节点进行匹配,匹配是内核调用总线提供的match函数,比较i2c_client.name和软件节点
i2c_driver.id_table.name,如果匹配成功,内核自动调用软件节点的probe函数,并且把匹配成功的硬件节点的
首地址传递给probe函数,如果匹配不成功,没关系,软件节点静静等待着硬件节点的到来
4.结论:驱动工程师只需关注两个结构体:
struct i2c_client
struct i2c_driver
只需做:定义初始化硬件或者软件节点注册到dev或者drv链表即可
内核帮你做:遍历,匹配,调用probe,给probe函数传递参数
2.6.详解struct i2c_client
struct i2c_client {
unsigned short addr;
char name[I2C_NAME_SIZE];
struct device dev;
int irq;
...
};
功能:描述I2C外设的纯硬件信息
addr:指定I2C外设的设备地址,用于找外设
name:用于匹配
irq:如果I2C外设和处理器之间连接中断线,irq保存着对应的中断号
dev:重点关注其中的void *platform_data,用于装载自定义的硬件信息
切记:其中addr和name必须初始化
注意:驱动工程师无需用此结构体定义初始化和注册struct i2c_client硬件节点对象
这些工作都是由linux内核帮你完成:帮你定义一个i2c_client对象,
帮你初始化i2c_client对象,帮你向dev链表注册i2c_client对象
问:内核可以帮我定义和注册i2c_client对象,内核怎么初始化i2c_client对象呢
内核怎么知道设备地址和name和中断号和自定义的硬件信息呢?这些初始化的信息
内核显然不知道
答:驱动工程师只需用以下结构体定义初始化注册硬件信息给linux内核使用即可
linux内核将来根据你提供的硬件信息来初始化i2c_client
struct i2c_board_info {
char type[I2C_NAME_SIZE];
unsigned short addr;
void *platform_data;
int irq;
};
功能:专门给linux内核提供初始化i2c_client的硬件信息
type:指定一个名称,将来会赋值给i2c_client.name,所以用于匹配
addr: 设备地址,将来会赋值给i2c_client.addr
platform_data:指定自定义的硬件信息,将来会赋值给i2c_client.dev.platform_data
irq:指定中断号,将来会赋值给i2c_client.irq
配套函数:
int i2c_register_board_info(int busnum, struct i2c_board_info const *info, unsigned n);
功能:向内核注册i2c外设的纯硬件信息,将来内核拿着这些信息帮你初始化i2c_client对象
参数:
busnum:指定I2C外设所在的总线编号,必须查看原理图
例如:MMA8653连接到S5P6818的I2C2总线上,所以:busnum=2
info:指向定义初始化好的i2c_board_info对象,这个对象就是纯硬件信息
n:指定初始化的硬件信息的个数,也就是i2c_board_info对象的个数
注意:i2c_board_info的定义初始化和注册代码必须和uImage写在一起,不能insmod和rmmod
一般代码添加到内核源码的arch/arm/plat-s5p6818/x6818/device.c文件中
案例:编写MMA8653的I2C设备驱动,目前添加它的硬件节点
上位机执行:
cd /opt/kernel
make menuconfig
Device Drivers->
<*> Hardware Monitoring support --->
//按N键去除官方的MMA8653的I2C设备驱动
<*> Freescale MMA865X 3-Axis Accelerometer
保存退出
vim arch/arm/plat-s5p6818/x6818/device.c
在文件的开头添加如下代码:
#include <linux/i2c.h> //struct i2c_board_info声明
//定义初始化MMA8653的纯硬件信息
static struct i2c_board_info mma8653 = {
.type = "mma8653", //用于匹配,将来会赋值给i2c_client.name
.addr = 0x1D //指定设备地址,用于找外设,将来会赋值给i2c_client.addr
};
//找到nxp_board_devs_register函数,因为大神的各种硬件信息的注册也都是在这个函数内部完成,所以照猫画虎
再次函数内部添加:
i2c_register_board_info(2, &mma8653, 1);//向内核注册将来初始化i2c_client的纯硬件信息
保存退出
心里明白:将来内核会把你定义一个i2c_client对象,内核会用你注册的硬件信息初始化i2c_client对象
然后内核向dev链表注册i2c_client对象,然后等待着软件节点的到来!
make uImage
用fastboot重新烧写uImage即可,此时内核一旦启动,就会有一个硬件节点i2c_client静静等待着对应的软件节点到来!
2.7.详解struct i2c_driver
struct i2c_driver {
struct device_driver driver;
const struct i2c_device_id *id_table;
int (*probe)(struct i2c_client *client, const struct i2c_device_id *id);
int (*remove)(struct i2c_client *client);
...
};
功能:描述I2C外设的纯软件信息
driver:其中的name不再用于匹配
而platform_driver的driver的name是用于匹配的
id_table:其中的name用于匹配
struct i2c_device_id {
char name[I2C_NAME_SIZE];
};
probe:匹配成功,内核调用此函数
形参client指针:指向匹配成功的硬件节点,利用client可以获取硬件信息
client->addr:获取设备地址
client->dev.platform_data:获取自定义的硬件信息
client->irq:获取中断号
形参id:跟id_table成员指向的对象是一样的,主要用于传递参数
remove:卸载软件节点内核调用此函数
形参client指针:指向匹配成功的硬件节点,利用client可以获取硬件信息
配套函数:
i2c_add_driver:向内核注册软件节点,内核帮你做四件事
i2c_del_driver:从内核中卸载软件节点
案例:编写MMA8653的I2C设备驱动,现在添加MMA8653的软件节点代码
参考代码:day11/3.0
下位机测试:
cd /home/drivers
insmod mma8653_drv.ko //此时调用probe函数
rmmod mma8653_drv //此时调用remove函数
案例:继续完善probe和remove函数,实现能够循环读取XYZ三个方向的加速度值
参考代码:day11/4.0
下位机测试:
cd /home/drivers
insmod mma8653_drv.ko //此时调用probe函数
./mma8653_test //观察XYZ加速值
rmmod mma8653_drv //此时调用remove函数
总结:内核提供的SMBUS接口函数使用步骤:
功能:I2C设备驱动调用SMBUS接口函数能够让I2C总线驱动帮你发起所需的时序,最终完成数据的读写操作!
1.打开SMBUS接口函数的说明文档:位于内核源码/opt/kernel/Documentation/i2c/smbus-protocol
注意:comm指的就是片内寄存器地址
2.然后根据你要求的时序在这个文档中选择对应的函数
例如:读取mma8653的id为例(片内寄存器地址是0x0d,id=0x5a)
st->0x1d<<1|0->(ack)->0x0d->(ack)->st->0x1d<<1|1->(ack)->(0x5a)->nack->sp
根据此时序在这个文档中找到对应的函数是:i2c_smbus_read_byte_data
3.一旦根据时序找到了对应的函数,然后利用sourceinsight在内核源码中找到此函数的定义
获取这个函数的原型
注意:这些函数的参数说明:
第一个参数client指针:永远指向匹配成功的硬件节点
第二个参数command:传递要访问的片内寄存器地址
第三个参数value:传递要向片内寄存器写入的数据(仅限于写寄存器)
返回值:如果是读片内寄存器值,返回值就是片内寄存器的数据
合并XYZ加速度值的位运算注意事项:
通过MMA8653芯片手册P19得到:
存储区X方向加速度值的寄存器地址分别是:0x01(存储10位加速值的高位)和0x02(存储10位加速度值的低位)
注意:0x01寄存器的8位作为10位加速度值的高8位(bit[9:2])
0x02寄存器的bit[7:6]作为10位加速度值的低2位(bit[1:0])
存储区Y方向加速度值的寄存器地址分别是:0x03(存储10位加速值的高位)和0x04(存储10位加速度值的低位)
注意:0x03寄存器的8位作为10位加速度值的高8位(bit[9:2])
0x04寄存器的bit[7:6]作为10位加速度值的低2位(bit[1:0])
存储区Z方向加速度值的寄存器地址分别是:0x05(存储10位加速值的高位)和0x06(存储10位加速度值的低位)
注意:0x05寄存器的8位作为10位加速度值的高8位(bit[9:2])
0x06寄存器的bit[7:6]作为10位加速度值的低2位(bit[1:0])