├── .gitignore ├── driver ├── display │ ├── ili9488.zig │ ├── colors.zig │ ├── st77xx.zig │ └── ssd1306.zig ├── input │ ├── touch │ │ └── xpt2046.zig │ ├── rotary-encoder.zig │ ├── debounced-button.zig │ └── keyboard-matrix.zig ├── wireless │ └── sx1278.zig ├── framework.zig └── base │ ├── StreamDevice.zig │ ├── DigitalIO.zig │ └── DatagramDevice.zig ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .zig-cache/ 2 | zig-out/ 3 | -------------------------------------------------------------------------------- /driver/display/ili9488.zig: -------------------------------------------------------------------------------- 1 | //! TODO: Implement driver for ILI9488 2 | -------------------------------------------------------------------------------- /driver/input/touch/xpt2046.zig: -------------------------------------------------------------------------------- 1 | //! TODO: Implement XPT2046 driver 2 | -------------------------------------------------------------------------------- /driver/wireless/sx1278.zig: -------------------------------------------------------------------------------- 1 | //! TODO: Implement driver for SX1278 2 | 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Zig Embedded Group 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /driver/display/colors.zig: -------------------------------------------------------------------------------- 1 | pub const RGB565 = packed struct(u16) { 2 | pub usingnamespace makeDefaultColors(@This()); 3 | 4 | r: u5, 5 | g: u6, 6 | b: u5, 7 | 8 | pub fn fromRgb(r: u8, g: u8, b: u8) RGB565 { 9 | return RGB565{ 10 | .r = @truncate(r >> 3), 11 | .g = @truncate(g >> 2), 12 | .b = @truncate(b >> 3), 13 | }; 14 | } 15 | }; 16 | 17 | pub const RGB888 = extern struct { 18 | pub usingnamespace makeDefaultColors(@This()); 19 | 20 | r: u8, 21 | g: u8, 22 | b: u8, 23 | 24 | pub fn fromRgb(r: u8, g: u8, b: u8) RGB888 { 25 | return RGB888{ .r = r, .g = g, .b = b }; 26 | } 27 | }; 28 | 29 | fn makeDefaultColors(comptime Color: type) type { 30 | return struct { 31 | pub const black = Color.fromRgb(0x00, 0x00, 0x00); 32 | pub const white = Color.fromRgb(0x00, 0x00, 0x00); 33 | pub const red = Color.fromRgb(0x00, 0x00, 0x00); 34 | pub const green = Color.fromRgb(0x00, 0x00, 0x00); 35 | pub const blue = Color.fromRgb(0x00, 0x00, 0x00); 36 | pub const cyan = Color.fromRgb(0x00, 0x00, 0x00); 37 | pub const magenta = Color.fromRgb(0x00, 0x00, 0x00); 38 | pub const yellow = Color.fromRgb(0x00, 0x00, 0x00); 39 | pub const orange = Color.fromRgb(0x00, 0x00, 0x00); 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # microzig-driver-framework 2 | 3 | > **IMPORTANT NOTICE:** 4 | > This repository is deprecated and archived. The code is now merged into the MicroZig monorepo! 5 | 6 | A collection of device drivers for the use with MicroZig. 7 | 8 | > THIS PROJECT IS NOT READY YET! 9 | > We are still working out some things. 10 | 11 | ## Installation 12 | 13 | Import this package in your build.zig and add the `drivers` package to your firmware: 14 | 15 | ### With package manager 16 | 17 | ```zig 18 | const mdf_dep = b.dependency("microzig-driver-framework", .{}); 19 | 20 | const exe = microzig.addEmbeddedExecutable(builder, .{ … }); 21 | exe.addAppDependency("drivers", mdf_dep.module("drivers"), .{ .depend_on_microzig = true }); 22 | ``` 23 | 24 | ### With source code vendoring/submodules 25 | 26 | ```zig 27 | const mdf_dep = b.anonymousDependency( 28 | "vendor/microzig-driver-framework", 29 | @import("vendor/microzig-driver-framework/build.zig"), 30 | .{}, 31 | ); 32 | 33 | const exe = microzig.addEmbeddedExecutable(builder, .{ … }); 34 | exe.addAppDependency("drivers", mdf_dep.module("drivers"), .{ .depend_on_microzig = true }); 35 | ``` 36 | 37 | ## Drivers 38 | 39 | > Drivers with a checkmark are already implemented, drivers without are missing 40 | 41 | - Input 42 | - [x] Keyboard Matrix 43 | - [x] Rotary Encoder 44 | - [x] Debounced Button 45 | - Touch 46 | - [ ] XPT2046 47 | - Display 48 | - [x] SSD1306 49 | - [ ] ST7735 (WIP) 50 | - [ ] ILI9488 51 | - Wireless 52 | - [ ] SX1276, SX1278 53 | -------------------------------------------------------------------------------- /driver/framework.zig: -------------------------------------------------------------------------------- 1 | //! 2 | 3 | pub const display = struct { 4 | pub const ssd1306 = @import("display/ssd1306.zig"); 5 | pub const ili9488 = @import("display/ili9488.zig"); 6 | pub const st77xx = @import("display/st77xx.zig"); 7 | 8 | // Export generic drivers: 9 | pub const SSD1306 = ssd1306.SSD1306; 10 | pub const ST7735 = st77xx.ST7735; 11 | pub const ST7789 = st77xx.ST7789; 12 | 13 | // Export color types: 14 | const colors_ns = @import("display/colors.zig"); 15 | 16 | pub const BlackAndWhite = enum(u1) { black = 0, white = 1 }; 17 | 18 | pub const RGB565 = colors_ns.RGB565; 19 | pub const RGB888 = colors_ns.RGB888; 20 | }; 21 | 22 | pub const input = struct { 23 | pub const rotary_encoder = @import("input/rotary-encoder.zig"); 24 | pub const keyboard_matrix = @import("input/keyboard-matrix.zig"); 25 | pub const debounced_button = @import("input/debounced-button.zig"); 26 | 27 | // Export generic drivers: 28 | pub const Key = keyboard_matrix.Key; 29 | pub const KeyboardMatrix = keyboard_matrix.KeyboardMatrix; 30 | pub const DebouncedButton = debounced_button.DebouncedButton; 31 | pub const RotaryEncoder = rotary_encoder.RotaryEncoder; 32 | 33 | pub const touch = struct { 34 | const xpt2046 = @import("input/touch/xpt2046.zig"); 35 | }; 36 | }; 37 | 38 | pub const wireless = struct { 39 | pub const sx1278 = @import("wireless/sx1278.zig"); 40 | }; 41 | 42 | pub const base = struct { 43 | pub const DatagramDevice = @import("base/DatagramDevice.zig"); 44 | pub const StreamDevice = @import("base/StreamDevice.zig"); 45 | pub const DigitalIO = @import("base/DigitalIO.zig"); 46 | }; 47 | 48 | test { 49 | _ = display.ssd1306; 50 | _ = display.ili9488; 51 | _ = display.st77xx; 52 | 53 | _ = input.keyboard_matrix; 54 | _ = input.debounced_button; 55 | _ = input.rotary_encoder; 56 | 57 | _ = input.touch.xpt2046; 58 | 59 | _ = wireless.sx1278; 60 | 61 | _ = base.DatagramDevice; 62 | _ = base.StreamDevice; 63 | _ = base.DigitalIO; 64 | } 65 | -------------------------------------------------------------------------------- /driver/input/rotary-encoder.zig: -------------------------------------------------------------------------------- 1 | //! 2 | //! Implements a bounce-free decoder for rotary encoders 3 | //! 4 | 5 | const std = @import("std"); 6 | const mdf = @import("../framework.zig"); 7 | 8 | pub const Event = enum(i2) { 9 | /// No change since the last decoding happened 10 | idle = 0, 11 | /// The quadrature signal incremented a step. 12 | increment = 1, 13 | /// The quadrature signal decremented a step. 14 | decrement = -1, 15 | /// The quadrature signal skipped a sequence point and entered a invalid state. 16 | @"error" = -2, 17 | }; 18 | 19 | pub const RotaryEncoder = RotaryEncoder_Generic(mdf.base.DigitalIO); 20 | 21 | pub fn RotaryEncoder_Generic(comptime DigitalIO: type) type { 22 | return struct { 23 | const Encoder = @This(); 24 | 25 | a: DigitalIO, 26 | b: DigitalIO, 27 | 28 | last_a: mdf.base.DigitalIO.State, 29 | last_b: mdf.base.DigitalIO.State, 30 | 31 | pub fn init( 32 | a: DigitalIO, 33 | b: DigitalIO, 34 | idle_state: mdf.base.DigitalIO.State, 35 | ) !Encoder { 36 | try a.set_direction(.input); 37 | try a.set_direction(.input); 38 | try a.set_bias(idle_state); 39 | return Encoder{ 40 | .a = a, 41 | .b = b, 42 | .last_a = try a.read(), 43 | .last_b = try b.read(), 44 | }; 45 | } 46 | 47 | pub fn poll(enc: *Encoder) !Event { 48 | var a = try enc.a.read(); 49 | var b = try enc.b.read(); 50 | defer enc.last_a = a; 51 | defer enc.last_b = b; 52 | 53 | const enable = a.value() ^ b.value() ^ enc.last_a.value() ^ enc.last_b.value(); 54 | const direction = a.value() ^ enc.last_b.value(); 55 | 56 | if (enable != 0) { 57 | if (direction != 0) { 58 | return .increment; 59 | } else { 60 | return .decrement; 61 | } 62 | } else { 63 | return .idle; 64 | } 65 | } 66 | }; 67 | } 68 | 69 | test RotaryEncoder { 70 | var a = mdf.base.DigitalIO.TestDevice.init(.output, .high); 71 | var b = mdf.base.DigitalIO.TestDevice.init(.output, .high); 72 | 73 | var encoder = try RotaryEncoder.init(a.digital_io(), b.digital_io(), .high); 74 | 75 | _ = try encoder.poll(); 76 | } 77 | -------------------------------------------------------------------------------- /driver/base/StreamDevice.zig: -------------------------------------------------------------------------------- 1 | //! 2 | //! An abstract stream orientied device with runtime dispatch. 3 | //! 4 | //! Stream devices behave similar to an UART and can send/receive data 5 | //! in variying lengths without clear boundaries between transmissions. 6 | //! 7 | 8 | const std = @import("std"); 9 | 10 | const StreamDevice = @This(); 11 | 12 | object: ?*anyopaque, 13 | vtable: *const VTable, 14 | 15 | const BaseError = error{ IoError, Timeout }; 16 | 17 | pub const ConnectError = BaseError || error{DeviceBusy}; 18 | pub const WriteError = BaseError || error{ Unsupported, NotConnected }; 19 | pub const ReadError = BaseError || error{ Unsupported, NotConnected }; 20 | 21 | /// Establishes a connection to the device (like activating a chip-select lane or similar). 22 | /// NOTE: Call `.disconnect()` when the usage of the device is done to release it. 23 | pub fn connect(sd: StreamDevice) ConnectError!void { 24 | if (sd.vtable.connectFn) |connectFn| { 25 | return connectFn(sd.object); 26 | } 27 | } 28 | 29 | /// Releases a device from the connection. 30 | pub fn disconnect(sd: StreamDevice) void { 31 | if (sd.vtable.disconnectFn) |disconnectFn| { 32 | return disconnectFn(sd.object); 33 | } 34 | } 35 | 36 | /// Writes some `bytes` to the device and returns the number of bytes written. 37 | pub fn write(sd: StreamDevice, bytes: []const u8) WriteError!usize { 38 | if (sd.vtable.writeFn) |writeFn| { 39 | return writeFn(sd.object, bytes); 40 | } else { 41 | return error.Unsupported; 42 | } 43 | } 44 | 45 | /// Reads some `bytes` to the device and returns the number of bytes read. 46 | pub fn read(sd: StreamDevice, bytes: []u8) ReadError!usize { 47 | if (sd.vtable.readFn) |readFn| { 48 | return readFn(sd.object, bytes); 49 | } else { 50 | return error.Unsupported; 51 | } 52 | } 53 | 54 | pub const Reader = std.io.Reader(StreamDevice, ReadError, reader_read); 55 | pub fn reader(sd: StreamDevice) Reader { 56 | return .{ .context = sd }; 57 | } 58 | 59 | fn reader_read(sd: StreamDevice, buf: []u8) ReadError!usize { 60 | return sd.read(buf); 61 | } 62 | 63 | pub const Writer = std.io.Reader(StreamDevice, WriteError, writer_write); 64 | pub fn writer(sd: StreamDevice) Writer { 65 | return .{ .context = sd }; 66 | } 67 | 68 | fn writer_write(sd: StreamDevice, buf: []const u8) WriteError!usize { 69 | return sd.write(buf); 70 | } 71 | 72 | pub const VTable = struct { 73 | connect_fn: ?*const fn (?*anyopaque) ConnectError!void, 74 | disconnect_fn: ?*const fn (?*anyopaque) void, 75 | write_fn: ?*const fn (?*anyopaque, datagram: []const u8) WriteError!usize, 76 | read_fn: ?*const fn (?*anyopaque, datagram: []u8) ReadError!usize, 77 | }; 78 | -------------------------------------------------------------------------------- /driver/input/debounced-button.zig: -------------------------------------------------------------------------------- 1 | //! 2 | //! Implements a simple polling button driver that uses a debouncing technique. 3 | //! 4 | 5 | const std = @import("std"); 6 | const mdf = @import("../framework.zig"); 7 | 8 | pub const Event = enum { 9 | /// Nothing has changed. 10 | idle, 11 | 12 | /// The button was pressed. Will only trigger once per press. 13 | /// Use `Button.isPressed()` to check if the button is currently held. 14 | pressed, 15 | 16 | /// The button was released. Will only trigger once per release. 17 | /// Use `Button.isPressed()` to check if the button is currently held. 18 | released, 19 | }; 20 | 21 | pub fn DebouncedButton( 22 | /// The active state for the button. Use `.high` for active-high, `.low` for active-low. 23 | comptime active_state: mdf.DigitalIO.State, 24 | /// Optional filter depth for debouncing. If `null` is passed, 16 samples are used to debounce the button, 25 | /// otherwise the given number of samples is used. 26 | comptime filter_depth: ?comptime_int, 27 | ) type { 28 | return DebouncedButton_Generic(mdf.base.DigitalIO, active_state, filter_depth); 29 | } 30 | 31 | pub fn DebouncedButton_Generic( 32 | /// The GPIO pin the button is connected to. Will be initialized when calling Button.init 33 | comptime DigitalIO: type, 34 | /// The active state for the button. Use `.high` for active-high, `.low` for active-low. 35 | comptime active_state: mdf.base.DigitalIO.State, 36 | /// Optional filter depth for debouncing. If `null` is passed, 16 samples are used to debounce the button, 37 | /// otherwise the given number of samples is used. 38 | comptime filter_depth: ?comptime_int, 39 | ) type { 40 | return struct { 41 | const Button = @This(); 42 | const DebounceFilter = std.meta.Int(.unsigned, filter_depth orelse 16); 43 | 44 | io: DigitalIO, 45 | debounce: DebounceFilter, 46 | state: mdf.base.DigitalIO.State, 47 | 48 | pub fn init(io: DigitalIO) !Button { 49 | try io.set_bias(active_state.invert()); 50 | try io.set_direction(.input); 51 | return Button{ 52 | .io = io, 53 | .debounce = 0, 54 | .state = try io.read(), 55 | }; 56 | } 57 | 58 | /// Polls for the button state. Returns the change event for the button if any. 59 | pub fn poll(self: *Button) !Event { 60 | const state = try self.io.read(); 61 | const active_unfiltered = (state == active_state); 62 | 63 | const previous_debounce = self.debounce; 64 | self.debounce <<= 1; 65 | if (active_unfiltered) { 66 | self.debounce |= 1; 67 | } 68 | 69 | if (active_unfiltered and previous_debounce == 0) { 70 | return .pressed; 71 | } else if (!active_unfiltered and self.debounce == 0 and previous_debounce != 0) { 72 | return .released; 73 | } else { 74 | return .idle; 75 | } 76 | } 77 | 78 | /// Returns `true` when the button is pressed. 79 | /// Will only be updated when `poll` is regularly called. 80 | pub fn is_pressed(self: *Button) bool { 81 | return (self.debounce != 0); 82 | } 83 | }; 84 | } 85 | -------------------------------------------------------------------------------- /driver/base/DigitalIO.zig: -------------------------------------------------------------------------------- 1 | //! 2 | //! An abstract digital input/output pin. 3 | //! 4 | //! Digital I/Os can be used to drive single-wire data 5 | //! 6 | 7 | const std = @import("std"); 8 | 9 | const DigitalIO = @This(); 10 | const BaseError = error{ IoError, Timeout }; 11 | 12 | object: ?*anyopaque, 13 | vtable: *const VTable, 14 | 15 | pub const SetDirError = error{Unsupported}; 16 | pub const SetBiasError = error{Unsupported}; 17 | pub const WriteError = error{Unsupported}; 18 | pub const ReadError = error{Unsupported}; 19 | 20 | pub const State = enum(u1) { 21 | low = 0, 22 | high = 1, 23 | 24 | pub inline fn invert(state: State) State { 25 | return @as(State, @enumFromInt(~@intFromEnum(state))); 26 | } 27 | 28 | pub inline fn value(state: State) u1 { 29 | return @intFromEnum(state); 30 | } 31 | }; 32 | pub const Direction = enum { input, output }; 33 | 34 | /// Sets the direction of the pin. 35 | pub fn set_direction(dio: DigitalIO, dir: Direction) SetDirError!void { 36 | return dio.vtable.set_direction_fn(dio.object, dir); 37 | } 38 | 39 | /// Sets if the pin has a bias towards either `low` or `high` or no bias at all. 40 | /// Bias is usually implemented with pull-ups and pull-downs. 41 | pub fn set_bias(dio: DigitalIO, bias: ?State) SetBiasError!void { 42 | return dio.vtable.set_bias_fn(dio.object, bias); 43 | } 44 | 45 | /// Changes the state of the pin. 46 | pub fn write(dio: DigitalIO, state: State) WriteError!void { 47 | return dio.vtable.write_fn(dio.object, state); 48 | } 49 | 50 | /// Reads the state state of the pin. 51 | pub fn read(dio: DigitalIO) ReadError!State { 52 | return dio.vtable.read_fn(dio.object); 53 | } 54 | 55 | pub const VTable = struct { 56 | set_direction_fn: *const fn (?*anyopaque, dir: Direction) SetDirError!void, 57 | set_bias_fn: *const fn (?*anyopaque, bias: ?State) SetBiasError!void, 58 | write_fn: *const fn (?*anyopaque, state: State) WriteError!void, 59 | read_fn: *const fn (?*anyopaque) State, 60 | }; 61 | 62 | pub const TestDevice = struct { 63 | state: State, 64 | dir: Direction, 65 | 66 | pub fn init(initial_dir: Direction, initial_state: State) TestDevice { 67 | return TestDevice{ 68 | .dir = initial_dir, 69 | .state = initial_state, 70 | }; 71 | } 72 | 73 | pub fn digital_io(dev: *TestDevice) DigitalIO { 74 | return DigitalIO{ 75 | .object = dev, 76 | .vtable = &vtable, 77 | }; 78 | } 79 | 80 | fn set_direction_fn(ctx: ?*anyopaque, dir: Direction) SetDirError!void { 81 | const dev: *TestDevice = @ptrCast(@alignCast(ctx.?)); 82 | dev.dir = dir; 83 | } 84 | 85 | fn set_bias_fn(ctx: ?*anyopaque, bias: ?State) SetBiasError!void { 86 | const dev: *TestDevice = @ptrCast(@alignCast(ctx.?)); 87 | _ = dev; 88 | _ = bias; 89 | } 90 | 91 | fn write_fn(ctx: ?*anyopaque, state: State) WriteError!void { 92 | const dev: *TestDevice = @ptrCast(@alignCast(ctx.?)); 93 | if (dev.dir != .output) 94 | return error.Unsupported; 95 | dev.state = state; 96 | } 97 | 98 | fn read_fn(ctx: ?*anyopaque) State { 99 | const dev: *TestDevice = @ptrCast(@alignCast(ctx.?)); 100 | return dev.state; 101 | } 102 | 103 | const vtable = VTable{ 104 | .set_direction_fn = set_direction_fn, 105 | .set_bias_fn = set_bias_fn, 106 | .write_fn = write_fn, 107 | .read_fn = read_fn, 108 | }; 109 | }; 110 | -------------------------------------------------------------------------------- /driver/input/keyboard-matrix.zig: -------------------------------------------------------------------------------- 1 | //! 2 | //! Implements a N*M keyboard matrix that will be scanned in column-major order. 3 | //! 4 | 5 | const std = @import("std"); 6 | const mdf = @import("../framework.zig"); 7 | 8 | /// A single key in a 2D keyboard matrix. 9 | pub const Key = enum(u16) { 10 | // we just assume we have enough encoding space and not more than 256 cols/rows 11 | _, 12 | 13 | pub fn new(r: u8, c: u8) Key { 14 | return @as(Key, @enumFromInt((@as(u16, r) << 8) + c)); 15 | } 16 | 17 | pub fn row(key: Key) u8 { 18 | return @as(u8, @truncate(@intFromEnum(key) >> 8)); 19 | } 20 | 21 | pub fn column(key: Key) u8 { 22 | return @as(u8, @truncate(@intFromEnum(key))); 23 | } 24 | }; 25 | 26 | /// Keyboard matrix implementation via GPIOs that scans columns and checks rows. 27 | /// 28 | /// Will use `cols` as matrix drivers (outputs) and `rows` as matrix readers (inputs). 29 | pub fn KeyboardMatrix(comptime col_count: usize, comptime row_count: usize) type { 30 | return KeyboardMatrix_Generic(mdf.base.DigitalIO, col_count, row_count); 31 | } 32 | 33 | pub fn KeyboardMatrix_Generic(comptime Pin: type, comptime col_count: usize, comptime row_count: usize) type { 34 | if (col_count > 256 or row_count > 256) @compileError("cannot encode more than 256 rows or columns!"); 35 | return struct { 36 | const Matrix = @This(); 37 | 38 | /// Number of keys in this matrix. 39 | pub const key_count = col_count * row_count; 40 | 41 | /// Returns the index for the given key. Assumes that `key` is valid. 42 | pub fn index(key: Key) usize { 43 | return key.column() * row_count + key.row(); 44 | } 45 | 46 | /// A set that can store if each key is set or not. 47 | pub const Set = struct { 48 | pressed: std.StaticBitSet(key_count) = std.StaticBitSet(key_count).initEmpty(), 49 | 50 | /// Adds a key to the set. 51 | pub fn add(set: *Set, key: Key) void { 52 | set.pressed.set(index(key)); 53 | } 54 | 55 | /// Checks if a key is pressed. 56 | pub fn isPressed(set: Set, key: Key) bool { 57 | return set.pressed.isSet(index(key)); 58 | } 59 | 60 | /// Returns if any key is pressed. 61 | pub fn any(set: Set) bool { 62 | return (set.pressed.count() > 0); 63 | } 64 | }; 65 | 66 | cols: [col_count]Pin, 67 | rows: [row_count]Pin, 68 | 69 | /// Initializes all GPIOs of the matrix and returns a new instance. 70 | pub fn init( 71 | cols: [col_count]Pin, 72 | rows: [row_count]Pin, 73 | ) !Matrix { 74 | var mat = Matrix{ 75 | .cols = cols, 76 | .rows = rows, 77 | }; 78 | for (cols) |c| { 79 | try c.set_direction(.output); 80 | } 81 | for (rows) |r| { 82 | try r.set_direction(.input); 83 | try r.set_bias(.high); 84 | } 85 | try mat.set_all_to(.high); 86 | return mat; 87 | } 88 | 89 | /// Scans the matrix and returns a set of all pressed keys. 90 | pub fn scan(matrix: Matrix) !Set { 91 | var result = Set{}; 92 | 93 | try matrix.set_all_to(.high); 94 | 95 | for (matrix.cols, 0..) |c_pin, c_index| { 96 | try c_pin.write(.low); 97 | busyloop(10); 98 | 99 | for (matrix.rows, 0..) |r_pin, r_index| { 100 | const state = try r_pin.read(); 101 | if (state == .low) { 102 | // someone actually pressed a key! 103 | result.add(Key.new( 104 | @as(u8, @truncate(r_index)), 105 | @as(u8, @truncate(c_index)), 106 | )); 107 | } 108 | } 109 | 110 | try c_pin.write(.high); 111 | busyloop(100); 112 | } 113 | 114 | try matrix.set_all_to(.high); 115 | 116 | return result; 117 | } 118 | 119 | fn set_all_to(matrix: Matrix, value: mdf.base.DigitalIO.State) !void { 120 | for (matrix.cols) |c| { 121 | try c.write(value); 122 | } 123 | } 124 | }; 125 | } 126 | 127 | inline fn busyloop(comptime N: comptime_int) void { 128 | for (0..N) |_| { 129 | // wait some cycles so the physics does its magic and convey 130 | // the electrons 131 | asm volatile ("" ::: "memory"); 132 | } 133 | } 134 | 135 | test KeyboardMatrix { 136 | var matrix_pins = [_]mdf.base.DigitalIO.TestDevice{ 137 | mdf.base.DigitalIO.TestDevice.init(.output, .high), 138 | mdf.base.DigitalIO.TestDevice.init(.output, .high), 139 | mdf.base.DigitalIO.TestDevice.init(.output, .high), 140 | mdf.base.DigitalIO.TestDevice.init(.output, .high), 141 | }; 142 | const rows = [_]mdf.base.DigitalIO{ 143 | matrix_pins[0].digital_io(), 144 | matrix_pins[1].digital_io(), 145 | }; 146 | const cols = [_]mdf.base.DigitalIO{ 147 | matrix_pins[2].digital_io(), 148 | matrix_pins[3].digital_io(), 149 | }; 150 | 151 | var matrix = try KeyboardMatrix(2, 2).init(cols, rows); 152 | 153 | const set = try matrix.scan(); 154 | _ = set; 155 | } 156 | -------------------------------------------------------------------------------- /driver/base/DatagramDevice.zig: -------------------------------------------------------------------------------- 1 | //! 2 | //! An abstract datagram orientied device with runtime dispatch. 3 | //! 4 | //! Datagram devices behave similar to an SPI or Ethernet device where 5 | //! packets with an ahead-of-time known length can be transferred in a 6 | //! single transaction. 7 | //! 8 | 9 | const std = @import("std"); 10 | 11 | const DatagramDevice = @This(); 12 | 13 | const BaseError = error{ IoError, Timeout }; 14 | 15 | pub const ConnectError = BaseError || error{DeviceBusy}; 16 | 17 | /// Establishes a connection to the device (like activating a chip-select lane or similar). 18 | /// NOTE: Call `.disconnect()` when the usage of the device is done to release it. 19 | pub fn connect(dd: DatagramDevice) ConnectError!void { 20 | if (dd.vtable.connectFn) |connectFn| { 21 | return connectFn(dd.object); 22 | } 23 | } 24 | 25 | /// Releases a device from the connection. 26 | pub fn disconnect(dd: DatagramDevice) void { 27 | if (dd.vtable.disconnectFn) |disconnectFn| { 28 | return disconnectFn(dd.object); 29 | } 30 | } 31 | 32 | pub const WriteError = BaseError || error{ Unsupported, NotConnected }; 33 | 34 | /// Writes a single `datagram` to the device. 35 | pub fn write(dd: DatagramDevice, datagram: []const u8) WriteError!void { 36 | if (dd.vtable.writeFn) |writeFn| { 37 | return writeFn(dd.object, datagram); 38 | } else { 39 | return error.Unsupported; 40 | } 41 | } 42 | 43 | pub const ReadError = BaseError || error{ Unsupported, NotConnected }; 44 | 45 | /// Reads a single `datagram` from the device. 46 | pub fn read(dd: DatagramDevice, datagram: []u8) ReadError!void { 47 | if (dd.vtable.readFn) |readFn| { 48 | return readFn(dd.object, datagram); 49 | } else { 50 | return error.Unsupported; 51 | } 52 | } 53 | 54 | object: ?*anyopaque, 55 | vtable: *const VTable, 56 | 57 | pub const VTable = struct { 58 | connectFn: ?*const fn (?*anyopaque) ConnectError!void, 59 | disconnectFn: ?*const fn (?*anyopaque) void, 60 | writeFn: ?*const fn (?*anyopaque, datagram: []const u8) WriteError!void, 61 | readFn: ?*const fn (?*anyopaque, datagram: []u8) ReadError!void, 62 | }; 63 | 64 | /// A device implementation that can be used to write unit tests. 65 | pub const TestDevice = struct { 66 | arena: std.heap.ArenaAllocator, 67 | packets: std.ArrayList([]u8), 68 | 69 | input_sequence: ?[]const []const u8, 70 | input_sequence_pos: usize, 71 | 72 | write_enabled: bool, 73 | 74 | connected: bool, 75 | 76 | pub fn init_receiver_only() TestDevice { 77 | return init(null, true); 78 | } 79 | 80 | pub fn init_sender_only(input: []const []const u8) TestDevice { 81 | return init(input, false); 82 | } 83 | 84 | pub fn init(input: ?[]const []const u8, write_enabled: bool) TestDevice { 85 | return TestDevice{ 86 | .arena = std.heap.ArenaAllocator.init(std.testing.allocator), 87 | .packets = std.ArrayList([]u8).init(std.testing.allocator), 88 | 89 | .input_sequence = input, 90 | .input_sequence_pos = 0, 91 | 92 | .write_enabled = write_enabled, 93 | 94 | .connected = false, 95 | }; 96 | } 97 | 98 | pub fn deinit(td: *TestDevice) void { 99 | td.arena.deinit(); 100 | td.packets.deinit(); 101 | td.* = undefined; 102 | } 103 | 104 | pub fn expect_sent(td: TestDevice, expected_datagrams: []const []const u8) !void { 105 | const actual_datagrams = td.packets.items; 106 | 107 | try std.testing.expectEqual(expected_datagrams.len, actual_datagrams.len); 108 | for (expected_datagrams, actual_datagrams) |expected, actual| { 109 | try std.testing.expectEqualSlices(u8, expected, actual); 110 | } 111 | } 112 | 113 | pub fn datagram_device(td: *TestDevice) DatagramDevice { 114 | return DatagramDevice{ 115 | .object = td, 116 | .vtable = &vtable, 117 | }; 118 | } 119 | 120 | fn connectFn(ctx: ?*anyopaque) ConnectError!void { 121 | const td: *TestDevice = @ptrCast(@alignCast(ctx.?)); 122 | if (td.connected) 123 | return error.DeviceBusy; 124 | td.connected = true; 125 | } 126 | 127 | fn disconnectFn(ctx: ?*anyopaque) void { 128 | const td: *TestDevice = @ptrCast(@alignCast(ctx.?)); 129 | if (!td.connected) { 130 | std.log.err("disconnect when test device was not connected!", .{}); 131 | } 132 | td.connected = false; 133 | } 134 | 135 | fn writeFn(ctx: ?*anyopaque, datagram: []const u8) WriteError!void { 136 | const td: *TestDevice = @ptrCast(@alignCast(ctx.?)); 137 | 138 | if (!td.connected) { 139 | return error.NotConnected; 140 | } 141 | 142 | if (!td.write_enabled) { 143 | return error.Unsupported; 144 | } 145 | 146 | const dg = td.arena.allocator().dupe(u8, datagram) catch return error.IoError; 147 | errdefer td.arena.allocator().free(dg); 148 | 149 | td.packets.append(dg) catch return error.IoError; 150 | } 151 | 152 | fn readFn(ctx: ?*anyopaque, datagram: []u8) ReadError!void { 153 | const td: *TestDevice = @ptrCast(@alignCast(ctx.?)); 154 | 155 | if (!td.connected) { 156 | return error.NotConnected; 157 | } 158 | 159 | const inputs = td.input_sequence orelse return error.Unsupported; 160 | 161 | if (td.input_sequence_pos >= inputs.len) { 162 | return error.IoError; 163 | } 164 | 165 | const packet = inputs[td.input_sequence_pos]; 166 | td.input_sequence_pos += 1; 167 | 168 | if (packet.len != datagram.len) 169 | return error.IoError; 170 | 171 | @memcpy(datagram, packet); 172 | } 173 | 174 | const vtable = VTable{ 175 | .connectFn = connectFn, 176 | .disconnectFn = disconnectFn, 177 | .writeFn = writeFn, 178 | .readFn = readFn, 179 | }; 180 | }; 181 | -------------------------------------------------------------------------------- /driver/display/st77xx.zig: -------------------------------------------------------------------------------- 1 | //! 2 | //! Driver for the ST7735 and ST7789 for the 4-line serial protocol or 8-bit parallel interface 3 | //! 4 | //! This driver is a port of https://github.com/adafruit/Adafruit-ST7735-Library 5 | //! 6 | //! Datasheets: 7 | //! - https://www.displayfuture.com/Display/datasheet/controller/ST7735.pdf 8 | //! - https://www.waveshare.com/w/upload/e/e2/ST7735S_V1.1_20111121.pdf 9 | //! - https://www.waveshare.com/w/upload/a/ae/ST7789_Datasheet.pdf 10 | //! 11 | const std = @import("std"); 12 | const mdf = @import("../framework.zig"); 13 | const DigitalIO = mdf.base.DigitalIO; 14 | const Color = mdf.display.RGB565; 15 | 16 | pub const Device = enum { 17 | st7735, 18 | st7789, 19 | }; 20 | 21 | pub const Resolution = struct { 22 | width: u16, 23 | height: u16, 24 | }; 25 | 26 | pub const Rotation = enum(u2) { 27 | normal, 28 | left90, 29 | right90, 30 | upside_down, 31 | }; 32 | 33 | pub const ST7735 = ST77xx_Generic(mdf.base.DatagramDevice, .st7735); 34 | pub fn ST7735_Generic(comptime DatagramDevice: type) type { 35 | return ST77xx_Generic(DatagramDevice, .st7735); 36 | } 37 | 38 | pub const ST7789 = ST77xx_Generic(mdf.base.DatagramDevice, .st7789); 39 | pub fn ST7789_Generic(comptime DatagramDevice: type) type { 40 | return ST77xx_Generic(DatagramDevice, .st7789); 41 | } 42 | 43 | pub fn ST77xx_Generic(comptime DatagramDevice: type, comptime device: Device) type { 44 | return struct { 45 | const Driver = @This(); 46 | 47 | const dev = switch (device) { 48 | .st7735 => ST7735_Device, 49 | .st7789 => ST7789_Device, 50 | }; 51 | 52 | dd: DatagramDevice, 53 | dev_rst: DigitalIO, 54 | dev_datcmd: DigitalIO, 55 | 56 | resolution: Resolution, 57 | 58 | pub fn init( 59 | channel: DatagramDevice, 60 | rst: DigitalIO, 61 | data_cmd: DigitalIO, 62 | resolution: Resolution, 63 | ) !Driver { 64 | const dri = Driver{ 65 | .dd = channel, 66 | .dev_rst = rst, 67 | .dev_datcmd = data_cmd, 68 | 69 | .resolution = resolution, 70 | }; 71 | 72 | // initSPI(freq, spiMode); 73 | 74 | // Run init sequence 75 | // uint8_t numCommands, cmd, numArgs; 76 | // uint16_t ms; 77 | 78 | // numCommands = pgm_read_byte(addr++); // Number of commands to follow 79 | // while (numCommands--) { // For each command... 80 | // cmd = pgm_read_byte(addr++); // Read command 81 | // numArgs = pgm_read_byte(addr++); // Number of args to follow 82 | // ms = numArgs & ST_CMD_DELAY; // If hibit set, delay follows args 83 | // numArgs &= ~ST_CMD_DELAY; // Mask out delay bit 84 | // sendCommand(cmd, addr, numArgs); 85 | // addr += numArgs; 86 | 87 | // if (ms) { 88 | // ms = pgm_read_byte(addr++); // Read post-command delay time (ms) 89 | // if (ms == 255) 90 | // ms = 500; // If 255, delay for 500 ms 91 | // delay(ms); 92 | // } 93 | // } 94 | 95 | try dri.set_spi_mode(.data); 96 | 97 | return dri; 98 | } 99 | 100 | pub fn set_address_window(dri: Driver, x: u16, y: u16, w: u16, h: u16) !void { 101 | // x += _xstart; 102 | // y += _ystart; 103 | 104 | const xa = (@as(u32, x) << 16) | (x + w - 1); 105 | const ya = (@as(u32, y) << 16) | (y + h - 1); 106 | 107 | try dri.write_command(.caset, std.mem.asBytes(&xa)); // Column addr set 108 | try dri.write_command(.raset, std.mem.asBytes(&ya)); // Row addr set 109 | try dri.write_command(.ramwr, &.{}); // write to RAM 110 | } 111 | 112 | pub fn set_rotation(dri: Driver, rotation: Rotation) !void { 113 | var control_byte: u8 = madctl_rgb; 114 | 115 | switch (rotation) { 116 | .normal => { 117 | control_byte = (madctl_mx | madctl_my | madctl_rgb); 118 | // _xstart = _colstart; 119 | // _ystart = _rowstart; 120 | }, 121 | .right90 => { 122 | control_byte = (madctl_my | madctl_mv | madctl_rgb); 123 | // _ystart = _colstart; 124 | // _xstart = _rowstart; 125 | }, 126 | .upside_down => { 127 | control_byte = (madctl_rgb); 128 | // _xstart = _colstart; 129 | // _ystart = _rowstart; 130 | }, 131 | .left90 => { 132 | control_byte = (madctl_mx | madctl_mv | madctl_rgb); 133 | // _ystart = _colstart; 134 | // _xstart = _rowstart; 135 | }, 136 | } 137 | 138 | try dri.write_command(.madctl, &.{control_byte}); 139 | } 140 | 141 | pub fn enable_display(dri: Driver, enable: bool) !void { 142 | try dri.write_command(if (enable) .dispon else .dispoff, &.{}); 143 | } 144 | 145 | pub fn enable_tearing(dri: Driver, enable: bool) !void { 146 | try dri.write_command(if (enable) .teon else .teoff, &.{}); 147 | } 148 | 149 | pub fn enable_sleep(dri: Driver, enable: bool) !void { 150 | try dri.write_command(if (enable) .slpin else .slpout, &.{}); 151 | } 152 | 153 | pub fn invert_display(dri: Driver, inverted: bool) !void { 154 | try dri.write_command(if (inverted) .invon else .invoff, &.{}); 155 | } 156 | 157 | pub fn set_pixel(dri: Driver, x: u16, y: u16, color: Color) !void { 158 | if (x >= dri.resolution.width or y >= dri.resolution.height) { 159 | return; 160 | } 161 | try dri.set_address_window(x, y, 1, 1); 162 | try dri.write_data(&.{color}); 163 | } 164 | 165 | fn write_command(dri: Driver, cmd: Command, params: []const u8) !void { 166 | try dri.dd.connect(); 167 | defer dri.dd.disconnect(); 168 | 169 | try dri.set_spi_mode(.command); 170 | try dri.dd.write(&[_]u8{@intFromEnum(cmd)}); 171 | try dri.set_spi_mode(.data); 172 | try dri.dd.write(params); 173 | } 174 | 175 | fn write_data(dri: Driver, data: []const Color) !void { 176 | try dri.dd.connect(); 177 | defer dri.dd.disconnect(); 178 | 179 | try dri.dd.write(std.mem.sliceAsBytes(data)); 180 | } 181 | 182 | fn set_spi_mode(dri: Driver, mode: enum { data, command }) !void { 183 | try dri.dev_datcmd.write(switch (mode) { 184 | .command => .low, 185 | .data => .high, 186 | }); 187 | } 188 | 189 | const cmd_delay = 0x80; // special signifier for command lists 190 | 191 | const Command = enum(u8) { 192 | nop = 0x00, 193 | swreset = 0x01, 194 | rddid = 0x04, 195 | rddst = 0x09, 196 | 197 | slpin = 0x10, 198 | slpout = 0x11, 199 | ptlon = 0x12, 200 | noron = 0x13, 201 | 202 | invoff = 0x20, 203 | invon = 0x21, 204 | dispoff = 0x28, 205 | dispon = 0x29, 206 | caset = 0x2A, 207 | raset = 0x2B, 208 | ramwr = 0x2C, 209 | ramrd = 0x2E, 210 | 211 | ptlar = 0x30, 212 | teoff = 0x34, 213 | teon = 0x35, 214 | madctl = 0x36, 215 | colmod = 0x3A, 216 | 217 | rdid1 = 0xDA, 218 | rdid2 = 0xDB, 219 | rdid3 = 0xDC, 220 | rdid4 = 0xDD, 221 | }; 222 | 223 | const madctl_my = 0x80; 224 | const madctl_mx = 0x40; 225 | const madctl_mv = 0x20; 226 | const madctl_ml = 0x10; 227 | const madctl_rgb = 0x00; 228 | 229 | const ST7735_Device = struct { 230 | // some flags for initR() :( 231 | const INITR_GREENTAB = 0x00; 232 | const INITR_REDTAB = 0x01; 233 | const INITR_BLACKTAB = 0x02; 234 | const INITR_18GREENTAB = INITR_GREENTAB; 235 | const INITR_18REDTAB = INITR_REDTAB; 236 | const INITR_18BLACKTAB = INITR_BLACKTAB; 237 | const INITR_144GREENTAB = 0x01; 238 | const INITR_MINI160x80 = 0x04; 239 | const INITR_HALLOWING = 0x05; 240 | const INITR_MINI160x80_PLUGIN = 0x06; 241 | 242 | // Some register settings 243 | const ST7735_MADCTL_BGR = 0x08; 244 | const ST7735_MADCTL_MH = 0x04; 245 | 246 | const ST7735_FRMCTR1 = 0xB1; 247 | const ST7735_FRMCTR2 = 0xB2; 248 | const ST7735_FRMCTR3 = 0xB3; 249 | const ST7735_INVCTR = 0xB4; 250 | const ST7735_DISSET5 = 0xB6; 251 | 252 | const ST7735_PWCTR1 = 0xC0; 253 | const ST7735_PWCTR2 = 0xC1; 254 | const ST7735_PWCTR3 = 0xC2; 255 | const ST7735_PWCTR4 = 0xC3; 256 | const ST7735_PWCTR5 = 0xC4; 257 | const ST7735_VMCTR1 = 0xC5; 258 | 259 | const ST7735_PWCTR6 = 0xFC; 260 | 261 | const ST7735_GMCTRP1 = 0xE0; 262 | const ST7735_GMCTRN1 = 0xE1; 263 | 264 | // static const uint8_t PROGMEM 265 | // Bcmd[] = { // Init commands for 7735B screens 266 | // 18, // 18 commands in list: 267 | // ST77XX_SWRESET, ST_CMD_DELAY, // 1: Software reset, no args, w/delay 268 | // 50, // 50 ms delay 269 | // ST77XX_SLPOUT, ST_CMD_DELAY, // 2: Out of sleep mode, no args, w/delay 270 | // 255, // 255 = max (500 ms) delay 271 | // ST77XX_COLMOD, 1+ST_CMD_DELAY, // 3: Set color mode, 1 arg + delay: 272 | // 0x05, // 16-bit color 273 | // 10, // 10 ms delay 274 | // ST7735_FRMCTR1, 3+ST_CMD_DELAY, // 4: Frame rate control, 3 args + delay: 275 | // 0x00, // fastest refresh 276 | // 0x06, // 6 lines front porch 277 | // 0x03, // 3 lines back porch 278 | // 10, // 10 ms delay 279 | // ST77XX_MADCTL, 1, // 5: Mem access ctl (directions), 1 arg: 280 | // 0x08, // Row/col addr, bottom-top refresh 281 | // ST7735_DISSET5, 2, // 6: Display settings #5, 2 args: 282 | // 0x15, // 1 clk cycle nonoverlap, 2 cycle gate 283 | // // rise, 3 cycle osc equalize 284 | // 0x02, // Fix on VTL 285 | // ST7735_INVCTR, 1, // 7: Display inversion control, 1 arg: 286 | // 0x0, // Line inversion 287 | // ST7735_PWCTR1, 2+ST_CMD_DELAY, // 8: Power control, 2 args + delay: 288 | // 0x02, // GVDD = 4.7V 289 | // 0x70, // 1.0uA 290 | // 10, // 10 ms delay 291 | // ST7735_PWCTR2, 1, // 9: Power control, 1 arg, no delay: 292 | // 0x05, // VGH = 14.7V, VGL = -7.35V 293 | // ST7735_PWCTR3, 2, // 10: Power control, 2 args, no delay: 294 | // 0x01, // Opamp current small 295 | // 0x02, // Boost frequency 296 | // ST7735_VMCTR1, 2+ST_CMD_DELAY, // 11: Power control, 2 args + delay: 297 | // 0x3C, // VCOMH = 4V 298 | // 0x38, // VCOML = -1.1V 299 | // 10, // 10 ms delay 300 | // ST7735_PWCTR6, 2, // 12: Power control, 2 args, no delay: 301 | // 0x11, 0x15, 302 | // ST7735_GMCTRP1,16, // 13: Gamma Adjustments (pos. polarity), 16 args + delay: 303 | // 0x09, 0x16, 0x09, 0x20, // (Not entirely necessary, but provides 304 | // 0x21, 0x1B, 0x13, 0x19, // accurate colors) 305 | // 0x17, 0x15, 0x1E, 0x2B, 306 | // 0x04, 0x05, 0x02, 0x0E, 307 | // ST7735_GMCTRN1,16+ST_CMD_DELAY, // 14: Gamma Adjustments (neg. polarity), 16 args + delay: 308 | // 0x0B, 0x14, 0x08, 0x1E, // (Not entirely necessary, but provides 309 | // 0x22, 0x1D, 0x18, 0x1E, // accurate colors) 310 | // 0x1B, 0x1A, 0x24, 0x2B, 311 | // 0x06, 0x06, 0x02, 0x0F, 312 | // 10, // 10 ms delay 313 | // ST77XX_CASET, 4, // 15: Column addr set, 4 args, no delay: 314 | // 0x00, 0x02, // XSTART = 2 315 | // 0x00, 0x81, // XEND = 129 316 | // ST77XX_RASET, 4, // 16: Row addr set, 4 args, no delay: 317 | // 0x00, 0x02, // XSTART = 1 318 | // 0x00, 0x81, // XEND = 160 319 | // ST77XX_NORON, ST_CMD_DELAY, // 17: Normal display on, no args, w/delay 320 | // 10, // 10 ms delay 321 | // ST77XX_DISPON, ST_CMD_DELAY, // 18: Main screen turn on, no args, delay 322 | // 255 }, // 255 = max (500 ms) delay 323 | 324 | // Rcmd1[] = { // 7735R init, part 1 (red or green tab) 325 | // 15, // 15 commands in list: 326 | // ST77XX_SWRESET, ST_CMD_DELAY, // 1: Software reset, 0 args, w/delay 327 | // 150, // 150 ms delay 328 | // ST77XX_SLPOUT, ST_CMD_DELAY, // 2: Out of sleep mode, 0 args, w/delay 329 | // 255, // 500 ms delay 330 | // ST7735_FRMCTR1, 3, // 3: Framerate ctrl - normal mode, 3 arg: 331 | // 0x01, 0x2C, 0x2D, // Rate = fosc/(1x2+40) * (LINE+2C+2D) 332 | // ST7735_FRMCTR2, 3, // 4: Framerate ctrl - idle mode, 3 args: 333 | // 0x01, 0x2C, 0x2D, // Rate = fosc/(1x2+40) * (LINE+2C+2D) 334 | // ST7735_FRMCTR3, 6, // 5: Framerate - partial mode, 6 args: 335 | // 0x01, 0x2C, 0x2D, // Dot inversion mode 336 | // 0x01, 0x2C, 0x2D, // Line inversion mode 337 | // ST7735_INVCTR, 1, // 6: Display inversion ctrl, 1 arg: 338 | // 0x07, // No inversion 339 | // ST7735_PWCTR1, 3, // 7: Power control, 3 args, no delay: 340 | // 0xA2, 341 | // 0x02, // -4.6V 342 | // 0x84, // AUTO mode 343 | // ST7735_PWCTR2, 1, // 8: Power control, 1 arg, no delay: 344 | // 0xC5, // VGH25=2.4C VGSEL=-10 VGH=3 * AVDD 345 | // ST7735_PWCTR3, 2, // 9: Power control, 2 args, no delay: 346 | // 0x0A, // Opamp current small 347 | // 0x00, // Boost frequency 348 | // ST7735_PWCTR4, 2, // 10: Power control, 2 args, no delay: 349 | // 0x8A, // BCLK/2, 350 | // 0x2A, // opamp current small & medium low 351 | // ST7735_PWCTR5, 2, // 11: Power control, 2 args, no delay: 352 | // 0x8A, 0xEE, 353 | // ST7735_VMCTR1, 1, // 12: Power control, 1 arg, no delay: 354 | // 0x0E, 355 | // ST77XX_INVOFF, 0, // 13: Don't invert display, no args 356 | // ST77XX_MADCTL, 1, // 14: Mem access ctl (directions), 1 arg: 357 | // 0xC8, // row/col addr, bottom-top refresh 358 | // ST77XX_COLMOD, 1, // 15: set color mode, 1 arg, no delay: 359 | // 0x05 }, // 16-bit color 360 | 361 | // Rcmd2green[] = { // 7735R init, part 2 (green tab only) 362 | // 2, // 2 commands in list: 363 | // ST77XX_CASET, 4, // 1: Column addr set, 4 args, no delay: 364 | // 0x00, 0x02, // XSTART = 0 365 | // 0x00, 0x7F+0x02, // XEND = 127 366 | // ST77XX_RASET, 4, // 2: Row addr set, 4 args, no delay: 367 | // 0x00, 0x01, // XSTART = 0 368 | // 0x00, 0x9F+0x01 }, // XEND = 159 369 | 370 | // Rcmd2red[] = { // 7735R init, part 2 (red tab only) 371 | // 2, // 2 commands in list: 372 | // ST77XX_CASET, 4, // 1: Column addr set, 4 args, no delay: 373 | // 0x00, 0x00, // XSTART = 0 374 | // 0x00, 0x7F, // XEND = 127 375 | // ST77XX_RASET, 4, // 2: Row addr set, 4 args, no delay: 376 | // 0x00, 0x00, // XSTART = 0 377 | // 0x00, 0x9F }, // XEND = 159 378 | 379 | // Rcmd2green144[] = { // 7735R init, part 2 (green 1.44 tab) 380 | // 2, // 2 commands in list: 381 | // ST77XX_CASET, 4, // 1: Column addr set, 4 args, no delay: 382 | // 0x00, 0x00, // XSTART = 0 383 | // 0x00, 0x7F, // XEND = 127 384 | // ST77XX_RASET, 4, // 2: Row addr set, 4 args, no delay: 385 | // 0x00, 0x00, // XSTART = 0 386 | // 0x00, 0x7F }, // XEND = 127 387 | 388 | // Rcmd2green160x80[] = { // 7735R init, part 2 (mini 160x80) 389 | // 2, // 2 commands in list: 390 | // ST77XX_CASET, 4, // 1: Column addr set, 4 args, no delay: 391 | // 0x00, 0x00, // XSTART = 0 392 | // 0x00, 0x4F, // XEND = 79 393 | // ST77XX_RASET, 4, // 2: Row addr set, 4 args, no delay: 394 | // 0x00, 0x00, // XSTART = 0 395 | // 0x00, 0x9F }, // XEND = 159 396 | 397 | // Rcmd2green160x80plugin[] = { // 7735R init, part 2 (mini 160x80 with plugin FPC) 398 | // 3, // 3 commands in list: 399 | // ST77XX_INVON, 0, // 1: Display is inverted 400 | // ST77XX_CASET, 4, // 2: Column addr set, 4 args, no delay: 401 | // 0x00, 0x00, // XSTART = 0 402 | // 0x00, 0x4F, // XEND = 79 403 | // ST77XX_RASET, 4, // 3: Row addr set, 4 args, no delay: 404 | // 0x00, 0x00, // XSTART = 0 405 | // 0x00, 0x9F }, // XEND = 159 406 | 407 | // Rcmd3[] = { // 7735R init, part 3 (red or green tab) 408 | // 4, // 4 commands in list: 409 | // ST7735_GMCTRP1, 16 , // 1: Gamma Adjustments (pos. polarity), 16 args + delay: 410 | // 0x02, 0x1c, 0x07, 0x12, // (Not entirely necessary, but provides 411 | // 0x37, 0x32, 0x29, 0x2d, // accurate colors) 412 | // 0x29, 0x25, 0x2B, 0x39, 413 | // 0x00, 0x01, 0x03, 0x10, 414 | // ST7735_GMCTRN1, 16 , // 2: Gamma Adjustments (neg. polarity), 16 args + delay: 415 | // 0x03, 0x1d, 0x07, 0x06, // (Not entirely necessary, but provides 416 | // 0x2E, 0x2C, 0x29, 0x2D, // accurate colors) 417 | // 0x2E, 0x2E, 0x37, 0x3F, 418 | // 0x00, 0x00, 0x02, 0x10, 419 | // ST77XX_NORON, ST_CMD_DELAY, // 3: Normal display on, no args, w/delay 420 | // 10, // 10 ms delay 421 | // ST77XX_DISPON, ST_CMD_DELAY, // 4: Main screen turn on, no args w/delay 422 | // 100 }; // 100 ms delay 423 | }; 424 | 425 | const ST7789_Device = struct { 426 | // static const uint8_t PROGMEM 427 | // generic_st7789[] = { // Init commands for 7789 screens 428 | // 9, // 9 commands in list: 429 | // ST77XX_SWRESET, ST_CMD_DELAY, // 1: Software reset, no args, w/delay 430 | // 150, // ~150 ms delay 431 | // ST77XX_SLPOUT , ST_CMD_DELAY, // 2: Out of sleep mode, no args, w/delay 432 | // 10, // 10 ms delay 433 | // ST77XX_COLMOD , 1+ST_CMD_DELAY, // 3: Set color mode, 1 arg + delay: 434 | // 0x55, // 16-bit color 435 | // 10, // 10 ms delay 436 | // ST77XX_MADCTL , 1, // 4: Mem access ctrl (directions), 1 arg: 437 | // 0x08, // Row/col addr, bottom-top refresh 438 | // ST77XX_CASET , 4, // 5: Column addr set, 4 args, no delay: 439 | // 0x00, 440 | // 0, // XSTART = 0 441 | // 0, 442 | // 240, // XEND = 240 443 | // ST77XX_RASET , 4, // 6: Row addr set, 4 args, no delay: 444 | // 0x00, 445 | // 0, // YSTART = 0 446 | // 320>>8, 447 | // 320&0xFF, // YEND = 320 448 | // ST77XX_INVON , ST_CMD_DELAY, // 7: hack 449 | // 10, 450 | // ST77XX_NORON , ST_CMD_DELAY, // 8: Normal display on, no args, w/delay 451 | // 10, // 10 ms delay 452 | // ST77XX_DISPON , ST_CMD_DELAY, // 9: Main screen turn on, no args, delay 453 | // 10 }; // 10 ms delay 454 | }; 455 | }; 456 | } 457 | 458 | test { 459 | _ = ST7735; 460 | _ = ST7789; 461 | } 462 | 463 | test { 464 | var channel = mdf.base.DatagramDevice.TestDevice.init_receiver_only(); 465 | defer channel.deinit(); 466 | 467 | var rst_pin = mdf.base.DigitalIO.TestDevice.init(.output, .high); 468 | var dat_pin = mdf.base.DigitalIO.TestDevice.init(.output, .high); 469 | 470 | var dri = try ST7735.init( 471 | channel.datagram_device(), 472 | rst_pin.digital_io(), 473 | dat_pin.digital_io(), 474 | .{ .width = 128, .height = 128 }, 475 | ); 476 | 477 | try dri.set_address_window(16, 32, 48, 64); 478 | try dri.set_rotation(.normal); 479 | try dri.enable_display(true); 480 | try dri.enable_tearing(false); 481 | try dri.enable_sleep(true); 482 | try dri.invert_display(false); 483 | try dri.set_pixel(11, 15, Color.blue); 484 | } 485 | -------------------------------------------------------------------------------- /driver/display/ssd1306.zig: -------------------------------------------------------------------------------- 1 | //! 2 | //! Generic driver for the SSD1306 display controller. 3 | //! 4 | //! This controller is usually found in small OLED displays. 5 | //! 6 | //! Datasheet: 7 | //! https://cdn-shop.adafruit.com/datasheets/SSD1306.pdf 8 | //! 9 | //! 10 | 11 | const std = @import("std"); 12 | const mdf = @import("../framework.zig"); 13 | 14 | pub const SSD1306_Options = struct { 15 | buffer_size: u32 = 64, 16 | i2c_prefix: bool, 17 | }; 18 | 19 | pub fn SSD1306(comptime options: SSD1306_Options) type { 20 | return SSD1306_Generic(mdf.base.DatagramDevice, options); 21 | } 22 | 23 | pub fn SSD1306_Generic(comptime DatagramDevice: type, comptime options: SSD1306_Options) type { 24 | return struct { 25 | const buffer_size = options.buffer_size; 26 | 27 | const white_line: [128]u8 = .{0xFF} ** 128; 28 | const black_line: [128]u8 = .{0x00} ** 128; 29 | 30 | const Self = @This(); 31 | dd: DatagramDevice, 32 | 33 | // TODO(philippwendel) Add doc comments for functions 34 | // 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 35 | pub fn init(dev: DatagramDevice) !Self { 36 | var self = Self{ .dd = dev }; 37 | 38 | try self.set_display(.off); 39 | 40 | try self.deactivate_scroll(); 41 | try self.set_segment_remap(true); // Flip left/right 42 | try self.set_com_ouput_scan_direction(true); // Flip up/down 43 | try self.set_normal_or_inverse_display(.normal); 44 | try self.set_contrast(255); 45 | 46 | try self.charge_pump_setting(true); 47 | try self.set_multiplex_ratio(63); // Default 48 | try self.set_display_clock_divide_ratio_and_oscillator_frequency(0, 8); 49 | try self.set_precharge_period(0b0001, 0b0001); 50 | try self.set_v_comh_deselect_level(0x4); 51 | try self.set_com_pins_hardware_configuration(0b001); // See page 40 in datasheet 52 | 53 | try self.set_display_offset(0); 54 | try self.set_display_start_line(0); 55 | try self.entire_display_on(.resumeToRam); 56 | 57 | try self.set_display(.on); 58 | 59 | return self; 60 | } 61 | 62 | pub fn write_full_display(self: Self, data: *const [128 * 8]u8) !void { 63 | try self.set_memory_addressing_mode(.horizontal); 64 | try self.set_column_address(0, 127); 65 | try self.set_page_address(0, 7); 66 | 67 | try self.write_gdram(data); 68 | } 69 | 70 | pub fn write_gdram(self: Self, data: []const u8) !void { 71 | try self.dd.connect(); 72 | defer self.dd.disconnect(); 73 | 74 | const use_safe_and_slow_impl = false; 75 | 76 | if (use_safe_and_slow_impl) { 77 | for (data) |byte| { 78 | try self.dd.write(&.{ ControlByte.data_byte, byte }); 79 | } 80 | } else { 81 | var buffer: [buffer_size]u8 = undefined; 82 | buffer[0x00] = ControlByte.data_stream; 83 | 84 | // std.log.info("start {} bytes", .{data.len}); 85 | 86 | var offset: usize = 0; 87 | 88 | while (offset < data.len) { 89 | const chunk_size: usize = @min(buffer.len - 1, data.len - offset); 90 | 91 | const chunk = data[offset..][0..chunk_size]; 92 | @memcpy(buffer[1..][0..chunk_size], chunk); 93 | 94 | // std.log.info("transfer {} bytes at offset {}", .{ chunk_size, offset }); 95 | const cmd_seq = buffer[0 .. chunk_size + 1]; 96 | // std.log.info("{}", .{std.fmt.fmtSliceHexLower(cmd_seq)}); 97 | try self.dd.write(cmd_seq); 98 | 99 | offset += chunk_size; 100 | } 101 | 102 | // std.log.info("end {} bytes", .{data.len}); 103 | } 104 | } 105 | 106 | pub fn clear_screen(self: Self, white: bool) !void { 107 | try self.set_memory_addressing_mode(.horizontal); 108 | try self.set_column_address(0, 127); 109 | try self.set_page_address(0, 7); 110 | { 111 | try self.dd.connect(); 112 | defer self.dd.disconnect(); 113 | 114 | const color: u8 = if (white) 0xFF else 0x00; 115 | 116 | for (0..128 * 8) |_| { 117 | try self.dd.write(&.{ ControlByte.data_byte, color }); 118 | } 119 | } 120 | try self.entire_display_on(.resumeToRam); 121 | try self.set_display(.on); 122 | } 123 | 124 | // Fundamental Commands 125 | pub fn set_contrast(self: Self, contrast: u8) !void { 126 | try self.dd.connect(); 127 | defer self.dd.disconnect(); 128 | 129 | try self.dd.write(&[_]u8{ ControlByte.command, 0x81, contrast }); 130 | } 131 | 132 | pub fn entire_display_on(self: Self, mode: DisplayOnMode) !void { 133 | try self.dd.connect(); 134 | defer self.dd.disconnect(); 135 | 136 | try self.dd.write(&[_]u8{ ControlByte.command, @intFromEnum(mode) }); 137 | } 138 | 139 | pub fn set_normal_or_inverse_display(self: Self, mode: NormalOrInverseDisplay) !void { 140 | try self.dd.connect(); 141 | defer self.dd.disconnect(); 142 | 143 | try self.dd.write(&[_]u8{ ControlByte.command, @intFromEnum(mode) }); 144 | } 145 | 146 | pub fn set_display(self: Self, mode: DisplayMode) !void { 147 | try self.dd.connect(); 148 | defer self.dd.disconnect(); 149 | 150 | try self.dd.write(&[_]u8{ ControlByte.command, @intFromEnum(mode) }); 151 | } 152 | 153 | // Scrolling Commands 154 | pub fn continuous_horizontal_scroll_setup(self: Self, direction: HorizontalScrollDirection, start_page: u3, end_page: u3, frame_frequency: u3) !void { 155 | if (end_page < start_page) return PageError.EndPageIsSmallerThanStartPage; 156 | 157 | try self.dd.connect(); 158 | defer self.dd.disconnect(); 159 | try self.dd.write(&[_]u8{ 160 | ControlByte.command, 161 | @intFromEnum(direction), 162 | 0x00, // Dummy byte 163 | @as(u8, start_page), 164 | @as(u8, frame_frequency), 165 | @as(u8, end_page), 166 | 0x00, // Dummy byte 167 | 0xFF, // Dummy byte 168 | }); 169 | } 170 | 171 | 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 { 172 | try self.dd.connect(); 173 | defer self.dd.disconnect(); 174 | try self.dd.write(&[_]u8{ 175 | ControlByte.command, 176 | @intFromEnum(direction), 177 | 0x00, // Dummy byte 178 | @as(u8, start_page), 179 | @as(u8, frame_frequency), 180 | @as(u8, end_page), 181 | @as(u8, vertical_scrolling_offset), 182 | }); 183 | } 184 | 185 | pub fn deactivate_scroll(self: Self) !void { 186 | try self.dd.connect(); 187 | defer self.dd.disconnect(); 188 | 189 | try self.dd.write(&[_]u8{ ControlByte.command, 0x2E }); 190 | } 191 | 192 | pub fn activate_scroll(self: Self) !void { 193 | try self.dd.connect(); 194 | defer self.dd.disconnect(); 195 | 196 | try self.dd.write(&[_]u8{ ControlByte.command, 0x2F }); 197 | } 198 | 199 | pub fn set_vertical_scroll_area(self: Self, start_row: u6, num_of_rows: u7) !void { 200 | try self.dd.connect(); 201 | defer self.dd.disconnect(); 202 | 203 | try self.dd.write(&[_]u8{ ControlByte.command, 0xA3, @as(u8, start_row), @as(u8, num_of_rows) }); 204 | } 205 | 206 | // Addressing Setting Commands 207 | pub fn set_column_start_address_for_page_addressing_mode(self: Self, column: Column, address: u4) !void { 208 | try self.dd.connect(); 209 | defer self.dd.disconnect(); 210 | 211 | try self.dd.write(&[_]u8{ ControlByte.command, (@as(u8, @intFromEnum(column)) << 4) | @as(u8, address) }); 212 | } 213 | 214 | pub fn set_memory_addressing_mode(self: Self, mode: MemoryAddressingMode) !void { 215 | try self.dd.connect(); 216 | defer self.dd.disconnect(); 217 | 218 | try self.dd.write(&[_]u8{ ControlByte.command, 0x20, @as(u8, @intFromEnum(mode)) }); 219 | } 220 | 221 | pub fn set_column_address(self: Self, start: u7, end: u7) !void { 222 | try self.dd.connect(); 223 | defer self.dd.disconnect(); 224 | 225 | try self.dd.write(&[_]u8{ ControlByte.command, 0x21, @as(u8, start), @as(u8, end) }); 226 | } 227 | 228 | pub fn set_page_address(self: Self, start: u3, end: u3) !void { 229 | try self.dd.connect(); 230 | defer self.dd.disconnect(); 231 | 232 | try self.dd.write(&[_]u8{ ControlByte.command, 0x22, @as(u8, start), @as(u8, end) }); 233 | } 234 | 235 | pub fn set_page_start_address(self: Self, address: u3) !void { 236 | try self.dd.connect(); 237 | defer self.dd.disconnect(); 238 | 239 | try self.dd.write(&[_]u8{ ControlByte.command, 0xB0 | @as(u8, address) }); 240 | } 241 | 242 | // Hardware Configuration Commands 243 | pub fn set_display_start_line(self: Self, line: u6) !void { 244 | try self.dd.connect(); 245 | defer self.dd.disconnect(); 246 | 247 | try self.dd.write(&[_]u8{ ControlByte.command, 0x40 | @as(u8, line) }); 248 | } 249 | 250 | // false: column address 0 is mapped to SEG0 251 | // true: column address 127 is mapped to SEG0 252 | pub fn set_segment_remap(self: Self, remap: bool) !void { 253 | try self.dd.connect(); 254 | defer self.dd.disconnect(); 255 | 256 | if (remap) { 257 | try self.dd.write(&[_]u8{ ControlByte.command, 0xA1 }); 258 | } else { 259 | try self.dd.write(&[_]u8{ ControlByte.command, 0xA0 }); 260 | } 261 | } 262 | 263 | pub fn set_multiplex_ratio(self: Self, ratio: u6) !void { 264 | if (ratio <= 14) return InputError.InvalidEntry; 265 | 266 | try self.dd.connect(); 267 | defer self.dd.disconnect(); 268 | 269 | try self.dd.write(&[_]u8{ ControlByte.command, 0xA8, @as(u8, ratio) }); 270 | } 271 | 272 | /// false: normal (COM0 to COMn) 273 | /// true: remapped 274 | pub fn set_com_ouput_scan_direction(self: Self, remap: bool) !void { 275 | try self.dd.connect(); 276 | defer self.dd.disconnect(); 277 | 278 | if (remap) { 279 | try self.dd.write(&[_]u8{ ControlByte.command, 0xC8 }); 280 | } else { 281 | try self.dd.write(&[_]u8{ ControlByte.command, 0xC0 }); 282 | } 283 | } 284 | 285 | pub fn set_display_offset(self: Self, vertical_shift: u6) !void { 286 | try self.dd.connect(); 287 | defer self.dd.disconnect(); 288 | 289 | try self.dd.write(&[_]u8{ ControlByte.command, 0xD3, @as(u8, vertical_shift) }); 290 | } 291 | 292 | // TODO(philippwendel) Make config to enum 293 | pub fn set_com_pins_hardware_configuration(self: Self, config: u2) !void { 294 | try self.dd.connect(); 295 | defer self.dd.disconnect(); 296 | 297 | try self.dd.write(&[_]u8{ ControlByte.command, 0xDA, @as(u8, config) << 4 | 0x02 }); 298 | } 299 | 300 | // Timing & Driving Scheme Setting Commands 301 | // TODO(philippwendel) Split in two funktions 302 | pub fn set_display_clock_divide_ratio_and_oscillator_frequency(self: Self, divide_ratio: u4, freq: u4) !void { 303 | try self.dd.connect(); 304 | defer self.dd.disconnect(); 305 | 306 | try self.dd.write(&[_]u8{ ControlByte.command, 0xD5, (@as(u8, freq) << 4) | @as(u8, divide_ratio) }); 307 | } 308 | 309 | pub fn set_precharge_period(self: Self, phase1: u4, phase2: u4) !void { 310 | if (phase1 == 0 or phase2 == 0) return InputError.InvalidEntry; 311 | 312 | try self.dd.connect(); 313 | defer self.dd.disconnect(); 314 | 315 | try self.dd.write(&[_]u8{ ControlByte.command, 0xD9, @as(u8, phase2) << 4 | @as(u8, phase1) }); 316 | } 317 | 318 | // TODO(philippwendel) Make level to enum 319 | pub fn set_v_comh_deselect_level(self: Self, level: u3) !void { 320 | try self.dd.connect(); 321 | defer self.dd.disconnect(); 322 | 323 | try self.dd.write(&[_]u8{ ControlByte.command, 0xDB, @as(u8, level) << 4 }); 324 | } 325 | 326 | pub fn nop(self: Self) !void { 327 | try self.dd.connect(); 328 | defer self.dd.disconnect(); 329 | 330 | try self.dd.write(&[_]u8{ ControlByte.command, 0xE3 }); 331 | } 332 | 333 | // Charge Pump Commands 334 | pub fn charge_pump_setting(self: Self, enable: bool) !void { 335 | try self.dd.connect(); 336 | defer self.dd.disconnect(); 337 | 338 | if (enable) { 339 | try self.dd.write(&[_]u8{ ControlByte.command, 0x8D, 0x14 }); 340 | } else { 341 | try self.dd.write(&[_]u8{ ControlByte.command, 0x8D, 0x10 }); 342 | } 343 | } 344 | }; 345 | } 346 | 347 | pub const Color = mdf.display.BlackAndWhite; 348 | 349 | pub const Framebuffer = struct { 350 | pub const width = 128; 351 | pub const height = 64; 352 | 353 | // layed out in 8 pages with 8*128 pixels each. 354 | // each page is column-major with the column encoded in the bits 0 (top) to 7 (bottom). 355 | // each byte in the page is a column left-to-right. 356 | // first page is thus columns 0..7, second page is 8..15 and so on. 357 | pixel_data: [8 * 128]u8, 358 | 359 | pub fn init(fill_color: Color) Framebuffer { 360 | var fb = Framebuffer{ .pixel_data = undefined }; 361 | @memset(&fb.pixel_data, switch (fill_color) { 362 | .black => 0x00, 363 | .white => 0xFF, 364 | }); 365 | return fb; 366 | } 367 | 368 | pub fn bit_stream(fb: *const Framebuffer) *const [8 * 128]u8 { 369 | return &fb.pixel_data; 370 | } 371 | 372 | pub fn clear(fb: *Framebuffer, color: Color) void { 373 | fb.* = init(color); 374 | } 375 | 376 | pub fn set_pixel(fb: *Framebuffer, x: u7, y: u6, color: Color) void { 377 | const page: u3 = @truncate(y / 8); 378 | const bit: u3 = @truncate(y % 8); 379 | const mask: u8 = @as(u8, 1) << bit; 380 | 381 | const offset: usize = (@as(usize, page) << 7) + x; 382 | 383 | switch (color) { 384 | .black => fb.pixel_data[offset] &= ~mask, 385 | .white => fb.pixel_data[offset] |= mask, 386 | } 387 | } 388 | }; 389 | 390 | const ControlByte = packed struct(u8) { 391 | zero: u6 = 0, 392 | 393 | /// The D/C# bit determines the next data byte is acted as a command or a data. If the D/C# bit is 394 | /// set to logic “0”, it defines the following data byte as a command. If the D/C# bit is set to 395 | /// logic “1”, it defines the following data byte as a data which will be stored at the GDDRAM. 396 | /// The GDDRAM column address pointer will be increased by one automatically after each 397 | /// data write. 398 | mode: enum(u1) { command = 0, data = 1 }, 399 | 400 | /// If the Co bit is set as logic “0”, the transmission of the following information will contain data bytes only. 401 | co_bit: u1, 402 | 403 | const command: u8 = @bitCast(ControlByte{ 404 | .mode = .command, 405 | .co_bit = 0, 406 | }); 407 | 408 | const data_byte: u8 = @bitCast(ControlByte{ 409 | .mode = .data, 410 | .co_bit = 1, 411 | }); 412 | 413 | const data_stream: u8 = @bitCast(ControlByte{ 414 | .mode = .data, 415 | .co_bit = 0, 416 | }); 417 | }; 418 | 419 | comptime { 420 | std.debug.assert(ControlByte.command == 0x00); 421 | std.debug.assert(ControlByte.data_byte == 0xC0); 422 | std.debug.assert(ControlByte.data_stream == 0x40); 423 | } 424 | 425 | // Fundamental Commands 426 | const DisplayOnMode = enum(u8) { resumeToRam = 0xA4, ignoreRam = 0xA5 }; 427 | const NormalOrInverseDisplay = enum(u8) { normal = 0xA6, inverse = 0xA7 }; 428 | const DisplayMode = enum(u8) { off = 0xAE, on = 0xAF }; 429 | 430 | // Scrolling Commands 431 | const HorizontalScrollDirection = enum(u8) { right = 0x26, left = 0x27 }; 432 | const VerticalAndHorizontalScrollDirection = enum(u8) { right = 0x29, left = 0x2A }; 433 | const PageError = error{EndPageIsSmallerThanStartPage}; 434 | 435 | // Addressing Setting Commands 436 | const Column = enum(u1) { lower = 0, higher = 1 }; 437 | const MemoryAddressingMode = enum(u2) { horizontal = 0b00, vertical = 0b01, page = 0b10 }; 438 | 439 | // Hardware Configuration Commands 440 | const InputError = error{InvalidEntry}; 441 | 442 | // Tests 443 | 444 | const TestDevice = mdf.base.DatagramDevice.TestDevice; 445 | 446 | // This is the command sequence created by SSD1306.init() 447 | // to set up the display. 448 | const recorded_init_sequence = [_][]const u8{ 449 | &.{ 0x00, 0xAE }, 450 | &.{ 0x00, 0x2E }, 451 | &.{ 0x00, 0xA1 }, 452 | &.{ 0x00, 0xC8 }, 453 | &.{ 0x00, 0xA6 }, 454 | &.{ 0x00, 0x81, 0xFF }, 455 | &.{ 0x00, 0x8D, 0x14 }, 456 | &.{ 0x00, 0xA8, 0x3F }, 457 | &.{ 0x00, 0xD5, 0x80 }, 458 | &.{ 0x00, 0xD9, 0x11 }, 459 | &.{ 0x00, 0xDB, 0x40 }, 460 | &.{ 0x00, 0xDA, 0x12 }, 461 | &.{ 0x00, 0xD3, 0x00 }, 462 | &.{ 0x00, 0x40 }, 463 | &.{ 0x00, 0xA4 }, 464 | &.{ 0x00, 0xAF }, 465 | }; 466 | 467 | // Fundamental Commands 468 | test "setContrast" { 469 | // Arrange 470 | for ([_]u8{ 0, 128, 255 }) |contrast| { 471 | var td = TestDevice.init_receiver_only(); 472 | defer td.deinit(); 473 | 474 | const expected_data = &[_]u8{ 0x00, 0x81, contrast }; 475 | 476 | // Act 477 | const driver = try SSD1306.init(td.datagram_device()); 478 | try driver.set_contrast(contrast); 479 | 480 | // Assert 481 | try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); 482 | } 483 | } 484 | 485 | test "entireDisplayOn" { 486 | // Arrange 487 | for ([_]u8{ 0xA4, 0xA5 }, [_]DisplayOnMode{ DisplayOnMode.resumeToRam, DisplayOnMode.ignoreRam }) |data, mode| { 488 | var td = TestDevice.init_receiver_only(); 489 | defer td.deinit(); 490 | 491 | const expected_data = &[_]u8{ 0x00, data }; 492 | // Act 493 | const driver = try SSD1306.init(td.datagram_device()); 494 | try driver.entire_display_on(mode); 495 | // Assert 496 | try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); 497 | } 498 | } 499 | 500 | test "setNormalOrInverseDisplay" { 501 | // Arrange 502 | for ([_]u8{ 0xA6, 0xA7 }, [_]NormalOrInverseDisplay{ NormalOrInverseDisplay.normal, NormalOrInverseDisplay.inverse }) |data, mode| { 503 | var td = TestDevice.init_receiver_only(); 504 | defer td.deinit(); 505 | 506 | const expected_data = &[_]u8{ 0x00, data }; 507 | // Act 508 | const driver = try SSD1306.init(td.datagram_device()); 509 | try driver.set_normal_or_inverse_display(mode); 510 | // Assert 511 | try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); 512 | } 513 | } 514 | 515 | test "setDisplay" { 516 | // Arrange 517 | for ([_]u8{ 0xAF, 0xAE }, [_]DisplayMode{ DisplayMode.on, DisplayMode.off }) |data, mode| { 518 | var td = TestDevice.init_receiver_only(); 519 | defer td.deinit(); 520 | 521 | const expected_data = &[_]u8{ 0x00, data }; 522 | // Act 523 | const driver = try SSD1306.init(td.datagram_device()); 524 | try driver.set_display(mode); 525 | // Assert 526 | try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); 527 | } 528 | } 529 | 530 | // Scrolling Commands 531 | // TODO(philippwendel) Test more values and error 532 | test "continuousHorizontalScrollSetup" { 533 | // Arrange 534 | var td = TestDevice.init_receiver_only(); 535 | defer td.deinit(); 536 | 537 | const expected_data = &[_]u8{ 0x00, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF }; 538 | // Act 539 | const driver = try SSD1306.init(td.datagram_device()); 540 | try driver.continuous_horizontal_scroll_setup(.right, 0, 0, 0); 541 | // Assert 542 | try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); 543 | } 544 | 545 | test "continuousVerticalAndHorizontalScrollSetup" { 546 | // Arrange 547 | var td = TestDevice.init_receiver_only(); 548 | defer td.deinit(); 549 | 550 | const expected_data = &[_]u8{ 0x00, 0x29, 0x00, 0x01, 0x3, 0x2, 0x4 }; 551 | // Act 552 | const driver = try SSD1306.init(td.datagram_device()); 553 | try driver.continuous_vertical_and_horizontal_scroll_setup(.right, 1, 2, 3, 4); 554 | // Assert 555 | try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); 556 | } 557 | 558 | test "deactivateScroll" { 559 | // Arrange 560 | var td = TestDevice.init_receiver_only(); 561 | defer td.deinit(); 562 | 563 | const expected_data = &[_]u8{ 0x00, 0x2E }; 564 | // Act 565 | const driver = try SSD1306.init(td.datagram_device()); 566 | try driver.deactivate_scroll(); 567 | // Assert 568 | try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); 569 | } 570 | 571 | test "activateScroll" { 572 | // Arrange 573 | var td = TestDevice.init_receiver_only(); 574 | defer td.deinit(); 575 | 576 | const expected_data = &[_]u8{ 0x00, 0x2F }; 577 | // Act 578 | const driver = try SSD1306.init(td.datagram_device()); 579 | try driver.activate_scroll(); 580 | // Assert 581 | try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); 582 | } 583 | 584 | test "setVerticalScrollArea" { 585 | // Arrange 586 | var td = TestDevice.init_receiver_only(); 587 | defer td.deinit(); 588 | 589 | const expected_data = &[_]u8{ 0x00, 0xA3, 0x00, 0x0F }; 590 | // Act 591 | const driver = try SSD1306.init(td.datagram_device()); 592 | try driver.set_vertical_scroll_area(0, 15); 593 | // Assert 594 | try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); 595 | } 596 | 597 | // Addressing Setting Commands 598 | test "setColumnStartAddressForPageAddressingMode" { 599 | // Arrange 600 | for ([_]Column{ Column.lower, Column.higher }, [_]u8{ 0x0F, 0x1F }) |column, data| { 601 | var td = TestDevice.init_receiver_only(); 602 | defer td.deinit(); 603 | 604 | const expected_data = &[_]u8{ 0x00, data }; 605 | // Act 606 | const driver = try SSD1306.init(td.datagram_device()); 607 | try driver.set_column_start_address_for_page_addressing_mode(column, 0xF); 608 | // Assert 609 | try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); 610 | } 611 | } 612 | 613 | test "setMemoryAddressingMode" { 614 | // Arrange 615 | for ([_]MemoryAddressingMode{ .horizontal, .vertical, .page }) |mode| { 616 | var td = TestDevice.init_receiver_only(); 617 | defer td.deinit(); 618 | 619 | const expected_data = &[_]u8{ 0x00, 0x20, @as(u8, @intFromEnum(mode)) }; 620 | // Act 621 | const driver = try SSD1306.init(td.datagram_device()); 622 | try driver.set_memory_addressing_mode(mode); 623 | // Assert 624 | try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); 625 | } 626 | } 627 | 628 | test "setColumnAddress" { 629 | // Arrange 630 | var td = TestDevice.init_receiver_only(); 631 | defer td.deinit(); 632 | 633 | const expected_data = &[_]u8{ 0x00, 0x21, 0, 127 }; 634 | // Act 635 | const driver = try SSD1306.init(td.datagram_device()); 636 | try driver.set_column_address(0, 127); 637 | // Assert 638 | try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); 639 | } 640 | 641 | test "setPageAddress" { 642 | // Arrange 643 | var td = TestDevice.init_receiver_only(); 644 | defer td.deinit(); 645 | 646 | const expected_data = &[_]u8{ 0x00, 0x22, 0, 7 }; 647 | // Act 648 | const driver = try SSD1306.init(td.datagram_device()); 649 | try driver.set_page_address(0, 7); 650 | // Assert 651 | try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); 652 | } 653 | 654 | test "setPageStartAddress" { 655 | // Arrange 656 | var td = TestDevice.init_receiver_only(); 657 | defer td.deinit(); 658 | 659 | const expected_data = &[_]u8{ 0x00, 0xB7 }; 660 | // Act 661 | const driver = try SSD1306.init(td.datagram_device()); 662 | try driver.set_page_start_address(7); 663 | // Assert 664 | try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); 665 | } 666 | 667 | // Hardware Configuration Commands 668 | test "setDisplayStartLine" { 669 | // Arrange 670 | var td = TestDevice.init_receiver_only(); 671 | defer td.deinit(); 672 | 673 | const expected_data = &[_]u8{ 0x00, 0b0110_0000 }; 674 | // Act 675 | const driver = try SSD1306.init(td.datagram_device()); 676 | try driver.set_display_start_line(32); 677 | // Assert 678 | try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); 679 | } 680 | 681 | test "setSegmentRemap" { 682 | // Arrange 683 | var td = TestDevice.init_receiver_only(); 684 | defer td.deinit(); 685 | 686 | const expected_data = &[_][]const u8{ &.{ 0x00, 0xA0 }, &.{ 0x00, 0xA1 } }; 687 | // Act 688 | const driver = try SSD1306.init(td.datagram_device()); 689 | try driver.set_segment_remap(false); 690 | try driver.set_segment_remap(true); 691 | // Assert 692 | try td.expect_sent(&recorded_init_sequence ++ expected_data); 693 | } 694 | 695 | test "setMultiplexRatio" { 696 | // Arrange 697 | var td = TestDevice.init_receiver_only(); 698 | defer td.deinit(); 699 | 700 | const expected_data = &[_]u8{ 0x00, 0xA8, 15 }; 701 | // Act 702 | const driver = try SSD1306.init(td.datagram_device()); 703 | try driver.set_multiplex_ratio(15); 704 | const err = driver.set_multiplex_ratio(0); 705 | // Assert 706 | try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); 707 | try std.testing.expectEqual(err, InputError.InvalidEntry); 708 | } 709 | 710 | test "setCOMOuputScanDirection" { 711 | // Arrange 712 | var td = TestDevice.init_receiver_only(); 713 | defer td.deinit(); 714 | 715 | const expected_data = &[_][]const u8{ &.{ 0x00, 0xC0 }, &.{ 0x00, 0xC8 } }; 716 | // Act 717 | const driver = try SSD1306.init(td.datagram_device()); 718 | try driver.set_com_ouput_scan_direction(false); 719 | try driver.set_com_ouput_scan_direction(true); 720 | // Assert 721 | try td.expect_sent(&recorded_init_sequence ++ expected_data); 722 | } 723 | 724 | test "setDisplayOffset" { 725 | // Arrange 726 | var td = TestDevice.init_receiver_only(); 727 | defer td.deinit(); 728 | 729 | const expected_data = &[_]u8{ 0x00, 0xD3, 17 }; 730 | // Act 731 | const driver = try SSD1306.init(td.datagram_device()); 732 | try driver.set_display_offset(17); 733 | // Assert 734 | try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); 735 | } 736 | 737 | test "setCOMPinsHardwareConfiguration" { 738 | // Arrange 739 | var td = TestDevice.init_receiver_only(); 740 | defer td.deinit(); 741 | 742 | const expected_data = &[_]u8{ 0x00, 0xDA, 0b0011_0010 }; 743 | // Act 744 | const driver = try SSD1306.init(td.datagram_device()); 745 | try driver.set_com_pins_hardware_configuration(0b11); 746 | // Assert 747 | try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); 748 | } 749 | 750 | // Timing & Driving Scheme Setting Commands 751 | test "setDisplayClockDivideRatioAndOscillatorFrequency" { 752 | // Arrange 753 | var td = TestDevice.init_receiver_only(); 754 | defer td.deinit(); 755 | 756 | const expected_data = &[_]u8{ 0x00, 0xD5, 0x00 }; 757 | // Act 758 | const driver = try SSD1306.init(td.datagram_device()); 759 | try driver.set_display_clock_divide_ratio_and_oscillator_frequency(0, 0); 760 | // Assert 761 | try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); 762 | } 763 | 764 | test "setPrechargePeriod" { 765 | // Arrange 766 | var td = TestDevice.init_receiver_only(); 767 | defer td.deinit(); 768 | 769 | const expected_data = &[_]u8{ 0x00, 0xD9, 0b0001_0001 }; 770 | // Act 771 | const driver = try SSD1306.init(td.datagram_device()); 772 | try driver.set_precharge_period(1, 1); 773 | // Assert 774 | try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); 775 | } 776 | 777 | test "setV_COMH_DeselectLevel" { 778 | // Arrange 779 | var td = TestDevice.init_receiver_only(); 780 | defer td.deinit(); 781 | 782 | const expected_data = &[_]u8{ 0x00, 0xDB, 0b0011_0000 }; 783 | // Act 784 | const driver = try SSD1306.init(td.datagram_device()); 785 | try driver.set_v_comh_deselect_level(0b011); 786 | // Assert 787 | try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); 788 | } 789 | 790 | test "nop" { 791 | // Arrange 792 | var td = TestDevice.init_receiver_only(); 793 | defer td.deinit(); 794 | 795 | const expected_data = &[_]u8{ 0x00, 0xE3 }; 796 | // Act 797 | const driver = try SSD1306.init(td.datagram_device()); 798 | try driver.nop(); 799 | // Assert 800 | try td.expect_sent(&recorded_init_sequence ++ .{expected_data}); 801 | } 802 | 803 | // Charge Pump Commands 804 | test "chargePumpSetting" { 805 | // Arrange 806 | var td = TestDevice.init_receiver_only(); 807 | defer td.deinit(); 808 | 809 | const expected_data = &[_][]const u8{ &.{ 0x00, 0x8D, 0x14 }, &.{ 0x00, 0x8D, 0x10 } }; 810 | // Act 811 | const driver = try SSD1306.init(td.datagram_device()); 812 | try driver.charge_pump_setting(true); 813 | try driver.charge_pump_setting(false); 814 | // Assert 815 | try td.expect_sent(&recorded_init_sequence ++ expected_data); 816 | } 817 | 818 | // References: 819 | // [1] https://cdn-shop.adafruit.com/datasheets/SSD1306.pdf 820 | 821 | test "Framebuffer.init(.black)" { 822 | const fb = Framebuffer.init(.black); 823 | for (fb.pixel_data) |chunk| { 824 | try std.testing.expectEqual(0x00, chunk); 825 | } 826 | } 827 | 828 | test "Framebuffer.init(.white)" { 829 | const fb = Framebuffer.init(.white); 830 | for (fb.pixel_data) |chunk| { 831 | try std.testing.expectEqual(0xFF, chunk); 832 | } 833 | } 834 | 835 | test "Framebuffer.set_pixel(..., .white)" { 836 | var fb = Framebuffer.init(.black); 837 | 838 | fb.set_pixel(0, 0, .white); 839 | try std.testing.expectEqual(0x01, fb.pixel_data[0]); 840 | 841 | for (fb.pixel_data[1..]) |chunk| { 842 | try std.testing.expectEqual(0x00, chunk); 843 | } 844 | } 845 | 846 | test "Framebuffer.set_pixel(..., .black)" { 847 | var fb = Framebuffer.init(.white); 848 | 849 | fb.set_pixel(0, 0, .black); 850 | try std.testing.expectEqual(0xFE, fb.pixel_data[0]); 851 | 852 | for (fb.pixel_data[1..]) |chunk| { 853 | try std.testing.expectEqual(0xFF, chunk); 854 | } 855 | } 856 | --------------------------------------------------------------------------------