我们迎来了第三阶段和第四阶段之间最宏伟、最复杂的“桥梁”工程:设计“网络接口” (Network Interface, NI)。
在之前的“理论课”中,我们称它为“翻译官”和“物流中心”。现在,我们要亲手来建造它。
我必须在一开始就说明白:这是我们目前为止要建造的、最复杂的单一模块。
router(路由器) 很复杂,但它的工作是“固定的”。ni(网络接口) 更复杂,因为它是一个“状态机”的“怪兽”。它必须能“理解”一种完全不同的、高级的“语言”——AXI总线协议——并将其“实时翻译”成我们NoC的“Flit语言”。
鉴于这个任务的极端复杂性,我们不可能一次性造出一个“全功能”的NI。我们将采用和你我一起建造router时相同的策略:
- 极大简化:我们将设计一个**“最小可行产品” (MVP)** 版本的NI。
- 明确目标:我们只设计**“Master NI”** (也叫 Initiator NI)。
- “Master NI”:它连接到一个“Master设备”(如CPU)。它在AXI总线一侧扮演“Slave”(接收CPU的命令),在NoC一侧扮演“Master”(向NoC注入Flits)。
- 简化协议:
- 我们只实现**“写操作” (Write-Only)**。
- 我们只实现**“固定长度” (Fixed-Length)** 的“突发”(Burst)。比如,我们规定“所有AXI突发必须是4拍 (4 data beats)”。
- 我们暂不处理NoC返回的“写响应” (
BVALID)。
我们的目标是:证明我们可以接收一个AXI写突发,将其打包成 (1个头 + 4个体 + 1个尾) Flits,然后成功注入到我们的router中。
1. 📦 NI 的“翻译”工作
“物流中心” (Master NI) 的工作流:
- 接收订单 (AXI AW/W): CPU(AXI Master)发来一个“写地址” (
AWADDR) 和一个“写请求” (AWVALID)。紧接着,它发来“货物” (WDATA) 和“货物有效” (WVALID)。 - 翻译地址 (Address Translation): NI 内部有一张“地址映射表”。
- 它看到地址
0xA000_1000。 - 它查表:“哦,这个地址属于‘内存’,它的‘NoC坐标’在
(3, 3)。”
- 它看到地址
- 打包 (Packetization): NI 开始组装“火车” (Packet)。
- Head Flit: 目的地
(3, 3),类型 (写),长度 (4)。 - Body Flits (x4): 4拍的
WDATA货物。 - Tail Flit: 标记“结束”。
- Head Flit: 目的地
- 发货 (Injection): NI 遵守“Valid/Ready”握手协议,把这 6 个 Flits (1+4+1) 一个接一个地“注入” (Inject) 到它所连接的
router的“本地”输入端口。
2. 📐 NI 的“蓝图” (Module Interface)
这个模块是“双面”的。
/*
* 模块: 最小可行 Master NI (MVP Master Network Interface)
* 功能: AXI4-Lite (简化版) -> NoC Flits (写操作)
* 假设: - 只支持写操作
* - 所有突发(Burst)固定为 4 拍 (DATA_BEATS = 4)
* - AXI Master 必须先发AW, 再发W
*/
module master_ni #(
parameter FLIT_WIDTH = 32,
parameter COORD_WIDTH = 4,
parameter DATA_BEATS = 4 // 我们的“简化”:所有Burst都是4拍
) (
input clk,
input rst_n,
// ---- AXI4-Slave 接口 (来自 CPU) ----
// -- AW (Address Write) Channel --
input [31:0] s_axi_awaddr, // CPU 写的地址
input s_axi_awvalid, // CPU 的地址有效
output reg s_axi_awready, // "NI: 地址我收到了"
// -- W (Write Data) Channel --
input [FLIT_WIDTH-1:0] s_axi_wdata, // CPU 写的 32-bit 数据
input s_axi_wlast, // CPU 说:"这是最后一个数据"
input s_axi_wvalid, // CPU 的数据有效
output reg s_axi_wready, // "NI: 数据我收到了"
// -- B (Write Response) Channel --
// (在我们的MVP中,我们"假装"总是成功,并立即回复)
output reg s_axi_bvalid, // "NI: 写入成功!"
input s_axi_bready, // CPU 收到"成功"
// ---- NoC Master 接口 (去往 Router) ----
output reg [FLIT_WIDTH-1:0] m_noc_flit, // 发给Router的Flit
output reg m_noc_valid, // "NI: 我的Flit有效"
input m_noc_ready // "Router: 我准备好了"
);
// TODO: 地址翻译器
// TODO: 核心状态机
endmodule
3. 🧠 核心逻辑:状态机 (FSM)
这是NI的“大脑”。它控制着“翻译”的每一步。
// ---------------------------------------------
// 内部寄存器
// ---------------------------------------------
reg [COORD_WIDTH-1:0] dest_x; // 锁存的目的地X
reg [COORD_WIDTH-1:0] dest_y; // 锁存的目的地Y
reg [DATA_BEATS-1:0] data_counter; // 0-3, 计数收了多少WDATA
// 我们用 FIFO 来“缓存”AXI WDATA (这是健壮设计的关键)
// 实例化一个我们之前造的 FIFO!
wire [FLIT_WIDTH-1:0] w_fifo_data;
wire w_fifo_empty;
wire w_fifo_full;
wire w_fifo_read_en;
simple_fifo #(
.FLIT_WIDTH(FLIT_WIDTH),
.FIFO_DEPTH(DATA_BEATS) // 深度刚好等于Burst长度
) w_data_buffer (
.clk (clk),
.rst_n (rst_n),
.i_write_en (s_axi_wvalid && s_axi_wready), // 当AXI握手成功时, 写入FIFO
.i_data (s_axi_wdata),
.o_full (w_fifo_full),
.i_read_en (w_fifo_read_en), // 由状态机控制“何时读”
.o_data (w_fifo_data),
.o_empty (w_fifo_empty)
);
// ---------------------------------------------
// 状态机定义
// ---------------------------------------------
localparam S_IDLE = 3'd0; // 0. 等待AXI命令
localparam S_RECV_DATA = 3'd1; // 1. 正在接收 WDATA (填满FIFO)
localparam S_TRANSLATE = 3'd2; // 2. (简化) 地址翻译
localparam S_INJECT_HEAD = 3'd3; // 3. 注入 "Head Flit"
localparam S_INJECT_BODY = 3'd4; // 4. 注入 "Body Flits" (从FIFO读)
localparam S_INJECT_TAIL = 3'd5; // 5. 注入 "Tail Flit"
localparam S_SEND_BRESP = 3'd6; // 6. (简化) 回复AXI "OK"
reg [2:0] state, next_state;
// ---------------------------------------------
// 地址翻译器 (简化的组合逻辑)
// ---------------------------------------------
// 在 "真实" 设计中, 这会是一个复杂的查表逻辑
always @(*) begin
if (s_axi_awaddr == 32'hA000_1000) begin // 假设这是"内存A"
dest_x = 4'd3;
dest_y = 4'd3;
end else if (s_axi_awaddr == 32'hB000_1000) begin // 假设这是"内存B"
dest_x = 4'd1;
dest_y = 4'd2;
end else begin // 默认
dest_x = 4'd0;
dest_y = 4'd0;
end
end
// ---------------------------------------------
// 状态机 (时序逻辑)
// ---------------------------------------------
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= S_IDLE;
end else begin
state <= next_state;
end
end
// ---------------------------------------------
// 状态机 (组合逻辑:状态转移 + 输出)
// ---------------------------------------------
always @(*) begin
// --- 默认输出 (防止Latch) ---
next_state = state;
s_axi_awready = 1'b0;
s_axi_wready = 1'b0;
s_axi_bvalid = 1'b0;
m_noc_valid = 1'b0;
m_noc_flit = 0;
w_fifo_read_en = 1'b0;
// --- 主状态机 ---
case (state)
// 0. 等待AXI命令
S_IDLE: begin
s_axi_awready = 1'b1; // 总是准备好接收新地址
if (s_axi_awvalid) begin
// (我们在这里"锁存"了地址, 因为 'dest_x'/'dest_y' 是组合逻辑)
next_state = S_RECV_DATA; // 下一拍, 去收数据
data_counter = 0; // 计数器清零
end
end
// 1. 正在接收 WDATA (填满FIFO)
S_RECV_DATA: begin
// "我准备好接收" = "我的数据FIFO还没满"
s_axi_wready = !w_fifo_full;
if (s_axi_wvalid && s_axi_wready) begin
data_counter = data_counter + 1;
if (s_axi_wlast) begin // CPU说"这是最后一个"
if (data_counter == DATA_BEATS-1) begin // 且数量也对
next_state = S_INJECT_HEAD; // 数据收齐, 开始发包
end else begin
// 错误!CPU发的长度和我们约定的(4)不符
next_state = S_IDLE; // (真实NI会报一个Error)
end
end
end
end
// 3. 注入 "Head Flit"
S_INJECT_HEAD: begin
m_noc_valid = 1'b1;
m_noc_flit[DEST_X_MSB:DEST_X_LSB] = dest_x; // 用上锁存的地址
m_noc_flit[DEST_Y_MSB:DEST_Y_LSB] = dest_y;
// m_noc_flit[... 'type' ...] = HEAD; (简化, 暂不实现)
// 握手:等"下游" (Router) 准备好
if (m_noc_ready) begin
next_state = S_INJECT_BODY;
data_counter = 0; // 重置计数器, 用于"发送"
end
end
// 4. 注入 "Body Flits" (从FIFO读)
S_INJECT_BODY: begin
// "我的Flit有效" = "我的FIFO里有货"
m_noc_valid = !w_fifo_empty;
m_noc_flit = w_fifo_data; // Flit = FIFO的输出
// "读FIFO" = "Flit有效" AND "下游Router准备好了"
w_fifo_read_en = m_noc_valid && m_noc_ready;
if (w_fifo_read_en) begin // 成功发送一个Body Flit
data_counter = data_counter + 1;
if (data_counter == DATA_BEATS - 1) begin // 发完了?
next_state = S_INJECT_TAIL;
end
end
end
// 5. 注入 "Tail Flit"
S_INJECT_TAIL: begin
m_noc_valid = 1'b1;
// m_noc_flit[... 'type' ...] = TAIL; (简化)
if (m_noc_ready) begin
// (在我们的MVP中, 突发结束, 立即回复OK)
next_state = S_SEND_BRESP;
end
end
// 6. (简化) 回复AXI "OK"
S_SEND_BRESP: begin
s_axi_bvalid = 1'b1; // "NI: 写入成功!"
if (s_axi_bready) begin // CPU 确认收到?
next_state = S_IDLE; // 完成闭环, 返回空闲
end
end
default: begin
next_state = S_IDLE;
end
endcase
end
endmodule
🔍 代码解析与“关键点”
-
“解耦” (Decoupling) 是灵魂
- 我们在NI内部放了一个
w_data_buffer(FIFO)。这是至关重要的。 - 为什么? AXI总线 (
s_axi_wvalid) 和 NoC (m_noc_ready) 的“吞吐能力”是独立的。 - 比如,CPU可能一口气(连续4个周期)把4个
WDATA全发过来,而我们的NoC可能在INJECT_HEAD时被“下游”的router“反压”(m_noc_ready=0)卡住了。 - 如果没有这个FIFO,
WDATA就会丢失。 - 这个FIFO起到了“削峰填谷”的**“解耦”作用:它让我们能“快速”接收CPU的数据(
s_axi_wready = !w_fifo_full),然后再“慢慢地”**按照NoC的节奏发送出去。
- 我们在NI内部放了一个
-
双重反压 (Dual Backpressure)
s_axi_wready:它向“上游”的CPU提供了反压。m_noc_valid:它向“下游”的Router提供了数据。- 在
S_INJECT_BODY状态,w_fifo_read_en = m_noc_valid && m_noc_ready这行代码,完美地把“下游NoC的反压”(m_noc_ready) 传递 给了“上游AXI总线”(s_axi_wready)(因为如果NoC卡住,FIFO会变满,s_axi_wready会变0)。
恭喜!你造出了“物流中心”
虽然这是一个“简化版”的NI,但它已经100%抓住了NI设计的核心精髓:状态机、协议翻译、地址解码、数据解耦。
你现在拥有了:
router.v:智能十字路口。master_ni.v:CPU侧的“物流中心”。
我们还缺“拼图”的最后一块。
我们有了“发货方” (master_ni),我们还需要一个“收货方”。
624

被折叠的 条评论
为什么被折叠?



