Testing Zig for embedded development

lobste.rs - Thu Jun 23 14:34

I have been using rust as embedded language for cortex M MCU for a while.

While I like rust, embedded dev in rust has some friction, and a few things are a bit hard to do. Especially C interop and direct memory manipulation.

I decided to give zig a try, and while it is still in very early stage compared to rust, I was able to get a working hello world program quite quickly. And I was able to get RTT working with the SEGGER C library.

In this post, I'll share what I learned and a few gotchas.

TLDR: The repo is here: https://github.com/kuon/zig-stm32test

Hardware

I have a STM32L011F3Px chip connected with a SEGGER jlink to my computer. The chip is barebone except for an external 32kHz crystal and decoupling capacitors.

Because everything is still work in progress, at the time of writing, master zig is needed. Hopefully, under Arch, it is easy with the AUR package, but your mileage may vary.

Ensure you have the following tools:

  • zig master
  • regz a small SVD to zig converter
  • openocd
  • make optional, but I like to have a makefile as a wrapper in all my projects
  • multitail a nice helper to have multiple process run in the same terminal window
  • arm toolchain

Directory structure

Create an empty directory, and inside, you will have the following structure:

.
├── build.zig                       # Build file
├── libs                            # Empty lib dir
│   └── microzig                    # Clone the microzig repository in it
├── Makefile                        # Makefile wrapper
├── ocd                             # OCD configuration directory
│   ├── debug.gdb                   # GDB configuration
│   ├── ocd.cfg                     # OCD basic configuration
│   └── ocd_rtt.cfg                 # OCD RTT configuration
└── src
    ├── main.zig                    # Main code file
    ├── rtt.zig                     # Segger RTT wrapper
    ├── SEGGER_RTT.c                # Segger RTT file from official SEGGER site
    ├── SEGGER_RTT_Conf.h
    ├── SEGGER_RTT.h
    └── STM32L0x1                   # This is my chipset support libraries
        ├── registers.svd           # Download this from ST website or google it
        └── registers.zig

microzig

Microzig is a small library that provide an entry point for MCU. As zig has no package manager yet, just clone the repository inside libs/microzig.

registers.zig

Download the svd file for your MCU. In my case it was STM32L0x1.svd. I renamed that to registers.svd and generated the zig file with regz registers.svd > registers.zig.

build.zig

This is the zig build file, it is a little bit similar to a Makfile for C.

You should create your chip definition like so:

const stm32l0x1 = microzig.Chip{
    .name = "STM32L011F3Px",
    .path = root() ++ "src/STM32L0x1/registers.zig",
    .cpu = microzig.cpus.cortex_m0plus,
    .memory_regions = &.{
        .{ .kind = .flash, .offset = 0x08000000, .length = 8 * 1024 },
        .{ .kind = .ram, .offset = 0x20000000, .length = 2 * 1024 },
    },
};

Then, to build the SEGGER lib, we need:

    exe.addCSourceFile(root() ++ "src/SEGGER_RTT.c", &[_][]const u8{
        "-std=c99",
    });

    exe.addIncludeDir(root() ++ "src");
    // This is required for RTT
    exe.addObjectFile("/usr/arm-none-eabi/lib/thumb/v6-m/nofp/libc.a");
    exe.addSystemIncludeDir("/usr/arm-none-eabi/include/");

    // This is important, without it, the linker removes the vector table
    exe.want_lto = false;

Change the location of your arm toolchain acordingly.

Makefile

The Makefile provide a few helper functions, look it up in the repository.

rtt.zig

This is a small wrapper around rtt for testing. Of course, a full library may contain more functions and ideally a version that do not depend on the C lib.


const rtt = @cImport({
    @cInclude("SEGGER_RTT.h");
});

pub fn init() void {
    rtt.SEGGER_RTT_Init();
}

pub fn write(str: []const u8) void {
    _ = rtt.SEGGER_RTT_Write(0, @ptrCast(*const anyopaque, str.ptr), str.len);
}

main.zig

This contains the main code, nothing special for zig. I really like how straightforward it is to set registers.

Running

If you run make flash it should build and flash the MCU. Then make run should run ocd and attach a console to rtt stream.

You should see something like this:

Open video directly

A few notes:

  • exe.want_lto = false; is required in build.zig otherwise it will wipe the vector table when adding a C file.
  • regz is missing interrupt names, to toggle an interrupt you have to do something like this (this set interrupt number 2, which is RTC):
regs.NVIC.ISER.modify(.{ .SETENA = 1 << 2 });
  • If your MCU is sleeping, you need connect_assert_srst in OpenOCD config when flashing.

Conclusion

I really like the zig onboarding for a one main reason: everything is simple.

The tooling is way less mature than rust that has probe-rs defmt and many other niceties for embedded dev. But as zig is simple, I was able to "put something together" with my prior knowledges and some help from the zig discord.

Being able to just throw a C lib in a zig project is really cool. I did write some rust bindings for C API, but it is much harder to please rust borrow checker.

While rust is excellent in many many ways, and I'll continue to use it for many use cases, for embedded it is "getting in the way" and I feel more "free" with zig.

I want to thank the zig-embedded discord members for the help provided, and microzig authors.

The repo is here: https://github.com/kuon/zig-stm32test

If you wish to comment or discuss this post, just mention me on Mastodon or Twitter.