4. 实战:读取工业温控器Modbus RTU数据
好,咱们今天直接上手一个真家伙——工业温控器。这东西在注塑机、烘箱、恒温槽里随处可见,说白了就是一个带PID算法的温度控制器。我当年第一次调这玩意儿的时候,差点被它的CRC校验搞疯掉。嗯,今天咱们就把这块硬骨头啃下来。
4.1 温控器的Modbus寄存器地图
动手之前,先得搞清楚温控器里哪些数据能读。我习惯先翻手册,找到寄存器地址表。下面这张表是我从某款国产温控器手册里摘出来的,很典型:
| 寄存器地址 | 数据类型 | 说明 | 读写属性 |
|---|---|---|---|
| 0x0000 | 16位有符号整数 | 当前温度值(单位0.1℃) | 只读 |
| 0x0001 | 16位有符号整数 | 目标温度设定值 | 读写 |
| 0x0002 | 16位无符号整数 | 报警状态位 | 只读 |
| 0x0003 | 16位无符号整数 | 输出功率百分比 | 只读 |
注意看,温度值单位是0.1℃。也就是说,如果读到数值是256,实际温度是25.6℃。这个细节我在项目里吃过亏——当时直接拿整数去显示,结果现场工程师说温度对不上,排查了半天才发现是单位问题。
4.2 构建Modbus RTU请求帧
Modbus RTU的帧结构其实不复杂。我习惯把它拆成四段:设备地址、功能码、数据区、CRC校验。咱们要读温控器的当前温度,设备地址假设是0x01,功能码用0x03(读保持寄存器)。
请求帧长这样:
设备地址: 0x01
功能码: 0x03
起始地址: 0x00 0x00
寄存器数: 0x00 0x01
CRC校验: 0x84 0x0A
你可能会问,CRC怎么算出来的?别急,我专门写了个Python函数,咱们直接拿来用:
def calc_crc16(data: bytes) -> bytes:
crc = 0xFFFF
for byte in data:
crc ^= byte
for _ in range(8):
if crc & 0x0001:
crc = (crc >> 1) ^ 0xA001
else:
crc >>= 1
return crc.to_bytes(2, byteorder='little')
这个算法是Modbus RTU标准规定的。我刚开始做的时候,网上抄了个CRC代码,结果高低字节顺序搞反了,通信一直报错。后来才明白,Modbus RTU的CRC是低字节在前、高字节在后。嗯,这里要注意。
4.3 用pyserial发送请求并接收响应
有了请求帧,接下来就是通过串口发出去。我用pyserial库,波特率9600,8数据位,无校验,1停止位——这是工业温控器最常用的配置。
import serial
import time
ser = serial.Serial(
port='/dev/ttyAMA0',
baudrate=9600,
bytesize=8,
parity='N',
stopbits=1,
timeout=1
)
# 构建请求帧
request = bytes([0x01, 0x03, 0x00, 0x00, 0x00, 0x01])
crc = calc_crc16(request)
request += crc
# 发送
ser.write(request)
time.sleep(0.05) # 给温控器一点处理时间
# 接收响应
response = ser.read(7) # 正常响应固定7字节
这里有个坑——响应字节数。读一个寄存器,正常响应是7字节:设备地址(1) + 功能码(1) + 数据长度(1) + 数据(2) + CRC(2)。但如果你读多个寄存器,数据长度会变。我建议用ser.in_waiting先查一下缓冲区有多少字节,再决定读多少。
4.4 解析响应数据
收到响应后,第一步是校验CRC。如果CRC不对,说明数据在传输过程中被干扰了,直接丢弃。我习惯写个校验函数:
def verify_crc(response: bytes) -> bool:
if len(response) < 3:
return False
received_crc = response[-2:]
calculated_crc = calc_crc16(response[:-2])
return received_crc == calculated_crc
CRC校验通过后,再解析数据。温度值在第3、4字节(索引从0开始),大端格式:
if verify_crc(response):
temp_raw = (response[3] << 8) | response[4]
temperature = temp_raw * 0.1
print(f"当前温度: {temperature:.1f}℃")
else:
print("CRC校验失败,数据已丢弃")
我在现场遇到过一种情况——温控器返回了异常响应。比如功能码最高位置1,后面跟着异常码。常见异常码有:
- 0x01:非法功能码
- 0x02:非法数据地址
- 0x03:非法数据值
- 0x04:从站设备故障
所以解析时得先判断功能码的最高位:
if response[1] & 0x80:
error_code = response[2]
print(f"温控器返回异常: 0x{error_code:02X}")
else:
# 正常解析
4.5 异常处理与重试机制
工业现场干扰大,通信失败是常态。我一般会加个重试机制,最多重试3次,每次间隔200毫秒:
def read_temperature(max_retries=3):
for attempt in range(max_retries):
try:
ser.write(request + calc_crc16(request))
time.sleep(0.05)
response = ser.read(7)
if len(response) != 7:
print(f"第{attempt+1}次尝试:响应长度异常")
continue
if not verify_crc(response):
print(f"第{attempt+1}次尝试:CRC校验失败")
continue
if response[1] & 0x80:
print(f"第{attempt+1}次尝试:温控器异常")
continue
temp_raw = (response[3] << 8) | response[4]
return temp_raw * 0.1
except serial.SerialException as e:
print(f"串口异常: {e}")
time.sleep(0.2)
return None
你想想看,如果不加重试,现场偶尔一次通信失败,整个系统就卡住了。我曾经在一个项目里没做重试,结果半夜温控器掉线,生产线停了,第二天被领导叫去喝茶……从那以后,重试机制成了我的标配。
4.6 完整代码与测试
把上面的代码拼起来,就是一个完整的温控器读取程序。我习惯在树莓派上先跑个测试,确认通信正常:
import serial
import time
def calc_crc16(data):
crc = 0xFFFF
for byte in data:
crc ^= byte
for _ in range(8):
if crc & 0x0001:
crc = (crc >> 1) ^ 0xA001
else:
crc >>= 1
return crc.to_bytes(2, 'little')
def verify_crc(response):
if len(response) < 3:
return False
return response[-2:] == calc_crc16(response[:-2])
def read_temperature(ser, max_retries=3):
request = bytes([0x01, 0x03, 0x00, 0x00, 0x00, 0x01])
for attempt in range(max_retries):
try:
ser.write(request + calc_crc16(request))
time.sleep(0.05)
response = ser.read(7)
if len(response) != 7:
continue
if not verify_crc(response):
continue
if response[1] & 0x80:
continue
temp_raw = (response[3] << 8) | response[4]
return temp_raw * 0.1
except:
time.sleep(0.2)
return None
ser = serial.Serial('/dev/ttyAMA0', 9600, timeout=1)
while True:
temp = read_temperature(ser)
if temp is not None:
print(f"温度: {temp:.1f}℃")
else:
print("读取失败")
time.sleep(2)
4.7 本章知识结构
下面这张图把整个流程串起来了,从构建请求帧到异常处理,一目了然:
核心要点:
- CRC校验是Modbus RTU的命根子,必须做
- 异常响应通过功能码最高位判断
- 重试机制是工业通信的保命符
- 温度值单位0.1℃,别忘了换算