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
|
//! UART0 as a duplex byte pipe, which is the one thing this toolchain had never done.
//!
//! Everything else here talks to the host through `soc.rom.print`, a mask-ROM `ets_printf`. That is
//! one-way and it is slow: the ROM formats, then pushes a byte at a time and spins on the FIFO. An
//! editor rendering a screen needs the other direction and needs the fast path, so this example
//! exists to prove three things on the die before anything larger depends on them:
//!
//! 1. **RX works at all.** `hal/uart.zig` has had `rxCount`/`popByte` since the differential
//! suite needed them, but that suite runs on UART1 in internal loopback - no byte has ever
//! arrived from the outside world on UART0.
//! 2. **UART0 can be driven without reconfiguring it.** The header of `hal/uart.zig` is blunt
//! about the hazard: resetting UART0 clears UART_CLKDIV, the console turns to garbage
//! mid-sentence and the board dies on a watchdog reset. So this touches no configuration
//! register - the second-stage bootloader already set the divider, the format and the pad
//! routing, and this code only reads and writes the FIFO.
//! 3. **What the wire rate actually is.** Printed by asking the hardware
//! (`Uart.baudrate`), not by assuming the 115200 the host tooling opens with.
//!
//! Protocol, so the host side has something unambiguous to assert on:
//!
//! any byte -> echoed back verbatim
//! CR (0x0d) -> echoed as CRLF, so a human sees lines
//! '!' -> also emit `bulk_len` bytes of a counted pattern and report the cycles it took
//! Ctrl-D -> print the byte/frame counters
//!
//! The echo is verbatim rather than uppercased or otherwise transformed on purpose: a transform
//! that happens to be idempotent hides a duplicated byte, and a duplicated byte is exactly the
//! failure a FIFO-polling loop produces when `rxCount` is misread.
const std = @import("std");
const soc = @import("soc");
const hal = @import("hal");
/// UART0. Instance 0 because that is the pad pair the CH340 is wired to and the one the ROM
/// configured; nothing here may reset it.
const con = hal.uart.Uart.init(0);
/// The bulk burst `!` emits. 4 KiB is ~35 ms of wire time at 115200 and ~4.5 ms at 921600, so the
/// difference between the two is obvious to the naked eye on the host.
const bulk_len = 4096;
/// The clock the UART's baud generator is dividing. XTAL is the reset default and what the ROM
/// leaves selected; `Uart.clockSource` is read below rather than assumed, so a bootloader that
/// switched to PLL_F80M shows up as a wrong rate instead of a silent 2x error.
fn sourceHz() u32 {
return con.clockSource().nominalHz();
}
/// Block until the TX FIFO has room, then push. The spin is bounded by the wire: at 115200 a full
/// 128-byte FIFO drains in 11 ms, and there is nothing else for this core to do.
///
/// `txFree` and not "is the FIFO empty": pushing whenever there is a single free slot keeps the
/// transmitter fed, which is what makes this ~10x the throughput of the ROM's per-byte printf.
fn put(byte: u8) void {
while (con.txFree() == 0) {}
con.pushByte(byte);
}
fn puts(bytes: []const u8) void {
for (bytes) |b| put(b);
}
export fn zig_main() noreturn {
// Deliberately the ROM path for the banner: if the direct-FIFO writes below are wrong, the
// banner still arrives and says so. Mixing the two is safe because both end up in the same
// FIFO and this is the only writer.
soc.rom.print("\r\nMARK ECHO_BOOT uart0 duplex echo\r\n", .{});
soc.rom.print("MARK ECHO_BAUD hw=%u src=%u Hz\r\n", .{ con.baudrate(sourceHz()), sourceHz() });
// Not `resetRxFifo`: that is a CONF0_SYNC read-modify-write plus two commits on the console
// UART, and this file's whole premise is that UART0's configuration is untouchable. Draining by
// popping has the same effect on the FIFO and touches only offset 0x000.
var dropped: u32 = 0;
while (con.rxCount() > 0) : (dropped += 1) _ = con.popByte();
soc.rom.print("MARK ECHO_DRAIN dropped=%u stale bytes\r\n", .{dropped});
puts("MARK ECHO_FIFO direct-fifo tx works\r\n");
puts("type; '!' bulk, ctrl-D stats\r\n");
var rx_total: u32 = 0;
var bursts: u32 = 0;
while (true) {
if (con.rxCount() == 0) continue;
const byte = con.popByte();
rx_total += 1;
switch (byte) {
'\r' => puts("\r\n"),
0x04 => {
var buf: [96]u8 = undefined;
const line = std.fmt.bufPrint(
&buf,
"\r\nMARK ECHO_STATS rx={d} bursts={d} baud={d}\r\n",
.{ rx_total, bursts, con.baudrate(sourceHz()) },
) catch "\r\nMARK ECHO_STATS fmt failed\r\n";
puts(line);
},
'!' => {
bursts += 1;
put(byte);
const t0 = soc.cycles();
// A counted pattern, not a constant: a run of identical bytes cannot reveal a
// dropped or reordered one, and the host asserts on the sequence.
var i: u32 = 0;
while (i < bulk_len) : (i += 1) put('0' + @as(u8, @intCast(i % 10)));
const cycles = soc.cycles() - t0;
var buf: [96]u8 = undefined;
const line = std.fmt.bufPrint(
&buf,
"\r\nMARK ECHO_BULK {d} bytes in {d} cycles\r\n",
.{ bulk_len, cycles },
) catch "\r\nMARK ECHO_BULK fmt failed\r\n";
puts(line);
},
else => put(byte),
}
}
}
/// Reset entry. Identical in shape to `src/main.zig`'s and for the same reasons - the bootloader
/// hands over with an unspecified stack pointer and the FPU off - but `std.fmt.bufPrint` above is
/// the reason the FPU bit matters here too: its float formatting path is reachable from a generic
/// `bufPrint` instantiation even when no argument is a float.
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
);
}
pub const panic = std.debug.FullPanic(struct {
fn call(msg: []const u8, _: ?usize) noreturn {
soc.rom.print("MARK ECHO_PANIC %s\r\n", .{msg.ptr});
while (true) {}
}
}.call);
|