Linux 驱动开发:基于 platform 总线 + misc 设备的 DS18B20 单总线温度驱动

DS18B20 是一款经典的数字温度传感器,采用 1-Wire 单总线协议 ,只需要一根数据线就可以完成通信。

这篇博客通过一个 DS18B20 驱动示例,把 platform 总线、misc 设备、GPIO 子系统、单总线时序、字符设备驱动框架 串起来。

一、DS18B20 与单总线协议

DS18B20 使用单总线通信,主机和从机共用一根数据线。通信前需要先复位,再检测从机是否存在,最后发送命令并读取数据。

常见读取流程如下:

  1. 主机发送复位脉冲;
  2. 从机返回存在脉冲;
  3. 主机发送 0xCC,跳过 ROM 匹配;
  4. 主机发送 0x44,启动温度转换;
  5. 等待温度转换完成;
  6. 再次复位;
  7. 发送 0xCC;
  8. 发送 0xBE,读取暂存器;
  9. 读取温度低字节和高字节;
  10. 组合成 16 位温度值返回用户空间。

这段驱动就是用 GPIO 模拟单总线时序,完成上述流程。

二、驱动整体框架

这个驱动属于 platform 驱动 + misc 字符设备驱动。

整体结构可以分成三层:

层次 作用 对应代码
platform 驱动层 负责设备匹配、资源获取、硬件初始化 ds18b20_drv、ds18b20_probe
字符设备层 向用户空间提供 read 接口 ds18b20_misc、ds18b20_read
协议层 模拟 DS18B20 单总线时序 master_send_reset、master_recv_respone、master_send_byte、master_recv_byte

驱动加载时,module_platform_driver(ds18b20_drv) 会把驱动注册到 platform 总线上。

当设备树中节点的 compatible 与驱动里的 of_match_table 匹配成功后,内核会调用 ds18b20_probe。

三、probe 函数分析

ds18b20_probe 主要做了三件事:

  1. 注册 misc 设备;

  2. 从设备树获取 DS18B20 使用的 GPIO;

  3. 申请 GPIO 并配置为输出模式。

    static int ds18b20_probe(struct platform_device *pdevice)
    {
    int ret = 0;

    复制代码
     ret = misc_register(&ds18b20_misc);
     if (ret) {
         pr_info("misc_register failed\n");
         return -1;
     }
    
     ds18b20gpiono = of_get_named_gpio(pdevice->dev.of_node, "gpio-ds18b20", 0);
     if (ds18b20gpiono < 0) {
         pr_info("of_get_named_gpio failed\n");
         return -1;
     }
    
     ret = devm_gpio_request(ds18b20_misc.this_device, ds18b20gpiono, "ds18b20-drv");
     if (ret) {
         pr_info("devm_gpio_request failed\n");
         return -1;
     }
    
     gpio_direction_output(ds18b20gpiono, 1);
    
     spin_lock_init(&lock);
    
     return 0;

    }

这里使用了 misc_register,所以驱动加载成功后会生成类似 /dev/ds18b20_misc 的设备节点,用户空间可以直接通过 read 读取温度。

四、单总线时序实现

1. 复位与应答

master_send_reset 负责发送复位脉冲:

复制代码
void master_send_reset(void)
{
    gpio_set_value(ds18b20gpiono, 0);
    udelay(480);
    gpio_set_value(ds18b20gpiono, 1);
}

master_recv_respone 负责检测从机是否存在:

复制代码
int master_recv_respone(void)
{
    int timeout = 0;

    gpio_direction_input(ds18b20gpiono);
    while (timeout < 50 && gpio_get_value(ds18b20gpiono)) {
        udelay(10);
        timeout++;
    }

    if (timeout >= 50)
        return -1;

    while (!gpio_get_value(ds18b20gpiono));

    gpio_direction_output(ds18b20gpiono, 1);

    return 0;
}

这段代码通过切换 GPIO 输入/输出方向,模拟单总线的复位和存在脉冲检测。

2. 写一个字节

复制代码
static void master_send_byte(unsigned char tmpbyte)
{
    int i = 0;
    unsigned char flag = 0x01;

    gpio_direction_output(ds18b20gpiono, 1);

    for (i = 0; i < 8; i++, flag <<= 1) {
        gpio_set_value(ds18b20gpiono, 0);
        udelay(5);

        if (tmpbyte & flag) {
            gpio_set_value(ds18b20gpiono, 1);
        }

        udelay(55);
        gpio_set_value(ds18b20gpiono, 1);
        udelay(5);
    }
}

这里按照单总线写时序,逐位发送数据。

3. 读一个字节

复制代码
static unsigned char master_recv_byte(void)
{
    int i = 0;
    unsigned char tmpbyte = 0x00;

    for (i = 0; i < 8; i++) {
        gpio_direction_output(ds18b20gpiono, 1);
        gpio_set_value(ds18b20gpiono, 0);
        udelay(3);
        gpio_set_value(ds18b20gpiono, 1);
        gpio_direction_input(ds18b20gpiono);
        udelay(8);
        if (gpio_get_value(ds18b20gpiono))
            tmpbyte |= (0x1 << i);
        gpio_direction_output(ds18b20gpiono, 1);
        udelay(50);
    }

    return tmpbyte;
}

读时序需要先拉低总线,再释放总线,然后在合适的时间窗口采样电平。

五、read 函数分析

ds18b20_read 完成一次完整的温度读取:

复制代码
static ssize_t ds18b20_read(struct file *fp, char __user *puser, size_t n, loff_t *off)
{
    int ret = 0;
    unsigned char TH = 0;
    unsigned char TL = 0;
    short temp = 0;
    unsigned long nret = 0;
    unsigned long flags = 0;

    master_send_reset();

    spin_lock_irqsave(&lock, flags);
    ret = master_recv_respone();
    if (ret) {
        pr_info("master_recv_respone failed\n");
        return -1;
    }
    master_send_byte(0xCC);
    master_send_byte(0x44);
    spin_unlock_irqrestore(&lock, flags);

    msleep(500);

    master_send_reset();
    spin_lock_irqsave(&lock, flags);
    ret = master_recv_respone();
    if (ret) {
        pr_info("master_recv_respone failed\n");
        return -1;
    }
    master_send_byte(0xCC);
    master_send_byte(0xBE);
    TL = master_recv_byte();
    TH = master_recv_byte();
    spin_unlock_irqrestore(&lock, flags);

    temp = (TH << 8 | TL);

    nret = copy_to_user(puser, &temp, sizeof(temp));
    if (nret) {
        pr_info("copy_to_user failed\n");
        return -1;
    }

    return sizeof(temp);
}

流程是:

  1. 复位;
  2. 检测应答;
  3. 发送 0xCC 跳过 ROM;
  4. 发送 0x44 启动转换;
  5. 等待 500ms;
  6. 再次复位;
  7. 发送 0xBE 读取暂存器;
  8. 读取 TL、TH;
  9. 组合温度值;
  10. 通过 copy_to_user 返回给用户空间。

六、platform 驱动注册

驱动通过 platform_driver 注册到 platform 总线:

复制代码
static struct platform_device_id ds18b20_id_tables[] = {
    {.name = "puteds18b20"},
    {},
};

static struct of_device_id ds18b20_of_match_tables[] = {
    {.compatible = "pute,puteds18b20"},
    {},
};

static struct platform_driver ds18b20_drv = {
    .driver = {
        .name = "puteds18b20",
        .owner = THIS_MODULE,
        .of_match_table = ds18b20_of_match_tables,
    },
    .probe = ds18b20_probe,
    .remove = ds18b20_remove,
    .id_table = ds18b20_id_tables,
};

module_platform_driver(ds18b20_drv);
MODULE_LICENSE("GPL");

设备树中需要配置对应节点,例如:

复制代码
ds18b20 {
    compatible = "pute,puteds18b20";
    gpio-ds18b20 = <&gpioX Y GPIO_ACTIVE_HIGH>;
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_ds18b20>;
    status = "okay";
};

其中 pinctrl 负责引脚复用配置,gpio-ds18b20 指定 DS18B20 使用的 GPIO。

七、可以改进的地方

这段代码已经能体现 DS18B20 驱动的基本思路,但如果是正式项目或秋招面试,还可以进一步优化。

1. 资源释放顺序

当前 remove 只调用了 misc_deregister,没有释放 GPIO。

更合理的做法是在 probe 失败或 remove 中反向释放资源。

2. GPIO 管理方式

代码中使用了 of_get_named_gpio 和 devm_gpio_request。

更现代的方式是使用 GPIO 描述符接口,例如 devm_gpiod_get。

3. 并发保护

驱动使用 spinlock 保护单总线时序,这点是合理的,因为 DS18B20 时序对时间比较敏感。

但 msleep(500) 放在 spin_lock_irqsave 之外是正确的,否则会导致长时间关中断。

4. 温度数据解析

当前直接把 TH << 8 | TL 返回给用户空间,用户还需要自行解析正负温度和分辨率。

更完善的实现应该在驱动中完成温度转换,返回摄氏度或毫摄氏度。

5. 生产环境建议

Linux 内核本身已经提供了 drivers/w1/ 子系统和 w1_therm 驱动,可以支持 DS18B20。

实际项目中,如果硬件平台支持 1-Wire 总线,更推荐直接使用内核自带子系统,而不是手写 GPIO 模拟。

手写 GPIO 模拟更适合学习单总线协议、platform 驱动和字符设备框架。

八、总结

这个 DS18B20 驱动虽然不大,但涉及很多 Linux 驱动基础知识点:

  • platform_driver 负责设备匹配;
  • probe 负责初始化硬件;
  • misc_register 提供用户空间接口;
  • GPIO 子系统控制引脚电平;
  • udelay/msleep 配合单总线时序;
  • copy_to_user 完成内核态到用户态的数据传递。

它很适合用来理解:驱动层如何把硬件协议、内核框架和用户空间接口串起来。


相关推荐
nice_lcj5201 小时前
Java Lambda 表达式入门:怎么写、怎么用、常用方法有哪些
java·开发语言·后端·面试
码农小河661 小时前
基于SSM+JSP+MySQL的在线医疗服务系统的设计与实现
java·开发语言·mysql
Dev_XH2 小时前
【成品设计】基于单片机的家用智能浇花系统的设计与实现
stm32·单片机·嵌入式硬件·物联网
GeW2 小时前
高端化转型进入“补底层”阶段:Linux与数据库的再定位
linux
silent2 小时前
龙芯3B6000M运行Yolov8+FastApi
linux·loongarch·loongnix·龙芯
意法半导体STM323 小时前
【官方原创】LAT1717 STM32N6 如何使用单个XSPI驱动双片相同HyperRAM
stm32·单片机·嵌入式硬件·mcu·stm32n6·hyperram
木子单片机3 小时前
基于单片机的直流电机调速系统设计
单片机·嵌入式硬件·51单片机·nosql·keil
西西弗Sisyphus3 小时前
理解 Linux 动态库加载:LD_LIBRARY_PATH
linux
AR-26710-3 小时前
Linux Day31——实操启停服务改端口重启
linux·运维
叼烟扛炮4 小时前
Linux 学习笔记(五十八):IO 多路复用封神——epoll 底层原理揭秘
linux·运维·服务器·网络·笔记·学习·php