文章目录
Linux内核i2c驱动编程
i2c总线特性
I2C总线是由Philips公司开发的一种简单、双向二线制同步串行总线。它只需要两根线即可在连接于总线上的器件之间传送信息。
主器件用于启动总线传送数据,并产生时钟以开放传送的器件,此时任何被寻址的器件均被认为是从器件.在总线上主和从、发和收的关系不是恒定的,而取决于此时数据传送方向。如果主机要发送数据给从器件,则主机首先寻址从器件,然后主动发送数据至从器件,最后由主机终止数据传送;如果主机要接收从器件的数据,首先由主器件寻址从器件.然后主机接收从器件发送的数据,最后由主机终止接收过程。在这种情况下.主机负责产生定时时钟和终止数据传送。
I2C总线特点可以概括如下:
- (1)在硬件上,I2C总线只需要一根数据线和一根时钟线两根线,总线接口已经集成在芯片内部,不需要特殊的接口电路,而且片上接口电路的滤波器可以滤去总线数据上的毛刺.因此I2C总线简化了硬件电路PCB布线,降低了系统成本,提高了系统可靠性。因为I2C芯片除了这两根线和少量中断线,与系统再没有连接的线,用户常用IC可以很容易形成标准化和模块化,便于重复利用。
- (2)I2C总线是一个真正的多主机总线,如果两个或多个主机同时初始化数据传输,可以通过冲突检测和仲裁防止数据破坏,每个连接到总线上的器件都有唯一的地址,任何器件既可以作为主机也可以作为从机,但同一时刻只允许有一个主机。数据传输和地址设定由软件设定,非常灵活。总线上的器件增加和删除不影响其他器件正常工作。
- (3)I2C总线可以通过外部连线进行在线检测,便于系统故障诊断和调试,故障可以立即被寻址,软件也利于标准化和模块化,缩短开发时间。
- (4)连接到相同总线上的IC数量只受总线最大电容的限制,串行的8位双向数据传输位速率在标准模式下可达100Kbit/s,快速模式下可达400Kbit/s,高速模式下可达3.4Mbit/s。
- (5)总线具有极低的电流消耗.抗高噪声干扰,增加总线驱动器可以使总线电容扩大10倍,传输距离达到15m;兼容不同电压等级的器件,工作温度范围宽。
字节格式
发送到SDA 线上的每个字节必须为8 位,每次传输可以发送的字节数量不受限制。每个字节后必须跟一个响应位。首先传输的是数据的最高位(MSB),如果从机要完成一些其他功能后(例如一个内部中断服务程序)才能接收或发送下一个完整的数据字节,可以使时钟线SCL 保持低电平,迫使主机进入等待状态,当从机准备好接收下一个数据字节并释放时钟线SCL 后数据传输继续。
应答响应
数据传输必须带响应,相关的响应时钟脉冲由主机产生。在响应的时钟脉冲期间发送器释放SDA 线(高)。
在响应的时钟脉冲期间,接收器必须将SDA 线拉低,使它在这个时钟脉冲的高电平期间保持稳定的低电平。
通常被寻址的接收器在接收到的每个字节后,除了用CBUS 地址开头的数据,必须产生一个响应。当从机不能响应从机地址时(例如它正在执行一些实时函数不能接收或发送),从机必须使数据线保持高电平,主机然后产生一个停止条件终止传输或者产生重复起始条件开始新的传输。
如果从机接收器响应了从机地址,但是在传输了一段时间后不能接收更多数据字节,主机必须再一次终止传输。这个情况用从机在第一个字节后没有产生响应来表示。从机使数据线保持高电平,主机产生一个停止或重复起始条件。
如果传输中有主机接收器,它必须通过在从机发出的最后一个字节时产生一个响应,向从机发送器通知数据结束。从机发送器必须释放数据线,允许主机产生一个停止或重复起始条件。
时钟同步
所有主机在SCL线上产生它们自己的时钟来传输I2C总线上的报文。数据只在时钟的高电平周期有效,因此需要一个确定的时钟进行逐位仲裁。
时钟同步通过线与连接I2C 接口到SCL 线来执行。这就是说SCL 线的高到低切换会使器件开始数它们的低电平周期,而且一旦器件的时钟变低电平,它会使SCL 线保持这种状态直到到达时钟的高电平。但是如果另一个时钟仍处于低电平周期,这个时钟的低到高切换不会改变SCL 线的状态。因此SCL 线被有最长低电平周期的器件保持低电平。此时低电平周期短的器件会进入高电平的等待状态。
当所有有关的器件数完了它们的低电平周期后,时钟线被释放并变成高电平。之后,器件时钟和SCL线的状态没有差别,而且所有器件会开始数它们的高电平周期。首先完成高电平周期的器件会再次将SCL线拉低。
这样产生的同步SCL 时钟的低电平周期由低电平时钟周期最长的器件决定,而高电平周期由高电平时钟周期最短的器件决定。
注意:数据在时钟低电平期间改变而高电平期间保持,也就是低放高取(低电平期间改变传输数据,高电平期间由从机从总线上读取数据)。
Linux内核i2c驱动
Linux内核i2c驱动包含两部分:i2c总线驱动和i2c设备驱动
i2c总线驱动
i2c总线驱动操作的硬件对象仅仅是芯片内的I2C控制器,该部分在内核中已经由各个芯片厂家完成,在进行驱动开发中只需要配置上相应的Linux内核选项即可。
make menuconfig
Device Drivers->
I2C supports->
I2C Hardware Bus support --->
<*> Slsiap I2C //S5P6818 I2C控制器的驱动
//也就是I2C总线驱动的支持
i2c设备驱动
i2c设备驱动操作的硬件对象就是i2c外设本身。i2c设备驱动就是发起i2c外设所需的时序从而控制i2c往外设。当然,这种时序最终还是由芯片内的i2c控制器完成。
Linux内核i2c驱动框架
i2c总线驱动和i2c设备驱动是如何配合的呢,以CPU获取MMA8653三轴加速度传感器ID为例说明(CPU通过i2c总线读取MMA8653片内寄存器0x0D的数据(0x5A))
- 在应用层空间,使用简单的读取设备文件的方式:
struct mma8653 {
unsigned char addr; //片内寄存器地址
unsigned char data; //片内寄存器数据
};
struct mma8653 mma; //分配用户缓冲区
mma.addr = 0x0D; //指定要访问的片内寄存器地址
mma.data = ?
ioctl(fd, MMA8653_READ, &mma);//发起读取MMA8653动作
printf("ID=%#x\n", mma.data); //mma.data=0x5A
- 而在i2c设备驱动层,需要完成ioctl对应的驱动接口,如下内容:
long mma8653_ioctl(file, cmd, arg) {
//1.分配内核缓冲区
struct mma8653 kmma;
//2.拷贝用户数据到内核缓冲区
copy_from_user(&kmma, (struct mma8653*)arg, sizeof(kmma));
//此时:kmma.addr=0x0D;kmma.data=?
//3.I2C设备驱动发起硬件操作时序要求
// 此要求最终由I2C总线驱动来完成
// I2C总线驱动操作I2C控制器发起
// I2C设备驱动要求的时序, I2C设备驱动只需调用内核提供的SMBUS
//接口函数即可完成相关的请求:
kmma.data = i2c_smbus_*(...,0x0D);
//kmma.data = 0x5A
//4.拷贝内核缓冲区的数据到用户缓冲区
copy_to_user((strut mma8653*)arg, &kmma, sizeof(kmma));
return 0;
}
- 这其中,SMBUS接口层由内核提供,作为i2c设备驱动和i2c总线驱动的桥梁,此接口都是为i2c设备驱动层使用。
- i2c总线驱动层,是芯片厂家根据SMBUS标准进行编写,已提供各个需要用到的i2c时序操作,根据i2c设备驱动的要求发起操作i2c控制器,最终产生符合外设要求的时序情况。
编写i2c设备驱动程序
如何编写i2c设备驱动程序呢?
- 编写i2c设备驱动程序同样是利用内核的分离思想,但是不是platform机制,但是同样是设备-总线-驱动编程模型。如图:
实现原理
- 首先,内核已经定义好了一个虚拟总线叫 i2c_bus_type,在这个总线上维护着两个链表:dev链表和drv链表。
- dev链表上每一个节点描述的都是 i2c 外设的纯硬件信息,对应的数据结构为 struct i2c_client,每当向dev链表添加一个 i2c 外设的硬件信息节点时,只需用此数据结构定义初始化一个对象即可,然后向 dev 链表添加,一旦添加完毕,内核会帮你遍历 drv 链表,从 drv 链表上取出每一个节点与当前这个要注册的硬件节点进行匹配,内核通过调用总线提供的 match 函数进行比较。比较 i2c_client 的name 和 i2c_driver 的id_table的 name ,如果匹配成功,硬件找到了对应的驱动节点,内核会调用 i2c_driver 匹配成功的节点内的probe函数,并且把匹配成功的硬件节点的首地址传递给probe函数,最终完成硬件和软件的再次结合。
- drv 链表上每一个节点描述的 i2c 外设的纯软件信息对应的数据结构为 struct i2c_driver ,每当向 drv 链表添加一个i2c外设的软件信息节点时,只需要用此数据结构定义初始化一个对象即可,然后向 drv 链表添加节点,一旦添加完毕,内核就会遍历 dev 链表,从 dev 链表上取出每一个硬件节点跟这个要注册的软件节点进行匹配,内核通过调用总线提供的 match 函数进行比较匹配。如果匹配成功,软件找到了对应的硬件,内核会自动调用 i2c_driver 的probe函数,并且把匹配成功的硬件节点的首地址传递给 probe 函数,最终完成硬件和软件的再次结合。
- 驱动开发者要实现一个 i2c 设备驱动,只需要关注以下两个数据结构:
struct i2c_client;
struct i2c_driver;
i2c_client
struct i2c_client {
unsigned short addr;
char name[I2C_NAME_SIZE];
struct device dev;
int irq;
...
};
功能:用于描述 i2c 外设的纯硬件信息。
成员:
- addr:I2C外设的设备地址,用于找外设,必须初始化!
- name:用于匹配,必须初始化!
- dev:重点关注其中的void *platform_data字段,此字段将来用于装载自定义的用于描述I2C外设的硬件信息。
- irq:如果I2C外设和CPU之间有中断,此字段保存对应的中断号。
切记: Linux内核对 i2c_client 的操作和 platform_device还是有所区别的。驱动开发者不用自己去定义初始化和注册一个 struct i2c_client 硬件节点对象,定义初始化和注册过程统一由内核完成。驱动开发者主需要利用如下数据结构将需要初始化的信息注册到内核中即可,也就是告诉内核将来要初始化的具体信息,内核根据提供的信息来完成初始化。
struct i2c_client硬件节点对象:
struct i2c_board_info {
char type[I2C_NAME_SIZE];
unsigned short addr;
void *platform_data;
int irq;
...
};
功能:驱动开发者利用此数据结果将 i2c 外设的硬件信息告诉Linux内核,将来内核根据提供的 i2c 外设的硬件信息定义初始化和注册一个 i2c_client 硬件节点对象到 dev 链表。
成员:
- type:指定硬件节点的名称,此字段将来会自动的赋值给i2c_client的name,将来用于匹配, 所以必须初始化!
- addr:指定I2C外设的设备地址,此字段将来会自动赋值给i2c_client的addr,将来用于找外设,所以必须初始化!
- platform_data:用于装载自定义的硬件信息,将来会自动赋值给i2c_client.dev.platform_data。
- irq:如果CPU和外设需要中断,此字段用来指定中断号,将来会赋值给i2c_client.irq
配套函数:
i2c_register_board_info(int busnum,struct i2c_board_info const *info, unsigned len)
函数功能:注册 i2c 外设硬件信息到内核,将来Linux内核会帮你定义初始化和注册 i2c_client 硬件节点到 dev 链表,内核初始化 i2c_client 所需的内容都是根据 i2c_board_info 来进行提供。
参数:
- busnum:I2C外设所在的CPU的I2C总线编号必须根据原理图获取。例如:X6818开发板上的mma8653所对应的I2C总线编号为2(第三路I2C总线)
- info:传递要注册的I2C硬件信息
- len:用i2c_board_info描述的硬件信息的个数
切记: struct i2c_board_info 的定义、初始化和注册不能采用 insmod / rmmod 进行,必须将代码和 uImage 编译在一起,一般要写到对应的平台文件(内核源码/arch/arm/plat-s5p6818/x6818/device.c)
i2c_driver
struct i2c_driver {
int (*probe)(struct i2c_client *client, const struct i2c_device_id *id);
int (*remove)(struct i2c_client *client);
const struct i2c_device_id *id_table;
...
};
说明:描述 I2C 外设的软件信息
成员:
- probe:硬件节点和软件节点匹配成功,内核调用形参client指针指向匹配成功的I2C外设的硬件节点。
- //心里念叨:此client指针里面的内容都是i2c_board_info提供。
- client->addr //获取I2C外设的设备地址
- client->irq //获取I2C外设的中断号
- client->dev.platform_data //获取自定义的硬件信息
- //心里念叨:此client指针里面的内容都是i2c_board_info提供。
- remove:卸载软件节点,内核调用此函数, 形参client指针指向匹配成功的I2C外设的硬件节点
- //心里念叨:此client指针里面的内容都是i2c_board_info提供
- client->addr //获取I2C外设的设备地址
- client->irq //获取I2C外设的中断号
- client->dev.platform_data //获取自定义的硬件信息
- //心里念叨:此client指针里面的内容都是i2c_board_info提供
- id_table:重点关注其中的name字段,此字段将来用于匹配
配套函数:
i2c_add_driver(&软件节点对象)
- 向内核drv链表注册添加I2C外设软件节点对象,将来内核会帮你遍历,匹配,调用probe函数,传递参数。
i2c_del_driver(&软件节点对象)
- 从内核drv链表删除软件节点对象,内核会帮你调用remove函数。
SMBUS接口函数的使用步骤
- 打开SMBUS接口函数的说明使用文档,在内核源码的Documentation\i2c\smbus-protocol,打开此文件
- 再打开MMA8653的芯片手册,找到对应的读时序图
- 根据读时序图在文档smbus-protocol中找到对应的实现函数
- 找到对应的函数以后,在sourceinsight中找到这个函数的定义,获取到函数的参数和返回值。
注意: smbus接口函数中的client指针一定要传递匹配成功的硬件节点对象指针。
示例(MMA8653三轴加速度传感器使用)
- 首先去除官方的MMA8653的三轴加速度传感器驱动。
```bash
cd /opt/kernel
make menuconfig
Deivce Drivers->
Hardware Monitoring support->
//按N键去除
<*>Freescale MMA865X 3-Axis Accelerometer
```
# 保存退出
make uImage
cp arch/arm/boot/uImage /tftpboot
#重启下位机,进入uboot
tftp 48000000 uImage
bootm 48000000
- 向内核添加MMA8653硬件信息
cd /opt/kernel
vim arch/arm/plat-s5p6818/x6818/device.c
# 在文件的最开头添加如下代码:
# 定义初始化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, ARRAY_SIZE(mma8653));
- 只要这条语句执行,内核就会定义初始化和注册一个i2c_client硬件节点对象到dev链表,并且开始各种遍历、匹配、调用、传递参数。i2c_client对象中的所有成员都是我这注册的i2c_board_info来提供的(i2c_client.name=type, i2c_client.addr,i2c_client.dev.platform_data=platform_data, i2c_client.irq = irq)
- i2c设备驱动 i2c_driver 部分可以通过驱动模块加载的方式加载,当我们加载i2c软件驱动部分时,内核就会完成匹配并自动调用驱动中的probe函数完成初始化。
具体代码如下:
-
arch/arm/plat-s5p6818/x6818/device.c
-
mma8653_drv.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/i2c.h> //struct i2c_driver ...
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/uaccess.h>
#include <linux/delay.h>
//声明描述MMA8653三轴加速度信息数据结构
struct mma8653_data {
short x; //X轴的加速值
short y; //Y轴的加速值
short z; //Z轴的加速值
};
//将来用于匹配的对象
static struct i2c_device_id mma8653_id[] = {
{"mma8653", 0}, //"mma8653"用于匹配
};
static struct i2c_client *g_client; //全局指针
//client指针指向匹配成功的硬件节点对象
static void mma8653_hw_init(struct i2c_client *client)
{
int ret = 0;
//I2C设备驱动调用SMBUS接口函数来操作I2C控制器
//最终发起硬件操作时序
//SMBUS接口函数的使用步骤:
// 1.打开SMBUS接口函数的说明使用文档,在内核源码的Documentation\i2c\smbus-protocol
// 打开此文件
// 2.再打开MMA8653的芯片手册,找到对应的读时序图
// 3.根据读时序图在文档smbus-protocol中找到对应的实现函数
// 4.找到对应的函数以后,在sourceinsight中找到这个函数的定义
//获取到函数的参数和返回值
//注意:smbus接口函数中的client指针一定要传递匹配成功的硬件节点对象指针
//读片内寄存器0x0D的数据
ret = i2c_smbus_read_byte_data(client, 0x0D);
printk("%s:addr = %#x, Read ID value is :%#x\n",
__func__, client->addr, ret);
i2c_smbus_write_byte_data(client, 0x2A, 0); //省电模式
i2c_smbus_write_byte_data(client, 0x0E,0); //设置测量范围+-2g
}
static void mma8653_read_data(struct mma8653_data *mma)
{
unsigned char tmp_data[7];
//判断新的数据是否合法有效
while(!(i2c_smbus_read_byte_data(g_client, 0x00) & 0x08)) {
printk("data is not ready!\n");
}
//将加速度值读取
i2c_smbus_read_i2c_block_data(g_client, 0x01, 7, tmp_data);
mma->x = ((tmp_data[0] << 8) & 0xff00) | tmp_data[1];
mma->y = ((tmp_data[2] << 8) & 0xff00) | tmp_data[3];
mma->z = ((tmp_data[4] << 8) & 0xff00) | tmp_data[5];
mma->x = (mma->x) >> 6;
mma->y = (mma->y) >> 6;
mma->z = (mma->z) >> 6;
msleep(20);
}
static void mma8653_config_mode(void)
{
unsigned char data;
data = i2c_smbus_read_byte_data(g_client, 0x2A);
data |= 1;
i2c_smbus_write_byte_data(g_client, 0x2A, data);
}
#define GS_MMA8653_GETXYZ_CMD 0x100001
static long mma8653_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
struct mma8653_data mma; //分配内核缓冲区
switch(cmd) {
case GS_MMA8653_GETXYZ_CMD:
mma8653_config_mode(); //激活MMA8653
mma8653_read_data(&mma); //读取寄存器的加速度值
copy_to_user((struct mma8653_data *)arg, &mma, sizeof(mma));
break;
default:
return -1;
}
return 0;
}
//定义初始化硬件操作接口
static struct file_operations mma8653_fops = {
.owner = THIS_MODULE,
.unlocked_ioctl = mma8653_ioctl
};
//定义初始化混杂设备对象
static struct miscdevice mma8653_misc = {
.minor = MISC_DYNAMIC_MINOR,
.name = "mma8653",
.fops = &mma8653_fops
};
//client指针指向匹配成功的硬件节点对象
static int mma8653_probe(
struct i2c_client *client,
const struct i2c_device_id *id)
{
printk("MMA8653 设备地址 = %d\n", client->addr);
//注册混杂设备对象,给用户提供访问操作接口
misc_register(&mma8653_misc);
//把局部进行全局化
g_client = client;
//初始化mma8653硬件信息
mma8653_hw_init(client);
return 0;
}
static int mma8653_remove(struct i2c_client *client)
{
misc_deregister(&mma8653_misc);
return 0;
}
//定义初始化I2C外设的软件节点对象
static struct i2c_driver mma8653_drv = {
.driver = {
.name = "tarena" //不重要
},
.id_table = mma8653_id, //其中的name用于匹配
.probe = mma8653_probe, //匹配成功调用
.remove = mma8653_remove //删除调用
};
static int mma8653_init(void)
{
//注册软件节点到drv
i2c_add_driver(&mma8653_drv);
return 0;
}
static void mma8653_exit(void)
{
//从drv删除软件节点
i2c_del_driver(&mma8653_drv);
}
module_init(mma8653_init);
module_exit(mma8653_exit);
MODULE_LICENSE("GPL");
- mma8653_test.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#define GS_MMA8653_GETXYZ_CMD 0x100001
struct mma8653_data {
short x;
short y;
short z;
};
int main(void)
{
int fd;
struct mma8653_data info; //分配用户缓冲区
fd = open("/dev/mma8653", O_RDWR);
if (fd < 0)
return -1;
while(1) {
ioctl(fd, GS_MMA8653_GETXYZ_CMD, &info);
printf("%d %d %d\n", info.x, info.y, info.z);
//usleep(50000);
}
close(fd);
return 0;
}
- Makefile
obj-m += mma8653_drv.o
#obj-m += mma8653fc.o
all:
make -C /opt/x6818_linux/kernel SUBDIRS=$(PWD) modules
clean:
make -C /opt/x6818_linux/kernel SUBDIRS=$(PWD) clean
- 执行结果: