User Reference

The Quickstart is a good reference for how to use Sentinel from the source repo. These next three sections discuss usage outside of the source tree.

Use in Verilog Code

Pre-Generated Verilog

Each Codeberg Release contains a standalone generated Verilog file of Sentinel CPU. If you opt to download the Verilog from releases, you do not need Python installed to use Sentinel. You can automate downloading the Verilog with a script like this:

SENTINEL_VER=v0.1.0-beta.2

wget -O sentinel-v-$SENTINEL_VER.zip https://codeberg.org/cr1901/sentinel/releases/download/$SENTINEL_VER/sentinel-v-$SENTINEL_VER.zip
unzip sentinel-v-$SENTINEL_VER.zip
set SentinelVer v0.1.0-beta.2

wget -OutFile sentinel-v-$SentinelVer.zip https://codeberg.org/cr1901/sentinel/releases/download/$SentinelVer/sentinel-v-$SentinelVer.zip
Expand-Archive -Path sentinel-v-$SentinelVer.zip -DestinationPath .

Verilog From Arbitrary Commits

Generating Verilog of arbitrary commits requires Python >= 3.11, pdm, and git.

If you’re using Sentinel with pdm as your top-level build system, I suggest adding a pdm script to provide a shortcut for Verilog generation in your pyproject.toml (call = "python -m sentinel_cpu.gen" does not work!):

[tool.pdm.scripts]
gen = { call = "sentinel_cpu.gen:cli", help="generate Sentinel Verilog file" }

For all other use cases, I recommend using pdm from your top-level build system, which manages the Sentinel checkout as a venv wrapper. Here is a user-configurable script as an example:

SENTINEL_PATH=./sentinel
SENTINEL_REF=next
SENTINEL_V=sentinel_cpu.v

if ! [ -d $SENTINEL_PATH/.venv ]; then
    git clone https://codeberg.org/cr1901/sentinel_cpu.git $SENTINEL_PATH
    git -C $SENTINEL_PATH checkout $SENTINEL_REF
    pdm install -p $SENTINEL_PATH -G yowasp
fi

pdm run -p $SENTINEL_PATH gen -o $SENTINEL_V
set SentinelPath ./sentinel
set SentinelRef next
set SentinelV sentinel_cpu.v

if (!(Test-Path $SentinelPath/.venv)){
    git clone https://codeberg.org/cr1901/sentinel_cpu.git $SentinelPath
    git -C $SentinelPath checkout $SentinelRef
    pdm install -p $SentinelPath -G yowasp
}

pdm run -p $SentinelPath gen -o $SentinelV

Use In Amaranth Code

Right now, even from Python, Sentinel consists of rather few tunable knobs. The only public Sentinel CPU module is the appropriately-named Top.

Top is an interface object whose Signature consists of a Wishbone Classic bus and an Interrupt ReQuest (IRQ) line. All interface members are synchronous to the sync clock domain. Explicit clk and rst lines are generated for the sync domain in generated Verilog code.

I expect most users to only need to import from sentinel_cpu.top to create their SoC:

from amaranth import Elaboratable
from sentinel_cpu.top import Top

class MySoC(Elaboratable):
    def __init__(self):
        self.cpu = Top()
        ...

    def elaborate(self, plat):
        m = Module()
        m.submodules.cpu = self.cpu
        ...

Since the Sentinel top-level is only a CPU, not a full computer system, the user must provide some sort of memory, and I/O to effectively run programs. One common way to do this is to connect Sentinel’s Wishbone bus to a Wishbone address decoder, behind which memory and I/O live.

See the AttoSoC class, and the corresponding section in the Quickstart, for a full working example.

Public API

The Sentinel RISC-V CPU Package.

There is no __all__; star imports are not presently supported. Users will likely want to import or run one of the following modules directly:

from sentinel_cpu.top import Top
python -m sentinel_cpu.gen --help

Top-Level Module for Amaranth Components of Sentinel CPU.

class sentinel_cpu.top.Top(*args, src_loc_at=0, **kwargs)

The Sentinel CPU Top-Level.

The Sentinel CPU top-level provides an interface to a Wishbone Classic bus and Interrupt ReQuest (IRQ) signal. Optionally, one may generate extra signals which are used for verifying core functionality using the RISC-V Formal Interface (RVFI).

Sentinel uses a single clock domain, called sync by convention. On the first cycle after reset is de-asserted, Sentinel begins execution at address 0.

The Wishbone bus provides your typical address, data, and control lines for transferring data to and from the CPU. Sentinel only support Wishbone Classic Single Xfers. Specifically, when CYC and STB are both asserted 1 on a given clock cycle, the following holds:

  • If WE is asserted, Sentinel wants to write data over its bus to peripherals on its DAT_W lines. Any byte in DAT_W where SEL is also asserted is valid data. Sentinel will wait for ACK to be asserted.

  • If WE is not asserted, Sentinel wants to read data from a peripheral over its DAT_R lines. Peripherals should ensure that any byte in DAT_R where SEL is also asserted is valid data before asserting ACK.

  • When ACK is sent to Sentinel on a given clock cycle, Sentinel will deassert CYC and STB on the next cycle, for at least one cycle.

At present (12/3/2024), I could not find good opportunities in the microcode program to try Block Xfers without potentially violating the spec.

The IRQ line triggers Machine External Interrupts when asserted (1)- i.e. it is level-triggered. For any peripherals, devices, etc that can assert an IRQ, the user must provide software, hardware, or a combination to also deassert their IRQ logic upon acknowledgement from the CPU. The Machine Software and Machine Timer interrupt lines are not presently implemented.

Todo

Seeing that it’s memory-mapped, there should probably be provisions for a user-supplied Machine Timer.

Sentinel directly reads the IRQ line in two related, but distinct, scenarios:

  • An instruction wants to read the Machine Interrupt Pending (MIP) register. In this case, Sentinel will directly latch the value of the IRQ line into MIP’s Machine External Interrupt Pending (MEIP) bit on the next positive edge of the sync clock domain.

  • The microcode is querying exceptions from the decoder. In this case, several control Components and the MCAUSE register depend on the sampled value of the IRQ line on the next positive edge of sync.

To ensure that all internal components of Sentinel see the same value of the IRQ line on a given clock cycle, a user must place their own synchronization logic before the IRQ input if interrupt sources can be triggerred asynchronously to sync.

See the CSRs section for more information on exception handling (including interrupts).

Parameters:

formal (bool) –

The Wishbone bus and IRQ line alone don’t give enough information to properly use the RISC-V Formal Interface to verify core properties. If True, Sentinel will gain an extra rvfi signature Member with signals required to implement RVFI.

As RVFI is meant for verification, the rvfi Member is not meant to be used in a synthesized design. It is best to leave this option disabled unless you are using Top in conjunction with FormalTop.

formal

If True, rvfi Member is present.

Type:

bool

bus

Wishbone Classic Bus.

Connect your memory and I/O to this bus. The signature is of the form

Signature({
        "adr":   Out(30),
        "dat_w": Out(32),
        "dat_r": In(32),
        "sel":   Out(4),
        "cyc":   Out(1),
        "stb":   Out(1),
        "we":    Out(1),
        "ack":   In(1),
})

where each Member corresponds to the equivalently-named Wishbone Classic signal.

Type:

Out(Signature)

irq

Interrupt Request Line.

External peripherals signal they need attention when this line is high.

Type:

In(1)

rvfi

Internal connections required for implementing the RISC-V Formal Interface.

The signature is of the form

Signature({
        "exception": Out(1),
        "decode": Out(self.decode.rvfi.signature)
})

where

exception: Out(1)

Asserted if an exception occurred this cycle.

decode: Out(Signature)

Forwarded RVFI signals from sentinel_cpu.decode.Decode.

Todo

Right now, the formal harness tends to directly “reach” into Top and Components to read the appropriate signals.

I would prefer encapsulation via an explicity rvfi port Member, but this process has been slow. I will wait to document until this interface Member is more stable.

Type:

Out(Signature)

alu
Type:

sentinel_cpu.alu.ALU

addr_align
Type:

sentinel_cpu.align.AddressAlign

a_src
Type:

sentinel_cpu.alu.ASrcMux

b_src
Type:

sentinel_cpu.alu.BSrcMux

control
Type:

sentinel_cpu.control.Control

datapath
Type:

sentinel_cpu.datapath.DataPath

decode
Type:

sentinel_cpu.decode.Decode

exception_router
Type:

sentinel_cpu.exception.ExceptionRouter

wdata_align
Type:

sentinel_cpu.align.WriteDataAlign

elaborate(platform)

Verilog generation module/script for Sentinel.

This module can be run directly from the command-line as __main__:

python -m sentinel_cpu.gen --help

Individual functions are documented for completeness. Only cli() should be treated as public (see Development Guidelines).

sentinel_cpu.gen.cli()

Scripting entry point to generate Sentinel core.

This function examines sys.argv and then generates a Verilog representation of Sentinel.

Todo

Use sphinx-argparse to document arguments automatically.

This function can be called in a number of ways, all of which should be equivalent:

  • From within a wrapper Python script:

    import sentinel_cpu.gen
    
    if __name__ == "__main__":
       sentinel_cpu.gen.cli()
    
  • Running the gen module (as __main__) from a shell script:

    python -m sentinel_cpu.gen --help
    
  • From within pdm:

    [tool.pdm.scripts]
    gen = { call = "sentinel_cpu.gen:cli", help="generate Sentinel Verilog file" }