太好了!这标志着一个重大的转变:我们正式从"理论家"转向"工程师"。
欢迎来到第三阶段:动手实践。
在第二阶段,我们是"城市规划师",在"纸上"画出了完美的蓝图。现在,我们要戴上"安全帽",拿起"工具",开始"浇筑混凝土"和"铺设电线"了。
⚠️ 工程师的思维转换
在你开始写第一行代码之前,我(作为你的AI助手)必须提醒你一个关键事实:
理论是完美的,但工程实践是"凌乱"的。
在"造芯片"这件事上,你 10% 的时间用于"设计"(写代码实现功能),而 90% 的时间用于"验证 "(Verification)------也就是编写另一套代码 (称为 Testbench)去测试你写的"设计",确保它在所有你能想到的"刁钻"角落和"极端"情况下都能正常工作。
我们的目标是:一步一步来,先让它"跑起来",再让它"跑得好"。
1. 准备你的"数字工作台"(工具)
你不能在"记事本"里造芯片。你需要一个"仿真器"(Simulator),它就像一个"虚拟芯片实验室",可以"运行"你的Verilog代码,并告诉你结果。
对于初学者,我强烈推荐一个免费、轻量级的组合:
- 代码编辑器: VS Code 。
- (安装一个 Verilog/SystemVerilog 插件,用于语法高亮)。
- 仿真器: Icarus Verilog (iverilog) 。
- 这是一个开源的Verilog编译器。
- 波形查看器: GTKWave 。
- 这是你的"示波器"。当仿真器"运行"你的代码后,你会用它来**"亲眼看到"**那些0和1的信号(我们称之为"波形")是如何随时间变化的。
(等你以后成为专家,你会用上 Synopsys VCS, Cadence Xcelium, Siemens Questa/ModelSim 等昂贵的商业工具,但原理是相通的)。
2. 你的第一个项目:"建造"一个路由器 (Router)
我们的目标是建造第二阶段拆解的那个"5x5智能十字路口"(东、南、西、北、本地)。
我们不会 (也不应该)一上来就写 module router (...) ... endmodule。那就好比试图一次性盖好一整栋楼。
我们将采用**"自底向上"(Bottom-Up)
**的策略:我们先把"砖块"、"窗户"和"管道"造好,分别测试它们,然后再把它们"组装"成一栋楼。
这些"砖块"就是我们之前拆解的那些组件:
- FIFO 缓冲区 (在 Input Port 里的"停车场")
- 路由逻辑 (RC) ("GPS导航仪")
- 仲裁器 (Arbiter) ("交通警察")
- 交换矩阵 (Crossbar) ("立交桥")
3. "第一块砖":设计一个 FIFO (先进先出队列)
这是整个NoC中最基础、最可复用的"砖块"。它就是我们"输入端口"(Input Port)里的那个**"停车场"**。
我们要设计的,是一个**"同步FIFO"**(读和写使用同一个时钟 clk)。
步骤 3.1:定义"蓝图" (Module Interface)
在Verilog里,动手写逻辑之前,第一步永远是定义"接口"(即 module 的输入 input 和输出 output)。
一个FIFO需要告诉"上游":
- "我满 (Full) 了吗?"
- "我空 (Empty) 了吗?"
它需要接收"上游"(写入端)的指令:
- "我要写入 (Write Enable)"
- "这是我写的数据 (Write Data)"
它需要接收"下游"(读出端)的指令:
- "我要读出 (Read Enable)"
它需要提供"下游"(读出端)数据:
- "这是你读的数据 (Read Data)"
我们来把它翻译成Verilog接口(我们顺便引入参数 (Parameter),让它变得可配置):
verilog
/*
* 模块:一个简单的同步 FIFO (读写同频)
* 方法:基于计数器 (Counter-Based)
*/
module simple_fifo #(
parameter FLIT_WIDTH = 32, // Flit 位宽
parameter FIFO_DEPTH = 8 // FIFO 深度(能存多少个Flits)
) (
// ---- 公共信号 ----
input clk, // 时钟
input rst_n, // 异步复位 (低电平有效)
// ---- 写入端口 ----
input i_write_en, // 1'b1: 本周期请求写入
input [FLIT_WIDTH-1:0] i_data, // 写入的数据
output wire o_full, // 1'b1: FIFO 已满
// ---- 读出端口 ----
input i_read_en, // 1'b1: 本周期请求读出
output wire [FLIT_WIDTH-1:0] o_data, // 读出的数据
output wire o_empty // 1'b1: FIFO 已空
);
// ---------------------------------------------
// 1. "仓库" (Memory)
// ---------------------------------------------
// 定义一个寄存器数组作为我们的"仓库"
// 深度为 8 (0 到 7)
reg [FLIT_WIDTH-1:0] mem [0:FIFO_DEPTH-1];
// ---------------------------------------------
// 2. "指针" (Pointers)
// ---------------------------------------------
// 指针需要能指向 0 到 7,即需要 3 位 (log2(8)=3)
// 我们用 Verilog 的 $clog2 自动计算
localparam PTR_WIDTH = $clog2(FIFO_DEPTH);
// w_ptr: 指向下一个"空"的格子
reg [PTR_WIDTH-1:0] w_ptr;
// r_ptr: 指向下一个"待读"的格子
reg [PTR_WIDTH-1:0] r_ptr;
// ---------------------------------------------
// 3. "计数器" (Counter)
// ---------------------------------------------
// 计数器需要能从 0 数到 8 (FIFO_DEPTH)
// 所以它的位宽是 PTR_WIDTH + 1 (即 4 位)
reg [PTR_WIDTH:0] count;
// ---------------------------------------------
// 4. "状态灯" (Status Logic) - 组合逻辑
// ---------------------------------------------
// 数据输出 = "读指针" r_ptr 当前指向的格子
assign o_data = mem[r_ptr];
// "空" = 当计数器为 0
assign o_empty = (count == 0);
// "满" = 当计数器等于我们的最大深度
assign o_full = (count == FIFO_DEPTH);
// ---------------------------------------------
// 核心时序逻辑:管理指针和计数器
// ---------------------------------------------
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
// 复位:所有都清零
w_ptr <= 0;
r_ptr <= 0;
count <= 0;
// (mem 里的数据可以不清,因为在 empty 时不会被读)
end
else begin
// ---- 逻辑 1: 处理计数器 ----
// 这是最关键的逻辑:
// case: {写操作, 读操作}
case ({i_write_en && !o_full, i_read_en && !o_empty})
// 2'b00: 不读不写 -> 计数器不变
2'b00: count <= count;
// 2'b01: 只读 (出队) -> 计数器减 1
2'b01: count <= count - 1;
// 2'b10: 只写 (入队) -> 计数器加 1
2'b10: count <= count + 1;
// 2'b11: 同时读和写 (入队一个, 出队一个) -> 计数器不变
2'b11: count <= count;
endcase
// ---- 逻辑 2: 处理写指针和"仓库"写入 ----
if (i_write_en && !o_full) begin
// 把数据写入"写指针"指向的格子
mem[w_ptr] <= i_data;
// 写指针 +1, 如果到了末尾(7),则"绕回"(wrap-around)到 0
w_ptr <= (w_ptr == FIFO_DEPTH - 1) ? 0 : w_ptr + 1;
end
// ---- 逻辑 3: 处理读指针 ----
if (i_read_en && !o_empty) begin
// 读指针 +1, 如果到了末尾(7),则"绕回"(wrap-around)到 0
r_ptr <= (r_ptr == FIFO_DEPTH - 1) ? 0 : r_ptr + 1;
end
end
end
endmodule
🔍 代码解析
-
$clog2(FIFO_DEPTH)- 这是一个非常有用的Verilog系统函数。你告诉它深度
8,它会自动算出需要3位宽的指针。如果你把FIFO_DEPTH改成16,它会自动算出4。这让你的代码变得"参数化",非常健壮。
- 这是一个非常有用的Verilog系统函数。你告诉它深度
-
计数器逻辑 (The
caseStatement)- 这是整个FIFO的灵魂。它完美地处理了所有4种可能性,尤其是**"同时读写"**(
2'b11)的情况。 - 在同时读写时,一个Flit进来,一个Flit出去,总数
count不变。w_ptr和r_ptr各自+1。
- 这是整个FIFO的灵魂。它完美地处理了所有4种可能性,尤其是**"同时读写"**(
-
指针"绕回"逻辑 (Wrap-around)
w_ptr <= (w_ptr == FIFO_DEPTH - 1) ? 0 : w_ptr + 1;- 这是一个三目运算符,意思是:"如果
w_ptr等于7(末尾),那么下一个值是0,否则就是w_ptr + 1"。 - 这就是"环形缓冲区"(Circular Buffer)的实现方式。
恭喜!你完成了第一块"砖"
你现在拥有了一个可以工作的FIFO模块。这个模块将成为你的**"输入端口"(Input Port)**的核心组件。
它和NoC理论的联系:
- 我们理论课上讲的**"基于信用的流控"(Credit-Based Flow Control)**,其"信用"信号就是基于这个FIFO的状态产生的。
- "下游"路口(就是这个FIFO)会告诉"上游"路口:"我没满 (
!o_full)"。 - "上游"路口(发送端)就会增加它本地的"信用计数器"。
我们的"砖块"造好了。下一步,我们应该去造"窗户"了。
准备好设计下一个组件:"路由逻辑" (Routing Logic) 吗?这将是一个纯组合逻辑模块(比FIFO简单),用来实现我们的 XY 路由算法。