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