From f5f8068fac59b4f16046c2022c2fc7c7e447ef4c Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 25 Aug 2026 12:40:53 -0300 Subject: zig-p4: pure-Zig ESP32-P4 toolchain build.zig generates the linker script and drives Zig's own LLD; tools/image.zig turns the ELF into a flashable image and tools/{rom,serial}.zig speak the mask ROM loader over the UART. No CMake, ninja, idf.py, esptool, or external linker. src/soc.zig is a comptime register model over ESP-IDF's own *_reg.h headers; src/hal/ adds peripheral sequences; src/io/ implements std.Io for the chip; src/oracle/ diffs this HAL against ESP-IDF's on the die. --- src/hal/i2c.zig | 1091 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1091 insertions(+) create mode 100644 src/hal/i2c.zig (limited to 'src/hal/i2c.zig') 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()); +} -- cgit v1.3