1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
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);
}
|