diff options
Diffstat (limited to 'src/oracle/i2c_ref.c')
| -rw-r--r-- | src/oracle/i2c_ref.c | 262 |
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; +} |
