Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

BlockBase addressSizeIts job
otp_ctrl0x401300000x1000one-time-programmable storage: root secrets, life cycle state
lc_ctrl0x401400000x100the life cycle controller: what the chip will and will not do
aes0x411000000x100symmetric encryption
hmac0x411100000x2000hashing and message authentication
kmac0x411200000x1000Keccak-based MAC, used in key derivation
keymgr0x411400000x100the key manager: derives Egret’s identity keys
csrng / entropy_src0x41150000 / 0x411600000x80 / 0x100random number generation
rom_ctrl0x411E00000x80the ROM controller that checks the boot ROM
acc0x413000000x20000the 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.