利用Python調試串口的示例代碼
概述:為什么需要專業(yè)的串口調試工具
在嵌入式開發(fā)、物聯(lián)網設備調試過程中,串口通信是最基礎的調試手段。但系統(tǒng)自帶的串口工具功能簡陋,商業(yè)軟件又價格昂貴。本文將帶你用Python+ttkbootstrap打造一款高顏值、多功能的串口調試助手,具備以下亮點功能:
核心功能亮點:
- 現(xiàn)代化UI界面 - 基于ttkbootstrap的多主題切換
- 實時數(shù)據(jù)統(tǒng)計 - 發(fā)送/接收字節(jié)計數(shù)
- 自動發(fā)送功能 - 可配置間隔時間
- 發(fā)送歷史記錄 - 支持上下箭頭導航
- 數(shù)據(jù)持久化 - 接收內容保存為文件
- 自動端口檢測 - 實時監(jiān)控串口熱插拔
項目架構設計
1.1 技術棧選型
import serial # 串口通信核心庫 import serial.tools.list_ports # 串口設備枚舉 import threading # 多線程處理 import queue # 線程安全隊列 import ttkbootstrap as ttk # 現(xiàn)代化UI框架 from tkinter import filedialog # 文件對話框
1.2 關鍵類說明
SerialTool:主控制類,采用MVC設計模式
數(shù)據(jù)層:serial_port管理物理連接
視圖層:create_widgets()構建界面
控制層:事件處理方法群
1.3 線程模型

定時輪詢異步推送定時觸發(fā)主線程接收隊列接收子線程自動發(fā)送任務
環(huán)境配置指南
2.1 基礎環(huán)境
# 必需庫安裝 pip install pyserial ttkbootstrap
2.2 硬件準備
任意USB轉串口設備(如CH340、CP2102等)
開發(fā)板或目標設備
2.3 兼容性說明
支持Windows/macOS/Linux
測試Python版本:3.8+
核心功能實現(xiàn)
3.1 串口通信核心
def open_serial(self):
# 參數(shù)映射轉換
parity_map = {
'None': serial.PARITY_NONE,
'Even': serial.PARITY_EVEN,
# ...其他校驗位映射
}
self.serial_port = serial.Serial(
port=self.port_cb.get(),
baudrate=int(self.baudrate_cb.get()),
parity=parity_map[self.parity_cb.get()],
timeout=0.1 # 非阻塞讀取
)
3.2 多線程數(shù)據(jù)處理
def receive_worker(self):
while not self.receive_thread_event.is_set():
try:
# 非阻塞讀取
if self.serial_port.in_waiting > 0:
data = self.serial_port.read(self.serial_port.in_waiting)
self.receive_queue.put(data)
except serial.SerialException:
break
3.3 自動發(fā)送機制
def auto_send_task(self):
if self.auto_send_flag:
try:
interval = int(self.interval_entry.get())
self.send_data() # 執(zhí)行發(fā)送
self.master.after(interval, self.auto_send_task) # 定時循環(huán)
except ValueError:
self.auto_var.set(False)
UI界面詳解
4.1 三欄式布局
main_frame = ttk.Frame(self.master) left_frame = ttk.Labelframe(main_frame, text="串口配置") # 左側配置區(qū) send_frame = ttk.Labelframe(right_frame, text="數(shù)據(jù)發(fā)送") # 右上發(fā)送區(qū) recv_frame = ttk.Labelframe(right_frame, text="數(shù)據(jù)接收") # 右下接收區(qū)
4.2 主題切換實現(xiàn)
def change_theme(self):
selected_theme = self.theme_cb.get()
self.style.theme_use(selected_theme) # 動態(tài)切換主題
4.3 控件亮點功能
歷史記錄導航:通過<Up>/<Down>鍵遍歷
智能滾動文本框:自動滾動到最新內容
狀態(tài)欄提示:實時顯示連接狀態(tài)
運行效果展示
5.1 主題切換演示




5.2 數(shù)據(jù)收發(fā)演示
[2023-08-20 14:25:36] [Send] AT+GMR
[2023-08-20 14:25:36] AT version:2.1.0.0-dev
5.3 統(tǒng)計功能展示
發(fā)送: 2456 字節(jié) | 接收: 18923 字節(jié)
源碼下載
import serial
import serial.tools.list_ports
import threading
import queue
import os
import time
import ttkbootstrap as ttk
from ttkbootstrap.constants import *
from ttkbootstrap.dialogs import Messagebox
from ttkbootstrap.scrolled import ScrolledText
from tkinter import BooleanVar, StringVar, IntVar
import platform
from tkinter import filedialog
import json
class SerialTool:
def __init__(self, master):
self.master = master
self.master.title("串口調試助手")
self.master.geometry("900x520")
self.master.resizable(False, False) # 禁止調整窗口大小
self.master.update() # 強制應用尺寸限制
# 初始化樣式
self.style = ttk.Style(theme='cosmo')
# 配置邊框線為純黑色的樣式
self.style.configure('BlackBorder.TLabelframe', bordercolor='#D3D3D3', relief='solid', borderwidth=1)
# 串口參數(shù)
self.serial_port = None
self.receive_queue = queue.Queue()
self.auto_send_flag = False
self.send_count = 0
self.receive_count = 0
self.receive_thread = None
self.receive_thread_event = threading.Event() # 用于控制接收線程的事件
# 發(fā)送歷史記錄
self.send_history = []
self.history_index = -1
# 自動檢測串口變化
self.last_port_count = 0
# 創(chuàng)建界面
self.create_widgets()
self.refresh_ports()
self.master.after(100, self.process_queue)
self.check_ports_change() # 開始檢測串口變化
def create_widgets(self):
"""創(chuàng)建三欄式布局"""
main_frame = ttk.Frame(self.master)
main_frame.pack(fill=BOTH, expand=True, padx=10, pady=10)
# 主題切換控件
theme_frame = ttk.Frame(self.master)
theme_frame.pack(fill=X, padx=10, pady=(0, 5))
ttk.Label(theme_frame, text="主題:").pack(side=LEFT, padx=5)
self.theme_cb = ttk.Combobox(
theme_frame,
values=sorted(ttk.Style().theme_names()),
state='readonly'
)
self.theme_cb.pack(side=LEFT, padx=5)
self.theme_cb.set('cosmo')
self.theme_cb.bind('<<ComboboxSelected>>', self.change_theme)
# 左側串口配置區(qū)
left_frame = ttk.Labelframe(main_frame, text="串口配置", padding=15, style='BlackBorder.TLabelframe')
left_frame.grid(row=0, column=0, sticky=NSEW, padx=5, pady=5)
# 右側上下分區(qū)
right_frame = ttk.Frame(main_frame)
right_frame.grid(row=0, column=1, sticky=NSEW, padx=5, pady=5)
# 發(fā)送區(qū)(右上)
send_frame = ttk.Labelframe(right_frame, text="數(shù)據(jù)發(fā)送", padding=15, style='BlackBorder.TLabelframe')
send_frame.pack(fill=BOTH, expand=True, side=TOP)
# 接收區(qū)(右下)
recv_frame = ttk.Labelframe(right_frame, text="數(shù)據(jù)接收", padding=15, style='BlackBorder.TLabelframe')
recv_frame.pack(fill=BOTH, expand=True, side=TOP)
# 配置網格權重
main_frame.columnconfigure(1, weight=1)
main_frame.rowconfigure(0, weight=1)
right_frame.rowconfigure(1, weight=1)
# 創(chuàng)建各區(qū)域組件
self.create_serial_controls(left_frame)
self.create_send_controls(send_frame)
self.create_recv_controls(recv_frame)
# 狀態(tài)欄
self.status_var = StringVar(value="就緒")
ttk.Label(self.master, textvariable=self.status_var,
bootstyle=(SECONDARY, INVERSE)).pack(fill=X, side=BOTTOM)
def change_theme(self, event=None):
"""切換主題"""
selected_theme = self.theme_cb.get()
self.style.theme_use(selected_theme)
def create_serial_controls(self, parent):
"""串口參數(shù)控件"""
param_frame = ttk.Frame(parent)
param_frame.pack(fill=X)
# 串口號
ttk.Label(param_frame, text="COM端口:").grid(row=0, column=0, padx=5, pady=5, sticky=W)
self.port_cb = ttk.Combobox(param_frame, width=15)
self.port_cb.grid(row=0, column=1, padx=5, pady=5)
# 波特率
ttk.Label(param_frame, text="波特率:").grid(row=1, column=0, padx=5, pady=5, sticky=W)
self.baudrate_cb = ttk.Combobox(param_frame, values=[
'9600', '115200', '57600', '38400',
'19200', '14400', '4800', '2400', '1200'
], width=15)
self.baudrate_cb.set('9600')
self.baudrate_cb.grid(row=1, column=1, padx=5, pady=5)
# 校驗位
ttk.Label(param_frame, text="校驗位:").grid(row=2, column=0, padx=5, pady=5, sticky=W)
self.parity_cb = ttk.Combobox(param_frame, values=[
'None', 'Even', 'Odd', 'Mark', 'Space'
], width=15)
self.parity_cb.set('None')
self.parity_cb.grid(row=2, column=1, padx=5, pady=5)
# 數(shù)據(jù)位
ttk.Label(param_frame, text="數(shù)據(jù)位:").grid(row=3, column=0, padx=5, pady=5, sticky=W)
self.databits_cb = ttk.Combobox(param_frame, values=['8', '7', '6', '5'], width=15)
self.databits_cb.set('8')
self.databits_cb.grid(row=3, column=1, padx=5, pady=5)
# 停止位
ttk.Label(param_frame, text="停止位:").grid(row=4, column=0, padx=5, pady=5, sticky=W)
self.stopbits_cb = ttk.Combobox(param_frame, values=['1', '1.5', '2'], width=15)
self.stopbits_cb.set('1')
self.stopbits_cb.grid(row=4, column=1, padx=5, pady=5)
# 操作按鈕
# 按鈕容器
btn_frame = ttk.Frame(parent)
btn_frame.pack(pady=10, fill=X)
# 配置網格列權重實現(xiàn)自動伸縮
btn_frame.columnconfigure((0, 1, 2), weight=1, uniform='btns') # uniform 確保列寬一致
# 刷新按鈕
ttk.Button(
btn_frame,
text="刷新端口",
command=self.refresh_ports,
bootstyle=OUTLINE
).grid(row=0, column=0, padx=5, sticky="ew")
# 連接按鈕
self.conn_btn = ttk.Button(
btn_frame,
text="打開串口",
command=self.toggle_connection,
bootstyle=OUTLINE + SUCCESS
)
self.conn_btn.grid(row=0, column=1, padx=5, sticky="ew")
# 手動發(fā)送按鈕(移動到此處)
ttk.Button(
btn_frame,
text="手動發(fā)送",
command=self.send_data,
bootstyle=OUTLINE + PRIMARY
).grid(row=0, column=2, padx=5, sticky="ew")
def create_send_controls(self, parent):
"""發(fā)送區(qū)控件"""
# 自動發(fā)送設置
auto_frame = ttk.Frame(parent)
auto_frame.pack(fill=X, pady=5)
self.auto_var = BooleanVar()
ttk.Checkbutton(auto_frame, text="自動發(fā)送", variable=self.auto_var,
command=self.toggle_auto_send).pack(side=LEFT)
ttk.Label(auto_frame, text="間隔(ms):").pack(side=LEFT, padx=5)
self.interval_entry = ttk.Entry(auto_frame, width=8)
self.interval_entry.insert(0, "1000")
self.interval_entry.pack(side=LEFT)
# 發(fā)送內容
self.send_text = ScrolledText(parent, height=4, autohide=True)
self.send_text.pack(fill=BOTH, expand=True)
# 綁定上下箭頭鍵用于歷史記錄導航
self.send_text.bind("<Up>", self.prev_history)
self.send_text.bind("<Down>", self.next_history)
def create_recv_controls(self, parent):
"""接收區(qū)控件"""
# 接收顯示
self.recv_text = ScrolledText(parent, height=5, autohide=True)
self.recv_text.pack(fill=BOTH, expand=True)
# 統(tǒng)計欄
stat_frame = ttk.Frame(parent)
stat_frame.pack(fill=X, pady=5)
ttk.Label(stat_frame, text="發(fā)送:").pack(side=LEFT, padx=5)
self.send_label = ttk.Label(stat_frame, text="0")
self.send_label.pack(side=LEFT)
ttk.Label(stat_frame, text="接收:").pack(side=LEFT, padx=10)
self.recv_label = ttk.Label(stat_frame, text="0")
self.recv_label.pack(side=LEFT)
# 添加保存接收按鈕
ttk.Button(stat_frame, text="保存接收", command=self.save_received,
bootstyle=OUTLINE + INFO).pack(side=RIGHT, padx=5)
ttk.Button(stat_frame, text="清空", command=self.clear_received,
bootstyle=OUTLINE + WARNING).pack(side=RIGHT)
def refresh_ports(self):
"""刷新端口列表"""
try:
ports = [p.device for p in serial.tools.list_ports.comports()]
self.port_cb['values'] = ports
self.status_var.set(f"自動檢測到主板有{len(ports)} 個串口可用,請注意選擇正確的。")
self.last_port_count = len(ports)
except Exception as e:
print(f"Error refreshing ports: {e}")
self.status_var.set(f"刷新端口時出錯: {e}")
def check_ports_change(self):
"""檢查串口變化"""
current_count = len(list(serial.tools.list_ports.comports()))
if current_count != self.last_port_count:
self.refresh_ports()
self.master.after(1000, self.check_ports_change) # 每秒檢查一次
def toggle_connection(self):
"""切換連接狀態(tài)"""
if self.serial_port and self.serial_port.is_open:
self.close_serial()
else:
self.open_serial()
def open_serial(self):
"""打開串口"""
try:
port = self.port_cb.get()
if not port:
raise ValueError("請選擇串口")
parity_map = {
'None': serial.PARITY_NONE,
'Even': serial.PARITY_EVEN,
'Odd': serial.PARITY_ODD,
'Mark': serial.PARITY_MARK,
'Space': serial.PARITY_SPACE
}
self.serial_port = serial.Serial(
port=port,
baudrate=int(self.baudrate_cb.get()),
parity=parity_map[self.parity_cb.get()],
bytesize=int(self.databits_cb.get()),
stopbits=float(self.stopbits_cb.get()),
timeout=0.1
)
self.conn_btn.configure(text="關閉串口", bootstyle=OUTLINE + SUCCESS)
self.status_var.set(f"已連接 {port}")
self.receive_thread_event.clear() # 清除事件標志
self.receive_thread = threading.Thread(target=self.receive_worker, daemon=True)
self.receive_thread.start()
except Exception as e:
Messagebox.show_error(f"主板上沒有這個串口或你選的被測端口跟主板端口不對應,請在設備管理器中確認正確的端口: {str(e)}", "錯誤")
self.status_var.set("連接失敗")
def close_serial(self):
"""關閉串口"""
self.receive_thread_event.set() # 設置事件標志,通知接收線程停止
if self.receive_thread and self.receive_thread.is_alive():
self.receive_thread.join() # 等待接收線程結束
if self.serial_port:
try:
self.serial_port.close()
except Exception as e:
print(f"關閉串口時出錯: {e}")
self.conn_btn.configure(text="打開串口", bootstyle=DANGER)
self.status_var.set("已斷開連接")
def receive_worker(self):
"""接收線程工作函數(shù)"""
while not self.receive_thread_event.is_set() and self.serial_port and self.serial_port.is_open:
try:
if self.serial_port.in_waiting > 0:
data = self.serial_port.read(self.serial_port.in_waiting)
self.receive_queue.put(data)
except Exception as e:
print(f"接收錯誤: {e}")
break
def process_queue(self):
"""處理接收隊列"""
while not self.receive_queue.empty():
data = self.receive_queue.get()
self.display_received(data)
self.receive_count += len(data)
self.recv_label.configure(text=str(self.receive_count))
self.master.after(100, self.process_queue)
def display_received(self, data):
"""顯示接收數(shù)據(jù)(帶時間戳)"""
timestamp = time.strftime("[%Y-%m-%d %H:%M:%S] ", time.localtime())
try:
text = data.decode('utf-8')
self.recv_text.insert(END, timestamp + text + '\n')
self.recv_text.see(END)
except UnicodeDecodeError:
self.recv_text.insert(END, timestamp + data.hex(' ') + '\n')
self.recv_text.see(END)
def toggle_auto_send(self):
"""切換自動發(fā)送"""
self.auto_send_flag = self.auto_var.get()
if self.auto_send_flag:
self.auto_send_task()
def auto_send_task(self):
"""自動發(fā)送任務"""
if self.auto_send_flag and self.serial_port and self.serial_port.is_open:
try:
interval = int(self.interval_entry.get())
self.send_data()
self.master.after(interval, self.auto_send_task)
except ValueError:
self.auto_var.set(False)
Messagebox.show_error("無效的間隔時間", "錯誤")
def send_data(self):
"""發(fā)送數(shù)據(jù)"""
if not self.serial_port or not self.serial_port.is_open:
Messagebox.show_warning("請先打開串口", "警告")
return
data = self.send_text.get(1.0, END).strip()
if not data:
return
try:
# 添加到歷史記錄
if data and (not self.send_history or data != self.send_history[0]):
self.send_history.insert(0, data)
if len(self.send_history) > 20: # 限制歷史記錄數(shù)量
self.send_history.pop()
self.history_index = -1 # 重置歷史索引
self.serial_port.write(data.encode('utf-8'))
self.send_count += len(data)
self.send_label.configure(text=str(self.send_count))
# 顯示發(fā)送的數(shù)據(jù)(帶時間戳)
timestamp = time.strftime("[%Y-%m-%d %H:%M:%S] ", time.localtime())
self.recv_text.insert(END, f"{timestamp}[Send] {data}\n")
self.recv_text.see(END)
except Exception as e:
Messagebox.show_error(f"發(fā)送失敗: {str(e)}", "錯誤")
def prev_history(self, event):
"""上一條歷史記錄"""
if self.send_history:
if self.history_index < len(self.send_history) - 1:
self.history_index += 1
self.send_text.delete(1.0, END)
self.send_text.insert(END, self.send_history[self.history_index])
return "break"
def next_history(self, event):
"""下一條歷史記錄"""
if self.history_index > 0:
self.history_index -= 1
self.send_text.delete(1.0, END)
self.send_text.insert(END, self.send_history[self.history_index])
elif self.history_index == 0:
self.history_index = -1
self.send_text.delete(1.0, END)
return "break"
def save_received(self):
"""保存接收內容到文件"""
filename = filedialog.asksaveasfilename(
defaultextension=".txt",
filetypes=[("Text Files", "*.txt"), ("All Files", "*.*")]
)
if filename:
try:
with open(filename, 'w', encoding='utf-8') as f:
f.write(self.recv_text.get(1.0, END))
self.status_var.set(f"接收內容已保存到 {filename}")
except Exception as e:
Messagebox.show_error(f"保存文件失敗: {str(e)}", "錯誤")
def clear_received(self):
"""清空接收區(qū)"""
self.recv_text.delete(1.0, END)
self.receive_count = 0
self.recv_label.configure(text="0")
self.send_text.delete(1.0, END)
self.send_count = 0
self.send_label.configure(text="0")
def on_closing(self):
"""安全關閉程序"""
# 停止自動發(fā)送循環(huán)
self.auto_send_flag = False
# 關閉串口連接
self.close_serial()
# 確保完全退出
self.master.quit() # 終止mainloop
self.master.destroy() # 銷毀所有Tkinter對象
self.master.after(500, self.force_exit) # 500ms后強制退出
def force_exit(self):
"""最終退出保障"""
import os
os._exit(0) # 強制終止進程
if __name__ == "__main__":
root = ttk.Window()
app = SerialTool(root)
root.protocol("WM_DELETE_WINDOW", app.on_closing)
root.mainloop()
總結與擴展
7.1 項目總結
- 采用生產者-消費者模式處理串口數(shù)據(jù)
- 通過隊列實現(xiàn)線程間安全通信
- 現(xiàn)代化UI提升使用體驗
7.2 擴展方向
- 增加協(xié)議解析功能(Modbus/AT指令等)
- 實現(xiàn)數(shù)據(jù)圖表可視化
- 添加插件系統(tǒng)支持
以上就是利用Python調試串口的示例代碼的詳細內容,更多關于Python調試串口的資料請關注腳本之家其它相關文章!
相關文章
python3.7 openpyxl 刪除指定一列或者一行的代碼
這篇文章主要介紹了python3.7 openpyxl 刪除指定一列或者一行,文中通過代碼給大家介紹了python3 openpyxl基本操作,代碼簡單易懂,需要的朋友可以參考下2019-10-10
Python 識別12306圖片驗證碼物品的實現(xiàn)示例
這篇文章主要介紹了Python 識別12306圖片驗證碼物品的實現(xiàn)示例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-01-01
Python實現(xiàn)json對值進行模糊搜索的示例詳解
我經常使用json進行存儲配置,于是常常遇到這樣的問題:如果想要對某個數(shù)組里的值進行模糊搜索,同時輸出相關的其他數(shù)組相同位置的的值該如何實現(xiàn)呢?本文就來和大家詳細聊聊2023-01-01
使用python框架Scrapy爬取數(shù)據(jù)的操作步驟
Scrapy是一個基于Python的強大的開源網絡爬蟲框架,用于從網站上抓取信息,它提供了廣泛的功能,使得爬取和分析數(shù)據(jù)變得相對容易,本文小編將給給大家介紹一下如何使用python框架Scrapy爬取數(shù)據(jù),需要的朋友可以參考下2023-10-10

