学完DS18B20之后的小项目:基于51单片机的室温监视上位机(完整教程+含源码分享)

发布时间:2026/8/13 8:50:47
学完DS18B20之后的小项目:基于51单片机的室温监视上位机(完整教程+含源码分享) 文章目录项目简介软硬件清单与环境搭建核心原理与系统设计运行调试与踩坑总结效果展示与完整源码获取项目复盘与进阶思考正文一、项目简介跟着江协科技学完51单片机之后想通过做点小项目来检验一下成果。于是运用DS18B20温度传感器基于51单片机检测室温然后在通过上位机电脑基于Python绘制出室温曲线。二、软硬件清单与环境搭建1.硬件普中51单片机我使用的是普中51单片机A2单片机上面集成了DS18B20温度传感器等等数据线。2.软件keil5C语言stc-isp(单片机烧录软件)VS code(Python)3.环境配置在VS code上配置虚拟环境在里面先下载绘制温度曲线所需要用到的库函数。需要的库函数有tkinter,matplotlib,serial,csv.在终端分别输入pip install (库函数)等待下载成功就可以了三、核心原理与系统设计核心原理单片机将物理世界的温度模拟/数字量通过串口以文本协议上传Python上位机通过轮询解析文本实现数据可视化与持久化。整体的流程采集单片机→发送串口→接收电脑→绘图Python→存储四、运行调试与踩坑总结1、一定要在烧录软件上面看清楚串口有的是COM3有的是COM52、要先将C语言程序烧录进单片机当中然后关闭烧录软件不然会占用串口。3、如果要使做出来的UI界面美观一定要注意图的排版布局管理pack与side4、每做完一步要进行调试避免最终运行调试时找不出bug来源5、不懂可询问AI善用AI五、效果展示与完整源码获取这是先在烧录软件上调试右边接收缓冲区有温度数据不断呈现这是主程序的代码截图这个主程序需要模块化编程的支持我在这里就不显示了#include REGX52.H #include stdio.h #include Delay.h #include DS18B20.h #include UART.h void main() { float T; char send_buf[20]; UART_Init(); DS18B20_ConvertT(); Delay(1000); while(1) { // 1. 启动温度转换 DS18B20_ConvertT(); Delay(1000); // 等待至少750ms这里给1000ms保险 // 2. 读取温度值 T DS18B20_ReadT(); // 3. 格式化为字符串例如 25.50\n sprintf(send_buf, %.2f\r\n, T); // 4. 通过串口发送给电脑 UART_SendString(send_buf); // 5. 间隔500ms再发下一次 Delay(500); } }接下来是温度曲线的绘制以及UI界面设计的代码import tkinter as tk from tkinter import messagebox, filedialog from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from matplotlib.figure import Figure import serial import csv from datetime import datetime import matplotlib.pyplot as plt # 解决中文乱码 # 设置matplotlib使用支持中文的字体Windows系统通常有SimHei plt.rcParams[font.sans-serif] [SimHei] # 用来正常显示中文标签 plt.rcParams[axes.unicode_minus] False # 用来正常显示负号 # 配置区 SERIAL_PORT COM5 # 改成你的实际端口号 BAUDRATE 9600 # 全局变量 ser None start_time None # 记录开始时刻 timer_running False # 计时器是否在运行 is_running False data_list [] time_list [] # 创建主窗口 root tk.Tk() root.configure(bg#0D2644) root.title(51单片机 室温监控上位机) root.geometry(820x600) # ---- 时间显示栏 ---- time_frame tk.Frame(root, bg#2B5B84) # 背景色与窗口一致 time_frame.pack(sidetk.TOP, filltk.X, pady(10, 0)) time_label tk.Label( time_frame, font(Arial, 18, bold), fgwhite, bg#000103, text2024-01-01 00:00:00 ) time_label.pack() # 控制栏 top_frame tk.Frame(root) top_frame.pack(sidetk.TOP, filltk.X, pady5) tk.Label(top_frame, text当前温度:, font(Arial, 14)).pack(sidetk.LEFT, padx5) temp_label tk.Label(top_frame, text-- ℃, font(Arial, 16, bold), fgblue) temp_label.pack(sidetk.LEFT, padx10) tk.Label(top_frame, text次数:, font(Arial, 12)).pack(sidetk.LEFT, padx(10,0)) count_label tk.Label(top_frame, text0, font(Arial, 12, bold), fgpurple) count_label.pack(sidetk.LEFT, padx2) tk.Label(top_frame, text平均:, font(Arial, 12)).pack(sidetk.LEFT, padx(10,0)) avg_label tk.Label(top_frame, text-- ℃, font(Arial, 12, bold), fgorange) avg_label.pack(sidetk.LEFT, padx2) # 创建图表 fig Figure(figsize(8, 4), dpi100) ax fig.add_subplot(111) ax.set_title(实时室温曲线) ax.set_xlabel(采集次数) ax.set_ylabel(温度 (°C)) ax.grid(True) line, ax.plot([], [], r-, linewidth2) canvas FigureCanvasTkAgg(fig, masterroot) canvas.get_tk_widget().pack(sidetk.TOP, filltk.BOTH, expandTrue) # 秒表显示栏 bottom_frame tk.Frame(root, bg#0D2644) # 与窗口背景一致 bottom_frame.pack(sidetk.BOTTOM, filltk.X, pady10) timer_label tk.Label( bottom_frame, text⏱️ 已采集: 00:00:00, font(Arial, 16, bold), fg#FFD700, # 亮金色 bg#0D2644 ) timer_label.pack() # ----- 按钮回调函数 ----- def start_collect(): global ser, is_running, data_list, time_list if is_running: return try: # 打开串口 ser serial.Serial(SERIAL_PORT, BAUDRATE, timeout0.1) is_running True global start_time, timer_running start_time datetime.now() # 记录开始时刻 timer_running True # 启动计时器 timer_label.config(text⏱️ 已采集: 00:00:00) # 重置显示 data_list.clear() time_list.clear() line.set_data([], []) ax.relim() ax.autoscale_view() canvas.draw() status_label.config(text状态: 采集中..., fggreen) # 启动更新循环 update_plot() print(f✅ 成功打开串口 {SERIAL_PORT}开始采集...) except Exception as e: messagebox.showerror(错误, f无法打开串口 {SERIAL_PORT}\n请检查端口号或单片机连接\n错误:{e}) print(f❌ 打开串口失败: {e}) def stop_collect(): global ser, is_running if is_running: is_running False global timer_running timer_running False # 停止计时保留当前显示 if ser and ser.is_open: ser.close() status_label.config(text状态: 已停止, fgred) if len(data_list) 0: ax.set_xlim(0, len(data_list)) canvas.draw() if len(data_list) 0 and messagebox.askyesno(保存数据, f共采集 {len(data_list)} 条数据导出CSV): export_csv() print(f⏹ 已停止采集共 {len(data_list)} 条数据) def export_csv(): if not data_list: return default_name f温度记录_{datetime.now().strftime(%Y%m%d_%H%M%S)}.csv file_path filedialog.asksaveasfilename( defaultextension.csv, filetypes[(CSV文件, *.csv)], initialfiledefault_name ) if file_path: with open(file_path, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([序号, 时间, 温度(℃)]) for i, (t, temp) in enumerate(zip(time_list, data_list)): writer.writerow([i1, t, f{temp:.2f}]) messagebox.showinfo(成功, f已保存至 {file_path}) print(f 数据已导出至 {file_path}) # ----- 按钮 ----- tk.Button(top_frame, text▶ 开始采集, commandstart_collect, bglightgreen, font(Arial, 12)).pack(sidetk.LEFT, padx10) tk.Button(top_frame, text⏹ 停止采集, commandstop_collect, bglightcoral, font(Arial, 12)).pack(sidetk.LEFT, padx10) tk.Button(top_frame, text 手动导出, commandexport_csv, bglightyellow, font(Arial, 12)).pack(sidetk.LEFT, padx10) status_label tk.Label(top_frame, text状态: 未连接, font(Arial, 12), fggray) status_label.pack(sidetk.RIGHT, padx20) # 核心更新函数增强异常打印 def update_plot(): global ser, is_running, data_list, time_list if not is_running: return if ser and ser.is_open: try: raw ser.readline() if raw: # 尝试解码并转为浮点数 decoded raw.decode(utf-8).strip() val float(decoded) # 成功解析存入数据 data_list.append(val) time_list.append(datetime.now().strftime(%H:%M:%S)) # 更新计数和平均值 count_label.config(textstr(len(data_list))) if len(data_list) 0: avg sum(data_list) / len(data_list) avg_label.config(textf{avg:.2f} ℃) temp_label.config(textf{val:.2f} ℃) line.set_data(range(len(data_list)), data_list) ax.relim() ax.autoscale_view() canvas.draw() # 如果没有读到数据什么也不做静默等待 except UnicodeDecodeError as ude: print(f⚠️ 解码错误: {ude}原始数据: {raw}) except ValueError as ve: print(f⚠️ 数据转换错误: {ve}原始内容: {raw}) except Exception as e: print(f❌ 未知错误: {e}) # 每100ms再次调用 root.after(100, update_plot) # 每秒刷新一次 # 窗口关闭处理 def on_closing(): global is_running, ser is_running False if ser and ser.is_open: ser.close() root.destroy() # 时间显示 def update_time(): now datetime.now().strftime(%Y-%m-%d %H:%M:%S) time_label.config(textnow) root.after(1000, update_time) root.protocol(WM_DELETE_WINDOW, on_closing) # 计时器 def update_timer(): global start_time, timer_running if timer_running and start_time is not None: elapsed datetime.now() - start_time # 格式化为 时:分:秒 total_seconds int(elapsed.total_seconds()) hours total_seconds // 3600 minutes (total_seconds % 3600) // 60 seconds total_seconds % 60 timer_label.config(textf⏱️ 已采集: {hours:02d}:{minutes:02d}:{seconds:02d}) # 每秒更新一次 root.after(1000, update_timer) # 启动 update_time() # 启动时间更新 update_timer() # 启动秒表更新循环 root.mainloop()最后是展示效果在中间突变是因为用手捏住DS18B20传感器六、项目复盘与进阶思考1. “上位机”和“下位机”的架构思维下位机51单片机只管采集和发送。不关心显示、不关心存储只负责在固定的时间间隔把数据丢出去。这体现了单一职责原则。上位机Python只管接收和处理。不关心传感器怎么读时序只负责把收到的数字画成图、算出平均值、存成文件。2. “通信协议”的本质协议设计定的协议是温度值\n纯文本换行结尾。学会了工程取舍——在资源充裕的场景下牺牲一点效率换取可维护性是可行的。3. 异步与缓冲单片机每1.5秒生产一个数据慢。Python每0.1秒去串口缓冲区消费一次快。得益于串口硬件自带的缓冲区只要上位机轮询速度大于生产速度就不会丢数据。你不需要去改单片机的延时只需要让Python轮询间隔100ms小于生产间隔1500ms即可。4.其实还有很多不足和需要学习完善的地方这个小项目还有进阶完善的空间可以使UI界面更加好看数据获取的速度更快绘制的速度更快等等。记录一下第一次小项目第一次写博客