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
|
//! 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, except by `setBaud`.** 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 else here touches FIFO offset 0x000 and the
//! status register, and nothing else.
//!
//! The DIVIDER is the one exception, and it was carved out for the second image rather than for this
//! one: `docs/registry.typ` `BOARD-1`. The editor's console is opened by a human at 115200 and the
//! firmware inherits that divider (which is why the paragraph above used to say "not the divider");
//! the 9P image (`nine.zig`) has a program on the far end that opens the port at whatever rate the
//! image was built for, and eight times the baud is eight times less latency on every `Tread`. So
//! `setBaud` exists, this file still never calls it, and `app.zig` still never calls it — the only
//! caller is the image whose host side is opened to match.
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;
/// Push as much of `bytes` as the transmitter has room for RIGHT NOW, and answer how much. Never
/// spins, never drops, never rescues — because it never waits for anything.
///
/// THIS IS THE SANS-IO WRITE, and it exists because `write` above is the wrong primitive for a
/// protocol server. `write` is right for a console: a frame of ANSI is atomic to the terminal on the
/// far end, half an escape sequence leaves it in the wrong colour, so blocking until the whole burst
/// is in the FIFO is real backpressure and worth the stall. A 9P reply is not atomic to anything:
/// every message carries its own length, the reader on the far end reassembles, and
/// `9p.Server.wrote(n)` exists precisely so that a partial write costs nothing but another trip
/// round the loop (`src/9p.zig:2248-2257`). So this hands over what fits and returns, and
/// `src/esp32p4_9p.zig`'s loop keeps the remainder queued in the server where it already was.
///
/// The difference that matters is DROPPING. `write`'s bounded spin gives up after a million status
/// reads and counts the loss, which turns a stalled transmitter into a diagnosable console; the same
/// behaviour on a 9P stream would truncate a reply mid-message and desynchronise the connection for
/// good. A short count cannot desynchronise anything.
pub fn writeSome(bytes: []const u8) usize {
const n = @min(@as(usize, uart0.txFree()), bytes.len);
for (bytes[0..n]) |b| uart0.pushByte(b);
return n;
}
/// Reprogram UART0's divider, and answer whether the rate is reachable from the clock this block is
/// running on. Nothing is touched when it is not.
///
/// THE ONE EXCEPTION to this file's rule, and see the header for who may call it: not this file, not
/// `app.zig`, only an image whose host side is opened at the same rate. `docs/registry.typ`
/// `BOARD-1` has the numbers — 921600 is one `UART_CLKDIV_SYNC` write on the existing 40 MHz XTAL,
/// int 43 frag 6, +0.064% error, and it takes a byte from 86.8 µs to 10.85 µs.
///
/// The hazard is worth restating where the call is: the bytes already in the FIFO go out at the OLD
/// rate, so anything written before this and not yet drained is corrupted, and a host that is not
/// reopened sees garbage from here on with no error to report. The 9P image calls this before it
/// answers its first message, which is the one moment when neither of those can have happened yet.
pub fn setBaud(baud: u32) bool {
return uart0.setBaudrate(baud, uart0.clockSource().nominalHz());
}
/// 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());
}
|