diff options
Diffstat (limited to 'examples/echo.zig')
| -rw-r--r-- | examples/echo.zig | 146 |
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); |
