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.
Tooling
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
masterregz
a small SVD to zig converteropenocd
make
optional, but I like to have a makefile as a wrapper in all my projectsmultitail
a nice helper to have multiple process run in the same terminal windowarm 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:
A few notes:
exe.want_lto = false;
is required inbuild.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