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
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
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
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,
};
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
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).
- Build the Zig library.
- Generate the cmake project.
- 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");
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);
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);
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
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)
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)
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;
}
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;
}
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
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
Top comments (8)
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.
Thanks. I'll write another post this week that will explain how to call pico-sdk code from Zig.
Many thanks.
I am really interested.
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.
Makes sense. Thanks. Zap is another project taking a similar approach.
Hi .. this looks really interesting. However, I tried to replicate your build and I ran into a few problems.
I am using release 0.13.0 - this may be the cause of the issues.
zig init-lib
.. but this does not seem to be available in this release, onlyzig init
zig build
fails at line 31 inbuild.zig
:Any thoughts as to how to fix these issues? It looks to me as though the target specification process may have changed ..
Thank you for sharing!
I keep seeing new Zig libraries or projects that are mainly C wrapped libraries. Is there a security benefit in doing this?