Rust언어以其内存安全、零成本抽象和优秀的并发性能,正在산업 자동화和嵌入式领域快速崛起。本文详细介绍Rust生态中三大主流Modbus库——tokio-modbus、modbus-rs和voltage_modbus的安装설정、API使用、TCP/RTU通讯实现、데이터유형处理、并发采集、服务端개발以及完整项目实战,帮助Rust개발者快速掌握工业Modbus通讯개발。
一、为什么用Rust개발工业Modbus
1.1 Rust在工业控制中的核心优势
- 内存安全:编译期保证无空指针、无缓冲区溢出、无데이터竞争,工业现场7×24小时运行更稳定
- 零成本抽象:高级语法不产生运行时开销,性能接近C언어
- 异步原生:async/await语法+tokio运行时,天然支持多设备并发采集
- 跨平台编译:支持x86/ARM/MIPS/RISC-V,可运行在산업 제어机、树莓派、嵌入式Linux设备上
- 유형系统:强大的유형系统在编译期捕获错误,减少现场디버깅时间
- 单2진수部署:编译为单个可执行文件,无需运行时환경,适合工业现场部署
- Cargo包管理:依赖管理简单,버전锁定可复现构建
1.2 Rust Modbus生态概览
| 库名 | 最신버전本 | 프로토콜支持 | 异步 | no_std | 服务端 | 特点 |
|---|---|---|---|---|---|---|
| tokio-modbus | 0.17.0 | TCP/RTU | 支持(tokio) | 不支持 | 支持 | 最流行,문서完善,社区活跃 |
| modbus-rs | 0.16.0 | TCP/RTU/ASCII | 支持 | 支持 | 支持 | 低内存占用,可嵌入式,功能门控 |
| voltage_modbus | 0.7.2 | TCP/RTU | 支持 | 不支持 | 不支持 | 高性能,请求流水线,읽기合并 |
| rodbus | 1.6.0 | TCP/RTU/TLS | 支持(tokio) | 不支持 | 支持 | 商业级,TLS支持,panic-free분석 |
二、tokio-modbus库详解(最主流)
2.1 安装与Cargo설정
tokio-modbus是Rust生态中最流行的Modbus库,基于tokio异步运行时,支持TCP和RTU两种传输방식,同时提供异步和同步API。
# 创建项目
cargo new modbus_demo
cd modbus_demo
# 추가依赖(完整功能)
cargo add tokio-modbus
cargo add tokio --features full
# Cargo.toml 最终설정
[dependencies]
tokio-modbus = "0.17"
tokio = { version = "1", features = ["full"] }
# 按需裁剪功能(减小2진수体积)
# 仅TCP异步클라이언트:
# tokio-modbus = { version = "0.17", default-features = false, features = ["tcp"] }
# 仅RTU异步클라이언트:
# tokio-modbus = { version = "0.17", default-features = false, features = ["rtu"] }
# TCP服务端:
# tokio-modbus = { version = "0.17", default-features = false, features = ["tcp-server"] }
# 同步클라이언트:
# tokio-modbus = { version = "0.17", default-features = false, features = ["tcp-sync", "rtu-sync"] }
2.2 功能开关설명
| Feature | 설명 | 默认활성화 |
|---|---|---|
| tcp | 异步TCP클라이언트 | 是 |
| rtu | 异步RTU클라이언트 | 是 |
| tcp-sync | 同步TCP클라이언트 | 否 |
| rtu-sync | 同步RTU클라이언트 | 否 |
| tcp-server | 异步TCP服务端 | 否 |
| rtu-server | 异步RTU服务端 | 否 |
| rtu-over-tcp-server | RTU over TCP服务端 | 否 |
2.3 第一个Modbus TCP程序
use tokio_modbus::prelude::*;
use tokio_modbus::client::tcp;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::오답>> {
// 步骤1:建立TCP연결
// connect() 直接연결到Modbus TCP设备,Unit ID默认为0xFF
let socket_addr = "192.168.1.100:502".parse()?;
let mut ctx = tcp::connect(socket_addr).await?;
println!("已연결到Modbus TCP设备:192.168.1.100:502");
// 步骤2:유지 레지스터 읽기 (FC03)(기능 코드03)
// 从주소0开始,읽기10个레지스터
// 返回值是 Vec<u16>,无需手动분석바이트
let regs = ctx.read_holding_registers(0, 10).await?;
println!("\n=== 유지 레지스터(기능 코드03)===");
for (i, &val) in regs.iter().enumerate() {
println!(" 레지스터{}(4{:04d}):{}", i, i + 1, val);
}
// 步骤3:코일 읽기 (FC01)(기능 코드01)
let coils = ctx.read_coils(0, 8).await?;
println!("\n=== 코일 상태(기능 코드01)===");
for (i, &state) in coils.iter().enumerate() {
println!(" 코일{}:{}", i, if state { "吸合" } else { "断开" });
}
// 步骤4:단일 레지스터 쓰기 (FC06)(기능 코드06)
ctx.write_single_register(100, 1234).await?;
println!("\n=== 写单个레지스터(기능 코드06)===");
println!(" 레지스터100已쓰기:1234");
// 步骤5:단일 코일 쓰기 (FC05)(기능 코드05)
// 注意:tokio-modbus直接接受bool值,不需要手动转换为0xFF00/0x0000
ctx.write_single_coil(0, true).await?;
println!("\n=== 写单个코일(기능 코드05)===");
println!(" 코일0已置为:吸合");
Ok(())
}
2.4 연결到指定슬레이브(通过게이트웨이)
当通过Modbus TCP게이트웨이연결RTU슬레이브设备时,需要使用connect_slave指定슬레이브 주소(Unit ID)。
use tokio_modbus::prelude::*;
use tokio_modbus::client::tcp;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::오답>> {
let socket_addr = "192.168.1.1:502".parse()?;
// 연결到게이트웨이后面的슬레이브 주소1
// Slave::from(1) 创建Unit ID为1的슬레이브
let slave = Slave::from(1);
let mut ctx = tcp::connect_slave(socket_addr, slave).await?;
// 后续操作都会自动带上Unit ID=1
let regs = ctx.read_holding_registers(0, 10).await?;
println!("슬레이브1的레지스터 값:{:?}", regs);
// 전환슬레이브 주소(同一연결可以전환Unit ID)
ctx.set_slave(Slave::from(2));
let regs2 = ctx.read_holding_registers(0, 10).await?;
println!("슬레이브2的레지스터 값:{:?}", regs2);
Ok(())
}
2.5 完整기능 코드예제
use tokio_modbus::prelude::*;
use tokio_modbus::client::tcp;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::오답>> {
let socket_addr = "192.168.1.100:502".parse()?;
let mut ctx = tcp::connect(socket_addr).await?;
// ====== 기능 코드01:读코일 ======
// 返回 Vec<bool>,每个元素对应一个코일 상태
let coils = ctx.read_coils(0, 16).await?;
println!("=== 读코일(01)===");
for (i, &c) in coils.iter().enumerate() {
print!("{}", if c { "1" } else { "0" });
}
println!();
// ====== 기능 코드02:读이산 입력 ======
let inputs = ctx.read_discrete_inputs(0, 8).await?;
println!("=== 读이산 입력(02)===");
println!("입력 상태:{:?}", inputs);
// ====== 기능 코드03:读유지 레지스터 ======
let holding = ctx.read_holding_registers(0, 10).await?;
println!("=== 读유지 레지스터(03)===");
println!("레지스터 값:{:?}", holding);
// ====== 기능 코드04:读입력 레지스터 ======
let input_regs = ctx.read_input_registers(0, 4).await?;
println!("=== 读입력 레지스터(04)===");
println!("模拟量值:{:?}", input_regs);
// ====== 기능 코드05:写单个코일 ======
// 直接传bool,库内部自动转换为0xFF00/0x0000
ctx.write_single_coil(0, true).await?; // 코일0置ON
ctx.write_single_coil(1, false).await?; // 코일1置OFF
println!("=== 写单个코일(05)===");
// ====== 기능 코드06:写单个레지스터 ======
ctx.write_single_register(50, 999).await?;
println!("=== 写单个레지스터(06)===");
// ====== 기능 코드0F:写多个코일 ======
// 传入 &[bool],库内部自动按位打包
let coil_데이터 = vec![true, false, true, false, true, false, true, false];
ctx.write_multiple_coils(0, &coil_데이터).await?;
println!("=== 写多个코일(0F)===");
// ====== 기능 코드10:写多个레지스터 ======
let reg_데이터 = vec![100u16, 200, 300, 400, 500];
ctx.write_multiple_registers(100, ®_데이터).await?;
println!("=== 写多个레지스터(10)===");
// ====== 기능 코드16:掩码写레지스터 ======
// 新值 = (当前值 AND and_mask) OR or_mask
// 例如:将레지스터200的bit3置1,其他位不变
// and_mask = !0x0008 = 0xFFF7(清除bit3)
// or_mask = 0x0008(设置bit3)
ctx.masked_write_register(200, 0xFFF7, 0x0008).await?;
println!("=== 掩码写레지스터(16)===");
// ====== 기능 코드17:读写多个레지스터 ======
// 先写后读!注意顺序与函数名相反
// 쓰기레지스터300-301,同时읽기레지스터0-4
let write_데이터 = vec![10u16, 20];
let read_데이터 = ctx.read_write_multiple_registers(
0, // 读시작주소
5, // 读数量
300, // 写시작주소
&write_데이터 // 写데이터
).await?;
println!("=== 读写多个레지스터(17)===");
println!("읽기到:{:?}", read_데이터);
Ok(())
}
三、데이터유형处理
tokio-modbus的read_holding_registers返回Vec<u16>,但实际工业设备中经常使用32位浮点数、32位整数、64位데이터等。以下提供完整的转换도구。
3.1 32位浮点数转换
/// 32位浮点数바이트序枚举
#[derive(Debug, Clone, 복사)]
pub enum FloatOrder {
Abcd, // 大端序:reg[0]=高16位, reg[1]=低16位(最常见)
Cdab, // 字交换:reg[0]=低16位, reg[1]=高16位
Dcba, // 小端序:完全反转
Badc, // 바이트交换:每个레지스터内部바이트交换
}
/// 从两个u16레지스터분석f32
pub fn parse_f32(regs: &[u16], order: FloatOrder) -> f32 {
let bytes = match order {
FloatOrder::Abcd => [
(regs[0] >> 8) as u8, regs[0] as u8,
(regs[1] >> 8) as u8, regs[1] as u8,
],
FloatOrder::Cdab => [
(regs[1] >> 8) as u8, regs[1] as u8,
(regs[0] >> 8) as u8, regs[0] as u8,
],
FloatOrder::Dcba => [
regs[1] as u8, (regs[1] >> 8) as u8,
regs[0] as u8, (regs[0] >> 8) as u8,
],
FloatOrder::Badc => [
regs[0] as u8, (regs[0] >> 8) as u8,
regs[1] as u8, (regs[1] >> 8) as u8,
],
};
f32::from_be_bytes(bytes)
}
/// 将f32转换为两个u16레지스터(ABCD大端序)
pub fn f32_to_regs(value: f32) -> [u16; 2] {
let bytes = value.to_be_bytes();
[
u16::from_be_bytes([bytes[0], bytes[1]]),
u16::from_be_bytes([bytes[2], bytes[3]]),
]
}
// 使用예제
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::오답>> {
use tokio_modbus::prelude::*;
use tokio_modbus::client::tcp;
let socket_addr = "192.168.1.100:502".parse()?;
let mut ctx = tcp::connect(socket_addr).await?;
// 읽기온도 센서(浮点数占2个레지스터,주소100-101)
let regs = ctx.read_holding_registers(100, 2).await?;
let temp = parse_f32(®s, FloatOrder::Abcd);
println!("当前온도:{:.2}°C", temp);
// 쓰기设定온도
let set_temp = 25.5f32;
let write_regs = f32_to_regs(set_temp);
ctx.write_multiple_registers(200, &write_regs).await?;
println!("设定온도已쓰기:{:.1}°C", set_temp);
Ok(())
}
3.2 32位/64位整数转换
/// 분석32位有符号整数(大端序)
pub fn parse_i32(regs: &[u16]) -> i32 {
let bytes = [
(regs[0] >> 8) as u8, regs[0] as u8,
(regs[1] >> 8) as u8, regs[1] as u8,
];
i32::from_be_bytes(bytes)
}
/// 분석32位无符号整数(大端序)
pub fn parse_u32(regs: &[u16]) -> u32 {
let bytes = [
(regs[0] >> 8) as u8, regs[0] as u8,
(regs[1] >> 8) as u8, regs[1] as u8,
];
u32::from_be_bytes(bytes)
}
/// 분석64位整数(大端序,占4个레지스터)
pub fn parse_i64(regs: &[u16]) -> i64 {
let mut bytes = [0u8; 8];
for i in 0..4 {
bytes[i * 2] = (regs[i] >> 8) as u8;
bytes[i * 2 + 1] = regs[i] as u8;
}
i64::from_be_bytes(bytes)
}
/// 분석64位浮点数(double,占4个레지스터)
pub fn parse_f64(regs: &[u16]) -> f64 {
let mut bytes = [0u8; 8];
for i in 0..4 {
bytes[i * 2] = (regs[i] >> 8) as u8;
bytes[i * 2 + 1] = regs[i] as u8;
}
f64::from_be_bytes(bytes)
}
/// 16位有符号整数(直接转换)
pub fn parse_i16(reg: u16) -> i16 {
reg as i16
}
/// 带缩放的物理量분석
/// 例如:온도레지스터原始值255,分辨率0.1,实际25.5°C
pub fn parse_scaled(reg: u16, scale: f32) -> f32 {
reg as f32 * scale
}
/// 带偏移的物理量분석
/// 例如:压力原始值1234,偏移-1000,实际234
pub fn parse_with_offset(reg: u16, offset: i32) -> i32 {
reg as i32 + offset
}
3.3 바이트序速查表
| 바이트序 | 32位值0x41420000的存储방식 | 常见设备 |
|---|---|---|
| ABCD(大端) | reg[0]=0x4142, reg[1]=0x0000 | 大多数PLC、표준Modbus设备 |
| CDAB(字交换) | reg[0]=0x0000, reg[1]=0x4142 | 部分西门子、部分国产设备 |
| DCBA(小端) | reg[0]=0x0000, reg[1]=0x4241 | 部分x86架构设备 |
| BADC(바이트交换) | reg[0]=0x4241, reg[1]=0x0000 | 较少见 |
四、Modbus RTU시리얼 포트通讯
4.1 RTU클라이언트설정
use tokio_modbus::prelude::*;
use tokio_modbus::client::rtu;
use tokio_serial::SerialStream;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::오답>> {
// 시리얼 포트设备路径
// Linux: /dev/ttyUSB0, /dev/ttyS0
// Windows: COM3
// macOS: /dev/tty.usbserial-xxx
let serial_path = "/dev/ttyUSB0";
// 시리얼 포트 매개변수(必须与슬레이브设备完全一致)
let baud_rate = 9600u32; // 통신 속도:9600, 19200, 38400, 115200
let 데이터_bits = 8; // 데이터 비트:通常为8
let stop_bits = tokio_serial::StopBits::One; // 정지 비트:1或2
let parity = tokio_serial::Parity::None; // 패리티:None, Even, Odd
// 构建시리얼 포트 설정
let builder = tokio_serial::new(serial_path, baud_rate)
.데이터_bits(데이터_bits)
.stop_bits(stop_bits)
.parity(parity);
// 시리얼 포트 열기
let serial = SerialStream::열기(&builder)?;
// 创建RTU클라이언트,指定슬레이브 주소
let slave = Slave::from(1); // 슬레이브 주소1-247
let mut ctx = rtu::attach_slave(serial, slave);
// 유지 레지스터 읽기 (FC03)
let regs = ctx.read_holding_registers(0, 10).await?;
println!("레지스터 값:{:?}", regs);
// 쓰기코일
ctx.write_single_coil(0, true).await?;
println!("코일0已置ON");
Ok(())
}
// Cargo.toml 需要추가:
// [dependencies]
// tokio-modbus = "0.17"
// tokio = { version = "1", features = ["full"] }
// tokio-serial = "5"
4.2 RTU常见문제排查
| 문제 | 可能原因 | 解决方法 |
|---|---|---|
| 시리얼 포트打不开 | 设备名错误/权限不足 | 检查/dev/ttyUSB0是否存在;Linux执行sudo usermod -aG dialout $USER后重新로그인 |
| 无响应 | A/B线接反/슬레이브 주소错误 | 交换A/B线;确认슬레이브 주소DIP 스위치或软件设置 |
| CRC错误 | 통신 속도/패리티不일치 | 确认主슬레이브시리얼 포트 매개변수完全一致 |
| 偶尔丢包 | 干扰/终端电阻 | 总线两端加120Ω终端电阻;使用屏蔽双绞线 |
| 响应超时 | 슬레이브处理慢/超时太短 | 增大超时时间到3-5秒 |
| 데이터错乱 | 바이트序错误 | 尝试ABCD/CDAB/DCBA三种바이트序 |
五、同步모드(阻塞API)
对于简单스크립트或不需要异步的시나리오,tokio-modbus提供了同步API。需要활성화tcp-sync或rtu-sync feature。
// Cargo.toml
// [dependencies]
// tokio-modbus = { version = "0.17", default-features = false, features = ["tcp-sync"] }
use tokio_modbus::prelude::*;
use tokio_modbus::client::sync::tcp;
fn main() -> Result<(), Box<dyn std::error::오답>> {
let socket_addr = "192.168.1.100:502".parse()?;
// 同步연결(阻塞直到연결成功)
let mut ctx = tcp::connect(socket_addr)?;
// 同步읽기(阻塞直到响应)
let regs = ctx.read_holding_registers(0, 10)?;
println!("레지스터 값:{:?}", regs);
// 同步쓰기
ctx.write_single_register(100, 1234)?;
Ok(())
}
六、并发与多设备采集
Rust的async/await和tokio运行时天然支持并发。以下예제同时采集多个Modbus设备的데이터。
use tokio_modbus::prelude::*;
use tokio_modbus::client::tcp;
use std::time::Duration;
/// 设备설정
struct DeviceConfig {
name: String,
주소: String,
slave: Slave,
}
/// 采集结果
struct Device데이터 {
name: String,
registers: Vec<u16>,
error: Option<String>,
}
/// 采集单个设备
async fn read_device(cfg: &DeviceConfig) -> Device데이터 {
let socket_addr = match cfg.주소.parse() {
Ok(addr) => addr,
Err(e) => return Device데이터 {
name: cfg.name.clone(),
registers: vec![],
error: Some(format!("주소분석失败:{}", e)),
},
};
// 每个设备独立연결,避免共享연결的并发冲突
let mut ctx = match tcp::connect_slave(socket_addr, cfg.slave).await {
Ok(ctx) => ctx,
Err(e) => return Device데이터 {
name: cfg.name.clone(),
registers: vec![],
error: Some(format!("연결失败:{}", e)),
},
};
// 设置超时(通过tokio::timeout)
let result = tokio::time::timeout(
Duration::from_secs(3),
ctx.read_holding_registers(0, 10)
).await;
match result {
Ok(Ok(regs)) => Device데이터 {
name: cfg.name.clone(),
registers: regs,
error: None,
},
Ok(Err(e)) => Device데이터 {
name: cfg.name.clone(),
registers: vec![],
error: Some(format!("읽기失败:{}", e)),
},
Err(_) => Device데이터 {
name: cfg.name.clone(),
registers: vec![],
error: Some("읽기超时".to_string()),
},
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::오답>> {
let devices = vec![
DeviceConfig {
name: "PLC-车间A".to_string(),
주소: "192.168.1.100:502".to_string(),
slave: Slave::from(1),
},
DeviceConfig {
name: "PLC-车间B".to_string(),
주소: "192.168.1.101:502".to_string(),
slave: Slave::from(1),
},
DeviceConfig {
name: "电表-总进线".to_string(),
주소: "192.168.1.102:502".to_string(),
slave: Slave::from(2),
},
];
// 并发采集所有设备
let handles: Vec<_> = devices.iter().map(|dev| {
tokio::spawn(read_device(dev))
}).collect();
// 대기所有任务完成
let mut results = vec![];
for handle in handles {
if let Ok(데이터) = handle.await {
results.push(데이터);
}
}
// 출력结果
println!("=== 采集结果 ===");
for 데이터 in &results {
match &데이터.error {
Some(e) => println!("[{}] 错误:{}", 데이터.name, e),
None => println!("[{}] 레지스터:{:?}", 데이터.name, 데이터.registers),
}
}
Ok(())
}
七、Modbus TCP服务端(슬레이브)
tokio-modbus提供了TCP服务端框架,可以快速实现Modbus슬레이브设备。需要활성화tcp-server feature。
// Cargo.toml
// [dependencies]
// tokio-modbus = { version = "0.17", default-features = false, features = ["tcp-server"] }
// tokio = { version = "1", features = ["full"] }
use tokio_modbus::prelude::*;
use tokio_modbus::server::{tcp, Server, Service};
use std::sync::{Arc, Mutex};
/// 슬레이브데이터存储
#[derive(Clone)]
struct Modbus데이터 {
coils: Arc<Mutex<Vec<bool>>>,
discrete_inputs: Arc<Mutex<Vec<bool>>>,
holding_registers: Arc<Mutex<Vec<u16>>>,
input_registers: Arc<Mutex<Vec<u16>>>,
}
impl Modbus데이터 {
fn new() -> Self {
Self {
coils: Arc::new(Mutex::new(vec![false; 1024])),
discrete_inputs: Arc::new(Mutex::new(vec![false; 1024])),
holding_registers: Arc::new(Mutex::new(vec![0u16; 1024])),
input_registers: Arc::new(Mutex::new(vec![0u16; 1024])),
}
}
}
/// 服务实现
struct ModbusService {
데이터: Modbus데이터,
}
impl Service for ModbusService {
type Request = Request;
type Response = Response;
type 오답 = std::io::오답;
fn call(&self, req: Self::Request) -> Result<Self::Response, Self::오답> {
match req {
Request::ReadCoils(addr, cnt) => {
let coils = self.데이터.coils.lock().unwrap();
let end = (addr + cnt) as usize;
if end > coils.len() {
return Err(std::io::오답::new(std::io::오답Kind::Invalid데이터, "주소越界"));
}
Ok(Response::ReadCoils(coils[addr as usize..end].to_vec()))
}
Request::ReadDiscreteInputs(addr, cnt) => {
let inputs = self.데이터.discrete_inputs.lock().unwrap();
let end = (addr + cnt) as usize;
Ok(Response::ReadDiscreteInputs(inputs[addr as usize..end].to_vec()))
}
Request::ReadHolding회원가입s(addr, cnt) => {
let regs = self.데이터.holding_registers.lock().unwrap();
let end = (addr + cnt) as usize;
Ok(Response::ReadHolding회원가입s(regs[addr as usize..end].to_vec()))
}
Request::ReadInput회원가입s(addr, cnt) => {
let regs = self.데이터.input_registers.lock().unwrap();
let end = (addr + cnt) as usize;
Ok(Response::ReadInput회원가입s(regs[addr as usize..end].to_vec()))
}
Request::WriteSingleCoil(addr, coil) => {
let mut coils = self.데이터.coils.lock().unwrap();
coils[addr as usize] = coil;
Ok(Response::WriteSingleCoil(addr, coil))
}
Request::WriteSingle회원가입(addr, value) => {
let mut regs = self.데이터.holding_registers.lock().unwrap();
regs[addr as usize] = value;
Ok(Response::WriteSingle회원가입(addr, value))
}
Request::WriteMultipleCoils(addr, coils_데이터) => {
let mut coils = self.데이터.coils.lock().unwrap();
for (i, &v) in coils_데이터.iter().enumerate() {
coils[addr as usize + i] = v;
}
Ok(Response::WriteMultipleCoils(addr, coils_데이터.len() as u16))
}
Request::WriteMultiple회원가입s(addr, regs_데이터) => {
let mut regs = self.데이터.holding_registers.lock().unwrap();
for (i, &v) in regs_데이터.iter().enumerate() {
regs[addr as usize + i] = v;
}
Ok(Response::WriteMultiple회원가입s(addr, regs_데이터.len() as u16))
}
_ => Err(std::io::오답::new(std::io::오답Kind::Invalid데이터, "不支持的기능 코드")),
}
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::오답>> {
let 데이터 = Modbus데이터::new();
// 设置初始值
{
let mut regs = 데이터.holding_registers.lock().unwrap();
regs[0] = 100;
regs[1] = 200;
regs[2] = 300;
}
{
let mut coils = 데이터.coils.lock().unwrap();
coils[0] = true;
coils[2] = true;
}
let socket_addr = "0.0.0.0:502".parse()?;
let server = tcp::Server::new(socket_addr);
println!("Modbus TCP服务端已启动,监听:0.0.0.0:502");
// 启动服务(阻塞)
server.serve(move || {
let service = ModbusService { 데이터: 데이터.clone() };
move |req| service.call(req)
}).await?;
Ok(())
}
八、错误处理与异常码
8.1 Modbus异常码
| 异常码 | 名称 | 含义 | 常见原因 |
|---|---|---|---|
| 0x01 | Illegal기능 | 非法기능 코드 | 슬레이브不支持该기능 코드 |
| 0x02 | Illegal데이터주소 | 非法데이터주소 | 请求주소超出范围 |
| 0x03 | Illegal데이터Value | 非法데이터值 | 쓰기值不合法 |
| 0x04 | ServerDeviceFailure | 슬레이브设备故障 | 设备内部错误 |
| 0x05 | Acknowledge | 确认 | 已接受但需时间处理 |
| 0x06 | ServerDeviceBusy | 슬레이브忙 | 正在处理其他请求 |
8.2 带重试的健壮읽기
use tokio_modbus::prelude::*;
use std::time::Duration;
/// 带重试和退避的레지스터읽기
async fn read_with_retry(
ctx: &mut Context,
addr: u16,
cnt: u16,
max_retries: u32,
) -> Result<Vec<u16>, String> {
let mut last_error = String::new();
for attempt in 0..max_retries {
match tokio::time::timeout(
Duration::from_secs(3),
ctx.read_holding_registers(addr, cnt)
).await {
Ok(Ok(regs)) => return Ok(regs),
Ok(Err(e)) => {
last_error = format!("프로토콜错误:{}", e);
eprintln!("第{}次읽기失败:{}", attempt + 1, last_error);
}
Err(_) => {
last_error = "읽기超时".to_string();
eprintln!("第{}次읽기超时", attempt + 1);
}
}
// 指数退避:500ms, 1s, 2s, 4s...
if attempt < max_retries - 1 {
let delay = Duration::from_millis(500 * (1 << attempt));
tokio::time::sleep(delay).await;
}
}
Err(format!("重试{}次后仍失败:{}", max_retries, last_error))
}
// 使用예제
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::오답>> {
use tokio_modbus::client::tcp;
let socket_addr = "192.168.1.100:502".parse()?;
let mut ctx = tcp::connect(socket_addr).await?;
match read_with_retry(&mut ctx, 0, 10, 3).await {
Ok(regs) => println!("읽기成功:{:?}", regs),
Err(e) => {
eprintln!("最终失败:{}", e);
// 보내기告警、记录로그、전환备用设备等
}
}
Ok(())
}
九、modbus-rs库详解(嵌入式/no_std)
modbus-rs是一个低内存占用、支持no_std的Modbus프로토콜栈,适合嵌入式MCU和자료受限환경。支持TCP/RTU/ASCII三种프로토콜,提供同步和异步API,通过feature门控控制2진수体积。
# Cargo.toml
[dependencies]
modbus-rs = "0.16"
# 嵌入式使用(no_std)
# modbus-rs = { version = "0.16", default-features = false, features = ["rtu", "embedded"] }
use modbus_rs::prelude::*;
fn main() -> Result<(), Box<dyn std::error::오답>> {
// 创建TCP클라이언트
let mut client = modbus_rs::TcpClient::new("192.168.1.100:502")?;
client.connect()?;
// 读유지 레지스터
let regs = client.read_holding_registers(1, 0, 10)?;
println!("레지스터 값:{:?}", regs);
// 写单个코일
client.write_single_coil(1, 0, CoilState::On)?;
Ok(())
}
十、voltage_modbus库详解(高性能)
voltage_modbus是一个面向高性能시나리오的Rust Modbus库,核心特性包括请求流水线(pipelining)、읽기合并(coalescing)和零分配热路径(zero-alloc),适合高频데이터采集시나리오。
# Cargo.toml
[dependencies]
voltage_modbus = "0.7"
use voltage_modbus::{ModbusTcpClient, ModbusResult};
#[tokio::main]
async fn main() -> ModbusResult<()> {
// 创建TCP클라이언트,超时5秒
let mut client = ModbusTcpClient::from_주소(
"127.0.0.1:502",
std::time::Duration::from_secs(5)
).await?;
// 读유지 레지스터(기능 코드03)
// 注意:voltage_modbus的方法名以read_03/read_04等기능 코드번호命名
let values = client.read_03(1, 0, 10).await?; // unit_id=1, addr=0, count=10
println!("레지스터 값:{:?}", values);
// 读코일(기능 코드01)
let coils = client.read_01(1, 0, 8).await?;
println!("코일 상태:{:?}", coils);
// 写单个레지스터(기능 코드06)
client.write_06(1, 100, 1234).await?;
Ok(())
}
10.1 三大库性能对比
| 特性 | tokio-modbus | modbus-rs | voltage_modbus |
|---|---|---|---|
| 请求流水线 | 不支持(严格串行) | 不支持 | 支持(N个请求约1个RTT) |
| 읽기合并 | 不支持 | 不支持 | 支持(自动合并相邻읽기) |
| 零分配热路径 | 每次请求堆分配 | 部分堆分配 | 栈帧+持久缓冲区 |
| no_std支持 | 不支持 | 支持 | 不支持 |
| 服务端 | 支持 | 支持 | 不支持 |
| 학습曲线 | 低(문서好) | 中 | 中 |
| 适用시나리오 | 通用工业项目 | 嵌入式/자료受限 | 高频采集/性能敏感 |
十一、完整项目实战:환경监测系统
以下是一个完整的工业환경监测系统,使用tokio-modbus并发采集多个온습도센서,支持데이터存储、异常告警和定时采集。
use tokio_modbus::prelude::*;
use tokio_modbus::client::tcp;
use std::time::Duration;
use std::sync::{Arc, Mutex};
/// 센서설정
#[derive(Clone)]
struct SensorConfig {
name: String,
주소: String,
slave: Slave,
temp_addr: u16, // 온도레지스터주소
hum_addr: u16, // 湿度레지스터주소
temp_type: TempType,
}
#[derive(Clone, 복사)]
enum TempType {
Int16, // 16位有符号整数,分辨率0.1°C
Float32, // 32位浮点数
}
/// 센서데이터
#[derive(Clone)]
struct Sensor데이터 {
name: String,
timestamp: chrono::DateTime<chrono::Local>,
temperature: f64,
humidity: f64,
error: Option<String>,
}
/// 분석32位浮点数(ABCD大端序)
fn parse_f32_abcd(regs: &[u16]) -> f32 {
let bytes = [
(regs[0] >> 8) as u8, regs[0] as u8,
(regs[1] >> 8) as u8, regs[1] as u8,
];
f32::from_be_bytes(bytes)
}
/// 采集单个센서
async fn read_sensor(cfg: &SensorConfig) -> Sensor데이터 {
let 데이터 = Sensor데이터 {
name: cfg.name.clone(),
timestamp: chrono::Local::now(),
temperature: 0.0,
humidity: 0.0,
error: None,
};
let socket_addr = match cfg.주소.parse() {
Ok(addr) => addr,
Err(e) => return Sensor데이터 { error: Some(format!("주소错误:{}", e)), ..데이터 },
};
let mut ctx = match tcp::connect_slave(socket_addr, cfg.slave).await {
Ok(ctx) => ctx,
Err(e) => return Sensor데이터 { error: Some(format!("연결失败:{}", e)), ..데이터 },
};
// 읽기온도
let temperature = match cfg.temp_type {
TempType::Float32 => {
match tokio::time::timeout(Duration::from_secs(3),
ctx.read_holding_registers(cfg.temp_addr, 2)).await
{
Ok(Ok(regs)) => parse_f32_abcd(®s) as f64,
Ok(Err(e)) => return Sensor데이터 { error: Some(format!("온도읽기失败:{}", e)), ..데이터 },
Err(_) => return Sensor데이터 { error: Some("온도읽기超时".to_string()), ..데이터 },
}
}
TempType::Int16 => {
match tokio::time::timeout(Duration::from_secs(3),
ctx.read_holding_registers(cfg.temp_addr, 1)).await
{
Ok(Ok(regs)) => regs[0] as i16 as f64 * 0.1,
Ok(Err(e)) => return Sensor데이터 { error: Some(format!("온도읽기失败:{}", e)), ..데이터 },
Err(_) => return Sensor데이터 { error: Some("온도읽기超时".to_string()), ..데이터 },
}
}
};
// 읽기湿度(16位整数,分辨率0.1%)
let humidity = match tokio::time::timeout(Duration::from_secs(3),
ctx.read_holding_registers(cfg.hum_addr, 1)).await
{
Ok(Ok(regs)) => regs[0] as f64 * 0.1,
Ok(Err(e)) => return Sensor데이터 { error: Some(format!("湿度읽기失败:{}", e)), ..데이터 },
Err(_) => return Sensor데이터 { error: Some("湿度읽기超时".to_string()), ..데이터 },
};
Sensor데이터 { temperature, humidity, ..데이터 }
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::오답>> {
let sensors = vec![
SensorConfig {
name: "车间A".to_string(),
주소: "192.168.1.100:502".to_string(),
slave: Slave::from(1),
temp_addr: 0,
hum_addr: 2,
temp_type: TempType::Float32,
},
SensorConfig {
name: "车间B".to_string(),
주소: "192.168.1.101:502".to_string(),
slave: Slave::from(1),
temp_addr: 0,
hum_addr: 2,
temp_type: TempType::Float32,
},
SensorConfig {
name: "仓库".to_string(),
주소: "192.168.1.102:502".to_string(),
slave: Slave::from(1),
temp_addr: 0,
hum_addr: 1,
temp_type: TempType::Int16,
},
];
// 历史데이터存储
let history = Arc::new(Mutex::new(Vec::<Sensor데이터>::new()));
println!("환경监测系统启动,每10秒采集一次...");
println!("按Ctrl+C退出\n");
let mut interval = tokio::time::interval(Duration::from_secs(10));
loop {
interval.tick().await;
// 并发采集所有센서
let handles: Vec<_> = sensors.iter().map(|s| {
tokio::spawn(read_sensor(s))
}).collect();
let mut results = vec![];
for handle in handles {
if let Ok(데이터) = handle.await {
results.push(데이터);
}
}
// 출력结果
println!("=== {} ===", chrono::Local::now().format("%Y-%m-%d %H:%M:%S"));
for 데이터 in &results {
match &데이터.error {
Some(e) => println!("[{}] 错误:{}", 데이터.name, e),
None => {
println!("[{}] 온도:{:.1}°C,湿度:{:.1}%", 데이터.name, 데이터.temperature, 데이터.humidity);
// 异常告警
if 데이터.temperature > 35.0 {
println!(" ⚠️ 온도过高告警:{:.1}°C", 데이터.temperature);
}
if 데이터.humidity > 80.0 {
println!(" ⚠️ 湿度过高告警:{:.1}%", 데이터.humidity);
}
}
}
}
println!();
// 저장历史데이터
history.lock().unwrap().extend(results);
}
}
// Cargo.toml 完整依赖:
// [dependencies]
// tokio-modbus = "0.17"
// tokio = { version = "1", features = ["full"] }
// chrono = "0.4"
十二、最佳实践与性能优化
12.1 最佳实践
- 批量읽기:尽量一次읽기连续的多个레지스터,而不是分多次읽기。一次读10个레지스터比读10次1个레지스터快10倍
- 연결复用:高频请求时保持长연결,避免每次TCP握手开销(约1-3ms)
- 合理超时:局域网设置1-3秒超时,RTU시리얼 포트设置3-5秒。使用tokio::time::timeout包裹请求
- 并发隔离:多设备并发时每个设备使用独立연결,不要共享同一个Context
- 错误重试:네트워크抖动时使用指数退避重试,避免바로重试导致雪崩
- 유형转换:提前确认设备바이트序,使用统一的转换도구函数,避免重复代码
- 로그记录:关键操作记录로그,包括请求주소、数量、响应值和错误信息
- 优雅닫기:使用tokio::select!处理退出信号,确保데이터完整저장
- 주소规划:提前规划레지스터주소表,编写문서,避免주소冲突
- 测试先行:先用pymodbus模拟器或Modbus Slave测试代码,再接入真实设备
12.2 性能优化技巧
- 减少기능 코드전환:同一设备连续읽기时,先读完所有유지 레지스터再读코일,减少设备内部전환开销
- 주소对齐:읽기시작주소尽量从0或偶数开始,部分设备对非对齐주소处理较慢
- 避免频繁写操作:写操作通常比读操作慢,且可能触发设备内部处理,尽量批量쓰기
- 使用release모드编译:cargo build --release,性能比debug모드高3-10倍
- 裁剪feature:只활성화需要的프로토콜(tcp/rtu),减小2진수体积和编译时间
- 연결池:高频시나리오下可实现연결池,避免频繁建连
- 데이터缓存:变化缓慢的데이터(如设备매개변수)可缓存,减少읽기频率
十三、Rust与其他언어Modbus库对比
| 언어 | 库名 | 返回유형 | 异步 | 部署复杂度 | 性能 |
|---|---|---|---|---|---|
| Rust | tokio-modbus | Vec<bool>/Vec<u16> | 原生async | 低(单2진수) | 极高 |
| Go | grid-x/modbus | []byte(需手动분석) | goroutine | 低(单2진수) | 高 |
| Python | pymodbus | 对象(.registers/.bits) | asyncio | 低(需解释器) | 中 |
| C | libmodbus | uint16_t数组 | 不支持 | 中(需编译) | 极高 |
| Java | EasyModbus | int[]/boolean[] | 不支持 | 高(需JVM) | 中 |
| C# | NModbus | ushort[]/bool[] | async/await | 中(需.NET) | 高 |
十四、학습路线与자료
| 阶段 | 학습콘텐츠 | 预计时间 | 目标 |
|---|---|---|---|
| 入门 | Rust基础语法、tokio异步、TCP연결、读레지스터 | 1周 | 能읽기设备데이터 |
| 基础 | 전체기능 코드、RTU시리얼 포트、浮点数处理、错误处理 | 1周 | 能完成常见读写操作 |
| 进阶 | 并发采集、服务端实现、도구类封装、性能优化 | 2周 | 能개발稳定的采集系统 |
| 高级 | no_std嵌入式、사용자 정의 프로토콜、TLS安全、산업용部署 | 1个月 | 能개발산업용Modbus적용 |
14.1 推荐자료
- 官方문서:https://docs.rs/tokio-modbus
- 源码仓库:https://github.com/slowtec/tokio-modbus
- Modbus프로토콜사양:https://modbus.org/specs.php
- Rust异步编程:https://tokio.rs/tokio/tutorial
- modbus-rs:https://crates.io/crates/modbus-rs
- voltage_modbus:https://crates.io/crates/voltage_modbus
Rust언어以其内存安全、高性能和原生异步支持,正在成为산업 자동화领域的新选择。tokio-modbus作为最成熟的Rust Modbus库,提供了完善的TCP/RTU클라이언트和服务端实现,返回유형友好(Vec<bool>/Vec<u16>),문서完善,社区活跃。本文提供的도구函数和完整项目예제可直接用于实际개발,建议先在模拟器환경中验证,再接入真实设备。
VIP专属:Rust언어Modbus개발完整代码包
tokio-modbus v0.17完整예제代码:TCP/RTU클라이언트、服务端、데이터분석、批量采集,含详细중국어注释,可直接복사运行。
开通VIP即可下载完整代码,同时解锁 30+ 엔지니어링实战자료包:디버깅스크립트、速查表、项目模板、排查案例……
前往VIP자료库下载 → 月费仅9.9元 / 年费199元