From 3cfe096d8113e0f336f5105b20d05df33f47dd91 Mon Sep 17 00:00:00 2001 From: nearology Date: Mon, 9 Nov 2026 20:43:58 +0330 Subject: [PATCH] =?UTF-8?q?=D9=88=D8=B1=DA=A9=20=D9=81=D9=84=D9=88=20?= =?UTF-8?q?=D9=86=D8=B5=D8=A8=20u-boot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/uboot-transfer.yml | 16 +++++++++ firmware/README.md | 10 ++++++ scripts/serial_command.py | 53 ++++++++++++++++++++++++++--- 3 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 .gitea/workflows/uboot-transfer.yml create mode 100644 firmware/README.md diff --git a/.gitea/workflows/uboot-transfer.yml b/.gitea/workflows/uboot-transfer.yml new file mode 100644 index 0000000..a2732c4 --- /dev/null +++ b/.gitea/workflows/uboot-transfer.yml @@ -0,0 +1,16 @@ +name: Transfer U-Boot to RAM + +on: + workflow_dispatch: + +jobs: + uboot-transfer: + runs-on: serial-runner + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Transfer firmware and verify RAM contents + run: | + /usr/bin/python3 scripts/serial_command.py transfer diff --git a/firmware/README.md b/firmware/README.md new file mode 100644 index 0000000..6a96a4e --- /dev/null +++ b/firmware/README.md @@ -0,0 +1,10 @@ +Place `u-boot-sunxi-with-spl.bin` in this directory before running the +Transfer U-Boot to RAM workflow. + +The existing TFTP server (`serverip` in U-Boot) must serve this directory +as its TFTP root, so the file is available as `u-boot-sunxi-with-spl.bin`. +Checking out this repository alone does not publish files over TFTP. + +The workflow uses the current U-Boot network settings. It verifies the +transferred size and the first 32 bytes against this local file. It does +not erase or write SPI flash, save the environment, or boot the image. diff --git a/scripts/serial_command.py b/scripts/serial_command.py index 5bd179a..70fbe19 100644 --- a/scripts/serial_command.py +++ b/scripts/serial_command.py @@ -4,6 +4,7 @@ import fcntl import re import sys import time +from pathlib import Path import serial @@ -55,7 +56,7 @@ class UBoot: self.prompt = candidate print("\nU-Boot-style CLI prompt detected: " + candidate, flush=True) - def command(self, command, expected=None, timeout=10): + def command(self, command, expected=None, timeout=10, allow_missing=()): print("\n>>> " + command, flush=True) self.port.write((command + "\r").encode()) self.port.flush() @@ -65,21 +66,57 @@ class UBoot: self.port.flush() status, _ = self.read_prompt(5) codes = re.findall(r"(?m)^__UBOOT_STATUS_ (\d+) *$", status) - if codes != ["0"]: + # Only the initial environment inspection may report unset variables. + missing_pattern = r'^## Error: "(' + "|".join(map(re.escape, allow_missing)) + r')" not defined$' + missing = re.findall(missing_pattern, output, re.MULTILINE) if allow_missing else [] + checked_output = re.sub(missing_pattern, "", output, flags=re.MULTILINE) if missing else output + if codes != ["0"] and not (codes == ["1"] and missing): raise RuntimeError("Command failed or its exit status could not be verified: " + command) - if ERROR.search(output): + if ERROR.search(checked_output): raise RuntimeError("Error in command response: " + command) if expected and not re.search(expected, output, re.MULTILINE): raise RuntimeError("Expected response missing: " + command) + if missing: + print("\nInitially unset: " + ", ".join(missing), flush=True) print("\nVerified: " + command, flush=True) return output +def transfer(cli, firmware): + image = firmware.read_bytes() + if not 32 <= len(image) <= 0x02000000: + raise RuntimeError("Firmware must contain at least 32 bytes and fit RAM at 0x42000000") + output = cli.command( + "tftp 0x42000000 u-boot-sunxi-with-spl.bin", + r"^Bytes transferred = \d+", timeout=120, + ) + transferred = re.findall(r"^Bytes transferred = (\d+)", output, re.MULTILINE) + if transferred != [str(len(image))]: + raise RuntimeError("TFTP byte count differs from the local firmware file") + output = cli.command("echo ${filesize}", r"^[0-9a-fA-F]+$") + 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:") + dumped = bytearray() + for address in ("42000000", "42000010"): + rows = re.findall(r"^" + 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) + 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; no flash write performed.") + + def main(): parser = argparse.ArgumentParser() - parser.add_argument("mode", choices=("check", "configure"), default="check", nargs="?") + parser.add_argument("mode", choices=("check", "configure", "transfer"), 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") args = parser.parse_args() + if args.mode == "transfer" 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: fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB) @@ -88,8 +125,14 @@ def main(): cli.detect() if args.mode == "check": return + if args.mode == "transfer": + transfer(cli, args.firmware) + return cli.command("version", r"^U-Boot \d") - cli.command("printenv ipaddr serverip ethaddr bootcmd bootargs") + cli.command( + "printenv ipaddr serverip ethaddr bootcmd bootargs", + allow_missing=("ipaddr", "serverip", "ethaddr", "bootcmd", "bootargs"), + ) cli.command("setenv ipaddr 192.168.75.100") cli.command("printenv ipaddr", r"^ipaddr=192\.168\.75\.100$") cli.command("setenv serverip 192.168.75.247")