Port image-header and test executor to ast10x0-bundle
diff --git a/.bazelrc b/.bazelrc
index bf0267e..45cc761 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -58,6 +58,7 @@
 # AST1060-EVB physical board (no QEMU, flash directly)
 common:k_ast1060_evb --config=k_common
 common:k_ast1060_evb --platforms=//target/ast10x0
+common:k_ast1060_evb --//target/ast10x0:uart_boot_header=true
 
 # `bazel {build,test,run} --config=virt_ast10x0 //target/ast10x0/...`
 # launches the AST10x0 system images under QEMU's ast1030-evb machine
diff --git a/target/ast10x0/BUILD.bazel b/target/ast10x0/BUILD.bazel
index 76aa365..bb7ccaf 100644
--- a/target/ast10x0/BUILD.bazel
+++ b/target/ast10x0/BUILD.bazel
@@ -17,6 +17,11 @@
     build_setting_default = False,
 )
 
+bool_flag(
+    name = "uart_boot_header",
+    build_setting_default = False,
+)
+
 config_setting(
     name = "qemu_enabled",
     flag_values = {":qemu": "true"},
diff --git a/target/ast10x0/console_backend.rs b/target/ast10x0/console_backend.rs
index 5abf4db..121a3fb 100644
--- a/target/ast10x0/console_backend.rs
+++ b/target/ast10x0/console_backend.rs
@@ -16,13 +16,13 @@
 /// MMIO base address of UART5 on the AST10x0 SoC (AST1060 TRM §28, Table 28-1).
 const UART5_BASE: *const device::uart::RegisterBlock = 0x7e78_4000 as *const _;
 
-// SAFETY: UART5_BASE is the UART5 MMIO base on AST10x0. This static is the
-// sole owner of the peripheral; the SpinLock ensures exclusive access.
-static UART: SpinLock<arch_arm_cortex_m::Arch, Usart> =
-    SpinLock::new(unsafe { Usart::new(UART5_BASE) });
+// Global console lock to serialize UART register access.
+static UART_LOCK: SpinLock<arch_arm_cortex_m::Arch, ()> = SpinLock::new(());
 
 #[unsafe(no_mangle)]
 pub fn console_backend_write_all(buf: &[u8]) -> Result<()> {
-    let mut uart = UART.lock(arch_arm_cortex_m::Arch);
+    let _lock = UART_LOCK.lock(arch_arm_cortex_m::Arch);
+    // UART is configured by ROM/bootloader before firmware starts.
+    let mut uart = unsafe { Usart::new_uninit(UART5_BASE) };
     uart.write_all(buf).map_err(|_| Error::DataLoss)
 }
diff --git a/target/ast10x0/harness/uart_upload_test.bzl b/target/ast10x0/harness/uart_upload_test.bzl
index aebf7ee..9f14ed0 100644
--- a/target/ast10x0/harness/uart_upload_test.bzl
+++ b/target/ast10x0/harness/uart_upload_test.bzl
@@ -6,18 +6,82 @@
 """
 
 load(
+    "@bazel_skylib//rules:common_settings.bzl",
+    "BuildSettingInfo",
+)
+
+load(
     "@pigweed//pw_kernel/tooling:system_image.bzl",
     "SystemImageInfo",
 )
 
+def _firmware_bin(ctx):
+    if SystemImageInfo in ctx.attr.image:
+        return ctx.attr.image[SystemImageInfo].bin
+    return ctx.file.image
+
+def _declare_uart_boot_image(ctx, firmware_bin, name):
+    output = ctx.actions.declare_file(name + ".bin")
+    ctx.actions.run_shell(
+        inputs = [firmware_bin],
+        outputs = [output],
+        arguments = [firmware_bin.path, output.path],
+        mnemonic = "Ast10x0UartBootImage",
+        command = """set -eu
+input=\"$1\"
+output=\"$2\"
+size=$(wc -c < \"$input\")
+aligned=$(( (size + 3) & ~3 ))
+
+emit_byte() {
+    printf '%b' "$(printf '\\%03o' \"$1\")"
+}
+
+{
+    emit_byte $((aligned & 255))
+    emit_byte $(((aligned >> 8) & 255))
+    emit_byte $(((aligned >> 16) & 255))
+    emit_byte $(((aligned >> 24) & 255))
+    cat \"$input\"
+    padding=$((aligned - size))
+    if [ \"$padding\" -gt 0 ]; then
+        dd if=/dev/zero bs=1 count=\"$padding\" status=none
+    fi
+} > \"$output\"
+""",
+    )
+    return output
+
+def _uart_boot_image_impl(ctx):
+    firmware_bin = _firmware_bin(ctx)
+    if ctx.attr._uart_boot_header[BuildSettingInfo].value:
+        output = _declare_uart_boot_image(ctx, firmware_bin, ctx.label.name)
+    else:
+        output = firmware_bin
+    return [
+        DefaultInfo(files = depset([output])),
+    ]
+
+uart_boot_image = rule(
+    implementation = _uart_boot_image_impl,
+    attrs = {
+        "image": attr.label(
+            mandatory = True,
+            doc = "system_image or binary target to wrap with the AST10x0 UART boot header",
+        ),
+        "_uart_boot_header": attr.label(
+            default = "//target/ast10x0:uart_boot_header",
+            providers = [BuildSettingInfo],
+        ),
+    },
+    doc = "Generate an AST10x0 UART boot image by prepending the 4-byte size header when enabled by config.",
+)
+
 def _uart_upload_test_impl(ctx):
     """Implementation of uart_upload_test rule."""
 
     # Get the firmware binary
-    if SystemImageInfo in ctx.attr.image:
-        firmware_bin = ctx.attr.image[SystemImageInfo].bin
-    else:
-        firmware_bin = ctx.file.image
+    firmware_bin = _firmware_bin(ctx)
 
     # Create test script
     test_script = ctx.actions.declare_file(ctx.label.name + "_test.sh")
@@ -147,7 +211,7 @@
             doc = "Upload firmware only, skip test monitoring",
         ),
         "_uart_test_exec": attr.label(
-            default = "//target/ast1060-evb/harness:uart_test_exec.py",
+            default = "//target/ast10x0/harness:uart_test_exec.py",
             allow_single_file = [".py"],
         ),
     },
@@ -163,11 +227,21 @@
 - SKIP_DEVICE_CHECK: Skip device existence check (for CI)
 
 Usage:
-    load("//target/ast1060-evb/harness:uart_upload_test.bzl", "uart_upload_test")
+    load(
+        "//target/ast10x0/harness:uart_upload_test.bzl",
+        "uart_boot_image",
+        "uart_upload_test",
+    )
+
+    # AST1030-EVB UART boot expects the 4-byte size header.
+    uart_boot_image(
+        name = "threads_uart",
+        image = ":threads",
+    )
 
     uart_upload_test(
         name = "threads_uart_test",
-        image = ":threads_uart",  # uart_boot_image target
+        image = ":threads_uart",
         test_timeout = 300,
     )
 
@@ -181,10 +255,7 @@
     """Implementation of uart_upload rule (non-test, just upload)."""
 
     # Get the firmware binary
-    if SystemImageInfo in ctx.attr.image:
-        firmware_bin = ctx.attr.image[SystemImageInfo].bin
-    else:
-        firmware_bin = ctx.file.image
+    firmware_bin = _firmware_bin(ctx)
 
     # Create upload script
     upload_script = ctx.actions.declare_file(ctx.label.name + "_upload.sh")
@@ -282,11 +353,11 @@
             doc = "Skip GPIO operations",
         ),
         "_uart_test_exec": attr.label(
-            default = "//target/ast1060-evb/harness:uart_test_exec.py",
+            default = "//target/ast10x0/harness:uart_test_exec.py",
             allow_single_file = [".py"],
         ),
     },
-    doc = """Upload firmware to AST1060 hardware via UART.
+    doc = """Upload firmware to AST10x0 hardware via UART.
 
 This is a non-test rule that just uploads firmware without monitoring.
 
diff --git a/target/ast10x0/peripherals/uart/mod.rs b/target/ast10x0/peripherals/uart/mod.rs
index ba5f96d..cec8ab1 100644
--- a/target/ast10x0/peripherals/uart/mod.rs
+++ b/target/ast10x0/peripherals/uart/mod.rs
@@ -3,8 +3,6 @@
 
 use ast1060_pac as device;
 use bitflags::bitflags;
-use core::cell::UnsafeCell;
-use core::marker::PhantomData;
 use embedded_hal_nb::serial as serial_nb;
 use embedded_io::{Read, Write};
 
@@ -102,7 +100,6 @@
 }
 pub struct Usart {
 	usart: *const device::uart::RegisterBlock,
-	_not_sync: PhantomData<UnsafeCell<()>>, // makes Usart !Sync
 }
 
 impl embedded_io::ErrorType for Usart {
@@ -245,6 +242,20 @@
 		}
 	}
 
+	/// Create an uninitialized USART instance without writing to registers.
+	///
+	/// This const function creates a Usart struct pointing to the register block
+	/// but does not perform any hardware initialization. Use this for static
+	/// initializers and call `new()` or follow with hardware initialization.
+	///
+	/// # Safety
+	///
+	/// - `usart` must be a valid, non-null pointer to the AST1060 UART register block.
+	/// - The pointed register block must remain valid for the lifetime of this `Usart`.
+	pub const unsafe fn new_uninit(usart: *const device::uart::RegisterBlock) -> Self {
+		Self { usart }
+	}
+
 	/// Create a new USART instance from a raw register-block pointer.
 	///
 	/// Configures RX/TX FIFO, 8 byte RX trigger level, 1.5MBaud, 8n1, and
@@ -259,7 +270,6 @@
 	pub unsafe fn new(usart: *const device::uart::RegisterBlock) -> Self {
 		let this = Self {
 			usart,
-			_not_sync: PhantomData,
 		};
 
 		unsafe {
diff --git a/target/ast10x0/tests/BUILD.bazel b/target/ast10x0/tests/BUILD.bazel
new file mode 100644
index 0000000..2831344
--- /dev/null
+++ b/target/ast10x0/tests/BUILD.bazel
@@ -0,0 +1,4 @@
+# Licensed under the Apache-2.0 license
+# SPDX-License-Identifier: Apache-2.0
+
+package(default_visibility = ["//visibility:public"])
diff --git a/target/ast10x0/tests/interrupts/kernel/BUILD.bazel b/target/ast10x0/tests/interrupts/kernel/BUILD.bazel
index e5f1ba4..2b3dd08 100644
--- a/target/ast10x0/tests/interrupts/kernel/BUILD.bazel
+++ b/target/ast10x0/tests/interrupts/kernel/BUILD.bazel
@@ -7,6 +7,7 @@
 load("@pigweed//pw_kernel/tooling/panic_detector:rust_binary_no_panics_test.bzl", "rust_binary_no_panics_test")
 load("@rules_rust//rust:defs.bzl", "rust_binary")
 load("//target/ast10x0:defs.bzl", "TARGET_COMPATIBLE_WITH")
+load("//target/ast10x0/harness:uart_upload_test.bzl", "uart_boot_image", "uart_upload_test")
 
 system_image(
     name = "interrupts",
@@ -17,6 +18,17 @@
     userspace = False,
 )
 
+uart_boot_image(
+    name = "interrupts_uart_image",
+    image = ":interrupts",
+)
+
+uart_upload_test(
+    name = "interrupts_uart_upload_test",
+    image = ":interrupts_uart_image",
+    test_timeout = 300,
+)
+
 system_image_test(
     name = "interrupts_test",
     image = ":interrupts",
diff --git a/target/ast10x0/tests/interrupts/user/BUILD.bazel b/target/ast10x0/tests/interrupts/user/BUILD.bazel
index af34475..0517efb 100644
--- a/target/ast10x0/tests/interrupts/user/BUILD.bazel
+++ b/target/ast10x0/tests/interrupts/user/BUILD.bazel
@@ -7,6 +7,7 @@
 load("@pigweed//pw_kernel/tooling/panic_detector:rust_binary_no_panics_test.bzl", "rust_binary_no_panics_test")
 load("@rules_rust//rust:defs.bzl", "rust_binary")
 load("//target/ast10x0:defs.bzl", "TARGET_COMPATIBLE_WITH")
+load("//target/ast10x0/harness:uart_upload_test.bzl", "uart_boot_image", "uart_upload_test")
 
 system_image(
     name = "interrupts",
@@ -20,6 +21,17 @@
     tags = ["kernel"],
 )
 
+uart_boot_image(
+    name = "interrupts_uart_image",
+    image = ":interrupts",
+)
+
+uart_upload_test(
+    name = "interrupts_uart_upload_test",
+    image = ":interrupts_uart_image",
+    test_timeout = 300,
+)
+
 system_image_test(
     name = "interrupts_test",
     image = ":interrupts",
diff --git a/target/ast10x0/tests/ipc/user/BUILD.bazel b/target/ast10x0/tests/ipc/user/BUILD.bazel
index fc1c562..6aac1fd 100644
--- a/target/ast10x0/tests/ipc/user/BUILD.bazel
+++ b/target/ast10x0/tests/ipc/user/BUILD.bazel
@@ -7,6 +7,7 @@
 load("@pigweed//pw_kernel/tooling/panic_detector:rust_binary_no_panics_test.bzl", "rust_binary_no_panics_test")
 load("@rules_rust//rust:defs.bzl", "rust_binary")
 load("//target/ast10x0:defs.bzl", "TARGET_COMPATIBLE_WITH")
+load("//target/ast10x0/harness:uart_upload_test.bzl", "uart_boot_image", "uart_upload_test")
 
 system_image(
     name = "ipc",
@@ -18,6 +19,17 @@
     target_compatible_with = TARGET_COMPATIBLE_WITH,
 )
 
+uart_boot_image(
+    name = "ipc_uart_image",
+    image = ":ipc",
+)
+
+uart_upload_test(
+    name = "ipc_uart_upload_test",
+    image = ":ipc_uart_image",
+    test_timeout = 300,
+)
+
 system_image_test(
     name = "ipc_test",
     image = ":ipc",
diff --git a/target/ast10x0/tests/threads/BUILD.bazel b/target/ast10x0/tests/threads/BUILD.bazel
new file mode 100644
index 0000000..2831344
--- /dev/null
+++ b/target/ast10x0/tests/threads/BUILD.bazel
@@ -0,0 +1,4 @@
+# Licensed under the Apache-2.0 license
+# SPDX-License-Identifier: Apache-2.0
+
+package(default_visibility = ["//visibility:public"])
diff --git a/target/ast10x0/tests/threads/kernel/BUILD.bazel b/target/ast10x0/tests/threads/kernel/BUILD.bazel
index 63ffd23..4504994 100644
--- a/target/ast10x0/tests/threads/kernel/BUILD.bazel
+++ b/target/ast10x0/tests/threads/kernel/BUILD.bazel
@@ -6,6 +6,7 @@
 load("@pigweed//pw_kernel/tooling/panic_detector:rust_binary_no_panics_test.bzl", "rust_binary_no_panics_test")
 load("@rules_rust//rust:defs.bzl", "rust_binary")
 load("//target/ast10x0:defs.bzl", "TARGET_COMPATIBLE_WITH")
+load("//target/ast10x0/harness:uart_upload_test.bzl", "uart_boot_image", "uart_upload_test")
 
 system_image(
     name = "threads",
@@ -22,6 +23,17 @@
     target_compatible_with = TARGET_COMPATIBLE_WITH,
 )
 
+uart_boot_image(
+    name = "threads_uart_image",
+    image = ":threads",
+)
+
+uart_upload_test(
+    name = "threads_uart_upload_test",
+    image = ":threads_uart_image",
+    test_timeout = 300,
+)
+
 rust_binary_no_panics_test(
     name = "no_panics_test",
     binary = ":threads",
diff --git a/target/ast10x0/tests/usart/BUILD.bazel b/target/ast10x0/tests/usart/BUILD.bazel
index 7ab85fd..20ab68e 100644
--- a/target/ast10x0/tests/usart/BUILD.bazel
+++ b/target/ast10x0/tests/usart/BUILD.bazel
@@ -8,6 +8,7 @@
 load("@pigweed//pw_kernel/tooling/panic_detector:rust_binary_no_panics_test.bzl", "rust_binary_no_panics_test")
 load("@rules_rust//rust:defs.bzl", "rust_binary")
 load("//target/ast10x0:defs.bzl", "TARGET_COMPATIBLE_WITH")
+load("//target/ast10x0/harness:uart_upload_test.bzl", "uart_boot_image", "uart_upload_test")
 
 filegroup(
     name = "system_config",
@@ -110,6 +111,17 @@
     visibility = ["//visibility:public"],
 )
 
+uart_boot_image(
+    name = "usart_uart_image",
+    image = ":usart",
+)
+
+uart_upload_test(
+    name = "usart_uart_upload_test",
+    image = ":usart_uart_image",
+    test_timeout = 300,
+)
+
 system_image_test(
     name = "usart_test",
     image = ":usart",