Skip to content

Commit 3023c47

Browse files
committed
Arduino: correct the link mode guidance and pin the board core in CI
Two users hit the same wall within an hour of the library reaching Library Manager: install it, open an example, Verify, and get Sketch too big; text section exceeds available space The README described Dynamic link mode as producing "no serial output at all, so the board looks dead", which is wrong -- it fails at compile time, so a user searching for the error they actually saw found nothing. Corrected, and moved up into the error table with the real message. Why it happens, since the README had no explanation: Dynamic does a relocatable link (-r). --gc-sections is passed in both modes but can only work in a final link, where the linker has an entry point to trace reachability from. Under -r nothing can be proven unreachable, so every vendored operator survives, and because a loadable extension is loaded into RAM the retained code is charged against RAM too. AddModel on core 0.90.0: 507,876 bytes static, 787,508 dynamic. Recorded that the library cannot work around this. Trimming the vendored operator sources to only the registered set -- 15 instead of 172 -- moved the dynamic build 852 bytes. The bulk is the runtime, flatbuffers and CMSIS-NN. Memory figures were measured on core 0.55.2, which reported a 131,072-byte RAM ceiling; 0.90.0 reports 262,144, so they were not comparable to anything a current user sees. Re-measured all three examples on 0.90.0. The KeywordSpotting arena band is now scoped to the core it was measured on, with the upper bound marked untested on 0.90.0 -- 40 KB is confirmed, above that is not. CI pins arduino:zephyr@0.90.0 and Arduino_RouterBridge@0.4.3 rather than resolving to latest, so a failure means a real incompatibility instead of an upstream release landing under us. The core moved 0.55.2 -> 0.90.0 unannounced during this work. Adds the retry and network.connection_timeout hardening the ~1 GB toolchain download needs, since that toolchain is a ~1 GB GitHub release asset that outruns arduino-cli's default HTTP timeout. Bumps library.properties to version 0.1.1. Tracking of dynamic-mode sizes belongs in meta-pytorch/executorch-arduino, on a nightly, rather than here: it is a property of the published artifact, and three extra compiles per pull request touching runtime/ or kernels/portable/ is a poor trade for telemetry this repo cannot act on. Authored with assistance from Claude Code.
1 parent c461421 commit 3023c47

3 files changed

Lines changed: 90 additions & 37 deletions

File tree

.github/workflows/_test_arduino_library.yml

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,36 @@ jobs:
6666
curl -fsSL --retry 3 --retry-delay 5 --retry-all-errors \
6767
"https://downloads.arduino.cc/arduino-cli/arduino-cli_${ARDUINO_CLI_VERSION}_Linux_64bit.tar.gz" \
6868
| tar xz -C "${ARDUINO_CI_DIR}/bin" arduino-cli
69+
# Pinned so a failure here means a real incompatibility rather than an
70+
# upstream release landing under us. Bump deliberately: the core went
71+
# 0.55.2 -> 0.90.0 without warning, and the reported RAM ceiling doubled
72+
# with it. Keep this matching what Library Manager users install.
73+
#
74+
# The Zephyr core pulls a ~1 GB toolchain from a GitHub release, which
75+
# outruns arduino-cli's default HTTP timeout often enough to matter, and
76+
# `core install` has no retry of its own.
77+
arduino-cli config init --overwrite
78+
arduino-cli config set network.connection_timeout 600s
6979
arduino-cli core update-index
70-
arduino-cli core install arduino:zephyr
71-
arduino-cli lib install Arduino_RouterBridge
7280
73-
# link_mode=static is not optional. The board defaults to Dynamic,
74-
# which builds the sketch as a Zephyr loadable extension; a library
75-
# this size never starts that way and prints nothing at all.
81+
retry() {
82+
local n=1
83+
until "$@"; do
84+
if [ "${n}" -ge 3 ]; then
85+
echo "::error::'$*' failed after ${n} attempts"
86+
return 1
87+
fi
88+
echo "attempt ${n} of 3 failed; retrying in $((n * 30))s"
89+
sleep $((n * 30))
90+
n=$((n + 1))
91+
done
92+
}
93+
retry arduino-cli core install arduino:zephyr@0.90.0
94+
retry arduino-cli lib install Arduino_RouterBridge@0.4.3
95+
96+
# link_mode=static is not optional. Dynamic is the board default and
97+
# does a relocatable link, which makes --gc-sections inert: nothing
98+
# unused is stripped and the build overflows flash.
7699
FQBN="arduino:zephyr:unoq:link_mode=static"
77100
for sketch in arduino_lib/ExecuTorch/examples/*/; do
78101
echo "::group::compile $(basename "${sketch}")"

examples/arduino/README.md

Lines changed: 61 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -247,11 +247,14 @@ A mismatch there is the bug, found in seconds instead of hours.
247247

248248
### Things that are not obvious
249249

250-
- **`link_mode=static` is mandatory.** The Uno Q defaults to Dynamic, which
251-
builds the sketch as a Zephyr loadable extension. A library this size never
252-
starts that way: no serial output at all, so the board looks dead and offers
253-
nothing to diagnose. Dynamic also reports only the extension's size, roughly
254-
half the real figure.
250+
- **`link_mode=static` is mandatory, and Dynamic is the default.** In the IDE
251+
that is `Tools > Link mode > Static`, which is per-sketch and resets every
252+
time you open another example. Dynamic does a relocatable link (`-r`), which
253+
makes `--gc-sections` inert: nothing unused is stripped, so every vendored
254+
operator survives and the build overflows flash with
255+
`Sketch too big; text section exceeds available space`. Same sketch, core
256+
0.90.0: 507,876 bytes on Static, 787,508 on Dynamic. It fails at compile
257+
time, so there is nothing to see on the serial port either way.
255258
- **`ET_LOG` has to be routed somewhere.** `zephyr.cpp` logs through `fprintf`,
256259
and `platform_stubs.c` stubs `fprintf` out. The build script rewrites the
257260
logger to call a weak `et_arduino_log` hook, which the examples implement
@@ -266,7 +269,8 @@ A mismatch there is the bug, found in seconds instead of hours.
266269

267270
| Symptom | Cause |
268271
|---|---|
269-
| No serial output at all | Built in Dynamic link mode, or `Arduino_RouterBridge` missing |
272+
| `Sketch too big; text section exceeds available space` | Built in Dynamic link mode. Set `Tools > Link mode > Static`, per sketch |
273+
| No serial output at all | `Arduino_RouterBridge` missing, or the monitor attached after `setup()` had already printed |
270274
| `Program::load` -> `0x23` | Model header put the array in a section the linker discards; use `pte_to_header.py` from this directory, not the Ethos-U one |
271275
| `load_method` -> `0x14` | Operator not in the registered set; regenerate with `ROOT_OPS=` |
272276
| `load_method` -> `0x21` | `method_pool` too small; the log line gives the exact shortfall |
@@ -474,38 +478,64 @@ currently supports (`architectures=zephyr`). Other Arduino cores do not run
474478
Zephyr and have no link mode setting; they need a platform abstraction layer
475479
port before they can compile at all, and their memory behaviour is untested.
476480

477-
On the Zephyr core, the Uno Q defaults to Dynamic link mode, which builds the
478-
sketch as a Zephyr loadable extension. Sketches this size never start that way: no serial output
479-
at all, so the board looks dead and offers nothing to diagnose. Build with
480-
`link_mode=static`. A 2 KB sketch runs fine under Dynamic, so the ceiling sits
481-
somewhere between that and these builds; it has not been pinned down.
481+
On the Zephyr core the Uno Q defaults to **Dynamic**, and every example fails to
482+
build that way. In the IDE, set `Tools > Link mode > Static`. It is a per-sketch
483+
setting: opening another example puts it back to Dynamic.
482484

483-
Dynamic also reports only the extension's own size, which reads far lower than
484-
what the board actually holds. Measured on an Arduino Uno Q, board core 0.55.2,
485-
against 786,432 bytes of flash and 131,072 bytes of RAM:
485+
Dynamic builds the sketch as a Zephyr loadable extension via a relocatable link
486+
(`-r`). `--gc-sections` is passed in both modes but can only work in a final
487+
link, where the linker has an entry point to trace reachability from. Under `-r`
488+
there is nothing to trace from and the output will be linked again later, so
489+
every section has to be kept. Nothing unused is stripped, and because a loadable
490+
extension is loaded into RAM, the retained code is charged against RAM as well
491+
as flash:
486492

487-
| Build | Flash (static) | RAM | Dynamic reported | On hardware |
488-
|-------|---------------|-----|------------------|-------------|
489-
| HelloExecuTorch | 472,728 (60%) | 3,060 (2%) | 27% | `Model loaded OK!`, 1 method |
490-
| AddModel | 507,664 (64%) | 11,252 (8%) | 30% | `[1,2,3] + 1 = [2.00, 3.00, 4.00]` |
491-
| KeywordSpotting (CMSIS-NN) | 557,520 (70%) | 46,068 (35%) | 30% | 10/10 keywords correct |
493+
| AddModel, core 0.90.0 | Flash | RAM |
494+
|---|---|---|
495+
| `link_mode=static` | 507,876 (64%) | 12,268 (4%) |
496+
| `link_mode=dynamic` | 787,508 (100%) — **overflows** | 249,021 (94%) |
497+
498+
This is not something the library can work around. Trimming the vendored
499+
operator set to only the registered ops — 15 sources instead of 172 — moved the
500+
Dynamic build by 852 bytes, still over the limit. The bulk is the runtime,
501+
flatbuffers and CMSIS-NN, all of which Static strips and Dynamic cannot.
502+
503+
Measured on an Arduino Uno Q, board core **0.90.0**, at `link_mode=static`,
504+
against 786,432 bytes of flash and 262,144 bytes of RAM:
505+
506+
| Build | Flash | RAM | On hardware |
507+
|-------|-------|-----|-------------|
508+
| HelloExecuTorch | 472,952 (60%) | 3,052 (1%) | `Model loaded OK!`, 1 method |
509+
| AddModel | 507,876 (64%) | 12,268 (4%) | `[1,2,3] + 1 = [2.00, 3.00, 4.00]` |
510+
| KeywordSpotting (CMSIS-NN) | 563,672 (71%) | 47,084 (17%) | detects `yes`, logit 8.95 |
511+
512+
Core 0.55.2 reported a 131,072-byte RAM ceiling; 0.90.0 reports 262,144. Figures
513+
from before that change are not comparable.
492514

493515
All CMSIS-NN sources are compiled, but the linker's
494516
`--gc-sections` discards unused functions from the final binary.
495517

496-
RAM is the binding constraint, not flash. Zephyr reserves 32 KB of main stack
497-
and a 32 KB heap out of 128 KB before the sketch gets any, and the arena the
498-
sketch hands to `MemoryManager` comes out of what remains. KeywordSpotting's
499-
DS-CNN plans 16 KB of buffers but needs considerably more for the method's own
500-
structures, which leaves a usable band rather than a floor to clear:
518+
RAM is the binding constraint, not flash. Zephyr reserves a main stack and a heap
519+
before the sketch gets any, and the arena the sketch hands to `MemoryManager`
520+
comes out of what remains. KeywordSpotting's DS-CNN plans 16 KB of buffers but
521+
needs considerably more for the method's own structures, which leaves a usable
522+
band rather than a floor to clear.
523+
524+
Measured on core **0.55.2**, where Zephyr took a 32 KB stack and a 32 KB heap out
525+
of 128 KB:
501526

502527
| Arena | Result |
503528
|-------|--------|
504529
| 28 KB | `load_method` fails, `MemoryAllocationFailed` (0x21), 180 B short |
505-
| 40 KB | Works. 46,068 bytes of globals (35%) — what the table above measures |
506-
| 64 KB | Pushes globals to 70,644 and past Zephyr's reservation; may appear to run, but it is overrunning memory |
507-
508-
The sketch ships 40 KB for that reason. Growing it is not automatically safer:
509-
`arduino-cli` reports RAM against the full 131,072 bytes and knows nothing about
510-
Zephyr's stack and heap, so a 64 KB arena still reports a comfortable 53% while
511-
already overlapping reserved memory. Read the arena as bounded on both sides.
530+
| 40 KB | Works. What the sketch ships |
531+
| 64 KB | Globals reach 70,644, past Zephyr's reservation. It ran and printed the right answer, which is what makes it dangerous rather than safe |
532+
533+
The sketch ships 40 KB for that reason, and growing it is not automatically
534+
safer: `arduino-cli` reports RAM against the whole region and knows nothing about
535+
Zephyr's stack and heap, so the 64 KB build still reported a comfortable 53%
536+
while overlapping reserved memory. Read the arena as bounded on both sides.
537+
538+
**The upper bound has not been re-measured on core 0.90.0**, which reports twice
539+
the RAM (262,144). 40 KB is confirmed working there — 47,084 bytes of globals,
540+
17% — but whether the ceiling moved with the reported total is unverified. Treat
541+
anything above 40 KB as untested on 0.90.0.

examples/arduino/library.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
# LICENSE file in the root directory of this source tree.
66

77
name=ExecuTorch
8-
version=0.1.0
8+
version=0.1.1
99
author=Meta Platforms
1010
maintainer=ExecuTorch Team <executorch@meta.com>
1111
sentence=Run PyTorch models on Arduino microcontrollers with ExecuTorch.

0 commit comments

Comments
 (0)