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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
|
//! GPIO and the IO MUX.
//!
//! The P4 has 57 pins (GPIO0-56) and every whole-bank register is therefore split in two: `out`
//! covers 0-31 and `out1` covers 32-56. Getting that split wrong is the classic P4 GPIO bug - a
//! write to `out` with a shift of 40 lands on pin 8 - so the bank arithmetic lives in exactly one
//! place here (`Bank`) and every operation goes through it.
//!
//! Levels and enables are driven through the `_W1TS`/`_W1TC` (write-1-to-set / write-1-to-clear)
//! aliases rather than read-modify-write on `out`/`enable`. That is what ESP-IDF's LL does, and it
//! is not a style choice: a read-modify-write of a whole bank races anything else touching another
//! pin in the same bank, and there is no lock here to prevent it.
//!
//! Pad configuration (direction of the *input* buffer, pulls, drive strength, function select) is
//! not in the GPIO peripheral at all - it is in the IO MUX, one register per pad. The two must be
//! kept in step: a pin driven by `enable` but with `fun_ie` clear cannot be read back, which is the
//! single most common "my GPIO does not work" on this part.
const std = @import("std");
const regs = @import("regs");
const mmio = @import("mmio");
const Reg = mmio.Reg;
const Field = mmio.Field;
/// GPIO0-56. 57 pins, and the last five (52-56) exist only on some packages.
pub const max_pin = 56;
pub const pin_count = max_pin + 1;
/// Which half of a split bank register a pin lives in, and its bit inside that half.
const Bank = struct {
high: bool,
bit: u5,
inline fn of(pin: u8) Bank {
std.debug.assert(pin <= max_pin);
return if (pin < 32)
.{ .high = false, .bit = @intCast(pin) }
else
.{ .high = true, .bit = @intCast(pin - 32) };
}
inline fn mask(self: Bank) u32 {
return @as(u32, 1) << self.bit;
}
inline fn pick(self: Bank, lo: Reg, hi: Reg) Reg {
return if (self.high) hi else lo;
}
};
// The whole-bank registers. `_W1TS`/`_W1TC` are separate addresses that set or clear only the bits
// written as 1, which is what makes a single-pin update atomic against the rest of the bank.
const out = Reg.at(regs.GPIO_OUT_REG);
const out1 = Reg.at(regs.GPIO_OUT1_REG);
const out_w1ts = Reg.at(regs.GPIO_OUT_W1TS_REG);
const out1_w1ts = Reg.at(regs.GPIO_OUT1_W1TS_REG);
const out_w1tc = Reg.at(regs.GPIO_OUT_W1TC_REG);
const out1_w1tc = Reg.at(regs.GPIO_OUT1_W1TC_REG);
const enable_w1ts = Reg.at(regs.GPIO_ENABLE_W1TS_REG);
const enable1_w1ts = Reg.at(regs.GPIO_ENABLE1_W1TS_REG);
const enable_w1tc = Reg.at(regs.GPIO_ENABLE_W1TC_REG);
const enable1_w1tc = Reg.at(regs.GPIO_ENABLE1_W1TC_REG);
const enable = Reg.at(regs.GPIO_ENABLE_REG);
const enable1 = Reg.at(regs.GPIO_ENABLE1_REG);
const in = Reg.at(regs.GPIO_IN_REG);
const in1 = Reg.at(regs.GPIO_IN1_REG);
/// One IO MUX register per pad, stride taken from two consecutive macros rather than assumed.
const pad = mmio.RegArray(
regs.PERIPHS_IO_MUX_U_PAD_GPIO0,
regs.PERIPHS_IO_MUX_U_PAD_GPIO1,
pin_count,
);
// Pad fields. These macros are unprefixed globals in io_mux_reg.h - they describe every pad, not
// one - which is why they read as bare `MCU_SEL` rather than `IO_MUX_GPIO7_MCU_SEL`.
const fun_ie = Field.of(regs.FUN_IE_S, regs.FUN_IE_V);
const fun_drv = Field.of(regs.FUN_DRV_S, regs.FUN_DRV_V);
const mcu_sel = Field.of(regs.MCU_SEL_S, regs.MCU_SEL_V);
// io_mux_reg.h defines no macros for the two pull bits; io_mux_struct.h documents them as
// `fun_wpd : R/W; bitpos: [7]` and `fun_wpu : R/W; bitpos: [8]`.
const fun_wpd = Field.bit(7);
const fun_wpu = Field.bit(8);
/// IO MUX function for a pad. Function 1 is plain GPIO on every P4 pad; the others select a
/// peripheral wired directly to that pad, and anything not on this list has to go through the GPIO
/// matrix instead.
pub const Function = enum(u3) {
f0 = 0,
/// Plain GPIO - the GPIO peripheral drives and samples the pad.
gpio = 1,
f2 = 2,
f3 = 3,
f4 = 4,
f5 = 5,
f6 = 6,
f7 = 7,
};
pub const Drive = enum(u2) {
/// ~5 mA
weakest = 0,
/// ~10 mA
weak = 1,
/// ~20 mA, the reset value
medium = 2,
/// ~40 mA
strong = 3,
};
pub const Pull = enum { none, up, down };
// ------------------------------------------------------------------------------------- levels
/// Drive a pin high or low. Uses the write-1-to-set/clear alias, so no other pin in the bank is
/// disturbed and no read is needed.
pub inline fn setLevel(pin: u8, level: u1) void {
const b = Bank.of(pin);
const r = if (level == 1)
b.pick(out_w1ts, out1_w1ts)
else
b.pick(out_w1tc, out1_w1tc);
r.writeRaw(b.mask());
}
pub inline fn setHigh(pin: u8) void {
setLevel(pin, 1);
}
pub inline fn setLow(pin: u8) void {
setLevel(pin, 0);
}
pub inline fn toggle(pin: u8) void {
const b = Bank.of(pin);
if (b.pick(out, out1).raw() & b.mask() != 0) setLow(pin) else setHigh(pin);
}
/// Sample the pad. Reads the *input* register, so it reports what the pin is actually at - which
/// for an open-drain or externally driven pin is not necessarily what was last written to `out`.
/// Requires the pad's input buffer to be enabled (`setInputEnable`).
pub inline fn getLevel(pin: u8) u1 {
const b = Bank.of(pin);
return @intCast((b.pick(in, in1).raw() >> b.bit) & 1);
}
/// What was last driven, from the output register rather than the pad.
pub inline fn getDrivenLevel(pin: u8) u1 {
const b = Bank.of(pin);
return @intCast((b.pick(out, out1).raw() >> b.bit) & 1);
}
// -------------------------------------------------------------------------------- direction
pub inline fn outputEnable(pin: u8) void {
const b = Bank.of(pin);
b.pick(enable_w1ts, enable1_w1ts).writeRaw(b.mask());
}
pub inline fn outputDisable(pin: u8) void {
const b = Bank.of(pin);
b.pick(enable_w1tc, enable1_w1tc).writeRaw(b.mask());
}
pub inline fn isOutputEnabled(pin: u8) bool {
const b = Bank.of(pin);
return b.pick(enable, enable1).raw() & b.mask() != 0;
}
/// The pad's input buffer. Independent of the output driver: both can be on at once, which is how a
/// pin is read back while being driven.
pub inline fn setInputEnable(pin: u8, on: bool) void {
pad.at(pin).modify(.{fun_ie.is(@intFromBool(on))});
}
/// Whether the pad's input buffer is on. The counterpart of `setInputEnable`, and worth having
/// because a routed input with `fun_ie` clear is indistinguishable from a card that never drove
/// the pin: both read as a constant.
pub inline fn isInputEnabled(pin: u8) bool {
return pad.at(pin).get(fun_ie) != 0;
}
// -------------------------------------------------------------------------------- pad config
pub inline fn setFunction(pin: u8, f: Function) void {
pad.at(pin).modify(.{mcu_sel.is(@intFromEnum(f))});
}
pub inline fn setDrive(pin: u8, d: Drive) void {
pad.at(pin).modify(.{fun_drv.is(@intFromEnum(d))});
}
/// Internal pull resistors. Setting one direction always clears the other in the same store: a pad
/// with both enabled is a fight between two resistors, and it is easy to reach by two calls.
pub inline fn setPull(pin: u8, p: Pull) void {
pad.at(pin).modify(.{
fun_wpu.is(@intFromBool(p == .up)),
fun_wpd.is(@intFromBool(p == .down)),
});
}
/// What `setPull` last left, read back from the pad. A pad with both resistors enabled cannot be
/// reached through `setPull`, but the reset value or another driver can leave one that way, so the
/// contradictory case is reported as `.none` rather than picking a winner.
pub inline fn getPull(pin: u8) Pull {
const w = pad.at(pin).raw();
const up = w & fun_wpu.mask() != 0;
const down = w & fun_wpd.mask() != 0;
if (up and !down) return .up;
if (down and !up) return .down;
return .none;
}
/// Open-drain: the pad drives low and releases high instead of driving both rails.
///
/// This one is not in the IO MUX with the other pad properties - it is `GPIO_PINn_PAD_DRIVER`, bit
/// 2 of the GPIO peripheral's per-pin register (`gpio_reg.h:363-368`, "1:open-drain. 0:normal"),
/// which is a different register file from `PERIPHS_IO_MUX_U_PAD_GPIOn`. A shared bus - I2C, or any
/// wired-AND signal - needs this on both pads *and* an external pull-up; the internal pull-up is
/// too weak for anything but a short trace at a low bit rate.
pub inline fn setOpenDrain(pin: u8, on: bool) void {
pin_cfg.at(pin).modify(.{pad_driver.is(@intFromBool(on))});
}
/// The GPIO peripheral's per-pin configuration register, one per pad. Not the IO MUX: this file
/// holds the open-drain select, the interrupt configuration and the input synchroniser bypasses.
const pin_cfg = mmio.RegArray(regs.GPIO_PIN0_REG, regs.GPIO_PIN1_REG, pin_count);
const pad_driver = Field.of(regs.GPIO_PIN0_PAD_DRIVER_S, regs.GPIO_PIN0_PAD_DRIVER_V);
// -------------------------------------------------------------------------- pin interrupts
/// How a pad raises its interrupt. `gpio_reg.h:377-381`: "0:disable GPIO interrupt. 1:trigger at
/// posedge. 2:trigger at negedge. 3:trigger at any edge. 4:valid at low level. 5:valid at high
/// level".
pub const IntrType = enum(u3) {
disable = 0,
posedge = 1,
negedge = 2,
anyedge = 3,
low_level = 4,
high_level = 5,
};
const int_type = Field.of(regs.GPIO_PIN0_INT_TYPE_S, regs.GPIO_PIN0_INT_TYPE_V);
/// Five bits, one per consumer of the pad's interrupt, not a boolean. `gpio_reg.h:400-402` says
/// "set bit 13 to enable CPU interrupt, set bit 14 to enable CPU(not shielded) interrupt", and
/// `gpio_ll.h:41,213` names bit 0 of the field `GPIO_LL_INTR0_ENA` and writes exactly that to
/// route a pad to the `gpio_intr0` source. Writing 1 here means "line 0", not "enabled".
const int_ena = Field.of(regs.GPIO_PIN0_INT_ENA_S, regs.GPIO_PIN0_INT_ENA_V);
/// Which of the P4's four GPIO interrupt outputs a pad drives. Each is a separate entry in the
/// interrupt matrix (`hal.intr.Source.gpio_intr0` .. `gpio_intr3`), and each has its own status
/// register pair. ESP-IDF only ever uses line 0 - `gpio_ll_intr_enable_on_core` hard-codes
/// `GPIO_LL_INTR0_ENA` with a "TODO: IDF-7995" beside it - so line 0 is the tested path.
pub const IntrLine = enum(u3) {
line0 = 0,
line1 = 1,
line2 = 2,
line3 = 3,
};
/// Per-line status, gated by `int_ena`. Reading `status`/`status1` instead would report pads whose
/// interrupt is configured but routed to a different line. `gpio_reg.h:277,291` for line 0,
/// `:302,316` for line 1; lines 2 and 3 continue the same +0x8 stride.
const intr_status = mmio.RegArray(regs.GPIO_INTR_0_REG, regs.GPIO_INTR_1_REG, 4);
const intr_status1 = mmio.RegArray(regs.GPIO_INTR1_0_REG, regs.GPIO_INTR1_1_REG, 4);
/// Status is cleared through a shared write-1-to-clear register, not a per-line one: one pad has
/// one latch however many lines observe it. `gpio_reg.h:233,269`.
const status_w1tc = Reg.at(regs.GPIO_STATUS_W1TC_REG);
const status1_w1tc = Reg.at(regs.GPIO_STATUS1_W1TC_REG);
/// Arm a pad's interrupt and route it to one of the four GPIO interrupt outputs.
///
/// This is the GPIO peripheral's half only. The other half is `hal.intr`: the chosen line still
/// has to be routed from `Source.gpio_intr0`+n to a CLIC line and given a handler. Doing it in two
/// calls is deliberate - one pad's interrupt and one CPU line are not the same resource, and
/// several pads normally share a line.
///
/// Stale latched status is cleared first. A pad that saw an edge before its interrupt was armed
/// otherwise fires immediately on enable, which looks exactly like a real event.
pub fn setInterrupt(pin: u8, t: IntrType, line: IntrLine) void {
std.debug.assert(pin <= max_pin);
clearInterrupt(pin);
pin_cfg.at(pin).modify(.{
int_type.is(@intFromEnum(t)),
int_ena.is(if (t == .disable) 0 else @as(u32, 1) << @intFromEnum(line)),
});
}
/// Disarm, leaving the trigger type alone so it can be re-enabled unchanged.
pub fn disableInterrupt(pin: u8) void {
pin_cfg.at(pin).modify(.{int_ena.is(0)});
}
pub fn interruptPending(pin: u8, line: IntrLine) bool {
const b = Bank.of(pin);
const i: u32 = @intFromEnum(line);
return b.pick(intr_status.at(i), intr_status1.at(i)).raw() & b.mask() != 0;
}
/// Every pad currently interrupting on `line`, as a 57-bit mask in two halves. One read of each
/// register, so a handler can dispatch the whole set without re-reading between pads.
pub fn pendingMask(line: IntrLine) struct { low: u32, high: u32 } {
const i: u32 = @intFromEnum(line);
return .{ .low = intr_status.at(i).raw(), .high = intr_status1.at(i).raw() };
}
pub fn clearInterrupt(pin: u8) void {
const b = Bank.of(pin);
b.pick(status_w1tc, status1_w1tc).writeRaw(b.mask());
}
pub fn clearInterrupts(low: u32, high: u32) void {
if (low != 0) status_w1tc.writeRaw(low);
if (high != 0) status1_w1tc.writeRaw(high);
}
/// Everything a pin needs to be a plain push-pull output, in the order the hardware wants: select
/// the pad's function before enabling the driver, so the pin never spends a moment driven by
/// whatever peripheral the IO MUX happened to be pointing at.
pub fn configureOutput(pin: u8, opts: struct {
drive: Drive = .medium,
/// Enable the input buffer too, so the pin can be read back.
readback: bool = false,
}) void {
setFunction(pin, .gpio);
// Point the matrix at the GPIO peripheral: a pad left routed to whatever signal was there
// before is the failure this line prevents.
func_out_sel.at(pin).modify(.{ out_sel.is(matrix_gpio_signal), oen_sel.is(0) });
pad.at(pin).modify(.{
fun_drv.is(@intFromEnum(opts.drive)),
fun_ie.is(@intFromBool(opts.readback)),
fun_wpu.is(0),
fun_wpd.is(0),
});
outputEnable(pin);
}
/// A plain input: driver off, input buffer on, optional pull.
pub fn configureInput(pin: u8, opts: struct { pull: Pull = .none }) void {
outputDisable(pin);
setFunction(pin, .gpio);
pad.at(pin).modify(.{
fun_ie.is(1),
fun_wpu.is(@intFromBool(opts.pull == .up)),
fun_wpd.is(@intFromBool(opts.pull == .down)),
});
}
// ------------------------------------------------------------------------------- GPIO matrix
/// The GPIO matrix: 256 peripheral output signals, any of which can be routed to any pad. This is
/// how a UART reaches a pin that has no direct IO MUX function for it.
const func_out_sel = mmio.RegArray(
regs.GPIO_FUNC0_OUT_SEL_CFG_REG,
regs.GPIO_FUNC1_OUT_SEL_CFG_REG,
pin_count,
);
// The input side of the matrix, indexed by *signal* rather than by pad: GPIO_FUNCn_IN_SEL_CFG
// selects which pad feeds peripheral input signal n. That is the opposite indexing from
// `func_out_sel` above, and it is why the two arrays exist separately.
//
// The base is FUNC1's address minus one word, not FUNC1's address. gpio_struct.h:849 declares
// `func_in_sel_cfg[256]` and notes func0 is reserved, so ESP-IDF's register header defines no
// GPIO_FUNC0_IN_SEL_CFG_REG at all - the array starts at +0x158 with a name-less word. Anchoring
// on FUNC1 with a count of 256 is off by one in both directions: `at(n)` would configure signal
// n+1, and `at(255)` would land on GPIO_FUNC0_OUT_SEL_CFG_REG (+0x558) and start driving a pad.
// Bounds checked against the headers: FUNC255_IN_SEL_CFG_REG is +0x554 = 0x158 + 4*255.
const func_in_sel = mmio.RegArray(
regs.GPIO_FUNC1_IN_SEL_CFG_REG - 4,
regs.GPIO_FUNC1_IN_SEL_CFG_REG,
256,
);
const out_sel = Field.of(regs.GPIO_FUNC0_OUT_SEL_S, regs.GPIO_FUNC0_OUT_SEL_V);
const oen_sel = Field.of(regs.GPIO_FUNC0_OEN_SEL_S, regs.GPIO_FUNC0_OEN_SEL_V);
// The input side's three fields. All of GPIO_FUNCn_IN_SEL_CFG's fields share these shifts, so as
// with the pad registers one macro triple describes all 256.
const in_sel = Field.of(regs.GPIO_FUNC1_IN_SEL_S, regs.GPIO_FUNC1_IN_SEL_V);
const in_inv_sel = Field.of(regs.GPIO_FUNC1_IN_INV_SEL_S, regs.GPIO_FUNC1_IN_INV_SEL_V);
/// 1 = take this signal from the GPIO matrix, 0 = from the pad's direct IO MUX function.
const sig_in_sel = Field.of(regs.GPIO_SIG1_IN_SEL_S, regs.GPIO_SIG1_IN_SEL_V);
/// Writing this value instead of a peripheral signal index means "the GPIO peripheral drives this
/// pad", which is the matrix's way of expressing plain GPIO output. It comes from IDF's own signal
/// map because it is chip-specific: 256 here, 128 on the ESP32-S3.
pub const matrix_gpio_signal: u32 = regs.SIG_GPIO_OUT_IDX;
/// Route a peripheral output signal to a pad through the matrix, and let that peripheral own the
/// pad's output enable.
///
/// `OEN_SEL` reads backwards from its name, and the differential test against ESP-IDF's LL is what
/// caught it: 1 means "use GPIO_ENABLE_REG[n] as the output enable", 0 means "use the peripheral's
/// own output enable signal" (gpio_reg.h, GPIO_FUNC0_OEN_SEL). A routed peripheral must have 0 - its
/// OE is part of the signal being routed. The first version of this function set 1 and then set the
/// matching GPIO_ENABLE bit to compensate, which worked by the wrong mechanism and left the pad
/// latently output-enabled: clear OEN_SEL later and the pin would start driving on its own.
pub fn matrixOut(pin: u8, signal: u32) void {
std.debug.assert(pin <= max_pin);
setFunction(pin, .gpio);
func_out_sel.at(pin).modify(.{ out_sel.is(signal), oen_sel.is(0) });
}
/// Route a pad to a peripheral *input* signal through the matrix.
///
/// Indexed by signal, not by pin, which is the opposite of `matrixOut`: one pad may feed any number
/// of input signals, but a signal has exactly one source. The three writes are one word, where
/// gpio_ll.h:613-618 uses three bitfield stores; the resulting word is identical and nothing here
/// depends on the intermediate states, whereas a driver that read the register back between them
/// could observe a signal sourced from the wrong pad.
///
/// This does not enable the pad's input buffer - `setInputEnable` does, and a routed input with
/// `fun_ie` clear reads as a constant. Callers that want the pad readable must do both.
pub fn matrixIn(pin: u8, signal: u32) void {
std.debug.assert(pin <= max_pin or pin == matrix_const_zero or pin == matrix_const_one);
std.debug.assert(signal < 256);
func_in_sel.at(signal).modify(.{
in_sel.is(pin),
in_inv_sel.is(0),
sig_in_sel.is(1),
});
}
/// Where a peripheral input signal is sourced from. The read side of `matrixIn`, for a diagnostic
/// that has to distinguish "routed to the wrong pad" from "not routed at all" - the two look the
/// same from the peripheral's end.
pub const MatrixIn = struct {
/// A pad index, or `matrix_const_zero`/`matrix_const_one`. Meaningless when `from_matrix` is
/// false: the field keeps its reset value in that case, which can read like a deliberate
/// tie-high and is not one.
pin: u8,
inverted: bool,
/// `sig_in_sel`. False means the matrix is bypassed entirely and the signal comes from the
/// pad's direct IO MUX function - which for a peripheral that has none is undefined.
from_matrix: bool,
};
pub fn matrixInSource(signal: u32) MatrixIn {
std.debug.assert(signal < 256);
const w = func_in_sel.at(signal).raw();
return .{
.pin = @intCast((w >> in_sel.shift) & in_sel.unshiftedMask()),
.inverted = w & in_inv_sel.mask() != 0,
.from_matrix = w & sig_in_sel.mask() != 0,
};
}
/// Two values of `matrixIn`'s `pin` that are not pins: they tie the signal to a constant level
/// inside the matrix. `gpio_reg.h:3717-3719` documents the encoding on the register itself -
/// "s=0-56: connect GPIO[s] to this port. s=0x3F: set this port always high level. s=0x3E: set
/// this port always low level" - and `soc/gpio_pins.h:13-14` gives them the names ESP-IDF's
/// drivers use. They are chip-specific: 0x38/0x30 on the ESP32, 0x1E/0x1F on the C3.
///
/// This is how an unwired peripheral input gets a defined level. Leaving one alone is not
/// equivalent: `in_sel` does default to 0x3F, but `sig_in_sel` defaults to 0, which bypasses the
/// matrix entirely and takes the signal from the pad's direct IO MUX function - which for a
/// peripheral that has none is not a constant anything. `hal/sdmmc.zig` needs both of these for
/// slot 1's card-detect and card-interrupt inputs.
pub const matrix_const_one: u8 = 0x3f;
pub const matrix_const_zero: u8 = 0x3e;
test "bank arithmetic splits at 32, which is where the P4's second register begins" {
try std.testing.expectEqual(@as(u5, 20), Bank.of(20).bit);
try std.testing.expect(!Bank.of(20).high);
try std.testing.expectEqual(@as(u5, 0), Bank.of(32).bit);
try std.testing.expect(Bank.of(32).high);
try std.testing.expectEqual(@as(u5, 24), Bank.of(56).bit);
try std.testing.expectEqual(@as(u32, 1) << 24, Bank.of(56).mask());
}
|