← Short article

Pico flash layout and XIP cache misses: report

This report covers the experiments behind the short article. The question: can a different arrangement of the code in flash make the Zephyr USB network stack fast on the Raspberry Pi Pico, without using the SRAM that an application needs? The background is in the previous post: one echo costs about 2,300 misses in the RP2040 XIP cache at about 70 clock cycles each.

Set-up and metrics

Board: Raspberry Pi Pico (RP2040, 125 MHz), connected over USB full speed, CDC-ECM class, Zephyr main of 31 October 2026, a small echo application, -Os. All images use the same source code; only the build or link options change. The XIP cache has 16 KB, two ways and 8-byte lines, so it has 1024 sets and a line at address a goes to set (a >> 3) mod 1024 (RP2040 datasheet §2.6.3.1).

Metrics
MetricHow it is measured
Misses per echoXIP_CTRL hit and access counters read over SWD; 300 pings at 20 ms intervals, minus an idle window of equal length.
Turnaroundusbmon on the host: time from the end of the OUT transfer to the end of the IN reply, median of 300 pings.
UDP echo rateHost sends UDP datagrams at fixed rates (50/s steps up to 3000/s); replies counted per step; 3 passes, each with a new flash of the image. The peak is the highest rate of replies.
FootprintFlush the cache over SWD, send one echo, count the misses, minus an idle window: the number of distinct 8-byte lines that one echo touches.

Repeated runs of the same image agree within 1–2 % for misses and within 0.005 ms for most turnaround values.

Finding the hot code

The Cortex-M0+ in the RP2040 has the DWT program-counter sample register. The debug probe reads it about 4,000 times per second while the board works, without stopping it. In the stock image almost every line that executes misses the cache on every echo, so the samples show where the misses are. Per library, 22 % of the busy time is in the IP stack, 20 % in the kernel, 7 % in the USB device driver, 5 % in the USB device stack, and smaller parts in Ethernet L2, net_buf and libc.

About 230 functions (20–25 KB) receive samples during a ping load. The cold-cache probe gives the true size of what one echo touches:

Distinct flash lines touched by one echo (cold cache)
EchoLinesBytes
ICMP ping, 56 B3,50627 KB
UDP echo through a socket, 64 B5,03139 KB
Cache capacity2,04816 KB

This is the central fact. A cyclic pass over 3,500 lines through a 2,048-line cache must miss about 1,000 times per pass, even with ideal replacement. Layout can remove conflict misses, not capacity misses.

Layouts without extra RAM

A linker snippet placed in the first output section of the image (after the vector table) lists the hot functions; GNU ld gives each input section to the first rule that matches, so these functions leave the general text section. Three arrangements were tested:

Time per echo for each layout, and against extra RAM
Left: layouts without extra RAM. Right: colouring plus the densest functions in SRAM (ping profile). Mean ± 1 SD of 3–9 runs.
ICMP ping, no extra RAM
LayoutMisses per echoTurnaround
Stock2,2771.747 ms
LTO1,6661.291 ms
Hot block1,4241.186 ms
Cache colouring (ping profile)9400.885 ms
LTO + colouring (UDP + ping profile)7730.802 ms

Adding a little SRAM

The colouring tool can also move the functions with the most samples per byte to SRAM, either with Zephyr’s zephyr_code_relocate(… FILTER …) or through the .ramfunc section (a custom linker script places .ramfunc before the text section; this route also works with LTO). Busy-wait loops, which have many samples but few misses, are excluded. The rest of the hot code is coloured as before.

UDP echo, profile from UDP + ping traffic
LayoutExtra RAMPeak UDP echoPing turnaround
Stock0322/s1.747 ms
Coloured0412/s1.118 ms
LTO + coloured0650/s0.802 ms
Coloured + 4 / 8 / 12 KB in SRAM4.8 / 9.0 / 13.4 KB700 / 951 / 1,650/s0.946 / 0.536 / 0.458 ms
Coloured + 16 / 20 KB in SRAM17.5 / 21.7 KB2,099 / 2,249/s0.394 / 0.400 ms
LTO + coloured + 8 / 12 / 16 KB in SRAM8.5 / 12.6 / 16.7 KB1,550 / 2,015 / 2,248/s0.440 / 0.391 / 0.416 ms
Whole network + USB libraries in SRAM48 KB1,950/s0.416 ms
Teensy 4.1, 600 MHz, stock (for comparison)–2,170/s0.193 ms
Peak UDP echo rate against extra RAM
Peak UDP echo rate against extra RAM, for a layout made from ping traffic only, from UDP + ping traffic, and with LTO.
UDP echo replies against offered rate
UDP echo replies against offered rate, mean ± 1 SD of 3 passes.

Does it generalize?

Checks of the layout outside its training conditions
CheckResult
Ping-trained layout, UDP trafficSmall gain only: 322 → 367/s (with 12 KB in SRAM: 1,050/s instead of 1,650/s). Profile the workload that matters.
Larger packets (1,000 B ping, not in the profile)Turnaround 3.31 → 1.93 ms: the same 1.38 ms saved per echo as at 56 B.
Name list only, no colouring16 KB list: 1,998/s with LTO, 1,700/s without (coloured: 2,248 and 2,099/s); 81–89 % of the gain.
Name list after a code change (network statistics enabled)Stock 284/s → 1,750/s with the unchanged list from the other build.

Colouring depends on exact function sizes and addresses, so it must be generated again for every build. A list of function names does not; it is the part that could become a reusable configuration.

What did not work

Limits and open points

Next

A natural upstream form is a list of hot function names for RP2040 networking, placed in SRAM by a Zephyr snippet or board option, with the measurements above as justification. Colouring stays an application-level tool. The PC-sampling profiler works over any SWD probe and needs no change to the firmware.

Evidence: all measurements on 1 November 2026 with the tools described above; mean of 3–9 independent runs per image.


Made with a notepad.