浅析I2C总线驱动

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Peter_tang6/article/details/77866419

最近在看驱动程序的时候,学习了解了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总线协议:

  • start信号:起始信号,由master发起,每当CPU要通过总线访问设备时,先发起这个信号,因为我们的默认电平为高电平(上拉电阻),因此此时我们的数据线便由高电平变为低电平。
  • stop信号:结束信号,每当CPU结束对设备的访问的时候,要发送这个信号,因为我们的默认电平为高电平,因此结束访问以后我们的电平应该变为原始状态,即此时的数据线由低电平变为高电平。
  • ACK信号:应答信号,表明设备是否在位,也表明数据的读写状态。因为我们发送起始信号以后,肯定要确认对方是否接收到了我们的信号,因此,第一个ACK信号就用于表明我们的设备是否准备好,如果没有,那么默认就是放弃访问,那么如何得知应该是哪一个设备应答呢?就是通过我们发送的设备的地址,该设备地址可由芯片手册获得(注意设备地址不包括读写位哦,即为前7位,然后右移一位,补零即可),ACK成功应答以后,接下来就是发送我们要操作的地址了,即我们要在设备的哪个位置进行读写,然后我们就可以发送我们的数据了。而ACK信号是以低电平有效的,因为我们有一个上拉电阻,可能会将其拉高,妨碍我们判断ACK应答是否成功。

我们发现,每次都是数据线改变,这是因为时钟线是用来控制我们的数据线了,我们的时钟不能乱,如果时序乱了,那么发送的数据也会出错,我们可以用示波器去测量捕捉这些信号,以查看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 <linux/i2c/at24.h>  /*添加头文件*/

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 <linux/init.h>
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/miscdevice.h>
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 <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

/*
 *./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<stdio.h>  
#include<stdlib.h>  
#include<linux/i2c.h>  
#include<linux/i2c-dev.h>  
#include<string.h>  
#include<sys/types.h>  
#include<unistd.h>  
#include<fcntl.h>  
#include<errno.h>  
/******************************************************************************** 
 *  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;i<length; i++)  
     e2prom_data.msgs[0].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设备驱动的实现方法

  1. I2C设备驱动管理的对象是I2C总线上的从设备
  2. 原理采用设备-总线-驱动模型,内核同样帮我们定义好了一个虚拟总线I2C,i2c_bus_type,这个总线上维护着两个链表,dev和drv,前者存放硬件信息,后者存放软件信息,dev链表里每一个硬件节点信息对应的数据结构struct i2c_client,drv链表里每一个软件节点信息对应的数据结构struct i2c_driver,一个完整的驱动程序必须有软件还有硬件:每当向dev链表中添加i2c_client硬件节点时,内核会帮你遍历drv链表,取出这个链表中的每一个软件节点,跟自己进行匹配,根据i2c_client.name和i2c_driver.id_table.name进行比较,如果一旦匹配成功,调用i2c_driver的probe函数,并且把匹配成功的硬件节点i2c_client首地址传递给probe函数。每当向drv链表中添加i2c_driver软件节点时,内核会 帮你遍历dev链表,取出这个链表中的每一个硬件节点,根自己进行匹配,根据i2c_driver.id_table.name和i2c_client.name进行比较,如果匹配成功,内核调用i2c_driver的probe函数,把硬件i2c_client传递给probe函数。

总结:I2C设备驱动程序其实就是围绕着i2c_client和i2c_driver两个结构体,i2c_client描述的I2C从设备的硬件信息,从设备的硬件信息最主要的就是从设备地址。

下面谈谈上面设备驱动的编写。

  1. i2c_client不必去手动的分配初始化注册,i2c_client的分配初始化注册要依赖struct i2c_board_info
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必须指定!

  1. 必须在平台代码中分配初始化和注册i2c_board_info
    分配初始化i2c_board_info
struct i2c_board_info eeprom_info[] = {
    {
        I2C_BOARD_INFO("24c02", 0x50)   
    }
  };
  1. 必须在平台代码中进行注册分配好的i2c_board_info
i2c_register_board_info(int busnum, 
struct i2c_board_info const *info, unsigned len) 

busnum是总线的编号,看看我们的从设备的芯片原理图即可知道

  1. 在平台代码执行时,驱动还没有执行到,平台代码首先会从设备的信息都会添加注册到内核维护的一个链表中,每当内核初始化注册I2C总线驱动时,总线驱动会遍历这个链表,取出这个链表中每一个从设备的设备地址,然后总线驱动就会发送一个START信号,然后在发送这个设备地址,如果设备存在这个总线上,设备肯定返回一个ACK,一旦被总线驱动捕获,那么内核会帮你实例化一个i2c_client,会帮你分配i2c_client,根据之前注册的i2c_board_info来初始化i2c_client最后帮你注册i2c_client.

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

猜你喜欢

转载自blog.csdn.net/Peter_tang6/article/details/77866419