Introduction
Hardware Root of Trust
Exploring OpenTitan and Pavona
Overview
This material is provided by RISC-V Ottawa as a hands-on study of the hardware root of trust (HWRoT): a small, trusted core that everything else in a secure system depends on.
Rather than reading about it in the abstract, we take a real open-source root of trust implementation, boot it in a simulator on a laptop, and pull it apart subsystem by subsystem until the whole chain of trust is understood end to end.
The final goal is to get to the point where we can use a HWRoT in the same way a real product would. For this, we’ll focus on the discrete secure element use case, where a separate host processor communicates to the HWRoT over SPI.
The two open implementations we study are OpenTitan, the open silicon root of trust from lowRISC, and Pavona, the newer, certification-aligned design derived from it that adds a post-quantum crypto stack. Everything here runs in simulation using Verilator, so you need no special hardware, only a machine with enough memory to build it.
No prior hardware-security experience is assumed, though comfort with C, the command line, and the basics of public-key cryptography will help.
How the book is organized
The point of this book is not to rehash the already stellar documentation provided by OpenTitan and Pavona. Instead, it is to get you to look, understand, simulate, integrate, simulate again, break, and teach. It opens with the ideas, what a root of trust is, why open silicon matters, and which threats it is built against.
The chapters after that each take one subsystem of the chip (secure boot, secure storage and lifecycle, identity and keys, attestation, post-quantum crypto, provisioning and ownership) and follow it from the specification down through to the RTL and the firmware that drives it, ending in an exercise you perform yourself.
The final part is the most exciting, since this is where we stop treating the chip as a lab specimen and instead use it as a real component: a secure co-processor serving a host that has to trust its answers.
Don’t jump to the end though! Because every chapter ends in something you can simulate yourself and show off, e.g., a waveform, a boot log, a certificate chain that validates, or a run that correctly refuses to proceed.
As mentioned, the reference documentation from OpenTitan and Pavona is excellent and we link to it constantly rather than restating it; what this book adds is the narrative thread through it, a live simulation on your own machine, and a view of the internal state running in your own simulation environment.
What will you learn?
By the end you should be able to:
- Explain what a hardware root of trust is and how it is built from open silicon
- Build and run a real root-of-trust chip entirely in simulation
- Trace the secure boot chain from immutable ROM up to owner firmware
- Follow a device identity from an OTP root secret through key derivation to an attestation certificate that a remote service can verify
- Reason about secure storage, lifecycle states, and post-quantum cryptography
- Provision a blank simulated chip and hand ownership of it to someone else
- Utilize the chip from a host processor as a secure co-processor, and understand the attack surface between them and what defenses are provided
The goal is to gain fundamental and transferable knowledge on what it takes to have trust rooted in hardware.
What is a Root of Trust?
There is an old story about what holds up the world. Some ancient cultures pictured the Earth resting on the back of a giant turtle…which invited the obvious follow-up question: what holds up that turtle? Answer: another turtle. And under that one? Another turtle. It is turtles all the way down.
Every secure device you rely on, your phone, your car, the servers your bank runs on, is built the same way. It is layers, and each layer is forced to trust the layer underneath it. Your operating system trusts the bootloader that started it; the bootloader trusts the firmware before it; that firmware trusts the very first instruction the processor ever executes. Turtles, all the way down. Except a device cannot do this forever. At the very bottom there has to be one final turtle, and the entire stack is standing on it.
A hardware root of trust is that final turtle: a small, immutable piece of silicon that holds the device’s identity and its secret keys, decides what software is allowed to run at all, and that nothing else vouches for. Nothing else can, which is what makes it different in kind from every layer above it. It also sets a hard constraint. The final turtle has to be small enough to audit and simple enough to reason about, because a flaw in it cannot be patched by anything above it. Egret’s boot ROM is 32 KiB. The entire security argument of a device can rest on code that would be a rounding error inside an operating system kernel.
This chapter builds the mental model the rest of the book depends on: the five jobs a root of trust has to do, and how the blocks of a real chip map onto them.
The five jobs
The final turtle does five things. Everything else in this book is one of these jobs examined up close.
- Identity. Something no other device can claim (no matter how hard they try), descending from a secret the chip is born with and never reveals. An identity is something you can’t copy or overwrite.
- Secure boot. The process to ensure a system can refuse to run code that is not signed by a trusted authority, This is started at the very first instruction, before any mutable code gets a chance to lie about itself. A gap at the start of the chain leaves a gap in the entire chain of trust.
- Attestation. Proves to a remote party what hardware and software the device is actually running, in a form that the party can verify independently.
- Secure storage. Keep secrets and state confidential and tamper-evident against an attacker who physically holds the chip, opens the package, and reads the memories directly, which is a threat that the Pavona threat model calls out explicitly.
- Cryptography. Hashing, signing, key exchange, random number generation, and, more recently, post-quantum signatures, provided in hardware so they are fast, constant-time, and hard to observe. Protecting against side-channels here is crucial, since an algorithm that is mathematically correct but leaks its key through timing or power consumption is a broken algorithm.
These five are not separate features bolted together. Identity depends on secure storage to hold its root secret and on cryptography to derive keys from it; attestation signs its evidence with those keys; secure boot checks signatures with the same primitives. Egret wires that dependency directly into the hardware.
The jobs mapped onto Egret
Egret is a concrete instance of everything discussed above The datasheet’s top-level block diagram given above shows the Ibex RISC-V core, the memories, and the peripherals on one bus. Read against the five jobs, that diagram sorts into five groups.
| Job | Egret blocks that do it |
|---|---|
| Identity | Key manager (with DICE support), OTP memory (holds the root secret), KMAC (performs the key derivation) |
| Secure boot | ROM (verified on boot), the Ibex core’s ePMP (isolates each boot stage), asymmetric cypto and the boot ROM software (verify the next stage’s signature) |
| Attestation | Key manager (derives the identity keys that sign the evidence), asymmetric crypto (does the public-key signing) |
| Secure storage | OTP and flash (which has access control plus memory scrambling), the ROM and SRAM controllers (scrambling), and life cycle controller (gates what the chip will do at all) |
| Cryptography | AES, HMAC, KMAC, asymmetric crypto (RSA, ECC, and the lattice-based post-quantum algorithms), CSRNG with entropy source (randomness) |
Several blocks appear in more than one row, this overlap is on purpose. When later chapters zoom into one of these blocks, you can refer back to this table to see how it connects back to the whole.
The rest of the book is, in a sense, a slow walk through these table rows, one block at a time, first to understand what each one guarantees and then, in the final part, to integrate this within a larger system to find out what it does, and does not, do.
Why open silicon: OpenTitan and Pavona
In almost every device shipping today, the final turtle is a black box. The design is secret, the firmware is sealed, and you are asked to trust it because the vendor says so. That is a strange arrangement for the one component whose failure nothing else in the system can compensate for. A root of trust asks to be trusted absolutely, which is precisely why it should be open to inspection. When the RTL, the boot ROM, and the verification environment are all public, the security argument can be checked by anyone who cares to, and the design earns trust by being examined rather than by being hidden.
OpenTitan, from the lowRISC project, is where that
changed. It is the original open-source silicon root of trust, open all the way
down to the chip design itself, and it runs on RISC-V. It is also not a paper
design. As of March 2026, OpenTitan silicon fabricated by Nuvoton
ships in commercially available Chromebooks,
the project’s first production deployment. Its top_earlgrey
configuration is the most thoroughly documented, and its silicon_creator C
code is the reference implementation of secure boot.
Open here means more than a published block diagram. Both designs in this book
ship their Verilog, their silicon_creator C code, their register descriptions,
their design verification testbenches, and their threat model under Apache 2.0.
That is also what makes this book possible at all. You cannot single-step a
proprietary secure element, and you certainly cannot watch its key manager
change state. In simulation, on open RTL, you can watch any signal in the
design.
Pavona is the newer design, built in part from OpenTitan
and steered by a foundation hosted by GlobalPlatform with
certification readiness as an explicit goal. It keeps OpenTitan’s architecture
and much of its software, and adds the acc asymmetric cryptography
coprocessor: a separate processor with its own memories and its own big-number
ISA, running RSA and ECC today and carrying vectorized instructions for the
lattice-based ML-KEM and ML-DSA algorithms. Pavona is the reference we actually
build and run, and the configuration that we focus on is top_egret, the
discrete secure microcontroller we will boot, decompose, and break.
Note, the two projects and implementations play slightly different roles in this book.
Egret is what we run. Every command you type, every boot log you read, and
every block you break is Egret in Verilator. Earl Grey is what we read when
Egret’s own documentation is unclear or lacking, which it may be, because OpenTitan is older,
more thoroughly written up, and the so-called silicon_creator code it has is the canonical
secure-boot implementation. The two are close enough that most of what you learn
about one transfers to the other, and this book says so explicitly each time it
steps from one to another.
Threats and trust boundaries
“Secure” is not a property a chip can have on its own. Every security claim is relative to an attacker with particular capabilities, and to a boundary that separates who is in charge of what. Pavona documents both, and both are scoped to Egret (and Dragonfly), so they describe the exact chip we are about to boot:
- The lightweight threat model says who is attacking and with what.
- The logical security model says who is in charge at each stage of the chip’s life.
One tells you who is trying to tip the final turtle over, the other who is entitled to stand on it. Read them early. Nearly every later chapter is one row of one of these two documents examined in hardware.
Who is attacking
The threat model names three attacker profiles:
- Physical access, during manufacturing or out in the field.
- Malicious device owners, attacking a device they legitimately possess in order to develop an attack that works on every other device of the same type.
- Remote actors, who can still measure side channels at a distance.
These are each interesting on their own, but let’s consider the second one for a second. A root of trust has to keep secrets from the person holding it, which is obviously harder than keeping secrets from someone across a network…this will explain some of the design decisions we’ll encounter that might look paranoid, until you accept the threat model that’s been chosen.
The attack surface is correspondingly wide: the chip surface itself, open to inspection, reverse engineering, and physical manipulation; the operating environment, meaning temperature and power; peripheral interfaces and the APIs exposed at the device boundary; the test and debug interfaces; the clock, power, and reset lines. Pretty much everything! It of course even includes the documentation and design data, which is the whole “open silicon” thing. But, interestingly, publishing the design is actually covered by the threat model, not a violation of it! This is because none of the security is meant to rely on obscurity (i.e., the design being secret).
The methods split into three families:
- Logical. Software bugs reachable through an interface, compromise of secure boot, impersonation of the silicon creator or owner, misuse of test and debug functionality, insider compromise of provisioning.
- Non-volatile memory. Firmware downgrade, data rollback, command replay. These target time rather than secrets, forcing the device back into a state that was valid once.
- Physical. Passive side channel analysis, extracting a key by watching execution time, power draw, or electromagnetic emissions while the device runs perfectly normally, and active fault injection, pushing voltage, clock, temperature, EM, or a laser outside specification to make the device skip a check or corrupt a comparison. Any combination of these are in scopes too.
Due to all of this, it becomes apparent why a large fraction of Egret is not actually compute or cryptography at all. Instead, as we’ll learn, it is redundancy and integrity checking: dual-core lockstep, bus and register file integrity, shadowed registers, hardened counters and program counter, alerts that escalate, and more. None of this helps against bad cryptography. All of it exists because of the rather terrifying number of physical-attack methods.
Who is in charge
The logical security model draws the other boundary, and it is about entities rather than adversaries. A Pavona chip deals with up to three over its lifetime:
- Silicon Creator. Designs, manufactures, tests, and provisions the chip with its first identity, the Creator Identity, which is what proves the chip is genuine.
- Silicon Owner. Whoever currently owns the device. It validates the Creator Identity, provisions a second identity of its own that the creator does not know, and supplies the functional software stack.
- Application Provider. Supplies the applications running on top of that stack, and can derive further identities from the owner’s, isolated from each other by owner software.
Note that these roles are “logical”, meaning they can also collapse. One vertically integrated company that creates the silicon and provisions all of its software leaves a single identity rather than two (essentially the TPM model).
The boundary is drawn in software stages, and owning a stage means controlling the key that signs it. The Silicon Creator owns the ROM, immutable after manufacture, and the ROM_EXT, a modifiable extension that patches hardware issues, applies the security settings judged too risky to hard-wire into ROM, and performs creator identity provisioning. As those stages run they lock out the parts of the chip that later stages have no business touching, and they do not execute again until the next reset. Everything after that belongs to the owner. Because creator code is measured into the Creator Identity, and because it is open, a prospective owner can inspect exactly what it is inheriting before accepting a chip.
Taking all of the above sections together, we see how the threat model tells you what a given mechanism is defending against, defining attacker profiles demonstrates how intense things can get out in the real world, and the entity model tells you whose key forms the base of trust at each step along the boot chain. When we trace from the initial ROM all the way up to owner firmware, or watch a device carry two separate identities, or step through a lifecycle transition, we must keep this model and its boundaries in mind.
Booting a chip on your laptop
The entire Pavona Egret design and source is completely open. You can read, edit, and create a cycle-accurate model of the whole chip that can run directly on the laptop in front of you. So, let’s get hands-on!.
By the end of this part, you will have compiled the chip into a simulator, booted real software on it,
watched control pass from ROM to flash, printed Hello World! over a simulated
UART, and traced those characters back to the exact hardware registers used.
Everything runs in Verilator, which magically translates synthesizable Verilog into a C++ program. This allows you to exercise things one clock edge at a time. But be warned, this fidelity has some costs, for example, the first build may take on the order of ten minutes, and booting the thing (which, in real silicon would finish in milliseconds) takes about a minute. Not that bad though! And what this buys you is visibility no physical chip can offer. Ibex writes out every instruction executed, and any signal anywhere in the design can be dumped to a waveform, including signals that would not reach any pin on real silicon. Clairvoyant!
Note
Booting a chip and securing a chip are different achievements. What we are about to run is the test ROM, a development image whose own README calls out as testing-only, and it verifies no signatures whatsoever. The real secure boot ROM lives in
sw/device/silicon_creator/romand is subject to later parts. This part is just to show the chip coming up, that it runs our code, and how we can watch it happen.
The chapters for this part:
- Development environment sets up the toolchain (Bazel, fusesoc, Verilator, and the Python tooling).
- Building Egret in Verilator covers building the chip, from RTL through fusesoc to a compiled simulation binary, and the three memory images it boots from.
- Hello, World! boots real software, reads the log line by line, and turns on the instrumentation.
- Reading the memory map closes the part by mapping the chip so you can navigate and locate each block by address.
Every command here is one you run against your own Pavona checkout, and you walk away with a reproducible green boot you can get back to from a clean tree.
Development environment
Before you can build a chip you need the tools that turn its source into something runnable. Pavona’s toolchain is larger than a typical firmware project’s because it spans two worlds at once, hardware and software…and the same repository builds both. Understanding what each tool does makes the setup feel less like wizardry and more like a normal down-to-earth toolbox.
At the center is Bazel, invoked through a wrapper script
called bazelisk.sh that fetches the exact Bazel version the project expects.
Bazel is the single entry point for almost everything. It builds the RISC-V
software that runs on the chip, and it also drives the hardware build. When you
ask Bazel for a sim_verilator target, it runs
fusesoc to gather the RTL, invokes Verilator
to generate and compile the C++ model, builds the device software, and finally
launches
opentitantool
to load the memory images and stream the chip’s output back to your terminal.
You rarely call Verilator, fusesoc, or the opentitantool yourself; Bazel orchestrates
them, which is why nearly every command in this part starts with ./bazelisk.sh.
Note
Bazel is like
make…but on steroids. You name a target and it works out what to rebuild just likemake, except Bazel also goes and fetches its own tools (Verilator included), runs every step sandboxed so the result does not depend on what is lying around your machine, and caches test results and not just build outputs.
In the container we’re going to use, Verilator is built from source rather
than installed from a package, because distribution packages tend to lag badly
behind. Pavona pins the version in third_party/verilator/extensions.bzl (5.046
at the time of writing) and Bazel compiles it for you the first time you build a
simulation target. That compile is the bulk of the first ten-minute wait, and it
only happens once as long as Bazel’s cache survives, which is why the
container instructions below bind-mount a directory for it. Second, the Python tooling (topgen, regtool, dvsim, and the
scripts that generate register files and documentation) runs in a
project-specific virtual environment, installed from python-requirements.txt
with version/hash pinning, so its dependencies never collide with any system Python packages in the container.
There are two options to get your dev environment set up. Pavona’s getting started guide documents a native one running on a Ubuntu host. It works well if you already run a supported Ubuntu version. The container path below is the simpler option and works more broadly, thus it is what the rest of this book assumes.
Containerized build with Podman or Docker
Pavona provides a container definition at util/container/Dockerfile, built on
Ubuntu 22.04, that installs every system dependency the getting started guide
lists. Your locally checked out version of the Pavona repo stays on the
host and is bind-mounted in, so your edits and your git history live in one
place and outlive any container you recycle.
Bazel’s build cache does not, unless you mount a second directory for it. By
default Bazel caches under ~/.cache/bazel, and inside the container that
resolves to /home/dev/.cache/bazel, which is not part of the bind mount
above. See the command below for instructions on how to mount this.
We use Podman here, but every invocation below is
identical under Docker (substitute docker for podman, drop the --userns
and --user flags explained below since Docker does not need them, and note
that Pavona’s own instructions run docker build under sudo).
Build the image once from the top of your Pavona checkout:
podman build -t pavona -f util/container/Dockerfile .
Then start an interactive shell in it, mapping your checkout to /home/dev/src
and a host directory to /home/dev/.cache so Bazel’s cache survives a
recycled container. The DEV_UID and DEV_GID variables give the container’s
dev user your own user and group IDs, so files it creates land back on the
host owned by you rather than by root:
mkdir -p ~/.cache/pavona-bazel
podman run -it \
--userns=keep-id \
--user root:root \
-v $(pwd):/home/dev/src \
-v ~/.cache/pavona-bazel:/home/dev/.cache \
--env DEV_UID=$(id -u) --env DEV_GID=$(id -g) \
pavona:latest \
bash
Note
Rootless Podman gives every container its own private user namespace, so by default a process the container thinks is UID 1000 is not the same as UID 1000 on your host, it is remapped. Using
--userns=keep-idfixes that by mapping your real host UID into the container as itself, but it also makes the container start as that UID instead of root, which breaks theDEV_UID/DEV_GIDremap the entrypoint script needs to run as root.--user root:rootputs root back for the entrypoint.
Ready
You can now open a shell inside the container and run the Bazel wrapper:
cd ~/src && ./bazelisk.sh
With no arguments it fetches the pinned Bazel release and prints Bazel’s usage message. This is enough proof that the wrapper works and the workspace is is good to go. Onwards!
Building Egret in Verilator
Building a chip sounds like it should involve a fab. Luckily not the case when doing simulation! Simulation takes the Verilog that describes Egret and turns it into a program that behaves exactly like the chip would.
From RTL to a running model
The build has two halves that Bazel stitches together. The first half is the
hardware. FuseSOC reads Egret’s core description, follows its dependencies to
collect every RTL file the design pulls in, and hands the whole set to
Verilator. Verilator translates that synthesizable Verilog into C++ and compiles
it into a single executable, Vchip_sim_tb, the cycle-accurate model of the
entire chip. Bazel exposes it as the //hw:verilator target.
The second half is the software. The RISC-V toolchain compiles the C you want to
run, hello_world.c to start, into an ELF, then converts that ELF into a flash
image in the form the chip expects. This image is one of three images the
chip needs in place before its first clock edge, the next section covers
where the other two come from.
The four memories, and the three you fill
Egret has four kinds of memory, knowing what each contains is necessary to get to a properly booting chip.
- ROM: 32 KiB of memory based at
0x8000, holds the first code the core executes. In simulation this is the test ROM, whose only job is to bring the chip up and jump to flash. - Flash: two 512 KiB banks based at
0x20000000, holds our applications. Thehello_worldimage lives here. - OTP: 2 KiB of one-time-programmable memory, holds no executable code at all. It carries root secrets, configuration, and the life cycle state.
- SRAM: 128 KiB based at
0x10000000, is working memory, populated at runtime rather than pre-loaded.
At reset the chip needs images for the three non-volatile memories and fills
SRAM as it runs. ROM code is what runs first, flash is what it hands off to, and OTP
is the configuration both of them read. When a run starts, opentitantool spawns
the chip model with exactly those three, as --meminit=rom0,...,
--meminit=flash0,... and --meminit=otp,.... The trailing digit on the first
two is a slot number, since ROM and flash can each be loaded in more than one
piece.
Interestingly, the ROM image is scrambled before it is loaded, and its words are 39 bits wide
rather than 32. Those extra seven bits are ECC (error correction code). Egret’s rom_ctrl descrambles
and integrity-checks every fetch on the fly, so the image on disk is already in
the form the hardware expects to see, which is why the file is named something
like test_rom_sim_verilator.39.scr.vmem. The top eight words of ROM are not
code either. They hold the expected 256-bit digest of everything below them, and
at power-on rom_ctrl hashes the ROM and compares. You met the consequence of
that check in the front matter: the digest it
computes is forwarded to keymgr.
The ROM image is not just the first code to run, it is an input to the chip’s
identity. See rom_ctrl’s
theory of operation
for the scrambling scheme, the 39-bit word format, and the digest-to-keymgr
handoff.
The OTP image is the default img_rma, which puts the chip in the RMA life
cycle state. RMA leaves debug features enabled, including JTAG access to the
main processor, which is exactly what you want while learning (and exactly what a
production device must NOT allow). Pavona’s threat model lists test and debug
interfaces as an attack surface in their own right, so keep in mind that the
chip you are booting is deliberately in its most open configuration for now.
Building the Hello World image
The example is at sw/device/examples/hello_world/ in the Pavona repo, three files: the BUILD
target, a README.md, and hello_world.c. A single Bazel invocation compiles it:
./bazelisk.sh build sw/device/examples/hello_world:hello_world
Note
The extra colon between the directory and the target name is Bazel’s label syntax, not a typo.
The build produces a set of files under bazel-bin/sw/device/examples/hello_world/, each named for its execution environment:
hello_world_sim_verilator.elf # the linked RISC-V binary
hello_world_sim_verilator.dis # its disassembly, human-readable
hello_world_sim_verilator.map # the linker map
hello_world_sim_verilator.64.vmem # the flash image the model loads
The sim_verilator in every name is the execution environment:
a program declares which environments it supports, Pavona then builds one variant per environment,
named <program>_<environment>. hello_world declares two, sim_verilator and
sim_dv. Larger tests such as sw/device/tests:aes_smoketest declare a dozen,
including FPGA boards and real silicon, from the same C source. The bare label
:hello_world is a suite over those variants rather than a binary itself, which
is why building it produces the sim_verilator files above.
Tip
Alongside
sim_verilator, Egret definessim_verilator_rom_with_fake_keys, which is the same simulated chip booting the realsw/device/silicon_creator/rominstead of the test ROM, with development signing keys. That is the environment the secure boot tests undersw/device/silicon_creator/rom/e2erun in, and it is where the next part starts. Everything in this part deliberately stays simple and on the test ROM.
A just have a compiled hello_world_sim_verilator target in bazel-bin/, with its ELF,
disassembly, map, and vmem image, and a Verilator model that is built and
cached. The next chapter goes over actually running all of this.
Hello, World!
We’ve prepared everything needed. One command can now be used to boot the simulated chip/model, run your software, and print things back over a simulated serial port.
./bazelisk.sh test sw/device/examples/hello_world:hello_world_sim_verilator --test_output=streamed
The target name is the program with the _sim_verilator execution environment
appended, and because it is a test target Bazel treats a successful run as a
passing test. This one line has Bazel build the Verilator model if it is not
already cached (the long ~10 min initial step), then hand off to opentitantool, which
spawns the model with the three memory images and connects to its simulated
UART. The --test_output=streamed flag is what makes the chip’s output appear on
your terminal in real time instead of being captured silently.
The run itself takes about a minute once the model is built. What you are watching is the simulated chip executing one clock edge at a time, faithfully enough that the UART bytes it emits are the bytes real silicon would send. Woohoo!
Note
Clocks are something that diverge in simulation vs. silicon.
In our execution environment, the software is told the core runs at 500 kHz and the peripherals at 125 kHz, with the UART at 7200 baud (you can read for yourself in
sw/device/lib/arch/device_sim_verilator.c). Egret in silicon runs its core at 100 MHz. Cycle-accurate means every clock edge is modelled, not that the clock is set to the frequency a real chip would use. Each simulated cycle costs extra time in simulation, so things are kept lower in sim.
Reading the boot log
A successful run prints something close to this:
I00001 test_rom.c:200] kChipInfo: scm_revision=54697461
I00002 test_rom.c:276] Test ROM complete, jumping to flash (addr: 20000480)!
I00000 hello_world.c:35] Hello World!
I00001 hello_world.c:38] Built at: Jul 25 2026, 22:00:47
I00002 hello_world.c:41] PASS!
Your run will differ in some details. For example, the revision number, the build timestamp, and the source line numbers all track whatever Pavona commit you checked out, so treat the line numbers as pointers to find rather than values to match.
The first two lines come from test_rom.c, the code in ROM that runs before
anything else. It reports the chip’s revision and then announces that it is
finished and jumping to flash, at 0x20000480 here, just past the base of the
flash region.
The next three lines come from hello_world.c running out of flash. Each is one
LOG_INFO call in the source, and the annotations point back at the lines that
emitted them. PASS! is Pavona’s test harness watching the UART
stream and deciding the verdict from it, matching PASS for success.
A run that came up, printed happily and never said PASS! is a failing test.
Running it again
Ask Bazel to run the same test a second time and you will likely see it report
(cached) PASSED almost instantly. Bazel caches test results and will not re-run
a test whose inputs have not changed. That is usually what you want, but when you
are iterating on the chip or the software and need to force a real run, disable
it:
./bazelisk.sh test sw/device/examples/hello_world:hello_world_sim_verilator \
--test_output=streamed --cache_test_results=false
Seeing more than the UART
There is much more to look at then just the UART output!
Ibex logs every instruction it executes. The trace lands in
trace_core_00000000.log inside Bazel’s cache, one tab-separated row per
instruction. There is actually a lot more than just the instructions though.
In the log file, you’ll find info about simulation time, cycle count, program counter, the instruction
word, its disassembly, then the registers it read and wrote and, for loads and
stores, the address it touched and the data it moved. Wow!
The log file is a bit buried, so to find it…use find:
find ~/.cache/bazel -name "trace_core_00000000.log"
That file is the complete software execution history of the boot you just watched, from the first instruction out of reset to the last.
For hardware state rather than software state, Verilator can dump an FST waveform.
Note
Turning on Verilator waveform tracing (
--trace=/tmp/sim.fst) makes the simulation run WAY slower than without tracing, so the test’s timeout value needs to be raised proportionally or Bazel may kill the test before it finishes.
./bazelisk.sh test sw/device/examples/hello_world:hello_world_sim_verilator \
--test_output=streamed \
--test_timeout=1000 \
--test_arg=--verilator-args=--trace=$HOME/src/sim.fst
Note
Neither GTKWave nor Surfer is in Pavona’s container image. If you want to view it, open the file on your host system, in whichever waveform viewer you have installed on the host, e.g., GTKWave or Surfer.
Tip
The simulation also prints its own process ID at startup and there is a trick worth noting: sending
SIGUSR1toggles tracing while it runs. On a long simulation you can leave tracing off, wait for the interesting moment, and capture only the window you care about instead of gigabytes of boot.
A good start
Great! We now have a green hello_world_sim_verilator run with Hello World! and PASS! on our
screens, an understanding of which lines came from ROM and which from the flash
application, and a clear sense of what the run did not check.
This starting point is what the rest of the book builds on, let’s keep going!
Reading the memory map
We’ve booted the chip. Now let’s take a second to learn how to navigate the different things on it. A microcontroller is, from software’s point of view, a set of hardware blocks crammed onto a single flat address space. You talk to any block by reading from or writing to its corresponding addresses. The memory map is the directory of that address space, and it is a reference we will need for every remaining part of this book.
Where the map comes from
Egret’s map is not written by hand, it is generated by Pavona’s so-called Architectural Composition Engine (ACE). Run it from the repo top:
make -C hw top_and_cmdgen
That target uses a script called topgen.py under the hood, but Pavona’s own build
always runs ACE through the Makefile, never topgen.py directly.
hw/top_egret/data/top_egret.hjson is the generated machine-readable description of how the chip is
assembled, listing every block, its interfaces, and where it lands in the address
space. Additionally, if you open hw/top_egret/sw/autogen/top_egret_memory.h and you will find
#define TOP_EGRET_UART0_BASE_ADDR 0x40000000
which is the same number, from the same source, as the uart0 row of the map.
The address in the document, the address decoded by the bus, and the constant
your C compiles against cannot drift apart, because all three are from
the same single top_egret.hjson map file.
Walking the address space
Peripherals begin at 0x40000000 with the
four UARTs, then GPIO, SPI, and I2C. uart0 at 0x40000000 is the one our
Hello World! came out of. Higher up sit the blocks that make this a root of
trust rather than a microcontroller:
| Block | Base address | Size | Its job |
|---|---|---|---|
otp_ctrl | 0x40130000 | 0x1000 | one-time-programmable storage: root secrets, life cycle state |
lc_ctrl | 0x40140000 | 0x100 | the life cycle controller: what the chip will and will not do |
aes | 0x41100000 | 0x100 | symmetric encryption |
hmac | 0x41110000 | 0x2000 | hashing and message authentication |
kmac | 0x41120000 | 0x1000 | Keccak-based MAC, used in key derivation |
keymgr | 0x41140000 | 0x100 | the key manager: derives Egret’s identity keys |
csrng / entropy_src | 0x41150000 / 0x41160000 | 0x80 / 0x100 | random number generation |
rom_ctrl | 0x411E0000 | 0x80 | the ROM controller that checks the boot ROM |
acc | 0x41300000 | 0x20000 | the asymmetric cryptography coprocessor |
Note, most blocks occupy a few hundred bytes: keymgr, the block that derives
the device’s identity, is a 0x100 block (sixty-four 32-bit registers). acc is
0x20000, a full 128 KiB. That is rather big for a register file. Turns out, it’s
big because it’s not just a register file mapped here for acc. Instead, it’s the
entire coprocessor’s instruction and data memories mapped into the address space -
that makes more sense given this block runs its own programs! Notably, this is also the first
hint of the need for a memory security boundary, which you can actually see described in the
ACC documentation:
Ibex loads a program and its inputs, sets a bit, and cannot read that memory back
while ACC is running.
Each row here is a later chapter. otp_ctrl and lc_ctrl are the Secure storage
and lifecycle part; keymgr, kmac, and otp_ctrl together are the Identity and
keys part; acc is the Post-quantum crypto part. The memory map is where those
chapters attach to the running chip, because every one of them is reached through
an address in this table.
Below the peripherals are the memories you loaded images into: ROM at 0x8000,
main SRAM at 0x10000000, and flash at 0x20000000. These are the addresses the
boot log referred to when the test ROM announced it was jumping to flash at
0x20000480, just past the base of the flash region.
Cross-referencing the disassembly
The map above becomes a bit more real when you reference it against the software we built. Open the disassembly from previous chapters:
less bazel-bin/sw/device/examples/hello_world/hello_world_sim_verilator.dis
LOG_INFO in hello_world.c turns into stores to uart0’s registers, all of
them inside the 0x40 byte window at 0x40000000. Find one of those stores in
the disassembly and match its address to the map’s uart0 row. Neat!
Even more, if you captured the instruction trace in the last chapter, you can do this a second way. The trace records the address and data of every load and store the core executed, so the same UART writes appear there as they actually happened, in order, with cycle counts attached. The disassembly tells you what the program intended; the trace tells you what the chip actually did. Reading both against one map is…a pain obviously, but if weird things start to happen in our adventures and we can’t explain them easily, this knowledge and approach will be our best (and last) resort to get more info.
Speaking of adventures, let’s continue ours in the next chapter.