
天空星GD32F407开发板驱动DS18B20单总线温度采集实战与代码解析最近在做一个环境监测的小项目需要用到温度传感器。考虑到布线的方便和成本我选择了经典的DS18B20数字温度传感器。它最大的特点就是只需要一根数据线就能和单片机通信特别适合需要多点测温或者布线空间有限的场景。正好手头有天空星的GD32F407开发板今天我就带大家一步步把DS18B20驱动起来把温度数据稳稳地读出来。这篇文章就是一份手把手的实战教程我会从DS18B20的基本原理讲起重点放在如何精确实现那个有点“挑剔”的单总线通信时序最后把完整的、可以直接用的代码移植到GD32F407的工程里。无论你是刚开始接触嵌入式还是正在为项目选型发愁跟着做一遍你就能掌握这个非常实用的技能。1. 认识我们的主角DS18B20温度传感器在动手写代码之前咱们先花几分钟了解一下DS18B20到底是个啥它有什么特点这样后面理解通信过程会容易很多。DS18B20是一个数字式的温度传感器它内部直接把测到的温度转换成了数字信号所以我们单片机读到的直接就是数字不需要自己再做复杂的模数转换。它的精度可以在9位到12位之间配置对应的温度分辨率分别是0.5°C、0.25°C、0.125°C和0.0625°C。出厂默认是12位也就是最精确的0.0625°C。它最厉害的地方就是“单总线”通信。顾名思义只需要一根数据线通常叫DQ线加上电源和地线就能完成所有数据的收发。更绝的是它甚至支持“寄生供电”模式连电源线都可以省掉直接从数据线上“偷电”用。不过为了稳定起见咱们这次还是用传统的三线接法VCC、GND、DQ。每个DS18B20在出厂时都被写入了一个全球唯一的64位序列号。这意味着你可以把很多个DS18B20挂在同一根数据线上通过序列号来分别和它们对话实现一个单片机监控几十上百个测温点的效果这在楼宇温控、农业大棚等场景非常有用。咱们这次用的模块参数很简单工作电压3.0V ~ 5.5V直接用开发板的3.3V或5V都行测量范围-55°C 到 125°C测量精度±0.5°C在-10°C到85°C范围内接口3Pin排针VCC, GND, DQ2. 单总线通信像对暗号一样严谨的时序DS18B20的所有操作都建立在一种严格的“时序”之上。你可以把它想象成一种非常精密的“对暗号”过程。主机我们的GD32和从机DS18B20之间没有时钟线同步所以全靠每一个高低电平的持续时间来传递信息。时序要是差得太多DS18B20就“听不懂”或者“不回答”了。整个通信过程建立在几种基本时序之上复位脉冲、应答脉冲、写时隙和读时隙。下面我结合代码把每个时序的关键点给你讲明白。2.1 初始化复位与应答这是每次通信开始前的“握手”仪式目的是检测总线上有没有DS18B20设备并让它做好准备。主机拉低总线我们的单片机把DQ引脚拉成低电平并且这个低电平要保持至少480微秒。这个长的低电平信号就是一个“复位脉冲”告诉总线上的所有DS18B20“注意我要开始讲话了”主机释放总线单片机把DQ引脚切换成输入模式高阻态。由于我们硬件上会在DQ线加一个上拉电阻比如4.7KΩ总线会被这个电阻拉回到高电平。从机应答如果总线上有DS18B20它会在等待大约15-60微秒后主动把总线拉低并保持60-240微秒。这个低电平就是它的“应答脉冲”意思是“我在呢请讲。”从机释放总线DS18B20释放总线总线再次被上拉电阻拉高。至此握手完成。在代码里这个过程对应着DS18B20_Check()函数。里面有两个关键的时间等待循环一个等待DS18B20拉低应答一个等待DS18B20释放总线。如果超时了函数就返回1表示没找到设备。uint8_t DS18B20_Check(void) { uint8_t timeout 0; // 1. 主机拉低总线至少480us (代码中是750us更保险) DQ_OUT(); // 设置为输出模式 DQ(0); // 拉低DQ delay_us(750); // 拉低750us DQ(1); // 拉高DQ delay_us(15); // 等待15us // 2. 切换为输入模式等待应答 DQ_IN(); // 等待DS18B20拉低总线应答 while (DQ_GET() timeout 200) { timeout; delay_us(1); }; if(timeout 200) return 1; // 超时无应答 timeout 0; // 3. 等待DS18B20释放总线拉高 while (!DQ_GET() timeout 240) { timeout; delay_us(1); }; if(timeout 240) return 1; // 超时释放失败 return 0; // 检测成功 }注意这里的延时时间如750us, 15us都是根据DS18B20数据手册要求的最小值再适当放宽一些以确保兼容性。在实际项目中如果时序不对第一个要检查的就是这些延时函数准不准。2.2 写一个比特写时隙握手成功后我们就可以给DS18B20发送命令了。命令是由一个个比特0或1组成的。写一个比特的时序有两种写“0”和写“1”。写“0”时隙主机拉低总线保持至少60微秒。主机释放总线切换为输入等待总线被上拉电阻拉高。整个时隙长度要大于60微秒。写“1”时隙主机拉低总线保持很短时间1-15微秒。然后主机必须在15微秒内释放总线并让总线保持高电平直到时隙结束。整个时隙长度也要大于60微秒。看出来了吗DS18B20是在主机拉低总线后大约15-60微秒这个窗口期内去采样总线电平的。如果采样时总线是低电平它就认为是“0”是高电平就认为是“1”。所以写“1”的关键在于主机拉低一下之后要迅速放开。void DS18B20_Write_Byte(uint8_t dat) { uint8_t i; DQ_OUT(); // 设置为输出模式 for (i0; i8; i) { if ( (dat 0x01) ) { // 判断要写的是1还是0 // 写“1”时序 DQ(0); // 拉低一下 delay_us(2); // 保持很短时间 DQ(1); // 迅速释放 delay_us(60); // 等待时隙结束 } else { // 写“0”时序 DQ(0); // 拉低 delay_us(60); // 保持低电平60us DQ(1); // 释放 delay_us(2); // 短暂恢复 } dat dat 1; // 准备发送下一个比特从低位开始发 } }2.3 读一个比特读时隙读数据的过程是由主机发起的。主机启动一个读时隙然后DS18B20会把数据放到总线上。主机启动时隙主机拉低总线至少1微秒。主机释放总线主机释放总线切换为输入并在拉低后的15微秒内采样总线电平。从机响应如果DS18B20要发送“0”它会在主机拉低后持续把总线拉低。如果发送“1”它则释放总线让上拉电阻把总线拉高。时隙结束整个读时隙从开始到结束必须至少持续60微秒然后需要至少1微秒的恢复时间才能开始下一个时隙。uint8_t DS18B20_Read_Byte(void) { uint8_t i0, dat0; for (i0; i8; i) { dat 1; // 先右移为接收新比特腾出最低位 DQ_OUT(); // 设置为输出模式 DQ(0); // 1. 主机拉低启动读时隙 delay_us(2); // 保持低电平至少1us DQ(1); // 2. 主机迅速释放总线 DQ_IN(); // 切换为输入模式准备采样 delay_us(12); // 等待约12us后采样在15us窗口内 if( DQ_GET() ) { // 3. 采样总线电平 dat dat | 0x80; // 如果为高收到的是“1”放到最高位 } delay_us(50); // 4. 等待读时隙结束总时长约21250 60us } return dat; }3. 移植到天空星GD32F407开发板原理搞清楚了现在咱们把代码搬到GD32F407的工程里。我假设你已经有一个基于天空星开发板的基础工程比如能点亮LED、串口能打印。3.1 硬件连接首先把DS18B20模块和开发板连起来非常简单DS18B20模块引脚连接到GD32F407开发板VCC (红线)3.3V 或 5V 电源引脚GND (黑线)GND 地线引脚DQ (黄/绿线)我们选择PB8提示单总线需要一个上拉电阻通常4.7KΩ连接到VCC以确保总线在空闲时为高电平。很多DS18B20模块已经内置了这个电阻如果你的模块没有需要在DQ和VCC之间自己加一个。3.2 文件准备与引脚配置我们需要两个文件bsp_ds18b20.c和bsp_ds18b20.h。你可以直接从提供的资料里复制或者自己新建。在头文件bsp_ds18b20.h里最关键的是引脚宏定义。这里我们把DQ线定义在了PB8引脚上。#ifndef _BSP_DS18B20_H_ #define _BSP_DS18B20_H_ #include gd32f4xx.h // 端口移植 - 根据你的实际连接修改这里 #define RCU_DQ RCU_GPIOB // DQ引脚所在的GPIO时钟 #define PORT_DQ GPIOB // DQ引脚所在的GPIO端口 #define GPIO_DQ GPIO_PIN_8 // DQ引脚编号 // 设置DQ为输出模式主机控制总线 #define DQ_OUT() gpio_mode_set(PORT_DQ, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_DQ) // 设置DQ为输入模式主机释放总线读取状态 #define DQ_IN() gpio_mode_set(PORT_DQ, GPIO_MODE_INPUT, GPIO_PUPD_PULLUP, GPIO_DQ) // 读取DQ引脚的电平 #define DQ_GET() gpio_input_bit_get(PORT_DQ, GPIO_DQ) // 设置DQ引脚输出高或低电平 #define DQ(x) gpio_bit_write(PORT_DQ, GPIO_DQ, (x ? SET : RESET)) // 函数声明 uint8_t DS18B20_Check(void); char DS18B20_GPIO_Init(void); void DS18B20_Start(void); float DS18B20_GetTemperture(void); uint8_t DS18B20_Read_Byte(void); void DS18B20_Write_Byte(uint8_t dat); #endif在源文件bsp_ds18b20.c的开头我们需要初始化这个GPIO引脚。这个初始化函数DS18B20_GPIO_Init()还会顺便调用检测函数看看设备在不在。char DS18B20_GPIO_Init(void) { unsigned char ret 255; /* 使能GPIOB的时钟 */ rcu_periph_clock_enable(RCU_DQ); /* 先配置为输出模式并初始化输出高电平释放总线*/ gpio_mode_set(PORT_DQ, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_DQ); gpio_output_options_set(PORT_DQ, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_DQ); DQ(1); // 初始化为高电平 ret DS18B20_Check(); // 检测器件是否存在 return ret; // 返回0表示存在1表示不存在 }3.3 核心命令与温度读取流程DS18B20的操作有一系列标准命令。我们最常用的就三个跳过ROM命令 (0xCC)当总线上只有一个DS18B20时可以用这个命令跳过寻址步骤直接对设备进行操作简化流程。开始温度转换命令 (0x44)发送这个命令后DS18B20开始进行一次温度测量。对于12位精度转换时间最长可达750ms。读取暂存器命令 (0xBE)发送这个命令后可以连续读取DS18B20内部暂存器的9个字节前两个字节就是温度值。获取一次温度的完整流程如下初始化复位应答检测。发送0xCC命令跳过ROM。发送0x44命令开始转换。等待转换完成重要可以简单延时750ms或者用更高效的方式查询。再次初始化。发送0xCC命令。发送0xBE命令读暂存器。连续读取两个字节温度值的低字节和高字节。将两个字节组合成一个16位整数并换算成实际温度值。这个流程在DS18B20_GetTemperture()函数里实现了float DS18B20_GetTemperture(void) { uint16_t temp; uint8_t dataL 0, dataH 0; float value; // 1. 启动一次温度转换 DS18B20_Start(); // 这个函数内部包含了初始化和发送0xCC, 0x44命令 // 2. 等待转换完成。这里为了简单直接延时。实际项目建议用查询DS18B20状态的方式。 delay_ms(750); // 3. 再次初始化准备读取数据 DS18B20_Check(); // 4. 发送跳过ROM和读取命令 DS18B20_Write_Byte(0xcc); // 跳过ROM DS18B20_Write_Byte(0xbe); // 读取暂存器命令 // 5. 读取温度值低字节在前高字节在后 dataL DS18B20_Read_Byte(); // LSB 低字节 dataH DS18B20_Read_Byte(); // MSB 高字节 // 6. 合成16位数据 temp (dataH 8) dataL; // 7. 温度换算 if(dataH 0X80) { // 判断符号位bit151为负数 // 对于负数数据以补码形式存储需要先取反加1得到原码 temp (~temp) 1; value temp * (-0.0625); // 乘以分辨率得到负温度 } else { value temp * 0.0625; // 正温度 } return value; }温度值换算说明 DS18B20输出的温度数据是16位二进制补码形式。默认12位分辨率下最低4位是小数部分。将读取到的16位整数乘以分辨率0.0625就得到了实际的摄氏温度值。例如读到的数据是0x0191十进制401那么温度就是401 * 0.0625 25.0625°C。4. 实战验证在main函数中读取并打印温度最后我们在主函数里调用这些函数并通过串口把温度打印出来看看。#include board.h #include bsp_ds18b20.h #include stdio.h // 用于printf int main(void) { // 开发板基础初始化时钟、延时、串口等 board_init(); bsp_uart_init(); // 初始化串口用于打印 // 初始化DS18B20 GPIO并检测设备 if(DS18B20_GPIO_Init() 0) { printf(DS18B20 Init OK!\r\n); } else { printf(DS18B20 Not Found!\r\n); while(1); // 卡住检查硬件连接 } while(1) { // 读取温度并打印保留两位小数 float temp DS18B20_GetTemperture(); printf(Temperature %.2f C\r\n, temp); // 每隔1秒读取一次 delay_ms(1000); } }把代码编译下载到天空星GD32F407开发板打开串口助手波特率根据你的bsp_uart_init()设置通常是115200你应该能看到每隔一秒输出一次当前的温度值。如果没成功按这个顺序排查硬件连接VCC、GND、DQ三根线是否接对、接牢上拉电阻有没有引脚定义检查bsp_ds18b20.h里的GPIO_DQ定义是否和你实际连接的引脚一致。延时函数delay_us()和delay_ms()是否准确单总线对微秒级延时很敏感。可以用示波器或者逻辑分析仪抓一下DQ线的波形对照数据手册的时序图看。电源干扰如果读数偶尔跳动或出错可能是电源噪声。尝试在DS18B20的VCC和GND之间加一个0.1uF的滤波电容。搞定现在你的天空星GD32F407开发板已经能熟练地和DS18B20“对话”了。这个单总线驱动代码是很多传感器如DHT11的基础理解透彻了以后再玩其他单总线设备就轻松多了。在实际项目中你可以把温度读取函数放到定时器中断里实现周期性的采集或者结合多个DS18B20实现区域温度监测玩法很多。