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 协议¶
协议层次¶
帧格式¶
常用功能码¶
| 功能码 | 说明 | 操作对象 |
|---|---|---|
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 个寄存器
响应帧:
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¶
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 | 从设备忙(正在处理长指令) |