//! GPIO's side of the differential test: the same operations expressed as ESP-IDF's LL calls and as //! this project's HAL calls. //! //! GPIO is restored by configuring rather than by resetting. It has no reset bit of its own in //! HP_SYS_CLKRST, and the pads are the board's wiring - the console's own pins are in this block, so //! a reset here would take the console with it. Configuring is sound for GPIO specifically because //! every field in the block is plain read/write: there is nothing self-clearing to restore. const std = @import("std"); const hal = @import("hal"); const regs = @import("regs"); const mmio = @import("mmio"); const types = @import("differ_types.zig"); extern fn oracle_gpio_uses_rom_api() c_int; extern fn oracle_gpio_set_level(pin: c_uint, level: c_uint) void; extern fn oracle_gpio_output_enable(pin: c_uint) void; extern fn oracle_gpio_output_disable(pin: c_uint) void; extern fn oracle_gpio_input_enable(pin: c_uint) void; extern fn oracle_gpio_input_disable(pin: c_uint) void; extern fn oracle_gpio_func_sel(pin: c_uint, func: c_uint) void; extern fn oracle_gpio_set_drive(pin: c_uint, strength: c_uint) void; extern fn oracle_gpio_pullup_en(pin: c_uint) void; extern fn oracle_gpio_pullup_dis(pin: c_uint) void; extern fn oracle_gpio_pulldown_en(pin: c_uint) void; extern fn oracle_gpio_pulldown_dis(pin: c_uint) void; extern fn oracle_gpio_matrix_out(pin: c_uint, signal: c_uint) void; extern fn oracle_gpio_od_enable(pin: c_uint) void; extern fn oracle_gpio_od_disable(pin: c_uint) void; /// Whether ESP-IDF's LL was compiled to call the mask ROM instead of writing registers. Must be 0, /// or the differential is comparing this HAL against `rom_gpio_set_output_level` rather than against /// IDF's register sequence. Governed by src/oracle/oracle_sdkconfig.h. pub fn usesRomApi() bool { return oracle_gpio_uses_rom_api() != 0; } /// The pin under test. A module-level variable because Zig has no closures and the harness stores /// plain `fn` pointers: a comptime-specialised pair per pin would compare code this project does not /// ship instead of the code it does. pub var pin: u8 = 20; /// Pins worth testing. 20 is the board's LED pin and 33 is a free header pin above the 32-boundary /// where this peripheral's bank arithmetic changes. GPIO54 is deliberately absent: it is this /// board's ESP32-C6 reset line, held high by an external pull-up, and driving it resets the radio. pub const pins = [_]u8{ 20, 33 }; /// Restore, built from register macros only. /// /// Nothing here may call the code under test. `differ.zig` runs restore, idf, snapshot, restore, /// ours, snapshot - so if restore is written with the HAL, run B starts from whatever IDF just wrote /// and a HAL function that does nothing at all compares equal. This suite used to restore with /// `hal.gpio.outputDisable` and `hal.gpio.setLow`, which made `output_disable` and `set_level(0)` /// no-op-versus-no-op: they could not fail. /// /// It must also be *total* over everything any case touches. Leaving `GPIO_PIN{n}_REG` alone made /// both `open_drain` cases vacuous, because run B inherited run A's pad_driver bit. fn restore() void { const b: u5 = @intCast(if (pin < 32) pin else pin - 32); const m = @as(u32, 1) << b; const enable_w1tc = if (pin < 32) regs.GPIO_ENABLE_W1TC_REG else regs.GPIO_ENABLE1_W1TC_REG; const out_w1tc = if (pin < 32) regs.GPIO_OUT_W1TC_REG else regs.GPIO_OUT1_W1TC_REG; mmio.Reg.atAddress(@intCast(enable_w1tc)).writeRaw(m); mmio.Reg.atAddress(@intCast(out_w1tc)).writeRaw(m); // The IO MUX pad word, the matrix output selector, and the GPIO block's own per-pin register. mmio.Reg.atAddress(@as(u32, @intCast(regs.PERIPHS_IO_MUX_U_PAD_GPIO0)) + 4 * @as(u32, pin)).writeRaw(0); mmio.Reg.atAddress(@as(u32, @intCast(regs.GPIO_FUNC0_OUT_SEL_CFG_REG)) + 4 * @as(u32, pin)) .writeRaw(@intCast(regs.SIG_GPIO_OUT_IDX)); mmio.Reg.atAddress(@as(u32, @intCast(regs.GPIO_PIN0_REG)) + 4 * @as(u32, pin)).writeRaw(0); } pub const suite: types.Suite = .{ .descriptor = .{ .name = "gpio", .base = @intCast(regs.GPIO_OUT_REG - 4), // GPIO_BT_SELECT_REG sits at +0x00 // 0x640 bytes. The window has to reach 0x558 + 4*57, where the matrix's per-pad output // configuration lives: a first version stopped at 0x1C0 and was blind to a real bug in // exactly those words - it saw the redundant GPIO_ENABLE write but not the wrong OEN_SEL // that made it necessary. .words = 400, .volatile_words = &.{ (0x03c - 0x000) / 4, // GPIO_IN - reflects the outside world, which moves (0x040 - 0x000) / 4, // GPIO_IN1 }, .restore = .{ .configure = restore }, }, .cases = &.{ .{ .name = "set_level", .arg = 1, .idf = idfSetHigh, .ours = ourSetHigh }, .{ .name = "set_level", .arg = 0, .idf = idfSetLow, .ours = ourSetLow }, .{ .name = "output_enable", .idf = idfOutEnable, .ours = ourOutEnable }, .{ .name = "output_disable", .idf = idfOutDisable, .ours = ourOutDisable }, .{ .name = "input_enable", .idf = idfInEnable, .ours = ourInEnable }, .{ .name = "input_disable", .idf = idfInDisable, .ours = ourInDisable }, .{ .name = "func_sel_gpio", .arg = 1, .idf = idfFuncGpio, .ours = ourFuncGpio }, .{ .name = "drive", .arg = 3, .idf = idfDriveStrong, .ours = ourDriveStrong }, .{ .name = "drive", .arg = 0, .idf = idfDriveWeakest, .ours = ourDriveWeakest }, .{ .name = "pull_up", .idf = idfPullUp, .ours = ourPullUp }, .{ .name = "pull_down", .idf = idfPullDown, .ours = ourPullDown }, .{ .name = "pull_none", .idf = idfPullNone, .ours = ourPullNone }, .{ .name = "matrix_out", .arg = 43, .idf = idfMatrixOut, .ours = ourMatrixOut }, // Open drain lives in the GPIO block's per-pin register, not the IO MUX pad register, and // had no accessor until the I2C port needed one - that bus is wired-AND, and a pin left // push-pull shorts it against another device's driver. .{ .name = "open_drain", .arg = 1, .idf = idfOdOn, .ours = ourOdOn }, .{ .name = "open_drain", .arg = 0, .idf = idfOdOff, .ours = ourOdOff }, }, }; /// The IO MUX, which the GPIO block's window does not reach. /// /// Every pad-configuration function on this chip writes `IO_MUX.gpio[n]` at /// PERIPHS_IO_MUX_U_PAD_GPIO0 = 0x500E1004 + 4*pin, and the GPIO block's compared window ends at /// 0x500E063F - 0xC00 bytes short. So `input_enable`, `input_disable`, `func_sel`, both `drive` /// cases and all three `pull` cases were comparing two identical snapshots of a register file none /// of them touches: 8 operations across 2 pins, 16 of the suite's cases, structurally unable to /// fail. They are the same cases; only the window is different. pub const iomux_suite: types.Suite = .{ .descriptor = .{ .name = "iomux", .base = @intCast(regs.PERIPHS_IO_MUX_U_PAD_GPIO0), .words = 57, // one per pad, GPIO0..GPIO56 .restore = .{ .configure = restoreIomux }, }, .cases = &.{ .{ .name = "input_enable", .idf = idfInEnable, .ours = ourInEnable }, .{ .name = "input_disable", .idf = idfInDisable, .ours = ourInDisable }, .{ .name = "func_sel_gpio", .arg = 1, .idf = idfFuncGpio, .ours = ourFuncGpio }, .{ .name = "drive", .arg = 3, .idf = idfDriveStrong, .ours = ourDriveStrong }, .{ .name = "drive", .arg = 0, .idf = idfDriveWeakest, .ours = ourDriveWeakest }, .{ .name = "pull_up", .idf = idfPullUp, .ours = ourPullUp }, .{ .name = "pull_down", .idf = idfPullDown, .ours = ourPullDown }, .{ .name = "pull_none", .idf = idfPullNone, .ours = ourPullNone }, }, }; fn restoreIomux() void { mmio.Reg.atAddress(@as(u32, @intCast(regs.PERIPHS_IO_MUX_U_PAD_GPIO0)) + 4 * @as(u32, pin)).writeRaw(0); } fn idfSetHigh() void { oracle_gpio_set_level(pin, 1); } fn ourSetHigh() void { hal.gpio.setHigh(pin); } fn idfSetLow() void { oracle_gpio_set_level(pin, 0); } fn ourSetLow() void { hal.gpio.setLow(pin); } fn idfOutEnable() void { oracle_gpio_output_enable(pin); } fn ourOutEnable() void { hal.gpio.outputEnable(pin); } fn idfOutDisable() void { oracle_gpio_output_disable(pin); } fn ourOutDisable() void { hal.gpio.outputDisable(pin); } fn idfInEnable() void { oracle_gpio_input_enable(pin); } fn ourInEnable() void { hal.gpio.setInputEnable(pin, true); } fn idfInDisable() void { oracle_gpio_input_disable(pin); } fn ourInDisable() void { hal.gpio.setInputEnable(pin, false); } fn idfFuncGpio() void { oracle_gpio_func_sel(pin, 1); } fn ourFuncGpio() void { hal.gpio.setFunction(pin, .gpio); } fn idfDriveStrong() void { oracle_gpio_set_drive(pin, 3); } fn ourDriveStrong() void { hal.gpio.setDrive(pin, .strong); } fn idfDriveWeakest() void { oracle_gpio_set_drive(pin, 0); } fn ourDriveWeakest() void { hal.gpio.setDrive(pin, .weakest); } fn idfPullUp() void { oracle_gpio_pullup_en(pin); oracle_gpio_pulldown_dis(pin); } fn ourPullUp() void { hal.gpio.setPull(pin, .up); } fn idfPullDown() void { oracle_gpio_pulldown_en(pin); oracle_gpio_pullup_dis(pin); } fn ourPullDown() void { hal.gpio.setPull(pin, .down); } fn idfPullNone() void { oracle_gpio_pullup_dis(pin); oracle_gpio_pulldown_dis(pin); } fn ourPullNone() void { hal.gpio.setPull(pin, .none); } fn idfMatrixOut() void { oracle_gpio_matrix_out(pin, 43); } fn ourMatrixOut() void { hal.gpio.matrixOut(pin, 43); } fn idfOdOn() void { oracle_gpio_od_enable(pin); } fn ourOdOn() void { hal.gpio.setOpenDrain(pin, true); } fn idfOdOff() void { oracle_gpio_od_disable(pin); } fn ourOdOff() void { hal.gpio.setOpenDrain(pin, false); }