Modbus RTU 与 Modbus TCP 温湿度采集

前言

本文基于真实工业温湿度变送器 ,从零讲解 Modbus RTU(串口 / RS485)Modbus TCP(以太网) 两种通信方式的区别、报文结构、CRC 校验、数据解析,并提供可直接运行的 .NET4.8 WPF 完整源码,适合工控采集、上位机开发新手快速落地。

不管是 RTU 还是 TCP温湿度、寄存器、数据解析 全都要做 高低字节反转

这是 Modbus 协议死规定,不换就读出来全是错数!


一、设备协议说明(核心)

本案例使用标准 Modbus-RTU 温湿度变送器,协议固定:

  1. 功能码:03(读取保持寄存器)
  2. 寄存器:
    • 温度:0000H(2 字节,有符号,÷10,单位℃)
    • 湿度:0001H(2 字节,无符号,÷10,单位 % RH)
  3. 串口参数:9600 8N1
  4. TCP 端口:默认502

二、Modbus RTU 与 TCP 终极区别(秒懂版)

1. 一句话总结

  • RTU :串口线 / 485 → 开头地址 + 指令 + CRC2 字节
  • TCP :网线 / WiFi → MBAP7 字节头 + 指令 + 无 CRC

2. 报文对比(读温湿度)

✅ RTU 报文

plaintext

复制代码
01 03 00 00 00 02 C4 0B
地址 功能 起始 长度  CRC
✅ TCP 报文

plaintext

复制代码
00 01 00 00 00 06 01 03 00 00 00 02
MBAP7字节头    地址 功能 起始 长度

3. 关键结论(必看)

  1. RTU 必须 CRC 校验 ,TCP 完全不需要 CRC
  2. TCP 多 7 字节 MBAP 头,仅为格式要求,无业务意义
  3. 温度 / 湿度解析代码 100% 通用
  4. RTU 靠01地址识别设备,TCP 靠IP 识别,01仅占位

三、Modbus RTU .NET4.8 WPF 完整源码

1. MainWindow.xaml(界面)

xml

复制代码
<Window x:Class="ModbusDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Modbus RTU 温湿度采集" Height="420" Width="520" WindowStartupLocation="CenterScreen">
    <Grid Margin="20">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <TextBlock Grid.Row="0" Text="串口配置" FontSize="16" FontWeight="Bold" Margin="0 0 0 10"/>
        <StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0 0 0 10">
            <TextBlock Text="串口号:" VerticalAlignment="Center"/>
            <ComboBox x:Name="CmbComPort" Width="100" Margin="5 0"/>
            <TextBlock Text="波特率:" VerticalAlignment="Center" Margin="10 0 0 0"/>
            <ComboBox x:Name="CmbBaudRate" Width="100" Margin="5 0">
                <ComboBoxItem>9600</ComboBoxItem>
            </ComboBox>
            <Button x:Name="BtnOpen" Content="打开串口" Width="85" Margin="10 0 0 0" Click="BtnOpen_Click"/>
            <Button x:Name="BtnClose" Content="关闭串口" Width="85" Margin="5 0 0 0" Click="BtnClose_Click" IsEnabled="False"/>
        </StackPanel>

        <StackPanel Grid.Row="2" Orientation="Horizontal" Margin="0 0 0 10">
            <TextBlock Text="设备地址:" VerticalAlignment="Center"/>
            <TextBox x:Name="TxtDeviceAddr" Text="1" Width="60" Margin="5 0"/>
            <Button x:Name="BtnRead" Content="读取数据" Width="85" Margin="10 0 0 0" Click="BtnRead_Click" IsEnabled="False"/>
            <Button x:Name="BtnAutoRead" Content="自动读取" Width="85" Margin="5 0 0 0" Click="BtnAutoRead_Click"/>
        </StackPanel>

        <Grid Grid.Row="3" Margin="0 10 15 10">
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            <Border BorderBrush="LightGray" BorderThickness="1" Margin="5" Padding="10">
                <StackPanel>
                    <TextBlock Text="温度" HorizontalAlignment="Center"/>
                    <TextBlock x:Name="TxtTemperature" Text="-- ℃" FontSize="24" FontWeight="Bold" Foreground="Red" HorizontalAlignment="Center"/>
                </StackPanel>
            </Border>
            <Border Grid.Column="1" BorderBrush="LightGray" BorderThickness="1" Margin="5" Padding="10">
                <StackPanel>
                    <TextBlock Text="湿度" HorizontalAlignment="Center"/>
                    <TextBlock x:Name="TxtHumidity" Text="-- %RH" FontSize="24" FontWeight="Bold" Foreground="Blue" HorizontalAlignment="Center"/>
                </StackPanel>
            </Border>
        </Grid>

        <GroupBox Grid.Row="4" Header="运行日志">
            <TextBox x:Name="TxtLog" IsReadOnly="True" VerticalScrollBarVisibility="Auto" TextWrapping="Wrap" FontFamily="Consolas"/>
        </GroupBox>

        <StatusBar Grid.Row="5" Margin="0 10 0 0">
            <TextBlock x:Name="TxtStatus" Text="就绪"/>
        </StatusBar>
    </Grid>
</Window>

2. MainWindow.xaml.cs(业务逻辑 + CRC 校验)

csharp

运行

复制代码
using System;
using System.IO.Ports;
using System.Linq;
using System.Windows;
using System.Windows.Threading;

namespace ModbusDemo
{
    public partial class MainWindow : Window
    {
        private SerialPort _serialPort;
        private DispatcherTimer _timer;

        public MainWindow()
        {
            InitializeComponent();
            InitSerial();
            InitTimer();
            LoadPorts();
        }

        private void InitSerial()
        {
            _serialPort = new SerialPort { DataBits = 8, StopBits = StopBits.One, Parity = Parity.None };
        }

        private void InitTimer()
        {
            _timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
            _timer.Tick += (s, e) => Read();
        }

        private void LoadPorts()
        {
            CmbComPort.Items.Clear();
            foreach (var p in SerialPort.GetPortNames()) CmbComPort.Items.Add(p);
            if (CmbComPort.Items.Count > 0) CmbComPort.SelectedIndex = 0;
            CmbBaudRate.SelectedIndex = 0;
        }

        private void BtnOpen_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                _serialPort.PortName = CmbComPort.Text;
                _serialPort.BaudRate = int.Parse(CmbBaudRate.Text);
                _serialPort.Open();
                BtnOpen.IsEnabled = false; BtnClose.IsEnabled = true; BtnRead.IsEnabled = true;
                AddLog("串口打开成功");
            }
            catch (Exception ex) { MessageBox.Show(ex.Message); }
        }

        private void BtnClose_Click(object sender, RoutedEventArgs e)
        {
            _timer.IsEnabled = false; BtnAutoRead.Content = "自动读取";
            _serialPort.Close();
            BtnOpen.IsEnabled = true; BtnClose.IsEnabled = false; BtnRead.IsEnabled = false;
            AddLog("串口已关闭");
        }

        private void BtnRead_Click(object sender, RoutedEventArgs e) => Read();

        private void BtnAutoRead_Click(object sender, RoutedEventArgs e)
        {
            if (!_serialPort.IsOpen) { MessageBox.Show("请先打开串口"); return; }
            _timer.IsEnabled = !_timer.IsEnabled;
            BtnAutoRead.Content = _timer.IsEnabled ? "停止" : "自动读取";
        }

        private void Read()
        {
            try
            {
                byte addr = byte.Parse(TxtDeviceAddr.Text);
                byte[] cmd = { addr, 0x03, 0x00, 0x00, 0x00, 0x02 };
                ushort crc = Crc16Modbus.Compute(cmd);
                byte[] frame = cmd.Concat(new[] { (byte)(crc & 0xFF), (byte)(crc >> 8) }).ToArray();

                _serialPort.DiscardInBuffer();
                _serialPort.Write(frame, 0, frame.Length);
                System.Threading.Thread.Sleep(100);

                int len = _serialPort.BytesToRead;
                if (len == 0) { AddLog("未收到数据"); return; }

                byte[] buf = new byte[len];
                _serialPort.Read(buf, 0, len);

                if (!Crc16Modbus.CheckValid(buf)) { AddLog("CRC校验失败"); return; }

                short tRaw = BitConverter.ToInt16(new[] { buf[4], buf[3] }, 0);
                ushort hRaw = BitConverter.ToUInt16(new[] { buf[6], buf[5] }, 0);

                Dispatcher.Invoke(() =>
                {
                    TxtTemperature.Text = $"{tRaw / 10f:F1} ℃";
                    TxtHumidity.Text = $"{hRaw / 10f:F1} %RH";
                });
                AddLog($"采集成功 → 温度:{tRaw / 10f:F1} ℃ 湿度:{hRaw / 10f:F1} %RH");
            }
            catch (Exception ex) { AddLog($"异常:{ex.Message}"); }
        }

        private void AddLog(string msg)
        {
            Dispatcher.Invoke(() =>
            {
                TxtLog.AppendText($"{DateTime.Now:HH:mm:ss} - {msg}\r\n");
                TxtLog.ScrollToEnd();
            });
        }

        protected override void OnClosed(EventArgs e)
        {
            if (_serialPort.IsOpen) _serialPort.Close();
            base.OnClosed(e);
        }
    }

    public static class Crc16Modbus
    {
        public static ushort Compute(byte[] data)
        {
            ushort crc = 0xFFFF;
            for (int i = 0; i < data.Length; i++)
            {
                crc ^= data[i];
                for (int j = 0; j < 8; j++)
                {
                    if ((crc & 1) != 0) { crc >>= 1; crc ^= 0xA001; }
                    else crc >>= 1;
                }
            }
            return crc;
        }

        public static bool CheckValid(byte[] frame)
        {
            if (frame.Length < 3) return false;
            var data = frame.Take(frame.Length - 2).ToArray();
            ushort calc = Compute(data);
            ushort recv = (ushort)((frame[frame.Length - 1] << 8) | frame[frame.Length - 2]);
            return calc == recv;
        }
    }
}

四、Modbus TCP 简化版报文(直接套用)

plaintext

复制代码
00 01 00 00 00 06 01 03 00 00 00 02
  • 前 7 字节:MBAP 固定头
  • 无 CRC,直接发送即可
  • 数据解析与 RTU完全一致

五、常见问题

  1. 串口黄色感叹号:安装 CH340/PL2303 驱动
  2. CRC 校验失败:干扰、波特率错误、接线松动
  3. TCP 无返回:IP 错误、端口 502 未开放、防火墙拦截
  4. 数据异常:字节序(大端)、÷10 比例未处理

六、总结

  1. RTU = 串口 + CRC + 地址
  2. TCP = 网线 + MBAP 头 + 无 CRC
  3. 温湿度解析完全通用
  4. 源码可直接用于工业采集、上位机开发

补充:

相关推荐
其实防守也摸鱼12 小时前
CTF密码学综合教学指南--第三章
开发语言·网络·python·安全·网络安全·密码学
其实防守也摸鱼12 小时前
CTF密码学综合教学指南--第四章
网络·笔记·安全·网络安全·密码学·ctf
草履虫君13 小时前
VMware 虚拟机网络性能优化指南:从 11 秒到 4 秒的完整调优实践
服务器·网络·经验分享·性能优化
@insist12314 小时前
信息安全-防火墙技术演进全景:从代理NAT 到下一代及专项防火墙
网络·安全·web安全·软考·信息安全工程师·软件水平考试
优化Henry14 小时前
TDD-LTE站点Rilink=3链路故障处理案例---BBU侧C口“有发光、无收光”的排查与恢复
运维·网络·信息与通信·tdd
浪客灿心14 小时前
Linux网络传输层协议
linux·运维·网络
段一凡-华北理工大学15 小时前
【高炉炼铁领域炉温监测、预警、调控智能体设计与应用】~系列文章06:智能决策:从经验驱动到数据驱动
网络·人工智能·数据挖掘·高炉炼铁·工业智能体·高炉炉温
时空系17 小时前
第7篇:功能——打造你的工具箱 Rust中文编程
开发语言·网络·rust
BizViewStudio17 小时前
甄选方法:2026 企业新媒体代运营的短视频精细化运营与流量转化技巧
大数据·网络·人工智能·媒体
凯瑟琳.奥古斯特17 小时前
NAT原理及作用详解
网络·网络协议