zig-zephyr
Case study of interweaving zephyr and zig
Motivation
Zephyr is a mature RTOS which is written in C, Zig is a new envolving programming language.
We like to investigate the possibilities of using Zig in the context of Zephyr. This could mean:
- writing the application in Zig
- write Zephyr drivers or modules in Zig
- use Zig as Build System
Could this be achieved in a minimal invasive approache?
One existing example for an aplication written in Zig using Zephyr can be found here: zig-zephyr-hello . What could be improved to get a smoother interaction without forcing us to write wrappers in C to make use of Zephyr from Zig.
Current Status
- [X] Devicetree is translated to Zig because C Devicetree macros are not usable from Zig.
- [X] Writing a Zephyr application in Zig, using C imported types and functions.
- [X] Compiling Zephyr using
zig cc
as toolchain (linking zephyr_pre0.elf step does not work). - [ ] Translate KConfig to Zig
- [ ] Building Zephyr using the Zig build system instead of CMake
Preconditions
- A working Zephyr setup. I set it up as I have described it here. I used the zephyr sdk as non-zig toolchain.
- In order to compile the Zig example the file
zephyrproject/zephyr/scripts/build/gen_syscalls.py
has to be patched by moving the line
wrap += "\t" + "compiler_barrier();\n"
just before the line:
wrap += "#endif\n"
or removing it at all. The reason for this is propably a bug in Zig which refuses to inline the syscall functions if the compiler (memory) barrier is there an results in undefined references.
- An Arm Board for testing. For other boards the file of this repository
zig-zephyr/cmake/compiler/zig/zig-target.cmake
could be extended.
- If using
zig cc
as toolchain, then the path of the zephyr sdk has to be adapted to your installation in the fileCMakeLists.txt
:set(ZEPHYR_SDK_INSTALL_DIR $ENV{HOME}/bin/zephyr-sdk-0.15.1)
and the file
zephyr-sdk-gcc
. - The file
src/c.zig
contains a board (in our case for adafruit_feather_nrf52840) specific define:@cDefine("NRF52840_XXAA", "");
this should be changed to suits your board.
Building the Examples
You have to adapt the commands for your board, I used the adafruit_feather_nrf52840 board. The C application (~src/main.c) is the blinky example taken from the Zephyr samples. The Zig application (~src/main.zig) is the equivalent in zig.
- Compile
main.c
using Zephyr sdkwest build -d build -b adafruit_feather_nrf52840 -p
- Compile
main.c
usingzig cc
west build -d build -b adafruit_feather_nrf52840 -p -- -DZIGCC=1
- Compile
main.zig
using Zephyr sdkwest build -d build -b adafruit_feather_nrf52840 -p -- -DZIGMAIN=1
- Compile
main.zig
usingzig cc
west build -d build -b adafruit_feather_nrf52840 -p -- -DZIGCC=1 -DZIGMAIN=1
Running
Is done like usual by flashing the image to the board, for example:
west flash -r blackmagicprobe --gdb-serial /dev/ttyACM0 --skip-rebuild --elf-file build/zephyr/zephyr.elf
Details of the journey
Inspired from Lup Yuen Lee’s Article about Building an App with Zig and Apache NuttX RTOS
we will try to translate C Code to Zig using zig translate-c
.
We have to do this in the context of the Zephyr Build process, all compiler flags
have to be the same like for invoking the C compiler.
Therefore I started with:
zig cc
C compiler to compile Zephyr
Using the We try to setup a toolchain which uses zig cc
as C compiler.
By following the Custom CMake Guide of Zephyr we use this repository
as TOOLCHAIN_ROOT
with the cmake files under the cmake
directory
and setting the TOOLCHAIN_VARIANT
to zig
.
For the generic (host) part of the build we use the zephyr-sdk toolchain.
We just direct some files to the correspondig files in ZEPHYR_BASE
.
Zig specific settings are in cmake/compiler/zig/target.cmake
.
I just tried it for an arm board so it is written for satisfy this needs.
Remarks
The critical step was the Unfixed size binary Build Step which builds zephyr_pre0.elf
.
If we use zigs clang it stops at
[149/159] Linking C executable zephyr/zephyr_pre0.elf
and tells
ld.lld: error: cannot find linker script -Map
The C compiler is called for this step.
Then I tried to use the zephyr-sdk-gcc for this step by
writing a conditional
clause in zigcc
, which is the wrapper for zig cc
.
If we do this we have no complaining about the arguments but get an
undefined reference to `__aeabi_memclr8'
This could be solved by adding -lc_nano
to the compiler ars which links
in the nano libc.
To view the invocation of the compiler do:
cd build ninja -v
zig build-obj
and use zephyr C code directly
Compile main.zig with I used zig translate-c
with the same compiler arguments we found Zephyr
was using to compile main.c
. From this I extracted the relevant part for main.zig
.
In this form all macros were already expanded.
With some CMake
wizardry I managed it to fit the compiled object file into
the Zephyr app application (see zig.cmake
)
Then I was faced with the compiler_barrier()
issue, mentioned in the *Preconditions.
I was happy to got it to compile, but the expanded macros were not
usable from a programmers point of view.
Thats way I
Translate the Devicetree to Zig
My aim was to have an easy usable perdant to the devicetree_generated.h
header file in Zig. Whereas the header file is an artwork of encoding the
devicetree data into C Preprocessor Macros my goal was to code the devicetree
data in a clean and simple way which could be human viewable and usable.
Therefore Zig’s reach possibilities to create and initialize struct
’s
were realy useful. We got a tree which looks nearly like the zepyr.dts
:
pub const soc = struct {
const gpio_50000300 = .{
._device = @as([*c]const c.struct_device, &c.__device_dts_ord_10),
.reg = [_]u32{0x50000300, 0x200, 0x50000800, 0x300},
.port = @as( u32, 1),
.gpio_controller = true,
.ngpios = @as( u32, 16),
.status = "okay",
.compatible = [_][]const u8{"nordic,nrf-gpio"},
.wakeup_source = false,
};
};
pub const leds = .{
.compatible = [_][]const u8{"gpio-leds"},
// 16
.led_0 = .{
.gpios = .{.ph=&soc.gpio_50000300,.pin=@as( u32, 15),.flags=@as( u32, 0)},
.label = "Red LED",
},
};
In this tree the phandles are real references of their targets. Thus refer to gpios of led_0 is as easy as writing:
dt.leds.led_0.gpios
which is the analog of Zephyrs:
GPIO_DT_SPEC_GET( DT_PATH( leds, led_0), gpios)
Right now not all properties are translated, but to complete it is not too difficult. (Aliases, Labels, Memory Maps for example)