#!/usr/bin/env python3

# Copyright (C) 2017-2022, Xilinx, Inc.  All rights reserved.
# Copyright (C) 2022-2026, Advanced Micro Devices, Inc.  All rights reserved.
# Portions of this file consist of AI-generated content.
#
#
# SPDX-License-Identifier: MIT

# AMD FPGA Wrapper used to emulate on-chip-loader behavior, configure qemu,
# and launch qemu for system emulation

from __future__ import annotations

import os
import sys
import subprocess

try:
    from amd_boot_image_loader.zynq import ZynqBootImageLoader
    from amd_boot_image_loader.zynqmp import ZynqmpBootImageLoader
    from amd_boot_image_loader.versal import VersalBootImageLoader
    from amd_boot_image_loader.versal_2ve_2vm import Versal2VE2VMBootImageLoader
except ModuleNotFoundError:
    # Relaunch the helper and look for nativepython3 in the same directory
    nativepython3 = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'nativepython3')
    args = [ nativepython3 ] + sys.argv
    env = os.environ
    os.execve(nativepython3, args, env)
    sys.exit(1)

# The other qemu executables are expected to be in our path
binpath = os.path.dirname(os.path.abspath(__file__))

VERSION = "2026.1"
SUPPORTED_ARCHS = [ 'zynq' , 'zynqmp', 'versal', 'versal2ve2vm']
SUPPORTED_MODES = { 'zynq' :        [ ( '1', 'QSPI' ), ( '5', 'SD0' ) ],
                    'zynqmp':       [ ( '1', 'QSPI' ), ( '2', 'QSPI' ), ( '3', 'SD0' ), ( '5', 'SD1' ), ( '6', 'eMMC1' ), ( '14', 'SD1' )  ],
                    'versal':       [ ( '1', 'QSPI' ), ( '2', 'QSPI' ), ( '3', 'SD0' ), ( '5', 'SD1' ), ( '6', 'eMMC1' ), ( '8', 'OSPI'), ( '14', 'SD1' )],
                    'versal2ve2vm': [ ( '1', 'QSPI' ), ( '2', 'QSPI' ), ( '3', 'SD0' ), ( '5', 'SD1' ), ( '6', 'eMMC1' ), ( '8', 'OSPI'), ( '11', 'UFS' ), ( '14', 'SD1' )],
                    }
QEMU_APU_ARGS = []
QEMU_PMU_ARGS = []
QEMU_PLM_ARGS = []
QEMU_ASU_ARGS = []

def get_boot_mode_name(boot_arch: str, boot_mode: str) -> str | None:
    """
    Get the boot mode description for a given architecture and mode.
    
    Args:
        boot_arch: Boot architecture (e.g., 'zynq')
        boot_mode: Boot mode number (e.g., '8', '12')
        
    Returns:
        str: Boot mode description (e.g., 'QSPI', 'SD0') or None if not found
    """
    if boot_arch not in SUPPORTED_MODES:
        return None
    
    for mode, desc in SUPPORTED_MODES[boot_arch]:
        if mode == boot_mode:
            return desc
    
    return None


def print_version() -> None:
    """Print version information."""
    print(f"qemu-system-amd-fpga-multiarch version {VERSION}")
    sys.exit(0)


def print_usage() -> None:
    """Print usage information."""
    # Build the boot modes section
    boot_modes_text = ""
    for arch in SUPPORTED_ARCHS:
        if arch in SUPPORTED_MODES:
            modes_list = [f"{mode} ({desc})" for mode, desc in SUPPORTED_MODES[arch]]
            boot_modes_text += f"\n                             {arch}: {', '.join(modes_list)}"
    
    usage_text = f"""
qemu-system-amd-fpga-multiarch version {VERSION}

Usage: qemu-system-amd-fpga-multiarch [OPTIONS]

Required Options:
  -boot arch=<arch>        Specify boot architecture (mandatory)
                           Supported: {', '.join(SUPPORTED_ARCHS)}
  -boot mode=<num>         Specify boot mode number (mandatory){boot_modes_text}

  -pmu-args <args>         Arguments passed to PMU emulation (zynqmp only)
  -plm-args <args>         Arguments passed to PLM emulation (versal, versal2ve2vm)
  -asu-args <args>         Arguments passed to ASU emulation (versal2ve2vm only)
                           Note: -pmu-args cannot be used with -plm-args or -asu-args
Optional Arguments:
  -boot multiboot=<value>  Specify multiboot value (int or hex)
  -boot pmufw              Default zynqmp behavior where FSBL is skipped and
                           pmufw plus applications are directly loaded to memory
                           (zynqmp only).
  -boot fsbl               Boot via FSBL so QEMU follows the hardware-like flow
                           from boot.bin (zynqmp only).
  -kernel <path>           Specify boot.bin path, used to verify extracted boot.bin
  -nokernel                Skip boot.bin validation and remove -kernel setting
  -dtb <path>              Specify APU 'hw-dtb' device tree path
  -machine-path <path>     Specify machine working directory
  --version                Display version information
  -h, -help, --help        Display this help message

All other options are passed through to the underlying QEMU system.
"""
    print(usage_text)
    sys.exit(0)


def parse_boot_options(boot_options: list[str]) -> dict[str, str | bool]:
    """
    Parse boot options in key=value format.
    
    Args:
        boot_options: List of boot option strings
        
    Returns:
        dict: Dictionary of parsed boot options
    """
    boot_config = {}
    
    for option in boot_options:
        if '=' in option:
            key, value = option.split('=', 1)
            boot_config[key.strip()] = value.strip()
        else:
            # Option without value, just store as flag
            boot_config[option.strip()] = True
    
    return boot_config


def parse_arguments(args: list[str]) -> tuple[dict[str, str | None], list[str]]:
    """
    Parse command line arguments, handling known arguments and preserving unknown ones.
    
    Known arguments:
    - "-boot <option>": Boot options (key=value format)
      Examples: -boot arch=zynq -boot mode=1 -boot multiboot=0x1000
    - "-kernel <path>": Kernel image path
    - "-nokernel": Skip kernel validation and remove -kernel value
    - "-dtb <path>": APU device tree path
    - "--version": Display version
    - "-h", "-help", "--help": Display help
    
    Returns:
        tuple: (known_args dict, unknown_args list)
    """
    known_args = {
        'boot_mode': None,
        'kernel': None,
        'nokernel': False,
        'boot_arch': None,
        'boot_multiboot': None,
        'boot_pmufw': False,
        'boot_fsbl': False,
        'dtb': None,
        'machine_path': None,
        'pmu_args': None,
        'plm_args': None,
        'asu_args': None
    }
    unknown_args = []
    boot_options = []
    
    i = 0
    while i < len(args):
        arg = args[i]
        
        # Handle help flags
        if arg in ['-h', '-help', '--help']:
            print_usage()
        
        # Handle version flag
        if arg == '--version':
            print_version()
        
        if arg == '-boot':
            # Collect boot options
            if i + 1 < len(args):
                boot_options.append(args[i + 1])
                i += 2
                continue
            else:
                # No value provided, treat as unknown
                unknown_args.append(arg)
                i += 1
        elif arg == '-kernel':
            # Handle -kernel <path>
            if i + 1 < len(args):
                known_args['kernel'] = args[i + 1]
                i += 2
                continue
            else:
                # No value provided, treat as unknown
                unknown_args.append(arg)
                i += 1
        elif arg == '-nokernel':
            # Handle -nokernel flag
            known_args['nokernel'] = True
            i += 1
            continue
        elif arg == '-dtb':
            # Handle -dtb <path>
            if i + 1 < len(args):
                known_args['dtb'] = args[i + 1]
                i += 2
                continue
            else:
                # No value provided, treat as unknown
                unknown_args.append(arg)
                i += 1
        elif arg == '-machine-path':
            # Handle -machine-path <path>
            if i + 1 < len(args):
                known_args['machine_path'] = args[i + 1]
                i += 2
                continue
            else:
                # No value provided, treat as unknown
                unknown_args.append(arg)
                i += 1
        elif arg == '-pmu-args':
            # Handle -pmu-args <args>
            if i + 1 < len(args):
                known_args['pmu_args'] = args[i + 1]
                i += 2
                continue
            else:
                # No value provided, treat as unknown
                unknown_args.append(arg)
                i += 1
        elif arg == '-plm-args':
            # Handle -plm-args <args>
            if i + 1 < len(args):
                known_args['plm_args'] = args[i + 1]
                i += 2
                continue
            else:
                # No value provided, treat as unknown
                unknown_args.append(arg)
                i += 1
        elif arg == '-asu-args':
            # Handle -asu-args <args>
            if i + 1 < len(args):
                known_args['asu_args'] = args[i + 1]
                i += 2
                continue
            else:
                # No value provided, treat as unknown
                unknown_args.append(arg)
                i += 1
        else:
            # Unknown argument, preserve it
            unknown_args.append(arg)
            i += 1
    
    # Process collected boot options
    if boot_options:
        boot_config = parse_boot_options(boot_options)
        if 'arch' in boot_config:
            known_args['boot_arch'] = boot_config['arch']
        if 'mode' in boot_config:
            known_args['boot_mode'] = boot_config['mode']
        if 'multiboot' in boot_config:
            known_args['boot_multiboot'] = boot_config['multiboot']
        if 'pmufw' in boot_config:
            known_args['boot_pmufw'] = True
        if 'fsbl' in boot_config:
            known_args['boot_fsbl'] = True

    # Boot flow flags are mutually exclusive.
    if known_args.get('boot_pmufw') and known_args.get('boot_fsbl'):
        print("Error: -boot pmufw and -boot fsbl cannot be used together", file=sys.stderr)
        sys.exit(1)
     
    # If -nokernel is set, remove the -kernel value
    if known_args.get('nokernel'):
        known_args['kernel'] = None

    # Check for mutual exclusivity of -pmu-args with -plm-args and -asu-args
    if known_args.get('pmu_args') and known_args.get('plm_args'):
        print(f"Error: -pmu-args and -plm-args cannot be used together", file=sys.stderr)
        sys.exit(1)
    
    if known_args.get('pmu_args') and known_args.get('asu_args'):
        print(f"Error: -pmu-args and -asu-args cannot be used together", file=sys.stderr)
        sys.exit(1)
    
    # Handle PMU arguments (only valid for zynqmp)
    if known_args.get('pmu_args'):
        if known_args['boot_arch'] == 'zynqmp':
            known_pmu_args, unknown_pmu_args = parse_pmu_arguments(known_args['pmu_args'])
            # Store known PMU args back into known_args for later use
            known_args['pmu_hw_dtb'] = known_pmu_args.get('hw_dtb')
            known_args['pmu_kernel'] = known_pmu_args.get('kernel')
            known_args['pmu_unknown'] = unknown_pmu_args
        else:
            print(f"Error: PMU arguments are only supported for zynqmp", file=sys.stderr)
            sys.exit(1)
    
    # Handle PLM arguments (only valid for versal and versal2ve2vm)
    if known_args.get('plm_args'):
        if known_args['boot_arch'] in ['versal', 'versal2ve2vm']:
            known_plm_args, unknown_plm_args = parse_plm_arguments(known_args['plm_args'])
            # Store known PLM args back into known_args for later use
            known_args['plm_hw_dtb'] = known_plm_args.get('hw_dtb')
            known_args['plm_unknown'] = unknown_plm_args
        else:
            print(f"Error: PLM arguments are only supported for versal and versal2ve2vm", file=sys.stderr)
            sys.exit(1)
    
    # Handle ASU arguments (only valid for versal2ve2vm)
    if known_args.get('asu_args'):
        if known_args['boot_arch'] == 'versal2ve2vm':
            known_asu_args, unknown_asu_args = parse_asu_arguments(known_args['asu_args'])
            # Store known ASU args back into known_args for later use
            known_args['asu_hw_dtb'] = known_asu_args.get('hw_dtb')
            known_args['asu_unknown'] = unknown_asu_args
        else:
            print(f"Error: ASU arguments are only supported for versal2ve2vm", file=sys.stderr)
            sys.exit(1)

    return known_args, unknown_args

def parse_pmu_arguments(args_string: str) -> tuple[dict[str, str | None], list[str]]:
    """
    Parse PMU command line arguments, handling known arguments and preserving unknown ones.
    
    Known PMU arguments:
    - "-hw-dtb <path>": Hardware device tree path
    - "-kernel <path>": PMU ROM path
    
    Args:
        args_string: String containing PMU arguments
        
    Returns:
        tuple: (known_pmu_args dict, unknown_pmu_args list)
    """
    known_pmu_args = {
        'hw_dtb': None,
        'kernel': None
    }
    unknown_pmu_args = []
    
    # Split the args string into a list
    args = args_string.split()
    
    i = 0
    while i < len(args):
        arg = args[i]
        
        if arg == '-hw-dtb':
            # Handle -hw-dtb <path>
            if i + 1 < len(args):
                known_pmu_args['hw_dtb'] = args[i + 1]
                i += 2
                continue
            else:
                # No value provided, treat as unknown
                unknown_pmu_args.append(arg)
                i += 1
        elif arg == '-kernel':
            # Handle -kernel <path>
            if i + 1 < len(args):
                known_pmu_args['kernel'] = args[i + 1]
                i += 2
                continue
            else:
                # No value provided, treat as unknown
                unknown_pmu_args.append(arg)
                i += 1
        else:
            # Unknown argument, preserve it
            unknown_pmu_args.append(arg)
            i += 1
    
    return known_pmu_args, unknown_pmu_args


def parse_plm_arguments(args_string: str) -> tuple[dict[str, str | None], list[str]]:
    """Parse PLM command line arguments, handling known arguments and preserving unknown ones.
    
    Known PLM arguments:
    - "-hw-dtb <path>": Hardware device tree path
    
    Args:
        args_string: String containing PLM arguments
        
    Returns:
        tuple: (known_plm_args dict, unknown_plm_args list)
    """
    known_plm_args = {
        'hw_dtb': None
    }
    unknown_plm_args = []
    
    # Split the args string into a list
    args = args_string.split()
    
    i = 0
    while i < len(args):
        arg = args[i]
        
        if arg == '-hw-dtb':
            # Handle -hw-dtb <path>
            if i + 1 < len(args):
                known_plm_args['hw_dtb'] = args[i + 1]
                i += 2
                continue
            else:
                # No value provided, treat as unknown
                unknown_plm_args.append(arg)
                i += 1
        else:
            # Unknown argument, preserve it
            unknown_plm_args.append(arg)
            i += 1
    
    return known_plm_args, unknown_plm_args


def parse_asu_arguments(args_string: str) -> tuple[dict[str, str | None], list[str]]:
    """Parse ASU command line arguments, handling known arguments and preserving unknown ones.
    
    Known ASU arguments:
    - "-hw-dtb <path>": Hardware device tree path
    
    Args:
        args_string: String containing ASU arguments
        
    Returns:
        tuple: (known_asu_args dict, unknown_asu_args list)
    """
    known_asu_args = {
        'hw_dtb': None
    }
    unknown_asu_args = []
    
    # Split the args string into a list
    args = args_string.split()
    
    i = 0
    while i < len(args):
        arg = args[i]
        
        if arg == '-hw-dtb':
            # Handle -hw-dtb <path>
            if i + 1 < len(args):
                known_asu_args['hw_dtb'] = args[i + 1]
                i += 2
                continue
            else:
                # No value provided, treat as unknown
                unknown_asu_args.append(arg)
                i += 1
        else:
            # Unknown argument, preserve it
            unknown_asu_args.append(arg)
            i += 1
    
    return known_asu_args, unknown_asu_args


def parse_drive_arguments(args: list[str]) -> dict[str, list[dict[str, str]]]:
    """
    Parse -drive and -device arguments from QEMU command line arguments.
    
    Extracts all -drive options and correlates them with -device specifications
    to determine the actual device type. Uses the 'id' field to match drives
    with their corresponding devices.
    
    Args:
        args: List of command line arguments
        
    Returns:
        dict: Dictionary with interface types as keys, each containing a list of drive configs
              Example: {
                  'sd': [{'file': 'sd.img', 'format': 'raw', 'id': 'sdcard', 'device_type': 'sd-card'}],
                  'virtio': [{'file': 'disk.img', 'id': 'mydrive', 'device_type': 'virtio-blk-device'}],
                  'mtd': [{'file': 'flash.bin', 'format': 'raw'}]
              }
    """
    drives = {
        'mtd': [],
        'sd': [],
        'scsi': [],
        'ide': [],
        'virtio': [],
        'floppy': [],
        'pflash': [],
        'none': [],
        'other': []
    }
    
    # First pass: collect all drives by their id
    drives_by_id = {}
    
    i = 0
    while i < len(args):
        if args[i] == '-drive':
            if i + 1 < len(args):
                drive_spec = args[i + 1]
                drive_config = {}
                
                # Parse key=value pairs in the drive specification
                # Handle comma-separated options
                for option in drive_spec.split(','):
                    if '=' in option:
                        key, value = option.split('=', 1)
                        drive_config[key.strip()] = value.strip()
                
                # Store drive by id if present
                if 'id' in drive_config:
                    drives_by_id[drive_config['id']] = drive_config
                
                # Categorize by interface type (if= parameter)
                interface = drive_config.get('if', 'none')
                
                if interface in drives:
                    drives[interface].append(drive_config)
                else:
                    # Unknown interface type, add to 'other'
                    drives['other'].append(drive_config)
                
                i += 2
            else:
                i += 1
        else:
            i += 1
    
    # Second pass: find -device arguments and correlate with drives
    i = 0
    while i < len(args):
        if args[i] == '-device':
            if i + 1 < len(args):
                device_spec = args[i + 1]
                device_config = {}
                device_type = None
                
                # Parse device specification
                # First part is the device type
                parts = device_spec.split(',')
                if parts:
                    device_type = parts[0].strip()
                    
                    # Parse remaining key=value pairs
                    for option in parts[1:]:
                        if '=' in option:
                            key, value = option.split('=', 1)
                            device_config[key.strip()] = value.strip()
                
                # If this device references a drive by id, update the drive info
                if 'drive' in device_config:
                    drive_id = device_config['drive']
                    if drive_id in drives_by_id:
                        drive = drives_by_id[drive_id]
                        drive['device_type'] = device_type
                        # Copy all device properties to drive (bus, channel, scsi-id, lun, logical_block_size, etc.)
                        for key, value in device_config.items():
                            if key != 'drive':  # Don't override the drive id reference
                                drive[key] = value
                        
                        # Re-categorize based on device type
                        # Remove from original category
                        original_if = drive.get('if', 'none')
                        if original_if in drives and drive in drives[original_if]:
                            drives[original_if].remove(drive)
                        
                        # Determine new category from device type
                        new_category = categorize_device_type(device_type)
                        if new_category in drives:
                            drives[new_category].append(drive)
                        else:
                            drives['other'].append(drive)
                
                i += 2
            else:
                i += 1
        else:
            i += 1
    
    # Return only drive types that have entries
    return {k: v for k, v in drives.items() if v}


def categorize_device_type(device_type: str) -> str:
    """
    Categorize a QEMU device type into a storage interface category.
    
    Args:
        device_type: QEMU device type string (e.g., 'virtio-blk-device', 'sd-card')
        
    Returns:
        str: Category name (mtd, sd, scsi, ide, virtio, etc.)
    """
    device_type_lower = device_type.lower()
    
    if 'sd-card' in device_type_lower or 'sdhci' in device_type_lower:
        return 'sd'
    elif 'virtio' in device_type_lower:
        return 'virtio'
    elif 'scsi' in device_type_lower:
        return 'scsi'
    elif 'ide' in device_type_lower:
        return 'ide'
    elif 'mtd' in device_type_lower or 'pflash' in device_type_lower or 'cfi' in device_type_lower:
        return 'mtd'
    elif 'floppy' in device_type_lower:
        return 'floppy'
    elif 'nvme' in device_type_lower:
        return 'nvme'
    else:
        return 'other'


def get_mtd_contents(mtd_file: str | None, kernel_file: str | None, boot_arch: str, machine_path: str, multiboot: int = 0, loader = None, mtd_file2: str | None = None) -> int:
    """Extract boot.bin from MTD file and optionally verify it matches the kernel file.
    
    Searches for a valid boot.bin at multiboot offsets (32KB each) in the MTD file.
    Extracts boot.bin from MTD file to machine_path.
    
    Args:
        mtd_file: Path to the MTD device file
        kernel_file: Path to the kernel file specified with -kernel (optional, if None skips verification)
        boot_arch: Boot architecture (for magic verification)
        machine_path: Path to machine working directory for extraction
        multiboot: Starting multiboot offset (default 0, or user-specified value)
        loader: boot image loader class for the arch, used to verify magic
        mtd_file2: Path to second MTD device file for split/striped configurations (optional)
        
    Returns:
        int: Multiboot value of loaded boot image, or -1 on failure
    """
    from spi_interleave import unstripe
    
    # Check if files exist
    if not mtd_file or not machine_path:
        return -1
    
    if not os.path.exists(mtd_file):
        print(f"Error: MTD file '{mtd_file}' not found", file=sys.stderr)
        return -1
    
    # Handle striped/split MTD configurations
    if mtd_file2:
        if not os.path.exists(mtd_file2):
            print(f"Error: Second MTD file '{mtd_file2}' not found", file=sys.stderr)
            return -1
        
        # Unstripe the two MTD files into a single file in machine_path
        def create_progress_callback(total_size: int, label: str = "Progress"):
            """Create a callback that displays current offset / total file size."""
            def progress_callback(offset: int) -> None:
                # The offset in an unstripe operation is only for one of the input file
                percentage = 100.0 * (offset * 2) / total_size if total_size > 0 else 0
                print(f"\r{label}: {offset * 2} / {total_size} bytes ({percentage:.1f}%)", 
                      end='', file=sys.stderr)
                if offset * 2 >= total_size:
                    print(file=sys.stderr)  # Newline at end
            return progress_callback

        unstriped_file = os.path.join(machine_path, 'mtd_unstriped.bin')
        print(f"Unstriping MTD files '{mtd_file}' and '{mtd_file2}' to '{unstriped_file}'")
        
        mtd_file_size = os.path.getsize(mtd_file) if os.path.exists(mtd_file) else 0
        mtd_file2_size = os.path.getsize(mtd_file2) if os.path.exists(mtd_file2) else 0
        if mtd_file_size != mtd_file2_size:
            print(f"Error: MTD files are not the same size {mtd_file_size} != {mtd_file2_size}", file=sys.stderr)
            return -1

        total_mtd_size = mtd_file_size + mtd_file2_size
        callback = create_progress_callback(total_mtd_size, "Unstriping")
        if not unstripe([mtd_file, mtd_file2], unstriped_file, status_callback=callback):
            print(f"Error: Failed to unstripe MTD files", file=sys.stderr)
            return -1
        
        # Use the unstriped file for all subsequent processing
        mtd_file = unstriped_file
        print(f"Using unstriped MTD file for boot image extraction")
    
    if kernel_file and not os.path.exists(kernel_file):
        print(f"Error: Kernel file '{kernel_file}' not found", file=sys.stderr)
        return -1
    
    if not loader:
        print(f"Error: Loader class not provided for architecture '{boot_arch}'", file=sys.stderr)
        return -1

    # Search for valid boot.bin starting at multiboot offset
    current_multiboot = multiboot if isinstance(multiboot, int) else 0
    multiboot_offset_size = 32 * 1024  # 32KB per multiboot offset
    
    # Calculate maximum number of offsets based on file size
    mtd_file_size = os.path.getsize(mtd_file)
    max_multiboot = mtd_file_size // multiboot_offset_size
    
    # Try to find a valid boot image at different multiboot offsets
    bootbin = None
    starting_multiboot = current_multiboot
    while current_multiboot < max_multiboot:
        offset = current_multiboot * multiboot_offset_size
        try:
            bootbin = loader(mtd_file, offset)
            bootbin.open()
            # Successfully opened and verified magic
            bootbin.close()
            break
        except (ValueError, Exception) as e:
            # Invalid magic or other error, close if opened and try next offset
            if bootbin:
                try:
                    bootbin.close()
                except Exception:
                    pass
            bootbin = None
            current_multiboot += 1
    else:
        print(f"Error: No valid boot image found in MTD file (searched {starting_multiboot} to {max_multiboot-1})", file=sys.stderr)
        return -1
    
    # Valid boot image found at current offset
    if current_multiboot != multiboot:
        print(f"Note: Valid boot image found at multiboot {current_multiboot} (searched from {multiboot})")
        
    # Determine the size of the boot image to extract
    if kernel_file:
        # If kernel provided, use its size for comparison and extraction
        boot_image_size = os.path.getsize(kernel_file)
        
        # Compare kernel file against MTD file contents at this offset
        # Only compare the size of the kernel file; ignore any additional data in MTD
        with open(mtd_file, 'rb') as f1, open(kernel_file, 'rb') as f2:
            f1.seek(offset)
            bytes_compared = 0
            while (kernel_contents := f2.read(4096)):
                boot_contents = f1.read(len(kernel_contents))
                
                if len(boot_contents) < len(kernel_contents):
                    print(f"Error: MTD file too short at offset {offset} (need {boot_image_size} bytes, found {bytes_compared + len(boot_contents)})", file=sys.stderr)
                    return -1
                
                if boot_contents != kernel_contents:
                    print(f"Error: boot.bin does not match kernel file at offset {offset + bytes_compared}", file=sys.stderr)
                    return -1
                
                bytes_compared += len(kernel_contents)
    else:
        # No kernel provided - use loader to parse and determine size
        try:
            bootbin = loader(mtd_file, offset)
            bootbin.open()
            bootbin.parse()
            
            # Calculate total size from partition headers
            # Find the maximum offset + size from all partitions
            max_end_offset = 0
            
            for image_header in bootbin.image_headers:
                for partition_header in image_header.partition_headers:
                    # Get partition data offset and length
                    data_offset = partition_header.get_data_offset_bytes()
                    partition_length = partition_header.get_total_partition_length_bytes()
                    end_offset = data_offset + partition_length
                    
                    if end_offset > max_end_offset:
                        max_end_offset = end_offset
            
            # Use the maximum end offset as the boot image size
            if max_end_offset == 0:
                print(f"Error: Could not determine boot image size from partition headers", file=sys.stderr)
                return -1
            
            boot_image_size = max_end_offset
            bootbin.close()
            
        except Exception as e:
            print(f"Error: Could not parse boot image to determine size: {e}", file=sys.stderr)
            if bootbin:
                bootbin.close()
            return -1
    
    # Extract boot.bin from MTD file at the found offset
    boot_bin_dest = os.path.join(machine_path, 'boot.bin')
    try:
        print(f"Extracting boot.bin from MTD offset {offset} to {boot_bin_dest} ({boot_image_size} bytes)")
        with open(mtd_file, 'rb') as f_src, open(boot_bin_dest, 'wb') as f_dst:
            f_src.seek(offset)
            bytes_remaining = boot_image_size
            chunk_size = 4096
            while bytes_remaining > 0:
                chunk = f_src.read(min(chunk_size, bytes_remaining))
                if not chunk:
                    break
                f_dst.write(chunk)
                bytes_remaining -= len(chunk)
    except Exception as e:
        print(f"Error extracting boot.bin from MTD file: {e}", file=sys.stderr)
        return -1

    if kernel_file:
        print(f"Extracted boot.bin matches -kernel file")
    else:
        print(f"Extracted boot.bin without verification")

    return current_multiboot


def get_sd_contents(sd_file: str | None, kernel_file: str | None, machine_path: str, multiboot: str | int = 'none', sector_size: int | None = None) -> int:
    """Extract boot.bin from SD partition 1 and optionally verify it matches the kernel file.
    
    Args:
        sd_file: Path to the SD card image file
        kernel_file: Path to the kernel file specified with -kernel (optional, if None skips verification)
        machine_path: Path to machine working directory for extraction
        multiboot: Multiboot value ('none' for strict boot.bin only, or numeric value)
        sector_size: Logical block size for wic commands (optional)
        
    Returns:
        int: Multiboot value of loaded boot image, or -1 on failure
    """
    if not sd_file or not machine_path:
        return -1
    
    try:
        # Check if files exist
        if not os.path.exists(sd_file):
            print(f"Error: SD file '{sd_file}' not found", file=sys.stderr)
            return -1
        
        if kernel_file and not os.path.exists(kernel_file):
            print(f"Error: Kernel file '{kernel_file}' not found", file=sys.stderr)
            return -1
        
        # Extract boot.bin from SD image
        boot_bin_dest = os.path.join(machine_path, 'boot.bin')
        result = extract_boot_bin_from_disk(sd_file, boot_bin_dest, multiboot=multiboot, sector_size=sector_size)
        
        if result is None:
            print(f"Error: Filesystem is missing boot.bin", file=sys.stderr)
            return -1
        
        # Compare extracted boot.bin with kernel file if provided
        if kernel_file:
            with open(boot_bin_dest, 'rb') as f1, open(kernel_file, 'rb') as f2:
                boot_contents = f1.read()
                kernel_contents = f2.read()
                
                if boot_contents == kernel_contents:
                    print(f"Extracted boot.bin matches -kernel file")
                    return result
                else:
                    print(f"Error: boot.bin does not match -kernel file", file=sys.stderr)
                    return -1
        else:
            print(f"Extracted boot.bin without verification")
            return result
    
    except Exception as e:
        print(f"Error verifying SD boot.bin: {e}", file=sys.stderr)
        return -1


def extract_boot_bin_from_disk(disk_image: str, destination: str, multiboot: int | str = 0, sector_size: int | None = None) -> int | None:
    """Extract boot.bin from disk image using wic tool.
    
    Args:
        disk_image: Path to the disk image file
        destination: Destination path where the file should be copied
        multiboot: Multiboot value (0 for boot.bin, 1-8190 for boot<num>.bin, 'none' for strict mode)
        sector_size: Logical block size to pass to wic commands (optional)
        
    Returns:
        int: Multiboot number of the file found (0 for boot.bin), or None if not found
    """
    import subprocess
    import re
    
    # Check if disk image exists
    if not os.path.exists(disk_image):
        print(f"Error: Disk image '{disk_image}' not found", file=sys.stderr)
        return None
    
    # Generate filename based on multiboot value
    def get_boot_filename(mb_value):
        """Generate boot filename from multiboot value."""
        if mb_value == 0:
            return "boot.bin"
        elif 1 <= mb_value <= 8191:
            return f"boot{mb_value:04d}.bin"
        else:
            return None
    
    # Get directory listing from partition 1
    try:
        wic_ls_cmd = ['wic', 'ls']
        if sector_size is not None:
            wic_ls_cmd.extend(['--sector-size', str(sector_size)])
        wic_ls_cmd.append(f'{disk_image}:1')
        
        result = subprocess.run(
            wic_ls_cmd,
            capture_output=True,
            text=True,
            check=True
        )
        file_listing = result.stdout
    except subprocess.CalledProcessError as e:
        print(f"Error listing disk image contents: {e}", file=sys.stderr)
        if e.stderr:
            print(e.stderr, file=sys.stderr)
        return None
    except FileNotFoundError:
        print(f"Error: 'wic' tool not found in PATH", file=sys.stderr)
        return None
    
    # Parse available boot*.bin files from listing
    # The listing is in DOS/FAT format like:
    # BOOT     BIN      1234 2011-04-05  23:00
    # or
    # BOOT0002 BIN      5678 2011-04-05  23:00
    available_files = {}
    
    for line in file_listing.split('\n'):
        # Skip header and empty lines
        line = line.strip()
        if not line or 'Volume' in line or 'Directory' in line or 'bytes' in line:
            continue
        
        # Match DOS 8.3 format: "BOOT     BIN" or "BOOT0002 BIN"
        # Also handle case where it might show as a single word
        match = re.match(r'^(BOOT(\d{4})?)(\s+BIN|\s*\.BIN)', line, re.IGNORECASE)
        if match:
            if match.group(2):  # BOOT####
                mb_num = int(match.group(2))
                available_files[mb_num] = f"boot{match.group(2)}.bin"
            else:  # BOOT (which is boot.bin)
                available_files[0] = "boot.bin"
    
    # Handle strict mode (multiboot='none')
    if multiboot == 'none':
        if 0 not in available_files:
            print(f"Error: boot.bin not found in {disk_image}:1 (strict mode)", file=sys.stderr)
            return None
        target_file = available_files[0]
        target_mb = 0
    else:
        # Convert multiboot to int if string
        if isinstance(multiboot, str):
            try:
                multiboot = int(multiboot)
            except ValueError:
                print(f"Error: Invalid multiboot value '{multiboot}'", file=sys.stderr)
                return None
        
        # Validate multiboot range
        if multiboot < 0 or multiboot > 8190:
            print(f"Error: Multiboot value {multiboot} out of range (0-8190)", file=sys.stderr)
            return None
        
        # Find requested file or next available
        target_mb = None
        target_file = None
        
        # Check if requested file exists
        if multiboot in available_files:
            target_mb = multiboot
            target_file = available_files[multiboot]
        else:
            # Search for next available larger number
            for mb_num in sorted(available_files.keys()):
                if mb_num >= multiboot:
                    target_mb = mb_num
                    target_file = available_files[mb_num]
                    break
        
        if target_file is None:
            requested_name = get_boot_filename(multiboot)
            print(f"Error: {requested_name} not found in {disk_image}:1", file=sys.stderr)
            if available_files:
                print(f"Available boot files: {', '.join(available_files.values())}", file=sys.stderr)
            else:
                print(f"No boot*.bin files found in disk image", file=sys.stderr)
            return None
        
        if target_mb != multiboot:
            print(f"Note: Requested multiboot {multiboot} not found, using {target_mb} ({target_file})")
    
    # Copy the file using wic
    try:
        print(f"Extracting {target_file} from {disk_image}:1/{target_file} to {destination}...")
        wic_cp_cmd = ['wic', 'cp']
        if sector_size is not None:
            wic_cp_cmd.extend(['--sector-size', str(sector_size)])
        wic_cp_cmd.extend([f'{disk_image}:1/{target_file}', destination])
        
        subprocess.run(
            wic_cp_cmd,
            check=True,
            capture_output=True,
            text=True
        )
        return target_mb
    except subprocess.CalledProcessError as e:
        print(f"Error copying {target_file} from disk image: {e}", file=sys.stderr)
        if e.stderr:
            print(e.stderr, file=sys.stderr)
        return None


def configure_zynq(known_args: dict[str, str | None], unknown_args: list[str], drives: dict[str, list[dict[str, str]]], machine_path: str) -> None:
    """Configure QEMU for Zynq7000 SoC.
    
    Configures the Zynq7000 boot process by:
    1. Selecting the appropriate boot file based on boot mode (QSPI or SD0)
    2. Extracting and verifying boot.bin from the boot device
    3. Parsing boot.bin to extract individual partition files
    4. Generating QEMU loader arguments for each partition
    5. Configuring Zynq-specific registers (SLCR, clocks, OCM)
    
    Sets the global APU_ARGS variable with complete QEMU command arguments.
    Exits on error if boot file cannot be found or processed.
    
    Args:
        known_args: Dictionary of known arguments (boot_arch, boot_mode, kernel, dtb, etc.)
        unknown_args: List of unknown arguments to pass through to QEMU
        drives: Dictionary of parsed drive configurations by interface type
        machine_path: Path to machine working directory for extracted files
    """
    
    # Get boot mode description
    boot_mode_name = get_boot_mode_name(known_args['boot_arch'], known_args.get('boot_mode'))
    
    # Select boot file based on boot mode
    boot_file = None
    boot_file2 = None
    boot_drive = None  # Store the drive object to access properties like logical_block_size
    
    if boot_mode_name == 'QSPI':
        # QSPI boot uses MTD device
        # QSPI: index 0 alone = single flash, index 0+1 = split/striped flash
        if 'mtd' in drives and len(drives['mtd']) > 0:
            # Find MTD drive with index 0 and 1
            mtd0_drive = None
            mtd1_drive = None
            for drive in drives['mtd']:
                if drive.get('index') == '0':
                    mtd0_drive = drive
                elif drive.get('index') == '1':
                    mtd1_drive = drive
            
            # Determine boot file based on QSPI
            if mtd0_drive:
                boot_file = mtd0_drive.get('file')
                # Check for split QSPI flash (index 0 + 1)
                if mtd1_drive:
                    boot_file2 = mtd1_drive.get('file')
                    print(f"Using MTD device with index 0 for QSPI (lower) boot: {boot_file}")
                    print(f"Using MTD device with index 1 for QSPI (upper) boot: {boot_file2}")
                else:
                    # Single QSPI flash (only index 0)
                    print(f"Using MTD device with index 0 for QSPI boot: {boot_file}")
            else:
                # Fallback to first MTD device if no index specified
                boot_file = drives['mtd'][0].get('file')
                print(f"Using first MTD device for QSPI boot: {boot_file}")
        else:
            print("Error: QSPI boot mode selected but no MTD device found", file=sys.stderr)
            sys.exit(1)
    
    elif boot_mode_name == 'SD0':
        # SD0 boot uses SD device with index 0
        if 'sd' in drives and len(drives['sd']) > 0:
            # Find the drive with index 0
            sd0_drive = None
            for drive in drives['sd']:
                if drive.get('index') == '0':
                    sd0_drive = drive
                    break
            
            if sd0_drive:
                boot_file = sd0_drive.get('file')
                boot_drive = sd0_drive  # Store drive for logical_block_size
                print(f"Using SD device with index 0 for SD0 boot: {boot_file}")
            else:
                # Fallback to first SD device if no index specified
                boot_drive = drives['sd'][0]
                boot_file = boot_drive.get('file')
                print(f"Using first SD device for SD0 boot: {boot_file}")
        else:
            print("Error: SD0 boot mode selected but no SD device found", file=sys.stderr)
            sys.exit(1)
    # Verify boot file contents if kernel is specified
    kernel_file = known_args.get('kernel')
    if not boot_file:
        boot_file = kernel_file

    if not boot_file:
        print(f"Error: unable to find a valid boot file")
        sys.exit(1)
    else:
        # Get user-specified multiboot value (default to 0)
        user_multiboot = known_args.get('boot_multiboot')
        multiboot_val = 0
        if user_multiboot is not None:
            try:
                multiboot_val = int(user_multiboot, 0)  # Support hex and decimal
            except ValueError:
                print(f"Warning: Invalid multiboot value '{user_multiboot}', using 0", file=sys.stderr)
                multiboot_val = 0
        
        if boot_mode_name == 'QSPI':
            # Pass boot architecture for magic verification
            actual_multiboot = get_mtd_contents(boot_file, kernel_file, known_args['boot_arch'], machine_path, multiboot=multiboot_val, loader=ZynqBootImageLoader, mtd_file2=boot_file2)
            if actual_multiboot == -1:
                sys.exit(1)
            if actual_multiboot != multiboot_val:
                multiboot_val = actual_multiboot
        elif boot_mode_name == 'SD0':
            # For Zynq7000, only boot.bin is supported (multiboot='none')
            sector_size = None
            if boot_drive and 'logical_block_size' in boot_drive:
                sector_size = int(boot_drive['logical_block_size'])
            actual_multiboot = get_sd_contents(boot_file, kernel_file, machine_path, multiboot='none', sector_size=sector_size)
            if actual_multiboot == -1:
                sys.exit(1)
            if actual_multiboot != multiboot_val:
                multiboot_val = actual_multiboot

    apu_args = ""
    # See UG585 for register information
    apu_args += " -device loader,addr=0xf8000008,data=0xdf0d,data-len=4" # SLCR_UNLOCK, the value of 0xdf0d enables writes to registers 0xf8000000 - 0xf8000b74
    apu_args += " -device loader,addr=0xf8000140,data=0x00500801,data-len=4" # GEM0_CLK_CTRL configure ethernet clock
    apu_args += " -device loader,addr=0xf800012c,data=0x1ed044d,data-len=4" # APER_CLK_CTRL configure peripheral clocks
    apu_args += " -device loader,addr=0xf8000108,data=0x0001e008,data-len=4" # IO_PLL_CTRL
    apu_args += " -device loader,addr=0xf8000910,data=0xf,data-len=4" # OCM_CFG, enable 4 64KB sections in high ram

    # Both CPUs start executing because we are not using the -kernel option
    # to load the items to memory from the boot.bin.  On reset, both CPUs
    # start executing from 0.  To avoid conflicts, we need to park CPU1
    # using a simple program of:
    #  1: wfi
    #     nb 1b
    apu_args += " -device loader,data=0xeafffffde320f003,data-len=8,addr=0xfffff000 -device loader,addr=0xfffff000,cpu-num=1"

    if known_args.get('boot_mode'):
        apu_args += f" -boot mode={known_args.get('boot_mode')}"

    # This does not work, but Zynq is simple enough it likely isn't required
    #if multiboot_val:
    #    # XDCFG_UNLOCK_OFFSET: Unlock the devci configuration registers
    #    apu_args += " -device loader,addr=0xf8007034,data=0x757bdf0d,data-len=4"
    #    # XDCFG_MULTIBOOT_ADDR_OFFSET: Multiboot offset register
    #    apu_args += f" -device loader,addr=0xf800702c,data={hex(multiboot_val & 0xFFF)},data-len=4"

    # Extract boot.bin into individual partition files
    boot_bin_path = os.path.join(machine_path, 'boot.bin')
    if os.path.exists(boot_bin_path):
        try:
            # Should the next item be run?
            enable_cpu = True
            need_dtb = True
            with ZynqBootImageLoader(boot_bin_path, 0) as loader:
                loader.parse()

                # Check for user_defined_fields, this might have boot.bin version info
                if loader.boot_header.user_defined_fields:
                    # Extract and print ASCII characters from the data
                    ascii_chars = []
                    for byte in loader.boot_header.user_defined_fields:
                        # Check if byte is printable ASCII (space to tilde: 32-126)
                        if 32 <= byte <= 126:
                            ascii_chars.append(chr(byte))
                        elif byte == 0:
                            # Null terminator - stop here
                            break
                        else:
                            # Non-printable, non-null character - stop
                            break
                    
                    if ascii_chars:
                        print(f"boot.bin udf: {''.join(ascii_chars)}")

                # Count total partitions across all images
                total_partitions = sum(len(partition_headers) for _, partition_headers in loader.image)
                print(f"There are {total_partitions} partition(s) from {len(loader.image)} image(s)...")
                
                for img_idx in range(len(loader.image)):
                    # Skip FSBL, no reason to load or extract as we can't execute it
                    if img_idx == 0:
                        continue
                    image_header, partition_headers = loader.image[img_idx]
                    for part_idx, partition_header in enumerate(partition_headers):
                        # Generate filename for this partition
                        if part_idx == 0:
                            filename = image_header.image_name.strip('\x00 ')
                        else:
                            # Insert partition index before file extension if present
                            if '.' in image_header.image_name:
                                name, ext = image_header.image_name.rsplit('.', 1)
                                filename = f"{name}.{part_idx}.{ext}".strip('\x00 ')
                            else:
                                filename = f"{image_header.image_name}.{part_idx}".strip('\x00 ')
                            
                        output_path = os.path.join(machine_path, filename)
                        print(f"  Extracting partition {img_idx}.{part_idx}: {output_path}")
                        loader.extract(img_idx, part_idx, output_path)
                        apu_args += f" -device loader,file={output_path},addr=0x{partition_header.destination_load_address:08x},force-raw=on"
                        if enable_cpu:
                            apu_args += ",cpu-num=0"
                            enable_cpu = False
                        if need_dtb and output_path.endswith('.dtb'):
                            apu_args += f" -dtb {output_path}"
                            need_dtb = False
            if need_dtb and known_args.get('dtb'):
                apu_args += f" -dtb {known_args.get('dtb')}"

        except Exception as e:
            print(f"Error: Could not extract boot files from boot.bin: {e}", file=sys.stderr)
            sys.exit(1)
    
    global QEMU_APU_ARGS
    QEMU_APU_ARGS = unknown_args + apu_args.split()


def configure_zynqmp(known_args: dict[str, str | None], unknown_args: list[str], drives: dict[str, list[dict[str, str]]], machine_path: str) -> None:
    """Configure QEMU for ZynqMP SoC.
    
    Configures the ZynqMP boot process by:
    1. Selecting the appropriate boot file based on boot mode (QSPI, SD0, SD1, or eMMC1)
    2. Extracting and verifying boot.bin from the boot device
    3. Parsing boot.bin to extract individual partition files
    4. Generating QEMU loader arguments for each partition
    5. Configuring ZynqMP-specific registers
    
    Sets the global APU_ARGS variable with QEMU command arguments.
    Sets the global PMU_ARGS variable with QEMU microblaze command arguments.
    Exits on error if boot file cannot be found or processed.
    
    Args:
        known_args: Dictionary of known arguments (boot_arch, boot_mode, kernel, dtb, etc.)
        unknown_args: List of unknown arguments to pass through to QEMU
        drives: Dictionary of parsed drive configurations by interface type
        machine_path: Path to machine working directory for extracted files
    """
    
    # Default behavior is PMUFW direct loading; -boot fsbl enables FSBL boot.
    fsbl_boot = bool(known_args.get('boot_fsbl'))
    if fsbl_boot:
        print("Using zynqmp FSBL boot flow: boot.bin partitions are loaded by FSBL")
    else:
        print("Using zynqmp PMUFW boot flow: FSBL is skipped and PMUFW plus applications are directly loaded")

    # Get boot mode description
    boot_mode_name = get_boot_mode_name(known_args['boot_arch'], known_args.get('boot_mode'))
    
    # Select boot file based on boot mode
    boot_file = None
    boot_file2 = None
    
    boot_drive = None  # Store the drive object to access properties like logical_block_size
    if boot_mode_name == 'QSPI':
        # QSPI boot uses MTD device
        # QSPI: index 0 alone = single flash, index 0+1 = split/striped flash
        if 'mtd' in drives and len(drives['mtd']) > 0:
            # Find MTD drive with index 0, and 1
            mtd0_drive = None
            mtd1_drive = None
            for drive in drives['mtd']:
                if drive.get('index') == '0':
                    mtd0_drive = drive
                elif drive.get('index') == '1':
                    mtd1_drive = drive
            
            # Determine boot file based on QSPI
            if mtd0_drive:
                boot_file = mtd0_drive.get('file')
                # Check for split QSPI flash (index 0 + 1)
                if mtd1_drive:
                    boot_file2 = mtd1_drive.get('file')
                    print(f"Using MTD device with index 0 for QSPI (lower) boot: {boot_file}")
                    print(f"Using MTD device with index 1 for QSPI (upper) boot: {boot_file2}")
                else:
                    # Single QSPI flash (only index 0)
                    print(f"Using MTD device with index 0 for QSPI boot: {boot_file}")
            else:
                # Fallback to first MTD device if no index specified
                boot_file = drives['mtd'][0].get('file')
                print(f"Using first MTD device for QSPI boot: {boot_file}")
        else:
            print("Error: QSPI boot mode selected but no MTD device found", file=sys.stderr)
            sys.exit(1)
    
    elif boot_mode_name == 'SD0':
        # SD0 boot uses SD device with index 0
        if 'sd' in drives and len(drives['sd']) > 0:
            # Find the drive with index 0
            sd0_drive = None
            for drive in drives['sd']:
                if drive.get('index') == '0':
                    sd0_drive = drive
                    break
            
            if sd0_drive:
                boot_drive = sd0_drive
                boot_file = sd0_drive.get('file')
                print(f"Using SD device with index 0 for SD0 boot: {boot_file}")
            else:
                # Fallback to first SD device if no index specified
                boot_file = drives['sd'][0].get('file')
                print(f"Using first SD device for SD0 boot: {boot_file}")
        else:
            print("Error: SD0 boot mode selected but no SD device found", file=sys.stderr)
            sys.exit(1)
    
    elif boot_mode_name == 'SD1':
        # SD1 boot uses SD device with index 1
        if 'sd' in drives and len(drives['sd']) > 0:
            # Find the drive with index 1
            sd1_drive = None
            for drive in drives['sd']:
                if drive.get('index') == '1':
                    sd1_drive = drive
                    break
            
            if sd1_drive:
                boot_drive = sd1_drive
                boot_file = sd1_drive.get('file')
                print(f"Using SD device with index 1 for SD1 boot: {boot_file}")
            else:
                print("Error: SD1 boot mode selected but no SD device with index 1 found", file=sys.stderr)
                sys.exit(1)
        else:
            print("Error: SD1 boot mode selected but no SD device found", file=sys.stderr)
            sys.exit(1)
    
    elif boot_mode_name == 'eMMC1':
        # eMMC1 boot uses SD device with index 3
        if 'sd' in drives and len(drives['sd']) > 0:
            # Find the drive with index 3
            emmc_drive = None
            for drive in drives['sd']:
                if drive.get('index') == '3':
                    emmc_drive = drive
                    break
            
            if emmc_drive:
                boot_file = emmc_drive.get('file')
                print(f"Using SD device with index 3 for eMMC1 boot: {boot_file}")
            else:
                print("Error: eMMC1 boot mode selected but no SD device with index 3 found", file=sys.stderr)
                sys.exit(1)
        else:
            print("Error: eMMC1 boot mode selected but no SD device found", file=sys.stderr)
            sys.exit(1)
    
    # Verify boot file contents if kernel is specified
    kernel_file = known_args.get('kernel')
    if not boot_file:
        boot_file = kernel_file

    if not boot_file:
        print(f"Error: unable to find a valid boot file")
        sys.exit(1)
    else:
        # Get user-specified multiboot value (default to 0)
        user_multiboot = known_args.get('boot_multiboot')
        multiboot_val = 0
        if user_multiboot is not None:
            try:
                multiboot_val = int(user_multiboot, 0)  # Support hex and decimal
            except ValueError:
                print(f"Warning: Invalid multiboot value '{user_multiboot}', using 0", file=sys.stderr)
                multiboot_val = 0
        
        if boot_mode_name == 'QSPI':
            # Pass boot architecture for magic verification
            actual_multiboot = get_mtd_contents(boot_file, kernel_file, known_args['boot_arch'], machine_path, multiboot=multiboot_val, loader=ZynqmpBootImageLoader, mtd_file2=boot_file2)
            if actual_multiboot == -1:
                sys.exit(1)
            if actual_multiboot != multiboot_val:
                multiboot_val = actual_multiboot
        elif boot_mode_name in ['SD0', 'SD1', 'eMMC1']:
            # For ZynqMP, only boot.bin is supported (multiboot='none')
            sector_size = None
            if boot_drive and 'logical_block_size' in boot_drive:
                sector_size = int(boot_drive['logical_block_size'])
            actual_multiboot = get_sd_contents(boot_file, kernel_file, machine_path, multiboot=multiboot_val, sector_size=sector_size)
            if actual_multiboot == -1:
                sys.exit(1)
            if actual_multiboot != multiboot_val:
                multiboot_val = actual_multiboot

    apu_args = ""
    pmu_args = ""
    
    if known_args.get('boot_mode'):
        apu_args += f" -boot mode={known_args.get('boot_mode')}"
    
    # Set multiboot register if multiboot value is specified
    if multiboot_val != 0:
        apu_args += f" -device loader,addr=0xffca0010,data={hex(multiboot_val)},data-len=4"
        # This is the boot count, set it to 0 as this is our first boot attempt
        apu_args += " -device loader,addr=0xffd80050,data=0x0,data-len=4"

    # Extract boot.bin into individual partition files
    boot_bin_path = os.path.join(machine_path, 'boot.bin')
    if os.path.exists(boot_bin_path):
        try:
            # Track which CPU numbers have been enabled (0-5)
            enabled_cpu_nums = set()
            # Track whether R5 CPUs have been targeted
            r5_cpus_used = set()
            
            # Map destination CPU to QEMU cpu-num value
            from amd_boot_image_loader.zynqmp import ZynqmpPartitionHeader
            cpu_num_map = {
                ZynqmpPartitionHeader.DestinationCpu.A53_0: 0,
                ZynqmpPartitionHeader.DestinationCpu.A53_1: 1,
                ZynqmpPartitionHeader.DestinationCpu.A53_2: 2,
                ZynqmpPartitionHeader.DestinationCpu.A53_3: 3,
                ZynqmpPartitionHeader.DestinationCpu.R5_0: 4,
                ZynqmpPartitionHeader.DestinationCpu.R5_1: 5,
                ZynqmpPartitionHeader.DestinationCpu.R5_LOCKSTEP: 4,  # Lockstep uses R5_0's cpu-num
            }
            
            trusted_firmware = 0
            with ZynqmpBootImageLoader(boot_bin_path, 0) as loader:
                loader.parse()

                # Check for user_defined_fields, this might have boot.bin version info
                if loader.boot_header.user_defined_fields:
                    # Extract and print ASCII characters from the data
                    ascii_chars = []
                    for byte in loader.boot_header.user_defined_fields:
                        # Check if byte is printable ASCII (space to tilde: 32-126)
                        if 32 <= byte <= 126:
                            ascii_chars.append(chr(byte))
                        elif byte == 0:
                            # Null terminator - stop here
                            break
                        else:
                            # Non-printable, non-null character - stop
                            break
                    
                    if ascii_chars:
                        print(f"boot.bin udf: {''.join(ascii_chars)}")

                # Count total partitions across all images
                total_partitions = sum(len(partition_headers) for _, partition_headers in loader.image)
                print(f"There are {total_partitions} partition(s) from {len(loader.image)} image(s)...")
                
                for img_idx in range(len(loader.image)):
                    if img_idx == 0:
                        if not fsbl_boot:
                            # Skip FSBL (first image), we're directly booting the items
                            continue
                    image_header, partition_headers = loader.image[img_idx]
                    for part_idx, partition_header in enumerate(partition_headers):
                        # Generate filename for this partition
                        if part_idx == 0:
                            filename = image_header.image_name.strip('\x00 ')
                        else:
                            # Insert partition index before file extension if present
                            if '.' in image_header.image_name:
                                name, ext = image_header.image_name.rsplit('.', 1)
                                filename = f"{name}.{part_idx}.{ext}".strip('\x00 ')
                            else:
                                filename = f"{image_header.image_name}.{part_idx}".strip('\x00 ')
                            
                        output_path = os.path.join(machine_path, filename)
                        print(f"  Extracting partition {img_idx}.{part_idx}: {output_path}")
                        loader.extract(img_idx, part_idx, output_path)
                        # Combine 64-bit load address
                        load_addr = (partition_header.destination_load_address_hi << 32) | partition_header.destination_load_address_lo
                        execution_addr = (partition_header.destination_execution_address_hi << 32) | partition_header.destination_execution_address_lo
                        
                        # Check destination CPU and route to appropriate args
                        dest_cpu = partition_header.get_destination_cpu()
                        
                        if dest_cpu == ZynqmpPartitionHeader.DestinationCpu.PMU:
                            # PMU partition - add to pmu_args (PMU uses 32-bit addresses)
                            pmu_args += f" -device loader,file={output_path},addr=0x{load_addr & 0xFFFFFFFF:08x},force-raw=on"
                        elif dest_cpu in cpu_num_map:
                            # APU or RPU partition - add to apu_args
                            apu_args += f" -device loader,file={output_path},addr=0x{load_addr:016x},force-raw=on"
                            
                            # Get the cpu-num for this destination CPU
                            cpu_num = cpu_num_map[dest_cpu]
                            
                            # Only add cpu-num if this is the first partition for this CPU that has valid addresses
                            if (cpu_num not in enabled_cpu_nums):
                                apu_args += f",cpu-num={cpu_num}"
                                enabled_cpu_nums.add(cpu_num)

                            # Track R5 CPU usage for later configuration
                            if dest_cpu in (ZynqmpPartitionHeader.DestinationCpu.R5_0, ZynqmpPartitionHeader.DestinationCpu.R5_1):
                                r5_cpus_used.add(dest_cpu)

                            if dest_cpu == ZynqmpPartitionHeader.DestinationCpu.A53_0 and trusted_firmware and img_idx > trusted_firmware:
                                # ZynqMP-specific register configuration
                                # See UG1085 for register information
                                # Replicate BootROM like behaviour, having loaded SPL and PMU(ROM+FW)
                                #
                                # In an actual device the FSBL will run first, load ATF and setup the
                                # following data structure to tell ATF what to continue booting with.
                                #
                                # In QEMU emulation we may start booting directly from ATF, so we need
                                # to setup the structure ourselves.
                                #
                                # Write to OCM (See UG1085 for more information), address 0xfffc0000
                                # the address to boot from (where u-boot is):
                                # fffc0000  58 4c 4e 58  01 00 00 00  |XLNX....|
                                # fffc0008  00 00 00 08  00 00 00 00  |........|
                                # fffc0010  10 00 00 00  00 00 00 00  |........|
                                #
                                # Then write that address (fffc0000) to 0xffd80048 so ATF can find this block
                                #
                                # fffc0008 defines the u-boot load address as 0x8000000, if u-boot is
                                # expected to be elsewhere in memory, you must adjust the value.
                                #
                                # Note: fffc0000 is the default FSBL load address, so ultimately we're
                                # emulating what the fsbl may have looked like, if used.
                                #
                                # We write the structure as big endian to make it easier to match/read
                                # the table above.  Remember the CPU is running in little endian mode,
                                # with the default resulting in:
                                # 00000000fffc0000: 0x584e4c58 0x00000001 0x08000000 0x00000000
                                # 00000000fffc0010: 0x00000010 0x00000000
                                #
                                # Note: load and execution address should be the same, so use load_addr
                                apu_args += " -device loader,addr=0xfffc0000,data=0x584c4e5801000000,data-be=true,data-len=8"
                                # Byte-swap the 32-bit load_addr and shift to upper 32-bits for correct big-endian representation
                                be_load_addr = int.from_bytes(load_addr.to_bytes(4, 'little'), 'big') << 32
                                apu_args += f" -device loader,addr=0xfffc0008,data=0x{be_load_addr:016x},data-be=true,data-len=8"
                                apu_args += " -device loader,addr=0xfffc0010,data=0x1000000000000000,data-be=true,data-len=8"
                                # PMU GLOBAL GEN STORAGE6: Tell ATF where the structure is
                                apu_args += " -device loader,addr=0xffd80048,data=0xfffc0000,data-len=4,attrs-secure=on"
                            
                            if dest_cpu == ZynqmpPartitionHeader.DestinationCpu.A53_0 and trusted_firmware == 0 and partition_header.get_trustzone():
                                # We found a trusted firmware, next image for the APU needs to be used as the secondary loader/application
                                trusted_firmware = img_idx
                    if img_idx == 0:
                        if fsbl_boot:
                            # When doing an FSBL boot, only extract the fsbl
                            break
                
                # After all partitions are processed, check if R5 CPUs were used
                if r5_cpus_used:
                    r5_0_used = ZynqmpPartitionHeader.DestinationCpu.R5_0 in r5_cpus_used
                    r5_1_used = ZynqmpPartitionHeader.DestinationCpu.R5_1 in r5_cpus_used
                    
                    if r5_0_used and r5_1_used:
                        print("  R5 configuration: Both R5_0 and R5_1 CPUs were used")
                    elif r5_0_used:
                        print("  R5 configuration: R5_0 CPU was used")
                    elif r5_1_used:
                        print("  R5 configuration: R5_1 CPU was used")
                    
                    # ZynqMP-specific register configuration for R5
                    # See UG1085 for register information
                    # 0xFF5E023C - CRL_APB: RST_LPD_TOP: Contains various resets, including R5_0 and R5_1
                    # Compute data value based on which R5 CPUs are used (bit 0 = R5_0, bit 1 = R5_1)
                    rst_lpd_top = 0x80008fdc  # Base value with bits 0 and 1 cleared
                    if r5_0_used and r5_1_used:
                        pass                # Nothing to be done, we've already cleared the bits
                    elif r5_0_used:
                        rst_lpd_top |= 0x2  # Enable bit 1 to keep R5_1 in reset
                    if r5_1_used:
                        rst_lpd_top |= 0x1  # Enable bit 0 to keep R5_0 in reset
                    apu_args += f" -device loader,addr=0xff5e023c,data=0x{rst_lpd_top:08x},data-len=4"
                    # Not sure what this register is for
                    apu_args += " -device loader,addr=0xff9a0000,data=0x80000218,data-len=4"

        except Exception as e:
            print(f"Error: Could not extract boot files from boot.bin: {e}", file=sys.stderr)
            sys.exit(1)
    
    if fsbl_boot:
        apu_args += " -global xlnx,zynqmp-boot.use-pmufw=false"
        apu_args += " -global xlnx,zynqmp-boot.load-pmufw-cfg=false"
        apu_args += " -global xlnx,zynqmp-boot.cpu-num=0"
    else:
        # Configure boot settings based on whether PMU firmware is present
        if pmu_args:
            # Note if we're running with pmu-firmware, the following starts the system
            apu_args += " -global xlnx,zynqmp-boot.cpu-num=0 -global xlnx,zynqmp-boot.use-pmufw=true"
        else:
            # If no PMU firmware is provided, we need to start the Cortex-A53 manually with:
            apu_args += " -device loader,addr=0xfd1a0104,data=0x8000000e,data-len=4" # CPU_RST_CTRL register, release Cortex-A53 from reset
    
    global QEMU_APU_ARGS, QEMU_PMU_ARGS
    QEMU_APU_ARGS = unknown_args + [ "-hw-dtb", known_args.get('dtb') ] + apu_args.split()
    
    if known_args.get('pmu_args'):
        QEMU_PMU_ARGS =  [ "-hw-dtb", known_args.get('pmu_hw_dtb') ]
        QEMU_PMU_ARGS += [ "-kernel", known_args.get('pmu_kernel') ]
        # Not sure why this is needed
        QEMU_PMU_ARGS += [ "-device", "loader,addr=0xfd1a0074,data=0x1011003,data-len=4" ] # DP_AUDIO_REF_CTRL?
        QEMU_PMU_ARGS += [ "-device", "loader,addr=0xfd1a007C,data=0x1010f03,data-len=4" ] # DP_STC_REF_CTRL?
        QEMU_PMU_ARGS += known_args.get('pmu_unknown')
        # Append any PMU partitions extracted from boot.bin
        if pmu_args:
            QEMU_PMU_ARGS += pmu_args.split()
    elif pmu_args:
        # PMU partitions were extracted from boot.bin but no -pmu-args provided
        # Create QEMU_PMU_ARGS with just the extracted PMU partitions
        QEMU_PMU_ARGS = pmu_args.split()

def configure_versal(known_args: dict[str, str | None], unknown_args: list[str], drives: dict[str, list[dict[str, str]]], machine_path: str) -> None:
    """Configure Versal boot settings and extract boot files.
    
    Configures the Versal boot process by:
    1. Selecting the appropriate boot file based on boot mode (QSPI, SD0, SD1, or eMMC1)
    2. Extracting and verifying boot.bin (pdi) from the boot device
    3. Parsing boot.bin (pdi) to extract individual partition files
    4. Generating QEMU loader arguments for each partition
    5. Configuring Versal-specific registers
    
    Sets the global APU_ARGS variable with QEMU command arguments.
    Sets the global PLM_ARGS variable with QEMU microblaze command arguments.
    Exits on error if boot file cannot be found or processed.
    
    Args:
        known_args: Dictionary of known arguments (boot_arch, boot_mode, kernel, dtb, etc.)
        unknown_args: List of unknown arguments to pass through to QEMU
        drives: Dictionary of parsed drive configurations by interface type
        machine_path: Path to machine working directory for extracted files
    """
    
    # Get boot mode description
    boot_mode_name = get_boot_mode_name(known_args['boot_arch'], known_args.get('boot_mode'))
    
    # Select boot file based on boot mode
    boot_file = None
    boot_file2 = None
    
    boot_drive = None  # Store the drive object to access properties like logical_block_size
    if boot_mode_name == 'QSPI':
        # QSPI boot uses MTD device
        # QSPI: index 0 alone = single flash, index 0+1 = split/striped flash
        if 'mtd' in drives and len(drives['mtd']) > 0:
            # Find MTD drive with index 0, and 1
            mtd0_drive = None
            mtd1_drive = None
            for drive in drives['mtd']:
                if drive.get('index') == '0':
                    mtd0_drive = drive
                elif drive.get('index') == '1':
                    mtd1_drive = drive
            
            # Determine boot file based on QSPI
            if mtd0_drive:
                boot_file = mtd0_drive.get('file')
                # Check for split QSPI flash (index 0 + 1)
                if mtd1_drive:
                    boot_file2 = mtd1_drive.get('file')
                    print(f"Using MTD device with index 0 for QSPI (lower) boot: {boot_file}")
                    print(f"Using MTD device with index 1 for QSPI (upper) boot: {boot_file2}")
                else:
                    # Single QSPI flash (only index 0)
                    print(f"Using MTD device with index 0 for QSPI boot: {boot_file}")
            else:
                # Fallback to first MTD device if no index specified
                boot_file = drives['mtd'][0].get('file')
                print(f"Using first MTD device for QSPI boot: {boot_file}")
        else:
            print("Error: QSPI boot mode selected but no MTD device found", file=sys.stderr)
            sys.exit(1)
    
    elif boot_mode_name == 'SD0':
        # SD0 boot uses SD device with index 0
        if 'sd' in drives and len(drives['sd']) > 0:
            # Find the drive with index 0
            sd0_drive = None
            for drive in drives['sd']:
                if drive.get('index') == '0':
                    sd0_drive = drive
                    break
            
            if sd0_drive:
                boot_file = sd0_drive.get('file')
                print(f"Using SD device with index 0 for SD0 boot: {boot_file}")
            else:
                # Fallback to first SD device if no index specified
                boot_file = drives['sd'][0].get('file')
                print(f"Using first SD device for SD0 boot: {boot_file}")
        else:
            print("Error: SD0 boot mode selected but no SD device found", file=sys.stderr)
            sys.exit(1)
    
    elif boot_mode_name == 'SD1':
        # SD1 boot uses SD device with index 1
        if 'sd' in drives and len(drives['sd']) > 0:
            # Find the drive with index 1
            sd1_drive = None
            for drive in drives['sd']:
                if drive.get('index') == '1':
                    sd1_drive = drive
                    break
            
            if sd1_drive:
                boot_file = sd1_drive.get('file')
                print(f"Using SD device with index 1 for SD1 boot: {boot_file}")
            else:
                print("Error: SD1 boot mode selected but no SD device with index 1 found", file=sys.stderr)
                sys.exit(1)
        else:
            print("Error: SD1 boot mode selected but no SD device found", file=sys.stderr)
            sys.exit(1)
    
    elif boot_mode_name == 'eMMC1':
        # eMMC1 boot uses SD device with index 3
        if 'sd' in drives and len(drives['sd']) > 0:
            # Find the drive with index 3
            emmc_drive = None
            for drive in drives['sd']:
                if drive.get('index') == '3':
                    emmc_drive = drive
                    break
            
            if emmc_drive:
                boot_file = emmc_drive.get('file')
                print(f"Using SD device with index 3 for eMMC1 boot: {boot_file}")
            else:
                print("Error: eMMC1 boot mode selected but no SD device with index 3 found", file=sys.stderr)
                sys.exit(1)
        else:
            print("Error: eMMC1 boot mode selected but no SD device found", file=sys.stderr)
            sys.exit(1)
    
    elif boot_mode_name == 'OSPI':
        # OSPI boot uses MTD device with index 4
        if 'mtd' in drives and len(drives['mtd']) > 0:
            # Find MTD drive with index 4
            ospi_drive = None
            for drive in drives['mtd']:
                if drive.get('index') == '4':
                    ospi_drive = drive
                    break
            
            if ospi_drive:
                boot_file = ospi_drive.get('file')
                print(f"Using MTD device with index 4 for OSPI boot: {boot_file}")
            else:
                print("Error: OSPI boot mode selected but no MTD device with index 4 found", file=sys.stderr)
                sys.exit(1)
        else:
            print("Error: OSPI boot mode selected but no MTD device found", file=sys.stderr)
            sys.exit(1)
    
    # Verify boot file contents if kernel is specified
    kernel_file = known_args.get('kernel')
    if not boot_file:
        boot_file = kernel_file

    if not boot_file:
        print(f"Error: unable to find a valid boot file")
        sys.exit(1)
    else:
        # Get user-specified multiboot value (default to 0)
        user_multiboot = known_args.get('boot_multiboot')
        multiboot_val = 0
        if user_multiboot is not None:
            try:
                multiboot_val = int(user_multiboot, 0)  # Support hex and decimal
            except ValueError:
                print(f"Warning: Invalid multiboot value '{user_multiboot}', using 0", file=sys.stderr)
                multiboot_val = 0
        
        if boot_mode_name in ['QSPI', 'OSPI']:
            # Pass boot architecture for magic verification
            actual_multiboot = get_mtd_contents(boot_file, kernel_file, known_args['boot_arch'], machine_path, multiboot=multiboot_val, loader=VersalBootImageLoader, mtd_file2=boot_file2)
            if actual_multiboot == -1:
                sys.exit(1)
            if actual_multiboot != multiboot_val:
                multiboot_val = actual_multiboot
        elif boot_mode_name in ['SD0', 'SD1', 'eMMC1']:
            # For Versal, only boot.pdi is supported (multiboot='none')
            sector_size = None
            if boot_drive and 'logical_block_size' in boot_drive:
                sector_size = int(boot_drive['logical_block_size'])
            actual_multiboot = get_sd_contents(boot_file, kernel_file, machine_path, multiboot=multiboot_val, sector_size=sector_size)
            if actual_multiboot == -1:
                sys.exit(1)
            if actual_multiboot != multiboot_val:
                multiboot_val = actual_multiboot

    apu_args = ""
    plm_args = ""
    
    if known_args.get('boot_mode'):
        apu_args += f" -boot mode={known_args.get('boot_mode')}"
    
    # Set multiboot register if multiboot value is specified
    if multiboot_val != 0:
        # PMC_MULTI_BOOT register
        apu_args += f" -device loader,addr=0xf1110004,data={hex(multiboot_val)},data-len=4"
        # Magic value for u-boot to think we came from the image selector (0x1D in bits 31:24)
        apu_args += " -device loader,addr=0xf1110060,data=0x1d000000,data-len=4"

    # Extract boot.pdi into individual partition files
    boot_pdi_path = os.path.join(machine_path, 'boot.bin')
    if os.path.exists(boot_pdi_path):
        try:
            # Track which CPU numbers have been enabled (0-5)
            enabled_cpu_nums = set()
            
            # Map destination CPU to QEMU cpu-num value
            from amd_boot_image_loader.versal import VersalPartitionHeader
            cpu_num_map = {
                VersalPartitionHeader.DestinationCpu.A72_0: 0,
                VersalPartitionHeader.DestinationCpu.A72_1: 1,
                VersalPartitionHeader.DestinationCpu.R5_0: 2,
                VersalPartitionHeader.DestinationCpu.R5_1: 3,
                VersalPartitionHeader.DestinationCpu.R5_LOCKSTEP: 2,  # Lockstep uses R5_0's cpu-num
            }
            
            with VersalBootImageLoader(boot_pdi_path, 0) as loader:
                loader.parse()
                
                # Check for optional data 0x21, this might have boot.bin version info
                if loader.optional_data:
                    for idx, opt_data in enumerate(loader.optional_data):
                        if opt_data.data_id == 0x21:
                            if opt_data.data:
                                # Extract and print ASCII characters from the data
                                ascii_chars = []
                                for byte in opt_data.data:
                                    # Check if byte is printable ASCII (space to tilde: 32-126)
                                    if 32 <= byte <= 126:
                                        ascii_chars.append(chr(byte))
                                    elif byte == 0:
                                        # Null terminator - stop here
                                        break
                                    else:
                                        # Non-printable, non-null character - stop
                                        break
                                
                                if ascii_chars:
                                    print(f"boot.bin od 0x21: {''.join(ascii_chars)}")

                # Extract boot header
                bh_filename = os.path.join(machine_path, 'boot_bh.bin')
                loader.dump_boot_header(bh_filename)
                print(f"Extracted boot header: {bh_filename}")
                plm_args += f" -device loader,file={bh_filename},addr=0xf201e000,force-raw=on"
                
                # Extract PMC CDO
                pmc_cdo_filename = os.path.join(machine_path, 'pmc_cdo.bin')
                loader.dump_pmc_cdo(pmc_cdo_filename)
                print(f"Extracted PMC CDO: {pmc_cdo_filename}")
                pmc_cdo_load_addr = loader.boot_header.pmc_data_load_address
                plm_args += f" -device loader,file={pmc_cdo_filename},addr=0x{pmc_cdo_load_addr:08x},force-raw=on"

                # Extract PLM (image 0, partition 0 from pmc_subsys)
                image_header, partition_headers = loader.image[0]
                # Extract the PLM partition
                plm_filename = os.path.join(machine_path, 'plm.bin')
                loader.extract(0, 0, plm_filename)
                print(f"Extracted PLM: {plm_filename}")
                        
                # Get PLM load address
                partition_header = partition_headers[0]
                load_addr = (partition_header.destination_load_address_hi << 32) | partition_header.destination_load_address_lo
                plm_args += f" -device loader,file={plm_filename},addr=0x{load_addr:016x},force-raw=on,cpu-num=1"
        
        except Exception as e:
            print(f"Error: Could not extract PLM files from boot.pdi: {e}", file=sys.stderr)
            sys.exit(1)

    # Magic PMC ROM values
    plm_args += " -device loader,addr=0xf0000000,data=0xba020004,data-len=4"
    plm_args += " -device loader,addr=0xf0000004,data=0xb800fffc,data-len=4"
    # PPU_RST_MODE: 0 Microblaze starts executing
    plm_args += " -device loader,addr=0xf1110624,data=0x0,data-len=4"
    # PPU_RST: 1 Release from reset
    plm_args += " -device loader,addr=0xf1110620,data=0x1,data-len=4"

    global QEMU_APU_ARGS, QEMU_PLM_ARGS
    QEMU_APU_ARGS = unknown_args + [ "-hw-dtb", known_args.get('dtb') ] + apu_args.split()
    
    # For Versal, PSM replaces PMU - handle PLM arguments if provided
    if known_args.get('plm_args'):
        QEMU_PLM_ARGS = [ "-hw-dtb", known_args.get('plm_hw_dtb') ]
        QEMU_PLM_ARGS += known_args.get('plm_unknown')
        # Append any PSM partitions extracted from boot.pdi
        if plm_args:
            QEMU_PLM_ARGS += plm_args.split()
    elif plm_args:
        # PSM partitions were extracted from boot.pdi but no -plm-args provided
        # Create QEMU_PMU_ARGS with just the extracted PSM partitions
        QEMU_PLM_ARGS = plm_args.split()

def configure_versal2ve2vm(known_args: dict[str, str | None], unknown_args: list[str], drives: dict[str, list[dict[str, str]]], machine_path: str) -> None:
    """Configure Versal Gen 2 (VE/VM) boot settings and extract boot files.
    
    Configures the Versal Gen 2 (VE/VM) boot process by:
    1. Selecting the appropriate boot file based on boot mode (QSPI, SD0, SD1, or eMMC1)
    2. Extracting and verifying boot.bin (pdi) from the boot device
    3. Parsing boot.bin (pdi) to extract individual partition files
    4. Generating QEMU loader arguments for each partition
    5. Configuring Versal Gen 2 (VE/VM)-specific registers
    
    Sets the global APU_ARGS variable with QEMU command arguments.
    Sets the global PLM_ARGS variable with QEMU microblaze command arguments.
    Exits on error if boot file cannot be found or processed.
    
    Args:
        known_args: Dictionary of known arguments (boot_arch, boot_mode, kernel, dtb, etc.)
        unknown_args: List of unknown arguments to pass through to QEMU
        drives: Dictionary of parsed drive configurations by interface type
        machine_path: Path to machine working directory for extracted files
    """
    
    # Get boot mode description
    boot_mode_name = get_boot_mode_name(known_args['boot_arch'], known_args.get('boot_mode'))
    
    # Select boot file based on boot mode
    boot_file = None
    boot_file2 = None
    
    boot_drive = None  # Store the drive object to access properties like logical_block_size
    if boot_mode_name == 'QSPI':
        # QSPI boot uses MTD device
        # QSPI: index 0 alone = single flash, index 0+1 = split/striped flash
        if 'mtd' in drives and len(drives['mtd']) > 0:
            # Find MTD drive with index 0, and 1
            mtd0_drive = None
            mtd1_drive = None
            for drive in drives['mtd']:
                if drive.get('index') == '0':
                    mtd0_drive = drive
                elif drive.get('index') == '1':
                    mtd1_drive = drive
            
            # Determine boot file based on QSPI
            if mtd0_drive:
                boot_file = mtd0_drive.get('file')
                # Check for split QSPI flash (index 0 + 1)
                if mtd1_drive:
                    boot_file2 = mtd1_drive.get('file')
                    print(f"Using MTD device with index 0 for QSPI (lower) boot: {boot_file}")
                    print(f"Using MTD device with index 1 for QSPI (upper) boot: {boot_file2}")
                else:
                    # Single QSPI flash (only index 0)
                    print(f"Using MTD device with index 0 for QSPI boot: {boot_file}")
            else:
                # Fallback to first MTD device if no index specified
                boot_file = drives['mtd'][0].get('file')
                print(f"Using first MTD device for QSPI boot: {boot_file}")
        else:
            print("Error: QSPI boot mode selected but no MTD device found", file=sys.stderr)
            sys.exit(1)
    
    elif boot_mode_name == 'SD0':
        # SD0 boot uses SD device with index 0
        if 'sd' in drives and len(drives['sd']) > 0:
            # Find the drive with index 0
            sd0_drive = None
            for drive in drives['sd']:
                if drive.get('index') == '0':
                    sd0_drive = drive
                    break
            
            if sd0_drive:
                boot_file = sd0_drive.get('file')
                print(f"Using SD device with index 0 for SD0 boot: {boot_file}")
            else:
                # Fallback to first SD device if no index specified
                boot_file = drives['sd'][0].get('file')
                print(f"Using first SD device for SD0 boot: {boot_file}")
        else:
            print("Error: SD0 boot mode selected but no SD device found", file=sys.stderr)
            sys.exit(1)
    
    elif boot_mode_name == 'SD1':
        # SD1 boot uses SD device with index 1
        if 'sd' in drives and len(drives['sd']) > 0:
            # Find the drive with index 1
            sd1_drive = None
            for drive in drives['sd']:
                if drive.get('index') == '1':
                    sd1_drive = drive
                    break
            
            if sd1_drive:
                boot_file = sd1_drive.get('file')
                print(f"Using SD device with index 1 for SD1 boot: {boot_file}")
            else:
                print("Error: SD1 boot mode selected but no SD device with index 1 found", file=sys.stderr)
                sys.exit(1)
        else:
            print("Error: SD1 boot mode selected but no SD device found", file=sys.stderr)
            sys.exit(1)
    
    elif boot_mode_name == 'eMMC1':
        # eMMC1 boot uses SD device with index 3
        if 'sd' in drives and len(drives['sd']) > 0:
            # Find the drive with index 3
            emmc_drive = None
            for drive in drives['sd']:
                if drive.get('index') == '3':
                    emmc_drive = drive
                    break
            
            if emmc_drive:
                boot_file = emmc_drive.get('file')
                print(f"Using SD device with index 3 for eMMC1 boot: {boot_file}")
            else:
                print("Error: eMMC1 boot mode selected but no SD device with index 3 found", file=sys.stderr)
                sys.exit(1)
        else:
            print("Error: eMMC1 boot mode selected but no SD device found", file=sys.stderr)
            sys.exit(1)
    
    elif boot_mode_name == 'OSPI':
        # OSPI boot uses MTD device with index 0
        if 'mtd' in drives and len(drives['mtd']) > 0:
            # Find MTD drive with index 0
            ospi_drive = None
            for drive in drives['mtd']:
                if drive.get('index') == '0':
                    ospi_drive = drive
                    break
            
            if ospi_drive:
                boot_file = ospi_drive.get('file')
                print(f"Using MTD device with index 0 for OSPI boot: {boot_file}")
            else:
                print("Error: OSPI boot mode selected but no MTD device with index 0 found", file=sys.stderr)
                sys.exit(1)
        else:
            print("Error: OSPI boot mode selected but no MTD device found", file=sys.stderr)
            sys.exit(1)
    
    elif boot_mode_name == 'UFS':
        # UFS boot uses SCSI device with bus=scsi.0, channel=0, scsi-id=0, and lowest lun
        if 'scsi' in drives and len(drives['scsi']) > 0:
            # Find SCSI drive with bus=scsi.0, channel=0, scsi-id=0 and lowest lun
            ufs_candidates = []
            for drive in drives['scsi']:
                # Check if this drive matches the UFS criteria
                # Looking for bus=scsi.0, channel=0, scsi-id=0
                # Note: drive dict may have these from -device specification
                bus = drive.get('bus', '')
                channel = drive.get('channel', '0')
                scsi_id = drive.get('scsi-id', '0')
                lun = drive.get('lun', '0')
                
                # Match bus=scsi.0, channel=0, scsi-id=0 (any lun)
                if bus == 'scsi.0' and channel == '0' and scsi_id == '0':
                    try:
                        lun_num = int(lun)
                        ufs_candidates.append((lun_num, drive))
                    except ValueError:
                        # If lun is not a valid integer, skip this drive
                        pass
            
            if ufs_candidates:
                # Sort by lun number and pick the lowest
                ufs_candidates.sort(key=lambda x: x[0])
                lowest_lun, ufs_drive = ufs_candidates[0]
                boot_file = ufs_drive.get('file')
                boot_drive = ufs_drive
                print(f"Using SCSI device (bus=scsi.0, channel=0, scsi-id=0, lun={lowest_lun}) for UFS boot: {boot_file}")
            else:
                # If no exact match, try to find first SCSI drive as fallback
                if len(drives['scsi']) > 0:
                    boot_file = drives['scsi'][0].get('file')
                    print(f"Using first SCSI device for UFS boot: {boot_file}")
                else:
                    print("Error: UFS boot mode selected but no matching SCSI device found", file=sys.stderr)
                    print("Expected: SCSI device with bus=scsi.0, channel=0, scsi-id=0", file=sys.stderr)
                    sys.exit(1)
        else:
            print("Error: UFS boot mode selected but no SCSI device found", file=sys.stderr)
            sys.exit(1)
    
    # Verify boot file contents if kernel is specified
    kernel_file = known_args.get('kernel')
    if not boot_file:
        boot_file = kernel_file

    if not boot_file:
        print(f"Error: unable to find a valid boot file")
        sys.exit(1)
    else:
        # Get user-specified multiboot value (default to 0)
        user_multiboot = known_args.get('boot_multiboot')
        multiboot_val = 0
        if user_multiboot is not None:
            try:
                multiboot_val = int(user_multiboot, 0)  # Support hex and decimal
            except ValueError:
                print(f"Warning: Invalid multiboot value '{user_multiboot}', using 0", file=sys.stderr)
                multiboot_val = 0
        
        if boot_mode_name in ['QSPI', 'OSPI']:
            # Pass boot architecture for magic verification
            actual_multiboot = get_mtd_contents(boot_file, kernel_file, known_args['boot_arch'], machine_path, multiboot=multiboot_val, loader=Versal2VE2VMBootImageLoader, mtd_file2=boot_file2)
            if actual_multiboot == -1:
                sys.exit(1)
            if actual_multiboot != multiboot_val:
                multiboot_val = actual_multiboot
        elif boot_mode_name in ['SD0', 'SD1', 'eMMC1', 'UFS']:
            # For Versal Gen 2, only boot.pdi is supported (multiboot='none')
            # UFS uses the same extraction method as SD/eMMC devices
            sector_size = None
            if boot_drive and 'logical_block_size' in boot_drive:
                sector_size = int(boot_drive['logical_block_size'])
            actual_multiboot = get_sd_contents(boot_file, kernel_file, machine_path, multiboot=multiboot_val, sector_size=sector_size)
            if actual_multiboot == -1:
                sys.exit(1)
            if actual_multiboot != multiboot_val:
                multiboot_val = actual_multiboot

    apu_args = ""
    plm_args = ""
    
    if known_args.get('boot_mode'):
        apu_args += f" -boot mode={known_args.get('boot_mode')}"
    
    # Set multiboot register if multiboot value is specified
    if multiboot_val != 0:
        # PMC_MULTI_BOOT register
        apu_args += f" -device loader,addr=0xf1110004,data={hex(multiboot_val)},data-len=4"
        # Magic value for u-boot to think we came from the image selector (0x1D in bits 31:24)
        apu_args += " -device loader,addr=0xf1110060,data=0x1d000000,data-len=4"

    # Extract boot.pdi into individual partition files
    boot_pdi_path = os.path.join(machine_path, 'boot.bin')
    if os.path.exists(boot_pdi_path):
        try:
            # Track which CPU numbers have been enabled (0-5)
            enabled_cpu_nums = set()
            
            # Map destination CPU to QEMU cpu-num value
            from amd_boot_image_loader.versal_2ve_2vm import Versal2VE2VMPartitionHeader
            cpu_num_map = {
                Versal2VE2VMPartitionHeader.DestinationCpu.A78_0: 0,
                Versal2VE2VMPartitionHeader.DestinationCpu.A78_1: 1,
                Versal2VE2VMPartitionHeader.DestinationCpu.A78_2: 2,
                Versal2VE2VMPartitionHeader.DestinationCpu.A78_3: 3,
                Versal2VE2VMPartitionHeader.DestinationCpu.R52_0: 4,
                Versal2VE2VMPartitionHeader.DestinationCpu.R52_1: 5,
            }
            
            with Versal2VE2VMBootImageLoader(boot_pdi_path, 0) as loader:
                loader.parse()

                # Check for optional data 0x21, this might have boot.bin version info
                if loader.optional_data:
                    for idx, opt_data in enumerate(loader.optional_data):
                        if opt_data.data_id == 0x21:
                            if opt_data.data:
                                # Extract and print ASCII characters from the data
                                ascii_chars = []
                                for byte in opt_data.data:
                                    # Check if byte is printable ASCII (space to tilde: 32-126)
                                    if 32 <= byte <= 126:
                                        ascii_chars.append(chr(byte))
                                    elif byte == 0:
                                        # Null terminator - stop here
                                        break
                                    else:
                                        # Non-printable, non-null character - stop
                                        break
                                
                                if ascii_chars:
                                    print(f"boot.bin od 0x21: {''.join(ascii_chars)}")

                # Extract boot header
                bh_filename = os.path.join(machine_path, 'boot_bh.bin')
                loader.dump_boot_header(bh_filename)
                print(f"Extracted boot header: {bh_filename}")
                plm_args += f" -device loader,file={bh_filename},addr=0xf201eec0,force-raw=on"
                
                # Extract Hash Block 0
                hb0_filename = os.path.join(machine_path, 'HashBlock0.bin')
                loader.dump_hash_block_0(hb0_filename)
                print(f"Extracted hash block 0: {hb0_filename}")
                plm_args += f" -device loader,file={hb0_filename},addr=0xf201ecc0,force-raw=on"
                
                # Extract PMC CDO
                pmc_cdo_filename = os.path.join(machine_path, 'pmc_cdo.bin')
                loader.dump_pmc_cdo(pmc_cdo_filename)
                print(f"Extracted PMC CDO: {pmc_cdo_filename}")
                pmc_cdo_load_addr = loader.boot_header.pmc_data_load_address
                plm_args += f" -device loader,file={pmc_cdo_filename},addr=0x{pmc_cdo_load_addr:08x},force-raw=on"

                # Extract PLM (image 0, partition 0 from pmc_subsys)
                image_header, partition_headers = loader.image[0]
                # Extract the PLM partition
                plm_filename = os.path.join(machine_path, 'plm.bin')
                loader.extract(0, 0, plm_filename)
                print(f"Extracted PLM: {plm_filename}")
                
                # Get PLM load address
                partition_header = partition_headers[0]
                load_addr = partition_header.destination_load_address_lo
                plm_args += f" -device loader,file={plm_filename},addr=0x{load_addr:08x},force-raw=on,cpu-num=1"
                
        
        except Exception as e:
            print(f"Error: Could not extract PLM files from boot.pdi: {e}", file=sys.stderr)
            sys.exit(1)

    # Magic PMC ROM values
    plm_args += " -device loader,addr=0xf0000000,data=0xba020004,data-len=4"
    plm_args += " -device loader,addr=0xf0000004,data=0xb800fffc,data-len=4"
    # PPU_RST_MODE: 0 Microblaze starts executing
    plm_args += " -device loader,addr=0xf1110624,data=0x0,data-len=4"
    # PPU_RST: 1 Release from reset
    plm_args += " -device loader,addr=0xf1110620,data=0x1,data-len=4"

    global QEMU_APU_ARGS, QEMU_PLM_ARGS, QEMU_ASU_ARGS
    QEMU_APU_ARGS = unknown_args + [ "-hw-dtb", known_args.get('dtb') ] + apu_args.split()
    
    # For Versal Gen 2, PSM replaces PMU - handle PLM arguments if provided
    if known_args.get('plm_args'):
        QEMU_PLM_ARGS = [ "-hw-dtb", known_args.get('plm_hw_dtb') ]
        QEMU_PLM_ARGS += known_args.get('plm_unknown')
        # Append any PSM partitions extracted from boot.pdi
        if plm_args:
            QEMU_PLM_ARGS += plm_args.split()
    elif plm_args:
        # PSM partitions were extracted from boot.pdi but no -plm-args provided
        # Create QEMU_PLM_ARGS with just the extracted PSM partitions
        QEMU_PLM_ARGS = plm_args.split()
    
    # Handle ASU arguments if provided
    if known_args.get('asu_args'):
        QEMU_ASU_ARGS = [ "-hw-dtb", known_args.get('asu_hw_dtb') ]
        QEMU_ASU_ARGS += known_args.get('asu_unknown')

def main() -> None:
    """Main entry point for the AMD FPGA multiarch wrapper.
    
    Orchestrates the complete QEMU boot process:
    1. Parses command line arguments to extract boot configuration
    2. Validates boot architecture and mode
    3. Creates or uses machine working directory for extracted files
    4. Configures architecture-specific boot settings
    5. Launches QEMU with appropriate arguments
    6. Monitors QEMU execution and reports errors
    
    Exits with:
    - 0 on successful QEMU execution
    - 1 on configuration errors or invalid arguments
    - QEMU exit code if QEMU fails
    """
    import tempfile
    import shutil
    import atexit
    
    # Skip the script name (first argument)
    args = sys.argv[1:]
    
    # Parse arguments
    known_args, unknown_args = parse_arguments(args)
    
    # Debug output - can be removed in production
    #print(f"Known arguments: {known_args}")
    #print(f"Unknown arguments: {unknown_args}")
    
    # Validate boot architecture is provided
    boot_arch = known_args.get('boot_arch')
    
    if boot_arch is None:
        print("Error: -boot arch=<arch> is required", file=sys.stderr)
        print("\nUse -h or --help for usage information", file=sys.stderr)
        sys.exit(1)
    
    # Validate boot architecture is supported
    if boot_arch not in SUPPORTED_ARCHS:
        print(f"Error: Unknown architecture '{boot_arch}'", file=sys.stderr)
        print(f"\nSupported architectures: {', '.join(SUPPORTED_ARCHS)}", file=sys.stderr)
        sys.exit(1)
    
    # Validate boot mode is provided
    boot_mode = known_args.get('boot_mode')
    if boot_mode is None:
        print("Error: -boot mode=<mode> is required", file=sys.stderr)
        print("\nUse -h or --help for usage information", file=sys.stderr)
        sys.exit(1)

    # Validate boot mode is supported
    valid_modes = [mode for mode, desc in SUPPORTED_MODES[boot_arch]]
    if boot_mode not in valid_modes:
        print(f"Error: Unknown boot mode '{boot_mode}'", file=sys.stderr)
        modes_list = [f"{mode} ({desc})" for mode, desc in SUPPORTED_MODES[boot_arch]]
        print(f"\nSupported modes for {boot_arch}: {', '.join(modes_list)}", file=sys.stderr)
        sys.exit(1)

    # Validate zynqmp-specific boot options
    if known_args.get('boot_pmufw') and boot_arch != 'zynqmp':
        print("Error: -boot pmufw is only supported for zynqmp", file=sys.stderr)
        sys.exit(1)
    if known_args.get('boot_fsbl') and boot_arch != 'zynqmp':
        print("Error: -boot fsbl is only supported for zynqmp", file=sys.stderr)
        sys.exit(1)
    
    # Now that we have done basic validation, we should ensure the required
    # qemu binaries are available and stop before doing a lot of expensive
    # processing.
    #
    # We will prefer any qemu binaries in the same path as ourselves,
    # otherwise falling back to the system path
    mb_cmd_path = None
    if boot_arch == 'zynqmp':
        if known_args.get('pmu_args'):
            mb_cmd_path = 'qemu-system-microblazeel'
            if os.path.exists(f'{binpath}/qemu-system-microblazeel'):
                mb_cmd_path = f'{binpath}/qemu-system-microblazeel'
        else:
            print(f"Error: {boot_arch} architecture, but no pmu_args defined", file=sys.stderr)
            sys.exit(1)

    if boot_arch in ['versal', 'versal2ve2vm']:
        if known_args.get('plm_args'):
            mb_cmd_path = 'qemu-system-microblazeel'
            if os.path.exists(f'{binpath}/qemu-system-microblazeel'):
                mb_cmd_path = f'{binpath}/qemu-system-microblazeel'
        else:
            print(f"Error: {boot_arch} architecture, but no plm_args defined", file=sys.stderr)
            sys.exit(1)

    rv_cmd_path = None
    if boot_arch == 'versal2ve2vm':
        if known_args.get('asu_args'):
            rv_cmd_path = 'qemu-system-riscv32'
            if os.path.exists(f'{binpath}/qemu-system-riscv32'):
                rv_cmd_path = f'{binpath}/qemu-system-riscv32'
        else:
            print(f"Error: {boot_arch} architecture, but no asu_args defined", file=sys.stderr)
            sys.exit(1)

    apu_cmd_path = 'qemu-system-aarch64'
    if os.path.exists(f'{binpath}/qemu-system-aarch64'):
        apu_cmd_path = f'{binpath}/qemu-system-aarch64'

    missing_qemu = False
    if mb_cmd_path:
        if not shutil.which(mb_cmd_path):
            print(f"Error: Unable to find required qemu binary {mb_cmd_path}", file=sys.stderr)
            missing_qemu = True

    if rv_cmd_path:
        if not shutil.which(rv_cmd_path):
            print(f"Error: Unable to find required qemu binary {rv_cmd_path}", file=sys.stderr)
            missing_qemu = True

    if apu_cmd_path:
        if not shutil.which(apu_cmd_path):
            print(f"Error: Unable to find required qemu binary {apu_cmd_path}", file=sys.stderr)
            missing_qemu = True

    if missing_qemu:
        sys.exit(1)

    # Parse drive arguments from unknown args
    drives = parse_drive_arguments(unknown_args)
    
    # Debug output - can be removed in production
    #print(f"Parsed drives: {drives}")

    # Handle machine-path
    machine_path = known_args.get('machine_path')
    temp_created = False
    
    if machine_path:
        # Expand and normalize the provided path
        machine_path = os.path.realpath(machine_path)
        # Create directory if it doesn't exist
        os.makedirs(machine_path, exist_ok=True)
    else:
        # Create temporary directory
        machine_path = tempfile.mkdtemp(prefix='qemu-amd-fpga-')
        temp_created = True
        #print(f"Created temporary machine directory: {machine_path}")
    
    # Register cleanup function for temporary directory
    def cleanup_temp_dir():
        if temp_created and os.path.exists(machine_path):
            try:
                shutil.rmtree(machine_path)
                #print(f"Cleaned up temporary directory: {machine_path}")
            except Exception as e:
                print(f"Warning: Failed to clean up temporary directory: {e}", file=sys.stderr)
    
    if temp_created:
        atexit.register(cleanup_temp_dir)
    
    print("")

    # Get boot mode description
    boot_mode_name = get_boot_mode_name(known_args['boot_arch'], known_args.get('boot_mode'))

    # Configure based on architecture
    print(f"Configuring for {known_args['boot_arch']} mode {boot_mode_name}...")
    
    if boot_arch == 'zynq':
        configure_zynq(known_args, unknown_args, drives, machine_path)
    elif boot_arch == 'zynqmp':
        configure_zynqmp(known_args, unknown_args, drives, machine_path)
    elif boot_arch == 'versal':
        configure_versal(known_args, unknown_args, drives, machine_path)
    elif boot_arch == 'versal2ve2vm':
        configure_versal2ve2vm(known_args, unknown_args, drives, machine_path)
    
    if not QEMU_APU_ARGS:
        print(f"ERROR: Nothing to do!", file=sys.stderr)
        sys.exit(1)

    mb_cmd_list = []
    mb_type = ""
    rv_cmd_list = []
    rv_type = ""

    if QEMU_PMU_ARGS and boot_arch == 'zynqmp':
        mb_cmd_list = [mb_cmd_path] + QEMU_PMU_ARGS + ['-machine-path', machine_path]
        mb_type = "PMU"

    elif QEMU_PLM_ARGS and boot_arch in ['versal', 'versal2ve2vm']:
        mb_cmd_list = [mb_cmd_path] + QEMU_PLM_ARGS + ['-machine-path', machine_path]
        mb_type = "PLM"

    if QEMU_ASU_ARGS and boot_arch == 'versal2ve2vm':
        rv_cmd_list = [rv_cmd_path] + QEMU_ASU_ARGS + ['-machine-path', machine_path]
        rv_type = "ASU"

    # qemu-system-aarch64 is valid for both Zynq (32-bit) and the 64-bit SoCs
    # Build command as list to avoid shell injection vulnerabilities
    apu_cmd_list = [apu_cmd_path] + QEMU_APU_ARGS + ['-machine-path', machine_path]

    process_mb = None
    process_rv = None
    process_apu = None

    if mb_cmd_list:
        print(f"\n{mb_type} instance cmd: {' '.join(mb_cmd_list)}\n")
        process_mb = subprocess.Popen(mb_cmd_list, stderr=subprocess.PIPE)

    if rv_cmd_list:
        print(f"\n{rv_type} instance cmd: {' '.join(rv_cmd_list)}\n")
        process_rv = subprocess.Popen(rv_cmd_list, stderr=subprocess.PIPE)

    apu_rc = 0
    if apu_cmd_list:
        print(f"\nAPU instance cmd: {' '.join(apu_cmd_list)}\n")
        process_apu = subprocess.Popen(apu_cmd_list, stderr=subprocess.PIPE)

    rc = 0
    error_msg = ""

    if apu_cmd_list and process_apu != None:
        apu_rc = process_apu.wait()
        if apu_rc:
            rc = apu_rc
            error_msg += f"\nAPU instance {apu_cmd_list[0]} failed ({apu_rc}):\n{process_apu.stderr.read().decode()}"
            # If APU failed, terminate other processes if still running
            if process_mb and process_mb.poll() is None:
                process_mb.terminate()
            if process_rv and process_rv.poll() is None:
                process_rv.terminate()
            
    try:
        if mb_cmd_list and process_mb != None:
            # Check if process already exited before waiting
            mb_rc = process_mb.poll()
            if mb_rc is None:
                mb_rc = process_mb.wait()
            if mb_rc: # < 0 process was terminated
                rc = rc + mb_rc
                error_msg += f"\n{mb_type} instance {mb_cmd_list[0]} failed ({mb_rc}):\n{process_mb.stderr.read().decode()}"
    except (subprocess.TimeoutExpired, KeyboardInterrupt):
        process_mb.kill()
    
    try:
        if rv_cmd_list and process_rv != None:
            # Check if process already exited before waiting
            rv_rc = process_rv.poll()
            if rv_rc is None:
                rv_rc = process_rv.wait()
            if rv_rc: # < 0 process was terminated
                rc = rc + rv_rc
                error_msg += f"\nQEMU {rv_cmd_list[0]} failed ({rv_rc}):\n{process_rv.stderr.read().decode()}"
    except (subprocess.TimeoutExpired, KeyboardInterrupt):
        process_rv.kill()

    if error_msg:
        print(error_msg)

    sys.exit(rc)


if __name__ == '__main__':
    main()
