Skip to content

rp2: Build with nano.specs, add linker cref table#19299

Open
projectgus wants to merge 2 commits into
micropython:masterfrom
projectgus:feature/rp2_nano_specs
Open

rp2: Build with nano.specs, add linker cref table#19299
projectgus wants to merge 2 commits into
micropython:masterfrom
projectgus:feature/rp2_nano_specs

Conversation

@projectgus

@projectgus projectgus commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Passes --specs=nano.specs on the gcc compiler & linker command line for rp2, meaning newlib-nano is used instead of regular newlib. As mentioned by @Gadgetoid in #11143. This reduces both the binary size and (significantly) the static RAM usage. Should also save some RAM at runtime, particularly the stack footprint of libc printf.

Includes a commit to add the --cref linker argument, which adds a cross-reference table in the linker map file. This is useful to figure out how/why certain symbols are linked.

The specific libc pieces which are linked in the default rp2 build seem to be malloc/free/realloc and some printf support.

  • The malloc/free/realloc looks like it's pulled in via pico_util queue.c and time.c, and operator new & delete (unclear why these are pulled in at all!) Can find via __wrap_free in the cross-reference table. This feels like with a small amount of work it could be removed, maybe...? Particularly as the default rp2 build has no C heap available.
  • snprintf looks like it's being pulled in via some mbedTLS features, shared/readline, and shared/netutils. Can find via __wrap_snprintf in the cross-reference table. This feels like it'd be harder to remove the dependency for.

I don't think any of these use cases require "full" newlib support.

The best description I know for gcc specs and nano vs normal newlib is https://metebalci.com/blog/demystifying-arm-gnu-toolchain-specs-nano-and-nosys/

Testing

  • Ran the default unit tests on RPI_PICO2.

Trade-offs and Alternatives

  • Keeping default newlib is the safer path, particularly if there are any C usermodules that happen to depend on "full" libc features. However, MicroPython itself doesn't depend on much libc at all.

Generative AI

I did not use generative AI tools when creating this PR.

@projectgus projectgus changed the title Feature/rp2 nano specs rp2: Link with nano.specs, add linker cref table Jun 3, 2026
@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown

Code size report:

Reference:  esp32: Rename the sdkconfig.spiram_xxx config snippets. [7e55a73]
Comparison: rp2: Build with nano.specs for newlib-nano. [merge of c65b256]
  mpy-cross:    +0 +0.000% 
   bare-arm:    +0 +0.000% 
minimal x86:    +0 +0.000% 
   unix x64:    +0 +0.000% standard
      stm32:    +0 +0.000% PYBV10
      esp32:    +0 +0.000% ESP32_GENERIC
     mimxrt:    +0 +0.000% TEENSY40
        rp2: -1172 -0.127% RPI_PICO_W[incl -720(bss)]
       samd:    +0 +0.000% ADAFRUIT_ITSYBITSY_M4_EXPRESS
  qemu rv32:    +0 +0.000% VIRT_RV32

@dpgeorge

dpgeorge commented Jun 4, 2026

Copy link
Copy Markdown
Member

Thanks for doing this. I like the idea, and on RPI_PICO_W it saves 1624 bytes of flash and 960 bytes of RAM.

But... the tests/net_hosted/ssl_verify_callback.py now locks up! Also tests/net_inet/asyncio_tls_open_connection_readline.py and probably others (these were just the first two I found). Note that some SSL tests still work though.

snprintf looks like it's being pulled in via some mbedTLS features

I guess that's the cause of the crashes? I tried adding shared/libc/printf.c to the build to fix that (and noted that MICROPY_USE_INTERNAL_PRINTF is already enabled) but that didn't fix it. I think anyway shared/libc/printf.c is already included in the cmake builds due to it being in MICROPY_SOURCE_EXTMOD.

I'd be interested to know exactly what mbedTLS is pulling in from libc that doesn't work with newlib-nano... it's a bit scary that we don't fully understand the dependencies here, especially since ports like stm32 work just fine with -nostdlib (libc is provided by our own code in shared/libc/ for that port).

@Gadgetoid I'm surprised you didn't see failures with your firmware when using nano.specs. Did you test SSL with your firmware?

@projectgus

Copy link
Copy Markdown
Contributor Author

But... the tests/net_hosted/ssl_verify_callback.py now locks up! Also tests/net_inet/asyncio_tls_open_connection_readline.py and probably others (these were just the first two I found). Note that some SSL tests still work though.

Oh wow, sorry! I didn't realise those tests weren't part of the default run-tests.py coverage!

I wonder if it's to do with linker placement instead of the actual features, there might be something that's now reading from cached flash. I'll do a bit of spelunking and try to figure this out.

@projectgus projectgus marked this pull request as draft June 4, 2026 06:48
@dpgeorge

dpgeorge commented Jun 4, 2026

Copy link
Copy Markdown
Member

I didn't realise those tests weren't part of the default run-tests.py coverage!

You mentioned you ran the tests on RPI_PICO2, which won't be able to run the network tests anyway.

Eventually octoprobe will help here, by just running all tests regardless. But also probably we should think about merging #16112; I use it all the time now (and is how I tested this PR).

Useful to see why particular symbols have been pulled in.

This work was funded through GitHub Sponsors.

Signed-off-by: Angus Gratton <angus@redyak.com.au>
@projectgus

projectgus commented Jun 10, 2026

Copy link
Copy Markdown
Contributor Author

Ah, OK... Here's a stack trace from a hanging test (which has actually panicked):

#3  check_alloc (mem=<optimized out>, size=<optimized out>)
    at /home/gus/ry/george/micropython/lib/pico-sdk/src/rp2_common/pico_malloc/malloc.c:60
#4  0x100719fa in __wrap_malloc (size=36) at /home/gus/ry/george/micropython/lib/pico-sdk/src/rp2_common/pico_malloc/malloc.c:79
#5  0x10074b10 in gmtime (tim_p=tim_p@entry=0x20041708) at ../../../../../../newlib-4.5.0.20241231/newlib/libc/time/gmtime.c:61
#6  0x10060bd2 in mbedtls_platform_gmtime_r (tt=tt@entry=0x20041708, tm_buf=tm_buf@entry=0x20041714)
    at /home/gus/ry/george/micropython/lib/mbedtls/library/platform_util.c:194
#7  0x1006ac2c in mbedtls_x509_time_gmtime (tt=<optimized out>, now=now@entry=0x2004178c)
    at /home/gus/ry/george/micropython/lib/mbedtls/library/x509.c:1057
#8  0x1006b470 in x509_crt_verify_chain (rs_ctx=<optimized out>, f_ca_cb=0x0, p_ca_cb=0x0, ca_crl=<optimized out>, crt=0x20018078, 
    trust_ca=0x20011294, profile=0x100d425c <mbedtls_x509_crt_profile_default>, ver_chain=0x200417e4)
    at /home/gus/ry/george/micropython/lib/mbedtls/library/x509_crt.c:2531

I believe what happens is that in the "nano" configuration struct _reent is made smaller to save RAM. It doesn't contain the _localtime_buf structure by default. When mbedTLS calls through to gmtime(), newlib expands the macro _REENT_CHECK_TM(reent); which is a no-op on "big" newlib but on newlib nano it does an on-demand allocation of the _localtime_buf structure for this thread.

Because MicroPython on rp2 has no C heap by default, this malloc fails and the pico-sdk malloc implementation panics.

Ironically we already patch out localtime() support in ports/rp2/datetime_patch.c so I think this buffer will never be used, but there's no easy way to change it out. EDIT: Can patch out easily by telling mbedTLS to call the reentrant gmtime_r

@projectgus

Copy link
Copy Markdown
Contributor Author

there's no easy way to change it out.

It looks like the way around this is to do what stm32 does for mbedtls_port.c and provide our own gmtime implementation layered on top of timeutils. Will take a crack at this, it should drop out a bunch of the other libc code which is being pulled in.

@dpgeorge

Copy link
Copy Markdown
Member

Because MicroPython on rp2 has no C heap by default, this malloc fails and the pico-sdk malloc implementation panics.

Interesting. Maybe that's why @Gadgetoid did not see any failures, if the C heap was enabled.

@projectgus projectgus force-pushed the feature/rp2_nano_specs branch from b353960 to 52ee6ff Compare June 10, 2026 01:24
@projectgus

Copy link
Copy Markdown
Contributor Author

This was easier than I thought, just need to patch mbedTLS to call gmtime_r instead of gmtime().

Before marking this as ready I'm going to audit the remaining code paths that lead to malloc(), though. From the cross-ref table:

__wrap_malloc                                     CMakeFiles/firmware.dir/home/gus/ry/george/micropython/lib/pico-sdk/src/rp2_common/pico_malloc/malloc.c.o
                                                  /usr/lib/gcc/arm-none-eabi/14.2.0/../../../../arm-none-eabi/lib/thumb/v6-m/nofp/libg_nano.a(libc_a-__atexit.o)
                                                  CMakeFiles/firmware.dir/home/gus/ry/george/micropython/lib/pico-sdk/src/rp2_common/pico_cxx_options/new_delete.cpp.o
                                                  CMakeFiles/firmware.dir/home/gus/ry/george/micropython/lib/pico-sdk/src/common/pico_time/time.c.o

(There's also possibly some follow-up work we could do to replace all of libc time functions with timeutils on rp2, would possibly save some code size.)

@projectgus projectgus force-pushed the feature/rp2_nano_specs branch from 52ee6ff to 54511f1 Compare June 10, 2026 01:30
@octoprobe-bot

Copy link
Copy Markdown

Octoprobe PR report

Test Tests
passed
Tests
skipped
Tests
xfailed
Tests
failed
format flash 5
run-tests.py 4723 563 1
run-tests.py --via-mpy --emit native 4658 628 1
run-tests.py --via-mpy 4724 563
run-perfbench.py 120
run-natmodtests.py 180 23 2
run-tests.py --test-dirs=extmod_hardware 9 25 11
run-tests.py --test-dirs=extmod_hardware --emit-native 9 25 11
Total 14428 1827 24 2
Failures

Group: run-tests.py

Test rp2
5334-
RPI_PICO2
rp2
5334-
RPI_PICO2-
RISCV
rp2
552b-
RPI_PICO2_W
rp2
5f2c-
RPI_PICO_W
rp2
6038-
RPI_PICO_W
extmod/socket_udp_nonblock.py skip skip pass pass FAIL

Group: run-tests.py --via-mpy --emit native

Test rp2
5334-
RPI_PICO2
rp2
5334-
RPI_PICO2-
RISCV
rp2
552b-
RPI_PICO2_W
rp2
5f2c-
RPI_PICO_W
rp2
6038-
RPI_PICO_W
extmod/socket_udp_nonblock.py skip skip pass pass FAIL

@dpgeorge

Copy link
Copy Markdown
Member

The above octoprobe failures of extmod/socket_udp_nonblock.py can be ignored.

@projectgus

Copy link
Copy Markdown
Contributor Author

Before marking this as ready I'm going to audit the remaining code paths that lead to malloc(), though. From the cross-ref table:

As far as I can tell, the other cross-references shown here aren't actually being linked into the final binary. At least, it doesn't look like it - and if I try to stub out calls like atexit() then it doesn't actually change the firmware size. So maybe a bug in the cross-reference table generation when gc-sections is enabled? Not really sure.

In any case, with the green Octoprobe run I think this is ready.

@projectgus projectgus marked this pull request as ready for review June 10, 2026 05:43
@projectgus projectgus force-pushed the feature/rp2_nano_specs branch from 54511f1 to df352bd Compare June 10, 2026 05:58
@projectgus projectgus changed the title rp2: Link with nano.specs, add linker cref table rp2: Build with nano.specs, add linker cref table Jun 10, 2026
@projectgus

Copy link
Copy Markdown
Contributor Author

Updated the PR to also add --specs=nano.specs on the compiler command line, as this adds the newlib-nano standard directory to the search path.

This doesn't seem to actually change anything in the build that I can see, but it's good practice. Ideally we'd also pass it when building pico-sdk files, but I can't find a clean way to do this (probably should talk to the pico-sdk maintainers about adding support properly).

Comment thread ports/rp2/mbedtls/mbedtls_port.c Outdated

@dpgeorge dpgeorge left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good now. I tested on RPI_PICO2_W in RISCV mode, running the network tests, and it passes.

@projectgus projectgus force-pushed the feature/rp2_nano_specs branch 2 times, most recently from 358a27b to de7adc0 Compare June 10, 2026 06:54
Reduces binary, static RAM, and (probably) runtime memory usage.

Necessary mbedTLS fix to prevent it calling the non-reentrant gmtime(),
which allocates the time buffer on demand with newlib-nano.

This work was funded through GitHub Sponsors.

Signed-off-by: Angus Gratton <angus@redyak.com.au>
@projectgus projectgus force-pushed the feature/rp2_nano_specs branch from de7adc0 to c65b256 Compare June 10, 2026 06:56
@Gadgetoid

Gadgetoid commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

if the C heap was enabled.

Plausible. I had the C heap enabled long before experimenting with this.

Edit: Note with respect to catching panics on Pico/Pico 2, I proposed this: https://github.com/orgs/micropython/discussions/18736 - I believe this might make a test fail with a useful message (depending on the verbosity of the panic) and avoid needing to sleuth quite so much in cases like this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants