summaryrefslogtreecommitdiff
path: root/examples/uartperf.zig
blob: 339f642eab42eacc77b2e4a33ce0d89b68982a15 (plain) (blame)
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
//! 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);