Pure Zig implementation of Linux peripheral I/O libraries — no C dependencies, no FFI, no
@cImport.
A ground-up Zig rewrite of c-periphery, providing idiomatic Zig APIs for common Linux embedded I/O interfaces: GPIO, I2C, SPI, serial (UART), PWM, LED, and memory-mapped I/O.
- Pure Zig — compiled entirely with Zig, no C headers, no system libraries required
- Cross-compilation ready — build for any
aarch64-linux-gnu,x86_64-linux-gnu, etc. - Idiomatic Zig API — error unions instead of return codes, comptime-checked constants
- Zig 0.17+ — uses
std.os.linuxraw syscalls, no@cImport - Zero heap allocation (optional) — all handles are stack-allocatable structs
| Module | Description | Interface |
|---|---|---|
gpio |
General Purpose I/O | GPIO v2 chardev (/dev/gpiochip*) |
i2c |
Inter-Integrated Circuit | ioctl (/dev/i2c-*) |
spi |
Serial Peripheral Interface | ioctl (/dev/spidev*) |
serial |
UART serial port | termios + poll |
pwm |
Pulse Width Modulation | sysfs (/sys/class/pwm/) |
led |
LED control | sysfs (/sys/class/leds/) |
mmio |
Memory-mapped I/O | mmap (/dev/mem) |
version |
Library version info | comptime constants |
- Zig ≥ 0.17.0-dev (tested with
0.17.0-dev.1426+58a94eaae) - Linux (targets
linux-gnu)
Add as a Zig build dependency:
zig fetch --save git+https://github.com/by965738071/zig-periphery.gitThen in your build.zig:
const c_periphery = b.dependency("zig_periphery", .{
.target = target,
.optimize = optimize,
});
// Add the module you need
const gpio_mod = c_periphery.module("gpio");
exe.root_module.addImport("gpio", gpio_mod);const std = @import("std");
const gpio = @import("gpio");
pub fn main() !void {
var g = gpio.Gpio.init();
defer g.close() catch {};
// Open GPIO line 23 as input on chip 0
try g.open("/dev/gpiochip0", 23, .in);
// Read value
const value = try g.read();
std.debug.print("GPIO 23: {}\n", .{value});
// Configure as output and write
try g.setDirection(.out_high);
try g.write(true);
}const i2c = @import("i2c");
pub fn main() !void {
var handle = try i2c.I2c.init(std.heap.page_allocator);
defer handle.deinit() catch {};
try handle.open("/dev/i2c-1");
// Write then read (I2C EEPROM example)
var tx_buf = [_]u8{ 0x00, 0x00 }; // register address
var rx_buf: [16]u8 = undefined;
var msgs = [_]i2c.I2cMsg{
.{ .addr = 0x50, .flags = 0, .len = 2, .buf = &tx_buf },
.{ .addr = 0x50, .flags = i2c.I2C_M_RD, .len = 16, .buf = &rx_buf },
};
try handle.transfer(&msgs);
}const spi = @import("spi");
pub fn main() !void {
var s = spi.Spi.init();
defer s.close() catch {};
// Open SPI bus 0, device 0, mode 0, 1MHz
try s.open("/dev/spidev0.0", 0, 1_000_000);
// Transfer data
var tx_buf = [_]u8{ 0xAA, 0xBB, 0xCC, 0xDD };
var rx_buf: [4]u8 = undefined;
try s.transfer(&tx_buf, &rx_buf, 4);
}const serial = @import("serial");
pub fn main() !void {
var s = serial.Serial.init();
defer s.close_port() catch {};
// Open at 115200 baud
try s.open_port("/dev/ttyUSB0", 115200);
// Write data
_ = try s.write_port("Hello UART!\n");
// Read with 100ms timeout
var buf: [128]u8 = undefined;
const n = try s.read_port(&buf, 100);
std.debug.print("Received {d} bytes\n", .{n});
}const pwm = @import("pwm");
pub fn main() !void {
var p = pwm.Pwm.init();
defer p.close() catch {};
// Open PWM chip 0, channel 0
try p.open(0, 0);
// Configure 1kHz, 50% duty cycle
try p.setPeriodNs(1_000_000); // 1ms period
try p.setDutyCycleNs(500_000); // 500us duty
try p.enable();
}const led = @import("led");
pub fn main() !void {
var l = try led.Led.init(std.heap.page_allocator);
defer l.deinit(std.heap.page_allocator);
try l.open("mmc0");
try l.write(true); // Turn on
try l.write(false); // Turn off
}const mmio = @import("mmio");
pub fn main() !void {
var m = mmio.Mmio.init();
defer m.close() catch {};
// Map a register at physical address 0x40000000, size 4096
try m.open(0x40000000, 4096);
// Read/write 32-bit register
const val = try m.read32(0x00);
try m.write32(0x04, 0xDEAD_BEEF);
}All modules return Zig error unions. Each handle carries an errmsg_buf and c_errno for detailed error info:
const g = gpio.Gpio.init();
g.open("/dev/gpiochip0", 999, .in) catch |err| {
std.debug.print("Error: {s}\n", .{g.errmsg()});
std.debug.print("errno: {d}\n", .{g.c_errno});
return err;
};# Build for aarch64-linux (cross-compile)
zig build -Dtarget=aarch64-linux-gnu --release=small
# Build for native
zig build --release=fast
# Run all tests
zig build testzig-periphery/
├── build.zig # Build configuration
├── build.zig.zon # Package manifest
├── src/
│ ├── gpio.zig # GPIO (chardev v2)
│ ├── i2c.zig # I2C (ioctl)
│ ├── spi.zig # SPI (ioctl)
│ ├── serial.zig # Serial/UART (termios)
│ ├── pwm.zig # PWM (sysfs)
│ ├── led.zig # LED (sysfs)
│ ├── mmio.zig # Memory-mapped I/O (mmap)
│ └── version.zig # Version info
├── c-periphery/ # Reference C source (not used at build time)
└── LICENSE
| c-periphery (C) | zig-periphery | |
|---|---|---|
| Language | C | Pure Zig |
| Dependencies | libc, kernel headers | None |
| Error handling | Integer return codes + errno | Zig error unions |
| Memory | malloc/free | Zig allocator (optional) |
| Build system | CMake/Make | Zig build system |
| Cross-compile | Manual toolchain setup | zig build -Dtarget=... |
| GPIO backend | chardev v2 + sysfs fallback | chardev v2 |
Tests cover pure logic (struct layouts, ioctl constants, conversions) and can run without hardware:
zig build testHardware-dependent tests (loopback, interactive) are not included — see c-periphery/tests/ for C reference tests.
MIT
- c-periphery by Ivan Googolev — the original C implementation this project is based on
- Zig — the programming language