STM32 Porting Guide First Pass

Hey all,

Put together a first pass at a porting guide for STM32 projects to Zig’s compiler:

I have grander aspirations of potentially adding a general “porting” section to zig.guide if there’s interest!

7 Likes

Thanks for putting this together. I just got a discovery board and wanted to start tinkering with it.

1 Like

Nice tutorial @haydenridd !

Note with my GCC version (12.3.1)

  • syscall.c: It is not needed when nosys lib is included
    and optional implementation is possible if not (weak functions)
  • sysmem.c : I need to implement _sbrk if nosys lib is not included

Float formatting support

I was able to enable float support by adding this line to my build.zig config

elf.forceUndefinedSymbol("_printf_float"); // Allow float formatting (printf, sprintf, ...)

Question

  • If you use libnosys.a, malloc will not work. Am I right ?
  • You don’t mention of libg_nano.a but I see that is included by GCC in my case at linking stage. What is the purpose of this library ?
  • Do you think that compile and provide this own libc (newlib, nano, …) can it be a part of this tutorial ? I have always wondered how difficult this task was.

Good callout, I was originally using the compiler version that comes “bundled” with the STM32CubeMX stuff, but I think I’ll update the tutorial to use the latest official release from ARM. Yup, essentially the sys**.c files provided by ST provide similar functionality to the “nosys” library. I actually ran into some interesting duplicate symbol errors using the Zig compiler because of this that wasn’t present in GCC. I believe zig cc/clang handles “weak” symbols differently than GCC which is a whole other nightmare I haven’t started digging into yet :slight_smile:

Whoa… I’ll hand it to newlib that is a proper cursed way of enabling an option via the linker :joy: You may want to read up on this it looks like you may want to tread lightly using floats with newlib’s printf() implementation.

Questions:

  • Sigh, this has been the source of so many headaches for me. TLDR, even with nosys newlib ships with a functional malloc implementation (and when you get down to the nitty gritty functional _sbrk implementation) that depends on having linker symbols that specify where the heap starts/ends (I think something like __heap_start__ and __heap_end__). So “malloc” may or may not work correctly depending on if you have the linker symbols it wants. And to find those you’ll have to go to the newlib source code and see what it wants. I believe since ST doesn’t define these symbols in their generated linker script, but use their own (_estack), you’ll want to stick with their implementation in sysmem.c. In general I would be wary of using malloc with this implementation as it is super simple and really is only suitable for allocating all your things at program start and never touching them after. Why dynamic allocation is tricky on MCU’s is a whole other topic, look up what a “Memory Management Unit” is and note that generally MCUs don’t have these.
  • Nice spot! Hadn’t seen this before but after some research it appears that’s just newlib-nano compiled with debug symbols included.
  • I think that would be super interesting. I think I would probably choose to offer a guide on compiling/using an alternative standard lib like this one someone on the Zig discord pointed me towards. Eventually we may even have a libc written in Zig we could compile/link against.

EDIT:
I went ahead and dug into the newlib source since I was curious. ST actually DOES provide the correct linker symbol, which in this case is end. Here’s the snippet from newlib source that uses it:

oid *
_sbrk (incr)
     int incr;
{
   extern char   end; /* Set by linker.  */
   static char * heap_end;
   char *        prev_heap_end;

   if (heap_end == 0)
     heap_end = & end;

   prev_heap_end = heap_end;
   heap_end += incr;

   return (void *) prev_heap_end;
}

And the section in the linker script that provides it:

  /* User_heap_stack section, used to check that there is enough RAM left */
  ._user_heap_stack (NOLOAD) :
  {
    . = ALIGN(8);
    PROVIDE ( end = . );
    PROVIDE ( _end = . );
    . = . + _Min_Heap_Size;`
    . = . + _Min_Stack_Size;
    . = ALIGN(8);
  } >RAM

So malloc will work out of the box even without ST’s sysmem.c.

I believe zig cc/clang handles “weak” symbols differently than GCC which is a whole other nightmare I haven’t started digging into yet :slight_smile:

me too :smiley:

it looks like you may want to tread lightly using floats with newlib’s printf() implementation.

O, nice topic. Implementing my own printf/sprintf can be nice to save flash memory indeed.
With forceUndefinedSymbol("_printf_float"), I wanted to know how to do the equivalent of gcc option: -u _printf_float

I would probably choose to offer a guide on compiling/using an alternative standard lib like this one someone on the Zig discord pointed me towards. Eventually we may even have a libc written in Zig we could compile/link against.

Picolibc is super interesting. I already tried to integrate it on STM32 without success, a long time ago. I’m going to take advantage of my learning on Zig to try again !

Optimization

I was able to reduce the output bin to close/equivalent to the GCC build by adding optimization level flag when compile C source files (ex:-0g)
By compiling the guide example, the output binary size from 18Kb is reduced to 6.3Kb

EDIT: LTO

I got warning by setting elf.want_lto to true:

ld.lld: warning: Linking two modules of different target triples: '[...]/stm32l4_test.elf.o' is 'thumb-unknown-unknown-eabihf' whereas 'ld-temp.o' is 'thumbv7em-unknown-unknown-eabihf'

It is tracked on zig repo here

@Sazerac + anyone else this might be relevant to:
Did some more digging, and fortunately I take back what I said about weak symbols, I believe zig/clang handles them more or less the same as GCC which is nice! My confusion came from the following behavior which I tracked down:

  • Compiling a program where a file syscalls.c defines neccessary system function symbols (_exit, _kill, etc.) AND a pre-compiled library (libnosys) is included that ALSO defines stubs for these symbols:
    • Standard Makefile with arm-none-eabi-gcc compiler/linker: Compiles + links fine
    • Modified Makefile with zig cc as the compiler/linker: Compiles + links fine
    • Equivalent build.zig file, compiles, but fails linking due to duplicate symbols

Took a while to figure out, but basically when linking, if you pass in your objects that define a symbol before a library that has that same symbol, it will simply not include the duplicate symbol and use yours. However, if you pass in the library first and then your object files, you get a duplicate symbol error since it nabbed the symbol from the library, then found your symbol (which it won’t ignore). Confirmed that the low level call to ld.lld was including the library files before my object files in the arg list, and when I switched the order manually, it linked fine.

1 Like

Updated repo with a couple of learnings + support for 0.13.0!