Skip to content
36 changes: 35 additions & 1 deletion doc/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -991,6 +991,7 @@ Arguments:

Used by:
- `OpenOCDDriver`_
- `PyOCDDriver`_

NetworkUSBDebugger
~~~~~~~~~~~~~~~~~~
Expand Down Expand Up @@ -1971,7 +1972,6 @@ Binds to:

Implements:
- :any:`ConsoleProtocol`
- :any:`ResetProtocol`

Arguments:
- txdelay (float, default=0.0): time in seconds to wait before sending a chunk
Expand Down Expand Up @@ -2331,6 +2331,40 @@ Arguments:
- board_config (str): optional, board config in the ``openocd/scripts/board/`` directory
- load_commands (list of str): optional, load commands to use instead of ``init``, ``bootstrap {filename}``, ``shutdown``

PyOCDDriver
~~~~~~~~~~~~~
An :any:`PyOCDDriver` controls *PyOCD* to bootstrap a target with a bootloader.

Binds to:
interface:
- `USBDebugger`_
- `NetworkUSBDebugger`_

Implements:
- :any:`BootstrapProtocol`
- :any:`ResetProtocol`

.. code-block:: yaml

PyOCDDriver:
image: 'bitstream'
target_name: 'nrf52832'
frequency: "400khz"
config: 'pyocd.yaml'
serial: '1234567890'
load_commands:
- 'init'
- 'svf -quiet {filename}'
- 'exit'

Arguments:
- image (str): optional, name of the image to bootstrap onto the device
- target_name (str): optional, set the target type. Should be one of the targets listed by 'pyocd list --targets'.
- frequency (str): optional, SWD/JTAG clock frequency in Hz
- config (str): optional, PyOCD configuration file
- serial (str): optional, USB-Serial to identify device if multiple are present.
- load_commands (list of str): optional, additional command line parameters for load to use instead of ``-e``, ``sector``

QuartusHPSDriver
~~~~~~~~~~~~~~~~
A :any:`QuartusHPSDriver` controls the "Quartus Prime Programmer and Tools" to
Expand Down
4 changes: 4 additions & 0 deletions doc/man/device-config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ TOOLS KEYS
Path to the openocd binary, used by the OpenOCDDriver.
See: https://openocd.org/

``pyocd``
Path to the pyocd binary, used by the PyOCDDriver.
See: https://pyocd.io/

``quartus_hps``
Path to the quartus_hps binary, used by the QuartusHPSDriver.
See: https://www.intel.com/content/www/us/en/docs/programmable/683039/22-3/hps-flash-programmer.html
Expand Down
1 change: 1 addition & 0 deletions labgrid/driver/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .fastbootdriver import AndroidFastbootDriver
from .dfudriver import DFUDriver
from .openocddriver import OpenOCDDriver
from .pyocddriver import PyOCDDriver
from .quartushpsdriver import QuartusHPSDriver
from .flashromdriver import FlashromDriver
from .onewiredriver import OneWirePIODriver
Expand Down
108 changes: 108 additions & 0 deletions labgrid/driver/pyocddriver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import attr

from ..factory import target_factory
from ..protocol import BootstrapProtocol, ResetProtocol
from ..step import step
from ..util.managedfile import ManagedFile
from ..util.helper import processwrapper
from .common import Driver


@target_factory.reg_driver
@attr.s(eq=False)
class PyOCDDriver(Driver, BootstrapProtocol, ResetProtocol):

priorities = {ResetProtocol: 5}

bindings = {
"interface": {
"USBDebugger",
"NetworkUSBDebugger",
},
}

image = attr.ib(
default=None,
validator=attr.validators.optional(attr.validators.instance_of(str)),
)
load_commands = attr.ib(
default=None,
validator=attr.validators.optional(attr.validators.instance_of((str, list))),
)
target_name = attr.ib(
default=None,
validator=attr.validators.optional(attr.validators.instance_of(str)),
)
frequency = attr.ib(
default=None,
validator=attr.validators.optional(attr.validators.instance_of(str)),
)
config = attr.ib(
default=None,
validator=attr.validators.optional(attr.validators.instance_of(str)),
)
serial = attr.ib(
default=None,
validator=attr.validators.optional(attr.validators.instance_of(str)),
)

def __attrs_post_init__(self):
super().__attrs_post_init__()

# FIXME make sure we always have an environment or config
if self.target.env:
self.tool = self.target.env.config.get_tool("pyocd")
if self.config:
self.config = self.target.env.config.resolve_path(self.config)
else:
self.tool = "pyocd"

def _run_commands(self, subcommand: str, commands: list | None = None):
cmd = [self.tool, subcommand]
if self.serial:
cmd += ["--uid", self.serial]
if self.target_name is not None and (not commands or "--target" not in commands):
cmd += ["--target", self.target_name]
if self.frequency is not None and (not commands or "--frequency" not in commands):
cmd += ["--frequency", self.frequency]

if self.config is not None:
mconfig = ManagedFile(self.config, self.interface)
mconfig.sync_to_resource()
cmd += ["--config", mconfig.get_remote_path()]
else:
cmd += ["--no-config"]

if commands:
cmd += commands
processwrapper.check_output(
command=self.interface.wrap_command(cmd),
print_on_silent_log=True,
)

@Driver.check_active
@step(args=["filename"])
def load(self, filename=None):

if filename is None and self.image is not None and self.target.env:
filename = self.target.env.config.get_image_path(self.image)

mf = ManagedFile(filename, self.interface)
mf.sync_to_resource()

if self.load_commands:
if isinstance(self.load_commands, str):
commands = self.load_commands.split()
else:
commands = self.load_commands

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed this while testing two consecutive load() calls on the same PyOCDDriver instance with list-based load_commands.

For example:

    load_commands: ["--frequency", "100"]

Then, in a custom test script:

    driver.load("first.hex")
    driver.load("second.hex")

The first call mutates the configured list to:

    ["--frequency", "100", "first.hex"]

As a result, the second call invokes:

    pyocd load --frequency 100 first.hex second.hex

This does not occur across separate labgrid-client bootstrap invocations, since each CLI invocation creates a new process and driver instance. It can occur whenever the same driver object is reused programmatically.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, thanks for pointing that out! I just added a new commit, that has a pytest and a fix for it.

else:
commands = ["-e", "sector"]

commands.append(mf.get_remote_path())

self._run_commands("load", commands)

@Driver.check_active
@step()
def reset(self):
self._run_commands("reset")
Loading
Loading