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.shRun 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 iioto 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_errorTypical 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:
Inherit from
labgrid.strategy.StrategyDefine required/optional bindings
Implement a state machine with an enum
Provide a
transition()methodRegister 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 -interactiveto verify xsdb worksEnsure 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, thentargets)
- Boot timeout:
Increase
wait_for_boot_timeoutCheck 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
dmesgto 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¶
Strategies API - Complete API reference for all strategies
Using Drivers - Information about available drivers
Configuring Resources - Resource configuration reference
Complete Boot Cycle Example - Complete boot workflow example
Labgrid documentation: https://labgrid.readthedocs.io/