Sentinel Internal Structure

Block Diagram

Below is a simplified block diagram of Sentinel, showing how the main Components connect to each other. Behavior of Components not represented in the block diagram will be explained in the next sections.

Block diagram of Sentinel CPU, showing the various components described
    on the remainder of this page.

Blue arrows represent outputs from the microcode ROM’s data Signals, while purple arrows represents inputs from each Component back to the microcode ROM address Signals. A blue arrow into the microcode ROM reflects the fact that some microcode data outputs feed back into the ROM’s address as inputs.

Register File

Sentinel datapath (register file and read/write ports) implementation.

class sentinel_cpu.datapath.ProgramCounter(*args, src_loc_at=0, **kwargs)

Sentinel RV32I Program Counter.

Unlike most registers, the ProgramCounter (PC) is implemented using sequential logic rather than in RAM, because the value needs to be available as an ALU source on any given clock cycle. See the bottom half of CSRFile documentation for more details. Before microcode takes an action on the PC besides HOLD, the PC points to the currently-executing instruction. After an action is taken, the PC will point to the next instruction address to be executed.

For any given clock cycle, the PC is modified on the next active edge by setting PcAction to a value besides HOLD:

  • If INC is selected, automatically increment the PC by 4 bytes on the next active edge. Physically, the increment is by 1 32-bit word, because the bottom two bits of the PC are unimplemented.

  • If LOAD_ALU_O is selected, load the PC with the current alu output on the next active edge.

ProgramCounter only physically implements the top 30 bits of the register, because the least-significant bits are the PC are always 0 for valid RV32I instructions. Loads to the PC from the alu output will discard the low two bits. With that said, microcode should take care to trigger an exception as if loading a non-zero value to the low bits of the PC would succeed, when appropriate (e.g. JAL and JALR instructions.)

ControlSignature = Signature({'action': Out(<enum 'PcAction'>)})

Program Counter microcode signals.

The signature is of the form

Signature({
    "action": Out(PcAction)
})

where

action: Out(PcAction)

Perform an action on the PC for the current clock cycle.

This is a Signature with a single Member in case I need to expand it.

Type:

Signature

PublicSignature = Signature({'dat_r': In(30), 'dat_w': Out(30), 'ctrl': Out(Signature({'action': Out(<enum 'PcAction'>)}))})

Program Counter interface that is passed to external modules.

The signature is of the form

Signature({
    "dat_r": In(30),
    "dat_w": Out(30),
    "ctrl": Out(ControlSignature)
})

where

dat_r: In(30)

Current value of the Program Counter.

dat_w: Out(30)

Value to write to the Program Counter from the ALU output this clock cycle, if action is LOAD_ALU_O.

ctrl: Out(ControlSignature)

Hold the Program Counter at the current value, increment it, or load it from the ALU this clock cycle.

type: Out(ControlSignature)

Type:

Signature

elaborate(platform)
class sentinel_cpu.datapath.RegFile(*args, src_loc_at=0, **kwargs)

RV32I General Purpose Registers (GP) register file and backing RAM.

The RegFile Component provides an Interface to read and write the 32 RV32I GP regs. The GP reg file is implemented as a 64x32 (e.g. 64 words, 32-bits per word) psuedo/simple dual-port synchronous RAM; on any given clock cycle, the core can do one read and one write to the RAM at independent addresses. Values are written to the RAM and appear on its read port on the subsequent active edge. The backing RAM is transparent; on a given clock, if the core reads and writes the same address, the newly-written value will appear on the read port on the next active edge.

Note

It is an implementation detail that the backing RAM is transparent. I’m not even sure I depend on it much. I’ll have to run the tests without it and see what fails; it might be a net size win if I can make non-transparent work :).

The backing RAM also holds Sentinel’s implemented RISC-V Configuration and Status Registers (CSRs) in its top 32 words. The synchronous RAM’s most-significant address line (A5) is morally equivalent to a “chip select”; A5=0 chooses one 32-word GP reg file RAM chip and A5=1 chooses a separate 32-word CSR reg file RAM, and their data lines are muxed together to read/write only one RAM at a time. See CSRFile for rationale on what I called shared RAM. Note that CSR addresses in CSRFile are relative to address 0x20 in the shared RAM.

RegFile snoops CSR accesses to decide which half of the shared RAM to access each clock cycle. If a given microinstruction tries to access the GP reg and CSR half simultaneously (e.g. if CSROp is not NONE), the CSR access takes priority.

Note

In the future, it is possible the top-half of the backing RAM will also be used for scratch space by the microprogram, though at present I do no such thing.

Unlike many RISC-V implementations, Sentinel does not have a hardwired zero register (x0). Rather, memory address 0 holds the “value” of x0. For a small core like Sentinel, x0 in RAM saves a lot of logic that would otherwise have to choose between a hardwired x0 and the backing memory. It is microcode’s responsibility to initialize address 0 to value 0 using allow_zero_wr before the first macroinstruction begins processing. If allow_zero_wr is not asserted on a given clock cycle, writes to address 0 are ignored, which implements RISC-V instruction semantics.

Parameters:

formal (bool) – Presently unused.

formal

Presently unused.

Type:

bool

m_data

In simulation, use this attribute to get access to the shared register file contents.

Type:

MemoryData

ControlSignature = Signature({'reg_read': Out(1), 'reg_write': Out(1), 'allow_zero_wr': Out(1)})

GP reg microcode control signals.

The signature is of the form

 Signature({
    "reg_read": Out(1),
    "reg_write": Out(1),
    "allow_zero_wr": Out(1),
})
reg_read: Out(1)

Perform a read from the GP reg file at adr_r this cycle. Data is valid on the next active edge.

reg_write: Out(1)

Perform a write from the GP reg file at adr_w this cycle. Data is written on the next active edge.

allow_zero_wr: Out(1)

By default, writes to address 0 are ignored; qualify reg_write with this signal to bypass that restriction for this clock cycle.

Type:

Signature

RoutingSignature = Signature({'reg_r_sel': Out(<enum 'RegRSel'>), 'reg_w_sel': Out(<enum 'RegWSel'>)})

Useful microcode signals concerned with routing to GP regs.

RegFile does not directly use this Signature; it is provided for convenience to route data sources to itself.

The signature is of the form

Signature({
    "reg_r_sel": Out(RegRSel),
    "reg_w_sel": Out(RegWSel),
})

where

reg_r_sel: Out(RegRSel)

Select a read address for RegFile.

reg_w_sel: Out(RegRSel)

Select a write address for RegFile.

Type:

Signature

PublicSignature = Signature({'adr_r': Out(5), 'adr_w': Out(5), 'dat_r': In(32), 'dat_w': Out(32), 'ctrl': Out(Signature({'reg_read': Out(1), 'reg_write': Out(1), 'allow_zero_wr': Out(1)}))})

GP register interface that is passed to external modules.

The signature is of the form

Signature({
    "adr_r": Out(5),
    "adr_w": Out(5),
    "dat_r": In(32),
    "dat_w": Out(32),
    "ctrl": Out(ControlSignature)
})

where

adr_r: Out(5)

GP Register address to read.

adr_w: Out(5)

GP Register address to write.

dat_r: In(32)

Data read from register specified by adr_r. Valid on the next active edge after adr_r is presented and reg_read is asserted

dat_w: In(32)

Data written to register specified by adr_w. Valid on the next active edge after adr_w is presented and reg_write is asserted.

ctrl: Out(ControlSignature)

Choose whether to perform a register read, write, or both. Writes to address 0 are ignored unless allow_zero_wr is asserted.

Type:

Out(ControlSignature)

Type:

Signature

_PrivateCSRAccessSignature = Signature({'adr': Out(5), 'dat_r': In(32), 'dat_w': Out(32), 'op': Out(<enum 'CSROp'>)})

Private interface to control accessing CSR regs stored in GP RAM.

This Signature is private, but documented for completeness/as a courtesy.

The signature is of the form

Signature({
    "adr": Out(5),
    "dat_r": In(32),
    "dat_w": Out(32),
    "op": Out(CSROp)
})

where

adr: Out(5)

CSR address being read, zero-extended to 5-bits, relative to address 0x20 in the shared RAM.

dat_r: In(5)

Data of CSR at address adr being read.

dat_w: Out(32)

Data of CSR at address adr being written.

csr_op: Out(CSROp)

CSR operation requested by CSRFile this cycle, if any.

pub: In(Signature({'adr_r': Out(5), 'adr_w': Out(5), 'dat_r': In(32), 'dat_w': Out(32), 'ctrl': Out(Signature({'reg_read': Out(1), 'reg_write': Out(1), 'allow_zero_wr': Out(1)}))}))

Public bus exposed to external modules.

Type:

In(PublicSignature)

priv: In(Signature({'adr': Out(5), 'dat_r': In(32), 'dat_w': Out(32), 'op': Out(<enum 'CSROp'>)}))

Private bus used to snoop accesses to CSRFile.

Type:

In(_PrivateCSRAccessSignature)

elaborate(platform)
class sentinel_cpu.datapath.CSRFile(*args, src_loc_at=0, **kwargs)

Control and Status Registers register file controller.

As hinted at by several microcode fields, CSRFile and RegFile share a single memory resource defined in RegFile; GP regs use the bottom half, and CSRs use the top half. This Component masquerades as a register file whose control signals are completely independent from RegFile, and generates the signals required to access the top half of the shared register memory. See MSTATUS, etc.

CSRFile and RegFile sharing the same block RAM was originally supposed to be an implementation detail. The current Interface attempts to hide these details, but the final design ended up more with a more tightly-coupled CSRFile and RegFile than I originally wanted. Right now, only a GP reg or a CSR reg op can happen on a given clock cycle, but not both.

Todo

I can likely relax this restriction without much extra logic (maybe even less logic!), but I haven’t decided what guarantees I’m willing to provide as of 1/10/2025.

Note that CSR registers are not necessarily stored sequentially in the top-half of block RAM. Rather, I store them at addresses that minimize the amount of logic to map the 12-bit CSR addresses to 4-bits.

MStatus, MIP, and MIE regs are special in that, similar to the ProgramCounter, they must be readable or writable on any given clock cycle for interrupt/exception handling. The block RAM as implemented in the RegFile cannot accomodate registers whose values are always available. Therefore, the above registers are implemented as sequential logic storage elements outside of the shared block RAM. Writes to these registers go to both the physical logic and the block RAM, while reads are always from the dedicated logic for these registers.

Note

In principle, one can add dedicated read/write ports for each register (i.e. fixed address) to the shared block RAM to implement registers which can always be written/read in a shared memory. In my opinion, this is far more complex than sequential logic and muxing, and it definitely requires more logic and RAM resources to implement!

If external logic asserts bits of MIP on the same cycle as the CPU attempts to clear those same bits, the external logic takes priority to avoid lost interrupts.

Note

As of 4/11/2026, only MIP.MEIP is physically implemented, and RISC-V mandates MIP.MEIP to be read-only. “External value takes priority” will apply to any future bits of MIP that I implement (MIP.MSIP, MIP.MTIP, the top 16 bits of MIP, etc).

ControlSignature = Signature({'op': Out(<enum 'CSROp'>), 'exception': Out(<enum 'ExceptCtl'>)})

CSR microcode control signals.

The signature is of the form

 Signature({
     "op": Out(CSROp),
     "exception": Out(ExceptCtl)
})
op: Out(CSROp)

CSR operation to perform this cycle, if any.

exception: Out(ExceptCtl)

If set to ENTER_INT, set MStatus.MPIE to MStatus.MIE, and set MStatus.MIE to 0.

If set to LEAVE_INT, set MStatus.MIE to MStatus.MPIE, and set MStatus.MPIE to 1.

Other values have no effect on CSRFile.

Type:

Signature

RoutingSignature = Signature({'csr_sel': Out(<enum 'CSRSel'>), 'target': Out(unsigned(8))})

Useful microcode signals concerned with routing to CSRs.

CSRFile does not directly use this Signature; it is provided for convenience to route data sources to itself.

The signature is of the form

Signature({
    "csr_sel": Out(CSRSel),
    "target": Out(Target)
})

where

csr_sel: Out(CSRSel)

Select a source from where to get a CSR address for CSRFile.

target: Out(Target)

The Target microcode field. Used as an explicitly-specified CSR address if TRG_CSR is selected.

Type:

Out(Target)

Type:

Signature

PublicSignature = Signature({'adr': Out(5), 'dat_r': In(32), 'dat_w': Out(32), 'ctrl': Out(Signature({'op': Out(<enum 'CSROp'>), 'exception': Out(<enum 'ExceptCtl'>)})), 'mstatus_r': In(<class 'sentinel_cpu.csr.MStatus'>), 'mip_w': Out(<class 'sentinel_cpu.csr.MIP'>), 'mip_r': In(<class 'sentinel_cpu.csr.MIP'>), 'mie_r': In(<class 'sentinel_cpu.csr.MIE'>)})

CSR register interface that is passed to external modules.

The signature is of the form

Signature({
    "adr": Out(5),
    "dat_r": In(32),
    "dat_w": Out(32),
    "ctrl": Out(ControlSignature),

    "mstatus_r": In(MStatus),
    "mip_w": Out(MIP),
    "mip_r": In(MIP),
    "mie_r": In(MIE),
})

where

adr: Out(5)

CSR Register address or write.

dat_r: In(32)

Data read from register specified by adr. Valid on the next active edge after adr is presented and ctrl is set to READ_CSR.

dat_w: Out(32)

Data written to register specified by adr. Valid on the next active edge after adr is presented and ctrl is set to WRITE_CSR.

ctrl: Out(ControlSignature)

Choose whether to perform a CSR register read or write this clock cycle. Also save and restore MStatus when entering and leaving an exception handler.

Type:

Out(ControlSignature)

mstatus_r: Out(MStatus)

Current read value of the MStatus register.

mip_w: Out(MIP)

Value of the MIP register, direct from the interrupt lines (currently only irq).

mip_r: Out(MIP)

Current read value of the MIP register. Right now, combinationally equivalent to mip_w.

Note

In the future, I might make mip_r a registered version of mip_w. I don’t quite remember the rationale of making mip_r a pass-through (probably saved space).

mie_r: Out(MIE)

Current read value of the MIE register.

Type:

Signature

pub: In(Signature({'adr': Out(5), 'dat_r': In(32), 'dat_w': Out(32), 'ctrl': Out(Signature({'op': Out(<enum 'CSROp'>), 'exception': Out(<enum 'ExceptCtl'>)})), 'mstatus_r': In(<class 'sentinel_cpu.csr.MStatus'>), 'mip_w': Out(<class 'sentinel_cpu.csr.MIP'>), 'mip_r': In(<class 'sentinel_cpu.csr.MIP'>), 'mie_r': In(<class 'sentinel_cpu.csr.MIE'>)}))

Public bus exposed to external modules.

Type:

In(PublicSignature)

priv: Out(Signature({'adr': Out(5), 'dat_r': In(32), 'dat_w': Out(32), 'op': Out(<enum 'CSROp'>)}))

Request to RegFile for CSR values stored in shared RAM.

Type:

Out(RegFile._PrivateCSRAccessSignature)

MSTATUS = 0

MStatus CSR address in the shared register file, relative to its top-half. Register is implemented as sequential logic, but a reg file address is required for decoding.

MIE = 4

MIE CSR address in the shared register file, relative to its top-half. Register is implemented as sequential logic, but a reg file address is required for decoding.

MTVEC = 5

MTVEC CSR address in the shared register file, relative to its top-half.

MSCRATCH = 8

MSCRATCH CSR address in the shared register file, relative to its top-half.

MEPC = 9

MEPC CSR address in the shared register file, relative to its top-half.

MCAUSE = 10

MCause CSR address in the shared register file, relative to its top-half.

MIP = 12

MIP CSR address in the shared register file, relative to its top-half. Register is implemented as sequential logic, but a reg file address is required for decoding.

elaborate(platform)
class sentinel_cpu.datapath.DataPathSrcMux(*args, src_loc_at=0, **kwargs)

Route decoded instruction and microcode control to DataPath.

Todo

DataPathSrcMux is analogous to ASrcMux and BSrcMux for the ALU. However, I couldn’t get it to optimize well compared to putting the logic directly in Top. I keep it around as “dead code” just in case in comes in handy later. If/when that time comes, I’ll properly document it.

elaborate(platform)
class sentinel_cpu.datapath.DataPath(*args, src_loc_at=0, **kwargs)

Public interface to Sentinel datapath.

This module forwards the public interfaces to the various register types. It is completely combinational logic.

Parameters:

formal (bool) – Presently unused.

formal

Presently unused.

Type:

bool

pc_mod
Type:

ProgramCounter

regfile
Type:

RegFile

csrfile
Type:

CSRFile

gp: In(Signature({'adr_r': Out(5), 'adr_w': Out(5), 'dat_r': In(32), 'dat_w': Out(32), 'ctrl': Out(Signature({'reg_read': Out(1), 'reg_write': Out(1), 'allow_zero_wr': Out(1)}))}))
csr: In(Signature({'adr': Out(5), 'dat_r': In(32), 'dat_w': Out(32), 'ctrl': Out(Signature({'op': Out(<enum 'CSROp'>), 'exception': Out(<enum 'ExceptCtl'>)})), 'mstatus_r': In(<class 'sentinel_cpu.csr.MStatus'>), 'mip_w': Out(<class 'sentinel_cpu.csr.MIP'>), 'mip_r': In(<class 'sentinel_cpu.csr.MIP'>), 'mie_r': In(<class 'sentinel_cpu.csr.MIE'>)}))
pc: In(Signature({'dat_r': In(30), 'dat_w': Out(30), 'ctrl': Out(Signature({'action': Out(<enum 'PcAction'>)}))}))
elaborate(platform)

Microcode ROM and Control

Microcode ROM assembly and Component.

class sentinel_cpu.ucoderom.UCodeROM(*args, src_loc_at=0, **kwargs)

Microcode ROM assembly and Component.

UCodeROM takes a microcode assembly file as input, parses the assembly file, creates a Memory to hold the assembled program, and dynamically creates a Signature which splits out all microcode fields.

Note

I have not personally tried using alternate microcodes. Because most of Sentinel uses ucodefields in some capacity, I generally expect that alternate microcodes will simply be extensions to the main microcode file.

This can be done, for instance, by copying and extending both microcode.asm and the default field_map. I may optimize the API for this extension use case after I get a better idea of how to support different Sentinel microcodes/profiles.

Parameters:
  • main_file (Path, optional) – Path to microcode file to assemble into ROM. If not supplied, use main_microcode_file().

  • field_defs (Path, optional) – Path to write field definitions extracted from assembly file.

  • hex (Path, optional) – Path to write contents of microcode ROM in hex output.

  • field_map (dict, optional) – Alternate field_map to use to generate a Signature. By default, use a field_map corresponding to main_microcode_file().

addr

Address bus. Width is determined by microcode assembly file, which also initializes the otherwise-private self.depth.

The default microcode file has an address space depth of 256 (1 byte).

Type:

In(ceil_log2(self.depth))

fields

Microcode field data output. StructLayout is determined by the microcode assembly file. A default fields layout will look like this:

StructLayout({'target': unsigned(8),
              'jmp_type': <enum 'JmpType'>,
              'cond_test': <enum 'CondTest'>,
              'invert_test': unsigned(1),
              'pc_action': <enum 'PcAction'>,
              'latch_a': unsigned(1),
              'latch_b': unsigned(1),
              'a_src': <enum 'ASrc'>,
              'b_src': <enum 'BSrc'>,
              'alu_op': <enum 'OpType'>,
              'alu_i_mod': <enum 'ALUIMod'>,
              'alu_o_mod': <enum 'ALUOMod'>,
              'reg_read': unsigned(1),
              'reg_write': unsigned(1),
              'reg_r_sel': <enum 'RegRSel'>,
              'reg_w_sel': <enum 'RegWSel'>,
              'csr_op': <enum 'CSROp'>,
              'csr_sel': <enum 'CSRSel'>,
              'mem_req': unsigned(1),
              'mem_sel': <enum 'MemSel'>,
              'mem_extend': <enum 'MemExtend'>,
              'latch_adr': unsigned(1),
              'latch_data': unsigned(1),
              'write_mem': unsigned(1),
              'insn_fetch': unsigned(1),
              'except_ctl': <enum 'ExceptCtl'>})

The default microcode file has a data width of 48 bits (6 bytes).

Type:

Out(StructLayout)

static main_microcode_file()

Return the default microcode file path.

The default microcode, titled microcode.asm, is supplied with Sentinel’s source code/package in the same directory as this file.

Returns:

Absolute path to microcode file supplied with Sentinel.

Return type:

Path

field_map: dict

Map of strings to ShapeLike, which are verified against the fields supplied in the microcode assembly file.

Each Enum class should have values in UPPER_CASE corresponding to an equivalent m5meta enum whose values are lower_case.

elaborate(platform)
assemble()

Verify and assemble the associated microcode source file.

Internally calls m5meta’s assemble function and verifies that the assembly file matches field_map.

Raises:

ValueError – If the assembly file uses multiple address spaces or its enums cannot be mapped to field_map.

Sentinel control unit implementation and microcode ROM wrapper.

class sentinel_cpu.control.MappingROM(*args, src_loc_at=0, **kwargs)

Sentinel Microcode Mapping ROM.

MappingROM is a hardware jump table that takes in 32-bit RISC-V instructions and maps them down to one of 256 addresses in UCodeROM. In addition, if MappingROM detects a CSR instruction (logic duplicated from ExceptionControl), it will map the CSR address field down to one of 16 addresses in much the same way.

For non-CSR instructions, the target UCodeROM address for the instruction will be available on requested_op on the first active edge after start is asserted. If MappingROM detects that the current instruction is a CSR instruction, mapping takes two cycles:

  • On the first active edge after start is asserted, requested_op will point to a temporary location that the microcode program jump to this cycle (0x24, as of 1/6/2025).

    In addition, csr_encoding- derived from the funct12 instruction field- will be valid at this point.

  • On the second active edge after start is asserted, the actual target address for the CSR instruction will be available on requested_op.

To use the latched address in requested_op, set the Sequencer to use JmpType.MAP via microcode on the clock cycle immediately after start is asserted.

Logically, MappingROM is part of Control. Physically, it’s part of Decode for space optimization reasons.

start: In(1)

If asserted, perform mapping this cycle. Mapping outputs will be valid either on the first active edge (non-CSR) or next two active edges (CSR instruction) after start is asserted.

This Signal is equivalent to do_decode.

insn: In(32)

The current RISC-V instruction, the same Signal as Decode.insn.

csr_attr: In(StructLayout({'ill': unsigned(1), 'ro0': unsigned(1)}))

CSR Attribute information for the current instruction from from the CSRAttributes look-up table.

The StructLayout matches that of CSRAttributes._DataLayout; the ad-hoc StructLayout object avoids circular-dependency import issues.

Type:

In(StructLayout)

requested_op: Out(8)

Mapped jump target into UCodeROM, calculated from insn. Valid on the first active edge after start is asserted. Also valid on the second active edge after start assertion for CSR instructions.

csr_encoding: Out(4)

Mapped CSR address output, calculated from insn. Valid on the first active edge after assertion of start, but only if MappingROM detects that the current instruction is, in fact, a CSR instruction.

elaborate(platform)
class sentinel_cpu.control.Control(*args, src_loc_at=0, **kwargs)

Sentinel Control Unit.

The Sentinel Control Unit consists of three parts:

In principle MappingROM is also part of the Control Unit. However, I found it to be a space win to tightly couple it to the Decode.

This Component is pure combinational logic. Beyond connecting the above parts together, Control propagates microcode ROM control signals to the rest of the core. In turn, it snoops control and data signals from other parts of the core to drive the microcode program forward.

Several interface Members of Control have Signatures of the form:

Signature({
    "route": Out(RoutingSignature),
    "ctrl": Out(ControlSignature)
})

RoutingSignature and ControlSignature are class variable Signature placeholders:

  • RoutingSignatures contain routing information to and from the containing Component.

  • ControlSignatures modify the containing Component's behavior, typically per clock cycle.

RoutingSignatures especially are subject to change as I improve how Sentinel is organized.

Parameters:

ucode (Optional[TextIO] = None) – Microcode file to assemble and load. By default, use main_microcode_file().

alu

ALU control signals. The signature is of the form

Signature({
    "route": Out(ALU.RoutingSignature),
    "ctrl": Out(ALU.ControlSignature)
})

See ALU.RoutingSignature and ALU.ControlSignature.

Type:

Out(Signature)

decode

Decode control signals. The signature is of the form

Signature({
    "opcode": Out(OpcodeType),
    "requested_op": Out(8),
})

where

opcode: Out(OpcodeType)

Major opcode from the decoded instruction.

requested_op: Out(8)

Decoded microcode program address, calculated from MappingROM. For use with Sequencer.opcode_adr.

Type:

In(Signature)

gp

RegFile control signals. The signature is of the form

Signature({
    "route": Out(RegFile.RoutingSignature),
    "ctrl": Out(RegFile.ControlSignature)
})

See RegFile.RoutingSignature and RegFile.ControlSignature.

Type:

Out(Signature)

pc

ProgramCounter control signals.

Type:

Out(ProgramCounter.ControlSignature)

csr

CSRFile control signals. The signature is of the form

Signature({
    "route": Out(CSRFile.RoutingSignature),
    "ctrl": Out(CSRFile.ControlSignature)
})

See CSRFile.RoutingSignature and CSRFile.ControlSignature.

Type:

Out(Signature)

mem

Memory transaction control signals. The signature is of the form

Signature({
    "req": Out(MemReq),
    "sel": Out(MemSel),
    "valid": In(1),
    "write": Out(WriteMem),
    "insn_fetch": Out(InsnFetch),
    "extend": Out(MemExtend),
    "latch_data": Out(LatchData),
    "latch_adr": Out(LatchAdr)
})

where

req: Out(MemReq)

Request/start a memory transfer.

Type:

Out(MemReq)

sel: Out(MemSel)

Select width of memory transfer.

valid: In(1)

When asserted, the memory transfer (read or write) has finished this cycle.

write: Out(WriteMem)

When asserted, the current memory transfer is a write.

Type:

Out(WriteMem)

insn_fetch: Out(InsnFetch)

When asserted, the current memory transfer is an instruction fetch.

Type:

Out(InsnFetch)

extend: Out(MemExtend)

For read memory transfers less than data bus width, either zero-extend or sign-extend the read data to fill the entire word.

latch_data: Out(LatchData)

Latch the aligned write data into an internal register which drives the external write data bus (DAT_O).

Type:

Out(LatchData)

latch_adr: Out(LatchAdr)

Latch the ALU output into an internal register which indirectly drives the address bus (ADR_O) and select (SEL_O) signals.

Type:

Out(LatchAdr)

Type:

Out(Signature)

exception

ExceptionRouter control signals.

Type:

Out(ExceptionRouter.ControlSignature)

ucoderom
Type:

UCodeROM

sequencer
Type:

Sequencer

elaborate(platform)
class sentinel_cpu.control.Sequencer(*args, src_loc_at=0, **kwargs)

Microprogram address generation. See sequencer.

Sequencer generates the address of the next microinstruction to execute using the semantics explained in JmpType. It also implicitly contains the microprogram counter (upc), accessed via CONT. On reset, the upc is reset to 2, and stays at 2 until reset is released.

Warning

Because the upc has “points to next-cycle instruction” semantics, the microinstruction driving the CPU is undefined for a single clock cycle after reset is explicitly asserted (i.e. non-power-on-reset). This may result in undesirable side effects that require extra guard logic or microcode to quash. Bugs I’ve found include:

  • WB_CYC and WB_STB do not deassert on the first cycle after reset (fixed by guard logic).

  • Initial instruction fetch uses address 4 instead of 0 (fixed in microcode).

I have basic tests for flushing out these types of bugs, and to the best of my knowledge (1/3/2025), all of the reset bugs I encountered have been fixed. But I need to create a verification step to characterize all possible reset behavior to flush out other possible issues. For now, if you find a reset bug, please let me know and try to reproduce, as this is the likely cause.

AFAICT, none of the above applies for power-on-reset; your target toolchain should provide logic so that the microinstruction at 2 is driving the CPU on power-on-reset. And even then, a power-on-reset circuit should be holding the design in reset for more than 1 clock cycle, which prevents at least some (all?) of the bad side-effects :).

Parameters:

ucoderom (UCodeROM) – Microcode program ROM to which to connect the Sequencer.

target

Target microinstruction address microcode field, used by the sequencer for DIRECT and DIRECT_ZERO.

Type:

Signal(Target)

jmp_type

Select the next microinstruction address to place in adr. Next addresses are calculated in parallel for all possible JmpTypes, and are selected/muxed here.

Type:

Signal(JmpType)

adr

Address of the next microinstruction to execute, subject to the input Signals.

Type:

Signal(Target)

opcode_adr

Address calculated from MappingROM, used for MAP. It typically contains a microprogram address for handling the currently-executing macroinstruction.

Type:

Signal(Target)

vec_adr

Currently unused signal for possible future expansion.

Type:

Signal(Target)

test

If set, the condition test described by the current value of CondTest succeeded this cycle. This in turn affects adr, depending on jmp_type.

Type:

Signal(1)

elaborate(platform)

Mapping Details

At present (1/6/2025), I mostly hand-calculated the MappingROM jump table. Many RV32I instruction bits can be reconstructed by other microcode fields after decoding, such as operand sources and immediates. From there, I found a reasonably small map in terms of combinational logic by playing with the remaining instruction bits- mostly major and minor opcodes- in a text file:

LOAD =      0b00001000 0x08
            0b00001001 0x09
            0b00001010 0x0A
            0b00001100 0x0C
            0b00001101 0x0D
MISC_MEM =  0b00110000 0x30
OP_IMM =    0b01000000 0x40
            0b01000010 0x42
            0b01000011 0x43
            0b01000100 0x44
            0b01000110 0x46
            0b01000111 0x47
            0b01000001 0x41
            0b01000101 0x45
            0b01001101 0x4D
AUIPC =     0b01010000 0x50
STORE =     0b10000000 0x80
            0b10000001 0x81
            0b10000010 0x82
BRANCH =    0b10001000 0x88
            0b10001001 0x89
            0b10001100 0x8C
            0b10001101 0x8D
            0b10001110 0x8E
            0b10001111 0x8F
JALR =      0b10011000 0x98
JAL =       0b10110000 0xB0
OP =        0b11000000 0xC0
            0b11000001 0xC1
            0b11000010 0xC2
            0b11000011 0xC3
            0b11000100 0xC4
            0b11000101 0xC5
            0b11001101 0xCD
            0b11000110 0xC6
            0b11000111 0xC7
            0b11001000 0xC8
SYSTEM =    0b11000000 0xC0 (handled specially)
            0b11000000 0xC0 (handled specially)
LUI =       0b11010000 0xD0

CSRs are placed wherever they fit; I chose 0x24 as a starting point.

CSR compression relies on the fact that Sentinel doesn’t actually implement most CSRs, and so their addresses can be treated as blanket don’t cares:

mstatus   0x300 => 0b001100000000 => 0bxxxxx0xxx000 => 0b0000 - ffs
mie       0x304 => 0b001100000100 => 0bxxxxx0xxx100 => 0b0100 - ffs
mtvec     0x305 => 0b001100000101 => 0bxxxxx0xxx101 => 0b0101 - bram
mscratch  0x340 => 0b001101000000 => 0bxxxxx1xxx000 => 0b1000 - bram
mepc      0x341 => 0b001101000001 => 0bxxxxx1xxx001 => 0b1001 - bram
mcause    0x342 => 0b001101000010 => 0bxxxxx1xxx010 => 0b1010 - bram
mip       0x344 => 0b001101000100 => 0bxxxxx1xxx100 => 0b1100 - ffs

Of course, if I physically implement more registers, the table will need to change, up to and including using more than 16 addresses :).

Todo

  • Perhaps include rest of the raw notes on how I derived start locations from opcodes; right now only the results are included.

  • Start locations need to be documented as constants, including “base” constants where the minor opcode is just added to the base to form the final constant.

Instruction Decoder

Sentinel RV32I_Zicsr instruction decoder implementation.

class sentinel_cpu.decode.CSRAttributes(*args, src_loc_at=0, **kwargs)

Look-Up Table for CSR access control.

Rather than explicitly use combinational circuitry, I check whether the CSR at a given input address is implemented, illegal to access, read-only zero, or has other properties through a large Switch.

Logic synthesizers are free to optimize this down to a block memory, logic, or whatever implementation is most desirable.

This table is only valid for Machine Mode CSRs at present; ExceptionControl handles trapping on CSRs outside of Machine Mode.

Note

I have no plans to support Supervisor Mode, but User Mode might be worthwhile in the future.

class _DataLayout(target)

Useful private module-local binding for CSR access control.

Any fields added should nominally be one-hot, where zero is allowed and represents “implemented and no special restrictions”.

Todo

Right now, this isn’t actually private; it is a leaky abstraction because e.g. an ad-hoc compatible Layout is created by sentinel_cpu.control.MappingROM.

ill: unsigned(1)

Set if CSR address is illegal.

ro0: unsigned(1)

Set if CSR is read-only zero.

addr: In(12)

12-bit CSR address to query.

data: Out(<class 'sentinel_cpu.decode.CSRAttributes._DataLayout'>)

Information on queried CSR register.

Data will be valid on the active clock edge after addr is received.

Type:

Out(_DataLayout)

elaborate(platform)
class sentinel_cpu.decode.ImmediateGenerator(*args, src_loc_at=0, **kwargs)

Decode the immediate field from a RISC-V instruction.

When enable is asserted, this Component examines an input instruction's OpcodeType, and extracts the correct sign-extended 32-bit immediate encoded in that OpcodeType. The immediate is available on imm the active edge after enable is asserted.

RISC-V instructions encode several forms and widths of 32-bit immediate operands, based upon an instruction’s major opcode. ImmediateGenerator abstracts this away with a straightforward “decode all immediate types simultaneously, select the correct immediate type based on opcode via a mux, and latch the mux output” impelementation. The mux is inactive if the current instruction doesn’t have an immediate field. Consequently, the output immediate is only valid for certain major opcodes in the input instruction:

Warning

The fact that ImmediateGenerator does currently check whether the current instruction has an immediate field or not is an implementation detail. It does not (intentionally!) forward “instruction contains/does not contain immediate” information to other parts of Sentinel via e.g. interface Members.

Assuming that the current instruction has an immediate field, class:ImmediateGenerator does not (and can not in some cases, really) check whether the immediate will cause an exception. For instance, adding certain immediates to ProgramCounter for JALR is supposed to trigger INSN_MISALIGNED. However, in the case of Sentinel, the immediate generator does not have access to ProgramCounter, and so the core doesn’t know whether a INSN_MISALIGNED exception will trigger until a later clock cycle (jump target calculation via ALU).

It is the microcode program’s responsibility to know whether the the current instruction has an immediate field at all, and assuming it does, check whether it is valid for the current instruction (e.g. does not cause exceptions).

enable: In(1)

If asserted, generate an immediate from insn his cycle. Output will be valid on the first active edge after enable is asserted.

This Signal is equivalent to do_decode.

insn: In(32)

The current RISC-V instruction, the same Signal as Decode.insn.

imm: Out(32)

The extracted sign-extended 32-bit immediate from insn. Valid on the first active edge after enable is asserted.

elaborate(platform)
class sentinel_cpu.decode.ExceptionControl(*args, src_loc_at=0, **kwargs)

Detect and handle Decode-releated exceptions.

When start is asserted, ExceptionControl examines the current instruction checks whether the input instruction fields are valid. Once start is asserted, exception information will be available on exception on the next active edge for non-CSR instructions and two active edges later for CSR instructions; like MappingROM, ExceptionControl is aware of CSR decode cycles.

ExceptionControl found an exception for the current instruction if it asserts the valid field of the exception amaranth.lib.wiring.Member on any active edge after start assertion (although in practice, exceptions are detected no later than two cycles after start assertion). It can trigger the following exceptions:

This Component should be used with conjunction with Insn and ImmediateGenerator, since neither of those classes check for instruction validity (to save logic).

start: In(1)

If asserted, check for exceptions this cycle. Output will be valid either on the first active edge (non-CSR) or next two active edges (CSR instruction) after start is asserted.

This Signal is equivalent to do_decode.

insn: In(32)

The current RISC-V instruction, the same Signal as Decode.insn.

csr_attr: In(<class 'sentinel_cpu.decode.CSRAttributes._DataLayout'>)

CSR Attribute information for the current instruction from from the CSRAttributes look-up table.

Type:

In(_DataLayout)

exception: Out(<class 'sentinel_cpu.exception.DecodeException'>)

If asserted, an exception was detected for this instruction. Valid on the first active edge after start is asserted for non-CSR instructions; valid on the second active edge after start assertion for CSR instructions.

Type:

Out(DecodeException)

elaborate(platform)
class sentinel_cpu.decode.Decode(*args, src_loc_at=0, **kwargs)

Full RV32I_Zicsr decoder implementation.

When do_decode is asserted, the RISC-V instruction present on insn Signal is decoded and split onto the remaining Members of the decoder interface.

Parameters:

formal (bool) – If True, the decoder will gain an extra rvfi port Member with signals relevant to implementing RVFI.

formal

If True, rvfi Member is present.

Type:

bool

do_decode

When asserted, do instruction decoding. Some Out Members in this interface are only valid are on the first or second active edge after do_decode is asserted.

Type:

In(1)

insn

Bit pattern of the current RISC-V instruction. It is an unregisted pass-through of Wishbone signal DAT_I.

Type:

In(32)

src_a_unreg

First source operand of the current instruction. Immediately valid once insn is stable without waiting for do_decode or an active edge.

With an unregistered source, Sentinel can preemptively load the first source operand, even if the current instruction is not valid!

Type:

Out(5)

src_a

First source operand of the current instruction. Valid on the active edge after do_decode is asserted.

Type:

Out(5)

src_b

Second source operand of the current instruction. Valid on the active edge after do_decode is asserted.

Type:

Out(5)

imm

Calculated immediate value of the current instruction. Valid on the active edge after do_decode is asserted. Valid only if the current instruction actually has an immediate field.

Type:

Out(32)

dst

Destination operand of the current instruction. Valid on the active edge after do_decode is asserted. Valid only if the current instruction actually has a register destination field.

Type:

Out(5)

opcode

Major opcode of the current instruction. Immediately valid once insn is stable without waiting for do_decode or an active edge.

Type:

Out(OpcodeType)

exception

Exception information related to decoding, used by ExceptionRouter. Can trigger ILLEGAL_INSN, ECALL_MMODE, and BREAKPOINT.

For CSR instructions, exception is valid two active edges after do_decode asserts. For all other instructions, exception is valid on the active edge after do_decode is asserted. Other components and the microcode program need to be (and are!) aware of the variable latency.

Type:

Out(DecodeException)

requested_op

Microcode address generated by MappingROM. The Sequencer uses this address to jump to macroinstruction-specific microcode.

Valid on the first active edge after do_decode is asserted. Additionally valid on the second active edge after do_decode is asserted for CSR instructions. On CSR instructions, the microcode program should be prepared to jump to a temporary location on the first active edge; on the second active edge, requested_op will hold a jump address to CSR-specific macroinstruction handling.

Type:

Out(8)

csr_encoding

Mapped CSR address generated by MappingROM. Valid on the first active edge after do_decode asserts if the current instruction is a CSR instruction.

Type:

Out(4)

rvfi

Internal connections required for implementing the RISC-V Formal Interface. Forwarded to Top. This sub-interface is purely combinational logic, and does not rely on do_decode.

The signature is of the form

Signature({
    "rs1": Out(5),
    "rs2": Out(5),
    "rd": Out(5),
    "rd_valid": Out(1),
    "do_decode": Out(1),
    "funct12": Out(12),
    "funct3": Out(3),
    "insn": Out(32),
})

where

rs1: Out(5)

Unregistered copy of src_a.

rs2: Out(5)

Unregistered copy of src_b.

rd: Out(5)

Unregistered copy of dst.

rd_valid: Out(1)

When asserted, indicates rd is valid for the current instruction.

do_decode: Out(5)

Copy of do_decode.

funct12: Out(12)

funct12 field of current instruction.

funct3: Out(3)

funct3 field of current instruction.

insn: Out(32)

Copy of insn.

Type:

Out(Signature)

csr_attr: CSRAttributes

imm_gen: ImmediateGenerator

mapping: ~sentinel_cpu.control.MappingROM

except_ctrl: ExceptionControl

elaborate(platform)

Instruction View classes.

This file is used to avoid circular import problems I had when using it directly in sentinel_cpu.decode (IIRC).

class sentinel_cpu.insn.Insn(value)

View of all immediately-apparent information in a RISC-V instruction.

“Immediately-apparent” means “I can get this info with concatenation, slices and replicates”. This class is morally equivalent to a View for the 32-bit Signal representing a RISC-V instruction. However, it does not inherit from View because Layouts are not designed to retrieve non-contiguous bits.

Parameters:

value (Value) – Raw value to interpret as a RISC-V instruction.

raw

The raw instruction value.

Type:

Value

imm

A moral equivalent to a View for extracting RISC-V immediate values from raw.

Type:

Imm

csr

A moral equivalent to a View for extracting CSR information from raw.

Type:

CSR

ECALL = 115

Bit pattern for ECALL instruction.

EBREAK = 1048691

Bit pattern for EBREAK instruction.

MRET = 807403635

Bit pattern for MRET instruction.

WFI = 273678451

Bit pattern for WFI instruction.

class Imm(value)

Extract a RISC-V immediate value from an instruction.

This class does not (and can not) check whether the given instruction contains an immediate field or whether the requested immediate type is correct. The user is expected to known which immediate type to use using external logic. See ImmediateGenerator for an example.

Parameters:

value (Value) – Raw value to interpret as a RISC-V immediate.

raw

The raw instruction value.

Type:

Value

sign
Type:

Value

property I

Extract an I-type immediate.

Type:

Value

property S

Extract an S-type immediate.

Type:

Value

property B

Extract a B-type immediate.

Type:

Value

property U

Extract a U-type immediate.

Type:

Value

property J

Extract a J-type immediate.

Type:

Value

class CSR(value)

Extract RISC-V CSR info from an instruction.

This class does not (and can not) check whether the given instruction is a CSR instruction. The user is expected to know a priori that the current instruction is a CSR instruction for results from this class to be valid. See ExceptionControl for an example.

Todo

Constants aren’t meaningfully used that much. Get rid of them and convert uses into properties?

Parameters:

value (Value) – Raw value from which to retrieve CSR info.

raw

The top 12 bits of the raw instruction value.

Type:

Value

RW = 1

Read-Write CSR instruction.

RS = 2

Read-Set CSR instruction.

RC = 3

Read-Clear CSR instruction.

RWI = 5

Read-Write Immediate CSR instruction.

RSI = 6

Read-Set Immediate CSR instruction.

RCI = 7

Read-Clear Immediate CSR instruction.

property addr

Return the CSR address.

Type:

Value

property quadrant

Return the CSR privilege level.

raw is interpreted as a Quadrant.

Type:

EnumView

property access

Return whether the CSR is read-only.

raw is interpreted as an AccessMode.

Type:

EnumView

property opcode

Return the major opcode.

Type:

OpcodeType

property rd

Return the destination register bits.

Type:

Value

property funct3

Return the minor opcode/funct3 bits.

Type:

Value

property rs1

Return the first source register bits.

Type:

Value

property rs2

Return the second source register bits.

Type:

Value

property funct7

Return the funct7 bits.

Type:

Value

property funct12

Return the funct12 bits.

Type:

Value

property sign

Return the sign bit.

Type:

Value

class sentinel_cpu.insn.OpcodeType(*values)

Enumeration of RV32I Major Opcode bit patterns.

OP_IMM = 4

Immediate Op instructions.

LUI = 13

Load Unsigned Immediate.

AUIPC = 5

Add Upper Immediate To Program Counter.

OP = 12

Register Op instructions.

JAL = 27

Jump And Link.

JALR = 25

Jump And Link Register.

BRANCH = 24

Branch instructions.

LOAD = 0

Load instructions.

STORE = 8

Store instructions.

CUSTOM_0 = 2

Unused Major Opcode for future custom instructions.

MISC_MEM = 3

Miscellaneous instructions.

SYSTEM = 28

System instructions.

Arithmetic Logic Unit (ALU)

Arithmetic Logic Unit (ALU) Components.

class sentinel_cpu.alu.ALU(*args, src_loc_at=0, **kwargs)

Basic Arithmetic Logic Unit.

The ALU Performs “A OP B”, where “OP” is chosen by ctrl. More operations can be synthesized from the ones directly supported by sentinel_cpu.ucodefields.OpType by using ctrl modifiers.

Parameters:

width (int) – Width in bits of the ALU inputs and output.

a

ALU A input.

Type:

In(width)

b

ALU B input.

Type:

In(width)

o

ALU output. Valid 1 clock cycle after inputs.

Type:

Out(width)

ctrl

Choose the ALU op to perform this cycle, possibly modifying the input or output. Also check if the ALU op on the previous cycle was 0.

Type:

In(ControlSignature)

ControlSignature = Signature({'op': Out(<enum 'OpType'>), 'imod': Out(<enum 'ALUIMod'>), 'omod': Out(<enum 'ALUOMod'>), 'zero': In(1)})

ALU microcode signals and useful state.

The signature is of the form

Signature({
    "op": Out(OpType),
    "imod": Out(ALUIMod),
    "omod": Out(ALUOMod),
    "zero": In(1)
})

where

op: Out(OpType)

ALU operation to perform this cycle.

imod: Out(ALUIMod)

Modify the inputs a and b before doing ALU operation.

omod: Out(ALUOMod)

Modify the output after doing ALU operation, but before latching the ALU output into o (for next cycle).

zero: In(1)

Set if the current output (i.e. the result of the ALU operation done last cycle) is 0.

Type:

Signature

RoutingSignature = Signature({'a_src': Out(<enum 'ASrc'>), 'b_src': Out(<enum 'BSrc'>), 'latch_a': Out(unsigned(1)), 'latch_b': Out(unsigned(1))})

Useful microcode signals concerned with routing to the ALU.

The ALU does not directly use this Signature; it is provided for convenience to route data sources to the ASrcMux and BSrcMux.

The signature is of the form

Signature({
    "a_src": Out(ASrc),
    "b_src": Out(BSrc),
    "latch_a": Out(LatchA),
    "latch_b": Out(LatchB),
})

where

a_src: Out(ASrc)

ASrcMux source to pass through this clock cycle.

b_src: Out(BSrc)

BSrcMux source to pass through this clock cycle.

latch_a: Out(LatchA)

If set, latch the selected source to the ASrcMux output this cycle.

Type:

Out(LatchA)

latch_b: Out(LatchB)

If set, latch the selected source to the BSrcMux output this cycle.

Type:

Out(LatchB)

Type:

Signature

elaborate(platform)

Exception Control

Exception control has not yet been incorporated into the above block diagram.

Exception control classes and Components.

class sentinel_cpu.exception.DecodeException(target)

Exception info from Decode.

This Struct has a very similar purpose to ExceptionRouter.out. In fact, the Layout is the same as the Signature of out! The main differences compared to out is that:

DecodeException physically belongs to Decode, but is placed in the exception module to solve circular import issues.

Todo

Eventually DecodeException should be defined as a nested class under Decode, similar to e.g. ALU.RoutingSignature.

valid: unsigned(1)

If asserted, Decode detected an exception last cycle.

e_type: Cause

Qualified by valid. Indicates the type of exception, if any, that Decode detected last cycle.

Type:

MCause.Cause

class sentinel_cpu.exception.ExceptionRouter(*args, src_loc_at=0, **kwargs)

Detect exceptions throughout the Sentinel core.

RISC-V defines a priority order for exceptions if multiple exceptions occur at the same time. Because Sentinel is microcoded and instructions take multiple cycles, priority checking is deferred to microcode routines. Specifcally, ExceptionRouter only checks for exceptions when qualified by values of ExceptCtl other than NONE; it can only check for one type of exception each clock cycle.

When ExceptCtl is not NONE, ExceptionRouter will output whether the queried exception type occurred immediately. It will latch which specific exception occurred at the next active edge. The exception info latch matches the layout of the MCAUSE register. The mcause port is physically distinct from the MCAUSE register, and so microcode should save the latch value to the actual MCAUSE register as part of exception handling.

While the MCause Struct knows about all currently-defined RISC-V M-Mode exception types, ExceptionRouter can only trigger a subset of exceptions:

ControlSignature = Signature({'mem_sel': Out(<enum 'MemSel'>), 'except_ctl': Out(<enum 'ExceptCtl'>), 'exception': In(1)})

Exception Router microcode signals and useful state.

The signature is of the form

Signature({
    "mem_sel": Out(MemSel),
    "except_ctl": Out(ExceptCtl),
    "exception": In(1)
})

where

op: Out(MemSel)

Memory operation in progress this cycle.

except_ctl: Out(ExceptCtl)

Choose which action ExceptionRouter performs this cycle, such as checking for a specific exception, or entering/leaving the exception handler.

exception: In(1)

Sent back to Control; if asserted, an exception condition was detected this cycle. The cause will be available on the next active edge.

Type:

Signature

src: In(Signature({'alu_lo': Out(2), 'csr': Out(Signature({'mstatus': Out(<class 'sentinel_cpu.csr.MStatus'>), 'mip': Out(<class 'sentinel_cpu.csr.MIP'>), 'mie': Out(<class 'sentinel_cpu.csr.MIE'>)})), 'ctrl': Out(Signature({'mem_sel': Out(<enum 'MemSel'>), 'except_ctl': Out(<enum 'ExceptCtl'>), 'exception': In(1)})), 'decode': Out(<class 'sentinel_cpu.exception.DecodeException'>)}))

Exception router sources.

The signature is of the form

Signature({
    "alu_lo": Out(2),
    "csr": Out(Signature({
        "mstatus": Out(:class:MStatus),
        "mip": Out(MIP),
        "mie": Out(MIE)
    })),
    "ctrl": Out(Signature({
        "mem_sel": Out(MemSel),
        "except_ctl": Out(ExceptCtl)
    })),
    "decode": Out(DecodeException)
})

where

alu_lo: Out(2)

The low 2 bits of the ALU output.

csr: Out(Signature)

Snoop CSR registers containing exception state. See MStatus, MIP, and MIE.

ctrl: Out(Signature)

Snoop microcode signals relevant to exceptions. See MemSel and ExceptCtl.

decode: Out(DecodeException)

Snoop decoder exception state.

Type:

In(Signature)

out: Out(Signature({'exception': Out(1), 'mcause': Out(<class 'sentinel_cpu.csr.MCause'>)}))

Information on current exception.

The signature is of the form

Signature({
    "exception": Out(1),
    "mcause": Out(MCause)
})

where

exception: Out(1)

If asserted, an exception occurred this cycle, and mcause will be valid next cycle.

mcause: Out(MCause)

Qualified by exception on the previous cycle. Indicates the type of exception, if any, which was detected last cycle where ExceptCtl was not NONE. Must be saved by microcode if the value is needed, as this is not meant to hold the MCAUSE register.

Type:

Out(Signature)

elaborate(platform)

ALU Sources

The ALU’s two inputs A and B are fed by two separate muxes. Each mux can choose from one of up to 8 data sources. Not all data sources are shared between the two muxes.

class sentinel_cpu.alu.ASrcMux(*args, src_loc_at=0, **kwargs)

Latch one of many ALU A input sources.

The ALU does not have registered inputs; the data output is registered and feeds immediately into the ALU A input.

latch: In(1)

When asserted, latch the selected input into data on the next clock edge.

sel: In(<enum 'ASrc'>)

Select input.

Type:

In(ASrc)

gp: In(32)

Input source. Register from the register file whose value is currently on the read port (e.g. the read address was supplied on the previous clock cycle).

imm: In(32)

Input source. Decoded immediate from current instruction.

alu: In(32)

Input source. Decoded immediate from current instruction.

data: Out(32)

The output. When latch is asserted, the data input selected by sel will appear here on the next clock cycle.

elaborate(platform)
class sentinel_cpu.alu.BSrcMux(*args, src_loc_at=0, **kwargs)

Latch one of many ALU B input sources.

The ALU does not have registered inputs; the data output is registered and feeds immediately into the ALU B input.

When requested, this module will automatically move/align the top 16-bits of the 32-bit read data bus input to the bottom 16-bits, or any of of 3 high bytes into the bottom 8-bits. The mux will latch the aligned data when selected rather than the original input data.

latch: In(1)

When asserted, latch the selected input into data on the next clock edge.

sel: In(<enum 'BSrc'>)

Select input.

Type:

In(BSrc)

mem_sel: In(<enum 'MemSel'>)

Choose which slice of the input dat_r appears on the ~BSrcMux.data output when selected.

Type:

In(MemSel)

mem_extend: In(<enum 'MemExtend'>)

When mem_sel is less than word width, choose whether to sign or zero-extend dat_r when it’s output onto data.

Type:

In(MemExtend)

data_adr: In(32)

Contents of the internal address register latched by LatchAdr. Used for deciding how to align dat_r.

gp: In(32)

Input source. Register from the register file whose value is currently on the read port (e.g. the read address was supplied on the previous clock cycle).

imm: In(32)

Input source. Decoded immediate from current instruction.

pc: In(30)

Input source. Current contents of the Program Counter.

dat_r: In(32)

Input source. Current contents of the unregistered DAT_I in Top's Wishbone Bus. Only valid when qualified by MEM_VALID.

As an input, DAT_I is always 32-bit aligned. The mux contains internal alignment circuitry when a read of 8 or 16-bits on a less-than-32-bit alignment is requested. When selected, the mux will latched this modified/aligned data into data.

csr_imm: In(5)

Input source. Decoded src_a from the current instruction, which for CSR instructions is reused for specifying 5-bit CSR immediates.

csr: In(32)

Input source. Register from the CSR file whose value is currently on the read port (e.g. the read address was supplied on the previous clock cycle).

mcause: In(<class 'sentinel_cpu.csr.MCause'>)

Input source. Current MCAUSE as determined by ExceptionRouter.

Type:

In(MCause)

data: Out(32)

The output. When latch is asserted, the data input selected by sel will appear here on the next clock cycle.

elaborate(platform)

These muxes and latches live in the implementation of Top.

Fetch/Load/Store Unit

The Fetch/Load/Store Unit is implemented in-line in Top using the components from the align module.

Components to align external data going in/out of the Sentinel Core.

class sentinel_cpu.align.AddressAlign(*args, src_loc_at=0, **kwargs)

Align internal address data before driving external address lines.

This Component is pure combinational logic that splits a 32-bit input address for a memory or I/O transfer into two parts:

  • A 30-bit address to/from which to write/read 32-bit data. This directly drives Wishbone signal ADR_O.

  • 4 select lines which determine which bytes of the 32-bit write or read data are valid/meaningful for the current transfer. These lines directly drive Wishbone signal SEL_O.

If insn_fetch is asserted:

  • The 30-bit address comes directly from the ProgramCounter.

  • All select lines are asserted.

If insn_fetch is not asserted:

  • The 30-bit address comes from the top 30 bits of latched_adr.

  • The bottom two bits of latched_adr and the mem_sel signals calculate the proper select lines to assert (all for a 32-bit transfer, top two or bottom two for 16-bit transfers, and one-hot for an 8-bit transfer).

For space reasons, AddressAlign does not qualify insn_fetch by the de-assertion of WriteMem, even though an instruction write transfer doesn’t make really sense.

mem_req: In(unsigned(1))

If set, indicates a memory transfer over the Wishbone bus is occurring. Outputs are qualified by this signal.

Type:

In(MemReq)

mem_sel: In(<enum 'MemSel'>)

Select whether to do an 8-bit, 16-bit, or 32-bit read, or ignore.

Type:

In(MemSel)

insn_fetch: In(unsigned(1))

If set, the current memory read is an instruction fetch.

Type:

In(InsnFetch)

pc: In(30)

Current value of the ProgramCounter, used to generate wb_adr when insn_fetch is asserted.

latched_adr: In(32)

Registered address of the memory transfer, latched on a previous cycle. Raw address before alignment used to generate wb_adr when and wb_sel when insn_fetch is not asserted.

wb_adr: Out(30)

32-bit aligned address, which directly drives Wishbone ADR_O.

wb_sel: Out(4)

Data select lines, which qualify which bytes in the 32-bit DAT_I and DAT_O are valid for the current memory transfer. Directly drives Wishbone SEL_O.

elaborate(platform)
class sentinel_cpu.align.ReadDataAlign(*args, src_loc_at=0, **kwargs)

Align external read data before latching internally.

This Component is pure combinational logic that aligns and then extends read data to 32-bits:

  • If mem_sel is BYTE, pass any of the 4 bytes of wb_dat_r to an data extension circuit, depending on address alignment. Then either zero or sign-extend the selected byte to 32-bits, depending on mem_extend. Finally, pass the extender output to data.

  • If mem_sel is HWORD, pass either the low 16-bits or high 16-bits of wb_dat_r to a data extension circuit, depending on address alignment. Then, either zero or sign-extend these 16-bits to 32-bits, depending on mem_extend. Finally, pass the extender output to data.

  • If mem_sel is WORD, pass wb_dat_r to data unaltered. The extension circuit is not used.

Because I found it to be a size win, I physically implement ReadDataAlign as part of sentinel_cpu.alu.BSrcMux; the sentinel_cpu.alu.BSrcMux.dat_r input feeds directly into ReadDataAlign.

AddressAlign ensures that the appropriate SEL_O lines are asserted for the read.

mem_sel: In(<enum 'MemSel'>)

Select whether to do an 8-bit, 16-bit, or 32-bit read, or ignore.

Type:

In(MemSel)

mem_extend: In(<enum 'MemExtend'>)

Zero or sign-extend reads of less than 32-bits.

Type:

In(MemExtend)

latched_adr: In(32)

Registered address from which data will be read, latched on a previous cycle.

wb_dat_r: In(32)

Unregistered raw input data, directly from Wishbone DAT_I.

data: Out(32)

Aligned and extended data output.

elaborate(platform)
class sentinel_cpu.align.WriteDataAlign(*args, src_loc_at=0, **kwargs)

Align internal write data before sending to external peripherals.

This Component is pure combinational logic that aligns write data to an appropriate offset within 32-bits:

Since not all 32-bits of DAT_O will necessarily be valid for a given write, AddressAlign ensures that the appropriate SEL_O lines are asserted for the write.

mem_sel: In(<enum 'MemSel'>)

Select whether to do an 8-bit, 16-bit, or 32-bit write, or ignore.

Type:

In(MemSel)

latched_adr: In(32)

Registered address to which data will be written, latched on a previous cycle.

data: In(32)

Data to be written externally, before alignment.

wb_dat_w: Out(32)

Aligned data to be latched, which then drives Wishbone DAT_O.

elaborate(platform)

Aside from aligning, the glue logic for latching addresses, read data, and write data is minimal and controlled directly by microcode signals.

Instruction Cycle Counts

Todo

I need to create a test that gets latency and throughput for each instruction type of the core.

The following counts are general observations (as of 11/18/2023), from examining the microcode (knowing that each microcode instruction always takes 1 clock cycle):

  • There is room for improvement, even without making the core bigger.

  • Fetch/Decode takes a minimum of two cycles thanks to Wishbone classic’s REQ/ACK handshake taking two cycles.

    • When Wishbone ACK is asserted, Decode is taking place.

    • The GP file is a synchronous single read port, single write port. Sentinel loads RS1 out of the register file during Decode.

  • All instructions share the same operation the cycle after ACK/Decode:

    • Check for exceptions/interrupts, go to exception handler if so.

    • Latch RS1 into the ALU.

    • Load RS2 out of the register file, in anticipation for a “simple” instruction.

    • Jump to the instruction-specific microcode block.

  • At minimum, an instruction (addi, or, etc) takes 3 cycles to retire after the initial shared cycles. This means Sentinel instructions have a minimum latency of 6 cycles per instruction (CPI).

    • I define “retirement” to mean “cycle in which we return to the Fetch/Decode or Exception Checking part of the microcode program”. This usually corresponds to “cycle after RD/PC was written with results”.

  • Sentinel instructions have a maximum throughput of 4 CPI by overlapping the 2 Fetch/Decode cycles of the next instruction after the initial 3 shared cycles of the current instruction when possible (“pipelining”).

    • Some instructions overlap one of the Fetch/Decode cycles, some don’t overlap either of them. In particular, shift instructions with a nonzero shift count don’t pipeline Fetch/Decode. It may be possible to always overlap at least one cycle, but I haven’t tweaked the core yet to ensure this is a sound optimization.

  • Shift instructions need work:

    • For a shift of zero, shift-immediate and shift-register latency is 9 CPI, and throughput is 7 CPI.

    • For a shift of nonzero n, shift-immediate and shift-register latency and throughput is 6 + 2*n CPI.

  • Branch-not-taken latency and throughput is 6 CPI. Branch-taken latency and throughput is 7 CPI.

  • JAL/JALR latency is 7 CPI, throughput is 6 CPI.

  • Store latency and throughput is 10 CPI minimum. 2 cycles minimum are spent waiting for Wishbone ACK.

    • The core will release STB/CYC between the store and fetch of the next instruction.

  • Load latency is 10 CPI minimum, and throughput is 9 CPI. 2 cycles minimum are spent waiting for Wishbone ACK.

    • The core will release STB/CYC between the load and fetch of the next instruction.

  • CSR instructions require an extra Decode cycle compared to all other instructions (to check for legality).

    • At minimum, a read of a read-only zero CSR register has a latency of 7 CPI, and a throughput of 6 CPI.

    • At maximum, csrrc[i] has a latency of 11 CPI, and a throughput of 10 CPI.

  • Entering an exception handler requires 5, 6 (branch exceptions), or 7 clocks (JAL[R] exceptions) from the cycle at which the exception condition is detected.

    • mret has a latency and throughput of 8 CPI.

CSRs

Sentinel physically implements the following CSRs:

  • mscratch

  • mcause

    • The core can only physically trigger a subset of defined exceptions:

      • Machine external interrupt

      • Instruction access misaligned

      • Illegal instruction

      • Breakpoint

      • Load address misaligned

      • Store address misaligned

      • Environment call from M-mode

      In particular worth noting:

      • Misaligned accesses are not implemented in hardware.

      • There is no machine timer (a 64-bit counter is a bit too much to ask for right now :(…).

  • mip

    • Only the MEIP bit is implemented. The MSIP and MTIP bits always read as zero. The RISC-V Privileged Spec (Version: 20260120 page 46) says:

      MEIP is read-only in mip, and is set and cleared by a platform-specific interrupt controller.

      The user must provide their own interrupt controller. See sentinel_cpu.top.Top.

      One simple implementation is to OR all external interrupt sources together and feed it to the IRQ line. When any of the OR inputs are asserted, this will be reflected in the MEIP bit, indicating at least one I/O peripheral needs attention. The Sentinel program will then query each I/O peripheral to figure out exactly which peripherals need attention. An example implementation can be found for the serial and timer peripherals in examples.attosoc and sentinel-rt.

      Note

      In the future, I may implement the high (platform-specific) 16-bits of mip/mie to make interrupt-handling quicker.

  • mie

    • Only the MEIE bit is implemented.

  • mstatus

    • Only the MPP, MPIE, and MIE bits are implemented.

  • mtvec

    • The BASE is writeable; only the Direct MODE setting is implemented.

      Todo

      A read-only BASE is allowed, but I believe the Rust support code assumes a writable BASE. I don’t wish to fork riscv-rt solely for a read-only BASE. So I deal with the potential loss of space savings for now.

      Revisit whether read-only BASE is feasible in the future.

  • mepc

The following CSRs are implemented as read-only zero and trigger an exception on an attempt to write:

  • mvendorid

  • marchid

  • mimpid

  • mhartid

  • mconfigptr

The following CSRs are implemented as read-only zero (no exception on write):

  • misa

  • mstatush

  • mcountinhibit

  • mtval

  • mcycle

  • minstret

  • mhpmcounter3-31

  • mhpmevent3-31

All remaining machine-mode CSRs are unimplemented and trigger an exception on any access:

  • medeleg

  • mideleg

  • mcounteren

  • mtinst

  • mtval2

  • menvcfg

  • menvcfgh

  • mseccfg

  • mseccfgh

Useful enums and structs for dealing with RISC-V CSRs.

Attributes should match the addresses, fields, and offsets defined in the RISC-V Privileged Specification. Therefore, I’ve only made basic remarks on registers fields and values in these docs. Consult the spec for detailed descriptions.

Only the registers that are not read-only-0 or illegal are implemented for Sentinel. And even then, only the used fields are explicitly laid out. Most Unused enum fields are defined, while unused Structs fields are not implemented. See the CSRs section for an idea of what is implemented.

class sentinel_cpu.csr.MachineAddr(*values)

Enumeration of all defined RISC-V Machine-Mode CSR addresses.

Note

Register names are UPPER_CASE versions of those defined in the spec. Please consult the spec for detailed information, because I don’t feel like typing all the descriptions out :).

MVENDORID = 3857
MARCHID = 3858
MIMPID = 3859
MHARTID = 3860
MCONFIGPTR = 3861
MSTATUS = 768
MISA = 769
MEDELEG = 770
MIDELEG = 771
MIE = 772
MTVEC = 773
MCOUNTEREN = 774
MSTATUSH = 784
MSCRATCH = 832
MEPC = 833
MCAUSE = 834
MTVAL = 835
MIP = 836
MTINST = 842
MTVAL2 = 843
MENVCFG = 778
MENVCFGH = 794
MSECCFG = 1863
MSECCFGH = 1879
PMPCFG0 = 928
PMPCFG1 = 929
PMPCFG2 = 930
PMPCFG3 = 931
PMPCFG4 = 932
PMPCFG5 = 933
PMPCFG6 = 934
PMPCFG7 = 935
PMPCFG8 = 936
PMPCFG9 = 937
PMPCFG10 = 938
PMPCFG11 = 939
PMPCFG12 = 940
PMPCFG13 = 941
PMPCFG14 = 942
PMPCFG15 = 943
PMPADDR0 = 944
PMPADDR1 = 945
PMPADDR2 = 946
PMPADDR3 = 947
PMPADDR4 = 948
PMPADDR5 = 949
PMPADDR6 = 950
PMPADDR7 = 951
PMPADDR8 = 952
PMPADDR9 = 953
PMPADDR10 = 954
PMPADDR11 = 955
PMPADDR12 = 956
PMPADDR13 = 957
PMPADDR14 = 958
PMPADDR15 = 959
PMPADDR16 = 960
PMPADDR17 = 961
PMPADDR18 = 962
PMPADDR19 = 963
PMPADDR20 = 964
PMPADDR21 = 965
PMPADDR22 = 966
PMPADDR23 = 967
PMPADDR24 = 968
PMPADDR25 = 969
PMPADDR26 = 970
PMPADDR27 = 971
PMPADDR28 = 972
PMPADDR29 = 973
PMPADDR30 = 974
PMPADDR31 = 975
PMPADDR32 = 976
PMPADDR33 = 977
PMPADDR34 = 978
PMPADDR35 = 979
PMPADDR36 = 980
PMPADDR37 = 981
PMPADDR38 = 982
PMPADDR39 = 983
PMPADDR40 = 984
PMPADDR41 = 985
PMPADDR42 = 986
PMPADDR43 = 987
PMPADDR44 = 988
PMPADDR45 = 989
PMPADDR46 = 990
PMPADDR47 = 991
PMPADDR48 = 992
PMPADDR49 = 993
PMPADDR50 = 994
PMPADDR51 = 995
PMPADDR52 = 996
PMPADDR53 = 997
PMPADDR54 = 998
PMPADDR55 = 999
PMPADDR56 = 1000
PMPADDR57 = 1001
PMPADDR58 = 1002
PMPADDR59 = 1003
PMPADDR60 = 1004
PMPADDR61 = 1005
PMPADDR62 = 1006
PMPADDR63 = 1007
MCYCLE = 2816
MINSTRET = 2818
MHPMCOUNTER3 = 2819
MHPMCOUNTER4 = 2820
MHPMCOUNTER5 = 2821
MHPMCOUNTER6 = 2822
MHPMCOUNTER7 = 2823
MHPMCOUNTER8 = 2824
MHPMCOUNTER9 = 2825
MHPMCOUNTER10 = 2826
MHPMCOUNTER11 = 2827
MHPMCOUNTER12 = 2828
MHPMCOUNTER13 = 2829
MHPMCOUNTER14 = 2830
MHPMCOUNTER15 = 2831
MHPMCOUNTER16 = 2832
MHPMCOUNTER17 = 2833
MHPMCOUNTER18 = 2834
MHPMCOUNTER19 = 2835
MHPMCOUNTER20 = 2836
MHPMCOUNTER21 = 2837
MHPMCOUNTER22 = 2838
MHPMCOUNTER23 = 2839
MHPMCOUNTER24 = 2840
MHPMCOUNTER25 = 2841
MHPMCOUNTER26 = 2842
MHPMCOUNTER27 = 2843
MHPMCOUNTER28 = 2844
MHPMCOUNTER29 = 2845
MHPMCOUNTER30 = 2846
MHPMCOUNTER31 = 2847
MCYCLEH = 2944
MINSTRETH = 2946
MHPMCOUNTER3H = 2947
MHPMCOUNTER4H = 2948
MHPMCOUNTER5H = 2949
MHPMCOUNTER6H = 2950
MHPMCOUNTER7H = 2951
MHPMCOUNTER8H = 2952
MHPMCOUNTER9H = 2953
MHPMCOUNTER10H = 2954
MHPMCOUNTER11H = 2955
MHPMCOUNTER12H = 2956
MHPMCOUNTER13H = 2957
MHPMCOUNTER14H = 2958
MHPMCOUNTER15H = 2959
MHPMCOUNTER16H = 2960
MHPMCOUNTER17H = 2961
MHPMCOUNTER18H = 2962
MHPMCOUNTER19H = 2963
MHPMCOUNTER20H = 2964
MHPMCOUNTER21H = 2965
MHPMCOUNTER22H = 2966
MHPMCOUNTER23H = 2967
MHPMCOUNTER24H = 2968
MHPMCOUNTER25H = 2969
MHPMCOUNTER26H = 2970
MHPMCOUNTER27H = 2971
MHPMCOUNTER28H = 2972
MHPMCOUNTER29H = 2973
MHPMCOUNTER30H = 2974
MHPMCOUNTER31H = 2975
MCOUNTINHIBIT = 800
MHPMEVENT3 = 803
MHPMEVENT4 = 804
MHPMEVENT5 = 805
MHPMEVENT6 = 806
MHPMEVENT7 = 807
MHPMEVENT8 = 808
MHPMEVENT9 = 809
MHPMEVENT10 = 810
MHPMEVENT11 = 811
MHPMEVENT12 = 812
MHPMEVENT13 = 813
MHPMEVENT14 = 814
MHPMEVENT15 = 815
MHPMEVENT16 = 816
MHPMEVENT17 = 817
MHPMEVENT18 = 818
MHPMEVENT19 = 819
MHPMEVENT20 = 820
MHPMEVENT21 = 821
MHPMEVENT22 = 822
MHPMEVENT23 = 823
MHPMEVENT24 = 824
MHPMEVENT25 = 825
MHPMEVENT26 = 826
MHPMEVENT27 = 827
MHPMEVENT28 = 828
MHPMEVENT29 = 829
MHPMEVENT30 = 830
MHPMEVENT31 = 831
TSELECT = 1952
TDATA1 = 1953
TDATA2 = 1954
TDATA3 = 1955
MCONTEXT = 1960
DCSR = 1968
DPC = 1969
DSCRATCH0 = 1970
DSCRATCH1 = 1971
class sentinel_cpu.csr.AccessMode(*values)

The top 2 bits of CSR address space.

READ_ONLY = 3

Read-only CSR addresses.

class sentinel_cpu.csr.Quadrant(*values)

Bits 8 and 9 of the CSR address space.

UNPRIVILEGED = 0

Unpriviled Mode CSRs.

SUPERVISOR = 1

Supervisor Mode CSRs.

HYPERVISOR = 2

Hypervisor Mode CSRs.

MACHINE = 3

Machine Mode CSRs.

class sentinel_cpu.csr.MStatus(target)

Machine Status Register (mstatus).

mie: unsigned(1)

Machine Interrupt Enable bit.

mpie: unsigned(1)

Machine Previous Interrupt Enable bit.

mpp: unsigned(2)

Machine Previous Privilege bits. Constant C(3, 2) in Sentinel, meaning Machine Mode.

class sentinel_cpu.csr.MTVec(target)

Machine Trap Vector Address Register (mtvec).

class Mode(*values)

Low 2 bits of mtvec.

DIRECT = 0

Jump to the address in base.

VECTORED = 1

On asynchronous interruept, add an offset to the address in base before jumping, dependendent on MCause. On synchornous exception, jump to the address in base.

mode: Mode

Set how the exception/interrupt address is calculated.

base: unsigned(30)

Base address to jump to on exception.

class sentinel_cpu.csr.MIP(target)

Machine Interrupt Pending Register (mip).

msip: unsigned(1)

Machine Sofware Interrupt Pending bit. Not implemented.

mtip: unsigned(1)

Machine Timer Interrupt Pending bit. Not implemented.

meip: unsigned(1)

Machine External Interrupt Pending bit.

class sentinel_cpu.csr.MIE(target)

Machine Interrupt-Enable Register (mie).

msie: unsigned(1)

Machine Sofware Interrupt Enable bit. Not implemented.

mtie: unsigned(1)

Machine Timer Interrupt Enable bit. Not implemented.

meie: unsigned(1)

Machine External Interrupt Enable bit.

class sentinel_cpu.csr.MCause(target)

Machine Trap Cause Register (mtcause).

class Cause(*values)

Cause of the current exception.

Note

For easy searching, the docstring for each enum variant (minus the period) should match a string in the privileged spec.

INSN_MISALIGNED = 0

Instruction address misaligned.

INSN_FAULT = 1

Instruction access fault.

ILLEGAL_INSN = 2

Illegal instruction.

BREAKPOINT = 3

Breakpoint.

LOAD_MISALIGNED = 4

Load address misaligned.

LOAD_FAULT = 5

Load access fault.

STORE_MISALIGNED = 6

Store/AMO address misaligned.

STORE_FAULT = 7

Store/AMO access fault.

ECALL_UMODE = 8

Environment call from U-mode.

ECALL_SMODE = 9

Environment call from S-mode.

ECALL_MMODE = 11

Environment call from M-mode.

INSN_PAGE_FAULT = 12

Instruction page fault.

LOAD_PAGE_FAULT = 13

Load page fault.

STORE_PAGE_FAULT = 15

Store/AMO page fault.

MSOFT_INT = 3

Machine software interrupt.

MTIMER_INT = 7

Machine timer interrupt.

MEXT_INT = 11

Machine external interrupt.

cause: Cause

Cause of the current trap/exception.

interrupt: unsigned(1)

Set if the current trap is an asynchronous interrupt, rather than a synchronous exception. If set, the values of MEXT_INT and friends are valid.