第四章:串口通信实战

串口通信,听起来是不是有点老古董?

说实话,我在做运动控制这行之前,也觉得这玩意儿过时了。直到我第一次调试一台老式伺服驱动器——那家伙只认RS232,USB口插上去根本没反应。嗯,从那以后,我再也不敢小看串口了。

今天咱们就来聊聊串口通信在运动控制里的实战。我会把RS232/RS485的区别、PySerial怎么用、数据帧怎么解析、CRC校验怎么实现,一次性讲清楚。

4.1 RS232 vs RS485:选哪个?

先解决一个最基础的问题:232和485,到底用哪个?

我个人习惯这样判断:

  • 距离短(<15米)、一对一通信:用RS232。简单,直接,电脑上就有DB9口。
  • 距离长(>15米)、多设备组网:用RS485。差分信号抗干扰强,能挂32个设备。

我在一个工厂项目里遇到过这种情况:电机驱动器在车间另一头,距离控制柜大概50米。用RS232,数据全是乱码。换成RS485,稳如老狗。你想想看,工业现场到处都是变频器、大电机,电磁干扰有多严重?RS485的差分传输,说白了就是靠两根线互相抵消干扰,这招很实用。

特性RS232RS485
传输距离约15米可达1200米
通信方式全双工半双工
节点数1对1最多32个节点
抗干扰强(差分信号)
典型应用调试、短距通信工业总线、多轴控制
小提示:如果你用的是USB转串口线,记得先装驱动。我见过太多人插上没反应,其实是驱动没装。

4.2 PySerial库:Python操作串口的利器

好了,硬件选型搞定了,接下来就是软件层面。Python里操作串口,PySerial是首选。没有之一。

安装很简单:

pip install pyserial

基本用法,我直接上代码:

import serial

# 打开串口
ser = serial.Serial(
    port='COM3',        # Windows下是COM口,Linux下是/dev/ttyUSB0
    baudrate=115200,    # 波特率,要和设备一致
    bytesize=8,         # 数据位,通常是8
    parity='N',         # 校验位:N无校验,E偶校验,O奇校验
    stopbits=1,         # 停止位,通常是1
    timeout=1           # 超时时间,单位秒
)

# 发送数据
ser.write(b'\x01\x03\x00\x00\x00\x01')

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

# 关闭串口
ser.close()

这里有个坑,我踩过:波特率一定要和设备匹配。有一次我调试一个步进电机驱动器,手册上写的是9600,我设成115200,结果读回来的数据全是0xFF。折腾了半小时才发现是波特率设错了。嗯,这种低级错误,犯过一次就记住了。

注意:串口打开后,其他程序就不能再用了。记得用完后调用close(),或者用with语句自动管理。

4.3 数据帧解析:从字节流里提取有用信息

串口通信的本质,就是收发字节流。但字节流本身没有意义,你得按照协议去解析。

常见的运动控制协议,比如Modbus RTU,它的数据帧结构是这样的:

| 地址码 | 功能码 | 数据区 | CRC校验 |
|  1字节  |  1字节  | N字节  |  2字节  |

举个例子,读取一个寄存器的值:

# 发送:读取地址为0x01的设备,寄存器地址0x0000,读取1个寄存器
request = bytes([0x01, 0x03, 0x00, 0x00, 0x00, 0x01, 0x84, 0x0A])

# 假设收到响应
response = bytes([0x01, 0x03, 0x02, 0x00, 0x64, 0xB8, 0x1E])

# 解析
slave_addr = response[0]      # 0x01
func_code = response[1]       # 0x03
data_len = response[2]        # 0x02,表示后面有2个字节数据
value = (response[3] << 8) | response[4]  # 0x0064 = 100
crc_received = (response[5] << 8) | response[6]  # 0x1EB8

print(f"从站地址: {slave_addr}")
print(f"功能码: {func_code}")
print(f"读取到的值: {value}")  # 输出100

解析的时候,我习惯用struct模块,更简洁:

import struct

# 假设收到6个字节的响应
raw = b'\x01\x03\x02\x00\x64\xB8\x1E'
addr, func, length = struct.unpack_from('!BBH', raw, 0)
value = struct.unpack_from('!H', raw, 3)[0]

print(f"地址={addr}, 功能码={func}, 长度={length}, 值={value}")
核心要点:解析数据帧,关键是搞清楚协议格式。每个字节的含义,偏移量是多少,都要和手册一一对应。别猜,猜错了就是乱码。

4.4 CRC校验:确保数据没被篡改

串口通信在工业现场,干扰是常态。数据在传输过程中可能被篡改,CRC校验就是用来检测这个的。

Modbus RTU用的是CRC-16,多项式是0x8005。我直接给你一个现成的实现:

def crc16_modbus(data: bytes) -> int:
    """计算Modbus CRC16校验值"""
    crc = 0xFFFF
    for byte in data:
        crc ^= byte
        for _ in range(8):
            if crc & 0x0001:
                crc = (crc >> 1) ^ 0xA001
            else:
                crc >>= 1
    return crc

# 使用示例
data = bytes([0x01, 0x03, 0x00, 0x00, 0x00, 0x01])
crc = crc16_modbus(data)
print(f"CRC校验值: {crc:#06x}")  # 输出 0x840A

# 发送时,把CRC附加到数据末尾
frame = data + struct.pack('<H', crc)
print(frame.hex())  # 输出 010300000001840A

收到响应后,也要校验:

def verify_crc(frame: bytes) -> bool:
    """验证CRC是否正确"""
    if len(frame) < 2:
        return False
    data = frame[:-2]
    crc_received = struct.unpack('<H', frame[-2:])[0]
    crc_calculated = crc16_modbus(data)
    return crc_received == crc_calculated

# 验证
response = bytes([0x01, 0x03, 0x02, 0x00, 0x64, 0xB8, 0x1E])
if verify_crc(response):
    print("CRC校验通过,数据可靠")
else:
    print("CRC校验失败,数据可能被篡改")
避坑指南:我曾经遇到过一种情况——CRC计算正确,但设备就是不响应。后来发现,CRC字节的顺序搞反了。Modbus RTU要求低字节在前,高字节在后。用struct.pack('<H', crc)就能保证顺序正确。

4.5 完整实战:读取电机编码器位置

好了,理论讲完了,咱们来一个完整的例子。假设我们要通过RS485读取一个伺服电机的编码器位置。

协议约定:

  • 从站地址:0x01
  • 功能码:0x03(读取寄存器)
  • 编码器位置寄存器地址:0x0020
  • 数据长度:2个寄存器(4字节)
import serial
import struct
import time

def crc16_modbus(data: bytes) -> int:
    crc = 0xFFFF
    for byte in data:
        crc ^= byte
        for _ in range(8):
            if crc & 0x0001:
                crc = (crc >> 1) ^ 0xA001
            else:
                crc >>= 1
    return crc

def build_request(slave_id, func_code, reg_addr, reg_count):
    """构建Modbus请求帧"""
    data = bytes([slave_id, func_code, reg_addr >> 8, reg_addr & 0xFF,
                  reg_count >> 8, reg_count & 0xFF])
    crc = crc16_modbus(data)
    return data + struct.pack('<H', crc)

def parse_response(response):
    """解析响应,返回编码器位置"""
    if len(response) < 5:
        return None
    # 校验CRC
    data = response[:-2]
    crc_received = struct.unpack('<H', response[-2:])[0]
    if crc16_modbus(data) != crc_received:
        print("CRC校验失败")
        return None
    # 解析数据:从第3个字节开始,取4个字节(2个寄存器)
    position = struct.unpack('>I', response[3:7])[0]
    return position

# 主程序
ser = serial.Serial('COM3', 115200, timeout=1)
request = build_request(0x01, 0x03, 0x0020, 0x0002)
ser.write(request)
time.sleep(0.1)
response = ser.read(9)  # 地址+功能码+长度+4字节数据+2字节CRC = 9字节
position = parse_response(response)
if position is not None:
    print(f"当前编码器位置: {position} 脉冲")
ser.close()

这个例子,我在调试一个三轴平台时用过。当时编码器反馈一直跳变,排查了半天,发现是RS485的终端电阻没加。加上120欧姆电阻后,数据就稳了。嗯,细节决定成败。

4.6 知识体系总览

说了这么多,咱们用一张图来总结一下串口通信的核心逻辑:

串口通信实战知识体系 硬件层 RS232 / RS485 软件层 PySerial库 协议层 Modbus RTU 数据帧解析 CRC校验实现 编码器位置读取 常见坑点 波特率不匹配 | CRC字节顺序 | 终端电阻缺失 | 串口未关闭 串口通信 = 硬件选型 + 软件驱动 + 协议解析 + 校验保障

这张图把串口通信的四个层次都串起来了。从硬件选型,到软件驱动,再到协议解析和校验,每一步都不能少。

我的经验:刚开始做串口通信时,别急着写代码。先把硬件接好,用串口调试助手发几个命令,确认设备能响应。这一步能省你后面80%的调试时间。

好了,串口通信实战就讲到这里。代码都在上面了,你拿去就能用。记住,工业通信没有玄学,只有协议和细节。把CRC校验写对,把超时时间设合理,你的通信就稳了。


公众号:蓝海资料掘金营,微信deep3321