c#Modbus通信

一、Modbus是什么

Modbus是一种由施耐德电气(原Modicon公司)于1979年发布的开放式工业通信协议,采用主从架构实现设备间简单可靠的数据交换,已成为工业自动化领域事实上的通用标准。其核心价值在于通过标准化的数据模型和轻量级通信机制,解决不同厂商设备间的互操作性问题,广泛应用于PLC、传感器、仪表等工业设备的互联互通。

简单来说,它定义了一套规则,让主站(如 PLC 或电脑)可以与一台或多台从站(如传感器、变频器)交换数据。

常见的协议有 Modbus RTU、 Modbus tcp、 Modbus ASCII(已逐步淘汰)


二、Modbus Rtu(串口)

Modbus RTU是Modbus协议在串行通信(RS-485/RS-232)上的二进制编码实现模式,也就是常说的串口;

2.1 硬件格式 RS-485/RS-232的区别

这两个是硬件的不同,主要区别是双工和使用长度以及主站从站个数3个不同;在代码上是一样的;

特性 RS-232 RS-485
通信模式 全双工 (可同时收发) 半双工 (同一时间只能发或收)
拓扑结构 点对点 (1个主站连1个从站) 总线型 (1个主站最多连32/128/256个从站)
信号与接线 电压信号,至少3根线(TX,RX,GND) 差分信号,2根线(A+/B-),抗干扰强
典型接口 电脑背后的9针COM口,或USB转232 USB转485转换器,或PLC、仪表的485口
C#数据收发控制 直接读写,全自动 必须手动控制方向引脚(收发切换)
通信距离 约15米(低速时最远可达30米左右,受环境影响大) 约1200米(在100kbps以下时;距离与速率成反比)

2.2 串口五要素

这个虽然和通信协议无关但是,是连接串口要设置的东西,所以先简单提一嘴;

2.2.1 波特率 (BaudRate)

主站和从站要同步,相当于要在同一层面;波特率越高通信速率越快但是越不稳定;一般都是9600;

2.2.2 数据位 (DataBits)

就是接收有效数据的位数,多少位算一个完整有效数据;一般是设置8正好8位一字节;7位用于标准ASCII码;

2.2.3 停止位(StopBits)

  • 定义标识数据帧结束的空闲位 ,用于校准时钟同步并分隔连续数据帧。
  • 标准值
    • 1位(最常用,效率最高)
    • 1.5位或2位(高噪声环境或老旧设备,增强同步容错能力)。
  • 关键作用
    • 接收端通过停止位的高电平状态重新对齐时序,补偿双方时钟微小偏差。

2.2.4 校验位(Parity )

  • 定义附加的检错位 ,通过简单逻辑检测传输错误(无法纠正错误)。
  • 常见模式
    • None(无校验):最常用,依赖上层协议纠错(如CRC)。
    • Odd(奇校验) :数据位+校验位中"1"的总数为奇数
    • Even(偶校验) :数据位+校验位中"1"的总数为偶数
  • 局限性
    • 无法检测偶数个比特同时出错(错误相互抵消)。

2.3 使用(重要)

2.3.1 发送报文格式

注意无论是发送还是接受的报文都是16进制的;报文就是主站发给从站的指令,从站收到指令再作出反应;Modbus Rtu的格式:

**地址 + 功能码 +**起始地址 **+**数量/内容 CRC16

例子:01, 03, 00, 00, 00, 0A, C5, CD

字段 长度 说明 示例值 (Hex)
从站地址 1 字节 目标设备的 ID (1-247)这个一般默认01 01 (1号设备)
功能码 1 字节 操作指令 (读/写) 03 (读保持寄存器)
起始地址 2 字节 要操作的寄存器地址 (高位在前) 00 00 (从第0个寄存器开始读)
数量/内容 2 字节 读取的数量 或 写入的值 00 0A (从00开始读10个地址)
CRC16 校验 2 字节 错误检测 (低位在前,高位在后) C5, CD

2.3.2返回报文

返回报文是从站收到主站发送的报文之后带着需要的数据返回给主站;一般拿到了是要解析的;
例子:01, 03, 14, 01,7D, 01, 2C, 00, 00, 00, 00, 01, 2C, 01, 7D, 04, B0, 03, 20, 01, C2, 00, 00, F3, ED

字段 长度 说明 示例值 (Hex)
从站地址 1 字节 回应者的 ID 01
功能码 1 字节 正常则与请求一致;异常则最高位置1 (如 83) 03
字节计数 1 字节 后面数据的字节总数 (数量 × 2) 14 (因为1个寄存器存2字节的数据上面读10个寄存器所以这里返回20个字节,14是16进制);
数据内容 N 字节 具体的数值 (高位在前) **01,7D, 01, 2C, 00, 00, 00, 00, 01, 2C, 01, 7D, 04,**B0, 03, 20, 01, C2, 00, 00,
CRC 16校验 2 字节 校验完整性 F3, ED

返回的数据解析:01 7D是第一个寄存器的值是0x017d,十进制就是381后面的以此类推就可以得到每个寄存器的值;

2.3.3写的报文例子

写入多个寄存器16(16进制就是0x10)

2.3.3 常用功能码

这个Tcp和Rtu差不多

读:

功能码 作用 对应寄存器类型 日常叫法
01 读线圈 离散输出 DO 读开关输出
02 读离散输入 数字输入 DI 读外部按钮 / 传感器
03 读保持寄存器 保持寄存器 最常用,读温度、压力、设定值
04 读输入寄存器 输入寄存器 只读模拟量 AI

写:

功能码 作用 适用场景
05 写单个线圈 控制继电器、开关启停
06 单个寄存器 最常用,设参数、设频率
0x0F 批量写多个线圈 一次性控制多路开关
0x10 批量写多个寄存器 最常用,一次性下发多组参数

2.4 c#的使用

使用这个报文首先要有串口,因为报文只是沟通的语言;依赖于系统的SerialPort 类( 要在NuGet 程序包"。里搜索System.IO.Ports下载)配置串口;在winform里有SerialPort这个控件也可以自己重写定义实现SerialPort 类来创造串口;

剩下的写报文就有两种方法了主要区别就是报文要不要自己写;用库就可以不用算CRC16校验也不用写完整报文;不用库的话就要直接编写完整报文;CRC16一般是直接浏览器在线计算;也可以用方法但是原理比较复杂所以这个方法也是只能网上找;

例子1:winform里自己使用SerialPort 类创建串口;自己编写报文;

cs 复制代码
  private void Form1_Load(object sender, EventArgs e)//窗口加载就创建串口
  {
      sp = new SerialPort();
      sp.PortName = "com1";//串口号
      sp.BaudRate = 9600;//波特率
      sp.DataBits = 8;//数据位
      sp.Parity = Parity.None;//奇偶校验
      sp.StopBits = StopBits.One;//停止位

  }

  private void button1_Click(object sender, EventArgs e)
  {
      sp.Open();//打开串口

      byte[] array = new byte[8];
      array[0] = 0x01;//地址
      array[1] = 0x03;//功能码 读
      array[2] = 0x00;//从地址0x0001开始读;
      array[3] = 0x01;
      array[4] = 0x00;//从地址0x0001往后读1个寄存器;
      array[5] = 0x01;
      array[6] = 0xD5;//校验码是0xCAD5;写报文要高位写在后面所以这里是D5
      array[7] = 0xCA;

      sp.Write(array,0,array.Length);//发送

  }

下面是虚拟串口测试结果

返回:这里直接使用了winform的SerialPort组件

cs 复制代码
  private void Form1_Load(object sender, EventArgs e)
  {
     
      sp.PortName = "com1";//串口号
      sp.BaudRate = 9600;//波特率
      sp.DataBits = 8;//数据位
      sp.Parity = Parity.None;//奇偶校验
      sp.StopBits = StopBits.One;//停止位

  }

  private void button1_Click(object sender, EventArgs e)
  {
      sp.Open();//打开串口

      byte[] array = new byte[8];
      array[0] = 0x01;//地址
      array[1] = 0x03;//功能码 读
      array[2] = 0x00;//从地址0x0001开始读;
      array[3] = 0x01;
      array[4] = 0x00;//从地址0x0001往后读1个寄存器;
      array[5] = 0x01;
      array[6] = 0xD5;//校验码是0xCAD5;写报文要高位写在后面所以这里是D5
      array[7] = 0xCA;

      sp.Write(array,0,array.Length);

  }

  private void serialPort1_DataReceived(object sender, SerialDataReceivedEventArgs e)
  {
      int read = sp.BytesToRead;//返回了多少数据
      byte[] array = new byte[read];//创建对应长度的数组
      sp.Read(array,0,read);//接收
      if(array.Count()>0)
      {
          var xx = sp;
      }
  }

也可以直接使用SerialPort类创造和发送的例子一样就是要自己写接受事件并且在初始化的时候添加;

cs 复制代码
 public SerialPort sp;
 public Form1()
 {
     InitializeComponent();
 }

 private void Form1_Load(object sender, EventArgs e)
 {
     sp = new SerialPort();
     sp.PortName = "com1";//串口号
     sp.BaudRate = 9600;//波特率
     sp.DataBits = 8;//数据位
     sp.Parity = Parity.None;//奇偶校验
     sp.StopBits = StopBits.One;//停止位

     sp.DataReceived += serialPort1_DataReceived;//添加串口的接收事件

 }

 private void button1_Click(object sender, EventArgs e)
 {
     sp.Open();//打开串口

     byte[] array = new byte[8];
     array[0] = 0x01;//地址
     array[1] = 0x03;//功能码 读
     array[2] = 0x00;//从地址0x0001开始读;
     array[3] = 0x01;
     array[4] = 0x00;//从地址0x0001往后读1个寄存器;
     array[5] = 0x01;
     array[6] = 0xD5;//校验码是0xCAD5;写报文要高位写在后面所以这里是D5
     array[7] = 0xCA;

     sp.Write(array,0,array.Length);

 }

 private void serialPort1_DataReceived(object sender, SerialDataReceivedEventArgs e)
 {
     int read = sp.BytesToRead;
     byte[] array = new byte[read];
     sp.Read(array,0,read);
     if(array.Count()>0)
     {
         var xx = array;
     }
 }

三、Modbus Tcp(网口)

Tcp协议也可以使用第三方库或者本地的用于网络通信的Socket 或 TcpClient两个类但是,用起来要注意很多东西所以一般是直接使用第三方库:hsl的或者是NModbus;因为是网络通信所以不管是用什么方法都需要先知道ip地址和端口号建立连接;再发送报文通信;

3.1 TCP的报文

报文分为mbap头 +PDU(功能码+数据 );和RTU相比已经不需要CRC校验了因为有了MBAP头;

MBAP头:

事务标识符:先理解为和返回的要对应相当于这次沟通事件的名字上传的 00 01 那返回的开头是00 01 的就是这个上传的返回 因为返回和上传可能会有很多所有要区分(简单理解上传的这里是多少返回的就得是多少 上传00 02那返回就是00 02开头);

协议标识符:00 00 就是modbus协议的(这个协议就是modbus tcp所有这里可以默认就是写死的)

后面的报文长度:就是数后面多少位数据(不同的功能码数据的长度会不同,比如写的话还要有写几位写入的数据等等)

从站地址:这个一般在要通讯的东西上设置(这次要通讯的是电批控制器就可以看控制器上设置的多少一般一台就是01 )

PDU ;这部分可以理解为modbus rtu的上传数据少一个从站地址因为从站地址已经在前面的MBAP头里有的(这里和rtu的差不多)

读的03

例子:00 01 00 00 00 06 01 03 00 4A 00 01

读寄存器4A的一个长度

返回

例子00 01 00 00 00 05 01 03 02 00 08

3.2 使用

我用的比较多的就是hsl所以下面只有hsl的示例,要是要使用别的第三方库网上搜就好了;hsl的库在TCP通信读写单个线圈都是有单独的方法直接用就可以了,下面的例子是发送报文的格式;(

在用hsl的库来进行tcp通讯的时候高于7.00的库的busTcpClient.ReadFromCoreServer这个方法的发送和接收都只需要输入Modbus核心报文,例如读取寄存器0的字数据 01 03 00 00 00 01,最前面的6个字节是自动添加的,收到的数据也是只有modbus核心报文,例如:01 03 02 00 00 , 所以在解析的时候需要注意。)

cs 复制代码
//创建一个实例 
public static hslModbusTcp ScrewTcp = new hslModbusTcp("192.168.0.10",5000,1);


  string xx = " 01 03 00 46 00 05";
  OperateResult<byte[]> read = busTcpClient.ReadFromCoreServer(HslCommunication.BasicFramework.SoftBasic.HexStringToBytes(xx));
  //Thread.Sleep(500);
  if (read.IsSuccess)
  {
   
      byte[] data = read.Content;
       ProductionMonitoringPage.torque = (float)(data[0] * 256 + data[1])/1000;
      ProductionMonitoringPage.Angle = data[4] * 256 + data[5];
      ProductionMonitoringPage. Circle = (float)ProductionMonitoringPage.Angle / 360;
      int state = data[8] * 256 + data[9];
      ProductionMonitoringPage. strstate = "";
      switch (state)
      {
          case 0:
              {
                  ProductionMonitoringPage.strstate = "运行中";
                  break;
              }
          case 1:
              {
                  ProductionMonitoringPage.strstate = "OK";
                  break;
              }
          case 2:
              {
                  ProductionMonitoringPage.strstate = "滑牙";
                  break;
              }
          case 3:
              {
                  ProductionMonitoringPage.strstate = "浮高";
                  break;
              }
          default:
              {
                  ProductionMonitoringPage.strstate = "NG";
                  break;
              }
      }
      //03C4 0000 0F9D 0000 000C
      
      return true;
      
  }
  else
  {
      return false;
  }

学习时间:

26.05.04


学习产出:

提示:这里统计学习计划的总量

例如:

  • 技术笔记 2 遍
  • CSDN 技术博客 3 篇
  • 习的 vlog 视频 1 个
相关推荐
念何架构之路2 小时前
Go Socket编程
开发语言·后端·golang
feifeigo1232 小时前
基于无迹变换的电网概率潮流分析 MATLAB 实现
开发语言·算法·matlab
时空系2 小时前
第13篇:综合实战——制作我的小游戏 Rust中文编程
开发语言·后端·rust
CoderCodingNo2 小时前
【信奥业余科普】C++ 的奇妙之旅 | 19:内存的门牌号——地址与指针的设计原理
开发语言·c++
@insist1233 小时前
信息安全工程师-物理隔离技术基础核心考点解析
开发语言·网络·安全·软考·信息安全工程师·软件水平考试
空中海3 小时前
02 状态、Hooks、副作用与数据流
开发语言·javascript·ecmascript
Aurorar0rua3 小时前
CS50 x 2024 Notes C - 09
c语言·开发语言·学习方法
兔小盈3 小时前
多线程篇-(二)线程创建、中断与终止
java·开发语言·多线程
hoiii1873 小时前
基于MATLAB实现内点法解决凸优化问题
开发语言·matlab