题目:
今天继续写一个轻松一点的例子:自动售货机找零。
假设一台售货机要退 37 元零钱,硬币盒里有 20 元、10 元、5 元、2 元、1 元几种面额。PLC 要算出每种面额各出几枚,既要把钱找对,又尽量少吐硬币。
这个问题听起来像生活小常识,其实背后就是一个很典型的算法:贪心算法。
它在自动化项目里也不陌生。配料时优先用大仓,分配任务时优先用空闲大工位,找可用资源时先满足最合适的一组条件,本质上都有一点“先拿最划算的”的味道。
本例用 SCL 写一个自动售货机找零功能,顺便聊聊贪心算法在 PLC 工程里该怎么用、哪里不能乱用。
设计分析:
找零的直觉很简单:先用大面额,再用小面额。
比如要找 37 元,有 20、10、5、2、1 这几种面额。先找 20,还剩 17;再找 10,还剩 7;再找 5,还剩 2;最后找 2,刚好结束。
得到的结果是:20 元 1 枚,10 元 1 枚,5 元 1 枚,2 元 1 枚。
这就是贪心算法的基本思路:每一步都选择当前看起来最合适的方案。
放到 PLC 里,我们还要考虑一个现场条件:每种面额的库存不是无限的。20 元硬币盒空了,就不能继续按理想算法出 20 元。
所以本例的输入包括三类数据:
changeAmount:需要找零的金额。
coinValue:每种面额的金额,按从大到小排列。
coinStock:每种面额当前库存数量。
输出则是 coinOut 数组,表示每种面额本次要出多少枚。
创建功能 FC:
本例创建一个 FC,命名为:
CalcCoinChangeGreedy
它是一个无状态计算功能,所以用 FC 比较合适。
如果后面要控制真实的出币电机、光电计数、卡币报警、出币超时,那些动作逻辑建议单独放在 FB 里。
定义接口变量:
定义输入变量:
enable:功能使能。
request:是否需要执行一次找零计算。
changeAmount:需要找零的金额,单位按项目约定,例如元或角。
count:面额种类数量,最大 8。
coinValue:面额数组,建议从大到小排列。
coinStock:每种面额当前库存数量。
定义输出变量:
返回值:本次总共需要出的硬币数量。
valid:是否可以完成找零。
remainAmount:计算结束后仍未找出的金额,0 表示找零成功。
coinOut:每种面额本次需要出的数量。
定义临时变量:
tempI:循环变量。
tempValidCount:有效面额数量。
tempNeedCount:当前面额理论上需要的数量。
tempUseCount:结合库存后实际使用的数量。
程序代码:
调用方法:
可以建立一个全局 DB,命名为:
GdbCoinChanger
在 DB 中定义面额、库存和计算结果,例如:
在 OB1 或设备主 FB 中调用:
一个小例子:
假设需要找零 changeAmount = 37。
面额数组为 20、10、5、2、1,并且库存都足够。
程序计算过程如下:先用 1 枚 20,剩 17;再用 1 枚 10,剩 7;再用 1 枚 5,剩 2;再用 1 枚 2,剩 0。
最终 coinOut 的结果是:
coinOut[1] = 1,对应 20 元。
coinOut[2] = 1,对应 10 元。
coinOut[3] = 1,对应 5 元。
coinOut[4] = 1,对应 2 元。
coinOut[5] = 0,对应 1 元。
如果 20 元库存为 0,程序会自动跳过 20 元,从 10 元开始计算。这就是库存受限后的找零逻辑。
代码测试:
调试时可以用监控表改 ChangeAmount 和 CoinStock,观察 CoinOut、RemainAmount 和 Valid。建议做下面几组测试:
1. ChangeAmount = 37,库存充足,确认结果为 20、10、5、2 各 1 枚。
2. 把 20 元库存改成 0,确认程序改用 10、10、10、5、2 等组合。
3. 把 1 元库存改成 0,再测试 ChangeAmount = 39,确认可能出现 RemainAmount 不为 0。
4. RequestCalc 为 FALSE 时,确认输出清零或保持默认无效结果。
5. Count 改成 3,确认只使用前 3 种面额参与计算。
这些测试能看出一个关键点:找零成功不只看算法,还要看库存是否够。
工程注意事项:
第一,面额数组必须从大到小排列。
贪心算法依赖顺序。你把 1 元放在第 1 个,程序就会先用 1 元找零,结果当然会变得很难看。
第二,贪心算法不是所有面额体系都最优。
常见的 20、10、5、2、1 这种面额,用贪心通常很自然。但如果你设计了一套奇怪面额,比如 4、3、1,某些金额下贪心不一定得到硬币数量最少的结果。工程里如果面额体系不是标准组合,就要单独验证。
第三,计算结果不要直接等于出币动作。
coinOut 只是计划。真正出币时,还要有电机控制、计数反馈、卡币检测、超时报警和库存扣减确认。计划和执行要分开,这一点很重要。
第四,金额单位要统一。
如果项目里有角、分、小数金额,建议统一放大成整数,例如全部按“角”计算,避免 REAL 小数带来比较误差。
常见问题:
第一,只算金额,不看库存。
纸面算法能找出来,不代表机器吐得出来。PLC 程序必须知道每个币盒里还有多少。
第二,找零失败没有输出原因。
RemainAmount 非常有用。它能告诉你到底还有多少金额没法找出来,HMI 上也能据此提示“零钱不足”。
第三,把出币数量算成临时变量。
HMI、调试和后续执行都需要看到 coinOut 数组。不要藏在 TEMP 里一闪而过。
第四,一次计算和连续执行混在一起。
找零计算可以一拍完成,但出币动作通常要一枚一枚执行。建议先算计划,再进入出币状态机。
老炮儿建议:
把找零计算写成 FC,把出币执行写成 FB,结构更清楚。
面额、库存、出币计划都放到 DB 里,方便 HMI 显示和调试。
如果要做真实设备,必须增加出币反馈、超时报警和库存校验,不要只停留在计算层。
如果面额组合不规则,先用离线测试把各种金额跑一遍,再决定能不能用贪心。
总结:
本例用 SCL 写了一个自动售货机找零算法。
它的核心是贪心算法:从大面额开始,结合库存限制,尽量用较少的硬币完成找零。
这个例子轻松,但里面的工程习惯并不轻:输入边界要限制,结果要有 valid,失败要有 remainAmount,计算和执行要分开。
很多 PLC 小程序写得是否稳定,差别就在这些地方。
实例分享链接:
https://pan.quark.cn/s/8aee8f37a0a1