温度传感器-DS18B20驱动框架编写

提示:温度传感器-DS18B20驱动框架编写

这里不仅仅是温度传感器驱动框架层编写,也基本上是所有驱动框架的基本框架,所以这里有整理、归整知识点的必要

文章目录


前言-引言

我们会发现,知识点1-6 其实之前都已经学习过或者说都实验过,有一定的基础。 这里唯一的知识点不就是在之前知识点基础上用单总线协议 来 实现文档传奇器 DS18B20 的温度获取和设置嘛。

这里核心点:把之前的知识点 字符设备驱动、平台总线、设备属、解析设备树、gpio子系统、pinctrl 子系统 知识点串联起来,然后理解和使用单总线协议 实现基本功能。

一、 参考资料

这里我们的目标是写一个驱动框架,如上前言描述 用到的是平台总线。 其实就是写一个平台总线的驱动框架。 之前我们有过平台总线相关知识点,平台总线驱动的代码暂未整理,但是其它知识点还是有必要回顾的:

二、源码骨架

对于新手而言,强烈建议先搞清楚基本知识点,如上: 参考资料 是基本知识点,也可以自己了解并掌握基本知识点先

1、源码实现

java 复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>


int ds18b20_probe(struct platform_device *dev){
    printk("This is probe \n");
    return 0;
}

const struct of_device_id ds18b20_match_table[] = {
    {.compatible = "ds18b20"},
    {},
};

struct platform_driver ds18b20_driver = {
    .driver = {
        .owner = THIS_MODULE,
        .name = "ds18b20",
        .of_match_table = ds18b20_match_table,
    },
    .probe = ds18b20_probe,
};


static int __init ds18b20_init(void){
    int ret;
    ret = platform_driver_register(&ds18b20_driver);
    if(ret < 0){
        printk("platform_driver_register error\n");
        return -1;
    }

    return 0;
}

static void __exit ds18b20_exit(void){
    platform_driver_unregister(&ds18b20_driver);
}

module_init(ds18b20_init);
module_exit(ds18b20_exit);
MODULE_LICENSE("GPL");

2、源码简要分析

驱动入口和出口函数

这两个是最没有含金量的,但是是入口和出口,照葫芦画瓢即可;核心点是其实现方法里面是 注册平台总线驱动和协助平台总线驱动

java 复制代码
module_init(ds18b20_init);
module_exit(ds18b20_exit);
java 复制代码
static int __init ds18b20_init(void){
    int ret;
    ret = platform_driver_register(&ds18b20_driver);
    if(ret < 0){
        printk("platform_driver_register error\n");
        return -1;
    }

    return 0;
}

static void __exit ds18b20_exit(void){
    platform_driver_unregister(&ds18b20_driver);
}

驱动注册函数platform_driver_register 分析

java 复制代码
 ret = platform_driver_register(&ds18b20_driver);

#define platform_driver_register(drv) \
	__platform_driver_register(drv, THIS_MODULE)



/**
 * __platform_driver_register - register a driver for platform-level devices
 * @drv: platform driver structure
 * @owner: owning module/driver
 */
int __platform_driver_register(struct platform_driver *drv,
				struct module *owner)
{
	drv->driver.owner = owner;
	drv->driver.bus = &platform_bus_type;
	drv->driver.probe = platform_drv_probe;
	drv->driver.remove = platform_drv_remove;
	drv->driver.shutdown = platform_drv_shutdown;

	return driver_register(&drv->driver);
}

如上跟踪代码,最终跟进到了 __platform_driver_register 函数,传递的参数是 platform_driver 指针,所以:platform_driver_register(&ds18b20_driver) 注册参数就是一个结构体的地址, 我们就定义一个 platform_driver 结构体即可

platform_driver 结构体定义

先搞清楚源码结构,对比源码和我们自己写的程序:

  • .probe 对象是什么?int (*probe)(struct platform_device *) 返回的是int ,所以 我们定义的一个方法返回int 值,传入platform_device 结构体对象
java 复制代码
int ds18b20_probe(struct platform_device *dev){
    printk("This is probe \n");
    return 0;
}
  • .driver对象,是一个结构体
    看源码对象中的定义:struct device_driver driver;,在实际开发中,我们直接实现的,当然可以定义一个结构体然后赋值,对比源码如下:
java 复制代码
.driver = {
        .owner = THIS_MODULE,
        .name = "ds18b20",
        .of_match_table = ds18b20_match_table,
    },

我们看我们定义的结构体,如下:

java 复制代码
struct platform_driver ds18b20_driver = {
    .driver = {
        .owner = THIS_MODULE,
        .name = "ds18b20",
        .of_match_table = ds18b20_match_table,
    },
    .probe = ds18b20_probe,
};

所以在结构体对象里面,我们实现了两个核心的platform_driver的对象调用:.driver。probe

platform_driver 结构体对象 .driver 实现

我们在.driver 对象中又实现了什么:

java 复制代码
  .owner = THIS_MODULE,
  .name = "ds18b20",
  .of_match_table = ds18b20_match_table,


platform_driver 结构体对象 .probe 实现

3、源码小结

代码是一个平台驱动框架:

  • 向 Linux 内核注册一个 ds18b20 平台驱动;
  • 通过设备树 compatible 属性匹配硬件设备;
  • 匹配成功后,内核自动调用 probe 函数(打印一句话);
  • 卸载驱动时,注销驱动并释放资源。

三、头文件逐行解析

java 复制代码
#include <linux/init.h>     // 模块初始化/卸载宏(__init / __exit)
#include <linux/module.h>   // 模块核心API( GPL协议、模块入口出口等)
#include <linux/platform_device.h> // 平台总线核心结构体(platform_driver、platform_device)
#include <linux/of.h>       // 设备树操作API(of_device_id 匹配表)
  • 这 4 个是平台驱动 + 设备树必备头文件,缺一不可。
  • linux/of.h 专门用于设备树匹配,没有它驱动无法和设备树绑定。

四、probe 函数解析(核心!)

java 复制代码
// 设备与驱动匹配成功后,内核自动调用的函数
int ds18b20_probe(struct platform_device *dev){
    printk("This is probe \n");  // 内核打印函数(类似printf)
    return 0;                    // 0=成功,负数=失败
}

关键知识点

probe 是驱动的「入口函数」

  • 不是我们手动调用,是内核自动调用;
  • 触发条件:设备树中的设备 和 驱动 匹配成功。

真实驱动中,probe 要做这些事:

  • 申请 GPIO / 中断 / I2C/SPI 资源;
  • 注册字符设备 / 杂项设备;
  • 初始化硬件(DS18B20 复位、读取 ID);
  • 创建 sysfs 节点、注册输入子系统等。

参数 struct platform_device *dev

  • 指向匹配成功的硬件设备;
  • 可以从中获取设备树配置(寄存器地址、GPIO 号等)。

五、设备树匹配表解析

java 复制代码
// 设备树匹配表(驱动 <-> 设备树 绑定的关键)
const struct of_device_id ds18b20_match_table[] = {
    {.compatible = "ds18b20"},  // 匹配规则:compatible 属性完全一致
    {},                         // 空结构体,代表表结束(必须写!)
};

核心作用:设备树与驱动的匹配规则

  • 内核会遍历设备树,查找 compatible = "ds18b20" 的节点;
  • 找到后,就把这个设备和我们的驱动绑定;
  • 绑定成功 → 自动调用 probe。

对应设备树节点写法(必须配套)

这里我们暂且 延伸一下如下基本知识点 设备属节点

java 复制代码
ds18b20@0 {
    compatible = "ds18b20";  // 必须和驱动里的字符串完全一样!
    pinctrl-names = "default";
    gpios = <&gpio 5 GPIO_ACTIVE_HIGH>;
};

六、platform_driver 结构体解析

java 复制代码
// 平台驱动核心结构体
struct platform_driver ds18b20_driver = {
    .driver = {  // 基础驱动信息
        .owner = THIS_MODULE,        // 固定写法:防止驱动被卸载时出现内存错误
        .name = "ds18b20",           // 驱动名字(sysfs可见,无设备树时也用它匹配)
        .of_match_table = ds18b20_match_table, // 绑定设备树匹配表
    },
    .probe = ds18b20_probe,  // 绑定匹配成功后的执行函数
};

关键字段说明

字段 说明
.owner = THIS_MODULE 内核必备,必须写,用于模块引用计数;
.name = "ds18b20" 驱动名称,在 /sys/bus/platform/drivers/ 下可见;无设备树时,靠这个名字匹配设备;
.of_match_table 设备树匹配的核心,把上面的匹配表挂到驱动上;
.probe 绑定我们实现的 probe 函数。

七、模块初始化函数解析

java 复制代码
// 模块安装时执行(insmod xx.ko)
static int __init ds18b20_init(void){
    int ret;
    // 向内核注册平台驱动
    ret = platform_driver_register(&ds18b20_driver);
    if(ret < 0){
        printk("platform_driver_register error\n");
        return -1;
    }
    return 0;
}

__init 宏:

  • 告诉内核,这个函数只执行一次,执行完释放内存;

platform_driver_register

  • 内核核心 API,把驱动注册到平台总线;
  • 注册后,内核立刻开始匹配设备树设备。

八、模块宏 + 协议声明

java 复制代码
module_init(ds18b20_init);   // 注册模块入口函数
module_exit(ds18b20_exit);   // 注册模块出口函数
MODULE_LICENSE("GPL");       // 声明开源协议(必须写,否则内核报错)
  • module_init:指定 insmod 时执行哪个函数;
  • module_exit:指定 rmmod 时执行哪个函数;
  • MODULE_LICENSE("GPL"):强制要求,没有会触发内核污染警告。

九、完整执行流程(最重要!)

java 复制代码
insmod ds18b20.ko
    ↓
ds18b20_init() 执行
    ↓
platform_driver_register() 注册驱动到平台总线
    ↓
内核遍历设备树
    ↓
找到 compatible = "ds18b20" 的设备
    ↓
设备与驱动匹配成功
    ↓
内核自动调用 ds18b20_probe()
    ↓
打印 This is probe
    ↓
驱动加载完成

=====================

rmmod ds18b20.ko
    ↓
ds18b20_exit() 执行
    ↓
platform_driver_unregister() 注销驱动
    ↓
驱动卸载完成

十、这个驱动的价值与扩展方向

价值

这是Linux 设备树 + 平台驱动的标准模板,几乎所有 ARM 板载外设驱动(GPIO、I2C、SPI、传感器)都用这套框架。

扩展成完整 DS18B20 驱动需要加什么?

在 probe 中

  • 从设备树获取 GPIO 编号;
  • 申请 GPIO;
  • 实现单总线(1-Wire)时序;
  • 实现温度读取函数;

2、注册字符设备 / 杂项设备,提供应用层接口;

3、添加定时器,周期性读取温度。

总结

  • 这是Linux Platform 平台驱动最简框架,支持设备树匹配;
  • probe 函数是驱动的灵魂,匹配成功自动执行;
  • of_device_id 是驱动与设备树绑定的桥梁,compatible 必须完全一致;
  • 核心 API:platform_driver_register(注册)、platform_driver_unregister(注销);
  • 适合所有片上外设、简单传感器的驱动开发。
  • 最后:个人理解,还是需要理解源码,学会查看源码,学会分析基本源码。此文就是一个平台总线的驱动骨架,驱动代码中最基础的内容,后续可以拿着直接复用 , 理解并掌握特别重要。