欢迎光临
我们一直在努力

Rust 实现异步ModbusTCP从站源码分享

Rust 实现异步ModbusTCP从站源码分享

  • 一、源码分享
    • 1、分享源码
    • 2、效果展示
  • 二、ModbusTCP协议详解
    • 1、Modbus 协议概述
    • 2、 Modbus TCP 与 Modbus RTU/ASCII 的区别
    • 3、 Modbus TCP 协议帧结构
      • 3.1、 MBAP 头(7 字节)
      • 3.2、 PDU(协议数据单元)
    • 4、 常用功能码详解
      • 4.1、 读线圈(0x01)
      • 4.2、 读离散输入(0x02)
      • 4.3 、读保持寄存器(0x03)
      • 4.4、 读输入寄存器(0x04)
      • 4.5 、写单个线圈(0x05)
      • 4.6 、写单个寄存器(0x06)
      • 4.7、 写多个线圈(0x0F)
      • 4.8 、写多个寄存器(0x10)
    • 5、 Modbus 地址映射
    • 6、错误响应格式
    • 7、 通信流程示例
      • 7.1 、TCP 连接建立
      • 7.2、 读保持寄存器请求(事务 ID=1,从站 ID=1)
      • 7.3 、正常响应
      • 7.4、 错误响应示例
    • 8、 实际应用注意事项
      • 8.1 、超时处理
      • 8.2 、数据对齐
      • 8.3 、性能优化
      • 8.4、 安全性考虑
    • 9、 调试工具推荐
    • 10、总结

在这里插入图片描述

一、源码分享

1、分享源码

use std::{
collections::HashMap,
future,
net::SocketAddr,
sync::{Arc, Mutex},
time::Duration,
};

use tokio::net::TcpListener;

use tokio_modbus::{
prelude::*,
server::tcp::{Server, accept_tcp_connection},
};

// ——————————
// 全局共享的数据
// ——————————
#[derive(Default, Clone)]
pub struct ModbusData {
pub coils: Arc<Mutex<HashMap<u16, bool>>>,
pub input_registers: Arc<Mutex<HashMap<u16, u16>>>,
pub holding_registers: Arc<Mutex<HashMap<u16, u16>>>,
}

impl ModbusData {
pub fn new() -> Self {
let mut coils = HashMap::new();
for i in 0..100 {
coils.insert(i, i % 2 == 0);
}

let mut input_registers = HashMap::new();
for i in 0..100 {
input_registers.insert(i, i);
}

let mut holding_registers = HashMap::new();
for i in 0..100 {
holding_registers.insert(i, i);
}

Self {
coils: Arc::new(Mutex::new(coils)),
input_registers: Arc::new(Mutex::new(input_registers)),
holding_registers: Arc::new(Mutex::new(holding_registers)),
}
}
}

// ——————————
// Modbus 服务
// ——————————
struct ExampleService {
data: ModbusData,
}

impl tokio_modbus::server::Service for ExampleService {
type Request = Request<'static>;
type Response = Response;
type Exception = ExceptionCode;
type Future = future::Ready<Result<Self::Response, Self::Exception>>;

fn call(&self, req: Self::Request) -> Self::Future {
let res = match req {
Request::ReadCoils(addr, cnt) =>
register_read_coil(&self.data.coils.lock().unwrap(), addr, cnt)
.map(Response::ReadCoils),

Request::WriteSingleCoil(addr, value) =>
register_write_single_coil(&mut self.data.coils.lock().unwrap(), addr, value)
.map(|_| Response::WriteSingleCoil(addr, value)),

Request::WriteMultipleCoils(addr, values) =>
register_write_multiple_coils(&mut self.data.coils.lock().unwrap(), addr, &values)
.map(|_| Response::WriteMultipleCoils(addr, values.len() as u16)),

Request::ReadDiscreteInputs(addr, cnt) =>
register_read_coil(&self.data.coils.lock().unwrap(), addr, cnt)
.map(Response::ReadDiscreteInputs),

Request::ReadInputRegisters(addr, cnt) =>
register_read_input(&self.data.input_registers.lock().unwrap(), addr, cnt)
.map(Response::ReadInputRegisters),

Request::ReadHoldingRegisters(addr, cnt) =>
register_read_holding(&mut self.data.holding_registers.lock().unwrap(), addr, cnt)
.map(Response::ReadHoldingRegisters),

Request::WriteMultipleRegisters(addr, values) =>
register_write_holding(&mut self.data.holding_registers.lock().unwrap(), addr, &values)
.map(|_| Response::WriteMultipleRegisters(addr, values.len() as u16)),

Request::WriteSingleRegister(addr, value) =>
register_write_holding(&mut self.data.holding_registers.lock().unwrap(), addr, std::slice::from_ref(&value))
.map(|_| Response::WriteSingleRegister(addr, value)),

_ => {
println!("SERVER: 不支持的功能码: {req:?}");
Err(ExceptionCode::IllegalFunction)
}
};
future::ready(res)
}
}

impl ExampleService {
fn new(data: ModbusData) -> Self {
Self { data }
}
}

// ——————————
// 工具函数
// ——————————
fn register_read_coil(
registers: &HashMap<u16, bool>,
addr: u16,
cnt: u16,
) -> Result<Vec<bool>, ExceptionCode> {
let mut res = vec![false; cnt as usize];
for i in 0..cnt {
let a = addr + i;
match registers.get(&a) {
Some(&v) => res[i as usize] = v,
None => return Err(ExceptionCode::IllegalDataAddress),
}
}
Ok(res)
}

fn register_write_single_coil(
registers: &mut HashMap<u16, bool>,
addr: u16,
value: bool,
) -> Result<(), ExceptionCode> {
if registers.contains_key(&addr) {
registers.insert(addr, value);
Ok(())
} else {
Err(ExceptionCode::IllegalDataAddress)
}
}

fn register_write_multiple_coils(
registers: &mut HashMap<u16, bool>,
addr: u16,
values: &[bool],
) -> Result<(), ExceptionCode> {
for (i, &val) in values.iter().enumerate() {
let a = addr + i as u16;
if !registers.contains_key(&a) {
return Err(ExceptionCode::IllegalDataAddress);
}
registers.insert(a, val);
}
Ok(())
}

fn register_read_input(
registers: &HashMap<u16, u16>,
addr: u16,
cnt: u16,
) -> Result<Vec<u16>, ExceptionCode> {
let mut res = vec![0; cnt as usize];
for i in 0..cnt {
let a = addr + i;
match registers.get(&a) {
Some(&v) => res[i as usize] = v,
None => return Err(ExceptionCode::IllegalDataAddress),
}
}
Ok(res)
}

fn register_read_holding(
registers: &mut HashMap<u16, u16>,
addr: u16,
cnt: u16,
) -> Result<Vec<u16>, ExceptionCode> {
let mut res = vec![0; cnt as usize];
for i in 0..cnt {
let a = addr + i;
if let Some(v) = registers.get_mut(&a) {
res[i as usize] = *v;
} else {
return Err(ExceptionCode::IllegalDataAddress);
}
}
Ok(res)
}

fn register_write_holding(
registers: &mut HashMap<u16, u16>,
addr: u16,
values: &[u16],
) -> Result<(), ExceptionCode> {
for (i, &val) in values.iter().enumerate() {
let a = addr + i as u16;
if !registers.contains_key(&a) {
return Err(ExceptionCode::IllegalDataAddress);
}
registers.insert(a, val);
}
Ok(())
}

// ——————————
// 服务器
// ——————————
async fn server_context(
socket_addr: SocketAddr,
data: ModbusData,
) -> anyhow::Result<()> {

let listener = TcpListener::bind(socket_addr).await?;
let server = Server::new(listener);

let on_connected = |stream, socket_addr| {
let data = data.clone();
async move {
let new_service = move |_| Ok(Some(ExampleService::new(data.clone())));
accept_tcp_connection(stream, socket_addr, new_service)
}
};

let on_process_error = |err| eprintln!("服务器错误: {err}");
server.serve(&on_connected, on_process_error).await?;
Ok(())
}

// ——————————
// 后台自动修改任务
// ——————————
async fn background_update_task(data: ModbusData) -> anyhow::Result<()> {
println!("✅ 后台更新线程启动:每1秒修改线圈0 和 保持寄存器0\\n");

loop {
tokio::time::sleep(Duration::from_millis(200)).await;

if let Ok(mut coils) = data.coils.lock() {
let v = coils.entry(0).or_insert(false);
*v = !*v;
println!("【后台】线圈0 = {}", *v);
}

if let Ok(mut reg) = data.holding_registers.lock() {
let val = reg.entry(0).or_insert(0);
*val += 1;
println!("【后台】保持寄存器0 = {}\\n", *val);
}
}
}

// ——————————
// 主函数
// ——————————
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let socket_addr: SocketAddr = "127.0.0.1:502".parse().unwrap();
let data = ModbusData::new();

println!("==================================================");
println!("✅ Modbus TCP 从站启动: {}", socket_addr);
println!("✅ 线圈 (0x01/0x05/0x0F) :读写");
println!("✅ 保持寄存器(0x03/0x06/0x10):读一次自动+1");
println!("✅ 输入寄存器(0x04):只读");
println!("==================================================");

// 同时运行服务器和后台任务
let srv = server_context(socket_addr, data.clone());
let update = background_update_task(data);

tokio::select! {
res = srv => res?,
res = update => res?,
}

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"
tokiomodbus = { version = "0.17.0", features = ["tcp", "server", "rtu","tcp-server"] }

2、效果展示

在这里插入图片描述 在这里插入图片描述

二、ModbusTCP协议详解

1、Modbus 协议概述

Modbus 是一种应用层报文传输协议,最初由 Modicon 公司(现为施耐德电气)于 1979 年发布,用于工业自动化系统中的控制器通信。它采用主从(Master-Slave)架构,支持多种物理层(如 RS-232、RS-485、TCP/IP)。

Modbus 协议特点:

  • 简单易用:协议格式简单,易于实现和调试
  • 开放标准:公开的协议规范,无需授权费用
  • 广泛应用:工业自动化、楼宇控制、能源管理等领域
  • 多传输方式:支持串行(RTU/ASCII)和以太网(TCP)传输

2、 Modbus TCP 与 Modbus RTU/ASCII 的区别

特性Modbus RTU/ASCIIModbus TCP
物理层 RS-232/RS-485 串行通信 以太网(TCP/IP)
数据帧 二进制(RTU)或 ASCII 字符 TCP 数据包
地址范围 1-247(从站地址) IP 地址 + 端口(默认 502)
传输速度 较低(最高 115200 bps) 高(百兆/千兆以太网)
最大距离 有限(最长 1200 米) 理论上无限制(通过路由器)
错误检测 CRC 校验(RTU)或 LRC(ASCII) TCP 自带校验,MBAP 头包含长度校验

3、 Modbus TCP 协议帧结构

Modbus TCP 在标准的 Modbus 协议上增加了 MBAP 头(Modbus Application Protocol Header),共 7 个字节。

3.1、 MBAP 头(7 字节)

0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 事务标识符 (2字节) | 协议标识符 (2字节) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 长度 (2字节) | 单元标识符 (1字节) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

各字段说明:

  • 事务标识符(Transaction Identifier):2 字节,由客户端生成,用于请求-响应匹配
  • 协议标识符(Protocol Identifier):2 字节,Modbus TCP 固定为 0x0000
  • 长度(Length):2 字节,表示后续数据的字节数(包括单元标识符和 PDU)
  • 单元标识符(Unit Identifier):1 字节,相当于串行链路的从站地址
  • 3.2、 PDU(协议数据单元)

    PDU 由 功能码 和 数据 两部分组成:

    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | 功能码 (1字节) | 数据 (N字节) |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

    4、 常用功能码详解

    4.1、 读线圈(0x01)

    • 作用:读取线圈(可读写位)状态
    • 请求格式:功能码(0x01) + 起始地址(2字节) + 线圈数量(2字节)
    • 响应格式:功能码(0x01) + 字节数(1字节) + 线圈状态(N字节)
    • 示例:读取地址 0x0000 开始的 10 个线圈请求:01 00 00 00 0A
      响应:01 02 CD 6B

    4.2、 读离散输入(0x02)

    • 作用:读取离散输入(只读位)状态
    • 格式:与读线圈类似,但功能码为 0x02

    4.3 、读保持寄存器(0x03)

    • 作用:读取保持寄存器(可读写16位)的值
    • 请求格式:功能码(0x03) + 起始地址(2字节) + 寄存器数量(2字节)
    • 响应格式:功能码(0x03) + 字节数(1字节) + 寄存器值(N×2字节)
    • 示例:读取地址 0x0000 开始的 2 个保持寄存器请求:03 00 00 00 02
      响应:03 04 00 0A 01 2C

    4.4、 读输入寄存器(0x04)

    • 作用:读取输入寄存器(只读16位)的值
    • 格式:与读保持寄存器类似,但功能码为 0x04

    4.5 、写单个线圈(0x05)

    • 作用:写入单个线圈状态
    • 请求格式:功能码(0x05) + 线圈地址(2字节) + 输出值(2字节)
    • 响应格式:回显请求数据
    • 注意:输出值固定为 0xFF00(ON)或 0x0000(OFF)

    4.6 、写单个寄存器(0x06)

    • 作用:写入单个保持寄存器的值
    • 请求格式:功能码(0x06) + 寄存器地址(2字节) + 寄存器值(2字节)
    • 响应格式:回显请求数据

    4.7、 写多个线圈(0x0F)

    • 作用:写入多个线圈状态
    • 请求格式:功能码(0x0F) + 起始地址(2字节) + 线圈数量(2字节) + 字节数(1字节) + 线圈值(N字节)
    • 响应格式:功能码(0x0F) + 起始地址(2字节) + 线圈数量(2字节)

    4.8 、写多个寄存器(0x10)

    • 作用:写入多个保持寄存器的值
    • 请求格式:功能码(0x10) + 起始地址(2字节) + 寄存器数量(2字节) + 字节数(1字节) + 寄存器值(N×2字节)
    • 响应格式:功能码(0x10) + 起始地址(2字节) + 寄存器数量(2字节)

    5、 Modbus 地址映射

    Modbus 使用不同的地址范围区分数据类型:

    数据类型地址范围功能码说明
    线圈(Coils) 00001-09999 0x01, 0x05, 0x0F 可读写位(布尔值)
    离散输入(Discrete Inputs) 10001-19999 0x02 只读位(布尔值)
    输入寄存器(Input Registers) 30001-39999 0x04 只读16位寄存器
    保持寄存器(Holding Registers) 40001-49999 0x03, 0x06, 0x10 可读写16位寄存器

    注意:协议中的地址从 0 开始,但传统上使用 1-based 的地址表示法(如 40001 对应协议地址 0)。

    6、错误响应格式

    当从站无法处理请求时,会返回错误响应:

    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | 错误功能码 (1字节) | 异常码 (1字节) |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

    • 错误功能码 = 请求功能码 + 0x80
    • 异常码:表示错误原因
      • 0x01:非法功能码
      • 0x02:非法数据地址
      • 0x03:非法数据值
      • 0x04:从站设备故障

    7、 通信流程示例

    7.1 、TCP 连接建立

    客户端 → 服务器:TCP 三次握手(端口 502)

    7.2、 读保持寄存器请求(事务 ID=1,从站 ID=1)

    请求帧(16进制):
    00 01 00 00 00 06 01 03 00 00 00 0A

    分解:
    MBAP头:00 01(事务ID) 00 00(协议ID) 00 06(长度) 01(单元ID)
    PDU:03(功能码) 00 00(起始地址) 00 0A(寄存器数量)

    7.3 、正常响应

    响应帧(16进制):
    00 01 00 00 00 17 01 03 14 00 00 00 01 00 02 00 03 00 04 00 05 00 06 00 07 00 08 00 09

    分解:
    MBAP头:00 01(事务ID) 00 00(协议ID) 00 17(长度) 01(单元ID)
    PDU:03(功能码) 14(字节数=20) + 10个寄存器值(每个2字节)

    7.4、 错误响应示例

    请求:00 02 00 00 00 06 01 03 00 64 00 01
    响应:00 02 00 00 00 03 01 83 02

    解释:
    83 = 03 + 80(错误功能码)
    02 = 非法数据地址

    8、 实际应用注意事项

    8.1 、超时处理

    • 连接超时:TCP 连接建立超时(通常 1-3 秒)
    • 响应超时:从站响应超时(通常 1-5 秒)
    • 重试机制:失败后自动重试(2-3 次)

    8.2 、数据对齐

    • 寄存器数据为 大端序(Big-Endian)
    • 多寄存器数据需要按顺序拼接
    • 浮点数通常占用 2 个寄存器(4 字节)

    8.3 、性能优化

    • 批量读取:一次读取多个寄存器,减少通信次数
    • 连接复用:保持 TCP 连接,避免频繁建立/断开
    • 异步处理:使用异步 I/O 提高并发性能(如示例中的 Rust 实现)

    8.4、 安全性考虑

    • 网络隔离:工业网络与办公网络物理隔离
    • 防火墙:限制对 502 端口的访问
    • 协议过滤:只允许合法的 Modbus 数据包通过

    9、 调试工具推荐

  • Modbus Poll:Windows 平台常用调试工具
  • QModMaster:开源跨平台 Modbus 主站工具
  • modbus-cli:命令行工具,适合自动化测试
  • Wireshark:抓包分析,支持 Modbus TCP 协议解析
  • 10、总结

    Modbus TCP 协议因其简单、可靠、开放的特点,在工业自动化领域得到了广泛应用。理解其协议细节有助于:

    • 正确实现客户端/服务器
    • 高效调试通信问题
    • 优化系统性能
    • 确保数据安全

    在这里插入图片描述

    赞(0)
    未经允许不得转载:171主机测评 » Rust 实现异步ModbusTCP从站源码分享
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址