做国产兼容FX3U源码,最怕的不是梯形图跑得慢,而是用户把编程软件一接,来一句“这跟原机不一样”。这不是玄学,是协议边界条件没处理好。我们这套兼容FX3U的PLC固件做了快一年,指令引擎早就稳定了,但这轮又集中改了三块——监控卡顿、8位口令功能、程序消除功能。前两个是体验和功能补齐,第三个是现场一次误擦事故逼出来的重写。这篇把定位过程、实现思路和测试安排都摊开写,给同样在做兼容机固件的朋友当个参考。
1. 兼容到“操作层面”才算及格:这轮改动的背景
1.1 兼容的及格线不只是指令集
很多人觉得,国产FX3U兼容机只要梯形图能跑、指令周期跟得上,就算过关。其实这只是第一道坎,后面还有一堆“看不见但摸得着”的活:编程软件能不能正常读出程序、监视窗口刷新顺不顺、参数写进去下次上电还在不在、口令能不能拦住想读程序的人、内存清除之后是不是干干净净回到出厂状态。
用户不会去数你的指令周期,他们的判断标准很简单:拿编程软件一连,把平时对原机做的事儿做一遍,顺不顺手。所以兼容固件做到中期以后,重心基本不在指令引擎,而在那些“操作层面”的边角功能。这轮我们改的三个问题,恰好都属于这一类。
1.2 三个问题是怎么陆续暴露的
问题不是自己冒出来的,都是客户在现场“用”出来的。第一个是关于监视窗口的:客户反映,程序一上电跑起来之后,编程软件的监视页面刷新像幻灯片一样,有时候点“强制写入”还要等半天。第二个是功能缺口:客户要求做8位口令,防止现场人员把程序读走,我们当时只做到了“提示存在口令”,但校验和拦截逻辑没串完整。第三个是事故:客户做了一次内存清除,结果PLC直接失联,重新上电也没反应,只能返厂刷机。
第三个事故最要命,因为它暴露的不只是擦除函数本身的问题,还牵扯到口令功能没做完、参数区保护缺失、掉电恢复没设计等多层漏洞。这篇就按排查顺序,把监控卡顿、口令、程序消除三件事逐个说透。
2. 监控卡顿的完整排查链路:从现象到根因,一步没跳
2.1 现象:监视窗口像“幻灯片刷新”
先说现象。客户的工程里有一段比较重的用户程序,主要是浮点运算加数据块搬移,扫描周期被拉到300ms以上。在这个程序运行状态下,编程软件的监视窗口刷新明显变慢:D寄存器数值不是平滑跳动,而是一顿一顿地更新,严重时页码切换也要等好几秒。点“强制写入”偶尔还会弹超时错误。
最初怀疑是USB转串口线兼容性、波特率不稳之类的硬件问题,因为这种“慢”很像链路丢包重传。但后来发现,同一个适配器换到我们另一台样机上就完全正常,所以怀疑对象从链路转到了固件调度上。
2.2 先把硬件链路嫌疑排除
为了排除链路因素,我做了两组对照:一组用USB转串口适配器连接,一组用原生RS-422口连接。两组都复现了卡顿,幅度几乎一样。这就说明问题不在适配器上,而在PLC固件对通信命令的响应方式上。
接着做了第二组对照:把客户的重程序清空,写一个几乎空的程序进去,监视立刻恢复流畅。这就更明确了——卡顿跟用户程序的体量强相关,PLC内部一定存在“程序执行”和“通信响应”抢资源的关系。
2.3 抓包定位:延时集中在一条命令上
拿到这个结论后,我直接在PLC的发送端串了一个逻辑分析仪,抓请求帧和响应帧的时间戳。编程软件监视窗口的刷新机制,本质上就是反复发送“字设备批量读取”命令,比如一次读32个D寄存器。抓包结果很扎眼:
| 测试场景 | 平均响应时间 | 最大响应时间 |
|---|---|---|
| 空程序 + 32字D读取 | 1.8ms | 2.6ms |
| 重程序 + 32字D读取 | 385ms | 612ms |
| 重程序 + M位设备读取 | 400ms | 500ms以上 |
延时不在某个特殊命令上,而是集中在所有“批量读软元件”的命令上。换句话说,不是某条命令写错了,而是命令的响应时机被整体拖后了。
2.4 根因:通信服务挂在扫描尾声,长扫描直接饿死通信
翻代码找根因,问题出在通信服务的调用位置。早期做协议栈的时候图省事,把“解析一帧请求→查软元件表→拼响应帧”整个流程放在了指令执行引擎的END处理里。梯形图逻辑扫描一轮,走到END,才去轮询一次接收环形缓冲区,有完整帧就现场处理完再回。
这个设计在扫描周期短的程序上完全没问题。但一旦用户程序重、扫描周期长到几百毫秒,通信服务就只能跟着扫描周期走——监视命令发过来,PLC要等这一轮扫描全部跑完才响应,自然表现为每秒刷新几次甚至几秒才动一下。
还有一个帮凶:拼响应帧的时候,为了避免接收缓冲被覆盖,我们关了一段时间的接收中断。但批量读命令的响应帧很长,头部加数据可能上百字节,逐字节拼帧期间外部又有新的监视帧进来,UART FIFO就溢出了,帧丢失触发重传,一来一回又叠加了额外延时。
2.5 修复思路:把通信处理拆成时间片
既然根因是“通信服务只在END跑一次、且处理过程太长”,修复就有两个方向:一是增加通信服务被调用的频率,二是缩短每次处理占用的时间。
我改成了这样:通信服务不再挂在END上,而是放进1ms的定时器节拍里,同时在指令执行引擎里设置安全点,分别是扫描前、指令调度循环内每隔固定指令数、扫描后。每次进入通信服务只做一小步工作:检查接收缓冲有没有完整帧,有就只把请求参数解析出来,放入待处理队列;真正读取软元件值、拼帧、发送,放到下一个时间片里分步完成。
软元件读取本身只有微秒级,真正的耗时在拼帧和异或校验循环。我把“读取值”和“拼帧发送”分离:读取值时用一个极短的临界区从软元件表拷贝到临时缓冲,拷贝完立刻释放中断,拼帧和发送就交给发送中断或DMA去跑。这样接收中断被关的时间从几百微秒缩短到几个时钟周期。
2.6 验证结果和遗留注意点
改完后同样场景再测,响应时间稳定在1.6ms左右,和空程序时的1.8ms几乎持平。监视窗口恢复实时刷新,强制写入也不再超时。
这里有个经验:定时器节拍里做通信分步,要特别注意重入。通信服务被扫描前和扫描后都可能触发,如果上一个处理还没完成,下一个节拍又进来,状态机就会乱。所以每一步都靠一个有限状态机推进,节拍只做“当前状态允许做的那一步”,其余情况直接返回。这个设计后来在8位口令和程序消除的场景里也复用了,效果不错。
3. 8位口令:校验、存储和会话状态联动
3.1 编程软件里的口令完整流程
8位口令在三菱FX3U体系里的表现是这样:用户在PLC参数里设置最多8个字符的口令,写入PLC后,下次有人从PLC读程序,编程软件先收到“需要口令”的应答,然后弹输入框;口令正确,程序读取继续;口令错误,弹出错误提示;只有口令校验通过之后,程序读取和写入才被允许。而监视软元件(看D、M的值)不需要口令,这是很多人容易搞错的地方。
我们之前的实现只做到了“把口令存下来”,但没把校验结果串到程序读取和写入的命令链路上,等于白做。这次补齐了整条链路。
3.2 存储设计:口令区独立于程序区
口令的存储位置很关键。我们把它放在独立的参数扇区里,不跟用户程序区混在一起。这样用户程序被整体覆写时不会冲掉口令;只有“全清除”才会连口令一起抹掉。
考虑到flash写入不可靠,口令区用了双份存储加XOR校验:8字节口令,存两份,每份附带校验字节。上电时先读第一份,校验失败就读第二份并自动修复第一份;两份都坏,就默认“无口令”,保证PLC不会被锁死成砖。
3.3 校验实现细节:别用strcmp去比二进制口令
口令虽然看起来是ASCII字符,但在通信帧里是按二进制传输的,有可能以0x00结尾。我们第一版实现用了字符串比较函数,结果遇到某些输入时,比较会在第一个0x00处截断,导致口令永远“匹配”。这里必须按长度加字节逐位比较,同时忽略尾部填充的0x00。
具体比较逻辑很简单:先比对有效长度,一致后再逐字节比较,任一位不等就返回失败。不需要搞什么复杂算法,重点是别被C字符串的“零结尾”思维带偏。
3.4 会话状态联动:一个标志位的事
口令校验通过后,要在通信链路上下文里记一个“已授权”标志。编程软件在同一个会话里连续读程序、写程序时,不再重复弹口令框;但新连接建立时,标志必须复位,强制重新校验。
这里有一个容易踩的坑:不要自己加“错误三次锁定五分钟”之类的逻辑。原机行为是错误就返回错误,编程软件自己会弹框让用户重输,锁定反而会让软件挂起等待,造成“PLC不响应”的假象。我们最初加了锁定逻辑,测出来软件直接卡住,只能赶紧拆掉。
3.5 踩坑记录:监视命令被口令拦住了
踩过的最大一个坑,是我们把口令校验错误地接到了监视命令上。当时觉得“有口令就必须全部拦住”,结果编程软件的监视窗口连软元件值都读不到,用户以为PLC死机了。回头查协议定义,监视(设备读取)是不受口令限制的,受限的只有程序区的读取和写入。这个边界要刻在脑子里。
另外,程序读取命令在口令未通过时,要返回协议规定的“需要口令”错误码,不能返回通用错误码。否则编程软件不会弹口令输入框,只会显示“无法读取”,用户根本不知道要去输口令。
4. 程序消除功能:一场误擦事故后的重写
4.1 现场事故回放
事故是这样的:客户在编程软件里执行“PLC内存清除”,只勾选了“程序”一项,结果PLC清完就失联了。返厂检查发现,参数扇区(波特率、站号、运行设置这些)也被擦掉了,PLC回到出厂默认的通信参数,跟客户现场设置的线缆参数对不上,自然连不上。
问题出在旧实现的“清除”命令映射得太粗暴:无论编程软件请求清除哪个范围,固件都执行“全片擦除”,把用户程序、参数、口令扇区一并抹掉。而且擦除过程中没有任何掉电保护,擦到一半断电,flash处于半擦状态,下次上电连引导程序都找不到合法内容,直接变砖。这个事故的另一个帮凶,正是3.5节说的口令校验没接到清除命令上——当时用户根本不需要口令就能执行清除,口令形同虚设。
4.2 Flash布局与三种“清除”语义
重新设计之前,先把固件的flash布局理清楚:
| 扇区 | 内容 | 清除命令作用 |
|---|---|---|
| A | 引导程序 + 擦除签名保留区 | 不擦 |
| B | 应用固件 | 不擦 |
| C | 参数区(站号、波特率、运行配置等) | 仅“参数清除”擦 |
| D | 口令区 | 仅“全清除”擦 |
| E | 用户程序区 | 程序/全清除都擦 |
| F | 文件寄存器/掉电保持数据 | 按选项决定是否复位 |
编程软件的清除命令其实带有范围选项:清除程序、清除参数、全清除。旧代码无视了选项,这是第一层错。新实现按选项拆开处理,每个扇区的擦除独立进行。
4.3 两阶段擦除与掉电恢复
解决了“擦哪里”之后,还得解决“擦一半断电怎么办”。flash擦除不是原子操作,掉电中断就可能留下坏块。我们的方案是两阶段擦除加启动恢复:
先把“擦除待定”签名(0xA5)写入引导区预留的一个字节;然后执行真正的扇区擦除;擦完读回校验,确认全是0xFF后,把签名清回0xFF。引导程序上电时检查签名,如果发现是0xA5,说明上次擦除没完成,就直接重新执行目标扇区的擦除和校验。
这个思路简单但有效。签名只占一个字节,写在引导区里,不参与应用擦除,天然不会被误清。掉电恢复在每次上电自检阶段完成,用户看到的现象就是“清了继续清完”,不会卡在半途。
4.4 清除过程中的运行状态保护
编程软件在PLC处于RUN状态时发送清除命令,原机不会直接执行,而是返回提示,让操作者先把PLC切到STOP再操作。我们最初想做得“智能”一点,收到命令自动停掉扫描再去擦,但测下来发现这会造成语义混乱——用户明明没碰开关,PLC却自己停了,很容易误判。最终跟进原机行为:RUN状态下收到清除命令,返回“请先切换到STOP”的提示错误码,不执行擦除。
擦除期间还有一个细节:要设置一个全局状态标志,禁止用户程序启动,同时屏蔽对flash的其它写操作,防止擦除过程中又有参数写入命令插进来,把扇区搞乱。擦除完成后再把这个标志清掉,PLC回到STOP状态,用户程序区表现为空,可以重新写入工程。
4.5 口令与擦除的边界
口令和擦除的关系必须明确:程序消除只清用户程序区,口令单独留在参数区里,不受影响;参数清除擦参数区,口令跟着一起没;全清除则是程序区、参数区、口令区一起还原成出厂状态。
但注意,执行“全清除”这个动作本身,如果口令已设置且未校验通过,必须被拒绝。原机的逻辑是:清除程序属于写操作,受口令保护。否则就形成一个漏洞——任何人不用口令先做全清除,口令被抹掉,然后就能自由读走残余数据。这轮我们正好把口令校验同时接到了程序写入和清除命令上,顺手把4.1事故的另一个漏洞也堵上了。
5. 回归测试矩阵与维护体会
5.1 监控场景怎么测
监控卡顿的回归测试,不能只拿空程序跑,必须构造一个重程序把扫描周期拉长。我写了一个FOR-NEXT循环一万次的测试程序,扫描周期稳定在400ms左右,然后开着监视窗口连续跑10分钟,记录响应时间和丢帧情况。
| 测试场景 | 通过标准 |
|---|---|
| 重程序 + 32字D批量读 | 响应时间 < 5ms,连续10分钟无丢帧 |
| 重程序 + M位批量读 | 响应时间 < 5ms |
| 监视窗口强制写入 | 无超时,写入后立即生效 |
| RUN/STOP切换再监视 | 切换后监视恢复时间 < 200ms |
5.2 口令场景怎么测
口令的测试要覆盖设置、更改、清除、掉电保持、特殊字符几个维度。特别要测字符串结尾是空格(0x20)的口令,以及口令开头带0x00的异常帧——虽然正常没人这么设,但协议上要防住不被绕过。
| 测试场景 | 通过标准 |
|---|---|
| 设置口令后读程序 | 弹出口令输入框 |
| 错误口令读程序 | 返回口令错误码,可重新输入 |
| 正确口令读程序 | 读取正常 |
| 未设口令时读程序 | 不弹框,直接读取 |
| 监视D/M值(已设口令) | 无需口令,正常监视 |
| 全清除(未验证口令) | 被拒绝,不能绕过 |
| 掉电重启后口令 | 依然生效 |
5.3 擦除场景怎么测(含掉电注入)
程序消除最怕掉电,回归测试必须有计划地注入掉电。我的做法是把PLC电源接到一个可控交流开关上,分别在四个时点拉闸:写入擦除签名之后、扇区擦除进行中、擦除完成后校验前、校验完成签名清除后。每次断电后重新上电,检查引导程序能不能根据签名恢复擦除,PLC能不能正常进入STOP,之后能不能重新写入程序。
| 掉电时机 | 上电预期行为 |
|---|---|
| 签名写入后、擦除前 | 引导程序检测签名,继续完成擦除 |
| 扇区擦除中 | 引导程序重新执行完整擦除 |
| 擦除后、校验前 | 引导程序重新执行完整擦除 |
| 校验后、签名清除前 | 引导程序重新执行完整擦除或确认已擦净 |
这套测试跑下来最大的价值,是把“擦一半断电”从随机事故变成了可复现用例。修完这轮之后,我们再没接到过清除失联的返厂单。
5.4 给同行的话:兼容固件的长期维护
做兼容固件这行,我的体会是:指令集才占工作量的一小截,真正决定口碑的是这些“软件操作层面”的边角行为。口令要不要拦监视命令、清除命令在RUN状态该不该执行、掉电擦除怎么恢复,这些细节原机手册不见得写全,但用户拿编程软件一操作就全暴露了。
以前我也觉得单元测试能兜底,现在看,最靠谱的对照物还是身边放一台原机。碰到拿不准的行为,把请求帧发过去,抓原机的响应帧回来一帧一帧对,比猜省太多时间。我们把每次对出来的差异记成一份“兼容决策文档”,哪条命令该返回什么错误码、哪个操作该在什么状态下执行,白纸黑字记下来,后续加功能、换人维护都能直接参考。这轮三个问题的修改,全部来源于这份文档里新增的三页记录。