From c6d58f5601d3fd920fb1612103a9859cadda84b7 Mon Sep 17 00:00:00 2001 From: MCKero Date: Fri, 28 Aug 2026 06:27:46 +0100 Subject: [PATCH] Surface firmware serial output instead of dropping it The firmware has been printing all along and nothing was listening. USART1 has no real model here -- it is one of the logging catch-all stubs -- so every byte went into qemu_log_mask(LOG_UNIMP) and vanished. Two things were needed, and the second was not in the plan: 1. Print USART1 DR writes (+0x04, per the vendor CMSIS header) as SERIAL lines. 2. Report TXE|TC in USART1 SR. This is the part I had missed. UART_Send() in App/driver/uart.c spins on LL_USART_IsActiveFlag_TXE() with a bounded timeout and *skips the byte* when the flag never sets. A stub returning 0 for SR meant the firmware discarded its own output before it ever reached DR -- the only write arriving was UART_Init()'s priming zero. So step 1 alone produced nothing, which is why the first attempt looked like "the build has no logging". Then a bug of my own: the priming byte is 0x00, I buffered it, and fprintf("%s") stopped at that NUL and printed an empty line while all 46 bytes sat behind it. NULs are now dropped, and a line flushes on CR as well as LF. Verified: SERIAL UV-K5 Firmware, EGZUMER-F4HWN+NR7Y c91cec95 keypad_test.py still passes, which matters because this file is where removing three fprintfs once silently deleted the keypad. --- qemu/py32f071.c | 63 ++++++++++++++++++++++++++++++++- tools/test_serial_capture.py | 68 ++++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 1 deletion(-) create mode 100755 tools/test_serial_capture.py diff --git a/qemu/py32f071.c b/qemu/py32f071.c index 2e4b948..7d0ff60 100644 --- a/qemu/py32f071.c +++ b/qemu/py32f071.c @@ -1273,17 +1273,75 @@ struct PY32StubState { uint32_t regs[0x100]; }; +/* USART_SR transmit flags, from the vendor header: TXE is bit 7, TC bit 6. */ +#define PY32_USART_SR_TC (1u << 6) +#define PY32_USART_SR_TXE (1u << 7) + static uint64_t py32_stub_read(void *opaque, hwaddr addr, unsigned size) { PY32StubState *s = opaque; const unsigned idx = addr >> 2; - const uint32_t value = idx < ARRAY_SIZE(s->regs) ? s->regs[idx] : 0; + uint32_t value = idx < ARRAY_SIZE(s->regs) ? s->regs[idx] : 0; + + /* + * USART1 SR must report the transmitter as ready, or the firmware discards + * everything it tries to print. + * + * UART_Send() in App/driver/uart.c spins on LL_USART_IsActiveFlag_TXE() with + * a bounded timeout and *skips the byte* when the flag never sets. A stub + * that returns 0 for SR therefore silently loses all serial output: the only + * write reaching DR is UART_Init()'s priming zero. Reporting TXE|TC keeps the + * transmitter permanently ready, which is exactly right for a model that + * consumes bytes instantly. + */ + if (addr == 0x00 && s->stub_name && !strcmp(s->stub_name, "usart1")) { + value |= PY32_USART_SR_TXE | PY32_USART_SR_TC; + } qemu_log_mask(LOG_UNIMP, "py32-%s: read 0x%03" HWADDR_PRIx " -> 0x%08x\n", s->stub_name ?: "stub", addr, value); return value; } +/* + * USART1 DR is the firmware's log output, so print it rather than dropping it. + * + * App/driver/uart.c drives USART1 at 38400 baud through UART_Send(), Main() sends + * UART_Version at boot, and _putchar() routes every printf_ there. USART1 has no + * real model here -- it is one of the logging catch-alls below -- so without this + * the bytes vanish and the firmware appears to print nothing at all. + * + * DR is at +0x04: the vendor CMSIS header (py32f071xB.h) lays USART_TypeDef out as + * SR at +0x00 then DR at +0x04. Buffered into a line so the output is readable + * instead of one message per character. + */ +static void py32_stub_serial_byte(char ch) +{ + static char line[256]; + static unsigned len; + + /* + * Drop NULs rather than buffering them. UART_Init() primes the transmitter + * with LL_USART_TransmitData8(USARTx, 0), so the very first byte of the + * session is 0x00; storing it made fprintf("%s") stop right there and print + * an empty line, even though the 46 bytes of UART_Version arrived fine. + */ + if (ch == '\0') { + return; + } + /* Flush on either terminator: the firmware sends CRLF, and a lone CR should + * not hold a finished line hostage. */ + if (ch == '\n' || ch == '\r' || len >= sizeof(line) - 1) { + line[len] = '\0'; + if (len > 0) { + fprintf(stderr, "SERIAL %s\n", line); + } + len = 0; + return; + } + line[len++] = ch; +} + static void py32_stub_write(void *opaque, hwaddr addr, uint64_t value, unsigned size) { PY32StubState *s = opaque; @@ -1292,6 +1350,9 @@ static void py32_stub_write(void *opaque, hwaddr addr, uint64_t value, unsigned if (idx < ARRAY_SIZE(s->regs)) { s->regs[idx] = value; } + if (addr == 0x04 && s->stub_name && !strcmp(s->stub_name, "usart1")) { + py32_stub_serial_byte((char)(value & 0xff)); + } qemu_log_mask(LOG_UNIMP, "py32-%s: write 0x%03" HWADDR_PRIx " = 0x%08" PRIx64 "\n", s->stub_name ?: "stub", addr, value); } diff --git a/tools/test_serial_capture.py b/tools/test_serial_capture.py new file mode 100755 index 0000000..f1697fe --- /dev/null +++ b/tools/test_serial_capture.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""Firmware serial output must reach the host. + +App/main.c sends UART_Version over USART1 right after UART_Init(), and _putchar +routes every printf_ there too. The machine has no USART1 model -- it is one of the +logging catch-all stubs -- so without help those bytes vanish and the firmware +appears to print nothing at all. + +Boots a real emulator on a private socket and checks the bytes come out of QEMU's +stderr as SERIAL lines. + +Run: python3 tools/test_serial_capture.py +""" +import os +import subprocess +import sys +import time + +HERE = os.path.dirname(os.path.abspath(__file__)) +SIM = os.path.dirname(HERE) +QEMU = os.path.expanduser("~/qemu-build/qemu-7.2+dfsg/build/qemu-system-arm") +ELF = os.path.expanduser("~/uvk5-port/uvk5-sat/build/CW/nr7y.cw.elf") +FLASH = os.path.join(SIM, "assets", "flash.img") +LOG = "/tmp/uvk5-serial-test.log" +QMP = "/tmp/uvk5-serial-test.sock" + + +def main(): + for path, what in ((QEMU, "QEMU binary"), (ELF, "firmware ELF"), + (FLASH, "flash image")): + if not os.path.exists(path): + sys.exit(f"missing {what}: {path}") + if os.path.exists(QMP): + os.unlink(QMP) + + with open(LOG, "wb") as fh: + proc = subprocess.Popen( + [QEMU, "-M", f"uv-k5-v3,flash-image={FLASH}", "-nographic", + "-monitor", "none", "-qmp", f"unix:{QMP},server=on,wait=off", + "-kernel", ELF], + stdout=subprocess.DEVNULL, stderr=fh) + try: + time.sleep(14) + finally: + proc.terminate() + try: + proc.wait(timeout=10) + except subprocess.TimeoutExpired: + proc.kill() + if os.path.exists(QMP): + os.unlink(QMP) + + text = open(LOG, errors="replace").read() + lines = [l for l in text.splitlines() if l.startswith("SERIAL")] + print(f"captured {len(lines)} SERIAL line(s)") + for line in lines[:10]: + print(" ", line) + + if not lines: + print("\nFAIL no SERIAL output; USART1 DR writes are still being dropped") + print(" (App/main.c:98 sends UART_Version, so something should appear)") + return 1 + print("\nPASS firmware serial output reaches the host") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())