Zig on RISC-V BL602: Quick Peek with Apache NuttX RTOS

📝 2 Jun 2022

Zig runs on BL602 with Apache NuttX RTOS

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 bundled with Apache NuttX RTOS

§1 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.

Zig on BL602

§1.1 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)

§1.2 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 u8Of 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)

§1.3 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…

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.

(Here’s the full declaration)

Enable Zig App in NuttX

§2 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

§3 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'

(Source)

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.

§4 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_zig.o`
##  TODO: Change "$HOME/nuttx" to your NuttX Project Directory
cp hello_zig_main.o $HOME/nuttx/apps/examples/hello_zig/*hello_zig.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…

Compile Zig App for BL602

§5 Zig Target

Why is the Zig Target riscv32-freestanding-none?

Zig Targets have the form “(arch)(sub)-(os)-(abi)”…

(More about Zig Targets)

Why is the Target CPU sifive_e76?

BL602 is designated as RV32IMACF

DesignationMeaning
RV32I32-bit RISC-V with Base Integer Instructions
MInteger Multiplication + Division
AAtomic Instructions
CCompressed Instructions
FSingle-Precision Floating-Point

(Source)

Among all Zig Targets, only sifive_e76 has the same designation…

$ zig targets
...
"sifive_e76": [ "a", "c", "f", "m" ],

(Source)

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…

Now comes another fun challenge, with a weird hack…

Floating-Point ABI issue

§6 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_zig.o" expands to something like "hello_main.c.home.user.nuttx.apps.examples.hello_zig.o"
$ riscv64-unknown-elf-readelf -h -A $HOME/nuttx/apps/examples/hello_zig/*hello_zig.o
ELF Header:
  Flags: 0x3, RVC, single-float ABI
  ...
File Attributes
  Tag_RISCV_arch: "rv32i2p0_m2p0_a2p0_f2p0_c2p0"

(Source)

NuttX was compiled for (Single-Precision) Hardware Floating-Point ABI

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"

(Source)

Zig Compiler produces an Object File with Software Floating-Point ABI

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)

§7 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

Patch the ELF Header

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 \
  ...

(See this)

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
cp hello_zig_main2.o hello_zig_main.o

This generates the Patched Object File at hello_zig_main2.o

(More about xxd)

Pine64 PineCone BL602 RISC-V Board

Pine64 PineCone BL602 RISC-V Board

§8 Zig Runs OK!

We’re ready to link the Patched Object File with NuttX…

##  Copy the modified object file to NuttX and overwrite `hello_zig.o`
##  TODO: Change "$HOME/nuttx" to your NuttX Project Directory
cp hello_zig_main.o $HOME/nuttx/apps/examples/hello_zig/*hello_zig.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! 🎉

Zig runs on BL602 with Apache NuttX RTOS

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

Pine64 PineCone BL602 Board (right) connected to Semtech SX1262 LoRa Transceiver (left) over SPI

§9 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)

Pine64 PineDio Stack BL604 (left) talking LoRaWAN to RAKwireless WisGate (right)

§10 Why Zig?

Why are we doing all this with Zig instead of C?

Here’s why…

“Zig has zig cc and zig 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.”

(Source)

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 in Active Development 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!

§11 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 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…

lupyuen.github.io/src/zig.md

§12 Notes

  1. This article is the expanded version of this Twitter Thread

  2. This article was inspired by a question from my GitHub Sponsor: “Can we run Zig on BL602 with Apache NuttX RTOS?”

  3. For Embedded Platforms (like Apache NuttX RTOS), we need to implement our own Panic Handler

    “Zig Panic Handler”

  4. 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”

    (Source)

  5. This Revert Commit might tell us what’s missing from the NuttX Makefiles…

    “Revert Zig Build”

  6. We made two Temporary Fixes to the NuttX Makefiles so that the Zig Object Files will be generated…

    nuttx/tools/Config.mk

    apps/Application.mk