diff --git a/.gitignore b/.gitignore index 8de4d7a4..5c171958 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,11 @@ -zig-out/ -zig-cache/ -.zig-cache/ -microzig-deploy/ +__pycache__/ +.direnv/ .DS_Store .gdbinit .lldbinit -.direnv/ -__pycache__/ .venv +.zig-cache/ boxzer-out +microzig-deploy/ +zig-cache/ +zig-out/ diff --git a/build/build.zig b/build/build.zig index 35b7a5ff..b64cf05b 100644 --- a/build/build.zig +++ b/build/build.zig @@ -2,6 +2,7 @@ host_build: *Build, self: *Build.Dependency, microzig_core: *Build.Dependency, +drivers_dep: *Build.Dependency, generate_linkerscript: *Build.Step.Compile, const std = @import("std"); @@ -48,12 +49,14 @@ pub fn init(b: *Build, opts: struct { }) *MicroZig { const mz_dep = b.dependency(opts.dependency_name, .{}); const core_dep = mz_dep.builder.dependency("microzig/core", .{}); + const drivers_dep = mz_dep.builder.dependency("microzig/drivers", .{}); const ret = b.allocator.create(MicroZig) catch @panic("OOM"); ret.* = MicroZig{ .host_build = b, .self = mz_dep, .microzig_core = core_dep, + .drivers_dep = drivers_dep, .generate_linkerscript = mz_dep.builder.addExecutable(.{ .name = "generate-linkerscript", .root_source_file = .{ .cwd_relative = comptime root() ++ "/src/generate_linkerscript.zig" }, @@ -146,6 +149,10 @@ pub fn add_firmware( .name = "config", .module = micro_build.createModule(.{ .root_source_file = config.getSource() }), }, + .{ + .name = "drivers", + .module = mz.drivers_dep.module("drivers"), + }, }, }), .cpu = undefined, diff --git a/build/build.zig.zon b/build/build.zig.zon index 202b3c3c..b5fb1740 100644 --- a/build/build.zig.zon +++ b/build/build.zig.zon @@ -14,6 +14,9 @@ .@"microzig/build/definitions" = .{ .path = "definitions", }, + .@"microzig/drivers" = .{ + .path = "../drivers", + }, }, .paths = .{ diff --git a/core/src/drivers.zig b/core/src/drivers.zig deleted file mode 100644 index 6ed28973..00000000 --- a/core/src/drivers.zig +++ /dev/null @@ -1 +0,0 @@ -pub const experimental = @import("drivers/experimental.zig"); diff --git a/core/src/drivers/experimental.zig b/core/src/drivers/experimental.zig deleted file mode 100644 index a3ec7277..00000000 --- a/core/src/drivers/experimental.zig +++ /dev/null @@ -1,7 +0,0 @@ -//! These are experimental driver interfaces. We want to see more use and -//! discussion of them before committing them to microzig's "official" API. -//! -//! They are bound to have breaking changes in the future, or even disappear, -//! so use at your own risk. -pub const button = @import("experimental/button.zig"); -pub const quadrature = @import("experimental/quadrature.zig"); diff --git a/core/src/drivers/experimental/button.zig b/core/src/drivers/experimental/button.zig deleted file mode 100644 index 7667ac6e..00000000 --- a/core/src/drivers/experimental/button.zig +++ /dev/null @@ -1,67 +0,0 @@ -const std = @import("std"); -const micro = @import("microzig"); - -pub const Event = enum { - /// Nothing has changed. - idle, - - /// The button was pressed. Will only trigger once per press. - /// Use `Button.isPressed()` to check if the button is currently held. - pressed, - - /// The button was released. Will only trigger once per release. - /// Use `Button.isPressed()` to check if the button is currently held. - released, -}; - -pub fn Button( - /// The GPIO pin the button is connected to. Will be initialized when calling Button.init - comptime gpio: type, - /// The active state for the button. Use `.high` for active-high, `.low` for active-low. - comptime active_state: micro.gpio.State, - /// Optional filter depth for debouncing. If `null` is passed, 16 samples are used to debounce the button, - /// otherwise the given number of samples is used. - comptime filter_depth: ?comptime_int, -) type { - return struct { - const Self = @This(); - const DebounceFilter = std.meta.Int(.unsigned, filter_depth orelse 16); - - debounce: DebounceFilter, - state: micro.gpio.State, - - pub fn init() Self { - gpio.init(); - return Self{ - .debounce = 0, - .state = gpio.read(), - }; - } - - /// Polls for the button state. Returns the change event for the button if any. - pub fn poll(self: *Self) Event { - const state = gpio.read(); - const active_unfiltered = (state == active_state); - - const previous_debounce = self.debounce; - self.debounce <<= 1; - if (active_unfiltered) { - self.debounce |= 1; - } - - if (active_unfiltered and previous_debounce == 0) { - return .pressed; - } else if (!active_unfiltered and self.debounce == 0 and previous_debounce != 0) { - return .released; - } else { - return .idle; - } - } - - /// Returns `true` when the button is pressed. - /// Will only be updated when `poll` is regularly called. - pub fn is_pressed(self: *Self) bool { - return (self.debounce != 0); - } - }; -} diff --git a/core/src/drivers/experimental/quadrature.zig b/core/src/drivers/experimental/quadrature.zig deleted file mode 100644 index c76ecabd..00000000 --- a/core/src/drivers/experimental/quadrature.zig +++ /dev/null @@ -1,50 +0,0 @@ -const micro = @import("microzig"); - -pub const Event = enum { - /// No change since the last decoding happened - idle, - /// The quadrature signal incremented a step. - increment, - /// The quadrature signal decremented a step. - decrement, - /// The quadrature signal skipped a sequence point and entered a invalid state. - @"error", -}; - -pub fn Decoder(comptime pin_a: type, comptime pin_b: type) type { - return struct { - const Self = @This(); - - last_a: micro.gpio.State, - last_b: micro.gpio.State, - - pub fn init() Self { - pin_a.init(); - pin_b.init(); - return Self{ - .last_a = pin_a.read(), - .last_b = pin_b.read(), - }; - } - - pub fn tick(self: *Self) Event { - var a = pin_a.read(); - var b = pin_b.read(); - defer self.last_a = a; - defer self.last_b = b; - - const enable = a.value() ^ b.value() ^ self.last_a.value() ^ self.last_b.value(); - const direction = a.value() ^ self.last_b.value(); - - if (enable != 0) { - if (direction != 0) { - return .increment; - } else { - return .decrement; - } - } else { - return .idle; - } - } - }; -} diff --git a/core/src/microzig.zig b/core/src/microzig.zig index d77e741b..2b85c292 100644 --- a/core/src/microzig.zig +++ b/core/src/microzig.zig @@ -31,10 +31,12 @@ pub const hal = if (config.has_hal) @import("hal") else void; /// Provides access to board features or is `void` when no board is present. pub const board = if (config.has_board) @import("board") else void; +/// Contains device-independent drivers for peripherial devices. +pub const drivers = @import("drivers"); + pub const mmio = @import("mmio.zig"); pub const interrupt = @import("interrupt.zig"); pub const core = @import("core.zig"); -pub const drivers = @import("drivers.zig"); pub const utilities = @import("utilities.zig"); /// The microzig default panic handler. Will disable interrupts and loop endlessly. diff --git a/drivers/README.md b/drivers/README.md new file mode 100644 index 00000000..d05e4161 --- /dev/null +++ b/drivers/README.md @@ -0,0 +1,20 @@ +# microzig-driver-framework + +A collection of device drivers for the use with MicroZig. + +## Drivers + +> Drivers with a checkmark are already implemented, drivers without are missing + +- Input + - [x] Keyboard Matrix + - [x] Rotary Encoder + - [x] Debounced Button + - Touch + - [ ] [XPT2046](https://github.com/ZigEmbeddedGroup/microzig/issues/247) +- Display + - [x] SSD1306 (I²C works, [3-wire SPI](https://github.com/ZigEmbeddedGroup/microzig/issues/251) and [4-wire SPI](https://github.com/ZigEmbeddedGroup/microzig/issues/252) are missing) + - [ ] [ST7735](https://github.com/ZigEmbeddedGroup/microzig/issues/250) (WIP) + - [ ] [ILI9488](https://github.com/ZigEmbeddedGroup/microzig/issues/249) +- Wireless + - [ ] [SX1276, SX1278](https://github.com/ZigEmbeddedGroup/microzig/issues/248) diff --git a/drivers/base/Datagram_Device.zig b/drivers/base/Datagram_Device.zig new file mode 100644 index 00000000..f455acc1 --- /dev/null +++ b/drivers/base/Datagram_Device.zig @@ -0,0 +1,309 @@ +//! +//! An abstract datagram orientied device with runtime dispatch. +//! +//! Datagram devices behave similar to an SPI or Ethernet device where +//! packets with an ahead-of-time known length can be transferred in a +//! single transaction. +//! + +const std = @import("std"); + +const Datagram_Device = @This(); + +/// Pointer to the object implementing the driver. +/// +/// If the implementation requires no `object` pointer, +/// you can safely use `undefined` here. +object: *anyopaque, + +/// Virtual table for the datagram device functions. +vtable: *const VTable, + +const BaseError = error{ IoError, Timeout }; + +pub const ConnectError = BaseError || error{DeviceBusy}; + +/// Establishes a connection to the device (like activating a chip-select lane or similar). +/// NOTE: Call `.disconnect()` when the usage of the device is done to release it. +pub fn connect(dd: Datagram_Device) ConnectError!void { + if (dd.vtable.connect_fn) |connectFn| { + return connectFn(dd.object); + } +} + +/// Releases a device from the connection. +pub fn disconnect(dd: Datagram_Device) void { + if (dd.vtable.disconnect_fn) |disconnectFn| { + return disconnectFn(dd.object); + } +} + +pub const WriteError = BaseError || error{ Unsupported, NotConnected }; + +/// Writes a single `datagram` to the device. +pub fn write(dd: Datagram_Device, datagram: []const u8) WriteError!void { + return try dd.writev(&.{datagram}); +} + +/// Writes a single `datagram` to the device. +pub fn writev(dd: Datagram_Device, datagrams: []const []const u8) WriteError!void { + const writev_fn = dd.vtable.writev_fn orelse return error.Unsupported; + return writev_fn(dd.object, datagrams); +} + +pub const ReadError = BaseError || error{ Unsupported, NotConnected, BufferOverrun }; + +/// Reads a single `datagram` from the device. +/// Function returns the number of bytes written in `datagram`. +/// +/// If `error.BufferOverrun` is returned, the `datagram` will stilled be fully filled +/// with the data that was received up till the overrun. The rest of the datagram +/// will be discarded. +pub fn read(dd: Datagram_Device, datagram: []u8) ReadError!usize { + return try dd.readv(&.{datagram}); +} + +/// Reads a single `datagram` from the device. +/// Function returns the number of bytes written in `datagrams`. +/// +/// If `error.BufferOverrun` is returned, the `datagrams` will stilled be fully filled +/// with the data that was received up till the overrun. The rest of the datagram +/// will be discarded. +pub fn readv(dd: Datagram_Device, datagrams: []const []u8) ReadError!usize { + const readv_fn = dd.vtable.readv_fn orelse return error.Unsupported; + return readv_fn(dd.object, datagrams); +} + +pub const VTable = struct { + connect_fn: ?*const fn (*anyopaque) ConnectError!void, + disconnect_fn: ?*const fn (*anyopaque) void, + writev_fn: ?*const fn (*anyopaque, datagrams: []const []const u8) WriteError!void, + readv_fn: ?*const fn (*anyopaque, datagrams: []const []u8) ReadError!usize, +}; + +/// A device implementation that can be used to write unit tests for datagram devices. +pub const Test_Device = struct { + arena: std.heap.ArenaAllocator, + packets: std.ArrayList([]u8), + + // If empty, reads are supported, but don't yield data. + // If `null`, reads are not supported. + input_sequence: ?[]const []const u8, + input_sequence_pos: usize, + + write_enabled: bool, + + connected: bool, + + pub fn init_receiver_only() Test_Device { + return init(null, true); + } + + pub fn init_sender_only(input: []const []const u8) Test_Device { + return init(input, false); + } + + pub fn init(input: ?[]const []const u8, write_enabled: bool) Test_Device { + return Test_Device{ + .arena = std.heap.ArenaAllocator.init(std.testing.allocator), + .packets = std.ArrayList([]u8).init(std.testing.allocator), + + .input_sequence = input, + .input_sequence_pos = 0, + + .write_enabled = write_enabled, + + .connected = false, + }; + } + + pub fn deinit(td: *Test_Device) void { + td.arena.deinit(); + td.packets.deinit(); + td.* = undefined; + } + + pub fn expect_sent(td: Test_Device, expected_datagrams: []const []const u8) !void { + const actual_datagrams = td.packets.items; + + try std.testing.expectEqual(expected_datagrams.len, actual_datagrams.len); + for (expected_datagrams, actual_datagrams) |expected, actual| { + try std.testing.expectEqualSlices(u8, expected, actual); + } + } + + pub fn datagram_device(td: *Test_Device) Datagram_Device { + return Datagram_Device{ + .object = td, + .vtable = &vtable, + }; + } + + fn connect(ctx: *anyopaque) ConnectError!void { + const td: *Test_Device = @ptrCast(@alignCast(ctx)); + if (td.connected) + return error.DeviceBusy; + td.connected = true; + } + + fn disconnect(ctx: *anyopaque) void { + const td: *Test_Device = @ptrCast(@alignCast(ctx)); + if (!td.connected) { + std.log.err("disconnect when test device was not connected!", .{}); + } + td.connected = false; + } + + fn writev(ctx: *anyopaque, datagrams: []const []const u8) WriteError!void { + const td: *Test_Device = @ptrCast(@alignCast(ctx)); + + if (!td.connected) { + return error.NotConnected; + } + + if (!td.write_enabled) { + return error.Unsupported; + } + + const total_len = blk: { + var len: usize = 0; + for (datagrams) |dg| { + len += dg.len; + } + break :blk len; + }; + + const dg = td.arena.allocator().alloc(u8, total_len) catch return error.IoError; + errdefer td.arena.allocator().free(dg); + + { + var offset: usize = 0; + for (datagrams) |datagram| { + @memcpy(dg[offset..][0..datagram.len], datagram); + offset += datagram.len; + } + std.debug.assert(offset == total_len); + } + + td.packets.append(dg) catch return error.IoError; + } + + fn readv(ctx: *anyopaque, datagrams: []const []u8) ReadError!usize { + const td: *Test_Device = @ptrCast(@alignCast(ctx)); + + if (!td.connected) { + return error.NotConnected; + } + + const inputs = td.input_sequence orelse return error.Unsupported; + + if (td.input_sequence_pos >= inputs.len) { + return error.IoError; + } + + const packet = inputs[td.input_sequence_pos]; + td.input_sequence_pos += 1; + + const total_len = blk: { + var len: usize = 0; + for (datagrams) |dg| { + len += dg.len; + } + break :blk len; + }; + + const written = @min(packet.len, total_len); + + { + var offset: usize = 0; + for (datagrams) |datagram| { + const amount = @min(datagram.len, written - offset); + @memcpy(datagram[0..amount], packet[offset..][0..amount]); + offset += amount; + if (amount < datagram.len) + break; + } + std.debug.assert(offset == written); + } + + if (packet.len > total_len) + return error.BufferOverrun; + + return written; + } + + const vtable = VTable{ + .connect_fn = Test_Device.connect, + .disconnect_fn = Test_Device.disconnect, + .writev_fn = Test_Device.writev, + .readv_fn = Test_Device.readv, + }; +}; + +test Test_Device { + var td = Test_Device.init(&.{ + "first datagram", + "second datagram", + "the very third datagram which overruns the buffer", + }, true); + defer td.deinit(); + + var buffer: [16]u8 = undefined; + + const dd = td.datagram_device(); + + // As long as we're not connected, the test device will handle + // this case and yield an error: + try std.testing.expectError(error.NotConnected, dd.write("not connected")); + try std.testing.expectError(error.NotConnected, dd.read(&buffer)); + + { + // The first connect call must succeed ... + try dd.connect(); + + // ... while the second call must fail: + try std.testing.expectError(error.DeviceBusy, dd.connect()); + + // After a disconnect... + dd.disconnect(); + + // ... the connect must succeed again: + try dd.connect(); + + // We'll keep the device connected for the rest of the test to + // ease handling. + } + + { + // The first input datagram will be received here: + const recv_len = try dd.read(&buffer); + try std.testing.expectEqualStrings("first datagram", buffer[0..recv_len]); + } + + { + // The second one here: + const recv_len = try dd.read(&buffer); + try std.testing.expectEqualStrings("second datagram", buffer[0..recv_len]); + } + + { + // The third datagram will overrun our buffer, so we're receiving an error + // which tells us that the whole buffer is filled, but there's data that + // was discarded: + try std.testing.expectError(error.BufferOverrun, dd.read(&buffer)); + try std.testing.expectEqualStrings("the very third d", &buffer); + } + + // As there's no fourth datagram available, the test device will yield + // an `IoError` for when no datagrams are available anymore: + try std.testing.expectError(error.IoError, dd.read(&buffer)); + + try dd.write("Hello, World!"); + try dd.writev(&.{ "See", " you ", "soon!" }); + + // Check if we had exactly these datagrams: + try td.expect_sent(&.{ + "Hello, World!", + "See you soon!", + }); +} diff --git a/drivers/base/Digital_IO.zig b/drivers/base/Digital_IO.zig new file mode 100644 index 00000000..ddb852d5 --- /dev/null +++ b/drivers/base/Digital_IO.zig @@ -0,0 +1,148 @@ +//! +//! An abstract digital input/output pin. +//! +//! Digital I/Os can be used to drive single-wire data +//! + +const std = @import("std"); + +const Digital_IO = @This(); + +/// Pointer to the object implementing the driver. +/// +/// If the implementation requires no `object` pointer, +/// you can safely use `undefined` here. +object: *anyopaque, + +/// Virtual table for the digital i/o functions. +vtable: *const VTable, + +const BaseError = error{ IoError, Timeout }; + +pub const SetDirError = BaseError || error{Unsupported}; +pub const SetBiasError = BaseError || error{Unsupported}; +pub const WriteError = BaseError || error{Unsupported}; +pub const ReadError = BaseError || error{Unsupported}; + +pub const State = enum(u1) { + low = 0, + high = 1, + + pub inline fn invert(state: State) State { + return @as(State, @enumFromInt(~@intFromEnum(state))); + } + + pub inline fn value(state: State) u1 { + return @intFromEnum(state); + } +}; +pub const Direction = enum { input, output }; + +/// Sets the direction of the pin. +pub fn set_direction(dio: Digital_IO, dir: Direction) SetDirError!void { + return dio.vtable.set_direction_fn(dio.object, dir); +} + +/// Sets if the pin has a bias towards either `low` or `high` or no bias at all. +/// Bias is usually implemented with pull-ups and pull-downs. +pub fn set_bias(dio: Digital_IO, bias: ?State) SetBiasError!void { + return dio.vtable.set_bias_fn(dio.object, bias); +} + +/// Changes the state of the pin. +pub fn write(dio: Digital_IO, state: State) WriteError!void { + return dio.vtable.write_fn(dio.object, state); +} + +/// Reads the state state of the pin. +pub fn read(dio: Digital_IO) ReadError!State { + return dio.vtable.read_fn(dio.object); +} + +pub const VTable = struct { + set_direction_fn: *const fn (*anyopaque, dir: Direction) SetDirError!void, + set_bias_fn: *const fn (*anyopaque, bias: ?State) SetBiasError!void, + write_fn: *const fn (*anyopaque, state: State) WriteError!void, + read_fn: *const fn (*anyopaque) ReadError!State, +}; + +pub const Test_Device = struct { + state: State, + dir: Direction, + + pub fn init(initial_dir: Direction, initial_state: State) Test_Device { + return Test_Device{ + .dir = initial_dir, + .state = initial_state, + }; + } + + pub fn digital_io(dev: *Test_Device) Digital_IO { + return Digital_IO{ + .object = dev, + .vtable = &vtable, + }; + } + + fn set_direction(ctx: *anyopaque, dir: Direction) SetDirError!void { + const dev: *Test_Device = @ptrCast(@alignCast(ctx)); + dev.dir = dir; + } + + fn set_bias(ctx: *anyopaque, bias: ?State) SetBiasError!void { + const dev: *Test_Device = @ptrCast(@alignCast(ctx)); + _ = dev; + _ = bias; + } + + fn write(ctx: *anyopaque, state: State) WriteError!void { + const dev: *Test_Device = @ptrCast(@alignCast(ctx)); + if (dev.dir != .output) + return error.Unsupported; + dev.state = state; + } + + fn read(ctx: *anyopaque) ReadError!State { + const dev: *Test_Device = @ptrCast(@alignCast(ctx)); + return dev.state; + } + + const vtable = VTable{ + .set_direction_fn = Test_Device.set_direction, + .set_bias_fn = Test_Device.set_bias, + .write_fn = Test_Device.write, + .read_fn = Test_Device.read, + }; +}; + +test Test_Device { + var td = Test_Device.init(.input, .high); + + const io = td.digital_io(); + + // Check if the initial state is correct: + try std.testing.expectEqual(.high, try io.read()); + + // Check if we can change the state + td.state = .low; + try std.testing.expectEqual(.low, try io.read()); + + // Check if we can change the state + td.state = .high; + try std.testing.expectEqual(.high, try io.read()); + + // We're currently in "input" state, so we can't write the pin level: + try std.testing.expectError(error.Unsupported, io.write(.low)); + + try io.set_direction(.output); + try std.testing.expectEqual(.output, td.dir); + + // Changing direction should not change the current level: + try std.testing.expectEqual(.high, try io.read()); + + try io.write(.low); + try std.testing.expectEqual(.low, td.state); + + try io.write(.high); + try std.testing.expectEqual(.high, td.state); +} diff --git a/drivers/base/Stream_Device.zig b/drivers/base/Stream_Device.zig new file mode 100644 index 00000000..5ece44e0 --- /dev/null +++ b/drivers/base/Stream_Device.zig @@ -0,0 +1,312 @@ +//! +//! An abstract stream orientied device with runtime dispatch. +//! +//! Stream devices behave similar to an UART and can send/receive data +//! in variying lengths without clear boundaries between transmissions. +//! + +const std = @import("std"); + +const Stream_Device = @This(); + +/// Pointer to the object implementing the driver. +/// +/// If the implementation requires no `object` pointer, +/// you can safely use `undefined` here. +object: *anyopaque, + +/// Virtual table for the stream device functions. +vtable: *const VTable, + +const BaseError = error{ IoError, Timeout }; + +pub const ConnectError = BaseError || error{DeviceBusy}; +pub const WriteError = BaseError || error{ Unsupported, NotConnected }; +pub const ReadError = BaseError || error{ Unsupported, NotConnected }; + +/// Establishes a connection to the device (like activating a chip-select lane or similar). +/// NOTE: Call `.disconnect()` when the usage of the device is done to release it. +pub fn connect(sd: Stream_Device) ConnectError!void { + const connect_fn = sd.vtable.connect_fn orelse return; + return connect_fn(sd.object); +} + +/// Releases a device from the connection. +pub fn disconnect(sd: Stream_Device) void { + const disconnect_fn = sd.vtable.disconnect_fn orelse return; + return disconnect_fn(sd.object); +} + +/// Writes some `bytes` to the device and returns the number of bytes written. +pub fn write(sd: Stream_Device, bytes: []const u8) WriteError!usize { + return sd.writev(&.{bytes}); +} + +/// Writes some `bytes` to the device and returns the number of bytes written. +pub fn writev(sd: Stream_Device, bytes_vec: []const []const u8) WriteError!usize { + const writev_fn = sd.vtable.writev_fn orelse return error.Unsupported; + return writev_fn(sd.object, bytes_vec); +} + +/// Reads some `bytes` to the device and returns the number of bytes read. +pub fn read(sd: Stream_Device, bytes: []u8) ReadError!usize { + return sd.readv(&.{bytes}); +} + +/// Reads some `bytes` to the device and returns the number of bytes read. +pub fn readv(sd: Stream_Device, bytes_vec: []const []u8) ReadError!usize { + const readv_fn = sd.vtable.readv_fn orelse return error.Unsupported; + return readv_fn(sd.object, bytes_vec); +} + +pub const Reader = std.io.Reader(Stream_Device, ReadError, reader_read); +pub fn reader(sd: Stream_Device) Reader { + return .{ .context = sd }; +} + +fn reader_read(sd: Stream_Device, buf: []u8) ReadError!usize { + return sd.read(buf); +} + +pub const Writer = std.io.Reader(Stream_Device, WriteError, writer_write); +pub fn writer(sd: Stream_Device) Writer { + return .{ .context = sd }; +} + +fn writer_write(sd: Stream_Device, buf: []const u8) WriteError!usize { + return sd.write(buf); +} + +pub const VTable = struct { + connect_fn: ?*const fn (*anyopaque) ConnectError!void, + disconnect_fn: ?*const fn (*anyopaque) void, + writev_fn: ?*const fn (*anyopaque, datagram: []const []const u8) WriteError!usize, + readv_fn: ?*const fn (*anyopaque, datagram: []const []u8) ReadError!usize, +}; + +/// A device implementation that can be used to write unit tests for datagram devices. +pub const Test_Device = struct { + input: ?std.io.FixedBufferStream([]const u8), + output: ?std.ArrayList(u8), + + connected: bool, + + // Public API + + pub fn init(input: ?[]const u8, write_enabled: bool) Test_Device { + return Test_Device{ + .connected = false, + + .input = if (input) |bytes| + std.io.FixedBufferStream([]const u8){ .buffer = bytes, .pos = 0 } + else + null, + + .output = if (write_enabled) + std.ArrayList(u8).init(std.testing.allocator) + else + null, + }; + } + + pub fn deinit(td: *Test_Device) void { + if (td.output) |*list| { + list.deinit(); + } + td.* = undefined; + } + + pub fn stream_device(td: *Test_Device) Stream_Device { + return Stream_Device{ + .object = td, + .vtable = &vtable, + }; + } + + // Testing interface: + + /// Checks if all of the provided input was consumed. + pub fn expect_all_input_consumed(td: Test_Device) !void { + // No input is totally fine and counts as "all input consumed" + const input = td.input orelse return; + + try std.testing.expectEqualSlices(u8, "", input.buffer[input.pos..]); + } + + /// Expects that all sent data is `expected`. + pub fn expect_sent(td: Test_Device, expected: []const u8) !void { + if (td.output) |output| { + try std.testing.expectEqualSlices(u8, expected, output.items); + } + } + + // Interface implementation + + const vtable = VTable{ + .connect_fn = Test_Device.connect, + .disconnect_fn = Test_Device.disconnect, + .writev_fn = Test_Device.writev, + .readv_fn = Test_Device.readv, + }; + + fn connect(ctx: *anyopaque) ConnectError!void { + const td: *Test_Device = @ptrCast(@alignCast(ctx)); + if (td.connected) + return error.DeviceBusy; + td.connected = true; + } + + fn disconnect(ctx: *anyopaque) void { + const td: *Test_Device = @ptrCast(@alignCast(ctx)); + if (!td.connected) { + std.log.err("disconnect when test device was not connected!", .{}); + } + td.connected = false; + } + + fn writev(ctx: *anyopaque, bytes_vec: []const []const u8) WriteError!usize { + const td: *Test_Device = @ptrCast(@alignCast(ctx)); + if (!td.connected) { + return error.NotConnected; + } + + const list = if (td.output) |*output| output else return error.Unsupported; + + var total_len: usize = 0; + for (bytes_vec) |bytes| { + list.appendSlice(bytes) catch return error.IoError; + total_len += bytes.len; + } + return total_len; + } + + fn readv(ctx: *anyopaque, bytes_vec: []const []u8) ReadError!usize { + const td: *Test_Device = @ptrCast(@alignCast(ctx)); + if (!td.connected) { + return error.NotConnected; + } + + const data_reader = if (td.input) |*stream| stream.reader() else return error.Unsupported; + + var total_length: usize = 0; + + for (bytes_vec) |bytes| { + const len = data_reader.read(bytes) catch comptime unreachable; + total_length += len; + if (len < bytes.len) + return total_length; + } + + return total_length; + } +}; + +test Test_Device { + const input_text = + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque pen" ++ + "atibus et magnis dis parturient montes, nascetur ridiculus mus. " ++ + "Donec quam felis, ultricies nec, pellentesque eu, pretium quis, " ++ + "sem."; + + const output_text = + "Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate el" ++ + "eifend tellus. Aenean leo ligula, porttitor eu, consequat vitae," ++ + " eleifend ac, enim."; + + var td = Test_Device.init(input_text, true); + defer td.deinit(); + + var buffer: [128]u8 = undefined; + + const sd = td.stream_device(); + + // As long as we're not connected, the test device will handle + // this case and yield an error: + try std.testing.expectError(error.NotConnected, sd.write("not connected")); + try std.testing.expectError(error.NotConnected, sd.read(&buffer)); + + { + // The first connect call must succeed ... + try sd.connect(); + + // ... while the second call must fail: + try std.testing.expectError(error.DeviceBusy, sd.connect()); + + // After a disconnect... + sd.disconnect(); + + // ... the connect must succeed again: + try sd.connect(); + + // We'll keep the device connected for the rest of the test to + // ease handling. + } + + { + const len = try sd.read(&buffer); + try std.testing.expectEqual(buffer.len, len); + try std.testing.expectEqualStrings(input_text[0..128], buffer[0..len]); + } + + { + const len = try sd.read(buffer[0..64]); + try std.testing.expectEqual(64, len); + try std.testing.expectEqualStrings(input_text[128 .. 128 + 64], buffer[0..len]); + } + + { + const len = try sd.read(buffer[0..64]); + try std.testing.expectEqual(64, len); + try std.testing.expectEqualStrings(input_text[128 + 64 .. 128 + 64 + 64], buffer[0..len]); + } + + { + const len = try sd.read(&buffer); + try std.testing.expectEqual(4, len); + try std.testing.expectEqualStrings(input_text[128 + 64 + 64 ..], buffer[0..len]); + } + + try td.expect_all_input_consumed(); + + var output_splitter = struct { + text: []const u8, + pos: usize = 0, + + fn fetch(h: *@This(), len: usize) []const u8 { + const str = h.text[h.pos..][0..len]; + h.pos += len; + return str; + } + + fn rest(h: *@This()) []const u8 { + const str = h.text[h.pos..]; + h.pos = h.text.len; + return str; + } + }{ .text = output_text }; + + { + const slice = output_splitter.fetch(64); + const len = try sd.write(slice); + try std.testing.expectEqual(slice.len, len); + } + + { + const len = try sd.writev(&.{ + output_splitter.fetch(16), + output_splitter.fetch(10), + output_splitter.fetch(6), + output_splitter.fetch(24), + output_splitter.fetch(8), + }); + try std.testing.expectEqual(64, len); + } + + { + const slice = output_splitter.rest(); + const len = try sd.write(slice); + try std.testing.expectEqual(slice.len, len); + } + + try td.expect_sent(output_text); +} diff --git a/drivers/build.zig b/drivers/build.zig new file mode 100644 index 00000000..e14185d5 --- /dev/null +++ b/drivers/build.zig @@ -0,0 +1,17 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const drivers_mod = b.addModule("drivers", .{ + .root_source_file = b.path("framework.zig"), + }); + + _ = drivers_mod; + + const test_suite = b.addTest(.{ + .root_source_file = b.path("framework.zig"), + .target = b.host, + .optimize = .Debug, + }); + + b.getInstallStep().dependOn(&b.addRunArtifact(test_suite).step); +} diff --git a/drivers/build.zig.zon b/drivers/build.zig.zon new file mode 100644 index 00000000..9a8b2553 --- /dev/null +++ b/drivers/build.zig.zon @@ -0,0 +1,12 @@ +.{ + .name = "microzig_driver_framework", + .version = "0.0.1", + .paths = .{ + "build.zig", + "build.zig.zon", + "framework.zig", + "base", + "display", + "input", + }, +} diff --git a/drivers/display/colors.zig b/drivers/display/colors.zig new file mode 100644 index 00000000..cf4c98f9 --- /dev/null +++ b/drivers/display/colors.zig @@ -0,0 +1,51 @@ +//! +//! This file provides common color types found on the supported displays. +//! + +/// A color type encoding only black and white. +pub const BlackWhite = enum(u1) { + black = 0, + white = 1, +}; + +pub const RGB565 = packed struct(u16) { + pub usingnamespace DefaultColors(@This()); + + r: u5, + g: u6, + b: u5, + + pub fn from_rgb(r: u8, g: u8, b: u8) RGB565 { + return RGB565{ + .r = @truncate(r >> 3), + .g = @truncate(g >> 2), + .b = @truncate(b >> 3), + }; + } +}; + +pub const RGB888 = extern struct { + pub usingnamespace DefaultColors(@This()); + + r: u8, + g: u8, + b: u8, + + pub fn from_rgb(r: u8, g: u8, b: u8) RGB888 { + return RGB888{ .r = r, .g = g, .b = b }; + } +}; + +/// Provides a namespace with the default colors for the given `Color` type. +pub fn DefaultColors(comptime Color: type) type { + return struct { + pub const black = Color.from_rgb(0x00, 0x00, 0x00); + pub const white = Color.from_rgb(0xFF, 0xFF, 0xFF); + pub const red = Color.from_rgb(0xFF, 0x00, 0x00); + pub const green = Color.from_rgb(0x00, 0xFF, 0x00); + pub const blue = Color.from_rgb(0x00, 0x00, 0xFF); + pub const cyan = Color.from_rgb(0x00, 0xFF, 0xFF); + pub const magenta = Color.from_rgb(0xFF, 0x00, 0xFF); + pub const yellow = Color.from_rgb(0xFF, 0xFF, 0x00); + }; +} diff --git a/drivers/display/ssd1306.zig b/drivers/display/ssd1306.zig new file mode 100644 index 00000000..11aed002 --- /dev/null +++ b/drivers/display/ssd1306.zig @@ -0,0 +1,986 @@ +//! +//! Generic driver for the SSD1306 display controller. +//! +//! This controller is usually found in small OLED displays. +//! +//! Datasheet: +//! https://cdn-shop.adafruit.com/datasheets/SSD1306.pdf +//! +//! + +const std = @import("std"); +const mdf = @import("../framework.zig"); + +/// SSD1306 driver for I²C based operation. +pub const SSD1306_I2C = SSD1306_Generic(.{ + .mode = .i2c, +}); + +pub const Driver_Mode = enum { + /// The driver operates in I²C mode, which requires a 8 bit datagram device. + /// Each datagram is prefixed with the corresponding command/data byte. + i2c, + + /// The driver operates in the 3-wire SPI mode, which requires a 9 bit datagram device. + spi_3wire, + + /// The driver operates in the 4-wire SPI mode, which requires an 8 bit datagram device + /// as well as a command/data digital i/o. + spi_4wire, + + /// The driver can be initialized with one of the other options and receives + /// the mode with initialization. + dynamic, +}; + +pub const SSD1306_Options = struct { + /// Defines the operation of the SSD1306 driver. + mode: Driver_Mode, + + /// Which datagram device interface should be used. + Datagram_Device: type = mdf.base.Datagram_Device, + + /// Which digital i/o interface should be used. + Digital_IO: type = mdf.base.Digital_IO, +}; + +pub fn SSD1306_Generic(comptime options: SSD1306_Options) type { + switch (options.mode) { + .i2c, .spi_4wire, .dynamic => {}, + .spi_3wire => @compileError("3-wire SPI operation is not supported yet!"), + } + + // TODO(philippwendel) Add doc comments for functions + // TODO(philippwendel) Find out why using 'inline if' in writeAll(&[_]u8{ControlByte.command, if(cond) val1 else val2 }); hangs code on atmega328p, since tests work fine + return struct { + const Self = @This(); + + const Datagram_Device = options.Datagram_Device; + const Digital_IO = switch (options.mode) { + // 4-wire SPI mode uses a dedicated command/data control pin: + .spi_4wire, .dynamic => options.Digital_IO, + + // The other two modes don't use that, so we use a `void` pin here to save + // memory: + .i2c, .spi_3wire => void, + }; + + pub const Driver_Init_Mode = union(enum) { + i2c: struct { + device: Datagram_Device, + }, + spi_3wire: noreturn, + spi_4wire: struct { + device: Datagram_Device, + dc_pin: Digital_IO, + }, + }; + + const Mode = switch (options.mode) { + .dynamic => Driver_Mode, + else => void, + }; + + dd: Datagram_Device, + mode: Mode, + dc_pin: Digital_IO, + + /// Initializes the device and sets up sane defaults. + pub const init = switch (options.mode) { + .i2c, .spi_3wire => init_without_io, + .spi_4wire => init_with_io, + .dynamic => init_with_mode, + }; + + /// Creates an instance with only a datagram device. + /// `init` will be an alias to this if the init requires no D/C pin. + fn init_without_io(dev: Datagram_Device) !Self { + var self = Self{ + .dd = dev, + .dc_pin = {}, + .mode = {}, + }; + try self.execute_init_sequence(); + return self; + } + + /// Creates an instance with a datagram device and the D/C pin. + /// `init` will be an alias to this if the init requires a D/C pin. + fn init_with_io(dev: Datagram_Device, data_cmd_pin: Digital_IO) !Self { + var self = Self{ + .dd = dev, + .dc_pin = data_cmd_pin, + .mode = {}, + }; + + // The DC pin must be an output: + try data_cmd_pin.set_direction(.output); + + try self.execute_init_sequence(); + + return self; + } + + fn init_with_mode(mode: Driver_Init_Mode) !Self { + var self = Self{ + .dd = switch (mode) { + .i2c => |opt| opt.device, + .spi_3wire => @compileError("TODO"), + .spi_4wire => |opt| opt.device, + }, + .dc_pin = switch (mode) { + .i2c => undefined, + .spi_3wire => @compileError("TODO"), + .spi_4wire => |opt| opt.dc_pin, + }, + .mode = switch (mode) { + .i2c => .i2c, + .spi_3wire => .spi_3wire, + .spi_4wire => .spi_4wire, + }, + }; + + if (self.mode == .spi_4wire) { + // The DC pin must be an output: + try self.dc_pin.set_direction(.output); + } + + try self.execute_init_sequence(); + + return self; + } + + /// Executes the device initialization sequence and sets up sane defaults. + fn execute_init_sequence(self: Self) !void { + try self.set_display(.off); + + try self.deactivate_scroll(); + try self.set_segment_remap(true); // Flip left/right + try self.set_com_ouput_scan_direction(true); // Flip up/down + try self.set_normal_or_inverse_display(.normal); + try self.set_contrast(255); + + try self.charge_pump_setting(true); + try self.set_multiplex_ratio(63); // Default + try self.set_display_clock_divide_ratio_and_oscillator_frequency(0, 8); + try self.set_precharge_period(0b0001, 0b0001); + try self.set_v_comh_deselect_level(0x4); + try self.set_com_pins_hardware_configuration(0b001); // See page 40 in datasheet + + try self.set_display_offset(0); + try self.set_display_start_line(0); + try self.entire_display_on(.resumeToRam); + + try self.set_display(.on); + } + + pub fn write_full_display(self: Self, data: *const [128 * 8]u8) !void { + try self.set_memory_addressing_mode(.horizontal); + try self.set_column_address(0, 127); + try self.set_page_address(0, 7); + + try self.write_gdram(data); + } + + pub fn write_gdram(self: Self, data: []const u8) !void { + try self.write_data(data); + } + + pub fn clear_screen(self: Self, white: bool) !void { + try self.set_memory_addressing_mode(.horizontal); + try self.set_column_address(0, 127); + try self.set_page_address(0, 7); + { + const color: u8 = if (white) 0xFF else 0x00; + + const chunk_size = 16; + + const chunk: [chunk_size]u8 = .{color} ** chunk_size; + + const count = comptime @divExact(128 * 8, chunk.len); + for (0..count) |_| { + try self.write_data(&chunk); + } + } + try self.entire_display_on(.resumeToRam); + try self.set_display(.on); + } + + // Fundamental Commands + pub fn set_contrast(self: Self, contrast: u8) !void { + try self.execute_command(0x81, &.{contrast}); + } + + pub fn entire_display_on(self: Self, mode: DisplayOnMode) !void { + try self.execute_command(@intFromEnum(mode), &.{}); + } + + pub fn set_normal_or_inverse_display(self: Self, mode: NormalOrInverseDisplay) !void { + try self.execute_command(@intFromEnum(mode), &.{}); + } + + pub fn set_display(self: Self, mode: DisplayMode) !void { + try self.execute_command(@intFromEnum(mode), &.{}); + } + + // Scrolling Commands + pub fn continuous_horizontal_scroll_setup(self: Self, direction: HorizontalScrollDirection, start_page: u3, end_page: u3, frame_frequency: u3) !void { + if (end_page < start_page) + return PageError.EndPageIsSmallerThanStartPage; + + try self.execute_command(@intFromEnum(direction), &.{ + 0x00, // Dummy byte + @as(u8, start_page), + @as(u8, frame_frequency), + @as(u8, end_page), + 0x00, // Dummy byte + 0xFF, // Dummy byte + }); + } + + pub fn continuous_vertical_and_horizontal_scroll_setup(self: Self, direction: VerticalAndHorizontalScrollDirection, start_page: u3, end_page: u3, frame_frequency: u3, vertical_scrolling_offset: u6) !void { + try self.execute_command(@intFromEnum(direction), &.{ + 0x00, // Dummy byte + @as(u8, start_page), + @as(u8, frame_frequency), + @as(u8, end_page), + @as(u8, vertical_scrolling_offset), + }); + } + + pub fn deactivate_scroll(self: Self) !void { + try self.execute_command(0x2E, &.{}); + } + + pub fn activate_scroll(self: Self) !void { + try self.execute_command(0x2F, &.{}); + } + + pub fn set_vertical_scroll_area(self: Self, start_row: u6, num_of_rows: u7) !void { + try self.execute_command(0xA3, &.{ @as(u8, start_row), @as(u8, num_of_rows) }); + } + + // Addressing Setting Commands + pub fn set_column_start_address_for_page_addressing_mode(self: Self, column: Column, address: u4) !void { + const cmd = (@as(u8, @intFromEnum(column)) << 4) | @as(u8, address); + + try self.execute_command(cmd, &.{}); + } + + pub fn set_memory_addressing_mode(self: Self, mode: MemoryAddressingMode) !void { + try self.execute_command(0x20, &.{@as(u8, @intFromEnum(mode))}); + } + + pub fn set_column_address(self: Self, start: u7, end: u7) !void { + try self.execute_command(0x21, &.{ @as(u8, start), @as(u8, end) }); + } + + pub fn set_page_address(self: Self, start: u3, end: u3) !void { + try self.execute_command(0x22, &.{ @as(u8, start), @as(u8, end) }); + } + + pub fn set_page_start_address(self: Self, address: u3) !void { + const cmd: u8 = 0xB0 | @as(u8, address); + + try self.execute_command(cmd, &.{}); + } + + // Hardware Configuration Commands + pub fn set_display_start_line(self: Self, line: u6) !void { + const cmd: u8 = 0x40 | @as(u8, line); + + try self.execute_command(cmd, &.{}); + } + + // false: column address 0 is mapped to SEG0 + // true: column address 127 is mapped to SEG0 + pub fn set_segment_remap(self: Self, remap: bool) !void { + if (remap) { + try self.execute_command(0xA1, &.{}); + } else { + try self.execute_command(0xA0, &.{}); + } + } + + pub fn set_multiplex_ratio(self: Self, ratio: u6) !void { + if (ratio <= 14) return InputError.InvalidEntry; + + try self.execute_command(0xA8, &.{@as(u8, ratio)}); + } + + /// false: normal (COM0 to COMn) + /// true: remapped + pub fn set_com_ouput_scan_direction(self: Self, remap: bool) !void { + if (remap) { + try self.execute_command(0xC8, &.{}); + } else { + try self.execute_command(0xC0, &.{}); + } + } + + pub fn set_display_offset(self: Self, vertical_shift: u6) !void { + try self.execute_command(0xD3, &.{@as(u8, vertical_shift)}); + } + + // TODO(philippwendel) Make config to enum + pub fn set_com_pins_hardware_configuration(self: Self, config: u2) !void { + try self.execute_command(0xDA, &.{@as(u8, config) << 4 | 0x02}); + } + + // Timing & Driving Scheme Setting Commands + // TODO(philippwendel) Split in two funktions + pub fn set_display_clock_divide_ratio_and_oscillator_frequency(self: Self, divide_ratio: u4, freq: u4) !void { + try self.execute_command(0xD5, &.{(@as(u8, freq) << 4) | @as(u8, divide_ratio)}); + } + + pub fn set_precharge_period(self: Self, phase1: u4, phase2: u4) !void { + if (phase1 == 0 or phase2 == 0) return InputError.InvalidEntry; + + try self.execute_command(0xD9, &.{@as(u8, phase2) << 4 | @as(u8, phase1)}); + } + + // TODO(philippwendel) Make level to enum + pub fn set_v_comh_deselect_level(self: Self, level: u3) !void { + try self.execute_command(0xDB, &.{@as(u8, level) << 4}); + } + + pub fn nop(self: Self) !void { + try self.execute_command(0xE3, &.{}); + } + + // Charge Pump Commands + pub fn charge_pump_setting(self: Self, enable: bool) !void { + const arg: u8 = if (enable) + 0x14 + else + 0x10; + + try self.execute_command(0x8D, &.{arg}); + } + + // Utilities: + + const i2c_command_preamble: []const u8 = &.{I2C_ControlByte.command}; + const i2c_data_preamble: []const u8 = &.{I2C_ControlByte.data_stream}; + + /// Sends command data to the SSD1306 controller. + fn execute_command(self: Self, cmd: u8, argv: []const u8) !void { + try self.set_dc_pin(.command); + + try self.dd.connect(); + defer self.dd.disconnect(); + + const command_preamble: []const u8 = switch (options.mode) { + .spi_3wire, .spi_4wire => "", + .i2c => i2c_command_preamble, + .dynamic => switch (self.mode) { + .i2c => i2c_command_preamble, + .spi_3wire, .spi_4wire => "", + .dynamic => unreachable, + }, + }; + + try self.dd.writev(&.{ command_preamble, &.{cmd}, argv }); + } + + /// Sends gdram data to the SSD1306 controller. + fn write_data(self: Self, data: []const u8) !void { + try self.set_dc_pin(.data); + + try self.dd.connect(); + defer self.dd.disconnect(); + + const data_preamble: []const u8 = switch (options.mode) { + .spi_3wire, .spi_4wire => "", + .i2c => i2c_data_preamble, + .dynamic => switch (self.mode) { + .i2c => i2c_data_preamble, + .spi_3wire, .spi_4wire => "", + .dynamic => unreachable, + }, + }; + + try self.dd.writev(&.{ data_preamble, data }); + } + + /// If present, sets the D/C pin to the required mode. + /// NOTE: This function must be called *before* activating the device + /// via chip select, so before calling `dd.connect`! + fn set_dc_pin(self: Self, mode: enum { command, data }) !void { + switch (options.mode) { + .i2c, .spi_3wire => return, + .spi_4wire => {}, + .dynamic => if (self.mode != .spi_4wire) + return, + } + + try self.dc_pin.write(switch (mode) { + .command => .low, + .data => .high, + }); + } + + // Tests + + const TestDevice = mdf.base.Datagram_Device.Test_Device; + + // This is the command sequence created by SSD1306_I2C.init() + // to set up the display. + const recorded_init_sequence = [_][]const u8{ + &.{ 0x00, 0xAE }, + &.{ 0x00, 0x2E }, + &.{ 0x00, 0xA1 }, + &.{ 0x00, 0xC8 }, + &.{ 0x00, 0xA6 }, + &.{ 0x00, 0x81, 0xFF }, + &.{ 0x00, 0x8D, 0x14 }, + &.{ 0x00, 0xA8, 0x3F }, + &.{ 0x00, 0xD5, 0x80 }, + &.{ 0x00, 0xD9, 0x11 }, + &.{ 0x00, 0xDB, 0x40 }, + &.{ 0x00, 0xDA, 0x12 }, + &.{ 0x00, 0xD3, 0x00 }, + &.{ 0x00, 0x40 }, + &.{ 0x00, 0xA4 }, + &.{ 0x00, 0xAF }, + }; + + // Fundamental Commands + test set_contrast { + // Arrange + for ([_]u8{ 0, 128, 255 }) |contrast| { + var td = TestDevice.init_receiver_only(); + defer td.deinit(); + + const expected_data = &[_]u8{ 0x00, 0x81, contrast }; + + // Act + const driver = try SSD1306_I2C.init(td.datagram_device()); + try driver.set_contrast(contrast); + + // Assert + try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); + } + } + + test entire_display_on { + // Arrange + for ([_]u8{ 0xA4, 0xA5 }, [_]DisplayOnMode{ DisplayOnMode.resumeToRam, DisplayOnMode.ignoreRam }) |data, mode| { + var td = TestDevice.init_receiver_only(); + defer td.deinit(); + + const expected_data = &[_]u8{ 0x00, data }; + // Act + const driver = try SSD1306_I2C.init(td.datagram_device()); + try driver.entire_display_on(mode); + // Assert + try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); + } + } + + test set_normal_or_inverse_display { + // Arrange + for ([_]u8{ 0xA6, 0xA7 }, [_]NormalOrInverseDisplay{ NormalOrInverseDisplay.normal, NormalOrInverseDisplay.inverse }) |data, mode| { + var td = TestDevice.init_receiver_only(); + defer td.deinit(); + + const expected_data = &[_]u8{ 0x00, data }; + // Act + const driver = try SSD1306_I2C.init(td.datagram_device()); + try driver.set_normal_or_inverse_display(mode); + // Assert + try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); + } + } + + test set_display { + // Arrange + for ([_]u8{ 0xAF, 0xAE }, [_]DisplayMode{ DisplayMode.on, DisplayMode.off }) |data, mode| { + var td = TestDevice.init_receiver_only(); + defer td.deinit(); + + const expected_data = &[_]u8{ 0x00, data }; + // Act + const driver = try SSD1306_I2C.init(td.datagram_device()); + try driver.set_display(mode); + // Assert + try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); + } + } + + // Scrolling Commands + // TODO(philippwendel) Test more values and error + test continuous_horizontal_scroll_setup { + // Arrange + var td = TestDevice.init_receiver_only(); + defer td.deinit(); + + const expected_data = &[_]u8{ 0x00, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF }; + // Act + const driver = try SSD1306_I2C.init(td.datagram_device()); + try driver.continuous_horizontal_scroll_setup(.right, 0, 0, 0); + // Assert + try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); + } + + test continuous_vertical_and_horizontal_scroll_setup { + // Arrange + var td = TestDevice.init_receiver_only(); + defer td.deinit(); + + const expected_data = &[_]u8{ 0x00, 0x29, 0x00, 0x01, 0x3, 0x2, 0x4 }; + // Act + const driver = try SSD1306_I2C.init(td.datagram_device()); + try driver.continuous_vertical_and_horizontal_scroll_setup(.right, 1, 2, 3, 4); + // Assert + try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); + } + + test deactivate_scroll { + // Arrange + var td = TestDevice.init_receiver_only(); + defer td.deinit(); + + const expected_data = &[_]u8{ 0x00, 0x2E }; + // Act + const driver = try SSD1306_I2C.init(td.datagram_device()); + try driver.deactivate_scroll(); + // Assert + try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); + } + + test activate_scroll { + // Arrange + var td = TestDevice.init_receiver_only(); + defer td.deinit(); + + const expected_data = &[_]u8{ 0x00, 0x2F }; + // Act + const driver = try SSD1306_I2C.init(td.datagram_device()); + try driver.activate_scroll(); + // Assert + try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); + } + + test set_vertical_scroll_area { + // Arrange + var td = TestDevice.init_receiver_only(); + defer td.deinit(); + + const expected_data = &[_]u8{ 0x00, 0xA3, 0x00, 0x0F }; + // Act + const driver = try SSD1306_I2C.init(td.datagram_device()); + try driver.set_vertical_scroll_area(0, 15); + // Assert + try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); + } + + // Addressing Setting Commands + test set_column_start_address_for_page_addressing_mode { + // Arrange + for ([_]Column{ Column.lower, Column.higher }, [_]u8{ 0x0F, 0x1F }) |column, data| { + var td = TestDevice.init_receiver_only(); + defer td.deinit(); + + const expected_data = &[_]u8{ 0x00, data }; + // Act + const driver = try SSD1306_I2C.init(td.datagram_device()); + try driver.set_column_start_address_for_page_addressing_mode(column, 0xF); + // Assert + try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); + } + } + + test set_memory_addressing_mode { + // Arrange + for ([_]MemoryAddressingMode{ .horizontal, .vertical, .page }) |mode| { + var td = TestDevice.init_receiver_only(); + defer td.deinit(); + + const expected_data = &[_]u8{ 0x00, 0x20, @as(u8, @intFromEnum(mode)) }; + // Act + const driver = try SSD1306_I2C.init(td.datagram_device()); + try driver.set_memory_addressing_mode(mode); + // Assert + try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); + } + } + + test set_column_address { + // Arrange + var td = TestDevice.init_receiver_only(); + defer td.deinit(); + + const expected_data = &[_]u8{ 0x00, 0x21, 0, 127 }; + // Act + const driver = try SSD1306_I2C.init(td.datagram_device()); + try driver.set_column_address(0, 127); + // Assert + try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); + } + + test set_page_address { + // Arrange + var td = TestDevice.init_receiver_only(); + defer td.deinit(); + + const expected_data = &[_]u8{ 0x00, 0x22, 0, 7 }; + // Act + const driver = try SSD1306_I2C.init(td.datagram_device()); + try driver.set_page_address(0, 7); + // Assert + try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); + } + + test set_page_start_address { + // Arrange + var td = TestDevice.init_receiver_only(); + defer td.deinit(); + + const expected_data = &[_]u8{ 0x00, 0xB7 }; + // Act + const driver = try SSD1306_I2C.init(td.datagram_device()); + try driver.set_page_start_address(7); + // Assert + try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); + } + + // Hardware Configuration Commands + test set_display_start_line { + // Arrange + var td = TestDevice.init_receiver_only(); + defer td.deinit(); + + const expected_data = &[_]u8{ 0x00, 0b0110_0000 }; + // Act + const driver = try SSD1306_I2C.init(td.datagram_device()); + try driver.set_display_start_line(32); + // Assert + try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); + } + + test set_segment_remap { + // Arrange + var td = TestDevice.init_receiver_only(); + defer td.deinit(); + + const expected_data = &[_][]const u8{ &.{ 0x00, 0xA0 }, &.{ 0x00, 0xA1 } }; + // Act + const driver = try SSD1306_I2C.init(td.datagram_device()); + try driver.set_segment_remap(false); + try driver.set_segment_remap(true); + // Assert + try td.expect_sent(&recorded_init_sequence ++ expected_data); + } + + test set_multiplex_ratio { + // Arrange + var td = TestDevice.init_receiver_only(); + defer td.deinit(); + + const expected_data = &[_]u8{ 0x00, 0xA8, 15 }; + // Act + const driver = try SSD1306_I2C.init(td.datagram_device()); + try driver.set_multiplex_ratio(15); + const err = driver.set_multiplex_ratio(0); + // Assert + try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); + try std.testing.expectEqual(err, InputError.InvalidEntry); + } + + test set_com_ouput_scan_direction { + // Arrange + var td = TestDevice.init_receiver_only(); + defer td.deinit(); + + const expected_data = &[_][]const u8{ &.{ 0x00, 0xC0 }, &.{ 0x00, 0xC8 } }; + // Act + const driver = try SSD1306_I2C.init(td.datagram_device()); + try driver.set_com_ouput_scan_direction(false); + try driver.set_com_ouput_scan_direction(true); + // Assert + try td.expect_sent(&recorded_init_sequence ++ expected_data); + } + + test set_display_offset { + // Arrange + var td = TestDevice.init_receiver_only(); + defer td.deinit(); + + const expected_data = &[_]u8{ 0x00, 0xD3, 17 }; + // Act + const driver = try SSD1306_I2C.init(td.datagram_device()); + try driver.set_display_offset(17); + // Assert + try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); + } + + test set_com_pins_hardware_configuration { + // Arrange + var td = TestDevice.init_receiver_only(); + defer td.deinit(); + + const expected_data = &[_]u8{ 0x00, 0xDA, 0b0011_0010 }; + // Act + const driver = try SSD1306_I2C.init(td.datagram_device()); + try driver.set_com_pins_hardware_configuration(0b11); + // Assert + try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); + } + + // Timing & Driving Scheme Setting Commands + test set_display_clock_divide_ratio_and_oscillator_frequency { + // Arrange + var td = TestDevice.init_receiver_only(); + defer td.deinit(); + + const expected_data = &[_]u8{ 0x00, 0xD5, 0x00 }; + // Act + const driver = try SSD1306_I2C.init(td.datagram_device()); + try driver.set_display_clock_divide_ratio_and_oscillator_frequency(0, 0); + // Assert + try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); + } + + test set_precharge_period { + // Arrange + var td = TestDevice.init_receiver_only(); + defer td.deinit(); + + const expected_data = &[_]u8{ 0x00, 0xD9, 0b0001_0001 }; + // Act + const driver = try SSD1306_I2C.init(td.datagram_device()); + try driver.set_precharge_period(1, 1); + // Assert + try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); + } + + test set_v_comh_deselect_level { + // Arrange + var td = TestDevice.init_receiver_only(); + defer td.deinit(); + + const expected_data = &[_]u8{ 0x00, 0xDB, 0b0011_0000 }; + // Act + const driver = try SSD1306_I2C.init(td.datagram_device()); + try driver.set_v_comh_deselect_level(0b011); + // Assert + try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); + } + + test nop { + // Arrange + var td = TestDevice.init_receiver_only(); + defer td.deinit(); + + const expected_data = &[_]u8{ 0x00, 0xE3 }; + // Act + const driver = try SSD1306_I2C.init(td.datagram_device()); + try driver.nop(); + // Assert + try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); + } + + // Charge Pump Commands + test charge_pump_setting { + // Arrange + var td = TestDevice.init_receiver_only(); + defer td.deinit(); + + const expected_data = &[_][]const u8{ &.{ 0x00, 0x8D, 0x14 }, &.{ 0x00, 0x8D, 0x10 } }; + // Act + const driver = try SSD1306_I2C.init(td.datagram_device()); + try driver.charge_pump_setting(true); + try driver.charge_pump_setting(false); + // Assert + try td.expect_sent(&recorded_init_sequence ++ expected_data); + } + }; +} + +pub const Color = mdf.display.colors.BlackWhite; + +/// A framebuffer suitable for operation with the SSD1306. +/// +/// Its memory layout is equivalent to the one in the SSD1306 RAM, +/// and the framebuffer can be copied verbatim to the device. +pub const Framebuffer = struct { + pub const width = 128; + pub const height = 64; + + // layed out in 8 pages with 8*128 pixels each. + // each page is column-major with the column encoded in the bits 0 (top) to 7 (bottom). + // each byte in the page is a column left-to-right. + // first page is thus columns 0..7, second page is 8..15 and so on. + pixel_data: [8 * 128]u8, + + /// Initializes a new framebuffer with the given clear color. + pub fn init(fill_color: Color) Framebuffer { + var fb = Framebuffer{ .pixel_data = undefined }; + @memset(&fb.pixel_data, switch (fill_color) { + .black => 0x00, + .white => 0xFF, + }); + return fb; + } + + /// Returns a pointer to the bit stream that can be passed to the + /// device. + pub fn bit_stream(fb: *const Framebuffer) *const [8 * 128]u8 { + return &fb.pixel_data; + } + + /// Clears the framebuffer to `color`. + pub fn clear(fb: *Framebuffer, color: Color) void { + fb.* = init(color); + } + + /// Sets the pixel at (`x`, `y`) to `color`. + pub fn set_pixel(fb: *Framebuffer, x: u7, y: u6, color: Color) void { + const page: u3 = @truncate(y / 8); + const bit: u3 = @truncate(y % 8); + const mask: u8 = @as(u8, 1) << bit; + + const offset: usize = (@as(usize, page) << 7) + x; + + switch (color) { + .black => fb.pixel_data[offset] &= ~mask, + .white => fb.pixel_data[offset] |= mask, + } + } + + // Tests: + + test init { + // .white + { + const fb = Framebuffer.init(.white); + for (fb.pixel_data) |chunk| { + try std.testing.expectEqual(0xFF, chunk); + } + } + + // .black + { + const fb = Framebuffer.init(.black); + for (fb.pixel_data) |chunk| { + try std.testing.expectEqual(0x00, chunk); + } + } + } + + test clear { + // .white + { + var fb = Framebuffer.init(.black); + fb.clear(.white); + for (fb.pixel_data) |chunk| { + try std.testing.expectEqual(0xFF, chunk); + } + } + + // .black + { + var fb = Framebuffer.init(.white); + fb.clear(.black); + for (fb.pixel_data) |chunk| { + try std.testing.expectEqual(0x00, chunk); + } + } + } + + test set_pixel { + // .white + { + var fb = Framebuffer.init(.black); + + fb.set_pixel(0, 0, .white); + try std.testing.expectEqual(0x01, fb.pixel_data[0]); + + for (fb.pixel_data[1..]) |chunk| { + try std.testing.expectEqual(0x00, chunk); + } + } + + // .black + { + var fb = Framebuffer.init(.white); + + fb.set_pixel(0, 0, .black); + try std.testing.expectEqual(0xFE, fb.pixel_data[0]); + + for (fb.pixel_data[1..]) |chunk| { + try std.testing.expectEqual(0xFF, chunk); + } + } + } +}; + +const I2C_ControlByte = packed struct(u8) { + zero: u6 = 0, + + /// The D/C# bit determines the next data byte is acted as a command or a data. If the D/C# bit is + /// set to logic “0”, it defines the following data byte as a command. If the D/C# bit is set to + /// logic “1”, it defines the following data byte as a data which will be stored at the GDDRAM. + /// The GDDRAM column address pointer will be increased by one automatically after each + /// data write. + mode: enum(u1) { command = 0, data = 1 }, + + /// If the Co bit is set as logic “0”, the transmission of the following information will contain data bytes only. + co_bit: u1, + + const command: u8 = @bitCast(I2C_ControlByte{ + .mode = .command, + .co_bit = 0, + }); + + const data_byte: u8 = @bitCast(I2C_ControlByte{ + .mode = .data, + .co_bit = 1, + }); + + const data_stream: u8 = @bitCast(I2C_ControlByte{ + .mode = .data, + .co_bit = 0, + }); +}; + +comptime { + std.debug.assert(I2C_ControlByte.command == 0x00); + std.debug.assert(I2C_ControlByte.data_byte == 0xC0); + std.debug.assert(I2C_ControlByte.data_stream == 0x40); +} + +// Fundamental Commands +pub const DisplayOnMode = enum(u8) { resumeToRam = 0xA4, ignoreRam = 0xA5 }; +pub const NormalOrInverseDisplay = enum(u8) { normal = 0xA6, inverse = 0xA7 }; +pub const DisplayMode = enum(u8) { off = 0xAE, on = 0xAF }; + +// Scrolling Commands +pub const HorizontalScrollDirection = enum(u8) { right = 0x26, left = 0x27 }; +pub const VerticalAndHorizontalScrollDirection = enum(u8) { right = 0x29, left = 0x2A }; +pub const PageError = error{EndPageIsSmallerThanStartPage}; + +// Addressing Setting Commands +pub const Column = enum(u1) { lower = 0, higher = 1 }; +pub const MemoryAddressingMode = enum(u2) { horizontal = 0b00, vertical = 0b01, page = 0b10 }; + +// Hardware Configuration Commands +pub const InputError = error{InvalidEntry}; + +test { + _ = SSD1306_I2C; + _ = Framebuffer; + + _ = SSD1306_Generic(.{ + .mode = .i2c, + }); + + _ = SSD1306_Generic(.{ + .mode = .spi_4wire, + }); + + _ = SSD1306_Generic(.{ + .mode = .dynamic, + }); +} diff --git a/drivers/display/st77xx.zig b/drivers/display/st77xx.zig new file mode 100644 index 00000000..10597873 --- /dev/null +++ b/drivers/display/st77xx.zig @@ -0,0 +1,494 @@ +//! +//! Driver for the ST7735 and ST7789 for the 4-line serial protocol or 8-bit parallel interface +//! +//! This driver is a port of https://github.com/adafruit/Adafruit-ST7735-Library +//! +//! Datasheets: +//! - https://www.displayfuture.com/Display/datasheet/controller/ST7735.pdf +//! - https://www.waveshare.com/w/upload/e/e2/ST7735S_V1.1_20111121.pdf +//! - https://www.waveshare.com/w/upload/a/ae/ST7789_Datasheet.pdf +//! +const std = @import("std"); +const mdf = @import("../framework.zig"); +const Color = mdf.display.colors.RGB565; + +pub const ST7735 = ST77xx_Generic(.{ + .device = .st7735, +}); + +pub const ST7789 = ST77xx_Generic(.{ + .device = .st7789, +}); + +pub const ST77xx_Options = struct { + /// Which SST77xx device does the driver target? + device: Device, + + /// Which datagram device interface should be used. + Datagram_Device: type = mdf.base.Datagram_Device, + + /// Which digital i/o interface should be used. + Digital_IO: type = mdf.base.Digital_IO, +}; + +pub fn ST77xx_Generic(comptime options: ST77xx_Options) type { + return struct { + const Driver = @This(); + const Datagram_Device = options.Datagram_Device; + const Digital_IO = options.Digital_IO; + + const dev = switch (options.device) { + .st7735 => ST7735_Device, + .st7789 => ST7789_Device, + }; + + dd: Datagram_Device, + dev_rst: Digital_IO, + dev_datcmd: Digital_IO, + + resolution: Resolution, + + pub fn init( + channel: Datagram_Device, + rst: Digital_IO, + data_cmd: Digital_IO, + resolution: Resolution, + ) !Driver { + const dri = Driver{ + .dd = channel, + .dev_rst = rst, + .dev_datcmd = data_cmd, + + .resolution = resolution, + }; + + // initSPI(freq, spiMode); + + // Run init sequence + // uint8_t numCommands, cmd, numArgs; + // uint16_t ms; + + // numCommands = pgm_read_byte(addr++); // Number of commands to follow + // while (numCommands--) { // For each command... + // cmd = pgm_read_byte(addr++); // Read command + // numArgs = pgm_read_byte(addr++); // Number of args to follow + // ms = numArgs & ST_CMD_DELAY; // If hibit set, delay follows args + // numArgs &= ~ST_CMD_DELAY; // Mask out delay bit + // sendCommand(cmd, addr, numArgs); + // addr += numArgs; + + // if (ms) { + // ms = pgm_read_byte(addr++); // Read post-command delay time (ms) + // if (ms == 255) + // ms = 500; // If 255, delay for 500 ms + // delay(ms); + // } + // } + + try dri.set_spi_mode(.data); + + return dri; + } + + pub fn set_address_window(dri: Driver, x: u16, y: u16, w: u16, h: u16) !void { + // x += _xstart; + // y += _ystart; + + const xa = (@as(u32, x) << 16) | (x + w - 1); + const ya = (@as(u32, y) << 16) | (y + h - 1); + + try dri.write_command(.caset, std.mem.asBytes(&xa)); // Column addr set + try dri.write_command(.raset, std.mem.asBytes(&ya)); // Row addr set + try dri.write_command(.ramwr, &.{}); // write to RAM + } + + pub fn set_rotation(dri: Driver, rotation: Rotation) !void { + var control_byte: u8 = madctl_rgb; + + switch (rotation) { + .normal => { + control_byte = (madctl_mx | madctl_my | madctl_rgb); + // _xstart = _colstart; + // _ystart = _rowstart; + }, + .right90 => { + control_byte = (madctl_my | madctl_mv | madctl_rgb); + // _ystart = _colstart; + // _xstart = _rowstart; + }, + .upside_down => { + control_byte = (madctl_rgb); + // _xstart = _colstart; + // _ystart = _rowstart; + }, + .left90 => { + control_byte = (madctl_mx | madctl_mv | madctl_rgb); + // _ystart = _colstart; + // _xstart = _rowstart; + }, + } + + try dri.write_command(.madctl, &.{control_byte}); + } + + pub fn enable_display(dri: Driver, enable: bool) !void { + try dri.write_command(if (enable) .dispon else .dispoff, &.{}); + } + + pub fn enable_tearing(dri: Driver, enable: bool) !void { + try dri.write_command(if (enable) .teon else .teoff, &.{}); + } + + pub fn enable_sleep(dri: Driver, enable: bool) !void { + try dri.write_command(if (enable) .slpin else .slpout, &.{}); + } + + pub fn invert_display(dri: Driver, inverted: bool) !void { + try dri.write_command(if (inverted) .invon else .invoff, &.{}); + } + + pub fn set_pixel(dri: Driver, x: u16, y: u16, color: Color) !void { + if (x >= dri.resolution.width or y >= dri.resolution.height) { + return; + } + try dri.set_address_window(x, y, 1, 1); + try dri.write_data(&.{color}); + } + + fn write_command(dri: Driver, cmd: Command, params: []const u8) !void { + try dri.dd.connect(); + defer dri.dd.disconnect(); + + try dri.set_spi_mode(.command); + try dri.dd.write(&[_]u8{@intFromEnum(cmd)}); + try dri.set_spi_mode(.data); + try dri.dd.write(params); + } + + fn write_data(dri: Driver, data: []const Color) !void { + try dri.dd.connect(); + defer dri.dd.disconnect(); + + try dri.dd.write(std.mem.sliceAsBytes(data)); + } + + fn set_spi_mode(dri: Driver, mode: enum { data, command }) !void { + try dri.dev_datcmd.write(switch (mode) { + .command => .low, + .data => .high, + }); + } + + const cmd_delay = 0x80; // special signifier for command lists + + const Command = enum(u8) { + nop = 0x00, + swreset = 0x01, + rddid = 0x04, + rddst = 0x09, + + slpin = 0x10, + slpout = 0x11, + ptlon = 0x12, + noron = 0x13, + + invoff = 0x20, + invon = 0x21, + dispoff = 0x28, + dispon = 0x29, + caset = 0x2A, + raset = 0x2B, + ramwr = 0x2C, + ramrd = 0x2E, + + ptlar = 0x30, + teoff = 0x34, + teon = 0x35, + madctl = 0x36, + colmod = 0x3A, + + rdid1 = 0xDA, + rdid2 = 0xDB, + rdid3 = 0xDC, + rdid4 = 0xDD, + }; + + const madctl_my = 0x80; + const madctl_mx = 0x40; + const madctl_mv = 0x20; + const madctl_ml = 0x10; + const madctl_rgb = 0x00; + + const ST7735_Device = struct { + // some flags for initR() :( + const INITR_GREENTAB = 0x00; + const INITR_REDTAB = 0x01; + const INITR_BLACKTAB = 0x02; + const INITR_18GREENTAB = INITR_GREENTAB; + const INITR_18REDTAB = INITR_REDTAB; + const INITR_18BLACKTAB = INITR_BLACKTAB; + const INITR_144GREENTAB = 0x01; + const INITR_MINI160x80 = 0x04; + const INITR_HALLOWING = 0x05; + const INITR_MINI160x80_PLUGIN = 0x06; + + // Some register settings + const ST7735_MADCTL_BGR = 0x08; + const ST7735_MADCTL_MH = 0x04; + + const ST7735_FRMCTR1 = 0xB1; + const ST7735_FRMCTR2 = 0xB2; + const ST7735_FRMCTR3 = 0xB3; + const ST7735_INVCTR = 0xB4; + const ST7735_DISSET5 = 0xB6; + + const ST7735_PWCTR1 = 0xC0; + const ST7735_PWCTR2 = 0xC1; + const ST7735_PWCTR3 = 0xC2; + const ST7735_PWCTR4 = 0xC3; + const ST7735_PWCTR5 = 0xC4; + const ST7735_VMCTR1 = 0xC5; + + const ST7735_PWCTR6 = 0xFC; + + const ST7735_GMCTRP1 = 0xE0; + const ST7735_GMCTRN1 = 0xE1; + + // static const uint8_t PROGMEM + // Bcmd[] = { // Init commands for 7735B screens + // 18, // 18 commands in list: + // ST77XX_SWRESET, ST_CMD_DELAY, // 1: Software reset, no args, w/delay + // 50, // 50 ms delay + // ST77XX_SLPOUT, ST_CMD_DELAY, // 2: Out of sleep mode, no args, w/delay + // 255, // 255 = max (500 ms) delay + // ST77XX_COLMOD, 1+ST_CMD_DELAY, // 3: Set color mode, 1 arg + delay: + // 0x05, // 16-bit color + // 10, // 10 ms delay + // ST7735_FRMCTR1, 3+ST_CMD_DELAY, // 4: Frame rate control, 3 args + delay: + // 0x00, // fastest refresh + // 0x06, // 6 lines front porch + // 0x03, // 3 lines back porch + // 10, // 10 ms delay + // ST77XX_MADCTL, 1, // 5: Mem access ctl (directions), 1 arg: + // 0x08, // Row/col addr, bottom-top refresh + // ST7735_DISSET5, 2, // 6: Display settings #5, 2 args: + // 0x15, // 1 clk cycle nonoverlap, 2 cycle gate + // // rise, 3 cycle osc equalize + // 0x02, // Fix on VTL + // ST7735_INVCTR, 1, // 7: Display inversion control, 1 arg: + // 0x0, // Line inversion + // ST7735_PWCTR1, 2+ST_CMD_DELAY, // 8: Power control, 2 args + delay: + // 0x02, // GVDD = 4.7V + // 0x70, // 1.0uA + // 10, // 10 ms delay + // ST7735_PWCTR2, 1, // 9: Power control, 1 arg, no delay: + // 0x05, // VGH = 14.7V, VGL = -7.35V + // ST7735_PWCTR3, 2, // 10: Power control, 2 args, no delay: + // 0x01, // Opamp current small + // 0x02, // Boost frequency + // ST7735_VMCTR1, 2+ST_CMD_DELAY, // 11: Power control, 2 args + delay: + // 0x3C, // VCOMH = 4V + // 0x38, // VCOML = -1.1V + // 10, // 10 ms delay + // ST7735_PWCTR6, 2, // 12: Power control, 2 args, no delay: + // 0x11, 0x15, + // ST7735_GMCTRP1,16, // 13: Gamma Adjustments (pos. polarity), 16 args + delay: + // 0x09, 0x16, 0x09, 0x20, // (Not entirely necessary, but provides + // 0x21, 0x1B, 0x13, 0x19, // accurate colors) + // 0x17, 0x15, 0x1E, 0x2B, + // 0x04, 0x05, 0x02, 0x0E, + // ST7735_GMCTRN1,16+ST_CMD_DELAY, // 14: Gamma Adjustments (neg. polarity), 16 args + delay: + // 0x0B, 0x14, 0x08, 0x1E, // (Not entirely necessary, but provides + // 0x22, 0x1D, 0x18, 0x1E, // accurate colors) + // 0x1B, 0x1A, 0x24, 0x2B, + // 0x06, 0x06, 0x02, 0x0F, + // 10, // 10 ms delay + // ST77XX_CASET, 4, // 15: Column addr set, 4 args, no delay: + // 0x00, 0x02, // XSTART = 2 + // 0x00, 0x81, // XEND = 129 + // ST77XX_RASET, 4, // 16: Row addr set, 4 args, no delay: + // 0x00, 0x02, // XSTART = 1 + // 0x00, 0x81, // XEND = 160 + // ST77XX_NORON, ST_CMD_DELAY, // 17: Normal display on, no args, w/delay + // 10, // 10 ms delay + // ST77XX_DISPON, ST_CMD_DELAY, // 18: Main screen turn on, no args, delay + // 255 }, // 255 = max (500 ms) delay + + // Rcmd1[] = { // 7735R init, part 1 (red or green tab) + // 15, // 15 commands in list: + // ST77XX_SWRESET, ST_CMD_DELAY, // 1: Software reset, 0 args, w/delay + // 150, // 150 ms delay + // ST77XX_SLPOUT, ST_CMD_DELAY, // 2: Out of sleep mode, 0 args, w/delay + // 255, // 500 ms delay + // ST7735_FRMCTR1, 3, // 3: Framerate ctrl - normal mode, 3 arg: + // 0x01, 0x2C, 0x2D, // Rate = fosc/(1x2+40) * (LINE+2C+2D) + // ST7735_FRMCTR2, 3, // 4: Framerate ctrl - idle mode, 3 args: + // 0x01, 0x2C, 0x2D, // Rate = fosc/(1x2+40) * (LINE+2C+2D) + // ST7735_FRMCTR3, 6, // 5: Framerate - partial mode, 6 args: + // 0x01, 0x2C, 0x2D, // Dot inversion mode + // 0x01, 0x2C, 0x2D, // Line inversion mode + // ST7735_INVCTR, 1, // 6: Display inversion ctrl, 1 arg: + // 0x07, // No inversion + // ST7735_PWCTR1, 3, // 7: Power control, 3 args, no delay: + // 0xA2, + // 0x02, // -4.6V + // 0x84, // AUTO mode + // ST7735_PWCTR2, 1, // 8: Power control, 1 arg, no delay: + // 0xC5, // VGH25=2.4C VGSEL=-10 VGH=3 * AVDD + // ST7735_PWCTR3, 2, // 9: Power control, 2 args, no delay: + // 0x0A, // Opamp current small + // 0x00, // Boost frequency + // ST7735_PWCTR4, 2, // 10: Power control, 2 args, no delay: + // 0x8A, // BCLK/2, + // 0x2A, // opamp current small & medium low + // ST7735_PWCTR5, 2, // 11: Power control, 2 args, no delay: + // 0x8A, 0xEE, + // ST7735_VMCTR1, 1, // 12: Power control, 1 arg, no delay: + // 0x0E, + // ST77XX_INVOFF, 0, // 13: Don't invert display, no args + // ST77XX_MADCTL, 1, // 14: Mem access ctl (directions), 1 arg: + // 0xC8, // row/col addr, bottom-top refresh + // ST77XX_COLMOD, 1, // 15: set color mode, 1 arg, no delay: + // 0x05 }, // 16-bit color + + // Rcmd2green[] = { // 7735R init, part 2 (green tab only) + // 2, // 2 commands in list: + // ST77XX_CASET, 4, // 1: Column addr set, 4 args, no delay: + // 0x00, 0x02, // XSTART = 0 + // 0x00, 0x7F+0x02, // XEND = 127 + // ST77XX_RASET, 4, // 2: Row addr set, 4 args, no delay: + // 0x00, 0x01, // XSTART = 0 + // 0x00, 0x9F+0x01 }, // XEND = 159 + + // Rcmd2red[] = { // 7735R init, part 2 (red tab only) + // 2, // 2 commands in list: + // ST77XX_CASET, 4, // 1: Column addr set, 4 args, no delay: + // 0x00, 0x00, // XSTART = 0 + // 0x00, 0x7F, // XEND = 127 + // ST77XX_RASET, 4, // 2: Row addr set, 4 args, no delay: + // 0x00, 0x00, // XSTART = 0 + // 0x00, 0x9F }, // XEND = 159 + + // Rcmd2green144[] = { // 7735R init, part 2 (green 1.44 tab) + // 2, // 2 commands in list: + // ST77XX_CASET, 4, // 1: Column addr set, 4 args, no delay: + // 0x00, 0x00, // XSTART = 0 + // 0x00, 0x7F, // XEND = 127 + // ST77XX_RASET, 4, // 2: Row addr set, 4 args, no delay: + // 0x00, 0x00, // XSTART = 0 + // 0x00, 0x7F }, // XEND = 127 + + // Rcmd2green160x80[] = { // 7735R init, part 2 (mini 160x80) + // 2, // 2 commands in list: + // ST77XX_CASET, 4, // 1: Column addr set, 4 args, no delay: + // 0x00, 0x00, // XSTART = 0 + // 0x00, 0x4F, // XEND = 79 + // ST77XX_RASET, 4, // 2: Row addr set, 4 args, no delay: + // 0x00, 0x00, // XSTART = 0 + // 0x00, 0x9F }, // XEND = 159 + + // Rcmd2green160x80plugin[] = { // 7735R init, part 2 (mini 160x80 with plugin FPC) + // 3, // 3 commands in list: + // ST77XX_INVON, 0, // 1: Display is inverted + // ST77XX_CASET, 4, // 2: Column addr set, 4 args, no delay: + // 0x00, 0x00, // XSTART = 0 + // 0x00, 0x4F, // XEND = 79 + // ST77XX_RASET, 4, // 3: Row addr set, 4 args, no delay: + // 0x00, 0x00, // XSTART = 0 + // 0x00, 0x9F }, // XEND = 159 + + // Rcmd3[] = { // 7735R init, part 3 (red or green tab) + // 4, // 4 commands in list: + // ST7735_GMCTRP1, 16 , // 1: Gamma Adjustments (pos. polarity), 16 args + delay: + // 0x02, 0x1c, 0x07, 0x12, // (Not entirely necessary, but provides + // 0x37, 0x32, 0x29, 0x2d, // accurate colors) + // 0x29, 0x25, 0x2B, 0x39, + // 0x00, 0x01, 0x03, 0x10, + // ST7735_GMCTRN1, 16 , // 2: Gamma Adjustments (neg. polarity), 16 args + delay: + // 0x03, 0x1d, 0x07, 0x06, // (Not entirely necessary, but provides + // 0x2E, 0x2C, 0x29, 0x2D, // accurate colors) + // 0x2E, 0x2E, 0x37, 0x3F, + // 0x00, 0x00, 0x02, 0x10, + // ST77XX_NORON, ST_CMD_DELAY, // 3: Normal display on, no args, w/delay + // 10, // 10 ms delay + // ST77XX_DISPON, ST_CMD_DELAY, // 4: Main screen turn on, no args w/delay + // 100 }; // 100 ms delay + }; + + const ST7789_Device = struct { + // static const uint8_t PROGMEM + // generic_st7789[] = { // Init commands for 7789 screens + // 9, // 9 commands in list: + // ST77XX_SWRESET, ST_CMD_DELAY, // 1: Software reset, no args, w/delay + // 150, // ~150 ms delay + // ST77XX_SLPOUT , ST_CMD_DELAY, // 2: Out of sleep mode, no args, w/delay + // 10, // 10 ms delay + // ST77XX_COLMOD , 1+ST_CMD_DELAY, // 3: Set color mode, 1 arg + delay: + // 0x55, // 16-bit color + // 10, // 10 ms delay + // ST77XX_MADCTL , 1, // 4: Mem access ctrl (directions), 1 arg: + // 0x08, // Row/col addr, bottom-top refresh + // ST77XX_CASET , 4, // 5: Column addr set, 4 args, no delay: + // 0x00, + // 0, // XSTART = 0 + // 0, + // 240, // XEND = 240 + // ST77XX_RASET , 4, // 6: Row addr set, 4 args, no delay: + // 0x00, + // 0, // YSTART = 0 + // 320>>8, + // 320&0xFF, // YEND = 320 + // ST77XX_INVON , ST_CMD_DELAY, // 7: hack + // 10, + // ST77XX_NORON , ST_CMD_DELAY, // 8: Normal display on, no args, w/delay + // 10, // 10 ms delay + // ST77XX_DISPON , ST_CMD_DELAY, // 9: Main screen turn on, no args, delay + // 10 }; // 10 ms delay + }; + }; +} + +pub const Device = enum { + st7735, + st7789, +}; + +pub const Resolution = struct { + width: u16, + height: u16, +}; + +pub const Rotation = enum(u2) { + normal, + left90, + right90, + upside_down, +}; + +test { + _ = ST7735; + _ = ST7789; +} + +test { + var channel = mdf.base.Datagram_Device.Test_Device.init_receiver_only(); + defer channel.deinit(); + + var rst_pin = mdf.base.Digital_IO.Test_Device.init(.output, .high); + var dat_pin = mdf.base.Digital_IO.Test_Device.init(.output, .high); + + var dri = try ST7735.init( + channel.datagram_device(), + rst_pin.digital_io(), + dat_pin.digital_io(), + .{ .width = 128, .height = 128 }, + ); + + try dri.set_address_window(16, 32, 48, 64); + try dri.set_rotation(.normal); + try dri.enable_display(true); + try dri.enable_tearing(false); + try dri.enable_sleep(true); + try dri.invert_display(false); + try dri.set_pixel(11, 15, Color.blue); +} diff --git a/drivers/framework.zig b/drivers/framework.zig new file mode 100644 index 00000000..1790e86e --- /dev/null +++ b/drivers/framework.zig @@ -0,0 +1,55 @@ +//! +//! The driver framework provides device-independent drivers for peripherials supported by MicroZig. +//! + +pub const display = struct { + pub const ssd1306 = @import("display/ssd1306.zig"); + pub const st77xx = @import("display/st77xx.zig"); + + // Export generic drivers: + pub const SSD1306_I2C = ssd1306.SSD1306_I2C; + pub const ST7735 = st77xx.ST7735; + pub const ST7789 = st77xx.ST7789; + + // Export color types: + pub const colors = @import("display/colors.zig"); +}; + +pub const input = struct { + pub const rotary_encoder = @import("input/rotary-encoder.zig"); + pub const keyboard_matrix = @import("input/keyboard-matrix.zig"); + pub const debounced_button = @import("input/debounced-button.zig"); + + // Export generic drivers: + pub const Key = keyboard_matrix.Key; + pub const Keyboard_Matrix = keyboard_matrix.Keyboard_Matrix; + pub const Debounced_Button = debounced_button.Debounced_Button; + pub const Rotary_Encoder = rotary_encoder.Rotary_Encoder; + + pub const touch = struct { + // const xpt2046 = @import("input/touch/xpt2046.zig"); + }; +}; + +pub const wireless = struct { + // pub const sx1278 = @import("wireless/sx1278.zig"); +}; + +pub const base = struct { + pub const Datagram_Device = @import("base/Datagram_Device.zig"); + pub const Stream_Device = @import("base/Stream_Device.zig"); + pub const Digital_IO = @import("base/Digital_IO.zig"); +}; + +test { + _ = display.ssd1306; + _ = display.st77xx; + + _ = input.keyboard_matrix; + _ = input.debounced_button; + _ = input.rotary_encoder; + + _ = base.Datagram_Device; + _ = base.Stream_Device; + _ = base.Digital_IO; +} diff --git a/drivers/input/debounced-button.zig b/drivers/input/debounced-button.zig new file mode 100644 index 00000000..93f7e51f --- /dev/null +++ b/drivers/input/debounced-button.zig @@ -0,0 +1,226 @@ +//! +//! Implements a simple polling button driver that uses a debouncing technique. +//! + +const std = @import("std"); +const mdf = @import("../framework.zig"); + +pub const Debounced_Button_Options = struct { + /// The active state for the button. Use `.high` for active-high, `.low` for active-low. + active_state: mdf.base.Digital_IO.State, + + /// Optional filter depth for debouncing. If `null` is passed, 16 samples are used to debounce the button, + /// otherwise the given number of samples is used. + filter_depth: ?comptime_int = null, + + /// Which digital i/o interface should be used. + Digital_IO: type = mdf.base.Digital_IO, +}; + +/// +/// A button sensor that uses a digital i/o to read the button state. +/// +/// It uses a simple filtering technique to debounce glitches on the digital status: +/// +/// A filter integer is used that stores the last N samples of the button and the button +/// is considered pressed if at least a single bit in the filter is set. +/// +/// Each poll, the filter integer is shifted by one bit and the new state is inserted. This +/// way, a simple "was it pressed in the last N polls" function is implemented. +/// +pub fn Debounced_Button(comptime options: Debounced_Button_Options) type { + return struct { + const Button = @This(); + const Filter_Int = std.meta.Int(.unsigned, options.filter_depth orelse 16); + + io: options.Digital_IO, + filter_buffer: Filter_Int, + + pub fn init(io: options.Digital_IO) !Button { + try io.set_bias(options.active_state.invert()); + try io.set_direction(.input); + return Button{ + .io = io, + .filter_buffer = 0, + }; + } + + /// Polls for the button state. Returns the change feventor the button if any. + pub fn poll(self: *Button) !Event { + const state = try self.io.read(); + const active_unfiltered = (state == options.active_state); + + const previous_debounce = self.filter_buffer; + self.filter_buffer <<= 1; + if (active_unfiltered) { + self.filter_buffer |= 1; + } + + if (active_unfiltered and previous_debounce == 0) { + return .pressed; + } else if (!active_unfiltered and self.filter_buffer == 0 and previous_debounce != 0) { + return .released; + } else { + return .idle; + } + } + + /// Returns `true` when the button is pressed. + /// + /// NOTE: Will only be updated when `.poll()` is called! + pub fn is_pressed(self: *Button) bool { + return (self.filter_buffer != 0); + } + }; +} + +pub const Event = enum { + /// Nothing has changed. + idle, + + /// The button was pressed. Will only trigger once per press. + /// Use `Button.is_pressed()` to check if the button is currently held. + pressed, + + /// The button was released. Will only trigger once per release. + /// Use `Button.is_pressed()` to check if the button is currently held. + released, +}; + +test Debounced_Button { + const Button = Debounced_Button(.{ + .active_state = .low, + .filter_depth = 4, + }); + + var button_input = mdf.base.Digital_IO.Test_Device.init(.input, .high); + + var button = try Button.init(button_input.digital_io()); + + // Basic test sequence: + // 1 2 3 + // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 + // ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ + // ╍╍━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╍╍ + // Input ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┃ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┃ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ + // ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ + // ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ + // ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ + // State ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┃ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┃ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ + // ╍╍━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╍╍ + // ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ + // ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┏━━┓ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ + // Pressed ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┃ ┃ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ + // ╍╍━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╍╍ + // ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ + // ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┏━━┓ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ + // Released ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┃ ┃ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ + // ╍╍━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━╍╍ + // ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ + + { + + // Button should be stable in its state in idle: + for (0..10) |_| { + try std.testing.expectEqual(.idle, try button.poll()); + try std.testing.expectEqual(false, button.is_pressed()); + } + + button_input.state = .low; + + try std.testing.expectEqual(.pressed, try button.poll()); + try std.testing.expectEqual(true, button.is_pressed()); + + // Button should be stable in its state in idle: + for (0..10) |_| { + try std.testing.expectEqual(.idle, try button.poll()); + try std.testing.expectEqual(true, button.is_pressed()); + } + + button_input.state = .high; + + // The button will stay in "high" state for three successive polls: + for (0..3) |_| { + try std.testing.expectEqual(.idle, try button.poll()); + try std.testing.expectEqual(true, button.is_pressed()); + } + + // then it should yield a ".released" event: + try std.testing.expectEqual(.released, try button.poll()); + try std.testing.expectEqual(false, button.is_pressed()); + + // afterwards it has to be stable again: + for (0..10) |_| { + try std.testing.expectEqual(.idle, try button.poll()); + try std.testing.expectEqual(false, button.is_pressed()); + } + } + + // Test sequence for two short consecutive signals: + // 1 + // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 + // ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ + // ╍╍━━━┓ ┏━━┓ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╍╍ + // Input ┆ ┃ ┃ ┃ ┃ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ + // ┆ ┗━━┛ ┗━━┛ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ + // ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ + // ┆ ┏━━━━━━━━━━━━━━━━━┓ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ + // State ┆ ┃ ┆ ┆ ┆ ┆ ┆ ┃ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ + // ╍╍━━━┛ ┆ ┆ ┆ ┆ ┆ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╍╍ + // ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ + // ┆ ┏━━┓ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ + // Pressed ┆ ┃ ┃ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ + // ╍╍━━━┛ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╍╍ + // ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ + // ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┏━━┓ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ + // Released ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┃ ┃ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ + // ╍╍━━━━━━━━━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━╍╍ + // ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ + { + button_input.state = .low; + + try std.testing.expectEqual(.pressed, try button.poll()); + try std.testing.expectEqual(true, button.is_pressed()); + + button_input.state = .high; + + try std.testing.expectEqual(.idle, try button.poll()); + try std.testing.expectEqual(true, button.is_pressed()); + + button_input.state = .low; + + try std.testing.expectEqual(.idle, try button.poll()); + try std.testing.expectEqual(true, button.is_pressed()); + + button_input.state = .high; + + // The button will stay in "high" state for three successive polls: + for (0..3) |_| { + try std.testing.expectEqual(.idle, try button.poll()); + try std.testing.expectEqual(true, button.is_pressed()); + } + + // then it should yield a ".released" event: + try std.testing.expectEqual(.released, try button.poll()); + try std.testing.expectEqual(false, button.is_pressed()); + + // afterwards it has to be stable again: + for (0..20) |_| { + try std.testing.expectEqual(.idle, try button.poll()); + try std.testing.expectEqual(false, button.is_pressed()); + } + } +} + +// waveform # sequence 1: +// Input | HHHHHHHHHHLLLLLLLLLLLHHHHHHHHHHHHHH +// State | LLLLLLLLLLHHHHHHHHHHHHHHLLLLLLLLLLL +// Pressed | LLLLLLLLLLHLLLLLLLLLLLLLLLLLLLLLLLL +// Released | LLLLLLLLLLLLLLLLLLLLLLLLHLLLLLLLLLL + +// waveform # sequence 2: + +// Input | HLHLHHHHHHHHHHHHHH +// State | LHHHHHHLLLLLLLLLLL +// Pressed | LHLLLLLLLLLLLLLLLL +// Released | LLLLLLLHLLLLLLLLLL diff --git a/drivers/input/keyboard-matrix.zig b/drivers/input/keyboard-matrix.zig new file mode 100644 index 00000000..eec5e8e5 --- /dev/null +++ b/drivers/input/keyboard-matrix.zig @@ -0,0 +1,249 @@ +//! +//! Implements a N*M keyboard matrix that will be scanned in column-major order. +//! + +const std = @import("std"); +const mdf = @import("../framework.zig"); + +pub const Keyboard_Matrix_Options = struct { + /// How many rows does the keyboard matrix have? + rows: usize, + + /// How many columns does the keyboard matrix have? + columns: usize, + + /// Which digital i/o interface should be used. + Digital_IO: type = mdf.base.Digital_IO, +}; + +/// Keyboard matrix implementation via GPIOs that scans columns and checks rows. +/// +/// Will use the columns as matrix drivers (outputs) and the rows as matrix readers (inputs). +pub fn Keyboard_Matrix(comptime options: Keyboard_Matrix_Options) type { + if (options.columns > 256 or options.rows > 256) + @compileError("cannot encode more than 256 rows or columns!"); + return struct { + const Matrix = @This(); + + const Digital_IO: type = options.Digital_IO; + const col_count: usize = options.columns; + const row_count: usize = options.rows; + + /// Number of keys in this matrix. + pub const key_count = col_count * row_count; + + /// Returns the index for the given key. Assumes that `key` is valid. + pub fn index(key: Key) usize { + return key.column() * row_count + key.row(); + } + + /// A set that can store if each key is set or not. + pub const Set = struct { + pressed: std.StaticBitSet(key_count) = std.StaticBitSet(key_count).initEmpty(), + + /// Adds a key to the set. + pub fn add(set: *Set, key: Key) void { + set.pressed.set(index(key)); + } + + /// Checks if a key is pressed. + pub fn is_pressed(set: Set, key: Key) bool { + return set.pressed.isSet(index(key)); + } + + /// Returns if any key is pressed. + pub fn any(set: Set) bool { + return (set.pressed.count() > 0); + } + }; + + cols: [col_count]Digital_IO, + rows: [row_count]Digital_IO, + + /// Initializes all GPIOs of the matrix and returns a new instance. + pub fn init( + cols: [col_count]Digital_IO, + rows: [row_count]Digital_IO, + ) !Matrix { + var mat = Matrix{ + .cols = cols, + .rows = rows, + }; + for (cols) |c| { + try c.set_direction(.output); + } + for (rows) |r| { + try r.set_direction(.input); + try r.set_bias(.high); + } + try mat.set_all_to(.high); + return mat; + } + + /// Scans the matrix and returns a set of all pressed keys. + pub fn scan(matrix: Matrix) !Set { + var result = Set{}; + + try matrix.set_all_to(.high); + + for (matrix.cols, 0..) |c_pin, c_index| { + try c_pin.write(.low); + busyloop(10); + + for (matrix.rows, 0..) |r_pin, r_index| { + const state = try r_pin.read(); + if (state == .low) { + // someone actually pressed a key! + result.add(Key.new( + @as(u8, @truncate(r_index)), + @as(u8, @truncate(c_index)), + )); + } + } + + try c_pin.write(.high); + busyloop(100); + } + + try matrix.set_all_to(.high); + + return result; + } + + fn set_all_to(matrix: Matrix, value: mdf.base.Digital_IO.State) !void { + for (matrix.cols) |c| { + try c.write(value); + } + } + }; +} + +/// A single key in a 2D keyboard matrix. +/// +/// NOTE: This type assumes there are not more than 256 rows and columns in a keyboard matrix. +pub const Key = enum(u16) { + // we just assume we have enough encoding space and not more than 256 cols/rows + _, + + pub fn new(r: u8, c: u8) Key { + return Encoding.to_key(.{ + .row = r, + .column = c, + }); + } + + pub fn row(key: Key) u8 { + return Encoding.from_key(key).row; + } + + pub fn column(key: Key) u8 { + return Encoding.from_key(key).column; + } + + const Encoding = packed struct(u16) { + column: u8, + row: u8, + + pub fn from_key(key: Key) Encoding { + const int: u16 = @intFromEnum(key); + return @bitCast(int); + } + + pub fn to_key(enc: Encoding) Key { + const int: u16 = @bitCast(enc); + return @enumFromInt(int); + } + }; +}; + +inline fn busyloop(comptime N: comptime_int) void { + for (0..N) |_| { + // wait some cycles so the physics does its magic and convey + // the electrons + asm volatile ("" ::: "memory"); + } +} + +test Keyboard_Matrix { + const key_tl = Key.new(0, 0); + const key_tr = Key.new(0, 1); + const key_bl = Key.new(1, 0); + const key_br = Key.new(1, 1); + + // Those are "drivers", so we initialize them inverse + // to what we'd expect them to be + var col_pins = [_]mdf.base.Digital_IO.Test_Device{ + mdf.base.Digital_IO.Test_Device.init(.input, .low), + mdf.base.Digital_IO.Test_Device.init(.input, .low), + }; + + // Those are "inputs", so we have to check the + // direction init, but not the state change: + var row_pins = [_]mdf.base.Digital_IO.Test_Device{ + mdf.base.Digital_IO.Test_Device.init(.output, .high), + mdf.base.Digital_IO.Test_Device.init(.output, .high), + }; + + const row_drivers = [_]mdf.base.Digital_IO{ + row_pins[0].digital_io(), + row_pins[1].digital_io(), + }; + + const col_drivers = [_]mdf.base.Digital_IO{ + col_pins[0].digital_io(), + col_pins[1].digital_io(), + }; + + const Matrix = Keyboard_Matrix(.{ + .rows = 2, + .columns = 2, + }); + + var matrix = try Matrix.init(col_drivers, row_drivers); + + // Check if rows are properly initialized: + for (row_pins) |pin| { + try std.testing.expectEqual(.input, pin.dir); + } + + // Check if columns are properly initialized: + for (col_pins) |pin| { + try std.testing.expectEqual(.output, pin.dir); + try std.testing.expectEqual(.high, pin.state); + } + + { + // by default, the matrix should be "unset" as long as our inputs are + // high: + const set = try matrix.scan(); + try std.testing.expectEqual(false, set.any()); + } + + { + // Test that if we set the top row to low, both "top" keys must be + // pressed, but the "bottom" keys must not: + row_pins[0].state = .low; + defer row_pins[0].state = .high; + + const set = try matrix.scan(); + try std.testing.expectEqual(true, set.any()); + try std.testing.expectEqual(true, set.is_pressed(key_tl)); + try std.testing.expectEqual(true, set.is_pressed(key_tr)); + try std.testing.expectEqual(false, set.is_pressed(key_bl)); + try std.testing.expectEqual(false, set.is_pressed(key_br)); + } + + { + // Test that if we set the bottom row to low, both "bottom" keys must be + // pressed, but the "top" keys must not: + row_pins[1].state = .low; + defer row_pins[1].state = .high; + + const set = try matrix.scan(); + try std.testing.expectEqual(true, set.any()); + try std.testing.expectEqual(false, set.is_pressed(key_tl)); + try std.testing.expectEqual(false, set.is_pressed(key_tr)); + try std.testing.expectEqual(true, set.is_pressed(key_bl)); + try std.testing.expectEqual(true, set.is_pressed(key_br)); + } +} diff --git a/drivers/input/rotary-encoder.zig b/drivers/input/rotary-encoder.zig new file mode 100644 index 00000000..f2ba6622 --- /dev/null +++ b/drivers/input/rotary-encoder.zig @@ -0,0 +1,139 @@ +//! +//! Implements a bounce-free decoder for rotary encoders +//! + +const std = @import("std"); +const mdf = @import("../framework.zig"); + +pub const Rotary_Encoder_Options = struct { + /// Which digital i/o interface should be used. + Digital_IO: type = mdf.base.Digital_IO, +}; + +pub fn Rotary_Encoder(comptime options: Rotary_Encoder_Options) type { + return struct { + const Encoder = @This(); + + const Digital_IO = options.Digital_IO; + + a: Digital_IO, + b: Digital_IO, + + last_a: mdf.base.Digital_IO.State, + last_b: mdf.base.Digital_IO.State, + + pub fn init( + a: Digital_IO, + b: Digital_IO, + idle_state: mdf.base.Digital_IO.State, + ) !Encoder { + try a.set_direction(.input); + try b.set_direction(.input); + try a.set_bias(idle_state); + try b.set_bias(idle_state); + return Encoder{ + .a = a, + .b = b, + .last_a = try a.read(), + .last_b = try b.read(), + }; + } + + pub fn poll(enc: *Encoder) !Event { + var a = try enc.a.read(); + var b = try enc.b.read(); + defer enc.last_a = a; + defer enc.last_b = b; + + const enable = a.value() ^ b.value() ^ enc.last_a.value() ^ enc.last_b.value(); + const direction = a.value() ^ enc.last_b.value(); + + if (enable != 0) { + if (direction != 0) { + return .increment; + } else { + return .decrement; + } + } else { + return .idle; + } + } + }; +} + +pub const Event = enum(i2) { + /// No change since the last decoding happened + idle = 0, + /// The quadrature signal incremented a step. + increment = 1, + /// The quadrature signal decremented a step. + decrement = -1, + /// The quadrature signal skipped a sequence point and entered a invalid state. + @"error" = -2, +}; + +test Rotary_Encoder { + var a = mdf.base.Digital_IO.Test_Device.init(.output, .high); + var b = mdf.base.Digital_IO.Test_Device.init(.output, .high); + + var encoder = try Rotary_Encoder(.{}).init( + a.digital_io(), + b.digital_io(), + .high, + ); + + // test for correct initialization: + try std.testing.expectEqual(.input, a.dir); + try std.testing.expectEqual(.input, b.dir); + + // test correct default state: + for (0..10) |_| { + try std.testing.expectEqual(.idle, try encoder.poll()); + } + + a.state = .low; + + try std.testing.expectEqual(.increment, try encoder.poll()); + try std.testing.expectEqual(.idle, try encoder.poll()); + + a.state = .high; + + try std.testing.expectEqual(.decrement, try encoder.poll()); + try std.testing.expectEqual(.idle, try encoder.poll()); + + b.state = .low; + + try std.testing.expectEqual(.decrement, try encoder.poll()); + try std.testing.expectEqual(.idle, try encoder.poll()); + + b.state = .high; + + try std.testing.expectEqual(.increment, try encoder.poll()); + try std.testing.expectEqual(.idle, try encoder.poll()); + + // Generate "positive" quadrature signal: + for (0..100) |_| { + a.state = a.state.invert(); + + try std.testing.expectEqual(.increment, try encoder.poll()); + try std.testing.expectEqual(.idle, try encoder.poll()); + + b.state = b.state.invert(); + + try std.testing.expectEqual(.increment, try encoder.poll()); + try std.testing.expectEqual(.idle, try encoder.poll()); + } + + // Generate "negative" quadrature signal: + for (0..100) |_| { + b.state = b.state.invert(); + + try std.testing.expectEqual(.decrement, try encoder.poll()); + try std.testing.expectEqual(.idle, try encoder.poll()); + + a.state = a.state.invert(); + + try std.testing.expectEqual(.decrement, try encoder.poll()); + try std.testing.expectEqual(.idle, try encoder.poll()); + } +} diff --git a/port/raspberrypi/rp2xxx/src/hal.zig b/port/raspberrypi/rp2xxx/src/hal.zig index c048d306..2f45aaab 100644 --- a/port/raspberrypi/rp2xxx/src/hal.zig +++ b/port/raspberrypi/rp2xxx/src/hal.zig @@ -20,6 +20,7 @@ pub const i2c = @import("hal/i2c.zig"); pub const time = @import("hal/time.zig"); pub const uart = @import("hal/uart.zig"); pub const usb = @import("hal/usb.zig"); +pub const drivers = @import("hal/drivers.zig"); pub const compatibility = @import("hal/compatibility.zig"); /// A default clock configuration with sensible defaults that will work diff --git a/port/raspberrypi/rp2xxx/src/hal/drivers.zig b/port/raspberrypi/rp2xxx/src/hal/drivers.zig new file mode 100644 index 00000000..8c7d4aab --- /dev/null +++ b/port/raspberrypi/rp2xxx/src/hal/drivers.zig @@ -0,0 +1,300 @@ +//! +//! This file implements driver abstractions based on HAL devices. +//! + +const std = @import("std"); +const microzig = @import("microzig"); +const hal = @import("../hal.zig"); + +const drivers = microzig.drivers.base; + +const Datagram_Device = drivers.Datagram_Device; +const Stream_Device = drivers.Stream_Device; +const Digital_IO = drivers.Digital_IO; + +/// +/// A datagram device attached to an I²C bus. +/// +pub const I2C_Device = struct { + pub const ConnectError = Datagram_Device.ConnectError; + pub const WriteError = Datagram_Device.WriteError; + pub const ReadError = Datagram_Device.ReadError; + + /// Selects I²C bus should be used. + bus: hal.i2c.I2C, + + /// The address of our I²C device. + address: hal.i2c.Address, + + pub fn init(bus: hal.i2c.I2C, address: hal.i2c.Address) I2C_Device { + return .{ + .bus = bus, + .address = address, + }; + } + + pub fn datagram_device(dev: *I2C_Device) Datagram_Device { + return .{ + .object = dev, + .vtable = &vtable, + }; + } + + pub fn connect(dev: I2C_Device) ConnectError!void { + _ = dev; + } + + pub fn disconnect(dev: I2C_Device) void { + _ = dev; + } + + pub fn write(dev: I2C_Device, datagram: []const []const u8) !void { + try dev.bus.write_blocking(dev.address, datagram, null); + } + + pub fn writev(dev: I2C_Device, datagrams: []const []const u8) !void { + try dev.bus.writev_blocking(dev.address, datagrams, null); + } + + pub fn read(dev: I2C_Device, datagram: []const u8) !usize { + try dev.bus.read_blocking(dev.address, datagram, null); + return datagram.len; + } + + pub fn readv(dev: I2C_Device, datagrams: []const []u8) !usize { + try dev.bus.readv_blocking(dev.address, datagrams, null); + return microzig.utilities.Slice_Vector([]u8).init(datagrams).size(); + } + + const vtable = Datagram_Device.VTable{ + .connect_fn = null, + .disconnect_fn = null, + .writev_fn = writev_fn, + .readv_fn = readv_fn, + }; + + fn writev_fn(dd: *anyopaque, chunks: []const []const u8) WriteError!void { + const dev: *I2C_Device = @ptrCast(@alignCast(dd)); + return dev.writev(chunks) catch |err| switch (err) { + error.DeviceNotPresent, + error.NoAcknowledge, + error.TargetAddressReserved, + => return error.Unsupported, + + error.UnknownAbort, + error.TxFifoFlushed, + => return error.IoError, + + error.Timeout => return error.Timeout, + error.NoData => {}, + }; + } + + fn readv_fn(dd: *anyopaque, chunks: []const []u8) ReadError!usize { + const dev: *I2C_Device = @ptrCast(@alignCast(dd)); + return dev.readv(chunks) catch |err| switch (err) { + error.DeviceNotPresent, + error.NoAcknowledge, + error.TargetAddressReserved, + => return error.Unsupported, + + error.UnknownAbort, + error.TxFifoFlushed, + => return error.IoError, + + error.Timeout => return error.Timeout, + error.NoData => return 0, + }; + } +}; + +/// +/// A datagram device attached to an SPI bus. +/// +pub const SPI_Device = struct { + pub const ConnectError = Datagram_Device.ConnectError; + pub const WriteError = Datagram_Device.WriteError; + pub const ReadError = Datagram_Device.ReadError; + + bus: hal.spi.SPI, + chip_select: hal.gpio.Pin, + active_level: Digital_IO.State, + rx_dummy_data: u8, + + pub const InitOptions = struct { + /// The active level for the chip select pin + active_level: Digital_IO.State = .low, + + /// Which dummy byte should be sent during reads + rx_dummy_data: u8 = 0x00, + }; + + pub fn init( + bus: hal.spi.SPI, + chip_select: hal.gpio.Pin, + options: InitOptions, + ) SPI_Device { + chip_select.set_function(.sio); + chip_select.set_direction(.out); + + var dev: SPI_Device = .{ + .bus = bus, + .chip_select = chip_select, + .active_level = options.active_level, + .rx_dummy_data = options.rx_dummy_data, + }; + // set the chip select to "deselect" the device + dev.disconnect(); + return dev; + } + + pub fn datagram_device(dev: *SPI_Device) Datagram_Device { + return .{ + .object = dev, + .vtable = &vtable, + }; + } + + pub fn connect(dev: SPI_Device) !void { + const actual_level = dev.chip_select.read(); + + const target_level: u1 = switch (dev.active_level) { + .low => 0, + .high => 1, + }; + + if (target_level == actual_level) + return error.DeviceBusy; + + dev.chip_select.put(target_level); + } + + pub fn disconnect(dev: SPI_Device) void { + dev.chip_select.put(switch (dev.active_level) { + .low => 1, + .high => 0, + }); + } + + pub fn write(dev: SPI_Device, datagrams: []const u8) !void { + dev.bus.writev_blocking(u8, datagrams); + } + + pub fn writev(dev: SPI_Device, datagrams: []const []const u8) !void { + dev.bus.writev_blocking(u8, datagrams); + } + + pub fn read(dev: SPI_Device, datagrams: []u8) !void { + dev.bus.read_blocking(u8, dev.rx_dummy_data, datagrams); + } + + pub fn readv(dev: SPI_Device, datagrams: []const []const u8) !usize { + dev.bus.readv_blocking(u8, dev.rx_dummy_data, datagrams); + return microzig.utilities.Slice_Vector([]u8).init(datagrams).size(); + } + + const vtable = Datagram_Device.VTable{ + .connect_fn = connect_fn, + .disconnect_fn = disconnect_fn, + .writev_fn = writev_fn, + .readv_fn = readv_fn, + }; + + fn connect_fn(dd: *anyopaque) ConnectError!void { + const dev: *SPI_Device = @ptrCast(@alignCast(dd)); + try dev.connect(); + } + + fn disconnect_fn(dd: *anyopaque) void { + const dev: *SPI_Device = @ptrCast(@alignCast(dd)); + dev.disconnect(); + } + + fn writev_fn(dd: *anyopaque, chunks: []const []const u8) WriteError!void { + const dev: *SPI_Device = @ptrCast(@alignCast(dd)); + dev.bus.writev_blocking(u8, chunks); + } + + fn readv_fn(dd: *anyopaque, chunks: []const []u8) ReadError!usize { + const dev: *SPI_Device = @ptrCast(@alignCast(dd)); + dev.bus.readv_blocking(u8, dev.rx_dummy_data, chunks); + return microzig.utilities.Slice_Vector([]u8).init(chunks).size(); + } +}; + +/// +/// Implementation of a digital i/o device. +/// +pub const GPIO_Device = struct { + pub const SetDirError = Digital_IO.SetDirError; + pub const SetBiasError = Digital_IO.SetBiasError; + pub const WriteError = Digital_IO.WriteError; + pub const ReadError = Digital_IO.ReadError; + + pub const State = Digital_IO.State; + pub const Direction = Digital_IO.Direction; + + pin: hal.gpio.Pin, + + pub fn init(pin: hal.gpio.Pin) GPIO_Device { + return .{ .pin = pin }; + } + + pub fn digital_io(dio: *GPIO_Device) Digital_IO { + return Digital_IO{ + .object = dio, + .vtable = &vtable, + }; + } + + pub fn set_direction(dio: GPIO_Device, dir: Direction) SetDirError!void { + dio.pin.set_direction(switch (dir) { + .output => .out, + .input => .in, + }); + } + + pub fn set_bias(dio: GPIO_Device, maybe_bias: ?State) SetBiasError!void { + dio.pin.set_pull(if (maybe_bias) |bias| switch (bias) { + .low => .down, + .high => .up, + } else .disabled); + } + + pub fn write(dio: GPIO_Device, state: State) WriteError!void { + dio.pin.put(state.value()); + } + + pub fn read(dio: GPIO_Device) ReadError!State { + return @enumFromInt(dio.pin.read()); + } + + const vtable = Digital_IO.VTable{ + .set_direction_fn = set_direction_fn, + .set_bias_fn = set_bias_fn, + .write_fn = write_fn, + .read_fn = read_fn, + }; + + fn set_direction_fn(io: *anyopaque, dir: Direction) SetDirError!void { + const gpio: *GPIO_Device = @ptrCast(@alignCast(io)); + try gpio.set_direction(dir); + } + + fn set_bias_fn(io: *anyopaque, bias: ?State) SetBiasError!void { + const gpio: *GPIO_Device = @ptrCast(@alignCast(io)); + + try gpio.set_bias(bias); + } + + fn write_fn(io: *anyopaque, state: State) WriteError!void { + const gpio: *GPIO_Device = @ptrCast(@alignCast(io)); + + try gpio.write(state); + } + + fn read_fn(io: *anyopaque) ReadError!State { + const gpio: *GPIO_Device = @ptrCast(@alignCast(io)); + return try gpio.read(); + } +};