DS18B20 是一款经典的数字温度传感器,采用 1-Wire 单总线协议 ,只需要一根数据线就可以完成通信。
这篇博客通过一个 DS18B20 驱动示例,把 platform 总线、misc 设备、GPIO 子系统、单总线时序、字符设备驱动框架 串起来。
一、DS18B20 与单总线协议
DS18B20 使用单总线通信,主机和从机共用一根数据线。通信前需要先复位,再检测从机是否存在,最后发送命令并读取数据。
常见读取流程如下:
- 主机发送复位脉冲;
- 从机返回存在脉冲;
- 主机发送
0xCC,跳过 ROM 匹配; - 主机发送
0x44,启动温度转换; - 等待温度转换完成;
- 再次复位;
- 发送
0xCC; - 发送
0xBE,读取暂存器; - 读取温度低字节和高字节;
- 组合成 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 主要做了三件事:
-
注册 misc 设备;
-
从设备树获取 DS18B20 使用的 GPIO;
-
申请 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);
}
流程是:
- 复位;
- 检测应答;
- 发送
0xCC跳过 ROM; - 发送
0x44启动转换; - 等待 500ms;
- 再次复位;
- 发送
0xBE读取暂存器; - 读取 TL、TH;
- 组合温度值;
- 通过
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完成内核态到用户态的数据传递。
它很适合用来理解:驱动层如何把硬件协议、内核框架和用户空间接口串起来。