ARM-Linux下DS18B20温度传感器驱动开发实战

1. DS18B20温度传感器与ARM-Linux驱动开发概述

DS18B20是Dallas半导体公司(现为Maxim Integrated)推出的一款经典数字温度传感器,采用单总线(1-Wire)协议通信。这款传感器在工业控制、环境监测、智能家居等领域有着广泛应用,尤其适合嵌入式系统的集成。与传统的模拟温度传感器不同,DS18B20直接将温度转换为数字信号输出,避免了复杂的信号调理电路。

在ARM-Linux平台上开发DS18B20驱动,本质上是要实现一个字符设备驱动,通过文件操作接口(open、read等)向用户空间提供温度数据访问能力。这种架构的优势在于:

  • 用户态程序可以通过标准文件I/O接口获取温度数据
  • 内核驱动负责处理底层硬件时序和协议细节
  • 系统资源由内核统一管理,避免直接操作硬件带来的稳定性问题

2. 硬件连接与电路设计

2.1 DS18B20引脚定义与接口电路

DS18B20采用TO-92封装时,引脚定义如下:

  • VDD:电源输入(3.0V-5.5V)
  • DQ:数据输入/输出(单总线)
  • GND:电源地

典型应用电路中,需要在DQ线上拉一个4.7kΩ电阻到VDD。对于寄生供电模式(不接VDD引脚),DQ线需要更强的上拉(通常1.5kΩ)。在ARM开发板上,我们通常选择GPIO口连接DQ线,例如S3C2410的GPH9引脚。

注意:长距离传输时(超过10米),需要考虑线路阻抗匹配和抗干扰设计,可采用屏蔽双绞线并降低上拉电阻值。

2.2 ARM处理器GPIO配置

以S3C2410为例,驱动中需要配置相关GPIO寄存器:

  • GPHCON:控制GPH端口的功能选择(输入/输出)
  • GPHDAT:GPH端口的数据寄存器
  • GPHUP:GPH端口的上拉电阻使能

具体到DS18B20连接:

#define GPHCON (*(volatile unsigned int *)S3C2410_GPHCON) #define GPHDAT (*(volatile unsigned int *)S3C2410_GPHDAT) #define GPHUP (*(volatile unsigned int *)S3C2410_GPHUP)

3. 单总线协议实现细节

3.1 复位与存在脉冲检测

单总线通信始于主机(ARM处理器)发出的复位脉冲:

unsigned int reset_ds18b20(void) { unsigned int retValue; set_conOUT(); // 配置为输出模式 set_data(1); // 先拉高总线 __udelay(1); // 短暂延时 set_data(0); // 拉低总线480us以上 __udelay(600); // 实际延时600us set_data(1); // 释放总线 __udelay(20); // 等待15-60us set_conIN(); // 切换为输入模式 __udelay(100); // 等待从机响应 retValue = (GPHDAT >> 9) & 0x01; // 读取存在脉冲 return retValue; // 0表示设备存在 }

3.2 数据读写时序实现

单总线协议规定:

  • 写0:拉低总线60-120us
  • 写1:拉低总线1-15us,然后释放总线
  • 读时序:主机拉低总线1us后释放,在15us内采样

读一位数据的实现:

unsigned int read_bit(void) { spin_lock(&lock); // 防止多线程竞争 set_conOUT(); set_data(0); // 拉低总线 __udelay(2); // 保持2us set_conIN(); // 切换输入模式 __udelay(1); // 等待1us spin_unlock(&lock); return ((GPHDAT >> 9) & 0x01); // 返回采样值 }

写一位数据的实现:

void write_bit(char bitValue) { spin_lock(&lock); set_conOUT(); set_data(0); // 拉低总线 __udelay(15); // 保持15us if(bitValue == 1){ set_data(1); // 写1时释放总线 } spin_unlock(&lock); __udelay(45); // 总周期至少60us set_conIN(); __udelay(2); // 恢复时间 }

4. Linux设备驱动框架实现

4.1 字符设备注册流程

典型的字符设备驱动注册过程:

static int ds18b20_init(void) { int result; dev = MKDEV(ds18b20_major,0); // 静态或动态申请设备号 if(ds18b20_major) result = register_chrdev_region(dev,1,"ds18b20"); else{ result = alloc_chrdev_region(&dev,0,1,"ds18b20"); ds18b20_major=MAJOR(dev); } if(result < 0){ printk(KERN_WARNING"ds18b20:unable to get major %d\n",ds18b20_major); return result; } ds18b20_setup_cdev(); // 设置cdev结构 return 0; }

4.2 文件操作结构体实现

定义设备支持的操作:

struct file_operations ds18b20_fops ={ .owner = THIS_MODULE, .open = ds18b20_open, .read = ds18b20_read, .write = ds18b20_write, .ioctl = ds18b20_ioctl, .release = ds18b20_release, };

4.3 温度读取功能实现

完整的温度读取流程包括:

  1. 初始化DS18B20
  2. 发送跳过ROM命令(0xCC)
  3. 启动温度转换(0x44)
  4. 等待转换完成(最大750ms)
  5. 再次初始化
  6. 发送读取命令(0xBE)
  7. 读取温度数据(2字节)

关键代码段:

static int ds18b20_read(struct file *filp, char *buffer, size_t count, loff_t *ppos) { char lowValue=0,highValue=0; unsigned int i; // 第一次初始化并启动转换 if(reset_ds18b20()){ printk("init error\n"); } __udelay(400); set_conOUT(); set_data(1); write_cmd(0xCC); // 跳过ROM write_cmd(0x44); // 启动转换 __udelay(100000); // 等待转换完成 // 第二次初始化并读取数据 if(reset_ds18b20()){ printk("init error\n"); } __udelay(400); set_conOUT(); set_data(1); write_cmd(0xCC); // 跳过ROM write_cmd(0xBE); // 读暂存器 // 读取温度数据 for(i=0; i<8; i++){ if(read_bit()){ lowValue |= (0x01<<i); } __udelay(62); } for(i=0; i<8; i++){ if(read_bit()){ highValue |= (0x01<<i); } __udelay(62); } // 数据处理并返回用户空间 highValue <<= 4; highValue |= ((lowValue&0xf0)>>4); copy_to_user(buffer, &highValue, sizeof(highValue)); return 0; }

5. 驱动开发中的关键问题与解决方案

5.1 时序精度问题

在Linux内核中实现精确的微秒级延时面临挑战:

  • 直接使用__udelay()依赖于CPU频率
  • 内核调度可能导致时序偏差

解决方案:

  1. 对于关键时序,禁用中断:
unsigned long flags; local_irq_save(flags); // 关键时序代码 local_irq_restore(flags);
  1. 必要时使用高精度定时器(hrtimer)

5.2 多设备支持改进

当前驱动只支持单个DS18B20设备。要支持多设备:

  1. 修改设备号分配逻辑:
alloc_chrdev_region(&dev,0,num_devices,"ds18b20");
  1. 为每个设备创建独立的cdev结构

  2. 实现ROM搜索算法(0xF0命令)来发现总线上的所有设备

5.3 用户空间接口优化

当前实现只返回原始温度数据,可以改进:

  1. 通过ioctl提供更多控制功能
  2. 在sysfs中暴露设备信息
  3. 实现poll接口支持异步通知

示例ioctl实现:

static long ds18b20_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { switch(cmd){ case DS18B20_GET_PRECISION: // 返回当前精度设置 break; case DS18B20_SET_PRECISION: // 设置转换精度(9-12位) break; default: return -ENOTTY; } return 0; }

6. 驱动测试与验证

6.1 内核模块操作

加载驱动:

insmod ds18b20.ko dmesg | tail # 查看加载日志

创建设备节点(如果未自动创建):

mknod /dev/ds18b20 c 230 0

6.2 用户空间测试程序

简单的测试程序示例:

#include <stdio.h> #include <fcntl.h> #include <unistd.h> int main() { int fd; char temp; fd = open("/dev/ds18b20", O_RDONLY); if(fd < 0){ perror("open device failed"); return -1; } if(read(fd, &temp, 1) != 1){ perror("read failed"); close(fd); return -1; } printf("Current temperature: %d°C\n", temp); close(fd); return 0; }

6.3 常见问题排查

  1. 读取返回-1:
  • 检查/sys/module/ds18b20/parameters调试信息
  • 用示波器观察DQ线波形
  • 确认上拉电阻连接正确
  1. 温度值不正确:
  • 检查电源稳定性
  • 确认时序延时参数
  • 尝试降低通信速率
  1. 设备不响应:
  • 测量DQ线电压(正常应在3V左右)
  • 尝试缩短连接线长度
  • 检查GPIO配置是否正确

7. 性能优化与高级功能

7.1 中断驱动实现

轮询方式效率较低,可以改进为中断驱动:

  1. 配置GPIO中断
  2. 实现中断处理函数
  3. 使用等待队列实现阻塞I/O

关键代码:

static DECLARE_WAIT_QUEUE_HEAD(ds18b20_waitq); static int data_ready = 0; // 中断处理函数 static irqreturn_t ds18b20_irq(int irq, void *dev_id) { data_ready = 1; wake_up_interruptible(&ds18b20_waitq); return IRQ_HANDLED; } // 读函数修改 static ssize_t ds18b20_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) { if(wait_event_interruptible(ds18b20_waitq, data_ready)) return -ERESTARTSYS; // 读取数据 data_ready = 0; ... }

7.2 电源管理支持

添加电源管理功能:

static int ds18b20_suspend(struct device *dev) { // 进入低功耗模式 return 0; } static int ds18b20_resume(struct device *dev) { // 恢复工作 return 0; } static const struct dev_pm_ops ds18b20_pm_ops = { .suspend = ds18b20_suspend, .resume = ds18b20_resume, };

7.3 设备树支持

现代Linux内核推荐使用设备树描述硬件:

  1. 添加设备树节点:
ds18b20 { compatible = "maxim,ds18b20"; gpios = <&gph 9 GPIO_ACTIVE_HIGH>; };
  1. 驱动中解析设备树:
static int ds18b20_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; int gpio; gpio = of_get_named_gpio(dev->of_node, "gpios", 0); // 初始化设备 ... }

8. 实际应用中的经验分享

8.1 长线传输优化

当传感器距离处理器较远时(>10米):

  1. 使用屏蔽双绞线
  2. 降低上拉电阻值(可尝试2.2kΩ)
  3. 在接收端添加小电容(10-100pF)滤波
  4. 降低通信速率,增加时序延时

8.2 抗干扰设计

工业环境中:

  1. 在电源端添加0.1μF去耦电容
  2. 使用TVS二极管保护DQ线
  3. 避免与高频信号线平行走线
  4. 考虑使用光耦隔离

8.3 多传感器组网

单总线上挂接多个DS18B20时:

  1. 每个设备必须有唯一64位ROM ID
  2. 实现搜索ROM算法(0xF0命令)
  3. 注意总线驱动能力限制(通常不超过10个设备)
  4. 为每个设备创建独立的设备节点

搜索ROM算法核心:

void search_rom(void) { uint8_t last_zero = 0; uint8_t rom_buffer[8]; reset_ds18b20(); write_byte(0xF0); // 搜索ROM命令 for(int bit_pos=0; bit_pos<64; bit_pos++){ int bit1 = read_bit(); int bit2 = read_bit(); if(bit1 && bit2){ // 无设备响应 break; }else if(bit1 != bit2){ // 所有设备该位相同 set_bit(rom_buffer, bit_pos, bit1); }else{ // 存在分歧 if(bit_pos == last_zero){ set_bit(rom_buffer, bit_pos, 1); }else if(bit_pos > last_zero){ last_zero = bit_pos; set_bit(rom_buffer, bit_pos, 0); } } } }

8.4 温度转换精度选择

DS18B20支持9-12位分辨率,对应不同的转换时间:

  • 9位:93.75ms
  • 10位:187.5ms
  • 11位:375ms
  • 12位:750ms

设置分辨率命令:

void set_resolution(int resolution) { reset_ds18b20(); write_byte(0xCC); // 跳过ROM write_byte(0x4E); // 写暂存器 write_byte(0xFF); // TH报警值 write_byte(0xFF); // TL报警值 write_byte(0x1F | ((resolution-9)<<5)); // 配置寄存器 }

实际项目中需要根据应用场景权衡精度和响应速度。对于温度变化缓慢的环境(如室温监测),9-10位分辨率通常足够;而对于快速变化的温度测量(如化学反应监测),则需要更高的分辨率。