Zig is a general-purpose language for maintaining robust, optimal, and reusable software.
BL602 is a 32-bit RISC-V SoC with WiFi and Bluetooth LE.
Let's run Zig on BL602!
We're running Zig bare metal on BL602?
Not quite. We'll need more work to get Zig talking to BL602 Hardware and printing to the console.
Instead we'll run Zig on top of a Real-Time Operating System (RTOS): Apache NuttX.
Zig on BL602 should be a piece of cake right?
Well Zig on RISC-V is kinda newish, and might present interesting new challenges.
In a while I'll explain the strange hack I did to run Zig on BL602...
Why are we doing all this?
Later below I'll share my thoughts about Embedded Zig and how we might use Zig to maintain Complex IoT Apps. (Like for LoRa and LoRaWAN)
I'm totally new to Zig, please bear with me as I wade through the water and start swimming in Zig! 🙏
Zig App
Below is the barebones Zig App that's bundled with Apache NuttX RTOS. We'll run this on BL602: hello_zig_main.zig
// Import the Zig Standard Library
const std = @import("std");
// Import printf() from C
pub extern fn printf(
_format: [*:0]const u8
) c_int;
// Main Function
pub export fn hello_zig_main(
_argc: c_int,
_argv: [*]const [*]const u8
) c_int {
_ = _argc;
_ = _argv;
_ = printf("Hello, Zig!\n");
return 0;
}
(We tweaked the code slightly)
The code above prints to the NuttX Console...
Hello, Zig!
Let's dive into the Zig code.
Import Standard Library
We begin by importing the Zig Standard Library...
// Import the Zig Standard Library
const std = @import("std");
Which has all kinds of Algos, Data Structures and Definitions.
(More about the Zig Standard Library)
Import printf
Next we cross into the grey zone between Zig and C...
// Import printf() from C
pub extern fn printf(
_format: [*:0]const u8
) c_int;
Here we import the printf()
function from the C Standard Library.
(Which is supported by NuttX because it's POSIX-Compliant)
What's [*:0]const u8
?
That's how we declare C Strings in Zig...
[*:0] |
Pointer to a Null-Terminated Array... |
const u8 |
Of Constant Unsigned Bytes |
Which feels like "const char *
" in C, but more expressive.
Zig calls this a Sentinel-Terminated Pointer.
(That's because it's Terminated by the Null Sentinel, not because of "The Matrix")
Why is the return type c_int
?
This says that printf()
returns an int
that's compatible with C. (See this)
Main Function
NuttX expects our Zig App to export a Main Function that follows the C Convention. So we so this in Zig...
// Main Function
pub export fn hello_zig_main(
_argc: c_int,
_argv: [*]const [*]const u8
) c_int {
argc
and argv
should look familiar, though argv
looks complicated...
-
"
[*]const u8
" is a Pointer to an Unknown Number of Constant Unsigned Bytes(Like "
const uint8_t *
" in C) -
"
[*]const [*]const u8
" is a Pointer to an Unknown Number of the above Pointers(Like "
const uint8_t *[]
" in C)
Inside the Main Function, we call printf()
to print a string...
_ = _argc;
_ = _argv;
_ = printf("Hello, Zig!\n");
return 0;
Why the "` = something`"?_
This tells the Zig Compiler that we're not using the value of "something
".
The Zig Compiler helpfully stops us if we forget to use a Variable (like _argc
) or the Returned Value for a Function (like for printf
).
Doesn't Zig have its own printf?
Yep we should call std.log.debug()
instead of printf()
. See this...
Did we forget something?
For simplicity we excluded the Variable Arguments for printf()
.
Our declaration for printf()
specifies only one parameter: the Format String. So it's good for printing one unformatted string.
Enable Zig App
We're ready to build our Zig App in NuttX!
Follow these steps to download and configure NuttX for BL602...
To enable the Zig App in NuttX, we do this...
make menuconfig
And select "Application Configuration" → "Examples" → "Hello Zig Example". (See pic above)
Save the configuration and exit menuconfig.
Something interesting happens when we build NuttX...
Build Fails on NuttX
When we build NuttX with the Zig App...
make
We'll see this error (pic above)...
LD: nuttx
riscv64-unknown-elf-ld: nuttx/staging/libapps.a(builtin_list.c.home.user.nuttx.apps.builtin.o):(.rodata.g_builtins+0xbc):
undefined reference to `hello_zig_main'
Which is probably due to some incomplete Build Rules in the NuttX Makefiles. (See this)
But no worries! Let's compile the Zig App ourselves and link it into the NuttX Firmware.
Compile Zig App
Follow these steps to install the Zig Compiler...
This is how we compile our Zig App for BL602 and link it with NuttX...
## Download our modified Zig App for NuttX
git clone --recursive https://github.com/lupyuen/zig-bl602-nuttx
cd zig-bl602-nuttx
## Compile the Zig App for BL602
## (RV32IMACF with Hardware Floating-Point)
zig build-obj \
-target riscv32-freestanding-none \
-mcpu sifive_e76 \
hello_zig_main.zig
## Copy the compiled app to NuttX and overwrite `hello.o`
## TODO: Change "$HOME/nuttx" to your NuttX Project Directory
cp hello_zig_main.o $HOME/nuttx/apps/examples/hello/*hello.o
## Build NuttX to link the Zig Object from `hello.o`
## TODO: Change "$HOME/nuttx" to your NuttX Project Directory
cd $HOME/nuttx/nuttx
make
Note that we specify "build-obj
" when compiling our Zig App.
This generates a RISC-V Object File hello_zig_main.o
that will be linked into our NuttX Firmware.
Let's talk about the Zig Target, which looks especially interesting for RISC-V...
Zig Target
Why is the Zig Target riscv32-freestanding-none?
Zig Targets have the form "(arch)(sub)-(os)-(abi)"...
riscv32
: Because BL602 is a 32-bit RISC-V processorfreestanding
: Because Embedded Targets don't need an OSnone
: Because Embedded Targets don't specify the ABI
Why is the Target CPU sifive_e76?
BL602 is designated as RV32IMACF...
Designation | Meaning |
---|---|
RV32I |
32-bit RISC-V with Base Integer Instructions |
M |
Integer Multiplication + Division |
A |
Atomic Instructions |
C |
Compressed Instructions |
F |
Single-Precision Floating-Point |
Among all Zig Targets, only sifive_e76
has the same designation...
$ zig targets
...
"sifive_e76": [ "a", "c", "f", "m" ],
Thus we use sifive_e76
as our Target CPU.
Or we may use baseline_rv32-d
as our Target CPU...
## Compile the Zig App for BL602
## (RV32IMACF with Hardware Floating-Point)
zig build-obj \
-target riscv32-freestanding-none \
-mcpu=baseline_rv32-d \
hello_zig_main.zig
That's because...
-
"
baseline_rv32
" means RV32IMACFD("D" for Double-Precision Floating-Point)
-
"
-d
" means remove the Double-Precision Floating-Point ("D")(But keep the Single-Precision Floating-Point)
Now comes another fun challenge, with a weird hack...
Floating-Point ABI
(Note: We observed this issue with Zig Compiler version 0.10.0, it might have been fixed in later versions of the compiler)
When we link the Compiled Zig App with NuttX, we see this error (pic above)...
## Build NuttX to link the Zig Object from `hello.o`
## TODO: Change "$HOME/nuttx" to your NuttX Project Directory
$ cd $HOME/nuttx/nuttx
$ make
...
riscv64-unknown-elf-ld: nuttx/staging/libapps.a(hello_main.c.home.user.nuttx.apps.examples.hello.o):
can't link soft-float modules with single-float modules
What is the meaning of this Soft-Float vs Single-Float? (Milk Shake?)
Let's sniff the NuttX Object Files produced by the NuttX Build...
## Dump the ABI for the compiled NuttX code.
## Do this BEFORE overwriting hello.o by hello_zig_main.o.
## "*hello.o" expands to something like "hello_main.c.home.user.nuttx.apps.examples.hello.o"
$ riscv64-unknown-elf-readelf -h -A $HOME/nuttx/apps/examples/hello/*hello.o
ELF Header:
Flags: 0x3, RVC, single-float ABI
...
File Attributes
Tag_RISCV_arch: "rv32i2p0_m2p0_a2p0_f2p0_c2p0"
The ELF Header says that the NuttX Object Files were compiled for the (Single-Precision) Hardware Floating-Point ABI (Application Binary Interface).
(NuttX compiles with the GCC Flags "-march=rv32imafc -mabi=ilp32f
")
Whereas our Zig Compiler produces an Object File with Software Floating-Point ABI...
## Dump the ABI for the compiled Zig app
$ riscv64-unknown-elf-readelf -h -A hello_zig_main.o
ELF Header:
Flags: 0x1, RVC, soft-float ABI
...
File Attributes
Tag_RISCV_arch: "rv32i2p0_m2p0_a2p0_f2p0_c2p0"
GCC won't let us link Object Files with different ABIs: Software Floating-Point vs Hardware Floating-Point!
Let's fix this with a quick hack...
(Why did the Zig Compiler produce an Object File with Software Floating-Point ABI, when sifive_e76
supports Hardware Floating-Point? See this)
Patch ELF Header
Earlier we discovered that the Zig Compiler generates an Object File with Software Floating-Point ABI (Application Binary Interface)...
## Dump the ABI for the compiled Zig app
$ riscv64-unknown-elf-readelf -h -A hello_zig_main.o
...
Flags: 0x1, RVC, soft-float ABI
Tag_RISCV_arch: "rv32i2p0_m2p0_a2p0_f2p0_c2p0"
But this won't link with NuttX because NuttX was compiled with Hardware Floating-Point ABI.
We fix this by modifying the ELF Header...
-
Edit
hello_zig_main.o
in a Hex Editor -
Change byte
0x24
(Flags) from0x01
(Soft Float) to0x03
(Hard Float)
We verify that the Object File has been changed to Hardware Floating-Point ABI...
## Dump the ABI for the modified object file
$ riscv64-unknown-elf-readelf -h -A hello_zig_main.o
...
Flags: 0x3, RVC, single-float ABI
Tag_RISCV_arch: "rv32i2p0_m2p0_a2p0_f2p0_c2p0"
This is now Hardware Floating-Point ABI and will link with NuttX.
Is it really OK to change the ABI like this?
Well technically the ABI is correctly generated by the Zig Compiler...
## Dump the ABI for the compiled Zig app
$ riscv64-unknown-elf-readelf -h -A hello_zig_main.o
...
Flags: 0x1, RVC, soft-float ABI
Tag_RISCV_arch: "rv32i2p0_m2p0_a2p0_f2p0_c2p0"
The last line translates to RV32IMACF, which means that the RISC-V Instruction Set is indeed targeted for Hardware Floating-Point.
We're only editing the ELF Header, because it didn't seem to reflect the correct ABI for the Object File.
Is there a proper fix for this?
In future the Zig Compiler might allow us to specify the Floating-Point ABI as the target...
## Compile the Zig App for BL602
## ("ilp32f" means Hardware Floating-Point ABI)
zig build-obj \
-target riscv32-freestanding-ilp32f \
...
Can we patch the Object File via Command Line instead?
Yep enter this at the Command Line to patch the ELF Header...
xxd -c 1 hello_zig_main.o \
| sed 's/00000024: 01/00000024: 03/' \
| xxd -r -c 1 - hello_zig_main2.o
This generates the Patched Object File at hello_zig_main2.o
Pine64 PineCone BL602 RISC-V Board
Zig Runs OK!
We're ready to link the Patched Object File with NuttX...
## Copy the modified object file to NuttX and overwrite `hello.o`
## TODO: Change "$HOME/nuttx" to your NuttX Project Directory
cp hello_zig_main.o $HOME/nuttx/apps/examples/hello/*hello.o
## Build NuttX to link the Zig Object from `hello.o`
## TODO: Change "$HOME/nuttx" to your NuttX Project Directory
cd $HOME/nuttx/nuttx
make
Finally our NuttX Build succeeds!
Follow these steps to flash and boot NuttX on BL602...
In the NuttX Shell, enter hello_zig
NuttShell (NSH) NuttX-10.3.0-RC2
nsh> hello_zig
Hello, Zig!
Yep Zig runs OK on BL602 with NuttX! 🎉
And that's it for our (barebones) Zig Experiment today!
Let's talk about building real-world Embedded and IoT Apps with Zig...
Pine64 PineCone BL602 Board (right) connected to Semtech SX1262 LoRa Transceiver (left) over SPI
Embedded Zig
Will Zig run on Bare Metal? Without an RTOS like NuttX?
Yep it can! Check out this project that runs Bare Metal Zig on the HiFive1 RISC-V board...
Can we build cross-platform Embedded Apps in Zig with GPIO, I2C, SPI, ...?
We're not quite there yet, but the Zig Embedded Group is creating a Common Interface and Hardware Abstraction Layer for Embedded Platforms...
With the microzig Library, someday we might blink the LED like so...
// Import microzig library
const micro = @import("microzig");
// Blink the LED
pub fn main() void {
// Open the LED GPIO at "/dev/gpio1"
const led_pin = micro.Pin("/dev/gpio1");
// Configure the LED GPIO for Output
const led = micro.Gpio(led_pin, .{
.mode = .output,
.initial_state = .low,
});
led.init();
// Loop forever blinking the LED
while (true) {
busyloop();
led.toggle();
}
}
// Wait a short while
fn busyloop() void {
const limit = 100_000;
var i: u24 = 0;
while (i < limit) : (i += 1) {
@import("std").mem.doNotOptimizeAway(i);
}
}
(Adapted from blinky.zig)
But our existing firmware is all in C. Do we rewrite everything in Zig?
Aha! Here comes the really interesting thing about Zig, read on to find out...
Pine64 PineDio Stack BL604 (left) talking LoRaWAN to RAKwireless WisGate (right)
Why Zig?
Why are we doing all this with Zig instead of C?
Here's why...
"Zig has
zig cc
andzig c++
, two commands that expose an interface flag-compatible with clang, allowing you to use the Zig compiler as a drop-in replacement for your existing C/C++ compiler."
Because of this, Zig works great for maintaining complex C projects...
Thus we might enjoy the benefits of Zig, without rewriting in Zig!
How is this relevant to Embedded Apps and NuttX?
Today we're running incredibly complex C projects on NuttX...
Zig might be the best way to maintain and extend these IoT Projects on NuttX.
Why not rewrite in Zig? Or another modern language?
That's because these C projects are still actively maintained and can change at any moment.
(Like when LoRaWAN introduces new Regional Frequencies for wireless networking)
Any rewrites of these projects will need to incorporate the updates very quickly. Which makes the maintenance of the rewritten projects horribly painful.
(Also LoRaWAN is Time Critical, we can't change any code that might break compliance with the LoRaWAN Spec)
So we'll have to keep the projects intact in C, but compile them with Zig Compiler instead?
Yeah probably the best way to maintain and extend these Complex IoT Projects is to compile them as-is with Zig.
But we can create new IoT Apps in Zig right?
Yep totally! Since Zig interoperates well with C, we can create IoT Apps in Zig that will call the C Libraries for LoRa / LoRaWAN / NimBLE.
I'm really impressed by this Wayland Compositor in Zig, how it imports a huge bunch of C Header Files, and calls them from Zig!
What's Next
This has been a very quick experiment with Zig on RISC-V Microcontrollers... But it looks super promising!
In the coming weeks I'll test Zig as a drop-in replacement for GCC. Let's find out whether Zig will cure our headaches in maintaining Complex IoT Projects!
Check out the testing updates here...
(Spoiler: It really works!)
Many Thanks to my GitHub Sponsors for supporting my work! This article wouldn't have been possible without your support.
Got a question, comment or suggestion? Create an Issue or submit a Pull Request here...
Notes
This article is the expanded version of this Twitter Thread
This article was inspired by a question from my GitHub Sponsor: "Can we run Zig on BL602 with Apache NuttX RTOS?"
-
For Embedded Platforms (like Apache NuttX RTOS), we need to implement our own Panic Handler...
-
Matheus Catarino França has a suggestion for fixing the NuttX Build for Zig Apps...
"make config is not running the compiler. I believe the problem must be in the application.mk in apps"
-
This Revert Commit might tell us what's missing from the NuttX Makefiles...
Top comments (1)
As for Zig 0.12.0, use option "-target riscv32-freestanding-eabihf" to emit Hardware Floating-Point ABI.