跳转至

RS485 / Modbus RTU

RS485 是工业界最广泛使用的有线通信标准,Modbus RTU 是运行在 RS485 之上的应用层协议。雷赛、奥托等工业步进驱动器、大多数 PLC IO 模块、传感器变送器(温湿度、压力、流量)都通过 Modbus RTU 通信。


一、RS485 原理

差分信号

RS485 使用差分电压传输,抗干扰能力强,支持长距离通信:

设备A                                      设备B
TX+  ─────────────────── A(正)
TX-  ─────────────────── B(负)
                                        RX+
                                        RX-

发送"1":A > B,差分电压 > +200 mV
发送"0":A < B,差分电压 < -200 mV
参数 RS485 RS232 UART TTL
信号方式 差分 单端 单端
最大距离 1200 m 15 m 5 m
最大速率 10 Mbps(短距离) ~1 Mbps ~几 Mbps
节点数量 32 标准 / 256+ 增强 1对1 1对1
典型应用 工业总线 PC 串口 MCU 调试

半双工方向控制

标准 RS485 是半双工:同一时刻只能发送或接收。必须通过 DE/RE 引脚控制收发方向:

MCU                    MAX485 / SP485
USART_TX ─────────► DI
USART_RX ◄───────── RO
GPIO_DIR ─────────► DE(Driver Enable,高=发送)
                 └─► RE(Receiver Enable,低=接收)
(通常 DE 和 RE 短接,GPIO 控制)

二、Modbus RTU 协议

协议层次

应用层: Modbus 协议(读/写寄存器)
物理层: RS485 差分串行总线

帧格式

│ 从机地址 │ 功能码 │ 数据区 │ CRC校验 │
    1字节      1字节    N字节     2字节(低字节在前)

常用功能码

功能码 说明 操作对象
0x01 读线圈(Read Coils) 单个 bit,可读写
0x02 读离散输入(Read Discrete Inputs) 单个 bit,只读
0x03 读保持寄存器(Read Holding Registers) 16-bit word,可读写
0x04 读输入寄存器(Read Input Registers) 16-bit word,只读
0x05 写单个线圈 单个 bit
0x06 写单个寄存器 16-bit word
0x10(16) 写多个寄存器 多个 16-bit word

读保持寄存器(0x03)示例

请求帧: 读从机地址 1,从寄存器 0x0000 开始,读 2 个寄存器

01  03  00 00  00 02  C4 0B
↑   ↑   ↑↑↑↑  ↑↑↑↑   ↑↑↑↑
地址 FC  起始地址  数量   CRC

响应帧:

01  03  04  01 F4  00 64  BB 04
↑   ↑   ↑    ↑↑↑↑  ↑↑↑↑   ↑↑↑↑
地址 FC 字节数 寄存器1 寄存器2 CRC
       (2×2=4)  (500)   (100)

CRC16 计算

uint16_t modbus_crc16(const uint8_t *buf, uint16_t len) {
    uint16_t crc = 0xFFFF;
    for (uint16_t i = 0; i < len; i++) {
        crc ^= buf[i];
        for (int j = 0; j < 8; j++) {
            if (crc & 0x0001)
                crc = (crc >> 1) ^ 0xA001;
            else
                crc >>= 1;
        }
    }
    return crc;  // 发送时:低字节在前(little-endian)
}

三、STM32 使用

硬件连接

STM32F4                 MAX485
PA9  (USART1_TX) ────► DI
PA10 (USART1_RX) ◄──── RO
PB0  (GPIO_OUT)  ────► DE/RE(高=发送,低=接收)
3.3V             ────► VCC
GND              ────► GND

                 MAX485          终端
                 A ──────────── 驱动器 A
                 B ──────────── 驱动器 B
               (总线两端各 120 Ω)

CubeMX 配置

  • USART1:Asynchronous,9600/19200/115200 bps(与驱动器一致),8N1
  • PB0:GPIO_Output,Speed High(用于方向切换)
  • 建议开启 USART DMA + 空闲中断(IDLE LINE)

Modbus RTU 主机实现(STM32 HAL)

#define RS485_TX()  HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, GPIO_PIN_SET)
#define RS485_RX()  HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, GPIO_PIN_RESET)

uint16_t modbus_crc16(const uint8_t *buf, uint16_t len) {
    uint16_t crc = 0xFFFF;
    for (uint16_t i = 0; i < len; i++) {
        crc ^= buf[i];
        for (int j = 0; j < 8; j++)
            crc = (crc & 1) ? (crc >> 1) ^ 0xA001 : crc >> 1;
    }
    return crc;
}

/* ---- 读保持寄存器 0x03 ---- */
// 返回读取的寄存器数量,-1 表示失败
int modbus_read_holding(uint8_t slave_id, uint16_t start_reg, uint16_t count, uint16_t *out) {
    uint8_t req[8];
    req[0] = slave_id;
    req[1] = 0x03;
    req[2] = start_reg >> 8;
    req[3] = start_reg & 0xFF;
    req[4] = count >> 8;
    req[5] = count & 0xFF;
    uint16_t crc = modbus_crc16(req, 6);
    req[6] = crc & 0xFF;      // CRC 低字节先
    req[7] = (crc >> 8) & 0xFF;

    uint8_t resp[5 + count * 2];

    RS485_TX();
    HAL_UART_Transmit(&huart1, req, 8, 100);
    // 等待发送完成(重要!)
    while (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TC) == RESET);
    RS485_RX();

    HAL_StatusTypeDef status = HAL_UART_Receive(&huart1, resp, 5 + count * 2, 100);
    if (status != HAL_OK) return -1;

    // 校验
    uint16_t resp_crc = (resp[3 + count*2 + 1] << 8) | resp[3 + count*2];
    if (modbus_crc16(resp, 3 + count * 2) != resp_crc) return -1;

    for (int i = 0; i < count; i++)
        out[i] = (resp[3 + i*2] << 8) | resp[4 + i*2];

    return count;
}

/* ---- 写单个寄存器 0x06 ---- */
int modbus_write_single(uint8_t slave_id, uint16_t reg, uint16_t value) {
    uint8_t req[8];
    req[0] = slave_id;
    req[1] = 0x06;
    req[2] = reg >> 8;
    req[3] = reg & 0xFF;
    req[4] = value >> 8;
    req[5] = value & 0xFF;
    uint16_t crc = modbus_crc16(req, 6);
    req[6] = crc & 0xFF;
    req[7] = (crc >> 8) & 0xFF;

    RS485_TX();
    HAL_UART_Transmit(&huart1, req, 8, 100);
    while (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TC) == RESET);
    RS485_RX();

    uint8_t resp[8];
    return HAL_UART_Receive(&huart1, resp, 8, 100) == HAL_OK ? 0 : -1;
}

/* ---- 写多个寄存器 0x10 ---- */
int modbus_write_multiple(uint8_t slave_id, uint16_t start_reg, uint16_t count, uint16_t *data) {
    uint16_t frame_len = 9 + count * 2;
    uint8_t req[frame_len];
    req[0] = slave_id;
    req[1] = 0x10;
    req[2] = start_reg >> 8;
    req[3] = start_reg & 0xFF;
    req[4] = count >> 8;
    req[5] = count & 0xFF;
    req[6] = count * 2;  // 字节数
    for (int i = 0; i < count; i++) {
        req[7 + i*2] = data[i] >> 8;
        req[8 + i*2] = data[i] & 0xFF;
    }
    uint16_t crc = modbus_crc16(req, 7 + count * 2);
    req[7 + count*2] = crc & 0xFF;
    req[8 + count*2] = (crc >> 8) & 0xFF;

    RS485_TX();
    HAL_UART_Transmit(&huart1, req, frame_len, 200);
    while (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TC) == RESET);
    RS485_RX();

    uint8_t resp[8];
    return HAL_UART_Receive(&huart1, resp, 8, 100) == HAL_OK ? 0 : -1;
}

实战:雷赛 DM332 步进驱动器

雷赛 DM332T-485 通过 Modbus RTU 控制(9600 bps,8N1):

#define DM332_SLAVE_ID  1

// 常用寄存器(查手册确认)
#define REG_CTRL_WORD    0x0000  // 控制字:使能、运动方向
#define REG_SPEED        0x0001  // 速度 RPM
#define REG_POSITION_H   0x0002  // 位置高16位(相对脉冲数)
#define REG_POSITION_L   0x0003  // 位置低16位
#define REG_STATUS       0x0100  // 状态字(只读)

void dm332_enable(bool en) {
    modbus_write_single(DM332_SLAVE_ID, REG_CTRL_WORD, en ? 0x0001 : 0x0000);
}

void dm332_set_speed(uint16_t rpm) {
    modbus_write_single(DM332_SLAVE_ID, REG_SPEED, rpm);
}

void dm332_move_relative(int32_t pulses) {
    uint16_t data[2] = {(pulses >> 16) & 0xFFFF, pulses & 0xFFFF};
    modbus_write_multiple(DM332_SLAVE_ID, REG_POSITION_H, 2, data);
}

uint16_t dm332_get_status(void) {
    uint16_t status;
    modbus_read_holding(DM332_SLAVE_ID, REG_STATUS, 1, &status);
    return status;
}

四、Linux 使用

硬件

  • USB 转 RS485:CH340/FT232 + RS485 收发器,插入后出现 /dev/ttyUSB0
  • RS485 HAT(树莓派扩展板):接 GPIO UART,通常 /dev/ttyAMA0/dev/ttyS0
# 确认设备
ls /dev/ttyUSB*

# 给用户串口权限
sudo usermod -aG dialout $USER

# 命令行测试(发送 16 进制数据)
printf '\x01\x03\x00\x00\x00\x01\x84\x0A' > /dev/ttyUSB0

Python pymodbus

pip install pymodbus
from pymodbus.client import ModbusSerialClient

# 创建客户端(Modbus RTU over RS485)
client = ModbusSerialClient(
    port='/dev/ttyUSB0',
    baudrate=9600,
    bytesize=8,
    parity='N',
    stopbits=1,
    timeout=1
)
client.connect()

# 读保持寄存器(功能码 0x03)
result = client.read_holding_registers(
    address=0x0000,   # 起始寄存器地址
    count=4,          # 读取数量
    slave=1           # 从机地址
)
if not result.isError():
    print(f"寄存器值: {result.registers}")

# 写单个寄存器(功能码 0x06)
client.write_register(address=0x0001, value=100, slave=1)

# 写多个寄存器(功能码 0x10)
client.write_registers(address=0x0002, values=[0x0000, 0x03E8], slave=1)

client.close()

不依赖 pymodbus 的原始实现

import serial
import struct
import time

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

class ModbusRTU:
    def __init__(self, port: str, baudrate: int = 9600, slave_id: int = 1):
        self.ser = serial.Serial(port, baudrate, timeout=0.5)
        self.slave_id = slave_id

    def _send_recv(self, req: bytes, resp_len: int) -> bytes | None:
        self.ser.reset_input_buffer()
        self.ser.write(req)
        resp = self.ser.read(resp_len)
        if len(resp) < resp_len:
            return None
        # 验证 CRC
        expected = crc16(resp[:-2])
        actual = struct.unpack_from('<H', resp, len(resp)-2)[0]
        return resp if expected == actual else None

    def read_holding(self, start: int, count: int) -> list[int] | None:
        req = struct.pack('>BBHH', self.slave_id, 0x03, start, count)
        req += struct.pack('<H', crc16(req))
        resp = self._send_recv(req, 5 + count * 2)
        if resp is None:
            return None
        return list(struct.unpack_from(f'>{count}H', resp, 3))

    def write_single(self, reg: int, value: int) -> bool:
        req = struct.pack('>BBHH', self.slave_id, 0x06, reg, value)
        req += struct.pack('<H', crc16(req))
        resp = self._send_recv(req, 8)
        return resp is not None

    def write_multiple(self, start: int, values: list[int]) -> bool:
        count = len(values)
        req = struct.pack(f'>BBHHB{count}H', 
                          self.slave_id, 0x10, start, count, count*2, *values)
        req += struct.pack('<H', crc16(req))
        resp = self._send_recv(req, 8)
        return resp is not None

    def close(self):
        self.ser.close()


# 使用示例
mb = ModbusRTU('/dev/ttyUSB0', baudrate=9600, slave_id=1)

# 读 4 个寄存器
regs = mb.read_holding(0x0000, 4)
print(f"寄存器: {regs}")

# 设置速度
mb.write_single(0x0001, 200)   # 200 RPM

mb.close()

五、注意事项与调试

硬件接线

  • A/B 接反是最常见的错误:接反后通信完全失败。可以调换 A/B 再试
  • 终端电阻:总线长度 > 1 m 时,两端加 120 Ω;短距离测试可以暂时不加
  • 共地(GND):RS485 差分信号理论上不需要共地,但强烈建议共地,避免共模电压超范围
  • 防雷/隔离:工厂环境中 RS485 收发器建议选带光耦隔离的型号(如 ADUM1201)

软件与协议

  • 方向切换时序:发送完成后必须等待 UART 发送寄存器清空(检查 TC 标志),再切换为接收模式,否则最后几个字节会被截断
  • 帧间隔:Modbus RTU 用 3.5 个字符时间的静默标识帧边界。不能过快连续发送
  • 超时设置:接收超时不能太短(驱动器处理需要时间),通常设 100~200 ms
  • 错误响应:功能码 | 0x80 是异常响应,如 0x83 表示读寄存器异常,后跟异常码

调试工具

  • Modbus Poll(Windows,免费):图形化 Modbus 主机工具,最方便
  • ModRSsim2(Windows):Modbus 从机模拟器,测试主机程序
  • minicom / screen:Linux 命令行查看原始数据
  • 逻辑分析仪:在 A/B 线上测量差分波形,确认电平正常(差分电压应 > 200 mV)
  • 万用表:测量 A 对地和 B 对地电压,正常时约 2.5V(差分 ~0V,空闲)

常见错误码

异常码 含义
0x01 非法功能码(该设备不支持此功能码)
0x02 非法数据地址(寄存器不存在)
0x03 非法数据值
0x04 从设备故障
0x06 从设备忙(正在处理长指令)