DS18B20 Linux 驱动开发实战:从时序图到温度读取的保姆级教学
目录
文章目录
- [DS18B20 Linux 驱动开发实战:从时序图到温度读取的保姆级教学](#DS18B20 Linux 驱动开发实战:从时序图到温度读取的保姆级教学)
-
- 目录
- [1. 前言:DS18B20 ------ 单总线温度传感器之王](#1. 前言:DS18B20 —— 单总线温度传感器之王)
- [2. 硬件接线与 GPIO 编号](#2. 硬件接线与 GPIO 编号
) - [3. DS18B20 存储器结构](#3. DS18B20 存储器结构)
-
- [3.1 ROM(64 位只读存储器)](#3.1 ROM(64 位只读存储器))
- [3.2 Scratchpad(暂存器,9 字节)](#3.2 Scratchpad(暂存器,9 字节))
- [4. 通信时序全解:复位、写、读](#4. 通信时序全解:复位、写、读)
-
- [4.1 初始化(复位)时序](#4.1 初始化(复位)时序)
- [4.2 写一位数据(写时隙)](#4.2 写一位数据(写时隙)
) - [4.3 读一位数据(读时隙)](#4.3 读一位数据(读时隙))
- [5. 命令与完整读取流程](#5. 命令与完整读取流程)
- [6. 驱动代码逐行精讲](#6. 驱动代码逐行精讲)
-
- [6.1 头文件与设备结构体](#6.1 头文件与设备结构体)
- [6.2 自旋锁与微秒延时函数](#6.2 自旋锁与微秒延时函数)
- [6.3 复位与应答检测函数](#6.3 复位与应答检测函数)
- [6.4 发送一个字节函数](#6.4 发送一个字节函数)
- [6.5 读取一个字节函数](#6.5 读取一个字节函数)
- [6.6 CRC 校验与温度计算函数](#6.6 CRC 校验与温度计算函数)
- [6.7 核心 read 函数:完整温度读取事务](#6.7 核心 read 函数:完整温度读取事务)
- [6.8 模块初始化与注销](#6.8 模块初始化与注销)
- [7. 现场实录:编译、加载、运行](#7. 现场实录:编译、加载、运行)
- [8. 自测 5 道题](#8. 自测 5 道题)
-
- [第 1 题](#第 1 题)
- [第 2 题](#第 2 题)
- [第 3 题](#第 3 题)
- [第 4 题](#第 4 题)
- [第 5 题](#第 5 题)
1. 前言:DS18B20 ------ 单总线温度传感器之王
DS18B20 是 Maxim 公司推出的数字温度传感器,它有三个"绝活":
- 单总线通信:只要一根数据线(加上电源和地),就能完成双向数据传输。
- 多个传感器可挂一条总线:每个 DS18B20 有全球唯一的 64 位 ROM 码,MCU 可以精确选址某一个传感器。
- 精度可调:支持 9~12 位分辨率,最高精度 0.0625°C。
本文用百问网 IMX6ULL 开发板(GPIO4_19,编号 115)实战驱动编写。我们从硬件接线、时序图剖析、代码逐行精讲,到最后编译运行,一步一步带你掌握 "自旋锁 + 精确忙等延时 + 状态机协议" 的经典内核驱动框架。
阅读建议:本文长但不啰嗦,每一步都配有"盲人朋友"式的生动比喻,建议边读边对着代码敲。
2. 硬件接线与 GPIO 编号

DS18B20 有三个引脚(TO-92 封装):
| 引脚 | 名称 | 连接 |
|---|---|---|
| 1 | GND | 接开发板 GND |
| 2 | DQ(数据) | 接 GPIO4_19,同时用 4.7kΩ~10kΩ 上拉电阻 接 3.3V |
| 3 | VDD | 接 3.3V(也可工作在"寄生供电"模式,这里用外部供电) |
GPIO 编号怎么算的?
- IMX6ULL 的 GPIO 分多组:GPIO1、GPIO2、GPIO3、GPIO4...
- 每组 32 个引脚。
- GPIO4_19 的编号 = 4 × 32 + 19 = 115。
你的板子 GPIOn_m 的编号通用公式:n × 32 + m。
上拉电阻很重要!DS18B20 的数据引脚是开漏输出,只能拉低无法主动拉高。没有上拉电阻,总线释放后无法恢复高电平,通信必然失败。可以把上拉电阻想象成"弹簧",没人碰总线时它会自动把电平弹回高。
3. DS18B20 存储器结构
理解存储器结构才能理解为什么要读 9 个字节、CRC 到底校验什么。
3.1 ROM(64 位只读存储器)
每个 DS18B20 出厂时被激光刻录了一个 全球唯一 的 64 位序列号:
| 8 位 | 48 位 | 8 位 |
|---|---|---|
| CRC 校验码 | 唯一序列号 | 产品族码(固定 28h) |
如果总线上只有一个传感器,可以发 0xCC(Skip ROM)命令直接跳过地址匹配。多个传感器时才需要 0x55(Match ROM)精确选址。
3.2 Scratchpad(暂存器,9 字节)
这是我们要读的核心数据区,共 9 个字节:
| 字节 | 内容 | 说明 |
|---|---|---|
| Byte 0 | 温度低字节(LSB) | 温度数据的最低 8 位 |
| Byte 1 | 温度高字节(MSB) | 最高几位 + 符号位 |
| Byte 2 | TH 寄存器 | 高温报警阈值(可写入 EEPROM) |
| Byte 3 | TL 寄存器 | 低温报警阈值 |
| Byte 4 | 配置寄存器 | 分辨率配置(9/10/11/12 位) |
| Byte 5 | 保留(FFh) | 厂家预留 |
| Byte 6 | 保留 | |
| Byte 7 | 保留(10h) | |
| Byte 8 | CRC 校验码 | 前 8 个字节的 CRC 值 |
温度值就在 Byte 0 和 Byte 1 里。 DS18B20 默认 12 位分辨率时,每 1°C 分成 16 等份(LSB 的低 4 位是小数部分)。
4. 通信时序全解:复位、写、读
DS18B20 通信完全靠一根线上的电平持续时间来传递信息。整个过程分三种基本时序。
4.1 初始化(复位)时序
初始化是每次通信的"敲门砖"。

步骤:
- 主机拉低至少 480µs ------ 这是复位信号,告诉传感器"准备通信"。
- 主机释放总线(回高),等待 15~60µs。
- DS18B20 应答:传感器检测到上升沿后,拉低总线 60~240µs,表示"我在线"。
- 主机检测:如果在步骤 2 后检测到 60~240µs 的低脉冲,说明初始化成功。
用生活比喻理解复位时序:
你去敲邻居的门(拉低 480µs),然后退回门口等着(释放总线)。邻居听到敲门声,推开一条门缝说"我在家"(拉低 60~240µs)。你看到门缝开了,就知道可以开始说话了。如果等了半天门都没动静,说明邻居不在家。
4.2 写一位数据(写时隙)
传感器在主机拉低后的 15~60µs 窗口 内采样数据线电平。
| 想写的值 | 主机动作 | 传感器采样时看到 | 读到的值 |
|---|---|---|---|
| 0 | 拉低至少 60µs | 低电平 | 0 |
| 1 | 拉低 1~2µs 后立即拉高 | 高电平 | 1 |
整个写时隙至少 60µs。
用"盲人朋友拍桌子"比喻写时序:
你想告诉盲人朋友"是"或"否",约定:你拍一下桌子(拉低),盲人朋友在拍桌后第 15 秒摸一下桌面。
- 说"是"(写 1):拍一下桌子立刻把手拿开(拉低 2µs 就拉高)。朋友第 15 秒摸的时候,桌面是空的 → "是"。
- 说"否"(写 0):拍一下桌子手一直按着(拉低 60µs)。朋友第 15 秒摸的时候,你的手还在桌面上 → "否"。
手按多久不重要,重要的是朋友摸的那一瞬间,手在还是不在。
4.3 读一位数据(读时隙)

步骤:
- 主机拉低至少 1µs,发起读时隙。
- 主机释放总线,切换为输入。
- 传感器立即输出数据 :
- 要发 0:传感器拉低总线并保持约 15µs。
- 要发 1:传感器不动作,总线上拉电阻维持高电平。
- 主机在 15µs 内采样,读取引脚电平。
- 整个读时隙至少 60µs。
采样时机为什么必须在 15µs 内?
因为对于发 0 的情况,传感器只维持约 15µs 的低电平,之后就会释放总线,总线被上拉电阻拉回高。如果采样太晚(比如 30µs),传感器早已释放,高低电平都一样了,无法区分。
用"拉绳子"比喻读时序:
你想让朋友通过一根弹簧绳告诉你"是"或"否"。
- 你先拉一下绳子然后松手(拉低再释放),朋友在另一端立即反应:
- 说"否"(0):朋友马上把绳子往下拽,保持 15 秒后松手。
- 说"是"(1):朋友完全不动,弹簧自己把绳子拉紧。
- 你必须在松手后的第 15 秒摸一下绳子:
- 绳子松的 → "否"。
- 绳子紧的 → "是"。
等到第 20 秒再摸,即使朋友说了"否",他也早就松手了,弹簧又把绳子拉紧,你就分不清了。
5. 命令与完整读取流程
有了上面的三种基本时序,就可以组合出各种命令。DS18B20 有两类命令:
ROM 命令(设备寻址相关):
| 命令 | 代码 | 说明 |
|---|---|---|
| Search ROM | 0xF0 | 搜索总线上所有设备的 ROM 码 |
| Read ROM | 0x33 | 读单个设备的 ROM 码 |
| Match ROM | 0x55 | 匹配指定 ROM 码的设备 |
| Skip ROM | 0xCC | 跳过 ROM 匹配(单设备时用) |
| Alarm Search | 0xEC | 搜索有报警的设备 |
功能命令(操作传感器):
| 命令 | 代码 | 说明 |
|---|---|---|
| Convert T | 0x44 | 启动温度转换 |
| Read Scratchpad | 0xBE | 读取暂存器 9 字节数据 |
| Write Scratchpad | 0x4E | 写暂存器(设置报警阈值等) |
| Copy Scratchpad | 0x48 | 将暂存器值复制到 EEPROM |
| Recall E² | 0xB8 | 从 EEPROM 回读配置 |
单个 DS18B20 的完整温度读取流程(重要!背下来):
| 阶段 | 主机动作 | 说明 |
|---|---|---|
| 启动转换 | ① 复位 + 等待应答 | 敲门 |
| ② 发送 0xCC(Skip ROM) | "不点名,全听我讲" | |
| ③ 发送 0x44(Convert T) | "开始量体温" | |
| 等待 | ④ 等待至少 750ms(12 位分辨率) | 用睡眠,不用忙等 |
| 读取数据 | ⑤ 复位 + 等待应答 | 再次敲门 |
| ⑥ 发送 0xCC(Skip ROM) | "不点名" | |
| ⑦ 发送 0xBE(Read Scratchpad) | "把体检报告给我" | |
| ⑧ 读取 9 字节数据 | 拿到数据 | |
| 处理 | ⑨ CRC 校验 | 确认数据没传输错误 |
| ⑩ 提取 Byte 0 和 Byte 1,计算温度 | 整数 + 小数 |
6. 驱动代码逐行精讲
下面按从零开始写驱动的顺序,逐段讲解代码。我会像坐在你旁边一样,告诉你每一句为什么这么写。
6.1 头文件与设备结构体
c
#include <linux/module.h> // 模块基本框架
#include <linux/fs.h> // file_operations 结构体
#include <linux/errno.h> // 错误码(EINVAL、EIO 等)
#include <linux/kernel.h> // printk
#include <linux/init.h> // __init / __exit 宏
#include <linux/device.h> // class_create、device_create
#include <linux/gpio.h> // GPIO 操作函数
#include <linux/spinlock.h> // 自旋锁
#include <linux/timekeeping.h> // ktime_get_ns
#include <linux/sched.h> // set_current_state、schedule_timeout
#include <linux/uaccess.h> // copy_to_user
每个头文件都是用到哪个函数就加哪个。比如
spin_lock_irqsave需要<linux/spinlock.h>,ktime_get_ns()需要<linux/timekeeping.h>。
c
struct gpio_desc {
int gpio; // GPIO 编号
char *name; // 标签名
};
static struct gpio_desc gpios[] = {
{115, "ds18b20"}, // GPIO4_19 = 4×32+19 = 115
};
结构体打包设备信息,方便未来扩展到多个传感器。不需要中断号,因为 DS18B20 用纯软件时序。
6.2 自旋锁与微秒延时函数
c
static spinlock_t ds18b20_spinlock; // 全局自旋锁
为什么需要自旋锁?
DS18B20 的时序是微秒级的。如果程序在发送数据的中途被中断打断,回来时时序早已面目全非。所以每次访问总线时必须关中断 + 加锁,保证完整的读写事务是原子的。
c
static void ds18b20_udelay(int us)
{
u64 time = ktime_get_ns(); // 记录开始时间(纳秒)
while (ktime_get_ns() - time < us * 1000); // 直到经过了 us 微秒
}
逐行解释:
ktime_get_ns():返回系统启动以来的纳秒数(64 位无符号整数),精度极高。us * 1000:微秒转纳秒(1µs = 1000ns)。us=480就是 480,000ns。ktime_get_ns() - time:算出从记录起点到此刻经过了多少纳秒。- 只要还没达到目标,就继续循环等待。
这个函数必须在关中断环境下运行。 如果被中断插队,循环中途出去执行中断服务程序,几十微秒后回来,实际延时远超设定值,时序就乱了。
6.3 复位与应答检测函数
c
static int ds18b20_reset_and_wait_ack(void)
{
int timeout;
// 1. 主机拉低 480µs,发出复位脉冲
gpio_direction_output(gpios[0].gpio, 0);
ds18b20_udelay(480);
// 2. 主机释放总线,切换输入
gpio_direction_input(gpios[0].gpio);
// 3. 等待 DS18B20 拉低应答
timeout = 100;
while (gpio_get_value(gpios[0].gpio) && timeout--)
ds18b20_udelay(1);
if (timeout == 0)
return -EIO; // 超时,传感器未应答
// 4. 等待 DS18B20 释放总线(恢复高电平)
timeout = 300;
while (!gpio_get_value(gpios[0].gpio) && timeout--)
ds18b20_udelay(1);
if (timeout == 0)
return -EIO; // 超时,总线卡死了
return 0;
}
两个 while 的超时机制解析:
第一个 while:
gpio_get_value返回高电平(非 0)时条件成立 → 继续循环等待。- 传感器拉低应答时,返回 0,条件为假 → 退出循环,
timeout还没到 0 → 成功。 - 如果等了 100µs 都没拉低 →
timeout减到 0 → 返回-EIO。
第二个 while:
!gpio_get_value表示"引脚是低电平时"为真(!0 = 1)。- 传感器释放后引脚恢复高电平 →
!高电平 = 0→ 条件为假 → 退出 → 成功。 - 如果传感器一直拉低不松 → 300µs 后
timeout耗尽 → 返回-EIO。
6.4 发送一个字节函数
c
static void ds18b20_send_cmd(unsigned char cmd)
{
int i;
gpio_direction_output(gpios[0].gpio, 1); // 确保总线空闲为高
for (i = 0; i < 8; i++) {
if (cmd & (1 << i)) { // 当前位是 1
gpio_direction_output(gpios[0].gpio, 0);
ds18b20_udelay(2); // 只拉低 2µs
gpio_direction_output(gpios[0].gpio, 1);
ds18b20_udelay(60); // 拉高凑满时隙 60µs
} else { // 当前位是 0
gpio_direction_output(gpios[0].gpio, 0);
ds18b20_udelay(60); // 拉低 60µs
gpio_direction_output(gpios[0].gpio, 1);
}
}
}
关键点:
1 << i测试第i位(LSB first),i=0是最低位。- 写 1:拉低 2µs 就释放。传感器在 15~30µs 采样,看到高电平 → 1。
- 写 0:拉低 60µs 才释放。传感器在 15~30µs 采样时总线还是低 → 0。
- 整个时隙至少 60µs,我们的操作都满足要求。
6.5 读取一个字节函数
c
static void ds18b20_read_data(unsigned char *buf)
{
int i;
unsigned char data = 0;
gpio_direction_output(gpios[0].gpio, 1); // 空闲高
for (i = 0; i < 8; i++) {
// 1. 主机拉低,启动读时隙
gpio_direction_output(gpios[0].gpio, 0);
ds18b20_udelay(2); // 保持至少 1µs
// 2. 主机释放,切换输入
gpio_direction_input(gpios[0].gpio);
// 3. 等 15µs 让信号稳定,然后采样
ds18b20_udelay(15);
if (gpio_get_value(gpios[0].gpio))
data |= (1 << i); // LSB first
// 4. 补满时隙到 60µs
ds18b20_udelay(50);
// 5. 拉高总线,准备下一个时隙
gpio_direction_output(gpios[0].gpio, 1);
}
*buf = data;
}
采样为什么在 15µs?
释放总线后,传感器立即输出数据:
- 发 0:传感器拉低总线,约 15µs 后释放。
- 发 1:传感器不动,总线保持高。
在 15µs 时刻采样:对于发 0 的情况,传感器还拉着总线是低电平;对于发 1 的情况,总线一直高。正好能区分。 等到 30µs 再采样,发 0 的已经释放,总线被上拉电阻拉高,就分不清了。
6.6 CRC 校验与温度计算函数
c
static unsigned char calcrc_1byte(unsigned char abyte)
{
unsigned char i, crc_1byte = 0;
for (i = 0; i < 8; i++) {
if (((crc_1byte ^ abyte) & 0x01)) {
crc_1byte ^= 0x18;
crc_1byte >>= 1;
crc_1byte |= 0x80;
} else {
crc_1byte >>= 1;
}
abyte >>= 1;
}
return crc_1byte;
}
static unsigned char calcrc_bytes(unsigned char *p, unsigned char len)
{
unsigned char crc = 0;
while (len--)
crc = calcrc_1byte(crc ^ *p++);
return crc;
}
static int ds18b20_verify_crc(unsigned char *buf)
{
unsigned char crc = calcrc_bytes(buf, 8);
return (crc == buf[8]) ? 0 : -1;
}
DS18B20 使用的 CRC-8 多项式是 x^8 + x^5 + x^4 + 1(对应 0x18),算法是逐个字节移位异或。校验通过说明数据传输没出错。
c
static void ds18b20_calc_val(unsigned char ds18b20_buf[], int result[])
{
unsigned char tempL = ds18b20_buf[0]; // 温度低字节
unsigned char tempH = ds18b20_buf[1]; // 温度高字节
unsigned int integer;
unsigned char decimal1, decimal2, decimal;
if (tempH > 0x7f) { // 最高位为 1 → 负温度(补码形式)
tempL = ~tempL;
tempH = ~tempH + 1;
integer = tempL / 16 + tempH * 16;
decimal1 = (tempL & 0x0f) * 10 / 16;
decimal2 = (tempL & 0x0f) * 100 / 16 % 10;
decimal = decimal1 * 10 + decimal2;
} else { // 正温度
integer = tempL / 16 + tempH * 16;
decimal1 = (tempL & 0x0f) * 10 / 16;
decimal2 = (tempL & 0x0f) * 100 / 16 % 10;
decimal = decimal1 * 10 + decimal2;
}
result[0] = integer; // 温度整数部分
result[1] = decimal; // 温度小数部分(两位)
}
温度值怎么从两个字节里算出来?
以正温度为例:
tempL的低 4 位是小数部分,高 4 位是整数部分的低 4 位。tempH的低 4 位是整数部分的高 4 位。- 整数 =
(tempH << 4) | (tempL >> 4)=tempL / 16 + tempH * 16。 - 小数 =
(tempL & 0x0f) / 16,再转成十进制。
6.7 核心 read 函数:完整温度读取事务
c
static ssize_t ds18b20_read(struct file *file, char __user *buf,
size_t size, loff_t *offset)
{
unsigned long flags;
int err;
unsigned char kern_buf[9];
int i;
int result_buf[2];
if (size != 8)
return -EINVAL;
为什么检查 size != 8?
我们要返回两个 int(整数和小数),正好 8 字节。用户提供一个不匹配的缓冲区会出错。
c
/* 第一阶段:启动温度转换 */
spin_lock_irqsave(&ds18b20_spinlock, flags);
err = ds18b20_reset_and_wait_ack();
if (err) {
spin_unlock_irqrestore(&ds18b20_spinlock, flags);
printk("ds18b20_reset_and_wait_ack err\n");
return err;
}
ds18b20_send_cmd(0xCC); // Skip ROM
ds18b20_send_cmd(0x44); // Convert T(启动转换)
spin_unlock_irqrestore(&ds18b20_spinlock, flags);
spin_lock_irqsave 做了什么?
保存当前 CPU 的中断使能状态到 flags,然后关闭本地 CPU 的中断 。
这样从复位到发送完命令的这短短几十微秒内,绝不会被中断打断。
错误处理 :如果复位失败,必须先开中断(spin_unlock_irqrestore)再返回,否则中断一直关着,系统就废了。
c
/* 第二阶段:等待转换完成 */
set_current_state(TASK_INTERRUPTIBLE);
schedule_timeout(msecs_to_jiffies(1000));
为什么用睡眠而不是忙等?
DS18B20 转换温度需要几百毫秒(12 位时最多 750ms)。用忙等会浪费 CPU,让进程睡 1 秒是标准做法。
set_current_state(TASK_INTERRUPTIBLE):把当前进程状态设为"可中断睡眠"。schedule_timeout(msecs_to_jiffies(1000)):调度出去,1 秒后超时被唤醒。
这期间 CPU 可以干别的事,比如切换去跑其他进程。
c
/* 第三阶段:读取温度数据 */
spin_lock_irqsave(&ds18b20_spinlock, flags);
err = ds18b20_reset_and_wait_ack();
if (err) {
spin_unlock_irqrestore(&ds18b20_spinlock, flags);
printk("ds18b20_reset_and_wait_ack second err\n");
return err;
}
ds18b20_send_cmd(0xCC); // Skip ROM
ds18b20_send_cmd(0xBE); // Read Scratchpad
for (i = 0; i < 9; i++)
ds18b20_read_data(&kern_buf[i]);
spin_unlock_irqrestore(&ds18b20_spinlock, flags);
为什么又关中断?
读数据和发命令一样,都需要严格的时序保护。每次总线操作都必须关中断 + 加锁。
读 9 个字节,顺序是 Byte 0 到 Byte 8,按 LSB first 逐位拼出。
c
/* 第四阶段:校验与计算 */
err = ds18b20_verify_crc(kern_buf);
if (err) {
printk("ds18b20_verify_crc err\n");
return err;
}
ds18b20_calc_val(kern_buf, result_buf);
err = copy_to_user(buf, result_buf, 8);
return 8;
}
CRC 校验失败就扔掉数据,返回错误,应用可以重试。校验通过后提取温度值,通过 copy_to_user 安全地传给用户空间。
6.8 模块初始化与注销
c
static int __init ds18b20_init(void)
{
int i, err;
spin_lock_init(&ds18b20_spinlock); // 初始化自旋锁
for (i = 0; i < ARRAY_SIZE(gpios); i++) {
err = gpio_request(gpios[i].gpio, gpios[i].name); // 申请 GPIO
if (err) printk("gpio_request failed\n");
gpio_direction_output(gpios[i].gpio, 1); // 输出高,总线空闲
}
major = register_chrdev(0, "100ask_ds18b20", &gpio_key_drv);
gpio_class = class_create(THIS_MODULE, "100ask_ds18b20_class");
device_create(gpio_class, NULL, MKDEV(major, 0), NULL, "myds18b20");
return 0;
}
static void __exit ds18b20_exit(void)
{
device_destroy(gpio_class, MKDEV(major, 0));
class_destroy(gpio_class);
unregister_chrdev(major, "100ask_ds18b20");
for (int i = 0; i < ARRAY_SIZE(gpios); i++)
gpio_free(gpios[i].gpio); // 释放 GPIO
}
module_init(ds18b20_init);
module_exit(ds18b20_exit);
MODULE_LICENSE("GPL");
资源获取与释放的顺序必须严格对称:
- 初始化:锁 → GPIO → 字符设备 → 类 → 设备节点
- 卸载:设备节点 → 类 → 字符设备 → GPIO
7. 现场实录:编译、加载、运行
bash
# 编译
book@100ask:~/07_ds18b20$ make
arm-buildroot-linux-gnueabihf-gcc -o button_test button_test.c
# 生成 gpio_drv.ko 和 button_test
# 推送到板子
book@100ask:~$ adb push button_test gpio_drv.ko /root
# 进入板子
book@100ask:~$ adb shell
# 加载驱动
sh-5.0# insmod gpio_drv.ko
# 运行测试(持续读取温度)
sh-5.0# ./button_test /dev/myds18b20
get ds18b20: 27.87
get ds18b20: 27.87
get ds18b20: 27.87
^C
sh-5.0#

成功!室温 27.87°C,数据稳定,每次都能正确读出,说明时序和 CRC 校验都没问题。
8. 自测 5 道题
第 1 题
问题 :spin_lock_irqsave 和 spin_unlock_irqrestore 在这里的作用是什么?如果去掉这两个调用,直接用 gpio_direction_output 访问总线,可能会出现什么后果?
点击查看答案
作用:保存当前中断状态并关闭本地 CPU 中断,然后获取自旋锁。这保证从复位到发完命令的整个时序关键区是"原子"的,不会被打断。
去掉会怎样:在发送 480µs 复位脉冲或读写微秒级时序时,如果被其他中断打断(比如网络中断、定时器中断),CPU 去执行中断服务程序,几十微秒后才回来,DS18B20 的时序早已跑飞,通信必然失败。
第 2 题
问题 :代码中等待转换完成用的是 set_current_state(TASK_INTERRUPTIBLE) + schedule_timeout(1秒),为什么不能用 mdelay(1000) 代替?
点击查看答案
mdelay(1000) 是忙等待,CPU 在 1 秒内不断循环空转("原地踏步"),无法做任何其他事。在 Linux 内核中用忙等 1 秒是极其浪费的行为,会拖死整个系统。
schedule_timeout 则让当前进程主动释放 CPU 进入睡眠,1 秒后被内核定时器唤醒。这段时间 CPU 可以自由切换到其他进程或处理中断。
第 3 题
问题 :读一个字节的函数中,采样前等待了 ds18b20_udelay(15)(15µs)。如果把 15µs 改成 30µs 或 1µs,分别会发生什么?
点击查看答案
- 改成 1µs:DS18B20 释放总线后需要时间建立电平。1µs 太短,信号可能还没稳定,读到毛刺。
- 改成 30µs:对于发 0 的情况,DS18B20 只维持低电平约 15µs 后就释放了。30µs 时总线已经被上拉电阻拉回高电平,0 和 1 分不清,读到的永远是 1。
15µs 是在稳定性和捕获窗口之间的最佳平衡点。
第 4 题
问题:为什么 DS18B20 的数据引脚要接 4.7kΩ~10kΩ 的上拉电阻?如果不接上拉电阻,直接连到 GPIO,会发生什么?
点击查看答案
DS18B20 的 DQ 引脚是开漏输出,只能输出低电平或高阻态。如果没有上拉电阻:
- 当主机和传感器都释放总线时,引脚处于"浮空"状态,电平随机,可能是高也可能是低。
- 主机拉低后释放总线时,总线无法自动恢复到高电平,传感器无法检测到上升沿,通信失败。
上拉电阻就像"弹簧":没人摸总线时,它自动把电平拉回高。
第 5 题
问题 :ds18b20_read 函数一开始检查 if (size != 8) return -EINVAL;。这个 8 是怎么来的?如果用户想只读温度整数(4 字节),把 size 改为 4 传入可以吗?为什么?
点击查看答案
8 是因为 result_buf[2] 是两个 int,共 2 × sizeof(int) = 8 字节。驱动硬编码了 copy_to_user(buf, result_buf, 8),如果用户传入 size=4:
size != 8条件成立,直接返回-EINVAL,用户什么也读不到。- 即使去掉检查,
copy_to_user只拷贝 4 字节,但驱动还是返回8给用户,造成用户 buffer 溢出或数据不完整。
正确的设计应该支持弹性长度或至少返回最小必要数据。这里是教学简化,实际产品应更灵活。
全文完。如果你从头到尾敲完了代码,通过了自测,那么恭喜你------DS18B20 的驱动框架已经被你吃透了。这个"自旋锁 + 精确延时 + 状态机时序"的思想,可以迁移到任何需要严格时序保护的外设驱动中。