Working with Strategies

Strategies are high-level state machines that coordinate multiple drivers to accomplish complex workflows. They abstract away the detailed choreography of hardware interactions, allowing test engineers to focus on the overall boot and test sequence.

Overview

A strategy represents a reusable workflow composed of multiple state transitions. Each transition may activate or deactivate drivers, execute commands, and wait for expected conditions. Strategies form the bridge between raw driver capabilities and practical test procedures.

Key Concepts

  • State Machine: Strategies are implemented as state machines with discrete states representing different phases of a workflow (e.g., powered_off, booting, shell).

  • Bindings: Each strategy declares required and optional driver/resource bindings that must be present on the target.

  • Transitions: The transition() method moves the strategy from one state to another, handling all intermediate steps automatically.

  • Activation Lifecycle: Drivers are activated and deactivated as needed by the strategy, not manually by the test.

  • Error Handling: StrategyError exceptions indicate invalid transitions or failed operations.

BootFPGASoC Strategy

Purpose: Boot an FPGA SoC device (e.g., Zynq UltraScale+) using an SD card mux and Kuiper release images.

State Machine:

The strategy manages 9 states:

        stateDiagram-v2
    [*] --> unknown

    unknown --> powered_off: Initialize
    powered_off --> sd_mux_to_host: Power off device
    sd_mux_to_host --> update_boot_files: Mux SD to host

    update_boot_files --> decide_image: Check update_image flag
    decide_image --> write_image: update_image=True
    decide_image --> copy_files: update_image=False
    write_image --> copy_files: Write full image
    copy_files --> sd_mux_to_dut: Copy boot files

    sd_mux_to_dut --> booting: Mux SD to device
    booting --> booted: Power on device
    booted --> shell: Wait for kernel + marker

    shell --> soft_off: Graceful shutdown
    soft_off --> [*]

    note right of sd_mux_to_host
        SD card accessible to host
    end note

    note right of write_image
        Optional: full image flash
    end note

    note right of booted
        Wait for Linux kernel + marker
    end note
    

Hardware Requirements:

  • Power control (VeSync, CyberPower, or other PowerProtocol implementation)

  • SD card mux (USBSDMuxDriver) to switch SD card between host and device

  • Mass storage access (MassStorageDriver) for copying files to SD card

  • Serial console access (ADIShellDriver) for boot monitoring and shell interaction

  • Kuiper release files (KuiperDLDriver) for boot artifacts

  • Optional: USB storage writer (USBStorageDriver) for full image flashing

Configuration Example:

targets:
  mydevice:
    resources:
      VesyncOutlet:
        outlet_names: 'Device Power'
        username: 'your_email@example.com'
        password: 'your_password'
        delay: 5.0

      USBSDMuxDriver:
        serial: '00012345'

      MassStorageDriver:
        path: '/dev/sda1'

      SerialPort:
        port: '/dev/ttyUSB0'
        baudrate: 115200

      KuiperDLDriver:
        release_version: '2024.r1'

    drivers:
      VesyncPowerDriver: {}
      USBSDMuxDriver: {}
      MassStorageDriver: {}
      ADIShellDriver:
        prompt: 'root@.*:.*#'
        login_prompt: 'login:'
        username: 'root'
        password: 'analog'
      KuiperDLDriver: {}

    strategies:
      BootFPGASoC:
        reached_linux_marker: 'analog'
        update_image: true  # Flash full image, not just boot files

Usage Example:

from labgrid import Environment
from adi_lg_plugins.strategies import BootFPGASoC

env = Environment("target.yaml")
target = env.get_target("mydevice")
strategy = target.get_strategy("BootFPGASoC")

# Boot the device to shell
strategy.transition("shell")

# Now shell access is available
shell = target.get_driver("ADIShellDriver")
shell.run_command("uname -a")

# Power off cleanly
strategy.transition("soft_off")

Advanced Usage - Full Image Flash:

When update_image=True, the strategy will flash the complete Kuiper release image to the SD card before copying individual boot files. This is useful for ensuring a clean filesystem:

# In target configuration:
# BootFPGASoC:
#   update_image: true

# The strategy automatically:
# 1. Muxes SD card to host
# 2. Writes full image using USB storage writer
# 3. Copies specific boot files on top
# 4. Muxes SD card back to device
# 5. Boots device

Error Handling:

The strategy raises StrategyError for invalid transitions. Common scenarios:

from labgrid.strategy import StrategyError

try:
    strategy.transition("shell")
except StrategyError as e:
    if "can not transition to unknown" in str(e):
        print("Cannot transition to initial state")
    elif "no transition found" in str(e):
        print("Invalid state transition")

State Awareness:

Always check the current state before transitioning:

from adi_lg_plugins.strategies.bootfpgasoc import Status

strategy = target.get_strategy("BootFPGASoC")

if strategy.status == Status.unknown:
    strategy.transition(Status.shell)
elif strategy.status == Status.shell:
    print("Already at shell")
elif strategy.status == Status.powered_off:
    strategy.transition(Status.shell)

BootFPGASoCSSH Strategy

Purpose: Boot an FPGA SoC device using SSH for file transfers instead of SD card mux. Useful when SSH access is available but SD card mux is not present.

State Machine:

The strategy manages 9 states with a two-stage boot process:

        stateDiagram-v2
    [*] --> unknown

    unknown --> powered_off: Initialize

    powered_off --> booting: Power on (if power driver)

    note right of powered_off
        Power control is optional
    end note

    booting --> booted: Wait for kernel + marker
    booted --> update_boot_files: SSH ready

    update_boot_files --> reboot: Transfer boot files via SSH

    note right of update_boot_files
        Validates and updates SSH IP address
    end note

    reboot --> booting_new: Issue reboot command
    booting_new --> shell: Wait for kernel + marker

    shell --> soft_off: Graceful shutdown
    soft_off --> [*]

    note right of booting
        First boot: initial system
    end note

    note right of booting_new
        Second boot: with updated files
    end note
    

Key Differences from BootFPGASoC:

  • Uses SSH (SSHDriver) instead of SD card mux for file transfer

  • Does not require mass storage driver or SD mux hardware

  • Power control is optional (devices may boot automatically on power)

  • More suitable for devices with network access

  • Faster boot cycles after initial boot (no physical SD mux switching)

Configuration Example:

targets:
  netdevice:
    resources:
      VesyncOutlet:
        outlet_names: 'Device Power'
        username: 'your_email@example.com'
        password: 'your_password'

      SerialPort:
        port: '/dev/ttyUSB0'
        baudrate: 115200

      NetworkInterface:
        hostname: 'analog.local'
        username: 'root'
        password: 'analog'

      KuiperDLDriver:
        release_version: '2024.r1'

    drivers:
      VesyncPowerDriver: {}
      ADIShellDriver:
        prompt: 'root@.*:.*#'
        login_prompt: 'login:'
        username: 'root'
        password: 'analog'
      SSHDriver:
        hostname: 'analog.local'
        username: 'root'
        password: 'analog'
      KuiperDLDriver: {}

    strategies:
      BootFPGASoCSSH:
        hostname: 'analog.local'
        reached_linux_marker: 'analog'

Usage Example:

env = Environment("target.yaml")
target = env.get_target("netdevice")
strategy = target.get_strategy("BootFPGASoCSSH")

# Boot to shell via SSH
strategy.transition("shell")

# Update boot files via SSH and reboot
strategy.transition("update_boot_files")
strategy.transition("reboot")
strategy.transition("shell")

Cleanup and Shutdown:

Always transition to soft_off to gracefully shutdown:

strategy.transition("soft_off")

Troubleshooting:

Boot files don’t transfer via SSH:
  • Verify SSH access works: ssh root@<device-ip>

  • Check SSH key is properly configured in resources

  • Ensure network connectivity between host and device

  • Verify pre_boot_boot_files and post_boot_boot_files paths are correct

First boot succeeds but restart fails:
  • This is expected behavior with SSH-based file transfer

  • Strategy only uploads files on first boot

  • To re-upload files, transition to powered_off and back to shell

SSH connection hangs during boot:
  • Increase wait_for_linux_prompt_timeout (default: 60s)

  • Check device is booting correctly via serial console

  • Verify network interface is configured in device tree

Permission denied errors:
  • Ensure SSH key has correct permissions (chmod 600)

  • Verify device allows root login via SSH

  • Check /boot partition is writable on device

BootSelMap Strategy

Purpose: Boot a dual-FPGA design with primary Zynq FPGA (running Linux) and secondary Virtex FPGA (booted via SelMap interface).

State Machine:

The strategy manages 11 states with dual-FPGA boot orchestration:

        stateDiagram-v2
    [*] --> unknown
    unknown --> powered_off: Initialize
    powered_off --> booting_zynq: Power on
    booting_zynq --> booted_zynq: Wait for boot

    booted_zynq --> update_zynq_boot_files: Check files needed
    update_zynq_boot_files --> update_zynq_boot_files: Restart if files uploaded
    update_zynq_boot_files --> update_virtex_boot_files: Zynq ready

    update_virtex_boot_files --> trigger_selmap_boot: Files uploaded
    trigger_selmap_boot --> wait_for_virtex_boot: Run SelMap script

    wait_for_virtex_boot --> wait_for_virtex_boot: Poll IIO device (30s)
    wait_for_virtex_boot --> wait_for_virtex_boot: Poll JESD status (120s)
    wait_for_virtex_boot --> booted_virtex: JESD complete

    booted_virtex --> shell: Activate shell
    shell --> soft_off: Graceful shutdown
    soft_off --> [*]

    note right of update_zynq_boot_files
        Self-transition for boot file restart
    end note

    note right of wait_for_virtex_boot
        Polls IIO and JESD completion
    end note
    

Hardware Requirements:

  • Power control for both FPGAs

  • Serial console access to Zynq (ADIShellDriver)

  • SSH access to Zynq after boot

  • SelMap interface connected from Zynq to Virtex for secondary FPGA programming

  • Kuiper release files for both Zynq and Virtex bitstreams

Configuration Example:

targets:
  dual_fpga:
    resources:
      VesyncOutlet:
        outlet_names: 'Dual FPGA Power'
        username: 'your_email@example.com'
        password: 'your_password'

      SerialPort:
        port: '/dev/ttyUSB0'
        baudrate: 115200

      NetworkInterface:
        hostname: 'zynq.local'
        username: 'root'
        password: 'analog'

      KuiperDLDriver:
        release_version: '2024.r1'

    drivers:
      VesyncPowerDriver: {}
      ADIShellDriver:
        prompt: 'root@.*:.*#'
        login_prompt: 'login:'
        username: 'root'
        password: 'analog'
      SSHDriver:
        hostname: 'zynq.local'
        username: 'root'
        password: 'analog'

    strategies:
      BootSelMap:
        reached_linux_marker: 'analog'
        ethernet_interface: 'eth0'
        iio_jesd_driver_name: 'axi-ad9081-rx-hpc'
        pre_boot_boot_files: null
        post_boot_boot_files: null

Usage Example:

env = Environment("target.yaml")
target = env.get_target("dual_fpga")
strategy = target.get_strategy("BootSelMap")

# Boot and wait for shell access
# The strategy automatically transitions through all intermediate states
strategy.transition("shell")

# Verify both FPGAs are booted
shell = target.get_driver("ADIShellDriver")
shell.run_command("cat /proc/device-tree/chosen/fpga/axi-ad9081-rx-hpc/status")

# Graceful shutdown
strategy.transition("soft_off")
Note:

The strategy automatically transitions through all intermediate states (powered_off → booting_zynq → booted_zynq → update_zynq_boot_files → update_virtex_boot_files → trigger_selmap_boot → wait_for_virtex_boot → booted_virtex → shell). You only need to request the final state.

Advanced: Pre/Post Boot Files:

The strategy supports optional pre-boot and post-boot file copying:

# In target configuration:
# BootSelMap:
#   pre_boot_boot_files: {'local_path': '/boot/remote_path'}
#   post_boot_boot_files: {'local_path': '/boot/remote_path'}

# Pre-boot files are copied before Zynq boot
# Post-boot files are copied after Zynq boots but before Virtex boot

Configuration Details:

The BootSelMap strategy requires careful configuration of boot files for both the Zynq (primary) and Virtex (secondary) FPGAs:

  • pre_boot_boot_files: Files uploaded to Zynq before Virtex configuration. Dictionary format: {local_path: remote_path}

    Example files: - Zynq device tree blob (system.dtb) - Zynq boot files (BOOT.BIN, image.ub)

    These files are uploaded via SSH, then the Zynq is rebooted to apply them.

  • post_boot_boot_files: Files uploaded after Zynq boots with new device tree. Dictionary format: {local_path: remote_path}

    Example files: - Virtex bitstream (.bin) - Virtex device tree overlay (.dtbo) - SelMap boot script (selmap_dtbo.sh)

    These files are used to configure the Virtex FPGA via SelMap interface.

  • ethernet_interface: Network interface name on target (e.g., “eth0”). Used to discover target IP address for SSH connections.

  • iio_jesd_driver_name: IIO device name to poll after Virtex boot. Example: “axi-ad9081-rx-hpc” for AD9081 transceiver. Strategy polls this device to verify Virtex has booted successfully.

Troubleshooting:

Zynq boots but Virtex doesn’t configure:
  • Check pre_boot_boot_files and post_boot_boot_files are correctly specified

  • Verify .dtbo and .bin files exist at specified paths

  • Check SelMap script exists: /boot/ci/selmap_dtbo.sh

  • Run script manually: cd /boot/ci && ./selmap_dtbo.sh -d vu11p.dtbo -b vu11p.bin

IIO JESD device not found (wait_for_virtex_boot timeout):
  • Increase timeout if device takes longer than 30s to appear

  • Check device tree is correct for your hardware

  • Verify Virtex bitstream matches Zynq device tree configuration

  • Run dmesg | grep iio to see IIO driver messages

JESD state machine doesn’t reach opt_post_running_stage:
  • Check JESD clock configuration in device tree

  • Verify AD9081 (or similar) transceiver is properly configured

  • Check for JESD sync errors: iio_attr -d <device> jesd204_fsm_error

  • Typical JESD states: link_setup → clocks → link → opt_post_running_stage

Files uploaded but boot still fails:
  • Strategy restarts after uploading pre_boot_boot_files

  • Check serial console for Zynq boot errors after restart

  • Verify uploaded files are valid (not corrupted)

  • Ensure sufficient space on /boot partition

“Permission denied” when uploading files via SSH:
  • Verify SSH key is configured correctly

  • Check target filesystem is mounted read-write

  • Ensure sufficient disk space on target

  • Try manual SSH: scp localfile root@<device>:/boot/

Restart loop with pre_boot_boot_files:
  • Strategy uploads files, restarts, and checks again

  • If files are different, it restarts again (infinite loop possible)

  • Ensure local files don’t change between boots

  • Check _copied_pre_boot_files flag is being set correctly

Best Practices

1. State Awareness

Always understand the current state before transitioning:

# Check current state
if strategy.status != Status.shell:
    strategy.transition("shell")

# Avoid redundant transitions
if strategy.status != Status.powered_off:
    strategy.transition("powered_off")

2. Error Recovery

Implement error handling for robust test sequences:

from labgrid.strategy import StrategyError

def safe_transition(strategy, target_state, max_retries=3):
    for attempt in range(max_retries):
        try:
            strategy.transition(target_state)
            return True
        except StrategyError as e:
            print(f"Transition failed (attempt {attempt+1}): {e}")
            if attempt < max_retries - 1:
                # Reset to known state
                try:
                    strategy.transition(Status.powered_off)
                except:
                    pass
                time.sleep(5)
    return False

3. Driver Lifecycle Management

Understand that strategies activate/deactivate drivers automatically. Do not manually activate drivers that are managed by the strategy:

# Good - Let strategy manage power driver
strategy.transition("shell")

# Bad - Don't double-activate
strategy.transition("shell")
power = target.get_driver("VesyncPowerDriver")  # Already active

4. Timeout Configuration

Set appropriate timeouts for slow hardware:

# In shell driver configuration
ADIShellDriver:
  prompt: 'root@.*:.*#'
  login_prompt: 'login:'
  username: 'root'
  password: 'analog'
  login_timeout: 120  # Increase for slow boots
  post_login_settle_time: 5  # Extra time after login

5. Cleanup on Failure

Always attempt graceful shutdown in test cleanup:

def test_device_functionality():
    try:
        strategy.transition("shell")
        # Run tests
        shell.run_command("some_test_command")
    finally:
        # Cleanup even if test fails
        try:
            strategy.transition("soft_off")
        except:
            pass  # Power off if soft off fails

6. Logging and Debugging

Enable debug logging to understand strategy behavior:

import logging

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger("labgrid")
logger.setLevel(logging.DEBUG)

strategy.transition("shell")  # Now see detailed debug output

Custom Strategy Development

Creating custom strategies follows the labgrid plugin pattern. A strategy must:

  1. Inherit from labgrid.strategy.Strategy

  2. Define required/optional bindings

  3. Implement a state machine with an enum

  4. Provide a transition() method

  5. Register with @target_factory.reg_driver

Minimal Example:

import enum
import attr
from labgrid.factory import target_factory
from labgrid.step import step
from labgrid.strategy import Strategy, StrategyError, never_retry

class MyStatus(enum.Enum):
    unknown = 0
    state_a = 1
    state_b = 2

@target_factory.reg_driver
@attr.s(eq=False)
class MyStrategy(Strategy):
    """Custom strategy template."""

    bindings = {
        "power": "PowerProtocol",
        "shell": "ADIShellDriver",
    }

    status = attr.ib(default=MyStatus.unknown)

    @never_retry
    @step()
    def transition(self, status, *, step):
        if not isinstance(status, MyStatus):
            status = MyStatus[status]

        if status == MyStatus.state_a:
            self.target.activate(self.power)
            self.power.on()
        elif status == MyStatus.state_b:
            self.transition(MyStatus.state_a)
            self.target.activate(self.shell)
        else:
            raise StrategyError(f"no transition to {status}")

        self.status = status

Advanced Pattern - Hooks:

@attr.s(eq=False)
class HookedStrategy(Strategy):
    """Strategy with before/after hooks."""

    status = attr.ib(default=Status.unknown)
    _hooks = attr.ib(factory=dict, init=False)

    def register_hook(self, state, hook_fn):
        """Register a callable to run after transitioning to state."""
        if state not in self._hooks:
            self._hooks[state] = []
        self._hooks[state].append(hook_fn)

    @never_retry
    @step()
    def transition(self, status, *, step):
        # ... transition logic ...

        # Run hooks
        if status in self._hooks:
            for hook in self._hooks[status]:
                hook()

BootFabric Strategy

Purpose: Boot logic-only Xilinx FPGAs (Virtex/Artix/Kintex) with Microblaze soft processors via JTAG.

Use Case: Useful for FPGA-based systems without SoC (no ARM cores), where the processor is implemented in FPGA fabric. Common with ADI high-speed data converter evaluation boards (AD9081, AD9371, etc.) on Xilinx FPGA development boards like VCU118.

State Machine:

        stateDiagram-v2
    [*] --> unknown
    unknown --> powered_off: Initialize
    powered_off --> powered_on: Power on FPGA
    powered_on --> flash_fpga: Flash bitstream and kernel via JTAG
    flash_fpga --> booted: Start kernel execution and wait for boot
    booted --> shell: Activate shell access
    shell --> soft_off: Graceful shutdown
    soft_off --> [*]

    note right of flash_fpga
        Flash bitstream, download kernel, start execution
    end note

    note right of booted
        Wait for kernel boot marker (e.g., "login:")
    end note
    

Configuration Example:

targets:
  vcu118:
    resources:
      RawSerialPort:
        port: "/dev/ttyUSB0"
        speed: 115200

      XilinxDeviceJTAG:
        root_target: 1
        microblaze_target: 3
        bitstream_path: "/builds/system_top.bit"
        kernel_path: "/builds/simpleImage.vcu118.strip"

      XilinxVivadoTool:
        vivado_path: "/tools/Xilinx/Vivado"
        version: "2023.2"

      NetworkPowerPort:
        model: "gude"
        host: "192.168.1.100"
        index: 1

    drivers:
      SerialDriver: {}
      ADIShellDriver: {}
      XilinxJTAGDriver: {}
      NetworkPowerDriver: {}

      BootFabric:
        reached_boot_marker: "login:"
        wait_for_boot_timeout: 120
        verify_iio_device: "axi-ad9081-rx-hpc"

Usage Example:

from labgrid import Environment

# Load environment
env = Environment("vcu118.yaml")
target = env.get_target("vcu118")

# Get strategy and boot
strategy = target.get_driver("BootFabric")
strategy.transition("shell")

# Run commands
shell = target.get_driver("ADIShellDriver")
stdout, _, _ = shell.run("cat /proc/cpuinfo")
print(stdout)

# Shutdown
strategy.transition("soft_off")

Attributes:

  • reached_boot_marker (str): String to expect in console when boot complete (default: “login:”)

  • wait_for_boot_timeout (int): Seconds to wait for boot marker (default: 120)

  • verify_iio_device (str, optional): IIO device name to verify after boot

Troubleshooting:

Bitstream flash fails:
  • Verify JTAG cable is connected

  • Check bitstream file exists and path is correct

  • Run xsdb -interactive to verify xsdb works

  • Ensure FPGA is powered on

Kernel download fails:
  • Verify kernel file exists and path is correct

  • Ensure bitstream was flashed first

  • Check Microblaze target ID matches your hardware (run xsdb, then targets)

Boot timeout:
  • Increase wait_for_boot_timeout

  • Check serial console is properly connected

  • Verify kernel is compatible with bitstream design

IIO device not found:
  • Check kernel has appropriate IIO drivers compiled

  • Verify device tree matches your hardware

  • Run dmesg to see kernel boot messages

State Transition Reference

This section provides quick reference tables for valid state transitions in each strategy.

BootSelMap State Transitions:

From State

To State

Actions Performed

unknown

powered_off

Initialize, power off

powered_off

booting_zynq

Power on Zynq

booting_zynq

booted_zynq

Wait for Zynq boot

booted_zynq

update_zynq_boot_files

Upload Zynq files via SSH (if configured)

update_zynq_boot_files

update_zynq_boot_files

Restart if boot files uploaded

update_zynq_boot_files

update_virtex_boot_files

Zynq ready for next stage

update_virtex_boot_files

trigger_selmap_boot

Virtex files uploaded

trigger_selmap_boot

wait_for_virtex_boot

Run SelMap script

wait_for_virtex_boot

booted_virtex

Verify IIO device and JESD completion

booted_virtex

shell

Activate shell driver

shell

soft_off

Graceful shutdown

soft_off

(end)

Both FPGAs powered down

BootFabric State Transitions:

From State

To State

Actions Performed

unknown

powered_off

Initialize

powered_off

powered_on

Power on FPGA

powered_on

flash_fpga

Flash bitstream via JTAG

flash_fpga

booted

Download kernel and start execution

booted

shell

Wait for boot marker and activate shell

shell

soft_off

Graceful shutdown

soft_off

(end)

FPGA powered down

See Also