<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
    <channel>
      <title>k4s.xyz</title>
      <link>https://k4s.xyz</link>
      <description></description>
      <generator>Zola</generator>
      <language>en</language>
      <atom:link href="https://k4s.xyz/rss.xml" rel="self" type="application/rss+xml"/>
      <lastBuildDate>Sun, 18 Oct 2026 00:00:00 +0000</lastBuildDate>
      <item>
          <title>Lesson 1: Anatomy of an RP2350 Image</title>
          <pubDate>Sun, 18 Oct 2026 00:00:00 +0000</pubDate>
          <author>kevstewa@protonmail.com (Kevin Stewart)</author>
          <link>https://k4s.xyz/blog/rp2350/lesson-1/</link>
          <guid>https://k4s.xyz/blog/rp2350/lesson-1/</guid>
          <description xml:base="https://k4s.xyz/blog/rp2350/lesson-1/">&lt;p&gt;This is the second lesson of &lt;a href=&quot;https://k4s.xyz/blog/rp2350/&quot;&gt;Embedded Rust on the
RP2350&lt;/a&gt;, a course in embedded Rust on the Raspberry Pi
RP2350, built from the reset vector up. It follows &lt;a href=&quot;https://k4s.xyz/blog/rp2350/lesson-0/&quot;&gt;Lesson
0&lt;/a&gt;, which sets up the toolchain and the
&lt;code&gt;rp2350-course&lt;/code&gt; workspace this lesson adds to.&lt;/p&gt;
&lt;h2 id=&quot;goals&quot;&gt;Goals&lt;/h2&gt;
&lt;p&gt;You finish with &lt;code&gt;lessons/l01-image&lt;/code&gt;, a crate with zero dependencies that the
RP2350&#39;s boot ROM accepts and runs. Every byte in it is yours:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A linker script that places your code in flash at 0x1000_0000 and defines the
top of the stack.&lt;/li&gt;
&lt;li&gt;A vector table built by hand: the initial stack pointer, then your reset
handler.&lt;/li&gt;
&lt;li&gt;A 20-byte IMAGE_DEF you assemble word by word.&lt;/li&gt;
&lt;li&gt;Proof on hardware that the boot ROM launched your code, and proof of what it
does when your image is wrong.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;rp-hal&#39;s blinky from Lesson 0 puts 40 bytes in &lt;code&gt;.start_block&lt;/code&gt;: a 20-byte
IMAGE_DEF followed by a 20-byte picotool header. This lesson builds only the
first part.&lt;/p&gt;
&lt;p&gt;Your reset handler stays deliberately small. Copying &lt;code&gt;.data&lt;/code&gt;, zeroing &lt;code&gt;.bss&lt;/code&gt; and
turning on the LED are Lesson 2.&lt;/p&gt;
&lt;h2 id=&quot;read-first&quot;&gt;Read first&lt;/h2&gt;
&lt;p&gt;All section numbers are from the &lt;a rel=&quot;external&quot; href=&quot;https://datasheets.raspberrypi.com/rp2350/rp2350-datasheet.pdf&quot;&gt;RP2350
datasheet&lt;/a&gt;, with
the page number printed in its footer.&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Section&lt;/th&gt;&lt;th&gt;Pages&lt;/th&gt;&lt;th&gt;Why&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;§5.1.4 Image definitions&lt;/td&gt;&lt;td&gt;328&lt;/td&gt;&lt;td&gt;What an IMAGE_DEF is for&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;§5.1.5 Blocks and block loops&lt;/td&gt;&lt;td&gt;328–329&lt;/td&gt;&lt;td&gt;Markers, items, and the link word&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;§5.1.12 Flash image boot&lt;/td&gt;&lt;td&gt;331–332&lt;/td&gt;&lt;td&gt;The first-4-KB rule, and the 20-byte minimum&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;§5.9.3.1 IMAGE_DEF item&lt;/td&gt;&lt;td&gt;383–384&lt;/td&gt;&lt;td&gt;The flags field, bit by bit&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;§5.9.3.3 VECTOR_TABLE item&lt;/td&gt;&lt;td&gt;385&lt;/td&gt;&lt;td&gt;Without this item, the vector table is assumed to be at the start of the image&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;§5.9.5.1 Minimum Arm IMAGE_DEF&lt;/td&gt;&lt;td&gt;389&lt;/td&gt;&lt;td&gt;The exact five words you&#39;ll type&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;§2.2 Address map&lt;/td&gt;&lt;td&gt;27&lt;/td&gt;&lt;td&gt;Where flash (0x1000_0000) and SRAM (0x2000_0000) live&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;For comparison after you&#39;ve built yours: &lt;code&gt;rp235x-hal/src/block.rs&lt;/code&gt; in rp-hal,
and &lt;code&gt;memory.x&lt;/code&gt; in &lt;code&gt;rp235x-hal-examples&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;concepts&quot;&gt;Concepts&lt;/h2&gt;
&lt;h3 id=&quot;what-the-boot-rom-does-before-your-code-exists&quot;&gt;What the boot ROM does before your code exists&lt;/h3&gt;
&lt;p&gt;At power-on the core runs code from its own mask ROM at address 0, not from
flash. The boot ROM sets up the QSPI flash in a simple, safe mode, then searches
the first 4 KB of flash for a block loop holding a valid IMAGE_DEF (§5.1.12).
If it finds one for an Arm executable, it looks for the vector table at the
start of the image, because your IMAGE_DEF names no other location (§5.9.3.3).
It loads the stack pointer from word 0 and jumps to the address in word 1. If it
finds nothing valid, it treats the flash as blank, drops into BOOTSEL mode, and
the board appears on your PC as a USB drive.&lt;/p&gt;
&lt;p&gt;Lesson 0&#39;s diagram showed the vector table at the start of flash. The boot ROM
never reads that table until the IMAGE_DEF has vouched for the image.&lt;/p&gt;
&lt;h3 id=&quot;blocks-items-and-loops&quot;&gt;Blocks, items and loops&lt;/h3&gt;
&lt;p&gt;A block is a run of 32-bit words (§5.1.5.1):&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A start marker, &lt;code&gt;0xffffded3&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;One or more items. The first item&#39;s type decides what kind of block it is.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;LAST&lt;/code&gt; item recording how many words of items came before it.&lt;/li&gt;
&lt;li&gt;A link: the relative offset to the next block in the loop.&lt;/li&gt;
&lt;li&gt;An end marker, &lt;code&gt;0xab123579&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A link of 0 points back at the block itself, a loop of one. Longer loops exist
so that tools can append blocks after linking: signing an image, for example,
adds a second, larger IMAGE_DEF (§5.1.5.2). The markers were chosen because
they&#39;re unlikely to appear in real Arm or RISC-V code.&lt;/p&gt;
&lt;h3 id=&quot;your-image-def-word-by-word&quot;&gt;Your IMAGE_DEF, word by word&lt;/h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Word&lt;/th&gt;&lt;th&gt;Value&lt;/th&gt;&lt;th&gt;Meaning&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;&lt;code&gt;0xffffded3&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Start marker&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;&lt;code&gt;0x10210142&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;IMAGE_TYPE&lt;/code&gt; item: byte 0 is type &lt;code&gt;0x42&lt;/code&gt;, byte 1 is the size (1 word), bytes 2–3 are the flags &lt;code&gt;0x1021&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;&lt;code&gt;0x000001ff&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;LAST&lt;/code&gt; item: byte 0 is type &lt;code&gt;0xff&lt;/code&gt;, bytes 1–2 say the items before it total 1 word&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;3&lt;/td&gt;&lt;td&gt;&lt;code&gt;0x00000000&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Link to the next block: offset 0, a loop of one&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;4&lt;/td&gt;&lt;td&gt;&lt;code&gt;0xab123579&lt;/code&gt;&lt;/td&gt;&lt;td&gt;End marker&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The flags &lt;code&gt;0x1021&lt;/code&gt; in binary are &lt;code&gt;0001 0000 0010 0001&lt;/code&gt;. Checked against Table
493 (§5.9.3.1):&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Bits&lt;/th&gt;&lt;th&gt;Field&lt;/th&gt;&lt;th&gt;Value&lt;/th&gt;&lt;th&gt;Meaning&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;0–3&lt;/td&gt;&lt;td&gt;Image type&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;Executable&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;4–5&lt;/td&gt;&lt;td&gt;Security&lt;/td&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;Runs in Secure mode&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;8–10&lt;/td&gt;&lt;td&gt;CPU&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;Arm&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;11&lt;/td&gt;&lt;td&gt;Extra security&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;Off (ignored before stepping A4)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;12–14&lt;/td&gt;&lt;td&gt;Chip&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;RP2350&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;15&lt;/td&gt;&lt;td&gt;Try before you buy&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;Off&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The image format and the boot ROM&#39;s search for it are the same on every RP2350
stepping. Bit 11 is the one difference, and this image leaves it clear anyway
(§5.9.3.1, p. 384).&lt;/p&gt;
&lt;p&gt;Little-endian storage is why a hex dump of the block shows &lt;code&gt;d3deffff 42012110&lt;/code&gt;:
the same words, low byte first.&lt;/p&gt;
&lt;h3 id=&quot;linker-scripts&quot;&gt;Linker scripts&lt;/h3&gt;
&lt;p&gt;The linker script is the contract between your code and the chip&#39;s address map.
Yours has four parts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MEMORY&lt;/code&gt; names the regions and their sizes.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SECTIONS&lt;/code&gt; says which input sections go where, in what order.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ENTRY&lt;/code&gt; names the symbol debuggers treat as the start. The chip ignores it.&lt;/li&gt;
&lt;li&gt;Symbols such as &lt;code&gt;_stack_start&lt;/code&gt; are addresses the script defines and your code
can read.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Two more pieces protect you. &lt;code&gt;KEEP&lt;/code&gt; stops the linker from discarding a section
nothing references. &lt;code&gt;ASSERT&lt;/code&gt; fails the link with your own message when a rule is
broken, which beats debugging a silent board.&lt;/p&gt;
&lt;h3 id=&quot;keeping-the-linker-from-deleting-your-work&quot;&gt;Keeping the linker from deleting your work&lt;/h3&gt;
&lt;p&gt;Nothing in your code calls the vector table or reads the IMAGE_DEF, so the
toolchain would happily throw both away. Three layers stop it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;#[used]&lt;/code&gt; tells the compiler to emit the static even though nothing uses it.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;#[unsafe(link_section = &quot;...&quot;)]&lt;/code&gt; puts it in a named section. It&#39;s &lt;code&gt;unsafe&lt;/code&gt;
because the wrong section can break the program in ways the compiler can&#39;t
check.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;KEEP(...)&lt;/code&gt; in the script tells the linker not to garbage-collect that
section.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Exercise 3 has you remove one layer and watch what disappears.&lt;/p&gt;
&lt;h3 id=&quot;the-thumb-bit&quot;&gt;The Thumb bit&lt;/h3&gt;
&lt;p&gt;Cortex-M only runs Thumb code, and bit 0 of a branch target says &quot;Thumb&quot;. When
you put a function in the vector table, the toolchain sets that bit for you.
That&#39;s why blinky&#39;s reset vector is &lt;code&gt;0x10000139&lt;/code&gt; while &lt;code&gt;Reset&lt;/code&gt; itself sits at
&lt;code&gt;0x10000138&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;build-it&quot;&gt;Build it&lt;/h2&gt;
&lt;p&gt;Four files go in a new crate, and one line changes in the workspace.&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;txt&quot;&gt;rp2350-course/
├── Cargo.toml            add the crate to members
└── lessons/l01-image/
    ├── Cargo.toml
    ├── build.rs          hands link.ld to the linker
    ├── link.ld           MEMORY, SECTIONS, ENTRY, _stack_start
    └── src/main.rs       vector table, IMAGE_DEF, Reset
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;1-register-the-crate&quot;&gt;1. Register the crate&lt;/h3&gt;
&lt;p&gt;In the workspace&#39;s root manifest:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;toml&quot;&gt;# Cargo.toml (workspace root)
[workspace]
resolver = &amp;quot;3&amp;quot;
members = [&amp;quot;lessons/l01-image&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the crate&#39;s own manifest:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;toml&quot;&gt;# lessons/l01-image/Cargo.toml
[package]
name = &amp;quot;l01-image&amp;quot;
version = &amp;quot;0.1.0&amp;quot;
edition.workspace = true

[[bin]]
name = &amp;quot;l01-image&amp;quot;
test = false
bench = false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;How to read these two files. TOML is &lt;code&gt;key = value&lt;/code&gt; lines grouped under &lt;code&gt;[table]&lt;/code&gt;
headers. A dotted key such as &lt;code&gt;edition.workspace&lt;/code&gt; is shorthand for a nested
table.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;[workspace]&lt;/code&gt; makes the root &lt;code&gt;Cargo.toml&lt;/code&gt; a &lt;em&gt;virtual manifest&lt;/em&gt;. It owns the
crates under it, but is not a crate itself. All of them share one &lt;code&gt;target/&lt;/code&gt;
directory and one &lt;code&gt;Cargo.lock&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;members&lt;/code&gt; lists each crate&#39;s directory. A crate is part of the workspace
because it is listed here.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;resolver = &quot;3&quot;&lt;/code&gt; picks the dependency-resolution rules. A crate gets this
default from its edition, but a virtual manifest has no edition of its own, so
it must say it. Version 3 is the one that comes with edition 2024.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[workspace.package]&lt;/code&gt; holds values that members can inherit.
&lt;code&gt;edition.workspace = true&lt;/code&gt; means &quot;take &lt;code&gt;edition&lt;/code&gt; from the workspace&quot;. That&#39;s
why the crate&#39;s manifest doesn&#39;t say 2024 itself.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[package]&lt;/code&gt; describes the crate. &lt;code&gt;name&lt;/code&gt; is also the default name of the binary
it produces.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[[bin]]&lt;/code&gt;, with double brackets, is TOML&#39;s &lt;em&gt;array of tables&lt;/em&gt;. Each &lt;code&gt;[[bin]]&lt;/code&gt;
header adds one more entry to a list of binary targets. Cargo would find
&lt;code&gt;src/main.rs&lt;/code&gt; on its own anyway. This entry exists only to change two settings
on that binary, and since its &lt;code&gt;name&lt;/code&gt; matches the package name, it adjusts the
binary Cargo already found instead of creating a new one.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;test = false&lt;/code&gt; and &lt;code&gt;bench = false&lt;/code&gt; keep this binary out of &lt;code&gt;cargo test&lt;/code&gt; and
&lt;code&gt;cargo bench&lt;/code&gt;. Those commands compile it with Rust&#39;s test harness (the &lt;code&gt;test&lt;/code&gt;
crate), which needs &lt;code&gt;std&lt;/code&gt;, so the build would fail on this target.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;2-hand-the-script-to-the-linker&quot;&gt;2. Hand the script to the linker&lt;/h3&gt;
&lt;pre&gt;&lt;code data-lang=&quot;rust&quot;&gt;// lessons/l01-image/build.rs
use std::{env, path::PathBuf};

fn main() {
    // Let the linker find link.ld in this crate&amp;#39;s directory.
    let dir = PathBuf::from(env::var(&amp;quot;CARGO_MANIFEST_DIR&amp;quot;).unwrap());
    println!(&amp;quot;cargo:rustc-link-search={}&amp;quot;, dir.display());

    // Use our script, and skip page-aligning sections
    // (flash isn&amp;#39;t paged memory).
    println!(&amp;quot;cargo:rustc-link-arg-bins=-Tlink.ld&amp;quot;);
    println!(&amp;quot;cargo:rustc-link-arg-bins=--nmagic&amp;quot;);

    println!(&amp;quot;cargo:rerun-if-changed=link.ld&amp;quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;How this file works:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A build script is a separate program.&lt;/strong&gt; If a crate has a &lt;code&gt;build.rs&lt;/code&gt;, Cargo
compiles it for your Linux machine, not the Pico, and runs it before compiling
the crate. It&#39;s ordinary &lt;code&gt;std&lt;/code&gt; Rust, which is why &lt;code&gt;std::env&lt;/code&gt; is available
here.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It talks to Cargo through stdout.&lt;/strong&gt; Each printed line that starts with
&lt;code&gt;cargo:&lt;/code&gt; is an instruction. Anything else is ignored. (Recent Cargo also
accepts &lt;code&gt;cargo::&lt;/code&gt;, with two colons. Both work.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;env::var(&quot;CARGO_MANIFEST_DIR&quot;)&lt;/code&gt;&lt;/strong&gt; reads an environment variable Cargo sets
for the script: the directory holding this crate&#39;s &lt;code&gt;Cargo.toml&lt;/code&gt;. Cargo always
sets it, so the &lt;code&gt;unwrap()&lt;/code&gt; can&#39;t fail.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;cargo:rustc-link-search=DIR&lt;/code&gt;&lt;/strong&gt; adds &lt;code&gt;-L DIR&lt;/code&gt; to the linker command. The
linker looks for the file named by &lt;code&gt;-T&lt;/code&gt; in its working directory first, then
in each &lt;code&gt;-L&lt;/code&gt; directory. Delete this line and the link fails with &lt;code&gt;rust-lld: error: cannot find linker script link.ld&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;cargo:rustc-link-arg-bins=FLAG&lt;/code&gt;&lt;/strong&gt; passes &lt;code&gt;FLAG&lt;/code&gt; to the linker, but only
when it links this crate&#39;s binaries. Tests, examples and the build script
itself are left alone. (&lt;code&gt;rustc-link-arg&lt;/code&gt;, without &lt;code&gt;-bins&lt;/code&gt;, would apply to
every target.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-Tlink.ld&lt;/code&gt;&lt;/strong&gt; tells the linker to use your script instead of its built-in
default layout.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;--nmagic&lt;/code&gt;&lt;/strong&gt; turns off page alignment of the program&#39;s segments. By default
the linker assumes an operating system will map the file into memory a page at
a time (64 KB pages for this target), so it pads the &lt;em&gt;file&lt;/em&gt; to match. Without
the flag, the ELF grows from 5,292 to 70,680 bytes, and each &lt;code&gt;LOAD&lt;/code&gt; segment in
&lt;code&gt;readelf -l&lt;/code&gt; reports &lt;code&gt;Align 0x10000&lt;/code&gt; instead of &lt;code&gt;0x4&lt;/code&gt;. The 58 bytes that reach
flash are identical either way. So with &lt;code&gt;rust-lld&lt;/code&gt;, the flag keeps the ELF
honest about a chip that has no pages; it doesn&#39;t save flash. Some guides say
it saves flash, including the config comments in Shawn Hymel&#39;s
introduction-to-embedded-rust (&quot;Prevent padding memory between sections&quot;), but
with this linker it doesn&#39;t, even with a section at a deliberately unaligned
address.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;cargo:rerun-if-changed=link.ld&lt;/code&gt;&lt;/strong&gt; narrows when Cargo reruns the script. By
default it reruns whenever any file in the crate changes. Once the script
prints a &lt;code&gt;rerun-if-changed&lt;/code&gt; line, Cargo reruns it only when a listed file (or
&lt;code&gt;build.rs&lt;/code&gt; itself) changes. Editing &lt;code&gt;link.ld&lt;/code&gt; still triggers a relink, which
is the case that matters.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;3-write-the-linker-script&quot;&gt;3. Write the linker script&lt;/h3&gt;
&lt;pre&gt;&lt;code data-lang=&quot;txt&quot;&gt;/* lessons/l01-image/link.ld */
/* Lesson 1: the memory map of a zero-dependency RP2350 image. */

MEMORY
{
    /* The Pico 2&amp;#39;s QSPI flash, seen through XIP. */
    FLASH : ORIGIN = 0x10000000, LENGTH = 4M
    /* SRAM0-7; the two 4 KB banks above are left alone. */
    RAM   : ORIGIN = 0x20000000, LENGTH = 512K
}

/* Debuggers treat this as the start. The chip ignores it and uses the
   vector table. */
ENTRY(Reset);

/* The stack starts at the top of RAM and grows down. */
_stack_start = ORIGIN(RAM) + LENGTH(RAM);

SECTIONS
{
    /* Word 0: initial stack pointer. Word 1: reset handler.
       Must be first in flash. */
    .vector_table ORIGIN(FLASH) :
    {
        KEEP(*(.vector_table));
    } &amp;gt; FLASH

    /* The IMAGE_DEF block. The boot ROM only searches the first 4 KB of
       flash for it. */
    .start_block : ALIGN(4)
    {
        KEEP(*(.start_block));
    } &amp;gt; FLASH

    .text : ALIGN(4)
    {
        *(.text .text.*);
    } &amp;gt; FLASH

    .rodata : ALIGN(4)
    {
        *(.rodata .rodata.*);
    } &amp;gt; FLASH

    /* Nothing copies .data or zeroes .bss until Lesson 2, so both must
       stay empty. */
    .data : ALIGN(4)
    {
        *(.data .data.*);
    } &amp;gt; RAM AT &amp;gt; FLASH

    .bss (NOLOAD) : ALIGN(4)
    {
        *(.bss .bss.*);
    } &amp;gt; RAM

    /* Unwind tables are for panics that unwind. This target aborts
       instead. */
    /DISCARD/ :
    {
        *(.ARM.exidx .ARM.exidx.* .ARM.extab.*);
    }
}

ASSERT(SIZEOF(.data) == 0 &amp;amp;&amp;amp; SIZEOF(.bss) == 0,
       &amp;quot;Lesson 1 has no runtime init: keep mutable statics out until Lesson 2&amp;quot;);
ASSERT(ADDR(.start_block) + SIZEOF(.start_block) &amp;lt;= ORIGIN(FLASH) + 4K,
       &amp;quot;the IMAGE_DEF must end within the first 4 KB of flash&amp;quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;reading-the-script-line-by-line&quot;&gt;Reading the script, line by line&lt;/h4&gt;
&lt;p&gt;Cargo hands this file to &lt;code&gt;rust-lld&lt;/code&gt;, LLVM&#39;s linker, running in GNU mode
(&lt;code&gt;-flavor gnu&lt;/code&gt; on its command line). It reads the same script language as GNU
&lt;code&gt;ld&lt;/code&gt;, so the GNU &lt;code&gt;ld&lt;/code&gt; manual&#39;s &quot;Linker Scripts&quot; chapter is the reference for
everything below. These rules apply throughout the file:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Comments are &lt;code&gt;/* ... */&lt;/code&gt; only. A &lt;code&gt;//&lt;/code&gt; line fails the link with &lt;code&gt;unknown directive: //&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;An assignment such as &lt;code&gt;_stack_start = ...;&lt;/code&gt; needs its semicolon. Elsewhere
semicolons are optional, and this script uses them anyway for consistency.&lt;/li&gt;
&lt;li&gt;Numbers take C syntax (&lt;code&gt;0x10000000&lt;/code&gt;), plus the suffixes &lt;code&gt;K&lt;/code&gt; (×1024) and &lt;code&gt;M&lt;/code&gt;
(×1024×1024). So &lt;code&gt;4M&lt;/code&gt; is 0x40_0000 and &lt;code&gt;512K&lt;/code&gt; is 0x8_0000.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;MEMORY { ... }&lt;/code&gt;&lt;/strong&gt; declares regions, and nothing else. Each line is &lt;code&gt;NAME : ORIGIN = start, LENGTH = size&lt;/code&gt;. A region is a named address range that output
sections can be placed into, plus a running &quot;next free address&quot; that starts at
&lt;code&gt;ORIGIN&lt;/code&gt;. The linker checks fit: if &lt;code&gt;.text&lt;/code&gt; grows past &lt;code&gt;LENGTH(FLASH)&lt;/code&gt;, the link
fails with an overflow error instead of producing an image that wraps. GNU
syntax also allows attributes such as &lt;code&gt;FLASH (rx) :&lt;/code&gt;. They only steer sections
the script doesn&#39;t mention, and this script mentions everything, so they&#39;re left
out.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;ENTRY(Reset);&lt;/code&gt;&lt;/strong&gt; writes &lt;code&gt;Reset&lt;/code&gt;&#39;s address into the ELF header&#39;s entry-point
field. Tools that load the ELF, such as GDB, read it as &quot;where the program
starts&quot;, and the linker treats it as a garbage-collection root. The chip never
sees it: it starts from the vector table.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;_stack_start = ORIGIN(RAM) + LENGTH(RAM);&lt;/code&gt;&lt;/strong&gt; defines a symbol, which is a
name for an address with no storage behind it. &lt;code&gt;ORIGIN()&lt;/code&gt; and &lt;code&gt;LENGTH()&lt;/code&gt; are
built-in functions that read a region&#39;s numbers back, so this is 0x2000_0000 +
0x8_0000 = 0x2008_0000. In Rust you declare it as &lt;code&gt;extern static _stack_start: u32&lt;/code&gt; and only ever take its address (&lt;code&gt;&amp;amp;raw const _stack_start&lt;/code&gt;). Reading its
&lt;em&gt;value&lt;/em&gt; would load whatever word sits at 0x2008_0000, which is the first word
of SRAM8.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;SECTIONS { ... }&lt;/code&gt;&lt;/strong&gt; is where the work happens. The compiler&#39;s object files
arrive full of &lt;em&gt;input sections&lt;/em&gt;. Each statement inside &lt;code&gt;SECTIONS&lt;/code&gt; builds one
&lt;em&gt;output section&lt;/em&gt; in the final ELF, in the order written. The general shape is:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;txt&quot;&gt;NAME [ADDRESS] [(TYPE)] : [ALIGN(n)]
{
    FILE-PATTERN(SECTION-PATTERNS)
    ...
} [&amp;gt; VMA-REGION] [AT &amp;gt; LMA-REGION]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Taking each piece in turn:&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Piece&lt;/th&gt;&lt;th&gt;Example&lt;/th&gt;&lt;th&gt;Meaning&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;NAME&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;.text&lt;/code&gt;&lt;/td&gt;&lt;td&gt;The output section&#39;s name in the ELF. Only &lt;code&gt;/DISCARD/&lt;/code&gt; is special; any other name is just a label.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;ADDRESS&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;ORIGIN(FLASH)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;An explicit start address. Without it, the section starts at the region&#39;s next free address. Only &lt;code&gt;.vector_table&lt;/code&gt; uses one, because the boot ROM expects the table at the very start of flash.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;(TYPE)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;(NOLOAD)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;NOLOAD&lt;/code&gt; reserves addresses but writes no bytes to the file. That&#39;s right for &lt;code&gt;.bss&lt;/code&gt;, which only has to be zero at run time.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;ALIGN(n)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;ALIGN(4)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Rounds the start address up to the next multiple of &lt;code&gt;n&lt;/code&gt; &lt;strong&gt;bytes&lt;/strong&gt;. The linker already aligns each section to its strictest input (a &lt;code&gt;u32&lt;/code&gt; forces 4), so &lt;code&gt;ALIGN&lt;/code&gt; sets a floor regardless of contents. &lt;code&gt;.text&lt;/code&gt; ends at 0x1000_003a. A &lt;code&gt;.rodata&lt;/code&gt; holding only a &lt;code&gt;[u8; 3]&lt;/code&gt; lands at 0x1000_003a without &lt;code&gt;ALIGN(4)&lt;/code&gt; and 0x1000_003c with it. Lesson 2&#39;s copy loop moves whole words, which is why it needs that floor.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&amp;gt; REGION&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;&amp;gt; FLASH&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Place the section in this region, at its next free address, and advance that address past it.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;AT &amp;gt; REGION&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;&amp;gt; RAM AT &amp;gt; FLASH&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Give the section a different load address (LMA). &lt;code&gt;.data&lt;/code&gt; runs at RAM addresses (VMA) but its starting bytes are stored in flash, right after &lt;code&gt;.rodata&lt;/code&gt;. That&#39;s the VMA/LMA split from Lesson 0: virtual memory address (where it runs) versus load memory address (where it&#39;s stored).&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Inside the braces: &lt;code&gt;*(.text .text.*)&lt;/code&gt;.&lt;/strong&gt; This is an input section description,
&lt;code&gt;FILE-PATTERN(SECTION-PATTERNS)&lt;/code&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;*&lt;/code&gt; outside the parentheses matches every input file: your crate&#39;s object
file, plus &lt;code&gt;libcore&lt;/code&gt; and &lt;code&gt;compiler_builtins&lt;/code&gt;. A narrower file pattern is
possible, but this course won&#39;t need one.&lt;/li&gt;
&lt;li&gt;Inside the parentheses is a space-separated list of glob patterns for section
names. &lt;code&gt;.text&lt;/code&gt; matches a section named exactly &lt;code&gt;.text&lt;/code&gt;. &lt;code&gt;.text.*&lt;/code&gt; matches any
name that starts with &lt;code&gt;.text.&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Why the second pattern matters:&lt;/strong&gt; rustc puts every function in its own
section, named &lt;code&gt;.text.&lt;/code&gt; plus the symbol name. Your &lt;code&gt;Reset&lt;/code&gt; arrives as
&lt;code&gt;.text.Reset&lt;/code&gt;, and the panic handler as &lt;code&gt;.text._RNv…rust_begin_unwind&lt;/code&gt;. That&#39;s
what lets the linker&#39;s &lt;code&gt;--gc-sections&lt;/code&gt; (Rust always passes it) delete unused
functions one at a time. &lt;code&gt;*(.text)&lt;/code&gt; alone would match none of your code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Order:&lt;/strong&gt; one pattern list places matches in the order the linker meets them.
&lt;code&gt;*(.text) *(.text.*)&lt;/code&gt; would instead put all plain &lt;code&gt;.text&lt;/code&gt; first, then
everything else. Here the result is the same.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.rodata&lt;/code&gt;, &lt;code&gt;.data&lt;/code&gt; and &lt;code&gt;.bss&lt;/code&gt; follow the same convention, so each gets the
same two patterns.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here&#39;s what happens if you write &lt;code&gt;*(.text)&lt;/code&gt; alone. The link still succeeds, with
no warning. &lt;code&gt;.text.Reset&lt;/code&gt; becomes an &quot;orphan&quot;, a section no rule claims, and the
linker places it by its own heuristics as an output section named &lt;code&gt;.text.Reset&lt;/code&gt;.
It happens to land at 0x1000_001c, so the image even runs. That&#39;s the danger:
an unmatched section is placed silently, wherever the linker sees fit. Adding
&lt;code&gt;-C link-arg=--orphan-handling=warn&lt;/code&gt; makes it name every orphan. It also lists
the debug sections, which is harmless, because they never reach flash.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;KEEP(...)&lt;/code&gt;&lt;/strong&gt; wraps an input section description. Sections inside it are roots
for garbage collection, so the linker never removes them. Anything they
reference survives too. &lt;code&gt;.text.Reset&lt;/code&gt; needs no &lt;code&gt;KEEP&lt;/code&gt; because the vector table
points at it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;/DISCARD/ : { ... }&lt;/code&gt;&lt;/strong&gt; is the one special output name: anything matched there
is thrown away. &lt;code&gt;.ARM.exidx&lt;/code&gt; and &lt;code&gt;.ARM.extab&lt;/code&gt; are unwind tables that let a panic
walk back up the stack. This target aborts on panic, so they&#39;re dead weight.
rustc still emits a small one for the panic handler.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;ASSERT(condition, &quot;message&quot;);&lt;/code&gt;&lt;/strong&gt; at the top level is checked after every
address is fixed. If the condition is false, the link fails with your message.
&lt;code&gt;ADDR(sec)&lt;/code&gt; is a section&#39;s start address, and &lt;code&gt;SIZEOF(sec)&lt;/code&gt; its size in bytes.&lt;/p&gt;
&lt;p&gt;rp-hal&#39;s &lt;code&gt;rp235x-hal-examples/memory.x&lt;/code&gt; uses one more piece you&#39;ll meet in
Lesson 2: &lt;code&gt;.&lt;/code&gt;, the location counter. Inside a section it means &quot;the current
address&quot;, so &lt;code&gt;__start_block_addr = .;&lt;/code&gt; records where the block begins. Each of
its &lt;code&gt;SECTIONS&lt;/code&gt; blocks closes with &lt;code&gt;INSERT AFTER&lt;/code&gt;, as in &lt;code&gt;} INSERT AFTER .vector_table;&lt;/code&gt;, because the file isn&#39;t a whole script. The blocks splice
&lt;code&gt;.start_block&lt;/code&gt; and a few other sections into &lt;code&gt;link.x&lt;/code&gt;, the full script the
&lt;code&gt;cortex-m-rt&lt;/code&gt; crate provides, which defines the vector table and everything
else.&lt;/p&gt;
&lt;h3 id=&quot;4-write-the-program&quot;&gt;4. Write the program&lt;/h3&gt;
&lt;pre&gt;&lt;code data-lang=&quot;rust&quot;&gt;// lessons/l01-image/src/main.rs
//! Lesson 1: an RP2350 image the boot ROM accepts, with zero dependencies.
#![no_std]
#![no_main]

use core::panic::PanicInfo;
use core::ptr;

unsafe extern &amp;quot;C&amp;quot; {
    /// Defined by link.ld at the top of RAM. Only its address means anything.
    static _stack_start: u32;
}

/// The first two words of the Armv8-M vector table. Lesson 8 adds the other 66.
#[repr(C)]
pub struct VectorTable {
    initial_sp: *const u32,
    reset: extern &amp;quot;C&amp;quot; fn() -&amp;gt; !,
}

// Raw pointers aren&amp;#39;t `Sync`, so a static holding one needs this promise.
// It holds: nothing ever writes the table, and it lives in flash anyway.
unsafe impl Sync for VectorTable {}

#[used]
#[unsafe(link_section = &amp;quot;.vector_table&amp;quot;)]
static VECTOR_TABLE: VectorTable = VectorTable {
    initial_sp: &amp;amp;raw const _stack_start,
    reset: Reset,
};

/// The minimum Arm IMAGE_DEF (RP2350 datasheet §5.9.5.1, p. 389).
#[used]
#[unsafe(link_section = &amp;quot;.start_block&amp;quot;)]
static IMAGE_DEF: [u32; 5] = [
    0xffff_ded3, // PICOBIN_BLOCK_MARKER_START
    0x1021_0142, // IMAGE_TYPE item, 1 word: EXE, Secure, Arm, RP2350
    0x0000_01ff, // LAST item: the items before it total 1 word
    0x0000_0000, // link to the next block: 0 means it loops to itself
    0xab12_3579, // PICOBIN_BLOCK_MARKER_END
];

/// A word of RAM you can watch from the probe. A raw address, not a static,
/// because a static here would land in .bss and nothing zeroes .bss yet.
const HEARTBEAT: *mut u32 = 0x2000_0000 as *mut u32;

#[unsafe(no_mangle)]
#[allow(non_snake_case)]
pub extern &amp;quot;C&amp;quot; fn Reset() -&amp;gt; ! {
    let mut count: u32 = 0;
    loop {
        count = count.wrapping_add(1);
        // Volatile, so the compiler can&amp;#39;t decide nobody is looking.
        unsafe { ptr::write_volatile(HEARTBEAT, count) };
    }
}

#[panic_handler]
fn panic(_info: &amp;amp;PanicInfo) -&amp;gt; ! {
    loop {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;reading-the-program-line-by-line&quot;&gt;Reading the program, line by line&lt;/h4&gt;
&lt;p&gt;Each variation below was built to check it. Where removing something breaks the
build, the error shown is the real one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Comments and crate attributes&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;//!&lt;/code&gt; is a doc comment for the thing that &lt;em&gt;contains&lt;/em&gt; it (here, the whole
crate). &lt;code&gt;///&lt;/code&gt; documents the item that &lt;em&gt;follows&lt;/em&gt; it. &lt;code&gt;//&lt;/code&gt; is a plain comment.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;#![...]&lt;/code&gt;, with the &lt;code&gt;!&lt;/code&gt;, is an &lt;em&gt;inner attribute&lt;/em&gt;: it applies to whatever
encloses it, here the crate. &lt;code&gt;#[...]&lt;/code&gt;, without the &lt;code&gt;!&lt;/code&gt;, applies to the next
item.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;#![no_std]&lt;/code&gt; links only &lt;code&gt;core&lt;/code&gt;, never &lt;code&gt;std&lt;/code&gt;. &lt;code&gt;core&lt;/code&gt; is the part of the
standard library that needs no operating system: pointers, integers, &lt;code&gt;Option&lt;/code&gt;,
slices, formatting. There&#39;s no heap, no threads and no files.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;#![no_main]&lt;/code&gt; tells rustc not to generate the usual program entry point, which
sets up &lt;code&gt;std&lt;/code&gt;&#39;s runtime and then calls your &lt;code&gt;fn main&lt;/code&gt;. Without it you get
&lt;code&gt;error[E0601]: `main` function not found in crate `l01_image` &lt;/code&gt;. Here, the
vector table is how the program starts.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Declaring a symbol from the linker script&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;rust&quot;&gt;unsafe extern &amp;quot;C&amp;quot; {
    static _stack_start: u32;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;An &lt;code&gt;extern&lt;/code&gt; block &lt;em&gt;declares&lt;/em&gt; things defined somewhere else; here, by
&lt;code&gt;link.ld&lt;/code&gt;. The compiler trusts the declaration and leaves the linker to supply
the address.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&quot;C&quot;&lt;/code&gt; names the ABI, meaning the C conventions for symbols and calls. It&#39;s the
one stable convention Rust shares with linkers and other languages.&lt;/li&gt;
&lt;li&gt;Since edition 2024, the block itself must be written &lt;code&gt;unsafe extern&lt;/code&gt;, because
the compiler can&#39;t check that your declaration matches reality. Leave out
&lt;code&gt;unsafe&lt;/code&gt; and you get &lt;code&gt;error: extern blocks must be unsafe&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Declaring the type as &lt;code&gt;u32&lt;/code&gt; is a convenient fiction. Nothing is stored at that
address, and the code never reads it. Taking its address with &lt;code&gt;&amp;amp;raw const&lt;/code&gt;
needs no &lt;code&gt;unsafe&lt;/code&gt; block, but reading the value would.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;The vector table type&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;#[repr(C)]&lt;/code&gt; fixes the struct&#39;s memory layout to C&#39;s rules: fields in the
order written, each at its natural alignment. Rust&#39;s default layout is
unspecified, and the compiler may reorder fields. The hardware reads offset 0
as the stack pointer and offset 4 as the reset address, so the order isn&#39;t
negotiable.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*const u32&lt;/code&gt; is a &lt;em&gt;raw pointer&lt;/em&gt;: an address with no lifetime and no borrow
checking. It may be null or dangling, so dereferencing one needs &lt;code&gt;unsafe&lt;/code&gt;.
Storing one is safe.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;extern &quot;C&quot; fn() -&amp;gt; !&lt;/code&gt; is a &lt;em&gt;function pointer type&lt;/em&gt;: the address of a function
that takes nothing, uses the C calling convention, and never returns. &lt;code&gt;!&lt;/code&gt; is
the &lt;em&gt;never type&lt;/em&gt;. A reset handler has nothing to return to.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unsafe impl Sync for VectorTable {}&lt;/code&gt;: every &lt;code&gt;static&lt;/code&gt; must be &lt;code&gt;Sync&lt;/code&gt;, meaning
it&#39;s safe to share between threads (and, on this chip, between the two cores
and interrupt handlers). Raw pointers aren&#39;t &lt;code&gt;Sync&lt;/code&gt;, so neither is the struct.
Delete the line and you get &lt;code&gt;error[E0277]: `*const u32` cannot be shared between threads safely&lt;/code&gt;. &lt;code&gt;unsafe impl&lt;/code&gt; is you telling the compiler &quot;this
holds&quot; for a property it can&#39;t prove. It holds here because the table is never
written.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;The two statics&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;static&lt;/code&gt;, not &lt;code&gt;const&lt;/code&gt;. A &lt;code&gt;static&lt;/code&gt; is one object at one fixed address, which is
exactly what a vector table is. A &lt;code&gt;const&lt;/code&gt; has no address; the compiler pastes
its value wherever it&#39;s used. Change &lt;code&gt;static&lt;/code&gt; to &lt;code&gt;const&lt;/code&gt; and you get &lt;code&gt;error: `#[used]` attribute cannot be used on constants&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;#[used]&lt;/code&gt; and &lt;code&gt;#[unsafe(link_section = &quot;...&quot;)]&lt;/code&gt; are explained under &quot;Keeping
the linker from deleting your work&quot;. Since edition 2024, attributes that can
break a program the compiler can&#39;t check must be wrapped in &lt;code&gt;unsafe(...)&lt;/code&gt;.
&lt;code&gt;link_section&lt;/code&gt; and &lt;code&gt;no_mangle&lt;/code&gt; are two of them.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;amp;raw const _stack_start&lt;/code&gt; creates a raw pointer directly. &lt;code&gt;&amp;amp;_stack_start&lt;/code&gt;
would create a &lt;em&gt;reference&lt;/em&gt;, which promises a valid, initialized &lt;code&gt;u32&lt;/code&gt; that the
code may read. &lt;code&gt;&amp;amp;raw const&lt;/code&gt; makes no such promise. That&#39;s the honest
description of a symbol that is only an address.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;reset: Reset&lt;/code&gt; uses the function&#39;s name as a value, which turns it into a
function pointer. The compiler can&#39;t know the final address, so it leaves a
relocation, and the linker fills in 0x1000_001d.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[u32; 5]&lt;/code&gt; is an array of five &lt;code&gt;u32&lt;/code&gt;s. The underscores in &lt;code&gt;0xffff_ded3&lt;/code&gt; are
digit separators with no meaning, like commas in 1,000.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;The heartbeat address&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;const HEARTBEAT: *mut u32 = 0x2000_0000 as *mut u32;&lt;/code&gt; casts a plain integer to
a raw pointer. A &lt;code&gt;const&lt;/code&gt; is right here, because it&#39;s just a number. &lt;code&gt;*mut&lt;/code&gt; means
writing through it is allowed (still only inside &lt;code&gt;unsafe&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The reset handler&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;#[unsafe(no_mangle)]&lt;/code&gt;: by default, rustc &lt;em&gt;mangles&lt;/em&gt; symbol names, encoding the
crate and path so names from different crates can&#39;t collide. Without this
attribute your function&#39;s symbol is &lt;code&gt;_RNvCs6rdpOkqRjUQ_9l01_image5Reset&lt;/code&gt;. The
image still builds and even runs, because the vector table refers to the
function through a relocation, not by name. But the linker warns &lt;code&gt;cannot find entry symbol Reset; not setting start address&lt;/code&gt;, and the ELF&#39;s entry point
becomes 0x0.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;#[allow(non_snake_case)]&lt;/code&gt; silences the lint that wants function names in
&lt;code&gt;snake_case&lt;/code&gt;. &lt;code&gt;Reset&lt;/code&gt; is capitalized to match cortex-m-rt&#39;s handler of the
same name, and the &lt;code&gt;ENTRY(Reset)&lt;/code&gt; in your script.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pub extern &quot;C&quot; fn Reset() -&amp;gt; !&lt;/code&gt; uses the C calling convention, because
hardware calls it, not Rust code. &lt;code&gt;-&amp;gt; !&lt;/code&gt; means the function can never return,
and the compiler enforces that: the body must diverge, as the &lt;code&gt;loop&lt;/code&gt; does.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;count.wrapping_add(1)&lt;/code&gt; wraps from &lt;code&gt;u32::MAX&lt;/code&gt; back to 0 in every build
profile. Plain &lt;code&gt;+&lt;/code&gt; would panic on overflow in debug builds.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unsafe { ptr::write_volatile(HEARTBEAT, count) }&lt;/code&gt; stores &lt;code&gt;count&lt;/code&gt; at
0x2000_0000 and promises the compiler it really happens. Writing through a
raw pointer is unchecked, hence &lt;code&gt;unsafe&lt;/code&gt;. Here&#39;s why &lt;code&gt;volatile&lt;/code&gt; matters.
Replace the line with a plain store, &lt;code&gt;unsafe { *HEARTBEAT = count };&lt;/code&gt;, and the
release build&#39;s entire loop compiles to this:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code data-lang=&quot;txt&quot;&gt;1000001c: b580    push {r7, lr}
1000001e: 466f    mov  r7, sp
10000020: e7fe    b    0x10000020 &amp;lt;Reset+0x4&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&#39;s a branch to itself, with no store anywhere. The compiler proved that
nothing in the program ever reads that memory, so it deleted the writes.
Volatile tells it that each access is visible outside the program (to the probe
here, and to hardware from Lesson 3 on), so every write must happen, in order.
That&#39;s the version that produced the unrolled loop of four &lt;code&gt;str&lt;/code&gt; instructions
you&#39;ll see in the disassembly below.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The panic handler&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;#[panic_handler]&lt;/code&gt; marks the function that runs when code panics. With &lt;code&gt;std&lt;/code&gt;
that function comes built in. Under &lt;code&gt;no_std&lt;/code&gt; you must provide exactly one, and
its signature must be &lt;code&gt;fn(&amp;amp;PanicInfo) -&amp;gt; !&lt;/code&gt;. This one just spins. Nothing in
&lt;code&gt;Reset&lt;/code&gt; can panic, so the linker removes the handler entirely, as the
&lt;code&gt;--print-gc-sections&lt;/code&gt; output below shows.&lt;/p&gt;
&lt;h4 id=&quot;watch-the-linker-work&quot;&gt;Watch the linker work&lt;/h4&gt;
&lt;p&gt;With &lt;code&gt;main.rs&lt;/code&gt; in place, the crate has something to link, so you can watch the
step 3 script at work. Two flags show you exactly what it did. Run them from the
workspace root:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;# A map: every output section, the input sections inside it,
# and their addresses
cargo rustc --release -p l01-image --bin l01-image -- \
    -C link-arg=-Map=$PWD/target/l01.map
grep -E &amp;#39;\.(vector_table|start_block|text)|Reset$&amp;#39; target/l01.map

# Garbage collection: every section the linker removed
# (touch forces a relink)
touch lessons/l01-image/src/main.rs
cargo rustc --release -p l01-image --bin l01-image -- \
    -C link-arg=--print-gc-sections 2&amp;gt;&amp;amp;1 | grep -v debug
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The map shows &lt;code&gt;.text.Reset&lt;/code&gt; inside your &lt;code&gt;.text&lt;/code&gt; output section:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;txt&quot;&gt;     VMA      LMA     Size Align Out     In      Symbol
10000000 10000000        8     4 .vector_table
10000000 10000000        8     4         l01_image-….rcgu.o:(.vector_table)
10000008 10000008       14     4 .start_block
10000008 10000008       14     4         l01_image-….rcgu.o:(.start_block)
1000001c 1000001c       1e     4 .text
1000001c 1000001c       1e     4         l01_image-….rcgu.o:(.text.Reset)
1000001d 1000001d       1e     1                 Reset
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The GC list shows what didn&#39;t survive: a stray empty &lt;code&gt;.text&lt;/code&gt;, the panic handler
&lt;code&gt;.text._RNv…rust_begin_unwind&lt;/code&gt;, and its &lt;code&gt;.ARM.exidx&lt;/code&gt; entry. Nothing in &lt;code&gt;Reset&lt;/code&gt;
can panic, so nothing references the handler, and the linker deletes it. You
wrote a panic handler and it costs zero bytes of flash.&lt;/p&gt;
&lt;h2 id=&quot;look-before-you-flash&quot;&gt;Look before you flash&lt;/h2&gt;
&lt;p&gt;From the workspace root:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;cargo build --release
cargo size --release --bin l01-image -- -A
cargo objdump --release --bin l01-image -- -h --show-lma
cargo objdump --release --bin l01-image -- -s -j .vector_table -j .start_block
cargo objdump --release --bin l01-image -- -d -j .text
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;-d&lt;/code&gt; disassembles, and &lt;code&gt;-j&lt;/code&gt; limits any of these to the named section. With Rust
1.97 the sections come out as below; &lt;code&gt;.text&lt;/code&gt; may differ a little with another
compiler version:&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Section&lt;/th&gt;&lt;th&gt;Address&lt;/th&gt;&lt;th&gt;Size&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;.vector_table&lt;/code&gt;&lt;/td&gt;&lt;td&gt;0x1000_0000&lt;/td&gt;&lt;td&gt;8 bytes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;.start_block&lt;/code&gt;&lt;/td&gt;&lt;td&gt;0x1000_0008&lt;/td&gt;&lt;td&gt;20 bytes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;.text&lt;/code&gt;&lt;/td&gt;&lt;td&gt;0x1000_001c&lt;/td&gt;&lt;td&gt;30 bytes&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;pre&gt;&lt;code data-lang=&quot;txt&quot;&gt;Contents of section .vector_table:
 10000000 00000820 1d000010                    ... ....
Contents of section .start_block:
 10000008 d3deffff 42012110 ff010000 00000000  ....B.!.........
 10000018 793512ab                             y5..
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The reset vector is &lt;code&gt;0x1000001d&lt;/code&gt;: &lt;code&gt;Reset&lt;/code&gt; at 0x1000_001c, plus the Thumb bit.
In the disassembly, the compiler unrolled your loop four times, and every store
still goes to 0x2000_0000, because the write is volatile. &lt;code&gt;cargo objcopy --release --bin l01-image -- -O binary l01.bin&lt;/code&gt; gives a 58-byte file, against
about 8 KB for blinky.&lt;/p&gt;
&lt;h2 id=&quot;flash-it-and-prove-it-runs&quot;&gt;Flash it and prove it runs&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;probe-rs download&lt;/code&gt; writes the ELF&#39;s &lt;code&gt;LOAD&lt;/code&gt; segments into flash at their
physical addresses. Then &lt;code&gt;probe-rs reset&lt;/code&gt; restarts the chip, so it boots through
the boot ROM as it would at power-on:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;probe-rs download --chip RP235x \
    target/thumbv8m.main-none-eabihf/release/l01-image
probe-rs reset --chip RP235x
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now read the heartbeat twice:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;probe-rs read --chip RP235x b32 0x20000000 1
probe-rs read --chip RP235x b32 0x20000000 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two different values mean the boot ROM accepted your IMAGE_DEF, loaded your
stack pointer, and jumped into your &lt;code&gt;Reset&lt;/code&gt;. Also read back your tag with
&lt;code&gt;probe-rs read --chip RP235x b32 0x10000008 5&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;cargo run --release&lt;/code&gt; works too, but &lt;code&gt;probe-rs run&lt;/code&gt; stays attached waiting for
logs that this image never sends, so press Ctrl-C to get the probe back.&lt;/p&gt;
&lt;h2 id=&quot;break-it-on-purpose&quot;&gt;Break it on purpose&lt;/h2&gt;
&lt;p&gt;Change the start marker in &lt;code&gt;IMAGE_DEF&lt;/code&gt; from &lt;code&gt;0xffff_ded3&lt;/code&gt; to &lt;code&gt;0xffff_ded4&lt;/code&gt;, then
rebuild, download and reset. The build succeeds, because the linker has no idea
what the words mean. The heartbeat stops changing. With the Pico 2&#39;s own USB
cable connected, run:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;lsusb | grep -i 2e8a
lsblk -o NAME,LABEL,SIZE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A Raspberry Pi device (vendor ID 2e8a) appears, along with a small drive labeled
RP2350. The boot ROM found no valid block in the first 4 KB and fell back to
BOOTSEL. Restore the marker, download, reset, and confirm the heartbeat ticks
again. The probe can flash the chip whether or not it&#39;s in BOOTSEL, so this is
always recoverable.&lt;/p&gt;
&lt;h2 id=&quot;how-rp-hal-does-it&quot;&gt;How rp-hal does it&lt;/h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Your piece&lt;/th&gt;&lt;th&gt;rp-hal&#39;s piece&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;link.ld&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;memory.x&lt;/code&gt; (regions, plus &lt;code&gt;.start_block&lt;/code&gt; and &lt;code&gt;.bi_entries&lt;/code&gt; placement) combined with cortex-m-rt&#39;s &lt;code&gt;link.x&lt;/code&gt; (everything else)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;IMAGE_DEF: [u32; 5]&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;ImageDef::secure_exe()&lt;/code&gt; from &lt;code&gt;rp235x-hal/src/block.rs&lt;/code&gt;, which builds the same five words&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Two-word &lt;code&gt;VectorTable&lt;/code&gt;&lt;/td&gt;&lt;td&gt;cortex-m-rt&#39;s 16 exception entries plus the PAC&#39;s 52 interrupt entries: 272 bytes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Reset&lt;/code&gt; that loops&lt;/td&gt;&lt;td&gt;cortex-m-rt&#39;s &lt;code&gt;Reset&lt;/code&gt;, which initializes RAM, enables the FPU, then calls &lt;code&gt;main&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;ASSERT&lt;/code&gt;s&lt;/td&gt;&lt;td&gt;cortex-m-rt asserts its own rules too. Find them in &lt;code&gt;link.x&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Open &lt;code&gt;block.rs&lt;/code&gt; and check that &lt;code&gt;secure_exe()&lt;/code&gt; produces your exact five words.&lt;/p&gt;
&lt;h2 id=&quot;exercises&quot;&gt;Exercises&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Past the 4 KB line.&lt;/strong&gt; Move &lt;code&gt;.start_block&lt;/code&gt; with `.start_block ORIGIN(FLASH)
&lt;ul&gt;
&lt;li&gt;4K : ALIGN(4)&lt;code&gt;. Your &lt;/code&gt;ASSERT` stops the build first. Comment the assert
out, predict what the boot ROM does, then flash and check. This is Lesson 0&#39;s
self-check question 4, answered on hardware.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trip the other assert.&lt;/strong&gt; Add &lt;code&gt;static mut SEEN: u32 = 7;&lt;/code&gt; and write to it
from &lt;code&gt;Reset&lt;/code&gt; with &lt;code&gt;ptr::write_volatile(&amp;amp;raw mut SEEN, 1)&lt;/code&gt;. Read the link
error. Which section did &lt;code&gt;SEEN&lt;/code&gt; try to land in, and what would go wrong if
the linker let it through?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Remove a layer.&lt;/strong&gt; Delete &lt;code&gt;KEEP&lt;/code&gt; from &lt;code&gt;.start_block&lt;/code&gt; and rebuild. Then put
it back and delete &lt;code&gt;#[used]&lt;/code&gt; instead. Check &lt;code&gt;cargo size -A&lt;/code&gt; each time. Which
change loses the IMAGE_DEF, and why?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fault with a two-word table.&lt;/strong&gt; Before the loop in &lt;code&gt;Reset&lt;/code&gt;, add &lt;code&gt;unsafe { ptr::read_volatile(0x0100_0000 as *const u32) }&lt;/code&gt;, a read from an unmapped
address. The bus fault becomes a HardFault, and the core fetches the
HardFault handler&#39;s address from word 3 of the vector table. Work out what&#39;s
at 0x1000_000c in your image, then predict whether the heartbeat ever
starts.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;self-check&quot;&gt;Self-check&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Why does the boot ROM need an IMAGE_DEF, when the vector table already says
where the code starts?&lt;/li&gt;
&lt;li&gt;What does the &lt;code&gt;LAST&lt;/code&gt; item&#39;s size field count, and what is it in your block?&lt;/li&gt;
&lt;li&gt;Why does the linker accept a wrong start marker without complaint?&lt;/li&gt;
&lt;li&gt;Where does the value of &lt;code&gt;_stack_start&lt;/code&gt; end up in the final image, and who
reads it first?&lt;/li&gt;
&lt;li&gt;Your image has no &lt;code&gt;.data&lt;/code&gt; and no &lt;code&gt;.bss&lt;/code&gt;. What would break if it had them?&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;p&gt;Previous: &lt;a href=&quot;https://k4s.xyz/blog/rp2350/lesson-0/&quot;&gt;Lesson 0: Toolchain and a Known-Good
Baseline&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Part of &lt;a href=&quot;https://k4s.xyz/blog/rp2350/&quot;&gt;Embedded Rust on the RP2350&lt;/a&gt;.&lt;/p&gt;
</description>
      </item>
      <item>
          <title>Lesson 0: Toolchain and a Known-Good Baseline</title>
          <pubDate>Sat, 17 Oct 2026 00:00:00 +0000</pubDate>
          <author>kevstewa@protonmail.com (Kevin Stewart)</author>
          <link>https://k4s.xyz/blog/rp2350/lesson-0/</link>
          <guid>https://k4s.xyz/blog/rp2350/lesson-0/</guid>
          <description xml:base="https://k4s.xyz/blog/rp2350/lesson-0/">&lt;p&gt;This is the first lesson of &lt;a href=&quot;https://k4s.xyz/blog/rp2350/&quot;&gt;Embedded Rust on the
RP2350&lt;/a&gt;, a course in embedded Rust on the Raspberry Pi
RP2350, built from the reset vector up. It assumes you know Rust reasonably well
and have done a little microcontroller work. Everything runs on Linux; the setup
steps are written for Fedora.&lt;/p&gt;
&lt;h2 id=&quot;goals-and-how-this-course-works&quot;&gt;Goals and how this course works&lt;/h2&gt;
&lt;p&gt;This lesson proves your bench end to end: a stock rp-hal example builds, flashes
and blinks, and you can read your firmware&#39;s bytes back out of the chip. That
matters for Lesson 1. When your hand-built image refuses to boot, you&#39;ll know
the fault is in the image, not the toolchain or the wiring.&lt;/p&gt;
&lt;p&gt;You finish with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A Rust toolchain that cross-compiles for the RP2350&#39;s Cortex-M33 cores (target
&lt;code&gt;thumbv8m.main-none-eabihf&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;A working flash path: a Raspberry Pi Debug Probe, over SWD.&lt;/li&gt;
&lt;li&gt;rp-hal cloned locally. It is the course&#39;s answer key.&lt;/li&gt;
&lt;li&gt;An inventory of every black-box piece in a typical RP2350 Rust project, each
mapped to the lesson that replaces it.&lt;/li&gt;
&lt;li&gt;An empty course workspace, ready for Lesson 1.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;The crate rule.&lt;/strong&gt; Part 1 uses zero dependencies. After that, a crate appears
only once you&#39;ve built a small version of what it does.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How every lesson is built.&lt;/strong&gt; Goals, datasheet sections to read, concepts, a
step-by-step build with complete code, a comparison with how rp-hal does it,
exercises, and self-check questions. Code accumulates in one Cargo workspace,
and each lesson builds on the one before it, so run each one on hardware before
moving on.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep these open:&lt;/strong&gt; the &lt;a rel=&quot;external&quot; href=&quot;https://datasheets.raspberrypi.com/rp2350/rp2350-datasheet.pdf&quot;&gt;RP2350
datasheet&lt;/a&gt;, the
&lt;a rel=&quot;external&quot; href=&quot;https://datasheets.raspberrypi.com/pico/pico-2-datasheet.pdf&quot;&gt;Pico 2
datasheet&lt;/a&gt;,
&lt;a rel=&quot;external&quot; href=&quot;https://github.com/rp-rs/rp-hal&quot;&gt;rp-hal&lt;/a&gt;, the &lt;a rel=&quot;external&quot; href=&quot;https://pico.implrust.com/&quot;&gt;pico-pico
book&lt;/a&gt; and &lt;a rel=&quot;external&quot; href=&quot;https://github.com/ShawnHymel/introduction-to-embedded-rust&quot;&gt;Shawn Hymel&#39;s course
repo&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;course-map&quot;&gt;Course map&lt;/h2&gt;
&lt;p&gt;PIO has its own lesson (10), and Lesson 11 uses it to build a logic analyzer, so
you don&#39;t need to buy one. Lesson 7 teaches Rust&#39;s concurrency tools on your
laptop first; Lesson 9 then puts the second Cortex-M33 to work, well before
Embassy.&lt;/p&gt;
&lt;p&gt;Lesson 21 comes right before the capstone so the station can sleep between
readings. A bench supply that reports current measures the awake states, and a
multimeter measures the deep ones. It leans on Lesson 5&#39;s clocks, Lesson 8&#39;s
interrupts and both cores going idle.&lt;/p&gt;
&lt;p&gt;After the capstone, a security track (S1 to S4) covers the RP2350&#39;s security
features. Short security notes appear earlier, wherever those mechanisms first
show up. Signed boot burns a key into OTP and can&#39;t be undone, so S3 runs on a
second Pico 2 kept for that purpose.&lt;/p&gt;
&lt;p&gt;The analyzer samples the chip&#39;s own pins while your code drives them, so one
board both runs the bus and watches it. You&#39;ll open captures in
&lt;a rel=&quot;external&quot; href=&quot;https://sigrok.org/wiki/PulseView&quot;&gt;PulseView&lt;/a&gt;, which is free. Lesson 18
upgrades it with a ring buffer so it can show what happened before the trigger.&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;#&lt;/th&gt;&lt;th&gt;Lesson&lt;/th&gt;&lt;th&gt;What you build&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;Toolchain and baseline&lt;/td&gt;&lt;td&gt;A proven bench: blinky flashed, ELF inspected, bytes read back&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;Anatomy of an RP2350 image&lt;/td&gt;&lt;td&gt;Linker script and a hand-assembled IMAGE_DEF the boot ROM accepts&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;Reset and the runtime contract&lt;/td&gt;&lt;td&gt;Reset handler in a naked function; LED on with raw writes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;3&lt;/td&gt;&lt;td&gt;GPIO from the registers up&lt;/td&gt;&lt;td&gt;Your register layer and type-state pins; blinky plus button&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;4&lt;/td&gt;&lt;td&gt;PAC and runtime crates&lt;/td&gt;&lt;td&gt;Your own PAC generated with svd2rust, then rp235x-pac and cortex-m-rt; defmt logging&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;5&lt;/td&gt;&lt;td&gt;Clocks from scratch&lt;/td&gt;&lt;td&gt;XOSC and PLLs to 150 MHz, checked by the frequency counter&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;6&lt;/td&gt;&lt;td&gt;Time, PWM and first trait impls&lt;/td&gt;&lt;td&gt;Mini-HAL v0.1: DelayNs, OutputPin, SetDutyCycle; the watchdog: enable, feed, reset reason, pause while debugging&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;7&lt;/td&gt;&lt;td&gt;Rust concurrency, laptop first&lt;/td&gt;&lt;td&gt;Small std programs on move closures, Send and Sync, Rc vs Arc, RefCell vs Mutex, atomics; then what changes without std&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;Interrupts and shared state&lt;/td&gt;&lt;td&gt;Interrupt-driven blinker, debounced button, your critical-section impl; a HardFault handler that reads the fault registers, saves them to watchdog scratch and reboots to report the crash&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;9&lt;/td&gt;&lt;td&gt;The second core (Arm only)&lt;/td&gt;&lt;td&gt;Core 1 booted by hand over the SIO FIFO; spinlocks, ordered atomics, a heap with Arc, your own safe spawn API&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;10&lt;/td&gt;&lt;td&gt;PIO from the instruction up&lt;/td&gt;&lt;td&gt;Hand-encoded PIO programs, then pio_asm!; side-set, FIFOs, clock dividers&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;11&lt;/td&gt;&lt;td&gt;Build a logic analyzer&lt;/td&gt;&lt;td&gt;PIO capture plus your first DMA transfer; captures opened in PulseView&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;12&lt;/td&gt;&lt;td&gt;UART and embedded-io&lt;/td&gt;&lt;td&gt;PL011 driver, interrupt-driven RX ring buffer, command console&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;13&lt;/td&gt;&lt;td&gt;I²C from registers&lt;/td&gt;&lt;td&gt;DesignWare I²C driver implementing I2c; bus scanner&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;14&lt;/td&gt;&lt;td&gt;Driver: SHT31-D&lt;/td&gt;&lt;td&gt;Standalone driver crate with host-side tests&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;15&lt;/td&gt;&lt;td&gt;SPI and bus sharing&lt;/td&gt;&lt;td&gt;PL022 SpiBus and shared-bus devices&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;16&lt;/td&gt;&lt;td&gt;Driver: ILI9340C TFT&lt;/td&gt;&lt;td&gt;Display driver, your bitmap font, DrawTarget&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;17&lt;/td&gt;&lt;td&gt;Driver: SD card&lt;/td&gt;&lt;td&gt;SPI-mode block driver, FAT32 walked by hand, embedded-sdmmc&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;18&lt;/td&gt;&lt;td&gt;DMA in depth&lt;/td&gt;&lt;td&gt;Chaining, ring buffers, safe buffer ownership; BMP viewer; analyzer pre-trigger&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;19&lt;/td&gt;&lt;td&gt;rp235x-hal under the hood&lt;/td&gt;&lt;td&gt;HAL swap with your drivers unchanged&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;20&lt;/td&gt;&lt;td&gt;Async to Embassy&lt;/td&gt;&lt;td&gt;Toy executor, then embassy-rp, async drivers, an executor per core&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;21&lt;/td&gt;&lt;td&gt;Power: sleep, dormant, power-down&lt;/td&gt;&lt;td&gt;Clock gating while asleep, DORMANT woken by a GPIO edge or the always-on timer, switched-core power-down that keeps SRAM; current logged from a bench supply by a small Rust host tool, with a multimeter for the microamp states&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;22&lt;/td&gt;&lt;td&gt;Capstone: environment station&lt;/td&gt;&lt;td&gt;SHT31 dashboard on the TFT, CSV logging to SD, work split across both cores, sleeping between samples, a watchdog fed only while both cores are alive&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;E1&lt;/td&gt;&lt;td&gt;TMC2209 stepper (after 12)&lt;/td&gt;&lt;td&gt;STEP/DIR ramps, single-wire UART driver, PIO step generator&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;E2&lt;/td&gt;&lt;td&gt;OLED FeatherWing (after 16)&lt;/td&gt;&lt;td&gt;Framebuffer driver plus the wing&#39;s buttons&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;S1&lt;/td&gt;&lt;td&gt;TrustZone-M&lt;/td&gt;&lt;td&gt;SAU setup, a Secure and Non-secure split, calls through &lt;code&gt;.gnu.sgstubs&lt;/code&gt; veneers&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;S2&lt;/td&gt;&lt;td&gt;OTP&lt;/td&gt;&lt;td&gt;Reading the OTP rows, what each controls, and which writes are permanent&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;S3&lt;/td&gt;&lt;td&gt;Hashed and signed images&lt;/td&gt;&lt;td&gt;Hash and signature items in the block loop, signing with picotool, secure boot on the spare board&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;S4&lt;/td&gt;&lt;td&gt;Hardening&lt;/td&gt;&lt;td&gt;Glitch detector, debug lockdown, and the published attacks on the A2 stepping&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;portfolio-projects&quot;&gt;Portfolio projects&lt;/h2&gt;
&lt;p&gt;Eight milestones end in a standalone repo you can publish. Between them, the
course workspace stays your lab notebook (a private repo works well) and keeps
building on its own copy.&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;After&lt;/th&gt;&lt;th&gt;Repo&lt;/th&gt;&lt;th&gt;What it shows&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;3&lt;/td&gt;&lt;td&gt;&lt;code&gt;rp2350-from-reset&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Boots with zero dependencies: your linker script, IMAGE_DEF, reset handler, GPIO from raw registers&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;&lt;code&gt;rp2350-minihal&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Your HAL implementing embedded-hal 1.0: clocks, timers, PWM, interrupts. New releases at 13, 15 and 18&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;9&lt;/td&gt;&lt;td&gt;&lt;code&gt;rp2350-dual-core&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Core 1 booted by hand, a safe spawn API, cross-core messaging, a measured speedup&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;11&lt;/td&gt;&lt;td&gt;&lt;code&gt;pio-logic-analyzer&lt;/code&gt;&lt;/td&gt;&lt;td&gt;A working tool: PIO and DMA capture, opened in PulseView. Version 2 adds pre-trigger at 18&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;14&lt;/td&gt;&lt;td&gt;&lt;code&gt;sht31&lt;/code&gt;&lt;/td&gt;&lt;td&gt;A HAL-agnostic driver crate with host-side and mock-bus tests&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;17&lt;/td&gt;&lt;td&gt;&lt;code&gt;tft-breakout&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Display and SD card drivers for the Adafruit 2.2&quot; breakout. A DMA image viewer joins at 18&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;22&lt;/td&gt;&lt;td&gt;&lt;code&gt;environment-station&lt;/code&gt;&lt;/td&gt;&lt;td&gt;The capstone: both cores, async tasks, live dashboard, SD logging&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;E1&lt;/td&gt;&lt;td&gt;&lt;code&gt;tmc2209&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Stepper driver crate: single-wire UART protocol plus PIO step generation&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Repo names are suggestions. Driver repos stay HAL-agnostic: their examples run
on your mini-HAL, pinned by git tag, and gain rp235x-hal examples after Lesson
19 so anyone can run them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Exporting a milestone&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Copy the crate out of the workspace into a fresh directory. The workspace
copy carries on into the next lesson untouched.&lt;/li&gt;
&lt;li&gt;Make it build alone. Replace each &lt;code&gt;*.workspace = true&lt;/code&gt; field with a real
value, and bring &lt;code&gt;.cargo/config.toml&lt;/code&gt;, &lt;code&gt;rust-toolchain.toml&lt;/code&gt;, &lt;code&gt;memory.x&lt;/code&gt; and
&lt;code&gt;build.rs&lt;/code&gt; along. Path dependencies on your mini-HAL become git dependencies
pinned to a tag.&lt;/li&gt;
&lt;li&gt;Add a README covering what it demonstrates, the wiring, how to flash it, and
what you learned building it. Add a license; &lt;code&gt;MIT OR Apache-2.0&lt;/code&gt; is the Rust
norm.&lt;/li&gt;
&lt;li&gt;From a fresh clone, run &lt;code&gt;cargo fmt --check&lt;/code&gt;, &lt;code&gt;cargo clippy&lt;/code&gt;, any host tests,
and &lt;code&gt;cargo build --release&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Commit, tag &lt;code&gt;v0.1.0&lt;/code&gt;, and push to your git host.&lt;/li&gt;
&lt;li&gt;Optional CI: a workflow that builds for the RP2350 target and runs the host
tests.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Lesson 3 walks through the first export end to end. A repo that later lessons
improve gets a new tagged release from a fresh copy, so every published version
stays a coherent snapshot. If you want a crate&#39;s commit history in its repo,
&lt;code&gt;git subtree split --prefix=lessons/&amp;lt;crate&amp;gt;&lt;/code&gt; extracts it; a plain copy starts
clean.&lt;/p&gt;
&lt;h2 id=&quot;what-you-need&quot;&gt;What you need&lt;/h2&gt;
&lt;p&gt;The two essentials are a Pico 2 and a Raspberry Pi Debug Probe. Every lesson
flashes and debugs through the probe.&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Item&lt;/th&gt;&lt;th&gt;When&lt;/th&gt;&lt;th&gt;Why&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Raspberry Pi Pico 2&lt;/td&gt;&lt;td&gt;Now&lt;/td&gt;&lt;td&gt;The course uses its pinout: 12 MHz crystal, LED on GPIO25&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Raspberry Pi Debug Probe&lt;/td&gt;&lt;td&gt;Now&lt;/td&gt;&lt;td&gt;Flashes over SWD, halts the core, reads any address; its UART port serves Lesson 12&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;USB cables that carry data, one per board&lt;/td&gt;&lt;td&gt;Now&lt;/td&gt;&lt;td&gt;A charge-only cable fails silently&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;3-pin header for the Pico 2&#39;s debug pads&lt;/td&gt;&lt;td&gt;Now&lt;/td&gt;&lt;td&gt;Connects the probe&#39;s SWD cable&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Breadboard, jumpers, LED with 330 Ω resistor, push button&lt;/td&gt;&lt;td&gt;Lesson 3&lt;/td&gt;&lt;td&gt;GPIO output and input&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Logic analyzer&lt;/td&gt;&lt;td&gt;Never&lt;/td&gt;&lt;td&gt;You build one in Lesson 11&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Bench supply that reports current (optional)&lt;/td&gt;&lt;td&gt;Lesson 21&lt;/td&gt;&lt;td&gt;Powers the Pico 2 through VSYS and logs its current while running and sleeping&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Multimeter with a µA range&lt;/td&gt;&lt;td&gt;Lesson 21&lt;/td&gt;&lt;td&gt;Dormant and power-down draw tens to hundreds of µA, below what most bench supplies resolve&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;A second Pico 2&lt;/td&gt;&lt;td&gt;Before S3&lt;/td&gt;&lt;td&gt;Secure boot burns OTP permanently; keep your main board unlocked&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Why the probe matters.&lt;/strong&gt; &lt;code&gt;cargo run&lt;/code&gt; flashes over SWD with no button presses.
You can halt the CPU, read memory, and stream logs. In Lesson 2, a broken reset
handler leaves the board silent, and the probe is your only window into it.&lt;/p&gt;
&lt;p&gt;Software, installed next: Rust stable with the &lt;code&gt;thumbv8m.main-none-eabihf&lt;/code&gt;
target, cargo-binutils, probe-rs, picotool, git, and an editor with
rust-analyzer. The course uses Helix. PulseView arrives with Lesson 11.&lt;/p&gt;
&lt;h2 id=&quot;fedora-setup&quot;&gt;Fedora setup&lt;/h2&gt;
&lt;p&gt;On a fresh Fedora install, do this section first. It adds what the next section
takes for granted: rustup, the libraries probe-rs builds against, picotool, and
permission to use the probe without sudo. After it, every command in the course
runs unchanged. These steps were checked against Fedora 44 on x86_64.&lt;/p&gt;
&lt;h3 id=&quot;1-packages-from-fedora&quot;&gt;1. Packages from Fedora&lt;/h3&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;# Rust&amp;#39;s installer, the editor, and what cargo needs to build probe-rs
sudo dnf install rustup helix helix-parsers helix-themes git gcc cmake \
  pkgconf-pkg-config libusb1-devel systemd-devel

# Rust itself, into ~/.rustup and ~/.cargo; the default choices are fine
rustup-init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open a new terminal afterwards so &lt;code&gt;~/.cargo/bin&lt;/code&gt; is on your PATH. The editor&#39;s
command is still &lt;code&gt;hx&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Fedora splits Helix into three packages. &lt;code&gt;helix&lt;/code&gt; is the editor; &lt;code&gt;helix-parsers&lt;/code&gt;
holds the grammars and queries behind syntax highlighting; &lt;code&gt;helix-themes&lt;/code&gt; holds
the themes. &lt;code&gt;helix&lt;/code&gt; only recommends the other two, and a minimal install skips
recommended packages, so name them. Without &lt;code&gt;helix-parsers&lt;/code&gt;, Helix opens files
with no highlighting at all.&lt;/p&gt;
&lt;p&gt;On the bare Linux console, with no desktop, only 16 colors exist. Pick a theme
built for that in &lt;code&gt;~/.config/helix/config.toml&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;toml&quot;&gt;# ~/.config/helix/config.toml
theme = &amp;quot;term16_dark&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Don&#39;t also install Fedora&#39;s &lt;code&gt;rust&lt;/code&gt; and &lt;code&gt;cargo&lt;/code&gt; packages. The workspace&#39;s
&lt;code&gt;rust-toolchain.toml&lt;/code&gt; only takes effect through rustup&#39;s own &lt;code&gt;cargo&lt;/code&gt;, so &lt;code&gt;which cargo&lt;/code&gt; should print a path under &lt;code&gt;~/.cargo/bin&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;2-picotool-from-rpm-fusion&quot;&gt;2. picotool from RPM Fusion&lt;/h3&gt;
&lt;p&gt;Fedora doesn&#39;t package picotool. RPM Fusion&#39;s nonfree repository does (version
2.3.1), and the package installs picotool&#39;s udev rules too. Enable RPM Fusion
the way its documentation shows, which adds the free and nonfree repositories
together, then install:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;sudo dnf install \
  https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm \
  https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm
sudo dnf install picotool
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you&#39;d rather not add RPM Fusion, use Raspberry Pi&#39;s prebuilt picotool from
the releases page linked in the next section. You then copy its udev rules into
&lt;code&gt;/etc/udev/rules.d&lt;/code&gt; yourself.&lt;/p&gt;
&lt;h3 id=&quot;3-probe-and-serial-permissions&quot;&gt;3. Probe and serial permissions&lt;/h3&gt;
&lt;p&gt;The probe-rs and picotool rules both grant access two ways: to whoever is logged
in at the machine&#39;s own screen and keyboard, and to members of the &lt;code&gt;plugdev&lt;/code&gt;
group. Over SSH only the group works. Fedora doesn&#39;t create &lt;code&gt;plugdev&lt;/code&gt;, and
probe-rs says to make it a system group on systemd newer than v258; Fedora 44
ships v259.&lt;/p&gt;
&lt;p&gt;Lesson 12&#39;s serial port, &lt;code&gt;/dev/ttyACM0&lt;/code&gt;, belongs to the &lt;code&gt;dialout&lt;/code&gt; group, and
logging in at the machine doesn&#39;t open it.&lt;/p&gt;
&lt;p&gt;Create the group and join both groups first. udev looks up a group name when it
loads a rules file, and drops the group from the rule if it doesn&#39;t exist yet.
Rules loaded before &lt;code&gt;plugdev&lt;/code&gt; existed wouldn&#39;t use it until udev reloaded them.&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;sudo groupadd --system plugdev
sudo usermod -aG plugdev,dialout $USER
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;probe-rs publishes its own udev rules. Install them, then reload, which picks up
picotool&#39;s rules from step 2 as well, now that &lt;code&gt;plugdev&lt;/code&gt; exists:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;sudo curl -fsSL -o /etc/udev/rules.d/69-probe-rs.rules \
  https://probe.rs/files/69-probe-rs.rules
sudo udevadm control --reload
sudo udevadm trigger
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Debug Probe has no entry of its own in that file. It matches the rule for
any adapter whose USB product name contains CMSIS-DAP.&lt;/p&gt;
&lt;p&gt;Log out and back in so the new groups apply, then check:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;groups            # lists plugdev and dialout
which cargo       # a path under ~/.cargo/bin
probe-rs list     # once probe-rs is installed: the probe, without sudo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;For later lessons.&lt;/strong&gt; PulseView (Lesson 11) and a serial terminal (Lesson 12)
are both packaged: &lt;code&gt;sudo dnf install pulseview tio&lt;/code&gt;. Fedora&#39;s &lt;code&gt;openocd&lt;/code&gt; and RPM
Fusion&#39;s &lt;code&gt;pico-sdk&lt;/code&gt; aren&#39;t needed; the course uses neither.&lt;/p&gt;
&lt;h2 id=&quot;install-the-toolchain&quot;&gt;Install the toolchain&lt;/h2&gt;
&lt;p&gt;Four tools cover the whole course: the Cortex-M33 compile target, LLVM&#39;s binary
inspectors, probe-rs, and picotool.&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;# 1. Rust stable plus the RP2350 Arm target (Armv8-M Mainline, hard-float ABI)
rustup update stable
rustup target add thumbv8m.main-none-eabihf

# 2. cargo size / objdump / nm, backed by LLVM&amp;#39;s tools
rustup component add llvm-tools
cargo install cargo-binutils

# 3. probe-rs: flashing, logs, memory reads
cargo install --locked probe-rs-tools
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Install picotool as well. Its &lt;code&gt;info&lt;/code&gt; command and UF2 conversion stay useful
alongside the probe.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Prebuilt binaries: &lt;a rel=&quot;external&quot; href=&quot;https://github.com/raspberrypi/pico-sdk-tools/releases&quot;&gt;raspberrypi/pico-sdk-tools
releases&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Linux distro packages exist, including RPM Fusion&#39;s for Fedora; make sure
&lt;code&gt;picotool version&lt;/code&gt; reports 2.x, which added RP2350 support.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Check everything answers:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;rustup target list --installed
cargo size --version
probe-rs --version
picotool version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Linux permissions.&lt;/strong&gt; udev rules let probe-rs and picotool run without sudo; on
Fedora, the setup above installs them. On another distribution, follow the same
order: make sure the &lt;code&gt;plugdev&lt;/code&gt; group exists and you&#39;re in it, then install
&lt;a rel=&quot;external&quot; href=&quot;https://probe.rs/files/69-probe-rs.rules&quot;&gt;probe-rs&#39;s rules&lt;/a&gt; and picotool&#39;s
(from its repository&#39;s &lt;code&gt;udev&lt;/code&gt; folder) into &lt;code&gt;/etc/udev/rules.d&lt;/code&gt; and reload udev.
If either tool ever demands sudo after an OS upgrade, check the rules first.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Editor: Helix.&lt;/strong&gt; Helix talks to rust-analyzer over LSP, so install the server
through rustup and confirm Helix finds it:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;rustup component add rust-analyzer
hx --health rust   # the rust-analyzer line should show a path, not &amp;quot;Not found&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Helix&#39;s debugger support is still experimental, so Lesson 2 single-steps the
reset handler from the command line through the probe instead.&lt;/p&gt;
&lt;h2 id=&quot;wire-it-up&quot;&gt;Wire it up&lt;/h2&gt;
&lt;p&gt;Connect ground first, then the signals. The probe&#39;s D port carries SWD and its U
port carries UART. Raspberry Pi&#39;s docs warn that boards on separate power need a
common ground before any signal wire goes on.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Raspberry Pi Debug Probe to Pico 2&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Probe port and wire&lt;/th&gt;&lt;th&gt;Pico 2 connection&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;D, black (GND)&lt;/td&gt;&lt;td&gt;Debug header GND&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;D, orange (SC)&lt;/td&gt;&lt;td&gt;Debug header SWCLK&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;D, yellow (SD)&lt;/td&gt;&lt;td&gt;Debug header SWDIO&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;U, black (GND)&lt;/td&gt;&lt;td&gt;Pin 3, GND&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;U, orange (probe TX)&lt;/td&gt;&lt;td&gt;Pin 2, GP1 (UART0 RX)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;U, yellow (probe RX)&lt;/td&gt;&lt;td&gt;Pin 1, GP0 (UART0 TX)&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;A plain Pico 2 has no JST debug connector. Solder a 3-pin header onto the SWCLK,
GND and SWDIO pads at the end opposite USB, then use the probe&#39;s
JST-to-female-header cable on the D port. The UART rows matter from Lesson 12,
but wiring them now costs nothing. Power each board from its own USB cable; the
shared GND wire ties them together.&lt;/p&gt;
&lt;p&gt;Source: &lt;a rel=&quot;external&quot; href=&quot;https://www.raspberrypi.com/documentation/microcontrollers/debug-probe.html&quot;&gt;Raspberry Pi Debug Probe
documentation&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;verify-the-chain&quot;&gt;Verify the chain&lt;/h2&gt;
&lt;p&gt;Confirm the host sees the probe, and the probe sees the chip, before you build
anything.&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;probe-rs list   # should list a CMSIS-DAP probe
probe-rs info   # reads the target&amp;#39;s debug port; no Rust involved yet
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If &lt;code&gt;info&lt;/code&gt; reports an SWD or wire-protocol error, recheck the ground wire first,
then check SC and SD aren&#39;t swapped.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Read your chip marking.&lt;/strong&gt; The top of the RP2350 carries a line such as
RP2350A0A2 or RP2350A0A4; a phone camera&#39;s zoom helps. The last two characters
are the stepping. Steppings A3 and later fix erratum E9, the GPIO input leakage
issue Lesson 3 covers (datasheet Appendix C.2.1, p. 1349). Note yours down.&lt;/p&gt;
&lt;h2 id=&quot;flash-the-baseline&quot;&gt;Flash the baseline&lt;/h2&gt;
&lt;p&gt;Flash rp-hal&#39;s own blinky before writing a line yourself. If the LED blinks,
every link in the chain works.&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;git clone https://github.com/rp-rs/rp-hal.git
cd rp-hal/rp235x-hal-examples
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open &lt;code&gt;.cargo/config.toml&lt;/code&gt; and find the &lt;code&gt;[target.thumbv8m.main-none-eabihf]&lt;/code&gt;
table. Point its &lt;code&gt;runner&lt;/code&gt; line at the probe:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;toml&quot;&gt;# .cargo/config.toml, in [target.thumbv8m.main-none-eabihf]
runner = &amp;quot;probe-rs run --chip RP235x&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Build and flash:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;cargo run --release --bin blinky
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;cargo run&lt;/code&gt; builds the ELF, then calls the runner with the ELF&#39;s path as the
last argument. The first build compiles every dependency, so it is the slow one.
With probe-rs, the command stays attached to stream output; Ctrl-C ends the
session.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;If it fails&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;probe-rs finds no probe: check the probe&#39;s USB cable, the udev rules, and that
no other program holds the probe.&lt;/li&gt;
&lt;li&gt;SWD or wire-protocol errors: ground first, then SC and SD swapped, then the
solder joints on the debug header.&lt;/li&gt;
&lt;li&gt;It flashes but nothing blinks: confirm the board is a Pico 2, not a Pico 2 W,
and that the example still drives &lt;code&gt;gpio25&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;the-magic-inventory&quot;&gt;The magic inventory&lt;/h2&gt;
&lt;p&gt;Seventeen pieces of this baseline run before or around your code without you
writing them. By Lesson 8, you will have rebuilt every one. Open
&lt;code&gt;.cargo/config.toml&lt;/code&gt;, &lt;code&gt;memory.x&lt;/code&gt;, &lt;code&gt;build.rs&lt;/code&gt; and &lt;code&gt;src/bin/blinky.rs&lt;/code&gt;, and find
each piece.&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Piece (where)&lt;/th&gt;&lt;th&gt;What it does&lt;/th&gt;&lt;th&gt;Rebuilt in&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Target &lt;code&gt;thumbv8m.main-none-eabihf&lt;/code&gt; (&lt;code&gt;.cargo/config.toml&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;Armv8-M Mainline, Thumb code, hard-float ABI&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;-Tlink.x&lt;/code&gt; and &lt;code&gt;--nmagic&lt;/code&gt; (&lt;code&gt;.cargo/config.toml&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;Hands the linker cortex-m-rt&#39;s script; skips page alignment&lt;/td&gt;&lt;td&gt;1, then 4&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;memory.x&lt;/code&gt; (crate root)&lt;/td&gt;&lt;td&gt;Flash and RAM regions; slots &lt;code&gt;.start_block&lt;/code&gt; after the vector table&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;build.rs&lt;/code&gt; (crate root)&lt;/td&gt;&lt;td&gt;Copies &lt;code&gt;memory.x&lt;/code&gt; to where the linker searches&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;#![no_std]&lt;/code&gt;, &lt;code&gt;#![no_main]&lt;/code&gt; (&lt;code&gt;blinky.rs&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;No OS, no runtime-provided &lt;code&gt;main&lt;/code&gt;&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Vector table (cortex-m-rt)&lt;/td&gt;&lt;td&gt;Initial stack pointer, then handler addresses&lt;/td&gt;&lt;td&gt;1; device IRQs in 8&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;IMAGE_DEF&lt;/code&gt; in &lt;code&gt;.start_block&lt;/code&gt; (&lt;code&gt;blinky.rs&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;Tells the boot ROM this is a valid Arm executable&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Reset handler (cortex-m-rt)&lt;/td&gt;&lt;td&gt;Copies &lt;code&gt;.data&lt;/code&gt;, zeroes &lt;code&gt;.bss&lt;/code&gt;, enables the FPU, calls &lt;code&gt;main&lt;/code&gt;&lt;/td&gt;&lt;td&gt;2&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;#[hal::entry]&lt;/code&gt; (&lt;code&gt;blinky.rs&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;rp235x-hal&#39;s wrapper around cortex-m-rt&#39;s entry attribute&lt;/td&gt;&lt;td&gt;2, compared in 4&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;The &lt;code&gt;panic_*&lt;/code&gt; crate import (&lt;code&gt;blinky.rs&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;Supplies the mandatory panic handler&lt;/td&gt;&lt;td&gt;2&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Peripherals::take()&lt;/code&gt; (&lt;code&gt;blinky.rs&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;Hands out each peripheral exactly once&lt;/td&gt;&lt;td&gt;3 raw, 4 via PAC&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Pins::new&lt;/code&gt;, &lt;code&gt;into_push_pull_output&lt;/code&gt; (&lt;code&gt;blinky.rs&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;Releases IO and pad resets; configures pad and pin function&lt;/td&gt;&lt;td&gt;3&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;init_clocks_and_plls&lt;/code&gt; (&lt;code&gt;blinky.rs&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;Crystal, PLLs, clock muxes&lt;/td&gt;&lt;td&gt;5&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Timer and &lt;code&gt;delay_ms&lt;/code&gt; (&lt;code&gt;blinky.rs&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;64-bit microsecond timer behind embedded-hal&#39;s DelayNs&lt;/td&gt;&lt;td&gt;6&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;set_high&lt;/code&gt;, &lt;code&gt;set_low&lt;/code&gt; (&lt;code&gt;blinky.rs&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;embedded-hal&#39;s OutputPin trait&lt;/td&gt;&lt;td&gt;6&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;-Tdefmt.x&lt;/code&gt; (&lt;code&gt;.cargo/config.toml&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;Linker support for defmt&#39;s compact logging&lt;/td&gt;&lt;td&gt;4&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;PICOTOOL_ENTRIES&lt;/code&gt; in &lt;code&gt;.bi_entries&lt;/code&gt; (&lt;code&gt;blinky.rs&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;Metadata that &lt;code&gt;picotool info&lt;/code&gt; displays&lt;/td&gt;&lt;td&gt;1, as an exercise&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Names drift between rp-hal versions. If a piece isn&#39;t where the table says, note
what you found instead; that is part of the exercise.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What IMAGE_DEF is.&lt;/strong&gt; The boot ROM won&#39;t run whatever happens to be in flash.
Before launching anything, it searches the first 4 KB of flash for a small tag
called an image definition, or IMAGE_DEF. The tag says &quot;this is a program,
built for an Arm core, for an RP2350&quot;. If the ROM can&#39;t find one, it treats the
flash as blank and drops into BOOTSEL mode. The smallest valid tag is 20 bytes:
a start marker, one item describing the image, a closing item, a link to the
next block, and an end marker (&lt;a rel=&quot;external&quot; href=&quot;https://datasheets.raspberrypi.com/rp2350/rp2350-datasheet.pdf&quot;&gt;RP2350
datasheet&lt;/a&gt;
§5.1.4, p. 328, and §5.9.5.1, p. 389). The ROM then finds the vector table at
the start of the image, because no item says otherwise (§5.9.3.3, p. 385).
rp-hal writes the tag for you with &lt;code&gt;ImageDef::secure_exe()&lt;/code&gt;. In Lesson 1 you
type out all five words yourself.&lt;/p&gt;
&lt;h2 id=&quot;look-inside-the-elf&quot;&gt;Look inside the ELF&lt;/h2&gt;
&lt;p&gt;The ELF is a container for your tools, not something the chip ever sees. The
boot ROM only meets the raw bytes that land in flash. This section opens the
container, then follows its bytes to their final addresses.&lt;/p&gt;
&lt;h3 id=&quot;the-container&quot;&gt;The container&lt;/h3&gt;
&lt;p&gt;ELF, the Executable and Linkable Format, wraps one blob of bytes in three kinds
of table. The ELF header says what the file is. Program headers describe
segments: which bytes to load, and where. Section headers name the regions the
linker built, such as &lt;code&gt;.text&lt;/code&gt; and &lt;code&gt;.data&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;txt&quot;&gt;The blinky ELF file                      Where each part goes
(file offsets increase downward)

+--------------------------------+
| ELF header                     |  52 bytes: class, machine, entry point
+--------------------------------+
| Program headers                |  segments: what to load, and where
+--------------------------------+
| Loadable section bytes         |  flashed    Flash at 0x1000_0000
|   .vector_table, .start_block, | ----------&amp;gt; probe-rs writes these bytes
|   .text, .rodata, and the      |             at the PhysAddr (LMA) of
|   initial values for .data     |             each segment
+--------------------------------+
| Non-allocated sections         |  read by    Stays on your PC
|   .debug_*, .symtab, .strtab,  | ----------&amp;gt; debugger: symbols, source
|   .comment, .ARM.attributes    |  tools      lines, variable types;
+--------------------------------+             defmt strings later
| Section headers                |  name, type and flags of every section
+--------------------------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;probe-rs reads the program headers to decide which bytes to write at which flash
addresses. It reads the symbol table and debug sections to show you variables,
backtraces and, from Lesson 4, logs.&lt;/p&gt;
&lt;p&gt;Look at all three tables from &lt;code&gt;rp-hal/rp235x-hal-examples&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;# ELF header: class, endianness, machine, entry point
cargo readobj --release --bin blinky -- --elf-output-style=GNU --file-headers

# Program headers: the loader&amp;#39;s view, segment by segment
cargo readobj --release --bin blinky -- --elf-output-style=GNU --program-headers

# Section headers: the linker&amp;#39;s view, with flags
cargo readobj --release --bin blinky -- --elf-output-style=GNU --section-headers
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;In the ELF header&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Class ELF32 with little-endian data: a 32-bit Arm core.&lt;/li&gt;
&lt;li&gt;Type EXEC: a fully linked executable, not an object file waiting for more
linking.&lt;/li&gt;
&lt;li&gt;The entry point is &lt;code&gt;Reset&lt;/code&gt; with bit 0 set. The chip never reads it; debuggers
do.&lt;/li&gt;
&lt;li&gt;The flags read 0x5000400: Arm EABI version 5 with the hard-float ABI, matching
the &lt;code&gt;eabihf&lt;/code&gt; in your target name.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;In the program headers&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Each LOAD segment pairs a VirtAddr (VMA, virtual memory address: where code
expects the bytes at run time) with a PhysAddr (LMA, load memory address:
where the bytes are stored). &quot;Virtual&quot; is a leftover from systems with an MMU;
on this chip both are plain physical addresses.&lt;/li&gt;
&lt;li&gt;For segments in flash the two match, because the RP2350 executes straight from
flash.&lt;/li&gt;
&lt;li&gt;When &lt;code&gt;.data&lt;/code&gt; isn&#39;t empty, its segment has a RAM VirtAddr and a flash PhysAddr.
That gap is the copy your reset handler performs. Blinky&#39;s &lt;code&gt;.data&lt;/code&gt; is empty,
so you won&#39;t see this segment until the experiment below.&lt;/li&gt;
&lt;li&gt;Where MemSiz exceeds FileSiz, the extra bytes exist only at run time and must
be zeroed. That is &lt;code&gt;.bss&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;In the section headers&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Flag A (alloc) means the section occupies target memory. X means executable, W
writable.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.bss&lt;/code&gt; has type NOBITS: it takes RAM but no bytes in the file.&lt;/li&gt;
&lt;li&gt;Sections without the A flag never reach the chip: &lt;code&gt;.debug_*&lt;/code&gt;, &lt;code&gt;.symtab&lt;/code&gt;,
&lt;code&gt;.strtab&lt;/code&gt;, &lt;code&gt;.comment&lt;/code&gt;, &lt;code&gt;.ARM.attributes&lt;/code&gt;. That is why &lt;code&gt;debug = 2&lt;/code&gt; costs no
flash.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What the non-allocated sections hold&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.debug_info&lt;/code&gt; describes every type, variable and function in DWARF, the
standard debug format. It is how a debugger shows a struct&#39;s fields by name.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.debug_line&lt;/code&gt; maps each code address to a source file and line. Stepping
through source, and line numbers in backtraces, both come from here.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.debug_frame&lt;/code&gt; records how to unwind each function&#39;s stack frame. probe-rs
uses it to print a backtrace after a panic or fault.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.debug_str&lt;/code&gt;, &lt;code&gt;.debug_abbrev&lt;/code&gt;, &lt;code&gt;.debug_ranges&lt;/code&gt; and &lt;code&gt;.debug_loc&lt;/code&gt; are supporting
tables: shared strings, encoding templates, address ranges, and where each
variable lives as code runs. Exact names vary a little with the DWARF version.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.symtab&lt;/code&gt; is the symbol table that &lt;code&gt;cargo nm&lt;/code&gt; prints: name, address, size and
type for every function and static. &lt;code&gt;.strtab&lt;/code&gt; holds those names.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.shstrtab&lt;/code&gt; holds the section names themselves.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.comment&lt;/code&gt; records the compiler and linker versions that built the file.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.ARM.attributes&lt;/code&gt; records the architecture, FPU and calling convention. Tools
use it to spot incompatible objects, such as soft-float code in a hard-float
build. Add &lt;code&gt;--arch-specific&lt;/code&gt; to the readobj command to see it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Thrown away at link time&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.ARM.exidx&lt;/code&gt; and &lt;code&gt;.ARM.extab&lt;/code&gt; hold unwinding tables for panics that unwind.
This target aborts on panic instead, so cortex-m-rt&#39;s linker script discards
them.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.got&lt;/code&gt;, the global offset table, serves position-independent code. cortex-m-rt
keeps it only to fail the link if anything needs one, so it should always be
empty.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;where-the-bytes-land&quot;&gt;Where the bytes land&lt;/h3&gt;
&lt;pre&gt;&lt;code data-lang=&quot;txt&quot;&gt;rp235x-hal blinky linked by cortex-m-rt; addresses increase downward

Flash (XIP) at 0x1000_0000           RAM at 0x2000_0000
+------------------------------+     +--------------------------------+
| Vector table                 |  +-&amp;gt;| .data                          |
|   initial SP, then handler   |  |  |   statics with initial values  |
|   addresses                  |  |  +--------------------------------+
+------------------------------+  |  | .bss                           |
| .start_block                 |  |  |   statics that start at zero;  |
|   IMAGE_DEF, then picotool&amp;#39;s |  |  |   zeroed at reset, no flash    |
|   header; must sit in the    |  |  |   copy                         |
|   first 4 KB of flash        |  |  +--------------------------------+
+------------------------------+  |  |                                |
| .text                        |  |  |   free RAM                     |
|   your code plus the runtime |  |  |                ^               |
+------------------------------+  |  +----------------|---------------+
| .bi_entries                  |  |  | Stack                          |
|   picotool metadata          |  |  |   starts at 0x2008_0000 and    |
+------------------------------+  |  |   grows toward lower addresses |
| .rodata                      |  |  +--------------------------------+
|   constants and strings      |  |
+------------------------------+  |
| .data load copy              |--+  copied at reset
|   initial values for RAM     |
|   statics                    |
+------------------------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Only &lt;code&gt;.data&lt;/code&gt; needs both regions: its initial values ship in flash and are copied
to RAM before &lt;code&gt;main&lt;/code&gt;. &lt;code&gt;.bss&lt;/code&gt; gets no flash bytes at all.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Section by section, in flash&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.vector_table&lt;/code&gt; sits at 0x1000_0000 and is 272 bytes long. Word 0 is the
initial stack pointer and word 1 the reset handler. The next 14 words serve
the Cortex-M33&#39;s own exceptions (NMI, HardFault, SVCall, PendSV, SysTick and
others), then come 52 words, one per RP2350 interrupt line. The boot ROM reads
the first two words at launch; the CPU reads the rest each time an exception
fires.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.start_block&lt;/code&gt; follows at 0x1000_0110. It holds the 20-byte IMAGE_DEF,
followed in current rp-hal by a 20-byte picotool binary-info header, 40 bytes
in all. Your running code never touches either one. The IMAGE_DEF exists so
the boot ROM accepts the image at all; the header tells picotool where
&lt;code&gt;.bi_entries&lt;/code&gt; lives.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.text&lt;/code&gt; is machine code: your functions, the HAL&#39;s, &lt;code&gt;core&lt;/code&gt;&#39;s, and compiler
helpers such as 64-bit division. Each function arrives in its own input
section, so the linker can drop everything unreachable. That is why unused HAL
code costs no flash. The CPU executes it in place from flash, through a cache.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.bi_entries&lt;/code&gt; is a table of pointers to picotool metadata such as the program
name and version. Only picotool reads it.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.rodata&lt;/code&gt; holds constant data: string literals, immutable statics, lookup
tables the compiler generates, and formatting strings when something prints.
It is never written, so it never needs RAM.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;.data&lt;/code&gt; load copy holds the starting values for &lt;code&gt;.data&lt;/code&gt;, packed after
&lt;code&gt;.rodata&lt;/code&gt;. The reset handler reads it exactly once.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.gnu.sgstubs&lt;/code&gt; holds TrustZone entry stubs, used only when firmware is split
into Secure and Non-secure halves. Blinky&#39;s is empty.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.end_block&lt;/code&gt; is a reserved spot after everything else, where a signature or
hash block can go for secure boot. Blinky leaves it empty.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Section by section, in RAM&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.data&lt;/code&gt; holds statics that can change and start with nonzero values. It costs
twice: flash for the starting values, RAM for the working copy.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.bss&lt;/code&gt; holds statics whose starting bytes are all zero. The reset handler
zeroes the region, so it costs RAM and no flash. The name comes from a 1950s
IBM 704 assembler directive, Block Started by Symbol.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.uninit&lt;/code&gt; holds statics you deliberately leave uninitialized. Reset neither
copies nor zeroes it, so its contents survive a soft reset. That suits crash
logs, and big buffers you fill anyway.&lt;/li&gt;
&lt;li&gt;The stack is not a section. It starts at &lt;code&gt;_stack_start&lt;/code&gt; (0x2008_0000) and
grows down toward &lt;code&gt;.uninit&lt;/code&gt;, holding local variables, saved registers, return
addresses, and the frame the CPU pushes when an interrupt fires. Nothing stops
an overflow by default; it silently overwrites your statics. Armv8-M&#39;s
stack-limit register, MSPLIM, can turn that into a fault, and Lesson 2 sets
it.&lt;/li&gt;
&lt;li&gt;There is no heap until you add one. cortex-m-rt marks where one could begin
with &lt;code&gt;__sheap&lt;/code&gt;, and Lesson 9 adds a heap for &lt;code&gt;Arc&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The two 4 KB banks at 0x2008_0000 and 0x2008_1000 sit outside the &lt;code&gt;RAM&lt;/code&gt;
region, unused for now. Lesson 9 gives each core its own stack there.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Where your Rust ends up&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Rust item&lt;/th&gt;&lt;th&gt;Section&lt;/th&gt;&lt;th&gt;Why&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Any &lt;code&gt;fn&lt;/code&gt;, method, closure or generic instance&lt;/td&gt;&lt;td&gt;&lt;code&gt;.text&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Code runs in place from flash&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&quot;hello&quot;&lt;/code&gt;, &lt;code&gt;b&quot;bytes&quot;&lt;/code&gt;, &lt;code&gt;&amp;amp;[1, 2, 3]&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;.rodata&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Constant data, never written&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;static LUT: [u8; 256] = [...]&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;.rodata&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Immutable static, so no RAM needed&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;const MAX: u32 = 100&lt;/code&gt;&lt;/td&gt;&lt;td&gt;none&lt;/td&gt;&lt;td&gt;A value, inlined wherever it is used&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;static N: AtomicU32 = AtomicU32::new(7)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;.data&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Can change, starts nonzero&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;static N: AtomicU32 = AtomicU32::new(0)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;.bss&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Can change, starts as zero bytes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;static mut BUF: [u8; 4096] = [0; 4096]&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;.bss&lt;/code&gt;&lt;/td&gt;&lt;td&gt;4 KB of RAM, no flash&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;let x = ...&lt;/code&gt; inside a function&lt;/td&gt;&lt;td&gt;registers or stack&lt;/td&gt;&lt;td&gt;Lives only while the function runs&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;A &lt;code&gt;MaybeUninit&lt;/code&gt; static with &lt;code&gt;#[unsafe(link_section = &quot;.uninit.LOG&quot;)]&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;.uninit&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Untouched at reset&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The rule: a static lands in RAM only if it can change, through &lt;code&gt;static mut&lt;/code&gt; or
interior mutability (atomics, &lt;code&gt;Cell&lt;/code&gt;, &lt;code&gt;UnsafeCell&lt;/code&gt;). Its starting bytes then
decide between &lt;code&gt;.data&lt;/code&gt; and &lt;code&gt;.bss&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Try it: move a static between sections.&lt;/strong&gt; Add these lines to blinky, rebuild,
and compare &lt;code&gt;cargo size --release --bin blinky -- -A&lt;/code&gt; before and after.&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;rust&quot;&gt;// Near the top of src/bin/blinky.rs
static mut BIG: [u8; 8192] = [0; 8192];

// Inside main, before the loop, so the linker keeps BIG
unsafe { core::ptr::write_volatile((&amp;amp;raw mut BIG).cast::&amp;lt;u8&amp;gt;(), 1) };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;.bss&lt;/code&gt; grows by 8192 bytes and flash stays the same. Now change the initializer
to &lt;code&gt;[1; 8192]&lt;/code&gt;: &lt;code&gt;.bss&lt;/code&gt; shrinks back, while &lt;code&gt;.data&lt;/code&gt; and the flash image each grow
by 8192.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The symbols that connect layout to code.&lt;/strong&gt; cortex-m-rt&#39;s linker script defines
symbols at the boundaries it draws. They are addresses with no storage behind
them, and the reset handler reads them to know what to copy and what to clear.&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Symbol&lt;/th&gt;&lt;th&gt;Marks&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;__sidata&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Start of the initial values for &lt;code&gt;.data&lt;/code&gt; in flash (its LMA)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;__sdata&lt;/code&gt;, &lt;code&gt;__edata&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Start and end of &lt;code&gt;.data&lt;/code&gt; in RAM&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;__sbss&lt;/code&gt;, &lt;code&gt;__ebss&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Start and end of &lt;code&gt;.bss&lt;/code&gt; in RAM&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;_stack_start&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Initial stack pointer, at the top of &lt;code&gt;RAM&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;cargo nm --release --bin blinky \
  | grep -E &amp;#39;__s(i)?data|__edata|__[se]bss|_stack_start&amp;#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;__sidata&lt;/code&gt; is where the starting values for &lt;code&gt;.data&lt;/code&gt; begin in flash, right after
&lt;code&gt;.rodata&lt;/code&gt;. In stock blinky &lt;code&gt;.data&lt;/code&gt; is empty, so &lt;code&gt;__sidata&lt;/code&gt; equals &lt;code&gt;__erodata&lt;/code&gt;
and the program headers have no &lt;code&gt;.data&lt;/code&gt; segment to compare with. Run the
experiment above with &lt;code&gt;[1; 8192]&lt;/code&gt; and a new LOAD segment appears with VirtAddr
0x2000_0000 and PhysAddr equal to &lt;code&gt;__sidata&lt;/code&gt;. In Lesson 2, your own reset
handler copies from &lt;code&gt;__sidata&lt;/code&gt; into &lt;code&gt;__sdata..__edata&lt;/code&gt;, zeroes &lt;code&gt;__sbss..__ebss&lt;/code&gt;,
then calls &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Run these from &lt;code&gt;rp-hal/rp235x-hal-examples&lt;/code&gt;, so its &lt;code&gt;.cargo/config.toml&lt;/code&gt;
applies:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;# Sections with sizes and addresses: 0x1000_xxxx is flash, 0x2000_xxxx is RAM
cargo size --release --bin blinky -- -A

# Section headers: VMA is where a section runs, LMA is where it is stored
# --show-lma adds the load address column; LLVM&amp;#39;s objdump hides it by default
cargo objdump --release --bin blinky -- -h --show-lma

# Raw bytes of the vector table and the boot block
cargo objdump --release --bin blinky -- -s -j .vector_table
cargo objdump --release --bin blinky -- -s -j .start_block

# The 15 biggest symbols, and where key ones landed
cargo nm --release --bin blinky -- --print-size --size-sort | tail -n 15
cargo nm --release --bin blinky | grep -E &amp;#39;IMAGE_DEF|Reset|main&amp;#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What to look for&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In the &lt;code&gt;-h --show-lma&lt;/code&gt; output, &lt;code&gt;.data&lt;/code&gt; is the one section whose VMA (RAM)
differs from its LMA (flash). With an empty &lt;code&gt;.data&lt;/code&gt;, the difference is still
there but covers zero bytes.&lt;/li&gt;
&lt;li&gt;The vector table&#39;s first word is the initial stack pointer. Expect 0x20080000,
the end of the 512 KB &lt;code&gt;RAM&lt;/code&gt; region in &lt;code&gt;memory.x&lt;/code&gt;. objdump prints bytes in
memory order, so it shows as &lt;code&gt;00000820&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The second word is the reset handler&#39;s address, and it is odd. Bit 0 marks
Thumb code.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.start_block&lt;/code&gt; holds 40 bytes. The first 20 are the IMAGE_DEF, from
&lt;code&gt;d3deffff&lt;/code&gt; to &lt;code&gt;793512ab&lt;/code&gt;: the block&#39;s start and end markers, stored
little-endian. Lesson 1 decodes the 12 bytes between them. The next 20, from
&lt;code&gt;f2eb8871&lt;/code&gt; to &lt;code&gt;90a31ae7&lt;/code&gt;, are picotool&#39;s binary-info header. They hold the
addresses of &lt;code&gt;__bi_entries_start&lt;/code&gt;, &lt;code&gt;__bi_entries_end&lt;/code&gt; and a mapping table.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.text&lt;/code&gt; starts right after &lt;code&gt;.start_block&lt;/code&gt;, because &lt;code&gt;memory.x&lt;/code&gt; sets &lt;code&gt;_stext&lt;/code&gt;
there.&lt;/li&gt;
&lt;li&gt;You may see more than one symbol containing &lt;code&gt;main&lt;/code&gt;. Lesson 2 explains why.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;from-elf-to-flash-image&quot;&gt;From ELF to flash image&lt;/h3&gt;
&lt;p&gt;Flash holds a plain image, not the ELF. objcopy extracts exactly those bytes, so
you can compare the two directly:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;cargo objcopy --release --bin blinky -- -O binary blinky.bin
ls -l blinky.bin
find .. -path &amp;#39;*/thumbv8m.main-none-eabihf/release/blinky&amp;#39; -exec ls -l {} \;
xxd blinky.bin | head -n 2      # or: hexdump -C blinky.bin | head -n 2
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;.bin&lt;/code&gt; is small, around 8 KB. The ELF is many times larger, and almost all
of the difference is debug info.&lt;/li&gt;
&lt;li&gt;Byte 0 of &lt;code&gt;blinky.bin&lt;/code&gt; is address 0x1000_0000. Its first 8 bytes are the
stack pointer and reset vector you read back in the next section.&lt;/li&gt;
&lt;li&gt;This is everything the boot ROM ever sees: no headers and no names, just bytes
at addresses. IMAGE_DEF is how it recognizes them as a program.&lt;/li&gt;
&lt;li&gt;UF2, the format behind BOOTSEL drag-and-drop, carries the same bytes in
256-byte chunks, each tagged with its target address.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; add up the FileSiz of every LOAD segment whose PhysAddr is in flash,
then compare the total with the size of &lt;code&gt;blinky.bin&lt;/code&gt;. Explain any difference.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Coming in Lesson 4:&lt;/strong&gt; defmt adds a &lt;code&gt;.defmt&lt;/code&gt; section without the A flag. Every
log message&#39;s format string lives there, in the ELF on your PC. The chip sends
only a small index, and probe-rs looks the string up.&lt;/p&gt;
&lt;h2 id=&quot;read-it-back-from-the-chip&quot;&gt;Read it back from the chip&lt;/h2&gt;
&lt;p&gt;The bytes objdump showed now sit in flash at 0x1000_0000. Reading them back
closes the loop between the file on your disk and the silicon.&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;# Vector table: initial stack pointer, then the reset vector
probe-rs read --chip RP235x b32 0x10000000 2

# The boot block, at the IMAGE_DEF address cargo nm printed (0x10000110)
probe-rs read --chip RP235x b32 0x10000110 5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;probe-rs prints whole 32-bit words, so the stack pointer reads &lt;code&gt;20080000&lt;/code&gt; here
rather than objdump&#39;s &lt;code&gt;00000820&lt;/code&gt;. Same bytes, different presentation. The reset
vector is an odd address just above the vector table, such as &lt;code&gt;10000139&lt;/code&gt;; the
exact value depends on your rp-hal version. The boot block should read
&lt;code&gt;ffffded3&lt;/code&gt; first and &lt;code&gt;ab123579&lt;/code&gt; last.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bonus: watch a peripheral tick.&lt;/strong&gt; TIMER0 starts at 0x400b0000. Find the offset
of its raw low-word register, TIMERAWL, in the datasheet&#39;s TIMER register list,
and add it to the base address. The offset alone points into the boot ROM, which
never changes. Read the full address twice, a few seconds apart, and estimate
the tick rate from the difference. That is your first peripheral register read;
Lesson 3 starts writing them.&lt;/p&gt;
&lt;h2 id=&quot;set-up-the-course-workspace&quot;&gt;Set up the course workspace&lt;/h2&gt;
&lt;p&gt;Create the workspace now so Lesson 1 drops straight into it. Keep it separate
from your rp-hal clone.&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;txt&quot;&gt;rp2350-course/
├── .cargo/config.toml
├── .helix/languages.toml
├── Cargo.toml
├── rust-toolchain.toml
└── lessons/            (Lesson 1 adds the first crate)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code data-lang=&quot;toml&quot;&gt;# Cargo.toml
[workspace]
resolver = &amp;quot;3&amp;quot;
members = []

[workspace.package]
edition = &amp;quot;2024&amp;quot;

[profile.release]
debug = 2   # full symbols in the ELF for probe-rs; none of it reaches flash
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code data-lang=&quot;toml&quot;&gt;# .cargo/config.toml
[build]
target = &amp;quot;thumbv8m.main-none-eabihf&amp;quot;

[target.thumbv8m.main-none-eabihf]
rustflags = [&amp;quot;-C&amp;quot;, &amp;quot;target-cpu=cortex-m33&amp;quot;]
runner = &amp;quot;probe-rs run --chip RP235x&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code data-lang=&quot;toml&quot;&gt;# rust-toolchain.toml
[toolchain]
channel = &amp;quot;stable&amp;quot;
targets = [&amp;quot;thumbv8m.main-none-eabihf&amp;quot;]
components = [&amp;quot;llvm-tools&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code data-lang=&quot;toml&quot;&gt;# .helix/languages.toml
# Workspace-local Helix config, merged over your global languages.toml
[language-server.rust-analyzer.config]
check.allTargets = false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Three choices worth noticing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No linker flags at the root. Lesson 1&#39;s crate links with a script you write,
and Lesson 4 switches to cortex-m-rt&#39;s, so each crate&#39;s &lt;code&gt;build.rs&lt;/code&gt; passes its
own.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;target-cpu=cortex-m33&lt;/code&gt; lets the compiler use the M33&#39;s DSP instructions and
tune for its pipeline.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;check.allTargets = false&lt;/code&gt; stops rust-analyzer checking test targets, which
need &lt;code&gt;std&lt;/code&gt; and can&#39;t build for this target. rust-analyzer already picks up the
target from &lt;code&gt;.cargo/config.toml&lt;/code&gt;, so Helix needs nothing else. If diagnostics
look wrong, run &lt;code&gt;hx --health rust&lt;/code&gt; from inside &lt;code&gt;rp2350-course/&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Run &lt;code&gt;git init&lt;/code&gt; too. You will want to diff your from-scratch code against
rp-hal&#39;s later.&lt;/p&gt;
&lt;h2 id=&quot;exercises-and-self-check&quot;&gt;Exercises and self-check&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Exercises&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Change the blink timing in &lt;code&gt;src/bin/blinky.rs&lt;/code&gt;, for example 100 ms on and 900
ms off, and reflash. Note how much faster the second build is.&lt;/li&gt;
&lt;li&gt;Build blinky in debug and in release, and compare their &lt;code&gt;.text&lt;/code&gt; sizes from
&lt;code&gt;cargo size -- -A&lt;/code&gt;. Explain the gap, and why debug info isn&#39;t the cause.&lt;/li&gt;
&lt;li&gt;List the five largest functions with &lt;code&gt;cargo nm&lt;/code&gt;. Guess what each does from
its name.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;picotool info -a target/thumbv8m.main-none-eabihf/release/blinky -t elf&lt;/code&gt;. Match each line of output to the source that produced it.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;probe-rs read&lt;/code&gt; a few times while watching the LED. Does it keep its
rhythm, and what does that suggest about how the probe reaches memory?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Self-check&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Why does &lt;code&gt;.data&lt;/code&gt; take space in both flash and RAM, while &lt;code&gt;.bss&lt;/code&gt; takes RAM
only?&lt;/li&gt;
&lt;li&gt;Why is the initial stack pointer 0x20080000, and which way does the stack
grow from there?&lt;/li&gt;
&lt;li&gt;Why is the reset vector an odd number?&lt;/li&gt;
&lt;li&gt;&lt;code&gt;memory.x&lt;/code&gt; puts &lt;code&gt;.start_block&lt;/code&gt; right after the vector table. What would
happen if it landed after 10 KB of &lt;code&gt;.text&lt;/code&gt; instead?&lt;/li&gt;
&lt;li&gt;Which pieces in the magic inventory run before &lt;code&gt;main&lt;/code&gt; is called?&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;p&gt;Next: &lt;a href=&quot;https://k4s.xyz/blog/rp2350/lesson-1/&quot;&gt;Lesson 1: Anatomy of an RP2350 Image&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Part of &lt;a href=&quot;https://k4s.xyz/blog/rp2350/&quot;&gt;Embedded Rust on the RP2350&lt;/a&gt;.&lt;/p&gt;
</description>
      </item>
      <item>
          <title>Sunlight to Server</title>
          <pubDate>Mon, 27 Nov 2023 00:00:00 +0000</pubDate>
          <author>kevstewa@protonmail.com (Kevin Stewart)</author>
          <link>https://k4s.xyz/blog/from-sunlight-to-server/</link>
          <guid>https://k4s.xyz/blog/from-sunlight-to-server/</guid>
          <description xml:base="https://k4s.xyz/blog/from-sunlight-to-server/">&lt;h2 id=&quot;charting-my-course-for-a-low-impact-web-presence&quot;&gt;Charting my Course for a Low-Impact Web Presence&lt;/h2&gt;
&lt;p&gt;With the hectic clamor of summer and fall coming to a close, it seems I can once again dream up projects that might actually come to fruition.
I&#39;ve been tossing around an idea for a while now, heavily inspired by &lt;a rel=&quot;external&quot; href=&quot;https://solar.lowtechmagazine.com/&quot;&gt;Low&amp;lt;-Tech Magazine&lt;/a&gt; and their solar-powered website, of self-hosting my website locally on a small Kubernetes cluster.&lt;/p&gt;
&lt;p&gt;The major motivation for this project comes from a desire to reduce my impact and explore how we can exceed the value provided by turn-key solutions and profit motivated corporations.
As we barrel down the tracks towards climate catastrophe and are ultimately consumed by the Goliath that is capitalism, I can hold my head above water even if for a minute and claim that this is one of my small efforts towards sustainability.&lt;/p&gt;
&lt;h2 id=&quot;hardware&quot;&gt;Hardware&lt;/h2&gt;
&lt;h3 id=&quot;compute&quot;&gt;Compute&lt;/h3&gt;
&lt;p&gt;The core of the platform will be a set of four &lt;a rel=&quot;external&quot; href=&quot;https://ameridroid.com/products/odroid-mc1-solo&quot;&gt;Odroid-MC1&lt;/a&gt; devices.
These were originally intended for the purpose of small, personal clusters.
I have successfully used &lt;a rel=&quot;external&quot; href=&quot;https://k3s.io/&quot;&gt;K3s&lt;/a&gt; on top of &lt;a rel=&quot;external&quot; href=&quot;https://www.armbian.com/&quot;&gt;Armbian&lt;/a&gt; for learning the basics of Kubernetes by running &lt;a rel=&quot;external&quot; href=&quot;https://github.com/cloudflare/cloudflared&quot;&gt;cloudflared&lt;/a&gt; for DoH to back my Pi-hole.&lt;/p&gt;
&lt;h3 id=&quot;power&quot;&gt;Power&lt;/h3&gt;
&lt;p&gt;I have experience with more complex solar setups but for the sake of keeping things simple and cheap, my eye falls on a &lt;a rel=&quot;external&quot; href=&quot;https://www.renogy.com/wanderer-10a-pwm-charge-controller/&quot;&gt;Renogy 10A charge controller&lt;/a&gt;.
This is plenty of allowance for even a relatively large panel.
I have an old ~20Ah SLA batter kicking around somewhere in the dungeon I can resurrect as well.
Together these should get me going.&lt;/p&gt;
&lt;h2 id=&quot;software&quot;&gt;Software&lt;/h2&gt;
&lt;h3 id=&quot;os&quot;&gt;OS&lt;/h3&gt;
&lt;p&gt;The current choice is &lt;a rel=&quot;external&quot; href=&quot;https://www.armbian.com/&quot;&gt;Armbian&lt;/a&gt; as there is excellent support for these boards with relatively recent kernels.&lt;/p&gt;
&lt;h3 id=&quot;web-server&quot;&gt;Web Server&lt;/h3&gt;
&lt;p&gt;I&#39;m leaning towards &lt;a rel=&quot;external&quot; href=&quot;https://nginx.org/en/&quot;&gt;Nginx&lt;/a&gt; since I&#39;m not super familiar and would like to learn.
&lt;a rel=&quot;external&quot; href=&quot;https://httpd.apache.org/&quot;&gt;Apache&lt;/a&gt; is my comfort zone and I&#39;ll come back to that if necessary or I get too lazy.
If I remember correctly, these boards are &lt;code&gt;arm32v6&lt;/code&gt; and both webservers have container images for this architecture.&lt;/p&gt;
&lt;h3 id=&quot;kubernetes&quot;&gt;Kubernetes&lt;/h3&gt;
&lt;p&gt;Here I&#39;ll be leaning on the wonderful &lt;a rel=&quot;external&quot; href=&quot;https://k3s.io/&quot;&gt;K3s&lt;/a&gt; project for these tiny systems.
This is should be obvious.&lt;/p&gt;
&lt;h4 id=&quot;cluster-datastore&quot;&gt;Cluster Datastore&lt;/h4&gt;
&lt;p&gt;I&#39;ll probably stick with &lt;a rel=&quot;external&quot; href=&quot;https://etcd.io/&quot;&gt;etcd&lt;/a&gt; for now but have used a &lt;a rel=&quot;external&quot; href=&quot;https://www.postgresql.org/&quot;&gt;Postgres&lt;/a&gt; database previously.
This will depend on whether I replace my aging NAS disks.&lt;/p&gt;
&lt;h3 id=&quot;text-editor&quot;&gt;Text Editor&lt;/h3&gt;
&lt;p&gt;Vim.&lt;/p&gt;
&lt;h2 id=&quot;closing-thoughts&quot;&gt;Closing Thoughts&lt;/h2&gt;
&lt;p&gt;Yes, this is needlessly complex for my website and traffic.
What can I say? I like to learn and I&#39;ve got the devices hanging around collecting dust.&lt;/p&gt;
&lt;p&gt;This is by no means an exhaustive exploration of my idea and it will flesh out over time.
I&#39;m slow at executing projects so this likely will be a long time in the works with plenty of time for updates and changes.&lt;/p&gt;
</description>
      </item>
      <item>
          <title>OpenBSD: Installing WiFi Firmware for a MacBook Pro</title>
          <pubDate>Sat, 14 Jan 2023 00:00:00 +0000</pubDate>
          <author>kevstewa@protonmail.com (Kevin Stewart)</author>
          <link>https://k4s.xyz/blog/openbsd-installing-wifi-firmware-macbook/</link>
          <guid>https://k4s.xyz/blog/openbsd-installing-wifi-firmware-macbook/</guid>
          <description xml:base="https://k4s.xyz/blog/openbsd-installing-wifi-firmware-macbook/">&lt;p&gt;After coming into a possession of an Early 2015 MacBook Pro, it turned out to be a perfect opportunity to install OpenBSD. This crystallized as a result of having some older, but still perfectly performant hardware, along with my recent run-ins with principles related to &lt;a rel=&quot;external&quot; href=&quot;http://viznut.fi/texts-en/permacomputing.html&quot;&gt;&lt;em&gt;permacomputing&lt;/em&gt;&lt;/a&gt;. The latter deserves a future post all on its own, but in short the ideas center around resilience, sustainability, and our relationships with hardware and software.&lt;/p&gt;
&lt;p&gt;This particular device has an Intel i5-5257U CPU, 8GB RAM, a 120GB SSD, and a Retina display. Installing OpenBSD 7.2 was rather uneventful and less involved than my experience with FreeBSD. Due to licensing, not all firmware is able to be shipped with the base install so I took the route of post-install configuration.&lt;/p&gt;
&lt;p&gt;Not knowing exactly what I needed, save for the wireless device, I downloaded all files located at &lt;a rel=&quot;external&quot; href=&quot;https://firmware.openbsd.org&quot;&gt;firmware.openbsd.org&lt;/a&gt; for &lt;a rel=&quot;external&quot; href=&quot;https://firmware.openbsd.org/firmware/7.2/&quot;&gt;7.2&lt;/a&gt;. Download firmware specific to your release.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ wget -r --no-parent https://firmware.openbsd.org/firmware/7.2/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I moved these files to a USB flash drive, mounted the device on the MacBook, and ran the &lt;code&gt;fw_update&lt;/code&gt; utility.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# doas mkdir /mnt/usb_drive
# doas mount /dev/sd2i /mnt/usb_drive
# cd /mnt/usb_drive
# doas fw_update -p .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should see output similar to that listed below:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fw_update: added bwfm,intel,inteldrm,vmm; updated none; kept none
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And that&#39;s really all there is to it. Be sure to follow up with an appropriately configured /etc/hostname.&lt;em&gt;xxx&lt;/em&gt; file for the device, in this case bwfm0, and reboot.&lt;/p&gt;
</description>
      </item>
      <item>
          <title>Retrocausation</title>
          <pubDate>Tue, 13 Dec 2022 00:00:00 +0000</pubDate>
          <author>kevstewa@protonmail.com (Kevin Stewart)</author>
          <link>https://k4s.xyz/blog/retrocausation/</link>
          <guid>https://k4s.xyz/blog/retrocausation/</guid>
          <description xml:base="https://k4s.xyz/blog/retrocausation/">&lt;p&gt;A quick note--I&#39;ve recently committed to restructuring the sort of content I post here. While technology is at the forefront of my interests, prepare for rambling on the many others.&lt;/p&gt;
&lt;p&gt;I&#39;m re-reading &lt;a rel=&quot;external&quot; href=&quot;https://bookshop.org/p/books/time-loops-precognition-retrocausation-and-the-unconscious-eric-wargo/7740460&quot;&gt;Time Loops&lt;/a&gt; by Eric Wargo after a recent and particularly reinvigorating syncronicity. I would recommend this book to anyone and I&#39;m excited to explore his relatively new book that I just found out about, &lt;a rel=&quot;external&quot; href=&quot;https://bookshop.org/p/books/precognitive-dreamwork-and-the-long-self-interpreting-messages-from-your-future-eric-wargo/14949372?ean=9781644112694&quot;&gt;Precognitive Dreamwork and the Long Self&lt;/a&gt;. He also has an interesting blog at &lt;a rel=&quot;external&quot; href=&quot;http://www.thenightshirt.com/&quot;&gt;thenightshirt.com&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This had seeded interest in &lt;a rel=&quot;external&quot; href=&quot;https://en.wikipedia.org/wiki/Philip_K._Dick#Paranormal_experiences&quot;&gt;Philip K. Dick&lt;/a&gt; on the first go and I&#39;m excited to continue down that path. I&#39;d also like to read &lt;a rel=&quot;external&quot; href=&quot;https://en.wikipedia.org/wiki/Story_of_Your_Life&quot;&gt;Story of Your Life&lt;/a&gt; by Ted Chiang. This was inspiration for the film &lt;a rel=&quot;external&quot; href=&quot;https://en.wikipedia.org/wiki/Arrival_(film)&quot;&gt;Arrival&lt;/a&gt;, one of my top 5 movies.&lt;/p&gt;
&lt;p&gt;Anyway, I&#39;ve had a refocus on precognition with the aforementioned synchronicity and several others. They seem to come in waves; I wonder if it is just awareness instead of any true increase. Regardless, they will certainly increase as I make my way through this book, just as it happened last time. Maybe I should start sharing them here...&lt;/p&gt;
</description>
      </item>
      <item>
          <title>Website Revival</title>
          <pubDate>Sun, 11 Dec 2022 00:00:00 +0000</pubDate>
          <author>kevstewa@protonmail.com (Kevin Stewart)</author>
          <link>https://k4s.xyz/blog/website-revival/</link>
          <guid>https://k4s.xyz/blog/website-revival/</guid>
          <description xml:base="https://k4s.xyz/blog/website-revival/">&lt;p&gt;When I began working on this website about a year and a half ago, it was biased by a job hunt. At the time, I wasn&#39;t satisfied with where I was at in my career. To advance, it was ideal to have a place to demonstrate some level of skill. As a result, I treated the content very mechanically and the authenticity I am now hunting for suffered.&lt;/p&gt;
&lt;p&gt;While I grew up with the internet in a state more or less functionally similar to what we have today, it was amorphous and I lacked context for what it represented and how it was structured; a story as old as time, not understanding what you have until it&#39;s gone. I want reduction in centralization and walled gardens. I want less &lt;em&gt;dependencies&lt;/em&gt; and more &lt;em&gt;efficiency&lt;/em&gt;. Most of all, I want to cultivate exploration in this intangible landscape.&lt;/p&gt;
&lt;p&gt;I&#39;m now reapproaching the content, structure, and mindset that goes in to having my personal corner of the World Wide Web. Already magnetically attracted to minimalism and simplicity in the analog, carrying these practices to my digital life has been a long-standing target. Along with the &lt;em&gt;feel&lt;/em&gt; of my personal site never sitting quite right, I&#39;ve been further driven by recent articles such as &lt;a rel=&quot;external&quot; href=&quot;https://bt.ht/dump/&quot;&gt;this one by Bradley Taut&lt;/a&gt; or &lt;a rel=&quot;external&quot; href=&quot;https://www.taniarascia.com/digital-gardening/&quot;&gt;another by Tania Rascia&lt;/a&gt; to focus less on fitting content within an imaginary box.&lt;/p&gt;
&lt;p&gt;So here it is--a rebuild from the ground up. I&#39;ve started from zero. No themes, no pre-built CSS, nada. There is, of course, copy-pasted snippets and inspriation from many sources, but this incantation of symbols is 100% unique. By just pressing buttons in the right order, I&#39;ve finally got something I can call my own--a place for me to treat mindfully knowing how it works from the bottom up, a place without pretension or bias or rigor. It can just exist as it is. Welcome...&lt;/p&gt;
</description>
      </item>
      <item>
          <title>Automount Home Directories on CentOS 8 With FreeIPA</title>
          <pubDate>Mon, 23 Aug 2021 00:00:00 +0000</pubDate>
          <author>kevstewa@protonmail.com (Kevin Stewart)</author>
          <link>https://k4s.xyz/blog/automount-home-directories-with-freeipa/</link>
          <guid>https://k4s.xyz/blog/automount-home-directories-with-freeipa/</guid>
          <description xml:base="https://k4s.xyz/blog/automount-home-directories-with-freeipa/">&lt;p&gt;FreeIPA is an identity management solution combining LDAP, Kerberos, NTP, and DNS, among others. I have deployed this in production for our small business to manage users across several satellite retail and production locations. Currently I have no shared home directory set up so when a user moves to a different workstation, they must reconfigure their GNOME and Firefox settings. This has minimal impact right now as users are usually using a single workstation to complete computer-related tasks, but in preparation for business growth, as well as catering to the unique users moving across multiple facilities, I will be testing out NFS-based automounting home directories.&lt;/p&gt;
&lt;p&gt;This guide expects you can configure your server securely and does not attempt to cover all possible configurations. Shortcuts are taken as this is a lab setup in a VLAN with limited ingress and egress access. I will utilize three VMs, all running CentOS 8 starting with a minimal install and additionally adding the standard and guest tools as via the graphical installer. Our lab environment is as follows:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;idm1.example.com as 10.0.51.150&lt;/li&gt;
&lt;li&gt;nfs.example.com as 10.0.51.151&lt;/li&gt;
&lt;li&gt;client.example.com as 10.0.51.152&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;idm1-example-com&quot;&gt;idm1.example.com&lt;/h2&gt;
&lt;p&gt;idm1 will be our identity management (idm) server running FreeIPA. This will be set up as a standalone service (no replicas) &lt;a rel=&quot;external&quot; href=&quot;https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/installing_identity_management/installing-an-ipa-server-with-integrated-dns_installing-identity-management&quot;&gt;with integrated DNS and with an integrated CA as the root CA&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;setup&quot;&gt;Setup&lt;/h3&gt;
&lt;p&gt;After logging in as root, update the server and install the necessary packages, first enabling the module containing the idm software:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# dnf module enable idm:DL1
# dnf distro-sync
# dnf module install idm:DL1/dns
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I chose idm:DL1/dns as I am installing with integrated DNS. Other options exist and you should familiarize yourself with them &lt;a rel=&quot;external&quot; href=&quot;https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/installing_identity_management/preparing-the-system-for-ipa-server-installation_installing-identity-management#packages-required-for-an-ipa-server_preparing-the-system-for-ipa-server-installation&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Next, I&#39;ll open the appropriate firewall ports using firewall-cmd. Service definitions found in /usr/lib/firewalld/services make this easy -- just add freeipa-4 and dns (the freeipa-ldap/s and related service files have been deprecated).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# firewall-cmd --permanent --add-service={freeipa-4,dns}
# firewall-cmd --reload
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should now see these services added:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# firewall-cmd --list-all
public (active)
  target: default
  icmp-block-inversion: no
  interfaces: ens18
  sources: 
  services: cockpit dhcpv6-client dns freeipa-4 ssh
  ports: 
  protocols: 
  masquerade: no
  forward-ports: 
  source-ports: 
  icmp-blocks: 
  rich rules:
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, add the hostname and IP information to the /etc/hosts file so our server can reference itself for proper domain setup:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# echo &amp;quot;10.0.51.150 idm1.example.com idm1&amp;quot; &amp;gt;&amp;gt; /etc/hosts
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;install&quot;&gt;Install&lt;/h3&gt;
&lt;p&gt;This part is relatively straightforward. I&#39;ll run the install command and, for the most part, accept the default values. Pay attention to the prompts as some defaults cause an abort or can uncover configuration mistakes, particularly with the assumed domain. Much of the output of the command below has been cut for brevity&#39;s sake:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ipa-server-install 

The log file for this installation can be found in /var/log/ipaserver-install.log
==============================================================================
This program will set up the IPA Server.
Version 4.9.2

Do you want to configure integrated DNS (BIND)? [no]: yes
Server host name [idm1.example.com]: 
Please confirm the domain name [example.com]: 
Please provide a realm name [EXAMPLE.COM]: 

Directory Manager password: 
Password (confirm): 

IPA admin password: 
Password (confirm): 

Do you want to configure DNS forwarders? [yes]:

Following DNS servers are configured in /etc/resolv.conf: 1.1.1.1, 1.0.0.1
Do you want to configure these servers as DNS forwarders? [yes]: 
Enter an IP address for a DNS forwarder, or press Enter to skip: 

Do you want to search for missing reverse zones? [yes]: 
Do you want to create reverse zone for IP 10.0.51.150 [yes]: 
Please specify the reverse zone name [51.0.10.in-addr.arpa.]: 

Do you want to configure chrony with NTP server or pool address? [no]: 

The IPA Master Server will be configured with:
Hostname:       idm1.example.com
IP address(es): 10.0.51.150
Domain name:    example.com
Realm name:     EXAMPLE.COM

The CA will be configured with:
Subject DN:   CN=Certificate Authority,O=EXAMPLE.COM
Subject base: O=EXAMPLE.COM
Chaining:     self-signed

BIND DNS server will be configured to serve IPA domain with:
Forwarders:       1.1.1.1, 1.0.0.1
Forward policy:   only
Reverse zone(s):  51.0.10.in-addr.arpa.

Continue to configure the system with these values? [no]: yes

...

==============================================================================
Setup complete

Next steps:
	1. You must make sure these network ports are open:
		TCP Ports:
		  * 80, 443: HTTP/HTTPS
		  * 389, 636: LDAP/LDAPS
		  * 88, 464: kerberos
		  * 53: bind
		UDP Ports:
		  * 88, 464: kerberos
		  * 53: bind
		  * 123: ntp

	2. You can now obtain a kerberos ticket using the command: &amp;#39;kinit admin&amp;#39;
	   This ticket will allow you to use the IPA tools (e.g., ipa user-add)
	   and the web user interface.

Be sure to back up the CA certificates stored in /root/cacert.p12
These files are required to create replicas. The password for these
files is the Directory Manager password
The ipa-server-install command was successful
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&#39;s it for the idm server. I&#39;ll use the ipa command line utilities for adding users and NFS service in the next part.&lt;/p&gt;
&lt;h2 id=&quot;nfs-example-com&quot;&gt;nfs.example.com&lt;/h2&gt;
&lt;p&gt;This server will be enrolled in our idm and provide our NFS server.&lt;/p&gt;
&lt;h3 id=&quot;setup-1&quot;&gt;Setup&lt;/h3&gt;
&lt;p&gt;Again, we&#39;re starting on a fresh minimal install as root. I&#39;ll update the server and make sure the necessary utilities are installed. No need for installing the idm:DL1 stream -- the ipa-client is available in the repositories.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# dnf update
# dnf install nfs-utils ipa-client
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I&#39;ll go ahead and add additional services allowed via firewalld:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# firewall-cmd --permanent --add-service={nfs,mountd,rpc-bind}
# firewall-cmd --reload
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output should confirm:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# firewall-cmd --list-all
public (active)
  target: default
  icmp-block-inversion: no
  interfaces: ens18
  sources: 
  services: cockpit dhcpv6-client mountd nfs rpc-bind ssh
  ports: 
  protocols: 
  masquerade: no
  forward-ports: 
  source-ports: 
  icmp-blocks: 
  rich rules: 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For the last setup task, I&#39;ll enroll this host with our previously created idm host. Make sure your nameserver in /etc/resolv.conf points to the idm host and verify your search domain is correct as well. Once again, pay attention to default values.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# cat /etc/resolv.conf
search example.com
nameserver 10.0.51.150

# ipa-client-install -p admin -W
This program will set up IPA client.
Version 4.9.2

Discovery was successful!
Do you want to configure chrony with NTP server or pool address? [no]: 
Client hostname: nfs.example.com
Realm: EXAMPLE.COM
DNS Domain: example.com
IPA Server: idm1.example.com
BaseDN: dc=x,dc=example,dc=net

Continue to configure the system with these values? [no]: yes
Synchronizing time
No SRV records of NTP servers found and no NTP server or pool address was provided.
Using default chrony configuration.
Attempting to sync time with chronyc.
Time synchronization was successful.
Password for admin@EXAMPLE.COM: 
Successfully retrieved CA cert
    Subject:     CN=Certificate Authority,O=EXAMPLE.COM
    Issuer:      CN=Certificate Authority,O=EXAMPLE.COM
    Valid From:  2021-08-23 21:00:55
    Valid Until: 2041-08-23 21:00:55

Enrolled in IPA realm EXAMPLE.COM
Created /etc/ipa/default.conf
Configured sudoers in /etc/authselect/user-nsswitch.conf
Configured /etc/sssd/sssd.conf
Configured /etc/krb5.conf for IPA realm EXAMPLE.COM
Systemwide CA database updated.
Hostname (nfs.example.com) does not have A/AAAA record.
Missing reverse record(s) for address(es): 10.0.51.151.
Adding SSH public key from /etc/ssh/ssh_host_ecdsa_key.pub
Adding SSH public key from /etc/ssh/ssh_host_ed25519_key.pub
Adding SSH public key from /etc/ssh/ssh_host_rsa_key.pub
SSSD enabled
Configured /etc/openldap/ldap.conf
Configured /etc/ssh/ssh_config
Configured /etc/ssh/sshd_config
Configuring example.com as NIS domain.
Client configuration complete.
The ipa-client-install command was successful
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;install-1&quot;&gt;Install&lt;/h3&gt;
&lt;p&gt;To start I will obtain a Kerberos ticket:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# kinit admin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next I&#39;ll create an NFS service entry:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ipa service-add nfs/nfs.example.com
---------------------------------------------------------------------------
Added service &amp;quot;nfs/nfs.example.com@EXAMPLE.COM&amp;quot;
---------------------------------------------------------------------------
  Principal name: nfs/nfs.example.com@EXAMPLE.COM
  Principal alias: nfs/nfs.example.com@EXAMPLE.COM
  Managed by: nfs.example.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Follow this with a keytab retrieval:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ipa-getkeytab -s idm1.example.com -p nfs/nfs.example.com -k /etc/krb5.keytab
Keytab successfully retrieved and stored in: /etc/krb5.keytab
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I&#39;ll verify that everything looks good using ipa service-show:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ipa service-show nfs/nfs.example.com
  Principal name: nfs/nfs.example.com@EXAMPLE.COM
  Principal alias: nfs/nfs.example.com@EXAMPLE.COM
  Keytab: True
  Managed by: nfs.example.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Configure the NFS settings:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ipa-client-automount
Searching for IPA server...
IPA server: DNS discovery
Location: default
Continue to configure the system with these values? [no]: yes
Configured /etc/idmapd.conf
Restarting sssd, waiting for it to become available.
Started autofs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We&#39;re getting close. Next I&#39;ll add the exports using the krb5p security setting. I&#39;m just going to export the home directory but you can use a different directory (or multiple directories) if you so desire.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# echo &amp;quot;/home  *(rw,sec=krb5:krb5i:krb5p)&amp;quot; &amp;gt; /etc/exports
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&#39;s enable and start NFS, and verify our directory exports:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# systemctl enable --now nfs-server.service
# export -rav
exporting *:/home
# showmount -e
Export list for nfs.example.com:
/home *
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, I am going to create the automount map:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ipa automountmap-add-indirect default auto.home --mount=/home
-------------------------------
Added automount map &amp;quot;auto.home&amp;quot;
-------------------------------
  Map: auto.home
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;...and key:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ipa automountkey-add default auto.home --key &amp;quot;*&amp;quot; --info &amp;quot;nfs.example.com:/home/&amp;amp;&amp;quot;
-----------------------
Added automount key &amp;quot;*&amp;quot;
-----------------------
  Key: *
  Mount information: nfs.example.com:/home/&amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we can create a user and their home directory. As we are exporting home, we won&#39;t need to move anything around.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ipa user-add --first McCoy --last Pauley --password --shell=/bin/bash
User login [mpauley]: 
Password: 
Enter Password again to verify: 
--------------------
Added user &amp;quot;mpauley&amp;quot;
--------------------
  User login: mpauley
  First name: McCoy
  Last name: Pauley
  Full name: McCoy Pauley
  Display name: McCoy Pauley
  Initials: MP
  Home directory: /home/mpauley
  GECOS: McCoy Pauley
  Login shell: /bin/bash
  Principal name: mpauley@EXAMPLE.COM
  Principal alias: mpauley@EXAMPLE.COM
  User password expiration: 20210823214024Z
  Email address: mpauley@example.com
  UID: 873600003
  GID: 873600003
  Password: True
  Member of groups: ipausers
  Kerberos keys available: True

# mkhomedir_helper mpauley 077
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I&#39;m going to create a text file to verify our NFS automounted home on our client next:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# echo &amp;quot;It works!&amp;quot; &amp;gt; /home/mpauley/test-file.txt
# chown mpauley: /home/mpauley/test-file.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;client-example-com&quot;&gt;client.example.com&lt;/h2&gt;
&lt;p&gt;Our client will have no services installed but will be enrolled in the idm server to see if our effort has paid off. We should see that our test file exists in the user&#39;s home directory.&lt;/p&gt;
&lt;h3 id=&quot;setup-2&quot;&gt;Setup&lt;/h3&gt;
&lt;p&gt;I won&#39;t repeat the full steps here. Refer back to the previous sections if you need guidance.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Update packages.&lt;/li&gt;
&lt;li&gt;Install ipa-client.&lt;/li&gt;
&lt;li&gt;Ensure the resolv.conf points to the idm server.&lt;/li&gt;
&lt;li&gt;Run ipa-client-install.&lt;/li&gt;
&lt;li&gt;Run ipa-client-automount.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I briefly tested this with GNOME as well, receiving subsequent AVC denials. In my case, this easily resolved by enabling an SELinux boolean*:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# sebool -P use_nfs_home_dirs 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;* Thanks to &lt;a rel=&quot;external&quot; href=&quot;https://www.reddit.com/r/linuxadmin/comments/papu3l/how_to_create_automounting_home_directories_using/&quot;&gt;/u/Smithore&lt;/a&gt; for the reminder!&lt;/p&gt;
&lt;h3 id=&quot;the-big-test&quot;&gt;The Big Test&lt;/h3&gt;
&lt;p&gt;Once these steps are complete, log out of the server and log in as our newly created user, mpauley:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ssh mpauley@client.example.com

(mpauley@10.0.51.152) Password:

$ ls -lh
total 4.0K
-rw-r--r--. 1 mpauley mpauley 43 Aug 23 19:43 test-file.txt
$ cat test-file.txt
It works!
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;While this example was merely a proof-of-concept for my further planning and was deliberately quite simple, we can of course dive in to the rabbit-hole of complexity these tools provide. I look forward to rolling this out, but perhaps I&#39;ll wait until I can beg for a few more bits through tiny pipes at a couple of sites. ¯\_(ツ)_/¯&lt;/p&gt;
</description>
      </item>
      <item>
          <title>Raspberry Pi 4 and FreeBSD 13 - The Unbootening</title>
          <pubDate>Mon, 16 Aug 2021 00:00:00 +0000</pubDate>
          <author>kevstewa@protonmail.com (Kevin Stewart)</author>
          <link>https://k4s.xyz/blog/freebsd-13-on-raspberry-pi-4/</link>
          <guid>https://k4s.xyz/blog/freebsd-13-on-raspberry-pi-4/</guid>
          <description xml:base="https://k4s.xyz/blog/freebsd-13-on-raspberry-pi-4/">&lt;p&gt;While it&#39;s relatively straight forward to install &lt;a rel=&quot;external&quot; href=&quot;https://download.freebsd.org/ftp/releases/arm64/aarch64/ISO-IMAGES/13.0/&quot;&gt;FreeBSD 13.0-RELEASE&lt;/a&gt; on the Raspberry Pi 4, I ran in to a particular issue that warrants additional visibility.&lt;/p&gt;
&lt;p&gt;A simple dd gets our extracted image on the SD card:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ sudo dd if=FreeBSD-13.0-RELEASE-arm64-aarch64-RPI.img of=/dev/{OUTPUT DEVICE} bs=4M status=progress
$ sync
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After inserting the SD card and attempting to boot, however, is where the issue arose. I enabled the UART in the U-Boot config.txt within the MSDOSBOOT partition and hooked up a serial cable.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[all]
arm_64bit=1
dtparam=audio=on,i2c_arm=on,spi=on
dtoverlay=mmc
dtoverlay=disable-bt
device_tree_address=0x4000
kernel=u-boot.bin
enable_uart=1

...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After getting some output from U-Boot in the serial console, it appears that the bootloader is not recognizing the partition table (or a related issue that causes this error).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...

Hit any key to stop autoboot: 0 
switch to partitions #0, OK 
mmc0 is current device 
** No partition table - mmc 0**

Device 0: unknown device 
ethernet@7d580000 Waiting for PHY auto negotiation to complete...

...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I wasn&#39;t finding what I needed through my go-to search engines but a hop, skip, and a jump over to the &lt;a rel=&quot;external&quot; href=&quot;https://forums.freebsd.org/&quot;&gt;FreeBSD Forums&lt;/a&gt; quickly yielded &lt;a rel=&quot;external&quot; href=&quot;https://forums.freebsd.org/threads/stuck-in-u-boot-during-freebsd-13-install-on-raspberry-pi-400.79941/&quot;&gt;this post&lt;/a&gt; which helpfully pointed to &lt;a rel=&quot;external&quot; href=&quot;https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=255080&quot;&gt;Bug 255080&lt;/a&gt; solving the issue.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=255080#c15&quot;&gt;Comment #15&lt;/a&gt; provides a &lt;a rel=&quot;external&quot; href=&quot;https://sourceforge.net/projects/fbsd-u-boot-2021-04-apr18-2021/files/u-boot.bin/download&quot;&gt;link&lt;/a&gt; to the 2021.04 build of the U-Boot binary. Overwrite the u-boot.bin file that lives in the MSDOSBOOT partition base directory and you&#39;ll be on your way!&lt;/p&gt;
&lt;p&gt;It looks as though the part SKUs have been adjusted slightly and the 2020.10 U-Boot version does not account differing changes in certain models of the RPi 4 and Pi 400. From what it seems, the fix is expected in the next release.&lt;/p&gt;
</description>
      </item>
      <item>
          <title>Reviving the Ubiquiti loco M5</title>
          <pubDate>Sat, 03 Jul 2021 00:00:00 +0000</pubDate>
          <author>kevstewa@protonmail.com (Kevin Stewart)</author>
          <link>https://k4s.xyz/blog/reviving-ubiquiti-loco-m5/</link>
          <guid>https://k4s.xyz/blog/reviving-ubiquiti-loco-m5/</guid>
          <description xml:base="https://k4s.xyz/blog/reviving-ubiquiti-loco-m5/">&lt;p&gt;I&#39;ve had a pair of &lt;a rel=&quot;external&quot; href=&quot;https://store.ui.com/collections/operator-airmax-devices/products/nanolocom5&quot;&gt;Ubiquiti NanoStation loco
M5&lt;/a&gt;
devices operating continually for around 5 years. They were finally
decommissioned in 2020, but recently were revived for a project at work
temporarily bridging two buildings while I transferred premesis equipment. I
suppose it goes without saying that of course I was pressed for time and of
course this would be a simple in-and-out. It also goes without saying that of
course things didn&#39;t work out as intended and of course these device decided
that their previous life of working 24/7 was over and retirement was in full
swing.&lt;/p&gt;
&lt;p&gt;Hours went by, and every time I actually got the devices to boot and grab an IP
they would mysteriously disappear on the network after any configuration
changes. A seemingly endless cycle of reboots, resets, and re-applicaiton of
configs and I felt like Jerry Seinfeld in the lobby of The Chinese Restaurant.
After wishing, begging the Silicon Gods for mercy, they miraculously held a
config.  They booted back up and linked. Not wont for disrupting the delicate
balance I just afforded myself, I tiptoed out convincing myself &quot;Sure, they had
been working for years with not even a peep - nothing to worry about&quot;. Not two
hours later, they were down. Not three and an ethernet cable was run. My hours
of hair-pulling were enshrined in two white plastic figurines and a long blue
stretch of Cat6.&lt;/p&gt;
&lt;p&gt;All said and done, I got them home and now neither were grabbing IPs, regardless
of how many times I power cycled. There wasn&#39;t a hint of network traffic. No
worries - I&#39;ll take any excuse to break out the &lt;a rel=&quot;external&quot; href=&quot;https://www.adafruit.com/product/70&quot;&gt;serial
cable&lt;/a&gt; and start poking around.&lt;/p&gt;
&lt;p&gt;Opening the case we find an unpopulated header, J5. Heading over to the
&lt;a rel=&quot;external&quot; href=&quot;https://openwrt.org/toh/ubiquiti/nanostationm5&quot;&gt;OpenWRT Table of Hardware&lt;/a&gt;
shows us the
&lt;a rel=&quot;external&quot; href=&quot;https://openwrt.org/toh/ubiquiti/nanostationm5#nanostation_loco_m5_serial_pins&quot;&gt;pinout&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I&#39;m going to use Minicom pointed at ttyUSB0, baud 115200, 8N1. Don&#39;t forget to
disable hardware flow control and enable software flow control under cOnfigure Minicom -&amp;gt;
Serial port setup.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ minicom -D /dev/ttyUSB0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Adding a header, powering up the device, the hardware appears to initialize and we get...&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;U-Boot 1.1.4-gb02ecccc (Feb 14 2020 - 10:53:11) 

DRAM:  64 MB                                   
Flash:  8 MB (0xc2, 0x20, 0x17)
Net:   AR8032 Detected                         
eth0, eth1                                     
Board: Ubiquiti Networks AR9342 board (e845-32564.1122.0030)
Radio: 0777:e845                               
Reset: Normal                                  
Hit any key to stop autoboot:  0 
## Booting image at 9f050000 ...
   Image Name:   MIPS Ubiquiti Linux-2.6.32.71
   Created:      2020-07-15  13:28:25 UTC
   Image Type:   MIPS Linux Kernel Image (lzma compressed)
   Data Size:    979899 Bytes = 956.9 kB
   Load Address: 80002000
   Entry Point:  80002000
   Verifying Checksum at 0x9f050040 ...OK
   Uncompressing Kernel Image ... OK

Starting kernel ...

Booting Atheros AR934x
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;...nowhere. It sits mocking me with it&#39;s single blinking &lt;del&gt;eye&lt;/del&gt; LED, going no
further.&lt;/p&gt;
&lt;p&gt;Well, it may be a hardware issue. There&#39;s no damage visible on the PCB or
evidence of water intrusion. If a fault exists, it might be a component. This
project might be hitting a dead end. No sense in wasting time, however; let&#39;s
head back to OpenWRT and grab the &lt;a href=&quot;https://k4s.xyz/blog/reviving-ubiquiti-loco-m5/openwrt-19.07.7-ar71xx-generic-ubnt-loco-m-xw-squashfs-factory.bin&quot;&gt;latest
bin&lt;/a&gt; for
this device (19.07.7 at the time of writing). My particular units are the
&quot;newer&quot; XW model, not the XM. The XW models use the Atheros AR9342 chips which
can be seen in the initial serial output. I have attempted to upload an XM image
to the XW model and the firmware check appropriately rejects it.&lt;/p&gt;
&lt;p&gt;With the device powered off, hold the reset button, and power back on. Hold the
reset button until you get alternating flashing lights on the device indicating
the TFTP mode is initializing. On the serial output, you&#39;ll see:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BOOTP broadcast 1
Sending discovery response
*** Unhandled DHCP Option in OFFER/ACK: 28
*** Unhandled DHCP Option in OFFER/ACK: 28
DHCP client bound to address 192.168.1.108
Starting TFTP server... 
Using eth0 (192.168.1.108), address: 0x81000000 
Will reset device configuration (Reset button active after 10 seconds).
Erasing sector 123..126 

First 0x7b last 0x7e sector size 0x10000
.... done
Waiting for connection: Sending discovery response
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If momentarily paused at &quot;Starting TFTP server...&quot;, continue holding the reset
button until the configuration is erased. At that point it will be ready for a
TFTP connection. Hop over to the tftp utility or your equivalent. Be sure to put
your client in binary mode and enable tracing for some additional feedback.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ tftp 192.168.1.108
tftp&amp;gt; binary
tftp&amp;gt; trace
Packet tracing on.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When ready, upload the image and the M5 takes care of the rest.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tftp&amp;gt; put openwrt-19.07.7-ar71xx-generic-ubnt-loco-m-xw-squashfs-factory.bin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On the other end, you&#39;ll see the M5 receiving the data, writing the flash
sectors, and confirmation when the update is complete.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Received 4194716 bytes
Firmware Version: XW.ar934x.v6.0.4-42.OpenWrt-r11306-c4a6851c72
Setting U-Boot environment variables
Un-Protected 1 sectors
Erasing Flash.... done
Erased 1 sectors
Writing to Flash... write addr: 9f040000
done
Protected 1 sectors
Copying partition &amp;#39;kernel&amp;#39; to flash memory:

First 0x5 last 0x14 sector size 0x10000
................ done
write addr: 9f050000
Copying partition &amp;#39;rootfs&amp;#39; to flash memory:

First 0x15 last 0x7a sector size 0x10000
................ done
write addr: 9f150000

Firmware update complete.

Resetting...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Okay, so the moment of truth. Are we going to get off, scot-free? Is it possible
it&#39;s just going to work?&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;U-Boot 1.1.4-gb02ecccc (Feb 14 2020 - 10:53:11) 

DRAM:  64 MB                                   
Flash:  8 MB (0xc2, 0x20, 0x17)
Net:   AR8032 Detected
eth0, eth1
Board: Ubiquiti Networks AR9342 board (e845-32564.1122.0030)
Radio: 0777:e845
Reset: Normal
Hit any key to stop autoboot:  0 
## Booting image at 9f050000 ...
   Image Name:   MIPS OpenWrt Linux-4.14.221
   Created:      2021-02-15  15:22:37 UTC
   Image Type:   MIPS Linux Kernel Image (lzma compressed)
   Data Size:    1589435 Bytes =  1.5 MB
   Load Address: 80060000
   Entry Point:  80060000
   Verifying Checksum at 0x9f050040 ...OK
   Uncompressing Kernel Image ... OK

Starting kernel ...

[    0.000000] Linux version 4.14.221 (builder@buildhost) (gcc version 7.5.0 (1
[    0.000000] bootconsole [early0] enabled                                    
[    0.000000] CPU0 revision is: 0001974c (MIPS 74Kc)                          
[    0.000000] SoC: Atheros AR9342 rev 2                                       
[    0.000000] Determined physical RAM map:                                    
[    0.000000]  memory: 04000000 @ 00000000 (usable)
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Turns out, yeah! It boots up and provides a serial terminal. We use
&lt;a rel=&quot;external&quot; href=&quot;https://openwrt.org/docs/guide-user/base-system/uci&quot;&gt;UCI&lt;/a&gt; to
set DHCP on the ethernet port to connect and configure, and to stop the
DHCP server from attempting to hand out leases. Restart the network service and
we&#39;ve got a fully functioning device with OpenWRT.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  _______                     ________        __
 |       |.-----.-----.-----.|  |  |  |.----.|  |_
 |   -   ||  _  |  -__|     ||  |  |  ||   _||   _|
 |_______||   __|_____|__|__||________||__|  |____|
          |__| W I R E L E S S   F R E E D O M
 -----------------------------------------------------
 OpenWrt 19.07.7, r11306-c4a6851c72
 -----------------------------------------------------
=== WARNING! =====================================
There is no root password defined on this device!
Use the &amp;quot;passwd&amp;quot; command to set up a new password
in order to prevent unauthorized SSH logins.
--------------------------------------------------
root@OpenWrt:/# uci set network.lan.proto=dhcp
root@OpenWrt:/# uci commit &amp;amp;&amp;amp; service network restart
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Whaddya know? The devices appear to be stable and I haven&#39;t run in to any issues
yet, though they certainly won&#39;t be seeing a production environment and can rest
easy in retirement. Perhaps we&#39;ll see them again in a future project, but until
then, thanks for stopping by and see you around!&lt;/p&gt;
</description>
      </item>
      <item>
          <title>GIS Beginnings</title>
          <pubDate>Mon, 10 May 2021 00:00:00 +0000</pubDate>
          <author>kevstewa@protonmail.com (Kevin Stewart)</author>
          <link>https://k4s.xyz/blog/gis-beginnings/</link>
          <guid>https://k4s.xyz/blog/gis-beginnings/</guid>
          <description xml:base="https://k4s.xyz/blog/gis-beginnings/">&lt;p&gt;Having patiently been awaiting attention on the always growing list of to-dos,
I&#39;ve finally directed focus towards GIS (geographic information system). Making
sense of maps has been a big component in many of my personal pursuits and
merging that with the extensibility of the digital world is an obvious next
step.&lt;/p&gt;
&lt;p&gt;I&#39;m going to start with Mapbox to practice making basic requests to their API:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;python&quot;&gt;import os
import requests
import urllib.parse
import mercantile

ACCESS_TOKEN = os.environ[&amp;#39;MAPBOX_ACCESS_TOKEN&amp;#39;]
USER_NAME = os.environ[&amp;#39;MAPBOX_USER_NAME&amp;#39;]
BASE_PATH = &amp;quot;https://api.mapbox.com&amp;quot;

STYLES_ENDPOINT = &amp;quot;/styles/v1/&amp;quot;
TILESET_ENDPOINT = &amp;quot;/v4/&amp;quot;

if __name__ == &amp;quot;__main__&amp;quot;:

    # List Styles ####################################################

    # Format: /styles/v1/{username}/{style_id}

    query_string = {&amp;#39;access_token&amp;#39;: ACCESS_TOKEN}
    query_string = urllib.parse.urlencode(query_string)

    request_url = f&amp;quot;{BASE_PATH}{STYLES_ENDPOINT}{USER_NAME}?{query_string}&amp;quot;

    try:
        response = requests.get(request_url)
    except requests.exceptions.RequestException as error:
        print(error)
    else:
        print(request_url)
        print(&amp;quot;Status: &amp;quot;, response.status_code)
        print(response.json())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A quick whip-up, no doubt, but here we test &lt;a rel=&quot;external&quot; href=&quot;https://docs.mapbox.com/api/maps/styles/&quot;&gt;pulling custom
styles&lt;/a&gt; first, not too interesting. I
used their Outdoor map style and made a few tweaks to make a custom style.&lt;/p&gt;
&lt;p&gt;Second, we pull vector tiles in mvt format. The tileset referenced was from my
Bridger Ridge Run race in 2019, the good ol&#39; pre-pandemic days.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/gis-beginnings/ridge_run.png&quot; alt=&quot;Ridge Run&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The tileset was made by first taking the GPX file and running it through a
Mapbox provided JavaScript tool &lt;a rel=&quot;external&quot; href=&quot;https://github.com/tmcw/togeojson&quot;&gt;togeojson&lt;/a&gt;
to obtain, well, a GeoJSON file.  Installing via NPM, you can feed in a GPX file
and direct stdout to the required file type.&lt;/p&gt;
&lt;p&gt;Once in the GeoJSON format, we can use
&lt;a rel=&quot;external&quot; href=&quot;https://github.com/mapbox/tippecanoe&quot;&gt;tippecanoe&lt;/a&gt; to convert to an MBTiles
database and upload to Mapbox either via Studio or the &lt;a rel=&quot;external&quot; href=&quot;https://docs.mapbox.com/api/maps/uploads/&quot;&gt;Uploads
API&lt;/a&gt;. To save time for now though, I
uploaded through Studio.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/gis-beginnings/ridge_run_tileset.png&quot; alt=&quot;Ridge Run Studio&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Now that a tileset exists in Studio, we can pull the associated tiles using the
&lt;a rel=&quot;external&quot; href=&quot;https://docs.mapbox.com/api/maps/vector-tiles/&quot;&gt;Vector Tiles API&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;python&quot;&gt;# Retrieve custom tileset #################################################
    
    # Format: /v4/{tileset_id}/{zoom}/{x}/{y}.{format}

    tileset_id = &amp;quot;kevstewa.2jgs4lck&amp;quot;
    zoom = &amp;quot;16&amp;quot;

    tile = mercantile.tile(-110.947, 45.848, 16)
    x = tile.x
    y = tile.y

    tile_format = &amp;quot;mvt&amp;quot;

    query_string = {&amp;#39;access_token&amp;#39;: ACCESS_TOKEN}
    query_string = urllib.parse.urlencode(query_string)

    request_url = f&amp;quot;{BASE_PATH}{TILESET_ENDPOINT}{tileset_id}/{zoom}/{x}/{y}.{tile_format}?{query_string}&amp;quot;

    try:
        response = requests.get(request_url)
    except requests.exceptions.RequestException as error:
        print(error)
    else:
        print(request_url)
        print(&amp;quot;Status: &amp;quot;, response.status_code)
        print(response.content)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here we use &lt;a rel=&quot;external&quot; href=&quot;https://github.com/mapbox/mercantile&quot;&gt;mercantile&lt;/a&gt; to get a specific
tile&#39;s X and Y based on the coordinates and zoom level provided. That is passed
along with the zoom level and format to the endpoint and back we get our data.&lt;/p&gt;
&lt;p&gt;Many of these steps can be skipped by uploading the original formats to Studio
for automatic conversion to a tileset, but that wasn&#39;t the purpose of this
exercise. Next I&#39;ll be looking at creating a heatmap of all my Strava
activities, this time using the uploads API and other features provided by
Mapbox. Their documentation is very good and can be found at
&lt;a rel=&quot;external&quot; href=&quot;https://docs.mapbox.com/&quot;&gt;docs.mapbox.com&lt;/a&gt;.&lt;/p&gt;
</description>
      </item>
      <item>
          <title>Getting Rusty</title>
          <pubDate>Mon, 03 May 2021 00:00:00 +0000</pubDate>
          <author>kevstewa@protonmail.com (Kevin Stewart)</author>
          <link>https://k4s.xyz/blog/getting-rusty/</link>
          <guid>https://k4s.xyz/blog/getting-rusty/</guid>
          <description xml:base="https://k4s.xyz/blog/getting-rusty/">&lt;p&gt;&lt;img src=&quot;/posts/getting-rusty/rust.jpeg&quot; alt=&quot;Rust&quot; /&gt;&lt;/p&gt;
&lt;p&gt;My current (and very much WIP) application is written in Python. I utilize
Metrc&#39;s API via GET requests to build a local database of facilities, packages,
harvests, sales, and transfers for further data analysis and it also provides an
extremely efficient way of interacting with the traceability system that isn&#39;t a
web app. Currently taking advantage of SQLite, a local database makes for a much
smoother experience.&lt;/p&gt;
&lt;p&gt;As of late, I&#39;ve been going through the &lt;a rel=&quot;external&quot; href=&quot;https://doc.rust-lang.org/stable/book/&quot;&gt;Rust
book&lt;/a&gt; and the exercises provided by
&lt;a rel=&quot;external&quot; href=&quot;https://github.com/rust-lang/rustlings&quot;&gt;Rustlings&lt;/a&gt; to wet my whistle, as it
were. I&#39;ve got some light background in embedded programming and moving back to
a lower level language has been attractive recently. Why not dive in to Rust?&lt;/p&gt;
&lt;p&gt;It&#39;s been a lot of fun getting familiar with Rust idioms and design patterns.
The book has been amazing for grasping fundamentals and I&#39;m constantly dreaming
up all the new ways I can refactor what I&#39;ve got so far.&lt;/p&gt;
&lt;p&gt;All this fluff is really just to demonstrate my two Metrc &quot;Hello, world.&quot; style
first-steps to a full rewrite, one using
&lt;a rel=&quot;external&quot; href=&quot;https://crates.io/crates/reqwest&quot;&gt;reqwest&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;rust&quot;&gt;// The first attempt using reqwest.

use std::env;
use serde_json;

fn print_json(data: &amp;amp;str) {
    let v: serde_json::Value = serde_json::from_str(data).unwrap();

    println!(&amp;quot;{:#?}&amp;quot;, v);
}

#[tokio::main]
async fn main() -&amp;gt; Result&amp;lt;(), Box&amp;lt;dyn std::error::Error&amp;gt;&amp;gt; {
    let user = env::var(&amp;quot;METRC_VENDOR_API_KEY&amp;quot;)?;
    let pass = env::var(&amp;quot;METRC_USER_API_KEY&amp;quot;)?;

    let client = reqwest::Client::new();

    let resp = client.get(&amp;quot;https://api-mt.metrc.com/facilities/v1&amp;quot;)
        .basic_auth(user.clone(), Some(pass.clone()))
        .send()
        .await?
        .text()
        .await?;

    print_json(&amp;amp;resp);

    Ok(())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;...and another using the more bare-bones hyper:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;rust&quot;&gt;use std::env;
use std::io::Write;
use hyper::{Client, Method, Request, Body};
use hyper_tls::HttpsConnector;
use base64::write::EncoderWriter as Base64Encoder;

#[tokio::main]
async fn main() -&amp;gt; Result&amp;lt;(), Box&amp;lt;dyn std::error::Error + Send + Sync&amp;gt;&amp;gt; {
    let username = env::var(&amp;quot;METRC_VENDOR_API_KEY&amp;quot;)?;
    let password = env::var(&amp;quot;METRC_USER_API_KEY&amp;quot;)?;

    let mut header_value = b&amp;quot;Basic &amp;quot;.to_vec();
    let mut encoder = 
        Base64Encoder::new(&amp;amp;mut header_value, base64::STANDARD);
    
    write!(encoder, &amp;quot;{}:&amp;quot;, username).unwrap();
    write!(encoder, &amp;quot;{}&amp;quot;, password).unwrap();

    let req = Request::builder()
        .method(Method::GET)
        .uri(&amp;quot;https://api-mt.metrc.com/facilities/v1/&amp;quot;)
        .header(&amp;quot;Authorization&amp;quot;, header_value)
        .body(Body::empty())
        .expect(&amp;quot;request builder&amp;quot;);

    let https = HttpsConnector::new();
    let client = Client::builder()
        .build::&amp;lt;_, hyper::Body&amp;gt;(https);

    let response = client.request(req).await?;

    println!(&amp;quot;Status: {}&amp;quot;, response.status());

    let buf = hyper::body::to_bytes(response).await?;

    println!(&amp;quot;Body: {:?}&amp;quot;, buf);

    Ok(())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hyper remains as advertised - a bit more involved as it&#39;s more like the bricks
to build custom libraries from the ground up. One has to set up the https
connector, as well as generate the basic authorization header value. There is a
const in the header struct that I have yet to piece together called
AUTHORIZATION.&lt;/p&gt;
&lt;p&gt;I&#39;m still wrapping my head around ownership and returning results, options, and
using traits but I can&#39;t wait to keep plugging away!&lt;/p&gt;
</description>
      </item>
    </channel>
</rss>
