Zig NEWS

Anders Holmberg
Anders Holmberg

Posted on • Updated on

Run Zig code on Raspberry Pico W

Did Santa bring you a Raspberry Pi Pico W(pico-w) and you really want it to run Zig code? This article describes how you can link Zig code with the official Raspberry Pi Pico SDK(pico-sdk) using a few simple step.

The strategy is to build the Zig part as a static library and link this with pico-sdk.

An alternative to pico-sdk is MicroZig. With MicroZig it will be Zig all the way down. The main reason for using pico-sdk is that it provides more features at the moment.

Before you start you need to set up the pico-w development environment. The official pico documentation describes this in detail. You also need Zig installed and I'm using version 0.11.0.

The steps in this article can be used for the Raspberry Pico (without the W) as well.

Create the pico-sdk project

We start by creating a pico-sdk project. One way to do this is to use a tool provided by the Raspberry Pi people, pico-project-generator. The pico-w documentation describes the necessary steps as an alternative.

But let's use the tool on the command line (it has a GUI if you prefer that).

$ pico-project.py --uart --debugger 0 --boardtype pico_w link_zig_with_pico_sdk
$ cd link_zig_with_pico_sdk && tree -I build
.
├── CMakeLists.txt
├── link_zig_with_pico_sdk.c
└── pico_sdk_import.cmake
Enter fullscreen mode Exit fullscreen mode

The created project provides a serial console over UART and support for an SWD debugger.

The pico has an SWD (Serial Wire Debug interface for debugging purposes. To flash code, set breakpoints etc additional hardware is required; the Raspberry Pico Probe, another pico programmed with the Probe firmware or a Raspberry Pi are examples of such hardware.

We use cmake to build the project. This creates an ELF-file that can be loaded on the pico-w.

$ cmake --build build
$ file build/link_zig_with_pico_sdk.elf
build/link_zig_with_pico_sdk.elf: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, not stripped
Enter fullscreen mode Exit fullscreen mode

Create the Zig project

The next step is to create a Zig project in the same directory.

$ zig init-lib
$ tree -I build
.
├── CMakeLists.txt
├── build.zig
├── link_zig_with_pico_sdk.c
├── pico_sdk_import.cmake
└── src
    └── main.zig
Enter fullscreen mode Exit fullscreen mode

The default build target is the host machine and we need to change that to support the pico-w.

Edit the build.zig file and replace the target initialisation.

const target = std.zig.CrossTarget{
    .abi = .eabi,
    .cpu_arch = .thumb,
    .cpu_model = .{ .explicit = &std.Target.arm.cpu.cortex_m0plus },
    .os_tag = .freestanding,
};
Enter fullscreen mode Exit fullscreen mode

If we build this a library will be created that targets the pico-w CPU.

$ zig build --summary all
Build Summary: 3/3 steps succeeded
install success
└─ install link_zig_with_pico_sdk success
   └─ zig build-lib link_zig_with_pico_sdk Debug thumb-freestanding-eabi success 61ms MaxRSS:74M
$ ls zig-out/lib
liblink_zig_with_pico_sdk.a
Enter fullscreen mode Exit fullscreen mode

Combine the two projects

What have we got so far? Two separate projects (one cmake and one Zig) that resides in one directory. Let's combine them.

Since this is mainly a Zig post, Zig will be the top build system (i.e. we will call cmake from build.zig).

Run cmake from zig build

What are the necessary build steps and in which order should they be executed? We want to build the Zig library first (and install it). Then we run cmake to build the ELF-file (and link the Zig library in the process).

  1. Build the Zig library.
  2. Generate the cmake project.
  3. Build the cmake project.

We add external command in build.zig by using the addSystemCommand function. Add the following lines (after b.installArtifact(lib)).

const cmake_generate = b.addSystemCommand(&.{ "cmake", "-B", "./build" });
cmake_generate.setName("cmake : generate project");

const cmake_build = b.addSystemCommand(&.{ "cmake", "--build", "./build" });
cmake_build.setName("cmake : build project");
Enter fullscreen mode Exit fullscreen mode

The calls to setName is not necessary but will provide a nicer zig build output.

The system commands will not be run unless they are added as dependencies. When you run zig build the default behaviour is to run the top level build step named "install". We have already added our library to the dependency list of the "install" step by calling b.installArtifact(lib)). We append the cmake build step to the same dependency list using this function call (after the lines above).

b.getInstallStep().dependOn(&cmake_build.step);
Enter fullscreen mode Exit fullscreen mode

But the cmake build step requires the cmake generate step being executed first. This can be accomplished by adding another dependency.

cmake_build.step.dependOn(&cmake_generate.step);
Enter fullscreen mode Exit fullscreen mode

If we run zig build we see that the build steps are executed in the correct order.

$ zig build --summary all
PICO_SDK_PATH is ~/.local/share/pico/pico-sdk
PICO platform is rp2040.
Build type is Release
PICO target board is pico_w.
...
Build Summary: 5/5 steps succeeded
install success
├─ install link_zig_with_pico_sdk cached
│  └─ zig build-lib link_zig_with_pico_sdk Debug thumb-freestanding-eabi cached 12ms MaxRSS:28M
└─ cmake : build project success 266ms MaxRSS:8M
   └─ cmake : generate project success 390ms MaxRSS:21Mo
Enter fullscreen mode Exit fullscreen mode

Link Zig library from cmake

What do we got so far? Running zig build builds both the Zig library and the final binary, but now it is time to link the Zig library.

To link the library we need to edit CMakeLists.txt; adding the following lines just above the call to target_link_libraries.

Note that these lines include the hard-coded path to the Zig output directory (zig-out/lib); preventing the user from providing a user defined prefix.

add_library(zig_library STATIC IMPORTED)
set_property(TARGET zig_library PROPERTY IMPORTED_LOCATION ../zig-out/lib/liblink_zig_with_pico_sdk.a)
Enter fullscreen mode Exit fullscreen mode

Add the library as a dependency by editing the target_link_library call.

 # Add the standard library to the build
 target_link_libraries(link_zig_with_pico_sdk
        zig_library
        pico_stdlib)
Enter fullscreen mode Exit fullscreen mode

Call Zig from C

Now only one step remains; to use the Zig code.

The Zig code exports a single function (see src/main.zig).

export fn add(a: i32, b: i32) i32 {
    return a + b;
}
Enter fullscreen mode Exit fullscreen mode

The entry point to the application is the main function in link_zig_with_pico_sdk.c. Updating this to call the Zig code looks something like this.

#include <stdio.h>
#include <stdint.h>

#include "pico/stdlib.h"

// Extern declaration of the function exported by Zig.
extern int32_t add(int32_t a, int32_t b);

int main()
{
    stdio_init_all();

    printf("result from zig : %d\n", add(10, 20));

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Build, flash and run

All parts are now in place. A final zig build will produce a binary we can load onto the pico-w.

$ zig build --summary all
...
Build Summary: 5/5 steps succeeded
install success
├─ install link_zig_with_pico_sdk cached
│  └─ zig build-lib link_zig_with_pico_sdk Debug thumb-freestanding-eabi cached 9ms MaxRSS:29M
└─ cmake : build project success 279ms MaxRSS:9M
   └─ cmake : generate project success 395ms MaxRSS:21M
Enter fullscreen mode Exit fullscreen mode

If you are using an SWD debugger you should flash the file build/link_zig_with_pico_sdk.elf to the pico-w. Otherwise the UF2 file build/link_zig_with_pico_sdk.uf2 can be flashed to the pico-w using the default bootloader.

I use Visual Studio Code together with the Raspberry Pico Probe.

And finally the output!

---- Opened the serial port /dev/tty.usbmodem84202 ----
result from zig : 30
Enter fullscreen mode Exit fullscreen mode

Top comments (7)

Collapse
 
mstrens profile image
Strens

Thanks for sharing.
In fact you show how to call a zig function from a c program based on the pico sdk.
Still your zig function (fn add(a: i32, b: i32) i32) does not use the pico sdk.

I would also like to write some zig functions (called from the c program) that contain call to some functions being part of the pico sdk (e.g. to control a gpio or generate a PWM or print to usb port).

Do you know if it is possible and which changes have to be done to the zig program containing those functions, to build.zig and to CMakeLists.txt.

I searched on the web for such an example but I did not find one that works on windows with zig version 0.11.0 (or above)
Thanks in advance for the help.

Collapse
 
anders profile image
Anders Holmberg

Thanks. I'll write another post this week that will explain how to call pico-sdk code from Zig.

Collapse
 
mstrens profile image
Strens

Many thanks.
I am really interested.

Collapse
 
anders profile image
Anders Holmberg

No I don't think there is a security benefit (if anything it should be easier to write secure and maintainable code in Zig). The reason is that wrapping a C library is a quick fix (I need this functionality now!). Just like in this article; wrapping pico-sdk is a quick fix. The long-term goal should be to get an all-Zig solution.

Collapse
 
kojikabuto profile image
Jose M.

Makes sense. Thanks. Zap is another project taking a similar approach.

Collapse
 
kristoff profile image
Loris Cro

Thank you for sharing!

Collapse
 
kojikabuto profile image
Jose M.

I keep seeing new Zig libraries or projects that are mainly C wrapped libraries. Is there a security benefit in doing this?