feat: add RTL unit tests for NPU-P3 job execution

This commit introduces a comprehensive set of unit tests for the NPU-P3 job execution, covering the DMA engine, job decoder, and core functionalities. The tests utilize Icarus and Verilator for simulation and include descriptor validation, fault handling, and various operational scenarios. Key features include:

- Descriptor construction and validation mirroring ABI 1.0.
- Detailed test cases for valid and invalid descriptors.
- Core testbench scenarios for various operations including convolution, pooling, and fully connected layers.
- Integration of golden model comparisons for output verification.

These tests aim to ensure robust functionality and adherence to specifications for the NPU-P3 architecture.
This commit is contained in:
maksyuki committed 2026-09-19 23:36:20 +08:00
1 parent f94cf5e7e2
commit a2b12f911a
20 files changed
+10304 -206

No files matched your search

+51 -12
View File
@@ -1,18 +1,57 @@
# 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]
# NPU isolated core STA constraints, mirroring generate_sdc.py conventions.
# Single-clock targets (P1 compute and the P3 engine modules) run at the
# 1000000/72 ps HP period regardless of the port's local name; the integrated
# shell (apb4_npu) also carries PCLK at its 48 MHz platform period and the two
# domains are grouped asynchronously. The locked OpenSTA build lacks
# remove_from_collection/filter_collection, so ports are selected by name
# iteration.
set npu_clk_ports [list]
set npu_rst_ports [list]
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"} {
if {$npu_port_name eq "clk_i" || $npu_port_name eq "clk_hp_i"} {
lappend npu_clk_ports $npu_port
} elseif {$npu_port_name eq "rst_n_i" || $npu_port_name eq "rst_hp_n_i"} {
lappend npu_rst_ports $npu_port
} else {
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]
set npu_hp_found 0
set npu_pclk_found 0
foreach npu_clk_port $npu_clk_ports {
set npu_clk_name [get_property $npu_clk_port name]
if {$npu_clk_name eq "clk_hp_i" || [llength $npu_clk_ports] == 1} {
# The 72 MHz HP fabric period (13.888889 ns).
create_clock -name npu_hp_clk -period 13.888889 $npu_clk_port
set npu_hp_found 1
} else {
# PCLK at its 48 MHz platform period (20.833333 ns).
create_clock -name npu_pclk -period 20.833333 $npu_clk_port
set npu_pclk_found 1
}
}
if {$npu_hp_found} {
set_clock_uncertainty -setup 0.2 [get_clocks npu_hp_clk]
set_clock_uncertainty -hold 0.1 [get_clocks npu_hp_clk]
set_clock_transition 0.1 [get_clocks npu_hp_clk]
}
if {$npu_pclk_found} {
set_clock_uncertainty -setup 0.2 [get_clocks npu_pclk]
set_clock_uncertainty -hold 0.1 [get_clocks npu_pclk]
set_clock_transition 0.1 [get_clocks npu_pclk]
}
if {$npu_hp_found && $npu_pclk_found} {
set_clock_groups -asynchronous -group [get_clocks npu_hp_clk] -group [get_clocks npu_pclk]
}
if {[llength $npu_rst_ports] > 0} {
set_false_path -from $npu_rst_ports
}
# Async-reset check arcs are not timed here: assertion/deassertion discipline
# is provided by the Common reset synchronizers/barriers, and this locked
# OpenSTA build does not cut RESET_B recovery arcs via set_clock_groups.
# Physical CDC/RDC signoff remains a separate commercial-delivery gap.
set_false_path -to [get_pins -hierarchical * -filter {name =~ */RESET_B}]
set_input_delay -clock npu_hp_clk 2.0 $npu_io_inputs
set_output_delay -clock npu_hp_clk 2.0 [all_outputs]
+197 -74
View File
@@ -1,12 +1,13 @@
// Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
// SPDX-License-Identifier: MulanPSL-2.0
// NPU Phase 2 top shell: the PCLK register file (npu_reg), the shell/HP
// control CDC with epoch reconciliation (npu_control_cdc), and the minimal HP
// lifecycle endpoint (npu_core). The private AXI4 master is tied safely idle:
// no AW/W/AR request is ever issued, the R/B receivers stay ready as required
// by the reserved-burst rule, and W never precedes AW by construction. The
// production DMA that will drive this master is Phase 3 scope.
// NPU top shell: the PCLK register file (npu_reg), the shell/HP control CDC
// with epoch reconciliation (npu_control_cdc), the HP job controller
// (npu_core), and the production AXI4 DMA engine (npu_dma) driving the private
// master directly (HP-native, no payload CDC or width conversion). At Phase 3
// the APB shell still rejects START (EXECUTION_READY stays 0), so the master
// issues nothing from software; the engine runs only through the production
// launch interface exercised by directed testbenches.
`include "npu_define.svh"
module apb4_npu (
@@ -74,7 +75,47 @@ module apb4_npu (
logic s_hp_busy_pclk;
logic s_hp_draining_pclk;
logic s_clock_paused;
logic s_abort_unused;
// software ABORT (PCLK pulse) crosses into the HP domain as a toggle
logic s_abort_toggle_d;
logic s_abort_toggle_q;
logic s_abort_toggle_hp;
logic s_abort_hp_d;
logic s_abort_hp_q;
logic s_abort_hp_pulse;
// discrete npu_dma engine interface of the job controller
logic s_dma_clear;
logic s_dma_read_req_valid;
logic s_dma_read_req_ready;
logic [ 31:0] s_dma_read_addr;
logic [ 31:0] s_dma_read_bytes;
logic s_dma_read_data_valid;
logic s_dma_read_data_ready;
logic [ 63:0] s_dma_read_data;
logic [ 7:0] s_dma_read_keep;
logic s_dma_read_last;
logic s_dma_write_req_valid;
logic s_dma_write_req_ready;
logic [ 31:0] s_dma_write_addr;
logic [ 31:0] s_dma_write_bytes;
logic s_dma_write_data_valid;
logic s_dma_write_data_ready;
logic [ 63:0] s_dma_write_data;
logic [ 7:0] s_dma_write_keep;
logic s_dma_write_last;
logic s_dma_write_done;
logic s_dma_busy;
logic s_dma_read_busy;
logic s_dma_write_busy;
logic s_dma_pause_ack;
logic s_dma_fault;
logic [ 3:0] s_dma_fault_code;
logic [ 31:0] s_dma_fault_addr;
logic [ 1:0] s_dma_fault_resp;
logic [ 63:0] s_dma_read_bytes_cnt;
logic [ 63:0] s_dma_write_bytes_cnt;
logic [ 63:0] s_dma_stall_cycles;
logic s_dma_read_cmd_err;
logic s_dma_write_cmd_err;
// PCLK shell idle for the resource controller: no launch slot or snapshot
// handshake pending, the link reconciled, and the HP side quiet.
@@ -159,78 +200,160 @@ module apb4_npu (
.clock_paused_o (s_clock_paused)
);
npu_core u_npu_core (
.clk_hp_i (clk_hp_i),
.rst_hp_n_i (rst_hp_n_i),
.launch_valid_i (s_hp_launch_valid),
.launch_ready_o (s_hp_launch_ready),
.launch_data_i (s_hp_launch_payload),
.result_valid_o (s_hp_result_valid),
.result_ready_i (s_hp_result_ready),
.result_data_o (s_hp_result_payload),
.snapshot_req_valid_i (s_hp_snap_req_valid),
.snapshot_req_ready_o (s_hp_snap_req_ready),
.snapshot_resp_valid_o(s_hp_snap_resp_valid),
.snapshot_resp_ready_i(s_hp_snap_resp_ready),
.snapshot_resp_data_o (s_hp_snap_resp_payload),
.epoch_req_i (s_hp_epoch_req),
.epoch_ack_o (s_hp_epoch_ack),
.quiesce_req_i (s_hp_quiesce_req),
.quiesce_ack_o (s_hp_quiesce_ack),
.busy_o (s_hp_busy),
.draining_o (s_hp_draining),
.block_new_i (hp_block_new_i),
.clock_pause_ack_o (hp_pause_ack_o),
.flush_i (hp_flush_i),
.flush_busy_o (hp_flush_busy_o),
.pause_active_o (s_hp_pause_active),
.idle_o (hp_idle_o)
npu_core #(
.ExecutionReady(1'b1)
) u_npu_core (
.clk_hp_i (clk_hp_i),
.rst_hp_n_i (rst_hp_n_i),
.launch_valid_i (s_hp_launch_valid),
.launch_ready_o (s_hp_launch_ready),
.launch_data_i (s_hp_launch_payload),
.result_valid_o (s_hp_result_valid),
.result_ready_i (s_hp_result_ready),
.result_data_o (s_hp_result_payload),
.snapshot_req_valid_i (s_hp_snap_req_valid),
.snapshot_req_ready_o (s_hp_snap_req_ready),
.snapshot_resp_valid_o (s_hp_snap_resp_valid),
.snapshot_resp_ready_i (s_hp_snap_resp_ready),
.snapshot_resp_data_o (s_hp_snap_resp_payload),
.epoch_req_i (s_hp_epoch_req),
.epoch_ack_o (s_hp_epoch_ack),
.quiesce_req_i (s_hp_quiesce_req),
.quiesce_ack_o (s_hp_quiesce_ack),
.busy_o (s_hp_busy),
.draining_o (s_hp_draining),
.block_new_i (hp_block_new_i),
.clock_pause_ack_o (hp_pause_ack_o),
.flush_i (hp_flush_i),
.flush_busy_o (hp_flush_busy_o),
.pause_active_o (s_hp_pause_active),
.idle_o (hp_idle_o),
.abort_i (s_abort_hp_pulse),
.dma_clear_o (s_dma_clear),
.dma_read_req_valid_o (s_dma_read_req_valid),
.dma_read_req_ready_i (s_dma_read_req_ready),
.dma_read_addr_o (s_dma_read_addr),
.dma_read_bytes_o (s_dma_read_bytes),
.dma_read_data_valid_i (s_dma_read_data_valid),
.dma_read_data_ready_o (s_dma_read_data_ready),
.dma_read_data_i (s_dma_read_data),
.dma_read_keep_i (s_dma_read_keep),
.dma_read_last_i (s_dma_read_last),
.dma_write_req_valid_o (s_dma_write_req_valid),
.dma_write_req_ready_i (s_dma_write_req_ready),
.dma_write_addr_o (s_dma_write_addr),
.dma_write_bytes_o (s_dma_write_bytes),
.dma_write_data_valid_o(s_dma_write_data_valid),
.dma_write_data_ready_i(s_dma_write_data_ready),
.dma_write_data_o (s_dma_write_data),
.dma_write_keep_o (s_dma_write_keep),
.dma_write_last_o (s_dma_write_last),
.dma_write_done_i (s_dma_write_done),
.dma_busy_i (s_dma_busy),
.dma_read_busy_i (s_dma_read_busy),
.dma_write_busy_i (s_dma_write_busy),
.dma_pause_ack_i (s_dma_pause_ack),
.dma_fault_i (s_dma_fault),
.dma_fault_code_i (s_dma_fault_code),
.dma_fault_addr_i (s_dma_fault_addr),
.dma_fault_resp_i (s_dma_fault_resp),
.dma_read_bytes_i (s_dma_read_bytes_cnt),
.dma_write_bytes_i (s_dma_write_bytes_cnt),
.dma_stall_cycles_i (s_dma_stall_cycles),
.dma_read_cmd_err_i (s_dma_read_cmd_err),
.dma_write_cmd_err_i (s_dma_write_cmd_err)
);
// Phase 2: the AXI4 master is tied safely idle. No request is ever issued
// and the response receivers remain ready; production DMA is Phase 3.
assign npu_axi4.awid = '0;
assign npu_axi4.awaddr = '0;
assign npu_axi4.awlen = '0;
assign npu_axi4.awsize = '0;
assign npu_axi4.awburst = '0;
assign npu_axi4.awlock = 1'b0;
assign npu_axi4.awcache = '0;
assign npu_axi4.awprot = '0;
assign npu_axi4.awqos = '0;
assign npu_axi4.awregion = '0;
assign npu_axi4.awuser = '0;
assign npu_axi4.awvalid = 1'b0;
assign npu_axi4.wdata = '0;
assign npu_axi4.wstrb = '0;
assign npu_axi4.wlast = 1'b0;
assign npu_axi4.wuser = '0;
assign npu_axi4.wvalid = 1'b0;
assign npu_axi4.bready = 1'b1;
assign npu_axi4.arid = '0;
assign npu_axi4.araddr = '0;
assign npu_axi4.arlen = '0;
assign npu_axi4.arsize = '0;
assign npu_axi4.arburst = '0;
assign npu_axi4.arlock = 1'b0;
assign npu_axi4.arcache = '0;
assign npu_axi4.arprot = '0;
assign npu_axi4.arqos = '0;
assign npu_axi4.arregion = '0;
assign npu_axi4.aruser = '0;
assign npu_axi4.arvalid = 1'b0;
assign npu_axi4.rready = 1'b1;
// The P3 AXI4 data plane: the production npu_dma engine drives the master
// port directly (HP-native, no payload CDC or width conversion). Its
// receivers are live whenever an accepted obligation exists, and presented
// but unaccepted requests stay stable through pause and abort per the
// frozen contract.
npu_dma u_npu_dma (
.clk_hp_i (clk_hp_i),
.rst_hp_n_i (rst_hp_n_i),
.clear_i (hp_flush_i || s_dma_clear),
.block_new_i (hp_block_new_i),
.pause_ack_o (s_dma_pause_ack),
.read_req_valid_i (s_dma_read_req_valid),
.read_req_ready_o (s_dma_read_req_ready),
.read_addr_i (s_dma_read_addr),
.read_bytes_i (s_dma_read_bytes),
.read_data_valid_o (s_dma_read_data_valid),
.read_data_ready_i (s_dma_read_data_ready),
.read_data_o (s_dma_read_data),
.read_keep_o (s_dma_read_keep),
.read_last_o (s_dma_read_last),
.write_req_valid_i (s_dma_write_req_valid),
.write_req_ready_o (s_dma_write_req_ready),
.write_addr_i (s_dma_write_addr),
.write_bytes_i (s_dma_write_bytes),
.write_data_valid_i(s_dma_write_data_valid),
.write_data_ready_o(s_dma_write_data_ready),
.write_data_i (s_dma_write_data),
.write_keep_i (s_dma_write_keep),
.write_last_i (s_dma_write_last),
.write_done_o (s_dma_write_done),
.busy_o (s_dma_busy),
.read_busy_o (s_dma_read_busy),
.write_busy_o (s_dma_write_busy),
.read_bytes_o (s_dma_read_bytes_cnt),
.write_bytes_o (s_dma_write_bytes_cnt),
.stall_cycles_o (s_dma_stall_cycles),
.fault_o (s_dma_fault),
.fault_code_o (s_dma_fault_code),
.fault_addr_o (s_dma_fault_addr),
.fault_resp_o (s_dma_fault_resp),
.read_cmd_err_o (s_dma_read_cmd_err),
.write_cmd_err_o (s_dma_write_cmd_err),
.axi4 (npu_axi4)
);
// The HP abort route is Phase 3 scope; the Phase 2 ABORT command on an idle
// NPU is a successful no-op and the busy path is unreachable.
assign s_abort_unused = s_abort;
// Software ABORT route: the PCLK pulse toggles a shell register that crosses
// through cdc_sync; the HP edge detector produces the abort pulse. A second
// ABORT arriving within the synchronization window is idempotent (the job is
// already cancelling), so no toggle can be lost that matters.
assign s_abort_toggle_d = s_abort ? !s_abort_toggle_q : s_abort_toggle_q;
dffr #(
.DATA_WIDTH(1)
) u_abort_toggle (
.clk_i (clk_i),
.rst_n_i(rst_n_i),
.dat_i (s_abort_toggle_d),
.dat_o (s_abort_toggle_q)
);
cdc_sync #(
.STAGE(2)
) u_abort_sync (
.clk_i (clk_hp_i),
.rst_n_i(rst_hp_n_i),
.dat_i (s_abort_toggle_q),
.dat_o (s_abort_toggle_hp)
);
assign s_abort_hp_d = s_abort_toggle_hp;
dffr #(
.DATA_WIDTH(1)
) u_abort_hp (
.clk_i (clk_hp_i),
.rst_n_i(rst_hp_n_i),
.dat_i (s_abort_hp_d),
.dat_o (s_abort_hp_q)
);
assign s_abort_hp_pulse = s_abort_toggle_hp ^ s_abort_hp_q;
`ifndef SV_ASSRT_DISABLE
always_ff @(posedge clk_hp_i) begin
if (rst_hp_n_i) begin
// The tied-off master never issues a request and stays ready to drain.
assert (!npu_axi4.awvalid && !npu_axi4.wvalid && !npu_axi4.arvalid);
assert (npu_axi4.bready && npu_axi4.rready);
logic s_p2_idle_master_q;
// The shell-level Phase 2 contract that the software-visible master stays
// silent while no job can be launched (START is still rejected at P3).
always_ff @(posedge clk_hp_i or negedge rst_hp_n_i) begin
if (!rst_hp_n_i) begin
s_p2_idle_master_q <= 1'b1;
end else begin
s_p2_idle_master_q <= !s_hp_busy;
if (s_p2_idle_master_q) begin
assert (!npu_axi4.awvalid && !npu_axi4.wvalid && !npu_axi4.arvalid);
end
end
end
`endif
+762 -100
View File
@@ -1,20 +1,45 @@
// Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
// SPDX-License-Identifier: MulanPSL-2.0
// NPU Phase 2 HP-domain lifecycle endpoint. It terminates the launch, result,
// and snapshot mailboxes of npu_control_cdc, participates in the epoch
// reconciliation handshake, and answers idle snapshots with a zero counter
// bank. Phase 2 has no jobs and no DMA: launches can never be accepted by the
// PCLK shell (START never passes), the sink below exists only to keep the
// mailbox protocol complete, no result is ever produced, and there is no AXI
// work to pause, drain, or flush.
// NPU HP-domain job execution core. With ExecutionReady=1'b0 this is exactly
// the Phase 2 lifecycle endpoint: it terminates the launch, result, and
// snapshot mailboxes of npu_control_cdc, participates in the epoch
// reconciliation handshake, answers idle snapshots with a zero counter bank,
// sinks the (shell-unreachable) launch, and produces no result. With
// ExecutionReady=1'b1 it is the Phase 3 job controller: an accepted launch
// latches {JOB_BASE, JOB_ID, TIMEOUT_CYCLES, JOB_COUNT}, resets the private
// DMA and the per-job counters, and runs npu_job_decoder (fetch + ABI-1.0
// validation) feeding npu_scheduler (transport) record by record until every
// descriptor retires (DONE), the first fault drains (ERROR), software abort
// or resource quiesce cancels (ABORTED), or a reset/flush epoch recovery
// cancels (RESET_CANCELLED, published after the fresh epoch is
// re-acknowledged). The npu_dma engine itself lives in apb4_npu (the Phase 2
// control-CDC testbench instantiates this module without an AXI fabric, so
// the engine interface is a discrete signal boundary); the P2 branch never
// references the P3 ports.
//
// Lifecycle invariants (both configurations): an epoch restart forces the
// endpoint to a safe state before re-acknowledging (at P3 the drain completes
// first: no new work, accepted transport obligations retire, presented but
// unaccepted VALID stays stable); the quiesce acknowledge is a registered
// round trip and at P3 additionally requires the terminal result latched for
// delivery; a flushed or cancelled job never reports DONE; busy_o/draining_o
// are truthful. The first-fault record follows the spec's deterministic
// same-cycle priority AXI_PROTOCOL > AXI_READ > AXI_WRITE > LOCAL_STATE >
// ARITHMETIC > DESCRIPTOR > UNSUPPORTED > RANGE > NO_PROGRESS; reset
// cancellation never overwrites a captured earlier fault.
`include "npu_define.svh"
module npu_core #(
parameter int unsigned LaunchPayloadWidth = `APB4_NPU__LAUNCH_PAYLOAD_WIDTH,
parameter int unsigned ResultPayloadWidth = `APB4_NPU__RESULT_PAYLOAD_WIDTH,
parameter int unsigned SnapshotPayloadWidth = `APB4_NPU__SNAPSHOT_PAYLOAD_WIDTH,
parameter int unsigned EpochWidth = `APB4_NPU__EPOCH_WIDTH
parameter int unsigned EpochWidth = `APB4_NPU__EPOCH_WIDTH,
// Phase 3 execution compile switch: the PCLK shell keeps START rejected at
// P3, so apb4_npu instantiates the live job controller while the software
// launch path stays closed; the P2 shell testbench elaborates the default
// (ExecutionReady=0) endpoint.
parameter bit ExecutionReady = 1'b0
) (
// verilog_format: off -- preserve the HP lifecycle boundary columns
input logic clk_hp_i,
@@ -41,19 +66,49 @@ module npu_core #(
input logic flush_i,
output logic flush_busy_o,
output logic pause_active_o,
output logic idle_o
output logic idle_o,
// Phase 3: software abort pulse (HP domain, toggle-synchronized in
// apb4_npu) and the discrete npu_dma engine interface
input logic abort_i,
output logic dma_clear_o,
output logic dma_read_req_valid_o,
input logic dma_read_req_ready_i,
output logic [31:0] dma_read_addr_o,
output logic [31:0] dma_read_bytes_o,
input logic dma_read_data_valid_i,
output logic dma_read_data_ready_o,
input logic [63:0] dma_read_data_i,
input logic [ 7:0] dma_read_keep_i,
input logic dma_read_last_i,
output logic dma_write_req_valid_o,
input logic dma_write_req_ready_i,
output logic [31:0] dma_write_addr_o,
output logic [31:0] dma_write_bytes_o,
output logic dma_write_data_valid_o,
input logic dma_write_data_ready_i,
output logic [63:0] dma_write_data_o,
output logic [ 7:0] dma_write_keep_o,
output logic dma_write_last_o,
input logic dma_write_done_i,
input logic dma_busy_i,
input logic dma_read_busy_i,
input logic dma_write_busy_i,
input logic dma_pause_ack_i,
input logic dma_fault_i,
input logic [ 3:0] dma_fault_code_i,
input logic [31:0] dma_fault_addr_i,
input logic [ 1:0] dma_fault_resp_i,
input logic [63:0] dma_read_bytes_i,
input logic [63:0] dma_write_bytes_i,
input logic [63:0] dma_stall_cycles_i,
input logic dma_read_cmd_err_i,
input logic dma_write_cmd_err_i
// verilog_format: on
);
typedef enum logic [0:0] {
SnapIdle,
SnapRespond
} state_e;
state_e s_state_d, s_state_q;
logic [EpochWidth-1:0] s_epoch_ack_d, s_epoch_ack_q;
logic s_quiesce_ack_d, s_quiesce_ack_q;
logic s_epoch_restart;
logic s_unused;
} snap_state_e;
`ifndef SYNTHESIS
initial begin
@@ -64,105 +119,712 @@ module npu_core #(
end
`endif
// Phase 2 protocol-complete sinks and constant sources: a launch is sunk
// (unreachable), no terminal result exists, the idle snapshot returns the
// zero bank with a zero job token, and no AXI activity ever needs draining.
assign launch_ready_o = 1'b1;
assign result_valid_o = 1'b0;
assign result_data_o = '0;
assign busy_o = 1'b0;
assign draining_o = 1'b0;
assign flush_busy_o = 1'b0;
assign snapshot_resp_data_o = '0;
if (!ExecutionReady) begin : gen_p2_endpoint
// Phase 2 protocol-complete sinks and constant sources: a launch is sunk
// (unreachable), no terminal result exists, the idle snapshot returns the
// zero bank with a zero job token, and no AXI activity ever needs
// draining. The P3 engine interface is tied off and its inputs are
// unread by construction.
snap_state_e s_state_d, s_state_q;
logic [EpochWidth-1:0] s_epoch_ack_d, s_epoch_ack_q;
logic s_quiesce_ack_d, s_quiesce_ack_q;
logic s_epoch_restart;
logic s_unused;
// With zero accepted AXI obligations and no internal work, the clock-pause
// acknowledge is immediate and the HP idle contribution follows the
// platform formula !block_new_i || clock_pause_ack_o.
assign clock_pause_ack_o = block_new_i;
assign pause_active_o = block_new_i && clock_pause_ack_o;
assign idle_o = !block_new_i || clock_pause_ack_o;
assign launch_ready_o = 1'b1;
assign result_valid_o = 1'b0;
assign result_data_o = '0;
assign busy_o = 1'b0;
assign draining_o = 1'b0;
assign flush_busy_o = 1'b0;
assign snapshot_resp_data_o = '0;
assign epoch_ack_o = s_epoch_ack_q;
assign quiesce_ack_o = s_quiesce_ack_q;
// With zero accepted AXI obligations and no internal work, the
// clock-pause acknowledge is immediate and the HP idle contribution
// follows the platform formula !block_new_i || clock_pause_ack_o.
assign clock_pause_ack_o = block_new_i;
assign pause_active_o = block_new_i && clock_pause_ack_o;
assign idle_o = !block_new_i || clock_pause_ack_o;
assign s_epoch_restart = (epoch_req_i != s_epoch_ack_q);
assign s_unused = ^{launch_data_i, result_ready_i, flush_i, 1'b0};
assign epoch_ack_o = s_epoch_ack_q;
assign quiesce_ack_o = s_quiesce_ack_q;
always_comb begin
s_state_d = s_state_q;
s_epoch_ack_d = s_epoch_ack_q;
s_quiesce_ack_d = quiesce_req_i;
snapshot_req_ready_o = 1'b0;
snapshot_resp_valid_o = 1'b0;
unique case (s_state_q)
SnapIdle: begin
snapshot_req_ready_o = 1'b1;
if (snapshot_req_valid_i) begin
s_state_d = SnapRespond;
assign s_epoch_restart = (epoch_req_i != s_epoch_ack_q);
assign s_unused = ^{launch_data_i, result_ready_i, flush_i, 1'b0};
assign dma_clear_o = 1'b0;
assign dma_read_req_valid_o = 1'b0;
assign dma_read_addr_o = '0;
assign dma_read_bytes_o = '0;
assign dma_read_data_ready_o = 1'b0;
assign dma_write_req_valid_o = 1'b0;
assign dma_write_addr_o = '0;
assign dma_write_bytes_o = '0;
assign dma_write_data_valid_o = 1'b0;
assign dma_write_data_o = '0;
assign dma_write_keep_o = '0;
assign dma_write_last_o = 1'b0;
always_comb begin
s_state_d = s_state_q;
s_epoch_ack_d = s_epoch_ack_q;
s_quiesce_ack_d = quiesce_req_i;
snapshot_req_ready_o = 1'b0;
snapshot_resp_valid_o = 1'b0;
unique case (s_state_q)
SnapIdle: begin
snapshot_req_ready_o = 1'b1;
if (snapshot_req_valid_i) begin
s_state_d = SnapRespond;
end
end
end
SnapRespond: begin
snapshot_resp_valid_o = 1'b1;
if (snapshot_resp_ready_i) begin
s_state_d = SnapIdle;
SnapRespond: begin
snapshot_resp_valid_o = 1'b1;
if (snapshot_resp_ready_i) begin
s_state_d = SnapIdle;
end
end
default: s_state_d = SnapIdle;
endcase
// A fresh epoch restarts the endpoint bookkeeping before it is
// acknowledged: any in-flight mailbox item is being flushed by the link
// reset held for the whole reconciliation, so the endpoint returns to
// SnapIdle first and only then publishes the new epoch.
if (s_epoch_restart) begin
s_state_d = SnapIdle;
s_epoch_ack_d = epoch_req_i;
end
default: s_state_d = SnapIdle;
endcase
// A fresh epoch restarts the endpoint bookkeeping before it is
// acknowledged: any in-flight mailbox item is being flushed by the link
// reset held for the whole reconciliation, so the endpoint returns to
// SnapIdle first and only then publishes the new epoch.
if (s_epoch_restart) begin
s_state_d = SnapIdle;
s_epoch_ack_d = epoch_req_i;
end
end
always_ff @(posedge clk_hp_i or negedge rst_hp_n_i) begin
if (!rst_hp_n_i) begin
s_state_q <= SnapIdle;
end else begin
s_state_q <= s_state_d;
always_ff @(posedge clk_hp_i or negedge rst_hp_n_i) begin
if (!rst_hp_n_i) begin
s_state_q <= SnapIdle;
end else begin
s_state_q <= s_state_d;
end
end
end
dffr #(
.DATA_WIDTH(EpochWidth)
) u_epoch_ack (
.clk_i (clk_hp_i),
.rst_n_i(rst_hp_n_i),
.dat_i (s_epoch_ack_d),
.dat_o (s_epoch_ack_q)
);
dffr #(
.DATA_WIDTH(1)
) u_quiesce_ack (
.clk_i (clk_hp_i),
.rst_n_i(rst_hp_n_i),
.dat_i (s_quiesce_ack_d),
.dat_o (s_quiesce_ack_q)
);
dffr #(
.DATA_WIDTH(EpochWidth)
) u_epoch_ack (
.clk_i (clk_hp_i),
.rst_n_i(rst_hp_n_i),
.dat_i (s_epoch_ack_d),
.dat_o (s_epoch_ack_q)
);
dffr #(
.DATA_WIDTH(1)
) u_quiesce_ack (
.clk_i (clk_hp_i),
.rst_n_i(rst_hp_n_i),
.dat_i (s_quiesce_ack_d),
.dat_o (s_quiesce_ack_q)
);
`ifndef SV_ASSRT_DISABLE
logic s_resp_wait_q;
logic s_epoch_restart_q;
logic s_resp_wait_q;
logic s_epoch_restart_q;
always_ff @(posedge clk_hp_i or negedge rst_hp_n_i) begin
if (!rst_hp_n_i) begin
s_resp_wait_q <= 1'b0;
s_epoch_restart_q <= 1'b0;
end else begin
s_resp_wait_q <= snapshot_resp_valid_o && !snapshot_resp_ready_i;
s_epoch_restart_q <= s_epoch_restart;
// The snapshot response holds valid and its payload stable until the
// mailbox accepts it; only an epoch restart may withdraw it.
if (s_resp_wait_q) begin
assert ((snapshot_resp_valid_o && $stable(snapshot_resp_data_o)) || s_epoch_restart_q);
always_ff @(posedge clk_hp_i or negedge rst_hp_n_i) begin
if (!rst_hp_n_i) begin
s_resp_wait_q <= 1'b0;
s_epoch_restart_q <= 1'b0;
end else begin
s_resp_wait_q <= snapshot_resp_valid_o && !snapshot_resp_ready_i;
s_epoch_restart_q <= s_epoch_restart;
// The snapshot response holds valid and its payload stable until the
// mailbox accepts it; only an epoch restart may withdraw it.
if (s_resp_wait_q) begin
assert ((snapshot_resp_valid_o && $stable(snapshot_resp_data_o)) || s_epoch_restart_q);
end
// No terminal result can ever appear at Phase 2.
assert (!result_valid_o);
end
// No terminal result can ever appear at Phase 2.
assert (!result_valid_o);
end
end
`endif
end else begin : gen_p3_core
// -----------------------------------------------------------------
// Phase 3 job controller
// -----------------------------------------------------------------
typedef enum logic [2:0] {
CoreIdle,
CoreRun,
CoreDrainFault,
CoreDrainAbort,
CoreDrainReset,
CorePublish
} core_state_e;
core_state_e s_state_d, s_state_q;
snap_state_e s_snap_state_d, s_snap_state_q;
logic [EpochWidth-1:0] s_epoch_ack_d, s_epoch_ack_q;
logic s_quiesce_ack_d, s_quiesce_ack_q;
logic s_epoch_restart;
// job context
logic [31:0] s_job_base_q;
logic [31:0] s_job_id_q;
logic [31:0] s_timeout_q;
logic [15:0] s_job_count_q;
logic [31:0] s_term_job_id_q;
// first-fault record
logic s_fault_seen_d, s_fault_seen_q;
logic [ 3:0] s_fault_code_d, s_fault_code_q;
logic [31:0] s_fault_addr_d, s_fault_addr_q;
logic [31:0] s_fault_info_d, s_fault_info_q;
logic [31:0] s_fault_desc_d, s_fault_desc_q;
// terminal record
logic [ 2:0] s_res_code_d, s_res_code_q;
logic [ 2:0] s_res_irq_d, s_res_irq_q;
logic [31:0] s_res_completed_d, s_res_completed_q;
// snapshot capture
logic [SnapshotPayloadWidth-1:0] s_snap_payload_d, s_snap_payload_q;
logic s_launch_accept;
logic s_job_busy;
logic s_dec_busy;
logic s_sch_busy;
logic s_sch_drained;
logic s_drained;
logic s_compose_terminal;
logic s_stop;
logic s_clear;
// decoder wires
logic s_dec_job_start;
logic s_dec_read_req_valid;
logic s_dec_read_req_ready;
logic [31:0] s_dec_read_addr;
logic [31:0] s_dec_read_bytes;
logic s_dec_read_data_valid;
logic s_dec_read_data_ready;
logic s_dec_pause_ok;
logic s_dec_all_done;
logic s_all_done_q;
logic s_dec_progress;
logic s_dec_fault_req;
logic [ 3:0] s_dec_fault_code;
logic [31:0] s_dec_fault_addr;
logic [31:0] s_dec_fault_info;
logic [31:0] s_dec_fault_desc;
logic [15:0] s_dec_cur_index;
logic s_dec_retired;
// record wires decoder -> scheduler
logic s_rec_valid;
logic s_rec_ready;
logic [ 3:0] s_rec_opcode;
logic [12:0] s_rec_h, s_rec_w, s_rec_cin, s_rec_cout, s_rec_oh, s_rec_ow;
logic [31:0] s_rec_input0_base, s_rec_output_base;
logic [31:0] s_rec_weight_base, s_rec_param_base;
logic [31:0] s_rec_irow, s_rec_orow;
logic [ 7:0] s_rec_kh, s_rec_kw, s_rec_sh, s_rec_sw;
logic [ 7:0] s_rec_pad_top, s_rec_pad_left, s_rec_in0_zero;
logic [31:0] s_rec_weight_bytes, s_rec_param_bytes;
logic [15:0] s_rec_desc_index;
// scheduler wires
logic s_sch_launch;
logic s_sch_retired;
logic [31:0] s_sch_completed;
logic s_sch_read_req_valid;
logic s_sch_read_req_ready;
logic [31:0] s_sch_read_addr;
logic [31:0] s_sch_read_bytes;
logic s_sch_read_data_valid;
logic s_sch_read_data_ready;
logic s_sch_write_req_valid;
logic [31:0] s_sch_write_addr;
logic [31:0] s_sch_write_bytes;
logic s_sch_write_data_valid;
logic [63:0] s_sch_write_data;
logic [ 7:0] s_sch_write_keep;
logic s_sch_write_last;
logic s_sch_pause_ok;
logic s_sch_terminal;
logic [639:0] s_sch_counters;
logic [639:0] s_sch_term_counters;
logic s_sch_fault_req;
logic [ 3:0] s_sch_fault_code;
logic [31:0] s_sch_fault_addr;
logic [31:0] s_sch_fault_info;
logic [31:0] s_sch_fault_desc;
// fault arbitration
logic s_evt_dec;
logic s_evt_sch;
logic s_evt_cmd_err;
logic [ 3:0] s_evt_code;
logic [31:0] s_evt_addr;
logic [31:0] s_evt_info;
logic [31:0] s_evt_desc;
logic s_evt_any;
// Deterministic same-cycle first-fault priority: lower rank wins.
function automatic logic [3:0] fault_rank(input logic [3:0] code_i);
logic [3:0] rank;
begin
unique case (code_i)
`APB4_NPU__FAULT_CODE_AXI_PROTOCOL: rank = 4'd0;
`APB4_NPU__FAULT_CODE_AXI_READ: rank = 4'd1;
`APB4_NPU__FAULT_CODE_AXI_WRITE: rank = 4'd2;
`APB4_NPU__FAULT_CODE_LOCAL_STATE: rank = 4'd3;
`APB4_NPU__FAULT_CODE_ARITHMETIC: rank = 4'd4;
`APB4_NPU__FAULT_CODE_DESCRIPTOR: rank = 4'd5;
`APB4_NPU__FAULT_CODE_UNSUPPORTED: rank = 4'd6;
`APB4_NPU__FAULT_CODE_RANGE: rank = 4'd7;
`APB4_NPU__FAULT_CODE_NO_PROGRESS: rank = 4'd8;
default: rank = 4'd9;
endcase
return rank;
end
endfunction
assign s_evt_dec = s_dec_fault_req;
assign s_evt_sch = s_sch_fault_req;
// A rejected DMA command means a scheduler/decoder programming bug.
assign s_evt_cmd_err = (dma_read_cmd_err_i || dma_write_cmd_err_i) && s_job_busy &&
!dma_fault_i && !s_fault_seen_q;
always_comb begin
s_evt_any = s_evt_dec || s_evt_sch || s_evt_cmd_err;
s_evt_code = `APB4_NPU__FAULT_CODE_LOCAL_STATE;
s_evt_addr = 32'd0;
s_evt_info = {16'd0, 8'd255, 4'd0, 2'd0, 2'd0};
s_evt_desc = 32'hffff_ffff;
if (s_evt_dec) begin
s_evt_code = s_dec_fault_code;
s_evt_addr = s_dec_fault_addr;
s_evt_info = s_dec_fault_info;
s_evt_desc = s_dec_fault_desc;
end
if (s_evt_sch && (!s_evt_dec ||
(fault_rank(s_sch_fault_code) <= fault_rank(s_dec_fault_code)))) begin
s_evt_code = s_sch_fault_code;
s_evt_addr = s_sch_fault_addr;
s_evt_info = s_sch_fault_info;
// A scheduler fault while it is idle (watchdog during the fetch
// phase) belongs to the descriptor the decoder holds.
s_evt_desc = s_dec_busy ? {16'd0, s_dec_cur_index} : s_sch_fault_desc;
end
if (s_evt_cmd_err && (!s_evt_dec && !s_evt_sch ||
(fault_rank(`APB4_NPU__FAULT_CODE_LOCAL_STATE) <=
fault_rank(s_evt_code)))) begin
s_evt_code = `APB4_NPU__FAULT_CODE_LOCAL_STATE;
s_evt_addr = 32'd0;
s_evt_info = {16'd0, 8'd255, 4'd0, 2'd0, 2'd0};
s_evt_desc = s_dec_busy ? {16'd0, s_dec_cur_index} : s_sch_fault_desc;
end
end
assign s_job_busy = (s_state_q != CoreIdle);
assign s_drained = !s_dec_busy && s_sch_drained && !dma_busy_i;
assign s_epoch_restart = (epoch_req_i != s_epoch_ack_q);
assign s_launch_accept = launch_valid_i && launch_ready_o;
assign s_stop = (s_state_q == CoreDrainFault) || (s_state_q == CoreDrainAbort) ||
(s_state_q == CoreDrainReset) || flush_i;
assign s_clear = flush_i ||
((s_state_q == CorePublish) && result_valid_o && result_ready_i) ||
((s_state_q == CoreDrainReset) && s_drained);
assign s_compose_terminal = ((s_state_q == CoreRun) && s_all_done_q) ||
(((s_state_q == CoreDrainFault) || (s_state_q == CoreDrainAbort) ||
(s_state_q == CoreDrainReset)) && s_drained);
// -------------------------------------------------------------
// Mailbox-facing outputs
// -------------------------------------------------------------
assign launch_ready_o = (s_state_q == CoreIdle) && !s_epoch_restart && !quiesce_req_i &&
!flush_i;
assign busy_o = s_job_busy;
assign draining_o = (s_state_q == CoreDrainFault) || (s_state_q == CoreDrainAbort) ||
(s_state_q == CoreDrainReset);
assign flush_busy_o = 1'b0;
assign idle_o = !block_new_i || clock_pause_ack_o;
assign clock_pause_ack_o = block_new_i && dma_pause_ack_i && s_dec_pause_ok &&
s_sch_pause_ok;
assign pause_active_o = block_new_i && clock_pause_ack_o;
assign epoch_ack_o = s_epoch_ack_q;
assign quiesce_ack_o = s_quiesce_ack_q;
assign dma_clear_o = flush_i || s_launch_accept;
// Read-channel ownership alternates fetch (decoder) and execute
// (scheduler) phases, which never overlap.
assign dma_read_req_valid_o = s_sch_busy ? s_sch_read_req_valid : s_dec_read_req_valid;
assign dma_read_addr_o = s_sch_busy ? s_sch_read_addr : s_dec_read_addr;
assign dma_read_bytes_o = s_sch_busy ? s_sch_read_bytes : s_dec_read_bytes;
assign s_dec_read_req_ready = !s_sch_busy && dma_read_req_ready_i;
assign s_sch_read_req_ready = s_sch_busy && dma_read_req_ready_i;
assign s_dec_read_data_valid = !s_sch_busy && dma_read_data_valid_i;
assign s_sch_read_data_valid = s_sch_busy && dma_read_data_valid_i;
assign dma_read_data_ready_o = s_sch_busy ? s_sch_read_data_ready : s_dec_read_data_ready;
assign dma_write_req_valid_o = s_sch_write_req_valid;
assign dma_write_addr_o = s_sch_write_addr;
assign dma_write_bytes_o = s_sch_write_bytes;
assign dma_write_data_valid_o = s_sch_write_data_valid;
assign dma_write_data_o = s_sch_write_data;
assign dma_write_keep_o = s_sch_write_keep;
assign dma_write_last_o = s_sch_write_last;
assign s_dec_job_start = s_launch_accept;
assign s_sch_launch = s_launch_accept;
assign s_dec_retired = s_sch_retired;
assign s_sch_terminal = s_compose_terminal;
// -------------------------------------------------------------
// Core FSM
// -------------------------------------------------------------
always_comb begin
s_state_d = s_state_q;
s_epoch_ack_d = s_epoch_ack_q;
s_quiesce_ack_d = s_quiesce_ack_q;
s_snap_state_d = s_snap_state_q;
s_snap_payload_d = s_snap_payload_q;
s_fault_seen_d = s_fault_seen_q;
s_fault_code_d = s_fault_code_q;
s_fault_addr_d = s_fault_addr_q;
s_fault_info_d = s_fault_info_q;
s_fault_desc_d = s_fault_desc_q;
s_res_code_d = s_res_code_q;
s_res_irq_d = s_res_irq_q;
s_res_completed_d = s_res_completed_q;
snapshot_req_ready_o = 1'b0;
snapshot_resp_valid_o = 1'b0;
// snapshot endpoint: atomic capture of the live or retained bank
unique case (s_snap_state_q)
SnapIdle: begin
snapshot_req_ready_o = 1'b1;
if (snapshot_req_valid_i) begin
s_snap_payload_d = s_job_busy ? {s_sch_counters, s_job_id_q} :
{s_sch_term_counters, s_term_job_id_q};
s_snap_state_d = SnapRespond;
end
end
SnapRespond: begin
snapshot_resp_valid_o = 1'b1;
if (snapshot_resp_ready_i) begin
s_snap_state_d = SnapIdle;
end
end
default: s_snap_state_d = SnapIdle;
endcase
// first-fault capture (also during abort/reset drains: a fault during
// cancellation converts the terminal record to ERROR)
if (s_evt_any && !s_fault_seen_q &&
((s_state_q == CoreRun) || (s_state_q == CoreDrainAbort) ||
(s_state_q == CoreDrainReset))) begin
s_fault_seen_d = 1'b1;
s_fault_code_d = s_evt_code;
s_fault_addr_d = s_evt_addr;
s_fault_info_d = s_evt_info;
s_fault_desc_d = s_evt_desc;
end
unique case (s_state_q)
CoreIdle: begin
if (s_launch_accept) begin
s_state_d = CoreRun;
end
end
CoreRun: begin
if (s_evt_any && !s_fault_seen_q) begin
s_state_d = CoreDrainFault;
end else if (s_all_done_q) begin
s_res_code_d = 3'(`APB4_NPU__RESULT_CODE_DONE);
s_res_irq_d = 3'b001;
s_res_completed_d = {16'd0, s_job_count_q};
s_fault_code_d = 4'(`APB4_NPU__FAULT_CODE_NONE);
s_fault_desc_d = 32'hffff_ffff;
s_fault_addr_d = 32'd0;
s_fault_info_d = 32'd0;
s_state_d = CorePublish;
end else if (abort_i) begin
s_state_d = CoreDrainAbort;
end else if (quiesce_req_i) begin
s_state_d = CoreDrainAbort;
end
end
CoreDrainFault, CoreDrainAbort: begin
if (s_drained) begin
if (s_fault_seen_q) begin
s_res_code_d = 3'(`APB4_NPU__RESULT_CODE_ERROR);
s_res_irq_d = 3'b010;
end else begin
s_res_code_d = 3'(`APB4_NPU__RESULT_CODE_ABORTED);
s_res_irq_d = 3'b100;
s_fault_code_d = 4'(`APB4_NPU__FAULT_CODE_NONE);
s_fault_desc_d = 32'hffff_ffff;
s_fault_addr_d = 32'd0;
s_fault_info_d = 32'd0;
end
s_res_completed_d = s_sch_completed;
s_state_d = CorePublish;
end
end
CoreDrainReset: begin
if (s_drained) begin
// stop/drain completed: re-acknowledge the fresh epoch, then
// deliver the retained cancellation record
s_epoch_ack_d = epoch_req_i;
if (s_fault_seen_q) begin
s_res_code_d = 3'(`APB4_NPU__RESULT_CODE_ERROR);
s_res_irq_d = 3'b010;
end else begin
s_res_code_d = 3'(`APB4_NPU__RESULT_CODE_RESET_CANCELLED);
s_res_irq_d = 3'b010;
s_fault_code_d = 4'(`APB4_NPU__FAULT_CODE_RESET_CANCELLED);
s_fault_desc_d = 32'hffff_ffff;
s_fault_addr_d = 32'd0;
s_fault_info_d = 32'd0;
end
s_res_completed_d = s_sch_completed;
s_state_d = CorePublish;
end
end
CorePublish: begin
if (result_ready_i) begin
s_state_d = CoreIdle;
end
end
default: s_state_d = CoreIdle;
endcase
// Quiesce acknowledge: idle endpoint answers immediately; an active one
// answers once the terminal result is latched in the result mailbox.
if (quiesce_req_i) begin
s_quiesce_ack_d = (s_state_q == CoreIdle) || (s_state_q == CorePublish);
end else begin
s_quiesce_ack_d = 1'b0;
end
// Epoch restart (link loss, resource reset, coordinated flush): force a
// safe state; idle is re-acknowledged immediately, an active or
// terminating job is cancelled and reconciled through the drain.
if (s_epoch_restart) begin
s_snap_state_d = SnapIdle;
if (s_state_q == CoreIdle) begin
s_epoch_ack_d = epoch_req_i;
end else begin
s_state_d = CoreDrainReset;
end
end
// Coordinated fabric flush cancels transport immediately; the epoch
// restart that follows reconciles the endpoint.
if (flush_i && (s_state_q != CoreIdle)) begin
s_state_d = CoreDrainReset;
end
end
assign result_valid_o = (s_state_q == CorePublish);
assign result_data_o = {s_res_irq_q, s_fault_code_q, s_res_code_q, s_fault_info_q,
s_fault_addr_q, s_fault_desc_q, s_res_completed_q, s_job_id_q};
assign snapshot_resp_data_o = s_snap_payload_q;
always_ff @(posedge clk_hp_i or negedge rst_hp_n_i) begin
if (!rst_hp_n_i) begin
s_state_q <= CoreIdle;
s_snap_state_q <= SnapIdle;
s_job_base_q <= '0;
s_job_id_q <= '0;
s_timeout_q <= '0;
s_job_count_q <= '0;
s_term_job_id_q <= '0;
s_fault_seen_q <= 1'b0;
s_fault_code_q <= '0;
s_fault_addr_q <= '0;
s_fault_info_q <= '0;
s_fault_desc_q <= `APB4_NPU__FAULT_DESCRIPTOR_RESET;
s_res_code_q <= '0;
s_res_irq_q <= '0;
s_res_completed_q <= '0;
s_snap_payload_q <= '0;
s_all_done_q <= 1'b0;
end else begin
s_state_q <= s_state_d;
s_snap_state_q <= s_snap_state_d;
s_snap_payload_q <= s_snap_payload_d;
s_fault_seen_q <= s_fault_seen_d;
s_fault_code_q <= s_fault_code_d;
s_fault_addr_q <= s_fault_addr_d;
s_fault_info_q <= s_fault_info_d;
s_fault_desc_q <= s_fault_desc_d;
s_res_code_q <= s_res_code_d;
s_res_irq_q <= s_res_irq_d;
s_res_completed_q <= s_res_completed_d;
if (s_launch_accept) begin
s_job_base_q <= launch_data_i[`APB4_NPU__LAUNCH_JOB_BASE+:32];
s_job_id_q <= launch_data_i[`APB4_NPU__LAUNCH_JOB_ID+:32];
s_timeout_q <= launch_data_i[`APB4_NPU__LAUNCH_TIMEOUT_CYCLES+:32];
s_job_count_q <= launch_data_i[`APB4_NPU__LAUNCH_JOB_COUNT+:16];
s_fault_seen_q <= 1'b0;
s_fault_desc_q <= `APB4_NPU__FAULT_DESCRIPTOR_RESET;
end
if (s_compose_terminal) begin
s_term_job_id_q <= s_job_id_q;
end
if ((s_state_q == CoreIdle) || (s_state_q == CorePublish)) begin
s_all_done_q <= 1'b0;
end else if (s_dec_all_done) begin
s_all_done_q <= 1'b1;
end
if (s_state_q == CoreIdle) begin
s_fault_seen_q <= 1'b0;
end
end
end
dffr #(
.DATA_WIDTH(EpochWidth)
) u_epoch_ack (
.clk_i (clk_hp_i),
.rst_n_i(rst_hp_n_i),
.dat_i (s_epoch_ack_d),
.dat_o (s_epoch_ack_q)
);
dffr #(
.DATA_WIDTH(1)
) u_quiesce_ack (
.clk_i (clk_hp_i),
.rst_n_i(rst_hp_n_i),
.dat_i (s_quiesce_ack_d),
.dat_o (s_quiesce_ack_q)
);
npu_job_decoder u_job_decoder (
.clk_hp_i (clk_hp_i),
.rst_hp_n_i (rst_hp_n_i),
.clear_i (s_stop),
.block_new_i (block_new_i),
.pause_ok_o (s_dec_pause_ok),
.job_start_i (s_dec_job_start),
.job_base_i (launch_data_i[`APB4_NPU__LAUNCH_JOB_BASE+:32]),
.job_count_i (launch_data_i[`APB4_NPU__LAUNCH_JOB_COUNT+:16]),
.read_req_valid_o (s_dec_read_req_valid),
.read_req_ready_i (s_dec_read_req_ready),
.read_addr_o (s_dec_read_addr),
.read_bytes_o (s_dec_read_bytes),
.read_data_valid_i (s_dec_read_data_valid),
.read_data_ready_o (s_dec_read_data_ready),
.read_data_i (dma_read_data_i),
.read_last_i (dma_read_last_i),
.dma_read_busy_i (dma_read_busy_i),
.dma_fault_i (dma_fault_i),
.dma_fault_code_i (dma_fault_code_i),
.dma_fault_addr_i (dma_fault_addr_i),
.dma_fault_resp_i (dma_fault_resp_i),
.rec_valid_o (s_rec_valid),
.rec_ready_i (s_rec_ready),
.rec_opcode_o (s_rec_opcode),
.rec_h_o (s_rec_h),
.rec_w_o (s_rec_w),
.rec_cin_o (s_rec_cin),
.rec_cout_o (s_rec_cout),
.rec_oh_o (s_rec_oh),
.rec_ow_o (s_rec_ow),
.rec_input0_base_o (s_rec_input0_base),
.rec_output_base_o (s_rec_output_base),
.rec_weight_base_o (s_rec_weight_base),
.rec_param_base_o (s_rec_param_base),
.rec_input0_row_bytes_o(s_rec_irow),
.rec_output_row_bytes_o(s_rec_orow),
.rec_kh_o (s_rec_kh),
.rec_kw_o (s_rec_kw),
.rec_sh_o (s_rec_sh),
.rec_sw_o (s_rec_sw),
.rec_pad_top_o (s_rec_pad_top),
.rec_pad_left_o (s_rec_pad_left),
.rec_input0_zero_o (s_rec_in0_zero),
.rec_weight_bytes_o(s_rec_weight_bytes),
.rec_param_bytes_o (s_rec_param_bytes),
.rec_desc_index_o (s_rec_desc_index),
.retired_i (s_dec_retired),
.all_done_o (s_dec_all_done),
.busy_o (s_dec_busy),
.cur_desc_index_o (s_dec_cur_index),
.progress_o (s_dec_progress),
.fault_req_o (s_dec_fault_req),
.fault_code_o (s_dec_fault_code),
.fault_addr_o (s_dec_fault_addr),
.fault_info_o (s_dec_fault_info),
.fault_desc_o (s_dec_fault_desc)
);
npu_scheduler u_scheduler (
.clk_hp_i (clk_hp_i),
.rst_hp_n_i (rst_hp_n_i),
.clear_i (s_clear),
.stop_i (s_stop),
.block_new_i (block_new_i),
.pause_ok_o (s_sch_pause_ok),
.launch_i (s_sch_launch),
.timeout_cycles_i (launch_data_i[`APB4_NPU__LAUNCH_TIMEOUT_CYCLES+:32]),
.job_active_i (s_state_q == CoreRun),
.pause_active_i (pause_active_o),
.rec_valid_i (s_rec_valid),
.rec_ready_o (s_rec_ready),
.rec_opcode_i (s_rec_opcode),
.rec_h_i (s_rec_h),
.rec_w_i (s_rec_w),
.rec_cin_i (s_rec_cin),
.rec_cout_i (s_rec_cout),
.rec_oh_i (s_rec_oh),
.rec_ow_i (s_rec_ow),
.rec_input0_base_i (s_rec_input0_base),
.rec_output_base_i (s_rec_output_base),
.rec_weight_base_i (s_rec_weight_base),
.rec_param_base_i (s_rec_param_base),
.rec_input0_row_bytes_i(s_rec_irow),
.rec_output_row_bytes_i(s_rec_orow),
.rec_kh_i (s_rec_kh),
.rec_kw_i (s_rec_kw),
.rec_sh_i (s_rec_sh),
.rec_sw_i (s_rec_sw),
.rec_pad_top_i (s_rec_pad_top),
.rec_pad_left_i (s_rec_pad_left),
.rec_input0_zero_i (s_rec_in0_zero),
.rec_weight_bytes_i (s_rec_weight_bytes),
.rec_param_bytes_i (s_rec_param_bytes),
.rec_desc_index_i (s_rec_desc_index),
.retired_o (s_sch_retired),
.completed_o (s_sch_completed),
.read_req_valid_o (s_sch_read_req_valid),
.read_req_ready_i (s_sch_read_req_ready),
.read_addr_o (s_sch_read_addr),
.read_bytes_o (s_sch_read_bytes),
.read_data_valid_i (s_sch_read_data_valid),
.read_data_ready_o (s_sch_read_data_ready),
.read_data_i (dma_read_data_i),
.read_keep_i (dma_read_keep_i),
.read_last_i (dma_read_last_i),
.write_req_valid_o (s_sch_write_req_valid),
.write_req_ready_i (dma_write_req_ready_i),
.write_addr_o (s_sch_write_addr),
.write_bytes_o (s_sch_write_bytes),
.write_data_valid_o (s_sch_write_data_valid),
.write_data_ready_i (dma_write_data_ready_i),
.write_data_o (s_sch_write_data),
.write_keep_o (s_sch_write_keep),
.write_last_o (s_sch_write_last),
.write_done_i (dma_write_done_i),
.dma_read_busy_i (dma_read_busy_i),
.dma_write_busy_i (dma_write_busy_i),
.dma_pause_ack_i (dma_pause_ack_i),
.dma_fault_i (dma_fault_i),
.dma_fault_code_i (dma_fault_code_i),
.dma_fault_addr_i (dma_fault_addr_i),
.dma_fault_resp_i (dma_fault_resp_i),
.dma_read_bytes_i (dma_read_bytes_i),
.dma_write_bytes_i (dma_write_bytes_i),
.dma_stall_cycles_i (dma_stall_cycles_i),
.progress_i (s_dec_progress),
.terminal_i (s_sch_terminal),
.counters_o (s_sch_counters),
.term_counters_o (s_sch_term_counters),
.fault_req_o (s_sch_fault_req),
.fault_code_o (s_sch_fault_code),
.fault_addr_o (s_sch_fault_addr),
.fault_info_o (s_sch_fault_info),
.fault_desc_o (s_sch_fault_desc),
.busy_o (s_sch_busy),
.drained_o (s_sch_drained)
);
end
endmodule
+729
View File
@@ -0,0 +1,729 @@
// Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
// SPDX-License-Identifier: MulanPSL-2.0
// NPU-P3 production AXI4 data-plane engine (docs/ip/npu.md "AXI4 data plane").
// 32-bit addresses, 64-bit data, ID width 3 fixed to zero, USER fixed to
// zero, INCR bursts of 1..16 beats, at most one outstanding read and one
// outstanding write, which may overlap. AxCACHE/AxLOCK/AxPROT/AxQOS/AxREGION
// are always zero.
//
// Burst planning: bulk uses full 64-bit beats; leading/trailing read edges
// use the largest naturally aligned narrow size (1/2/4 bytes) so reads never
// overfetch; write edges are full-width aligned beats whose scheduler-built
// WSTRB covers only addressed lanes; no burst crosses a 4 KiB page.
//
// Reservation: ARVALID is presented only when the 16-entry receive FIFO is
// empty, so an accepted read burst always completes regardless of stream
// backpressure. A write burst gathers its complete payload into the 16-entry
// write FIFO before AWVALID; W beats are driven only after the AW handshake.
//
// Command rejection: a zero byte count or an addr+bytes wrap is refused
// combinationally (ready low, cmd_err high while presented) and is never
// accepted; only bus-visible events raise fault_o with codes 4 (AXI_READ),
// 5 (AXI_WRITE) or 6 (AXI_PROTOCOL), with same-cycle priority
// AXI_PROTOCOL > AXI_READ > AXI_WRITE. fault_o is sticky until clear_i. After
// a fault no new burst is presented and accepted obligations drain; a
// malformed R/B response is quarantined (its receiver stops) until clear_i.
// A write payload beat whose write_last_i does not match the planned segment
// end pulses write_cmd_err_o and aborts the segment locally without any AXI
// fault.
//
// Pause: block_new_i stops command acceptance, new burst presentation and
// payload gathering at register-safe boundaries. Presented-but-unaccepted
// ARVALID/AWVALID stay asserted with stable payload and accepted transfers
// drain. pause_ack_o asserts while block_new_i is held once no AR-accepted
// read and no AW-accepted write (W/B) obligation remains; it does not require
// empty command slots or busy_o low, and unblocking resumes identical work.
//
// Counters are 64-bit saturating and clear on reset and clear_i.
// read_bytes_o counts the addressed lanes of every accepted R beat, including
// beats of error-response bursts. write_bytes_o counts the accepted WSTRB
// bytes of every W beat, including bursts whose B response errors (erroneous
// traffic stays visible). stall_cycles_o counts cycles with a ready work item
// blocked on an AXI handshake (AR/AW/W backpressure or a pending R/B
// response). fault_addr_o records the failing beat address (for a write
// response, the final W beat of the failed burst) and fault_resp_o the AXI
// response bits.
module npu_dma (
input logic clk_hp_i,
input logic rst_hp_n_i,
input logic clear_i,
input logic block_new_i,
output logic pause_ack_o,
// read segment command
input logic read_req_valid_i,
output logic read_req_ready_o,
input logic [31:0] read_addr_i,
input logic [31:0] read_bytes_i,
// read data stream
output logic read_data_valid_o,
input logic read_data_ready_i,
output logic [63:0] read_data_o,
output logic [ 7:0] read_keep_o,
output logic read_last_o,
// write segment command
input logic write_req_valid_i,
output logic write_req_ready_o,
input logic [31:0] write_addr_i,
input logic [31:0] write_bytes_i,
// write payload stream
input logic write_data_valid_i,
output logic write_data_ready_o,
input logic [63:0] write_data_i,
input logic [ 7:0] write_keep_i,
input logic write_last_i,
output logic write_done_o,
// status
output logic busy_o,
output logic read_busy_o,
output logic write_busy_o,
output logic [63:0] read_bytes_o,
output logic [63:0] write_bytes_o,
output logic [63:0] stall_cycles_o,
output logic fault_o,
output logic [ 3:0] fault_code_o,
output logic [31:0] fault_addr_o,
output logic [ 1:0] fault_resp_o,
output logic read_cmd_err_o,
output logic write_cmd_err_o,
axi4_if.master axi4
);
localparam int unsigned FifoDepth = 16;
localparam logic [3:0] FaultAxiRead = 4'd4;
localparam logic [3:0] FaultAxiWrite = 4'd5;
localparam logic [3:0] FaultAxiProtocol = 4'd6;
typedef enum logic [1:0] {
ReadIdle,
ReadPlan,
ReadAddress,
ReadData
} read_state_e;
typedef enum logic [2:0] {
WriteIdle,
WritePlan,
WriteGather,
WriteStaged,
WriteAddress,
WriteData,
WriteResponse
} write_state_e;
read_state_e s_read_state_q;
write_state_e s_write_state_q;
logic s_read_cmd_illegal;
logic s_read_cmd_accept;
logic [31:0] s_read_addr_q;
logic [31:0] s_read_bytes_left_q;
logic [31:0] s_read_burst_addr_q;
logic [ 4:0] s_read_burst_beats_q;
logic [ 2:0] s_read_burst_size_q;
logic [ 7:0] s_read_burst_bytes_q;
logic s_read_burst_last_q;
logic [ 4:0] s_read_beats_left_q;
logic [31:0] s_read_beat_addr_q;
logic s_read_proto_fault_q;
logic [ 4:0] s_read_plan_beats;
logic [ 2:0] s_read_plan_size;
logic [ 7:0] s_read_plan_bytes;
logic [ 3:0] s_read_beat_bytes;
logic [ 7:0] s_read_beat_keep;
logic s_read_last_expected;
logic s_read_proto_error;
logic s_read_rsp_accept;
logic s_rfifo_push;
logic s_rfifo_pop;
logic s_write_cmd_illegal;
logic s_write_cmd_accept;
logic [31:0] s_write_addr_q;
logic [31:0] s_write_bytes_left_q;
logic [31:0] s_write_burst_addr_q;
logic [ 4:0] s_write_burst_beats_q;
logic [ 7:0] s_write_burst_bytes_q;
logic s_write_burst_last_q;
logic [ 4:0] s_write_beats_left_q;
logic [31:0] s_write_beat_addr_q;
logic [ 4:0] s_write_gather_left_q;
logic s_write_proto_fault_q;
logic [31:0] s_write_plan_addr;
logic [ 4:0] s_write_plan_beats;
logic [ 7:0] s_write_plan_bytes;
logic s_write_plan_last;
logic s_write_gather_accept;
logic s_write_gather_last;
logic s_write_stream_err;
logic s_write_w_accept;
logic s_write_rsp_accept;
logic s_wfifo_push;
logic s_wfifo_pop;
logic [63:0] s_rfifo_data_q [FifoDepth];
logic [ 7:0] s_rfifo_keep_q [FifoDepth];
logic s_rfifo_last_q [FifoDepth];
logic [ 3:0] s_rfifo_rptr_q;
logic [ 3:0] s_rfifo_wptr_q;
logic [ 4:0] s_rfifo_count_q;
logic [63:0] s_wfifo_data_q [FifoDepth];
logic [ 7:0] s_wfifo_keep_q [FifoDepth];
logic [ 3:0] s_wfifo_rptr_q;
logic [ 3:0] s_wfifo_wptr_q;
logic [ 4:0] s_wfifo_count_q;
logic s_fault_q;
logic [ 3:0] s_fault_code_q;
logic [31:0] s_fault_addr_q;
logic [ 1:0] s_fault_resp_q;
logic s_evt_proto_read;
logic s_evt_axi_read;
logic s_evt_proto_write;
logic s_evt_axi_write;
logic [63:0] s_read_bytes_q;
logic [63:0] s_write_bytes_q;
logic [63:0] s_stall_cycles_q;
logic s_stall_event;
function automatic logic [3:0] count_ones(input logic [7:0] value_i);
logic [3:0] count;
begin
count = 4'd0;
for (int unsigned lane = 0; lane < 8; lane++) begin
count = count + {3'd0, value_i[lane]};
end
return count;
end
endfunction
function automatic logic [63:0] saturating_add(input logic [63:0] value_i,
input logic [3:0] increment_i);
logic [64:0] widened;
begin
widened = {1'b0, value_i} + {61'd0, increment_i};
if (widened[64]) begin
return 64'hffff_ffff_ffff_ffff;
end
return widened[63:0];
end
endfunction
assign s_read_cmd_illegal = (read_bytes_i == 32'd0) ||
({1'b0, read_addr_i} + {1'b0, read_bytes_i} > 33'h1_0000_0000);
assign read_req_ready_o = (s_read_state_q == ReadIdle) && !s_read_cmd_illegal && !s_fault_q &&
!block_new_i && !clear_i;
assign read_cmd_err_o = read_req_valid_i && s_read_cmd_illegal && !clear_i;
assign s_read_cmd_accept = read_req_valid_i && read_req_ready_o;
// Read burst plan: full 64-bit beats in the bulk, the largest naturally
// aligned narrow transfer at the leading/trailing edges, never crossing a
// 4 KiB page, never fetching bytes outside the segment.
always_comb begin
logic [12:0] s_bytes_to_4k;
logic [12:0] s_beats_to_4k;
logic [31:0] s_full_beats;
logic [ 3:0] s_to_alignment;
s_bytes_to_4k = 13'd4096 - {1'b0, s_read_addr_q[11:0]};
s_beats_to_4k = s_bytes_to_4k >> 3;
s_full_beats = s_read_bytes_left_q >> 3;
s_to_alignment = 4'd8 - {1'b0, s_read_addr_q[2:0]};
s_read_plan_beats = 5'd1;
s_read_plan_size = 3'd0;
s_read_plan_bytes = 8'd1;
if ((s_read_addr_q[2:0] == 3'd0) && (s_read_bytes_left_q >= 32'd8)) begin
s_read_plan_beats = (s_full_beats > 32'd16) ? 5'd16 : s_full_beats[4:0];
if (s_beats_to_4k < {8'd0, s_read_plan_beats}) begin
s_read_plan_beats = s_beats_to_4k[4:0];
end
s_read_plan_size = 3'd3;
s_read_plan_bytes = {s_read_plan_beats, 3'b000};
end else if ((s_read_addr_q[1:0] == 2'd0) && (s_to_alignment >= 4'd4) &&
(s_read_bytes_left_q >= 32'd4)) begin
s_read_plan_size = 3'd2;
s_read_plan_bytes = 8'd4;
end else if ((s_read_addr_q[0] == 1'b0) && (s_to_alignment >= 4'd2) &&
(s_read_bytes_left_q >= 32'd2)) begin
s_read_plan_size = 3'd1;
s_read_plan_bytes = 8'd2;
end
end
assign s_read_beat_bytes = 4'd1 << s_read_burst_size_q;
assign s_read_beat_keep = (8'hff >> (4'd8 - s_read_beat_bytes)) << s_read_beat_addr_q[2:0];
assign s_read_last_expected = s_read_beats_left_q == 5'd1;
assign s_read_proto_error = (axi4.rid != '0) || (axi4.rlast != s_read_last_expected);
assign s_read_rsp_accept = axi4.rvalid && axi4.rready;
assign axi4.arid = '0;
assign axi4.araddr = s_read_burst_addr_q;
assign axi4.arlen = {3'd0, s_read_burst_beats_q} - 8'd1;
assign axi4.arsize = s_read_burst_size_q;
assign axi4.arburst = 2'b01;
assign axi4.arlock = 1'b0;
assign axi4.arcache = 4'd0;
assign axi4.arprot = 3'd0;
assign axi4.arqos = 4'd0;
assign axi4.arregion = 4'd0;
assign axi4.aruser = '0;
assign axi4.arvalid = (s_read_state_q == ReadAddress) && !clear_i;
assign axi4.rready = (s_read_state_q == ReadData) && !s_read_proto_fault_q && !clear_i;
assign s_rfifo_push = s_read_rsp_accept && !s_read_proto_error;
assign s_rfifo_pop = read_data_valid_o && read_data_ready_i;
assign read_data_valid_o = (s_rfifo_count_q != 5'd0) && !clear_i;
assign read_data_o = s_rfifo_data_q[s_rfifo_rptr_q];
assign read_keep_o = s_rfifo_keep_q[s_rfifo_rptr_q];
assign read_last_o = s_rfifo_last_q[s_rfifo_rptr_q];
assign s_write_cmd_illegal = (write_bytes_i == 32'd0) ||
({1'b0, write_addr_i} + {1'b0, write_bytes_i} > 33'h1_0000_0000);
assign write_req_ready_o = (s_write_state_q == WriteIdle) && !s_write_cmd_illegal &&
!s_fault_q && !block_new_i && !clear_i;
assign write_cmd_err_o = (write_req_valid_i && s_write_cmd_illegal && !clear_i) ||
s_write_stream_err;
assign s_write_cmd_accept = write_req_valid_i && write_req_ready_o;
// Write burst plan: the address-aligned beat span of the remaining segment,
// at most 16 beats, never crossing a 4 KiB page. Byte lanes outside the
// segment are left to the scheduler-built WSTRB.
always_comb begin
logic [12:0] s_bytes_to_4k;
logic [12:0] s_beats_to_4k;
logic [31:0] s_span_beats;
logic [31:0] s_burst_room;
s_write_plan_addr = {s_write_addr_q[31:3], 3'b000};
s_bytes_to_4k = 13'd4096 - {1'b0, s_write_plan_addr[11:0]};
s_beats_to_4k = s_bytes_to_4k >> 3;
s_span_beats = ({29'd0, s_write_addr_q[2:0]} + s_write_bytes_left_q + 32'd7) >> 3;
s_write_plan_beats = (s_span_beats > 32'd16) ? 5'd16 : s_span_beats[4:0];
if (s_beats_to_4k < {8'd0, s_write_plan_beats}) begin
s_write_plan_beats = s_beats_to_4k[4:0];
end
s_burst_room = ({24'd0, s_write_plan_beats, 3'b000} - {29'd0, s_write_addr_q[2:0]});
s_write_plan_last = s_write_bytes_left_q <= s_burst_room;
s_write_plan_bytes = s_write_plan_last ? s_write_bytes_left_q[7:0] : s_burst_room[7:0];
end
assign axi4.awid = '0;
assign axi4.awaddr = s_write_burst_addr_q;
assign axi4.awlen = {3'd0, s_write_burst_beats_q} - 8'd1;
assign axi4.awsize = 3'd3;
assign axi4.awburst = 2'b01;
assign axi4.awlock = 1'b0;
assign axi4.awcache = 4'd0;
assign axi4.awprot = 3'd0;
assign axi4.awqos = 4'd0;
assign axi4.awregion = 4'd0;
assign axi4.awuser = '0;
assign axi4.awvalid = (s_write_state_q == WriteAddress) && !clear_i;
assign axi4.wdata = s_wfifo_data_q[s_wfifo_rptr_q];
assign axi4.wstrb = s_wfifo_keep_q[s_wfifo_rptr_q];
assign axi4.wlast = s_write_beats_left_q == 5'd1;
assign axi4.wuser = '0;
assign axi4.wvalid = (s_write_state_q == WriteData) && (s_wfifo_count_q != 5'd0) && !clear_i;
assign axi4.bready = (s_write_state_q == WriteResponse) && !s_write_proto_fault_q && !clear_i;
assign s_write_w_accept = axi4.wvalid && axi4.wready;
assign s_write_rsp_accept = axi4.bvalid && axi4.bready;
assign write_data_ready_o = (s_write_state_q == WriteGather) && !block_new_i && !s_fault_q &&
!clear_i;
assign s_write_gather_accept = write_data_valid_i && write_data_ready_o;
assign s_write_gather_last = (s_write_gather_left_q == 5'd1) && s_write_burst_last_q;
assign s_write_stream_err = s_write_gather_accept && (write_last_i != s_write_gather_last);
assign s_wfifo_push = s_write_gather_accept && !s_write_stream_err;
assign s_wfifo_pop = s_write_w_accept;
assign write_done_o = s_write_rsp_accept && (axi4.bid == '0) && (axi4.bresp == 2'b00) &&
s_write_burst_last_q;
assign s_evt_proto_read = axi4.rvalid && !clear_i && !s_read_proto_fault_q &&
((s_read_state_q != ReadData) || s_read_proto_error);
assign s_evt_axi_read = s_read_rsp_accept && (axi4.rresp != 2'b00);
assign s_evt_proto_write = axi4.bvalid && !clear_i && !s_write_proto_fault_q &&
((s_write_state_q != WriteResponse) || (axi4.bid != '0));
assign s_evt_axi_write = s_write_rsp_accept && (axi4.bresp != 2'b00);
assign s_stall_event = (axi4.arvalid && !axi4.arready) || (axi4.awvalid && !axi4.awready) ||
(axi4.wvalid && !axi4.wready) ||
((s_read_state_q == ReadData) && !axi4.rvalid && !s_read_proto_fault_q) ||
((s_write_state_q == WriteResponse) && !axi4.bvalid &&
!s_write_proto_fault_q);
assign read_busy_o = s_read_state_q != ReadIdle;
assign write_busy_o = s_write_state_q != WriteIdle;
assign busy_o = read_busy_o || write_busy_o;
// Zero accepted obligations: a presented-but-unaccepted AR/AW does not
// block the acknowledge, an AR-accepted read or AW-accepted write does.
assign pause_ack_o = block_new_i && (s_read_state_q != ReadData) &&
(s_write_state_q != WriteData) && (s_write_state_q != WriteResponse);
assign read_bytes_o = s_read_bytes_q;
assign write_bytes_o = s_write_bytes_q;
assign stall_cycles_o = s_stall_cycles_q;
assign fault_o = s_fault_q;
assign fault_code_o = s_fault_code_q;
assign fault_addr_o = s_fault_addr_q;
assign fault_resp_o = s_fault_resp_q;
always_ff @(posedge clk_hp_i or negedge rst_hp_n_i) begin
if (!rst_hp_n_i) begin
s_read_state_q <= ReadIdle;
s_read_addr_q <= '0;
s_read_bytes_left_q <= '0;
s_read_burst_addr_q <= '0;
s_read_burst_beats_q <= '0;
s_read_burst_size_q <= '0;
s_read_burst_bytes_q <= '0;
s_read_burst_last_q <= 1'b0;
s_read_beats_left_q <= '0;
s_read_beat_addr_q <= '0;
s_read_proto_fault_q <= 1'b0;
s_write_state_q <= WriteIdle;
s_write_addr_q <= '0;
s_write_bytes_left_q <= '0;
s_write_burst_addr_q <= '0;
s_write_burst_beats_q <= '0;
s_write_burst_bytes_q <= '0;
s_write_burst_last_q <= 1'b0;
s_write_beats_left_q <= '0;
s_write_beat_addr_q <= '0;
s_write_gather_left_q <= '0;
s_write_proto_fault_q <= 1'b0;
s_rfifo_rptr_q <= '0;
s_rfifo_wptr_q <= '0;
s_rfifo_count_q <= '0;
s_wfifo_rptr_q <= '0;
s_wfifo_wptr_q <= '0;
s_wfifo_count_q <= '0;
s_fault_q <= 1'b0;
s_fault_code_q <= '0;
s_fault_addr_q <= '0;
s_fault_resp_q <= '0;
s_read_bytes_q <= '0;
s_write_bytes_q <= '0;
s_stall_cycles_q <= '0;
end else if (clear_i) begin
// Coordinated fabric flush: in-flight state cancels together with the
// fabric; afterwards the engine is clean, idle and fault-free.
s_read_state_q <= ReadIdle;
s_read_proto_fault_q <= 1'b0;
s_write_state_q <= WriteIdle;
s_write_proto_fault_q <= 1'b0;
s_rfifo_rptr_q <= '0;
s_rfifo_wptr_q <= '0;
s_rfifo_count_q <= '0;
s_wfifo_rptr_q <= '0;
s_wfifo_wptr_q <= '0;
s_wfifo_count_q <= '0;
s_fault_q <= 1'b0;
s_fault_code_q <= '0;
s_fault_addr_q <= '0;
s_fault_resp_q <= '0;
s_read_bytes_q <= '0;
s_write_bytes_q <= '0;
s_stall_cycles_q <= '0;
end else begin
unique case (s_read_state_q)
ReadIdle: begin
if (s_read_cmd_accept) begin
s_read_addr_q <= read_addr_i;
s_read_bytes_left_q <= read_bytes_i;
s_read_state_q <= ReadPlan;
end
end
ReadPlan: begin
// Reserve receive storage for the whole burst before presenting AR.
if (!block_new_i && !s_fault_q && (s_rfifo_count_q == 5'd0)) begin
s_read_burst_addr_q <= s_read_addr_q;
s_read_burst_beats_q <= s_read_plan_beats;
s_read_burst_size_q <= s_read_plan_size;
s_read_burst_bytes_q <= s_read_plan_bytes;
s_read_burst_last_q <= {24'd0, s_read_plan_bytes} == s_read_bytes_left_q;
s_read_beats_left_q <= s_read_plan_beats;
s_read_beat_addr_q <= s_read_addr_q;
s_read_proto_fault_q <= 1'b0;
s_read_state_q <= ReadAddress;
end
end
ReadAddress: begin
if (axi4.arvalid && axi4.arready) begin
s_read_state_q <= ReadData;
end
end
ReadData: begin
if (s_read_rsp_accept) begin
if (s_read_proto_error) begin
// Malformed responses are quarantined until the flush.
s_read_proto_fault_q <= 1'b1;
end else if (s_read_beats_left_q == 5'd1) begin
s_read_bytes_left_q <= s_read_bytes_left_q - {24'd0, s_read_burst_bytes_q};
s_read_addr_q <= s_read_burst_addr_q + {24'd0, s_read_burst_bytes_q};
if (s_fault_q || s_evt_axi_read || s_read_burst_last_q) begin
s_read_state_q <= ReadIdle;
end else begin
s_read_state_q <= ReadPlan;
end
end else begin
s_read_beats_left_q <= s_read_beats_left_q - 1'b1;
s_read_beat_addr_q <= s_read_beat_addr_q + {28'd0, s_read_beat_bytes};
end
end
end
default: s_read_state_q <= ReadIdle;
endcase
unique case (s_write_state_q)
WriteIdle: begin
if (s_write_cmd_accept) begin
s_write_addr_q <= write_addr_i;
s_write_bytes_left_q <= write_bytes_i;
s_write_state_q <= WritePlan;
end
end
WritePlan: begin
s_write_burst_addr_q <= s_write_plan_addr;
s_write_burst_beats_q <= s_write_plan_beats;
s_write_burst_bytes_q <= s_write_plan_bytes;
s_write_burst_last_q <= s_write_plan_last;
s_write_beats_left_q <= s_write_plan_beats;
s_write_gather_left_q <= s_write_plan_beats;
s_write_beat_addr_q <= s_write_plan_addr;
s_write_proto_fault_q <= 1'b0;
s_write_state_q <= WriteGather;
end
WriteGather: begin
// Reserve the complete write payload before presenting AW.
if (s_write_stream_err) begin
// Locally rejected segment: drop the partial payload.
s_write_state_q <= WriteIdle;
s_wfifo_rptr_q <= '0;
s_wfifo_wptr_q <= '0;
s_wfifo_count_q <= '0;
end else if (s_write_gather_accept) begin
if (s_write_gather_left_q == 5'd1) begin
s_write_state_q <= WriteStaged;
end else begin
s_write_gather_left_q <= s_write_gather_left_q - 1'b1;
end
end
end
WriteStaged: begin
if (!block_new_i && !s_fault_q) begin
s_write_state_q <= WriteAddress;
end
end
WriteAddress: begin
if (axi4.awvalid && axi4.awready) begin
s_write_state_q <= WriteData;
end
end
WriteData: begin
if (s_write_w_accept) begin
if (s_write_beats_left_q == 5'd1) begin
s_write_state_q <= WriteResponse;
end else begin
s_write_beats_left_q <= s_write_beats_left_q - 1'b1;
s_write_beat_addr_q <= s_write_beat_addr_q + 32'd8;
end
end
end
WriteResponse: begin
if (s_write_rsp_accept) begin
if (axi4.bid != '0) begin
// Malformed responses are quarantined until the flush.
s_write_proto_fault_q <= 1'b1;
end else begin
s_write_bytes_left_q <= s_write_bytes_left_q - {24'd0, s_write_burst_bytes_q};
s_write_addr_q <= s_write_burst_addr_q + {24'd0, s_write_burst_beats_q, 3'b000};
if (s_fault_q || s_evt_axi_write || s_write_burst_last_q) begin
s_write_state_q <= WriteIdle;
end else begin
s_write_state_q <= WritePlan;
end
end
end
end
default: s_write_state_q <= WriteIdle;
endcase
if (s_rfifo_push) begin
s_rfifo_data_q[s_rfifo_wptr_q] <= axi4.rdata;
s_rfifo_keep_q[s_rfifo_wptr_q] <= s_read_beat_keep;
s_rfifo_last_q[s_rfifo_wptr_q] <= (s_read_beats_left_q == 5'd1) && s_read_burst_last_q;
s_rfifo_wptr_q <= s_rfifo_wptr_q + 1'b1;
end
if (s_rfifo_pop) begin
s_rfifo_rptr_q <= s_rfifo_rptr_q + 1'b1;
end
unique case ({
s_rfifo_push, s_rfifo_pop
})
2'b10: s_rfifo_count_q <= s_rfifo_count_q + 1'b1;
2'b01: s_rfifo_count_q <= s_rfifo_count_q - 1'b1;
default: begin
end
endcase
if (s_wfifo_push) begin
s_wfifo_data_q[s_wfifo_wptr_q] <= write_data_i;
s_wfifo_keep_q[s_wfifo_wptr_q] <= write_keep_i;
s_wfifo_wptr_q <= s_wfifo_wptr_q + 1'b1;
end
if (s_wfifo_pop) begin
s_wfifo_rptr_q <= s_wfifo_rptr_q + 1'b1;
end
unique case ({
s_wfifo_push, s_wfifo_pop
})
2'b10: s_wfifo_count_q <= s_wfifo_count_q + 1'b1;
2'b01: s_wfifo_count_q <= s_wfifo_count_q - 1'b1;
default: begin
end
endcase
if (!s_fault_q) begin
// Deterministic first-fault priority: AXI_PROTOCOL, AXI_READ, AXI_WRITE.
if (s_evt_proto_read || s_evt_proto_write) begin
s_fault_q <= 1'b1;
s_fault_code_q <= FaultAxiProtocol;
s_fault_addr_q <= s_evt_proto_read ? s_read_beat_addr_q : s_write_beat_addr_q;
s_fault_resp_q <= s_evt_proto_read ? axi4.rresp : axi4.bresp;
end else if (s_evt_axi_read) begin
s_fault_q <= 1'b1;
s_fault_code_q <= FaultAxiRead;
s_fault_addr_q <= s_read_beat_addr_q;
s_fault_resp_q <= axi4.rresp;
end else if (s_evt_axi_write) begin
s_fault_q <= 1'b1;
s_fault_code_q <= FaultAxiWrite;
s_fault_addr_q <= s_write_beat_addr_q;
s_fault_resp_q <= axi4.bresp;
end
end
if (s_read_rsp_accept) begin
s_read_bytes_q <= saturating_add(s_read_bytes_q, count_ones(s_read_beat_keep));
end
if (s_write_w_accept) begin
s_write_bytes_q <= saturating_add(s_write_bytes_q, count_ones(axi4.wstrb));
end
if (s_stall_event && (s_stall_cycles_q != 64'hffff_ffff_ffff_ffff)) begin
s_stall_cycles_q <= s_stall_cycles_q + 1'b1;
end
end
end
`ifndef SV_ASSRT_DISABLE
logic s_aw_wait_q;
logic s_ar_wait_q;
logic s_w_wait_q;
logic s_r_wait_q;
logic s_b_wait_q;
logic s_read_outstanding_q;
logic s_write_outstanding_q;
logic s_write_aw_accepted_q;
logic s_pause_ack_q;
logic s_arvalid_q;
logic s_awvalid_q;
logic s_reset_seen_q;
always_ff @(posedge clk_hp_i or negedge rst_hp_n_i) begin
if (!rst_hp_n_i) begin
s_aw_wait_q <= 1'b0;
s_ar_wait_q <= 1'b0;
s_w_wait_q <= 1'b0;
s_r_wait_q <= 1'b0;
s_b_wait_q <= 1'b0;
s_read_outstanding_q <= 1'b0;
s_write_outstanding_q <= 1'b0;
s_write_aw_accepted_q <= 1'b0;
s_pause_ack_q <= 1'b0;
s_arvalid_q <= 1'b0;
s_awvalid_q <= 1'b0;
s_reset_seen_q <= 1'b1;
end else begin
s_reset_seen_q <= 1'b0;
s_pause_ack_q <= pause_ack_o;
s_arvalid_q <= axi4.arvalid;
s_awvalid_q <= axi4.awvalid;
if (clear_i) begin
s_aw_wait_q <= 1'b0;
s_ar_wait_q <= 1'b0;
s_w_wait_q <= 1'b0;
s_r_wait_q <= 1'b0;
s_b_wait_q <= 1'b0;
s_read_outstanding_q <= 1'b0;
s_write_outstanding_q <= 1'b0;
s_write_aw_accepted_q <= 1'b0;
end else begin
s_aw_wait_q <= axi4.awvalid && !axi4.awready;
s_ar_wait_q <= axi4.arvalid && !axi4.arready;
s_w_wait_q <= axi4.wvalid && !axi4.wready;
s_r_wait_q <= axi4.rvalid && !axi4.rready;
s_b_wait_q <= axi4.bvalid && !axi4.bready;
if (axi4.arvalid && axi4.arready) begin
s_read_outstanding_q <= 1'b1;
end else if (s_read_rsp_accept && (s_read_beats_left_q == 5'd1)) begin
s_read_outstanding_q <= 1'b0;
end
if (axi4.awvalid && axi4.awready) begin
s_write_outstanding_q <= 1'b1;
s_write_aw_accepted_q <= 1'b1;
end
if (s_write_rsp_accept) begin
s_write_outstanding_q <= 1'b0;
end
if (s_write_w_accept && (s_write_beats_left_q == 5'd1)) begin
s_write_aw_accepted_q <= 1'b0;
end
end
// VALID payloads stay stable through any stall; only the coordinated
// fabric flush may invalidate a presented transaction.
if (s_aw_wait_q) begin
assert ((axi4.awvalid && $stable({axi4.awaddr, axi4.awlen, axi4.awsize})) || clear_i);
end
if (s_ar_wait_q) begin
assert ((axi4.arvalid && $stable({axi4.araddr, axi4.arlen, axi4.arsize})) || clear_i);
end
if (s_w_wait_q) begin
assert ((axi4.wvalid && $stable({axi4.wdata, axi4.wstrb, axi4.wlast})) || clear_i);
end
if (s_r_wait_q) begin
assert ((axi4.rvalid && $stable({axi4.rid, axi4.rdata, axi4.rresp, axi4.rlast})) ||
clear_i);
end
if (s_b_wait_q) begin
assert ((axi4.bvalid && $stable({axi4.bid, axi4.bresp})) || clear_i);
end
// W is only ever driven after its AW handshake.
if (axi4.wvalid) begin
assert (s_write_aw_accepted_q);
end
// At most one outstanding transaction per direction.
if (axi4.arvalid && axi4.arready) begin
assert (!s_read_outstanding_q);
end
if (axi4.awvalid && axi4.awready) begin
assert (!s_write_outstanding_q);
end
// No new AR/AW presentation begins while the pause is acknowledged.
if (s_pause_ack_q && !s_arvalid_q) begin
assert (!axi4.arvalid);
end
if (s_pause_ack_q && !s_awvalid_q) begin
assert (!axi4.awvalid);
end
// Address valids stay low out of reset.
if (s_reset_seen_q) begin
assert (!axi4.arvalid && !axi4.awvalid && !axi4.wvalid);
end
// Reserved storage can never overflow.
if (s_rfifo_push) begin
assert (s_rfifo_count_q < 5'd16);
end
if (s_wfifo_push) begin
assert (s_wfifo_count_q < 5'd16);
end
end
end
`endif
endmodule
+981
View File
@@ -0,0 +1,981 @@
// Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
// SPDX-License-Identifier: MulanPSL-2.0
// NPU-P3 job decoder: bounded linear fetch and full ABI-1.0 validation of the
// 128-byte descriptor records of one job (docs/ip/npu.md "Descriptor ABI 1.0"
// and "Job submission and ordering"). Each record is fetched through the
// npu_dma read channel into 32 private staging words, validated in the exact
// check order of the golden host validator scripts/npu_descriptors.py
// (structural record, opcode support, unused fields, geometry, operator,
// strides/spans, alignment, overlap) with widened no-wrap arithmetic, and is
// then emitted to the scheduler as a typed execute record. Records are
// fetched, validated, executed and retired strictly in order; a validation
// failure or a fetch bus fault is terminal for the job and earlier records
// are not rolled back.
//
// Fault metadata conventions: a validation fault reports the record fetch
// address plus 4*word (the ABI word index) on fault_addr_o; overlap faults
// and the descriptor-region wrap carry no single word and report the record
// base. A fetch fault forwards the DMA code (AXI_READ or AXI_PROTOCOL), the
// failing beat address and the AXI response, with FAULT_INFO direction=read
// and the not-applicable lane 255. A descriptor address or region that wraps
// the 32-bit space is refused locally as RANGE before any bus request.
`include "npu_define.svh"
module npu_job_decoder (
input logic clk_hp_i,
input logic rst_hp_n_i,
// coordinated cancel from npu_core (abort/quiesce/reset/flush): stop at
// the current segment boundary, drain the in-flight fetch, then idle
input logic clear_i,
input logic block_new_i,
output logic pause_ok_o,
// job launch from npu_core
input logic job_start_i,
input logic [31:0] job_base_i,
input logic [15:0] job_count_i,
// npu_dma read command channel (owned while fetching)
output logic read_req_valid_o,
input logic read_req_ready_i,
output logic [31:0] read_addr_o,
output logic [31:0] read_bytes_o,
// npu_dma read stream
input logic read_data_valid_i,
output logic read_data_ready_o,
input logic [63:0] read_data_i,
input logic read_last_i,
// npu_dma status
input logic dma_read_busy_i,
input logic dma_fault_i,
input logic [ 3:0] dma_fault_code_i,
input logic [31:0] dma_fault_addr_i,
input logic [ 1:0] dma_fault_resp_i,
// validated execute record to the scheduler
output logic rec_valid_o,
input logic rec_ready_i,
output logic [ 3:0] rec_opcode_o,
output logic [12:0] rec_h_o,
output logic [12:0] rec_w_o,
output logic [12:0] rec_cin_o,
output logic [12:0] rec_cout_o,
output logic [12:0] rec_oh_o,
output logic [12:0] rec_ow_o,
output logic [31:0] rec_input0_base_o,
output logic [31:0] rec_output_base_o,
output logic [31:0] rec_weight_base_o,
output logic [31:0] rec_param_base_o,
output logic [31:0] rec_input0_row_bytes_o,
output logic [31:0] rec_output_row_bytes_o,
output logic [ 7:0] rec_kh_o,
output logic [ 7:0] rec_kw_o,
output logic [ 7:0] rec_sh_o,
output logic [ 7:0] rec_sw_o,
output logic [ 7:0] rec_pad_top_o,
output logic [ 7:0] rec_pad_left_o,
output logic [ 7:0] rec_input0_zero_o,
output logic [31:0] rec_weight_bytes_o,
output logic [31:0] rec_param_bytes_o,
output logic [15:0] rec_desc_index_o,
// scheduler retirement handshake
input logic retired_i,
output logic all_done_o,
output logic busy_o,
output logic [15:0] cur_desc_index_o,
// one-cycle progress pulse for the job no-progress watchdog
output logic progress_o,
// terminal fault report (held until clear_i)
output logic fault_req_o,
output logic [ 3:0] fault_code_o,
output logic [31:0] fault_addr_o,
output logic [31:0] fault_info_o,
output logic [31:0] fault_desc_o
);
localparam logic [31:0] FaultInfoInternal = {16'd0, 8'd255, 4'd0, 2'd0, 2'd0};
typedef enum logic [4:0] {
DecIdle,
DecFetchCmd,
DecFetchData,
DecDrain,
DecCompute,
DecCompute2,
DecCompute3,
DecStruct1,
DecStruct2,
DecStruct3,
DecStruct4,
DecOpcode,
DecUnused,
DecGeom1,
DecGeom2,
DecOper,
DecOperCommon,
DecStride,
DecAlign,
DecOverlap,
DecEmit,
DecWaitRetire,
DecFault
} state_e;
state_e s_state_d, s_state_q;
logic [31:0] s_words_q[32];
logic [31:0] s_base_d, s_base_q;
logic [15:0] s_count_d, s_count_q;
logic [15:0] s_index_d, s_index_q;
logic [ 4:0] s_beat_q;
logic [ 3:0] s_fault_code_q;
logic [31:0] s_fault_addr_q;
logic [31:0] s_fault_info_q;
logic [15:0] s_fault_index_q;
logic [31:0] s_desc_addr;
logic s_fetch_wrap;
logic s_cmd_accept;
logic s_stream_accept;
logic s_last_accept;
// decoded record views of the staged words
logic [ 7:0] s_opcode;
logic [15:0] s_dim_h, s_dim_w, s_dim_cin, s_dim_cout, s_dim_oh, s_dim_ow;
logic [7:0] s_kh, s_kw, s_sh, s_sw;
logic [7:0] s_pad_t, s_pad_b, s_pad_l, s_pad_r;
logic [7:0] s_tile_h, s_tile_w;
logic s_is_kernel_op;
logic s_is_weighted_op;
logic s_is_param_op;
logic s_is_add;
logic s_is_gap;
logic signed [31:0] s_extent_h;
logic signed [31:0] s_extent_w;
logic [15:0] s_khkw;
logic [63:0] s_full_k;
logic [63:0] s_weight_expect;
logic [63:0] s_span_in0;
logic [63:0] s_span_in1;
logic [63:0] s_span_out;
logic [63:0] s_min_row_in;
logic [63:0] s_min_row_out;
logic [63:0] s_region_end;
// registered derived quantities (staged across DecCompute..DecCompute3 with
// at most one multiply level per stage, consumed by the check sequencer:
// keeps the wide multiplies/compares off the FSM path)
logic signed [31:0] s_extent_h_q;
logic signed [31:0] s_extent_w_q;
logic [15:0] s_khkw_q;
logic [63:0] s_full_k_q;
logic [63:0] s_weight_expect_q;
logic [63:0] s_span_in0_q;
logic [63:0] s_span_in1_q;
logic [63:0] s_span_out_q;
logic [63:0] s_min_row_in_q;
logic [63:0] s_min_row_out_q;
logic [63:0] s_region_end_q;
logic [63:0] s_rng_base_q [6];
logic [63:0] s_rng_end_q [6];
logic s_overlap_pair_q;
// per-check-state combinational verdict
logic s_chk_fail;
logic [ 3:0] s_chk_code;
logic [ 5:0] s_chk_word;
logic s_chk_wordless;
// overlap ranges
logic [63:0] s_rng_base [6];
logic [63:0] s_rng_end [6];
logic [ 5:0] s_rng_valid;
logic s_overlap_pair;
assign s_desc_addr = s_base_q + {9'd0, s_index_q, 7'd0};
assign s_fetch_wrap = ({1'b0, s_desc_addr} + 33'd128) > 33'h1_0000_0000;
assign s_cmd_accept = read_req_valid_o && read_req_ready_i;
assign s_stream_accept = read_data_valid_i && read_data_ready_o;
assign s_last_accept = s_stream_accept && read_last_i;
assign s_opcode = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_VERSION_OPCODE][7:0];
assign s_dim_h = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_INPUT_HW][15:0];
assign s_dim_w = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_INPUT_HW][31:16];
assign s_dim_cin = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_CHANNELS][15:0];
assign s_dim_cout = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_CHANNELS][31:16];
assign s_dim_oh = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_OUTPUT_HW][15:0];
assign s_dim_ow = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_OUTPUT_HW][31:16];
assign s_kh = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_KERNEL_STRIDE][7:0];
assign s_kw = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_KERNEL_STRIDE][15:8];
assign s_sh = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_KERNEL_STRIDE][23:16];
assign s_sw = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_KERNEL_STRIDE][31:24];
assign s_pad_t = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_PADDING][7:0];
assign s_pad_b = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_PADDING][15:8];
assign s_pad_l = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_PADDING][23:16];
assign s_pad_r = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_PADDING][31:24];
assign s_tile_h = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_TILE_HW][7:0];
assign s_tile_w = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_TILE_HW][15:8];
assign s_is_kernel_op = (s_opcode == `APB4_NPU__OP_CONV2D) ||
(s_opcode == `APB4_NPU__OP_DEPTHWISE3X3) || (s_opcode == `APB4_NPU__OP_MAX_POOL) ||
(s_opcode == `APB4_NPU__OP_AVERAGE_POOL);
assign s_is_weighted_op = (s_opcode == `APB4_NPU__OP_CONV2D) ||
(s_opcode == `APB4_NPU__OP_DEPTHWISE3X3) || (s_opcode == `APB4_NPU__OP_FULLY_CONNECTED);
assign s_is_param_op = s_is_weighted_op || (s_opcode == `APB4_NPU__OP_ADD);
assign s_is_add = (s_opcode == `APB4_NPU__OP_ADD);
assign s_is_gap = (s_opcode == `APB4_NPU__OP_GLOBAL_AVERAGE_POOL);
// Computed output extent: positive floor semantics reproduced with the
// multiply-frame equivalence floor(t/s)+1 == e <=> t >= 0 &&
// (e-1)*s <= t < e*s for s > 0, which avoids a hardware divider.
assign s_extent_h = $signed(
{16'd0, s_dim_h}
) + $signed(
{24'd0, s_pad_t}
) + $signed(
{24'd0, s_pad_b}
) - $signed(
{24'd0, s_kh}
);
assign s_extent_w = $signed(
{16'd0, s_dim_w}
) + $signed(
{24'd0, s_pad_l}
) + $signed(
{24'd0, s_pad_r}
) - $signed(
{24'd0, s_kw}
);
// Kernel-position count: kh*kw is the first multiply level, registered in
// DecCompute; the per-opcode reduction below completes in DecCompute2.
assign s_khkw = {8'd0, s_kh} * {8'd0, s_kw};
always_comb begin
unique case (s_opcode)
`APB4_NPU__OP_CONV2D: begin
s_full_k = {48'd0, s_khkw_q} * {48'd0, s_dim_cin};
end
`APB4_NPU__OP_DEPTHWISE3X3: s_full_k = 64'd9;
`APB4_NPU__OP_FULLY_CONNECTED: s_full_k = {48'd0, s_dim_cin};
`APB4_NPU__OP_MAX_POOL, `APB4_NPU__OP_AVERAGE_POOL: begin
s_full_k = {48'd0, s_khkw_q};
end
`APB4_NPU__OP_GLOBAL_AVERAGE_POOL: s_full_k = {48'd0, s_dim_h} * {48'd0, s_dim_w};
default: s_full_k = 64'd1;
endcase
end
// Expected weight bytes: the last multiply level, registered in DecCompute3.
always_comb begin
if (s_opcode == `APB4_NPU__OP_DEPTHWISE3X3) begin
s_weight_expect = (({48'd0, s_dim_cin} + 64'd7) >> 3) * 64'd72;
end else begin
s_weight_expect = (({48'd0, s_dim_cout} + 64'd7) >> 3) * s_full_k_q * 64'd8;
end
end
assign s_min_row_in = {48'd0, s_dim_w} * {48'd0, s_dim_cin};
assign s_min_row_out = {48'd0, s_dim_ow} * {48'd0, s_dim_cout};
assign s_span_in0 = ({48'd0, s_dim_h} - 64'd1) *
{32'd0, s_words_q[`APB4_NPU__DESCRIPTOR_WORD_INPUT0_ROW_BYTES]} + s_min_row_in_q;
assign s_span_in1 = ({48'd0, s_dim_h} - 64'd1) *
{32'd0, s_words_q[`APB4_NPU__DESCRIPTOR_WORD_INPUT1_ROW_BYTES]} + s_min_row_in_q;
assign s_span_out = ({48'd0, s_dim_oh} - 64'd1) *
{32'd0, s_words_q[`APB4_NPU__DESCRIPTOR_WORD_OUTPUT_ROW_BYTES]} + s_min_row_out_q;
assign s_region_end = {32'd0, s_base_q} + {41'd0, s_count_q, 7'd0};
// Overlap ranges in the golden validator's membership order: input0,
// output, input1 (ADD only), weights, parameters, descriptor region.
always_comb begin
s_rng_base[0] = {32'd0, s_words_q[`APB4_NPU__DESCRIPTOR_WORD_INPUT0_BASE]};
s_rng_end[0] = s_rng_base[0] + {32'd0, s_words_q[`APB4_NPU__DESCRIPTOR_WORD_INPUT0_BYTES]};
s_rng_base[1] = {32'd0, s_words_q[`APB4_NPU__DESCRIPTOR_WORD_OUTPUT_BASE]};
s_rng_end[1] = s_rng_base[1] + {32'd0, s_words_q[`APB4_NPU__DESCRIPTOR_WORD_OUTPUT_BYTES]};
s_rng_base[2] = {32'd0, s_words_q[`APB4_NPU__DESCRIPTOR_WORD_INPUT1_BASE]};
s_rng_end[2] = s_rng_base[2] + {32'd0, s_words_q[`APB4_NPU__DESCRIPTOR_WORD_INPUT1_BYTES]};
s_rng_base[3] = {32'd0, s_words_q[`APB4_NPU__DESCRIPTOR_WORD_WEIGHT_BASE]};
s_rng_end[3] = s_rng_base[3] + {32'd0, s_words_q[`APB4_NPU__DESCRIPTOR_WORD_WEIGHT_BYTES]};
s_rng_base[4] = {32'd0, s_words_q[`APB4_NPU__DESCRIPTOR_WORD_PARAM_BASE]};
s_rng_end[4] = s_rng_base[4] + {32'd0, s_words_q[`APB4_NPU__DESCRIPTOR_WORD_PARAM_BYTES]};
s_rng_base[5] = {32'd0, s_base_q};
s_rng_end[5] = s_region_end;
s_rng_valid[0] = 1'b1;
s_rng_valid[1] = 1'b1;
s_rng_valid[2] = s_is_add;
s_rng_valid[3] = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_WEIGHT_BYTES] != 32'd0;
s_rng_valid[4] = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_PARAM_BYTES] != 32'd0;
s_rng_valid[5] = 1'b1;
s_overlap_pair = 1'b0;
for (int unsigned range_a = 0; range_a < 5; range_a++) begin
for (int unsigned range_b = range_a + 1; range_b < 6; range_b++) begin
if (s_rng_valid[range_a] && s_rng_valid[range_b] &&
!(s_is_add && (range_a == 0) && (range_b == 2))) begin
if ((s_rng_base[range_a] < s_rng_end[range_b]) &&
(s_rng_base[range_b] < s_rng_end[range_a])) begin
s_overlap_pair = 1'b1;
end
end
end
end
end
// Validation check sequencer: one state per golden-validator stage, with the
// intra-stage priority matching the script's first-fault order.
always_comb begin
logic [31:0] s_w0;
logic [31:0] s_w1;
logic [31:0] s_w3;
logic [31:0] s_w5;
logic [31:0] s_w11;
logic [31:0] s_w12;
logic [31:0] s_w13;
logic [31:0] s_w14;
logic [31:0] s_w15;
logic [31:0] s_word16;
logic [31:0] s_w17;
logic [31:0] s_w18;
logic [31:0] s_w19;
logic [31:0] s_w20;
logic [31:0] s_w21;
logic [31:0] s_w22;
logic [31:0] s_w23;
logic [31:0] s_w24;
logic [31:0] s_w25;
logic s_extent_h_pos;
logic s_extent_w_pos;
logic s_extent_h_match;
logic s_extent_w_match;
logic [31:0] s_extent_h_u;
logic [31:0] s_extent_w_u;
s_w0 = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_VERSION_OPCODE];
s_w1 = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_RESERVED];
s_w3 = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_INPUT1_BASE];
s_w5 = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_PARAM_BASE];
s_w11 = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_INPUT1_ROW_BYTES];
s_w12 = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_KERNEL_STRIDE];
s_w13 = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_PADDING];
s_w14 = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_TILE_HW];
s_w15 = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_K_SLICE];
s_word16 = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_INPUT0_BYTES];
s_w17 = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_INPUT1_BYTES];
s_w18 = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_OUTPUT_BYTES];
s_w19 = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_PARAM_BYTES];
s_w20 = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_INPUT0_ZERO];
s_w21 = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_INPUT1_ZERO];
s_w22 = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_OUTPUT_ZERO];
s_w23 = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_ACTIVATION_BOUNDS];
s_w24 = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_WEIGHT_BASE];
s_w25 = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_WEIGHT_BYTES];
s_extent_h_pos = !s_extent_h_q[31];
s_extent_w_pos = !s_extent_w_q[31];
s_extent_h_u = {1'b0, s_extent_h_q[30:0]};
s_extent_w_u = {1'b0, s_extent_w_q[30:0]};
s_extent_h_match = (({16'd0, s_dim_oh} - 32'd1) * {24'd0, s_sh} <= s_extent_h_u) &&
(s_extent_h_u < {16'd0, s_dim_oh} * {24'd0, s_sh});
s_extent_w_match = (({16'd0, s_dim_ow} - 32'd1) * {24'd0, s_sw} <= s_extent_w_u) &&
(s_extent_w_u < {16'd0, s_dim_ow} * {24'd0, s_sw});
s_chk_fail = 1'b0;
s_chk_code = `APB4_NPU__FAULT_CODE_DESCRIPTOR;
s_chk_word = 6'd0;
s_chk_wordless = 1'b0;
unique case (s_state_q)
DecStruct1: begin
if (s_w0[31:16] != `APB4_NPU__DESCRIPTOR_VERSION) begin
s_chk_fail = 1'b1;
end else if (s_w0[15:8] != 8'd0) begin
s_chk_fail = 1'b1;
end else if (s_w1 != 32'd0) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd1;
end
end
DecStruct2: begin
for (int unsigned word = 26; word < 32; word++) begin
if (!s_chk_fail && (s_words_q[word] != 32'd0)) begin
s_chk_fail = 1'b1;
s_chk_word = 6'(word);
end
end
end
DecStruct3: begin
if (s_w14[31:16] != 16'd0) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd14;
end else if (s_w20[31:8] != 24'd0) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd20;
end else if (s_w22[31:8] != 24'd0) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd22;
end else if (s_w23[31:16] != 16'd0) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd23;
end else if ($signed(s_w23[7:0]) > $signed(s_w23[15:8])) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd23;
end
end
DecStruct4: begin
if ((s_tile_h == 8'd0) || (s_tile_h > 8'd8) || (s_tile_w == 8'd0) ||
(s_tile_w > 8'd8)) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd14;
end else if ({24'd0, s_tile_h} * {24'd0, s_tile_w} > 32'd8) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd14;
end else if ((s_w15 == 32'd0) || (s_w15 > 32'd1024)) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd15;
end
end
DecOpcode: begin
if ((s_opcode == 8'd0) || (s_opcode > 8'd8)) begin
s_chk_fail = 1'b1;
s_chk_code = `APB4_NPU__FAULT_CODE_UNSUPPORTED;
end
end
DecUnused: begin
if (!s_is_add && (s_w3 != 32'd0)) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd3;
end else if (!s_is_add && (s_w11 != 32'd0)) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd11;
end else if (!s_is_add && (s_w17 != 32'd0)) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd17;
end else if (!s_is_add && (s_w21 != 32'd0)) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd21;
end else if (!s_is_weighted_op && (s_w24 != 32'd0)) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd24;
end else if (!s_is_weighted_op && (s_w25 != 32'd0)) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd25;
end else if (!s_is_param_op && (s_w5 != 32'd0)) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd5;
end else if (!s_is_param_op && (s_w19 != 32'd0)) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd19;
end else if (s_is_gap && (s_w12 != 32'd0)) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd12;
end else if (s_is_gap && (s_w13 != 32'd0)) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd13;
end
end
DecGeom1: begin
s_chk_code = `APB4_NPU__FAULT_CODE_RANGE;
if ((s_dim_h == 16'd0) || (s_dim_h > 16'd4096)) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd6;
end else if ((s_dim_w == 16'd0) || (s_dim_w > 16'd4096)) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd6;
end else if ((s_dim_cin == 16'd0) || (s_dim_cin > 16'd4096)) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd7;
end else if ((s_dim_cout == 16'd0) || (s_dim_cout > 16'd4096)) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd7;
end else if ((s_dim_oh == 16'd0) || (s_dim_oh > 16'd4096)) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd8;
end else if ((s_dim_ow == 16'd0) || (s_dim_ow > 16'd4096)) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd8;
end
end
DecGeom2: begin
if (s_is_kernel_op) begin
s_chk_code = `APB4_NPU__FAULT_CODE_RANGE;
if ((s_kh == 8'd0) || (s_kw == 8'd0) || (s_sh == 8'd0) || (s_sw == 8'd0)) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd12;
end else if (({24'd0, s_pad_t} >= {24'd0, s_kh}) ||
({24'd0, s_pad_b} >= {24'd0, s_kh}) ||
({24'd0, s_pad_l} >= {24'd0, s_kw}) ||
({24'd0, s_pad_r} >= {24'd0, s_kw})) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd13;
end else if (!s_extent_h_pos || !s_extent_w_pos) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd8;
end else if (!s_extent_h_match || !s_extent_w_match) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd8;
end
end
end
DecOper: begin
unique case (s_opcode)
`APB4_NPU__OP_CONV2D: begin
s_chk_code = `APB4_NPU__FAULT_CODE_UNSUPPORTED;
if ((s_kh > 8'd16) || (s_kw > 8'd16)) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd12;
end else if (((s_sh != 8'd1) && (s_sh != 8'd2)) ||
((s_sw != 8'd1) && (s_sw != 8'd2))) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd12;
end
end
`APB4_NPU__OP_DEPTHWISE3X3: begin
s_chk_code = `APB4_NPU__FAULT_CODE_UNSUPPORTED;
if ((s_kh != 8'd3) || (s_kw != 8'd3)) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd12;
end else if (((s_sh != 8'd1) && (s_sh != 8'd2)) ||
((s_sw != 8'd1) && (s_sw != 8'd2))) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd12;
end else if (s_dim_cout != s_dim_cin) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd7;
end
end
`APB4_NPU__OP_FULLY_CONNECTED: begin
s_chk_code = `APB4_NPU__FAULT_CODE_UNSUPPORTED;
if ((s_dim_h != 16'd1) || (s_dim_w != 16'd1)) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd6;
end else if ((s_dim_oh != 16'd1) || (s_dim_ow != 16'd1)) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd8;
end else if (s_w12 != 32'h0101_0101) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd12;
end else if (s_w13 != 32'd0) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd13;
end
end
`APB4_NPU__OP_ADD: begin
if (s_w21[31:8] != 24'd0) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd21;
end else if (s_w3 == 32'd0) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd3;
end else begin
s_chk_code = `APB4_NPU__FAULT_CODE_UNSUPPORTED;
if ((s_dim_oh != s_dim_h) || (s_dim_ow != s_dim_w)) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd8;
end else if (s_dim_cout != s_dim_cin) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd7;
end else if (s_w12 != 32'h0101_0101) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd12;
end else if (s_w13 != 32'd0) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd13;
end
end
end
`APB4_NPU__OP_MAX_POOL, `APB4_NPU__OP_AVERAGE_POOL: begin
s_chk_code = `APB4_NPU__FAULT_CODE_UNSUPPORTED;
if ((s_kh > 8'd16) || (s_kw > 8'd16)) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd12;
end else if (((s_sh != 8'd1) && (s_sh != 8'd2)) ||
((s_sw != 8'd1) && (s_sw != 8'd2))) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd12;
end else if (s_dim_cout != s_dim_cin) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd7;
end else if (s_w20[7:0] != s_w22[7:0]) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd22;
end
end
`APB4_NPU__OP_GLOBAL_AVERAGE_POOL: begin
s_chk_code = `APB4_NPU__FAULT_CODE_UNSUPPORTED;
if ((s_dim_oh != 16'd1) || (s_dim_ow != 16'd1)) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd8;
end else if (s_dim_cout != s_dim_cin) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd7;
end else if (s_w20[7:0] != s_w22[7:0]) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd22;
end
end
default: begin
// CLAMP
s_chk_code = `APB4_NPU__FAULT_CODE_UNSUPPORTED;
if ((s_dim_oh != s_dim_h) || (s_dim_ow != s_dim_w)) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd8;
end else if (s_dim_cout != s_dim_cin) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd7;
end else if (s_w20[7:0] != s_w22[7:0]) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd22;
end else if (s_w12 != 32'h0101_0101) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd12;
end else if (s_w13 != 32'd0) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd13;
end
end
endcase
end
DecOperCommon: begin
if ({32'd0, s_w15} > s_full_k_q) begin
s_chk_fail = 1'b1;
s_chk_code = `APB4_NPU__FAULT_CODE_UNSUPPORTED;
s_chk_word = 6'd15;
end else if (({24'd0, s_tile_h} > {16'd0, s_dim_oh}) ||
({24'd0, s_tile_w} > {16'd0, s_dim_ow})) begin
s_chk_fail = 1'b1;
s_chk_code = `APB4_NPU__FAULT_CODE_RANGE;
s_chk_word = 6'd14;
end else if (s_is_weighted_op) begin
if (s_w24 == 32'd0) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd24;
end else if ({32'd0, s_w25} != s_weight_expect_q) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd25;
end else if (s_w5 == 32'd0) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd5;
end else if ({32'd0, s_w19} != ({48'd0, s_dim_cout} * 64'd16)) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd19;
end
end else if (s_is_add) begin
if (s_w5 == 32'd0) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd5;
end else if (s_w19 != 32'd32) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd19;
end
end
end
DecStride: begin
s_chk_code = `APB4_NPU__FAULT_CODE_RANGE;
if ({32'd0, s_words_q[`APB4_NPU__DESCRIPTOR_WORD_INPUT0_ROW_BYTES]} < s_min_row_in_q) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd9;
end else if ({32'd0, s_words_q[`APB4_NPU__DESCRIPTOR_WORD_OUTPUT_ROW_BYTES]} <
s_min_row_out_q) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd10;
end else if (s_is_add && ({32'd0, s_w11} < s_min_row_in_q)) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd11;
end else if (s_span_in0_q > {32'd0, s_word16}) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd16;
end else if ((s_rng_base_q[0] + s_span_in0_q) > 64'h1_0000_0000) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd2;
end else if (s_rng_end_q[0] > 64'h1_0000_0000) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd2;
end else if (s_is_add && (s_span_in1_q > {32'd0, s_w17})) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd17;
end else if (s_is_add && ((s_rng_base_q[2] + s_span_in1_q) > 64'h1_0000_0000)) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd3;
end else if (s_is_add && (s_rng_end_q[2] > 64'h1_0000_0000)) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd3;
end else if (s_span_out_q > {32'd0, s_w18}) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd18;
end else if ((s_rng_base_q[1] + s_span_out_q) > 64'h1_0000_0000) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd4;
end else if (s_rng_end_q[1] > 64'h1_0000_0000) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd4;
end else if ((s_w25 != 32'd0) && (s_rng_end_q[3] > 64'h1_0000_0000)) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd24;
end else if ((s_w19 != 32'd0) && (s_rng_end_q[4] > 64'h1_0000_0000)) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd5;
end
end
DecAlign: begin
if ((s_w5 != 32'd0) && (s_w5[5:0] != 6'd0)) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd5;
end else if ((s_w24 != 32'd0) && (s_w24[5:0] != 6'd0)) begin
s_chk_fail = 1'b1;
s_chk_word = 6'd24;
end
end
DecOverlap: begin
s_chk_code = `APB4_NPU__FAULT_CODE_RANGE;
s_chk_wordless = 1'b1;
if (s_region_end_q > 64'h1_0000_0000) begin
s_chk_fail = 1'b1;
end else if (s_overlap_pair_q) begin
s_chk_fail = 1'b1;
end
end
default: begin
end
endcase
end
assign read_req_valid_o = (s_state_q == DecFetchCmd) && !s_fetch_wrap && !clear_i;
assign read_addr_o = s_desc_addr;
assign read_bytes_o = 32'd128;
assign read_data_ready_o = ((s_state_q == DecFetchData) || (s_state_q == DecDrain)) && !clear_i;
assign busy_o = (s_state_q != DecIdle);
assign pause_ok_o = (s_state_q != DecFetchData) && (s_state_q != DecDrain);
assign cur_desc_index_o = s_index_q;
assign progress_o = s_stream_accept || (rec_valid_o && rec_ready_i) || retired_i;
assign fault_req_o = (s_state_q == DecFault);
assign rec_valid_o = (s_state_q == DecEmit) && !clear_i;
// all_done pulses when the final record retires; the core owns publication.
assign all_done_o = (s_state_q == DecWaitRetire) && retired_i &&
((s_index_q + 16'd1) == s_count_q);
assign fault_code_o = s_fault_code_q;
assign fault_addr_o = s_fault_addr_q;
assign fault_info_o = s_fault_info_q;
assign fault_desc_o = {16'd0, s_fault_index_q};
assign rec_opcode_o = 4'(s_opcode);
assign rec_h_o = 13'(s_dim_h);
assign rec_w_o = 13'(s_dim_w);
assign rec_cin_o = 13'(s_dim_cin);
assign rec_cout_o = 13'(s_dim_cout);
assign rec_oh_o = 13'(s_dim_oh);
assign rec_ow_o = 13'(s_dim_ow);
assign rec_input0_base_o = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_INPUT0_BASE];
assign rec_output_base_o = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_OUTPUT_BASE];
assign rec_weight_base_o = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_WEIGHT_BASE];
assign rec_param_base_o = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_PARAM_BASE];
assign rec_input0_row_bytes_o = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_INPUT0_ROW_BYTES];
assign rec_output_row_bytes_o = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_OUTPUT_ROW_BYTES];
assign rec_kh_o = s_kh;
assign rec_kw_o = s_kw;
assign rec_sh_o = s_sh;
assign rec_sw_o = s_sw;
assign rec_pad_top_o = s_pad_t;
assign rec_pad_left_o = s_pad_l;
assign rec_input0_zero_o = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_INPUT0_ZERO][7:0];
assign rec_weight_bytes_o = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_WEIGHT_BYTES];
assign rec_param_bytes_o = s_words_q[`APB4_NPU__DESCRIPTOR_WORD_PARAM_BYTES];
assign rec_desc_index_o = s_index_q;
always_comb begin
s_state_d = s_state_q;
s_base_d = s_base_q;
s_count_d = s_count_q;
s_index_d = s_index_q;
unique case (s_state_q)
DecIdle: begin
if (job_start_i) begin
s_base_d = job_base_i;
s_count_d = job_count_i;
s_index_d = 16'd0;
s_state_d = DecFetchCmd;
end
end
DecFetchCmd: begin
// A wrapped record address is a local RANGE rejection, never a bus
// request; the DMA is never asked to fetch outside the 32-bit space.
if (s_fetch_wrap || dma_fault_i) begin
s_state_d = DecFault;
end else if (s_cmd_accept) begin
s_state_d = DecFetchData;
end
end
DecFetchData: begin
if (dma_fault_i) begin
s_state_d = DecFault;
end else if (s_last_accept) begin
s_state_d = DecCompute;
end
end
// Three pipeline cycles that register every wide derived quantity (one
// multiply level each); the check sequencer below consumes registers.
DecCompute: s_state_d = DecCompute2;
DecCompute2: s_state_d = DecCompute3;
DecCompute3: s_state_d = DecStruct1;
DecStruct1: s_state_d = s_chk_fail ? DecFault : DecStruct2;
DecStruct2: s_state_d = s_chk_fail ? DecFault : DecStruct3;
DecStruct3: s_state_d = s_chk_fail ? DecFault : DecStruct4;
DecStruct4: s_state_d = s_chk_fail ? DecFault : DecOpcode;
DecOpcode: s_state_d = s_chk_fail ? DecFault : DecUnused;
DecUnused: s_state_d = s_chk_fail ? DecFault : DecGeom1;
DecGeom1: s_state_d = s_chk_fail ? DecFault : DecGeom2;
DecGeom2: s_state_d = s_chk_fail ? DecFault : DecOper;
DecOper: s_state_d = s_chk_fail ? DecFault : DecOperCommon;
DecOperCommon: s_state_d = s_chk_fail ? DecFault : DecStride;
DecStride: s_state_d = s_chk_fail ? DecFault : DecAlign;
DecAlign: s_state_d = s_chk_fail ? DecFault : DecOverlap;
DecOverlap: s_state_d = s_chk_fail ? DecFault : DecEmit;
DecEmit: begin
if (rec_ready_i) begin
s_state_d = DecWaitRetire;
end
end
DecWaitRetire: begin
if (retired_i) begin
if ((s_index_q + 16'd1) == s_count_q) begin
s_state_d = DecIdle;
end else begin
s_index_d = s_index_q + 16'd1;
s_state_d = DecFetchCmd;
end
end
end
DecDrain: begin
// Coordinated-cancel fetch drain: the accepted read segment always
// runs out; after a DMA fault the stream is consumed while it lasts.
if (s_last_accept || (!dma_read_busy_i && !read_data_valid_i)) begin
s_state_d = DecIdle;
end
end
DecFault: begin
// Terminal until the core's coordinated cancel.
end
default: s_state_d = DecIdle;
endcase
if (clear_i) begin
unique case (s_state_q)
// A presented-but-unaccepted fetch is withdrawn; an accepted one is
// drained like any other in-flight segment.
DecFetchCmd: s_state_d = s_cmd_accept ? DecDrain : DecIdle;
DecFetchData: s_state_d = DecDrain;
default: s_state_d = DecIdle;
endcase
end
end
always_ff @(posedge clk_hp_i or negedge rst_hp_n_i) begin
if (!rst_hp_n_i) begin
s_state_q <= DecIdle;
s_base_q <= '0;
s_count_q <= '0;
s_index_q <= '0;
s_beat_q <= '0;
s_fault_code_q <= '0;
s_fault_addr_q <= '0;
s_fault_info_q <= '0;
s_fault_index_q <= '0;
s_extent_h_q <= '0;
s_extent_w_q <= '0;
s_khkw_q <= '0;
s_full_k_q <= '0;
s_weight_expect_q <= '0;
s_span_in0_q <= '0;
s_span_in1_q <= '0;
s_span_out_q <= '0;
s_min_row_in_q <= '0;
s_min_row_out_q <= '0;
s_region_end_q <= '0;
s_overlap_pair_q <= 1'b0;
for (int unsigned range = 0; range < 6; range++) begin
s_rng_base_q[range] <= '0;
s_rng_end_q[range] <= '0;
end
for (int unsigned word = 0; word < 32; word++) begin
s_words_q[word] <= '0;
end
end else begin
s_state_q <= s_state_d;
s_base_q <= s_base_d;
s_count_q <= s_count_d;
s_index_q <= s_index_d;
if (s_cmd_accept) begin
s_beat_q <= 5'd0;
end else if ((s_state_q == DecFetchData) && s_stream_accept) begin
s_beat_q <= s_beat_q + 5'd1;
end
if ((s_state_q == DecFetchData) && s_stream_accept) begin
s_words_q[{s_beat_q[3:0], 1'b0}] <= read_data_i[31:0];
s_words_q[{s_beat_q[3:0], 1'b1}] <= read_data_i[63:32];
end
if (s_state_q == DecCompute) begin
s_extent_h_q <= s_extent_h;
s_extent_w_q <= s_extent_w;
s_khkw_q <= s_khkw;
s_min_row_in_q <= s_min_row_in;
s_min_row_out_q <= s_min_row_out;
s_region_end_q <= s_region_end;
s_overlap_pair_q <= s_overlap_pair;
for (int unsigned range = 0; range < 6; range++) begin
s_rng_base_q[range] <= s_rng_base[range];
s_rng_end_q[range] <= s_rng_end[range];
end
end
if (s_state_q == DecCompute2) begin
s_full_k_q <= s_full_k;
s_span_in0_q <= s_span_in0;
s_span_in1_q <= s_span_in1;
s_span_out_q <= s_span_out;
end
if (s_state_q == DecCompute3) begin
s_weight_expect_q <= s_weight_expect;
end
if ((s_state_q != DecFault) && (s_state_d == DecFault)) begin
if (s_fetch_wrap) begin
// Local wrap rejection: RANGE with no single offending word.
s_fault_code_q <= `APB4_NPU__FAULT_CODE_RANGE;
s_fault_addr_q <= s_desc_addr;
s_fault_info_q <= FaultInfoInternal;
end else if (dma_fault_i) begin
// Bus fault while fetching: forward code/address/response.
s_fault_code_q <= dma_fault_code_i;
s_fault_addr_q <= dma_fault_addr_i;
s_fault_info_q <= {16'd0, 8'd255, 4'd0, 2'd1, dma_fault_resp_i};
end else begin
s_fault_code_q <= s_chk_code;
s_fault_addr_q <= s_chk_wordless ? s_desc_addr :
(s_desc_addr + {24'd0, s_chk_word, 2'b00});
s_fault_info_q <= FaultInfoInternal;
end
s_fault_index_q <= s_index_q;
end
end
end
`ifndef SV_ASSRT_DISABLE
logic s_rec_wait_q;
logic [4:0] s_beat_seen_q;
always_ff @(posedge clk_hp_i or negedge rst_hp_n_i) begin
if (!rst_hp_n_i) begin
s_rec_wait_q <= 1'b0;
s_beat_seen_q <= 5'd0;
end else begin
s_rec_wait_q <= rec_valid_o && !rec_ready_i;
if (s_state_q == DecFetchCmd) begin
s_beat_seen_q <= 5'd0;
end else if ((s_state_q == DecFetchData) && s_stream_accept) begin
s_beat_seen_q <= s_beat_seen_q + 5'd1;
end
// A presented record holds stable until the scheduler accepts it.
if (s_rec_wait_q) begin
assert (rec_valid_o);
end
// A descriptor fetch is exactly sixteen 64-bit beats.
if ((s_state_q == DecFetchData) && (s_state_d == DecCompute)) begin
assert (s_beat_seen_q == 5'd15);
end
// The staging writes stay inside the 32-word record.
if ((s_state_q == DecFetchData) && s_stream_accept) begin
assert (s_beat_q < 5'd16);
end
end
end
`endif
endmodule
File diff suppressed because it is too large. Load diff
+6
View File
@@ -179,6 +179,12 @@
/ip/multimedia/jpeg_dma.sv
/ip/multimedia/jpeg_reg.sv
/ip/multimedia/apb4_jpeg.sv
/ip/multimedia/npu_pkg.sv
/ip/multimedia/npu_local_sram.sv
/ip/multimedia/npu_patch_packer.sv
/ip/multimedia/npu_dma.sv
/ip/multimedia/npu_job_decoder.sv
/ip/multimedia/npu_scheduler.sv
/ip/multimedia/npu_reg.sv
/ip/multimedia/npu_control_cdc.sv
/ip/multimedia/npu_core.sv
@@ -359,6 +359,12 @@ def source_files(target: str) -> list[Path]:
TOP / "hp_axi4_mux3.sv",
SCRIPT_DIR / "gateway_a_formal.sv",
]
if target == "npu_dma":
return [
COMMON_RTL / "interface/axi4_if.sv",
MULTIMEDIA / "npu_dma.sv",
SCRIPT_DIR / "npu_dma_formal.sv",
]
if target == "sdio":
return [
COMMON_RTL / "interface/apb4_if.sv",
@@ -436,6 +442,7 @@ def parse_args() -> argparse.Namespace:
"apu_sequencer",
"gateway_a",
"sdio",
"npu_dma",
),
required=True,
)
+488
View File
@@ -0,0 +1,488 @@
// Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
// retroSoC is licensed under Mulan PSL v2.
// You can use this software according to the terms and conditions of the Mulan PSL v2.
// You may obtain a copy of Mulan PSL v2 at:
// http://license.coscl.org.cn/MulanPSL2
// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
// See the Mulan PSL v2 for more details.
// Bounded formal harness for the NPU-P3 AXI4 data-plane engine (npu_dma).
// One legal-but-adversarial command is offered per direction from the first
// active cycle until it is accepted (anyconst address/byte-count fields), so
// edge alignments, short segments and post-flush re-issue are all explored.
// The constrained AXI4 responder below keeps at most one outstanding burst
// per direction, runs independent per-channel stall state machines (0..3
// stall cycles per handshake beat, chosen once per run by anyconst
// personalities), and optionally injects SLVERR/DECERR or malformed
// RID/RLAST/BID responses selected by anyconst knobs. block_new_i and
// clear_i toggle inside anyconst-timed windows; clear_i models the
// coordinated fabric flush, so the responder cancels in-flight transfers
// together with the engine and never presents residual R/B beats to a
// flushed receiver (BMC-sound suppression). A burst quarantined by a
// malformed response is likewise retired silently because the engine stops
// its receiver until the flush. All legality assumptions on the command and
// stream sources live in npu_dma_formal_props.sv.
module npu_dma_formal_design (
// verilog_format: off -- protocol observations are grouped by channel.
input logic clk_i,
output logic rst_n_i,
output logic f_past_valid,
output logic clear,
output logic block_new,
output logic pause_ack,
output logic read_req_valid,
output logic read_req_ready,
output logic [31:0] read_addr,
output logic [31:0] read_bytes,
output logic read_data_valid,
output logic read_data_ready,
output logic [63:0] read_data,
output logic [ 7:0] read_keep,
output logic read_last,
output logic write_req_valid,
output logic write_req_ready,
output logic [31:0] write_addr,
output logic [31:0] write_bytes,
output logic write_data_valid,
output logic write_data_ready,
output logic [63:0] write_data,
output logic [ 7:0] write_keep,
output logic write_last,
output logic write_done,
output logic busy,
output logic read_busy,
output logic write_busy,
output logic [63:0] read_bytes_done,
output logic [63:0] write_bytes_done,
output logic [63:0] stall_cycles,
output logic fault,
output logic [ 3:0] fault_code,
output logic [31:0] fault_addr,
output logic [ 1:0] fault_resp,
output logic read_cmd_err,
output logic write_cmd_err,
output logic [ 4:0] rfifo_count,
output logic [ 4:0] wfifo_count,
output logic [ 1:0] read_fault,
output logic [63:0] read_data_const,
output logic [63:0] write_data_const,
output logic [ 7:0] write_keep_const,
output logic awvalid,
output logic awready,
output logic [ 2:0] awid,
output logic [31:0] awaddr,
output logic [ 7:0] awlen,
output logic [ 2:0] awsize,
output logic [ 1:0] awburst,
output logic awlock,
output logic [ 3:0] awcache,
output logic [ 2:0] awprot,
output logic [ 3:0] awqos,
output logic [ 3:0] awregion,
output logic awuser,
output logic wvalid,
output logic wready,
output logic [63:0] wdata,
output logic [ 7:0] wstrb,
output logic wlast,
output logic wuser,
output logic bvalid,
output logic bready,
output logic [ 2:0] bid,
output logic [ 1:0] bresp,
output logic buser,
output logic arvalid,
output logic arready,
output logic [ 2:0] arid,
output logic [31:0] araddr,
output logic [ 7:0] arlen,
output logic [ 2:0] arsize,
output logic [ 1:0] arburst,
output logic arlock,
output logic [ 3:0] arcache,
output logic [ 2:0] arprot,
output logic [ 3:0] arqos,
output logic [ 3:0] arregion,
output logic aruser,
output logic rvalid,
output logic rready,
output logic [ 2:0] rid,
output logic [63:0] rdata,
output logic [ 1:0] rresp,
output logic rlast,
output logic ruser
// verilog_format: on
);
localparam logic [1:0] AxiRespOkay = 2'b00;
localparam logic [1:0] AxiRespSlverr = 2'b10;
localparam logic [1:0] AxiRespDecerr = 2'b11;
// Command fields, payload constants and fault-injection selectors are
// constant per run. Payload data/keep values never influence the engine's
// control path, so they are anyconst (this also keeps the solver cheap);
// the properties file cross-checks the data path against these constants.
(* anyconst *)logic [31:0] f_read_addr;
(* anyconst *)logic [31:0] f_read_bytes;
(* anyconst *)logic [31:0] f_write_addr;
(* anyconst *)logic [31:0] f_write_bytes;
(* anyconst *)logic [ 1:0] f_read_fault;
(* anyconst *)logic f_read_proto_rlast;
(* anyconst *)logic [ 1:0] f_write_fault;
(* anyconst *)logic [63:0] f_read_data;
(* anyconst *)logic [63:0] f_write_data;
(* anyconst *)logic [ 7:0] f_write_keep;
// Per-channel stall personalities, constant per run: each address
// handshake is stalled f_ar_stall/f_aw_stall cycles before the grant, each
// R beat waits f_r_gap cycles before presentation, each W beat is stalled
// f_w_stall cycles, and B waits f_b_gap cycles after the final W beat.
// Bounding stalls to 0..3 keeps the bounded proof tractable; longer stalls
// add no new engine states because the engine holds its registers while a
// handshake is stalled (payload stability is checked on the first stalled
// cycle already), and the control-event windows below still place
// pause/clear/stream events at every possible stall cycle.
(* anyconst *)logic [ 1:0] f_ar_stall;
(* anyconst *)logic [ 1:0] f_r_gap;
(* anyconst *)logic [ 1:0] f_aw_stall;
(* anyconst *)logic [ 1:0] f_w_stall;
(* anyconst *)logic [ 1:0] f_b_gap;
// Control-event windows, constant per run. Both commands are presented
// from the first active cycle until they are accepted (and are re-presented
// after a flush): the engine has no timers, so an absolute issue phase
// carries no contract meaning. block_new_i is held over the [start, stop)
// window, clear_i pulses for one cycle, and the read stream is stalled over
// its own window. Windows are bounded to the first 16 active cycles, where
// every engine phase of a reachable burst occurs; this keeps every
// adversarial event placement reachable while avoiding per-cycle free
// inputs, which are what make deep bounded queries expensive.
(* anyconst *)logic [ 3:0] f_block_start;
(* anyconst *)logic [ 3:0] f_block_stop;
(* anyconst *)logic f_clear_en;
(* anyconst *)logic [ 3:0] f_clear_at;
(* anyconst *)logic [ 3:0] f_rdstall_start;
(* anyconst *)logic [ 3:0] f_rdstall_stop;
// Only the write stream last flag is free per cycle: the solver chooses a
// correct segment end for success traces or a wrong one to exercise the
// local write_cmd_err abort. Every check in the properties file is a
// safety property and needs no fairness assumption.
(* anyseq *)logic f_write_last;
logic [ 5:0] s_cycle_q;
logic s_read_done_q;
logic s_write_done_q;
logic s_r_active_q;
logic [ 8:0] s_r_beats_q;
logic s_r_beat_valid_q;
logic s_r_first_q;
logic [ 1:0] s_r_resp_q;
logic s_r_proto_rid_q;
logic s_r_proto_rlast_q;
logic s_w_active_q;
logic [ 8:0] s_w_beats_q;
logic s_w_bvalid_q;
logic s_w_bpending_q;
logic [ 1:0] s_w_bresp_q;
logic s_w_bid_bad_q;
logic [ 1:0] s_ar_stall_q;
logic [ 1:0] s_r_gap_q;
logic [ 1:0] s_aw_stall_q;
logic [ 1:0] s_w_stall_q;
logic [ 1:0] s_b_gap_q;
axi4_if #(
.ADDR_WIDTH(32),
.DATA_WIDTH(64),
.ID_WIDTH (3),
.USER_WIDTH(1)
) axi4 (
.aclk (clk_i),
.aresetn(rst_n_i)
);
// Constrained responder: independent per-channel stall state machines
// driven by the anyconst personalities; a burst carries one latched fault
// personality.
assign axi4.arready = !s_r_active_q && !s_r_beat_valid_q && (s_ar_stall_q >= f_ar_stall);
assign axi4.rid = s_r_proto_rid_q ? 3'd1 : 3'd0;
assign axi4.rdata = f_read_data;
assign axi4.rresp = s_r_resp_q;
assign axi4.rlast = (s_r_beats_q == 9'd1) || (s_r_proto_rlast_q && s_r_first_q);
assign axi4.ruser = 1'b0;
assign axi4.rvalid = s_r_beat_valid_q;
assign axi4.awready = !s_w_active_q && !s_w_bvalid_q && !s_w_bpending_q &&
(s_aw_stall_q >= f_aw_stall);
assign axi4.wready = s_w_active_q && (s_w_stall_q >= f_w_stall);
assign axi4.bid = s_w_bid_bad_q ? 3'd1 : 3'd0;
assign axi4.bresp = s_w_bresp_q;
assign axi4.buser = 1'b0;
assign axi4.bvalid = s_w_bvalid_q;
assign read_req_valid = rst_n_i && !s_read_done_q;
assign read_addr = f_read_addr;
assign read_bytes = f_read_bytes;
assign read_data_ready = rst_n_i &&
!((s_cycle_q >= {2'd0, f_rdstall_start}) &&
(s_cycle_q < {2'd0, f_rdstall_stop}));
assign write_req_valid = rst_n_i && !s_write_done_q;
assign write_addr = f_write_addr;
assign write_bytes = f_write_bytes;
assign write_data_valid = rst_n_i;
assign write_data = f_write_data;
assign write_keep = f_write_keep;
assign write_last = f_write_last;
assign block_new = rst_n_i && (s_cycle_q >= {2'd0, f_block_start}) &&
(s_cycle_q < {2'd0, f_block_stop});
assign clear = rst_n_i && f_clear_en && (s_cycle_q == {2'd0, f_clear_at});
assign read_fault = f_read_fault;
assign read_data_const = f_read_data;
assign write_data_const = f_write_data;
assign write_keep_const = f_write_keep;
npu_dma u_dut (
.clk_hp_i (clk_i),
.rst_hp_n_i (rst_n_i),
.clear_i (clear),
.block_new_i (block_new),
.pause_ack_o (pause_ack),
.read_req_valid_i (read_req_valid),
.read_req_ready_o (read_req_ready),
.read_addr_i (read_addr),
.read_bytes_i (read_bytes),
.read_data_valid_o (read_data_valid),
.read_data_ready_i (read_data_ready),
.read_data_o (read_data),
.read_keep_o (read_keep),
.read_last_o (read_last),
.write_req_valid_i (write_req_valid),
.write_req_ready_o (write_req_ready),
.write_addr_i (write_addr),
.write_bytes_i (write_bytes),
.write_data_valid_i(write_data_valid),
.write_data_ready_o(write_data_ready),
.write_data_i (write_data),
.write_keep_i (write_keep),
.write_last_i (write_last),
.write_done_o (write_done),
.busy_o (busy),
.read_busy_o (read_busy),
.write_busy_o (write_busy),
.read_bytes_o (read_bytes_done),
.write_bytes_o (write_bytes_done),
.stall_cycles_o (stall_cycles),
.fault_o (fault),
.fault_code_o (fault_code),
.fault_addr_o (fault_addr),
.fault_resp_o (fault_resp),
.read_cmd_err_o (read_cmd_err),
.write_cmd_err_o (write_cmd_err),
.axi4 (axi4)
);
assign awvalid = axi4.awvalid;
assign awready = axi4.awready;
assign awid = axi4.awid;
assign awaddr = axi4.awaddr;
assign awlen = axi4.awlen;
assign awsize = axi4.awsize;
assign awburst = axi4.awburst;
assign awlock = axi4.awlock;
assign awcache = axi4.awcache;
assign awprot = axi4.awprot;
assign awqos = axi4.awqos;
assign awregion = axi4.awregion;
assign awuser = axi4.awuser;
assign wvalid = axi4.wvalid;
assign wready = axi4.wready;
assign wdata = axi4.wdata;
assign wstrb = axi4.wstrb;
assign wlast = axi4.wlast;
assign wuser = axi4.wuser;
assign bvalid = axi4.bvalid;
assign bready = axi4.bready;
assign bid = axi4.bid;
assign bresp = axi4.bresp;
assign buser = axi4.buser;
assign arvalid = axi4.arvalid;
assign arready = axi4.arready;
assign arid = axi4.arid;
assign araddr = axi4.araddr;
assign arlen = axi4.arlen;
assign arsize = axi4.arsize;
assign arburst = axi4.arburst;
assign arlock = axi4.arlock;
assign arcache = axi4.arcache;
assign arprot = axi4.arprot;
assign arqos = axi4.arqos;
assign arregion = axi4.arregion;
assign aruser = axi4.aruser;
assign rvalid = axi4.rvalid;
assign rready = axi4.rready;
assign rid = axi4.rid;
assign rdata = axi4.rdata;
assign rresp = axi4.rresp;
assign rlast = axi4.rlast;
assign ruser = axi4.ruser;
assign rfifo_count = u_dut.s_rfifo_count_q;
assign wfifo_count = u_dut.s_wfifo_count_q;
initial begin
rst_n_i = 1'b0;
f_past_valid = 1'b0;
end
always_ff @(posedge clk_i) begin
rst_n_i <= 1'b1;
f_past_valid <= 1'b1;
if (!rst_n_i) begin
s_cycle_q <= 6'd0;
end else if (s_cycle_q != 6'h3f) begin
s_cycle_q <= s_cycle_q + 6'd1;
end
end
// Responder state. The clear_i branch is the coordinated fabric flush:
// in-flight bursts cancel with the engine, so no residual R/B beat is ever
// presented to a flushed receiver. A burst whose malformed beat has been
// accepted (bad RID or early RLAST) is retired silently because the engine
// quarantines its receiver until the flush.
always_ff @(posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
s_r_active_q <= 1'b0;
s_r_beats_q <= 9'd0;
s_r_beat_valid_q <= 1'b0;
s_r_first_q <= 1'b0;
s_r_resp_q <= AxiRespOkay;
s_r_proto_rid_q <= 1'b0;
s_r_proto_rlast_q <= 1'b0;
s_w_active_q <= 1'b0;
s_w_beats_q <= 9'd0;
s_w_bvalid_q <= 1'b0;
s_w_bpending_q <= 1'b0;
s_w_bresp_q <= AxiRespOkay;
s_w_bid_bad_q <= 1'b0;
s_ar_stall_q <= 2'd0;
s_r_gap_q <= 2'd0;
s_aw_stall_q <= 2'd0;
s_w_stall_q <= 2'd0;
s_b_gap_q <= 2'd0;
s_read_done_q <= 1'b0;
s_write_done_q <= 1'b0;
end else if (clear) begin
s_r_active_q <= 1'b0;
s_r_beats_q <= 9'd0;
s_r_beat_valid_q <= 1'b0;
s_r_first_q <= 1'b0;
s_r_resp_q <= AxiRespOkay;
s_r_proto_rid_q <= 1'b0;
s_r_proto_rlast_q <= 1'b0;
s_w_active_q <= 1'b0;
s_w_beats_q <= 9'd0;
s_w_bvalid_q <= 1'b0;
s_w_bpending_q <= 1'b0;
s_w_bresp_q <= AxiRespOkay;
s_w_bid_bad_q <= 1'b0;
s_ar_stall_q <= 2'd0;
s_r_gap_q <= 2'd0;
s_aw_stall_q <= 2'd0;
s_w_stall_q <= 2'd0;
s_b_gap_q <= 2'd0;
// A flushed command is offered again, so post-flush retry is covered.
s_read_done_q <= 1'b0;
s_write_done_q <= 1'b0;
end else begin
if (read_req_valid && read_req_ready) begin
s_read_done_q <= 1'b1;
end
if (write_req_valid && write_req_ready) begin
s_write_done_q <= 1'b1;
end
// Per-channel stall counters saturate above the anyconst personality.
if (axi4.arvalid && axi4.arready) begin
s_ar_stall_q <= 2'd0;
end else if (axi4.arvalid && !axi4.arready && (s_ar_stall_q != 2'd3)) begin
s_ar_stall_q <= s_ar_stall_q + 2'd1;
end
if (axi4.awvalid && axi4.awready) begin
s_aw_stall_q <= 2'd0;
end else if (axi4.awvalid && !axi4.awready && (s_aw_stall_q != 2'd3)) begin
s_aw_stall_q <= s_aw_stall_q + 2'd1;
end
if (axi4.arvalid && axi4.arready) begin
s_r_active_q <= 1'b1;
s_r_beats_q <= {1'b0, axi4.arlen} + 9'd1;
s_r_first_q <= 1'b1;
s_r_gap_q <= 2'd0;
if (f_read_fault == 2'd1) begin
s_r_resp_q <= AxiRespSlverr;
end else if (f_read_fault == 2'd2) begin
s_r_resp_q <= AxiRespDecerr;
end else begin
s_r_resp_q <= AxiRespOkay;
end
s_r_proto_rid_q <= (f_read_fault == 2'd3) && !f_read_proto_rlast;
s_r_proto_rlast_q <= (f_read_fault == 2'd3) && f_read_proto_rlast;
end
if (s_r_active_q && !s_r_beat_valid_q) begin
if (s_r_gap_q >= f_r_gap) begin
s_r_beat_valid_q <= 1'b1;
end else begin
s_r_gap_q <= s_r_gap_q + 2'd1;
end
end
if (axi4.rvalid && axi4.rready) begin
s_r_beat_valid_q <= 1'b0;
s_r_first_q <= 1'b0;
s_r_gap_q <= 2'd0;
if ((s_r_beats_q == 9'd1) || axi4.rlast || s_r_proto_rid_q) begin
s_r_active_q <= 1'b0;
s_r_beats_q <= 9'd0;
end else begin
s_r_beats_q <= s_r_beats_q - 9'd1;
end
end
if (axi4.awvalid && axi4.awready) begin
s_w_active_q <= 1'b1;
s_w_beats_q <= {1'b0, axi4.awlen} + 9'd1;
s_w_stall_q <= 2'd0;
if (f_write_fault == 2'd1) begin
s_w_bresp_q <= AxiRespSlverr;
end else if (f_write_fault == 2'd2) begin
s_w_bresp_q <= AxiRespDecerr;
end else begin
s_w_bresp_q <= AxiRespOkay;
end
s_w_bid_bad_q <= (f_write_fault == 2'd3);
end
if (axi4.wvalid && !axi4.wready && (s_w_stall_q != 2'd3)) begin
s_w_stall_q <= s_w_stall_q + 2'd1;
end
if (axi4.wvalid && axi4.wready) begin
s_w_stall_q <= 2'd0;
if (s_w_beats_q == 9'd1) begin
s_w_active_q <= 1'b0;
s_w_beats_q <= 9'd0;
s_w_bpending_q <= 1'b1;
s_b_gap_q <= 2'd0;
end else begin
s_w_beats_q <= s_w_beats_q - 9'd1;
end
end
if (s_w_bpending_q) begin
if (s_b_gap_q >= f_b_gap) begin
s_w_bpending_q <= 1'b0;
s_w_bvalid_q <= 1'b1;
end else begin
s_b_gap_q <= s_b_gap_q + 2'd1;
end
end
if (axi4.bvalid && axi4.bready) begin
s_w_bvalid_q <= 1'b0;
end
end
end
endmodule
+640
View File
@@ -0,0 +1,640 @@
// Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
// retroSoC is licensed under Mulan PSL v2.
// You can use this software according to the terms and conditions of the Mulan PSL v2.
// You may obtain a copy of Mulan PSL v2 at:
// http://license.coscl.org.cn/MulanPSL2
// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
// See the Mulan PSL v2 for more details.
// Property set for the NPU-P3 AXI4 data-plane engine (npu_dma). A shadow
// scoreboard tracks every accepted command, burst and beat from the bus so
// the contract checks below are purely bus-derived.
//
// Assumption list (all explicit; nothing else is assumed):
// (A1) command legality: non-zero byte counts bounded to 128 bytes and no
// 32-bit address wrap, so bounded runs reach the deepest burst plans
// and the combinational command rejection never fires;
// (A2) the write payload stream obeys valid/ready: the last flag is stable
// while a beat is stalled (valid itself and data/keep are constant).
// Commands are held by the harness until acceptance, so no command-channel
// assumption is needed.
// The AXI4 responder is RTL inside npu_dma_formal.sv (not assumed), so R/B
// stability and response placement are checked there as model self-checks.
// No fairness assumptions are needed: every assertion is a safety property.
// Responder stalls are bounded (0..3 cycles per handshake beat, anyconst per
// run — the engine holds its registers during a stall, so longer stalls add
// no new engine states), and the covers below stand in as bounded-liveness
// spot checks.
module npu_dma_formal;
// verilog_format: off -- formal observations mirror npu_dma_formal_design ports.
(* anyseq *) (* gclk *) reg clk_i;
wire rst_n_i;
wire f_past_valid;
wire clear;
wire block_new;
wire pause_ack;
wire read_req_valid;
wire read_req_ready;
wire [31:0] read_addr;
wire [31:0] read_bytes;
wire read_data_valid;
wire read_data_ready;
wire [63:0] read_data;
wire [ 7:0] read_keep;
wire read_last;
wire write_req_valid;
wire write_req_ready;
wire [31:0] write_addr;
wire [31:0] write_bytes;
wire write_data_valid;
wire write_data_ready;
wire [63:0] write_data;
wire [ 7:0] write_keep;
wire write_last;
wire write_done;
wire busy;
wire read_busy;
wire write_busy;
wire [63:0] read_bytes_done;
wire [63:0] write_bytes_done;
wire [63:0] stall_cycles;
wire fault;
wire [ 3:0] fault_code;
wire [31:0] fault_addr;
wire [ 1:0] fault_resp;
wire read_cmd_err;
wire write_cmd_err;
wire [ 4:0] rfifo_count;
wire [ 4:0] wfifo_count;
wire [ 1:0] read_fault;
wire [63:0] read_data_const;
wire [63:0] write_data_const;
wire [ 7:0] write_keep_const;
wire awvalid;
wire awready;
wire [ 2:0] awid;
wire [31:0] awaddr;
wire [ 7:0] awlen;
wire [ 2:0] awsize;
wire [ 1:0] awburst;
wire awlock;
wire [ 3:0] awcache;
wire [ 2:0] awprot;
wire [ 3:0] awqos;
wire [ 3:0] awregion;
wire awuser;
wire wvalid;
wire wready;
wire [63:0] wdata;
wire [ 7:0] wstrb;
wire wlast;
wire wuser;
wire bvalid;
wire bready;
wire [ 2:0] bid;
wire [ 1:0] bresp;
wire buser;
wire arvalid;
wire arready;
wire [ 2:0] arid;
wire [31:0] araddr;
wire [ 7:0] arlen;
wire [ 2:0] arsize;
wire [ 1:0] arburst;
wire arlock;
wire [ 3:0] arcache;
wire [ 2:0] arprot;
wire [ 3:0] arqos;
wire [ 3:0] arregion;
wire aruser;
wire rvalid;
wire rready;
wire [ 2:0] rid;
wire [63:0] rdata;
wire [ 1:0] rresp;
wire rlast;
wire ruser;
// verilog_format: on
logic f_rd_outstanding_q;
logic [ 8:0] f_rd_beats_left_q;
logic [ 2:0] f_rd_size_q;
logic [31:0] f_rd_beat_addr_q;
logic [31:0] f_rd_seg_addr_q;
logic [32:0] f_rd_seg_end_q;
logic [31:0] f_rd_next_burst_q;
logic f_rd_done_q;
logic f_wr_aw_q;
logic [ 8:0] f_wr_beats_left_q;
logic f_wr_b_q;
logic [31:0] f_wr_beat_addr_q;
logic [31:0] f_wr_next_burst_q;
wire f_rd_cmd_accept;
wire f_wr_cmd_accept;
wire f_ar_accept;
wire f_r_accept;
wire f_r_last_due;
wire f_r_proto_now;
wire f_aw_accept;
wire f_w_accept;
wire f_b_accept;
wire [ 3:0] f_wstrb_ones;
wire f_evt_proto_rd;
wire f_evt_axi_rd;
wire f_evt_proto_wr;
wire f_evt_axi_wr;
assign f_rd_cmd_accept = read_req_valid && read_req_ready;
assign f_wr_cmd_accept = write_req_valid && write_req_ready;
assign f_ar_accept = arvalid && arready;
assign f_r_accept = rvalid && rready;
assign f_r_last_due = f_rd_beats_left_q == 9'd1;
assign f_r_proto_now = rvalid && f_rd_outstanding_q && ((rid != 3'd0) || (rlast != f_r_last_due));
assign f_aw_accept = awvalid && awready;
assign f_w_accept = wvalid && wready;
assign f_b_accept = bvalid && bready;
assign f_wstrb_ones = {3'd0, wstrb[0]} + {3'd0, wstrb[1]} + {3'd0, wstrb[2]} +
{3'd0, wstrb[3]} + {3'd0, wstrb[4]} + {3'd0, wstrb[5]} +
{3'd0, wstrb[6]} + {3'd0, wstrb[7]};
// Bus-visible fault events, mirroring the engine's fault sources: a
// malformed R beat faults on presentation, a malformed BID faults on
// presentation, and error responses fault on acceptance.
assign f_evt_proto_rd = f_r_proto_now && !clear;
assign f_evt_axi_rd = f_r_accept && (rresp != 2'b00);
assign f_evt_proto_wr = bvalid && f_wr_b_q && (bid != 3'd0) && !clear;
assign f_evt_axi_wr = f_b_accept && (bresp != 2'b00);
npu_dma_formal_design u_design (
.clk_i (clk_i),
.rst_n_i (rst_n_i),
.f_past_valid (f_past_valid),
.clear (clear),
.block_new (block_new),
.pause_ack (pause_ack),
.read_req_valid (read_req_valid),
.read_req_ready (read_req_ready),
.read_addr (read_addr),
.read_bytes (read_bytes),
.read_data_valid (read_data_valid),
.read_data_ready (read_data_ready),
.read_data (read_data),
.read_keep (read_keep),
.read_last (read_last),
.write_req_valid (write_req_valid),
.write_req_ready (write_req_ready),
.write_addr (write_addr),
.write_bytes (write_bytes),
.write_data_valid(write_data_valid),
.write_data_ready(write_data_ready),
.write_data (write_data),
.write_keep (write_keep),
.write_last (write_last),
.write_done (write_done),
.busy (busy),
.read_busy (read_busy),
.write_busy (write_busy),
.read_bytes_done (read_bytes_done),
.write_bytes_done(write_bytes_done),
.stall_cycles (stall_cycles),
.fault (fault),
.fault_code (fault_code),
.fault_addr (fault_addr),
.fault_resp (fault_resp),
.read_cmd_err (read_cmd_err),
.write_cmd_err (write_cmd_err),
.rfifo_count (rfifo_count),
.wfifo_count (wfifo_count),
.read_fault (read_fault),
.read_data_const (read_data_const),
.write_data_const(write_data_const),
.write_keep_const(write_keep_const),
.awvalid (awvalid),
.awready (awready),
.awid (awid),
.awaddr (awaddr),
.awlen (awlen),
.awsize (awsize),
.awburst (awburst),
.awlock (awlock),
.awcache (awcache),
.awprot (awprot),
.awqos (awqos),
.awregion (awregion),
.awuser (awuser),
.wvalid (wvalid),
.wready (wready),
.wdata (wdata),
.wstrb (wstrb),
.wlast (wlast),
.wuser (wuser),
.bvalid (bvalid),
.bready (bready),
.bid (bid),
.bresp (bresp),
.buser (buser),
.arvalid (arvalid),
.arready (arready),
.arid (arid),
.araddr (araddr),
.arlen (arlen),
.arsize (arsize),
.arburst (arburst),
.arlock (arlock),
.arcache (arcache),
.arprot (arprot),
.arqos (arqos),
.arregion (arregion),
.aruser (aruser),
.rvalid (rvalid),
.rready (rready),
.rid (rid),
.rdata (rdata),
.rresp (rresp),
.rlast (rlast),
.ruser (ruser)
);
// Shadow scoreboard. A malformed accepted R/B beat quarantines the
// receiver in the engine, so the shadow retains the burst obligation until
// the flush; address/beat tracking otherwise follows the accepted traffic.
always @(posedge clk_i) begin
if (!rst_n_i) begin
f_rd_outstanding_q <= 1'b0;
f_rd_beats_left_q <= 9'd0;
f_rd_size_q <= 3'd0;
f_rd_beat_addr_q <= 32'd0;
f_rd_seg_addr_q <= 32'd0;
f_rd_seg_end_q <= 33'd0;
f_rd_next_burst_q <= 32'd0;
f_rd_done_q <= 1'b0;
f_wr_aw_q <= 1'b0;
f_wr_beats_left_q <= 9'd0;
f_wr_b_q <= 1'b0;
f_wr_beat_addr_q <= 32'd0;
f_wr_next_burst_q <= 32'd0;
end else if (clear) begin
f_rd_outstanding_q <= 1'b0;
f_rd_beats_left_q <= 9'd0;
f_rd_size_q <= 3'd0;
f_rd_beat_addr_q <= 32'd0;
f_rd_seg_addr_q <= 32'd0;
f_rd_seg_end_q <= 33'd0;
f_rd_next_burst_q <= 32'd0;
f_rd_done_q <= 1'b0;
f_wr_aw_q <= 1'b0;
f_wr_beats_left_q <= 9'd0;
f_wr_b_q <= 1'b0;
f_wr_beat_addr_q <= 32'd0;
f_wr_next_burst_q <= 32'd0;
end else begin
if (f_rd_cmd_accept) begin
f_rd_seg_addr_q <= read_addr;
f_rd_seg_end_q <= {1'b0, read_addr} + {1'b0, read_bytes};
f_rd_next_burst_q <= read_addr;
f_rd_done_q <= 1'b0;
end
if (f_wr_cmd_accept) begin
f_wr_next_burst_q <= {write_addr[31:3], 3'b000};
end
if (f_r_accept) begin
if ((rid != 3'd0) || (rlast != f_r_last_due)) begin
f_rd_outstanding_q <= 1'b1;
end else if (f_r_last_due) begin
f_rd_outstanding_q <= 1'b0;
if ((f_rd_next_burst_q == f_rd_seg_end_q[31:0]) && (rresp == 2'b00)) begin
f_rd_done_q <= 1'b1;
end
end else begin
f_rd_beats_left_q <= f_rd_beats_left_q - 9'd1;
f_rd_beat_addr_q <= f_rd_beat_addr_q + (32'd1 << f_rd_size_q);
end
end
if (f_w_accept) begin
if (f_wr_beats_left_q == 9'd1) begin
f_wr_aw_q <= 1'b0;
f_wr_b_q <= 1'b1;
end else begin
f_wr_beats_left_q <= f_wr_beats_left_q - 9'd1;
f_wr_beat_addr_q <= f_wr_beat_addr_q + 32'd8;
end
end
if (f_b_accept && (bid == 3'd0)) begin
f_wr_b_q <= 1'b0;
end
if (f_ar_accept) begin
f_rd_outstanding_q <= 1'b1;
f_rd_beats_left_q <= {1'b0, arlen} + 9'd1;
f_rd_size_q <= arsize;
f_rd_beat_addr_q <= araddr;
f_rd_next_burst_q <= araddr + (({24'd0, arlen} + 32'd1) << arsize);
end
if (f_aw_accept) begin
f_wr_aw_q <= 1'b1;
f_wr_beats_left_q <= {1'b0, awlen} + 9'd1;
f_wr_beat_addr_q <= awaddr;
f_wr_next_burst_q <= awaddr + (({24'd0, awlen} + 32'd1) << 3);
end
end
end
// Assumptions A1 (command legality) and A2 (write stream last-flag
// stability). Commands are held by the harness until accepted, so no
// command-channel assumption is needed.
always @(posedge clk_i) begin
if (rst_n_i) begin
assume (read_bytes != 32'd0);
assume (read_bytes <= 32'd128);
assume ({1'b0, read_addr} + {1'b0, read_bytes} <= 33'h1_0000_0000);
assume (write_bytes != 32'd0);
assume (write_bytes <= 32'd128);
assume ({1'b0, write_addr} + {1'b0, write_bytes} <= 33'h1_0000_0000);
end
if (f_past_valid && $past(rst_n_i)) begin
if ($past(write_data_valid && !write_data_ready)) begin
assume (write_last == $past(write_last));
end
end
end
// Responder model self-checks: a failure here is a harness bug, not an
// engine bug. Responses arrive only inside an outstanding burst, payload
// is stable through any stall (the flush invalidates in-flight beats), and
// RLAST is well placed unless a protocol fault is being injected.
always @(posedge clk_i) begin
if (rst_n_i) begin
if (rvalid) begin
assert (f_rd_outstanding_q);
end
if (bvalid) begin
assert (f_wr_b_q);
end
if (rvalid && (read_fault != 2'd3)) begin
assert (rlast == f_r_last_due);
end
end
if (f_past_valid && $past(rst_n_i) && !$past(clear) && !clear) begin
if ($past(rvalid && !rready)) begin
assert (rvalid);
assert (rid == $past(rid));
assert (rdata == $past(rdata));
assert (rresp == $past(rresp));
assert (rlast == $past(rlast));
assert (ruser == $past(ruser));
end
if ($past(bvalid && !bready)) begin
assert (bvalid);
assert (bid == $past(bid));
assert (bresp == $past(bresp));
assert (buser == $past(buser));
end
end
end
// Engine contract, current-cycle checks.
always @(posedge clk_i) begin
if (rst_n_i) begin
// Emitted read-address attributes: ID zero, INCR, at most 16 beats,
// sizes 1/2/4/8 with narrow transfers only as single-beat edges,
// naturally aligned, never crossing a 4 KiB page, never fetching
// outside the accepted segment, and only with an empty receive FIFO.
if (arvalid) begin
assert (arid == 3'd0);
assert (arburst == 2'b01);
assert (arlen <= 8'd15);
assert (arsize <= 3'd3);
assert ((arsize == 3'd3) || (arlen == 8'd0));
assert ((araddr & ((32'd1 << arsize) - 32'd1)) == 32'd0);
assert ({1'b0, araddr[11:0]} + (({5'd0, arlen} + 13'd1) << arsize) <= 13'd4096);
assert (arlock == 1'b0);
assert (arcache == 4'd0);
assert (arprot == 3'd0);
assert (arqos == 4'd0);
assert (arregion == 4'd0);
assert (aruser == 1'b0);
assert (rfifo_count == 5'd0);
assert (araddr == f_rd_next_burst_q);
assert ({1'b0, araddr} >= {1'b0, f_rd_seg_addr_q});
assert ({1'b0, araddr} + (({25'd0, arlen} + 33'd1) << arsize) <= f_rd_seg_end_q);
end
// Emitted write-address attributes: ID zero, INCR, full-width aligned
// beats only, at most 16 beats, never crossing a 4 KiB page, with the
// complete payload already staged in the write FIFO.
if (awvalid) begin
assert (awid == 3'd0);
assert (awburst == 2'b01);
assert (awlen <= 8'd15);
assert (awsize == 3'd3);
assert (awaddr[2:0] == 3'd0);
assert ({1'b0, awaddr[11:0]} + (({5'd0, awlen} + 13'd1) << 3) <= 13'd4096);
assert (awlock == 1'b0);
assert (awcache == 4'd0);
assert (awprot == 3'd0);
assert (awqos == 4'd0);
assert (awregion == 4'd0);
assert (awuser == 1'b0);
assert ({4'd0, wfifo_count} == ({1'b0, awlen} + 9'd1));
assert (awaddr == f_wr_next_burst_q);
end
if (wvalid) begin
assert (wuser == 1'b0);
// W is only ever driven after its AW handshake.
assert (f_wr_aw_q);
// The write FIFO passes the staged payload through unmodified.
assert (wdata == write_data_const);
assert (wstrb == write_keep_const);
end
if (read_data_valid) begin
// The receive FIFO passes the response data through unmodified.
assert (read_data == read_data_const);
end
// At most one outstanding transaction per direction.
if (f_ar_accept) begin
assert (!f_rd_outstanding_q);
end
if (f_aw_accept) begin
assert (!f_wr_aw_q);
assert (!f_wr_b_q);
end
// Accepted-transfer conservation: the engine never consumes more beats
// than the burst carries, WLAST marks exactly the final W beat, and the
// receivers close after the final beat of a burst.
if (f_r_accept) begin
assert (f_rd_beats_left_q != 9'd0);
end
if (f_w_accept) begin
assert (f_wr_beats_left_q != 9'd0);
assert (wlast == (f_wr_beats_left_q == 9'd1));
end
if (write_done) begin
assert (f_b_accept);
assert (bid == 3'd0);
assert (bresp == 2'b00);
end
// Legal commands never raise the combinational rejection.
assert (!read_cmd_err);
// Pause acknowledge is exactly "block_new held with no AR-accepted
// read and no AW-accepted write obligation".
if (block_new && !f_rd_outstanding_q && !f_wr_aw_q && !f_wr_b_q) begin
assert (pause_ack);
end
if (pause_ack) begin
assert (block_new);
assert (!f_rd_outstanding_q);
assert (!f_wr_aw_q);
assert (!f_wr_b_q);
end
end
end
// Engine contract, registered checks.
always @(posedge clk_i) begin
if (f_past_valid && $past(rst_n_i)) begin
// VALID payloads stay stable through any stall on the engine-driven
// channels and the read-data stream; only the coordinated fabric flush
// may retract a presented transfer.
if ($past(arvalid && !arready) && !clear) begin
assert (arvalid);
assert (arid == $past(arid));
assert (araddr == $past(araddr));
assert (arlen == $past(arlen));
assert (arsize == $past(arsize));
assert (arburst == $past(arburst));
assert (arlock == $past(arlock));
assert (arcache == $past(arcache));
assert (arprot == $past(arprot));
assert (arqos == $past(arqos));
assert (arregion == $past(arregion));
assert (aruser == $past(aruser));
end
if ($past(awvalid && !awready) && !clear) begin
assert (awvalid);
assert (awid == $past(awid));
assert (awaddr == $past(awaddr));
assert (awlen == $past(awlen));
assert (awsize == $past(awsize));
assert (awburst == $past(awburst));
assert (awlock == $past(awlock));
assert (awcache == $past(awcache));
assert (awprot == $past(awprot));
assert (awqos == $past(awqos));
assert (awregion == $past(awregion));
assert (awuser == $past(awuser));
end
if ($past(wvalid && !wready) && !clear) begin
assert (wvalid);
assert (wdata == $past(wdata));
assert (wstrb == $past(wstrb));
assert (wlast == $past(wlast));
assert (wuser == $past(wuser));
end
if ($past(read_data_valid && !read_data_ready) && !clear) begin
assert (read_data_valid);
assert (read_data == $past(read_data));
assert (read_keep == $past(read_keep));
assert (read_last == $past(read_last));
end
// No new AR/AW presentation begins while the pause is acknowledged.
if ($past(pause_ack && !arvalid) && !$past(clear)) begin
assert (!arvalid);
end
if ($past(pause_ack && !awvalid) && !$past(clear)) begin
assert (!awvalid);
end
// Receivers close after the final accepted beat of a burst.
if ($past(f_r_accept && f_r_last_due)) begin
assert (!rready);
end
if ($past(f_w_accept && (f_wr_beats_left_q == 9'd1))) begin
assert (!wvalid);
end
// Sticky first fault with deterministic priority
// AXI_PROTOCOL (6) > AXI_READ (4) > AXI_WRITE (5).
if (!$past(fault)) begin
if ($past(f_evt_proto_rd)) begin
assert (fault);
assert (fault_code == 4'd6);
assert (fault_resp == $past(rresp));
assert (fault_addr == $past(f_rd_beat_addr_q));
end else if ($past(f_evt_proto_wr)) begin
assert (fault);
assert (fault_code == 4'd6);
assert (fault_resp == $past(bresp));
assert (fault_addr == $past(f_wr_beat_addr_q));
end else if ($past(f_evt_axi_rd)) begin
assert (fault);
assert (fault_code == 4'd4);
assert (fault_resp == $past(rresp));
assert (fault_addr == $past(f_rd_beat_addr_q));
end else if ($past(f_evt_axi_wr)) begin
assert (fault);
assert (fault_code == 4'd5);
assert (fault_resp == $past(bresp));
assert (fault_addr == $past(f_wr_beat_addr_q));
end
end
// The fault is sticky with stable fields until clear_i.
if ($past(fault) && !$past(clear) && !clear) begin
assert (fault);
assert (fault_code == $past(fault_code));
assert (fault_addr == $past(fault_addr));
assert (fault_resp == $past(fault_resp));
end
if (fault) begin
assert ((fault_code == 4'd4) || (fault_code == 4'd5) || (fault_code == 4'd6));
end
// Counters are monotone without clear_i and count exactly the accepted
// traffic (addressed read lanes, accepted WSTRB bytes).
if (!$past(clear) && !clear) begin
assert (read_bytes_done >= $past(read_bytes_done));
assert (write_bytes_done >= $past(write_bytes_done));
assert (stall_cycles >= $past(stall_cycles));
end
if ($past(f_r_accept)) begin
assert (read_bytes_done == ($past(read_bytes_done) + (64'd1 << $past(f_rd_size_q))));
end
if ($past(f_w_accept)) begin
assert (write_bytes_done == ($past(write_bytes_done) + {60'd0, $past(f_wstrb_ones)}));
end
end
// Valids stay low out of reset. pause_ack is exempt: it combinationally
// follows block_new on an idle engine, which is legal at any time.
if (f_past_valid && rst_n_i && !$past(rst_n_i)) begin
assert (!arvalid && !awvalid && !wvalid && !rready && !bready);
assert (!read_data_valid && !write_data_ready && !write_done);
assert (!busy && !fault);
end
if (!rst_n_i) begin
assert (!arvalid && !awvalid && !wvalid && !rready && !bready);
assert (!read_data_valid && !write_data_ready && !write_done);
assert (!busy && !fault && !pause_ack);
end
end
// Reachability covers: successful read and write bursts, the fault cases,
// pause with a retained unaccepted VALID, and a mid-flight flush.
always @(posedge clk_i) begin
if (rst_n_i) begin
cover (f_ar_accept && (arlen == 8'd15));
cover (f_ar_accept && (arsize != 3'd3));
cover (f_ar_accept && (arlen >= 8'd3));
cover (f_rd_done_q && !fault);
cover (f_aw_accept && (awlen >= 8'd1));
cover (write_done);
cover (fault && (fault_code == 4'd4));
cover (fault && (fault_code == 4'd5));
cover (fault && (fault_code == 4'd6));
cover (pause_ack && arvalid && !arready);
cover (pause_ack && awvalid && !awready);
cover (clear && f_rd_outstanding_q);
cover (clear && (f_wr_aw_q || f_wr_b_q));
cover (f_rd_outstanding_q && (f_wr_aw_q || f_wr_b_q));
cover (write_cmd_err);
cover (read_data_valid && read_data_ready && read_last);
end
end
endmodule
+12 -5
View File
@@ -16,6 +16,8 @@ FORMAL_DMA_DEPTH ?= 24
FORMAL_DMA_COVER_DEPTH ?= 32
FORMAL_GA2D_DEPTH ?= 36
FORMAL_GA2D_COVER_DEPTH ?= 199
FORMAL_NPU_DMA_DEPTH ?= 16
FORMAL_NPU_DMA_COVER_DEPTH ?= 32
FORMAL_APU_DEPTH ?= 24
FORMAL_APU_CODEC_DEPTH ?= 32
FORMAL_APU_LOADER_DEPTH ?= 128
@@ -35,20 +37,21 @@ FORMAL_OPIPSRAM_TIMEOUT ?= 300
FORMAL_OPIPSRAM_BMC_TIMEOUT ?= 600
FORMAL_DMA_TIMEOUT ?= 120
FORMAL_GA2D_TIMEOUT ?= 7200
FORMAL_NPU_DMA_TIMEOUT ?= 7200
FORMAL_APU_TIMEOUT ?= 300
FORMAL_GATEWAY_A_TIMEOUT ?= 120
FORMAL_SDIO_TIMEOUT ?= 120
FORMAL_APU_P5_LOADER_TARGETS := apu_loader_p5_success apu_loader_p5_bound apu_loader_p5_abort apu_loader_p5_resource_reset apu_loader_p5_memo_fallback
FORMAL_APU_LOADER_TARGETS := apu_loader_success apu_loader_header_range apu_loader_descriptor_range apu_loader_crc apu_loader_control_flow apu_loader_abort apu_loader_resource_reset $(FORMAL_APU_P5_LOADER_TARGETS)
FORMAL_APU_PRIMITIVE_TARGETS := apu_primitives_invalid_read apu_primitives_local apu_primitives_local_overflow apu_primitives_kernel apu_primitives_input_fifo apu_primitives_alignment apu_primitives_output_fifo
FORMAL_TARGET_TIMEOUT = $(if $(filter ga2d,$*),$(FORMAL_GA2D_TIMEOUT),$(if $(filter dma,$*),$(FORMAL_DMA_TIMEOUT),$(if $(filter apu apu_codec apu_kws apu_primitives apu_sequencer $(FORMAL_APU_LOADER_TARGETS) $(FORMAL_APU_PRIMITIVE_TARGETS),$*),$(FORMAL_APU_TIMEOUT),$(if $(filter gateway_a,$*),$(FORMAL_GATEWAY_A_TIMEOUT),$(if $(filter sdio,$*),$(FORMAL_SDIO_TIMEOUT),$(if $(filter opipsram,$*),$(FORMAL_OPIPSRAM_TIMEOUT),$(if $(filter ws2812,$*),$(FORMAL_WS2812_TIMEOUT),$(if $(filter i2c,$*),$(FORMAL_I2C_TIMEOUT),$(FORMAL_TIMEOUT)))))))))
FORMAL_TARGET_TIMEOUT = $(if $(filter ga2d,$*),$(FORMAL_GA2D_TIMEOUT),$(if $(filter npu_dma,$*),$(FORMAL_NPU_DMA_TIMEOUT),$(if $(filter dma,$*),$(FORMAL_DMA_TIMEOUT),$(if $(filter apu apu_codec apu_kws apu_primitives apu_sequencer $(FORMAL_APU_LOADER_TARGETS) $(FORMAL_APU_PRIMITIVE_TARGETS),$*),$(FORMAL_APU_TIMEOUT),$(if $(filter gateway_a,$*),$(FORMAL_GATEWAY_A_TIMEOUT),$(if $(filter sdio,$*),$(FORMAL_SDIO_TIMEOUT),$(if $(filter opipsram,$*),$(FORMAL_OPIPSRAM_TIMEOUT),$(if $(filter ws2812,$*),$(FORMAL_WS2812_TIMEOUT),$(if $(filter i2c,$*),$(FORMAL_I2C_TIMEOUT),$(FORMAL_TIMEOUT))))))))))
FORMAL_APU_TARGET_DEPTH = $(if $(filter $(FORMAL_APU_P5_LOADER_TARGETS),$*),$(FORMAL_APU_P5_LOADER_DEPTH),$(if $(filter $(FORMAL_APU_LOADER_TARGETS),$*),$(FORMAL_APU_LOADER_DEPTH),$(if $(filter apu_kws,$*),$(FORMAL_APU_KWS_DEPTH),$(if $(filter apu_sequencer,$*),$(FORMAL_APU_SEQUENCER_DEPTH),$(if $(filter apu_codec,$*),$(FORMAL_APU_CODEC_DEPTH),$(FORMAL_APU_DEPTH))))))
FORMAL_APU_TARGET_COVER_DEPTH = $(if $(filter $(FORMAL_APU_P5_LOADER_TARGETS),$*),$(FORMAL_APU_P5_LOADER_COVER_DEPTH),$(if $(filter $(FORMAL_APU_LOADER_TARGETS),$*),$(FORMAL_APU_LOADER_COVER_DEPTH),$(FORMAL_APU_TARGET_DEPTH)))
FORMAL_TARGET_TOP = $(if $(filter $(FORMAL_APU_LOADER_TARGETS),$*),apu_loader,$(if $(filter $(FORMAL_APU_PRIMITIVE_TARGETS),$*),apu_primitives,$*))
FORMAL_PROPERTIES_SOURCE = $(if $(filter ga2d,$*),$(RTL_PATH)/formal/ga2d_formal.sv,$(RTL_PATH)/formal/$(FORMAL_TARGET_TOP)_formal_props.sv)
FORMAL_PROPERTIES_DEFINE = $(if $(filter ga2d,$*),--properties-define GA2D_FORMAL_PROPERTIES,)
FORMAL_COMPACT_COVER_TARGETS := i2c opipsram dma ga2d $(FORMAL_APU_LOADER_TARGETS) $(FORMAL_APU_PRIMITIVE_TARGETS)
FORMAL_TARGETS := bus rib_adapter rib2apb sysctrl pll_rcu gpio ws2812 uart i2c timer clint dvp i2s psram onchip_ram opipsram dma ga2d apu apu_codec apu_kws $(FORMAL_APU_PRIMITIVE_TARGETS) $(FORMAL_APU_LOADER_TARGETS) apu_sequencer gateway_a sdio
FORMAL_TARGETS := bus rib_adapter rib2apb sysctrl pll_rcu gpio ws2812 uart i2c timer clint dvp i2s psram onchip_ram opipsram dma ga2d apu apu_codec apu_kws $(FORMAL_APU_PRIMITIVE_TARGETS) $(FORMAL_APU_LOADER_TARGETS) apu_sequencer gateway_a sdio npu_dma
FORMAL_FILELIST_GENERATOR := $(RTL_PATH)/formal/generate_formal_filelist.py
FORMAL_SBY_GENERATOR := $(RTL_PATH)/formal/generate_sby_config.py
FORMAL_RESULT_GENERATOR := $(RTL_PATH)/formal/formal_results.py
@@ -104,6 +107,8 @@ FORMAL_SOURCE_FILES := $(RTL_PATH)/formal/bus_formal.sv \
$(RTL_PATH)/formal/gateway_a_formal_props.sv \
$(RTL_PATH)/formal/sdio_formal.sv \
$(RTL_PATH)/formal/sdio_formal_props.sv \
$(RTL_PATH)/formal/npu_dma_formal.sv \
$(RTL_PATH)/formal/npu_dma_formal_props.sv \
$(RTL_PATH)/top/rib_bus.sv \
$(RTL_PATH)/top/rib_error_slave.sv \
$(RTL_PATH)/top/rib_if.sv \
@@ -234,14 +239,14 @@ $(FORMAL_DIR)/%/prove.sby: $(FORMAL_DIR)/%/design.v \
$(FORMAL_SBY_GENERATOR)
python3 $(FORMAL_SBY_GENERATOR) --top $(FORMAL_TARGET_TOP)_formal --input $< \
--properties $(FORMAL_PROPERTIES_SOURCE) $(FORMAL_PROPERTIES_DEFINE) --solver $(FORMAL_SOLVER) \
--mode $(if $(filter ga2d,$*),bmc,$(if $(filter dma apu apu_codec apu_kws apu_primitives apu_sequencer $(FORMAL_APU_LOADER_TARGETS) $(FORMAL_APU_PRIMITIVE_TARGETS) gateway_a sdio onchip_ram,$*),bmc,prove)) --depth $(if $(filter ga2d,$*),$(FORMAL_GA2D_DEPTH),$(if $(filter pll_rcu,$*),$(FORMAL_PLL_RCU_DEPTH),$(if $(filter dma,$*),$(FORMAL_DMA_DEPTH),$(if $(filter apu apu_codec apu_kws apu_primitives apu_sequencer $(FORMAL_APU_LOADER_TARGETS) $(FORMAL_APU_PRIMITIVE_TARGETS),$*),$(FORMAL_APU_TARGET_DEPTH),$(if $(filter gateway_a,$*),$(FORMAL_GATEWAY_A_DEPTH),$(if $(filter sdio,$*),$(FORMAL_SDIO_DEPTH),$(if $(filter opipsram,$*),$(FORMAL_OPIPSRAM_DEPTH),$(if $(filter ws2812,$*),$(FORMAL_WS2812_DEPTH),$(if $(filter i2c,$*),$(FORMAL_I2C_DEPTH),$(if $(filter clint,$*),$(FORMAL_CLINT_DEPTH),$(if $(filter psram,$*),$(FORMAL_PSRAM_DEPTH),$(FORMAL_DEPTH)))))))))))) --output $@
--mode $(if $(filter ga2d,$*),bmc,$(if $(filter dma npu_dma apu apu_codec apu_kws apu_primitives apu_sequencer $(FORMAL_APU_LOADER_TARGETS) $(FORMAL_APU_PRIMITIVE_TARGETS) gateway_a sdio onchip_ram,$*),bmc,prove)) --depth $(if $(filter ga2d,$*),$(FORMAL_GA2D_DEPTH),$(if $(filter pll_rcu,$*),$(FORMAL_PLL_RCU_DEPTH),$(if $(filter npu_dma,$*),$(FORMAL_NPU_DMA_DEPTH),$(if $(filter dma,$*),$(FORMAL_DMA_DEPTH),$(if $(filter apu apu_codec apu_kws apu_primitives apu_sequencer $(FORMAL_APU_LOADER_TARGETS) $(FORMAL_APU_PRIMITIVE_TARGETS),$*),$(FORMAL_APU_TARGET_DEPTH),$(if $(filter gateway_a,$*),$(FORMAL_GATEWAY_A_DEPTH),$(if $(filter sdio,$*),$(FORMAL_SDIO_DEPTH),$(if $(filter opipsram,$*),$(FORMAL_OPIPSRAM_DEPTH),$(if $(filter ws2812,$*),$(FORMAL_WS2812_DEPTH),$(if $(filter i2c,$*),$(FORMAL_I2C_DEPTH),$(if $(filter clint,$*),$(FORMAL_CLINT_DEPTH),$(if $(filter psram,$*),$(FORMAL_PSRAM_DEPTH),$(FORMAL_DEPTH))))))))))))) --output $@
@if [ "$*" = opipsram ]; then sed -i '/^async2sync/i clk2fflogic' $@; fi
$(FORMAL_DIR)/%/cover.sby: $(FORMAL_DIR)/%/design.v \
$(FORMAL_SBY_GENERATOR)
python3 $(FORMAL_SBY_GENERATOR) --top $(FORMAL_TARGET_TOP)_formal --input $< \
--properties $(FORMAL_PROPERTIES_SOURCE) $(FORMAL_PROPERTIES_DEFINE) --solver $(FORMAL_SOLVER) \
--mode cover --depth $(if $(filter ga2d,$*),$(FORMAL_GA2D_COVER_DEPTH),$(if $(filter pll_rcu,$*),$(FORMAL_PLL_RCU_DEPTH),$(if $(filter dma,$*),$(FORMAL_DMA_COVER_DEPTH),$(if $(filter apu apu_codec apu_kws apu_primitives apu_sequencer $(FORMAL_APU_LOADER_TARGETS) $(FORMAL_APU_PRIMITIVE_TARGETS),$*),$(FORMAL_APU_TARGET_COVER_DEPTH),$(if $(filter gateway_a,$*),$(FORMAL_GATEWAY_A_DEPTH),$(if $(filter sdio,$*),$(FORMAL_SDIO_COVER_DEPTH),$(if $(filter opipsram,$*),$(FORMAL_OPIPSRAM_DEPTH),$(if $(filter ws2812,$*),$(FORMAL_WS2812_DEPTH),$(if $(filter i2c,$*),$(FORMAL_I2C_DEPTH),$(if $(filter clint,$*),$(FORMAL_CLINT_DEPTH),$(if $(filter psram,$*),$(FORMAL_PSRAM_DEPTH),$(FORMAL_DEPTH))))))))))) \
--mode cover --depth $(if $(filter ga2d,$*),$(FORMAL_GA2D_COVER_DEPTH),$(if $(filter pll_rcu,$*),$(FORMAL_PLL_RCU_DEPTH),$(if $(filter npu_dma,$*),$(FORMAL_NPU_DMA_COVER_DEPTH),$(if $(filter dma,$*),$(FORMAL_DMA_COVER_DEPTH),$(if $(filter apu apu_codec apu_kws apu_primitives apu_sequencer $(FORMAL_APU_LOADER_TARGETS) $(FORMAL_APU_PRIMITIVE_TARGETS),$*),$(FORMAL_APU_TARGET_COVER_DEPTH),$(if $(filter gateway_a,$*),$(FORMAL_GATEWAY_A_DEPTH),$(if $(filter sdio,$*),$(FORMAL_SDIO_COVER_DEPTH),$(if $(filter opipsram,$*),$(FORMAL_OPIPSRAM_DEPTH),$(if $(filter ws2812,$*),$(FORMAL_WS2812_DEPTH),$(if $(filter i2c,$*),$(FORMAL_I2C_DEPTH),$(if $(filter clint,$*),$(FORMAL_CLINT_DEPTH),$(if $(filter psram,$*),$(FORMAL_PSRAM_DEPTH),$(FORMAL_DEPTH))))))))))))) \
$(if $(filter $(FORMAL_APU_P5_LOADER_TARGETS),$*),--skip $(FORMAL_APU_P5_LOADER_COVER_SKIP)) \
$(if $(filter $(FORMAL_COMPACT_COVER_TARGETS),$*),--no-vcd) --output $@
@if [ "$*" = opipsram ]; then sed -i '/^async2sync/i clk2fflogic' $@; fi
@@ -355,10 +360,12 @@ formal-gateway-a: $(FORMAL_DIR)/gateway_a/.stamp | manifest
formal-sdio: $(FORMAL_DIR)/sdio/.stamp | manifest
formal-npu_dma: $(FORMAL_DIR)/npu_dma/.stamp | manifest
formal-doctor:
$(MAKE) FORMAL=YES SIMU=IVERILOG SYNTH=YOSYS STA=NONE doctor
formal-clean:
python3 $(ROOT_PATH)/scripts/clean.py --root $(ROOT_PATH) --path $(FORMAL_DIR)
.PHONY: formal formal-bus formal-rib-adapter formal-rib2apb formal-sysctrl formal-pll-rcu formal-gpio formal-ws2812 formal-uart formal-i2c formal-timer formal-clint formal-dvp formal-i2s formal-psram formal-onchip-ram formal-opipsram formal-dma formal-ga2d formal-apu formal-apu-kws formal-apu-loader formal-apu-loader-p5 formal-apu-sequencer formal-gateway-a formal-sdio formal-doctor formal-clean
.PHONY: formal formal-bus formal-rib-adapter formal-rib2apb formal-sysctrl formal-pll-rcu formal-gpio formal-ws2812 formal-uart formal-i2c formal-timer formal-clint formal-dvp formal-i2s formal-psram formal-onchip-ram formal-opipsram formal-dma formal-ga2d formal-apu formal-apu-kws formal-apu-loader formal-apu-loader-p5 formal-apu-sequencer formal-gateway-a formal-sdio formal-npu_dma formal-doctor formal-clean
+3
View File
@@ -201,12 +201,15 @@
"npu_control_cdc.sv",
"npu_core.sv",
"npu_define.svh",
"npu_dma.sv",
"npu_job_decoder.sv",
"npu_local_sram.sv",
"npu_mac_array.sv",
"npu_patch_packer.sv",
"npu_pkg.sv",
"npu_reg.sv",
"npu_requantizer.sv",
"npu_scheduler.sv",
"npu_vector.sv"
],
"rtl/ip/peripheral": [
+191 -10
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Run the isolated NPU-P1 IHP130 synthesis and 72 MHz STA flow."""
"""Run the isolated NPU IHP130 synthesis and 72 MHz STA flow (P1/P3 modules)."""
from __future__ import annotations
@@ -31,6 +31,11 @@ TOPS = (
"npu_accumulator",
"npu_vector",
"npu_requantizer",
"npu_dma",
"npu_job_decoder",
"npu_scheduler",
"npu_core",
"apb4_npu",
)
NPU_RTL = (
"npu_pkg.sv",
@@ -40,7 +45,159 @@ NPU_RTL = (
"npu_accumulator.sv",
"npu_vector.sv",
"npu_requantizer.sv",
"npu_dma.sv",
"npu_job_decoder.sv",
"npu_scheduler.sv",
"npu_reg.sv",
"npu_control_cdc.sv",
"npu_core.sv",
"apb4_npu.sv",
)
# Tops whose only non-logic port is an AXI4 master interface get a generated
# synthesis wrapper in the build tree (never tracked): the interface is
# instantiated inside the wrapper so the DUT has no unconnected ports.
INTERFACE_WRAPPED = {
"npu_dma": """
module npu_dma_synth_top (
input logic clk_hp_i,
input logic rst_hp_n_i,
input logic clear_i,
input logic block_new_i,
output logic pause_ack_o,
input logic read_req_valid_i,
output logic read_req_ready_o,
input logic [31:0] read_addr_i,
input logic [31:0] read_bytes_i,
output logic read_data_valid_o,
input logic read_data_ready_i,
output logic [63:0] read_data_o,
output logic [ 7:0] read_keep_o,
output logic read_last_o,
input logic write_req_valid_i,
output logic write_req_ready_o,
input logic [31:0] write_addr_i,
input logic [31:0] write_bytes_i,
input logic write_data_valid_i,
output logic write_data_ready_o,
input logic [63:0] write_data_i,
input logic [ 7:0] write_keep_i,
input logic write_last_i,
output logic write_done_o,
output logic busy_o,
output logic read_busy_o,
output logic write_busy_o,
output logic [63:0] read_bytes_o,
output logic [63:0] write_bytes_o,
output logic [63:0] stall_cycles_o,
output logic fault_o,
output logic [ 3:0] fault_code_o,
output logic [31:0] fault_addr_o,
output logic [ 1:0] fault_resp_o,
output logic read_cmd_err_o,
output logic write_cmd_err_o
);
axi4_if #(
.ADDR_WIDTH(32),
.DATA_WIDTH(64),
.ID_WIDTH (3),
.USER_WIDTH(1)
) u_axi4 (
.aclk (clk_hp_i),
.aresetn(rst_hp_n_i)
);
npu_dma u_dut (
.clk_hp_i (clk_hp_i),
.rst_hp_n_i (rst_hp_n_i),
.clear_i (clear_i),
.block_new_i (block_new_i),
.pause_ack_o (pause_ack_o),
.read_req_valid_i (read_req_valid_i),
.read_req_ready_o (read_req_ready_o),
.read_addr_i (read_addr_i),
.read_bytes_i (read_bytes_i),
.read_data_valid_o (read_data_valid_o),
.read_data_ready_i (read_data_ready_i),
.read_data_o (read_data_o),
.read_keep_o (read_keep_o),
.read_last_o (read_last_o),
.write_req_valid_i (write_req_valid_i),
.write_req_ready_o (write_req_ready_o),
.write_addr_i (write_addr_i),
.write_bytes_i (write_bytes_i),
.write_data_valid_i(write_data_valid_i),
.write_data_ready_o(write_data_ready_o),
.write_data_i (write_data_i),
.write_keep_i (write_keep_i),
.write_last_i (write_last_i),
.write_done_o (write_done_o),
.busy_o (busy_o),
.read_busy_o (read_busy_o),
.write_busy_o (write_busy_o),
.read_bytes_o (read_bytes_o),
.write_bytes_o (write_bytes_o),
.stall_cycles_o (stall_cycles_o),
.fault_o (fault_o),
.fault_code_o (fault_code_o),
.fault_addr_o (fault_addr_o),
.fault_resp_o (fault_resp_o),
.read_cmd_err_o (read_cmd_err_o),
.write_cmd_err_o (write_cmd_err_o),
.axi4 (u_axi4)
);
endmodule
""",
"apb4_npu": """
module apb4_npu_synth_top (
input logic clk_i,
input logic rst_n_i,
input logic clk_hp_i,
input logic rst_hp_n_i,
input logic [1:0] resource_owner_i,
input logic resource_owner_lock_i,
input logic resource_quiesce_i,
input logic resource_reset_i,
output logic idle_o,
output logic block_ack_o,
output logic irq_o,
input logic hp_block_new_i,
output logic hp_pause_ack_o,
input logic hp_flush_i,
output logic hp_flush_busy_o,
output logic hp_idle_o
);
apb4_if u_apb4 (.pclk(clk_i), .presetn(rst_n_i));
axi4_if #(
.ADDR_WIDTH(32),
.DATA_WIDTH(64),
.ID_WIDTH (3),
.USER_WIDTH(1)
) u_npu_axi4 (
.aclk (clk_hp_i),
.aresetn(rst_hp_n_i)
);
apb4_npu u_dut (
.clk_i (clk_i),
.rst_n_i (rst_n_i),
.clk_hp_i (clk_hp_i),
.rst_hp_n_i (rst_hp_n_i),
.resource_owner_i (resource_owner_i),
.resource_owner_lock_i(resource_owner_lock_i),
.resource_quiesce_i (resource_quiesce_i),
.resource_reset_i (resource_reset_i),
.apb4 (u_apb4),
.idle_o (idle_o),
.block_ack_o (block_ack_o),
.irq_o (irq_o),
.hp_block_new_i (hp_block_new_i),
.hp_pause_ack_o (hp_pause_ack_o),
.hp_flush_i (hp_flush_i),
.hp_flush_busy_o (hp_flush_busy_o),
.hp_idle_o (hp_idle_o),
.npu_axi4 (u_npu_axi4)
);
endmodule
""",
}
SRAM_MACRO = "RM_IHPSG13_1P_1024x32_c2_bm_bist"
EXPECTED_SRAM_MACROS = 16
TARGET_PERIOD_PS = 13889
@@ -60,6 +217,10 @@ def _run_flow(tool: str, log: Path, result: Path, env: dict[str, str], command:
raise RuntimeError(f"{tool} flow failed for {env.get('TOP_DESIGN', env.get('OPENSTA_TOP'))}: {log}")
def _synth_top(top: str) -> str:
return f"{top}_synth_top" if top in INTERFACE_WRAPPED else top
def _write_filelist(path: Path) -> None:
lines = [
"+define+PDK_IHP130",
@@ -72,16 +233,36 @@ def _write_filelist(path: Path) -> None:
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}")
raise RuntimeError(f"missing NPU RTL source: {source}")
lines.append(str(source))
common_rtl = ROOT / "rtl/managed/clusterip/common/rtl"
lines.append(str(ROOT / "rtl/tech/tc_sram.sv"))
lines.append(str(ROOT / "rtl/managed/clusterip/common/rtl/utils/register.sv"))
for extra in (
"interface/apb4_if.sv",
"interface/axi4_if.sv",
"utils/register.sv",
"utils/xchecker.sv",
"utils/fifo.sv",
"clkrst/rst_sync.sv",
"cdc/cdc_sync.sv",
"cdc/cdc_rst_ctrlr.sv",
"cdc/async_reqack.sv",
):
source = common_rtl / extra
if not source.is_file():
raise RuntimeError(f"missing Common source: {source}")
lines.append(str(source))
path.parent.mkdir(parents=True, exist_ok=True)
for top, wrapper in INTERFACE_WRAPPED.items():
wrapper_path = path.parent / f"{_synth_top(top)}.sv"
wrapper_path.write_text(wrapper, encoding="utf-8")
lines.append(str(wrapper_path))
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
synth_top = _synth_top(top)
for sub in ("out", "tmp", "rpt"):
(base / sub).mkdir(parents=True, exist_ok=True)
env = {
@@ -92,17 +273,17 @@ def _synth(top: str, build_root: Path, filelist: Path) -> dict[str, object]:
"SRAM_SIZE_KIB": "32",
"YOSYS_TARGET_PERIOD_PS": str(TARGET_PERIOD_PS),
"SV_FLIST": str(filelist),
"TOP_DESIGN": top,
"PROJ_NAME": top,
"TOP_DESIGN": synth_top,
"PROJ_NAME": synth_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"),
"NETLIST": str(base / "out" / f"{synth_top}_yosys.v"),
"CONFIG": str(base / "out" / f"{synth_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"))
area = json.loads((base / "rpt" / f"{synth_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 = {
@@ -142,7 +323,7 @@ def _sta(top: str, build_root: Path, netlist: Path) -> dict[str, object]:
"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,
"OPENSTA_TOP": _synth_top(top),
}
log = base / f"{top}.log"
_run_flow("opensta", log, base / "result-sta.json", env, ["sta", "-no_init", "-exit", str(STA_TCL)])
@@ -216,7 +397,7 @@ def main() -> int:
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"
netlist = build_root / "syn/yosys-npu" / top / "out" / f"{_synth_top(top)}_yosys.v"
if args.sta:
if not netlist.is_file():
raise RuntimeError(f"missing netlist for {top}: {netlist}; run --synth first")
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+669
View File
@@ -0,0 +1,669 @@
`timescale 1ns / 1ps
// NPU-P3 npu_job_decoder unit test: the decoder drives a production npu_dma
// read channel against a byte-memory BFM with optional per-beat SLVERR/DECERR
// and malformed-RID injection. Descriptor images and expected verdicts come
// from plusargs generated by tests/test_npu_rtl_p3.py with
// scripts/npu_descriptors.py as the golden validator. Covered: valid records
// of every opcode (emitted record fields compared word-for-word against the
// image), every validator rejection class with first-fault code/word/index,
// multi-descriptor in-order traversal, fetch SLVERR/DECERR and protocol
// faults with forwarded address/response, descriptor-region truncation,
// pause retention mid-fetch, and no reuse of the launch slot after a terminal
// fault without a fresh job_start.
`include "npu_define.svh"
module npu_job_decoder_tb;
localparam logic [31:0] SramBase = 32'h3000_0000;
localparam int unsigned MemoryBytes = 262144;
localparam int unsigned PhaseTimeout = 20000;
// plusarg-driven case configuration
string s_img_path;
logic [31:0] s_job_base;
logic [31:0] s_job_count;
logic [31:0] s_mode; // 0 accept, 1 validation fault, 2 fetch fault, 3 pause fetch
logic [31:0] s_exp_fcode;
logic [31:0] s_exp_findex;
logic s_exp_wordless;
logic [31:0] s_exp_fword;
logic [31:0] s_rderr_addr;
logic [31:0] s_rderr_code;
logic [31:0] s_rderr_badid;
logic clk_hp_i = 1'b0;
logic rst_hp_n_i = 1'b0;
logic clear_i = 1'b0;
logic block_new_i = 1'b0;
logic dec_pause_ok;
logic job_start_i = 1'b0;
logic [31:0] job_base_i = '0;
logic [15:0] job_count_i = '0;
logic s_dec_read_req_valid;
logic s_dec_read_req_ready;
logic [31:0] s_dec_read_addr;
logic [31:0] s_dec_read_bytes;
logic s_dec_read_data_valid;
logic s_dec_read_data_ready;
logic [63:0] s_dec_read_data;
logic [ 7:0] s_dec_read_keep;
logic s_dec_read_last;
logic rec_valid_o;
logic rec_ready_i = 1'b0;
logic [ 3:0] rec_opcode_o;
logic [12:0] rec_h_o, rec_w_o, rec_cin_o, rec_cout_o, rec_oh_o, rec_ow_o;
logic [31:0] rec_input0_base_o, rec_output_base_o, rec_weight_base_o, rec_param_base_o;
logic [31:0] rec_input0_row_bytes_o, rec_output_row_bytes_o;
logic [7:0] rec_kh_o, rec_kw_o, rec_sh_o, rec_sw_o;
logic [7:0] rec_pad_top_o, rec_pad_left_o, rec_input0_zero_o;
logic [31:0] rec_weight_bytes_o, rec_param_bytes_o;
logic [15:0] rec_desc_index_o;
logic retired_i = 1'b0;
logic all_done_o;
logic busy_o;
logic [15:0] cur_desc_index_o;
logic progress_o;
logic fault_req_o;
logic [ 3:0] fault_code_o;
logic [31:0] fault_addr_o;
logic [31:0] fault_info_o;
logic [31:0] fault_desc_o;
logic [ 7:0] s_memory [0:MemoryBytes-1];
// read channel BFM state
logic s_rvalid_q;
logic [ 2:0] s_rid_q;
logic [ 1:0] s_rresp_q;
logic [31:0] s_raddr_q;
logic [63:0] s_rdata_q;
logic [ 2:0] s_rsize_q;
logic [ 8:0] s_rbeats_q;
// counters and bookkeeping
int unsigned s_ar_count;
int unsigned s_rd_outstanding_q;
int unsigned s_rec_count;
logic s_saw_all_done_q;
string s_phase;
int unsigned s_phase_cycles_q;
logic s_progress;
logic s_pause_armed_i;
logic s_pause_done_q;
axi4_if #(
.ADDR_WIDTH(32),
.DATA_WIDTH(64),
.ID_WIDTH (3),
.USER_WIDTH(1)
) npu_axi4 (
.aclk (clk_hp_i),
.aresetn(rst_hp_n_i)
);
npu_dma u_dma (
.clk_hp_i (clk_hp_i),
.rst_hp_n_i (rst_hp_n_i),
.clear_i (clear_i),
.block_new_i (block_new_i),
.pause_ack_o (),
.read_req_valid_i (s_dec_read_req_valid),
.read_req_ready_o (s_dec_read_req_ready),
.read_addr_i (s_dec_read_addr),
.read_bytes_i (s_dec_read_bytes),
.read_data_valid_o (s_dec_read_data_valid),
.read_data_ready_i (s_dec_read_data_ready),
.read_data_o (s_dec_read_data),
.read_keep_o (s_dec_read_keep),
.read_last_o (s_dec_read_last),
.write_req_valid_i (1'b0),
.write_req_ready_o (),
.write_addr_i (32'd0),
.write_bytes_i (32'd0),
.write_data_valid_i(1'b0),
.write_data_ready_o(),
.write_data_i (64'd0),
.write_keep_i (8'd0),
.write_last_i (1'b0),
.write_done_o (),
.busy_o (),
.read_busy_o (s_dma_read_busy),
.write_busy_o (),
.read_bytes_o (),
.write_bytes_o (),
.stall_cycles_o (),
.fault_o (s_dma_fault),
.fault_code_o (s_dma_fault_code),
.fault_addr_o (s_dma_fault_addr),
.fault_resp_o (s_dma_fault_resp),
.read_cmd_err_o (),
.write_cmd_err_o (),
.axi4 (npu_axi4)
);
logic s_dma_read_busy;
logic s_dma_fault;
logic [ 3:0] s_dma_fault_code;
logic [31:0] s_dma_fault_addr;
logic [ 1:0] s_dma_fault_resp;
npu_job_decoder u_dut (
.clk_hp_i (clk_hp_i),
.rst_hp_n_i (rst_hp_n_i),
.clear_i (clear_i),
.block_new_i (block_new_i),
.pause_ok_o (dec_pause_ok),
.job_start_i (job_start_i),
.job_base_i (job_base_i),
.job_count_i (job_count_i),
.read_req_valid_o (s_dec_read_req_valid),
.read_req_ready_i (s_dec_read_req_ready),
.read_addr_o (s_dec_read_addr),
.read_bytes_o (s_dec_read_bytes),
.read_data_valid_i (s_dec_read_data_valid),
.read_data_ready_o (s_dec_read_data_ready),
.read_data_i (s_dec_read_data),
.read_last_i (s_dec_read_last),
.dma_read_busy_i (s_dma_read_busy),
.dma_fault_i (s_dma_fault),
.dma_fault_code_i (s_dma_fault_code),
.dma_fault_addr_i (s_dma_fault_addr),
.dma_fault_resp_i (s_dma_fault_resp),
.rec_valid_o (rec_valid_o),
.rec_ready_i (rec_ready_i),
.rec_opcode_o (rec_opcode_o),
.rec_h_o (rec_h_o),
.rec_w_o (rec_w_o),
.rec_cin_o (rec_cin_o),
.rec_cout_o (rec_cout_o),
.rec_oh_o (rec_oh_o),
.rec_ow_o (rec_ow_o),
.rec_input0_base_o (rec_input0_base_o),
.rec_output_base_o (rec_output_base_o),
.rec_weight_base_o (rec_weight_base_o),
.rec_param_base_o (rec_param_base_o),
.rec_input0_row_bytes_o(rec_input0_row_bytes_o),
.rec_output_row_bytes_o(rec_output_row_bytes_o),
.rec_kh_o (rec_kh_o),
.rec_kw_o (rec_kw_o),
.rec_sh_o (rec_sh_o),
.rec_sw_o (rec_sw_o),
.rec_pad_top_o (rec_pad_top_o),
.rec_pad_left_o (rec_pad_left_o),
.rec_input0_zero_o (rec_input0_zero_o),
.rec_weight_bytes_o (rec_weight_bytes_o),
.rec_param_bytes_o (rec_param_bytes_o),
.rec_desc_index_o (rec_desc_index_o),
.retired_i (retired_i),
.all_done_o (all_done_o),
.busy_o (busy_o),
.cur_desc_index_o (cur_desc_index_o),
.progress_o (progress_o),
.fault_req_o (fault_req_o),
.fault_code_o (fault_code_o),
.fault_addr_o (fault_addr_o),
.fault_info_o (fault_info_o),
.fault_desc_o (fault_desc_o)
);
always #5 clk_hp_i = ~clk_hp_i;
function automatic logic [7:0] memory_byte(input logic [31:0] address_i);
if ((address_i >= SramBase) && (address_i < (SramBase + MemoryBytes))) begin
return s_memory[address_i-SramBase];
end
return 8'd0;
endfunction
function automatic logic [63:0] memory_word(input logic [31:0] address_i);
logic [63:0] data;
logic [31:0] aligned_address;
begin
data = '0;
aligned_address = {address_i[31:3], 3'b000};
for (int unsigned lane = 0; lane < 8; lane++) begin
data[lane*8+:8] = memory_byte(aligned_address + lane);
end
return data;
end
endfunction
function automatic logic [31:0] image_word(input int unsigned desc_i, input int unsigned word_i);
logic [31:0] value;
begin
value = '0;
for (int unsigned lane = 0; lane < 4; lane++) begin
value[lane*8+:8] =
memory_byte(s_job_base + 32'(desc_i) * 32'd128 + 32'(word_i) * 32'd4 + 32'(lane));
end
return value;
end
endfunction
function automatic logic [1:0] bfm_read_resp(input logic [31:0] beat_addr_i);
if ((s_rderr_addr != 32'hffff_ffff) && (beat_addr_i == s_rderr_addr)) begin
return s_rderr_code[1:0];
end
return 2'b00;
endfunction
// ------------------------- AXI read slave BFM -------------------------
assign npu_axi4.arready = !s_hold_ar_i && !s_rvalid_q;
assign npu_axi4.rid = s_rid_q;
assign npu_axi4.rdata = s_rdata_q;
assign npu_axi4.rresp = s_rresp_q;
assign npu_axi4.rlast = (s_rbeats_q == 9'd1);
assign npu_axi4.ruser = '0;
assign npu_axi4.rvalid = !s_hold_r_i && s_rvalid_q;
// the decoder never writes
assign npu_axi4.awready = 1'b0;
assign npu_axi4.wready = 1'b0;
assign npu_axi4.bid = '0;
assign npu_axi4.bresp = 2'b00;
assign npu_axi4.buser = '0;
assign npu_axi4.bvalid = 1'b0;
logic s_hold_ar_i;
logic s_hold_r_i;
always_ff @(posedge clk_hp_i or negedge rst_hp_n_i) begin
if (!rst_hp_n_i) begin
s_rvalid_q <= 1'b0;
s_rid_q <= '0;
s_rresp_q <= 2'd0;
s_raddr_q <= '0;
s_rdata_q <= '0;
s_rsize_q <= '0;
s_rbeats_q <= '0;
s_ar_count <= 0;
s_rd_outstanding_q <= 0;
end else if (clear_i) begin
s_rvalid_q <= 1'b0;
s_rbeats_q <= '0;
s_rd_outstanding_q <= 0;
end else begin
if (npu_axi4.arvalid && npu_axi4.arready) begin
if (s_rd_outstanding_q != 0) begin
$fatal(1, "NPU decoder TB: second outstanding read");
end
if ((npu_axi4.araddr & ((32'd1 << npu_axi4.arsize) - 32'd1)) != 32'd0) begin
$fatal(1, "NPU decoder TB: unaligned read transfer");
end
if ((npu_axi4.arsize != 3'd3) && (npu_axi4.arlen != 8'd0)) begin
$fatal(1, "NPU decoder TB: multi-beat narrow read burst");
end
if ({1'b0, npu_axi4.araddr[11:0]} + (({5'd0, npu_axi4.arlen} + 13'd1) << npu_axi4.arsize)
> 13'd4096) begin
$fatal(1, "NPU decoder TB: read burst crossed a 4 KiB boundary");
end
if ((npu_axi4.araddr < s_job_base) ||
(npu_axi4.araddr >= (s_job_base + {16'd0, s_job_count[15:0], 7'd0}))) begin
$fatal(1, "NPU decoder TB: read escaped the descriptor region");
end
s_rvalid_q <= 1'b1;
s_rid_q <= ((s_rderr_badid != 32'd0) &&
(npu_axi4.araddr == {s_rderr_addr[31:3], 3'b000})) ? 3'd1 : 3'd0;
s_rresp_q <= bfm_read_resp(npu_axi4.araddr);
s_raddr_q <= npu_axi4.araddr;
s_rdata_q <= memory_word(npu_axi4.araddr);
s_rsize_q <= npu_axi4.arsize;
s_rbeats_q <= {1'b0, npu_axi4.arlen} + 1'b1;
s_ar_count <= s_ar_count + 1;
s_rd_outstanding_q <= s_rd_outstanding_q + 1;
end else if (s_rvalid_q && npu_axi4.rvalid && npu_axi4.rready) begin
if (s_rbeats_q == 9'd1) begin
s_rvalid_q <= 1'b0;
s_rbeats_q <= '0;
s_rd_outstanding_q <= s_rd_outstanding_q - 1;
end else begin
s_raddr_q <= s_raddr_q + (32'd1 << s_rsize_q);
s_rdata_q <= memory_word(s_raddr_q + (32'd1 << s_rsize_q));
s_rresp_q <= bfm_read_resp(s_raddr_q + (32'd1 << s_rsize_q));
s_rbeats_q <= s_rbeats_q - 1'b1;
end
end
if (npu_axi4.awvalid || npu_axi4.wvalid) begin
$fatal(1, "NPU decoder TB: unexpected write channel activity");
end
end
end
// sticky all_done capture (the pulse is one cycle)
always_ff @(posedge clk_hp_i or negedge rst_hp_n_i) begin
if (!rst_hp_n_i) begin
s_saw_all_done_q <= 1'b0;
end else if (all_done_o) begin
s_saw_all_done_q <= 1'b1;
end
end
// progress watchdog: any decoder or bus forward motion resets the phase timer
assign s_progress = (npu_axi4.arvalid && npu_axi4.arready) ||
(npu_axi4.rvalid && npu_axi4.rready) ||
(s_dec_read_req_valid && s_dec_read_req_ready) ||
(rec_valid_o && rec_ready_i) || retired_i || progress_o;
always_ff @(posedge clk_hp_i or negedge rst_hp_n_i) begin
if (!rst_hp_n_i) begin
s_phase_cycles_q <= 0;
end else begin
if (s_progress) begin
s_phase_cycles_q <= 0;
end else begin
s_phase_cycles_q <= s_phase_cycles_q + 1;
if (s_phase_cycles_q >= PhaseTimeout) begin
$fatal(1, "NPU decoder TB phase timed out: %s", s_phase);
end
end
end
end
// pause stimulus: hold block_new_i for a 30-cycle window once the fetch
// command of descriptor 0 is presented
logic [5:0] s_pause_cnt_q;
always_ff @(posedge clk_hp_i or negedge rst_hp_n_i) begin
if (!rst_hp_n_i) begin
block_new_i <= 1'b0;
s_pause_done_q <= 1'b0;
s_pause_cnt_q <= 6'd0;
end else begin
if (s_pause_armed_i && !s_pause_done_q && s_dec_read_req_valid) begin
block_new_i <= 1'b1;
s_pause_cnt_q <= 6'd0;
s_pause_done_q <= 1'b1;
end else if (s_pause_done_q && block_new_i) begin
if (s_pause_cnt_q == 6'd30) begin
block_new_i <= 1'b0;
end else begin
s_pause_cnt_q <= s_pause_cnt_q + 6'd1;
end
end
end
end
task automatic idle_cycles(input int unsigned cycles_i);
begin
repeat (cycles_i) @(posedge clk_hp_i);
end
endtask
task automatic set_phase(input string name_i);
begin
@(negedge clk_hp_i);
s_phase = name_i;
idle_cycles(1);
end
endtask
task automatic load_image(input string path_i, input logic [31:0] base_i);
logic [31:0] s_img [0:65535];
int unsigned s_img_words;
begin
$readmemh(path_i, s_img);
s_img_words = 32'(s_job_count) * 32;
for (int unsigned index = 0; index < s_img_words; index++) begin
for (int unsigned lane = 0; lane < 4; lane++) begin
s_memory[base_i-SramBase+index*4+lane] = s_img[index][lane*8+:8];
end
end
end
endtask
task automatic launch_job;
begin
@(negedge clk_hp_i);
job_base_i = s_job_base;
job_count_i = s_job_count[15:0];
job_start_i = 1'b1;
@(negedge clk_hp_i);
job_start_i = 1'b0;
end
endtask
task automatic check_record(input int unsigned desc_i);
logic [31:0] s_w0, s_w6, s_w7, s_w8, s_w12, s_w13, s_w20;
begin
s_w0 = image_word(desc_i, 0);
s_w6 = image_word(desc_i, 6);
s_w7 = image_word(desc_i, 7);
s_w8 = image_word(desc_i, 8);
s_w12 = image_word(desc_i, 12);
s_w13 = image_word(desc_i, 13);
s_w20 = image_word(desc_i, 20);
if (32'(rec_opcode_o) != {24'd0, s_w0[7:0]}) begin
$fatal(1, "record %0d opcode mismatch: %h", desc_i, rec_opcode_o);
end
if ((20'(rec_h_o) != {4'd0, s_w6[15:0]}) || (20'(rec_w_o) != {4'd0, s_w6[31:16]})) begin
$fatal(1, "record %0d input geometry mismatch", desc_i);
end
if ((20'(rec_cin_o) != {4'd0, s_w7[15:0]}) || (20'(rec_cout_o) != {4'd0, s_w7[31:16]})) begin
$fatal(1, "record %0d channels mismatch", desc_i);
end
if ((20'(rec_oh_o) != {4'd0, s_w8[15:0]}) || (20'(rec_ow_o) != {4'd0, s_w8[31:16]})) begin
$fatal(1, "record %0d output geometry mismatch", desc_i);
end
if ((rec_input0_base_o != image_word(
desc_i, 2
)) || (rec_output_base_o != image_word(
desc_i, 4
)) || (rec_weight_base_o != image_word(
desc_i, 24
)) || (rec_param_base_o != image_word(
desc_i, 5
))) begin
$fatal(1, "record %0d base address mismatch", desc_i);
end
if ((rec_input0_row_bytes_o != image_word(
desc_i, 9
)) || (rec_output_row_bytes_o != image_word(
desc_i, 10
))) begin
$fatal(1, "record %0d row stride mismatch", desc_i);
end
if ((rec_kh_o != s_w12[7:0]) || (rec_kw_o != s_w12[15:8]) ||
(rec_sh_o != s_w12[23:16]) || (rec_sw_o != s_w12[31:24])) begin
$fatal(1, "record %0d kernel/stride mismatch", desc_i);
end
if ((rec_pad_top_o != s_w13[7:0]) || (rec_pad_left_o != s_w13[23:16]) ||
(rec_input0_zero_o != s_w20[7:0])) begin
$fatal(1, "record %0d padding/zero mismatch", desc_i);
end
if ((rec_weight_bytes_o != image_word(
desc_i, 25
)) || (rec_param_bytes_o != image_word(
desc_i, 19
))) begin
$fatal(1, "record %0d weight/param bytes mismatch", desc_i);
end
if (rec_desc_index_o != 16'(desc_i)) begin
$fatal(1, "record %0d index mismatch: %0d", desc_i, rec_desc_index_o);
end
end
endtask
task automatic retire_record;
begin
idle_cycles(3);
@(negedge clk_hp_i);
retired_i = 1'b1;
@(negedge clk_hp_i);
retired_i = 1'b0;
end
endtask
task automatic expect_fault;
int unsigned s_wait;
begin
s_wait = 0;
while (!fault_req_o) begin
@(posedge clk_hp_i);
s_wait++;
if (s_wait > 4000) $fatal(1, "NPU decoder fault never reported in %s", s_phase);
end
if ({28'd0, fault_code_o} != s_exp_fcode) begin
$fatal(1, "NPU decoder fault code mismatch: got %0d expected %0d (%s)", fault_code_o,
s_exp_fcode, s_phase);
end
if (fault_desc_o != s_exp_findex) begin
$fatal(1, "NPU decoder fault index mismatch: got %0d expected %0d (%s)", fault_desc_o,
s_exp_findex, s_phase);
end
if (s_mode == 32'd2) begin
if (fault_addr_o != s_rderr_addr) begin
$fatal(1, "NPU decoder fault address mismatch: got %h expected %h", fault_addr_o,
s_rderr_addr);
end
if (s_rderr_badid == 32'd0) begin
if (fault_info_o[1:0] != s_rderr_code[1:0] || fault_info_o[3:2] != 2'd1 ||
fault_info_o[15:8] != 8'd255) begin
$fatal(1, "NPU decoder fault info mismatch: %h", fault_info_o);
end
end
end else begin
if (s_exp_wordless) begin
if (fault_addr_o != (s_job_base + s_exp_findex * 32'd128)) begin
$fatal(1, "NPU decoder wordless fault address mismatch: got %h expected %h",
fault_addr_o, s_job_base + s_exp_findex * 32'd128);
end
end else if (fault_addr_o != (s_job_base + s_exp_findex * 32'd128 + s_exp_fword * 32'd4)) begin
$fatal(1, "NPU decoder fault address mismatch: got %h expected %h", fault_addr_o,
s_job_base + s_exp_findex * 32'd128 + s_exp_fword * 32'd4);
end
if (fault_info_o != {16'd0, 8'd255, 4'd0, 2'd0, 2'd0}) begin
$fatal(1, "NPU decoder validation fault info mismatch: %h", fault_info_o);
end
end
// terminal: no record emission, no new fetch while the fault is held
idle_cycles(20);
if (rec_valid_o || s_dec_read_req_valid || !fault_req_o) begin
$fatal(1, "NPU decoder kept working after a terminal fault");
end
// coordinated cancel returns the decoder to idle
@(negedge clk_hp_i);
clear_i = 1'b1;
idle_cycles(2);
@(negedge clk_hp_i);
clear_i = 1'b0;
idle_cycles(10);
if (busy_o || fault_req_o) $fatal(1, "NPU decoder did not clear after cancel");
end
endtask
initial begin
s_phase = "plusargs";
s_hold_ar_i = 1'b0;
s_hold_r_i = 1'b0;
s_pause_armed_i = 1'b0;
// keep the plusarg targets procedural variables for Icarus vpi
s_job_base = 32'd0;
s_job_count = 32'd0;
s_mode = 32'd0;
s_exp_fcode = 32'd0;
s_exp_findex = 32'd0;
s_exp_fword = 32'hffff_ffff;
s_rderr_addr = 32'hffff_ffff;
s_rderr_code = 32'd0;
s_rderr_badid = 32'd0;
if (!$value$plusargs("IMG=%s", s_img_path)) $fatal(1, "missing +IMG");
if (!$value$plusargs("BASE=%h", s_job_base)) $fatal(1, "missing +BASE");
if (!$value$plusargs("COUNT=%d", s_job_count)) $fatal(1, "missing +COUNT");
if (!$value$plusargs("MODE=%d", s_mode)) $fatal(1, "missing +MODE");
if (!$value$plusargs("FCODE=%d", s_exp_fcode)) $fatal(1, "missing +FCODE");
if (!$value$plusargs("FINDEX=%d", s_exp_findex)) $fatal(1, "missing +FINDEX");
if (!$value$plusargs("FWORD=%d", s_exp_fword)) $fatal(1, "missing +FWORD");
if (!$value$plusargs("RDERR_ADDR=%h", s_rderr_addr)) $fatal(1, "missing +RDERR_ADDR");
if (!$value$plusargs("RDERR_CODE=%d", s_rderr_code)) $fatal(1, "missing +RDERR_CODE");
if (!$value$plusargs("RDERR_BADID=%d", s_rderr_badid)) $fatal(1, "missing +RDERR_BADID");
s_exp_wordless = (s_exp_fword == 32'hffff_ffff);
for (int unsigned index = 0; index < MemoryBytes; index++) begin
s_memory[index] = 8'(index >> 12) ^ 8'(index);
end
set_phase("reset");
repeat (4) @(posedge clk_hp_i);
@(negedge clk_hp_i);
rst_hp_n_i = 1'b1;
idle_cycles(4);
set_phase("load image");
load_image(s_img_path, s_job_base);
if (s_mode == 32'd3) begin
s_pause_armed_i = 1'b1;
end
if ((s_mode == 32'd1) || (s_mode == 32'd2)) begin
// validation/fetch fault: the fault may follow earlier valid records
set_phase("fault traversal");
launch_job();
for (int unsigned desc = 0; desc < 32'(s_exp_findex); desc++) begin
while (!rec_valid_o) begin
@(posedge clk_hp_i);
if (fault_req_o) $fatal(1, "NPU decoder faulted early at %0d", desc);
end
if (rec_desc_index_o != 16'(desc)) $fatal(1, "NPU decoder record order broken");
check_record(desc);
@(negedge clk_hp_i);
rec_ready_i = 1'b1;
@(negedge clk_hp_i);
rec_ready_i = 1'b0;
retire_record();
end
expect_fault();
end else begin
// accept (mode 0) and pause-during-fetch (mode 3)
set_phase("accept");
launch_job();
for (int unsigned desc = 0; desc < 32'(s_job_count); desc++) begin
while (!rec_valid_o) begin
@(posedge clk_hp_i);
if (fault_req_o) $fatal(1, "NPU decoder faulted on a valid image at %0d", desc);
end
if (rec_desc_index_o != 16'(desc)) $fatal(1, "NPU decoder record order broken");
check_record(desc);
@(negedge clk_hp_i);
rec_ready_i = 1'b1;
@(negedge clk_hp_i);
rec_ready_i = 1'b0;
retire_record();
end
if (!s_saw_all_done_q) $fatal(1, "NPU decoder all_done missing");
idle_cycles(10);
if (busy_o || fault_req_o) $fatal(1, "NPU decoder stayed busy after all_done");
if (s_ar_count != 32'(s_job_count)) begin
$fatal(1, "NPU decoder fetch count mismatch: %0d", s_ar_count);
end
end
if ((s_mode == 32'd1) || (s_mode == 32'd2)) begin
// no reuse of the launch slot after terminal without a fresh start; a
// fresh job_start re-fetches and re-validates the same image
set_phase("no reuse");
idle_cycles(20);
if (rec_valid_o || s_dec_read_req_valid) begin
$fatal(1, "NPU decoder reused the terminal launch slot");
end
if ((s_mode == 32'd1) || (s_mode == 32'd2)) begin
launch_job();
for (int unsigned desc = 0; desc < 32'(s_exp_findex); desc++) begin
while (!rec_valid_o) begin
@(posedge clk_hp_i);
if (fault_req_o) $fatal(1, "NPU decoder refetch faulted early at %0d", desc);
end
check_record(desc);
@(negedge clk_hp_i);
rec_ready_i = 1'b1;
@(negedge clk_hp_i);
rec_ready_i = 1'b0;
retire_record();
end
expect_fault();
end
end
set_phase("done");
$display("NPU job decoder test passed");
$finish;
end
initial begin
repeat (500000) @(posedge clk_hp_i);
$fatal(1, "NPU decoder TB global timeout in %s", s_phase);
end
endmodule
+4 -5
View File
@@ -725,14 +725,13 @@ module npu_reg_tb;
$finish;
end
// The tied-off Phase 2 master must never issue a request and stays ready.
// With START still rejected at Phase 3, no software-visible job can launch,
// so the master must never issue a request in this shell context. The R/B
// receivers are engine-driven at Phase 3 (live while obligations exist).
always @(posedge clk_hp_i) begin
if (rst_hp_n_i) begin
if (npu_axi4.awvalid || npu_axi4.wvalid || npu_axi4.arvalid) begin
$fatal(1, "NPU Phase 2 AXI master issued a request");
end
if (!npu_axi4.bready || !npu_axi4.rready) begin
$fatal(1, "NPU Phase 2 AXI receivers not ready");
$fatal(1, "NPU shell master issued a request without an accepted job");
end
end
end
+813
View File
@@ -0,0 +1,813 @@
"""NPU-P3 job-execution RTL unit tests (Icarus and Verilator).
``test_npu_dma`` drives the AXI4 DMA engine testbench. ``test_npu_job_decoder``
drives the descriptor fetch/validation testbench with descriptor images and
expected verdicts generated here, using ``scripts/npu_descriptors.py`` as the
golden validator (fault code and ABI word parity). ``test_npu_core`` drives the
job-level testbench (production launch interface into npu_core + npu_dma over a
byte-memory BFM) with directed transport jobs whose stored bytes are checked
against a Python golden model of the documented P3 transport-content echo rule
(output byte (oy, ox, c) echoes the gather byte at reduction index c mod G).
"""
from __future__ import annotations
import os
import shutil
import struct
import subprocess
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
COMMON = ROOT / "rtl/managed/clusterip/common/rtl"
MULTIMEDIA = ROOT / "rtl/ip/multimedia"
TECH = ROOT / "rtl/tech"
sys.path.insert(0, str(ROOT / "scripts"))
from npu_descriptors import ( # noqa: E402
DESCRIPTOR_BYTES,
Descriptor,
DescriptorError,
validate_descriptor,
)
INCDIRS = [COMMON, COMMON / "interface", MULTIMEDIA]
COMMON_SOURCES = [
COMMON / "interface/axi4_if.sv",
]
DMA_SOURCES = [
MULTIMEDIA / "npu_dma.sv",
]
DECODER_SOURCES = [
MULTIMEDIA / "npu_pkg.sv",
MULTIMEDIA / "npu_dma.sv",
MULTIMEDIA / "npu_job_decoder.sv",
]
CORE_SOURCES = [
COMMON / "utils/register.sv",
MULTIMEDIA / "npu_pkg.sv",
TECH / "tc_sram.sv",
MULTIMEDIA / "npu_local_sram.sv",
MULTIMEDIA / "npu_patch_packer.sv",
MULTIMEDIA / "npu_dma.sv",
MULTIMEDIA / "npu_job_decoder.sv",
MULTIMEDIA / "npu_scheduler.sv",
MULTIMEDIA / "npu_core.sv",
]
MEM_BASE = 0x3000_0000
DEC_MEM_BYTES = 0x40000 # decoder TB memory: 256 KiB
CORE_MEM_BYTES = 0x80000 # core TB memory: 512 KiB
GAP_FILL = 0xA5
# fault codes
F_DESCRIPTOR = 1
F_UNSUPPORTED = 2
F_RANGE = 3
F_AXI_READ = 4
F_AXI_WRITE = 5
F_AXI_PROTOCOL = 6
F_ARITHMETIC = 7
F_NO_PROGRESS = 8
F_LOCAL_STATE = 9
F_RESET_CANCELLED = 10
# result codes
R_DONE = 1
R_ERROR = 2
R_ABORTED = 3
R_RESET_CANCELLED = 4
UNIT_KERNEL_STRIDE = 0x01010101
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, top: str,
sources: list[Path]) -> Path:
filelist = tmp_path / f"{name}.fl"
filelist.write_text(
"\n".join(
[
"+define+SV_ASSRT_DISABLE",
"+define+PDK_BEHAV",
*(f"+incdir+{incdir}" for incdir in INCDIRS),
*(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"
subprocess.run(
[tools["iverilog"], "-g2012", "-s", top, "-o", str(simulation), str(converted)],
check=True,
)
return simulation
def _build_verilator(tools: dict[str, str], tmp_path: Path, name: str, top: str,
sources: list[Path]) -> 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",
"+define+PDK_BEHAV",
"--top-module",
top,
*(f"+incdir+{incdir}" for incdir in INCDIRS),
*(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 _run_p3_test(tools: dict[str, str], simulator: str, tmp_path: Path, name: str, top: str,
testbench: Path, sources: list[Path], marker: str) -> None:
full_sources = [*COMMON_SOURCES, *sources, testbench]
if simulator == "iverilog":
simulation = _build_iverilog(tools, tmp_path, name, top, full_sources)
command = [tools["vvp"], str(simulation)]
elif simulator == "verilator":
command = [str(_build_verilator(tools, tmp_path, name, top, full_sources))]
else:
raise AssertionError(f"unknown simulator {simulator}")
result = subprocess.run(command, check=True, text=True, capture_output=True)
assert marker in result.stdout
def _build_sim(tools: dict[str, str], simulator: str, tmp_path: Path, name: str, top: str,
testbench: Path, sources: list[Path]) -> list[str]:
full_sources = [*COMMON_SOURCES, *sources, testbench]
if simulator == "iverilog":
return [tools["vvp"], str(_build_iverilog(tools, tmp_path, name, top, full_sources))]
if simulator == "verilator":
return [str(_build_verilator(tools, tmp_path, name, top, full_sources))]
raise AssertionError(f"unknown simulator {simulator}")
def _run(command: list[str], plusargs: list[str], marker: str) -> str:
result = subprocess.run([*command, *plusargs], check=True, text=True, capture_output=True)
assert marker in result.stdout
return result.stdout
# ---------------------------------------------------------------------------
# Descriptor construction (mirrors scripts/npu_descriptors.py ABI 1.0)
# ---------------------------------------------------------------------------
def _slot_bases(slot: int, shift: int = 0) -> dict[str, int]:
# fully disjoint operand regions per descriptor slot (decoder validation
# is arithmetic-only; the core TB executes the contents)
return {
"in0": 0x3000_4000 + (slot + shift) * 0x4000,
"in1": 0x3001_4000 + (slot + shift) * 0x4000,
"out": 0x3002_0000 + (slot + shift) * 0x8000,
"w": 0x3004_0000 + (slot + shift) * 0x8000,
"p": 0x3006_0000 + (slot + shift) * 0x8000,
}
def _valid_descriptor(opcode: int, slot: int, shift: int = 0) -> Descriptor:
"""One ABI-1.0-valid record for opcode in operand slot `slot`."""
b = _slot_bases(slot, shift)
d = Descriptor()
d.version_opcode = 0x0100_0000 | opcode
d.act_min = -128
d.act_max = 127
d.input0_base = b["in0"]
d.input1_base = 0
d.output_base = b["out"]
d.param_base = 0
d.weight_base = 0
d.weight_bytes = 0
d.param_bytes = 0
d.input1_zero = 0
if opcode == 1: # CONV2D
h, w, cin, cout = 5, 6, 3, 5
kh, kw, sh, sw = 3, 2, 1, 2
pt, pb, pl, pr = 1, 1, 0, 1
oh = (h + pt + pb - kh) // sh + 1
ow = (w + pl + pr - kw) // sw + 1
d.input_hw = (w << 16) | h
d.channels = (cout << 16) | cin
d.output_hw = (ow << 16) | oh
d.input0_row_bytes = w * cin + 2
d.output_row_bytes = ow * cout + 1
d.kernel_stride = (sw << 24) | (sh << 16) | (kw << 8) | kh
d.padding = (pr << 24) | (pl << 16) | (pb << 8) | pt
d.tile_hw = (2 << 8) | 2
full_k = kh * kw * cin
d.k_slice = 7
assert d.k_slice <= full_k
d.input0_bytes = (h - 1) * d.input0_row_bytes + w * cin
d.output_bytes = (oh - 1) * d.output_row_bytes + ow * cout
d.weight_base = b["w"]
d.weight_bytes = ((cout + 7) // 8) * full_k * 8
d.param_base = b["p"]
d.param_bytes = cout * 16
elif opcode == 2: # DEPTHWISE3X3
h, w, cin = 6, 7, 5
sh = sw = 2
pt, pl = 1, 1
oh = (h + pt - 3) // sh + 1
ow = (w + pl - 3) // sw + 1
d.input_hw = (w << 16) | h
d.channels = (cin << 16) | cin
d.output_hw = (ow << 16) | oh
d.input0_row_bytes = w * cin + 1
d.output_row_bytes = ow * cin + 1
d.kernel_stride = (sw << 24) | (sh << 16) | (3 << 8) | 3
d.padding = pl << 16 | pt
d.tile_hw = (2 << 8) | 2
d.k_slice = 9
d.input0_bytes = (h - 1) * d.input0_row_bytes + w * cin
d.output_bytes = (oh - 1) * d.output_row_bytes + ow * cin
d.weight_base = b["w"]
d.weight_bytes = ((cin + 7) // 8) * 72
d.param_base = b["p"]
d.param_bytes = cin * 16
elif opcode == 3: # FULLY_CONNECTED
cin, cout = 13, 4
d.input_hw = (1 << 16) | 1
d.channels = (cout << 16) | cin
d.output_hw = (1 << 16) | 1
d.input0_row_bytes = cin + 3
d.output_row_bytes = cout + 4
d.kernel_stride = UNIT_KERNEL_STRIDE
d.padding = 0
d.tile_hw = (1 << 8) | 1
d.k_slice = 13
d.input0_bytes = cin
d.output_bytes = cout
d.weight_base = b["w"]
d.weight_bytes = ((cout + 7) // 8) * cin * 8
d.param_base = b["p"]
d.param_bytes = cout * 16
elif opcode == 4: # ADD
h, w, cin = 2, 3, 3
d.input_hw = (w << 16) | h
d.channels = (cin << 16) | cin
d.output_hw = (w << 16) | h
d.input0_row_bytes = w * cin + 3
d.output_row_bytes = w * cin + 3
d.input1_row_bytes = w * cin + 3
d.input1_base = b["in1"]
d.input1_bytes = (h - 1) * d.input1_row_bytes + w * cin
d.input1_zero = -5
d.kernel_stride = UNIT_KERNEL_STRIDE
d.padding = 0
d.tile_hw = (2 << 8) | 2
d.k_slice = 1
d.input0_bytes = (h - 1) * d.input0_row_bytes + w * cin
d.output_bytes = (h - 1) * d.output_row_bytes + w * cin
d.param_base = b["p"]
d.param_bytes = 32
elif opcode in (5, 6): # MAX_POOL / AVERAGE_POOL
h, w, cin = 6, 6, 4
kh = kw = 3 if opcode == 5 else 2
sh = sw = 1 if opcode == 5 else 2
pt = pb = pl = pr = 1
oh = (h + pt + pb - kh) // sh + 1
ow = (w + pl + pr - kw) // sw + 1
d.input_hw = (w << 16) | h
d.channels = (cin << 16) | cin
d.output_hw = (ow << 16) | oh
d.input0_row_bytes = w * cin
d.output_row_bytes = ow * cin
d.kernel_stride = (sw << 24) | (sh << 16) | (kw << 8) | kh
d.padding = (pr << 24) | (pl << 16) | (pb << 8) | pt
d.tile_hw = (2 << 8) | 2
d.k_slice = min(5, kh * kw)
d.input0_bytes = (h - 1) * d.input0_row_bytes + w * cin
d.output_bytes = (oh - 1) * d.output_row_bytes + ow * cin
d.input0_zero = 11
d.output_zero = 11
elif opcode == 7: # GLOBAL_AVERAGE_POOL
h, w, cin = 4, 5, 6
d.input_hw = (w << 16) | h
d.channels = (cin << 16) | cin
d.output_hw = (1 << 16) | 1
d.input0_row_bytes = w * cin + 2
d.output_row_bytes = cin
d.kernel_stride = 0
d.padding = 0
d.tile_hw = (1 << 8) | 1
d.k_slice = 9
d.input0_bytes = (h - 1) * d.input0_row_bytes + w * cin
d.output_bytes = cin
d.input0_zero = -3
d.output_zero = -3
elif opcode == 8: # CLAMP
h, w, cin = 3, 5, 4
d.input_hw = (w << 16) | h
d.channels = (cin << 16) | cin
d.output_hw = (w << 16) | h
d.input0_row_bytes = w * cin + 4
d.output_row_bytes = w * cin + 12
d.kernel_stride = UNIT_KERNEL_STRIDE
d.padding = 0
d.tile_hw = (2 << 8) | 2
d.k_slice = 1
d.input0_bytes = (h - 1) * d.input0_row_bytes + w * cin
d.output_bytes = (h - 1) * d.output_row_bytes + w * cin
d.input0_zero = 7
d.output_zero = 7
else:
raise AssertionError(f"no factory for opcode {opcode}")
validate_descriptor(d, descriptor_region=(MEM_BASE, 4 * DESCRIPTOR_BYTES))
return d
def _words_of(desc: Descriptor) -> list[int]:
return list(struct.unpack(f"<{DESCRIPTOR_BYTES // 4}I", desc.to_bytes()))
def _image_hex(path: Path, words: list[int]) -> None:
path.write_text("\n".join(f"{word & 0xFFFFFFFF:08x}" for word in words) + "\n",
encoding="utf-8")
def _descriptor_case_image(records: list[Descriptor], count: int) -> list[int]:
words: list[int] = []
for record in records:
words.extend(_words_of(record))
while len(words) < count * 32:
words.append(0xDEAD0000 | (len(words) & 0xFFFF))
return words
def _expected_fault(records: list[Descriptor], count: int):
"""(code, word, index) of the first validator failure, or None."""
region = (MEM_BASE, count * DESCRIPTOR_BYTES)
for index, record in enumerate(records[:count]):
wire = Descriptor.from_bytes(record.to_bytes())
try:
validate_descriptor(wire, descriptor_region=region)
except DescriptorError as error:
return error.code, error.word, index
return None
# ---------------------------------------------------------------------------
# Decoder testbench cases
# ---------------------------------------------------------------------------
def _decoder_cases(tmp_path: Path) -> list[list[str]]:
cases: list[list[str]] = []
def emit(name: str, records: list[Descriptor], count: int, mode: int, fcode: int,
findex: int, fword: int, rderr_addr: int = 0xFFFFFFFF,
rderr_code: int = 0, rderr_badid: int = 0) -> None:
image = tmp_path / f"dec_{name}.hex"
_image_hex(image, _descriptor_case_image(records, count))
cases.append(
[
f"+IMG={image}",
f"+BASE={MEM_BASE:x}",
f"+COUNT={count}",
f"+MODE={mode}",
f"+FCODE={fcode}",
f"+FINDEX={findex}",
f"+FWORD={fword & 0xFFFFFFFF}",
f"+RDERR_ADDR={rderr_addr:x}",
f"+RDERR_CODE={rderr_code}",
f"+RDERR_BADID={rderr_badid}",
f"+NAME={name}",
]
)
# valid accept per opcode, plus one multi-record in-order traversal, plus
# the ADD input0/input1 alias legality case, plus pause-during-fetch
for opcode in range(1, 9):
emit(f"valid_op{opcode}", [_valid_descriptor(opcode, 0)], 1, 0, 0, 0, 0xFFFFFFFF)
multi = [
_valid_descriptor(8, 0),
_valid_descriptor(1, 1),
_valid_descriptor(7, 2),
_valid_descriptor(4, 3),
]
assert _expected_fault(multi, 4) is None
emit("valid_multi4", multi, 4, 0, 0, 0, 0xFFFFFFFF)
alias = _valid_descriptor(4, 0)
alias.input1_base = alias.input0_base
validate_descriptor(alias, descriptor_region=(MEM_BASE, 2 * DESCRIPTOR_BYTES))
emit("valid_add_alias", [alias], 1, 0, 0, 0, 0xFFFFFFFF)
emit("pause_fetch", [_valid_descriptor(8, 0), _valid_descriptor(1, 1)], 2, 3, 0, 0,
0xFFFFFFFF)
# validation rejection classes
def mutate(name: str, opcode: int, fn) -> None:
base_record = _valid_descriptor(opcode, 0)
words = _words_of(base_record)
fn(words)
record = Descriptor.from_bytes(struct.pack(f"<{len(words)}I", *words))
expected = _expected_fault([record], 3)
assert expected is not None, f"mutation {name} unexpectedly valid"
code, word, index = expected
assert index == 0
emit(name, [record], 3, 1, code, index, 0xFFFFFFFF if word is None else word)
mutate("bad_version", 8, lambda w: w.__setitem__(0, 0x0200_0000 | 8))
mutate("bad_version_mid", 8, lambda w: w.__setitem__(0, 0x0100_0100 | 8))
mutate("bad_reserved1", 8, lambda w: w.__setitem__(1, 1))
mutate("bad_reserved26", 8, lambda w: w.__setitem__(26, 1))
mutate("bad_reserved31", 8, lambda w: w.__setitem__(31, 0x8000_0000))
mutate("bad_tile_upper", 8, lambda w: w.__setitem__(14, w[14] | 0x0001_0000))
mutate("bad_in0zero_upper", 8, lambda w: w.__setitem__(20, w[20] | 0x100))
mutate("bad_outzero_upper", 8, lambda w: w.__setitem__(22, w[22] | 0x0100_0000))
mutate("bad_bounds_upper", 8, lambda w: w.__setitem__(23, w[23] | 0x0001_0000))
mutate("bad_bounds_order", 8, lambda w: w.__setitem__(23, 0x0000_0305))
mutate("bad_tile_h0", 8, lambda w: w.__setitem__(14, w[14] & 0xFFFF_FF00))
mutate("bad_tile_product", 8, lambda w: w.__setitem__(14, 0x0000_0303))
mutate("bad_kslice0", 8, lambda w: w.__setitem__(15, 0))
mutate("bad_kslice1025", 8, lambda w: w.__setitem__(15, 1025))
mutate("bad_opcode0", 8, lambda w: w.__setitem__(0, w[0] & 0xFFFF_FF00))
mutate("bad_opcode9", 8, lambda w: w.__setitem__(0, (w[0] & 0xFFFF_FF00) | 9))
mutate("unused_input1_base", 8, lambda w: w.__setitem__(3, 0x3001_4000))
mutate("unused_input1_row", 8, lambda w: w.__setitem__(11, 4))
mutate("unused_input1_bytes", 8, lambda w: w.__setitem__(17, 4))
mutate("unused_input1_zero", 8, lambda w: w.__setitem__(21, 1))
mutate("unused_weight_base", 8, lambda w: w.__setitem__(24, 0x3002_8000))
mutate("unused_weight_bytes", 8, lambda w: w.__setitem__(25, 8))
mutate("unused_param_base", 8, lambda w: w.__setitem__(5, 0x3003_0000))
mutate("unused_param_bytes", 8, lambda w: w.__setitem__(19, 8))
mutate("gap_kernel_word", 7, lambda w: w.__setitem__(12, 1))
mutate("gap_padding", 7, lambda w: w.__setitem__(13, 1))
mutate("dim_h0", 8, lambda w: w.__setitem__(6, w[6] & 0xFFFF_0000))
mutate("dim_w0", 8, lambda w: w.__setitem__(6, w[6] & 0x0000_FFFF))
mutate("dim_cin0", 8, lambda w: w.__setitem__(7, w[7] & 0xFFFF_0000))
mutate("dim_cout0", 8, lambda w: w.__setitem__(7, w[7] & 0x0000_FFFF))
mutate("dim_oh0", 8, lambda w: w.__setitem__(8, w[8] & 0xFFFF_0000))
mutate("dim_ow0", 8, lambda w: w.__setitem__(8, w[8] & 0x0000_FFFF))
mutate("dim_h4097", 8, lambda w: w.__setitem__(6, (w[6] & 0xFFFF_0000) | 4097))
mutate("conv_kh0", 1, lambda w: w.__setitem__(12, w[12] & 0xFFFF_FF00))
mutate("conv_pad_ge_kernel", 1, lambda w: w.__setitem__(13, (w[13] & 0xFFFF_FF00) | 3))
mutate("conv_extent_negative", 1,
lambda w: (w.__setitem__(6, (w[6] & 0xFFFF_0000) | 2), w.__setitem__(13, 0)))
mutate("conv_extent_mismatch", 1, lambda w: w.__setitem__(8, w[8] + 1))
mutate("conv_kh17", 1, lambda w: w.__setitem__(12, w[12] | 17))
mutate("conv_stride3", 1, lambda w: w.__setitem__(12, (w[12] & 0xFF00_FFFF) | (3 << 16)))
mutate("dw_kh5", 2, lambda w: w.__setitem__(12, w[12] | 5))
mutate("dw_cout_ne", 2, lambda w: w.__setitem__(7, w[7] + (1 << 16)))
mutate("fc_h2", 3, lambda w: w.__setitem__(6, w[6] | 2))
mutate("fc_oh2", 3, lambda w: w.__setitem__(8, w[8] | 2))
mutate("fc_kernel0", 3, lambda w: w.__setitem__(12, 0))
mutate("fc_padding", 3, lambda w: w.__setitem__(13, 1))
mutate("add_input1_zero_upper", 4, lambda w: w.__setitem__(21, w[21] | 0x100))
mutate("add_input1_base0", 4, lambda w: w.__setitem__(3, 0))
mutate("add_broadcast", 4, lambda w: w.__setitem__(8, w[8] + 1))
mutate("add_cout_ne", 4, lambda w: w.__setitem__(7, w[7] + (1 << 16)))
mutate("add_kernel", 4, lambda w: w.__setitem__(12, 0))
mutate("add_padding", 4, lambda w: w.__setitem__(13, 1))
mutate("pool_kh17", 5, lambda w: w.__setitem__(12, w[12] | 17))
mutate("pool_cout_ne", 5, lambda w: w.__setitem__(7, w[7] + (1 << 16)))
mutate("pool_zero_mismatch", 5, lambda w: w.__setitem__(22, w[22] ^ 1))
mutate("gap_oh2", 7, lambda w: w.__setitem__(8, w[8] | 2))
mutate("gap_cout_ne", 7, lambda w: w.__setitem__(7, w[7] + (1 << 16)))
mutate("gap_zero_mismatch", 7, lambda w: w.__setitem__(22, w[22] ^ 1))
mutate("clamp_oh_ne", 8, lambda w: w.__setitem__(8, w[8] + 1))
mutate("clamp_zero_mismatch", 8, lambda w: w.__setitem__(22, w[22] ^ 1))
mutate("clamp_kernel", 8, lambda w: w.__setitem__(12, 0))
mutate("clamp_padding", 8, lambda w: w.__setitem__(13, 1))
mutate("kslice_gt_full", 1, lambda w: w.__setitem__(15, 19))
mutate("tile_gt_oh", 8, lambda w: w.__setitem__(14, (w[14] & 0xFFFF_FF00) | 4))
mutate("weight_base0", 1, lambda w: w.__setitem__(24, 0))
mutate("weight_bytes_bad", 1, lambda w: w.__setitem__(25, w[25] + 8))
mutate("param_base0", 1, lambda w: w.__setitem__(5, 0))
mutate("param_bytes_bad", 1, lambda w: w.__setitem__(19, w[19] + 16))
mutate("add_param_bytes_bad", 4, lambda w: w.__setitem__(19, 16))
clamp_min_in = 5 * 4 # W*Cin of the CLAMP factory
clamp_min_out = 5 * 4 # OW*Cout of the CLAMP factory
add_min_in = 3 * 3 # W*Cin of the ADD factory
mutate("stride_in0_low", 8, lambda w: w.__setitem__(9, clamp_min_in - 1))
mutate("stride_out_low", 8, lambda w: w.__setitem__(10, clamp_min_out - 1))
mutate("add_stride1_low", 4, lambda w: w.__setitem__(11, add_min_in - 1))
mutate("span_in0", 8, lambda w: w.__setitem__(16, w[16] - 1))
mutate("span_out", 8, lambda w: w.__setitem__(18, w[18] - 1))
mutate("wrap_in0_alloc", 8,
lambda w: (w.__setitem__(2, 0xFFFF_F000), w.__setitem__(16, 0x2000)))
mutate("wrap_out", 8,
lambda w: (w.__setitem__(4, 0xFFFF_FFC0), w.__setitem__(18, 96)))
mutate("wrap_weight", 1, lambda w: w.__setitem__(24, 0xFFFF_FF80))
mutate("wrap_param", 1, lambda w: w.__setitem__(5, 0xFFFF_FFC0))
mutate("align_param", 1, lambda w: w.__setitem__(5, w[5] | 4))
mutate("align_weight", 1, lambda w: w.__setitem__(24, w[24] | 4))
mutate("overlap_in0_out", 8, lambda w: w.__setitem__(4, w[2]))
mutate("overlap_out_desc", 8, lambda w: w.__setitem__(4, MEM_BASE))
mutate("overlap_weight_param", 1, lambda w: w.__setitem__(5, w[24]))
# first-fault order: earlier validator stage wins
mutate("priority_struct", 8,
lambda w: (w.__setitem__(0, 0x0300_0000 | 8), w.__setitem__(1, 7)))
mutate("priority_unused_over_geometry", 8,
lambda w: (w.__setitem__(3, 0x3001_4000), w.__setitem__(6, 0)))
# validation fault after a valid prefix record
prefix = _valid_descriptor(8, 0)
faulty = _valid_descriptor(1, 1)
words = _words_of(faulty)
words[15] = 19 # K_SLICE above full K
faulty = Descriptor.from_bytes(struct.pack(f"<{len(words)}I", *words))
expected = _expected_fault([prefix, faulty], 4)
assert expected == (F_UNSUPPORTED, 15, 1)
emit("fault_at_index1", [prefix, faulty], 4, 1, expected[0], 1, expected[1])
# fetch bus faults
emit("fetch_slverr", [_valid_descriptor(8, 0)], 1, 2, F_AXI_READ, 0, 0xFFFFFFFF,
rderr_addr=MEM_BASE + 24, rderr_code=2)
emit("fetch_decerr_index1", [_valid_descriptor(8, 0), _valid_descriptor(1, 1)], 2, 2,
F_AXI_READ, 1, 0xFFFFFFFF, rderr_addr=MEM_BASE + 128, rderr_code=3)
emit("fetch_bad_rid", [_valid_descriptor(8, 0)], 1, 2, F_AXI_PROTOCOL, 0, 0xFFFFFFFF,
rderr_addr=MEM_BASE, rderr_badid=1)
# truncation: the third record does not exist in memory
emit("fetch_truncated", [_valid_descriptor(8, 0), _valid_descriptor(1, 1)], 3, 2,
F_AXI_READ, 2, 0xFFFFFFFF, rderr_addr=MEM_BASE + 256, rderr_code=3)
return cases
@pytest.mark.parametrize("simulator", ("iverilog", "verilator"))
def test_npu_dma(tmp_path: Path, simulator: str) -> None:
tools = _tools()
_run_p3_test(
tools,
simulator,
tmp_path,
f"npu_dma_{simulator}",
"npu_dma_tb",
ROOT / "tests/rtl/npu_dma_tb.sv",
DMA_SOURCES,
"NPU DMA test passed",
)
@pytest.mark.parametrize("simulator", ("iverilog", "verilator"))
def test_npu_job_decoder(tmp_path: Path, simulator: str) -> None:
tools = _tools()
command = _build_sim(
tools,
simulator,
tmp_path,
f"npu_job_decoder_{simulator}",
"npu_job_decoder_tb",
ROOT / "tests/rtl/npu_job_decoder_tb.sv",
DECODER_SOURCES,
)
cases = _decoder_cases(tmp_path)
assert len(cases) >= 70
for plusargs in cases:
_run(command, plusargs, "NPU job decoder test passed")
# ---------------------------------------------------------------------------
# Core testbench scenarios
# ---------------------------------------------------------------------------
def _pat(address: int) -> int:
"""Deterministic input/weight/param content byte."""
return (address ^ (address >> 7) ^ 0x36) & 0xFF
def _fill_descriptor_content(mem: bytearray, desc: Descriptor) -> None:
for offset in range(desc.input0_bytes):
mem[desc.input0_base - MEM_BASE + offset] = _pat(desc.input0_base + offset)
if desc.input1_base:
for offset in range(desc.input1_bytes):
mem[desc.input1_base - MEM_BASE + offset] = _pat(desc.input1_base + 3 + offset)
if desc.weight_bytes:
for offset in range(desc.weight_bytes):
mem[desc.weight_base - MEM_BASE + offset] = _pat(desc.weight_base + 1 + offset)
if desc.param_bytes:
for offset in range(desc.param_bytes):
mem[desc.param_base - MEM_BASE + offset] = _pat(desc.param_base + 2 + offset)
def _gather_byte(desc: Descriptor, mem: bytearray, oy: int, ox: int, g: int) -> int:
"""Gather byte at reduction index g of output position (oy, ox): the P3
transport-content rule. Reduction order kh, kw, cin; GAP rasters H*W."""
cin = desc.cin
kp, ci = divmod(g, cin)
if desc.opcode == 7: # GLOBAL_AVERAGE_POOL: whole-input raster
iy, ix = divmod(kp, desc.w)
else:
kh, kw = divmod(kp, desc.kw)
iy = oy * desc.sh + kh - desc.pad_top
ix = ox * desc.sw + kw - desc.pad_left
if 0 <= iy < desc.h and 0 <= ix < desc.w:
return mem[desc.input0_base - MEM_BASE + iy * desc.input0_row_bytes + ix * cin + ci]
return desc.input0_zero & 0xFF
def _golden_window(descs: list[Descriptor], mem: bytearray) -> tuple[int, bytes]:
"""Byte-exact output window content after a full run of the job."""
lo = min(d.output_base for d in descs)
hi = max(d.output_base + (d.oh - 1) * d.output_row_bytes + d.ow * d.cout for d in descs)
window = bytearray(mem[lo - MEM_BASE : hi - MEM_BASE])
for desc in descs:
if desc.opcode == 7:
kp_count = desc.h * desc.w
elif desc.opcode in (1, 2, 5, 6):
kp_count = desc.kh * desc.kw
else:
kp_count = 1
gather_len = kp_count * desc.cin
for oy in range(desc.oh):
for ox in range(desc.ow):
for channel in range(desc.cout):
g = channel % gather_len
byte = _gather_byte(desc, mem, oy, ox, g)
window[desc.output_base - lo +
oy * desc.output_row_bytes + ox * desc.cout + channel] = byte
return lo, bytes(window)
def _core_case(tmp_path: Path, name: str, descs: list[Descriptor], scen: int,
timeout: int = 20_000_000, rderr_addr: int = 0xFFFFFFFF, rderr_code: int = 0,
werr_addr: int = 0xFFFFFFFF, werr_code: int = 0, exp_faddr: int = 0,
wchk: bool = False) -> list[str]:
for desc in descs:
validate_descriptor(desc, descriptor_region=(MEM_BASE, len(descs) * DESCRIPTOR_BYTES))
mem = bytearray([GAP_FILL] * CORE_MEM_BYTES)
for index, desc in enumerate(descs):
words = _words_of(desc)
struct.pack_into(f"<{len(words)}I", mem, MEM_BASE - MEM_BASE + index * 128, *words)
_fill_descriptor_content(mem, desc)
image = tmp_path / f"core_{name}.hex"
_image_hex(image, list(struct.unpack(f"<{CORE_MEM_BYTES // 4}I", bytes(mem))))
gold_base, gold = _golden_window(descs, mem)
golden = tmp_path / f"core_{name}_gold.hex"
pad = (-len(gold)) % 4
_image_hex(golden, list(struct.unpack(f"<{(len(gold) + pad) // 4}I", bytes(gold) +
bytes(pad))))
first = descs[0]
return [
f"+IMG={image}",
f"+GOLD={golden}",
f"+BASE={MEM_BASE:x}",
f"+COUNT={len(descs)}",
"+JOBID=1a2b3c4d",
f"+TIMEOUT={timeout}",
f"+SCEN={scen}",
f"+GOLD_BASE={gold_base:x}",
f"+GOLD_BYTES={len(gold)}",
f"+RDERR_ADDR={rderr_addr:x}",
f"+RDERR_CODE={rderr_code}",
f"+WERR_ADDR={werr_addr:x}",
f"+WERR_CODE={werr_code}",
f"+EXP_FADDR={exp_faddr:x}",
f"+WCHK={1 if wchk else 0}",
f"+WCHK_BASE={first.weight_base:x}",
f"+WCHK_BYTES={first.weight_bytes}",
f"+NAME={name}",
]
def _conv_kws_shape(slot: int) -> Descriptor:
"""KWS conv1 shape: 10x4 kernel, stride 2, Cin=1, reduced geometry."""
b = _slot_bases(slot)
d = Descriptor()
d.version_opcode = 0x0100_0000 | 1
d.act_min = -128
d.act_max = 127
h, w, cin, cout = 13, 8, 1, 8
kh, kw, sh, sw = 10, 4, 2, 2
oh = (h - kh) // sh + 1
ow = (w - kw) // sw + 1
d.input_hw = (w << 16) | h
d.channels = (cout << 16) | cin
d.output_hw = (ow << 16) | oh
d.input0_base = b["in0"]
d.output_base = b["out"]
d.input0_row_bytes = w * cin
d.output_row_bytes = ow * cout
d.kernel_stride = (sw << 24) | (sh << 16) | (kw << 8) | kh
d.padding = 0
d.tile_hw = (2 << 8) | 2
d.k_slice = 40
d.input0_bytes = (h - 1) * d.input0_row_bytes + w * cin
d.output_bytes = (oh - 1) * d.output_row_bytes + ow * cout
d.weight_base = b["w"]
d.weight_bytes = ((cout + 7) // 8) * kh * kw * cin * 8
d.param_base = b["p"]
d.param_bytes = cout * 16
validate_descriptor(d, descriptor_region=(MEM_BASE, 4 * DESCRIPTOR_BYTES))
return d
def _clamp_strided(slot: int) -> Descriptor:
d = _valid_descriptor(8, slot)
d.input0_row_bytes = d.w * d.cin + 7
d.output_row_bytes = d.ow * d.cout + 11
d.input0_bytes = (d.h - 1) * d.input0_row_bytes + d.w * d.cin
d.output_bytes = (d.oh - 1) * d.output_row_bytes + d.ow * d.cout
validate_descriptor(d, descriptor_region=(MEM_BASE, 4 * DESCRIPTOR_BYTES))
return d
def _core_cases(tmp_path: Path) -> list[list[str]]:
cases: list[list[str]] = []
# directed transport jobs, one invocation per shape
cases.append(_core_case(tmp_path, "clamp_small", [_valid_descriptor(8, 0)], 0))
cases.append(_core_case(tmp_path, "clamp_strided", [_clamp_strided(0)], 0))
cases.append(_core_case(tmp_path, "conv_kws_shape", [_conv_kws_shape(0)], 0, wchk=True))
cases.append(_core_case(tmp_path, "dw33", [_valid_descriptor(2, 0)], 0))
cases.append(_core_case(tmp_path, "maxpool", [_valid_descriptor(5, 0)], 0))
cases.append(_core_case(tmp_path, "avgpool", [_valid_descriptor(6, 0)], 0))
cases.append(_core_case(tmp_path, "gap", [_valid_descriptor(7, 0)], 0))
cases.append(_core_case(tmp_path, "fc", [_valid_descriptor(3, 0)], 0))
cases.append(_core_case(tmp_path, "add", [_valid_descriptor(4, 0)], 0))
cases.append(_core_case(
tmp_path, "multi3",
[_valid_descriptor(8, 0), _conv_kws_shape(1), _clamp_strided(2)], 0))
# lifecycle scenarios use a two-descriptor job
pair = [_valid_descriptor(8, 0), _clamp_strided(1)]
cases.append(_core_case(tmp_path, "b_order", pair, 1))
cases.append(_core_case(tmp_path, "watchdog", [_valid_descriptor(8, 0)], 2,
timeout=2000))
cases.append(_core_case(tmp_path, "abort", pair, 3))
cases.append(_core_case(tmp_path, "quiesce", pair, 4))
cases.append(_core_case(tmp_path, "reset", pair, 5))
cases.append(_core_case(tmp_path, "flush", pair, 6))
cases.append(_core_case(
tmp_path, "snapshot_race",
[_valid_descriptor(8, 0), _conv_kws_shape(1), _clamp_strided(2)], 7))
# write fault: error on the first store burst of descriptor 0
fault_job = _valid_descriptor(8, 0)
burst_base = fault_job.output_base & ~7
span_beats = ((fault_job.output_base & 7) + fault_job.cout + 7) >> 3
beats_4k = (4096 - (burst_base & 0xFFF)) >> 3
beats = min(16, span_beats, beats_4k)
cases.append(_core_case(tmp_path, "write_fault", [fault_job], 8,
werr_addr=burst_base, werr_code=2,
exp_faddr=burst_base + (beats - 1) * 8))
# fetch fault: SLVERR on descriptor 0's first beat
cases.append(_core_case(tmp_path, "read_fault", [_valid_descriptor(8, 0)], 9,
rderr_addr=MEM_BASE, rderr_code=2, exp_faddr=MEM_BASE))
# administrative pause mid-job with a short job watchdog timeout: the
# pause must be excluded from the watchdog and the job must resume whole
cases.append(_core_case(tmp_path, "pause", [_conv_kws_shape(0)], 10, timeout=2000))
return cases
@pytest.mark.parametrize("simulator", ("iverilog", "verilator"))
def test_npu_core(tmp_path: Path, simulator: str) -> None:
tools = _tools()
command = _build_sim(
tools,
simulator,
tmp_path,
f"npu_core_{simulator}",
"npu_core_tb",
ROOT / "tests/rtl/npu_core_tb.sv",
CORE_SOURCES,
)
for plusargs in _core_cases(tmp_path):
_run(command, plusargs, "NPU core test passed")
+7
View File
@@ -27,6 +27,13 @@ COMMON_SOURCES = [
COMMON / "cdc/async_reqack.sv",
]
SHELL_SOURCES = [
MULTIMEDIA / "npu_pkg.sv",
ROOT / "rtl/tech/tc_sram.sv",
MULTIMEDIA / "npu_local_sram.sv",
MULTIMEDIA / "npu_patch_packer.sv",
MULTIMEDIA / "npu_dma.sv",
MULTIMEDIA / "npu_job_decoder.sv",
MULTIMEDIA / "npu_scheduler.sv",
MULTIMEDIA / "npu_reg.sv",
MULTIMEDIA / "npu_control_cdc.sv",
MULTIMEDIA / "npu_core.sv",
+52
View File
@@ -335,6 +335,7 @@ def test_formal_filelists_are_scoped_to_the_protocol_duts(tmp_path: Path) -> Non
apu_loader_filelist = tmp_path / "apu_loader.fl"
apu_sequencer_filelist = tmp_path / "apu_sequencer.fl"
gateway_a_filelist = tmp_path / "gateway_a.fl"
npu_dma_filelist = tmp_path / "npu_dma.fl"
assert generate_formal_filelist("bus", bus_filelist, memory_map, topology, user_extensions)
assert generate_formal_filelist(
"rib_adapter", rib_adapter_filelist, memory_map, topology, user_extensions
@@ -374,6 +375,9 @@ def test_formal_filelists_are_scoped_to_the_protocol_duts(tmp_path: Path) -> Non
assert generate_formal_filelist(
"gateway_a", gateway_a_filelist, memory_map, topology, user_extensions
)
assert generate_formal_filelist(
"npu_dma", npu_dma_filelist, memory_map, topology, user_extensions
)
bus = parse_filelists([bus_filelist], require_files=False)
rib_adapter = parse_filelists([rib_adapter_filelist], require_files=False)
@@ -392,6 +396,7 @@ def test_formal_filelists_are_scoped_to_the_protocol_duts(tmp_path: Path) -> Non
apu_loader = parse_filelists([apu_loader_filelist], require_files=False)
apu_sequencer = parse_filelists([apu_sequencer_filelist], require_files=False)
gateway_a = parse_filelists([gateway_a_filelist], require_files=False)
npu_dma = parse_filelists([npu_dma_filelist], require_files=False)
assert "+define+SV_ASSRT_DISABLE" in bus.defines
assert "+define+PDK_BEHAV" in onchip_ram.defines
assert "+define+SYNTHESIS" in onchip_ram.defines
@@ -483,6 +488,10 @@ def test_formal_filelists_are_scoped_to_the_protocol_duts(tmp_path: Path) -> Non
ROOT / "rtl/managed/clusterip/common/rtl/stream/round_robin_arbiter.sv" in gateway_a.files
)
assert ROOT / "rtl/mini/formal/gateway_a_formal.sv" in gateway_a.files
assert "+define+SV_ASSRT_DISABLE" in npu_dma.defines
assert ROOT / "rtl/ip/multimedia/npu_dma.sv" in npu_dma.files
assert ROOT / "rtl/managed/clusterip/common/rtl/interface/axi4_if.sv" in npu_dma.files
assert ROOT / "rtl/mini/formal/npu_dma_formal.sv" in npu_dma.files
def test_opipsram_formal_keeps_full_depth_with_hosted_runner_budget() -> None:
@@ -548,6 +557,49 @@ def test_ga2d_formal_target_uses_the_p5_production_hierarchy() -> None:
assert "FormatA8" in properties
def test_npu_dma_formal_target_pins_the_bounded_axi4_contract() -> None:
formal_makefile = (ROOT / "rtl/mini/mk/formal.mk").read_text(encoding="utf-8")
filelist_generator = (
ROOT / "rtl/mini/formal/generate_formal_filelist.py"
).read_text(encoding="utf-8")
design = (ROOT / "rtl/mini/formal/npu_dma_formal.sv").read_text(encoding="utf-8")
properties = (ROOT / "rtl/mini/formal/npu_dma_formal_props.sv").read_text(
encoding="utf-8"
)
assert re.search(r"^FORMAL_NPU_DMA_DEPTH\s+\?= 16$", formal_makefile, re.MULTILINE)
assert re.search(r"^FORMAL_NPU_DMA_COVER_DEPTH\s+\?= 32$", formal_makefile, re.MULTILINE)
assert re.search(r"^FORMAL_NPU_DMA_TIMEOUT\s+\?= 7200$", formal_makefile, re.MULTILINE)
assert re.search(r"^FORMAL_TARGETS\s+:=.* npu_dma$", formal_makefile, re.MULTILINE)
assert "formal-npu_dma: $(FORMAL_DIR)/npu_dma/.stamp | manifest" in formal_makefile
assert 'if target == "npu_dma":' in filelist_generator
for source in ("npu_dma.sv", "npu_dma_formal.sv"):
assert source in filelist_generator
assert "npu_dma u_dut" in design
assert "(* anyconst *)logic [ 1:0] f_read_fault;" in design
assert "(* anyconst *)logic [ 3:0] f_block_start;" in design
assert "(* anyseq *)logic f_write_last;" in design
assert "s_r_proto_rlast_q" in design
assert "s_w_bid_bad_q" in design
assert "npu_dma_formal_design u_design" in properties
assert "assert (arlen <= 8'd15);" in properties
assert "assert (awlen <= 8'd15);" in properties
assert "assert (awsize == 3'd3);" in properties
assert "assert ((arsize == 3'd3) || (arlen == 8'd0));" in properties
assert "assert (araddr == f_rd_next_burst_q);" in properties
assert "assert ({4'd0, wfifo_count} == ({1'b0, awlen} + 9'd1));" in properties
assert "assert (!f_rd_outstanding_q);" in properties
assert "assert (f_wr_aw_q);" in properties
assert "assert (fault_code == 4'd6);" in properties
assert "assert (fault_code == 4'd4);" in properties
assert "assert (fault_code == 4'd5);" in properties
assert "assume (read_bytes <= 32'd128);" in properties
assert "cover (pause_ack && arvalid && !arready);" in properties
assert "cover (clear && f_rd_outstanding_q);" in properties
assert "cover (fault && (fault_code == 4'd6));" in properties
assert "cover (write_done);" in properties
def test_sysctrl_formal_properties_use_exported_user_core_shape() -> None:
design = (ROOT / "rtl/mini/formal/sysctrl_formal.sv").read_text(encoding="utf-8")
properties = (ROOT / "rtl/mini/formal/sysctrl_formal_props.sv").read_text(encoding="utf-8")