3、模块化设计原则:高内聚低耦合原则在PLC程序中的应用,接口定义与数据隐藏

好,咱们接着聊模块化设计。前面我讲了为什么要拆分程序,怎么拆分。但光拆分还不够,你得知道拆得好不好。评判标准是什么?就是两个词:高内聚低耦合

这两个概念,说白了就是——一个模块内部要拧成一股绳,模块之间要尽量少扯皮。我在现场调试时见过太多反面教材了,一个功能块改了,结果七八个地方跟着报错。嗯,这就是耦合太紧的典型症状。

3.1 什么是高内聚?

高内聚,指的是一个模块内部的所有元素,都服务于同一个单一目标。你想想看,一个电机控制功能块,里面既管启停逻辑,又管报警记录,还顺带算了一下产量——这就乱套了。

我个人习惯,一个功能块只做一件事。比如:

  • FB_MotorControl:只负责电机的启停、正反转、速度给定
  • FB_AlarmHandler:只负责报警的采集、确认、复位
  • FB_ProductionCounter:只负责计数值的累加和清零

为什么要这么较真?我在项目中遇到过一件事:一个同事把温度PID控制和阀门反馈校验写在了同一个功能块里。结果PID参数整定时,阀门反馈的滤波时间常数改了一下,PID输出也跟着抖。查了两天才发现是内聚不够,数据互相干扰。

高内聚的好处:
  • 模块内部逻辑清晰,容易理解和维护
  • 修改一个功能时,不会波及到其他功能
  • 复用性高,拿过来就能用

3.2 什么是低耦合?

低耦合,就是模块之间的依赖关系要尽量少。两个模块可以通信,但不能你中有我、我中有你。

举个例子。你有一个FB_TankLevel(液位检测模块),它输出当前液位值。另一个FB_PumpControl(泵控制模块),它需要根据液位来决定启停泵。

低耦合的做法是:FB_PumpControl只读取FB_TankLevel输出的液位值,不直接访问它的内部变量。更不要在里面写“如果液位大于50.0,就启动泵”这种硬编码。

为什么?因为一旦液位判断逻辑变了,比如改成“液位大于45.0且持续3秒”,你就要改FB_PumpControl。这就耦合了。

正确的做法是:在FB_TankLevel里增加一个输出bHighLevel(高液位标志),由它自己判断。泵控制模块只管读这个标志。这样,液位判断逻辑变了,只改FB_TankLevel,泵控制模块纹丝不动。

我的经验:判断耦合度有一个简单方法——如果你改了一个模块的代码,需要同时改另外两个模块,那耦合度就太高了。赶紧拆。

3.3 接口定义:模块之间的“合同”

接口,就是模块对外暴露的输入输出参数。它像一份合同,规定了双方怎么打交道。

我建议接口设计遵循几个原则:

  • 接口要精简:能用一个BOOL解决的问题,别用三个。接口越多,耦合越深。
  • 接口要稳定:一旦确定,尽量不改。改接口就像改合同,所有签了合同的模块都得跟着改。
  • 接口要自解释:参数命名要一看就懂。比如bStartCmdin1好一万倍。

来看一个实际的接口定义示例:

FUNCTION_BLOCK FB_ConveyorControl
VAR_INPUT
    bStartCmd      : BOOL;      // 启动命令(上升沿有效)
    bStopCmd       : BOOL;      // 停止命令(上升沿有效)
    bEmergencyStop : BOOL;      // 急停信号(常闭触点)
    rSpeedSetpoint : REAL;      // 速度设定值(0-100%)
END_VAR
VAR_OUTPUT
    bRunning       : BOOL;      // 运行状态
    bFault         : BOOL;      // 故障状态
    rActualSpeed   : REAL;      // 实际速度反馈
END_VAR
VAR_IN_OUT
    stMotorRef     : REF_TO ST_Motor;  // 电机结构体引用
END_VAR

你看,这个接口很清晰。调用者只需要知道:给什么命令,读什么状态。至于内部怎么处理急停逻辑、怎么计算速度,那是模块自己的事。

注意:不要为了“方便”把内部变量也暴露出来。比如把PID的积分项系数作为输出,让外部去读。这等于把模块的肚子剖开给人看,耦合度瞬间飙升。

3.4 数据隐藏:把秘密锁在模块里

数据隐藏,就是模块的内部数据对外部不可见。外部只能通过接口来间接操作。

为什么要隐藏?

  • 防止误操作:外部程序不小心改了内部变量,可能导致逻辑混乱。
  • 降低复杂度:调用者不需要知道内部细节,只用关心接口。
  • 便于升级:内部实现改了,只要接口不变,外部不用动。

在PLC编程中,数据隐藏主要通过两种方式实现:

  1. 使用VAR(内部变量):这些变量只在功能块内部可见,外部无法直接访问。
  2. 使用结构化数据类型:把相关数据打包成一个结构体,只暴露必要的引用。

举个例子。一个FB_TemperatureControl,内部有PID参数、积分累加值、上次采样值等。这些都应该声明为VAR,而不是VAR_INPUT或VAR_OUTPUT。

FUNCTION_BLOCK FB_TemperatureControl
VAR
    rIntegralSum   : REAL := 0.0;   // 积分累加值(对外隐藏)
    rLastError     : REAL := 0.0;   // 上次偏差(对外隐藏)
    rLastOutput    : REAL := 0.0;   // 上次输出值(对外隐藏)
    tLastCycleTime : TIME := T#0MS; // 上次执行时间(对外隐藏)
END_VAR

我曾经见过一个项目,有人把积分累加值直接暴露成VAR_OUTPUT。结果操作员在HMI上不小心写了个大数,PID直接飞了,温度冲到了120度。嗯,这就是数据没隐藏好的代价。

避坑指南:我曾经在调试一个多工位协同系统时,发现两个功能块互相读写对方的内部变量。查了三天,最后发现是数据隐藏没做好。从那以后,我定了一条铁律:所有内部变量一律用VAR,绝不用VAR_IN_OUT暴露内部状态

3.5 高内聚低耦合的实际案例

咱们来看一个完整的例子。假设你要做一个物料分拣系统,包含传送带、传感器、推杆三个部分。

低内聚高耦合的做法(反面教材):

// 一个功能块干所有事
FUNCTION_BLOCK FB_SortingSystem
VAR
    bConveyorRun   : BOOL;
    rConveyorSpeed : REAL;
    bSensorTrigger : BOOL;
    bPusherOut     : BOOL;
    nProductCount  : INT;
END_VAR

这个功能块里,传送带控制、传感器读取、推杆动作、计数逻辑全混在一起。改传送带速度,可能影响推杆时序。改计数逻辑,可能误触推杆。耦合得一塌糊涂。

高内聚低耦合的做法(推荐):

// 模块1:传送带控制
FUNCTION_BLOCK FB_Conveyor
VAR_INPUT
    bRunCmd  : BOOL;
    rSpeed   : REAL;
END_VAR
VAR_OUTPUT
    bRunning : BOOL;
END_VAR

// 模块2:传感器检测
FUNCTION_BLOCK FB_Sensor
VAR_INPUT
    bSensorRaw : BOOL;
END_VAR
VAR_OUTPUT
    bDetected  : BOOL;
    tDetectTime: TIME;
END_VAR

// 模块3:推杆控制
FUNCTION_BLOCK FB_Pusher
VAR_INPUT
    bTrigger  : BOOL;
END_VAR
VAR_OUTPUT
    bExtended : BOOL;
    bRetracted: BOOL;
END_VAR

// 模块4:计数统计
FUNCTION_BLOCK FB_Counter
VAR_INPUT
    bCountPulse : BOOL;
END_VAR
VAR_OUTPUT
    nCount      : INT;
END_VAR

你看,每个模块各司其职。传送带只管转,传感器只管检测,推杆只管推,计数器只管数。它们之间通过接口传递必要的信息,内部细节互不干扰。

调用的时候,主程序只需要把它们的接口连起来:

// 主程序调用
fbConveyor(bRunCmd:=bStart, rSpeed:=50.0);
fbSensor(bSensorRaw:=bPhotoEye);
IF fbSensor.bDetected THEN
    fbPusher(bTrigger:=TRUE);
    fbCounter(bCountPulse:=TRUE);
END_IF

这样,任何一个模块出了问题,都可以单独修改、单独测试。不会牵一发而动全身。

总结一下:
  • 高内聚:一个模块只做一件事,内部逻辑紧密围绕这个目标
  • 低耦合:模块之间只通过接口通信,不直接访问内部数据
  • 接口定义:精简、稳定、自解释,像合同一样清晰
  • 数据隐藏:内部变量锁起来,外部只能通过接口间接操作

做到这四点,你的PLC程序就像搭积木一样,拆得开、装得上、改得动。