1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
|
//! JP1, the JC-ESP32P4-M3-DEV's 26-pin header, as ONE TABLE that everything else is derived from:
//! the ASCII drawing the `Gpio` word prints, and the pin directories the board's 9P tree generates.
//!
//! WHY THIS IS ITS OWN FILE, and it is the whole reason it exists. The drawing lived in
//! `src/board_memory.zig`, which imports `pardes.zig` and therefore the entire core; the board's 9P
//! image (`src/esp32p4_9p.zig`) links no core at all, so it could not have reached it. The two
//! ways out of that were a second copy of the header in the 9P tree — a table of thirteen rows
//! transcribed off a schematic, maintained twice, with no test that could tell you the day they
//! disagreed — or this: a LEAF that imports `std` and nothing else, so both sides import the same
//! thirteen rows. `board_memory.zig` keeps its `pinout` name as an alias of `jp1_text` and its own
//! shape test, so the console word's output is unchanged to the byte.
//!
//! WHY A TABLE AND NOT THE STRING. The string was the source before, and a string is fine for one
//! consumer that prints it. It is no use at all to the second, which needs to know WHICH of these
//! twenty-six pins are the P4's own GPIOs, because that is the set of directories its tree has. A
//! consumer would have to parse the drawing back out — scan for `GPIO `, take the digits, hope
//! nobody aligned a column differently — which is exactly the sort of code that works until the
//! day the drawing is edited. So the rows are data, the drawing is RENDERED from them at comptime,
//! and `gpio_pins` is COLLECTED from them at comptime. Adding a pin to the header is one row, and
//! the drawing, the pin list and the 9P tree all move together because there is only one of them.
//!
//! READ OFF THE VENDOR SCHEMATIC, sheet 2 "Expand IO"
//! (`01-esp32p4-m3/docs/schematics/2_EXPAND_IO&BAT.png`), which is the only document that carries
//! this mapping — the specification PDF's "Interface Description" page is a marketing render, and
//! there is no board user guide. The sheet is a 872x1168 raster, so the assignment was taken from
//! the drawing's own geometry rather than by eye: thirteen wires leave each side of the symbol, a
//! net wire runs ~100 px to its label and a power stub ~21 px, which is what identifies pin 8 as
//! unconnected rather than as the first of the GPIO4x labels. Cross-checked against a second,
//! independent source: `05-zig-p4/build.zig` has always documented `-Dled=20` as "JP1 pin 17", and
//! GPIO20 lands on pin 17 here.
const std = @import("std");
/// What is behind one header pin, and the ONE distinction that matters to both consumers: whether
/// this pad is a GPIO of the ESP32-P4 this program is running on.
///
/// `.none` is a pin the header brings out with nothing behind it (pin 8). `.net` is a pad that is
/// not the P4's to drive as a GPIO: `3V3`, `5V` and `GND` are power, `C6_*` are the ESP32-C6
/// companion's pins — toggling a P4 GPIO cannot reach them — and `ES_I2C_*` is the audio codec's
/// bus. The codec's two ARE P4 pads, and they are `.net` anyway, deliberately: the schematic does
/// not name their GPIO numbers, and a tree that invented one would offer a file that drives an
/// unknown pin. They stay in the drawing because a shared bus is a reason to know the pin is there.
pub const Pad = union(enum) {
none,
/// a P4 GPIO, by the number the schematic, the silkscreen and the datasheet all use
gpio: u8,
/// a named net that is not a P4 GPIO
net: []const u8,
/// The text this pad wears in the drawing. `GPIO 47` and not `GPIO47`: the space is what the
/// header has always printed, and the shape test in `board_memory.zig` matches on it.
pub fn label(p: Pad) []const u8 {
return switch (p) {
.none => "--",
.gpio => |n| std.fmt.comptimePrint("GPIO {d}", .{n}),
.net => |s| s,
};
}
};
/// One row of the header: the odd pin on the left, the even pin on its right, exactly as the board
/// wears it. The pin NUMBERS are not stored — row `i` is pins `2i+1` and `2i+2` — because a
/// hand-written number beside a row is a number that can disagree with its position.
pub const Row = struct { left: Pad, right: Pad };
/// JP1 itself: thirteen rows, pin 1 at the top left. THE SINGLE SOURCE for the drawing below, for
/// `gpio_pins`, and for the per-pin directories in `src/board9p.zig`.
pub const jp1 = [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" } },
};
/// The row format, and it is load-bearing rather than cosmetic: a header drawn in two columns stops
/// being a header the moment a row wraps or a column slips, and the widest row here is 34 columns
/// against the board's own 80-column grid. Ten for the left label right-aligned, two for each pin
/// number, and the three bars land under the box's own corners because the left label's field plus
/// one space is eleven characters and `+---------+` is eleven wide.
///
/// `board_memory.zig`'s "the pinout fits the board's own grid" test is the check that this stays
/// true, and it checks the RENDERED text mechanically — every pin row's first bar in the same
/// column — rather than trusting this string.
const row_format = "{s:>10} | {d:>2} | {d:>2} | {s}\n";
/// The box the pin numbers sit inside. Eleven characters, indented by the left label's field width
/// plus the space before the first bar, so its corners are the bars.
const border = " +---------+\n";
/// JP1 as the text the `Gpio` word prints and a read of the 9P tree's `gpio/pinout` returns — the
/// SAME BYTES, which is a test in `src/board9p.zig` and not a hope.
///
/// The trailer names the `Gpio` word, which the 9P image does not have. It is here anyway, because
/// "the same bytes" is worth more than a sentence that is true of both faces and useful to neither:
/// a person reading this table through 9P is a person who has the editor's own console in the other
/// window, and telling them the word that flips a pin is telling them something they can use. The
/// 9P equivalent — writing `0` or `1` to `gpio/<n>/value` — is documented where a 9P client will
/// look for it, which is the tree's own doc comment.
pub const jp1_text = text: {
var out: []const u8 =
\\JP1 header - 26 pins, pin 1 top left.
\\Every number here is DECIMAL.
\\
\\
;
out = out ++ border;
for (jp1, 0..) |row, i| out = out ++ std.fmt.comptimePrint(
row_format,
.{ row.left.label(), 2 * i + 1, 2 * i + 2, row.right.label() },
);
break :text out ++ border ++
\\
\\Gpio <pin> flips one: 0->1 or 1->0.
\\
;
};
/// Every P4 GPIO JP1 brings out, ascending. THE SET OF PIN DIRECTORIES the board's 9P tree has, so
/// that tree has exactly the pins this board has and not a range somebody typed.
///
/// Ascending rather than in header order, because the consumer is `ls`: the header's order puts 47
/// between 2 and 3, and a directory listing that counts 1 2 3 4 5 20 32 33 45 46 47 is one a person
/// can scan. Nothing depends on the order — the names are the pin numbers — so it may as well be
/// the readable one.
pub const gpio_pins = pins: {
var found: [2 * jp1.len]u8 = undefined;
var n: usize = 0;
for (jp1) |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].*;
};
// The drawing, byte for byte, because it is the one thing here whose CORRECTNESS IS ITS SHAPE and
// because it used to be a string literal: this is the check that the renderer above reproduces what
// the console has always printed. A golden test is the right kind of duplication — the expectation
// is the thing being asserted, and if the two ever differ the diff says which byte.
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 <pin> flips one: 0->1 or 1->0.
\\
, jp1_text);
}
// The pin list is the tree's shape, so it is asserted as a list rather than as a count: a row edited
// wrongly changes WHICH pins the board offers, and a count would not notice a 45 that became a 44.
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);
// Pin 8 is unconnected and pin 24 is the C6's, so neither contributes a pad. Both are counted
// here rather than only drawn, because "the tree has exactly the pins the board has" is a claim
// about what is ABSENT as much as what is present.
try std.testing.expectEqual(Pad.none, jp1[3].right);
try std.testing.expectEqualStrings("C6_IO9", jp1[11].right.net);
}
|