Python串口助手怎么实现十六进制(Hex)数据收发与显示

Luna
Luna
发布时间:2026-11-05| 8人浏览过
原创

本文详细讲解如何使用Python及pyserial库实现串口助手中的十六进制(Hex)数据收发与显示功能,包含底层原理、数据清洗、偶数对齐、格式化输出及完整代码示例。

应用开发 / 串口通讯 运行环境:Python 3.8+ / pyserial 数据类型:bytes 与 Hex 互转
Python串口助手怎么实现十六进制(Hex)数据收发与显示
核心解决路径:Python 实现串口十六进制(Hex)收发与显示的核心在于掌握 bytes(原生二进制字节序列)与十六进制字符串之间的转换。底层驱动 pyserial 始终通过 bytes 与硬件进行全双工数据交换;发送时通过输入清洗与 bytes.fromhex() 将文本打包为字节串写入端口;接收时读取原始 bytes,并通过 bytes.hex(' ').upper() 格式化为大写空格分隔的十六进制字符串进行渲染展示。

一、串口收发模式的本质:ASCII 文本与 Hex 字节流的差异

很多初学者在编写 Python 串口助手时,最容易混淆的是“字符”与“字节”的关系。在物理层面上,RS-232、RS-485 或 TTL 串口传输的只有高低电平构成的位流(Bit),串口控制器(UART)以 8 位为一个字节(Byte)向操作系统交付。串口硬件本身没有“文本模式”或“十六进制模式”的概念,这两种模式完全是上位机软件(Python 脚本)对字节数据的不同理解与呈现策略。

Python串口助手怎么实现十六进制(Hex)数据收发与显示
图 1:ASCII 文本模式与十六进制(Hex)模式通信机制对比

为了在实际应用中做出正确选择,我们需要在相同串口通道条件下,对比这两种数据形态的关键差异:

对比维度 ASCII 文本模式 十六进制 (Hex) 二进制模式
Python 数据类型 str(字符串) bytes(不可变字节序列)
转换方式 依赖编码表:s.encode('utf-8') 与 b.decode('utf-8') 面向原生数值:bytes.fromhex() 与 b.hex()
特殊字节容忍度 极差。遇到 0x00、0xFF、控制符时可能截断、显示乱码或抛出 UnicodeDecodeError 优秀。0x00 ~ 0xFF 共 256 种状态全部等权透传,物理值 1:1 无损映射
数据帧占用体积 数值 255 若传字符串 "255" 需占 3 个字节,效率较低 数值 255 仅占 1 个字节(0xFF),高带宽压缩与高密度传输
典型应用协议 蜂窝模组 AT 指令、GPS NMEA-0183 语句、控制台 CLI 交互 Modbus RTU 工业总线、传感器私有帧、固件升级包(IAP)、带 CRC 校验帧

理解这种差异至关重要:在工业控制与传感器数据采集中,由于校验码(如 CRC16)、传感器读数很可能产生 0x00 或不可见字符,如果误用 ASCII 模式的 .decode() 处理,不仅界面乱码,整个接收逻辑还会因解码异常而中断崩溃。因此,十六进制收发是工业级串口助手必不可少的核心能力。

二、开发准备与串口初始化配置

在 Python 中实现串口通信,标准的第三方库是 pyserial。注意在安装时应安装 pyserial,切勿误装同名的 serial 库:

pip install pyserial

安装完成后,在代码中通过 import serial 导入。初始化串口对象时,需要明确端口号(Port)、波特率(Baudrate)以及超时时间(Timeout)。其中 timeout 参数对 Hex 收发的平稳运行至关重要:

import serial

# 初始化串口对象
# Windows 下端口通常为 'COM3', 'COM4' 等;Linux/macOS 下通常为 '/dev/ttyUSB0', '/dev/tty.usbserial-*'
ser = serial.Serial(
    port='COM3',
    baudrate=115200,
    bytesize=serial.EIGHTBITS,
    parity=serial.PARITY_NONE,
    stopbits=serial.STOPBITS_ONE,
    timeout=0.5  # 读操作超时时间设置为 0.5 秒
)
提示:若设置 timeout=None,ser.read() 会无限期阻塞直至请求的字节数全部到达;若设置 timeout=0 则为纯非阻塞模式,调用立即返回;合理设置 timeout=0.2 ~ 0.5 秒,可以在保持响应的同时防止主线程死锁。

三、十六进制数据发送实现:数据清洗与打包

在图形化串口助手或命令行交互中,用户输入的十六进制数据往往形式多样,例如 "AA BB 01"、"0xAA 0xBB 0x01"、"AABB01" 或包含尾部空格换行。Python 的 ser.write() 只接受 bytes 类型作为参数。因此,发送模块必须经历五个步骤:接收原始输入、清洗无效符号、奇数长度校验与补零、构建 bytes 对象、写入串口。

Python串口助手怎么实现十六进制(Hex)数据收发与显示
图 2:十六进制发送数据处理流水线

1. 清洗输入文本与偶数补齐

每个十六进制字节严格由 2 位十六进制字符(0-9, A-F)构成。在调用转换函数前,必须剥除多余的空格、逗号与 0x 前缀。如果用户输入的有效字符数为奇数(例如输入 "A" 或 "10A"),直接调用解析会导致报错,应当在高位补 "0",将其规范化为 "0A" 或 "010A"。

2. 使用 bytes.fromhex() 完成类型转换

从 Python 3 起,内置的 bytes.fromhex() 是最高效且标准的做法。它会自动忽略字符串中间的单空格(Python 3.8+),但为了兼容包含前缀与符号的混杂输入,我们编写一个稳健的发送辅助函数:

import re
import serial

def send_hex_data(ser: serial.Serial, hex_string: str) -> int:
    """
    清洗用户输入的十六进制字符串,并将其打包成 bytes 发送到串口
    :param ser: 已打开的 serial.Serial 实例
    :param hex_string: 用户输入的十六进制文本,如 "0xAA, bb 01 f"
    :return: 实际发送的字节数
    """
    if not ser.is_open:
        raise serial.SerialException("串口未打开,无法发送数据")

    # 步骤 1 & 2: 过滤 0x 前缀,移除所有非十六进制字符(只保留 0-9, a-f, A-F)
    clean_hex = re.sub(r'0[xX]', '', hex_string)
    clean_hex = re.sub(r'[^0-9a-fA-F]', '', clean_hex)

    if not clean_hex:
        return 0  # 无有效发送内容

    # 步骤 3: 奇数位检查与高位补零
    if len(clean_hex) % 2 != 0:
        clean_hex = '0' + clean_hex

    # 步骤 4: 转换为 bytes 字节串
    try:
        data_to_send = bytes.fromhex(clean_hex)
    except ValueError as err:
        raise ValueError(f"十六进制解析失败: {err}")

    # 步骤 5: 写入串口
    bytes_written = ser.write(data_to_send)
    ser.flush()  # 确保缓冲区数据全部刷入硬件
    return bytes_written
执行发送示例与输出状态
# 示例 1:输入含空格与混合大小写的字符串
send_hex_data(ser, "AA bb 01 03 00 01")
>>> 成功写入 6 字节: b'ª» '

# 示例 2:输入包含 0x 前缀和奇数位的控制帧
send_hex_data(ser, "0x01, 0x3, 0x0A")
>>> 自动清洗补零为 "01030A",成功写入 3 字节: b' '

四、十六进制数据接收与显示:缓冲读取与格式化

串口数据在物理线路上是连续的流,当下位机(如单片机)发送一串字节时,操作系统的串口接收缓冲区会逐步蓄积数据。Python 串口助手若要实现稳定、美观的 Hex 显示,需要解决“数据帧读取”与“格式化展示”两个核心环节。

Python串口助手怎么实现十六进制(Hex)数据收发与显示
图 3:串口字节流接收、断帧与十六进制格式化显示管道

1. 检测待读字节并处理分包断帧

串口通信没有天然的数据包边界。例如从机返回 8 个字节的 Modbus 响应,因为波特率传输耗时或操作系统调度,Python 可能先读到 3 个字节,隔了 2 毫秒又读到 5 个字节。若直接读到即打印,UI 上的十六进制数据就会被拆成破碎的多行。

常见的断帧策略是“字符超时判断”(类似 Modbus 协议中的 3.5 字符时间):当 ser.in_waiting > 0 时,稍作短暂停顿(如 time.sleep(0.02)),若后续不再有新字节涌入,则认为当前帧发送完毕,一次性将缓冲区字节全部读出。

2. 将原始字节格式化为大写 Hex 字符串

从串口读出的数据类型是 bytes。在 Python 3.8 及以上版本中,bytes 对象自带了支持分隔符的 hex() 方法,一行代码即可输出标准大写的 Hex 串:

# Python 3.8+ 原生高效方法
raw_data = b'ªU '
display_hex = raw_data.hex(' ').upper()
print(display_hex)
# 输出: "AA 55 01 03 00 01"

如果在需要兼容较早版本 Python 的环境中,可以使用列表推导式配合格式化字符串:

# 兼容旧版本 Python 的等价写法
display_hex = ' '.join(f'{b:02X}' for b in raw_data)

五、完整可运行代码:具备 Hex 收发的控制台串口助手

下面是一个完整的、工业可用的控制台串口助手示例。该脚本在一个后台守护线程中持续监听串口接收事件并按 Hex 格式化输出,主线程负责接收用户的十六进制输入并调用发送逻辑:

# -*- coding: utf-8 -*-
"""
Python 串口十六进制 (Hex) 收发助手完整参考实现
环境依赖: pip install pyserial
"""
import time
import threading
import datetime
import re
import serial

class HexSerialAssistant:
    def __init__(self, port: str, baudrate: int = 115200):
        self.port = port
        self.baudrate = baudrate
        self.ser = None
        self.is_running = False
        self.rx_thread = None

    def open(self):
        """打开串口并启动后台接收监听线程"""
        try:
            self.ser = serial.Serial(
                port=self.port,
                baudrate=self.baudrate,
                bytesize=serial.EIGHTBITS,
                parity=serial.PARITY_NONE,
                stopbits=serial.STOPBITS_ONE,
                timeout=0.1
            )
            self.is_running = True
            self.rx_thread = threading.Thread(target=self._rx_loop, daemon=True)
            self.rx_thread.start()
            print(f"[系统] 成功打开串口 {self.port},波特率 {self.baudrate}")
        except serial.SerialException as err:
            print(f"[错误] 打开串口失败: {err}")
            raise

    def send_hex(self, hex_input: str) -> bool:
        """格式化并发送十六进制数据"""
        if not self.ser or not self.ser.is_open:
            print("[警告] 串口未连接,无法发送")
            return False

        # 过滤非 16 进制字符
        clean = re.sub(r'0[xX]', '', hex_input)
        clean = re.sub(r'[^0-9a-fA-F]', '', clean)
        if not clean:
            print("[提示] 发送内容为空或无有效 Hex 字符")
            return False

        # 奇数位补零
        if len(clean) % 2 != 0:
            clean = '0' + clean

        try:
            payload = bytes.fromhex(clean)
            self.ser.write(payload)
            self.ser.flush()
            now_str = datetime.datetime.now().strftime("%H:%M:%S.%f")[:-3]
            formatted_show = payload.hex(' ').upper()
            print(f"[{now_str}] [TX 发送 {len(payload)}B]: {formatted_show}")
            return True
        except Exception as err:
            print(f"[错误] 发送失败: {err}")
            return False

    def _rx_loop(self):
        """后台数据接收与断帧显示循环"""
        buffer = bytearray()
        last_receive_time = 0

        while self.is_running:
            try:
                waiting = self.ser.in_waiting
                if waiting > 0:
                    chunk = self.ser.read(waiting)
                    buffer.extend(chunk)
                    last_receive_time = time.time()
                else:
                    # 当接收缓冲区有数据,且超过 30ms 无后续字节输入时,判定一帧接收完毕
                    if len(buffer) > 0 and (time.time() - last_receive_time > 0.03):
                        now_str = datetime.datetime.now().strftime("%H:%M:%S.%f")[:-3]
                        hex_display = bytes(buffer).hex(' ').upper()
                        print(f"[{now_str}] [RX 接收 {len(buffer)}B]: {hex_display}")
                        buffer.clear()
                    time.sleep(0.01)
            except serial.SerialException:
                break
            except Exception as e:
                print(f"[异常] 接收处理错误: {e}")
                break

    def close(self):
        """安全释放串口资源"""
        self.is_running = False
        if self.rx_thread and self.rx_thread.is_alive():
            self.rx_thread.join(timeout=1.0)
        if self.ser and self.ser.is_open:
            self.ser.close()
            print("[系统] 串口已安全关闭")

# 交互式执行入口
if __name__ == '__main__':
    # 请根据实际开发板端口调整,例如 COM3 或 /dev/ttyUSB0
    target_port = "COM3"
    assistant = HexSerialAssistant(port=target_port, baudrate=115200)

    try:
        assistant.open()
        print("--- 已进入 Hex 交互模式 (输入 quit 退出) ---")
        print("请输入要发送的 Hex 数据 (如: AA 55 01 02 或 0xA0 0x11):")
        while True:
            user_input = input("> ")
            if user_input.strip().lower() in ['quit', 'exit']:
                break
            if user_input.strip():
                assistant.send_hex(user_input)
    except KeyboardInterrupt:
        print("\n[系统] 捕获中断信号,正在退出...")
    finally:
        assistant.close()
交互运行输出效果示例
[系统] 成功打开串口 COM3,波特率 115200
--- 已进入 Hex 交互模式 (输入 quit 退出) ---
请输入要发送的 Hex 数据 (如: AA 55 01 02 或 0xA0 0x11):
> 01 03 00 00 00 02 C4 0B
[11:21:04.102] [TX 发送 8B]: 01 03 00 00 00 02 C4 0B
[11:21:04.135] [RX 接收 9B]: 01 03 04 00 19 00 00 5B 9E
> aa 55 01
[11:21:08.540] [TX 发送 3B]: AA 55 01
[11:21:08.572] [RX 接收 4B]: AA 55 01 00

六、常见问题排查与避坑指南

在实际串口项目落地时,开发者经常会遭遇几类隐蔽问题,以下是排查与解决方案:

1. 为什么发送包含 0x00 后的数据在对方设备上被截断?
在部分硬件固件(如 C/C++ 编写的单片机程序)中,若下位机误将接收缓冲当做 char* 字符串并使用 strlen() 计算长度,遇到 0x00(ASCII 的 NUL)就会判定为字符串结束。排查建议:检查单片机串口接收中断,确保以实际接收的字节计数值(DMA 计数或中断计数)为准,而非依靠字符串结束符。
2. 接收时数据出现“粘包”或“碎包”怎么解决?
如果串口助手打印出来的十六进制数据一会儿只有 2 个字节,一会儿又跟上一帧连在一起,这是典型的分包断帧失步。解决措施:(1)对于固定帧结构协议(如 Modbus),先解析帧头与长度字段,收满指定长度再投递显示;(2)对于不定长协议,采用时间间隔断帧算法,保证每包数据接收间隔超过 3.5 个字符时间后才确认断包。
3. 校验码(CRC16/CheckSum)在 Hex 数组中的大小端字节序问题
在计算例如 Modbus RTU 的 CRC16 时,计算结果为一个 16 位的整数(如 0xC40B)。在转换为 Hex 字节流发送时,必须确认设备要求的是低字节在前(小端,如 b'Ä')还是高字节在前(大端,如 b'Ä')。在 Python 中建议使用 struct.pack('<H', crc)(小端)或 struct.pack('>H', crc)(大端)进行转换,避免手动移位产生错误。
Python串口助手怎么实现十六进制(Hex)数据收发与显示
图 4:Python 串口助手 Hex 收发全链路架构与速查总结

七、总结

在 Python 串口助手中实现十六进制收发与显示,本质并不复杂。核心要领可以凝练为两句话:

  • 发送管线:“先清洗,再补齐,bytes.fromhex() 转字节,write() 写入缓冲区”。
  • 接收管线:“定时轮询查 in_waiting,间隔断帧保完整,bytes.hex(' ').upper() 转十六进制可视化”。

只要严守 bytes 数据通道,避免对二进制协议帧执行任何文本编码转换,即可实现高可靠、零丢包、全透传的专业串口调试工具。

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。