You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Modbus协议的设备数据读取、轮询实现及返回值查看求助

如何查看Modbus设备返回的数据并实现轮询?

嘿,我来帮你搞定这个问题!你已经把串口初始化的代码写得差不多了,接下来咱们一步步解决查看设备返回数据和实现轮询功能这两个需求~

一、查看Modbus设备返回的数据

Modbus RTU协议是基于二进制的,设备返回的是字节数组,直接打印看不到有效信息,咱们可以把字节转成十六进制字符串(方便看原始帧),或者解析成实际的寄存器数值(如果是读寄存器场景)。下面结合你的代码补全逻辑:

1. 基础读取与打印原始返回帧

假设你已经有组装Modbus读取请求帧的逻辑,咱们可以这样读取并打印返回:

static void Main(string[] args)
{
    SerialPort mySerialPort = new SerialPort("COM6");
    mySerialPort.BaudRate = 19200;
    mySerialPort.Parity = Parity.None;
    mySerialPort.StopBits = StopBits.One;
    mySerialPort.DataBits = 8;
    mySerialPort.Handshake = Handshake.None;
    mySerialPort.ReadTimeout = 450;
    mySerialPort.WriteTimeout = 450;

    try
    {
        mySerialPort.Open();
        Console.WriteLine("串口已打开,开始读取数据...");

        // 替换成你自己的Modbus请求帧组装函数
        byte[] modbusRequest = GetModbusReadRequest();

        // 发送请求
        mySerialPort.Write(modbusRequest, 0, modbusRequest.Length);

        // 读取返回数据
        byte[] responseBuffer = new byte[256];
        int bytesRead = mySerialPort.Read(responseBuffer, 0, responseBuffer.Length);
        byte[] actualResponse = new byte[bytesRead];
        Array.Copy(responseBuffer, actualResponse, bytesRead);

        // 转成十六进制字符串打印,直观查看原始帧
        string hexResponse = BitConverter.ToString(actualResponse).Replace("-", " ");
        Console.WriteLine($"设备返回原始帧(十六进制):{hexResponse}");

        // 示例:解析读寄存器返回的数值(大端模式)
        if (bytesRead >= 4)
        {
            int registerValue = (actualResponse[2] << 8) | actualResponse[3];
            Console.WriteLine($"解析得到寄存器值:{registerValue}");
        }
    }
    catch (TimeoutException)
    {
        Console.WriteLine("读取超时,设备未响应");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"发生错误:{ex.Message}");
    }
    finally
    {
        if (mySerialPort.IsOpen)
            mySerialPort.Close();
    }

    Console.ReadLine();
}

// 示例:组装读保持寄存器的Modbus RTU请求(设备地址0x01,读起始寄存器0x0000的1个寄存器)
static byte[] GetModbusReadRequest()
{
    byte[] request = new byte[8];
    request[0] = 0x01; // 设备地址
    request[1] = 0x03; // 功能码:读保持寄存器
    request[2] = 0x00; // 起始寄存器高字节
    request[3] = 0x00; // 起始寄存器低字节
    request[4] = 0x00; // 寄存器数量高字节
    request[5] = 0x01; // 寄存器数量低字节
    // 最后两个字节是CRC16校验,实际使用时需要计算,这里是示例固定值
    request[6] = 0x84;
    request[7] = 0x0A;
    return request;
}

2. 用Modbus库简化解析

如果不想自己处理CRC校验和帧解析,推荐用成熟的Modbus库(比如NModbus),它会直接帮你把返回数据转换成寄存器数值,代码更简洁:

// 先通过NuGet安装NModbus包
using Modbus.Device;

static void Main(string[] args)
{
    using (SerialPort serialPort = new SerialPort("COM6"))
    {
        serialPort.BaudRate = 19200;
        serialPort.Parity = Parity.None;
        serialPort.StopBits = StopBits.One;
        serialPort.DataBits = 8;
        serialPort.Open();

        // 创建Modbus RTU主站
        IModbusSerialMaster master = ModbusSerialMaster.CreateRtu(serialPort);
        byte slaveId = 0x01; // 设备地址
        ushort startAddress = 0x0000;
        ushort numRegisters = 0x01;

        try
        {
            // 直接读取保持寄存器数值
            ushort[] registers = master.ReadHoldingRegisters(slaveId, startAddress, numRegisters);
            Console.WriteLine($"读取到寄存器值:{registers[0]}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"读取失败:{ex.Message}");
        }
    }
    Console.ReadLine();
}

二、实现轮询功能

轮询就是每隔固定时间重复发送读取请求,这里推荐用System.Timers.Timer实现,比循环加Thread.Sleep更灵活,且不会阻塞主线程:

using System.Timers;

static SerialPort _serialPort;
static IModbusSerialMaster _modbusMaster;
static Timer _pollingTimer;

static void Main(string[] args)
{
    // 初始化串口
    _serialPort = new SerialPort("COM6");
    _serialPort.BaudRate = 19200;
    _serialPort.Parity = Parity.None;
    _serialPort.StopBits = StopBits.One;
    _serialPort.DataBits = 8;
    _serialPort.ReadTimeout = 450;
    _serialPort.WriteTimeout = 450;

    try
    {
        _serialPort.Open();
        Console.WriteLine("串口已打开,开始轮询...");

        // 初始化Modbus主站(用NModbus简化操作)
        _modbusMaster = ModbusSerialMaster.CreateRtu(_serialPort);

        // 设置轮询间隔为500ms(可根据设备响应速度调整)
        _pollingTimer = new Timer(500);
        _pollingTimer.Elapsed += PollingTimer_Elapsed;
        _pollingTimer.AutoReset = true; // 自动重复触发
        _pollingTimer.Start();

        Console.WriteLine("按任意键停止轮询...");
        Console.ReadKey();
    }
    catch (Exception ex)
    {
        Console.WriteLine($"初始化失败:{ex.Message}");
    }
    finally
    {
        _pollingTimer?.Stop();
        _pollingTimer?.Dispose();
        if (_serialPort.IsOpen)
            _serialPort.Close();
    }
}

private static void PollingTimer_Elapsed(object sender, ElapsedEventArgs e)
{
    // 定时器事件在非主线程触发,串口操作必须加锁保证线程安全
    lock (_serialPort)
    {
        try
        {
            byte slaveId = 0x01;
            ushort startAddress = 0x0000;
            ushort numRegisters = 0x01;

            ushort[] registers = _modbusMaster.ReadHoldingRegisters(slaveId, startAddress, numRegisters);
            Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} - 读取到寄存器值:{registers[0]}");
        }
        catch (TimeoutException)
        {
            Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} - 轮询超时");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} - 轮询出错:{ex.Message}");
        }
    }
}

轮询注意事项

  • 线程安全:定时器的Elapsed事件在独立线程执行,串口操作必须加锁,避免多线程冲突。
  • 间隔设置:不要把轮询间隔设得太小,要给设备足够的响应时间,否则容易出现超时或丢帧。
  • 异常处理:一定要捕获超时、串口断开等异常,避免程序崩溃。

内容的提问来源于stack exchange,提问作者Tamerlan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 10:49:07