第4章:串口通信实战:RS232/RS485原理、pySerial库详解、串口参数配置、数据收发与校验

各位同学,欢迎来到串口通信实战这一章。

说实话,在工控领域摸爬滚打这么多年,串口通信是我打交道最多的东西。你想想看,从PLC到变频器,从温控表到扫码枪,十个设备里至少有七八个都带着串口。我甚至遇到过一台老旧的数控机床,上位机软件都找不到了,最后硬是靠串口把它给“盘活”了。

这一章,咱们就把串口通信彻底讲透。从物理层的RS232/RS485区别,到Python里怎么用pySerial库,再到实际项目中的数据收发和校验,一步到位。

4.1 RS232与RS485:物理层的那些事儿

很多新手容易混淆RS232和RS485。其实说白了,它们俩都是串口通信的物理层标准,但脾气秉性完全不同。

4.1.1 RS232:点对点的老将

RS232是最早的串口标准,我刚开始做项目时用的就是它。它的特点是:

  • 传输距离短:一般不超过15米。我在一个车间里试过拉20米的线,数据就开始丢包了。
  • 速率不高:常见的是9600bps、115200bps。再高就容易出错。
  • 只能点对点:一个串口只能接一个设备。想接多个?不行。
  • 电压摆幅大:逻辑“1”是-3V到-15V,逻辑“0”是+3V到+15V。抗干扰能力还行,但功耗大。

我个人习惯,RS232主要用于调试口,或者连接那些距离近、速率要求不高的设备,比如老式的条码枪、串口打印机。

4.1.2 RS485:工业现场的主力军

RS485就不一样了。它是我在工业项目中最常用的串口标准。为什么?

  • 传输距离远:理论上一千米没问题。我在一个大型水处理项目中,用RS485拉了800米的线,照样稳定通信。
  • 支持多节点:一条总线上可以挂32个设备(甚至更多)。你想想看,一个车间里几十个传感器,用RS485一条线就搞定了。
  • 差分信号:用两根线(A和B)传输,抗共模干扰能力极强。电机启动时那种强干扰,RS232可能就挂了,但RS485稳如老狗。
  • 半双工:同一时刻只能发送或接收。这需要我们在软件里做好收发切换。

嗯,这里要注意:RS485虽然好,但必须加终端电阻。我曾经在一个项目中忘了加,结果总线上的信号反射得一塌糊涂,数据全是乱码。后来老老实实加了120欧姆的电阻,问题立刻解决。

4.1.3 选型对比表

特性 RS232 RS485
传输方式 单端 差分
最大距离 约15米 约1200米
最大节点数 1对1 32个(标准)
工作模式 全双工 半双工
抗干扰能力 一般
典型应用 调试口、短距设备 工业总线、传感器网络

4.2 pySerial库详解:Python操作串口的利器

好了,物理层讲完了,咱们来看看软件层。在Python里操作串口,最常用的就是pySerial库。我几乎所有的工控上位机项目都用它,稳定、简单、跨平台。

4.2.1 安装与基本使用

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

pip install pyserial

然后就可以在代码里导入使用了。我一般这样写:

import serial

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

# 发送数据
ser.write(b'Hello, PLC!')

# 读取数据
data = ser.read(10)     # 读取10个字节
line = ser.readline()   # 读取一行

# 关闭串口
ser.close()

你看,是不是很简单?但这里有个坑:打开串口前一定要确认端口号。我遇到过好几次,同事把USB转串口插上后,系统分配的COM口变了,结果程序一直报错。所以,我建议在代码里加个自动检测端口的功能,或者用配置文件来管理。

4.2.2 常用方法与属性

pySerial提供了很多实用的方法和属性,我挑几个最常用的:

  • ser.is_open:检查串口是否打开。
  • ser.in_waiting:返回接收缓冲区中的字节数。这个很有用,可以避免阻塞读取。
  • ser.flushInput():清空接收缓冲区。我在每次发送命令前都会调用一下,确保没有残留数据干扰。
  • ser.flushOutput():清空发送缓冲区。
  • ser.setDTR()ser.setRTS():控制硬件流控引脚。有些老设备需要这个来复位。

举个例子,我经常这样写一个循环读取的函数:

def read_response(ser, expected_bytes):
    """读取响应数据,直到收到指定字节数或超时"""
    data = b''
    while len(data) < expected_bytes:
        if ser.in_waiting > 0:
            data += ser.read(ser.in_waiting)
        else:
            # 没有数据,等待一小会儿
            time.sleep(0.01)
    return data

这个函数我在很多项目里都用过,比如读取温控表的温度值、读取变频器的运行状态等。

4.3 串口参数配置:波特率、数据位、校验位、停止位

串口通信要成功,通信双方必须“说同一种语言”。这个语言就是串口参数。我见过太多因为参数不匹配导致通信失败的案例了。

4.3.1 四大参数详解

  • 波特率(Baud Rate):每秒传输的符号数。常见的有9600、19200、38400、115200。速率越高,传输越快,但抗干扰能力越差。我个人习惯,工业现场用9600或19200最稳妥。
  • 数据位(Data Bits):每个数据包中实际数据的位数。通常是7位或8位。7位用于ASCII码传输,8位用于二进制数据。现在绝大多数设备都用8位。
  • 校验位(Parity):用于简单的错误检测。有N(无校验)、E(偶校验)、O(奇校验)三种。我建议在工业环境中尽量用偶校验,能发现单比特错误。
  • 停止位(Stop Bits):每个数据包结束的标志。通常是1位或2位。2位停止位用于低速设备或噪声较大的环境。

4.3.2 配置示例

假设我们要连接一个Modbus RTU设备,它的参数是:9600波特率、8数据位、偶校验、1停止位。代码这样写:

ser = serial.Serial(
    port='COM1',
    baudrate=9600,
    bytesize=serial.EIGHTBITS,
    parity=serial.PARITY_EVEN,
    stopbits=serial.STOPBITS_ONE,
    timeout=0.5
)

嗯,这里要注意:一定要先看设备的说明书。我曾经想当然地以为所有设备都是9600 8N1,结果接上一台进口的流量计,死活通信不上。后来翻说明书才发现,它的默认参数是19200 7E1。改了之后,一次成功。

4.4 数据收发与校验:实战中的那些坑

串口通信的核心就是收发数据。但实际项目中,数据不是发出去就完事了,你得确保它是对的。

4.4.1 发送数据

发送数据很简单,用ser.write()就行。但要注意:发送的数据必须是字节类型。如果你要发送字符串,记得先编码:

# 发送字符串
ser.write('AT+OK\r\n'.encode('ascii'))

# 发送十六进制数据
ser.write(bytes([0x01, 0x03, 0x00, 0x00, 0x00, 0x01, 0x84, 0x0A]))

我个人习惯,在发送前先打印一下要发送的数据,方便调试:

print(f"发送: {data.hex().upper()}")

4.4.2 接收数据

接收数据比发送复杂一些。因为串口是异步的,你不知道设备什么时候会回复。常用的方法有:

  • 固定长度读取:用ser.read(n)读取n个字节。适用于协议中规定了响应长度的场景。
  • 按行读取:用ser.readline()读取直到换行符。适用于ASCII协议,比如AT指令。
  • 超时读取:设置timeout,在指定时间内读取所有可用数据。

我常用的方法是“先等一会儿,再读所有数据”:

def read_all(ser, wait_time=0.1):
    """等待一段时间后,读取所有可用数据"""
    time.sleep(wait_time)
    return ser.read(ser.in_waiting)

这个方法虽然简单,但在很多场景下都够用。比如读取温控表的当前温度,发送查询命令后等100毫秒,然后一口气读完。

4.4.3 数据校验:CRC16与LRC

串口通信中,数据在传输过程中可能被干扰。所以,很多协议都带了校验机制。最常用的是CRC16(用于Modbus RTU)和LRC(用于Modbus ASCII)。

我曾经在一个项目中,因为CRC校验写错了,导致上位机一直报“数据错误”。查了两天才发现,是CRC的高低位顺序搞反了。所以,我建议直接用现成的库,比如modbus_tk或者自己写一个经过验证的函数。

这里给一个我常用的CRC16计算函数:

def crc16_modbus(data):
    """计算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 bytes([crc & 0xFF, (crc >> 8) & 0xFF])

使用的时候,把校验码附加到数据末尾即可:

data = bytes([0x01, 0x03, 0x00, 0x00, 0x00, 0x01])
crc = crc16_modbus(data)
frame = data + crc
ser.write(frame)

核心要点:串口通信的本质就是“发出去,收回来,校验对”。参数匹配是前提,数据校验是保障。别嫌麻烦,每一步都做到位,系统才能稳定运行。

我的小技巧:在调试串口通信时,我习惯先用串口调试助手(比如SSCOM、Putty)手动发一条命令,看看设备回复什么。确认无误后,再写代码。这样能快速定位问题是出在硬件还是软件。

警告:千万不要在带电的情况下插拔串口线!尤其是RS232,电压摆幅大,热插拔很容易烧毁串口芯片。我见过不止一个同事因此报销了电脑的串口。正确的做法是:先断电,再接线,最后上电。

好了,这一章的内容就到这里。串口通信看似简单,但里面的门道不少。下一章,咱们会讲Modbus协议,这是工业自动化领域最常用的应用层协议。到时候,你会看到串口通信和Modbus是怎么结合在一起的。

记住,多动手,多实践。光看不练,永远成不了高手。