这次做固件扩容时,我一开始有一个很自然的想法:既然已经在固件里加入了 6 个效果,那就继续往上加,10 个听起来也并不夸张。效果号还有空间,描述符也已经能够找到,数组里似乎还留着空位。只要把新增效果接进去,事情看起来就完成了。
但真正把约束一层层摊开以后,我发现“还能不能再加”不是一个由单一数字回答的问题。效果号、描述符、菜单容量和默认值写入域,分别属于不同层次。它们任何一个没有对上,扩容都可能只是把一个局部可行的改动,变成整体不可用的固件。
固件扩容的第一步不是数还能塞多少,而是确认每一层到底允许什么。
先别被效果号和描述符说服
最先让我产生乐观判断的,是效果号上界和描述符数量。复核后,效果号上界是 31,描述符数量是 34。单看这两个数字,很容易得出一个结论:编号没有用完,描述信息也有余量,继续增加效果应该不会有大问题。
这个判断的问题在于,它把“系统能识别”误当成了“用户能使用”。效果号解决的是身份,描述符解决的是说明和关联,它们只能证明某个效果在某些内部结构里有位置,并不能证明菜单能够展示它,也不能证明初始化时会给它写入正确的默认值。
如果不把这些层次分开,后面会出现一种很难排查的结果:固件内部能找到新增效果,某些检查也能通过,但用户在菜单里看不到它,或者第一次使用时拿到的是未初始化的参数。表面上是扩容成功,实际是功能只完成了一半。
这件事让我重新意识到,逆向和改造工作里最危险的不是没有数字,而是数字太容易让人提前放心。一个上界、一个数量,只有放回完整的数据流里,才知道它们究竟代表能力,还是只代表存储空间。
真正的硬约束是菜单容量
继续往下看,菜单只有 20 格。这是整个判断发生变化的地方。效果号上界是 31,并不意味着菜单也有 31 个可用位置;描述符有 34 个,也不代表用户界面能同时容纳 34 项。对这次扩容来说,菜单容量才是新增效果真正面对的硬约束。
当菜单已经接近或达到上限时,再增加内部编号,只会制造一种“已经加入”的错觉。新效果可能存在于固件的数据结构中,却没有可见入口。更麻烦的是,原厂效果和新增效果之间还要发生真实取舍:如果不替换已有项目,就没有位置给新项目;如果直接挤掉项目,又必须说明牺牲了什么。
可用槽位不是数组里的空位,而是从识别、展示、初始化到实际操作都能闭合的一条链。
所以,菜单容量不能放到最后才检查。它应该在方案筛选之前就被画进约束图里。否则,前面花很多时间证明效果号有效、描述符完整,最后仍然要因为菜单装不下而推翻方案。
默认值写入域不能靠照搬
菜单容量解决的是“能不能看见”,默认值写入域解决的是“第一次使用时是什么状态”。这次继续核对时发现,新增槽的默认值初始化区域只覆盖原有 5 款,不能直接照搬到第 6 款以后。
这里最容易误判。既然第 6 款已经在某处出现,直觉上就会认为,只要把第 5 款的初始化逻辑顺延一次,第 6 款以及后面的效果就能获得同样的默认值。但初始化区域不是一个可以凭感觉延长的名单。它有自己的边界,边界之外可能没有对应的写入位置,也可能没有经过验证的映射关系。
如果忽略这个问题,扩容后的效果就可能带着空值、旧值或不匹配的参数启动。用户看到的不是一个稳定的新效果,而是每次进入时表现不一致的状态。更糟的是,这类问题未必在静态检查中立刻暴露,往往要到实际操作时才发现。
因此,默认值能力必须单独列为一个判断维度。它不能因为菜单能显示、编号能识别,就被默认视为已经完成。对新增槽来说,“有没有写入域”比“有没有名字”更接近真实可用性。
从 10 槽退回 8 槽
最初的方案是扩到 10 槽。这个数字有一种明显的吸引力:它整齐,也让人感觉一次把空间利用到位。但菜单只有 20 格之后,10 槽就不再是一个单纯的目标数字,而是一个会挤压原厂能力的方案。
我后来把方案改成先做 8 槽,不是因为 10 这个数字本身不漂亮,而是因为 8 槽更容易在现有结构里完成闭环。它仍然能带来明显扩容,同时减少需要替换的原厂项目,也降低一次改动覆盖太多未知区域的风险。
我选择先做 8 槽,是因为它在菜单容量、原厂保留和验证范围之间留下了可控余量。
这里的“保守”不是少做一点就算了,而是把一次改动拆成可以回滚和比较的阶段。如果 8 槽方案能够稳定运行,下一步是否继续扩容,就有真实结果可以参考;如果一开始就冲到 10 槽,出现问题时很难判断到底是菜单替换、默认值还是新增结构造成的。
顶替比硬改结构更适合第一步
在可选路径里,我把方案分成结构改动、顶替改动和默认值能力 3 类。结构改动最直接,却也最容易牵动代码和数据布局;默认值能力决定新增效果是否稳定,但不能凭空补出没有验证的写入域;顶替改动则是在菜单已有位置里重新安排内容。
这次我更愿意先走零结构改动路径:只写颜色矩阵和多语言名称,不改代码立即数。它并不能解决所有扩容问题,却把变化限制在已经明确的内容区域里。为了腾出位置,最终牺牲最不常用的 2 个原厂效果,保留其余原厂能力。
这个取舍并不完美,但它的边界清楚。少掉的是什么,新增的是什么,哪里没有动,都可以单独记录。相较于直接扩数组、改跳转或重排未知结构,顶替方案更容易恢复,也更容易做前后对照。
如果不先控制结构变化,后果不只是某个效果不能用,还可能把原厂功能、菜单索引和默认值逻辑一起带进未知状态。到那时,所谓扩容就变成了系统性重写,问题的规模也会失去控制。
一张约束图比一个目标数字可靠
这次形成的可复用方法,是先画完整约束图,再决定扩容数量。第一层看效果号:确认编号上界和实际调用是否一致;第二层看描述符:确认名称、说明和关联关系是否齐全;第三层看菜单:确认新增效果是否有真实展示位置;第四层看默认值写入域:确认首次使用时是否有可验证的初始化来源。
4 层都通过,才把方案放进下一轮比较。比较时不能只列“能增加多少”,还要同时列出牺牲项、不可逆风险和独立验收门禁。这样,10 槽和 8 槽就不再是两个凭感觉竞争的数字,而是两组不同的代价组合。
我还会把方案分成 3 种状态:可以不改结构完成的顶替,必须改结构才能完成的扩展,以及默认值能力尚未闭合的半成品。第 3 种不能因为菜单里出现了名字就算交付。它应该留在待验证区,等写入域和实际表现都有证据后再推进。
一个扩容方案真正成熟的标志,不是它塞进了多少效果,而是每个新增效果都能说明自己为何存在、如何初始化、在哪个位置被使用,以及出问题后怎样退回。
这件事让我重新意识到,技术上的“更多”经常只是结构上的更多。对用户来说,只有可见、可用、稳定且不破坏原有能力,才算真正增加。扩容不是把容量填满,而是在有限约束里留下值得承担的变化。
下次我会怎么做
下次再遇到类似的固件扩容,我会先问 4 个问题:效果号的上界在哪里,描述符是否完整,菜单还能容纳多少,默认值写入域是否覆盖新增槽。4 个问题没有回答完之前,不先承诺目标数量,也不把数组空位当成可用槽位。
然后我会把方案按结构改动、顶替改动和默认值能力拆开,逐项写出牺牲项、回滚方式和验收门禁。能零结构改动完成的,先用最小范围验证;需要改代码立即数的,单独提高风险等级;默认值尚未闭合的,明确标为未完成,不用“看起来能启动”替代证据。
这次从 10 槽退回 8 槽,最后牺牲 2 个最不常用的原厂效果,表面上像是少做了 2 个位置,实际上是保住了整体可控性。以后我会把这种取舍提前写出来:扩容数量不是愿望,而是 4 个约束共同算出来的结果。