最近在看驱动程序的时候,学习了解了I2C总线,前面有platform平台总线,是一种虚拟的总线,用于分离设备和驱动,便于驱动工程师移植程序。而I2C是一种实实在在的,具备多主机系统所需的包括总线裁决和高低速器件同步功能的高性能串行总线,I2C总线是两线式的串行总线,用于连接微控制器及其外围设备,如果我们的外设是用I2C总线连接的,那就意味着我们可以直接使用I2C驱动来控制设备了,那么我们为什么要使用I2C来控制设备,而不用其他的总线呢?下面就来说说I2C的一些特点吧。
I2C最重要的优点是简单有效,接口直接在设备组件的上面,因此I2C总线占用的空间非常少,减少了电路板的空间和芯片管脚的数量,降低了互联成本,可以接很多CPU和外设,当然,一般CPU只有一个啦,而且上面还挂接很多上拉电阻,默认我们的两线为高电平,至于上拉电阻的作用,待会说明。
I2C有两根双向信号线,一根是数据线SDA,用于传输数据。另一根是时钟线SCL,用于控制数据的传输,拥有时钟线是相比一线式总线最大的一个优点。而我们对设备的操作无非就是读和写,时钟线控制数据线一个时钟周期发送一个bit的数据。
下面说一下要用到的几个概念:
1. 主设备(master):CPU(一般)
2. 从设备(slave):外设
3. SDA:数据线,master和slave之间的数据传输,如果master向slave写入数据,SDA便由master控制,如果master从slave读取数据,SDA便由slave控制,万一master和slave都不控制,则由上拉电阻控制,默认为高电平
4. SCL:时钟线,提供一个数据传输的控制信号,控制着数据在时钟周期完成传输,比如CPU在SCL为低电平的时候写入数据到数据线上,那么slave就会在高电平的时候从数据线上获取数据,我们的时钟信号是由master发起的
另外,I2C总线的时钟频率有三种:100K,400K,3.2M
显然,要完成master和slave的数据交互,双方必须要制定一个可靠的协议吧,下面说一下I2C总线协议:
我们发现,每次都是数据线改变,这是因为时钟线是用来控制我们的数据线了,我们的时钟不能乱,如果时序乱了,那么发送的数据也会出错,我们可以用示波器去测量捕捉这些信号,以查看I2C总线是否是完好的。在时钟线拉低的时候,进行数据的写操作,为高电平的时候,进行读操作,具体的操作应该看对应芯片手册的时序。
I2C的基本介绍就到这里,下面谈谈如何针对设备来使用我们的I2C驱动吧。
先在内核中添加对I2C驱动的支持:
Device Drivers --->
<*> I2C support --->
[*] Enable compatibility bits for old user-space
<*> I2C device interface
< > I2C bus multiplexing support
[*] Autoselect pertinent helper modules
I2C Hardware Bus support --->
<*> S3C2410 I2C Driver
<*> Simtec Generic I2C interface
[*] I2C Core debugging messages
[*] I2C Algorithm debugging messages
[ ] I2C Bus debugging messages
然后添加我们的EEPROM设备,因为这个小的存储器用于存储一些小的信息,我们可以用I2C驱动来读写它。
[*] Misc devices --->
EEPROM support --->
<*> I2C EEPROMs from most vendors
<*> SPI EEPROMs from most vendors
<*> Old I2C EEPROM reader
< > Maxim MAX6874/5 power supply supervisor
<*> EEPROM 93CX6 support
再在我们的平台文件中将其添加进去
#include /*添加头文件*/
static struct at24_platform_data at24c02 ={
.byte_len = SZ_2K / 8,
.page_size = 8,
.flags = 0,
}
static struct i2c_board_info __initdata smdk_i2c_devices[] = {
/* more devices can be added using expansion connectors */
{
I2C_BOARD_INFO("24c02", 0x50),
.platform_data = &at24c02,
},
};
在machine_init函数中添加:
i2c_register_board_info(0, smdk_i2c_devices, ARRAY_SIZE(smdk_i2c_devices));
完成以上操作后重新编译内核并烧进开发板,然后进入/sys/devices/platform/s3c2440-i2c/i2c-0/0-0050/,可以看见我们对应的eeprom设备文件,然后用echo等命令可直接写进去,表示我们的eeprom启动成功。(当然我的cat到的内容是乱码。。。)
要操纵我们的I2C的设备,有两种方法,第一个是自己写对应的驱动,另一个利用是系统为我们提供的适配器来操控设备。
从书上看到一个自己写的设备驱动程序:
#include
#include
#include
#include
#include
#include
#include
static struct i2c_device_id at24c02_id[] = {
{"24c02", 0},
//”24c02“必须和前面添加的i2c_board_info的name一致,匹配靠它进行
};
static struct i2c_client *g_client; //记录匹配成功的i2c_client
//从EEPROM读取数据
static ssize_t at24c02_read(struct file *file,
char __user *buf,
size_t count,
loff_t *ppos)
{
unsigned char addr;
unsigned char data;
//1.从用户空间拷贝操作的数据到内核空间
copy_from_user(&addr, buf, 1); //拷贝读得地址信息到内核空间
//2.使用SMBUS接口将数据丢给I2C总线驱动,启动I2C总线的硬件传输
//2.1打开SMBUS文档:内核源码\Documentation\i2c\smbus-protocol
//找到对应的SMBUS接口函数
//2.2打开芯片操作时序图
//2.3根据时序图找对应的SMBUS操作函数
//2.4将addr和匹配成功的i2c_client通过函数丢给I2C总线驱动
//然后启动I2C总线的硬件传输
data = i2c_smbus_read_byte_data(g_client, addr);
if (data < 0)
return -EIO;
//3.读取到数据以后,将数据返回给用户空间
copy_to_user(buf, &data, 1);
return count;
}
//写数据到EEPROM中
static ssize_t at24c02_write(struct file *file,
char __user *buf,
size_t count,
loff_t *ppos)
{
unsigned char buffer[2];
unsigned char addr;
unsigned char data;
int ret;
//1.从用户空间拷贝操作的数据到内核空间
copy_from_user(buffer, buf, 2);
addr = buffer[0]; //地址
data = buffer[1]; //数据
//2.使用SMBUS接口将数据丢给I2C总线驱动,启动I2C总线的硬件传输
//2.1打开SMBUS文档:内核源码\Documentation\i2c\smbus-protocol
//找到对应的SMBUS接口函数
//2.2打开芯片操作时序图
//2.3根据时序图找对应的SMBUS操作函数
//2.4将addr,data和匹配成功的i2c_client通过函数丢给I2C总线驱动
//然后启动I2C总线的硬件传输
ret = i2c_smbus_write_byte_data(g_client, addr, data);
if (ret < 0) { //写失败
printk("write error!\n");
return -EIO;
}
return count;
}
static struct file_operations at24c02_fops = {
.owner = THIS_MODULE,
.read = at24c02_read, //读EEPROM的数据
.write = at24c02_write //写数据到EEPROM中
};
//分配初始化miscdevice
static struct miscdevice at24c02_dev = {
.minor = MISC_DYNAMIC_MINOR, //自动分配次设备号
.name = "at24c02", //dev/at24c02
.fops = &at24c02_fops
};
//client指向内核帮咱们通过i2c_board_info实例化的i2c_client
//client里面包含设备地址addr
static int at24c02_probe(struct i2c_client *client,
struct i2c_device_id *id)
{
//1.注册混杂设备驱动
misc_register(&at24c02_dev);
//2.记录匹配成功的i2c_client
g_client = client;
printk("%s\n", __func__);
return 0; //成功返回0,失败返回负值
}
static int at24c02_remove(struct i2c_client *client)
{
//卸载混杂设备
misc_deregister(&at24c02_dev);
printk("%s\n", __func__);
return 0; //成功返回0,失败返回负值
}
//分配初始化i2c_driver软件信息
static struct i2c_driver at24c02_drv = {
.driver = {
.name = "tangyanjun" //不重要,匹配不靠它
},
.probe = at24c02_probe, //匹配成功执行
.remove = at24c02_remove,
.id_table = at24c02_id
};
static int at24c02_init(void)
{
//注册i2c_driver
i2c_add_driver(&at24c02_drv);
return 0;
}
static void at24c02_exit(void)
{
//卸载
i2c_del_driver(&at24c02_drv);
}
module_init(at24c02_init);
module_exit(at24c02_exit);
MODULE_LICENSE("GPL");
测试程序:
#include
#include
#include
#include
/*
*./at24c02_test w addr data
./at24c02_test w 0x10 0x10
./at24c02_test r addr
./at24c02_test r 0x10
* */
void print_info(char *info)
{
printf("usage:\n");
printf("%s w addr data\n", info);
printf("%s r addr\n", info);
}
int main(int argc, char *argv[])
{
int fd;
unsigned char buf[2];
unsigned char addr;
unsigned char data;
if ((argc != 3) && (argc != 4)) {
print_info(argv[0]);
return -1;
}
fd = open("/sys/devices/platform/s3c2440-i2c/i2c-0/0-0050/eeprom", O_RDWR);
if (fd < 0) {
printf("open at24c02 failed.\n");
return -1;
}
if (strcmp(argv[1], "w") == 0) { //写数据到EEPROM
addr = strtoul(argv[2], NULL, 0);
data = strtoul(argv[3], NULL, 0);
buf[0] = addr;
buf[1] = data;
write(fd, buf, 2);
} else if (strcmp(argv[1], "r") == 0) {//从EEPROM读数据
addr = strtoul(argv[2], NULL, 0);
buf[0] = addr;
read(fd, buf, 1); //read前是地址,read后是数据
printf("data: %c %d %#x\n",
buf[0], buf[0], buf[0]);
}
close(fd);
return 0;
}
结果:
>: ./at24c02_test r 0x20
data: 32 0x20
>: ./at24c02_test w 100 0x10
>: ./at24c02_test r 0x10
data: d 100 0x64
其中读写函数按照上面的路径打开文档并结合芯片读写时序格式即可找出合适的函数。
下面是第二种方式,根据系统提供的适配器来读写eeprom
#include
#include
#include
#include
#include
#include
#include
#include
#include
/********************************************************************************
* Description:
* Input Args:
* Output Args:
* Return Value:
********************************************************************************/
int main(int argc, char **argv)
{
int i;
int fd;
int ret;
int length;
unsigned char rdwr_addr = 0x00; //eeprom 读写地址
unsigned char device_addr = 0x50; //eeprom 设备地址
unsigned char data[] = "the lifeline";
struct i2c_rdwr_ioctl_data e2prom_data;
e2prom_data.nmsgs = 1;
printf("open i2c device...\n");
fd = open("/dev/i2c-0",O_RDWR);
if (fd < 0)
{
printf("open faild");
return -1;
}
e2prom_data.msgs = (struct i2c_msg *)malloc(e2prom_data.nmsgs * sizeof(struct i2c_msg));
if (e2prom_data.msgs == NULL)
{
printf("malloc error");
return -1;
}
ioctl(fd, I2C_TIMEOUT, 1);// 设置超时
ioctl(fd, I2C_RETRIES, 2);// 设置重试次数
/* 向e2prom中写入数据 */
length = sizeof(data);
e2prom_data.msgs[0].len = length;
e2prom_data.msgs[0].addr = device_addr;
e2prom_data.msgs[0].buf =(unsigned char *)malloc(length);
e2prom_data.msgs[0].buf[0] = rdwr_addr;
for(i = 0;i0].buf[1+i] = data[i]; /* write data */
ret = ioctl(fd, I2C_RDWR, (unsigned long)&e2prom_data);
if(ret <0)
{
perror("write data error");
return -1;
}
printf("write data: %s to address %#x\n", data, rdwr_addr);
return 0;
}
哎呀,走到最后,貌似忘了分析一下I2C的驱动结构………………。下面就来个象征性的总结吧,因为本人对看代码这方面的能力确实不强。。
首先,我们的eeprom是256K的,当写满以后他会返回覆盖前面写的,他的读写的设备地址在芯片手册上已经被厂商规定好了,我们必须遵循。然后对于CPU自带的I2C控制器,控制器对于很多设备都有,比如还有LCD的控制器,我们通过访问相应控制器来访问I2C,当然得根据协议
内核将I2C驱动分为设备驱动层、总线驱动层以及核心层。设备驱动层关注数据的特定含义,即数据如何通过总线进行传输,由控制器来实现,其管理的对象是I2C从设备。内核提供了统一的操作方法,I2C设备驱动根据这些方法将数据递交给I2C总线驱动,由I2C总线驱动来操作I2C控制器,完成硬件的数据传输。而一些设备接口文件就在smbus文件中。I2C总线驱动层管理的对象是I2C的硬件控制器,操作控制器完成数据的硬件传输,不关心数据的特定含义,只负责如何传输。然后其传输的数据包括了设备地址、读写位、地址和数据,这些都需要I2C设备驱动层发给I2C总线驱动。核心层也就是关于适配器的问题了,负责设备的配对
而对于我们来说,只负责对设备驱动的开发即可,下面谈谈I2C设备驱动的实现方法
总结:I2C设备驱动程序其实就是围绕着i2c_client和i2c_driver两个结构体,i2c_client描述的I2C从设备的硬件信息,从设备的硬件信息最主要的就是从设备地址。
下面谈谈上面设备驱动的编写。
struct i2c_board_info {
char type[I2C_NAME_SIZE];//最终会赋值给 i2c_client.name
unsigned short flags; //读写标志
unsigned short addr; //设备地址,最终会赋值 i2c_client.addr(i2c_client就有了从设备硬件信息)
void *platform_data;//存放硬件私有的,额外的信 息,最终会赋值给i2c_client.dev.platform_data
int irq;//如果有中断,保存中断信息,最终赋值给i2c_client.irq
};
注意:type和addr必须指定!
struct i2c_board_info eeprom_info[] = {
{
I2C_BOARD_INFO("24c02", 0x50)
}
};
i2c_register_board_info(int busnum,
struct i2c_board_info const *info, unsigned len)
busnum是总线的编号,看看我们的从设备的芯片原理图即可知道
i2c_driver如何使用?
1. 分配i2c_driver
2. 初始化i2c_driver
struct i2c_driver eeprom_drv = {
.driver = {
.name = //不重要
},
.probe = 匹配成功调用
.remove = 卸载调用
.id_table = 其中的name很重,匹配靠它来进行
}
3. 注册i2c_driver
i2c_add_driver();
<1>. 添加软件节点
<2>. 遍历dev链表,取出内核初始化根据i2c_board_info的信息实例化的i2c_client,进行匹配,如果匹配成功,调用probe函数。
probe函数要做什么事,完全由你来决定。
对于I2C浅显的分析就到这里了,更详细的代码讲解请看这篇文章:http://m.blog.csdn.net/The_Lifeline/article/details/71512709