Skip to content

单片机专业综合实践-上位机下位机串口通信恒温控制系统

Notifications You must be signed in to change notification settings

vestjin/temperature-control-system

Repository files navigation

[toc]

一个简易的报告

0. 运行结果

恒温控制系统由上位机和下位机组成,通过串口通信实现温度控制。详细解释一下系统的运行过程和结果:

  1. 系统初始化:
    • 下位机初始化串口和定时器0。
    • 上位机初始化串口连接。
  2. 温度设定:
    • 用户通过上位机界面设置目标温度。
    • 上位机将设定温度发送给下位机,格式为:0x55 (起始字节), 0x01 (数据类型), 高字节, 低字节, 0xaa (结束字节)。
  3. 下位机接收设定温度:
    • 串口中断服务函数 serial_isr 接收数据。
    • process_received_packet 函数解析数据包,更新 SetValue
  4. PID控制循环:
    • PID_Control 函数读取当前温度,计算误差。
    • 使用PID算法计算控制输出。
    • 更新PWM值,控制加热元件。
  5. 温度显示:
    • update_display_buffer 函数更新显示缓冲区。
    • update_display 函数在数码管上显示当前温度。
  6. 温度反馈:
    • 定时器0中断每50ms触发一次,设置发送标志。
    • 主循环中每5个周期(约250ms)发送一次温度数据到上位机。
    • send_temperature 函数发送格式为:0x55, 0x02, 温度高字节, 温度低字节, 0xaa。
  7. 上位机接收和显示:
    • 上位机接收温度数据,解析并显示在界面上。
    • 可能会绘制温度-时间曲线。
  8. 循环控制:
    • 系统持续运行,不断调整PWM输出以维持设定温度。

运行结果:

  1. 稳定性:系统应能在短时间内达到并维持设定温度,温度波动应较小。
  2. 响应性:当设定温度改变时,系统应能迅速调整以达到新的目标温度。
  3. 精确度:实际温度应与设定温度非常接近,误差应在可接受范围内。
  4. 可视化:
    • 下位机数码管实时显示当前温度。
    • 上位机界面显示设定温度、当前温度,可能还有温度变化曲线。
  5. 抗干扰能力:系统应能应对外部温度变化,保持稳定控制。
  6. 安全性:PWM输出有限幅保护,防止过热。
  7. 通信稳定性:上下位机之间保持稳定的数据交换,确保控制的连续性。

总的来说,实现了一个闭环控制的恒温系统,通过PID算法实现精确的温度控制,并提供了良好的用户界面和反馈机制。系统的性能将取决于PID参数的调整、硬件的质量和环境因素。

硬件原理图

硬件原理图

实时温度数据监控

实时温度监控

历史温度数据查询

历史温度数据

1. 使用的软件

在实现上位机和下位机的串口通信以构建恒温控制系统时,您使用了多种软件。每个软件都有其特定的用途和功能,下面是它们的简介:

Keil

Keil 是一个集成开发环境(IDE),用于嵌入式系统的开发,特别是针对基于 ARM 架构和 8051 微控制器的项目。

  • 功能
    • 代码编辑和调试:提供强大的代码编辑和调试工具,可以方便地编写、编译和调试嵌入式 C/C++ 代码。
    • 模拟和仿真:支持在仿真环境中运行和测试代码,减少了在实际硬件上进行测试的需求。
    • 集成开发工具:集成了编译器、链接器和其他开发工具,使开发过程更加高效。

Proteus

Proteus 是一个电子设计自动化(EDA)工具,广泛用于电路设计、仿真和 PCB 设计。

  • 功能
    • 电路仿真:可以模拟实际电路的行为,支持各种模拟和数字元件,包括微控制器和传感器。
    • PCB 设计:提供强大的 PCB 设计工具,能够从原理图直接生成 PCB 布局。
    • 虚拟仪器:提供虚拟示波器、信号发生器等工具,方便调试和测试电路。

VSPD (Virtual Serial Port Driver)

VSPD 是一个虚拟串口驱动程序,用于创建虚拟串口对,允许在没有物理串口的情况下进行串口通信测试。

  • 功能
    • 虚拟串口对:创建成对的虚拟串口,两个虚拟串口之间可以互相通信,就像真实的物理串口一样。
    • 调试和测试:方便在软件开发和调试过程中模拟串口通信,特别是在上位机和下位机之间进行通信时。

串口调试助手

串口调试助手是一种工具软件,用于测试和调试串口通信,广泛用于嵌入式系统开发和调试过程中。

  • 功能
    • 发送和接收数据:可以手动或自动发送和接收串口数据,监视串口通信情况。
    • 数据格式化:支持多种数据格式(如十六进制、ASCII)显示和编辑,方便调试和分析。
    • 日志记录:记录和保存通信数据日志,便于后续分析和故障排查。

PyCharm

PyCharm 是一个专为 Python 开发设计的集成开发环境(IDE),由 JetBrains 开发。


2. 上位机程序简单分析

这个程序实现了一个温度控制系统,包含以下主要功能:

  1. 串口通信

    • 初始化串口通信,尝试连接到指定的串口。
    • 通过串口接收温度数据并发送设定温度。
  2. SQLite 数据库

    • 创建并连接到 SQLite 数据库,创建用于存储温度数据的表。
    • 插入和查询温度数据。
  3. 多线程处理

    • 使用多线程读取串口数据,防止主线程阻塞。
    • 使用数据队列和数据锁管理并发数据访问。
  4. Pygame 音频

    • 初始化 Pygame 混音器,用于播放音频警告。
  5. Dash 图表

    • 使用 Dash 和 Plotly 实现实时温度数据图表的显示。
    • 在浏览器中显示实时温度数据,并定期更新图表。
  6. Tkinter GUI

    • 使用 Tkinter 创建图形用户界面。
    • 实现设定温度、打开/关闭串口、实时数据显示和查询历史数据的功能。
    • 添加按钮和输入框用于用户交互。
    • 美化界面,设置主题和样式。

以下是各个功能的详细分析:

串口通信

def init_serial():
    global ser
    try:
        ser = serial.Serial('COM3', 9600, timeout=1)
    except serial.SerialException as e:
        print(f"Serial error: {e}")
        ser = None
  • 初始化串口,尝试连接到 COM3,波特率为 9600
  • 如果连接失败,捕获异常并输出错误信息。
def send_set_temp(set_temp):
    global ser
    if ser and ser.is_open:
        if set_temp < 15:
            set_temp = 50
            play_sound('low_temp_alert.wav')  # 播放低温警告音频
            update_status("温度过低! 调整为 50 °C 并播放警告音.")
        elif set_temp > 150:
            set_temp = 50
            play_sound('high_temp_alert.wav')  # 播放高温警告音频
            update_status("温度过高! 调整为 50 °C 并播放警告音.")
        else:
            update_status(f"设定温度为 {set_temp} °C")

        set_temp = int(set_temp * 100)
        data = bytearray([0x55, 0x01, (set_temp >> 8) & 0xFF, set_temp & 0xFF, 0xaa])
        ser.write(data)
    else:
        update_status("串口未打开")
  • 发送设定温度数据至串口。
  • 检测温度范围,并播放相应警告音频。
  • 将温度值放大 100 倍,并转换为字节数组发送。

SQLite 数据库

# 创建SQLite数据库连接
conn = sqlite3.connect('temperature_data.db', check_same_thread=False)
cursor = conn.cursor()

# 创建表
table_name = 'data_' + time.strftime('%Y%m%d%H%M%S')
cursor.execute(f'''
    CREATE TABLE IF NOT EXISTS {table_name} (
        timestamp TEXT,
        temperature REAL
    )
''')
conn.commit()
  • 创建并连接到名为 temperature_data.db 的 SQLite 数据库。
  • 根据当前时间创建一个新表,用于存储时间戳和温度数据。

多线程处理

def start_reading():
    read_thread = Thread(target=read_temperature)
    read_thread.daemon = True
    read_thread.start()
  • 使用线程读取温度数据,防止主线程阻塞。
  • 使用 QueueLock 管理数据的并发访问。
def read_temperature():
    global ser, data_queue, data_lock
    try:
        while True:
            if ser and ser.is_open:
                if ser.in_waiting:
                    data = ser.read(5)
                    if data[0] == 0x55 and data[1] == 0x02 and data[4] == 0xaa:
                        temp = (data[2] << 8) | data[3]
                        timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
                        temp = temp / 100.0
                        with data_lock:
                            data_queue.put((timestamp, temp))
                        update_status(f"读取温度: {temp} °C")
                time.sleep(0.1)
            else:
                time.sleep(1)
    except Exception as e:
        update_status(f"错误: {str(e)}")
  • 持续从串口读取数据,并将解析后的温度数据和时间戳放入队列中。

Pygame 音频

# 初始化pygame混音器
pygame.mixer.init()

def play_sound(file):
    pygame.mixer.music.load(file)
    pygame.mixer.music.play()
  • 初始化 Pygame 混音器,用于播放音频文件。
  • 根据设定温度的范围播放相应的警告音频。

Dash 图表

def open_graph():
    app = Dash(__name__)

    app.layout = html.Div(
        children=[
            html.H1(children='实时温度监控'),
            dcc.Graph(id='live-graph'),
            dcc.Interval(
                id='interval-component',
                interval=1 * 1000,
                n_intervals=0
            )
        ]
    )

    @app.callback(Output('live-graph', 'figure'), [Input('interval-component', 'n_intervals')])
    def update_graph_live(n):
        with sqlite3.connect('temperature_data.db') as dash_conn:
            dash_cursor = dash_conn.cursor()
            dash_cursor.execute(f"SELECT * FROM {table_name} ORDER BY timestamp ASC LIMIT 500")
            rows = dash_cursor.fetchall()

        if not rows:
            return go.Figure()

        timestamps = [datetime.strptime(row[0], '%Y-%m-%d %H:%M:%S') for row in rows]
        start_time = timestamps[0]
        temperatures = [row[1] for row in rows]
        time_deltas = [(timestamp - start_time).total_seconds() for timestamp in timestamps]

        fig = go.Figure(go.Scatter(x=time_deltas, y=temperatures, mode='lines+markers', name='Temperature'))

        y_range = [min(temperatures) - 5, max(temperatures) + 5]

        fig.update_layout(
            title='实时温度监控',
            xaxis_title='时间 (s)',
            yaxis_title='温度',
            yaxis=dict(range=y_range),
            xaxis=dict(
                rangeslider=dict(visible=True),
                range=[max(0, max(time_deltas) - 20), max(time_deltas)]
            )
        )

        return fig

    dash_thread = Thread(target=lambda: app.run_server(debug=False, port=8050))
    dash_thread.daemon = True
    dash_thread.start()

    webbrowser.open('http://localhost:8050')
  • 使用 Dash 和 Plotly 实现实时温度监控图表。
  • 每秒更新一次数据,并显示在浏览器中。

Tkinter GUI

# GUI应用程序
root = tk.Tk()
root.title("温度控制系统")
root.configure(bg='#e0e0e0')

# 创建顶部控制框架
control_frame = ttk.Frame(root)
control_frame.pack(pady=20)

# 创建设定温度输入框及按钮
ttk.Label(control_frame, text="设定温度 (°C):").grid(row=0, column=0, padx=10, pady=10)
set_temp_entry = ttk.Entry(control_frame, width=10)
set_temp_entry.grid(row=0, column=1, padx=10, pady=10)

set_temp_button = ttk.Button(control_frame, text="发送设定", command=lambda: send_set_temp(float(set_temp_entry.get())))
set_temp_button.grid(row=0, column=2, padx=10, pady=10)

# 创建串口控制按钮
serial_control_frame = ttk.Frame(root)
serial_control_frame.pack(pady=20)

open_button = ttk.Button(serial_control_frame, text="打开串口", command=open_serial)
open_button.grid(row=0, column=0, padx=10, pady=10)

close_button = ttk.Button(serial_control_frame, text="关闭串口", command=close_serial)
close_button.grid(row=0, column=1, padx=10, pady=10)

# 创建状态标签
status_label = ttk.Label(root, text="等待命令...")
status_label.pack(pady=10)

# 创建实时数据显示框架
data_frame = ttk.Frame(root)
data_frame.pack(pady=20)

# 创建数据表格
columns = ("时间", "温度")
display_data = ttk.Treeview(data_frame, columns=columns, show="headings")
display_data.heading("时间", text="时间")
display_data.heading("温度", text="温度")
display_data.pack(pady=10)

# 创建历史数据查询按钮
query_button = ttk.Button(root, text="查询历史数据", command=query_data)
query_button.pack(pady=10)
# 添加“打开实时数据绘图”按钮
graph

_button = ttk.Button(root, text="打开实时数据绘图", command=open_graph)
graph_button.pack(pady=10)

# 设置主题
style = ttk.Style()
style.theme_use('clam')  # 选择主题
style.configure('TLabel', font=('Helvetica', 12))
style.configure('TButton', font=('Helvetica', 12))
style.configure('TEntry', font=('Helvetica', 12))

style.configure('Control.TFrame', background='#f0f0f0')
style.configure('Status.TLabel', background='#d0d0d0', font=('Helvetica', 12, 'bold'))

# 启动读取温度数据的线程
def start_reading():
    read_thread = Thread(target=read_temperature)
    read_thread.daemon = True
    read_thread.start()

# 更新显示
update_display()

# 运行主循环
root.mainloop()
  • 创建 Tkinter GUI 窗口,包括设置温度、打开/关闭串口、实时数据显示和查询历史数据的按钮和输入框。
  • 使用 ttk.Style 设置组件样式和主题,使界面更加美观。

总结

这个程序结合了多种技术,包括串口通信、SQLite 数据库、多线程、Pygame 音频、Dash 图表和 Tkinter GUI,实现了一个功能全面的温度控制系统。它能够设定温度、实时监控温度、查询历史数据,并通过图表直观显示温度变化。

  • 功能
    • 代码编辑和调试:提供智能代码编辑、语法高亮、自动补全、代码重构等功能,支持强大的调试工具。
    • 集成开发工具:集成了版本控制系统(如 Git)、测试框架、虚拟环境管理器等,支持完整的开发流程。
    • 插件和扩展:支持多种插件和扩展,能够自定义和扩展 IDE 的功能。

这些软件各自具备不同的功能,能够在恒温控制系统的开发过程中协同工作,实现从代码编写、仿真测试、串口通信调试到上位机界面开发的完整流程。


3. 下位机程序简单分析

好的,让我们详细分析这段用于下位机的代码。该代码实现了一个基于C51单片机的温度控制系统,其中包括PID控制算法、串口通信和数码管显示。

包含的头文件

#include <reg51.h>
#include <absacc.h>
  • reg51.h:这是8051系列单片机的寄存器定义头文件。
  • absacc.h:用于绝对地址访问的头文件,方便对外部数据存储器进行操作。

数据类型定义

#define u8  unsigned char
#define u16 unsigned int
#define u32 unsigned long  
  • 定义了常用的数据类型别名,方便代码书写和理解。

端口定义

#define DT_DA_PORT XBYTE[0xe400]     // 数码管数据端口
#define DT_DI_PORT XBYTE[0xe800]
#define PWM_OUT_PORT XBYTE[0xc400]   // 传感器及PWM输出端口
#define SPT_LOW_INPORT XBYTE[0xc100]
#define SPT_HIG_INPORT XBYTE[0xc200]
  • 定义了外部存储器地址,以绝对地址访问的方式操作这些端口。
    • DT_DA_PORTDT_DI_PORT:用于数码管显示。
    • PWM_OUT_PORT:用于PWM输出。
    • SPT_LOW_INPORTSPT_HIG_INPORT:用于读取传感器数据。

全局变量和常量

int SetValue;  // 温度设定值

// PID控制参数
float Kp = 0.5f;
float Ki = 0.017f;
float Kd = 0.30f;

// 误差变量
int et = 0;
int et_1 = 0;
int et_2 = 0;

// PID积分和微分项
float integral = 0.0f;
float derivative = 0.0f;

// PWM变量
static float pwm = 0.0f;  

// 显示缓冲区
u8 DispBuff[8] = {0, 0, 0, 0, 1, 1, 1, 7};

// 温度数据
u16 temperature = 0;

// 发送标志
bit send_flag = 0;
  • 定义了温度设定值、PID控制参数、误差变量、PID积分和微分项、PWM变量、显示缓冲区、温度数据和发送标志等全局变量。

定时器0初始化

void Timer0_Init(void) {
    TMOD |= 0x01;  // 设置定时器0为模式1 (16位定时器)
    TH0 = 0x3C;    // 设置初值以便定时 50ms (假设晶振频率为11.0592MHz)
    TL0 = 0xB0;
    ET0 = 1;       // 使能定时器0中断
    TR0 = 1;       // 启动定时器0
}
  • 初始化定时器0为模式1,设置定时器初值以便定时50ms,并使能中断。

串口初始化

void UART_Init(void) {
    SCON = 0x50;   // 设置串口为模式1 (8位UART)
    TMOD |= 0x20;  // 设置定时器1为模式2 (8位自动重装)
    TH1 = 0xFD;    // 波特率9600 (假设晶振频率为11.0592MHz)
    TL1 = 0xFD;
        
    TR1 = 1;       // 启动定时器1
    ES = 1;        // 使能串口中断
    EA = 1;        // 使能全局中断
}
  • 初始化串口为模式1,设置波特率为9600,启动定时器1,并使能串口中断和全局中断。

发送一个字节

void send_byte(u8 dat) {
    SBUF = dat;
    while (!TI);
    TI = 0;
}
  • 发送一个字节数据,通过串口发送缓冲区(SBUF),并等待发送完成。

发送温度数据

void send_temperature(void) {
    send_byte(0x55);  // 起始字节
    send_byte(0x02);  // 数据类型 (温度)
    send_byte((u8)(temperature >> 8));  // 高字节
    send_byte((u8)(temperature));       // 低字节
    send_byte(0xaa);  // 结束字节
}
  • 发送温度数据包,包含起始字节、数据类型、温度高低字节和结束字节。

更新显示

void update_display(void) {
    static u8 CurrentBit = 0;
    u8 SevenSegCode[10] = {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F};
    u8 SevenSegBT[8] = {0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80};

    DT_DI_PORT = 0;
    DT_DA_PORT = SevenSegCode[DispBuff[CurrentBit]];
    DT_DI_PORT = SevenSegBT[CurrentBit];

    CurrentBit++;
    if (CurrentBit >= 8) CurrentBit = 0;
}
  • 更新数码管显示,每次更新一个位。

读取传感器数据

u16 read_sensor(void) {
    u16 x;
    *((u8 *)&x + 1) = SPT_LOW_INPORT;
    *((u8 *)&x + 0) = SPT_HIG_INPORT;
    return x;
}
  • 读取传感器数据,合并高低字节。

限幅函数

float limit_value(float value, float min, float max) {
    if (value > max) return max;
    if (value < min) return min;
    return value;
}
  • 对输入值进行限幅,确保其在指定范围内。

更新显示缓冲区

void update_display_buffer(u16 value) {
    value = value / 100;  // 假设接收到的是放大100倍的值,需要先除以100
    DispBuff[4] = value / 1000;
    value %= 1000;
    DispBuff[5] = value / 100;
    value %= 100;
    DispBuff[6] = value / 10;
    DispBuff[7] = value % 10;
}
  • 更新显示缓冲区,按千位、百位、十位和个位分别赋值。

PID控制计算

void PID_Control(void) {
    float pid_output;
    u16 temp;

    temperature = read_sensor();  // 读取传感器数据

    et_2 = et_1;
    et_1 = et;
    et = SetValue - (int)temperature;  // 计算误差

    integral += et;  // 计算积分项
    integral = limit_value(integral, -1000, 1000);  // 应用积分限幅

    derivative = et - et_1;  // 计算微分项

    pid_output = Kp * et + Ki * integral + Kd * derivative;  // PID控制算法

    pwm += pid_output;  // 更新PWM值
    pwm = limit_value(pwm, 0, 255);  // PWM限幅

    PWM_OUT_PORT = (u8)pwm;  // 输出PWM

    temp = temperature;
    update_display_buffer(temp);  // 更新显示缓冲区
}
  • 实现PID控制算法,计算误差、积分和微分项,更新PWM值并输出。

处理接收到的数据包

void process_received_packet(u8 *buffer) {
    if (buffer[0] == 0x55 && buffer[1] == 0x01 && buffer[4] == 0xaa) {
        SetValue = (buffer[2] << 8) | buffer[3];  // 解析温度设定值
    }
}
  • 处理接收到的数据包,解析温度设定值。

串口中断服务函数

void serial_isr(void) interrupt 4 {
    static u8 rx_buffer[5];  // 接收缓冲区
    static u8 rx_index = 0;

    if (RI) {
        RI = 0;
        rx_buffer[rx_index++] = SBUF;

        if (rx_index == 5) {
            process_received_packet(rx_buffer);  // 处理接收到的数据包
            rx_index = 0; 

 // 重置索引
        }
    }
}
  • 串口中断服务函数,接收数据并处理。

定时器0中断服务函数

void timer0_isr(void) interrupt 1 {
    TH0 = 0x3C;  // 重装定时器初值
    TL0 = 0xB0;
    send_flag = 1;  // 设置发送标志
}
  • 定时器0中断服务函数,每50ms触发一次,设置发送标志。

主函数

void main(void) {
    u8 send_counter = 0;
    UART_Init();    // 初始化串口
    Timer0_Init();  // 初始化定时器0
    while (1) {
        PID_Control();  // 执行PID控制
        update_display();  // 更新显示

        if (send_flag) {
            send_counter++;
            if (send_counter >= 5) {  // 每5个周期发送一次
                send_temperature();
                send_counter = 0;
            }
            send_flag = 0;  // 重置发送标志
        }
    }
}
  • 主函数初始化串口和定时器0,并在主循环中执行PID控制、更新显示和发送温度数据。

通过这段代码,单片机可以实现温度的实时监控和控制,并通过PID算法调整输出PWM信号,以实现恒温控制。此外,通过串口通信,可以接收上位机发送的设定温度值,并定时发送当前温度数据到上位机。


Q & A 关于其中的一些问题

  • 在进行上位机的实验时,发现实时绘制的图表有时候总是有一两个点的顺序与采集的数据顺序不同,导致绘图出现逆差。

    后来发现是因为,我使用的是采集的数据先存储到数据库中,然后再从数据库中查询数据再进行温度曲线的绘制,问题出在了向数据库中插入数据这里,插入数据时,发生了插入顺序的混乱导致的,即异步,因此需要用到操作系统中学到的解决异步的方法,这里进行了加锁来解决的。

  • 在一开始,我的那个下位机

About

单片机专业综合实践-上位机下位机串口通信恒温控制系统

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published