From f5f8068fac59b4f16046c2022c2fc7c7e447ef4c Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 25 Aug 2026 12:40:53 -0300 Subject: 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. --- src/oracle/all.zig | 51 ++++ src/oracle/clkrst_cases.zig | 131 +++++++++ src/oracle/clkrst_ref.c | 55 ++++ src/oracle/differ_types.zig | 72 +++++ src/oracle/gpio_cases.zig | 233 ++++++++++++++++ src/oracle/gpio_ref.c | 116 ++++++++ src/oracle/i2c_cases.zig | 627 ++++++++++++++++++++++++++++++++++++++++++ src/oracle/i2c_ref.c | 262 ++++++++++++++++++ src/oracle/intr_cases.zig | 595 +++++++++++++++++++++++++++++++++++++++ src/oracle/intr_ref.c | 211 ++++++++++++++ src/oracle/ledc_cases.zig | 513 ++++++++++++++++++++++++++++++++++ src/oracle/ledc_ref.c | 210 ++++++++++++++ src/oracle/oracle_sdkconfig.h | 43 +++ src/oracle/runtime_ref.c | 19 ++ src/oracle/sdmmc_cases.zig | 567 ++++++++++++++++++++++++++++++++++++++ src/oracle/sdmmc_ref.c | 262 ++++++++++++++++++ src/oracle/timg_cases.zig | 435 +++++++++++++++++++++++++++++ src/oracle/timg_ref.c | 197 +++++++++++++ src/oracle/uart_cases.zig | 326 ++++++++++++++++++++++ src/oracle/uart_ref.c | 163 +++++++++++ 20 files changed, 5088 insertions(+) create mode 100644 src/oracle/all.zig create mode 100644 src/oracle/clkrst_cases.zig create mode 100644 src/oracle/clkrst_ref.c create mode 100644 src/oracle/differ_types.zig create mode 100644 src/oracle/gpio_cases.zig create mode 100644 src/oracle/gpio_ref.c create mode 100644 src/oracle/i2c_cases.zig create mode 100644 src/oracle/i2c_ref.c create mode 100644 src/oracle/intr_cases.zig create mode 100644 src/oracle/intr_ref.c create mode 100644 src/oracle/ledc_cases.zig create mode 100644 src/oracle/ledc_ref.c create mode 100644 src/oracle/oracle_sdkconfig.h create mode 100644 src/oracle/runtime_ref.c create mode 100644 src/oracle/sdmmc_cases.zig create mode 100644 src/oracle/sdmmc_ref.c create mode 100644 src/oracle/timg_cases.zig create mode 100644 src/oracle/timg_ref.c create mode 100644 src/oracle/uart_cases.zig create mode 100644 src/oracle/uart_ref.c (limited to 'src/oracle') diff --git a/src/oracle/all.zig b/src/oracle/all.zig new file mode 100644 index 0000000..9537eac --- /dev/null +++ b/src/oracle/all.zig @@ -0,0 +1,51 @@ +//! Every peripheral registered with the differential harness. +//! +//! One line per peripheral. The harness walks this list, so adding a peripheral to the oracle is +//! three new files (`_ref.c`, `_cases.zig`, `src/hal/.zig`) plus one line here. + +pub const types = @import("differ_types.zig"); + +pub const gpio = @import("gpio_cases.zig"); +pub const clkrst = @import("clkrst_cases.zig"); +pub const timg = @import("timg_cases.zig"); +pub const uart = @import("uart_cases.zig"); +pub const intr = @import("intr_cases.zig"); +pub const ledc = @import("ledc_cases.zig"); +pub const i2c = @import("i2c_cases.zig"); +pub const sdmmc = @import("sdmmc_cases.zig"); + +/// The suites, in the order they run. +/// +/// GPIO first: the console's own pins live in that block, so a failure there explains failures +/// everywhere else. `clkrst` last of the always-on set, because its cases deliberately gate +/// peripherals off and its restore is what puts them back. +pub const suites = [_]types.Suite{ + gpio.suite, + gpio.iomux_suite, + timg.suite, + uart.suite, + intr.suite, + intr.clic_suite, + intr.thresh_suite, + // LEDC's state is not contiguous, so it comes as four windows rather than one: the block + // itself, the gamma RAM aperture (whose restore has to zero the RAM, because a peripheral reset + // does not), the GPIO words its pin routing touches, and the one HP_SYS_CLKRST word the P4 + // moved its clock mux into. + ledc.suite, + ledc.gamma_suite, + ledc.routing_suite, + ledc.clock_suite, + // I2C likewise needs two: half of setBusTiming lands outside the I2C block, because the + // controller-clock divider is in HP_SYS_CLKRST. At 10 kHz that divider is 4, so an + // implementation that wrote all ten timing registers perfectly and the divider not at all would + // run the bus four times too fast and pass every case in the first suite. + i2c.suite, + i2c.clock_suite, + // SDMMC, likewise in two windows: the controller block, and the host clock generator that the + // P4 put in HP_SYS_CLKRST rather than in the peripheral. At 40 MHz the whole division happens + // in the second one, so a suite that covered only the first would pass on a bus running four + // times too fast. + sdmmc.suite, + sdmmc.clock_suite, + clkrst.suite, +}; diff --git a/src/oracle/clkrst_cases.zig b/src/oracle/clkrst_cases.zig new file mode 100644 index 0000000..f004dd4 --- /dev/null +++ b/src/oracle/clkrst_cases.zig @@ -0,0 +1,131 @@ +//! HP_SYS_CLKRST's side of the differential: the clock-gate and reset pairing table. +//! +//! This suite exists because of a bug that a hardware test failed to catch. `src/hal/clkrst.zig` +//! maps each peripheral to the register and bit that gate and reset it, and every row of that table +//! is a transcription from ESP-IDF's LL source - the field macros do not record which register they +//! live in, so there is nothing to derive it from. Two rows were wrong: timg0, timg1, systimer and +//! twai0 had their APB clock enables in `PERI_CLK_CTRL21` instead of `SOC_CLK_CTRL2`, so +//! `setClockEnabled` wrote a bit of an unrelated register. +//! +//! `examples/halcheck.zig` printed exactly the expected `twai0 boot=0 on=1 off=0` throughout, +//! because `isClockEnabled` read back the same wrong bit `setClockEnabled` had just written. A +//! self-consistent test proves the two halves of your own code agree; it does not prove either one +//! touches the hardware, and that one would have passed with the chip unplugged. +//! +//! ESP-IDF reaches these bits through its own generated struct definitions - a genuinely independent +//! path - so this comparison is the check a read-back cannot be. +//! +//! Not covered here: TWAI0. Its bus clock is the one that is gated off at power-on, which makes it +//! the interesting case, but ESP-IDF's TWAI bus-clock LL takes a controller handle this suite has no +//! business constructing. The four rows below share the two registers TWAI0's row uses, so a +//! transcription error in it would have to be independent of theirs to survive. + +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_clkrst_timg_bus_clock(group: c_uint, enable: c_int) void; +extern fn oracle_clkrst_timg_reset(group: c_uint) void; +extern fn oracle_clkrst_systimer_bus_clock(enable: c_int) void; +extern fn oracle_clkrst_systimer_reset() void; +extern fn oracle_clkrst_uart_bus_clock(port: c_uint, enable: c_int) void; + +/// Known state: every peripheral this suite touches with its bus clock on, which is also the state +/// the chip powers up in ("All peripheral clocks are default enabled after chip is powered on", +/// esp_system/port/soc/esp32p4/clk.c:200). Nothing else in the block is touched - UART0's gates in +/// particular, because that is the console this result is printed over. +/// Restore through ESP-IDF's LL, never through the code under test. +/// +/// This suite exists to catch a `setClockEnabled` that writes the wrong register. Restoring with +/// `hal.clkrst.setClockEnabled` defeated exactly that: `differ.zig` runs restore, idf, snapshot, +/// restore, ours, snapshot, so with the HAL on both the restore and the "ours" side, a +/// `setClockEnabled` that did nothing at all would leave run B's snapshot equal to run A's and pass +/// all six clock cases. Which is how the original bug - four peripherals' gate bits in +/// PERI_CLK_CTRL21 instead of SOC_CLK_CTRL2 - could have survived this suite too. +fn restore() void { + oracle_clkrst_timg_bus_clock(1, 1); + oracle_clkrst_systimer_bus_clock(1); + oracle_clkrst_uart_bus_clock(1, 1); +} + +pub const suite: types.Suite = .{ + .descriptor = .{ + .name = "clkrst", + .base = @intCast(regs.HP_SYS_CLKRST_SOC_CLK_CTRL1_REG - 0x18), // block base + // 0x00 through HP_RST_EN2 at +0xC8: covers SOC_CLK_CTRL1/2 (+0x18, +0x1c), every + // PERI_CLK_CTRL register, and all three HP_RST_EN registers. Everything either + // implementation could plausibly hit is inside this window, which is the property that + // makes a difference detectable rather than merely absent. + .words = 52, + .restore = .{ .configure = restore }, + }, + .cases = &.{ + // Disable first in each pair: the restored state has them on, so "disable" is the operation + // with an observable effect and "enable" would otherwise be a no-op comparison. + .{ .name = "timg1_bus_clock", .arg = 0, .idf = idfTimgOff, .ours = ourTimgOff }, + .{ .name = "systimer_bus_clock", .arg = 0, .idf = idfSystimerOff, .ours = ourSystimerOff }, + .{ .name = "uart1_bus_clock", .arg = 0, .idf = idfUartOff, .ours = ourUartOff }, + .{ .name = "timg1_reset", .idf = idfTimgReset, .ours = ourTimgReset }, + .{ .name = "systimer_reset", .idf = idfSystimerReset, .ours = ourSystimerReset }, + // Last, so the block is left with everything on regardless of which side ran last. + .{ .name = "timg1_bus_clock", .arg = 1, .idf = idfTimgOn, .ours = ourTimgOn }, + .{ .name = "systimer_bus_clock", .arg = 1, .idf = idfSystimerOn, .ours = ourSystimerOn }, + .{ .name = "uart1_bus_clock", .arg = 1, .idf = idfUartOn, .ours = ourUartOn }, + }, +}; + +fn idfTimgOff() void { + oracle_clkrst_timg_bus_clock(1, 0); +} +fn ourTimgOff() void { + hal.clkrst.setClockEnabled(.timg1, false); +} +fn idfTimgOn() void { + oracle_clkrst_timg_bus_clock(1, 1); +} +fn ourTimgOn() void { + hal.clkrst.setClockEnabled(.timg1, true); +} +fn idfSystimerOff() void { + oracle_clkrst_systimer_bus_clock(0); +} +fn ourSystimerOff() void { + hal.clkrst.setClockEnabled(.systimer, false); +} +fn idfSystimerOn() void { + oracle_clkrst_systimer_bus_clock(1); +} +fn ourSystimerOn() void { + hal.clkrst.setClockEnabled(.systimer, true); +} +fn idfUartOff() void { + oracle_clkrst_uart_bus_clock(1, 0); +} +fn ourUartOff() void { + hal.clkrst.setClockEnabled(.uart1, false); +} +fn idfUartOn() void { + oracle_clkrst_uart_bus_clock(1, 1); +} +fn ourUartOn() void { + hal.clkrst.setClockEnabled(.uart1, true); +} + +/// The reset pairing, which is the other half of the table and the half that was right. IDF pulses +/// the bit and returns; so does ours, except for the timer groups, where it additionally clears the +/// flash-boot watchdog protection that the reset re-arms - so a difference in the WDT register is +/// expected and lives outside this window, while HP_RST_EN1 itself must match. +fn idfTimgReset() void { + oracle_clkrst_timg_reset(1); +} +fn ourTimgReset() void { + hal.clkrst.resetPeripheral(.timg1); +} +fn idfSystimerReset() void { + oracle_clkrst_systimer_reset(); +} +fn ourSystimerReset() void { + hal.clkrst.resetPeripheral(.systimer); +} diff --git a/src/oracle/clkrst_ref.c b/src/oracle/clkrst_ref.c new file mode 100644 index 0000000..b35436c --- /dev/null +++ b/src/oracle/clkrst_ref.c @@ -0,0 +1,55 @@ +/* ESP-IDF's own bus-clock and reset control, as the reference for src/hal/clkrst.zig. + * + * This suite exists because of a specific bug. `clkrst.zig` maps each peripheral to the register and + * bit that gate and reset it, and that table is hand-written: the field macros do not say which + * register they live in, so every row is a transcription from ESP-IDF's LL source. Two rows were + * wrong. The second batch - timg0, timg1, systimer and twai0 - had their APB clock enables in + * HP_SYS_CLKRST_PERI_CLK_CTRL21 instead of SOC_CLK_CTRL2, so `setClockEnabled` poked a bit of an + * unrelated register. + * + * It survived a hardware test, which is the point. `examples/halcheck.zig` printed exactly the + * expected `twai0 boot=0 on=1 off=0`, because the write and the read-back both went through the same + * wrong address: a self-consistent test that would have passed with the chip unplugged. + * + * IDF reaches these bits by a completely independent path - its own generated struct definitions - + * so comparing against it is the check that a read-back cannot be. + */ + +/* IDF shadows every clock/reset LL function with a macro that references this identifier, which it + * deliberately never defines, so that an unguarded call fails to compile: the only legal caller + * holds a spinlock. There is no FreeRTOS here and core 1 is held in reset at power-on, so declaring + * the name is exactly as safe as the lock would be. IDF's own bootloader does the same thing + * (bootloader_support/src/bootloader_console.c:53). */ +static int __DECLARE_RCC_ATOMIC_ENV __attribute__((unused)); +/* IDF uses a second name for the same trick on the peripherals whose gate lives in a register shared + * with the CPU's own clocking - systimer among them. Same reasoning applies. */ +static int __DECLARE_RCC_RC_ATOMIC_ENV __attribute__((unused)); + +#include "hal/timg_ll.h" +#include "hal/systimer_ll.h" +#include "hal/uart_ll.h" + +void oracle_clkrst_timg_bus_clock(unsigned group, int enable) +{ + _timg_ll_enable_bus_clock(group, enable != 0); +} + +void oracle_clkrst_timg_reset(unsigned group) +{ + _timg_ll_reset_register(group); +} + +void oracle_clkrst_systimer_bus_clock(int enable) +{ + systimer_ll_enable_bus_clock(enable != 0); +} + +void oracle_clkrst_systimer_reset(void) +{ + systimer_ll_reset_register(); +} + +void oracle_clkrst_uart_bus_clock(unsigned port, int enable) +{ + _uart_ll_enable_bus_clock(port, enable != 0); +} diff --git a/src/oracle/differ_types.zig b/src/oracle/differ_types.zig new file mode 100644 index 0000000..6f8e03c --- /dev/null +++ b/src/oracle/differ_types.zig @@ -0,0 +1,72 @@ +//! The contract between the differential harness and a peripheral under test. +//! +//! Adding a peripheral to the oracle is three files and no edits to the harness: +//! +//! src/oracle/_ref.c external-linkage wrappers over ESP-IDF's `*_ll.h` functions +//! src/oracle/_cases.zig a `descriptor` and a `cases` array, both of the types below +//! src/hal/.zig this project's implementation, which is what is being tested +//! +//! The harness then, for every case: brings the peripheral to a known state, runs ESP-IDF's version, +//! photographs the register block, restores, runs ours, photographs again, and compares. + +/// Everything the harness needs to test a peripheral without breaking the board. +pub const Peripheral = struct { + name: [*:0]const u8, + + /// First address of the register block, and how many 32-bit words to compare. + base: u32, + words: u32, + + /// Word offsets that must never be *read*, because reading them changes hardware state. + /// + /// This cannot be derived from the headers: `UART_FIFO_REG` sits at offset 0 of every UART + /// block, its only field is annotated `RO`, and reading it pops the RX FIFO. A generic + /// block-snapshot loop over a UART eats received bytes - including on the console. + no_read: []const u32 = &.{}, + + /// Word offsets whose value legitimately changes between two runs: counters, FIFO depths, live + /// input levels. Compared they would produce noise, so they are excluded. + volatile_words: []const u32 = &.{}, + + /// The bus-clock enable bit that must read 1 for a snapshot to mean anything. + /// + /// Reading a clock-gated block does not fault and does not return zeros - it returns the last + /// value latched, so two snapshots of a gated peripheral can compare *equal* while describing + /// nothing. The harness checks this before every comparison and fails the case if it is clear. + clock: ?Bit = null, + + /// How to return the peripheral to a known state between the two implementations. + restore: Restore, + + pub const Bit = struct { reg: u32, bit: u5 }; + + pub const Restore = union(enum) { + /// Pulse the peripheral's reset bit in HP_SYS_CLKRST. The only sound restore for a block + /// with write-to-trigger or write-only fields, because it is what the datasheet defines the + /// reset values against. Writing a snapshot back is *not* an option: ~10% of this chip's + /// fields perform an action when written, and writing one saved word back to a UART's + /// offset 0 transmits a character. + reset_bit: Bit, + /// A function that configures the block to a fixed state. For peripherals with no reset bit + /// of their own (GPIO, IO_MUX) or where resetting would take the console with it (UART0). + configure: *const fn () void, + }; +}; + +/// One operation, expressed twice: ESP-IDF's way and ours. They must be the same operation with the +/// same arguments, or the comparison means nothing. +pub const Case = struct { + name: [*:0]const u8, + /// Printed with the result, so a failure names the arguments that produced it. + arg: u32 = 0, + idf: *const fn () void, + ours: *const fn () void, +}; + +/// What a `_cases.zig` module must expose. +pub const Suite = struct { + descriptor: Peripheral, + cases: []const Case, + /// Run once before the suite: bring the peripheral far enough up that its registers are live. + setup: ?*const fn () void = null, +}; 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); +} diff --git a/src/oracle/gpio_ref.c b/src/oracle/gpio_ref.c new file mode 100644 index 0000000..b4070a2 --- /dev/null +++ b/src/oracle/gpio_ref.c @@ -0,0 +1,116 @@ +/* The reference implementation, which is ESP-IDF's own. + * + * ESP-IDF's `*_ll.h` headers are `static inline` functions over the same registers this project's + * Zig HAL drives. Compiled by Zig's clang for riscv32-freestanding they link into the same image as + * the Zig code, which is what makes a differential test possible at all: one binary, one boot, one + * set of clocks, both implementations, and the diff taken on the die. + * + * These wrappers exist only to give the inline functions external linkage so Zig can call them. + * There is no logic here - anything clever in this file would be a third implementation to doubt. + */ + +/* IDF's clock and reset LL functions are shadowed by a wrapper macro that references + * `__DECLARE_RCC_ATOMIC_ENV`, an identifier IDF never defines anywhere; its purpose is to make an + * unguarded call fail to compile, because the only legal caller holds a spinlock. There is no + * FreeRTOS here, and core 1 is held in reset at power-on, so declaring the name is exactly as safe + * as the spinlock would be - and it is what IDF's own bootloader does + * (bootloader_support/src/bootloader_console.c:53 declares a dummy local for the same reason). */ +static int __DECLARE_RCC_ATOMIC_ENV __attribute__((unused)); + +#include "hal/gpio_ll.h" +#include "soc/gpio_struct.h" +#include "soc/io_mux_struct.h" + +/* Whether this translation unit was built with the ROM path switched on. The harness prints it, so + * that a differential run can never silently be "my registers versus the mask ROM". */ +int oracle_gpio_uses_rom_api(void) +{ +#if HAL_CONFIG(GPIO_USE_ROM_API) + return 1; +#else + return 0; +#endif +} + +void oracle_gpio_set_level(unsigned pin, unsigned level) +{ + gpio_ll_set_level(&GPIO, pin, level); +} + +int oracle_gpio_get_level(unsigned pin) +{ + return gpio_ll_get_level(&GPIO, pin); +} + +void oracle_gpio_output_enable(unsigned pin) +{ + gpio_ll_output_enable(&GPIO, pin); +} + +void oracle_gpio_output_disable(unsigned pin) +{ + gpio_ll_output_disable(&GPIO, pin); +} + +void oracle_gpio_input_enable(unsigned pin) +{ + gpio_ll_input_enable(&GPIO, pin); +} + +void oracle_gpio_input_disable(unsigned pin) +{ + gpio_ll_input_disable(&GPIO, pin); +} + +void oracle_gpio_func_sel(unsigned pin, unsigned func) +{ + gpio_ll_func_sel(&GPIO, pin, func); +} + +void oracle_gpio_set_drive(unsigned pin, unsigned strength) +{ + gpio_ll_set_drive_capability(&GPIO, pin, (gpio_drive_cap_t)strength); +} + +void oracle_gpio_pullup_en(unsigned pin) +{ + gpio_ll_pullup_en(&GPIO, pin); +} + +void oracle_gpio_pullup_dis(unsigned pin) +{ + gpio_ll_pullup_dis(&GPIO, pin); +} + +void oracle_gpio_pulldown_en(unsigned pin) +{ + gpio_ll_pulldown_en(&GPIO, pin); +} + +void oracle_gpio_pulldown_dis(unsigned pin) +{ + gpio_ll_pulldown_dis(&GPIO, pin); +} + +/* Open drain, which lives in the GPIO block's own per-pin register (GPIO_PINn_PAD_DRIVER) rather + * than in the IO MUX pad register - a different register file for the same pad. The I2C HAL needs it + * because that bus is wired-AND, and a pin left push-pull shorts a shared bus against another + * device's driver. One bit, and expensive to get wrong. */ +void oracle_gpio_od_enable(unsigned pin) +{ + gpio_ll_od_enable(&GPIO, pin); +} + +void oracle_gpio_od_disable(unsigned pin) +{ + gpio_ll_od_disable(&GPIO, pin); +} + +/* Route a peripheral signal to a pad through the GPIO matrix. This is the one GPIO operation with a + * real sequence rather than a single field write, and therefore the one where a write-trace + * comparison can find something a state comparison cannot. */ +void oracle_gpio_matrix_out(unsigned pin, unsigned signal) +{ + gpio_ll_set_output_signal_matrix_source(&GPIO, pin, signal, false); + gpio_ll_set_output_enable_ctrl(&GPIO, pin, true, false); +} diff --git a/src/oracle/i2c_cases.zig b/src/oracle/i2c_cases.zig new file mode 100644 index 0000000..0546066 --- /dev/null +++ b/src/oracle/i2c_cases.zig @@ -0,0 +1,627 @@ +//! I2C's side of the differential test: the same operations expressed as ESP-IDF's LL calls and as +//! this project's HAL calls. +//! +//! Two suites, because this peripheral's state lives in two register blocks that are 0x24000 bytes +//! apart and the harness compares one window per suite: +//! +//! * `suite` - the I2C0 block itself (0x500C4000, 128 words). Timing, FIFOs, the command list, +//! the filter, the timeout. +//! * `clock_suite` - the two HP_SYS_CLKRST words that hold I2C's controller clock: source select, +//! clock enable and the divider, for *both* ports (HP_SYS_CLKRST_PERI_CLK_CTRL10/11). Without +//! this second window the divider half of `setBusTiming` would be untested, because the divider +//! write does not land in the I2C block at all. Registering only the first suite would leave a +//! bus that is a factor of `clkm_div` too fast with nothing to notice. +//! +//! Restore differs between the two, and both choices are forced: +//! +//! * The I2C block is restored by its **reset bit**. It has three write-to-trigger fields +//! (`trans_start`, `fsm_rst`, `conf_upgate`) and a self-setting `command_done` per slot, so +//! writing a snapshot back would trigger a transaction. HP_SYS_CLKRST's reset bit is what the +//! datasheet defines the reset values against, and I2C0 carries nothing this board needs - no +//! console, no flash - so pulsing it is safe. +//! * The clock words cannot be reset that way: they are in HP_SYS_CLKRST, not in the I2C block, and +//! PERI_CLK_CTRL11 also holds three I2S0_RX clock fields. Restore there is a configure function +//! that writes only I2C's own fields back to their documented reset value of zero. +//! +//! The restore function deliberately builds its field descriptors from the macros itself rather than +//! calling into `hal.i2c`: a restore that shared the HAL's idea of where a field lives would agree +//! with a HAL that had it wrong, and the case would pass while configuring the wrong bits. Same +//! reason `i2c_ref.c` maps command *kinds* to IDF's `I2C_LL_CMD_*` macros instead of taking an +//! opcode number from Zig. + +const std = @import("std"); +const hal = @import("hal"); +const regs = @import("regs"); +const mmio = @import("mmio"); +const types = @import("differ_types.zig"); + +const Reg = mmio.Reg; +const Field = mmio.Field; + +extern fn oracle_i2c_enable_bus_clock(port: c_int, enable: c_int) void; +extern fn oracle_i2c_reset_register(port: c_int) void; +extern fn oracle_i2c_enable_controller_clock(port: c_int, enable: c_int) void; +extern fn oracle_i2c_set_source_clk(port: c_int, src: c_int) void; +extern fn oracle_i2c_master_init(port: c_int) void; +extern fn oracle_i2c_set_mode_master(port: c_int) void; +extern fn oracle_i2c_enable_pins_open_drain(port: c_int, enable_od: c_int) void; +extern fn oracle_i2c_update(port: c_int) void; +extern fn oracle_i2c_fsm_rst(port: c_int) void; +extern fn oracle_i2c_set_bus_timing(port: c_int, source_hz: c_uint, bus_hz: c_uint) void; +extern fn oracle_i2c_set_start_timing(port: c_int, setup: c_int, hold: c_int) void; +extern fn oracle_i2c_set_stop_timing(port: c_int, setup: c_int, hold: c_int) void; +extern fn oracle_i2c_set_sda_timing(port: c_int, sample: c_int, hold: c_int) void; +extern fn oracle_i2c_set_tout(port: c_int, tout: c_int) void; +extern fn oracle_i2c_set_scl_timeout_us(port: c_int, source_hz: c_uint, timeout_us: c_uint) void; +extern fn oracle_i2c_set_filter(port: c_int, filter_num: c_uint) void; +extern fn oracle_i2c_txfifo_rst(port: c_int) void; +extern fn oracle_i2c_rxfifo_rst(port: c_int) void; +extern fn oracle_i2c_enable_fifo_mode(port: c_int, fifo_mode_en: c_int) void; +extern fn oracle_i2c_set_fifo_thresholds(port: c_int, tx_empty: c_uint, rx_full: c_uint) void; +extern fn oracle_i2c_write_txfifo_pattern(port: c_int, len: c_uint) void; +extern fn oracle_i2c_write_cmd( + port: c_int, + slot: c_int, + kind: c_uint, + byte_num: c_uint, + ack_en: c_int, + ack_exp: c_int, + ack_val: c_int, +) void; +extern fn oracle_i2c_clear_intr_mask(port: c_int, mask: c_uint) void; +extern fn oracle_i2c_disable_intr_mask(port: c_int, mask: c_uint) void; +extern fn oracle_i2c_get_hw_version(port: c_int) c_uint; +extern fn oracle_i2c_cmd_reg_num() c_uint; +extern fn oracle_i2c_fifo_len() c_uint; + +/// ESP-IDF's own view of two chip constants this HAL hard-codes. The harness prints them; a +/// disagreement means `hal.i2c.cmd_slots` or `fifo_len` was read out of the wrong chip's header, +/// which is a mistake no register comparison would ever show. +pub fn idfCmdSlots() u32 { + return oracle_i2c_cmd_reg_num(); +} + +pub fn idfFifoLen() u32 { + return oracle_i2c_fifo_len(); +} + +pub fn hardwareVersion() u32 { + return oracle_i2c_get_hw_version(0); +} + +comptime { + // These are constants in both implementations, so they can be checked here rather than on the + // die - but only against the *header*, which is why the runtime accessors above exist too. + if (hal.i2c.cmd_slots != 8) @compileError("this chip has eight command slots"); + if (hal.i2c.fifo_len != 32) @compileError("this chip's I2C FIFO is 32 bytes"); +} + +// There is no module-level "port under test" variable here, unlike the GPIO suite's `pin`, and the +// reason is in the descriptors: a `Peripheral` carries one `base` and one `clock`, both constants, +// so the I2C-block suite is pinned to I2C0 by construction and running it "for port 1" would need a +// second descriptor rather than a variable. Nothing is lost by that, because the only per-port +// arithmetic in this peripheral is which HP_SYS_CLKRST field a port's clock lives in - and both +// ports' fields are inside `clock_suite`'s two-word window, where the cases name the port directly. + +/// 40 MHz crystal, which is what `Timing.calculate` is fed on both sides. Not a measurement: the +/// board's crystal, and the P4's only XTAL frequency. +const source_hz: u32 = hal.i2c.xtal_hz; + +// --------------------------------------------------------------------------- the I2C0 block + +fn resetI2c0() void { + // Same pulse the harness's `.reset_bit` restore performs, for `setup` to use before the first + // case. Interrupt-masked because HP_RST_EN1 holds every peripheral's reset bit. + const guard = hal.clkrst.maskInterrupts(); + defer guard.release(); + const r = Reg.at(regs.HP_SYS_CLKRST_HP_RST_EN1_REG); + const bit = @as(u32, 1) << @intCast(regs.HP_SYS_CLKRST_REG_RST_EN_I2C0_S); + r.writeRaw(r.raw() | bit); + r.writeRaw(r.raw() & ~bit); +} + +/// Bring I2C0 far enough up that its registers are live and its state machine is clocked. +/// +/// The APB gate defaults to 1 on this chip so the registers are readable from boot, but the +/// *controller* clock defaults to 0 - and that one is in HP_SYS_CLKRST, outside the block, so the +/// reset-bit restore between cases does not disturb it. +fn setupI2c0() void { + hal.clkrst.setClockEnabled(.i2c0, true); + hal.i2c.setControllerClockEnabled(0, true); + resetI2c0(); +} + +pub const suite: types.Suite = .{ + .descriptor = .{ + .name = "i2c", + .base = @intCast(regs.I2C_SCL_LOW_PERIOD_REG(0)), // I2C0 + 0x000 + // 128 words = 0x200 bytes, which is the whole instance: configuration and the command list + // end at +0x84, the version word is at +0xf8, and the two 32-byte FIFO RAMs are at +0x100 + // (TX) and +0x180 (RX). The RAMs are in the window on purpose - a TX FIFO write is otherwise + // observable only as a count in I2C_SR, and a count is a much weaker witness than the bytes + // themselves. If those words ever turn out to read unstably in FIFO mode - ESP-IDF only ever + // touches them in non-FIFO mode - they belong in `volatile_words`, not out of the window. + .words = 128, + // Reading I2C_DATA_REG pops the RX FIFO. The register header gives no hint of it: the only + // field is annotated `HRO` and described as "Rx FIFO read data" (i2c_reg.h:464-474). What + // settles it is that `i2c_ll_read_rxfifo` reads this one address `len` times and expects + // `len` different bytes (i2c_ll.h:691-697), which is only possible if the read advances the + // FIFO - and `i2c_ll_write_txfifo` writes the same address to fill the *other* FIFO + // (i2c_ll.h:674-680). Same shape as UART_FIFO_REG. A snapshot loop that reads it would eat + // received bytes and desynchronise the read pointer under the case being measured. + .no_read = &.{hal.i2c.data_word_offset}, + .clock = .{ + .reg = @intCast(regs.HP_SYS_CLKRST_SOC_CLK_CTRL2_REG), + .bit = @intCast(regs.HP_SYS_CLKRST_REG_I2C0_APB_CLK_EN_S), + }, + .restore = .{ .reset_bit = .{ + .reg = @intCast(regs.HP_SYS_CLKRST_HP_RST_EN1_REG), + .bit = @intCast(regs.HP_SYS_CLKRST_REG_RST_EN_I2C0_S), + } }, + }, + .cases = &.{ + // ---- bus timing. Five frequencies, chosen for the branches rather than for roundness. + // 100 kHz and 400 kHz are the two speeds every device supports; 1 MHz is fast-mode-plus, + // where half_cycle is down to 20 source cycles and the minus-one asymmetries dominate; + // 50 kHz and 10 kHz are on the other side of the 80 kHz boundary where the scl_wait_high + // split changes formula (i2c_ll.h:112-115); and 10 kHz is the one that needs a controller + // clock divider greater than 1 - the half that this window cannot see, which is what + // `clock_suite` is for. + .{ .name = "bus_timing_100k", .arg = 100_000, .idf = idfTiming100k, .ours = ourTiming100k }, + .{ .name = "bus_timing_400k", .arg = 400_000, .idf = idfTiming400k, .ours = ourTiming400k }, + .{ .name = "bus_timing_1M", .arg = 1_000_000, .idf = idfTiming1M, .ours = ourTiming1M }, + .{ .name = "bus_timing_50k", .arg = 50_000, .idf = idfTiming50k, .ours = ourTiming50k }, + .{ .name = "bus_timing_10k", .arg = 10_000, .idf = idfTiming10k, .ours = ourTiming10k }, + + // ---- master bring-up, and the open-drain polarity on its own. + .{ .name = "master_init", .idf = idfMasterInit, .ours = ourMasterInit }, + .{ .name = "pins_open_drain", .arg = 1, .idf = idfOpenDrainOn, .ours = ourOpenDrainOn }, + .{ .name = "pins_push_pull", .arg = 0, .idf = idfOpenDrainOff, .ours = ourOpenDrainOff }, + .{ .name = "fifo_mode", .arg = 1, .idf = idfFifoMode, .ours = ourFifoMode }, + .{ .name = "nonfifo_mode", .arg = 0, .idf = idfNonFifoMode, .ours = ourNonFifoMode }, + + // ---- FIFOs. The resets are two stores each (the bit is not self-clearing), so a + // half-done reset shows up as a FIFO held in reset rather than as a wrong value. + .{ .name = "txfifo_rst", .idf = idfTxFifoRst, .ours = ourTxFifoRst }, + .{ .name = "rxfifo_rst", .idf = idfRxFifoRst, .ours = ourRxFifoRst }, + .{ .name = "txfifo_write", .arg = 4, .idf = idfWrite4, .ours = ourWrite4 }, + .{ .name = "txfifo_write", .arg = 31, .idf = idfWrite31, .ours = ourWrite31 }, + .{ .name = "fifo_thresholds", .arg = 8, .idf = idfThresholds, .ours = ourThresholds }, + + // ---- filter. Three cases because "off" is not "on with a threshold of zero": both enables + // default to 1 with zero thresholds, so disabling has to clear the enables and leave the + // thresholds alone (i2c_ll.h:753-764). + .{ .name = "filter_7", .arg = 7, .idf = idfFilter7, .ours = ourFilter7 }, + .{ .name = "filter_15", .arg = 15, .idf = idfFilter15, .ours = ourFilter15 }, + .{ .name = "filter_off", .arg = 0, .idf = idfFilter0, .ours = ourFilter0 }, + + // ---- timeout. The field is five bits and holds an *exponent*: the bus times out after + // 2^value source-clock cycles, so 12 is 102 us at 40 MHz and 31 is the largest the register + // can hold. The third case goes through the microsecond conversion IDF's driver uses + // (i2c_ll.h:1060-1065) for its documented 2000 us default, which comes out as 17. + .{ .name = "tout_12", .arg = 12, .idf = idfTout12, .ours = ourTout12 }, + .{ .name = "tout_31", .arg = 31, .idf = idfTout31, .ours = ourTout31 }, + .{ .name = "scl_timeout_us", .arg = 2000, .idf = idfSclTimeoutUs, .ours = ourSclTimeoutUs }, + + // ---- the explicit timing setters, where IDF's minus-one convention is least uniform: + // start setup as given but start hold minus one, stop and sda both as given. + .{ .name = "start_timing", .arg = 7, .idf = idfStartTiming, .ours = ourStartTiming }, + .{ .name = "stop_timing", .arg = 5, .idf = idfStopTiming, .ours = ourStopTiming }, + .{ .name = "sda_timing", .arg = 11, .idf = idfSdaTiming, .ours = ourSdaTiming }, + + // ---- the command list, one opcode per slot. The IDF side names the opcode + // (`I2C_LL_CMD_*`) and the ours side names it too (`Op.restart`), so the *numbers* are never + // passed across: this chip's register header documents the pre-C3 numbering, and a test that + // handed the number over would agree with a wrong constant instead of catching it. + .{ .name = "cmd_restart", .arg = 0, .idf = idfCmdRestart, .ours = ourCmdRestart }, + .{ .name = "cmd_write_ack", .arg = 5, .idf = idfCmdWrite, .ours = ourCmdWrite }, + .{ .name = "cmd_read_ack", .arg = 3, .idf = idfCmdReadAck, .ours = ourCmdReadAck }, + .{ .name = "cmd_read_nack", .arg = 1, .idf = idfCmdReadNack, .ours = ourCmdReadNack }, + .{ .name = "cmd_stop", .arg = 0, .idf = idfCmdStop, .ours = ourCmdStop }, + .{ .name = "cmd_end", .arg = 0, .idf = idfCmdEnd, .ours = ourCmdEnd }, + .{ .name = "cmd_list_write", .arg = 4, .idf = idfCmdListWrite, .ours = ourCmdListWrite }, + + // ---- interrupt state. Not an interrupt-driven driver - this HAL polls - but the clear + // register is write-1-to-clear, so getting it wrong (a read-modify-write instead of a raw + // store) is a class of bug worth one case. + .{ .name = "clear_intr", .idf = idfClearIntr, .ours = ourClearIntr }, + .{ .name = "disable_intr", .idf = idfDisableIntr, .ours = ourDisableIntr }, + }, + .setup = setupI2c0, +}; + +// ---- bus timing -------------------------------------------------------------------------------- +// Each pair is IDF's calculate-and-write (i2c_hal.c:27-32) against ours (hal.i2c.setBusTiming). The +// comparison covers ten in-block registers at once, so a single wrong subtraction anywhere in the +// derivation shows up here. + +fn idfTiming100k() void { + oracle_i2c_set_bus_timing(0, source_hz, 100_000); +} +fn ourTiming100k() void { + hal.i2c.setBusTiming(0, source_hz, 100_000); +} +fn idfTiming400k() void { + oracle_i2c_set_bus_timing(0, source_hz, 400_000); +} +fn ourTiming400k() void { + hal.i2c.setBusTiming(0, source_hz, 400_000); +} +fn idfTiming1M() void { + oracle_i2c_set_bus_timing(0, source_hz, 1_000_000); +} +fn ourTiming1M() void { + hal.i2c.setBusTiming(0, source_hz, 1_000_000); +} +fn idfTiming50k() void { + oracle_i2c_set_bus_timing(0, source_hz, 50_000); +} +fn ourTiming50k() void { + hal.i2c.setBusTiming(0, source_hz, 50_000); +} +fn idfTiming10k() void { + oracle_i2c_set_bus_timing(0, source_hz, 10_000); +} +fn ourTiming10k() void { + hal.i2c.setBusTiming(0, source_hz, 10_000); +} + +// ---- bring-up ---------------------------------------------------------------------------------- + +fn idfMasterInit() void { + oracle_i2c_master_init(0); +} +fn ourMasterInit() void { + hal.i2c.initMaster(0); +} +fn idfOpenDrainOn() void { + oracle_i2c_enable_pins_open_drain(0, 1); +} +fn ourOpenDrainOn() void { + hal.i2c.setPinsOpenDrain(0, true); +} +fn idfOpenDrainOff() void { + oracle_i2c_enable_pins_open_drain(0, 0); +} +fn ourOpenDrainOff() void { + hal.i2c.setPinsOpenDrain(0, false); +} +fn idfFifoMode() void { + oracle_i2c_enable_fifo_mode(0, 1); +} +fn ourFifoMode() void { + hal.i2c.setFifoMode(0, true); +} +fn idfNonFifoMode() void { + oracle_i2c_enable_fifo_mode(0, 0); +} +fn ourNonFifoMode() void { + hal.i2c.setFifoMode(0, false); +} + +// ---- FIFOs ------------------------------------------------------------------------------------- + +fn idfTxFifoRst() void { + oracle_i2c_txfifo_rst(0); +} +fn ourTxFifoRst() void { + hal.i2c.resetTxFifo(0); +} +fn idfRxFifoRst() void { + oracle_i2c_rxfifo_rst(0); +} +fn ourRxFifoRst() void { + hal.i2c.resetRxFifo(0); +} + +/// The same pattern `oracle_i2c_write_txfifo_pattern` generates: 0xA0 + i, so every byte differs +/// from its neighbours and from the 0x00/0xFF a broken FIFO produces. +const pattern: [hal.i2c.fifo_len]u8 = blk: { + var p: [hal.i2c.fifo_len]u8 = undefined; + for (&p, 0..) |*b, i| b.* = 0xA0 + @as(u8, @intCast(i)); + break :blk p; +}; + +fn idfWrite4() void { + oracle_i2c_write_txfifo_pattern(0, 4); +} +fn ourWrite4() void { + hal.i2c.writeTxFifo(0, pattern[0..4]); +} +// 31 bytes rather than 32: one short of full, so the case cannot be passed by a FIFO that silently +// wrapped and cannot trip the overflow protection either. +fn idfWrite31() void { + oracle_i2c_write_txfifo_pattern(0, 31); +} +fn ourWrite31() void { + hal.i2c.writeTxFifo(0, pattern[0..31]); +} +fn idfThresholds() void { + oracle_i2c_set_fifo_thresholds(0, 8, 20); +} +fn ourThresholds() void { + hal.i2c.setFifoThresholds(0, 8, 20); +} + +// ---- filter and timeout ------------------------------------------------------------------------ + +fn idfFilter7() void { + oracle_i2c_set_filter(0, 7); +} +fn ourFilter7() void { + hal.i2c.setFilter(0, 7); +} +fn idfFilter15() void { + oracle_i2c_set_filter(0, 15); +} +fn ourFilter15() void { + hal.i2c.setFilter(0, 15); +} +fn idfFilter0() void { + oracle_i2c_set_filter(0, 0); +} +fn ourFilter0() void { + hal.i2c.setFilter(0, 0); +} +fn idfTout12() void { + oracle_i2c_set_tout(0, 12); +} +fn ourTout12() void { + hal.i2c.setTimeout(0, 12); +} +fn idfTout31() void { + oracle_i2c_set_tout(0, 31); +} +fn ourTout31() void { + hal.i2c.setTimeout(0, 31); +} +fn idfSclTimeoutUs() void { + oracle_i2c_set_scl_timeout_us(0, source_hz, 2000); +} +fn ourSclTimeoutUs() void { + hal.i2c.setTimeout(0, hal.i2c.timeoutExponent(source_hz, 2000)); +} + +// ---- explicit timing setters ------------------------------------------------------------------- +// The same registers `applyTiming` writes, but reached by IDF's three narrow setters, whose +// minus-one convention is *different* from the one in the calculate-and-write path: start setup as +// given and start hold minus one, both stop values as given, both sda values as given +// (i2c_ll.h:452-486 against i2c_ll.h:210-217). Numbers with no relation to any real bus frequency, +// so a HAL that quietly recomputed them from a frequency instead of writing what it was given would +// show up here rather than passing. + +fn idfStartTiming() void { + oracle_i2c_set_start_timing(0, 7, 9); +} +fn ourStartTiming() void { + hal.i2c.setStartTiming(0, 7, 9); +} +fn idfStopTiming() void { + oracle_i2c_set_stop_timing(0, 5, 6); +} +fn ourStopTiming() void { + hal.i2c.setStopTiming(0, 5, 6); +} +fn idfSdaTiming() void { + oracle_i2c_set_sda_timing(0, 11, 3); +} +fn ourSdaTiming() void { + hal.i2c.setSdaTiming(0, 11, 3); +} + +// ---- the command list -------------------------------------------------------------------------- + +// Kind numbers as `i2c_ref.c` reads them: 0 restart, 1 write, 2 read, 3 stop, 4 end. Only the *kind* +// crosses the language boundary; the C side turns it into an opcode with IDF's own macro. +const kind_restart: c_uint = 0; +const kind_write: c_uint = 1; +const kind_read: c_uint = 2; +const kind_stop: c_uint = 3; +const kind_end: c_uint = 4; + +fn idfCmdRestart() void { + oracle_i2c_write_cmd(0, 0, kind_restart, 0, 0, 0, 0); +} +fn ourCmdRestart() void { + hal.i2c.writeCommand(0, 0, .{ .op = .restart }); +} +fn idfCmdWrite() void { + oracle_i2c_write_cmd(0, 1, kind_write, 5, 1, 0, 0); +} +fn ourCmdWrite() void { + hal.i2c.writeCommand(0, 1, .{ .op = .write, .bytes = 5, .ack_check = true }); +} +fn idfCmdReadAck() void { + oracle_i2c_write_cmd(0, 2, kind_read, 3, 0, 0, 0); +} +fn ourCmdReadAck() void { + hal.i2c.writeCommand(0, 2, .{ .op = .read, .bytes = 3, .ack_value = 0 }); +} +fn idfCmdReadNack() void { + oracle_i2c_write_cmd(0, 3, kind_read, 1, 0, 0, 1); +} +fn ourCmdReadNack() void { + hal.i2c.writeCommand(0, 3, .{ .op = .read, .bytes = 1, .ack_value = 1 }); +} +fn idfCmdStop() void { + oracle_i2c_write_cmd(0, 4, kind_stop, 0, 0, 0, 0); +} +fn ourCmdStop() void { + hal.i2c.writeCommand(0, 4, .{ .op = .stop }); +} +fn idfCmdEnd() void { + oracle_i2c_write_cmd(0, 5, kind_end, 0, 0, 0, 0); +} +fn ourCmdEnd() void { + hal.i2c.writeCommand(0, 5, .{ .op = .end }); +} + +/// A whole list, in the shape `hal.i2c.write` builds for a four-byte transfer: RSTART, WRITE of +/// 1 + 4 bytes with ACK checking, STOP. Slots 3 to 7 keep the reset value on both sides. +fn idfCmdListWrite() void { + oracle_i2c_write_cmd(0, 0, kind_restart, 0, 0, 0, 0); + oracle_i2c_write_cmd(0, 1, kind_write, 5, 1, 0, 0); + oracle_i2c_write_cmd(0, 2, kind_stop, 0, 0, 0, 0); +} +fn ourCmdListWrite() void { + hal.i2c.writeCommands(0, &.{ + .{ .op = .restart }, + .{ .op = .write, .bytes = 5, .ack_check = true }, + .{ .op = .stop }, + }); +} + +// ---- interrupt state --------------------------------------------------------------------------- + +fn idfClearIntr() void { + oracle_i2c_clear_intr_mask(0, hal.i2c.all_interrupts); +} +fn ourClearIntr() void { + hal.i2c.clearInterrupts(0, hal.i2c.all_interrupts); +} +fn idfDisableIntr() void { + oracle_i2c_disable_intr_mask(0, hal.i2c.all_interrupts); +} +fn ourDisableIntr() void { + hal.i2c.disableInterrupts(0); +} + +// ------------------------------------------------------- the clock domain: HP_SYS_CLKRST words +// +// Field descriptors built here rather than borrowed from hal.i2c, on purpose: the restore function +// below must not share the HAL's idea of where these fields live, or a HAL with a field in the wrong +// place would be restored consistently with its own mistake and every case would pass. + +const peri_clk_ctrl10 = Reg.at(regs.HP_SYS_CLKRST_PERI_CLK_CTRL10_REG); +const peri_clk_ctrl11 = Reg.at(regs.HP_SYS_CLKRST_PERI_CLK_CTRL11_REG); + +const i2c0_clock_fields = [_]Field{ + Field.of(regs.HP_SYS_CLKRST_REG_I2C0_CLK_SRC_SEL_S, regs.HP_SYS_CLKRST_REG_I2C0_CLK_SRC_SEL_V), + Field.of(regs.HP_SYS_CLKRST_REG_I2C0_CLK_EN_S, regs.HP_SYS_CLKRST_REG_I2C0_CLK_EN_V), + Field.of(regs.HP_SYS_CLKRST_REG_I2C0_CLK_DIV_NUM_S, regs.HP_SYS_CLKRST_REG_I2C0_CLK_DIV_NUM_V), + Field.of(regs.HP_SYS_CLKRST_REG_I2C0_CLK_DIV_NUMERATOR_S, regs.HP_SYS_CLKRST_REG_I2C0_CLK_DIV_NUMERATOR_V), + Field.of(regs.HP_SYS_CLKRST_REG_I2C0_CLK_DIV_DENOMINATOR_S, regs.HP_SYS_CLKRST_REG_I2C0_CLK_DIV_DENOMINATOR_V), + Field.of(regs.HP_SYS_CLKRST_REG_I2C1_CLK_SRC_SEL_S, regs.HP_SYS_CLKRST_REG_I2C1_CLK_SRC_SEL_V), + Field.of(regs.HP_SYS_CLKRST_REG_I2C1_CLK_EN_S, regs.HP_SYS_CLKRST_REG_I2C1_CLK_EN_V), +}; + +const i2c1_divider_fields = [_]Field{ + Field.of(regs.HP_SYS_CLKRST_REG_I2C1_CLK_DIV_NUM_S, regs.HP_SYS_CLKRST_REG_I2C1_CLK_DIV_NUM_V), + Field.of(regs.HP_SYS_CLKRST_REG_I2C1_CLK_DIV_NUMERATOR_S, regs.HP_SYS_CLKRST_REG_I2C1_CLK_DIV_NUMERATOR_V), + Field.of(regs.HP_SYS_CLKRST_REG_I2C1_CLK_DIV_DENOMINATOR_S, regs.HP_SYS_CLKRST_REG_I2C1_CLK_DIV_DENOMINATOR_V), +}; + +/// Zero every I2C clock field in the two words, which is their documented reset value +/// (hp_sys_clkrst_reg.h: all ten default to 0), leaving everything else in those words alone. +/// +/// "Everything else" is not empty: PERI_CLK_CTRL11 also holds `REG_I2S0_RX_CLK_EN` and +/// `REG_I2S0_RX_CLK_SRC_SEL` in bits 24-26. Restoring by writing a whole word would take I2S0's +/// receive clock with it, which is exactly the class of collateral damage the harness's +/// no-write-back rule exists to prevent - so this is a masked read-modify-write, interrupt-masked +/// because these registers are shared. +fn restoreI2cClocks() void { + const guard = hal.clkrst.maskInterrupts(); + defer guard.release(); + var mask10: u32 = 0; + for (i2c0_clock_fields) |f| mask10 |= f.mask(); + peri_clk_ctrl10.writeRaw(peri_clk_ctrl10.raw() & ~mask10); + var mask11: u32 = 0; + for (i2c1_divider_fields) |f| mask11 |= f.mask(); + peri_clk_ctrl11.writeRaw(peri_clk_ctrl11.raw() & ~mask11); +} + +/// I2C's controller clock: source, gate and divider, for both ports, in two words. +/// +/// This is the other half of `setBusTiming`. The divider is what keeps `half_cycle` inside the +/// nine-bit period fields at low bus frequencies - 10 kHz needs `clkm_div` 4 - so an implementation +/// that wrote the timing registers correctly and the divider not at all would produce a bus four +/// times too fast and pass every case in the suite above. +pub const clock_suite: types.Suite = .{ + .descriptor = .{ + .name = "i2c_clk", + .base = @intCast(regs.HP_SYS_CLKRST_PERI_CLK_CTRL10_REG), + // Two words: CTRL10 (all of I2C0's clock fields plus I2C1's source select and gate) and + // CTRL11 (I2C1's divider, and three I2S0_RX bits neither side touches). + .words = 2, + // No reset bit for HP_SYS_CLKRST, and no gate in front of it either: it is the block that + // holds every other block's gate. + .restore = .{ .configure = restoreI2cClocks }, + }, + .cases = &.{ + .{ .name = "source_xtal", .arg = 0, .idf = idfSourceXtal0, .ours = ourSourceXtal0 }, + .{ .name = "source_rc_fast", .arg = 0, .idf = idfSourceRcFast0, .ours = ourSourceRcFast0 }, + .{ .name = "source_xtal_p1", .arg = 1, .idf = idfSourceXtal1, .ours = ourSourceXtal1 }, + .{ .name = "source_rc_fast_p1", .arg = 1, .idf = idfSourceRcFast1, .ours = ourSourceRcFast1 }, + .{ .name = "controller_clock_on", .arg = 0, .idf = idfCtrlClkOn0, .ours = ourCtrlClkOn0 }, + .{ .name = "controller_clock_off", .arg = 0, .idf = idfCtrlClkOff0, .ours = ourCtrlClkOff0 }, + .{ .name = "controller_clock_on_p1", .arg = 1, .idf = idfCtrlClkOn1, .ours = ourCtrlClkOn1 }, + // The divider written by the same calculate-and-write pair as the timing cases, at the two + // frequencies either side of where clkm_div stops being 1. + .{ .name = "divider_100k", .arg = 100_000, .idf = idfDiv100k, .ours = ourDiv100k }, + .{ .name = "divider_10k", .arg = 10_000, .idf = idfDiv10k, .ours = ourDiv10k }, + // ... and on port 1, where the divider is in the *other* word from its own source select. + .{ .name = "divider_10k_p1", .arg = 10_000, .idf = idfDiv10kP1, .ours = ourDiv10kP1 }, + }, + .setup = null, +}; + +fn idfSourceXtal0() void { + oracle_i2c_set_source_clk(0, 0); +} +fn ourSourceXtal0() void { + hal.i2c.setSource(0, .xtal); +} +fn idfSourceRcFast0() void { + oracle_i2c_set_source_clk(0, 1); +} +fn ourSourceRcFast0() void { + hal.i2c.setSource(0, .rc_fast); +} +fn idfSourceXtal1() void { + oracle_i2c_set_source_clk(1, 0); +} +fn ourSourceXtal1() void { + hal.i2c.setSource(1, .xtal); +} +fn idfSourceRcFast1() void { + oracle_i2c_set_source_clk(1, 1); +} +fn ourSourceRcFast1() void { + hal.i2c.setSource(1, .rc_fast); +} +fn idfCtrlClkOn0() void { + oracle_i2c_enable_controller_clock(0, 1); +} +fn ourCtrlClkOn0() void { + hal.i2c.setControllerClockEnabled(0, true); +} +fn idfCtrlClkOff0() void { + oracle_i2c_enable_controller_clock(0, 0); +} +fn ourCtrlClkOff0() void { + hal.i2c.setControllerClockEnabled(0, false); +} +fn idfCtrlClkOn1() void { + oracle_i2c_enable_controller_clock(1, 1); +} +fn ourCtrlClkOn1() void { + hal.i2c.setControllerClockEnabled(1, true); +} +fn idfDiv100k() void { + oracle_i2c_set_bus_timing(0, source_hz, 100_000); +} +fn ourDiv100k() void { + hal.i2c.setBusTiming(0, source_hz, 100_000); +} +fn idfDiv10k() void { + oracle_i2c_set_bus_timing(0, source_hz, 10_000); +} +fn ourDiv10k() void { + hal.i2c.setBusTiming(0, source_hz, 10_000); +} +fn idfDiv10kP1() void { + oracle_i2c_set_bus_timing(1, source_hz, 10_000); +} +fn ourDiv10kP1() void { + hal.i2c.setBusTiming(1, source_hz, 10_000); +} diff --git a/src/oracle/i2c_ref.c b/src/oracle/i2c_ref.c new file mode 100644 index 0000000..b569260 --- /dev/null +++ b/src/oracle/i2c_ref.c @@ -0,0 +1,262 @@ +/* I2C's reference implementation, which is ESP-IDF's own. + * + * These wrappers exist only to give IDF's `static inline` LL functions external linkage so Zig can + * call them. There is no logic here - anything clever in this file would be a third implementation + * to doubt - with one deliberate exception, `opcode_of`, explained where it appears. + */ + +/* IDF's clock and reset LL functions are shadowed by a wrapper macro that references + * `__DECLARE_RCC_ATOMIC_ENV`, an identifier IDF never defines anywhere; its purpose is to make an + * unguarded call fail to compile, because the only legal caller holds a spinlock. There is no + * FreeRTOS here and core 1 is held in reset at power-on, so declaring the name is exactly as safe as + * the spinlock would be - and it is what IDF's own bootloader does + * (bootloader_support/src/bootloader_console.c:53 declares a dummy local for the same reason). + * + * For I2C this covers four functions: i2c_ll_enable_bus_clock, i2c_ll_reset_register, + * i2c_ll_set_source_clk and the LP_I2C ones this file does not use. */ +static int __DECLARE_RCC_ATOMIC_ENV __attribute__((unused)); + +#include "hal/i2c_ll.h" + +/* I2C0 and I2C1 are addresses PROVIDEd by soc/esp32p4/ld/esp32p4.peripherals.ld (lines 17-18: + * 0x500C4000 and 0x500C5000), which the build links when -Doracle is passed. Several LL functions + * dispatch on the *pointer* - i2c_ll_master_set_bus_timing compares `hw == &I2C0` to decide which + * HP_SYS_CLKRST divider to write - so passing the right one of these two is load-bearing, and a + * third port cannot be faked. */ +static i2c_dev_t *dev(int port) +{ + return (port == 0) ? &I2C0 : &I2C1; +} + +/* ------------------------------------------------------------------ clocks, reset, bring-up */ + +void oracle_i2c_enable_bus_clock(int port, int enable) +{ + i2c_ll_enable_bus_clock(port, enable != 0); +} + +void oracle_i2c_reset_register(int port) +{ + i2c_ll_reset_register(port); +} + +void oracle_i2c_enable_controller_clock(int port, int enable) +{ + i2c_ll_enable_controller_clock(dev(port), enable != 0); +} + +/* src: 0 = XTAL, 1 = RC_FAST. Passed as IDF's own enum values rather than as the register bit, so a + * wrong bit polarity in the Zig would show up as a difference. */ +void oracle_i2c_set_source_clk(int port, int src) +{ + i2c_ll_set_source_clk(dev(port), (src == 1) ? I2C_CLK_SRC_RC_FAST : I2C_CLK_SRC_XTAL); +} + +/* i2c_hal_master_init, i2c_hal.c:39-50, inlined here because i2c_hal.c is not linked into this + * image - only the LL headers are. The sequence is IDF's, unchanged. */ +void oracle_i2c_master_init(int port) +{ + i2c_dev_t *hw = dev(port); + i2c_ll_set_mode(hw, I2C_BUS_MODE_MASTER); + i2c_ll_enable_pins_open_drain(hw, true); + i2c_ll_enable_arbitration(hw, false); + i2c_ll_master_rx_full_ack_level(hw, false); + i2c_ll_set_data_mode(hw, I2C_DATA_MODE_MSB_FIRST, I2C_DATA_MODE_MSB_FIRST); + i2c_ll_txfifo_rst(hw); + i2c_ll_rxfifo_rst(hw); +} + +void oracle_i2c_set_mode_master(int port) +{ + i2c_ll_set_mode(dev(port), I2C_BUS_MODE_MASTER); +} + +void oracle_i2c_enable_pins_open_drain(int port, int enable_od) +{ + i2c_ll_enable_pins_open_drain(dev(port), enable_od != 0); +} + +void oracle_i2c_update(int port) +{ + i2c_ll_update(dev(port)); +} + +void oracle_i2c_fsm_rst(int port) +{ + i2c_ll_master_fsm_rst(dev(port)); +} + +/* ------------------------------------------------------------------------------- bus timing */ + +/* _i2c_hal_set_bus_timing, i2c_hal.c:27-32: calculate then write. The calculation + * (i2c_ll_master_cal_bus_clk, i2c_ll.h:104-128) is the part this project reimplements in Zig, and + * this is the only honest way to compare it - the computed struct never leaves the register file, so + * the comparison has to be of the registers it produced. */ +void oracle_i2c_set_bus_timing(int port, unsigned source_hz, unsigned bus_hz) +{ + i2c_hal_clk_config_t clk_cal = {0}; + i2c_ll_master_cal_bus_clk(source_hz, bus_hz, &clk_cal); + i2c_ll_master_set_bus_timing(dev(port), &clk_cal); +} + +/* The three timing setters that take explicit periods, which is where IDF's minus-one convention is + * least uniform: start_setup is written as given while start_hold is written minus one + * (i2c_ll.h:452-456), both stop values are written as given (i2c_ll.h:467-471), and so are both sda + * values (i2c_ll.h:482-486). None of that is derivable from the register headers. */ +void oracle_i2c_set_start_timing(int port, int setup, int hold) +{ + i2c_ll_master_set_start_timing(dev(port), setup, hold); +} + +void oracle_i2c_set_stop_timing(int port, int setup, int hold) +{ + i2c_ll_master_set_stop_timing(dev(port), setup, hold); +} + +void oracle_i2c_set_sda_timing(int port, int sample, int hold) +{ + i2c_ll_set_sda_timing(dev(port), sample, hold); +} + +void oracle_i2c_set_tout(int port, int tout) +{ + i2c_ll_set_tout(dev(port), tout); +} + +/* i2c_hal_master_set_scl_timeout_val, i2c_hal.c:66-70. */ +void oracle_i2c_set_scl_timeout_us(int port, unsigned source_hz, unsigned timeout_us) +{ + uint32_t reg_val = i2c_ll_calculate_timeout_us_to_reg_val(source_hz, timeout_us); + i2c_ll_set_tout(dev(port), reg_val); +} + +void oracle_i2c_set_filter(int port, unsigned filter_num) +{ + i2c_ll_master_set_filter(dev(port), (uint8_t)filter_num); +} + +/* --------------------------------------------------------------------------------- the FIFOs */ + +void oracle_i2c_txfifo_rst(int port) +{ + i2c_ll_txfifo_rst(dev(port)); +} + +void oracle_i2c_rxfifo_rst(int port) +{ + i2c_ll_rxfifo_rst(dev(port)); +} + +void oracle_i2c_enable_fifo_mode(int port, int fifo_mode_en) +{ + i2c_ll_enable_fifo_mode(dev(port), fifo_mode_en != 0); +} + +void oracle_i2c_set_fifo_thresholds(int port, unsigned tx_empty, unsigned rx_full) +{ + i2c_ll_set_txfifo_empty_thr(dev(port), (uint8_t)tx_empty); + i2c_ll_set_rxfifo_full_thr(dev(port), (uint8_t)rx_full); +} + +/* A pattern rather than a caller-supplied buffer: the point is that both implementations push the + * same bytes through the same FIFO port, and a fixed generator makes the two sides impossible to + * accidentally disagree about. 0xA0 + i is chosen so every byte differs from its neighbours and from + * 0x00/0xFF, which are the values a broken FIFO produces. */ +void oracle_i2c_write_txfifo_pattern(int port, unsigned len) +{ + uint8_t buf[32]; + if (len > sizeof(buf)) { + len = sizeof(buf); + } + for (unsigned i = 0; i < len; i++) { + buf[i] = (uint8_t)(0xA0 + i); + } + i2c_ll_write_txfifo(dev(port), buf, (uint8_t)len); +} + +/* ------------------------------------------------------------------------- the command list */ + +/* The one piece of logic in this file, and it is here on purpose: it maps a command *kind* to + * ESP-IDF's own `I2C_LL_CMD_*` macro, so the opcode number crosses the boundary as a name rather + * than as an integer. If the Zig side had the numbers wrong - and this chip's register header + * documents the pre-ESP32-C3 numbering, so getting them wrong is easy - passing the raw number + * through would make both sides agree on the same mistake and the differential would prove nothing. + * + * Kinds: 0 restart, 1 write, 2 read, 3 stop, 4 end. */ +static uint32_t opcode_of(unsigned kind) +{ + switch (kind) { + case 0: return I2C_LL_CMD_RESTART; + case 1: return I2C_LL_CMD_WRITE; + case 2: return I2C_LL_CMD_READ; + case 3: return I2C_LL_CMD_STOP; + default: return I2C_LL_CMD_END; + } +} + +void oracle_i2c_write_cmd(int port, int slot, unsigned kind, unsigned byte_num, + int ack_en, int ack_exp, int ack_val) +{ + i2c_ll_hw_cmd_t cmd = { + .byte_num = byte_num, + .ack_en = (ack_en != 0), + .ack_exp = (ack_exp != 0), + .ack_val = (ack_val != 0), + .op_code = opcode_of(kind), + }; + i2c_ll_master_write_cmd_reg(dev(port), cmd, slot); +} + +/* ---------------------------------------------------------------------------- interrupt state */ + +void oracle_i2c_clear_intr_mask(int port, unsigned mask) +{ + i2c_ll_clear_intr_mask(dev(port), mask); +} + +void oracle_i2c_disable_intr_mask(int port, unsigned mask) +{ + i2c_ll_disable_intr_mask(dev(port), mask); +} + +/* ------------------------------------------------------------------------------ observations */ + +/* Not part of any comparison - these exist so the harness can print what the reference thinks the + * hardware says, next to what ours says, when a case fails. */ +unsigned oracle_i2c_get_hw_version(int port) +{ + return i2c_ll_get_hw_version(dev(port)); +} + +unsigned oracle_i2c_get_txfifo_len(int port) +{ + uint32_t len = 0; + i2c_ll_get_txfifo_len(dev(port), &len); + return len; +} + +unsigned oracle_i2c_get_rxfifo_cnt(int port) +{ + uint32_t len = 0; + i2c_ll_get_rxfifo_cnt(dev(port), &len); + return len; +} + +int oracle_i2c_get_tout(int port) +{ + int tout = 0; + i2c_ll_get_tout(dev(port), &tout); + return tout; +} + +/* The chip's command-slot count as ESP-IDF's own header states it, so the Zig constant is checked + * against IDF rather than against a reading of IDF. */ +unsigned oracle_i2c_cmd_reg_num(void) +{ + return I2C_LL_CMD_REG_NUM; +} + +unsigned oracle_i2c_fifo_len(void) +{ + return I2C_LL_FIFO_LEN; +} diff --git a/src/oracle/intr_cases.zig b/src/oracle/intr_cases.zig new file mode 100644 index 0000000..78f2965 --- /dev/null +++ b/src/oracle/intr_cases.zig @@ -0,0 +1,595 @@ +//! The interrupt controller's side of the differential test. +//! +//! **What this can and cannot prove.** A register comparison is weaker evidence for an interrupt +//! controller than for any other peripheral here, and pretending otherwise would be the worst thing +//! this file could do. What it establishes is that for each operation below, this HAL leaves the +//! same words behind that ESP-IDF's code does - the matrix address arithmetic, the `+ 16` offset, +//! the two different priority encodings, the trigger encoding, and which bits each operation is +//! allowed to disturb. What it cannot establish is that an interrupt is ever *taken*: that depends +//! on mtvec, MTVT, mstatus.MIE, the trap entry's register save and the CLIC's arbitration, none of +//! which appear in any register this harness photographs. The behavioural test is spelled out at +//! the foot of this file and the parent must run it separately. +//! +//! **Three windows, three suites.** The controller is three disjoint pieces of address space: +//! * the interrupt matrix at DR_REG_INTERRUPT_CORE0_BASE, 128 words, one per source; +//! * the CLIC's global registers at 0x2080_0000, three words, the third being the threshold; +//! * the CLIC's per-interrupt control file at 0x2080_1000, 48 words, one per CLIC ID. +//! A single window spanning them would have to read about a thousand words of address space nothing +//! is mapped at. Each descriptor below covers only registers these cases actually touch. +//! +//! **Nothing here enables interrupts.** No case calls `hal.intr.init()`, sets mstatus.MIE or writes +//! mtvec; the cases enable *lines* at the CLIC, which with MIE clear is inert. `setup` clears MIE +//! explicitly, so that is true even if something earlier in the image set it. +//! +//! **The two sides reach the hardware by different paths**, which is what makes this a test rather +//! than a tautology: IDF's side goes through `src/oracle/intr_ref.c` into IDF's own inlines and +//! macros, ours goes through `src/hal/intr.zig`, and the harness compares raw words rather than +//! either side's read-back. + +const std = @import("std"); +const hal = @import("hal"); +const regs = @import("regs"); +const mmio = @import("mmio"); +const types = @import("differ_types.zig"); + +const intr = hal.intr; +const Reg = mmio.Reg; +const Field = mmio.Field; + +// ---------------------------------------------------------------- ESP-IDF's side + +extern fn oracle_intr_intthresh_standard() c_int; +extern fn oracle_intr_mintstatus_csr() c_int; +extern fn oracle_intr_mtvt_csr() c_int; +extern fn oracle_intr_nlbits() c_int; +extern fn oracle_intr_ext_offset() c_int; +extern fn oracle_intr_thresh_reg_addr() c_uint; +extern fn oracle_intr_ctrl_reg_addr(clic_id: c_uint) c_uint; + +extern fn oracle_intr_route(intr_src: c_uint, line: c_uint) void; +extern fn oracle_intr_unroute(intr_src: c_uint) void; +extern fn oracle_intr_set_vectored(line: c_uint, vectored: c_int) void; +extern fn oracle_intr_get_type(line: c_uint) c_int; +extern fn oracle_intr_get_priority(line: c_uint) c_int; +extern fn oracle_intr_enable(line: c_uint) void; +extern fn oracle_intr_disable(line: c_uint) void; +extern fn oracle_intr_set_type(line: c_uint, trig: c_uint) void; +extern fn oracle_intr_set_priority(line: c_uint, priority: c_uint) void; +extern fn oracle_intr_edge_ack(line: c_uint) void; +extern fn oracle_intr_enabled_mask() c_uint; +extern fn oracle_intr_set_threshold(level: c_uint) void; +extern fn oracle_intr_get_threshold() c_uint; +extern fn oracle_intr_set_mtvt(mtvt: c_uint) void; + +/// The constants ESP-IDF's half was compiled with. Every one of these is a way the experiment could +/// be quietly meaningless, so the harness should print them rather than assume them: +/// * `intthresh_standard` **must be 0**. A 1 means the C side switched to the `mintthresh` CSR, +/// which this die does not implement, and the threshold comparison would be against a register +/// the interrupt arbiter never reads. (`intr_ref.c` also makes this a compile error.) +/// * `mintstatus_csr` must be 0x346 - the non-standard number for pre-v3 silicon. 0xFB1 would mean +/// the rev-3 header path got selected. +/// * `ext_offset` must be 16 and `nlbits` 3: all the arithmetic on both sides rests on those two. +/// * `thresh_reg` must be 0x20800008, not a CSR number. +pub const RefConfig = struct { + intthresh_standard: u32, + mintstatus_csr: u32, + mtvt_csr: u32, + nlbits: u32, + ext_offset: u32, + thresh_reg: u32, +}; + +pub fn refConfig() RefConfig { + return .{ + .intthresh_standard = @intCast(oracle_intr_intthresh_standard()), + .mintstatus_csr = @intCast(oracle_intr_mintstatus_csr()), + .mtvt_csr = @intCast(oracle_intr_mtvt_csr()), + .nlbits = @intCast(oracle_intr_nlbits()), + .ext_offset = @intCast(oracle_intr_ext_offset()), + .thresh_reg = @intCast(oracle_intr_thresh_reg_addr()), + }; +} + +// ---------------------------------------------------------------- what is under test + +/// The source under test. A module-level variable because Zig has no closures and the harness holds +/// plain `fn` pointers - the same reason `gpio_cases.zig:39` has one. +pub var source: intr.Source = .tg1_t0; + +/// The external line under test, 0..31. +pub var line: u5 = 5; + +/// Sources worth routing, chosen to exercise the address arithmetic rather than to be interesting: +/// the first mapping register (`lp_rtc`, +0x000), the last one that exists on this die +/// (`assist_debug`, +0x1FC), and three in between. A wrong scale factor or a wrong base would be +/// invisible at `lp_rtc` and unmissable at `assist_debug`. +/// +/// The three rev-3-only sources (IDs 133-135) are deliberately absent: their mapping registers are +/// not implemented on pre-v3 silicon, so a comparison there would compare two reads of nothing. +pub const sources = [_]intr.Source{ .lp_rtc, .i2c1, .tg1_t0, .gpio_intr3, .assist_debug }; + +/// Lines worth testing: one in the low half, one in the high half. A line is offset by 16 before it +/// indexes the CLIC, so line 24 lands at CLIC ID 40 - past the point where a missing offset would +/// still have landed inside the 48-word table and gone unnoticed. +pub const lines = [_]u5{ 5, 24 }; + +// ---------------------------------------------------------------- addresses + +const matrix_base: u32 = @intCast(regs.DR_REG_INTERRUPT_CORE0_BASE); +const clic_ctrl_base: u32 = @intCast(regs.DR_REG_CLIC_CTRL_BASE); + +const int_map = Field.of(regs.INTERRUPT_CORE0_UART0_INT_MAP_S, regs.INTERRUPT_CORE0_UART0_INT_MAP_V); +const int_ctl = Field.of(regs.CLIC_INT_CTL_S, regs.CLIC_INT_CTL_V); +const int_attr_trig = Field.of(regs.CLIC_INT_ATTR_TRIG_S, regs.CLIC_INT_ATTR_TRIG_V); +const int_attr_shv = Field.of(regs.CLIC_INT_ATTR_SHV_S, regs.CLIC_INT_ATTR_SHV_V); +const int_ie = Field.of(regs.CLIC_INT_IE_S, regs.CLIC_INT_IE_V); + +inline fn mapReg(source_id: u8) Reg { + return Reg.atAddress(matrix_base + 4 * @as(u32, source_id)); +} +inline fn ctrlReg(clic_id: u32) Reg { + return Reg.atAddress(clic_ctrl_base + 4 * clic_id); +} + +// ---------------------------------------------------------------- restore + +/// Reset value of the CLIC_INT_CTL priority field: 0x1f (`soc/clic_reg.h:70`). Restoring to 0 would +/// be restoring to a state the hardware never boots in, and the two implementations would then be +/// compared from a starting point neither of them produces. +const ctl_reset: u32 = 0x1f; + +/// Bring all three blocks back to a known state between the two implementations. +/// +/// Configuring rather than resetting, and here that is not a preference: neither the matrix nor the +/// CLIC has a reset bit in HP_SYS_CLKRST, and there is no sound way for code to reset the interrupt +/// controller of the core it is running on. Configuring is legitimate because every field this +/// touches is plain read/write. +/// +/// Three things this does that a narrower restore would not, each for a reason: +/// +/// 1. **All five sources, not just the current one.** A case that wrote the wrong mapping register +/// would otherwise leave that write behind; the next case's two snapshots would both inherit it +/// and compare equal. The bug would be visible exactly once and then absorbed. +/// +/// 2. **A sweep of all 128 mapping registers for anything pointing at a line under test.** This is +/// what makes "nothing can assert into these lines during the run" true rather than hoped. The +/// ROM bootloader is under no obligation to leave the matrix clear, and a live peripheral routed +/// to CLIC ID 21 or 40 would set that line's pending bit between the two snapshots and read as a +/// false difference. 128 reads is nothing; guessing is not free. +/// +/// 3. **Both lines, not just the current one**, for the same reason as (1). +fn restore() void { + for (sources) |s| intr.unroute(s); + + // (2): detach anything at all that aims at a line this suite uses. + for (lines) |l| { + const id = @as(u32, l) + intr.ext_offset; + var src: u32 = 0; + while (src <= intr.max_source_id) : (src += 1) { + const r = mapReg(@intCast(src)); + if (r.get(int_map) == id) r.modify(.{int_map.is(0)}); + } + } + + for (lines) |l| { + ctrlReg(@as(u32, l) + intr.ext_offset).modify(.{ + int_ctl.is(ctl_reset), + int_attr_trig.is(0), + int_attr_shv.is(0), + int_ie.is(0), + }); + } + + intr.setThreshold(0); +} + +/// Run once, before anything. Makes the "no interrupt can be taken during this suite" claim true +/// rather than assumed: the cases enable CLIC lines, and an enabled line with MIE set would vector +/// through whatever mtvec the bootloader happened to leave behind. +fn setup() void { + intr.globalDisable(); + restore(); +} + +// ---------------------------------------------------------------- the matrix suite + +/// The interrupt matrix: 128 mapping registers, source 0 at +0x000 through `assist_debug` at +/// +0x1FC. The whole block is in the window deliberately - the cases touch five of the 128, and the +/// other 123 are the point: a routing write that landed on the wrong register shows up as a +/// difference in a word no case names. +/// +/// No `volatile_words`. Each word is a 6-bit read/write field plus reserved bits; nothing here is +/// read-to-clear, nothing self-clears, and no hardware writes these - they are pure configuration. +/// +/// No `clock` either, and that is structural rather than lucky: the matrix and the CLIC are in the +/// CPU's own clock domain and have no gate in HP_SYS_CLKRST, because a core cannot be allowed to +/// gate off the block that delivers its own interrupts. +pub const suite: types.Suite = .{ + .descriptor = .{ + .name = "intr_matrix", + .base = matrix_base, + .words = 128, + .restore = .{ .configure = restore }, + }, + .setup = setup, + .cases = &.{ + .{ .name = "route", .idf = idfRoute, .ours = ourRoute }, + .{ .name = "unroute", .idf = idfUnroute, .ours = ourUnroute }, + .{ .name = "route_rewrite", .idf = idfRouteTwice, .ours = ourRouteTwice }, + .{ .name = "route_preserves_reserved", .idf = idfRouteOverJunk, .ours = ourRouteOverJunk }, + }, +}; + +// ---------------------------------------------------------------- the CLIC control suite + +/// The CLIC's per-interrupt control file: 48 words, one per CLIC ID, the 16 internal IDs included. +/// They are in the window on purpose - every accessor in `hal/intr.zig` adds 16 to the caller's line +/// number, and an implementation that forgot to would write into IDs 5 and 24 instead of 21 and 40. +/// Both of those are inside this window and neither is excluded below, so a missing offset is a +/// visible difference rather than silence. +/// +/// **The pending bits, and why only three words are excluded.** CLIC_INT_IP is bit 0 of every one of +/// these words and the hardware sets it on its own when a source asserts. For the 32 external IDs +/// that cannot happen during this run: `restore` sweeps all 128 mapping registers and detaches +/// anything aimed at a line under test, and the other 30 external lines have nothing routed to them +/// that these cases did not route. The three excluded words are the standard RISC-V internal +/// interrupts, whose pending bits are driven by the core's own timer and software-interrupt +/// hardware rather than by the matrix, and which this file therefore cannot promise are quiet. +/// Excluding them costs nothing: no operation here can reach an internal ID except by the off-by-16 +/// bug, and that bug lands on IDs 5 and 24, which are still compared. +pub const clic_suite: types.Suite = .{ + .descriptor = .{ + .name = "intr_clic", + .base = clic_ctrl_base, + .words = 48, + .volatile_words = &.{ + 3, // machine software interrupt - IP driven by the msip mechanism + 7, // machine timer interrupt - IP driven by the core timer, which is running + 11, // machine external interrupt - IP driven from outside the matrix + }, + .restore = .{ .configure = restore }, + }, + .setup = setup, + .cases = &.{ + .{ .name = "enable", .arg = 1, .idf = idfEnable, .ours = ourEnable }, + .{ .name = "disable", .arg = 0, .idf = idfDisable, .ours = ourDisable }, + .{ .name = "enable_other_line", .idf = idfEnableOther, .ours = ourEnableOther }, + .{ .name = "trigger_level", .arg = 0, .idf = idfTrigLevel, .ours = ourTrigLevel }, + .{ .name = "trigger_rising", .arg = 1, .idf = idfTrigRising, .ours = ourTrigRising }, + .{ .name = "trigger_falling", .arg = 3, .idf = idfTrigFalling, .ours = ourTrigFalling }, + .{ .name = "priority", .arg = 7, .idf = idfPrio7, .ours = ourPrio7 }, + .{ .name = "priority", .arg = 1, .idf = idfPrio1, .ours = ourPrio1 }, + .{ .name = "priority", .arg = 0, .idf = idfPrio0, .ours = ourPrio0 }, + .{ .name = "vectored_on", .arg = 1, .idf = idfVectoredOn, .ours = ourVectoredOn }, + .{ .name = "vectored_off", .arg = 0, .idf = idfVectoredOff, .ours = ourVectoredOff }, + .{ .name = "edge_ack", .idf = idfEdgeAck, .ours = ourEdgeAck }, + .{ .name = "configure_line", .arg = 3, .idf = idfConfigure, .ours = ourConfigure }, + }, +}; + +// ---------------------------------------------------------------- the threshold suite + +/// The CLIC's three global registers, and the reason this file exists at all. +/// +/// 0x2080_0000 CLIC_INT_CONFIG (R/W in MNLBITS, untouched here), 0x2080_0004 CLIC_INT_INFO (RO, +/// reads 48 interrupts / 4 CTL bits), 0x2080_0008 CLIC_INT_THRESH. Three words, contiguous, all +/// mapped - a window of exactly the registers involved, rather than a widened one that reaches the +/// threshold by crossing 4 KiB of nothing. +/// +/// **This is the die where the threshold is a register and not a CSR.** `soc/interrupt_reg.h:28-40` +/// sets INTTHRESH_STANDARD 0 under CONFIG_ESP32P4_SELECTS_REV_LESS_V3, and `riscv/csr_clic.h:37-47` +/// then never defines MINTTHRESH_CSR. A HAL that wrote CSR 0x347 instead would pass every other +/// case in this file and fail every one of these three, which is exactly the discrimination the +/// suite is for: the mistake is silent everywhere else. +pub const thresh_suite: types.Suite = .{ + .descriptor = .{ + .name = "intr_thresh", + .base = @intCast(regs.DR_REG_CLIC_BASE), + .words = 3, + .restore = .{ .configure = restoreThreshold }, + }, + .setup = setup, + .cases = &.{ + .{ .name = "threshold", .arg = 0, .idf = idfThresh0, .ours = ourThresh0 }, + .{ .name = "threshold", .arg = 3, .idf = idfThresh3, .ours = ourThresh3 }, + .{ .name = "threshold", .arg = 7, .idf = idfThresh7, .ours = ourThresh7 }, + }, +}; + + +/// Restored through ESP-IDF's side, never through the code under test. `differ.zig` runs restore, +/// idf, snapshot, restore, ours, snapshot: with the HAL on both the restore and the "ours" side, a +/// HAL function that does nothing leaves run B's snapshot equal to run A's and the case passes. That +/// makes a suite blind to precisely the failure it was written to catch. +fn restoreThreshold() void { + oracle_intr_set_threshold(0); +} + +// ---------------------------------------------------------------- matrix cases + +fn idfRoute() void { + oracle_intr_route(@intFromEnum(source), line); +} +fn ourRoute() void { + intr.route(source, line); +} + +fn idfUnroute() void { + oracle_intr_route(@intFromEnum(source), line); + oracle_intr_unroute(@intFromEnum(source)); +} +fn ourUnroute() void { + intr.route(source, line); + intr.unroute(source); +} + +/// Re-routing a source that is already routed. The mapping register is a read-modify-write of the +/// low 6 bits (`interrupt_clic_ll.h:46`, RV_INT_MASK 63 at line 25), so the second write must +/// *replace* the first rather than OR into it. An `|=` implementation passes the single-write case +/// and fails this one: 31+16 = 47 or'd with 5+16 = 21 is 63, not 21. +fn idfRouteTwice() void { + oracle_intr_route(@intFromEnum(source), 31); + oracle_intr_route(@intFromEnum(source), line); +} +fn ourRouteTwice() void { + intr.route(source, 31); + intr.route(source, line); +} + +/// Route over a word whose reserved bits [31:6] are all set. Both sides must preserve them - IDF's +/// REG_SET_BITS masks with 63, ours is a `Field` of the same width - and this is the case that says +/// so rather than assuming it. A `write` where a `modify` belonged clears them. +fn dirtyMapReg() void { + mapReg(@intFromEnum(source)).writeRaw(0xffff_ffc0); +} +fn idfRouteOverJunk() void { + dirtyMapReg(); + oracle_intr_route(@intFromEnum(source), line); +} +fn ourRouteOverJunk() void { + dirtyMapReg(); + intr.route(source, line); +} + +// ---------------------------------------------------------------- CLIC control cases + +fn idfEnable() void { + oracle_intr_enable(line); +} +fn ourEnable() void { + intr.setEnabled(line, true); +} + +fn idfDisable() void { + oracle_intr_enable(line); + oracle_intr_disable(line); +} +fn ourDisable() void { + intr.setEnabled(line, true); + intr.setEnabled(line, false); +} + +fn otherLine() u5 { + return if (line == lines[0]) lines[1] else lines[0]; +} + +/// Enable the line under test and then enable and disable the *other* one. Catches an index bug +/// that a single-line case cannot: both lines are inside the window, so touching the wrong one is a +/// visible difference rather than an invisible no-op, and the enable/disable pair means the correct +/// answer is "only the first line ends up enabled". +fn idfEnableOther() void { + oracle_intr_enable(line); + oracle_intr_enable(otherLine()); + oracle_intr_disable(otherLine()); +} +fn ourEnableOther() void { + intr.setEnabled(line, true); + intr.setEnabled(otherLine(), true); + intr.setEnabled(otherLine(), false); +} + +fn idfTrigLevel() void { + oracle_intr_set_type(line, 0); +} +fn ourTrigLevel() void { + intr.setTrigger(line, .level); +} + +fn idfTrigRising() void { + oracle_intr_set_type(line, 1); +} +fn ourTrigRising() void { + intr.setTrigger(line, .rising_edge); +} + +/// 0b11, falling edge. IDF's own non-ROM helper only ever writes rising - `esp_tee_rv_utils.h:97-98` +/// is a TODO saying as much - so this is an encoding IDF documents (`clic_reg.h:84-88`) but does not +/// exercise, and its reference is the transcribed REG_SET_FIELD from `esp_rom_clic.c:21` rather than +/// a call into IDF. That makes it the case here most likely to disagree, which is why it is here. +fn idfTrigFalling() void { + oracle_intr_set_type(line, 3); +} +fn ourTrigFalling() void { + intr.setTrigger(line, .falling_edge); +} + +fn idfPrio7() void { + oracle_intr_set_priority(line, 7); +} +fn ourPrio7() void { + intr.setPriority(line, 7); +} + +fn idfPrio1() void { + oracle_intr_set_priority(line, 1); +} +fn ourPrio1() void { + intr.setPriority(line, 1); +} + +/// Priority 0 writes 0x00 over the reset value 0x1f, so this is the case that proves the low +/// `8 - NLBITS` bits are being *cleared*. If either side padded them with ones - which is what the +/// *threshold* encoding does, `csr_clic.h:59` - the two words would differ by 0x1F000000 and by +/// nothing else. Priorities 1 and 7 both leave those bits zero either way and cannot see it. +fn idfPrio0() void { + oracle_intr_set_priority(line, 0); +} +fn ourPrio0() void { + intr.setPriority(line, 0); +} + +fn idfVectoredOn() void { + oracle_intr_set_vectored(line, 1); +} +fn ourVectoredOn() void { + intr.setVectored(line, true); +} + +fn idfVectoredOff() void { + oracle_intr_set_vectored(line, 1); + oracle_intr_set_vectored(line, 0); +} +fn ourVectoredOff() void { + intr.setVectored(line, true); + intr.setVectored(line, false); +} + +/// Writing 1 to IP. With nothing routed to this line there is nothing pending to clear, so what is +/// compared is the *store*: which word, which bit, and whether the surrounding fields survive. That +/// the hardware then reads that write as an acknowledgement is behavioural and out of reach here. +fn idfEdgeAck() void { + oracle_intr_set_type(line, 1); + oracle_intr_edge_ack(line); +} +fn ourEdgeAck() void { + intr.setTrigger(line, .rising_edge); + intr.edgeAck(line); +} + +/// The whole per-line configuration in one go. Our side goes through `configureLine`, not through +/// four separate calls, because that function - not its pieces - is what a driver will use, and +/// because four fields in one word is where an ordering bug or a `write` that should have been a +/// `modify` shows up while each field alone still passes. +fn idfConfigure() void { + oracle_intr_set_type(line, 1); + oracle_intr_set_priority(line, 3); + oracle_intr_set_vectored(line, 0); + oracle_intr_enable(line); +} +fn ourConfigure() void { + intr.configureLine(line, .{ + .handler = noopHandler, + .trigger = .rising_edge, + .priority = 3, + .vectored = false, + }); +} + +/// Installed and never called: `configureLine` requires a handler and the differ never sets MIE. +/// Its address goes into a RAM array no descriptor's window covers, so it cannot perturb a +/// comparison. +fn noopHandler(_: u5) void {} + +// ---------------------------------------------------------------- threshold cases + +fn idfThresh0() void { + oracle_intr_set_threshold(0); +} +fn ourThresh0() void { + intr.setThreshold(0); +} + +fn idfThresh3() void { + oracle_intr_set_threshold(3); +} +fn ourThresh3() void { + intr.setThreshold(3); +} + +fn idfThresh7() void { + oracle_intr_set_threshold(7); +} +fn ourThresh7() void { + intr.setThreshold(7); +} + +// --------------------------------------------------------------------------------------------- +// THE BEHAVIOURAL TEST - which the parent must run, because this file cannot. +// --------------------------------------------------------------------------------------------- +// +// Everything above compares register *state*. None of it touches the parts of this peripheral that +// exist only while an interrupt is in flight: mtvec's mode bits, the MTVT fetch, the CLIC's +// arbitration against the threshold, mcause's EXCCODE, the trap entry's register save, and `mret`. +// A HAL that passes every case above and still never delivers an interrupt is entirely possible - +// it is in fact the expected failure mode, because the single most likely mistake here (writing the +// `mintthresh` CSR instead of CLIC_INT_THRESH_REG) leaves no trace in any register. +// +// The cheap test, with the numbers it needs: +// +// 1. `hal.clkrst.init(.timg1)` - clock on, reset pulsed, flash-boot protection cleared. TIMG's +// reset re-arms the flash-boot watchdog; skipping the clear reboots the board a second later +// with nothing on the console to explain it. +// 2. Arm TIMG1 timer 0 for a one-shot alarm a few milliseconds out, alarm enabled, and the +// timer's own interrupt enable set (TIMG_T0_INT_ENA). +// 3. `hal.intr.init()` - fills the 48-entry vector table with the trap entry, writes MTVT +// (CSR 0x307), writes mtvec = trap_entry | 3, and opens the threshold to 0. +// 4. `hal.intr.attach(.tg1_t0, 5, .{ .handler = h, .trigger = .level, .priority = 1 })`. +// `.tg1_t0` is source ID 49, so the mapping register is DR_REG_INTERRUPT_CORE0_BASE + 0xC4 and +// the value written is 5 + 16 = 21. Priority 1 against threshold 0 is the minimum that is not +// masked: the threshold comparison is inclusive, so priority 0 would never fire. +// 5. The handler increments a counter and **clears TIMG1's interrupt status**. That is mandatory +// for a level source: the CLIC has no acknowledge for level, so a handler that returns without +// clearing the peripheral re-enters immediately and the board sits inside the trap entry with +// the console silent. That failure looks exactly like a crash and is not one. +// 6. `hal.intr.globalEnable()`, spin ~50 ms, `hal.intr.globalDisable()`. +// +// Pass is `counter == 1` **and** `hal.intr.spurious == 0`. Both halves matter: a counter of 1 with a +// non-zero spurious count means an interrupt also arrived on a line nobody claimed, i.e. a matrix +// write went somewhere unintended. +// +// Diagnostics worth printing on failure, because they separate the ways this can go wrong: +// * `hal.intr.getThreshold()` beside `oracle_intr_get_threshold()` - a disagreement means the +// threshold mechanism is the fault, which is what this die's non-standard CLIC invites. +// * `hal.intr.routedLine(.tg1_t0)` - null means the matrix write missed. +// * `hal.intr.isPending(5)` with the counter at 0 - the CLIC latched it and the core never took +// it, so the fault is mtvec, MTVT or MIE, and is neither the matrix nor the threshold. +// * TIMG1's raw interrupt status - if that is 0 the timer never fired and the test is measuring +// something else entirely. +// +// A second, sharper test once the first passes: set the threshold to 7 *before* enabling, confirm +// the counter stays 0 while `isPending(5)` becomes 1, then drop the threshold to 0 and confirm the +// pending interrupt is delivered. That is the only way to show the memory-mapped threshold register +// is the one the arbiter actually reads, and it is the claim this whole file is least able to +// support on its own. + +// --------------------------------------------------------------------------------------------- + +test "the line-to-CLIC-ID offset the cases assume is the one the header defines" { + try std.testing.expectEqual(@as(u32, 16), intr.ext_offset); + try std.testing.expectEqual(@as(u32, 48), intr.total_ids); + // Both test lines land inside the 48-word CLIC window, which is what makes an off-by-16 in + // hal/intr.zig visible to the harness rather than silent... + for (lines) |l| try std.testing.expect(@as(u32, l) + intr.ext_offset < intr.total_ids); + // ...and neither un-offset line is one of the three words excluded as volatile, or the bug + // would land in a word the harness ignores. + for (lines) |l| for (clic_suite.descriptor.volatile_words) |w| try std.testing.expect(w != l); +} + +test "the matrix window covers every source the cases route" { + for (sources) |s| { + try std.testing.expect(!s.isRev3Only()); + try std.testing.expect(@intFromEnum(s) < suite.descriptor.words); + } + // The extremes really are in the set - that is the point of the choice. + try std.testing.expectEqual(@as(u8, 0), @intFromEnum(sources[0])); + try std.testing.expectEqual(@as(u8, 127), @intFromEnum(sources[sources.len - 1])); +} + +test "the threshold window is the register block, not a CSR, and holds all three words" { + try std.testing.expectEqual(@as(u32, 0x2080_0000), thresh_suite.descriptor.base); + // CLIC_INT_THRESH_REG is the third word. If this ever stops being true the window is wrong. + try std.testing.expectEqual( + @as(u32, 0x2080_0008), + thresh_suite.descriptor.base + 4 * (thresh_suite.descriptor.words - 1), + ); +} diff --git a/src/oracle/intr_ref.c b/src/oracle/intr_ref.c new file mode 100644 index 0000000..ceeaa0d --- /dev/null +++ b/src/oracle/intr_ref.c @@ -0,0 +1,211 @@ +/* ESP-IDF's own CLIC code, given external linkage so the differential harness can call it. + * + * The interrupt controller is the one peripheral where "wrap IDF's LL header" is not the whole + * story, and the reason is worth recording rather than papering over. + * + * ESP-IDF splits CLIC access across four places: + * 1. components/hal/include/hal/interrupt_clic_ll.h - the matrix route, SHV, and the two + * getters. Included below and wrapped directly; this is the LL proper. + * 2. components/riscv/include/esp_private/interrupt_clic.h - MTVT, the threshold, edge-ack and + * the enabled-mask scan, all `FORCE_INLINE_ATTR`. Also included below and wrapped directly. + * 3. the **mask ROM** - esprv_intc_int_enable / _set_priority / _set_type, aliased into + * esprv_int_* by components/riscv/ld/rom.api.ld. There is no C source for these, so they + * cannot be compiled into this image as a reference. Worse, one of them is *wrong* on this + * die: components/esp_rom/patches/esp_rom_clic.c:12-22 exists because the ROM's + * esprv_intc_int_set_type silently configures LEVEL when asked for EDGE, on exactly the + * CONFIG_ESP32P4_SELECTS_REV_LESS_V3 silicon this board is. + * 4. components/esp_tee/.../clic/esp_tee_rv_utils.h - a non-ROM implementation of enable, + * disable, set_type and set_priority, written as byte stores. + * + * For (3) the reference below is the register expression from IDF's own replacement code, copied + * statement for statement with the file and line it came from, and using IDF's macros so the + * numbers are still IDF's. That is a transcription, and it is the weakest link in this file; it is + * marked as such at each site. Everything else calls IDF's code directly. + * + * Note also what the ROM situation means for the differential's *value* here: for enable, priority + * and trigger the comparison is against IDF's non-ROM path, which is the path IDF itself uses on + * TEE builds and the path its ROM patch restores. It is not against the ROM function a stock + * app_main would reach. + */ + +/* IDF's clock and reset LL functions are shadowed by a wrapper macro referencing + * `__DECLARE_RCC_ATOMIC_ENV`, an identifier IDF never defines anywhere, so that an unguarded call + * fails to compile. Nothing in this translation unit gates a clock, but the header chain reaches + * those declarations, so the name has to exist. Same reasoning as src/oracle/gpio_ref.c:18. */ +static int __DECLARE_RCC_ATOMIC_ENV __attribute__((unused)); + +/* **First, and load-bearing.** soc/interrupt_reg.h tests CONFIG_ESP32P4_SELECTS_REV_LESS_V3 but + * does not include sdkconfig.h itself - it relies on the caller having done so, which in IDF's own + * build happens because CMake force-includes it. Include it *after* any header below and + * INTTHRESH_STANDARD comes out 1, the rev-3 answer, and this reference would be built against the + * mintthresh CSR that this silicon does not implement. That is not hypothetical: this file's first + * version had the includes in the obvious order and the #error below fired. + * + * build.zig now also passes `-include oracle_sdkconfig.h` to every reference translation unit, so + * this line is belt as well as braces. It stays because the ordering constraint is a property of + * IDF's headers, not of our build flags, and the next person to reorder these should see why. */ +#include "sdkconfig.h" +#include "soc/soc.h" +#include "soc/clic_reg.h" +#include "soc/interrupt_reg.h" +#include "hal/interrupt_clic_ll.h" +#include "esp_private/interrupt_clic.h" + +/* Guard the whole point of this file: if the build ever stopped defining + * CONFIG_ESP32P4_SELECTS_REV_LESS_V3 (src/oracle/oracle_sdkconfig.h:25), interrupt_reg.h:28-40 would + * flip INTTHRESH_STANDARD to 1 and every threshold function below would silently switch from the + * memory-mapped register to the mintthresh CSR - which this die does not implement. The reference + * would then be comparing against a threshold mechanism that does not exist, and would agree with + * nothing. Fail the compile instead. */ +#if INTTHRESH_STANDARD +#error "this die uses the memory-mapped CLIC threshold; INTTHRESH_STANDARD must be 0 here" +#endif + +/* Report the numbers this reference was compiled with, so a run can never silently be against the + * wrong variant of the controller. */ +int oracle_intr_intthresh_standard(void) +{ + return INTTHRESH_STANDARD; +} + +int oracle_intr_mintstatus_csr(void) +{ + return MINTSTATUS_CSR; +} + +int oracle_intr_mtvt_csr(void) +{ + return MTVT_CSR; +} + +int oracle_intr_nlbits(void) +{ + return NLBITS; +} + +int oracle_intr_ext_offset(void) +{ + return CLIC_EXT_INTR_NUM_OFFSET; +} + +unsigned oracle_intr_thresh_reg_addr(void) +{ + return (unsigned)CLIC_INT_THRESH_REG; +} + +unsigned oracle_intr_ctrl_reg_addr(unsigned clic_id) +{ + return (unsigned)CLIC_INT_CTRL_REG(clic_id); +} + +/* ---------------------------------------------------------------- the interrupt matrix */ + +/* interrupt_clic_ll.h:35-48, with the `+ RV_EXTERNAL_INT_OFFSET` that riscv/interrupt_clic.c:26 + * applies before calling it. Core 0 only: this image never releases core 1. */ +void oracle_intr_route(unsigned intr_src, unsigned line) +{ + interrupt_clic_ll_route(0, (int)intr_src, (int)line + RV_EXTERNAL_INT_OFFSET); +} + +/* esp_system/port/cpu_start.c:185 - IDF's own way to detach a source, writing ETS_INVALID_INUM + * (0 on this chip, soc/esp32p4/include/soc/soc.h:251) with no offset added. */ +void oracle_intr_unroute(unsigned intr_src) +{ + interrupt_clic_ll_route(0, (int)intr_src, ETS_INVALID_INUM); +} + +/* ---------------------------------------------------------------- per-line control */ + +/* interrupt_clic_ll.h:99-102 via riscv/interrupt_clic.c:48-51. */ +void oracle_intr_set_vectored(unsigned line, int vectored) +{ + interrupt_clic_ll_set_vectored((int)line + RV_EXTERNAL_INT_OFFSET, vectored != 0); +} + +/* interrupt_clic_ll.h:58-61 via riscv/interrupt_clic.c:30-33: 1 for edge, 0 for level. */ +int oracle_intr_get_type(unsigned line) +{ + return interrupt_clic_ll_get_type((int)line + RV_EXTERNAL_INT_OFFSET); +} + +/* interrupt_clic_ll.h:71-75 via riscv/interrupt_clic.c:36-39. */ +int oracle_intr_get_priority(unsigned line) +{ + return interrupt_clic_ll_get_priority((int)line + RV_EXTERNAL_INT_OFFSET); +} + +/* TRANSCRIBED, not called: the ROM owns esprv_intc_int_enable and there is no source for it. + * The store is esp_tee/subproject/main/include/clic/esp_tee_rv_utils.h:74, verbatim - a byte write + * of BYTE_CLIC_INT_IE to BYTE_CLIC_INT_IE_REG. Byte 1 of the control word holds nothing but IE, so + * this and a 32-bit read-modify-write of CLIC_INT_IE leave the same word behind; that equivalence + * is precisely what the differential is there to check rather than assert. */ +void oracle_intr_enable(unsigned line) +{ + const unsigned id = line + CLIC_EXT_INTR_NUM_OFFSET; + *(uint8_t volatile *)(BYTE_CLIC_INT_IE_REG(id)) = BYTE_CLIC_INT_IE; +} + +/* TRANSCRIBED: esp_tee_rv_utils.h:88. */ +void oracle_intr_disable(unsigned line) +{ + const unsigned id = line + CLIC_EXT_INTR_NUM_OFFSET; + *(uint8_t volatile *)(BYTE_CLIC_INT_IE_REG(id)) = 0; +} + +/* TRANSCRIBED: esp_rom/patches/esp_rom_clic.c:21, which is IDF's *replacement* for the ROM's + * broken esprv_intc_int_set_type on pre-v3 P4 silicon. A 32-bit REG_SET_FIELD on CLIC_INT_ATTR_TRIG, + * so unlike the TEE build's byte store it preserves SHV by read-modify-write rather than by the + * byte's other bits happening to be reloaded - same result, different mechanism. `type` is the raw + * two-bit encoding (0 level, 1 rising, 3 falling; clic_reg.h:84-88). */ +void oracle_intr_set_type(unsigned line, unsigned type) +{ + const unsigned id = line + CLIC_EXT_INTR_NUM_OFFSET; + REG_SET_FIELD(CLIC_INT_CTRL_REG(id), CLIC_INT_ATTR_TRIG, type); +} + +/* TRANSCRIBED: esp_tee_rv_utils.h:112. Note the encoding - priority left-aligned into the top + * NLBITS of the byte with the low bits **zero**, which differs from the threshold's encoding + * below. */ +void oracle_intr_set_priority(unsigned line, unsigned priority) +{ + const unsigned id = line + CLIC_EXT_INTR_NUM_OFFSET; + *(uint8_t volatile *)(BYTE_CLIC_INT_CTL_REG(id)) = (uint8_t)(priority << BYTE_CLIC_INT_CTL_S); +} + +/* esp_private/interrupt_clic.h, rv_utils_intr_edge_ack: writing 1 to IP is what *clears* an + * edge-triggered pending. Called directly - this one is a real IDF inline. */ +void oracle_intr_edge_ack(unsigned line) +{ + rv_utils_intr_edge_ack(line); +} + +/* esp_private/interrupt_clic.h, rv_utils_intr_get_enabled_mask. */ +unsigned oracle_intr_enabled_mask(void) +{ + return rv_utils_intr_get_enabled_mask(); +} + +/* ---------------------------------------------------------------- the threshold */ + +/* esp_private/interrupt_clic.h:153-156 -> :129-146. Called directly, so the reference includes + * IDF's own read-back-to-force-the-store and IDF's own NLBITS_TO_BYTE padding, and the harness + * compares against those rather than against a re-derivation of them. */ +void oracle_intr_set_threshold(unsigned level) +{ + rv_utils_restore_intlevel(level); +} + +/* esp_private/interrupt_clic.h:44-57. Returns an absolute level 0..7. */ +unsigned oracle_intr_get_threshold(void) +{ + return rv_utils_get_interrupt_threshold(); +} + +/* ---------------------------------------------------------------- vector table */ + +/* esp_private/interrupt_clic.h:63-66. MTVT is CSR 0x307. Writing it has no effect on any register + * the harness photographs, so this exists for the behavioural test rather than for the diff. */ +void oracle_intr_set_mtvt(unsigned mtvt) +{ + rv_utils_set_mtvt(mtvt); +} diff --git a/src/oracle/ledc_cases.zig b/src/oracle/ledc_cases.zig new file mode 100644 index 0000000..5a39f83 --- /dev/null +++ b/src/oracle/ledc_cases.zig @@ -0,0 +1,513 @@ +//! LEDC's side of the differential test: every operation expressed as ESP-IDF's LL calls and as this +//! project's HAL calls. +//! +//! **A register diff cannot prove that a commit happened.** `LEDC_PARA_UP_CHn` and +//! `LEDC_TIMERn_PARA_UP` are write-to-trigger bits that the hardware clears again by itself, so the +//! word that carried the commit reads back exactly as it did before, and the shadow registers the +//! commit copies into are not addressable. Two snapshots therefore agree whether or not either +//! implementation committed anything at all. Nothing in this file claims otherwise. +//! +//! What the diff *can* prove, and what these cases are shaped to prove: +//! +//! * The **staged values** match. Every case stages through the same fields IDF's LL stages, so a +//! wrong shift, a wrong instance stride or a `write` where a `modify` was needed shows up in the +//! staged word - which is the register the commit will read. +//! * The commit **did not destroy the staging**. This is the real hazard of a commit bit that lives +//! inside the word it commits: `LEDC_PARA_UP_CH0` is bit 4 of `LEDC_CH0_CONF0_REG`, so a commit +//! implemented as `writeRaw(1 << 4)` would zero `TIMER_SEL`, `SIG_OUT_EN`, `IDLE_LV` and +//! `OVF_NUM` on its way past. That failure is loud here: the staged word would differ. +//! * `stage_without_commit` pins the distinction down. It stages a duty and stops, on both sides. +//! It must pass, and it must pass for the same reason a committed case passes - which is the +//! evidence that "passes" says nothing about the commit. +//! +//! `LEDC_CHn_DUTY_R_REG` is the one register that reflects the committed shadow rather than the +//! staged value, and it is listed as volatile below rather than used as proof: it updates when the +//! timer next overflows, so what it holds at snapshot time depends on where the counter happened to +//! be. Proving the commit needs an oscilloscope, or the ovf-count interrupt, not a register read. +//! +//! Four windows, because LEDC's state is not in one place: the peripheral block, its gamma RAM +//! aperture, the GPIO matrix (pin routing touches no LEDC register at all) and HP_SYS_CLKRST (where +//! the P4 moved LEDC's clock mux). One suite each, since a `Peripheral` descriptor is one contiguous +//! range of words. + +const std = @import("std"); +const hal = @import("hal"); +const regs = @import("regs"); +const mmio = @import("mmio"); +const types = @import("differ_types.zig"); + +const ledc = hal.ledc; + +extern fn oracle_ledc_enable_function_clock(enable: c_int) void; +extern fn oracle_ledc_set_clock_source(sel: c_uint) void; +extern fn oracle_ledc_divisor(src_clk_freq: c_uint, freq_hz: c_int, precision: c_uint) c_uint; +extern fn oracle_ledc_set_clock_divider(timer: c_uint, div: c_uint) void; +extern fn oracle_ledc_set_duty_resolution(timer: c_uint, bits: c_uint) void; +extern fn oracle_ledc_commit_timer(timer: c_uint) void; +extern fn oracle_ledc_reset_timer(timer: c_uint) void; +extern fn oracle_ledc_pause_timer(timer: c_uint) void; +extern fn oracle_ledc_resume_timer(timer: c_uint) void; +extern fn oracle_ledc_configure_timer(timer: c_uint, src_hz: c_uint, freq_hz: c_int, resolution: c_uint) void; +extern fn oracle_ledc_bind_timer(channel: c_uint, timer: c_uint) void; +extern fn oracle_ledc_set_hpoint(channel: c_uint, hpoint: c_uint) void; +extern fn oracle_ledc_set_duty(channel: c_uint, duty: c_uint) void; +extern fn oracle_ledc_set_output_enabled(channel: c_uint, enable: c_int) void; +extern fn oracle_ledc_set_idle_level(channel: c_uint, level: c_uint) void; +extern fn oracle_ledc_commit_channel(channel: c_uint) void; +extern fn oracle_ledc_start(channel: c_uint) void; +extern fn oracle_ledc_stop(channel: c_uint, idle_level: c_uint) void; +extern fn oracle_ledc_configure_channel( + channel: c_uint, + timer: c_uint, + duty: c_uint, + hpoint: c_uint, + idle_level: c_uint, + output_enabled: c_int, +) void; +extern fn oracle_ledc_set_pin(pin: c_uint, channel: c_uint) void; + +/// The channel, timer and pad under test. Module-level variables because Zig has no closures and the +/// harness stores plain `fn` pointers; the alternative, a comptime-specialised pair per channel, +/// would compare code this project does not ship. +/// +/// The suite is safe to run once per pair, the way GPIO's is run once per pin - `channels` and +/// `timers` name the pairs worth using: instance 0, and the far end of each range, where a wrong +/// `RegArray` stride would land outside the block. +pub var channel: u32 = 0; +pub var timer: u32 = 0; +/// GPIO33 is a free pin on this board's JP1 header. GPIO20 is the LED, which the harness itself +/// leaves blinking, and GPIO54 is the ESP32-C6's reset line and must never be driven. +pub var pin: u8 = 33; + +pub const channels = [_]u32{ 0, 7 }; +pub const timers = [_]u32{ 0, 3 }; + +/// 40 MHz XTAL: `ClockSource.xtal.hz()`, and what `setup` selects. Passed explicitly to both sides +/// so the two arithmetics are compared on the same input rather than on each side's idea of the +/// clock tree. +const src_hz: u32 = ledc.xtal_hz; + +/// Bring LEDC up before the first case: its APB gate is off at power-on, so without this every +/// snapshot would be the last value the bus latched and the harness would (correctly) skip the whole +/// suite on the `clock` check. +fn setup() void { + ledc.init(.xtal); +} + +// ------------------------------------------------------------------- the peripheral block itself + +pub const suite: types.Suite = .{ + .descriptor = .{ + .name = "ledc", + .base = @intCast(regs.LEDC_CH0_CONF0_REG), + // 96 words, 0x000-0x17f: eight channels (0x000-0x09f), four timers (0x0a0-0x0bf), the + // interrupt registers, the per-channel gamma *configuration* at 0x100-0x11f (the range + // count lives there, and `setDuty` writes it), the ETM enables, the timer compare and + // capture registers, and LEDC_CONF/LEDC_DATE at 0x170/0x174. Wide enough that every + // register any operation in this file touches is inside it except the gamma RAM aperture at + // 0x400, which has its own suite below. + // + // The reserved gaps (0x0d0-0x0ff, 0x130-0x13f, 0x160-0x16f) are read as well, deliberately: + // if a reserved word does not read back stably the diff will name the offset instead of + // hiding it. + .words = 96, + .volatile_words = &.{ + // LEDC_CHn_DUTY_R: the committed duty shadow, reloaded on timer overflow. + (0x010 - 0x000) / 4, (0x024 - 0x000) / 4, (0x038 - 0x000) / 4, (0x04c - 0x000) / 4, + (0x060 - 0x000) / 4, (0x074 - 0x000) / 4, (0x088 - 0x000) / 4, (0x09c - 0x000) / 4, + // LEDC_TIMERn_VALUE: the live counters. + (0x0a4 - 0x000) / 4, (0x0ac - 0x000) / 4, (0x0b4 - 0x000) / 4, (0x0bc - 0x000) / 4, + // LEDC_INT_RAW and LEDC_INT_ST: overflow and fade-end bits latch while the timers run. + (0x0c0 - 0x000) / 4, (0x0c4 - 0x000) / 4, + // LEDC_TIMERn_CNT_CAP: captured counter values. + (0x150 - 0x000) / 4, (0x154 - 0x000) / 4, (0x158 - 0x000) / 4, (0x15c - 0x000) / 4, + }, + // REG_LEDC_APB_CLK_EN, bit 0 of SOC_CLK_CTRL3 (ledc_ll.h:135). Its reset value is 0, so this + // check is not a formality for LEDC: it is the difference between a snapshot and a memory of + // one. + .clock = .{ + .reg = @intCast(regs.HP_SYS_CLKRST_SOC_CLK_CTRL3_REG), + .bit = @intCast(regs.HP_SYS_CLKRST_REG_LEDC_APB_CLK_EN_S), + }, + // The peripheral reset, REG_RST_EN_LEDC, bit 29 of HP_RST_EN1 (ledc_ll.h:150, + // hp_sys_clkrst_reg.h:3497-3503). Sound here where a configure-restore would not be: this + // block has write-to-trigger fields (both PARA_UPs, OVF_CNT_RESET) whose reset value is only + // defined by the reset, and `LEDC_TIMERn_RST` is one of the fields whose reset value is 1 - + // so "write zeros everywhere" would not be a restore at all. Measured safe on this board: + // pulsing it for 1 ms left the console untouched and returned LEDC_CH0_CONF0 to 0. + .restore = .{ .reset_bit = .{ + .reg = @intCast(regs.HP_SYS_CLKRST_HP_RST_EN1_REG), + .bit = @intCast(regs.HP_SYS_CLKRST_REG_RST_EN_LEDC_S), + } }, + }, + .setup = setup, + .cases = &.{ + // Timer: the whole sequence, at four target frequencies across three duty resolutions. Each + // side computes its own divider - IDF's `ledc_calculate_divisor`, ours `hal.ledc.divisor` - + // so a mismatch in the fixed-point arithmetic lands in LEDC_TIMERn_CONF[22:5] and is caught + // here rather than being argued about. The four dividers are 1250, 500, 2000 and 2083. + .{ .name = "configure_timer_1kHz_13bit", .arg = 1_000, .idf = idfTimer1k13, .ours = ourTimer1k13 }, + .{ .name = "configure_timer_20kHz_10bit", .arg = 20_000, .idf = idfTimer20k10, .ours = ourTimer20k10 }, + .{ .name = "configure_timer_5kHz_10bit", .arg = 5_000, .idf = idfTimer5k10, .ours = ourTimer5k10 }, + .{ .name = "configure_timer_300Hz_14bit", .arg = 300, .idf = idfTimer300_14, .ours = ourTimer300_14 }, + // The divider store and the arithmetic behind it, without the resolution/resume/reset tail. + .{ .name = "clock_divider_only", .arg = 1_250, .idf = idfDivider, .ours = ourDivider }, + .{ .name = "duty_resolution_only", .arg = 13, .idf = idfResolution, .ours = ourResolution }, + .{ .name = "timer_pause", .idf = idfPause, .ours = ourPause }, + .{ .name = "timer_resume", .idf = idfResume, .ours = ourResume }, + .{ .name = "timer_reset", .idf = idfTimerReset, .ours = ourTimerReset }, + // Channel. + .{ .name = "bind_timer", .idf = idfBind, .ours = ourBind }, + .{ .name = "set_hpoint", .arg = 0x400, .idf = idfHpoint, .ours = ourHpoint }, + .{ .name = "set_duty", .arg = 0x1000, .idf = idfDuty4096, .ours = ourDuty4096 }, + .{ .name = "set_duty", .arg = 0, .idf = idfDuty0, .ours = ourDuty0 }, + // Staged and left uncommitted, on both sides. Passes for the same reason the committed cases + // pass, which is the point: the commit is not in the picture the harness takes. + .{ .name = "stage_without_commit", .arg = 0x555, .idf = idfStageOnly, .ours = ourStageOnly }, + .{ .name = "channel_start", .idf = idfStart, .ours = ourStart }, + .{ .name = "channel_stop_idle_low", .arg = 0, .idf = idfStopLow, .ours = ourStopLow }, + .{ .name = "channel_stop_idle_high", .arg = 1, .idf = idfStopHigh, .ours = ourStopHigh }, + .{ .name = "configure_channel", .arg = 0x800, .idf = idfConfigureChannel, .ours = ourConfigureChannel }, + .{ .name = "full_rf_config_25MHz_1bit", .arg = 25, .idf = idfFullRf, .ours = ourFullRf }, + }, +}; + +// -------------------------------------------------------------------------- the gamma RAM window + +/// Zero the whole gamma RAM aperture and pulse the peripheral reset. +/// +/// The zeroing is the load-bearing half. Gamma RAM is RAM: the peripheral reset does *not* clear it, +/// so without this the second run would inherit whatever the first run wrote, and an implementation +/// that wrote no gamma entry at all would compare equal to one that did - the self-consistent test +/// that proves nothing. All 128 words rather than the channel under test's 16, so that the state the +/// two runs start from does not depend on which cases ran before. +fn restoreGamma() void { + var w: u32 = 0; + while (w < 128) : (w += 1) { + mmio.Reg.atAddress(@as(u32, @intCast(regs.LEDC_CH0_GAMMA_RANGE0_REG)) + 4 * w).writeRaw(0); + } + hal.clkrst.resetPeripheral(.ledc); +} + +/// The gamma RAM aperture, 0x400-0x5ff: sixteen entries for each of the eight channels. +/// +/// It has its own suite because it is not contiguous with the register block - between them lies a +/// 0x288-byte hole that nothing documents, and reading unmapped peripheral space to get from one to +/// the other is not a risk worth taking on the only board. +/// +/// What it covers: on the P4 a constant duty is a degenerate one-step fade, because +/// `DUTY_NUM`/`DUTY_CYCLE`/`DUTY_SCALE`/`DUTY_INC` moved out of `LEDC_CHn_CONF1_REG` into this RAM. +/// `setDuty` writes entry 0 accordingly (ledc.c:263-280), and this is the window that sees it. +pub const gamma_suite: types.Suite = .{ + .descriptor = .{ + .name = "ledc_gamma", + .base = @intCast(regs.LEDC_CH0_GAMMA_RANGE0_REG), + .words = 128, + .clock = .{ + .reg = @intCast(regs.HP_SYS_CLKRST_SOC_CLK_CTRL3_REG), + .bit = @intCast(regs.HP_SYS_CLKRST_REG_LEDC_APB_CLK_EN_S), + }, + .restore = .{ .configure = restoreGamma }, + }, + .setup = setup, + .cases = &.{ + .{ .name = "set_duty_writes_entry0", .arg = 0x1000, .idf = idfDuty4096, .ours = ourDuty4096 }, + .{ .name = "set_duty_writes_entry0", .arg = 0, .idf = idfDuty0, .ours = ourDuty0 }, + .{ .name = "configure_channel_writes_entry0", .arg = 0x800, .idf = idfConfigureChannel, .ours = ourConfigureChannel }, + }, +}; + +// ------------------------------------------------------------------------------- the GPIO window + +/// The pad back to a known state: driver off, IO MUX word zeroed, matrix pointing at plain GPIO. +/// The same restore GPIO's own suite uses, for the same reason - there is no reset bit for GPIO and +/// the pads are the board's wiring. +fn restorePad() void { + hal.gpio.outputDisable(pin); + 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(hal.gpio.matrix_gpio_signal); + hal.gpio.setLow(pin); +} + +/// Pin routing touches no LEDC register: the peripheral has no pad of its own, and `attachPin` is +/// entirely a GPIO matrix operation. So it is compared in the GPIO window, where its effect is - and +/// what is actually under test here is the signal index, `LEDC_LS_SIG_OUT_PAD_OUT0_IDX + channel`, +/// which is the one piece of arithmetic in the routing path. +pub const routing_suite: types.Suite = .{ + .descriptor = .{ + .name = "ledc_pin", + .base = @intCast(regs.GPIO_OUT_REG - 4), // GPIO_BT_SELECT_REG sits at +0x00 + .words = 400, + .volatile_words = &.{ + (0x03c - 0x000) / 4, // GPIO_IN - the outside world, which moves + (0x040 - 0x000) / 4, // GPIO_IN1 + }, + .restore = .{ .configure = restorePad }, + }, + .cases = &.{ + .{ .name = "attach_pin", .idf = idfAttachPin, .ours = ourAttachPin }, + .{ .name = "attach_pin_channel7", .arg = 7, .idf = idfAttachPin7, .ours = ourAttachPin7 }, + }, +}; + +// -------------------------------------------------------------------------- the HP_SYS_CLKRST word + +/// LEDC's clock mux and function-clock gate back to what `setup` establishes. Only LEDC's own fields +/// are written: PERI_CLK_CTRL22 also holds RMT's, and this is a live board. + +/// Restored through ESP-IDF's side, never through the code under test. `differ.zig` runs restore, +/// idf, snapshot, restore, ours, snapshot: with the HAL on both the restore and the "ours" side, a +/// HAL function that does nothing leaves run B's snapshot equal to run A's and the case passes. That +/// makes a suite blind to precisely the failure it was written to catch. +fn restoreClk() void { + oracle_ledc_set_clock_source(0); // 0 = XTAL, the value idfSrcXtal uses + oracle_ledc_enable_function_clock(1); +} + +/// One word: `HP_SYS_CLKRST_PERI_CLK_CTRL22_REG`, which on the P4 holds LEDC's clock source select +/// and its function-clock gate (ledc_ll.h:179, :241). This is where the LEDC clock source lives on +/// this die - not in `LEDC_CONF_REG.APB_CLK_SEL`, which the register map still documents with a +/// *different* encoding and which IDF's P4 LL never writes. A HAL that wrote the in-block register +/// would pass every case in the `ledc` suite above and produce no PWM at all; this window is what +/// makes that visible. +/// +/// The case order matters: the last case must leave the function clock on and the source at XTAL, +/// because the harness restores *before* each case and not after the last one. +pub const clock_suite: types.Suite = .{ + .descriptor = .{ + .name = "ledc_clk", + .base = @intCast(regs.HP_SYS_CLKRST_PERI_CLK_CTRL22_REG), + .words = 1, + .restore = .{ .configure = restoreClk }, + }, + .setup = setup, + .cases = &.{ + .{ .name = "clock_source_rc_fast", .arg = 1, .idf = idfSrcRcFast, .ours = ourSrcRcFast }, + .{ .name = "clock_source_pll_div", .arg = 2, .idf = idfSrcPllDiv, .ours = ourSrcPllDiv }, + .{ .name = "clock_source_xtal", .arg = 0, .idf = idfSrcXtal, .ours = ourSrcXtal }, + .{ .name = "function_clock_off", .arg = 0, .idf = idfFuncClkOff, .ours = ourFuncClkOff }, + .{ .name = "function_clock_on", .arg = 1, .idf = idfFuncClkOn, .ours = ourFuncClkOn }, + }, +}; + +/// All four windows, in the order they should run: the block first, because a failure there explains +/// failures in the other three. +pub const suites = [_]types.Suite{ suite, gamma_suite, routing_suite, clock_suite }; + +// --------------------------------------------------------------------------------- the case pairs +// +// `catch {}` rather than `catch unreachable` on the `configureTimer` calls: all four divider values +// are inside the field's range (checked on the host against IDF's own expression), so the error path +// is dead - but if this HAL's validity check ever disagreed with IDF's, doing nothing leaves the +// timer unconfigured and the harness reports a diff, where `unreachable` would be undefined +// behaviour in a ReleaseSmall build and would report nothing. + +fn idfTimer1k13() void { + oracle_ledc_configure_timer(timer, src_hz, 1_000, 13); +} +fn ourTimer1k13() void { + ledc.configureTimer(timer, .{ .src_hz = src_hz, .freq_hz = 1_000, .resolution = 13 }) catch {}; +} +fn idfTimer20k10() void { + oracle_ledc_configure_timer(timer, src_hz, 20_000, 10); +} +fn ourTimer20k10() void { + ledc.configureTimer(timer, .{ .src_hz = src_hz, .freq_hz = 20_000, .resolution = 10 }) catch {}; +} +fn idfTimer5k10() void { + oracle_ledc_configure_timer(timer, src_hz, 5_000, 10); +} +fn ourTimer5k10() void { + ledc.configureTimer(timer, .{ .src_hz = src_hz, .freq_hz = 5_000, .resolution = 10 }) catch {}; +} +fn idfTimer300_14() void { + oracle_ledc_configure_timer(timer, src_hz, 300, 14); +} +fn ourTimer300_14() void { + ledc.configureTimer(timer, .{ .src_hz = src_hz, .freq_hz = 300, .resolution = 14 }) catch {}; +} + +// Each side computes the divider with its own arithmetic and stores it with its own code: 40 MHz, +// 1 kHz, 13 bits, which is 1250 = 0x4E2 = 4.8828 in Q10.8. +fn idfDivider() void { + oracle_ledc_set_clock_divider(timer, oracle_ledc_divisor(src_hz, 1_000, 1 << 13)); + oracle_ledc_commit_timer(timer); +} +fn ourDivider() void { + ledc.setClockDivider(timer, ledc.divisor(src_hz, 1_000, 13)); + ledc.commitTimer(timer); +} + +fn idfResolution() void { + oracle_ledc_set_duty_resolution(timer, 13); + oracle_ledc_commit_timer(timer); +} +fn ourResolution() void { + ledc.setDutyResolution(timer, 13); + ledc.commitTimer(timer); +} + +fn idfPause() void { + oracle_ledc_pause_timer(timer); +} +fn ourPause() void { + ledc.pauseTimer(timer); +} +fn idfResume() void { + oracle_ledc_resume_timer(timer); +} +fn ourResume() void { + ledc.resumeTimer(timer); +} +fn idfTimerReset() void { + oracle_ledc_reset_timer(timer); +} +fn ourTimerReset() void { + ledc.resetTimer(timer); +} + +fn idfBind() void { + oracle_ledc_bind_timer(channel, timer); + oracle_ledc_commit_channel(channel); +} +fn ourBind() void { + ledc.bindTimer(channel, timer); + ledc.commitChannel(channel); +} + +fn idfHpoint() void { + oracle_ledc_set_hpoint(channel, 0x400); + oracle_ledc_commit_channel(channel); +} +fn ourHpoint() void { + ledc.setHpoint(channel, 0x400); + ledc.commitChannel(channel); +} + +fn idfDuty4096() void { + oracle_ledc_set_duty(channel, 0x1000); + oracle_ledc_commit_channel(channel); +} +fn ourDuty4096() void { + ledc.setDuty(channel, 0x1000); + ledc.commitChannel(channel); +} +fn idfDuty0() void { + oracle_ledc_set_duty(channel, 0); + oracle_ledc_commit_channel(channel); +} +fn ourDuty0() void { + ledc.setDuty(channel, 0); + ledc.commitChannel(channel); +} + +// No commit on either side. The staged duty and gamma entry must still match. +fn idfStageOnly() void { + oracle_ledc_set_duty(channel, 0x555); +} +fn ourStageOnly() void { + ledc.setDuty(channel, 0x555); +} + +fn idfStart() void { + oracle_ledc_start(channel); +} +fn ourStart() void { + ledc.start(channel); +} +fn idfStopLow() void { + oracle_ledc_stop(channel, 0); +} +fn ourStopLow() void { + ledc.stop(channel, 0); +} +fn idfStopHigh() void { + oracle_ledc_stop(channel, 1); +} +fn ourStopHigh() void { + ledc.stop(channel, 1); +} + +fn idfConfigureChannel() void { + oracle_ledc_configure_channel(channel, timer, 0x800, 0x200, 1, 1); +} +fn ourConfigureChannel() void { + ledc.configureChannel(channel, .{ + .timer = timer, + .duty = 0x800, + .hpoint = 0x200, + .idle_level = 1, + .output_enabled = true, + }); +} + +fn idfAttachPin() void { + oracle_ledc_set_pin(pin, channel); +} +fn ourAttachPin() void { + ledc.attachPin(channel, pin); +} +// Channel 7 explicitly, because the signal index is arithmetic on the channel number and 0 is the +// one value that cannot catch an off-by-one in it. +fn idfAttachPin7() void { + oracle_ledc_set_pin(pin, 7); +} +fn ourAttachPin7() void { + ledc.attachPin(7, pin); +} + +/// The report's RF configuration, end to end: 1-bit resolution at 25 MHz, duty 1, hpoint 0, on +/// channel 0 / timer 0. Reproduced from the ESP-IDF firmware in 02-esp32p4-m3-radio/main/main.c:77-94. +/// +/// This case exists because the two implementations disagree *on the die* for exactly this +/// configuration and nothing smaller: the IDF firmware's carrier toggles GPIO20 at 25 MHz (proven by +/// its own ADC witness catching both rails), and this project's HAL leaves the pad static, while +/// every individual register operation compares equal. So the difference is in the composition, and +/// comparing the whole block after each full bring-up is the only thing that can localise it. +/// Note the source: 80 MHz, not this suite's default `src_hz` (which is XTAL at 40 MHz). At 40 MHz a +/// 1-bit 25 MHz target needs divider 205, below the legal minimum of 256, and the two sides then +/// disagree for a reason that has nothing to do with the RF experiment: this HAL rejects it with +/// DividerOutOfRange while ESP-IDF's *LL* programs it anyway, because IDF's range check lives one +/// layer up in ledc.c rather than in the LL. Worth knowing - it means an IDF LL caller can silently +/// program an illegal divider - but it is not what this case is for. +fn idfFullRf() void { + oracle_ledc_configure_timer(0, ledc.pll_div_hz, 25_000_000, 1); + oracle_ledc_configure_channel(0, 0, 1, 0, 0, 1); +} +fn ourFullRf() void { + ledc.configureTimer(0, .{ .src_hz = ledc.pll_div_hz, .freq_hz = 25_000_000, .resolution = 1 }) catch return; + ledc.configureChannel(0, .{ .timer = 0, .duty = 1, .hpoint = 0, .idle_level = 0 }); +} + +fn idfSrcXtal() void { + oracle_ledc_set_clock_source(0); +} +fn ourSrcXtal() void { + ledc.setClockSource(.xtal); +} +fn idfSrcRcFast() void { + oracle_ledc_set_clock_source(1); +} +fn ourSrcRcFast() void { + ledc.setClockSource(.rc_fast); +} +fn idfSrcPllDiv() void { + oracle_ledc_set_clock_source(2); +} +fn ourSrcPllDiv() void { + ledc.setClockSource(.pll_div); +} +fn idfFuncClkOff() void { + oracle_ledc_enable_function_clock(0); +} +fn ourFuncClkOff() void { + ledc.setFunctionClockEnabled(false); +} +fn idfFuncClkOn() void { + oracle_ledc_enable_function_clock(1); +} +fn ourFuncClkOn() void { + ledc.setFunctionClockEnabled(true); +} + diff --git a/src/oracle/ledc_ref.c b/src/oracle/ledc_ref.c new file mode 100644 index 0000000..6b1403a --- /dev/null +++ b/src/oracle/ledc_ref.c @@ -0,0 +1,210 @@ +/* LEDC's reference implementation: ESP-IDF's own LL, given external linkage. + * + * There is no logic here except where a comment says otherwise, and there is exactly one such + * place - `oracle_ledc_divisor` - because the divider arithmetic lives in a `static inline` inside + * `esp_driver_ledc/src/ledc.c` and is therefore unreachable from a header. It is transcribed + * character for character, with the line number, so that the on-die comparison covers the + * arithmetic and not only the store that follows it. + */ + +/* IDF's clock and reset LL functions are shadowed by a wrapper macro that references + * `__DECLARE_RCC_ATOMIC_ENV`, an identifier IDF never defines anywhere; its purpose is to make an + * unguarded call fail to compile, because the only legal caller holds a spinlock. There is no + * FreeRTOS here and core 1 is held in reset at power-on, so declaring the name is exactly as safe + * as the spinlock would be - and it is what IDF's own bootloader does. */ +static int __DECLARE_RCC_ATOMIC_ENV __attribute__((unused)); + +/* `ledc_ll_set_slow_clk_sel` and `ledc_ll_get_slow_clk_sel` call `abort()` in the default arm of + * their switch (ledc_ll.h:238, :273). Freestanding, nothing declares it; the arms below are all + * reached with constants, so the call folds away and no definition is needed. */ +void abort(void); + +#include + +#include "hal/ledc_ll.h" +#include "hal/gpio_ll.h" +#include "soc/gpio_struct.h" +#include "soc/gpio_sig_map.h" + +/* P4 has one speed mode: low. `ledc_ll.h` still takes the parameter because the LL is shared with + * parts that have two. */ +#define MODE LEDC_LOW_SPEED_MODE + +/* ---------------------------------------------------------------- clocks, outside the LEDC block */ + +void oracle_ledc_enable_bus_clock(int enable) +{ + ledc_ll_enable_bus_clock(enable != 0); +} + +void oracle_ledc_enable_function_clock(int enable) +{ + ledc_ll_enable_clock(LEDC_LL_GET_HW(), enable != 0); +} + +/* `sel` is this project's `ClockSource` enum, which is HP_SYS_CLKRST's own encoding: 0 XTAL, + * 1 RC_FAST, 2 PLL_DIV. Split into three constant calls so that IDF's switch folds and its + * `abort()` arm never reaches the linker. */ +void oracle_ledc_set_clock_source(unsigned sel) +{ + switch (sel) { + case 0: + ledc_ll_set_slow_clk_sel(LEDC_LL_GET_HW(), LEDC_SLOW_CLK_XTAL); + break; + case 1: + ledc_ll_set_slow_clk_sel(LEDC_LL_GET_HW(), LEDC_SLOW_CLK_RC_FAST); + break; + case 2: + ledc_ll_set_slow_clk_sel(LEDC_LL_GET_HW(), LEDC_SLOW_CLK_PLL_DIV); + break; + default: + break; + } +} + +/* ---------------------------------------------------------------------------- divider arithmetic */ + +/* Verbatim from esp_driver_ledc/src/ledc.c:468-497 (v6.0.2), which is `static inline` in a .c file + * and so cannot be called. The 32-bit wrap of `freq_hz * precision` and the truncation of the + * 64-bit quotient into `uint32_t` are IDF's, and are the whole reason this exists: they are what + * src/hal/ledc.zig's `divisor` has to reproduce. */ +uint32_t oracle_ledc_divisor(uint32_t src_clk_freq, int freq_hz, uint32_t precision) +{ + return (((uint64_t) src_clk_freq << LEDC_LL_FRACTIONAL_BITS) + freq_hz * precision / 2) + / (freq_hz * precision); +} + +/* ------------------------------------------------------------------------------------- timers */ + +void oracle_ledc_set_clock_divider(unsigned timer, uint32_t div) +{ + ledc_ll_set_clock_divider(LEDC_LL_GET_HW(), MODE, (ledc_timer_t)timer, div); +} + +void oracle_ledc_set_duty_resolution(unsigned timer, uint32_t bits) +{ + ledc_ll_set_duty_resolution(LEDC_LL_GET_HW(), MODE, (ledc_timer_t)timer, bits); +} + +void oracle_ledc_commit_timer(unsigned timer) +{ + ledc_ll_ls_timer_update(LEDC_LL_GET_HW(), MODE, (ledc_timer_t)timer); +} + +void oracle_ledc_reset_timer(unsigned timer) +{ + ledc_ll_timer_rst(LEDC_LL_GET_HW(), MODE, (ledc_timer_t)timer); +} + +void oracle_ledc_pause_timer(unsigned timer) +{ + ledc_ll_timer_pause(LEDC_LL_GET_HW(), MODE, (ledc_timer_t)timer); +} + +void oracle_ledc_resume_timer(unsigned timer) +{ + ledc_ll_timer_resume(LEDC_LL_GET_HW(), MODE, (ledc_timer_t)timer); +} + +/* `ledc_set_timer_params` (ledc.c:244-261) followed by the resume/reset pair `ledc_timer_config` + * does on success (ledc.c:816-818). The clock-source step of `ledc_set_timer_params` is absent on + * purpose: on the P4 there is no timer-specific mux (SOC_LEDC_HAS_TIMER_SPECIFIC_MUX is unset), so + * that step compiles out of IDF too. */ +void oracle_ledc_configure_timer(unsigned timer, uint32_t src_hz, int freq_hz, uint32_t resolution) +{ + uint32_t div = oracle_ledc_divisor(src_hz, freq_hz, 1u << resolution); + ledc_ll_set_clock_divider(LEDC_LL_GET_HW(), MODE, (ledc_timer_t)timer, div); + ledc_ll_set_duty_resolution(LEDC_LL_GET_HW(), MODE, (ledc_timer_t)timer, resolution); + ledc_ll_ls_timer_update(LEDC_LL_GET_HW(), MODE, (ledc_timer_t)timer); + ledc_ll_timer_resume(LEDC_LL_GET_HW(), MODE, (ledc_timer_t)timer); + ledc_ll_timer_rst(LEDC_LL_GET_HW(), MODE, (ledc_timer_t)timer); +} + +/* ------------------------------------------------------------------------------------ channels */ + +void oracle_ledc_bind_timer(unsigned channel, unsigned timer) +{ + ledc_ll_bind_channel_timer(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel, (ledc_timer_t)timer); +} + +void oracle_ledc_set_hpoint(unsigned channel, uint32_t hpoint) +{ + ledc_ll_set_hpoint(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel, hpoint); +} + +/* `ledc_duty_config` (ledc.c:263-280) with `hpoint_val` left alone: the duty integer part, then the + * degenerate one-step fade in gamma RAM entry 0 that a constant duty needs on this die, then the + * range count. `ledc_hal_clear_left_off_fade_param` is deliberately not called - it zeroes ranges + * 1..15, which only matters once real fades are in scope. */ +void oracle_ledc_set_duty(unsigned channel, uint32_t duty) +{ + ledc_ll_set_duty_int_part(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel, duty); + ledc_ll_set_fade_param_range(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel, 0, 1, 1, 0, 1); + ledc_ll_set_range_number(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel, 1); +} + +void oracle_ledc_set_output_enabled(unsigned channel, int enable) +{ + ledc_ll_set_sig_out_en(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel, enable != 0); +} + +void oracle_ledc_set_idle_level(unsigned channel, uint32_t level) +{ + ledc_ll_set_idle_level(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel, level); +} + +void oracle_ledc_start_fade(unsigned channel) +{ + ledc_ll_set_duty_start(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel); +} + +void oracle_ledc_commit_channel(unsigned channel) +{ + ledc_ll_ls_channel_update(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel); +} + +/* `_ledc_update_duty`, ledc.c:1021-1026. */ +void oracle_ledc_start(unsigned channel) +{ + ledc_ll_set_sig_out_en(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel, true); + ledc_ll_set_duty_start(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel); + ledc_ll_ls_channel_update(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel); +} + +/* `ledc_stop`, ledc.c:1039-1050: idle level staged before the output is disabled, one commit. */ +void oracle_ledc_stop(unsigned channel, uint32_t idle_level) +{ + ledc_ll_set_idle_level(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel, idle_level); + ledc_ll_set_sig_out_en(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel, false); + ledc_ll_ls_channel_update(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel); +} + +/* The register half of `ledc_channel_config` (ledc.c:869-1019): stage timer, hpoint, duty, idle + * level and output enable, hand the duty over, commit once. */ +void oracle_ledc_configure_channel(unsigned channel, unsigned timer, uint32_t duty, uint32_t hpoint, + uint32_t idle_level, int output_enabled) +{ + ledc_ll_bind_channel_timer(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel, (ledc_timer_t)timer); + ledc_ll_set_hpoint(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel, hpoint); + ledc_ll_set_duty_int_part(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel, duty); + ledc_ll_set_fade_param_range(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel, 0, 1, 1, 0, 1); + ledc_ll_set_range_number(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel, 1); + ledc_ll_set_idle_level(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel, idle_level); + ledc_ll_set_sig_out_en(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel, output_enabled != 0); + ledc_ll_set_duty_start(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel); + ledc_ll_ls_channel_update(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel); +} + +/* ---------------------------------------------------------------------------------- pin routing */ + +/* The hardware effect of `ledc_set_pin` (ledc.c:823-836). `gpio_matrix_output` is + * `gpio_hal_matrix_out` (gpio_hal.c:60-69): pad function, matrix source, then the output-enable + * control last "to avoid undesired level change". The signal index is + * `ledc_periph_signal[0].sig_out0_idx + channel`, and that field is initialised to + * `LEDC_LS_SIG_OUT_PAD_OUT0_IDX` in esp_hal_ledc/esp32p4/ledc_periph.c:14-18. */ +void oracle_ledc_set_pin(unsigned pin, unsigned channel) +{ + gpio_ll_func_sel(&GPIO, pin, PIN_FUNC_GPIO); + gpio_ll_set_output_signal_matrix_source(&GPIO, pin, LEDC_LS_SIG_OUT_PAD_OUT0_IDX + channel, false); + gpio_ll_set_output_enable_ctrl(&GPIO, pin, true, false); +} diff --git a/src/oracle/oracle_sdkconfig.h b/src/oracle/oracle_sdkconfig.h new file mode 100644 index 0000000..c74fb50 --- /dev/null +++ b/src/oracle/oracle_sdkconfig.h @@ -0,0 +1,43 @@ +/* The Kconfig surface ESP-IDF's LL headers are compiled against when they are used as the + * differential reference. Deliberately minimal and deliberately *ours*. + * + * An earlier attempt borrowed sdkconfig.h from an unrelated ESP-IDF project in this workspace. That + * is a trap with a measurable cost: the borrowed file sets CONFIG_HAL_GPIO_USE_ROM_IMPL=1, which + * makes gpio_ll_set_level() call rom_gpio_set_output_level() and write no GPIO register at all - + * so the very first differential would have compared this HAL against the mask ROM rather than + * against IDF's register sequence. Recompiling the same LL headers against a different sdkconfig + * changes the emitted .text of six of nine tier-1/2 peripherals, so this file is part of the + * experiment's definition, not incidental. + * + * Anything not defined here is simply absent, which for IDF's `#if` tests means zero. That is the + * behaviour we want: the register path, with nothing optional switched on. + */ +#pragma once + +/* Target selection. Everything under components/soc and components/hal keys off this. */ +#define CONFIG_IDF_TARGET_ESP32P4 1 +#define CONFIG_IDF_TARGET "esp32p4" + +/* Pre-v3 silicon: this die is rev v1.3. The same condition selects register/hw_ver1 in IDF's own + * build (soc/CMakeLists.txt:37-41) and esp32p4.rom.ld rather than esp32p4.rom.eco5.ld - 237 of 452 + * common ROM symbols have different addresses between those two files, so the pairing is not + * cosmetic. build.zig asserts the register module was built from hw_ver1 to match. */ +#define CONFIG_ESP32P4_SELECTS_REV_LESS_V3 1 +#define CONFIG_ESP32P4_REV_MIN_FULL 100 +#define CONFIG_ESP32P4_REV_MAX_FULL 199 + +/* 40 MHz crystal, as fitted. Reaches the HAL through HAL_CONFIG_XTAL_HINT_FREQ_MHZ. */ +#define CONFIG_XTAL_FREQ 40 + +/* Assertions off, and this one is a real choice rather than tidiness: at level 2 HAL_ASSERT expands + * to __assert_func (a libc symbol this image does not have), and at 0 it becomes + * __builtin_unreachable(), which lets clang delete the argument-checking branches. The reference + * implementation should be the code IDF ships in a release build, and a harness that wants to test + * argument validation must not rely on a branch the compiler is entitled to remove. */ +#define CONFIG_HAL_DEFAULT_ASSERTION_LEVEL 0 + +/* NOT defined, on purpose: + * CONFIG_HAL_GPIO_USE_ROM_IMPL - would route gpio_ll_set_level through the mask ROM (see above). + * CONFIG_IDF_ENV_FPGA - would change efuse and clock behaviour to the FPGA model. + * CONFIG_PM_*, CONFIG_FREERTOS_* - no power management and no OS in this image. + */ diff --git a/src/oracle/runtime_ref.c b/src/oracle/runtime_ref.c new file mode 100644 index 0000000..659661b --- /dev/null +++ b/src/oracle/runtime_ref.c @@ -0,0 +1,19 @@ +/* The few libc symbols ESP-IDF's LL code reaches for, supplied so the reference can link into a + * freestanding image. + * + * There is exactly one so far, and it is reached by design rather than by accident: + * `_uart_ll_set_baudrate` (uart_ll.h:532-535) calls `abort()` when handed an LP_UART instance, + * because that path needs `lp_uart_ll_set_baudrate` instead. The differential harness only ever + * passes HP UART instances, so this is unreachable in practice - but the linker does not know that, + * and a missing `abort` fails the build with a symbol name that explains nothing about why. + * + * Spinning rather than resetting is deliberate: if a reference implementation ever does call this, + * the board stops with its last console line intact, which is the difference between a diagnosable + * failure and a reboot loop. + */ + +__attribute__((noreturn)) void abort(void) +{ + for (;;) { + } +} diff --git a/src/oracle/sdmmc_cases.zig b/src/oracle/sdmmc_cases.zig new file mode 100644 index 0000000..4b53aed --- /dev/null +++ b/src/oracle/sdmmc_cases.zig @@ -0,0 +1,567 @@ +//! SDMMC's side of the differential test. +//! +//! Two windows, because this peripheral's state is not contiguous. The controller's own register +//! block is at 0x50083000; its *host* clock generator - source mux, two-stage divider, sampling +//! phase - is not in it at all, but in HP_SYS_CLKRST, where the P4 moved it. That is the same +//! split I2C has (`i2c.clock_suite`), and for the same reason: a driver that programmed every +//! register inside the block perfectly and the divider not at all would run the bus at the wrong +//! frequency and pass every case in the first suite. +//! +//! **No case sends a command to the card.** The five `cmd_word_*` cases write the command register +//! with `start_command` (bit 31) cleared, which is what makes them safe: bit 31 is the launch, and +//! a word without it is inert. The C6 is in reset for the whole of a differ run - GPIO54 is never +//! released - so a real CMD52 would sit out its response timeout and prove nothing. What is being +//! compared is the encoding, and the encoding is entirely visible in the staged word. +//! +//! **Restore is the peripheral's own reset**, LP_AON_CLKRST bit 28, which is legitimate here and +//! not merely convenient: this block is full of self-clearing and write-1-to-clear bits (the three +//! reset bits in CTRL, every bit of RINTSTS, the IDMAC's software reset), and writing a snapshot +//! back would trigger a reset rather than undo one. Nothing in this file restores through the code +//! under test; the only Zig the harness runs between the two halves is `mmio`. + +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_sdmmc_bus_clock(enable: c_int) void; +extern fn oracle_sdmmc_reset_register() void; +extern fn oracle_sdmmc_set_host_clock_div(div: c_uint) void; +extern fn oracle_sdmmc_select_clk_source_pll160m() void; +extern fn oracle_sdmmc_init_phase_delay() void; +extern fn oracle_sdmmc_set_card_clock_div(slot: c_uint, div: c_uint) void; +extern fn oracle_sdmmc_enable_card_clock(slot: c_uint, enable: c_int) void; +extern fn oracle_sdmmc_enable_card_clock_low_power(slot: c_uint, enable: c_int) void; +extern fn oracle_sdmmc_reset_controller() void; +extern fn oracle_sdmmc_reset_dma() void; +extern fn oracle_sdmmc_reset_fifo() void; +extern fn oracle_sdmmc_module_reset() void; +extern fn oracle_sdmmc_set_card_width(slot: c_uint, width: c_uint) void; +extern fn oracle_sdmmc_set_block_size(size: c_uint) void; +extern fn oracle_sdmmc_set_data_transfer_len(len: c_uint) void; +extern fn oracle_sdmmc_set_timeouts(data_cycles: c_uint, response_cycles: c_uint) void; +extern fn oracle_sdmmc_set_fifo_threshold(rx: c_uint, tx: c_uint, msize: c_uint) void; +extern fn oracle_sdmmc_configure_interrupts() void; +extern fn oracle_sdmmc_init_dma() void; +extern fn oracle_sdmmc_enable_dma(enable: c_int) void; +extern fn oracle_sdmmc_set_desc_addr(addr: c_uint) void; +extern fn oracle_sdmmc_enable_sdio_interrupt(slot: c_uint, enable: c_int) void; +extern fn oracle_sdmmc_stage_command( + index: c_uint, + response_long: c_int, + response_expect: c_int, + check_crc: c_int, + data: c_int, + send_init: c_int, + wait_prvdata: c_int, + update_clk: c_int, + slot: c_uint, +) void; +extern fn oracle_sdmmc_version_id() c_uint; +extern fn oracle_sdmmc_hw_config() c_uint; + +/// Printed by the harness's caller, so a run records which controller it was talking to. A version +/// ID of 0 or 0xffffffff means the block is gated or absent and every result below is noise. +pub fn versionId() u32 { + return oracle_sdmmc_version_id(); +} + +pub fn hwConfig() u32 { + return oracle_sdmmc_hw_config(); +} + +// Force the whole of `hal/sdmmc.zig` through the compiler for the *chip*. +// +// Zig analyses a function only when something references it, and this is the only build that +// compiles that file for riscv32 at all - the plain application never mentions SDMMC, and the +// host test root reaches only the pure encoding functions (it cannot reach the rest: reading the +// `cycle` CSR does not assemble for x86). So without this list, `cmd53Read`, `cardInit` and the +// whole transfer path would be text that has never been type-checked against the target, which is +// a bad thing to discover on a board. +// +// A `-Doracle` build failing here is the intended behaviour: it means the driver does not +// compile, and it says so before anything is flashed. +comptime { + _ = &hal.sdmmc.init; + _ = &hal.sdmmc.cardInit; + _ = &hal.sdmmc.cmd52Read; + _ = &hal.sdmmc.cmd52Write; + _ = &hal.sdmmc.cmd53Read; + _ = &hal.sdmmc.cmd53Write; + _ = &hal.sdmmc.slaveInterruptPending; + _ = &hal.sdmmc.clearSlaveInterrupt; + _ = &hal.sdmmc.setSlaveInterruptEnabled; + _ = &hal.sdmmc.rca; + _ = &hal.sdmmc.configurePins; + _ = &hal.sdmmc.setBusClock; + _ = &hal.sdmmc.dividersFor; + _ = &hal.sdmmc.cmd52Arg; + _ = &hal.sdmmc.cmd53Arg; + _ = hal.sdmmc.interrupt_source; + _ = hal.sdmmc.bounce_len; + _ = hal.sdmmc.c6_pins; +} + +/// The slot under test. Slot 1 is where the ESP32-C6 is; slot 0's pads are the P4's own flash +/// interface on this board and are never touched. +const slot: u1 = 1; + +const cmd_reg = mmio.Reg.atAddress(@intCast(regs.SDHOST_CMD_REG)); + +/// Stage the word our HAL would send, with the launch bit removed. `hal.sdmmc.commandWord` is the +/// code under test; the store is one line and is not. +fn stage(c: hal.sdmmc.Command) void { + cmd_reg.writeRaw(hal.sdmmc.commandWord(c) & ~(@as(u32, 1) << 31)); +} + +// -------------------------------------------------------------------------- the register block + +pub const suite: types.Suite = .{ + .descriptor = .{ + .name = "sdmmc", + .base = @intCast(regs.SDHOST_CTRL_REG), // offset 0 of the block + // 0x000 through ENSHIFT at +0x110. The window deliberately stops short of BUFFIFO at + // +0x200: that is the data FIFO, and a snapshot loop that read it would pop received + // words - the same hazard `UART_FIFO_REG` poses at offset 0 of every UART. The three + // registers above it (CLK_EDGE_SEL, RAW_INTS, DLL_CLK_CONF at +0x800) belong to the + // high-speed delay-line path this driver does not use. + .words = 69, + .volatile_words = &.{ + (0x40 - 0x00) / 4, // MINTSTS - the C6 can raise its SDIO interrupt at any moment + (0x44 - 0x00) / 4, // RINTSTS - likewise, and write-1-to-clear + (0x48 - 0x00) / 4, // STATUS - FIFO count, FSM state, live DAT levels + (0x50 - 0x00) / 4, // CDETECT - a live input + (0x54 - 0x00) / 4, // WRTPRT - a live input + (0x5c - 0x00) / 4, // TCBCNT - transferred card byte count + (0x60 - 0x00) / 4, // TBBCNT - transferred host byte count + (0x8c - 0x00) / 4, // IDSTS - IDMAC status, write-1-to-clear + (0x94 - 0x00) / 4, // DSCADDR - the IDMAC's current descriptor pointer + (0x98 - 0x00) / 4, // BUFADDR - the IDMAC's current buffer pointer + }, + // Unlike most of this chip, SDMMC powers up with its bus clock *off* + // (HP_SYS_CLKRST SOC_CLK_CTRL1 REG_SDMMC_SYS_CLK_EN, default 0), so this check is the one + // that catches a setup that silently did not happen: a gated block returns the last value + // latched, not zeros, and two such snapshots compare equal while describing nothing. + .clock = .{ + .reg = @intCast(regs.HP_SYS_CLKRST_SOC_CLK_CTRL1_REG), + .bit = @intCast(regs.HP_SYS_CLKRST_REG_SDMMC_SYS_CLK_EN_S), + }, + // LP_AON_CLKRST.hp_sdmmc_emac_rst_ctrl.rst_en_sdmmc - `sdmmc_ll.h:158-163`. Not in + // HP_SYS_CLKRST with almost every other peripheral's reset, which is the single most + // surprising fact about this block's clock and reset wiring. + .restore = .{ .reset_bit = .{ + .reg = @intCast(regs.LP_CLKRST_HP_SDMMC_EMAC_RST_CTRL_REG), + .bit = @intCast(regs.LP_CLKRST_RST_EN_SDMMC_S), + } }, + }, + .cases = &.{ + // --- resets. Each of the three bits is self-clearing, so what these compare is mostly + // that the *other* bits of CTRL come out the same: a reset function that wrote bit 5 + // (dma_enable) instead of bit 2 (dma_reset) would leave a trace, and that is exactly the + // kind of slip the two undocumented CTRL bits invite. + .{ .name = "reset_controller", .idf = idfResetCtl, .ours = ourResetCtl }, + .{ .name = "reset_fifo", .idf = idfResetFifo, .ours = ourResetFifo }, + .{ .name = "reset_dma", .idf = idfResetDma, .ours = ourResetDma }, + .{ .name = "module_reset", .idf = idfModuleReset, .ours = ourModuleReset }, + // --- the card clock: CLKDIV, CLKSRC, CLKENA. Divider 0 is bypass (40 MHz through the + // host divider alone); divider 20 is the 400 kHz probing setting. + .{ .name = "card_clock_div", .arg = 0, .idf = idfCardDiv0, .ours = ourCardDiv0 }, + .{ .name = "card_clock_div", .arg = 20, .idf = idfCardDiv20, .ours = ourCardDiv20 }, + .{ .name = "card_clock_enable", .arg = 1, .idf = idfCclkOn, .ours = ourCclkOn }, + .{ .name = "card_clock_low_power", .arg = 0, .idf = idfLpOff, .ours = ourLpOff }, + .{ .name = "card_clock_low_power", .arg = 1, .idf = idfLpOn, .ours = ourLpOn }, + // --- bus width. The measured working dump has ctype=0x00000002, i.e. bit 1: slot 1 in + // 4-bit mode, which is what `bus_width(4)` must produce and nothing else. + .{ .name = "bus_width", .arg = 4, .idf = idfWidth4, .ours = ourWidth4 }, + .{ .name = "bus_width", .arg = 1, .idf = idfWidth1, .ours = ourWidth1 }, + // --- transfer geometry. + .{ .name = "block_size", .arg = 512, .idf = idfBlk512, .ours = ourBlk512 }, + .{ .name = "block_size", .arg = 4, .idf = idfBlk4, .ours = ourBlk4 }, + .{ .name = "timeouts", .idf = idfTimeouts, .ours = ourTimeouts }, + // A deliberately non-default watermark set, so the case is not "both wrote the reset + // value". ESP-IDF has no LL function for FIFOTH at all and never writes the register on + // any target, so the reference here goes through IDF's `SDMMC.fifoth` bitfields instead - + // which is still IDF's definition of where those three fields sit. + .{ .name = "fifo_threshold", .arg = 255, .idf = idfFifoth, .ours = ourFifoth }, + .{ .name = "fifo_threshold_default", .arg = 511, .idf = idfFifothDefault, .ours = ourFifothDefault }, + // --- interrupts and DMA. + .{ .name = "configure_interrupts", .idf = idfIntrs, .ours = ourIntrs }, + .{ .name = "sdio_interrupt", .arg = 1, .idf = idfSdioIntOn, .ours = ourSdioIntOn }, + .{ .name = "init_dma", .idf = idfInitDma, .ours = ourInitDma }, + .{ .name = "desc_addr", .idf = idfDescAddr, .ours = ourDescAddr }, + // --- command-word encodings. The five commands `cardInit` sends, plus both directions of + // CMD53 and the clock update command that is not a command at all. + .{ .name = "cmd_word_cmd0", .arg = 0, .idf = idfCmd0, .ours = ourCmd0 }, + .{ .name = "cmd_word_cmd5", .arg = 5, .idf = idfCmd5, .ours = ourCmd5 }, + .{ .name = "cmd_word_cmd3", .arg = 3, .idf = idfCmd3, .ours = ourCmd3 }, + .{ .name = "cmd_word_cmd7", .arg = 7, .idf = idfCmd7, .ours = ourCmd7 }, + .{ .name = "cmd_word_cmd52_read", .arg = 52, .idf = idfCmd52R, .ours = ourCmd52R }, + .{ .name = "cmd_word_cmd52_write", .arg = 52, .idf = idfCmd52W, .ours = ourCmd52W }, + .{ .name = "cmd_word_cmd53_read", .arg = 53, .idf = idfCmd53R, .ours = ourCmd53R }, + .{ .name = "cmd_word_cmd53_write", .arg = 53, .idf = idfCmd53W, .ours = ourCmd53W }, + .{ .name = "cmd_word_clock_update", .idf = idfCmdClk, .ours = ourCmdClk }, + // --- the peripheral reset itself, which is in LP_AON_CLKRST and observable here only by + // its effect: configure the block distinctively through IDF's LL on both sides, then let + // each implementation reset it. A `resetPeripheral(.sdmmc)` that wrote the wrong bit - + // there is no HP_SYS_CLKRST reset for SDMMC, so writing one is the obvious mistake - would + // leave the configuration standing. + .{ .name = "peripheral_reset", .idf = idfPeriphReset, .ours = ourPeriphReset }, + }, + .setup = setup, +}; + +/// Bring the block up far enough that its registers are live, and settle the HAL's idea of which +/// slot it is driving. +/// +/// The clock and the reset go through ESP-IDF's LL, not ours: setup runs once, before any case, +/// and a setup written with the code under test would hide a broken `clkrst.init(.sdmmc)` behind +/// its own success. `hal.sdmmc.init` runs afterwards for a different reason - it is the only way +/// to tell the HAL that this is slot 1, and running it here means a bring-up that hangs shows up +/// as a stalled suite rather than as a wrong register somewhere later. Its result is discarded: +/// every case restores the block by resetting it, so nothing init leaves behind is load-bearing, +/// and a card that never answers must not stop the register comparison from running. +fn setup() void { + oracle_sdmmc_bus_clock(1); + oracle_sdmmc_reset_register(); + hal.sdmmc.init(.{ .slot = slot, .width = .four, .khz = 40_000 }) catch {}; +} + +fn idfResetCtl() void { + oracle_sdmmc_reset_controller(); +} +fn ourResetCtl() void { + mmio.Reg.atAddress(@intCast(regs.SDHOST_CTRL_REG)).modify(.{ + mmio.Field.of(regs.SDHOST_CONTROLLER_RESET_S, regs.SDHOST_CONTROLLER_RESET_V).is(1), + }); +} +fn idfResetFifo() void { + oracle_sdmmc_reset_fifo(); +} +fn ourResetFifo() void { + mmio.Reg.atAddress(@intCast(regs.SDHOST_CTRL_REG)).modify(.{ + mmio.Field.of(regs.SDHOST_FIFO_RESET_S, regs.SDHOST_FIFO_RESET_V).is(1), + }); +} +fn idfResetDma() void { + oracle_sdmmc_reset_dma(); +} +fn ourResetDma() void { + mmio.Reg.atAddress(@intCast(regs.SDHOST_CTRL_REG)).modify(.{ + mmio.Field.of(regs.SDHOST_DMA_RESET_S, regs.SDHOST_DMA_RESET_V).is(1), + }); +} +fn idfModuleReset() void { + oracle_sdmmc_module_reset(); +} +fn ourModuleReset() void { + hal.sdmmc.resetController() catch {}; +} + +fn idfCardDiv0() void { + oracle_sdmmc_set_card_clock_div(slot, 0); +} +fn ourCardDiv0() void { + hal.sdmmc.setCardClockDiv(0); +} +fn idfCardDiv20() void { + oracle_sdmmc_set_card_clock_div(slot, 20); +} +fn ourCardDiv20() void { + hal.sdmmc.setCardClockDiv(20); +} + +fn idfCclkOn() void { + oracle_sdmmc_enable_card_clock(slot, 1); +} +fn ourCclkOn() void { + hal.sdmmc.setCardClockEnabled(true); +} +fn idfLpOff() void { + oracle_sdmmc_enable_card_clock_low_power(slot, 0); +} +fn ourLpOff() void { + hal.sdmmc.setCardClockLowPower(false); +} +fn idfLpOn() void { + oracle_sdmmc_enable_card_clock_low_power(slot, 1); +} +fn ourLpOn() void { + hal.sdmmc.setCardClockLowPower(true); +} + +fn idfWidth4() void { + oracle_sdmmc_set_card_width(slot, 4); +} +fn ourWidth4() void { + hal.sdmmc.setBusWidth(.four); +} +fn idfWidth1() void { + oracle_sdmmc_set_card_width(slot, 1); +} +fn ourWidth1() void { + hal.sdmmc.setBusWidth(.one); +} + +fn idfBlk512() void { + oracle_sdmmc_set_block_size(512); + oracle_sdmmc_set_data_transfer_len(512); +} +fn ourBlk512() void { + hal.sdmmc.setBlockSize(512); + hal.sdmmc.setDataTransferLen(512); +} +fn idfBlk4() void { + // The geometry the measured working dump was taken at: blksiz=4 bytcnt=4, the four-byte + // register read ESP-Hosted does to find out how much the slave has queued. + oracle_sdmmc_set_block_size(4); + oracle_sdmmc_set_data_transfer_len(4); +} +fn ourBlk4() void { + hal.sdmmc.setBlockSize(4); + hal.sdmmc.setDataTransferLen(4); +} + +fn idfTimeouts() void { + // 100 ms of card clocks at 40 MHz, and the maximum response timeout - `sd_host_sdmmc.c:531-535`. + oracle_sdmmc_set_timeouts(100 * 40_000, 255); +} +fn ourTimeouts() void { + hal.sdmmc.setTimeouts(100 * 40_000, 255); +} + +fn idfFifoth() void { + oracle_sdmmc_set_fifo_threshold(255, 8, 2); +} +fn ourFifoth() void { + hal.sdmmc.setFifoThreshold(255, 8, 2); +} +fn idfFifothDefault() void { + oracle_sdmmc_set_fifo_threshold(511, 0, 0); +} +fn ourFifothDefault() void { + hal.sdmmc.setFifoThreshold( + hal.sdmmc.default_rx_watermark, + hal.sdmmc.default_tx_watermark, + hal.sdmmc.default_dma_msize, + ); +} + +fn idfIntrs() void { + oracle_sdmmc_configure_interrupts(); +} +fn ourIntrs() void { + hal.sdmmc.configureInterrupts(); +} +fn idfSdioIntOn() void { + oracle_sdmmc_enable_sdio_interrupt(slot, 1); +} +fn ourSdioIntOn() void { + hal.sdmmc.setSlaveInterruptEnabled(true); +} + +fn idfInitDma() void { + oracle_sdmmc_init_dma(); + oracle_sdmmc_enable_dma(1); +} +fn ourInitDma() void { + hal.sdmmc.initDma(); + hal.sdmmc.setDmaEnabled(true); +} + +/// An address in L2MEM with the low bits set to something a bug would round away: DBADDR ignores +/// bits [1:0] internally but stores what is written. +const test_desc_addr: u32 = 0x4ff1_0140; + +fn idfDescAddr() void { + oracle_sdmmc_set_desc_addr(test_desc_addr); +} +fn ourDescAddr() void { + hal.sdmmc.setDescriptorAddr(test_desc_addr); +} + +// The command words. Each pair is the same command expressed twice: once through ESP-IDF's +// `sdmmc_hw_cmd_t` bitfields, once through this project's `commandWord`. + +fn idfCmd0() void { + oracle_sdmmc_stage_command(0, 0, 0, 0, 0, 1, 0, 0, slot); +} +fn ourCmd0() void { + stage(.{ .index = 0, .send_init = true, .wait_prvdata = false, .slot = slot }); +} +fn idfCmd5() void { + oracle_sdmmc_stage_command(5, 0, 1, 0, 0, 0, 1, 0, slot); +} +fn ourCmd5() void { + stage(.{ .index = 5, .response = .short, .check_crc = false, .slot = slot }); +} +fn idfCmd3() void { + oracle_sdmmc_stage_command(3, 0, 1, 1, 0, 0, 1, 0, slot); +} +fn ourCmd3() void { + stage(.{ .index = 3, .response = .short, .check_crc = true, .slot = slot }); +} +fn idfCmd7() void { + oracle_sdmmc_stage_command(7, 0, 1, 1, 0, 0, 1, 0, slot); +} +fn ourCmd7() void { + stage(.{ .index = 7, .response = .short, .check_crc = true, .slot = slot }); +} +fn idfCmd52R() void { + oracle_sdmmc_stage_command(52, 0, 1, 1, 0, 0, 1, 0, slot); +} +fn ourCmd52R() void { + stage(.{ .index = 52, .response = .short, .check_crc = true, .slot = slot }); +} +fn idfCmd52W() void { + // CMD52 carries its payload in the argument, not in a data phase, so the word is identical to + // the read one. Kept as its own case because that is a claim worth checking rather than + // assuming: an implementation that set `rw` for a write would fail here and nowhere else. + oracle_sdmmc_stage_command(52, 0, 1, 1, 0, 0, 1, 0, slot); +} +fn ourCmd52W() void { + stage(.{ .index = 52, .response = .short, .check_crc = true, .slot = slot }); +} +fn idfCmd53R() void { + oracle_sdmmc_stage_command(53, 0, 1, 1, 1, 0, 1, 0, slot); +} +fn ourCmd53R() void { + stage(.{ .index = 53, .response = .short, .check_crc = true, .data = .read, .slot = slot }); +} +fn idfCmd53W() void { + oracle_sdmmc_stage_command(53, 0, 1, 1, 2, 0, 1, 0, slot); +} +fn ourCmd53W() void { + stage(.{ .index = 53, .response = .short, .check_crc = true, .data = .write, .slot = slot }); +} +fn idfCmdClk() void { + oracle_sdmmc_stage_command(0, 0, 0, 0, 0, 0, 1, 1, slot); +} +fn ourCmdClk() void { + stage(.{ .index = 0, .update_clock = true, .slot = slot }); +} + +/// A configuration distinctive enough that failing to clear it is visible in three registers. +fn configureDistinctively() void { + oracle_sdmmc_set_card_width(slot, 4); + oracle_sdmmc_set_block_size(4); + oracle_sdmmc_set_fifo_threshold(255, 8, 2); +} + +fn idfPeriphReset() void { + configureDistinctively(); + oracle_sdmmc_reset_register(); +} +fn ourPeriphReset() void { + configureDistinctively(); + hal.clkrst.resetPeripheral(.sdmmc); +} + +// ------------------------------------------------------------------- the host clock generator + +/// The other half of "set the bus to 40 MHz", which is not in the SDMMC block. +/// +/// `HP_SYS_CLKRST.peri_clk_ctrl01` holds the source mux and the gate, `peri_clk_ctrl02` the +/// three-edge divider and the driving/sampling phase clocks (`sdmmc_ll.h:212-315`). At 40 MHz the +/// host divider is 4 and the card divider is 0, so *all* of the division happens here: an +/// implementation that wrote CLKDIV correctly and this register not at all would clock the C6 at +/// 160 MHz, which is four times the part's limit and would fail as a wiring problem. +pub const clock_suite: types.Suite = .{ + .descriptor = .{ + .name = "sdmmc_clk", + .base = @intCast(regs.HP_SYS_CLKRST_SOC_CLK_CTRL1_REG - 0x18), // block base + // 0x00 through PERI_CLK_CTRL03 at +0x3c: SOC_CLK_CTRL0..3 (the bus-clock gates) and + // PERI_CLK_CTRL00..03 (the SDIO clock generator). + .words = 16, + // No clock check: HP_SYS_CLKRST is the block that holds every other block's gate and has + // none of its own, and one of the cases below deliberately turns SDMMC's off. + .restore = .{ .configure = restoreClocks }, + }, + .cases = &.{ + // Disable first, so "enable" is not a no-op against a restored state that already has it + // on - the shape clkrst_cases.zig arrived at for the same reason. + .{ .name = "bus_clock", .arg = 0, .idf = idfBusClkOff, .ours = ourBusClkOff }, + .{ .name = "host_clock_div", .arg = 4, .idf = idfHostDiv4, .ours = ourHostDiv4 }, + .{ .name = "host_clock_div", .arg = 8, .idf = idfHostDiv8, .ours = ourHostDiv8 }, + .{ .name = "host_clock_div", .arg = 10, .idf = idfHostDiv10, .ours = ourHostDiv10 }, + .{ .name = "select_clk_source", .idf = idfSelectSrc, .ours = ourSelectSrc }, + .{ .name = "init_phase_delay", .idf = idfPhase, .ours = ourPhase }, + // Last, so the block is left clocked whichever side ran last: every suite after this one + // that touches SDMMC depends on it. + .{ .name = "bus_clock", .arg = 1, .idf = idfBusClkOn, .ours = ourBusClkOn }, + }, +}; + +const soc_clk_ctrl1 = mmio.Reg.atAddress(@intCast(regs.HP_SYS_CLKRST_SOC_CLK_CTRL1_REG)); +const peri01 = mmio.Reg.atAddress(@intCast(regs.HP_SYS_CLKRST_PERI_CLK_CTRL01_REG)); +const peri02 = mmio.Reg.atAddress(@intCast(regs.HP_SYS_CLKRST_PERI_CLK_CTRL02_REG)); + +/// Every SDIO field of the three registers this suite's cases touch, back to its reset value - +/// and nothing else, because these words also hold the gates and clock muxes of peripherals that +/// have nothing to do with SDMMC (MIPI DSI's D-PHY source select is bits 30-31 of PERI_CLK_CTRL02). +/// Built from register macros only; nothing here calls the code under test. +fn restoreClocks() void { + soc_clk_ctrl1.modify(.{ + mmio.Field.of(regs.HP_SYS_CLKRST_REG_SDMMC_SYS_CLK_EN_S, regs.HP_SYS_CLKRST_REG_SDMMC_SYS_CLK_EN_V).is(1), + }); + peri01.modify(.{ + mmio.Field.of(regs.HP_SYS_CLKRST_REG_SDIO_HS_MODE_S, regs.HP_SYS_CLKRST_REG_SDIO_HS_MODE_V).is(0), + mmio.Field.of(regs.HP_SYS_CLKRST_REG_SDIO_LS_CLK_SRC_SEL_S, regs.HP_SYS_CLKRST_REG_SDIO_LS_CLK_SRC_SEL_V).is(0), + mmio.Field.of(regs.HP_SYS_CLKRST_REG_SDIO_LS_CLK_EN_S, regs.HP_SYS_CLKRST_REG_SDIO_LS_CLK_EN_V).is(0), + }); + peri02.modify(.{ + mmio.Field.of(regs.HP_SYS_CLKRST_REG_SDIO_LS_CLK_EDGE_L_S, regs.HP_SYS_CLKRST_REG_SDIO_LS_CLK_EDGE_L_V).is(0), + mmio.Field.of(regs.HP_SYS_CLKRST_REG_SDIO_LS_CLK_EDGE_H_S, regs.HP_SYS_CLKRST_REG_SDIO_LS_CLK_EDGE_H_V).is(0), + mmio.Field.of(regs.HP_SYS_CLKRST_REG_SDIO_LS_CLK_EDGE_N_S, regs.HP_SYS_CLKRST_REG_SDIO_LS_CLK_EDGE_N_V).is(0), + mmio.Field.of(regs.HP_SYS_CLKRST_REG_SDIO_LS_SLF_CLK_EDGE_SEL_S, regs.HP_SYS_CLKRST_REG_SDIO_LS_SLF_CLK_EDGE_SEL_V).is(0), + mmio.Field.of(regs.HP_SYS_CLKRST_REG_SDIO_LS_DRV_CLK_EDGE_SEL_S, regs.HP_SYS_CLKRST_REG_SDIO_LS_DRV_CLK_EDGE_SEL_V).is(0), + mmio.Field.of(regs.HP_SYS_CLKRST_REG_SDIO_LS_SAM_CLK_EDGE_SEL_S, regs.HP_SYS_CLKRST_REG_SDIO_LS_SAM_CLK_EDGE_SEL_V).is(0), + mmio.Field.of(regs.HP_SYS_CLKRST_REG_SDIO_LS_SLF_CLK_EN_S, regs.HP_SYS_CLKRST_REG_SDIO_LS_SLF_CLK_EN_V).is(0), + mmio.Field.of(regs.HP_SYS_CLKRST_REG_SDIO_LS_DRV_CLK_EN_S, regs.HP_SYS_CLKRST_REG_SDIO_LS_DRV_CLK_EN_V).is(0), + mmio.Field.of(regs.HP_SYS_CLKRST_REG_SDIO_LS_SAM_CLK_EN_S, regs.HP_SYS_CLKRST_REG_SDIO_LS_SAM_CLK_EN_V).is(0), + }); +} + +fn idfBusClkOff() void { + oracle_sdmmc_bus_clock(0); +} +fn ourBusClkOff() void { + hal.clkrst.setClockEnabled(.sdmmc, false); +} +fn idfBusClkOn() void { + oracle_sdmmc_bus_clock(1); +} +fn ourBusClkOn() void { + hal.clkrst.setClockEnabled(.sdmmc, true); +} + +fn idfHostDiv4() void { + oracle_sdmmc_set_host_clock_div(4); +} +fn ourHostDiv4() void { + hal.sdmmc.setHostClockDiv(4); +} +fn idfHostDiv8() void { + oracle_sdmmc_set_host_clock_div(8); +} +fn ourHostDiv8() void { + hal.sdmmc.setHostClockDiv(8); +} +fn idfHostDiv10() void { + oracle_sdmmc_set_host_clock_div(10); +} +fn ourHostDiv10() void { + hal.sdmmc.setHostClockDiv(10); +} +fn idfSelectSrc() void { + oracle_sdmmc_select_clk_source_pll160m(); +} +fn ourSelectSrc() void { + hal.sdmmc.selectPll160m(); +} +fn idfPhase() void { + oracle_sdmmc_init_phase_delay(); +} +fn ourPhase() void { + hal.sdmmc.initPhaseDelay(); +} diff --git a/src/oracle/sdmmc_ref.c b/src/oracle/sdmmc_ref.c new file mode 100644 index 0000000..4cc1e59 --- /dev/null +++ b/src/oracle/sdmmc_ref.c @@ -0,0 +1,262 @@ +/* SDMMC's reference half: ESP-IDF's own code, compiled into this image. + * + * Most of what follows is a one-line wrapper over a `sdmmc_ll_*` function, for the same reason + * `gpio_ref.c`'s are: the LL functions are `static inline`, so Zig cannot call them until + * something gives them external linkage, and anything clever here would be a third implementation + * to doubt. + * + * Three of them are not wrappers, and it is worth being explicit about which and why. + * + * 1. `oracle_sdmmc_set_fifo_threshold` writes SDHOST_FIFOTH through `SDMMC.fifoth`. There is no + * `sdmmc_ll` function for that register - ESP-IDF never writes it, on any target - so there + * is nothing to wrap. Writing it through IDF's own bitfield union still makes the bit + * positions IDF's, which is the property the comparison needs. + * + * 2. `oracle_sdmmc_stage_command` transcribes `make_hw_cmd` (sd_trans_sdmmc.c:190-229) and the + * three fields `sd_host_slot_start_command` adds afterwards (sd_host_sdmmc.c:859-881). + * `make_hw_cmd` is `static` in a `.c` file and unreachable from a header, so this is the one + * place the reference is a transcription rather than a call. It is a transcription *into + * IDF's `sdmmc_hw_cmd_t`*, so every bit position still comes from + * `soc/sdmmc_struct.h:354-485` and not from this file; what is being compared is whether the + * Zig side's `Field.of` shifts land in the same places, which is exactly the kind of + * transcription error the oracle exists to catch. + * + * It stages the word with `start_command` cleared. Bit 31 is what launches a command, so a + * staged word is inert: the register can be photographed without the CIU trying to talk to a + * radio that is still in reset. + * + * 3. `oracle_sdmmc_configure_controller` and `oracle_sdmmc_module_reset` are short LL sequences, + * in the order `sd_host_sdmmc.c` performs them. Sequences are the part of a driver that + * register macros cannot express, so a reference for one has to be a sequence too - + * `gpio_ref.c`'s `oracle_gpio_matrix_out` is the same shape. + */ + +/* IDF's clock-and-reset LL functions are shadowed by a macro that references + * `__DECLARE_RCC_ATOMIC_ENV`, an identifier IDF never defines anywhere; its purpose is to make an + * unguarded call fail to compile, because the only legal caller holds a spinlock. There is no + * FreeRTOS here and core 1 is held in reset at power-on, so declaring the name is exactly as safe + * as the spinlock would be. Same as gpio_ref.c and clkrst_ref.c. */ +static int __DECLARE_RCC_ATOMIC_ENV __attribute__((unused)); + +/* `sdmmc_ll_set_command` (sdmmc_ll.h:693-696) calls `memcpy`, and this build's `string.h` is an + * empty stand-in - the register headers need the name to exist, not its contents. Declaring it + * here is enough; the symbol comes from compiler_rt at link time, and at -O2 clang turns a 4-byte + * copy into a single store anyway. */ +#include /* the shim's `size_t` */ +void *memcpy(void *dst, const void *src, size_t n); + +#include "hal/sdmmc_ll.h" +#include "soc/sdmmc_struct.h" + +/* ------------------------------------------------------------------ clocks and reset */ + +void oracle_sdmmc_bus_clock(int enable) +{ + sdmmc_ll_enable_bus_clock(0, enable != 0); +} + +void oracle_sdmmc_reset_register(void) +{ + sdmmc_ll_reset_register(0); +} + +void oracle_sdmmc_set_host_clock_div(unsigned div) +{ + sdmmc_ll_set_clock_div(&SDMMC, div); +} + +void oracle_sdmmc_select_clk_source_pll160m(void) +{ + sdmmc_ll_select_clk_source(&SDMMC, SDMMC_CLK_SRC_PLL160M); +} + +void oracle_sdmmc_init_phase_delay(void) +{ + sdmmc_ll_init_phase_delay(&SDMMC); +} + +void oracle_sdmmc_set_card_clock_div(unsigned slot, unsigned div) +{ + sdmmc_ll_set_card_clock_div(&SDMMC, slot, div); +} + +void oracle_sdmmc_enable_card_clock(unsigned slot, int enable) +{ + sdmmc_ll_enable_card_clock(&SDMMC, slot, enable != 0); +} + +void oracle_sdmmc_enable_card_clock_low_power(unsigned slot, int enable) +{ + sdmmc_ll_enable_card_clock_low_power(&SDMMC, slot, enable != 0); +} + +/* ------------------------------------------------------------------ controller resets */ + +void oracle_sdmmc_reset_controller(void) +{ + sdmmc_ll_reset_controller(&SDMMC); +} + +void oracle_sdmmc_reset_dma(void) +{ + sdmmc_ll_reset_dma(&SDMMC); +} + +void oracle_sdmmc_reset_fifo(void) +{ + sdmmc_ll_reset_fifo(&SDMMC); +} + +/* `s_module_reset` plus its completion poll, sd_host_sdmmc.c:917-950. The poll is what makes this + * comparable with the Zig side, which also waits: without it the two could be photographed at + * different points in a self-clearing bit's life. */ +void oracle_sdmmc_module_reset(void) +{ + sdmmc_ll_reset_controller(&SDMMC); + sdmmc_ll_reset_dma(&SDMMC); + sdmmc_ll_reset_fifo(&SDMMC); + while (!(sdmmc_ll_is_controller_reset_done(&SDMMC) && + sdmmc_ll_is_dma_reset_done(&SDMMC) && + sdmmc_ll_is_fifo_reset_done(&SDMMC))) { + /* bounded by the caller: the harness runs this with the bus clock on, where the three bits + * clear in a handful of cycles. */ + } +} + +/* ------------------------------------------------------------------ transfer geometry */ + +void oracle_sdmmc_set_card_width(unsigned slot, unsigned width) +{ + sdmmc_ll_set_card_width(&SDMMC, slot, + width == 4 ? SD_BUS_WIDTH_4_BIT : SD_BUS_WIDTH_1_BIT); +} + +void oracle_sdmmc_set_block_size(unsigned size) +{ + sdmmc_ll_set_block_size(&SDMMC, size); +} + +void oracle_sdmmc_set_data_transfer_len(unsigned len) +{ + sdmmc_ll_set_data_transfer_len(&SDMMC, len); +} + +void oracle_sdmmc_set_timeouts(unsigned data_cycles, unsigned response_cycles) +{ + sdmmc_ll_set_data_timeout(&SDMMC, data_cycles); + sdmmc_ll_set_response_timeout(&SDMMC, response_cycles); +} + +/* No `sdmmc_ll` function exists for this register; see note 1 at the head of the file. */ +void oracle_sdmmc_set_fifo_threshold(unsigned rx_wmark, unsigned tx_wmark, unsigned msize) +{ + SDMMC.fifoth.rx_wmark = rx_wmark; + SDMMC.fifoth.tx_wmark = tx_wmark; + SDMMC.fifoth.dma_multiple_transaction_size = msize; +} + +/* ------------------------------------------------------------------ interrupts and DMA */ + +/* sd_host_sdmmc.c:120-124, in order: clear everything, mask everything, global off, unmask the + * default set, global on - and then the one thing this project does that ESP-IDF does not: mask + * and clear card detect. + * + * That last pair is a deliberate deviation, so it is expressed here through IDF's own LL rather + * than left to differ. There is no card-detect pin on this board; `configurePins` ties the signal + * to a matrix constant, the transition latches RINTSTS.cd, and nothing in the command path clears + * bit 0 - so an unmasked cd holds the controller's line into the CLIC high forever. Comparing an + * IDF sequence that leaves it unmasked against a Zig one that does not would report a difference + * that is the point rather than a bug; comparing the same intent on both sides still catches a + * wrong bit, a wrong register or a wrong order. */ +void oracle_sdmmc_configure_interrupts(void) +{ + sdmmc_ll_clear_interrupt(&SDMMC, 0xffffffff); + sdmmc_ll_enable_interrupt(&SDMMC, 0xffffffff, false); + sdmmc_ll_enable_global_interrupt(&SDMMC, false); + sdmmc_ll_enable_interrupt(&SDMMC, SDMMC_LL_EVENT_DEFAULT, true); + sdmmc_ll_enable_interrupt(&SDMMC, SDMMC_LL_EVENT_CD, false); + sdmmc_ll_clear_interrupt(&SDMMC, SDMMC_LL_EVENT_CD); + sdmmc_ll_enable_global_interrupt(&SDMMC, true); +} + +void oracle_sdmmc_init_dma(void) +{ + sdmmc_ll_init_dma(&SDMMC); +} + +void oracle_sdmmc_enable_dma(int enable) +{ + sdmmc_ll_enable_dma(&SDMMC, enable != 0); +} + +void oracle_sdmmc_set_desc_addr(unsigned addr) +{ + sdmmc_ll_set_desc_addr(&SDMMC, addr); +} + +void oracle_sdmmc_enable_sdio_interrupt(unsigned slot, int enable) +{ + sdmmc_ll_enable_interrupt(&SDMMC, slot == 0 ? SDMMC_LL_EVENT_IO_SLOT0 : SDMMC_LL_EVENT_IO_SLOT1, + enable != 0); +} + +/* ------------------------------------------------------------------ the command word */ + +/* make_hw_cmd (sd_trans_sdmmc.c:190-229) + sd_host_slot_start_command's three additions + * (sd_host_sdmmc.c:859-881), staged with start_command cleared. See note 2 at the head of the + * file for why this one is a transcription. + * + * `data` is 0 for none, 1 for read, 2 for write - the same three-way choice `cmd->data` and + * `SCF_CMD_READ` encode between them. */ +void oracle_sdmmc_stage_command(unsigned index, int response_long, int response_expect, + int check_crc, int data, int send_init, int wait_prvdata, + int update_clk, unsigned slot) +{ + sdmmc_hw_cmd_t res = { 0 }; + + res.cmd_index = index; + if (send_init) { + res.send_init = 1; + } + if (wait_prvdata) { + res.wait_complete = 1; + } + if (response_expect) { + res.response_expect = 1; + if (response_long) { + res.response_long = 1; + } + } + if (check_crc) { + res.check_response_crc = 1; + } + if (data) { + res.data_expected = 1; + if (data == 2) { + res.rw = 1; + } + } + if (update_clk) { + res.update_clk_reg = 1; + } + + /* sd_host_slot_start_command: "Outputs should be synchronized to cclk_out". */ + res.use_hold_reg = 1; + res.card_num = slot; + /* Deliberately *not* res.start_command = 1: staging, not sending. */ + res.start_command = 0; + + sdmmc_ll_set_command(&SDMMC, res); +} + +/* ------------------------------------------------------------------ observation */ + +unsigned oracle_sdmmc_version_id(void) +{ + return sdmmc_ll_get_version_id(&SDMMC); +} + +unsigned oracle_sdmmc_hw_config(void) +{ + return sdmmc_ll_get_hw_config_info(&SDMMC); +} diff --git a/src/oracle/timg_cases.zig b/src/oracle/timg_cases.zig new file mode 100644 index 0000000..34a31de --- /dev/null +++ b/src/oracle/timg_cases.zig @@ -0,0 +1,435 @@ +//! TIMG's side of the differential test: the same timer and watchdog operations expressed as +//! ESP-IDF's LL calls and as this project's HAL calls. +//! +//! **TIMG1 throughout, never TIMG0.** TIMG0 hosts MWDT0, the watchdog the rest of the system relies +//! on staying quiet; this image's bootloader has already disabled it. A mistake in a case that ran +//! against group 0 would not fail a comparison, it would reboot the board mid-run with nothing on +//! the console to explain it. +//! +//! The block is restored by `configure` rather than by the harness pulsing a `reset_bit`, and the +//! reason is the whole safety story of this peripheral: resetting a timer group re-arms +//! `WDT_FLASHBOOT_MOD_EN`, which runs the watchdog independently of `WDT_EN`, so a bare reset-bit +//! pulse arms a watchdog nobody is feeding. `clkrst.resetPeripheral(.timg1)` pulses the bit *and* +//! clears that flag - exactly as `_timg_ll_reset_register` does (timg_ll.h:60-71) - and the harness's +//! `reset_bit` path does only the pulse. So the restore goes through the HAL, and the reset sequence +//! itself becomes one of the cases below instead. +//! +//! The restore deliberately leaves the watchdog **write-protected**. That makes the unlock half of +//! every watchdog case load-bearing: an implementation that forgot to lift protection would have its +//! stage and prescaler writes silently dropped and would differ from IDF's in the snapshot, rather +//! than passing because both sides happened to be unlocked already. +//! +//! What is *not* here, and why: the timers' function-clock source and per-timer gate live in +//! HP_SYS_CLKRST (PERI_CLK_CTRL20/21), and the group's bus-clock gate in SOC_CLK_CTRL2, none of +//! which is inside this block. The harness compares one contiguous window of at most 512 words and +//! HP_SYS_CLKRST is ~0x1e000 bytes away from TIMG1, so a gate case here would compare two identical +//! TIMG snapshots and pass no matter what it wrote. Those pairings need a HP_SYS_CLKRST suite of +//! their own; the reset case below is the one part of that story this window can see, and it does +//! see it, because a group reset and the flashboot fixup both land in these 64 words. + +const std = @import("std"); +const hal = @import("hal"); +const regs = @import("regs"); +const mmio = @import("mmio"); +const types = @import("differ_types.zig"); + +const timg = hal.timg; + +// ------------------------------------------------------------------- ESP-IDF's side, from timg_ref.c + +extern fn oracle_timg_set_divider(group: c_int, timer: c_uint, divider: c_uint) void; +extern fn oracle_timg_set_direction_up(group: c_int, timer: c_uint, up: c_int) void; +extern fn oracle_timg_set_auto_reload(group: c_int, timer: c_uint, en: c_int) void; +extern fn oracle_timg_enable_counter(group: c_int, timer: c_uint, en: c_int) void; +extern fn oracle_timg_enable_alarm(group: c_int, timer: c_uint, en: c_int) void; +extern fn oracle_timg_set_alarm_value(group: c_int, timer: c_uint, value: c_ulonglong) void; +extern fn oracle_timg_set_reload_value(group: c_int, timer: c_uint, value: c_ulonglong) void; +extern fn oracle_timg_trigger_soft_reload(group: c_int, timer: c_uint) void; +extern fn oracle_timg_read_counter(group: c_int, timer: c_uint) c_ulonglong; +extern fn oracle_timg_reset_register(group: c_int) void; + +extern fn oracle_mwdt_set_stage(group: c_int, stage: c_uint, timeout: c_uint, action: c_uint) void; +extern fn oracle_mwdt_disable_stage(group: c_int, stage: c_uint) void; +extern fn oracle_mwdt_set_prescaler(group: c_int, prescaler: c_uint) void; +extern fn oracle_mwdt_set_cpu_reset_length(group: c_int, length: c_uint) void; +extern fn oracle_mwdt_set_sys_reset_length(group: c_int, length: c_uint) void; +extern fn oracle_mwdt_set_flashboot_en(group: c_int, en: c_int) void; +extern fn oracle_mwdt_set_enabled(group: c_int, en: c_int) void; +extern fn oracle_mwdt_feed(group: c_int) void; +extern fn oracle_mwdt_write_protect_disable(group: c_int) void; +extern fn oracle_mwdt_write_protect_enable(group: c_int) void; + +/// The group under test, as a number for the C side. Deliberately a constant rather than a variable: +/// unlike GPIO's pin, this is not a parameter to sweep, it is a safety property. +const group_id: c_int = 1; +const group: timg.Group = .timg1; + +/// The timer under test. A module-level `var` because Zig has no closures and the harness stores +/// plain `fn` pointers; the suite runs the whole list once per timer in `timers`. +pub var timer: timg.Timer = .t0; + +/// Both general-purpose timers of the group (TIMG_LL_GPTIMERS_PER_INST is 2 on the P4). Worth +/// sweeping because the timer index is a *stride* in this HAL rather than a separate set of macros, +/// and a wrong stride writes into the neighbouring timer's registers. +pub const timers = [_]timg.Timer{ .t0, .t1 }; + +inline fn timerId() c_uint { + return @intFromEnum(timer); +} + +// ------------------------------------------------------------------------------------ restore + +fn restore() void { + // Pulses HP_RST_EN1's TIMERGRP1 bit and then clears WDT_FLASHBOOT_MOD_EN, which the pulse + // re-armed. Both halves matter; see the file comment. + // ESP-IDF's reset, not ours: this suite's `reset_register_clears_flashboot` case exists to + // compare the two, and restoring with ours would let a no-op reset pass it. + oracle_timg_reset_register(group_id); + // IDF's reset re-arms flash-boot protection and does not clear it, so clear it here through the + // register directly - the board reboots a few seconds later otherwise. + mmio.Reg.atAddress(@intCast(regs.TIMG_WDTCONFIG0_REG(1))) + .modify(.{mmio.Field.of(regs.TIMG_WDT_FLASHBOOT_MOD_EN_S, regs.TIMG_WDT_FLASHBOOT_MOD_EN_V).is(0)}); + // Leave write protection on, so every watchdog case has to lift it itself. + timg.unlock(group).release(); +} + +// ------------------------------------------------------------------------------------- suite + +pub const suite: types.Suite = .{ + .descriptor = .{ + .name = "timg1", + // TIMG_T0CONFIG_REG is at +0x00 of the group's block (timer_group_reg.h:19), and the group + // stride is 0x1000 (:14). + .base = @intCast(regs.TIMG_T0CONFIG_REG(1)), + // 0x100 bytes: the last register in the block is TIMG_REGCLK_REG at +0xfc. The window has to + // reach it - TIMG_WDTWPROTECT_REG is at +0x64 and the four stage-timeout registers at + // +0x50..+0x5c, so a window that stopped at the timers (+0x48) would be blind to every + // watchdog case in this file. + .words = 64, + .volatile_words = &.{ + (0x04 - 0x00) / 4, // TIMG_T0LO - the captured counter, which moves between snapshots + (0x08 - 0x00) / 4, // TIMG_T0HI + (0x28 - 0x00) / 4, // TIMG_T1LO + (0x2c - 0x00) / 4, // TIMG_T1HI + (0x68 - 0x00) / 4, // TIMG_RTCCALICFG - RTC calibration runs cyclically by default + (0x6c - 0x00) / 4, // TIMG_RTCCALICFG1 - and latches a new count each cycle + (0x74 - 0x00) / 4, // TIMG_INT_RAW_TIMERS - alarm/watchdog raw status, set by hardware + (0x78 - 0x00) / 4, // TIMG_INT_ST_TIMERS + (0x80 - 0x00) / 4, // TIMG_RTCCALICFG2 + }, + // TIMG1's bus clock: SOC_CLK_CTRL2 bit 22 (hp_sys_clkrst_reg.h:763, and timg_ll.h:35-42 + // for the register it belongs to - not PERI_CLK_CTRL21, which is where this project's + // clkrst table had it until this suite was written). A snapshot of a gated block returns + // the last latched value rather than zeros, so the harness checks this first. + .clock = .{ + .reg = @intCast(regs.HP_SYS_CLKRST_SOC_CLK_CTRL2_REG), + .bit = @intCast(regs.HP_SYS_CLKRST_REG_TIMERGRP1_APB_CLK_EN_S), + }, + .restore = .{ .configure = restore }, + }, + .cases = &.{ + // ---- prescaler. 2 is the hardware minimum and 65536 is the maximum, encoded as 0 + // (timer_ll.h:191-199) - the one arithmetic edge in this peripheral. + .{ .name = "divider", .arg = 2, .idf = idfDivider2, .ours = ourDivider2 }, + .{ .name = "divider", .arg = 1234, .idf = idfDivider1234, .ours = ourDivider1234 }, + .{ .name = "divider", .arg = 65535, .idf = idfDivider65535, .ours = ourDivider65535 }, + .{ .name = "divider_wraps_to_zero", .arg = 65536, .idf = idfDivider65536, .ours = ourDivider65536 }, + // ---- direction, auto-reload, counter and alarm enables + .{ .name = "direction_up", .arg = 1, .idf = idfDirUp, .ours = ourDirUp }, + .{ .name = "direction_down", .arg = 0, .idf = idfDirDown, .ours = ourDirDown }, + .{ .name = "auto_reload_on", .arg = 1, .idf = idfReloadOn, .ours = ourReloadOn }, + .{ .name = "auto_reload_off", .arg = 0, .idf = idfReloadOff, .ours = ourReloadOff }, + .{ .name = "counter_enable", .arg = 1, .idf = idfCounterOn, .ours = ourCounterOn }, + .{ .name = "counter_disable", .arg = 0, .idf = idfCounterOff, .ours = ourCounterOff }, + .{ .name = "alarm_enable", .arg = 1, .idf = idfAlarmOn, .ours = ourAlarmOn }, + .{ .name = "alarm_disable", .arg = 0, .idf = idfAlarmOff, .ours = ourAlarmOff }, + // ---- the 54-bit pairs. 0x2a_5555_aaaa exercises all 22 bits of the high word: a value + // that fit in 32 bits would pass even if the high half were dropped entirely. + .{ .name = "alarm_value_54bit", .arg = 0x5555_aaaa, .idf = idfAlarmValue, .ours = ourAlarmValue }, + .{ .name = "alarm_value_zero", .arg = 0, .idf = idfAlarmValueZero, .ours = ourAlarmValueZero }, + .{ .name = "load_value_54bit", .arg = 0x1234_5678, .idf = idfLoadValue, .ours = ourLoadValue }, + // Write-to-trigger: nothing in the compared window changes, and the counter registers are + // volatile. The case is here because it would catch the trigger landing on the wrong + // address - TIMG_T0LOAD_REG is one word past TIMG_T0LOADHI_REG - which is a live risk when + // the timer index is a stride rather than a distinct macro. + .{ .name = "soft_reload_trigger", .idf = idfSoftReload, .ours = ourSoftReload }, + // The latch-then-read sequence. Register-identical by construction, so what it really + // proves is that our poll terminates: this peripheral acknowledges a capture by *clearing* + // TxUPDATE, and waiting for it to be set instead hangs the run. + .{ .name = "read_counter_latch", .idf = idfReadCounter, .ours = ourReadCounter }, + // ---- watchdog. Every one of these has to lift write protection and put it back; the + // restored state has it on, so a dropped unlock shows up as a difference. + .{ .name = "wdt_write_protect_dance", .idf = idfWdtDance, .ours = ourWdtDance }, + .{ .name = "wdt_stage0_interrupt", .arg = 2_000_000, .idf = idfWdtStage0, .ours = ourWdtStage0 }, + .{ .name = "wdt_stage1_reset_cpu", .arg = 5_000, .idf = idfWdtStage1, .ours = ourWdtStage1 }, + .{ .name = "wdt_stage2_reset_system", .arg = 123_456, .idf = idfWdtStage2, .ours = ourWdtStage2 }, + .{ .name = "wdt_stage3_off", .idf = idfWdtStage3Off, .ours = ourWdtStage3Off }, + .{ .name = "wdt_prescaler", .arg = 20_000, .idf = idfWdtPrescaler, .ours = ourWdtPrescaler }, + .{ .name = "wdt_cpu_reset_length", .arg = 7, .idf = idfWdtCpuLen, .ours = ourWdtCpuLen }, + .{ .name = "wdt_sys_reset_length", .arg = 4, .idf = idfWdtSysLen, .ours = ourWdtSysLen }, + .{ .name = "wdt_flashboot_off", .arg = 0, .idf = idfWdtFlashbootOff, .ours = ourWdtFlashbootOff }, + .{ .name = "wdt_feed", .idf = idfWdtFeed, .ours = ourWdtFeed }, + // Safe on TIMG1 only because the restored state has all four stages off and flashboot mode + // cleared, so an enabled watchdog here has no action to take before the next restore. + .{ .name = "wdt_enable", .arg = 1, .idf = idfWdtEnable, .ours = ourWdtEnable }, + .{ .name = "wdt_disable", .arg = 0, .idf = idfWdtDisable, .ours = ourWdtDisable }, + // ---- the reset sequence itself, which is the only part of the clock/reset table this + // window can see: the group reset plus the flashboot fixup that has to follow it. + .{ .name = "reset_register_clears_flashboot", .idf = idfResetRegister, .ours = ourResetRegister }, + }, + .setup = setup, +}; + +/// The group's bus clock. Already 1 out of reset (hp_sys_clkrst_reg.h:763, default 1) and this image +/// never runs `esp_perip_clk_init`, so this is belt-and-braces - but a snapshot of a gated block is +/// stale rather than zero, and the harness would rather fail the gate check than compare noise. +fn setup() void { + hal.clkrst.setClockEnabled(.timg1, true); +} + +// -------------------------------------------------------------------------- the case pairs +// Same operation, same arguments, twice. IDF's LL on one side, this HAL on the other; a read-back +// through our own accessor would prove nothing, which is the whole point of the arrangement. + +fn idfDivider2() void { + oracle_timg_set_divider(group_id, timerId(), 2); +} +fn ourDivider2() void { + timg.setDivider(group, timer, 2); +} +fn idfDivider1234() void { + oracle_timg_set_divider(group_id, timerId(), 1234); +} +fn ourDivider1234() void { + timg.setDivider(group, timer, 1234); +} +fn idfDivider65535() void { + oracle_timg_set_divider(group_id, timerId(), 65535); +} +fn ourDivider65535() void { + timg.setDivider(group, timer, 65535); +} +fn idfDivider65536() void { + oracle_timg_set_divider(group_id, timerId(), 65536); +} +fn ourDivider65536() void { + timg.setDivider(group, timer, 65536); +} + +fn idfDirUp() void { + oracle_timg_set_direction_up(group_id, timerId(), 1); +} +fn ourDirUp() void { + timg.setDirection(group, timer, .up); +} +fn idfDirDown() void { + oracle_timg_set_direction_up(group_id, timerId(), 0); +} +fn ourDirDown() void { + timg.setDirection(group, timer, .down); +} + +fn idfReloadOn() void { + oracle_timg_set_auto_reload(group_id, timerId(), 1); +} +fn ourReloadOn() void { + timg.setAutoReload(group, timer, true); +} +fn idfReloadOff() void { + oracle_timg_set_auto_reload(group_id, timerId(), 0); +} +fn ourReloadOff() void { + timg.setAutoReload(group, timer, false); +} + +fn idfCounterOn() void { + oracle_timg_enable_counter(group_id, timerId(), 1); +} +fn ourCounterOn() void { + timg.setCounterEnabled(group, timer, true); +} +fn idfCounterOff() void { + oracle_timg_enable_counter(group_id, timerId(), 0); +} +fn ourCounterOff() void { + timg.setCounterEnabled(group, timer, false); +} + +fn idfAlarmOn() void { + oracle_timg_enable_alarm(group_id, timerId(), 1); +} +fn ourAlarmOn() void { + timg.setAlarmEnabled(group, timer, true); +} +fn idfAlarmOff() void { + oracle_timg_enable_alarm(group_id, timerId(), 0); +} +fn ourAlarmOff() void { + timg.setAlarmEnabled(group, timer, false); +} + +/// 54 bits: 22 in the high word, 32 in the low one. +const alarm_value: u64 = 0x2a_5555_aaaa; +const load_value: u64 = 0x15_1234_5678; + +fn idfAlarmValue() void { + oracle_timg_set_alarm_value(group_id, timerId(), alarm_value); +} +fn ourAlarmValue() void { + timg.setAlarmValue(group, timer, alarm_value); +} +fn idfAlarmValueZero() void { + oracle_timg_set_alarm_value(group_id, timerId(), 0); +} +fn ourAlarmValueZero() void { + timg.setAlarmValue(group, timer, 0); +} +fn idfLoadValue() void { + oracle_timg_set_reload_value(group_id, timerId(), load_value); +} +fn ourLoadValue() void { + timg.setLoadValue(group, timer, load_value); +} +fn idfSoftReload() void { + oracle_timg_set_reload_value(group_id, timerId(), load_value); + oracle_timg_trigger_soft_reload(group_id, timerId()); +} +fn ourSoftReload() void { + timg.setLoadValue(group, timer, load_value); + timg.load(group, timer); +} + +fn idfReadCounter() void { + _ = oracle_timg_read_counter(group_id, timerId()); +} +fn ourReadCounter() void { + // Discarding the value is the point: the comparison is over registers, and what this exercises + // is the handshake. A null return means our poll gave up after 10,000 reads, which IDF's + // version cannot report because it spins forever. + _ = timg.read(group, timer); +} + +// ------------------------------------------------------------------------------ watchdog pairs + +fn idfWdtDance() void { + oracle_mwdt_write_protect_disable(group_id); + oracle_mwdt_write_protect_enable(group_id); +} +fn ourWdtDance() void { + const wdt = timg.unlock(group); + wdt.release(); +} + +fn idfWdtStage0() void { + oracle_mwdt_set_stage(group_id, 0, 2_000_000, @intFromEnum(timg.Action.interrupt)); +} +fn ourWdtStage0() void { + const wdt = timg.unlock(group); + defer wdt.release(); + wdt.setStage(.stage0, 2_000_000, .interrupt); +} + +fn idfWdtStage1() void { + oracle_mwdt_set_stage(group_id, 1, 5_000, @intFromEnum(timg.Action.reset_cpu)); +} +fn ourWdtStage1() void { + const wdt = timg.unlock(group); + defer wdt.release(); + wdt.setStage(.stage1, 5_000, .reset_cpu); +} + +fn idfWdtStage2() void { + oracle_mwdt_set_stage(group_id, 2, 123_456, @intFromEnum(timg.Action.reset_system)); +} +fn ourWdtStage2() void { + const wdt = timg.unlock(group); + defer wdt.release(); + wdt.setStage(.stage2, 123_456, .reset_system); +} + +fn idfWdtStage3Off() void { + // Configure it to something first, so "off" has something to undo and the case cannot pass by + // both sides doing nothing. + oracle_mwdt_set_stage(group_id, 3, 999, @intFromEnum(timg.Action.interrupt)); + oracle_mwdt_disable_stage(group_id, 3); +} +fn ourWdtStage3Off() void { + const wdt = timg.unlock(group); + defer wdt.release(); + wdt.setStage(.stage3, 999, .interrupt); + wdt.disableStage(.stage3); +} + +fn idfWdtPrescaler() void { + oracle_mwdt_set_prescaler(group_id, 20_000); +} +fn ourWdtPrescaler() void { + const wdt = timg.unlock(group); + defer wdt.release(); + wdt.setPrescaler(20_000); +} + +fn idfWdtCpuLen() void { + oracle_mwdt_set_cpu_reset_length(group_id, @intFromEnum(timg.ResetLength.us_3_2)); +} +fn ourWdtCpuLen() void { + const wdt = timg.unlock(group); + defer wdt.release(); + wdt.setCpuResetLength(.us_3_2); +} + +fn idfWdtSysLen() void { + oracle_mwdt_set_sys_reset_length(group_id, @intFromEnum(timg.ResetLength.ns_500)); +} +fn ourWdtSysLen() void { + const wdt = timg.unlock(group); + defer wdt.release(); + wdt.setSysResetLength(.ns_500); +} + +fn idfWdtFlashbootOff() void { + oracle_mwdt_set_flashboot_en(group_id, 0); +} +fn ourWdtFlashbootOff() void { + const wdt = timg.unlock(group); + defer wdt.release(); + wdt.setFlashbootEnabled(false); +} + +fn idfWdtFeed() void { + oracle_mwdt_feed(group_id); +} +fn ourWdtFeed() void { + timg.feed(group); +} + +fn idfWdtEnable() void { + oracle_mwdt_set_enabled(group_id, 1); +} +fn ourWdtEnable() void { + const wdt = timg.unlock(group); + defer wdt.release(); + wdt.setEnabled(true); +} + +fn idfWdtDisable() void { + oracle_mwdt_set_enabled(group_id, 0); +} +fn ourWdtDisable() void { + const wdt = timg.unlock(group); + defer wdt.release(); + wdt.setEnabled(false); +} + +fn idfResetRegister() void { + oracle_timg_reset_register(group_id); +} +fn ourResetRegister() void { + // ESP-IDF's reset, not ours: this suite's `reset_register_clears_flashboot` case exists to + // compare the two, and restoring with ours would let a no-op reset pass it. + oracle_timg_reset_register(group_id); + // IDF's reset re-arms flash-boot protection and does not clear it, so clear it here through the + // register directly - the board reboots a few seconds later otherwise. + mmio.Reg.atAddress(@intCast(regs.TIMG_WDTCONFIG0_REG(1))) + .modify(.{mmio.Field.of(regs.TIMG_WDT_FLASHBOOT_MOD_EN_S, regs.TIMG_WDT_FLASHBOOT_MOD_EN_V).is(0)}); +} diff --git a/src/oracle/timg_ref.c b/src/oracle/timg_ref.c new file mode 100644 index 0000000..38bccc8 --- /dev/null +++ b/src/oracle/timg_ref.c @@ -0,0 +1,197 @@ +/* The reference implementation for the timer groups and their watchdogs, which is ESP-IDF's own. + * + * Thin external-linkage wrappers over `timer_ll.h`, `mwdt_ll.h` and `timg_ll.h`, so Zig can call + * IDF's `static inline` functions and the differential harness can run both implementations in one + * image on one boot. There is no logic here: anything clever would be a third implementation to + * doubt. + * + * Two things about the watchdog wrappers are deliberate. The write-protect dance is *inside* each + * wrapper (`mwdt_ll_write_protect_disable` ... `mwdt_ll_write_protect_enable`) because that is what + * IDF's callers do - `mwdt_ll_config_stage` itself will silently do nothing if protection is on - + * and because our side does the same thing through `timg.unlock`/`release`. The two sides have to be + * the same operation, key register included, or comparing WDTWPROTECT afterwards means nothing. + * And nothing here ever calls `mwdt_ll_enable` on group 0: TIMG0 hosts the watchdog the rest of the + * system depends on not firing. + */ + +/* IDF's clock and reset LL functions are shadowed by a wrapper macro referencing + * `__DECLARE_RCC_ATOMIC_ENV` / `__DECLARE_RCC_RC_ATOMIC_ENV`, identifiers IDF never defines + * anywhere: their purpose is to make an unguarded call fail to compile, because the only legal + * caller holds a FreeRTOS spinlock. There is no FreeRTOS here and core 1 is held in reset at + * power-on, so declaring the names is exactly as safe as the spinlock would be - and it is what + * IDF's own bootloader does (bootloader_support/src/bootloader_console.c:53). */ +static int __DECLARE_RCC_ATOMIC_ENV __attribute__((unused)); +static int __DECLARE_RCC_RC_ATOMIC_ENV __attribute__((unused)); + +#include "hal/timer_ll.h" +#include "hal/mwdt_ll.h" +#include "hal/timg_ll.h" +#include "soc/timer_group_struct.h" + +static timg_dev_t *grp(int group) +{ + return TIMER_LL_GET_HW(group); +} + +/* ------------------------------------------------------------------ general purpose timer */ + +void oracle_timg_set_divider(int group, unsigned timer, unsigned divider) +{ + timer_ll_set_clock_prescale(grp(group), timer, divider); +} + +void oracle_timg_set_direction_up(int group, unsigned timer, int up) +{ + timer_ll_set_count_direction(grp(group), timer, up ? GPTIMER_COUNT_UP : GPTIMER_COUNT_DOWN); +} + +void oracle_timg_set_auto_reload(int group, unsigned timer, int en) +{ + timer_ll_enable_auto_reload(grp(group), timer, en != 0); +} + +void oracle_timg_enable_counter(int group, unsigned timer, int en) +{ + timer_ll_enable_counter(grp(group), timer, en != 0); +} + +void oracle_timg_enable_alarm(int group, unsigned timer, int en) +{ + timer_ll_enable_alarm(grp(group), timer, en != 0); +} + +void oracle_timg_set_alarm_value(int group, unsigned timer, unsigned long long value) +{ + timer_ll_set_alarm_value(grp(group), timer, value); +} + +void oracle_timg_set_reload_value(int group, unsigned timer, unsigned long long value) +{ + timer_ll_set_reload_value(grp(group), timer, value); +} + +unsigned long long oracle_timg_get_reload_value(int group, unsigned timer) +{ + return timer_ll_get_reload_value(grp(group), timer); +} + +void oracle_timg_trigger_soft_reload(int group, unsigned timer) +{ + timer_ll_trigger_soft_reload(grp(group), timer); +} + +/* The latch-then-read sequence: `timer_ll_trigger_soft_capture` writes TxUPDATE and spins until the + * hardware clears it, and only then is the TxHI/TxLO pair meaningful. Exposed as one call because + * that is how our `timg.read` expresses it, and splitting it would compare halves of a sequence. */ +unsigned long long oracle_timg_read_counter(int group, unsigned timer) +{ + timer_ll_trigger_soft_capture(grp(group), timer); + return timer_ll_get_counter_value(grp(group), timer); +} + +void oracle_timg_set_clock_source_xtal(int group, unsigned timer) +{ + timer_ll_set_clock_source(group, timer, GPTIMER_CLK_SRC_XTAL); +} + +void oracle_timg_set_clock_source_pll80m(int group, unsigned timer) +{ + timer_ll_set_clock_source(group, timer, GPTIMER_CLK_SRC_PLL_F80M); +} + +void oracle_timg_enable_timer_clock(int group, unsigned timer, int en) +{ + timer_ll_enable_clock(group, timer, en != 0); +} + +void oracle_timg_enable_bus_clock(int group, int en) +{ + timg_ll_enable_bus_clock(group, en != 0); +} + +/* Pulses the group's reset bit and then clears WDT_FLASHBOOT_MOD_EN, which the reset re-arms + * (timg_ll.h:51-71). The clearing is the interesting half: leave it out and the board reboots a + * moment later with nothing on the console to explain it. */ +void oracle_timg_reset_register(int group) +{ + timg_ll_reset_register(group); +} + +/* --------------------------------------------------------------------------------- watchdog */ + +void oracle_mwdt_set_stage(int group, unsigned stage, unsigned timeout, unsigned action) +{ + mwdt_ll_write_protect_disable(grp(group)); + mwdt_ll_config_stage(grp(group), (wdt_stage_t)stage, timeout, (wdt_stage_action_t)action); + mwdt_ll_write_protect_enable(grp(group)); +} + +void oracle_mwdt_disable_stage(int group, unsigned stage) +{ + mwdt_ll_write_protect_disable(grp(group)); + mwdt_ll_disable_stage(grp(group), stage); + mwdt_ll_write_protect_enable(grp(group)); +} + +void oracle_mwdt_set_prescaler(int group, unsigned prescaler) +{ + mwdt_ll_write_protect_disable(grp(group)); + mwdt_ll_set_prescaler(grp(group), prescaler); + mwdt_ll_write_protect_enable(grp(group)); +} + +void oracle_mwdt_set_cpu_reset_length(int group, unsigned length) +{ + mwdt_ll_write_protect_disable(grp(group)); + mwdt_ll_set_cpu_reset_length(grp(group), (wdt_reset_sig_length_t)length); + mwdt_ll_write_protect_enable(grp(group)); +} + +void oracle_mwdt_set_sys_reset_length(int group, unsigned length) +{ + mwdt_ll_write_protect_disable(grp(group)); + mwdt_ll_set_sys_reset_length(grp(group), (wdt_reset_sig_length_t)length); + mwdt_ll_write_protect_enable(grp(group)); +} + +void oracle_mwdt_set_flashboot_en(int group, int en) +{ + mwdt_ll_write_protect_disable(grp(group)); + mwdt_ll_set_flashboot_en(grp(group), en != 0); + mwdt_ll_write_protect_enable(grp(group)); +} + +void oracle_mwdt_set_enabled(int group, int en) +{ + mwdt_ll_write_protect_disable(grp(group)); + if (en) { + mwdt_ll_enable(grp(group)); + } else { + mwdt_ll_disable(grp(group)); + } + mwdt_ll_write_protect_enable(grp(group)); +} + +void oracle_mwdt_feed(int group) +{ + mwdt_ll_write_protect_disable(grp(group)); + mwdt_ll_feed(grp(group)); + mwdt_ll_write_protect_enable(grp(group)); +} + +/* The two halves of the protection dance on their own, so a case can check that our key value and + * IDF's are the same word rather than only that a guarded sequence ends up locked. */ +void oracle_mwdt_write_protect_disable(int group) +{ + mwdt_ll_write_protect_disable(grp(group)); +} + +void oracle_mwdt_write_protect_enable(int group) +{ + mwdt_ll_write_protect_enable(grp(group)); +} + +int oracle_mwdt_is_enabled(int group) +{ + return mwdt_ll_check_if_enabled(grp(group)) ? 1 : 0; +} diff --git a/src/oracle/uart_cases.zig b/src/oracle/uart_cases.zig new file mode 100644 index 0000000..444d235 --- /dev/null +++ b/src/oracle/uart_cases.zig @@ -0,0 +1,326 @@ +//! UART's side of the differential test. +//! +//! **The peripheral under test is UART1, and that is a safety constraint rather than a preference.** +//! UART0 carries this board's console. The harness restores a UART by pulsing its reset bit, and +//! resetting UART0 clears UART_CLKDIV: the console's output turns to garbage mid-character and the +//! board takes a watchdog reset with nothing readable left to explain it. That was measured on this +//! board. UART1 is otherwise idle here, has no pins routed at power-on, and resets cleanly. +//! +//! **Word 0 is on the no-read list.** `UART_FIFO_REG` is at offset 0x000 - the first word any "read +//! the whole block" loop touches - its only field is annotated `RO` in uart_reg.h:18, and that +//! annotation is wrong in the way that matters: the read is the FIFO pop. A generic snapshot of a +//! UART eats received bytes. +//! +//! **What this suite cannot see, stated plainly.** The descriptor is one contiguous window and the +//! UART's is 0xa0 bytes at its own base, so three things this HAL does land outside it: +//! +//! * the integer pre-divider `REG_UART1_SCLK_DIV_NUM` and the source select +//! `REG_UART1_CLK_SRC_SEL`, which are in HP_SYS_CLKRST at a different base; +//! * the GPIO matrix registers the routing cases write, which are in the GPIO block; +//! * the FIFO contents themselves, which have no addressable state to compare. +//! +//! The pre-divider is not unobserved, though, only observed indirectly: `clk_div` is +//! `(sclk_freq << 4) / (baud * sclk_div)`, so the in-window CLKDIV_SYNC word is a function of the +//! pre-divider, and the two sides disagreeing on `sclk_div` shows up as a different CLKDIV unless +//! the two errors cancel exactly. The `baud_300` case exists specifically because it is the one +//! rate here whose pre-divider is not 1. The routing cases are honestly weak in this window - what +//! they compare is that both sides leave the *UART* untouched, and their real evidence is that +//! `gpio_cases`' `matrix_out` case passes against the same GPIO LL functions this file calls. +//! +//! Both sides reach the hardware by different paths throughout: the `idf` half calls ESP-IDF's +//! `uart_ll.h` compiled by clang, the `ours` half calls src/hal/uart.zig. Nothing here reads a +//! value back through the accessor that wrote it, because that proves only that the accessor is +//! self-consistent. + +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_uart_set_sclk(num: c_uint, sel: c_uint) void; +/// The source select, named on the C side. `UART_SCLK_XTAL` is a `soc_module_clk_t` enumerator whose +/// numeric value is an accident of a chip-wide enum, so it must not cross this boundary as an +/// integer - passing 0 selects nothing that exists and hangs the next commit. +extern fn oracle_uart_set_sclk_xtal(num: c_uint) void; +extern fn oracle_uart_sclk_enable(num: c_uint) void; +extern fn oracle_uart_enable_bus_clock(num: c_uint, enable: c_int) void; +extern fn oracle_uart_set_baudrate(num: c_uint, baud: c_uint, sclk_freq: c_uint) c_int; +extern fn oracle_uart_set_data_bit_num(num: c_uint, bits: c_uint) void; +extern fn oracle_uart_set_stop_bits(num: c_uint, stop: c_uint) void; +extern fn oracle_uart_set_parity(num: c_uint, parity: c_uint) void; +extern fn oracle_uart_txfifo_rst(num: c_uint) void; +extern fn oracle_uart_rxfifo_rst(num: c_uint) void; +extern fn oracle_uart_set_loop_back(num: c_uint, enable: c_int) void; +extern fn oracle_uart_update(num: c_uint) void; +extern fn oracle_uart_route_tx(num: c_uint, pin: c_uint) void; +extern fn oracle_uart_route_rx(num: c_uint, pin: c_uint) void; + +/// The instance under test. A module-level `var` because Zig has no closures and the harness stores +/// plain `fn` pointers. It is a `var` rather than a constant so a future run can move to UART2-4, +/// but it must never become 0: see this file's header. +pub var port: u8 = 1; + +/// The pad the routing cases use. GPIO33 is a free pin on this board's JP1 header - the same one +/// `gpio_cases` uses for its high-bank tests, and for the same reason. +pub var route_pin: u8 = 33; + +/// The clock source frequency the baud cases assume, matching what `setup` selects. XTAL is 40 MHz +/// on the P4 and is the only source whose frequency is exact, which is what makes an expected +/// divider computable by hand. +const sclk_freq: u32 = 40_000_000; + +fn ours() hal.uart.Uart { + return hal.uart.Uart.init(port); +} + +/// Bring UART1 far enough up that its registers answer and its baud generator runs: APB bus clock, +/// core clock, and a source select. Done through IDF's LL rather than ours, so that a bug in our +/// clock code cannot make the whole suite silently compare two dead blocks - and the harness +/// re-checks the bus clock gate before every case regardless. +fn setup() void { + restore(); +} + +/// Known state: out of reset, bus clock on, core clock on, source selected. Every case starts here. +/// +/// The reset is what makes this a sound restore for a block whose CONF0_SYNC carries two +/// write-to-act FIFO resets and whose offset 0 transmits when written - there is nothing here that +/// could be restored by writing a saved snapshot back. The re-enable is what makes it *usable* +/// afterwards. +fn restore() void { + const guard = hal.clkrst.maskInterrupts(); + const rst = mmio.Reg.at(regs.HP_SYS_CLKRST_HP_RST_EN1_REG); + const bit = @as(u32, 1) << regs.HP_SYS_CLKRST_REG_RST_EN_UART1_APB_S; + rst.writeRaw(rst.raw() | bit); + rst.writeRaw(rst.raw() & ~bit); + guard.release(); + + oracle_uart_enable_bus_clock(port, 1); + oracle_uart_sclk_enable(port); + oracle_uart_set_sclk_xtal(port); +} + +// The clock source is selected through oracle_uart_set_sclk_xtal, which names the enumerator on the +// C side. It used to be an integer constant here, and 0 is not XTAL - see that function's comment. + +pub const suite: types.Suite = .{ + .descriptor = .{ + .name = "uart1", + // UART1's block: DR_REG_UART0_BASE + 1 * 0x1000 (soc.h:20). + .base = @intCast(regs.DR_REG_UART0_BASE + 0x1000), + // 40 words, 0x000 through 0x09c. The last register in the block is UART_ID at +0x9c + // (uart_reg.h:1568) and the commit bit UART_REG_UPDATE is at +0x98 - a window that stopped + // at UART_CLK_CONF (+0x88) would be blind to whether the commit even happened, which is the + // single most likely difference against IDF on this peripheral. + .words = 40, + // The read that is a write. See the header. + .no_read = &.{0x00 / 4}, + .volatile_words = &.{ + 0x04 / 4, // UART_INT_RAW - write-1-to-clear, and TXFIFO_EMPTY_INT_RAW moves on its own + 0x08 / 4, // UART_INT_ST - read-only view of the above + 0x1c / 4, // UART_STATUS - live FIFO counts, and the RXD/CTS/DSR pad levels + 0x68 / 4, // UART_MEM_TX_STATUS - FIFO read/write pointers + 0x6c / 4, // UART_MEM_RX_STATUS + 0x70 / 4, // UART_FSM_STATUS - the transmitter's state machine + 0x74 / 4, // UART_POSPULSE - autobaud edge counters, which count whatever the pad does + 0x78 / 4, // UART_NEGPULSE + 0x7c / 4, // UART_LOWPULSE + 0x80 / 4, // UART_HIGHPULSE + 0x84 / 4, // UART_RXD_CNT + 0x90 / 4, // UART_AFIFO_STATUS - the async FIFO's empty/full flags + 0x98 / 4, // UART_REG_UPDATE - self-clearing; reads 0 once the commit lands, but is + // 1 for a few core-clock cycles and a snapshot can catch it + }, + // The APB gate that must read 1 for a snapshot of this block to mean anything. A gated UART + // does not read as zeros, it reads as the last value latched, so two meaningless snapshots + // can compare equal. Pairing from uart_ll.h:257-259, which reads UART1's APB enable out of + // HP_SYS_CLKRST.soc_clk_ctrl2. + .clock = .{ + .reg = @intCast(regs.HP_SYS_CLKRST_SOC_CLK_CTRL2_REG), + .bit = regs.HP_SYS_CLKRST_REG_UART1_APB_CLK_EN_S, + }, + // Reset is the only sound restore for this block: CONF0_SYNC's two FIFO-reset bits and + // REG_UPDATE are write-to-act, and writing a saved word back to offset 0x000 would transmit + // a character. Pairing from uart_ll.h:340-342. Safe here only because this is UART1; + // the same line for UART0 kills the console. + // Reset, and then put the clocking back - which is why this is `.configure` and not + // `.reset_bit`. The harness's reset path does only the pulse, and a UART reset clears the + // core-clock enable and the source select along with everything else. IDF's + // `uart_ll_update` then spins forever waiting for a REG_UPDATE commit that a clockless + // peripheral will never acknowledge: the harness reached the first UART case and stopped, + // with the console silent, looking exactly like a crash. + .restore = .{ .configure = restore }, + }, + .cases = &.{ + // --- baud rate. Four rates spanning the interesting parts of the arithmetic: two ordinary + // ones where the pre-divider is 1, one low enough to need a pre-divider of 33, and one fast + // enough that the integer part gets small and the fraction carries most of the accuracy. + .{ .name = "baudrate", .arg = 115200, .idf = idfBaud115200, .ours = ourBaud115200 }, + .{ .name = "baudrate", .arg = 9600, .idf = idfBaud9600, .ours = ourBaud9600 }, + .{ .name = "baudrate_needs_predivider", .arg = 300, .idf = idfBaud300, .ours = ourBaud300 }, + .{ .name = "baudrate", .arg = 1000000, .idf = idfBaud1M, .ours = ourBaud1M }, + + // --- data format. Each of these is one CONF0_SYNC field plus a commit. + .{ .name = "word_length", .arg = 8, .idf = idfBits8, .ours = ourBits8 }, + .{ .name = "word_length", .arg = 5, .idf = idfBits5, .ours = ourBits5 }, + .{ .name = "stop_bits", .arg = 2, .idf = idfStop2, .ours = ourStop2 }, + .{ .name = "stop_bits_1_5", .arg = 15, .idf = idfStop15, .ours = ourStop15 }, + .{ .name = "parity_odd", .arg = 3, .idf = idfParityOdd, .ours = ourParityOdd }, + .{ .name = "parity_even", .arg = 2, .idf = idfParityEven, .ours = ourParityEven }, + // The asymmetric one: IDF leaves the odd/even bit alone when disabling parity, because 0 + // carries no odd/even information (uart_ll.h:819-822). Setting odd and then disabling is + // the sequence that makes the difference visible, so the case does both. + .{ .name = "parity_odd_then_disable", .idf = idfParityOddThenOff, .ours = ourParityOddThenOff }, + + // --- loopback. Worth a case of its own beyond being one more CONF0_SYNC bit: it is the only + // way to move a byte through this UART with nothing wired to the board. + .{ .name = "loopback_on", .arg = 1, .idf = idfLoopOn, .ours = ourLoopOn }, + .{ .name = "loopback_off", .arg = 0, .idf = idfLoopOff, .ours = ourLoopOff }, + + // --- FIFO resets. These are sequences, not field writes: assert, commit, deassert, commit, + // four stores where a state comparison alone would accept one. Getting the commits wrong + // leaves the register reading exactly as asked and the FIFO not reset. + .{ .name = "txfifo_rst", .idf = idfTxFifoRst, .ours = ourTxFifoRst }, + .{ .name = "rxfifo_rst", .idf = idfRxFifoRst, .ours = ourRxFifoRst }, + + // --- the bare commit, as its own case. If this one differs, every case above is suspect. + .{ .name = "update", .idf = idfUpdate, .ours = ourUpdate }, + + // --- pin routing. Window-blind by construction: the effect is in the GPIO block, so what + // these compare is that neither side disturbs the UART while routing. Kept because a + // routing call that accidentally wrote a UART register would be caught by nothing else, and + // because the pair documents which signal index each side uses. + .{ .name = "route_tx", .arg = 33, .idf = idfRouteTx, .ours = ourRouteTx }, + .{ .name = "route_rx", .arg = 33, .idf = idfRouteRx, .ours = ourRouteRx }, + }, + .setup = setup, +}; + +// ------------------------------------------------------------------------------------- baud rate + +fn idfBaud115200() void { + _ = oracle_uart_set_baudrate(port, 115200, sclk_freq); +} +fn ourBaud115200() void { + _ = ours().setBaudrate(115200, sclk_freq); +} +fn idfBaud9600() void { + _ = oracle_uart_set_baudrate(port, 9600, sclk_freq); +} +fn ourBaud9600() void { + _ = ours().setBaudrate(9600, sclk_freq); +} +fn idfBaud300() void { + _ = oracle_uart_set_baudrate(port, 300, sclk_freq); +} +fn ourBaud300() void { + _ = ours().setBaudrate(300, sclk_freq); +} +fn idfBaud1M() void { + _ = oracle_uart_set_baudrate(port, 1_000_000, sclk_freq); +} +fn ourBaud1M() void { + _ = ours().setBaudrate(1_000_000, sclk_freq); +} + +// ----------------------------------------------------------------------------------- data format +// The numeric arguments to IDF's side are its own enum values from uart_types.h: word length is +// (bits - 5), stop bits are 1/2/3 for 1/1.5/2, parity is 0/2/3 for disable/even/odd. + +fn idfBits8() void { + oracle_uart_set_data_bit_num(port, 3); +} +fn ourBits8() void { + ours().setWordLength(.bits8); +} +fn idfBits5() void { + oracle_uart_set_data_bit_num(port, 0); +} +fn ourBits5() void { + ours().setWordLength(.bits5); +} +fn idfStop2() void { + oracle_uart_set_stop_bits(port, 3); +} +fn ourStop2() void { + ours().setStopBits(.two); +} +fn idfStop15() void { + oracle_uart_set_stop_bits(port, 2); +} +fn ourStop15() void { + ours().setStopBits(.one_and_half); +} +fn idfParityOdd() void { + oracle_uart_set_parity(port, 3); +} +fn ourParityOdd() void { + ours().setParity(.odd); +} +fn idfParityEven() void { + oracle_uart_set_parity(port, 2); +} +fn ourParityEven() void { + ours().setParity(.even); +} +fn idfParityOddThenOff() void { + oracle_uart_set_parity(port, 3); + oracle_uart_set_parity(port, 0); +} +fn ourParityOddThenOff() void { + const u = ours(); + u.setParity(.odd); + u.setParity(.disable); +} + +// -------------------------------------------------------------------------------------- loopback + +fn idfLoopOn() void { + oracle_uart_set_loop_back(port, 1); +} +fn ourLoopOn() void { + ours().setLoopback(true); +} +fn idfLoopOff() void { + oracle_uart_set_loop_back(port, 0); +} +fn ourLoopOff() void { + ours().setLoopback(false); +} + +// ------------------------------------------------------------------------------ FIFO and commit + +fn idfTxFifoRst() void { + oracle_uart_txfifo_rst(port); +} +fn ourTxFifoRst() void { + ours().resetTxFifo(); +} +fn idfRxFifoRst() void { + oracle_uart_rxfifo_rst(port); +} +fn ourRxFifoRst() void { + ours().resetRxFifo(); +} +fn idfUpdate() void { + oracle_uart_update(port); +} +fn ourUpdate() void { + _ = ours().update(); +} + +// ----------------------------------------------------------------------------------- pin routing + +fn idfRouteTx() void { + oracle_uart_route_tx(port, route_pin); +} +fn ourRouteTx() void { + ours().routeTx(route_pin); +} +fn idfRouteRx() void { + oracle_uart_route_rx(port, route_pin); +} +fn ourRouteRx() void { + ours().routeRx(route_pin); +} diff --git a/src/oracle/uart_ref.c b/src/oracle/uart_ref.c new file mode 100644 index 0000000..6476fd4 --- /dev/null +++ b/src/oracle/uart_ref.c @@ -0,0 +1,163 @@ +/* UART's reference implementation: ESP-IDF's own `uart_ll.h`, given external linkage. + * + * No logic here. Anything clever in this file would be a third implementation to doubt, and the + * whole point of the differential is that one side is unmodified IDF. + * + * Two IDF-specific notes: + * + * - Several of these LL functions are shadowed by a macro that references + * `__DECLARE_RCC_ATOMIC_ENV`, an identifier IDF never defines anywhere, so that an unguarded call + * fails to compile: HP_SYS_CLKRST's PERI_CLK_CTRL and SOC_CLK_CTRL registers are shared with + * unrelated peripherals and the only legal caller holds a spinlock. There is no FreeRTOS here and + * core 1 is held in reset at power-on, so declaring the name is as safe as the spinlock would be, + * and it is what IDF's own bootloader does (bootloader_console.c:53). + * - `uart_ll_set_sclk` and `uart_ll_set_baudrate` are *function-like macros* wrapping + * `uart_ll_set_sclk` / `_uart_ll_set_baudrate` (uart_ll.h:477-481, 578-586). Calling the + * underscored inner function directly would skip the very guard above, so these wrappers call + * through the macro. For `set_sclk` the macro and the function share a name, which works only + * because the macro is defined after the function. + */ + +static int __DECLARE_RCC_ATOMIC_ENV __attribute__((unused)); + +#include "hal/uart_ll.h" +#include "soc/uart_struct.h" + +/* Nothing here touches UART0. Resetting UART0 clears UART_CLKDIV, and UART0 is this board's + * console: the output turns to garbage mid-character and the board takes a watchdog reset with + * nothing readable left to say why. Measured on this board. The suite drives UART1. */ +static inline uart_dev_t *dev(unsigned num) +{ + return UART_LL_GET_HW(num); +} + +/* ---------------------------------------------------------------------- clocks, reset, baud */ + +/* Only the operations uart_cases.zig actually pairs are wrapped. A wrapper with no case behind it + * is unreachable code that looks like coverage - and for `uart_ll_reset_register` in particular it + * would be a loaded gun, since the harness restores this block through its own reset-bit path and + * calling that function with num = 0 kills the console. */ + +/* The whole point of the UART suite: IDF's baud-rate arithmetic, which picks an integer pre-divider + * and then a 12-bit divider with a 4-bit fraction, and commits through REG_UPDATE. Returns false on + * the rates it cannot represent - which is a real outcome, not an error path, since a 12-bit divider + * cannot reach every baud from every source clock. uart_ll.h:532-588. */ +int oracle_uart_set_baudrate(unsigned num, unsigned baud, unsigned sclk_freq) +{ + return _uart_ll_set_baudrate(UART_LL_GET_HW(num), baud, sclk_freq) ? 1 : 0; +} + +void oracle_uart_enable_bus_clock(unsigned num, int enable) +{ + uart_ll_enable_bus_clock((uart_port_t)num, enable != 0); +} + +void oracle_uart_sclk_enable(unsigned num) +{ + uart_ll_sclk_enable(dev(num)); +} + + +/* `sel` is the soc_module_clk_t value, not the raw field encoding: UART_SCLK_XTAL, + * UART_SCLK_RTC, UART_SCLK_PLL_F80M. The mapping from those to the 2-bit field is the part of + * uart_ll_set_sclk under test. */ +/* The clock source, named on the C side rather than passed as a number. + * + * `UART_SCLK_XTAL` is an enumerator of `soc_module_clk_t`, not a small ordinal: clk_tree_defs.h:280 + * defines it as SOC_MOD_CLK_XTAL, whose value is whatever position it happens to occupy in a + * chip-wide enum. Passing 0 from Zig - which is what a first version did - lands on an unmatched + * case in `_uart_ll_set_sclk`, whose default is HAL_ASSERT(false); compiled at assertion level 0 + * that is `__builtin_unreachable()`, so the select is left at a value with no clock behind it and + * the very next `uart_ll_update` spins forever on a commit that cannot land. The harness reached the + * first UART case and went silent, which looks exactly like a crash. + * + * Keeping the enumerator on this side of the boundary removes the class of bug entirely. */ +void oracle_uart_set_sclk_xtal(unsigned num) +{ + uart_ll_set_sclk(UART_LL_GET_HW(num), UART_SCLK_XTAL); +} + +void oracle_uart_set_sclk_pll(unsigned num) +{ + uart_ll_set_sclk(UART_LL_GET_HW(num), UART_SCLK_PLL_F80M); +} + +void oracle_uart_set_sclk(unsigned num, unsigned sel) +{ + uart_ll_set_sclk(dev(num), (soc_module_clk_t)sel); +} + + +/* -------------------------------------------------------------------------------- data format */ + +void oracle_uart_set_data_bit_num(unsigned num, unsigned bits) +{ + uart_ll_set_data_bit_num(dev(num), (uart_word_length_t)bits); +} + +void oracle_uart_set_stop_bits(unsigned num, unsigned stop) +{ + uart_ll_set_stop_bits(dev(num), (uart_stop_bits_t)stop); +} + +void oracle_uart_set_parity(unsigned num, unsigned parity) +{ + uart_ll_set_parity(dev(num), (uart_parity_t)parity); +} + +/* ---------------------------------------------------------------------------------------- FIFO */ + +void oracle_uart_txfifo_rst(unsigned num) +{ + uart_ll_txfifo_rst(dev(num)); +} + +void oracle_uart_rxfifo_rst(unsigned num) +{ + uart_ll_rxfifo_rst(dev(num)); +} + + +/* ------------------------------------------------------------------------- loopback and update */ + +void oracle_uart_set_loop_back(unsigned num, int enable) +{ + uart_ll_set_loop_back(dev(num), enable != 0); +} + +void oracle_uart_update(unsigned num) +{ + uart_ll_update(dev(num)); +} + +/* ---------------------------------------------------------------------------------- pin routing */ + +/* IDF routes UART pins through esp_rom_gpio_connect_*_signal / gpio_ll, not through uart_ll, so the + * reference for pin routing is the GPIO LL - the same functions gpio_ref.c wraps, called with this + * UART's signal indices. Kept here rather than in gpio_ref.c because the *signal index* is the part + * under test, and it belongs to the UART. */ +#include "hal/gpio_ll.h" +#include "soc/gpio_struct.h" +/* The signal indices themselves, which are not in any *_ll.h - and are the whole point of these two + * wrappers: the Zig side reads the same macros through translate-c. */ +#include "soc/gpio_sig_map.h" + +void oracle_uart_route_tx(unsigned num, unsigned pin) +{ + const unsigned sig[5] = { + UART0_TXD_PAD_OUT_IDX, UART1_TXD_PAD_OUT_IDX, UART2_TXD_PAD_OUT_IDX, + UART3_TXD_PAD_OUT_IDX, UART4_TXD_PAD_OUT_IDX, + }; + gpio_ll_set_output_signal_matrix_source(&GPIO, pin, sig[num], false); + gpio_ll_set_output_enable_ctrl(&GPIO, pin, true, false); +} + +void oracle_uart_route_rx(unsigned num, unsigned pin) +{ + const unsigned sig[5] = { + UART0_RXD_PAD_IN_IDX, UART1_RXD_PAD_IN_IDX, UART2_RXD_PAD_IN_IDX, + UART3_RXD_PAD_IN_IDX, UART4_RXD_PAD_IN_IDX, + }; + gpio_ll_input_enable(&GPIO, pin); + gpio_ll_set_input_signal_matrix_source(&GPIO, sig[num], pin, false); +} -- cgit v1.3