第四章:串口通信实战
串口通信,听起来是不是有点老古董?
说实话,我在做运动控制这行之前,也觉得这玩意儿过时了。直到我第一次调试一台老式伺服驱动器——那家伙只认RS232,USB口插上去根本没反应。嗯,从那以后,我再也不敢小看串口了。
今天咱们就来聊聊串口通信在运动控制里的实战。我会把RS232/RS485的区别、PySerial怎么用、数据帧怎么解析、CRC校验怎么实现,一次性讲清楚。
4.1 RS232 vs RS485:选哪个?
先解决一个最基础的问题:232和485,到底用哪个?
我个人习惯这样判断:
- 距离短(<15米)、一对一通信:用RS232。简单,直接,电脑上就有DB9口。
- 距离长(>15米)、多设备组网:用RS485。差分信号抗干扰强,能挂32个设备。
我在一个工厂项目里遇到过这种情况:电机驱动器在车间另一头,距离控制柜大概50米。用RS232,数据全是乱码。换成RS485,稳如老狗。你想想看,工业现场到处都是变频器、大电机,电磁干扰有多严重?RS485的差分传输,说白了就是靠两根线互相抵消干扰,这招很实用。
| 特性 | RS232 | RS485 |
|---|---|---|
| 传输距离 | 约15米 | 可达1200米 |
| 通信方式 | 全双工 | 半双工 |
| 节点数 | 1对1 | 最多32个节点 |
| 抗干扰 | 弱 | 强(差分信号) |
| 典型应用 | 调试、短距通信 | 工业总线、多轴控制 |
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。折腾了半小时才发现是波特率设错了。嗯,这种低级错误,犯过一次就记住了。
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校验失败,数据可能被篡改")
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 知识体系总览
说了这么多,咱们用一张图来总结一下串口通信的核心逻辑:
这张图把串口通信的四个层次都串起来了。从硬件选型,到软件驱动,再到协议解析和校验,每一步都不能少。
好了,串口通信实战就讲到这里。代码都在上面了,你拿去就能用。记住,工业通信没有玄学,只有协议和细节。把CRC校验写对,把超时时间设合理,你的通信就稳了。
公众号:蓝海资料掘金营,微信deep3321