Initial Commit

This commit is contained in:
Elara 2023-12-11 19:20:44 -08:00
commit 92f0d28999
9 changed files with 624 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
zig-out/
zig-cache/

39
README.md Normal file
View File

@ -0,0 +1,39 @@
# zig-gpio
**zig-gpio** is a Zig library for controlling GPIO lines on Linux systems
This library can be used to access GPIO on devices such as [Raspberry Pis](https://www.raspberrypi.com/) or the [Milk-V Duo](https://milkv.io/duo) (which is the board I created it for and tested it with).
This is my first public Zig project, so I'm open to any suggestions!
## Compatibility
**zig-gpio** uses the v2 character device API, which means it will work on any Linux system running kernel 5.10 or above. All you need to do is find out which `gpiochip` device controls which pin and what the offsets are, which you can do by either finding documentation online, or using the `gpiodetect` and `gpioinfo` tools from `libgpiod`.
I plan to eventually write a Zig replacement for `gpiodetect` and `gpioinfo`.
## Try it yourself!
Here's an example of a really simple program that requests pin 22 from `gpiochip2` and makes it blink at a 1 second interval. That pin offset is the LED of a Milk-V Duo board, so if you're using a different board, make sure to change it.
```zig
const std = @import("std");
const gpio = @import("gpio");
pub fn main() !void {
var chip = try gpio.getChip("/dev/gpiochip2");
defer chip.close();
std.debug.print("Chip Name: {s}\n", .{chip.name});
var line = try chip.requestLine(22, .{ .output = true });
defer line.close();
while (true) {
try line.setHigh();
std.time.sleep(std.time.ns_per_s);
try line.setLow();
std.time.sleep(std.time.ns_per_s);
}
}
```
For more examples, see the [_examples](_examples) directory. You can build all the examples using the `zig build examples` command.

19
_examples/blinky.zig Normal file
View File

@ -0,0 +1,19 @@
const std = @import("std");
const gpio = @import("gpio");
pub fn main() !void {
var chip = try gpio.getChip("/dev/gpiochip2");
defer chip.close();
try chip.setConsumer("blinky");
std.debug.print("Chip Name: {s}\n", .{chip.name});
var line = try chip.requestLine(22, .{ .output = true });
defer line.close();
while (true) {
try line.setHigh();
std.time.sleep(std.time.ns_per_s);
try line.setLow();
std.time.sleep(std.time.ns_per_s);
}
}

27
_examples/multi.zig Normal file
View File

@ -0,0 +1,27 @@
const std = @import("std");
const gpio = @import("gpio");
pub fn main() !void {
var chip = try gpio.getChip("/dev/gpiochip0");
defer chip.close();
try chip.setConsumer("multi");
std.debug.print("Chip Name: {s}\n", .{chip.name});
// Request the lines with offsets 26, 27, 28, and 29 as outputs.
var lines = try chip.requestLines(&.{ 26, 27, 28, 29 }, .{ .output = true });
defer lines.close();
// Alternate between lines 27/29 and 26/28 being high
while (true) {
// Set lines 27 and 29 as low (off)
try lines.setLow(&.{ 1, 3 });
// Set lines 26 and 28 as high (on)
try lines.setHigh(&.{ 0, 2 });
std.time.sleep(std.time.ns_per_s);
// Set lines 26 and 28 as low (off)
try lines.setLow(&.{ 0, 2 });
// Set lines 27 and 28 as high (on)
try lines.setHigh(&.{ 1, 3 });
std.time.sleep(std.time.ns_per_s);
}
}

31
build.zig Normal file
View File

@ -0,0 +1,31 @@
const std = @import("std");
pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
var gpio_module = b.createModule(.{ .source_file = .{ .path = "index.zig" } });
try b.modules.put(b.dupe("gpio"), gpio_module);
const examples_step = b.step("examples", "build all the examples");
inline for ([_]struct { name: []const u8, src: []const u8 }{
.{ .name = "blinky", .src = "_examples/blinky.zig" },
.{ .name = "multi", .src = "_examples/multi.zig" },
}) |cfg| {
const desc = try std.fmt.allocPrint(b.allocator, "build the {s} example", .{cfg.name});
const step = b.step(cfg.name, desc);
const exe = b.addExecutable(.{
.name = cfg.name,
.root_source_file = .{ .path = cfg.src },
.target = target,
.optimize = optimize,
});
exe.addModule("gpio", gpio_module);
const build_step = b.addInstallArtifact(exe, .{});
step.dependOn(&build_step.step);
examples_step.dependOn(&build_step.step);
}
}

5
build.zig.zon Normal file
View File

@ -0,0 +1,5 @@
.{
.name = "zig-gpio",
.version = "0.0.1",
.paths = .{""},
}

252
gpio.zig Normal file
View File

@ -0,0 +1,252 @@
const std = @import("std");
const gpio = @import("index.zig");
/// Opens the file at path and uses the file descriptor to get the gpiochip.
pub fn getChip(path: []const u8) !Chip {
var fl = try std.fs.openFileAbsolute(path, .{});
return try getChipByFd(fl.handle);
}
/// Same as `getChip` but the `path` parameter is null-terminated.
pub fn getChipZ(path: [*:0]const u8) !Chip {
var fl = try std.fs.openFileAbsoluteZ(path, .{});
return try getChipByFd(fl.handle);
}
/// Returns a `chip` with the given file descriptor.
pub fn getChipByFd(fd: std.os.fd_t) !Chip {
var info = try gpio.uapi.getChipInfo(fd);
return Chip{
.name = info.name,
.label = info.label,
.handle = fd,
.lines = info.lines,
};
}
/// Represents a single Linux `gpiochip` character device.
pub const Chip = struct {
/// The name of the `gpiochip` device.
name: [gpio.uapi.MAX_NAME_SIZE]u8,
/// The label of the `gpiochip` device
label: [gpio.uapi.MAX_NAME_SIZE]u8,
/// An optional consumer value to use when requesting lines.
/// Can be set using `set_consumer` or `set_consumer_z`.
/// If it isn't set, "zig-gpio" will be used instead.
consumer: ?[gpio.uapi.MAX_NAME_SIZE]u8 = null,
/// The file descriptor of the `gpiochip` device.
handle: std.os.fd_t,
// The amount of lines available under this device.
lines: u32,
closed: bool = false,
/// Sets the chip's consumer value to `consumer`.
pub fn setConsumer(self: *Chip, consumer: []const u8) !void {
if (consumer.len > gpio.uapi.MAX_NAME_SIZE) return error.ConsumerTooLong;
self.consumer = std.mem.zeroes([gpio.uapi.MAX_NAME_SIZE]u8);
std.mem.copyForwards(u8, &self.consumer.?, consumer);
}
/// Same as setConsumer but the `consumer` parameter is null-terminated.
pub fn setConsumerZ(self: *Chip, consumer: [*:0]const u8) !void {
self.consumer = std.mem.zeroes([gpio.uapi.MAX_NAME_SIZE]u8);
@memcpy(&self.consumer.?, consumer);
}
/// Returns information about the GPIO line at the given `offset`.
pub fn getLineInfo(self: Chip, offset: u32) !gpio.uapi.LineInfo {
if (self.closed) return error.ChipClosed;
return gpio.uapi.getLineInfo(self.handle, offset);
}
/// Requests and returns a single line at the given `offset`, from the given `chip`.
pub fn requestLine(self: Chip, offset: u32, flags: gpio.uapi.LineFlags) !Line {
var l = try self.requestLines(&.{offset}, flags);
return Line{ .lines = l };
}
/// Requests control of a collection of lines on the chip. If granted, control is maintained until
/// the `lines` are closed.
pub fn requestLines(self: Chip, offsets: []const u32, flags: gpio.uapi.LineFlags) !Lines {
if (self.closed) return error.ChipClosed;
if (offsets.len > gpio.uapi.MAX_LINES) return error.TooManyLines;
var lr = std.mem.zeroes(gpio.uapi.LineRequest);
lr.num_lines = @truncate(offsets.len);
lr.config.flags = flags;
if (self.consumer != null) {
lr.consumer = self.consumer.?;
} else {
std.mem.copyForwards(u8, &lr.consumer, "zig-gpio");
}
for (0.., offsets) |i, offset| {
if (offset >= self.lines) return error.OffsetOutOfRange;
lr.offsets[i] = offset;
}
const line_fd = try gpio.uapi.getLine(self.handle, lr);
return Lines{
.handle = line_fd,
.num_lines = lr.num_lines,
.offsets = offsets,
};
}
/// Releases all resources held by the `chip`.
pub fn close(self: *Chip) void {
if (self.closed) return;
self.closed = true;
std.os.close(self.handle);
}
};
/// Represents a collection of lines requested from a `chip`.
pub const Lines = struct {
/// The file descriptor of the lines.
handle: std.os.fd_t,
/// The amount of lines being controlled.
num_lines: u32,
/// The offsets of the lines being controlled.
offsets: []const u32,
closed: bool = false,
/// Sets the lines at the given indices as high (on).
///
/// Note that this function takes indices and not offsets.
/// The indices correspond to the index of the offset in your request.
/// For example, if you requested `&.{22, 20, 23}`,
/// `22` will correspond to `0`, `20` will correspond to `1`,
/// and `23` will correspond to `2`.
pub fn setHigh(self: Lines, indices: []const u32) !void {
if (self.closed) return error.LineClosed;
var vals = gpio.uapi.LineValues{};
for (indices) |index| {
if (index >= self.num_lines) return error.IndexOutOfRange;
vals.bits.set(index);
vals.mask.set(index);
}
try gpio.uapi.setLineValues(self.handle, vals);
}
/// Sets the lines at the given indices as low (off).
///
/// Note that this function takes indices and not offsets.
/// The indices correspond to the index of the offset in your request.
/// For example, if you requested `&.{22, 20, 23}`,
/// `22` will correspond to `0`, `20` will correspond to `1`,
/// and `23` will correspond to `2`.
pub fn setLow(self: Lines, indices: []const u32) !void {
if (self.closed) return error.LineClosed;
var vals = gpio.uapi.LineValues{};
for (indices) |index| {
if (index >= self.num_lines) return error.IndexOutOfRange;
vals.mask.set(index);
}
try gpio.uapi.setLineValues(self.handle, vals);
}
/// Sets the configuration flags of the lines at the given indices.
///
/// Note that this function takes indices and not offsets.
/// The indices correspond to the index of the offset in your request.
/// For example, if you requested `&.{22, 20, 23}`,
/// `22` will correspond to `0`, `20` will correspond to `1`,
/// and `23` will correspond to `2`.
pub fn reconfigure(self: Lines, indices: []const u32, flags: gpio.uapi.LineFlags) !void {
var lc = std.mem.zeroes(gpio.uapi.LineConfig);
lc.attrs[0] = gpio.uapi.LineConfigAttribute{
.attr = .{
.id = .Flags,
.data = .{ .flags = flags },
},
};
for (indices) |index| {
if (index >= self.num_lines) return error.IndexOutOfRange;
lc.attrs[0].mask.set(index);
}
try gpio.uapi.setLineConfig(self.handle, lc);
}
/// Sets the debounce period of the lines at the given indices.
///
/// Note that this function takes indices and not offsets.
/// The indices correspond to the index of the offset in your request.
/// For example, if you requested `&.{22, 20, 23}`,
/// `22` will correspond to `0`, `20` will correspond to `1`,
/// and `23` will correspond to `2`.
pub fn setDebouncePeriod(self: Lines, indices: []const u32, duration_us: u32) !void {
var lc = std.mem.zeroes(gpio.uapi.LineConfig);
lc.attrs[0] = gpio.uapi.LineConfigAttribute{
.attr = .{
.id = .Debounce,
.data = .{ .debounce_period_us = duration_us },
},
};
for (indices) |index| {
if (index >= self.num_lines) return error.IndexOutOfRange;
lc.attrs[0].mask.set(index);
}
try gpio.uapi.setLineConfig(self.handle, lc);
}
/// Returns the values of all the controlled lines as a bitset.
pub fn getValues(self: Lines) !gpio.uapi.LineValueBitset {
if (self.closed) return error.LineClosed;
const vals = try gpio.uapi.getLineValues(self.handle);
return vals.bits;
}
/// Releases all the resources held by the requested `lines`.
pub fn close(self: *Lines) void {
if (self.closed) return;
self.closed = true;
std.os.close(self.handle);
}
};
/// Represents a single line requested from a `chip`.
pub const Line = struct {
/// The `Lines` value containing the line.
lines: Lines,
/// Sets the line as high (on).
pub fn setHigh(self: Line) !void {
try self.lines.setHigh(&.{0});
}
/// Sets the line as low (off).
pub fn setLow(self: Line) !void {
try self.lines.setLow(&.{0});
}
/// Sets the configuration flags of the line.
pub fn reconfigure(self: Line, flags: gpio.uapi.LineFlags) !void {
try self.lines.reconfigure(&.{0}, flags);
}
/// Sets the debounce period of the line.
pub fn setDebouncePeriod(self: Line, duration_us: u32) !void {
try self.lines.setDebouncePeriod(&.{0}, duration_us);
}
/// Gets the current value of the line as a boolean.
pub fn getValue(self: Line) !bool {
const vals = try self.lines.getValues();
return vals.isSet(0);
}
/// Releases all the resources held by the `line`.
pub fn close(self: *Line) void {
self.lines.close();
}
};

7
index.zig Normal file
View File

@ -0,0 +1,7 @@
pub const uapi = @import("uapi.zig");
pub const getChip = @import("gpio.zig").getChip;
pub const getChipZ = @import("gpio.zig").getChipZ;
pub const Chip = @import("gpio.zig").Chip;
pub const Lines = @import("gpio.zig").Lines;
pub const Line = @import("gpio.zig").Line;

242
uapi.zig Normal file
View File

@ -0,0 +1,242 @@
const std = @import("std");
/// The maximum size of name and label arrays
pub const MAX_NAME_SIZE = 32;
/// Information about a certain GPIO chip
pub const ChipInfo = extern struct {
/// The Linux kernel name of this GPIO chip
name: [MAX_NAME_SIZE]u8,
/// A functional name for this GPIO chip, such as a product
/// number, may be empty (i.e. `label[0] == '\0'`)
label: [MAX_NAME_SIZE]u8,
/// The number of GPIO lines on this chip
lines: u32,
};
/// The maximum number of configuration attributes associated with a line request.
pub const MAX_LINE_NUM_ATTRS = 10;
/// Information about a certain GPIO line
pub const LineInfo = extern struct {
/// The name of this GPIO line, such as the output pin of the line on
/// the chip, a rail or a pin header name on a board, as specified by the
/// GPIO chip, may be empty (i.e. `name[0] == '\0'`)
name: [MAX_NAME_SIZE]u8,
/// a functional name for the consumer of this GPIO line as set
/// by whatever is using it, will be empty if there is no current user,
/// but may also be empty if the consumer doesn't set this up
consumer: [MAX_NAME_SIZE]u8,
/// The local offset on this GPIO chip, fill this in when
/// requesting the line information from the kernel
offset: u32,
/// The number of attributes in `attrs`
num_attrs: u32,
/// Configuration flags for this GPIO line
flags: LineFlags,
/// The configuration attributes associated with the line
attrs: [MAX_LINE_NUM_ATTRS]LineAttribute,
/// Reserved for future use
_padding: [4]u32,
};
/// LineAttribute ID values
pub const LineAttributeId = enum(u32) {
/// Indicates that the line attribute contains flags
Flags = 1,
/// Indicates that the line attribute contains output values
OutputValues = 2,
/// Indicates that the line attribute contains a debounce period
Debounce = 3,
};
/// A configurable attribute of a line
pub const LineAttribute = extern struct {
id: LineAttributeId,
_padding: u32 = 0,
data: extern union {
flags: LineFlags,
values: u64,
debounce_period_us: u32,
},
};
/// A configuration attribute
pub const LineConfigAttribute = extern struct {
attr: LineAttribute,
mask: LineValueBitset = .{ .mask = 0 },
};
/// Maximum number of requested lines
pub const MAX_LINES = 64;
/// Information about a request for GPIO lines
pub const LineRequest = extern struct {
offsets: [MAX_LINES]u32,
consumer: [MAX_NAME_SIZE]u8,
config: LineConfig,
num_lines: u32,
event_buffer_size: u32,
_padding: [5]u32 = [5]u32{ 0, 0, 0, 0, 0 },
fd: i32,
};
/// Configuration flags for GPIO lines
pub const LineFlags = packed struct {
/// Line is not available for request
used: bool = false,
/// Line active state is physical low
active_low: bool = false,
/// Line is an input
input: bool = false,
/// Line is an output
output: bool = false,
/// Line detects rising (inactive to active) edges
edge_rising: bool = false,
/// Line detects falling (active to inactive) edges
edge_falling: bool = false,
/// Line is an open drain output
open_drain: bool = false,
/// Line is an open source output
open_source: bool = false,
/// Line has pull-up bias enabled
bias_pull_up: bool = false,
/// Line has pull-down bias enabled
bias_pull_down: bool = false,
/// Line has bias disabled
bias_disabled: bool = false,
/// Line events contain REALTIME timestamps
event_clock_real_time: bool = false,
/// Line events contain timestamps from hardware timestamp engine
event_clock_hte: bool = false,
/// Reserved for future use
_padding: u51 = 0,
};
/// Configuration for GPIO lines
pub const LineConfig = extern struct {
/// Configuration flags for the GPIO lines. This is the default for
/// all requested lines but may be overridden for particular lines
/// using `attrs`.
flags: LineFlags,
/// The number of attributes in `attrs`
num_attrs: u32,
/// Reserved for future use and must be zero-filled
_padding: [5]u32 = [5]u32{ 0, 0, 0, 0, 0 },
/// The configuration attributes associated with the requested lines
attrs: [MAX_LINE_NUM_ATTRS]LineConfigAttribute,
};
/// A bitset representing GPIO line values
pub const LineValueBitset = std.bit_set.IntegerBitSet(MAX_LINES);
/// Values of GPIO lines
pub const LineValues = extern struct {
/// A bitmap containing the value of the lines, set to 1 for active
/// and 0 for inactive.
bits: LineValueBitset = .{ .mask = 0 },
/// A bitmap identifying the lines to get or set, with each bit
/// number corresponding to the index in LineRequest.offsets
mask: LineValueBitset = .{ .mask = 0 },
};
/// `LineInfoChanged.type` values
pub const ChangeType = enum(u32) {
/// Line has been requested
Requested = 1,
/// Line has been released
Released = 2,
/// Line has been reconfigured
Config = 3,
};
/// Information about a change in status of a GPIO line
pub const LineInfoChanged = extern struct {
/// Updated line information
info: LineInfo,
/// Estimate of the time when the status change occurred, in nanoseconds
timestamp_ns: u64,
/// The type of change
type: ChangeType,
/// Reserved for future use
_padding: [5]u32 = [5]u32{ 0, 0, 0, 0, 0 },
};
/// Returns an error based on the given return code
fn handleErrno(ret: usize) !void {
if (ret == 0) return;
return switch (std.os.errno(ret)) {
.BUSY => error.DeviceIsBusy,
.INVAL => error.InvalidArgument,
.BADF => error.BadFileDescriptor,
.NOTTY => error.InappropriateIOCTLForDevice,
.FAULT => unreachable,
else => |err| return std.os.unexpectedErrno(err),
};
}
/// Executes `GPIO_GET_CHIPINFO_IOCTL` on the given fd and returns the resulting
/// `ChipInfo` value
pub fn getChipInfo(fd: std.os.fd_t) !ChipInfo {
const req = std.os.linux.IOCTL.IOR(0xB4, 0x01, ChipInfo);
var info = std.mem.zeroes(ChipInfo);
try handleErrno(std.os.linux.ioctl(fd, req, @intFromPtr(&info)));
return info;
}
/// Executes `GPIO_V2_GET_LINEINFO_IOCTL` on the given fd and returns the resulting
/// `LineInfo` value
pub fn getLineInfo(fd: std.os.fd_t, offset: u32) !LineInfo {
const req = std.os.linux.IOCTL.IOWR(0xB4, 0x05, LineInfo);
var info = std.mem.zeroes(LineInfo);
info.offset = offset;
try handleErrno(std.os.linux.ioctl(fd, req, @intFromPtr(&info)));
return info;
}
/// Executes `GPIO_V2_GET_LINEINFO_WATCH_IOCTL` on the given fd and returns the resulting
/// `LineInfo` value
pub fn watchLineInfo(fd: std.os.fd_t, offset: u32) !LineInfo {
const req = std.os.linux.IOCTL.IOWR(0xB4, 0x06, LineInfo);
var info = std.mem.zeroes(LineInfo);
info.offset = offset;
try handleErrno(std.os.linux.ioctl(fd, req, @intFromPtr(&info)));
return info;
}
/// Executes `GPIO_GET_LINEINFO_UNWATCH_IOCTL` on the given fd
pub fn unwatchLineInfo(fd: std.os.fd_t, offset: u32) !void {
const req = std.os.linux.IOCTL.IOWR(0xB4, 0x0C, u32);
try handleErrno(std.os.linux.ioctl(fd, req, @intFromPtr(&offset)));
}
/// Executes `GPIO_V2_GET_LINE_IOCTL` on the given fd and returns the resulting
/// line descriptor
pub fn getLine(fd: std.os.fd_t, lr: LineRequest) !std.os.fd_t {
const lrp = &lr;
const req = std.os.linux.IOCTL.IOWR(0xB4, 0x07, LineRequest);
try handleErrno(std.os.linux.ioctl(fd, req, @intFromPtr(lrp)));
return lrp.fd;
}
/// Executes `GPIO_V2_LINE_GET_VALUES_IOCTL` on the given fd and returns the resulting
/// `LineValues` value
pub fn getLineValues(fd: std.os.fd_t) !LineValues {
const req = std.os.linux.IOCTL.IOWR(0xB4, 0x0E, LineValues);
var values = std.mem.zeroes(LineValues);
try handleErrno(std.os.linux.ioctl(fd, req, @intFromPtr(&values)));
return values;
}
/// Executes `GPIO_V2_LINE_SET_VALUES_IOCTL` on the given fd
pub fn setLineValues(fd: std.os.fd_t, lv: LineValues) !void {
const req = std.os.linux.IOCTL.IOWR(0xB4, 0x0F, LineValues);
try handleErrno(std.os.linux.ioctl(fd, req, @intFromPtr(&lv)));
}
/// Executes `GPIO_V2_LINE_SET_CONFIG_IOCTL` on the given fd
pub fn setLineConfig(fd: std.os.fd_t, lc: LineConfig) !void {
const req = std.os.linux.IOCTL.IOWR(0xB4, 0x0D, LineConfig);
try handleErrno(std.os.linux.ioctl(fd, req, @intFromPtr(&lc)));
}