☰
C#通过FINS协议读取欧姆龙PLC内存区:UDP报文构建与解析实战
2026/10/17 8:32:23 网站建设 项目流程

简介:面向需要借助C#与欧姆龙可编程控制器联调的上位机程序员和工业自动化工程师,以FINS协议通信为切入点,从TCP连接建立讲起,完整演示了如何利用System.Net.Sockets类库中的TcpClient和NetworkStream连接可编程控制器,并构造FINS请求帧、解析响应数据、读写寄存器等常见操作,配套教学课件、Windows窗体示例源码和通讯手册,可直接对照修改使用。包内共45个文件,包含9个C#源文件、4个exe可执行程序、通讯手册PDF、Word直播素材、网络模型图与协议格式图等,既能看到实现代码,也有可运行的Demo和辅助调试的NetAssist测试工具,整体仅4.38MB,查阅成本低。已有1117人学习下载。内容覆盖连接建立、FINS命令格式、错误处理与多寄存器读写等细节,配合目录中清晰的源码工程、代码文本和课件图片,能帮助读者快速绕过协议文档的晦涩之处,掌握一套可直接落地的C#与欧姆龙PLC通信方案。

1. C#通过FINS读取欧姆龙PLC协议例子:先从一个采集需求说起

某产线的上位机项目,需要在C#端通过FINS协议读取欧姆龙PLC的DM区数据,用来实时显示温度、压力和运行状态。设备那边没有采购现成的通信组件,项目进度又不允许引入新的依赖,所以只能在基础Socket上把FINS报文直接拼出来。你会发现这个需求很典型:不复杂,但帧头、节点号、内存区地址、大端字节序这些细节里全是坑。真正动手时最容易被绊倒的,不是C#语法,而是报文里那一堆字节到底该填什么。这套方案适合正在做上位机与欧姆龙PLC通信课设或毕设的同学,也适合需要快速交付采集功能的工程师参考。下面从协议结构、C#实现、常见排查、批量轮询到验收答辩完整展开,目标是让你照着一份代码就能把FINS通信跑起来,并且知道出了问题该往哪里看。

2. FINS协议基础与报文结构:帧头10字节、命令码和内存区代码怎么定

2.1 FINS/UDP通信的选型:为什么先从UDP端口9600入手

欧姆龙PLC的以太网通信支持FINS/TCP和FINS/UDP两种承载方式。多数PLC默认开放的端口都是9600,但UDP实现更轻:不需要建立连接,每次调用就是一个完整的请求-响应过程。在C#里,UdpClient发送一条请求后,阻塞等待接收响应,单条请求的语义非常清楚。

我一般在项目初期先用UDP打通链路,原因有两个。第一,UDP调试简单,一条Send和一条Receive就能完成一次通信,出问题时可以快速用抓包工具确认PLC有没有应答。第二,上位机采集场景基本都是周期性轮询,每次请求独立,UDP天然契合这种模式;TCP反而要处理连接断开、粘包、心跳,在PLC主动断电或网线松动时会增加很多边界逻辑。

需要说明的是,UDP模式下PLC不会对每条报文做传输层确认,所以应用层的超时重试必须自己负责。也就是说,Send之后不一定能等到Receive,可能丢包,也可能PLC根本就没收到。这个问题在第四章会展开讲。

如果你最终要面对的是多台上位机同时读取一台PLC,FINS协议本身没有复杂的会话管理,UDP也够用。只要保证每台上位机的源节点号SA1不同,PLC就能区分请求来源。我曾经在一条产线上用三台工控机同时采集同一台PLC,全部走UDP,没有冲突。

这里还要区分一个概念:FINS/UDP的报文在以太网上不需要额外校验字节,因为IP包自带校验。做TCP方案时反而要在FINS报文前面加4字节长度前缀,多一个粘包拆包的步骤。所以从“快速跑通”的角度看,UDP是成本最低的切入点。

2.2 报文结构:ICF、DA1、SA1这些字节到底填什么

FINS报文头和命令体的关系,可以用一张字节表讲清楚。发送一条内存区读命令时,前10个字节是固定格式的报文头,后面紧跟命令码和参数。

字节位置字段含义一般取值
0ICF信息控制标志0x80,表示需要响应
1RSV保留0x00
2GCT网关数0x02
3DNA目标网络号0x00,本网络
4DA1目标节点号PLC的FINS节点号
5DA2目标单元号0x00,CPU单元
6SNA源网络号0x00
7SA1源节点号本机FINS节点号
8SA2源单元号0x00
9SID服务标识每次请求自增或随机

这里最容易被忽略的是DA1和SA1。FINS网络中的“节点号”不是IP地址,而是PLC通信设置里的FINS节点编号。默认情况下,欧姆龙PLC会自动把节点号设置为IP地址的最后一段,例如192.168.250.1对应节点号1。如果PLC手动改过节点号,报文里的DA1也必须跟着改,否则PLC会直接丢弃请求。

ICF填0x80表示“本机请求,需要响应”。如果填0x00,PLC可能不回包。SID的作用是匹配请求和响应:在异步场景下,如果同时发多条命令,靠SID区分是哪一条的应答。即使在同步轮询场景里,SID每次加一也能避免旧响应干扰新请求。

有个细节值得注意:FINS节点号范围是1到254,0和255有特殊含义。如果你把DA1填成255,在某些PLC上会被当作广播,可能导致多个单元同时响应。为避免这种混乱,单机通信时DA1就老老实实地填PLC的实际节点号。

2.3 内存区读取命令0101:DM区、CIO区、WR区的代码对照

命令码0101是“内存区读”,它的参数由四个部分组成:内存区代码、起始地址(2字节)、起始位地址、读取数量(2字节)。起始位地址在读字数据时填0,只有读位数据时才需要指定位号。

常用内存区代码对照如下:

内存区代码(十六进制)地址说明
CIO区0xB0按字地址直接填
WR区0xB1W0对应地址0
HR区0xB2H0对应地址0
DM区0x82D0对应地址0,D100对应地址100

有一个常见的困惑:PLC软件里显示的D100,FINS命令里的起始地址到底填99还是100?答案是不加偏移的100,也就是0x0064。欧姆龙的DM区地址在FINS协议里就是从0开始按字编号的,D100就是地址100,不需要做减一操作。如果你看到别人代码里减一,那可能是他读取的是某个具体通道映射,不是通用的FINS编址。

这里再补一个计算示例:读D100到D104连续5个字,那么内存区代码0x82,起始地址高字节0x00、低字节0x64,位地址0x00,读取数量高字节0x00、低字节0x05。整个参数段是82 00 64 00 00 05,前面加命令码01 01,再加10字节帧头,就是一条完整的请求。

如果读CIO区,地址按通道号直接填。例如CIO100,地址就是100,转成十六进制0x0064。WR区里的W0对应地址0,W10对应地址10。HR区同理。这些区分在跨PLC类型移植时特别重要,不能因为都是“一个字地址”就混用代码。

FINS命令还有一个“内存区读”的约束:单次请求读取的字数上限通常是999个字,位上限是1999个位。如果你的采集点位很多,后面第五章会讲怎么分批。总之,协议层的结构就这么多,接下来看C#怎么把这些字节组织起来。

3. C#实现FINS读取示例:从构建帧到解析响应

3.1 类结构与连接初始化:UdpClient和超时

设计一个OmronFinsClient类,把所有协议细节收在类内部。外部调用方只需要传入IP、端口和节点号,不需要知道报文怎么拼。构造函数里创建UdpClient并连接目标,设置ReceiveTimeout,这样Receive超时会抛异常,方便统一处理重试。

using System; using System.Net; using System.Net.Sockets; public class OmronFinsClient { private readonly UdpClient _udp; private readonly byte _sa1; private readonly byte _da1; private byte _sid; public OmronFinsClient(string ip, int port, byte da1, byte sa1) { _udp = new UdpClient(); _udp.Client.ReceiveTimeout = 2000; _udp.Connect(ip, port); _da1 = da1; _sa1 = sa1; _sid = 0; } public void Close() { _udp?.Close(); } }

参数说明:ip是PLC的IP地址;port默认9600,和PLC以太网单元设置一致;da1是PLC的FINS节点号,通常等于IP最后一段;sa1是本机节点号,需要和PLC侧的“允许通信节点”设置保持一致。ReceiveTimeout单位是毫秒,这里给了2000,实际项目中建议按PLC响应能力调整到1000至3000毫秒之间。

初始化之后不要立刻关闭Socket,同一个UdpClient可以重复收发。UdpClient.Connect的意义在于后面Send和Receive不用每次传远程地址,这在单目标PLC的场景下代码更简洁。如果以后要支持多台PLC,建议一个PLC对应一个客户端实例,避免地址混淆。

这里有一个容易忽视的问题:ReceiveTimeout只对同步Receive方法生效,而且一旦超时会抛SocketException。如果你在异步方法里用ReceiveAsync,这个超时设置就不生效了。所以在采集线程里我倾向于用同步Receive加外层循环,代码清晰且好控制重试。

3.2 构建读取DM区的请求帧:字节填充与端序

读取DM区指定起始地址和数量的方法,核心是构建18字节的请求帧。代码里我把帧头、命令码和参数逐字节写出,注释标明每个字段的位置。

public byte[] BuildReadDmFrame(ushort startAddress, ushort count) { byte[] frame = new byte[18]; frame[0] = 0x80; // ICF:请求,需要响应 frame[1] = 0x00; // RSV 保留 frame[2] = 0x02; // GCT 网关默认 frame[3] = 0x00; // DNA 目标网络号 frame[4] = _da1; // DA1 目标节点号 frame[5] = 0x00; // DA2 目标单元号 frame[6] = 0x00; // SNA 源网络号 frame[7] = _sa1; // SA1 源节点号 frame[8] = 0x00; // SA2 源单元号 frame[9] = _sid++; // SID 服务标识 frame[10] = 0x01; // 命令码 0101:内存区读 frame[11] = 0x01; frame[12] = 0x82; // 内存区代码:DM区 frame[13] = (byte)(startAddress >> 8); // 起始地址高字节 frame[14] = (byte)(startAddress & 0xFF); // 起始地址低字节 frame[15] = 0x00; // 位地址,字读取填0 frame[16] = (byte)(count >> 8); // 读取数量高字节 frame[17] = (byte)(count & 0xFF); // 读取数量低字节 return frame; }

帧总长固定18字节,原因前面说过:10字节头加8字节命令参数。注意起始地址和数量都用大端序,也就是高字节在前。C#里直接强转低字节会得到相反顺序,所以务必用移位和与运算拆开高位和低位。

这段代码只负责构建帧,实际发送由调用方完成。我习惯把“构建帧”和“发送接收”分开,这样以后要加日志、加多内存区支持时,不需要改动帧拼装逻辑。如果你把帧构建和收发写在一个方法里,将来加协议扩展会越改越乱。

如果要读取CIO区或WR区,只需要把frame[12]换成对应的内存区代码,地址仍按字编号填。为了方便复用,可以把这个方法改成接收一个内存区代码参数,而不是硬编码0x82。下面这个重载就是通用版本:

public byte[] BuildReadFrame(byte areaCode, ushort startAddress, ushort count) { byte[] frame = new byte[18]; frame[0] = 0x80; frame[1] = 0x00; frame[2] = 0x02; frame[3] = 0x00; frame[4] = _da1; frame[5] = 0x00; frame[6] = 0x00; frame[7] = _sa1; frame[8] = 0x00; frame[9] = _sid++; frame[10] = 0x01; frame[11] = 0x01; frame[12] = areaCode; frame[13] = (byte)(startAddress >> 8); frame[14] = (byte)(startAddress & 0xFF); frame[15] = 0x00; frame[16] = (byte)(count >> 8); frame[17] = (byte)(count & 0xFF); return frame; }

这样ReadDm就变成了BuildReadFrame(0x82, startAddress, count)的一个特例。以后项目里要读W区、H区,不需要新写方法,传参就能解决。这也是方案里可复用的设计点。

3.3 响应解析:完成码、数据区、Int16转换

发送请求后同步等待响应。响应帧的前10字节头部与请求一致,第10、11字节是命令码,第12、13字节是完成码,第14字节开始才是真正的数据。

public short[] ReadDmValues(ushort startAddress, ushort count) { byte[] request = BuildReadDmFrame(startAddress, count); _udp.Send(request, request.Length); IPEndPoint remote = new IPEndPoint(IPAddress.Any, 0); byte[] response = _udp.Receive(ref remote); if (response[12] != 0x00 || response[13] != 0x00) { int errCode = response[12] * 256 + response[13]; throw new Exception($"FINS响应错误,完成码0x{errCode:X4}"); } int dataOffset = 14; short[] values = new short[count]; for (int i = 0; i < count; i++) { int high = response[dataOffset + i * 2]; int low = response[dataOffset + i * 2 + 1]; values[i] = (short)(high * 256 + low); } return values; }

这里有一个容易踩坑的点:响应中的数据是大端序,一个字是两个字节,高字节在前。直接用BitConverter.ToInt16会得到小端结果,必须自己组合。如果你读取的是无符号数,可以用ushort接收,但欧姆龙PLC的DM区也可以存放负数,所以用short更通用,由用户按实际数据类型解释。

响应帧的长度不是固定的,它会随读取数量变化。读取5个字时,响应总长度是10字节头+2字节命令码+2字节完成码+10字节数据,一共24字节。解析时不能假设固定长度,而是根据count计算数据部分长度。

完成码的常见情况不止0x0000一种。比如0x0105表示地址越界,0x1101表示内存区代码非法,0x0101表示目标节点不存在。排查时把完成码转成十六进制打印出来,对照手册里的错误码表,比逐字节猜要快得多。

还有一个细节:UDP层的Receive返回的字节数组长度并不一定等于响应帧长度。如果网络环境比较差,可能会收到被分片重组的包,但IP层会保证重组,所以应用层看到的通常是一整个UDP数据报。不需要自己做粘包处理,这是UDP相对TCP省事的地方。

3.4 写一个Demo:连续读取多组数据并显示

用一个控制台示例把整个过程串起来:连接PLC、读取D100开始的10个字、打印到屏幕。

using System; class Program { static void Main() { OmronFinsClient client = new OmronFinsClient( "192.168.250.1", 9600, da1: 1, sa1: 2); try { short[] values = client.ReadDmValues(100, 10); for (int i = 0; i < values.Length; i++) { Console.WriteLine($"D{100 + i} = {values[i]}"); } } catch (Exception ex) { Console.WriteLine($"读取失败:{ex.Message}"); } finally { client.Close(); } } }

实际项目里不会只在Main里调用一次,而是启动一个后台采集线程,按固定周期读取。这时要注意UdpClient的同步Receive会阻塞线程,超时是按ReceiveTimeout内部计时,不会因为外层循环而失效。每轮读取之间加一个Sleep或者用Timer控制节奏,避免把PLC请求频率拉太高。

这个Demo可以作为最小可运行例子,放到课设的“核心功能实现”章节里,配合抓包截图展示请求和响应。它虽然简单,但已经把“请求帧构建-发送-接收-解析-展示”的完整链路打通了。后面的问题排查和批量扩展都建立在它之上。

如果你还要写“内存区写入”功能,命令码改成0102,参数换成写入数据和数量就行,结构很接近。很多课设会要求既能读也能写,建议把读写两个方法一起封装,这样演示时可以先用写入功能给DM区置数,再从界面读出来验证。

4. 常见坑与排查:连不上、读错数、数据全零的几种翻车现场

4.1 现象:UDP请求发出后总超时,PLC没有任何响应

这是最常见的问题。PLC IP能ping通,但FINS请求石沉大海。原因多半是DA1节点号或ICF设置不对。我在一个项目里遇到过,PLC的FINS节点号被手动改成了10,而报文里仍然填IP最后一位1,结果PLC根本不认这个帧。

解决:先用PLC编程软件读出通信设置,确认FINS节点号到底是多少。报文头里DA1必须填PLC设置里的实际节点号。还有一个排查技巧:用抓包工具过滤PLC的IP和9600端口,看请求有没有到达,以及PLC有没有回包。如果只看得到请求看不到响应,基本就是节点号或协议参数问题。

4.2 现象:能收到响应,但完成码是0x1101或0x0105

0x1101表示内存区代码错误,0x0105表示起始地址或数量越界。出现这两个错误,先检查参数拼装顺序。比如内存区代码位置是frame[12],如果你把地址高位放到frame[12],PLC会认为内存区代码不对。

解决:写一个打印帧内容的调试工具,把请求帧逐字节以十六进制打印出来,对照手册逐字节检查。另外读取数量不能超过FINS协议限制,字数据一次最多999个字,超过会报地址越界。数量字段是两个字节,03E7就是999,别填到0x04D2这种超限值。

4.3 现象:完成码为0,但读回来的数据全是0或明显错位

这通常是地址换算问题。如果你把PLC里的D100对应成FINS地址99,读回来的就是D99之前的数据,很可能是0或者其他通道的值。我一般会用PLC软件往D100写入一个已知值,比如12345,然后上位机去读,看是否对得上。

解决:明确DM区地址直接用D编号,D100就是100,不要减一。如果是其他区,比如W区,直接按字编号填。CIO区如果读取位,还要注意位地址那一字节的编码,按手册来。数据错位还有一个原因:响应数据偏移算错。记住数据从第14字节开始,不是第15字节,这个最容易看走眼。

4.4 现象:偶尔超时,重试后又能成功

UDP本身不可靠,加上现场网络有交换机,广播风暴或带宽占用会导致丢包。不能用一次性超时判断通信故障。PLC扫描周期太忙时,FINS响应也可能变慢,甚至超过你设的2秒超时。

解决:封装一个Retry方法,发送失败或超时后重试两三次,重试间隔建议300到500毫秒。重试时SID要重新生成,避免PLC把重试报文当成重复帧。同时在代码里统计超时次数,连续失败超过比如5次再报通信故障,触发上位机告警,而不是第一次失败就弹窗。

4.5 现象:同一个UdpClient被多个线程同时调用,响应错乱

我见过把读取函数丢到后台线程,又在UI按钮点击时直接调,两个线程同时发请求,导致接收到的响应可能不是当前请求的。FINS协议本身没有请求关联机制,全靠SID匹配,但同步接收模式下,先发出的请求先等待,后发出的请求可能先收到响应。

解决:把读取操作锁住,或者用一个独立的采集线程串行发送请求,禁止多线程直接读写同一个UdpClient。如果一定要并发,就为每个线程创建独立连接,或者改用异步发送并用SID做字典匹配响应。简单方案永远是串行,吞吐量不够时再加并发。

5. 扩展设计:从单个读取到轮询采集与多区批量读取

5.1 轮询调度:多台PLC或多种内存区如何排队

真正交付的采集程序不是只读一次D100,而是要持续读多个区域、多台PLC。我一般会给每个PLC实例建一个请求队列,后台线程从队列里取请求,发送后等待响应,超时重试,完成后再取下一条。

using System.Threading.Channels; public class PollingWorker { private readonly Channel<Func<byte[]>> _queue; private readonly CancellationTokenSource _cts = new(); public PollingWorker(int capacity = 1000) { _queue = Channel.CreateBounded<Func<byte[]>>(new BoundedChannelOptions(capacity) { FullMode = BoundedChannelFullMode.Wait }); } public async Task StartAsync(Func<Func<byte[]>, Task> handler, CancellationToken token) { await foreach (var task in _queue.Reader.ReadAllAsync(token)) { await handler(task); } } public void Enqueue(Func<byte[]> task) { _queue.Writer.TryWrite(task); } }

用C#的Channel 比直接用Queue加锁更干净。消费端从Channel读出一个任务,执行请求,再把结果交给结果处理器。这样生产者只需要关心“要读什么”,不需要知道底层Socket什么时候能响应。FullMode设为Wait,队列满时生产方会等待,避免内存无限增长。

消费端串行处理的优点是天然保证同一个UdpClient不会被并发调用,彻底避免第四章说的响应错乱。如果只有一台PLC,一个消费线程就足够;如果有多台PLC,可以每台PLC启动一个PollingWorker,各自持有一个UdpClient实例。

5.2 批量读取:一次读多个字还是逐字读的效率对比

逐字读简单,但每读一个字都要走一次UDP请求-响应,假设一次往返20毫秒,读100个字要2秒。而一次读取100个字,总耗时依然是一次往返加解析。所以批量读是必须的。

欧姆龙FINS内存区读命令对字数据单次最多可以读999个字,位数据最多可读1999个位。但建议每包不要超过512个字,避免PLC扫描周期受影响。如果目标数据是连续的DM区段,就在一个请求里读完;如果不连续,考虑读一个大区间然后本地切片,前提是区间内没有不希望访问的特殊地址。

批量读取对帧构造没有额外要求,只要把count参数改大。解析时仍然按每2字节一个字去切。实测中,一次读200个字和读10个字的时间几乎一样,按这个特性设计采集周期可以把CPU和网络开销降得很低。

在实际项目里,我会把点位表按内存区合并成读取批次。比如DM区从D100到D500是一段连续工艺参数,那就合并成一个批次,数量401个字;但D1000到D1010是一段报警区,单独一个批次。合并规则通常是“连续地址且同一内存区”,减少请求数量,但也要避免把一个跨越特殊地址的大区间整体读回来,造成不必要的网络流量。

5.3 与上位机展示层对接:并发安全与数据刷新

读取结果不能直接丢给UI线程。桌面程序里,后台线程修改控件会抛跨线程异常;Web服务里,直接把数据对象放到静态字典也会产生并发问题。我通常在采集线程和展示层之间放一个ConcurrentDictionary,以“区名+地址”为键,保存最新值。

var latestValues = new ConcurrentDictionary<string, DataPoint>(); // 采集线程写入 latestValues["DM100"] = new DataPoint { Value = 12345, Timestamp = DateTime.Now }; // UI线程读取 if (latestValues.TryGetValue("DM100", out var point)) { textBox.Text = point.Value.ToString(); }

展示层定时器每隔几百毫秒去字典里取最新值刷新界面,不关心采集线程什么时候更新。这样即使PLC暂时无响应,界面也不会闪断,最多显示上一次的值。如果业务上要求“数据新鲜度”,可以在值对象里加时间戳,展示层超过阈值标记灰色。

这种设计与FINS协议本身解耦,无论读DM区还是CIO区,只需要保证写入字典时使用统一的键格式。以后增加报警判断、历史存储,也都从这份字典取数,不需要改动通信层。

5.4 点位表配置:把地址写进配置文件而不是写死在代码里

课设里把点位写死在代码里没问题,但交付给现场的项目,点位变更很频繁。我一般会把点位表放到一个JSON文件里,每一条包含内存区、起始地址、长度、数据类型和描述。

[ { "Name": "温度", "Area": "DM", "StartAddress": 100, "Count": 1, "DataType": "Int16" }, { "Name": "压力批次", "Area": "DM", "StartAddress": 101, "Count": 20, "DataType": "Int16" } ]

程序启动时加载这个配置,根据Area映射内存区代码,根据StartAddress和Count计算读取批次。这样做的好处是现场增减点位不需要改代码重新编译,只需要编辑JSON并重启采集服务。答辩时可以强调这个设计提升了系统的可维护性,也更容易让评审理解你的工程化意识。

6. 验收与答辩:怎么证明这套FINS通信是可靠的

验收不能只看“能读到数”。我会把验收分成三层:功能正确性、稳定性、异常恢复。

功能正确性测试用例可以用一张表列出来:

测试项操作预期结果
DM区读取PLC写入已知值0x1234,上位机读取D100返回4660
连续读取循环读取100次,每次间隔200ms全部成功无错包
地址越界请求读取超出DM区范围地址返回0x0105错误码
断网恢复拔掉网线5秒后恢复重试成功,数据恢复更新

稳定性方面,我要求至少连续运行8小时,统计丢包率和请求成功率。用代码计数器记录发送次数和超时次数,正常情况下网线不插拔,成功率应该在99.9%以上。如果高于万分之一的丢包,就要排查网络设备或PLC负载。这个数据可以直接写进课设报告,比“测试正常”四个字有说服力。

异常恢复最容易被忽略。在验收脚本里模拟PLC重启:读取过程中切断PLC电源,等待PLC重新上线后,采集程序要在没有人工干预的情况下自动恢复。这里的关键是重试逻辑不能因为连续失败就退出线程,而要在失败时保持连接对象,成功后自动恢复。我在实际项目中就吃过这个亏,第一次验收时PLC一断电重启,采集进程直接崩了,后来才加重试和异常捕获。

答辩时有一个很加分的技巧:准备一张Wireshark抓包截图,展示请求帧和响应帧的十六进制对照。你可以指着帧里的DA1和SA1说“这里目标节点号是1,源节点号是2,响应帧的完成码是0,数据区10个字和PLC里一致”,这比口头说“我用了FINS协议”有说服力得多。

另一个答辩切入点是“为什么自己拼协议而不是用现成库”。可以从无外部依赖、可控性好、数据可追溯三个角度讲,同时承认UDP丢包需要重试,体现你清楚它的边界。导师通常喜欢听你分析协议字段和异常场景,而不是听你描述写了多少行代码。

从那以后我每次做PLC通信,都会先在PLC里写一组已知数值,再开始写上位机解析代码。先证明帧格式对,再谈业务功能,能少走很多弯路。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询