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 温度读取功能实现
完整的温度读取流程包括:
- 初始化DS18B20
- 发送跳过ROM命令(0xCC)
- 启动温度转换(0x44)
- 等待转换完成(最大750ms)
- 再次初始化
- 发送读取命令(0xBE)
- 读取温度数据(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频率
- 内核调度可能导致时序偏差
解决方案:
- 对于关键时序,禁用中断:
unsigned long flags; local_irq_save(flags); // 关键时序代码 local_irq_restore(flags);- 必要时使用高精度定时器(hrtimer)
5.2 多设备支持改进
当前驱动只支持单个DS18B20设备。要支持多设备:
- 修改设备号分配逻辑:
alloc_chrdev_region(&dev,0,num_devices,"ds18b20");为每个设备创建独立的cdev结构
实现ROM搜索算法(0xF0命令)来发现总线上的所有设备
5.3 用户空间接口优化
当前实现只返回原始温度数据,可以改进:
- 通过ioctl提供更多控制功能
- 在sysfs中暴露设备信息
- 实现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 06.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:
- 检查/sys/module/ds18b20/parameters调试信息
- 用示波器观察DQ线波形
- 确认上拉电阻连接正确
- 温度值不正确:
- 检查电源稳定性
- 确认时序延时参数
- 尝试降低通信速率
- 设备不响应:
- 测量DQ线电压(正常应在3V左右)
- 尝试缩短连接线长度
- 检查GPIO配置是否正确
7. 性能优化与高级功能
7.1 中断驱动实现
轮询方式效率较低,可以改进为中断驱动:
- 配置GPIO中断
- 实现中断处理函数
- 使用等待队列实现阻塞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内核推荐使用设备树描述硬件:
- 添加设备树节点:
ds18b20 { compatible = "maxim,ds18b20"; gpios = <&gph 9 GPIO_ACTIVE_HIGH>; };- 驱动中解析设备树:
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米):
- 使用屏蔽双绞线
- 降低上拉电阻值(可尝试2.2kΩ)
- 在接收端添加小电容(10-100pF)滤波
- 降低通信速率,增加时序延时
8.2 抗干扰设计
工业环境中:
- 在电源端添加0.1μF去耦电容
- 使用TVS二极管保护DQ线
- 避免与高频信号线平行走线
- 考虑使用光耦隔离
8.3 多传感器组网
单总线上挂接多个DS18B20时:
- 每个设备必须有唯一64位ROM ID
- 实现搜索ROM算法(0xF0命令)
- 注意总线驱动能力限制(通常不超过10个设备)
- 为每个设备创建独立的设备节点
搜索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位分辨率通常足够;而对于快速变化的温度测量(如化学反应监测),则需要更高的分辨率。
