STM32 Hal Import

I’ve been trying to get a project on an STM32F4 microcontroller up and running. I have the startup code all in zig, with the reset handler and vector table, but I’m having a lot of trouble importing ST’s HAL since I don’t really want to roll my own just to get a small project up and running. I’d rather not load the generated C project with its own main, which is what a lot of examples do with importing STM32 C code. So I am just including the CMSIS and ST HAL libraries into a hal.zig for use in the application.

Using cImport for STM32 HAL like this:

pub const stm32f407 = @cImport({
    @cDefine("USE_HAL_DRIVER", "");
    @cDefine("STM32F407xx", "");
    @cDefine("__CORTEX_M4", "");
    // remove cmsis vector table initialization macro from cmsis_gcc.h
    @cDefine("__PROGRAM_START", "");
 
    @cInclude("stm32f4xx.h");
    @cInclude("core_cm4.h");
 
    @cInclude("stm32f4xx_hal.h");
    @cInclude("stm32f4xx_hal_conf.h");
});

Results in a lot of compileError defined macros in the generated cimport.zig such as:

pub const __HAL_RCC_PWR_CLK_ENABLE = @compileError("unable to translate macro: undefined identifier `tmpreg`");

The problem with this one specifically is possibly __IO which is from core_cm4.h:

#define     __IO    volatile             /*!< Defines 'read / write' permissions */

which results in zig,

pub const __IO = @compileError("unable to translate C expr: unexpected token 'volatile'");

Is zig not able to understand volatile? I am thinking I don’t have the necessary C bindings somehow but I’m not sure exactly what the problem is.

1 Like

The build.zig is probably relevant here.

const std = @import("std");
const builtin = @import("builtin");

pub fn build(b: *std.Build) void {
    const exe_name = "dweomer_example";

    const query: std.Target.Query = .{
        .cpu_arch = .thumb,
        .cpu_model = .{ .explicit = &std.Target.arm.cpu.cortex_m4 },
        .os_tag = .freestanding,
        .abi = .eabihf,
        .glibc_version = null,
    };
    const target = b.resolveTargetQuery(query);

    const optimize = b.standardOptimizeOption(.{});

    const c_optimization = if (optimize == .Debug) "-Og" else if (optimize == .ReleaseSmall) "-Os" else "-O2";

    // HAL --------------------------------------------------------------------------------------------------
    const hal_device = "stm32f4xx"; // match the submodule path
    const hal_cmsis = "cmsis-device-f4"; // match submodule path

    const hal_includes = [_][]const u8{
        "src/bsp/hal",
        "src/bsp/hal/" ++ hal_device ++ "/Inc",
        "src/bsp/hal/" ++ hal_device ++ "/Inc/Legacy",
        "src/bsp/hal/" ++ hal_cmsis ++ "/Include",
        "src/bsp/hal/cmsis/CMSIS/Core/Include",
        "src/bsp/hal/cmsis/Device/ARM/ARMCM4/Include",
    };

    const hal_sources = [_][]const u8{
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_adc.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_adc_ex.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_can.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_cec.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_cortex.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_crc.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_cryp.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_cryp_ex.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_dac.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_dac_ex.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_dcmi.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_dcmi_ex.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_dfsdm.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_dma.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_dma_ex.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_dma2d.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_dsi.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_eth.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_exti.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_flash.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_flash_ex.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_flash_ramfunc.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_fmpi2c.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_fmpi2c_ex.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_fmpsmbus.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_fmpsmbus_ex.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_gpio.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_hash.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_hash_ex.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_hcd.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_i2c.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_i2c_ex.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_i2s.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_i2s_ex.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_irda.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_iwdg.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_lptim.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_ltdc.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_ltdc_ex.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_mmc.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_msp_template.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_nand.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_nor.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_pccard.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_pcd.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_pcd_ex.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_pwr.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_pwr_ex.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_qspi.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_rcc.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_rcc_ex.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_rng.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_rtc.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_rtc_ex.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_sai.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_sai_ex.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_sd.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_sdram.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_smartcard.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_smbus.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_spdifrx.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_spi.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_sram.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_tim.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_tim_ex.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_uart.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_usart.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_hal_wwdg.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_ll_adc.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_ll_crc.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_ll_dac.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_ll_dma.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_ll_dma2d.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_ll_exti.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_ll_fmc.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_ll_fmpi2c.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_ll_fsmc.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_ll_gpio.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_ll_i2c.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_ll_lptim.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_ll_pwr.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_ll_rcc.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_ll_rng.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_ll_rtc.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_ll_sdmmc.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_ll_spi.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_ll_tim.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_ll_usart.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_ll_usb.c",
        "src/bsp/hal/" ++ hal_device ++ "/Src/" ++ hal_device ++ "_ll_utils.c",
        "src/bsp/hal/" ++ hal_cmsis ++ "/Source/Templates/system_" ++ hal_device ++ ".c",
    };

    // -------------------------------------------------------------------------------------------------- HAL

    const main_mod = b.addModule("main", .{
        .root_source_file = b.path("src/main.zig"),
    });

    for (hal_includes) |path| {
        main_mod.addIncludePath(b.path(path));
    }

    const s_mod = b.createModule(.{
        .root_source_file = b.path("src/bsp/startup.zig"),
        .target = target,
        .optimize = optimize,
        .link_libc = null,
        .strip = true,
        .single_threaded = true, // single core cpu
        .sanitize_c = if (optimize == .Debug) false else true,
    });

    s_mod.addImport("main", main_mod);

    const elf = b.addExecutable(.{
        .name = exe_name ++ ".elf",
        .linkage = .static,
        .root_module = s_mod,
    });

    elf.setLinkerScript(b.path("src/bsp/linker.ld"));
    elf.entry = .{ .symbol_name = "resetHandler" };
    elf.link_data_sections = true; // -fdata-sections
    elf.link_function_sections = true; // -ffunction-sections
    elf.link_gc_sections = true; // -Wl,--gc-sections
    elf.want_lto = false; // -flto (.isr_vector size will be zero if set to true)

    // include HAL

    const hal_flags = [_][]const u8{
        c_optimization,
        // "-std=gnu17",
        "-std=c99",
        "-Wall",
        "-Wextra",
        "-DUSE_HAL_DRIVER",
        "-DSTM32F407xx",
        "-D__CORTEX_M4",
    };

    elf.addCSourceFiles(.{
        .files = &hal_sources,
        .flags = &hal_flags,
    });

    for (hal_includes) |path| {
        elf.addIncludePath(b.path(path));
    }

    // display section sizes

    const size_prog: ?[]const u8 = b.findProgram(&.{"arm-none-eabi-size"}, &.{}) catch
        b.findProgram(&.{"llvm-size"}, &.{}) catch blk: {
        std.log.warn("could not find arm-none-eabi-size or llvm-size, skipping size step", .{});
        break :blk null;
    };

    if (size_prog) |prog| {
        const size_run = b.addSystemCommand(&[_][]const u8{
            prog,
            "zig-out/bin/" ++ exe_name ++ ".elf",
        });

        size_run.step.dependOn(&elf.step);
        b.getInstallStep().dependOn(&size_run.step);
    }

    const readelf_prog: ?[]const u8 = b.findProgram(&.{"arm-none-eabi-readelf"}, &.{}) catch null;

    if (readelf_prog) |prog| {
        const readelf_run = b.addSystemCommand(&[_][]const u8{
            prog,
            "--sections",
            "zig-out/bin/" ++ exe_name ++ ".elf",
        });

        readelf_run.step.dependOn(&elf.step);
        b.getInstallStep().dependOn(&readelf_run.step);
    }

    // get bin and hex formats from elf
    const obj_bin = b.addObjCopy(elf.getEmittedBin(), .{
        .format = .bin,
    });
    obj_bin.step.dependOn(&elf.step);
    const bin_out = b.addInstallBinFile(obj_bin.getOutput(), exe_name ++ ".bin");
    b.default_step.dependOn(&bin_out.step);

    const obj_hex = b.addObjCopy(elf.getEmittedBin(), .{
        .format = .hex,
    });
    obj_hex.step.dependOn(&elf.step);
    const hex_out = b.addInstallBinFile(obj_hex.getOutput(), exe_name ++ ".hex");
    b.default_step.dependOn(&hex_out.step);

    // get srec from output uising srecord
    const srec_prog: ?[]const u8 = b.findProgram(&.{"srec_cat"}, &.{}) catch null;

    if (srec_prog) |prog| {
        const srec_cmd = b.addSystemCommand(&[_][]const u8{
            prog,
            b.getInstallPath(.bin, exe_name ++ ".hex"),
            "-Intel",
            "-o",
            b.getInstallPath(.bin, exe_name ++ ".srec"),
            "-Motorola",
        });

        srec_cmd.step.dependOn(&hex_out.step);
        b.getInstallStep().dependOn(&srec_cmd.step);
    } else {
        std.log.warn("could not find srec_cat program", .{});
    }

    // This declares intent for the executable to be installed into the
    // standard location when the user invokes the "install" step (the default
    // step when running `zig build`).
    b.default_step.dependOn(&elf.step);
    b.installArtifact(elf);
}

When Zig translates C code for its own use, it handles Macros (like __IO) in two ways:

  • It create a constant or function depending on the type of macro if it’s possible. Not all kinds of macros can be translated like for example creating alternative names to keywords (like this __IO).
  • It expands the macro everywhere possible (like if it would work on the source after the preprocessor ran). So if the C source has for example __IO int pinA, it would be treated like volatile int pinA.

In general, if a construct can’t be translated, it will be marked with @compileError but you will only actually get an error while compiling if you try to use the construct.

Now to regards to volatile in specific: In Zig volatile can only be used on pointers (to be exact, the value they point to), but not on variables in general. This means that it can’t translate volatile int pinA, only int volatile * pinA (which would end up (after checking) as pub export var pinA: [*c]volatile c_int = @import("std").mem.zeroes([*c]volatile c_int) (and yes, a standard conforming C compiler would zero it too and yes, afaik this is equivalent)).

Another thing to keep in mind, in Zig you can’t introduce alternative names for keywords.

Can you give me the source of where you got this HAL specifically (and the version)? I can look into it if you want.

That would be generous, thanks.

There are three needed. ST keeps the vendor specific device header separate from the HAL itself, and then there’s ARM’s CMSIS files.

https://github.com/STMicroelectronics/cmsis-device-f4.git
Version 5 (55b19837f5703e418ca37894d5745b1dc05e4c91)

https://github.com/STMicroelectronics/stm32f4xx-hal-driver.git
Version 2.6.10 (0e6ca953c6c6fd8fbb93605cb4d565eb89b1cbc9)

https://github.com/ARM-software/CMSIS_5.git
Version 1.8.3 (9677f4f71f6496d5647cd02a032b468a3139b0ae)

The project generation tool STM32CubeMX will pull these libraries and generate a C project that calls into them. I am fairly sure there’s no difference between grabbing them from the generated project or from github.

I am a bit confused by the volatile thing since that is used in many macros, although most cases would be pointers as you say, such as the registers within a GPIO port or something.

Zig also fails to expand other macros that seem like they should have direct zig equivalents, such as:

pub const __HAL_RCC_PWR_CLK_DISABLE = @compileError("unable to translate C expr: expected ')' instead got '&='");

Which is defined simply

#define __HAL_RCC_PWR_CLK_DISABLE()    (RCC->APB1ENR &= ~(RCC_APB1ENR_PWREN))

To expand APB1ENR however runs into __IO uint32_t APB1ENR; which is not a pointer, but a u32 member of a struct.

Oh, it made a pretty big difference as it seems. For example the file stm32f4xx_hal_conf.h just isn’t found. STM32CubeMX likely generates it for you.

Yes, this one technically has an equivalent, but APB1ENR (which this macro depends on) doesn’t have an equivalence. And well, this can cause everything up that dependency chain to fail too.

One major problem is that in many HALs (or similar) people decided to put (cross-cpu) shared state (like pins) as variables instead of pointers and let the linker (via linker scripts) deal with putting it into the right place in memory. This is something you will run into from time to time.

Zig is touted as a “drop in compiler” for C, which seems to be true when I look at a project like this. The difference here is the main.c which contains all the HAL code, and the startup and init code is also all in C. But zig is still compiling all of the HAL files, which contain volatile variables. It would be nice to just use the CMSIS and HAL C files in a pure zig project, with the startup code also in zig.

You can technically do that with a “bit” of manual work, as in you manually create binding for these volatile things (as in you make C import a pointer to a volatile and init it (globally) to the variable).

So if you have a volatile int pinA you write a bit of C code which imports the original HAL, then creates volatile int * const pPinA = &pinA; and a corresponding header which has extern volatile int * const pPinA; which is something Zig can translate again (or you just make that header straight up a Zig file).

Obviously, that’s quite a bit more work than just having it be done automatically.