简介:一篇面向片上网络(NoC)与路由器设计人员的技术文献,系统介绍基于电路交换的NoC路由器设计与实现,针对传统总线结构下的片上通信瓶颈,给出面向无线通信等有保障服务场景的完整方案。资源为1个PDF文件,共696KB,内容涵盖NoC网络模型、电路交换原理、路由器功能定义、接口与通路划分、Verilog HDL代码实现以及Altera FPGA硬件验证等,目录层次清晰,便于按章节查阅。目前已有134人学习,适合集成电路、计算机体系结构方向的工程师、高校师生及研究者参考。读者可从中获得支持5×5端口、每端口9.6Gbit/s、总交换容量48Gbit/s、100%无阻塞交换并可配置为多播/广播模式的路由器完整设计思路与实现细节,对理解NoC路由器设计、确定性路由策略、Verilog建模和FPGA原型验证均具实用价值。该设计面向需要保证吞吐量和低延迟的无线通信场景,为克服纳米级SoC设计中的带宽和接口标准化难题提供了有益借鉴。 做多核SoC互连设计的朋友应该都有过这种经历:核心数一超过八个,全局总线的仲裁优先级就难写,仿真波形拉到十万个cycle,到处都在排队等带宽。我自己在项目里切到NoC(Network-on-Chip,片上网络)之后,一开始直接用了主流的分组交换方案,跑起来确实比总线强很多。但后来遇到一批以长块连续传输为主的应用,才发现分组交换让每个数据包在每个路由器里反复查表、排队、仲裁,白白浪费了大量面积和功耗。于是就有了这篇文章的主角:一个基于电路交换(Circuit Switching)的NoC路由器。它是我自己从零开始设计、写RTL、做验证、跑FPGA综合的一套完整模块。下面我把整个设计过程,包括架构、握手时序、仲裁策略、实测数据和调试教训,从头到尾讲一遍。
1. 为什么电路交换方案值得动手做一次
在展开架构之前,先把动因讲清楚。搞懂为什么用方案A而不是方案B,比直接看RTL更重要。
1.1 全局总线的共享介质模型撑不住
传统SoC的第一反应是AMBA总线。AHB/AXI在四五个主设备的时候表现尚可,一旦扩展到八个以上主设备,仲裁矩阵和多路选择器的逻辑深度会明显增长,时钟频率往下掉。更麻烦的是,总线带宽被所有主设备共享,只要有两个高带宽模块同时发起长时间传输,总线上就会出现严重的拥塞。这不是靠把数据位宽从64bit加到128bit就能解决的,因为限制上限的是共享介质的冲突概率,而不是物理线宽。总线本质上是一个“所有节点共享一份介质”的模型,想要线性扩展,只能换互连拓扑。
1.2 分组交换的主流方案在两种场景下很吃亏
NoC替代总线之后,内部采用分组交换是主流。每个数据包被切成flit,每经过一个路由器都要走“路由计算、写入FIFO、排队仲裁、穿crossbar”这套流程。这套流程的优点是流量适应性强,能被不同应用动态均衡;缺点是每个端口都要放一堆FIFO,面积和功耗都不低,而且只要有拥塞,端到端延迟就会明显抖动。这导致它面对两类场景时很吃亏:一是流式长块传输,比如DMA搬运、视频编解码模块和加速器之间的连续数据流;二是对延迟抖动有硬性要求的应用,比如实时信号处理、同步采样控制。这些场景里,数据说白了就是一连串长flit流,根本不需要在每个路由器里重新决策。
1.3 电路交换的做法:把决策前移,把传输变直通
电路交换的思路完全不同。数据开始传输前,先发一个连接建立请求,沿途路由器只配置一次交叉开关,把源端到目的端的物理路径锁定;配置完成之后,路径上所有路由器不再介入数据流向判断,数据一拍一个flit地从交叉开关上直通过去;传完再发一个释放信号,把交叉开关资源收回来。这样省掉了FIFO、省掉了逐拍仲裁,延迟抖动也几乎为零。代价是需要额外的建立时间,而且链路一旦占用就是独占的,不适合短小密集的突发流量。所以我不认为电路交换能全面替代分组交换,但把它用在对的场景里,收益非常可观。这次做的4x4 mesh电路交换NoC路由器,就是为了把“对场景下的收益”这组数据量化出来。
2. 路由器整体架构:哪些模块是电路交换独有的
2.1 五端口路由器的模块组成
我采用的是二维mesh里最经典的五端口路由器结构:东、西、南、北四个网络端口,加上一个本地处理单元(PE)端口。内部模块划分如下:
| 模块 | 作用 | 电路交换方案的特殊性 |
|---|---|---|
| 输入端口控制器 | 对输入信号采样、对齐和握手 | 不需要大容量FIFO,只需寄存器锁存 |
| 路由计算单元(RC) | 根据目标坐标计算下一跳方向 | 只在链路建立阶段工作,传输阶段完全闲置 |
| 链路控制单元(LC) | 维护每条链路的状态,处理建立与释放 | 这是电路交换的核心,分组交换里没有 |
| 交叉开关(Crossbar) | 把输入数据导向输出端口 | 需要支持锁存配置,配置后不再逐拍仲裁 |
| 配置寄存器组 | 记录端口占用情况、链路统计信息 | 必须有,用于异常排查和性能计数 |
从表里能看出,电路交换路由器和分组交换路由器最大的区别,就是把“大容量FIFO+逐拍仲裁器”换成了“链路控制单元+可锁存的Crossbar”。这个思路听起来简单,但代价是LC的逻辑必须足够严谨,因为全网络的链路状态都依赖它维护。
2.2 控制通路和数据通路一定要分开
设计过程中一个教训是:最初为了省布线和逻辑,我把建立信号、ACK信号和数据flit放在同一套交叉开关里传输。结果在功能仿真里就出现了矛盾——链路建立是需要配置crossbar的,可已建好的数据连接又占着crossbar不放,两个操作直接冲突。后来我彻底分成两条通路:控制通路用一条窄位总线专门传SETUP、TEARDOWN、ACK/NAK信令;数据通路只在链路进入Active状态后才被使用。两条通路仍然同源时钟,但逻辑上完全解耦。这种分层在电路交换里不是可选项而是必选项。如果你也在写这类设计,建议第一步就把控制通路和数据通路分开定义,不然后面重构成本很高。
2.3 位宽选择与传输粒度的细节
数据通路的位宽我选了64bit数据加8bit控制,总共72bit每拍。选这个值主要考虑两点:一是PE侧的AXI接口是64bit,对齐后不用额外转换;二是crossbar面积会随位宽超线性增长,盲目加到128bit会让交换矩阵大出一倍还不止。传输粒度上,电路交换不需要在传输过程中定义包的边界,源端建好链路后可以连续发送任意长度的flit序列。不过我额外加了一个小功能:SETUP请求里捎带期望传输的flit总数,沿途路由器记录后用于自动释放。这样即使源端软件出错忘记发释放信号,硬件也能在计数器归零后自动拆链,避免链路悬挂。这个功能本来是顺手加的,却在后面的调试中救了很多次。
3. 链路建立与释放机制:状态机和握手时序设计
3.1 两阶段配置的链路建立流程
链路建立我用的是“SETUP逐跳配置+ACK逐跳确认”的两阶段方式。源端发出一份SETUP请求,里面包含目的节点坐标X/Y、期望传输长度L和源端口号。SETUP沿路由算法指定的方向逐跳前进,每一跳的中间路由器要完成三件事:解析目的坐标并算出下一跳方向;尝试把输入端口到对应输出端口在Crossbar上做一个临时连接;连接成功就把SETUP继续转发,失败则向源端方向回NAK。SETUP到达目的端后,目的端不直接开传数据,而是沿原路径逐跳回送ACK。沿途路由器收到ACK,才把临时连接升格为正式连接。源端收到ACK后,整条链路才算真正打通。这个“临时-正式”两阶段设计,是为了防止部分路径已经配置好而另一部分还没配置时,数据提前灌入导致中间节点状态错乱。
3.2 输出端口状态机:Idle、Setup、Active、Teardown
每个输出端口对应一个链路控制单元(LC),内部状态机我设计成四个状态:Idle表示端口空闲,可被SETUP请求竞争;Setup表示已有SETUP在路径上传送,等待ACK或NAK;Active表示链路已建立,数据流直通crossbar;Teardown表示正在回收crossbar资源,等待释放完成。核心状态转移逻辑用Verilog描述大致是:
always @(posedge clk or negedge rst_n) begin if (!rst_n) state <= IDLE; else begin case (state) IDLE: if (setup_granted) state <= SETUP; SETUP: if (ack_received) state <= ACTIVE; else if (nak_received) state <= IDLE; ACTIVE: if (teardown_req) state <= TEARDOWN; TEARDOWN: if (teardown_done) state <= IDLE; endcase end end项目里还加了超时、阻塞重试等条件,主干就是这四态的循环。每种跳转都对应协议里的一个明确信号,排查问题的时候逻辑非常清晰。
3.3 释放链路与异常兜底
释放链路最常规的做法是源端在最后一个flit发出后,沿原路径发TEARDOWN信号,沿途路由器收到后恢复crossbar配置,回到Idle。但只做这一点是不够的,我在三个位置加了保险。第一是长度计数器:建立时记录的flit总数在Active状态每个cycle递减,减到0就自动发起内部释放,解决源端忘记发TEARDOWN的问题。第二是看门狗:每条链路建立时记录时间戳,如果传输计数归零后一段时间仍未完成释放,就强制清除crossbar配置并上报异常计数。第三是部分释放:如果某个TEARDOWN在中间跳丢失,由中间跳的LC在超时后主动撤销自己的配置,避免链路残留。加上这套兜底之后,“链路悬挂”这类故障在长时间随机测试里基本没有再见过了。
4. 路由计算与仲裁:竞争场景下的细节处理
4.1 为什么选XY维序路由
在mesh网络里可用的路由算法不少,我这个项目选XY维序路由,理由有两个。第一是正确性:XY是维序路由的一种,任何一条从源到目标的路径都是先沿X方向走到目标列、再沿Y方向走到目标行,在拓扑里是一条没有回头的单调折线。既然是单调的,就不存在多条链路的持有-等待环路,天然不会死锁,不需要引入额外虚拟通道。第二是硬件实现简单:下一跳计算就是坐标比较加符号判断,一小组合逻辑就能完成。相比之下,自适应路由需要实时感知邻居拥塞状态,硬件复杂度、验证工作量都会大不少。对于第一版验证电路交换收益的项目,XY是性价比最高的起点。
4.2 建立仲裁:先到先服务加原子锁定
多个SETUP请求同时到达同一个输出端口是必然事件。我在每个输出端口放了独立仲裁器,输入来自四个方向的SETUP请求和本地PE的SETUP请求。仲裁策略是“先到先服务+原子锁定”:一旦某个SETUP在仲裁中胜出,该输出端口立即进入锁定状态,后续SETUP不能抢占它,直到该连接成功进入Active或因为NAK被释放。这个原子锁定非常关键,没有它就会出现两个SETUP在crossbar上各占一半、互相错位的尴尬场景。同一拍到达的多个SETUP,则用一个轮询指针决定优先级,每完成一次成功建立指针后移一格,这样长时间运行下每个方向都有建立机会,不会出现某方向被饿死的现象。
4.3 建立冲突时:有限阻塞加超时回滚
当SETUP走到中间一跳,发现输出端口已经被其他链路锁定,怎么处理很影响整体表现。最直接的想法是立刻回NAK,让源端整体重来。这种做法的缺点是竞争激烈时会导致大量反复建立尝试,网络有效利用率会降低。我最后采用的是“有限阻塞+超时回滚”:被阻塞的SETUP在节点内暂时寄存,LC周期性检查目标端口,一旦释放就继续往下游传;只有阻塞超过设定周期(我设为16个cycle)才回NAK。好处是竞争高峰过去后,多数被阻塞的SETUP能自动续传完成,而不是把压力全部压回源端。实测中这个策略显著提高了多流并发场景下的建立成功率,代价只是每个中间节点多存一份SETUP上下文的寄存器组,开销可以接受。
5. 验证与实测:延迟、吞吐和面积的真实收益
5.1 验证平台与三类测试用例
验证环境搭在SystemVerilog加VCS下,核心是cycle级参考模型加自动比较器。参考模型知道每一对源目的节点之间的最短路径,也知道每跳建立、传输、释放各消耗几个cycle,所以能精确预测每个数据flit应该在哪个节点哪个cycle出现。比较器在每个cycle把参考模型输出和DUT输出做逐位比对,出现flit丢失、错位、顺序颠倒会立即打印错误信息。测试用例分三类:单流长传输,两个节点连续传4096个flit,验证完整链路正确性;多流竞争模式,随机选择多对源目的节点以不同注入率同时发起连接,重点验证仲裁和公平性;对角线长路径,让流量跨越大半个网络,压测长路径下的建立延迟和释放恢复。所有用例最后都跑通,连续200万cycle没有死锁和链路悬挂。
5.2 延迟与吞吐的实测表现
性能数字方面,在4x4 mesh中一条跨3跳的典型路径,从源端发出SETUP到收到ACK,建立延迟实测大约6到8个cycle,其中包含每跳的流水级和仲裁等待。建立完成后,数据每个cycle稳定输出一个flit,不是平均一拍,而是每一拍都在传。这是电路交换和分组交换最本质的区别:路径建好后,网络中不再存在缓冲排队这个动作。多流极限竞争时,建立延迟会明显上升,最差能到60个cycle以上,但一旦建立成功,传输带宽仍然保持每拍一个flit,不会因为网络拥塞而抖动。对实时性要求高的场景,“建立阶段的等待可预测、传输阶段零抖动”这两点非常有用。需要说明的是,不同工艺、不同频率下这些数字会浮动,但反映的设计特性是一致的。
5.3 与分组交换对比:面积和功耗的差距
为了量化收益,我拿之前做过的同规格分组交换路由器做参照,在同一个FPGA上做了综合对比。分组交换方案每个端口要放深度8个flit的FIFO,加上逐拍仲裁逻辑,LUT和FF的消耗明显更大;电路交换方案把FIFO彻底去掉,仲裁逻辑只在建立阶段工作,单路由器面积可以做到分组交换的一半以下。功耗的差距更明显:空闲状态下,分组交换的FIFO指针、仲裁器状态都在随时钟翻转;电路交换处于Active的链路只是保持crossbar导通,没有额外翻转,动态功耗降了一大截。当然这个对比不是单方面碾压。如果流量是大量短包突发,比如几十个3-flit的包来回打,链路建立开销会被摊到很短的传输段上,电路交换的优势会消失甚至变成劣势。它的适用面很明确:长块连续传输占主流的场景。
6. 三个把我坑到凌晨的调试问题
6.1 配置寄存器跨时钟域导致端口假占用
第一个坑在配置寄存器组的跨时钟域处理上。最初我把CPU配置总线的信号直接连到核心逻辑,没有做同步。结果长时间随机测试里,偶尔会出现“某端口明明没有链路却一直显示被占用”的假象。定位到最后,是配置总线上的异步写信号和核心逻辑时钟沿发生竞争,违反了触发器的建立保持时间,把中间态捕获进了寄存器。修复方案就是标准的两级触发器同步器加请求-应答握手。这里想提醒一句:不要因为两个时钟标称同频就跳过跨时钟域处理,只要相位关系不确定,亚稳态就一定会存在。
6.2 释放慢半拍导致的链路残留
第二个坑很隐蔽。在Teardown状态里,我最开始先恢复crossbar连接,再置teardown_done信号。功能仿真一切正常,但时序仿真里出现偶发窜扰:新的链路建立后,数据偶尔会打进上一个连接的输出端口。反复对比波形才发现,crossbar连接使能的撤销路径上有一级组合延迟,teardown_done已经置位,但crossbar连接实际还保持半拍。修复方法不复杂:在Teardown里强制插入一个额外cycle的延时,确认crossbar连接真正撤销后再回到Idle。代价是释放时间增加一个cycle,但彻底消除了残留连接问题。这类问题功能仿真查不出来,必须靠时序仿真去暴露。
6.3 建立路径组合逻辑过长,频率上不去
第三个坑在综合阶段。SETUP请求的信号路径要穿越“地址解析-路由计算-输出仲裁-crossbar配置”这条长组合链,在4x4网络里还要跨多级路由器,关键路径严重超预期。最初目标250MHz,实测只能跑到180MHz左右。解决办法是把建立流程拆成三个流水级:第一级做地址解析和路由计算,第二级做仲裁判定,第三级做crossbar配置。流水化之后建立延迟多了2个cycle,但关键路径大幅缩短,最终频率稳定在250MHz以上。对类似的建立路径时序问题,流水化几乎是唯一靠谱的出路,靠堆组合逻辑优化很难拿到理想结果。
这三个问题有个共同点:功能仿真都查不出来,全部是时序仿真或综合后才暴露的。所以做类似项目时,尽早引入时序仿真和综合约束,别等RTL全部写完再集中回头处理,那时候定位成本会高很多。如果重来一遍,我会在第一天就把控制通路的时序约束单独提出来。电路交换NoC的核心不在数据通道而在控制通道,控制通道的时序质量,直接决定了整个网络能稳定跑多快。
本文还有配套的精品资源,点击获取