libanemo/include/libcpu/riscv/privilege_module.hh

1281 lines
46 KiB
C++

#ifndef LIBCPU_RISCV_PRIVILEGE_MODULE_HH
#define LIBCPU_RISCV_PRIVILEGE_MODULE_HH
#include <algorithm>
#include <cassert>
#include <climits>
#include <cstddef>
#include <cstdint>
#include <libcpu/memory.hh>
#include <libcpu/riscv/riscv.hh>
#include <libcpu/riscv/user_core.hh>
#include <libvio/bus.hh>
#include <libanemo/width.hh>
#include <optional>
#include <type_traits>
namespace libcpu::riscv {
/**
* @brief RISC-V Privilege Module
*
* This class implements the RISC-V privilege architecture as defined in the
* RISC-V Privileged Specification. It manages:
* - Privilege levels (User, Supervisor, Machine)
* - Control/Status Registers (CSRs)
* - Address translation (via SATP register)
* - Exception and interrupt handling
* - Privilege transitions
*
* The module works in conjunction with the user core to handle all privileged
* operations while maintaining separation of concerns.
*
* @tparam WORD_T The word type for the architecture (uint32_t for RV32, uint64_t for RV64)
*
* @note All CSR accesses and privilege transitions must go through this module
* @note Memory operations are delegated to this module
* @see riscv_user_core for the unprivileged counterpart
*/
template <typename WORD_T, size_t TLB_SIZE_LOG2=12>
class privilege_module {
public:
using dispatch_t = riscv::dispatch_t;
using decode_t = riscv::decode_t;
using exec_result_type_t = riscv::exec_result_type_t;
using exec_result_t = riscv::exec_result_t<WORD_T>;
using satp_mode_t = riscv::satp_mode_t;
using tlb_entry_t = struct {
uint64_t ppn; ///< guest physical page number
WORD_T vpn; ///< guest virtual page number
}; ///< a virtual tlb entry translating guest virtual page number to host address
using address_translation_t = struct {
uint64_t paddr;
bool access_fault;
bool page_fault;
};
priv_level_t priv_level;
WORD_T mepc, mtvec, mcause, mtval, mscratch, mie, mip, medeleg, mideleg;
WORD_T sepc, stvec, scause, stval, sscratch, sie, sip;
memory_view *instr_bus;
memory_view *data_bus;
libvio::io_agent *mmio_bus;
struct {
priv_level_t mpp;
bool mxr; bool sum; bool spp; bool mpie; bool spie; bool mie; bool sie;
} status;
struct {
satp_mode_t mode;
uint16_t asid;
WORD_T ppn;
} satp;
tlb_entry_t tlb_r[1<<TLB_SIZE_LOG2]; ///< virtual tlb for load
tlb_entry_t tlb_w[1<<TLB_SIZE_LOG2]; ///< virtual tlb for store
tlb_entry_t tlb_x[1<<TLB_SIZE_LOG2]; ///< virtual tlb for instruction fetching
/**
* @brief Physical address of the last load-reserved, with a reservation flag in the low 3 bits.
*
* If (reserved_set & 0x7) == 0, no address is reserved.
* Otherwise, the 64-bit data at (reserved_set & ~0x7) is reserved.
*/
WORD_T reserved_set = 0;
/**
* @brief Translate virtual address to physical address
*
* Performs address translation based on current privilege level and SATP register.
* This function returns a 64-bit address regardless of WORD_T since the physical address
* in sv32 is also wider than 32 bits.
*
* This function is used for debugging. It ignores the page permission and do not modify the PTE.
*
* @param vaddr Virtual address to translate
* @return Physical address if translation succeeds, empty if fails
*/
std::optional<uint64_t> vaddr_to_paddr(WORD_T vaddr) const;
/**
* @brief Fetch instruction using physical address
*
* This function is designed for simulating processors without virtual memory.
* `op` will be populated with the `fetch` variant if successful, or `trap` if not.
*
* @param op Execution result structure to populate with fetch details
*/
void paddr_fetch_instruction(exec_result_t &op) const;
/**
* @brief Fetch instruction using virtual address
*
* Handles instruction fetch with virtual address translation.
* `op` will be populated with the `fetch` variant if successful, or `trap` if not.
*
* @param op Execution result structure to populate with fetch details
*/
void vaddr_fetch_instruction(exec_result_t &op);
/**
* @brief Perform physical address load operation
*
* This function is designed for simulating processors without virtual memory.
* `op` will be populated with the `retire` variant if successful, or the `trap` variant if not.
*
* @param op Execution result structure containing load details
*/
void paddr_load(exec_result_t &op);
/**
* @brief Perform physical address store operation
*
* This function is designed for simulating processors without virtual memory.
* `op` will be populated with the `retire` variant if successful, or the `trap` variant if not.
*
* @param op Execution result structure containing store details
*/
void paddr_store(exec_result_t &op);
/**
* @brief Perform virtual address load operation
*
* Handles memory load operations using virtual addresses, including full address translation
* and privilege checks according to the current memory management configuration.
* `op` will be populated with the `retire` variant if successful, or the `trap` variant
* if translation fails or access privileges are violated.
*
* @param op Execution result structure containing load details
*/
void vaddr_load(exec_result_t &op);
/**
* @brief Perform virtual address store operation
*
* Handles memory store operations using virtual addresses, including full address translation
* and privilege checks according to the current memory management configuration.
* `op` will be populated with the `retire` variant if successful, or the `trap` variant
* if translation fails or access privileges are violated.
*
* @param op Execution result structure containing store details
*/
void vaddr_store(exec_result_t &op);
WORD_T amo_op(amo_type_t type, libanemo::width_t width, WORD_T rs_value, WORD_T load_value) const;
void paddr_amo(exec_result_t &op);
void vaddr_amo(exec_result_t &op);
/**
* @brief Reset privilege module
*
* Initializes all privilege-related registers and state to their reset values.
*/
void reset(void);
/**
* @brief Raise an interrupt
*
* Records interrupt cause and updates interrupt pending bits.
*
* @param cause Interrupt cause code
*/
void raise_interrupt(WORD_T cause);
/**
* @brief Handle exception
*
* Processes exceptions by updating CSRs and potentially changing privilege level.
* `op` will be populated with the `retire` varient, and `op.next_pc` will be changed
* if there is an exception to handle.
*
* @param op Execution result structure containing exception details
*/
void handle_exception(exec_result_t &op);
/**
* @brief Handle interrupt
*
* Processes interrupts by updating CSRs and potentially changing privilege level.
* `op` will be populated with the `retire` varient, and `op.next_pc` will be changed
* if there is an interrupt to handle.
*
* @param op Execution result structure containing interrupt details
*/
void handle_interrupt(exec_result_t &op);
/**
* @brief Perform CSR operation
*
* Executes CSR read/write/set/clear operations with proper privilege checks.
* `op` will be populated with the `retire` varient if successful, or the `trap` varient if not.
*
* @param op Execution result structure containing CSR operation details
*/
void csr_op(exec_result_t &op);
/**
* @brief Handle ecall, mret, sret instructions
*
* @param op Execution result structure containing execution details
*/
void sys_op(exec_result_t &op);
/**
* @brief get the physical address of a leaf page table entry
*
* @param vaddr the virtual address
* @param read whether to load from the virtual address
* @param write whether to store the virtual address
* @param exec whether to fetch instruction from from the virtual address
* @return the physical address
*/
address_translation_t page_walk(WORD_T vaddr, bool read, bool write, bool exec);
/**
* @brief invalidate the entire TLB
*/
void flush_tlb(void);
};
template <typename WORD_T, size_t TLB_SIZE_LOG2>
void privilege_module<WORD_T, TLB_SIZE_LOG2>::reset(void) {
priv_level = priv_level_t::m;
mepc = 0; sepc = 0;
mtvec = 0; stvec = 0;
mcause = 0; scause = 0;
mtval = 0; stval = 0;
mscratch = 0; sscratch = 0;
medeleg = 0; mideleg = 0;
mie = 0; sie = 0;
mip = 0; sip = 0;
status = {
.mpp=priv_level_t::m, .spp=false,
.mpie=false, .spie=false,
.mie=false, .sie=false,
};
satp = {
.mode=satp_mode_t::bare,
.asid=0, .ppn=0,
};
reserved_set = 0;
}
template <typename WORD_T, size_t TLB_SIZE_LOG2>
std::optional<uint64_t> privilege_module<WORD_T, TLB_SIZE_LOG2>::vaddr_to_paddr(WORD_T vaddr) const {
if (priv_level==priv_level_t::m) {
return vaddr;
}
int n_levels;
int vaddr_size;
int paddr_size;
int top_ppn_size;
// Size of one level virtual page number and non-root level physical page number
constexpr int vpn_size = sizeof(WORD_T)*CHAR_BIT==32 ? 10 : 9;
// Mask of one level virtual page number
constexpr WORD_T vpn_mask = libanemo::bit_mask<WORD_T>(vpn_size, 0);
switch (satp.mode) {
case satp_mode_t::bare: {
return vaddr;
break;
}
case satp_mode_t::sv32: {
assert(sizeof(WORD_T)*CHAR_BIT == 32);
n_levels = 2;
vaddr_size = 32;
paddr_size = 34;
top_ppn_size = 12;
break;
}
case satp_mode_t::sv39: {
assert(sizeof(WORD_T)*CHAR_BIT == 64);
n_levels = 3;
vaddr_size = 39;
paddr_size = 56;
top_ppn_size = 26;
break;
}
case satp_mode_t::sv48: {
assert(sizeof(WORD_T)*CHAR_BIT == 64);
n_levels = 4;
vaddr_size = 48;
paddr_size = 56;
top_ppn_size = 17;
break;
}
case satp_mode_t::sv57: {
assert(sizeof(WORD_T)*CHAR_BIT == 64);
n_levels = 5;
vaddr_size = 57;
paddr_size = 56;
top_ppn_size = 8;
break;
}
case satp_mode_t::sv64: {
// This is not yet implemented
assert(false);
break;
}
}
// This avoids silly mistakes
// PTE size here mean PTE size without the top reserved bits
int pte_size = (top_ppn_size+vpn_size*(n_levels-1)+10);
assert(vpn_size*n_levels+12 == vaddr_size);
assert(top_ppn_size+vpn_size*(n_levels-1)+12 == paddr_size);
assert(sizeof(WORD_T)*CHAR_BIT==32&&pte_size==32 || sizeof(WORD_T)*CHAR_BIT==64&&pte_size==54);
// check whether the virtual address is sign extended
if constexpr (sizeof(WORD_T)*CHAR_BIT == 64) {
if (vaddr&libanemo::bit_mask<WORD_T>(vaddr_size+1, vaddr_size)) {
if (vaddr>>vaddr_size != libanemo::bit_mask<WORD_T>(64-vaddr_size, 0)) {
return std::nullopt;
}
} else {
if (vaddr>>vaddr_size != 0) {
return std::nullopt;
}
}
}
WORD_T ppn = satp.ppn;
for (int i=n_levels-1; i>=0; --i) {
// construct the address of the current level page table
WORD_T vpn = vaddr>>(i*vpn_size+12) & vpn_mask;
WORD_T pte_paddr = (ppn<<12) | (vpn<<(12-vpn_size));
std::optional<WORD_T> pte_opt = data_bus->read(pte_paddr, libanemo::int_type_to_width_t<WORD_T>());
if (!pte_opt.has_value()) {
// the physical addressof the PTE is not accessible
return std::nullopt;
}
WORD_T pte = pte_opt.value();
bool pte_v = pte & pte_mask<WORD_T>::v;
bool pte_r = pte & pte_mask<WORD_T>::r;
bool pte_w = pte & pte_mask<WORD_T>::w;
bool pte_x = pte & pte_mask<WORD_T>::x;
bool pte_u = pte & pte_mask<WORD_T>::u;
bool pte_a = pte & pte_mask<WORD_T>::a;
bool pte_d = pte & pte_mask<WORD_T>::d;
// Check the V bit and whether the permission fields are invalid
if (!pte_v || !pte_r&&pte_w) {
return std::nullopt;
}
// A, U and D fields of a non-leaf PTE is reserved
if (!(pte_r||pte_x) && (pte_u||pte_a||pte_d)) {
return std::nullopt;
}
// Reserved bits of the PTE should be zero
if constexpr (sizeof(WORD_T)*CHAR_BIT != 32) {
if (pte>>pte_size != 0) {
return std::nullopt;
}
}
// Now the PTE is valid, get the current level of PPN from it
ppn = pte >> 10;
if (pte_r || pte_x) {
// PTE is leaf
// Check if the large page is aligned
bool large_page_aligned = true;
for (int j=i-1; j>=0; --j) {
if (((ppn>>(j*vpn_size))&vpn_mask) != 0) {
large_page_aligned = false;
}
}
if (! large_page_aligned) {
return std::nullopt;
}
return (ppn<<12) | (vaddr&libanemo::bit_mask<WORD_T>(i*vpn_size+12, 0));
} else if (i == 0) {
// Level 0 page table must be a leaf
return std::nullopt;
}
}
// Should not reach here
assert(false);
}
template <typename WORD_T, size_t TLB_SIZE_LOG2>
void privilege_module<WORD_T, TLB_SIZE_LOG2>::paddr_fetch_instruction(exec_result_t &op) const {
assert(op.type==exec_result_type_t::retire || op.type==exec_result_type_t::trap);
WORD_T paddr = op.pc;
std::optional<uint32_t> instr_opt = instr_bus->read(paddr, libanemo::width_t::word);
if (instr_opt.has_value()) {
op.type = exec_result_type_t::fetch;
op.instr = instr_opt.value();
} else {
op.type = exec_result_type_t::trap;
op.trap = {
.cause = riscv::mcause<WORD_T>::except_instr_fault,
.tval = paddr,
};
}
}
template <typename WORD_T, size_t TLB_SIZE_LOG2>
void privilege_module<WORD_T, TLB_SIZE_LOG2>::vaddr_fetch_instruction(exec_result_t &op) {
assert(op.type==exec_result_type_t::retire || op.type==exec_result_type_t::trap);
WORD_T vaddr = op.pc;
WORD_T vpn = vaddr >> 12;
size_t tlb_index = vpn & libanemo::bit_mask<WORD_T>(TLB_SIZE_LOG2, 0);
tlb_entry_t tlb_entry = tlb_x[tlb_index];
uint64_t paddr;
if (tlb_entry.vpn != vpn) {
address_translation_t translation = page_walk(vaddr, false, false, true);
if (translation.access_fault) {
op.type = exec_result_type_t::trap;
op.trap = {
.cause = riscv::mcause<WORD_T>::except_instr_fault,
.tval = vaddr,
};
return;
} else if (translation.page_fault) {
op.type = exec_result_type_t::trap;
op.trap = {
.cause = riscv::mcause<WORD_T>::except_instr_page_fault,
.tval = vaddr,
};
return;
}
// now address translation successful
paddr = translation.paddr;
// now physical address is valid, update TLB
tlb_entry.vpn = vpn;
tlb_entry.ppn = paddr >> 12;
tlb_x[tlb_index] = tlb_entry;
} else {
paddr = (tlb_entry.ppn<<12) | (vaddr&libanemo::bit_mask<WORD_T>(12, 0));
}
// instruction fetch uses the instruction bus, not the data bus;
// no MMIO fallback for instruction fetch
std::optional<uint64_t> instr_opt = instr_bus->read(paddr, libanemo::width_t::word);
if (instr_opt.has_value()) {
op.type = exec_result_type_t::fetch;
op.instr = instr_opt.value();
} else {
op.type = exec_result_type_t::trap;
op.trap = {
.cause = riscv::mcause<WORD_T>::except_instr_fault,
.tval = vaddr,
};
}
}
template <typename WORD_T, size_t TLB_SIZE_LOG2>
void privilege_module<WORD_T, TLB_SIZE_LOG2>::paddr_load(exec_result_t &op) {
assert(op.type == exec_result_type_t::load);
auto [paddr, width, sign_extend, rd, reserved] = op.load;
std::optional<uint64_t> data_opt = data_bus->read(paddr, width);
// fall back to MMIO if the address is out of RAM
if (!data_opt.has_value() && mmio_bus!=nullptr) {
data_opt = mmio_bus->read(paddr, width);
}
if (data_opt.has_value()) {
// `data` is zero extended
WORD_T data = data_opt.value();
if (sign_extend) {
data = libanemo::sign_extend<WORD_T>(data, width);
}
if (reserved) {
reserved_set = paddr | 0x7;
}
op.type = exec_result_type_t::retire;
op.retire = {
.rd=rd, .value=data,
};
} else {
op.type = exec_result_type_t::trap;
op.trap = {
.cause = riscv::mcause<WORD_T>::except_load_fault,
.tval = paddr,
};
}
}
template <typename WORD_T, size_t TLB_SIZE_LOG2>
void privilege_module<WORD_T, TLB_SIZE_LOG2>::paddr_store(exec_result_t &op) {
assert(op.type == exec_result_type_t::store);
auto [paddr, width, data, rd, conditional] = op.store;
bool reservation_match = (reserved_set&0x7)!=0 && (reserved_set|0x7)==(paddr|0x7);
// The reservation will always be invalidated if it matches or this is sc
// - If there is no memory access trap, the memory access invalidates it.
// - If there is a trap, the trap invalidates it
if (reservation_match || conditional) {
reserved_set = 0;
}
// If store conditional does not have a matched reservation, skip memory access
if (conditional && !reservation_match) {
op.type = exec_result_type_t::retire;
op.retire = {.rd=rd, .value=1};
return;
}
bool success = data_bus->write(paddr, width, data);
// fall back to MMIO
if (!success && mmio_bus!=nullptr) {
success = mmio_bus->write(paddr, width, data);
}
if (success) {
op.type = exec_result_type_t::retire;
op.retire = {
.rd=rd, .value=0,
};
} else {
// both RAM and MMIO failed
op.type = exec_result_type_t::trap;
op.trap = {
.cause = riscv::mcause<WORD_T>::except_store_fault,
.tval = paddr,
};
}
}
template <typename WORD_T, size_t TLB_SIZE_LOG2>
void privilege_module<WORD_T, TLB_SIZE_LOG2>::vaddr_load(exec_result_t &op) {
assert(op.type == exec_result_type_t::load);
auto [vaddr, width, sign_extend, rd, reserved] = op.load;
WORD_T vpn = vaddr >> 12;
size_t tlb_index = vpn & libanemo::bit_mask<WORD_T>(TLB_SIZE_LOG2, 0);
tlb_entry_t tlb_entry = tlb_r[tlb_index];
uint64_t paddr;
if (tlb_entry.vpn != vpn) {
address_translation_t translation = page_walk(vaddr, true, false, false);
if (translation.access_fault) {
op.type = exec_result_type_t::trap;
op.trap = {
.cause = riscv::mcause<WORD_T>::except_load_fault,
.tval = vaddr,
};
return;
} else if (translation.page_fault) {
op.type = exec_result_type_t::trap;
op.trap = {
.cause = riscv::mcause<WORD_T>::except_load_page_fault,
.tval = vaddr,
};
return;
}
// now address translation successful
paddr = translation.paddr;
// now physical address is valid, update TLB
tlb_entry.vpn = vpn;
tlb_entry.ppn = paddr >> 12;
tlb_r[tlb_index] = tlb_entry;
} else {
paddr = (tlb_entry.ppn<<12) | (vaddr&libanemo::bit_mask<WORD_T>(12, 0));
}
std::optional<uint64_t> data_opt = data_bus->read(paddr, width);
// fall back to MMIO if the address is out of RAM
if (!data_opt.has_value() && mmio_bus!=nullptr) {
data_opt = mmio_bus->read(paddr, width);
}
if (data_opt.has_value()) {
// `data` is zero extended
WORD_T data = data_opt.value();
if (sign_extend) {
data = libanemo::sign_extend<WORD_T>(data, width);
}
if (reserved) {
reserved_set = paddr | 0x7;
}
op.type = exec_result_type_t::retire;
op.retire = {
.rd=rd, .value=data,
};
} else {
op.type = exec_result_type_t::trap;
op.trap = {
.cause = riscv::mcause<WORD_T>::except_load_fault,
.tval = vaddr,
};
}
}
template <typename WORD_T, size_t TLB_SIZE_LOG2>
void privilege_module<WORD_T, TLB_SIZE_LOG2>::vaddr_store(exec_result_t &op) {
assert(op.type == exec_result_type_t::store);
auto [vaddr, width, value, rd, conditional] = op.store;
WORD_T vpn = vaddr >> 12;
size_t tlb_index = vpn & libanemo::bit_mask<WORD_T>(TLB_SIZE_LOG2, 0);
tlb_entry_t tlb_entry = tlb_w[tlb_index];
uint64_t paddr;
if (tlb_entry.vpn != vpn) {
address_translation_t translation = page_walk(vaddr, false, true, false);
if (translation.access_fault) {
op.type = exec_result_type_t::trap;
op.trap = {
.cause = riscv::mcause<WORD_T>::except_store_fault,
.tval = vaddr,
};
return;
} else if (translation.page_fault) {
op.type = exec_result_type_t::trap;
op.trap = {
.cause = riscv::mcause<WORD_T>::except_store_page_fault,
.tval = vaddr,
};
return;
}
// now address translation successful
paddr = translation.paddr;
// now physical address is valid, update TLB
tlb_entry.vpn = vpn;
tlb_entry.ppn = paddr >> 12;
tlb_w[tlb_index] = tlb_entry;
} else {
paddr = (tlb_entry.ppn<<12) | (vaddr&libanemo::bit_mask<WORD_T>(12, 0));
}
bool reservation_match = (reserved_set&0x7)!=0 && (reserved_set|0x7)==(paddr|0x7);
// The reservation will always be invalidated if it matches or this is sc
// - If there is no memory access trap, the memory access invalidates it.
// - If there is a trap, the trap invalidates it
if (reservation_match || conditional) {
reserved_set = 0;
}
// If store conditional does not have a matched reservation, skip memory access
if (conditional && !reservation_match) {
op.type = exec_result_type_t::retire;
op.retire = {.rd=rd, .value=1};
return;
}
bool success = data_bus->write(paddr, width, value);
// fall back to MMIO if the address is out of RAM
if (!success && mmio_bus!=nullptr) {
success = mmio_bus->write(paddr, width, value);
}
if (success) {
op.type = exec_result_type_t::retire;
op.retire = {.rd=rd, .value=0};
} else {
op.type = exec_result_type_t::trap;
op.trap = {
.cause = riscv::mcause<WORD_T>::except_store_fault,
.tval = vaddr,
};
}
}
template <typename WORD_T, size_t TLB_SIZE_LOG2>
void privilege_module<WORD_T, TLB_SIZE_LOG2>::raise_interrupt(WORD_T cause) {
WORD_T cause_mask = 1 << (cause & ~riscv::mcause<WORD_T>::intr_mask);
if (mideleg & cause_mask) {
sip |= cause_mask;
} else {
mip |= cause_mask;
}
}
template <typename WORD_T, size_t TLB_SIZE_LOG2>
void privilege_module<WORD_T, TLB_SIZE_LOG2>::handle_interrupt(exec_result_t &op) {
constexpr size_t word_size = sizeof(WORD_T) * CHAR_BIT;
assert(op.type == exec_result_type_t::retire);
WORD_T pc = op.next_pc;
priv_level_t target_priv_level;
WORD_T cause;
if ((mie&mip) && (status.mie||priv_level!=priv_level_t::m)) {
for (size_t i=0; i<word_size; ++i) {
if (((mie&mip)>>i)&1) {
target_priv_level = priv_level_t::m;
cause = i;
break;
}
}
} else if ((sie&sip) && priv_level!=priv_level_t::m && (status.sie||priv_level==priv_level_t::u)) {
for (size_t i=0; i<word_size; ++i) {
if (((sie&sip)>>i)&1) {
target_priv_level = priv_level_t::s;
cause = i;
break;
}
}
} else {
return;
}
WORD_T vector_base;
bool is_vectord;
if (target_priv_level == priv_level_t::m) {
vector_base = mtvec & ~riscv::mtvec<WORD_T>::vectored;
is_vectord = mtvec & riscv::mtvec<WORD_T>::vectored;
} else {
vector_base = stvec & ~riscv::mtvec<WORD_T>::vectored;
is_vectord = stvec & riscv::mtvec<WORD_T>::vectored;
}
WORD_T target_addr;
if (is_vectord) {
target_addr = vector_base + 4*cause;
} else {
target_addr = vector_base;
}
if (target_priv_level == priv_level_t::m) {
mcause = cause | riscv::mcause<WORD_T>::intr_mask;
mtval = 0;
mepc = pc;
status.mpp = priv_level;
status.mpie = status.mie;
status.mie = false;
} else {
assert(priv_level!=priv_level_t::m);
scause = cause | riscv::mcause<WORD_T>::intr_mask;
stval = 0;
sepc = pc;
status.spp = priv_level==priv_level_t::s;
status.spie = status.sie;
status.sie = false;
}
priv_level = target_priv_level;
reserved_set = 0;
op.type = exec_result_type_t::retire;
op.next_pc = target_addr;
op.retire.rd = 0;
}
template <typename WORD_T, size_t TLB_SIZE_LOG2>
void privilege_module<WORD_T, TLB_SIZE_LOG2>::handle_exception(exec_result_t &op) {
assert(op.type == exec_result_type_t::trap);
WORD_T pc = op.pc;
WORD_T cause = op.trap.cause;
WORD_T tval = op.trap.tval;
WORD_T trap_no = cause & ~riscv::mcause<WORD_T>::intr_mask;
priv_level_t target_priv_level;
if (priv_level!=priv_level_t::m && (medeleg & (1 << cause))) {
target_priv_level = priv_level_t::s;
} else {
target_priv_level = priv_level_t::m;
}
WORD_T target_addr;
if (target_priv_level == priv_level_t::m) {
target_addr = mtvec & ~riscv::mtvec<WORD_T>::vectored;
} else {
target_addr = stvec & ~riscv::mtvec<WORD_T>::vectored;
}
if (target_priv_level == priv_level_t::m) {
mcause = cause;
mtval = tval;
mepc = pc;
status.mpp = priv_level;
status.mpie = status.mie;
status.mie = false;
} else {
assert(priv_level!=priv_level_t::m);
scause = cause;
stval = tval;
sepc = pc;
status.spp = priv_level==priv_level_t::s;
status.spie = status.sie;
status.sie = false;
}
priv_level = target_priv_level;
reserved_set = 0;
op.type = exec_result_type_t::retire;
op.next_pc = target_addr;
op.retire.rd = 0;
}
#define CSRRCS(name) \
case riscv::csr_addr::name: { \
op.retire.value = name; \
if (write) { \
name = value; \
} else if (set) { \
name |= value; \
} else if (clear) { \
name &= ~value; \
} \
break; \
}
#define STATUSRCS(csr_name, bit_name) \
if (status.bit_name) { \
op.retire.value |= riscv::csr_name<WORD_T>::bit_name; \
} \
if (write) { \
status.bit_name = value & riscv::csr_name<WORD_T>::bit_name; \
} else if (set) { \
status.bit_name = value&riscv::csr_name<WORD_T>::bit_name ? true : status.bit_name; \
} else if (clear) { \
status.bit_name = value&riscv::csr_name<WORD_T>::bit_name ? false : status.bit_name; \
}
template <typename WORD_T, size_t TLB_SIZE_LOG2>
void privilege_module<WORD_T, TLB_SIZE_LOG2>::csr_op(exec_result_t &op) {
constexpr bool is_rv64 = sizeof(WORD_T) * CHAR_BIT == 64;
assert(op.type == exec_result_type_t::csr_op);
auto [addr, rd, read, write, set, clear, value] = op.csr_op;
// check whether it has access
bool read_access = static_cast<int>(priv_level) >= (addr>>8 & 0x3);
if (!read_access && read) {
op.type = exec_result_type_t::trap;
op.trap.cause = riscv::mcause<WORD_T>::except_illegal_instr;
op.trap.tval = op.instr;
return;
}
bool write_access = read_access && (addr>>10)!=0x3;
if (!write_access && (write||set||clear)) {
op.type = exec_result_type_t::trap;
op.trap.cause = riscv::mcause<WORD_T>::except_illegal_instr;
op.trap.tval = op.instr;
return;
}
op.type = exec_result_type_t::retire;
op.retire.rd = rd;
op.retire.value = 0;
switch (addr) {
case riscv::csr_addr::misa: {
if constexpr (is_rv64) {
op.retire.value = uint64_t(2)<<62 | 0x101100;
} else {
op.retire.value = 0x40101100;
}
break;
}
case riscv::csr_addr::mhartid: {
op.retire.value = 0;
}
CSRRCS(mepc);
CSRRCS(sepc);
CSRRCS(mtvec);
CSRRCS(stvec);
CSRRCS(mcause);
CSRRCS(scause);
CSRRCS(mtval);
CSRRCS(stval);
CSRRCS(mscratch);
CSRRCS(sscratch);
CSRRCS(medeleg);
CSRRCS(mideleg);
CSRRCS(mie);
CSRRCS(sie);
CSRRCS(mip);
CSRRCS(sip);
case riscv::csr_addr::mstatus: {
WORD_T mpp = static_cast<WORD_T>(status.mpp);
op.retire.value |= mpp << 11;
if (write) {
mpp = (value>>11) & 3;
} else if (set) {
mpp |= (value>>11) & 3;
} else if (clear) {
mpp &= ~((value>>11) & 3);
}
STATUSRCS(mstatus, mxr);
STATUSRCS(mstatus, sum);
status.mpp = static_cast<priv_level_t>(mpp);
// prevent invalid values
status.mpp = status.mpp!=priv_level_t::u && status.mpp!=priv_level_t::s ? priv_level_t::m : status.mpp;
STATUSRCS(mstatus, spp);
STATUSRCS(mstatus, mpie);
STATUSRCS(mstatus, spie);
STATUSRCS(mstatus, mie);
STATUSRCS(mstatus, sie);
break;
}
case riscv::csr_addr::sstatus: {
STATUSRCS(sstatus, mxr);
STATUSRCS(sstatus, sum);
STATUSRCS(sstatus, spp);
STATUSRCS(sstatus, spie);
STATUSRCS(sstatus, sie);
break;
}
case riscv::csr_addr::satp: {
WORD_T ppn = satp.ppn;
uint16_t asid = satp.asid;
uint8_t mode_val = static_cast<uint8_t>(satp.mode);
WORD_T new_ppn; uint16_t new_asid; uint8_t new_mode_val;
if constexpr (is_rv64) {
op.retire.value = static_cast<WORD_T>(satp.mode)<<60 | WORD_T(satp.asid)<<44 | satp.ppn;
new_ppn = value & libanemo::bit_mask<uint64_t>(44, 0);
new_asid = (value>>44) & libanemo::bit_mask<WORD_T>(16, 0);
new_mode_val = value >> 60;
} else {
op.retire.value = static_cast<WORD_T>(satp.mode)<<31 | WORD_T(satp.asid)<<22 | satp.ppn;
new_ppn = value & libanemo::bit_mask<uint64_t>(22, 0);
new_asid = (value>>22) & libanemo::bit_mask<WORD_T>(9, 0);
new_mode_val = value >> 31;
}
if (write) {
ppn = new_ppn;
asid = new_asid;
mode_val = new_mode_val;
flush_tlb();
} else if (set) {
ppn |= new_ppn;
asid |= new_asid;
mode_val |= new_mode_val;
flush_tlb();
} else if (clear) {
ppn &= ~new_ppn;
asid &= ~new_asid;
mode_val &= ~new_mode_val;
flush_tlb();
}
satp_mode_t mode;
switch (mode_val) {
case static_cast<uint64_t>(satp_mode_t::bare):
mode = satp_mode_t::bare;
break;
case static_cast<uint64_t>(satp_mode_t::sv32):
if constexpr (is_rv64) {
// unsupported mode, do not change anything
return;
} else {
mode = satp_mode_t::sv32;
}
break;
// mode_val can only be bare (0) or sv32 (1) on 32 bit
case static_cast<uint64_t>(satp_mode_t::sv39):
mode = satp_mode_t::sv39;
break;
case static_cast<uint64_t>(satp_mode_t::sv48):
mode = satp_mode_t::sv48;
break;
case static_cast<uint64_t>(satp_mode_t::sv57):
mode = satp_mode_t::sv57;
break;
default:
// unsupported mode, do not change anything
return;
}
satp.ppn = ppn;
satp.asid = asid;
satp.mode = mode;
break;
}
default: {
// Attempt of accessing not implemented CSR
// raise an illegal instruction trap
op.type = exec_result_type_t::trap;
op.trap.cause = riscv::mcause<WORD_T>::except_illegal_instr;
op.trap.tval = op.instr;
}
}
}
#undef CSRRCS
#undef STATUSRCS
template <typename WORD_T, size_t TLB_SIZE_LOG2>
void privilege_module<WORD_T, TLB_SIZE_LOG2>::sys_op(exec_result_t &op) {
assert(op.type == exec_result_type_t::sys_op);
if (op.sys_op.ecall) {
op.type = exec_result_type_t::trap;
if (priv_level == priv_level_t::u) {
op.trap.cause = riscv::mcause<WORD_T>::except_env_call_u;
} else if (priv_level == priv_level_t::s) {
op.trap.cause = riscv::mcause<WORD_T>::except_env_call_s;
} else if (priv_level == priv_level_t::m) {
op.trap.cause = riscv::mcause<WORD_T>::except_env_call_m;
}
op.trap.tval = 0;
} else if (op.sys_op.mret) {
if (priv_level != priv_level_t::m) {
op.type = exec_result_type_t::trap;
op.trap = {.cause=riscv::mcause<WORD_T>::except_illegal_instr, .tval=op.instr};
} else {
priv_level = status.mpp;
status.mie = status.mpie;
status.mpie = 1;
status.mpp = priv_level_t::u;
op.type = exec_result_type_t::retire;
op.retire.rd = 0;
op.next_pc = mepc;
}
} else if (op.sys_op.sret) {
if (priv_level == priv_level_t::u) {
op.type = exec_result_type_t::trap;
op.trap = {.cause=riscv::mcause<WORD_T>::except_illegal_instr, .tval=op.instr};
} else {
priv_level = status.spp ? priv_level_t::s : priv_level_t::u;
status.sie = status.spie;
status.spie = 1;
status.spp = 0;
op.type = exec_result_type_t::retire;
op.retire.rd = 0;
op.next_pc = sepc;
}
} else if (op.sys_op.sfence_vma) {
if (priv_level == priv_level_t::u) {
op.type = exec_result_type_t::trap;
op.trap = {.cause=riscv::mcause<WORD_T>::except_illegal_instr, .tval=op.instr};
} else {
flush_tlb();
op.type = exec_result_type_t::retire;
op.retire.rd = 0;
}
}
}
template <typename WORD_T, size_t TLB_SIZE_LOG2>
privilege_module<WORD_T, TLB_SIZE_LOG2>::address_translation_t privilege_module<WORD_T, TLB_SIZE_LOG2>::page_walk(WORD_T vaddr, bool read, bool write, bool exec) {
if (priv_level==priv_level_t::m) {
return {.paddr=vaddr, .access_fault=false, .page_fault=false};
}
int n_levels;
int vaddr_size;
int paddr_size;
int top_ppn_size;
// Size of one level virtual page number and non-root level physical page number
constexpr int vpn_size = sizeof(WORD_T)*CHAR_BIT==32 ? 10 : 9;
// Mask of one level virtual page number
constexpr WORD_T vpn_mask = libanemo::bit_mask<WORD_T>(vpn_size, 0);
switch (satp.mode) {
case satp_mode_t::bare: {
return {.paddr=vaddr, .access_fault=false, .page_fault=false};
break;
}
case satp_mode_t::sv32: {
assert(sizeof(WORD_T)*CHAR_BIT == 32);
n_levels = 2;
vaddr_size = 32;
paddr_size = 34;
top_ppn_size = 12;
break;
}
case satp_mode_t::sv39: {
assert(sizeof(WORD_T)*CHAR_BIT == 64);
n_levels = 3;
vaddr_size = 39;
paddr_size = 56;
top_ppn_size = 26;
break;
}
case satp_mode_t::sv48: {
assert(sizeof(WORD_T)*CHAR_BIT == 64);
n_levels = 4;
vaddr_size = 48;
paddr_size = 56;
top_ppn_size = 17;
break;
}
case satp_mode_t::sv57: {
assert(sizeof(WORD_T)*CHAR_BIT == 64);
n_levels = 5;
vaddr_size = 57;
paddr_size = 56;
top_ppn_size = 8;
break;
}
case satp_mode_t::sv64: {
// This is not yet implemented
assert(false);
break;
}
}
// This avoids silly mistakes
// PTE size here mean PTE size without the top reserved bits
int pte_size = (top_ppn_size+vpn_size*(n_levels-1)+10);
assert(vpn_size*n_levels+12 == vaddr_size);
assert(top_ppn_size+vpn_size*(n_levels-1)+12 == paddr_size);
assert(sizeof(WORD_T)*CHAR_BIT==32&&pte_size==32 || sizeof(WORD_T)*CHAR_BIT==64&&pte_size==54);
// check whether the virtual address is sign extended
if constexpr (sizeof(WORD_T)*CHAR_BIT == 64) {
if (vaddr&libanemo::bit_mask<WORD_T>(vaddr_size+1, vaddr_size)) {
if (vaddr>>vaddr_size != libanemo::bit_mask<WORD_T>(64-vaddr_size, 0)) {
return {.paddr=0, .access_fault=false, .page_fault=true};
}
} else {
if (vaddr>>vaddr_size != 0) {
return {.paddr=0, .access_fault=false, .page_fault=true};
}
}
}
WORD_T ppn = satp.ppn;
for (int i=n_levels-1; i>=0; --i) {
// construct the address of the current level page table
WORD_T vpn = vaddr>>(i*vpn_size+12) & vpn_mask;
WORD_T pte_paddr = (ppn<<12) | (vpn<<(12-vpn_size));
std::optional<WORD_T> pte_opt = data_bus->read(pte_paddr, libanemo::int_type_to_width_t<WORD_T>());
if (!pte_opt.has_value()) {
// the physical addressof the PTE is not accessible
return {.paddr=0, .access_fault=true, .page_fault=false};
}
WORD_T pte = pte_opt.value();
bool pte_v = pte & pte_mask<WORD_T>::v;
bool pte_r = pte & pte_mask<WORD_T>::r;
bool pte_w = pte & pte_mask<WORD_T>::w;
bool pte_x = pte & pte_mask<WORD_T>::x;
bool pte_u = pte & pte_mask<WORD_T>::u;
bool pte_a = pte & pte_mask<WORD_T>::a;
bool pte_d = pte & pte_mask<WORD_T>::d;
// Check the V bit and whether the permission fields are invalid
if (!pte_v || !pte_r&&pte_w) {
return {.paddr=0, .access_fault=false, .page_fault=true};
}
// A, U and D fields of a non-leaf PTE is reserved
if (!(pte_r||pte_x) && (pte_u||pte_a||pte_d)) {
return {.paddr=0, .access_fault=false, .page_fault=true};
}
// Reserved bits of the PTE should be zero
if constexpr (sizeof(WORD_T)*CHAR_BIT != 32) {
if (pte>>pte_size != 0) {
return {.paddr=0, .access_fault=false, .page_fault=true};
}
}
// Now the PTE is valid, get the current level of PPN from it
ppn = pte >> 10;
if (pte_r || pte_x) {
// PTE is leaf
// Check if the large page is aligned
bool large_page_aligned = true;
for (int j=i-1; j>=0; --j) {
if (((ppn>>(j*vpn_size))&vpn_mask) != 0) {
large_page_aligned = false;
}
}
// Check page permissions
bool page_fault =
!large_page_aligned || // the page is not aligned
(!status.sum||exec)&&pte_u&&priv_level==priv_level_t::s || !pte_u&&priv_level==priv_level_t::u || // access not allowd by u bit
!(pte_r||pte_x&&status.mxr)&&read || !pte_w&&write || !pte_x&&exec; // do not have permission
// Now set the bit flags in the PTE
if (!page_fault) {
if (read || exec) {
pte |= pte_mask<WORD_T>::a;
}
if (write) {
pte |= pte_mask<WORD_T>::d;
}
bool result = data_bus->write(pte_paddr, libanemo::int_type_to_width_t<WORD_T>(), pte);
if (!result) {
return {.paddr=0, .access_fault=true, .page_fault=false};
}
}
return {
.paddr = (ppn<<12) | (vaddr&libanemo::bit_mask<WORD_T>(i*vpn_size+12, 0)),
.access_fault = false,
.page_fault = page_fault,
};
} else if (i == 0) {
// Level 0 page table must be a leaf
return {.paddr=0, .access_fault=false, .page_fault=true};
}
}
// Should not reach here
assert(false);
}
template <typename WORD_T, size_t TLB_SIZE_LOG2>
void privilege_module<WORD_T, TLB_SIZE_LOG2>::flush_tlb(void) {
for (tlb_entry_t& entry: tlb_r) {
entry.vpn = ~WORD_T(0);
}
for (tlb_entry_t& entry: tlb_w) {
entry.vpn = ~WORD_T(0);
}
for (tlb_entry_t& entry: tlb_x) {
entry.vpn = ~WORD_T(0);
}
}
template <typename WORD_T, size_t TLB_SIZE_LOG2>
WORD_T privilege_module<WORD_T, TLB_SIZE_LOG2>::amo_op(amo_type_t type, libanemo::width_t width, WORD_T rs_value, WORD_T load_value) const {
switch (type) {
case amo_type_t::swap:
return rs_value;
case amo_type_t::add:
return rs_value + load_value;
case amo_type_t::xor_:
return rs_value ^ load_value;
case amo_type_t::and_:
return rs_value & load_value;
case amo_type_t::or_:
return rs_value | load_value;
case amo_type_t::min:
return std::min(std::make_signed_t<WORD_T>(libanemo::sign_extend(rs_value, width)), std::make_signed_t<WORD_T>(load_value));
case amo_type_t::max:
return std::max(std::make_signed_t<WORD_T>(libanemo::sign_extend(rs_value, width)), std::make_signed_t<WORD_T>(load_value));
case amo_type_t::min_u:
return std::min(libanemo::zero_truncate(rs_value, width), libanemo::zero_truncate(load_value, width));
case amo_type_t::max_u:
return std::max(libanemo::zero_truncate(rs_value, width), libanemo::zero_truncate(load_value, width));
}
}
template <typename WORD_T, size_t TLB_SIZE_LOG2>
void privilege_module<WORD_T, TLB_SIZE_LOG2>::paddr_amo(exec_result_t &op) {
assert(op.type == exec_result_type_t::amo);
auto [paddr, width, data, rd, type] = op.amo;
// load the data
exec_result_t load_op = {
.type = exec_result_type_t::load,
.load = {.addr=paddr, .width=width, .sign_extend=true, .rd=0, .reserved=false},
};
paddr_load(load_op);
// convert load trap to store/amo trap
if (load_op.type == exec_result_type_t::trap) {
op.type = exec_result_type_t::trap;
op.trap = {.cause=riscv::mcause<WORD_T>::except_store_fault, .tval=paddr};
return;
}
assert(load_op.type == exec_result_type_t::retire);
// do computation
WORD_T result = amo_op(type, width, data, load_op.retire.value);
op.type = exec_result_type_t::store;
op.store = {.addr=paddr, .width=width, .data=result, .rd=0, .conditional=false};
// store the result back
paddr_store(op);
// set rd to thle loaded value
if (op.type == exec_result_type_t::retire) {
op.retire.rd = rd;
op.retire.value = load_op.retire.value;
} else {
assert(op.type == exec_result_type_t::trap);
}
}
template <typename WORD_T, size_t TLB_SIZE_LOG2>
void privilege_module<WORD_T, TLB_SIZE_LOG2>::vaddr_amo(exec_result_t &op) {
assert(op.type == exec_result_type_t::amo);
auto [vaddr, width, data, rd, type] = op.amo;
// load the data
exec_result_t load_op = {
.type = exec_result_type_t::load,
.load = {.addr=vaddr, .width=width, .sign_extend=true, .rd=0, .reserved=false},
};
vaddr_load(load_op);
// convert load trap to store/amo trap
if (load_op.type == exec_result_type_t::trap) {
op.type = exec_result_type_t::trap;
if (load_op.trap.cause == riscv::mcause<WORD_T>::except_load_fault) {
op.trap.cause=riscv::mcause<WORD_T>::except_store_fault;
} else if (load_op.trap.cause == riscv::mcause<WORD_T>::except_load_page_fault) {
op.trap.cause=riscv::mcause<WORD_T>::except_store_page_fault;
} else {
assert(0);
}
op.trap.tval=vaddr;
return;
}
assert(load_op.type == exec_result_type_t::retire);
// do the computation
WORD_T result = amo_op(type, width, data, load_op.retire.value);
op.type = exec_result_type_t::store;
op.store = {.addr=vaddr, .width=width, .data=result, .rd=0, .conditional=false};
// store the result back
vaddr_store(op);
// set rd to loaded value
if (op.type == exec_result_type_t::retire) {
op.retire.rd = rd;
op.retire.value = load_op.retire.value;
} else {
assert(op.type == exec_result_type_t::trap);
}
}
}
#endif