Learning Tock from the ground up · Chapter 1
‹ ContentsA microcontroller gives software exactly one move: put a number at a numbered location. Everything the hardware does, it does because of that. This chapter follows one such move all the way down to a wire changing voltage — and shows why it forces an operating system like Tock to exist.
0xD0000018 and say which piece of hardware answers to it, and why that one.Plan on forty minutes, not twenty. There is nothing to install and no hardware needed, and nothing here is a race.
In the book: nothing on this. It names memory-mapped I/O in doc/design and development/peripheral without stopping to say what one is.
These get used constantly from here on. None is hard, and you do not have to remember them — each is repeated in context when it matters.
What is worth having now is a place for each one, because they name things in three different places.
Every word the chapter uses, including these, is collected in the glossary at the end.
Click a word. The picture shows you where that thing actually is.
Three of them name something you could point at on the board in front of you.
Three name part of a number, which is the only thing that ever travels between the other two.
Three name something sealed inside the chip, which you will never see.
Start with what is physically in front of you, because it is smaller and plainer than people expect.
The chip is a black square about 7 mm on a side, soldered flat, with nothing sticking out of it. What you actually touch is the board it sits on.
Click any hole — or tab to the board and use the arrow keys. Find the one your light will go in.
26 holes carry a GPIO you may drive. Any of these will light a small lamp. This is the ordinary case.
2 of those are already spoken for. Tock sends its own console output through GPIO 0 and GPIO 1, so driving either one cuts off the kernel while you watch.
8 holes are 0 volts. Seven are plain ground; the eighth is a quieter ground kept for the four pins that measure voltages. Your lamp’s far end goes to one of the seven plain ones.
5 holes are power, not signal. Some feed the board and some are fed by it. None of them answers to a store.
1 hole resets the chip. Pull it to 0 volts and the RP2350 stops and starts again.
Four of the chip’s thirty GPIOs never reach a hole at all. The board kept them, and what it kept them for is the difference between the two boards:
On the wireless board the light is not on the RP2350 at all. It hangs off the radio chip, and the only way to reach it is to ask the radio.
Each of those holes is a pin, and when the chip drives one, it can do exactly two things. That is the whole vocabulary. Every blinking light, every spinning motor, every message down a wire is built from that one choice, repeated quickly.
Turn it on and off. Watch which parts of the picture change, and which do not.
High. The pin is connected to 3.3 volts. Current runs from the pin, through the lamp, to ground, and the lamp glows.
Low. The pin is connected to 0 volts. Both ends of the lamp are now at the same voltage, so no current runs and the lamp is dark.
0x02000000 goes to a different place.If you have used an Arduino you have done this by writing digitalWrite(25, HIGH). It reads like an order given to a device: pin, turn on.
But nothing in the chip receives orders. There is no device listening. So how does a line of code change a voltage?
The answer is short, and it is why this chapter exists.
One instruction puts one number at one address. That is the entire mechanism.
Everything else is bookkeeping around that. The interesting part is how the chip turns a number and an address into 3.3 volts on one particular pin, and it does that by answering three questions in a row.
Each question has a different answer, and mixing them up is the main reason this subject feels slippery. So they get one section each.
The same word means two different things, depending on the machine.
Which stretch is which was burned into the silicon once, by the chip designers. So the chip's first question is: who does this address belong to?
It answers it by looking at a single digit.
That digit is written in a counting system you may not have met: sixteens rather than tens, called hexadecimal, flagged by the 0x in front.
Sixteens need sixteen digits, so the usual ten run on into six letters.
One hex digit is exactly four bits, always. So an eight-digit address is exactly 32 bits, and each digit owns a fixed slice of them. Take one apart.
Pick a digit. Then flip its four bits below and watch that digit change.
The four bits of the digit you picked, the one at bits 31:2827:2423:2019:1615:1211:87:43:0. Each one is worth what it says, and the digit is whatever they add up to.
That is not a simplification invented for teaching. It is what the hardware does, and the datasheet says so in one line:
Rough address decode is first performed on bits 31:28 of the address:
Bits 31 to 28 are the top four bits, which is the first hex digit. Before anything else happens, the chip reads that digit and hands the whole store to whichever block owns it. The remaining digits are that block's business.
Sixteen values a first hex digit can take, and each one routes a store to some block of the chip. How many of the sixteen name real hardware?
Nothing catches the rest. Nine of the sixteen belong to nobody, and the datasheet is blunt about them: Unmapped address ranges raise a bus error when accessed.
Closer, and still generous. Seven is not half of sixteen, and the seven are not evenly spread either — two of them are next door to each other and one sits on its own at the far end.
Right, and the nine that are left are the interesting part. This space is mostly empty, with a few occupied stripes, which is the opposite of how a laptop's memory feels. A store into one of the nine is a fault rather than a value going quietly nowhere.
The digit means everything. It is the whole routing decision, made on four bits before any of the other twenty-eight are looked at.
Click a first digit. See who claims the address, and who does not.
ROM answers. Built-in code, burned in at the factory. This is where both processors start looking when power arrives.
Flash answers. Your program. The kernel and the applications both live here, and the chip runs code straight out of it.
SRAM answers. The only general read/write memory. Variables, stacks and heaps. Holds whatever was there at power-on until something writes it.
Nobody answers. Nothing is wired to this range. The store raises a fault rather than quietly vanishing.
Peripherals answers. The big shared group: clocks, resets, serial ports, timers, and the blocks that configure the pins electrically.
Peripherals answers. A second peripheral group on a faster internal connection, used by the blocks that move a lot of data.
Nobody answers. Nothing is wired to this range. The store raises a fault rather than quietly vanishing.
Nobody answers. Nothing is wired to this range. The store raises a fault rather than quietly vanishing.
Nobody answers. Nothing is wired to this range. The store raises a fault rather than quietly vanishing.
Nobody answers. Nothing is wired to this range. The store raises a fault rather than quietly vanishing.
Nobody answers. Nothing is wired to this range. The store raises a fault rather than quietly vanishing.
Nobody answers. Nothing is wired to this range. The store raises a fault rather than quietly vanishing.
Nobody answers. Nothing is wired to this range. The store raises a fault rather than quietly vanishing.
SIO answers. The block that owns the output state of every pin. This is the one an address that moves a light has to reach.
Processor's own registers answers. Controls belonging to the processor itself rather than to the chip around it.
Nobody answers. Nothing is wired to this range. The store raises a fault rather than quietly vanishing.
So the D in 0xD0000018 is not decoration. It is the routing decision, and it sends the store to the block named SIO.
SIO stands for single-cycle input/output. It is the peripheral that owns the output state of every pin, which is why an address that changes a light starts with D and not something else.
The RP2350 has 520 kB of SRAM. How does the kernel know how much of it there is to hand out?
Nothing asks. There is no register that reports how much memory is fitted, and no discovery step anywhere in the boot sequence — the number has to be written down by somebody.
Right, and it was wrong for the whole life of this port. The script declared 264 kB, a number inherited unchanged from the RP2040 Pico board where 264 kB was correct. Half the chip's memory was never handed out to anything. Nothing prevented the kernel touching it; the port simply did not know it was there.
The code that describes the chip holds the addresses of peripherals, not the size of memory. Where a board's memory starts and ends is the board's business, and boards differ.
That gives the bottom of the free space, not the top. Something still has to say where the memory stops, and nothing on the chip will tell you.
Click any stripe to see what is really there, and where that fact was checked.
Code etched into the chip at manufacture and unchangeable afterwards. The datasheet puts it plainly: located at address zero, which is the starting point for both Arm processors when the device is reset.
Chapter 3 is largely about what this code does before yours runs.
Two things share this space: a 28-byte header declaring the image runnable, and a 256-byte loader that sets up flash so code can run straight out of it. Chapter 3 opens them up.
The kernel itself. Code, so it never changes at runtime. The header in the boot block points here, which is how the chip finds it.
Your compiled applications, one after another. Note that they sit in a different part of flash from the kernel; that separation is where isolation starts.
The general read/write memory: kernel data and every application's memory come out of this one block. Look hard at the size, because for the whole life of this port so far it was wrong. The RP2350 has 520 kB, but Tock's linker script for this board declared 264 kB — a number inherited unchanged from the RP2040 Pico board, where 264 kB was correct. Half the chip's memory was never handed out to anything: nothing prevented the kernel touching it, the port simply did not know it was there. It was corrected to 520 kB on 20 August 2026, three days after the commit this chapter is pinned to. That makes this stripe the one place on the page reporting a tree newer than the pin. A linker script is a claim about the hardware, and claims can be wrong.
Decides how fast the chip runs. Get this wrong early in boot and nothing else works.
Holds other blocks switched off. Most of the chip is dead until something clears its bit here.
Decides what each pin is for. A pin can be plain output, or a serial line, or several other things; this picks which.
The electrical side of each pin: how hard it drives, and whether it is gently pulled high or low when nothing else is driving it.
The crystal oscillator: a real quartz crystal on the board, and the most accurate timing reference the chip has. It is not the only one. Two less accurate oscillators sit inside the chip, and at reset the processors are running from one of those.
Multiplies the crystal's frequency up to the speed the processors run at, as high as 150 MHz.
A second multiplier, fixed at 48 MHz for USB, so USB timing survives retuning the main clock.
The serial port Tock's console uses. Kernel messages left the chip one byte at a time through a register in here.
A second, identical serial port. Same layout, different base — the pattern from Figure 12.
A counter that never stops, ticking once a microsecond. Reading it gives the current time, which has nothing to do with anything you wrote.
Divides a clock down to the slower rates the timers and the watchdog need.
The pin-driving block, and the one this chapter is about. The datasheet calls it Core-local Peripherals
and says each processor gets its own connection to it. gpio_out_set sits 0x018 past this base, which is where 0xD0000018 comes from.
Controls belonging to the processor itself rather than to the chip around it. What sits here is the memory protection unit that later chapters use to fence applications in, the processor's own timer, and the controller that decides what it stops for. This is the odd entry in the list. The datasheet notes these registers are “internal to the processors, not accessed through the system bus”, so a store here is not routed by the fabric at all. Each processor sees a different set behind the same address.
Question 1 is now settled, and it settles a lot. But reading a stripe off a map is not the same as knowing what is behind it.
The map tells you who answers. It says nothing about what answering means, and that turns out to be where people go wrong. First, though, the address has more to say.
SIO is not a single switch. It is a panel of them, and each switch on that panel is a register with its own address.
Registers are laid out the same way on every peripheral of this chip, and on nearly every chip you will meet. The block gets a base address — SIO's is 0xD0000000 — and each register sits a small fixed distance beyond it. That distance is the register's offset.
So the address we started with splits into two decisions that have already been made.
A warning about the word "register." It has two unrelated meanings and this chapter needs both.
r0, r1 and so on, holding whatever it is working on this instantSame word, different things. Where it matters below, the text says which one.
Here is SIO's output section: the offsets and their order exactly as Tock's chip crate declares them, with the address arithmetic worked alongside.
Click a register. Watch its address get built out of two halves.
Four of these eight are for the high half: GPIO 32 to 47, plus the pins that talk to the flash chip and the USB socket. Each one sits directly after its twin. Take them away and watch what the offsets do.
0x018 and 0x18 are the same number — a leading zero pads the column and changes nothing. Later figures print it both ways, because the padding is only there to line the column up. The offsets and the order are chips/rp2350/src/gpio.rs, lines 88–108, with none dropped; the last two columns are written for this page rather than quoted. Tock's driver for this chip neither reads nor writes any of the four gpio_hi_ registers.Two patterns there are arithmetic you can check rather than trivia you have to trust. Neighbours land 4 apart because a register is 4 bytes and addresses count bytes, so every offset is a multiple of 4. Do not expect every step to be 4, though: a block can leave gaps, and this file leaves several further down. Take the twins away and the four that remain are 8 apart, the same reason applied twice.
Those higher GPIO numbers make a point the code cannot. Two packages carry this same silicon, and the datasheet spells out the consequence:
GPIOs 0 through 29 are available in all package variants. GPIOs 30 through 47 are available only in QFN-80 (RP2350B) package.
Change the package. The three facts under the same either way hold for both.
RP2350A — the one on your deskQFN-60 — sixty pins on a square 7 × 7 mm. Thirty GPIOs, numbered 0 to 29, and four analogue inputs. Bits 30 and 31 of gpio_out are real bits in a real register with no pin behind them, and so is every GPIO bit of the four gpio_hi_ registers.
RP2350BQFN-80 — eighty pins on a square 10 × 10 mm. Forty-eight GPIOs, numbered 0 to 47, and eight analogue inputs. Bits 30 and 31 drive pins here, and the four gpio_hi_ registers drive GPIO 32 to 47 — the same registers, finally with something behind them.
0xD0000000. gpio_out_set sits 0x018 past it. Tock's crate for this chip defines GPIO0 through GPIO29 and stops, whichever package the silicon came in.
Hold on to that. It comes back in question 3, where it explains something that otherwise looks like a mistake.
Before any explanation, go and play with this. Drag the slider, watch which square lights and what the code line underneath says, and see whether you can work out the rule before it is stated.
Drag to choose a pin. Try 25, then 0, then 30.
Here is the rule you were looking for. A register is 32 bits wide, and SIO spends those bits one per pin: bit 0 is GPIO 0, bit 25 is GPIO 25. A group of pins driven by one register like this is called a bank.
To act on pin 25 you need a number with bit 25 set and every other bit clear. Written in code, that is 1 << 25: take the number 1, and shift it 25 places to the left. That is the whole reason Tock's source says 1 << self.pin.
So the three questions are answered by three different mechanisms, and now they can sit in one line together.
That is the anatomy. What it does not show is the order things happen in, and the question this chapter opened with was a question about order: how does a line of code change a voltage?
So here is the same store again, one moment at a time.
Drag the handle, or use the arrow keys, to move through the eight moments.
One honest qualification. That trace assumes the pin was already set up. Two things had to be arranged first.
To drive a pin with the SIO's GPIO registers, the GPIO multiplexer for this pin must first be configured to select the SIO GPIO function.
Both are stores to other registers, in other blocks, obeying exactly the three questions above — which is why they are not a special case worth learning separately. Tock does them when it starts. So "one instruction changes the pin" is true of changing the pin, and the setup that made it possible is its own story.
Worth doing once by hand, because the arithmetic is the idea. Below is the calculation fully worked for pin 25, then the same calculation with pieces missing.
Work down the three cards. They get emptier as you go.
Bit 25 is the twenty-sixth bit, counting from bit 0 at the right. Shifting 1 left by 25 places lands it there and leaves everything else at zero.
The address does not depend on which pin you want. Only the value does.
Turning a pin off is a different register, not a different value. Figure 7 has it.
0x. Nothing is scored and nothing is recorded.Now the payoff from question 2. Drag Figure 8 up to pin 30 or 31 and the square goes dashed, because Tock's crate for this chip defines GPIO0 through GPIO29 and stops.
Those two bits are perfectly real. The store would succeed and drive nothing, because this package brings out no such pin. On the 80-pin part they drive GPIO 30 and GPIO 31 exactly as you would expect.
So the lesson is sharper than "the register has spare bits."
How wide a register is and how many pins exist are two separate facts. Only one of them is visible from the code.
Everything above is a fact about the chip. What a pin is wired to is a fact about the board, and those are different documents written by different people.
On a plain Pico 2, the board's datasheet lists GPIO 25 as "Connected to user LED", which is why the store we have been building lights something. On the wireless boards it is wired to the radio instead. Raspberry Pi's own header for the Pico 2 W says so:
#define CYW43_DEFAULT_PIN_WL_REG_ON 23u
#define CYW43_DEFAULT_PIN_WL_DATA_OUT 24u
#define CYW43_DEFAULT_PIN_WL_CLOCK 29u
#define CYW43_DEFAULT_PIN_WL_CS 25u
#define CYW43_WL_GPIO_LED_PIN 0
pico-sdk, src/boards/include/boards/pico2_w.h — five lines picked out of that file and grouped together. The values are exact; the order and the surrounding #ifndef guards are not.
So on a Pico 2 W the store still works perfectly. The voltage on that pin still changes. It just moves the radio's chip-select line rather than a light, and the onboard light is pin 0 of the radio — which the chip can only reach by talking to the radio first.
If you want to see this with your own eyes on a W: pick any GPIO from 2–22 or 26–28. The board takes 23, 24, 25 and 29, and Tock itself takes 0 and 1 for the console, so driving either of those would cut off the kernel's own output. Put a light and a resistor between it and a ground pin. The resistor is what sets the current, and 330 ohms is the conventional choice. That lands right about at the pin's 4 mA default drive strength, and anything up to a kilohm still lights an LED. Then everything in this chapter applies unchanged, with your pin number in place of 25.
This is the first real lesson about layering, and it is worth more than the LED. The chip says "pin 25". The board says what pin 25 reaches. Confusing the two is how people lose an afternoon to a light that was never there.
Reading this again will not make it stick. Neither will highlighting it — that is one of the few study habits the research is actually rude about.
What does work is producing the answer before you look at one. Three questions, three choices each: commit to one and the reasoning opens underneath it. Getting one wrong is useful information, not a problem — it is the whole reason to answer before reading.
1. An address begins with 0x4. What do you already know about it?
Right. Not quite. That it belongs to the peripherals on the APB side of the chip — the serial ports, the timers, the pin-configuration blocks. The first hex digit is the whole routing decision, so it is the only part of an address you can interpret without looking anything up.
2. Why is a register listing written as offsets rather than finished addresses?
Right. Not quite. Because the offsets are the part that is shared. UART0 and UART1 are the same hardware built twice, so 0x000 is the data register on both; only the base differs. Writing offsets lets one driver serve both, and it matches the processor's addressing mode, which takes a base in a register plus a small fixed offset.
3. A 32-bit register drives a bank of pins. How many pins does the chip have?
Right. Not quite. Unknown from that fact alone — and this is the trap. The part on a Pico 2 has 30, so bits 30 and 31 drive nothing. The 80-pin version of the same silicon has 48, and those bits drive real pins there. Register width and pin count are two independent facts.
Next
The three questions are answered and the light is on. Now try the obvious next thing: read that address back.
Chapter 2 is what happens when ordinary instincts about variables meet a register. What a read gives back is a separate decision from what a write does. An optimizing compiler will quietly delete a loop that polls one. And a second processor can undo your value between your reading it and your storing it.
It ends on the sentence the rest of this series answers.
Worth carrying forward from this chapter:
0xD0000000.0x. One hex digit is exactly four bits.0xD0000000. The peripheral owning the output state of every pin.Every claim on this page comes from one of two documents: the Tock tree at commit 83bad9388, and the RP2350 and Pico 2 datasheets. Nothing here was written from memory. One number has moved in the tree since that commit — the Pico 2's declared RAM size — and Figure 6 reports the correction rather than the pin.
0xD0000000, IO_BANK0 0x40028000, PADS 0x40038000 — chips/rp2350/src/gpio.rs:1164–11690x010–0x02C, including the gpio_hi_ siblings — chips/rp2350/src/gpio.rs:88–108, "Output value for GPIO32...47, QSPI IOs and USB pins"GPIO0–GPIO29 — chips/rp2350/src/gpio.rs:1245–1250PADS_BANK0: GPIOx, field DRIVE, reset 0x1pico-sdk/src/boards/include/boards/pico2_w.h agrees. Tock has no Pico 2 W board yet, so no claim here rests on the kernel treeVBUS 40, VSYS 39, 3V3_EN 37, 3V3 36, ADC_VREF 35, AGND 33, RUN 30) — Pico 2 datasheet §3.1 and its pinout figure. The figure counts AGND with the grounds instead, which is what it is electrically, so it shows eight of those and five power pins. The Pico 2 W datasheet lists the same seven at the same numbers, which is why one drawing serves bothboards/raspberry_pi_pico_2/layout.ld. Its RAM length read 264K at the pinned commit; it was corrected to 520K by PR 5086, merged 20 August 2026METADATA_BLOCK and a 256-byte FLASH_BOOTLOADER — boards/raspberry_pi_pico_2/src/flash_bootloader.rsThe datasheets are linked from Raspberry Pi's silicon documentation page. They are licensed no-derivatives, so this page quotes them briefly with attribution rather than reproducing them.
One earlier draft of this chapter said SIO was reached by a route separate from the peripheral fabric. That was wrong. The datasheet says it is accessed via the AHB Fabric, with a dedicated bus interface per core. The mistake came from recalling the RP2040, where SIO does sit on a single-cycle port off the processor. Different chip, different answer, and it is recorded here rather than quietly fixed.
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.