summaryrefslogtreecommitdiff
path: root/examples/echo.zig
diff options
context:
space:
mode:
Diffstat (limited to 'examples/echo.zig')
-rw-r--r--examples/echo.zig146
1 files changed, 146 insertions, 0 deletions
diff --git a/examples/echo.zig b/examples/echo.zig
new file mode 100644
index 0000000..6f17c98
--- /dev/null
+++ b/examples/echo.zig
@@ -0,0 +1,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);