3. 串口通信基础:UART协议简介、Python的pySerial库安装与基本使用、打开与关闭串口

好,咱们进入第三章。这一章聊的是串口通信,也就是UART。说实话,做嵌入式固件开发,串口就是你的耳朵和嘴巴。没有它,你连芯片在干什么都不知道。

我记得刚入行那会儿,有一次调试一块新板子,焊好电源、接好JTAG,结果程序死活跑不起来。折腾了两天,最后发现是串口没配置对,打印信息全丢了。从那以后,我养成了一个习惯:任何板子到手,第一件事就是先把串口调通。

3.1 UART协议简介

UART,全称是Universal Asynchronous Receiver/Transmitter,通用异步收发器。说白了,就是一种串行通信协议,用一根线发数据,另一根线收数据。

为什么叫异步?因为它没有单独的时钟线。收发双方靠约定好的波特率来同步。你想想看,两个人约好每分钟说多少个字,那就不用看表也能对上节奏。

UART的数据帧结构是这样的:

起始位 数据位 校验位 停止位
1位(固定为0) 5~9位(常用8位) 可选(奇/偶/无) 1~2位(固定为1)

常见的配置是:115200波特率、8位数据位、无校验、1位停止位,简写为115200-8-N-1。我在项目中几乎都用这个配置,除非遇到老旧的设备才用9600。

关键参数:

  • 波特率:每秒传输的符号数。常见值:9600、19200、38400、115200、921600
  • 数据位:一帧中实际数据的位数。ASCII字符用7位,二进制数据用8位
  • 停止位:帧结束的标志。1位最常用,2位用于慢速设备
  • 校验位:简单的错误检测。实际项目中我很少用,因为上层协议通常有更可靠的校验

为什么会用115200?因为它是标准晶振频率的分频结果,误差最小。我曾经试过用非标准波特率,结果在高温下通信就丢包了。嗯,这里要注意,波特率误差超过2%就容易出问题。

3.2 Python的pySerial库安装

Python操作串口,最常用的库就是pySerial。它跨平台,Windows、Linux、macOS都能用。

安装很简单,一行命令搞定:

pip install pyserial

如果你用的是Python 3,记得用pip3:

pip3 install pyserial

我个人习惯在虚拟环境里装,避免污染全局环境。你可以这样:

python -m venv serial_env
source serial_env/bin/activate  # Linux/macOS
# 或者 serial_env\Scripts\activate  # Windows
pip install pyserial

小提示:安装完成后,可以用 python -c "import serial; print(serial.__version__)" 验证是否安装成功。如果没报错,就说明装好了。

3.3 pySerial基本使用

先来看一个最简单的例子:打开串口,发一条数据,然后关闭。

import serial

# 打开串口
ser = serial.Serial(
    port='COM3',        # Windows下用COM口,Linux下用/dev/ttyUSB0或/dev/ttyAMA0
    baudrate=115200,
    bytesize=8,
    parity='N',         # N:无校验, E:偶校验, O:奇校验
    stopbits=1,
    timeout=1           # 超时时间,单位秒
)

# 发送数据
ser.write(b'Hello, UART!\n')

# 读取数据
data = ser.read(10)     # 读取10个字节
print(data)

# 关闭串口
ser.close()

这段代码看起来简单,但有几个坑我得提醒你。

避坑指南:

  • 端口号问题:Windows上COM10以上要用 \\\\.\\COM10 这种写法,否则打不开。我曾经被这个坑过一整个下午。
  • 超时设置:timeout=None表示一直等,timeout=0表示非阻塞。我建议设个1秒,既不会卡死,也不会漏数据。
  • 写数据要加b:pySerial要求发送bytes类型,字符串要加b前缀或者用.encode()转换。

3.4 打开与关闭串口的正确姿势

很多新手会犯一个错误:打开串口后忘记关闭。结果下次再打开就报错"端口被占用"。

正确的做法是用 with 语句,它会自动管理资源:

import serial

with serial.Serial('COM3', 115200, timeout=1) as ser:
    ser.write(b'Hello\n')
    response = ser.readline()
    print(response)
# 退出with块后,串口自动关闭

如果你需要手动控制,那就用try-finally:

ser = serial.Serial('COM3', 115200, timeout=1)
try:
    ser.write(b'AT\r\n')
    data = ser.read(100)
finally:
    ser.close()

我个人更推荐with语句,代码更简洁,也不容易漏掉close。

3.5 检测可用串口

有时候你不知道设备插在了哪个端口上。pySerial提供了一个工具函数:

import serial.tools.list_ports

ports = serial.tools.list_ports.comports()
for port in ports:
    print(f"端口: {port.device}, 描述: {port.description}")

输出大概是这样:

端口: COM3, 描述: USB Serial Port (COM3)
端口: COM5, 描述: Arduino Uno (COM5)

这样你就能找到正确的端口号了。我在项目中经常用这个来写自动检测脚本,省得每次手动改代码。

3.6 实战小技巧

最后分享几个我常用的技巧:

  • 清空缓冲区:打开串口后,先调用 ser.reset_input_buffer()ser.reset_output_buffer(),把之前残留的数据清掉。不然一读就是一堆乱码。
  • 读取一行:用 ser.readline() 读取直到换行符。注意设置timeout,否则可能卡死。
  • 读取所有可用数据:用 ser.in_waiting 获取缓冲区字节数,然后 ser.read(ser.in_waiting) 一次性读完。
# 清空缓冲区
ser.reset_input_buffer()
ser.reset_output_buffer()

# 读取一行
line = ser.readline().decode('utf-8').strip()

# 读取所有可用数据
if ser.in_waiting > 0:
    data = ser.read(ser.in_waiting)

好了,这一章就到这里。串口通信是固件调试的基础,一定要熟练掌握。下一章我们会讲如何用pySerial实现自动烧录,那才是真正的实战环节。