简介这份课程设计文档面向电子信息工程等专业学生及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 复位主控选 AT89C514KB 片内 Flash、128B RAM、32 根 IO、2 个 16 位定时器、5 个中断源5V 供电。项目正文里提到过为什么不用 80318031 没有片内程序存储器必须外扩 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 电压按 τRC100ms 的时间常数下降远大于 51 要求的两个机器周期复位脉宽。这里用 10μF 而不是 1μF 是有讲究的容量太小电源爬升慢的板子可能复位不干净容量太大按键复位松开后要等很久才能重新运行。环节器件/参数关键约束主控AT89C515V 供电需外接复位与时钟片内无 EEPROM晶振12MHz 2×22pF机器周期 1μs决定所有软件延时复位10KΩ 10μFτ100ms上电与按键复位共用 RC位选P2.4~P2.7低电平选通每根线最多灌 20mA段选P0 口必须外接 10K 排阻上拉温度DS18B20DQ 接 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 位分辨率下单次转换需要 750ms9 位只要 93.75ms。片内结构分四块64 位光刻 ROM存唯一 ID、温度传感器、非易失的 TH/TL 报警触发器、配置寄存器。TH、TL 和配置寄存器落在 EEPROM 里掉电不丢这正是课程设计里上下限设置能被保存下来的物理基础。对单器件系统DQ 一根线就够但有两个硬件前提必须满足。第一是 4.7K 上拉电阻单总线是开漏结构没有上拉就没有高电平读回来全是 0x00 或者 0xFF。开发板上如果 DS18B20 是插在独立模块上的模块自带上拉如果是自己焊的裸管一定要补。第二是供电方式DS18B20 支持寄生电源VDD 接地从 DQ 偷电和外部电源VDD 接 5V两种。寄生电源在单器件、短连线、转换期间总线被强上拉的条件下能工作但一旦总线空闲或者挂多个器件转换电流供不上读数会崩。我一般直接用外部供电三根线 VDD、GND、DQ 全接省心。时序上一次完整的温度读取要走五步复位 → 跳过 ROM0xCC→ 启动转换0x44→ 等转换完成 → 再复位 → 跳过 ROM → 读暂存器0xBE→ 读两个字节。每一步的时间窗口都在百微秒级靠软件延时凑。项目正文里给的ds18b20_init()就是标准的复位流程主机拉低 DQ 至少 480μs释放后等 15 到 60μsDS18B20 会主动拉低 60 到 240μs 作为存在脉冲。代码里ds18b20_delayus(80)和ds18b20_delayus(150)这两个魔数就是往这个窗口里塞的但while(t--)在 12MHz 下每次循环到底几个机器周期取决于编译器优化等级换个 Keil 版本就可能跑飞。这是纯软件延时的通病后面第 4 章会讲怎么用示波器校。2.3 显示与报警通路位选驱动能力、段码表和蜂鸣器三极管显示用 4 位一体共阴数码管。共阴的意思是 8 个 LED 的阴极连在一起做公共端段引脚给高电平才点亮公共端给低电平才选通。所以 P0 输出段码高有效P2.4 到 P2.7 输出位选低有效。源码里wei10开第一位、wei11关第一位就是这个逻辑。这里有两个硬件坑必须提前说清楚。第一个是 P0 口内部没有上拉电阻做通用 IO 输出高电平时驱动能力极弱必须外接 10K 排阻到 VCC否则数码管亮度暗得几乎看不见。第二个是位选的灌电流一位数码管 8 段全亮时假设每段 5mA公共端要灌 40mA而 51 单个引脚的最大灌电流只有 20mA直接接会烧口或者亮度严重不足。常见做法是加 8550 PNP 三极管做位驱动基极串 1K 电阻接 P2.4~P2.7或者用 ULN2003 达林顿阵列。开发板上如果已经有 74HC245 或者三极管阵列这部分可以省掉。段码表在display.h里定义了三组值得逐个核对。temperature1[]是不带小数点的 0 到 9temperature2[]是带小数点的 0. 到 9.temperature3[]是特殊符号不显示、单独小数点、负号、字母 H、字母 L。段序是 dp-g-f-e-d-c-b-a共阴高有效。字符段码二进制dp g f e d c b a00x3F0011 111110x060000 011020x5B0101 101180x7F0111 11110.0xBF1011 1111-0x400100 0000H0x760111 0110L0x380011 1000全灭0x000000 0000报警通路比较简单蜂鸣器接 P1.0红灯接 P1.1都是低电平有效。源码里beer1关、beer0开说明蜂鸣器是有源型给电平就响中间应该有一级 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、s1ds18b20.hds18b20_initds18b20_readds18b20_writeget_temperaturestore_talarmDQP2^3、beerP1^0、ledP1^1、temp、temp_d、f、f_max、f_min、wdisplay.hdisplaydisplay1timer1_inittimer1wei1~wei4P2^4~P2^7、temperature1/2/3[]keyscan.hkeyscan()K1~K4 对应引脚有个细节要先指出display.h里把timer1_init和中断服务函数都写进去了但timer1_init(0)在主函数开头被调用中断函数void timer1() interrupt 3却是定时器 1 的中断向量。而函数体里写的是TMOD0x10; TH00x3c; TL00xb0;——配置的是定时器 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μs80 次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外面包一层EA0; ... EA1;这点在原始代码里没有是实机上偶尔读到错值的主要原因。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℃ 为例原始码 0x0191a0x91b0x01a4得 0x09b4得 0x10或起来是 0x19 即 25a 0x0F得 1对应 1×0.06250.0625℃。显示层把temp_d乘 0.625 得到 0.625转成uchar后截断为 0最终显示 25.0——注意这里丢掉了 0.0625是截断不是四舍五入。负温度分支就是那个经典 buga ~a 1; b ~b;只对低字节做了取反加 1高字节只是取反当低字节为 0x00 时取反加 1 会溢出成 0x00 并向高位进位而代码里没有把这个进位加到 b 上。正确写法是把 a 和 b 拼成 16 位再整体取反加 1。以 -0.5℃ 为例原始码 0xFFF8a0xF8b0xFF按代码算a0x08b0x00temp(0x084)|(0x004)0temp_d8显示 0.5 而不是 -0.5连符号都丢了。这个坑在课设验收时如果只测室温根本发现不了一放冰水就露馅。参数说明flag为 1 表示首次启动会额外读 TH/TL 并播放开机动画为 0 表示常规读取。temp存整数部分0 到 125 或 0 到 55temp_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 —— 太长了 } }逻辑说明消隐顺序很关键——必须先关掉其余三位再开当前位否则会出现鬼影也就是上一位的段码残留在当前位上。代码里的写法是先wei21; wei31; wei41;再wei10顺序正确。真正的问题在display_delay(10)。这个函数内层是for(j120;j0;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的消除前导零逻辑清空了负号无处可放。这是课程设计里负温度显示普遍处理不好的地方改进办法是在f1时强制第一位输出负号不参与消除前导零。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()存进 EEPROMget_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里写的是TMOD0x10定时器 1 方式 1但装初值用的是TH0和TL0定时器 0 的寄存器中断函数写的是interrupt 3定时器 1 的向量启动用的是TR1。结果就是定时器 1 从 0x0000 开始计数12MHz 下 65536μs 溢出一次而不是预期的 50msTH0/TL0的赋值完全没起作用。num累加到 5 的实际时间是 327ms 而不是 200ms累加到 20 是 1.31s 而不是 1s节奏全乱。更糟的是如果某个分支里TR1没被置 1中断永远不触发beer就一直是 1蜂鸣器不响。解决统一成定时器 0三处一起改void timer0_init(bit t) { TMOD 0x01; // 定时器 0方式 116 位 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的消除前导零逻辑在f1且整数部分小于 10 时会把第一第二位都清空负号没有位置显示。解决换算部分改成先拼 16 位再整体取反加 1uint raw ((uint)b 8) | a; // 拼成 16 位原始码 if(raw 0x8000) // 最高位为 1负温度 { f 1; raw (~raw) 1; // 整体取反加 1进位自动传播 } temp raw 4; // 整数部分 temp_d raw 0x0F; // 小数部分显示部分则要在f1时强制第一位输出负号跳过消除前导零的判断。这两处改完-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 和四个头文件在同名资源包里电路图和段码表都是现成的照着上面几节的口径把定时器、负温度和扫描延时改一遍基本就能一次点亮。希望帮到你。本文还有配套的精品资源点击获取