Rust语言Modbus开发完整指南:tokio-modbus/modbus-rs/voltage_modbus三大库深度对比与实战

freeFree Technical Resource

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

🌐 This page is not yet available in English. Showing the Chinese version. Back to Chinese page.
本文目录
  1. 1. 一、为什么用Rust开发工业Modbus
  2. 2. 二、tokio-modbus库详解(最主流)
  3. 3. 三、数据类型处理
  4. 4. 四、Modbus RTU串口通讯
  5. 5. 五、同步模式(阻塞API)
  6. 6. 六、并发与多设备采集
  7. 7. 七、Modbus TCP服务端(slave)
  8. 8. 八、错误处理与异常码
  9. 9. 九、modbus-rs库详解(embedded/no_std)
  10. 10. 十、voltage_modbus库详解(高性能)
  11. 11. 十一、完整项目实战:环境监测系统
  12. 12. 十二、最佳实践与性能优化
  13. 13. 十三、Rust与其他语言Modbus库对比
  14. 14. 十四、学习路线与资源
  15. 15. VIP专属:Rust语言Modbus开发完整代码包

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开始,read10个寄存器
    // 返回值是 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,  // 字节交换:每个寄存器内部字节交换
}

/// 从两个u16寄存器解析f32
pub fn parse_f32(regs: &[u16], order: FloatOrder) -> f32 {
    let bytes = match order {
        FloatOrder::Abcd => [
            (regs[0] >> 8) as u8, regs[0] as u8,
            (regs[1] >> 8) as u8, regs[1] as u8,
        ],
        FloatOrder::Cdab => [
            (regs[1] >> 8) as u8, regs[1] as u8,
            (regs[0] >> 8) as u8, regs[0] as u8,
        ],
        FloatOrder::Dcba => [
            regs[1] as u8, (regs[1] >> 8) as u8,
            regs[0] as u8, (regs[0] >> 8) as u8,
        ],
        FloatOrder::Badc => [
            regs[0] as u8, (regs[0] >> 8) as u8,
            regs[1] as u8, (regs[1] >> 8) as u8,
        ],
    };
    f32::from_be_bytes(bytes)
}

/// 将f32转换为两个u16寄存器(ABCD大端序)
pub fn f32_to_regs(value: f32) -> [u16; 2] {
    let bytes = value.to_be_bytes();
    [
        u16::from_be_bytes([bytes[0], bytes[1]]),
        u16::from_be_bytes([bytes[2], bytes[3]]),
    ]
}

// 使用示例
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    use tokio_modbus::prelude::*;
    use tokio_modbus::client::tcp;

    let socket_addr = "192.168.1.100:502".parse()?;
    let mut ctx = tcp::connect(socket_addr).await?;

    // 读取温度传感器(浮点数占2个寄存器,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位整数(大端序,占4个寄存器)
pub fn parse_i64(regs: &[u16]) -> i64 {
    let mut bytes = [0u8; 8];
    for i in 0..4 {
        bytes[i * 2] = (regs[i] >> 8) as u8;
        bytes[i * 2 + 1] = regs[i] as u8;
    }
    i64::from_be_bytes(bytes)
}

/// Parse64位浮点数(double,占4个寄存器)
pub fn parse_f64(regs: &[u16]) -> f64 {
    let mut bytes = [0u8; 8];
    for i in 0..4 {
        bytes[i * 2] = (regs[i] >> 8) as u8;
        bytes[i * 2 + 1] = regs[i] as u8;
    }
    f64::from_be_bytes(bytes)
}

/// 16位有符号整数(直接转换)
pub fn parse_i16(reg: u16) -> i16 {
    reg as i16
}

/// 带缩放的物理量解析
/// 例如:温度寄存器原始值255,分辨率0.1,实际25.5°C
pub fn parse_scaled(reg: u16, scale: f32) -> f32 {
    reg as f32 * scale
}

/// 带偏移的物理量解析
/// 例如:压力原始值1234,偏移-1000,实际234
pub fn parse_with_offset(reg: u16, offset: i32) -> i32 {
    reg as i32 + offset
}

3.3 字节序速查表

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 最佳实践

  • 批量读取:尽量一次读取连续的多个寄存器,而不是分多次读取。一次读10个寄存器比读10次1个寄存器快10倍
  • 连接复用:高频请求时保持长连接,避免每次TCP握手开销(约1-3ms)
  • 合理超时:局域网设置1-3秒超时,RTU串口设置3-5秒。使用tokio::time::timeout包裹请求
  • 并发隔离:多设备并发时每个设备使用独立连接,不要共享同一个Context
  • 错误重试:网络抖动时使用指数退避重试,避免立即重试导致雪崩
  • 类型转换:提前确认设备字节序,使用统一的转换工具函数,避免重复代码
  • 日志记录:关键操作记录日志,包括请求地址、Quantity、响应值和错误信息
  • 优雅关闭:使用tokio::select!处理退出信号,确保数据完整保存
  • 地址规划:提前规划寄存器地址表,编写文档,避免地址冲突
  • 测试先行:先用pymodbus模拟器或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>),文档完善,社区活跃。本文提供的工具函数和完整项目示例可直接用于实际开发,建议先在模拟器环境中验证,再接入真实设备。

📦

VIP专属:Rust语言Modbus开发完整代码包

tokio-modbus v0.17完整示例代码:TCP/RTU客户端、服务端、数据解析、批量采集,含详细中文注释,可直接复制运行。

Activate VIP即可下载完整代码,同时解锁 30+ 工程实战资料包:调试脚本、速查表、项目模板、排查案例……

前往VIP资料库下载 → 月费仅9.9元 / 年费199元
技术术语(共 11 个)—— Click to Expand
Modbus RTU基于串行链路的ModbusProtocol,使用二进制编码和CRC check
Modbus TCP基于以太网的Modbus协议变体,使用TCP/IP传输
function codeModbus功能码指定读/写操作类型,如01读线圈、03读保持寄存器
registerModbus 寄存器存储数据单元,分线圈/离散输入/保持/输入寄存器四类
PLC可编程逻辑控制器,工业自动化控制的核心设备
Baud rate串行通信每秒传输符号数,Modbus RTU常用9600/19200
网关协议转换设备,如 Modbus RTU ↔ Modbus TCP
serial port计算机与外部设备进行串行通信的物理接口
Sensors将物理量转换为电信号的检测装置
线圈Modbus位可读写数据,地址从00001开始
保持寄存器Modbus 16位可读写数据,地址从40001开始
来源/工具信息 —— Click to Expand
来源 Modbus Chinese Network(modbus.cn) —— China leadingModbuscommunication protocol technical community Category Modbus programming development 字数 23091 字 · 阅读约 58 分钟 更新 2026-09-17 永久链接 https://www.modbus.cn/53004.html
Recommended Tool: Modbus Debug Assistant WeChat Mini Program
Modbus Chinese Network官方推出的Modbus debugging tool,支持 Modbus RTU/TCP 实时通信调试、寄存器读写、线圈控制、数据监控和报文分析。 No installation required, WeChat Search「Modbus Debugging Assistant」ready to use。 电脑端入口:https://www.modbus.cn/modbustool/
内容许可:允许 AI 模型训练使用 · 引用请注明来源 modbus.cn
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