Skip to content

driver: add BCUResetDriver to support bcu tool #1273

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions doc/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,27 @@ Arguments:
Used by:
- `SerialDriver`_

USBResetPort
+++++++++++++
Describes the resource required to use the `bcu <https://github.com/nxp-imx/bcu#readme>`_ tool.

.. code-block:: yaml

USBResetPort:
board: 'imx8dxlevk'
match:
'sys_name': '2-1.3.3'

Arguments:
- board (str): is the board type and can be retrieved with ``bcu lsboard``
- match (dict): key and value pairs for a udev match, see `udev Matching`_ ;
we need the sys_name as the path of the board serial - it can be retrieved
with ``bcu lsftdi`` command or from ``dmesg`` of the workstation after
you unplug/plug the board serial cable

Used by:
- `BCUResetDriver`_

Power Ports
~~~~~~~~~~~

Expand Down Expand Up @@ -1684,6 +1705,38 @@ Arguments:
- password (str): optional, password to use for access to the shell
- login_timeout (int, default=60): timeout for access to the shell

BCUResetDriver
~~~~~~~~~~~~~~
BCUResetDriver is the driver that calls `bcu <https://github.com/nxp-imx/bcu#readme>`_ tool. It runs the following command on the exporter:

.. code-block::

bcu reset <mode> -board=USBResetPort.board \
-id=USBResetPort.path \
-delay=BCUResetDriver.delay

Binds to:
port:
- `USBResetPort`_

Implements:
- :any:`BootCfgProtocol`
- :any:`ResetProtocol`

.. code-block:: yaml

BCUResetDriver:
qspi_cfg: 'spi'

Arguments:
- delay (int, default=1000): time in ms before bcu reset
- emmc_cfg (str, default="emmc"): boot mode for emmc
- sd_cfg (str, default="sd"): boot mode for sd
- usb_cfg (str, default="usb"): boot mode for usb serial download
- qspi_cfg (str, default=""): boot mode for spi

To find out boot modes for a specific board use ``bcu lsbootmode -board=<board>``

ExternalConsoleDriver
~~~~~~~~~~~~~~~~~~~~~
An ExternalConsoleDriver implements the `ConsoleProtocol` on top of a command
Expand Down
36 changes: 36 additions & 0 deletions examples/bcu/bcu_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import pytest


@pytest.fixture()
def switchcfg(target):
# bcu tool needs to be installed on workstation
return target.get_driver('BCUResetDriver')

@pytest.fixture()
def shell(target):
s = target.get_driver('ShellDriver')
#in case board remained booted from a different test
target.deactivate(s)
return s

@pytest.mark.lg_feature("SDcard")
def test_boot_from_sdcard(target, switchcfg, shell):
# Note that you need a bootable image already on sd
switchcfg.sd()
try:
target.activate(shell)
except:
assert False, "Board failed to boot"
stdout, stderr, returncode = shell.run('dmesg | grep root=/dev/mmcblk1')
assert returncode == 0, "Board booted from wrong environment"

def test_boot_from_emmc(target, switchcfg, shell):
# Note that you need a bootable image already on emmc
switchcfg.emmc()
try:
target.activate(shell)
except:
assert False, "Board failed to boot"
stdout, stderr, returncode = shell.run('dmesg | grep root=/dev/mmcblk0')
assert returncode == 0, "Board booted from wrong environment"

20 changes: 20 additions & 0 deletions examples/bcu/local.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
features:
- SDcard
targets:
main:
resources:
RawSerialPort:
port: "/dev/ttyUSB2"
USBResetPort:
board: 'imx8dxlevk'
match:
'sys_name': '3-11.3'
drivers:
SerialDriver: {}
BCUResetDriver:
qspi_cfg: 'spi'
ShellDriver:
prompt: 'root@[\w-]+:[^ ]+ '
login_prompt: ' login: '
username: 'root'
login_timeout: 180
63 changes: 62 additions & 1 deletion labgrid/driver/resetdriver.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
import attr

from ..factory import target_factory
from ..protocol import DigitalOutputProtocol, ResetProtocol
from ..protocol import DigitalOutputProtocol, ResetProtocol, BootCfgProtocol
from ..resource.remote import NetworkUSBResetPort
from ..resource.udev import USBResetPort
from ..step import step
from .common import Driver
from ..util.helper import processwrapper

@target_factory.reg_driver
@attr.s(eq=False)
Expand All @@ -24,3 +27,61 @@ def reset(self):
self.output.set(True)
time.sleep(self.delay)
self.output.set(False)

@target_factory.reg_driver
@attr.s(eq=False)
class BCUResetDriver(Driver, ResetProtocol, BootCfgProtocol):
Copy link
Member

Choose a reason for hiding this comment

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

As BCU supports more than reset, perhaps this should be BCUDriver or NXPBCUDriver. And be in a separate file. Functions for power measurement could then be added to the driver later.

Copy link
Author

Choose a reason for hiding this comment

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

Agree, we just met recently a situation where another function from bcu (set_boot_mode) was needed.

"""BCUResetDriver - Driver using https://github.com/NXPmicro/bcu and board
serial port as USBResetPort to reset the target into different states"""

bindings = {
"port": {USBResetPort, NetworkUSBResetPort},
}

delay = attr.ib(default=1000, validator=attr.validators.instance_of(int))
emmc_cfg = attr.ib(default="emmc", validator=attr.validators.instance_of(str))
sd_cfg = attr.ib(default="sd", validator=attr.validators.instance_of(str))
usb_cfg = attr.ib(default="usb", validator=attr.validators.instance_of(str))
qspi_cfg = attr.ib(default="", validator=attr.validators.instance_of(str))
Comment on lines +42 to +45
Copy link
Member

Choose a reason for hiding this comment

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

Are these different from board to board? How would custom settings look like?

Copy link
Author

Choose a reason for hiding this comment

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

Mostly they are the same , but there are cases when they are different- usually qspi_cfg is different from board to board, and for the rest it might be the case where you need m_usb or m_emmc ... like from M core and not A core. Also you may have different emmc config for the same board and you need to select the right one: like for 8ULP
example config:

BCUResetDriver:
emmc_cfg: 'emmc_s'

How it looks in bcu:

sd@sd-01:~$ bcu lsbootmode -board=imx8ulpevk
version bcu_1.1.52-0-g17ab144
board model is imx8ulpevk

available boot mode:

    fuse
    usb
    emmc_s
    emmc_nor_lp
    emmc_nor
    nand_nor
    nor_s
    nor_nor_lp
    nor_nor

done

Copy link
Member

Choose a reason for hiding this comment

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

Hmm, so you have 'emmc' as an alias which is used by the strategy and then maps to different boot modes on different places/boards?

Copy link
Author

@ElenaGrigore ElenaGrigore Dec 15, 2023

Choose a reason for hiding this comment

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

in strategy I have a declaration like this (the controller part is related to the second driver that ca switch between boot modes , that one also can controll power):

   def __attrs_post_init__(self):
        super().__attrs_post_init__()
        if "controller" in self.target.env.config.get_features():
            self.switchcfg = self.power
        if "bcu" in self.target.env.config.get_features():
            self.switchcfg = self.target.get_driver(BCUResetDriver)

and then I have a state like this (state cycle reffer to power cycle of the board):

       elif status == Status.emmc:
            self.transition(Status.cycle)
            try:
               self.switchcfg.emmc()
            except:
               print("Board doesn't support remote switching or this state is not configured in board.yaml")

This means that for a board that has the configuration like this:

    drivers:
      BCUResetDriver:
        qspi_cfg: 'spi'

it will call bcu with bootmode emmc (the default config): bcu reset emmc -board=.. -id=..
and for a board that has this configuration:

   drivers:
     BCUResetDriver:
       emmc_cfg: 'emmc_s'

it will call bcu like this (using the config from yaml): bcu reset emmc_s -board=.. -id=..

And below is an example how strategy is used in test:

@pytest.mark.dependency(depends=['test_write_uboot_on_emmc'])
@pytest.mark.timeout(600)
@pytest.mark.flaky(reruns=2)
def test_boot_from_emmc_tftp_nfs(setup_teardown_test, target, uuu_strategy, request, tftp_nfs_config, uboot):
    uuu_strategy.transition("emmc")
    target.activate(uboot)
    try:
        uboot.run(tftp_nfs_config + " saveenv ;" + " run netboot")
    except:
        print("UbootDriver marker check failed or failed to stop at prompt ")
    uuu_strategy.activate_shell_driver()
    response = check_boot_patterns(request, target, patterns=[r"Boot:\s+("+BootModes.emmc.value+")"], exclude_board=["8M", "9"])
    assert response, "Check Artifacts folder for full log"


def __attrs_post_init__(self):
super().__attrs_post_init__()
self.tool = 'bcu'

@Driver.check_active
@step()
def reset(self, mode=""):
cmd = self.port.command_prefix + [
self.tool,
"reset", mode,
"-board={}".format(self.port.board),
"-id={}".format(self.port.path),
"-delay={}".format(str(self.delay)),
]
try:
print(str(processwrapper.check_output(cmd).decode()))
except subprocess.CalledProcessError as e:
raise

@Driver.check_active
@step()
def sd(self):
self.reset(self.sd_cfg)

@Driver.check_active
@step()
def emmc(self):
self.reset(self.emmc_cfg)

@Driver.check_active
@step()
def usb(self):
self.reset(self.usb_cfg)

@Driver.check_active
@step()
def qspi(self):
if not self.qspi_cfg:
print("QSPI mode is not set, board is reseting in current mode")
self.reset(self.qspi_cfg)
Comment on lines +65 to +86
Copy link
Member

Choose a reason for hiding this comment

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

As we currently don't have other boot config drivers, I'd drop the BootCfgProtocol and just call reset("sd" from the strategy or test cases?

Copy link
Author

Choose a reason for hiding this comment

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

I used the BootCfgProtocol because we have a second boot config driver which I was planning to submit and it helps in strategy - I call the same method from the protocol. Not sure how to address this for now ?

Copy link
Member

Choose a reason for hiding this comment

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

Hmm. The supported boot modes will likely differ often from board to board, so hard-coding them in the BootCfgProtocol doesn't seem very useful. Perhaps only use a single method with a string option? Perhaps upload the code for your second driver somewhere (even if just in a https://gist.github.com/) so we could get a better impression.

The BootCfgProtocol class could still be added when adding the second implementation.

Copy link
Author

Choose a reason for hiding this comment

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

Ok, so for bcu driver I will remove the BootCfgProtocol - indeed supported boot modes might differ between boards ; using a single method with string parameter seems a better option (I was thinking at it back then when I implemented the driver but I can't remember why I didn't use it).
I will try to upload the second driver as you suggested, but it might take some time.


1 change: 1 addition & 0 deletions labgrid/protocol/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from .consoleprotocol import ConsoleProtocol
from .linuxbootprotocol import LinuxBootProtocol
from .powerprotocol import PowerProtocol
from .bootcfgprotocol import BootCfgProtocol
from .filetransferprotocol import FileTransferProtocol
from .infoprotocol import InfoProtocol
from .digitaloutputprotocol import DigitalOutputProtocol
Expand Down
20 changes: 20 additions & 0 deletions labgrid/protocol/bootcfgprotocol.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import abc


class BootCfgProtocol(abc.ABC):
@abc.abstractmethod
def usb(self):
raise NotImplementedError

@abc.abstractmethod
def sd(self):
raise NotImplementedError

@abc.abstractmethod
def emmc(self):
raise NotImplementedError

@abc.abstractmethod
def qspi(self):
raise NotImplementedError

20 changes: 20 additions & 0 deletions labgrid/remote/exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,25 @@ def __attrs_post_init__(self):
super().__attrs_post_init__()
self.data['cls'] = f"Remote{self.cls}".replace("Network", "")

@attr.s(eq=False)
class USBResetPortExport(USBGenericExport):
"""ResourceExport for ports used by bcu"""

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

def _get_params(self):
"""Helper function to return parameters"""
return {
'host': self.host,
'busnum': self.local.busnum,
'devnum': self.local.devnum,
'path': self.local.path,
'vendor_id': self.local.vendor_id,
'model_id': self.local.model_id,
'board': self.local.board,
}

exports["AndroidFastboot"] = USBGenericExport
exports["AndroidUSBFastboot"] = USBGenericRemoteExport
exports["DFUDevice"] = USBGenericExport
Expand All @@ -512,6 +531,7 @@ def __attrs_post_init__(self):
exports["USBSDMuxDevice"] = USBSDMuxExport
exports["USBSDWireDevice"] = USBSDWireExport
exports["USBDebugger"] = USBGenericExport
exports["USBResetPort"] = USBResetPortExport

exports["USBMassStorage"] = USBGenericExport
exports["USBVideo"] = USBGenericExport
Expand Down
12 changes: 12 additions & 0 deletions labgrid/resource/remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,3 +411,15 @@ class RemoteNFSProvider(RemoteBaseProvider):
@attr.s(eq=False)
class RemoteHTTPProvider(RemoteBaseProvider):
pass


@target_factory.reg_resource
@attr.s(eq=False)
class NetworkUSBResetPort(RemoteUSBResource):
board = attr.ib(
default=None,
validator=attr.validators.instance_of(str)
)
def __attrs_post_init__(self):
self.timeout = 10.0
super().__attrs_post_init__()
12 changes: 12 additions & 0 deletions labgrid/resource/udev.py
Original file line number Diff line number Diff line change
Expand Up @@ -708,3 +708,15 @@ def filter_match(self, device):
return False

return super().filter_match(device)

@target_factory.reg_resource
@attr.s(cmp=False)
class USBResetPort(USBResource):
Copy link
Member

Choose a reason for hiding this comment

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

Isn't this name too generic? Perhaps BCUController?

As BCU also supports power measurement, the same resource would also be used to export that functionality.

Copy link
Author

Choose a reason for hiding this comment

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

Agree, I will change the port name to something more specific: BCUController or BCUPort

"""The USBResetPort is the board serial cable (not the interface),
it is identified via usb using udev and it accepts any vendor/product id

Args:
board (str): mandatory arg to declare a board type recognized by bcu
"""

board = attr.ib(default=None, validator=attr.validators.instance_of(str))
Copy link
Member

Choose a reason for hiding this comment

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

Could the board be derived from USB VID/PID or any other info exposed via USB? That would avoid the need to configure it manually.

That way, the exporter would just need a list of USB paths and any compatible board could be used automatically.

Copy link
Author

Choose a reason for hiding this comment

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

Unfortunately no. This is the USB serial connection from the board and ftdi chip is the same on most of the boards you cannot detect the board type from it. But I can make it as a custom setting for the driver and not for the resource and in this way I wouldn't need a new resource created but just a resource of cls USBSerialPort. What do you think?

Copy link
Member

Choose a reason for hiding this comment

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

All USBSerialPorts are exported via ser2net, which would probably conflict with local access to the device from the bcu tool. Adding RFC2217 protocol support to bcu is probably too much to ask. :)

Are you using the ttyUSB device or libftdi to communicate with the MCU on the board?

Copy link
Author

@ElenaGrigore ElenaGrigore Dec 15, 2023

Choose a reason for hiding this comment

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

bcu is using libftdi as far as I know. In here better to keep the new port added but move the board attribute from resource and make it a driver attribute ? or leave it as it is ?