diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-25 12:40:53 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-25 12:46:51 -0300 |
| commit | f5f8068fac59b4f16046c2022c2fc7c7e447ef4c (patch) | |
| tree | 2731a3ed4e51cae09e184e25778eded5fc37d1f5 /src/hal/gpio.zig | |
| download | esp32p4-f5f8068fac59b4f16046c2022c2fc7c7e447ef4c.tar.gz esp32p4-f5f8068fac59b4f16046c2022c2fc7c7e447ef4c.zip | |
zig-p4: pure-Zig ESP32-P4 toolchain
build.zig generates the linker script and drives Zig's own LLD; tools/image.zig
turns the ELF into a flashable image and tools/{rom,serial}.zig speak the mask
ROM loader over the UART. No CMake, ninja, idf.py, esptool, or external linker.
src/soc.zig is a comptime register model over ESP-IDF's own *_reg.h headers;
src/hal/ adds peripheral sequences; src/io/ implements std.Io for the chip;
src/oracle/ diffs this HAL against ESP-IDF's on the die.
Diffstat (limited to 'src/hal/gpio.zig')
| -rw-r--r-- | src/hal/gpio.zig | 471 |
1 files changed, 471 insertions, 0 deletions
diff --git a/src/hal/gpio.zig b/src/hal/gpio.zig new file mode 100644 index 0000000..88a8675 --- /dev/null +++ b/src/hal/gpio.zig @@ -0,0 +1,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()); +} |
