简介:这是一套基于STC15W4K32S4单片机读取DS18B20温度并通过串口发送的完整工程,面向单片机入门与进阶学习者,适合在Proteus仿真与Keil开发环境中对照学习。压缩包共37个文件、约233KB,包含 main.c、ds18b20.c/h、uart.c/h 等C源码,Proteus仿真工程(pdsprj/pdsbak)与Keil工程(uvproj),以及编译生成的hex烧录文件;其中obj/lst为编译中间文件,bat为辅助脚本,文件类型覆盖从源码到仿真、烧录的完整链路。已有3201人学习。通过本工程可掌握STC15系列单总线时序、DS18B20读写流程(信号线接P3.6口)、串口1波特率配置与数据发送,理解UART协议在无时钟同步下的通信方式,还能在Proteus中实时观察温度变化与串口输出,学习软硬件联调的思路,直接用于课程设计、毕业设计或项目原型验证。 做单片机开发的人应该都懂,光看数据手册和理论教程,永远学不会真正的时序控制。DS18B20这颗温度传感器,单总线协议看着简单,但实际写驱动、调时序、验证数据,每一步都是坑。我这次用STC15W4K32S4在proteus仿真里完整走了一遍“读取DS18B20温度 → 串口输出”的流程,配合keil写源码、仿真调参、虚拟串口查数据,踩了不少坑也算积累了不少经验。这篇就把整个项目从方案设计到代码实现、仿真搭建、问题排查全部拆开来讲,适合正在学51单片机、准备做温度采集类课设或想入门proteus仿真校验的同学直接参考。
1. 项目概述与整体方案设计
1.1 为什么选STC15W4K32S4 + DS18B20这个组合
先说选型逻辑。STC15W4K32S4属于STC15系列的增强型8051内核单片机,1T工作模式,主频可以跑到30MHz左右(实际手册上STC15W4K32S4最高支持24MHz~30MHz范围,仿真和实体芯片有一些差异),SRAM有4K,Flash 32K,带两路串口和丰富的外设。相比传统STC89C52,它在Proteus仿真里的优势很明显:内部时钟源可选、不需要外部晶振,还能直接使用定时器2做串口波特率发生器,让代码更贴近现代8051的开发习惯。
DS18B20这颗传感器估计没人不知道,一线总线通信,两根线搞定供电和数据传输,测温范围-55℃到+125℃,12位分辨率下的精度虽然标称±0.5℃,但实际看使用环境会有一些偏差。选它的原因无外乎三点:一是单总线接口省IO口,二是数据直接是数字量不用ADC,三是Proteus里有成熟的仿真模型,适合和单片机配合做完整的系统验证。
这个组合放在一起,最典型的使用场景就是温度监控终端:单片机周期性读取DS18B20的温度值,经过格式化处理后通过串口把数据发到上位机或者串口调试助手。在Proteus里做一套这样的仿真,不需要硬件焊接,成本低、调试方便,特别适合课设和入门级项目。
1.2 整体数据流与系统架构
整个系统的数据流向不复杂,画出来其实就是三个环节:
- DS18B20把环境温度转换为16位二进制温度值,通过单总线DQ线传给单片机;
- STC15W4K32S4使用GPIO模拟单总线时序,读出原始温度值后做符号判断和温度换算;
- 单片机把换算后的温度值拼成ASCII字符串,通过UART串口TXD引脚发送。
Proteus仿真中,温度的变化可以通过修改DS18B20模型的温度值来模拟(仿真模型上点击可以调整),串口侧我用虚拟终端(Virtual Terminal)直接看输出,也可以用COM物理接口模块连接电脑串口调试助手,看你需要哪种验证方式。电源方面,STC15W4K32S4每个电源引脚都要接上VCC和GND,Proteus仿真环境必须明确标出电源网络,否则仿真会报错,这个细节后面细说。
2. DS18B20温度读取的核心逻辑:时序与驱动代码
2.1 单总线协议的基础原理
DS18B20和单片机之间只有一根数据线DQ,这意味着通信双方必须严格约定时序,称为单总线协议。所有通信都从单片机发出的复位脉冲开始:主机拉低总线至少480μs,然后释放总线,DS18B20会在15~60μs内拉低总线输出一个存在脉冲(presence pulse),告诉主机“我在线”。这一步做不好,后面所有读写都会失败。
存在脉冲之后,主机执行ROM命令(0xCC跳过ROM,0x44启动温度转换),转换需要一定时间,12位分辨率下典型转换时间是750ms。转换完成后再发复位脉冲、跳过ROM、发送读暂存器命令0xBE,就可以逐字节读出温度数据、校验数据等暂存器内容。
这里有个容易踩的坑:Proteus仿真模型对时序的宽容度比真实硬件要高一些,但这不代表你可以随便写延时。如果你在一个实际STC15W4K32S4芯片上跑,1T 8051的指令执行速度是传统12T的12倍,同样的延时函数在89C52上跑得好好的,换到STC15上可能时序就乱了。所以延时函数必须结合主频重新计算,不能用以前的老代码直接搬运。
2.2 初始化、读位、写字节的代码实现
Keil C51工程里,我先定义一个DS18B20操作的头文件,用sbit声明DQ引脚连到单片机的P1口:
#ifndef DS18B20_H #define DS18B20_H #include "STC15W4K32S4.h" sbit DS18B20_DQ = P1^0; unsigned char DS18B20_Init(void); void DS18B20_WriteByte(unsigned char dat); unsigned char DS18B20_ReadByte(void); float DS18B20_ReadTemper(void); #endif初始化函数是单总线通信的“握手信号”,也是排查问题的第一步。标准的代码框架如下:
unsigned char DS18B20_Init(void) { unsigned char presence = 0; DS18B20_DQ = 0; Delay_OneWire_Us(500); // 拉低480μs以上 DS18B20_DQ = 1; // 释放总线 Delay_OneWire_Us(70); // 等待DS18B20拉低总线 presence = DS18B20_DQ; // 读取存在脉冲 Delay_OneWire_Us(410); // 剩余时序补全 return presence; }注意最后返回的是DQ的电平值。如果为0说明DS18B20正确响应了存在脉冲,如果是1则说明总线上没有器件。Proteus仿真中DS18B20引脚接错、没有接上拉电阻或者电源没接好,都会导致存在脉冲异常,这个返回值就是第一道排查窗口。
写字节和读字节的时序,要牢记每个bit的读写窗口都是60μs,位与位之间至少要有1μs的恢复时间:
void DS18B20_WriteByte(unsigned char dat) { unsigned char i; for (i = 0; i < 8; i++) { DS18B20_DQ = 0; Delay_OneWire_Us(10); DS18B20_DQ = dat & 0x01; Delay_OneWire_Us(50); DS18B20_DQ = 1; dat >>= 1; } } unsigned char DS18B20_ReadByte(void) { unsigned char i, dat = 0; for (i = 0; i < 8; i++) { dat >>= 1; DS18B20_DQ = 0; Delay_OneWire_Us(5); DS18B20_DQ = 1; Delay_OneWire_Us(8); if (DS18B20_DQ) dat |= 0x80; Delay_OneWire_Us(50); } return dat; }这段代码看着简单,但里面“写1”和“读位采样”的延时区间需要特别小心。写1时的低电平时间不能超过15μs,否则DS18B20会把这个bit当成写0;读位采样则必须在拉低总线后的15μs内完成,太晚了器件已经释放总线,读到的数据就不准了。我当时调这个采样点延时,从3μs到15μs挨个试了一遍,最稳定的区间其实集中在7~10μs。
2.3 温度计算与负温度处理
DS18B20返回的温度原始值是16位有符号数,分辨率与配置的转换精度有关。默认12位精度下,LSB代表0.0625℃。换算公式很简单:
float DS18B20_ReadTemper(void) { unsigned char low, high; int raw_temp; float temperature; if (DS18B20_Init() != 0) // 存在脉冲异常处理 return -999.0; DS18B20_WriteByte(0xCC); // 跳过ROM DS18B20_WriteByte(0x44); // 启动温度转换 Delay_OneWire_Ms(750); // 等待转换完成 DS18B20_Init(); DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0xBE); // 读暂存器 low = DS18B20_ReadByte(); high = DS18B20_ReadByte(); raw_temp = (high << 8) | low; if (raw_temp & 0x8000) // 负温度处理 raw_temp = raw_temp - 0x10000; temperature = raw_temp * 0.0625; return temperature; }这个函数返回的-999.0是一个明显不合理的温度值,用来给主函数做错误判断。实际编码时我建议即使只做正温度范围,也务必处理符号位,因为DS18B20在-0.5℃时会输出0xFFF8这样的数据,不处理的话换算后会变成一个很大的正数,串口输出就会看到异常跳变。另外Delay_OneWire_Ms(750)在高主频下要注意不要用普通的循环延时代替,最好用定时器或者精确的延时函数,否则整个读取周期会偏差比较大。
3. STC15串口通信与Keil代码实现
3.1 串口初始化的关键参数计算
STC15W4K32S4的串口1,推荐使用定时器2做波特率发生器,因为这样定时器1可以留作他用,而且设置清晰。我用的系统时钟是12MHz,目标波特率是9600bps。STC15系列1T模式下,波特率计算公式为:
波特率 = 系统时钟 / 4 / (65536 - 重载值)
推导一下重载值:
65536 - 重载值 = 12000000 / 4 / 9600 = 312.5
取整后重载值约65536 - 312 = 65224,对应十六进制0xFEC8。如果时钟是11.0592MHz,重载值就会是65536 - 288 = 65248,所以换主频后重载值必须重新算。实际经验是:Proteus仿真中对波特率误差不太敏感,但接真实串口设备时,要尽量用整数分频或误差小于2%的配置。我的初始化代码如下:
void UART1_Init(void) { SCON = 0x50; // 模式1:8位UART,允许接收 AUXR |= 0x04; // T2为波特率发生器 AUXR |= 0x01; // T2运行在1T模式 T2L = 0xC8; // 12MHz -> 9600bps T2H = 0xFE; ES = 1; EA = 1; }3.2 温度数据的格式化与输出逻辑
串口发送部分,我写了两个函数:一个是单字节发送,一个是字符串发送。温度数值是float类型,直接发二进制不方便上位机解析,所以我用sprintf把它转成ASCII字符串再发送,这样串口调试助手直接就能看到可读内容:
void UART1_SendByte(unsigned char dat) { SBUF = dat; while (!TI); TI = 0; } void UART1_SendString(char *str) { while (*str) { UART1_SendByte(*str++); } }主循环中考虑到DS18B20的转换时间比较长,我用了一个500ms的周期标志位来控制采样频率,避免串口数据刷新太快导致刷屏。实际输出的字符串格式类似:
Temp = 26.31 C如果只发这一句,串口助手看到的是一行一行的温度数据。如果加上时间戳或加入固定帧头,比如“TEMP:26.31”,后续做上位机解析会更容易,这个可以根据自己的需求扩展。
3.3 主循环、错误处理与防卡死
主函数的逻辑结构很简单,但我在错误处理上花了点心思:
void main(void) { float temperature; UART1_Init(); UART1_SendString("System Init OK\r\n"); while (1) { temperature = DS18B20_ReadTemper(); if (temperature == -999.0) { UART1_SendString("DS18B20 Error!\r\n"); } else { char buf[20]; sprintf(buf, "Temp = %.2f C\r\n", temperature); UART1_SendString(buf); } Delay_OneWire_Ms(500); } }这里有个我踩过的坑:DS18B20_ReadTemper内部已经包含了750ms转换等待,主循环再延时500ms,意味着整个温度刷新周期是1.25秒左右。对温度监控这种慢变系统来说完全够用,但如果以后改为读取湿度传感器或者其他需要更高频率的传感器,就不要再额外加延时,用定时器做分频逻辑更合理。
另外,STC15W4K32S4的IO口默认是高阻态还是准双向口,不同型号和不同烧录配置有区别。我在代码里没显式配置P1M0/P1M1,默认准双向口状态可以正常工作,但如果你在Proteus仿真的时候发现DQ引脚电平异常,可以先检查一下IO模式寄存器配置,尤其是在复制别人工程的时候最容易漏掉这一步。
4. Proteus仿真环境搭建与联调
4.1 Proteus电路连接与关键元件参数
Proteus里新建工程后,从元件库添加STM32?不对,是添加STC15W4K32S4单片机、DS18B20温度传感器、上拉电阻、虚拟串口终端和电源端子。画电路的时候有几个点必须注意:
- 单片机的VCC引脚要全部接到+5V,GND引脚全部接地。STC15W4K32S4可能有多个电源引脚,Proteus仿真中漏接一个电源脚,程序运行会静默失败,很难排查;
- DS18B20的第2脚DQ要接一个4.7kΩ的上拉电阻到VCC。这个电阻是实际硬件中必需的,仿真中如果不接,单总线时序依然可能因为模型原因“碰巧能跑通”,但你要是拿着这套图去打板就废了;
- 串口TXD引脚连接到虚拟终端的RXD,单片机的RXD连接到虚拟终端的TXD,两个设备的交叉连接不能搞反。
关于STC15W4K32S4在Proteus里的型号,新版本Proteus 8.x的库里有STC15系列模型,我用的版本能直接找到STC15W4K32S4。如果找不到,可以找同系列封装接近的型号替代,但此时必须仔细核对引脚定义,尤其是串口引脚的编号,否则仿真根本跑不起来。
4.2 Keil工程配置与Hex文件加载
Keil里新建C51工程,单片机型号一栏如果找不到STC15W4K32S4,一般使用STC MCU Database里的STC15W4K32S4选项,或者接近的STC15系列型号。编译前需要做两件事:
- 在Options for Target里把晶振频率调整成12MHz,和proteus里的单片机属性保持一致;
- 勾选Create HEX File,这样编译后才会生成Hex文件。
Proteus中双击单片机元件,在Program File里加载Keil生成的Hex文件。有一点容易被忽略:加载完Hex文件后,需要把单片机元件的Clock Frequency同样设置为12MHz。很多人在Proteus里忘了改这个参数,导致仿真时序和代码里延时函数假设的主频不一致,串口波特率全乱套。
4.3 虚拟终端与串口助手的连接方式
Proteus里查看串口输出,最直接的办法是放一个Virtual Terminal虚拟终端,然后连到单片机的串口引脚上。启动仿真后,虚拟终端会显示单片机发来的ASCII字符串,这就是最直观的验证。
如果想和真实的串口调试助手联动,可以用Proteus的COMPIM组件,把它配置成物理串口模式。但说实话,在纯仿真阶段虚拟终端已经够用,COMPIM还需要额外的虚拟串口软件配合,设置繁琐且容易出诡异问题,初学阶段不建议折腾。
虚拟终端默认显示的是ASCII字符,波特率要设置成和单片机一致(我这里是9600),否则屏幕上全是乱码。另外虚拟终端支持在仿真中右键“Edit Properties”调节字体和缓冲区大小,调试大流量数据时缓冲区可以适当加大。
5. 常见问题与排查技巧实录
5.1 串口输出乱码或没有输出
这是整个项目里最常碰到的问题,我把它排第一位。出现乱码,80%以上是波特率不匹配:单片机实际波特率和虚拟终端波特率不一致。注意Proteus中单片机的主频设置、Keil里调用延时函数时假设的主频、波特率寄存器计算用的主频,这三者必须完全一致。我在调试中就出现过Keil工程设置的12MHz、Proteus里单片机却保持默认1MHz的情况,结果是串口数据完全不可读。
如果完全没有输出,先用电压探针或者逻辑探针看TXD引脚有没有电平变化。没变化说明程序可能根本没跑到串口发送部分,这时候先查主循环是否死循环在DS18B20初始化里,最简单的方法是在串口发送函数前加一个LED翻转,程序跑没跑、跑到哪里一眼就能看出来。
5.2 DS18B20初始化失败或温度恒为85℃
DS18B20初次上电后,如果不发转换命令直接读暂存器,读到的可能是85℃这个上电默认值。这个问题在proteus仿真里也常出现,原因是读时序的采样点太早或太晚,导致读到的字节全是0xFF,最终算出来一个离谱的温度。
初始化失败则要检查复位时序:低电平时间有没有满足480μs以上,释放后等待存在脉冲的时间窗口是否在60μs以内。仿真环境对时间精度要求低于真实硬件,但如果延时函数里用了错误的循环模型(比如用传统12T的延时思路写给1T单片机),时序偏差会大得离谱。
我这里提供一个排查表格,方便你对照检查:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 串口完全无数据 | 单片机没运行 | 检查电源、Hex加载、复位引脚 |
| 串口乱码 | 波特率不一致 | 核对主频、T2重载值、终端波特率 |
| 温度恒为85℃ | 没发温度转换命令 | 检查是否发送0x44并等待750ms |
| 温度读数跳变异常 | 负温度未处理 | 检查符号位扩展逻辑 |
| DS18B20返回错误 | 上拉电阻缺失或时序不对 | 检查4.7k上拉、复位脉冲宽度 |
5.3 Proteus仿真与真实硬件之间的差异
说实话,Proteus仿真DS18B20确实能跑通,但和真实硬件还是有几个明显区别需要心里有数:
- 仿真模型不涉及实际电气噪声,所以抗干扰问题在仿真中完全暴露不了;
- DS18B20模型的时序容差比较宽,有些在仿真里能正常执行的代码,放到真实芯片上可能因为延时精度不够而失败;
- Proteus里的DS18B20温度值是通过模型属性手动改的,不会像真实环境那样连续平滑变化,验证逻辑可以,验证传感器精度就不行了。
所以我的一般做法是:先用Proteus把整体逻辑跑通,然后再把代码烧录到STC15W4K32S4真实芯片上做连线验证。仿真阶段的价值在于快速验证协议理解和代码框架,真机阶段的价值在于验证时序和稳定性,两者各司其职。
5.4 一个容易被忽略的Proteus电源设置问题
STC15W4K32S4在Proteus里不止一个VCC和GND引脚,如果你在原理图上用的是默认电源网络,一定要在Design菜单里设置正确。我见过不少人仿真跑不起来,最后发现是单片机电源网络悬空,这种问题排查起来非常隐蔽。
主流做法是:在电路里放置一个POWER端子命名为VCC,接地端命名为GND,所有电源网络都统一用这两个名字。DS18B20的VDD、上拉电阻的上端、单片机的VCC引脚全部连到VCC网络,这样就不会出现“某个引脚没接电”的情况。
最后说两句
这个项目做下来,我最深的体会是:DS18B20的时序代码不是背下来就完事了,一定得自己动手调一遍,尤其是延时参数和主频的关系。很多初学者照着网上的51代码改一改放到STC15上跑,发现温度读不出来就怀疑芯片坏了,其实大概率是主频不一致导致时序计算全部偏离。如果你打算扩展到多路温度采集、OLED显示、PID控温这类更复杂的应用,这一套“Proteus仿真 + Keil工程 + 串口验证”的调试模式还能继续复用。建议先把单个DS18B20的时序摸透,再往上叠功能,不然出问题的时候你会分不清是时序的锅还是新增逻辑的锅。
本文还有配套的精品资源,点击获取