台达PLC的RS232通信C#实战:从串口接线到Modbus RTU报文轮询
2026/9/14 19:47:08 网站建设 项目流程

简介:这份资源是一套面向PLC及上位机开发者的台达PLC RS232通信测试软件源码,基于C#实现,主要解决如何快速查看与验证台达PLC输入输出状态的问题。无论刚接触工控通信的新手,还是需要二次开发的工程师,都可以通过它快速上手串口通信与PLC交互逻辑。压缩包共37个文件,包含9个C#源文件、可执行exe、pdb调试文件、工程配置与资源文件等,形成完整的项目结构,压缩后仅269KB,轻量便于部署。该资源已有299人学习,体量虽小但结构清楚,既提供可直接运行的exe用于即时测试,也保留了Form1.cs、Program.cs等源码以及配置文件,便于阅读串口参数设置、通信报文和界面逻辑,适合作为学习PLC串口通信的入门范例或实际项目的移植基础。

1. 台达PLC的RS232 C#通信测试,先解决三根线再谈报文

调试台达PLC时,最容易被忽略的不是波特率,而是对方型号的通信模式。给台达DVP系列接上RS232线,用串口助手发01 03 00 00 00 02 C4 0B,十次有九次等不到应答。换线、换USB转串口、换软件都没用,最后发现是站号不对,或者是PLC的COM2口被设置成了Modbus ASCII模式。RS232通信测试软件的核心不是把数据发出去,而是把台达PLC侧的实际参数、报文格式、校验方式和C#代码逐一对应上。下面这套源码骨架,按串口参数、Modbus帧构造、CRC校验、轮询和日志几个模块拆开,直接改端口号和寄存器地址就能用在真机上。

2. 台达PLC的RS232接线与通信参数选择

2.1 RS232交叉接法才是第一道关卡

台达DVP系列PLC的COM2口通常引出DB9公头,或者通过编程电缆转成DB9。PC侧一般是USB转RS232线,驱动装好后映射成COM3、COM4这类端口。RS232是单端电平,两机通信必须交叉:PLC侧TXD接PC侧RXD,PLC侧RXD接PC侧TXD,地线接GND。很多现场用的所谓“RS232直通线”其实已经是交叉线,再接一个交叉头就会把收发对调,表现出来的故障是发送方能看到数据,接收方永远收不到完整帧。

不建议只看接头外形判断,用万用表蜂鸣档量一下两端针脚更可靠。将串口头两端分别量TXD、RXD、GND三根线的通断关系,确认是2-3交叉、3-2交叉、5-5直连,再插到设备上。USB转RS232线靠近PC端是公头还是母头也会影响接线,稳妥做法是先接一个串口延长头,再把PLC侧线缆插上,避免反复拔插PLC端子。

提示:RS232线长超过15米或经过变频器柜时,优先加磁环,并让信号线远离动力线。乱码不一定来自波特率,先怀疑线缆屏蔽。

2.2 台达PLC的RS232通信参数怎么定最稳

台达大多数DVP系列支持标准Modbus RTU和Modbus ASCII。在C#上位机里推荐用RTU,原因很直接:RTU用CRC16校验,ASCII用LRC校验且每字节拆成两个ASCII字符,帧长翻倍,测试工具处理起来更麻烦。参数表如下:

参数项测试工具推荐值说明
波特率9600对短距离调试足够,抗干扰比115200好;产线大量轮询时可改为38400或更高
数据位8Modbus RTU固定8位,ASCII才需要配合7位
停止位1常用默认,不需要设2位
校验位NoneRTU本身带CRC,帧错误能检出;如PLC侧强制Even,两边必须一致
站号1台达PLC站号通过工程参数或特殊寄存器设置,常见默认是1
超时1000ms测试阶段给1秒足够,连续轮询时建议降到200ms

注意台达部分机型存在“用COM2跑内部专用协议”的设置,一旦打开,标准Modbus帧会被忽略。测试前应进入PLC编程软件确认COM2协议选的是MODBUS RTU,而不是LINK或出厂默认的专用格式。站号也和PC机不同,不是自动识别的,C#发出去的地址必须和PLC侧一致。

2.3 在PLC侧确认通信配置的落地步骤

  • 用台达编程软件打开PLC工程,找到串行口设置页面,选择COM2。
  • 将通信协议设为MODBUS RTU,波特率设为9600,数据格式设为8-N-1。
  • 站号设为1,和测试工具发送地址保持一致。
  • 写回PLC后重新上电,让通信参数重新加载。
  • 不经过编程软件直接发Modbus帧,只能做被动测试,改不了PLC侧参数。

修改通信参数后没有重新上电,是调试串口通信最容易踩的坑,表现为参数明明保存了却始终不响应。上电后先观察PLC上的通信指示灯,有数据收发时会闪烁,完全不闪大概率是RS232线没通。

3. C#串口库SerialPort的封装与线程安全

3.1 打开台达PLC串口的最小稳定代码

C#里操作RS232最直接的是System.IO.Ports.SerialPort。测试工具不需要引入第三方组件,.NET Framework 4.x或者.NET 6都可以跑通。打开串口的代码要处理两个问题:端口已经打开、端口被其他程序占用。下面这段封装可以直接放到窗体类里:

using System; using System.IO.Ports; using System.Windows.Forms; public class PlcSerial { private SerialPort _sp; public bool Open(string portName, int baudRate) { if (_sp != null && _sp.IsOpen) Close(); _sp = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One) { ReadTimeout = 1000, WriteTimeout = 1000 }; try { _sp.Open(); return _sp.IsOpen; } catch (UnauthorizedAccessException) { MessageBox.Show("串口被占用,请关闭其他串口助手"); return false; } catch (IOException ex) { MessageBox.Show("串口不存在或已被拔出:" + ex.Message); return false; } } public void Close() { if (_sp == null) return; if (_sp.IsOpen) _sp.Close(); _sp.Dispose(); _sp = null; } }

ReadTimeoutWriteTimeout分别控制读写等待时间,测试阶段设为1000ms,避免PLC无响应时界面卡死。UnauthorizedAccessException代表串口被串口助手或组态软件占用,IOException则多出现在USB转串口线拔掉或驱动异常时。不要把Open()包在using里,因为上位机工具要长时间保持端口打开。

3.2 DataReceived事件与轮询线程的取舍

很多人会直接在SerialPort.DataReceived事件里处理应答,这个做法在纯手动发送场景没问题。一旦做连续轮询,PLC每帧应答到达时间不固定,事件触发和下一次请求发送会竞争同一个串口,容易出现半包。常见做法是放弃事件驱动,改成专用轮询线程:线程里先发请求,再同步读应答,读完后再发下一帧,整个收发过程串行化。

如果非要用DataReceived,也要注意事件回调运行在线程池线程,不能直接修改TextBoxDataGridView,需要BeginInvoke切换到UI线程。测试工具是给人看的,日志和界面更新频率控制在每100ms一次就够了,不需要在每次应答里刷新所有控件。

3.3 关闭串口时最容易崩溃的隐藏问题

关闭串口时有一种很隐蔽的异常:轮询线程还在Read()里阻塞,UI线程调用Close(),线程会抛出InvalidOperationExceptionObjectDisposedException。正确顺序是先取消轮询,再关闭串口。可以用一个bool标志位让轮询循环退出,或者用CancellationToken。调试时为了让工具更健壮,可以在轮询方法里catch掉InvalidOperationException,避免偶发崩溃:

try { _sp.Write(frame, 0, frame.Length); _sp.Read(buffer, 0, buffer.Length); } catch (InvalidOperationException) { // 串口已关闭,轮询线程直接退出即可 }

这段代码让测试工具在关闭串口时不会弹异常。注意catch里什么都不做是为了让线程干净退出,如果后续还要上报状态,可以设置一个枚举字段标记通信状态。

4. C#构造台达PLC报文:CRC校验与解析

4.1 台达PLC的RS232串口协议报文解析:一帧Modbus RTU

在C#里发Modbus RTU帧前,先明确RTU帧结构。读D区寄存器最常见的就是功能码03,读取台达D0开始的连续寄存器。帧格式如下:

字段长度示例值说明
站号1字节0x01对应PLC侧COM2站号
功能码1字节0x0303是读保持寄存器
起始地址2字节0x0000寄存器地址高字节在前
寄存器数量2字节0x0002一次最多建议125个
CRC162字节0xC40B低字节在前发送

一个完整的读请求是01 03 00 00 00 02 C4 0B。很多网友问为什么不是0B C4,这是Modbus RTU的固化规则:CRC先传低字节再传高字节。字节序错了,台达PLC会丢弃整帧。

4.2 CRC16-Modbus逐位计算实现

CRC16-Modbus使用多项式x^16 + x^15 + x^2 + 1的反射形式0xA001,初始值为0xFFFF。逐位计算比查表法代码更短,学习阶段方便验证,实测在串口速率下性能差可以忽略。C#实现如下:

static byte[] GetCrc(byte[] data) { ushort crc = 0xFFFF; foreach (byte b in data) { crc ^= b; for (int i = 0; i < 8; i++) { if ((crc & 0x0001) != 0) crc = (ushort)((crc >> 1) ^ 0xA001); else crc >>= 1; } } return new byte[] { (byte)(crc & 0xFF), (byte)(crc >> 8) }; }

返回值就是按Modbus顺序排好的低字节和高字节。需要注意crc >> 1的结果默认是int类型,乘位运算必须强转回ushort,否则连续移动8位后高位数据可能被截断。计算CRC时只计算除CRC后的所有字节,把CRC追加到帧尾即可:

static byte[] BuildFrame(byte station, byte func, ushort addr, ushort count) { byte[] body = { station, func, (byte)(addr >> 8), (byte)(addr & 0xFF), (byte)(count >> 8), (byte)(count & 0xFF) }; List<byte> frame = new List<byte>(body); frame.AddRange(GetCrc(body)); return frame.ToArray(); }

addr >> 8取高8位,addr & 0xFF取低8位,完全按Modbus高位在前的顺序。寄存器地址从0x0000铺开,对应台达D0寄存器,0x0001对应D1,依次类推。功能码03读的是保持寄存器,台达D区一般映射到这里。

4.3 报文应答解析与异常码处理

PLC正常应答格式是站号 + 功能码 + 字节数 + 数据 + CRC。例如读2个保持寄存器成功,应答可能是01 03 04 12 34 AB CD 校验,其中04表示后面跟随4个字节数据。解析时先校验CRC,再按两个字节合成一个寄存器值:

static List<ushort> ParseHoldRegisters(byte[] resp) { if (resp.Length < 5) return new List<ushort>(); int dataLen = resp[2]; var values = new List<ushort>(); for (int i = 3; i < 3 + dataLen; i += 2) { values.Add((ushort)((resp[i] << 8) | resp[i + 1])); } return values; }

如果响应长度只有3个字节,表示PLC返回了异常帧,比如01 83 02。其中83是功能码03加上最高位0x80得到的,02是异常码。C#工具应该把异常码单独显示,而不是直接当超时处理。常见异常码含义如下:

异常码含义调试方向
0x01非法功能码当前站不支持该功能码
0x02非法数据地址寄存器地址超出范围
0x03非法数据值写入值超限或数量为0
0x04从站设备故障PLC内部通信处理器未就绪
0x06从站忙重新发送上一帧

台达部分旧型号对超出范围的寄存器地址会静默丢弃,表现为完全无响应而不是返回异常码,这在测试时要区分:无响应优先查站号和地址范围,返回异常码则说明通路已经打通。

5. 台达PLC通信测试软件源码的轮询、界面与日志

5.1 测试工具界面布局与控件规划

一套可运行的上位机测试界面,至少要包含四个区域:串口参数区、报文显示区、连续轮询区和日志区。不要试图把所有参数塞到一个窗体里,测试工具的定位是快速验证,界面越直接越好。控件规划可以参考下表:

控件类型控件名用途
ComboBoxcmbPort枚举本机COM口
ComboBoxcmbBaud波特率选择
ButtonbtnOpen打开/关闭串口
NumericUpDownnudStationPLC站号
NumericUpDownnudRegAddress起始寄存器地址
NumericUpDownnudCount读取寄存器个数
ButtonbtnReadOnce手动读取一次
ButtonbtnStartPolling启动或停止连续轮询
DataGridViewdgvValues显示每个寄存器的地址和值
TextBoxtxtLog显示收发十六进制报文

枚举串口用SerialPort.GetPortNames(),把返回的字符串数组填充到cmbPort中。注意拔掉USB转串口后再插入,端口号可能变化,每次打开串口前都重新刷新一次列表。

5.2 连续轮询的完整代码骨架

连续轮询不要在Button点击事件里用while(true),那样UI线程会被阻塞。简单做法是用Task.Run在线程池执行,用CancellationToken退出。下面的轮询逻辑包含发帧、读响应、解析、更新界面,能直接放在源码主体里:

private async void btnStartPolling_Click(object sender, EventArgs e) { if (_pollTask != null && !_pollTask.IsCompleted) return; var cts = new CancellationTokenSource(); _pollTask = Task.Run(() => PollLoop(cts.Token)); } private void PollLoop(CancellationToken ct) { byte station = (byte)nudStation.Value; ushort regAddr = (ushort)nudRegAddress.Value; ushort count = (ushort)nudCount.Value; var frame = BuildFrame(station, 0x03, regAddr, count); while (!ct.IsCancellationRequested) { try { byte[] resp = RequestResponse(frame); List<ushort> values = ParseHoldRegisters(resp); BeginInvoke(new Action<List<ushort>>(UpdateGrid), values); } catch (TimeoutException) { BeginInvoke(new Action<string>>(AppendLog), "读取超时"); } ct.WaitHandle.WaitOne(200); } }

RequestResponse内部先写请求帧,再根据应答头读取完整响应。这里给每条请求留200ms周期,适合9600波特率下的常规调试。如果PLC现场有多个从站且每个站响应时间不同,轮询间隔应通过界面暴露给用户调节,不要写死在代码里。用BeginInvoke更新DataGridViewTextBox,比直接调用线程安全的第三方控件更可控。

提示:Count参数在C#的NumericUpDown里是int类型,转成ushort时要检查是否超过125,超过要提示用户拆成多次读取。一次读125个寄存器长度491字节,已经接近RS232在9600波特率下的极限稳定帧长。

5.3 报文日志记录:把每次收发写进文件

日志是排查RS232乱码和丢帧的抓手。测试软件里最常见的是把发送和收到的原始十六进制追加到一个文本文件,加时间戳。实现如下:

private void AppendLog(string direction, byte[] data) { string hex = BitConverter.ToString(data); string line = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} {direction}: {hex}"; File.AppendAllText("plc_comm.log", line + Environment.NewLine, Encoding.ASCII); BeginInvoke(new Action<string>>(s => txtLog.AppendText(s + Environment.NewLine)), line); }

写文件时用Encoding.ASCII而不是默认UTF8,可以避免换行符被多写字节。BitConverter.ToString会把字节数组转成01-03-00-02这种格式,和Modbus调试助手看到的十六进制格式一致,复制到串口助手里可以直接对比。每次发送和接收都调用AppendLog,一帧请求配一帧响应,日志行数的对应关系就一目了然。

如果测试时间很长,建议对日志文件做大小限制,比如超过5MB就重命名成plc_comm_old.log再新建新文件,避免单个文件过大导致写入越来越慢。

6. 真机联调中的RS232乱码与报文解析排错技巧

6.1 先用回环测试排除串口线和驱动问题

接上台达PLC之前,先把RS232线对侧的TXD和RXD短接,在C#工具里发送一帧AA 55,正常能原样收到AA 55。收不到说明串口驱动、COM口号或线缆方向有问题,和PLC无关。如果收到的是乱码,比如3F 3F,大多是波特率不匹配或USB转串口芯片异常。这一步可以把排查范围缩小一半,不要跳过。

6.2 一表对照RS232乱码、超时和异常响应

联调时的常见现象和处理方向集中在下面这张表里:

现象最可能原因排查动作
完全无响应站号不匹配、PLC侧协议不是Modbus RTU用编程软件确认COM2配置
乱码波特率、数据位、校验位不一致把测试工具和PLC侧参数逐项对比
偶发超时RS232线未屏蔽、线太长缩短线缆间距,或换带屏蔽层串口线
响应CRC错误帧头粘包、USB串口缓存断帧每次请求前清空接收缓冲区
返回非法数据地址寄存器地址超出该系列PLC范围换一个已知存在的D区再试

其中“清空接收缓冲区”在C#里先调用_sp.DiscardInBuffer()再发送请求,能解决不少USB转串口带来的残留数据问题。注意发送应答之间的间隔要大于RTU要求的静默时间,通常软件里加10ms到20ms的Thread.Sleep就足够。

6.3 连续轮询时的周期和寄存器数量调优

台达PLC对单帧读取数量有限制,一次读125个寄存器是最稳妥的上限。数量超过上限时PLC会拒绝处理,或者只返回一部分数据。调试工具里如果一次要读几百个寄存器,正确的做法是分成多个帧,每帧读128个地址范围以内。轮询周期从200ms起步,先确认每帧都稳定应答,再慢慢降到20ms,观察是否有丢帧。当发现丢帧时不要只调波特率,先看日志里的时间戳间隔,如果两次请求间隔小于PLC实际响应时间,说明上位机发送频率超过了从站处理能力。把响应时间和周期加在一起,才是合理的轮询间隔。

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

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

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

立即咨询