//! 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()); }