LinuxI2C总线驱动

一.概念

 I2C总线:


1.回顾相关的概念
串行传输:
一个时钟周期传输1bit
并行传输:
一个时钟周期传输多字节
"一个时钟周期":CPU在时钟的高电平或者下降沿将数据发送到数据线上,那么设备在同周期的低电平或者上升沿从数据线上获取数据;

总线:硬件上实实在在存在的总线,总线上可以挂接多个外设,将来CPU通过总线来访问具体的某个外设

2.I2C总线概念:
两线式串行总线
"两线式":CPU和外设之间的数据通信只需两根线即可搞定;
两根线分别是:数据线SDA和时钟线SCL;
SDA:数据线,用于传输数据,如果CPU向设备写数,SDA的控制权交给CPU;如果CPU从设备读数,SDA的控制权交给外设;

"控制权":谁配输出谁控制,谁配输入谁释放控制权;

SCL:时钟线,用于控制CPU和外设的数据同步,时钟线只能由CPU发起,CPU控制;
例如CPU在时钟的高电平或者下降沿将数据发送到数据线上,那么设备在同周期的低电平或者上 升沿从数据线上获取数据;

上拉电阻:SDA和SCL都会连接一个上拉电阻,默认电平为高电平;

“串行”:I2C总线在数据传输时,是1个时钟周期传输1bit;

"总线":SCL和SDA这两根信号线上可以挂接多个外设,将来CPU就是通过两根信号线来访问某个外设;

LinuxI2C总线驱动_第1张图片

二.编程实现

问:如何利用GPIO模拟I2C时序操作外设?

1.linux内核I2C驱动框架:
1.1.明确:
I2C控制器:一般集成在CPU内部,主动帮你发起I2C的时序,这里需要配合相关的寄存器,

比如:
设备地址相关寄存器,用于存储要操作的设备地址
片内地址相关寄存器,用于处处要操作的片内地址
片内数据相关的寄存器,用于存储要操作的片内数据

其他的,例如START,STOP,ACK信号都是标准的,I2C控制器来帮你发起,至于设备地址,片内地址,片内数据,
只需往对应的寄存器写入或者读取即可,也最终是有硬件进行操作!

I2C驱动分为两类:I2C总线驱动和I2C设备驱动

1.2.I2C总线驱动
管理的硬件设备是I2C控制器
只负责发起硬件的操作时序
不关注操作的数据含义(设备地址,片内地址,片内数据)
操作的数据(设备地址,片内地址,片内数据)来源I2C设备驱动
此代码都是由芯片厂家完成,驱动开发者只需要做配置内核,添加对应的I2C总线驱动的支持即可:
cd /opt/kernel
make menuconfig
Device Drivers->
I2C supports->
I2C Hardware Bus support --->
<*> S3C2410 I2C Driver //S5PV210 I2C控制器的驱动配置选项

问:对应的源码找出来?

1.3.I2C设备驱动
管理的硬件设备是I2C外设本身
不关心数据如何传输,传输靠I2C总线驱动,但是I2C设备驱动需要将数据丢给I2C总线驱动

此代码一般都是由驱动开发者来实现!


*****************************************************************
1.4.I2C驱动框架分层
应用程序:
将0x55写入AT24C02片内地址0x10存储空间中
struct at24c02_data {
unsigned char addr; //片内地址
unsigned char data; //片内数据
};
struct at24c02_data data;
data.addr = 0x10;
data.data = 0x55;
ioctl(fd, AT24C02_WRITE, &data);
------------------------------------------------------
I2C设备驱动层:
at24c02_ioctl(cmd, arg) {
struct at24c02_data kdata;
copy_from_user(&kdata, arg, sizeof);
//将用户要操作的片内地址和片内数据拷贝到内核
kdata.addr = 0x10; //片内地址
kdata.data = 0x55; //片内数据

//将这些数据丢给I2C总线驱动,完成最终的硬件传输
}
------------------------------------------------------
SMBUS接口层:由内核已经写好
作用:实现I2C设备驱动和I2C总线驱动的数据交互
本质:桥梁的作用
------------------------------------------------------
I2C总线驱动层:
负责处理从I2C设备驱动发送来的数据
------------------------------------------------------
硬件层:
I2C控制器 《=》I2C外设


三.实现I2C设备驱动

1.问如何实现一个I2C设备驱动呢?
答:采用设备-总线-驱动编程模型

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

1.1.回顾platform机制:
虚拟总线:platform_bus_type
.match函数 用于匹配

硬件节点:struct platform_device
.name 用于匹配
.dev = {
.platform_data 装载自定义的硬件信息
},
.resource 装载resource类型的硬件信息
软件节点:struct platform_driver
.driver = {
.name 用于匹配
}
.probe 匹配成功调用,形参pdev指向硬件
.remove 卸载调用,形参pdev指向硬件


~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

1.2.i2c设备驱动采用设备-总线-驱动编程模型
内核定义的I2C虚拟总线:i2c_bus_type
.match 用于匹配,内核调用
描述I2C外设的硬件数据结构:struct i2c_client
描述I2C外设的软件数据结构:struct i2c_driver

总结:完成一个I2C设备驱动程序,只需外围着以上两个数据结构即可!

1.3.struct i2c_client的使用
struct i2c_client {
unsigned short addr;
char name[I2C_NAME_SIZE];
struct device dev;
int irq;
};

作用:描述I2C外设的硬件信息
成员:
addr:I2C外设的设备地址
name:I2C外设硬件节点的名称,将来用于匹配
以上两个字段切记一定要初始化!

dev:其中的platform_data装载自定义的硬件信息
irq:如果采用中断,装载中断号

注意:
驱动程序不会直接对i2c_client进行操作,
例如:
1.定义一个i2c_client对象
2.初始化对象
3.注册硬件节点到内核dev链表上
以上三步骤都是由内核来帮你完成!
此时驱动只需将I2C外设的设备地址和硬件节点的名称告诉给内核,内核就帮你完成以上三步骤;

驱动将I2C外设信息告诉内核的方法采用:
struct i2c_board_info {
char type[I2C_NAME_SIZE];
unsigned short addr;
void *platform_data;
int irq;
};
作用:描述I2C外设的硬件信息,将来驱动利用此数据结构将硬件信息告诉内核,
内核在利用提交的信息,将i2c_client进行初始化,完成最终的注册过程;
成员:
type:将来用于匹配,将来会赋值给i2c_client.name
addr:设备地址,将来会赋值给i2c_client.addr
platform_data:装载自定义的硬件信息,将来会赋值给i2c_client.dev.platform_data
irq:中断号,将来会赋值给i2c_client.irq
注意:type,addr必须要初始化,内核对于这两个字段单独给出了一个宏进行初始化:

I2C_BOARD_INFO(“tarena”, 0x50);
.type = "tarena" //将来赋值给i2c_client.name
.addr = 0x50 //将来赋值给i2c_client.addr

驱动开发者使用的操作步骤:
明确:以下代码操作必须在内核源码的平台代码文件中进行;不能以模块的形式动态加载或者卸载!

某个开发板的平台代码:
1.cd /opt/kernel
2.vim arch/arm/mach-s5pv210/mach-cw210.c
3.以开发板自带的at24c02存储器为例,添加这个芯片的i2c_board_info信息
4.在平台代码中添加at24c02的i2c_board_info的支持
在头文件后面,添加以下代码:
static struct i2c_board_info at24c02[] = {
{
I2C_BOARD_INFO("at24c02", 0x50)
}
};

5.在smdkc110_machine_init函数中注册AT24C02的硬件信息到内核,供内核使用,将来内核根据注册的硬件信息来初始化i2c_client

i2c_register_board_info(0, at24c02, ARRAY_SIZE(at24c02));
作用:注册定义初始化的硬件信息到内核
参数:
第一个参数表示I2C外设所在的总线编号,通过原理图获取
...

6.make zImage

7.cp arch/arm/boot/zImage /tftpboot

8.重启开发板,一旦内核启动完毕,
此时此刻,在内核的dev链表上就有一个描述AT24C02的硬件节点(i2c_client),静静等待软件节点i2c_driver的到来!

2.4.struct i2c_driver的使用
struct i2c_driver {
struct device_driver 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外设的软件信息
成员:
driver:其中的name不再用于匹配,也不重要
id_table:其中的name用于匹配,必须初始化,将来要和i2c_client的name进行对比
probe:匹配成功调用,形参client指向匹配成功的硬件信息
remove:卸载软件调用,形参client指向匹配成功的硬件信息

驱动使用步骤:
1.定义初始化软件节点
2.调用i2c_add_driver注册
内核遍历dev链表,进行匹配,调用
4.调用i2c_del_driver卸载

案例:编写开发板的AT24C02的I2C设备驱动
从ftp/drv/下载at24c02.tar.bz2解压缩
先看at24c02_1

cp at24c02_1 /opt/drivers/day12/
cd /opt/drivers/day12/at24c02_1
阅读源码
make
cp at24c02_drv.ko /opt/rootfs

ARM执行:
1.insmod at24c02_drv.ko
2.查看打印信息

2.6.probe函数一般做三件事
1.通过形参client指针获取硬件信息
2.处理硬件信息
3.注册I2C外设的硬件操作方法

案例:

1.参看at24c02_2源码

#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define I2C_READ    0x100001
#define I2C_WRITE   0x100002

struct eeprom_data {
    unsigned char addr;
    unsigned char data;
};

static struct i2c_device_id at24c02_id[] = {
        {"at24c02", 0}, 
        //”at24c02“必须和i2c_board_info的type一致,匹配靠它进行
};

static struct i2c_client *g_client; //记录匹配成功的i2c_client

//从EEPROM读取数据
/*
 *           addr
    app:ioctl----->
        <--------data
                     addr
        at24c02_ioctl----->
                <---------data
                          addr
                    SMBUS----->
                    <---------data
                                    addr
                            总线驱动-----> START 设备地址 写  ACK addr ACK START 设备地址 读 ACK 返回数据data NOACK STOP
                                    <----data
 */
static unsigned char at24c02_i2c_read(unsigned char addr)
{
    /*
       1.使用SMBUS接口将数据(地址和设备地址)丢给I2C总线驱动,启动I2C总线的硬件传输
       1.1打开SMBUS文档:内核源码\Documentation\i2c\smbus-protocol找到对应的SMBUS接口函数
       1.2打开芯片操作时序图
       1.3根据时序图找对应的SMBUS操作函数
       1.4将addr和匹配成功的i2c_client通过函数丢给I2C总线驱动然后启动I2C总线的硬件传输
    */
    return i2c_smbus_read_byte_data(g_client, addr);
}

//写数据到EEPROM中
/*
 *           addr,data
    app:ioctl--------->
                     addr,data
        at24c02_ioctl---------->
                          addr,data
                    SMBUS---------->
                                    addr,data
                            总线驱动----------> START 设备地址 写  ACK addr ACK data ACK STOP
 */
static void at24c02_i2c_write(unsigned char addr,unsigned char data)
{
    /*
       1.使用SMBUS接口将数据(地址,数据,设备地址(g_client->addr))丢给I2C总线驱动,启动I2C总线的硬件传输
       1.1打开SMBUS文档:内核源码\Documentation\i2c\smbus-protocol
       找到对应的SMBUS接口函数
       1.2打开芯片操作时序图
       1.3根据时序图找对应的SMBUS操作函数
       1.4将addr,data和匹配成功的i2c_client通过函数丢给I2C总线驱动然后启动I2C总线的硬件传输
    */
    i2c_smbus_write_byte_data(g_client, addr, data);
}

static long at24c02_ioctl(struct file *file,
                            unsigned int cmd,
                            unsigned long arg)
{
    struct eeprom_data eeprom;

    //拷贝用户空间操作的数据信息到内核空间
    copy_from_user(&eeprom, 
                (struct eeprom_data *)arg, 
                sizeof(eeprom));

    switch(cmd) {
            case I2C_READ: //读
                    eeprom.data = at24c02_i2c_read(eeprom.addr);
                    copy_to_user((struct eeprom_data *)arg,
                                    &eeprom, sizeof(eeprom));
                break;
            case I2C_WRITE://写
                    at24c02_i2c_write(eeprom.addr, eeprom.data);
                break;
            default:
                return -1;
    }
    return 0;
}

static struct file_operations at24c02_fops = {
    .owner = THIS_MODULE,
    .unlocked_ioctl = at24c02_ioctl
};


//分配初始化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;
    return 0; //成功返回0,失败返回负值
}

static int at24c02_remove(struct i2c_client *client) 
{
    //卸载混杂设备
    misc_deregister(&at24c02_dev); 
    return 0; //成功返回0,失败返回负值
}

//分配初始化i2c_driver软件信息
static struct i2c_driver at24c02_drv = {
    .driver = {
        .name = "tarena" //不重要,匹配不靠它
    },
    .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 
#include 

#define GPIO_I2C_READ    0x100001
#define GPIO_I2C_WRITE   0x100002

struct eeprom_data {
    unsigned char addr;
    unsigned char data;
};

int main(void)
{
    struct eeprom_data eeprom;
    int i;
    char *pversion = "S14091207"; //软件版本信息
    char *p = pversion;
    char buf[10] = {0};

    int fd = open("/dev/at24c02", O_RDWR);
    if (fd < 0)
        return -1;

    //写入版本信息
    for (i = 0; i < strlen(pversion); i++) {
        eeprom.data = *p++;
        eeprom.addr = i;
        ioctl(fd, GPIO_I2C_WRITE, &eeprom);
        usleep(5000);
    }
    
    //读取版本信息
    for (i = 0; i < strlen(pversion); i++) {
        eeprom.addr = i;
		//eeprom.data = ?
        ioctl(fd, GPIO_I2C_READ, &eeprom);
        buf[i] = eeprom.data;
    }

    //打印版本信息
    printf("version = %s\n", buf);
    close(fd);
    return 0;
}


  ********************************
2.6.SMBUS接口的使用步骤:
1.打开SMBUS接口说明文档
内核源码\Documentation\i2c\smbus-protocol
2.打开I2C外设的芯片手册,找到要操作的时序图
3.根据手册的操作时序图,在smbus-protocol文档中找到对应的函数,例如:
i2c_smbus_read_byte() //将来的硬件操作时序如下:
S Addr Rd [A] [Data] NA P 看看这个函数的时序根硬件手册的时序是否匹配
4.一旦找到合适的函数,赋值函数名,在内核源码中搜索函数的原型,然后再驱动代码中使用此函数

案例:
软件版本号格式:SYYMMDDXY,例如:S15080400
硬件版本号格式:HYYMMDDXY,例如:H15080400
版本信息储存在AT24C02中;
涉及到地址分配的问题:
软件版本号的存储地址:0x00 ~ 0x10
硬件版本号的存储地址: 0x11 ~ 0x20

你可能感兴趣的:(驱动开发)