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 完成内核态到用户态的数据传递。

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


相关推荐
devilnumber1 小时前
Java 代理模式(简单理解回顾)
java·开发语言·代理模式
奶盐cookie1 小时前
内存区域:栈、全局区和静态区
c语言
头像很酷1 小时前
嵌入式虚拟化-xvisor-跨越边界的读客
c语言·hypervisor·xvisor·嵌入式虚拟化
zcmodeltech1 小时前
沙盘模型控制系统STM32和PLC方案对比:选型逻辑与代码实现
stm32·嵌入式硬件·自动化
invicinble1 小时前
对服务器资源的理解--理解用户请求,部署,服务器资源相关的映射层
开发语言
Android系统攻城狮1 小时前
Linux Gstreamer深度解析之gst_audio_ring_buffer_activate调用流程与实战(七十九)
linux·运维·服务器·gstreamer音视频·gstreamer音视频进阶
漫步人生走在路上1 小时前
用小语种SEO突破外贸B2B天花板:法语、德语市场的实测数据
开发语言·前端·javascript·人工智能·搜索引擎·ecmascript
HQB20262 小时前
EG11731|屹晶微电子 100V 内置功率管大电流同步降压 DC‑DC
嵌入式硬件·硬件工程
yuniko-n2 小时前
【JUC】线程池
java·开发语言