#!/usr/bin/env python3 """The firmware must receive serial bytes and answer a programming command. Serial receive used to be impossible. USART1 was a register stub with no chardev, so nothing could be sent in; and App/driver/uart.c locates incoming data with write_ptr = sizeof(UART_DMA_Buffer) - LL_DMA_GetDataLength(DMA1, CHANNEL_2) over a circular DMA channel, while the DMA model only ever serviced SPI and never decremented CNDTR for USART. That expression was therefore always 0 and the buffer always looked empty, which made the whole UV-K5 programming protocol in App/app/uart.c unreachable: 0x0514 handshake, 0x051B/0x051D EEPROM read and write, 0x05DD reset. This sends a real 0x0514 handshake and checks for a reply. Wire format, from App/app/uart.c: AB CD DC BA The payload is obfuscated with a fixed XOR key, and the CRC is CRC-16/XMODEM over the payload. Replies use the same framing. """ import gzip import os import shutil import socket import struct import subprocess import sys import tempfile import time import uvk5_socket import uvk5_testenv HERE = os.path.dirname(os.path.abspath(__file__)) ROOT = os.path.dirname(HERE) QEMU = uvk5_testenv.qemu() ELF = uvk5_testenv.firmware() PRISTINE = os.path.join(ROOT, "assets", "pristine", "flash-pristine.img.gz") BOOT_SECONDS = 20 # App/app/uart.c: Obfuscation[] applied to the payload of every frame. XOR_KEY = bytes([ 0x16, 0x6C, 0x14, 0xE6, 0x2E, 0x91, 0x0D, 0x40, 0x21, 0x35, 0xD5, 0x40, 0x13, 0x03, 0xE9, 0x80, ]) def crc16_xmodem(data: bytes) -> int: crc = 0 for byte in data: crc ^= byte << 8 for _ in range(8): crc = ((crc << 1) ^ 0x1021) & 0xFFFF if crc & 0x8000 else (crc << 1) & 0xFFFF return crc def obfuscate(payload: bytes) -> bytes: return bytes(b ^ XOR_KEY[i % len(XOR_KEY)] for i, b in enumerate(payload)) def build_frame(payload: bytes) -> bytes: body = payload + struct.pack(" len(buf): break length = struct.unpack_from(" len(buf) or length > 512: i = start + 2 continue body = obfuscate(buf[start + 4:end]) out.append(body[:length]) i = end + 2 return out def main(): for path, what in ((QEMU, "QEMU"), (ELF, "firmware"), (PRISTINE, "pristine flash image")): if path is None or not os.path.exists(path): print("SKIP: %s is missing (%s); see the README Quick start" % (what, path or "not found")) return 0 workdir = tempfile.mkdtemp(prefix="uvk5-serial-") image = os.path.join(workdir, "flash.img") sock_path = os.path.join(workdir, "serial.sock") with gzip.open(PRISTINE, "rb") as src, open(image, "wb") as dst: shutil.copyfileobj(src, dst) # A listening socket for QEMU's serial chardev to connect back to. Through # uvk5_socket, so this is a unix socket where the platform has them and TCP where # it does not -- a Windows QEMU cannot create a unix one, and this test is part of # the verification path that has to work wherever the emulator does. srv, serial_endpoint = uvk5_socket.listen("serial") srv.settimeout(40) proc = subprocess.Popen( [QEMU, "-M", f"uv-k5-v3,flash-image={image}", "-nographic", "-monitor", "none", "-serial", serial_endpoint, "-kernel", ELF], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) failures = [] try: conn, _ = srv.accept() conn.settimeout(15) print("serial connected") time.sleep(BOOT_SECONDS) # Drain the boot banner the firmware transmits, so it is not mistaken for a # reply. Transmit already worked; this test is about the other direction. conn.setblocking(False) banner = b"" deadline = time.time() + 2 while time.time() < deadline: try: chunk = conn.recv(4096) if not chunk: break banner += chunk except BlockingIOError: time.sleep(0.1) print(f"boot output: {len(banner)} bytes" f"{' (' + banner[:40].decode(errors='replace').strip() + ')' if banner else ''}") conn.setblocking(True) # 0x0514: hello. Payload is the command id plus a 4-byte timestamp. payload = struct.pack("