先明确整体背景
这个示例是一个WinForm 工控上位机程序,核心功能是通过Modbus TCP 协议和 PLC(或 Modbus 模拟器)通信,全程用async/await + Action/Func<Task>实现异步操作,避免 UI 卡死 —— 这是工控上位机开发的核心要求(UI 必须流畅,通信不能阻塞)。

一、前置基础配置(程序初始化)
// Modbus TCP配置(根据实际PLC修改)
private readonly string _plcIp = "192.168.1.100"; // PLC/模拟器IP
private readonly int _plcPort = 502; // Modbus TCP默认端口
private readonly int _startAddress = 40001; // 保持寄存器起始地址(4x区)
private readonly int _registerCount = 3; // 读取寄存器数量
public Form1()
{
InitializeComponent();
txtLog.Multiline = true;
txtLog.ScrollBars = ScrollBars.Vertical;
}
关键说明:
- •
_plcIp/_plcPort:PLC 的网络地址,Modbus TCP 默认端口是 502(几乎所有 PLC / 模拟器都用这个); - •
_startAddress:Modbus 保持寄存器(4x 区)的起始地址,工控中 40001 是最常用的起始地址(对应 PLC 的模拟量 / 数值型数据); - •
_registerCount:要读取的寄存器数量(这里读 3 个,分别存温度、压力、运行状态)。
- 2. UI 初始化:设置日志文本框为 “多行 + 滚动条”,是工控上位机日志显示的标准配置。
二、核心工具方法:线程安全的日志输出
private void Log(string message)
{
if (txtLog.InvokeRequired)
{
Action<string> logAction = Log;
txtLog.Invoke(logAction, message);
return;
}
txtLog.AppendText($"[{DateTime.Now:HH:mm:ss.fff}] {message}{Environment.NewLine}");
}
关键说明(工控开发必懂):
- 1. 为什么需要这个方法?
异步任务(如 Modbus 通信)运行在后台线程,WinForm 的 UI 控件(如 txtLog)只能由UI 线程更新,直接在后台线程改 UI 会抛异常 —— 这是 WinForm 跨线程更新 UI 的核心坑。 - •
txtLog.InvokeRequired:判断当前线程是不是 UI 线程; - •
Action<string> logAction = Log:把 Log 方法封装成Action委托; - •
txtLog.Invoke(logAction, message):让 UI 线程执行这个 Action,实现线程安全的 UI 更新。
- 3. 工控场景价值:上位机需要实时显示通信日志、设备状态,这个方法是所有 UI 更新的基础,保证日志不会 “卡壳” 或报错。
三、场景 1:异步读取 PLC Modbus TCP 数据(核心功能)
这是整个示例的核心,拆解成 5 个步骤讲解:
步骤 1:按钮点击事件(UI 触发入口)
private async void btnReadPLC_Click(object sender, EventArgs e)
{
btnReadPLC.Enabled = false; // 禁用按钮,防止重复点击(工控必做)
Log($"开始连接PLC:IP={_plcIp}, 端口={_plcPort}");
try
{
// 封装Modbus读取逻辑为异步委托
Func<Task<string>> readPlcLogic = async () =>
{
// 异步执行Modbus通信(下面讲)
};
// 异步执行,不阻塞UI
string plcData = await readPlcLogic();
lblPLCData.Text = plcData; // 更新PLC数据显示
Log($"PLC数据读取完成:{plcData}");
}
catch (Exception ex)
{
// 异常捕获(工控中必须有,通信失败要提示)
Log($"读取PLC失败:{ex.Message}");
MessageBox.Show($"Modbus通信异常:{ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
btnReadPLC.Enabled = true; // 恢复按钮
}
}
关键说明:
- •
async void:仅用于UI 事件处理程序(如按钮点击),这是 C# 允许async void的唯一安全场景; - •
btnReadPLC.Enabled = false:工控中必须做 —— 防止用户快速多次点击,导致同时建立多个 PLC 连接,引发端口占用 / 通信混乱; - •
Func<Task<string>> readPlcLogic:把 Modbus 读取逻辑封装成异步委托(有返回值,返回 PLC 数据字符串),替代不能直接用的async Action。
步骤 2:Modbus 通信核心逻辑(readPlcLogic 内部)
return await Task.Run(() =>
{
TcpClient tcpClient = null;
IModbusMaster modbusMaster = null;
try
{
// 1. 建立TCP连接
tcpClient = new TcpClient();
tcpClient.Connect(_plcIp, _plcPort);
Log("PLC TCP连接成功");
// 2. 创建Modbus主站
modbusMaster = ModbusIpMaster.CreateIp(tcpClient);
modbusMaster.Transport.ReadTimeout = 2000; // 读取超时2秒
// 3. 读取保持寄存器(地址偏移转换是核心!)
int startOffset = _startAddress - 40001; // 40001 → 偏移0
ushort[] registers = modbusMaster.ReadHoldingRegisters(
slaveAddress: 1, // 从站地址(PLC站号,默认1)
startAddress: startOffset,
numberOfPoints: _registerCount);
// 4. 解析寄存器数据(工控数据解析的典型方式)
float temperature = registers[0] / 10.0f; // 寄存器值/10 = 实际温度
float pressure = registers[1] / 100.0f; // 寄存器值/100 = 实际压力
int status = registers[2]; // 运行状态(0=停止,1=运行)
return $"温度={temperature}℃,压力={pressure}MPa,运行状态={(status == 1 ? "正常" : "停止")}";
}
finally
{
// 5. 释放资源(工控中必须做,否则会占用TCP端口)
modbusMaster?.Dispose();
tcpClient?.Close();
tcpClient?.Dispose();
}
});
关键说明(工控开发重点):
- •
TcpClient.Connect:和 PLC 建立 TCP 连接,这是 Modbus TCP 的基础(Modbus TCP 本质是基于 TCP/IP 的应用层协议); - • 必须放在
Task.Run里:因为Connect是同步阻塞操作,放异步委托里避免 UI 卡死。
- 2. **Modbus 地址偏移转换(最易错点)**:
- • PLC 手册里的地址是
40001,但 NModbus4 要求传偏移量(从 0 开始),所以要减 40001; - • 比如 40001→0、40002→1、40003→2,这是所有 Modbus 库的通用规则,错了就读不到数据!
- • PLC 的寄存器是
ushort(无符号 16 位整数),工控中不会直接存浮点数(如 25.6℃),而是存整数(256),上位机再除以 10/100 解析 —— 这是为了减少 PLC 数据传输开销,是行业惯例; - • 运行状态用 0/1 表示,是工控中 “离散状态” 的典型存储方式。
- •
finally块里释放modbusMaster和tcpClient:工控现场 PLC 连接数有限,不释放会导致 “连接耗尽”,PLC 拒绝新连接,这是线上故障的高频原因!
四、场景 2:异步批量下发 Modbus 指令(写入寄存器)
private async void btnSendCommands_Click(object sender, EventArgs e)
{
btnSendCommands.Enabled = false;
Log("开始批量下发Modbus指令到PLC...");
try
{
// 模拟指令列表:(寄存器地址, 写入值)
List<(int Address, ushort Value)> commands = new List<(int, ushort)>
{
(40004, 1), // 启动指令
(40005, 50),// 设定转速
(40006, 25) // 设定温度
};
// 封装单条指令写入逻辑为Action
Action<(int Address, ushort Value)> sendCommandAction = (cmd) =>
{
// 内部逻辑和读取类似:建立连接→写入寄存器→释放资源
TcpClient tcpClient = null;
IModbusMaster modbusMaster = null;
try
{
tcpClient = new TcpClient(_plcIp, _plcPort);
modbusMaster = ModbusIpMaster.CreateIp(tcpClient);
modbusMaster.Transport.WriteTimeout = 2000;
int offset = cmd.Address - 40001;
// 写入单个寄存器(工控中下发指令的核心API)
modbusMaster.WriteSingleRegister(1, offset, cmd.Value);
Log($"指令下发完成:寄存器{cmd.Address} = {cmd.Value}");
}
finally
{
modbusMaster?.Dispose();
tcpClient?.Close();
tcpClient?.Dispose();
}
};
// 并行执行所有指令(提升效率)
List<Task> tasks = new List<Task>();
foreach (var cmd in commands)
{
tasks.Add(Task.Run(() => sendCommandAction(cmd)));
}
await Task.WhenAll(tasks);
Log("所有Modbus指令下发完成!");
}
catch (Exception ex)
{
Log($"下发指令失败:{ex.Message}");
}
finally
{
btnSendCommands.Enabled = true;
}
}
关键说明:
- •
Action<(int Address, ushort Value)>:把单条写入指令封装成 Action,复用性强(比如不同设备的指令可以用同一个 Action); - • 工控中指令通常是 “地址 + 值” 的形式,比如 40004 写入 1 表示 “启动设备”,是 PLC 程序约定的逻辑。
- 2. **并行执行(Task.WhenAll)**:
- • 批量下发指令时,并行执行比串行快数倍(比如 3 条指令串行要 3 秒,并行只要 1 秒);
- • 适合工控中的 “广播指令”“批量参数配置” 场景,比如同时给多个寄存器设值。
- 3. WriteTimeout:写入超时必须设置,防止 PLC 忙时上位机无限等待。
五、场景 3:异步记录 Modbus 通信日志(非核心但必要)
private async void btnLog_Click(object sender, EventArgs e)
{
Log("开始异步记录Modbus通信日志...");
// 封装日志记录逻辑
Func<Task> logLogic = async () =>
{
await Task.Delay(500); // 模拟写入文件/数据库耗时
string logContent = $"Modbus通信日志:PLC IP={_plcIp},最后通信时间={DateTime.Now},状态=正常";
Log($"日志已存储:{logContent}");
};
// 异步执行,捕获异常(火并忘但不丢异常)
_ = logLogic().ContinueWith(task =>
{
if (task.Exception != null)
{
Log($"日志记录失败:{task.Exception.InnerException.Message}");
}
});
Log("Modbus日志记录请求已提交(异步执行)!");
}
关键说明:
- • **(Fire and Forget)**:日志记录是 “非核心业务”,不需要阻塞用户操作,所以用
_ = logLogic()执行; - •
ContinueWith:必须加 —— 防止日志写入失败时异常 “吞掉” 或导致程序崩溃,工控系统哪怕日志写失败,核心业务也不能停。
总结(核心要点回顾)
- 1. 异步核心:用
async/await + Task.Run封装 Modbus 同步通信逻辑,避免 UI 卡死;Action/Func<Task>封装可复用的业务逻辑(日志、指令、读取); - 2. Modbus 关键:地址偏移转换(40001→0)、资源释放(finally 块)、超时设置(Read/WriteTimeout)是通信成功的三大核心;
- 3. 工控规范:按钮禁用防重复操作、线程安全更新 UI、异常捕获提示、日志记录,是工控上位机稳定运行的必备设计。
这个示例完全复刻了实际工控项目的开发思路,你只要替换 PLC IP、寄存器地址和数据解析逻辑,就能直接用到现场项目中。
阅读原文:https://mp.weixin.qq.com/s/b91coPCOk56L3w1uPHjEBA
该文章在 2026/1/24 11:40:22 编辑过