每年到了备赛季,总有人私信问我“蓝桥杯单片机到底该学什么”“传感器哪个最值得吃透”。我的答案里一定有温度传感器DS18B20。这颗小小的三脚器件在单片机竞赛里出现频率高得惊人,代码量不大,但背后的单总线时序、硬件连接、显示配合,几乎能把单片机的基础知识串个遍。考场上真正拉开差距的往往不是谁背的模板多,而是谁能在几分钟内把一个时序不稳的传感器调稳定。
这篇文章适合两类人:一类是刚接触蓝桥杯单片机、准备把常用模块逐个击破的入门选手;另一类是已经能跑通例程,但一上电温度就显示85度、或者一开数码管数据就乱跳的“受害者”。我会从竞赛考法、单总线协议、手写驱动、上板集成、现场调试五个角度讲透,所有代码都按竞赛板常用主控写,可直接抄进工程里跑。
1. 竞赛视角:DS18B20为什么是蓝桥杯的常客
1.1 从考纲到真题:一个传感器串起的时间题
蓝桥杯单片机设计与开发赛道每年题目都会在几个“固定嘉宾”里选角:DS18B20测温、DS1302时钟、PCF8591的ADC/DAC、AT24C02存储,再加上按键、数码管、LED和蜂鸣器。DS18B20之所以每次都稳坐C位,是因为它天然适合出“综合性大题”。一道典型题目可以这样设计:用DS18B20实时采集温度,显示在数码管上;按键切换显示摄氏温度和华氏温度;超过设定的上限时,驱动蜂鸣器报警;再配合DS1302把时间日期显示出来,自动记录当前温度值。你看,一颗温度传感器就把“采集—处理—显示—交互—存储”整个链路全打通了。
更关键的是,器件的考法层级分明。低难度题目只要求你会读温度、显示出来;中等难度会要求你处理负温度、小数位;高难度则会把单总线协议玩出花——比如要求你通过ROM匹配指令读取多个DS18B20的温度,或者要求温度刷新刷新间隔小于某毫秒数。很多同学只背了一个读温度的例程就上场,结果题目一改就懵。你只有真正理解单总线的每一次电平变化,才能从容应对这些变化。
1.2 考场上它通常和谁搭配出现
从近几年的趋势看,DS18B20几乎很少单独出场。最常见的一组搭配是“DS18B20 + DS1302 + 数码管 + 独立按键”,做成一个温度时钟记录器;另一种是“DS18B20 + PCF8591 + 串口”,做成一个数据采集终端,既采集温度又采集光照强度或电位器电压。传感器之间的联动逻辑往往是拿分重点:比如“每秒钟把温度刷新到数码管,每30秒把温度写入存储芯片”,这对主循环的调度能力要求不低。
还有一个容易被忽略的点:温度传感器的“读取时机”会影响整个系统的时序。DS18B20的单总线协议要求严格的微秒级波形,如果你在主循环里写了一大堆按键扫描、数码管刷新,很容易在读取过程中被拖垮。竞赛题目不会明说“你要注意时序”,但每道题本质上都在考察你协调外设的能力。我见过太多人代码全部正确,就因为读取温度时被中断打断,最终显示的温度就是跳来跳去的。
所以在动driver之前,先把DS18B20放在整个系统框架里想清楚:什么时候启动转换、什么时候读取结果、其他外设能不能在这期间占用CPU。想清楚这个,你已经在思路上赢了一半。
2. 单总线协议:一根信号线上的“地道战”
2.1 硬件电路:就一根线,却牵一发动全身
DS18B20号称单总线器件,是因为它的供电和数据共用一根线,市面上常见封装是TO-92的三脚:VCC、GND、DQ。竞赛板上DQ通常通过一个排针引出,再配一个上拉电阻接到VCC,常见阻值在4.7kΩ左右。上拉电阻不是可有可无的,它是保证总线空闲时处于高电平的关键——没有它,复位脉冲后器件无法正确产生应答脉冲。
竞赛板上的接线一般已经固定,但你要学会看原理图。我提醒一句:开发板上的DS18B20接口如果周围有跳帽,务必检查跳帽是否插好。“接触不良导致温度恒85度”这个坑,我每年都能在答疑区看到。接线这种事,看似简单,实则是考场心态的试金石。
2.2 复位时序:开场白必须板正
单总线的所有通信都是从“复位脉冲”开始的。主机先把DQ拉低至少480μs(通常是480~960μs),然后释放总线。器件在检测到上升沿后的15~60μs内,会通过把总线拉低60~240μs的方式回应一个“存在脉冲”。主机就在释放总线后等待15~60μs,去读这个脉冲的有无,以此确认传感器在不在线。
这个时序里的每一个数字都有讲究。拉低时间太短,器件来不及识别;太长,可能把供电断掉;释放后读取过早,还没等到应答脉冲;读取过晚,应答脉冲已经结束。实践中最容易犯的毛病是“复位函数返回值给颠倒了”:有应答时DQ被拉低,也就是读到0,很多例程却返回1表示成功。代码是无所谓的,只要前后逻辑一致就行,但你要清楚自己在判断什么。
2.3 读时序与写时序:快与慢的分寸
读一位时序:主机拉低DQ约1~15μs,然后释放,释放后1~15μs内从DQ采样。关键是主机必须在释放后的短暂窗口内完成采样,否则器件已经准备好下一位的数据,你读到的就是错的。
写一位时序:主机拉低DQ后,如果要写0,就保持拉低60~120μs;如果要写1,则在拉低1~15μs后释放总线,让上拉电阻把它拉高。器件会在总线电平落下后的15~60μs内采样,所以写0的时间必须充足,写1之后也必须等够60μs的写时隙周期,再进入下一个位。
很多人看时序图看得一头雾水,我的类比是:读时序像“你去按门铃,铃响后侧耳听里面有没有动静”,动手要快,但等回复要有耐心;写时序像“铁路上扳道岔”,要扳到位、停留够时间,才能保证列车走对轨道。DS18B20的“道岔时间”就是60μs档位,你用delay函数粗算一下,宁可多别可少。
DS18B20常见时序参数对比如下,记牢这几个数,以后看任何例程都不会慌:
| 时序类型 | 关键时间参数 | 判断要点 |
|---|---|---|
| 复位 | 拉低480~960μs,释放后15~60μs内采样 | 应答低脉冲60~240μs |
| 写0 | 拉低60~120μs后释放 | 器件约15~60μs采样 |
| 写1 | 拉低1~15μs后释放 | 释放后由上拉变高 |
| 读位 | 拉低1~15μs后释放 | 释放后1~15μs内采样 |
3. 驱动代码逐段拆解:从复位到读出温度的完整链路
3.1 先解决微秒级延时:不同单片机内核的巨大差异
写驱动之前,第一件事是解决延时。这里必须停下来多讲两句:蓝桥杯用的竞赛板主控是某型号STC15系列,属于1T内核,机器周期等于时钟周期,而很多网上的老教程是给12T的传统51写的,机器周期是时钟周期的12倍。你把老教程的延时函数原封不动照抄过来,所有时序都会被放慢十几倍,结果就是“时而能读、时而读不出来”。
我的建议是:直接用STC-ISP软件里的软件延时计算器,选好你的主频和内核模式,生成几个固定时长的延时函数。竞赛板常用主频是24MHz或12MHz,下面这段是按24MHz生成的微秒级延时函数示例,实际使用时请按你的主频重新生成校准:
void Delay_Microseconds(unsigned int us) { while (us--) { _nop_(); // 高优先级:编译器不要优化掉 _nop_(); _nop_(); } } void Delay_Milliseconds(unsigned int ms) { unsigned int i, j; for (i = 0; i < ms; i++) { for (j = 0; j < 120; j++); } }严格说这个Delay_Microseconds不是精确实时函数,但用在DS18B20的60μs、480μs这种量级上,只要数量级对,稳定性是够的。如果你发现传感器时好时坏,优先怀疑延时不准确,不要先怀疑传感器坏了。
3.2 初始化、写字节、读字节:三个函数扛起全局
先把引脚定义放在头文件或者主文件顶部。竞赛板上DS18B20的数据脚要按你的原理图改,下面默认是P1^4口,准双向口模式下即可工作:
sbit DQ = P1^4; bit DS18B20_Reset(void) { bit presence; DQ = 0; Delay_Microseconds(600); // 拉低超过480us DQ = 1; Delay_Microseconds(60); // 释放后在15~60us时采样 presence = DQ; // 读到0说明器件应答 Delay_Microseconds(500); // 等待整个复位周期结束 return presence; // 返回0表示有器件,1表示无器件 }这里有一个很多人踩过的逻辑坑:返回值是0才表示“器件存在”,因为应答脉冲是低电平。有些例程为了直观,会写成return !presence;,这样调用时更方便。你只要统一习惯即可,我写代码时习惯直接用原始电平,然后在调用处判断。
写一个字节,DS18B20的协议是低位在前,所以要从最低位开始逐位发送:
void DS18B20_WriteByte(unsigned char dat) { unsigned char i; for (i = 0; i < 8; i++) { DQ = 0; _nop_(); _nop_(); if (dat & 0x01) // 写1:释放总线,由外部上拉拉高 DQ = 1; Delay_Microseconds(60); // 写0:保持低电平足够时间 DQ = 1; _nop_(); dat >>= 1; } }读一字节的函数需要格外小心。网上流传的写法里,很多是把读到的位直接往最高位拼,结果得到的是按位反转的数据。虽然有些例程也能阴差阳错地工作,但等你换成多传感器、自己处理温度字节时就会出问题。最稳的写法是按位放进对应的bit位置,完全对应LSB-first协议:
unsigned char DS18B20_ReadBit(void) { unsigned char dat; DQ = 0; _nop_(); _nop_(); DQ = 1; _nop_(); _nop_(); dat = DQ; // 释放后尽快采样 Delay_Microseconds(60); // 等读时隙结束 return dat; } unsigned char DS18B20_ReadByte(void) { unsigned char i, dat = 0; for (i = 0; i < 8; i++) { dat |= DS18B20_ReadBit() << i; // 第0位放bit0,第1位放bit1... } return dat; }这种按位拼装的写法看似绕了一点,但逻辑完全正确:DS18B20先发最低位,我们就把第一个读到的位放到dat的bit0,第二个放到bit1,最终得到的就是原本的字节值。很多同学抄网上的代码读出来温度总是“差一点”,往往就是位序处理错了。
3.3 温度转换与读取:两条命令+一次完整时序
DS18B20本身是“被动式”的,你不能直接读温度值,要先发转换命令让它把模拟量转成数字量,再发读暂存器命令把结果拿出来。命令就两条:0x44是启动温度转换,0xBE是读取暂存器内容。发送任何命令之前都要先做一次复位。
读温度的完整流程如下:
unsigned int DS18B20_ReadRaw(void) { unsigned char low, high; unsigned int raw; DS18B20_Reset(); DS18B20_WriteByte(0xCC); // 跳过ROM匹配 DS18B20_WriteByte(0x44); // 启动温度转换 Delay_Milliseconds(750); // 12位分辨率下最大转换时间约750ms DS18B20_Reset(); DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0xBE); // 读取暂存器 low = DS18B20_ReadByte(); // 温度低字节 high = DS18B20_ReadByte(); // 温度高字节 return ((unsigned int)high << 8) | low; }这里有一个非常典型的效率陷阱:如果你在每次循环里都先发0x44再等750ms再读,那么整个系统就会被卡死,数码管来不及刷新,按键响应也迟钝。竞赛现场的时间是差一秒都心疼的。正确做法是“后台转换”:上电后先发一次0x44,等待第一次转换完成,然后进入主循环,每次只发复位+0xBE读取上一次转换的结果,读完之后立刻再发0x44启动下一次转换。这样传感器在后台忙着转换,CPU只管干别的活,刷新频率能轻松做到1秒几次。
原始数据换算成温度值,需要处理小数和负数。DS18B20默认12位分辨率,每个LSB对应0.0625℃,温度寄存器是16位补码格式:常温下读取的原始数值,低12位是有效数据,高5位是符号扩展。具体换算代码如下:
float DS18B20_GetTemperature(void) { unsigned int raw; float temp; raw = DS18B20_ReadRaw(); if ((raw & 0xF800) == 0xF800) // 高5位全为1,表示负数 { raw = (~raw) + 1; // 补码转原码 temp = (float)raw * 0.0625; return -temp; } else { temp = (float)raw * 0.0625; return temp; } }测试时如果不想总盯着串口看,可以在代码里做一个简单整数版换算:把温度乘以16塞进一个int变量,显示的时候通过整数除法和取余拆出个位、小数位,避免在主循环里频繁使用浮点数。蓝桥杯赛题通常允许一定误差,事实证明整数运算在抗干扰和代码体积上都有优势。
4. 集成上板:数码管实时显示温度
4.1 显示器框架:定时中断扫描的思路
温度读出来了,下一步是让它“看得见”。蓝桥杯竞赛板的数码管通常是共阳极,位选由译码器控制,段选由锁存器分时送出。这里我不贴和具体电路绑死的代码,而是讲一个通用思路:用定时器产生1ms左右的中断,在中断里刷新某一位的段码和位码,多个位轮流点亮,利用人眼视觉暂留实现全屏显示。
主循环里就不需要关心数码管的扫描细节了,只需要在全局缓冲区里填写要显示的数字。这种“显示与逻辑分离”的架构是竞赛高分代码的标配,因为它天然避开了“按键扫描占用液晶显示时间”这类问题。以“显示XX.X ℃”为例,四位或六位数字的分配逻辑:
- 第1位:符号。温度为负时显示“-”,正数时可以不亮或者显示“0”。
- 第2位:十位。用
temp_int / 1000之类的整数组合拆分。 - 第3位:个位,后面带小数点。
- 第4位:小数位。
拆分整数的代码很基础,但要注意“负温度”的坑:如果你用unsigned int存温度,负值换算后是一个很大的正数,直接拆位会显示出一堆乱码。稳妥做法是先把温度判断为负后取绝对值,再拆位,最后单独设置符号位。
4.2 负温度与小数处理:别让显示逻辑拖后腿
DS18B20分辨率为0.0625℃,显示一位小数绰绰有余。但负温度在竞赛题里经常被忽略:题目要求“当温度低于零度时,数码管最高位显示负号”,很多同学在常温调试时根本没测过负值,一上真机就露馅。
我的经验处理流程:先从DS18B20_GetTemperature拿到float温度,再乘10转成“一位小数整数”,这样显示时拆位最容易。举个例子,温度是-12.5℃,乘以10就是-125,取绝对值125,拆出百位1、十位2、个位5,再把符号位点亮;如果温度是-3.25℃,四舍五入成-3.3℃,也就是-33,拆出十位3、个位3。这个转换逻辑单独写一个子函数,不要在中断里做浮点运算。
4.3 增加按键与阈值报警:把传感器变成一个小系统
会“读+显示”之后,DS18B20在蓝桥杯里的发挥空间就打开了。最常见的加分项是“阈值报警”:用独立按键进入设置模式,按键A切换“上限设置”“下限设置”“正常显示”三个状态,按键B和C调节数值,温度越界时蜂鸣器响。
报警逻辑写起来不复杂,核心是“主循环轮询”和“防抖”:
void Key_Scan(void) { if (KEY_SET == 0) { Delay_Milliseconds(10); if (KEY_SET == 0) { mode = (mode + 1) % 3; while (KEY_SET == 0); // 等待松手 } } if (mode == 1) // 上限设置 { if (KEY_UP == 0) { Delay_Milliseconds(10); if (KEY_UP == 0) threshold_high += 5; while (KEY_UP == 0); } if (KEY_DOWN == 0) { Delay_Milliseconds(10); if (KEY_DOWN == 0) threshold_high -= 5; while (KEY_DOWN == 0); } } // 下限设置逻辑类似,写成mode==2分支 } void Loop_Alarm(void) { if (mode == 0 && (current_temp >= threshold_high || current_temp <= threshold_low)) BEEP = 0; // 低电平触发蜂鸣器 else BEEP = 1; }注意按键扫描不要写得太长,否则会影响主循环里读取温度的次数。一个稳妥的方案是:按键扫描放在主循环开头,每次只处理一个按键事件;温度读取采用3.3节说的“后台转换”模式,两者互不阻塞。做到这一步,一个“温度检测报警系统”已经成型,这种完整度放到竞赛作品里,拿基础分以上的成绩没有问题。
5. 现场调试踩坑录:掉进这些坑再爬出来
5.1 温度恒85度:不是传感器坏了
DS18B20第一次上电时,如果直接发0xBE读取而不发0x44转换,暂存器里存的默认值就是0x0550,换算过来正好是85℃。所以一上电就显示85度,大概率是你漏发了启动转换命令,或者复位时序不对导致命令根本没发进去。
另一个隐含坑是:首次上电后芯片需要等待“上电复位”完成,有些竞赛板为了省电会把传感器电源接到某个可控引脚上,如果程序里没给这个引脚写高电平,传感器压根没供电,读出来的也是85度恒定值。排查时先看原理图,确认VCC是直连电源还是受控。
5.2 数码管一亮就乱跳:中断与时序打架
这是最隐蔽、最让人崩溃的问题。数码管用定时中断扫描时,定时器每秒中断几千次,每次中断都要执行一堆段码切换语句。如果此时主循环正在执行DS18B20的复位时序或读位时序,中断一来,DQ引脚的延时就被拉长几十微秒甚至上百微秒,单总线的严格时序直接被破坏。
解决思路有两条。第一条:在DS18B20的时序函数里临时关闭总中断:
void DS18B20_DelayAndRead(void) { EA = 0; // 执行复位/读写字节 DS18B20_Reset(); DS18B20_WriteByte(0xCC); ... EA = 1; }这种方法效率最高,代价是关中断期间数码管刷新会停顿。由于整体时间只有几毫秒,肉眼根本看不出来。第二条:把数码管刷新改成“只在一个位切换”的中断快速版本,把中断服务程序缩短到几十个机器周期,减小对主循环时序的冲击。竞赛现场如果时间紧张,我强烈建议先用第一条顶着,稳定优先。
5.3 温度乱跳/显示0:延时与上拉的连锁反应
温度来回跳、显示忽大忽小,常见原因有四个:一是读位采样太晚,读到的是相邻位之间的模糊状态;二是复位延时不足,命令偶尔丢失;三是DQ引脚配置成推挽输出,干扰了“释放总线”这个动作;四是漏了上拉电阻或上拉电阻阻值异常。
排查方法很简单但很管用:在复位函数里用一个LED映射返回值,正常时点灯,异常时关灯。如果LED状态稳定,说明复位OK;再往前排查读写时序。没有示波器的话,我一般建议用串口打印原始值,把DS18B20_ReadRaw返回的十六进制数据直接发到电脑,看它有没有规律:如果连续多次读出的原始数据只有少数几位在变化,说明通信本身是通的,问题在换算;如果每次读到的数据完全随机,那一定出在时序上,和传感器本身无关。
5.4 只有手头传感器但没示波器:用串口辅助判断
我再分享一个实战中非常管用的技巧:把DS18B20的读时序改成一个测试版函数,每次只读一个位,然后把这个位用串口发出去。正常工作时你应该能看到“10101010”这种规律的位流(1和0交替),如果看到的全是0或全是1,说明时序偏得厉害。这个方法不需要示波器和逻辑分析仪,只要一个串口助手就能定位绝大部分问题。
多传感器场景下,如果你需要挂两个DS18B20,就不能再用0xCC跳过ROM了,而要走0x33读ROM、0x55匹配ROM的流程。竞赛板上通常只焊了一个DS18B20,但有的题会要求你在同一个数据口上读多个传感器。这时候要理解“ROM匹配”的原理:先读每个传感器的64位序列号,存到数组里,之后发0x55+某个序列号,就能只跟那个传感器通信。这部分属于进阶内容,但确实在历年真题里出现过,不要把序列号硬编码到代码里,要从实际器件上读出来。
回到最初的问题:DS18B20到底值不值得花一整周去钻研?我的答案毋庸置疑。这个器件是少有的“协议完整、硬件简单、坑位密集”的学习样本,吃透它,你对微秒级时序的感觉、对中断与主循环协作的理解,都会上一个台阶。别人调一晚上还在85度的时候,你已经是那个能指出“不要急着怀疑传感器,先看看时序对不对”的人了。
最后再分享一个小习惯:每次写完驱动,我都会把所有延时函数打印出来贴在纸上,旁边标好“复位600us”“写0 60us”“读中等待60us”这些用途说明。上了考场,你会有一种手里握着地图的感觉——地图带得越熟,路就越不会走偏。祝所有备赛的朋友都能在现场少踩几个坑,多拿几分稳。