1. 驱动概述与设计思路
DS18B20作为一款经典的单总线数字温度传感器,在嵌入式测温领域有着广泛应用。这个驱动基于STM32 HAL库开发,专门针对单从机使用场景进行了优化设计。单总线协议虽然只需要一根数据线,但时序要求极为严格,这也是许多初学者容易出错的地方。
我在实际项目中遇到过不少DS18B20驱动问题,发现80%的故障都源于时序控制不当。这个驱动采用低位先行的位操作逻辑,严格遵循DS18B20协议的核心时序要求。特别值得注意的是,当前版本仅处理正温度数据(0°C以上),负温度数据留作业务层处理,这种设计既简化了驱动复杂度,又为后续功能扩展留出了空间。
驱动核心功能包括:
- 温度转换指令发送(0x44)
- 温度原始数据读取(0xBE)
- 16位温度数据解析(高低8位分开读取)
提示:DS18B20的0.0625°C/LSB分辨率是温度计算的关键,这个值直接来源于传感器规格书,不要随意更改。
2. 硬件连接与初始化
2.1 硬件定义解析
驱动中硬件相关的定义集中在以下宏:
c复制#define ONEPORT GPIOA
#define ONEPIN GPIO_PIN_4
这种设计使得引脚修改非常方便,只需改动这两处定义即可适配不同的硬件连接方案。在我的实际项目中,建议将DS18B20数据线通过4.7kΩ上拉电阻连接到STM32的GPIO引脚,这是保证信号完整性的关键。
2.2 初始化时序详解
初始化(复位)时序是单总线通信的第一步,也是最容易出错的地方:
c复制void onewire_init(void)
{
onewire_pin_set(0); // 拉低总线
delay_us(500); // 保持480us以上(实际用500us留余量)
onewire_pin_set(1); // 释放总线
delay_us(250); // 等待从机响应
}
这里有几个关键点:
- 拉低时间必须大于480us(DS18B20规格书要求)
- 释放总线后等待时间应在15-60us内检测从机响应
- 实际代码中等待250us是为了简化逻辑,牺牲了从机存在检测
注意:如果系统中有多个DS18B20设备,必须实现完整的从机检测逻辑,当前驱动仅适用于单从机场景。
3. 数据收发实现细节
3.1 字节发送机制
发送单字节的函数采用位操作方式,严格遵循单总线协议的时序要求:
c复制void SendByte(uint8_t onebyte)
{
for(uint8_t time=0;time<8;time++) {
onewire_pin_set(1); // 先拉高
delay_us(2); // 短暂保持
onewire_pin_set(0); // 开始写时序
delay_us(2); // 保持至少1us
onewire_pin_set((0x01<<time)&onebyte); // 按位发送
delay_us(60); // 保持写时序
}
onewire_pin_set(1); // 最终拉高
}
每个位的发送都包含以下阶段:
- 拉高总线(至少1us)
- 拉低总线(开始写时序)
- 在15us内设置数据位电平
- 保持60us完成写周期
3.2 字节读取技巧
读取字节时同样采用位操作方式,但时序控制更为关键:
c复制uint8_t ReadByte(void)
{
uint8_t result=0;
for(uint8_t time=0;time<8;time++) {
onewire_pin_set(1); // 拉高
delay_us(2); // 保持1us以上
onewire_pin_set(0); // 拉低(开始读时序)
delay_us(2); // 保持至少1us
delay_us(10); // 等待10us后采样
result|=(HAL_GPIO_ReadPin(ONEPORT,ONEPIN)<<time);
delay_us(50); // 完成读周期
}
return result;
}
读时序的关键点:
- 主设备拉低总线至少1us后释放
- 在15us内采样数据线状态
- 整个读周期应保持至少60us
4. 温度采集全流程解析
4.1 单次温度转换流程
完整的温度采集流程封装在singleslave()函数中:
c复制uint8_t singleslave(void)
{
float final=0;
uint8_t low=0,high=0,save=0;
// 1. 初始化总线
onewire_init();
// 2. 发送跳过ROM命令(0xCC)
SendByte(0xcc);
// 3. 发送温度转换命令(0x44)
SendByte(0x44);
// 4. 等待转换完成(最大750ms)
HAL_Delay(800);
// 5. 再次初始化总线
onewire_init();
// 6. 发送跳过ROM命令(0xCC)
SendByte(0xcc);
// 7. 发送读取暂存器命令(0xBE)
SendByte(0xbe);
// 8. 读取温度值(低字节和高字节)
low=ReadByte();
high=ReadByte();
// 9. 计算实际温度值
final=(high*256+low)*0.0625;
save=(uint8_t)final;
return save;
}
4.2 温度数据解析
DS18B20的温度数据格式需要特别注意:
- 16位数据(2字节)存储温度信息
- 低字节的bit0-bit3是小数部分
- 高字节的bit4-bit7是符号位和整数部分
- 0.0625是固定系数(1/16)
温度计算公式:
code复制实际温度 = (高字节×256 + 低字节) × 0.0625
注意:当前驱动将最终温度强制转换为uint8_t,会丢失小数部分和负温度信息。如需完整精度,应直接返回float类型的final值。
5. 常见问题与调试技巧
5.1 初始化失败排查
现象:读取的温度值始终为0或255
可能原因:
- 硬件连接问题(检查上拉电阻和数据线连接)
- 时序不满足要求(用逻辑分析仪检查波形)
- 延时函数精度不足(确保delay_us()准确)
5.2 温度值异常排查
现象:温度值明显错误或波动大
解决方法:
- 检查电源稳定性(DS18B20对电源噪声敏感)
- 确保转换等待时间足够(12位精度需750ms)
- 检查总线是否有干扰(缩短数据线长度)
5.3 优化建议
- 增加CRC校验(提高数据可靠性)
- 实现负温度处理(完善业务逻辑)
- 添加多点测温支持(改进初始化检测)
- 采用DMA+定时器实现精确延时(提升时序精度)
6. 延时函数实现要点
驱动依赖精确的微秒级延时函数,HAL库本身不提供delay_us(),需要自行实现。常见的实现方式有:
6.1 基于SysTick的实现
c复制void delay_us(uint32_t us)
{
uint32_t start = HAL_GetTick();
while((HAL_GetTick() - start) < us);
}
注意:这种方法精度有限,通常只能达到毫秒级。
6.2 基于定时器的精确延时
c复制void delay_us(uint32_t us)
{
__HAL_TIM_SET_COUNTER(&htimX, 0);
HAL_TIM_Base_Start(&htimX);
while(__HAL_TIM_GET_COUNTER(&htimX) < us);
HAL_TIM_Base_Stop(&htimX);
}
这种方法精度可达微秒级,但需要配置一个基本定时器(如TIM6/TIM7)。
在实际项目中,我发现使用定时器实现的延时函数稳定性更好,特别是在不同主频下的移植性更强。如果对时序要求极高,建议采用定时器方案。