资讯详情

资讯详情

Python上位机开发实战:pyserial与tkinter串口通信控制LCD1602

1. 项目缘起与整体设计思路1.1 为什么要自己写一个上位机市面上现成的串口调试助手一抓一大把随便搜一下就有十几种功能从基础的收发到协议解析、波形绘制应有尽有。那我为什么还要自己写一个原因其实很朴素现成工具要么功能太重、界面花哨要么就是缺少我想要的那几个特定功能比如一键发送自定义指令集、自动记录带时间戳的日志、按固定格式解析LCD1602的返回数据。每次调试都要在好几个软件之间来回切换效率很低。自己写上位机的第一个好处是完全可控。我想让按钮长什么样就长什么样想加什么快捷指令就加什么快捷指令想把日志存成什么格式就存成什么格式。第二个好处是学习成本其实很低。Python的pyserial库封装得非常好核心API就那么几个一个下午就能把基本收发跑通。第三个好处是可复用性强。这次是跟LCD1602通信下次换成别的串口设备改改协议解析部分就能直接用。这个项目适合谁呢如果你正在做单片机相关的开发手头有LCD1602液晶屏需要调试或者你刚接触串口通信想找个练手项目再或者你厌倦了每次都用别人的工具想自己造一个轮子那这个项目就挺适合你的。不需要你有多深的Python功底会基本的语法、能看懂函数定义就行。1.2 整体架构怎么搭整个系统的架构其实很简单就是PC端Python程序 USB转串口模块 单片机 LCD1602这条链路。PC端负责发送指令和显示接收到的数据USB转串口模块负责电平转换和协议转换单片机负责解析指令并驱动LCD1602显示对应内容。我选择用分层设计的思路来组织代码主要分三层串口通信层封装pyserial的打开、关闭、读写操作处理超时和异常协议解析层定义指令格式负责把用户操作翻译成字节流也负责把接收到的字节流解析成有意义的信息界面交互层用tkinter搭建简单的GUI提供按钮、输入框、日志显示区域这样分层的好处是如果以后要换界面框架比如从tkinter换成PyQt只需要改界面层串口和协议部分完全不用动。同样如果要换通信协议也只改协议层就行。1.3 为什么选pyserial和tkinterPython下串口通信的库有好几个pyserial是事实上的标准选择。它的API设计非常直观serial.Serial()打开串口write()发送read()读取基本上看名字就知道怎么用。跨平台支持也好Windows、Linux、macOS都能跑代码几乎不用改。社区活跃遇到问题搜一下基本都有答案。界面框架选tkinter主要图它零依赖。Python标准库自带不用额外pip install打包成exe也方便。虽然界面美观度比不上PyQt但对于一个调试工具来说功能到位比好看更重要。而且tkinter的布局管理器用熟了之后做这种简单的工具界面效率很高。提示如果你打算把工具分发给同事用tkinter打包出来的exe体积会小很多PyQt打包动辄上百MBtkinter通常二三十MB就搞定了。1.4 通信协议怎么定协议这部分是我踩坑最多的地方。一开始想得太复杂定义了一套带帧头、长度、命令字、数据、校验、帧尾的完整协议结果调试的时候光对齐字节就花了大半天。后来简化成文本指令 换行符结尾的格式调试效率一下子提上来了。最终的协议格式是这样的指令类型格式示例说明清屏CLR\nCLR\n清除LCD显示显示文本TXT:行号:内容\nTXT:0:Hello\n在第0行显示Hello设置光标CUR:行:列\nCUR:1:5\n光标移到第1行第5列背光控制BL:0/1\nBL:1\n打开背光查询状态STA?\nSTA?\n返回当前状态用文本格式的好处是人眼可读。调试的时候直接看串口助手里的原始数据一眼就能看出指令对不对不用去翻协议文档对照十六进制。坏处是传输效率比二进制低但对于LCD1602这种低速设备来说这点开销完全可以接受。2. 核心细节解析与实操要点2.1 pyserial的核心API与参数配置pyserial用起来简单但有几个参数如果配错了后面会出各种奇怪的问题。先把最关键的几个参数说清楚。打开串口的典型代码import serial ser serial.Serial( portCOM3, baudrate9600, bytesizeserial.EIGHTBITS, parityserial.PARITY_NONE, stopbitsserial.STOPBITS_ONE, timeout1 )port参数在Windows下是COMxLinux下是/dev/ttyUSBx或/dev/ttyACMx。这里有个坑Windows下COM口的编号不是固定的换个USB口插可能就从COM3变成COM5了。我的做法是在程序里做一个端口扫描把所有可用端口列出来让用户选。baudrate波特率必须和单片机端一致这个不用多说。LCD1602本身不关心波特率但单片机端的串口初始化必须匹配。9600是最常用的如果传输数据量大可以上115200。timeout这个参数很关键。默认是None意味着read()会一直阻塞直到读到指定数量的字节。这在GUI程序里是致命的会把界面卡死。我设成1秒配合非阻塞的读取逻辑界面就不会卡。bytesize、parity、stopbits这三个一般用默认值就行8N1是绝大多数场景的标准配置。除非你的单片机端特意改了否则不用动。2.2 串口数据的读取策略读取串口数据有两种常见方式轮询和多线程。轮询就是在主循环里不断检查ser.in_waiting有数据就读。多线程就是开一个单独的线程专门负责读串口读到数据后通过队列传给主线程。我一开始用的是轮询在tkinter里用after()方法定时调用读取函数。这种方式简单但有个问题如果数据来得比较密集轮询间隔设长了会丢数据设短了又占CPU。后来改成多线程 队列的方案读取线程只管往队列里塞数据主线程定时从队列取数据更新界面两边互不干扰。import threading import queue data_queue queue.Queue() def serial_reader(ser, q): while ser.is_open: try: if ser.in_waiting 0: data ser.read(ser.in_waiting) q.put(data) else: time.sleep(0.01) except serial.SerialException: break注意读取线程里不要直接操作GUI组件tkinter不是线程安全的。所有界面更新都必须通过队列回到主线程来做。2.3 LCD1602的驱动要点LCD1602是16列2行的字符型液晶驱动芯片一般是HD44780兼容的。它支持4位和8位两种数据模式4位模式省IO口但时序稍复杂8位模式占用IO多但编程简单。我用的8位模式因为手头单片机IO够用。LCD1602的显存地址和屏幕位置的对应关系是新手最容易搞混的地方显示位置显存地址第1行第1列0x80第1行第16列0x8F第2行第1列0xC0第2行第16列0xCF写指令的时候地址要加上0x80第1行或0xC0第2行。比如要在第2行第3列显示字符地址就是0xC0 2 0xC2。还有一个坑是初始化时序。LCD1602上电后需要等待一段时间才能接收指令通常建议延时50ms以上。初始化序列也有固定顺序不能随便改。我用的初始化流程是延时50ms → 功能设置 → 延时5ms → 功能设置 → 延时1ms → 功能设置 → 显示开关控制 → 清屏 → 输入模式设置。2.4 指令解析的状态机设计单片机端接收串口数据是不定长的可能一次收到完整指令也可能分几次收到半截指令。所以需要一个状态机来逐字节处理遇到换行符才认为一条指令结束。状态机设计很简单就两个状态接收中和指令完整。用一个缓冲区存收到的字节每收到一个字节就检查是不是\n是的话就把缓冲区内容拿去解析然后清空缓冲区。#define BUF_SIZE 64 char rx_buf[BUF_SIZE]; uint8_t rx_idx 0; void uart_rx_isr(uint8_t byte) { if (byte \n) { rx_buf[rx_idx] \0; parse_command(rx_buf); rx_idx 0; } else if (rx_idx BUF_SIZE - 1) { rx_buf[rx_idx] byte; } }这个逻辑放在串口接收中断里主循环只管解析和执行。缓冲区大小设64字节足够了LCD1602一行才16个字符指令不会太长。3. 实操过程与核心环节实现3.1 环境搭建与依赖安装PC端需要安装Python 3.7以上版本然后装pyserialpip install pyserialtkinter是标准库自带的不用额外装。如果你的Python是最小化安装的可能需要单独装一下tk# Ubuntu/Debian sudo apt-get install python3-tk # CentOS/RHEL sudo yum install tkinterWindows下一般自带tk不用管。验证安装是否成功import serial import serial.tools.list_ports import tkinter print(serial.VERSION) print([p.device for p in serial.tools.list_ports.comports()])能打印出版本号和端口列表就说明环境没问题。单片机端我用的是一款常见的8位单片机开发环境是Keil。串口初始化和LCD驱动都是标准操作这里不展开。重点说一下串口接收中断的配置这是整个通信链路的关键。3.2 上位机界面布局与功能实现界面我做得比较简洁主要分三个区域顶部串口选择下拉框、波特率选择、打开/关闭按钮中部指令输入区包含几个快捷按钮清屏、背光开、背光关和一个自定义指令输入框底部日志显示区带时间戳可滚动用tkinter的grid布局管理器来排布比pack更灵活。关键代码结构class SerialTool: def __init__(self, root): self.root root self.ser None self.reader_thread None self.data_queue queue.Queue() self.setup_ui() self.refresh_ports() self.root.after(100, self.process_queue) def setup_ui(self): # 串口选择区 frame_top tk.Frame(self.root) frame_top.pack(filltk.X, padx5, pady5) tk.Label(frame_top, text端口:).pack(sidetk.LEFT) self.port_var tk.StringVar() self.port_combo ttk.Combobox(frame_top, textvariableself.port_var, width10) self.port_combo.pack(sidetk.LEFT, padx5) # ... 其他控件process_queue方法定时从队列取数据更新日志区用after(100, ...)实现定时循环。这样界面刷新和串口读取就解耦了。3.3 指令发送与日志记录发送指令的流程是从输入框获取文本 → 编码成字节 → 加上换行符 → 通过串口发送 → 记录到日志。def send_command(self, cmd): if not self.ser or not self.ser.is_open: messagebox.showwarning(提示, 请先打开串口) return try: data (cmd \n).encode(ascii) self.ser.write(data) self.log(f[发送] {cmd}) except serial.SerialException as e: self.log(f[错误] 发送失败: {e})日志记录我加了时间戳格式是[HH:MM:SS.mmm]精确到毫秒。调试的时候时间戳很有用能看出指令之间的间隔也能判断单片机响应是否及时。def log(self, msg): ts datetime.now().strftime(%H:%M:%S.%f)[:-3] self.log_text.insert(tk.END, f[{ts}] {msg}\n) self.log_text.see(tk.END)see(tk.END)让日志区自动滚动到最新一行这个细节很提升使用体验。3.4 单片机端指令解析与LCD驱动单片机端的主循环逻辑很清晰检查是否有完整指令 → 解析指令类型 → 执行对应操作。void parse_command(char *cmd) { if (strncmp(cmd, CLR, 3) 0) { lcd_clear(); } else if (strncmp(cmd, TXT:, 4) 0) { uint8_t row cmd[4] - 0; char *content cmd 6; lcd_set_cursor(row, 0); lcd_print(content); } else if (strncmp(cmd, CUR:, 4) 0) { uint8_t row cmd[4] - 0; uint8_t col cmd[6] - 0; lcd_set_cursor(row, col); } else if (strncmp(cmd, BL:, 3) 0) { if (cmd[3] 1) lcd_backlight_on(); else lcd_backlight_off(); } else if (strncmp(cmd, STA?, 4) 0) { uart_send_string(OK:READY\n); } }LCD驱动部分写指令和写数据的函数是基础void lcd_write_cmd(uint8_t cmd) { LCD_RS 0; LCD_RW 0; LCD_DATA cmd; LCD_EN 1; delay_us(5); LCD_EN 0; delay_ms(2); } void lcd_write_data(uint8_t data) { LCD_RS 1; LCD_RW 0; LCD_DATA data; LCD_EN 1; delay_us(5); LCD_EN 0; delay_ms(2); }delay_ms(2)是给LCD1602足够的执行时间这个延时不能省省了会出现显示乱码或者指令丢失。3.5 联调过程与参数确认联调的时候我按这个顺序来排查先确认硬件连接USB转串口模块的TX接单片机RXRX接单片机TXGND对接。这个搞反了是最常见的问题。确认波特率一致上位机设9600单片机端串口初始化也必须是9600。我习惯在单片机端上电后先往串口发一个READY\n上位机能收到就说明链路通了。测试单条指令先发CLR\n看LCD有没有清屏。再发TXT:0:Hello\n看第一行有没有显示Hello。测试边界情况发超长文本看会不会溢出发非法指令看程序会不会跑飞。实测下来9600波特率下发送一条16字符的指令从点击按钮到LCD显示出来延迟大概在20-30ms左右完全在可接受范围内。4. 常见问题与排查技巧实录4.1 串口打不开怎么办这是新手遇到的第一个拦路虎。常见原因和排查方法现象可能原因解决方法提示拒绝访问串口被其他程序占用关闭其他串口助手、IDE的串口监视器提示找不到端口驱动没装好装CH340或CP2102驱动端口列表为空USB线只供电不传数据换一根带数据线的USB线打开后立即断开波特率不匹配检查两端波特率设置我踩过最坑的一次是USB线的问题。那根线只能充电不能传数据插上后设备管理器里能看到串口但就是打不开。换了一根线就好了。所以遇到莫名其妙的问题先换线试试。4.2 数据乱码怎么排查乱码的原因基本就三个波特率不匹配、数据位/校验位/停止位不匹配、地线没接。波特率不匹配是最常见的。如果两边都设的9600但还是乱码检查一下单片机的晶振频率和波特率计算是否正确。有些单片机用内部RC振荡器频率偏差比较大波特率误差超过3%就可能乱码。这种情况要么换外部晶振要么降低波特率。地线没接这个问题容易被忽略。USB转串口模块和单片机之间如果只接了TX和RX没有共地信号电平没有参考点也会乱码。一定要把GND对接上。4.3 LCD显示不正常的情况LCD1602显示异常有几种典型表现全屏方块初始化失败检查初始化时序和延时显示乱码数据线接触不良或者时序太快只显示一行第二行地址设置错误检查0xC0地址字符缺笔画对比度调节不当调一下V0引脚上的电位器背光亮但无显示对比度问题或者初始化没成功实操心得LCD1602的对比度调节电位器建议用10K的太大或太小都不好调。调的时候慢慢转看到字符清晰出现就停。4.4 上位机界面卡死怎么解决界面卡死基本上都是因为在主线程里做了阻塞操作。比如直接在按钮回调里调用ser.read()如果串口没数据read()会一直等界面就卡住了。解决办法就是我前面说的多线程 队列。读取线程负责阻塞等待数据主线程只管从队列取数据更新界面。两边通过队列通信互不阻塞。还有一个容易忽略的点是ser.write()也可能阻塞。如果发送缓冲区满了write()会等待。虽然概率不大但保险起见可以把发送也放到单独线程里或者设置write_timeout参数。4.5 打包成exe后的坑用PyInstaller打包的时候如果程序里用了serial.tools.list_ports可能会报错说找不到模块。这是因为PyInstaller没有自动把pyserial的子模块打进去。解决办法是在打包命令里加--hidden-importserial.tools.list_ports。pyinstaller -F -w --hidden-importserial.tools.list_ports main.py-F是打包成单个exe-w是运行时不显示控制台窗口。打包出来的exe第一次运行会慢一点因为要解压后面就正常了。4.6 常见问题速查表问题排查顺序快速解决串口打不开占用→驱动→线材关其他程序换线数据乱码波特率→数据位→地线统一9600 8N1接GNDLCD无显示对比度→初始化→接线调电位器检查延时界面卡死阻塞调用→线程改多线程队列打包报错隐藏导入→依赖加--hidden-import指令无响应换行符→解析逻辑确认发的是\n不是\r\n这个表我贴在显示器旁边调试的时候对照着看省了不少时间。5. 一些可以继续折腾的方向这套东西跑通之后能扩展的地方还挺多的。比如把日志存成CSV文件方便后续分析加一个指令脚本功能可以按顺序自动发送多条指令或者把界面换成Web版用浏览器访问。我最近在试的是加一个简单的波形显示把单片机上传的传感器数据实时画出来用的是matplotlib的动画功能效果还不错。另外协议部分也可以升级现在用的是明文文本如果以后要传二进制数据可以加一个简单的校验和防止传输过程中出错。不过对于LCD1602这种应用场景现在的方案已经够用了没必要过度设计。我在实际使用中最大的体会是工具顺手比功能多更重要。自己写的上位机可能功能比不上那些成熟的串口助手但每一个按钮的位置、每一个快捷键的设置都是按自己的习惯来的用起来就是舒服。而且遇到问题的时候源码在手想怎么改就怎么改这种掌控感是现成工具给不了的。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →