summaryrefslogtreecommitdiff
path: root/src/oracle/gpio_cases.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-25 12:40:53 -0300
committerGabriel Schneider <[email protected]>2026-08-25 12:46:51 -0300
commitf5f8068fac59b4f16046c2022c2fc7c7e447ef4c (patch)
tree2731a3ed4e51cae09e184e25778eded5fc37d1f5 /src/oracle/gpio_cases.zig
downloadesp32p4-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/oracle/gpio_cases.zig')
-rw-r--r--src/oracle/gpio_cases.zig233
1 files changed, 233 insertions, 0 deletions
diff --git a/src/oracle/gpio_cases.zig b/src/oracle/gpio_cases.zig
new file mode 100644
index 0000000..ea32eff
--- /dev/null
+++ b/src/oracle/gpio_cases.zig
@@ -0,0 +1,233 @@
+//! 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);
+}