简介:在工业自动化领域,上位机与PLC通信是实现设备监控与数据采集的核心技术。其原理基于TCP/IP网络,通过特定的应用层协议(如三菱MC协议)进行数据交换,实现计算机与工业控制器之间的可靠对话。这项技术的价值在于打通了信息层与控制层,为设备状态可视化、生产数据记录和报警触发提供了基础。在C#上位机开发中,通过Socket编程实现MC协议通信,是连接三菱FX系列PLC的常见应用场景。本文聚焦于MC协议3E帧的解析与FX3U软元件寻址,详细阐述了如何构建请求帧、处理响应数据,并针对工业现场常见的网络不稳定问题,提供了连接保活、断线重连等稳定性优化方案,是构建可靠数据监控系统的关键实践。
1. 项目缘起:当C#上位机遇上三菱FX3U
最近在做一个设备数据监控的项目,客户现场的主力控制器是一台老当益壮的三菱FX3U系列PLC。需求很明确:需要开发一个运行在工控机上的C#上位机软件,实时读取PLC内部M辅助继电器的状态,用于在电脑屏幕上显示设备运行状态、触发报警和记录生产数据。
这个需求在工业自动化领域非常典型,就是所谓的“上位机与PLC通信”。三菱FX3U本身自带编程口和422/485接口,但这次客户为了布线方便和未来扩展,已经在PLC上扩展了一块以太网模块,比如FX3U-ENET-ADP或者FX3U-ENET-L。这就意味着,我们不能再走传统的串口协议(比如编程口协议、无顺序协议),而是要走网络通信。
在众多工业以太网协议中,三菱自家的MC协议(MELSEC Communication Protocol)是一个绕不开的选择。它是一种基于TCP/IP的应用层协议,专门用于三菱MELSEC系列PLC(包括FX, Q, L系列等)与外部设备(如HMI、SCADA、定制上位机)进行数据交换。对于C#开发者来说,这意味着我们需要在.NET环境中,通过Socket编程,按照MC协议的帧格式去组包和解包,从而实现与FX3U的“对话”。
这个项目的核心挑战不在于C#的网络编程本身,而在于对MC协议细节的精确把握,以及处理工业现场网络通信中各种稳定性问题。下面,我就把这次从零搭建通信链路、成功读取M区值的过程,以及中间踩过的坑和总结的经验,完整地分享出来。
2. 通信基石:理解三菱MC协议与FX3U寻址
在动手写代码之前,必须先把通信的“语言”——MC协议搞清楚。我们用的是MC协议3E帧(二进制码),这是目前最常用、效率较高的一种格式。它基于TCP客户端/服务器模型,我们的C#程序作为TCP客户端,PLC的以太网模块作为TCP服务器。
一个完整的MC协议通信过程,就是客户端向服务器的指定端口(默认为5002或5003,通常用5002)发送一个请求帧,PLC处理后再回复一个响应帧。
2.1 MC协议3E帧结构拆解
请求帧和响应帧都有固定的结构。对于我们“读取位设备(M区)”这个目标,请求帧主要包含以下几个部分:
- 副头部:固定为
50 00,代表这是MC协议。 - 网络编号:通常为
00(FX系列PLC固定为0)。 - PLC编号:通常为
FF(广播,或根据网络设置)。 - 请求目标模块I/O编号:通常为
FF 03(CPU模块)。 - 请求目标模块站号:通常为
00。 - 请求数据长度:后面所有数据的字节长度。
- CPU监视定时器:设置通信超时时间,例如
10 00表示16*10ms=160ms。 - 指令:
04 00代表“位设备批量读取”。 - 子指令:
00 00。 - 起始软元件地址:这是最关键也是最容易出错的部分。它指定了从哪个M点开始读。例如,要读取M0,这里的值不是简单的
00 00。MC协议采用了一种特殊的编码方式,需要将软元件号转换成“软元件代码+偏移地址”。
对于FX3U的M区,其软元件代码是90。假设我们要读取M0,计算过程如下:
- 软元件代码:
90 - M0的偏移量:
0 - 在帧中,我们需要按
90 00 00 00四个字节来表示。即先放软元件代码90,后面三个字节放偏移地址00 00 00(小端序,低位在前)。
- 读取点数:用两个字节表示。例如,要连续读取10个M点(M0-M9),就是
0A 00。
把上面这些部分按顺序拼接成一个字节数组,就是我们的请求帧。PLC收到后,如果正常,会返回一个响应帧。响应帧的前面部分是对请求的回应(包含结束代码,00 00表示正常),后面跟着的就是读取到的数据。每个M点的状态(ON/OFF)用一个位(bit)表示,8个点凑成一个字节。
2.2 FX3U软元件地址计算实战
很多人在这里迷糊,我举个例子。假设我们要读取M100开始的5个点。
- 确定软元件代码:M区代码是
90(十六进制)。 - 计算偏移地址:M100的编号是100。在MC协议中,这个编号需要直接作为偏移量。100的十六进制是
64。 - 构建地址字段:地址字段共4字节。第一字节是软元件代码
90。后三字节是偏移地址,需要转换成小端序的3字节格式。数字100 (64) 用3字节小端序表示就是64 00 00。- 所以,完整的起始地址字段是:
90 64 00 00
- 所以,完整的起始地址字段是:
- 构建读取点数字段:读取5个点,即
05 00(小端序)。
那么,在请求帧中,对应“起始软元件地址”的部分,我们就填入90 64 00 00。这一点和读取D寄存器时用A8代码不同,务必区分。
注意:三菱不同系列的PLC(Q/L/FX),甚至不同批次的FX3U固件,对于某些特殊M点(如M8000以上)的地址映射可能有细微差别。最稳妥的方法是先在GX Works2的“软元件批量监视”功能里确认该点的实际地址,或者查阅对应以太网模块的详细手册。
3. 环境搭建与Socket通信层实现
理解了协议,我们就可以开始搭建C#项目了。我使用的是Visual Studio 2022,.NET 6框架(兼容性好,也可用.NET Framework 4.7.2+)。
3.1 创建项目与核心类设计
首先创建一个C#控制台应用或WinForms/WPF应用。我将通信核心功能封装在一个单独的类MitsubishiMcProtocol.cs中,这样便于复用和管理。
这个类的核心成员包括:
TcpClient _tcpClient: 用于TCP连接。NetworkStream _stream: 网络流,用于读写数据。string _ipAddress和int _port: PLC的IP和端口。int _receiveTimeout: 接收超时时间。
构造函数中传入IP和端口进行初始化。
public class MitsubishiMcProtocol { private TcpClient _tcpClient; private NetworkStream _stream; private string _ipAddress; private int _port; private int _receiveTimeout = 2000; // 默认2秒超时 public MitsubishiMcProtocol(string ip, int port = 5002) { _ipAddress = ip; _port = port; } }3.2 建立与断开连接
连接方法需要处理异常,因为工业现场网络可能不稳定。
public bool Connect() { try { _tcpClient = new TcpClient(); // 设置连接超时,避免长时间阻塞 var connectTask = _tcpClient.ConnectAsync(_ipAddress, _port); if (connectTask.Wait(TimeSpan.FromMilliseconds(3000))) // 等待3秒 { if (_tcpClient.Connected) { _stream = _tcpClient.GetStream(); _stream.ReadTimeout = _receiveTimeout; Console.WriteLine($"成功连接到PLC {_ipAddress}:{_port}"); return true; } } else { Console.WriteLine("连接PLC超时。"); // 手动取消并清理 _tcpClient?.Close(); } } catch (SocketException ex) { Console.WriteLine($"网络连接错误: {ex.SocketErrorCode} - {ex.Message}"); } catch (Exception ex) { Console.WriteLine($"连接发生异常: {ex.Message}"); } return false; } public void Disconnect() { try { _stream?.Close(); _tcpClient?.Close(); Console.WriteLine("与PLC连接已断开。"); } catch { } }实操心得一:连接超时设置:直接使用
_tcpClient.Connect(ip, port)在无法连接时会阻塞很长时间。我采用ConnectAsync配合Wait和超时时间的方式,可以更友好地控制连接等待时长,给用户及时的反馈。同时,一定要在UI线程外进行连接操作,防止界面卡死。
3.3 核心通信方法:发送与接收帧
这是最底层、最核心的方法,负责把组装好的字节数组发出去,并把收到的字节数组读回来。
private byte[] SendAndReceive(byte[] requestData) { if (_stream == null || !_tcpClient.Connected) { throw new InvalidOperationException("未连接到PLC。"); } // 发送请求帧 _stream.Write(requestData, 0, requestData.Length); // Console.WriteLine($"已发送: {BitConverter.ToString(requestData)}"); // 接收响应帧 // 先读取固定长度的头部(11字节),以确定后续数据长度 byte[] headerBuffer = new byte[11]; int bytesRead = ReadBytesFromStream(headerBuffer, 0, 11); if (bytesRead != 11) { throw new IOException($"读取响应头失败,期望11字节,实际收到{bytesRead}字节。"); } // 从响应头第9-10字节(小端序)获取后续数据长度 int extraDataLength = BitConverter.ToInt16(headerBuffer, 9); // 总响应长度 = 11字节头 + 后续数据长度 byte[] fullResponse = new byte[11 + extraDataLength]; Array.Copy(headerBuffer, 0, fullResponse, 0, 11); // 读取剩余的数据部分 if (extraDataLength > 0) { bytesRead = ReadBytesFromStream(fullResponse, 11, extraDataLength); if (bytesRead != extraDataLength) { throw new IOException($"读取响应数据失败,期望{extraDataLength}字节,实际收到{bytesRead}字节。"); } } // Console.WriteLine($"已接收: {BitConverter.ToString(fullResponse)}"); return fullResponse; } // 一个可靠的从流中读取指定字节数的方法 private int ReadBytesFromStream(byte[] buffer, int offset, int count) { int totalRead = 0; while (totalRead < count) { int read = _stream.Read(buffer, offset + totalRead, count - totalRead); if (read == 0) { throw new IOException("网络流已关闭。"); } totalRead += read; } return totalRead; }实操心得二:粘包处理与长度解析:工业TCP通信要特别注意“粘包”问题。MC协议的响应帧是定长头部+可变长度数据体的结构。绝对不能简单地用
_stream.Read读到缓冲区满为止,那样可能会把下一次的响应帧也读进来。正确做法是:先读取固定11字节的头部,然后从头部中解析出后续数据体的长度(第9、10字节),再精确读取对应长度的数据体。这是稳定通信的关键。
4. 协议组装与M区读取功能实现
有了可靠的通信底层,接下来就是按照第2章分析的协议格式,组装具体的“读取M区”请求帧。
4.1 构建读取位设备的请求帧
我将其封装成一个独立的方法BuildReadMRequest。
private byte[] BuildReadMRequest(ushort startAddress, ushort pointCount) { // 1. 计算起始地址字段 byte deviceCode = 0x90; // M软元件代码 // 将起始地址转换为3字节小端序数组 byte[] addressBytes = BitConverter.GetBytes(startAddress); // BitConverter.GetBytes 得到的是2字节(ushort),我们需要3字节,且小端序。 // 例如 startAddress = 100 (0x0064), addressBytes = [0x64, 0x00] // 我们需要的是 [0x64, 0x00, 0x00] 作为后三字节。 byte[] fullAddress = new byte[4]; fullAddress[0] = deviceCode; fullAddress[1] = addressBytes[0]; // 低位 fullAddress[2] = addressBytes[1]; // 高位 fullAddress[3] = 0x00; // 第三字节补0 // 2. 计算请求数据长度 (指令以后的所有数据字节数) // 指令(4) + 子指令(2) + 地址(4) + 点数(2) = 12 字节 ushort requestDataLength = 12; byte[] lengthBytes = BitConverter.GetBytes(requestDataLength); // 3. 构建完整帧 using (MemoryStream ms = new MemoryStream()) using (BinaryWriter bw = new BinaryWriter(ms)) { // 副头部 bw.Write((ushort)0x5000); // 网络号,PLC号,目标模块IO,目标站号 bw.Write((byte)0x00); // 网络号 bw.Write((byte)0xFF); // PLC号 bw.Write((ushort)0x03FF); // 目标模块IO (0x03FF) bw.Write((byte)0x00); // 目标站号 // 请求数据长度 bw.Write(lengthBytes); // 小端序写入 // CPU监视定时器 bw.Write((ushort)0x0010); // 160ms // 指令:位设备批量读取 bw.Write((ushort)0x0004); // 子指令 bw.Write((ushort)0x0000); // 起始软元件地址 bw.Write(fullAddress); // 读取点数 bw.Write(BitConverter.GetBytes(pointCount)); return ms.ToArray(); } }4.2 解析响应帧并提取M点状态
发送请求后,我们需要解析PLC返回的响应帧,提取出M点的ON/OFF状态。
public bool[] ReadMBatch(ushort startAddress, ushort pointCount) { byte[] request = BuildReadMRequest(startAddress, pointCount); byte[] response = SendAndReceive(request); // 检查结束代码 (响应帧第9-10字节) ushort endCode = BitConverter.ToUInt16(response, 9); if (endCode != 0x0000) { throw new Exception($"PLC返回错误,结束代码: 0x{endCode:X4}。请检查地址和点数。"); } // 数据部分从第11字节开始 int dataStartIndex = 11; // 计算返回的数据字节数。每个字节包含8个M点的状态。 int byteCount = (pointCount + 7) / 8; // 向上取整 bool[] results = new bool[pointCount]; for (int i = 0; i < pointCount; i++) { int byteIndex = i / 8; int bitIndex = i % 8; if (dataStartIndex + byteIndex < response.Length) { byte dataByte = response[dataStartIndex + byteIndex]; // 判断特定位是否为1 (通常1表示ON) results[i] = ((dataByte >> bitIndex) & 0x01) == 0x01; } else { // 理论上不会发生,因为前面检查过长度 results[i] = false; } } return results; }4.3 提供一个简单的读取单个M点的方法
为了方便使用,可以再封装一个更简单的方法。
public bool ReadMSingle(ushort address) { bool[] results = ReadMBatch(address, 1); return results[0]; }5. 从Demo到实战:完整应用与稳定性打磨
把上面的类封装好,就可以写一个简单的控制台程序进行测试了。
class Program { static void Main(string[] args) { string plcIp = "192.168.1.100"; // 替换为你的PLC实际IP MitsubishiMcProtocol mc = new MitsubishiMcProtocol(plcIp, 5002); try { if (mc.Connect()) { // 示例1:读取M0到M9,共10个点 Console.WriteLine("读取 M0-M9:"); bool[] m0To9 = mc.ReadMBatch(0, 10); for (int i = 0; i < m0To9.Length; i++) { Console.WriteLine($" M{i} = {m0To9[i]}"); } // 示例2:读取单个点M100 Console.WriteLine("\n读取 M100:"); bool m100State = mc.ReadMSingle(100); Console.WriteLine($" M100 = {m100State}"); // 示例3:读取M200开始的16个点(用于监控一组设备状态) Console.WriteLine("\n读取 M200-M215 (16个点):"); bool[] groupStatus = mc.ReadMBatch(200, 16); // 可以在这里将状态数组与具体的设备名称映射显示 } } catch (Exception ex) { Console.WriteLine($"操作失败: {ex.Message}"); } finally { mc.Disconnect(); } Console.ReadLine(); } }如果一切顺利,运行程序后就能在控制台看到PLC内部M点的状态了。但这仅仅是开始,要把这个功能用到实际的工业上位机软件中,还需要解决很多稳定性问题。
5.1 通信超时与重试机制
工业网络环境复杂,偶尔丢一两个包是常事。我们的通信层必须有重试机制。
public bool[] ReadMBatchWithRetry(ushort startAddress, ushort pointCount, int maxRetries = 3) { int retryCount = 0; while (retryCount < maxRetries) { try { return ReadMBatch(startAddress, pointCount); } catch (IOException ex) // 网络异常 { retryCount++; Console.WriteLine($"第{retryCount}次读取失败 ({ex.Message}),{retryCount < maxRetries ? "正在重试..." : "已达最大重试次数。"}"); if (retryCount >= maxRetries) { throw new Exception($"读取M{startAddress}失败,已达最大重试次数。", ex); } Thread.Sleep(100 * retryCount); // 退避等待 } catch (Exception ex) // 协议错误等非网络异常,不重试 { throw; } } return new bool[pointCount]; // 理论上不会执行到这里 }5.2 连接保活与断线重连
对于需要长时间运行的监控软件,不能假设连接永远不断。需要实现一个后台心跳或定时读取机制,并在检测到断线时自动重连。
public class PlcDataService { private MitsubishiMcProtocol _protocol; private System.Timers.Timer _heartbeatTimer; private bool _isConnected = false; public PlcDataService(string ip) { _protocol = new MitsubishiMcProtocol(ip); _heartbeatTimer = new System.Timers.Timer(5000); // 5秒一次心跳 _heartbeatTimer.Elapsed += HeartbeatElapsed; _heartbeatTimer.AutoReset = true; } public void Start() { TryReconnect(); _heartbeatTimer.Start(); } private void HeartbeatElapsed(object sender, System.Timers.ElapsedEventArgs e) { try { // 尝试读取一个固定的、无害的M点(比如M8000,常ON点)作为心跳 // 注意:FX3U中M8000是运行监视常ON触点,读取它是安全的。 bool status = _protocol.ReadMSingle(8000); _isConnected = true; // Console.WriteLine("心跳检测:连接正常"); } catch { _isConnected = false; Console.WriteLine("心跳检测:连接异常,尝试重连..."); TryReconnect(); } } private void TryReconnect() { for (int i = 0; i < 3; i++) { try { _protocol.Disconnect(); // 先清理旧连接 if (_protocol.Connect()) { _isConnected = true; Console.WriteLine("断线重连成功。"); return; } } catch { } Thread.Sleep(2000); } Console.WriteLine("断线重连失败,请检查网络和PLC状态。"); } }5.3 性能优化:批量读取与异步操作
如果需要监控上百个M点,逐点读取效率极低,网络压力也大。务必使用批量读取ReadMBatch。在UI程序中,为了不阻塞界面,所有PLC通信操作都应该放在后台线程或使用异步方法。
// 在WinForms或WPF中,使用异步方法避免UI卡顿 public async Task<bool[]> ReadMBatchAsync(ushort startAddress, ushort pointCount, CancellationToken cancellationToken) { return await Task.Run(() => { // 这里可以加入取消令牌检查 if (cancellationToken.IsCancellationRequested) { return new bool[pointCount]; } return ReadMBatchWithRetry(startAddress, pointCount); }, cancellationToken); }然后在UI按钮事件中调用:
private async void btnReadM_Click(object sender, EventArgs e) { btnReadM.Enabled = false; try { bool[] status = await _plcService.ReadMBatchAsync(0, 100, _cts.Token); // 更新UI显示状态... } catch (OperationCanceledException) { MessageBox.Show("读取操作被取消。"); } catch (Exception ex) { MessageBox.Show($"读取失败: {ex.Message}"); } finally { btnReadM.Enabled = true; } }6. 深度排错:当通信失败时该怎么办
即使代码看起来完美,第一次调试也大概率会失败。下面是我总结的排查链路,按照这个顺序查,基本能解决99%的问题。
6.1 第一步:基础网络连通性检查
这是最根本的一步。在运行C#程序的电脑上,打开命令提示符,执行ping 192.168.1.100(替换为你的PLC IP)。如果ping不通,说明物理层或网络层有问题。
- 检查网线:是否插好?交换机/路由器是否通电?
- 检查IP设置:确保电脑和PLC的IP地址在同一网段,且子网掩码一致。例如,PLC是
192.168.1.100,电脑可以是192.168.1.50,子网掩码都是255.255.255.0。 - 关闭防火墙:临时关闭电脑和网络设备上的防火墙,排除拦截可能。
6.2 第二步:确认PLC以太网模块设置
通过GX Works2软件连接PLC(可以通过USB编程线),检查以太网模块的参数设置。
- IP地址:确认是否和我们程序中写的一致。
- 端口号:确认MC协议使用的端口号(通常是5002)。
- 协议:确认以太网模块的“打开方式”中,是否允许MC协议通信。有些模块默认只允许“MELSOFT连接”(即GX Works2编程通信),需要手动勾选“TCP”或“MC协议”。
- 站号设置:通常保持默认即可。
6.3 第三步:使用网络抓包工具分析
这是定位协议层问题最强大的手段。在电脑上安装Wireshark,开始抓包,然后运行你的C#程序。
- 过滤器:在Wireshark中输入
tcp.port == 5002过滤出与PLC通信的数据包。 - 观察TCP握手:看是否有完整的TCP三次握手(SYN, SYN-ACK, ACK)。如果没有,说明连接被拒绝或路由有问题。
- 分析应用层数据:找到你的程序发出的第一个数据包(通常是发送请求帧)。右键 -> “追踪流” -> “TCP流”。这时你可以看到原始的十六进制通信数据。
- 对比请求帧:将Wireshark中显示的十六进制数据,与你程序中
BuildReadMRequest方法生成的字节数组进行逐字节对比。重点检查“起始软元件地址”字段(第21-24字节左右)是否正确。一个字节不对,PLC就不会响应。 - 查看响应帧:如果PLC回复了,看响应帧的“结束代码”(第9-10字节)。如果不是
00 00,就根据三菱手册查询错误码含义。常见的C050表示软元件地址错误,C054表示访问点数超限。
- 对比请求帧:将Wireshark中显示的十六进制数据,与你程序中
踩坑实录:我曾遇到一个诡异的问题,程序发出的请求帧和手册例子一模一样,但PLC就是不回复。用Wireshark抓包对比后发现,我程序里“请求数据长度”字段计算错了,导致整个帧长度不对。PLC的协议栈可能直接丢弃了格式错误的帧,连错误代码都不回。所以,抓包是验证你发出的数据是否“标准”的唯一金标准。
6.4 第四步:检查代码中的常见陷阱
- 字节序问题:MC协议中,多字节数据(如长度、点数、地址偏移量)都采用小端序(Little-Endian),即低位字节在前。C#的
BitConverter.GetBytes()方法在小端序的Windows系统上默认就是小端序,所以通常没问题。但心里一定要有这个意识。 - 软元件代码错误:读取M区用
0x90,读取D寄存器用0xA8,读取X输入点用0x9C,读取Y输出点用0x9D。用错了代码,PLC会返回地址错误。 - 点数超限:FX3U单次读取M点的数量是有限制的,根据使用的指令和模块不同,可能是512点或更少。一次不要请求太多点,可以分批读取。
- 线程阻塞:在UI线程中同步调用
ReadMBatch,如果网络超时,会导致程序界面“卡死”数秒。务必使用异步调用。
6.5 第五步:利用PLC侧诊断功能
FX3U的以太网模块上有LED指示灯,可以直观判断状态:
- LINK/ACT灯:常亮表示物理链路正常,闪烁表示有数据收发。如果不亮,检查网线。
- OPEN灯:常亮表示TCP连接已建立。如果你的程序连接后这个灯不亮,说明TCP连接没成功,回到第一步和第二步检查。
7. 功能扩展与进阶思考
实现了基础的M点读取,这个通信框架就可以作为基石,扩展出更多功能。
7.1 写入M点
写入和读取类似,但指令码不同(14 00表示位设备批量写入)。你需要构建的请求帧,除了地址和点数,还要附带要写入的数据。数据部分也是每个位对应一个M点,ON为1,OFF为0。务必谨慎使用写入功能,特别是写入M8000以上的系统特殊继电器,可能导致PLC停止运行。
7.2 读取D寄存器
读取D寄存器(字设备)的软元件代码是0xA8。指令码为01 00(字设备批量读取)。响应帧中的数据部分,每两个字节代表一个D寄存器的值(0-65535)。解析时需要用BitConverter.ToUInt16进行转换。
7.3 封装通用读写方法
可以设计更通用的方法,通过枚举来指定软元件类型。
public enum DeviceType { M, D, X, Y } public object ReadDeviceBatch(DeviceType type, ushort startAddress, ushort pointCount) { byte deviceCode = GetDeviceCode(type); // 根据类型返回 0x90, 0xA8等 // ... 后续组装逻辑类似,根据是位设备还是字设备,返回bool[]或ushort[] }7.4 考虑使用成熟的通信库
如果项目通信需求复杂,或者想更快上手,可以考虑使用一些成熟的开源或商业的三菱PLC通信库,例如Mitsubishi.MC.Protocol(开源)或S7NetPlus的兄弟库(如果有)。这些库封装了协议细节,提供了更友好的API。但自己实现一遍的最大好处是,出了问题你知道从哪里下手去查,而不是对着黑盒库一筹莫展。
这次从零实现C#通过MC协议读取FX3U M区的过程,让我对工业协议底层的严谨性和现场调试的复杂性有了更深的认识。代码本身的逻辑并不复杂,难的是对细节的把握和对异常情况的处理。最深的体会就是:一定要用抓包工具验证,它比任何日志都可靠;一定要加超时和重试,工业现场没有“绝对稳定”的网络。把这个基础打牢了,后续无论是扩展功能还是移植到其他品牌PLC,都会顺畅很多。
本文还有配套的精品资源,点击获取