今天的学习目标非常明确,就是攻克那个经典又让人爱恨交加的"单总线"器件------DS18B20数字温度传感器。作为一个在嵌入式领域经久不衰的明星元件,它虽然只需要一根线就能通信,但背后的时序逻辑却极其考验对单片机GPIO口的操作功底。
整理这份笔记与代码,不仅是记录参数,更是对"线与逻辑"和"严格时序"的一次深度梳理。
一、 认识硬件:不仅是测温,更是精度的艺术
DS18B20之所以应用广泛,首先在于它出色的参数指标。在我们的设备温度和环境温度监测场景中,它的表现相当稳健。
量程与精度是首要关注的指标。它支持 -55℃ 到 +125℃ 的宽范围测量。在常温25℃的环境下,其精度可达 ±0.5℃。这意味着如果你测到24.5℃到25.5℃,都在合理误差范围内。
更有意思的是它的分辨率配置。DS18B20允许我们根据需求在9位到12位之间切换,这直接决定了温度变化的"细腻程度":
·9位时,分辨率是0.5℃(变化跨度大);
·10位是0.25℃;
·11位是0.125℃;
·而最高12位分辨率下,精度可达 0.0625℃。这对于需要捕捉微小温度波动的场景至关重要。
在工作电压方面,它支持 3V - 5.5V,完美兼容51单片机的5V逻辑电平。
二、 通信原理:独特的"单总线"与"线与特性"
DS18B20最特别的地方在于它采用单总线(One-Wire)协议。51单片机只需要一个IO口(通常接P3.7)即可完成双向通信。但这并不是简单的"你发我收",而是一种半双工、异步、串行的通信方式。
这里必须理解"线与特性"(Wired-AND)。在硬件电路上,DS18B20的DQ引脚是开漏输出的。这意味着:
·当单片机输出高电平(1),而DS18B20输出低电平(0)时,总线电平为0(低电平优先)。
·只有当双方都输出高电平时,总线才会被上拉电阻拉高到1。
这就解释了为什么我们在操作时,"释放总线"(即单片机输出1,由外部上拉电阻决定电平)是如此重要的动作。
在代码实现上,我们可以用宏定义来简化位操作,避免影响 P3 端口的其他引脚:
#define DQ_PIN_LOW (P3 &= ~(1 << 7)) // DQ引脚拉低
#define DQ_PIN_HIGH (P3 |= (1 << 7)) // DQ引脚拉高
#define DQ_CHECK ((P3 & (1 << 7)) != 0) // 判断DQ引脚为高电平
三、 核心时序:与微秒赛跑(代码实现篇)
这一部分的学习是最具挑战性的,因为DS18B20对时间极其敏感。任何一步的延时错误,都会导致通信失败。我们将时序逻辑直接落实在代码中。
- 复位时序(初始化)
通信的第一步永远是"唤醒"。单片机必须先将总线拉低 480us - 960us 发送复位脉冲,然后释放总线等待 15-60us。如果DS18B20存在且正常,它会在 60-240us 后拉低总线作为"应答脉冲"。
// ds18b20 复位
int ds18b20_reset(void)
{
int time = 0;
// 1. 主机向ds18b20发送复位脉冲
DQ_PIN_LOW;
Delay10us(70); // 拉低约700us (满足480us-960us)
DQ_PIN_HIGH;
Delay10us(6); // 释放总线,等待约60us
// 2. 等待低电平到来(检测DS18B20的应答脉冲)
while (DQ_CHECK && time < 30) // 超时时间约300us
{
Delay10us(1);
time++;
}
// 3. 超时处理
if (time >= 30)
{
uart_sendstr("wait ds18b20_high timeout!\n");
return -2;
}
return 1;
}
【代码笔记】:time < 30 是一个很好的超时退出机制,防止因为传感器损坏导致程序死循环。
- 写时序(Write)
写操作分为写"0"和写"1"。所有的写操作,都必须由主机从拉低总线开始。
·写0: 拉低总线 60us - 120us,然后释放。
·写1: 拉低总线 小于15us,然后迅速释放。
void write_ds18b20(unsigned char dat)
{
int i = 0;
for (i = 0; i < 8; i++)
{
if (dat & 1) // 写1
{
DQ_PIN_LOW;
nop(); nop(); // 拉低约2us (<15us)
DQ_PIN_HIGH;
Delay10us(6); // 等待60us,给DS18B20采样时间
}
else // 写0
{
DQ_PIN_LOW;
Delay10us(6); // 拉低约60us (满足60us-120us)
DQ_PIN_HIGH;
nop(); // 【关键细节】释放总线后加1us恢复时间
}
dat >>= 1;
}
}
【代码笔记】:写0结束时加一个 nop() 是非常重要的,这确保了总线有充分的恢复时间,防止DS18B20将连续的写0误判。
- 读时序(Read)
读操作同样由主机发起。主机拉低总线大于1us,然后释放,接着在15us内进行采样。
// 主机读取一个字节
unsigned char read_ds18b20(void)
{
unsigned char dat = 0;
int i = 0;
for (i = 0; i < 8; i++)
{
DQ_PIN_LOW;
nop(); nop();
DQ_PIN_HIGH; // 主机拉低>1us后释放总线
// 【关键细节】等待约10us,给DS18B20拉低总线的反应时间
Delay10us(1);
if (DQ_CHECK) // 此时读取总线电平
{
dat |= (1 << i); // 代表读到1
}
Delay10us(6); // 位周期等待,满足60us的读时隙
}
return dat;
}
【代码笔记】:很多初学者读出来全是0,就是因为 DQ_PIN_HIGH 之后没有延时,立刻去读,此时DS18B20还没反应过来把总线拉低。加上 Delay10us(1) 就能解决。
四、 采集流程:从指令到数据
掌握了时序,采集温度的逻辑就变得像流水线一样清晰。整个流程主要分为两个阶段:
-
启动转换:复位 -> 发送 0xCC (跳过ROM) -> 发送 0x44 (开启转换) -> 延时等待。
-
读取温度:复位 -> 发送 0xCC (跳过ROM) -> 发送 0xBE (读取暂存器) -> 读取两个字节。
// 读取ds18b20温度
float get_temp(void)
{
unsigned char temp_low = 0;
unsigned char temp_high = 0;
short temp = 0; // 【关键】使用有符号短整型,以处理负数温度
// 阶段一:启动温度转换
ds18b20_reset(); // 复位
write_ds18b20(0xCC); // skip ROM
write_ds18b20(0x44); // 开启温度转换
// 等待温度转换完毕。12位分辨率最大需要750ms,这里给足1s
Delay1ms(1000);
// 阶段二:读取温度
ds18b20_reset(); // 复位
write_ds18b20(0xCC); // skip ROM
write_ds18b20(0xBE); // 读取温度
temp_low = read_ds18b20(); // 读取温度低位
temp_high = read_ds18b20(); // 读取温度高位
// 2个字节合并温度值(符号位扩展)
temp = temp_high << 8;
temp |= temp_low;
// 12位分辨率下,最小刻度是 0.0625℃
return temp * 0.0625;
}
【代码笔记】:这里 short temp 的使用非常关键。如果 DS18B20 测到负温度(例如 -10℃),高字节的最高位会是1。如果使用 unsigned short,负数会被解析成一个巨大的正数。short 会自动完成补码转原码的过程,配合 * 0.0625 就能得到正确的负数温度。
五、 总结与反思
通过今天的学习与代码编写,我深刻体会到DS18B20虽然硬件连接简单(VCC、GND、DQ三根线),但软件模拟时序的复杂度不容小觑。它的"单总线"协议实际上是用时间换空间的典型设计。
对于51单片机来说,没有硬件外设支持,全靠GPIO口的拉高拉低和微秒级的延时函数来"硬"写时序。这让我明白,在嵌入式开发中,对时间的敬畏是第一位的。哪怕是一个几十微秒的延时偏差,都会导致读写数据的错位。
最后,加上 UART 串口的 sprintf 打印函数,形成完整的闭环,使得调试过程一目了然:
// main.c 主逻辑
int main(void)
{
float temp = 0;
char s32 = {0};
uart_init();
while (1)
{
temp = get_temp();
sprintf(s, "temp = %.2f\n", temp);
uart_sendstr(s);
// 注意:这里实际上建议加一个延时,比如 Delay1ms(1000),
// 否则串口打印太快,且DS18B20会一直处于转换状态导致发热影响测量
}
return 0;
}
搞定DS18B20,对于后续理解其他复杂的单总线或I2C器件,绝对是一次极好的练兵。