Learning Tock from the ground up · Chapter 3
‹ ContentsChapter 1 followed one instruction down to a wire changing voltage. Something had to be running that instruction. This chapter follows the chip from the instant power arrives to the first line of Tock's main() — and most of that journey turns out to be a search.
Plan on forty minutes. Chapter 1 is assumed: what an address is, and what a store does.
In the book: doc/startup, which walks the same sequence in prose. This chapter watches it happen on an RP2350.
Each is one sentence now and repeated in context later. None is hard.
Power arrives. Both processors come out of reset and begin fetching instructions — but not from your program.
They begin at address 0x00000000, which chapter 1's map of the address space labelled ROM: built-in code, burned in at the factory. It is unchangeable, it is the same on every RP2350 ever made, and it has never heard of Tock.
So the chip's first job is to find your program. Most of what follows is that search.
Both processors start together, and they do not stay that way. The boot code's first decision is to read CPUID, which lives at 0xD0000000 — the SIO block chapter 1 spent its whole length on. Core 1 reads a 1 there, goes into a deep sleep, and takes no further part. Everything below is core 0.
Pick a moment. The panel says who is in control and what they are doing.
Both processors come out of reset and start together. Within a few instructions core 1 is asleep, and nothing of yours has been looked at yet.
Code burned into the chip at manufacture takes control at 0x00000000. It searches flash for something that looks like a program, and it will hand over only if it finds one.
The reset handler runs, sets up memory, and calls main(). Everything chapter 1 described happens after this point.
The ROM does not trust flash to contain a program. Flash might be blank, half-written, or holding something that was never meant to run.
So it searches the first four kilobytes for a block: a structure that says, in a format the ROM knows, "I am an executable image, here is how to start me."
Tock's block is twenty-eight bytes long and sits at the very front of the image. Every word of it is doing a job.
Click a word. These are seven words at consecutive addresses at the very front of flash, which is the only reason a scan can find them; the panel says what the ROM does with each.
The marker that says a block starts here. The ROM scans for exactly this number, which was chosen to be unlikely to appear in real Arm or RISC-V instructions.
One item, one word long, describing the image: it is executable, it runs in Secure mode, it is Arm rather than RISC-V, and it is for this chip family.
The header of a second item, two words long. Its type says vector table, so the next word is an address.
That address. It is where the vector table sits, and it is also where the kernel itself begins — one kilobyte into flash, right after this block.
The item that says there are no more. Its size field counts the words of items above it, which is three: one for the image type, two for the vector table.
Where the next block is, relative to this one. Zero means this block, so the list of blocks is a loop containing only itself.
The matching end marker. A block with no footer is not a block, and the ROM keeps looking.
Those bytes are METADATA_BLOCK in boards/raspberry_pi_pico_2/src/flash_bootloader.rs, and the format is the RP2350 datasheet, §5.9.
The block is twenty-eight bytes and the kernel does not start until 0x10000400. Chapter 1 said two things share that space. Here is the other one.
The loader is the interesting one, because it is no longer required. On the RP2040 every image had to begin with a routine like this, checksummed, or the chip would not boot. The RP2350's own ROM sets the flash up well enough to run before it hands over.
Tock ships one anyway, and the reason is speed rather than correctness: the ROM's setup is deliberately cautious, and a program is free to replace it with a faster one. That is what these 256 bytes are.
The bytes are FLASH_BOOTLOADER in the same file, and its comment records where they came from. The datasheet is explicit that they are optional now — §5.9.5.
The ROM now knows where to look, and what it finds there is not an instruction. It is a list of addresses.
A Cortex-M processor expects a program to begin with a vector table: one entry for each thing that can happen to it. The first two entries are the only ones that matter at boot.
0x10000400
Click either of the first two entries. The rest are for later.
Not code — a number. The processor loads it into the stack pointer before running anything, because code needs somewhere to keep its working values. Tock puts that stack at the bottom of RAM on purpose: overflowing it runs off the end into nothing, which faults, instead of quietly overwriting something.
The reset handler: the address of the first instruction of yours that runs. The processor jumps here, and everything after this point is Tock.
The reset handler has a name that says exactly what it does: initialize_ram_jump_to_main. It is two loops and a call.
It has to be, because RAM at power-on holds whatever it happened to hold. Not zero. Whatever was there, from the last time the board ran or from nothing at all. Every static — every variable that lives for the whole run, rather than one inside a function — has to be put there by somebody, and that somebody is these two loops. Variables inside a function are a different story, set up by that function as it runs.
main()
Pick a loop and step it. Both walk a range of memory one word at a time, using the store from chapter 1 — and the first thing to notice is what RAM holds before either of them has run.
_etext — the values the linker put there
_szero — six words of it
RAM at power-on holds whatever it held. Not zero — whatever was there. Step the loop and watch it become something a program can rely on.
Zeroing. Each step is one store of r2, which is 0, and the ! on stm r0! is what moves the address on. Notice the second word: it was already zero, and the loop writes it anyway, because nothing told it not to.
Copying. Each step is a load from flash and a store into RAM, with both pointers stepping. These are the values a static mut N: u32 = 5 needs, which is why they had to be kept in flash until now.
Done, and the loop stopped because r0 reached r1. Nothing counted anything: the comparison is against an address the linker wrote, and that is the whole termination condition.
Writes zero, one word at a time, from _szero to _ezero. That range is every static your program expects to start at zero. The ! on stm r0! is what steps the address forward, so the loop needs no separate add.
Copies, one word at a time, from _etext in flash into the range starting at _srelocate in RAM. That range is every static expecting to start at something other than zero, and the values it copies were stored in flash by the linker. Then bl main, and Tock is running.
Both loops are initialize_ram_jump_to_main in arch/cortex-m/src/lib.rs, shortened here only by dropping the comments. The five names beginning with an underscore are defined by the linker script, not by any Rust code.
Which loop a variable belongs to is decided by one thing: what it starts as. The consequence is not the one people expect, and it is easier to drag than to read.
Two declarations in your program: a 4 kB buffer of zeroes, and a 100-byte table of constants. Which one takes more room in flash?
Bigger in RAM, and free in flash. Nothing needs storing, because the value is zero and there is already a loop that writes zeroes over a range.
Right, and it is the opposite of the instinct. Asking for more memory can cost nothing; asking for a particular value costs flash, one byte per byte. Those hundred bytes have to be written down somewhere and copied in before your code runs.
Both end up in RAM. Only one of them has to be carried in flash to get there, and it is the one whose contents somebody chose.
There is no page rounding in this accounting. The image carries exactly the bytes whose values were specified, and a range of zeroes specifies nothing.
Pick what it starts as, then drag how big it is. One of the two bars grows and the other does not, and which is which is the whole of this figure.
static mut BUF: [u8; 1024] = [0; 1024];
Starting at zero costs nothing to store, because zero is what the first loop writes anyway. The linker records the range and no more.
Starting at five means the five has to be kept somewhere until the chip runs. It is kept in flash, four bytes of it, and the second loop copies it into RAM.
A kilobyte of RAM, and still nothing in flash. The first loop writes zeros over the range, so a large buffer full of zeros is free to store and only costs the time to clear it.
The linker script says both halves out loud. _szero and _ezero "define the range of the BSS, SRAM that Tock will zero on boot". The data section is placed with AT (_etext), which is what puts its initial values in flash. Both lines are in boards/build_scripts/tock_kernel_layout.ld.
Every step above, as one list. Nothing here is new, but walking it end to end is where the shape shows: how much of starting a program is the chip looking for it.
Eight moments between power reaching the chip and your kernel running. At which of the eight does code you compiled first execute?
It jumps straight to something, and that something is not yours. Address zero is ROM burned in at the factory, and it is where both processors start looking.
Still not yours. Five of the eight are the chip looking for your program: reading ROM, finding flash, reading a header, checking it, setting flash up so code can run out of it.
Right, and it is further in than almost anyone guesses. Five of the eight are the chip hunting for your program, one is the loader it found, and only the last two are your program running.
By the eighth the kernel is already running. The handover is one step earlier, and what happens at the eighth is your own code continuing.
main(), in eight steps
Walk it from power to main(). Four things about the machine are shown above the steps, and the moment worth watching for is the one where three of the four change at once.
The linker script names an entry point, jump_to_bootloader, and it is tempting to read that as where the chip starts. It is not.
It is there for the debugger. Loading a program over the debug probe does not reset the chip. Without this stub the debugger would start you at the front of flash, which, as Figure 2 showed, is metadata rather than code.
So this stub sends the chip back to the beginning, using five instructions you have already seen and one you have not.
Click a line. Five of the six you have already met; the panel says which one you have not.
The source writes the second constant as 0xe0000000 + 0x0000ed08, which is the same number said in two halves.
Put zero in a register. It is about to be used as an address, and as a value.
The address of the register that tells the processor where its vector table is. Chapter 1's map labels that whole stripe as the processor's own registers.
The store from chapter 1, doing something drastic: point the vector table back at zero, which is the boot ROM's own.
Read the first two words at zero — the ROM's stack pointer and its entry address — exactly as the hardware would at reset.
Set the stack pointer to the first of them. This is the one new instruction: msr writes one of the processor's special registers, and the stack pointer is one of those rather than an ordinary numbered one.
Jump to the second. The chip is now doing what it would have done on a power-on, and the whole search happens again.
Four questions, three choices each. Commit to one and the reasoning opens underneath it.
1. Where does this chip fetch its first instruction?
Right. Not quite. From 0x00000000, which is the boot ROM. That code was burned in at manufacture and is the same on every one of these chips. Your program does not run until the ROM has found it and handed over.
2. Why does a Tock image begin with twenty-eight bytes that are not instructions?
Right. Not quite. So the boot ROM can recognise it. Flash might be blank, or half-written, or holding something never meant to run. The block is how an image says what it is, and where to start it.
3. What does the first loop of the reset handler do?
Right. Not quite. Writes zeros over the range between _szero and _ezero. RAM at power-on holds whatever it held, so anything expecting to start at zero has to be put there.
4. One variable is declared = 0, another = 5. Which costs flash?
Right. Not quite. The one set to 5. Its value has to be kept somewhere until the chip runs, so it sits in flash and the second loop copies it. Zero costs nothing, because zero is what the first loop writes anyway.
Tock is running. It has the whole chip: every address, every peripheral, and nothing standing in the way — which is exactly where chapter 1 left off.
The kernel is not one program, though. It is a core with drivers bolted on, and a driver is where most of the code, and most of the mistakes, live.
So the question chapter 4 asks is: when you add a driver, what should it be allowed to touch?
Capsules, and the interfaces they are handed. A driver that cannot reach hardware it was not given — enforced by the type system rather than by the chip.
On the way: where the unsafe from chapter 2 lives, and why almost none of Tock needs it.
Every claim on this page comes from the Tock tree or the RP2350 datasheet. Nothing here was written from memory.
CPUID sitting at SIO's base, offset 0x000 — chips/rp2350/src/gpio.rs, the SIORegisters block0xffffded3 and 0xab123579, and the rule that the last item counts the words of the block's items — RP2350 datasheet §5.9.10x03 — RP2350 datasheet §5.9.3.3boards/raspberry_pi_pico_2/src/flash_bootloader.rsFLASH_BOOTLOADER in the same file, and RP2350 datasheet §5.9.50x10000400 being where the kernel begins — the rom region's origin in boards/raspberry_pi_pico_2/layout.ldinitialize_ram_jump_to_main — chips/rp2350/src/lib.rsarch/cortex-m/src/lib.rs.stack section in boards/build_scripts/tock_kernel_layout.ld_szero and _ezero marking "the range of the BSS, SRAM that Tock will zero on boot" — boards/build_scripts/tock_kernel_layout.ldAT (_etext), which is what puts its initial values in flash — the same filejump_to_bootloader in boards/raspberry_pi_pico_2/src/main.rs0xE000ED08 being the vector table offset register — RP2350 datasheet, Table 202Text, diagrams and interactive figures © Jon Hillesheim 2026, licensed CC BY-SA 4.0 — share and adapt freely with credit, under the same license. Tock source excerpts quoted above remain under their own Apache-2.0 OR MIT license and are not relicensed here.