当前位置:首页>python>Tkinter Python串口编程:RS485工业通信详解

Tkinter Python串口编程:RS485工业通信详解

  • 2026-07-08 18:06:40
Tkinter Python串口编程:RS485工业通信详解

🏭 工业现场的通信困局

在工控项目里,有一个场景几乎每个做上位机的开发者都经历过:设备就在眼前,串口线插着,程序跑着,但数据就是对不上——要么帧丢失,要么校验错,要么多台设备同时响应把总线打崩。

RS485是工业现场用得最多的有线通信协议之一,半双工、差分信号、最远1200米、最多32个节点,这些特性让它在PLC、变频器、仪表、传感器的互联场景里几乎无可替代。但偏偏,很多Python开发者在第一次对接RS485设备时会踩一连串的坑:收发切换时序没处理好、Modbus帧结构理解有偏差、多设备轮询时线程安全没做、GUI界面和串口线程互相阻塞……

这篇文章会带你从零搭建一套基于Tkinter + pyserial的RS485工业通信上位机,覆盖从串口配置、Modbus RTU帧构造、多设备轮询调度,到GUI线程安全更新的完整链路。读完之后,你手里会有三段可以直接拿去项目里用的代码模板。

测试环境:Windows 10/11、Python 3.10+、pyserial 3.5、USB转RS485适配器(CH340芯片)。


🔌 RS485与Modbus RTU:先把概念理清楚

很多人把RS485和Modbus混为一谈,这是第一个认知误区。RS485是物理层协议,只管信号怎么传;Modbus RTU是应用层协议,规定数据怎么打包、怎么校验。两者的关系,就像高速公路和货车——公路决定能跑多快,货车决定装什么货。

RS485采用差分信号(A/B两线),抗干扰能力强,这是它在工业现场生命力旺盛的根本原因。半双工意味着同一时刻总线上只能有一个设备在发送,这就引出了收发切换这个在Python串口编程里最容易被忽视的细节。

Modbus RTU的帧结构很简单,但细节不能马虎:

1| 设备地址(1B) | 功能码(1B) | 数据域(nB) | CRC校验(2B, 低字节在前) |

CRC是小端序——低字节先发,这个顺序如果搞反,设备会直接丢帧,而且不会给你任何报错提示,只是静静地不回应,让你怀疑人生。


🛠️ 环境搭建与串口基础配置

先把依赖装好:

bash1pip install pyserial

Tkinter是Python标准库自带的,不需要额外安装。

下面是一个串口配置的基础封装,把常用参数都收拢在一起,后续所有示例都基于这个基类:

python1import serial2import serial.tools.list_ports3from typing import Optional45class RS485Port:6"""7    RS485串口封装基类8    测试环境: Windows 10, pyserial 3.5, CH340 USB转RS4859    """10def __init__(11        self,12        port: str,13        baudrate: int = 9600,14        bytesize: int = 8,15        parity: str = 'N',       # N=无校验, E=偶校验, O=奇校验16        stopbits: float = 1,17        timeout: float = 0.5,    # 读超时,单位秒18        write_timeout: float = 1.019    ):20        self.port = port21        self.config = {22'baudrate': baudrate,23'bytesize': bytesize,24'parity': parity,25'stopbits': stopbits,26'timeout': timeout,27'write_timeout': write_timeout28        }29        self._serial: Optional[serial.Serial] = None3031def open(self) -> bool:32try:33            self._serial = serial.Serial(self.port, **self.config)34            self._serial.reset_input_buffer()35            self._serial.reset_output_buffer()36return True37except serial.SerialException as e:38print(f"[RS485] 串口打开失败: {e}")39return False4041def close(self):42if self._serial and self._serial.is_open:43            self._serial.close()4445def is_open(self) -> bool:46return self._serial is not None and self._serial.is_open4748@staticmethod49def list_ports() -> list[str]:50"""枚举当前系统可用串口"""51return [p.device for p in serial.tools.list_ports.comports()]

reset_input_buffer() 和 reset_output_buffer() 这两行别省——打开串口时缓冲区里可能残留上次通信的脏数据,不清掉会直接污染第一帧的解析结果。


📦 方案一:Modbus RTU帧的构造与CRC校验

这是最基础也是最核心的部分。工业设备绝大多数都支持Modbus RTU,掌握帧构造就掌握了和绝大多数仪表、PLC通信的能力。

python1import struct23def calc_crc16(data: bytes) -> bytes:4"""5    Modbus RTU CRC16校验6    返回2字节,低字节在前(小端序)7    """8    crc = 0xFFFF9for byte in data:10        crc ^= byte11for _ in range(8):12if crc & 0x0001:13                crc = (crc >> 1) ^ 0xA00114else:15                crc >>= 116# 低字节在前,高字节在后17return struct.pack('<H', crc)181920def build_read_holding_registers(21    slave_id: int,22    start_addr: int,23    count: int24) -> bytes:25"""26    构造功能码03:读保持寄存器27    slave_id:   从站地址 (1-247)28    start_addr: 起始寄存器地址29    count:      读取寄存器数量30    """31    payload = struct.pack('>BBHH', slave_id, 0x03, start_addr, count)32return payload + calc_crc16(payload)333435def build_write_single_register(36    slave_id: int,37    reg_addr: int,38    value: int39) -> bytes:40"""41    构造功能码06:写单个保持寄存器42    """43    payload = struct.pack('>BBHH', slave_id, 0x06, reg_addr, value)44return payload + calc_crc16(payload)454647def verify_crc(frame: bytes) -> bool:48"""校验接收帧的CRC"""49if len(frame) < 4:50return False51    received_crc = frame[-2:]52    calc = calc_crc16(frame[:-2])53return received_crc == calc545556def parse_read_response(frame: bytes) -> list[int] | None:57"""58    解析功能码03响应帧59    返回寄存器值列表,校验失败返回None60    """61if not verify_crc(frame):62return None63# frame[2] 是字节数64    byte_count = frame[2]65    values = []66for i in range(byte_count // 2):67        offset = 3 + i * 268        val = struct.unpack('>H', frame[offset:offset+2])[0]69        values.append(val)70return values717273# 使用示例74if __name__ == '__main__':75# 读取从站1,从地址0x0000开始的4个寄存器76    frame = build_read_holding_registers(slave_id=1, start_addr=0x0000, count=4)77print(f"发送帧: {frame.hex(' ').upper()}")

踩坑预警:CRC的多项式是0xA001(反转形式),不是0x8005。有些网上的代码用了错误的多项式,设备不响应时很难排查。另外,struct.pack('>BBHH'...)里的>是大端序,Modbus数据域是大端的,但CRC是小端的,两者混用是新手最容易搞混的地方。


🔄 方案二:多设备轮询调度与线程安全

单台设备通信搞定之后,真实项目里往往是一条总线挂着十几台设备,需要轮询读取。这时候两个问题同时出现:RS485总线的互斥访问Tkinter主线程的UI更新

Tkinter的控件不是线程安全的,在子线程里直接操作Label、Text等控件会导致随机崩溃,有时候能跑,有时候直接报错,而且复现率不稳定,非常难调试。正确做法是用queue.Queue做数据中转,主线程用after()定时轮询队列。

python1import struct2import threading3import queue4import time5import tkinter as tk6from tkinter import ttk, scrolledtext, messagebox7from typing import Optional8import serial9import serial.tools.list_ports101112class RS485Port:13"""14    RS485串口封装基类15    测试环境: Windows 10, pyserial 3.5, CH340 USB转RS48516    """17def __init__(18            self,19            port: str,20            baudrate: int = 9600,21            bytesize: int = 8,22            parity: str = 'N',  # N=无校验, E=偶校验, O=奇校验23            stopbits: float = 1,24            timeout: float = 0.5,  # 读超时,单位秒25            write_timeout: float = 1.026    ):27        self.port = port28        self.config = {29'baudrate': baudrate,30'bytesize': bytesize,31'parity': parity,32'stopbits': stopbits,33'timeout': timeout,34'write_timeout': write_timeout35        }36        self._serial: Optional[serial.Serial] = None3738def open(self) -> bool:39try:40            self._serial = serial.Serial(self.port, **self.config)41            self._serial.reset_input_buffer()42            self._serial.reset_output_buffer()43return True44except serial.SerialException as e:45print(f"[RS485] 串口打开失败: {e}")46return False4748def close(self):49if self._serial and self._serial.is_open:50            self._serial.close()5152def is_open(self) -> bool:53return self._serial is not None and self._serial.is_open5455@staticmethod56def list_ports() -> list[str]:57"""枚举当前系统可用串口"""58return [p.device for p in serial.tools.list_ports.comports()]596061def calc_crc16(data: bytes) -> bytes:62"""63    Modbus RTU CRC16校验64    返回2字节,低字节在前(小端序)65    """    crc = 0xFFFF66for byte in data:67        crc ^= byte68for _ in range(8):69if crc & 0x0001:70                crc = (crc >> 1) ^ 0xA00171else:72                crc >>= 173# 低字节在前,高字节在后74return struct.pack('<H', crc)757677def build_read_holding_registers(78        slave_id: int,79        start_addr: int,80        count: int81) -> bytes:82"""83    构造功能码03:读保持寄存器84    slave_id:   从站地址 (1-247)    start_addr: 起始寄存器地址85    count:      读取寄存器数量86    """    payload = struct.pack('>BBHH', slave_id, 0x03, start_addr, count)87return payload + calc_crc16(payload)888990def build_write_single_register(91        slave_id: int,92        reg_addr: int,93        value: int94) -> bytes:95"""96    构造功能码06:写单个保持寄存器97    """    payload = struct.pack('>BBHH', slave_id, 0x06, reg_addr, value)98return payload + calc_crc16(payload)99100101def verify_crc(frame: bytes) -> bool:102"""校验接收帧的CRC"""103if len(frame) < 4:104return False105    received_crc = frame[-2:]106    calc = calc_crc16(frame[:-2])107return received_crc == calc108109110def parse_read_response(frame: bytes) -> list[int] | None:111"""112    解析功能码03响应帧113    返回寄存器值列表,校验失败返回None114    """    if not verify_crc(frame):115return None116# frame[2] 是字节数117    byte_count = frame[2]118    values = []119for i in range(byte_count // 2):120        offset = 3 + i * 2121        val = struct.unpack('>H', frame[offset:offset + 2])[0]122        values.append(val)123return values124125126class ModbusPoller:127"""128    多设备轮询调度器129    使用独立线程轮询,通过Queue向GUI线程传递数据130    """131def __init__(self, port: RS485Port, data_queue: queue.Queue):132        self.port = port133        self.queue = data_queue134        self._devices: list[dict] = []  # 待轮询设备列表135        self._running = False136        self._lock = threading.Lock()  # 保护串口的互斥锁137        self._thread: threading.Thread | None = None138139def add_device(self, slave_id: int, reg_addr: int, reg_count: int, label: str):140"""注册一个待轮询的设备"""141        self._devices.append({142'slave_id': slave_id,143'reg_addr': reg_addr,144'reg_count': reg_count,145'label': label146        })147148def start(self):149        self._running = True150        self._thread = threading.Thread(target=self._poll_loop, daemon=True)151        self._thread.start()152153def stop(self):154        self._running = False155156def _poll_loop(self):157while self._running:158for device in self._devices:159if not self._running:160break161                self._query_device(device)162                time.sleep(0.05)  # 帧间隔,给总线喘息时间163            time.sleep(0.1)  # 一轮轮询结束后的间隔164165def _query_device(self, device: dict):166        frame = build_read_holding_registers(167            device['slave_id'],168            device['reg_addr'],169            device['reg_count']170        )        with self._lock:171if not self.port.is_open():172return173try:174                self.port._serial.reset_input_buffer()175                self.port._serial.write(frame)176# 等待响应:从站地址(1)+功能码(1)+字节数(1)+数据(n*2)+CRC(2)177                expected_len = 5 + device['reg_count'] * 2178                response = self.port._serial.read(expected_len)179except serial.SerialException as e:180                self.queue.put({181'type''error',182'label': device['label'],183'msg'str(e)184                })185return186187if len(response) == expected_len:188            values = parse_read_response(response)189            self.queue.put({190'type''data',191'label': device['label'],192'values': values,193'raw': response.hex(' ').upper()194            })195else:196            self.queue.put({197'type''timeout',198'label': device['label'],199'received'len(response)200            })201202203class ModbusGUI:204"""205    Modbus RTU 调试界面206    """207def __init__(self):208        self.root = tk.Tk()209        self.root.title("Modbus RTU 调试工具")210        self.root.geometry("800x600")211212        self.port = None213        self.poller = None214        self.data_queue = queue.Queue()215216        self.create_widgets()217        self.update_port_list()218219# 定时检查数据队列220        self.root.after(100, self.check_queue)221222def create_widgets(self):223# 串口配置区域224        config_frame = ttk.LabelFrame(self.root, text="串口配置", padding=10)225        config_frame.pack(fill=tk.X, padx=10, pady=5)226227# 串口选择228        ttk.Label(config_frame, text="串口:").grid(row=0, column=0, sticky=tk.W, padx=5)229        self.port_var = tk.StringVar()230        self.port_combo = ttk.Combobox(config_frame, textvariable=self.port_var, width=10)231        self.port_combo.grid(row=0, column=1, padx=5)232233        ttk.Button(config_frame, text="刷新", command=self.update_port_list).grid(row=0, column=2, padx=5)234235# 波特率236        ttk.Label(config_frame, text="波特率:").grid(row=0, column=3, sticky=tk.W, padx=5)237        self.baudrate_var = tk.StringVar(value="9600")238        baudrate_combo = ttk.Combobox(config_frame, textvariable=self.baudrate_var,239                                      values=["9600""19200""38400""57600""115200"], width=8)240        baudrate_combo.grid(row=0, column=4, padx=5)241242# 连接按钮243        self.connect_btn = ttk.Button(config_frame, text="连接", command=self.toggle_connection)244        self.connect_btn.grid(row=0, column=5, padx=20)245246# 设备管理区域247        device_frame = ttk.LabelFrame(self.root, text="设备配置", padding=10)248        device_frame.pack(fill=tk.X, padx=10, pady=5)249250# 添加设备控件251        ttk.Label(device_frame, text="从站ID:").grid(row=0, column=0, sticky=tk.W, padx=5)252        self.slave_id_var = tk.StringVar(value="1")253        ttk.Entry(device_frame, textvariable=self.slave_id_var, width=5).grid(row=0, column=1, padx=5)254255        ttk.Label(device_frame, text="起始地址:").grid(row=0, column=2, sticky=tk.W, padx=5)256        self.start_addr_var = tk.StringVar(value="0")257        ttk.Entry(device_frame, textvariable=self.start_addr_var, width=8).grid(row=0, column=3, padx=5)258259        ttk.Label(device_frame, text="寄存器数:").grid(row=0, column=4, sticky=tk.W, padx=5)260        self.reg_count_var = tk.StringVar(value="2")261        ttk.Entry(device_frame, textvariable=self.reg_count_var, width=5).grid(row=0, column=5, padx=5)262263        ttk.Label(device_frame, text="标签:").grid(row=0, column=6, sticky=tk.W, padx=5)264        self.label_var = tk.StringVar(value="设备1")265        ttk.Entry(device_frame, textvariable=self.label_var, width=10).grid(row=0, column=7, padx=5)266267        ttk.Button(device_frame, text="添加设备", command=self.add_device).grid(row=0, column=8, padx=10)268269# 控制区域270        control_frame = ttk.Frame(self.root)271        control_frame.pack(fill=tk.X, padx=10, pady=5)272273        self.start_btn = ttk.Button(control_frame, text="开始轮询", command=self.start_polling, state=tk.DISABLED)274        self.start_btn.pack(side=tk.LEFT, padx=5)275276        self.stop_btn = ttk.Button(control_frame, text="停止轮询", command=self.stop_polling, state=tk.DISABLED)277        self.stop_btn.pack(side=tk.LEFT, padx=5)278279        ttk.Button(control_frame, text="清空日志", command=self.clear_log).pack(side=tk.LEFT, padx=5)280281# 手动操作区域282        manual_frame = ttk.LabelFrame(self.root, text="手动操作", padding=10)283        manual_frame.pack(fill=tk.X, padx=10, pady=5)284285        ttk.Label(manual_frame, text="从站ID:").grid(row=0, column=0, sticky=tk.W, padx=5)286        self.manual_slave_var = tk.StringVar(value="1")287        ttk.Entry(manual_frame, textvariable=self.manual_slave_var, width=5).grid(row=0, column=1, padx=5)288289        ttk.Label(manual_frame, text="寄存器地址:").grid(row=0, column=2, sticky=tk.W, padx=5)290        self.manual_addr_var = tk.StringVar(value="0")291        ttk.Entry(manual_frame, textvariable=self.manual_addr_var, width=8).grid(row=0, column=3, padx=5)292293        ttk.Label(manual_frame, text="值:").grid(row=0, column=4, sticky=tk.W, padx=5)294        self.manual_value_var = tk.StringVar(value="100")295        ttk.Entry(manual_frame, textvariable=self.manual_value_var, width=8).grid(row=0, column=5, padx=5)296297        ttk.Button(manual_frame, text="读寄存器", command=self.manual_read).grid(row=0, column=6, padx=10)298        ttk.Button(manual_frame, text="写寄存器", command=self.manual_write).grid(row=0, column=7, padx=5)299300# 日志显示区域301        log_frame = ttk.LabelFrame(self.root, text="通信日志", padding=10)302        log_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)303304        self.log_text = scrolledtext.ScrolledText(log_frame, height=15)305        self.log_text.pack(fill=tk.BOTH, expand=True)306307def update_port_list(self):308"""更新可用串口列表"""309        ports = RS485Port.list_ports()310        self.port_combo['values'] = ports311if ports:312            self.port_combo.current(0)313314def toggle_connection(self):315"""切换串口连接状态"""316if self.port is None:317# 连接串口318            port_name = self.port_var.get()319if not port_name:320                messagebox.showerror("错误""请选择串口")321return322323try:324                baudrate = int(self.baudrate_var.get())325except ValueError:326                messagebox.showerror("错误""波特率格式错误")327return328329            self.port = RS485Port(port_name, baudrate=baudrate)330if self.port.open():331                self.connect_btn.config(text="断开")332                self.start_btn.config(state=tk.NORMAL)333                self.log(f"串口 {port_name} 连接成功 (波特率: {baudrate})")334else:335                messagebox.showerror("错误"f"串口 {port_name} 连接失败")336                self.port = None337else:338# 断开串口339            self.stop_polling()340            self.port.close()341            self.port = None342            self.connect_btn.config(text="连接")343            self.start_btn.config(state=tk.DISABLED)344            self.log("串口已断开")345346def add_device(self):347"""添加轮询设备"""348if self.port is None:349            messagebox.showerror("错误""请先连接串口")350return351352try:353            slave_id = int(self.slave_id_var.get())354            start_addr = int(self.start_addr_var.get())355            reg_count = int(self.reg_count_var.get())356            label = self.label_var.get()357except ValueError:358            messagebox.showerror("错误""参数格式错误")359return360361if self.poller is None:362            self.poller = ModbusPoller(self.port, self.data_queue)363364        self.poller.add_device(slave_id, start_addr, reg_count, label)365        self.log(f"已添加设备: {label} (从站ID:{slave_id}, 地址:{start_addr}, 数量:{reg_count})")366367def start_polling(self):368"""开始轮询"""369if self.poller is None:370            messagebox.showwarning("警告""请先添加设备")371return372373        self.poller.start()374        self.start_btn.config(state=tk.DISABLED)375        self.stop_btn.config(state=tk.NORMAL)376        self.log("开始轮询...")377378def stop_polling(self):379"""停止轮询"""380if self.poller:381            self.poller.stop()382        self.start_btn.config(state=tk.NORMAL)383        self.stop_btn.config(state=tk.DISABLED)384        self.log("停止轮询")385386def manual_read(self):387"""手动读寄存器"""388if self.port is None or not self.port.is_open():389            messagebox.showerror("错误""串口未连接")390return391392try:393            slave_id = int(self.manual_slave_var.get())394            reg_addr = int(self.manual_addr_var.get())395except ValueError:396            messagebox.showerror("错误""参数格式错误")397return398399        frame = build_read_holding_registers(slave_id, reg_addr, 1)400401try:402            self.port._serial.reset_input_buffer()403            self.port._serial.write(frame)404            response = self.port._serial.read(7)  # 单个寄存器响应长度405406if len(response) == 7:407                values = parse_read_response(response)408if values:409                    self.log(410f"手动读取 - 从站{slave_id} 地址{reg_addr}: {values[0]} (原始: {response.hex(' ').upper()})")411else:412                    self.log(f"手动读取 - CRC校验失败: {response.hex(' ').upper()}")413else:414                self.log(f"手动读取 - 响应长度错误,期望7字节,收到{len(response)}字节")415except Exception as e:416            self.log(f"手动读取失败: {e}")417418def manual_write(self):419"""手动写寄存器"""420if self.port is None or not self.port.is_open():421            messagebox.showerror("错误""串口未连接")422return423424try:425            slave_id = int(self.manual_slave_var.get())426            reg_addr = int(self.manual_addr_var.get())427            value = int(self.manual_value_var.get())428except ValueError:429            messagebox.showerror("错误""参数格式错误")430return431432        frame = build_write_single_register(slave_id, reg_addr, value)433434try:435            self.port._serial.reset_input_buffer()436            self.port._serial.write(frame)437            response = self.port._serial.read(8)  # 写单寄存器响应长度438439if len(response) == 8:440if verify_crc(response):441                    self.log(f"手动写入成功 - 从站{slave_id} 地址{reg_addr}: {value}")442else:443                    self.log(f"手动写入 - CRC校验失败: {response.hex(' ').upper()}")444else:445                self.log(f"手动写入 - 响应长度错误,期望8字节,收到{len(response)}字节")446except Exception as e:447            self.log(f"手动写入失败: {e}")448449def check_queue(self):450"""检查数据队列并更新界面"""451try:452while True:453                data = self.data_queue.get_nowait()454455if data['type'] == 'data':456                    self.log(f"[{data['label']}] 寄存器值: {data['values']} (原始: {data['raw']})")457elif data['type'] == 'error':458                    self.log(f"[{data['label']}] 通信错误: {data['msg']}")459elif data['type'] == 'timeout':460                    self.log(f"[{data['label']}] 超时,收到{data['received']}字节")461462except queue.Empty:463pass464465# 定时检查466        self.root.after(100, self.check_queue)467468def log(self, message: str):469"""添加日志"""470        timestamp = time.strftime("%H:%M:%S")471        self.log_text.insert(tk.ENDf"[{timestamp}] {message}\n")472        self.log_text.see(tk.END)473474def clear_log(self):475"""清空日志"""476        self.log_text.delete(1.0, tk.END)477478def run(self):479"""启动GUI"""480try:481            self.root.mainloop()482finally:483if self.poller:484                self.poller.stop()485if self.port:486                self.port.close()487488489def test_basic_functions():490"""基础功能测试(不需要实际硬件)"""491print("=== Modbus RTU 基础功能测试 ===")492493# 测试CRC计算494    test_frame = b'\x01\x03\x00\x00\x00\x02'495    crc = calc_crc16(test_frame)496print(f"CRC测试: {test_frame.hex(' ').upper()} -> CRC: {crc.hex(' ').upper()}")497498# 测试读寄存器帧构造499    read_frame = build_read_holding_registers(102)500print(f"读寄存器帧: {read_frame.hex(' ').upper()}")501502# 测试写寄存器帧构造503    write_frame = build_write_single_register(10100)504print(f"写寄存器帧: {write_frame.hex(' ').upper()}")505506# 测试响应解析(模拟正常响应)507    mock_response = b'\x01\x03\x04\x00\x64\x00\xc8\xfa\x6e'  # 从站1,功能码3,4字节数据,值100和200508    values = parse_read_response(mock_response)509print(f"响应解析测试: {mock_response.hex(' ').upper()} -> 值: {values}")510511512if __name__ == "__main__":513# 运行基础测试514test_basic_functions()515516print("\n=== 启动GUI界面 ===")517518# 启动GUI应用519    app = ModbusGUI()520    app.run()

time.sleep(0.05) 这个帧间隔不是随意写的。RS485协议要求帧间隔至少3.5个字符时间,9600波特率下约为4ms,实际项目里建议留10~50ms的余量,避免总线上出现粘帧。


🖥️ 方案三:完整Tkinter GUI上位机

把前面两个模块组合起来,做一个完整的工业监控界面:

python1import queue2import threading3import time4from tkinter import ttk, scrolledtext5from typing import Optional6import tkinter as tk7from serialdemo.m2 import ModbusPollerRS485Port89class RS485Monitor(tk.Tk):10"""11    RS485工业监控上位机12    功能:串口配置、多设备轮询、实时数据显示、帧日志13    """    def __init__(self):14super().__init__()15        self.title("RS485工业通信监控")16        self.geometry("800x600")17        self.resizable(TrueTrue)1819        self._port = None20        self._poller = None21        self._data_queue = queue.Queue()22        self._data_vars: dict[str, tk.StringVar] = {}2324        self._build_ui()25        self._schedule_queue_check()2627#  UI构建28def _build_ui(self):29# 顶部:串口配置栏30        cfg_frame = ttk.LabelFrame(self, text="串口配置", padding=8)31        cfg_frame.pack(fill='x', padx=10, pady=6)3233        ttk.Label(cfg_frame, text="端口:").grid(row=0, column=0, padx=4)34        self._port_var = tk.StringVar()35        self._port_combo = ttk.Combobox(36            cfg_frame, textvariable=self._port_var, width=1037        )38        self._port_combo['values'] = RS485Port.list_ports()39        self._port_combo.grid(row=0, column=1, padx=4)4041        ttk.Label(cfg_frame, text="波特率:").grid(row=0, column=2, padx=4)42        self._baud_var = tk.StringVar(value='9600')43        ttk.Combobox(44            cfg_frame, textvariable=self._baud_var,45            values=['4800','9600','19200','38400','57600','115200'], width=846        ).grid(row=0, column=3, padx=4)4748        self._connect_btn = ttk.Button(49            cfg_frame, text="连接", command=self._toggle_connect50        )51        self._connect_btn.grid(row=0, column=4, padx=8)5253        self._status_var = tk.StringVar(value="未连接")54        ttk.Label(cfg_frame, textvariable=self._status_var,55                  foreground='red').grid(row=0, column=5, padx=4)5657# 中部:数据显示区58        data_frame = ttk.LabelFrame(self, text="实时数据", padding=8)59        data_frame.pack(fill='both', expand=True, padx=10, pady=4)6061# 预注册3个设备的显示位62for i, label in enumerate(['温度传感器(#1)''压力仪表(#2)''变频器(#3)']):63            ttk.Label(data_frame, text=label, width=16).grid(64                row=i, column=0, sticky='w', pady=465            )66            var = tk.StringVar(value='--')67            self._data_vars[label] = var68            ttk.Label(69                data_frame, textvariable=var,70                font=('Consolas'14'bold'), foreground='#0066cc'71            ).grid(row=i, column=1, sticky='w', padx=12)7273# 底部:帧日志74        log_frame = ttk.LabelFrame(self, text="通信日志", padding=6)75        log_frame.pack(fill='x', padx=10, pady=6)76        self._log = scrolledtext.ScrolledText(77            log_frame, height=8, font=('Consolas'9),78            state='disabled', wrap='none'79        )80        self._log.pack(fill='x')8182#  串口连接控制83def _toggle_connect(self):84if self._poller and self._poller._running:85            self._disconnect()86else:87            self._connect()8889def _connect(self):90        port_name = self._port_var.get()91if not port_name:92            self._log_msg("请先选择串口端口")93return94        self._port = RS485Port(95            port=port_name,96            baudrate=int(self._baud_var.get())97        )98if not self._port.open():99            self._status_var.set("连接失败")100return101102        self._poller = ModbusPoller(self._port, self._data_queue)103# 注册设备:(从站ID, 起始寄存器, 读取数量, 显示标签)104        self._poller.add_device(10x00002'温度传感器(#1)')105        self._poller.add_device(20x00002'压力仪表(#2)')106        self._poller.add_device(30x00002'变频器(#3)')107        self._poller.start()108109        self._status_var.set(f"已连接 {port_name}")110        self._connect_btn.config(text="断开")111        self._log_msg(f"串口 {port_name} @ {self._baud_var.get()} bps 已打开")112113def _disconnect(self):114if self._poller:115            self._poller.stop()116if self._port:117            self._port.close()118        self._status_var.set("未连接")119        self._connect_btn.config(text="连接")120        self._log_msg("串口已关闭")121122#  Queue轮询(主线程安全更新UI)123def _schedule_queue_check(self):124        self._check_queue()125        self.after(100, self._schedule_queue_check)  # 每100ms检查一次126127def _check_queue(self):128try:129while True:130                msg = self._data_queue.get_nowait()131                self._handle_msg(msg)132except queue.Empty:133pass134135def _handle_msg(self, msg: dict):136        label = msg.get('label''')137if msg['type'] == 'data':138            vals = msg.get('values'or []139            display = ' | '.join(str(v) for v in vals) if vals else 'CRC错误'140if label in self._data_vars:141                self._data_vars[label].set(display)142            self._log_msg(f"[{label}] {msg.get('raw','')} → {display}")143elif msg['type'] == 'timeout':144if label in self._data_vars:145                self._data_vars[label].set('超时')146            self._log_msg(f"[{label}] 响应超时,收到{msg['received']}字节")147elif msg['type'] == 'error':148            self._log_msg(f"[{label}] 错误: {msg['msg']}")149150def _log_msg(self, text: str):151        self._log.config(state='normal')152        self._log.insert('end'f"{time.strftime('%H:%M:%S')}  {text}\n")153        self._log.see('end')154        self._log.config(state='disabled')155156def on_close(self):157        self._disconnect()158        self.destroy()159160161if __name__ == '__main__':162    app = RS485Monitor()163    app.protocol("WM_DELETE_WINDOW", app.on_close)164    app.mainloop()

这里有一个细节值得专门说一下:after(100, self._schedule_queue_check) 是Tkinter里实现"定时任务"的标准方式,它在主线程的事件循环里执行,完全线程安全。很多人会用threading.Timer或者在子线程里直接调用label.config(),这两种做法在Windows下都可能触发随机的RuntimeError,而且只在高负载时才暴露,测试阶段很难发现。


⚡ 性能对比与实测数据

主线程同步阻塞的方案之所以CPU占用高,是因为serial.read()在等待超时的过程中会持续占用GIL,导致Tkinter的事件循环也被阻塞,界面响应迟钝,这是工控上位机开发里最常见的性能反模式。


🚨 常见踩坑汇总

1. 收发切换问题:普通USB转RS485模块通常支持自动收发切换,不需要手动控制RTS引脚。但部分工业级隔离模块需要通过rts引脚手动控制方向,发送前拉高,发送后拉低,时序不对会导致数据被自己截断。

2. 波特率不匹配:RS485设备出厂默认波特率不一定是9600,常见的还有4800、19200、38400。如果设备完全没有响应,第一步就是用串口调试工具逐个波特率试。

3. 超时时间设置timeout太短会导致数据还没到就关闭读取,太长会拖慢轮询周期。经验值:9600bps读取10字节的理论时间约为10ms,设置50~200ms的超时通常是合理范围。

4. 地址冲突:总线上多台设备的从站地址必须唯一,如果两台设备地址相同,它们会同时响应,信号叠加后主站收到的是乱码。地址通过设备自身的拨码开关或上位机配置工具设置。


🗺️ 学习路径延伸

掌握了这套基础之后,自然的下一步是引入pymodbus库来处理更复杂的Modbus功能码(如线圈读写、批量写多寄存器),以及把通信层和业务层进一步解耦,用发布-订阅模式替代直接的Queue传递,让代码在设备数量增长时依然好维护。

再往后,如果项目规模扩大到需要同时管理多条RS485总线,或者需要对接OPC UA、MQTT等工业物联网协议,那就到了考虑引入中间件层的时候了——但那是另一篇文章的话题了。


💬 聊聊你的经历

你在对接RS485设备时遇到过哪些让你印象深刻的坑?是CRC算错了排查了半天,还是多线程把界面搞崩了?欢迎在评论区分享你的实战经验,说不定能帮到正在踩同一个坑的人。

另外抛一个小挑战:如果总线上有一台设备偶发性不响应(不是超时,是响应帧CRC错误),你会怎么设计重试机制,同时又不影响其他设备的正常轮询? 这个问题在实际项目里挺有意思,思路不止一种。


#Python#串口编程#RS485#工业通信#Tkinter#上位机开发#Modbus

最新文章

随机文章

基本 文件 流程 错误 SQL 调试
  1. 请求信息 : 2026-07-10 12:48:57 HTTP/2.0 GET : https://f.mffb.com.cn/a/503887.html
  2. 运行时间 : 0.148107s [ 吞吐率:6.75req/s ] 内存消耗:4,797.88kb 文件加载:140
  3. 缓存信息 : 0 reads,0 writes
  4. 会话信息 : SESSION_ID=6b9169b52e90fcd54b216360d4a4185f
  1. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/public/index.php ( 0.79 KB )
  2. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/autoload.php ( 0.17 KB )
  3. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_real.php ( 2.49 KB )
  4. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/platform_check.php ( 0.90 KB )
  5. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/ClassLoader.php ( 14.03 KB )
  6. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_static.php ( 4.90 KB )
  7. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper.php ( 8.34 KB )
  8. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/helper.php ( 2.19 KB )
  9. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/helper.php ( 1.47 KB )
  10. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/stubs/load_stubs.php ( 0.16 KB )
  11. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Exception.php ( 1.69 KB )
  12. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Facade.php ( 2.71 KB )
  13. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/deprecation-contracts/function.php ( 0.99 KB )
  14. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap.php ( 8.26 KB )
  15. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap80.php ( 9.78 KB )
  16. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/Resources/functions/dump.php ( 1.49 KB )
  17. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-dumper/src/helper.php ( 0.18 KB )
  18. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/VarDumper.php ( 4.30 KB )
  19. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/App.php ( 15.30 KB )
  20. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Container.php ( 15.76 KB )
  21. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/container/src/ContainerInterface.php ( 1.02 KB )
  22. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/provider.php ( 0.19 KB )
  23. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Http.php ( 6.04 KB )
  24. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Str.php ( 7.29 KB )
  25. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Env.php ( 4.68 KB )
  26. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/common.php ( 0.03 KB )
  27. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/helper.php ( 18.78 KB )
  28. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Config.php ( 5.54 KB )
  29. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/app.php ( 0.95 KB )
  30. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cache.php ( 0.78 KB )
  31. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/console.php ( 0.23 KB )
  32. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cookie.php ( 0.56 KB )
  33. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/database.php ( 2.48 KB )
  34. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Env.php ( 1.67 KB )
  35. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/filesystem.php ( 0.61 KB )
  36. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/lang.php ( 0.91 KB )
  37. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/log.php ( 1.35 KB )
  38. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/middleware.php ( 0.19 KB )
  39. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/route.php ( 1.89 KB )
  40. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/session.php ( 0.57 KB )
  41. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/trace.php ( 0.34 KB )
  42. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/view.php ( 0.82 KB )
  43. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/event.php ( 0.25 KB )
  44. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Event.php ( 7.67 KB )
  45. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/service.php ( 0.13 KB )
  46. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/AppService.php ( 0.26 KB )
  47. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Service.php ( 1.64 KB )
  48. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Lang.php ( 7.35 KB )
  49. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/lang/zh-cn.php ( 13.70 KB )
  50. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/Error.php ( 3.31 KB )
  51. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/RegisterService.php ( 1.33 KB )
  52. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/services.php ( 0.14 KB )
  53. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/PaginatorService.php ( 1.52 KB )
  54. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ValidateService.php ( 0.99 KB )
  55. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ModelService.php ( 2.04 KB )
  56. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Service.php ( 0.77 KB )
  57. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Middleware.php ( 6.72 KB )
  58. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/BootService.php ( 0.77 KB )
  59. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Paginator.php ( 11.86 KB )
  60. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/Validate.php ( 63.20 KB )
  61. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Model.php ( 23.55 KB )
  62. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Attribute.php ( 21.05 KB )
  63. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/AutoWriteData.php ( 4.21 KB )
  64. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Conversion.php ( 6.44 KB )
  65. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/DbConnect.php ( 5.16 KB )
  66. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/ModelEvent.php ( 2.33 KB )
  67. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/RelationShip.php ( 28.29 KB )
  68. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Arrayable.php ( 0.09 KB )
  69. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Jsonable.php ( 0.13 KB )
  70. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/contract/Modelable.php ( 0.09 KB )
  71. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Db.php ( 2.88 KB )
  72. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/DbManager.php ( 8.52 KB )
  73. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Log.php ( 6.28 KB )
  74. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Manager.php ( 3.92 KB )
  75. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerTrait.php ( 2.69 KB )
  76. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerInterface.php ( 2.71 KB )
  77. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cache.php ( 4.92 KB )
  78. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/simple-cache/src/CacheInterface.php ( 4.71 KB )
  79. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Arr.php ( 16.63 KB )
  80. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/driver/File.php ( 7.84 KB )
  81. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/Driver.php ( 9.03 KB )
  82. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php ( 1.99 KB )
  83. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/Request.php ( 0.09 KB )
  84. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Request.php ( 55.78 KB )
  85. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/middleware.php ( 0.25 KB )
  86. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Pipeline.php ( 2.61 KB )
  87. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/TraceDebug.php ( 3.40 KB )
  88. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/middleware/SessionInit.php ( 1.94 KB )
  89. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Session.php ( 1.80 KB )
  90. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/driver/File.php ( 6.27 KB )
  91. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php ( 0.87 KB )
  92. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/Store.php ( 7.12 KB )
  93. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Route.php ( 23.73 KB )
  94. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleName.php ( 5.75 KB )
  95. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Domain.php ( 2.53 KB )
  96. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleGroup.php ( 22.43 KB )
  97. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Rule.php ( 26.95 KB )
  98. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleItem.php ( 9.78 KB )
  99. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/route/app.php ( 1.72 KB )
  100. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Route.php ( 4.70 KB )
  101. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/dispatch/Controller.php ( 4.74 KB )
  102. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Dispatch.php ( 10.44 KB )
  103. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/controller/Index.php ( 4.81 KB )
  104. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/BaseController.php ( 2.05 KB )
  105. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/facade/Db.php ( 0.93 KB )
  106. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/connector/Mysql.php ( 5.44 KB )
  107. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/PDOConnection.php ( 52.47 KB )
  108. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Connection.php ( 8.39 KB )
  109. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/ConnectionInterface.php ( 4.57 KB )
  110. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/builder/Mysql.php ( 16.58 KB )
  111. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Builder.php ( 24.06 KB )
  112. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseBuilder.php ( 27.50 KB )
  113. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Query.php ( 15.71 KB )
  114. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseQuery.php ( 45.13 KB )
  115. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php ( 7.43 KB )
  116. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php ( 3.26 KB )
  117. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php ( 20.07 KB )
  118. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ParamsBind.php ( 3.66 KB )
  119. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ResultOperation.php ( 7.01 KB )
  120. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/WhereQuery.php ( 19.37 KB )
  121. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php ( 7.11 KB )
  122. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php ( 2.63 KB )
  123. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/Transaction.php ( 2.77 KB )
  124. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/driver/File.php ( 5.96 KB )
  125. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php ( 0.86 KB )
  126. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/Channel.php ( 3.89 KB )
  127. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/event/LogRecord.php ( 1.02 KB )
  128. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/Collection.php ( 16.47 KB )
  129. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/View.php ( 1.70 KB )
  130. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/View.php ( 4.39 KB )
  131. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Response.php ( 8.81 KB )
  132. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/response/View.php ( 3.29 KB )
  133. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cookie.php ( 6.06 KB )
  134. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-view/src/Think.php ( 8.38 KB )
  135. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php ( 1.60 KB )
  136. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/Template.php ( 46.61 KB )
  137. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/driver/File.php ( 2.41 KB )
  138. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/contract/DriverInterface.php ( 0.86 KB )
  139. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/runtime/temp/067d451b9a0c665040f3f1bdd3293d68.php ( 11.98 KB )
  140. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Html.php ( 4.42 KB )
  1. CONNECT:[ UseTime:0.000713s ] mysql:host=127.0.0.1;port=3306;dbname=f_mffb;charset=utf8mb4
  2. SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.000812s ]
  3. SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.000324s ]
  4. SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.000333s ]
  5. SHOW FULL COLUMNS FROM `set` [ RunTime:0.000638s ]
  6. SELECT * FROM `set` [ RunTime:0.000232s ]
  7. SHOW FULL COLUMNS FROM `article` [ RunTime:0.000506s ]
  8. SELECT * FROM `article` WHERE `id` = 503887 LIMIT 1 [ RunTime:0.003231s ]
  9. UPDATE `article` SET `lasttime` = 1783658938 WHERE `id` = 503887 [ RunTime:0.011544s ]
  10. SELECT * FROM `fenlei` WHERE `id` = 66 LIMIT 1 [ RunTime:0.000322s ]
  11. SELECT * FROM `article` WHERE `id` < 503887 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.000435s ]
  12. SELECT * FROM `article` WHERE `id` > 503887 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.007736s ]
  13. SELECT * FROM `article` WHERE `id` < 503887 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.000675s ]
  14. SELECT * FROM `article` WHERE `id` < 503887 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.010625s ]
  15. SELECT * FROM `article` WHERE `id` < 503887 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.025198s ]
0.149779s