本节摘要:结构体把相关信号抱团,数组把同类结构排成队列,自定义类型把抱团规则命名沉淀——三者叠加,是从"零散变量"走向"数据建模"的三级台阶。本节沿着"追踪一瓶水"这条线索,完整设计出青线的瓶体结构与配方容器。
3.1节解决了单个变量的格子问题,但零散格子撑不起一条产线:灌装工位一个瓶位就有到位、剔除、灌装完成、瓶盖检测四个信号,八个瓶位就是三十二个变量,命名写到怀疑人生还容易张冠李戴。这一节讲的就是怎么把它们组织成有结构的数据。
青线的瓶体追踪是数据设计里最有代表性的一道题:每个瓶位在产线上流动,系统要随时知道"这个瓶位里有没有瓶、是合格瓶还是待剔除瓶、灌装了几号配方"。散装变量方案要为八个瓶位各起一套名字,而结构体方案先定义"一个瓶位长什么样":
TYPE Bottle_Slot : // 一个瓶位的全部家当 STRUCT Present : BOOL; // 瓶在位 Filled : BOOL; // 已灌装 Recipe_No : INT; // 所用配方号 Fill_Time : TIME; // 灌装耗时(诊断用) Reject : BOOL; // 待剔除标记 END_STRUCT; END_TYPE VAR Slots : ARRAY[1..8] OF Bottle_Slot; // 八个瓶位一排 END_VAR
这段声明里藏着三个台阶。第一级,STRUCT把五个相关信号抱成一个"瓶位",从此访问写作Slots[3].Present,读到名字就知道是三号瓶位的是否在位——结构体给信号上了户口。第二级,ARRAY把八个瓶位排成一排,处理逻辑可以用循环遍历:第八个瓶位与第一个的处理代码完全相同,只是下标不同——数组让逻辑从"复制八遍"变成"循环一遍"。复制与循环的差别不止行数:复制八遍的代码在第二次改动时就可能出现七处一致一处漏,循环遍历的结构性一致从根上消灭这类偏差。第三级,TYPE给这个抱团规则起了名字,任何程序块都能声明自己的瓶位数据——自定义类型让数据结构本身成为可复用的资产。
自定义类型(很多平台称UDT)的工程价值在变更时兑现。项目中期工艺提出:瓶体追踪要增加"瓶盖扭矩抽检结果"字段。因为全线引用的是同一个Bottle_Slot类型,我们改类型定义一处,所有引用该类型的变量自动长出新字段,编译器还会把每个需要赋新字段的代码位置报出来——一次结构变更从"全线大搜查"变成"改一处、补赋值"。这一改动全程四十分钟,其中三十分钟花在给两个既有逻辑补上新字段的默认值——字段的"出生登记"比出生本身更费事,这是数据字典要提前想到的。
反过来,如果当时八排瓶位是手抄的八套散装变量,这次变更就是三四十处代码的散弹枪修改,漏一处就是一个潜伏到联调甚至投产才现形的bug。数据建模的回报周期看起来长,其实很短:第一次结构变更就把设计时间全数赚回。
字段取舍是自定义类型真正的功夫。青线评审 Bottle_Slot 时砍掉了两个字段:候选字段"瓶体批号文本"被删,理由是瓶级批号由喷码机与MES管理,PLC里存文本纯属重复登记,两处数据迟早打架;候选字段"预估温度"被删,理由是没有任何逻辑读它。数据结构的纪律与3.1节一脉相承:字段必须有读者,没有读者的字段是负债不是资产。
| 容器 | 适用场景 | 青线实例 | 纪律 |
|---|---|---|---|
| STRUCT | 相关信号抱团 | 瓶位、阀位反馈组 | 字段必须有读者 |
| ARRAY | 同类结构成批 | 八瓶位、六电机 | 循环遍历,禁止手抄展开 |
| UDT | 抱团规则命名化 | Bottle_Slot、Motor_UDT | 一处定义全线引用 |
| ARRAY OF UDT | 成批的设备 | 全线阀组 | 命名带语义区间 |
结构体的真正威力在嵌套:结构体成员可以是另一个结构体。青线的瓶位结构里嵌了一个"诊断子结构"——最后动作、动作时刻、连续失败次数——工位级数据与诊断数据一体两面,装进同一个户口。嵌套的纪律是深度控制:两层为宜,三层封顶,过深的嵌套让访问路径Slots[3].Diag.LastAction之后还要再一层,可读性迅速滑坡。这与2.4节"功能块嵌套不超两层"的纪律同源:数据与代码的复杂度都要摊平了放。
数组与嵌套组合出青线最常用的形态——ARRAY[1..8] OF Bottle_Slot之外,配方库是ARRAY[1..20] OF Recipe_UDT,电机数组是ARRAY[1..6] OF Motor_UDT。循环遍历它们时有一个易错点值得点名:数组下标越界在部分平台的运行时不报错、只是读写到相邻内存——这类错误的症状("隔壁设备的状态莫名变了")极具迷惑性。防御写法是遍历用常量边界、动态下标先校验再访问,把越界拦在发生之前。
容器虽好,两种场景要克制。反模式一:把两个毫不相关的变量硬塞进一个结构"图省事"——结构与结构的读者绑在一起,塞进无关字段等于给每个读者递一张冗余名片,字段变更时无辜者陪着改。反模式二:用一个巨型UDT装整台设备的全部数据(一百多个字段)——它是3.4节会被批判的"大杂烩类"的数据版前身,正确做法是按职责拆分:控制数据一组、诊断数据一组、统计数据一组,各自成为小结构再组合。结构的粒度应该跟着"被一起读写的频率"走:总是同时读写的数据才值得同居一屋。
瓶位之外,配方是结构体的第二个主战场。青线的灌装配方含七个参数:目标液位、灌装时限、冲洗时长、电导率阈值、旋盖扭矩、剔除窗口、放行延时。把这七个参数打包成Recipe_UDT,再排一个ARRAY[1..20] OF Recipe_UDT,就是二十张配方槽位;切换配方时一条语句整体搬运,再也不存在"换配方漏改一个参数"这类事故。
// 配方切换(青线HMI确认后调用) Cur_Recipe := Recipe_Lib[Sel_No]; // 整包搬运,杜绝漏项
老线换配方靠纸质单据逐项手改参数,工艺员最怕改到一半被打断——回来忘了改到第几项。结构体整体搬运在机制上消灭了这个风险:要么全换完,要么没换。数据的组织方式本身成了一种防错机制,这是本节比语法更重要的启示。
结构体与数组还有两件常被轻视的小事。第一件是初始化:全局数据块有初始值机制,但"声明即初始化"只在下载或冷启动时生效——运行中生成的数据并不会自动回到初值。青线的规矩:凡参与控制判断的结构,使用前显式清零或赋初值,放在启动任务或状态机入口里,绝不依赖平台的隐式行为。第二件是拷贝语义:结构体整体赋值是逐字节搬运,源与目标的类型必须完全一致——改过一次UDT定义后忘了重新编译所有引用块,是整体搬运失效的头号原因。两件小事合并成一句口诀:容器的生命周期要自己签字,别让隐式行为代签。
容器建好了,下一节回到物理层:这些变量在CPU里住哪个地址、断电后谁还能活着。