ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

IC验证全流程指南:从DUT到回归交付的关键环节

IC验证全流程指南:从DUT到回归交付的关键环节 做IC验证这几年最常被问的一句话不是“这个波形怎么抓”而是“你们验证到底在忙什么”。这个问题要展开讲其实可以写一本书。但拉高了看核心就一条线从一个DUT出发用各种手段证明它符合设计规格直到有底气把它交付出去。我入行第一个项目就被师父扔到验证组前三个月基本是在波形里游泳边学SystemVerilog边补UVM概念。真正对“验证”这两个字建立全局认识是后来独立带模块、跑完一轮从环境搭建到回归交付的完整流程之后。回头再看IC验证的门槛不在语法而在于你有没有一张完整的地图知道现在处在哪个阶段下一步该做什么交付时需要拿出什么证据。这篇文章我把这套地图完整捋一遍覆盖从理解DUT、写验证计划、搭testbench到激励生成、自检查、覆盖率收敛再到回归跑测和最终交付的每一个关键环节。想入行的同学可以把它当全流程索引已经做验证的朋友也可以对照自己的项目看看有没有漏掉的收口动作。1. 验证的全貌从DUT到可以交付流程到底在跑什么1.1 为什么验证越来越烧钱、也越来越值钱芯片行业有个残酷的现实前端设计和验证的人力配比在很多团队已经到1比2甚至1比3。你说验证凭什么叫这么多人因为芯片不能像软件一样出了问题随手热修流片一次的费用动辄几百上千万改版周期以月为单位。一颗SoC里有CPU、总线、各类外设IP成千上万个信号在跑任何一个逻辑漏洞漏到流片后代价都不是人力成本能衡量的。验证就是唯一能在芯片真正落地前大规模、系统性发现逻辑错误的手段。为什么说“大规模”因为现代验证早就不是写几个定向case点一下信号就完事而是靠受约束随机生成海量场景再靠自动化检查器判断对错最后用覆盖率告诉你测到了什么、没测到什么。这套体系跑起来一次回归可能就是几百万个仿真周期、几千个seed目标只有一个把DUT里头的bug提前逼出来。验证同时还承担着“质量证据”的职能。流片前芯片要tapeout老板要签字靠的是什么不是拍脑袋而是验证团队交出来的回归报告、覆盖率报告、bug趋势图。所以验证工程师做的不只是“找bug”更是在给整个项目做“质检背书”。这也是为什么回归交付这最后一步在流程里有着不可替代的地位。1.2 全流程的六大阶段我把一套完整的IC验证流程拆成六个阶段这也是绝大多数团队实际在跑的主线。规格理解读设计文档搞清楚DUT对外接口长什么样、内部有哪些关键模块、支持哪些功能点、边界行为是什么。验证计划把规格翻译成可执行的验证策略列出feature清单、对应测试场景、检查手段、覆盖率目标。环境搭建基于UVM搭一套可复用的testbench包含激励产生、驱动、采样、参考模型、计分板、覆盖率收集等组件。用例调试从定向用例入手跑通基本场景再上随机约束发现bug后定位、报单、修复、复验。覆盖率收敛通过功能覆盖率和代码覆盖率发现未测到的功能点补用例、补约束直到达到项目门禁。回归与交付把全量case加随机seed跑回归出报告整理验证交付物完成sign-off评审。你会发现这六个阶段不是线性走一遍就结束的更像一个循环覆盖率收敛过程中发现新feature没测到要回到验证计划补充场景回归里挂了一个随机用例要回到调试环节分析是DUT bug还是环境bug。这种迭代推进的模式恰恰是验证工作最真实的状态。整个流程有一个核心锚点——DUT。所有的验证计划、环境组件、激励策略、检查手段全都要围着DUT的接口和功能转。所以接下来我先讲清楚DUT到底怎么“装”进验证环境里。2. testbench从零搭建DUT怎么装进验证环境2.1 先搞定DUT的接口“长相”DUT是Design Under Test被测设计一般是RTL代码模块比如一个UART控制器、一个SPI主设备、一个AHB2APB桥。做验证的第一步不是急着写代码而是把这个模块当成一个黑盒子把它的“长相”研究透。这里的“长相”指的就是接口协议。DUT有哪些输入输出信号是同步还是异步走的是什么总线协议APB、AHB还是AXI时钟怎么给、复位怎么撤寄存器的位域定义是什么我见过太多入门同学一上来就打开RTL开始逐行读代码其实验证阶段看RTL更多是为了确认行为细节第一优先级永远是先吃透接口协议和datasheet。理解了接口之后要做的第一件事是定义一个合理的接口封装。SystemVerilog的interface就是干这个的。它能把一组相关信号打包起来让driver、monitor通过virtual interface去连接DUT避免在成千上万个组件里到处拉信号线。我贴一个APB接口的例子这是最常用的低速外设总线非常适合入门看interface apb_if(input bit clk); logic psel; logic penable; logic pwrite; logic [31:0] paddr; logic [31:0] pwdata; logic [31:0] prdata; logic pready; clocking drv_ck (posedge clk); output psel, penable, pwrite, paddr, pwdata; input prdata, pready; endclocking clocking mon_ck (posedge clk); input psel, penable, pwrite, paddr, pwdata, prdata, pready; endclocking task wait_penable(); (posedge clk iff (psel penable)); endtask endinterface定义好interface之后在顶层testbench里把DUT的端口和interface对应连上DUT就被“装”进了验证环境。剩下的事情就是让验证环境里的各个组件通过这个interface去驱动和采样信号。2.2 验证环境里那五个固定角色一套典型的UVM验证环境不管测什么模块核心角色基本固定我习惯把它理解成一家小型公司的分工sequencer工单分发员接收sequence产生的激励item再按协议时序交给driver去执行。driver执行员从sequencer拿到transaction按协议把它变成DUT引脚上的电平变化也就是驱动总线。monitor记录员默默盯着总线把引脚上的电平变化重新采样成transaction然后分发给参考模型、计分板、覆盖率收集器。reference model标准答案库用高级建模的方式模拟DUT的期望行为输入同样的激励输出期望结果。scoreboard裁判员把DUT的真实输出和reference model的期望输出做比较不一致就报错。我举个生活化例子driver像翻译官把计算机的命令翻译成对方听得懂的方言monitor像速记员别人说了什么它原样记下来scoreboard是裁判DUT说了“3”标准答案说“5”那就判DUT错了。这五个角色各司其职构成了验证环境的主干。在此基础上还要有一个顶层test类来组装它们并告诉UVM“我要跑哪个测试”。这也是为什么很多新人在看UVM代码时会觉得绕——其实组件就这么几个规则就一套看多了自然就顺了。2.3 从手写testbench到UVM解放生产力的关键在UVM流行之前很多人写的testbench是“一次性的”几个initial块跑到时间就$finishDUT一换验证环境基本作废。UVM解决了这个痛点它把验证环境标准化、可复用化了。factory机制负责对象的创建和覆盖phase机制统一了仿真推进节奏config_db机制让组件配置可以灵活传递。UVM最核心的价值在于它把“这个环境该怎么跑”这件事流程化了。比如build_phase负责创建子组件connect_phase负责连接组件之间的端口run_phase里才是真正的激励驱动。如果你用的是标准UVM写法不同模块之间、不同项目之间的环境结构高度相似新人接手成本会低很多。举个例子一个最基础的UVM test长这样class apb_base_test extends uvm_test; uvm_component_utils(apb_base_test) apb_env env; function new(string name, uvm_component parent); super.new(name, parent); endfunction function void build_phase(uvm_phase phase); super.build_phase(phase); env apb_env::type_id::create(env, this); endfunction endclass这段代码干的事就三件注册组件、创建test、创建env。看着简单但它是整个UVM环境启动的起点。实际项目里还会在build_phase里通过uvm_config_db去设置virtual interface、配置参数让testbench能灵活适配不同场景。关于环境搭建我的建议是不要一上来就迷信“全自动UVM生成器”。自己手动搭一遍最小环境搞清楚每个组件在build和connect阶段做了什么比直接用工具生成的模板要理解得深得多。环境是验证的厂房厂房搭得糊涂后面生产出来的验证结果你也不敢信。3. 激励、检查、覆盖率验证的三大核心机制3.1 受约束随机让激励“既随机又合法”环境搭好之后接下来要回答的问题是拿什么去测DUT早期验证主要靠定向用例写一个sequence发一个特定地址的写请求然后检查结果。定向用例的优点是可控性好缺点也明显——你只能测到自己想到的东西。真实芯片在应用场景中会遇到各种组合靠人一个个写永远写不完。现代验证的主流思路是受约束随机。约束求解器会按照你给定的constraint在合法空间里自动生成大量不同的激励组合。比如测一个地址对齐的传输我可以约束地址必须是4字节对齐的class aligned_transaction extends apb_transaction; constraint c_addr_aligned { paddr[1:0] 2b00; } endclass随机带来的最大好处是用机器代替人肉枚举。跑1000个seed等于把1000种不同的场景组合全试了一遍。但随机不是乱来约束就是兜底的缰绳——既保证激励合法、能被DUT正常响应又能覆盖到各种边界值。这也是UVM里sequence机制存在的意义限制型随机和场景复用基本都靠sequence这一层去实现。实际项目中定向用例并不是被淘汰了而是从主角变成了配角。我常用“28法则”来理解80%的场景跑随机20%的关键路径用定向case去精确打击比如寄存器复位值、中断行为这种必须逐bit核对的场景交给随机是不太靠谱的。3.2 参考模型和自检查让DUT自己报对错有了激励下一步是怎么知道DUT答得对不对。这是验证环境和仿真调试最大的区别仿真只是看波形验证是让环境具备“自动判题”能力。最常见的做法是搭一个reference model。它用SystemVerilog或C模型实现DUT的期望行为输入同样的激励产生期望输出再和DUT的真实输出在scoreboard里对拍。比较对象可以是总线上的数据、状态机的跳变、寄存器的值也可以是某个内部信号。这里有个重要的工程判断reference model要做到什么粒度我的经验是“接口级比较”优于“内部信号级比较”。比如验证AMBA总线桥重点比较的是外部总线上看到的读写行为和返回数据而不是去比对DUT里面某个触发器的翻转。接口级比较的参考模型好写、好维护误报率也低。只有当你怀疑问题出在DUT内部某个具体逻辑时才需要拉内部信号做白盒检查。除了参考模型断言assertion是另一类强大的检查手段。SVASystemVerilog Assertions可以监控一组信号之间的时序关系比如“请求发出后最多10个周期内必须收到响应”。断言能把“非法状态”在第一时间钉在现场而不是等到数据传到scoreboard才发现出错那样定位成本会高很多。我把断言比作路口的摄像头scoreboard比作终点站的验票员两者配合查错效率是最高的。3.3 覆盖率知道你测了多少、还有多少没测验证里有一句经典的话没有覆盖率衡量标准的验证都是盲人摸象。你怎么知道bug测完了怎么知道这块DUT可以交付了覆盖率就是那根探针。覆盖率主要分两类。代码覆盖率是工具自动统计的包括行覆盖率、分支覆盖率、条件覆盖率、状态机覆盖率等看的是“RTL代码被执行了多少”。功能覆盖率是验证工程师自己定义的需要根据spec提取功能点、编写covergroup和coverpoint看的是“功能场景被hit到了多少”。前端那句经典话是代码覆盖率再高也不代表功能验证完备功能覆盖率想要高前提是代码能被执行到。两者互补缺一不可。我写一个UVM里非常常见的covergroup片段覆盖APB总线的访问类型和地址范围covergroup apb_cg (posedge ifc.clk); cp_access: coverpoint {ifc.psel, ifc.penable} { bins idle {2b00}; bins setup {2b10}; bins access {2b11}; } cp_paddr: coverpoint ifc.paddr[15:0] { bins low {[16h0000 : 16h0FFF]}; bins mid {[16h1000 : 16h1FFF]}; bins high {[16h2000 : 16hFFFF]}; } cross cp_access, cp_paddr; endgroup覆盖率数据最后是要拿来驱动验证动作的。当你发现某个cross bin始终为0要么是这个场景没被激励产生出来要么是DUT的行为没满足预期。前者回去补约束后者可能就是个潜在bug。覆盖率收敛本质上就是一个“测到没覆盖的区域、补上缺口、再测、再收敛”的迭代过程一直到全项目达到门禁指标为止。4. 实操视角从验证计划到跑通一个用例4.1 验证计划里必须写清楚的东西在实际写代码之前有一个被很多人忽视但极其重要的输出验证计划Verification Plan。它是整个验证活动的“宪法”团队成员按它分工评审按它验收。一份合格的验证计划至少要把下面这些维度列清楚feature清单从spec里逐条提取出可验证的功能点这是计划的灵魂。场景展开每个feature对应哪些正常场景、异常场景、边界场景。检查策略每个场景下检查什么用什么手段检查——reference model对拍、断言、scoreboard比较。覆盖率目标每个feature对应哪些功能覆盖点期望覆盖到百分之多少。优先级哪些是P0核心功能哪些是P1常规功能哪些是P2低概率场景。我拿一个APB外设接口来示例计划表可以长这样功能点测试场景检查手段覆盖率目标优先级APB写传输单次写、burst写、地址边界写scoreboard对拍写操作coverpoint 100%P0APB读传输单次读、连续读、读后写scoreboard对拍读操作coverpoint 100%P0地址译码非法地址访问、边界地址命中断言scoreboard地址区间覆盖95%P1时钟域复位复位撤销后首次访问scoreboard对拍复位coverpoint 100%P1这里有个经验验证计划写得好不好直接决定验证周期长不长。我见过太多项目因为计划阶段没把feature拆透到了覆盖率收敛阶段才发现漏了一个功能点环境要大改时间全搭进去了。宁可计划阶段多花一周也别让验证执行阶段多花一个月。4.2 跑通第一个用例的最小步骤计划写完之后第一步不是写所有组件而是把“最小可运行环境”先跑通。所谓最小可运行就是DUT加上最精简的driver和monitor先跑一个最简单的定向用例比如“APB写一个寄存器然后读回来”。这一步的意义不在于测出多少bug而在于验证你的环境链路是通的。具体步骤我建议按这个顺序来搭建interface和DUT连接跑一个空的testbench确认编译通过。在test里创建一个简单的sequence发送一个transaction。driver实现从transaction到接口时序的驱动逻辑至少能完成一次写入。monitor实现采样把接口上的信号采样成transaction并打印出来。运行仿真打开波形确认驱动时序和采样数据与自己预期一致。你可能会觉得这个过程太基础了但我见过不少新人在这一步翻车driver里的时钟沿没对齐、interface的clocking块方向写错、config_db里的virtual interface没配置成功导致仿真一跑就一片X态。最小环境就像是房子的地基地基稳了后面往上盖楼层才敢加速。当你发现波形上驱动出来的psel、penable、paddr、pwdata都符合APB协议时序时这个最小环境就算跑通了。到这一步你对“DUT是怎么被激励起来”这件事就有了最直接的体感。4.3 调试三板斧波形、日志、断言用例跑起来之后真正的重头戏是调试。同样一个用例挂了新人喜欢盯着波形一帧一帧看效率很低老手会先看日志和断言快速缩小可疑范围再用波形确认细节。我把这套流程总结为“调试三板斧”。第一板斧是打日志。在driver、monitor、scoreboard的关键节点加上uvm_info打印比如“发送了读请求地址0x1000”“读取返回数据0xDEADBEEF”。看日志能快速判断错误发生在哪一段而不是一头扎进上万个周期的波形里。日志不是越多越好太多了反而是噪音。我的习惯是在关键协议转换点和数据比对点打印而且一定要带上时间戳和事务描述。第二板斧是断言。如果DUT的行为违反了你预先写好的SVA时序要求断言会在违规发生的那一拍立刻报警直接告诉你问题出在哪个时间点哪个信号上。这比scoreboard接收到的错误更精准因为scoreboard比较是端到端的可能DUT内部早就错了但错误结果要过好几个周期才传到输出端。第三板斧才是波形。当日志和断言帮你锁定时间点之后再打开波形只看那个时间点前后几十个周期的信号变化效率能翻好几倍。新人最容易犯的错就是把波形从头拉到尾眼睛盯到发花也没找到问题在哪。抓重点、定点看是调试的基本功。调试过程中还有一个特别实用的技巧随机用例挂了一定要保留失败时的seed。UVM的随机种子决定了sequence产生的全部激励内容用同一个seed重新跑就能精确复现同一个失败场景。我刚入行时犯过傻跑挂了没记seed回头再想复现怎么跑都不挂了那个bug硬是在下一轮回归里又冒出来白白浪费了排查时间。5. 回归交付验证里程碑怎么收口5.1 回归不是“把case跑一遍”回归交付里的“回归”这两个字在项目语境里指的就是回归测试regression。很多人以为回归就是把所有case跑一遍、全绿就算完事真正做起来远没这么简单。一次像样的回归要考虑怎么跑、跑多少、怎么确定结果可信。首先是回归的规模。现代验证里同一个testcase往往会配置几十甚至上百个不同的随机种子去跑以保证激励的多样性。比如一个APB的随机读写测试配上50个seed每个seed的激励序列都不一样相当于50次独立的随机场景。全项目跑下来用例数乘以种子数总量很容易上千甚至上万。跑完之后需要自动比对每个case的pass/fail状态汇总成一份可读的回归报告。其次是回归的执行和调度。这么多用例不能用一台机器串行跑跑完黄花菜都凉了。实际项目里一般会搭回归服务器或者用云端资源把几千个task分发到多台机器并行跑我见过一个项目高峰期同时调度了500多个仿真进程。调度层还要支持断点续跑、失败用例自动重跑确认、结果归档。这一层你做得好不好直接影响团队每天等回归结果的效率。回归的意义不只是“跑绿”更重要的是跑出“变化”。每次RTL提交新版本之后都要跑一轮回归目的有两个验证新功能没问题同时确保老功能没有被改坏。这就是“回归”这个词的本质——防止功能回退。很多项目会配一个持续集成CI系统代码一提交自动触发回归结果不合格直接拦住合入这也是团队质量工程成熟度的一个重要标志。5.2 覆盖率收敛和质量门禁回归一直绿不代表验证就可以交付了。你还要回答一个问题测够了没有这就回到了前面说的覆盖率。回归和覆盖率是配套的回归跑完把所有仿真产生的覆盖率数据merge到一起得到整个验证周期累计的覆盖率报告。merge很有讲究。如果你只看最后一次回归的覆盖率数字会很低因为很多case是之前跑的。正确做法是把所有case、所有seed的覆盖率数据合并得到“整个验证阶段到底累计hit到了多少”。覆盖率报告中那些长期为0的coverpoint就是你的下一步行动清单是补激励、补约束还是这个功能点本身就是不可达的如果是不可达需要在注释里说明理由并让评审确认。项目能不能sign-off通常有一组明确的质量门禁。每家公司的指标不太一样但大体包括致命和严重级别的bug清零、功能覆盖率达标比如99%以上视具体模块而定、代码覆盖率达标、验证计划里的feature全部有对应结果、没有遗留的不合规问题。这些门禁通常是验证和设计联合评审时逐项核对的硬性条件达不到就不允许流片。我特别想强调一点覆盖率收敛阶段最容易出现“凑数”心态。某个coverpoint跑不动了不是回过去分析功能空间而是硬写一个专门射那个bin的定向case把数字刷上去。这种做法在交付评审时一旦被问穿整个验证报告的可信度都会打折扣。覆盖率数字重要但覆盖率背后的分析更重要。5.3 验证交付物与sign-off清单当验证执行完成、覆盖率收敛达标之后最后一步就是把整套证据整理成交付物提交给项目评审。我做了几个项目后整理出一份常用的交付检查清单交付物内容说明评审关注点验证计划原始计划及更新记录feature是否完整计划变更是否有评审验证环境说明环境结构、组件清单、构成方式可复用性、可维护性回归报告多轮回归的case统计、通过率、失败分析失败是否有闭环有无残留问题覆盖率报告功能覆盖率、代码覆盖率及未覆盖分析未覆盖点是否有合理解释Bug清单生命周期完整的bug记录严重bug是否全部关闭异常与风险项已知缺陷、未测项、规避说明风险是否被项目组接受这整个收口的过程就是我标题里说的“回归交付”。它不只是一个动作而是一套保证验证结果可信、可追溯、可评审的机制。很多刚入行的同学觉得写文档麻烦但当你经历过一次流片后芯片因为某个被漏掉的功能点而改版你就会明白交付物里每一个被记录、被分析、被解释过的细节都是在给项目买保险。做验证这几年我最大的感受是验证工程师是那个在幕后默默守门的人做得好没人觉得你牛因为bug本来就该被拦下来做得不好漏了一个bug到流片后所有人都会记得你。所以我一直有个习惯每次项目走到回归交付阶段我都会自己亲手再过一遍覆盖率报告和bug清单而不是全交给自动化工具。工具能帮你跑数据但判断“这个未覆盖点能不能接受”这件事最终还是得由人来承担。6. 这些年踩过的坑给入门者提个醒6.1 三个最常见的项目事故踩坑是成长的加速剂但这几个坑如果能提前避开能省下不少加班时间。第一个坑是验证计划漏feature。这是所有坑里代价最高的。我见过一个验证同学把总线协议的“错误重试”场景漏掉了覆盖率阶段发现一个一直为0的bin才回头补环境。这时候补的不只是一个case还要改driver和参考模型牵一发而动全身。所以写验证计划时我习惯拉着设计一起过一遍spec让设计把代码实现时觉得“容易出错”的点一个个讲出来这些点往往就是验证要重点覆盖的feature能提前暴露很多盲区。第二个坑是环境复用时隔离没做好。很多项目会复用上一个模块的验证环境这是好事但复用不等于直接拷过来。不同模块的协议细节、寄存器布局、时序要求可能都有差别。我见过一个团队复用环境后忘了改地址映射结果整个模块在跑随机时数据全部写到错误地址还绕过了reference model的检查最后靠覆盖率异常才查出来。复用环境一定要在组件内部把“上一代模块”的硬编码清理干净接口适配层写清楚不能图快直接cv。第三个坑是断言写错方向。SVA写反了危害很大本来不该报警的地方狂报你以为DUT有问题排查半天发现是断言的触发条件写成了依赖输出信号形成了路径依赖或者反过来该报警的时候不报让bug一路溜到scoreboard。写断言的核心原则是断言应基于规范和时序要求而不是基于DUT的实现细节。依赖内部信号写断言等于让考生自己出卷自己答看着全对实际毫无意义。下面这张表是我整理的现场排查速查可以贴在工位上异常现象优先排查方向常用手段仿真一跑就挂环境编译配置、interface连接、config_db看前几十行日志查UVM topology树波形全X态时钟/复位驱动、interface方向单独抓时钟复位信号检查TB层连接scoreboard大量报错参考模型逻辑、transaction字段对齐打印transaction, 对比输入输出随机用例偶发挂约束冲突、激励序列出现边界组合保留seed复现分析sequence序列覆盖率长期不涨约束太紧或太松、激励生成有死角分析未覆盖bin对比constraint6.2 入门IC验证该怎么练聊完流程和踩坑再给准备入行或者刚入行的同学一点实操建议。别看那么多验证的书单真正上手才是最快的路径。我推荐三条具体的练习路径。第一把UVM的源码当教材读。UVM本身是开源的安装完工具之后在库目录里就能找到全部源码。刚接触UVM的时候很多概念你记不住比如factory机制怎么注册、sequencer和driver之间怎么握手。与其在网上找二手解读不如直接在源码里搜task run_phase、class uvm_driver这些定义看原始实现。很多当初想不通的设计意图看几行源码就通了。第二搭一个最小UVM env模仿真实项目结构。可以拿一个简单的模块比如APB slave或者UART接收器从零开始搭环境从最简单的driver和monitor写起再到scoreboard、reference model、covergroup。不用追求功能多复杂关键是把这个链路亲手走通。这个练习做完你再看团队的project代码基本不会一脸茫然了。第三练波形阅读和bug定位。找一些别人记录的真实bug波形或者经典debug案例练习不看答案自己分析看看能不能通过波形还原出DUT的错误行为。这个能力特别像侦探破案需要你熟悉协议时序又需要你有耐心。练得多了以后很多bug你扫一眼波形的前几个周期就能猜到大半位置。我在带新人的时候常说一句话验证这份工作技术深度和沟通宽度都得有。技术上是和RTL较劲看谁先找到谁的问题沟通上是要和最较真的设计、架构、项目经理打交道你得能把自己的验证结论讲得让人信服。干得越久越发现验证不只是技术工作更是一项工程质量工程。最后分享一个我自己坚持了很多年的小习惯每天手工跑了反例之后第一时间把波形的采样点截到bug报告里并注明复现seed和当时环境版本。这个动作看着小半年后回溯问题时能帮你省下大量翻聊天记录和翻日志的时间。验证做到最后拼的不是谁会写的语法更花哨而是你的流程靠不靠得住交付的时候自己心里有没有底。希望这套从DUT到回归交付的框架能让你在下一个项目里少走点弯路。
返回列表