Learning Tock from the ground up · Chapter 4
‹ ContentsChapter 3 left the kernel running and holding the whole chip. Every address, every peripheral, nothing standing in the way. Then you add a driver. This chapter is about how much of that the driver gets, and the answer turns out to be one field.
Plan on forty minutes. Chapters 1 and 3 are assumed: what an address is, what a store does, and how the kernel got started. So is enough Rust to read a struct with a lifetime and a type parameter on it, which the Rust Book reaches in its chapter 10.
In the book: doc/design and development/hil, which give the rule this chapter tests by handing a driver something it was not meant to have.
Each is one sentence now and repeated in context below. None is hard.
Chapter 3 ended with Tock running and holding the entire machine: every address, every peripheral, and nothing in the way — which is exactly where chapter 1 left off.
That is defensible for the kernel. It is thirty-two thousand lines, and every one of them is read by people who work on Tock itself.
Tock calls them capsules, and this chapter uses both words for the same thing: the crate is named one and the job is the other.
Drivers are a different proposition. There are eighty-eight thousand lines of them, they are written one board at a time, and they are what a newcomer writes first. So the question chapter 3 ended on: when you add a driver, what should it be allowed to touch?
The usual answer is a rule and a code review. Here is what you may do; please do not do the other thing. Tock's answer is different, and it is the subject of this chapter. A driver reaches less than the kernel not because it agreed to, but because there is no way to write down the thing it would reach.
Change the declaration and read the table. One row says no in all nine combinations, and finding out which one is the whole of what this chapter is for.
Two parts never move. 'a is the only lifetime this driver has, and it says the references in leds outlive the driver holding them. The board is what guarantees that, and Figure 7 shows the same slot filled in with 'static. And NUM_LEDS is not: a pin number. It is how many there are, fixed when the board is compiled. The driver checks against it by hand one line above the call, if data >= NUM_LEDS, because indexing past the end of an array in safe Rust does not corrupt anything. It panics, and what that costs is Figure 8.
| can this driver… | |
|---|---|
| turn LED 3 on | yes |
| ask whether LED 3 is on | yes |
| reach LED 9, when the board handed it four | no |
| make a pin an input | no |
| say which chip it is running on | no |
| read or write through an address of its own | no |
| touch anything the board did not hand it | no |
The real declaration, in capsules/core/src/led.rs, 158 lines including its own documentation. One field, one lifetime, and a bound of five methods — init, on, off, toggle, read. What the name leaves out is any chip at all: nothing here or in that file says RP2350.
Ten more methods, because gpio::Configure declares ten. The driver can now make its LEDs into inputs, which is a strange thing for an LED driver to want. It is the bound that decides, not the board and not what the type turns out to be.
The type parameter is gone and the field names the chip's own type, so this is now a driver for one chip. Everything RPGpioPin has is reachable, and the price is on the readout: this file no longer builds for any board that is not this one. The generic version is not a style choice, it is what makes one driver serve every board Tock runs on.
A second thing handed over, and the reach grows by exactly what was handed. It can configure these pins because the board chose to give them; it still cannot reach a pin that is not in that slice, and nothing in the declaration would let it name one.
It may hold the pointer. It may not read or write through it, because that needs the word unsafe, and the crate this lives in opens with #![forbid(unsafe_code)]. So the field is inert: an address the driver owns, and no way to reach it. That is what the whole of this chapter is describing.
At the top of the crate that driver lives in there is a single line: #![forbid(unsafe_code)].
Rust has four settings for a rule like this, and they are not equally strong.
Capsule crates use the last one. So a capsule containing the word unsafe is not something a reviewer catches. It does not build.
Pick a line, then move it between the two crates. Two of the three change their answer and one does not — and nothing about the lines themselves changes when you move them.
No unsafe anywhere in it, so the crate's rule has nothing to say about this line. It builds in either crate, and it is the only one of the three that does.
The crate opens with #![forbid(unsafe_code)], and this line contains the word. That is not a reviewer catching it and not a warning: the build stops. forbid is also the level that refuses to be switched off further down, so no code inside the crate can take it back.
The chip crate does not forbid the word, so the same line is ordinary code here. Nothing about it changed — not a character — and this is where it really lives.
Legal, and this is what the driver really does. Nothing here is an address: leds is the field from Figure 1, and on is one of the five methods the trait promised.
Refused — and not by a person. This is chapter 1's store written out by hand, and turning a number into something you can store through means dereferencing a raw pointer, which requires unsafe. The crate forbids the word, so the build stops here.
Also refused, and this is the interesting one, because those are the exact words that make the LED work. That is the line, verbatim, from chips/rp2350/src/gpio.rs:1168, and what makes it legal there and illegal here is only which crate it sits in.
Somewhere, something has to turn a number from the datasheet into a thing a program can use. Rust cannot check that step, because nothing in the language knows what is at 0xD0000000. The question is only how much code that step is allowed to contaminate.
Tock's drivers are about 88,000 lines of Rust. How many times does the word unsafe appear in them?
Close, and the reason is right: all five driver crates open with #![forbid(unsafe_code)], which is an error that code further down cannot turn back off. The word does appear, though, and where it appears is the interesting part.
Right. Five occurrences in eighty-eight thousand lines, none of them code. A capsule containing that word is not something a reviewer has to catch: it does not build.
No driver in this tree touches hardware directly. Reaching the machine is what its fields are for, and the fields were handed to it by a board.
That is the instinct from other kernels, where a driver is the part that pokes registers. Here the driver is the part that cannot: the chip crate holds every address, and drivers are handed types instead.
Pick a part of the tree. The counts are of the word unsafe in the Tock source at this chapter's pinned commit.
Eighty-eight thousand lines of drivers, and the word appears five times — every one of them inside a comment. Five crates make up this directory, and all five open with #![forbid(unsafe_code)].
The scheduler, the syscall layer, and the code that starts and stops processes. Notice what is not in that list: there is no allocator. The kernel is no_std and never asks for memory it did not already have, which is the problem chapter 8 is about.
Fourteen files describing one chip. Eighteen of the twenty-four are the same line with a different number in it, and Figure 5 is those eighteen.
Everything specific to a Cortex-M processor rather than to the RP2350: the vector table from chapter 3, the fault handlers, the register saving. Shared by every Arm board Tock supports.
Four, and all four are unsafe fn declarations rather than blocks: panic_fmt, get_peripherals, setup, and main. They are the entry points something outside Rust calls, plus the one this board offers to the Pico 2 W crate that builds on it.
The answer is twelve lines, and Figure 5 is those twelve. They read better after seeing what stands on top of them, so take one call from the top and follow it all the way down.
A process asks for its first LED to be turned on. How that request crosses from the process into the kernel is chapter 7; pick it up on the kernel's side, in the capsule.
Walk it down. Three questions are asked of every layer; watch for where their answers turn over, and whether they turn over together. The last step is the instruction chapter 1 spent its whole length on.
Command number 1 means on; the 0 is which LED. The capsule matches on the number and does nothing else with it.
The bounds check is the line above: if data >= NUM_LEDS, and out of range returns an error code. Nothing here names a chip yet.
The layer that knows this LED lights when its pin goes high. On a board wired the other way the type is LedLow, and this same method calls clear instead.
A trait method with no body: three lines of documentation saying what an implementation must do. "Set the GPIO pin high. If the pin is not an output or input/output, this call is ignored."
The first layer that knows this is an RP2350. It checks the pin is configured as an output, and if it is not, does nothing — which is the promise the trait made one step up.
Base 0xD0000000, offset 0x018, bit 25 set and no other. The store from chapter 1, reached without anybody above this line knowing a number.
Below the chip layer there is one more floor, and it is the last one. Each peripheral on the RP2350 gets a single declaration that turns its base address into a typed reference, and that declaration is where unsafe appears.
Roam the map with the slider, or press the button to jump from one named base to the next. Two things to find: how far apart they are, and the three blocks whose addresses are not written down anywhere — a function makes them from a base it is handed.
You are on a base exactly. This is one of the thirty-one addresses the chip crate names, and every one of them was written by somebody reading the datasheet.
Past a base, and inside no block this figure can vouch for. What the readout gives is the nearest named base at or below, and how far past it you are — not whether the block is that big. The crate names bases; how far each one reaches is the register struct's business.
Below everything the crate names on this bus. Nothing in the chip crate resolves here, which is most of the map: thirty-one addresses in six megabytes of space, and the spaces between them are the honest part of this picture.
This address is not written down anywhere. A const fn makes it from a base it is handed, so no line in any file holds the number. The two lines in pio.rs take a base and add to it, and three call sites hand them three different bases. A list of declarations cannot show you this address. Working it out is the only way to see it.
—
The clock generators. Chapter 1 called this the block that decides how fast the chip runs.
Holds other blocks switched off. Most of the chip is dead until something clears its bit here.
Decides what each pin is for: plain output, or a serial line, or one of several other things.
The electrical side of each pin — how hard it drives, and which way it is pulled when nothing is driving it.
The crystal oscillator: a real quartz crystal on the board, and the most accurate timing the chip has.
Multiplies the crystal up to the speed the processors run at.
A second multiplier, held at 48 MHz for USB so its timing survives retuning the main clock.
The serial port Tock's console uses. Kernel messages leave the chip one byte at a time through here.
A second, identical serial port. Same layout, different base.
A synchronous serial port. On the Pico 2 W this is how the kernel reaches the radio, over four wires instead of one.
A second one, same layout. Nothing on the plain Pico 2 uses it.
A counter that never stops, ticking once a microsecond.
Divides a clock down to the slower rates the timers and the watchdog need.
Sixteen channels that copy memory without the processor doing the copying. One StaticRef covers all sixteen, because the declaration names an array.
Which channels have finished, and which ones are allowed to say so. A named constant away from the channels above it rather than a number of its own.
A small programmable machine that drives pins on its own schedule. Three of these, and the same two lines describe all three.
The same block's interrupt registers, at a fixed distance into it. The distance differs from the RP2040's, which is the sort of thing a shared driver has to be told.
This one. Chapter 1 spent its whole length inside this block, and gpio_out_set sits 0x018 past this base. Every layer above this line works in types; this line is where the number stops being a number.
The function those twelve lines call is StaticRef::new, and it is declared unsafe because it cannot check its own argument. What it asks of the caller instead is written above it, in two lines:
TTwelve callers make that promise once each, and what backs it is the datasheet. Everything above them — the chip layer, the HIL, the capsule — works in types and never has to make it again.
A capsule cannot ask for anything. It is handed what it has, in the board's main(), and it has that for the rest of the run.
Click each line. One of the two is commented out, which usually means it does nothing. Work out what this one is still deciding.
The board hands the LED driver exactly one pin, and it is GPIO 25 — the pin chapter 1 lit. The driver did not ask for it, cannot ask for a different one, and will never know its number.
Forty lines earlier, the general-purpose GPIO driver is given its own list of pins a process may drive directly. Pin 25 is in that list, commented out, with // LED pin written above it. The board author took it back by hand.
Figure 1 said the driver knows only that its type implements a trait. That is what generic means in practice, and it is what makes the next comparison possible: there is no second copy of this driver for the second board.
The same LED driver runs on this board and on a different one — different architecture, different number of LEDs, and an LED wired the opposite way round. How much of capsules/core/src/led.rs differs between the two?
Right, and nothing about that is luck. Everything that differs between the two boards is in the type the board hands over, so the driver never learns which chip it is on and has nothing to say about it.
There is no conditional. Nothing in the file names a chip, an architecture or a pin, so there is nothing for a conditional to switch on.
Polarity is handled, and not here. The board wraps its pin in a type that knows which way round the LED is, and the driver calls the same method either way.
That is the arrangement this design exists to avoid, and it is what makes one driver serve every board Tock runs on.
Pick a board. Everything that differs between them is in the type.
LedDriver<'static, LedHigh<'static, RPGpioPin>, 1> — a Cortex-M33, one LED, wired to light when the pin goes high.
LedDriver<'static, LedLow<'static, sifive::gpio::GpioPin>, 3> — a RISC-V processor, three LEDs, wired to light when the pin goes low. A different instruction set, and the capsule does not notice.
Tock's own documentation calls a capsule semi-trusted, and the second half of that word is doing work.
Pick a capsule and pick what it does. None of these is a memory error and the compiler prevents none of them. What is left to ask is how far each one reaches, and the answer comes from what that capsule was handed.
There is one #[panic_handler] in the whole binary and its return type is !, meaning it never returns. The comment above that decision says the system "is no longer in a well-defined state". On this board the handler then takes GPIO 25 for itself and blinks it forever — the same pin Figure 6 handed to the driver. Which capsule panicked makes no difference at all.
A timeslice governs a process, not kernel code. A capsule is called by the kernel and runs until it returns, so one that never returns is never left. Nothing preempts it and nothing restarts it, and again it does not matter which one it was.
Here it does matter. What a capsule was handed is often shared. This board puts three capsules on one serial port and two on one timer, each through a virtualizer whose stated job is to "allow multiple Tock capsules to use the same UART bus". A capsule that takes more than its share degrades every other client of it, and nobody else.
Nobody else. This driver was handed GPIO 25 and nothing else, and Figure 6 is the reason. The board author took that pin out of the general-purpose GPIO driver's list by hand, so there is no second client to degrade. Hogging a thing you are the only holder of is not a failure anyone can see.
Figure 4 ended on the store, and its fifth step said the chip layer checks the pin is configured as an output and does nothing if it is not. The trait one step further up says the same: an implementation may ignore the call. Both are true. Neither says how that layer decides.
It decides from two registers, and the field that names what drives the pin is neither of them. Every pin on this chip has a function-select field, FUNCSEL, holding the block that owns it — SIO, or SPI, or one of a handful of others. The mode the chip layer reports is worked out without reading that field.
Choose what the board did to the pin, and read the guard's two inputs. Only one of the four leaves a pin that a store can move — and pointing it at SIO is not enough.
The pin belongs to another block, and get_mode cannot tell. It never reads FUNCSEL, and there is a literal //TODO - read alternate function on the line saying so. What it sees is a cleared output-disable and an unset gpio_oe, which is the pair it reports as Input. Every set and clear aimed at this pin returns having stored nothing, and returns (), so there is no error anywhere to notice. Pointed at SIO and still not writable, which is the part the SPI story hides. set_function clears the pad's output-disable and never touches gpio_oe, so the pair reads exactly as it did for SPI. Naming the right block is not the same as being an output. Untouched since reset. The pad's output-disable is still set, which is the one input that produces LowPower rather than Input — and it is still not a pin a store can move. The only one of the four that works. make_output is set_function(SIO), then activate_pads, then gpio_oe_set — and it is that third line, the one set_function does not do, that makes the pair read as an output.
The board points the pin at the SPI block. This writes FUNCSEL, clears the pin's isolation, and through activate_pads clears its output-disable bit. It never touches gpio_oe, the SIO register listing which pins SIO is driving, because SIO is not driving this one any more.
The line the guard turns on. It reads two things: the pin's output-disable bit, and that same gpio_oe. Disable cleared and gpio_oe unset is the pair it reports as Configuration::Input. Its first line is //TODO - read alternate function, and that comment is exact. FUNCSEL is never read here, so nothing it returns can tell a pin that is an input from a pin that belongs to SPI.
Input is neither Output nor InputOutput, so the store is skipped and set returns. Its return type is (). Nothing is reported, and there is no value for the caller to look at even if it wanted to.
So a pin handed to SPI reports itself an input, and every set and clear aimed at it returns having stored nothing. A chip select driven that way never moves, and there is no error anywhere to notice, because no layer thinks anything went wrong.
Every line quoted in this chapter is a fact about boards/raspberry_pi_pico_2, and that crate is the plain Pico 2. This branch also has raspberry_pi_pico_2_w, on the same chip, and it is a short file: it takes the whole platform from the crate above and changes one thing. Reading the two side by side is the shortest answer to what a board crate is.
One line above cares about the difference. The panic handler builds its own LedHigh on GPIO 25, under a comment reading // LED is connected to GPIO 25. On a plain Pico 2 it is. On a W that pin is the radio's chip-select line and the light is pin 0 of the radio, which chapter 1 laid out in full.
So the W crate blinks nothing you can see, and does not try: it prints and stops, because the light is on the radio. The message still comes out over the console on pins 0 and 1, and what goes missing is the blink, the one signal a dying kernel gives that needs no console at all. Build the plain crate on a W and it is worse than missing, because the handler drives the radio's chip-select line instead, and holds it there.
Nothing else here changes. Every boundary this chapter describes is drawn at compile time, and a compiler has no idea what a pin is wired to.
Memory safety, then, and not good behaviour. That is the whole of what a type system was ever going to give, and it is why chapter 5 is about code the kernel does not compile at all.
A capsule holds one field: &'a [&'a L; NUM_LEDS]. What would it take to read gpio_out_set from there?
A raw pointer, and the crate will not compile one. No HIL offers a raw register either, and the kernel is not asked at run time — the refusal happens at build time. Chapter 2 adds a second reason: that register is write-only, so even the chip layer is promised nothing by reading it.
Which of the six steps in Figure 4 first knows the chip is an RP2350?
Step 5. Steps 3 and 4 are in the kernel's HIL and are shared by every chip; step 5 is the first line in a file that exists only for this one.
A capsule hits a loop it never leaves. What stops it?
Nothing. A capsule is not scheduled, so there is no moment at which the kernel gets control back and could decide anything about it. This is the half of "semi-trusted" the type system does not cover.
A board author hands the same pin to two capsules. What catches it?
Nothing. Both capsules hold a shared reference, which is legal Rust, and both would drive the pin. Figure 6's commented-out line is a person preventing it, and this is the weaker of the chapter's two guarantees.
A driver in Tock is a struct whose fields are the whole of its reach, in a crate where the word that would let it reach further does not compile. The address it eventually moves is named twelve floors down, once, by a line that quotes the datasheet.
That answers the question for code that ships inside the kernel binary and is trusted only that far. It says nothing about code that is not trusted at all.
Next
A capsule is compiled with the kernel and cannot reach past its own fields. A process is a separate binary that the kernel has never seen, written by somebody else, possibly hostile. On the way: what a process actually is in flash and in memory, and what the isolation costs to run.
Every claim on this page comes from the Tock tree at commit 83bad9388. Nothing here was written from memory. The counts in Figure 3 were taken by counting the word in the .rs files under each path at that commit, once with comments stripped and once whole.
capsules/core/src/led.rs:71 and :112Led trait — kernel/src/hil/led.rs:19–32, "Return the on/off state of the LED"wc -l capsules/core/src/led.rs at that commitpub struct declarations under capsules/ at that commit, excluding tests and virtualizers; the largest is extra/src/rf233.rs#![no_std] at kernel/src/lib.rs:93, and no crate under kernel/ or chips/ declaring extern crate allocuart_mux and mux_alarm in boards/raspberry_pi_pico_2/src/lib.rs:292–380capsules/core/src/virtualizers/virtual_uart.rs:7, "this allows multiple Tock capsules to use the same UART bus"#![forbid(unsafe_code)] opening all five capsule crates — capsules/core/src/lib.rs:5, and the same line 5 of extra, system, aes_gcm and ecdsa_swLedHigh calling set and LedLow calling clear for the same on() — kernel/src/hil/led.rs:57–95Output::set contract quoted in Figure 4 — kernel/src/hil/gpio.rs:152–154, in the Output trait declared at :151chips/rp2350/src/gpio.rs:1514–1523set_function writing FUNCSEL and clearing the pin's isolation, and never touching gpio_oe — chips/rp2350/src/gpio.rs:1329–1337, with activate_pads clearing output-disable at :1353get_mode inferring the mode from output-disable and gpio_oe, under a //TODO - read alternate function — chips/rp2350/src/gpio.rs:1310–1321chips/rp2350/src/, the StaticRef::new call in each of clocks.rs, resets.rs, gpio.rs, xosc.rs, uart.rs, timer.rs and ticks.rs# Safety section on StaticRef::new, kernel/src/utilities/static_ref.rs:29–35boards/raspberry_pi_pico_2/src/main.rs:95 and boards/raspberry_pi_pico_2/src/lib.rs:347boards/raspberry_pi_pico_2_w/src/main.rs, which has no LED driver at allunsafe being panic_fmt, get_peripherals, setup and main — boards/raspberry_pi_pico_2/src/io.rs:89, boards/raspberry_pi_pico_2/src/lib.rs:201 and :226, and boards/raspberry_pi_pico_2/src/main.rs:87boards/hifive1/src/main.rs:242–246forbid being the level that also refuses to be turned off later — the Rust reference on lint levelsdoc/ExternalDependencies.md:161, "capsules are a mechanism to provide semi-trusted infrastructure to a Tock board"!, and the three capsule crates compiled into it — boards/raspberry_pi_pico_2/src/io.rs:88–89 and Cargo.toml:19–21kernel/src/debug.rs:280–284 and panic_blink_forever at :387LedHigh on GPIO 25 rather than going through the capsule — boards/raspberry_pi_pico_2/src/io.rs:90–96Kernel::do_process, kernel/src/kernel.rs:450–460, "Transfer control from the kernel to a userspace process"Text, 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.
Every line reference below links to that commit on the fork it was read from, at the lines it names.