summaryrefslogtreecommitdiff
path: root/examples/uartperf.zig
diff options
context:
space:
mode:
Diffstat (limited to 'examples/uartperf.zig')
-rw-r--r--examples/uartperf.zig229
1 files changed, 229 insertions, 0 deletions
diff --git a/examples/uartperf.zig b/examples/uartperf.zig
new file mode 100644
index 0000000..339f642
--- /dev/null
+++ b/examples/uartperf.zig
@@ -0,0 +1,229 @@
+//! The board half of the link measurement: answer `tools/perfproto.zig` frames over UART0.
+//!
+//! This is the CEILING the editor is measured against. `p4-bench` against this firmware says what
+//! the wire and the UART driver can do with nothing else running; `p4-bench` against the editor says
+//! how much of that the editor manages to use. Optimising the editor without the first number is
+//! guessing, because at 115200 baud a good deal of what feels slow is simply the wire, and no amount
+//! of firmware work moves it.
+//!
+//! Three things it deliberately does NOT do, each of which would corrupt the number:
+//!
+//! * **No `soc.rom.print`.** The mask ROM's `ets_printf` formats and then pushes one byte at a
+//! time, spinning on the FIFO for each - the exact cost this is trying to measure around. Every
+//! byte here goes through the same batched FIFO path `src/pardes/uart.zig` uses.
+//! * **No UART reconfiguration.** Not the divider, not the format, not `reset()`. The
+//! second-stage bootloader configured this block; `hal/uart.zig:195-211` records that resetting
+//! it returns UART_CLKDIV to its power-on value and takes the session with it.
+//! * **No allocation.** One parse buffer and one send buffer, both static, both sized by the
+//! protocol's own `max_payload`. A measurement that shared a heap with anything would measure
+//! the heap.
+//!
+//! The verification is the point. `sink` accumulates a CRC across every payload byte received and
+//! `report` hands it back, so the host can prove that what arrived is what it sent - at this baud a
+//! silent RX overrun is the failure mode that matters, and a byte count alone cannot see it.
+
+const std = @import("std");
+const hal = @import("hal");
+const proto = @import("perfproto");
+
+const uart0 = hal.uart.Uart.init(0);
+
+/// Room for one whole frame. The protocol caps a payload at 1024 precisely so this can be static.
+var rx: [proto.header_len + proto.max_payload]u8 = undefined;
+var rx_len: usize = 0;
+
+var tx: [proto.header_len + proto.max_payload]u8 = undefined;
+
+/// The running `sink` accumulators, reported and reset by `report`.
+var sunk_bytes: u32 = 0;
+var sunk_crc: std.hash.Crc32 = undefined;
+var bad_frames: u32 = 0;
+var tx_dropped: u32 = 0;
+
+/// Push bytes through the TX FIFO, reading the status once per burst rather than once per byte.
+///
+/// The spin is bounded because an unbounded one is indistinguishable from a hang on a board with no
+/// debugger, and because this program's whole purpose is to report numbers: a wedged transmitter
+/// that increments a counter can still be diagnosed, while one that spins forever cannot.
+fn write(bytes: []const u8) void {
+ var rest = bytes;
+ while (rest.len > 0) {
+ var room = uart0.txFree();
+ var spins: u32 = 0;
+ while (room == 0) {
+ spins += 1;
+ if (spins > 1_000_000) {
+ tx_dropped +%= @intCast(rest.len);
+ return;
+ }
+ room = uart0.txFree();
+ }
+ const n = @min(room, rest.len);
+ for (rest[0..n]) |b| uart0.pushByte(b);
+ rest = rest[n..];
+ }
+}
+
+fn send(op: proto.Op, payload: []const u8) void {
+ write(proto.encode(&tx, op, payload));
+}
+
+fn sendStat() void {
+ var buf: [proto.Stat.encoded_len]u8 = undefined;
+ const s: proto.Stat = .{
+ .bytes = sunk_bytes,
+ .crc = sunk_crc.final(),
+ .bad_frames = bad_frames,
+ .tx_dropped = tx_dropped,
+ };
+ s.encode(&buf);
+ send(.stat, &buf);
+ sunk_bytes = 0;
+ sunk_crc = .init();
+ bad_frames = 0;
+}
+
+/// Stream `n` pattern bytes back as `data` frames, then a `stat` whose CRC covers all of them.
+///
+/// Filled a frame at a time from the shared generator rather than from a table: the host computes
+/// the same sequence from the same function, so a disagreement is a real transport fault and not two
+/// copies of a constant drifting apart.
+fn source(n: u32) void {
+ var chunk: [proto.max_payload]u8 = undefined;
+ var sent: u32 = 0;
+ var hash: std.hash.Crc32 = .init();
+ while (sent < n) {
+ const take: u32 = @min(@as(u32, proto.max_payload), n - sent);
+ proto.fillPattern(chunk[0..take], sent);
+ hash.update(chunk[0..take]);
+ send(.data, chunk[0..take]);
+ sent += take;
+ }
+ var buf: [proto.Stat.encoded_len]u8 = undefined;
+ const s: proto.Stat = .{
+ .bytes = sent,
+ .crc = hash.final(),
+ .bad_frames = bad_frames,
+ .tx_dropped = tx_dropped,
+ };
+ s.encode(&buf);
+ send(.stat, &buf);
+}
+
+/// Consume one complete frame from the head of `rx`. Returns the bytes consumed, or 0 when the
+/// frame is not all here yet.
+fn step() usize {
+ const header = proto.parseHeader(rx[0..rx_len]) catch {
+ // Lost sync. Drop ONE byte and let the next call try again from there: the magic is two
+ // bytes, so resynchronising by scanning is the only correct recovery, and dropping the whole
+ // buffer would discard a good frame that happened to follow a corrupt one.
+ return 1;
+ } orelse return 0;
+
+ const total = proto.header_len + @as(usize, header.len);
+ if (rx_len < total) return 0;
+ const payload = rx[proto.header_len..total];
+
+ if (proto.crc(payload) != header.crc) {
+ // Corruption, not loss: the length was plausible and the bytes were not. Counted and
+ // discarded, because acting on it would put the wrong answer in the host's hands.
+ bad_frames +%= 1;
+ return total;
+ }
+
+ switch (header.op) {
+ .ping => send(.pong, payload),
+ .sink => {
+ sunk_bytes +%= header.len;
+ sunk_crc.update(payload);
+ },
+ .report => sendStat(),
+ .source => {
+ const n = if (header.len >= 4) std.mem.readInt(u32, payload[0..4], .little) else 0;
+ source(n);
+ },
+ // Replies are ours to send, never to receive. A reply arriving here means the host is
+ // confused or the wire is looping back; count it rather than answering it.
+ .pong, .stat, .data => bad_frames +%= 1,
+ }
+ return total;
+}
+
+export fn zig_main() noreturn {
+ // FIRST, before any `.rodata` is touched - and the marker below IS `.rodata`. Without this the
+ // bootloader's stale cache lines make that string read as machine code, the board emits noise,
+ // and it looks exactly like a firmware that never started. Measured here before the call was
+ // added: `\xefc\xff\xff\xd5\xb7...` instead of the marker.
+ @import("soc").flushFlashCache();
+
+ // The bootloader arms the RTC watchdog and expects the application to take it over. Nothing in
+ // this repo ever did, so every image here was being reset on a ten-second cycle - invisible to
+ // a program that prints once and spins, and fatal to one that must answer for a minute. It is
+ // why this responder booted, printed its marker, and then went silent: `rst:0x10
+ // (CHIP_LP_WDT_RESET)` in the next boot log, measured.
+ _ = hal.rwdt.disable();
+ sunk_crc = .init();
+
+ // Announce readiness in plain text rather than as a frame: the host watches for this during
+ // reset, while the bootloader's own chatter is still arriving and no frame parser is in sync.
+ write("\r\nMARK UARTPERF_READY\r\n");
+
+ while (true) {
+ // Fill from the FIFO first and always, so the RX FIFO is never left to overflow while this
+ // loop is busy elsewhere. 128 bytes at 115200 is 107 ms of slack and a `source` burst can
+ // hold the transmitter far longer than that, which is exactly the hazard being measured on
+ // the editor - so the measuring instrument must not have it.
+ if (rx_len < rx.len) {
+ const room = rx.len - rx_len;
+ var got: usize = 0;
+ while (got < room and uart0.rxCount() > 0) {
+ rx[rx_len + got] = uart0.popByte();
+ got += 1;
+ }
+ rx_len += got;
+ }
+
+ var off: usize = 0;
+ while (off < rx_len) {
+ const n = step();
+ if (n == 0) break;
+ off += n;
+ }
+ if (off > 0) {
+ std.mem.copyForwards(u8, rx[0 .. rx_len - off], rx[off..rx_len]);
+ rx_len -= off;
+ }
+ }
+}
+
+/// Reset entry, the same shape as every other application here: the bootloader hands over with an
+/// unspecified stack pointer and the FPU off, and `.bss` is not cleared for us. The FPU bit matters
+/// even in a program with no floats, because a generic `bufPrint` instantiation can reach std's
+/// float formatting path - and `std.hash.Crc32`'s table generation is comptime, so nothing here
+/// needs the FPU at run time, but nothing here is worth a trap either.
+export fn _start() linksection(".text.entry") callconv(.naked) noreturn {
+ asm volatile (
+ \\ li t0, 1 << 13
+ \\ csrs mstatus, t0
+ \\ la sp, __stack_top
+ \\ mv fp, sp
+ \\ la t0, __bss_start
+ \\ la t1, __bss_end
+ \\ bgeu t0, t1, 2f
+ \\1:
+ \\ sw zero, 0(t0)
+ \\ addi t0, t0, 4
+ \\ bltu t0, t1, 1b
+ \\2:
+ \\ j zig_main
+ );
+}
+
+/// A panic here would be a measurement that silently stopped, so it says so on the wire it was
+/// measuring - through the ROM's printf, because a panic may well be the UART path itself failing.
+pub const panic = std.debug.FullPanic(struct {
+ fn call(msg: []const u8, _: ?usize) noreturn {
+ @import("soc").rom.print("MARK UARTPERF_PANIC %s\r\n", .{msg.ptr});
+ while (true) {}
+ }
+}.call);