Linux驱动-I2C子系统驱动

最近在学Linux下I2C驱动,了解了I2C子系统框架之后,实现LM75温度传感器驱动。

LM75是一款常用I2C接口温度传感器,通过I2C总线读取温度寄存器里的数据,我们这里用I2C子系统 + 杂项设备(miscdevice) 来实现驱动,不用手动申请主设备号,开发简单,很适合I2C驱动入门。

一、整体思路

Linux内核提供I2C子系统,把I2C适配器、I2C设备、I2C驱动做了分层。我们写驱动的时候,不用去操作底层寄存器模拟I2C时序,只需要按照框架写probe、remove函数,通过i2c_msg构造I2C读写消息,调用master_xfer完成I2C通信。

同时用misc杂项设备,自动分配次设备号,生成/dev/lm75_misc设备文件。应用层直接open这个设备,read就能读到传感器原始数据,再在应用程序里换算成实际温度。

整套分为两部分:内核驱动代码 + 用户层测试APP。

二、驱动代码拆解

1. 头文件说明

复制代码
#include <linux/fs.h>        // 文件操作结构体file_operations
#include <linux/cdev.h>      // 字符设备,这里没用,misc内部封装了cdev
#include <linux/module.h>    // 模块加载、卸载宏
#include <linux/init.h>      // __init __exit宏
#include <linux/i2c.h>       // I2C子系统核心头文件,i2c_client、i2c_msg都在这里
#include <linux/miscdevice.h>// 杂项设备miscdevice
#include <asm/uaccess.h>     // copy_to_user 用户态内核态数据拷贝

module_init/module_exit:驱动入口出口,insmod时执行init,rmmod执行exit。

THIS_MODULE:指向当前模块,防止驱动被卸载时还在被调用。

copy_to_user:内核空间的数据拷贝到用户空间,不能直接在内核里访问用户层指针,会触发内存异常。

2. 全局变量 i2c_client

复制代码
static struct i2c_client *lm75_client = NULL;

i2c_client代表I2C从设备(也就是我们的LM75),设备树匹配成功后,probe函数会拿到这个结构体,里面存放从机地址、所属I2C适配器等信息。后面I2C通信都依赖它。

3. lm75_read:设备read回调函数

应用层调用read(fd, buf, len),内核就会跑到这个函数。

复制代码
static ssize_t lm75_read(struct file *fp, char __user *puser, size_t n, loff_t *off)
{
struct i2c_msg sendmsg;
struct i2c_msg recvmsg;
int ret = 0;
unsigned char tmpbuff[2] = {0};
unsigned long nret = 0;
memset(&sendmsg, 0, sizeof(sendmsg));
memset(&recvmsg, 0, sizeof(recvmsg));

    sendmsg.addr = lm75_client->addr;
    tmpbuff[0] = 0x00;    // LM75温度寄存器地址0x00
    sendmsg.buf = tmpbuff;
    sendmsg.len = 1;
    // 发送消息:告诉LM75,我要读0x00寄存器
    ret = lm75_client->adapter->algo->master_xfer(lm75_client->adapter, &sendmsg, 1);
if (ret != 1) {
        pr_info("master_xfer sendmsg failed ret = %d\n", ret);
return -1;
    }
    recvmsg.addr = lm75_client->addr;
    recvmsg.flags |= I2C_M_RD; // 标记这是I2C读消息
    recvmsg.buf = tmpbuff;
    recvmsg.len = 2;           // LM75温度寄存器是2字节
    // 读取2字节温度原始数据
    ret = lm75_client->adapter->algo->master_xfer(lm75_client->adapter, &recvmsg, 1);
if (ret != 1) {
        pr_info("master_xfer recvmsg failed ret = %d\n", ret);
return -1;
    }
    // 内核数据拷贝到用户空间
    nret = copy_to_user(puser, tmpbuff, sizeof(tmpbuff));
if (nret) {
        pr_info("copy_to_user failed\n");
return -1;
    }
return sizeof(tmpbuff);
}

i2c_msg:I2C子系统里面,一次I2C通信由一个或多个i2c_msg组成。

第一步写操作:发送寄存器地址0x00,告诉传感器接下来要读哪个寄存器。

第二步读操作:带上I2C_M_RD标志,读取2字节原始温度。>

注意:master_xfer是I2C适配器底层传输函数,实际开发更推荐用i2c_transfer封装接口,我这里直接调用adapter的master_xfer,方便理解底层调用链路。

pr_info内核打印,相当于内核里的printf,dmesg可以查看打印信息。

4. file_operations 和 miscdevice

复制代码
static struct file_operations fops = {
    .read = lm75_read,
};
static struct miscdevice lm75_misc = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "lm75_misc",
    .fops = &fops,
};

杂项设备本质就是字符设备,主设备号固定为10。MISC_DYNAMIC_MINOR让内核自动分配次设备号,不用自己手动管理设备号,非常适合简单外设驱动。注册成功后,自动生成/dev/lm75_misc。

5. probe 和 remove

复制代码
static int lm75_probe(struct i2c_client *pclient, const struct i2c_device_id *pdev)
{
int ret = 0;
    lm75_client = pclient;
    ret = misc_register(&lm75_misc);
if (ret) {
        pr_info("misc_register failed\n");
return -1;
    }
return 0;
}
static int lm75_remove(struct i2c_client *pclient)
{
int ret = 0;
    ret = misc_deregister(&lm75_misc);
if (ret) {
        pr_info("misc_deregister failed\n");
return -1;
    }
return 0;
}

I2C驱动匹配机制:设备树或者i2c总线里的设备和驱动匹配成功,内核自动调用probe函数。

probe:保存i2c_client指针,注册misc设备。

remove:设备卸载的时候调用,注销misc设备,资源释放。

6. 匹配表 i2c_device_id & of_device_id

复制代码
static struct i2c_device_id lm75_id_tables[] = {
    {.name = "putelm75"},
    {},
};
static struct of_device_id lm75_of_match_tables[] = {
    {.compatible = "pute,putelm75"},
    {},
};

两个匹配表对应两种匹配方式:

  1. of_device_id:设备树匹配,设备树节点compatible属性和这里字符串一致,就匹配。现在嵌入式开发基本都用设备树。
  2. i2c_device_id:传统非设备树匹配方式。

7. i2c_driver 结构体、模块入口出口

复制代码
struct i2c_driver lm75_drv = {
    .probe = lm75_probe,
    .remove = lm75_remove,
    .driver = {
        .name = "lm75_drv",
        .owner = THIS_MODULE,
        .of_match_table = lm75_of_match_tables,
    },
    .id_table = lm75_id_tables,
};
static int __init lm75_drv_init(void)
{
return i2c_add_driver(&lm75_drv);
}
static void __exit lm75_drv_exit(void)
{
    i2c_del_driver(&lm75_drv);
return;
}
module_init(lm75_drv_init);
module_exit(lm75_drv_exit);
MODULE_LICENSE("GPL");

i2c_add_driver:把I2C驱动注册进内核I2C子系统,内核开始拿这个驱动去总线上匹配I2C设备。

MODULE_LICENSE("GPL"):许可证声明,不加会报内核tainted警告,做驱动开发一定要写。

三、用户层测试程序

应用层代码很简单,打开设备文件,循环读取2字节原始数据,换算温度打印。

复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
int main(void)
{
int fd = 0;
unsigned char tmpbuff[2] = {0};
    fd = open("/dev/lm75_misc", O_RDWR);
if (-1 == fd)
    {
        perror("fail to open");
return -1;
    }
while (1)
    {
        read(fd, tmpbuff, sizeof(tmpbuff));
printf("temp = %.2lf\n", ((tmpbuff[0] << 8 | tmpbuff[1]) >> 5) * 0.125);
        sleep(1);
    }
    close(fd);
return 0;
}

温度换算公式解释:

LM75输出11位有效温度数据,两个字节:

高字节tmpbuff[0]是温度高8位

低字节tmpbuff[1]里面高3位是有效数据,剩下5位无效

拼接后整体右移5位,得到11bit原始值,分辨率0.125℃,乘以0.125就是实际温度。

四、编译、测试步骤

  1. 写Makefile,交叉编译内核模块.ko,还有用户测试程序。
  2. 把ko文件传到开发板,insmod lm75_drv.ko加载驱动。
  3. 设备树节点compatible和驱动匹配成功,触发probe,生成/dev/lm75_misc。
  4. 运行测试程序,每隔1秒打印一次温度。
  5. 调试:dmesg看内核打印,排查I2C通信失败、注册失败问题。
  6. 卸载:rmmod lm75_drv。

五、小结

  1. 内核态和用户态内存不能直接访问 ,必须用copy_to_user,忘记会直接Oops崩溃。
  2. I2C消息顺序不能乱:先发寄存器地址,再发读消息,很多新手这里搞反,读出来全是乱码。
  3. I2C_M_RD标记不能丢,不设置这个标志,master_xfer会当成写操作。
  4. 设备树compatible字符串必须和驱动of_device_id完全一致,多一个少一个字符都匹配不上,probe函数不会执行。
  5. 驱动里面不要随便用sleep,内核驱动延时优先用msleep,不要直接sleep(sleep是用户态函数)。
相关推荐
HRTOS1 小时前
HRTOS 4.0 实测:8051 中断响应延迟与中断嵌套性能测试
单片机·嵌入式硬件·51单片机
NendoZen1 小时前
万用板焊接LM2940实现5V电源模块
单片机·嵌入式硬件
FPGA小徐1 小时前
硬件面试真题:运放滞回比较器波形分析
单片机·嵌入式硬件·pcb
沐欣工作室_lvyiyi2 小时前
基于单片机的智能婴儿摇篮控制系统设计(论文+源码)
单片机·嵌入式硬件·物联网·数据库设计·物联网毕业设计·智能婴儿摇篮
狂奔v蜗牛2 小时前
电子保险丝(eFuse)电路
嵌入式硬件
木由里予2 小时前
3D NAND 闪存的可靠性
开发语言·嵌入式硬件
ouynagda3 小时前
iMX6ull Linux 嵌入式驱动开发笔记(LCD / ADXL345 / SPI 子系统)
linux·arm开发·驱动开发·笔记·嵌入式硬件
传感器与混合集成电路3 小时前
LIFM1 LIFM2 LIFM3多路IF转换模块量程输出频率精度等级选型
嵌入式硬件
一直C3 小时前
嵌入式 Linux 驱动开发学习笔记(三)设备树、GPIO子系统、中断与外设驱动实战
驱动开发·单片机·嵌入式硬件·嵌入式·arm·imx6ull