summaryrefslogtreecommitdiff
path: root/src/oracle/uart_cases.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/oracle/uart_cases.zig')
-rw-r--r--src/oracle/uart_cases.zig326
1 files changed, 326 insertions, 0 deletions
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);
+}