简介:这份课程设计文档面向电子信息工程等专业学生及51单片机初学者,围绕基于AT89C51与DS18B20的数字温度计展开,可用于课程设计参考、实验搭建与答辩准备。文档完整梳理设计目的、要求与实现方案,涵盖主控芯片、时钟与复位电路、温度传感电路、4位一体共阴LED显示电路及上下限温度报警电路,并给出系统框图与源程序要点,便于读者理解从温度采集、数据处理到显示报警的整体链路。压缩包共1个doc文件,约1.34MB,内容以课程设计报告和程序说明为主,结构较完整,适合对照开发板进行电路连接与功能调试。已有4053人学习下载,可作为DS18B20测温、数码管显示和蜂鸣器与红灯报警等知识点的实践参考资料。
1. 答辩前一晚只亮两位数码管,这套 51 单片机方案到底缺什么
课程设计最容易翻车的时刻不是焊板子,是上电那一刻:数码管只亮两位、蜂鸣器一直长鸣、温度停在 85.0 不动。这份「基于 51 单片机数字温度计设计」的 doc 配四个头文件,把 AT89C51 最小系统、DS18B20 单总线测温、4 位共阴数码管动态扫描、上下限报警与按键设置串成了一条完整链路。它比多数只画框图的课设文档实在,keyscan.h、display.h、ds18b20.h的模块划分已经接近可交付状态,但定时器初值、负温度换算、扫描延时这三处留了尾巴,直接编译烧录大概率跑不出报告里写的效果。适合正在做单片机课设、需要一份带真实电路参数和可编译源码参考的电子信息类学生,也适合手上有块 51 开发板、想拿单总线器件练手的人。
2. 系统拆解与选型:AT89C51 配 DS18B20 加 4 位共阴管怎么搭
这套方案的骨架非常经典:一颗 51 内核单片机做控制和运算,一颗单总线数字温度传感器做采集,一组 4 位一体共阴数码管做显示,外加蜂鸣器和 LED 两条报警支路。选型本身没有太多花样,真正决定成败的是每个环节的参数边界——晶振多少、上拉多大、位选能不能直接灌电流。下面按信号流向拆成三块讲。
2.1 主控与最小系统:12MHz 晶振、22pF 负载电容与 10K/10μF 复位
主控选 AT89C51,4KB 片内 Flash、128B RAM、32 根 IO、2 个 16 位定时器、5 个中断源,5V 供电。项目正文里提到过为什么不用 8031:8031 没有片内程序存储器,必须外扩 ROM,三总线一拉出来,板子面积和调试复杂度都翻倍,对一个只需要采一个温度、驱动四位数字的课题来说完全没必要。AT89C51 的两个 IO 口就够用——P0 出段码,P2 出位选,P1 留给蜂鸣器、LED 和按键,P3 甚至还能空出来。
时钟电路采用内部振荡方式,XTAL1 和 XTAL2 之间接 12MHz 晶振,两端各对地接一只 22pF 瓷片电容。这两个电容不是随便选的,它们和晶振一起构成皮尔斯振荡器,容值偏差过大会导致起振困难或者频率偏移。常见做法是 15pF 到 33pF 之间取值,22pF 是最通用的中间档,配 12MHz 晶振的负载电容规格正好。这里有个容易忽略的换算:51 的机器周期是 12 个时钟周期,12MHz 下正好 1μs,这意味着源码里所有软件延时函数的基准都是 1μs。如果换成 11.0592MHz(为了串口波特率整除),机器周期变成 1.085μs,所有ds18b20_delayus()和display_delay()都会偏长约 8.5%,单总线时序可能直接失效。数字温度计不跑串口,12MHz 是更划算的选择。
复位电路是上电复位加按键复位。10K 电阻和 10μF 电解电容串联,电容正极接 VCC,负极接 RST 和电阻。上电瞬间电容相当于短路,RST 被拉到高电平,随着电容充电,RST 电压按 τ=RC=100ms 的时间常数下降,远大于 51 要求的两个机器周期复位脉宽。这里用 10μF 而不是 1μF 是有讲究的:容量太小,电源爬升慢的板子可能复位不干净;容量太大,按键复位松开后要等很久才能重新运行。
| 环节 | 器件/参数 | 关键约束 |
|---|---|---|
| 主控 | AT89C51,5V 供电 | 需外接复位与时钟,片内无 EEPROM |
| 晶振 | 12MHz + 2×22pF | 机器周期 1μs,决定所有软件延时 |
| 复位 | 10KΩ + 10μF,τ=100ms | 上电与按键复位共用 RC |
| 位选 | P2.4~P2.7 | 低电平选通,每根线最多灌 20mA |
| 段选 | P0 口 | 必须外接 10K 排阻上拉 |
| 温度 | DS18B20,DQ 接 P2.3 | 单总线,需 4.7K 上拉 |
| 报警 | 蜂鸣器 P1.0,红灯 P1.1 | 低电平有效,经三极管驱动 |
2.2 单总线测温链路:DS18B20 的分辨率、转换时间与上拉电阻
DS18B20 是这套设计里唯一有"协议"的器件,也是坑最集中的地方。它的核心参数如下:测温范围 -55℃ 到 +125℃,在 -10℃ 到 +85℃ 区间精度 ±0.5℃;分辨率可编程为 9 到 12 位,对应 0.5℃、0.25℃、0.125℃、0.0625℃ 四个步进;12 位分辨率下单次转换需要 750ms,9 位只要 93.75ms。片内结构分四块:64 位光刻 ROM(存唯一 ID)、温度传感器、非易失的 TH/TL 报警触发器、配置寄存器。TH、TL 和配置寄存器落在 EEPROM 里,掉电不丢,这正是课程设计里"上下限设置"能被保存下来的物理基础。
对单器件系统,DQ 一根线就够,但有两个硬件前提必须满足。第一是 4.7K 上拉电阻,单总线是开漏结构,没有上拉就没有高电平,读回来全是 0x00 或者 0xFF。开发板上如果 DS18B20 是插在独立模块上的,模块自带上拉;如果是自己焊的裸管,一定要补。第二是供电方式,DS18B20 支持寄生电源(VDD 接地,从 DQ 偷电)和外部电源(VDD 接 5V)两种。寄生电源在单器件、短连线、转换期间总线被强上拉的条件下能工作,但一旦总线空闲或者挂多个器件,转换电流供不上,读数会崩。我一般直接用外部供电,三根线 VDD、GND、DQ 全接,省心。
时序上,一次完整的温度读取要走五步:复位 → 跳过 ROM(0xCC)→ 启动转换(0x44)→ 等转换完成 → 再复位 → 跳过 ROM → 读暂存器(0xBE)→ 读两个字节。每一步的时间窗口都在百微秒级,靠软件延时凑。项目正文里给的ds18b20_init()就是标准的复位流程:主机拉低 DQ 至少 480μs,释放后等 15 到 60μs,DS18B20 会主动拉低 60 到 240μs 作为存在脉冲。代码里ds18b20_delayus(80)和ds18b20_delayus(150)这两个魔数就是往这个窗口里塞的,但while(t--)在 12MHz 下每次循环到底几个机器周期,取决于编译器优化等级,换个 Keil 版本就可能跑飞。这是纯软件延时的通病,后面第 4 章会讲怎么用示波器校。
2.3 显示与报警通路:位选驱动能力、段码表和蜂鸣器三极管
显示用 4 位一体共阴数码管。共阴的意思是 8 个 LED 的阴极连在一起做公共端,段引脚给高电平才点亮,公共端给低电平才选通。所以 P0 输出段码(高有效),P2.4 到 P2.7 输出位选(低有效)。源码里wei1=0开第一位、wei1=1关第一位,就是这个逻辑。
这里有两个硬件坑必须提前说清楚。第一个是 P0 口内部没有上拉电阻,做通用 IO 输出高电平时驱动能力极弱,必须外接 10K 排阻到 VCC,否则数码管亮度暗得几乎看不见。第二个是位选的灌电流,一位数码管 8 段全亮时,假设每段 5mA,公共端要灌 40mA,而 51 单个引脚的最大灌电流只有 20mA,直接接会烧口或者亮度严重不足。常见做法是加 8550 PNP 三极管做位驱动,基极串 1K 电阻接 P2.4~P2.7,或者用 ULN2003 达林顿阵列。开发板上如果已经有 74HC245 或者三极管阵列,这部分可以省掉。
段码表在display.h里定义了三组,值得逐个核对。temperature1[]是不带小数点的 0 到 9,temperature2[]是带小数点的 0. 到 9.,temperature3[]是特殊符号:不显示、单独小数点、负号、字母 H、字母 L。段序是 dp-g-f-e-d-c-b-a,共阴高有效。
| 字符 | 段码 | 二进制(dp g f e d c b a) |
|---|---|---|
| 0 | 0x3F | 0011 1111 |
| 1 | 0x06 | 0000 0110 |
| 2 | 0x5B | 0101 1011 |
| 8 | 0x7F | 0111 1111 |
| 0. | 0xBF | 1011 1111 |
| - | 0x40 | 0100 0000 |
| H | 0x76 | 0111 0110 |
| L | 0x38 | 0011 1000 |
| 全灭 | 0x00 | 0000 0000 |
报警通路比较简单:蜂鸣器接 P1.0,红灯接 P1.1,都是低电平有效。源码里beer=1关、beer=0开,说明蜂鸣器是有源型(给电平就响),中间应该有一级 PNP 三极管做电流放大,因为 51 的 IO 拉电流能力推不动蜂鸣器。红灯串 1K 限流电阻直接接 P1.1 即可。项目要求里还提到"上下限警报温度设置电路",实际就是 K1 到 K4 四个独立按键,接在 P3 口或者 P1 剩余引脚上,由keyscan.h处理。
3. 把源码跑起来:四个文件的分工与烧录前的自检
拿到这份资源,第一件事不是急着编译,而是先把文件和引脚的对应关系理清楚。源码被拆成了一个主文件加三个头文件,这种拆法在 51 项目里不算规范(头文件里定义变量会导致重复定义风险),但对课设规模来说够用。下面按"看清楚 → 改对 → 烧进去"三步走。
3.1 四个文件的职责边界与引脚映射
主文件里只有main()和两个全局变量max、min,加上两个标志位s和s1。ds18b20.h负责单总线时序、温度换算和 EEPROM 写入;display.h负责段码表、位选扫描、定时器和开机动画;keyscan.h负责四个按键的扫描与模式切换。
| 文件 | 核心函数 | 全局变量/引脚 |
|---|---|---|
| 主文件 | main() | max、min、s、s1 |
| ds18b20.h | ds18b20_initds18b20_readds18b20_writeget_temperaturestore_talarm | DQ=P2^3、beer=P1^0、led=P1^1、temp、temp_d、f、f_max、f_min、w |
| display.h | displaydisplay1timer1_inittimer1 | wei1~wei4=P2^4~P2^7、temperature1/2/3[] |
| keyscan.h | keyscan() | K1~K4 对应引脚 |
有个细节要先指出:display.h里把timer1_init和中断服务函数都写进去了,但timer1_init(0)在主函数开头被调用,中断函数void timer1() interrupt 3却是定时器 1 的中断向量。而函数体里写的是TMOD=0x10; TH0=0x3c; TL0=0xb0;——配置的是定时器 1 为方式 1,装的却是定时器 0 的初值。这是个货真价实的 bug,第 5 章会专门拆。
3.2 主循环的四步顺序:为什么 keyscan 必须排在 get_temperature 前面
主函数的结构非常短,但顺序有讲究:
void main() { beer = 1; // 上电先关蜂鸣器,避免误鸣 led = 1; // 上电先关红灯 timer1_init(0); // 初始化定时器,参数 0 表示暂不启动 get_temperature(1); // 首次读取:同时把 EEPROM 里的 TH/TL 读进 max/min while(1) { keyscan(); // 第一步:先处理按键,更新模式和上下限 get_temperature(0); // 第二步:读当前温度到 temp/temp_d display(temp, temp_d*0.625); // 第三步:刷新四位显示 alarm(); // 第四步:比较阈值,决定是否报警 } }逻辑说明:keyscan()排在最前面,是因为它可能修改max、min或者显示模式标志a,后面的display()要用到最新值;如果放在温度读取之后,按键响应会延迟一个完整循环,12 位转换 750ms 的节奏下,按一下要等将近一秒才有反应,手感很差。get_temperature(1)那个参数是"首次启动"标志,为 1 时会调用display1(1)播一段开机动画来消耗时间,同时把 EEPROM 里的 TH/TL 读出来存进max、min。参数说明:temp是整数部分,temp_d是原始小数部分(0 到 15),乘 0.625 之后才是显示用的十分位数字,原因在 4.2 节展开。
3.3 编译、烧录与上电自检的三条判据
编译用 Keil C51,新建工程选 AT89C51 器件,把主文件和三个头文件放同一目录,输出勾选生成 HEX。烧录用对应的下载器,烧录前确认芯片型号和晶振频率设置一致。
上电自检按三条判据走,能快速定位问题出在哪一层:
第一,看开机动画。display1(1)会让四位数码管从左到右依次显示"-",如果四段都能亮且顺序正确,说明段码表和位选扫描都没问题,问题只可能在温度链路。如果只有一两位亮,回头查位选三极管和 P2 口的接线。
第二,看初始显示值。刚上电时温度还没转换完,temp和temp_d都是 0,正常应该显示 0.0 或者按select_1、select_2的消除前导零逻辑显示。如果显示 85.0,说明读到了 DS18B20 的上电默认值,转换根本没执行完,跳去 5.2 节。如果显示 H 或者 L,说明a标志被置成了 1 或 2,按键扫描误触发了。
第三,按 K2 和 K3 看上下限。按一下 K2 应该显示上限约 1 秒然后自动退出,按 K3 看下限。如果按下去没反应,查按键引脚和keyscan.h里的端口定义;如果显示的是 0,说明 EEPROM 里从来没写过,max和min还是初值 0x00。
提示:烧录前先把
max和min的初值临时改成 30 和 10,能省掉第一次上电就被报警声吓一跳的麻烦。
4. 代码级拆解:单总线时序、16 位温度换算与动态扫描
这部分是整份资源的技术含量所在。三个模块各有各的时间尺度:单总线是微秒级,温度换算是位运算,动态扫描是毫秒级,混在一起调试时很容易顾此失彼。
4.1 复位、写、读三段时序的时间窗口
单总线协议的全部内容就是"什么时候拉低、拉低多久、什么时候采样"。DS18B20 的数据手册给了每个窗口的上下界,代码要做的是用软件延时把动作塞进窗口中间。
/* 复位:主机拉低 >=480us,释放后等存在脉冲 */ void ds18b20_init() { DQ = 1; DQ = 0; // 主机拉低总线 ds18b20_delayus(80); // 保持低电平,实际需 >=480us,见下方说明 DQ = 1; // 释放总线,由上拉电阻拉高 while(DQ); // 等待 DS18B20 拉低(存在脉冲) ds18b20_delayus(150); // 存在脉冲结束后等待总线恢复 DQ = 1; } /* 写一位:拉低 1~15us 表示写 1,拉低 60~120us 表示写 0 */ void ds18b20_write(uchar d) { uchar i; for(i = 8; i > 0; i--) { DQ = 0; // 每个时隙都以拉低开始 DQ = d & 0x01; // 写 1 则立即释放,写 0 则继续拉低 ds18b20_delayus(5); // 保持时间,写 0 时应更长 DQ = 1; // 释放总线,至少 1us 恢复 d >>= 1; // 低位先发 } } /* 读一位:主机拉低 >=1us 后释放,15us 内采样 */ uchar ds18b20_read() { uchar i, d = 0; DQ = 1; for(i = 8; i > 0; i--) { d >>= 1; DQ = 0; // 启动读时隙 _nop_(); _nop_(); // 拉低约 2us 后释放 DQ = 1; if(DQ) d |= 0x80; // 15us 内采样,高电平则为 1 ds18b20_delayus(10); // 补足时隙长度 } return d; }逻辑说明:这三段代码的骨架是对的,问题在延时数值的可移植性。ds18b20_delayus(80)的注释写"延时 15-80μs",但复位要求是拉低至少 480μs,80 次while(t--)循环在 12MHz 下大概 600 到 800μs,勉强够;换个优化等级或者换 11.0592MHz 晶振,就可能掉到 480μs 以下,DS18B20 直接不应答。ds18b20_write里写 0 和写 1 用的是同一个delayus(5),写 0 需要拉低 60μs 以上,5 次循环大约 40μs,处于临界。这些数值的正确做法是用逻辑分析仪或者示波器抓 DQ 波形,量出实际拉低时间,再反推循环次数。
参数说明:ds18b20_write的形参d是待发送的一个字节,函数内部从 bit0 开始逐位右移,符合单总线"低位先发"的规定;ds18b20_read的返回值是读到的字节,d >>= 1放在采样前,配合d |= 0x80实现低位先入。
还有一条不能省的:读写期间必须关总中断。51 的定时器中断一旦插进来,单总线时隙就被拉长到毫秒级,DS18B20 会认为通信超时。稳妥的写法是在ds18b20_read和ds18b20_write外面包一层EA=0; ... EA=1;,这点在原始代码里没有,是实机上偶尔读到错值的主要原因。
4.2 从 16 位原始码到「25.6」:符号位、整数位与小数位
DS18B20 的暂存器第 0、1 字节是温度值,低字节在前,高字节在后,组成一个 16 位有符号数。12 位分辨率下,低 4 位是小数部分,高 12 位是整数部分(含符号位),1 个 LSB 等于 0.0625℃。
void get_temperature(bit flag) { uchar a = 0, b = 0; uint i; ds18b20_init(); ds18b20_write(0xCC); // 跳过 ROM,单器件专用 ds18b20_write(0x44); // 启动温度转换 if(flag == 1) display1(1); // 首次启动用开机动画消耗转换时间 else ds18b20_delayms(1); // 非首次只延时 1ms —— 这里是个坑 ds18b20_init(); ds18b20_write(0xCC); ds18b20_write(0xBE); // 读暂存器 a = ds18b20_read(); // 低字节 LSB b = ds18b20_read(); // 高字节 MSB if(flag == 1) // 首次读取时顺带把 TH/TL 读出来 { max = ds18b20_read(); min = ds18b20_read(); } /* 符号位判断:高字节最高位为 1 表示负温度 */ if((max & 0x80) == 0x80) { f_max = 1; max = max - 0x80; } if((min & 0x80) == 0x80) { f_min = 1; min = min - 0x80; } i = b; i >>= 4; if(i == 0) // 正温度 { f = 0; temp = ((a >> 4) | (b << 4)); // 整数部分 temp_d = a & 0x0F; // 小数部分(原始 4 位) } else // 负温度 { f = 1; a = ~a + 1; // 只对低字节取反加 1 b = ~b; // 高字节单独取反,没有处理进位 temp = ((a >> 4) | (b << 4)); temp_d = a & 0x0F; } }逻辑说明:正温度分支的位拼接是标准做法。以 25.0625℃ 为例,原始码 0x0191,a=0x91,b=0x01,a>>4得 0x09,b<<4得 0x10,或起来是 0x19 即 25,a & 0x0F得 1,对应 1×0.0625=0.0625℃。显示层把temp_d乘 0.625 得到 0.625,转成uchar后截断为 0,最终显示 25.0——注意这里丢掉了 0.0625,是截断不是四舍五入。
负温度分支就是那个经典 bug:a = ~a + 1; b = ~b;只对低字节做了取反加 1,高字节只是取反,当低字节为 0x00 时取反加 1 会溢出成 0x00 并向高位进位,而代码里没有把这个进位加到 b 上。正确写法是把 a 和 b 拼成 16 位再整体取反加 1。以 -0.5℃ 为例,原始码 0xFFF8,a=0xF8,b=0xFF,按代码算:a=0x08,b=0x00,temp=(0x08>>4)|(0x00<<4)=0,temp_d=8,显示 0.5 而不是 -0.5,连符号都丢了。这个坑在课设验收时如果只测室温根本发现不了,一放冰水就露馅。
参数说明:flag为 1 表示首次启动,会额外读 TH/TL 并播放开机动画;为 0 表示常规读取。temp存整数部分(0 到 125 或 0 到 55),temp_d存 0 到 15 的原始小数码,f、f_max、f_min三个标志位分别标记测量值、上限、下限的正负。
4.3 4 位动态扫描:位选、消隐与小数点负号的位置
四位一体数码管是分时复用的:任意时刻只有一位亮,靠刷新速度骗过人眼。display()里用 for 循环依次选通第 1 到第 4 位,每一位根据当前显示模式a决定输出什么段码。
void display(uchar t, uchar t_d) { uchar i; for(i = 0; i < 4; i++) { switch(i) { case 0: // 第一位:百位或负号 wei2 = 1; wei3 = 1; wei4 = 1; wei1 = 0; // 先关其余三位再开本位 if(a == 0) select_1(f, t); // 测温度模式 if(a == 1) P0 = temperature3[3]; // 显示 'H' if(a == 2) P0 = temperature3[4]; // 显示 'L' break; case 1: // 第二位:十位 wei1 = 1; wei3 = 1; wei4 = 1; wei2 = 0; if(a == 0) select_2(f, t); break; case 2: // 第三位:个位,带小数点 wei1 = 1; wei2 = 1; wei4 = 1; wei3 = 0; if(a == 0) P0 = temperature2[t % 10]; break; case 3: // 第四位:十分位 wei1 = 1; wei2 = 1; wei3 = 1; wei4 = 0; if(a == 0) P0 = temperature1[t_d]; break; } display_delay(10); // 每一位停留约 10ms —— 太长了 } }逻辑说明:消隐顺序很关键——必须先关掉其余三位再开当前位,否则会出现"鬼影",也就是上一位的段码残留在当前位上。代码里的写法是先wei2=1; wei3=1; wei4=1;再wei1=0,顺序正确。
真正的问题在display_delay(10)。这个函数内层是for(j=120;j>0;j--),12MHz 下大约 1ms,外层 10 次就是 10ms,四位扫完一轮 40ms,刷新率只有 25Hz,低于人眼 50Hz 的临界闪烁频率,肉眼能看出明显抖动。而且主循环里还有keyscan()、get_temperature()、alarm()三段耗时,实际周期更长。常见做法是把每位停留压到 1 到 3ms,整轮 4 到 12ms,刷新率 83 到 250Hz,或者干脆把扫描搬进定时器中断,主循环只负责更新显示缓冲区。
小数点位置也值得说一下。第三位用的是temperature2[]表,这个表里每个段码都置了 dp 位,所以小数点固定亮在个位后面,形成"25.6"的格式。负号显示在第一位,由select_1里的temperature3[2]输出,条件是k % 100 / 10 != 0,也就是只有当十位数不为 0 时才显示负号,避免出现"-0.5"这种把负号放在百位上的错位。这套逻辑对 -10℃ 以下的温度是对的,对 -0.5℃ 就失效了——因为第一位和第二位都被select_1、select_2的消除前导零逻辑清空了,负号无处可放。这是课程设计里负温度显示普遍处理不好的地方,改进办法是在f==1时强制第一位输出负号,不参与消除前导零。
5. 避坑与排查:五个把课程设计卡在验收前夜的现场问题
这一节全部来自实板调试时最常撞上的现象。每一条按"现象 → 原因 → 解决"写,能对号入座就别绕弯。
5.1 上电蜂鸣器长鸣不止,红灯常亮
现象:烧录完一上电,蜂鸣器立刻响,红灯亮,数码管显示的温度数字来回跳。
原因:max和min这两个全局变量初值是 0x00,首次调用get_temperature(1)时从 DS18B20 的 EEPROM 里读 TH/TL。如果这颗片子从来没被写过,TH 和 TL 的出厂值就是 0。于是上限 0、下限 0,当前室温 25℃ 立刻判定超限,w置 1,定时器启动,蜂鸣器和红灯同时动作。
解决:在main()里get_temperature(1)之后加一段判空,如果max == 0 && min == 0就写一组合理默认值并调store_t()存进 EEPROM:
get_temperature(1); if(max == 0x00 && min == 0x00) // 从未写过阈值 { max = 30; // 默认上限 30℃ min = 10; // 默认下限 10℃ store_t(); // 写入 EEPROM,掉电不丢 }顺带提一句,store_t()里的流程是先发 0x4E 写暂存器,再发 0x48 把暂存器拷进 EEPROM,中间不能再插别的命令,否则拷贝的是旧值。
5.2 温度永远显示 85.0 不动
现象:数码管稳定显示 85.0,用热源烤传感器也不变。
原因:85.0 是 DS18B20 上电复位后的默认温度寄存器值。代码里启动转换0x44之后,非首次读取只延时ds18b20_delayms(1),也就是 1ms,而 12 位分辨率的转换需要 750ms。1ms 后去读暂存器,转换还没完成,读回来的就是默认值 85。首次启动之所以正常,是因为display1(1)播动画耗掉了几百毫秒,凑巧覆盖了转换时间。
解决:把非首次的延时改成 750ms 以上,或者改成读忙等待。前者简单粗暴但会让整个系统响应变慢,750ms 才刷新一次;后者更优雅,但 DS18B20 的单总线不支持"读忙"查询,只能定时。如果对刷新率有要求,可以把分辨率降到 9 位(93.75ms)或者 10 位(187.5ms),改配置寄存器即可:
/* 把分辨率设为 10 位:转换时间 187.5ms,步进 0.25℃ */ ds18b20_init(); ds18b20_write(0xCC); ds18b20_write(0x4E); // 写暂存器 ds18b20_write(max); // TH ds18b20_write(min); // TL ds18b20_write(0x3F); // 配置寄存器:10 位分辨率 ds18b20_init(); ds18b20_write(0xCC); ds18b20_write(0x48); // 拷入 EEPROM配置寄存器的 bit6 和 bit5 决定分辨率:00 是 9 位,01 是 10 位,10 是 11 位,11 是 12 位,其余位固定。0x3F 就是 10 位配置。
5.3 数码管亮度不均、边缘两位明显变暗
现象:四位数码管亮度不一致,第一第四位比中间两位暗,或者整屏肉眼可见地抖。
原因:就是 4.3 节说的display_delay(10)。每一位停留 10ms,整轮 40ms,刷新率 25Hz。同时主循环里keyscan()和alarm()的耗时波动会让每一轮的周期不固定,占空比忽大忽小,表现出来就是亮度不均加闪烁。
解决:把display_delay的参数从 10 改到 2 或 3,每位 2 到 3ms,整轮 8 到 12ms,刷新率上百赫兹,肉眼基本看不出闪。如果改完发现亮度整体偏暗,说明位选三极管的基极电阻偏大,把 1K 降到 470Ω 试试。更彻底的做法是把扫描搬进定时器 0 中断,每 2ms 进一次中断刷新一位,主循环只维护一个 4 字节的显示缓冲区。
5.4 报警时蜂鸣器不按间歇节奏响,或者压根不响
现象:温度超限后蜂鸣器长鸣不停,不是"响 200ms 停 800ms"的间歇节奏;或者超限了蜂鸣器一点反应没有。
原因:定时器配置错配。timer1_init里写的是TMOD=0x10(定时器 1 方式 1),但装初值用的是TH0和TL0(定时器 0 的寄存器),中断函数写的是interrupt 3(定时器 1 的向量),启动用的是TR1。结果就是定时器 1 从 0x0000 开始计数,12MHz 下 65536μs 溢出一次,而不是预期的 50ms;TH0/TL0的赋值完全没起作用。num累加到 5 的实际时间是 327ms 而不是 200ms,累加到 20 是 1.31s 而不是 1s,节奏全乱。更糟的是如果某个分支里TR1没被置 1,中断永远不触发,beer就一直是 1,蜂鸣器不响。
解决:统一成定时器 0,三处一起改:
void timer0_init(bit t) { TMOD = 0x01; // 定时器 0,方式 1(16 位) TH0 = 0x3C; // 12MHz 下 50ms 初值:65536 - 50000 = 15536 = 0x3CB0 TL0 = 0xB0; EA = 1; ET0 = 1; // 开定时器 0 中断 TR0 = t; } void timer0_isr() interrupt 1 // 定时器 0 的向量号是 1 { TH0 = 0x3C; TL0 = 0xB0; /* 其余逻辑不变 */ }参数说明:50ms 的初值算法是 65536 - (50000μs / 1μs) = 15536,转十六进制 0x3CB0,高字节装 TH0,低字节装 TL0。要改成 20ms 就是 65536 - 20000 = 45536 = 0xB1E0。
5.5 负温度显示乱码或者符号丢失
现象:把传感器放进冰水混合物,本该显示 -0.5 左右,实际显示 0.5 或者 15.5 或者一串乱码。
原因:4.2 节拆过的取反加 1 问题,只对低字节做补码运算,高字节漏了进位。另外select_1和select_2的消除前导零逻辑在f==1且整数部分小于 10 时,会把第一第二位都清空,负号没有位置显示。
解决:换算部分改成先拼 16 位再整体取反加 1:
uint raw = ((uint)b << 8) | a; // 拼成 16 位原始码 if(raw & 0x8000) // 最高位为 1,负温度 { f = 1; raw = (~raw) + 1; // 整体取反加 1,进位自动传播 } temp = raw >> 4; // 整数部分 temp_d = raw & 0x0F; // 小数部分显示部分则要在f==1时强制第一位输出负号,跳过消除前导零的判断。这两处改完,-0.5℃ 才能正确显示成-0.5。
6. 从能跑到能交:报警回差、掉电保存与一张验收自测表
跑通只是及格线,要让这套课设经得起老师追问,还得补三个细节。
第一个是报警回差。现在的alarm()是硬比较,temp <= min || temp >= max就报警。室温在阈值附近波动时,比如设定上限 30℃、实测 29.9 和 30.1 来回跳,蜂鸣器会"哒哒哒"地反复启停,听着像接触不良。工程上的做法是加回差:超过上限 0.5℃ 才置位,低于上限 0.3℃ 才复位。整数比较实现不了 0.5℃,得把温度放大 10 倍再比——temp*10 + temp_d*0.625得到一个 0.1℃ 精度的整数,阈值也放大 10 倍:
int t10 = temp * 10 + (int)(temp_d * 0.625); // 当前温度 ×10 int hi = max * 10; int lo = min * 10; if(t10 >= hi + 5) w = 1; // 超上限 0.5℃ 才报警 if(t10 <= hi - 3) w = 0; // 回落到上限下 0.3℃ 才解除第二个是掉电保存的完整闭环。store_t()已经实现了写暂存器和拷 EEPROM,但原始代码里没有在按键退出调节模式时调用它。结果是每次调完上下限,一断电就回到出厂值。正确的挂载点是keyscan()里确认退出的那个分支,退出前调一次store_t()。另外要提醒的是 DS18B20 的 EEPROM 擦写寿命约 5 万次,按键长按连加的时候不要每加一次就写一次,等松手确认再写。
第三个是分辨率与显示位数的匹配。4 位数码管显示"-55.0"要占满四位,显示"125.0"要五位,装不下。这套设计的上限实际是 99.9℃,超过就溢出。如果测试场景可能到 100℃ 以上,要么改成三位整数加一位小数用满四位并放弃负号,要么换 5 位数码管。课设验收一般只测常温和冰水,问题不大,但被问到要能答上来。
验收前的自测我习惯按这张表走一遍,每一项都对应前面某一节:
| 自测项 | 操作 | 期望结果 | 失败时回查 |
|---|---|---|---|
| 开机动画 | 上电 | 四位依次显示- | 3.3 / 2.3 位选驱动 |
| 常温显示 | 静置 | 显示室温,误差 ≤0.5℃ | 5.2 转换时间 |
| 冰水测试 | 探头入冰水 | 显示接近 0.0,负温度带符号 | 5.5 补码与负号 |
| 上限报警 | K1 调到低于室温 | 蜂鸣器间歇响、红灯亮 | 5.4 定时器配置 |
| 下限报警 | K1 调到高于室温 | 同上 | 5.1 初值判断 |
| 掉电保存 | 调完阈值断电重上 | 阈值保持不变 | 6.2 store_t 挂载点 |
那张表我后来贴在了工作台前面。刚接触单总线那会儿,我图省事没接上拉电阻,读回来全是 0xFF,对着代码查了一整晚时序,最后拿万用表量到 DQ 悬空才反应过来——从那以后每次焊完板子,我都要先量一遍 DQ 对 VCC 的阻值,确认 4.7K 在那儿,再上电。这套 51 单片机温度计的 doc 和四个头文件在同名资源包里,电路图和段码表都是现成的,照着上面几节的口径把定时器、负温度和扫描延时改一遍,基本就能一次点亮。希望帮到你。
本文还有配套的精品资源,点击获取