IIC设备驱动实例调试

时间:2022-07-22
本文章向大家介绍IIC设备驱动实例调试,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

简介:

在应用到linux的设备(特别是手机)中,大部分硬件设备与主芯片都是通过iic通讯的,譬如TP、加速度传感器、温湿度传感器等等。记录一次自己调试linux开发板iic器件(ap3216c光敏设备)。

概述:

iic通讯线一般只有两条,一条用于时钟控制,一条用于数据通讯。当然也存在单总线通讯,像单片机经常用到的ds18b20。简单介绍两者的区别: 单总线通讯:省略了时钟控制线,其数据格式是时钟+数据。即最先发射时钟,然后再将数据传输出去。从机设备收到时钟时,就会响应主机。 双线通讯:时钟线和数据线。时钟线是固定频率的方波,数据线则在时钟线低电平时传输数据。 iic通讯协议网上总结的相当到位,有时间会整理一下。本篇对iic通讯协议的介绍到此为止,主要是对iic实际设备的代码总结。

撸码:

1详细信息:

平台:imx6ull开发板 linux版本:4.9.88 开发编辑器:gediit

2概要:

在linux源码中,已经实现了iic主机通讯协议传输的各种传输接口。在实际编码时,只需要调用这些接口实现对特殊iic设备的读写,并向外提供读写接口即可。

3流程:

初始化函数:

static struct i2c_driver ap3216c_device_driver = {
    .probe  = ap3216c_probe,
    .remove = ap3216c_remove,
    .driver = {
        .name = PLATFORM_NAME,
        .owner = THIS_MODULE,
        .of_match_table = ap3216c_table,
    },
    .id_table = ap3216c_id,
};

static int __init ap3216c_init(void)
{
    int ret = 0;
    printk("%s:%d: Entry %s rn", __FILE__, __LINE__, __func__);

    ret = i2c_add_driver(&ap3216c_device_driver);
    
    return ret;
}

static void __exit ap3216c_exit(void)
{
    printk("%s:%d: Entry %s rn", __FILE__, __LINE__, __func__);

    i2c_del_driver(&ap3216c_device_driver);
}

module_init(ap3216c_init);
module_exit(ap3216c_exit);

跟普通字符驱动的注册没什么区别,只不过iic注册和卸载驱动的API:

 i2c_add_driver();
 i2c_del_driver();

本质是将iic驱动加载到系统的iic的链表中。在系统初始化时,会遍历iic驱动链表将本驱动注册。具体原理可查阅代码include/linux/i2c.h。

设备树:

&i2c1 {
    clock-frequency = <100000>;
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_i2c1>;
    status = "okay";

    ap3216c: ap3216c@1e {
        compatible = "100ask,ap3216c";
        reg = <0x1e>;
        status = "okay";
    };
};

因为板上的ap3216c挂在了iic1总线上,所以在设备树配置中,只需要在i2c1下添加上ap3216c设备节点即可,根据手册查阅本设备iic地址为0x1e。

入口函数: 本驱动采用了platform总线架构。在platform总线驱动注册完成,驱动.of_match_table成员字符串会与设备树通过compatible匹配,匹配成功后,会进入驱动probe入口函数中。

static int register_driver(void)
{
    /* 1. 设置设备号
     * 主设备号已知, 静态注册;未知, 动态注册。
     */
    if (ap3216c_dev.major){
        ap3216c_dev.devid = MKDEV(ap3216c_dev.major, 0);
        register_chrdev_region(ap3216c_dev.devid, AP3216C_NUM, AP3216C_NAME);
    } else {
        alloc_chrdev_region(&ap3216c_dev.devid, 0, AP3216C_NUM, AP3216C_NAME);
        ap3216c_dev.major = MAJOR(ap3216c_dev.devid);  
    }

    /* 2. 注册驱动结构体 */
    ap3216c_dev.cdev.owner = THIS_MODULE;
    cdev_init(&ap3216c_dev.cdev, &ap3216c_fops);
    cdev_add(&ap3216c_dev.cdev, ap3216c_dev.devid, AP3216C_NUM);

    /* 3. 创建类 */
    ap3216c_dev.class = class_create(THIS_MODULE, AP3216C_CLASS_NAME);  
    if(IS_ERR(ap3216c_dev.class)) {
        printk("Failed:%s:%d: %s under class created failed! rn", 
                __func__, __LINE__, AP3216C_DEVICE_NAME);
        return ERROR;
    }
    /* 4.创建设备 */
    ap3216c_dev.device = device_create(ap3216c_dev.class, NULL, 
                                    ap3216c_dev.devid, NULL, AP3216C_DEVICE_NAME);
    if(NULL == ap3216c_dev.device) {
        printk("Failed:%s:%d: %s device created failed! rn", 
                __func__, __LINE__,  AP3216C_DEVICE_NAME);  
        return ERROR;  
    }  
    
    return OK;
}

static int ap3216c_probe(struct i2c_client * client, const struct i2c_device_id *id)
{
    int ret = -1 ;
    printk("%s:%d: Entry %s rn", __FILE__, __LINE__, __func__);
    
    ret = register_driver();
    if(ERROR == ret ) {
        printk("Failed:%s:%d: driver register error! rn", __func__, __LINE__);
    }  else {
        printk("%s:%d: driver register successfully rn", __func__, __LINE__);
    }
    ap3216c_dev.private_data = client;      
    
    return 0;
}

在probe函数中,主要实现:申请设备号、向外提供读写接口(file_operations结构体)和创建class下设备节点。

读写接口实例化:

static int ap3216c_open(struct inode *inode, struct file *file)
{
    /* ap3216c初始化 在probe配置也可以 */
    file->private_data = &ap3216c_dev;
    ap3216c_write_reg(&ap3216c_dev, AP3216C_SYSTEMCONG, 0x04);
    mdelay(50);    
    ap3216c_write_reg(&ap3216c_dev, AP3216C_SYSTEMCONG, 0x03);

    return 0;
}

static ssize_t ap3216c_read(struct file *file, char __user *buf, size_t cnt, loff_t *off)
{
    /* 读取I2C设备数据 */
    unsigned short data[3];
    int ret = 0;
    struct sap3216c_dev *dev = (struct sap3216c_dev *)file->private_data;    
    
    ap3216c_readdata(dev);
    
    data[0] = dev->ir;
    data[1] = dev->als;
    data[2] = dev->ps;
    ret = copy_to_user(buf, data, sizeof(data));
    
    return 0;
}

static int ap3216c_release(struct inode *inode, struct file *file)
{
    return 0;
}

/* 驱动结构体 */
static struct file_operations ap3216c_fops = {
    .owner = THIS_MODULE,
    .open  = ap3216c_open,
    .read  = ap3216c_read,
    .release = ap3216c_release,
};

由于ap3216c对于上层来讲,只需要读取其检测的3个光感参数值,因此这里只需要提供读取接口即可。具体对ap3216c硬件的如何读写过程单独封装,便于程序多次调用。

问题排查:

在驱动写完以后,发现注册模块时,并不能进入到probe入口函数中。 首先排查设备树与驱动的compatible值,发现一致,找不出问题。 最后经过网上搜索,发现匹配不上的原因,是因为driver中.id_table成员也需要赋值。是因为内核在注册iic设备时,会检测id_table成员是否赋值,若没有赋值则不予注册。因此添加上:

static struct i2c_device_id  ap3216c_id[] = {
    {"100ask,ap3216c", 0},
    {}
};
.id_table = ap3216c_id,

实际效果:

由于我在家调试时,房间开灯,所以数据差值不是太大。具体调试,可自行实验。

总结:

本篇主要记录了iic设备ap3216c的驱动简单实现,对于iic通讯原理并没有做太详细的分析。 在实际学习中,我的经验是:先对iic协议有个详细的了解,然后实际实现一个iic驱动设备,再从头复习iic通讯协议,会对iic通讯有一个更加清晰深刻的认识。 对于使用过单片机调试iic设备的同学,对于iic通讯应该很熟悉了,主要就是对linux驱动注册流程稍加研究即可。