summaryrefslogtreecommitdiff
path: root/src/oracle/i2c_ref.c
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-25 12:40:53 -0300
committerGabriel Schneider <[email protected]>2026-08-25 12:46:51 -0300
commitf5f8068fac59b4f16046c2022c2fc7c7e447ef4c (patch)
tree2731a3ed4e51cae09e184e25778eded5fc37d1f5 /src/oracle/i2c_ref.c
downloadesp32p4-f5f8068fac59b4f16046c2022c2fc7c7e447ef4c.tar.gz
esp32p4-f5f8068fac59b4f16046c2022c2fc7c7e447ef4c.zip
zig-p4: pure-Zig ESP32-P4 toolchain
build.zig generates the linker script and drives Zig's own LLD; tools/image.zig turns the ELF into a flashable image and tools/{rom,serial}.zig speak the mask ROM loader over the UART. No CMake, ninja, idf.py, esptool, or external linker. src/soc.zig is a comptime register model over ESP-IDF's own *_reg.h headers; src/hal/ adds peripheral sequences; src/io/ implements std.Io for the chip; src/oracle/ diffs this HAL against ESP-IDF's on the die.
Diffstat (limited to 'src/oracle/i2c_ref.c')
-rw-r--r--src/oracle/i2c_ref.c262
1 files changed, 262 insertions, 0 deletions
diff --git a/src/oracle/i2c_ref.c b/src/oracle/i2c_ref.c
new file mode 100644
index 0000000..b569260
--- /dev/null
+++ b/src/oracle/i2c_ref.c
@@ -0,0 +1,262 @@
+/* I2C's reference implementation, which is ESP-IDF's own.
+ *
+ * These wrappers exist only to give IDF's `static inline` LL functions external linkage so Zig can
+ * call them. There is no logic here - anything clever in this file would be a third implementation
+ * to doubt - with one deliberate exception, `opcode_of`, explained where it appears.
+ */
+
+/* IDF's clock and reset LL functions are shadowed by a wrapper macro that references
+ * `__DECLARE_RCC_ATOMIC_ENV`, an identifier IDF never defines anywhere; its purpose is to make an
+ * unguarded call fail to compile, because the only legal caller holds a spinlock. There is no
+ * FreeRTOS here and core 1 is held in reset at power-on, so declaring the name is exactly as safe as
+ * the spinlock would be - and it is what IDF's own bootloader does
+ * (bootloader_support/src/bootloader_console.c:53 declares a dummy local for the same reason).
+ *
+ * For I2C this covers four functions: i2c_ll_enable_bus_clock, i2c_ll_reset_register,
+ * i2c_ll_set_source_clk and the LP_I2C ones this file does not use. */
+static int __DECLARE_RCC_ATOMIC_ENV __attribute__((unused));
+
+#include "hal/i2c_ll.h"
+
+/* I2C0 and I2C1 are addresses PROVIDEd by soc/esp32p4/ld/esp32p4.peripherals.ld (lines 17-18:
+ * 0x500C4000 and 0x500C5000), which the build links when -Doracle is passed. Several LL functions
+ * dispatch on the *pointer* - i2c_ll_master_set_bus_timing compares `hw == &I2C0` to decide which
+ * HP_SYS_CLKRST divider to write - so passing the right one of these two is load-bearing, and a
+ * third port cannot be faked. */
+static i2c_dev_t *dev(int port)
+{
+ return (port == 0) ? &I2C0 : &I2C1;
+}
+
+/* ------------------------------------------------------------------ clocks, reset, bring-up */
+
+void oracle_i2c_enable_bus_clock(int port, int enable)
+{
+ i2c_ll_enable_bus_clock(port, enable != 0);
+}
+
+void oracle_i2c_reset_register(int port)
+{
+ i2c_ll_reset_register(port);
+}
+
+void oracle_i2c_enable_controller_clock(int port, int enable)
+{
+ i2c_ll_enable_controller_clock(dev(port), enable != 0);
+}
+
+/* src: 0 = XTAL, 1 = RC_FAST. Passed as IDF's own enum values rather than as the register bit, so a
+ * wrong bit polarity in the Zig would show up as a difference. */
+void oracle_i2c_set_source_clk(int port, int src)
+{
+ i2c_ll_set_source_clk(dev(port), (src == 1) ? I2C_CLK_SRC_RC_FAST : I2C_CLK_SRC_XTAL);
+}
+
+/* i2c_hal_master_init, i2c_hal.c:39-50, inlined here because i2c_hal.c is not linked into this
+ * image - only the LL headers are. The sequence is IDF's, unchanged. */
+void oracle_i2c_master_init(int port)
+{
+ i2c_dev_t *hw = dev(port);
+ i2c_ll_set_mode(hw, I2C_BUS_MODE_MASTER);
+ i2c_ll_enable_pins_open_drain(hw, true);
+ i2c_ll_enable_arbitration(hw, false);
+ i2c_ll_master_rx_full_ack_level(hw, false);
+ i2c_ll_set_data_mode(hw, I2C_DATA_MODE_MSB_FIRST, I2C_DATA_MODE_MSB_FIRST);
+ i2c_ll_txfifo_rst(hw);
+ i2c_ll_rxfifo_rst(hw);
+}
+
+void oracle_i2c_set_mode_master(int port)
+{
+ i2c_ll_set_mode(dev(port), I2C_BUS_MODE_MASTER);
+}
+
+void oracle_i2c_enable_pins_open_drain(int port, int enable_od)
+{
+ i2c_ll_enable_pins_open_drain(dev(port), enable_od != 0);
+}
+
+void oracle_i2c_update(int port)
+{
+ i2c_ll_update(dev(port));
+}
+
+void oracle_i2c_fsm_rst(int port)
+{
+ i2c_ll_master_fsm_rst(dev(port));
+}
+
+/* ------------------------------------------------------------------------------- bus timing */
+
+/* _i2c_hal_set_bus_timing, i2c_hal.c:27-32: calculate then write. The calculation
+ * (i2c_ll_master_cal_bus_clk, i2c_ll.h:104-128) is the part this project reimplements in Zig, and
+ * this is the only honest way to compare it - the computed struct never leaves the register file, so
+ * the comparison has to be of the registers it produced. */
+void oracle_i2c_set_bus_timing(int port, unsigned source_hz, unsigned bus_hz)
+{
+ i2c_hal_clk_config_t clk_cal = {0};
+ i2c_ll_master_cal_bus_clk(source_hz, bus_hz, &clk_cal);
+ i2c_ll_master_set_bus_timing(dev(port), &clk_cal);
+}
+
+/* The three timing setters that take explicit periods, which is where IDF's minus-one convention is
+ * least uniform: start_setup is written as given while start_hold is written minus one
+ * (i2c_ll.h:452-456), both stop values are written as given (i2c_ll.h:467-471), and so are both sda
+ * values (i2c_ll.h:482-486). None of that is derivable from the register headers. */
+void oracle_i2c_set_start_timing(int port, int setup, int hold)
+{
+ i2c_ll_master_set_start_timing(dev(port), setup, hold);
+}
+
+void oracle_i2c_set_stop_timing(int port, int setup, int hold)
+{
+ i2c_ll_master_set_stop_timing(dev(port), setup, hold);
+}
+
+void oracle_i2c_set_sda_timing(int port, int sample, int hold)
+{
+ i2c_ll_set_sda_timing(dev(port), sample, hold);
+}
+
+void oracle_i2c_set_tout(int port, int tout)
+{
+ i2c_ll_set_tout(dev(port), tout);
+}
+
+/* i2c_hal_master_set_scl_timeout_val, i2c_hal.c:66-70. */
+void oracle_i2c_set_scl_timeout_us(int port, unsigned source_hz, unsigned timeout_us)
+{
+ uint32_t reg_val = i2c_ll_calculate_timeout_us_to_reg_val(source_hz, timeout_us);
+ i2c_ll_set_tout(dev(port), reg_val);
+}
+
+void oracle_i2c_set_filter(int port, unsigned filter_num)
+{
+ i2c_ll_master_set_filter(dev(port), (uint8_t)filter_num);
+}
+
+/* --------------------------------------------------------------------------------- the FIFOs */
+
+void oracle_i2c_txfifo_rst(int port)
+{
+ i2c_ll_txfifo_rst(dev(port));
+}
+
+void oracle_i2c_rxfifo_rst(int port)
+{
+ i2c_ll_rxfifo_rst(dev(port));
+}
+
+void oracle_i2c_enable_fifo_mode(int port, int fifo_mode_en)
+{
+ i2c_ll_enable_fifo_mode(dev(port), fifo_mode_en != 0);
+}
+
+void oracle_i2c_set_fifo_thresholds(int port, unsigned tx_empty, unsigned rx_full)
+{
+ i2c_ll_set_txfifo_empty_thr(dev(port), (uint8_t)tx_empty);
+ i2c_ll_set_rxfifo_full_thr(dev(port), (uint8_t)rx_full);
+}
+
+/* A pattern rather than a caller-supplied buffer: the point is that both implementations push the
+ * same bytes through the same FIFO port, and a fixed generator makes the two sides impossible to
+ * accidentally disagree about. 0xA0 + i is chosen so every byte differs from its neighbours and from
+ * 0x00/0xFF, which are the values a broken FIFO produces. */
+void oracle_i2c_write_txfifo_pattern(int port, unsigned len)
+{
+ uint8_t buf[32];
+ if (len > sizeof(buf)) {
+ len = sizeof(buf);
+ }
+ for (unsigned i = 0; i < len; i++) {
+ buf[i] = (uint8_t)(0xA0 + i);
+ }
+ i2c_ll_write_txfifo(dev(port), buf, (uint8_t)len);
+}
+
+/* ------------------------------------------------------------------------- the command list */
+
+/* The one piece of logic in this file, and it is here on purpose: it maps a command *kind* to
+ * ESP-IDF's own `I2C_LL_CMD_*` macro, so the opcode number crosses the boundary as a name rather
+ * than as an integer. If the Zig side had the numbers wrong - and this chip's register header
+ * documents the pre-ESP32-C3 numbering, so getting them wrong is easy - passing the raw number
+ * through would make both sides agree on the same mistake and the differential would prove nothing.
+ *
+ * Kinds: 0 restart, 1 write, 2 read, 3 stop, 4 end. */
+static uint32_t opcode_of(unsigned kind)
+{
+ switch (kind) {
+ case 0: return I2C_LL_CMD_RESTART;
+ case 1: return I2C_LL_CMD_WRITE;
+ case 2: return I2C_LL_CMD_READ;
+ case 3: return I2C_LL_CMD_STOP;
+ default: return I2C_LL_CMD_END;
+ }
+}
+
+void oracle_i2c_write_cmd(int port, int slot, unsigned kind, unsigned byte_num,
+ int ack_en, int ack_exp, int ack_val)
+{
+ i2c_ll_hw_cmd_t cmd = {
+ .byte_num = byte_num,
+ .ack_en = (ack_en != 0),
+ .ack_exp = (ack_exp != 0),
+ .ack_val = (ack_val != 0),
+ .op_code = opcode_of(kind),
+ };
+ i2c_ll_master_write_cmd_reg(dev(port), cmd, slot);
+}
+
+/* ---------------------------------------------------------------------------- interrupt state */
+
+void oracle_i2c_clear_intr_mask(int port, unsigned mask)
+{
+ i2c_ll_clear_intr_mask(dev(port), mask);
+}
+
+void oracle_i2c_disable_intr_mask(int port, unsigned mask)
+{
+ i2c_ll_disable_intr_mask(dev(port), mask);
+}
+
+/* ------------------------------------------------------------------------------ observations */
+
+/* Not part of any comparison - these exist so the harness can print what the reference thinks the
+ * hardware says, next to what ours says, when a case fails. */
+unsigned oracle_i2c_get_hw_version(int port)
+{
+ return i2c_ll_get_hw_version(dev(port));
+}
+
+unsigned oracle_i2c_get_txfifo_len(int port)
+{
+ uint32_t len = 0;
+ i2c_ll_get_txfifo_len(dev(port), &len);
+ return len;
+}
+
+unsigned oracle_i2c_get_rxfifo_cnt(int port)
+{
+ uint32_t len = 0;
+ i2c_ll_get_rxfifo_cnt(dev(port), &len);
+ return len;
+}
+
+int oracle_i2c_get_tout(int port)
+{
+ int tout = 0;
+ i2c_ll_get_tout(dev(port), &tout);
+ return tout;
+}
+
+/* The chip's command-slot count as ESP-IDF's own header states it, so the Zig constant is checked
+ * against IDF rather than against a reading of IDF. */
+unsigned oracle_i2c_cmd_reg_num(void)
+{
+ return I2C_LL_CMD_REG_NUM;
+}
+
+unsigned oracle_i2c_fifo_len(void)
+{
+ return I2C_LL_FIFO_LEN;
+}