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.
This commit is contained in:
mckero committed 2026-08-28 06:27:46 +01:00
1 parent 5b53cbbc82
commit c6d58f5601
2 files changed
+130 -1

No files matched your search

+62 -1
View File
@@ -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);
}
+68
View File
@@ -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())