Minimal, from-scratch Zephyr RTOS workspace for the Microchip PIC32CM PL10 Curiosity Nano board, plus a pinned, non-interactive script that reproduces the entire installation — RTOS clone, HAL modules, Python venv, toolchain, build, and flash — on another Windows machine.
Fresh Windows machine, no Zephyr yet? Clone this repo, make sure the five host tools are present (a helper installs any that are missing), then run the one reproduction script. It clones a PL10-tailored Zephyr (~1.8 GB), sets up the toolchain, builds the app, and — if a PL10 Curiosity Nano is plugged in — flashes it, all in one go (~5–20 min):
git clone https://github.com/zabooh/pic32cm-pl10-cnano-zephyr.git C:\zw
cd C:\zw
powershell -ExecutionPolicy Bypass -File install-prerequisites.ps1 # installs any missing host tool; then open a new shell
powershell -ExecutionPolicy Bypass -File reproduce-install.ps1install-prerequisites.ps1 leaves already-installed tools untouched and only pulls the
pinned version of whatever is missing (Python, Git, Ninja, CMake, 7-Zip, via winget) —
skip it if you already have all five. Open a new terminal between the two scripts so
freshly installed tools land on PATH. Details and exactly what gets version-pinned:
Reproducing this setup elsewhere. Keep the target
path short (Windows' ~260-char limit; see Quick start).
Already ran this once and just want to rebuild / re-flash? → Quick start.
- Start here
- Executive summary
- Key takeaways
- Quick start
- Project structure
- Working in VS Code
- Reproducing this setup elsewhere
- Design decisions
- Updating the Zephyr version
- Memory usage
- Known issues
- What is
RUNBOOK.md? - Deep dives — how it works →
DEEPDIVE.md
This is a small interactive firmware — LED control, ADC sampling, and a reset over a serial command line — built to answer one question: can Zephyr RTOS run comfortably on an 8 KB-RAM microcontroller, or does it need a beefier part to be worth using?
- Interactive serial commands:
led on/off/toggle/blink <ms>,adc read,adc stream start/stop,button(read the SW0 user button),load on/off(hold the CPU busy to make the idle-vs-active current draw visible on a meter),standby <s>(drop the SoC into real ~2 µA Standby for<s>seconds, then reboot on wake — the PL10's headline low-power mode, driven directly since Zephyr has no PM port for this part yet; it reboots rather than resuming in place, see STANDBY.md for why),reset,threads(live per-thread stack usage),mem <addr>(raw hex dump),help— with a hand-rolled line editor featuring bash-style Up/Down command history, and a boot banner stamped with the build time - Genuinely multi-threaded: seven threads (LED blink, ADC stream, command console, button
watcher, an on-demand CPU-load generator, plus the kernel's main/idle) — a real RTOS app,
not a superloop; a
threadscommand shows each one's live stack high-water mark - Fully reproducible setup: one script rebuilds the whole toolchain (Zephyr, HAL modules, SDK, Python deps) from scratch on any Windows machine, pinned to exact versions
- Lean by construction: only the Zephyr modules this board actually needs are cloned
(not the dozens of unrelated HALs/subsystems a default
west updatewould pull in), keeping the whole installation under 2 GB instead of several GB - VS Code integration: build/flash tasks, source-line debugging, IntelliSense
- Only 70% RAM / 32% flash used of this board's 8 KB RAM / 60 KB flash — still ~30% of RAM free with all of the above running (thread introspection included)
Where to go from here:
- Just want to build and flash it? → Quick start
- Want to reproduce this install elsewhere? → Reproducing this setup elsewhere
- Want to understand why a decision was made? →
RUNBOOK.md - Want to understand how it works under the hood (drivers, interrupts, power, peripheral
support, other PL10 parts)? →
DEEPDIVE.md
- Zephyr RTOS runs comfortably on the PIC32CM PL10's 8 KB RAM — contrary to the common assumption that it needs a bigger part.
- A real multi-threaded interactive app (LED blink + ADC streaming + a command console with history + a button watcher + a CPU-load generator + thread introspection, seven threads total) still leaves ~30% of RAM free — 5,744 B used of 8 KB. A bare blinky started far lower (2,744 B / 33.50%; see Memory usage).
- The entire toolchain — Zephyr revision, HAL modules, SDK, Python packages — is version-pinned and reproducible with one script on a fresh Windows machine.
- Source-line debugging works reliably in VS Code once a couple of Windows-specific Cortex-Debug/pyOCD quirks are worked around (documented once, not re-discovered every time — see Known issues).
- Staying lean is a choice, not luck: no Zephyr shell subsystem (a hand-rolled parser instead), only the four HAL/lib modules this board needs, and trimmed kernel stacks.
C:\zwthroughout this README (andRUNBOOK.md) is just an example path, not a requirement — use whatever directory you like. Keep it short, though: Zephyr's build tree nests deeply, and a long workspace path can hit Windows' ~260-character path limit (see "Path too long" inRUNBOOK.md→ Troubleshooting).
On a machine already set up (venv, SDK, and workspace in place):
& C:\zw\.venv\Scripts\Activate.ps1
Set-Location C:\zw\zephyr
$env:CMAKE_GENERATOR = "Ninja"
west build -p always -b pic32cm_pl10_cnano -d C:\zw\build C:\zw\app
Set-Location C:\zw\build
west flashIn cmd.exe instead of PowerShell, use activate.bat and set instead of
Activate.ps1/$env:...:
C:\zw\.venv\Scripts\activate.bat
set CMAKE_GENERATOR=Ninja
cd C:\zw\zephyr
west build -p always -b pic32cm_pl10_cnano -d C:\zw\build C:\zw\app
cd C:\zw\build
west flash
⚠️ Activate the venv first — callingwest.exe/pyocd.exeby full path without activating works forbuildbut not forflash. See Known issues.
Straight from the clone root — in cmd.exe, after cd-ing into the cloned directory,
build and flash with relative paths only (nothing hardcoded, works on any machine):
.venv\Scripts\activate.bat
set CMAKE_GENERATOR=Ninja
cd zephyr
west build -p always -b pic32cm_pl10_cnano -d ..\build ..\app
cd ..\build
west flashIf
west flashstops withSWD/JTAG communication failure (FAULT ACK), the core is asleep / the debug port is in a leftover state — or a GDB server still holds the probe (stop any debug session first; only one pyOCD can hold the probe). Reset once and retry, from thebuilddir:pyocd reset -t pic32cm6408pl10048 -f 100000 west flashThe VS Code Zephyr: Flash task does this reset automatically.
Connect a serial terminal (PuTTY, TeraTerm, ...) to the board's virtual COM port at
115200 baud. A boot banner with the build timestamp prints, then the prompt
pl10:~$ . Type a command and press Enter; Up/Down arrow recalls the last five
commands. Try help for the full list:
pl10:~$ led on
pl10:~$ led blink 100
pl10:~$ adc read
adc: 3676 (2.962 V)
pl10:~$ adc stream start
adc: streaming every 500 ms
adc: 3688 (2.972 V)
...
pl10:~$ adc stream stop
pl10:~$ button
button: released
pl10:~$ button: pressed <-- printed on its own when you press SW0
button: released
pl10:~$ standby 5
standby: Standby (~2 uA) for 5 s, then reboot...
*** Booting Zephyr OS build v4.4.0-... *** <-- ~5 s later: wakes itself, reboots clean
PIC32CM PL10 Blinky - built ...
pl10:~$ reset
(standby reboots on wake rather than resuming in place — that's deliberate and reliable;
STANDBY.md explains why resume-in-place isn't achievable without an upstream
PL10 PM driver.)
(On the bare board the ADC input pin floats, so adc read returns noisy values near the
supply rail — that's expected, it confirms the ADC is actually converting.)
Don't have a set-up machine yet? See Reproducing this setup elsewhere.
| Path | Purpose |
|---|---|
app/ |
The application: CMakeLists.txt, prj.conf, app.overlay (SW0 user-button node + led0 active-low polarity fix), cmd_sections.ld (command-registry linker section), and src/ split one module per domain — main.c (startup wiring), led_ctrl.c (LED + blink thread + led command), pl10_adc.c (ADC driver + stream thread + adc command), button.c (SW0 read + press-notifier thread + button command), cmd_parser.c (console infrastructure: line editor, history, registry dispatch), cmd.h (command-registry interface), diag.c (reset/threads/mem commands), load.c (CPU-load generator + load command), standby.c (~2 µA Standby + reboot-on-wake, standby command — see STANDBY.md), fault.c (fatal-error handler), app_threads.h (central thread stack/priority budgets) |
zephyr/ |
Zephyr RTOS source (shallow clone, pinned revision) |
modules/ |
Only the HAL/library modules actually needed: hal_microchip, cmsis, cmsis_6, picolibc |
build/ |
CMake/Ninja build output; build/zephyr/zephyr.hex is the flashable artifact |
.venv/ |
Python virtual environment (west, pyOCD, build dependencies) |
install-prerequisites.ps1 |
Checks for the host tools (Python, Git, Ninja, CMake, 7-Zip) and installs the pinned version of any that are missing (via winget); leaves existing installs untouched |
reproduce-install.ps1 |
Pinned, non-interactive script that recreates this whole setup from scratch |
requirements-lock.txt |
Exact pinned versions of every Python package in .venv |
.vscode/ |
Build/flash tasks, debug configs, and IntelliSense settings for working in VS Code (see below) |
RUNBOOK.md |
Step-by-step build log with rationale, troubleshooting table, and lessons learned |
DEEPDIVE.md |
Longer how it works essays (drivers, interrupts, power, peripheral support, porting) kept out of this README |
HARMONY-EXAMPLES.md |
Catalogue of Microchip's bare-metal Harmony 3 example apps for the PL10 family (26 examples by topic) — the register-level reference used to cross-check the bridging pattern |
- Board: PIC32CM PL10 Curiosity Nano (Zephyr board name
pic32cm_pl10_cnano, SoCpic32cm6408pl10048, Arm Cortex-M0+) - On-board debugger: Microchip nEDBG (CMSIS-DAP over USB), also exposes a virtual COM port for the serial console
- No external debug probe needed — just a USB data cable
In plain words: the app in app/ is a small firmware that blinks an LED and listens
for typed commands over the serial port. You type a command in a terminal and the board
acts on it — control the LED (led), read the analog input (adc), read the user button
(button), inspect the running threads or raw memory (threads, mem), or reboot
(reset); Up/Down recalls previous commands. Pressing the on-board button also prints a
message on its own. It's deliberately a real multi-threaded RTOS app — the LED blink, the ADC
stream, the button watcher, and the console run concurrently, not in one big loop — as a
demonstration that Zephyr fits
comfortably on this 8 KB-RAM chip. The organizing idea is one small module per job,
wired together through a tiny flash-resident command registry instead of Zephyr's
RAM-heavy shell.
Concretely: the app is split one module per functional domain. No shell subsystem — the
command console is hand-rolled on console_getchar(). cmd_parser.c is feature-agnostic
infrastructure: it tokenizes a line and dispatches by looking the command up in a
flash-resident command registry. Each feature module self-registers its command with
one CMD_REGISTER() macro (cmd.h), so the parser has no dependency on led_ctrl /
pl10_adc / diag and adding a command touches only its owning module.
app/src/ (7 threads: main + idle + blink_tid + adc_stream_tid + cmd_tid + button_tid + load_tid)
+-------------------------------+
| cmd_parser.c (cmd_tid) | feature-agnostic console:
| console_getchar, line editor, | tokenize a line, look argv[0]
| history, registry dispatch | up in the registry, dispatch;
| | built-in "help"
+---------------+---------------+
| STRUCT_SECTION_FOREACH(cmd, ...)
v
+-------------------------------------------+ "cmd" iterable ROM section
| command registry (flash) | (cmd.h / CMD_REGISTER, ~0 B RAM)
+---^---------^---------^---------^----------+
| | | | each module self-registers its command(s)
+---+----+ +--+-----+ +-+------+ +-+------+
|led_ctrl| |pl10_adc| |button.c| | diag.c |
|blink_ti| |adc_strm| |button_t| | reset |
| GPIO | | ADC0 | | SW0 | | threads|
| "led" | | "adc" | |"button"| | mem |
+---+----+ +--+-----+ +-+------+ +-+------+
| | | | (fault.c overrides the fatal handler)
v v v v
+-----------------------------------------------------------+
| Zephyr RTOS kernel (GPIO, UART, clock drivers) |
+----------------------------+------------------------------+
v
+-----------------------------------------------------------+
| HAL / library modules: hal_microchip, cmsis, cmsis_6, |
| picolibc (ADC0 register defs come from hal_microchip) |
+----------------------------+------------------------------+
v
+-----------------------------------------------------------+
| PIC32CM PL10 (Arm Cortex-M0+) |
+-----------------------------------------------------------+
main.c: startup wiring only (led_ctrl_init + default blink).
Footprint on the PL10 (8 KB RAM / 60 KB flash): about 5.6 KB RAM (~70 %) and 19 KB flash (~32 %) — RAM is the tight resource, flash is roomy (two-thirds free). Most of the RAM is the five app threads (each: its stack — 640 B console, 384 B button, 352 B blink, 352 B ADC, 320 B load generator — plus a ~120 B control block); dropping the Zephyr shell in favor of the hand-rolled parser is a big part of why a genuinely multi-threaded app still leaves ~30 % of RAM free. Full per-symbol breakdown: Memory usage.
Open the workspace root itself in VS Code — the cloned directory that contains app/,
zephyr/, and .vscode/ (not a subfolder). The checked-in .vscode/ configs make
build, flash, and debug work with no per-machine editing (all paths are relative or
resolved via ${workspaceFolder} / ${env:USERPROFILE}). .vscode/extensions.json will
prompt you to install the two extensions this setup relies on:
- C/C++ (
ms-vscode.cpptools) — IntelliSense, backed bybuild/compile_commands.json - Cortex-Debug (
marus25.cortex-debug) — GDB/pyOCD debugging UI
Build — Ctrl+Shift+B, or Terminal → Run Task:
- Zephyr: Build (pristine) (default) — full
-p alwaysrebuild, also regeneratescompile_commands.json - Zephyr: Build (incremental)
- Zephyr: Menuconfig
- Zephyr: Clean build dir
Flash — Terminal → Run Task → Zephyr: Flash. Runs a pristine build, then resets the
board and flashes in one step — always shipping a fresh binary. The reset first clears the
sleeping-core / leftover-debug DAP state that otherwise makes flashing fail with
SWD/JTAG communication failure (FAULT ACK) (see Known issues). Picks up
the board's SWD frequency from board.cmake automatically. Stop any debug session and
the Start GDB Server (pyOCD) task before flashing — only one pyOCD instance can hold
the USB probe at a time, and a lingering GDB server is itself a common cause of the
FAULT ACK.
Debug — Run and Debug panel (Ctrl+Shift+D) offers three configs; the reliable one
on Windows is Zephyr: Debug (attach to running GDB server) - start server task first! —
see the workflow below for why and how. All tasks call west/pyocd directly from
.venv/Scripts/ (no need to activate the venv first) and use the pinned pyocd==0.43.0
explicitly, so Cortex-Debug can't accidentally pick up a different (broken) pyOCD from
PATH.
- Build, then flash — Terminal → Run Task → Zephyr: Build (pristine), wait for it to finish, then Terminal → Run Task → Zephyr: Flash. (Skip this if the board is already running the binary you want to debug.)
- Start the GDB server — Terminal → Run Task → Zephyr: Start GDB Server (pyOCD).
This runs
pyocd gdbserverin its own dedicated terminal panel and stays running. Wait until that terminal printsGDB server listening on port 3333before moving on. - Set a breakpoint — open one of the app sources and click in the gutter on a line
inside a function body — e.g. a command handler in
app/src/cmd_parser.c, or a line inside a thread'swhile (1)loop body (cmd_parser.c,led_ctrl.c). Don't put it on thewhile (1) {line itself — that only ever fires once per debug session (see Known issues). - Attach — open the Run and Debug panel (
Ctrl+Shift+D), pick Zephyr: Debug (attach to running GDB server) - start server task first! from the dropdown, and press F5. This config'spostAttachCommandsrunsmonitor reset haltautomatically, so the CPU starts from the reset vector instead of attaching mid-sleep-cycle (see Known issues for why that matters). - Execution stops at the reset vector; press Continue (F5) to run until your breakpoint hits. From there, step/inspect variables/watch expressions as normal.
- When done, stop the debug session (Shift+F5) and separately stop the Zephyr: Start
GDB Server (pyOCD) task (trash-can icon in its terminal panel, or
Ctrl+Cin it) — only onepyocd gdbservercan hold the USB probe at a time.
Full root-cause writeups for the Cortex-Debug/SWD quirks this workflow works around:
RUNBOOK.md → Step 11c (debug config) and 11e (plain command-line GDB, also useful
outside VS Code).
git clone https://github.com/zabooh/pic32cm-pl10-cnano-zephyr.git C:\zw2
Set-Location C:\zw2
powershell -ExecutionPolicy Bypass -File reproduce-install.ps1cd into the clone first and call the script by its bare filename (not a full path to
some other copy) — it determines its target directory from $PSScriptRoot, the actual
location of the .ps1 file being run, so everything it builds resolves to that clone.
The script clones the pinned Zephyr revision, fetches only the four modules this board
needs, installs the pinned SDK and Python packages, builds the app, and flashes the
board — finishing with Reproduction complete. in green. Expect roughly 10-20 minutes
depending on network speed.
Two things the script does not do for you:
- Prerequisites — Python (3.12+), Git, Ninja, CMake, and 7-Zip must already be on
PATH.reproduce-install.ps1checks for them and stops with a clear error if one is missing; it does not install them. Runinstall-prerequisites.ps1first to auto-install (via winget) the pinned version of any that are missing — already-present tools are left untouched. Open a new shell afterwards so newly addedPATHentries take effect. - Board connection — the board must be plugged in via USB before the script reaches
its last step (
pyocd flash). Everything before that succeeds without it attached.
If you're copying straight from this machine or a network share instead of cloning, copy
reproduce-install.ps1, requirements-lock.txt, and app\ into an empty target
directory yourself, then run the script the same way from there — it does not fetch or
copy those two items itself, they must already sit next to it.
Everything here is version-pinned — the exact Zephyr commit, module revisions, SDK version, and every Python package — so two runs on different machines produce the same toolchain, not whatever happens to be "latest" that day. Nothing auto-updates; see Design decisions for how to move a pin forward deliberately.
Full detail on prerequisites and exactly what's pinned and why: RUNBOOK.md →
"Checking prerequisites" and Steps 1-10.
Goals this workspace optimizes for:
- Reproducibility — one pinned script, same result every time
- Minimal footprint — the leanest module/toolchain set that still builds
- No unnecessary Zephyr subsystems — the shell was replaced with a minimal command parser (see Memory usage)
- VS Code support out of the box
- Easy portability to another machine
How that's achieved:
- Shallow clone (
--fetch-opt=--depth=1) — no full Git history for Zephyr or its modules. manifest.group-filterexcludes coarse optional groups (optional,babblesim,ci).manifest.project-filter(the important one) is an explicit allow-list —-.*,+hal_microchip,+cmsis,+cmsis_6,+picolibc— so only the four modules this board actually needs are cloned, instead of the dozens of unrelated HALs, crypto/TEE, and RTOS extension modules thatwest updatewould otherwise pull in by default.- SDK: ARM-only toolchain (
-t arm-zephyr-eabi), no bundled host tools (-H) since flashing goes through pyOCD, not OpenOCD/QEMU. - Python deps: only
zephyr/scripts/requirements-base.txtpluspyocd— not Zephyr's full test/compliance requirement set. - The firmware itself: no shell subsystem — a hand-rolled command parser on
console_getchar()instead, with apl10:~$prompt,help, and bash-style Up/Down history for UX parity at a fraction of the shell's RAM.
Net result: a full, working toolchain and firmware in a few hundred MB instead of the several GB a default Zephyr installation would use.
Moving a version pin forward: this is a deliberate, occasional step, not a routine
west update — it can drag along module revision changes and SDK-compatibility shifts,
so it needs its own end-to-end test (build + flash + interaction). west update only
ever syncs the four module projects to whatever zephyr/'s currently-checked-out
west.yml pins them to — it never touches zephyr/ itself, which is pinned to one exact
commit. How to actually do it safely: Updating the Zephyr version
below; the mechanism behind it: RUNBOOK.md → Appendix B.
This workspace is pinned to one exact Zephyr commit ($ZEPHYR_REV in
reproduce-install.ps1) so builds are reproducible and don't break when upstream moves.
When you do want to move forward on purpose — typically to pick up newer PIC32CM PL10
support Microchip has merged into Zephyr mainline — there's a guided procedure for it:
RUNBOOK-update-zephyr-pin.md.
Why a runbook instead of "just change the hash": editing $ZEPHYR_REV alone only affects a
fresh from-scratch reproduction, and it silently skips several things that routinely bite.
west update never touches zephyr/ itself (so the pin has to be moved by hand); a new
driver may need a module the project-filter currently excludes; a newer Zephyr can demand
a newer SDK or west; and RAM/flash headroom on this 8 KB part is thin. The runbook walks all
of that, in order.
It's written as an agent-driven interview — the intended way to run it is to let Claude Code drive it inside VS Code. Open the workspace and say something like "update the Zephyr pin" (or "lese RUNBOOK-update-zephyr-pin.md"). Claude then:
- Asks you for the target commit or tag (and why) — it won't guess one or silently grab
origin/main. - Moves the pin in your live workspace —
git checkout <commit>insidezephyr/, thenwest update, which pulls the matchinghal_microchip/module revision bumps. - Works through the pitfalls — widening
project-filterif a new module is needed, bumping the SDK/west if the newer Zephyr requires it, a pristine rebuild, aram_report/rom_reportheadroom check, a flash + serial-console test, and an ISR-stack re-test if the new code adds interrupt sources. - Persists the pin only after it built and flashed — writes the new
$ZEPHYR_REV(plus any SDK/west/filter changes) back intoreproduce-install.ps1, so the next reproduction matches what was just verified.
The rule the runbook enforces: never commit a pin you haven't actually built, flashed,
and exercised on the board. Two practical notes: you need a reproduced workspace to run it
in — a bare git clone has no zephyr/ yet, so run reproduce-install.ps1 once first — and
you can of course follow the steps by hand instead of via Claude; the file reads as a plain
checklist either way.
This board has only 8 KB RAM / 60 KB flash, so usage is worth watching, not just "does it fit." Current build (LED blink + ADC + command console with history + a button watcher + a CPU-load generator + thread introspection, seven threads):
| Used | Capacity | % used | |
|---|---|---|---|
| RAM | 5,744 B | 8,192 B | 70.12% |
| Flash | 19,840 B | 61,440 B | 32.29% |
Check live numbers with west build -d C:\zw\build -t ram_report / -t rom_report
(per-symbol breakdown) — or, on the running board, the threads console command,
which prints each thread's live stack high-water mark.
Most of the RAM is the five application threads — each k_thread costs its stack (352 B
blink, 352 B ADC stream, 640 B console, 384 B button, 320 B CPU-load generator) plus a 120 B
thread control block, all directly visible in ram_report. Those stack sizes live in one place (app_threads.h) and were
set with headroom over the measured high-water marks — the threads command flagged the
first cut (256/320 B) as running at 100%/97%, so they were bumped. As a runtime safety
net, CONFIG_STACK_SENTINEL is enabled (this MPU-less Cortex-M0+ can't use hardware stack
protection): if a thread ever overflows anyway, the kernel raises a fatal error naming it
instead of silently corrupting memory. Fatal errors (a stack overflow, or a HardFault from
a bad mem address) print their dump and then auto-reboot to the prompt
(CONFIG_RESET_ON_FATAL_ERROR) rather than hanging. The rest is kernel scaffolding, itself trimmed
(ISR_STACK_SIZE 2048→1024, MAIN_STACK_SIZE 1024→512).
Staying this lean is deliberate: no Zephyr shell subsystem (a hand-rolled
console_getchar() parser instead, which alone saved the shell's ~40-47% of RAM), only
the four HAL/lib modules this board needs, CONFIG_LOG=n.
A bare blinky earlier in this project's history sat at just 2,744 B (33.50%). Full
version-by-version numbers, the per-module ram_report/rom_report breakdown, a
comparison against a hand-tuned CMSIS/RTX5 app on the same chip, and the measured
per-thread RAM cost: RUNBOOK.md → Appendix A (those figures predate the ADC/threads/
history work — this section has the current totals).
- pyOCD 0.44.1 is broken for this board. Flashing/connecting fails with
SWD/JTAG communication failure (FAULT ACK)/(No ACK)regardless of connect mode or clock speed. Stay on the pinnedpyocd==0.43.0; re-pin if you everpip install --upgrade pyocdand flashing suddenly breaks. - A missing Zephyr SDK fails hard everywhere, never silently.
reproduce-install.ps1aborts at itswest sdk installstep; a manualwest buildwithout the SDK fails at CMake's configure step with "could not find Zephyr SDK"; VS Code debugging/IntelliSense fail to findarm-zephyr-eabi-gdb.exe/compiler include paths. Nothing produces a half-finished result. - Calling
west.exe/pyocd.exeby full path without activating the venv works forwest buildbut notwest flash— its pyOCD runner shells out to a barepyocdcommand and relies onPATHto find it. Activate the venv first. - Cortex-Debug 1.12.1 on Windows can't reliably auto-launch its own pyOCD server. Use
the "start GDB server task, then attach" workflow under
Working in VS Code instead of the two auto-launch debug configs.
Full root cause:
RUNBOOK.md→ Step 11c. - Attaching to a sleeping core causes intermittent SWD
FAULT ACKfaults — this board's SWD port becomes briefly inaccessible while the CPU is ink_msleep()-driven WFI idle. Fixed automatically viamonitor reset haltin the attach configs'postAttachCommands. Also: put breakpoints inside a loop body, not on thewhile (1) {line itself — it only fires once. Full root cause:RUNBOOK.md→ Step 11c. launch.json/c_cpp_properties.jsonreference the SDK via${env:USERPROFILE}/zephyr-sdk-1.0.1/.... This is portable across machines as-is:${env:USERPROFILE}resolves per user automatically, and the version segment matches the SDK the reproduction script pins and installs to the default per-user location. So on a normal clone-and-reproduce, no editing is needed. Hand-edit both files only if you install the SDK somewhere other than that default location, or bump the SDK version (in which case also update$SDK_VERinreproduce-install.ps1to match). SeeRUNBOOK.md→ Step 11 for the full rationale and JSON.
This README documents how to use the workspace. RUNBOOK.md documents how it was
built — the step-by-step log the whole thing was constructed from, written as
instructions an agent actually followed (see its "Task" and "Constraints" sections at the
top), not a retrospective summary. It's longer and more detailed than this README on
purpose:
- Steps 1-9 — scaffold the workspace,
west init, narrow the module set down to the four this board needs (theproject-filterreasoning — whygroup-filteralone can't do it), fetch modules shallow, export/trim the toolchain, verify the board target, write the app (main.c+prj.conf, with the shell-vs-console_getline()decision and numbers), build/flash, and a functional test over the serial console. - Step 10 — how
reproduce-install.ps1itself was produced: harvesting the exact pinned values (Zephyr commit, SDK/pyOCD versions, module revisions) out of a verified working install, plus a bug found and fixed along the way (west init --mr <SHA>doesn't work —git's--branchcan't resolve a raw commit SHA). - Step 11 — the VS Code integration: why each task/debug-config/IntelliSense setting
in
.vscode/looks the way it does, including the Cortex-Debug andFAULT ACKdebugging issues referenced in Known issues. - Appendix A — the full memory analysis behind Memory usage.
- Appendix B — the full
west updateexplanation behind Design decisions. - A Troubleshooting table keyed by symptom, and a completion checklist.
Read RUNBOOK.md when: you're about to change module filters, SDK setup, or flashing config (CLAUDE.md says so explicitly); you hit a build/flash/debug failure this README's Known issues doesn't already cover; or you want the why behind a decision, not just the current end state.
The step-by-step build log lives in RUNBOOK.md; the longer how it actually works
essays live in DEEPDIVE.md, kept out of this README so the front page
stays about getting the board running. What's in there:
- Peripheral support status — what Zephyr drives on the PL10 today (clock/GPIO/UART ready; SPI/I²C/DMA a config step away; ADC/PWM/DAC/etc. need bridging), as a snapshot of the pinned revision.
- Bridging a peripheral before Zephyr supports it
— the stopgap-driver pattern behind
pl10_adc.c, and how to swap in the real Zephyr driver later in one file. - Interrupts and the system tick — the whole interrupt surface (UART + SysTick), tickless timing, round-robin slicing.
- The driver model behind a pin toggle
— what
gpio_pin_toggle_dt()actually compiles to, and the one indirection that survives. - Idle, WFI, and CPU load — why the core sleeps ~99 % of the time, how to measure CPU load, the ~1.2 mA WFI vs. ~2 µA Standby power ladder, and why a Zephyr PM/Standby port is the key missing piece.
- Sleep modes, wake-up latency, and keeping state across a sleep
— the PL10 has only Idle + Standby (no RAM-wiping deep sleep); both retain SRAM and resume
after
WFIwithout a reset, so wake is 24–120 µs, not a reboot. Plus the current-vs-latency knob, and how to survive a true reset/power loss (RCAUSE+ Flash). - PM Standby, and why the
standbycommand reboots on wake — the full story behind thestandby <s>command: the ~2 µA direct-register Standby, why a resume-in-place Zephyr PM port isn't achievable on the PL10 without an upstream driver (the kernel tick dies after the nap), and the reboot-on-wake design that ships instead. - PIC32CM PL10 device series — the wider family (3204/6408/1216), package options, and full orderable part numbers.
- Porting to another PL10 device — effort by target (64 KB easy, 128 KB a small dtsi, 32 KB a RAM diet), and the mechanical changes.