I2C总线是由Philips公司开发的一种简单、双向二线制同步串行总线。它只需要两根线即可在连接于总线上的器件之间传送信息。
主器件用于启动总线传送数据,并产生时钟以开放传送的器件,此时任何被寻址的器件均被认为是从器件.在总线上主和从、发和收的关系不是恒定的,而取决于此时数据传送方向。如果主机要发送数据给从器件,则主机首先寻址从器件,然后主动发送数据至从器件,最后由主机终止数据传送;如果主机要接收从器件的数据,首先由主器件寻址从器件.然后主机接收从器件发送的数据,最后由主机终止接收过程。在这种情况下.主机负责产生定时时钟和终止数据传送。
发送到SDA 线上的每个字节必须为8 位,每次传输可以发送的字节数量不受限制。每个字节后必须跟一个响应位。首先传输的是数据的最高位(MSB),如果从机要完成一些其他功能后(例如一个内部中断服务程序)才能接收或发送下一个完整的数据字节,可以使时钟线SCL 保持低电平,迫使主机进入等待状态,当从机准备好接收下一个数据字节并释放时钟线SCL 后数据传输继续。
数据传输必须带响应,相关的响应时钟脉冲由主机产生。在响应的时钟脉冲期间发送器释放SDA 线(高)。
在响应的时钟脉冲期间,接收器必须将SDA 线拉低,使它在这个时钟脉冲的高电平期间保持稳定的低电平。
通常被寻址的接收器在接收到的每个字节后,除了用CBUS 地址开头的数据,必须产生一个响应。当从机不能响应从机地址时(例如它正在执行一些实时函数不能接收或发送),从机必须使数据线保持高电平,主机然后产生一个停止条件终止传输或者产生重复起始条件开始新的传输。
如果从机接收器响应了从机地址,但是在传输了一段时间后不能接收更多数据字节,主机必须再一次终止传输。这个情况用从机在第一个字节后没有产生响应来表示。从机使数据线保持高电平,主机产生一个停止或重复起始条件。
如果传输中有主机接收器,它必须通过在从机发出的最后一个字节时产生一个响应,向从机发送器通知数据结束。从机发送器必须释放数据线,允许主机产生一个停止或重复起始条件。
所有主机在SCL线上产生它们自己的时钟来传输I2C总线上的报文。数据只在时钟的高电平周期有效,因此需要一个确定的时钟进行逐位仲裁。
时钟同步通过线与连接I2C 接口到SCL 线来执行。这就是说SCL 线的高到低切换会使器件开始数它们的低电平周期,而且一旦器件的时钟变低电平,它会使SCL 线保持这种状态直到到达时钟的高电平。但是如果另一个时钟仍处于低电平周期,这个时钟的低到高切换不会改变SCL 线的状态。因此SCL 线被有最长低电平周期的器件保持低电平。此时低电平周期短的器件会进入高电平的等待状态。
当所有有关的器件数完了它们的低电平周期后,时钟线被释放并变成高电平。之后,器件时钟和SCL线的状态没有差别,而且所有器件会开始数它们的高电平周期。首先完成高电平周期的器件会再次将SCL线拉低。
这样产生的同步SCL 时钟的低电平周期由低电平时钟周期最长的器件决定,而高电平周期由高电平时钟周期最短的器件决定。
注意:数据在时钟低电平期间改变而高电平期间保持,也就是低放高取(低电平期间改变传输数据,高电平期间由从机从总线上读取数据)。
Linux内核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总线驱动和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
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;
}
如何编写i2c设备驱动程序呢?
struct i2c_client;
struct i2c_driver;
struct i2c_client {
unsigned short addr;
char name[I2C_NAME_SIZE];
struct device dev;
int irq;
...
};
功能:用于描述 i2c 外设的纯硬件信息。
成员:
切记: 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 链表。
成员:
配套函数:
i2c_register_board_info(int busnum,struct i2c_board_info const *info, unsigned len)
函数功能:注册 i2c 外设硬件信息到内核,将来Linux内核会帮你定义初始化和注册 i2c_client 硬件节点到 dev 链表,内核初始化 i2c_client 所需的内容都是根据 i2c_board_info 来进行提供。
参数:
切记: struct i2c_board_info 的定义、初始化和注册不能采用 insmod / rmmod 进行,必须将代码和 uImage 编译在一起,一般要写到对应的平台文件(内核源码/arch/arm/plat-s5p6818/x6818/device.c)
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 外设的软件信息
成员:
配套函数:
i2c_add_driver(&软件节点对象)
i2c_del_driver(&软件节点对象)
注意: smbus接口函数中的client指针一定要传递匹配成功的硬件节点对象指针。
```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
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
}
};
i2c_register_board_info(2, mma8653, ARRAY_SIZE(mma8653));
#include
#include
#include //struct i2c_driver ...
#include
#include
#include
#include
//声明描述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");
#include
#include
#include
#include
#include
#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;
}
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