Exploring Zig on STM32 with freeRTOS

Hi everybody,

I share with you the experience I had in order to run FreeRTOS on an embedded device (stm32) with Zig.
Here is the repository with some blinky leds examples (with and without freertos)

Context

These examples utilize the STM32CubeMX code generator as well as the drivers provided by ST.
I wanted to see how well the Zig language can be integrated into an existing C codebase that use theses and the complexity of integrating it

Run Zig

The Code Generator will initialize any necessary peripheral system and then run our application. I then want to start my code in Zig. To do this, nothing could be simpler.

Add this to the main.c :

extern void zig_entrypoint(void);
...
zig_entrypoint(); //This function never return

here, the main.zig need :

export fn zig_entrypoint() void {
  //code here (don't return)
}

Now, let’s do some C with zig

HAL Drivers

To blinker the led, we will use the HAL library provided by ST. Let’s import them!

const c = @cImport({
    @cDefine("USE_HAL_DRIVER", {});
    @cDefine("STM32L476xx", {});
    @cInclude("main.h"); //It will include HAL and some pin definition from nucleo
});

First problem :

stm32l476_nucleo_blinky_freertos_zig/.zig-cache/o/cb530c3ca305f0acc6cbaafe4137f394/cimport.zig:182:36: error: use of undeclared identifier '__copy_table_t'
extern const __copy_table_start__: __copy_table_t;
                                   ^~~~~~~~~~~~~~
stm32l476_nucleo_blinky_freertos_zig/.zig-cache/o/cb530c3ca305f0acc6cbaafe4137f394/cimport.zig:183:34: error: use of undeclared identifier '__copy_table_t'
extern const __copy_table_end__: __copy_table_t;
                                 ^~~~~~~~~~~~~~
stm32l476_nucleo_blinky_freertos_zig/.zig-cache/o/cb530c3ca305f0acc6cbaafe4137f394/cimport.zig:184:36: error: use of undeclared identifier '__zero_table_t'
extern const __zero_table_start__: __zero_table_t;
                                   ^~~~~~~~~~~~~~
stm32l476_nucleo_blinky_freertos_zig/.zig-cache/o/cb530c3ca305f0acc6cbaafe4137f394/cimport.zig:185:34: error: use of undeclared identifier '__zero_table_t'
extern const __zero_table_end__: __zero_table_t;

After looking the issue here: Failed to include `cmsis_gcc.h` because c-translation puts extern variables outside of inline function. ¡ Issue #19687 ¡ ziglang/zig ¡ GitHub
It is seem that some C feature are not yet well translated when we import C code.
In this issue we got a workaround : It will disable the generation of the problematic code. We don’t need it because this code has already been executed long before entering the zig function

  • Import HAL v2
const c = @cImport({
    @cDefine("USE_HAL_DRIVER", {});
    @cDefine("STM32L476xx", {});
    @cDefine("__PROGRAM_START", {}); //bug: https://github.com/ziglang/zig/issues/19687
    @cInclude("main.h");
});

Let’s implement FreeRTOS now.

FreeRTOS

Same as for the drivers, I take what is provided by the manufacturer. I’m adding that to my build.zig. Let’s compile the program :slight_smile:

stm32l476_nucleo_blinky_freertos_zig/Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM4F/port.c:445:3: error: instruction requires: fp registers
 " vstmdbeq r0!, {s16-s31}    \n"
  ^
<inline asm>:9:2: note: instantiated into assembly here
stm32l476_nucleo_blinky_freertos_zig/Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM4F/port.c:447:3: error: incorrect condition in IT block; got 'al', but expected 'eq'
 " stmdb r0!, {r4-r11, r14}   \n" /* Save the core registers. */
  ^
<inline asm>:11:7: note: instantiated into assembly here
...

Damn, it seems that zig (clang?) doesn’t recognize these assembly instructions. However, I don’t have a problem with gcc. Let’s dig :

I don’t know if the previous issues are all related, but it seems that the CPU settings have not been properly passed to clang. I tested to compile the source code (without zig) with clang-18 with the help of this DropinZigccMakefile. I just replaced zig cc with clang-18. The compilation works…

Well I still want to add freeRTOS! If zig can’t compile the code, then let’s gcc or clang do it. For this I will use the cmake script that I use to compile my stm32 programs, then I will generate a library. The script will generate a libfreertos.a that I will include in my zig project.

Now it is compiling. Les’t import the library in my main.zig

const os = @cImport({
    @cInclude("FreeRTOS.h");
    @cInclude("task.h");
});

No problem!

Blinky code

Let try everything together now

export fn zig_task(params: ?*anyopaque) callconv(.C) void {
    _ = params;

    while (true) {
        c.HAL_GPIO_WritePin(c.LD2_GPIO_Port, c.LD2_Pin, c.GPIO_PIN_RESET);
        os.vTaskDelay(200);
        c.HAL_GPIO_WritePin(c.LD2_GPIO_Port, c.LD2_Pin, c.GPIO_PIN_SET);
        os.vTaskDelay(200);
    }
}
export fn zig_entrypoint() void {

    _ = os.xTaskCreate(zig_task, "Blinky", 256, null, 15, null);
    os.vTaskStartScheduler(); //Start the app
    unreachable;
}

It is working !!! What a nice blinky led !

Conclusion

I’ve been working for a long time in embedded systems on microcontrollers.
I’ve always done C and give up the idea of doing C++ for the additional problems it brought me without exceptional additions.
Despite the bugs and not knowing the language zig well yet, I found it simple and intuitive to integrate it into C code and vice versa, not to mention all the advantages brought by the modernity of the language.
Now that I understand how to generate programs for microcontrollers, I’m going to start learning the language very seriously!

Bonus and notes

  • Not investigate it yet but if I want to use these macros: os.portTICK_PERIOD_MS or pdMS_TO_TICKS() example : os.vTaskDelay(os.pdMS_TO_TICKS(200));, I got this error:
zig-linux-x86_64-0.13.0/lib/std/zig/c_translation.zig:448:13: error: Cannot promote `u32`; a C ABI type is required
            @compileError("Cannot promote `" ++ @typeName(T) ++ "`; a C ABI type is required");
            ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
zig-linux-x86_64-0.13.0/lib/std/zig/c_translation.zig:484:39: note: called from here
    const A_Promoted = PromotedIntType(A);
                       ~~~~~~~~~~~~~~~^~~
zig-linux-x86_64-0.13.0/lib/std/zig/c_translation.zig:542:60: note: called from here
    pub fn div(a: anytype, b: anytype) ArithmeticConversion(@TypeOf(a), @TypeOf(b)) {

Problem when translating C to zig ?

  • If I couldn’t compile the assembly code with the specific FPU instructions, I imagine any operation with the floats won’t use them?
    This part seem relevant to it :
.cpu_features_add = std.Target.arm.featureSet(&[_]std.Target.arm.Feature{std.Target.arm.Feature.}),

I don’t seem to have seen the clang equivalent of these parameters: -mfpu=fpv4-sp-d16 -mfloat-abi=hard

6 Likes

Great! I want to dive into the FreeRTOS and your post was appreciated! :slight_smile:

Great work! Stuff like this is exactly what we need to “pipe clean” this sort of process and bring all the headaches to light :slight_smile:

I’ve dug into this pretty deeply after cloning your repo, and all sorts of golden learnings came from this. But first, the quick fix for you FreeRTOS compilation problem:

  • I was able to successfully build by adding (you were on the right track) the following “cpu feature” to your std.zig.CrossTarget declaration:
    .cpu_features_add = std.Target.arm.featureSet(&[_]std.Target.arm.Feature{std.Target.arm.Feature.vfp4d16sp}),
  • You can also now ditch "-mfloat-abi=hard", "-mfpu=fpv4-sp-d16" from your c_sources_compile_flags .

But, buckle up, because now we enter a deep dive on why this didn’t work to begin with using your original settings (because it “should have”), it ended up being more complicated than I thought.

Architecture Related Compile Flags

Why were we getting a compiler error when specifying the flags “GCC style” via -mfloat-abi=hard and -mfpu=fpv4-sp-d16? Because those ARE valid flags to pass to Clang and it will treat them like you expect. Well, after some digging I realized via inspecting a verbose build with zig build --verbose-cc, that Zig passes ALL possible Clang “cpu features” to the call to clang during compilation, and either “adds” (+feature) or “removes” (-feature) them based on what you’ve selected in your target definition.

The issue is when you don’t specify a floating point feature directly in your target, that means you get a call to clang for compilation that looks something to the effect of:
/usr/local/bin/zig clang main.c ...[lots of flags/features]... -Xclang -vfp4d16sp [removes this feature!] ...[lots of flags/features]... (my manually specified flags:) -std=c11 -mfpu=fpv4-sp-d16 -mfloat-abi=hard

It appears that the removal of the floating point cpu feature by default -Xclang -vfp4d16sp is overriding the addition of it later via the “GCC style” flags -mfpu=fpv4-sp-d16 -mfloat-abi=hard. So, given that your FreeRTOS port requries hardware floating point (since it uses hardware floating point assembly instructions), the compilation then fails.

And to put the cherry on top, FreeRTOS even “attempts” to check for floating point support with this line in port.c:

#ifndef __VFP_FP__
	#error This port can only be used when the project options are configured to enable hardware floating point support.
#endif

Hilariously, though, that macro is a deprecated macro that used to be used to check for an ancient floating port format differentiation, and now is just constantly set to 1 independent of compile settings for both GCC and Clang compilers:
https://reviews.llvm.org/D100372

So, I will be doing the following all from this single post:

5 Likes

Thank you !
It is work and it feels cleaner !
Very interesting to understand how zig passes the parameters to clang.

While adding the option, an error occurred

zig-linux-x86_64-0.13.0/lib/std/zig/system.zig:294:45: 0x10dcd60 in resolveTargetQuery (build)
    if (query.os_version_min) |min| switch (min) {

I have removed the problematic lines :

        .os_version_min = undefined,
        .os_version_max = undefined,

I don’t think this is normal behavior. Why would adding the arm option trigger OS version control ? I thought that in freestanding, these parameters were not checked. Putting them in “undefined” wasn’t a good idea either I guess…

1 Like

Oh whoops, yeah I forgot to comment on those lines! You’re using freestanding so OS version isn’t applicable in this case. Generally speaking you don’t ever want to set something to undefined to indicate “unused”, you’re looking for null (I still make this mistake constantly for some reason, they mix in my mind). In fact, that’s what os_version_min and os_version_max are set to by default in that struct, so you can go ahead and just delete those lines!

1 Like

A PR I just opened into FreeRTOS’s kernel for anyone interested: