summaryrefslogtreecommitdiff
path: root/src/oracle/i2c_cases.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-25 12:40:53 -0300
committerGabriel Schneider <[email protected]>2026-08-25 12:46:51 -0300
commitf5f8068fac59b4f16046c2022c2fc7c7e447ef4c (patch)
tree2731a3ed4e51cae09e184e25778eded5fc37d1f5 /src/oracle/i2c_cases.zig
downloadesp32p4-f5f8068fac59b4f16046c2022c2fc7c7e447ef4c.tar.gz
esp32p4-f5f8068fac59b4f16046c2022c2fc7c7e447ef4c.zip
zig-p4: pure-Zig ESP32-P4 toolchain
build.zig generates the linker script and drives Zig's own LLD; tools/image.zig turns the ELF into a flashable image and tools/{rom,serial}.zig speak the mask ROM loader over the UART. No CMake, ninja, idf.py, esptool, or external linker. src/soc.zig is a comptime register model over ESP-IDF's own *_reg.h headers; src/hal/ adds peripheral sequences; src/io/ implements std.Io for the chip; src/oracle/ diffs this HAL against ESP-IDF's on the die.
Diffstat (limited to 'src/oracle/i2c_cases.zig')
-rw-r--r--src/oracle/i2c_cases.zig627
1 files changed, 627 insertions, 0 deletions
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);
+}