diff --git a/.gitea/workflows/dtb-transfer.yml b/.gitea/workflows/dtb-transfer.yml new file mode 100644 index 0000000..83924ee --- /dev/null +++ b/.gitea/workflows/dtb-transfer.yml @@ -0,0 +1,16 @@ +name: Transfer, Write and Verify DTB + +on: + workflow_dispatch: + +jobs: + dtb-transfer: + runs-on: serial-runner + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Transfer DTB, write SPI flash and verify read-back + run: | + /usr/bin/python3 scripts/serial_command.py transfer-dtb diff --git a/.gitea/workflows/kernel-transfer.yml b/.gitea/workflows/kernel-transfer.yml new file mode 100644 index 0000000..fad854a --- /dev/null +++ b/.gitea/workflows/kernel-transfer.yml @@ -0,0 +1,16 @@ +name: Transfer, Write and Verify Kernel + +on: + workflow_dispatch: + +jobs: + kernel-transfer: + runs-on: serial-runner + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Transfer Kernel, write SPI flash and verify read-back + run: | + /usr/bin/python3 scripts/serial_command.py transfer-kernel diff --git a/firmware/README.md b/firmware/README.md index 9c67382..e14ce4d 100644 --- a/firmware/README.md +++ b/firmware/README.md @@ -10,3 +10,12 @@ with the local file, probes SPI flash, writes at offset 0, reads back to No erase, reboot or saveenv command is issued. Any failed or unverified command stops execution; flash writes are never automatically retried. + +For the DTB workflow, place `sun8i-v3s-licheepi-zero-dock.dtb` here. +It uses the same checks, with maximum size 0x110000, RAM address +0x41000000 and flash offset 0x200000. Read-back uses 0x42800000. + +For the Kernel workflow, place `zImage` here. It uses maximum size +0x500000, RAM address 0x42000000 and flash offset 0x310000. The first +64 bytes are checked against the local file; full read-back comparison +uses 0x42800000. The same stale-filesize and transfer checks apply. diff --git a/scripts/serial_command.py b/scripts/serial_command.py index e6bac86..016802e 100644 --- a/scripts/serial_command.py +++ b/scripts/serial_command.py @@ -82,14 +82,19 @@ class UBoot: return output -def transfer(cli, firmware): +def transfer(cli, firmware, *, dtb=False, kernel=False): + address = 0x41000000 if dtb else 0x42000000 + offset = 0x310000 if kernel else (0x200000 if dtb else 0) + limit = 0x500000 if kernel else (0x110000 if dtb else 0x200000) + dump_size = 64 if kernel else 32 + filename = "zImage" if kernel else ("sun8i-v3s-licheepi-zero-dock.dtb" if dtb else "u-boot-sunxi-with-spl.bin") image = firmware.read_bytes() - if not 32 <= len(image) <= 0x200000: - raise RuntimeError("Firmware size must be between 32 and 0x200000 bytes") + if not dump_size <= len(image) <= limit: + raise RuntimeError(f"Firmware size must be between {dump_size} and {limit:#x} bytes") # Invalidate any previous transfer length before attempting a new transfer. cli.command("setenv filesize") output = cli.command( - "tftp 0x42000000 u-boot-sunxi-with-spl.bin", + f"tftp {address:#x} {filename}", r"^Bytes transferred = \d+", timeout=120, ) transferred = re.findall(r"^Bytes transferred = (\d+)", output, re.MULTILINE) @@ -99,42 +104,46 @@ def transfer(cli, firmware): sizes = re.findall(r"^([0-9a-fA-F]+)$", output, re.MULTILINE) if len(sizes) != 1 or int(sizes[0], 16) != len(image): raise RuntimeError("U-Boot filesize differs from the local firmware file") - output = cli.command("md.b 0x42000000 0x20", r"^42000000:") + output = cli.command(f"md.b {address:#x} {dump_size:#x}", rf"^{address:08x}:") dumped = bytearray() - for address in ("42000000", "42000010"): - rows = re.findall(r"^" + address + r":((?: [0-9a-fA-F]{2}){16})(?:\s|$)", output, re.MULTILINE) + for row_offset in range(0, dump_size, 16): + row_address = f"{address + row_offset:08x}" + rows = re.findall(r"^" + row_address + r":((?: [0-9a-fA-F]{2}){16})(?:\s|$)", output, re.MULTILINE) if len(rows) != 1: - raise RuntimeError("Missing or invalid memory dump at " + address) + raise RuntimeError("Missing or invalid memory dump at " + row_address) dumped.extend(bytes.fromhex(rows[0])) - if dumped != image[:32]: - raise RuntimeError("First 32 bytes in RAM differ from the local firmware file") - print("\nTransfer verified: size and first 32 bytes match.") + if dumped != image[:dump_size]: + raise RuntimeError(f"First {dump_size} bytes in RAM differ from the local firmware file") + print(f"\nTransfer verified: size and first {dump_size} bytes match.") cli.command("sf probe 0:0", r"^SF: Detected .+", timeout=20) cli.command( - "sf write 0x42000000 0x0 ${filesize}", - rf"^SF: {len(image)} bytes @ 0x0 Written: OK\s*$", timeout=120, + f"sf write {address:#x} {offset:#x} ${{filesize}}", + rf"^SF: {len(image)} bytes @ {offset:#x} Written: OK\s*$", timeout=120, ) cli.command( - "sf read 0x42800000 0x0 ${filesize}", - rf"^SF: {len(image)} bytes @ 0x0 Read: OK\s*$", timeout=120, + f"sf read 0x42800000 {offset:#x} ${{filesize}}", + rf"^SF: {len(image)} bytes @ {offset:#x} Read: OK\s*$", timeout=120, ) output = cli.command( - "cmp.b 0x42000000 0x42800000 ${filesize}", + f"cmp.b {address:#x} 0x42800000 ${{filesize}}", r"^Total of \d+ byte\(s\) were the same", timeout=60, ) compared = re.findall(r"^Total of (\d+) byte\(s\) were the same", output, re.MULTILINE) if compared != [str(len(image))]: raise RuntimeError("Flash comparison byte count differs from firmware size") - print("\nU-Boot flash write and full read-back comparison verified.") + print("\nFirmware flash write and full read-back comparison verified.") def main(): parser = argparse.ArgumentParser() - parser.add_argument("mode", choices=("check", "configure", "transfer"), default="check", nargs="?") + parser.add_argument("mode", choices=("check", "configure", "transfer", "transfer-dtb", "transfer-kernel"), default="check", nargs="?") parser.add_argument("--port", default="/dev/ttyUSB0") - parser.add_argument("--firmware", type=Path, default=Path(__file__).resolve().parent.parent / "firmware" / "u-boot-sunxi-with-spl.bin") + parser.add_argument("--firmware", type=Path, default=None) args = parser.parse_args() - if args.mode == "transfer" and not args.firmware.is_file(): + if args.firmware is None: + filename = "zImage" if args.mode == "transfer-kernel" else ("sun8i-v3s-licheepi-zero-dock.dtb" if args.mode == "transfer-dtb" else "u-boot-sunxi-with-spl.bin") + args.firmware = Path(__file__).resolve().parent.parent / "firmware" / filename + if args.mode in ("transfer", "transfer-dtb", "transfer-kernel") and not args.firmware.is_file(): raise RuntimeError("Firmware file not found: " + str(args.firmware)) # Shared by both workflows: fail rather than interleave serial commands. with open("/tmp/uboot-serial.lock", "w") as lock: @@ -144,8 +153,8 @@ def main(): cli.detect() if args.mode == "check": return - if args.mode == "transfer": - transfer(cli, args.firmware) + if args.mode in ("transfer", "transfer-dtb", "transfer-kernel"): + transfer(cli, args.firmware, dtb=args.mode == "transfer-dtb", kernel=args.mode == "transfer-kernel") return cli.command("version", r"^U-Boot \d") cli.command(