-
Notifications
You must be signed in to change notification settings - Fork 70
Description
TLDR;
Fix linking with LTO in Arduino IDE v2.x using this nice solution by @dfleck : Move weak handlers from startup_ch32v00x.S to ch32v00x_misc.c
Edit: See PR #215 for this fix.
Less user friendly method for Windows without code-changes:
cd %TEMP%/arduino/cores/<latest core folder>
"%LOCALAPPDATA%\\Arduino15\\packages\\WCH\\tools\\riscv-none-embed-gcc\\8.2.0/bin/riscv-none-embed-ar" mb "HardwareSerial.cpp.o" "core.a" "startup_ch32yyxx.S.o"
Background story
One irritating issue I encountered in many CH32V003 projects is that the compiled flash rapidly grows too large. The flash space of the V003 (and the V002) is limited to 16kB, which is filled easily, even if only using a few of the peripherals and a little bit of custom code. Having insufficient room severely limits use of the Arduino environment for these otherwise nice and affordable chips.
As mentioned in this comment, one of the ways to reduce flash usage, is to use Link Time Optimization (LTO). LTO removes code that is never called. This optimization worked great in version 1.8.19 of the Arduino IDE, but didn't work in any v2.x IDE (see also another comment). Since I like using 2.x for the interactive debugger, I also want it to make the smallest optimized hex-file possible, using optimization option "Smallest (-Os) with LTO".
In the past period I've been looking into this issue and recently got a bit further. I'll use this issue to document my progress.
Digging deeper into LTO
When comparing the output with IDE 1.8.19 and the resulting .map files, the mere order of object files seems to make LTO not include critical pieces of code (e.g. interrupt handlers). When testing a simple Blinky with LTO in IDE 2.3.4, it does execute the first digitalWrite(), but then blocks.
The most important missing parts for the Blink sketch seemed to be the SysTick_Handler
. The .map file only showed an empty (2 byte) handler. Some other handlers were also empty, such as EXTI7_0_IRQHandler
.
Looking for these handlers in the source code revealed a .weak declaration in startup_ch32yyxx.S
and the actual implementation in core files such as clock.c
and interrupt.cpp
.
Finding cause
After months of googling didn't get me any further, I finally seemed to get on the right track. Using keywords ".weak LTO" I found a known GCC linking issue: LTO removes C functions declared as weak in assembler. Sounded just like my issue and it confirmed my earlier observation that object order makes a difference. In short: the startup object should be first, and the actual implementation somewhere later.
Changing order
To further confirm this, I tried to change that order and compare the resulting size and map files. In Windows the Arduino IDE v2.x stores the core archive in the Temp folder under C:\\Users\\<name>\\AppData\\Local
. The GCC tools are somewhere down in the Arduino15 subfolder. For easy typing environment variables %TEMP%
and %LOCALAPPDATA%
can be used.
To change the order of an object in the compiled core.a archive on my Windows 11 PC, I used these commands;
dir %TEMP%\arduino\cores\
cd %TEMP%\arduino\cores\<latest core folder>
"%LOCALAPPDATA%\\Arduino15\\packages\\WCH\\tools\\riscv-none-embed-gcc\\8.2.0/bin/riscv-none-embed-ar" m "core.a" "clock.c.o"
That m
option of the ar
command moves the clock.c
object to the end of the core archive. Another important object could be interrupt.cpp.o
, but that depends on the objects actually used by the sketch.
Better order
For a more generic fix it is better to have the startup object be moved to the start of the core archive. This command moves the startup object to before clock.c.o (which in my project came before interrupt.cpp.o):
"%LOCALAPPDATA%\\Arduino15\\packages\\WCH\\tools\\riscv-none-embed-gcc\\8.2.0/bin/riscv-none-embed-ar" mb "clock.c.o" "core.a" "startup_ch32yyxx.S.o"
In another sketch HardwareSerial.cpp.o
came first, so perhaps the best way is to find the first object using this command:
"%LOCALAPPDATA%\\Arduino15\\packages\\WCH\\tools\\riscv-none-embed-gcc\\8.2.0/bin/riscv-none-embed-ar" tov "core.a"
FYI - If you want to look into the compiled sketch, you can see a disassembly using objdump
:
cd %TEMP%\arduino\sketches\<sketch folder>
"%LOCALAPPDATA%\\Arduino15\\packages\\WCH\\tools\\riscv-none-embed-gcc\\8.2.0/bin/riscv-none-embed-objdump" -d -S "Blink.ino.elf" > dump.txt
Confirmation
After moving objects within the core,a archive, I recompiled and uploaded my Blink sketch, which used the modified core archive. The table below shows the flash size of the Blink.ino example sketch, compiled for the CH32V003 using various optimization options and all peripherals disabled.
Option | Flash usage | RAM usage | result |
---|---|---|---|
-Os | 2724 (16%) | 240 (11%) | blinks fine |
-Os LTO (unchanged) | 872 (5%) | 152 (7%) | not blinking |
-Os LTO (moved clock.c.o) | 920 (5%) | 152 (7%) | it blinks! |
-Os LTO (moved startup.S.o) | 1076 bytes (6%) | 152 (7%) | still blinks |
FYI: the same example compiled for V003 using IDE v1.8.19 also resulted in 1076 bytes (6%) flash usage and 152 bytes (7%) RAM usage. Most likely moving the startup object to be first, makes it contains a few more handlers (and possibly some other code) compared to solely moving clock.c.o to the end.
Conclusion
As shown, the size difference is significant. For the standard Blink example, moving the clock object seems sufficient to get it working. After moving the object the resulting flash was slightly larger. The .map file now showed a realistic size for SysTick_Handler
.
At the moment I have not (yet) found a way to make this a permanent fix in platform.txt or board.txt files. The LTO .weak issue is supposedly fixed in a newer version of the GCC suite, but I've not looked deep into that,
I mainly tested the V003, new findings with V002/V006 are listed in comments below.
Edit: @dfleck found a better solution that doesn't require board/platform changes: Move weak handlers from startup_ch32v00x.S to ch32v00x_misc.c
(TODO: make PR for this fix).