commit 92f0d28999ffe4cc7e3391a2b58d8b59c38842eb Author: Elara6331 Date: Mon Dec 11 19:20:44 2023 -0800 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8bc911a --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +zig-out/ +zig-cache/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..40bb089 --- /dev/null +++ b/README.md @@ -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. \ No newline at end of file diff --git a/_examples/blinky.zig b/_examples/blinky.zig new file mode 100644 index 0000000..8258e8e --- /dev/null +++ b/_examples/blinky.zig @@ -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); + } +} diff --git a/_examples/multi.zig b/_examples/multi.zig new file mode 100644 index 0000000..42b7dc9 --- /dev/null +++ b/_examples/multi.zig @@ -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); + } +} diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..560fbfa --- /dev/null +++ b/build.zig @@ -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); + } +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..8635c4a --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,5 @@ +.{ + .name = "zig-gpio", + .version = "0.0.1", + .paths = .{""}, +} diff --git a/gpio.zig b/gpio.zig new file mode 100644 index 0000000..10b6eb6 --- /dev/null +++ b/gpio.zig @@ -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(); + } +}; diff --git a/index.zig b/index.zig new file mode 100644 index 0000000..f78df3e --- /dev/null +++ b/index.zig @@ -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; diff --git a/uapi.zig b/uapi.zig new file mode 100644 index 0000000..4024b76 --- /dev/null +++ b/uapi.zig @@ -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))); +}