Cover image for Low(ish) Level GPIO on the Raspberry Pi with Zig

Low(ish) Level GPIO on the Raspberry Pi with Zig

・6 min read

One of my goals is to use Zig to gain a better understanding of systems and embedded programming. So I decided to undertake a Raspberry Pi project, which is still very much a work in progress. I use a Raspberry Pi 2B, but the ideas should be transferable to newer models with minimal modification.


For my project, I need to read from and write to the GPIO pins of the Pi. There are no hard real-time requirements, so I'm fine with using the Raspian operating system.

In an effort too boring to write down, I never quite succeeded in using and cross-compiling existing GPIO libraries (like pigpio) with Zig. Despite having no clue how to talk to a GPIO pin on my own, I thought: why don't I ...

Rewrite it in Zig

The first thing I needed to understand is how to talk to the GPIO pins. This blog post by Pieter-Jan Van de Maele and the source code for TinyGPIO and MinimalGPIO were very helpful, but there is no way around studying the BCM2835 ARM Peripherals Manual (henceforth "The Manual").

Memory Mapped IO

Reading from and writing to the GPIO pins is achieved by simply reading from and writing to certain associated registers, which are located at dedicated physical memory addresses. Any program running "normally" in Linux will run in so-called user-space and cannot simply access arbitrary physical addresses.

The most basic way to access physical memory from user space is using the mmap(2) system call to map dev/mem. There are many references out there with good explanations, so let's gloss over the details here. The important thing is that dev/mem exposes all physical memory and we have to find the GPIO registers manually by looking in the appropriate places in the documentation. Luckily, the Raspberry Pi is nice enough to expose another device dev/gpiomem, which covers just the GPIO registers.

In the Manual we see that there are 41 GPIO registers of 32-bit each. So we'll define some helper types and constants before mapping the memory:

// type for one register
const GpioRegister = u32;
// the whole register memory as a slice
const GpioRegisterSlice = []align(1) volatile GpioRegister;
// the file descriptor
const devgpiomem = try std.fs.openFileAbsolute("/dev/gpiomem", std.fs.File.OpenFlags{ .read = true, .write = true });
defer devgpiomem.close();
// this is how we map the memory...
var g_raw_mem = try std.os.mmap(null, NUM_GPIO_REGISTERS * @sizeOf(GpioRegister), std.os.PROT.READ | std.os.PROT.WRITE, std.os.MAP.SHARED, devgpiomem.handle, 0);
//... and convert it to the slice
var g_gpio_registers: GpioRegisterSlice = std.mem.bytesAsSlice(GpioRegister, g_raw_mem);
Enter fullscreen mode Exit fullscreen mode

Note that the volatile here is one of the few justified use cases of this keyword. We can now use g_gpio_registers to perform reads and writes on the registers. Using the table in section 6.1 of the Manual we learn that the register at index 0 of our slice is some GPFSEL0.

Interacting with the Registers

I won't go into too much detail of how to interact with the registers because the principle is very nicely explained in Pieter-Jans blog post. I can say that I really enjoyed Zig expressiveness and type safety while implementing this functionality.

Expressive Code with Enums

As an example, let's have a look at selecting the function for a GPIO pin. For that, there are 6 function select registers GPFSEL0,..., GPFSEL5, which are contiguous in memory. Each pin is associated with a 3-bit block that we can use to set / read the pin mode. For example 000 sets a pin to output mode, while 001 is input. Pin 0 is located at the 3 least significant bits of register GPFSEL0 and then we move to the higher bits in 3 bit blocks until we hit pin 53 (the highest GPIO pin number) somewhere in GPFSEL5.

To encode the pin modes I used an enum and gave it a 3-bit wide tag type with the variant values in convenient binary notation:

const Mode = enum(u3) {
    Input = 0b000,
    Output = 0b001,
    Alternate0 = 0b100,
    Alternate1 = 0b101,
    Alternate2 = 0b110,
    Alternate3 = 0b111,
    Alternate4 = 0b011,
    Alternate5 = 0b010,
Enter fullscreen mode Exit fullscreen mode

To set the mode for a pin we can define a function like so:

pub fn setMode(pin_number: u8, mode: Mode) Error!void {
    try checkPinNumber(pin_number, bcm2835.BoardInfo);
    const pins_per_register = comptime @divTrunc(@bitSizeOf(GpioRegister), @bitSizeOf(Mode));
    const gpfsel_register_zero = comptime gpioRegisterZeroIndex("GPFSEL", bcm2835.BoardInfo);
    const n: @TypeOf(pin_number) = @divTrunc(pin_number, pins_per_register);
    g_gpio_registers[gpfsel_register_zero + n] &= clearMask(pin_number);
    g_gpio_registers[gpfsel_register_zero + n] |= modeMask(pin_number, mode); 
Enter fullscreen mode Exit fullscreen mode

We can get the number of pins_per_register without using any magic numbers, which is nice. Furthermore, I have defined a function gpioRegisterZeroIndex which takes a BoardInfo structure and allows me to access the zero-register (like GPFSEL0, GPLEV0, ...) by name at compile time. Finally, I have to clear the three bit block corresponding to the pin, before writing the actual mode to it by bitwise or-ing an appropriate mask to it. The mask that sets the mode is created like so:

inline fn modeMask(pin_number: u8, mode: Mode) peripherals.GpioRegister {
    const pins_per_register = comptime @divTrunc(@bitSizeOf(peripherals.GpioRegister), @bitSizeOf(Mode));
    const pin_bit_idx = pin_number % pins_per_register;
    return @intCast(peripherals.GpioRegister, @enumToInt(mode)) << @intCast(u5, (pin_bit_idx * @bitSizeOf(Mode)));
Enter fullscreen mode Exit fullscreen mode

It produces a 32bit word with all zeros except at the 3-bit block that sets the new mode. The clearMask function works very similar, but produces a bit with all ones, except a 3-bit block of zeroes at the block corresponding to the pin. Since the input mode is already binary 000 we could omit the mode mask in this case, but I chose not to over-optimize at this point.

I like the readability that the enum brings. It gives me type safety, but the underlying numeric value is still accessible. It is even cooler when we read a mode, because we can use an inline for loop over the variants of the enum to compare it against the value in the register, which makes for very neat and expressive code.

Let's leave it at this for how to manipulate the pins and have a look how we can design our code such that we can develop and test rapidly.

Designing for Testing

Up to now, I have used the global variable g_gpio_registers to access the registers. This is not quite what I did in my code. First of all, I stuck all my functionality (like setMode, getMode, setLevel, getLevel,...) in a gpio.zig file. I did use a global variable g_gpio_registers, but I made it an optional type ?GpioRegisterSlice. I then require an init function to be called first or all other functions just fail with an Error.Uninitialized.

This init function takes an interface, namely a pointer to an instace of GpioMemInterface. The interface abstracts the call to mmap and exposes a function memoryMap that is expected to provide access to registers as a slice.

pub const GpioMemInterface = struct {
    map_fn: fn (*GpioMemInterface) anyerror!GpioRegisterSlice,
    pub fn memoryMap(interface: *GpioMemInterface) !GpioRegisterSlice {
        return interface.map_fn(interface);
Enter fullscreen mode Exit fullscreen mode

Note that this is the old way of designing interfaces and a potentially faster way has recently been established.

What's nice about this is that we can create two implementations: first a real Bcm2835MemoryInterface that maps dev/gpiomem on the Pi, but also a MockMemoryInterface, which just returns some other appropriately sized slice of memory. We can use the latter on our host machines to test that the implementation is sound. Since all exposed GPIO functions just mess with the bits in the registers, all we need to do is verify that they do that correctly. There's no need for plugging LEDs into our Pi and scratching our heads why they won't light up yet. Using Zig's great testing support, this makes for pretty fun testing on the host and helped me to get a simple demo running on the Pi pretty quickly.

Summary and Outlook

I have given an overview and a behind the scenes of a very simple GPIO library. You can check out the source code here, but beware that this is a work in progress. The next step would be to expose a functionality that invokes custom callbacks when events are detected on the pins. As of now, I have no idea how to do this elegantly, which makes me as clueless about this aspect as I had been about GPIO access when I started the project.

Discussion (4)

kristoff profile image
Loris Cro

Thank you for sharing! Looking forward to see more Zig dev in the embedded space and one day to be able to program my custom keyboard in Zig, I really don't like the qmk toolchain.

geo_ant profile image
Geo Author

Thanks, I agree. Zig is such a great fit for the embedded ecosystem and it's only getting better. Since you mentioned keyboard firmware, are you aware of the article Why I rewrote my Rust keyboard firmware in Zig by Kevin Lynagh?

pixeli profile image
Tommi Sinivuo

Thanks - this is a great post! I’m just doing some GPIO stuff in Zig, so this gave me some good ideas about how I could approach abstracting the details away.

geo_ant profile image
Geo Author

Very cool, I'd be interested to see how you go about your project and would love to read about it some day.