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)
小技巧:调试阶段可以把串口波特率降到2400,抗干扰能力更强。等通信稳定了再调回9600。

4.7 本章知识结构

下面这张图把整个流程串起来了,从构建请求帧到异常处理,一目了然:

温控器Modbus RTU读取流程 构建请求帧 串口发送请求 接收响应数据 CRC校验 失败 成功 异常处理/重试 解析温度数据 输出温度值 重试最多3次,间隔200ms

核心要点:

  • CRC校验是Modbus RTU的命根子,必须做
  • 异常响应通过功能码最高位判断
  • 重试机制是工业通信的保命符
  • 温度值单位0.1℃,别忘了换算
警告:千万不要在生产线上直接调试!先用一个温控器在实验台上跑通,确认所有异常情况都处理好了,再接入实际设备。我见过有人直接在产线上改代码,结果把温控器参数写乱了,整条线停了两个小时。