Rust言語Modbus開発の完全ガイド:tokio-modbus/modbus-rs/voltage_modbus 3つのライブラリの詳細な比較と実践

無料無料技术资料

这篇内容可直接阅读,适合用于基础学习和検索引流。

メモリセキュリティ性、ゼロコストの抽象化、優れた並列性により、Rust言語は産業オートメーションや組込みの分野で急速に台頭しています。この記事では、Rustエコシステムにおける3つの主要なModbusライブラリ(tokio-modbus、modbus-rs、voltage_modbus)のインストール設定、APIの使用、TCP/RTU通信の実装、データ型処理、同時取得、サーバーサイド開発、完全なプロジェクトの実践について詳しく説明し、Rust開発者が産業用Modbus通信の開発をすばやく習得できるようにします。

産業用ModbusをRustで開発する理由

1.1 Rust在工业控制中的核心优势

  • メモリのセキュリティ性:编译期保证无空指针、无バッファオーバーフロー、无データ竞争,工业现场7×24小时运行更稳定
  • 零成本抽象:高水準構文は実行時のオーバーヘッドがなく、C言語に近い性能を発揮
  • 非同期ネイティブ:async/await構文+tokioランタイム、マルチデバイス同時取得を自然にサポート
  • 跨プラットフォーム编译:x 86/ARM/MIPS/RISC-Vをサポートし、産業用コンピュータ、Raspberry Pi、組み込みLinuxデバイスで動作します。
  • タイプ·システムコンパイル時にエラーをキャッチし、オンサイトデバッグ時間を短縮する強力な型システム
  • 单二进制部署:実行時環境を必要とせず、産業現場での展開に適した単一の実行ファイルにコンパイル
  • Cargoパッケージ管理簡単な依存関係管理、バージョンロックの再現性

1.2 Rust Modbusエコシステムの概要

库名最新バージョンのプロトコルサポート非同期no_stdサーバー側。特点
Tokyo-Modbus0.17.0TCP/RTUサポート(tokio)サポートなし。サポートする。最も人気のある、文書化された、アクティブなコミュニティ
モドバス-RS0.16.0TCP/RTU/ASCIIサポートする。サポートする。サポートする。低メモリ占有率、組み込み可能、機能ゲート
voltage_modbus0.7.2TCP/RTUサポートする。サポートなし。サポートなし。高いパフォーマンスを、要求パイプライン、読み取りマージ
ロドバス1.6.0TCP/RTU/TLSサポート(tokio)サポートなし。サポートする。商用グレード、TLSサポート、パニックフリー解析

Tokio-modbusライブラリの詳細な説明(最も主流)

2.1 インストール与Cargo設定

tokio-modbusはRustエコシステムで最も人気のあるModbusライブラリであり、tokio非同期ランタイムに基づいており、TCPとRTUの両方のトランスポートモードをサポートし、非同期APIと同期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 Feature説明書はデフォルトの有効化
TCPとは非同期TCPクライアントはい。
rtuさん异步RTUクライアント側ははい。
tcp-sync同期TCPクライアント{{どうきTCPくらいあんと}}いいえ、いいえ
rtu-sync同期RTUクライアントいいえ、いいえ
tcp-server异步TCPサーバー側。いいえ、いいえ
rtu-server非同期RTUサーバいいえ、いいえ
r-over-t-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:建立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:リードホールドレジスタ(機能コード03)
    // 住所から。0开始,読み取り101つのレジスタ
    // 戻り値は是 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:コイル状態の読み取り(機能コード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:写入单1つのレジスタ(機能コード06)
    ctx.write_single_register(100, 1234).await?;
    println!("n=== 単一レジスタの書き込み(機能コード06)===");
    println!("  レジスタ100書き込み済み。:1234");

    // 步骤5:写入单1つのコイル(機能コード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_modprel *;
use tokio_modbus client tcp;

#[tokio main]
async fn main-Result Box dyn std error Error {
    let socket_addr =“19 2.16 8.1.1(50 2)”.parse();

    //ゲートウェイの背後にあるスレーブアドレス1への接続
    // Slave from 1 Unit ID 1のスレーブを作成する
    let slave = Slave from 1;
    let mut ctx = t_slave s_slave.await;

    //以降の操作は自動的にユニットID=1を取得します。
    let regs = ctx.read 0 10.await;
    println! "スレーブステーション1のレジスタ値:{ "regs;

    //スレーブアドレスを切り替える(同じ接続でユニットIDを切り替えることができます)
    ctx.set_slave Slave from 2;
    let regs2 = ctx.read_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::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_data = vec![true, false, true, false, true, false, true, false];
    ctx.write_multiple_coils(0, &coil_data).await?;
    println!("=== 複数のコイルを書く。(0F)===");

    // ====== 機能コード10:複数のレジスタを書く。 ======
    let reg_data = vec![100u16, 200, 300, 400, 500];
    ctx.write_multiple_registers(100, &reg_data).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_data = vec![10u16, 20];
    let read_data = ctx.read_write_multiple_registers(
        0,      // 读開始アドレス
        5,      // 读数量
        300,    // 写開始アドレス
        &write_data  // データの書き込み
    ).await?;
    println!("=== 複数のレジスタの読み書き(17)===");
    println!("読み取り到:{:?}", read_data);

    Ok(())
}

III.データ型の処理

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,  // 言葉の交換: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つのレジスタ,アドレス100-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ビット整数変换

///32ビット符号付き整数(ビッグエンディアン)を解析
pub parse_i32 regs [u16]-i32 {
    bytes = [
        regs[0] 8
        regs[1] 8
    ];
    i32 from_be_bb
}

//32ビット符号なし整数(ビッグエンディアン)を解析
pub parse_u32 regs [u16]-u32 {
    bytes = [
        regs[0] 8
        regs[1] 8
    ];
    u32 from_be_bb
}

///64ビット整数(ビッグエンディアン、4レジスタ)を解析
pub parse_i64 regs [u16]-i64 {
    let mut bytes = [0 u 8; 8];
    私は0にいる。(4)
        b [i * 2] = regs[i] 8 as u8;
        bytes[i * 2 + 1] = regs[i] as u 8;
    }
    i64 from_be_bb
}

///64ビット浮動小数点数(ダブル、4レジスタ)を解析
pub parse_f64 regs [u16]-f64 {
    let mut bytes = [0 u 8; 8];
    私は0にいる。(4)
        b [i * 2] = regs[i] 8 as u8;
        bytes[i * 2 + 1] = regs[i] as u 8;
    }
    f64 from_be_bytes bytes
}

/// 16ビット符号付き整数(直接変換)
pub parse_i16 u16-i16 {
    I 16としての再設計
}

///スケーリングによる物理量解析
///例:温度レジスタの生値255、分解能0.1、実際の25.5°C
pub parse_ed u16 f32-f32 {
    F 32 * スケールで指定
}

///オフセット付き物理量の解析
///例:元の圧力1234、オフセット-1000、実際の234
pub parse_with_u16 i32-i32 {
    i 32+オフセットのような
}

3.3バイト順クイックチェックテーブル

バイトエンディアン32ビット値0 x 41420000の格納方法共通装備。
ABCD(ビッグエンディアン)[0]=0x4142、ほとんどのPLC、標準Modbusデバイス
CDAB(ワードスワップ)[0]=0x0000、部分シーメンス、部分国产デバイス
DCBA(リトル·エンド)[0]=0x0000、一部のx 86アーキテクチャデバイス
BADC(バイトスワップ)[0]=0x4241、Lessシングル

4. Modbus RTU通信

4.1 RTUクライアント側は設定

use tokio_modprel *;
use tokio_modr;
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";

    //シリアルパラメータ(スレーブデバイスと完全に一致する必要があります)
    let baud_rate = 9600 u 32;     //ボーレート:96 00、19200、38 400、115200
    let data_bits = 8;            //データビット:通常は8
    let stop_bits = tokio_serial StopBits One; // 停止ビット:1または2
    let parity = tokio_serial Parity;      //検証ビット:None、Even、Odd

    //シリアル構成を構築
    let builder = tokio_serial new serial_path baud_
        .data_bits data_bits
        ._bits_bits
        .parity(parity);

    //シリアルポートを開く
    let serial = serialStream open builder;

    //スレーブアドレスを指定したRTUクライアントの作成
    let slave = Slave from 1; //スレーブアドレス1-247
    let mut ctx = rtu attach_slave serial slave;

    //保持レジスタの読み込み
    let regs = ctx.read 0 10.await;
    println!“”レジスタ値{} "regs;

    //write coil
    ctx._le_coil 0 true.await;
    println!“コイル0”がON"

    Ok
}

//Cargo.tomlを追加する必要があります:
// [依存関係]
//tok-mod = "0.17"
// tokio = { version = "1" features = ["full"] }
//tok-serial = "5"

4.2 RTUよくある質問

問題考えられる原因解決策は
シリアルポートが開けないデバイス名エラー/权限不足チェック/dev/ttyUSB0存在するか;Linux执行sudo usermod -aG dialout $USER后重新ログイン
応答なし。A/B逆/スレーブアドレスエラーA/B回線の交換、スレーブアドレスのダイヤルコードまたはソフトウェア設定の確認
CRCエラーボーレート/チェックビットの不一致マスタースレーブステーションのシリアルパラメータが完全に一致することを確認する
時折の損失。干渉/終端抵抗バス両端に120Ω終端抵抗を追加、シールド付きツイストペアを使用
レスポンス·タイムアウトスレーブ処理が遅い/タイムアウトが短すぎるタイムアウト時間を3-5秒に延長
データの乱れエンディアン·エラーABCD/CDAB/DCBAの3つのエンディアンを試す

5.同期モード(ブロッキングAPI)

对于简单脚本或必要なし。异步的场景,tokio-modbus提供了同步API。需要有効tcp-sync或rtu-sync feature。

//カーゴ·トム
// [依存関係]
// tokio-modbus = { version = "0.17" default-features = false features = ["tcp-sync"] }

use tokio_modprel *;
use tokio_modbus client sync tcp;

main
    let socket_addr =“19 2.16 8.1.100(50 2)”.parse();

    //同期接続(接続が成功するまでブロックする)
    let mut ctx = ts_;

    //同期読み取り(応答までブロック)
    let regs = ctx.read 0 10;
    println!“”レジスタ値{} "regs;

    //同期書き込み
    ctx._le_register 100 1234;

    Ok
}

6.同時およびマルチデバイス収集

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!("読み取り失敗:{}", 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!("[{}] エラー:{}", data.name, e),
            None => println!("[{}] レジスタ:{:?}", data.name, data.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 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.エラー処理と例外コード

8.1 Modbus例外コード

異常コード。名前は意味の意味常见原因
0 x 0 1IllegalFunction違法な機能コードこの機能コードはサポートされません。
0 x 0 2IllegalDataAddress違法なデータアドレス要求アドレスが範囲外です
0x03IllegalDataValue不正なデータ値書き込み値が不正
0 x 0 4ServerDeviceFailureスレーブ機器の障害デバイスの内部エラー
0 x 0 5Acknowledgeより確認承認済みだが処理に時間がかかる
0 x 0 6ServerDeviceBusy駅から忙しい。処理中です。其他リクエスト

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::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は、組込みMCUやリソースに制約のある環境に適した低メモリフットプリント、no_std対応のModbusプロトコルスタックです。TCP/RTU/ASCI Iの3つのプロトコルをサポートし、同期APIと非同期APIを提供し、機能ゲート制御によりバイナリボリュームを制御します。

#カーゴ·トム
[依存する]
modbus-rs = "0.16"

#埋め込み使用no_std
# modbus-rs = { version = "0.16" default-features = false features = ["rtu""embedded"] }

use mod_rs prel *;

main
    //TCPクライアントの作成
    let mut client = modbus_rs((TcpClient)(new)“19 2.16 8.1.100(502));
    client.connect;

    //リードホールドレジスタ
    let regs = client.read_hing_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)が含まれ、高周波データ収集シナリオに適しています。

#カーゴ·トム
[依存する]
voltage_modbus = "0.7"

use voltage_modbus {ModbusTcpClient ModbusResult};

#[tokio main]
async fn main-ModbusResult {
    //TCPクライアントの作成、5秒のタイムアウト
    let mut = ModbusTClient from_
        "127.0.0.1 502"
        std time Duration from_secs 5
    .await

    //リードホールドレジスタ(機能コード0 3)
    // 注意:voltage_modbusメソッド名はread_03/read_04などの関数コード番号で命名されます。
    let values = client.read_03 1 0 10.await;  //_id=1、=0、count=10
    println!“”レジスタ値{} "values;

    //リードコイル(関数コード0 1)
    let coils = client.read_01 1 0 8.await;
    println! "コイルステータス"coils;

    //単一レジスタに書き込む(関数コード0 6)
    client.write_06 1 100 1234.await;

    Ok
}

10.1 3大図書館の性能比較

特性Tokyo-Modbusモドバス-RSvoltage_modbus
リクエスト流水线サポートなし。(严格串行)サポートなし。サポート(Nリクエスト約1 RTT)
連結の読み取りサポートなし。サポートなし。サポート隣接読み取りの自動マージ
ゼロ分配熱パスリクエストごとのヒープ割り当て部分ヒープ割り当て栈フレーム+持久バッファ
no_stdサポートサポートなし。サポートする。サポートなし。
サーバー側。サポートする。サポートする。サポートなし。
学习曲线低い(良いファイル)中では中では
適用可能なシーン通用工业项目組み込み型/リソースの制約高周波キャプチャ/パフォーマンス感度

11.完全なプロジェクトの実践:環境モニタリングシステム

以下は、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>,
}

/// 解析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) -> 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!("[{}] エラー:{}", data.name, e),
                None => {
                    println!("[{}] 温度:{:.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.ベストプラクティスとパフォーマンス最適化

12.1ベストプラクティスの例

  • バッチ読み取り複数のレジスタではなく、連続した複数のレジスタを一度に読み出す。1つのレジスタを10回読み取るよりも10倍高速です。
  • 接続の多重化高周波リクエスト時に長い接続を維持し、TCPハンドシェイクのオーバーヘッド(約1-3ms)を回避します。
  • 合理的な残業LAN設定は1-3秒、RTUシリアルポート設定は3-5秒です。tokio time timeoutを使用して要求をラップする
  • 同時分離複数デバイス同時実行では、デバイスごとに別々の接続を使用し、同じコンテキストを共有しない
  • エラー再試行:ネットワークジッタ時に指数バックオフ再試行を使用し、即時再試行による雪崩を回避
  • 型の変換:デバイスのエンディアンを事前に確認し、統一変換ツール機能を使用してコードの重複を避ける
  • ジャーナル·レコードリクエストアドレス、数量、レスポンス値、エラー情報を含む重要な運用ログ
  • 優雅な閉鎖。tokio終了信号を処理し、データを完全に保存する
  • 住所計画の策定:事前にレジスタアドレステーブルを計画し、ドキュメントを作成し、アドレス競合を回避する
  • 最初のテスト。pymodbusエミュレータまたはModbus Slaveでコードをテストし、実際のデバイスに接続する

12.2パフォーマンス最適化テクニック

  • 减少機能コード切换:同一デバイス连续読み取り时,先读完すべてのレジスタを保持する。再リードコイルを読む,减少デバイス内部切换开销
  • アドレスの整列読み取り開始アドレスは0または偶数から開始し、一部のデバイスは非整列アドレスの処理が遅くなります。
  • 頻繁な書き込みを避ける:書き込み操作。通常比読み取り操作。慢,且可能触发デバイス内部処理,尽量一括書き込みき込み入
  • releaseモードでのコンパイル:cargo build--releaseはデバッグモードの3-10倍のパフォーマンスを発揮する
  • カットフィーチャー:只有効需要的协议(tcp/rtu),减小二进制体积和编译时间
  • コネクションプール高周波シーンで接続プールを実現し、頻繁な接続を回避
  • データキャッシュ:变化缓慢的データ(如デバイス参数)可缓存,减少読み取り频率

Rustと他の言語のModbusライブラリの比較

言語はクワの名前戻り値の型非同期部署复杂度パフォーマンス
RustとはTokyo-ModbusVec<bool>/Vec<u16>原生async低(单二进制)非常に高い。
GO GO!grid-x/modbus[]byte手動解析が必要goroutineLow(シングルバイナリ)ハイ·ハイ
Pythonでpymodbus对象(.registers/.bits)Asyncio低(通訳が必要)中では
CはlibmodbusUint16_Tの配列サポートなし。中(翻訳が必要)非常に高い。
JavaEasyModbusint[]/boolean[]サポートなし。高(JVMが必要)中では
C#のNModbusshort []/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プロトコル仕様modbus.org/specs.php
  • Rust非同期プログラミング:https://tokio.rs/tokio/tutorial
  • モドバス-RScrates.io/crates/modbus-rs
  • voltage_modbuscrates.io/crates/voltage_modbus

メモリセキュリティ性、高いパフォーマンスを、ネイティブ非同期サポートを備えたRust言語は、産業オートメーションの新しい選択肢になりつつあります。最も成熟したRust Modbusライブラリであるtokio-modbusは、TCP/RTUクライアントとサーバの完全な実装、戻り値の型に優しい(Vec bool/Vec u 16)、文書化されたコミュニティを提供します。この記事で提供するツール機能と完全なプロジェクト例は、実際の開発に直接使用でき、シミュレータ環境で検証し、実際のデバイスに接続することを推奨します。

📦

VIP专属:Rust语言Modbus完全なコードパッケージの開発

tokio-modbus v 0.17完全なサンプルコード:TCP/RTUクライアント、サーバ、データ解析、バッチ収集、詳細な中国語注釈付き、直接コピー実行することができます。

完全なコードをダウンロードしてロックを解除 30歳以上。 工程実践パッケージの内容:スクリプトのデバッグ、クイックチェック·テーブル、项目模板、トラブルシューティング案例……

VIPデータベースからダウンロード 月額9.9ドル/年間199ドル
この資料を実際のプロジェクトに活用しませんか?

ツールセンターで電文解析、CRCチェック、デバイスデバッグを行うか、要件を送信して選定と接続のアドバイスを受けましょう。

エンジニア会員

この記事を実行可能なデバッグ資料に

登録すると、高度な電文解析、資料パックのダウンロード、コード例、エンジニアリングケース、優先技術サポートが利用可能に。実際のプロジェクト納品に最適です。

高度なツール無制限
資料パックとコードパック
完全なエンジニアリングケースライブラリ
優先技術サポート窓口