diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-25 12:40:53 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-25 12:46:51 -0300 |
| commit | f5f8068fac59b4f16046c2022c2fc7c7e447ef4c (patch) | |
| tree | 2731a3ed4e51cae09e184e25778eded5fc37d1f5 /src/oracle/i2c_cases.zig | |
| download | esp32p4-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.zig | 627 |
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); +} |
