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.

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 anALU sourceon any given clock cycle. See the bottom half ofCSRFiledocumentation for more details. Before microcode takes anactionon the PC besidesHOLD, 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
PcActionto a value besidesHOLD:If
INCis selected, automatically increment the PC by4bytes on the next active edge. Physically, the increment is by132-bit word, because the bottom two bits of the PC are unimplemented.If
LOAD_ALU_Ois selected, load the PC with the currentalu outputon the next active edge.
ProgramCounteronly physically implements the top 30 bits of the register, because the least-significant bits are the PC are always0for valid RV32I instructions. Loads to the PC from thealu outputwill 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.JALandJALRinstructions.)- ControlSignature = Signature({'action': Out(<enum 'PcAction'>)})
Program Counter microcode signals.
The signature is of the form
Signature({ "action": Out(PcAction) })
where
This is a
Signaturewith a singleMemberin 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 outputthis clock cycle, ifactionisLOAD_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
RegFileComponentprovides 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=0chooses one 32-word GP reg file RAM chip andA5=1chooses a separate 32-word CSR reg file RAM, and their data lines are muxed together to read/write only one RAM at a time. SeeCSRFilefor rationale on what I called shared RAM. Note that CSR addresses inCSRFileare relative to address0x20in the shared RAM.RegFilesnoopsCSR 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. ifCSROpis notNONE), 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 address0holds the “value” ofx0. For a small core like Sentinel,x0in RAM saves a lot of logic that would otherwise have to choose between a hardwiredx0and the backing memory. It is microcode’s responsibility to initialize address0to value0usingallow_zero_wrbefore the first macroinstruction begins processing. Ifallow_zero_wris not asserted on a given clock cycle, writes to address0are ignored, which implements RISC-V instruction semantics.- Parameters:
formal (bool) – Presently unused.
- 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_rthis cycle.Datais valid on the next active edge.
- reg_write: Out(1)
Perform a write from the GP reg file at
adr_wthis cycle.Datais written on the next active edge.
- allow_zero_wr: Out(1)
By default, writes to address
0are ignored; qualifyreg_writewith 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.
RegFiledoes not directly use thisSignature; 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 addressforRegFile.
- reg_w_sel: Out(RegRSel)
Select a
write addressforRegFile.
- 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 afteradr_ris presented andreg_readis asserted
- dat_w: In(32)
Data written to register specified by
adr_w. Valid on the next active edge afteradr_wis presented andreg_writeis asserted.
- ctrl: Out(ControlSignature)
Choose whether to perform a register read, write, or both. Writes to address
0are ignored unlessallow_zero_wris 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
Signatureis 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
0x20in the shared RAM.
- dat_r: In(5)
Data of CSR at address
adrbeing read.
- dat_w: Out(32)
Data of CSR at address
adrbeing written.
- 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:
- elaborate(platform)
- class sentinel_cpu.datapath.CSRFile(*args, src_loc_at=0, **kwargs)
Control and Status Registers register file controller.
As
hintedatbyseveralmicrocodefields,CSRFileandRegFileshare a single memory resource defined inRegFile; GP regs use the bottom half, and CSRs use the top half. ThisComponentmasquerades as a register file whose control signals are completely independent fromRegFile, and generates the signals required to access the top half of the shared register memory. SeeMSTATUS, etc.CSRFileandRegFilesharing 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-coupledCSRFileandRegFilethan 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
mapthe12-bitCSR addresses to 4-bits.MStatus,MIP, andMIEregs are special in that, similar to theProgramCounter, they must be readable or writable on any given clock cycle for interrupt/exception handling. The block RAM as implemented in theRegFilecannot 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
MIPon 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.MEIPis physically implemented, and RISC-V mandatesMIP.MEIPto be read-only. “External value takes priority” will apply to any future bits ofMIPthat I implement (MIP.MSIP,MIP.MTIP, the top 16 bits ofMIP, 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) })
- exception: Out(ExceptCtl)
If set to
ENTER_INT, setMStatus.MPIEtoMStatus.MIE, and setMStatus.MIEto0.If set to
LEAVE_INT, setMStatus.MIEtoMStatus.MPIE, and setMStatus.MPIEto1.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.
CSRFiledoes not directly use thisSignature; 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 addressforCSRFile.
- target: Out(Target)
The
Targetmicrocode field. Used as an explicitly-specified CSR address ifTRG_CSRis 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 afteradris presented andctrlis set toREAD_CSR.
- dat_w: Out(32)
Data written to register specified by
adr. Valid on the next active edge afteradris presented andctrlis set toWRITE_CSR.
- ctrl: Out(ControlSignature)
Choose whether to perform a CSR register read or write this clock cycle. Also save and restore
MStatuswhen entering and leaving an exception handler.- Type:
Out(
ControlSignature)
- mip_r: Out(MIP)
Current read value of the
MIPregister. Right now, combinationally equivalent tomip_w.
- 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
RegFilefor CSR values stored in shared RAM.- Type:
- MSTATUS = 0
MStatusCSR 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
MIECSR 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
MTVECCSR address in the shared register file, relative to its top-half.
- MSCRATCH = 8
MSCRATCHCSR address in the shared register file, relative to its top-half.
- MEPC = 9
MEPCCSR address in the shared register file, relative to its top-half.
- MIP = 12
MIPCSR 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
DataPathSrcMuxis analogous toASrcMuxandBSrcMuxfor theALU. However, I couldn’t get it to optimize well compared to putting the logic directly inTop. 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.
- pc_mod
- Type:
- 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.
UCodeROMtakes a microcode assembly file as input, parses the assembly file, creates aMemoryto hold the assembled program, and dynamically creates aSignaturewhich splits out all microcode fields.Note
I have not personally tried using alternate microcodes. Because most of Sentinel uses
ucodefieldsin 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.asmand the defaultfield_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_mapto use to generate aSignature. By default, use afield_mapcorresponding tomain_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.
StructLayoutis determined by the microcode assembly file. A defaultfieldslayout 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:
- field_map: dict
Map of strings to
ShapeLike, which are verified against thefieldssupplied in the microcode assembly file.Each
Enumclass should have values inUPPER_CASEcorresponding to an equivalent m5metaenumwhose values are lower_case.
- elaborate(platform)
- assemble()
Verify and assemble the associated microcode source file.
Internally calls m5meta’s
assemblefunction and verifies that the assembly file matchesfield_map.- Raises:
ValueError – If the assembly file uses multiple address spaces or its
enums cannot be mapped tofield_map.
Sentinel control unit implementation and microcode ROM wrapper.
- class sentinel_cpu.control.MappingROM(*args, src_loc_at=0, **kwargs)
Sentinel Microcode Mapping ROM.
MappingROMis a hardware jump table that takes in 32-bit RISC-V instructions and maps them down to one of 256 addresses inUCodeROM. In addition, ifMappingROMdetects a CSR instruction (logic duplicated fromExceptionControl), it will map the CSR address field down to one of 16 addresses in much the same way.For non-CSR instructions, the target
UCodeROMaddress for the instruction will be available onrequested_opon the first active edge afterstartis asserted. IfMappingROMdetects that the current instruction is a CSR instruction, mapping takes two cycles:On the first active edge after
startis asserted,requested_opwill 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 thefunct12instruction field- will be valid at this point.On the second active edge after
startis asserted, the actual target address for the CSR instruction will be available onrequested_op.
To use the latched address in
requested_op, set theSequencerto useJmpType.MAPvia microcode on the clock cycle immediately afterstartis asserted.Logically,
MappingROMis part ofControl. Physically, it’s part ofDecodefor 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
startis asserted.
- 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
instructionfrom from theCSRAttributeslook-up table.The
StructLayoutmatches that ofCSRAttributes._DataLayout; the ad-hocStructLayoutobject avoids circular-dependency import issues.- Type:
In(
StructLayout)
- requested_op: Out(8)
Mapped jump target into
UCodeROM, calculated frominsn. Valid on the first active edge afterstartis asserted. Also valid on the second active edge afterstartassertion for CSR instructions.
- csr_encoding: Out(4)
Mapped CSR address output, calculated from
insn. Valid on the first active edge after assertion ofstart, but only ifMappingROMdetects 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:
Multiplexer for
condition testsources.
In principle
MappingROMis also part of the Control Unit. However, I found it to be a space win to tightly couple it to theDecode.This
Componentis pure combinational logic. Beyond connecting the above parts together,Controlpropagates microcode ROMcontrol signalsto 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
MembersofControlhaveSignaturesof the form:Signature({ "route": Out(RoutingSignature), "ctrl": Out(ControlSignature) })
RoutingSignatureandControlSignatureare class variableSignatureplaceholders:RoutingSignatures contain routing information to and from the containingComponent.ControlSignatures modify the containingComponent'sbehavior, 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
ALUcontrol signals. The signature is of the formSignature({ "route": Out(ALU.RoutingSignature), "ctrl": Out(ALU.ControlSignature) })
See
ALU.RoutingSignatureandALU.ControlSignature.- Type:
Out(Signature)
- decode
Decodecontrol signals. The signature is of the formSignature({ "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 withSequencer.opcode_adr.
- Type:
In(Signature)
- gp
RegFilecontrol signals. The signature is of the formSignature({ "route": Out(RegFile.RoutingSignature), "ctrl": Out(RegFile.ControlSignature) })
See
RegFile.RoutingSignatureandRegFile.ControlSignature.- Type:
Out(Signature)
- pc
ProgramCountercontrol signals.- Type:
- csr
CSRFilecontrol signals. The signature is of the formSignature({ "route": Out(CSRFile.RoutingSignature), "ctrl": Out(CSRFile.ControlSignature) })
See
CSRFile.RoutingSignatureandCSRFile.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
- valid: In(1)
When asserted, the memory transfer (read or write) has finished this cycle.
- 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 datainto an internal register which drives the external write data bus (DAT_O).- Type:
Out(
LatchData)
- latch_adr: Out(LatchAdr)
Latch the
ALU outputinto an internal register whichindirectlydrives the address bus (ADR_O) and select (SEL_O) signals.- Type:
Out(
LatchAdr)
- Type:
Out(Signature)
- exception
ExceptionRoutercontrol signals.- Type:
- elaborate(platform)
- class sentinel_cpu.control.Sequencer(*args, src_loc_at=0, **kwargs)
Microprogram address generation. See sequencer.
Sequencergenerates the address of the next microinstruction to execute using the semantics explained inJmpType. It also implicitly contains the microprogram counter (upc), accessed viaCONT. On reset, the upc is reset to2, and stays at2until 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_CYCandWB_STBdo not deassert on the first cycle after reset (fixed by guard logic).Initial instruction fetch uses address
4instead of0(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
2is 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 :).- target
Target microinstruction address microcode field, used by the sequencer for
DIRECTandDIRECT_ZERO.- Type:
Signal(
Target)
- jmp_type
Select the next microinstruction address to place in
adr. Next addresses are calculated in parallel for all possibleJmpTypes, 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 forMAP. It typically contains a microprogram address for handling the currently-executing macroinstruction.- Type:
Signal(
Target)
- test
If set, the condition test described by the current value of
CondTestsucceeded this cycle. This in turn affectsadr, depending onjmp_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;
ExceptionControlhandles 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
Layoutis created bysentinel_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
addris 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
enableis asserted, thisComponentexamines aninput instruction'sOpcodeType, and extracts the correct sign-extended 32-bit immediate encoded in thatOpcodeType. The immediate is available onimmthe active edge afterenableis asserted.RISC-V instructions encode several forms and widths of 32-bit immediate operands, based upon an instruction’s
major opcode.ImmediateGeneratorabstracts 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, theoutput immediateis only valid for certainmajor opcodesin theinput instruction:Warning
The fact that
ImmediateGeneratordoes 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. interfaceMembers.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
ProgramCounterforJALRis supposed to triggerINSN_MISALIGNED. However, in the case of Sentinel, the immediate generator does not have access toProgramCounter, and so the core doesn’t know whether aINSN_MISALIGNEDexception will trigger until a later clock cycle (jump target calculation viaALU).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
insnhis cycle. Output will be valid on the first active edge afterenableis asserted.
- 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 afterenableis asserted.
- elaborate(platform)
- class sentinel_cpu.decode.ExceptionControl(*args, src_loc_at=0, **kwargs)
Detect and handle
Decode-releated exceptions.When
startis asserted,ExceptionControlexamines the current instruction checks whether theinput instructionfieldsare valid. Oncestartis asserted, exception information will be available onexceptionon the next active edge for non-CSR instructions and two active edges later for CSR instructions; likeMappingROM,ExceptionControlis aware of CSR decode cycles.ExceptionControlfound an exception for the current instruction if it asserts thevalidfield of theexceptionamaranth.lib.wiring.Memberon any active edge afterstartassertion (although in practice, exceptions are detected no later than two cycles afterstartassertion). It can trigger the following exceptions:This
Componentshould be used with conjunction withInsnandImmediateGenerator, 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
startis asserted.
- 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
instructionfrom from theCSRAttributeslook-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
startis asserted for non-CSR instructions; valid on the second active edge afterstartassertion 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_decodeis asserted, the RISC-V instruction present oninsnSignal is decoded and split onto the remainingMembersof the decoder interface.- Parameters:
formal (bool) – If
True, the decoder will gain an extrarvfiportMemberwith signals relevant to implementing RVFI.
- do_decode
When asserted, do instruction decoding. Some
OutMembersin this interface are only valid are on the first or second active edge afterdo_decodeis 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 operandof the current instruction. Immediately valid onceinsnis stable without waiting fordo_decodeor 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 operandof the current instruction. Valid on the active edge afterdo_decodeis asserted.- Type:
Out(5)
- src_b
Second source operandof the current instruction. Valid on the active edge afterdo_decodeis asserted.- Type:
Out(5)
- imm
Calculated immediate value of the current instruction. Valid on the active edge after
do_decodeis asserted. Valid only if the current instruction actually has an immediate field.- Type:
Out(32)
- dst
Destination operandof the current instruction. Valid on the active edge afterdo_decodeis 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
insnis stable without waiting fordo_decodeor an active edge.- Type:
Out(OpcodeType)
- exception
Exception information related to decoding,
used byExceptionRouter. Can triggerILLEGAL_INSN,ECALL_MMODE, andBREAKPOINT.For CSR instructions,
exceptionis valid two active edges afterdo_decodeasserts. For all other instructions,exceptionis valid on the active edge afterdo_decodeis asserted.Othercomponentsand the microcode program need to be (and are!) aware of the variable latency.- Type:
Out(DecodeException)
- requested_op
Microcode address generated by
MappingROM. TheSequenceruses this address to jump to macroinstruction-specific microcode.Valid on the first active edge after
do_decodeis asserted. Additionally valid on the second active edge afterdo_decodeis 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_opwill hold a jump address to CSR-specific macroinstruction handling.- Type:
Out(8)
- csr_encoding
Mapped CSR addressgenerated byMappingROM. Valid on the first active edge afterdo_decodeasserts 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 ondo_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.
- do_decode: Out(5)
Copy of
do_decode.
- funct12: Out(12)
funct12field 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
Viewfor the 32-bit Signal representing a RISC-V instruction. However, it does not inherit fromViewbecauseLayoutsare not designed to retrieve non-contiguous bits.- Parameters:
value (Value) – Raw value to interpret as a RISC-V instruction.
- ECALL = 115
Bit pattern for
ECALLinstruction.
- EBREAK = 1048691
Bit pattern for
EBREAKinstruction.
- MRET = 807403635
Bit pattern for
MRETinstruction.
- WFI = 273678451
Bit pattern for
WFIinstruction.
- 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
ImmediateGeneratorfor an example.- Parameters:
value (Value) – Raw value to interpret as a RISC-V immediate.
- 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
ExceptionControlfor 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.
- 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 access
Return whether the CSR is read-only.
rawis interpreted as anAccessMode.- Type:
- property opcode
Return the major opcode.
- Type:
- 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 bysentinel_cpu.ucodefields.OpTypeby usingctrlmodifiers.- 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
- omod: Out(ALUOMod)
Modify the output after doing ALU operation, but before latching the ALU output into
o(for next cycle).
- 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 theASrcMuxandBSrcMux.The signature is of the form
Signature({ "a_src": Out(ASrc), "b_src": Out(BSrc), "latch_a": Out(LatchA), "latch_b": Out(LatchB), })
where
- latch_a: Out(LatchA)
If set, latch the
selectedsource to theASrcMux outputthis cycle.- Type:
Out(
LatchA)
- latch_b: Out(LatchB)
If set, latch the
selectedsource to theBSrcMux outputthis 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
Structhas a very similar purpose toExceptionRouter.out. In fact, theLayoutis the same as theSignatureofout! The main differences compared tooutis that:Decodecan only physically trigger a subset of exceptions thatoutcan:Both
validande_typeare synchronous to thesyncclock domain (which is why, AFAIR,DecodeExceptioncan be aStructin the first place). Inout, onlymcauseis synchronous tosync;exceptionis combinationally driven.
DecodeExceptionphysically belongs toDecode, but is placed in theexceptionmodule to solve circular import issues.Todo
Eventually
DecodeExceptionshould be defined as a nested class underDecode, similar to e.g.ALU.RoutingSignature.
- 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,
ExceptionRouteronly checks for exceptions when qualified by values ofExceptCtlother thanNONE; it can only check for one type of exception each clock cycle.When
ExceptCtlis notNONE,ExceptionRouterwill 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 theMCAUSEregister. Themcauseport is physically distinct from theMCAUSEregister, and so microcode should save the latch value to the actualMCAUSEregister as part of exception handling.While the
MCauseStructknows about all currently-defined RISC-V M-Mode exception types,ExceptionRoutercan only trigger a subset of exceptions:Anything from
DecodeException.e_type.
- 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
- except_ctl: Out(ExceptCtl)
Choose which action
ExceptionRouterperforms 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. Thecausewill 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.
- 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
mcausewill be valid next cycle.
- mcause: Out(MCause)
Qualified by
exceptionon the previous cycle. Indicates the type of exception, if any, which was detected last cycle whereExceptCtlwas notNONE. Must be saved by microcode if the value is needed, as this is not meant to hold theMCAUSEregister.
- 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 inputsources.The ALU does not have registered inputs; the
dataoutput is registered and feeds immediately into the ALU A input.- gp: In(32)
Input source. Register from the
register filewhose 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 immediatefrom current instruction.
- alu: In(32)
Input source.
Decoded immediatefrom current instruction.
- data: Out(32)
The output. When
latchis asserted, the data input selected byselwill 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 inputsources.The ALU does not have registered inputs; the
dataoutput is registered and feeds immediately into the ALU B input.When requested, this module will automatically
move/alignthe top 16-bits of the 32-bitread data bus inputto the bottom 16-bits, or any of of 3 high bytes into the bottom 8-bits. The mux will latch the aligned data whenselectedrather than the original input data.- mem_sel: In(<enum 'MemSel'>)
Choose which slice of the input
dat_rappears on the ~BSrcMux.data output whenselected.- Type:
In(MemSel)
- mem_extend: In(<enum 'MemExtend'>)
When
mem_selis less than word width, choose whether to sign or zero-extenddat_rwhen it’s output ontodata.- Type:
In(MemExtend)
- data_adr: In(32)
Contents of the internal address register latched by
LatchAdr. Used for deciding how to aligndat_r.
- gp: In(32)
Input source. Register from the
register filewhose 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 immediatefrom 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_IinTop's Wishbone Bus. Only valid when qualified byMEM_VALID.As an input,
DAT_Iis always 32-bit aligned. The mux containsinternal alignment circuitrywhen a read of 8 or 16-bits on a less-than-32-bit alignment is requested. Whenselected, the mux will latched this modified/aligned data intodata.
- csr_imm: In(5)
Input source.
Decoded src_afrom the current instruction, which for CSR instructions is reused for specifying 5-bit CSR immediates.
- csr: In(32)
Input source. Register from the
CSR filewhose 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
MCAUSEas determined byExceptionRouter.- Type:
In(MCause)
- data: Out(32)
The output. When
latchis asserted, the data input selected byselwill 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
Componentis pure combinational logic that splits a 32-bit input address for a memory or I/O transfer into two parts:A
30-bit addressto/from which to write/read 32-bit data. This directly drives Wishbone signalADR_O.4 select lineswhich determine which bytes of the 32-bitwriteorread dataare valid/meaningful for the current transfer. These lines directly drive Wishbone signalSEL_O.
If
insn_fetchis asserted:The 30-bit address comes directly from the
ProgramCounter.All select lines are asserted.
If
insn_fetchis not asserted:The 30-bit address comes from the top 30 bits of
latched_adr.The bottom two bits of
latched_adrand themem_selsignals 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,
AddressAligndoes not qualifyinsn_fetchby the de-assertion ofWriteMem, 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 generatewb_adrwheninsn_fetchis asserted.
- latched_adr: In(32)
Registered address of the memory transfer, latched on a previous cycle. Raw address before alignment used to generate
wb_adrwhen andwb_selwheninsn_fetchis 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_IandDAT_Oare valid for the current memory transfer. Directly drives WishboneSEL_O.
- elaborate(platform)
- class sentinel_cpu.align.ReadDataAlign(*args, src_loc_at=0, **kwargs)
Align external read data before latching internally.
This
Componentis pure combinational logic that aligns and then extends read data to 32-bits:If
mem_selisBYTE, pass any of the 4 bytes ofwb_dat_rto an data extension circuit, depending onaddressalignment. Then either zero or sign-extend the selected byte to 32-bits, depending onmem_extend. Finally, pass the extender output todata.If
mem_selisHWORD, pass either the low 16-bits or high 16-bits ofwb_dat_rto a data extension circuit, depending onaddressalignment. Then, either zero or sign-extend these 16-bits to 32-bits, depending onmem_extend. Finally, pass the extender output todata.If
mem_selisWORD, passwb_dat_rtodataunaltered. The extension circuit is not used.
Because I found it to be a size win, I physically implement
ReadDataAlignas part ofsentinel_cpu.alu.BSrcMux; thesentinel_cpu.alu.BSrcMux.dat_rinput feeds directly intoReadDataAlign.AddressAlignensures that the appropriateSEL_Olines 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
Componentis pure combinational logic that aligns write data to an appropriate offset within 32-bits:If
mem_selisBYTE, move the low byte ofdatato one of the 4 constituent bytes ofwb_dat_w, depending onaddressalignment. The other bytes ofwb_dat_ware invalid.If
mem_selisHWORD, the low 16-bitsdatamove into either the low 16-bits or high 16-bits ofwb_dat_w, depending onaddressalignment. The other 16-bits ofwb_dat_wis invalid.If
mem_selisWORD, passdatatowb_dat_wunaltered. All bits are valid.
Since not all 32-bits of
DAT_Owill necessarily be valid for a given write,AddressAlignensures that the appropriateSEL_Olines 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.
- 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*nCPI.
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.
mrethas a latency and throughput of 8 CPI.
CSRs
Sentinel physically implements the following CSRs:
mscratchmcauseThe 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 :(…).
mipOnly the
MEIPbit is implemented. TheMSIPandMTIPbits always read as zero. The RISC-V Privileged Spec (Version: 20260120 page 46) says:MEIPis read-only inmip, 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
ORall external interrupt sources together and feed it to theIRQ line. When any of theORinputs are asserted, this will be reflected in theMEIPbit, 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 inexamples.attosocandsentinel-rt.Note
In the future, I may implement the high (platform-specific) 16-bits of
mip/mieto make interrupt-handling quicker.
mieOnly the
MEIEbit is implemented.
mstatusOnly the
MPP,MPIE, andMIEbits are implemented.
mtvecThe
BASEis writeable; only the DirectMODEsetting is implemented.Todo
A read-only
BASEis allowed, but I believe the Rust support code assumes a writableBASE. I don’t wish to forkriscv-rtsolely for a read-onlyBASE. So I deal with the potential loss of space savings for now.Revisit whether read-only
BASEis feasible in the future.
mepc
The following CSRs are implemented as read-only zero and trigger an exception on an attempt to write:
mvendoridmarchidmimpidmhartidmconfigptr
The following CSRs are implemented as read-only zero (no exception on write):
misamstatushmcountinhibitmtvalmcycleminstretmhpmcounter3-31mhpmevent3-31
All remaining machine-mode CSRs are unimplemented and trigger an exception on any access:
medelegmidelegmcounterenmtinstmtval2menvcfgmenvcfghmseccfgmseccfgh
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.
- 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.