Rust 实现异步ModbusRTU 主机源码分享
- 一、源码分享
-
- 1、分享源码
- 2、效果展示
- 二、ModbusRTU协议详解
-
- 1、ModbusRTU 协议概述
- 2、 ModbusRTU 帧结构
-
- 2.1 、帧格式详解
- 2.2、 帧间隔要求
- 3、常用功能码详解
-
- 3.1 、线圈操作(Coils)
-
- 0x01:读线圈
- 0x05:写单个线圈
- 3.2 寄存器操作(Registers)
-
- 0x03:读保持寄存器
- 0x06:写单个寄存器
- 3.3 批量操作
-
- 0x0F:写多个线圈
- 0x10:写多个寄存器
- 4、CRC-16 校验算法
- 5、通信参数配置
- 6、 错误处理与异常码
- 7、ModbusRTU vs ModbusTCP 对比
- 8、实际应用示例
-
- 8.1 、读取温度传感器数据
- 8.2、 控制继电器输出
- 9、 调试工具推荐
- 10.、注意事项

一、源码分享
1、分享源码
use tokio_serial::SerialStream;
use tokio_modbus::prelude::*;
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let tty_path = "COM1";
let slave = Slave(1);
let builder = tokio_serial::new(tty_path, 19200);
let port = SerialStream::open(&builder).unwrap();
let mut ctx = rtu::attach_slave(port, slave);
loop {
let function_code = 0x03; // 读取保持寄存器的功能码
match ctx.read_holding_registers(0, 10).await {
Ok(values) => {
println!("功能码: 0x{:02X}, 寄存器值: {:?}", function_code, values);
// 在这里处理读取到的寄存器值
}
Err(err) => {
println!("功能码: 0x{:02X}, 读取寄存器失败: {:?}", function_code, err);
// 在这里处理错误
}
}
// 添加适当的延迟,避免过于频繁的请求
tokio::time::sleep(std::time::Duration::from_millis(300)).await;
}
ctx.disconnect().await?;
Ok(())
}
toml
[package]
name = "ttttest"
version = "0.1.0"
edition = "2024"
[dependencies]
futures = "0.3.31"
tokio = { version = "1.47.1", features = ["full"] }
anyhow = "1.0"
tokio–modbus = { version = "0.17.0", features = ["tcp", "server", "rtu","tcp-server"] }
2、效果展示

二、ModbusRTU协议详解
1、ModbusRTU 协议概述
ModbusRTU(Remote Terminal Unit)是 Modbus 协议的一种串行通信实现方式,采用二进制数据格式,通过 RS-232、RS-422 或 RS-485 等物理层进行传输。相比 ModbusTCP,RTU 版本更适用于工业现场环境,具有传输效率高、数据包紧凑的特点。
主要特性:
- 传输方式:主从式(Master-Slave)通信
- 数据格式:二进制(十六进制)
- 校验方式:CRC-16 循环冗余校验
- 帧间隔:至少 3.5 个字符时间的静默间隔作为帧分隔
- 地址范围:从站地址 1-247(0 为广播地址)
2、 ModbusRTU 帧结构
一个完整的 ModbusRTU 帧由以下部分组成:
[从站地址] [功能码] [数据] [CRC校验]
2.1 、帧格式详解
| 从站地址 | 1字节 | 目标设备地址(1-247) |
| 功能码 | 1字节 | 指定要执行的操作类型 |
| 数据 | N字节 | 具体操作参数和数据 |
| CRC校验 | 2字节 | 循环冗余校验,低字节在前 |
2.2、 帧间隔要求
- 帧间间隔:至少 3.5 个字符时间的静默
- 字符间间隔:最大 1.5 个字符时间
- 如果间隔超过 3.5 个字符时间,接收方认为新帧开始
3、常用功能码详解
3.1 、线圈操作(Coils)
0x01:读线圈
请求帧:
[地址][01][起始地址高][起始地址低][线圈数量高][线圈数量低][CRC低][CRC高]
响应帧:
[地址][01][字节数][线圈状态字节1][线圈状态字节2]…[CRC低][CRC高]
0x05:写单个线圈
请求帧:
[地址][05][输出地址高][输出地址低][输出值高][输出值低][CRC低][CRC高]
- 输出值:FF00 表示 ON,0000 表示 OFF
3.2 寄存器操作(Registers)
0x03:读保持寄存器
请求帧:
[地址][03][起始地址高][起始地址低][寄存器数量高][寄存器数量低][CRC低][CRC高]
响应帧:
[地址][03][字节数][寄存器值高][寄存器值低]…[CRC低][CRC高]
0x06:写单个寄存器
请求帧:
[地址][06][寄存器地址高][寄存器地址低][寄存器值高][寄存器值低][CRC低][CRC高]
3.3 批量操作
0x0F:写多个线圈
请求帧:
[地址][0F][起始地址高][起始地址低][线圈数量高][线圈数量低][字节数][线圈状态字节1][线圈状态字节2]…[CRC低][CRC高]
0x10:写多个寄存器
请求帧:
[地址][10][起始地址高][起始地址低][寄存器数量高][寄存器数量低][字节数][寄存器值高][寄存器值低]…[CRC低][CRC高]
4、CRC-16 校验算法
ModbusRTU 使用 CRC-16(多项式:0x8005)进行数据校验,计算步骤如下:
// Rust 实现的 CRC-16 Modbus 校验函数
fn crc16_modbus(data: &[u8]) -> u16 {
let mut crc: u16 = 0xFFFF;
for &byte in data {
crc ^= byte as u16;
for _ in 0..8 {
if (crc & 0x0001) != 0 {
crc >>= 1;
crc ^= 0xA001;
} else {
crc >>= 1;
}
}
}
crc
}
// 使用示例
let frame = vec![0x01, 0x03, 0x00, 0x00, 0x00, 0x02];
let crc = crc16_modbus(&frame);
println!("CRC: {:04X}", crc); // 输出:C40B
5、通信参数配置
ModbusRTU 需要配置以下通信参数:
| 波特率 | 9600, 19200, 38400, 115200 | 通信速率 |
| 数据位 | 8 bits | 数据位数 |
| 停止位 | 1 bit, 2 bits | 停止位数 |
| 校验位 | None, Even, Odd | 奇偶校验 |
| 流控制 | None | 通常不使用 |
典型配置:
- 波特率:9600 bps
- 数据位:8 bits
- 停止位:1 bit
- 校验位:None(无校验)
6、 错误处理与异常码
当从站检测到错误时,会返回异常响应帧:
[地址][功能码 + 0x80][异常码][CRC低][CRC高]
常见异常码:
- 0x01:非法功能码(不支持的功能)
- 0x02:非法数据地址(地址不可用)
- 0x03:非法数据值(数据值超出范围)
- 0x04:从站设备故障(执行失败)
7、ModbusRTU vs ModbusTCP 对比
| 物理层 | RS-232/RS-485 | Ethernet |
| 数据格式 | 二进制 | TCP/IP 报文 |
| 寻址方式 | 从站地址 | IP地址 + 单元标识符 |
| 校验方式 | CRC-16 | TCP 校验和 |
| 传输效率 | 高(无协议头开销) | 较低(有TCP/IP头) |
| 传输距离 | 较短(<1200m) | 远(局域网/互联网) |
| 适用场景 | 工业现场、设备层 | 监控层、系统集成 |
8、实际应用示例
8.1 、读取温度传感器数据
假设温度传感器地址为 1,温度值存储在保持寄存器 30001(对应 Modbus 地址 0x00):
请求帧(主机→从站):
01 03 00 00 00 01 84 0A
- 01:从站地址
- 03:读保持寄存器功能码
- 00 00:起始地址 0x0000
- 00 01:读取 1 个寄存器
- 84 0A:CRC 校验
响应帧(从站→主机):
01 03 02 00 64 B9 9C
- 01:从站地址
- 03:功能码
- 02:字节数(2字节)
- 00 64:温度值 100(0.1℃ 分辨率 = 10.0℃)
- B9 9C:CRC 校验
8.2、 控制继电器输出
控制地址为 2 的设备的线圈 00001(对应 Modbus 地址 0x00):
请求帧(写单个线圈 ON):
02 05 00 00 FF 00 8C 3A
- 02:从站地址
- 05:写单个线圈功能码
- 00 00:线圈地址 0x0000
- FF 00:输出值 ON
- 8C 3A:CRC 校验
9、 调试工具推荐
10.、注意事项





