//! 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);