diff --git a/dv/cosim/cosim.core b/dv/cosim/cosim.core new file mode 100644 index 00000000..4ff9e9a8 --- /dev/null +++ b/dv/cosim/cosim.core @@ -0,0 +1,19 @@ +CAPI=2: +# Copyright lowRISC contributors. +# Licensed under the Apache License, Version 2.0, see LICENSE for details. +# SPDX-License-Identifier: Apache-2.0 + +name: "lowrisc:dv:cosim" +description: "Co-simulator framework" +filesets: + files_cpp: + files: + - cosim.h: { is_include_file: true } + - spike_cosim.cc + - spike_cosim.h: { is_include_file: true } + file_type: cppSource + +targets: + default: + filesets: + - files_cpp diff --git a/dv/cosim/cosim.h b/dv/cosim/cosim.h new file mode 100644 index 00000000..e6bd445e --- /dev/null +++ b/dv/cosim/cosim.h @@ -0,0 +1,137 @@ +// Copyright lowRISC contributors. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +#include +#include + +#ifndef COSIM_H_ +#define COSIM_H_ + +// Information about a dside transaction observed on the DUT memory interface +struct DSideAccessInfo { + // set when the access is a store, in which case `data` is the store data from + // the DUT. Otherwise `data` is the load data provided to the DUT. + bool store; + // `addr` is the address and must be 32-bit aligned. `data` and `be` are + // aligned to the address. For example an 8-bit store of 0xff to 0x12 has + // `data` as 0x00ff0000, `addr` as 0x10 and `be` as 0b0100. + uint32_t data; + uint32_t addr; + uint32_t be; + + // set if an error response to the transaction is seen. + bool error; + + // `misaligned_first` and `misaligned_second` are set when the transaction is + // generated for a misaligned store or load instruction. `misaligned_first` + // is true when the transaction is for the lower half and `misaligned_second` + // is true when the transaction is for the upper half, if it exists. + // + // For example an unaligned 32-bit load to 0x3 produces a transaction with + // `addr` as 0x0 and `misaligned_first` set to true, then a transaction with + // `addr` as 0x4 and `misaligned_second` set to true. An unaligned 16-bit load + // to 0x01 only produces a transaction with `addr` as 0x0 and + // `misaligned_first` set to true, there is no second half. + bool misaligned_first; + bool misaligned_second; +}; + +class Cosim { + public: + virtual ~Cosim() {} + + // Add a memory to the co-simulator environment. + // + // Use `backdoor_write_mem`/`backdoor_read_mem` to access it from the + // simulation environment. + virtual void add_memory(uint32_t base_addr, size_t size) = 0; + + // Write bytes to co-simulator memory. + // + // returns false if write fails (e.g. because no memory exists at the bytes + // being written). + virtual bool backdoor_write_mem(uint32_t addr, size_t len, + const uint8_t *data_in) = 0; + + // Read bytes from co-simulator memory. + // + // returns false if read fails (e.g. because no memory exists at the bytes + // being read). + virtual bool backdoor_read_mem(uint32_t addr, size_t len, + uint8_t *data_out) = 0; + + // Step the co-simulator, checking register write and PC of executed + // instruction match the supplied values. `write_reg` gives the index of the + // written register along with `write_reg_data` which provides the data. A + // `write_reg` of 0 indicates no register write occurred. + // + // `sync_trap` is set to true when the instruction caused a synchronous trap. + // In this case the instruction doesn't retire so no register write occurs (so + // `write_reg` must be 0). + // + // Returns false if there are any errors; use `get_errors` to obtain details + virtual bool step(uint32_t write_reg, uint32_t write_reg_data, uint32_t pc, + bool sync_trap) = 0; + + // When more than one of `set_mip`, `set_nmi` or `set_debug_req` is called + // before `step` which one takes effect is chosen by the co-simulator. Which + // should take priority is architecturally defined by the RISC-V + // specification. + + // Set the value of MIP. + // + // At the next call of `step`, the MIP value will take effect (i.e. if it's a + // new interrupt that is enabled it will step straight to that handler). + virtual void set_mip(uint32_t mip) = 0; + + // Set the state of the NMI (non-maskable interrupt) line. + // + // The NMI signal is a level triggered interrupt. When the NMI is taken the + // CPU ignores the NMI line until an `mret` instruction is executed. If the + // NMI is high following the `mret` (regardless of whether it has been low or + // not whilst the first NMI is being handled) a new NMI is taken. + // + // When an NMI is due to be taken that will occur at the next call of `step`. + virtual void set_nmi(bool nmi) = 0; + + // Set the debug request. + // + // When set to true the core will enter debug mode at the next step + virtual void set_debug_req(bool debug_req) = 0; + + // Set the value of mcycle. + // + // The co-simulation model doesn't alter the value of mcycle itself (other + // than instructions that do a direct CSR write). mcycle should be set to the + // correct value before any `step` call that may execute an instruction that + // observes the value of mcycle. + // + // A full 64-bit value is provided setting both the mcycle and mcycleh CSRs. + virtual void set_mcycle(uint64_t mcycle) = 0; + + // Tell the co-simulation model about observed transactions on the dside + // memory interface of the DUT. Accesses are notified once the response to a + // transaction is seen. + // + // Observed transactions for the DUT are checked against accesses from the + // co-simulation model when a memory access occurs during a `step`. If there + // is a mismatch an error is reported which can be obtained via `get_errors`. + virtual void notify_dside_access(const DSideAccessInfo &access_info) = 0; + + // Tell the co-simulation model about an error response to an iside fetch. + // + // `addr` must be 32-bit aligned. + // + // The next step following a call to `set_iside_error` must produce an + // instruction fault at the given address. + virtual void set_iside_error(uint32_t addr) = 0; + + // Get a vector of strings describing errors that have occurred during `step` + virtual const std::vector &get_errors() = 0; + + // Clear internal vector of error descriptions + virtual void clear_errors() = 0; +}; + +#endif // COSIM_H_ diff --git a/dv/cosim/cosim_dpi.cc b/dv/cosim/cosim_dpi.cc new file mode 100644 index 00000000..676fc85e --- /dev/null +++ b/dv/cosim/cosim_dpi.cc @@ -0,0 +1,94 @@ +// Copyright lowRISC contributors. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +#include +#include + +#include "cosim.h" +#include "cosim_dpi.h" + +int riscv_cosim_step(Cosim *cosim, const svBitVecVal *write_reg, + const svBitVecVal *write_reg_data, const svBitVecVal *pc, + svBit sync_trap) { + assert(cosim); + + return cosim->step(write_reg[0], write_reg_data[0], pc[0], sync_trap) ? 1 : 0; +} + +void riscv_cosim_set_mip(Cosim *cosim, const svBitVecVal *mip) { + assert(cosim); + + cosim->set_mip(mip[0]); +} + +void riscv_cosim_set_nmi(Cosim *cosim, svBit nmi) { + assert(cosim); + + cosim->set_nmi(nmi); +} + +void riscv_cosim_set_debug_req(Cosim *cosim, svBit debug_req) { + assert(cosim); + + cosim->set_debug_req(debug_req); +} + +void riscv_cosim_set_mcycle(Cosim *cosim, svBitVecVal *mcycle) { + assert(cosim); + + uint64_t mcycle_full = mcycle[0] | (uint64_t)mcycle[1] << 32; + cosim->set_mcycle(mcycle_full); +} + +void riscv_cosim_notify_dside_access(Cosim *cosim, svBit store, + svBitVecVal *addr, svBitVecVal *data, + svBitVecVal *be, svBit error, + svBit misaligned_first, + svBit misaligned_second) { + assert(cosim); + + cosim->notify_dside_access( + DSideAccessInfo{.store = store, + .data = data[0], + .addr = addr[0], + .be = be[0], + .error = error, + .misaligned_first = misaligned_first, + .misaligned_second = misaligned_second}); +} + +void riscv_cosim_set_iside_error(Cosim *cosim, svBitVecVal *addr) { + assert(cosim); + + cosim->set_iside_error(addr[0]); +} + +int riscv_cosim_get_num_errors(Cosim *cosim) { + assert(cosim); + + return cosim->get_errors().size(); +} + +const char *riscv_cosim_get_error(Cosim *cosim, int index) { + assert(cosim); + + if (index >= cosim->get_errors().size()) { + return nullptr; + } + + return cosim->get_errors()[index].c_str(); +} + +void riscv_cosim_clear_errors(Cosim *cosim) { + assert(cosim); + + cosim->clear_errors(); +} + +void riscv_cosim_write_mem_byte(Cosim *cosim, const svBitVecVal *addr, + const svBitVecVal *d) { + assert(cosim); + uint8_t byte = d[0] & 0xff; + cosim->backdoor_write_mem(addr[0], 1, &byte); +} diff --git a/dv/cosim/cosim_dpi.core b/dv/cosim/cosim_dpi.core new file mode 100644 index 00000000..8a5023a8 --- /dev/null +++ b/dv/cosim/cosim_dpi.core @@ -0,0 +1,20 @@ +CAPI=2: +# Copyright lowRISC contributors. +# Licensed under the Apache License, Version 2.0, see LICENSE for details. +# SPDX-License-Identifier: Apache-2.0 + +name: "lowrisc:dv:cosim_dpi" +description: "DPI wrapper for Co-simulator framework" +filesets: + files_cpp: + depend: + - lowrisc:dv:cosim + files: + - cosim_dpi.cc: { file_type: cppSource } + - cosim_dpi.h: { file_type: cppSource, is_include_file: true } + - cosim_dpi.svh: {file_type: systemVerilogSource } + +targets: + default: + filesets: + - files_cpp diff --git a/dv/cosim/cosim_dpi.h b/dv/cosim/cosim_dpi.h new file mode 100644 index 00000000..7eb76259 --- /dev/null +++ b/dv/cosim/cosim_dpi.h @@ -0,0 +1,35 @@ +// Copyright lowRISC contributors. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +#ifndef COSIM_DPI_H_ +#define COSIM_DPI_H_ + +#include +#include + +// This adapts the C++ interface of the `Cosim` class to be used via DPI. See +// the documentation in cosim.h for further details + +extern "C" { +int riscv_cosim_step(Cosim *cosim, const svBitVecVal *write_reg, + const svBitVecVal *write_reg_data, const svBitVecVal *pc, + svBit sync_trap); +void riscv_cosim_set_mip(Cosim *cosim, const svBitVecVal *mip); +void riscv_cosim_set_nmi(Cosim *cosim, svBit nmi); +void riscv_cosim_set_debug_req(Cosim *cosim, svBit debug_req); +void riscv_cosim_set_mcycle(Cosim *cosim, svBitVecVal *mcycle); +void riscv_cosim_notify_dside_access(Cosim *cosim, svBit store, + svBitVecVal *addr, svBitVecVal *data, + svBitVecVal *be, svBit error, + svBit misaligned_first, + svBit misaligned_second); +void riscv_cosim_set_iside_error(Cosim *cosim, svBitVecVal *addr); +int riscv_cosim_get_num_errors(Cosim *cosim); +const char *riscv_cosim_get_error(Cosim *cosim, int index); +void riscv_cosim_clear_errors(Cosim *cosim); +void riscv_cosim_write_mem_byte(Cosim *cosim, const svBitVecVal *addr, + const svBitVecVal *d); +} + +#endif // COSIM_DPI_H_ diff --git a/dv/cosim/cosim_dpi.svh b/dv/cosim/cosim_dpi.svh new file mode 100644 index 00000000..c9bdf6a7 --- /dev/null +++ b/dv/cosim/cosim_dpi.svh @@ -0,0 +1,24 @@ +// Copyright lowRISC contributors. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +// DPI interface to co-simulation model, see `cosim.h` for the interface description. + +// Implemented as a header file as VCS needs `import` declarations included in each verilog file +// that uses them. + +import "DPI-C" function int riscv_cosim_step(chandle cosim_handle, bit [4:0] write_reg, + bit [31:0] write_reg_data, bit [31:0] pc, bit sync_trap); +import "DPI-C" function void riscv_cosim_set_mip(chandle cosim_handle, bit [31:0] mip); +import "DPI-C" function void riscv_cosim_set_nmi(chandle cosim_handle, bit nmi); +import "DPI-C" function void riscv_cosim_set_debug_req(chandle cosim_handle, bit debug_req); +import "DPI-C" function void riscv_cosim_set_mcycle(chandle cosim_handle, bit [63:0] mcycle); +import "DPI-C" function void riscv_cosim_notify_dside_access(chandle cosim_handle, bit store, + bit [31:0] addr, bit [31:0] data, bit [3:0] be, bit error, bit misaligned_first, + bit misaligned_second); +import "DPI-C" function int riscv_cosim_set_iside_error(chandle cosim_handle, bit [31:0] addr); +import "DPI-C" function int riscv_cosim_get_num_errors(chandle cosim_handle); +import "DPI-C" function string riscv_cosim_get_error(chandle cosim_handle, int index); +import "DPI-C" function void riscv_cosim_clear_errors(chandle cosim_handle); +import "DPI-C" function void riscv_cosim_write_mem_byte(chandle cosim_handle, bit [31:0] addr, + bit [7:0] d); diff --git a/dv/cosim/spike_cosim.cc b/dv/cosim/spike_cosim.cc new file mode 100644 index 00000000..26e0d6bc --- /dev/null +++ b/dv/cosim/spike_cosim.cc @@ -0,0 +1,540 @@ +// Copyright lowRISC contributors. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +#include "spike_cosim.h" +#include "config.h" +#include "decode.h" +#include "devices.h" +#include "log_file.h" +#include "processor.h" +#include "simif.h" + +#include +#include +#include + +SpikeCosim::SpikeCosim(uint32_t start_pc, uint32_t start_mtvec, + const std::string &trace_log_path, bool secure_ibex, + bool icache_en) + : pending_iside_error(false), nmi_mode(false) { + FILE *log_file = nullptr; + if (trace_log_path.length() != 0) { + log = std::make_unique(trace_log_path.c_str()); + log_file = log->get(); + } + + processor = std::make_unique("RV32IMC", "MU", DEFAULT_VARCH, + this, 0, false, log_file, nullptr, + secure_ibex, icache_en); + + processor->set_mmu_capability(IMPL_MMU_SBARE); + processor->get_state()->pc = start_pc; + processor->get_state()->mtvec->write(start_mtvec); + + if (log) { + processor->set_debug(true); + processor->enable_log_commits(); + } +} + +// always return nullptr so all memory accesses go via mmio_load/mmio_store +char *SpikeCosim::addr_to_mem(reg_t addr) { return nullptr; } + +bool SpikeCosim::mmio_load(reg_t addr, size_t len, uint8_t *bytes) { + bool bus_error = !bus.load(addr, len, bytes); + + bool dut_error = false; + + // Incoming access may be an iside or dside access. Calculate 32-bit aligned + // address and current 32-bit aligned PC to help determine which. + uint32_t aligned_addr = addr & 0xfffffffc; + uint32_t aligned_pc = processor->get_state()->pc & 0xfffffffc; + + if (pending_iside_error && (aligned_addr == pending_iside_err_addr)) { + // Check if the incoming access is subject to an iside error, in which case + // assume it's an iside access and produce an error. + pending_iside_error = false; + dut_error = true; + } else if (aligned_pc != aligned_addr && (aligned_pc + 4) != aligned_addr) { + // Otherwise check if the aligned PC matches with the aligned address or an + // incremented aligned PC (to capture the unaligned 4-byte instruction + // case). Assume a successful iside access if either of these checks are + // true, otherwise assume a dside access and check against DUT dside + // accesses. If the RTL produced a bus error for the access, or the + // checking failed produce a memory fault in spike. + dut_error = (check_mem_access(false, addr, len, bytes) != kCheckMemOk); + } + + return !(bus_error || dut_error); +} + +bool SpikeCosim::mmio_store(reg_t addr, size_t len, const uint8_t *bytes) { + bool bus_error = !bus.store(addr, len, bytes); + // If the RTL produced a bus error for the access, or the checking failed + // produce a memory fault in spike. + bool dut_error = (check_mem_access(true, addr, len, bytes) != kCheckMemOk); + + return !(bus_error || dut_error); +} + +void SpikeCosim::proc_reset(unsigned id) {} + +const char *SpikeCosim::get_symbol(uint64_t addr) { return nullptr; } + +void SpikeCosim::add_memory(uint32_t base_addr, size_t size) { + auto new_mem = std::make_unique(size); + bus.add_device(base_addr, new_mem.get()); + mems.emplace_back(std::move(new_mem)); +} + +bool SpikeCosim::backdoor_write_mem(uint32_t addr, size_t len, + const uint8_t *data_in) { + return bus.store(addr, len, data_in); +} + +bool SpikeCosim::backdoor_read_mem(uint32_t addr, size_t len, + uint8_t *data_out) { + return bus.load(addr, len, data_out); +} + +bool SpikeCosim::step(uint32_t write_reg, uint32_t write_reg_data, uint32_t pc, + bool sync_trap) { + assert(write_reg < 32); + + if (pc_is_mret(pc)) { + nmi_mode = false; + } + + uint32_t initial_pc = (processor->get_state()->pc & 0xffffffff); + bool initial_pc_match = initial_pc == pc; + + // Execute the next instruction + processor->step(1); + + if (processor->get_state()->last_inst_pc == PC_INVALID) { + if (processor->get_state()->mcause->read() & 0x80000000) { + // Interrupt occurred, step again to execute first instruction of + // interrupt + processor->step(1); + // TODO: Deal with exception on first instruction of interrupt + assert(processor->get_state()->last_inst_pc != PC_INVALID); + } else { + // Otherwise a synchronous trap has occurred, check the DUT reported a + // synchronous trap at the same point + if (!sync_trap) { + std::stringstream err_str; + err_str << "Synchronous trap was expected at ISS PC: " << std::hex + << processor->get_state()->pc + << " but DUT didn't report one at PC " << pc; + errors.emplace_back(err_str.str()); + + return false; + } + + if (!initial_pc_match) { + std::stringstream err_str; + err_str << "PC mismatch at synchronous trap, DUT: " << std::hex << pc + << " expected: " << std::hex << initial_pc; + errors.emplace_back(err_str.str()); + + return false; + } + + if (write_reg != 0) { + std::stringstream err_str; + err_str << "Synchronous trap occurred at PC: " << std::hex << pc + << "but DUT wrote to register: x" << std::dec << write_reg; + errors.emplace_back(err_str.str()); + + return false; + } + + // Errors may have been generated outside of step() (e.g. in + // check_mem_access()), return false if there are any. + return errors.size() == 0; + } + } + + // Check PC of executed instruction matches the expected PC + // TODO: Confirm details of why spike sign extends PC, something to do with + // 32-bit address as 64-bit address must be sign extended? + if ((processor->get_state()->last_inst_pc & 0xffffffff) != pc) { + std::stringstream err_str; + err_str << "PC mismatch, DUT: " << std::hex << pc + << " expected: " << std::hex + << processor->get_state()->last_inst_pc; + errors.emplace_back(err_str.str()); + + return false; + } + + // Check register writes from executed instruction match what is expected + auto ®_changes = processor->get_state()->log_reg_write; + + bool gpr_write_seen = false; + + for (auto reg_change : reg_changes) { + // reg_change.first provides register type in bottom 4 bits, then register + // index above that + + // Ignore writes to x0 + if (reg_change.first == 0) + continue; + + if ((reg_change.first & 0xf) == 0) { + // register is GPR + // should never see more than one GPR write per step + assert(!gpr_write_seen); + + if (!check_gpr_write(reg_change, write_reg, write_reg_data)) { + return false; + } + + gpr_write_seen = true; + } else if ((reg_change.first & 0xf) == 4) { + // register is CSR + on_csr_write(reg_change); + } else { + // should never see other types + assert(false); + } + } + + if (write_reg != 0 && !gpr_write_seen) { + std::stringstream err_str; + err_str << "DUT wrote register x" << write_reg + << " but a write was not expected" << std::endl; + errors.emplace_back(err_str.str()); + + return false; + } + + if (pending_iside_error) { + std::stringstream err_str; + err_str << "DUT generated an iside error for address: " << std::hex + << pending_iside_err_addr << " but the ISS didn't produce one"; + errors.emplace_back(err_str.str()); + + return false; + } + + pending_iside_error = false; + + // Errors may have been generated outside of step() (e.g. in + // check_mem_access()), return false if there are any. + return errors.size() == 0; +} + +bool SpikeCosim::check_gpr_write(const commit_log_reg_t::value_type ®_change, + uint32_t write_reg, uint32_t write_reg_data) { + uint32_t cosim_write_reg = (reg_change.first >> 4) & 0x1f; + + if (write_reg == 0) { + std::stringstream err_str; + err_str << "DUT didn't write to register x" << cosim_write_reg + << ", but a write was expected"; + errors.emplace_back(err_str.str()); + + return false; + } + + if (write_reg != cosim_write_reg) { + std::stringstream err_str; + err_str << "Register write index mismatch, DUT: x" << write_reg + << " expected: x" << cosim_write_reg; + errors.emplace_back(err_str.str()); + + return false; + } + + // TODO: Investigate why this fails (may be because spike can produce PCs + // with high 32 bits set). + // assert((reg_change.second.v[0] & 0xffffffff00000000) == 0); + uint32_t cosim_write_reg_data = reg_change.second.v[0]; + + if (write_reg_data != cosim_write_reg_data) { + std::stringstream err_str; + err_str << "Register write data mismatch to x" << cosim_write_reg + << " DUT: " << std::hex << write_reg_data + << " expected: " << cosim_write_reg_data; + errors.emplace_back(err_str.str()); + + return false; + } + + return true; +} + +void SpikeCosim::on_csr_write(const commit_log_reg_t::value_type ®_change) { + int cosim_write_csr = (reg_change.first >> 4) & 0xfff; + + // TODO: Investigate why this fails (may be because spike can produce PCs + // with high 32 bits set). + // assert((reg_change.second.v[0] & 0xffffffff00000000) == 0); + uint32_t cosim_write_csr_data = reg_change.second.v[0]; + + // Spike and Ibex have different WARL behaviours so after any CSR write + // check the fields and adjust to match Ibex behaviour. + fixup_csr(cosim_write_csr, cosim_write_csr_data); +} + +void SpikeCosim::set_mip(uint32_t mip) { + processor->get_state()->mip->write_with_mask(0xffffffff, mip); +} + +void SpikeCosim::set_nmi(bool nmi) { + if (nmi && !nmi_mode && !processor->get_state()->debug_mode) { + processor->get_state()->nmi = true; + nmi_mode = true; + } +} + +void SpikeCosim::set_debug_req(bool debug_req) { + processor->halt_request = + debug_req ? processor_t::HR_REGULAR : processor_t::HR_NONE; +} + +void SpikeCosim::set_mcycle(uint64_t mcycle) { + processor->get_state()->mcycle = mcycle; +} + +void SpikeCosim::notify_dside_access(const DSideAccessInfo &access_info) { + // Address must be 32-bit aligned + assert((access_info.addr & 0x3) == 0); + + pending_dside_accesses.emplace_back( + PendingMemAccess{.dut_access_info = access_info, .be_spike = 0}); +} + +void SpikeCosim::set_iside_error(uint32_t addr) { + // Address must be 32-bit aligned + assert((addr & 0x3) == 0); + + pending_iside_error = true; + pending_iside_err_addr = addr; +} + +const std::vector &SpikeCosim::get_errors() { return errors; } + +void SpikeCosim::clear_errors() { errors.clear(); } + +void SpikeCosim::fixup_csr(int csr_num, uint32_t csr_val) { + switch (csr_num) { + case CSR_MSTATUS: + reg_t mask = + MSTATUS_MIE | MSTATUS_MPIE | MSTATUS_MPRV | MSTATUS_MPP | MSTATUS_TW; + + reg_t new_val = csr_val & mask; + processor->set_csr(csr_num, new_val); + break; + } +} + +SpikeCosim::check_mem_result_e SpikeCosim::check_mem_access( + bool store, uint32_t addr, size_t len, const uint8_t *bytes) { + assert(len >= 1 && len <= 4); + // Expect that no spike memory accesses cross a 32-bit boundary + assert(((addr + (len - 1)) & 0xfffffffc) == (addr & 0xfffffffc)); + + std::string iss_action = store ? "store" : "load"; + + // Check if there are any pending DUT accesses to check against + if (pending_dside_accesses.size() == 0) { + std::stringstream err_str; + err_str << "A " << iss_action << " at address " << std::hex << addr + << " was expected but there are no pending accesses"; + errors.emplace_back(err_str.str()); + + return kCheckMemCheckFailed; + } + + auto &top_pending_access = pending_dside_accesses.front(); + auto &top_pending_access_info = top_pending_access.dut_access_info; + + std::string dut_action = top_pending_access_info.store ? "store" : "load"; + + // Check for an address match + uint32_t aligned_addr = addr & 0xfffffffc; + if (aligned_addr != top_pending_access_info.addr) { + std::stringstream err_str; + err_str << "DUT generated " << dut_action << " at address " << std::hex + << top_pending_access_info.addr << " but " << iss_action + << " at address " << aligned_addr << " was expected"; + errors.emplace_back(err_str.str()); + + return kCheckMemCheckFailed; + } + + // Check access type match + if (store != top_pending_access_info.store) { + std::stringstream err_str; + err_str << "DUT generated " << dut_action << " at addr " << std::hex + << top_pending_access_info.addr << " but a " << iss_action + << " was expected"; + errors.emplace_back(err_str.str()); + + return kCheckMemCheckFailed; + } + + // Calculate bytes within aligned 32-bit word that spike has accessed + uint32_t expected_be = ((1 << len) - 1) << (addr & 0x3); + + bool pending_access_done = false; + bool misaligned = top_pending_access_info.misaligned_first || + top_pending_access_info.misaligned_second; + + if (misaligned) { + // For misaligned accesses spike will generated multiple single byte + // accesses where the DUT will generate an access covering all bytes within + // an aligned 32-bit word. + + // Check bytes accessed this time haven't already been been seen for the DUT + // access we are trying to match against + if ((expected_be & top_pending_access.be_spike) != 0) { + std::stringstream err_str; + err_str << "DUT generated " << dut_action << " at address " << std::hex + << top_pending_access_info.addr << " with BE " + << top_pending_access_info.be << " and expected BE " + << expected_be << " has been seen twice, so far seen " + << top_pending_access.be_spike; + + errors.emplace_back(err_str.str()); + + return kCheckMemCheckFailed; + } + + // Check expected access isn't trying to access bytes that the DUT access + // didn't access. + if ((expected_be & ~top_pending_access_info.be) != 0) { + std::stringstream err_str; + err_str << "DUT generated " << dut_action << " at address " << std::hex + << top_pending_access_info.addr << " with BE " + << top_pending_access_info.be << " but expected BE " + << expected_be << " has other bytes enabled"; + errors.emplace_back(err_str.str()); + return kCheckMemCheckFailed; + } + + // Record which bytes have been seen from spike + top_pending_access.be_spike |= expected_be; + + // If all bytes have been seen from spike we're done with this DUT access + if (top_pending_access.be_spike == top_pending_access_info.be) { + pending_access_done = true; + } + } else { + // For aligned accesses bytes from spike access must precisely match bytes + // from DUT access in one go + if (expected_be != top_pending_access_info.be) { + std::stringstream err_str; + err_str << "DUT generated " << dut_action << " at address " << std::hex + << top_pending_access_info.addr << " with BE " + << top_pending_access_info.be << " but BE " << expected_be + << " was expected"; + errors.emplace_back(err_str.str()); + + return kCheckMemCheckFailed; + } + + pending_access_done = true; + } + + // Check data from expected access matches pending DUT access. + // Data is ignored on error responses to loads so don't check it. Always check + // store data. + if (store || !top_pending_access_info.error) { + // Combine bytes into a single word + uint32_t expected_data = 0; + for (int i = 0; i < len; ++i) { + expected_data |= bytes[i] << (i * 8); + } + + // Shift bytes into their position within an aligned 32-bit word + expected_data <<= (addr & 0x3) * 8; + + // Mask off bytes expected access doesn't touch and check bytes match for + // those that it does + uint32_t expected_be_bits = (((uint64_t)1 << (len * 8)) - 1) + << ((addr & 0x3) * 8); + uint32_t masked_dut_data = top_pending_access_info.data & expected_be_bits; + + if (expected_data != masked_dut_data) { + std::stringstream err_str; + err_str << "DUT generated " << iss_action << " at address " << std::hex + << top_pending_access_info.addr << " with data " + << masked_dut_data << " but data " << expected_data + << " was expected with byte mask " << expected_be; + + errors.emplace_back(err_str.str()); + + return kCheckMemCheckFailed; + } + } + + bool pending_access_error = top_pending_access_info.error; + + if (pending_access_error && misaligned) { + // When misaligned accesses see an error, if they have crossed a 32-bit + // boundary DUT will generate two accesses. If the top pending access from + // the DUT was the first half of a misaligned access which accesses the top + // byte, it must have crossed the 32-bit boundary and generated a second + // access + if (top_pending_access_info.misaligned_first && + ((top_pending_access_info.be & 0x8) != 0)) { + // Check the second access DUT exists + if ((pending_dside_accesses.size() < 2) || + !pending_dside_accesses[1].dut_access_info.misaligned_second) { + std::stringstream err_str; + err_str << "DUT generated first half of misaligned " << iss_action + << " at address " << std::hex << top_pending_access_info.addr + << " but second half was expected and not seen"; + + errors.emplace_back(err_str.str()); + + return kCheckMemCheckFailed; + } + + // Check the second access had the expected address + if (pending_dside_accesses[1].dut_access_info.addr != + (top_pending_access_info.addr + 4)) { + std::stringstream err_str; + err_str << "DUT generated first half of misaligned " << iss_action + << " at address " << std::hex << top_pending_access_info.addr + << " but second half had incorrect address " + << pending_dside_accesses[1].dut_access_info.addr; + + errors.emplace_back(err_str.str()); + + return kCheckMemCheckFailed; + } + + // TODO: How to check BE? May need length of transaction? + + // Remove the top pending access now so both the first and second DUT + // accesses for this misaligned access are removed. + pending_dside_accesses.erase(pending_dside_accesses.begin()); + } + + // For any misaligned access that sees an error immediately indicate to + // spike the error has occured, so ensure the top pending access gets + // removed. + pending_access_done = true; + } + + if (pending_access_done) { + pending_dside_accesses.erase(pending_dside_accesses.begin()); + } + + return pending_access_error ? kCheckMemBusError : kCheckMemOk; +} + +bool SpikeCosim::pc_is_mret(uint32_t pc) { + uint32_t insn; + + if (!backdoor_read_mem(pc, 4, reinterpret_cast(&insn))) { + return false; + } + + return insn == 0x30200073; +} diff --git a/dv/cosim/spike_cosim.h b/dv/cosim/spike_cosim.h new file mode 100644 index 00000000..060f0bb2 --- /dev/null +++ b/dv/cosim/spike_cosim.h @@ -0,0 +1,92 @@ +// Copyright lowRISC contributors. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +#ifndef SPIKE_COSIM_H_ +#define SPIKE_COSIM_H_ + +#include "cosim.h" +#include "devices.h" +#include "log_file.h" +#include "processor.h" +#include "simif.h" + +#include +#include +#include +#include +#include + +class SpikeCosim : public simif_t, public Cosim { + private: + std::unique_ptr processor; + std::unique_ptr log; + bus_t bus; + std::vector> mems; + std::vector errors; + bool nmi_mode; + + void fixup_csr(int csr_num, uint32_t csr_val); + + struct PendingMemAccess { + DSideAccessInfo dut_access_info; + uint32_t be_spike; + }; + + std::vector pending_dside_accesses; + + bool pending_iside_error; + uint32_t pending_iside_err_addr; + + typedef enum { + kCheckMemOk, // Checks passed and access succeded in RTL + kCheckMemCheckFailed, // Checks failed + kCheckMemBusError // Checks passed, but access generated bus error in RTL + } check_mem_result_e; + + check_mem_result_e check_mem_access(bool store, uint32_t addr, size_t len, + const uint8_t *bytes); + + bool pc_is_mret(uint32_t pc); + + bool check_gpr_write(const commit_log_reg_t::value_type ®_change, + uint32_t write_reg, uint32_t write_reg_data); + + void on_csr_write(const commit_log_reg_t::value_type ®_change); + + public: + SpikeCosim(uint32_t start_pc, uint32_t start_mtvec, + const std::string &trace_log_path, bool secure_ibex, + bool icache_en); + + // simif_t implementation + virtual char *addr_to_mem(reg_t addr) override; + virtual bool mmio_load(reg_t addr, size_t len, uint8_t *bytes) override; + virtual bool mmio_store(reg_t addr, size_t len, + const uint8_t *bytes) override; + virtual void proc_reset(unsigned id) override; + virtual const char *get_symbol(uint64_t addr) override; + + // Cosim implementation + void add_memory(uint32_t base_addr, size_t size) override; + bool backdoor_write_mem(uint32_t addr, size_t len, + const uint8_t *data_in) override; + bool backdoor_read_mem(uint32_t addr, size_t len, uint8_t *data_out) override; + bool step(uint32_t write_reg, uint32_t write_reg_data, uint32_t pc, + bool sync_trap) override; + void set_mip(uint32_t mip) override; + void set_nmi(bool nmi) override; + void set_debug_req(bool debug_req) override; + void set_mcycle(uint64_t mcycle) override; + void notify_dside_access(const DSideAccessInfo &access_info) override; + // The spike co-simulator assumes iside and dside accesses within a step are + // disjoint. If both access the same address within a step memory faults may + // be incorrectly cause on one rather than the other or the access checking + // will break. + // TODO: Work on spike changes to remove this restriction + void set_iside_error(uint32_t addr) override; + const std::vector &get_errors() override; + void clear_errors() override; +}; + +#endif // SPIKE_COSIM_H_