
我一直觉得做SKILL开发最怕的不是写不出功能而是写出来的东西只有自己能看懂过两个月连自己都看不懂。零散的脚本堆了一堆每次改需求都要翻半天代码牵一发动全身。这次我下了个决心不再用“写到哪算哪”的方式而是把整套SKILL重构成“框架细节”的双层结构并且用一份完整的测试项目文档来验证这套打法到底行不行。整个过程走下来有些收获确实超出预期今天把思路和实操记录下来。先说清楚这个项目是干什么的。SKILL是Cadence Allegro等EDA工具内置的扩展语言做PCB设计二次开发的人都不陌生画封装、导报表、批量改属性、检查设计规则基本都靠它。但SKILL脚本有一个天然的短板语法风格偏Lisp写起来很自由自由到几乎没有约束。做几个小工具还行一旦工具多起来命令分散、全局变量满天飞、函数依赖纠缠不清维护成本直线上升。我这次的目标就是把这套烂摊子收拢起来搭一个可复用的框架层把公共能力沉淀下来再把具体功能拆成独立的细节模块。同时用一份结构化的测试项目文档把每个模块的功能清单、用例步骤、预期结果全部落在纸面上让后续改动有据可依。如果你也是做EDA二次开发、写SKILL脚本写到想重构的工程师或者刚接触SKILL想从一开始就走对路的新手这篇内容应该能给你一些参考。文中会拆解框架层的设计思路、细节层的组织方式以及那份测试项目文档到底怎么编、怎么用。1. 为什么要做这次“框架细节”的尝试1.1 零散SKILL脚本的三宗罪先聊聊我过去的项目是什么状态。PCB设计组的同事陆陆续续提了几十个需求今天要一个一键导出坐标文件的命令明天要一个批量修改位号字高的命令后天又要一个检查差分对间距的命令。每来一个需求我就在allegro.ilinit里加一行load语句把新写好的.il文件挂进去。功能倒是都能跑但问题非常明显。第一宗罪是命令命名混乱。有的命令叫export_coord有的叫chg_text_size有的干脆叫t1、t2这种随手敲的名字。同事记不住我也经常记混Allegro命令栏里一敲tab弹出的列表又长又乱谁也分不清哪个是哪个。第二宗罪是代码重复率极高。十几个脚本里几乎每个都有一段获取当前设计文件名的代码、一段弹窗提示的代码、一段写日志的代码。当时觉得复制粘贴省事后来发现一旦某个公共逻辑要改就得把所有脚本翻出来挨个改累且容易漏。第三宗罪是完全没法测试。脚本之间互相影响全局变量在A脚本里设了个值B脚本读到的却是脏数据。出了问题根本定位不了只能靠printf往代码里埋点跑一遍看一遍效率非常低。这三宗罪叠加起来最直接的后果就是每次新需求进来我都要在上百个函数里先做一轮“考古”搞清楚哪段代码在干什么然后小心翼翼地打补丁。所谓“能用”不过是“暂时没出大问题”的委婉说法。1.2 框架细节一个可复用的分层思路既然零散脚本的路走不下去就得找一种更可持续的组织方式。我参考了软件开发里很常见的分层思想把它映射到SKILL开发里就成了“框架细节”的模式。所谓框架层就是所有SKILL脚本共享的底层设施。它不做具体的业务功能只负责三件事一是统一的入口和命令注册中心所有功能都通过一个统一的注册机制暴露给Allegro命令名规范、前缀统一、互不冲突二是公共的基础能力比如日志、错误捕获、用户交互封装、设计数据库访问封装这些能力写一次所有细节模块都能调用三是初始化与回收机制在Allegro加载SKILL时完成环境检查、全局变量清空、菜单动态加载等一系列准备动作避免脚本之间互相污染。所谓细节层就是具体的业务功能模块。每一类功能被拆成一个独立的文件文件内部只做一件事。比如位号处理相关的功能全放在refdes.il里铺铜检查相关的全放在shape_check.il里。模块之间不互相调用如果需要访问公共能力就调用框架层暴露的API。这样一分好处立刻体现出来新增功能只需要新建一个细节文件实现自己的逻辑然后在框架层的注册表里登记一下就行改动某个功能只影响自己这个文件不会波及其他模块公共逻辑改了所有模块自动受益。整个项目从一团乱麻变成了一棵清晰的树。1.3 为什么拿“测试项目文档”当验证方式光有代码层面的重构还不够我这次刻意把“编写测试项目文档”作为整个尝试的验证闭环。原因很简单过去写脚本几乎是写完就跑功能能出结果就算完事从来没想过“这个功能到底要达到什么标准才算合格”。标准缺失代码质量自然全靠自觉。测试项目文档实际起的是“契约”作用。在写代码之前先把每个功能模块的输入、输出、操作步骤、预期结果、异常场景全部定义清楚代码是实现这份文档的而不是文档为代码做解释。这相当于把“需求—设计—编码—验收”这个链路彻底理顺了。而且这份文档不是应付验收的装饰品。它后面还会承担回归测试的职责框架层或者某个公共API改了把文档里的用例跑一遍就能快速确认哪些模块受影响、哪些仍然正常。对一个SKILL工具集来说这种“可回归”的能力比多写几个功能点重要得多。2. “框架细节”双层结构怎么落地2.1 框架层骨架、生命周期、注册中心这次我搭的框架层核心是三个文件fw_init.il负责应用入口和初始化流程fw_util.il存放公共函数库fw_register.il维护命令注册表。三个文件各司其职组合起来就是整个SKILL工具集的“骨架”。fw_init.il里最关键的一段逻辑是初始化流程的控制。Allegro加载SKILL时会把allegro.ilinit里load的所有文件依次加载。但文件加载顺序和加载完成后的初始化动作是有讲究的必须先加载框架层的公共库再加载具体功能模块最后才能执行菜单创建、命令注册这类动作。如果顺序反了某个功能模块在加载时调用了还没定义的公共函数就会直接报错。我用一个全局状态变量来标记初始化阶段比如定义为fw_phase初始值为0。每个文件被load时先设置对应的相位值等所有文件都加载完再统一触发fw_phase 2这个“全部就绪”阶段在这个阶段才真正执行命令注册和菜单挂载。这样就避免了一个经典的时序问题单个文件加载时只想定义函数不要立即执行需要其他模块支撑的代码。fw_util.il是公共函数库目前沉淀了四类能力文件操作、日志输出、用户交互封装、设计数据访问。举例来说日志输出函数fw_log会往固定的日志目录追加写入带时间戳的文本同时根据一个全局开关决定是否在Allegro的Console窗口同步显示。用户交互封装则统一了axlUIViewFileOpen、axlUIPopup这类调用的方式后续所有模块里的弹窗风格就完全统一了不会再出现这个模块用旧式弹窗、那个模块用新式弹窗的割裂感。fw_register.il可以说是框架层最有价值的部分。它维护一张命令注册表本质上是一个关联列表每一项包含三个信息命令名、对应函数、所属模块。注册函数fw_reg_cmd接收这三个参数自动完成三件事给命令名加上统一的业务前缀、把函数指针存入注册表、调用axlCmdRegister向Allegro注册。以后想查某个命令是哪个模块负责的直接查这张表就行比翻Ilint文件高效得多。2.2 细节层一个文件一个功能域的原子化组织细节层的组织规则我用一句话总结一个文件只服务一个功能域一个功能域只解决一类问题一个函数只做一件事。这听起来像常识但在SKILL这种自由度极高的语言里严格遵守还真不容易。拿位号处理来说所有涉及位号的工具被我收拢进了refdes.il。这个文件里目前有三个函数refdes_batch_shrink批量缩小位号字高、refdes_rename重排位号、refdes_list_missing列出缺失位号。每个函数的实现都很短短到单个函数基本可以一屏看完。这带来的直接好处是每个函数只需要在自己的职责边界内思考不需要同时考虑十几个其他功能的逻辑。细节层模块在被框架层加载时只做一件事定义自己的函数然后调用fw_reg_cmd注册命令。如果需要读取配置就通过框架层提供的配置读取函数来拿不允许直接自己开文件读配置。如果需要操作数据库就通过框架层封装好的API来访问当前设计不允许自己到处axlDBGetDesign。这些“不允许”一开始会觉得是约束但真正跑起来才知道正是这些约束让代码变得干净。2.3 目录结构与命名规范代码组织方式的落地还需要在文件层面形成约束。这次项目的目录结构如下skill_project/ ├── allegro.ilinit ├── framework/ │ ├── fw_init.il │ ├── fw_util.il │ └── fw_register.il ├── modules/ │ ├── refdes.il │ ├── shape_check.il │ ├── drill_report.il │ └── stackup_util.il └── docs/ ├── TEST_PLAN.md ├── TEST_CASE_refdes.md ├── TEST_CASE_shape_check.md └── TEST_REPORT.mdallegro.ilinit是整个工具集的装配文件里面只写load语句和最后的启动函数调用。framework目录放框架层modules目录放细节层docs目录放测试项目文档。目录结构本身就是架构的直观表达新同事来了看一遍目录基本就能理解整个工具集是怎么组织的。命名规范方面这次我强制自己遵守几条硬性约定函数名统一用模块前缀_动词_对象的格式例如refdes_batch_shrink命令名统一挂在业务前缀下例如pcb_refdes_shrink全局变量统一加g_前缀例如g_fw_phase。这几个约定看着简单实际上彻底根治了过去代码里那种“读半天不知道这变量是干嘛的”的顽疾。2.4 把配置从代码中剥离还有一个细节值得单独说就是配置剥离。过去写SKILL最烦的就是要改参数时得进代码里找常量。字体高度、铜皮间距、日志路径、默认输出目录全散落在各个脚本里每次调整都要重新load一遍脚本还要小心别改错地方。这轮重构我在框架层加了一个配置模块统一用一个fw_conf.il文件维护所有可调参数并且支持从外部配置文件读取覆盖值。默认值写在代码里外部配置文件只要存在就优先采用外部值。这样一来现场部署时只需要改一个config.ini文件完全不用碰代码。比如位号缩小功能默认字高是40mil客户要求改成30mil直接改配置就行连重启Allegro都不用热加载一下配置就生效。配置剥离这件事做得越早越省心。如果一开始就把参数全焊死在代码里后面做项目交付时会痛苦到怀疑人生。3. 测试项目文档到底怎么编3.1 文档结构和核心关注点这次写的测试项目文档不是那种摆样子的文档而是真正能指导开发、辅助验证的“活文档”。整套文档拆成了三份测试计划、分模块测试用例、测试报告。测试计划管全局内容包括项目背景、测试环境、测试范围、测试策略、进度安排。测试范围里明确列了“本轮测试覆盖哪些模块、哪些功能点、哪些边界条件”也列了“不覆盖什么”避免范围膨胀。测试策略说明了测试方法比如采用“手动功能测试极简自动冒烟测试”的组合。分模块测试用例是整个文档的核心。每个模块一份文档里面是表格化的用例集。每一条用例都包含用例编号、优先级、操作步骤、输入数据、预期结果、实际结果、测试结论、备注这8个字段。用Excel做也行用Markdown表格也行关键是要结构化方便查询和追溯。测试报告是最后沉淀的记录在用例执行完之后生成忠实记录每一轮测试执行的时间、环境、通过率、缺陷情况。它是判断重构是否达标的直接依据也是未来做回归测试时的对照基线。3.2 测试用例设计的关键逐条拆解用例设计这件事最核心的功力体现在“会设计边界场景”上。这次我为每个模块编写的用例都坚持了几个设计原则逐一拆解一下。第一是正常路径优先覆盖。这是基础中的基础正常操作流程下的所有功能点必须跑通。比如位号重排功能正常输入是选中器件后执行命令预期结果是位号按照从上到下、从左到右的顺序重新编号并刷新到画布。这类用例是金字塔的塔基数量要够覆盖面要全确保最常用的路线不出错。第二是边界值必测。凡是涉及数值参数的功能边界值就是雷区。比如批量缩小位号字高功能参数范围设计是10~200mil那测试用例就必须包含10mil、200mil、10mil以下、200mil以上四类输入。尤其是超出范围时程序能不能给出清晰友好的提示而不是抛出一段晦涩的SKILL错误信息这个很考验开发者的细节功力。第三是异常场景模拟。EDA设计里的异常输入五花八门没有打开设计文件直接执行命令、当前设计里没有器件、选中的对象不是目标类型、文件路径不存在、权限不足无法写入……每一种异常都能单独写用例。这次我在异常场景上尝到了很大的甜头很多过去“感觉应该不会出错”的地方一测就暴露问题。第四是回归用例固化。每个被修复的缺陷都要转化成一条回归用例跟着文档长期保留。这些用例是整套文档里价值最高的部分因为它们是真实踩过的坑的具象化。3.3 从文档反推动代码设计一个补充流程这里要说一个我自己加的小流程也是这次尝试里我觉得最值得分享的点在写代码之前先通过测试用例文档把“功能契约”定义出来再倒推代码实现。以前我的习惯是拿到需求直接写代码写完再补文档。这次反过来了。比如要实现shape_check_delta这个检查铺铜到板边距离的功能我先整理了一份用例表格正常情况是铺铜与板边距离等于设定值时提示通过边界值是距离等于最小值时算通过还是算不通过异常情况是铺铜层为内层时检查逻辑是否不同。把这些场景一条条想清楚、写明白了再回头写代码思路会异常清晰。这种“用例先行”的做法相当于在动手前就把每个分支都想透了代码写起来几乎不会出现“写着写着发现某个分支怎么处理”的卡顿。而且用后续执行结果对照最初的用例每一条都能找到明确的判定依据不会出现“我觉得应该算通过吧”这种模糊状态。3.4 文档模板字段可以这样定义实测下来几个字段的填写质量直接影响文档的可用度。我踩过坑之后对字段做了规范化处理。用例编号的格式定为“模块名_功能名_序号”比如REFDES_SHRINK_001。这样一个编号出来立刻知道对应哪个模块的哪个功能。优先级用P0、P1、P2三级P0是核心功能必须全通过P1是重要功能尽量通过P2是优化项不强制。操作步骤要求写细细到每步点击、每次输入都写清楚别人照着做能100%复现才算合格。预期结果的描述要求可判定不能写“界面正常”这种模糊话要写“对话框显示总器件数和已修改器件数”这种可以直接核对的内容。有一点必须承认写高质量用例非常费时间。一个中等复杂度的模块用例写个三五十行很正常。但这笔投入绝对值得因为用例越细后面改代码时的安全感越强。4. 实操过程与关键环节的实现4.1 环境准备与基线建立这次的验证环境我选的是Cadence Allegro 17.4版本系统为Windows 10 64位。准备工作第一步是把所有待重构的旧脚本做一个Git基线提交确保万一新方案跑不通还能退回旧版本。然后建立新的目录结构把allegro.ilinit里的load路径全部指向新目录。基线建立这一步的作用主要是给了自己一个安全网。重构过程中改错了、改乱了随时可以回滚到上一个可用状态。如果没有这层保障重构的心态差很多。实际加载验证也做了一轮。新目录搭好后启动Allegro打开一个测试用的.brd设计文件确认全流程正常加载命令列表里能看到以pcb_前缀开头的命令。这一步过了环境和装配就算初步OK。4.2 框架层核心代码的实现示例框架层的几个文件里fw_register.il的注册函数是最有代表性的。它的核心实现思路是这样一段SKILL代码; 全局命令注册表 g_fw_cmd_table nil ; 注册命令cmd_name 为命令名func_sym 为函数符号mod_name 为模块名 defun( fw_reg_cmd (cmd_name func_sym mod_name) let( (full_name) ; 自动加上业务前缀避免命令过短或冲突 full_name strcat(pcb_ cmd_name) ; 记录到注册表方便后续查询和卸载 g_fw_cmd_table cons(list(full_name func_sym mod_name) g_fw_cmd_table) ; 真正向Allegro注册 axlCmdRegister( full_name func_sym ?cmdType general) printf(CMD: %s registered by %s\n full_name mod_name) ) )这段代码的核心是做了两件事一是用业务前缀统一了命令名二是把命令与模块的映射关系记录到注册表里。以后想看项目里有哪些命令一个printf把g_fw_cmd_table打出来即可。查询命令数量的时候一条length(g_fw_cmd_table)就拿到总数省去了数allegro.ilinitload行的麻烦。框架层的初始化函数同样关键。它在所有文件加载完成后执行逻辑是依次调用fw_conf_load加载配置、fw_create_menu创建菜单、fw_reg_auto_cmds注册需要自动挂载的命令。这个初始化动作由allegro.ilinit的最后一行触发load(strcat(axfEdaHomeDir /skill_project/framework/fw_init.il)) fw_init_after_all()这里有个小坑值得提醒fw_init_after_all()这一行必须放在所有load语句之后否则某个模块还没加载完初始化就去注册它的命令必然会报“函数未定义”的错误。4.3 细节层模块的开发示例细节层的代表是refdes.il。它实现了一个很典型的PCB设计需求批量缩小位号字高。这个功能看起来简单但涉及的设计对象类型不少包括普通文本、器件位号、属性文本每种对象修改字高的API调用方式还有差异。我在模块里把函数拆成了三层。最外层refdes_batch_shrink负责与用户交互弹窗收集参数调中间层中间层refdes_apply_shrink负责遍历选中对象、判断类型、逐个调用底层修改函数底层refdes_shrink_text负责真正修改某种类型的文本字高。三层职责清晰各自只关心自己的事。; 批量缩小位号字高的入口函数 defun( refdes_batch_shrink () let( (ratio new_height) ; 从框架层配置模块读取默认字高而不是硬编码 new_height fw_conf_get(REFDES_DEFAULT_HEIGHT 40) ; 调用框架层封装的交互函数获取用户输入 when( fw_user_input(请输入新的位号字高(mil): new_height 10 200) refdes_apply_shrink(new_height) ) ) )修改完成后函数还会调用框架层的日志函数把修改的器件数量、耗时写入日志文件。这个“做了什么事都要留痕”的习惯在排查问题时特别有用。比如客户反馈某个版图文件位号为什么变小了一看日志日期、时间、操作人都清清楚楚。4.4 按文档执行测试与验证环境准备和代码开发完成后就进入了真正的验证环节。这一步是整套方法论的临门一脚也是最容易照出问题的地方。我按照测试项目文档里的用例逐条执行先把所有P0用例跑一遍。第一条用例就是最基础的功能验证打开设计文件选中若干器件运行pcb_refdes_shrink输入40点确定检查画布上位号是否全部变成40mil。实测结果是通过但紧接着就暴露了一个预期之外的异常当选中了一个锁定的Symbol时修改字高操作会跳过该Symbol但界面没有任何提醒用户会以为全部修改成功了实际上有一个没改掉。这个场景在用例表里属于“异常场景”分类本来写用例时我猜它可能会失败没想到真的一验就中。我当场把用例的“实际结果”一栏填上“部分通过锁定器件被跳过且无提示”并记录到缺陷清单里随后在代码里加入了锁定检测和提示逻辑。整轮验证下来共执行用例约70条P0用例占35条其中首次通过33条2条缺陷待修正P1和P2用例还有几处体验类问题。这个结果基本达到了预期也让后续的修复工作有了明确优先级。5. 常见问题与排查技巧实录5.1 常见问题速查表这轮从零到一构建“框架细节”模式的过程中我遇到的几个典型问题整理成一张速查表给可能踩同样坑的同行做个参考。问题现象可能原因排查方法解决方案加载完毕后初始化函数报错“函数未定义”初始化函数调用时某个模块还没load完检查allegro.ilinit中load语句顺序把初始化调用放到所有load语句之后命令注册成功但执行时报“无法识别命令”函数符号写错或未正确加载用axlCmdList查看已注册命令检查注册函数的函数符号是否和实际定义一致多个脚本同时修改全局变量导致数据错乱全局变量命名冲突搜索代码里同名全局变量强制使用g_前缀并约束模块间不共享变量新模块加载后旧功能行为异常修改了公共函数但没有做回归测试查看公共函数变更记录框架层变更后必须跑一遍全部P0用例外部配置文件改了不生效配置加载时机早于文件修改或路径不对打开日志检查配置加载时间和读取值确保配置读取函数被正确调用并支持热加载这五类问题中命令注册失败和全局变量污染是我过去遇到频率最高的两类也是这次重构要重点解决的。框架层的注册中心在机制上就保证了不会出现命令重复注册的问题而通过变量命名规范和初始化清零全局变量污染也被压到了最低。5.2 围绕SKILL开发方法的体会除了问题排查这轮重构里还有几个关于SKILL开发方法本身的感受想一并分享。第一个感受是SKILL代码必须接受“看起来啰嗦”这件事。SKILL的Lisp式前缀语法写惯过程式语言的会觉得反直觉。但一旦接受了它的表达方式写出来的代码反而更接近自然语言逻辑分支一目了然。别扭是初期的收益是长期的。第二个感受是别迷信“一次性写对”。SKILL缺少现代化的调试工具断点、单步、变量监视都很难用。这个环境下测试用例文档的价值会显得格外突出。用例写清楚了按用例去执行、验证、定位比在代码里猜要快得多。很多时候写用例的过程就是在做逻辑推演推演清楚了代码里的bug自然就少。第三个感受是关于文档的“保质期”。测试文档如果写完放在那里不动三个月后就会失去价值。所以这次我给它定义了一个更新规则任何一个模块的代码发生行为变更必须同步更新对应文档中的用例预期结果。在Git提交里代码变更的提交信息里统一标注关联的用例编号实现“代码—用例”双向可追溯。这是一套非常实用的工程实践。最后再说一个小技巧。在Allegro里调试SKILL强烈建议利用好printf日志。我在框架层做了一套带[fw]、[refdes]这类模块前缀的日志格式加载和运行时所有关键动作都会打印出来。排查问题时只要看日志就能快速定位到是哪个模块、哪一步出了状况。别嫌麻烦日志是SKILL开发者最好的朋友。这次“框架细节”模式的尝试核心收获不在于代码量而在于建立了一套可维护、可验证、可回归的机制。测试项目文档也不是一套摆设而是整个开发流程里真正承上启下的关键环节。如果你也正为SKILL脚本越堆越乱而头疼不妨从一份用例文档开始把功能边界理清楚再回头收拾代码或许会看到完全不一样的风景。