新手在 RTL 设计中往往以算法原型的功能为出发点,而低估了控制流的实现难度。实际开发过程中,算法功能对应的模块很快完成,而“黏合”模块间的控制流设计却迟迟不能按预期进度验证通过,不断延误开发周期 本人血淋淋的教训。本文将尝试量化分析模块之间的复杂度来源 (inter-module),并给出降低设计复杂度的方案。
魔鬼存在模块之间
我们从最简单的一个例子开始,多个模块首尾相连,流水依次传递数据,并且每个接口都是不涉及反压的 valid 接口。控制逻辑一般会抽象出一组状态用以表示全局的同步“时刻”,再由该状态生成各个模块具体控制驱动信号流。此时各个子模块之间的数据延时可以简单计算得出,进而分配各个模块控制信号延时深度。
设计该控制流时我们经历了两个设计过程:
- (Intra-Module)计算各个模块内部延时,一般是一个固定周期延时常数;
- (Inter-Module) 根据各个模块内部延时,推导“全局状态 - 每个模块控制流 ” 之间的传递延时,这里是对各个子模块延时求积分。
上述例子过于理想,实际电路设计模块之间面临额外三个复杂度来源:
- (Graph) 模块之间连接关系往往是非规则的图拓扑结构,而非序列结构;
- (Back Pressure) 各个模块并不能总是就位,涉及反压以及反压处理机制(比如堵塞停顿);
- (Reconfigurable) 在不同功能下会复用子模块资源,这通过状态切换和重构互联通路完成,不同功能下的连接拓扑和反压设计可能都不一样。
最悲观的感性计算,理论上对于 \(N\) 个子模块,最多存在 \(N^2\) 种可能方向连接,考虑 \(R\) 种功能重构,需要完成一个控制逻辑能够完美协调 \(O(RN^2)\) 条(带反压机制)的连接逻辑。除了设计需要考虑的参数量很多导致大脑过载外,这大概是一个非常不规则的设计逻辑,缺乏经验很难在实践前每一个细节都想清楚,导致引起额外的迭代代价。
反压的必要性:反压的设计来源自对模块状态的不确定性,那么是否能够改变设计,使得各个模块能够“完美”地配合在一起从而避免引入反压机制呢?不确定性存在硬性和弹性两种,1)硬性是一定存在的不确定性,比如跨 IO、跨时钟域,运行动态数据 if-else 的控制分歧;2)弹性则是可有可无,比如虽然理论上最顶层模块可以获得所有子模块的状态,存在完美规划避免反压的可能,但实现复杂度太高。不妨假设状态不确定用一个统一的反压逻辑来处理,从而降低设计复杂度。
从另一个角度理解,模块之间大致可划分为空间和时间的关系,比如模块连接关系是空间,不同模块控制信号流水打拍这是时间。而硬件设计中时间和空间二者是耦合的,比如空间连接关系可能影响要延时多少拍,而不同时间电路互联可能重构改变空间连接关系。时空的耦合性质是复杂度的另一个来源。
解耦输出时刻和响应时刻
最简单的数据处理过程包含接收输入数据并产生相应的输出数据,所以除了处理数据的主体之外还需要考虑上游数据产生来源和下游数据接受目标。假设数据依次流过三个主体 "A->B->C" ,如果 A 和 C 是不同模块则是某一条单相的数据通道,若 A 和 C 是同一模块则是通讯的一次过程。把握手周期分为 4 个时刻:
数据 | 传输方向 | 描述 | 符号 |
---|---|---|---|
Rsq | A->B | A 发出请求 | \(t_{rsq,gen}\) |
Rsq->Ack | B 内部 | B 处理数据 | \(t_{rsq,proc}\) |
Ack | B->C | B 发出请求 | \(t_{ack,gen}\) |
Ack->Ack-Ack | C 内部 | C 处理数据 | \(t_{ack,proc}\) |
以及相应的 3 个差值
- rsq 等待处理时间 \(T_{rsq,padding}\)
- rsq 处理时间 \(T_{proc}\)
- ack 等待发送时间 \(T_{ack, padding}\)
\(T_{proc}\) 来自 intra-module ,一般只和功能有关固定延时,而 \(T_{padding}\in\{T_{rsq,padding} T_{ack,padding}\}\) 需要考虑 inter-module 之间的相互作用关系以满足 “数据 ready” 与 “模块 ready” 两个条件。电路设计中使用固定的延时控制 \(T_{padding}\),则是考虑 inter-module 关系耦合了 \(t_{rsq,gen}\) 和 \(t_{rsq,proc}\) 两个时刻的表现。那么能否解耦这两个时刻呢?解耦两个时刻很自然联想到 FIFO 结构。
Channel-Driven Design
考虑从 Buffer 读取数据,计算单元完成计算,再将输出结果写回 Buffer。数据在端口间的流通关系如下图。其中任意曲线是两个端口之间的一条信号通道,包含图中总共有 8 个端口以及 7 个通道,假设所有通道都是 FIFO/Queue 结构。我们关注三个点:
- 如何根据此图推导通道数据读取/写入逻辑?
- 如何考虑反压逻辑处理?
- 如何根据通道判断执行状态?
反压源识别及反压传播路径分析
- 通道 \(i\rightarrow j\) 的读取(deq)的条件: \(\text{Not Empty}_{i \rightarrow j} \land \text{Ready}_{\text{Module j}}\)
- 通道 \(i\rightarrow j\) 的写入(enq)的条件:\(\text{Not Full}_{i\rightarrow j}\lor \text{Valid}_{\text{Module i}}\)
其中 \(\text{Ready}_{\text{Module j}}\) 包含两个条件,模块 j 内部未堵塞可以继续读取数据(Intra-Module),模块 j 输出未堵塞可以输出数据(Inter-Module)。输出侧的堵塞最终也反压回模块内部引起模块堵塞,这两类条件划分是根据堵塞的来源区分,前者堵塞源来自模块自身,表现在接口上即是模块带有 ready 反压输出,而后者堵塞源来自于后继模块返回传导的过程。
Channel-driven 分析分为两个设计阶段:
- Intra-Module: 识别反压源
- Inter-Module: 根据连接关系,从反压源沿着 channel 往回推,找到所有反压可能影响的路径
Channel-driven Design 将模块之间互联延时-打拍逻辑关系简化为了分析反压源沿着互联网络的反向传导,不涉及具体时序延时周期分析,只需要保证 channel 深度冗余即可,用冗余的 queue 资源换取设计复杂度的降低。
比如假设同一时刻其他模块会争夺 buffer 读取接口,需要仲裁分配资源,因此可能在读请求接口发生反压,图中用红色标记反压端口并将反压影响所有路径红色高亮。反压处理逻辑是当下游捕捉到阻塞时,将中断信号通知上游以阻止信息继续发送。具体而言传播回上游的方式有两种,一种是从后往前逐级传播,传播有一个和通道长度一致的延时,因此需要通过 skid buffer 分配额外的 FIFO 资源暂存传播期间上游发送的数据;另一种则是跳跃传播,直接将中断信号传播回堵塞的源头。
这里选择跳跃传播的方法,直接从反压源引出反馈到数据源头,此时所有其余通道不会堵塞(考虑跳跃连接如果要求即使相应可能面临组合逻辑过长的时序问题,此时可以从读口前级的 FIFO 引出反馈信号,假设 FIFO 深度为 D,根据反馈打拍的级数设置为 P,则 FIFO 数据量达到 \(D-P\) 时触发反馈,当 \(P\) 等于前向传播路径级数时,等效为 skid buffer)。
特殊 Chanel 分析
对于有多个输入通道(比如 \(i\rightarrow j, k\rightarrow j\))的读取接口,两个通道共用一个读取接口,其读取的条件为:
图中将拥有唯一的写入和读取接口,并且不会发生堵塞的 Channel 用蓝色标出,因此其读取的必要条件退化为:
而非空和写入互为因果关系,此时写入条件和读取条件等同为一个条件,可以不用 FIFO 退化为简单的 valid-only Pipeline 通道。
运行状态判断
有向图中有两类特殊节点,“源”和“汇”,所有数据的源头和终止,例子中特殊拥有唯一的源 Top State 以及唯一的汇 Write Rsq. 。一般控制逻辑抽象出全局唯一的状态机“源”,如果一个有向图包括多个“源”,可能设计内包含复杂的动态仲裁机制争夺资源。
状态以“源”发送数据为起点,以所有“汇”完成数据处理为终点。因此状态结束的判断逻辑即是所有的 channel 内部数据排空,不仅包括 inter-module 的连接,也要记录 intra-module 内部剩余数据。
设计方法总结
文中还未提到时空重构,channel 复用的设计方法。本文提到的大部分思想属于 TLM 的子集,但这种思想不仅可用于 C-model 建模,亦可用于分析、预估、简化 RTL 设计复杂度。
总结设计流程:
- Intra-module:完成模块功能设计,提取延时、反压源等指标。
- Inter-module:
- 可视化模块间互联关系,并识别反压源
- 根据反压源反向传播,识别反压影响路径,添加反压处理机制
- 识别通道特性,所有通道都可用 FIFO 实现(对应 Chisel 源于
chisel3.util.Queue
),而特殊单向无堵塞通道可退化为无反压 pipeline(对应 Chisel 原语chisel3.util.Pipe
)。并提取状态切换逻辑。