Learning Tock from the ground up · Chapter 0
‹ ContentsThe rest of this series reads Tock's source and checks every claim against the tree it lives in. This chapter is the forty minutes before that: putting Tock on a chip you can hold, and getting it to say one sentence back to you.
Plan on forty minutes, and most of it is wiring. Nothing in this chapter is assumed from the rest of the series, because there is no chapter before it. You need the hardware in the first figure and a machine you can install Rust on.
In the book: getting_started, which names five boards and not one of them a Pico. This is the route for a Pico 2.
Each is one sentence now and used in context below. None is hard, and several are ordinary words being used narrowly.
Everything after this reads source. That works from an armchair, and for five of the seven chapters that follow you never have to touch anything.
But the last chapter closes by telling you to stop reading and run a command, and until now the series never said how to get to the point where that command works. This chapter is that gap, filled in.
It is worth doing for its own sake as well. A claim about a kernel you have watched boot is a different kind of claim from one you have only read about. You notice which kind you are holding the first time something goes wrong.
You have the board and a USB cable. You skip the debug probe to save the money. What does that cost you?
It does, and that is exactly why people skip it. What it costs is not capability, it is the loop: four steps and two hands for every edit, and no console coming back the other way, so a kernel that dies mid-boot dies silently.
You can. Hold the button down while replugging and it comes up as a drive you can copy a file onto. It works; it is just four steps and two hands, every time.
The output is a separate matter from flashing, and there is more than one way to get it. What the probe changes is that both arrive on the same three wires you already have connected.
Nothing about Tock needs a probe. The probe is about the loop you run for every edit, which is the thing you will do a few hundred times.
Take the probe out of the list and put it back. Everything else on the bench is unchanged; what changes is the loop you run for every edit, for the rest of the chapter. Then click a part to read what it is.
make flash-openocdA Raspberry Pi Pico 2. The chip on it is an RP2350, and this repository has a directory named for exactly that board, which is what everything below builds. A Pico 2 W is a different board and there is a section about it near the end.
A Raspberry Pi Debug Probe. It is the part you could skip, and skipping it costs more than it saves. Without one you hold a button, replug the board, and copy a file by hand for every change.
Three wires from the probe's debug connector to the header labelled DEBUG. These carry the flashing. Both ends name the same three signals and they go straight across, so there is nothing to get backwards.
Three wires from the probe's serial connector to two pins on the board, plus a ground. These carry the talking, and unlike the debug three, two of them cross over. Figure 4 is that crossing.
Rust, and OpenOCD. Those two are the whole list for the route this chapter takes, and there is no converter among them. Which OpenOCD is not free choice, and the section on flashing says why. That short list is not true of the other three routes, and Figure 3 is about why. It can also be two machines rather than one, which is the section after Figure 5.
Tock pins its toolchain in a file at the top of the tree, so there is no version to choose and nothing to decide. At the commit this chapter is written against, rust-toolchain.toml asks for the nightly of 21 July 2026, and lists the targets it wants installed alongside it.
Rust reads that file on its own. The first build in a fresh clone spends a while fetching that toolchain, and after that:
$ cd boards/raspberry_pi_pico_2
$ make
Finished `release` profile [optimized + debuginfo] target(s) in 5.64s
text data bss dec hex filename
69164 0 19828 88992 15ba0 raspberry_pi_pico_2
That is the whole build, and the numbers are real: 7.2 seconds of wall time on an Apple laptop, of which 5.64 were the compiler. A kernel is a small program, and this is one of the quiet pleasures of the work.
The three numbers are worth a glance even now. Around 68 kB of code, no initialised data, and about 19 kB of space the kernel wants at runtime for variables that start out empty.
Click a part of the path itself. Before you do: of the four, which are about the chip, and which are about the machine you are typing on?
One directory at the top of the tree holds everything every build produces, for every board. Deleting it costs you nothing but the time to build again.
The full name is thumbv8m.main-none-eabi, and it is set for this board in its own .cargo/config.toml. Read it as: the Arm instruction set this chip speaks, no operating system underneath, and a standard calling convention. Your laptop is none of those things, which is the point.
The optimised build rather than the debugging one. Tock is built this way by default because an unoptimised kernel does not fit comfortably and runs slowly enough to change behaviour.
The kernel, as an ELF: the code, and a description of which addresses each part of it is meant to occupy. Everything in the next figure is a different answer to the question of how those bytes get onto the chip.
This board's Makefile offers four routes onto the chip. They are not four flavours of one thing: two drive the probe, two produce a UF2 and copy it to a drive that the board pretends to be when you hold its button down.
The difference that matters is not which route is more modern. It is that the two copying routes check for a folder, and when that folder is not there they print a suggestion and exit successfully. Where that folder is depends on the machine you are building on, which is what the next figure is for.
Pick the machine in front of you, then click a route. Two of the four change their answer and two never do — and the two that change are the two that fail without saying so.
The two copy routes test for a folder under /run/media, which is a Linux path. A Mac mounts removable drives under /Volumes, so that folder is never there and the copy never happens.
The folder the Makefile tests for is /run/media/$(USER)/RP2350, and this board's README gives the default as /media/$(USER)/RP2350. Those are different paths, and which one your desktop uses is a fact about your distribution rather than about Tock, so the two copy routes may or may not find the drive.
Told where the drive actually mounted, both copy routes do what they say. This is the fix, and it is one variable — but you have to know to look for it, because nothing about the failure suggests it.
Kernel only, down the probe. It hands the ELF straight to OpenOCD, which writes it, reads it back to check, resets the board and exits. No button, no drive, no converter. OpenOCD is the one thing you fetch, and which build of it you fetch is the only part of this that is specific to the chip.
Kernel only, as a UF2 copied to the drive. It needs a separate tool to make the UF2 first. Then it tests whether a folder exists and, if it does not, prints Please edit the BOOTSEL_FOLDER variable and stops — with a success status, so nothing downstream notices.
The same copy, with an application built into the image beside the kernel. It fails loudly if you give it no application, which is good, and then fails quietly in exactly the way above if you do.
Kernel plus an application, down the probe. This is the one the last chapter of this series sends you to, and it is the right one. It needs a compiled application to hand it, and the toolchain's arm-none-eabi-objcopy to put the two together, which is a later problem than this.
/run/media, which is a Linux path; a Mac mounts under /Volumes. So on a Mac the copy never happens, the command reports success, and the board keeps running whatever it held before. That reads exactly like a kernel that built fine and does nothing.The command in the next section resets the board as it finishes, and the one line you are waiting for goes out at that moment. If nothing is listening yet, you miss it — and a missed line looks exactly like a board that never started. So wire first.
The kernel prints to a serial line on two pins, and the probe has a second connector for exactly this.
Which pins is not a convention to look up. It is a line in this board's own source: pin 0 transmits and pin 1 receives, at 115200 baud. Those two pins are handed to the console and are not available for anything else.
Anything that speaks to a serial port will read it. A Mac has screen already; Debian and its relatives usually want picocom installed first. The probe presents itself as a serial device when plugged in, and the surest way to learn its name is to look before and after doing so.
Every command in this section has been run. The name the probe takes was read off a Raspberry Pi, where it is /dev/ttyACM0; the listing above is how to find it on a machine that calls it something else. That is why this gives you a method rather than a name to copy.
# find it: list before and after plugging the probe in $ ls /dev/tty.* # a Mac $ ls /dev/ttyACM* # Linux # then open whichever name appeared $ picocom -b 115200 /dev/ttyACM0
The probe has a lead labelled TX. The board has a pin labelled TX. Where does the probe's TX go?
Two devices both transmitting on the wire they each call transmit are shouting at each other and listening to nobody. Nothing reports it; the console is simply blank.
Right. Every serial connector is labelled from the point of view of the thing it is attached to, so one end's transmit is the other end's receive, and the pair has to cross.
Nothing works it out. These are two wires and a voltage; there is no negotiation on this link and no error when it is wrong.
Ground is a third wire and carries no data. Each direction has its own wire, which is why there are three.
Put each of the probe's three console leads on a board pin. Get it wrong on purpose — every wrong answer here is a real bench failure, and the point is that almost none of them say so.
Three of the six are settled, and are drawn dashed above. They run from the probe's debug connector to the header the board silkscreens DEBUG: SWCLK, GND, SWDIO, joining like to like. Both ends name the same three signals, so there is nothing to get backwards. Before any of it does anything: neither of the probe's connectors carries power. Both are three signal wires and a ground, so the board needs powering some other way, and wiring only the probe gives you a dead board and no reason for it.
The banner arrives when the board resets, and typing list gets an answer back. Both directions carry, and the two sides agree what a one and a zero are.
Nothing readable. The probe's transmit and the board's transmit are on the same wire, both driving it, so neither side can be heard. This is the mistake the figure is named for, and it looks exactly like a board that never started.
Occasional characters, mostly wrong. Without a shared ground the two sides have no agreement about what a one and a zero are, so the levels arrive but do not decode. The symptom is not silence, which is what makes it worth telling apart.
Nothing at all. Neither direction is connected, so the banner never leaves the board and nothing you type reaches it. Indistinguishable from a kernel that did not boot — which is why the flash reporting success is worth something.
The banner arrives, and then typing does nothing. The board's transmit reaches the probe, so you see everything the kernel prints; nothing carries the other way. This is the one silence that tells you which wire to look at.
Nothing arrives, though what you type does reach the board. The kernel is running and answering into a wire that goes nowhere. The most misleading of the five, because the board really is working.
With the probe wired to the board and a terminal open on it, one command does the rest:
$ make flash-openocd
OpenOCD is the one piece of software you install for this, and on the Mac these pages were written on it was the only thing missing. Everything else was already there for Rust.
Which OpenOCD is not a detail. The released version, and the one most package managers hand you, is 0.12.0, and it predates this chip: it carries a configuration for the RP2040 and none for the RP2350. So the command stops at Error: Can't find target/rp2350.cfg, having read two lines of configuration and never reached the probe. Raspberry Pi's own build has the file, and this board's README has the recipe. Installing OpenOCD is the step; installing the released one is a step that looks finished and is not.
What that runs is OpenOCD, an ordinary program on your machine that speaks to the probe over USB, and through it to the chip. The board's own configuration for it is two lines long: use a standard debug adapter, and the chip is an RP2350.
The command writes the kernel, reads it back to confirm it landed, resets the board and exits. If it cannot find the probe it says so and stops, which is the failure you want — loud, immediate, and about the thing that is actually wrong.
The reading back is silent when it works. It names the differing addresses when it fails and says nothing at all when it does not, so no news is the good news here.
You may also meet a five-line warning about old probe firmware and a low-performance workaround. It looks alarming and is not: the flash completes correctly either way.
That command ends by resetting the board, and with the console already open you watch the result arrive. What you are waiting for is one line, printed at the end of the kernel's own setup, before it looks for applications and before its main loop starts:
Initialization complete. Enter main loop tock$
That is the whole success signal. The first line comes from a single call near the bottom of this board's start-up code, and its only job is to be the thing you are reading now. The second is a prompt, and it is worth a moment of its own.
Reset the board, then type into it. Click any line the console prints to see what that line proves — and watch how long it takes before anything proves the wire going into the board.
Nothing yet. Reset the board and watch what arrives. Three things proved, all by one line: the kernel built, it landed on the chip, and the wire out of the board is carrying. Nothing yet says anything about the wire going the other way. And now the other direction. A reply is the only thing that could have proved it, which is why typing is the last step rather than the first.
Printed once, at the end of setting up. Seeing it proves three separate things at once: the kernel built, it landed on the chip, and the wire out of the board is carrying. That is a lot of evidence from one line, and it is still silent about the wire going the other way.
A prompt from a piece of the kernel that exists to be talked to. Its arrival proves nothing the banner did not, because it is printed outbound on the very same wire. What tests the other direction is typing something and getting an answer.
A second line can follow the banner, saying it could not load processes. It comes after, not instead: the kernel looks for applications between printing the banner and starting its main loop. An empty chip is not what produces it, because finding no applications is quiet, so this line means something actually went wrong.
Type it and the console prints a header — name, state and a few counters — and then one row for each application. After this chapter there are no rows, because you flashed a kernel and nothing else. That is the right answer, and getting any answer at all is the point: a reply is what proves the wire into the board.
Everything above assumed one machine doing both jobs, so that the computer which compiled the kernel is the one with the probe in it. That is the simplest arrangement and nothing is wrong with it.
It is not the only one. The two jobs have almost nothing to do with each other, and you may not want unfamiliar hardware plugged into the machine your work lives on.
What makes splitting them cheap is how little has to cross. The far machine needs no copy of this repository and no Rust at all. It needs OpenOCD, something that reads a serial port, and a way to receive one file.
Tock's own hardware testing is built this way, which is worth knowing as confirmation rather than as coincidence. Its workflow describes a testbed of small hosts sitting next to real boards, while the building happens on ordinary machines elsewhere.
Split the bench across two machines and watch which steps move. Three of the four stay where they were; only one changes its command, and nothing changes about the wiring.
Always the machine with Rust on it, and it never needs the probe or the board attached to it. This is the step you repeat most often, and the one timed earlier at 7.2 seconds.
One ELF: the file Figure 2 ended on. Copy it across however you like, and scp is plenty. Nothing else has to cross — no copy of this repository, no toolchain, and nothing you would mind leaving on a spare machine.
OpenOCD, on whichever machine the probe is plugged into. Where that is the machine you built on, make flash-openocd is this step and you are done. Where it is not, you run OpenOCD there yourself.
The same machine as the probe, because the serial line arrives over the same USB connection that carries the flashing. On a split setup the banner appears on the far machine, which is not the one you are building on.
Written out, the split is two commands rather than one:
# on the machine that builds
$ scp target/thumbv8m.main-none-eabi/release/raspberry_pi_pico_2.elf \
appliance:
# on the machine with the probe
$ K=raspberry_pi_pico_2.elf
$ openocd -f interface/cmsis-dap.cfg -f target/rp2350.cfg \
-c "adapter speed 5000" \
-c "program $K; verify_image $K; reset; shutdown;"
Those two -f arguments are not invented here. They are the two lines of this board's own OpenOCD config, spelled out, which is exactly why the far machine can get by without a copy of the repository. The rest of the line is what the Makefile target runs.
No single step here is hard. What is hard is that several different failures produce the same nothing, so without a way to separate them you end up changing two things at once and learning nothing from either. The figure below is that separation, run backwards: break the bench yourself and see which silence you get.
Set any of the four wrong. Watch the two readouts, and then read the third — because two of these faults are indistinguishable from outside, and knowing which two is the whole skill.
unable to find a matching CMSIS-DAP device, and exits non-zero.
list prints a header row and no rows under it.
Nothing at all.
The banner, as the board resets — and then typing does nothing at all.
Characters, arriving at about the right moments, and almost all of them wrong.
There is no device to open. The probe is powered and invisible, so its serial side is missing too.
Both directions carry and both ends agree on the speed. This is the state the rest of the chapter assumes.
Nothing to separate, and that is what makes this the good failure. It is loud, it happens immediately, and it names the probe rather than anything you wrote. A great many USB cables carry power and no data, and one of those leaves the probe powered and invisible, so swap the cable before rewiring anything.
Silence, and two quite different causes produce it. The flash reported success, so the kernel is on the chip and the debug side is fine — which leaves the three console wires, or a terminal opened on the wrong device. Those two look identical from here. List /dev before and after plugging the probe in and open the name that appeared, because that costs seconds and rewiring does not.
One wire, and the banner arriving is what tells you which. It proves the board's transmit, the probe's receive and the shared ground are all good, and the kernel is up. The only thing left untested is the other direction, which is the probe's transmit to the board's receive — Figure 4's middle two.
Nonsense is not silence, and that is the useful part: characters arriving at all proves the board is transmitting and something is listening. Two causes remain, a wrong speed and a missing ground. Check the number first, because it is free, and only then go back to the wire.
The wireless version of this board is the easier one to find, and it is the one every command in this chapter was run against.
This branch has a raspberry_pi_pico_2_w crate. Use it: substitute its name for raspberry_pi_pico_2 in the commands above and everything else here is unchanged. It is not in the Tock repository yet, so if you are reading against upstream rather than this branch, build the plain Pico 2 crate and read on.
Two crates for one chip, and the whole difference is one pin. On a plain Pico 2 the user light is on pin 25, and that is the pin its crate hands to the light driver. On a Pico 2 W that pin is the radio's chip select, and the light is on the radio rather than on the chip. So the Pico 2 W crate hands nothing to the light driver, and its panic handler stops after printing where the other goes on to blink that pin.
Everything this chapter asks of the board works on either one: it builds, it flashes, it boots, and it talks. Nothing above needs that pin, and nothing above lights anything, because an idle kernel with no application loaded drives no light at all.
Where that shows is chapter 1, which follows one write all the way down to that pin, and chapter 4, which watches the panic handler blink it. Both say which of the two crates they mean.
That is no longer a guess. This chapter was checked on a Pico 2 W: the kernel flashed over the probe, booted, printed its line and took typed commands on that board. The chips are the same family and the W's extra parts simply go unclaimed, which is how it read from the source and is now what a board did.
make flash prints a message about a variable and exits without an error. What happened?
It built. The copy is the only step that did not happen, and it did not happen because the folder it checks for is a Linux path that a Mac never has. The exit status is success, so nothing after it notices, and the board goes on running whatever it held before.
The banner appears on the console, but typing list does nothing at all. What is wrong?
One wire. The banner arriving proves the board's transmit and the shared ground are both good, and proves the kernel is up. The only thing left untested is the other direction, which is the probe's transmit to the board's receive.
Why does the output path contain thumbv8m.main-none-eabi rather than something naming your computer?
It names the target: the chip's instruction set, no operating system beneath it, and a standard calling convention. Two different laptops building this produce the same path, because the path is about the destination rather than the origin.
OpenOCD says it cannot find the probe. What is the cheapest thing to rule out first?
The cable, because it costs ten seconds and the others do not. Charge-only USB cables are common, and one on the probe leaves it powered and invisible, which reads as a wiring fault worth an hour. Rewiring is the next cheapest thing to try; replacing the board is the last.
You have a board running a kernel you built, and a line of text proving it. None of that makes this chapter a prerequisite, and five of the seven after it never ask you to touch the board again. What it gives you is somewhere to go and look, when a later chapter says the kernel does something.
You also have the beginnings of a habit this series cares about more than any particular fact. Two of the four ways to flash this board fail without saying so, and the way you find that out is by checking what a command did rather than what it reported.
Next
The board in front of you is now running something. Chapter 1 asks what the smallest thing it can do actually is. The answer is one instruction putting a number at a numbered location, followed down to a wire changing voltage, and to the reason Tock has to exist.
Every claim on this page about what a file says comes from the Tock tree at commit 83bad9388. The build output in the second section is a real run on an Apple laptop, not a transcription.
rust-toolchain.toml:6–15boards/raspberry_pi_pico_2/.cargo/config.toml:11, "target = "thumbv8m.main-none-eabi""boards/raspberry_pi_pico_2/Makefile:23, :27, :32 and :42program refusing to run at all with no application given — the same file, :34–36flash-openocd actually runs: write, verify, reset, exit — the same file, :25. The form run on the bench was the split one below, which is that line with the config spelled out; the two are the same command because the config is only those two lines, "install: flash"boards/raspberry_pi_pico_2/openocd-picoprobe.cfg:5–6.github/workflows/treadmill-ci.yml:5–8, "Treadmill is a distributed hardware testbed developed within the Tock OS project"boards/raspberry_pi_pico_2/src/lib.rs:260–263boards/raspberry_pi_pico_2/src/main.rs:100, "kernel::debug!("Initialization complete. Enter main loop")"load_processes at :117, then :132list prints, both quoted as the process console writes them — capsules/core/src/process_console.rs:1036 for tock$ , and :897–898 for the headerboards/ at that commitboards/raspberry_pi_pico_2/src/main.rs:95, "LedHigh::new(peripherals.pins.get_pin(RPGpio::GPIO25))"boards/raspberry_pi_pico_2_w/src/main.rs and boards/raspberry_pi_pico_2_w/src/io.rs/usr/bin/time. The block above is that run, abridged: five Compiling lines and a trailing checksum are left out, and the size line's filename column is shortened from an absolute path. No number in it is alteredscreen already, and picocom needing installing on Debian — checked on the machine these pages were written on, which had screen and neither of the other twoSWD DPIDR 0x4c013477 and both Cortex-M33 cores, programmed the kernel, and the banner arrived on the console as the board reset.text flipped, and OpenOCD named all four by address; against the true image it says nothing at all/dev/ttyACM0 on Linux — lsusb showed 2e8a:000c alongside it, and the console ran over it in both directionsunable to find a matching CMSIS-DAP device, exit 1, captured by unplugging itbrew install open-ocd on this Mac installed 0.12.0, whose scripts/target holds rp2040.cfg and no rp2350.cfg. Run against this board's own config it printed Error: Can't find target/rp2350.cfg at line 6, the line that names the chip, so the failure lands before anything is asked of the probepicocom -b 115200 /dev/ttyACM0 opened the port at that baud and exited cleanly. Both ls forms were run too, one on a Mac and one on the Pi. No device name is quoted here that nobody has seenmake flash run on this machine with no board attached. The conversion succeeded, so the copy was the only step that did not happen, and nothing reported itText, 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.