Rust语言以其内存セキュリティ、零成本抽象和优秀的并发性能,正在工业自動化和エンベデッド·イン领域快速崛起。本記事详细介绍Rust生态中三大主流Modbus库——tokio-modbus、modbus-rs和voltage_modbus的インストール設定、API使用、TCP/RTU通讯実装、データ型処理、并发采集、サーバー側。开发以及完全项目実践,帮助Rust開発者たちはスピードマスター。工业Modbus通讯开发。
一、なぜか。用Rust开发工业Modbus
1.1 Rust在工业控制中的核心优势
- 内存セキュリティ:编译期保证无空指针、无バッファオーバーフロー、无データ竞争,industrial site7×24小时运行更稳定
- 零成本抽象:エキスパート语法不产生运行时开销,性能接近C语言
- 异步原生:async/await语法+tokio运行时,天然サポート多デバイス并发采集
- 跨プラットフォーム编译:サポートx86/ARM/MIPS/RISC-V,可运行在工控机、Raspberry Pi、embeddedLinuxデバイス上
- タイプ系统:强大的タイプ系统在编译期捕获エラー,减少オンサイトデバッグ时间
- 单二进制部署:编译为单个可执行文件,无需运行时环境,适合工业现场部署
- Cargo包管理:依赖管理简单,版本锁定可复现构建
1.2 Rust Modbus生态概览
| 库名 | 最新版本 | 协议サポート | asynchronous | 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-freeParse |
二、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"] }
# 按需裁剪功能(减小二进制体积)
# 仅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 | asynchronousTCPクライアント側は | 是 |
| rtu | asynchronousRTUクライアント側は | 是 |
| tcp-sync | 同步TCPクライアント側は | 否 |
| rtu-sync | 同步RTUクライアント側は | 否 |
| tcp-server | asynchronousTCPサーバー側。 | 否 |
| rtu-server | asynchronousRTUサーバー側。 | 否 |
| 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::Error>> {
// 步骤1:建立TCPconnect
// connect() 直接连接到Modbus TCPdevice,Unit IDデフォルト为0xFF
let socket_addr = "192.168.1.100:502".parse()?;
let mut ctx = tcp::connect(socket_addr).await?;
println!("接続しましたModbus TCPdevice:192.168.1.100:502");
// 步骤2:Read and hold register(function code03)
// 住所から。0开始,read101つのレジスタ
// 戻り値は是 Vec<u16>,无需手动解析バイト
let regs = ctx.read_holding_registers(0, 10).await?;
println!("\n=== レジスタを保持する。(function code03)===");
for (i, &val) in regs.iter().enumerate() {
println!(" register{}(4{:04d}):{}", i, i + 1, val);
}
// 步骤3:Read coil status(function code01)
let coils = ctx.read_coils(0, 8).await?;
println!("\n=== Coil status(function code01)===");
for (i, &state) in coils.iter().enumerate() {
println!(" コイル{}:{}", i, if state { "吸合" } else { "断开" });
}
// 步骤4:Write to a single register(function code06)
ctx.write_single_register(100, 1234).await?;
println!("\n=== write single register(function code06)===");
println!(" register100書き込み済み。:1234");
// 步骤5:Write into a single coil(function code05)
// 注意:tokio-modbus直接接受bool值,必要なし。手动转换为0xFF00/0x0000
ctx.write_single_coil(0, true).await?;
println!("\n=== write single coil(function code05)===");
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::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!("slave1的レジスタの値:{:?}", regs);
// 切换駅からの住所(同一连接可以切换Unit ID)
ctx.set_slave(Slave::from(2));
let regs2 = ctx.read_holding_registers(0, 10).await?;
println!("slave2的レジスタの値:{:?}", regs2);
Ok(())
}
2.5 完全機能コードサンプル
use tokio_modbus::prelude::*;
use tokio_modbus::client::tcp;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let socket_addr = "192.168.1.100:502".parse()?;
let mut ctx = tcp::connect(socket_addr).await?;
// ====== function code01:リードコイルを読む ======
// 戻る 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!();
// ====== function code02:read discrete inputs ======
let inputs = ctx.read_discrete_inputs(0, 8).await?;
println!("=== read discrete inputs(02)===");
println!("input state:{:?}", inputs);
// ====== function code03:read holding registers ======
let holding = ctx.read_holding_registers(0, 10).await?;
println!("=== read holding registers(03)===");
println!("register value:{:?}", holding);
// ====== function code04:read input registers ======
let input_regs = ctx.read_input_registers(0, 4).await?;
println!("=== read input registers(04)===");
println!("模拟量值:{:?}", input_regs);
// ====== function code05:write single coil ======
// 直接传bool,库内部自动转换为0xFF00/0x0000
ctx.write_single_coil(0, true).await?; // コイル0置ON
ctx.write_single_coil(1, false).await?; // コイル1置OFF
println!("=== write single coil(05)===");
// ====== function code06:write single register ======
ctx.write_single_register(50, 999).await?;
println!("=== write single register(06)===");
// ====== function code0F:write multiple coils ======
// 传入 &[bool],库内部自动按位パッキング
let coil_data = vec![true, false, true, false, true, false, true, false];
ctx.write_multiple_coils(0, &coil_data).await?;
println!("=== write multiple coils(0F)===");
// ====== function code10:write multiple registers ======
let reg_data = vec![100u16, 200, 300, 400, 500];
ctx.write_multiple_registers(100, ®_data).await?;
println!("=== write multiple registers(10)===");
// ====== function code16:掩码写レジスタ ======
// 新值 = (current value AND and_mask) OR or_mask
// 例如:将レジスタ200的bit3置1,其他位不变
// and_mask = !0x0008 = 0xFFF7(Clearbit3)
// or_mask = 0x0008(設定bit3)
ctx.masked_write_register(200, 0xFFF7, 0x0008).await?;
println!("=== 掩码写レジスタ(16)===");
// ====== function code17:read and write multiple registers ======
// 最初に書いてから読む。!注意顺序与函数名相反
// 写入レジスタ300-301,同时読み取りレジスタ0-4
let write_data = vec![10u16, 20];
let read_data = ctx.read_write_multiple_registers(
0, // 读開始アドレス
5, // read quantity
300, // 写開始アドレス
&write_data // simulate error
).await?;
println!("=== read and write multiple registers(17)===");
println!("読み取り到:{:?}", read_data);
Ok(())
}
三、データ型処理
tokio-modbus的read_holding_registers戻るVec<u16>,但实际工业デバイス中经常使用32ビット浮動小数点数、32ビット整数、64ビット·データ等。以下提供完全的转换工具。
3.1 32ビット浮動小数点数转换
/// 32ビット浮動小数点数バイトオーダー枚举
#[derive(Debug, Clone, Copy)]
pub enum FloatOrder {
Abcd, // ビッグエンディアン序:reg[0]=高16位, reg[1]=低16位(最も一般的な)
Cdab, // Word exchange:reg[0]=低16位, reg[1]=高16位
Dcba, // リトルエンディアン序:完全反转
Badc, // バイトスワップ:すべてのレジスタ。内部バイトスワップ
}
/// 从2つ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转换为2つ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::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?;
// 読み取り温度センサー(浮動小数点数占21つのレジスタ,address100-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ビット整数変換
/// Parse32ビット符号付き整数(ビッグエンディアン序)
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)
}
/// Parse32ビット符号なし整数(ビッグエンディアン序)
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)
}
/// Parse64ビット整数(ビッグエンディアン序,占41つのレジスタ)
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)
}
/// Parse64ビット浮動小数点数(double,占41つのレジスタ)
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 バイトオーダークイックチェック·テーブル
| endianness | 32位值0x41420000的存储方式 | 共通装備。 |
|---|---|---|
| ABCD(ビッグエンディアン) | reg[0]=0x4142, reg[1]=0x0000 | 大多数PLC、標準Modbusdevice |
| CDAB(Word exchange) | 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::Error>> {
// シリアルデバイス路径
// Linux: /dev/ttyUSB0, /dev/ttyS0
// Windows: COM3
// macOS: /dev/tty.usbserial-xxx
let serial_path = "/dev/ttyUSB0";
// Serial port parameters(必须与スレーブデバイス完全一致)
let baud_rate = 9600u32; // Baud rate:9600, 19200, 38400, 115200
let data_bits = 8; // data bit:通常为8
let stop_bits = tokio_serial::StopBits::One; // stop bit:1或2
let parity = tokio_serial::Parity::None; // check digit:None, Even, Odd
// 构建シリアルポート設定
let builder = tokio_serial::new(serial_path, baud_rate)
.data_bits(data_bits)
.stop_bits(stop_bits)
.parity(parity);
// Open the serial port
let serial = SerialStream::open(&builder)?;
// 作成RTUクライアント側は,指定駅からの住所
let slave = Slave::from(1); // slave address1-247
let mut ctx = rtu::attach_slave(serial, slave);
// Read and hold register
let regs = ctx.read_holding_registers(0, 10).await?;
println!("register value:{:?}", 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よくある質問トラブルシューティング
| 問題 | 可能原因 | 解決方法 |
|---|---|---|
| シリアルポート打不开 | デバイス名エラー/Insufficient permissions | チェック/dev/ttyUSB0存在するか;Linux执行sudo usermod -aG dialout $USER后重新ログイン |
| 応答なし。 | A/Bライン·リバース/駅からの住所エラー | 交換A/B线;駅住所の確認拨码或软件設定 |
| CRCError | Baud rate/チェックビット不マッチング | 確認マスター·マスター駅シリアルパラメータ完全一致 |
| 偶尔丢包 | 干渉/終端抵抗 | バス两端加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::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!("register value:{:?}", 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,
address: String,
slave: Slave,
}
/// 結果の収集
struct DeviceData {
name: String,
registers: Vec<u16>,
error: Option<String>,
}
/// 采集单个デバイス
async fn read_device(cfg: &DeviceConfig) -> DeviceData {
let socket_addr = match cfg.address.parse() {
Ok(addr) => addr,
Err(e) => return DeviceData {
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 DeviceData {
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)) => DeviceData {
name: cfg.name.clone(),
registers: regs,
error: None,
},
Ok(Err(e)) => DeviceData {
name: cfg.name.clone(),
registers: vec![],
error: Some(format!("Read failed:{}", e)),
},
Err(_) => DeviceData {
name: cfg.name.clone(),
registers: vec![],
error: Some("読み取りタイムアウト".to_string()),
},
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let devices = vec![
DeviceConfig {
name: "PLC-车间A".to_string(),
address: "192.168.1.100:502".to_string(),
slave: Slave::from(1),
},
DeviceConfig {
name: "PLC-车间B".to_string(),
address: "192.168.1.101:502".to_string(),
slave: Slave::from(1),
},
DeviceConfig {
name: "電気メーター-总进线".to_string(),
address: "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(data) = handle.await {
results.push(data);
}
}
// 出力結果
println!("=== 結果の収集 ===");
for data in &results {
match &data.error {
Some(e) => println!("[{}] Error:{}", data.name, e),
None => println!("[{}] register:{:?}", data.name, data.registers),
}
}
Ok(())
}
七、Modbus TCPサーバー側。(slave)
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 ModbusData {
coils: Arc<Mutex<Vec<bool>>>,
discrete_inputs: Arc<Mutex<Vec<bool>>>,
holding_registers: Arc<Mutex<Vec<u16>>>,
input_registers: Arc<Mutex<Vec<u16>>>,
}
impl ModbusData {
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 {
data: ModbusData,
}
impl Service for ModbusService {
type Request = Request;
type Response = Response;
type Error = std::io::Error;
fn call(&self, req: Self::Request) -> Result<Self::Response, Self::Error> {
match req {
Request::ReadCoils(addr, cnt) => {
let coils = self.data.coils.lock().unwrap();
let end = (addr + cnt) as usize;
if end > coils.len() {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "アドレス越界"));
}
Ok(Response::ReadCoils(coils[addr as usize..end].to_vec()))
}
Request::ReadDiscreteInputs(addr, cnt) => {
let inputs = self.data.discrete_inputs.lock().unwrap();
let end = (addr + cnt) as usize;
Ok(Response::ReadDiscreteInputs(inputs[addr as usize..end].to_vec()))
}
Request::ReadHoldingRegisters(addr, cnt) => {
let regs = self.data.holding_registers.lock().unwrap();
let end = (addr + cnt) as usize;
Ok(Response::ReadHoldingRegisters(regs[addr as usize..end].to_vec()))
}
Request::ReadInputRegisters(addr, cnt) => {
let regs = self.data.input_registers.lock().unwrap();
let end = (addr + cnt) as usize;
Ok(Response::ReadInputRegisters(regs[addr as usize..end].to_vec()))
}
Request::WriteSingleCoil(addr, coil) => {
let mut coils = self.data.coils.lock().unwrap();
coils[addr as usize] = coil;
Ok(Response::WriteSingleCoil(addr, coil))
}
Request::WriteSingleRegister(addr, value) => {
let mut regs = self.data.holding_registers.lock().unwrap();
regs[addr as usize] = value;
Ok(Response::WriteSingleRegister(addr, value))
}
Request::WriteMultipleCoils(addr, coils_data) => {
let mut coils = self.data.coils.lock().unwrap();
for (i, &v) in coils_data.iter().enumerate() {
coils[addr as usize + i] = v;
}
Ok(Response::WriteMultipleCoils(addr, coils_data.len() as u16))
}
Request::WriteMultipleRegisters(addr, regs_data) => {
let mut regs = self.data.holding_registers.lock().unwrap();
for (i, &v) in regs_data.iter().enumerate() {
regs[addr as usize + i] = v;
}
Ok(Response::WriteMultipleRegisters(addr, regs_data.len() as u16))
}
_ => Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "サポートなし。的機能コード")),
}
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let data = ModbusData::new();
// 設定初期値は
{
let mut regs = data.holding_registers.lock().unwrap();
regs[0] = 100;
regs[1] = 200;
regs[2] = 300;
}
{
let mut coils = data.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 { data: data.clone() };
move |req| service.call(req)
}).await?;
Ok(())
}
八、エラー処理与異常コード。
8.1 ModbusException Code
| Exception Code | Name | 含义 | 常见原因 |
|---|---|---|---|
| 0x01 | IllegalFunction | 違法な機能コード | スレーブはこの機能をサポートしません码 |
| 0x02 | IllegalDataAddress | Illegal data address | 要求アドレスが範囲外です |
| 0x03 | IllegalDataValue | Illegal data value | 値の書き込み不合法 |
| 0x04 | ServerDeviceFailure | Substation equipment malfunction | デバイスの内部エラー |
| 0x05 | Acknowledge | Confirm | 已接受但需时间処理 |
| 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!("No.{}次読み取り失敗:{}", attempt + 1, last_error);
}
Err(_) => {
last_error = "読み取りタイムアウト".to_string();
eprintln!("No.{}次読み取りタイムアウト", 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::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库详解(embedded/no_std)
modbus-rs是一个低内存占用、サポートno_std的Modbusプロトコル栈,适合エンベデッド·インMCU和リソース受限环境。サポートTCP/RTU/ASCII三种协议,提供同步和异步API,経由feature门控控制二进制体积。
# 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::Error>> {
// 作成TCPクライアント側は
let mut client = modbus_rs::TcpClient::new("192.168.1.100:502")?;
client.connect()?;
// read holding registers
let regs = client.read_holding_registers(1, 0, 10)?;
println!("register value:{:?}", regs);
// write single coil
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_address(
"127.0.0.1:502",
std::time::Duration::from_secs(5)
).await?;
// read holding registers(function code03)
// 注意:voltage_modbus的方法名以read_03/read_04等機能コード编号命名
let values = client.read_03(1, 0, 10).await?; // unit_id=1, addr=0, count=10
println!("register value:{:?}", values);
// リードコイルを読む(function code01)
let coils = client.read_01(1, 0, 8).await?;
println!("Coil status:{:?}", coils);
// write single register(function code06)
client.write_06(1, 100, 1234).await?;
Ok(())
}
10.1 三大库性能对比
| characteristic | tokio-modbus | modbus-rs | voltage_modbus |
|---|---|---|---|
| リクエスト流水线 | サポートなし。(严格串行) | サポートなし。 | サポート(N个リクエスト约1个RTT) |
| 読み取り合并 | サポートなし。 | サポートなし。 | サポート(自动合并相邻読み取り) |
| ゼロ分布。热路径 | 每次リクエスト堆分配 | 部分堆分配 | 栈フレーム+持久バッファ |
| no_stdサポート | サポートなし。 | サポート | サポートなし。 |
| サーバー側。 | サポート | サポート | サポートなし。 |
| 学习曲线 | 低(ドキュメント好) | 中 | 中 |
| 適用可能なシーン | 通用工业项目 | embedded/リソース受限 | 高频采集/性能敏感 |
十一、完全项目実践:環境モニタリング系统
以下是一个完全的工业環境モニタリング系统,使用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,
address: String,
slave: Slave,
temp_addr: u16, // 温度レジスター·アドレス
hum_addr: u16, // 湿度レジスター·アドレス
temp_type: TempType,
}
#[derive(Clone, Copy)]
enum TempType {
Int16, // 16ビット符号付き整数,分辨率0.1°C
Float32, // 32ビット浮動小数点数
}
/// センサーはデータ
#[derive(Clone)]
struct SensorData {
name: String,
timestamp: chrono::DateTime<chrono::Local>,
temperature: f64,
humidity: f64,
error: Option<String>,
}
/// Parse32ビット浮動小数点数(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) -> SensorData {
let data = SensorData {
name: cfg.name.clone(),
timestamp: chrono::Local::now(),
temperature: 0.0,
humidity: 0.0,
error: None,
};
let socket_addr = match cfg.address.parse() {
Ok(addr) => addr,
Err(e) => return SensorData { error: Some(format!("アドレスエラー:{}", e)), ..data },
};
let mut ctx = match tcp::connect_slave(socket_addr, cfg.slave).await {
Ok(ctx) => ctx,
Err(e) => return SensorData { error: Some(format!("接続の失敗:{}", e)), ..data },
};
// 読み取り温度
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 SensorData { error: Some(format!("温度読み取り失敗:{}", e)), ..data },
Err(_) => return SensorData { error: Some("温度読み取りタイムアウト".to_string()), ..data },
}
}
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 SensorData { error: Some(format!("温度読み取り失敗:{}", e)), ..data },
Err(_) => return SensorData { error: Some("温度読み取りタイムアウト".to_string()), ..data },
}
}
};
// 読み取り湿度(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 SensorData { error: Some(format!("湿度読み取り失敗:{}", e)), ..data },
Err(_) => return SensorData { error: Some("湿度読み取りタイムアウト".to_string()), ..data },
};
SensorData { temperature, humidity, ..data }
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let sensors = vec![
SensorConfig {
name: "车间A".to_string(),
address: "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(),
address: "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(),
address: "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::<SensorData>::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(data) = handle.await {
results.push(data);
}
}
// 出力結果
println!("=== {} ===", chrono::Local::now().format("%Y-%m-%d %H:%M:%S"));
for data in &results {
match &data.error {
Some(e) => println!("[{}] Error:{}", data.name, e),
None => {
println!("[{}] temperature:{:.1}°C,湿度:{:.1}%", data.name, data.temperature, data.humidity);
// 異常告警
if data.temperature > 35.0 {
println!(" ⚠️ 温度过高告警:{:.1}°C", data.temperature);
}
if data.humidity > 80.0 {
println!(" ⚠️ 湿度过高告警:{:.1}%", data.humidity);
}
}
}
}
println!();
// 保存历史データ
history.lock().unwrap().extend(results);
}
}
// Cargo.toml 完全依赖:
// [dependencies]
// tokio-modbus = "0.17"
// tokio = { version = "1", features = ["full"] }
// chrono = "0.4"
十二、ベストプラクティス与パフォーマンス最適化
12.1 ベストプラクティス
- バッチ読み取り:尽量一次読み取り连续的多1つのレジスタ,而ではない分多次読み取り。一次读101つのレジスタ比读10次11つのレジスタ快10倍
- 连接复用:高频リクエスト时保持长连接,避免每次TCP握手开销(约1-3ms)
- 合理タイムアウト:局域网設定1-3秒タイムアウト,RTUシリアルポート設定3-5秒。使用tokio::time::timeout包裹リクエスト
- 并发絶縁:多デバイス并发时每个デバイス使用独立连接,不要共享同一个Context
- エラー重试:网络抖动使用するとき指数退避重试,避免立即重试导致雪崩
- タイプ转换:提前機器の確認バイトオーダー,使用统一的转换工具函数,避免重复代码
- ジャーナル·レコード:关键操作记录日志,リクエストを含むアドレス、Quantity、レスポンス值和エラー信息
- 优雅关闭:使用tokio::select!処理退出信号,确保データ完全保存
- アドレス规划:提前规划レジスター·アドレス表,编写ドキュメント,避免アドレス冲突
- 测试先行:先用pymodbusSimulator(シミュレータ)或Modbus Slave测试代码,再接入真实デバイス
12.2 パフォーマンス最適化技巧
- 减少機能コード切换:同一デバイス连续読み取り时,先读完すべてのレジスタを保持する。再リードコイルを読む,减少デバイス内部切换开销
- アドレス对齐:読み取り開始アドレス尽量从0或偶数开始,部分デバイス对非对齐アドレス処理较慢
- 避免频繁書き込み操作。:書き込み操作。通常比読み取り操作。慢,且可能触发デバイス内部処理,尽量一括書き込みき込み入
- 使用releaseモード编译:cargo build --release,性能比debugモード高3-10倍
- 裁剪feature:只有効需要的协议(tcp/rtu),减小二进制体积和编译时间
- 连接池:高频场景下可実装连接池,避免频繁建连
- データ缓存:变化缓慢的データ(如デバイス参数)可缓存,减少読み取り频率
十三、Rust与其他语言Modbus库对比
| 语言 | 库名 | 戻るタイプ | asynchronous | 部署复杂度 | 性能 |
|---|---|---|---|---|---|
| Rust | tokio-modbus | Vec<bool>/Vec<u16> | 原生async | 低(单二进制) | 极高 |
| Go | grid-x/modbus | []byte(需手动解析) | goroutine | 低(单二进制) | 高 |
| Python | pymodbus | 对象(.registers/.bits) | asyncio | 低(需解释器) | 中 |
| C | libmodbus | uint16_t配列 | サポートなし。 | 中(需编译) | 极高 |
| Java | EasyModbus | int[]/boolean[] | サポートなし。 | 高(需JVM) | 中 |
| C# | NModbus | ushort[]/bool[] | async/await | 中(需.NET) | 高 |
十四、学习路线与リソース
| 阶段 | 学习内容 | 预计时间 | 目标 |
|---|---|---|---|
| 入門 | Rust基础语法、tokioasynchronous、TCPconnect、读レジスタ | 1周 | 能読み取り設備のデータ |
| 基础 | すべて機能コード、RTUserial port、浮動小数点数処理、エラー処理 | 1周 | 能完成常见读書き込み操作。 |
| 上級 | 并发采集、サーバー側。実装、工具类封装、パフォーマンス最適化 | 2周 | 能开发稳定的采集系统 |
| エキスパート | no_stdembedded、Custom Protocol、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>),ドキュメント完善,社区活跃。本記事提供的工具函数和完全项目サンプル可直接使用する。于实际开发,建议先在Simulator(シミュレータ)环境中验证,再接入真实デバイス。
VIP专属:Rust语言Modbus完全なコードパッケージの開発
tokio-modbus v0.17完全サンプルコード:TCP/RTUクライアント側は、サーバー側。、データの解析、バッチ収集。,詳細な中国語解説,可直接コピー运行。
Activate VIP即可下载完全代码,同时解锁 30+ 工程実践パッケージの内容:スクリプトのデバッグ、クイックチェック·テーブル、项目模板、トラブルシューティング案例……
前往VIP资料库下载 → 月费仅9.9元 / 年费199元