Zig <3 Raspberry-Pi GPIO

I recently built the PiHut Xmas Tree. I ran the example python script, and played a little with GPIO Zero, but Zig is my low-level hobby language so I wanted to try that.

Hello, World

Firstly, I wanted to get a basic hello world setup and running:

hello.zig
const std = @import("std");

pub fn main() void {
    std.debug.print("Hello, world!\n", .{});
}

I compiled & stripped to get a small 4KB binary. I got the -target and -mcpu options from searching around the internet.

$ zig build-exe hello.zig -target arm-linux-musleabihf -mcpu arm1176jzf_s -O ReleaseSmall --strip

Hello, libgpiod

I had thought that interfacing with the kernel directly would be my easiest path forward. Unfortunately, the "easy" API in sysfs is now deprecated. The Linux GPIO consumer API seems a little poorly deprecated, and it was introduced alongside libgpiod which took most of the spotlight in terms of information.

Unfortunately, libgpiod is not packaged for Void Linux, so I had some work to do!

I grabbed https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git/snapshot/libgpiod-1.6.2.tar.gz from https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git/refs/ in the Tag section. I ran the autogen, with cross-arm-linux-musleabihf installed, and targeted the same host as Zig. The ac_cv_func_malloc_0_nonnull=yes will get malloc working. I followed that with a make to build everything.

$ ./autogen.sh --host=arm-linux-musleabihf --prefix=$PWD/foo ac_cv_func_malloc_0_nonnull=yes
$ make
$ make install

I created a build.zig in order to do the more complex includes of objects:

build.zig
const Builder = @import("std").build.Builder;
const CrossTarget = @import("std").zig.CrossTarget;

pub fn build(b: *Builder) void {
    // Standard release options allow the person running `zig build` to select
    // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
    const mode = b.standardReleaseOptions();

    const exe = b.addExecutable("hello-gpio", "./hello-gpio.zig");
    // zig build -Dtarget arm-linux-musleabihf -mcpu arm1176jzf_s -Drelease-small=true --strip
    exe.setTarget(CrossTarget.parse(
        .{
            .arch_os_abi = "arm-linux-musleabihf",
            .cpu_features = "arm1176jzf_s",
        },
    ) catch |err| {
        return;
    });
    exe.setBuildMode(mode);
    exe.install();

    exe.strip = true;
    exe.linkLibC();

    exe.addObjectFile("libgpiod-1.6.2/foo/lib/libgpiod.a");
    exe.addIncludeDir("libgpiod-1.6.2/foo/include");

    const run_cmd = exe.run();
    run_cmd.step.dependOn(b.getInstallStep());
    if (b.args) |args| {
        run_cmd.addArgs(args);
    }

    const run_step = b.step("run", "Run the app");
    run_step.dependOn(&run_cmd.step);
}

Ideally, we would build libgpiod from source, but https://github.com/ziglang/zig/issues/3287 gets in the way of that.

And finally, here’s hello-gpio.zig, which was the easiest example to convert from Christophe BLAESS’s example libgpiod usage.

hello-gpio.zig
const std = @import("std");

const c = @cImport({
    @cInclude("gpiod.h");
});

pub fn main() void {
    var value = c.gpiod_ctxless_get_value("/dev/gpiochip0", 3, false, "zigconsumer");
    std.debug.print("Hello, {}!\n", .{value});
}

This will output to zig-cache/bin/hello-gpio (instead of in CWD like before), and will read out the current value of GPIO pin 3!

pi@raspberrypi:~$ ./hello-gpio
Hello, 1!

Toggling LED

Finally I wanted to actually set the value to something, so I ported a libgpiod example I found. To make this easier, I refactored the libgpiod additions to build.zig so I could apply them to new executables, such as the new addition of toggle-led.

build.zig
const LibExeObjStep = @import("std").build.LibExeObjStep;
const Builder = @import("std").build.Builder;
const CrossTarget = @import("std").zig.CrossTarget;
const Mode = @import("std").builtin.Mode;

pub fn libgpio_build(exe: *LibExeObjStep, mode: Mode) void {
    exe.setTarget(CrossTarget.parse(
        .{
            .arch_os_abi = "arm-linux-musleabihf",
            .cpu_features = "arm1176jzf_s",
        },
    ) catch |err| {
        return;
    });
    exe.setBuildMode(mode);
    exe.install();

    exe.strip = true;
    exe.linkLibC();

    exe.addObjectFile("libgpiod-1.6.2/foo/lib/libgpiod.a");
    exe.addIncludeDir("libgpiod-1.6.2/foo/include");
}

pub fn build(b: *Builder) void {
    // Standard release options allow the person running `zig build` to select
    // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
    const mode = b.standardReleaseOptions();

    const hello_gpio = b.addExecutable("hello-gpio", "./hello-gpio.zig");
    const toggle_led = b.addExecutable("toggle-led", "./toggle-led.zig");
    libgpio_build(hello_gpio, mode);
    libgpio_build(toggle_led, mode);
}

And here’s the program which simply toggles on/off an LED. I chose 2 as it’s the BCM number for the star LED at the top of my christmas tree.

toggle-led.zig
const sleep = @import("std").time.sleep;

const c = @cImport({
    @cInclude("gpiod.h");
});

const MyErrors = error{
    ChipOpenError,
    LineOpenError,
    RequestOutputError,
    SetValueError,
};

pub fn main() !void {
    // https://github.com/starnight/libgpiod-example/blob/master/libgpiod-led/main.c
    var chipname = "gpiochip0";
    const line_num = 2;
    var chip: *c.gpiod_chip = undefined;
    var line: *c.gpiod_line = undefined;

    chip = c.gpiod_chip_open_by_name(chipname) orelse return MyErrors.ChipOpenError;
    defer c.gpiod_chip_close(chip);

    line = c.gpiod_chip_get_line(chip, line_num) orelse return MyErrors.LineOpenError;
    defer c.gpiod_line_release(line);

    if (c.gpiod_line_request_output(line, "toggle-led", 0) < 0) {
        return MyErrors.RequestOutputError;
    }

    comptime var i = 20;
    var val = true;
    while (i > 0) {
        i -= 1;
        if (c.gpiod_line_set_value(line, if (val) 1 else 0) < 0) {
            return MyErrors.SetValueError;
        }
        val = !val;
        // 1 second in nanoseconds
        sleep(1000000000);
    }
}

Conclusion

Working with Raspberry Pi’s GPIO from a low-level is not too bad! I’m not too impressed with how big the binaries get (nearly 200K!) nor the process for including libgpiod in my program. I’d like to experiment with bypassing libgpiod and instead doing some direct ioctl work on the character device that’s exposed by Linux. Additionally, I’d like to try that as I think Zig’s async functionality might result in straightforward code try alongside LED manipulation. As always, Zig’s C interop is incredibly good and makes working with libgpiod a delight.