依赖51 单片机的 Modbus 协议温度采集与外设控制系统的实现

一、项目概述

本项目以 STC89C52(51 内核)单片机为核心,基于自定义 Modbus 协议帧格式,实现了串口指令解析LED 控制数码管数字显示定时器频率调节(即蜂鸣器频率调节)DS18B20 温度采集等功能。系统通过 UART 串口接收上位机指令,解析后执行对应操作,并支持指令响应回传,适用于小型工业控制、教学实验等场景。

核心功能清单

表格

功能码 功能描述
0x01 控制指定 LED 点亮
0x02 触发数码管显示指定数字
0x03 配置定时器 0 频率(200/400/600/800/1000Hz),蜂鸣器频率调节
0x04 读取 DS18B20 温度值并通过串口回传

二、硬件设计

2.1 硬件架构框图(注意P2.1与蜂鸣器相连)

2.2 关键硬件连接(STC89C52 为例)

表格

外设 单片机引脚 说明
UART 串口 P3^0(RX)/P3^1(TX) 2400bps 波特率,中断接收
DS18B20 P3^7(DQ) 单总线通信
LED 阵列 P2 口 共阳 LED,低电平点亮
数码管 P0 (段码)/P1 (位选) 四位共阳数码管
定时器 0 T0(P3^4) 方式 1,中断控制 蜂鸣器
定时器 1 T1(P3^5) 方式 2,作为 UART 波特率发生器

2.3 硬件实物示意图(文字描述)

  • 核心板:STC89C52 最小系统(含 11.0592MHz 晶振、复位电路);
  • 外设模块:
    • LED:8 个共阳 LED 接 P2.0~P2.7,串联 220Ω 限流电阻;
    • 数码管:四位共阳数码管,P0 接 a~dp 段(串 220Ω 电阻),P1.0~P1.3 接位选;
    • DS18B20:DQ 引脚接 P3.7,外接 4.7K 上拉电阻,VCC 接 5V,GND 接地;
    • 串口:CH340 模块转 USB,TX/RX 分别接单片机 RX/TX。

三、软件设计

3.1 软件整体流程

3.2 核心模块代码解析

(1)UART 驱动模块(uart.c)

负责串口初始化、中断接收、数据发送,是系统与上位机通信的核心。

c

运行

复制代码
#include <reg51.h>
#include "uart.h"
#include "modbus.h"
#include "led.h"

xdata char recv_buffer[32]; // 接收缓冲区(扩展RAM)
unsigned int pos = 0;       // 缓冲区指针

// UART接收中断服务函数(中断号4)
void uart_handle(void) interrupt 4
{
    if((SCON & (1 << 0)) == 1) // 检测接收完成标志RI
    {
        if(pos < 32) // 防止缓冲区溢出
        {
            recv_buffer[pos++] = SBUF; // 读取接收数据
            recv_buffer[pos] = 0;      // 字符串结束符
        }
        
        SCON &= ~(1 << 0); // 清RI标志
        
        flag_digiter = 0;  // 复位数码管标志
        led_all_off();     // LED全灭
        IE &= ~(1 << 1);   // 关闭Timer0中断(可选)
    }
}

// UART初始化:波特率38400、8位数据、1位停止位
void uart_init(void)
{
    // SCON配置:8位数据,可变波特率(SM0=0, SM1=1);REN=1(允许接收)
    SCON &= ~(3 << 6);
    SCON |= 1 << 6;
    SCON &= ~(1 << 7);
    SCON |= 1 << 4;

    // PCON配置:SMOD=1(波特率加倍)
    PCON &= ~(3 << 6);
    PCON |= 1 << 7;
    PCON &= ~(1 << 6);

    // Timer1配置:方式2(8位自动重装)
    TMOD &= ~(0xF0 << 0);
    TMOD |= 1 << 5;
    TMOD &= ~(1 << 4);

    // Timer1重装值:11.0592MHz晶振,2400波特率
    TL1 = 232;
    TH1 = 232;

    TCON |= 1 << 6; // 启动Timer1(TR1=1)

    IE |= 1 << 7; // 开总中断(EA=1)
    IE |= 1 << 4; // 开UART中断(ES=1)
}

// 发送单个字符
void send_char(unsigned char ch)
{
    SBUF = ch;
    while((SCON & (1 << 1)) == 0); // 等待发送完成(TI=1)
    SCON &= ~(1 << 1); // 清TI标志
}

// 发送字符串
void send_str(const char *pstr)
{
    while(*pstr != '\0')
    {
        send_char(*pstr++);
    }
}

// 发送指定长度缓冲区
void send_buff(const char *pbuff, int len)
{
    while(len--)
    {
        send_char(*pbuff++);
    }
}

关键说明

  • 波特率定时器初值: 2^8-2^smod * focs / 32 / bps / 12

    其中smod表示PCON的B7,根据实际情况带入,不是0就是1;

    focs晶振频率,我们这是11.0592M;

    bps目标波特率我们这是2400(最终得到TH1 = TL1 = 232, 8位自动重装载)

  • 接收中断采用缓冲区 + 指针的方式,避免数据丢失;

  • 发送函数通过轮询 TI 标志确保数据发送完成。

(2)DS18B20 温度传感器驱动(ds18b20.c)

实现单总线通信的复位、读写、温度转换与解析:

c

运行

复制代码
#include <reg51.h>
#include "uart.h"
#include "delay.h"
#include "ds18b20.h"
#include <intrins.h>

#define DQ_PIN_HIGH (P3 |= (1 << 7) )  // DQ引脚置高
#define DQ_PIN_LOW (P3 &= ~(1 << 7) )  // DQ引脚置低
#define DQ_PIN_CHECK ((P3 & (1 << 7)) != 0) // 检测DQ电平

// DS18B20复位(单总线核心步骤)
int ds18b20_reset(void)
{
    int time = 0;

    DQ_PIN_LOW;
    Delay10us(70); // 拉低至少480us(70*10us=700us)

    DQ_PIN_HIGH;
    Delay10us(5);  // 释放总线,等待传感器响应(15~60us)

    // 等待传感器拉低总线
    while(DQ_PIN_CHECK && time < 30)
    {
        Delay10us(1);
        time++;
    }
    if(time >= 30)
    {
        send_str("wait ds18b20 low fail\r\n");
        return -1;
    }
    
    // 等待传感器释放总线
    time = 0;
    while(!DQ_PIN_CHECK && time < 30)
    {
        Delay10us(1);
        time++;
    }
    if(time >= 30)
    {
        send_str("wait ds18b20 high fail\r\n");
        return -1;
    }
    
    return 0;
}

// 向DS18B20写入1字节数据
void write_ds18b20(unsigned char dat)
{
    int i = 0;
    for(i = 0; i < 8; i++)
    {
        if(dat & 1) // 写1:拉低<15us,释放总线
        {
            DQ_PIN_LOW;
            _nop_();
            _nop_();
            DQ_PIN_HIGH;
            Delay10us(5);
        }
        else // 写0:拉低≥60us,释放总线
        {
            DQ_PIN_LOW;
            Delay10us(6);
            DQ_PIN_HIGH;
        }
        dat >>= 1; // 处理下一位
    }
}

// 从DS18B20读取1字节数据
unsigned char read_ds18b20(void)
{
    unsigned char dat = 0;
    int i = 0;
    for(i = 0; i < 8; i++)
    {
        DQ_PIN_LOW;
        _nop_();
        _nop_();
        DQ_PIN_HIGH; // 拉低<15us后释放
        _nop_();
        _nop_();
        _nop_();
        _nop_();
        
        if(DQ_PIN_CHECK) // 读取当前电平
        {
            dat |= (1 << i);
        }
        
        Delay10us(6); // 等待时隙结束
    }
    return dat;
}

// 读取温度值(转换为浮点型)
float get_tmp(void)
{
    short tmp = 0;
    unsigned char tmp_low = 0;
    unsigned char tmp_high = 0;

    ds18b20_reset();
    write_ds18b20(0xCC); // 跳过ROM指令(单传感器)
    write_ds18b20(0x44); // 启动温度转换
    Delay1ms(1000);      // 等待转换完成(最大750ms)

    ds18b20_reset();
    write_ds18b20(0xCC); // 跳过ROM指令
    write_ds18b20(0xBE); // 读取暂存器指令

    tmp_low = read_ds18b20();  // 读取低字节
    tmp_high = read_ds18b20(); // 读取高字节

    tmp = tmp_high << 8;
    tmp |= tmp_low;

    return tmp * 0.0625; // DS18B20分辨率0.0625℃/LSB
}

关键说明

  • 单总线复位是通信前提,需严格遵守时序(拉低→释放→等待响应);
  • 温度值为 16 位有符号数,高字节为符号位,低字节为小数位,乘以 0.0625 得到实际温度。
(3)Modbus 协议解析(modbus.c)

自定义 Modbus 帧格式(适配 51 单片机资源),实现指令解析与功能执行:

c

运行

复制代码
#include <stdio.h>
#include <string.h>
#include "modbus.h"
#include "uart.h"
#include "led.h"
#include "timer.h"
#include "digiter.h"
#include "ds18b20.h"

#define DEV_ADDRESS   0x01    // 设备地址
unsigned int flag_digiter = 0; // 数码管显示标志

// 解析Modbus功能码(帧格式:0xAA + 地址 + 功能码 + 数据 + 校验和 + 0xBB)
int PraseFunCode(void)
{
    int i = 0;
    int ret = 0;
    unsigned char sum = 0;

    // 帧头帧尾校验
    if((unsigned char)recv_buffer[0] == 0xAA && (unsigned char)recv_buffer[6] == 0xBB)
    {
        // 设备地址校验
        if((unsigned char)recv_buffer[1] == 0x01)
        {
            // 计算前5字节校验和
            for(i = 0; i < 5; i++)
            {
                sum += recv_buffer[i];
            }
            // 校验和匹配则返回功能码
            if(sum == recv_buffer[5])
            {
                ret = recv_buffer[2];
            }
        }
    }
    return ret;
}

// 响应帧回传(功能码最高位置1表示响应)
void call_back(void)
{
    int i = 0;
    unsigned char sum = 0;
    xdata char tmpbuff[10] ={0};
    
    memcpy(tmpbuff, recv_buffer, 7);
    tmpbuff[2] |= 1 << 7; // 功能码最高位标记响应

    if((unsigned char)tmpbuff[0] == 0xAA && (unsigned char)tmpbuff[6] == 0xBB)
    {
        if((unsigned char)tmpbuff[1] == 0x01)
        {
            for(i = 0; i < 5; i++)
            {
                sum += tmpbuff[i];
            }
            tmpbuff[5] = sum; // 重新计算校验和
            send_buff(tmpbuff, 7); // 发送响应帧
        }
    }
}

// 功能码执行逻辑
void do_handle(int funcode)
{
    float tmp = 0;
    xdata char tmpbuff[32] = {0};

    switch (funcode)
    {
        case 1: // 控制LED点亮
            led_on(recv_buffer[3]);
            break;
        case 2: // 触发数码管显示
            flag_digiter = 1;
            break;
        case 3: // 配置定时器频率
            timer0_init();
            switch ((unsigned char)recv_buffer[3])
            {
                case 0x01: frequency = HZ_200; break;
                case 0x02: frequency = HZ_400; break;
                case 0x03: frequency = HZ_600; break;
                case 0x04: frequency = HZ_800; break;
                case 0x05: frequency = HZ_1000; break;
            }                
            break;    
        case 4: // 读取温度并回传
            tmp = get_tmp();
            sprintf(tmpbuff, "tmp:%.2f\r\n", tmp);
            send_str(tmpbuff);
            break;
    }
}

关键说明

  • 自定义帧格式:0xAA(帧头) + 0x01(地址) + 功能码 + 数据 + 校验和 + 0xBB(帧尾)
  • 校验和为前 5 字节累加和,简化 51 单片机的计算开销;
  • 响应帧通过功能码最高位标记,便于上位机区分指令 / 响应。
(4)主函数(main.c)

系统入口,实现循环检测与指令处理:

c

运行

复制代码
#include <reg51.h>
#include "uart.h"
#include "led.h"
#include "timer.h"
#include "delay.h"
#include "modbus.h"
#include "digiter.h"

void main(void)
{
    int ret = 0;

    uart_init(); // 初始化UART
    
    while(1) // 主循环
    {
        if(pos != 0) // 接收缓冲区有数据
        {
            delay(0x4FFFF); // 消抖/等待帧接收完成
            ret = PraseFunCode(); // 解析功能码
            if(ret != 0)
            {
                do_handle(ret); // 执行功能
            }
            if(ret != 0)
            {
                call_back(); // 回传响应
            }
            pos = 0; // 清空缓冲区指针
        }
        
        if(flag_digiter == 1) // 数码管显示标志置位
        {
            while(flag_digiter)
            {
                num_show((unsigned char)recv_buffer[3]); // 显示指定数字
            }
        }
    }
}

四、功能测试与验证

4.1 测试环境

  • 硬件:STC89C52 核心板、DS18B20 模块、LED / 数码管模块、CH340 串口模块;
  • 软件:串口调试助手(波特率 38400、8N1、无校验)。

4.2 测试用例

表格

测试功能 发送指令帧(16 进制) 预期结果
LED1 点亮 AA 01 01 01 00 AA BB P2.0 对应的 LED 点亮
数码管显示数字 5 AA 01 02 05 00 AD BB 数码管循环显示数字 5
定时器 200Hz AA 01 03 01 00 AB BB 蜂鸣器以200Hz发声
读取温度 AA 01 04 00 00 AC BB 串口回显 "tmp:XX.XX\r\n"

4.3 常见问题排查

  1. DS18B20 复位失败:检查 4.7K 上拉电阻、DQ 引脚接线、时序函数(Delay10us)精度;
  2. 串口接收乱码:核对波特率(11.0592MHz 晶振)、SMOD 位配置、TX/RX 接线;
  3. 数码管显示异常:段码表是否匹配共阳 / 共阴、位选 / 段选引脚接线。

五、项目总结与优化

5.1 项目亮点

  1. 模块化设计:将 UART、DS18B20、LED、Modbus 等功能拆分为独立模块,便于维护和扩展;
  2. 中断驱动:UART 接收采用中断方式,避免主循环阻塞,提升实时性;
  3. 轻量化协议:自定义 Modbus 帧格式适配 51 单片机资源,简化校验逻辑。

5.2 优化方向

  1. 协议增强:替换为标准 Modbus RTU 协议,增加 CRC16 校验,提升可靠性;
  2. 多传感器支持:扩展 DS18B20 的 ROM 指令,支持多传感器组网;
  3. 低功耗优化:增加睡眠模式,仅在指令交互 / 温度采集时唤醒;
  4. 错误处理:增加指令帧长度校验、功能码合法性校验,提升鲁棒性;

六、附录:关键头文件(示例)

uart.h为例,定义核心函数声明:

c

运行

复制代码
#ifndef __UART_H__
#define __UART_H__

void uart_init(void);
void send_char(unsigned char ch);
void send_str(const char *pstr);
void send_buff(const char *pbuff, int len);

#endif

本项目完整实现了 51 单片机的多外设联动与串口指令解析,既适合 51 单片机入门学习,也可作为小型工业控制场景的基础框架。通过对代码的模块化解析和硬件逻辑的梳理,能够清晰理解嵌入式系统中 "外设驱动 + 协议解析 + 主循环调度" 的核心设计思路。

相关推荐
JSMSEMI112 小时前
JSM1040T 1Mbps高速具有总线唤醒功能的CAN总线收发器
单片机·嵌入式硬件
weiabc2 小时前
今日C/C++学习笔记20260223
c语言·c++·学习
jianqiang.xue2 小时前
ESP32-S3 运行 Linux 全指南:从 RISC-V 模拟器移植到 8 秒快速启动
linux·stm32·单片机·mongodb·risc-v·esp32s3
busideyang2 小时前
STC8H单片机delay_ms函数闪烁不准?原因是参数溢出!
c语言·单片机·嵌入式硬件·嵌入式
Hello_Embed2 小时前
LVGL 入门(十五):接口优化
前端·笔记·stm32·单片机·嵌入式
DREW_Smile3 小时前
数据在内存中的存储
c语言·开发语言
是翔仔呐3 小时前
第10章 模拟量采集基础:外置ADC/DAC芯片驱动(PCF8591/ADC0832)
c语言·开发语言·单片机·嵌入式硬件·51单片机
somi73 小时前
ARM-04-蜂鸣器
arm开发·单片机·嵌入式硬件
Rabitebla3 小时前
[特殊字符] TopK问题全解析(TomGo复习版|讲人话 + 原理打穿)
c语言·数据结构·算法·链表