mirror of
https://github.com/openhwgroup/cve2.git
synced 2025-04-22 21:17:59 -04:00
[dv] Add co-simulation framework
This commit is contained in:
parent
f4e3eefcfb
commit
648fadb34a
8 changed files with 961 additions and 0 deletions
19
dv/cosim/cosim.core
Normal file
19
dv/cosim/cosim.core
Normal file
|
@ -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
|
137
dv/cosim/cosim.h
Normal file
137
dv/cosim/cosim.h
Normal file
|
@ -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 <string>
|
||||
#include <vector>
|
||||
|
||||
#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<std::string> &get_errors() = 0;
|
||||
|
||||
// Clear internal vector of error descriptions
|
||||
virtual void clear_errors() = 0;
|
||||
};
|
||||
|
||||
#endif // COSIM_H_
|
94
dv/cosim/cosim_dpi.cc
Normal file
94
dv/cosim/cosim_dpi.cc
Normal file
|
@ -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 <svdpi.h>
|
||||
#include <cassert>
|
||||
|
||||
#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);
|
||||
}
|
20
dv/cosim/cosim_dpi.core
Normal file
20
dv/cosim/cosim_dpi.core
Normal file
|
@ -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
|
35
dv/cosim/cosim_dpi.h
Normal file
35
dv/cosim/cosim_dpi.h
Normal file
|
@ -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 <stdint.h>
|
||||
#include <svdpi.h>
|
||||
|
||||
// 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_
|
24
dv/cosim/cosim_dpi.svh
Normal file
24
dv/cosim/cosim_dpi.svh
Normal file
|
@ -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);
|
540
dv/cosim/spike_cosim.cc
Normal file
540
dv/cosim/spike_cosim.cc
Normal file
|
@ -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 <cassert>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
|
||||
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<log_file_t>(trace_log_path.c_str());
|
||||
log_file = log->get();
|
||||
}
|
||||
|
||||
processor = std::make_unique<processor_t>("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<mem_t>(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<std::string> &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<uint8_t *>(&insn))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return insn == 0x30200073;
|
||||
}
|
92
dv/cosim/spike_cosim.h
Normal file
92
dv/cosim/spike_cosim.h
Normal file
|
@ -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 <stdint.h>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class SpikeCosim : public simif_t, public Cosim {
|
||||
private:
|
||||
std::unique_ptr<processor_t> processor;
|
||||
std::unique_ptr<log_file_t> log;
|
||||
bus_t bus;
|
||||
std::vector<std::unique_ptr<mem_t>> mems;
|
||||
std::vector<std::string> 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<PendingMemAccess> 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<std::string> &get_errors() override;
|
||||
void clear_errors() override;
|
||||
};
|
||||
|
||||
#endif // SPIKE_COSIM_H_
|
Loading…
Add table
Add a link
Reference in a new issue