添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
乖乖的生姜  ·  对象存储 - 文档中心·  1 年前    · 

Modbus已经成为工业领域通信协议的业界标准(De facto),并且现在是工业电子设备之间常用的连接方式。所以这也是我们工控领域软件开发的所必懂的通讯协议,我也是初次学习,先贴上我的学习笔记.

一 、协议概述

1、Modbus协议是应用于控制器上的一种通用语言,实现控制器之间,控制器通过网络和其他设备之间的通信,支持传统RS232/RS422/RS485和以太网设备,它已经成为一种通用的工业标准,有了它不同厂商生产的控制设备可以连成工业网络,进行集中控制,此协议定义了一个控制器能认识使用的消息结构。

2、如果按照国际 ISO/OSI 的 7 层网络模型来说,标准 MODBUS 协议定义了通信物理层、链路层及应用层;

物理层:定义了基于 RS232 和 RS485 的异步串行通信规范;

链路层:规定了基于站号识别、主 / 从方式的介质访问控制;

应用层:规定了信息规范(或报文格式)及通信服务功能;

二、协议要点

1、MODBUS 是主 / 从通信协议。主站主动发送报文 , 只有与主站发送报文中呼叫地址相同的从站才向主站发送回答报文。

2、报文以 0 地址发送时为广播模式,无需从站应答,可作为广播报文发送,包括:

①修改线圈状态;

②修改寄存器内容;

③强置多线圈;

④预置多寄存器;

⑤询问诊断;

3、MODBUS 规定了 2 种字符传输模式: ASCII 模式、 RTU (二进制)模式;两种传输模式不能混用;

4、传输错误校验

传输错误校验有奇偶校验、冗余校验检验。

当校验出错时,报文处理停止,从机不再继续通信,不对此报文产生应答;

通信错误一旦发生,报文便被视为不可靠; MODBUS 主机在一定时间过后仍未收到从站应答,即作出“通信错误已发生”的判断。

5、报文级(字符级)采用 CRC-16 (循环冗余错误校验)

6、MODBUS 报文 RTU 格式

三、异常应答

1、从机接收到的主机报文,没有传输错误,但从机无法正确执行主机命令或无法作出正确应答,从机将以“异常应答”回答之。

2、异常应答报文格式

例:主机发请求报文,功能码 01 :读 1 个 04A1 线圈值

由于从机最高线圈地址为 0400 ,则 04A1 超地址上限,从机作出异常应答如下(注意:功能码最高位置 1 ):

3、异常应答码

四、寄存器和功能码

modbus的功能码很多,且不同功能码对应的报文也不一致,后续博客我会借用开源库实现一个modbus master 测试功能码 解析报文,下边我用表格总结一下寄存器,功能码,报文格式

(1)、报文中的所有字节均为16进制

(2)、由上图我们总结出不同的功能码的报文(无论询问报文还是响应报文)前8个字节都是一致的 都是2字节消息号+2字节ModBus标识+2字节长度+1字节站号+1字节功能码 后边根据功能码不同而不同

(3)、报文中,指定线圈通断标志  FF00 置线圈为ON  0000置线圈为OFF

五、具体实现

接下来我们使用 开源库NModbus库 ,来 实现一个Modbus master。

创建工程,从NuGet管理器安装NModbusu

先简单介绍一下NModbus中的几个重要方法

接下来做具体实现代码:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using NModbus;
using System.Net.Sockets;
using System.Threading;
namespace ModbusTcp
    public partial class Form1 : Form
        private static ModbusFactory modbusFactory;
        private static IModbusMaster master;
        //写线圈或写寄存器数组
        bool[] coilsBuffer;
        ushort[] registerBuffer;
        //功能码
        string functionCode;
        //参数(分别为站号,起始地址,长度)
        byte slaveAddress;
        ushort startAddress;
        ushort numberOfPoints;
        public Form1()
            InitializeComponent();
        private void Form1_Load(object sender, EventArgs e)
            //初始化modbusmaster
            modbusFactory = new ModbusFactory();
            //在本地测试 所以使用回环地址,modbus协议规定端口号 502
            master = modbusFactory.CreateMaster(new TcpClient("127.0.0.1", 502));
            //设置读取超时时间
            master.Transport.ReadTimeout = 2000;
            master.Transport.Retries = 2000;
            groupBox1.Enabled = false;
            groupBox2.Enabled = false;
        /// <summary>
        /// 读/写
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void button1_Click(object sender, EventArgs e)
            ExecuteFunction();
        private async void ExecuteFunction()
                //重新实例化是为了 modbus slave更换连接时不报错
                master = modbusFactory.CreateMaster(new TcpClient("127.0.0.1", 502));
                if (functionCode != null)
                    switch (functionCode)
                        case "01 Read Coils"://读取单个线圈
                            SetReadParameters();
                            coilsBuffer = master.ReadCoils(slaveAddress, startAddress, numberOfPoints);
                            for (int i = 0; i < coilsBuffer.Length; i++)
                                SetMsg(coilsBuffer[i] + "");
                            break;
                        case "02 Read DisCrete Inputs"://读取输入线圈/离散量线圈
                            SetReadParameters();
                            coilsBuffer = master.ReadInputs(slaveAddress, startAddress, numberOfPoints);
                            for (int i = 0; i < coilsBuffer.Length; i++)
                                SetMsg(coilsBuffer[i] + "");
                            break;
                        case "03 Read Holding Registers"://读取保持寄存器
                            SetReadParameters();
                            registerBuffer = master.ReadHoldingRegisters(slaveAddress, startAddress, numberOfPoints);
                            for (int i = 0; i < registerBuffer.Length; i++)
                                SetMsg(registerBuffer[i] + "");
                            break;
                        case "04 Read Input Registers"://读取输入寄存器
                            SetReadParameters();
                            registerBuffer = master.ReadInputRegisters(slaveAddress, startAddress, numberOfPoints);
                            for (int i = 0; i < registerBuffer.Length; i++)
                                SetMsg(registerBuffer[i] + "");
                            break;
                        case "05 Write Single Coil"://写单个线圈
                            SetWriteParametes();
                            await master.WriteSingleCoilAsync(slaveAddress, startAddress, coilsBuffer[0]);
                            break;
                        case "06 Write Single Registers"://写单个输入线圈/离散量线圈
                            SetWriteParametes();
                            await master.WriteSingleRegisterAsync(slaveAddress, startAddress, registerBuffer[0]);
                            break;
                        case "0F Write Multiple Coils"://写一组线圈
                            SetWriteParametes();
                            await master.WriteMultipleCoilsAsync(slaveAddress, startAddress, coilsBuffer);
                            break;
                        case "10 Write Multiple Registers"://写一组保持寄存器
                            SetWriteParametes();
                            await master.WriteMultipleRegistersAsync(slaveAddress, startAddress, registerBuffer);
                            break;
                        default:
                            break;
                    MessageBox.Show("请选择功能码!");
               master.Dispose();
            catch (Exception ex)
                MessageBox.Show(ex.Message);
        private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
            if (comboBox1.SelectedIndex >= 4)
                groupBox2.Enabled = true;
                groupBox1.Enabled = false;
                groupBox1.Enabled = true;
                groupBox2.Enabled = false;
            comboBox1.Invoke(new Action(() => { functionCode = comboBox1.SelectedItem.ToString(); }));
        /// <summary>
        /// 初始化读参数
        /// </summary>
        private void SetReadParameters()
            if (txt_startAddr1.Text == "" || txt_slave1.Text == "" || txt_length.Text == "")
                MessageBox.Show("请填写读参数!");
                slaveAddress = byte.Parse(txt_slave1.Text);
                startAddress = ushort.Parse(txt_startAddr1.Text);
                numberOfPoints = ushort.Parse(txt_length.Text);
        /// <summary>
        /// 初始化写参数
        /// </summary>
        private void SetWriteParametes()
            if (txt_startAddr2.Text == "" || txt_slave2.Text == "" || txt_data.Text == "")
                MessageBox.Show("请填写写参数!");
                slaveAddress = byte.Parse(txt_slave2.Text);
                startAddress = ushort.Parse(txt_startAddr2.Text);
                //判断是否写线圈
                if (comboBox1.SelectedIndex == 4 || comboBox1.SelectedIndex == 6)
                    string[] strarr = txt_data.Text.Split(' ');
                    coilsBuffer = new bool[strarr.Length];
                    //转化为bool数组
                    for (int i = 0; i < strarr.Length; i++)
                        // strarr[i] == "0" ? coilsBuffer[i] = true : coilsBuffer[i] = false;
                        if (strarr[i] == "0")
                            coilsBuffer[i] = false;
                            coilsBuffer[i] = true;
                    //转化ushort数组
                    string[] strarr = txt_data.Text.Split(' ');
                    registerBuffer = new ushort[strarr.Length];
                    for (int i = 0; i < strarr.Length; i++)
                        registerBuffer[i] = ushort.Parse(strarr[i]);
        /// <summary>
        /// 清除文本
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void button2_Click(object sender, EventArgs e)
            richTextBox1.Clear();
        /// <summary>
        /// SetMessage
        /// </summary>
        /// <param name="msg"></param>
        public void SetMsg(string msg)
            richTextBox1.Invoke(new Action(() => { richTextBox1.AppendText(msg + "\r\n"); }));

六 、功能测试及报文解析

这里功能测试我们需要借助测试工具  Modbus Slave(Modbus从站客户端)

链接:https://pan.baidu.com/s/1Z3bET3l_2a4e6cu_p250tg    提取码:hq1r

简单说明一下,这里我实现了常用的几个功能码

0x01 读一组线圈

0x02 读一组输入线圈/离散量线圈

0x03 读一组保持寄存器

0x04 读一组输入寄存器

0x05 写单个线圈

0x06 写单个保持寄存器

0x0F 写多个线圈

0x10 写多个保持寄存器

简单说一下Modbus Slave 的操作

打开连接,建立连接,选择连接方式为Tcp/Ip 设置 Ip和端口号

选择线圈或寄存器

点击Setup->Slave Definition,这里的Function我们需要读/写什么线圈或寄存器就对应选择

测试1 功能码0x01

这里我们所有的测试从站都使用站号1 起始地址0 长度10

功能码0x01 读取线圈 Modbus Slave的Function选择01 Coil Status(0x)

测试结果:

点击Display->Communication 可以截取报文,我也不知道为什么他报文字体那么小(绝望ing)

000000-Rx:00 01 00 00 00 06 01 01 00 00 00 05
000001-Tx:00 01 00 00 00 04 01 01 01 06

测试2  功能码0x10

功能码0x10 写入一组数据到保持寄存器  Modbus Slave的Function选择03 Holding Register(4x) (说明一下 线圈和保持寄存器才有写操作)

000070-Rx:00 01 00 00 00 11 01 10 00 00 00 05 0A 00 0C 00 22 00 38 00 4E 00 5A
000071-Tx:00 01 00 00 00 06 01 10 00 00 00 05

上文测试了一个读操作和一个写操作,其他功能码的测试与上文一致,有兴趣的可以自行测试,

下一篇博客我要针对不同的功能码做对应的报文解析

程序源码:

链接:https://pan.baidu.com/s/1mPAhRixLbsDb7h2ePENTRA
提取码:b5w6

以上都为我自己学习总结并实现,有错误之处,希望大家不吝赐教,感谢(抱拳)!

https://www.cnblogs.com/pandefu/p/10824331.html

作者: Peter.Pan

出处: https://www.cnblogs.com/pandefu/>

邮箱: defu_pan@163.com

关于作者:.Net,WindowsForm,工控软件,Modbus,OPC/Tcp/Ip,串口通讯

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出 原文链接,否则保留追究法律责任的权利。 如有问题, 可邮件咨询。

原文链接:https://www.cnblogs.com/pandefu/p/10824331.html 在上述示例中,我们创建一个SerialPort对象,并配置其参数以匹配Modbus主站的通信设置。在上述示例中,我们首先创建一个SerialPort对象,并配置其参数以匹配Modbus从设备的通信设置。最后,我们使用ReadHoldingRegisters方法从从设备的保持寄存器中读取数据,并对读取到的数据进行处理。如果你对Modbus协议的更深入了解感兴趣,可以进一步研究Modus协议的更深入了解感兴趣,可以进一步研究Modbus协议规范和相关资料。选择适合你项目需求的库,并将其添加到你的C#项目中。 因为项目要用到连接传感器的业务需求,所以学习了这几种协议,并记录下来,下面所有的代码,我都使用过,并且能够拿取数据。(下面是我参考网上代码做的学习测试总结) MODBUS-RTU 1、Modbus Slave连接串口 一、简单说一下: (1)、Modbus Slave是模拟传感器,连接串口的,相当于模拟的传感器 (2)、Configure Virtual Serial Port Driver这个软件是,开启串口的软件,可以在电脑开模拟串口 二、Modbus Slave以 Serial... 建议先大致了解c# Socket通讯,之后再学习modbusTCP协议ModbusTCP 协议概念:1996年公司推出基于以太网TCP/IP的Modbus协议。Modbus协议是一项应用层报文传输协议,包括三种报文类型。TCP 协议 知道格式就完事了,主要是格式里面的数据。 Modbus由MODICON公司于1979年开发,是一种工业现场总线协议标准。1996年施耐德公司推出基于以太网TCP/IP的Modbus协议:ModbusTCPModbus协议是一项应用层报文传输协议,包括ASCII、RTU、TCP三种报文类型。标准的Modbus协议物理层接口有RS232、RS422、RS485和以太网接口,采用master/slave方式通信。 ModbusTCP的数据帧可分为两部分:MBAP+PDU。 MBAP为报文头,长度为7字节,组成如下:事务处理标识+协议标识+长度+单元标识 c#引用EasyModbusTcp包于ModbusSlave进行通信,包含01线圈读取,02离散读取和03保持寄存器写入,其他功能可以自行用创建的对象调用相应的方法,这里只展示几种。 资源名:C#ModbusTCP客户端程序源码.zip 资源类型:程序源代码 源码说明: C#ModbusTCP客户端,配合PLC测试OK,绝对可以用ModbusTCP客户端,下载绝对不吃亏,C#源代码! 适合人群:新手及有一定经验的开发人员
 
推荐文章