前言

通过前面的四篇文章,我们已经了解到了ModbusRTU具体是什么,以及如何生成正确的报文,也完成了一个完整实现ModbusRTU的Demo。

前面我们的实现方式,是从零开始一点点自己写,所以我们需要完整实现整个串口通讯以及报文的生成与收发。在实际使用的时候,我们其实不需要写那么多东西,GitHub上其实有很多现成的类库,可以直接导入使用,无需自己生成通讯报文。

由于本专栏并非仅仅介绍实际应用,所以前面花费了不少篇幅来讲解ModbusRTU的报文。如果能完全理解前面四篇文章的内容,那么当我们遇到需要使用常用的八种功能码以外的报文的情况的时候,我们也能举一反三地写出对应的程序,而不必依赖他人的类库。

在实际应用的开发过程中,如果充当主站的应用程序与子站直接的物理连接,并非完全是RS485等的串行通讯连接方式,而是中间使用了串口服务器将串行信号转换为以太网信号,那么则需要我们使用Socket通讯的方式将报文发送出去,所以就不能使用NModbus4这样的类库了。在这种情况下,虽然串行信号被转换成了以太网的信号,但是我们需要收发的报文格式是不变的,所以这时,我们只需要用我们前面写好的报文生成和解析的方法,结合Socket通讯,实现数据的请求与解析。

传送门:

C#实现ModbusRTU详解【一】—— 简介及仿真配置

C#实现ModbusRTU详解【二】—— 生成读取报文

C#实现ModbusRTU详解【三】—— 生成写入报文

C#实现ModbusRTU详解【四】—— 通讯Demo


引入类库

以下以Winform为例。创建一个Winform项目,然后在NuGet中安装NModbus4,如下图所示:


窗体界面

根据自己喜好创建布局,参考如下:


读写方法

首先我们需要引入NModbusRTU。

using Modbus.Device;

创建一个串口对象,还有一个ModbusRTU的实例对象。

/// <summary>
/// 私有串口实例
/// </summary>
private SerialPort serialPort = new SerialPort();

/// <summary>
/// 私有ModbusRTU主站字段
/// </summary>
private static IModbusMaster master;

 根据八种不同的功能,创建读写方法。

/// <summary>
/// 写入单个线圈
/// </summary>
private void WriteSingleCoil()
{
    bool result = false;
    if (rbxRWMsg.Text.Equals("true", StringComparison.OrdinalIgnoreCase) || rbxRWMsg.Text.Equals("1", StringComparison.OrdinalIgnoreCase))
    {
        result = true;
    }
    master.WriteSingleCoil((byte)nudSlaveID.Value, (ushort)nudStartAdr.Value, result);
}

/// <summary>
/// 批量写入线圈
/// </summary>
private void WriteArrayCoil()
{
    List<string> strList = rbxRWMsg.Text.Split(',').ToList();

    List<bool> result = new List<bool>();

    strList.ForEach(m => result.Add(m.Equals("true", StringComparison.OrdinalIgnoreCase) || m.Equals("1", StringComparison.OrdinalIgnoreCase)));

    master.WriteMultipleCoils((byte)nudSlaveID.Value, (ushort)nudStartAdr.Value, result.ToArray());
}

/// <summary>
/// 写入单个寄存器
/// </summary>
private void WriteSingleRegister()
{
    ushort result = Convert.ToUInt16(rbxRWMsg.Text);

    master.WriteSingleRegister((byte)nudSlaveID.Value, (ushort)nudStartAdr.Value, result);
}

/// <summary>
/// 批量写入寄存器
/// </summary>
private void WriteArrayRegister()
{
    List<string> strList = rbxRWMsg.Text.Split(',').ToList();

    List<ushort> result = new List<ushort>();

    strList.ForEach(m => result.Add(Convert.ToUInt16(m)));

    master.WriteMultipleRegisters((byte)nudSlaveID.Value, (ushort)nudStartAdr.Value, result.ToArray());
}

/// <summary>
/// 读取输出线圈
/// </summary>
/// <returns></returns>
private bool[] ReadCoils()
{
    return master.ReadCoils((byte)nudSlaveID.Value, (ushort)nudStartAdr.Value, (ushort)nudLength.Value);
}

/// <summary>
/// 读取输入线圈
/// </summary>
/// <returns></returns>
private bool[] ReadInputs()
{
    return master.ReadInputs((byte)nudSlaveID.Value, (ushort)nudStartAdr.Value, (ushort)nudLength.Value);
}

/// <summary>
/// 读取保持型寄存器
/// </summary>
/// <returns></returns>
private ushort[] ReadHoldingRegisters()
{
    return master.ReadHoldingRegisters((byte)nudSlaveID.Value, (ushort)nudStartAdr.Value, (ushort)nudLength.Value);
}

/// <summary>
/// 读取输入寄存器
/// </summary>
/// <returns></returns>
private ushort[] ReadInputRegisters()
{
    return master.ReadInputRegisters((byte)nudSlaveID.Value, (ushort)nudStartAdr.Value, (ushort)nudLength.Value);
}

创建显示读取数据到窗体的方法。

/// <summary>
/// 界面显示读取结果
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="result"></param>
private void SetMsg<T>(List<T> result)
{
    string msg = string.Empty;

    result.ForEach(m => msg += $"{m} ");

    rbxRWMsg.Text = msg.Trim();
}

 在按钮事件中调用读写方法。

/// <summary>
/// 读写事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnRW_Click(object sender, EventArgs e)
{
    //设定串口参数
    serialPort.PortName = cbxPort.SelectedItem.ToString();
    serialPort.BaudRate = (int)cbxBaudRate.SelectedItem;
    serialPort.Parity = GetSelectedParity();
    serialPort.DataBits = (int)cbxDataBits.SelectedItem;
    serialPort.StopBits = GetSelectedStopBits();

    //创建ModbusRTU主站实例
    master = ModbusSerialMaster.CreateRtu(serialPort);

    //打开串口
    if (!serialPort.IsOpen) serialPort.Open();

    //根据选择的模式进行读写
    switch (cbxMode.SelectedItem.ToString())
    {
        case "读取输出线圈":
            SetMsg(ReadCoils().ToList());
            break;
        case "读取离散输入":
            SetMsg(ReadInputs().ToList());
            break;
        case "读取保持型寄存器":
            SetMsg(ReadHoldingRegisters().ToList());
            break;
        case "读取输入寄存器":
            SetMsg(ReadInputRegisters().ToList());
            break;
        case "写入单个线圈":
            if (rbxRWMsg.Text.Contains(","))
            {
                MessageBox.Show("输入值过多");
                return;
            }
            WriteSingleCoil();
            break;
        case "写入多个线圈":
            WriteArrayCoil();
            break;
        case "写入单个寄存器":
            if (rbxRWMsg.Text.Contains(","))
            {
                MessageBox.Show("输入值过多");
                return;
            }
            WriteSingleRegister();
            break;
        case "写入多个寄存器":
            WriteArrayRegister();
            break;
        default:
            break;
    }

    //关闭串口
    serialPort.Close();
}

读写测试

读取输出线圈

读取输入线圈

读取保持型寄存器

读取输入寄存器

写入单个线圈

写入多个线圈

写入单个寄存器

写入多个寄存器


完整代码

using Modbus.Device;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO.Ports;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace NModbusDemo
{
    public partial class DemoForm : Form
    {
        /// <summary>
        /// 私有串口实例
        /// </summary>
        private SerialPort serialPort = new SerialPort();

        /// <summary>
        /// 私有ModbusRTU主站字段
        /// </summary>
        private static IModbusMaster master;

        /// <summary>
        /// 构造函数
        /// </summary>
        public DemoForm()
        {
            InitializeComponent();            
        }

        /// <summary>
        /// 窗体加载事件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void DemoForm_Load(object sender, EventArgs e)
        {
            //设置可选串口
            cbxPort.Items.AddRange(SerialPort.GetPortNames());
            //设置可选波特率
            cbxBaudRate.Items.AddRange(new object[] { 9600, 19200 });
            //设置可选奇偶校验
            cbxParity.Items.AddRange(new object[] { "None", "Odd", "Even", "Mark", "Space" });
            //设置可选数据位
            cbxDataBits.Items.AddRange(new object[] { 5, 6, 7, 8 });
            //设置可选停止位
            cbxStopBits.Items.AddRange(new object[] { 1, 1.5, 2 });
            //设置读写模式
            cbxMode.Items.AddRange(new object[] {
                "读取输出线圈",
                "读取离散输入",
                "读取保持型寄存器",
                "读取输入寄存器",
                "写入单个线圈",
                "写入多个线圈",
                "写入单个寄存器",
                "写入多个寄存器"
            });

            //设置默认选中项
            cbxPort.SelectedIndex = 1;
            cbxBaudRate.SelectedIndex = 0;
            cbxParity.SelectedIndex = 0;
            cbxDataBits.SelectedIndex = 3;
            cbxStopBits.SelectedIndex = 0;
            cbxMode.SelectedIndex = 0;

            nudLength.Minimum = 1;
            nudSlaveID.Minimum = 1;
            nudStartAdr.Minimum = 0;

            //设置为默认输入法,即为英文半角
            rbxRWMsg.ImeMode = ImeMode.Disable;            
        }

        /// <summary>
        /// 模式切换事件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void cbxMode_SelectedIndexChanged(object sender, EventArgs e)
        {
            rbxRWMsg.Clear();
            if (cbxMode.SelectedItem.ToString().Contains("读取"))
            {
                btnRW.Text = "读取";
                rbxRWMsg.Enabled = false;
                nudLength.Enabled = true;
            }
            else
            {
                btnRW.Text = "写入";
                rbxRWMsg.Enabled = true;
                nudLength.Enabled = false;
            }
        }

        /// <summary>
        /// 读写事件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnRW_Click(object sender, EventArgs e)
        {
            //设定串口参数
            serialPort.PortName = cbxPort.SelectedItem.ToString();
            serialPort.BaudRate = (int)cbxBaudRate.SelectedItem;
            serialPort.Parity = GetSelectedParity();
            serialPort.DataBits = (int)cbxDataBits.SelectedItem;
            serialPort.StopBits = GetSelectedStopBits();

            //创建ModbusRTU主站实例
            master = ModbusSerialMaster.CreateRtu(serialPort);

            //打开串口
            if (!serialPort.IsOpen) serialPort.Open();
            
            //根据选择的模式进行读写
            switch (cbxMode.SelectedItem.ToString())
            {
                case "读取输出线圈":
                    SetMsg(ReadCoils().ToList());
                    break;
                case "读取离散输入":
                    SetMsg(ReadInputs().ToList());
                    break;
                case "读取保持型寄存器":
                    SetMsg(ReadHoldingRegisters().ToList());
                    break;
                case "读取输入寄存器":
                    SetMsg(ReadInputRegisters().ToList());
                    break;
                case "写入单个线圈":
                    if (rbxRWMsg.Text.Contains(","))
                    {
                        MessageBox.Show("输入值过多");
                        serialPort.Close();
                        return;
                    }
                    WriteSingleCoil();
                    break;
                case "写入多个线圈":
                    WriteArrayCoil();
                    break;
                case "写入单个寄存器":
                    if (rbxRWMsg.Text.Contains(","))
                    {
                        MessageBox.Show("输入值过多");
                        serialPort.Close();
                        return;
                    }
                    WriteSingleRegister();
                    break;
                case "写入多个寄存器":
                    WriteArrayRegister();
                    break;
                default:
                    break;
            }

            //关闭串口
            serialPort.Close();
        }

        /// <summary>
        /// 更新写入值计数
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void rbxRWMsg_TextChanged(object sender, EventArgs e)
        {
            nudLength.Value = Regex.Matches(rbxRWMsg.Text, ",").Count + 1;
        }

        /// <summary>
        /// 获取窗体选中的奇偶校验
        /// </summary>
        /// <returns></returns>
        private Parity GetSelectedParity()
        {
            switch (cbxParity.SelectedItem.ToString())
            {
                case "Odd":
                    return Parity.Odd;
                case "Even":
                    return Parity.Even;
                case "Mark":
                    return Parity.Mark;
                case "Space":
                    return Parity.Space;
                case "None":
                default:
                    return Parity.None;
            }
        }

        /// <summary>
        /// 获取窗体选中的停止位
        /// </summary>
        /// <returns></returns>
        private StopBits GetSelectedStopBits()
        {
            switch (Convert.ToDouble(cbxStopBits.SelectedItem))
            {
                case 1:
                    return StopBits.One;
                case 1.5:
                    return StopBits.OnePointFive;
                case 2:
                    return StopBits.Two;
                default:
                    return StopBits.One;
            }
        }

        /// <summary>
        /// 写入单个线圈
        /// </summary>
        private void WriteSingleCoil()
        {
            bool result = false;
            if (rbxRWMsg.Text.Equals("true", StringComparison.OrdinalIgnoreCase) || rbxRWMsg.Text.Equals("1", StringComparison.OrdinalIgnoreCase))
            {
                result = true;
            }
            master.WriteSingleCoil((byte)nudSlaveID.Value, (ushort)nudStartAdr.Value, result);
        }

        /// <summary>
        /// 批量写入线圈
        /// </summary>
        private void WriteArrayCoil()
        {
            List<string> strList = rbxRWMsg.Text.Split(',').ToList();

            List<bool> result = new List<bool>();

            strList.ForEach(m => result.Add(m.Equals("true", StringComparison.OrdinalIgnoreCase) || m.Equals("1", StringComparison.OrdinalIgnoreCase)));

            master.WriteMultipleCoils((byte)nudSlaveID.Value, (ushort)nudStartAdr.Value, result.ToArray());
        }

        /// <summary>
        /// 写入单个寄存器
        /// </summary>
        private void WriteSingleRegister()
        {
            ushort result = Convert.ToUInt16(rbxRWMsg.Text);

            master.WriteSingleRegister((byte)nudSlaveID.Value, (ushort)nudStartAdr.Value, result);
        }

        /// <summary>
        /// 批量写入寄存器
        /// </summary>
        private void WriteArrayRegister()
        {
            List<string> strList = rbxRWMsg.Text.Split(',').ToList();

            List<ushort> result = new List<ushort>();

            strList.ForEach(m => result.Add(Convert.ToUInt16(m)));

            master.WriteMultipleRegisters((byte)nudSlaveID.Value, (ushort)nudStartAdr.Value, result.ToArray());
        }

        /// <summary>
        /// 读取输出线圈
        /// </summary>
        /// <returns></returns>
        private bool[] ReadCoils()
        {
            return master.ReadCoils((byte)nudSlaveID.Value, (ushort)nudStartAdr.Value, (ushort)nudLength.Value);
        }

        /// <summary>
        /// 读取输入线圈
        /// </summary>
        /// <returns></returns>
        private bool[] ReadInputs()
        {
            return master.ReadInputs((byte)nudSlaveID.Value, (ushort)nudStartAdr.Value, (ushort)nudLength.Value);
        }

        /// <summary>
        /// 读取保持型寄存器
        /// </summary>
        /// <returns></returns>
        private ushort[] ReadHoldingRegisters()
        {
            return master.ReadHoldingRegisters((byte)nudSlaveID.Value, (ushort)nudStartAdr.Value, (ushort)nudLength.Value);
        }

        /// <summary>
        /// 读取输入寄存器
        /// </summary>
        /// <returns></returns>
        private ushort[] ReadInputRegisters()
        {
            return master.ReadInputRegisters((byte)nudSlaveID.Value, (ushort)nudStartAdr.Value, (ushort)nudLength.Value);
        }

        /// <summary>
        /// 界面显示读取结果
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="result"></param>
        private void SetMsg<T>(List<T> result)
        {
            string msg = string.Empty;

            result.ForEach(m => msg += $"{m} ");

            rbxRWMsg.Text = msg.Trim();
        }
    }
}

结尾

至此,本专栏需要介绍的所有东西都已经介绍完毕了。

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐