摘要

  • 本文描述了如何使用C#实现ModbusTCP通讯功能,使用System.Net.Sockets库自定义实现
  • 【作者】:编程笔记in


前言

  • Modbus TCP 是一种基于 TCP/IP 协议的工业通信协议。废话不多说,本文描述如何使用System.Net.Sockets库实现ModbusTCP通讯,通过Socket对象发送报文请求、接收响应数据实现ModbusTCP数据的读写功能,详细内容下面展开描述。

(一)案例功能

  • 如下案例程序实现了ModbusTCP常用的基本功能

C#实现ModbusTCP主站通讯..._Modbus通信


  • 除了上面的功能外,还编写了简单的界面实现基本的交互功能,如连接、读取、写入、消息显示(显示读写的数据、报文)。
  • 报文的显示是通过触发事件返回,流程如下:

C#实现ModbusTCP主站通讯..._Modbus通信_02

/// <summary>
/// 事件:请求报文
/// </summary>
public event EventHandler<ModbusMessageEvents> RequestMessage;
/// <summary>
/// 事件:响应报文
/// </summary>
public event EventHandler<ModbusMessageEvents> ResponseMessage;
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

(二)基本通讯流程

C#实现ModbusTCP主站通讯..._Modbus通信_03

/// <summary>
/// 构建Modbus读取请求:
/// </summary>
/// <param name="funcCode">功能码</param>
/// <param name="startAddress">起始地址</param>
/// <param name="numberOfPoints">地址个数</param>
/// <param name="unitId">单元ID</param>
/// <returns>请求数组(byte)</returns>
private byte[] BuildReadRequest(byte funcCode, ushort startAddress, ushort numberOfPoints, byte unitId = 1)
{
    // 1、创建基本长度数组
    byte[] request = request = new byte[12];
    // 2、事务标识符
    request[0] = (byte)(_transactionId >> 8);
    request[1] = (byte)_transactionId;
    // 3、协议标识符 (0 for Modbus)
    request[2] = 0;
    request[3] = 0;
    // 4、剩余长度6:(unitId,功能代码,地址,长度)
    request[4] = 0;
    request[5] = 6;
    // 5、单元标识符:
    request[6] = unitId;
    // 6、功能码
    request[7] = funcCode;
    // 7、起始地址
    request[8] = (byte)(startAddress >> 8);
    request[9] = (byte)startAddress;
    // 8、数据长度
    request[10] = (byte)(numberOfPoints >> 8);
    request[11] = (byte)numberOfPoints;
    _transactionId++;
    return request;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.

运行环境

  • 操作系统:Win11
  • 编程软件:Visual Studio 2022
  • .Net版本:.Net Framework 4.8.0

一、预览

(一)运行效果

C#实现ModbusTCP主站通讯..._WinForm_04

二、代码

MainForm 设计

  • 界面如下图,添加一些通讯参数、读写、消息显示控件。读取和写入共用功能码、起始地址。读取时才需要数据长度。

(一)MainForm代码

public partial class MainForm : Form
{
    ModbusTcp modbusTcp;

    #region 界面初始化、加载、初始化参数
    public MainForm()
    {
        InitializeComponent();
        this.CenterToParent();
        rtbx_Message.ForeColor = Color.Gray;
    }
    private void MainForm_Load(object sender, EventArgs e)
    {
        Initialize();
    }
    private void Initialize()
    {
        modbusTcp = new ModbusTcp();
        modbusTcp.RequestMessage += ModbusTcp_RequestMessage;
        modbusTcp.ResponseMessage += ModbusTcp_ResponseMessage;
        //控件
        ControlStateUpdate();
        string[] funcCodes = {  "01_读取线圈","02_读取离散输入",  
                                "03_读取保持寄存器", "04_读取输入寄存器",
                                "05_写单个线圈", "06_写单个寄存器",
                                "15_写多个线圈", "16_写多个寄存器" };
        cbx_FuncCode.DataSource = funcCodes;
    }
    #endregion

    #region 事件方法
    private void btn_Connect_Click(object sender, EventArgs e)
    {
        try
        {
            if (modbusTcp == null) return;
            if (!modbusTcp.IsConnected)
            {
                modbusTcp.Connect(tbx_IPAddress.Text);
                MessageUpdate("连接成功...", Color.Green);
                ControlStateUpdate();
            }
            else
            {
                modbusTcp.Disconnect();
                ControlStateUpdate();
                MessageUpdate("断开连接...", Color.Red);
            }
        }
        catch (Exception ex)
        {
            MessageUpdate(ex.Message, Color.Red);
            modbusTcp.Disconnect();
            ControlStateUpdate();
        }
    }
    private void btn_ReadData_Click(object sender, EventArgs e)
    {
        if (!modbusTcp.IsConnected)
        {
            MessageUpdate("ModbusTCP未连接!!!请连接后再尝试读取数据...",Color.Red);
            return;
        }
        bool[] coils = null;
        ushort[] registers = null;
        switch (modbusTcp.FuncCode)
        {
            case "01":
               coils = modbusTcp.ReadCoils(modbusTcp.DataModel.ReadStartAddress, modbusTcp.DataModel.ReadDataLength);
                MessageUpdate($"[{modbusTcp.DataModel.ReadStartAddress}][{ArrayToString(coils)}]",
                   Color.Blue, $"# 读取 线圈 >");
                break;
            case "02":
                coils = modbusTcp.ReadDiscreteInputs(modbusTcp.DataModel.ReadStartAddress, modbusTcp.DataModel.ReadDataLength);
                MessageUpdate($"[{modbusTcp.DataModel.ReadStartAddress}][{ArrayToString(coils)}]",
                   Color.Blue, $"# 读取 离散输入>");
                
                break;
            case "03":
                registers = modbusTcp.ReadHoldingRegisters(modbusTcp.DataModel.ReadStartAddress, modbusTcp.DataModel.ReadDataLength);
                MessageUpdate($"[{modbusTcp.DataModel.ReadStartAddress}][{ArrayToString(registers)}]",
                    Color.Blue, $"# 读取 保持寄存器 >");
                break;
            case "04":
                registers = modbusTcp.ReadInputRegisters(modbusTcp.DataModel.ReadStartAddress, modbusTcp.DataModel.ReadDataLength);
                MessageUpdate($"[{modbusTcp.DataModel.ReadStartAddress}][{ArrayToString(registers)}]",
                    Color.Blue, $"# 读取 输入寄存器 >");
                break;
        }
    }
    private void btn_SendData_Click(object sender, EventArgs e)
    {
        byte[] responseData = null;
        switch (modbusTcp.FuncCode)
        {
            case "05":
                string coil = rtbx_SendData.Text.Trim().ToLower();
                if (coil.Equals("true")|| coil.Equals("false"))
                {
                    responseData = modbusTcp.WriteSingleCoil(modbusTcp.DataModel.ReadStartAddress, coil.Equals("true")? true:false);
                    MessageUpdate($"{coil}", Color.Green, $"# 写入 单个线圈 >");
                }
                else
                    MessageUpdate($"请输入有效数值类型...如:true 或 false", Color.Red, $"# 写入数据 >");
                break;
            case "06":
                string register = rtbx_SendData.Text.Trim().ToLower();
                if (ushort.TryParse(register, out ushort result))
                {
                    responseData = modbusTcp.WriteSingleRegister(modbusTcp.DataModel.ReadStartAddress, result);
                    MessageUpdate($"{register}", Color.Green, $"# 写入 单个寄存器 >");
                }
                else
                    MessageUpdate($"请输入有效数值类型...如:0,1,2,3...", Color.Red, $"# 写入数据 >");
                break;
            case "15":
                responseData = modbusTcp.WriteMultipleCoils(modbusTcp.DataModel.ReadStartAddress, ParseArray<bool>(rtbx_SendData.Text));
                MessageUpdate($"{ArrayToString(ParseArray<bool>(rtbx_SendData.Text))}", Color.Green, $"# 写入 多个线圈 >");
                break;
            case "16":
                responseData = modbusTcp.WriteMultipleRegisters(modbusTcp.DataModel.ReadStartAddress, ParseArray<ushort>(rtbx_SendData.Text));
                MessageUpdate($"{ArrayToString(ParseArray<ushort>(rtbx_SendData.Text))}", Color.Green, $"# 写入 寄存器 >");
                break;
        }
    }
    private void btn_ClearMessage_Click(object sender, EventArgs e)
    {
        rtbx_Message.Clear();
    }
    private void btn_ClearSendData_Click(object sender, EventArgs e)
    {
        rtbx_SendData.Clear();
    }
    private void rtbx_Message_TextChanged(object sender, EventArgs e)
    {
        label_RecvLine.Text = $"row:{rtbx_Message.Lines.Length}";
    }
    /// <summary>
    /// 响应报文
    /// </summary>
    private void ModbusTcp_ResponseMessage(object sender, ModbusMessageEvents args)
    {
        if (checkBox_PrintResponseMessage.Checked)
        {
            ModbusMessageEvents message = args as ModbusMessageEvents;
            MessageUpdate($"{ArrayToHex<byte>(message.Message)}", Color.Blue, $"# 接收:响应报文 >");
        }
    }
    /// <summary>
    /// 请求报文
    /// </summary>
    private void ModbusTcp_RequestMessage(object