前阵子调一个现场项目,用三菱FX5U去读一台老式称重仪表,仪表只支持MODBUS ASCII协议。折腾通信格式、LRC校验、RS指令时序的那几天,踩了不少坑,也把FX5U走RS485做ASCII帧通信的整个流程彻底捋顺了。这篇就把我的使用记录完整整理出来,从协议机制、硬件接线、GX Works3参数设置,到PLC程序组帧、发送接收时序,再到故障排查,一次说清楚。文章比较长,建议收藏了当操作手册用,适合现场调试工程师、自学PLC通信的朋友,也适合手里正好有FX5U要对接非标串口设备的人。
1. 项目背景与通信方案选型
1.1 这个项目要解决什么问题
现场情况很简单:一台FX5U-32MR/ES作为控制核心,需要定时读取称重仪表当前的重量值、零点漂移数据,偶尔还要远程写一次零点校准指令。仪表通讯口只支持RS485,协议只支持MODBUS ASCII。控制柜到仪表之间的距离大概有80米,现场已经预埋了一根两芯屏蔽电缆,所以直接利用现成的485总线,不再增加任何硬件布线。
这里有个关键背景:仪表说明书里明确写了“MODBUS ASCII,默认9600,8数据位,无校验,1停止位”。也就是说,我在PLC侧要做的不是选一个现成的“ASCII通信功能”,而是手动构造符合MODBUS ASCII规则的帧,通过FX5U的串口发出去,再解析返回的ASCII帧。
很多新手一开始会疑惑:三菱FX5U不是自带MODBUS通信指令吗?直接MBUS读写不就完了?确实,FX5U有成熟的MODBUS指令,但那套指令在处理ASCII模式时灵活性有限,尤其面对一些非标仪表(比如寄存器地址非标准、返回数据带额外字节、多帧连发等场景),自由组帧的RS指令方式反而更可靠。这也是我选RS指令来做的根本原因——设备的协议越老、越非标,越需要把帧的控制权握在自己手里。
1.2 ASCII模式和RTU模式怎么选
MODBUS协议有两个串行传输模式:RTU和ASCII。两者的物理链路都是RS485,差别全在数据帧的编码和校验方式上。
RTU模式直接用二进制字节传输,数据紧凑,同样的内容RTU帧只有8个字节左右,ASCII帧要17个字节,传输效率差了一倍多。RTU采用CRC16校验,检错能力更强。但RTU有一个很“矫情”的地方:它要求帧内字节间隔必须小于1.5个字符时间,整个帧必须连续发送,中间断了就判定为帧错误。在一些老的设备、干扰大的现场,这个时序要求很容易被打破。
ASCII模式把每一个十六进制字节展开成两个ASCII字符,比如0x1B变成'1'和'B'两个字符的ASCII码,再在帧头加一个冒号“:”,帧尾加回车换行。校验方式是LRC纵向冗余校验,算法比CRC简单得多。最舒服的是帧间隔要求宽松,MODBUS协议规定ASCII帧之间的空闲时间甚至可以到1秒,这对CPU负载高、扫描周期不稳定的PLC来说非常友好。
举个直观对比,同样是读保持寄存器第0号地址,长度1个寄存器:
| 项目 | RTU帧 | ASCII帧 |
|---|---|---|
| 原始字节序列 | 01 03 00 00 00 01 CRC_L CRC_H | 3A 30 31 30 33 30 30 30 30 30 30 30 30 31 46 42 0D 0A |
| 帧文本形式 | 不可见乱码 | :010300000001FB\r\n |
| 总字节数 | 8 | 17 |
| 校验 | CRC16 | LRC |
| 帧间隔要求 | 严格1.5字符时间 | 放宽到1秒内 |
如果你问现场工程选哪个,我的经验是:设备支持哪个就选哪个。设备两种都支持的话,短距离、干扰小的柜内通信可以用RTU;走长线、电磁环境差、或者后期要频繁调试抓包分析,ASCII有肉眼可读的巨大优势。像前面说的那台仪表,固件只支持ASCII,那就没得选,踏踏实实把ASCII帧做好。
1.3 为什么用RS指令而不是直接调功能块
FX5U的串口通信有两种路线:一种是配置专用的MODBUS通信功能,系统自动生成RTU帧或者通过专用指令读写,另一种是使用RS指令做无协议的自由口通信。
专用功能块的特点是省事,配置好通信协议后直接输入寄存器地址、数据长度,PLC内部自己组装RTU帧、算CRC。但它有个问题:针对ASCII模式,实际调试中可控制的余地小,帧头冒号、结束符、LRC的生成细节没有完全开放,遇到非标设备时反而成了障碍。
RS指令则完全是“裸”的:你往发送缓冲区里放什么字节,485口就原样发什么字节;接收缓冲区里收到什么,你就能读到什么。这个特性放在ASCII帧通信里太好用了——我可以精确控制帧头、帧尾、每一个ASCII字符、LRC校验值,也可以按设备要求加任何自定义字节。所以我的方案就是:FX5U用RS指令 + 手动组帧 + 手动计算LRC,完整实现MODBUS ASCII从站通信逻辑。
2. RS485 ASCII通信协议细节拆解
2.1 帧结构:从冒号到回车换行
MODBUS ASCII帧的标准结构是:
: 起始符,固定为冒号的ASCII码0x3A 地址 从站地址,2个ASCII字符,比如01 功能码 2个ASCII字符,比如03(读保持寄存器)、06(写单个寄存器) 数据区 变长,2个ASCII字符表示一个数据字节 LRC校验 2个ASCII字符 回车换行 CR LF,即0x0D 0x0A
所以完整的帧文本长这个样子:
:010300000001FB\r\n
这一个文本串在485线上实际发送的字节序列是:
3A 30 31 30 33 30 30 30 30 30 30 30 30 31 46 42 0D 0A
注意,其中字符“0”“1”“3”“F”“B”都是字符本身的ASCII码,不是十六进制数值。比如“01”这个地址,在485线上是0x30、0x31两个字节,而不是0x01一个字节。这是ASCII模式最容易搞混的地方:我们PLC程序里存的、发的是ASCII码,不是原始十六进制数据。
2.2 LRC校验算法与手工计算
LRC的全称是Longitudinal Redundancy Check,纵向冗余校验。算法步骤是:把帧中除去起始冒号和结尾CR LF之外的所有原始字节(也就是地址、功能码、数据区,按十六进制值计算)累加求和,然后取补码,忽略超过一个字节的进位。
还是拿读寄存器举例。原始数据字节是:
01 03 00 00 00 01
逐个求和:
01 + 03 + 00 + 00 + 00 + 01 = 05
LRC校验值等于对05取二进制补码:
0x100 - 0x05 = 0xFB
所以LRC就是FB,帧末尾写“FB”。再验证一个写单个寄存器的例子。写寄存器地址0000,写入值0001,功能码06:
原始字节:01 06 00 00 00 01
求和:01 + 06 + 00 + 00 + 00 + 01 = 08
LRC:0x100 - 0x08 = 0xF8
完整帧文本就是:
:010600000001F8\r\n
实际写程序的时候,很多设备返回的数据也会带LRC,比如功能码03的响应帧可能长这样:
:010302007B7F\r\n
解析过程就是:收到17个字符,先把16个字符(地址、功能码、数据、LRC)两两合成原始字节,对前面的字节做同样的LRC计算,再和收到的校验字节比对。一致则帧有效,不一致则丢弃。
有个细节特别值得注意:LRC计算是对十六进制字节值求和,不是对ASCII字符值求和。你收到一帧“010300000001FB”,千万不能直接拿0x30、0x31这些ASCII码去加,而是先把“01”转成0x01,“03”转成0x03,算完再做补码。
2.3 典型读取与写入帧
现场最常用的是功能码03和06。我把它们整理成一张速查表,方便现场对照组帧。
| 功能 | 功能码 | 帧格式示例 | 数据区含义 |
|---|---|---|---|
| 读保持寄存器 | 03 | :010300000001FB\r\n | 从地址0x0000开始读1个寄存器 |
| 写单个寄存器 | 06 | :010600000001F8\r\n | 写0x0001到地址0x0000 |
| 读输入寄存器 | 04 | :010400000001F5\r\n | 从地址0x0000开始读1个输入寄存器 |
要特别提醒的是,不同仪表的寄存器地址映射差别很大。有的仪表说明书直接写寄存器地址,有的写PLC地址(比如40001、40010这种),需要减去起始偏移才是帧里的地址。前期最好先用串口调试助手把帧发给仪表,确认返回数据正确,再在PLC里写程序,能省掉很多来回烧程序的痛苦。
2.4 从HEX到ASCII的转换逻辑
由于RS指令发的是ASCII字节,程序里必须先做进制转换。转换规则是:一个原始字节拆成高四位和低四位,分别映射成对应的ASCII码字符。
举个例子,原始字节0x01:
高四位0x0 -> 字符'0' -> ASCII 0x30 低四位0x1 -> 字符'1' -> ASCII 0x31
所以0x01转换成两个ASCII字符后占两个字节:0x30 0x31。同理:
0xFB的高四位0xF -> 字符'F' -> 0x46,低四位0xB -> 字符'B' -> 0x42,转换后是0x46 0x42。
这个转换看起来简单,但在PLC里做的时候要注意:三菱的十六进制转ASCII通常是把一个16位寄存器D的低8位和高8位分别转换。如果D0里放的是0x01 0x03两个字节,转换后要占到D0的低8位和高8位两个十六进制字符位。实际操作中我习惯用一个循环子程序,按字节逐个处理,避免漏掉高字节或低字节的转换。
3. 硬件准备、接线与GX Works3参数配置
3.1 硬件扩展与安装位置
FX5U本体并没有标配RS485物理接口(部分FX5UJ型号内置了485),所以要跟仪表通信,最常用的方案是加装一块FX5-485ADP适配器模块。这个模块体积不大,安装在FX5U主机的左侧扩展口上,从上往下卡进去后用固定螺丝锁紧。装好之后,CPU会自动识别,不需要额外的地址分配。
我的项目里用的就是FX5-485ADP。配线时把模块端子的SDA、SDB、RDA、RDB、SG和仪表的对应端子接好。注意FX5-485ADP的端子排是弹簧压接式的,用小一字螺丝刀捅一下端子侧面的压片,把剥好的线芯插进去,松手就卡住了,不需要拧螺丝。
3.2 接线要点:SDA/SDB、RDA/RDB、共地与终端电阻
485接线有四个信号端子容易让人绕晕:SDA、SDB、RDA、RDB。SDA和SDB是485发送差分对(对应仪表的接收端),RDA和RDB是接收差分对(对应仪表的发送端)。接线时一定要记住“交叉接”原则:PLC的SDA接仪表的A(或D+),PLC的SDB接仪表的B(或D-);PLC的RDA接仪表的B,PLC的RDB接仪表的A。很多设备端子标的是A/B而不是D+/D-,这时候就要看说明书上的极性定义。
SG端子(信号地)一定要接。我在现场遇到过一个问题:485两根线都接对了,但通信时好时坏,数据隔几分钟就乱一次。排查到最后发现是仪表和控制柜没有共地。后来把SG端子跟仪表的地接在一起,通信立刻稳定了。别小看这根地线,它决定了整个485总线的参考电位,不共地的话长距离传输很容易因为电位漂移产生乱码。
另外就是终端电阻。总线上如果只有两个设备,距离几十米,通常可以不加。但假如总线上挂了多个从站,或者距离超过100米,建议在物理线路两端的设备端各并一个120Ω终端电阻。一个常见的误区是只在PLC侧加,仪表侧不加,这样阻抗不匹配,信号反射仍然存在。
我这边80米距离、485总线只有PLC和仪表两台设备,实际调试时没有加终端电阻,通信也稳定。但如果你发现波形边沿震荡、偶发通信超时,优先考虑加终端电阻。
3.3 GX Works3参数设置逐项说明
FX5U的串口参数在GX Works3里的路径是:工程导航窗口 -> [参数] -> [模块信息] -> [485串口] -> [通信格式设置]。打开后有几个关键项。
波特率:仪表说明书是9600,这里就选9600。两边不一致是通信故障第一号原因,现场不要想当然地觉得“9600太慢,我设快一点”,设备不支持就是不行。
数据位、校验位、停止位:项目里仪表是8数据位、无校验、1停止位,简称8N1。有些设备要求偶校验,比如8E1,那就要同步改。有些人会问:MODBUS ASCII是不是强制无校验?并不是,校验位完全看设备配置。
站号设置:这个是在PLC侧设定本机的站号,做从站通信时才有意义,做主站时也可以不管。但为了统一管理,我一般把它设成和仪表不冲突的地址,比如2。
帧间隔:FX5U的通信格式设置里有帧间隔相关的选项。ASCII模式本身对帧间隔要求宽松,但设备说明书可能对“接收空闲时间”有额外要求,按说明书设就好。如果这项设置不明确,保持默认即可。
参数修改完成后需要断电重启PLC才能使通信参数完全生效,热复位有时不起作用。这个细节我踩过坑:改完参数直接在线监控,发现发送端口状态没变,最后断电再上电才正常。
4. PLC程序设计:组帧、转换与收发时序
4.1 通信数据区规划
PLC程序设计的第一步是规划数据区。我的习惯是把通信数据分成四个区域:原始帧区、发送帧区、接收帧区、控制字区。
原始帧区存放要发送的原始十六进制字节序列,比如要读取仪表重量,我先在D10~D15里放01 03 00 09 00 01,这些是组帧前的原始数据。然后再由程序计算LRC,并把所有字节转换成ASCII码,放到发送帧区D100开始的空间里。
这里有一个重要经验:RS指令发送的缓冲区存的是ASCII字符,不是原始十六进制。如果把原始字节直接放进发送缓冲区,仪表收到的就是乱码帧,完全没有冒号、字符数字,通信必然失败。
接收缓冲区D200开始,长度根据响应帧可能的最大长度预留。我这次预留了64个字节,实际每次只收17个字节。控制字区用D0存发送长度,D1存接收长度,方便RS指令直接引用。
注意:发送缓冲区和接收缓冲区不要重叠。我曾经图省事把发送和接收都放在D100开始的区域,结果PLC一边收一边发,数据互相覆盖,查了很久才发现是缓冲区冲突。
4.2 HEX转ASCII与LRC计算子程序
把原始字节转换成ASCII字符,我习惯用子程序来做。核心逻辑是:对缓冲区里每个字节,取高四位转成一个ASCII码,取低四位转成一个ASCII码,连续写入发送帧区。
判断逻辑可以这样写:
// 伪代码示范:以16位数据D10为输入,输出两个ASCII字节到D100 // 实际FX5U用梯形图或ST都行,这里展示核心逻辑 FOR i := 0 TO n-1 DO // 取当前原始字节 rawByte := SHR(D10+i, 8) AND 16#00FF; // 取高字节 // 高四位 highNibble := SHR(rawByte, 4) AND 16#0F; IF highNibble <= 9 THEN tempHigh := highNibble + 16#30; // 0-9 -> '0'-'9' ELSE tempHigh := highNibble + 16#37; // A-F -> 'A'-'F' END_IF; // 低四位 lowNibble := rawByte AND 16#0F; IF lowNibble <= 9 THEN tempLow := lowNibble + 16#30; ELSE tempLow := lowNibble + 16#37; END_IF; // 写入发送帧区,一个原始字节占两个ASCII字节 D100 + i*2 := tempHigh; D100 + i*2 + 1 := tempLow; END_FOR;LRC计算相对简单,对原始帧区的所有字节累加,忽略进位后取补码。实际程序里我加了边界保护:如果原始帧长度是0或者超过上限,直接返回错误。
4.3 RS指令的发送与接收时序
FX5U的RS指令基本沿用三菱FX系列的用法,指令格式大致是:
RS D100 D0 D200 D1其中D100是发送缓冲区首地址,D0存发送长度;D200是接收缓冲区首地址,D1存接收长度。发送长度单位是字节数,这里要注意:对于上面的ASCII读帧,发送缓冲区里包含冒号、地址、功能码、数据、LRC、CR LF,一共17个字节,所以D0要赋值17。
收发控制依赖通信标志位。沿用FX3U时代的习惯,发送请求用M8122(或者FX5U里的SM8122,具体看固件定义),置位后PLC把发送缓冲区内容发出去,发送完成自动复位。接收完成标志是M8123,收到一帧数据后自动置位,程序处理完数据后要手动复位才能继续下一次接收。
时序流程是关键:
第一步,初始化。上电后设置通信参数,把D0置为要发送的长度,清空接收缓冲区,复位M8123。
第二步,组帧。把要读的寄存器地址、长度写入原始帧区,计算LRC,转换成ASCII码填入发送缓冲区,更新D0为17。
第三步,置位M8122发起发送。这时RS指令开始工作,串口按缓冲区内容逐字节发射。
第四步,等待响应。发送完成后等待M8123置位,意味着接收完成。这里必须加一个超时判断,我用一个100ms的定时器做看门狗:如果100ms内M8123没有置位,就判定超时,清空缓冲区、复位M8123,下次扫描重新发起通信。为什么选100ms?因为9600波特率下,17个字节的ASCII帧传输时间大约17.7ms,加上设备处理时间,100ms足够。太短容易误判,太长会拖慢整个控制周期。
第五步,解析数据。M8123置位后,D200开始的接收缓冲区里就是仪表返回的ASCII码,按“两两转十六进制”的规则还原,校验LRC,取出重量值,然后复位M8123。
整个流程用梯形图写出来会有几十行,但我建议把发送、等待、接收、解析做成一个子程序块,用步进梯形图或者状态字的方式管理。这样主程序只需要一个触发信号,调用通信子程序,不用把时序逻辑散落在梯形图各处。
4.4 接收数据处理与异常恢复
接收返回帧的解析比发送更考验细节。仪表返回的ASCII帧,比如:
:010302007B7F\r\n
实际收到的字节是3A 30 31 30 33 30 32 30 30 37 42 37 46 0D 0A,共17个字节。处理逻辑是:
先检查首字节是不是冒号0x3A,尾字节是不是0x0D 0x0A,如果不是,整帧丢掉。
然后把中间的14个字节按两位一组转换成7个十六进制字节。正常情况下,第1个字节是从站地址,第2个是功能码,第3个是数据字节数,后面是数据,最后一个是LRC。
对前面6个字节(地址、功能码、字节数、数据)做LRC计算,和收到的LRC第七字节比对。一致才认为数据有效,取出重量值;不一致就标记通信故障。
异常恢复的逻辑同样重要。485通信的环境往往不理想,偶尔会碰到干扰导致的错帧。我的程序里设计了连续三次通信失败后就输出一个报警标志,同时复位所有通信相关寄存器,再自动重试。重试前清空D200接收区,以免旧数据干扰判断。
5. 实测记录与故障排查速查
5.1 调试过程中的几个典型现象
这台FX5U加485适配器调试的过程中,我至少碰上了三次有代表性的坑,记录一下省得大家再走弯路。
第一次是接线后完全没响应。用万用表量485的A-B线间电压,空闲时只有0.2V左右,正常应该在2V到6V之间。检查后发现SDA、SDB和RDA、RDB搞反了,PLC发送的时候把数据发到了仪表自己的发送线上,仪表自然收不到。把四根线重新交叉对接后,电压正常,通信立即通了。
第二次是通信响应正常,但仪表返回的数据总是校验失败。用串口调试助手截包,发现PLC发的帧末尾是“FB”,但仪表返回的响应里校验值跟它不一致。后来对仪表手册发现,仪表对LRC计算的范围要求和我理解的不一样——它把回车换行前的所有字符都算进去,也就是连LRC本身都要参与累加。这种非标准处理在老旧设备里不算少见,解决方式就是按仪表手册修正LRC计算范围。
第三次是长时间运行后偶发通信卡死。现象是PLC程序里M8123一直不置位,通信子程序停在等待状态。排查发现是我的超时逻辑写在了通信流程的外面,导致超时到了也不复位。后来把超时判断嵌进状态机里,每次接收等待都强制判断超时,问题再也没出现过。
5.2 常见问题速查表
| 故障现象 | 可能原因 | 排查与处理 |
|---|---|---|
| 发送后仪表无任何响应 | 接线交叉错误 | 核对SDA/SDB与RDA/RDB,用万用表测A-B空闲电压是否在2V-6V |
| 响应乱码 | 波特率、校验位不匹配 | 核对设备说明书,保证PLC侧与仪表全参数一致 |
| 数据时好时坏 | SG未共地、屏蔽层未接 | 接上SG信号地,屏蔽层单端接地 |
| 偶发响应超时 | 终端电阻缺失或过多 | 总线两端加120Ω电阻,但不要三个以上设备都加 |
| LRC校验总是失败 | 计算范围错误或未按字节转十六进制 | 确认仪表对LRC范围的定义,先转十六进制再求和 |
| 通信一段时间后卡死 | 超时逻辑未复位 | 检查M8123是否一直为1,程序里加超时强制复位 |
| 返回数据倒数第二个字节总是0D或0A | 把CR LF当成数据使用 | 解析数据时跳过最后两个字节,或者按长度截取 |
| 一帧数据拆成了两帧接收 | 帧间隔设置过短 | ASCII模式适当放宽帧间隔,确保一次收完 |
5.3 几点个人建议
第一个建议:调试阶段一定要用USB转RS485模块加串口调试助手,先把仪表单独调通,再介入PLC。你就能清楚看到PLC发出的帧是不是符合协议,仪表返回了什么。没有这一步,PLC程序里的问题和不稳定因素很难分清。
第二个建议:程序里尽量把所有通信相关参数集中到一个子程序里管理。波特率、站号、发送数据区、接收数据区、超时时间,这些集中定义,后期换设备改协议时,一改就是一处,不用满工程翻梯形图。
第三个建议:给通信模块留一个手动触发通道。比如用一个内部继电器M0,置位时执行一次通信,配合监控面板调试,比强制循环发送方便得多。我实际调仪表时都是先手动触发几次,确认数据稳定了再切到自动循环。
第四个建议:如果条件允许,把通信数据处理放到中断或者定时器里执行,避免和主程序扫描周期抢时间。FX5U的扫描周期本身就比老FX系列快,但RS485通信是异步过程,主程序里如果同时有大段的浮点运算,通信标志的判断时机就容易不准。
最后再说一个很实际的小技巧:组帧的时候在发送帧区的最后预留两个字节,专门放CR LF。因为有些设备对CR LF很敏感,有的甚至只认LF不认CR,也有只认CR的。预留出来,现场遇到响应异常时直接改这两个字节测试,不需要动整个组帧逻辑。这个细节是从设备说明书里看不到的,只有拿着串口助手反复试才会发现。