1. 八套程序九个补丁:为什么要赌一个统一FB
先给这篇文字定个调,我说的是工业自动化里最常见的场景:设备厂商做同一系列的多款机型,8个型号,PLC程序八成相似,两成不同。这里的ST就是IEC 61131-3里那个结构化文本(Structured Text),FB是功能块(Function Block)。顺便说一句,这个缩写太容易撞车——电源芯片上的FB反馈引脚、社交平台的FB广告,都叫FB,但和本文毫无关系。这篇讲的纯粹是PLC层面的代码复用架构:用一个设备模板FB,覆盖8种机型。
场景还原一下:我们做的是包装设备,产品线从基础型到全线型一共8个型号,机械结构一眼看过去就是同一个家族,但细节差异能列一页A4纸。早期项目交付时,每个型号都有一套独立程序,从A型复制改出B型,再从B型复制改出C型,美其名曰"改型设计",实际上就是整段整段地搬代码。这种做法刚交付时确实省事,但真正的麻烦发生在八个月之后——同时来了三个售后需求:A型要加一个剔除工位,C型要修一个复位时序的bug,E型的节拍要优化。每改一处,都得对着8套程序挨个打补丁。
1.1 代码复制不是开发效率,是维护债务
我见过太多的设备厂商倒在"复制改型"这条路上。第一台机器调试完,第二台复制过来改几个轴参数,两周交付,看着很爽。但等到第三个版本、第四个版本出来,版本库开始失控:同一台设备有17个"最终版"工程文件,其中两个显然是上次改完忘了同步;同一个报警逻辑在不同机型程序里写得完全不一样,修了一个漏了七个。
复制代码带来的不是开发效率,是维护债务。这个道理做软件的人都懂,但PLC圈子里很多人还在用二十年前的办法管理程序。原因也简单:设备交付后软件就冻结了,没人再改,所以复制的恶果要过很久才暴露。可一旦你的产品线还在持续迭代、还在接新订单、还在给老客户升级改造,这套债务迟早连本带利还回来。
1.2 那次夜班故障成了压垮骆驼的最后一根稻草
真正让我下决心推倒重来的,是一次夜班故障。客户现场的E型机报警,复位逻辑有问题,我们远程翻代码翻到凌晨两点,才发现E型的复位动作是从B型复制来的,而B型当时的复位流程和E型根本不是一个逻辑——bug在代码诞生的那一刻就种下了,只是等到现场才发作。
第二天我跟团队说了三句话,算是给这次重构立了军令状:第一,从下个平台版本开始,8个机型只允许存在一个设备模板FB;第二,差异全部做成配置数据,代码里不允许出现"机型专属"的散装分支;第三,任何人往模板里塞特殊逻辑,必须经过架构评审。后面所有工作,都是围绕这三句话展开的。
2. 动手前先盘家底:8种机型差异的二维拆解
说干就干之前,我们花了两周做差异盘点。这一步千万别省,因为"8种机型"这个数字本身没有意义,有意义的是差异的形状和分布。差异的形状决定你用哪种机制去抽象,差异的分布决定你的配置数据结构长什么样。
2.1 一维差异:参数、结构和时序
我把8个机型的差异整理成三类,这三类对应完全不同的处理手段。
第一类是参数差异,比如气缸到位时间、伺服速度、软限位、回零方式、节拍目标值。这类差异数据量大,但不改变逻辑结构,用配置结构体的字段去承载即可。比如A型轴速度上限是600毫米每秒,E型是1500,那就给每个轴的配置里加一个速度限制字段,逻辑代码里只读配置,不写字面量。谁要是直接在代码里写死某个速度值,代码评审直接打回。
第二类是结构差异,比如有没有涂胶单元、有没有贴标单元、是单工位还是双工位。这类差异的本质不是参数,而是"设备有没有这个模块"。公共流程里那些可有可无的步骤,需要按机型做条件启停。处理手段是模块使能标志位,加上对应的I/O映射表。结构差异处理不好,最容易出现"所有机型共用同一套变量,但某些机型某些变量永远没意义"的脏代码。
第三类是时序差异,也就是同样一个流程——上料、定位、装配、检测、下料——有的机型在装配前要预热,有的机型在检测后要剔除,有的机型装配是双工位交错的。这类差异最棘手,因为它动的是状态的先后关系,而不是参数的多少。处理不当,模板就会变成一堆嵌套的CASE,改起来心慌。
提示:盘点差异时,建议给每个差异项打三个标签:参数类、结构类、时序类。标签决定它在模板里落位的方式。如果某个差异既改结构又改时序,那它基本就是一台"新机型",要先评估是否超出一套模板的合理边界。
2.2 盘点结果长什么样
盘完之后的成果物,是一张差异对照表。这张表不能存在某个人脑子里,要贴在团队共享空间里,后面设计、编码、评审都对着它看:
| 差异项 | A型基础版 | C型胶机版 | E型全线版 | 差异类型 |
|---|---|---|---|---|
| 伺服轴数 | 8 | 12 | 24 | 结构 |
| 涂胶单元 | 无 | 有 | 有 | 结构、时序 |
| 贴标单元 | 无 | 无 | 有 | 结构、时序 |
| 单/双工位 | 单 | 单 | 双 | 结构、时序 |
| 节拍目标 | 15秒/件 | 12秒/件 | 8秒/件 | 参数 |
| 复位时序 | 标准 | 先回胶枪再归零 | 分段复位 | 时序 |
这张AB表看着简单,但它是后面所有设计的输入。没这张表,你设计出来的FB只有两种结局:要么过度设计,什么机型都能干,结果满屏CASE分支没人敢改;要么抽象不够,第9种机型一来就崩。所以我的建议是,盘点阶段宁可多花两周,把每条差异落到表格里,把差异类型标清楚,后面设计阶段就顺了。
2.3 一个反直觉的结论:差异10%的时候最该抽象
经验上有个反直觉的规律:两台设备完全相同,不需要抽象,直接写一份代码就行;两台设备差异极大,抽象成本高得离谱,硬凑一个模板会牺牲太多特异性能;恰恰是差异在10%到30%之间时,统一模板的收益最大。我们这8个机型,逻辑主干一致率大概在85%左右,正好落在甜区。
如果哪天某个机型的新工艺导致主干一致率掉到70%以下,我的建议是让它独立建工程,而不是继续往模板里塞开关。这个判断标准最好写进团队的开发规范里,写成硬性门槛。不然每来一个新需求,都有人想往模板里加一个"仅某某机型启用"的flag,加着加着,模板就变成了一个谁都不敢碰的巨兽。
3. 模板FB的骨架:接口、配置结构和状态机怎么设计
差异盘点完了,进入设计阶段。模板FB的设计我总结成一句话:对外,接口尽量小;对内,状态机尽量规矩;差异的部分,全部藏在配置数据后面。这一章讲骨架怎么搭。
3.1 FB对外接口:只开五个口子
FB的输入输出越少,调用方越不容易用错,模板也越好维护。我们把接口收敛成这样:
FUNCTION_BLOCK FB_EquipmentTemplate VAR_INPUT bEnable : BOOL; // 总使能 eCmd : E_CommandType; // 命令:待机、自动启动、暂停、复位 iRecipeNo : INT; // 工单/配方号 stConfig : ST_MachineConfig; // 机型配置,运行期只读 END_VAR VAR_OUTPUT bBusy : BOOL; bDone : BOOL; bError : BOOL; wErrorId : WORD; sLastError : STRING(80); stStatus : ST_EquipmentStatus; // 统一状态字,供HMI和上位机读取 END_VAR注意,这里故意没有把每个气缸、每个轴的使能都拉出来当接口。真实设备控制中,手动、单步操作需要单独的点动面板,那是另一个FB的事;自动流程FB只管自动循环,接口越少,组合越干净。HMI要显示什么,统一从状态结构体里取,不会出现HMI直接访问模板内部变量的情况——那样等于把内部实现暴露给了上位,以后内部一重构,上位就崩。
状态结构体我们也做了统一设计,里面包含运行状态枚举、当前步骤号、当前激活的差异钩子编号、当前运动的轴名、累计循环数等。8个机型共用同一套状态字,HMI的报警页面、生产统计页面、诊断页面可以完全共用一套,不用每个机型单独画屏。
3.2 配置结构体:把"8种机型"翻译成一个UDT
ST语言相比梯形图的一个明显优势,就是用户自定义类型(UDT)非常成熟,你可以让配置结构体直接表达出设备的物理结构。核心配置大致是这样:
TYPE ST_MachineConfig : STRUCT eMachineType : E_MachineType; // 机型枚举 iModuleCount : INT; // 启用的可选单元数量 aAxis : ARRAY[1..24] OF ST_AxisConfig; // 轴配置,最多24轴 bHasGlueUnit : BOOL; // 是否带涂胶单元 bHasLabeller : BOOL; // 是否带贴标单元 bDualStation : BOOL; // 是否双工位 rCycleTarget : LREAL; // 目标节拍,用于效率统计 END_STRUCT END_TYPE每个轴的配置也是一个结构体:
TYPE ST_AxisConfig : STRUCT sAxisName : STRING(32); // 轴名,用在报警文本和HMI nAxisNo : INT; // 0表示本机未启用该轴 eAxisType : E_AxisType; // 伺服、步进、气缸 rVelocity : LREAL; // 默认运行速度 rAccel : LREAL; // 加速度 bDirectionInvert : BOOL; // 方向取反 rSoftLimitMin : LREAL; // 软限位下限 rSoftLimitMax : LREAL; // 软限位上限 eHomingMode : E_HomingMode; // 回零方式 END_STRUCT END_TYPE轴配置数组按最大轴数定义为24,这是设备家族里E型的上限。你可能担心浪费内存,但24个结构体撑死几百个字节,在PLC里可以忽略不计。重要的是,逻辑代码在遍历轴时永远用"轴计数变量+循环"的写法,而不是硬编码写成1到8。任何地方出现数字字面量,都会成为第9种机型入场时的定时炸弹。
配置结构体只做一件事:把"机型"这个概念翻译成数据。模板代码里看到的不是"A型""E型"这样的名字,而是一堆标志位和数值。代码和机型的解耦,从这一步就开始了。
3.3 状态机骨架:公共流程与差异钩子
模板内部是一个标准的状态机,骨架对所有机型一致。为了直观,我用伪代码示意:
CASE byStep OF 0: // 待机 IF bStartCmd THEN byStep := 10; END_IF 10: // 检查启动条件 IF CheckReadyConditions() THEN byStep := 20; ELSE RaiseError(1001, '启动条件不满足'); END_IF 20: // 上料(公共动作) If bLoadDone THEN byStep := 30; END_IF 30: // 定位(公共动作) DoPosition(); IF bPositionDone THEN byStep := 40; END_IF 40: // 差异钩子1:装配前处理,按机型分发 ExecModelSpecificStep(1); IF bHookDone THEN byStep := 50; END_IF 50: // 检测(公共动作) DoInspection(); IF bInspectionDone THEN byStep := 60; END_IF 60: // 差异钩子2:出料前特殊处理 ExecModelSpecificStep(2); IF bHookDone THEN byStep := 70; END_IF 70: // 下料,回到待机 DoUnload(); IF bUnloadDone THEN byStep := 0; END_IF END_CASE这里最关键的思维是:公共流程里不该出现按机型分发的代码。第30步定位,所有机型都有定位动作,那就写成公共段;第40步装配,A型是伺服压装,C型要先涂胶再压装,这时候才调差异钩子去分发。哪个动作属于公共段,哪个属于差异钩子,依据的是第2章的差异盘点表,不靠拍脑袋。
差异钩子是这套架构的核心机制,我给它起了个名字叫"extension point",就是刻意模仿软件工程里的扩展点概念。公共流程只负责把钩子按顺序调一遍,至于钩子里具体干什么,由各机型的配置数据和方法去决定。这样,当第9种机型需要在第50步检测之后多加一个动作时,不是去改公共流程,而是去配置一个钩子。
4. ST语言里怎么表达"差异":三种机制和我的取舍
很多PLC工程师误以为"代码复用"就是把公共程序抄到一个FB里。真正的问题在于差异怎么表达。ST语言里有三种常见的差异表达机制,我逐个说清楚,你选型时能少走弯路。
4.1 CASE枚举分发:最直接,但要控制粒度
第一种是在FB内部用CASE语句按机型枚举分发:
CASE stConfig.eMachineType OF eModel_Glue: GlueCycle(); // 涂胶工艺段 eModel_Labeller: LabellerCycle(); // 贴标工艺段 END_CASE这种写法的优点是直白、好懂,新人上手就能改。缺点是分支数量会随机型增长,CASE胀到十几个分支之后,每个人都在自己的分支里加私货,模板就开始腐化。
我的经验是:整段的分发——比如整个装配工段都不同——可以用CASE,但细粒度动作分发不要用。也就是说,CASE的粒度要控制在"工段"级别,别下沉到"某个气缸动一下"的级别。气缸级别的差异,都用配置数据去驱动,别用CASE。否则你会得到一个几百行的CASE块,每个分支里7个动作有6个相同,只有1个不同,这种情况下你该反思的不是CASE写法,而是为什么要在这个粒度上做机型分支。
4.2 配置数据驱动:让数据替你写逻辑
第二种是把差异变成数据。最典型的例子是"哪些步骤在哪个机型上执行"。我们给每个步骤打一个机型掩码:
TYPE ST_StepEntry : STRUCT nStepNo : INT; // 步骤号 eModule : E_ModuleType; // 执行模块:上料、装配、检测 dwModelMask : DWORD; // 位0~位7对应机型1~8,置1表示启用 rTimeOut : LREAL; // 超时时间,单位为秒 END_STRUCT END_TYPE然后在FB里用一个步骤表数组驱动主循环。判断某一步是否属于当前机型,就是一次位运算:
// 判断当前机型是否启用该步骤 IF (dwStepMask AND dwCurrentModelMask) <> 0 THEN // 执行该步骤对应的动作 END_IF这就是把"时序差异"也翻译成了数据。8个机型各自的流程步骤序列,不用写8份CASE,而是写8行配置。这个思路是从工艺配方表里借来的,本质上是把程序的骨架交还给配置数据管理,而不是锁死在代码里。
数据驱动是ST代码复用最值钱的手段,但有个前提:步骤执行的动作必须能映射到足够小的动作库。动作库里的每个动作符号得是稳定的,比如移动轴、夹紧、涂胶启动。如果你的动作库本身不稳定,今天叫这个名明天叫那个名,配置表就废了。所以动作库的设计要在模板骨架之前完成,并且要经过评审冻结下来。
4.3 FB方法:面向对象的差异封装
第三种是给FB添加方法,把不同机型的特有逻辑拆成独立方法,在公共流程里调用。这相当于PLC世界里的面向对象——FB就是类,方法就是成员函数。我们给模板定义了一个私有方法,专门处理差异钩子:
// 在FB内部声明私有方法 METHOD PRIVATE ExecModelSpecificStep : BOOL VAR_INPUT iHookId : INT; // 钩子编号,对应流程图里的差异点 END_VAR方法内部再按钩子编号分发,每个钩子内部再按机型枚举细分。这样做的最大好处,是每个差异点有一个独立的名字,排错时能直接从状态字里看到"当前执行到哪个钩子、钩子里的哪个分支";而且方法内部可以再调方法,把复杂差异逐级拆解。
代价是这要求团队有一定的抽象思维。纯梯形图出身的工程师,第一次看到"方法里面套方法"会非常不适应。我的取舍是:结构差异和参数差异全部走配置数据,只有动作级时序差异走方法。换句话说,能放进数据块里的绝不写进代码,代码里只放动作。一个可复用的FB模板,最理想的状态是:代码本身没有任何机型字样,机型只出现在配置数据和注释里。
5. 配置数据怎么管:8台机器的参数落地方案
模板代码写得再好,配置数据管理跟不上也是白搭。这一章讲落地细节:8台机器的配置放在哪、怎么装载、怎么和HMI配合、怎么防错。这些事看起来琐碎,但恰恰是决定模板能不能真正用起来的关键。
5.1 一个工程8个实例,还是8个工程各一个实例
先回答一个常见问题:8种机型,是一个主站里实例化8个FB,还是每个机型单独一个PLC工程?
我的答案取决于生产线形态。如果8台机器是单机交付、各自独立的PLC,那模板只是程序库里的一个公共FB,每个机型的工程自己实例化一次,配置是一个固定数据块。这种情况最干净,模板文件放入共享库,各工程引用同一个文件,机型差异只体现在各自的数据块里。
如果是整线项目、一个控制器带多台设备,那就用实例数组:
VAR aEquipment : ARRAY[1..8] OF FB_EquipmentTemplate; aMachineConfig : ARRAY[1..8] OF ST_MachineConfig; // 启动时从配方装载 END_VAR // 初始化装载 FOR n := 1 TO 8 DO aEquipment[n](stConfig := aMachineConfig[n], bEnable := xEnableLine[n]); END_FOR数组方式最大的好处是程序里可以写统一逻辑:比如月度保养时遍历所有实例检查运行时间,或者一键复位全部设备。但坏处是程序变量表看着大,而且某个实例FB内部如果出现异常,会影响整个主循环的扫描节奏。所以如果机型之间物理上就是独立电柜,我推荐还是各建各的工程,模板文件共享即可。模板文件本身的版本管理,下文会说。
5.2 配置的存放方式:数据块、配方还是CSV
ST工程里配置数据常见的存放方式有三种,我列个对比:
| 存放方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 全局数据块硬编码 | 简单直接,下载程序即生效 | 改参数要改程序、重新下载 | 出厂后工艺冻结的设备 |
| 配方数据块 | HMI可切配方,操作工可调 | 跨机型切换不安全 | 同机型不同工艺参数 |
| CSV/XML导入 | 售后可离线改参数,灵活 | 需要做格式校验和容错 | 通用柜体、多机型共用PLC |
我的建议是分开管理:机型级配置,也就是结构差异相关的枚举、轴号、模块使能,用配方或CSV;工艺级参数,比如速度、节拍、压力阈值,用配方。两者要分开,别混在一个数据块里。混在一起的结果是,操作工想调节拍,不小心把机型枚举给改了,设备直接起不来,你还得远程猜原因。
我们在后期给售后用的是这种组合:柜内存储卡里放一份机型参数CSV,开机时PLC读取并做校验,通过后装载到配置结构体;工艺参数则放在HMI配方页里,操作工只动这个页面。两个数据域物理隔离,从机制上杜绝了误改。
5.3 配置校验:宁可报警也不瞎跑
模板FB里一定要写配置校验逻辑。上电后先跑一遍校验,检查枚举是否合法、轴号是否越界、启用的模块对应的I/O映射是否完整。不合法就直接拒绝进入自动模式,并给出明确的错误码。
// 上电校验示意 IF stConfig.eMachineType = eUnspecified THEN RaiseError(9001, '机型未配置'); ELSIF stConfig.iAxisCount > 24 THEN RaiseError(9002, '轴数超过模板上限'); ELSIF NOT CheckIOMappingValid() THEN RaiseError(9003, 'I/O映射不完整,请检查配置文件'); END_IF这步很多人忽略,觉得"参数都是我自己配的,怎么会错"。但现场调试时什么都会发生。尤其当配置从CSV导入时,某一行文本编码错了、列对不齐了、换行符不对了,如果模板不校验,设备会在运行到一半才暴露一个诡异的轴动不了,排错成本巨大。配置校验花半小时写,能省现场三天,这笔账怎么算都划算。
6. 上线后的真实战场:迁移、排错和新增第9种机型
模板设计得再漂亮,迁移和排错才是真正的考验。这一章讲我们上线后遇到的三类真问题,以及相应的应对方式。
6.1 存量机型迁移:先冻结行为再动代码
我们有4个老机型已经在客户现场跑着,迁移不是推倒重来,而是要行为等价替换。我们的办法是:先把老程序跑起来,用报文记录所有关键软元件的动作序列,形成一份行为基线;新模板迁移后,在离线仿真模式下跑同样的生产流程,对比动作序列是否一致。不一致的先列出来,逐条判断是新模板的正确行为,还是迁移引入的回归。
这个步骤非常花时间,但值得。我们吃过一次亏:有个机型迁移时,模板逻辑是全新的,动作序列和老程序差了一位——某个夹具的松开时机提前了一个步骤,结果现场首件就撞坏了治具。从那以后,行为基线对比成为迁移的硬性流程,不允许跳过。具体操作是写一个简单的录制功能,把步骤号、钩子号、动作使能位、轴位置按时间戳存进环形缓冲,然后离线导出对比。模板的接口统一之后,这套录制逻辑只写一份,8个机型通用。
6.2 排错:统一状态字和步骤回放
模板化的一个好处是排错逻辑统一了。每个实例都有统一状态字,里面包含当前步骤号、当前激活的钩子、当前运动的轴名。HMI上直接显示"设备当前在第40步,装配钩子1,轴Y2运动中"。8个机型共用一套HMI诊断页面,售后工程师不用再对着不同机型的程序手册找变量。
更实用的是做步骤回放:把最近100个步骤的进出时刻、超时与否、报警码记录成环形缓冲,故障发生后可以读出来看最后几步到底发生了什么。这个功能在8种机型共用一套模板后变得非常便宜,因为记录逻辑只写一份。排错时,远程指导客户按下"导出诊断数据",几秒钟后拿到一份文本文件,里面从上电开始的所有关键事件都在。比起让客户现场拍屏、描述现象、我们盲猜,效率完全不是一个量级。
6.3 新机型入场:第9种机型到底要改什么
新机型入场是模板架构真正的试金石。我们的经验是,第9种机型来了之后,先对照第2章的差异盘点表跑一遍:
- 如果只是参数差异,只填配置,不动代码,工作量按天算;
- 如果是结构差异,比如多了一个单元,需要新增一个模块FB,并在步骤表里插入几步,工作量按周算;
- 如果是时序差异,整个流程都变了,先评估主干一致率,还在70%以上就用钩子方案扩展,否则建议独立工程。
这套流程写进团队规范后,新机型开发的评审会变得很快。大家拿着差异盘点表逐项对照,而不是坐在会议室里争论"要不要新写一套程序"。效率和可预期性都上来了。第9种和第10种机型我们各花了不到一个月完成调试,这在以前是根本不敢想的速度——以前光是复制改型加测试,没有两个月下不来。
7. 从这次重构里沉淀下来的几条工程判断
最后写几条纯粹的个人体会,不占据正确性,但都是这次项目换来的教训。
7.1 模板版本、配置版本、工程版本要三线分开
模板的版本管理要三线分开:FB模板版本、配置数据结构版本、各机型工程版本。FB改了,配置结构可能不变;配置加了字段,FB版本也要跟着升。建议把版本号写进FB的注释头,并在启动时做一次比对——把模板版本和配置版本读出来比对,不匹配就报警。
这事我们栽过跟头:售后把新配置灌进了旧模板的工程,新字段全被当成垃圾数据,设备动作全乱。排查了很久才发现版本不匹配。从那以后,模板里加了一个启动自检,版本对不上直接拒绝运行,并给出"请更新PLC程序"的提示。
7.2 保留"虚拟机型"测试模式
一定要保留一个虚拟机型测试模式——用一个仿真枚举位,让模板在没有任何真实I/O的情况下,用虚拟反馈跑完整流程。8个机型每次代码变更后,都要用这个模式跑一遍回归。
没有这个模式,你根本不敢动公共代码。改一个超时参数都可能影响8个机型的行为,靠真人拿真实设备测试,一次回归要占产线半天,业务部门不骂人才怪。虚拟机型模式的代码成本不高,但很多项目第一版都没做,结果就是模板改起来人人自危。现在我们的流程是:任何公共代码变更,先在虚拟模式跑完8个机型的动作序列,再出自动化对比报告,最后才允许提交。
7.3 别把特殊功能硬塞进模板
最实在的一条:别把特殊功能硬塞进模板。模板的边界要设得早,凡是"只有未来某机型才会用"的功能,宁可留扩展接口,也别预先写进公共流程。很多人死于过度设计——模板里躺着一堆从未被任何机型用到的分支,没人知道它们为什么存在,也没人敢删。这种死代码比没有模板更危险,因为它让每个后续开发者在阅读公共代码时都要背着额外的理解成本。
代码复用架构的本质,不是把所有东西塞进一个FB,而是把稳定的主干和变化的部分清晰分开,让主干足够小,让变化足够便宜。判断的主干是否足够"小",有一个很朴素的标准:改一个公共动作,能否在半小时内想清楚它对所有机型的影响。如果不行,说明主干承载了太多不该它管的东西。
就这些。这套架构从立项到8个机型全部迁移完成,花了我们大约10个月,中途有两次差点放弃,一次是迁移回归对不上的时候,一次是第2台机型现场调试出现诡异时序的时候。但坚持下来的回报,是之后18个月里新增的两台机型,每台从出图到出厂调试只用了以前三分之二的时间。如果你们也在为同一系列的多种机型维护多套重复代码,不妨也先盘一盘差异,画一张二维对照表,再决定要不要赌这一把。