前一部分我们写好了一个 Modbus RTU 通信库,但那个库是”没有脸”的——你只能在代码里写死参数去调用,看不到也摸不着。这一部分,我们给它配上一张”脸”:做一个图形界面软件,让你在窗口里点按钮、看数据,真正体验”上位机”是什么感觉。

这一部分用的是 WinForm(而不是 WPF)。WinForm 的特点是”拖控件 + 写代码”,不需要写 XAML,非常适合刚入门的你。我们一边拖控件设计界面,一边写事件代码,把这个测试软件一点点做出来。

  1. 先新建一个 WinForm 项目;
  2. 把上一部分的通信库加进来(直接复用,不用重写);
  3. 拖控件设计界面(4.1);
  4. 写”参数初始化 + 串口连接”代码(4.2);
  5. 逐个功能写测试代码(4.3 到 4.9);
  6. 用 Modbus Slave 模拟从站,跑起来看效果。
  • Visual Studio(推荐 2022);
  • 已经会一点第 3 部分的内容(知道怎么开串口、怎么调读写方法);
  • 最好装一个 Modbus Slave 软件,用来模拟从站,方便测试。

准备工作:新建 WinForm 项目

第一步:用 Visual Studio 新建项目

  1. 打开 Visual Studio,点击”创建新项目”;
  2. 搜索”窗体”,选择”Windows 窗体应用“(语言选 C#,框架选 .NET 8 或更高);
  3. 项目名称填 ModbusRTUTest,位置选你找得到的地方,点”创建”。

创建完成后,你会看到设计器里有一个空白窗体(默认叫 Form1),这就是我们软件的主窗口。

第二步:安装串口依赖包

和第 3 部分一样,需要装 System.IO.Ports

  1. 右键项目名 ModbusRTUTest
  2. 选择”管理 NuGet 程序包”;
  3. 浏览搜 System.IO.Ports,安装。

第三步:把通信库加进来

第 3 部分我们已经写好了三个库文件(SerialBase.cs、IModbusRW.cs、ModbusRTU.cs),现在直接复用,不用重写:

  1. 右键项目名 ModbusRTUTest
  2. 选择”添加”,然后”现有项“;
  3. 找到第 3 部分项目文件夹里的这三个文件,选中加进来。

你的项目里就有了开串口、读线圈、读寄存器等全部能力。接下来我们只需要专心做界面,用这些能力就行。

第四步:认识 WinForm 的三个窗口

左边是”工具箱”,里面装满了各种控件(按钮、文本框、下拉框等),我们要拖的就是它们。

中间是”窗体设计器”,显示窗体的样子,拖进去的控件会实时显示。

右边是”属性”窗口,选中某个控件后,在这里改它的名字、显示文字、大小等。


4.1 Modbus RTU 软件 UI 界面设计

这一节要做什么

设计软件的界面。我们的界面分三块:顶部”串口连接”、中间”功能测试”(7 个标签页)、底部”日志”。

整体布局

区域 控件 作用
顶部 GroupBox(串口连接) 放串口号、波特率等参数,以及打开/关闭按钮
中间 TabControl(7 个标签页) 每个功能一个页,互不干扰
底部 GroupBox(日志) 显示操作记录

认识我们要用的几种控件

Label:标签,就是一行说明文字,比如”串口号”。

ComboBox:下拉框,点一下能选一个选项,比如选波特率。

TextBox:文本框,用来输入内容,比如输入从站地址、寄存器地址。

Button:按钮,点一下触发一个动作,比如”打开串口”、”读取”。

GroupBox:分组框,一个带标题的框,把一组相关控件框起来。

TabControl:标签页容器,顶部有一排标签,点不同标签切换不同页面。

手把手:拖控件、设属性

下面以”串口连接区”为例,演示完整的拖控件流程。其它区域的做法完全一样,照着类推即可。

第一步:拖一个 GroupBox。

在工具箱里找到 GroupBox(在”容器”分类下),按住鼠标左键把它拖到窗体上,松开。它的默认标题是 groupBox1,我们在属性窗口把它的 Text 改成”串口连接”。

第二步:拖 Label 和 ComboBox。

  • 拖一个 Label 到 GroupBox 里,属性 Text 改成”串口号”,Name 改成 lblPort
  • 拖一个 ComboBox 到它旁边,Name 改成 cboPort

第三步:照这个套路拖完其它参数。

依次拖出:波特率、校验位、数据位、停止位、从站地址,一共 5 组 Label 加输入控件。它们的 Name 建议按下面的清单来,这样后面写代码时一眼就知道是谁。

第四步:拖两个按钮。

拖两个 Button,Text 分别改成”打开串口”和”关闭串口”,Name 改成 btnOpenbtnClose

  • Name:控件的”内部代号”,代码里用它来操作这个控件,比如 txtSlave.Text。这个必须自己起一个好记的名字。
  • Text:控件”显示出来的文字”,是给用户看的,比如”打开串口”。

新手最容易混淆这两个。记住:Name 是给代码用的,Text 是给人看的。

完整控件清单

下面是整个软件用到的所有控件,你可以照着这个清单一个个拖出来,并设置好 Name(Text 也照填)。这是本部分最重要的一张表,建议边拖边对照。

串口连接区(GroupBox:grpConnect)

控件类型 Name Text(默认显示) 用途
Label lblPort 串口号 说明文字
ComboBox cboPort 选串口号
Label lblBaud 波特率 说明文字
ComboBox cboBaud 选波特率
Label lblParity 校验位 说明文字
ComboBox cboParity 选校验位
Label lblDataBits 数据位 说明文字
ComboBox cboDataBits 选数据位
Label lblStopBits 停止位 说明文字
ComboBox cboStopBits 选停止位
Label lblSlave 从站地址 说明文字
TextBox txtSlave 1 填从站地址
Button btnOpen 打开串口 打开串口
Button btnClose 关闭串口 关闭串口

功能测试区(TabControl:tabMain,含 7 个 TabPage)

标签页 页内控件 Name Text 用途
读线圈 Label lblReadCoilAddr 起始地址 说明
读线圈 TextBox txtReadCoilAddr 0 起始地址
读线圈 Label lblReadCoilCount 数量 说明
读线圈 TextBox txtReadCoilCount 8 数量
读线圈 Button btnReadCoil 读取 执行读取
读线圈 TextBox txtReadCoilResult 显示结果(多行)
读寄存器 Label / TextBox / Button lblReadRegAddr / txtReadRegAddr / lblReadRegCount / txtReadRegCount / btnReadReg / txtReadRegResult 同读线圈 读寄存器
写单线圈 Label / TextBox lblWriteCoilAddr / txtWriteCoilAddr 线圈地址 地址
写单线圈 Label / ComboBox lblWriteCoilValue / cboWriteCoilValue 选开/关
写单线圈 Button btnWriteCoil 写入 执行写入
写多线圈 Label / TextBox lblWriteCoilsAddr / txtWriteCoilsAddr 起始地址 地址
写多线圈 Label / TextBox lblWriteCoilsValues / txtWriteCoilsValues 值(逗号分隔)
写多线圈 Button btnWriteCoils 写入 执行写入
写单寄存器 Label / TextBox / Button lblWriteRegAddr / txtWriteRegAddr / lblWriteRegValue / txtWriteRegValue / btnWriteReg 寄存器地址 / 值 / 写入 写单寄存器
写多寄存器 Label / TextBox / Button lblWriteRegsAddr / txtWriteRegsAddr / lblWriteRegsValues / txtWriteRegsValues / btnWriteRegs 起始地址 / 值(逗号分隔) / 写入 写多寄存器
写寄存器位 Label / TextBox lblWriteBitAddr / txtWriteBitAddr 寄存器地址 地址
写寄存器位 Label / TextBox lblWriteBitIndex / txtWriteBitIndex 位号(0-15) 第几位
写寄存器位 Label / ComboBox lblWriteBitValue / cboWriteBitValue 选开/关
写寄存器位 Button btnWriteBit 写入 执行写入

日志区(GroupBox:grpLog)

控件类型 Name Text 用途
TextBox txtLog 显示日志(多行、只读)

读线圈、读寄存器的结果框,以及日志框,都要设成”多行只读”:选中控件,在属性窗口里把 Multiline 设为 True、ReadOnly 设为 True、ScrollBars 设为 Vertical。这样它们只显示内容,不能被用户乱改,内容多了还能滚动。

TabControl 默认没有标签页,需要点它右上角的小三角,选”添加选项卡”来添加 TabPage,并把每个 TabPage 的 Text 改成”读线圈””读寄存器”等。标签页里的控件要拖到对应的页里,而不是拖到窗体空白处。

如果你觉得拖控件太繁琐,我也准备了一份”参考答案”:把整份界面代码(Form1.Designer.cs)放在了项目里,你直接看它就知道每个控件长什么样、摆在哪里。但强烈建议你先自己拖一遍,拖控件是 WinForm 的基本功。


界面:

image-20260829143903489


4.2 通信参数初始化及串口连接

界面搭好了,现在开始写代码。这一节做两件事:一是给界面上的下拉框填好选项和默认值(参数初始化),二是实现”打开/关闭串口”。

先引入库和通信对象

打开 Form1.cs,在文件顶部加一句 using ModbusRTULib;,然后在类里声明一个通信对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System;
using System.IO.Ports;
using System.Windows.Forms;
using ModbusRTULib; // 引入我们的通信库

namespace ModbusRTUTest;

public partial class Form1 : Form
{
// 通信对象,后面所有操作都靠它
private ModbusRTU modbus = new ModbusRTU();

public Form1()
{
InitializeComponent();
InitControls(); // 初始化参数
}
}

private ModbusRTU modbus = new ModbusRTU(); 是造一个通信对象。以后所有读写,都是 modbus.某个方法(...) 这样调用。这个对象是整个软件的”发动机”。

写参数初始化方法 InitControls

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
private void InitControls()
{
// 波特率选项
cboBaud.Items.AddRange(new object[] { "9600", "19200", "38400", "57600", "115200" });
cboBaud.SelectedIndex = 0; // 默认选第一项 9600

// 校验位选项
cboParity.Items.AddRange(new object[] { "None", "Odd", "Even" });
cboParity.SelectedIndex = 0;

// 数据位选项
cboDataBits.Items.AddRange(new object[] { "8", "7" });
cboDataBits.SelectedIndex = 0;

// 停止位选项
cboStopBits.Items.AddRange(new object[] { "1", "2" });
cboStopBits.SelectedIndex = 0;

// 串口号:动态获取当前电脑有哪些串口
cboPort.Items.Clear();
string[] ports = SerialPort.GetPortNames();
if (ports.Length > 0)
{
cboPort.Items.AddRange(ports);
cboPort.SelectedIndex = 0;
}

// 写单线圈 / 写寄存器位的"值"下拉框
cboWriteCoilValue.Items.AddRange(new object[] { "开", "关" });
cboWriteCoilValue.SelectedIndex = 0;
cboWriteBitValue.Items.AddRange(new object[] { "开", "关" });
cboWriteBitValue.SelectedIndex = 0;
}

逐段讲解:

  • cboBaud.Items.AddRange(...):给下拉框”塞”进去几个选项。Items 是下拉框的选项集合,AddRange 是一次性加一串。
  • cboBaud.SelectedIndex = 0:默认选中第 0 项(第一项)。
  • SerialPort.GetPortNames():这是系统接口,能自动查出电脑上现在有哪些串口,比我们写死 COM1、COM2 更聪明。查出来填进 cboPort,用户直接选就行。

串口号不是固定的,插不同的 USB 转串口线,串口号会变。用 GetPortNames() 自动列出来,用户就不会选错。这是上位机软件的常见做法。

写”打开串口”和”关闭串口”

回到设计器,双击”打开串口”按钮,Visual Studio 会自动生成一个事件方法 btnOpen_Click。在里面写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void btnOpen_Click(object sender, EventArgs e)
{
try
{
// 从下拉框读参数
string port = cboPort.Text;
int baud = Convert.ToInt32(cboBaud.Text);
Parity parity = (Parity)Enum.Parse(typeof(Parity), cboParity.Text);
int dataBits = Convert.ToInt32(cboDataBits.Text);
StopBits stopBits = (StopBits)Enum.Parse(typeof(StopBits), cboStopBits.Text);

// 调用库打开串口
modbus.Open(port, baud, parity, dataBits, stopBits);

Log("串口 " + port + " 打开成功");
}
catch (Exception ex)
{
MessageBox.Show("打开失败:" + ex.Message);
}
}

逐段讲解:

  • cboPort.Text:取下拉框当前选中的文字(就是串口号)。
  • Convert.ToInt32(cboBaud.Text):把”9600”这样的文字转成数字 9600。
  • (Parity)Enum.Parse(typeof(Parity), cboParity.Text):把”None”这样的文字转成枚举值 Parity.None。Enum.Parse 是”把文字转成枚举”,前面加 (Parity) 是类型转换。
  • modbus.Open(...):调用我们库里的开串口方法。
  • try catch:万一出错(比如串口被占用),不崩溃,而是弹窗提示。

“关闭串口”按钮同理,双击它写:

1
2
3
4
5
6
7
8
9
10
11
12
private void btnClose_Click(object sender, EventArgs e)
{
try
{
modbus.Close();
Log("串口已关闭");
}
catch (Exception ex)
{
MessageBox.Show("关闭失败:" + ex.Message);
}
}

写一个日志辅助方法

后面每个功能都要往日志框写一行字,所以先写一个 Log 方法复用:

1
2
3
4
private void Log(string msg)
{
txtLog.AppendText(msg + "\r\n");
}

AppendText 是”在文本框末尾追加文字”,\r\n 是换行。

写一个取从站地址的辅助方法

每个功能都要读”从站地址”输入框,并且要检查它合不合法(1 到 247)。写一个公共方法,避免每个按钮里重复写:

1
2
3
4
5
6
7
8
9
10
private bool GetSlaveAddr(out byte slaveAddr)
{
slaveAddr = 0;
if (!byte.TryParse(txtSlave.Text.Trim(), out slaveAddr) || slaveAddr < 1 || slaveAddr > 247)
{
MessageBox.Show("从站地址不对,请填 1 到 247 之间的数字");
return false;
}
return true;
}

逐段讲解:

  • byte.TryParse(文字, out slaveAddr):尝试把文字转成 byte,成功就 true,并把结果放进 slaveAddr;失败就 false。
  • out:表示”这个方法会把结果传出来”。这是 C# 里返回多个值的一种方式。
  • 后面判断范围 1 到 247,不合法就弹窗并返回 false。

Log 和 GetSlaveAddr 都是”会被反复用到的小工具”。把它们单独写成方法,后面 7 个功能测试里直接调用,代码就干净多了,不用复制粘贴几十遍。


4.3 读取输入输出线圈方法测试

这一节要做什么

在”读线圈”标签页实现:用户填好起始地址和数量,点”读取”,就把线圈状态读出来显示在结果框里。

完整代码

双击”读线圈”页里的”读取”按钮,写:

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
private void btnReadCoil_Click(object sender, EventArgs e)
{
try
{
// 1. 取从站地址,不合法就退出
if (!GetSlaveAddr(out byte slave)) return;

// 2. 取起始地址和数量
ushort addr = Convert.ToUInt16(txtReadCoilAddr.Text.Trim());
ushort count = Convert.ToUInt16(txtReadCoilCount.Text.Trim());

// 3. 调用库读取输出线圈
bool[] result = modbus.ReadOutputCoil(slave, addr, count);

// 4. 把结果拼成 0/1,每 8 个换一行,方便看
string text = "";
for (int i = 0; i < result.Length; i++)
{
text += result[i] ? "1" : "0";
if ((i + 1) % 8 == 0) text += "\r\n";
}
txtReadCoilResult.Text = text;

// 5. 记日志
Log("读线圈成功:从站 " + slave + ",地址 " + addr + ",数量 " + count);
}
catch (Exception ex)
{
MessageBox.Show("读取失败:" + ex.Message);
}
}

逐段讲解

  • Convert.ToUInt16(...):把文字转成 ushort 类型(16 位无符号整数)。
  • modbus.ReadOutputCoil(slave, addr, count):调用库方法,返回一个 bool 数组,每个元素是”开(true)或关(false)”。
  • 显示时把 true 拼成 “1”、false 拼成 “0”,每 8 个换一行,看起来清楚。(i + 1) % 8 == 0 表示”每满 8 个就换行”。

ReadOutputCoil 对应功能码 01(读输出线圈)。如果你想测输入线圈(功能码 02),把方法换成 modbus.ReadInputCoil(slave, addr, count) 就行,其它一模一样。我们界面上这个按钮默认测输出线圈,方便演示。


4.4 读取输入输出寄存器方法测试

这一节要做什么

在”读寄存器”页,读保持寄存器的值,显示在结果框。

完整代码

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
private void btnReadReg_Click(object sender, EventArgs e)
{
try
{
if (!GetSlaveAddr(out byte slave)) return;
ushort addr = Convert.ToUInt16(txtReadRegAddr.Text.Trim());
ushort count = Convert.ToUInt16(txtReadRegCount.Text.Trim());

// 调用库读取保持寄存器
ushort[] result = modbus.ReadOutputRegister(slave, addr, count);

// 逐行显示每个寄存器的值
string text = "";
for (int i = 0; i < result.Length; i++)
{
text += "寄存器[" + (addr + i) + "] = " + result[i] + "\r\n";
}
txtReadRegResult.Text = text;

Log("读寄存器成功:从站 " + slave + ",地址 " + addr + ",数量 " + count);
}
catch (Exception ex)
{
MessageBox.Show("读取失败:" + ex.Message);
}
}

逐段讲解

和读线圈几乎一样,区别在于:

  • 方法换成 ReadOutputRegister(对应功能码 03);
  • 返回的是 ushort[](一队数字);
  • 显示时把每个寄存器连地址一起打印,比如”寄存器[5] = 100”,更直观。

想测输入寄存器(功能码 04),换成 modbus.ReadInputRegister(slave, addr, count)


4.5 写入单输出线圈方法测试

这一节要做什么

在”写单线圈”页,把某个线圈打开或关闭。

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void btnWriteCoil_Click(object sender, EventArgs e)
{
try
{
if (!GetSlaveAddr(out byte slave)) return;
ushort addr = Convert.ToUInt16(txtWriteCoilAddr.Text.Trim());
bool value = cboWriteCoilValue.Text == "开"; // 下拉框选"开"就是 true

// 调用库写单个线圈
modbus.WriteSingleCoil(slave, addr, value);

Log("写单线圈成功:从站 " + slave + ",地址 " + addr + ",值 " + (value ? "开" : "关"));
}
catch (Exception ex)
{
MessageBox.Show("写入失败:" + ex.Message);
}
}

逐段讲解

  • cboWriteCoilValue.Text == "开":下拉框选的是”开”还是”关”,转成 bool(开是 true,关是 false)。
  • modbus.WriteSingleCoil(slave, addr, value):调用库方法写入。
  • 写操作没有返回值,所以成功后写一行日志即可。

4.6 写入多输出线圈方法测试

这一节要做什么

在”写多线圈”页,一次性写一串线圈。值用逗号分隔的 0/1 输入,比如”1,0,1,0,1,0,1,0”。

完整代码

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
private void btnWriteCoils_Click(object sender, EventArgs e)
{
try
{
if (!GetSlaveAddr(out byte slave)) return;
ushort addr = Convert.ToUInt16(txtWriteCoilsAddr.Text.Trim());

// 把 "1,0,1,0" 这样的字符串拆成 bool 数组
string[] parts = txtWriteCoilsValues.Text.Split(',');
bool[] values = new bool[parts.Length];
for (int i = 0; i < parts.Length; i++)
{
values[i] = parts[i].Trim() == "1";
}

// 调用库写多个线圈
modbus.WriteMultiCoils(slave, addr, values);

Log("写多线圈成功:从站 " + slave + ",地址 " + addr + ",数量 " + values.Length);
}
catch (Exception ex)
{
MessageBox.Show("写入失败:" + ex.Message);
}
}

逐段讲解

这段唯一的新东西是”把字符串拆成数组”:

  • txtWriteCoilsValues.Text.Split(','):按逗号把字符串切开,得到一串小字符串,比如”1,0,1”切成”1”、”0”、”1”。
  • 循环里 parts[i].Trim() == "1":去掉空格后判断是不是”1”,是就是 true,否则 false。

Split 是”拆分”的意思,是处理”逗号分隔”这类输入的常用方法。写多寄存器那里还会用到它。


4.7 写入单个寄存器方法测试

这一节要做什么

在”写单寄存器”页,把某个寄存器的值改成指定的数字。

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void btnWriteReg_Click(object sender, EventArgs e)
{
try
{
if (!GetSlaveAddr(out byte slave)) return;
ushort addr = Convert.ToUInt16(txtWriteRegAddr.Text.Trim());
ushort value = Convert.ToUInt16(txtWriteRegValue.Text.Trim());

// 调用库写单个寄存器
modbus.WriteSingleRegister(slave, addr, value);

Log("写单寄存器成功:从站 " + slave + ",地址 " + addr + ",值 " + value);
}
catch (Exception ex)
{
MessageBox.Show("写入失败:" + ex.Message);
}
}

逐段讲解

和写单线圈几乎一样,区别在于:写入的值是一个数字(ushort),直接从文本框转过来即可,不需要”开/关”下拉框。


4.8 写入多个寄存器方法测试

这一节要做什么

在”写多寄存器”页,一次性写多个寄存器。值用逗号分隔的数字输入,比如”100,200,300”。

完整代码

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
private void btnWriteRegs_Click(object sender, EventArgs e)
{
try
{
if (!GetSlaveAddr(out byte slave)) return;
ushort addr = Convert.ToUInt16(txtWriteRegsAddr.Text.Trim());

// 把 "100,200,300" 这样的字符串拆成 ushort 数组
string[] parts = txtWriteRegsValues.Text.Split(',');
ushort[] values = new ushort[parts.Length];
for (int i = 0; i < parts.Length; i++)
{
values[i] = Convert.ToUInt16(parts[i].Trim());
}

// 调用库写多个寄存器
modbus.WriteMultiRegisters(slave, addr, values);

Log("写多寄存器成功:从站 " + slave + ",地址 " + addr + ",数量 " + values.Length);
}
catch (Exception ex)
{
MessageBox.Show("写入失败:" + ex.Message);
}
}

逐段讲解

和写多线圈结构一样,区别是:每个小字符串 parts[i]Convert.ToUInt16 转成数字(而不是判断是否等于”1”)。


4.9 写入寄存器位地址方法测试

这一节要做什么

在”写寄存器位”页,只修改某个寄存器的某一位(0 到 15),其它位保持不变。

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void btnWriteBit_Click(object sender, EventArgs e)
{
try
{
if (!GetSlaveAddr(out byte slave)) return;
ushort addr = Convert.ToUInt16(txtWriteBitAddr.Text.Trim());
int bitIndex = Convert.ToInt32(txtWriteBitIndex.Text.Trim());
bool value = cboWriteBitValue.Text == "开";

// 调用库写寄存器某一位(内部是"读-改-写")
modbus.WriteRegisterBit(slave, addr, bitIndex, value);

Log("写寄存器位成功:从站 " + slave + ",地址 " + addr + ",第 " + bitIndex + " 位 = " + (value ? "1" : "0"));
}
catch (Exception ex)
{
MessageBox.Show("写入失败:" + ex.Message);
}
}

逐段讲解

  • 输入有三样:寄存器地址、位号(0 到 15)、值(开/关)。
  • modbus.WriteRegisterBit(slave, addr, bitIndex, value):调用库方法。还记得第 3 部分吗?这个方法内部是”先读、再改位、再写回”,所以我们界面上只管传”改哪一位、改成什么”,剩下的它自己搞定。

到这里,读线圈、读寄存器、写单线圈、写多线圈、写单寄存器、写多寄存器、写寄存器位,7 个功能测试全部写完。整个测试软件就成型了。


运行测试:用 Modbus Slave 模拟从站

软件写好了,但”读”需要有设备回应才有数据。手头没有真实 PLC 也没关系,用 Modbus Slave 软件模拟一个从站即可。

测试步骤

  1. 打开 Modbus Slave 软件,建立一个从站(比如站号 1,功能码 03 读保持寄存器);
  2. 在 Modbus Slave 里设置几个寄存器,填上一些值(比如地址 0 填 100,地址 1 填 200);
  3. 如果用串口,先用 VSPD 虚拟一对串口(比如 COM3 和 COM4),Modbus Slave 连 COM4,我们的软件连 COM3;
  4. 回到我们自己的软件,选好串口和参数,点”打开串口”;
  5. 切到”读寄存器”页,从站地址填 1,起始地址 0,数量 2,点”读取”,结果框里应该出现 100 和 200;
  6. 再切到”写单寄存器”页,把地址 0 的值改成 300,点”写入”,然后回到 Modbus Slave 看,那个值已经变成 300 了。

当你在自己写的软件里点一下”读取”,结果框里蹦出真实的寄存器数值时,你就完整走通了”上位机软件”的全过程:界面、串口、协议、数据,全都打通了。


第四部分小结

这一部分我们把通信库真正用了起来,做出了一个有界面的 Modbus RTU 测试软件。

  1. 怎么新建 WinForm 项目,怎么拖控件、设属性;
  2. Name(给代码用)和 Text(给人看)的区别;
  3. 用 ComboBox 做参数选择,用 SerialPort.GetPortNames 动态获取串口;
  4. 用 try catch 处理异常,避免程序崩溃;
  5. 把界面输入(文字)转成程序需要的数据类型(数字、bool、数组);
  6. 把上一部分的库方法接上按钮事件,做出完整功能。

我能新建一个 WinForm 项目并装好 System.IO.Ports

我能把上一部分的 3 个库文件加进新项目

我能拖控件并设置 Name 和 Text

我理解 InitControls 是干嘛的

我能解释 try catch 的作用

我照着写完了 7 个功能的按钮事件

我能用 Modbus Slave 模拟从站并读到数据

这一部分我们做的是”测试软件”,界面还比较朴素,输入要靠手动填。下一部分(或后续阶段),我们会做一个更实用的”串口助手”或”数据看板”,加入定时自动读取、数据显示、甚至曲线图表,让上位机软件真正”用起来”。有任何一步卡住,把报错发给我,我陪你一起查。