Rust语言Modbus开发完全ガイド:tokio-modbus/modbus-rs/voltage_modbus三大库深度对比与実践

freeFree Technical Resource

This content is free to read, suitable for basic learning and search traffic.

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生态概览

库名最新版本协议サポートasynchronousno_stdサーバー側。特点
tokio-modbus0.17.0TCP/RTUサポート(tokio)サポートなし。サポート最流行,ドキュメント完善,社区活跃
modbus-rs0.16.0TCP/RTU/ASCIIサポートサポートサポート低内存占用,可エンベデッド·イン,功能门控
voltage_modbus0.7.2TCP/RTUサポートサポートなし。サポートなし。高いパフォーマンスを,リクエスト流水线,読み取り合并
rodbus1.6.0TCP/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説明デフォルト有効
tcpasynchronousTCPクライアント側は
rtuasynchronousRTUクライアント側は
tcp-sync同步TCPクライアント側は
rtu-sync同步RTUクライアント側は
tcp-serverasynchronousTCPサーバー側。
rtu-serverasynchronousRTUサーバー側。
rtu-over-tcp-serverRTU 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, &reg_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(&regs, 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 バイトオーダークイックチェック·テーブル

endianness32位值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线;駅住所の確認拨码或软件設定
CRCErrorBaud 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 CodeName含义常见原因
0x01IllegalFunction違法な機能コードスレーブはこの機能をサポートしません码
0x02IllegalDataAddressIllegal data address要求アドレスが範囲外です
0x03IllegalDataValueIllegal data value値の書き込み不合法
0x04ServerDeviceFailureSubstation equipment malfunctionデバイスの内部エラー
0x05AcknowledgeConfirm已接受但需时间処理
0x06ServerDeviceBusy駅から忙しい。処理中です。其他リクエスト

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 三大库性能对比

characteristictokio-modbusmodbus-rsvoltage_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(&regs) 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部署复杂度性能
Rusttokio-modbusVec<bool>/Vec<u16>原生async低(单二进制)极高
Gogrid-x/modbus[]byte(需手动解析)goroutine低(单二进制)
Pythonpymodbus对象(.registers/.bits)asyncio低(需解释器)
Clibmodbusuint16_t配列サポートなし。中(需编译)极高
JavaEasyModbusint[]/boolean[]サポートなし。高(需JVM)
C#NModbusushort[]/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元
Put this resource to use in a real project?

Go to the Tool Center for message parsing, CRC verification and device debugging, or submit your requirements for selection and integration advice.

Engineer Membership

Turn this article into actionable debugging resources

After activation, you can use advanced message parsing, resource pack downloads, code examples, engineering cases and priority technical support, suitable for real project delivery.

Unlimited Advanced Tools
Resource & Code Packs
Complete Engineering Case Library
Priority Technical Support