简介:面向三菱Q系列PLC工程师与工控入门者的MODBUS通信实战资料,聚焦QJ71C24N串口模块的RTU配置与G.CPRTCL指令应用,解决PLC与变频器等外部设备数据读写对接的实际问题。压缩包共1个doc文件,约854KB,为图文混排的实操笔记,涵盖模块起始XY地址设定、开关设置中CH2协议与通信速度配置、通信协议支持功能里功能码03读保持寄存器的建立、软元件批量设置与模块写入流程。文中结合安川A1000变频器案例,给出D7000~D7002地址分配、协议执行个数与标准RTU接线说明,并附G.CPRTCL各参数含义及多协议连续执行的软元件排布,可作为现场调试的对照手册与排错参考。已有6752人学习。
1. 一次 MODBUS 读取,为什么梯形图里连一帧报文都看不见
第一次翻别人的 QJ71C24N 工程,很多人会觉得有点玄学:整个程序里找不到 CRC 计算,找不到卡 3.5 字符静默期的定时器,甚至连一个发送缓冲区的影子都没有,只有一行G.CPRTCL和一堆D7000、D7052这样的软元件地址。可它偏偏能把变频器的输出频率稳定读上来。原因不复杂——MODBUS RTU 的整条协议栈被搬进了模块内部,CPU 只负责“给参数、收结果”。搞懂这次“搬家”搬走了什么、留下什么,你才看得懂那些 D 区地址为什么这么排,也才能在通信不通报错时,判断该查接线、查指令参数,还是查模块里那份没写进去的协议表。
2. 协议栈搬家:一条 G.CPRTCL 背后模块替你干了多少活
2.1 3.5 个字符的静默期:帧边界为什么不能靠扫描周期去卡
MODBUS RTU 没有起始符也没有结束符,它分辨一帧的开始和结束,全靠总线上的“安静时间”:帧与帧之间至少 3.5 个字符时间的静默,帧内字符间隔不能超过 1.5 个字符时间。这个规定看起来只是协议细节,落到 PLC 上却是个硬约束。以 9600bps、8 数据位、偶校验、1 停止位算,一个字符 11 位,约 1.15ms;3.5 个字符就是 4ms 左右,1.5 个字符约 1.7ms。
问题在于 CPU 的扫描周期是会抖的。一段看起来“很短”的逻辑,实际扫描时间可能在 1ms 到十几毫秒之间浮动,中间还夹着中断、通信服务、其他智能模块的刷新。你想用梯形图去保证“两个字节之间不超过 1.7ms、两帧之间安静 4ms”,本质上是在用软件去赌硬件的时序窗口,赌赢一次容易,连续跑三个月不翻车很难。这就是为什么早期自己拼帧的项目,现场经常表现为“白天好好的,晚上负载一高就 CRC 错”。
模块的做法是把这件事交给自己的 UART 和硬件定时器。字节进出、字符间隔、帧尾静默,全在模块的通信控制芯片里完成,CPU 侧看不到。你在 GX Works2 里做的那些设置——CH2 的通信协议选 MODBUS RTU、通信速度、数据位、校验位、停止位——本质是在配置这个硬件状态机,而不是在写软件逻辑。
提示:通信速度、数据位、校验方式、停止位这四项必须和从站设备完全一致。少一位校验,现象不是“明显报错”,而是全部超时或者偶发数据错位,排查起来非常费时间。
2.2 协议表被写进模块:一份“预编译帧序列”长什么样
理解了时序归硬件管,再看那份“协议”就顺了。在 GX Works2 里走一遍:工具 → 通信协议支持功能 → 串行通信模块,新建,再点添加,会跳出一个协议库,里面按功能码列好了条目——本例选的是 MODBUS 的读保持寄存器,功能码 03。
添加完成后,模块里就多了一条“协议”,它包含两部分:发送侧知道自己要按什么顺序把从站地址、功能码、寄存器地址、点数拼成字节并补 CRC;接收侧知道回帧应该多长、怎么校验、收到之后把数据搬到哪个软元件。你在参数里填的 D7000、D7001、D7002,只是喂给这条协议的三粒“输入”。
这套配置最后不是留在工程文件里,而是用 在线 → 模块写入 写进模块本体。这一步很多人会忽略它的意义:协议表存在模块里,等于把“怎么说话”这件事从 CPU 程序中剥离了。换一台 CPU、重写一遍梯形图,只要模块没换、协议表还在,通信逻辑不用重做。反过来,你要是只下载了程序没做模块写入,程序里那行G.CPRTCL指向的是一个不存在的协议编号,模块只会给你一个异常结束。
和“无顺序协议自己拼帧”对比一下,差别很直观:
| 环节 | 模块内置协议 | 梯形图自己拼帧 |
|---|---|---|
| 帧结构组装 | 模块按协议表生成 | CPU 逐字节 MOV |
| CRC16 计算 | 模块内部 | CPU 每帧算一遍 |
| 3.5 字符帧边界 | 硬件定时 | 定时器 + 扫描周期,难保证 |
| 换从站/换寄存器 | 改几个 D 区参数 | 改逻辑 |
| 出错定位 | 协议执行结果 + 错误代码 | 全靠自己打的标志位 |
真正值钱的不是省了那几行 MOV,是那 4ms 的静默期不再由你的扫描周期负责。
2.3 一次调用的时序:从执行条件 ON 到完成位上升沿
G.CPRTCL是异步指令,这一点决定了整个程序的写法。执行条件 ON 的那个扫描周期,指令把“我要执行第几个协议、参数在哪”告诉模块,然后就返回了,CPU 继续扫下一行。模块在后台跑串口事务:把请求帧发出去、等回帧、校验、把数据写进控制区。
整个链路大致是这样几步:
- 执行条件 ON,指令读取 (S) 起始的控制区,取出从站地址、寄存器地址、点数,以及 (S) 区里指定的协议编号。
- 模块按协议编号取出对应的预编译帧序列,拼出 8 字节请求帧,补上 CRC,从这个通道发出去。
- 等待从站响应。这段时间 CPU 完全不管,扫描照常进行。
- 收到回帧,校验 CRC、校验从站地址和功能码,通过后把数据写进控制区对应位置。
- 完成标志 (D) 置 ON。程序在 (D) 的上升沿取数据、做后续处理,然后把启动条件复位。
第 5 步是关键。新手最常见的写法是把取值逻辑和启动指令放在同一段无条件执行的逻辑里,结果永远拿到上一次的值,或者第一次拿到全零,看着像通信慢半拍,其实是时序没对上。稳妥的做法是把启动条件做成脉冲:完成位 ON,取数,复位启动条件,等下一个轮询节拍再拉起。
3. G.CPRTCL 指令解剖:Un、n1、n2 与 D 区的地址算术
3.1 起始 XY 地址怎么变成 Un
打开 GX Works2 新建工程,右键智能功能模块,安装位置要按硬件实际插槽填,这里的“起始 XY 地址”就是后面所有事情的根。假设填的是 0090,那么模块占用的 I/O 编号从 0090 开始,G.CPRTCL的第一个操作数 Un 就指向这个起始 I/O 号,实际写成H90这一档。
原文示例里出现的写法是 H9 和 0090 混着来,很多人看到这儿会懵。这不是笔误,而是不同软件版本、不同显示位置对末位那个 0 的省略习惯不同造成的。判断标准只有一个:以模块属性里实际读到的起始 I/O 号为准,别照抄别人工程里的十六进制常数。
; 缓冲存储器设置:关闭回波(示例中起始 I/O 为 0090,Un 写作 H90) TOP H90 H1C2 K1 K1 ; Un 缓冲 点数 写入值 ; 地址 地址这段TOP指令直接写模块的缓冲存储器,绕过了协议层。H1C2 是缓冲存储器地址,K1 表示写一个点,第二个 K1 是把该位置 1。这一位的作用是“禁止回波传送”,为什么需要它,后面第 5 章会展开——它是两线制 485 上最容易忽略的一环。写缓冲存储器这类操作,地址和含义一定要对着模块手册的缓冲存储器映射表核,不要凭记忆。
Un 写错的后果比想象中严重:指令不会报“参数非法”,它会跑到 I/O 编号对应的那个模块上去访问缓冲存储器。运气好是执行异常,运气不好是把旁边那个模块的配置改掉了。
3.2 控制数据区 (S):D7000 到 D7052 各自装什么
(S) 是控制数据的起始软元件,示例用 D7000。指令执行时,模块从这里读参数,也往这里写结果。原文示例里几个关键位置的含义是这样的:
| 软元件 | 示例值 | 作用 |
|---|---|---|
| D7000 | 从站站号 | 外部设备的 MODBUS 从站地址 |
| D7001 | H24 | 要访问的寄存器地址(0024H) |
| D7002 | K2 | 读取的寄存器个数 |
| D7052 | 协议编号 | 本次执行的协议(功能码 03 那条) |
新手最容易问的问题是:为什么参数在 D7000、D7001、D7002 连着放,协议编号却跑到 D7052 那么远?因为这段控制区不是“三个参数挨着排”,而是按“一次调用可连续执行最多 8 个协议”设计的:编号区要留出 8 个位置,依次对应第 1 次到第 8 次执行的协议编号;参数区和结果区也要按协议次序铺开。原文里那句“(Sn+2) 第一次执行的协议编号……(Sn+9) 第八次”,说的就是这套编号区的排布。
这里有个必须点破的地方:不同软件版本、不同协议数量下,这些偏移的具体基址会变,网上抄来的 D 区地址表经常对不上号。正确做法是用工具自身的功能去看——工具 → 设置软元件一览显示,它会把这个工程实际用到的软元件布局列出来,包括输入参数、输出结果、完成标志。以这份清单为准,比背任何一张表都可靠。
; 参数区准备(每个协议用到的参数按工具导出的软元件一览填) MOV K1 D7000 ; 从站站号,本例变频器站号 1 MOV H24 D7001 ; 起始寄存器 0024H MOV K2 D7002 ; 连续读 2 个寄存器:0024H、0025H MOV K1 D7052 ; 第一次执行的协议编号(功能码 03 那条)代码里每一行都不是“随便放个数”,而是在填协议执行时需要的槽位。MOV H24 D7001里的 H24 是十六进制常数,表示 0024H 这个寄存器地址,注意别写成十进制 24——十进制 24 是 0018H,读回来的是另一个量,而且往往“有数据但不对”,比读不到更难查。
3.3 完成标志 (D):什么时候该读数据,什么时候该看异常
(D) 在示例中被定义成“通信结束时 ON 的标志位”,用位软元件,比如 M200。但“结束”不等于“成功”,这是必须分清的。
常见的实现思路是给 (D) 配一个伴生位:(D) 本身表示执行完成,(D)+1 表示异常结束。程序里判断的顺序应该是先看异常位,再看数据。只看完成位就去解析数据,遇到从站掉线、寄存器地址越界、CRC 错这类情况,D 区里躺的还是上一次的有效数据,你会以为通信正常,只是数据“偶尔不准”。
; 完成处理:先用异常位兜底,再取值 LD M200 ; 完成标志 ANI M201 ; 没有异常结束才取数 MOV D7003 D100 ; 第一个寄存器的值 → 频率 MOV D7004 D101 ; 第二个寄存器的值 → 电压 LD M200 RST M100 ; 复位启动条件,等下一个轮询节拍取值之前先卡住异常分支,取值之后立刻复位启动条件,这个顺序不能反。复位放在取值后面,是为了防止下一个扫描周期启动条件还没稳住又触发一次;复位放在异常分支之外,是为了保证无论成功失败,启动条件都能收回来,不至于卡死在一帧失败的通信上。
4. 读保持寄存器 03:从软元件参数到线上 9 个字节
4.1 03 的请求与响应:字节级拆解
功能码 03 是 MODBUS 里最常用的读保持寄存器。它的请求很短,固定 8 个字节;响应长度随点数变化,是 5+2N 字节。以读 2 个寄存器为例:
| 位置 | 请求(8 字节) | 响应(9 字节) |
|---|---|---|
| 1 | 从站地址 | 从站地址 |
| 2 | 03 | 03 |
| 3 | 起始寄存器高字节 | 数据字节数(04) |
| 4 | 起始寄存器低字节 | 第 1 个寄存器高字节 |
| 5 | 寄存器数量高字节 | 第 1 个寄存器低字节 |
| 6 | 寄存器数量低字节 | 第 2 个寄存器高字节 |
| 7 | CRC 低字节 | 第 2 个寄存器低字节 |
| 8 | CRC 高字节 | CRC 低字节 |
| 9 | — | CRC 高字节 |
从这张表能看出两件事。第一,MODBUS 的寄存器是 16 位、大端序,高字节在前,PLC 侧读回来要按同样顺序拼,拼反了会得到一个“看起来像那么回事”的数值,比如频率 50.00Hz 变成 12800 之类。第二,CRC 在报文里是低字节在前,这跟数据部分的高字节在前正好相反,手工算过 CRC 的人基本都在这里踩过坑。
寄存器 0024H 和 0025H 这两个地址来自设备手册,原文工程带的变频器手册把输出频率放在 0024H、输出电压放在 0025H。不同品牌、不同型号的寄存器映射完全不一样,别把一张表套到所有设备上。另外要注意有些量是 32 位的,比如累计电量、运行时间,会跨两个连续寄存器,这时点数要 K4,回来的 8 个数据字节还得自己拼成双字。
还有一层容易被忽略:寄存器里放的是原始值,工程单位要按设备手册的缩放比例换算。同样是 5000,可能是 50.00Hz,也可能是 5.000Hz,全看手册怎么写。
4.2 MOV H24 D7001 / MOV K2 D7002 的参数语义
回到那两行看似平淡的指令。MOV H24 D7001是把十六进制 24 送进控制区里的“寄存器地址”槽位,MOV K2 D7002是把十进制 2 送进“点数”槽位。模块执行时,会把这两个值按协议表的规则编码进请求帧的第 3 到第 6 字节,剩下的从站地址、功能码、CRC 由协议表自己生成。
这就是“预编译协议”的实际含义:你填的是语义参数(读谁、从哪读、读几个),模块负责把它翻译成字节。你不需要知道 CRC 多项式是 0xA001,也不需要知道帧尾要静默 4ms,这些都在协议表里。
但这不意味着可以随便填。点数的上限受模块接收缓冲大小限制,一次读几百个寄存器会直接超时;寄存器地址跨功能码边界(比如把线圈地址当保持寄存器填)会收到异常响应,功能码最高位被置 1,返回 83。遇到这种情况,异常代码通常会在控制区里留一份,取出来对照手册看是哪一类错误,比在梯形图里瞎猜快得多。
4.3 多从站轮询:一条 485 总线的排队与时间预算
两线制 RS-485 是半双工总线,同一时刻只能有一个主站发问、一个从站回答。这意味着不管总线上挂了多少台设备,必须排队。程序里通常用步进节拍(M 区一组状态位)串起来:第 1 步发站号 1 的参数、启动,等完成位;完成位 ON 后取值、复位、进第 2 步;一轮跑完回到第 1 步。
排队之前先算一笔时间账,能省掉很多“为什么通信跟不上”的困惑:
def txn_ms(baud=9600, req_bytes=8, resp_bytes=9, slave_delay_ms=15): """估算一次 MODBUS 事务的耗时(8 数据位 + 偶校验 + 1 停止位 = 11 位/字符)""" char_ms = 11 * 1000 / baud # 每字符耗时,9600 下约 1.15ms send_ms = req_bytes * char_ms # 请求帧发送时间,8 字节约 9.2ms recv_ms = resp_bytes * char_ms # 响应帧接收时间,9 字节约 10.3ms frame_gap = 3.5 * char_ms # 两帧之间的静默期,约 4.0ms return send_ms + recv_ms + frame_gap + slave_delay_ms print(round(txn_ms(), 1), "ms") # 单次事务约 38.5ms print(round(txn_ms() * 8, 1), "ms") # 8 个从站轮一轮约 308ms这个函数里没有一项是随便加的:char_ms由串口参数决定,改波特率它跟着变;slave_delay_ms是从站的处理延迟,变频器一般在 5~20ms,老设备可能更久;frame_gap就是前面说的 3.5 字符静默。按 8 个从站算下来,一轮要 300ms 左右——如果你的工艺要求 100ms 刷新一次,那不是程序写得不好,是物理上排不下,只能提高波特率、减少点数、或者拆成两条总线。
把这段账算清楚,再看轮询逻辑就很明确了:相邻两帧之间必须有间隔,间隔可以靠“完成位 ON 之后延迟几个扫描周期再启动下一次”这种笨办法实现,也可以靠定时器节拍。用笨办法时注意,延时太短会让从站还在处理上一帧时就收到新请求,表现为偶发无响应。
5. 避坑与排查:五个让通信“看着通其实不通”的坑
5.1 回波没关:模块把自己发的帧收下了
现象:通信时好时坏,CRC 错误率不固定,抓包能看到一堆看不出规律的错帧;有时候前几个字节是对的,后面全乱。
原因:两线制 RS-485 是半双工,模块在发送时,它自己发出去的差分电平会同时进到自己的接收器里。如果模块没屏蔽这段回波,它就会把自己刚发的请求帧当成从站的回帧收下来,两段字节在接收缓冲里错位,后面的解析自然全乱。
解决:把回波传送关掉。示例里的做法是TOP H90 H1C2 K1 K1,直接给缓冲存储器对应位置 1。这一步要在通信开始前做一次,放在初始化段里,别放在轮询循环里反复写。
5.2 XY 地址填错:指令跑去访问别的模块
现象:G.CPRTCL一执行就异常结束,或者执行完不报错但一点数据都没有;换个插槽以后现象又变了。
原因:工程里给智能功能模块填的起始 XY 地址和实际插槽不一致。Un 本质上是 I/O 编号,填错了,指令就去访问那个编号上的另一个模块,参数被写到别人的缓冲存储器里。
解决:先在硬件组态里确认起始 XY 地址,再核对G.CPRTCL的 Un 和缓冲区操作指令的操作数,两处必须一致。改插槽、加模块之后,这一段要重新过一遍,不能想当然。
5.3 协议没写进模块:程序没问题,模块里是空的
现象:程序是对的,参数也是对的,但G.CPRTCL一跑就异常;用监视功能看,模块里根本没有那条协议的编号。
原因:只做了程序下载,没做 在线 → 模块写入。协议表存在 GX Works2 的工程里,不写进模块等于没配置。
解决:每次改完协议表,走一次模块写入,并确认弹出的是写入成功提示、选中的是正确模块。现场有多台设备时尤其要看清目标,写错模块会把另一个通道的配置覆盖掉,而且覆盖完不会有任何警告。
5.4 串口参数不一致:连接正常,全是超时
现象:接线查了三遍、地址也确认过,就是收不到回帧;偶尔能收到一次,但数据不对。
原因:波特率、数据位、校验位、停止位这四项必须和从站完全一致。PL 侧设了偶校验、从站是 None,或者一边 9600 一边 19200,从站收到的就是一串垃圾,自然不回话。
解决:先按从站的手册把四项抄下来,再去 CH2 的设置里逐项核对。核对顺序建议从波特率开始,波特率错了连字符都切不准,后面几项对了也没用。改完参数记得重新写模块。
5.5 同一扫描周期取结果:永远拿到上一次的数据
现象:通信明明是通的,读回来的频率总是滞后一个周期;第一次读到的全是零;轮询越快,数据越像没更新。
原因:G.CPRTCL是异步指令,启动条件 ON 的那个扫描周期里数据还没回来。在同一段逻辑里紧接着取 D 区数据,拿到的必然是上一次的结果,或者初始值。
解决:把取数逻辑挂在完成位 (D) 的上升沿之后,取完再复位启动条件。同时确认启动条件是脉冲而非常 ON——常 ON 会让模块在完成之后立刻又启动,总线被占满,反而拖慢整体轮询。
6. 设计取舍:预编译协议这件事,今天还值不值得学
把这套东西拆完,会发现它的核心取舍只有一句话:把“时序敏感的协议状态机”和“时序不敏感的工艺逻辑”拆到两个处理器上。模块那颗芯片只干一件事——按 3.5 字符的规矩收发字节;CPU 只干一件事——什么时候发起、拿到结果怎么用。代价也很清楚:换协议要重新走一遍模块写入,参数和软元件的对应关系依赖工具导出,可读性远不如一段自解释的代码。
横向比一下三种做法,取舍就更明白了:
| 方案 | 帧与 CRC 谁算 | 时序谁保证 | 换协议要动什么 |
|---|---|---|---|
| 模块内置协议表 | 模块 | 硬件定时器 | 改协议表 + 模块写入 |
| 无顺序协议自己拼帧 | CPU | CPU 扫描 + 定时器 | 改梯形图逻辑 |
| 外挂网关 / 串口服务器 | 网关 | 网关 | 改网关配置 |
第一种在实时性上最稳,第二种最灵活但最容易在时序上翻车,第三种最省事但多了一个可能没人管的黑匣子。我做过的现场里,最怕的不是技术选型,而是选完之后没人知道边界在哪——网关配错了找不到人改,梯形图拼帧的定时器被后来人“优化”掉了,模块里的协议表被覆盖了没人发现。
所以验证这件事的方法也得跟着拆。协议这一层,用工具的通信状态监视看发送字节数、接收字节数和错误代码,能直接判断是“没发出去”还是“发出去没回来”;电气这一层,用串口分析仪或者在 A/B 线上挂个示波器,看差分电平和帧间隔,是判断终端电阻、屏蔽接地、共地这类问题的后悔药。两层都过一遍,剩下的基本就是参数映射问题,查手册能解决。
从那以后我每次接手这类通信,不管是第一次上电还是别人交接过来的老工程,都强制走一遍这个顺序:先确认模块里有没有协议表,再核对 Un 和起始 XY,再抓一次真实报文,最后才动程序逻辑。跳过前三步直接改梯形图,十有八九是在给一个没配好的模块改代码。希望帮到你。
本文还有配套的精品资源,点击获取