Zig NEWS

Anders Holmberg
Anders Holmberg

Posted on

Call C from Zig on Raspberry Pico W

In the first article I described how to call Zig from C. But all that functionality provided by pico-sdk is locked away on the C side of the project. Let's change that.

Create a Zig main function

Start by cleaning up the code from the previous example, creating a main function in Zig.

// link_with_pico_sdk.c
extern void zig_main(); 

int main()
{
    zig_main();
    return 0;
}
Enter fullscreen mode Exit fullscreen mode
// src/main.zig
export fn zig_main() void {
}
Enter fullscreen mode Exit fullscreen mode

This should compile and run on your pico (but do nothing).

Write a string to stdout

As a next step let's output a string to stdout (on the Pico UART).

First we must call the pico-sdk function stdio_init_all to setup the stdio interface (you can configure this to use either UART or USB on the pico). Then we can call puts to output a static string.

Zig needs to know the call signature of these functions; i.e. each functions name, return type and argument types. One way to do this is the provide an external function declaration directly in the Zig file.

Update the Zig file. Note the comment that shows the corresponding function declaration in C; those should match.

// src/main.zig

// C declaration: `void stdio_init_all();`.
extern fn stdio_init_all() void;

// C declaration: `int puts(const char *s);`
extern fn puts(str: [*:0]const u8) c_int;

export fn zig_main() void {
    stdio_init_all();

    _ = puts("Hello world\n");
}
Enter fullscreen mode Exit fullscreen mode

Again it should compile and run on the pico.

---- Opened the serial port /dev/tty.usbmodem84302 ----
Hello world
Enter fullscreen mode Exit fullscreen mode

How does an external function declaration work? If we look at the Zig documentation the extern keyword is described as.

extern can be used to declare a function or variable that will be resolved at link time, when linking statically or at runtime, when linking dynamically.

Ok, as long as the call signature match we can rely on the linker to resolve to the function that is actually called (if the linker fails to find a match we will get an undefined reference error).

But where are these functions defined? In the build directory we have a map file, build/link_zig_with_pico_sdk.elf.map. Among other things a map file will list all symbols in the executable. If you search for stdio_init_all you will find.

.text.stdio_init_all
0x0000000010003e8c pico-sdk/src/rp2_common/pico_stdio/stdio.c.obj
Enter fullscreen mode Exit fullscreen mode

Why not @cImport pico-sdk header files?

When interfacing C code with Zig the default way is to import header files using the @cImport function. Again turning to the Zig documentation.

This function (@cImport) parses C code and imports the functions, types, variables, and compatible macro definitions into a new empty struct type, and then returns that type.

The main reason to not do that with the pico-sdk is complexity. The pico-sdk has a lot of include files in many include directories and it is a bit of a rabbit hole to get it to work. That said, there are projects on Github that does just that. But in this article we take the easy route.

How many directories are named include?

find . -type d -name "include" | wc -l 
     180
Enter fullscreen mode Exit fullscreen mode

Why not @cImport my own C header file?

As an alternative to writing the external function declarations directly in Zig, you could create a simplified C header that only contains the information that is required by the application.

As an example...

// src/pico_sdk.h
#ifndef _PICO_SDK_H_
#define _PICO_SDK_H_

extern void stdio_init_all();
extern int puts(const char *s);

#endif // _PICO_SDK_H_
Enter fullscreen mode Exit fullscreen mode

But I'll keep adding declarations to Zig in this article.

How do I find these call signatures?

Well, that depends.

Functions defined in the pico-sdk can be found by roaming the pico-sdk source code or the pico-sdk documentation.

Standard C functions (such as puts) can be found in uncountable places on the internet, e.g. The Open Group Library. The header files are also provided by the compiler (arm-none-eabi-gcc).

A more complex example

Let's write a more complex application where we read a GPIO and print the result to stdio. It would look something like this in C.

#include "pico/stdlib.h"
#include "hardware/gpio.h"

int main()
{
    stdio_init_all();

    const uint PIN = 15;

    gpio_init(PIN);
    gpio_set_dir(PIN, GPIO_IN);

    while (true) {
        if (gpio_get(PIN)) {
            printf("GPIO %d SET\n", PIN); 
        } else {
            printf("GPIO %d NOT SET\n", PIN); 
        }
        sleep_ms(1000);
    }

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Looking up the call signatures, this Zig variant looks like a reasonable appoach.

extern fn stdio_init_all() void;

extern fn printf(format: [*:0]const u8, ...) c_int;

extern fn sleep_ms(ms: u32) void;

extern fn gpio_init(gpio: u32) void;
extern fn gpio_set_dir(gpio: u32, out: bool) void;
extern fn gpio_pull_up(gpio: u32) void;
extern fn gpio_get(gpio: u32) bool;

export fn zig_main() void {
    stdio_init_all();

    const pin: u32 = 15;

    gpio_init(pin);
    gpio_set_dir(pin, false);
    gpio_pull_up(pin);

    while (true) {
        const value = gpio_get(pin);
        if (value) {
            _ = printf("GPIO %d SET\n", pin);
        } else {
            _ = printf("GPIO %d NOT SET\n", pin);
        }
        sleep_ms(1000);
    }

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

But trying to compile it it fails with undefined references to gpio_set_dir, gpio_pull_up and gpio_get. Why?

Diving into pico-sdk we find that the functions are declared in src/rp2_common/hardware_gpio/include/hardware/gpio.h

// gpio.h excerpt...
void gpio_init(uint gpio);
static inline void gpio_set_dir(uint gpio, bool out) {
static inline void gpio_pull_up(uint gpio) {
static inline bool gpio_get(uint gpio) {
Enter fullscreen mode Exit fullscreen mode

An inlined function declaration suggests to the C compiler that it should attempt to embed the functions code directly at the call site, rather than handling it as a regular function call. Examining the map file again, we find no trace of gpio_set_dir, gpio_pull_up and gpio_get. Looks like the C compiler took the hint.

The easiest way to fix this is to wrap the inlined functions in a C file, and update the Zig code to use these wrapped functions instead. Create a new file src/pico_sdk.c.

// src/pico_sdk.c
#include <stdint.h>
#include <hardware/gpio.h>

void __wrap_gpio_set_dir(uint32_t gpio, bool out) 
{
    gpio_set_dir(gpio, out);
}

void __wrap_gpio_pull_up(uint32_t gpio)
{
    gpio_pull_up(gpio);
}

bool __wrap_gpio_get(uint32_t gpio)
{
    return gpio_get(gpio);
}
Enter fullscreen mode Exit fullscreen mode

Update the add_executableline in CMakelist.txt to add the new C file to the build process.

add_executable(link_zig_with_pico_sdk link_zig_with_pico_sdk.c src/pico_sdk.c)
Enter fullscreen mode Exit fullscreen mode

Update the Zig file to call the wrapped functions and all should build and work as expected.

extern fn stdio_init_all() void;

extern fn printf(format: [*:0]const u8, ...) c_int;

extern fn sleep_ms(ms: usize) void;

extern fn gpio_init(gpio: usize) void;
extern fn __wrap_gpio_set_dir(gpio: usize, out: bool) void;
extern fn __wrap_gpio_pull_up(gpio: usize) void;
extern fn __wrap_gpio_get(gpio: usize) bool;

export fn zig_main() void {
    stdio_init_all();

    const pin: usize = 15;

    gpio_init(pin);
    __wrap_gpio_set_dir(pin, false);
    __wrap_gpio_pull_up(pin);

    while (true) {
        const value = __wrap_gpio_get(pin);
        if (value) {
            _ = printf("GPIO %d SET\n", pin);
        } else {
            _ = printf("GPIO %d NOT SET\n", pin);
        }
        sleep_ms(1000);
    }

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Output from the application.

GPIO 15 SET
GPIO 15 SET
GPIO 15 NOT SET
GPIO 15 NOT SET
GPIO 15 SET
GPIO 15 SET
Enter fullscreen mode Exit fullscreen mode

One last note - symbol wrapping

If you really really don't like the __wrap_ prefixes, they can be removed using a linker feature.

The --wrap option as described in the GNU linker manual.

Use a wrapper function for symbol. Any undefined reference to symbol will be resolved to __wrap_symbol. Any undefined reference to __real_symbol will be resolved to symbol.

And it gives an example.

void *
__wrap_malloc (size_t c)
{
  printf ("malloc called with %zu\n", c);
  return __real_malloc (c);
}
Enter fullscreen mode Exit fullscreen mode

We only need the first part, to resolve the undefined reference to e.g. gpio_get to __wrap_gpio_get without having to write the prefix at the call site (in the Zig code).

Remove the __wrap_ prefixes from src/main.zig (i.e. revert it to how it looked before we added the prefixes) but keep src/pico_sdk.c intact.

Update the target_link_libraries line in CMakelists.txt, adding linker options for wrapping the gpio_set_dir, gpio_pull_up and gpio_getfunctions.

target_link_libraries(link_zig_with_pico_sdk
        zig_library
        pico_stdlib
        -Wl,--wrap=gpio_set_dir 
        -Wl,--wrap=gpio_pull_up
        -Wl,--wrap=gpio_get
        )
Enter fullscreen mode Exit fullscreen mode

This should now compile and work as expected.

As a side-note. If you search the map file for puts you will find that it is defined as __wrap_puts. This means that the pico-sdk has a wrapper function that intercepts each call to puts and this is executed instead of any libc implementation.

Latest comments (7)

Collapse
 
mstrens profile image
Strens

fyi, I now put on github a project to demonstrate the use of a pio (to control a rgb led) and of a I2C sensor (baro ms5611).
It is here :
github.com/mstrens/zig_test_MS5611...

Collapse
 
anders profile image
Anders Holmberg

Cool! I've got a WS2812 LED strip mounted on the backside of a IKEA Skådis pegboard. Have only programmed a static warm glow at the moment but the possibilties are endless. Also got a Pimoroni LED panel with 1024 LEDs (with a Pico). But software for that is on the todo-list so far.

Collapse
 
mstrens profile image
Strens

fyi, I made some more changes in order to blink a ws2812 rgb led (on a rp2040-zero board). It demonstrates the use of a pio.
The pio program was reused from an sdk example.
I still had to use a C wrapper for one of the sdk functions (pio_add_program()) because I did not find the way to pass one parameter directly from the zig code.
Please let me know if you know how to do it.

This project is on github at
github.com/mstrens/pico_sdk4.git

Collapse
 
mstrens profile image
Strens

I made a version that works on windows (and normally on other OS too) where "include" paths are not harcoded anymore.
This version is also in named pico_sdk5.zip and is available here:
github.com/nemuibanila/zig-pico-cm...

Collapse
 
mstrens profile image
Strens

I made a merge of your code with the one from github/nemuibanila.
I mainly replace addObject() by addStaticLibrary().
Therefore I added a main() function in a c file (hello.c). This c function call a zig-main() from a main.zig file.

This code works on my pico.
Note : all "include" are hardcoded because the "find" command does not work in windows in the same way as on linux

My code is available in this issue : github.com/nemuibanila/zig-pico-cm...

Perhaps you can write a new part in zignews to further explain the solution using @cimport().

Thanks again for your help.

Collapse
 
mstrens profile image
Strens • Edited

Thank you for this part 2.
It is useful to have a solution.
It is very well explained.

Still when the number of sdk functions being used increases, having to wrap them becomes a significant drawback. I expect that it has also a negative impact on the performance (due to the intermediate functions).
So I expect that the solution with @cImport would be better.

I presume that you now this project (that uses @cImport)
github.com/nemuibanila/zig-pico-cmake

I tried to use it on a windows machine but it does not work.
I expect that there are 2 reasons related to build.zig program:

  • it uses a "find" command to retrieve the "include" folders.
  • It also has this code // default arm-none-eabi includes lib.linkLibC(); lib.addSystemIncludePath(.{.path = "/usr/arm-none-eabi/include"}); I tried to find back the "include" folders and to add them with hardcode path. I also changed lib.addSystemIncludePath(.{.path = "/usr/lib/arm-none-eabi/include"}); based on the location where windows installed the arm-none-eabi.

Still this did not solved the errors I had when running "zig build".
I got many errors like include from arm-none-eabi is not found.

I installed VirtualBox and Debian on my pc and tested the orginal github project (just adapting some paths).
Under linux I can compile it without error.
So the solution works.

Do you have any idea about what has to be changed build.zig to let it run on a windows machine?

Note: when I run "zig build" on linux I got the gcc command being execute and I noticed that there are not 180 "include" folders but about 67.
It should not be a big issue to hardcode them in build.zig.

Collapse
 
anders profile image
Anders Holmberg

Thanks for the feedback!

I'm on macOS so I don't know how to get it to work on Windows; sorry.

I haven't tested github.com/nemuibanila/zig-pico-cmake before (but I have looked at a couple of other older projects). Had to change the include path to the arm-none-eabi include files; then it built without issue on macOS. A really nice project. Hopefully this article can explain some of the "magic" that happens behind the scenes.