blob: 01f26e79087e8416c5237bff035f084a355c1f53 [file] [log] [blame]
// Copyright lowRISC contributors.
// Licensed under the Apache License, Version 2.0, see LICENSE for details.
// SPDX-License-Identifier: Apache-2.0
#include <algorithm>
#include <cassert>
#include <cstring>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <string>
#include <svdpi.h>
#include "iss_wrapper.h"
#include "otbn_memutil.h"
#include "otbn_trace_checker.h"
#include "sv_scoped.h"
#include "sv_utils.h"
// Read (the start of) the contents of a file at path as a vector of bytes.
// Expects num_bytes bytes of data. On failure, throws a std::runtime_error.
static std::vector<uint8_t> read_vector_from_file(const std::string &path,
size_t num_bytes);
// Write a vector of bytes to a new file at path. On failure, throws a
// std::runtime_error.
static void write_vector_to_file(const std::string &path,
const std::vector<uint8_t> &data);
namespace {
struct OtbnModel {
public:
OtbnModel(const std::string &mem_scope, const std::string &design_scope,
unsigned imem_size_words, unsigned dmem_size_words)
: mem_util_(mem_scope),
design_scope_(design_scope),
imem_size_words_(imem_size_words),
dmem_size_words_(dmem_size_words) {}
// True if this model is running in a simulation that has an RTL
// implementation too (which needs checking).
bool has_rtl() const { return !design_scope_.empty(); }
// Start a new run with the model, writing IMEM/DMEM and jumping to the given
// start address. Returns 0 on success; -1 on failure.
int start(unsigned start_addr);
// Step once in the model. Returns 1 if the model has finished, 0 if not and
// -1 on failure. If gen_trace is true, pass trace entries to the trace
// checker. If the model has finished, writes otbn.ERR_BITS to *err_bits.
int step(svLogic edn_rnd_data_valid,
svLogicVecVal *edn_rnd_data, /* logic [255:0] */
svLogic edn_urnd_data_valid, svBitVecVal *insn_cnt /* bit [31:0] */,
svBitVecVal *err_bits /* bit [31:0] */,
svBitVecVal *stop_pc /* bit [31:0] */);
// Check model against RTL (if there is any) 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() const;
// Grab contents of dmem from the model and load it back into the RTL
// simulation. This is used when there's no RTL model of the design. Returns
// 0 on success; -1 on failure.
int load_dmem() const;
// Flush any information in the model
void reset();
private:
// Constructs an ISS wrapper if necessary. If something goes wrong, this
// function prints a message and then returns null. If ensure is true, it
// will never return null without printing a message, so error handling at
// the callsite can silently return a failure code.
ISSWrapper *ensure_wrapper() {
if (!iss_) {
try {
iss_.reset(new ISSWrapper());
} catch (const std::runtime_error &err) {
std::cerr << "Error when constructing ISS wrapper: " << err.what()
<< "\n";
return nullptr;
}
}
assert(iss_);
return iss_.get();
}
std::vector<uint8_t> get_sim_memory(bool is_imem) const {
const MemArea &mem_area = mem_util_.GetMemArea(is_imem);
return mem_area.Read(0, mem_area.GetSizeWords());
}
void set_sim_memory(bool is_imem, const std::vector<uint8_t> &data) const {
mem_util_.GetMemArea(is_imem).Write(0, data);
}
// Grab contents of dmem from the model and compare them 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.
bool check_dmem(ISSWrapper &iss) const;
// 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.
bool check_regs(ISSWrapper &iss) const;
// Compare contents of ISS call stack 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.
bool check_call_stack(ISSWrapper &iss) const;
// We want to create the model in an initial block in the SystemVerilog
// simulation, but might not actually want to spawn the ISS. To handle that
// in a non-racy way, the most convenient thing is to spawn the ISS the first
// time it's actually needed. Use ensure_iss() to create as needed.
std::unique_ptr<ISSWrapper> iss_;
OtbnMemUtil mem_util_;
std::string design_scope_;
unsigned imem_size_words_, dmem_size_words_;
};
} // namespace
extern "C" {
// These functions are only implemented if DesignScope != "", i.e. if we're
// running a block-level simulation. Code needs to check at runtime if
// otbn_rf_peek() and otbn_stack_element_peek() are available before calling
// them.
int otbn_rf_peek(int index, svBitVecVal *val) __attribute__((weak));
int otbn_stack_element_peek(int index, svBitVecVal *val) __attribute__((weak));
}
#define RUNNING_BIT (1U << 0)
#define CHECK_DUE_BIT (1U << 1)
#define FAILED_STEP_BIT (1U << 2)
#define FAILED_CMP_BIT (1U << 3)
static std::vector<uint8_t> read_vector_from_file(const std::string &path,
size_t num_bytes) {
std::filebuf fb;
if (!fb.open(path.c_str(), std::ios::in | std::ios::binary)) {
std::ostringstream oss;
oss << "Cannot open the file '" << path << "'.";
throw std::runtime_error(oss.str());
}
std::vector<uint8_t> buf(num_bytes);
std::streamsize chars_in =
fb.sgetn(reinterpret_cast<char *>(&buf[0]), num_bytes);
if (chars_in != num_bytes) {
std::ostringstream oss;
oss << "Cannot read " << num_bytes << " bytes of memory data from " << path
<< " (actually got " << chars_in << ").";
throw std::runtime_error(oss.str());
}
return buf;
}
// Write a vector of bytes to a new file at path
static void write_vector_to_file(const std::string &path,
const std::vector<uint8_t> &data) {
std::filebuf fb;
if (!fb.open(path.c_str(), std::ios::out | std::ios::binary)) {
std::ostringstream oss;
oss << "Cannot open the file '" << path << "'.";
throw std::runtime_error(oss.str());
}
// Write out the data
std::streamsize chars_out =
fb.sputn(reinterpret_cast<const char *>(&data[0]), data.size());
if (chars_out != data.size()) {
std::ostringstream oss;
oss << "Cannot write " << data.size() << " bytes of memory data to " << path
<< "' (actually wrote " << chars_out << ").";
throw std::runtime_error(oss.str());
}
}
// Extract 256-bit RND EDN data from a 4 state logic value. RND data is placed
// into 8 element uint32_t array dst.
static void set_rnd_data(uint32_t dst[8], const svLogicVecVal src[8]) {
for (int i = 0; i < 8; ++i) {
// All bits should be known
assert(src[i].bval == 0);
dst[i] = src[i].aval;
}
}
static bool is_xz(svLogic l) { return l == sv_x || l == sv_z; }
template <typename T>
static std::array<T, 32> get_rtl_regs(const std::string &reg_scope) {
std::array<T, 32> ret;
static_assert(sizeof(T) <= 256 / 8, "Can only copy 256 bits");
SVScoped scoped(reg_scope);
// otbn_rf_peek passes data as a packed array of svBitVecVal words (for a
// "bit [255:0]" argument). Allocate 256 bits (= 32 bytes) as
// 32/sizeof(svBitVecVal) words on the stack.
svBitVecVal buf[256 / 8 / sizeof(svBitVecVal)];
// The implementation of otbn_rf_peek() is only available if DesignScope != ""
// (the function is implemented in SystemVerilog, and imported through DPI).
// We should not reach the code here if that's the case.
assert(otbn_rf_peek);
for (int i = 0; i < 32; ++i) {
if (!otbn_rf_peek(i, buf)) {
std::ostringstream oss;
oss << "Failed to peek into RTL to get value of register " << i
<< " at scope `" << reg_scope << "'.";
throw std::runtime_error(oss.str());
}
memcpy(&ret[i], buf, sizeof(T));
}
return ret;
}
template <typename T>
static std::vector<T> get_stack(const std::string &stack_scope) {
std::vector<T> ret;
static_assert(sizeof(T) <= 256 / 8, "Can only copy 256 bits");
SVScoped scoped(stack_scope);
// otbn_stack_element_peek passes data as a packed array of svBitVecVal words
// (for a "bit [255:0]" argument). Allocate 256 bits (= 32 bytes) as
// 32/sizeof(svBitVecVal) words on the stack.
svBitVecVal buf[256 / 8 / sizeof(svBitVecVal)];
// The implementation of otbn_stack_element_peek() is only available if
// DesignScope != "" (the function is implemented in SystemVerilog, and
// imported through DPI). We should not reach the code here if that's the
// case.
assert(otbn_stack_element_peek);
int i = 0;
while (1) {
int peek_result = otbn_stack_element_peek(i, buf);
// otbn_stack_element_peek is defined in otbn_stack_snooper_if.sv. Possible
// return values are: 0 on success, if we've returned an element. 1 if the
// stack doesn't have an element at index i. 2 if something terrible has
// gone wrong (such as a completely bogus index).
assert(peek_result <= 2);
if (peek_result == 2) {
std::ostringstream oss;
oss << "Failed to peek into RTL to get value of stack element " << i
<< " at scope `" << stack_scope << "'.";
throw std::runtime_error(oss.str());
}
if (peek_result == 1) {
// No more elements on stack
break;
}
T stack_element;
memcpy(&stack_element, buf, sizeof(T));
ret.push_back(stack_element);
++i;
}
return ret;
}
int OtbnModel::start(unsigned start_addr) {
const MemArea &imem = mem_util_.GetMemArea(true);
assert(start_addr % 4 == 0);
assert(start_addr / 4 < imem.GetSizeWords());
ISSWrapper *iss = ensure_wrapper();
if (!iss)
return -1;
std::string dfname(iss->make_tmp_path("dmem"));
std::string ifname(iss->make_tmp_path("imem"));
try {
write_vector_to_file(dfname, get_sim_memory(false));
write_vector_to_file(ifname, get_sim_memory(true));
} catch (const std::exception &err) {
std::cerr << "Error when dumping memory contents: " << err.what() << "\n";
return -1;
}
try {
iss->load_d(dfname);
iss->load_i(ifname);
iss->start(start_addr);
} catch (const std::runtime_error &err) {
std::cerr << "Error when starting ISS: " << err.what() << "\n";
return -1;
}
return 0;
}
int OtbnModel::step(svLogic edn_rnd_data_valid,
svLogicVecVal *edn_rnd_data, /* logic [255:0] */
svLogic edn_urnd_data_valid,
svBitVecVal *insn_cnt /* bit [31:0] */,
svBitVecVal *err_bits /* bit [31:0] */,
svBitVecVal *stop_pc /* bit [31:0] */) {
assert(err_bits);
ISSWrapper *iss = ensure_wrapper();
if (!iss)
return -1;
assert(!is_xz(edn_rnd_data_valid));
assert(!is_xz(edn_urnd_data_valid));
try {
if (edn_rnd_data_valid) {
uint32_t int_edn_rnd_data[8];
set_rnd_data(int_edn_rnd_data, edn_rnd_data);
iss->edn_rnd_data(int_edn_rnd_data);
}
if (edn_urnd_data_valid) {
iss->edn_urnd_reseed_complete();
}
switch (iss->step(has_rtl())) {
case -1:
// Something went wrong, such as a trace mismatch. We've already printed
// a message to stderr so can just return -1.
return -1;
case 1:
// The simulation has stopped. Fill in insn_cnt, err_bits and stop_pc.
set_sv_u32(insn_cnt, iss->get_insn_cnt());
set_sv_u32(err_bits, iss->get_err_bits());
set_sv_u32(stop_pc, iss->get_stop_pc());
return 1;
case 0:
// The simulation is still running. Update insn_cnt.
set_sv_u32(insn_cnt, iss->get_insn_cnt());
return 0;
default:
// This shouldn't happen
assert(0);
}
} catch (const std::runtime_error &err) {
std::cerr << "Error when stepping ISS: " << err.what() << "\n";
return -1;
}
}
int OtbnModel::check() const {
if (!has_rtl())
return 1;
ISSWrapper *iss = iss_.get();
if (!iss) {
std::cerr << "Cannot check OTBN model: ISS has not started.\n";
return -1;
}
bool good = true;
good &= OtbnTraceChecker::get().Finish();
try {
good &= check_dmem(*iss);
} catch (const std::exception &err) {
std::cerr << "Failed to check DMEM: " << err.what() << "\n";
return -1;
}
try {
good &= check_regs(*iss);
} catch (const std::exception &err) {
std::cerr << "Failed to check registers: " << err.what() << "\n";
return -1;
}
try {
good &= check_call_stack(*iss);
} catch (const std::exception &err) {
std::cerr << "Failed to check call stack: " << err.what() << "\n";
return -1;
}
return good ? 1 : 0;
}
int OtbnModel::load_dmem() const {
ISSWrapper *iss = iss_.get();
if (!iss) {
std::cerr << "Cannot load dmem from OTBN model: ISS has not started.\n";
return -1;
}
const MemArea &dmem = mem_util_.GetMemArea(false);
std::string dfname(iss->make_tmp_path("dmem_out"));
try {
iss->dump_d(dfname);
set_sim_memory(false, read_vector_from_file(dfname, dmem.GetSizeBytes()));
} catch (const std::exception &err) {
std::cerr << "Error when loading dmem from ISS: " << err.what() << "\n";
return -1;
}
return 0;
}
void OtbnModel::reset() {
ISSWrapper *iss = iss_.get();
if (iss)
iss->reset(has_rtl());
}
bool OtbnModel::check_dmem(ISSWrapper &iss) const {
const MemArea &dmem = mem_util_.GetMemArea(false);
uint32_t dmem_bytes = dmem.GetSizeBytes();
std::string dfname(iss.make_tmp_path("dmem_out"));
iss.dump_d(dfname);
std::vector<uint8_t> iss_data = read_vector_from_file(dfname, dmem_bytes);
assert(iss_data.size() == dmem_bytes);
std::vector<uint8_t> rtl_data = get_sim_memory(false);
assert(rtl_data.size() == dmem_bytes);
// If the arrays match, we're done.
if (0 == memcmp(&iss_data[0], &rtl_data[0], dmem_bytes))
return true;
// If not, print out the first 10 mismatches
std::ios old_state(nullptr);
old_state.copyfmt(std::cerr);
std::cerr << "ERROR: Mismatches in dmem data:\n"
<< std::hex << std::setfill('0');
int bad_count = 0;
for (size_t i = 0; i < dmem_bytes; ++i) {
if (iss_data[i] != rtl_data[i]) {
std::cerr << " @offset 0x" << std::setw(3) << i << ": rtl has 0x"
<< std::setw(2) << (int)rtl_data[i] << "; iss has 0x"
<< std::setw(2) << (int)iss_data[i] << "\n";
++bad_count;
if (bad_count == 10) {
std::cerr << " (skipping further errors...)\n";
break;
}
}
}
std::cerr.copyfmt(old_state);
return false;
}
bool OtbnModel::check_regs(ISSWrapper &iss) const {
assert(design_scope_.size());
std::string base_scope =
design_scope_ +
".u_otbn_rf_base.gen_rf_base_ff.u_otbn_rf_base_inner.u_snooper";
std::string wide_scope =
design_scope_ +
".u_otbn_rf_bignum.gen_rf_bignum_ff.u_otbn_rf_bignum_inner.u_snooper";
auto rtl_gprs = get_rtl_regs<uint32_t>(base_scope);
auto rtl_wdrs = get_rtl_regs<ISSWrapper::u256_t>(wide_scope);
std::array<uint32_t, 32> iss_gprs;
std::array<ISSWrapper::u256_t, 32> iss_wdrs;
iss.get_regs(&iss_gprs, &iss_wdrs);
bool good = true;
for (int i = 0; i < 32; ++i) {
// Register index 1 is call stack, which is checked separately
if (i == 1)
continue;
if (rtl_gprs[i] != iss_gprs[i]) {
std::ios old_state(nullptr);
old_state.copyfmt(std::cerr);
std::cerr << std::setfill('0') << "RTL computed x" << i << " as 0x"
<< std::hex << rtl_gprs[i] << ", but ISS got 0x" << iss_gprs[i]
<< ".\n";
std::cerr.copyfmt(old_state);
good = false;
}
}
for (int i = 0; i < 32; ++i) {
if (0 != memcmp(rtl_wdrs[i].words, iss_wdrs[i].words,
sizeof(rtl_wdrs[i].words))) {
std::ios old_state(nullptr);
old_state.copyfmt(std::cerr);
std::cerr << "RTL computed w" << i << " as 0x" << std::hex
<< std::setfill('0');
for (int j = 0; j < 8; ++j) {
if (j)
std::cerr << "_";
std::cerr << std::setw(8) << rtl_wdrs[i].words[7 - j];
}
std::cerr << ", but ISS got 0x";
for (int j = 0; j < 8; ++j) {
if (j)
std::cerr << "_";
std::cerr << std::setw(8) << iss_wdrs[i].words[7 - j];
}
std::cerr << ".\n";
std::cerr.copyfmt(old_state);
good = false;
}
}
return good;
}
bool OtbnModel::check_call_stack(ISSWrapper &iss) const {
assert(design_scope_.size());
std::string call_stack_snooper_scope =
design_scope_ + ".u_otbn_rf_base.u_call_stack_snooper";
auto rtl_call_stack = get_stack<uint32_t>(call_stack_snooper_scope);
auto iss_call_stack = iss.get_call_stack();
bool good = true;
if (iss_call_stack.size() != rtl_call_stack.size()) {
std::cerr << "Call stack size mismatch, RTL call stack has "
<< rtl_call_stack.size() << " elements and ISS call stack has "
<< iss_call_stack.size() << " elements\n";
good = false;
}
// Iterate through both call stacks where both have elements
std::size_t call_stack_size =
std::min(rtl_call_stack.size(), iss_call_stack.size());
for (std::size_t i = 0; i < call_stack_size; ++i) {
if (iss_call_stack[i] != rtl_call_stack[i]) {
std::ios old_state(nullptr);
old_state.copyfmt(std::cerr);
std::cerr << std::setfill('0') << "RTL call stack element " << i
<< " is 0x" << std::hex << rtl_call_stack[i]
<< ", but ISS has 0x" << iss_call_stack[i] << ".\n";
std::cerr.copyfmt(old_state);
good = false;
}
}
return good;
}
extern "C" OtbnModel *otbn_model_init(const char *mem_scope,
const char *design_scope,
unsigned imem_words,
unsigned dmem_words) {
assert(mem_scope && design_scope);
return new OtbnModel(mem_scope, design_scope, imem_words, dmem_words);
}
extern "C" void otbn_model_destroy(OtbnModel *model) { delete model; }
// 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: check_due True if the model finished running last cycle
// Bit 2: failed_step Something failed when trying to start/step ISS
// Bit 3: 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), has a check due (bit 1 of status), or when start
// is asserted. At other times, it will return immediately (but wastes a DPI
// call).
//
// If the model is running and start 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. Otherwise, it writes the new value of otbn.INSN_CNT to
// *insn_cnt.
//
// If nothing goes wrong and the ISS finishes its run, we set running to false,
// write out err_bits and stop_pc and do the post-run task. If the model's
// 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 the model's design_scope is the empty string, we grab
// the contents of DMEM from the ISS and inject them into the simulation
// memory.
//
// If start is true, we start the model at start_addr and then step once (as
// described above).
extern "C" unsigned otbn_model_step(
OtbnModel *model, svLogic start, unsigned start_addr, unsigned status,
svLogic edn_rnd_data_valid, svLogicVecVal *edn_rnd_data, /* logic [255:0] */
svLogic edn_urnd_data_valid, svBitVecVal *insn_cnt /* bit [31:0] */,
svBitVecVal *err_bits /* bit [31:0] */,
svBitVecVal *stop_pc /* bit [31:0] */) {
assert(model && insn_cnt && err_bits && stop_pc);
// Run model checks if needed. This usually happens just after an operation
// has finished.
if (model->has_rtl() && (status & CHECK_DUE_BIT)) {
switch (model->check()) {
case 1:
// Match (success)
break;
case 0:
// Mismatch
status |= FAILED_CMP_BIT;
break;
default:
// Something went wrong
return (status & ~RUNNING_BIT) | FAILED_STEP_BIT;
}
status &= ~CHECK_DUE_BIT;
}
assert(!is_xz(start));
// Start the model if requested
if (start) {
switch (model->start(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 (model->step(edn_rnd_data_valid, edn_rnd_data, edn_urnd_data_valid,
insn_cnt, err_bits, stop_pc)) {
case 0:
// Still running: no change
break;
case 1:
// Finished
status = (status & ~RUNNING_BIT) | CHECK_DUE_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 just stopped running and there's no RTL, load the contents of
// DMEM back from the ISS
if (!model->has_rtl()) {
switch (model->load_dmem()) {
case 0:
// Success
break;
default:
// Failed to load DMEM
return (status & ~RUNNING_BIT) | FAILED_STEP_BIT;
}
}
return status;
}
// Flush any information in the model
extern "C" void otbn_model_reset(OtbnModel *model) {
assert(model);
model->reset();
}