//! ESP32-P4 JP1 GPIO data and its freestanding filesystem. const std = @import("std"); // JP1: JC-ESP32P4-M3-DEV schematic, sheet 2 "Expand IO": // 01-esp32p4-m3/docs/schematics/2_EXPAND_IO&BAT.png pub const Header = struct { pub const Pad = union(enum) { none, gpio: u8, // C6 and power nets are not P4 GPIOs; the schematic gives no GPIO numbers for ES_I2C. net: []const u8, fn label(comptime p: Pad) []const u8 { return switch (p) { .none => "--", .gpio => |n| std.fmt.comptimePrint("GPIO {d}", .{n}), .net => |s| s, }; } }; pub const Row = struct { left: Pad, right: Pad }; pub const rows = [13]Row{ .{ .left = .{ .net = "3V3" }, .right = .{ .net = "5V" } }, .{ .left = .{ .net = "3V3" }, .right = .{ .net = "5V" } }, .{ .left = .{ .net = "GND" }, .right = .{ .net = "GND" } }, .{ .left = .{ .gpio = 1 }, .right = .none }, .{ .left = .{ .gpio = 2 }, .right = .{ .gpio = 47 } }, .{ .left = .{ .gpio = 3 }, .right = .{ .gpio = 46 } }, .{ .left = .{ .gpio = 4 }, .right = .{ .gpio = 45 } }, .{ .left = .{ .gpio = 5 }, .right = .{ .net = "GND" } }, .{ .left = .{ .gpio = 20 }, .right = .{ .net = "3V3" } }, .{ .left = .{ .gpio = 32 }, .right = .{ .net = "C6_U0RXD" } }, .{ .left = .{ .gpio = 33 }, .right = .{ .net = "C6_U0TXD" } }, .{ .left = .{ .net = "ES_I2C_SDA" }, .right = .{ .net = "C6_IO9" } }, .{ .left = .{ .net = "ES_I2C_SCL" }, .right = .{ .net = "C6_CHIP_PU" } }, }; const row_format = "{s:>10} | {d:>2} | {d:>2} | {s}\n"; const border = " +---------+\n"; pub const text = rendered: { var out: []const u8 = \\JP1 header - 26 pins, pin 1 top left. \\Every number here is DECIMAL. \\ \\ ; out = out ++ border; for (rows, 0..) |row, i| out = out ++ std.fmt.comptimePrint( row_format, .{ row.left.label(), 2 * i + 1, 2 * i + 2, row.right.label() }, ); break :rendered out ++ border ++ \\ \\Gpio flips one: 0->1 or 1->0. \\ ; }; pub const gpio_pins = pins: { var found: [2 * rows.len]u8 = undefined; var n: usize = 0; for (rows) |row| for ([2]Pad{ row.left, row.right }) |p| switch (p) { .gpio => |g| { found[n] = g; n += 1; }, else => {}, }; std.mem.sort(u8, found[0..n], {}, std.sort.asc(u8)); break :pins found[0..n].*; }; test "the rendered header is the drawing the console has always printed" { try std.testing.expectEqualStrings( \\JP1 header - 26 pins, pin 1 top left. \\Every number here is DECIMAL. \\ \\ +---------+ \\ 3V3 | 1 | 2 | 5V \\ 3V3 | 3 | 4 | 5V \\ GND | 5 | 6 | GND \\ GPIO 1 | 7 | 8 | -- \\ GPIO 2 | 9 | 10 | GPIO 47 \\ GPIO 3 | 11 | 12 | GPIO 46 \\ GPIO 4 | 13 | 14 | GPIO 45 \\ GPIO 5 | 15 | 16 | GND \\ GPIO 20 | 17 | 18 | 3V3 \\ GPIO 32 | 19 | 20 | C6_U0RXD \\ GPIO 33 | 21 | 22 | C6_U0TXD \\ES_I2C_SDA | 23 | 24 | C6_IO9 \\ES_I2C_SCL | 25 | 26 | C6_CHIP_PU \\ +---------+ \\ \\Gpio flips one: 0->1 or 1->0. \\ , text); } test "the header's own GPIOs, and only those" { try std.testing.expectEqualSlices(u8, &.{ 1, 2, 3, 4, 5, 20, 32, 33, 45, 46, 47 }, &gpio_pins); try std.testing.expectEqual(Pad.none, rows[3].right); try std.testing.expectEqualStrings("C6_IO9", rows[11].right.net); } }; pub const E = struct { pub const NOENT: u16 = 2; pub const NOTDIR: u16 = 20; pub const INVAL: u16 = 22; }; pub const root: u64 = 1; const gpio_node: u64 = 16; const pinout_node: u64 = 17; const Entry = struct { node: u64, parent: u64, name: []const u8, kind: enum { directory, pinout, value }, pin: u8 = 0, }; const entries = table: { var result: [3 + 2 * Header.gpio_pins.len]Entry = undefined; result[0] = .{ .node = root, .parent = root, .name = "/", .kind = .directory }; result[1] = .{ .node = gpio_node, .parent = root, .name = "gpio", .kind = .directory }; result[2] = .{ .node = pinout_node, .parent = gpio_node, .name = "pinout", .kind = .pinout }; for (Header.gpio_pins, 0..) |pin, i| { const directory = 18 + i; result[3 + 2 * i] = .{ .node = directory, .parent = gpio_node, .name = std.fmt.comptimePrint("{d}", .{pin}), .kind = .directory, }; result[4 + 2 * i] = .{ .node = (2 + i) * 16 + 1, .parent = directory, .name = "value", .kind = .value, .pin = pin, }; } break :table result; }; const dirent_fixed = 10; const out_capacity = size: { var result: usize = 2; for (entries) |directory| { if (directory.kind != .directory) continue; var bytes: usize = 0; for (entries) |entry| { if (entry.parent == directory.node and entry.node != directory.node) bytes += dirent_fixed + entry.name.len; } result = @max(result, bytes); } break :size result; }; pub fn Fs(comptime Pads: type) type { return struct { const Self = @This(); pub const name_capacity = 28; pub const Op = enum(u8) { lookup, getattr, setattr, open, read, write, release, readdir }; pub const Status = enum(u8) { ok, again, err }; pub const Req = struct { tag: u64, op: Op, node: u64, handle: u32 = 0, off: u64 = 0, size: u32 = 0, data: []const u8 = &.{}, truncate: bool = false, }; pub const Reply = struct { tag: u64, status: Status = .ok, errno: u16 = 0, attr: Attr = .{}, handle: u32 = 0, written: u32 = 0, pub const Attr = struct { name: []const u8 = "", node: u64 = 0, dir: bool = false, size: u64 = 0, mode: u16 = 0o600, }; pub fn fail(tag: u64, e: u16) Reply { return .{ .tag = tag, .status = .err, .errno = e }; } }; // Reply bytes are borrowed until the next handle call. pub const Answer = struct { reply: Reply, bytes: []const u8 = "" }; out: [out_capacity]u8 = undefined, fn find(node: u64) ?*const Entry { for (&entries) |*entry| if (entry.node == node) return entry; return null; } fn attributes(entry: *const Entry) Reply.Attr { return .{ .name = entry.name, .node = entry.node, .dir = entry.kind == .directory, .mode = switch (entry.kind) { .directory => 0o500, .pinout => 0o400, .value => 0o600, }, .size = switch (entry.kind) { .directory => 0, .pinout => Header.text.len, .value => 2, }, }; } pub fn handle(fs: *Self, req: Req) Answer { const entry = find(req.node) orelse return .{ .reply = .fail(req.tag, E.NOENT) }; switch (req.op) { .lookup => { if (entry.kind != .directory) return .{ .reply = .fail(req.tag, E.NOTDIR) }; if (std.mem.eql(u8, req.data, "..")) return .{ .reply = .{ .tag = req.tag, .attr = attributes(find(entry.parent).?) } }; for (&entries) |*child| { if (child.parent != req.node or child.node == req.node) continue; if (!std.mem.eql(u8, child.name, req.data)) continue; return .{ .reply = .{ .tag = req.tag, .attr = attributes(child) } }; } return .{ .reply = .fail(req.tag, E.NOENT) }; }, .getattr => return .{ .reply = .{ .tag = req.tag, .attr = attributes(entry) } }, .setattr => { // GPIO register views have no storage to truncate. if (entry.kind == .directory) return .{ .reply = .fail(req.tag, E.INVAL) }; return .{ .reply = .{ .tag = req.tag, .attr = attributes(entry) } }; }, .open => return .{ .reply = .{ .tag = req.tag, .handle = 1 } }, .release => return .{ .reply = .{ .tag = req.tag } }, .read => { const bytes = switch (entry.kind) { .directory => return .{ .reply = .fail(req.tag, E.INVAL) }, .pinout => Header.text, .value => value: { fs.out[0] = '0' + @as(u8, Pads.level(entry.pin)); fs.out[1] = '\n'; break :value fs.out[0..2]; }, }; const off: usize = @intCast(@min(req.off, bytes.len)); return .{ .reply = .{ .tag = req.tag }, .bytes = bytes[off..][0..@min(bytes.len - off, req.size)] }; }, .write => { if (entry.kind != .value or req.off != 0) return .{ .reply = .fail(req.tag, E.INVAL) }; const value = std.mem.trimEnd(u8, req.data, "\r\n"); if (value.len != 1 or (value[0] != '0' and value[0] != '1')) return .{ .reply = .fail(req.tag, E.INVAL) }; Pads.drive(entry.pin, @intCast(value[0] - '0')); return .{ .reply = .{ .tag = req.tag, .written = @intCast(req.data.len) } }; }, .readdir => { if (entry.kind != .directory) return .{ .reply = .fail(req.tag, E.NOTDIR) }; var len: usize = 0; var skip = req.off; for (entries) |child| { if (child.parent != req.node or child.node == req.node) continue; if (skip != 0) { skip -= 1; continue; } std.mem.writeInt(u64, fs.out[len..][0..8], child.node, .little); fs.out[len + 8] = @intFromBool(child.kind == .directory); fs.out[len + 9] = @intCast(child.name.len); @memcpy(fs.out[len + dirent_fixed ..][0..child.name.len], child.name); len += dirent_fixed + child.name.len; } return .{ .reply = .{ .tag = req.tag }, .bytes = fs.out[0..len] }; }, } } }; } const testing = std.testing; const StubPads = struct { var driven: [64]u1 = @splat(0); var log: [16]Call = undefined; var log_len: usize = 0; const Call = struct { pin: u8, level: u1 }; fn reset() void { driven = @splat(0); log_len = 0; } fn level(pin: u8) u1 { return driven[pin]; } fn drive(pin: u8, want: u1) void { driven[pin] = want; log[log_len] = .{ .pin = pin, .level = want }; log_len += 1; } }; const Board = Fs(StubPads); fn walk(t: *Board, path: []const []const u8) !Board.Reply.Attr { var at: u64 = root; var attr: Board.Reply.Attr = .{ .node = root, .dir = true, .mode = 0o500 }; for (path) |name| { const a = t.handle(.{ .tag = 1, .op = .lookup, .node = at, .data = name }); if (a.reply.status == .err) return switch (a.reply.errno) { E.NOENT => error.NoEntry, E.NOTDIR => error.NotDirectory, else => error.Refused, }; attr = a.reply.attr; at = attr.node; } return attr; } fn readAll(t: *Board, node: u64) !Board.Answer { const open = t.handle(.{ .tag = 1, .op = .open, .node = node }); try testing.expectEqual(Board.Status.ok, open.reply.status); return t.handle(.{ .tag = 2, .op = .read, .node = node, .handle = open.reply.handle, .size = 65535 }); } test "GPIO: the generated tree has exactly the header's pins, and nothing else" { var t: Board = .{}; try testing.expect((try walk(&t, &.{"gpio"})).dir); try testing.expect(!(try walk(&t, &.{ "gpio", "pinout" })).dir); for (Header.gpio_pins, 0..) |pin, i| { var name: [4]u8 = undefined; const dir = try std.fmt.bufPrint(&name, "{d}", .{pin}); const directory = try walk(&t, &.{ "gpio", dir }); try testing.expect(directory.dir); try testing.expectEqual(@as(u64, 18 + i), directory.node); const value = try walk(&t, &.{ "gpio", dir, "value" }); try testing.expect(!value.dir); try testing.expectEqual(@as(u64, (2 + i) * 16 + 1), value.node); try testing.expectEqual(@as(u16, 0o600), value.mode); } try testing.expectError(error.NoEntry, walk(&t, &.{ "gpio", "6" })); try testing.expectError(error.NoEntry, walk(&t, &.{ "gpio", "21" })); try testing.expectError(error.NoEntry, walk(&t, &.{ "gpio", "20", "level" })); try testing.expectError(error.NoEntry, walk(&t, &.{"mem"})); } test "GPIO: a read of gpio/pinout is the bytes the Gpio word draws" { var t: Board = .{}; const at = try walk(&t, &.{ "gpio", "pinout" }); const a = try readAll(&t, at.node); try testing.expectEqualStrings(Header.text, a.bytes); try testing.expectEqual(Header.text.len, at.size); try testing.expectEqual(@as(u16, 0o400), at.mode); const w = t.handle(.{ .tag = 3, .op = .write, .node = at.node, .data = "x" }); try testing.expectEqual(E.INVAL, w.reply.errno); } test "GPIO: writing 1 then 0 drives the pad twice, through the seam" { StubPads.reset(); var t: Board = .{}; const at = try walk(&t, &.{ "gpio", "20", "value" }); const before = try readAll(&t, at.node); try testing.expectEqualStrings("0\n", before.bytes); const one = t.handle(.{ .tag = 4, .op = .write, .node = at.node, .data = "1" }); try testing.expectEqual(Board.Status.ok, one.reply.status); try testing.expectEqual(@as(u32, 1), one.reply.written); try testing.expectEqualStrings("1\n", (try readAll(&t, at.node)).bytes); const zero = t.handle(.{ .tag = 5, .op = .write, .node = at.node, .data = "0\n" }); try testing.expectEqual(@as(u32, 2), zero.reply.written); try testing.expectEqualStrings("0\n", (try readAll(&t, at.node)).bytes); try testing.expectEqual(@as(usize, 2), StubPads.log_len); try testing.expectEqual(StubPads.Call{ .pin = 20, .level = 1 }, StubPads.log[0]); try testing.expectEqual(StubPads.Call{ .pin = 20, .level = 0 }, StubPads.log[1]); } test "GPIO: a pad takes 0 and 1 and refuses everything else, without touching the pads" { StubPads.reset(); var t: Board = .{}; const at = try walk(&t, &.{ "gpio", "45", "value" }); for ([_][]const u8{ "2", "", "01", "x", "true", "high", "\n", "1 ", " 1", "10" }) |bad| { const a = t.handle(.{ .tag = 6, .op = .write, .node = at.node, .data = bad }); try testing.expectEqual(Board.Status.err, a.reply.status); try testing.expectEqual(E.INVAL, a.reply.errno); } try testing.expectEqual(@as(usize, 0), StubPads.log_len); const off = t.handle(.{ .tag = 7, .op = .write, .node = at.node, .off = 1, .data = "1" }); try testing.expectEqual(E.INVAL, off.reply.errno); try testing.expectEqual(@as(usize, 0), StubPads.log_len); } test "GPIO: node parents are explicit, not derived from node bits" { for (&entries) |*e| { const serial = e.node >> 4; const file = e.node & 0xF; const derived: u64 = if (e.node == root or serial == 0 or file == 0) root else serial << 4; if (derived == e.parent) continue; try testing.expectEqualStrings("value", e.name); var t: Board = .{}; const a = t.handle(.{ .tag = 8, .op = .getattr, .node = derived }); try testing.expectEqual(E.NOENT, a.reply.errno); } } test "GPIO: parent lookup follows every generated directory" { var t: Board = .{}; for (&entries) |*entry| { if (entry.kind != .directory) continue; const answer = t.handle(.{ .tag = 1, .op = .lookup, .node = entry.node, .data = ".." }); try testing.expectEqual(Board.Status.ok, answer.reply.status); try testing.expectEqual(entry.parent, answer.reply.attr.node); try testing.expect(answer.reply.attr.dir); } } test "GPIO: a directory read lists what the table generated, in table order" { var t: Board = .{}; try testing.expectEqualStrings("gpio", (try names(&t, root, 0))[0]); try testing.expectEqual(@as(usize, 1), (try names(&t, root, 0)).len); const gpio = (try walk(&t, &.{"gpio"})).node; const listing = try names(&t, gpio, 0); try testing.expectEqual(Header.gpio_pins.len + 1, listing.len); try testing.expectEqualStrings("pinout", listing[0]); for (Header.gpio_pins, 0..) |pin, i| { var buf: [4]u8 = undefined; try testing.expectEqualStrings(try std.fmt.bufPrint(&buf, "{d}", .{pin}), listing[i + 1]); } const rest = try names(&t, gpio, 5); try testing.expectEqual(Header.gpio_pins.len + 1 - 5, rest.len); try testing.expectEqualStrings("5", rest[0]); } var name_slots: [32][]const u8 = undefined; fn names(t: *Board, node: u64, skip: u64) ![][]const u8 { const a = t.handle(.{ .tag = 9, .op = .readdir, .node = node, .off = skip, .size = 65535 }); try testing.expectEqual(Board.Status.ok, a.reply.status); var n: usize = 0; var i: usize = 0; while (i < a.bytes.len) { const len = a.bytes[i + 9]; name_slots[n] = a.bytes[i + 10 ..][0..len]; n += 1; i += 10 + len; } return name_slots[0..n]; } test "GPIO: the whole tree costs one buffer, and the table says how big" { try testing.expectEqual(@as(usize, 143), out_capacity); try testing.expectEqual(@as(usize, 143), @sizeOf(Board)); try testing.expectEqual(@as(usize, 28), Board.name_capacity); try testing.expect(Header.text.len > out_capacity); } test "GPIO: stat and truncation preserve the register value and reads respect offsets" { StubPads.reset(); var fs: Board = .{}; const value = try walk(&fs, &.{ "gpio", "20", "value" }); for (0..2) |_| { const written = fs.handle(.{ .tag = 1, .op = .write, .node = value.node, .data = "1\r\n" }); try testing.expectEqual(@as(u32, 3), written.reply.written); } try testing.expectEqual(@as(usize, 2), StubPads.log_len); const stat = fs.handle(.{ .tag = 2, .op = .getattr, .node = value.node }); try testing.expectEqual(@as(u64, 2), stat.reply.attr.size); const truncated = fs.handle(.{ .tag = 3, .op = .setattr, .node = value.node, .truncate = true }); try testing.expectEqual(Board.Status.ok, truncated.reply.status); try testing.expectEqual(@as(u1, 1), StubPads.driven[20]); const part = fs.handle(.{ .tag = 4, .op = .read, .node = value.node, .off = 1, .size = 1 }); try testing.expectEqualStrings("\n", part.bytes); const eof = fs.handle(.{ .tag = 5, .op = .read, .node = value.node, .off = 2, .size = 1 }); try testing.expectEqual(@as(usize, 0), eof.bytes.len); const pinout = try walk(&fs, &.{ "gpio", "pinout" }); const text = fs.handle(.{ .tag = 6, .op = .read, .node = pinout.node, .off = 10, .size = 12 }); try testing.expectEqualStrings(Header.text[10..22], text.bytes); } const ninep = @import("9p.zig"); test "GPIO: a real 9P client reads a pin's value off this tree" { StubPads.reset(); const Server = ninep.Server(Board, ninep.board_fids); var in: [1024]u8 = undefined; var out: [2048]u8 = undefined; var fsys: Board = .{}; var srv = Server.init(.{ .in = &in, .out = &out, .root = root }); var scratch: [256]u8 = undefined; const send = struct { fn call(s: *Server, f: *Board, buf: []u8, tag: u16, msg: ninep.Msg) !void { const bytes = try ninep.encode(msg, tag, buf); try testing.expectEqual(bytes.len, s.push(bytes)); while (s.retry()) |req| { const a = f.handle(req); s.reply(&a.reply, a.bytes); } while (s.next()) |req| { const a = f.handle(req); s.reply(&a.reply, a.bytes); } } }.call; const reap = struct { fn call(s: *Server) !ninep.Decoded { const queued = s.output(); const len = ninep.frameLen(queued) orelse return error.NoReply; const got = try ninep.decode(queued[0..len]); s.wrote(len); return got; } }.call; try send(&srv, &fsys, &scratch, ninep.notag, .{ .tversion = .{ .msize = 8192, .version = "9P2000" } }); const v = try reap(&srv); try testing.expectEqual(@as(u32, 1024), v.msg.rversion.msize); try send(&srv, &fsys, &scratch, 1, .{ .tattach = .{ .fid = 0, .afid = ninep.nofid, .uname = "goblin", .aname = "" } }); try testing.expectEqual(root, (try reap(&srv)).msg.rattach.qid.path); var wname: [ninep.max_welem][]const u8 = @splat(""); wname[0] = "gpio"; wname[1] = "20"; wname[2] = "value"; try send(&srv, &fsys, &scratch, 2, .{ .twalk = .{ .fid = 0, .newfid = 1, .nwname = 3, .wname = wname } }); try testing.expectEqual(@as(u16, 3), (try reap(&srv)).msg.rwalk.nwqid); try send(&srv, &fsys, &scratch, 3, .{ .topen = .{ .fid = 1, .mode = ninep.ordwr } }); _ = try reap(&srv); try send(&srv, &fsys, &scratch, 4, .{ .twrite = .{ .fid = 1, .offset = 0, .data = "1\n" } }); try testing.expectEqual(@as(u32, 2), (try reap(&srv)).msg.rwrite.count); try testing.expectEqual(@as(u1, 1), StubPads.driven[20]); try send(&srv, &fsys, &scratch, 5, .{ .tread = .{ .fid = 1, .offset = 0, .count = 512 } }); try testing.expectEqualStrings("1\n", (try reap(&srv)).msg.rread.data); try send(&srv, &fsys, &scratch, 6, .{ .twrite = .{ .fid = 1, .offset = 0, .data = "on" } }); try testing.expectEqualStrings(ninep.errString(E.INVAL), (try reap(&srv)).msg.rerror.ename); try testing.expectEqual(@as(usize, 1), StubPads.log_len); }