1. 开工之前先想明白:密码锁在PLC里到底怎么“锁”的
1.1 为什么是PLC来做这件事
又到了案例系列更新的时候,这是第4个:S7-200 PLC 6位密码锁程序。名字听起来很直白,就是给机柜门、操作面板或者实验室设备加一把电子锁,密码输入正确才允许操作,输错一定次数就报警并锁死一段时间。做这套东西的起因是我给某套小型控制柜做升级时,客户提了个需求:不想再额外装单片机门禁板,希望用现有PLC把权限管理一起做掉。
用PLC做密码锁,很多人第一反应是“杀鸡用牛刀”。但实际跑过项目就会发现,这个选择非常合理。设备柜里本来就有PLC,开关电源和继电器回路都是现成的,加密码锁只需要增加几个按钮和一只电控锁,几乎没有新增的电路设计成本。更重要的是,PLC程序可以远程下载和维护,密码规则、锁定时间、允许输错次数都能在程序里调整,不需要像硬件门禁板那样重新换板子或飞线。
另一个原因是可靠性。PLC是循环扫描执行程序的,天然没有单片机程序里那些复杂的“事件驱动”问题。按键信号采集、状态判断、输出驱动都在同一个扫描周期里被约束得很好,误动作概率很低。对于很多设备厂商来说,密码锁的真正目的不是防黑客,而是防误操作、防无关人员随意改变设备参数。比如配电柜、车载设备、生产线操作台,这些场合更需要的是“谁在什么条件下能操作”的管理逻辑,而不只是一把物理钥匙。
1.2 6位密码背后的可靠性取舍
为什么不用4位密码?因为4位密码只有1万种组合,对于连续暴力尝试来说太脆弱了。虽然PLC密码锁一般只控制设备操作权限,但万一现场有好事者反复试密码,4位的穷举时间并不长,在安全上明显不够。6位密码有100万种组合,配合输错次数限制和锁定计时,能把暴力破解的成本推到很高的程度。程序实现上,6位只是把比较逻辑从4位扩展出两条指令,硬件资源消耗几乎可以忽略,所以直接上6位是很划算的。
不过也要把丑话说在前面:PLC密码锁不是保险箱密码锁,它解决的是“应用授权”问题,挡不住那种把整个控制柜电都停掉的物理暴力。在设计时,程序里必须加入报警输出、锁定计时、错误计数这些机制,而不是单纯做密码比较。这也是我在这篇里会重点展开的部分:一个能用的密码锁程序,真正核心的不是“判断密码对不对”,而是“密码错了之后系统怎么反应”。
2. 硬件配置与I/O分配:一台普通S7-200能扛住这套程序吗
2.1 我用的CPU型号和接线思路
这个项目我使用的是S7-200 CPU224 DC/DC/DC,14个数字量输入和10个数字量输出,对于一套6位密码锁绰绰有余。实际上CPU222只有8个输入,刻薄一点说也够,但CPU224留出了很大余量,后面接急停、模式开关、外部状态信号都不用再改PLC本体。输入输出都是24VDC,现场按钮供电和PLC输入回路可以直接共用一组直流电源。
接线方面有一个细节很关键:S7-200的DC数字量输入既可以接成漏型也可以接成源型。实际施工时,如果按钮用的是常开触点,公共端的极性接反,会出现输入点一直亮或者信号完全不进来的现象。我习惯在通电前先查一遍按钮公共端与PLC输入公共端(1M、2M)的极性,再检查一遍常开触点是否接在输入端子与公共端之间。这一条如果忽略了,程序写得再漂亮也是白搭。
输出端Q0.0不直接带电磁锁,而是通过一只中间继电器去控制。电磁锁如果带直流线圈,一定要在继电器线圈两端反向并联续流二极管,否则分断瞬间的感性浪涌会直接冲击输出晶体管。Q0.1报警输出接声光报警器,同样先用继电器隔离。Q0.2是状态指示灯,指示“密码锁已锁定”状态,也可以省略,但现场调试时有个指示灯真的能省很多事。
2.2 输入按键和输出执行器的地址规划
I/O地址规划是这类程序最容易忽略的地方。做完之后我才发现,地址规划得好不好,直接影响程序维护时的心情。我使用的分配如下表:
| 信号 | 地址 | 说明 |
|---|---|---|
| 数字键0~3(BCD编码) | I0.0~I0.3 | 配合键盘模块,读取0~9 |
| 确认键 | I0.4 | 密码输入完成后的校验按键 |
| 清除键 | I0.5 | 输入错误时逐位清除或整体清除 |
| 管理模式开关 | I0.6 | 进入修改密码模式 |
| 复位报警键 | I0.7 | 报警复位和解除锁定 |
| 开锁输出 | Q0.0 | 驱动中间继电器控制电磁锁 |
| 报警输出 | Q0.1 | 驱动声光报警器 |
| 锁定状态指示灯 | Q0.2 | 提示当前密码锁处于锁定状态 |
数字键部分我直接用了带BCD编码输出的薄膜键盘模块,0~9十个按键压缩到4个输入点上。如果没有这类模块,用10个按钮接10个输入点也可以,只是输入点占用多。程序处理逻辑其实一样,区别只在按键识别那段:用10个独立按钮的场合,每个按钮一个网络写一个“数字值”到缓冲区;用BCD键盘的场合,多一步把4位二进制码翻译成数字值。为了演示方便,下面程序逻辑里的按键值来源我按“每个数字键最终把0~9的数值写入缓冲区”来写,具体是10个按钮还是BCD编码,不影响后半段的比较和输出。
内部位M区和V区我也做了规划。M0.0作为正确标记,M0.1作为错误标记,M0.2作为锁定标记;VW1020用来存放“已输入位数”,VB1999作为“当前按键代码”的临时寄存器;密码本体放在VB1000~VB1005,输入缓冲区放在VB1010~VB1015。这样划分之后,后面写子程序时不会出现地址乱飞的情况。
3. 程序功能拆解:从手指按下按键到锁舌弹出的全链路
3.1 六段式操作流程
密码锁程序的运行流程,从操作员角度看很简单:输入6位密码,按确认,锁开,过一会儿自动闭上。但把这段流程拆成程序逻辑,实际上是六个状态在循环切换:
- 上电复位:所有输出复位,锁定标记置位,等待输入。
- 密码输入:操作员逐个按下数字键,每次有效按键将数字写入缓冲区,同时已输入位数加1。
- 确认校验:按下确认键时,程序先检查已输入位数是否为6,不是则忽略,是则逐位比较。
- 验证通过:正确标记置位,开锁输出闭合,启动开锁保持定时器。
- 验证失败:错误计数加1,缓冲区清零,如果错误次数达到上限,则报警输出启动,同时进入锁定计时。
- 复位状态:锁定计时结束后,错误计数清零,报警复位,系统重新回到等待输入状态。
这其实是典型的有限状态机思路,只是用PLC梯形图实现时不需要显式定义状态机,只需要用一组M继电器和定时器把“当前处于哪个阶段”表达出来。我在编程时习惯把每一个阶段对应的输出条件写清楚,避免出现“开锁输出和报警输出同时有效”这种尴尬状态。
3.2 每个功能模块在程序里长什么样
按功能拆,程序可以分成四个子程序:按键解析、输入缓冲管理、密码校验、输出与定时控制。主程序OB1里只需要按顺序调用它们就行。
按键解析子程序处理的是信号抖动和有效沿。机械按钮按下去的时候,触点会弹跳几百微秒到几毫秒,S7-200的扫描周期通常只有几毫秒,如果不对信号做处理,一次按键可能被识别成两次甚至三四次。程序里我用了一个10ms的去抖定时器配合上升沿指令,只有稳定之后的边沿才被认为是有效按键。这一步在论文里看着不起眼,但现场没有它的话,密码输入的位数计数会疯狂跳变。
输入缓冲管理子程序负责把每次有效按键写入缓冲区。写入前,缓冲区的数据从后往前平移,新按下的数字放到第一个位置。这样处理的好处是代码整齐,不管按了几位,缓冲区的前面几位始终是最新输入的数字。配合一个“已输入位数”计数器,按下确认键时就可以知道当前到底输入了6位还是只有3位。
密码校验子程序只在确认键按下的那个扫描周期执行。先判断VW1020是否等于6,不等于就直接清空缓冲区并退出。等于6的时候,对6个密码字节和6个输入缓冲字节做逐位比较。S7-200的字节比较指令可以串联在一起,所有比较触点为真时,最终输出才为真,逻辑上很直接。
输出与定时控制子程序管理开锁时间、锁定时间、报警复位。开锁时间我用T37定时器,设为10秒后自动断电锁。错误锁定用T38,这里要注意定时器分辨率。T37的时基是10ms,预设值300就代表3秒;预设值3000代表30秒。我在调试时吃过亏:把3000当成毫秒数,结果实际锁定时间是30秒而不是3秒,最后对着手册整理了一张定时器分辨率表才理清楚。
4. 核心程序段编写:密码存储、输入缓冲与逐位比较
4.1 密码存储区的规划方式
密码本体我放在VB1000到VB1005,每个字节存一位数字。程序里默认密码是“123456”,即VB1000=1,VB1001=2,依此类推。为什么要用字节而不用整数VW?因为逐位比较时,字节比较指令最直观,密码判断就是六个字节判断的串联,一眼就能看出哪一位不匹配。如果用整数存储,还要考虑高低字节顺序和进制转换,维护成本反而高了。
这里有个电源保持的坑必须说。S7-200的V区默认不具备掉电保持功能,如果要让现场修改后的密码在断电后不丢,必须在系统块里把密码区对应的V区范围设置成保持型存储区,或者把密码写入数据块并下载到控制器。我曾经在一个设备上遇到“改了密码,断电重启后密码又变回123456”的问题,排查到最后才发现是V区保持范围没设置。这个细节不处理,密码锁程序就只能当演示程序用,没法交付到现场。
4.2 按键输入缓存如何逐位落位
输入缓冲区VB1010到VB1015也是六个字节。每次有效按键后,程序把缓冲区整体向左平移一位,也就是VB1011赋给VB1010,VB1012赋给VB1011,直到VB1015赋给VB1014,然后把当前按键代码写入VB1010。这是最典型的“最新输入进队首,旧数据依次后退”的做法,省掉了判断输入位数的分支逻辑。
按键解析里,我已经把0~9这十个键的识别结果统一成数字代码放在VB1999。比如按下数字键“5”,VB1999就被写入5。编制梯形图时,十个按键对应十个并联网络,每个网络都是一条上升沿指令加上一条MOVB指令。这个做法看起来有点土,但最不容易出错,而且调试时可以用状态表同时监控十个按钮的状态和VB1999的数值,哪里没通一目了然。
VW1020保存已输入位数。每次有效按键后加1,最大到6。按下确认键后,无论校验成功还是失败,VW1020都清零。确认校验里如果发现VW1020不等于6,甚至可以直接忽略这次确认操作,同时把缓冲区清零,这其实也是一种防误触设计。
4.3 逐字节比较再统一置位,以及开锁输出
校验部分的核心逻辑可以这样描述:只有当VW1020等于6并且确认键按下时,才开始比较。比较网络是六个字节相等触点串联,全部为真时置位M0.0正确标记。比较失败时,M0.1错误标记被置位,同时错误计数器加1。错误计数我放在VB1030里,每失败一次加1,当VB1030的值等于3时,启动锁定计时T38和报警输出Q0.1。
这里有一个容易犯的错:比较条件和确认键使用DELETE指令很容易把确认键的上升沿误用成电平时,导致操作员一直按着确认键的时候逻辑被反复触发。我后来规定:比较网络内只用确认键的一个上升沿右边沿作为允许条件,执行M0.0置位或者M0.1置位只发生在确认键弹下的那一次扫描。程序下载到现场后,这个“按一下只触发一次”的行为是否正常,是第一个要验证的点。
开锁输出逻辑干净利落:M0.0正确标记的上升沿置位Q0.0并启动T37,T37到时释放Q0.0。M0.1错误标记的上升沿清除缓冲区并增加错误次数。T38锁定计时到点后,同时复位Q0.1报警输出和VB1030错误计数。锁定期间内,除了复位报警键和清除键,其他数字键都被屏蔽,相当于系统进入一个拒绝输入的状态。
4.4 程序里用到的关键指令对照
为了减少翻手册的时间,我把这套程序里最常用到的S7-200指令整理出来,方便想要直接抄作业的朋友对照:
| 功能 | 指令 | 用途说明 |
|---|---|---|
| 上电初始化 | SM0.1 | 首次扫描时为ON,用于复位状态 |
| 常为真 | SM0.0 | 可作为某些网络的前置条件 |
| 上升沿 | EU | 按键有效边沿识别 |
| 字节传送 | MOVB | 把数字代码写入缓冲区 |
| 字节比较相等 | LDB= / 触点比较 | 密码逐位比较 |
| 整数比较相等 | LDW= / 触点比较 | 判断已输入位数 |
| 加1 | INCB | 错误计数和输入位数累加 |
| 定时器 | TON | 开锁保持和锁定计时 |
| 置位/复位 | S / R | 控制输出和状态位 |
5. 调试中遇到的坑和对应处理:计数、锁定与防抖的细节
5.1 输入抖动造成的重复计数
这套程序刚写完做内部测试时,发现一个现象:输入一个数字,有时缓冲区的位数会变成2甚至3。一开始以为是PLC扫描周期太快导致的逻辑错误,后来用万用表和示波器看了一眼按钮触点波形,发现是机械抖动捣的鬼。触点在闭合瞬间会出现多次通断,每次通断都被PLC当成一次有效信号,于是VW1020就往上跳。
解决办法是在按键解析子程序里给每个输入加了一个10ms的延时确认。我用的不是复杂滤波算法,而是最简单有效的做法:先将输入信号通过一个TON定时器,延时10ms后再去取上升沿。这样能过滤大部分机械抖动。对锁类应用来说,操作员按一个键的时间通常超过100ms,10ms的去抖延时不会影响操作手感。
5.2 错误次数锁定与解锁机制的时序问题
第二类坑出现在错误计数和锁定时间上。最初版本的程序是在比较失败后立即置位锁定标记,锁定标记一出来,输入全部被屏蔽,操作员想用清除键重新输入都不行。这在实际使用中非常不友好,一旦按错一个数字,整把锁就锁死几分钟。
后来我调整了时序:比较失败后只清除缓冲区,允许操作员重新输入,错误次数加到2次仍不锁定;只有当错误次数加到3次时才锁定。这个设计既保留了容错空间,也保证了安全性。锁定期间复位键也做了特殊处理:必须按住3秒以上才解除报警并复位错误计数,短按无效。这样防止误触到复位键就把报警轻轻松松消掉。
5.3 用Micro/WIN状态表排查V区数据和地址冲突
调试过程中最有价值的工具是Micro/WIN的“状态表”功能。把VB1000到VB1015、VW1020、VB1030、M0.0到M0.2全部添加到状态表里,PLC运行时就能实时看到这些地址的数值变化。密码输入对不上时,我第一时间不是看梯形图,而是看状态表里缓冲区每个字节的数值是否和按键一致。快速核对后能发现是按键值错误、缓冲移位错误还是比较逻辑错误,定位效率比盯梯形图高得多。
地址冲突也是实际项目中遇到过的问题。最开始我把密码区放在了VB100到VB105,结果程序里另一段与通信相关的代码也用了V100附近的地段区,密码被不定时改写成随机值。后来我改成将不同的V区用途按高位分段:VB1000以上全部给密码锁程序使用,其它功能模块使用VB500以前的空间,并在程序文件开头用一大段注释对V区分配做了记录。这个习惯沿用到了后续每个项目里,再也没出现数据互相覆盖的问题。
6. 按同一思路还能扩展什么玩法
6.1 多级权限与多人开锁
这套密码锁的底座其实是一个很通用的授权判断框架,稍微改改就能变成多级权限。如果现场不止一个操作员,可以分配不同密码组:一组密码能开锁但不能进入维护模式,另一组密码能开锁还能修改密码,第三组只能查看状态。实现方式是把VB1000之后的密码存储区扩展成三组,每组6个字节,比较时先用一个权限等级标志决定用哪一组密码进行6字节比对。
想做成“双人开锁”也可以:设定两个密码段,第一段和第二段由不同操作员各输入6位,程序把两个输入缓冲区分别与两组密码比较后,再输出一个与逻辑。这种功能在一些贵重仪器柜、两班倒交接的终端设备上挺实用,避免单一操作员拥有完整权限。
6.2 界面替换、密码修改通道与上位机联动
如果觉得实体按键不够直观,可以换成触摸屏。S7-200加一台小型HMI,界面里直接做数字键盘和状态显示,密码输入从物理按钮变成屏幕触控,操作体验会好很多。比较逻辑不用改动,只是输入来源从输入映像区I变成HMI写入的V区变量。这种情况下反而要更注意权限隔离,HMI密码和PLC密码要分开管理,避免任何人拿到触摸屏界面就能直接改V区的密码字节。
关于修改密码,我强烈建议不要在生产运行时做“任意密码可改密码”的逻辑。较稳妥的做法是:进入管理模式需要拨通I0.6并输入一次旧密码,然后对新密码进行两次确认输入,两次输入一致才写入密码存储区。密码变更之后必须同步考虑掉电保存问题,也就是前面提到的保持型范围设置,不然这个功能就是摆设。
另外,如果现场有上位机或者远程监控系统,密码锁程序还可以把开锁记录、错误次数、报警发生时间全部写到V区留档,再由上位机定时读取。这样就不只是简单开锁,而成了一套设备操作审计系统。某个时间段谁开过锁、有没有连续输错密码、报警是否及时复位,在监控界面上都能看到。调试这套系统时,我先后把通信和密码锁两个子程序分别测稳定,再联调,整个过程顺利很多。项目管理上的体会是:密码锁这种小功能模块,单独验证到位后并入大系统,比从头到尾一起联调要省力得多。