[otbn] Add proper error tracking to otbn_core_model
This turned out to be a bit painful: if you want to update more than
one SystemVerilog variable, you need to put the logic in a task, but
then verilator notices that you're being called from an always_ff
block and tells you to use non-blocking assignments (not
unreasonably). It also complains if you mix blocking and non-blocking
assignments. If you still need to inspect the return code from DPI
functions, you might try to change this pattern:
retcode = call_dpi(arg1, arg2);
unique case (retcode)
...
endcase
to this one:
unique case (call_dpi(arg1, arg2))
...
endcase
but that doesn't work because call_dpi(..) gets evaluated (with
side-effects) for each branch of the case statement until a match is
found.
So I gave up, packing the status bits that I cared about into an
integer and just doing the stateful updates in the C++ code.
Signed-off-by: Rupert Swarbrick <rswarbrick@lowrisc.org>
diff --git a/hw/ip/otbn/dv/model/otbn_core_model.sv b/hw/ip/otbn/dv/model/otbn_core_model.sv
index 3f36fa7..a62a829 100644
--- a/hw/ip/otbn/dv/model/otbn_core_model.sv
+++ b/hw/ip/otbn/dv/model/otbn_core_model.sv
@@ -35,30 +35,32 @@
input logic start_i, // start the operation
output logic done_o, // operation done
- input logic [ImemAddrWidth-1:0] start_addr_i // start byte address in IMEM
-);
+ input logic [ImemAddrWidth-1:0] start_addr_i, // start byte address in IMEM
+ output bit err_o // something went wrong
+);
import "DPI-C" function chandle otbn_model_init();
import "DPI-C" function void otbn_model_destroy(chandle handle);
import "DPI-C" context function
- int otbn_model_start(chandle model,
- string imem_scope,
- int imem_size,
- string dmem_scope,
- int dmem_size,
- int start_addr);
- import "DPI-C" function int otbn_model_step(chandle model);
- import "DPI-C" context function
- int otbn_model_load_dmem(chandle model,
- string dmem_scope,
- int dmem_size);
- import "DPI-C" context function
- int otbn_model_check(chandle model,
- string dmem_scope,
- int dmem_size,
- string design_scope);
+ int unsigned otbn_model_step(chandle model,
+ string imem_scope,
+ int unsigned imem_size,
+ string dmem_scope,
+ int unsigned dmem_size,
+ string design_scope,
+ logic start_i,
+ int unsigned start_addr,
+ int unsigned status);
+ localparam ImemSizeWords = ImemSizeByte / 4;
+ localparam DmemSizeWords = DmemSizeByte / (WLEN / 8);
+
+ `ASSERT_INIT(StartAddr32_A, ImemAddrWidth <= 32);
+ logic [31:0] start_addr_32;
+ assign start_addr_32 = {{32 - ImemAddrWidth{1'b0}}, start_addr_i};
+
+ // Create and destroy an object through which we can talk to the ISS
chandle model_handle;
initial begin
model_handle = otbn_model_init();
@@ -69,73 +71,47 @@
model_handle = 0;
end
+ // A packed "status" value. This gets assigned by DPI function calls that need to update both
+ // whether we're running and also error flags at the same time. The contents are magic simulation
+ // values, so get initialized before reset (to avoid stopping the simulation before it even
+ // starts).
+ int unsigned status = 0;
- localparam ImemSizeWords = ImemSizeByte / 4;
- localparam DmemSizeWords = DmemSizeByte / (WLEN / 8);
+ // Extract particular bits of the status value.
+ //
+ // running: The ISS is currently running
+ // failed_step: Something went wrong when trying to start or step the ISS.
+ // failed_cmp: The consistency check at the end of run failed.
+ bit failed_cmp, failed_step, running;
+ assign {failed_cmp, failed_step, running} = status[2:0];
- `ASSERT_INIT(StartAddr32_A, ImemAddrWidth <= 32);
- logic [31:0] start_addr_32;
- assign start_addr_32 = {{32 - ImemAddrWidth{1'b0}}, start_addr_i};
-
- // The control loop for the model. We track whether we're currently running in the running
- // variable. The step_iss function is run every cycle when not in reset. It steps the ISS if
- // necessary and returns the new value for running.
- function automatic bit step_iss(bit running);
- int retcode;
- bit new_run = running;
-
- // If start_i is asserted, start again (regardless of whether we're currently running).
- if (start_i) begin
- retcode = otbn_model_start(model_handle,
- ImemScope, ImemSizeWords,
- DmemScope, DmemSizeWords,
- start_addr_32);
- unique case (retcode)
- 0: new_run = 1'b1;
- // Something went wrong. Assume we didn't manage to start.
- default: new_run = 1'b0;
- endcase
- end
-
- // Otherwise, if we aren't currently running then there's nothing more to do.
- if (!new_run) begin
- return 1'b0;
- end
-
- // We are running. Step by one instruction.
- retcode = otbn_model_step(model_handle);
- unique case (retcode)
- 0: new_run = 1'b1;
- 1: begin
- // The model has just finished running. If this is a standalone model (which we can tell
- // because DesignScope is empty), write the ISS's DMEM contents back to the simulation.
- // Otherwise, check the ISS and simulation agree (TODO: We don't do error handling properly
- // at the moment; for now, the C++ code just prints error messages to stderr).
- if (DesignScope.len() == 0) begin
- void'(otbn_model_load_dmem(model_handle, DmemScope, DmemSizeWords));
- end else begin
- void'(otbn_model_check(model_handle, DmemScope, DmemSizeWords, DesignScope));
- end
- new_run = 1'b0;
- end
- // Something went wrong. Assume we've stopped.
- default: new_run = 1'b0;
- endcase
- return new_run;
- endfunction
-
- bit running, running_r;
always_ff @(posedge clk_i or negedge rst_ni) begin
if (!rst_ni) begin
- running <= 1'b0;
- running_r <= 1'b0;
+ // Clear status (stop running, and forget any errors)
+ status <= 0;
end else begin
- running <= step_iss(running);
- running_r <= running;
+ if (start_i | running) begin
+ status <= otbn_model_step(model_handle,
+ ImemScope, ImemSizeWords,
+ DmemScope, DmemSizeWords,
+ DesignScope,
+ start_i, start_addr_32,
+ status);
+ end else begin
+ // If we're not running and we're not being told to start, there's nothing to do.
+ end
end
end
- // Track negedges of running and expose that as a "done" output.
+ // Track negedges of running_q and expose that as a "done" output.
+ bit running_r = 1'b0;
+ always_ff @(posedge clk_i or negedge rst_ni) begin
+ if (!rst_ni) begin
+ running_r <= 1'b0;
+ end else begin
+ running_r <= running;
+ end
+ end
assign done_o = running_r & ~running;
// If DesignScope is not empty, we have a design to check. Bind a copy of otbn_rf_snooper into
@@ -148,4 +124,6 @@
bind otbn_rf_bignum_ff otbn_rf_snooper #(.Width (256), .Depth (32)) u_snooper (.rf (rf));
end endgenerate
+ assign err_o = failed_step | failed_cmp;
+
endmodule
diff --git a/hw/ip/otbn/dv/model/otbn_model.cc b/hw/ip/otbn/dv/model/otbn_model.cc
index 06275c9..371e244 100644
--- a/hw/ip/otbn/dv/model/otbn_model.cc
+++ b/hw/ip/otbn/dv/model/otbn_model.cc
@@ -19,6 +19,44 @@
extern "C" int simutil_verilator_set_mem(int index, const svBitVecVal *val);
extern "C" int otbn_rf_peek(int index, svBitVecVal *val);
+#define RUNNING_BIT (1U << 0)
+#define FAILED_STEP_BIT (1U << 1)
+#define FAILED_CMP_BIT (1U << 2)
+
+// The main entry point to the OTBN model, exported from here and used in
+// otbn_core_model.sv.
+//
+// This communicates state with otbn_core_model.sv through the status
+// parameter, which has the following bits:
+//
+// Bit 0: running True if the model is currently running
+// Bit 1: failed_step Something failed when trying to start/step ISS
+// Bit 2: failed_cmp Consistency check at end of run failed
+//
+// The otbn_model_step function should only be called when either the model is
+// running (bit 0 of status) or when start_i is asserted. At other times, it
+// will return immediately (but wastes a DPI call).
+//
+// If the model is running and start_i is false, otbn_model_step steps the ISS
+// by a single cycle. If something goes wrong, it will set failed_step to true
+// and running to false.
+//
+// If nothing goes wrong, but the ISS finishes its run, we still set running to
+// false, but also do the post-run task. If design_scope is non-empty, it
+// should be the scope of an RTL implementation. In that case, we compare
+// register and memory contents with that implementation, printing to stderr
+// and setting the failed_cmp bit if there are any mismatches. If design_scope
+// is the empty string, we grab the contents of DMEM from the ISS and inject
+// them into the memory at dmem_scope.
+//
+// If start_i is true, we start the model at start_addr and then step once (as
+// described above).
+extern "C" unsigned otbn_model_step(ISSWrapper *model, const char *imem_scope,
+ unsigned imem_words, const char *dmem_scope,
+ unsigned dmem_words,
+ const char *design_scope, svLogic start_i,
+ unsigned start_addr, unsigned status);
+
// Use simutil_verilator_get_mem to read data one word at a time from the given
// scope and collect the results up in a vector of uint8_t values.
static std::vector<uint8_t> get_sim_memory(const char *scope, size_t num_words,
@@ -149,13 +187,13 @@
// Start a new run with the model, writing IMEM/DMEM and jumping to the given
// start address. Returns 0 on success; -1 on failure.
-extern "C" int otbn_model_start(ISSWrapper *model, const char *imem_scope,
- int imem_words, const char *dmem_scope,
- int dmem_words, int start_addr) {
+static int start_model(ISSWrapper *model, const char *imem_scope,
+ unsigned imem_words, const char *dmem_scope,
+ unsigned dmem_words, unsigned start_addr) {
assert(model);
assert(imem_words >= 0);
assert(dmem_words >= 0);
- assert(start_addr >= 0 && start_addr < (imem_words * 4));
+ assert(start_addr < (imem_words * 4));
std::string ifname(model->make_tmp_path("imem"));
std::string dfname(model->make_tmp_path("dmem"));
@@ -182,7 +220,7 @@
// Step once in the model. Returns 1 if the model has finished, 0 if not and -1
// on failure.
-extern "C" int otbn_model_step(ISSWrapper *model) {
+static int step_model(ISSWrapper *model) {
assert(model);
try {
return model->step() ? 1 : 0;
@@ -194,8 +232,8 @@
// Grab contents of dmem from the model and load it back into the RTL. Returns
// 0 on success; -1 on failure.
-extern "C" int otbn_model_load_dmem(ISSWrapper *model, const char *dmem_scope,
- int dmem_words) {
+static int load_dmem(ISSWrapper *model, const char *dmem_scope,
+ unsigned dmem_words) {
assert(model);
std::string dfname(model->make_tmp_path("dmem_out"));
try {
@@ -211,10 +249,9 @@
// Grab contents of dmem from the model and compare it with the RTL.
// Prints messages to stderr on failure or mismatch. Returns true on
// success; false on mismatch. Throws a std::runtime_error on failure.
-static bool otbn_model_check_dmem(ISSWrapper *model, const char *dmem_scope,
- int dmem_words) {
+static bool check_dmem(ISSWrapper *model, const char *dmem_scope,
+ unsigned dmem_words) {
assert(model);
- assert(dmem_words >= 0);
size_t dmem_bytes = dmem_words * 32;
std::string dfname(model->make_tmp_path("dmem_out"));
@@ -281,8 +318,7 @@
// Compare contents of ISS registers with those from the design. Prints
// messages to stderr on failure or mismatch. Returns true on success; false on
// mismatch. Throws a std::runtime_error on failure.
-static bool otbn_model_check_regs(ISSWrapper *model,
- const std::string &design_scope) {
+static bool check_regs(ISSWrapper *model, const std::string &design_scope) {
assert(model);
std::string base_scope =
@@ -337,25 +373,25 @@
return good;
}
-// Check model against RTL when a run has finished. Prints messages to
-// stderr on failure or mismatch. Returns 1 for a match, 0 for a
-// mismatch, -1 for some other failure.
-extern "C" int otbn_model_check(ISSWrapper *model, const char *dmem_scope,
- int dmem_words, const char *design_scope) {
+// Check model against RTL when a run has finished. Prints messages to stderr
+// on failure or mismatch. Returns 1 for a match, 0 for a mismatch, -1 for some
+// other failure.
+int check_model(ISSWrapper *model, const char *dmem_scope, unsigned dmem_words,
+ const char *design_scope) {
assert(model);
assert(dmem_words >= 0);
assert(design_scope);
bool good = true;
try {
- good &= otbn_model_check_dmem(model, dmem_scope, dmem_words);
+ good &= check_dmem(model, dmem_scope, dmem_words);
} catch (const std::exception &err) {
std::cerr << "Failed to check DMEM: " << err.what() << "\n";
return -1;
}
try {
- good &= otbn_model_check_regs(model, design_scope);
+ good &= check_regs(model, design_scope);
} catch (const std::exception &err) {
std::cerr << "Failed to check registers: " << err.what() << "\n";
return -1;
@@ -363,3 +399,80 @@
return good ? 1 : 0;
}
+
+extern "C" unsigned otbn_model_step(ISSWrapper *model, const char *imem_scope,
+ unsigned imem_words, const char *dmem_scope,
+ unsigned dmem_words,
+ const char *design_scope, svLogic start_i,
+ unsigned start_addr, unsigned status) {
+ assert(model && imem_scope && dmem_scope && design_scope);
+
+ // Start the model if requested
+ if (start_i) {
+ switch (start_model(model, imem_scope, imem_words, dmem_scope, dmem_words,
+ start_addr)) {
+ case 0:
+ // All good
+ status |= RUNNING_BIT;
+ break;
+
+ default:
+ // Something went wrong.
+ return (status & ~RUNNING_BIT) | FAILED_STEP_BIT;
+ }
+ }
+
+ // If the model isn't running, there's nothing more to do.
+ if (!(status & RUNNING_BIT))
+ return status;
+
+ // Step the model once
+ switch (step_model(model)) {
+ case 0:
+ // Still running: no change
+ break;
+
+ case 1:
+ // Finished
+ status = status & ~RUNNING_BIT;
+ break;
+
+ default:
+ // Something went wrong
+ return (status & ~RUNNING_BIT) | FAILED_STEP_BIT;
+ }
+
+ // If we're still running, there's nothing more to do.
+ if (status & RUNNING_BIT)
+ return status;
+
+ // If we've stopped running, either load DMEM or check against the RTL.
+ if (design_scope[0] == '\0') {
+ switch (load_dmem(model, dmem_scope, dmem_words)) {
+ case 0:
+ // Success
+ break;
+
+ default:
+ // Failed to load DMEM
+ return (status & ~RUNNING_BIT) | FAILED_STEP_BIT;
+ }
+ } else {
+ switch (check_model(model, dmem_scope, dmem_words, design_scope)) {
+ case 1:
+ // Match (success)
+ break;
+
+ case 0:
+ // Mismatch
+ status |= FAILED_CMP_BIT;
+ break;
+
+ default:
+ // Something went wrong
+ return (status & ~RUNNING_BIT) | FAILED_STEP_BIT;
+ }
+ }
+
+ return status;
+}
diff --git a/hw/ip/otbn/dv/verilator/otbn_top_sim.sv b/hw/ip/otbn/dv/verilator/otbn_top_sim.sv
index 6bab4cc..a5a108e 100644
--- a/hw/ip/otbn/dv/verilator/otbn_top_sim.sv
+++ b/hw/ip/otbn/dv/verilator/otbn_top_sim.sv
@@ -198,7 +198,9 @@
.start_i ( otbn_start ),
.done_o ( otbn_model_done ),
- .start_addr_i ( ImemStartAddr )
+ .start_addr_i ( ImemStartAddr ),
+
+ .err_o ()
);
always_ff @(posedge IO_CLK or negedge IO_RST_N) begin
diff --git a/hw/ip/otbn/rtl/otbn.sv b/hw/ip/otbn/rtl/otbn.sv
index fc49a67..133f561 100644
--- a/hw/ip/otbn/rtl/otbn.sv
+++ b/hw/ip/otbn/rtl/otbn.sv
@@ -441,7 +441,9 @@
.start_i (start),
.done_o (done),
- .start_addr_i (start_addr)
+ .start_addr_i (start_addr),
+
+ .err_o ()
);
end else begin : gen_impl_rtl
otbn_core #(