L10. I2C设备驱动实例调试

简介:

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

概述:

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

撸码:

详细信息:

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

概要:

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

流程:

初始化函数:

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 \r\n", __FILE__, __LINE__, __func__);

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

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

    i2c_del_driver(&ap3216c_device_driver);
}

module_init(ap3216c_init);
module_exit(ap3216c_exit);

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

i2c_add_driver();
 i2c_del_driver();

本质是将i2c驱动加载到系统的i2c的链表中。在系统初始化时,会遍历i2c驱动链表将本驱动注册。具体原理可查阅代码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挂在了i2c1总线上,所以在设备树配置中,只需要在i2c1下添加上ap3216c设备节点即可,根据手册查阅本设备i2c地址为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! \r\n", 
                __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! \r\n", 
                __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 \r\n", __FILE__, __LINE__, __func__);
    
    ret = register_driver();
    if(ERROR == ret ) {
        printk("Failed:%s:%d: driver register error! \r\n", __func__, __LINE__);
    }  else {
        printk("%s:%d: driver register successfully \r\n", __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成员也需要赋值。是因为内核在注册i2c设备时,会检测id_table成员是否赋值,若没有赋值则不予注册。因此添加上:

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

实际效果:

 
效果图.png

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

总结:

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

 

本篇代码下载:关注下方公众号,聊天窗口回复“ap3216c”,即可获取下载方式。

记录历经的路,分享个人总结与感悟。欢迎关注公众号“开源519”

 
开源519.jpg

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

拂去尘世尘

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值