Arduino与Python上位机联调:电子秤串口通信实战解析
简介面向全国大学生电子设计竞赛及山西省电子设计大赛的参赛者这份资料包围绕“电子秤”赛题提供完整的上位机与下位机实现方案。内容包含Arduino下位机数据采集程序与Python上位机监控程序能够覆盖称重信号读取、数据处理、界面展示与串口通信等核心环节帮助选手快速理解软硬件联调思路也可作为课程设计或竞赛准备的参考模板。包内共5个文件包括2个Python脚本分别承担界面交互与核心逻辑、1个Arduino下位机程序、1个一键启动批处理脚本和1张运行效果图压缩后仅9KB结构精简、便于快速部署。目前已有89人学习适合初步接触电赛、希望借助完整示例快速跑通流程的同学。所有程序均来自真实赛题实战调试经测试可直接运行。拿到后可以对照学习Arduino编程与Python上位机开发的关键写法弄清下位机如何采集传感器数据、上位机如何接收并展示数据配合批处理脚本还能一键启动节省环境配置时间从而把更多精力放在赛题原理与代码优化上对后续参加电子设计类竞赛大有帮助。1. 电子设计大赛里的双端工程值得拆开重看一遍电子设计大赛的题目每年都换但“电子秤”这类题几乎年年有队伍挑中原因很简单技术链路短但两头都得通。一头是 Arduino 下位机负责把应变式压力传感器的模拟信号通过 HX711 放大并转成数字量再换算成重量最后通过串口定时输出另一头是 Python 上位机负责读取串口、解析数据帧、把重量实时显示到窗口里遇到超限还要报警或记录。2018 年山西省电子设计大赛里这份“电子秤上位机 Python 程序及下位机 Arduino 程序”本质上就是这条链路的完整样例只是很多人下载后卡在了第一步——打不开、装不上库、串口连不上。对从业者来说这类比赛代码的价值不在“能称重”而在它把串口通信协议设计、数据解析、标定漂移处理、跨语言联调这些日常上位机开发的基本功压缩在一个几百行的小工程里。无论是接手别人的项目还是自己从零搭一套采集系统读完这套程序的思路都能少走弯路。下面按我平时做这类项目的顺序从上位机和下位机两条线分别展开讲清原理、给出可直接修改的代码和参数再把最容易出问题的联调细节单独拎出来说。2. Arduino 下位机从 HX711 读数到串口数据帧2.1 硬件接线与 HX711 的选型理由电子秤的核心是电阻应变式称重传感器输出的是毫伏级的差分电压信号Arduino 的 ADC 根本采不干净。HX711 是 24 位高精度 A/D 转换芯片专为桥式传感器设计内部集成了稳压电源和可编程增益放大器增益可选 32、64、128。常见的做法是用 128 增益配合 5V 供电的传感器把满量程电压放大到 Arduino 能识别的范围。接线顺序固定HX711 的 DT 接 Arduino 数字口 3SCK 接数字口 2VCC 接 5VGND 共地。传感器红黑接 E 和 E-白绿接 A 和 A-接反了读数会是负值这是新手上板后第一个要检查的点。HX711 的库很多最常用的是HX711.hbogde 版库函数里已经封装好了read()和get_units()。但比赛程序里往往不需要高层的get_units()直接调read()拿原始值反而更直观——标定系数可以完全掌控在上位机端。下面这个示例代码就是按“下位机只负责上报原始值”的思路写的这样上位机改标定参数时不用反复刷固件。2.2 下位机最小可用代码定时读取并串口输出#include HX711.h #define DOUT 3 #define SCK 2 HX711 scale; void setup() { Serial.begin(115200); scale.begin(DOUT, SCK); scale.set_scale(); // 先不做标定直接读原始值 scale.tare(); // 上电去皮读取当前值作为零点 } void loop() { // 串口输出两行一行原始值一行换算后的克数临时系数 long raw scale.read(); float weight (raw - scale.get_offset()) / 430.0f; // 430 为临时标定系数 Serial.print(raw); Serial.print(,); Serial.println(weight, 2); // 保留两位小数输出 delay(200); // 5Hz 刷新避免串口拥塞 }代码里的430.0f只是示例系数真实标定必须用砝码测出来。delay(200)控制上报频率比赛要求通常不高5Hz 足够如果你做的是动态称重需要把 delay 降到 50ms 以内但要注意串口缓冲区的溢出风险。这套代码的逻辑是“上位机要什么下位机就给什么”不做过多的本地判断协议简单后续改上位机时下位机一次都不用动。2.3 串口协议设计为什么建议用“帧头 数据 校验”比赛程序里最常见的串口输出就是Serial.println(raw)这种裸数据测试时没问题但一旦上位机打开串口的时间不对读到半个数据行解析就乱了。更稳的做法是自定义一个轻量协议例如每帧固定格式$W,12345,28.75*AB\n其中$W是帧头12345是原始 ADC 值28.75是换算后的克数*AB是校验字节。上位机只需要在缓冲区里找$W和\n中间内容做一次异或校验非法帧直接丢弃这就规避了串口粘包和半包的绝大部分问题。校验算法不复杂从$之后到*之前的所有字符逐个异或结果转成两位大写十六进制。下位机改造成本只有十几行但换来的是上位机解析代码可以简化一个数量级。做比赛项目时你可能觉得没必要但如果你之后做 BMS 上位机或工业采集项目这个习惯会省下大量排查脏数据的时间。3. Python 上位机串口读取、数据解析与显示3.1 串口库选型与虚拟串口调试Python 侧读写串口的标准库是pyserial安装命令是pip install pyserial。选它的理由没有悬念跨平台、API 稳定、安装不会有编译失败的问题。另一个常被提到的库是serial那是旧版包名已经废弃用pip install serial装到的是另一个不相关的串口工具库这坑很多人踩过。如果你用 VS Code 做开发记得在.vscode/settings.json里指定解释器路径否则终端里 pip 装好了F5 运行时用的还是系统自带 Python会报ModuleNotFoundError。调试阶段如果手头没有 Arduino 板子可以用虚拟串口工具Windows 上是 com0comLinux 上可以用socat创建一对互联的虚拟串口然后用任意串口助手往其中一个端口定时发模拟数据帧Python 程序去读另一个端口。这样就能在不依赖硬件的情况下把上位机逻辑跑通。下面这段代码演示了如何枚举串口并打开设备。import serial import serial.tools.list_ports # 列出所有可用串口打印设备名和描述 ports serial.tools.list_ports.comports() for p in ports: print(p.device, p.description) # 打开一个串口超时设为 0.5 秒 ser serial.Serial( portCOM3, # Windows 示例Linux 下通常是 /dev/ttyUSB0 baudrate115200, # 必须与 Arduino 端 Serial.begin 一致 bytesizeserial.EIGHTBITS, parityserial.PARITY_NONE, stopbitsserial.STOPBITS_ONE, timeout0.5 )参数里最容易出错的是波特率Arduino 端Serial.begin(115200)和 Python 端baudrate115200必须严格一致差一个数字都是乱码。timeout0.5表示读串口时最多阻塞 0.5 秒没有数据就返回空主线程不会被卡死。bytesize、parity、stopbits三个参数保持默认 8N1 即可这是串口通信的绝对主流配置。3.2 读取线程与数据帧解析的代码骨架上位机实时显示有两条路一是while True循环里阻塞读串口界面用after或定时器刷新二是专门的读取线程把解析好的数据通过队列发给界面线程。推荐后者因为 PyQt5 的界面主循环一旦被串口阻塞窗口就会假死拖拽、缩放全部无响应。下面是一个生产级的最小读取线程。import threading import queue import serial class SerialReader(threading.Thread): def __init__(self, port, baudrate, data_queue): super().__init__() self.ser serial.Serial(port, baudrate, timeout0.2) self.data_queue data_queue self.running True def run(self): while self.running: line self.ser.readline() # 按行读取遇到 \n 返回 if len(line) 0: continue try: # 假设下位机格式: raw,weight\n text line.decode(utf-8).strip() raw_str, weight_str text.split(,) payload { raw: int(raw_str), weight: float(weight_str) } self.data_queue.put(payload) # 放入队列主界面取走 except ValueError: pass # 丢弃损坏帧 def stop(self): self.running False self.ser.close() # 使用方式 q queue.Queue() reader SerialReader(COM3, 115200, q) reader.start()readline()依赖换行符\n所以下位机必须用println输出如果下位机用的是print末尾带\r\n也没问题strip()会去掉首尾空白字符。ValueError捕获很关键——串口刚打开时缓冲区里可能有半截历史数据解析失败直接丢弃而不是让整个线程崩溃。如果你在 Arduino 端只发送Serial.print(weight)而不带换行readline()就会一直等下去超时后返回空字符串这是 readline 模式的典型隐患。3.3 PyQt5 界面数据刷新与超限提醒界面部分不必做得复杂QLabel 显示实时重量、QLineEdit 显示原始 ADC 值、QPushButton 做去皮归零再加一个阈值输入框就够了。数据从队列取出来后用自定义信号把 payload 传给槽函数更新控件避免跨线程直接操作 UI。下面这段是信号槽的关键片段。from PyQt5.QtCore import QThread, pyqtSignal class WeightBridge(QThread): weight_updated pyqtSignal(int, float) def __init__(self, reader, data_queue): super().__init__() self.reader reader self.queue data_queue def run(self): while True: try: payload self.queue.get(timeout0.5) self.weight_updated.emit(payload[raw], payload[weight]) except queue.Empty: pass # 槽函数里只做 UI 更新 def on_weight(raw, weight): if weight threshold: label.setStyleSheet(color: red;) else: label.setStyleSheet(color: black;) label.setText(f{weight:.2f} g)这个结构的价值在于把串口 I/O、数据处理、UI 刷新三层彻底解耦。后续你想加数据记录、曲线绘制或者数据库存储都只需要在槽函数里追加代码不必动读取线程。调试时如果界面卡顿优先检查是不是在槽函数里做了耗时操作——比如写了 Excel 或发了网络请求这些都要挪到单独的线程去处理。4. 上下位机联调串口参数、标定系数与常见故障排查4.1 联调前必须确认的三个一致性上下位机合在一起最先暴露的问题几乎都出在“不一致”上。第一是波特率一致下位机Serial.begin(9600)、上位机写115200出来的数据全是乱码。第二是数据格式一致下位机输出raw,weight\n上位机解析时就得按逗号分割同时要容忍浮点数的多种写法28.75和28.750都要能解析。第三是串口号正确Windows 设备管理器里看到的 COM 号和 Python 代码里写的 COM 号必须一致插拔 USB 后 COM 号还会变上位机里最好提供下拉选择框而不是硬编码。下面这张表可以把联调初期的状态快速对上号现象原因排查方向串口打不开被串口助手或调试工具占用关闭所有占用程序换一个 COM 号读到乱码波特率不一致确认两端都是 115200 或 9600有数据但不更新换行符不匹配检查下位机是否用println或手动加\n重量跳变很大供电不稳或没有滤波改用独立电源加滑动平均滤波数值为负传感器接线反了交换传感器 A 和 A-4.2 标定系数的确定方法下位机代码里的430.0f只是占位符真实系数需要两步标定。第一步把空秤上的传感器读数记录为零点这就是tare()干的事第二步放一个已知质量的砝码比如 500g记录 ADC 原始值然后用“质量 ÷当前原始值 - 零点原始值”算出系数。公式看起来简单但实际操作里有几个坑砝码必须放在秤台正中央因为偏心加载会让传感器四个桥臂受力不均每次标定前要等示值稳定去皮后至少等 3 秒再放砝码换电源、换传感器线、改增益系数都会变必须重新标定。如果你希望上位机能直接标定可以在上位机加一个“输入已知重量”的对话框上位机读取当前平均原始值后把计算好的系数通过串口下发给下位机下位机存到 EEPROM 里。这样每次重刷固件也不用重新标定。比赛程序一般不做到这个程度但你要是想拿去接实际项目这一步很加分。4.3 故障快速定位的三板斧第一板斧是把下位机直接扔到串口助手如 PuTTY、Arduino 自带的串口监视器里看输出。串口助手能收到正确数据说明下位机没问题问题在上位机代码。第二板斧是用 Python 的repr()打印读到的原始字节而不是直接打印解码后的字符串因为\n、\r、空格这类不可见字符会严重影响解析repr()能让你一眼看出格式问题。第三板斧是关掉所有可能抢占串口的程序——浏览器插件、串口调试工具、甚至微信小程序Windows 下串口是独占设备一个进程占住别的进程就打不开报错信息还特别容易误导你。line ser.readline() print(repr(line)) # 输出 b12345,28.75\r\n 这种形式方便看尾部如果你是用 Windows 开发出现了PermissionError或SerialException: could not open port九成是端口被占用。把调试工具全部关掉或者重新拔插 USB 让端口释放问题立刻解决。Linux 下则要检查当前用户是否在dialout用户组里不在组里会报权限错误执行sudo usermod -a -G dialout $USER并重新登录即可。4.4 数据漂移和零点偏移怎么处理电子秤最大的敌人是漂移漂移分两类一是零点漂移表现为空载时显示从 0 慢慢涨到 1.5g这通常是温度影响或 HX711 电源纹波太大二是增益漂移表现为放同一个砝码早晚读数差出好几克这往往是传感器本身特性。前者可以在下位机做滑动平均滤波每 5 次读数取一次平均或者动态去皮——每隔几秒把低于最小阈值的读数强制归零。后者只能靠经常标定或升级到更高精度的传感器软件层面能做的补救是限幅滤波超过上次值 30% 的跳变视为干扰直接丢帧这样至少不会出现瞬时毛刺把界面刷新搞乱。滑动平均滤波的窗口长度不建议超过 10窗口太大会让重量变化响应变慢倒东西或放砝码时读数会“拖尾”。对于比赛要求“响应快”的场景我一般用 3 到 5 点平均已经能把 HX711 的高频噪声压得比较干净。你要是在调试时发现数值已经稳定但显示还是跳优先检查是不是接地问题——Arduino 的 USB 供电和 HX711 的传感器供电共地有时候电源噪声叠加到参考电压上读数就会周期性抖动。5. 实战让这套程序达到“可直接演示”的状态5.1 用 Wokwi 做纯软件仿真验证很多人拿到比赛程序后手上并没有 Arduino 板和传感器卡在硬件这一步。Wokwi 仿真平台可以直接在浏览器里模拟 Arduino Uno 加 HX711 加应变片的组合还能虚拟串口输出到网页控制台。虽然 Wokwi 对 HX711 的模拟精度和真实芯片有差距但用来验证数据帧格式、串口时序、上位机解析逻辑完全够用。常见做法是在 Wokwi 里加载 HX711 示例工程修改代码里的串口输出部分然后看虚拟串口的输出是否符合上位机的协议预期。Wokwi 不支持直接对外暴露虚拟串口给本机 Python 程序使用最终的联调还是得靠真实串口但至少能在写上位机代码前先把下位机的协议设计想清楚。5.2 串口自动重连与断线保护比赛现场最尴尬的场景是 Arduino 突然被碰掉 USB 线上位机直接崩溃或者失去响应。成熟的方案是给串口读取线程增加断线重连机制每次读写抛出异常时关闭当前串口每隔 2 秒尝试重新打开直到成功。配合界面上的连接状态指示灯观众能明确知道系统处于离线还是在线状态而不是看着一个死掉的窗口发呆。def reconnect_loop(self): while not self.stop_event.is_set(): try: self.ser serial.Serial(self.port, self.baudrate, timeout0.2) return True except serial.SerialException: time.sleep(2) # 2 秒后重试 return False这段代码要放在读取线程的异常处理分支里捕获到SerialException或OSError时就进入重连循环。重连成功后注意要把串口缓冲区的旧数据清掉self.ser.reset_input_buffer()可以防止重连的一瞬间读到半截残帧。如果你在处理的是质量相关数据重连后还必须自动去皮一次否则残留的重量会叠加到下一次读数里。5.3 数据记录与曲线回溯上位机如果只显示实时重量演示效果比较单薄。我一般会加一个轻量级的数据记录功能在槽函数里把(timestamp, weight)追加写入 CSV 文件文件按日期命名每次启动程序新建一个文件用 Excel 可以直接打开核对数据趋势。这个功能用 Python 标准库csv就能实现不需要引入 pandas 或数据库比赛和演示场景足够用了。import csv import time from datetime import datetime def record_to_csv(weight): filename datetime.now().strftime(%Y%m%d) .csv with open(filename, a, newline) as f: writer csv.writer(f) writer.writerow([time.strftime(%H:%M:%S), f{weight:.2f}]) # 在 weight_updated 信号槽里调用 record_to_csv(weight)5.4 演示前五分钟检查清单比赛或验收前按这个顺序过一遍确认下位机供电线牢固USB 线不松动打开串口助手看下位机输出是否稳定原始值跳变幅度小于 3 个 ADC 计数把砝码放在秤盘上观察上位机读数是否在 1 秒内稳定按下去皮键后读数是否为 0.00关闭上位机程序重新打开串口能否立即重连成功。这几项全部通过系统在演示环境里基本不会翻车。这套检查清单同样适用于你之后接手的任何串口采集类项目因为电子秤项目的结构就是上下位机项目的通用骨架把称重换成温度、湿度、电压采集思路完全一样。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →