文章目录
- 1. 项目概述
- 实现功能
- 硬件环境
- 2. 核心技术原理
- 2.1 DS18B20单总线温度采集
- 2.1.1 单总线通信基础规则
- 2.1.2 温度读取完整流程
- 2.1.3 ReadTemp函数逐行解析
- 2.2 自定义二进制串口通信协议
- 2.2.1 帧格式定义
- 2.2.2 功能码定义
- 2.3 串口中断接收机制
- 为什么必须用中断?
- 接收流程
- 2.4 PWM蜂鸣器驱动与系统定时
- PWM原理
- 经典踩坑:蜂鸣器上电就响
- 1秒定时实现
- 2.5 自动温控与告警逻辑
- 2.1 DS18B20单总线温度采集
- 3. 完整工程代码(12MHz晶振)
- 4. 上位机调试指南
- 常用指令对照表
- 5. 经典踩坑与解决方案
- 坑1:串口完全收不到数据
- 坑2:发了指令没回复
- 坑3:蜂鸣器上电就响/关不死
- 坑4:开启自动模式不采集温度
- 坑5:DS18B20读出来全是0/255
- 6. 总结
1. 项目概述
本文基于STC89C52RC单片机,实现了一套完整的主从式温控下位机系统,涵盖单总线温度采集、自定义二进制串口协议、PWM蜂鸣器驱动、自动温控告警四大核心模块。项目从硬件时序到底层协议、从中断驱动到业务逻辑完整打通,非常适合作为嵌入式入门的综合实战项目。
实现功能
硬件环境
- 主控:STC89C52RC(12MHz晶振)
- 传感器:DS18B20数字温度传感器(单总线)
- 外设:LED指示灯、无源蜂鸣器
- 通信:USB转TTL模块,波特率2400
2. 核心技术原理
2.1 DS18B20单总线温度采集
单总线(1-Wire)是Dallas半导体的专利总线,所有数据和控制都通过一根线传输,物理层采用开漏输出+上拉电阻架构,任何设备都只能拉低总线,高电平靠上拉电阻实现。
2.1.1 单总线通信基础规则
2.1.2 温度读取完整流程
DS18B20读取温度必须分两次通信完成:
- 第一次通信:复位 → 跳过ROM(0xCC) → 启动转换(0x44) → 等待转换完成
- 第二次通信:复位 → 跳过ROM(0xCC) → 读暂存器(0xBE) → 读取温度字节
2.1.3 ReadTemp函数逐行解析
int ReadTemp(void)
{
unsigned char LSBVal = 0; // 温度低字节
unsigned char MSBVal = 0; // 温度高字节
int Val = 0; // 16位原始温度值
// 总线就绪:拉高总线1ms,确保传感器处于空闲状态
DQ = 1;
Delay480us();
Delay480us();
// ========== 第一次通信:启动温度转换 ==========
DQReset(); // 主机发复位脉冲(拉低480μs)
ReadDQRespone(); // 等待从机应答脉冲,确认传感器在线
SendByte(0xCC); // 跳过ROM指令:单设备时跳过地址匹配
SendByte(0x44); // 启动温度转换指令:传感器开始AD转换
Delay800ms(); // 等待转换完成:12位分辨率最长750ms
// ========== 第二次通信:读取温度结果 ==========
DQReset(); // 重新复位同步
ReadDQRespone();
SendByte(0xCC); // 再次跳过ROM
SendByte(0xBE); // 读暂存器指令:传感器开始输出内部寄存器
LSBVal = RecvByte();// 先读低字节(低位先发)
MSBVal = RecvByte();// 后读高字节
// ========== 数据换算 ==========
Val = (MSBVal << 8) | LSBVal; // 拼接成16位原始值
Val >>= 4; // 舍弃低4位小数,得到整数温度
return Val;
}
寄存器格式说明:DS18B20默认12位分辨率,16位寄存器中高5位为符号位,中间7位为整数部分,低4位为小数部分,精度0.0625℃。右移4位直接得到整数摄氏温度,负温以补码存储,int类型会自动识别符号。
2.2 自定义二进制串口通信协议
采用类Modbus的固定帧长二进制协议,每帧7字节,相比字符串协议传输效率更高、抗干扰能力更强,适合工业级嵌入式通信。
2.2.1 帧格式定义
| 0 | 帧头 | 0xAA | 标记一帧数据的起始,用于同步 |
| 1 | 从机地址 | 0x01 | 总线上可挂载多设备,通过地址区分 |
| 2 | 功能码 | 0x01~0x07 | 标识本条指令的功能 |
| 3 | 数据位1 | 参数高字节 | 传递功能参数 |
| 4 | 数据位2 | 参数低字节 | 传递功能参数 |
| 5 | 和校验 | 计算值 | 帧头+地址+功能码+数据1+数据2+帧尾,取低8位 |
| 6 | 帧尾 | 0xBB | 标记一帧数据的结束 |
2.2.2 功能码定义
| 0x01 | 读取当前温度 | 数据位无意义,应答帧返回温度值 |
| 0x02 | LED开关控制 | 数据位2:0x00关,0x01开 |
| 0x03 | 蜂鸣器开关控制 | 数据位2:0x00关,0x01开 |
| 0x05 | 设置温度上限 | 数据位1+数据位2:16位阈值 |
| 0x06 | 设置温度下限 | 数据位1+数据位2:16位阈值 |
| 0x07 | 工作模式切换 | 数据位2:0x00手动,0x01自动 |
2.3 串口中断接收机制
串口接收采用中断接收+主循环处理的经典架构,这是嵌入式串口通信最可靠的实现方式。
为什么必须用中断?
上位机的指令是异步随机到达的,如果用主循环查询接收,很容易因为主循环正在执行延时、读温度等操作错过字节,造成丢包。
- 中断接收:硬件收到一个完整字节就自动触发中断,立刻把数据存入缓冲区,绝对不会丢包
- 主循环处理:中断只做最精简的数据搬运,复杂的协议解析、逻辑处理放在主循环空闲时执行
接收流程
2.4 PWM蜂鸣器驱动与系统定时
使用定时器0同时实现两个功能:PWM驱动蜂鸣器 + 1秒系统定时,实现定时器资源复用。
PWM原理
通过定时器中断周期性翻转引脚电平,控制一个周期内高电平的占比,即可调节蜂鸣器的音量和音调。本项目采用100级占空比,每10次定时器溢出完成一个PWM周期。
经典踩坑:蜂鸣器上电就响
很多人写PWM都会遇到Duty=0时蜂鸣器还微响的问题,根源是边界判断错误:
// 错误写法:curcnt清零为0时,0 <= 0 成立,会输出一个窄脉冲
Buzzer = (curcnt <= Duty) ? 1 : 0;
// 正确写法:Duty=0时完全没有输出
Buzzer = (curcnt < Duty) ? 1 : 0;
1秒定时实现
利用定时器中断累计次数实现秒级计时,无需额外定时器。PWM频率为1047Hz时,每秒约产生10470次中断,累计到阈值就置位秒标志。
2.5 自动温控与告警逻辑
自动模式下,系统每秒自动采集一次温度,与设定阈值对比:
- 温度 < 下限:点亮LED + 启动蜂鸣器,声光告警
- 温度 ≥ 下限:关闭所有告警,恢复正常状态
注意:自动模式默认只做本地采集和告警,不会主动向上位机上报温度;如需主动上报,可在定时采集逻辑中添加发送帧函数。
3. 完整工程代码(12MHz晶振)
#include <reg52.h>
#include <intrins.h>
#include <string.h>
/************************* 硬件引脚定义 *************************/
sbit DQ = P3^7; // DS18B20单总线
sbit Led1 = P2^0; // LED指示灯
sbit Buzzer = P2^1; // 蜂鸣器
/************************* 全局变量定义 *************************/
// 串口协议接收
volatile unsigned char RecvBuff[7]; // 7字节固定帧缓冲区
volatile unsigned char RecvLen = 0; // 当前接收字节数
volatile unsigned char IsFrame = 0; // 收到完整帧标志
// 系统状态
unsigned char AutoMode = 0; // 0=手动模式,1=自动模式
int TempHigh = 30; // 温度上限,默认30℃
int TempLow = 10; // 温度下限,默认10℃
int CurTemp = 0; // 当前采集温度
unsigned char SecFlag = 0; // 1秒定时标志
// PWM蜂鸣器参数
unsigned char Duty = 0;
unsigned char ReloadTH0 = 0;
unsigned char ReloadTL0 = 0;
#define BEEP_FREQ 1047 // 蜂鸣器音调频率
#define BEEP_DUTY 50 // 蜂鸣器占空比
/************************* 软件延时函数 @12MHz *************************/
void Delay800ms(void)
{
unsigned char data i, j, k;
nop();
i = 6; j = 143; k = 129;
do {
do { while (–k); } while (–j);
} while (–i);
}
void Delay480us(void)
{
unsigned char data i;
nop();
i = 191;
while (–i);
}
void Delay10us(void)
{
unsigned char data i;
i = 2;
while (–i);
}
/************************* DS18B20温度驱动 *************************/
void DQReset(void)
{
DQ = 0;
Delay480us();
DQ = 1;
}
void ReadDQRespone(void)
{
Delay10us();
while (DQ);
while (!DQ);
}
void SendByte(unsigned char TmpByte)
{
unsigned char n;
for (n = 0; n < 8; n++)
{
if (TmpByte & (0x01 << n))
{
DQ = 0; nop(); nop();
DQ = 1;
Delay10us();Delay10us();Delay10us();
Delay10us();Delay10us();Delay10us();
}
else
{
DQ = 0;
Delay10us();Delay10us();Delay10us();
Delay10us();Delay10us();Delay10us();
DQ = 1; nop(); nop();
}
}
}
unsigned char RecvByte(void)
{
unsigned char TmpByte = 0x00, n;
for (n = 0; n < 8; n++)
{
DQ = 0; nop(); nop();
DQ = 1;
Delay10us();
if (DQ) TmpByte |= (0x01 << n);
Delay10us();Delay10us();Delay10us();
Delay10us();Delay10us();Delay10us();
}
return TmpByte;
}
int ReadTemp(void)
{
unsigned char LSB, MSB;
int Val;
DQ = 1;
Delay480us(); Delay480us();
// 启动温度转换
DQReset(); ReadDQRespone();
SendByte(0xCC); SendByte(0x44);
Delay800ms();
// 读取温度寄存器
DQReset(); ReadDQRespone();
SendByte(0xCC); SendByte(0xBE);
LSB = RecvByte(); MSB = RecvByte();
Val = (MSB << 8) | LSB;
Val >>= 4;
return Val;
}
/************************* 串口通信模块 *************************/
void UartInit(void)
{
SCON &= ~(0x03 << 6);
SCON |= (0x01 << 6); // 串口方式1:8位UART
REN = 1; // 允许接收
PCON &= ~(0x01 << 7); // 波特率不加倍
TMOD &= ~(0x03 << 4);
TMOD |= (0x01 << 5); // 定时器1模式2:8位自动重装
TH1 = 0xF3; TL1 = 0xF3; // 12MHz下2400波特率
TR1 = 1;
ES = 1; EA = 1; // 开启串口中断
}
void UartSendByte(unsigned char dat)
{
SBUF = dat;
while (!TI);
TI = 0;
}
void SendFrame(unsigned char cmd, unsigned char data1, unsigned char data2)
{
unsigned char buf[7], checksum, i;
buf[0] = 0xAA;
buf[1] = 0x01;
buf[2] = cmd;
buf[3] = data1;
buf[4] = data2;
buf[6] = 0xBB;
checksum = buf[0] + buf[1] + buf[2] + buf[3] + buf[4] + buf[6];
buf[5] = checksum;
for (i = 0; i < 7; i++)
UartSendByte(buf[i]);
}
void UartHandler(void) interrupt 4
{
if (RI)
{
RecvBuff[RecvLen] = SBUF;
RI = 0;
RecvLen++;
if (RecvLen >= 7) IsFrame = 1;
}
}
/************************* 定时器0 + PWM蜂鸣器 *************************/
void SetPwmFreq(unsigned long freq, unsigned char TmpDuty)
{
unsigned long value;
value = 65536UL – ((1000000UL / freq) / 10UL);
ReloadTH0 = value / 256;
ReloadTL0 = value % 256;
TH0 = ReloadTH0; TL0 = ReloadTL0;
Duty = TmpDuty;
}
void BeepOn(void)
{
Duty = BEEP_DUTY;
}
void BeepOff(void)
{
Duty = 0;
Buzzer = 0;
}
void Timer0Handler(void) interrupt 1
{
static unsigned char curcnt = 0;
static unsigned int sec_cnt = 0;
TH0 = ReloadTH0;
TL0 = ReloadTL0;
// PWM占空比控制
curcnt += 10;
if (curcnt >= 100) curcnt = 0;
Buzzer = (curcnt < Duty) ? 1 : 0;
// 1秒计时
sec_cnt++;
if (sec_cnt >= 10470)
{
sec_cnt = 0;
SecFlag = 1;
}
}
void Timer0Init(void)
{
TMOD &= ~0x03;
TMOD |= 0x01;
SetPwmFreq(BEEP_FREQ, 0);
ET0 = 1; EA = 1;
TR0 = 1; // 定时器默认常开,保证秒计数正常
}
/************************* 协议解析与处理 *************************/
void ProcProtocol(void)
{
unsigned char checksum;
if (RecvBuff[0] != 0xAA || RecvBuff[6] != 0xBB)
goto END;
checksum = RecvBuff[0] + RecvBuff[1] + RecvBuff[2] + RecvBuff[3] + RecvBuff[4] + RecvBuff[6];
if (checksum != RecvBuff[5])
goto END;
switch (RecvBuff[2])
{
case 0x01:
CurTemp = ReadTemp();
SendFrame(0x01, CurTemp>>8, CurTemp&0xFF);
break;
case 0x02:
Led1 = (RecvBuff[4] == 0x01) ? 1 : 0;
SendFrame(0x02, 0x00, RecvBuff[4]);
break;
case 0x03:
(RecvBuff[4] == 0x01) ? BeepOn() : BeepOff();
SendFrame(0x03, 0x00, RecvBuff[4]);
break;
case 0x05:
TempHigh = (RecvBuff[3] << 8) | RecvBuff[4];
SendFrame(0x05, RecvBuff[3], RecvBuff[4]);
break;
case 0x06:
TempLow = (RecvBuff[3] << 8) | RecvBuff[4];
SendFrame(0x06, RecvBuff[3], RecvBuff[4]);
break;
case 0x07:
AutoMode = RecvBuff[4];
if (AutoMode) CurTemp = ReadTemp();
SendFrame(0x07, 0x00, RecvBuff[4]);
break;
default: break;
}
END:
memset((void*)RecvBuff, 0, sizeof(RecvBuff));
RecvLen = 0;
IsFrame = 0;
}
/************************* 自动模式逻辑 *************************/
void AutoModeProc(void)
{
if (!AutoMode || !SecFlag) return;
SecFlag = 0;
CurTemp = ReadTemp();
if (CurTemp < TempLow)
{
Led1 = 1;
BeepOn();
}
else
{
Led1 = 0;
BeepOff();
}
}
/************************* 主函数 *************************/
int main(void)
{
UartInit();
Timer0Init();
Led1 = 0;
BeepOff();
while (1)
{
if (IsFrame) ProcProtocol();
AutoModeProc();
}
return 0;
}
4. 上位机调试指南
使用串口调试助手,按以下配置即可测试:
- 波特率:2400
- 数据位:8
- 停止位:1
- 校验位:无
- 发送/接收:HEX模式
常用指令对照表
| 读取当前温度 | AA 01 01 00 00 67 BB |
| 打开LED | AA 01 02 00 01 69 BB |
| 关闭LED | AA 01 02 00 00 68 BB |
| 打开蜂鸣器 | AA 01 03 00 01 6A BB |
| 关闭蜂鸣器 | AA 01 03 00 00 69 BB |
| 设置温度上限30℃ | AA 01 05 00 1E 89 BB |
| 设置温度下限10℃ | AA 01 06 00 0A 76 BB |
| 开启自动模式 | AA 01 07 00 01 6E BB |
| 关闭自动模式 | AA 01 07 00 00 6D BB |
5. 经典踩坑与解决方案
坑1:串口完全收不到数据
- 排查顺序:先确认TX/RX交叉连接、GND共地 → 回环测试确认串口线正常 → 检查REN、ES、EA三个中断开关是否全开
- 最常见原因:TXD和RXD接反,或者忘记共地
坑2:发了指令没回复
- 排查顺序:先注释掉校验判断,看能不能回复 → 能回复就是校验算法不一致 → 不能回复就是帧解析逻辑问题
- 注意:校验算法必须收发完全一致,是包含帧尾还是不包含帧尾要提前约定
坑3:蜂鸣器上电就响/关不死
- 90%是边界bug:把<=改成<即可解决
- 剩下10%:蜂鸣器是低电平触发,输出逻辑写反了,翻转一下即可
坑4:开启自动模式不采集温度
- 核心原因:定时器0只在蜂鸣器开启时才运行,温度正常时定时器停了,秒计数也就停了
- 解决方案:定时器默认常开,蜂鸣器只通过占空比控制,不关闭定时器
坑5:DS18B20读出来全是0/255
- 时序问题:延时函数和晶振不匹配,单总线时序误差超过10%就会通信失败
- 硬件问题:DQ引脚没有接上拉电阻,或者接线错误
6. 总结
本项目虽然代码量不大,但覆盖了嵌入式开发最核心的基础能力:单总线协议时序、串口中断通信、自定义协议设计、定时器PWM、状态机逻辑。理解了这套代码,就掌握了8位单片机外设驱动和通信开发的核心方法论,后续扩展数码管显示、按键输入、风扇控制等功能都可以快速复用这套框架。



