驱动学习笔记12 内核I2C驱动编程框架

2.linux内核I2C驱动编程框架


面试题:谈谈对I2C总线的理解


2.1.回顾I2C总线协议


    以MMA8653三轴加速度传感器为例 
    定义:七个字/SCL/SDA/串行/并行/总线/上拉电阻
    提问题:
        CPU如何找到外设:通过设备地址
        CPU如何和外设进行数据传输呢:框框圈圈图,单字节寄存器写/读,多字节寄存器的写/读
        SCL和SDA的配合:四个字:低放高取(大招)

2.2.问:linux内核如何编写一个I2C总线驱动程序呢?


      答:很简单,只需将ARM裸板课上编写的I2C总线程序移植改造即可,只需将ARM裸板课上的物理地址
      做个ioremap映射即可,然后封装read,write,ioctl接口即可访问
      弊端:肯定是没有问题的,但是代码编写的工作量极大,又要操作I2C控制器和内部的寄存器,又要关心
      I2C外设本身的各种寄存器信息,特别多的操作

2.3.linux内核I2C驱动编程框架(分层思想) 


    分层思想:就是将自己用ioremap实现的I2C驱动代码进行高度的封装,让开发者尽量避免查看
    I2C控制器相关的操作,I2C控制器如何操作你不用关心,这些芯片厂家帮你完成!
问:如何体现分层思想呢?
答:linux内核将I2C驱动分成三部分:I2C总线驱动,I2C设备驱动,SMBUS接口层
1.I2C总线驱动特点
    操作硬件:I2C总线驱动只操作I2C控制器本身(也就是操作各种I2C控制器内部的寄存器)
    功能:既然操作I2C控制器和内部的寄存器,也就是最终发起I2C外设要求的各种时序
    最终完成设备地址,片内寄存器地址和数据的传输
    类似:卡车,只负责运输,不关心运输的货物(设备地址,片内寄存器地址和片内寄存器数据)
    好消息:此驱动代码由芯片厂家提供
    掌握其配置添加过程:
    cd /opt/kernel
    make menuconfig
        Device Drivers->
            <*>I2C supports->
                 I2C Hardware Bus support  ---> //配置I2C总线驱动入口
                     <*> Slsiap I2C  //S5P6818的I2C控制器的驱动,又称总线驱动
                                  只负责发起时序,至于传输什么数据它不操心


2.I2C设备驱动:
    操作硬件:只关心I2C外设相关的信息(设备地址,片内寄存器地址,片内寄存器数据)
        I2C总线驱动发起硬件时序,时序里面传输的数据(设备地址,片内寄存器地址,片内寄存器数据)
        都是由I2C设备驱动来提供的
        并且还要给应用程序提供操作接口
        类似:农民,提供货物(设备地址,片内寄存器地址,片内寄存器数据)
                  将来这些获取丢给I2C总线驱动来负责运输
        好消息:此驱动由驱动工程师编写
3.SMBUS接口层
    功能:连接I2C总线驱动和I2C设备驱动,起到了桥梁的作用,类似:农民和卡车司机中间的介绍人
    I2C设备驱动将来调用SMBUS接口成将传输的数据(设备地址,片内寄存器地址,片内寄存器数据)丢给I2C
    总线驱动,最终由I2C总线驱动操作I2C控制器发起硬件时序完成数据的传输
    好消息:此代码由linux内核完成!

2.4.linux内核I2C驱动分层:


    以CPU读取MMA8653的ID值为例,ID值为0x5A,并且对应的片内寄存器地址是0x0D
    1.应用层(驱动工程师)
        #define  MMA_READ    0x100001 //读片内寄存器命令
        #define  MMA_WRITE    0x100002 //写片内寄存器命令
        struct  mma {
            unsigned char addr; //指定访问的片内寄存器地址
            unsigned char data; //指定访问的片内寄存器数据
        };
        struct mma mma; //分配用户缓冲区
        mma.addr = 0x0D; //指定要访问的片内寄存器地址是0x0D
        mma.data = ? //目标
        ioctl(fd, MMA_READ, &mma);
        printf("ioctl返回之后,ID=%#x\n", mma.data); //ID=0x5A
    
    2.I2C设备驱动层(驱动工程师)
    功能:负责提供操作接口,获取操作的数据信息,根据用户的需求(读或者写需求)调用内核提供的
    SMBUS接口函数最终完成和I2C总线驱动的数据传输
    参考代码:
    //给用户提供的ioctl接口
    long mma_ioctl(file, cmd, unsigned long buf) { //注意:buf=&mma=用户缓冲区的首地址
        //分配内核缓冲区
        struct mma kmma;

        //拷贝用户缓冲区到内核缓冲区,获取用户要访问的片内寄存器地址或者数据
        copy_from_user(&kmma, (struct mma *)buf, sizeof(kmma));
        
        //此时:kmma.addr = 0x0D, kmma.data = ?
        //调用内核提供的SMBUS接口函数将片内寄存器的地址丢给I2C总线驱动
        并且让I2C总线驱动发起读单字节的时序获取ID=0x5A返回给I2C设备驱动
        kmma.data = i2c_smbus_read...(..., kmma.addr/*发送片内寄存器地址*/, 0x1D/*顺便发个设备地址*/);
        
        //将内核缓冲区的数据拷贝到用户缓冲区
        copy_to_user((struct mma *)buf, &kmma, sizeof(kmma));
        return 0;
    }
    
    3.SMBUS接口层(linux内核提供)
        连接I2C设备驱动和I2C总线驱动
        unsigned char i2c_smbus_read...(...., unsigned char addr, unsigned char dev_addr)
        {
            //大神调用I2C总线驱动提供的接口函数完成对I2C总线驱动的调用
            发起硬件时序的操作
            return  I2C总线驱动read1字节(addr, dev_addr);
        }
    
    4.I2C总线驱动层(芯片厂家提供)
        只负责操作I2C控制器,只负责根据不同的需求发起不同的时序
        unsigned char  I2C总线驱动read1字节(unsigned char addr, unsigned char dev_addr) {
            //各种ioremap寄存器的物理地址,然后各种操作寄存器的相应的bit位
            //最终发起读1字节的时序,并且获取ID值
            ST->dev_addr<<1|0->(ACK)->addr->(ACK)->ST->dev_addr<<1|1->(ACK)
            <-(0x5A)->NACK->SP
            return 0x5A;
        }
    5.硬件层(硬件工程师)
        S5P6818<-----SCL/SDA------->MMA8653
                               时序
    总结:linux内核分层思想的终极目标就是尽可能的让开发人员少关注底层的硬件操作细节!降低开发的工作量!

2.5.问:如何开发一个I2C设备驱动呢?


    答:采用设备dev-总线bus-驱动drv编程模型(分离思想)
    platform也是采用这种编程模型,I2C设备驱动也是采用此编程模型    
    实现过程如下:
    1.在linux内核中已经帮你定义好了一个虚拟总线叫i2c_bus_type(platform的虚拟总线叫platform_bus_type)在这个总线上(就是一个结构体变量)维护着两个链表:dev链表和drv链表
    
    2.dev链表上每个节点描述的是I2C外设的纯硬件信息(设备地址啊,中断号啊等)对应的结构体叫struct i2c_client,每当向dev链表添加一个硬件节点,驱动工程师只需定义初始化一个struct i2c_client硬件节点然后注册到dev链表即可,内核会帮你遍历drv链表,取出drv链表上每个软件节点跟这个要注册的硬件节点进行匹配,匹配是内核调用总线提供的match函数,比i2c_client.name和软件节点i2c_driver.id_table.name,如果匹配成功,内核自动调用软件节点probe函数,并且把匹配成功的硬件节点的首地址传递给probe函数,如果匹配不成功,没关系,硬件节点静静等待着软件节点的到来    

    3.drv链表上每个节点描述的是I2C外设的纯软件信息(混杂设备,操作接口等)
    对应的结构体叫struct i2c_driver,每当向drv链表添加一个软件节点,驱动工程师只需定义初始化一个
    struct i2c_driver软件节点然后注册到drv链表即可,内核会帮你遍历dev链表,取出dev链表上每个硬件节点
    跟这个要注册的软件节点进行匹配,匹配是内核调用总线提供的match函数,比较i2c_client.name和软件节点
    i2c_driver.id_table.name,如果匹配成功,内核自动调用软件节点的probe函数,并且把匹配成功的硬件节点的
    首地址传递给probe函数,如果匹配不成功,没关系,软件节点静静等待着硬件节点的到来

    4.结论:驱动工程师只需关注两个结构体:
        struct i2c_client
        struct i2c_driver

        只需做:定义初始化硬件或者软件节点注册到dev或者drv链表即可
        内核帮你做:遍历,匹配,调用probe,给probe函数传递参数

2.6.详解struct i2c_client


    struct i2c_client {
        unsigned short addr;
        char name[I2C_NAME_SIZE];
        struct  device dev;
        int irq; 
        ...
    };    
    功能:描述I2C外设的纯硬件信息
    addr:指定I2C外设的设备地址,用于找外设
    name:用于匹配
    irq:如果I2C外设和处理器之间连接中断线,irq保存着对应的中断号
    dev:重点关注其中的void *platform_data,用于装载自定义的硬件信息
    切记:其中addr和name必须初始化
    注意:驱动工程师无需用此结构体定义初始化和注册struct i2c_client硬件节点对象
        这些工作都是由linux内核帮你完成:帮你定义一个i2c_client对象,
        帮你初始化i2c_client对象,帮你向dev链表注册i2c_client对象
    问:内核可以帮我定义和注册i2c_client对象,内核怎么初始化i2c_client对象呢
    内核怎么知道设备地址和name和中断号和自定义的硬件信息呢?这些初始化的信息
    内核显然不知道
    答:驱动工程师只需用以下结构体定义初始化注册硬件信息给linux内核使用即可
    linux内核将来根据你提供的硬件信息来初始化i2c_client
    struct i2c_board_info {
        char        type[I2C_NAME_SIZE];
        unsigned short    addr;
        void        *platform_data;
        int        irq;
    };
    功能:专门给linux内核提供初始化i2c_client的硬件信息
    type:指定一个名称,将来会赋值给i2c_client.name,所以用于匹配
    addr: 设备地址,将来会赋值给i2c_client.addr
    platform_data:指定自定义的硬件信息,将来会赋值给i2c_client.dev.platform_data
    irq:指定中断号,将来会赋值给i2c_client.irq
    
    配套函数:
    int  i2c_register_board_info(int busnum, struct i2c_board_info const *info, unsigned n);
    功能:向内核注册i2c外设的纯硬件信息,将来内核拿着这些信息帮你初始化i2c_client对象
    参数:
    busnum:指定I2C外设所在的总线编号,必须查看原理图
        例如:MMA8653连接到S5P6818的I2C2总线上,所以:busnum=2
    info:指向定义初始化好的i2c_board_info对象,这个对象就是纯硬件信息
    n:指定初始化的硬件信息的个数,也就是i2c_board_info对象的个数
    
    注意:i2c_board_info的定义初始化和注册代码必须和uImage写在一起,不能insmod和rmmod
    一般代码添加到内核源码的arch/arm/plat-s5p6818/x6818/device.c文件中

案例:编写MMA8653的I2C设备驱动,目前添加它的硬件节点


上位机执行:
cd /opt/kernel
make menuconfig
    Device Drivers->
         <*> Hardware Monitoring support  ---> 
            //按N键去除官方的MMA8653的I2C设备驱动            
             <*>   Freescale MMA865X 3-Axis Accelerometer
保存退出
vim arch/arm/plat-s5p6818/x6818/device.c 
在文件的开头添加如下代码:
#include <linux/i2c.h> //struct i2c_board_info声明
//定义初始化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, 1);//向内核注册将来初始化i2c_client的纯硬件信息
保存退出

心里明白:将来内核会把你定义一个i2c_client对象,内核会用你注册的硬件信息初始化i2c_client对象
然后内核向dev链表注册i2c_client对象,然后等待着软件节点的到来!

make uImage
用fastboot重新烧写uImage即可,此时内核一旦启动,就会有一个硬件节点i2c_client静静等待着对应的软件节点到来!

2.7.详解struct i2c_driver


    struct i2c_driver {
        struct device_driver driver;
        const struct i2c_device_id *id_table;
        int (*probe)(struct i2c_client *client, const struct i2c_device_id *id);
        int (*remove)(struct i2c_client *client);
        ...
    };
    功能:描述I2C外设的纯软件信息
    driver:其中的name不再用于匹配
        而platform_driver的driver的name是用于匹配的
    id_table:其中的name用于匹配
        struct i2c_device_id {
            char name[I2C_NAME_SIZE];
        };
    probe:匹配成功,内核调用此函数
        形参client指针:指向匹配成功的硬件节点,利用client可以获取硬件信息
            client->addr:获取设备地址
            client->dev.platform_data:获取自定义的硬件信息
            client->irq:获取中断号
        形参id:跟id_table成员指向的对象是一样的,主要用于传递参数
    remove:卸载软件节点内核调用此函数
        形参client指针:指向匹配成功的硬件节点,利用client可以获取硬件信息
    
    配套函数:
    i2c_add_driver:向内核注册软件节点,内核帮你做四件事
    i2c_del_driver:从内核中卸载软件节点


    
    案例:编写MMA8653的I2C设备驱动,现在添加MMA8653的软件节点代码


    参考代码:day11/3.0
    下位机测试:
    cd /home/drivers
    insmod mma8653_drv.ko //此时调用probe函数
    rmmod mma8653_drv //此时调用remove函数


    
    案例:继续完善probe和remove函数,实现能够循环读取XYZ三个方向的加速度值


    参考代码:day11/4.0
    下位机测试:
    cd /home/drivers
    insmod mma8653_drv.ko //此时调用probe函数
    ./mma8653_test //观察XYZ加速值
    rmmod mma8653_drv //此时调用remove函数

总结:内核提供的SMBUS接口函数使用步骤:
功能:I2C设备驱动调用SMBUS接口函数能够让I2C总线驱动帮你发起所需的时序,最终完成数据的读写操作!
1.打开SMBUS接口函数的说明文档:位于内核源码/opt/kernel/Documentation/i2c/smbus-protocol
   注意:comm指的就是片内寄存器地址
2.然后根据你要求的时序在这个文档中选择对应的函数
    例如:读取mma8653的id为例(片内寄存器地址是0x0d,id=0x5a)
    st->0x1d<<1|0->(ack)->0x0d->(ack)->st->0x1d<<1|1->(ack)->(0x5a)->nack->sp
    根据此时序在这个文档中找到对应的函数是:i2c_smbus_read_byte_data
3.一旦根据时序找到了对应的函数,然后利用sourceinsight在内核源码中找到此函数的定义
    获取这个函数的原型
    注意:这些函数的参数说明:
    第一个参数client指针:永远指向匹配成功的硬件节点
    第二个参数command:传递要访问的片内寄存器地址
    第三个参数value:传递要向片内寄存器写入的数据(仅限于写寄存器)
    返回值:如果是读片内寄存器值,返回值就是片内寄存器的数据

合并XYZ加速度值的位运算注意事项:
通过MMA8653芯片手册P19得到:
    存储区X方向加速度值的寄存器地址分别是:0x01(存储10位加速值的高位)和0x02(存储10位加速度值的低位)
    注意:0x01寄存器的8位作为10位加速度值的高8位(bit[9:2])
              0x02寄存器的bit[7:6]作为10位加速度值的低2位(bit[1:0])
    存储区Y方向加速度值的寄存器地址分别是:0x03(存储10位加速值的高位)和0x04(存储10位加速度值的低位)
    注意:0x03寄存器的8位作为10位加速度值的高8位(bit[9:2])
              0x04寄存器的bit[7:6]作为10位加速度值的低2位(bit[1:0])
    存储区Z方向加速度值的寄存器地址分别是:0x05(存储10位加速值的高位)和0x06(存储10位加速度值的低位)
    注意:0x05寄存器的8位作为10位加速度值的高8位(bit[9:2])
              0x06寄存器的bit[7:6]作为10位加速度值的低2位(bit[1:0])

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
学习 Linux 驱动开发需要掌握以下几个方面的知识: 1. Linux 操作系统的内核架构和基本原理。 2. C 语言编程基础和数据结构。 3. 设备驱动的基本概念和工作原理。 4. Linux 驱动程序的开发方法和调试技巧。 以下是我给出的学习笔记: 1. Linux 操作系统的内核架构和基本原理 - Linux 内核的基本组成部分 - 进程管理 - 内存管理 - 文件系统 - 网络协议栈 - 设备驱动 - Linux 内核的编译和安装 - 下载内核源码 - 配置内核选项 - 编译内核 - 安装内核 - Linux 内核的模块化 - 模块的编写和编译 - 模块的加载和卸载 - 模块的依赖关系 - Linux 内核的调试方法 - printk 函数 - kerneloops 机制 - oops 分析工具 2. C 语言编程基础和数据结构 - C 语言基础 - 变量和常量 - 运算符和表达式 - 控制语句 - 函数和指针 - 数据结构 - 数组和指针 - 链表和树 - 栈和队列 - 散列表和堆 3. 设备驱动的基本概念和工作原理 - 设备驱动的分类 - 字符设备驱动 - 块设备驱动 - 网络设备驱动 - 设备驱动的基本概念 - 设备文件 - 设备节点 - 设备号 - 设备驱动的工作原理 - 设备文件的打开和关闭 - 设备文件的读和写 - 设备文件的控制 4. Linux 驱动程序的开发方法和调试技巧 - 设备驱动的开发方法 - 设备驱动框架 - 设备驱动的初始化和清理 - 设备驱动的操作函数 - 设备驱动的中断处理函数 - 设备驱动的调试技巧 - printk 函数的使用 - ftrace 工具的使用 - gdb 调试工具的使用 以上是我给出的 Linux 学习笔记,希望对你有所帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值