summaryrefslogtreecommitdiff
path: root/src/esp32p4/uart.zig
blob: 0afd145bf8c0caec716c471b21210e9aa7980358 (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
//! UART0 as the editor's terminal: bytes out, bytes in, and nothing else.
//!
//! This is the whole of the firmware's I/O. There is no framebuffer and no keyboard; the board
//! emits ANSI and consumes ANSI, and the terminal emulator on the far end of the CH340 does the
//! rest of the work - including answering the editor's own capability queries, which travel down
//! this wire like any other bytes.
//!
//! WHY IT IS IN THIS REPOSITORY. It is not a UART driver - that is `hal.uart`, which stays in the
//! toolchain package and is checked against ESP-IDF's own headers by `zig build diff` there. This is
//! the EDITOR's use of one: which instance the console is, that the transmitter is real
//! backpressure because a truncated escape sequence corrupts the host terminal, that rescued
//! keystrokes must come out before FIFO ones, and that the first thing to do at startup is discard
//! the host bridge's synthetic resize report. Every one of those is a statement about the editor, so
//! the file moved to sit beside it. See `app.zig`'s header for the boundary in full.
//!
//! What it expects from the toolchain package is exactly one module: `hal`, for `hal.uart.Uart`.
//! Nothing else here reaches the chip. The sibling `input_rescue.zig` is a plain file, not a module,
//! because it is this repository's own policy.
//!
//! Deliberately not a `std.Io.Writer`. The ANSI encoding lives on the other side of the C ABI, next
//! to the vaxis that produces it (see `app.zig` for why the seam is there and not elsewhere), so
//! what crosses into this file is already a finished run of bytes. A writer here would be a second
//! buffer in front of one that already exists.
//!
//! Two decisions worth stating, because both are measurements rather than preferences.
//!
//! **Batched FIFO access.** The naive push is `while (txFree() == 0) {}` then `pushByte`, once per
//! byte: one MMIO read per byte at best, many while the FIFO is full. Reading `txFree` once and
//! then pushing that many cuts the status reads by up to the FIFO depth (128, the toolchain
//! package's `src/hal/uart.zig:52`). At 115200 the wire costs ~86 us per byte and dwarfs either
//! version, so today this is merely free - and it stops being free the moment the divider is raised.
//!
//! **UART0's configuration is never touched.** Not the divider, not the format, not the pad
//! routing, and above all not `reset()`. The second-stage bootloader configured this block, and
//! `src/hal/uart.zig:195-211` records what happens if it is reset: UART_CLKDIV returns to its
//! power-on value, the console turns to garbage mid-sentence, and the board takes a watchdog reset
//! with nothing readable left to explain it. Everything here touches FIFO offset 0x000 and the
//! status register, and nothing else.

const hal = @import("hal");
const input_rescue = @import("input_rescue.zig");

/// UART0: the instance the CH340 is wired to, and the one the ROM and bootloader configured.
const uart0 = hal.uart.Uart.init(0);

/// Keystrokes taken off the receiver while the transmitter was full. See `input_rescue`: without
/// this, anything typed into a frame longer than the 128-byte FIFO was silently gone.
var rescued: input_rescue.Ring = .{};

/// Push `bytes` into the TX FIFO, blocking while it is full.
///
/// The spin is normally bounded by the wire - a full 128-byte FIFO drains in 11 ms at 115200 - and
/// dropping instead of waiting would truncate an escape sequence, leaving the host terminal in the
/// wrong colour for the rest of the session. So the wait is real backpressure.
///
/// But it is BOUNDED, for the reason the toolchain package's `src/hal/uart.zig:182-186` gives about
/// `update()`: a UART whose core clock has been gated never makes progress, and "on a board with no
/// debugger an infinite spin is indistinguishable from a crash". That is not hypothetical here - it
/// is how this port spent an afternoon: output stopped mid-boot with no panic and no watchdog (the
/// RTC watchdog having been correctly disabled), which looked like a hang in whatever code came next
/// rather than a stalled transmitter. A bounded wait turns that into visibly dropped output plus a
/// counter, which is a diagnosis instead of a mystery.
///
/// The limit is per burst, not per call, and generous: 1,000,000 status reads is far longer than
/// any legitimate drain and still a fraction of a second.
pub fn write(bytes: []const u8) void {
    dropped +%= input_rescue.pump(uart0, &rescued, bytes, 1_000_000);
}

/// Bytes abandoned because the transmitter stopped making progress. Nonzero means the console is
/// lying about what happened, so it is worth printing.
pub var dropped: u32 = 0;

/// One byte, for callers that must not touch `.rodata` to say anything - which during bring-up is
/// the difference between a diagnostic and a second copy of the bug being diagnosed.
pub fn writeByte(b: u8) void {
    var spins: u32 = 0;
    while (uart0.txFree() == 0) {
        spins += 1;
        if (spins > 1_000_000) {
            dropped +%= 1;
            return;
        }
    }
    uart0.pushByte(b);
}

/// Emit `n` bytes read from `addr` as two hex digits each, computing the digits arithmetically so
/// nothing here reads a lookup table. Used to answer "does a load from this address return what the
/// linker put there", which is not a question a string literal can be trusted to ask.
pub fn dumpHex(addr: u32, n: u32) void {
    const p: [*]const volatile u8 = @ptrFromInt(addr);
    var i: u32 = 0;
    while (i < n) : (i += 1) {
        const byte = p[i];
        for ([2]u8{ byte >> 4, byte & 0xf }) |nib| {
            writeByte(if (nib < 10) '0' + nib else 'a' + (nib - 10));
        }
    }
    writeByte('\r');
    writeByte('\n');
}

/// A u32 as eight hex digits, reading no memory at all.
pub fn dumpWord(v: u32) void {
    var shift: u5 = 28;
    while (true) {
        const nib: u8 = @intCast((v >> shift) & 0xf);
        writeByte(if (nib < 10) '0' + nib else 'a' + (nib - 10));
        if (shift == 0) break;
        shift -= 4;
    }
    writeByte('\r');
    writeByte('\n');
}

/// Move whatever the host has sent into `buf`, without waiting. Returns the count.
///
/// Non-blocking on purpose: the loop has a frame to render and a core to pump, and the editor must
/// not stall on a keystroke that may never come. `rxCount` is read once per call and the FIFO
/// drained to that mark, so a fast typist or a pasted buffer cannot hold the loop here.
pub fn read(buf: []u8) usize {
    // RESCUED BYTES FIRST. They arrived before anything still sitting in the FIFO, and an editor
    // that reorders keystrokes is worse than one that drops them.
    var n = rescued.pop(buf);
    const waiting = @min(uart0.rxCount(), buf.len - n);
    for (buf[n..][0..waiting]) |*slot| slot.* = uart0.popByte();
    n += waiting;
    return n;
}

/// Take whatever has arrived off the receiver right now, without waiting and without handing it to
/// anyone. For callers that are about to spend a while not reading: `write` does this while the
/// transmitter is full, and the loop does it between chunks of input, because applying a keystroke
/// gets more expensive as the line grows and 128 bytes of FIFO is only 11 ms at 115200.
pub fn rescueNow() void {
    input_rescue.rescue(uart0, &rescued);
}

/// Input abandoned because even the rescue buffer overflowed. Distinct from `dropped`, which is
/// OUTPUT abandoned by a stalled transmitter.
pub fn inputDropped() u32 {
    return rescued.dropped;
}

/// Discard anything already received, returning how much. Used once at startup: the host-side
/// bridge injects a window-size report before this program exists, and the bootloader's chatter has
/// already been echoed at the host. Neither is user input.
///
/// Pops rather than calling `resetRxFifo`, which is a CONF0_SYNC read-modify-write plus two commits
/// on the console UART - see this file's header.
pub fn drainInput() u32 {
    var discarded: u32 = 0;
    while (uart0.rxCount() > 0) : (discarded += 1) _ = uart0.popByte();
    discarded += @intCast(rescued.len);
    rescued.clear();
    return discarded;
}

/// The rate the hardware is actually producing, by reading its dividers back. Reported rather than
/// assumed: the host has to be opened at the same rate, and a mismatch shows up as garbage on the
/// screen rather than as an error anyone can act on.
pub fn baudrate() u32 {
    return uart0.baudrate(uart0.clockSource().nominalHz());
}