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.