//! 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); }