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/oracle/uart_cases.zig | 326 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 326 insertions(+) create mode 100644 src/oracle/uart_cases.zig (limited to 'src/oracle/uart_cases.zig') diff --git a/src/oracle/uart_cases.zig b/src/oracle/uart_cases.zig new file mode 100644 index 0000000..444d235 --- /dev/null +++ b/src/oracle/uart_cases.zig @@ -0,0 +1,326 @@ +//! UART's side of the differential test. +//! +//! **The peripheral under test is UART1, and that is a safety constraint rather than a preference.** +//! UART0 carries this board's console. The harness restores a UART by pulsing its reset bit, and +//! resetting UART0 clears UART_CLKDIV: the console's output turns to garbage mid-character and the +//! board takes a watchdog reset with nothing readable left to explain it. That was measured on this +//! board. UART1 is otherwise idle here, has no pins routed at power-on, and resets cleanly. +//! +//! **Word 0 is on the no-read list.** `UART_FIFO_REG` is at offset 0x000 - the first word any "read +//! the whole block" loop touches - its only field is annotated `RO` in uart_reg.h:18, and that +//! annotation is wrong in the way that matters: the read is the FIFO pop. A generic snapshot of a +//! UART eats received bytes. +//! +//! **What this suite cannot see, stated plainly.** The descriptor is one contiguous window and the +//! UART's is 0xa0 bytes at its own base, so three things this HAL does land outside it: +//! +//! * the integer pre-divider `REG_UART1_SCLK_DIV_NUM` and the source select +//! `REG_UART1_CLK_SRC_SEL`, which are in HP_SYS_CLKRST at a different base; +//! * the GPIO matrix registers the routing cases write, which are in the GPIO block; +//! * the FIFO contents themselves, which have no addressable state to compare. +//! +//! The pre-divider is not unobserved, though, only observed indirectly: `clk_div` is +//! `(sclk_freq << 4) / (baud * sclk_div)`, so the in-window CLKDIV_SYNC word is a function of the +//! pre-divider, and the two sides disagreeing on `sclk_div` shows up as a different CLKDIV unless +//! the two errors cancel exactly. The `baud_300` case exists specifically because it is the one +//! rate here whose pre-divider is not 1. The routing cases are honestly weak in this window - what +//! they compare is that both sides leave the *UART* untouched, and their real evidence is that +//! `gpio_cases`' `matrix_out` case passes against the same GPIO LL functions this file calls. +//! +//! Both sides reach the hardware by different paths throughout: the `idf` half calls ESP-IDF's +//! `uart_ll.h` compiled by clang, the `ours` half calls src/hal/uart.zig. Nothing here reads a +//! value back through the accessor that wrote it, because that proves only that the accessor is +//! self-consistent. + +const std = @import("std"); +const hal = @import("hal"); +const regs = @import("regs"); +const mmio = @import("mmio"); +const types = @import("differ_types.zig"); + +extern fn oracle_uart_set_sclk(num: c_uint, sel: c_uint) void; +/// The source select, named on the C side. `UART_SCLK_XTAL` is a `soc_module_clk_t` enumerator whose +/// numeric value is an accident of a chip-wide enum, so it must not cross this boundary as an +/// integer - passing 0 selects nothing that exists and hangs the next commit. +extern fn oracle_uart_set_sclk_xtal(num: c_uint) void; +extern fn oracle_uart_sclk_enable(num: c_uint) void; +extern fn oracle_uart_enable_bus_clock(num: c_uint, enable: c_int) void; +extern fn oracle_uart_set_baudrate(num: c_uint, baud: c_uint, sclk_freq: c_uint) c_int; +extern fn oracle_uart_set_data_bit_num(num: c_uint, bits: c_uint) void; +extern fn oracle_uart_set_stop_bits(num: c_uint, stop: c_uint) void; +extern fn oracle_uart_set_parity(num: c_uint, parity: c_uint) void; +extern fn oracle_uart_txfifo_rst(num: c_uint) void; +extern fn oracle_uart_rxfifo_rst(num: c_uint) void; +extern fn oracle_uart_set_loop_back(num: c_uint, enable: c_int) void; +extern fn oracle_uart_update(num: c_uint) void; +extern fn oracle_uart_route_tx(num: c_uint, pin: c_uint) void; +extern fn oracle_uart_route_rx(num: c_uint, pin: c_uint) void; + +/// The instance under test. A module-level `var` because Zig has no closures and the harness stores +/// plain `fn` pointers. It is a `var` rather than a constant so a future run can move to UART2-4, +/// but it must never become 0: see this file's header. +pub var port: u8 = 1; + +/// The pad the routing cases use. GPIO33 is a free pin on this board's JP1 header - the same one +/// `gpio_cases` uses for its high-bank tests, and for the same reason. +pub var route_pin: u8 = 33; + +/// The clock source frequency the baud cases assume, matching what `setup` selects. XTAL is 40 MHz +/// on the P4 and is the only source whose frequency is exact, which is what makes an expected +/// divider computable by hand. +const sclk_freq: u32 = 40_000_000; + +fn ours() hal.uart.Uart { + return hal.uart.Uart.init(port); +} + +/// Bring UART1 far enough up that its registers answer and its baud generator runs: APB bus clock, +/// core clock, and a source select. Done through IDF's LL rather than ours, so that a bug in our +/// clock code cannot make the whole suite silently compare two dead blocks - and the harness +/// re-checks the bus clock gate before every case regardless. +fn setup() void { + restore(); +} + +/// Known state: out of reset, bus clock on, core clock on, source selected. Every case starts here. +/// +/// The reset is what makes this a sound restore for a block whose CONF0_SYNC carries two +/// write-to-act FIFO resets and whose offset 0 transmits when written - there is nothing here that +/// could be restored by writing a saved snapshot back. The re-enable is what makes it *usable* +/// afterwards. +fn restore() void { + const guard = hal.clkrst.maskInterrupts(); + const rst = mmio.Reg.at(regs.HP_SYS_CLKRST_HP_RST_EN1_REG); + const bit = @as(u32, 1) << regs.HP_SYS_CLKRST_REG_RST_EN_UART1_APB_S; + rst.writeRaw(rst.raw() | bit); + rst.writeRaw(rst.raw() & ~bit); + guard.release(); + + oracle_uart_enable_bus_clock(port, 1); + oracle_uart_sclk_enable(port); + oracle_uart_set_sclk_xtal(port); +} + +// The clock source is selected through oracle_uart_set_sclk_xtal, which names the enumerator on the +// C side. It used to be an integer constant here, and 0 is not XTAL - see that function's comment. + +pub const suite: types.Suite = .{ + .descriptor = .{ + .name = "uart1", + // UART1's block: DR_REG_UART0_BASE + 1 * 0x1000 (soc.h:20). + .base = @intCast(regs.DR_REG_UART0_BASE + 0x1000), + // 40 words, 0x000 through 0x09c. The last register in the block is UART_ID at +0x9c + // (uart_reg.h:1568) and the commit bit UART_REG_UPDATE is at +0x98 - a window that stopped + // at UART_CLK_CONF (+0x88) would be blind to whether the commit even happened, which is the + // single most likely difference against IDF on this peripheral. + .words = 40, + // The read that is a write. See the header. + .no_read = &.{0x00 / 4}, + .volatile_words = &.{ + 0x04 / 4, // UART_INT_RAW - write-1-to-clear, and TXFIFO_EMPTY_INT_RAW moves on its own + 0x08 / 4, // UART_INT_ST - read-only view of the above + 0x1c / 4, // UART_STATUS - live FIFO counts, and the RXD/CTS/DSR pad levels + 0x68 / 4, // UART_MEM_TX_STATUS - FIFO read/write pointers + 0x6c / 4, // UART_MEM_RX_STATUS + 0x70 / 4, // UART_FSM_STATUS - the transmitter's state machine + 0x74 / 4, // UART_POSPULSE - autobaud edge counters, which count whatever the pad does + 0x78 / 4, // UART_NEGPULSE + 0x7c / 4, // UART_LOWPULSE + 0x80 / 4, // UART_HIGHPULSE + 0x84 / 4, // UART_RXD_CNT + 0x90 / 4, // UART_AFIFO_STATUS - the async FIFO's empty/full flags + 0x98 / 4, // UART_REG_UPDATE - self-clearing; reads 0 once the commit lands, but is + // 1 for a few core-clock cycles and a snapshot can catch it + }, + // The APB gate that must read 1 for a snapshot of this block to mean anything. A gated UART + // does not read as zeros, it reads as the last value latched, so two meaningless snapshots + // can compare equal. Pairing from uart_ll.h:257-259, which reads UART1's APB enable out of + // HP_SYS_CLKRST.soc_clk_ctrl2. + .clock = .{ + .reg = @intCast(regs.HP_SYS_CLKRST_SOC_CLK_CTRL2_REG), + .bit = regs.HP_SYS_CLKRST_REG_UART1_APB_CLK_EN_S, + }, + // Reset is the only sound restore for this block: CONF0_SYNC's two FIFO-reset bits and + // REG_UPDATE are write-to-act, and writing a saved word back to offset 0x000 would transmit + // a character. Pairing from uart_ll.h:340-342. Safe here only because this is UART1; + // the same line for UART0 kills the console. + // Reset, and then put the clocking back - which is why this is `.configure` and not + // `.reset_bit`. The harness's reset path does only the pulse, and a UART reset clears the + // core-clock enable and the source select along with everything else. IDF's + // `uart_ll_update` then spins forever waiting for a REG_UPDATE commit that a clockless + // peripheral will never acknowledge: the harness reached the first UART case and stopped, + // with the console silent, looking exactly like a crash. + .restore = .{ .configure = restore }, + }, + .cases = &.{ + // --- baud rate. Four rates spanning the interesting parts of the arithmetic: two ordinary + // ones where the pre-divider is 1, one low enough to need a pre-divider of 33, and one fast + // enough that the integer part gets small and the fraction carries most of the accuracy. + .{ .name = "baudrate", .arg = 115200, .idf = idfBaud115200, .ours = ourBaud115200 }, + .{ .name = "baudrate", .arg = 9600, .idf = idfBaud9600, .ours = ourBaud9600 }, + .{ .name = "baudrate_needs_predivider", .arg = 300, .idf = idfBaud300, .ours = ourBaud300 }, + .{ .name = "baudrate", .arg = 1000000, .idf = idfBaud1M, .ours = ourBaud1M }, + + // --- data format. Each of these is one CONF0_SYNC field plus a commit. + .{ .name = "word_length", .arg = 8, .idf = idfBits8, .ours = ourBits8 }, + .{ .name = "word_length", .arg = 5, .idf = idfBits5, .ours = ourBits5 }, + .{ .name = "stop_bits", .arg = 2, .idf = idfStop2, .ours = ourStop2 }, + .{ .name = "stop_bits_1_5", .arg = 15, .idf = idfStop15, .ours = ourStop15 }, + .{ .name = "parity_odd", .arg = 3, .idf = idfParityOdd, .ours = ourParityOdd }, + .{ .name = "parity_even", .arg = 2, .idf = idfParityEven, .ours = ourParityEven }, + // The asymmetric one: IDF leaves the odd/even bit alone when disabling parity, because 0 + // carries no odd/even information (uart_ll.h:819-822). Setting odd and then disabling is + // the sequence that makes the difference visible, so the case does both. + .{ .name = "parity_odd_then_disable", .idf = idfParityOddThenOff, .ours = ourParityOddThenOff }, + + // --- loopback. Worth a case of its own beyond being one more CONF0_SYNC bit: it is the only + // way to move a byte through this UART with nothing wired to the board. + .{ .name = "loopback_on", .arg = 1, .idf = idfLoopOn, .ours = ourLoopOn }, + .{ .name = "loopback_off", .arg = 0, .idf = idfLoopOff, .ours = ourLoopOff }, + + // --- FIFO resets. These are sequences, not field writes: assert, commit, deassert, commit, + // four stores where a state comparison alone would accept one. Getting the commits wrong + // leaves the register reading exactly as asked and the FIFO not reset. + .{ .name = "txfifo_rst", .idf = idfTxFifoRst, .ours = ourTxFifoRst }, + .{ .name = "rxfifo_rst", .idf = idfRxFifoRst, .ours = ourRxFifoRst }, + + // --- the bare commit, as its own case. If this one differs, every case above is suspect. + .{ .name = "update", .idf = idfUpdate, .ours = ourUpdate }, + + // --- pin routing. Window-blind by construction: the effect is in the GPIO block, so what + // these compare is that neither side disturbs the UART while routing. Kept because a + // routing call that accidentally wrote a UART register would be caught by nothing else, and + // because the pair documents which signal index each side uses. + .{ .name = "route_tx", .arg = 33, .idf = idfRouteTx, .ours = ourRouteTx }, + .{ .name = "route_rx", .arg = 33, .idf = idfRouteRx, .ours = ourRouteRx }, + }, + .setup = setup, +}; + +// ------------------------------------------------------------------------------------- baud rate + +fn idfBaud115200() void { + _ = oracle_uart_set_baudrate(port, 115200, sclk_freq); +} +fn ourBaud115200() void { + _ = ours().setBaudrate(115200, sclk_freq); +} +fn idfBaud9600() void { + _ = oracle_uart_set_baudrate(port, 9600, sclk_freq); +} +fn ourBaud9600() void { + _ = ours().setBaudrate(9600, sclk_freq); +} +fn idfBaud300() void { + _ = oracle_uart_set_baudrate(port, 300, sclk_freq); +} +fn ourBaud300() void { + _ = ours().setBaudrate(300, sclk_freq); +} +fn idfBaud1M() void { + _ = oracle_uart_set_baudrate(port, 1_000_000, sclk_freq); +} +fn ourBaud1M() void { + _ = ours().setBaudrate(1_000_000, sclk_freq); +} + +// ----------------------------------------------------------------------------------- data format +// The numeric arguments to IDF's side are its own enum values from uart_types.h: word length is +// (bits - 5), stop bits are 1/2/3 for 1/1.5/2, parity is 0/2/3 for disable/even/odd. + +fn idfBits8() void { + oracle_uart_set_data_bit_num(port, 3); +} +fn ourBits8() void { + ours().setWordLength(.bits8); +} +fn idfBits5() void { + oracle_uart_set_data_bit_num(port, 0); +} +fn ourBits5() void { + ours().setWordLength(.bits5); +} +fn idfStop2() void { + oracle_uart_set_stop_bits(port, 3); +} +fn ourStop2() void { + ours().setStopBits(.two); +} +fn idfStop15() void { + oracle_uart_set_stop_bits(port, 2); +} +fn ourStop15() void { + ours().setStopBits(.one_and_half); +} +fn idfParityOdd() void { + oracle_uart_set_parity(port, 3); +} +fn ourParityOdd() void { + ours().setParity(.odd); +} +fn idfParityEven() void { + oracle_uart_set_parity(port, 2); +} +fn ourParityEven() void { + ours().setParity(.even); +} +fn idfParityOddThenOff() void { + oracle_uart_set_parity(port, 3); + oracle_uart_set_parity(port, 0); +} +fn ourParityOddThenOff() void { + const u = ours(); + u.setParity(.odd); + u.setParity(.disable); +} + +// -------------------------------------------------------------------------------------- loopback + +fn idfLoopOn() void { + oracle_uart_set_loop_back(port, 1); +} +fn ourLoopOn() void { + ours().setLoopback(true); +} +fn idfLoopOff() void { + oracle_uart_set_loop_back(port, 0); +} +fn ourLoopOff() void { + ours().setLoopback(false); +} + +// ------------------------------------------------------------------------------ FIFO and commit + +fn idfTxFifoRst() void { + oracle_uart_txfifo_rst(port); +} +fn ourTxFifoRst() void { + ours().resetTxFifo(); +} +fn idfRxFifoRst() void { + oracle_uart_rxfifo_rst(port); +} +fn ourRxFifoRst() void { + ours().resetRxFifo(); +} +fn idfUpdate() void { + oracle_uart_update(port); +} +fn ourUpdate() void { + _ = ours().update(); +} + +// ----------------------------------------------------------------------------------- pin routing + +fn idfRouteTx() void { + oracle_uart_route_tx(port, route_pin); +} +fn ourRouteTx() void { + ours().routeTx(route_pin); +} +fn idfRouteRx() void { + oracle_uart_route_rx(port, route_pin); +} +fn ourRouteRx() void { + ours().routeRx(route_pin); +} -- cgit v1.3