From a470a8c1a4e211f61515bfc9ad476454882ebca3 Mon Sep 17 00:00:00 2001 From: matpala Date: Wed, 7 Oct 2026 21:45:14 -0300 Subject: [PATCH 1/3] drivers: add optional full-duplex transceive to DatagramDevice --- drivers/README.md | 8 +++ drivers/src/base/DatagramDevice.zig | 97 +++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/drivers/README.md b/drivers/README.md index 70d0781d7..cf4e74581 100644 --- a/drivers/README.md +++ b/drivers/README.md @@ -113,6 +113,11 @@ pub const DatagramDevice = struct { write_chunks: []const []const u8, read_chunks: []const []u8, ) (WriteError || ReadError)!void = null, + transceivev_fn: ?*const fn ( + *anyopaque, + write_chunks: []const []const u8, + read_chunks: []const []u8, + ) (WriteError || ReadError)!void = null, }; }; ``` @@ -129,6 +134,9 @@ pub const DatagramDevice = struct { - `read(buffer)` / `readv(buffers)` - Read datagrams, returns bytes read - `write_then_read(src, dst)` - Atomic write-then-read in single transaction - `writev_then_readv(write_chunks, read_chunks)` - Vectored write-then-read +- `transceive(src, dst)` / `transceivev(write_chunks, read_chunks)` - Full-duplex write and read in + a single transaction; only the total lengths must match. Optional: returns `error.Unsupported` if + the device doesn't implement it. Drivers using concrete types can check for it with `@hasDecl`. **Usage Example**: ```zig diff --git a/drivers/src/base/DatagramDevice.zig b/drivers/src/base/DatagramDevice.zig index a75724335..bf5799a57 100644 --- a/drivers/src/base/DatagramDevice.zig +++ b/drivers/src/base/DatagramDevice.zig @@ -70,6 +70,30 @@ pub fn writev_then_readv( return writev_then_readv_fn(dd.ptr, write_chunks, read_chunks); } +/// Writes `src` while reading into `dst`, in a single full-duplex transaction. +/// Both must have the same length. +/// +/// Returns `error.Unsupported` if the device can't transfer in both directions +/// at the same time. +pub fn transceive(dd: DatagramDevice, src: []const u8, dst: []u8) (WriteError || ReadError)!void { + return try dd.transceivev(&.{src}, &.{dst}); +} + +/// Writes `write_chunks` while reading into `read_chunks`, in a single +/// full-duplex transaction. Each side is the concatenation of its chunks, so +/// only the total lengths have to match, not the chunk boundaries. +/// +/// Returns `error.Unsupported` if the device can't transfer in both directions +/// at the same time. +pub fn transceivev( + dd: DatagramDevice, + write_chunks: []const []const u8, + read_chunks: []const []u8, +) (WriteError || ReadError)!void { + const transceivev_fn = dd.vtable.transceivev_fn orelse return error.Unsupported; + return transceivev_fn(dd.ptr, write_chunks, read_chunks); +} + /// Reads a single `datagram` from the device. /// Function returns the number of bytes written in `datagram`. /// @@ -99,6 +123,11 @@ pub const VTable = struct { write_chunks: []const []const u8, read_chunks: []const []u8, ) (WriteError || ReadError)!void = null, + transceivev_fn: ?*const fn ( + *anyopaque, + write_chunks: []const []const u8, + read_chunks: []const []u8, + ) (WriteError || ReadError)!void = null, }; /// A device implementation that can be used to write unit tests for datagram devices. @@ -257,12 +286,29 @@ pub const TestDevice = struct { _ = try TestDevice.readv(ctx, read_chunks); } + fn transceivev(ctx: *anyopaque, write_chunks: []const []const u8, read_chunks: []const []u8) (WriteError || ReadError)!void { + const read_len = chunks_len(read_chunks); + std.debug.assert(chunks_len(write_chunks) == read_len); + + try TestDevice.writev(ctx, write_chunks); + // A full-duplex transfer fills the whole read buffer. + if (try TestDevice.readv(ctx, read_chunks) != read_len) + return error.IoError; + } + + fn chunks_len(chunks: anytype) usize { + var len: usize = 0; + for (chunks) |chunk| len += chunk.len; + return len; + } + const vtable = VTable{ .connect_fn = TestDevice.connect, .disconnect_fn = TestDevice.disconnect, .writev_fn = TestDevice.writev, .readv_fn = TestDevice.readv, .writev_then_readv_fn = TestDevice.writev_then_readv, + .transceivev_fn = TestDevice.transceivev, }; }; @@ -333,3 +379,54 @@ test TestDevice { "See you soon!", }); } + +test transceive { + const reply = "abc"; + var td = TestDevice.init(&.{ reply, reply }, true); + defer td.deinit(); + + const dd = td.datagram_device(); + try dd.connect(); + defer dd.disconnect(); + + var buffer: [reply.len]u8 = undefined; + try dd.transceive("xyz", &buffer); + try std.testing.expectEqualStrings(reply, &buffer); + + // Only the total lengths have to match, not the chunk boundaries. + var head: [1]u8 = undefined; + var tail: [reply.len - head.len]u8 = undefined; + try dd.transceivev(&.{ "x", "y", "z" }, &.{ &head, &tail }); + try std.testing.expectEqualStrings(reply[0..head.len], &head); + try std.testing.expectEqualStrings(reply[head.len..], &tail); + + try td.expect_sent(&.{ "xyz", "xyz" }); +} + +test "transceive fails if the reply doesn't fill the buffer" { + var td = TestDevice.init(&.{"ab"}, true); + defer td.deinit(); + + const dd = td.datagram_device(); + try dd.connect(); + defer dd.disconnect(); + + var buffer: [3]u8 = undefined; + try std.testing.expectError(error.IoError, dd.transceive("xyz", &buffer)); +} + +test "transceive is unsupported without transceivev_fn" { + const dd: DatagramDevice = .{ + .ptr = undefined, + .vtable = &.{ + .connect_fn = null, + .disconnect_fn = null, + .writev_fn = null, + .readv_fn = null, + }, + }; + + var buffer: [1]u8 = undefined; + try std.testing.expectError(error.Unsupported, dd.transceive("x", &buffer)); + try std.testing.expectError(error.Unsupported, dd.transceivev(&.{"x"}, &.{&buffer})); +} From fca5d838f9ad1c961116e8008fa7ba376c2c73ea Mon Sep 17 00:00:00 2001 From: matpala Date: Fri, 9 Oct 2026 15:36:30 -0300 Subject: [PATCH 2/3] drivers: check transceive lengths with an error instead of an assert --- drivers/src/base/DatagramDevice.zig | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/drivers/src/base/DatagramDevice.zig b/drivers/src/base/DatagramDevice.zig index bf5799a57..1c92127da 100644 --- a/drivers/src/base/DatagramDevice.zig +++ b/drivers/src/base/DatagramDevice.zig @@ -288,7 +288,7 @@ pub const TestDevice = struct { fn transceivev(ctx: *anyopaque, write_chunks: []const []const u8, read_chunks: []const []u8) (WriteError || ReadError)!void { const read_len = chunks_len(read_chunks); - std.debug.assert(chunks_len(write_chunks) == read_len); + if (chunks_len(write_chunks) != read_len) return error.IoError; try TestDevice.writev(ctx, write_chunks); // A full-duplex transfer fills the whole read buffer. @@ -415,6 +415,19 @@ test "transceive fails if the reply doesn't fill the buffer" { try std.testing.expectError(error.IoError, dd.transceive("xyz", &buffer)); } +test "transceive fails if the lengths differ" { + var td = TestDevice.init(&.{"abc"}, true); + defer td.deinit(); + + const dd = td.datagram_device(); + try dd.connect(); + defer dd.disconnect(); + + var buffer: [2]u8 = undefined; + try std.testing.expectError(error.IoError, dd.transceive("xyz", &buffer)); + try td.expect_sent(&.{}); +} + test "transceive is unsupported without transceivev_fn" { const dd: DatagramDevice = .{ .ptr = undefined, From d9e4737f437d5d8e2ee9f87aa3f340ebdb8405aa Mon Sep 17 00:00:00 2001 From: matpala Date: Mon, 12 Oct 2026 14:58:32 -0300 Subject: [PATCH 3/3] drivers: add a configurable assert enabled by MicroBuild's asserts option --- build.zig | 4 +--- drivers/build.zig | 6 ++++++ drivers/src/base/DatagramDevice.zig | 16 ++-------------- drivers/src/root.zig | 10 ++++++++++ 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/build.zig b/build.zig index e693baadb..2064937e6 100644 --- a/build.zig +++ b/build.zig @@ -126,7 +126,6 @@ pub fn MicroBuild(port_select: PortSelect) type { dep: *Build.Dependency, internals_dep: *Build.Dependency, core_dep: *Build.Dependency, - drivers_dep: *Build.Dependency, /// Contains all the ports you selected. ports: SelectedPorts, @@ -209,7 +208,6 @@ pub fn MicroBuild(port_select: PortSelect) type { .dep = dep, .internals_dep = dep.builder.dependency("build-internals", .{}), .core_dep = dep.builder.dependency("core", .{}), - .drivers_dep = dep.builder.dependency("drivers", .{}), .ports = ports, }; return mb; @@ -341,7 +339,7 @@ pub fn MicroBuild(port_select: PortSelect) type { }, .{ .name = "drivers", - .module = mb.drivers_dep.module("drivers"), + .module = b.dependency("drivers", .{ .asserts = options.asserts }).module("drivers"), }, }, }); diff --git a/drivers/build.zig b/drivers/build.zig index 66ebde85f..0cc95a1f4 100644 --- a/drivers/build.zig +++ b/drivers/build.zig @@ -3,6 +3,10 @@ const std = @import("std"); pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); + const asserts = b.option(bool, "asserts", "Enable driver assert in every optimize mode") orelse false; + + const options = b.addOptions(); + options.addOption(bool, "asserts", asserts); const drivers_mod = b.addModule("drivers", .{ .root_source_file = b.path("src/root.zig"), @@ -14,6 +18,8 @@ pub fn build(b: *std.Build) void { }}, }); + drivers_mod.addOptions("config", options); + const test_suite = b.addTest(.{ .root_module = drivers_mod, .use_llvm = true, diff --git a/drivers/src/base/DatagramDevice.zig b/drivers/src/base/DatagramDevice.zig index 1c92127da..bc9dab152 100644 --- a/drivers/src/base/DatagramDevice.zig +++ b/drivers/src/base/DatagramDevice.zig @@ -7,6 +7,7 @@ //! const std = @import("std"); +const assert = @import("../root.zig").assert; const DatagramDevice = @This(); @@ -288,7 +289,7 @@ pub const TestDevice = struct { fn transceivev(ctx: *anyopaque, write_chunks: []const []const u8, read_chunks: []const []u8) (WriteError || ReadError)!void { const read_len = chunks_len(read_chunks); - if (chunks_len(write_chunks) != read_len) return error.IoError; + assert(chunks_len(write_chunks) == read_len); try TestDevice.writev(ctx, write_chunks); // A full-duplex transfer fills the whole read buffer. @@ -415,19 +416,6 @@ test "transceive fails if the reply doesn't fill the buffer" { try std.testing.expectError(error.IoError, dd.transceive("xyz", &buffer)); } -test "transceive fails if the lengths differ" { - var td = TestDevice.init(&.{"abc"}, true); - defer td.deinit(); - - const dd = td.datagram_device(); - try dd.connect(); - defer dd.disconnect(); - - var buffer: [2]u8 = undefined; - try std.testing.expectError(error.IoError, dd.transceive("xyz", &buffer)); - try td.expect_sent(&.{}); -} - test "transceive is unsupported without transceivev_fn" { const dd: DatagramDevice = .{ .ptr = undefined, diff --git a/drivers/src/root.zig b/drivers/src/root.zig index a2e52ea78..4f80bd037 100644 --- a/drivers/src/root.zig +++ b/drivers/src/root.zig @@ -2,6 +2,16 @@ //! The driver framework provides device-independent drivers for peripherials supported by MicroZig. //! const std = @import("std"); +const config = @import("config"); + +/// Panics if `ok` is false, in any optimize mode, but only when asserts are +/// enabled: by the `asserts` option of MicroBuild's `add_firmware` (the same +/// flag as `microzig.assert`), or with `-Dasserts=true` when building the +/// drivers alone. Otherwise it does nothing. +pub fn assert(ok: bool) void { + if (!config.asserts) return; + if (!ok) @panic("Assertion failed"); +} pub const display = struct { pub const sh1106 = @import("display/sh1106.zig");