mirror of
https://github.com/retroSoC/retroSoC.git
synced 2026-11-04 22:32:36 +00:00
feat: add RTL tests for NPU vector, memory, and patch packer
- Introduced for comprehensive testing of the NPU vector module, covering bias preload, lane-masked accumulation, and fault conditions. - Created to implement differential tests for NPU compute-core RTL using Icarus Verilog and Verilator, ensuring bit-exact results against a numerical oracle. - Added to validate NPU memory and patch packer functionality, including SRAM models and various packing scenarios.
This commit is contained in:
19 files changed
+4595
-1
No files matched your search
@@ -0,0 +1,18 @@
|
||||
# NPU-P1 isolated core STA constraints: single 72 MHz HP clock at the
|
||||
# 1000000/72 ps target period, mirroring generate_sdc.py conventions.
|
||||
# NOTE: the locked OpenSTA build has no remove_from_collection/filter_collection,
|
||||
# so the input-delay exclusion of clock/reset ports is done by name iteration.
|
||||
create_clock -name clk_i -period 13.888889 [get_ports clk_i]
|
||||
set_clock_uncertainty -setup 0.2 [get_clocks clk_i]
|
||||
set_clock_uncertainty -hold 0.1 [get_clocks clk_i]
|
||||
set_clock_transition 0.1 [get_clocks clk_i]
|
||||
set_false_path -from [get_ports rst_n_i]
|
||||
set npu_io_inputs [list]
|
||||
foreach npu_port [all_inputs] {
|
||||
set npu_port_name [get_property $npu_port name]
|
||||
if {$npu_port_name ne "clk_i" && $npu_port_name ne "rst_n_i"} {
|
||||
lappend npu_io_inputs $npu_port
|
||||
}
|
||||
}
|
||||
set_input_delay -clock clk_i 2.0 $npu_io_inputs
|
||||
set_output_delay -clock clk_i 2.0 [all_outputs]
|
||||
@@ -0,0 +1,54 @@
|
||||
# NPU-P1 isolated OpenSTA run: parameterized top with path-based metrics.
|
||||
# The locked OpenSTA 3.0.0 build reports no rows from report_checks/report_wns
|
||||
# under this constraint set, so WNS/TNS are computed from find_timing_paths.
|
||||
foreach variable {OPENSTA_NETLIST OPENSTA_LIBERTY OPENSTA_LINK_LIBS OPENSTA_SDC OPENSTA_REPORT OPENSTA_METRICS OPENSTA_TOP} {
|
||||
if {![info exists ::env($variable)] || $::env($variable) eq ""} {
|
||||
error "required environment variable is missing: $variable"
|
||||
}
|
||||
}
|
||||
|
||||
foreach liberty $::env(OPENSTA_LINK_LIBS) {
|
||||
read_liberty $liberty
|
||||
}
|
||||
read_liberty $::env(OPENSTA_LIBERTY)
|
||||
if {[info exists ::env(OPENSTA_SRAM_LIBS)] && $::env(OPENSTA_SRAM_LIBS) ne ""} {
|
||||
foreach liberty $::env(OPENSTA_SRAM_LIBS) {
|
||||
read_liberty $liberty
|
||||
}
|
||||
}
|
||||
read_verilog $::env(OPENSTA_NETLIST)
|
||||
link_design $::env(OPENSTA_TOP)
|
||||
read_sdc $::env(OPENSTA_SDC)
|
||||
|
||||
proc npu_collect_paths {delay report} {
|
||||
set worst 999.0
|
||||
set total 0.0
|
||||
set count 0
|
||||
puts $report "path_delay $delay"
|
||||
foreach path [find_timing_paths -path_delay $delay -slack_max 999.0 \
|
||||
-group_path_count 100000 -endpoint_path_count 100000] {
|
||||
set slack [get_property $path slack]
|
||||
incr count
|
||||
if {$slack < $worst} { set worst $slack }
|
||||
if {$slack < 0.0} {
|
||||
set total [expr {$total + $slack}]
|
||||
puts $report "VIOLATED [get_property $path startpoint] -> [get_property $path endpoint] slack=$slack"
|
||||
}
|
||||
}
|
||||
if {$count == 0} { set worst 0.0 }
|
||||
return [list $worst $total $count]
|
||||
}
|
||||
|
||||
set report [open $::env(OPENSTA_REPORT) "w"]
|
||||
set setup [npu_collect_paths max $report]
|
||||
set hold [npu_collect_paths min $report]
|
||||
close $report
|
||||
|
||||
set metrics [open $::env(OPENSTA_METRICS) "w"]
|
||||
puts $metrics "wns_max=[lindex $setup 0]"
|
||||
puts $metrics "tns_max=[lindex $setup 1]"
|
||||
puts $metrics "paths_max=[lindex $setup 2]"
|
||||
puts $metrics "wns_min=[lindex $hold 0]"
|
||||
puts $metrics "tns_min=[lindex $hold 1]"
|
||||
puts $metrics "paths_min=[lindex $hold 2]"
|
||||
close $metrics
|
||||
@@ -0,0 +1,369 @@
|
||||
// Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
|
||||
// SPDX-License-Identifier: MulanPSL-2.0
|
||||
|
||||
// Two accumulator contexts, each 8x8 checked INT32 sums for numerical
|
||||
// profile 1. Acquiring a context preloads every sum from the per-channel bias
|
||||
// (sum[p][c] = bias[c]); the bias is added exactly once per context lifetime,
|
||||
// so multi-K-slice reductions keep accumulating rows without a new acquire.
|
||||
// Accepted rows are pipelined: stage A registers the products with a one-hot
|
||||
// context token, stage B performs the checked INT32 adds one cycle later, so
|
||||
// the interface sustains one row per cycle. Each context owns its add array;
|
||||
// there is no shared context-selected sum read. On the first overflowing
|
||||
// update (lowest sum index p*8+c wins, evaluated with a log-depth encoder)
|
||||
// the sticky fault and fault_sum_o are latched, the faulting sums retain
|
||||
// their stored values (no wrap, no saturation), and the context moves to
|
||||
// Fault. A drain request moves an Accumulate context to DrainPending; once
|
||||
// its in-flight row has landed in the sums and the single drain port is free,
|
||||
// the context moves to Draining and publishes the 64 sums exactly once in
|
||||
// position-major order (p = 0..7, c = 0..7 inside each position) through a
|
||||
// valid/ready stream. A Fault context never drains and only clear_i releases
|
||||
// it. clear_i synchronously returns every context to Idle, drops in-flight
|
||||
// rows, and clears the fault record; it also blocks start/row/drain
|
||||
// acceptance in the same cycle.
|
||||
module npu_accumulator (
|
||||
input logic clk_i,
|
||||
input logic rst_n_i,
|
||||
input logic clear_i,
|
||||
input logic start_valid_i,
|
||||
output logic start_ready_o,
|
||||
input logic [npu_pkg::ChannelLanes-1:0][31:0] start_bias_i,
|
||||
output logic start_context_o,
|
||||
input logic row_valid_i,
|
||||
output logic row_ready_o,
|
||||
input logic row_context_i,
|
||||
input logic [npu_pkg::SpatialLanes-1:0][npu_pkg::ChannelLanes-1:0][16:0] row_products_i,
|
||||
input logic drain_valid_i,
|
||||
input logic drain_context_i,
|
||||
output logic drain_data_valid_o,
|
||||
input logic drain_data_ready_i,
|
||||
output logic signed [31:0] drain_data_o,
|
||||
output logic drain_last_o,
|
||||
output logic busy_o,
|
||||
output logic fault_sticky_o,
|
||||
output logic [5:0] fault_sum_o
|
||||
);
|
||||
typedef enum logic [2:0] {
|
||||
Idle,
|
||||
Accumulate,
|
||||
DrainPending,
|
||||
Draining,
|
||||
Fault
|
||||
} context_state_e;
|
||||
|
||||
localparam logic [5:0] SumLastIndex = 6'(npu_pkg::AccSumCount - 1);
|
||||
|
||||
logic [npu_pkg::AccContextCount-1:0][2:0] s_context_state_d;
|
||||
logic [npu_pkg::AccContextCount-1:0][2:0] s_context_state_q;
|
||||
logic [npu_pkg::AccContextCount-1:0][npu_pkg::AccSumCount-1:0][npu_pkg::AccWidth-1:0] s_sum_q;
|
||||
// Accumulate stage A: accepted-row register with a one-hot context token.
|
||||
logic [npu_pkg::AccContextCount-1:0] s_rowa_valid_d;
|
||||
logic [npu_pkg::AccContextCount-1:0] s_rowa_valid_q;
|
||||
logic [npu_pkg::AccSumCount-1:0][npu_pkg::ProductWidth-1:0] s_rowa_products_q;
|
||||
// Accumulate stage B: per-context checked adds and overflow flags.
|
||||
logic [npu_pkg::AccContextCount-1:0][npu_pkg::AccSumCount-1:0][npu_pkg::AccWidth-1:0]
|
||||
s_rowb_updated;
|
||||
logic [npu_pkg::AccContextCount-1:0][npu_pkg::AccSumCount-1:0] s_rowb_overflow;
|
||||
logic [npu_pkg::AccContextCount-1:0] s_rowb_apply;
|
||||
logic [npu_pkg::AccContextCount-1:0] s_rowb_fault_ctx;
|
||||
logic [npu_pkg::AccContextCount-1:0][5:0] s_rowb_index_ctx;
|
||||
logic s_rowb_fault;
|
||||
logic [5:0] s_rowb_fault_index;
|
||||
logic [npu_pkg::AccContextCount-1:0] s_context_idle;
|
||||
logic [npu_pkg::AccContextCount-1:0] s_context_accumulating;
|
||||
logic [npu_pkg::AccContextCount-1:0] s_context_draining;
|
||||
logic [npu_pkg::AccContextCount-1:0] s_pending_ready;
|
||||
logic [npu_pkg::AccContextCount-1:0] s_drain_grant;
|
||||
logic s_prior_pending_ready;
|
||||
logic s_start_accept;
|
||||
logic s_row_accept;
|
||||
logic s_draining_any;
|
||||
logic s_drain_grant_any;
|
||||
logic s_drain_context_sel;
|
||||
logic s_drain_done;
|
||||
logic [5:0] s_drain_index_q;
|
||||
logic s_fault_sticky_q;
|
||||
logic [5:0] s_fault_sum_q;
|
||||
|
||||
function automatic logic signed [npu_pkg::AccWidth-1:0] sum_update(
|
||||
input logic signed [npu_pkg::AccWidth-1:0] sum_i,
|
||||
input logic signed [npu_pkg::ProductWidth-1:0] product_i);
|
||||
logic signed [npu_pkg::AccWidth-1:0] s_product_ext;
|
||||
begin
|
||||
s_product_ext = {
|
||||
{(npu_pkg::AccWidth - npu_pkg::ProductWidth) {product_i[npu_pkg::ProductWidth-1]}},
|
||||
product_i
|
||||
};
|
||||
return sum_i + s_product_ext;
|
||||
end
|
||||
endfunction
|
||||
|
||||
function automatic logic sum_overflows(input logic signed [npu_pkg::AccWidth-1:0] sum_i,
|
||||
input logic signed [npu_pkg::ProductWidth-1:0] product_i);
|
||||
logic signed [npu_pkg::AccWidth-1:0] s_product_ext;
|
||||
logic signed [npu_pkg::AccWidth-1:0] s_updated;
|
||||
begin
|
||||
s_product_ext = {
|
||||
{(npu_pkg::AccWidth - npu_pkg::ProductWidth) {product_i[npu_pkg::ProductWidth-1]}},
|
||||
product_i
|
||||
};
|
||||
s_updated = sum_i + s_product_ext;
|
||||
return (sum_i[npu_pkg::AccWidth-1] == s_product_ext[npu_pkg::AccWidth-1]) &&
|
||||
(s_updated[npu_pkg::AccWidth-1] != sum_i[npu_pkg::AccWidth-1]);
|
||||
end
|
||||
endfunction
|
||||
|
||||
// Log-depth lowest-set-index encoder over the 64 per-sum overflow flags:
|
||||
// each level takes the lower half whenever it contains a set bit.
|
||||
function automatic logic [5:0] lowest_set_index(input logic [63:0] flags_i);
|
||||
logic [31:0] s_w32;
|
||||
logic [15:0] s_w16;
|
||||
logic [ 7:0] s_w8;
|
||||
logic [ 3:0] s_w4;
|
||||
logic [ 1:0] s_w2;
|
||||
logic [ 5:0] s_index;
|
||||
begin
|
||||
s_index[5] = ~|flags_i[31:0];
|
||||
s_w32 = s_index[5] ? flags_i[63:32] : flags_i[31:0];
|
||||
s_index[4] = ~|s_w32[15:0];
|
||||
s_w16 = s_index[4] ? s_w32[31:16] : s_w32[15:0];
|
||||
s_index[3] = ~|s_w16[7:0];
|
||||
s_w8 = s_index[3] ? s_w16[15:8] : s_w16[7:0];
|
||||
s_index[2] = ~|s_w8[3:0];
|
||||
s_w4 = s_index[2] ? s_w8[7:4] : s_w8[3:0];
|
||||
s_index[1] = ~|s_w4[1:0];
|
||||
s_w2 = s_index[1] ? s_w4[3:2] : s_w4[1:0];
|
||||
s_index[0] = ~s_w2[0];
|
||||
return s_index;
|
||||
end
|
||||
endfunction
|
||||
|
||||
for (genvar ctx = 0; ctx < npu_pkg::AccContextCount; ctx++) begin : gen_decode
|
||||
assign s_context_idle[ctx] = s_context_state_q[ctx] == Idle;
|
||||
assign s_context_accumulating[ctx] = s_context_state_q[ctx] == Accumulate;
|
||||
assign s_context_draining[ctx] = s_context_state_q[ctx] == Draining;
|
||||
// clear_i suppresses the stage-B landing of an in-flight row.
|
||||
assign s_rowb_apply[ctx] = s_rowa_valid_q[ctx] && !clear_i;
|
||||
assign s_rowb_fault_ctx[ctx] = s_rowb_apply[ctx] && (|s_rowb_overflow[ctx]);
|
||||
assign s_rowb_index_ctx[ctx] = lowest_set_index(s_rowb_overflow[ctx]);
|
||||
// A pending drain may proceed only once the context has no in-flight row.
|
||||
assign s_pending_ready[ctx] = (s_context_state_q[ctx] == DrainPending) && !s_rowa_valid_q[ctx];
|
||||
end
|
||||
|
||||
assign busy_o = !(&s_context_idle);
|
||||
assign start_ready_o = (|s_context_idle) && !clear_i;
|
||||
// Stage B always retires a row in one cycle, so stage A never backpressures
|
||||
// and the interface sustains one row per cycle.
|
||||
assign row_ready_o = s_context_accumulating[row_context_i] && !clear_i;
|
||||
assign s_start_accept = start_valid_i && start_ready_o;
|
||||
assign s_row_accept = row_valid_i && row_ready_o;
|
||||
assign s_draining_any = |s_context_draining;
|
||||
assign s_drain_grant_any = |s_drain_grant;
|
||||
assign s_drain_done = s_draining_any && drain_data_ready_i && (s_drain_index_q == SumLastIndex);
|
||||
|
||||
assign drain_data_valid_o = s_draining_any;
|
||||
assign drain_last_o = s_draining_any && (s_drain_index_q == SumLastIndex);
|
||||
assign drain_data_o = s_sum_q[s_drain_context_sel][s_drain_index_q];
|
||||
|
||||
assign fault_sticky_o = s_fault_sticky_q;
|
||||
assign fault_sum_o = s_fault_sum_q;
|
||||
|
||||
// One-hot acceptance token for stage A.
|
||||
always_comb begin
|
||||
s_rowa_valid_d = '0;
|
||||
if (s_row_accept) begin
|
||||
s_rowa_valid_d[row_context_i] = 1'b1;
|
||||
end
|
||||
end
|
||||
|
||||
// Lowest Idle context index is acquired; the loop ends with the lowest hit.
|
||||
always_comb begin
|
||||
start_context_o = 1'b0;
|
||||
for (int ctx = int'(npu_pkg::AccContextCount) - 1; ctx >= 0; ctx--) begin
|
||||
if (s_context_idle[ctx]) begin
|
||||
start_context_o = 1'(ctx);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
// Pending drains are granted lowest-index first once their pipeline stage is
|
||||
// empty and the single drain port is free.
|
||||
always_comb begin
|
||||
s_prior_pending_ready = 1'b0;
|
||||
s_drain_grant = '0;
|
||||
for (int ctx = 0; ctx < int'(npu_pkg::AccContextCount); ctx++) begin
|
||||
s_drain_grant[ctx] = s_pending_ready[ctx] && !s_draining_any && !s_prior_pending_ready;
|
||||
s_prior_pending_ready = s_prior_pending_ready || s_pending_ready[ctx];
|
||||
end
|
||||
end
|
||||
|
||||
// Selects the draining context; at most one context can be Draining because
|
||||
// grants are blocked while a drain is in flight.
|
||||
always_comb begin
|
||||
s_drain_context_sel = 1'b0;
|
||||
for (int ctx = 0; ctx < int'(npu_pkg::AccContextCount); ctx++) begin
|
||||
if (s_context_draining[ctx]) begin
|
||||
s_drain_context_sel = 1'(ctx);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
// Stage-B combinational checked update: each context owns its add array, so
|
||||
// no context-selected read of the sum bank exists on this path.
|
||||
for (genvar ctx = 0; ctx < npu_pkg::AccContextCount; ctx++) begin : gen_stage_b
|
||||
for (genvar index = 0; index < npu_pkg::AccSumCount; index++) begin : gen_sum_update
|
||||
assign s_rowb_overflow[ctx][index] = sum_overflows(
|
||||
s_sum_q[ctx][index], s_rowa_products_q[index]
|
||||
);
|
||||
assign s_rowb_updated[ctx][index] = sum_update(s_sum_q[ctx][index], s_rowa_products_q[index]);
|
||||
end
|
||||
end
|
||||
|
||||
// Lowest context index wins the fault record; the loop ends with that hit.
|
||||
always_comb begin
|
||||
s_rowb_fault = 1'b0;
|
||||
s_rowb_fault_index = 6'd0;
|
||||
for (int ctx = int'(npu_pkg::AccContextCount) - 1; ctx >= 0; ctx--) begin
|
||||
if (s_rowb_fault_ctx[ctx]) begin
|
||||
s_rowb_fault = 1'b1;
|
||||
s_rowb_fault_index = s_rowb_index_ctx[ctx];
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
always_comb begin
|
||||
s_context_state_d = s_context_state_q;
|
||||
for (int ctx = 0; ctx < int'(npu_pkg::AccContextCount); ctx++) begin
|
||||
unique case (s_context_state_q[ctx])
|
||||
Idle: begin
|
||||
if (s_start_accept && (start_context_o == 1'(ctx))) begin
|
||||
s_context_state_d[ctx] = Accumulate;
|
||||
end
|
||||
end
|
||||
Accumulate: begin
|
||||
if (s_rowb_fault_ctx[ctx]) begin
|
||||
s_context_state_d[ctx] = Fault;
|
||||
end else if (drain_valid_i && !clear_i && (drain_context_i == 1'(ctx))) begin
|
||||
s_context_state_d[ctx] = DrainPending;
|
||||
end
|
||||
end
|
||||
DrainPending: begin
|
||||
if (s_rowb_fault_ctx[ctx]) begin
|
||||
s_context_state_d[ctx] = Fault;
|
||||
end else if (s_drain_grant[ctx]) begin
|
||||
s_context_state_d[ctx] = Draining;
|
||||
end
|
||||
end
|
||||
Draining: begin
|
||||
if (s_drain_done) begin
|
||||
s_context_state_d[ctx] = Idle;
|
||||
end
|
||||
end
|
||||
Fault: begin
|
||||
end
|
||||
default: begin
|
||||
s_context_state_d[ctx] = Idle;
|
||||
end
|
||||
endcase
|
||||
end
|
||||
if (clear_i) begin
|
||||
s_context_state_d = '0;
|
||||
end
|
||||
end
|
||||
|
||||
always_ff @(posedge clk_i or negedge rst_n_i) begin
|
||||
if (!rst_n_i) begin
|
||||
s_context_state_q <= '0;
|
||||
end else begin
|
||||
s_context_state_q <= s_context_state_d;
|
||||
end
|
||||
end
|
||||
|
||||
// Stage-A row register. The products register is fully overwritten on every
|
||||
// acceptance, so only the one-hot token needs reset.
|
||||
always_ff @(posedge clk_i or negedge rst_n_i) begin
|
||||
if (!rst_n_i) begin
|
||||
s_rowa_valid_q <= '0;
|
||||
end else if (clear_i) begin
|
||||
s_rowa_valid_q <= '0;
|
||||
end else begin
|
||||
s_rowa_valid_q <= s_rowa_valid_d;
|
||||
end
|
||||
end
|
||||
|
||||
always_ff @(posedge clk_i) begin
|
||||
if (s_row_accept) begin
|
||||
s_rowa_products_q <= row_products_i;
|
||||
end
|
||||
end
|
||||
|
||||
// On acquire every sum preloads from the per-channel bias; on a stage-B row
|
||||
// each non-overflowing sum takes the checked update while an overflowing sum
|
||||
// retains its stored value. Sums are not cleared by clear_i because the next
|
||||
// acquire fully preloads them before any read.
|
||||
for (genvar ctx = 0; ctx < npu_pkg::AccContextCount; ctx++) begin : gen_context
|
||||
localparam logic ContextIndex = 1'(ctx);
|
||||
for (genvar index = 0; index < npu_pkg::AccSumCount; index++) begin : gen_sum
|
||||
localparam int unsigned ChannelIndex = index % npu_pkg::ChannelLanes;
|
||||
|
||||
always_ff @(posedge clk_i or negedge rst_n_i) begin
|
||||
if (!rst_n_i) begin
|
||||
s_sum_q[ctx][index] <= 32'd0;
|
||||
end else if (s_start_accept && (start_context_o == ContextIndex)) begin
|
||||
s_sum_q[ctx][index] <= start_bias_i[ChannelIndex];
|
||||
end else if (s_rowb_apply[ctx] && !s_rowb_overflow[ctx][index]) begin
|
||||
s_sum_q[ctx][index] <= s_rowb_updated[ctx][index];
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
always_ff @(posedge clk_i or negedge rst_n_i) begin
|
||||
if (!rst_n_i) begin
|
||||
s_drain_index_q <= 6'd0;
|
||||
end else if (clear_i || s_drain_grant_any || s_drain_done) begin
|
||||
s_drain_index_q <= 6'd0;
|
||||
end else if (s_draining_any && drain_data_ready_i) begin
|
||||
s_drain_index_q <= s_drain_index_q + 6'd1;
|
||||
end
|
||||
end
|
||||
|
||||
// First fault is retained until clear_i; a later fault in the other context
|
||||
// does not overwrite the record.
|
||||
always_ff @(posedge clk_i or negedge rst_n_i) begin
|
||||
if (!rst_n_i) begin
|
||||
s_fault_sticky_q <= 1'b0;
|
||||
s_fault_sum_q <= 6'd0;
|
||||
end else if (clear_i) begin
|
||||
s_fault_sticky_q <= 1'b0;
|
||||
s_fault_sum_q <= 6'd0;
|
||||
end else if (s_rowb_fault && !s_fault_sticky_q) begin
|
||||
s_fault_sticky_q <= 1'b1;
|
||||
s_fault_sum_q <= s_rowb_fault_index;
|
||||
end
|
||||
end
|
||||
|
||||
`ifndef SV_ASSRT_DISABLE
|
||||
logic s_start_stall_q;
|
||||
logic s_row_stall_q;
|
||||
|
||||
always_ff @(posedge clk_i or negedge rst_n_i) begin
|
||||
if (!rst_n_i) begin
|
||||
s_start_stall_q <= 1'b0;
|
||||
s_row_stall_q <= 1'b0;
|
||||
end else begin
|
||||
s_start_stall_q <= start_valid_i && !start_ready_o;
|
||||
s_row_stall_q <= row_valid_i && !row_ready_o;
|
||||
if (row_valid_i) begin
|
||||
assert (int'(row_context_i) < int'(npu_pkg::AccContextCount));
|
||||
end
|
||||
if (s_start_stall_q) begin
|
||||
assert ($stable(start_bias_i));
|
||||
end
|
||||
if (s_row_stall_q) begin
|
||||
assert ($stable(row_context_i));
|
||||
assert ($stable(row_products_i));
|
||||
end
|
||||
end
|
||||
end
|
||||
`endif
|
||||
endmodule
|
||||
@@ -0,0 +1,449 @@
|
||||
// Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
|
||||
// SPDX-License-Identifier: MulanPSL-2.0
|
||||
|
||||
// NPU private storage: sixteen tc_sram_1024x32 banks (64 KiB) with per-word
|
||||
// validity metadata and half-ownership arbitration. Frozen partition map from
|
||||
// npu_pkg: raw gather banks 0-3 (two 8192-byte halves), packed A banks 4-7
|
||||
// (two 8192-byte halves), packed W banks 8-11 (two 8192-byte halves), output
|
||||
// staging banks 12-13 (two 4096-byte halves), descriptor/parameter banks
|
||||
// 14-15 (one 8192-byte region).
|
||||
//
|
||||
// Contract refinements relative to the phase-1 port sketch (same semantics):
|
||||
// - Half-local byte addresses are widened so they span a whole half: raw and
|
||||
// packed A/W addresses are [12:0] (8192-byte halves), the output-staging
|
||||
// address is [11:0] (4096-byte halves) and parameter addresses are [12:0].
|
||||
// - A parameter write port (param_write_*) completes the read-only parameter
|
||||
// region; without it no parameter content could ever become valid.
|
||||
//
|
||||
// Access semantics:
|
||||
// - Every port accepts one access per cycle while its *_valid_i is asserted.
|
||||
// All reads are synchronous with one-cycle latency; the returning bank is
|
||||
// selected by a selector registered at request time, so read data is never
|
||||
// valid in the request cycle.
|
||||
// - 64-bit A/W accesses drive the two banks of one half simultaneously with
|
||||
// addr[12:3] as the row; addr[2:0] must be zero (misaligned 64-bit access
|
||||
// is an error). 32-bit ports ignore addr[1:0] (word select). Out-of-range
|
||||
// addresses are impossible by construction: each address exactly spans its
|
||||
// region, so no range error exists.
|
||||
// - Each accepted write sets the per-32-bit-word valid bit of every word it
|
||||
// touches (any byte strobe asserted). clear_i clears all valid bits and the
|
||||
// sticky error: functional invalidation only, physical SRAM contents are
|
||||
// never bulk-reset. Reads of invalid words raise the sticky error; error
|
||||
// read data is don't-care.
|
||||
// - Port groups touch disjoint banks except raw_write vs raw_read (raw banks)
|
||||
// and pack_write vs a_read/w_read (A/W banks). Concurrent same-half access
|
||||
// is a scheduler programming error: the sticky error is raised and the read
|
||||
// port wins the bank, dropping the write. Different-half concurrent access
|
||||
// (read the active half, write the inactive half) is legal. The parameter
|
||||
// write and read ports arbitrate the same way (read wins) without flagging.
|
||||
// - access_err_port_o encodes the first failing port, sticky until clear_i:
|
||||
// 3'd0 raw_write conflict, 3'd1 raw_read, 3'd2 pack_write, 3'd3 a_read,
|
||||
// 3'd4 w_read, 3'd5 out_req, 3'd6 param_read; 3'd7 means no error latched.
|
||||
module npu_local_sram (
|
||||
input logic clk_i,
|
||||
input logic rst_n_i,
|
||||
input logic clear_i,
|
||||
// raw gather: gather/DMA write port + packer read port (banks 0-3)
|
||||
input logic raw_write_valid_i,
|
||||
input logic raw_write_half_i,
|
||||
input logic [12:0] raw_write_addr_i,
|
||||
input logic [31:0] raw_write_data_i,
|
||||
input logic [ 3:0] raw_write_strb_i,
|
||||
input logic raw_read_valid_i,
|
||||
input logic raw_read_half_i,
|
||||
input logic [12:0] raw_read_addr_i,
|
||||
output logic [31:0] raw_read_data_o,
|
||||
// packed A/W: packer 64-bit write port + compute 64-bit read ports
|
||||
input logic pack_write_valid_i,
|
||||
input logic pack_write_sel_w_i,
|
||||
input logic pack_write_half_i,
|
||||
input logic [12:0] pack_write_addr_i,
|
||||
input logic [63:0] pack_write_data_i,
|
||||
input logic [ 7:0] pack_write_strb_i,
|
||||
input logic a_read_valid_i,
|
||||
input logic a_read_half_i,
|
||||
input logic [12:0] a_read_addr_i,
|
||||
output logic [63:0] a_read_data_o,
|
||||
input logic w_read_valid_i,
|
||||
input logic w_read_half_i,
|
||||
input logic [12:0] w_read_addr_i,
|
||||
output logic [63:0] w_read_data_o,
|
||||
// output staging: single port (banks 12-13, halves of 4096 bytes)
|
||||
input logic out_req_valid_i,
|
||||
input logic out_req_write_i,
|
||||
input logic out_req_half_i,
|
||||
input logic [11:0] out_req_addr_i,
|
||||
input logic [31:0] out_req_data_i,
|
||||
input logic [ 3:0] out_req_strb_i,
|
||||
output logic [31:0] out_read_data_o,
|
||||
// parameter region: scheduler write port + compute read port (banks 14-15)
|
||||
input logic param_write_valid_i,
|
||||
input logic [12:0] param_write_addr_i,
|
||||
input logic [31:0] param_write_data_i,
|
||||
input logic [ 3:0] param_write_strb_i,
|
||||
input logic param_read_valid_i,
|
||||
input logic [12:0] param_read_addr_i,
|
||||
output logic [31:0] param_read_data_o,
|
||||
// validity/ownership errors
|
||||
output logic access_err_sticky_o,
|
||||
output logic [ 2:0] access_err_port_o
|
||||
);
|
||||
localparam int unsigned BankCount = npu_pkg::BankCount;
|
||||
localparam int unsigned BankWords = 2 ** npu_pkg::BankAddrWidth;
|
||||
localparam logic [2:0] ErrPortRawWrite = 3'd0;
|
||||
localparam logic [2:0] ErrPortRawRead = 3'd1;
|
||||
localparam logic [2:0] ErrPortPackWrite = 3'd2;
|
||||
localparam logic [2:0] ErrPortARead = 3'd3;
|
||||
localparam logic [2:0] ErrPortWRead = 3'd4;
|
||||
localparam logic [2:0] ErrPortOutReq = 3'd5;
|
||||
localparam logic [2:0] ErrPortParamRead = 3'd6;
|
||||
localparam logic [2:0] ErrPortNone = 3'd7;
|
||||
|
||||
logic [BankCount-1:0] s_bank_cs;
|
||||
logic [BankCount-1:0] s_bank_wren;
|
||||
logic [BankCount-1:0][ 9:0] s_bank_addr;
|
||||
logic [BankCount-1:0][ 31:0] s_bank_wdata;
|
||||
logic [BankCount-1:0][ 3:0] s_bank_wmask;
|
||||
logic [BankCount-1:0][ 31:0] s_bank_rdata;
|
||||
logic [BankCount-1:0][BankWords-1:0] s_valid_q;
|
||||
|
||||
logic [ 1:0] s_raw_write_bank;
|
||||
logic [ 9:0] s_raw_write_row;
|
||||
logic [ 1:0] s_raw_read_bank;
|
||||
logic [ 9:0] s_raw_read_row;
|
||||
logic s_raw_conflict;
|
||||
logic s_raw_write_en;
|
||||
logic [ 9:0] s_pack_write_row;
|
||||
logic [ 9:0] s_a_read_row;
|
||||
logic [ 9:0] s_w_read_row;
|
||||
logic [ 3:0] s_pack_a_bank_lo;
|
||||
logic [ 3:0] s_pack_w_bank_lo;
|
||||
logic [ 3:0] s_a_read_bank_lo;
|
||||
logic [ 3:0] s_w_read_bank_lo;
|
||||
logic s_pack_write_misaligned;
|
||||
logic s_a_read_misaligned;
|
||||
logic s_w_read_misaligned;
|
||||
logic s_a_conflict;
|
||||
logic s_w_conflict;
|
||||
logic s_pack_a_en;
|
||||
logic s_pack_w_en;
|
||||
logic [ 3:0] s_out_bank;
|
||||
logic [ 3:0] s_param_read_bank;
|
||||
logic [ 3:0] s_param_write_bank;
|
||||
|
||||
logic [ 1:0] s_raw_read_bank_q;
|
||||
logic s_a_read_half_q;
|
||||
logic s_w_read_half_q;
|
||||
logic s_out_read_half_q;
|
||||
logic s_param_read_bank_q;
|
||||
logic [ 3:0] s_a_read_bank_lo_q;
|
||||
logic [ 3:0] s_w_read_bank_lo_q;
|
||||
|
||||
logic s_err_raw_write;
|
||||
logic s_err_raw_read;
|
||||
logic s_err_pack_write;
|
||||
logic s_err_a_read;
|
||||
logic s_err_w_read;
|
||||
logic s_err_out;
|
||||
logic s_err_param;
|
||||
logic s_access_err;
|
||||
logic [ 2:0] s_access_err_port;
|
||||
logic s_access_err_sticky_q;
|
||||
logic [ 2:0] s_access_err_port_q;
|
||||
logic s_unused;
|
||||
|
||||
assign s_raw_write_bank = {raw_write_half_i, raw_write_addr_i[12]};
|
||||
assign s_raw_write_row = raw_write_addr_i[11:2];
|
||||
assign s_raw_read_bank = {raw_read_half_i, raw_read_addr_i[12]};
|
||||
assign s_raw_read_row = raw_read_addr_i[11:2];
|
||||
assign s_raw_conflict = raw_write_valid_i && raw_read_valid_i &&
|
||||
(raw_write_half_i == raw_read_half_i);
|
||||
assign s_raw_write_en = raw_write_valid_i && !s_raw_conflict;
|
||||
|
||||
assign s_pack_write_row = pack_write_addr_i[12:3];
|
||||
assign s_a_read_row = a_read_addr_i[12:3];
|
||||
assign s_w_read_row = w_read_addr_i[12:3];
|
||||
assign s_pack_a_bank_lo = 4'(npu_pkg::PackABankFirst) + {pack_write_half_i, 1'b0};
|
||||
assign s_pack_w_bank_lo = 4'(npu_pkg::PackWBankFirst) + {pack_write_half_i, 1'b0};
|
||||
assign s_a_read_bank_lo = 4'(npu_pkg::PackABankFirst) + {a_read_half_i, 1'b0};
|
||||
assign s_w_read_bank_lo = 4'(npu_pkg::PackWBankFirst) + {w_read_half_i, 1'b0};
|
||||
assign s_pack_write_misaligned = pack_write_addr_i[2:0] != 3'd0;
|
||||
assign s_a_read_misaligned = a_read_addr_i[2:0] != 3'd0;
|
||||
assign s_w_read_misaligned = w_read_addr_i[2:0] != 3'd0;
|
||||
assign s_a_conflict = pack_write_valid_i && !pack_write_sel_w_i && a_read_valid_i &&
|
||||
(pack_write_half_i == a_read_half_i);
|
||||
assign s_w_conflict = pack_write_valid_i && pack_write_sel_w_i && w_read_valid_i &&
|
||||
(pack_write_half_i == w_read_half_i);
|
||||
assign s_pack_a_en = pack_write_valid_i && !pack_write_sel_w_i && !s_a_conflict &&
|
||||
!s_pack_write_misaligned;
|
||||
assign s_pack_w_en = pack_write_valid_i && pack_write_sel_w_i && !s_w_conflict &&
|
||||
!s_pack_write_misaligned;
|
||||
|
||||
assign s_out_bank = 4'(npu_pkg::OutBankFirst) + {3'd0, out_req_half_i};
|
||||
assign s_param_read_bank = 4'(npu_pkg::ParamBankFirst) + {3'd0, param_read_addr_i[12]};
|
||||
assign s_param_write_bank = 4'(npu_pkg::ParamBankFirst) + {3'd0, param_write_addr_i[12]};
|
||||
|
||||
// Raw gather banks 0-3: the read port wins any bank-level clash; a same-half
|
||||
// concurrent write/read pair is a programming error that drops the write.
|
||||
for (genvar bank = 0; bank < 4; bank++) begin : gen_raw_bank
|
||||
localparam logic [1:0] BankIndex = 2'(bank);
|
||||
logic s_raw_read_sel;
|
||||
logic s_raw_write_sel;
|
||||
|
||||
assign s_raw_read_sel = raw_read_valid_i && (s_raw_read_bank == BankIndex);
|
||||
assign s_raw_write_sel = s_raw_write_en && (s_raw_write_bank == BankIndex);
|
||||
assign s_bank_cs[npu_pkg::RawBankFirst+bank] = s_raw_read_sel || s_raw_write_sel;
|
||||
assign s_bank_wren[npu_pkg::RawBankFirst+bank] = s_raw_write_sel && !s_raw_read_sel;
|
||||
assign s_bank_addr[npu_pkg::RawBankFirst+bank] =
|
||||
s_raw_read_sel ? s_raw_read_row : s_raw_write_row;
|
||||
assign s_bank_wdata[npu_pkg::RawBankFirst+bank] = raw_write_data_i;
|
||||
assign s_bank_wmask[npu_pkg::RawBankFirst+bank] = raw_write_strb_i;
|
||||
end
|
||||
|
||||
// Packed A banks 4-7 and packed W banks 8-11: two banks per half, low word
|
||||
// in the even bank, high word in the odd bank; compute reads win conflicts.
|
||||
for (genvar half = 0; half < 2; half++) begin : gen_pack_a_bank
|
||||
localparam int unsigned BankLow = npu_pkg::PackABankFirst + 2 * half;
|
||||
localparam logic HalfValue = 1'(half);
|
||||
logic s_a_read_sel;
|
||||
logic s_pack_a_sel;
|
||||
|
||||
assign s_a_read_sel = a_read_valid_i && (a_read_half_i == HalfValue);
|
||||
assign s_pack_a_sel = s_pack_a_en && (pack_write_half_i == HalfValue);
|
||||
assign s_bank_cs[BankLow] = s_a_read_sel || s_pack_a_sel;
|
||||
assign s_bank_wren[BankLow] = s_pack_a_sel && !s_a_read_sel;
|
||||
assign s_bank_addr[BankLow] = s_a_read_sel ? s_a_read_row : s_pack_write_row;
|
||||
assign s_bank_wdata[BankLow] = pack_write_data_i[31:0];
|
||||
assign s_bank_wmask[BankLow] = pack_write_strb_i[3:0];
|
||||
assign s_bank_cs[BankLow+1] = s_a_read_sel || s_pack_a_sel;
|
||||
assign s_bank_wren[BankLow+1] = s_pack_a_sel && !s_a_read_sel;
|
||||
assign s_bank_addr[BankLow+1] = s_a_read_sel ? s_a_read_row : s_pack_write_row;
|
||||
assign s_bank_wdata[BankLow+1] = pack_write_data_i[63:32];
|
||||
assign s_bank_wmask[BankLow+1] = pack_write_strb_i[7:4];
|
||||
end
|
||||
|
||||
for (genvar half = 0; half < 2; half++) begin : gen_pack_w_bank
|
||||
localparam int unsigned BankLow = npu_pkg::PackWBankFirst + 2 * half;
|
||||
localparam logic HalfValue = 1'(half);
|
||||
logic s_w_read_sel;
|
||||
logic s_pack_w_sel;
|
||||
|
||||
assign s_w_read_sel = w_read_valid_i && (w_read_half_i == HalfValue);
|
||||
assign s_pack_w_sel = s_pack_w_en && (pack_write_half_i == HalfValue);
|
||||
assign s_bank_cs[BankLow] = s_w_read_sel || s_pack_w_sel;
|
||||
assign s_bank_wren[BankLow] = s_pack_w_sel && !s_w_read_sel;
|
||||
assign s_bank_addr[BankLow] = s_w_read_sel ? s_w_read_row : s_pack_write_row;
|
||||
assign s_bank_wdata[BankLow] = pack_write_data_i[31:0];
|
||||
assign s_bank_wmask[BankLow] = pack_write_strb_i[3:0];
|
||||
assign s_bank_cs[BankLow+1] = s_w_read_sel || s_pack_w_sel;
|
||||
assign s_bank_wren[BankLow+1] = s_pack_w_sel && !s_w_read_sel;
|
||||
assign s_bank_addr[BankLow+1] = s_w_read_sel ? s_w_read_row : s_pack_write_row;
|
||||
assign s_bank_wdata[BankLow+1] = pack_write_data_i[63:32];
|
||||
assign s_bank_wmask[BankLow+1] = pack_write_strb_i[7:4];
|
||||
end
|
||||
|
||||
// Output staging banks 12-13: one request port, one bank per half.
|
||||
for (genvar half = 0; half < 2; half++) begin : gen_out_bank
|
||||
localparam int unsigned BankIndex = npu_pkg::OutBankFirst + half;
|
||||
localparam logic HalfValue = 1'(half);
|
||||
logic s_out_sel;
|
||||
|
||||
assign s_out_sel = out_req_valid_i && (out_req_half_i == HalfValue);
|
||||
assign s_bank_cs[BankIndex] = s_out_sel;
|
||||
assign s_bank_wren[BankIndex] = s_out_sel && out_req_write_i;
|
||||
assign s_bank_addr[BankIndex] = out_req_addr_i[11:2];
|
||||
assign s_bank_wdata[BankIndex] = out_req_data_i;
|
||||
assign s_bank_wmask[BankIndex] = out_req_strb_i;
|
||||
end
|
||||
|
||||
// Parameter banks 14-15: independent write and read ports; the read port
|
||||
// wins a same-bank clash without flagging an error.
|
||||
for (genvar bank = 0; bank < 2; bank++) begin : gen_param_bank
|
||||
localparam int unsigned BankIndex = npu_pkg::ParamBankFirst + bank;
|
||||
localparam logic BankValue = 1'(bank);
|
||||
logic s_param_read_sel;
|
||||
logic s_param_write_sel;
|
||||
|
||||
assign s_param_read_sel = param_read_valid_i && (param_read_addr_i[12] == BankValue);
|
||||
assign s_param_write_sel = param_write_valid_i && (param_write_addr_i[12] == BankValue);
|
||||
assign s_bank_cs[BankIndex] = s_param_read_sel || s_param_write_sel;
|
||||
assign s_bank_wren[BankIndex] = s_param_write_sel && !s_param_read_sel;
|
||||
assign s_bank_addr[BankIndex] =
|
||||
s_param_read_sel ? param_read_addr_i[11:2] : param_write_addr_i[11:2];
|
||||
assign s_bank_wdata[BankIndex] = param_write_data_i;
|
||||
assign s_bank_wmask[BankIndex] = param_write_strb_i;
|
||||
end
|
||||
|
||||
`ifdef HAVE_SRAM_MACRO
|
||||
for (genvar bank = 0; bank < BankCount; bank++) begin : gen_local_bank
|
||||
tc_sram_1024x32 u_local_sram (
|
||||
.clk_i (clk_i),
|
||||
.cs_i (s_bank_cs[bank]),
|
||||
.addr_i(s_bank_addr[bank]),
|
||||
.data_i(s_bank_wdata[bank]),
|
||||
.mask_i(s_bank_wmask[bank]),
|
||||
.wren_i(s_bank_wren[bank]),
|
||||
.data_o(s_bank_rdata[bank])
|
||||
);
|
||||
end
|
||||
`else
|
||||
// Inferred behavioral banks for portable unit simulations (compile with
|
||||
// +define+PDK_BEHAV), mirroring the tc_sram_1024x32 wrapper: synchronous
|
||||
// read, byte write masks, no reset.
|
||||
for (genvar bank = 0; bank < BankCount; bank++) begin : gen_local_bank
|
||||
logic [31:0] s_storage_q[0:BankWords-1];
|
||||
logic [31:0] s_data_q;
|
||||
|
||||
assign s_bank_rdata[bank] = s_data_q;
|
||||
always_ff @(posedge clk_i) begin
|
||||
if (s_bank_cs[bank]) begin
|
||||
if (!s_bank_wren[bank]) begin
|
||||
s_data_q <= s_storage_q[s_bank_addr[bank]];
|
||||
end else begin
|
||||
if (s_bank_wmask[bank][0]) begin
|
||||
s_storage_q[s_bank_addr[bank]][7:0] <= s_bank_wdata[bank][7:0];
|
||||
end
|
||||
if (s_bank_wmask[bank][1]) begin
|
||||
s_storage_q[s_bank_addr[bank]][15:8] <= s_bank_wdata[bank][15:8];
|
||||
end
|
||||
if (s_bank_wmask[bank][2]) begin
|
||||
s_storage_q[s_bank_addr[bank]][23:16] <= s_bank_wdata[bank][23:16];
|
||||
end
|
||||
if (s_bank_wmask[bank][3]) begin
|
||||
s_storage_q[s_bank_addr[bank]][31:24] <= s_bank_wdata[bank][31:24];
|
||||
end
|
||||
s_data_q <= 32'bx;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
`endif
|
||||
|
||||
// Per-word validity metadata: set by accepted writes, functionally cleared
|
||||
// by clear_i (or reset); physical SRAM contents are never bulk-reset.
|
||||
always_ff @(posedge clk_i or negedge rst_n_i) begin
|
||||
if (!rst_n_i) begin
|
||||
s_valid_q <= '0;
|
||||
end else if (clear_i) begin
|
||||
s_valid_q <= '0;
|
||||
end else begin
|
||||
if (s_raw_write_en && (raw_write_strb_i != 4'd0)) begin
|
||||
s_valid_q[s_raw_write_bank][s_raw_write_row] <= 1'b1;
|
||||
end
|
||||
if (s_pack_a_en) begin
|
||||
if (pack_write_strb_i[3:0] != 4'd0) begin
|
||||
s_valid_q[s_pack_a_bank_lo][s_pack_write_row] <= 1'b1;
|
||||
end
|
||||
if (pack_write_strb_i[7:4] != 4'd0) begin
|
||||
s_valid_q[s_pack_a_bank_lo+4'd1][s_pack_write_row] <= 1'b1;
|
||||
end
|
||||
end
|
||||
if (s_pack_w_en) begin
|
||||
if (pack_write_strb_i[3:0] != 4'd0) begin
|
||||
s_valid_q[s_pack_w_bank_lo][s_pack_write_row] <= 1'b1;
|
||||
end
|
||||
if (pack_write_strb_i[7:4] != 4'd0) begin
|
||||
s_valid_q[s_pack_w_bank_lo+4'd1][s_pack_write_row] <= 1'b1;
|
||||
end
|
||||
end
|
||||
if (out_req_valid_i && out_req_write_i && (out_req_strb_i != 4'd0)) begin
|
||||
s_valid_q[s_out_bank][out_req_addr_i[11:2]] <= 1'b1;
|
||||
end
|
||||
if (param_write_valid_i && (param_write_strb_i != 4'd0) &&
|
||||
!(param_read_valid_i && (param_read_addr_i[12] == param_write_addr_i[12]))) begin
|
||||
s_valid_q[s_param_write_bank][param_write_addr_i[11:2]] <= 1'b1;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
// Registered read-return selectors (one-cycle synchronous read latency).
|
||||
always_ff @(posedge clk_i or negedge rst_n_i) begin
|
||||
if (!rst_n_i) begin
|
||||
s_raw_read_bank_q <= 2'd0;
|
||||
s_a_read_half_q <= 1'b0;
|
||||
s_w_read_half_q <= 1'b0;
|
||||
s_out_read_half_q <= 1'b0;
|
||||
s_param_read_bank_q <= 1'b0;
|
||||
end else begin
|
||||
if (raw_read_valid_i) begin
|
||||
s_raw_read_bank_q <= s_raw_read_bank;
|
||||
end
|
||||
if (a_read_valid_i) begin
|
||||
s_a_read_half_q <= a_read_half_i;
|
||||
end
|
||||
if (w_read_valid_i) begin
|
||||
s_w_read_half_q <= w_read_half_i;
|
||||
end
|
||||
if (out_req_valid_i && !out_req_write_i) begin
|
||||
s_out_read_half_q <= out_req_half_i;
|
||||
end
|
||||
if (param_read_valid_i) begin
|
||||
s_param_read_bank_q <= param_read_addr_i[12];
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
assign s_a_read_bank_lo_q = 4'(npu_pkg::PackABankFirst) + {s_a_read_half_q, 1'b0};
|
||||
assign s_w_read_bank_lo_q = 4'(npu_pkg::PackWBankFirst) + {s_w_read_half_q, 1'b0};
|
||||
assign raw_read_data_o = s_bank_rdata[{2'b00, s_raw_read_bank_q}];
|
||||
assign a_read_data_o = {s_bank_rdata[s_a_read_bank_lo_q+4'd1], s_bank_rdata[s_a_read_bank_lo_q]};
|
||||
assign w_read_data_o = {s_bank_rdata[s_w_read_bank_lo_q+4'd1], s_bank_rdata[s_w_read_bank_lo_q]};
|
||||
assign out_read_data_o = s_bank_rdata[4'(npu_pkg::OutBankFirst)+{3'd0, s_out_read_half_q}];
|
||||
assign param_read_data_o = s_bank_rdata[4'(npu_pkg::ParamBankFirst)+{3'd0, s_param_read_bank_q}];
|
||||
|
||||
assign s_err_raw_write = s_raw_conflict;
|
||||
assign s_err_raw_read = raw_read_valid_i && !s_valid_q[s_raw_read_bank][s_raw_read_row];
|
||||
assign s_err_pack_write = pack_write_valid_i &&
|
||||
(s_pack_write_misaligned || s_a_conflict || s_w_conflict);
|
||||
assign s_err_a_read = a_read_valid_i &&
|
||||
(s_a_read_misaligned || !s_valid_q[s_a_read_bank_lo][s_a_read_row] ||
|
||||
!s_valid_q[s_a_read_bank_lo+4'd1][s_a_read_row]);
|
||||
assign s_err_w_read = w_read_valid_i &&
|
||||
(s_w_read_misaligned || !s_valid_q[s_w_read_bank_lo][s_w_read_row] ||
|
||||
!s_valid_q[s_w_read_bank_lo+4'd1][s_w_read_row]);
|
||||
assign s_err_out = out_req_valid_i && !out_req_write_i &&
|
||||
!s_valid_q[s_out_bank][out_req_addr_i[11:2]];
|
||||
assign s_err_param = param_read_valid_i && !s_valid_q[s_param_read_bank][param_read_addr_i[11:2]];
|
||||
|
||||
// First-failing-port priority encode; lowest port code wins within a cycle.
|
||||
always_comb begin
|
||||
s_access_err = 1'b1;
|
||||
if (s_err_raw_write) begin
|
||||
s_access_err_port = ErrPortRawWrite;
|
||||
end else if (s_err_raw_read) begin
|
||||
s_access_err_port = ErrPortRawRead;
|
||||
end else if (s_err_pack_write) begin
|
||||
s_access_err_port = ErrPortPackWrite;
|
||||
end else if (s_err_a_read) begin
|
||||
s_access_err_port = ErrPortARead;
|
||||
end else if (s_err_w_read) begin
|
||||
s_access_err_port = ErrPortWRead;
|
||||
end else if (s_err_out) begin
|
||||
s_access_err_port = ErrPortOutReq;
|
||||
end else if (s_err_param) begin
|
||||
s_access_err_port = ErrPortParamRead;
|
||||
end else begin
|
||||
s_access_err = 1'b0;
|
||||
s_access_err_port = ErrPortNone;
|
||||
end
|
||||
end
|
||||
|
||||
always_ff @(posedge clk_i or negedge rst_n_i) begin
|
||||
if (!rst_n_i) begin
|
||||
s_access_err_sticky_q <= 1'b0;
|
||||
s_access_err_port_q <= ErrPortNone;
|
||||
end else if (clear_i) begin
|
||||
s_access_err_sticky_q <= 1'b0;
|
||||
s_access_err_port_q <= ErrPortNone;
|
||||
end else if (s_access_err && !s_access_err_sticky_q) begin
|
||||
s_access_err_sticky_q <= 1'b1;
|
||||
s_access_err_port_q <= s_access_err_port;
|
||||
end
|
||||
end
|
||||
|
||||
assign access_err_sticky_o = s_access_err_sticky_q;
|
||||
assign access_err_port_o = s_access_err_port_q;
|
||||
|
||||
assign s_unused = ^{raw_write_addr_i[1:0], raw_read_addr_i[1:0], out_req_addr_i[1:0],
|
||||
param_write_addr_i[1:0], param_read_addr_i[1:0]} && 1'b0;
|
||||
endmodule
|
||||
@@ -0,0 +1,106 @@
|
||||
// Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
|
||||
// SPDX-License-Identifier: MulanPSL-2.0
|
||||
|
||||
// Dense numerical-profile-1 MAC row: 8 output positions x 8 output channels.
|
||||
// One request presents eight raw INT8 activation bytes, eight raw INT8 weight
|
||||
// bytes, the signed input zero point, and per-lane valid masks. An accepted
|
||||
// request centers each activation to signed nine bits,
|
||||
// a9 = sign_extend(byte) - sign_extend(zero point), range -255..255, and
|
||||
// registers the 64 signed 9x8 products one cycle later. Masked lanes produce
|
||||
// exactly zero and never raise arithmetic faults. Weight bytes are consumed
|
||||
// directly as signed INT8 (zero point 0). The single registered output stage
|
||||
// applies backpressure with req_ready_o = resp_ready_i || !resp_valid_o.
|
||||
module npu_mac_array (
|
||||
input logic clk_i,
|
||||
input logic rst_n_i,
|
||||
input logic req_valid_i,
|
||||
output logic req_ready_o,
|
||||
input logic [npu_pkg::SpatialLanes-1:0][ 7:0] req_a_bytes_i,
|
||||
input logic [npu_pkg::ChannelLanes-1:0][ 7:0] req_w_bytes_i,
|
||||
input logic signed [ 7:0] req_in_zero_i,
|
||||
input logic [npu_pkg::SpatialLanes-1:0] req_a_valid_i,
|
||||
input logic [npu_pkg::ChannelLanes-1:0] req_w_valid_i,
|
||||
output logic resp_valid_o,
|
||||
input logic resp_ready_i,
|
||||
output logic [npu_pkg::SpatialLanes-1:0][npu_pkg::ChannelLanes-1:0][16:0] resp_products_o
|
||||
);
|
||||
logic [npu_pkg::SpatialLanes-1:0][npu_pkg::ChannelLanes-1:0][npu_pkg::ProductWidth-1:0]
|
||||
s_products_d;
|
||||
logic [npu_pkg::SpatialLanes-1:0][npu_pkg::ChannelLanes-1:0][npu_pkg::ProductWidth-1:0]
|
||||
s_products_q;
|
||||
logic s_resp_valid_q;
|
||||
|
||||
function automatic logic signed [npu_pkg::ProductWidth-1:0] lane_product(
|
||||
input logic [7:0] a_byte_i, input logic [7:0] w_byte_i, input logic signed [7:0] in_zero_i,
|
||||
input logic lanes_valid_i);
|
||||
logic signed [npu_pkg::CenteredWidth-1:0] s_a_ext;
|
||||
logic signed [npu_pkg::CenteredWidth-1:0] s_zero_ext;
|
||||
logic signed [npu_pkg::CenteredWidth-1:0] s_centered;
|
||||
logic signed [ npu_pkg::ProductWidth-1:0] s_product;
|
||||
begin
|
||||
s_a_ext = {a_byte_i[7], a_byte_i};
|
||||
s_zero_ext = {in_zero_i[7], in_zero_i};
|
||||
s_centered = s_a_ext - s_zero_ext;
|
||||
s_product = s_centered * $signed(w_byte_i);
|
||||
return lanes_valid_i ? s_product : '0;
|
||||
end
|
||||
endfunction
|
||||
|
||||
for (genvar position = 0; position < npu_pkg::SpatialLanes; position++) begin : gen_position
|
||||
for (genvar channel = 0; channel < npu_pkg::ChannelLanes; channel++) begin : gen_channel
|
||||
assign s_products_d[position][channel] = lane_product(
|
||||
req_a_bytes_i[position],
|
||||
req_w_bytes_i[channel],
|
||||
req_in_zero_i,
|
||||
req_a_valid_i[position] && req_w_valid_i[channel]
|
||||
);
|
||||
end
|
||||
end
|
||||
|
||||
assign req_ready_o = resp_ready_i || !s_resp_valid_q;
|
||||
assign resp_valid_o = s_resp_valid_q;
|
||||
assign resp_products_o = s_products_q;
|
||||
|
||||
// The product register is fully overwritten on every acceptance, so only the
|
||||
// handshake state needs reset.
|
||||
always_ff @(posedge clk_i) begin
|
||||
if (req_valid_i && req_ready_o) begin
|
||||
s_products_q <= s_products_d;
|
||||
end
|
||||
end
|
||||
|
||||
always_ff @(posedge clk_i or negedge rst_n_i) begin
|
||||
if (!rst_n_i) begin
|
||||
s_resp_valid_q <= 1'b0;
|
||||
end else if (req_valid_i && req_ready_o) begin
|
||||
s_resp_valid_q <= 1'b1;
|
||||
end else if (resp_ready_i) begin
|
||||
s_resp_valid_q <= 1'b0;
|
||||
end
|
||||
end
|
||||
|
||||
`ifndef SV_ASSRT_DISABLE
|
||||
logic s_req_stall_q;
|
||||
logic s_resp_stall_q;
|
||||
|
||||
always_ff @(posedge clk_i or negedge rst_n_i) begin
|
||||
if (!rst_n_i) begin
|
||||
s_req_stall_q <= 1'b0;
|
||||
s_resp_stall_q <= 1'b0;
|
||||
end else begin
|
||||
s_req_stall_q <= req_valid_i && !req_ready_o;
|
||||
s_resp_stall_q <= resp_valid_o && !resp_ready_i;
|
||||
if (s_req_stall_q) begin
|
||||
assert ($stable(req_a_bytes_i));
|
||||
assert ($stable(req_w_bytes_i));
|
||||
assert ($stable(req_in_zero_i));
|
||||
assert ($stable(req_a_valid_i));
|
||||
assert ($stable(req_w_valid_i));
|
||||
end
|
||||
if (s_resp_stall_q) begin
|
||||
assert ($stable(resp_products_o));
|
||||
end
|
||||
end
|
||||
end
|
||||
`endif
|
||||
endmodule
|
||||
@@ -0,0 +1,186 @@
|
||||
// Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
|
||||
// SPDX-License-Identifier: MulanPSL-2.0
|
||||
|
||||
// NPU patch packer: bounded NHWC gather transpose from a raw gather half into
|
||||
// packed A rows. Dense (PackDense): packed row k (byte address k*8) holds
|
||||
// byte lane p = raw_byte(p*K + k) for p < M and the input zero point above;
|
||||
// raw layout is [spatial_lane][k_in_slice]. Depthwise (PackDepthwise): packed
|
||||
// row (s*K + k) holds lane c = raw_byte((s*K + k)*8 + c) for c < channels and
|
||||
// the input zero point above; raw layout is [spatial_lane][kernel_k]
|
||||
// [channel_lane] and K <= 9. Contract refinement: the raw read and packed
|
||||
// write addresses are [12:0] so they span an 8192-byte half.
|
||||
//
|
||||
// Serial gather: one raw 32-bit read per produced byte, pipelined one byte
|
||||
// per cycle after the one-cycle synchronous-read fill. A packed 64-bit row
|
||||
// retires every (lane_count + 2) cycles, i.e. 0.8 bytes/cycle at M = 8.
|
||||
module npu_patch_packer (
|
||||
input logic clk_i,
|
||||
input logic rst_n_i,
|
||||
input logic start_valid_i,
|
||||
output logic start_ready_o,
|
||||
input npu_pkg::pack_mode_e start_mode_i,
|
||||
input logic [10:0] start_k_i, // dense 1..1024, depthwise 1..9
|
||||
input logic [ 3:0] start_positions_i, // dense: M output positions 1..8
|
||||
input logic [ 3:0] start_channels_i, // depthwise: valid channel lanes 1..8
|
||||
input logic signed [ 7:0] start_in_zero_i,
|
||||
output logic busy_o,
|
||||
output logic done_pulse_o,
|
||||
output logic raw_read_valid_o,
|
||||
output logic [12:0] raw_read_addr_o,
|
||||
input logic [31:0] raw_read_data_i,
|
||||
output logic pack_write_valid_o,
|
||||
output logic [12:0] pack_write_addr_o,
|
||||
output logic [63:0] pack_write_data_o,
|
||||
output logic [ 7:0] pack_write_strb_o
|
||||
);
|
||||
typedef enum logic [1:0] {
|
||||
Idle,
|
||||
Gather,
|
||||
Write,
|
||||
Done
|
||||
} state_e;
|
||||
|
||||
state_e s_state_d;
|
||||
state_e s_state_q;
|
||||
npu_pkg::pack_mode_e s_mode_q;
|
||||
logic [10:0] s_k_q;
|
||||
logic [ 3:0] s_positions_q;
|
||||
logic [ 3:0] s_channels_q;
|
||||
logic signed [ 7:0] s_in_zero_q;
|
||||
logic [10:0] s_row_cnt_q;
|
||||
logic [ 3:0] s_issue_lane_q;
|
||||
logic [ 3:0] s_capture_cnt_q;
|
||||
logic [ 2:0] s_capture_lane_q;
|
||||
logic [ 1:0] s_capture_sel_q;
|
||||
logic s_inflight_q;
|
||||
logic [12:0] s_lane_base_q;
|
||||
logic [63:0] s_row_q;
|
||||
logic [ 3:0] s_lane_count;
|
||||
logic [10:0] s_row_total;
|
||||
logic [12:0] s_issue_addr;
|
||||
logic s_issue_fire;
|
||||
logic s_last_capture;
|
||||
logic s_row_start;
|
||||
|
||||
// Valid lanes per row: M dense positions, channel lanes depthwise. Row
|
||||
// count: K dense rows, M*K depthwise rows (K <= 9, so the product fits).
|
||||
assign s_lane_count = (s_mode_q == npu_pkg::PackDense) ? s_positions_q : s_channels_q;
|
||||
assign s_row_total = (s_mode_q == npu_pkg::PackDense) ? s_k_q :
|
||||
11'(8'(s_positions_q) * 8'(s_k_q[3:0]));
|
||||
// Dense byte address lane*K + row (lane base accumulated per issued lane);
|
||||
// depthwise byte address row*8 + lane ([spatial][kernel_k][channel] order).
|
||||
assign s_issue_addr = (s_mode_q == npu_pkg::PackDense) ?
|
||||
s_lane_base_q + {2'b00, s_row_cnt_q} :
|
||||
{s_row_cnt_q[9:0], 3'b000} + {10'd0, s_issue_lane_q[2:0]};
|
||||
assign s_issue_fire = (s_state_q == Gather) && (s_issue_lane_q < s_lane_count);
|
||||
assign s_last_capture = (s_state_q == Gather) && s_inflight_q &&
|
||||
(s_capture_cnt_q == (s_lane_count - 4'd1));
|
||||
|
||||
always_comb begin
|
||||
s_state_d = s_state_q;
|
||||
s_row_start = 1'b0;
|
||||
unique case (s_state_q)
|
||||
Idle: begin
|
||||
if (start_valid_i) begin
|
||||
s_state_d = Gather;
|
||||
s_row_start = 1'b1;
|
||||
end
|
||||
end
|
||||
Gather: begin
|
||||
if (s_last_capture) begin
|
||||
s_state_d = Write;
|
||||
end
|
||||
end
|
||||
Write: begin
|
||||
if (s_row_cnt_q == (s_row_total - 11'd1)) begin
|
||||
s_state_d = Done;
|
||||
end else begin
|
||||
s_state_d = Gather;
|
||||
s_row_start = 1'b1;
|
||||
end
|
||||
end
|
||||
Done: begin
|
||||
s_state_d = Idle;
|
||||
end
|
||||
default: begin
|
||||
s_state_d = Idle;
|
||||
end
|
||||
endcase
|
||||
end
|
||||
|
||||
assign start_ready_o = (s_state_q == Idle);
|
||||
assign busy_o = (s_state_q != Idle);
|
||||
assign done_pulse_o = (s_state_q == Done);
|
||||
assign raw_read_valid_o = s_issue_fire;
|
||||
assign raw_read_addr_o = s_issue_addr;
|
||||
assign pack_write_valid_o = (s_state_q == Write);
|
||||
assign pack_write_addr_o = {s_row_cnt_q[9:0], 3'b000};
|
||||
assign pack_write_data_o = s_row_q;
|
||||
assign pack_write_strb_o = 8'hff;
|
||||
|
||||
always_ff @(posedge clk_i or negedge rst_n_i) begin
|
||||
if (!rst_n_i) begin
|
||||
s_state_q <= Idle;
|
||||
s_mode_q <= npu_pkg::PackDense;
|
||||
s_k_q <= 11'd1;
|
||||
s_positions_q <= 4'd1;
|
||||
s_channels_q <= 4'd1;
|
||||
s_in_zero_q <= 8'd0;
|
||||
s_row_cnt_q <= 11'd0;
|
||||
s_issue_lane_q <= 4'd0;
|
||||
s_capture_cnt_q <= 4'd0;
|
||||
s_capture_lane_q <= 3'd0;
|
||||
s_capture_sel_q <= 2'd0;
|
||||
s_inflight_q <= 1'b0;
|
||||
s_lane_base_q <= 13'd0;
|
||||
s_row_q <= 64'd0;
|
||||
end else begin
|
||||
s_state_q <= s_state_d;
|
||||
s_inflight_q <= s_issue_fire;
|
||||
if ((s_state_q == Idle) && start_valid_i) begin
|
||||
s_mode_q <= start_mode_i;
|
||||
s_k_q <= start_k_i;
|
||||
s_positions_q <= start_positions_i;
|
||||
s_channels_q <= start_channels_i;
|
||||
s_in_zero_q <= start_in_zero_i;
|
||||
s_row_cnt_q <= 11'd0;
|
||||
end
|
||||
if (s_row_start) begin
|
||||
s_issue_lane_q <= 4'd0;
|
||||
s_capture_cnt_q <= 4'd0;
|
||||
s_lane_base_q <= 13'd0;
|
||||
// Invalid lanes keep the input zero point; valid lanes are overwritten
|
||||
// by captures.
|
||||
s_row_q <= {8{(s_state_q == Idle) ? start_in_zero_i : s_in_zero_q}};
|
||||
end
|
||||
if (s_issue_fire) begin
|
||||
s_issue_lane_q <= s_issue_lane_q + 4'd1;
|
||||
s_capture_lane_q <= s_issue_lane_q[2:0];
|
||||
s_capture_sel_q <= s_issue_addr[1:0];
|
||||
if (s_mode_q == npu_pkg::PackDense) begin
|
||||
s_lane_base_q <= s_lane_base_q + {2'b00, s_k_q};
|
||||
end
|
||||
end
|
||||
if (s_inflight_q && (s_state_q == Gather)) begin
|
||||
s_row_q[s_capture_lane_q*8+:8] <= raw_read_data_i[s_capture_sel_q*8+:8];
|
||||
s_capture_cnt_q <= s_capture_cnt_q + 4'd1;
|
||||
end
|
||||
if ((s_state_q == Write) && (s_state_d == Gather)) begin
|
||||
s_row_cnt_q <= s_row_cnt_q + 11'd1;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
`ifndef SV_ASSRT_DISABLE
|
||||
always_ff @(posedge clk_i) begin
|
||||
if (rst_n_i && (s_state_q == Idle) && start_valid_i) begin
|
||||
assert ((start_k_i >= 11'd1) && (start_k_i <= 11'(npu_pkg::MaxKSlice)));
|
||||
assert ((start_positions_i >= 4'd1) && (start_positions_i <= 4'd8));
|
||||
assert ((start_channels_i >= 4'd1) && (start_channels_i <= 4'd8));
|
||||
if (start_mode_i == npu_pkg::PackDepthwise) begin
|
||||
assert (start_k_i <= 11'd9);
|
||||
end
|
||||
end
|
||||
end
|
||||
`endif
|
||||
endmodule
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
|
||||
// SPDX-License-Identifier: MulanPSL-2.0
|
||||
|
||||
package npu_pkg;
|
||||
// Frozen compute geometry: 8 output positions x 8 output channels dense,
|
||||
// eight depthwise lanes, one pipelined scalar requantizer.
|
||||
localparam int unsigned SpatialLanes = 8;
|
||||
localparam int unsigned ChannelLanes = 8;
|
||||
localparam int unsigned DenseMacCount = SpatialLanes * ChannelLanes;
|
||||
localparam int unsigned DepthwiseLanes = 8;
|
||||
localparam int unsigned MaxTilePositions = 8;
|
||||
localparam int unsigned MaxKSlice = 1024;
|
||||
// Sixteen 1024x32 banks = 64 KiB private storage, frozen partition map:
|
||||
// raw gather 0x0000..0x3fff, packed A 0x4000..0x7fff, packed W 0x8000..0xbfff,
|
||||
// output staging 0xc000..0xdfff, descriptor/parameter 0xe000..0xffff.
|
||||
localparam int unsigned BankCount = 16;
|
||||
localparam int unsigned BankAddrWidth = 10;
|
||||
localparam int unsigned BankBytes = 4096;
|
||||
localparam int unsigned LocalBytes = BankCount * BankBytes;
|
||||
localparam int unsigned RawBankFirst = 0;
|
||||
localparam int unsigned PackABankFirst = 4;
|
||||
localparam int unsigned PackWBankFirst = 8;
|
||||
localparam int unsigned OutBankFirst = 12;
|
||||
localparam int unsigned ParamBankFirst = 14;
|
||||
// Two accumulator contexts, each 8x8 checked INT32 sums.
|
||||
localparam int unsigned AccContextCount = 2;
|
||||
localparam int unsigned AccSumCount = DenseMacCount;
|
||||
// Numerical profile 1 widths: centered signed nine-bit activations,
|
||||
// signed eight-bit weights, signed 9x8 products, checked INT32 sums.
|
||||
localparam int unsigned CenteredWidth = 9;
|
||||
localparam int unsigned WeightWidth = 8;
|
||||
localparam int unsigned AccWidth = 32;
|
||||
localparam int unsigned ProductWidth = 17;
|
||||
|
||||
typedef logic signed [CenteredWidth-1:0] centered_t;
|
||||
typedef logic signed [WeightWidth-1:0] weight_t;
|
||||
typedef logic signed [AccWidth-1:0] acc_t;
|
||||
typedef logic signed [ProductWidth-1:0] product_t;
|
||||
|
||||
typedef enum logic [0:0] {
|
||||
PackDense,
|
||||
PackDepthwise
|
||||
} pack_mode_e;
|
||||
endpackage
|
||||
@@ -0,0 +1,287 @@
|
||||
// Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
|
||||
// SPDX-License-Identifier: MulanPSL-2.0
|
||||
|
||||
// Scalar pipelined numerical-profile-1 requantizer, bit-exact with
|
||||
// scripts/npu_reference.py. Each item applies, in this exact order: checked
|
||||
// left shift by max(shift,0) (an out-of-INT32 result flags the item, data
|
||||
// 8'sd0, never wrapped), SaturatingRoundingDoublingHighMul with the
|
||||
// multiplier (2^30 nudge for p >= 0, 1-2^30 for p < 0, division by 2^31
|
||||
// truncating toward zero, INT32_MIN-times-INT32_MIN saturation), then
|
||||
// RoundingDivideByPOT by max(-shift,0), then a widened add of the output zero
|
||||
// point and a clamp to [act_min, act_max]. This is the pinned double-rounding
|
||||
// profile; no single-rounding substitution is made. Five elastic valid/ready
|
||||
// register stages (shift, 32x32 multiply, nudge+truncating divide,
|
||||
// RoundingDivideByPOT, zero-point add+clamp) carry the per-item
|
||||
// configuration and fault flag; req_ready_o is high only when stage 0 is
|
||||
// free or shifting out.
|
||||
module npu_requantizer (
|
||||
input logic clk_i,
|
||||
input logic rst_n_i,
|
||||
input logic req_valid_i,
|
||||
output logic req_ready_o,
|
||||
input logic signed [31:0] req_acc_i,
|
||||
input logic [31:0] req_multiplier_i,
|
||||
input logic signed [ 7:0] req_shift_i,
|
||||
input logic signed [ 7:0] req_zout_i,
|
||||
input logic signed [ 7:0] req_act_min_i,
|
||||
input logic signed [ 7:0] req_act_max_i,
|
||||
output logic resp_valid_o,
|
||||
input logic resp_ready_i,
|
||||
output logic signed [ 7:0] resp_data_o,
|
||||
output logic resp_fault_o
|
||||
);
|
||||
// Stage 0 (checked left shift) registers and combinational results.
|
||||
logic s_shift_valid_q;
|
||||
logic signed [31:0] s_shift_data_q;
|
||||
logic s_shift_fault_q;
|
||||
logic [31:0] s_shift_multiplier_q;
|
||||
logic signed [ 7:0] s_shift_shift_q;
|
||||
logic signed [ 7:0] s_shift_zout_q;
|
||||
logic signed [ 7:0] s_shift_act_min_q;
|
||||
logic signed [ 7:0] s_shift_act_max_q;
|
||||
logic signed [31:0] s_shift_data_d;
|
||||
logic s_shift_fault_d;
|
||||
logic signed [63:0] s_shifted_wide;
|
||||
// Stage 1 (32x32 to 64-bit signed multiply) registers and result.
|
||||
logic s_mul_valid_q;
|
||||
logic signed [63:0] s_mul_product_q;
|
||||
logic s_mul_fault_q;
|
||||
logic signed [ 7:0] s_mul_shift_q;
|
||||
logic signed [ 7:0] s_mul_zout_q;
|
||||
logic signed [ 7:0] s_mul_act_min_q;
|
||||
logic signed [ 7:0] s_mul_act_max_q;
|
||||
logic signed [63:0] s_mul_product_d;
|
||||
// Stage 2 (nudge and truncating division by 2^31) registers and result.
|
||||
logic s_nud_valid_q;
|
||||
logic signed [31:0] s_nud_data_q;
|
||||
logic s_nud_fault_q;
|
||||
logic signed [ 7:0] s_nud_shift_q;
|
||||
logic signed [ 7:0] s_nud_zout_q;
|
||||
logic signed [ 7:0] s_nud_act_min_q;
|
||||
logic signed [ 7:0] s_nud_act_max_q;
|
||||
logic signed [31:0] s_nud_data_d;
|
||||
// Stage 3 (RoundingDivideByPOT) registers and result.
|
||||
logic s_rdp_valid_q;
|
||||
logic signed [31:0] s_rdp_data_q;
|
||||
logic s_rdp_fault_q;
|
||||
logic signed [ 7:0] s_rdp_zout_q;
|
||||
logic signed [ 7:0] s_rdp_act_min_q;
|
||||
logic signed [ 7:0] s_rdp_act_max_q;
|
||||
logic signed [31:0] s_rdp_data_d;
|
||||
logic signed [ 7:0] s_rdp_exponent;
|
||||
// Stage 4 (widened zero-point add and clamp) registers and result.
|
||||
logic s_out_valid_q;
|
||||
logic signed [ 7:0] s_out_data_q;
|
||||
logic s_out_fault_q;
|
||||
logic signed [ 7:0] s_out_data_d;
|
||||
logic signed [32:0] s_zout_sum;
|
||||
logic signed [32:0] s_act_min_ext;
|
||||
logic signed [32:0] s_act_max_ext;
|
||||
logic s_shift_ready;
|
||||
logic s_mul_ready;
|
||||
logic s_nud_ready;
|
||||
logic s_rdp_ready;
|
||||
|
||||
// Pinned Q31 nudge and trunc-toward-zero division by 2^31. The
|
||||
// INT32_MIN-times-INT32_MIN saturation is the only way to reach
|
||||
// product_i == 2^62, so the product value identifies the special case.
|
||||
function automatic logic signed [31:0] nudge_truncate(input logic signed [63:0] product_i);
|
||||
logic signed [63:0] s_nudged;
|
||||
logic signed [63:0] s_quotient;
|
||||
begin
|
||||
if (product_i == 64'sh4000_0000_0000_0000) begin
|
||||
return 32'sh7fff_ffff;
|
||||
end
|
||||
s_nudged = product_i + ((product_i >= 64'sd0) ? 64'sd1073741824 : -64'sd1073741823);
|
||||
if (s_nudged < 64'sd0) begin
|
||||
s_quotient = -((-s_nudged) >>> 31);
|
||||
end else begin
|
||||
s_quotient = s_nudged >>> 31;
|
||||
end
|
||||
return 32'(s_quotient);
|
||||
end
|
||||
endfunction
|
||||
|
||||
// Pinned correctly rounded division by 2^exponent_i (exponent 0 returns value_i).
|
||||
function automatic logic signed [31:0] rounding_divide_by_pot(input logic signed [31:0] value_i,
|
||||
input logic [4:0] exponent_i);
|
||||
logic [31:0] s_mask;
|
||||
logic [31:0] s_remainder;
|
||||
logic [31:0] s_threshold;
|
||||
logic signed [31:0] s_quotient;
|
||||
begin
|
||||
if (exponent_i == 5'd0) begin
|
||||
return value_i;
|
||||
end
|
||||
s_mask = (32'd1 << exponent_i) - 32'd1;
|
||||
s_remainder = value_i & s_mask;
|
||||
s_threshold = (s_mask >> 1) + ((value_i < 32'sd0) ? 32'd1 : 32'd0);
|
||||
s_quotient = value_i >>> exponent_i;
|
||||
return s_quotient + ((s_remainder > s_threshold) ? 32'sd1 : 32'sd0);
|
||||
end
|
||||
endfunction
|
||||
|
||||
// Stage 0: checked left shift; the 64-bit intermediate detects any result
|
||||
// outside signed INT32 without wrapping. Valid shifts are -31..30.
|
||||
always_comb begin
|
||||
s_shift_data_d = req_acc_i;
|
||||
s_shift_fault_d = 1'b0;
|
||||
s_shifted_wide = 64'(req_acc_i);
|
||||
if (req_shift_i > 8'sd0) begin
|
||||
s_shifted_wide = 64'(req_acc_i) <<< req_shift_i[5:0];
|
||||
s_shift_data_d = s_shifted_wide[31:0];
|
||||
s_shift_fault_d = s_shifted_wide != 64'($signed(s_shifted_wide[31:0]));
|
||||
end
|
||||
end
|
||||
|
||||
// Stage 1: the multiplier is consumed as a nonnegative signed INT32 value;
|
||||
// multiplier 0 is the flushed case. The full 64-bit product is registered.
|
||||
always_comb begin
|
||||
s_mul_product_d = s_shift_data_q * $signed(s_shift_multiplier_q);
|
||||
end
|
||||
|
||||
// Stage 2: nudge and truncating division by 2^31 (SRDHM back half).
|
||||
always_comb begin
|
||||
s_nud_data_d = nudge_truncate(s_mul_product_q);
|
||||
end
|
||||
|
||||
// Stage 3: RoundingDivideByPOT applies only for negative shifts, r = -shift.
|
||||
always_comb begin
|
||||
s_rdp_exponent = -s_nud_shift_q;
|
||||
s_rdp_data_d = s_nud_data_q;
|
||||
if (s_nud_shift_q < 8'sd0) begin
|
||||
s_rdp_data_d = rounding_divide_by_pot(s_nud_data_q, s_rdp_exponent[4:0]);
|
||||
end
|
||||
end
|
||||
|
||||
// Stage 4: widened 33-bit zero-point add, then clamp to the signed bounds;
|
||||
// a flagged item publishes 8'sd0 with resp_fault_o set.
|
||||
always_comb begin
|
||||
s_zout_sum = {s_rdp_data_q[31], s_rdp_data_q} + {{25{s_rdp_zout_q[7]}}, s_rdp_zout_q};
|
||||
s_act_min_ext = {{25{s_rdp_act_min_q[7]}}, s_rdp_act_min_q};
|
||||
s_act_max_ext = {{25{s_rdp_act_max_q[7]}}, s_rdp_act_max_q};
|
||||
s_out_data_d = s_zout_sum[7:0];
|
||||
if (s_zout_sum > s_act_max_ext) begin
|
||||
s_out_data_d = s_rdp_act_max_q;
|
||||
end else if (s_zout_sum < s_act_min_ext) begin
|
||||
s_out_data_d = s_rdp_act_min_q;
|
||||
end
|
||||
if (s_rdp_fault_q) begin
|
||||
s_out_data_d = 8'sd0;
|
||||
end
|
||||
end
|
||||
|
||||
assign s_shift_ready = !s_mul_valid_q || s_mul_ready;
|
||||
assign s_mul_ready = !s_nud_valid_q || s_nud_ready;
|
||||
assign s_nud_ready = !s_rdp_valid_q || s_rdp_ready;
|
||||
assign s_rdp_ready = !s_out_valid_q || resp_ready_i;
|
||||
assign req_ready_o = !s_shift_valid_q || s_shift_ready;
|
||||
|
||||
assign resp_valid_o = s_out_valid_q;
|
||||
assign resp_data_o = s_out_data_q;
|
||||
assign resp_fault_o = s_out_fault_q;
|
||||
|
||||
// Datapath registers load only when their stage transfers, so they need no
|
||||
// reset; only the per-stage valid bits are reset.
|
||||
always_ff @(posedge clk_i or negedge rst_n_i) begin
|
||||
if (!rst_n_i) begin
|
||||
s_shift_valid_q <= 1'b0;
|
||||
s_mul_valid_q <= 1'b0;
|
||||
s_nud_valid_q <= 1'b0;
|
||||
s_rdp_valid_q <= 1'b0;
|
||||
s_out_valid_q <= 1'b0;
|
||||
end else begin
|
||||
if (req_valid_i && req_ready_o) begin
|
||||
s_shift_valid_q <= 1'b1;
|
||||
end else if (s_shift_ready) begin
|
||||
s_shift_valid_q <= 1'b0;
|
||||
end
|
||||
if (s_shift_valid_q && s_shift_ready) begin
|
||||
s_mul_valid_q <= 1'b1;
|
||||
end else if (s_mul_ready) begin
|
||||
s_mul_valid_q <= 1'b0;
|
||||
end
|
||||
if (s_mul_valid_q && s_mul_ready) begin
|
||||
s_nud_valid_q <= 1'b1;
|
||||
end else if (s_nud_ready) begin
|
||||
s_nud_valid_q <= 1'b0;
|
||||
end
|
||||
if (s_nud_valid_q && s_nud_ready) begin
|
||||
s_rdp_valid_q <= 1'b1;
|
||||
end else if (s_rdp_ready) begin
|
||||
s_rdp_valid_q <= 1'b0;
|
||||
end
|
||||
if (s_rdp_valid_q && s_rdp_ready) begin
|
||||
s_out_valid_q <= 1'b1;
|
||||
end else if (resp_ready_i) begin
|
||||
s_out_valid_q <= 1'b0;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
always_ff @(posedge clk_i) begin
|
||||
if (req_valid_i && req_ready_o) begin
|
||||
s_shift_data_q <= s_shift_data_d;
|
||||
s_shift_fault_q <= s_shift_fault_d;
|
||||
s_shift_multiplier_q <= req_multiplier_i;
|
||||
s_shift_shift_q <= req_shift_i;
|
||||
s_shift_zout_q <= req_zout_i;
|
||||
s_shift_act_min_q <= req_act_min_i;
|
||||
s_shift_act_max_q <= req_act_max_i;
|
||||
end
|
||||
if (s_shift_valid_q && s_shift_ready) begin
|
||||
s_mul_product_q <= s_mul_product_d;
|
||||
s_mul_fault_q <= s_shift_fault_q;
|
||||
s_mul_shift_q <= s_shift_shift_q;
|
||||
s_mul_zout_q <= s_shift_zout_q;
|
||||
s_mul_act_min_q <= s_shift_act_min_q;
|
||||
s_mul_act_max_q <= s_shift_act_max_q;
|
||||
end
|
||||
if (s_mul_valid_q && s_mul_ready) begin
|
||||
s_nud_data_q <= s_nud_data_d;
|
||||
s_nud_fault_q <= s_mul_fault_q;
|
||||
s_nud_shift_q <= s_mul_shift_q;
|
||||
s_nud_zout_q <= s_mul_zout_q;
|
||||
s_nud_act_min_q <= s_mul_act_min_q;
|
||||
s_nud_act_max_q <= s_mul_act_max_q;
|
||||
end
|
||||
if (s_nud_valid_q && s_nud_ready) begin
|
||||
s_rdp_data_q <= s_rdp_data_d;
|
||||
s_rdp_fault_q <= s_nud_fault_q;
|
||||
s_rdp_zout_q <= s_nud_zout_q;
|
||||
s_rdp_act_min_q <= s_nud_act_min_q;
|
||||
s_rdp_act_max_q <= s_nud_act_max_q;
|
||||
end
|
||||
if (s_rdp_valid_q && s_rdp_ready) begin
|
||||
s_out_data_q <= s_out_data_d;
|
||||
s_out_fault_q <= s_rdp_fault_q;
|
||||
end
|
||||
end
|
||||
|
||||
`ifndef SV_ASSRT_DISABLE
|
||||
logic s_req_stall_q;
|
||||
logic s_resp_stall_q;
|
||||
|
||||
always_ff @(posedge clk_i or negedge rst_n_i) begin
|
||||
if (!rst_n_i) begin
|
||||
s_req_stall_q <= 1'b0;
|
||||
s_resp_stall_q <= 1'b0;
|
||||
end else begin
|
||||
s_req_stall_q <= req_valid_i && !req_ready_o;
|
||||
s_resp_stall_q <= resp_valid_o && !resp_ready_i;
|
||||
if (s_req_stall_q) begin
|
||||
assert ($stable(req_acc_i));
|
||||
assert ($stable(req_multiplier_i));
|
||||
assert ($stable(req_shift_i));
|
||||
assert ($stable(req_zout_i));
|
||||
assert ($stable(req_act_min_i));
|
||||
assert ($stable(req_act_max_i));
|
||||
end
|
||||
if (s_resp_stall_q) begin
|
||||
assert ($stable(resp_data_o));
|
||||
assert ($stable(resp_fault_o));
|
||||
end
|
||||
end
|
||||
end
|
||||
`endif
|
||||
endmodule
|
||||
@@ -0,0 +1,242 @@
|
||||
// Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
|
||||
// SPDX-License-Identifier: MulanPSL-2.0
|
||||
|
||||
// NPU vector unit: one depthwise context of eight checked INT32 lane sums
|
||||
// plus an independent scalar auxiliary unit (running signed-byte max and
|
||||
// checked INT32 byte sum). start_valid_i acquires the idle context and
|
||||
// preloads each lane sum from start_bias_i once (bias is added once, not per
|
||||
// row). Each accepted depthwise row centers every valid lane byte to signed
|
||||
// nine bits (byte - in_zero), forms the signed 9x8 product and performs a
|
||||
// checked INT32 add; any overflow enters Fault with the lowest faulting lane
|
||||
// index recorded, the sums freeze and never wrap. The scalar aux is usable in
|
||||
// Idle or Accumulate and is independent of the depthwise handshake; its
|
||||
// overflow reports the dedicated fault-lane encoding 3'd7 (the aux unit has
|
||||
// no lane). start_valid_i has priority over a same-cycle aux operation in
|
||||
// Idle; a depthwise overflow has priority over a same-cycle aux overflow for
|
||||
// the recorded lane. aux_result_o combinationally reflects the register
|
||||
// selected by the most recent aux operation (the sum register after
|
||||
// start/clear). drain_valid_i, accepted only in Accumulate with dw_valid_i
|
||||
// low, streams the eight lane sums in lane order with exactly-once
|
||||
// valid/ready handshakes and drain_last_o on the eighth beat; the context
|
||||
// frees after the final handshake. Fault and drain requests outside their
|
||||
// accepted states are ignored. Fault releases only via clear_i, which also
|
||||
// clears the sums and aux registers.
|
||||
module npu_vector (
|
||||
input logic clk_i,
|
||||
input logic rst_n_i,
|
||||
input logic clear_i,
|
||||
input logic start_valid_i,
|
||||
output logic start_ready_o,
|
||||
input logic [npu_pkg::DepthwiseLanes-1:0][31:0] start_bias_i,
|
||||
input logic dw_valid_i,
|
||||
output logic dw_ready_o,
|
||||
input logic [npu_pkg::DepthwiseLanes-1:0][ 7:0] dw_a_bytes_i,
|
||||
input logic [npu_pkg::DepthwiseLanes-1:0][ 7:0] dw_w_bytes_i,
|
||||
input logic signed [ 7:0] dw_in_zero_i,
|
||||
input logic [npu_pkg::DepthwiseLanes-1:0] dw_lane_valid_i,
|
||||
input logic aux_valid_i,
|
||||
input logic [ 1:0] aux_op_i,
|
||||
input logic signed [ 7:0] aux_data_i,
|
||||
input logic drain_valid_i,
|
||||
output logic drain_data_valid_o,
|
||||
input logic drain_data_ready_i,
|
||||
output logic signed [ 31:0] drain_data_o,
|
||||
output logic drain_last_o,
|
||||
output logic signed [ 31:0] aux_result_o,
|
||||
output logic busy_o,
|
||||
output logic fault_sticky_o,
|
||||
output logic [ 2:0] fault_lane_o
|
||||
);
|
||||
localparam int unsigned LaneCount = npu_pkg::DepthwiseLanes;
|
||||
// Aux opcodes (2'd0 is nop) and the dedicated aux fault-lane encoding.
|
||||
localparam logic [1:0] AuxOpMax = 2'd1;
|
||||
localparam logic [1:0] AuxOpSum = 2'd2;
|
||||
localparam logic [2:0] AuxFaultLane = 3'd7;
|
||||
|
||||
typedef enum logic [1:0] {
|
||||
Idle,
|
||||
Accumulate,
|
||||
Draining,
|
||||
Fault
|
||||
} state_e;
|
||||
|
||||
state_e s_state_d;
|
||||
state_e s_state_q;
|
||||
logic [LaneCount-1:0][31:0] s_sum_q;
|
||||
logic [ 2:0] s_drain_cnt_q;
|
||||
logic [ 2:0] s_fault_lane_q;
|
||||
logic signed [ 7:0] s_aux_max_q;
|
||||
logic signed [ 31:0] s_aux_sum_q;
|
||||
logic [ 1:0] s_aux_mode_q;
|
||||
logic [LaneCount-1:0][ 8:0] s_centered;
|
||||
logic [LaneCount-1:0][16:0] s_product;
|
||||
logic [LaneCount-1:0][32:0] s_sum_ext;
|
||||
logic [LaneCount-1:0] s_lane_ovf;
|
||||
logic s_dw_ovf_any;
|
||||
logic [ 2:0] s_dw_fault_lane;
|
||||
logic [ 32:0] s_aux_sum_ext;
|
||||
logic s_aux_ovf;
|
||||
logic s_dw_accept;
|
||||
logic s_aux_accept;
|
||||
logic s_aux_sum_ovf_evt;
|
||||
logic s_drain_accept;
|
||||
|
||||
// Signed 9-bit centering (byte - in_zero), signed 9x8 product and checked
|
||||
// 33-bit accumulation per lane; overflow when the carry into the sign bit
|
||||
// differs from the new sign bit.
|
||||
for (genvar lane = 0; lane < LaneCount; lane++) begin : gen_lane_datapath
|
||||
assign s_centered[lane] = $signed(
|
||||
{dw_a_bytes_i[lane][7], dw_a_bytes_i[lane]}
|
||||
) - $signed(
|
||||
{dw_in_zero_i[7], dw_in_zero_i}
|
||||
);
|
||||
assign s_product[lane] = $signed(s_centered[lane]) * $signed(dw_w_bytes_i[lane]);
|
||||
assign s_sum_ext[lane] = {s_sum_q[lane][31], s_sum_q[lane]} +
|
||||
{{16{s_product[lane][16]}}, s_product[lane]};
|
||||
assign s_lane_ovf[lane] = s_sum_ext[lane][32] != s_sum_ext[lane][31];
|
||||
end
|
||||
|
||||
assign s_dw_ovf_any = |(dw_lane_valid_i & s_lane_ovf);
|
||||
assign s_aux_sum_ext = {s_aux_sum_q[31], s_aux_sum_q} + {{25{aux_data_i[7]}}, aux_data_i};
|
||||
assign s_aux_ovf = s_aux_sum_ext[32] != s_aux_sum_ext[31];
|
||||
assign s_dw_accept = dw_valid_i && (s_state_q == Accumulate);
|
||||
assign s_aux_accept = aux_valid_i && (aux_op_i != 2'd0) &&
|
||||
((s_state_q == Idle) || (s_state_q == Accumulate)) &&
|
||||
!((s_state_q == Idle) && start_valid_i);
|
||||
assign s_aux_sum_ovf_evt = s_aux_accept && (aux_op_i == AuxOpSum) && s_aux_ovf;
|
||||
assign s_drain_accept = drain_valid_i && !dw_valid_i && (s_state_q == Accumulate);
|
||||
|
||||
// Lowest faulting lane wins within a cycle.
|
||||
always_comb begin
|
||||
s_dw_fault_lane = 3'd0;
|
||||
for (int lane = LaneCount - 1; lane >= 0; lane--) begin
|
||||
if (dw_lane_valid_i[lane] && s_lane_ovf[lane]) begin
|
||||
s_dw_fault_lane = 3'(lane);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
always_comb begin
|
||||
s_state_d = s_state_q;
|
||||
unique case (s_state_q)
|
||||
Idle: begin
|
||||
if (start_valid_i) begin
|
||||
s_state_d = Accumulate;
|
||||
end else if (s_aux_sum_ovf_evt) begin
|
||||
s_state_d = Fault;
|
||||
end
|
||||
end
|
||||
Accumulate: begin
|
||||
if (s_dw_accept && s_dw_ovf_any) begin
|
||||
s_state_d = Fault;
|
||||
end else if (s_aux_sum_ovf_evt) begin
|
||||
s_state_d = Fault;
|
||||
end else if (s_drain_accept) begin
|
||||
s_state_d = Draining;
|
||||
end
|
||||
end
|
||||
Draining: begin
|
||||
if (drain_data_ready_i && (s_drain_cnt_q == 3'(LaneCount - 1))) begin
|
||||
s_state_d = Idle;
|
||||
end
|
||||
end
|
||||
Fault: begin
|
||||
s_state_d = Fault;
|
||||
end
|
||||
default: begin
|
||||
s_state_d = Idle;
|
||||
end
|
||||
endcase
|
||||
end
|
||||
|
||||
assign start_ready_o = (s_state_q == Idle);
|
||||
assign dw_ready_o = (s_state_q == Accumulate);
|
||||
assign drain_data_valid_o = (s_state_q == Draining);
|
||||
assign drain_data_o = $signed(s_sum_q[s_drain_cnt_q]);
|
||||
assign drain_last_o = (s_state_q == Draining) && (s_drain_cnt_q == 3'(LaneCount - 1));
|
||||
assign aux_result_o = (s_aux_mode_q == AuxOpMax) ?
|
||||
{{24{s_aux_max_q[7]}}, s_aux_max_q} : s_aux_sum_q;
|
||||
assign busy_o = (s_state_q != Idle);
|
||||
assign fault_sticky_o = (s_state_q == Fault);
|
||||
assign fault_lane_o = s_fault_lane_q;
|
||||
|
||||
always_ff @(posedge clk_i or negedge rst_n_i) begin
|
||||
if (!rst_n_i) begin
|
||||
s_state_q <= Idle;
|
||||
s_sum_q <= '0;
|
||||
s_drain_cnt_q <= 3'd0;
|
||||
s_fault_lane_q <= 3'd0;
|
||||
s_aux_max_q <= 8'h80;
|
||||
s_aux_sum_q <= 32'd0;
|
||||
s_aux_mode_q <= 2'd0;
|
||||
end else if (clear_i) begin
|
||||
s_state_q <= Idle;
|
||||
s_sum_q <= '0;
|
||||
s_drain_cnt_q <= 3'd0;
|
||||
s_fault_lane_q <= 3'd0;
|
||||
s_aux_max_q <= 8'h80;
|
||||
s_aux_sum_q <= 32'd0;
|
||||
s_aux_mode_q <= 2'd0;
|
||||
end else begin
|
||||
s_state_q <= s_state_d;
|
||||
if ((s_state_q == Idle) && start_valid_i) begin
|
||||
// Bias preloads each lane sum exactly once at context acquire.
|
||||
for (int lane = 0; lane < LaneCount; lane++) begin
|
||||
s_sum_q[lane] <= start_bias_i[lane];
|
||||
end
|
||||
s_drain_cnt_q <= 3'd0;
|
||||
s_aux_max_q <= 8'h80;
|
||||
s_aux_sum_q <= 32'd0;
|
||||
s_aux_mode_q <= 2'd0;
|
||||
end
|
||||
if (s_dw_accept && !s_dw_ovf_any) begin
|
||||
for (int lane = 0; lane < LaneCount; lane++) begin
|
||||
if (dw_lane_valid_i[lane]) begin
|
||||
s_sum_q[lane] <= s_sum_ext[lane][31:0];
|
||||
end
|
||||
end
|
||||
end
|
||||
// Depthwise overflow wins the recorded lane over a same-cycle aux
|
||||
// overflow; the aux unit reports its dedicated encoding otherwise.
|
||||
if (s_dw_accept && s_dw_ovf_any) begin
|
||||
s_fault_lane_q <= s_dw_fault_lane;
|
||||
end else if (s_aux_sum_ovf_evt) begin
|
||||
s_fault_lane_q <= AuxFaultLane;
|
||||
end
|
||||
if (s_aux_accept) begin
|
||||
if (aux_op_i == AuxOpMax) begin
|
||||
if ($signed(aux_data_i) > s_aux_max_q) begin
|
||||
s_aux_max_q <= aux_data_i;
|
||||
end
|
||||
s_aux_mode_q <= AuxOpMax;
|
||||
end else if (!s_aux_ovf) begin
|
||||
s_aux_sum_q <= s_aux_sum_ext[31:0];
|
||||
s_aux_mode_q <= AuxOpSum;
|
||||
end
|
||||
end
|
||||
if ((s_state_q == Accumulate) && (s_state_d == Draining)) begin
|
||||
s_drain_cnt_q <= 3'd0;
|
||||
end else if ((s_state_q == Draining) && drain_data_ready_i) begin
|
||||
s_drain_cnt_q <= s_drain_cnt_q + 3'd1;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
`ifndef SV_ASSRT_DISABLE
|
||||
// A stalled drain beat holds its lane sums and lane index: the flag samples
|
||||
// "no handshake completed last cycle", so the $stable checks compare across
|
||||
// exactly the cycles where no update was allowed.
|
||||
logic s_drain_stall_q;
|
||||
always_ff @(posedge clk_i or negedge rst_n_i) begin
|
||||
if (!rst_n_i) begin
|
||||
s_drain_stall_q <= 1'b0;
|
||||
end else begin
|
||||
s_drain_stall_q <= (s_state_q == Draining) && !drain_data_ready_i;
|
||||
if (s_drain_stall_q) begin
|
||||
assert ($stable(s_sum_q));
|
||||
assert ($stable(s_drain_cnt_q));
|
||||
end
|
||||
end
|
||||
end
|
||||
`endif
|
||||
endmodule
|
||||
@@ -195,7 +195,14 @@
|
||||
"jpeg_table_cache.sv",
|
||||
"jpeg_table_register_bank.sv",
|
||||
"jpeg_table_store.sv",
|
||||
"jpeg_transform.sv"
|
||||
"jpeg_transform.sv",
|
||||
"npu_accumulator.sv",
|
||||
"npu_local_sram.sv",
|
||||
"npu_mac_array.sv",
|
||||
"npu_patch_packer.sv",
|
||||
"npu_pkg.sv",
|
||||
"npu_requantizer.sv",
|
||||
"npu_vector.sv"
|
||||
],
|
||||
"rtl/ip/peripheral": [
|
||||
"apb4_clint.sv",
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run the isolated NPU-P1 IHP130 synthesis and 72 MHz STA flow."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
SYNTH_TCL = ROOT / "physical/smoke/syn/yosys/script/synth.tcl"
|
||||
STA_TCL = ROOT / "physical/smoke/sta/opensta/npu_p1.tcl"
|
||||
STA_SDC = ROOT / "physical/smoke/sta/opensta/npu_p1.sdc"
|
||||
RUN_FLOW = ROOT / "scripts/run_flow.py"
|
||||
|
||||
PDK_ROOT = ROOT / "physical/pdk/IHP-Open-PDK/ihp-sg13g2/libs.ref"
|
||||
STA_LIBERTY = PDK_ROOT / "sg13g2_stdcell/lib/sg13g2_stdcell_slow_1p08V_125C.lib"
|
||||
STA_LINK_LIB = PDK_ROOT / "sg13g2_io/lib/sg13g2_io_slow_1p08V_3p0V_125C.lib"
|
||||
|
||||
TOPS = (
|
||||
"npu_local_sram",
|
||||
"npu_patch_packer",
|
||||
"npu_mac_array",
|
||||
"npu_accumulator",
|
||||
"npu_vector",
|
||||
"npu_requantizer",
|
||||
)
|
||||
NPU_RTL = (
|
||||
"npu_pkg.sv",
|
||||
"npu_local_sram.sv",
|
||||
"npu_patch_packer.sv",
|
||||
"npu_mac_array.sv",
|
||||
"npu_accumulator.sv",
|
||||
"npu_vector.sv",
|
||||
"npu_requantizer.sv",
|
||||
)
|
||||
SRAM_MACRO = "RM_IHPSG13_1P_1024x32_c2_bm_bist"
|
||||
EXPECTED_SRAM_MACROS = 16
|
||||
TARGET_PERIOD_PS = 13889
|
||||
|
||||
VIOLATED = re.compile(r"VIOLATED")
|
||||
LATCH = re.compile(r"\$dlatch|latch", re.IGNORECASE)
|
||||
|
||||
|
||||
def _run_flow(tool: str, log: Path, result: Path, env: dict[str, str], command: list[str]) -> None:
|
||||
log.parent.mkdir(parents=True, exist_ok=True)
|
||||
args = [sys.executable, str(RUN_FLOW), "--tool", tool, "--log", str(log), "--result", str(result)]
|
||||
for name, value in env.items():
|
||||
args += ["--env", f"{name}={value}"]
|
||||
args += ["--", *command]
|
||||
completed = subprocess.run(args, cwd=ROOT)
|
||||
if completed.returncode != 0:
|
||||
raise RuntimeError(f"{tool} flow failed for {env.get('TOP_DESIGN', env.get('OPENSTA_TOP'))}: {log}")
|
||||
|
||||
|
||||
def _write_filelist(path: Path) -> None:
|
||||
lines = [
|
||||
"+define+PDK_IHP130",
|
||||
"+define+HAVE_SRAM_MACRO",
|
||||
"+define+SYNTHESIS",
|
||||
"+define+SV_ASSRT_DISABLE",
|
||||
f"+incdir+{ROOT / 'rtl/ip/multimedia'}",
|
||||
f"+incdir+{ROOT / 'rtl/managed/clusterip/common/rtl'}",
|
||||
]
|
||||
for name in NPU_RTL:
|
||||
source = ROOT / "rtl/ip/multimedia" / name
|
||||
if not source.is_file():
|
||||
raise RuntimeError(f"missing NPU-P1 RTL source: {source}")
|
||||
lines.append(str(source))
|
||||
lines.append(str(ROOT / "rtl/tech/tc_sram.sv"))
|
||||
lines.append(str(ROOT / "rtl/managed/clusterip/common/rtl/utils/register.sv"))
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def _synth(top: str, build_root: Path, filelist: Path) -> dict[str, object]:
|
||||
base = build_root / "syn/yosys-npu" / top
|
||||
for sub in ("out", "tmp", "rpt"):
|
||||
(base / sub).mkdir(parents=True, exist_ok=True)
|
||||
env = {
|
||||
"PDK": "IHP130",
|
||||
"SOC": "MINI",
|
||||
"SYNTH_RECIPE": "balanced",
|
||||
"HAVE_SRAM_MACRO": "YES",
|
||||
"SRAM_SIZE_KIB": "32",
|
||||
"YOSYS_TARGET_PERIOD_PS": str(TARGET_PERIOD_PS),
|
||||
"SV_FLIST": str(filelist),
|
||||
"TOP_DESIGN": top,
|
||||
"PROJ_NAME": top,
|
||||
"BUILD": str(base / "out"),
|
||||
"WORK": str(base / "tmp"),
|
||||
"REPORTS": str(base / "rpt"),
|
||||
"NETLIST": str(base / "out" / f"{top}_yosys.v"),
|
||||
"CONFIG": str(base / "out" / f"{top}_yosys.config"),
|
||||
}
|
||||
log = build_root / "syn/yosys-npu" / f"{top}.log"
|
||||
_run_flow("yosys", log, base / "result-synth.json", env, ["yosys", "-c", str(SYNTH_TCL)])
|
||||
area = json.loads((base / "rpt" / f"{top}_area.json").read_text(encoding="utf-8"))
|
||||
design = area["design"]
|
||||
macro_count = design.get("num_cells_by_type", {}).get(SRAM_MACRO, 0)
|
||||
latch_cells = {
|
||||
name: count
|
||||
for name, count in design.get("num_cells_by_type", {}).items()
|
||||
if "dlatch" in name.lower() or "latch" in name.lower()
|
||||
}
|
||||
log_text = log.read_text(encoding="utf-8", errors="replace")
|
||||
latch_lines = [
|
||||
line
|
||||
for line in log_text.splitlines()
|
||||
if LATCH.search(line) and "PROC_DLATCH pass" not in line
|
||||
]
|
||||
return {
|
||||
"top": top,
|
||||
"area_um2": design.get("area"),
|
||||
"num_cells": design.get("num_cells"),
|
||||
"sram_macro_count": macro_count,
|
||||
"latch_cells": latch_cells,
|
||||
"latch_log_lines": latch_lines[:10],
|
||||
"log": str(log),
|
||||
"netlist": env["NETLIST"],
|
||||
}
|
||||
|
||||
|
||||
def _sta(top: str, build_root: Path, netlist: Path) -> dict[str, object]:
|
||||
base = build_root / "sta/opensta-npu" / top
|
||||
base.mkdir(parents=True, exist_ok=True)
|
||||
sram_libs = sorted(
|
||||
glob.glob(str(PDK_ROOT / "sg13g2_sram/lib/*_slow_1p08V_125C.lib"))
|
||||
)
|
||||
env = {
|
||||
"OPENSTA_NETLIST": str(netlist),
|
||||
"OPENSTA_LIBERTY": str(STA_LIBERTY),
|
||||
"OPENSTA_LINK_LIBS": str(STA_LINK_LIB),
|
||||
"OPENSTA_SRAM_LIBS": " ".join(sram_libs),
|
||||
"OPENSTA_SDC": str(STA_SDC),
|
||||
"OPENSTA_REPORT": str(base / f"{top}_checks.rpt"),
|
||||
"OPENSTA_METRICS": str(base / f"{top}_timing_metrics.rpt"),
|
||||
"OPENSTA_TOP": top,
|
||||
}
|
||||
log = base / f"{top}.log"
|
||||
_run_flow("opensta", log, base / "result-sta.json", env, ["sta", "-no_init", "-exit", str(STA_TCL)])
|
||||
metrics = {}
|
||||
for line in (base / f"{top}_timing_metrics.rpt").read_text(encoding="utf-8").splitlines():
|
||||
name, _, value = line.partition("=")
|
||||
metrics[name] = float(value)
|
||||
checks = (base / f"{top}_checks.rpt").read_text(encoding="utf-8", errors="replace")
|
||||
return {
|
||||
"top": top,
|
||||
"wns_setup_ns": metrics.get("wns_max"),
|
||||
"tns_setup_ns": metrics.get("tns_max"),
|
||||
"setup_paths": metrics.get("paths_max"),
|
||||
"wns_hold_ns": metrics.get("wns_min"),
|
||||
"tns_hold_ns": metrics.get("tns_min"),
|
||||
"hold_paths": metrics.get("paths_min"),
|
||||
"violated_paths": len(VIOLATED.findall(checks)),
|
||||
"log": str(log),
|
||||
}
|
||||
|
||||
|
||||
def _tool_version(command: list[str]) -> str:
|
||||
completed = subprocess.run(command, capture_output=True, text=True)
|
||||
return (completed.stdout or completed.stderr).splitlines()[0].strip()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--build-root", type=Path, default=ROOT / "build/npu-p1-flow")
|
||||
parser.add_argument("--top", action="append", choices=TOPS, help="limit to one top module")
|
||||
parser.add_argument("--synth", action="store_true")
|
||||
parser.add_argument("--sta", action="store_true")
|
||||
parser.add_argument("--evidence", type=Path, help="evidence JSON output path")
|
||||
args = parser.parse_args()
|
||||
if not args.synth and not args.sta:
|
||||
args.synth = args.sta = True
|
||||
|
||||
tops = tuple(args.top) if args.top else TOPS
|
||||
build_root = args.build_root.resolve()
|
||||
filelist = build_root / "npu_p1.fl"
|
||||
_write_filelist(filelist)
|
||||
|
||||
evidence: dict[str, object] = {
|
||||
"phase": "NPU-P1",
|
||||
"verification_ids": ["NPU-V017"],
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"git_revision": subprocess.check_output(
|
||||
["git", "-C", str(ROOT), "rev-parse", "HEAD"], text=True
|
||||
).strip(),
|
||||
"tools": {
|
||||
"yosys": _tool_version(["yosys", "--version"]),
|
||||
"opensta": _tool_version(["sta", "-version"]),
|
||||
},
|
||||
"target_period_ps": TARGET_PERIOD_PS,
|
||||
"filelist": str(filelist),
|
||||
"tops": {},
|
||||
"failures": [],
|
||||
}
|
||||
failures: list[str] = evidence["failures"]
|
||||
for top in tops:
|
||||
record: dict[str, object] = {}
|
||||
if args.synth:
|
||||
record["synth"] = _synth(top, build_root, filelist)
|
||||
synth = record["synth"]
|
||||
if top == "npu_local_sram" and synth["sram_macro_count"] != EXPECTED_SRAM_MACROS:
|
||||
failures.append(
|
||||
f"npu_local_sram maps {synth['sram_macro_count']} {SRAM_MACRO}, "
|
||||
f"expected {EXPECTED_SRAM_MACROS}"
|
||||
)
|
||||
if synth["latch_log_lines"] or synth["latch_cells"]:
|
||||
failures.append(
|
||||
f"{top} synthesis shows latches: {synth['latch_log_lines']} {synth['latch_cells']}"
|
||||
)
|
||||
netlist = build_root / "syn/yosys-npu" / top / "out" / f"{top}_yosys.v"
|
||||
if args.sta:
|
||||
if not netlist.is_file():
|
||||
raise RuntimeError(f"missing netlist for {top}: {netlist}; run --synth first")
|
||||
record["sta"] = _sta(top, build_root, netlist)
|
||||
sta = record["sta"]
|
||||
if not (sta["wns_setup_ns"] >= 0.0):
|
||||
failures.append(f"{top} setup WNS {sta['wns_setup_ns']} ns < 0")
|
||||
if not (sta["tns_setup_ns"] == 0.0):
|
||||
failures.append(f"{top} setup TNS {sta['tns_setup_ns']} ns != 0")
|
||||
if sta["violated_paths"]:
|
||||
failures.append(f"{top} has {sta['violated_paths']} violating paths")
|
||||
if not sta["setup_paths"]:
|
||||
failures.append(f"{top} has no constrained setup paths (vacuous STA)")
|
||||
evidence["tops"][top] = record
|
||||
|
||||
evidence["verdict"] = "PASS" if not failures else "FAIL"
|
||||
payload = json.dumps(evidence, indent=2, sort_keys=True) + "\n"
|
||||
evidence_path = args.evidence or (build_root / "evidence-p1.json")
|
||||
evidence_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
evidence_path.write_text(payload, encoding="utf-8")
|
||||
print(payload, end="")
|
||||
return 0 if not failures else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,631 @@
|
||||
// Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
|
||||
// SPDX-License-Identifier: MulanPSL-2.0
|
||||
//
|
||||
// Scripted testbench for npu_accumulator: two-context acquire/bias preload,
|
||||
// checked row accumulation with a 64-bit TB model, position-major drain with
|
||||
// stall patterns, interleaved contexts, K-chunk continuation, back-to-back
|
||||
// reuse, exactly-once publication, pending drain while another drain is in
|
||||
// flight, intentional positive/negative overflow faults (sticky flag, lowest
|
||||
// sum index, no drain from Fault), and clear_i release.
|
||||
|
||||
`timescale 1ns / 1ps
|
||||
|
||||
module npu_accumulator_tb;
|
||||
logic clk_i = 1'b0;
|
||||
logic rst_n_i = 1'b0;
|
||||
logic clear;
|
||||
logic start_valid;
|
||||
logic start_ready;
|
||||
logic [ 7:0][31:0] start_bias;
|
||||
logic start_context;
|
||||
logic row_valid;
|
||||
logic row_ready;
|
||||
logic row_context;
|
||||
logic [ 7:0][ 7:0][16:0] row_products;
|
||||
logic [63:0][16:0] products_drive;
|
||||
logic drain_valid;
|
||||
logic drain_context;
|
||||
logic drain_data_valid;
|
||||
logic drain_data_ready;
|
||||
logic signed [31:0] drain_data;
|
||||
logic drain_last;
|
||||
logic busy;
|
||||
logic fault_sticky;
|
||||
logic [ 5:0] fault_sum;
|
||||
|
||||
// 64-bit TB model of the 2x64 sums; index = context*64 + position*8 + channel.
|
||||
longint signed model_sum [0:127];
|
||||
|
||||
always #5 clk_i = ~clk_i;
|
||||
|
||||
assign row_products = products_drive;
|
||||
|
||||
npu_accumulator u_dut (
|
||||
.clk_i (clk_i),
|
||||
.rst_n_i (rst_n_i),
|
||||
.clear_i (clear),
|
||||
.start_valid_i (start_valid),
|
||||
.start_ready_o (start_ready),
|
||||
.start_bias_i (start_bias),
|
||||
.start_context_o (start_context),
|
||||
.row_valid_i (row_valid),
|
||||
.row_ready_o (row_ready),
|
||||
.row_context_i (row_context),
|
||||
.row_products_i (row_products),
|
||||
.drain_valid_i (drain_valid),
|
||||
.drain_context_i (drain_context),
|
||||
.drain_data_valid_o(drain_data_valid),
|
||||
.drain_data_ready_i(drain_data_ready),
|
||||
.drain_data_o (drain_data),
|
||||
.drain_last_o (drain_last),
|
||||
.busy_o (busy),
|
||||
.fault_sticky_o (fault_sticky),
|
||||
.fault_sum_o (fault_sum)
|
||||
);
|
||||
|
||||
function automatic longint signed product_at(input integer index_i);
|
||||
logic signed [16:0] s_product;
|
||||
begin
|
||||
s_product = products_drive[index_i];
|
||||
product_at = s_product;
|
||||
end
|
||||
endfunction
|
||||
|
||||
task automatic hard_reset;
|
||||
begin
|
||||
rst_n_i = 1'b0;
|
||||
repeat (3) @(posedge clk_i);
|
||||
rst_n_i = 1'b1;
|
||||
repeat (2) @(posedge clk_i);
|
||||
end
|
||||
endtask
|
||||
|
||||
task automatic fill_products(input logic signed [16:0] value_i);
|
||||
begin
|
||||
products_drive = {64{value_i}};
|
||||
end
|
||||
endtask
|
||||
|
||||
task automatic set_product(input integer index_i, input logic signed [16:0] value_i);
|
||||
begin
|
||||
products_drive[index_i] = value_i;
|
||||
end
|
||||
endtask
|
||||
|
||||
task automatic acquire(input logic expected_context_i);
|
||||
begin
|
||||
@(negedge clk_i);
|
||||
start_valid = 1'b1;
|
||||
for (int cycle = 0; cycle < 100; cycle++) begin
|
||||
@(posedge clk_i);
|
||||
if (start_ready) begin
|
||||
if (start_context !== expected_context_i) begin
|
||||
$fatal(1, "NPU accumulator acquire context mismatch got=%0d expected=%0d",
|
||||
start_context, expected_context_i);
|
||||
end
|
||||
for (int index = 0; index < 64; index++) begin
|
||||
model_sum[expected_context_i*64+index] = $signed(start_bias[index%8]);
|
||||
end
|
||||
@(negedge clk_i);
|
||||
start_valid = 1'b0;
|
||||
return;
|
||||
end
|
||||
end
|
||||
$fatal(1, "NPU accumulator acquire timeout context=%0d", expected_context_i);
|
||||
end
|
||||
endtask
|
||||
|
||||
// Accepted rows must never overflow the 64-bit-checked TB model here; the
|
||||
// faulting-row task below is the only overflow path. When with_drain_i is
|
||||
// set, the drain request for the same context is raised together with the
|
||||
// row: the context enters DrainPending in the acceptance cycle and the
|
||||
// drain may publish only after this row has landed in the sums.
|
||||
task automatic send_row(input logic context_i, input logic with_drain_i);
|
||||
begin
|
||||
@(negedge clk_i);
|
||||
row_context = context_i;
|
||||
row_valid = 1'b1;
|
||||
if (with_drain_i) begin
|
||||
drain_context = context_i;
|
||||
drain_valid = 1'b1;
|
||||
end
|
||||
for (int cycle = 0; cycle < 100; cycle++) begin
|
||||
@(posedge clk_i);
|
||||
if (row_ready) begin
|
||||
for (int index = 0; index < 64; index++) begin
|
||||
model_sum[context_i*64+index] = model_sum[context_i*64+index] + product_at(index);
|
||||
if ((model_sum[context_i*64+index] > 64'sd2147483647) ||
|
||||
(model_sum[context_i*64+index] < -64'sd2147483648)) begin
|
||||
$fatal(1, "NPU accumulator TB model overflow on non-fault row context=%0d index=%0d",
|
||||
context_i, index);
|
||||
end
|
||||
end
|
||||
@(negedge clk_i);
|
||||
row_valid = 1'b0;
|
||||
if (with_drain_i) begin
|
||||
drain_valid = 1'b0;
|
||||
end
|
||||
return;
|
||||
end
|
||||
end
|
||||
$fatal(1, "NPU accumulator row timeout context=%0d", context_i);
|
||||
end
|
||||
endtask
|
||||
|
||||
// Drive a burst of rows accepted on consecutive cycles (row_valid stays
|
||||
// high, products change every cycle) to prove the one-row-per-cycle
|
||||
// sustained throughput of the pipelined accumulate: stage A must keep
|
||||
// accepting while stage B accumulates the previous row. Row products are
|
||||
// base_i + row*step_i replicated on all 64 lanes.
|
||||
task automatic send_row_burst(input logic context_i, input integer row_count_i,
|
||||
input integer base_i, input integer step_i);
|
||||
begin
|
||||
for (int row = 0; row < row_count_i; row++) begin
|
||||
@(negedge clk_i);
|
||||
row_context = context_i;
|
||||
products_drive = {64{17'(base_i + row * step_i)}};
|
||||
row_valid = 1'b1;
|
||||
for (int cycle = 0; cycle < 100; cycle++) begin
|
||||
@(posedge clk_i);
|
||||
if (row_ready) begin
|
||||
for (int index = 0; index < 64; index++) begin
|
||||
model_sum[context_i*64+index] = model_sum[context_i*64+index] + product_at(index);
|
||||
if ((model_sum[context_i*64+index] > 64'sd2147483647) ||
|
||||
(model_sum[context_i*64+index] < -64'sd2147483648)) begin
|
||||
$fatal(1, "NPU accumulator TB model overflow on burst row context=%0d index=%0d",
|
||||
context_i, index);
|
||||
end
|
||||
end
|
||||
break;
|
||||
end
|
||||
if (cycle == 99) begin
|
||||
$fatal(1, "NPU accumulator burst row timeout context=%0d row=%0d", context_i, row);
|
||||
end
|
||||
end
|
||||
end
|
||||
@(negedge clk_i);
|
||||
row_valid = 1'b0;
|
||||
end
|
||||
endtask
|
||||
|
||||
// Drive a row whose model predicts at least one overflowing update; the DUT
|
||||
// must latch the sticky fault and the lowest overflowing sum index, and the
|
||||
// stored sums (and therefore the TB model) stay untouched.
|
||||
task automatic send_row_expect_fault(input logic context_i);
|
||||
begin
|
||||
longint signed s_trial;
|
||||
integer s_fault_index;
|
||||
logic s_found;
|
||||
s_found = 1'b0;
|
||||
s_fault_index = 0;
|
||||
for (int index = 63; index >= 0; index--) begin
|
||||
s_trial = model_sum[context_i*64+index] + product_at(index);
|
||||
if ((s_trial > 64'sd2147483647) || (s_trial < -64'sd2147483648)) begin
|
||||
s_found = 1'b1;
|
||||
s_fault_index = index;
|
||||
end
|
||||
end
|
||||
if (!s_found) begin
|
||||
$fatal(1, "NPU accumulator TB expected a fault but the model does not overflow");
|
||||
end
|
||||
@(negedge clk_i);
|
||||
row_context = context_i;
|
||||
row_valid = 1'b1;
|
||||
for (int cycle = 0; cycle < 100; cycle++) begin
|
||||
@(posedge clk_i);
|
||||
if (row_ready) begin
|
||||
@(negedge clk_i);
|
||||
row_valid = 1'b0;
|
||||
break;
|
||||
end
|
||||
if (cycle == 99) begin
|
||||
$fatal(1, "NPU accumulator fault row timeout context=%0d", context_i);
|
||||
end
|
||||
end
|
||||
repeat (2) @(posedge clk_i);
|
||||
if (fault_sticky !== 1'b1) begin
|
||||
$fatal(1, "NPU accumulator fault_sticky not set context=%0d", context_i);
|
||||
end
|
||||
if (fault_sum !== 6'(s_fault_index)) begin
|
||||
$fatal(1, "NPU accumulator fault_sum mismatch got=%0d expected=%0d", fault_sum,
|
||||
s_fault_index);
|
||||
end
|
||||
end
|
||||
endtask
|
||||
|
||||
task automatic drain_launch(input logic context_i);
|
||||
begin
|
||||
@(negedge clk_i);
|
||||
drain_context = context_i;
|
||||
drain_valid = 1'b1;
|
||||
for (int cycle = 0; cycle < 100; cycle++) begin
|
||||
@(posedge clk_i);
|
||||
if (drain_data_valid) begin
|
||||
break;
|
||||
end
|
||||
if (cycle == 99) begin
|
||||
$fatal(1, "NPU accumulator drain launch timeout context=%0d", context_i);
|
||||
end
|
||||
end
|
||||
@(negedge clk_i);
|
||||
drain_valid = 1'b0;
|
||||
drain_data_ready = 1'b0;
|
||||
end
|
||||
endtask
|
||||
|
||||
// Streams the 64 sums of a context already in Draining, checking
|
||||
// position-major order, values, and drain_last; applies deterministic stall
|
||||
// stretches, optionally interleaves rows to the other accumulating context
|
||||
// at items 16/32/48, and optionally raises a held pending drain request for
|
||||
// another context at item 40 (left asserted for the caller to observe).
|
||||
task automatic drain_stream(input logic context_i, input logic interleave_en_i,
|
||||
input logic interleave_context_i, input logic pending_en_i,
|
||||
input logic pending_context_i);
|
||||
begin
|
||||
integer stalls;
|
||||
for (int index = 0; index < 64; index++) begin
|
||||
if (interleave_en_i && ((index == 16) || (index == 32) || (index == 48))) begin
|
||||
fill_products(17'sd900);
|
||||
send_row(interleave_context_i, 1'b0);
|
||||
end
|
||||
if (pending_en_i && (index == 40)) begin
|
||||
@(negedge clk_i);
|
||||
drain_context = pending_context_i;
|
||||
drain_valid = 1'b1;
|
||||
end
|
||||
stalls = ((index % 7) == 3) ? 3 : (index % 2);
|
||||
repeat (stalls) @(posedge clk_i);
|
||||
@(negedge clk_i);
|
||||
drain_data_ready = 1'b1;
|
||||
for (int cycle = 0; cycle < 100; cycle++) begin
|
||||
@(posedge clk_i);
|
||||
if (drain_data_valid && drain_data_ready) begin
|
||||
if ($signed(drain_data) !== model_sum[context_i*64+index]) begin
|
||||
$fatal(1, "NPU accumulator drain mismatch context=%0d index=%0d got=%0d expected=%0d",
|
||||
context_i, index, $signed(drain_data), model_sum[context_i*64+index]);
|
||||
end
|
||||
if (drain_last !== (index == 63)) begin
|
||||
$fatal(1, "NPU accumulator drain_last mismatch context=%0d index=%0d", context_i,
|
||||
index);
|
||||
end
|
||||
break;
|
||||
end
|
||||
if (cycle == 99) begin
|
||||
$fatal(1, "NPU accumulator drain item timeout context=%0d index=%0d", context_i, index);
|
||||
end
|
||||
end
|
||||
@(negedge clk_i);
|
||||
drain_data_ready = 1'b0;
|
||||
end
|
||||
end
|
||||
endtask
|
||||
|
||||
task automatic drain_and_check(input logic context_i);
|
||||
begin
|
||||
drain_launch(context_i);
|
||||
drain_stream(context_i, 1'b0, 1'b0, 1'b0, 1'b0);
|
||||
end
|
||||
endtask
|
||||
|
||||
// A drain request naming an Idle or Fault context must never publish data.
|
||||
task automatic check_drain_ignored(input logic context_i);
|
||||
begin
|
||||
@(negedge clk_i);
|
||||
drain_context = context_i;
|
||||
drain_valid = 1'b1;
|
||||
repeat (4) @(posedge clk_i);
|
||||
if (drain_data_valid) begin
|
||||
$fatal(1, "NPU accumulator drain published from non-accumulating context=%0d", context_i);
|
||||
end
|
||||
@(negedge clk_i);
|
||||
drain_valid = 1'b0;
|
||||
end
|
||||
endtask
|
||||
|
||||
task automatic check_row_rejected(input logic context_i);
|
||||
begin
|
||||
@(negedge clk_i);
|
||||
row_context = context_i;
|
||||
row_valid = 1'b1;
|
||||
repeat (3) @(posedge clk_i);
|
||||
if (row_ready) begin
|
||||
$fatal(1, "NPU accumulator row accepted on non-accumulating context=%0d", context_i);
|
||||
end
|
||||
@(negedge clk_i);
|
||||
row_valid = 1'b0;
|
||||
end
|
||||
endtask
|
||||
|
||||
task automatic check_start_blocked;
|
||||
begin
|
||||
@(negedge clk_i);
|
||||
start_valid = 1'b1;
|
||||
repeat (3) @(posedge clk_i);
|
||||
if (start_ready) begin
|
||||
$fatal(1, "NPU accumulator start accepted while both contexts occupied");
|
||||
end
|
||||
@(negedge clk_i);
|
||||
start_valid = 1'b0;
|
||||
end
|
||||
endtask
|
||||
|
||||
task automatic pulse_clear;
|
||||
begin
|
||||
@(negedge clk_i);
|
||||
clear = 1'b1;
|
||||
@(negedge clk_i);
|
||||
clear = 1'b0;
|
||||
@(posedge clk_i);
|
||||
if (fault_sticky !== 1'b0) begin
|
||||
$fatal(1, "NPU accumulator fault_sticky not released by clear_i");
|
||||
end
|
||||
if (busy !== 1'b0) begin
|
||||
$fatal(1, "NPU accumulator busy not released by clear_i");
|
||||
end
|
||||
if (start_ready !== 1'b1) begin
|
||||
$fatal(1, "NPU accumulator start_ready not restored by clear_i");
|
||||
end
|
||||
end
|
||||
endtask
|
||||
|
||||
initial begin
|
||||
clear = 1'b0;
|
||||
start_valid = 1'b0;
|
||||
start_bias = '0;
|
||||
row_valid = 1'b0;
|
||||
row_context = 1'b0;
|
||||
products_drive = '0;
|
||||
drain_valid = 1'b0;
|
||||
drain_context = 1'b0;
|
||||
drain_data_ready = 1'b0;
|
||||
for (int index = 0; index < 128; index++) begin
|
||||
model_sum[index] = 64'sd0;
|
||||
end
|
||||
|
||||
hard_reset();
|
||||
if ((busy !== 1'b0) || (start_ready !== 1'b1) || (fault_sticky !== 1'b0)) begin
|
||||
$fatal(1, "NPU accumulator bad reset state busy=%0d start_ready=%0d fault=%0d", busy,
|
||||
start_ready, fault_sticky);
|
||||
end
|
||||
|
||||
// Scenario 1: ctx0 bias preload, two acquire-free accumulate bursts
|
||||
// (K-chunk continuation, including an all-zero row), drain with stalls.
|
||||
for (int channel = 0; channel < 8; channel++) begin
|
||||
start_bias[channel] = (channel * 12345) - 30000;
|
||||
end
|
||||
acquire(1'b0);
|
||||
if (busy !== 1'b1) begin
|
||||
$fatal(1, "NPU accumulator busy not set after acquire");
|
||||
end
|
||||
fill_products(17'sd300);
|
||||
set_product(26, -17'sd1000);
|
||||
set_product(47, 17'sd32512);
|
||||
send_row(1'b0, 1'b0);
|
||||
fill_products(-17'sd250);
|
||||
send_row(1'b0, 1'b0);
|
||||
fill_products(17'sd1000);
|
||||
send_row(1'b0, 1'b0);
|
||||
fill_products(17'sd0);
|
||||
send_row(1'b0, 1'b0);
|
||||
fill_products(-17'sd3000);
|
||||
set_product(0, 17'sd32512);
|
||||
send_row(1'b0, 1'b0);
|
||||
drain_and_check(1'b0);
|
||||
if (busy !== 1'b0) begin
|
||||
$fatal(1, "NPU accumulator busy stuck after drain");
|
||||
end
|
||||
|
||||
// Scenario 1b: back-to-back burst at one row per cycle; stage A must keep
|
||||
// accepting while stage B accumulates the previous row (full-rate RAW
|
||||
// chaining through the sums).
|
||||
for (int channel = 0; channel < 8; channel++) begin
|
||||
start_bias[channel] = (channel * 91) - 500;
|
||||
end
|
||||
acquire(1'b0);
|
||||
send_row_burst(1'b0, 24, 200, -11);
|
||||
drain_and_check(1'b0);
|
||||
if (busy !== 1'b0) begin
|
||||
$fatal(1, "NPU accumulator busy stuck after burst drain");
|
||||
end
|
||||
|
||||
// Scenario 2: exactly-once publication; a repeated drain request on the
|
||||
// now-Idle context publishes nothing.
|
||||
check_drain_ignored(1'b0);
|
||||
|
||||
// Scenario 3: back-to-back reuse of ctx0 after its drain completed.
|
||||
for (int channel = 0; channel < 8; channel++) begin
|
||||
start_bias[channel] = 100000 - (channel * 999);
|
||||
end
|
||||
acquire(1'b0);
|
||||
fill_products(17'sd2000);
|
||||
send_row(1'b0, 1'b0);
|
||||
set_product(63, -17'sd32768);
|
||||
send_row(1'b0, 1'b0);
|
||||
drain_and_check(1'b0);
|
||||
|
||||
// Scenario 4: interleave ctx1 acquire/accumulate while ctx0 drains, and
|
||||
// start backpressure while both contexts are occupied.
|
||||
for (int channel = 0; channel < 8; channel++) begin
|
||||
start_bias[channel] = channel * 100;
|
||||
end
|
||||
acquire(1'b0);
|
||||
for (int channel = 0; channel < 8; channel++) begin
|
||||
start_bias[channel] = (channel * 50) - 400;
|
||||
end
|
||||
acquire(1'b1);
|
||||
check_start_blocked();
|
||||
fill_products(17'sd500);
|
||||
send_row(1'b0, 1'b0);
|
||||
fill_products(-17'sd700);
|
||||
set_product(63, 17'sd12345);
|
||||
send_row(1'b1, 1'b0);
|
||||
drain_launch(1'b0);
|
||||
drain_stream(1'b0, 1'b1, 1'b1, 1'b0, 1'b0);
|
||||
drain_and_check(1'b1);
|
||||
if (busy !== 1'b0) begin
|
||||
$fatal(1, "NPU accumulator busy stuck after interleaved drains");
|
||||
end
|
||||
|
||||
// Scenario 5: a held drain request for ctx1 raised mid-drain of ctx0
|
||||
// launches ctx1 automatically once ctx0 finishes (single drain port).
|
||||
for (int channel = 0; channel < 8; channel++) begin
|
||||
start_bias[channel] = channel * 7;
|
||||
end
|
||||
acquire(1'b0);
|
||||
for (int channel = 0; channel < 8; channel++) begin
|
||||
start_bias[channel] = 50 - channel;
|
||||
end
|
||||
acquire(1'b1);
|
||||
fill_products(17'sd33);
|
||||
send_row(1'b0, 1'b0);
|
||||
fill_products(-17'sd44);
|
||||
send_row(1'b1, 1'b0);
|
||||
drain_launch(1'b0);
|
||||
drain_stream(1'b0, 1'b0, 1'b0, 1'b1, 1'b1);
|
||||
for (int cycle = 0; cycle < 100; cycle++) begin
|
||||
@(posedge clk_i);
|
||||
if (drain_data_valid) begin
|
||||
break;
|
||||
end
|
||||
if (cycle == 99) begin
|
||||
$fatal(1, "NPU accumulator pending drain did not launch after ctx0 finished");
|
||||
end
|
||||
end
|
||||
@(negedge clk_i);
|
||||
drain_valid = 1'b0;
|
||||
drain_stream(1'b1, 1'b0, 1'b0, 1'b0, 1'b0);
|
||||
if (busy !== 1'b0) begin
|
||||
$fatal(1, "NPU accumulator busy stuck after pending drain");
|
||||
end
|
||||
|
||||
// Scenario 5b: a drain request raised in the same cycle as the final row
|
||||
// acceptance must wait for the pipelined row to land in the sums before
|
||||
// publishing (DrainPending waits for the accumulate pipeline).
|
||||
for (int channel = 0; channel < 8; channel++) begin
|
||||
start_bias[channel] = 1000 + channel;
|
||||
end
|
||||
acquire(1'b0);
|
||||
fill_products(17'sd123);
|
||||
send_row(1'b0, 1'b0);
|
||||
fill_products(-17'sd456);
|
||||
send_row(1'b0, 1'b1);
|
||||
drain_stream(1'b0, 1'b0, 1'b0, 1'b0, 1'b0);
|
||||
if (busy !== 1'b0) begin
|
||||
$fatal(1, "NPU accumulator busy stuck after same-cycle row+drain");
|
||||
end
|
||||
|
||||
// Scenario 6: intentional positive overflow on ctx1. Two sums overflow in
|
||||
// the same row; the lowest index (10) must win the fault record. The
|
||||
// faulting context rejects rows, never drains, and clear_i releases it.
|
||||
// ctx0 is acquired first so the near-maximum bias lands on ctx1.
|
||||
for (int channel = 0; channel < 8; channel++) begin
|
||||
start_bias[channel] = channel;
|
||||
end
|
||||
acquire(1'b0);
|
||||
for (int channel = 0; channel < 8; channel++) begin
|
||||
start_bias[channel] = 100;
|
||||
end
|
||||
start_bias[2] = 32'h7fff_ff00;
|
||||
acquire(1'b1);
|
||||
fill_products(17'sd0);
|
||||
set_product(10, 17'sd1000);
|
||||
set_product(26, 17'sd2000);
|
||||
send_row_expect_fault(1'b1);
|
||||
check_row_rejected(1'b1);
|
||||
check_drain_ignored(1'b1);
|
||||
// The other context keeps working while ctx1 is faulted.
|
||||
fill_products(17'sd7);
|
||||
send_row(1'b0, 1'b0);
|
||||
drain_and_check(1'b0);
|
||||
pulse_clear();
|
||||
// clear_i released the Fault context: acquire and drain both again.
|
||||
for (int channel = 0; channel < 8; channel++) begin
|
||||
start_bias[channel] = channel * 3;
|
||||
end
|
||||
acquire(1'b0);
|
||||
for (int channel = 0; channel < 8; channel++) begin
|
||||
start_bias[channel] = channel * 5;
|
||||
end
|
||||
acquire(1'b1);
|
||||
fill_products(17'sd11);
|
||||
send_row(1'b1, 1'b0);
|
||||
drain_and_check(1'b1);
|
||||
fill_products(17'sd13);
|
||||
send_row(1'b0, 1'b0);
|
||||
drain_and_check(1'b0);
|
||||
|
||||
// Scenario 6b: a stage-B fault landing while the context sits in
|
||||
// DrainPending (row and drain request accepted in the same cycle) must win
|
||||
// over the drain: the context faults and the drain never publishes.
|
||||
for (int channel = 0; channel < 8; channel++) begin
|
||||
start_bias[channel] = channel * 2;
|
||||
end
|
||||
acquire(1'b0);
|
||||
for (int channel = 0; channel < 8; channel++) begin
|
||||
start_bias[channel] = 50;
|
||||
end
|
||||
start_bias[2] = 32'h7fff_ff00;
|
||||
acquire(1'b1);
|
||||
fill_products(17'sd0);
|
||||
set_product(10, 17'sd1000);
|
||||
@(negedge clk_i);
|
||||
row_context = 1'b1;
|
||||
row_valid = 1'b1;
|
||||
drain_context = 1'b1;
|
||||
drain_valid = 1'b1;
|
||||
for (int cycle = 0; cycle < 100; cycle++) begin
|
||||
@(posedge clk_i);
|
||||
if (row_ready) begin
|
||||
break;
|
||||
end
|
||||
if (cycle == 99) begin
|
||||
$fatal(1, "NPU accumulator pending-fault row timeout");
|
||||
end
|
||||
end
|
||||
@(negedge clk_i);
|
||||
row_valid = 1'b0;
|
||||
drain_valid = 1'b0;
|
||||
// The fault lands one cycle after acceptance; the drain must never publish.
|
||||
repeat (6) begin
|
||||
@(posedge clk_i);
|
||||
if (drain_data_valid) begin
|
||||
$fatal(1, "NPU accumulator drain published from faulting pending context");
|
||||
end
|
||||
end
|
||||
if (fault_sticky !== 1'b1) begin
|
||||
$fatal(1, "NPU accumulator pending fault not latched");
|
||||
end
|
||||
if (fault_sum !== 6'd10) begin
|
||||
$fatal(1, "NPU accumulator pending fault_sum mismatch got=%0d expected=10", fault_sum);
|
||||
end
|
||||
check_row_rejected(1'b1);
|
||||
pulse_clear();
|
||||
// Both contexts are healthy again after the clear.
|
||||
for (int channel = 0; channel < 8; channel++) begin
|
||||
start_bias[channel] = channel;
|
||||
end
|
||||
acquire(1'b0);
|
||||
fill_products(17'sd5);
|
||||
send_row(1'b0, 1'b0);
|
||||
drain_and_check(1'b0);
|
||||
|
||||
// Scenario 7: intentional negative overflow on ctx0 at sum index 5.
|
||||
for (int channel = 0; channel < 8; channel++) begin
|
||||
start_bias[channel] = 0;
|
||||
end
|
||||
start_bias[5] = 32'h8000_0010;
|
||||
acquire(1'b0);
|
||||
fill_products(17'sd0);
|
||||
set_product(5, -17'sd1000);
|
||||
send_row_expect_fault(1'b0);
|
||||
pulse_clear();
|
||||
|
||||
repeat (2) @(posedge clk_i);
|
||||
if ((busy !== 1'b0) || (start_ready !== 1'b1) || (fault_sticky !== 1'b0)) begin
|
||||
$fatal(1, "NPU accumulator bad final state busy=%0d start_ready=%0d fault=%0d", busy,
|
||||
start_ready, fault_sticky);
|
||||
end
|
||||
$display("NPU accumulator test passed");
|
||||
$finish;
|
||||
end
|
||||
endmodule
|
||||
@@ -0,0 +1,557 @@
|
||||
`timescale 1ns / 1ps
|
||||
|
||||
// Directed unit test for npu_local_sram. Runs identically against the
|
||||
// inferred behavioral banks (+define+PDK_BEHAV) and the IHP130 macro wrapper
|
||||
// (+define+PDK_IHP130 +define+HAVE_SRAM_MACRO + the RM_IHPSG13_1P models).
|
||||
module npu_local_sram_tb;
|
||||
logic clk_i = 1'b0;
|
||||
logic rst_n_i = 1'b0;
|
||||
logic clear_i = 1'b0;
|
||||
logic raw_write_valid_i = 1'b0;
|
||||
logic raw_write_half_i = 1'b0;
|
||||
logic [12:0] raw_write_addr_i = 13'd0;
|
||||
logic [31:0] raw_write_data_i = 32'd0;
|
||||
logic [ 3:0] raw_write_strb_i = 4'd0;
|
||||
logic raw_read_valid_i = 1'b0;
|
||||
logic raw_read_half_i = 1'b0;
|
||||
logic [12:0] raw_read_addr_i = 13'd0;
|
||||
logic [31:0] raw_read_data_o;
|
||||
logic pack_write_valid_i = 1'b0;
|
||||
logic pack_write_sel_w_i = 1'b0;
|
||||
logic pack_write_half_i = 1'b0;
|
||||
logic [12:0] pack_write_addr_i = 13'd0;
|
||||
logic [63:0] pack_write_data_i = 64'd0;
|
||||
logic [ 7:0] pack_write_strb_i = 8'd0;
|
||||
logic a_read_valid_i = 1'b0;
|
||||
logic a_read_half_i = 1'b0;
|
||||
logic [12:0] a_read_addr_i = 13'd0;
|
||||
logic [63:0] a_read_data_o;
|
||||
logic w_read_valid_i = 1'b0;
|
||||
logic w_read_half_i = 1'b0;
|
||||
logic [12:0] w_read_addr_i = 13'd0;
|
||||
logic [63:0] w_read_data_o;
|
||||
logic out_req_valid_i = 1'b0;
|
||||
logic out_req_write_i = 1'b0;
|
||||
logic out_req_half_i = 1'b0;
|
||||
logic [11:0] out_req_addr_i = 12'd0;
|
||||
logic [31:0] out_req_data_i = 32'd0;
|
||||
logic [ 3:0] out_req_strb_i = 4'd0;
|
||||
logic [31:0] out_read_data_o;
|
||||
logic param_write_valid_i = 1'b0;
|
||||
logic [12:0] param_write_addr_i = 13'd0;
|
||||
logic [31:0] param_write_data_i = 32'd0;
|
||||
logic [ 3:0] param_write_strb_i = 4'd0;
|
||||
logic param_read_valid_i = 1'b0;
|
||||
logic [12:0] param_read_addr_i = 13'd0;
|
||||
logic [31:0] param_read_data_o;
|
||||
logic access_err_sticky_o;
|
||||
logic [ 2:0] access_err_port_o;
|
||||
integer checks = 0;
|
||||
|
||||
always #5 clk_i = ~clk_i;
|
||||
|
||||
npu_local_sram u_npu_local_sram (
|
||||
.clk_i (clk_i),
|
||||
.rst_n_i (rst_n_i),
|
||||
.clear_i (clear_i),
|
||||
.raw_write_valid_i (raw_write_valid_i),
|
||||
.raw_write_half_i (raw_write_half_i),
|
||||
.raw_write_addr_i (raw_write_addr_i),
|
||||
.raw_write_data_i (raw_write_data_i),
|
||||
.raw_write_strb_i (raw_write_strb_i),
|
||||
.raw_read_valid_i (raw_read_valid_i),
|
||||
.raw_read_half_i (raw_read_half_i),
|
||||
.raw_read_addr_i (raw_read_addr_i),
|
||||
.raw_read_data_o (raw_read_data_o),
|
||||
.pack_write_valid_i (pack_write_valid_i),
|
||||
.pack_write_sel_w_i (pack_write_sel_w_i),
|
||||
.pack_write_half_i (pack_write_half_i),
|
||||
.pack_write_addr_i (pack_write_addr_i),
|
||||
.pack_write_data_i (pack_write_data_i),
|
||||
.pack_write_strb_i (pack_write_strb_i),
|
||||
.a_read_valid_i (a_read_valid_i),
|
||||
.a_read_half_i (a_read_half_i),
|
||||
.a_read_addr_i (a_read_addr_i),
|
||||
.a_read_data_o (a_read_data_o),
|
||||
.w_read_valid_i (w_read_valid_i),
|
||||
.w_read_half_i (w_read_half_i),
|
||||
.w_read_addr_i (w_read_addr_i),
|
||||
.w_read_data_o (w_read_data_o),
|
||||
.out_req_valid_i (out_req_valid_i),
|
||||
.out_req_write_i (out_req_write_i),
|
||||
.out_req_half_i (out_req_half_i),
|
||||
.out_req_addr_i (out_req_addr_i),
|
||||
.out_req_data_i (out_req_data_i),
|
||||
.out_req_strb_i (out_req_strb_i),
|
||||
.out_read_data_o (out_read_data_o),
|
||||
.param_write_valid_i(param_write_valid_i),
|
||||
.param_write_addr_i (param_write_addr_i),
|
||||
.param_write_data_i (param_write_data_i),
|
||||
.param_write_strb_i (param_write_strb_i),
|
||||
.param_read_valid_i (param_read_valid_i),
|
||||
.param_read_addr_i (param_read_addr_i),
|
||||
.param_read_data_o (param_read_data_o),
|
||||
.access_err_sticky_o(access_err_sticky_o),
|
||||
.access_err_port_o (access_err_port_o)
|
||||
);
|
||||
|
||||
function automatic logic [31:0] merge32(input logic [31:0] old_i, input logic [31:0] new_i,
|
||||
input logic [3:0] strb_i);
|
||||
merge32 = old_i;
|
||||
for (int byte_idx = 0; byte_idx < 4; byte_idx++) begin
|
||||
if (strb_i[byte_idx]) merge32[byte_idx*8+:8] = new_i[byte_idx*8+:8];
|
||||
end
|
||||
endfunction
|
||||
|
||||
function automatic logic [63:0] merge64(input logic [63:0] old_i, input logic [63:0] new_i,
|
||||
input logic [7:0] strb_i);
|
||||
merge64 = old_i;
|
||||
for (int byte_idx = 0; byte_idx < 8; byte_idx++) begin
|
||||
if (strb_i[byte_idx]) merge64[byte_idx*8+:8] = new_i[byte_idx*8+:8];
|
||||
end
|
||||
endfunction
|
||||
|
||||
task automatic raw_write(input logic half_i, input logic [12:0] addr_i, input logic [31:0] data_i,
|
||||
input logic [3:0] strb_i);
|
||||
@(negedge clk_i);
|
||||
raw_write_valid_i = 1'b1;
|
||||
raw_write_half_i = half_i;
|
||||
raw_write_addr_i = addr_i;
|
||||
raw_write_data_i = data_i;
|
||||
raw_write_strb_i = strb_i;
|
||||
@(negedge clk_i);
|
||||
raw_write_valid_i = 1'b0;
|
||||
raw_write_strb_i = 4'd0;
|
||||
endtask
|
||||
|
||||
task automatic raw_read_check(input logic half_i, input logic [12:0] addr_i,
|
||||
input logic [31:0] expected_i);
|
||||
@(negedge clk_i);
|
||||
raw_read_valid_i = 1'b1;
|
||||
raw_read_half_i = half_i;
|
||||
raw_read_addr_i = addr_i;
|
||||
@(negedge clk_i);
|
||||
raw_read_valid_i = 1'b0;
|
||||
if (raw_read_data_o !== expected_i) begin
|
||||
$fatal(1, "raw read half=%0d addr=%h data=%h expected=%h", half_i, addr_i, raw_read_data_o,
|
||||
expected_i);
|
||||
end
|
||||
checks = checks + 1;
|
||||
endtask
|
||||
|
||||
task automatic pack_write(input logic sel_w_i, input logic half_i, input logic [12:0] addr_i,
|
||||
input logic [63:0] data_i, input logic [7:0] strb_i);
|
||||
@(negedge clk_i);
|
||||
pack_write_valid_i = 1'b1;
|
||||
pack_write_sel_w_i = sel_w_i;
|
||||
pack_write_half_i = half_i;
|
||||
pack_write_addr_i = addr_i;
|
||||
pack_write_data_i = data_i;
|
||||
pack_write_strb_i = strb_i;
|
||||
@(negedge clk_i);
|
||||
pack_write_valid_i = 1'b0;
|
||||
pack_write_strb_i = 8'd0;
|
||||
endtask
|
||||
|
||||
task automatic a_read_check(input logic half_i, input logic [12:0] addr_i,
|
||||
input logic [63:0] expected_i);
|
||||
@(negedge clk_i);
|
||||
a_read_valid_i = 1'b1;
|
||||
a_read_half_i = half_i;
|
||||
a_read_addr_i = addr_i;
|
||||
@(negedge clk_i);
|
||||
a_read_valid_i = 1'b0;
|
||||
if (a_read_data_o !== expected_i) begin
|
||||
$fatal(1, "A read half=%0d addr=%h data=%h expected=%h", half_i, addr_i, a_read_data_o,
|
||||
expected_i);
|
||||
end
|
||||
checks = checks + 1;
|
||||
endtask
|
||||
|
||||
task automatic w_read_check(input logic half_i, input logic [12:0] addr_i,
|
||||
input logic [63:0] expected_i);
|
||||
@(negedge clk_i);
|
||||
w_read_valid_i = 1'b1;
|
||||
w_read_half_i = half_i;
|
||||
w_read_addr_i = addr_i;
|
||||
@(negedge clk_i);
|
||||
w_read_valid_i = 1'b0;
|
||||
if (w_read_data_o !== expected_i) begin
|
||||
$fatal(1, "W read half=%0d addr=%h data=%h expected=%h", half_i, addr_i, w_read_data_o,
|
||||
expected_i);
|
||||
end
|
||||
checks = checks + 1;
|
||||
endtask
|
||||
|
||||
task automatic out_write(input logic half_i, input logic [11:0] addr_i, input logic [31:0] data_i,
|
||||
input logic [3:0] strb_i);
|
||||
@(negedge clk_i);
|
||||
out_req_valid_i = 1'b1;
|
||||
out_req_write_i = 1'b1;
|
||||
out_req_half_i = half_i;
|
||||
out_req_addr_i = addr_i;
|
||||
out_req_data_i = data_i;
|
||||
out_req_strb_i = strb_i;
|
||||
@(negedge clk_i);
|
||||
out_req_valid_i = 1'b0;
|
||||
out_req_write_i = 1'b0;
|
||||
out_req_strb_i = 4'd0;
|
||||
endtask
|
||||
|
||||
task automatic out_read_check(input logic half_i, input logic [11:0] addr_i,
|
||||
input logic [31:0] expected_i);
|
||||
@(negedge clk_i);
|
||||
out_req_valid_i = 1'b1;
|
||||
out_req_write_i = 1'b0;
|
||||
out_req_half_i = half_i;
|
||||
out_req_addr_i = addr_i;
|
||||
@(negedge clk_i);
|
||||
out_req_valid_i = 1'b0;
|
||||
if (out_read_data_o !== expected_i) begin
|
||||
$fatal(1, "out read half=%0d addr=%h data=%h expected=%h", half_i, addr_i, out_read_data_o,
|
||||
expected_i);
|
||||
end
|
||||
checks = checks + 1;
|
||||
endtask
|
||||
|
||||
task automatic param_write(input logic [12:0] addr_i, input logic [31:0] data_i,
|
||||
input logic [3:0] strb_i);
|
||||
@(negedge clk_i);
|
||||
param_write_valid_i = 1'b1;
|
||||
param_write_addr_i = addr_i;
|
||||
param_write_data_i = data_i;
|
||||
param_write_strb_i = strb_i;
|
||||
@(negedge clk_i);
|
||||
param_write_valid_i = 1'b0;
|
||||
param_write_strb_i = 4'd0;
|
||||
endtask
|
||||
|
||||
task automatic param_read_check(input logic [12:0] addr_i, input logic [31:0] expected_i);
|
||||
@(negedge clk_i);
|
||||
param_read_valid_i = 1'b1;
|
||||
param_read_addr_i = addr_i;
|
||||
@(negedge clk_i);
|
||||
param_read_valid_i = 1'b0;
|
||||
if (param_read_data_o !== expected_i) begin
|
||||
$fatal(1, "param read addr=%h data=%h expected=%h", addr_i, param_read_data_o, expected_i);
|
||||
end
|
||||
checks = checks + 1;
|
||||
endtask
|
||||
|
||||
task automatic check_err(input logic [2:0] port_i);
|
||||
if (access_err_sticky_o !== 1'b1) begin
|
||||
$fatal(1, "sticky error not raised for port %0d", port_i);
|
||||
end
|
||||
if (access_err_port_o !== port_i) begin
|
||||
$fatal(1, "error port mismatch got=%0d expected=%0d", access_err_port_o, port_i);
|
||||
end
|
||||
checks = checks + 1;
|
||||
endtask
|
||||
|
||||
task automatic sram_clear;
|
||||
@(negedge clk_i);
|
||||
clear_i = 1'b1;
|
||||
@(negedge clk_i);
|
||||
clear_i = 1'b0;
|
||||
if (access_err_sticky_o !== 1'b0) $fatal(1, "sticky error survived clear_i");
|
||||
if (access_err_port_o !== 3'd7) $fatal(1, "error port not cleared");
|
||||
checks = checks + 1;
|
||||
endtask
|
||||
|
||||
initial begin
|
||||
repeat (4) @(negedge clk_i);
|
||||
rst_n_i = 1'b1;
|
||||
@(negedge clk_i);
|
||||
if (access_err_sticky_o !== 1'b0) $fatal(1, "sticky error after reset");
|
||||
if (access_err_port_o !== 3'd7) $fatal(1, "error port not None after reset");
|
||||
|
||||
// ---- write/read every partition via its ports ----
|
||||
raw_write(1'b0, 13'h0000, 32'h10203040, 4'hf);
|
||||
raw_write(1'b0, 13'h1004, 32'h50607080, 4'hf);
|
||||
raw_write(1'b1, 13'h0004, 32'h90a0b0c0, 4'hf);
|
||||
raw_write(1'b1, 13'h1ffc, 32'hd0e0f000, 4'hf);
|
||||
raw_read_check(1'b0, 13'h0000, 32'h10203040);
|
||||
raw_read_check(1'b0, 13'h1004, 32'h50607080);
|
||||
raw_read_check(1'b1, 13'h0004, 32'h90a0b0c0);
|
||||
raw_read_check(1'b1, 13'h1ffc, 32'hd0e0f000);
|
||||
|
||||
// ---- byte strobes: every 4-bit pattern on the raw port ----
|
||||
for (int pattern = 0; pattern < 16; pattern++) begin
|
||||
raw_write(1'b0, 13'h0040, 32'haabbccdd, 4'hf);
|
||||
raw_write(1'b0, 13'h0040, 32'h11223344, 4'(pattern));
|
||||
raw_read_check(1'b0, 13'h0040, merge32(32'haabbccdd, 32'h11223344, 4'(pattern)));
|
||||
end
|
||||
// ---- byte strobes on the packed A 64-bit port ----
|
||||
for (int pattern = 0; pattern < 256; pattern += 85) begin
|
||||
pack_write(1'b0, 1'b0, 13'h0080, 64'haabbccdd11223344, 8'hff);
|
||||
pack_write(1'b0, 1'b0, 13'h0080, 64'h55667788990abcde, 8'(pattern));
|
||||
a_read_check(1'b0, 13'h0080, merge64(64'haabbccdd11223344, 64'h55667788990abcde, 8'(pattern)
|
||||
));
|
||||
end
|
||||
|
||||
// ---- 64-bit A/W accesses ----
|
||||
pack_write(1'b0, 1'b0, 13'h0100, 64'h1122334455667788, 8'hff);
|
||||
a_read_check(1'b0, 13'h0100, 64'h1122334455667788);
|
||||
pack_write(1'b1, 1'b1, 13'h0180, 64'hdeadbeefcafebabe, 8'hff);
|
||||
w_read_check(1'b1, 13'h0180, 64'hdeadbeefcafebabe);
|
||||
|
||||
// ---- two independent A+W reads in one cycle ----
|
||||
@(negedge clk_i);
|
||||
a_read_valid_i = 1'b1;
|
||||
a_read_half_i = 1'b0;
|
||||
a_read_addr_i = 13'h0100;
|
||||
w_read_valid_i = 1'b1;
|
||||
w_read_half_i = 1'b1;
|
||||
w_read_addr_i = 13'h0180;
|
||||
@(negedge clk_i);
|
||||
a_read_valid_i = 1'b0;
|
||||
w_read_valid_i = 1'b0;
|
||||
if (a_read_data_o !== 64'h1122334455667788) $fatal(1, "concurrent A read mismatch");
|
||||
if (w_read_data_o !== 64'hdeadbeefcafebabe) $fatal(1, "concurrent W read mismatch");
|
||||
checks = checks + 1;
|
||||
|
||||
// ---- inactive-half pack write concurrent with active-half read ----
|
||||
@(negedge clk_i);
|
||||
a_read_valid_i = 1'b1;
|
||||
a_read_half_i = 1'b0;
|
||||
a_read_addr_i = 13'h0100;
|
||||
pack_write_valid_i = 1'b1;
|
||||
pack_write_sel_w_i = 1'b0;
|
||||
pack_write_half_i = 1'b1;
|
||||
pack_write_addr_i = 13'h0200;
|
||||
pack_write_data_i = 64'h0123456789abcdef;
|
||||
pack_write_strb_i = 8'hff;
|
||||
@(negedge clk_i);
|
||||
a_read_valid_i = 1'b0;
|
||||
pack_write_valid_i = 1'b0;
|
||||
pack_write_strb_i = 8'd0;
|
||||
if (a_read_data_o !== 64'h1122334455667788) $fatal(1, "cross-half concurrent read mismatch");
|
||||
if (access_err_sticky_o !== 1'b0) $fatal(1, "legal cross-half access flagged");
|
||||
a_read_check(1'b1, 13'h0200, 64'h0123456789abcdef);
|
||||
checks = checks + 1;
|
||||
|
||||
// ---- output staging both halves ----
|
||||
out_write(1'b0, 12'h000, 32'h11112222, 4'hf);
|
||||
out_write(1'b1, 12'hffc, 32'h33334444, 4'hf);
|
||||
out_read_check(1'b0, 12'h000, 32'h11112222);
|
||||
out_read_check(1'b1, 12'hffc, 32'h33334444);
|
||||
|
||||
// ---- parameter region both banks ----
|
||||
param_write(13'h0000, 32'h55556666, 4'hf);
|
||||
param_write(13'h1004, 32'h77778888, 4'hf);
|
||||
param_read_check(13'h0000, 32'h55556666);
|
||||
param_read_check(13'h1004, 32'h77778888);
|
||||
|
||||
// ---- explicit synchronous-read latency probe ----
|
||||
raw_write(1'b0, 13'h0020, 32'h5555aaaa, 4'hf);
|
||||
@(negedge clk_i);
|
||||
raw_read_valid_i = 1'b1;
|
||||
raw_read_half_i = 1'b0;
|
||||
raw_read_addr_i = 13'h0020;
|
||||
#1;
|
||||
if (raw_read_data_o === 32'h5555aaaa) $fatal(1, "raw read is not synchronous");
|
||||
@(negedge clk_i);
|
||||
raw_read_valid_i = 1'b0;
|
||||
if (raw_read_data_o !== 32'h5555aaaa) $fatal(1, "raw read latency not one cycle");
|
||||
checks = checks + 1;
|
||||
|
||||
// ---- invalid-word read errors on every read port ----
|
||||
sram_clear;
|
||||
@(negedge clk_i);
|
||||
raw_read_valid_i = 1'b1;
|
||||
raw_read_half_i = 1'b1;
|
||||
raw_read_addr_i = 13'h0800;
|
||||
@(negedge clk_i);
|
||||
raw_read_valid_i = 1'b0;
|
||||
check_err(3'd1);
|
||||
sram_clear;
|
||||
@(negedge clk_i);
|
||||
a_read_valid_i = 1'b1;
|
||||
a_read_half_i = 1'b0;
|
||||
a_read_addr_i = 13'h0800;
|
||||
@(negedge clk_i);
|
||||
a_read_valid_i = 1'b0;
|
||||
check_err(3'd3);
|
||||
sram_clear;
|
||||
@(negedge clk_i);
|
||||
w_read_valid_i = 1'b1;
|
||||
w_read_half_i = 1'b1;
|
||||
w_read_addr_i = 13'h1000;
|
||||
@(negedge clk_i);
|
||||
w_read_valid_i = 1'b0;
|
||||
check_err(3'd4);
|
||||
sram_clear;
|
||||
@(negedge clk_i);
|
||||
out_req_valid_i = 1'b1;
|
||||
out_req_write_i = 1'b0;
|
||||
out_req_half_i = 1'b1;
|
||||
out_req_addr_i = 12'h400;
|
||||
@(negedge clk_i);
|
||||
out_req_valid_i = 1'b0;
|
||||
check_err(3'd5);
|
||||
sram_clear;
|
||||
@(negedge clk_i);
|
||||
param_read_valid_i = 1'b1;
|
||||
param_read_addr_i = 13'h1800;
|
||||
@(negedge clk_i);
|
||||
param_read_valid_i = 1'b0;
|
||||
check_err(3'd6);
|
||||
sram_clear;
|
||||
|
||||
// ---- misaligned 64-bit accesses ----
|
||||
@(negedge clk_i);
|
||||
a_read_valid_i = 1'b1;
|
||||
a_read_half_i = 1'b0;
|
||||
a_read_addr_i = 13'h0004;
|
||||
@(negedge clk_i);
|
||||
a_read_valid_i = 1'b0;
|
||||
check_err(3'd3);
|
||||
sram_clear;
|
||||
@(negedge clk_i);
|
||||
w_read_valid_i = 1'b1;
|
||||
w_read_half_i = 1'b0;
|
||||
w_read_addr_i = 13'h0001;
|
||||
@(negedge clk_i);
|
||||
w_read_valid_i = 1'b0;
|
||||
check_err(3'd4);
|
||||
sram_clear;
|
||||
// misaligned pack write is suppressed and flagged
|
||||
pack_write(1'b0, 1'b0, 13'h0040, 64'haaaabbbbccccdddd, 8'hff);
|
||||
@(negedge clk_i);
|
||||
pack_write_valid_i = 1'b1;
|
||||
pack_write_sel_w_i = 1'b0;
|
||||
pack_write_half_i = 1'b0;
|
||||
pack_write_addr_i = 13'h0042;
|
||||
pack_write_data_i = 64'h1111111111111111;
|
||||
pack_write_strb_i = 8'hff;
|
||||
@(negedge clk_i);
|
||||
pack_write_valid_i = 1'b0;
|
||||
pack_write_strb_i = 8'd0;
|
||||
check_err(3'd2);
|
||||
a_read_check(1'b0, 13'h0040, 64'haaaabbbbccccdddd);
|
||||
sram_clear;
|
||||
|
||||
// ---- raw same-half conflict (same bank): read wins, write dropped ----
|
||||
raw_write(1'b0, 13'h0100, 32'haaaa0001, 4'hf);
|
||||
raw_write(1'b0, 13'h0104, 32'haaaa0002, 4'hf);
|
||||
@(negedge clk_i);
|
||||
raw_write_valid_i = 1'b1;
|
||||
raw_write_half_i = 1'b0;
|
||||
raw_write_addr_i = 13'h0100;
|
||||
raw_write_data_i = 32'h99999999;
|
||||
raw_write_strb_i = 4'hf;
|
||||
raw_read_valid_i = 1'b1;
|
||||
raw_read_half_i = 1'b0;
|
||||
raw_read_addr_i = 13'h0100;
|
||||
@(negedge clk_i);
|
||||
raw_write_valid_i = 1'b0;
|
||||
raw_write_strb_i = 4'd0;
|
||||
raw_read_valid_i = 1'b0;
|
||||
if (raw_read_data_o !== 32'haaaa0001) $fatal(1, "raw read did not win the conflict");
|
||||
check_err(3'd0);
|
||||
raw_read_check(1'b0, 13'h0100, 32'haaaa0001);
|
||||
sram_clear;
|
||||
|
||||
// ---- raw same-half conflict (different banks): still an error ----
|
||||
raw_write(1'b0, 13'h0000, 32'hbbbb0001, 4'hf);
|
||||
raw_write(1'b0, 13'h1000, 32'hbbbb0002, 4'hf);
|
||||
@(negedge clk_i);
|
||||
raw_write_valid_i = 1'b1;
|
||||
raw_write_half_i = 1'b0;
|
||||
raw_write_addr_i = 13'h1000;
|
||||
raw_write_data_i = 32'h88888888;
|
||||
raw_write_strb_i = 4'hf;
|
||||
raw_read_valid_i = 1'b1;
|
||||
raw_read_half_i = 1'b0;
|
||||
raw_read_addr_i = 13'h0000;
|
||||
@(negedge clk_i);
|
||||
raw_write_valid_i = 1'b0;
|
||||
raw_write_strb_i = 4'd0;
|
||||
raw_read_valid_i = 1'b0;
|
||||
if (raw_read_data_o !== 32'hbbbb0001) $fatal(1, "raw read did not win cross-bank conflict");
|
||||
check_err(3'd0);
|
||||
raw_read_check(1'b0, 13'h1000, 32'hbbbb0002);
|
||||
sram_clear;
|
||||
|
||||
// ---- pack write vs a_read same-half conflict ----
|
||||
pack_write(1'b0, 1'b0, 13'h0080, 64'hcccc000000000001, 8'hff);
|
||||
@(negedge clk_i);
|
||||
pack_write_valid_i = 1'b1;
|
||||
pack_write_sel_w_i = 1'b0;
|
||||
pack_write_half_i = 1'b0;
|
||||
pack_write_addr_i = 13'h0080;
|
||||
pack_write_data_i = 64'h7777777777777777;
|
||||
pack_write_strb_i = 8'hff;
|
||||
a_read_valid_i = 1'b1;
|
||||
a_read_half_i = 1'b0;
|
||||
a_read_addr_i = 13'h0080;
|
||||
@(negedge clk_i);
|
||||
pack_write_valid_i = 1'b0;
|
||||
pack_write_strb_i = 8'd0;
|
||||
a_read_valid_i = 1'b0;
|
||||
if (a_read_data_o !== 64'hcccc000000000001) $fatal(1, "A read did not win the conflict");
|
||||
check_err(3'd2);
|
||||
a_read_check(1'b0, 13'h0080, 64'hcccc000000000001);
|
||||
sram_clear;
|
||||
|
||||
// ---- pack write vs w_read same-half conflict ----
|
||||
pack_write(1'b1, 1'b1, 13'h0100, 64'hdddd000000000002, 8'hff);
|
||||
@(negedge clk_i);
|
||||
pack_write_valid_i = 1'b1;
|
||||
pack_write_sel_w_i = 1'b1;
|
||||
pack_write_half_i = 1'b1;
|
||||
pack_write_addr_i = 13'h0100;
|
||||
pack_write_data_i = 64'h6666666666666666;
|
||||
pack_write_strb_i = 8'hff;
|
||||
w_read_valid_i = 1'b1;
|
||||
w_read_half_i = 1'b1;
|
||||
w_read_addr_i = 13'h0100;
|
||||
@(negedge clk_i);
|
||||
pack_write_valid_i = 1'b0;
|
||||
pack_write_strb_i = 8'd0;
|
||||
w_read_valid_i = 1'b0;
|
||||
if (w_read_data_o !== 64'hdddd000000000002) $fatal(1, "W read did not win the conflict");
|
||||
check_err(3'd2);
|
||||
w_read_check(1'b1, 13'h0100, 64'hdddd000000000002);
|
||||
sram_clear;
|
||||
|
||||
// ---- first failing port is sticky until clear_i ----
|
||||
@(negedge clk_i);
|
||||
raw_read_valid_i = 1'b1;
|
||||
raw_read_half_i = 1'b0;
|
||||
raw_read_addr_i = 13'h0400;
|
||||
@(negedge clk_i);
|
||||
raw_read_valid_i = 1'b0;
|
||||
check_err(3'd1);
|
||||
@(negedge clk_i);
|
||||
a_read_valid_i = 1'b1;
|
||||
a_read_half_i = 1'b0;
|
||||
a_read_addr_i = 13'h0400;
|
||||
@(negedge clk_i);
|
||||
a_read_valid_i = 1'b0;
|
||||
if (access_err_port_o !== 3'd1) $fatal(1, "first failing port not retained");
|
||||
checks = checks + 1;
|
||||
sram_clear;
|
||||
|
||||
// ---- clear_i invalidates previously written words ----
|
||||
raw_write(1'b0, 13'h0300, 32'hdddd0001, 4'hf);
|
||||
raw_read_check(1'b0, 13'h0300, 32'hdddd0001);
|
||||
sram_clear;
|
||||
@(negedge clk_i);
|
||||
raw_read_valid_i = 1'b1;
|
||||
raw_read_half_i = 1'b0;
|
||||
raw_read_addr_i = 13'h0300;
|
||||
@(negedge clk_i);
|
||||
raw_read_valid_i = 1'b0;
|
||||
check_err(3'd1);
|
||||
sram_clear;
|
||||
|
||||
$display("NPU_LOCAL_SRAM_PASS checks=%0d", checks);
|
||||
$finish;
|
||||
end
|
||||
|
||||
initial begin
|
||||
#5000000;
|
||||
$fatal(1, "npu_local_sram_tb timeout");
|
||||
end
|
||||
endmodule
|
||||
@@ -0,0 +1,151 @@
|
||||
// Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
|
||||
// SPDX-License-Identifier: MulanPSL-2.0
|
||||
//
|
||||
// Vector file format (flat 32-bit hex words, 5 + 64 words per item, generated
|
||||
// by tests/test_npu_rtl_compute.py):
|
||||
// word 0: activation bytes lanes 3..0 (lane p occupies byte p)
|
||||
// word 1: activation bytes lanes 7..4
|
||||
// word 2: weight bytes lanes 3..0
|
||||
// word 3: weight bytes lanes 7..4
|
||||
// word 4: {8'h00, in_zero[7:0], a_valid[7:0], w_valid[7:0]}
|
||||
// words 5..68: expected signed 17-bit products, sign-extended signed 32,
|
||||
// in position-major order (index = position*8 + channel)
|
||||
|
||||
`timescale 1ns / 1ps
|
||||
|
||||
module npu_mac_array_tb;
|
||||
localparam int unsigned MaxVectorCount = 256;
|
||||
localparam int unsigned WordsPerItem = 5 + 64;
|
||||
|
||||
logic clk_i = 1'b0;
|
||||
logic rst_n_i = 1'b0;
|
||||
logic req_valid;
|
||||
logic req_ready;
|
||||
logic [ 7:0][ 7:0] req_a_bytes;
|
||||
logic [ 7:0][ 7:0] req_w_bytes;
|
||||
logic signed [ 7:0] req_in_zero;
|
||||
logic [ 7:0] req_a_valid;
|
||||
logic [ 7:0] req_w_valid;
|
||||
logic resp_valid;
|
||||
logic resp_ready;
|
||||
logic [ 7:0][ 7:0][16:0] resp_products;
|
||||
|
||||
logic [31:0] vectors [0:MaxVectorCount*WordsPerItem-1];
|
||||
logic [63:0][16:0] products_flat;
|
||||
integer vector_count;
|
||||
string vector_path;
|
||||
integer stall_count;
|
||||
integer error_count;
|
||||
|
||||
always #5 clk_i = ~clk_i;
|
||||
|
||||
assign products_flat = resp_products;
|
||||
|
||||
npu_mac_array u_dut (
|
||||
.clk_i (clk_i),
|
||||
.rst_n_i (rst_n_i),
|
||||
.req_valid_i (req_valid),
|
||||
.req_ready_o (req_ready),
|
||||
.req_a_bytes_i (req_a_bytes),
|
||||
.req_w_bytes_i (req_w_bytes),
|
||||
.req_in_zero_i (req_in_zero),
|
||||
.req_a_valid_i (req_a_valid),
|
||||
.req_w_valid_i (req_w_valid),
|
||||
.resp_valid_o (resp_valid),
|
||||
.resp_ready_i (resp_ready),
|
||||
.resp_products_o(resp_products)
|
||||
);
|
||||
|
||||
// Deterministic response backpressure: ready is low in two-cycle stall
|
||||
// stretches inside a seven-cycle period.
|
||||
always @(posedge clk_i or negedge rst_n_i) begin
|
||||
if (!rst_n_i) begin
|
||||
stall_count <= 0;
|
||||
resp_ready <= 1'b0;
|
||||
end else begin
|
||||
stall_count <= (stall_count == 6) ? 0 : stall_count + 1;
|
||||
resp_ready <= (stall_count < 2) || (stall_count > 3);
|
||||
end
|
||||
end
|
||||
|
||||
task automatic hard_reset;
|
||||
begin
|
||||
rst_n_i = 1'b0;
|
||||
repeat (3) @(posedge clk_i);
|
||||
rst_n_i = 1'b1;
|
||||
repeat (2) @(posedge clk_i);
|
||||
end
|
||||
endtask
|
||||
|
||||
initial begin
|
||||
integer base;
|
||||
vector_count = 0;
|
||||
if (!$value$plusargs("VECTOR_COUNT=%d", vector_count)) begin
|
||||
$fatal(1, "NPU mac array vector count missing");
|
||||
end
|
||||
if (!$value$plusargs("VECTORS=%s", vector_path)) begin
|
||||
$fatal(1, "NPU mac array vector path missing");
|
||||
end
|
||||
if ((vector_count < 1) || (vector_count > MaxVectorCount)) begin
|
||||
$fatal(1, "NPU mac array vector count out of range count=%0d", vector_count);
|
||||
end
|
||||
$readmemh(vector_path, vectors);
|
||||
|
||||
req_valid = 1'b0;
|
||||
req_a_bytes = '0;
|
||||
req_w_bytes = '0;
|
||||
req_in_zero = 8'd0;
|
||||
req_a_valid = 8'd0;
|
||||
req_w_valid = 8'd0;
|
||||
error_count = 0;
|
||||
|
||||
hard_reset();
|
||||
|
||||
for (int item = 0; item < vector_count; item++) begin
|
||||
base = item * WordsPerItem;
|
||||
// Deterministic request idle gaps, zero to two cycles.
|
||||
repeat (item % 3) @(posedge clk_i);
|
||||
@(negedge clk_i);
|
||||
req_a_bytes = {vectors[base+1], vectors[base+0]};
|
||||
req_w_bytes = {vectors[base+3], vectors[base+2]};
|
||||
req_in_zero = vectors[base+4][23:16];
|
||||
req_a_valid = vectors[base+4][15:8];
|
||||
req_w_valid = vectors[base+4][7:0];
|
||||
req_valid = 1'b1;
|
||||
for (int cycle = 0; cycle < 200; cycle++) begin
|
||||
@(posedge clk_i);
|
||||
if (req_ready) begin
|
||||
@(negedge clk_i);
|
||||
req_valid = 1'b0;
|
||||
break;
|
||||
end
|
||||
if (cycle == 199) begin
|
||||
$fatal(1, "NPU mac array request timeout item=%0d", item);
|
||||
end
|
||||
end
|
||||
for (int cycle = 0; cycle < 200; cycle++) begin
|
||||
@(posedge clk_i);
|
||||
if (resp_valid && resp_ready) begin
|
||||
for (int index = 0; index < 64; index++) begin
|
||||
if (products_flat[index] !== vectors[base+5+index][16:0]) begin
|
||||
$display("NPU mac array mismatch item=%0d index=%0d got=%05x expected=%05x", item,
|
||||
index, products_flat[index], vectors[base+5+index][16:0]);
|
||||
error_count = error_count + 1;
|
||||
end
|
||||
end
|
||||
break;
|
||||
end
|
||||
if (cycle == 199) begin
|
||||
$fatal(1, "NPU mac array response timeout item=%0d", item);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
repeat (2) @(posedge clk_i);
|
||||
if (error_count != 0) begin
|
||||
$fatal(1, "NPU mac array mismatches=%0d", error_count);
|
||||
end
|
||||
$display("NPU mac array test passed");
|
||||
$finish;
|
||||
end
|
||||
endmodule
|
||||
@@ -0,0 +1,142 @@
|
||||
`timescale 1ns / 1ps
|
||||
|
||||
// Vector-driven unit test for npu_patch_packer. Raw contents and expected
|
||||
// packed rows come from $readmemh files selected by plusargs; the raw read
|
||||
// port is modeled behaviorally with one-cycle synchronous latency and the
|
||||
// packed write port is captured and compared row by row.
|
||||
module npu_patch_packer_tb;
|
||||
logic clk_i = 1'b0;
|
||||
logic rst_n_i = 1'b0;
|
||||
logic start_valid_i = 1'b0;
|
||||
logic start_ready_o;
|
||||
npu_pkg::pack_mode_e start_mode_i = npu_pkg::PackDense;
|
||||
logic [ 10:0] start_k_i = 11'd1;
|
||||
logic [ 3:0] start_positions_i = 4'd1;
|
||||
logic [ 3:0] start_channels_i = 4'd1;
|
||||
logic signed [ 7:0] start_in_zero_i = 8'd0;
|
||||
logic busy_o;
|
||||
logic done_pulse_o;
|
||||
logic raw_read_valid_o;
|
||||
logic [ 12:0] raw_read_addr_o;
|
||||
logic [ 31:0] raw_read_data_i;
|
||||
logic pack_write_valid_o;
|
||||
logic [ 12:0] pack_write_addr_o;
|
||||
logic [ 63:0] pack_write_data_o;
|
||||
logic [ 7:0] pack_write_strb_o;
|
||||
|
||||
logic [ 31:0] raw_mem [0:2047];
|
||||
logic [ 31:0] raw_data_q = 32'd0;
|
||||
logic [ 63:0] expected_mem [0:1023];
|
||||
logic [ 63:0] actual_mem [0:1023];
|
||||
logic [1023:0] written = 1024'd0;
|
||||
integer write_count = 0;
|
||||
integer done_count = 0;
|
||||
integer mode = 0;
|
||||
integer k_value = 1;
|
||||
integer m_value = 1;
|
||||
integer channels_value = 1;
|
||||
integer in_zero_value = 0;
|
||||
integer rows = 0;
|
||||
string raw_path;
|
||||
string expected_path;
|
||||
|
||||
always #5 clk_i = ~clk_i;
|
||||
|
||||
npu_patch_packer u_npu_patch_packer (
|
||||
.clk_i (clk_i),
|
||||
.rst_n_i (rst_n_i),
|
||||
.start_valid_i (start_valid_i),
|
||||
.start_ready_o (start_ready_o),
|
||||
.start_mode_i (start_mode_i),
|
||||
.start_k_i (start_k_i),
|
||||
.start_positions_i (start_positions_i),
|
||||
.start_channels_i (start_channels_i),
|
||||
.start_in_zero_i (start_in_zero_i),
|
||||
.busy_o (busy_o),
|
||||
.done_pulse_o (done_pulse_o),
|
||||
.raw_read_valid_o (raw_read_valid_o),
|
||||
.raw_read_addr_o (raw_read_addr_o),
|
||||
.raw_read_data_i (raw_read_data_i),
|
||||
.pack_write_valid_o(pack_write_valid_o),
|
||||
.pack_write_addr_o (pack_write_addr_o),
|
||||
.pack_write_data_o (pack_write_data_o),
|
||||
.pack_write_strb_o (pack_write_strb_o)
|
||||
);
|
||||
|
||||
// Raw gather half behavioral model: one-cycle synchronous read.
|
||||
assign raw_read_data_i = raw_data_q;
|
||||
always_ff @(posedge clk_i) begin
|
||||
if (raw_read_valid_o) begin
|
||||
raw_data_q <= raw_mem[raw_read_addr_o[12:2]];
|
||||
end
|
||||
end
|
||||
|
||||
// Packed write capture with protocol checks.
|
||||
always_ff @(posedge clk_i) begin
|
||||
if (pack_write_valid_o) begin
|
||||
if (pack_write_strb_o !== 8'hff) $fatal(1, "pack write strobe not full");
|
||||
if (pack_write_addr_o[2:0] != 3'd0) $fatal(1, "pack write misaligned");
|
||||
if (written[pack_write_addr_o[12:3]]) $fatal(1, "duplicate pack write row");
|
||||
written[pack_write_addr_o[12:3]] <= 1'b1;
|
||||
actual_mem[pack_write_addr_o[12:3]] <= pack_write_data_o;
|
||||
write_count <= write_count + 1;
|
||||
end
|
||||
if (done_pulse_o) begin
|
||||
done_count <= done_count + 1;
|
||||
end
|
||||
end
|
||||
|
||||
initial begin
|
||||
integer cycles;
|
||||
if (!$value$plusargs("MODE=%d", mode)) $fatal(1, "MODE plusarg missing");
|
||||
if (!$value$plusargs("K=%d", k_value)) $fatal(1, "K plusarg missing");
|
||||
if (!$value$plusargs("M=%d", m_value)) $fatal(1, "M plusarg missing");
|
||||
if (!$value$plusargs("CHANNELS=%d", channels_value)) $fatal(1, "CHANNELS plusarg missing");
|
||||
if (!$value$plusargs("IN_ZERO=%d", in_zero_value)) $fatal(1, "IN_ZERO plusarg missing");
|
||||
if (!$value$plusargs("ROWS=%d", rows)) $fatal(1, "ROWS plusarg missing");
|
||||
if (!$value$plusargs("RAW=%s", raw_path)) $fatal(1, "RAW plusarg missing");
|
||||
if (!$value$plusargs("EXPECTED=%s", expected_path)) $fatal(1, "EXPECTED plusarg missing");
|
||||
$readmemh(raw_path, raw_mem);
|
||||
$readmemh(expected_path, expected_mem);
|
||||
|
||||
repeat (4) @(negedge clk_i);
|
||||
rst_n_i = 1'b1;
|
||||
@(negedge clk_i);
|
||||
if (start_ready_o !== 1'b1) $fatal(1, "start_ready low in Idle");
|
||||
if (busy_o !== 1'b0) $fatal(1, "busy high in Idle");
|
||||
|
||||
start_valid_i = 1'b1;
|
||||
start_mode_i = (mode == 0) ? npu_pkg::PackDense : npu_pkg::PackDepthwise;
|
||||
start_k_i = 11'(k_value);
|
||||
start_positions_i = 4'(m_value);
|
||||
start_channels_i = 4'(channels_value);
|
||||
start_in_zero_i = 8'(in_zero_value);
|
||||
@(negedge clk_i);
|
||||
start_valid_i = 1'b0;
|
||||
if (busy_o !== 1'b1) $fatal(1, "busy not raised after start");
|
||||
if (start_ready_o !== 1'b0) $fatal(1, "start_ready high while running");
|
||||
|
||||
cycles = 0;
|
||||
while ((done_pulse_o !== 1'b1) && (cycles < 400000)) begin
|
||||
@(negedge clk_i);
|
||||
cycles = cycles + 1;
|
||||
end
|
||||
if (done_pulse_o !== 1'b1) $fatal(1, "packer timeout after %0d cycles", cycles);
|
||||
@(negedge clk_i);
|
||||
if (busy_o !== 1'b0) $fatal(1, "busy stuck after done");
|
||||
if (done_count != 1) $fatal(1, "done pulse count %0d != 1", done_count);
|
||||
if (start_ready_o !== 1'b1) $fatal(1, "start_ready not restored");
|
||||
if (write_count != rows) $fatal(1, "row count %0d != expected %0d", write_count, rows);
|
||||
for (int row = 0; row < rows; row++) begin
|
||||
if (written[row] !== 1'b1) $fatal(1, "row %0d never written", row);
|
||||
if (actual_mem[row] !== expected_mem[row]) begin
|
||||
$fatal(1, "row %0d mismatch actual=%h expected=%h", row, actual_mem[row],
|
||||
expected_mem[row]);
|
||||
end
|
||||
end
|
||||
|
||||
$display("NPU_PATCH_PACKER_PASS mode=%0d k=%0d m=%0d ch=%0d zero=%0d rows=%0d cycles=%0d",
|
||||
mode, k_value, m_value, channels_value, in_zero_value, rows, cycles);
|
||||
$finish;
|
||||
end
|
||||
endmodule
|
||||
@@ -0,0 +1,171 @@
|
||||
// Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
|
||||
// SPDX-License-Identifier: MulanPSL-2.0
|
||||
//
|
||||
// Vector file format (flat 32-bit hex words, eight words per item, generated
|
||||
// by tests/test_npu_rtl_compute.py from scripts/npu_reference.py):
|
||||
// word 0: acc (signed INT32 accumulator input)
|
||||
// word 1: multiplier (nonnegative INT32)
|
||||
// word 2: shift (sign-extended signed 32, low 8 bits used, range -31..30)
|
||||
// word 3: zout (sign-extended signed 32, low 8 bits used, range -128..127)
|
||||
// word 4: act_min (sign-extended signed 32, low 8 bits used)
|
||||
// word 5: act_max (sign-extended signed 32, low 8 bits used)
|
||||
// word 6: expected output byte, sign-extended signed 32 (0 on fault items)
|
||||
// word 7: expected fault flag (0 or 1)
|
||||
|
||||
`timescale 1ns / 1ps
|
||||
|
||||
module npu_requantizer_tb;
|
||||
localparam int unsigned MaxVectorCount = 512;
|
||||
localparam int unsigned WordsPerItem = 8;
|
||||
|
||||
logic clk_i = 1'b0;
|
||||
logic rst_n_i = 1'b0;
|
||||
logic req_valid;
|
||||
logic req_ready;
|
||||
logic signed [31:0] req_acc;
|
||||
logic [31:0] req_multiplier;
|
||||
logic signed [ 7:0] req_shift;
|
||||
logic signed [ 7:0] req_zout;
|
||||
logic signed [ 7:0] req_act_min;
|
||||
logic signed [ 7:0] req_act_max;
|
||||
logic resp_valid;
|
||||
logic resp_ready;
|
||||
logic signed [ 7:0] resp_data;
|
||||
logic resp_fault;
|
||||
|
||||
logic [31:0] vectors [0:MaxVectorCount*WordsPerItem-1];
|
||||
integer vector_count;
|
||||
string vector_path;
|
||||
integer recv_count;
|
||||
integer error_count;
|
||||
integer stall_count;
|
||||
|
||||
always #5 clk_i = ~clk_i;
|
||||
|
||||
npu_requantizer u_dut (
|
||||
.clk_i (clk_i),
|
||||
.rst_n_i (rst_n_i),
|
||||
.req_valid_i (req_valid),
|
||||
.req_ready_o (req_ready),
|
||||
.req_acc_i (req_acc),
|
||||
.req_multiplier_i(req_multiplier),
|
||||
.req_shift_i (req_shift),
|
||||
.req_zout_i (req_zout),
|
||||
.req_act_min_i (req_act_min),
|
||||
.req_act_max_i (req_act_max),
|
||||
.resp_valid_o (resp_valid),
|
||||
.resp_ready_i (resp_ready),
|
||||
.resp_data_o (resp_data),
|
||||
.resp_fault_o (resp_fault)
|
||||
);
|
||||
|
||||
// Deterministic response backpressure: ready is low in three-cycle stall
|
||||
// stretches inside an eleven-cycle period.
|
||||
always @(posedge clk_i or negedge rst_n_i) begin
|
||||
if (!rst_n_i) begin
|
||||
stall_count <= 0;
|
||||
resp_ready <= 1'b0;
|
||||
end else begin
|
||||
stall_count <= (stall_count == 10) ? 0 : stall_count + 1;
|
||||
resp_ready <= (stall_count < 4) || (stall_count > 6);
|
||||
end
|
||||
end
|
||||
|
||||
// In-order response checker: one item retires per handshake.
|
||||
always @(posedge clk_i) begin
|
||||
if (rst_n_i && resp_valid && resp_ready) begin
|
||||
if (recv_count >= vector_count) begin
|
||||
$fatal(1, "NPU requantizer published more items than sent");
|
||||
end
|
||||
if ((resp_data !== vectors[recv_count*WordsPerItem+6][7:0]) ||
|
||||
(resp_fault !== vectors[recv_count*WordsPerItem+7][0])) begin
|
||||
$display(
|
||||
"NPU requantizer mismatch item=%0d got data=%08x fault=%0d expected data=%08x fault=%08x",
|
||||
recv_count, resp_data, resp_fault, vectors[recv_count*WordsPerItem+6],
|
||||
vectors[recv_count*WordsPerItem+7]);
|
||||
error_count <= error_count + 1;
|
||||
end
|
||||
recv_count <= recv_count + 1;
|
||||
end
|
||||
end
|
||||
|
||||
task automatic hard_reset;
|
||||
begin
|
||||
rst_n_i = 1'b0;
|
||||
repeat (3) @(posedge clk_i);
|
||||
rst_n_i = 1'b1;
|
||||
repeat (2) @(posedge clk_i);
|
||||
end
|
||||
endtask
|
||||
|
||||
initial begin
|
||||
vector_count = 0;
|
||||
if (!$value$plusargs("VECTOR_COUNT=%d", vector_count)) begin
|
||||
$fatal(1, "NPU requantizer vector count missing");
|
||||
end
|
||||
if (!$value$plusargs("VECTORS=%s", vector_path)) begin
|
||||
$fatal(1, "NPU requantizer vector path missing");
|
||||
end
|
||||
if ((vector_count < 1) || (vector_count > MaxVectorCount)) begin
|
||||
$fatal(1, "NPU requantizer vector count out of range count=%0d", vector_count);
|
||||
end
|
||||
$readmemh(vector_path, vectors);
|
||||
|
||||
req_valid = 1'b0;
|
||||
req_acc = 32'd0;
|
||||
req_multiplier = 32'd0;
|
||||
req_shift = 8'd0;
|
||||
req_zout = 8'd0;
|
||||
req_act_min = 8'd0;
|
||||
req_act_max = 8'd0;
|
||||
recv_count = 0;
|
||||
error_count = 0;
|
||||
|
||||
hard_reset();
|
||||
|
||||
for (int item = 0; item < vector_count; item++) begin
|
||||
// Deterministic request idle gaps, zero to three cycles.
|
||||
repeat (item % 4) @(posedge clk_i);
|
||||
@(negedge clk_i);
|
||||
req_acc = vectors[item*WordsPerItem+0];
|
||||
req_multiplier = vectors[item*WordsPerItem+1];
|
||||
req_shift = vectors[item*WordsPerItem+2][7:0];
|
||||
req_zout = vectors[item*WordsPerItem+3][7:0];
|
||||
req_act_min = vectors[item*WordsPerItem+4][7:0];
|
||||
req_act_max = vectors[item*WordsPerItem+5][7:0];
|
||||
req_valid = 1'b1;
|
||||
for (int cycle = 0; cycle < 200; cycle++) begin
|
||||
@(posedge clk_i);
|
||||
if (req_ready) begin
|
||||
@(negedge clk_i);
|
||||
req_valid = 1'b0;
|
||||
break;
|
||||
end
|
||||
if (cycle == 199) begin
|
||||
$fatal(1, "NPU requantizer request timeout item=%0d", item);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for (int cycle = 0; cycle < 20000; cycle++) begin
|
||||
@(posedge clk_i);
|
||||
if (recv_count == vector_count) begin
|
||||
break;
|
||||
end
|
||||
if (cycle == 19999) begin
|
||||
$fatal(1, "NPU requantizer response timeout sent=%0d received=%0d", vector_count,
|
||||
recv_count);
|
||||
end
|
||||
end
|
||||
repeat (2) @(posedge clk_i);
|
||||
if (recv_count != vector_count) begin
|
||||
$fatal(1, "NPU requantizer item count mismatch sent=%0d received=%0d", vector_count,
|
||||
recv_count);
|
||||
end
|
||||
if (error_count != 0) begin
|
||||
$fatal(1, "NPU requantizer mismatches=%0d", error_count);
|
||||
end
|
||||
$display("NPU requantizer test passed");
|
||||
$finish;
|
||||
end
|
||||
endmodule
|
||||
@@ -0,0 +1,301 @@
|
||||
`timescale 1ns / 1ps
|
||||
|
||||
// Scripted unit test for npu_vector: bias preload, lane-masked checked
|
||||
// accumulation against TB-computed golden sums, exactly-once drain with
|
||||
// backpressure, scalar aux max/sum (including all-negative and ties), aux and
|
||||
// accumulation overflow faults, and clear_i release.
|
||||
module npu_vector_tb;
|
||||
logic clk_i = 1'b0;
|
||||
logic rst_n_i = 1'b0;
|
||||
logic clear_i = 1'b0;
|
||||
logic start_valid_i = 1'b0;
|
||||
logic start_ready_o;
|
||||
logic [ 7:0][31:0] start_bias_i = '0;
|
||||
logic dw_valid_i = 1'b0;
|
||||
logic dw_ready_o;
|
||||
logic [ 7:0][ 7:0] dw_a_bytes_i = '0;
|
||||
logic [ 7:0][ 7:0] dw_w_bytes_i = '0;
|
||||
logic signed [ 7:0] dw_in_zero_i = 8'd0;
|
||||
logic [ 7:0] dw_lane_valid_i = 8'd0;
|
||||
logic aux_valid_i = 1'b0;
|
||||
logic [ 1:0] aux_op_i = 2'd0;
|
||||
logic signed [ 7:0] aux_data_i = 8'd0;
|
||||
logic drain_valid_i = 1'b0;
|
||||
logic drain_data_valid_o;
|
||||
logic drain_data_ready_i = 1'b0;
|
||||
logic signed [31:0] drain_data_o;
|
||||
logic drain_last_o;
|
||||
logic signed [31:0] aux_result_o;
|
||||
logic busy_o;
|
||||
logic fault_sticky_o;
|
||||
logic [ 2:0] fault_lane_o;
|
||||
integer checks = 0;
|
||||
logic signed [63:0] tb_sum [0:7];
|
||||
|
||||
always #5 clk_i = ~clk_i;
|
||||
|
||||
npu_vector u_npu_vector (
|
||||
.clk_i (clk_i),
|
||||
.rst_n_i (rst_n_i),
|
||||
.clear_i (clear_i),
|
||||
.start_valid_i (start_valid_i),
|
||||
.start_ready_o (start_ready_o),
|
||||
.start_bias_i (start_bias_i),
|
||||
.dw_valid_i (dw_valid_i),
|
||||
.dw_ready_o (dw_ready_o),
|
||||
.dw_a_bytes_i (dw_a_bytes_i),
|
||||
.dw_w_bytes_i (dw_w_bytes_i),
|
||||
.dw_in_zero_i (dw_in_zero_i),
|
||||
.dw_lane_valid_i (dw_lane_valid_i),
|
||||
.aux_valid_i (aux_valid_i),
|
||||
.aux_op_i (aux_op_i),
|
||||
.aux_data_i (aux_data_i),
|
||||
.drain_valid_i (drain_valid_i),
|
||||
.drain_data_valid_o(drain_data_valid_o),
|
||||
.drain_data_ready_i(drain_data_ready_i),
|
||||
.drain_data_o (drain_data_o),
|
||||
.drain_last_o (drain_last_o),
|
||||
.aux_result_o (aux_result_o),
|
||||
.busy_o (busy_o),
|
||||
.fault_sticky_o (fault_sticky_o),
|
||||
.fault_lane_o (fault_lane_o)
|
||||
);
|
||||
|
||||
task automatic vec_start(input logic signed [31:0] bias_i[0:7]);
|
||||
@(negedge clk_i);
|
||||
start_valid_i = 1'b1;
|
||||
for (int lane = 0; lane < 8; lane++) begin
|
||||
start_bias_i[lane] = bias_i[lane];
|
||||
// Explicit sign extension: sv2v drops task-argument signedness.
|
||||
tb_sum[lane] = {{32{bias_i[lane][31]}}, bias_i[lane]};
|
||||
end
|
||||
@(negedge clk_i);
|
||||
start_valid_i = 1'b0;
|
||||
if (busy_o !== 1'b1) $fatal(1, "busy not set after start");
|
||||
if (dw_ready_o !== 1'b1) $fatal(1, "dw_ready not set after start");
|
||||
if (aux_result_o !== 32'd0) $fatal(1, "aux sum register not cleared at start");
|
||||
checks = checks + 1;
|
||||
endtask
|
||||
|
||||
task automatic dw_row(input logic [63:0] a_i, input logic [63:0] w_i,
|
||||
input logic signed [7:0] zero_i, input logic [7:0] lanes_i);
|
||||
logic signed [ 8:0] a9;
|
||||
logic signed [ 7:0] w8;
|
||||
logic signed [31:0] prod;
|
||||
@(negedge clk_i);
|
||||
dw_valid_i = 1'b1;
|
||||
dw_a_bytes_i = a_i;
|
||||
dw_w_bytes_i = w_i;
|
||||
dw_in_zero_i = zero_i;
|
||||
dw_lane_valid_i = lanes_i;
|
||||
@(negedge clk_i);
|
||||
dw_valid_i = 1'b0;
|
||||
dw_lane_valid_i = 8'd0;
|
||||
for (int lane = 0; lane < 8; lane++) begin
|
||||
if (lanes_i[lane]) begin
|
||||
a9 = 9'($signed(a_i[lane*8+:8])) - 9'($signed(zero_i));
|
||||
w8 = $signed(w_i[lane*8+:8]);
|
||||
prod = a9 * w8;
|
||||
tb_sum[lane] = tb_sum[lane] + prod;
|
||||
if ((tb_sum[lane] > 64'sd2147483647) || (tb_sum[lane] < -64'sd2147483648)) begin
|
||||
$fatal(1, "TB golden overflow lane %0d", lane);
|
||||
end
|
||||
end
|
||||
end
|
||||
checks = checks + 1;
|
||||
endtask
|
||||
|
||||
// Drives a row that must raise the sticky fault: the DUT freezes the sums,
|
||||
// so the golden model intentionally does not update.
|
||||
task automatic dw_row_fault(input logic [63:0] a_i, input logic [63:0] w_i,
|
||||
input logic signed [7:0] zero_i, input logic [7:0] lanes_i);
|
||||
@(negedge clk_i);
|
||||
dw_valid_i = 1'b1;
|
||||
dw_a_bytes_i = a_i;
|
||||
dw_w_bytes_i = w_i;
|
||||
dw_in_zero_i = zero_i;
|
||||
dw_lane_valid_i = lanes_i;
|
||||
@(negedge clk_i);
|
||||
dw_valid_i = 1'b0;
|
||||
dw_lane_valid_i = 8'd0;
|
||||
checks = checks + 1;
|
||||
endtask
|
||||
|
||||
task automatic drain_check;
|
||||
integer idx;
|
||||
logic stalled;
|
||||
@(negedge clk_i);
|
||||
drain_valid_i = 1'b1;
|
||||
@(negedge clk_i);
|
||||
drain_valid_i = 1'b0;
|
||||
if (drain_data_valid_o !== 1'b1) $fatal(1, "drain did not start");
|
||||
idx = 0;
|
||||
stalled = 1'b0;
|
||||
while (idx < 8) begin
|
||||
if (drain_data_o !== 32'(tb_sum[idx])) begin
|
||||
$fatal(1, "drain data mismatch lane=%0d got=%h expected=%h", idx, drain_data_o,
|
||||
32'(tb_sum[idx]));
|
||||
end
|
||||
if (drain_last_o !== (idx == 7)) $fatal(1, "drain_last mismatch at beat %0d", idx);
|
||||
if ((idx % 3 == 1) && !stalled) begin
|
||||
drain_data_ready_i = 1'b0;
|
||||
@(negedge clk_i);
|
||||
if (drain_data_valid_o !== 1'b1) $fatal(1, "drain valid dropped during stall");
|
||||
if (drain_data_o !== 32'(tb_sum[idx])) $fatal(1, "drain data changed during stall");
|
||||
stalled = 1'b1;
|
||||
end else begin
|
||||
drain_data_ready_i = 1'b1;
|
||||
stalled = 1'b0;
|
||||
idx = idx + 1;
|
||||
@(negedge clk_i);
|
||||
end
|
||||
end
|
||||
drain_data_ready_i = 1'b0;
|
||||
if (drain_data_valid_o !== 1'b0) $fatal(1, "drain did not complete");
|
||||
if (busy_o !== 1'b0) $fatal(1, "context not freed after final drain handshake");
|
||||
if (start_ready_o !== 1'b1) $fatal(1, "start_ready not restored after drain");
|
||||
checks = checks + 1;
|
||||
endtask
|
||||
|
||||
task automatic aux_op(input logic [1:0] op_i, input logic signed [7:0] data_i);
|
||||
@(negedge clk_i);
|
||||
aux_valid_i = 1'b1;
|
||||
aux_op_i = op_i;
|
||||
aux_data_i = data_i;
|
||||
@(negedge clk_i);
|
||||
aux_valid_i = 1'b0;
|
||||
aux_op_i = 2'd0;
|
||||
checks = checks + 1;
|
||||
endtask
|
||||
|
||||
task automatic aux_check(input logic signed [31:0] expected_i);
|
||||
if (aux_result_o !== expected_i) begin
|
||||
$fatal(1, "aux result mismatch got=%0d expected=%0d", aux_result_o, expected_i);
|
||||
end
|
||||
checks = checks + 1;
|
||||
endtask
|
||||
|
||||
task automatic vec_clear;
|
||||
@(negedge clk_i);
|
||||
clear_i = 1'b1;
|
||||
@(negedge clk_i);
|
||||
clear_i = 1'b0;
|
||||
if (busy_o !== 1'b0) $fatal(1, "busy stuck after clear_i");
|
||||
if (fault_sticky_o !== 1'b0) $fatal(1, "fault stuck after clear_i");
|
||||
if (start_ready_o !== 1'b1) $fatal(1, "start_ready low after clear_i");
|
||||
if (aux_result_o !== 32'd0) $fatal(1, "aux not cleared by clear_i");
|
||||
checks = checks + 1;
|
||||
endtask
|
||||
|
||||
initial begin
|
||||
logic signed [31:0] bias[0:7];
|
||||
repeat (4) @(negedge clk_i);
|
||||
rst_n_i = 1'b1;
|
||||
@(negedge clk_i);
|
||||
if (start_ready_o !== 1'b1) $fatal(1, "start_ready low after reset");
|
||||
if (busy_o !== 1'b0) $fatal(1, "busy high after reset");
|
||||
if (fault_sticky_o !== 1'b0) $fatal(1, "fault high after reset");
|
||||
if (dw_ready_o !== 1'b0) $fatal(1, "dw_ready high in Idle");
|
||||
if (drain_data_valid_o !== 1'b0) $fatal(1, "drain valid in Idle");
|
||||
|
||||
// ---- scalar aux in Idle: max with negatives and ties, then sum ----
|
||||
aux_op(2'd1, 8'sd3);
|
||||
aux_check(32'd3);
|
||||
aux_op(2'd1, -8'sd5);
|
||||
aux_check(32'd3);
|
||||
aux_op(2'd1, 8'sd3);
|
||||
aux_check(32'd3);
|
||||
aux_op(2'd1, -8'sd128);
|
||||
aux_check(32'd3);
|
||||
aux_op(2'd1, 8'sd7);
|
||||
aux_check(32'd7);
|
||||
aux_op(2'd0, 8'sd100);
|
||||
aux_check(32'd7);
|
||||
aux_op(2'd2, 8'sd10);
|
||||
aux_check(32'd10);
|
||||
aux_op(2'd2, -8'sd20);
|
||||
aux_check(-32'd10);
|
||||
aux_op(2'd2, 8'sd100);
|
||||
aux_check(32'd90);
|
||||
|
||||
// ---- bias preload and lane-masked accumulation ----
|
||||
bias = '{-32'd5, 32'd0, 32'd100, -32'd1000, 32'd7, 32'd64, -32'd1, 32'd255};
|
||||
vec_start(bias);
|
||||
if (aux_result_o !== 32'd0) $fatal(1, "start did not re-initialize aux");
|
||||
dw_row(64'h0102030405060708, 64'h0202020202020202, 8'sd0, 8'hff);
|
||||
dw_row(64'h7f80817f0080807f, 64'h8080808080808080, 8'sd0, 8'ha5);
|
||||
dw_row(64'h0000000000000000, 64'h7f7f7f7f7f7f7f7f, -8'sd128, 8'h0f);
|
||||
dw_row(64'hffffffffffffffff, 64'h0101010101010101, 8'sd127, 8'h00);
|
||||
dw_row(64'h8080808080808080, 64'h8181818181818181, 8'sd127, 8'hff);
|
||||
drain_check;
|
||||
|
||||
// ---- second context: extreme centering and single-lane rows ----
|
||||
bias = '{32'd0, -32'd32640, 32'd32640, 32'd1, -32'd1, 32'd1000000, -32'd999, 32'd42};
|
||||
vec_start(bias);
|
||||
dw_row(64'h0000000000000080, 64'h0000000000000080, 8'sd127, 8'h80);
|
||||
dw_row(64'h000000000000007f, 64'h0000000000000080, -8'sd128, 8'h01);
|
||||
drain_check;
|
||||
|
||||
// ---- accumulation overflow: single faulting lane ----
|
||||
bias = '{32'd0, 32'd0, 32'd0, 32'h7ffffff0, 32'd0, 32'd0, 32'd0, 32'd0};
|
||||
vec_start(bias);
|
||||
dw_row_fault(64'h0000000064000000, 64'h0000000064000000, 8'sd0, 8'h08);
|
||||
if (fault_sticky_o !== 1'b1) $fatal(1, "accumulation overflow fault missing");
|
||||
if (fault_lane_o !== 3'd3) $fatal(1, "fault lane %0d != 3", fault_lane_o);
|
||||
if (busy_o !== 1'b1) $fatal(1, "fault context not busy");
|
||||
if (dw_ready_o !== 1'b0) $fatal(1, "dw_ready high in Fault");
|
||||
@(negedge clk_i);
|
||||
drain_valid_i = 1'b1;
|
||||
@(negedge clk_i);
|
||||
drain_valid_i = 1'b0;
|
||||
if (drain_data_valid_o !== 1'b0) $fatal(1, "drain accepted in Fault");
|
||||
vec_clear;
|
||||
|
||||
// ---- accumulation overflow: lowest of two faulting lanes wins ----
|
||||
bias = '{32'd0, 32'h7fffffff, 32'd0, 32'd0, 32'd0, 32'h7ffffffe, 32'd0, 32'd0};
|
||||
vec_start(bias);
|
||||
dw_row_fault(64'h0000010000000100, 64'h0000020000000100, 8'sd0, 8'h22);
|
||||
if (fault_sticky_o !== 1'b1) $fatal(1, "two-lane overflow fault missing");
|
||||
if (fault_lane_o !== 3'd1) $fatal(1, "lowest fault lane %0d != 1", fault_lane_o);
|
||||
vec_clear;
|
||||
|
||||
// ---- aux sum positive overflow: dedicated fault-lane encoding ----
|
||||
bias = '{32'd0, 32'd0, 32'd0, 32'd0, 32'd0, 32'd0, 32'd0, 32'd0};
|
||||
vec_start(bias);
|
||||
aux_op(2'd2, 8'sd127);
|
||||
aux_check(32'd127);
|
||||
force u_npu_vector.s_aux_sum_q = 32'h7ffffff8;
|
||||
@(negedge clk_i);
|
||||
release u_npu_vector.s_aux_sum_q;
|
||||
aux_op(2'd2, 8'sd127);
|
||||
if (fault_sticky_o !== 1'b1) $fatal(1, "aux positive overflow fault missing");
|
||||
if (fault_lane_o !== 3'd7) $fatal(1, "aux fault lane encoding %0d != 7", fault_lane_o);
|
||||
vec_clear;
|
||||
|
||||
// ---- aux sum negative overflow ----
|
||||
vec_start(bias);
|
||||
force u_npu_vector.s_aux_sum_q = -32'h7ffffff8;
|
||||
@(negedge clk_i);
|
||||
release u_npu_vector.s_aux_sum_q;
|
||||
aux_op(2'd2, -8'sd128);
|
||||
if (fault_sticky_o !== 1'b1) $fatal(1, "aux negative overflow fault missing");
|
||||
if (fault_lane_o !== 3'd7) $fatal(1, "aux fault lane encoding %0d != 7", fault_lane_o);
|
||||
vec_clear;
|
||||
|
||||
// ---- drain request in Idle is ignored ----
|
||||
@(negedge clk_i);
|
||||
drain_valid_i = 1'b1;
|
||||
@(negedge clk_i);
|
||||
drain_valid_i = 1'b0;
|
||||
if (drain_data_valid_o !== 1'b0) $fatal(1, "drain accepted in Idle");
|
||||
if (busy_o !== 1'b0) $fatal(1, "Idle drain request disturbed the context");
|
||||
|
||||
$display("NPU_VECTOR_PASS checks=%0d", checks);
|
||||
$finish;
|
||||
end
|
||||
|
||||
initial begin
|
||||
#2000000;
|
||||
$fatal(1, "npu_vector_tb timeout");
|
||||
end
|
||||
endmodule
|
||||
@@ -0,0 +1,344 @@
|
||||
"""NPU-P1 compute-core RTL differential tests on Icarus Verilog and Verilator.
|
||||
|
||||
Golden vectors are generated at test time from the pinned numerical oracle in
|
||||
scripts/npu_reference.py, so the RTL is checked bit-exactly against numerical
|
||||
profile 1. Both simulators must pass; missing tools fail the test loudly.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
|
||||
from npu_reference import ( # noqa: E402
|
||||
INT32_MAX,
|
||||
INT32_MIN,
|
||||
ArithmeticFault,
|
||||
multiply_by_quantized_multiplier,
|
||||
)
|
||||
|
||||
MULTIMEDIA = ROOT / "rtl/ip/multimedia"
|
||||
NPU_PKG = MULTIMEDIA / "npu_pkg.sv"
|
||||
MAC_TB = ROOT / "tests/rtl/npu_mac_array_tb.sv"
|
||||
ACC_TB = ROOT / "tests/rtl/npu_accumulator_tb.sv"
|
||||
REQ_TB = ROOT / "tests/rtl/npu_requantizer_tb.sv"
|
||||
|
||||
MAC_PASS_MARKER = "NPU mac array test passed"
|
||||
ACC_PASS_MARKER = "NPU accumulator test passed"
|
||||
REQ_PASS_MARKER = "NPU requantizer test passed"
|
||||
|
||||
|
||||
def _to_int8(value: int) -> int:
|
||||
return value - 256 if value >= 128 else value
|
||||
|
||||
|
||||
def _require_tools() -> tuple[str, str, str, str]:
|
||||
tools = {
|
||||
"iverilog": shutil.which("iverilog"),
|
||||
"vvp": shutil.which("vvp"),
|
||||
"sv2v": shutil.which("sv2v"),
|
||||
"verilator": shutil.which("verilator"),
|
||||
}
|
||||
missing = [name for name, path in tools.items() if path is None]
|
||||
if missing:
|
||||
pytest.fail(f"NPU RTL compute tests require missing tools: {', '.join(missing)}")
|
||||
return tools["iverilog"], tools["vvp"], tools["sv2v"], tools["verilator"]
|
||||
|
||||
|
||||
def _run_icarus(
|
||||
tmp_path: Path,
|
||||
iverilog: str,
|
||||
vvp: str,
|
||||
sv2v: str,
|
||||
top: str,
|
||||
sources: list[Path],
|
||||
plusargs: list[str],
|
||||
) -> str:
|
||||
del sv2v # convt_sv2v.py resolves the executable itself
|
||||
source_list = tmp_path / f"{top}.fl"
|
||||
source_list.write_text(
|
||||
"\n".join(
|
||||
["+define+SV_ASSRT_DISABLE", *(str(source) for source in sources), ""]
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
converted = tmp_path / f"{top}.v"
|
||||
subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(ROOT / "rtl/mini/script/convt_sv2v.py"),
|
||||
"-f",
|
||||
str(source_list),
|
||||
"--output",
|
||||
str(converted),
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
simulation = tmp_path / f"{top}_iverilog"
|
||||
subprocess.run(
|
||||
[iverilog, "-g2012", "-s", top, "-o", str(simulation), str(converted)],
|
||||
check=True,
|
||||
)
|
||||
result = subprocess.run(
|
||||
[vvp, str(simulation), *plusargs],
|
||||
check=True,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
)
|
||||
return result.stdout
|
||||
|
||||
|
||||
def _run_verilator(
|
||||
tmp_path: Path, verilator: str, top: str, sources: list[Path], plusargs: list[str]
|
||||
) -> str:
|
||||
object_dir = tmp_path / f"{top}_obj"
|
||||
ccache_dir = tmp_path / f"{top}_ccache"
|
||||
ccache_dir.mkdir()
|
||||
subprocess.run(
|
||||
[
|
||||
verilator,
|
||||
"--binary",
|
||||
"--timing",
|
||||
"-Wno-fatal",
|
||||
"--top-module",
|
||||
top,
|
||||
"-Mdir",
|
||||
str(object_dir),
|
||||
"-o",
|
||||
"simv",
|
||||
*(str(source) for source in sources),
|
||||
],
|
||||
check=True,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
env={
|
||||
**os.environ,
|
||||
"CCACHE_DIR": str(ccache_dir),
|
||||
"CCACHE_TEMPDIR": str(ccache_dir),
|
||||
},
|
||||
)
|
||||
result = subprocess.run(
|
||||
[str(object_dir / "simv"), *plusargs],
|
||||
check=True,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
)
|
||||
return result.stdout
|
||||
|
||||
|
||||
def _run_dual(
|
||||
tmp_path: Path, top: str, rtl: list[Path], plusargs: list[str], marker: str
|
||||
) -> None:
|
||||
iverilog, vvp, sv2v, verilator = _require_tools()
|
||||
sources = [NPU_PKG, *rtl]
|
||||
icarus_stdout = _run_icarus(
|
||||
tmp_path, iverilog, vvp, sv2v, top, sources + [ROOT / "tests/rtl" / f"{top}.sv"], plusargs
|
||||
)
|
||||
assert marker in icarus_stdout, f"Icarus run of {top} did not pass"
|
||||
verilator_stdout = _run_verilator(
|
||||
tmp_path, verilator, top, sources + [ROOT / "tests/rtl" / f"{top}.sv"], plusargs
|
||||
)
|
||||
assert marker in verilator_stdout, f"Verilator run of {top} did not pass"
|
||||
|
||||
|
||||
def _mac_expected(
|
||||
a_bytes: list[int], w_bytes: list[int], in_zero: int, a_valid: int, w_valid: int
|
||||
) -> list[int]:
|
||||
products = []
|
||||
for position in range(8):
|
||||
centered = _to_int8(a_bytes[position]) - in_zero
|
||||
for channel in range(8):
|
||||
if ((a_valid >> position) & 1) and ((w_valid >> channel) & 1):
|
||||
products.append(centered * _to_int8(w_bytes[channel]))
|
||||
else:
|
||||
products.append(0)
|
||||
return products
|
||||
|
||||
|
||||
def _mac_vectors(tmp_path: Path) -> tuple[Path, int]:
|
||||
"""Write 5 + 64 words per item; see tests/rtl/npu_mac_array_tb.sv for the format."""
|
||||
rng = random.Random(0x9C1A55)
|
||||
items: list[tuple[list[int], list[int], int, int, int]] = []
|
||||
directed = [
|
||||
([0x00] * 8, [0x00] * 8, 0, 0xFF, 0xFF),
|
||||
([0xFF] * 8, [0xFF] * 8, 0, 0xFF, 0xFF),
|
||||
([0xFF] * 8, [0xFF] * 8, -128, 0xFF, 0xFF),
|
||||
([0x00] * 8, [0x80] * 8, 127, 0xFF, 0xFF),
|
||||
([0x00] * 8, [0x7F] * 8, -128, 0xFF, 0xFF),
|
||||
([0x80] * 8, [0x7F] * 8, 127, 0xFF, 0xFF),
|
||||
([0x7F] * 8, [0x80] * 8, -128, 0xFF, 0xFF),
|
||||
([0x53] * 8, [0xD6] * 8, 83, 0xFF, 0xFF),
|
||||
([0x2C] * 8, [0x19] * 8, 83, 0xFF, 0xFF),
|
||||
([0x11, 0xEE, 0x5A, 0xA5, 0x00, 0xFF, 0x7F, 0x80],
|
||||
[0x01, 0xFE, 0x40, 0xC0, 0x10, 0xF0, 0x08, 0x88], 0, 0xFF, 0xFF),
|
||||
([0x40] * 8, [0x40] * 8, 0, 0x00, 0xFF),
|
||||
([0x40] * 8, [0x40] * 8, 0, 0xFF, 0x00),
|
||||
([0x40] * 8, [0x40] * 8, 0, 0x00, 0x00),
|
||||
([0x55] * 8, [0x2A] * 8, -5, 0x01, 0xFF),
|
||||
([0x55] * 8, [0x2A] * 8, -5, 0x7F, 0xFF),
|
||||
([0x55] * 8, [0x2A] * 8, -5, 0xFF, 0x01),
|
||||
([0x55] * 8, [0x2A] * 8, -5, 0xFF, 0x7F),
|
||||
([0x55] * 8, [0x2A] * 8, -5, 0x80, 0x80),
|
||||
([0x55] * 8, [0x2A] * 8, -5, 0xAA, 0x55),
|
||||
([0x55] * 8, [0x2A] * 8, -5, 0xFF, 0x09),
|
||||
]
|
||||
items.extend(directed)
|
||||
for _ in range(40):
|
||||
items.append(
|
||||
(
|
||||
[rng.randrange(256) for _ in range(8)],
|
||||
[rng.randrange(256) for _ in range(8)],
|
||||
rng.randint(-128, 127),
|
||||
rng.randrange(256),
|
||||
rng.randrange(256),
|
||||
)
|
||||
)
|
||||
|
||||
words: list[int] = []
|
||||
for a_bytes, w_bytes, in_zero, a_valid, w_valid in items:
|
||||
a_packed = sum(byte << (8 * lane) for lane, byte in enumerate(a_bytes))
|
||||
w_packed = sum(byte << (8 * lane) for lane, byte in enumerate(w_bytes))
|
||||
flags = ((in_zero & 0xFF) << 16) | (a_valid << 8) | w_valid
|
||||
words.append(a_packed & 0xFFFFFFFF)
|
||||
words.append((a_packed >> 32) & 0xFFFFFFFF)
|
||||
words.append(w_packed & 0xFFFFFFFF)
|
||||
words.append((w_packed >> 32) & 0xFFFFFFFF)
|
||||
words.append(flags)
|
||||
for product in _mac_expected(a_bytes, w_bytes, in_zero, a_valid, w_valid):
|
||||
words.append(product & 0xFFFFFFFF)
|
||||
|
||||
vector_path = tmp_path / "npu_mac_array_vectors.hex"
|
||||
vector_path.write_text("\n".join(f"{word:08x}" for word in words) + "\n", encoding="utf-8")
|
||||
return vector_path, len(items)
|
||||
|
||||
|
||||
def _requantizer_expected(
|
||||
acc: int, multiplier: int, shift: int, zout: int, act_min: int, act_max: int
|
||||
) -> tuple[int, int]:
|
||||
try:
|
||||
quantized = multiply_by_quantized_multiplier(acc, multiplier, shift)
|
||||
except ArithmeticFault:
|
||||
return 0, 1
|
||||
return max(act_min, min(act_max, quantized + zout)), 0
|
||||
|
||||
|
||||
def _requantizer_vectors(tmp_path: Path) -> tuple[Path, int]:
|
||||
"""Write 8 words per item; see tests/rtl/npu_requantizer_tb.sv for the format."""
|
||||
rng = random.Random(0x5EED1234)
|
||||
full = (-128, 127)
|
||||
cases: list[tuple[int, int, int, int, int, int]] = [
|
||||
# acc, multiplier, shift, zout, act_min, act_max
|
||||
(0, 1 << 30, 0, 0, *full),
|
||||
(1, 1 << 30, 0, 0, *full),
|
||||
(-1, 1 << 30, 0, 0, *full),
|
||||
(INT32_MAX, INT32_MAX, 0, 0, *full),
|
||||
(INT32_MIN, INT32_MAX, 0, 0, *full),
|
||||
(INT32_MIN, INT32_MAX, -1, 0, *full),
|
||||
(INT32_MIN, 1 << 30, 0, 0, *full),
|
||||
(12345, 0, 0, 0, *full), # flushed multiplier (m = 0)
|
||||
(-54321, 0, 0, 5, *full),
|
||||
(1000, 0x50000000, -31, 0, *full),
|
||||
(1000, 0x50000000, -1, 0, *full),
|
||||
(1000, 0x50000000, 1, 0, *full),
|
||||
(3, 0x60000000, 30, 0, *full),
|
||||
# RoundingDivideByPOT negative ties and rounding directions.
|
||||
(5, INT32_MAX, -2, 0, *full),
|
||||
(-5, INT32_MAX, -2, 0, *full),
|
||||
(7, INT32_MAX, -3, 0, *full),
|
||||
(-7, INT32_MAX, -3, 0, *full),
|
||||
(3, 1 << 29, 0, 0, *full),
|
||||
(-3, 1 << 29, 0, 0, *full),
|
||||
(1, 1 << 29, -1, 0, *full),
|
||||
(-1, 1 << 29, -1, 0, *full),
|
||||
(INT32_MIN, INT32_MAX, -31, 0, *full),
|
||||
(INT32_MAX, INT32_MAX, -31, 0, *full),
|
||||
# Checked left shift: boundaries and overflows.
|
||||
(0x40000000, 1 << 30, 2, 0, *full), # 2^30 << 2 overflows
|
||||
(INT32_MAX, 1 << 30, 1, 0, *full),
|
||||
(INT32_MIN, 1 << 30, 1, 0, *full),
|
||||
(-(1 << 30), 1 << 30, 1, 0, *full), # exactly -2^31, no fault
|
||||
(-(1 << 30), 1 << 30, 2, 0, *full), # -2^32, fault
|
||||
(1, 1 << 30, 30, 0, *full),
|
||||
(-1, 1 << 30, 30, 0, *full),
|
||||
# Output zero-point edges and activation clamps.
|
||||
(0, 1 << 30, 0, -128, *full),
|
||||
(0, 1 << 30, 0, 127, *full),
|
||||
(-200, INT32_MAX, 0, 127, *full),
|
||||
(200, INT32_MAX, 0, -128, *full),
|
||||
(100000, INT32_MAX, 0, 0, -10, 10),
|
||||
(-100000, INT32_MAX, 0, 0, -10, 10),
|
||||
(50, INT32_MAX, 0, 0, 0, 0),
|
||||
(-50, INT32_MAX, 0, 3, 3, 3),
|
||||
(0, INT32_MAX, 0, 127, -128, 0),
|
||||
(0, INT32_MAX, 0, -128, 0, 127),
|
||||
]
|
||||
for _ in range(200):
|
||||
act_low = rng.randint(-128, 127)
|
||||
act_high = rng.randint(-128, 127)
|
||||
cases.append(
|
||||
(
|
||||
rng.randint(INT32_MIN, INT32_MAX),
|
||||
rng.choice(
|
||||
[0, 1, rng.randint(0, INT32_MAX), rng.randint(1 << 29, INT32_MAX), INT32_MAX]
|
||||
),
|
||||
rng.randint(-31, 30),
|
||||
rng.randint(-128, 127),
|
||||
min(act_low, act_high),
|
||||
max(act_low, act_high),
|
||||
)
|
||||
)
|
||||
|
||||
words: list[int] = []
|
||||
for acc, multiplier, shift, zout, act_min, act_max in cases:
|
||||
expected, fault = _requantizer_expected(acc, multiplier, shift, zout, act_min, act_max)
|
||||
words.extend(
|
||||
value & 0xFFFFFFFF
|
||||
for value in (acc, multiplier, shift, zout, act_min, act_max, expected, fault)
|
||||
)
|
||||
|
||||
vector_path = tmp_path / "npu_requantizer_vectors.hex"
|
||||
vector_path.write_text("\n".join(f"{word:08x}" for word in words) + "\n", encoding="utf-8")
|
||||
return vector_path, len(cases)
|
||||
|
||||
|
||||
def test_npu_mac_array_dual_simulator(tmp_path: Path) -> None:
|
||||
vector_path, vector_count = _mac_vectors(tmp_path)
|
||||
_run_dual(
|
||||
tmp_path,
|
||||
"npu_mac_array_tb",
|
||||
[MULTIMEDIA / "npu_mac_array.sv"],
|
||||
[f"+VECTORS={vector_path}", f"+VECTOR_COUNT={vector_count}"],
|
||||
MAC_PASS_MARKER,
|
||||
)
|
||||
|
||||
|
||||
def test_npu_accumulator_dual_simulator(tmp_path: Path) -> None:
|
||||
_run_dual(
|
||||
tmp_path,
|
||||
"npu_accumulator_tb",
|
||||
[MULTIMEDIA / "npu_accumulator.sv"],
|
||||
[],
|
||||
ACC_PASS_MARKER,
|
||||
)
|
||||
|
||||
|
||||
def test_npu_requantizer_dual_simulator(tmp_path: Path) -> None:
|
||||
vector_path, vector_count = _requantizer_vectors(tmp_path)
|
||||
assert vector_count > 200
|
||||
_run_dual(
|
||||
tmp_path,
|
||||
"npu_requantizer_tb",
|
||||
[MULTIMEDIA / "npu_requantizer.sv"],
|
||||
[f"+VECTORS={vector_path}", f"+VECTOR_COUNT={vector_count}"],
|
||||
REQ_PASS_MARKER,
|
||||
)
|
||||
@@ -0,0 +1,290 @@
|
||||
"""NPU-P1 memory, patch packer and vector RTL unit tests (Icarus and Verilator)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
MULTIMEDIA = ROOT / "rtl/ip/multimedia"
|
||||
SRAM_MODEL_ROOT = ROOT / "physical/pdk/IHP-Open-PDK/ihp-sg13g2/libs.ref/sg13g2_sram/verilog"
|
||||
SRAM_MACRO_MODELS = [
|
||||
SRAM_MODEL_ROOT / "RM_IHPSG13_1P_core_behavioral_bm_bist.v",
|
||||
SRAM_MODEL_ROOT / "RM_IHPSG13_1P_1024x32_c2_bm_bist.v",
|
||||
]
|
||||
BEHAV_DEFINES = ("PDK_BEHAV",)
|
||||
MACRO_DEFINES = ("PDK_IHP130", "HAVE_SRAM_MACRO", "FUNCTIONAL", "SYNTHESIS")
|
||||
|
||||
|
||||
def _tools() -> dict[str, str]:
|
||||
tools: dict[str, str] = {}
|
||||
missing = []
|
||||
for name in ("iverilog", "vvp", "sv2v", "verilator"):
|
||||
path = shutil.which(name)
|
||||
if path is None:
|
||||
missing.append(name)
|
||||
else:
|
||||
tools[name] = path
|
||||
if missing:
|
||||
pytest.fail(f"required simulation tools missing: {', '.join(missing)}")
|
||||
return tools
|
||||
|
||||
|
||||
def _build_iverilog(
|
||||
tools: dict[str, str],
|
||||
tmp_path: Path,
|
||||
name: str,
|
||||
sources: list[Path],
|
||||
top: str,
|
||||
defines: tuple[str, ...],
|
||||
extra_sources: list[Path] | None = None,
|
||||
) -> Path:
|
||||
filelist = tmp_path / f"{name}.fl"
|
||||
filelist.write_text(
|
||||
"\n".join(
|
||||
[
|
||||
*(f"+define+{define}" for define in defines),
|
||||
"+define+SV_ASSRT_DISABLE",
|
||||
*(str(source) for source in sources),
|
||||
"",
|
||||
]
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
converted = tmp_path / f"{name}.v"
|
||||
subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(ROOT / "rtl/mini/script/convt_sv2v.py"),
|
||||
"-f",
|
||||
str(filelist),
|
||||
"--output",
|
||||
str(converted),
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
simulation = tmp_path / f"{name}.iverilog"
|
||||
command = [
|
||||
tools["iverilog"],
|
||||
"-g2012",
|
||||
"-s",
|
||||
top,
|
||||
"-o",
|
||||
str(simulation),
|
||||
str(converted),
|
||||
]
|
||||
if extra_sources:
|
||||
command.extend(("-DFUNCTIONAL", "-DSYNTHESIS"))
|
||||
command.extend(str(source) for source in extra_sources)
|
||||
subprocess.run(command, check=True)
|
||||
return simulation
|
||||
|
||||
|
||||
def _build_verilator(
|
||||
tools: dict[str, str],
|
||||
tmp_path: Path,
|
||||
name: str,
|
||||
sources: list[Path],
|
||||
top: str,
|
||||
defines: tuple[str, ...],
|
||||
) -> Path:
|
||||
object_dir = tmp_path / f"obj-{name}"
|
||||
ccache_dir = tmp_path / f"ccache-{name}"
|
||||
ccache_dir.mkdir()
|
||||
subprocess.run(
|
||||
[
|
||||
tools["verilator"],
|
||||
"--binary",
|
||||
"--timing",
|
||||
"-Wno-fatal",
|
||||
"--top-module",
|
||||
top,
|
||||
*(f"+define+{define}" for define in defines),
|
||||
*(str(source) for source in sources),
|
||||
"-Mdir",
|
||||
str(object_dir),
|
||||
"-o",
|
||||
"simv",
|
||||
],
|
||||
check=True,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
env={
|
||||
**os.environ,
|
||||
"CCACHE_DIR": str(ccache_dir),
|
||||
"CCACHE_TEMPDIR": str(ccache_dir),
|
||||
},
|
||||
)
|
||||
return object_dir / "simv"
|
||||
|
||||
|
||||
def _build(
|
||||
tools: dict[str, str],
|
||||
simulator: str,
|
||||
tmp_path: Path,
|
||||
name: str,
|
||||
sources: list[Path],
|
||||
top: str,
|
||||
defines: tuple[str, ...],
|
||||
extra_sources: list[Path] | None = None,
|
||||
) -> list[str]:
|
||||
if simulator == "iverilog":
|
||||
simulation = _build_iverilog(
|
||||
tools, tmp_path, name, sources, top, defines, extra_sources
|
||||
)
|
||||
return [tools["vvp"], str(simulation)]
|
||||
if simulator == "verilator":
|
||||
simv = _build_verilator(
|
||||
tools, tmp_path, name, [*(extra_sources or []), *sources], top, defines
|
||||
)
|
||||
return [str(simv)]
|
||||
raise AssertionError(f"unknown simulator {simulator}")
|
||||
|
||||
|
||||
def _run(command: list[str], plusargs: tuple[str, ...] = ()) -> str:
|
||||
result = subprocess.run(
|
||||
[*command, *plusargs], check=True, text=True, capture_output=True
|
||||
)
|
||||
return result.stdout
|
||||
|
||||
|
||||
@pytest.mark.parametrize("simulator", ("iverilog", "verilator"))
|
||||
@pytest.mark.parametrize("variant", ("behav", "macro"))
|
||||
def test_npu_local_sram(tmp_path: Path, simulator: str, variant: str) -> None:
|
||||
tools = _tools()
|
||||
for model in SRAM_MACRO_MODELS:
|
||||
assert model.is_file()
|
||||
sources = [
|
||||
MULTIMEDIA / "npu_pkg.sv",
|
||||
ROOT / "rtl/tech/tc_sram.sv",
|
||||
MULTIMEDIA / "npu_local_sram.sv",
|
||||
ROOT / "tests/rtl/npu_local_sram_tb.sv",
|
||||
]
|
||||
defines = BEHAV_DEFINES if variant == "behav" else MACRO_DEFINES
|
||||
extra = [] if variant == "behav" else SRAM_MACRO_MODELS
|
||||
command = _build(
|
||||
tools,
|
||||
simulator,
|
||||
tmp_path,
|
||||
f"npu_local_sram_{variant}",
|
||||
sources,
|
||||
"npu_local_sram_tb",
|
||||
defines,
|
||||
extra_sources=extra,
|
||||
)
|
||||
assert "NPU_LOCAL_SRAM_PASS" in _run(command)
|
||||
|
||||
|
||||
def _pack_golden(mode: int, k: int, m: int, channels: int, in_zero: int, raw: bytes) -> list[int]:
|
||||
fill = in_zero & 0xFF
|
||||
rows = []
|
||||
if mode == 0:
|
||||
assert len(raw) == m * k
|
||||
for row in range(k):
|
||||
value = 0
|
||||
for lane in range(8):
|
||||
byte = raw[lane * k + row] if lane < m else fill
|
||||
value |= byte << (8 * lane)
|
||||
rows.append(value)
|
||||
else:
|
||||
assert len(raw) == m * k * 8
|
||||
for row in range(m * k):
|
||||
value = 0
|
||||
for lane in range(8):
|
||||
byte = raw[row * 8 + lane] if lane < channels else fill
|
||||
value |= byte << (8 * lane)
|
||||
rows.append(value)
|
||||
return rows
|
||||
|
||||
|
||||
def _packer_cases() -> list[tuple[int, int, int, int, int]]:
|
||||
cases = []
|
||||
for m in (1, 7, 8):
|
||||
for k in (1, 9, 1024):
|
||||
for in_zero in (-128, 0, 83, 127):
|
||||
cases.append((0, k, m, 8, in_zero))
|
||||
for m, channels in ((1, 8), (3, 5)):
|
||||
for in_zero in (-128, 0, 83, 127):
|
||||
cases.append((1, 9, m, channels, in_zero))
|
||||
return cases
|
||||
|
||||
|
||||
def _packer_plusargs(
|
||||
tmp_path: Path, case_index: int, case: tuple[int, int, int, int, int]
|
||||
) -> tuple[str, ...]:
|
||||
mode, k, m, channels, in_zero = case
|
||||
rng = random.Random(0x5EED0000 + case_index)
|
||||
raw_byte_count = m * k if mode == 0 else m * k * 8
|
||||
raw = bytes(rng.randrange(256) for _ in range(raw_byte_count))
|
||||
words = [0] * ((raw_byte_count + 3) // 4)
|
||||
for index, byte in enumerate(raw):
|
||||
words[index // 4] |= byte << (8 * (index % 4))
|
||||
raw_path = tmp_path / f"packer_raw_{case_index}.hex"
|
||||
raw_path.write_text("\n".join(f"{word:08x}" for word in words) + "\n", encoding="utf-8")
|
||||
rows = _pack_golden(mode, k, m, channels, in_zero, raw)
|
||||
expected_path = tmp_path / f"packer_expected_{case_index}.hex"
|
||||
expected_path.write_text(
|
||||
"\n".join(f"{row:016x}" for row in rows) + "\n", encoding="utf-8"
|
||||
)
|
||||
return (
|
||||
f"+MODE={mode}",
|
||||
f"+K={k}",
|
||||
f"+M={m}",
|
||||
f"+CHANNELS={channels}",
|
||||
f"+IN_ZERO={in_zero & 0xFF}",
|
||||
f"+ROWS={len(rows)}",
|
||||
f"+RAW={raw_path}",
|
||||
f"+EXPECTED={expected_path}",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("simulator", ("iverilog", "verilator"))
|
||||
def test_npu_patch_packer(tmp_path: Path, simulator: str) -> None:
|
||||
tools = _tools()
|
||||
sources = [
|
||||
MULTIMEDIA / "npu_pkg.sv",
|
||||
MULTIMEDIA / "npu_patch_packer.sv",
|
||||
ROOT / "tests/rtl/npu_patch_packer_tb.sv",
|
||||
]
|
||||
command = _build(
|
||||
tools,
|
||||
simulator,
|
||||
tmp_path,
|
||||
"npu_patch_packer",
|
||||
sources,
|
||||
"npu_patch_packer_tb",
|
||||
BEHAV_DEFINES,
|
||||
)
|
||||
cases = _packer_cases()
|
||||
assert len(cases) == 44
|
||||
for case_index, case in enumerate(cases):
|
||||
plusargs = _packer_plusargs(tmp_path, case_index, case)
|
||||
output = _run(command, plusargs)
|
||||
assert "NPU_PATCH_PACKER_PASS" in output, f"case {case} failed"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("simulator", ("iverilog", "verilator"))
|
||||
def test_npu_vector(tmp_path: Path, simulator: str) -> None:
|
||||
tools = _tools()
|
||||
sources = [
|
||||
MULTIMEDIA / "npu_pkg.sv",
|
||||
MULTIMEDIA / "npu_vector.sv",
|
||||
ROOT / "tests/rtl/npu_vector_tb.sv",
|
||||
]
|
||||
command = _build(
|
||||
tools,
|
||||
simulator,
|
||||
tmp_path,
|
||||
"npu_vector",
|
||||
sources,
|
||||
"npu_vector_tb",
|
||||
BEHAV_DEFINES,
|
||||
)
|
||||
assert "NPU_VECTOR_PASS" in _run(command)
|
||||
Reference in new issue
Block a user