summaryrefslogtreecommitdiff
path: root/src/hal/i2c.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/hal/i2c.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/hal/i2c.zig')
-rw-r--r--src/hal/i2c.zig1091
1 files changed, 1091 insertions, 0 deletions
diff --git a/src/hal/i2c.zig b/src/hal/i2c.zig
new file mode 100644
index 0000000..2a1d8db
--- /dev/null
+++ b/src/hal/i2c.zig
@@ -0,0 +1,1091 @@
+//! I2C0 and I2C1 in master mode, FIFO access, no interrupts and no DMA.
+//!
+//! Slave mode, LP_I2C and the RAM (non-FIFO) access path are deliberately absent.
+//!
+//! Three things about this peripheral are not visible in the register headers, and each one is a
+//! way for a port to produce a bus that half-works:
+//!
+//! **1. The timing is a dozen registers computed from one number.** SCL low, SCL high, SCL
+//! wait-high, SDA hold, SDA sample, start hold, restart setup, stop hold, stop setup and the
+//! timeout exponent all come from a single `half_cycle` derived from the source clock and the wanted
+//! SCL frequency, and several of them are written *minus one* while two deliberately are not. The
+//! arithmetic is reproduced from ESP-IDF exactly, with the line numbers, in `Timing.calculate` and
+//! `applyTiming` below - including the parts that look like bugs and are not.
+//!
+//! **2. Nothing takes effect until `CONF_UPGATE` is written.** The timing and control registers feed
+//! a synchroniser rather than the state machine directly, so a driver that configures the block and
+//! starts a transaction without `commitConfig()` runs on the *previous* configuration. It is a
+//! write-to-trigger bit that reads back 0, so nothing about the register state afterwards shows
+//! whether it was ever written - which is exactly the kind of bug a state-comparing differential
+//! test cannot see, so it is called out here instead. ESP-IDF puts the call in the driver
+//! (`esp_driver_i2c/i2c_master.c:96`, `i2c_ll_update` at `i2c_ll.h:137-141`), not in the LL
+//! functions that write the timing.
+//!
+//! **3. The command opcode numbers changed after the original ESP32, and this chip's own register
+//! header still documents the old ones.** `i2c_struct.h:1009-1021` and `i2c_reg.h:1174-1186` say
+//! "0: RSTART, 1: WRITE, 2: READ, 3: STOP, 4: END". ESP-IDF's P4 LL says RESTART=6, WRITE=1,
+//! READ=3, STOP=2 (`i2c_ll.h:55-59`), which is what every post-ESP32 target uses (esp32c3, esp32c6
+//! and esp32p4 agree; only `esp32/include/hal/i2c_ll.h:47-51` has the numbers the P4 header's prose
+//! describes). The LL is the version the shipping driver runs on silicon, so it is the one here, and
+//! `i2c_ref.c` builds its command words from IDF's own `I2C_LL_CMD_*` macros so that the
+//! differential test would catch a wrong constant here rather than agreeing with it.
+//!
+//! And one hazard for anything that snapshots this block: **reading `I2C_DATA_REG` pops the RX
+//! FIFO.** See `Data register` below.
+
+const std = @import("std");
+const regs = @import("regs");
+const mmio = @import("mmio");
+const gpio = @import("gpio.zig");
+const clkrst = @import("clkrst.zig");
+
+const Reg = mmio.Reg;
+const Field = mmio.Field;
+
+/// HP I2C instances. LP_I2C is a third `i2c_dev_t` in ESP-IDF (`SOC_I2C_NUM` is 3, `soc_caps.h:310`)
+/// but it lives in the LP domain with its own clock and pad rules, and is out of scope here.
+pub const port_count: u8 = 2;
+
+/// Bytes in each direction. `i2c_ll.h:29` (`I2C_LL_FIFO_LEN`); the RAM behind it is 32 bytes at
+/// +0x100 (TX) and +0x180 (RX), reachable directly only in non-FIFO mode.
+pub const fifo_len: u8 = 32;
+
+/// Command slots. **Eight on this chip**, not sixteen: `i2c_ll.h:31` says `I2C_LL_CMD_REG_NUM 8`,
+/// `i2c_struct.h:1073` declares `command[8]`, and the register header stops at `I2C_COMD7_REG`
+/// (+0x74). ESP-IDF's own `i2c_ll_master_write_cmd_reg` doc comment claims "should be less than 16"
+/// (`i2c_ll.h:433`) - that comment is stale, and `i2c_ll_master_is_cmd_done` two hundred lines later
+/// says 8 (`i2c_ll.h:1043`). Eight slots is why the driver's long transfers end a chunk with an END
+/// opcode and continue: there is no room for a command per byte.
+pub const cmd_slots: u8 = 8;
+
+// ------------------------------------------------------------------------------------ registers
+//
+// One array per register, indexed by port. The stride is checked against I2C1's own macro rather
+// than assumed: `REG_I2C_BASE(i)` is `DR_REG_I2C0_BASE + i * 0x1000` (`soc/esp32p4/include/soc/
+// soc.h:24`), which the linker script agrees with (`esp32p4.peripherals.ld:17-18`, I2C0 =
+// 0x500C4000, I2C1 = 0x500C5000).
+
+fn portArray(comptime macro0: anytype, comptime macro1: anytype) type {
+ return mmio.RegArray(macro0, macro1, port_count);
+}
+
+const scl_low_period = portArray(regs.I2C_SCL_LOW_PERIOD_REG(0), regs.I2C_SCL_LOW_PERIOD_REG(1));
+const ctr = portArray(regs.I2C_CTR_REG(0), regs.I2C_CTR_REG(1));
+const sr = portArray(regs.I2C_SR_REG(0), regs.I2C_SR_REG(1));
+const to = portArray(regs.I2C_TO_REG(0), regs.I2C_TO_REG(1));
+const fifo_st = portArray(regs.I2C_FIFO_ST_REG(0), regs.I2C_FIFO_ST_REG(1));
+const fifo_conf = portArray(regs.I2C_FIFO_CONF_REG(0), regs.I2C_FIFO_CONF_REG(1));
+const data = portArray(regs.I2C_DATA_REG(0), regs.I2C_DATA_REG(1));
+const int_raw = portArray(regs.I2C_INT_RAW_REG(0), regs.I2C_INT_RAW_REG(1));
+const int_clr = portArray(regs.I2C_INT_CLR_REG(0), regs.I2C_INT_CLR_REG(1));
+const int_ena = portArray(regs.I2C_INT_ENA_REG(0), regs.I2C_INT_ENA_REG(1));
+const sda_hold = portArray(regs.I2C_SDA_HOLD_REG(0), regs.I2C_SDA_HOLD_REG(1));
+const sda_sample = portArray(regs.I2C_SDA_SAMPLE_REG(0), regs.I2C_SDA_SAMPLE_REG(1));
+const scl_high_period = portArray(regs.I2C_SCL_HIGH_PERIOD_REG(0), regs.I2C_SCL_HIGH_PERIOD_REG(1));
+const scl_start_hold = portArray(regs.I2C_SCL_START_HOLD_REG(0), regs.I2C_SCL_START_HOLD_REG(1));
+const scl_rstart_setup = portArray(regs.I2C_SCL_RSTART_SETUP_REG(0), regs.I2C_SCL_RSTART_SETUP_REG(1));
+const scl_stop_hold = portArray(regs.I2C_SCL_STOP_HOLD_REG(0), regs.I2C_SCL_STOP_HOLD_REG(1));
+const scl_stop_setup = portArray(regs.I2C_SCL_STOP_SETUP_REG(0), regs.I2C_SCL_STOP_SETUP_REG(1));
+const filter_cfg = portArray(regs.I2C_FILTER_CFG_REG(0), regs.I2C_FILTER_CFG_REG(1));
+const comd0 = portArray(regs.I2C_COMD0_REG(0), regs.I2C_COMD0_REG(1));
+const scl_sp_conf = portArray(regs.I2C_SCL_SP_CONF_REG(0), regs.I2C_SCL_SP_CONF_REG(1));
+
+/// First address of a port's register block, for the differential harness's window.
+pub inline fn base(port: u8) u32 {
+ std.debug.assert(port < port_count);
+ return @intCast(scl_low_period.base + scl_low_period.stride * port);
+}
+
+// I2C_CTR_REG. `trans_start`, `fsm_rst` and `conf_upgate` are write-to-trigger: they read back 0,
+// so a read-modify-write of this register does not re-trigger them.
+const sda_force_out = Field.of(regs.I2C_SDA_FORCE_OUT_S, regs.I2C_SDA_FORCE_OUT_V);
+const scl_force_out = Field.of(regs.I2C_SCL_FORCE_OUT_S, regs.I2C_SCL_FORCE_OUT_V);
+const rx_full_ack_level = Field.of(regs.I2C_RX_FULL_ACK_LEVEL_S, regs.I2C_RX_FULL_ACK_LEVEL_V);
+const ms_mode = Field.of(regs.I2C_MS_MODE_S, regs.I2C_MS_MODE_V);
+const trans_start = Field.of(regs.I2C_TRANS_START_S, regs.I2C_TRANS_START_V);
+const tx_lsb_first = Field.of(regs.I2C_TX_LSB_FIRST_S, regs.I2C_TX_LSB_FIRST_V);
+const rx_lsb_first = Field.of(regs.I2C_RX_LSB_FIRST_S, regs.I2C_RX_LSB_FIRST_V);
+const arbitration_en = Field.of(regs.I2C_ARBITRATION_EN_S, regs.I2C_ARBITRATION_EN_V);
+const fsm_rst = Field.of(regs.I2C_FSM_RST_S, regs.I2C_FSM_RST_V);
+const conf_upgate = Field.of(regs.I2C_CONF_UPGATE_S, regs.I2C_CONF_UPGATE_V);
+
+// I2C_SR_REG, all read-only.
+const resp_rec = Field.of(regs.I2C_RESP_REC_S, regs.I2C_RESP_REC_V);
+const arb_lost = Field.of(regs.I2C_ARB_LOST_S, regs.I2C_ARB_LOST_V);
+const bus_busy = Field.of(regs.I2C_BUS_BUSY_S, regs.I2C_BUS_BUSY_V);
+const rxfifo_cnt = Field.of(regs.I2C_RXFIFO_CNT_S, regs.I2C_RXFIFO_CNT_V);
+const txfifo_cnt = Field.of(regs.I2C_TXFIFO_CNT_S, regs.I2C_TXFIFO_CNT_V);
+
+// I2C_TO_REG. `time_out_value` is only five bits wide - the timeout is 2^value source-clock cycles,
+// so 31 is the largest legal exponent and the arithmetic below never approaches it.
+const time_out_value = Field.of(regs.I2C_TIME_OUT_VALUE_S, regs.I2C_TIME_OUT_VALUE_V);
+const time_out_en = Field.of(regs.I2C_TIME_OUT_EN_S, regs.I2C_TIME_OUT_EN_V);
+
+// I2C_FIFO_CONF_REG. `rx_fifo_rst`/`tx_fifo_rst` are annotated R/W, not self-clearing: they hold
+// the FIFO in reset until written back to 0, which is why resetting one is two stores.
+const rxfifo_wm_thrhd = Field.of(regs.I2C_RXFIFO_WM_THRHD_S, regs.I2C_RXFIFO_WM_THRHD_V);
+const txfifo_wm_thrhd = Field.of(regs.I2C_TXFIFO_WM_THRHD_S, regs.I2C_TXFIFO_WM_THRHD_V);
+const nonfifo_en = Field.of(regs.I2C_NONFIFO_EN_S, regs.I2C_NONFIFO_EN_V);
+const rx_fifo_rst = Field.of(regs.I2C_RX_FIFO_RST_S, regs.I2C_RX_FIFO_RST_V);
+const tx_fifo_rst = Field.of(regs.I2C_TX_FIFO_RST_S, regs.I2C_TX_FIFO_RST_V);
+const fifo_prt_en = Field.of(regs.I2C_FIFO_PRT_EN_S, regs.I2C_FIFO_PRT_EN_V);
+
+// Timing fields. Every period is nine bits ([8:0], max 511) except `scl_wait_high_period`, which is
+// seven ([15:9], max 127) and shares its register with `scl_high_period`.
+const scl_low_period_f = Field.of(regs.I2C_SCL_LOW_PERIOD_S, regs.I2C_SCL_LOW_PERIOD_V);
+const scl_high_period_f = Field.of(regs.I2C_SCL_HIGH_PERIOD_S, regs.I2C_SCL_HIGH_PERIOD_V);
+const scl_wait_high_period_f = Field.of(regs.I2C_SCL_WAIT_HIGH_PERIOD_S, regs.I2C_SCL_WAIT_HIGH_PERIOD_V);
+const sda_hold_time = Field.of(regs.I2C_SDA_HOLD_TIME_S, regs.I2C_SDA_HOLD_TIME_V);
+const sda_sample_time = Field.of(regs.I2C_SDA_SAMPLE_TIME_S, regs.I2C_SDA_SAMPLE_TIME_V);
+const scl_start_hold_time = Field.of(regs.I2C_SCL_START_HOLD_TIME_S, regs.I2C_SCL_START_HOLD_TIME_V);
+const scl_rstart_setup_time = Field.of(regs.I2C_SCL_RSTART_SETUP_TIME_S, regs.I2C_SCL_RSTART_SETUP_TIME_V);
+const scl_stop_hold_time = Field.of(regs.I2C_SCL_STOP_HOLD_TIME_S, regs.I2C_SCL_STOP_HOLD_TIME_V);
+const scl_stop_setup_time = Field.of(regs.I2C_SCL_STOP_SETUP_TIME_S, regs.I2C_SCL_STOP_SETUP_TIME_V);
+
+// I2C_FILTER_CFG_REG. Both thresholds are four bits, both filters default *enabled* with a
+// threshold of 0 - which filters nothing - so "disable" and "enable with 0" are different words.
+const scl_filter_thres = Field.of(regs.I2C_SCL_FILTER_THRES_S, regs.I2C_SCL_FILTER_THRES_V);
+const sda_filter_thres = Field.of(regs.I2C_SDA_FILTER_THRES_S, regs.I2C_SDA_FILTER_THRES_V);
+const scl_filter_en = Field.of(regs.I2C_SCL_FILTER_EN_S, regs.I2C_SCL_FILTER_EN_V);
+const sda_filter_en = Field.of(regs.I2C_SDA_FILTER_EN_S, regs.I2C_SDA_FILTER_EN_V);
+
+// I2C_SCL_SP_CONF_REG: the hardware bus-clear generator.
+const scl_rst_slv_en = Field.of(regs.I2C_SCL_RST_SLV_EN_S, regs.I2C_SCL_RST_SLV_EN_V);
+const scl_rst_slv_num = Field.of(regs.I2C_SCL_RST_SLV_NUM_S, regs.I2C_SCL_RST_SLV_NUM_V);
+
+/// Data register offset in words, for the harness's `no_read` list. See `Data register` below.
+pub const data_word_offset: u32 = (0x1c - 0x00) / 4;
+
+// ----------------------------------------------------------------------------- clocks and reset
+//
+// I2C has clock control in two places, and the split is not symmetrical between the two ports:
+//
+// * the APB bus clock gate and the block reset are in HP_SYS_CLKRST's shared registers, and live
+// in `clkrst.zig` with every other peripheral's (`i2c_ll.h:149-176`);
+// * the *controller* clock - the one the bus state machine runs on - its source select and its
+// divider are I2C-specific fields of HP_SYS_CLKRST_PERI_CLK_CTRL10/11, and are here.
+//
+// The asymmetry is the trap: I2C1's source select and controller-clock enable are in PERI_CLK_CTRL10
+// beside I2C0's (bits 26 and 27, `i2c_ll.h:851-852` and `i2c_ll.h:944-945`), while I2C1's *divider*
+// is in PERI_CLK_CTRL11 (`i2c_ll.h:196-199`). Reading the field names alone would put all of I2C1
+// in ctrl11.
+
+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_clk_src_sel = Field.of(regs.HP_SYS_CLKRST_REG_I2C0_CLK_SRC_SEL_S, regs.HP_SYS_CLKRST_REG_I2C0_CLK_SRC_SEL_V);
+const i2c1_clk_src_sel = Field.of(regs.HP_SYS_CLKRST_REG_I2C1_CLK_SRC_SEL_S, regs.HP_SYS_CLKRST_REG_I2C1_CLK_SRC_SEL_V);
+const i2c0_clk_en = Field.of(regs.HP_SYS_CLKRST_REG_I2C0_CLK_EN_S, regs.HP_SYS_CLKRST_REG_I2C0_CLK_EN_V);
+const i2c1_clk_en = Field.of(regs.HP_SYS_CLKRST_REG_I2C1_CLK_EN_S, regs.HP_SYS_CLKRST_REG_I2C1_CLK_EN_V);
+const i2c0_div_num = Field.of(regs.HP_SYS_CLKRST_REG_I2C0_CLK_DIV_NUM_S, regs.HP_SYS_CLKRST_REG_I2C0_CLK_DIV_NUM_V);
+const i2c0_div_numerator = Field.of(regs.HP_SYS_CLKRST_REG_I2C0_CLK_DIV_NUMERATOR_S, regs.HP_SYS_CLKRST_REG_I2C0_CLK_DIV_NUMERATOR_V);
+const i2c0_div_denominator = Field.of(regs.HP_SYS_CLKRST_REG_I2C0_CLK_DIV_DENOMINATOR_S, regs.HP_SYS_CLKRST_REG_I2C0_CLK_DIV_DENOMINATOR_V);
+const i2c1_div_num = Field.of(regs.HP_SYS_CLKRST_REG_I2C1_CLK_DIV_NUM_S, regs.HP_SYS_CLKRST_REG_I2C1_CLK_DIV_NUM_V);
+const i2c1_div_numerator = Field.of(regs.HP_SYS_CLKRST_REG_I2C1_CLK_DIV_NUMERATOR_S, regs.HP_SYS_CLKRST_REG_I2C1_CLK_DIV_NUMERATOR_V);
+const i2c1_div_denominator = Field.of(regs.HP_SYS_CLKRST_REG_I2C1_CLK_DIV_DENOMINATOR_S, regs.HP_SYS_CLKRST_REG_I2C1_CLK_DIV_DENOMINATOR_V);
+
+/// Controller clock source. Two choices on this chip (`clk_tree_defs.h:486-494`), and the register
+/// field is one bit: 0 = XTAL, 1 = RC_FAST (`i2c_ll.h:848-852`).
+pub const Source = enum(u1) {
+ /// 40 MHz on this board, and the default. Accurate, which for a bus with a specified maximum
+ /// clock is the whole point.
+ xtal = 0,
+ /// The internal RC oscillator, ~20 MHz and temperature-dependent. Usable only because I2C is a
+ /// clocked bus with no baud-rate agreement to keep.
+ rc_fast = 1,
+};
+
+/// XTAL frequency on this board, as the source frequency to hand `Timing.calculate` for
+/// `Source.xtal`. Fixed by the crystal, not by the clock tree: 40 MHz.
+pub const xtal_hz: u32 = 40_000_000;
+
+/// Select the controller clock source. A read-modify-write of a register shared with the other
+/// port's clock fields, so it takes the interrupt guard.
+pub fn setSource(port: u8, src: Source) void {
+ std.debug.assert(port < port_count);
+ const v: u32 = @intFromEnum(src);
+ const guard = clkrst.maskInterrupts();
+ defer guard.release();
+ peri_clk_ctrl10.modify(.{if (port == 0) i2c0_clk_src_sel.is(v) else i2c1_clk_src_sel.is(v)});
+}
+
+/// The controller clock gate, which is *not* the APB gate in `clkrst.zig`: registers stay readable
+/// and writable with this off, and only the bus state machine stops. It defaults to 0
+/// (`hp_sys_clkrst_reg.h`, REG_I2C0_CLK_EN default 0), so unlike most peripherals on this chip I2C
+/// genuinely needs this call before it will do anything. `_i2c_hal_init` (`i2c_hal.c:52-58`) is
+/// where ESP-IDF makes it.
+pub fn setControllerClockEnabled(port: u8, on: bool) void {
+ std.debug.assert(port < port_count);
+ const v: u32 = @intFromBool(on);
+ const guard = clkrst.maskInterrupts();
+ defer guard.release();
+ peri_clk_ctrl10.modify(.{if (port == 0) i2c0_clk_en.is(v) else i2c1_clk_en.is(v)});
+}
+
+// -------------------------------------------------------------------------------------- timing
+
+/// Everything the bus timing registers need, in source-clock cycles, as ESP-IDF computes it.
+///
+/// The field widths are ESP-IDF's: `i2c_hal_clk_config_t` is nine `uint16_t`
+/// (`hal/i2c_types.h:46-56`). That matters at the extremes - a value that would exceed 65535 wraps
+/// there too - and it is why this is `u16` rather than `u32`.
+pub const Timing = struct {
+ /// Controller clock divider, as a *count*: the register takes this minus one.
+ clkm_div: u16,
+ scl_low: u16,
+ scl_high: u16,
+ scl_wait_high: u16,
+ sda_hold: u16,
+ sda_sample: u16,
+ /// Both the start-condition and the stop-condition setup time.
+ setup: u16,
+ /// Both the start-condition and the stop-condition hold time.
+ hold: u16,
+ /// Timeout *exponent*: the bus times out after 2^tout source-clock cycles.
+ tout: u16,
+
+ /// Reproduce `i2c_ll_master_cal_bus_clk` (`i2c_ll.h:104-128`) exactly.
+ ///
+ /// The whole derivation, because every line of it is load-bearing:
+ ///
+ /// clkm_div = source / (bus * 1024) + 1
+ /// sclk = source / clkm_div
+ /// half = sclk / bus / 2
+ ///
+ /// The `+ 1` is not rounding, it is a floor: the period registers are nine bits, so `half` must
+ /// stay under 512, and dividing the source clock until `sclk <= 1024 * bus` is what guarantees
+ /// it. At 40 MHz that makes `clkm_div` 1 for every bus frequency above 39 kHz and grows it
+ /// below - 10 kHz gives `clkm_div` 4, `sclk` 10 MHz, `half` 500 - so the divider is not an
+ /// optional refinement, it is what makes slow buses representable at all.
+ ///
+ /// From `half`, in source-clock cycles:
+ ///
+ /// scl_low = half
+ /// scl_wait_high = half/2 - 2 if bus >= 80 kHz, else half/4
+ /// scl_high = half - scl_wait_high
+ /// sda_hold = half/4
+ /// sda_sample = half/2
+ /// setup = hold = half
+ /// tout = 32 - clz(5 * half) + 2
+ ///
+ /// `scl_wait_high` is the part of the high period during which the master waits for the slave to
+ /// release SCL (clock stretching); `scl_high` is the part it drives. They sum to `half`, so the
+ /// nominal frequency is the same either way, and IDF's own comment (`i2c_ll.h:112-114`) records
+ /// why the split changes at 80 kHz: below that, too much wait-high measurably *raises* the
+ /// frequency on real hardware.
+ ///
+ /// The `tout` expression is `log2(5 * half) + 2` written with a count-leading-zeros: a timeout
+ /// of about 20 half-cycles, i.e. ten bit times, rounded up to the next power of two because the
+ /// register holds an exponent. IDF writes it as
+ /// `sizeof(half_cycle) * 8 - __builtin_clz(5 * half_cycle) + 2` with `half_cycle` a `uint32_t`,
+ /// hence the 32 here.
+ ///
+ /// Not reproduced: the `HAL_ASSERT` at `i2c_ll.h:126-127` that
+ /// `scl_wait_high < sda_sample < scl_high`. It holds for every frequency this can be asked for
+ /// at 40 MHz (checked from 10 kHz to 1 MHz), and an assert that cannot fire is noise; the
+ /// ordering it protects is a hardware requirement, not something this code can choose.
+ pub fn calculate(source_hz: u32, bus_hz: u32) Timing {
+ std.debug.assert(bus_hz > 0);
+ std.debug.assert(source_hz / 2 > bus_hz);
+
+ const clkm_div: u32 = source_hz / (bus_hz * 1024) + 1;
+ const sclk_hz: u32 = source_hz / clkm_div;
+ const half: u32 = sclk_hz / bus_hz / 2;
+
+ const wait_high: u32 = if (bus_hz >= 80_000) half / 2 - 2 else half / 4;
+ return .{
+ .clkm_div = @truncate(clkm_div),
+ .scl_low = @truncate(half),
+ .scl_wait_high = @truncate(wait_high),
+ .scl_high = @truncate(half - wait_high),
+ .sda_hold = @truncate(half / 4),
+ .sda_sample = @truncate(half / 2),
+ .setup = @truncate(half),
+ .hold = @truncate(half),
+ // @clz(0) is 32 in Zig where __builtin_clz(0) is undefined in C, so this differs from
+ // IDF only for half == 0, which the assert above rules out.
+ .tout = @truncate(32 - @clz(5 * half) + 2),
+ };
+ }
+};
+
+/// Write a computed `Timing` to the peripheral's ten timing registers and the controller-clock
+/// divider - `i2c_ll_master_set_bus_timing` (`i2c_ll.h:190-220`).
+///
+/// **Which values are written minus one and which are not is the substance of this function.**
+/// Eight of the ten are `value - 1`, because the hardware counts from zero. `scl_high_period` and
+/// `scl_wait_high_period` are written as-is, and that asymmetry is deliberate: IDF's comment
+/// (`i2c_ll.h:201-205`) says the Technical Reference Manual asks for minus one on those two as well,
+/// and that following it measurably produces an SCL a little *faster* than asked for, so they do not
+/// subtract. A port that "fixes" this by making all ten consistent gets a bus that is out of spec at
+/// the top end and passes every test that does not include an oscilloscope.
+///
+/// Subtractions are done in `u32` with wrapping and truncated by the field write, which is what the
+/// C does for a `uint16_t` of 0 as well - it is unreachable here anyway, since `calculate` asserts
+/// `half >= 1`.
+pub fn applyTiming(port: u8, t: Timing) void {
+ std.debug.assert(port < port_count);
+ setClockDivider(port, t.clkm_div);
+
+ scl_low_period.at(port).modify(.{scl_low_period_f.is(@as(u32, t.scl_low) -% 1)});
+ // One store where IDF does two read-modify-writes of the same register (`i2c_ll.h:207-208`).
+ // Same final word; a write-trace comparison sees the difference, a state comparison does not.
+ scl_high_period.at(port).modify(.{
+ scl_high_period_f.is(t.scl_high),
+ scl_wait_high_period_f.is(t.scl_wait_high),
+ });
+ sda_hold.at(port).modify(.{sda_hold_time.is(@as(u32, t.sda_hold) -% 1)});
+ sda_sample.at(port).modify(.{sda_sample_time.is(@as(u32, t.sda_sample) -% 1)});
+ scl_rstart_setup.at(port).modify(.{scl_rstart_setup_time.is(@as(u32, t.setup) -% 1)});
+ scl_stop_setup.at(port).modify(.{scl_stop_setup_time.is(@as(u32, t.setup) -% 1)});
+ scl_start_hold.at(port).modify(.{scl_start_hold_time.is(@as(u32, t.hold) -% 1)});
+ scl_stop_hold.at(port).modify(.{scl_stop_hold_time.is(@as(u32, t.hold) -% 1)});
+ to.at(port).modify(.{ time_out_value.is(t.tout), time_out_en.is(1) });
+}
+
+/// Compute and apply the timing for a target SCL frequency. The whole point of the file.
+///
+/// Does **not** commit: call `commitConfig` when the rest of the configuration is in place. That is
+/// ESP-IDF's division too - `_i2c_hal_set_bus_timing` (`i2c_hal.c:27-32`) is calculate-then-write,
+/// and the driver commits separately.
+pub fn setBusTiming(port: u8, source_hz: u32, bus_hz: u32) void {
+ applyTiming(port, Timing.calculate(source_hz, bus_hz));
+}
+
+/// The controller clock divider: register field is the divider *minus one*, with the fractional
+/// numerator and denominator zeroed because ESP-IDF does not use them
+/// (`i2c_ll.h:193-199`, `i2c_ll.h:229-239`).
+pub fn setClockDivider(port: u8, clkm_div: u16) void {
+ std.debug.assert(port < port_count);
+ const num: u32 = @as(u32, clkm_div) -% 1;
+ const guard = clkrst.maskInterrupts();
+ defer guard.release();
+ if (port == 0) {
+ peri_clk_ctrl10.modify(.{
+ i2c0_div_num.is(num),
+ i2c0_div_numerator.is(0),
+ i2c0_div_denominator.is(0),
+ });
+ } else {
+ peri_clk_ctrl11.modify(.{
+ i2c1_div_num.is(num),
+ i2c1_div_numerator.is(0),
+ i2c1_div_denominator.is(0),
+ });
+ }
+}
+
+// The three narrow timing setters, for tuning one condition without recomputing the whole set - a
+// slow slave that needs a longer SDA hold, say.
+//
+// **These do not use the same convention as `applyTiming`, and that is ESP-IDF's inconsistency, not
+// a transcription error.** `i2c_ll_master_set_start_timing` writes `scl_rstart_setup = setup` but
+// `scl_start_hold = hold - 1` (`i2c_ll.h:452-456`); `i2c_ll_master_set_stop_timing` writes both as
+// given (`i2c_ll.h:467-471`); `i2c_ll_set_sda_timing` writes both as given (`i2c_ll.h:482-486`).
+// `i2c_ll_master_set_bus_timing`, meanwhile, subtracts one from all six of those
+// (`i2c_ll.h:210-217`). The reconciliation is that `cal_bus_clk` produces *cycle counts* and these
+// setters take *register values*, with the single exception of `start_hold` - and IDF's own getters
+// agree: `i2c_ll_get_start_timing` adds one back to the hold and not to the setup
+// (`i2c_ll.h:644-648`), while `i2c_ll_get_stop_timing` adds nothing (`i2c_ll.h:659-663`). Anything
+// tidier here would be a different peripheral configuration from the one IDF produces.
+
+pub fn setStartTiming(port: u8, setup: u32, hold: u32) void {
+ std.debug.assert(port < port_count);
+ scl_rstart_setup.at(port).modify(.{scl_rstart_setup_time.is(setup)});
+ scl_start_hold.at(port).modify(.{scl_start_hold_time.is(hold -% 1)});
+}
+
+pub fn setStopTiming(port: u8, setup: u32, hold: u32) void {
+ std.debug.assert(port < port_count);
+ scl_stop_setup.at(port).modify(.{scl_stop_setup_time.is(setup)});
+ scl_stop_hold.at(port).modify(.{scl_stop_hold_time.is(hold)});
+}
+
+pub fn setSdaTiming(port: u8, sample: u32, hold: u32) void {
+ std.debug.assert(port < port_count);
+ sda_hold.at(port).modify(.{sda_hold_time.is(hold)});
+ sda_sample.at(port).modify(.{sda_sample_time.is(sample)});
+}
+
+/// Timeout exponent for a wanted timeout in microseconds -
+/// `i2c_ll_calculate_timeout_us_to_reg_val` (`i2c_ll.h:1060-1065`).
+///
+/// `32 - clz(cycles_per_us * timeout_us)` is `log2` rounded *up*, which is the only sensible
+/// direction for a bus timeout. IDF's own default for the SCL timeout is 2000 us
+/// (`i2c_ll.h:88`).
+pub fn timeoutExponent(source_hz: u32, timeout_us: u32) u32 {
+ const cycles_per_us = source_hz / 1_000_000;
+ return 32 - @clz(cycles_per_us * timeout_us);
+}
+
+/// Set just the timeout exponent, leaving the enable bit alone - `i2c_ll_set_tout`
+/// (`i2c_ll.h:358-361`). The field is five bits: 2^31 source cycles is the longest expressible
+/// timeout, which at 40 MHz is 54 seconds.
+pub fn setTimeout(port: u8, exponent: u32) void {
+ std.debug.assert(port < port_count);
+ to.at(port).modify(.{time_out_value.is(exponent)});
+}
+
+pub fn setTimeoutEnabled(port: u8, on: bool) void {
+ std.debug.assert(port < port_count);
+ to.at(port).modify(.{time_out_en.is(@intFromBool(on))});
+}
+
+/// Glitch filter: pulses shorter than `cycles` source-clock cycles are ignored on both SDA and SCL.
+/// `cycles == 0` disables both filters - `i2c_ll_master_set_filter` (`i2c_ll.h:753-764`).
+///
+/// Note what "disable" means here: the two enable bits default to 1 with thresholds of 0, so the
+/// reset state is "filtering enabled, filtering nothing", and disabling is not the same word as
+/// enabling with a threshold of 0. Passing 0 therefore leaves the thresholds untouched, exactly as
+/// IDF does, rather than zeroing them - a difference the register comparison would catch.
+pub fn setFilter(port: u8, cycles: u4) void {
+ std.debug.assert(port < port_count);
+ const r = filter_cfg.at(port);
+ if (cycles > 0) {
+ r.modify(.{
+ scl_filter_thres.is(cycles),
+ sda_filter_thres.is(cycles),
+ scl_filter_en.is(1),
+ sda_filter_en.is(1),
+ });
+ } else {
+ r.modify(.{ scl_filter_en.is(0), sda_filter_en.is(0) });
+ }
+}
+
+// ----------------------------------------------------------------------------------- bring-up
+
+/// Put a port into master mode with the defaults ESP-IDF's `i2c_hal_master_init` establishes
+/// (`i2c_hal.c:39-50`), in the same order.
+///
+/// The four control bits are one store where IDF does five separate read-modify-writes of the same
+/// register; the resulting word is identical. Each one matters:
+///
+/// * `ms_mode = 1` - master.
+/// * `sda_force_out = scl_force_out = 0` - open drain. The names are inverted:
+/// `i2c_ll_enable_pins_open_drain` writes `!enable_od` (`i2c_ll.h:971-975`), so *zero* is
+/// open-drain and one is push-pull. Push-pull on a shared bus is a short circuit the moment two
+/// devices disagree, so this is the bit that must not be got backwards.
+/// * `arbitration_en = 0` - IDF's master init disables arbitration, which defaults to 1. With a
+/// single master there is nothing to arbitrate, and a false arbitration-lost abort on a noisy
+/// line is worse than none.
+/// * `rx_full_ack_level = 0` - ACK, not NACK, when the RX FIFO hits its threshold.
+/// * `tx_lsb_first = rx_lsb_first = 0` - MSB first, which is what I2C is.
+///
+/// Then both FIFOs are reset, as IDF does, so the block starts with empty FIFOs whatever the
+/// previous user left behind.
+pub fn initMaster(port: u8) void {
+ std.debug.assert(port < port_count);
+ ctr.at(port).modify(.{
+ ms_mode.is(1),
+ sda_force_out.is(0),
+ scl_force_out.is(0),
+ arbitration_en.is(0),
+ rx_full_ack_level.is(0),
+ tx_lsb_first.is(0),
+ rx_lsb_first.is(0),
+ });
+ resetTxFifo(port);
+ resetRxFifo(port);
+}
+
+/// Latch the configuration into the state machine. Write-to-trigger, self-clearing, and required:
+/// see note 2 in this file's header. `i2c_ll_update` (`i2c_ll.h:137-141`).
+pub inline fn commitConfig(port: u8) void {
+ ctr.at(port).modify(.{conf_upgate.is(1)});
+}
+
+/// Reset the master state machine without touching its configuration. Self-clearing in hardware -
+/// IDF writes 1 and never writes 0 (`i2c_ll.h:785-789`, "fsm_rst is a self cleared bit"). For a
+/// master that has hung mid-transaction; the bus itself may still need `clearBus`.
+pub inline fn resetFsm(port: u8) void {
+ ctr.at(port).modify(.{fsm_rst.is(1)});
+}
+
+/// Drive up to `pulses` SCL clocks to free a slave that is holding SDA low, then a STOP -
+/// `i2c_ll_master_clr_bus` (`i2c_ll.h:803-810`). Nine pulses is IDF's default
+/// (`I2C_LL_RESET_SLV_SCL_PULSE_NUM_DEFAULT`, `i2c_ll.h:87`): enough for any slave to finish the
+/// byte it is stuck in and see a NACK.
+///
+/// The enable bit is cleared *by hardware* when the pulses have been sent, so completion is polled
+/// through `isBusClearDone`, and `commitConfig` is needed both to start it and, per IDF's comment,
+/// to resynchronise afterwards. Only meaningful with SCL and SDA actually routed to pads.
+pub fn clearBus(port: u8, pulses: u5) void {
+ std.debug.assert(port < port_count);
+ scl_sp_conf.at(port).modify(.{ scl_rst_slv_num.is(pulses), scl_rst_slv_en.is(1) });
+ commitConfig(port);
+}
+
+pub inline fn isBusClearDone(port: u8) bool {
+ return scl_sp_conf.at(port).get(scl_rst_slv_en) == 0;
+}
+
+/// Open-drain or push-pull SCL and SDA, at the peripheral end.
+///
+/// **The register fields are the inverse of this argument.** `i2c_ll_enable_pins_open_drain` writes
+/// `sda_force_out = scl_force_out = !enable_od` (`i2c_ll.h:971-975`), so a zero in either field is
+/// what makes that line release instead of driving high. `initMaster` already establishes
+/// open-drain; this exists to be able to change it, and to have the polarity checked against IDF's
+/// on its own rather than only as part of a seven-field store.
+///
+/// This is the *peripheral's* driver behaviour. The pad also has an open-drain bit of its own in the
+/// GPIO block (`gpio.setOpenDrain`), and a real bus needs both: the pad hardware must not drive
+/// high, and the peripheral must not ask it to.
+pub fn setPinsOpenDrain(port: u8, open_drain: bool) void {
+ std.debug.assert(port < port_count);
+ const v: u32 = @intFromBool(!open_drain);
+ ctr.at(port).modify(.{ sda_force_out.is(v), scl_force_out.is(v) });
+}
+
+// --------------------------------------------------------------------------------------- FIFOs
+
+/// FIFO or RAM access. FIFO mode is `nonfifo_en = 0`, i.e. the field is the inverse of the name of
+/// this function - `i2c_ll_enable_fifo_mode` (`i2c_ll.h:345-348`).
+pub fn setFifoMode(port: u8, fifo: bool) void {
+ std.debug.assert(port < port_count);
+ fifo_conf.at(port).modify(.{nonfifo_en.is(@intFromBool(!fifo))});
+}
+
+/// Hold the TX FIFO in reset, then release it. Two stores, because the bit is plain R/W and not
+/// self-clearing: writing only the 1 leaves the FIFO permanently reset and every subsequent
+/// transmission silently empty (`i2c_ll.h:248-253`).
+pub fn resetTxFifo(port: u8) void {
+ std.debug.assert(port < port_count);
+ const r = fifo_conf.at(port);
+ r.modify(.{tx_fifo_rst.is(1)});
+ r.modify(.{tx_fifo_rst.is(0)});
+}
+
+pub fn resetRxFifo(port: u8) void {
+ std.debug.assert(port < port_count);
+ const r = fifo_conf.at(port);
+ r.modify(.{rx_fifo_rst.is(1)});
+ r.modify(.{rx_fifo_rst.is(0)});
+}
+
+/// FIFO watermark thresholds, and the two side effects ESP-IDF attaches to setting them.
+///
+/// `fifo_prt_en` gates the watermark interrupts *and* the overflow/underflow protection
+/// (`i2c_reg.h:449-459`), and IDF sets it in both threshold setters
+/// (`i2c_ll.h:496-500` and `i2c_ll.h:510-515`), so it is set here rather than left to the caller.
+///
+/// The other side effect is less obvious and is copied deliberately: IDF's
+/// `i2c_ll_set_rxfifo_full_thr` also writes `ctr.rx_full_ack_level = 0`, in a different register.
+/// That is coherent rather than sloppy - an RX threshold means "ACK up to here", and a master that
+/// NACKed at the threshold would end the transfer instead of pausing it - but it means this
+/// operation touches two registers, and after a peripheral reset (where `rx_full_ack_level` defaults
+/// to 1) leaving it out is an observable difference rather than a stylistic one.
+pub fn setFifoThresholds(port: u8, tx_empty: u5, rx_full: u5) void {
+ std.debug.assert(port < port_count);
+ fifo_conf.at(port).modify(.{
+ fifo_prt_en.is(1),
+ txfifo_wm_thrhd.is(tx_empty),
+ rxfifo_wm_thrhd.is(rx_full),
+ });
+ ctr.at(port).modify(.{rx_full_ack_level.is(0)});
+}
+
+// ------------------------------------------------------------------------------- Data register
+//
+// **Reading `I2C_DATA_REG` pops the RX FIFO.** The register header does not say so - it annotates
+// the single field `I2C_FIFO_RDATA` as `HRO` and describes the register as "Rx FIFO read data"
+// (`i2c_reg.h:464-474`) - but ESP-IDF's LL settles it: `i2c_ll_read_rxfifo` reads *the same address*
+// `len` times into successive bytes of a buffer (`i2c_ll.h:691-697`), which can only produce
+// distinct bytes if each read advances the FIFO. The write direction is the same address for the
+// other FIFO: `i2c_ll_write_txfifo` stores `len` bytes to `hw->data.val` (`i2c_ll.h:674-680`). One
+// address, two FIFOs, both with side effects - the same shape as `UART_FIFO_REG`, and the reason
+// this offset is in the differential harness's `no_read` list.
+
+/// Push bytes into the TX FIFO. In FIFO mode each store is one byte into the FIFO regardless of the
+/// width of the access; the FIFO is `fifo_len` deep and there is no flow control here, so the caller
+/// must not exceed `txSpace`.
+pub fn writeTxFifo(port: u8, bytes: []const u8) void {
+ std.debug.assert(port < port_count);
+ std.debug.assert(bytes.len <= fifo_len);
+ const r = data.at(port);
+ for (bytes) |b| r.writeRaw(b);
+}
+
+/// Pop bytes out of the RX FIFO. Destructive by construction - see above.
+pub fn readRxFifo(port: u8, out: []u8) void {
+ std.debug.assert(port < port_count);
+ const r = data.at(port);
+ for (out) |*b| b.* = @truncate(r.raw());
+}
+
+/// Bytes waiting in the RX FIFO.
+pub inline fn rxCount(port: u8) u32 {
+ return sr.at(port).get(rxfifo_cnt);
+}
+
+/// Bytes queued in the TX FIFO.
+pub inline fn txCount(port: u8) u32 {
+ return sr.at(port).get(txfifo_cnt);
+}
+
+/// Room left in the TX FIFO, saturating at 0 the way `i2c_ll_get_txfifo_len` does
+/// (`i2c_ll.h:604-608`) - the counter can read `fifo_len` and the subtraction must not wrap.
+pub inline fn txSpace(port: u8) u32 {
+ const used = txCount(port);
+ return if (used >= fifo_len) 0 else fifo_len - used;
+}
+
+pub inline fn isBusBusy(port: u8) bool {
+ return sr.at(port).get(bus_busy) == 1;
+}
+
+// -------------------------------------------------------------------------------- command list
+//
+// A transaction is up to eight commands written into I2C_COMD0..7 and then triggered as a unit. The
+// register header exposes each slot as a single 14-bit field `I2C_COMMANDn` plus a `_DONE` bit at 31
+// and stops there: the sub-fields exist only in `i2c_ll_hw_cmd_t` (`i2c_ll.h:41-52`). So this is one
+// of the few places where the field geometry cannot come from a macro pair, and the comptime check
+// below is what keeps that honest - the five sub-fields must tile exactly the bits the header calls
+// I2C_COMMANDn.
+
+const cmd_byte_num = Field.of(0, 0xff);
+const cmd_ack_en = Field.bit(8);
+const cmd_ack_exp = Field.bit(9);
+const cmd_ack_val = Field.bit(10);
+const cmd_op_code = Field.of(11, 0x7);
+const cmd_done = Field.of(regs.I2C_COMMAND0_DONE_S, regs.I2C_COMMAND0_DONE_V);
+
+comptime {
+ const command_field = Field.of(regs.I2C_COMMAND0_S, regs.I2C_COMMAND0_V);
+ const tiled = cmd_byte_num.mask() | cmd_ack_en.mask() | cmd_ack_exp.mask() |
+ cmd_ack_val.mask() | cmd_op_code.mask();
+ if (tiled != command_field.mask()) @compileError(
+ "the command sub-fields from i2c_ll.h do not tile I2C_COMMAND0 - one of the two headers moved",
+ );
+ if (cmd_done.mask() & command_field.mask() != 0) @compileError("command done bit overlaps the command");
+}
+
+/// Opcodes, from `i2c_ll.h:55-59`. **Not** the numbers this chip's own register header describes -
+/// see note 3 in the file header.
+pub const Op = enum(u3) {
+ write = 1,
+ stop = 2,
+ read = 3,
+ /// Hand the command list back to software with the bus still held, so the next chunk can be
+ /// loaded. This is how a transfer longer than eight commands or 32 bytes is done without DMA.
+ end = 4,
+ /// START, and equally a repeated START.
+ restart = 6,
+};
+
+/// One command slot as a value rather than a raw word.
+///
+/// The three ACK fields only mean something for one direction each, which is why they are separate
+/// rather than one "ack" number:
+///
+/// * `ack_check` (WRITE) - compare the ACK bit the slave returns against `ack_expected` and abort
+/// the list if it differs. This is what turns a missing device into a NACK error instead of a
+/// transfer into the void.
+/// * `ack_value` (READ) - the ACK bit this master sends after each byte it reads. Zero (ACK) for
+/// every byte but the last, one (NACK) for the last, which is how a slave is told to stop
+/// driving the bus.
+pub const Command = struct {
+ op: Op,
+ /// Bytes to move. Only WRITE and READ use it; a READ of n bytes is one command, not n.
+ bytes: u8 = 0,
+ ack_check: bool = false,
+ ack_expected: u1 = 0,
+ ack_value: u1 = 0,
+
+ pub inline fn encode(self: Command) u32 {
+ return (@as(u32, self.bytes) << cmd_byte_num.shift) |
+ (@as(u32, @intFromBool(self.ack_check)) << cmd_ack_en.shift) |
+ (@as(u32, self.ack_expected) << cmd_ack_exp.shift) |
+ (@as(u32, self.ack_value) << cmd_ack_val.shift) |
+ (@as(u32, @intFromEnum(self.op)) << cmd_op_code.shift);
+ }
+};
+
+/// One command slot. The slot stride is checked against the header's own COMD1 macro rather than
+/// assumed to be 4.
+inline fn cmdReg(port: u8, slot: u8) Reg {
+ std.debug.assert(slot < cmd_slots);
+ const stride = comptime mmio.addr(regs.I2C_COMD1_REG(0)) - mmio.addr(regs.I2C_COMD0_REG(0));
+ comptime {
+ // ... and the array is contiguous all the way to the last slot.
+ if (mmio.addr(regs.I2C_COMD7_REG(0)) != mmio.addr(regs.I2C_COMD0_REG(0)) + stride * 7)
+ @compileError("the command registers are not a contiguous array of 8");
+ }
+ return Reg.atAddress(comd0.at(port).address + stride * slot);
+}
+
+/// Write a command into a slot. A whole-word store, as IDF's `i2c_ll_master_write_cmd_reg` does
+/// (`i2c_ll.h:437-441`): it is the one register here where establishing the entire word is right,
+/// because the `done` bit must go back to 0 for the slot to be waited on again.
+pub fn writeCommand(port: u8, slot: u8, cmd: Command) void {
+ std.debug.assert(port < port_count);
+ cmdReg(port, slot).writeRaw(cmd.encode());
+}
+
+/// Load a whole command list, in order. Any slot the list does not reach keeps whatever it held -
+/// which is harmless, because the sequencer stops at the STOP or END that the list must contain.
+pub fn writeCommands(port: u8, cmds: []const Command) void {
+ std.debug.assert(cmds.len <= cmd_slots);
+ for (cmds, 0..) |c, i| writeCommand(port, @intCast(i), c);
+}
+
+/// Whether the sequencer has finished a slot. Set by hardware (`R/W/SS`), cleared by writing the
+/// slot again. `i2c_ll_master_is_cmd_done` (`i2c_ll.h:1047-1051`).
+pub inline fn isCommandDone(port: u8, slot: u8) bool {
+ return cmdReg(port, slot).get(cmd_done) == 1;
+}
+
+// --------------------------------------------------------------------------------- transactions
+
+// The master event bits, in I2C_INT_RAW/I2C_INT_ST/I2C_INT_CLR - the same bit numbers in all three
+// (`i2c_ll.h:61-70`). Reading INT_RAW is safe: the bits are `R/SS/WTC`, set by hardware and cleared
+// only by writing a 1 to the same position in INT_CLR, so polling does not consume them. Writing
+// INT_CLR is the one place in this file that must be `writeRaw` rather than `modify`.
+const int_trans_complete = Field.of(regs.I2C_TRANS_COMPLETE_INT_RAW_S, regs.I2C_TRANS_COMPLETE_INT_RAW_V);
+const int_end_detect = Field.of(regs.I2C_END_DETECT_INT_RAW_S, regs.I2C_END_DETECT_INT_RAW_V);
+const int_nack = Field.of(regs.I2C_NACK_INT_RAW_S, regs.I2C_NACK_INT_RAW_V);
+const int_arbitration_lost = Field.of(regs.I2C_ARBITRATION_LOST_INT_RAW_S, regs.I2C_ARBITRATION_LOST_INT_RAW_V);
+const int_time_out = Field.of(regs.I2C_TIME_OUT_INT_RAW_S, regs.I2C_TIME_OUT_INT_RAW_V);
+const int_scl_st_to = Field.of(regs.I2C_SCL_ST_TO_INT_RAW_S, regs.I2C_SCL_ST_TO_INT_RAW_V);
+const int_scl_main_st_to = Field.of(regs.I2C_SCL_MAIN_ST_TO_INT_RAW_S, regs.I2C_SCL_MAIN_ST_TO_INT_RAW_V);
+
+/// The mask ESP-IDF uses for "all interrupts" - `I2C_LL_INTR_MASK`, `i2c_ll.h:1097`.
+///
+/// It is 14 bits, and this block has 19 (`I2C_SLAVE_ADDR_UNMATCH_INT` is bit 18). The five it leaves
+/// out are slave-mode and general-call events, which is presumably why IDF's mask stops where it
+/// does; the value is IDF's rather than a recount so that clearing "everything" means the same thing
+/// on both sides of the differential.
+pub const all_interrupts: u32 = 0x3fff;
+
+/// Clear interrupt flags. Write-1-to-clear, so this is a raw store of a mask and never a
+/// read-modify-write: reading INT_RAW and writing it back would clear whatever had arrived in
+/// between and nothing else.
+pub inline fn clearInterrupts(port: u8, mask: u32) void {
+ int_clr.at(port).writeRaw(mask);
+}
+
+/// Mask every interrupt at the peripheral. This HAL polls; nothing here reaches the CLIC.
+///
+/// A whole-word zero rather than IDF's `int_ena &= ~mask` (`i2c_ll.h:305-309`), so it also covers
+/// the five slave-mode bits outside `all_interrupts`. Reaching the same word from a block whose
+/// `int_ena` reset value is 0 either way, which is why the differential case for it agrees.
+pub inline fn disableInterrupts(port: u8) void {
+ int_ena.at(port).writeRaw(0);
+}
+
+/// How a triggered command list ended.
+pub const Outcome = enum {
+ /// The list ran to its STOP.
+ complete,
+ /// The list hit an END opcode: the bus is still held and the next chunk can be loaded.
+ end_detect,
+ /// A slave did not acknowledge. The usual meaning is "nothing at that address".
+ nack,
+ /// Another master won the bus. Only possible with `arbitration_en` set, which `initMaster`
+ /// clears.
+ arbitration_lost,
+ /// SCL was held low past the configured timeout - `I2C_TO_REG`. Almost always a slave holding
+ /// the clock, or no pull-up on the line at all.
+ timeout,
+ /// The SCL state machine stalled: `scl_st_to` or `scl_main_st_to`. IDF's driver treats this as
+ /// the signal that a bus deadlock may have happened and `clearBus` is worth trying
+ /// (`i2c_ll.h:795`).
+ stalled,
+ /// Nothing had happened yet.
+ pending,
+};
+
+/// Trigger the loaded command list. Write-to-trigger; the bit reads back 0, so this leaves no trace
+/// in a register snapshot. `i2c_ll_start_trans` (`i2c_ll.h:629-633`).
+pub inline fn startTransaction(port: u8) void {
+ ctr.at(port).modify(.{trans_start.is(1)});
+}
+
+/// Read the outcome so far from one load of INT_RAW.
+///
+/// Errors are reported ahead of completion, and in the order they matter: an arbitration loss or a
+/// NACK can be raised in the same word as `trans_complete`, and calling that transaction complete
+/// is how a driver comes to believe a device answered when it did not.
+pub fn outcome(port: u8) Outcome {
+ const raw = int_raw.at(port).raw();
+ if (raw & int_arbitration_lost.mask() != 0) return .arbitration_lost;
+ if (raw & int_nack.mask() != 0) return .nack;
+ if (raw & int_time_out.mask() != 0) return .timeout;
+ if (raw & (int_scl_st_to.mask() | int_scl_main_st_to.mask()) != 0) return .stalled;
+ if (raw & int_trans_complete.mask() != 0) return .complete;
+ if (raw & int_end_detect.mask() != 0) return .end_detect;
+ return .pending;
+}
+
+/// Spin until the transaction resolves. Returns `.pending` if it never does, rather than hanging:
+/// a bus with no pull-up produces exactly that, and it is a fault to report rather than a board to
+/// power-cycle.
+///
+/// `spins` is a loop count, not a time. At the ~90 MHz this board boots at, a 100 kHz transfer of a
+/// few bytes needs on the order of 10^4 iterations of this loop; the default of 200,000 leaves an
+/// order of magnitude of headroom and still returns in well under a second.
+pub fn waitTransaction(port: u8, spins: u32) Outcome {
+ var n: u32 = 0;
+ while (n < spins) : (n += 1) {
+ const o = outcome(port);
+ if (o != .pending) return o;
+ }
+ return .pending;
+}
+
+/// The status register's own error bits, which are not the interrupt flags: `resp_rec` is the last
+/// ACK level *received* and `arb_lost` is the state machine's own latch. Both are read-only and
+/// survive an interrupt clear, so they are what to look at when diagnosing a transfer after the fact.
+pub const Status = struct {
+ /// The ACK bit the slave last returned: 0 = ACK, 1 = NACK.
+ last_ack: u1,
+ arbitration_lost: bool,
+ bus_busy: bool,
+ rx_bytes: u32,
+ tx_bytes: u32,
+};
+
+pub fn status(port: u8) Status {
+ const raw = sr.at(port).raw();
+ return .{
+ .last_ack = @intCast((raw >> resp_rec.shift) & 1),
+ .arbitration_lost = raw & arb_lost.mask() != 0,
+ .bus_busy = raw & bus_busy.mask() != 0,
+ .rx_bytes = (raw >> rxfifo_cnt.shift) & rxfifo_cnt.unshiftedMask(),
+ .tx_bytes = (raw >> txfifo_cnt.shift) & txfifo_cnt.unshiftedMask(),
+ };
+}
+
+// ------------------------------------------------------------------------------------ the pads
+//
+// I2C is a two-wire open-drain bus and the P4 reaches it only through the GPIO matrix: there is no
+// IO MUX function for I2C on any pad, so both signals go out through `matrixOut` and come back in
+// through `matrixIn`. Both directions are needed even for a write-only master - the master samples
+// SDA to read the slave's ACK, and samples SCL to detect stretching - which is why every pad here
+// gets its input buffer enabled as well as its driver.
+
+/// The GPIO matrix signal indices for a port, from ESP-IDF's own signal map
+/// (`gpio_sig_map.h:141-148`) via `i2c_periph.c`. On this chip a signal's input and output index
+/// happen to be the same number, which is not true on every part and is not something to rely on.
+pub fn sclSignal(port: u8) u32 {
+ return switch (port) {
+ 0 => regs.I2C0_SCL_PAD_OUT_IDX,
+ else => regs.I2C1_SCL_PAD_OUT_IDX,
+ };
+}
+
+pub fn sdaSignal(port: u8) u32 {
+ return switch (port) {
+ 0 => regs.I2C0_SDA_PAD_OUT_IDX,
+ else => regs.I2C1_SDA_PAD_OUT_IDX,
+ };
+}
+
+/// Route SCL and SDA to two pads, open-drain, following `i2c_common_set_pins`
+/// (`esp_driver_i2c/i2c_common.c:318-345`) step for step.
+///
+/// **The internal pull-ups are not enough for a real bus.** They are on the order of 45 kOhm, which
+/// with a few tens of picofarads of trace and device capacitance gives a rise time far past the
+/// 1 us that 100 kHz I2C allows. ESP-IDF says the same thing in its own driver documentation and
+/// enables them anyway as a convenience for a single device on a short wire. A bus that is expected
+/// to work needs external resistors - 4.7 kOhm to 3.3 V is the usual choice at 100 kHz, 2.2 kOhm at
+/// 400 kHz - and then `internal_pullups` should be false, because two resistors in parallel is not
+/// what either calculation assumed.
+///
+/// The order matters in one place: the pad is driven high *before* its output is enabled, so
+/// enabling the driver cannot pull the bus low for the few cycles before the peripheral takes over.
+/// A low SCL glitch is a clock edge to every device on the bus.
+pub fn configurePins(port: u8, scl_pin: u8, sda_pin: u8, opts: struct {
+ internal_pullups: bool = false,
+}) void {
+ std.debug.assert(port < port_count);
+ for ([_]struct { pin: u8, signal: u32 }{
+ .{ .pin = scl_pin, .signal = sclSignal(port) },
+ .{ .pin = sda_pin, .signal = sdaSignal(port) },
+ }) |wire| {
+ gpio.setHigh(wire.pin);
+ gpio.setInputEnable(wire.pin, true);
+ gpio.setOpenDrain(wire.pin, true);
+ gpio.setPull(wire.pin, if (opts.internal_pullups) .up else .none);
+ gpio.matrixOut(wire.pin, wire.signal);
+ gpio.matrixIn(wire.pin, wire.signal);
+ }
+}
+
+// ------------------------------------------------------------------------------- transfers
+
+/// Bring a port up as a master on a given bus frequency, in the order the hardware requires:
+/// clocks, then reset, then configuration, then commit.
+///
+/// Reset before configure, because a reset drops everything configured before it. `clkrst.init`
+/// does the gate-then-reset pair; the controller clock is separate and enabled after, since it only
+/// feeds the state machine.
+pub fn init(port: u8, opts: struct {
+ source: Source = .xtal,
+ source_hz: u32 = xtal_hz,
+ bus_hz: u32 = 100_000,
+ /// Glitch filter width in source-clock cycles. ESP-IDF's driver default is 7.
+ filter_cycles: u4 = 7,
+}) void {
+ std.debug.assert(port < port_count);
+ switch (port) {
+ 0 => clkrst.init(.i2c0),
+ else => clkrst.init(.i2c1),
+ }
+ setControllerClockEnabled(port, true);
+ setSource(port, opts.source);
+
+ initMaster(port);
+ setFifoMode(port, true);
+ disableInterrupts(port);
+ clearInterrupts(port, all_interrupts);
+ setBusTiming(port, opts.source_hz, opts.bus_hz);
+ setFilter(port, opts.filter_cycles);
+ commitConfig(port);
+}
+
+/// Default spin budget for `write`/`read`. See `waitTransaction`.
+pub const default_spins: u32 = 200_000;
+
+/// Write `bytes` to a 7-bit address as one command list.
+///
+/// RSTART | WRITE (1 + len bytes, ack checked) | STOP
+///
+/// The address byte goes in the TX FIFO ahead of the data and is counted in the WRITE command's byte
+/// count: to the sequencer the address is just the first byte written after a START. `ack_check` is
+/// on, so a missing device comes back as `.nack` rather than as a successful write into nothing.
+///
+/// One command list, one FIFO load: at most `fifo_len - 1` = 31 data bytes. Longer transfers need
+/// the END-and-continue loop that ESP-IDF's driver runs from its interrupt handler, which is out of
+/// scope here - hence the assert rather than a partial write.
+pub fn write(port: u8, address: u7, bytes: []const u8, spins: u32) Outcome {
+ std.debug.assert(bytes.len < fifo_len);
+ resetTxFifo(port);
+ resetRxFifo(port);
+ clearInterrupts(port, all_interrupts);
+
+ writeTxFifo(port, &[_]u8{@as(u8, address) << 1});
+ writeTxFifo(port, bytes);
+
+ writeCommands(port, &.{
+ .{ .op = .restart },
+ .{ .op = .write, .bytes = @intCast(bytes.len + 1), .ack_check = true },
+ .{ .op = .stop },
+ });
+ commitConfig(port);
+ startTransaction(port);
+ return waitTransaction(port, spins);
+}
+
+/// Read into `out` from a 7-bit address as one command list.
+///
+/// RSTART | WRITE 1 (address|read, ack checked) | READ n-1 sending ACK | READ 1 sending NACK | STOP
+///
+/// The last byte is a separate command because its ACK bit differs: a master that ACKs the final
+/// byte tells the slave to keep going, and the slave then holds SDA for a byte that will never be
+/// clocked out. That is the classic I2C read bug, and it is a *command list* bug - which is why the
+/// split is here rather than being something the caller can get wrong.
+///
+/// Reads of one byte collapse to a single NACKed READ, so the list is four commands instead of five.
+pub fn read(port: u8, address: u7, out: []u8, spins: u32) Outcome {
+ std.debug.assert(out.len > 0);
+ std.debug.assert(out.len <= fifo_len);
+ resetTxFifo(port);
+ resetRxFifo(port);
+ clearInterrupts(port, all_interrupts);
+
+ writeTxFifo(port, &[_]u8{(@as(u8, address) << 1) | 1});
+
+ writeCommand(port, 0, .{ .op = .restart });
+ writeCommand(port, 1, .{ .op = .write, .bytes = 1, .ack_check = true });
+ var slot: u8 = 2;
+ if (out.len > 1) {
+ writeCommand(port, slot, .{ .op = .read, .bytes = @intCast(out.len - 1), .ack_value = 0 });
+ slot += 1;
+ }
+ writeCommand(port, slot, .{ .op = .read, .bytes = 1, .ack_value = 1 });
+ writeCommand(port, slot + 1, .{ .op = .stop });
+
+ commitConfig(port);
+ startTransaction(port);
+ const result = waitTransaction(port, spins);
+ if (result == .complete) readRxFifo(port, out);
+ return result;
+}
+
+test "the timing arithmetic reproduces ESP-IDF's, including where it looks wrong" {
+ // 100 kHz on a 40 MHz XTAL: the case every I2C device supports, worked through by hand from
+ // i2c_ll.h:104-128. clkm_div = 40e6/(100e3*1024) + 1 = 0 + 1 = 1, so sclk stays 40 MHz and
+ // half = 40e6/100e3/2 = 200.
+ const t100 = Timing.calculate(40_000_000, 100_000);
+ try std.testing.expectEqual(@as(u16, 1), t100.clkm_div);
+ try std.testing.expectEqual(@as(u16, 200), t100.scl_low);
+ try std.testing.expectEqual(@as(u16, 98), t100.scl_wait_high); // half/2 - 2
+ try std.testing.expectEqual(@as(u16, 102), t100.scl_high); // half - wait_high
+ try std.testing.expectEqual(@as(u16, 50), t100.sda_hold);
+ try std.testing.expectEqual(@as(u16, 100), t100.sda_sample);
+ try std.testing.expectEqual(@as(u16, 200), t100.setup);
+ try std.testing.expectEqual(@as(u16, 200), t100.hold);
+ // 5*200 = 1000, which needs 10 bits, so 32 - 22 + 2 = 12: a timeout of 2^12 = 4096 cycles,
+ // 102 us at 40 MHz, about ten bit times.
+ try std.testing.expectEqual(@as(u16, 12), t100.tout);
+
+ // 400 kHz: same divider, quarter the half-cycle.
+ const t400 = Timing.calculate(40_000_000, 400_000);
+ try std.testing.expectEqual(@as(u16, 1), t400.clkm_div);
+ try std.testing.expectEqual(@as(u16, 50), t400.scl_low);
+ try std.testing.expectEqual(@as(u16, 23), t400.scl_wait_high);
+ try std.testing.expectEqual(@as(u16, 27), t400.scl_high);
+ try std.testing.expectEqual(@as(u16, 10), t400.tout);
+
+ // 10 kHz: the branch that actually uses the controller-clock divider. 40e6/(10e3*1024) = 3, so
+ // clkm_div = 4, sclk = 10 MHz and half = 500 - just inside the nine-bit period fields, which is
+ // what the divider exists to guarantee.
+ const t10 = Timing.calculate(40_000_000, 10_000);
+ try std.testing.expectEqual(@as(u16, 4), t10.clkm_div);
+ try std.testing.expectEqual(@as(u16, 500), t10.scl_low);
+ // Below 80 kHz the wait-high split changes: half/4 rather than half/2 - 2.
+ try std.testing.expectEqual(@as(u16, 125), t10.scl_wait_high);
+ try std.testing.expectEqual(@as(u16, 375), t10.scl_high);
+
+ // The hardware ordering constraint IDF asserts (i2c_ll.h:126-127) across the whole range.
+ for ([_]u32{ 10_000, 50_000, 100_000, 400_000, 1_000_000 }) |hz| {
+ const t = Timing.calculate(40_000_000, hz);
+ try std.testing.expect(t.scl_wait_high < t.sda_sample);
+ try std.testing.expect(t.sda_sample < t.scl_high);
+ // Every period register is nine bits wide, and scl_low is written minus one.
+ try std.testing.expect(t.scl_low - 1 <= 511);
+ try std.testing.expect(t.scl_wait_high <= 127); // this one is seven
+ try std.testing.expect(t.tout <= 31); // and the timeout exponent is five
+ }
+}
+
+test "the timeout exponent rounds up, and where the five-bit field runs out" {
+ // 2000 us at 40 MHz is 80,000 cycles; 2^17 = 131,072 is the first power of two above it, so
+ // IDF's documented default SCL timeout comes out as 17 - which fits the five-bit field with
+ // room to spare. This test exists because the first version of this file asserted the opposite.
+ try std.testing.expectEqual(@as(u32, 17), timeoutExponent(40_000_000, 2000));
+ try std.testing.expect(timeoutExponent(40_000_000, 2000) <= time_out_value.max());
+ // The field runs out at 2^31 source cycles, 53.7 seconds at 40 MHz - a timeout no I2C bus has a
+ // use for, which is why neither IDF nor this file range-checks it. Past that the exponent is
+ // truncated by the field write rather than rejected, exactly as IDF's bitfield store does.
+ try std.testing.expectEqual(@as(u32, 32), timeoutExponent(40_000_000, 100_000_000));
+ try std.testing.expect(timeoutExponent(40_000_000, 100_000_000) > time_out_value.max());
+}
+
+test "commands encode to the layout i2c_ll_hw_cmd_t describes" {
+ // A WRITE of three bytes with ACK checking: byte_num=3, ack_en=1, op_code=1.
+ try std.testing.expectEqual(
+ @as(u32, 3) | (1 << 8) | (1 << 11),
+ (Command{ .op = .write, .bytes = 3, .ack_check = true }).encode(),
+ );
+ // RESTART is opcode 6 on this chip, not 0 - the number the register header's prose still gives.
+ try std.testing.expectEqual(@as(u32, 6 << 11), (Command{ .op = .restart }).encode());
+ // A final READ NACKs: ack_val=1 at bit 10, opcode 3.
+ try std.testing.expectEqual(
+ @as(u32, 1) | (1 << 10) | (3 << 11),
+ (Command{ .op = .read, .bytes = 1, .ack_value = 1 }).encode(),
+ );
+ // STOP is 2 and READ is 3, which is the pair the ESP32-era numbering had the other way around.
+ try std.testing.expectEqual(@as(u32, 2 << 11), (Command{ .op = .stop }).encode());
+}