做数字IC后端这些年,我最大的感受是:Flat Flow在芯片规模面前迟早会撞墙。尤其是当你拿到一个几千万门、甚至上亿门级别的SoC,跑一次全芯片place和CTS可能就需要好几天,工具内存动不动飙到几百GB,就算服务器扛住了,团队协作和迭代效率也撑不住。这时候,层次化(Hierarchical)设计流程就不再是加分项,而是必选项。这篇东西我早就想写了,把我在多个量产项目里反复折腾过的Sub-System层次、block partition策略、pin assignment细节,以及那些文档里不会明说、只有踩过坑才会懂的教训,一次性盘清楚。
文章主要面向有一定后端基础、正准备从Flat Flow切换到Hierarchical Flow的工程师,也适合刚接触大型芯片物理设计的同学用来建立整体认知。我会尽量避免空谈概念,尽量把每个决策背后的逻辑和代价都讲透。
1. 为什么非要用层次化设计:Flat Flow的“死线”到底在哪里
1.1 容量与迭代速度的双重瓶颈
先算一笔账。一个典型的5nm工艺、8核应用处理器级别的die,总面积大概在90到120平方毫米,逻辑cell数量在8000万到1.5亿之间。用Flat Flow做place时,工具需要同时维护所有cell的物理位置、时序弧、功耗和绕线资源信息,内存占用通常会达到库文件大小乘以一个不小系数的状态,实践中经常超过300GB甚至500GB。这还没算上CTS阶段插入的几十万个clock buffer和ECO阶段新增的cell。
内存只是第一道坎,更致命的是迭代速度。Flat Flow下每改一次约束、每加一条ECO,都需要把整个芯片重新跑一遍place和routing。即使机器够强,一次完整迭代花掉三到五天是很正常的事。在项目后期,前端三天两头改RTL,物理设计这边根本追不上节奏,整个项目就会卡在后端收敛上。
Hierarchical Flow的核心思路很简单:把一个大问题分解成多个可以并行、可以独立迭代的小问题,然后在顶层只做协调和接口工作,而不是所有事情都搅在一起。它并没有减少总工作量,但它让“迭代局部化”成为可能——某一个block(子模块)内部改逻辑,不需要惊动整个芯片。
1.2 层次化不等于简单切一刀
很多第一次接触Hierarchical Flow的工程师会以为,这条所谓“层次化”就是把RTL里的module边界当成物理block边界,然后按图索骥地切块。实际上,这种理解太过天真。RTL的module划分往往是站在功能验证角度设计的,服务于代码可读性和验证复用,不一定适合作为物理实现的边界。
我在一个项目里就吃过这个亏。当时RTL里一个很大的DMA controller被写成一个module,功能上完全没有问题,但这个module内部包含了大量分散的RAM阵列和一堆分散在不同物理位置的加速器逻辑,硬生生把这块区域切得七零八落。如果我们严格按照RTL边界做物理block,这个block的pin数量会飙到非常夸张的数值,pin密度过高导致绕线拥塞(congestion),而且block边界附近的时序收敛异常困难。
所以,在后端物理设计中,block partition更准确地说是一种物理设计行为,RTL层次只是参考,不是金科玉律。我们需要结合floorplan、模块间数据流、功耗隔离、复用需求、以及团队解耦来综合决定怎么切。
1.3 什么情况下值得上Hierarchical Flow
不是所有项目都需要层次化,也不是所有设计都适合层次化。基于我自己的项目经验,以下几个信号出现时,你基本就可以确定要上有层次的流程了:
- 规模门槛:全芯片cell数超过3000万到5000万,或者单die面积很大、后端迭代一次以天计,不上层次化效率完全扛不住。
- 存在明显的同构复用单元:比如四核CPU集群、多组DSP、多组ISP,这些sub-system物理结构高度相似,完全可以做成一个物理block,然后在顶层多次放置,也就是常说的“multi-instantiate”或“reuse block”。
- 团队并行需求:多个工程师需要同时在同一颗芯片的不同区域工作,不能互相阻塞,需要清晰的物理和逻辑边界。
- 混合信号/模拟IP集成:数模混合设计里,模拟版图需要全定制处理,与数字后端边界必须物理隔离,层次化是天然的选择。
一旦决定用Hierarchical Flow,接下来最关键的就是partition和pin assignment,这两件事做得烂,后面所有环节都会异常痛苦。
2. Block Partition:切好第一刀才算赢在起跑线
2.1 划分的输入:从数据流反推物理位置
在我做过的项目里,最省心的partition往往不是一个纯粹从floorplan出发的划分,而是先认真看模块之间的数据流和连接关系,再反推物理位置。简单说,你希望“连接关系紧密的逻辑尽量被切在同一个block里”,因为切开的界面需要经过pin、顶层连线、再进入另一个block,这部分路径通常是时序收敛的重灾区。
我一般会先做如下几件事:
- 打开RTL的hierarchy tree,统计每个子模块的cell count、port数量和与外部模块的连接数量。
- 把连接数量矩阵画出来,找出天然的高内聚、低耦合区域。连接超过几百根的模块对,如果物理上又要分开,就要慎重考虑接口信号的绕线空间。
- 结合前端给的预估面积,在floorplan上把主要IP(CPU、GPU、NPU、DDR控制器、serdes等)提前摆放好,再在这些anchor之间的空隙里“填空”。
然后才是决定“哪个功能块做成独立block”“哪几个逻辑层次合并成一个super block”的问题。
这里有一个很实用的经验:block边界尽量选择在“寄存器层”上切。什么意思?两条路径从一个block到另一个block,如果路径的起点是发送方的reg,终点是接收方的reg,中间组合逻辑完全落在某一个block内部,那跨block路径就只是从reg到port的延迟,时序可控性最好。如果边界切在组合逻辑中间,即使不是不可能收敛,你也要花大量时间在顶层做balance和buffer insertion,相当痛苦。
2.2 Block大小的黄金比例:多久收敛一次最重要
很多团队在定义block size时只关心“工具能不能跑得动”,而不关心“收敛速度是否令人满意”。实际上,在机器资源足够的前提下,block不是越大越好,也不是越小越好,关键是block的迭代周期和全芯片的迭代周期能否匹配上。
我个人的倾向是,单个物理block的面积控制在全芯片面积的1/15到1/5之间,逻辑规模控制在500万到1500万cell以内。再大,block内部place/CTS的一次迭代就要超过半天,和全芯片的顶层迭代节奏脱节;再小,partition的overhead(抽象模型生成、接口约束、顶层绕线资源预留)占的比重反而过大,不划算。
以我曾经做过的某视频编解码芯片为例,全芯片大概7000万cell,我们切成了五个主要block:两个同构的视频处理cluster、一个CPU sub-system、一个DDR/memory controller block、一个外设/IO block,再加上顶层剩下的少量随机逻辑。每个block的cell规模在800万到1200万之间,工程师团队刚好可以每人负责一到两个block,步调非常齐整。
2.3 同构模块复用:省下的是三倍时间
同构模块(reuse block)的复用,是Hierarchical Flow带给团队最大的礼物之一。我们那个视频项目里有两个cluster,RTL上是一模一样的vcodec core实例化两次,物理上分居芯片左右两侧。我们只需要把一个block完整跑完——从floorplan到DRC/LVS全部signoff——然后通过简单的坐标变换生成第二个instance的物理数据。
但这里有一个特别注意点:同构block可以做physical reuse,但布局位置必须保持对称性或者完全相同的朝向和电源结构。一旦你在顶层为两个cluster规划了不同的power mesh方向、或者周围有不对称的hard macro遮挡,那么“一模一样”只是纸面上的,实际绕线资源和pin escape方向都会变化,信号完整性也会不同。遇到这种情况,比较稳妥的做法是先做一次物理可行性评估:把第一个block的def放到两个位置分别跑一版顶层place,比较两边congestion和pin density是否一致,再决定要不要做physical reuse。
另一个容易遗漏的点是:两个同构block的内部时序signoff可以互相借用,但跨block的接口时序不行。因为接口路径的另外一侧(比如DDR controller到cluster A的连接)环境不同,顶层时序收敛必须分别看。拿到第一个block的抽象模型后,第二个block直接copy是省事的,但前提是两边接口环境的约束(包括虚拟端口、input/output delay)完全一致。
2.4 切完之后的顶层:别把脏活累活全甩给顶层
Partition切完之后,顶层往往还剩下两类让人头疼的东西:
- 跨block的feedthrough信号:一个信号从block A的port进入、穿过顶层绕线、再从block B的port出去。这些信号如果在partition阶段没有规划好,顶层的绕线congestion会非常集中在某个区域,尤其当信号总线很宽(比如256-bit的AXI)且跨了很多block的时候。
- 顶层自己的逻辑:总有那么一些零散的clock gating cell、spare cell、或者胶水逻辑,不归属于任何block。如果这些逻辑太少,你可能觉得无所谓;但如果顶层随机逻辑超过几十万cell,顶层就成了一个没有完整floorplan的“乱炖锅”,CTS做起来极难收敛。
我的建议是:在partition阶段就应该明确一个原则——顶层只做端口连接和少量clock tree buffer,不做任何大数据通路逻辑。如果前端在顶层挂了大量组合逻辑或mux逻辑,push它要么下沉到某个block内部,要么在顶层做一个统一的“顶层逻辑block”,给它一个明确的物理区域和边界,而不是让工具在整个die上自由摆放。
3. Pin Assignment的完整落地细节:从spec到定稿
3.1 Pin Assignment的本质:为数据流找一条最短路径
如果说partition是切蛋糕,那pin assignment就是决定“每一刀上的图案长什么样”。Pin assignment的核心目标是让每一根跨block信号在block边界上有最短、最不拥挤、且对block内部绕线干扰最小的出入口。
很多初级工程师容易把pin assignment理解成“把port摆到block boundary上”这么简单。实际上,pin assignment的内容远不止于此:
- pin的位置(落在boundary的哪一段、水平边还是垂直边)
- pin的layer选择(用M4还是M7,是否和电源网格冲突)
- pin的朝向(朝左、朝右、朝上、朝下,即pin layer上的延伸方向)
- pin的shape(是单pin还是pin cluster/bundle)
- pin之间的spacing(是否满足同层绕线和通孔需求)
- pin对应的绕线资源预留(在pin旁边留出足够的escape route)
- pin和block内部时序路径的关系(这个pin到内部时序单元的距离是否合理)
3.2 先决定Pin Layer,再谈具体坐标
在我参与的早期项目里,曾经为了省事,直接把所有pin都放在默认的较低金属层(比如M3/M4)。结果block内部的standard cell绕线本来就在低层,大量跨越边界的信号也挤在低层抢道,整个block边界附近的congestion简直惨不忍睹。
后来我们总结出了一条比较固定的策略,Pin Layer至少要选择block内部全局电源网格之上、并且最好不遮挡高层信号绕线的金属层。在当前主流工艺下,M5/M6/M7甚至M8才是更合理的选择,M5/M6兼顾了信号层与绕线资源的关系,M7/M8虽然更宽、绕线更顺,但是往往这些高层金属也是电源/地网格的主战场,pin放多了会和power mesh打架。
一个比较实用的做法是,先让后端把block内部的电源mesh画好,再在mesh间隙里找能够连续放置大量pin的“走廊”,pin放在这些走廊里,pin的金属层和pin的宽度都按这个层的最小宽度与最小间距设置,但建议在pin两端各留出至少两个standard cell track的余量,作为“escape route”。
3.3 按数据流方向排布Pin的方向
Pin的方向问题,业内常说“pin要朝着它要去的方向”。这句话听起来像废话,但实际执行起来特别容易出错。
假设block A在芯片的左侧,block B在右侧,它们之间有一组高速数据总线,那么A的output pin应当放在A block的右侧边界上,并且pin的“朝向”(也就是pin金属层从boundary伸出去的方向)应当朝右,指向B。同理,B的input pin应当放在B block的左侧边界上,pin伸出方向朝左,指向A。如果pin放错边,总线就得绕半个block才到达对端,绕线长度和延迟都会显著恶化。
有一种特殊情况值得单独说:上下两边都有对端模块时,你必须在partition阶段就把pin的归属规划好,哪些信号走上方、哪些走下方。这一步如果做晚了,后面的顶层绕线很难救回来。我个人会准备一张框图,把每个block的四条边的pin数量预算(estimated pin count per edge)都列出来,再把这个预算和block内部latency、congestion做一轮同步。
3.4 Pin的合并、分组与bus排布技巧
我们在实际项目里,很少会把每个RTL port单独当成一个pin来assign,更多时候是要做pin grouping:
- 同一组bus的pin尽量放在相邻track上,保持大小一致、金属层一致、间距一致,这样方便顶层绕线工具做bus routing,也能减少不同bus之间的交叉。
- 同名端口组里的differential pair或clock group尽量成对出现,避免某一根信号夹在两条完全不同的总线之间,导致coupling和SI问题。
- spare cell的enable信号、scan enable、test mode这类全局信号的pin尽量集中放在block某个角落,而不是平均散布,便于后续ECO和测试模式切换。
另外,我强烈建议在pin assignment阶段就打开congestion map检查一遍。你可以提前在block周围画几条假想的feedthrough lane(也叫pin escape channel),把pin的排放和“从pin出来后要走哪几条track出block”一起规划。如果pin区域下面的局部congestion已经偏红,就应该调整pin spacing或迁移到相邻走廊。
3.5 一个可复用的Pin Assignment Checklist
下面是我在项目里用的、经过多轮实战打磨过的checklist,每次定稿前都会对着过一遍:
| 检查项 | 具体要求 |
|---|---|
| Pin Layer | 与电源mesh不冲突,建议高层金属走廊,避免M3/M4密集区 |
| Port方向 | 每个pin的方向与目标block位置一致,避免绕block长线 |
| Bus排布 | 同bus优先顺序相邻,同金属层,尽量同track |
| Pin间距 | 满足同层via与绕线最小间距,并多留1~2个track escape |
| Power/ground pin | 与顶层电源mesh对齐,保证PG连接可打孔 |
| 关键时序路径 | 时序关键pin尽量靠近block内部sink/source寄存器方向 |
| 时钟相关pin | 与clock port对齐,避免clock pin在block内跨大片区域 |
| Test/DFT相关pin | 集中摆放,避免测试模式下的绕线拥塞 |
| ECO预留 | 在pin区域附近留spare cell与spare route通道 |
4. 层次化时序收敛:抽象模型、接口时序与跨模块Debug
4.1 为什么顶层时钟不能直接拉进block
这是很多初次做Hierarchical Flow的工程师会犯的严重错误。Flat Flow里,clock tree是从top一路长到每一级寄存器的,CTS可以很自然地balance所有clock path。但在Hierarchical Flow下,每个block在独立做CTS时,block内部只能看到自己层面的clock port和约束,它并不知道全芯片的clock tree长什么样。
如果你在顶层直接把时钟模块的时钟信号一路拉到block内部的寄存器,而不经过block自己的clock port和clock tree,会出现几个问题:首先,顶层时钟到达每个block的时间不一致,做CTS时根本无法balance跨block路径;其次,block内部做clock tree synthesis时没有包含顶层网络的实际延迟,时钟偏差(clock skew)完全失控;最后,这种网络在物理上往往需要大量高层金属布线,会干扰顶层其他信号绕线。
标准做法是:每个block在边界定义一组clock port,block内部自行做CTS,然后在block抽象模型里给这个clock port保留一个明确的时钟延迟/transition信息。顶层只看到每个block的clock port,把block抽象为“时钟从port进去,到内部reg的延迟是多少”的黑盒,跨block路径的时序计算基于这些抽象延迟完成。
4.2 端口时序建模:从简单Block Model到ILM
顶层做时序分析时,必须对每个block的行为做一个模型。模型越精确,顶层时序可信度越高,但计算代价也越大。常见的方案有三种:
- Black box model:把block当成完全黑的盒子,顶层只知道端口的电容/电阻特性,不知道内部cell和时序关系。这个模型只适用于顶层做早期可行性评估,完全不能用于时序signoff。
- Block abstract / ETM (Extracted Timing Model):在block完成内部实现后,用抽象库模型描述“输入到输出的延迟”、“端口到端口的时序弧”、“时钟端口到内部寄存器的setup/hold关系”。这是最主流的做法,顶层的综合和物理设计都基于这个模型。但ETM是纯时序行为描述,不包含内部逻辑细节,因此当顶层ECO改变了接口延迟时,模型需要重新生成。
- ILM (Interface Logic Model):这是目前大型SoC项目收敛性最好的方案。ILM保留了block边界附近一定深度的真实逻辑网表(通常几级组合逻辑+相关寄存器),顶层在做时序分析时能真正看到接口路径的电路。ILM的仿真和STA结果比ETM更接近真实,代价是抽象文件更大、顶层仿真更慢。
在我的项目里,跨block的高速接口(DDR、PCIe、NoC)几乎全部使用ILM,而低速、非关键的接口则用ETM。这样既保证了收敛精度,又不会让顶层仿真慢到不可接受。
4.3 输入输出延迟的设置和Budgeting策略
Hierarchical Flow下,每个block在独立签核时要“假装”知道外部环境,这个“假装”用的是set_input_delay / set_output_delay约束。但问题是,这些delay值是从哪来的?
常见的做法是先跑一版顶层设计,用比较粗糙的block抽象模型做时序分析,得到每个block端口上的arrival time和required time,然后把结果作为外部延时的初值反馈给各个block。block完成内部收敛后,再生成新的抽象模型,更新顶层时序,再计算新的端口delay。如此迭代几轮,直到收敛。
这一套流程听起来顺理成章,实际操作中最容易翻车的地方在于跨block路径的budgeting过于乐观。比如顶层给了block A的output port 2ns的available time,block A在内部实现时也顺利收敛了,但顶层把路径另一侧的block B的输入延迟调整了一点点,整个跨block路径就violate了,而且往往violate得很剧烈,因为两侧的报文可能都接近极限。
我的建议是,跨block的budget要刻意留出一部分“灰色状态”空间,比如把目标时钟周期的5%到10%作为跨block margin预留,不要把每条路径都压到零余量。这样项目后期即使有局部微调,也不至于牵一发而动全身。
4.4 在顶层看到的跨模块时序问题怎么Debug
当你真的在顶层看到一条跨block的violation路径时,不要急着改某一个block的约束。先定位三个问题:路径的起点在哪个block、终点在哪个block、接口delay是由哪一侧贡献的。
常用的流程是我自己的三板斧:
- 看顶层STA report里这条path的clock path信息。如果两侧的clock skew异常偏大,先去检查两个block的clock port到内部reg的抽象延迟是否一致,很多时候是某个block的CTS没有做够balance。
- 看data path在顶层部分花了多少时间。如果顶层绕线占了path delay的一半以上,先去看是不是pin assignment的位置选得不好导致绕线绕了远路,还是top层congestion导致工具被迫绕行。
- 如果顶层绕线不多,但path还是收不回来,那就回到两侧block内部,看接口路径的源寄存器到output pin、input pin到目的寄存器的delay是否都合理。哪一侧太紧,就回到那一侧的block去优化。
还有一个我吃了不少亏的点:跨block路径的SPEF(寄生参数)必须包含顶层和两侧block的联合RC,单独用block内部SPEF + 顶层理想线延迟是不准的。尤其在高频接口(比如2GHz以上)上,寄生电容对延迟的影响非常明显,用不完整的寄生模型做出来的结果基本都是过度乐观或过度悲观的假信号。
5. 电源规划、物理验证与团队交接:层次化最容易翻车的隐藏环节
5.1 PG连接:层次化里“看不见的经络”
Pin assignment通常只关心signal pin,但层次化流程里power/ground连接更是重灾区。每个block内部有自己的power ring、power stripe,甚至独立的power domain(多电压域);顶层有一个覆盖整个die的power mesh。block和顶层之间的PG连接,就靠block边界上的power pin(vendor pin/domain pin)和顶层的power via来实现。
这里面最常见的坑是:block内部IR drop已经收敛,但并到顶层以后IR drop就变差,原因往往是block边界上的power pin数量不足、或者pin的位置没有和顶层power mesh的网格对齐。
我建议在partition阶段就把block的power pin location和顶层的power mesh结构一并规划,至少在block的四周每一条边上都得有足够密度的pin,供顶层打via接mesh。对于大电流的block(比如CPU cluster),更要在顶层mesh上做一次全局IR drop仿真,确认压降主要落在哪个区域、是不是因为block某条边忘留了PG pin。
5.2 DRC/LVS的边界地带
层次化设计的物理验证最难的不是block内部,而是boundary上的那些悬空金属、重叠cut、以及跨层结构。举个例子,两个block相邻,它们各自的DRC都干净,但拼到一起之后,两边的power rail和signal pin之间可能形成新的、只在交界处才出现的间距违例。
严格做法是block级做一遍完整DRC/LVS,顶层也做一遍完整DRC/LVS,然后把两边的违例坐标对齐,人工review哪些是block边界造成的。这个活儿很费人力,也是我每次项目后期最不愿干但不得不干的事情之一。
有一个小技巧是:在block signoff时就要求每个block在boundary上留出足够宽的空置区域,不要贴着boundary放critical cell和dense wire,这样两个block拼起来之后,交界处自然有一条“缓冲区”,DRC违例数量能大幅下降。代价是面积利用率会损失一点,但和后期修DRC的返工成本相比,这点面积损失完全值得。
5.3 层次化数据交接清单
多个工程师并行在同一个工程里工作,数据交接的规范化程度直接决定项目会不会乱。我整理过一份前后端设计团队通用交接清单,你可以根据自己的项目调整:
| 数据类型 | 说明 | 更新频率 |
|---|---|---|
| Netlist | block内部最终网表,必须和顶层集成列表匹配 | 每次ECO后更新 |
| LEF / DEF | block的物理信息、pin位置、blockage、routing信息 | 每次floorplan/pin变化后更新 |
| Abstract View (LEF abstract + lib) | 供顶层使用的简化物理与时序模型 | block signoff后生成,ECO后重新生成 |
| SPEF | block内寄生参数,用于顶层联合STA | 每轮signoff时同步输出 |
| SDC / constraints | block接口约束与内部时钟约束 | 随RTL/时序变化更新 |
| UPF / CPF | 电源 intent,多电压域信息 | 每次电源规划调整后更新 |
| GDS / OASIS | 最终版图数据 | 每次需要tapeout时同步 |
| DRC / LVS report | 含waiver清单和top vs block的违例差异 | DRC/LVS clean后归档 |
交接的时候我强烈建议搞一个“golden file”概念——任何后续修改都必须基于最新golden的版本进行,并且每次交接都要附一个changelog,写清楚这版改了什么、影响了哪个block、有没有导致接口时序或pin位置变化。没有changelog的庞大数据库,两周之后连你自己都分不清当前def里的pin坐标是不是最终版。
5.4 多人协作里的版本管理与ECO流
最后聊聊版本管理。Hierarchical Flow的ECO比Flat Flow麻烦得多,因为一次ECO可能只影响某一个block的内部逻辑,但顶层的抽象模型和SPEF也必须跟着更新,否则顶层STA就是挂在旧模型的错误分析上。
我见过太多项目在ECO环节翻车,就是为了赶进度,block内部直接在版图上手工改cell,改完就流片,完全没回头更新抽象模型。结果顶层的时序报告看起来一切正常,实际芯片里跨block路径早就变了。
所以,无论多赶,每次ECO都必须把“修改block网表→重新生成抽象模型→更新顶层SPEF→跑顶层回归”这条链路走完整。宁可顶层回归多跑一个晚上,也不要让模型和版图脱节。
6. 我的一些个人经验和踩坑总结
做层次化流程这几年,如果只让我分享三条最实在的经验,我会说:
第一,Pin assignment的优先级永远高于block内部congestion优化。很多人习惯先做floorplan和place,后面再挪pin,结果就是pin挪一次,内部绕线全部受影响。反过来,先花一两天时间把pin放到位,哪怕内部congestion稍微差一点,后面都有很多方法去修;pin位置乱了,后面基本只能推倒重来。
第二,跨block路径的margin不要抠太死。Hierarchical Flow的时序收敛本质上是一个反复迭代的过程,每一层抽象都会带来一点点延迟误差。你在一开始就把压力拉满,后面任何一个小扰动都会让整个收敛过程崩溃。多留5%到10%的裕量,看着保守,实际上能让你省下好几轮全芯片迭代的时间。
第三,团队内部的沟通规范远比工具流程重要。一个block的pin坐标变了,是不是同步通知了顶层负责人?抽象模型更新了,是不是在changelog里写了清楚?这些看似琐碎的“项目管理”问题,在层次化流程里会直接转化为你晚上要不要被叫起来debug的成本。我会在工作群里养成一个习惯:所有pin map调整必须@顶层和DRC/LVS负责人,所有抽象模型更新必须@顶层STA负责人。规矩很简单,但坚持做下来,项目的后期会顺畅很多。
层次化设计不是一条一劳永逸的路,它只是把原来“全芯片一把梭”的复杂度,转移到了“如何切分、如何交接、如何收敛接口”这些新的维度上。希望这篇盘点能帮你在这些维度上少踩一些坑。以后有机会,我再接着写abstract view生成和顶层集成ECO的更深入细节。