aboutsummaryrefslogtreecommitdiff
path: root/targets/rp2040
diff options
context:
space:
mode:
Diffstat (limited to 'targets/rp2040')
-rw-r--r--targets/rp2040/BUILD.bazel2
-rw-r--r--targets/rp2040/BUILD.gn22
-rw-r--r--targets/rp2040/pico_executable.gni8
-rw-r--r--targets/rp2040/py/BUILD.gn37
-rw-r--r--targets/rp2040/py/pyproject.toml16
-rw-r--r--targets/rp2040/py/rp2040_utils/__init__.py0
-rw-r--r--targets/rp2040/py/rp2040_utils/device_detector.py183
-rw-r--r--targets/rp2040/py/rp2040_utils/py.typed0
-rw-r--r--targets/rp2040/py/rp2040_utils/unit_test_client.py58
-rw-r--r--targets/rp2040/py/rp2040_utils/unit_test_runner.py195
-rw-r--r--targets/rp2040/py/rp2040_utils/unit_test_server.py139
-rw-r--r--targets/rp2040/py/setup.cfg34
-rw-r--r--targets/rp2040/target_docs.rst155
13 files changed, 789 insertions, 60 deletions
diff --git a/targets/rp2040/BUILD.bazel b/targets/rp2040/BUILD.bazel
index ddd50ed85..d37fe0d8c 100644
--- a/targets/rp2040/BUILD.bazel
+++ b/targets/rp2040/BUILD.bazel
@@ -25,7 +25,7 @@ licenses(["notice"])
# is missing from the bazel build. There's no plans yet to do a Bazel build for
# the Pi Pico.
#
-# TODO(b/260639642): Support Pico in the Bazel build.
+# TODO: b/260639642 - Support Pico in the Bazel build.
pw_cc_library(
name = "pico_logging_test_main",
srcs = [
diff --git a/targets/rp2040/BUILD.gn b/targets/rp2040/BUILD.gn
index 329d95ea6..9e2c9e536 100644
--- a/targets/rp2040/BUILD.gn
+++ b/targets/rp2040/BUILD.gn
@@ -20,6 +20,10 @@ import("$dir_pw_docgen/docs.gni")
import("$dir_pw_toolchain/arm_gcc/toolchains.gni")
import("$dir_pw_toolchain/generate_toolchain.gni")
+declare_args() {
+ pw_targets_ENABLE_RP2040_TEST_RUNNER = false
+}
+
if (current_toolchain != default_toolchain) {
pw_source_set("pico_logging_test_main") {
deps = [
@@ -30,6 +34,13 @@ if (current_toolchain != default_toolchain) {
]
sources = [ "pico_logging_test_main.cc" ]
}
+
+ # We don't want this linked into the boot_stage2 binary, so make the printf
+ # float config a source set added to pw_build_LINK_DEPS (which is dropped on
+ # the boot_stage2 binary) rather than as a default_config.
+ pw_source_set("float_printf_adapter") {
+ all_dependent_configs = [ "$dir_pw_toolchain/arm_gcc:enable_float_printf" ]
+ }
}
generate_toolchain("rp2040") {
@@ -47,13 +58,19 @@ generate_toolchain("rp2040") {
pw_build_EXECUTABLE_TARGET_TYPE = "pico_executable"
pw_build_EXECUTABLE_TARGET_TYPE_FILE =
get_path_info("pico_executable.gni", "abspath")
+ if (pw_targets_ENABLE_RP2040_TEST_RUNNER) {
+ _test_runner_script = "py/rp2040_utils/unit_test_client.py"
+ pw_unit_test_AUTOMATIC_RUNNER =
+ get_path_info(_test_runner_script, "abspath")
+ }
pw_unit_test_MAIN = "$dir_pigweed/targets/rp2040:pico_logging_test_main"
pw_assert_BACKEND = dir_pw_assert_basic
pw_log_BACKEND = dir_pw_log_basic
- pw_sys_io_BACKEND = dir_pw_sys_io_pico
+ pw_sys_io_BACKEND = dir_pw_sys_io_rp2040
pw_sync_INTERRUPT_SPIN_LOCK_BACKEND =
"$dir_pw_sync_baremetal:interrupt_spin_lock"
+ pw_chrono_SYSTEM_CLOCK_BACKEND = "$dir_pw_chrono_rp2040:system_clock"
pw_sync_MUTEX_BACKEND = "$dir_pw_sync_baremetal:mutex"
pw_rpc_CONFIG = "$dir_pw_rpc:disable_global_mutex"
@@ -63,8 +80,11 @@ generate_toolchain("rp2040") {
pw_build_LINK_DEPS = [
"$dir_pw_assert:impl",
"$dir_pw_log:impl",
+ get_path_info(":float_printf_adapter", "abspath"),
]
+ default_configs += [ "$dir_pw_build:extra_strict_warnings" ]
+
current_cpu = "arm"
current_os = ""
}
diff --git a/targets/rp2040/pico_executable.gni b/targets/rp2040/pico_executable.gni
index baba6f3c0..14eb1026c 100644
--- a/targets/rp2040/pico_executable.gni
+++ b/targets/rp2040/pico_executable.gni
@@ -33,8 +33,11 @@ template("pico_executable") {
forward_variables_from(invoker, "*")
}
- pw_exec(_uf2_name) {
+ pw_exec(target_name) {
_elf2uf2_target = "$dir_pw_third_party/pico_sdk/src:elf2uf2($dir_pigweed/targets/host:host_clang_debug)"
+ if (host_os == "win") {
+ _elf2uf2_target = "$dir_pw_third_party/pico_sdk/src:elf2uf2($dir_pigweed/targets/host:host_gcc_debug)"
+ }
_uf2_out_path = "${target_out_dir}/${_uf2_name}"
deps = [
":${_elf_name}",
@@ -47,8 +50,5 @@ template("pico_executable") {
]
outputs = [ _uf2_out_path ]
}
- group(target_name) {
- deps = [ ":${_uf2_name}" ]
- }
}
}
diff --git a/targets/rp2040/py/BUILD.gn b/targets/rp2040/py/BUILD.gn
new file mode 100644
index 000000000..ef0eee7a0
--- /dev/null
+++ b/targets/rp2040/py/BUILD.gn
@@ -0,0 +1,37 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/python.gni")
+
+pw_python_package("py") {
+ setup = [
+ "pyproject.toml",
+ "setup.cfg",
+ ]
+ sources = [
+ "rp2040_utils/__init__.py",
+ "rp2040_utils/device_detector.py",
+ "rp2040_utils/unit_test_client.py",
+ "rp2040_utils/unit_test_runner.py",
+ "rp2040_utils/unit_test_server.py",
+ ]
+ pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
+ python_deps = [
+ "$dir_pw_cli/py",
+ "$dir_pw_unit_test/py",
+ ]
+}
diff --git a/targets/rp2040/py/pyproject.toml b/targets/rp2040/py/pyproject.toml
new file mode 100644
index 000000000..78668a709
--- /dev/null
+++ b/targets/rp2040/py/pyproject.toml
@@ -0,0 +1,16 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+[build-system]
+requires = ['setuptools', 'wheel']
+build-backend = 'setuptools.build_meta'
diff --git a/targets/rp2040/py/rp2040_utils/__init__.py b/targets/rp2040/py/rp2040_utils/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/targets/rp2040/py/rp2040_utils/__init__.py
diff --git a/targets/rp2040/py/rp2040_utils/device_detector.py b/targets/rp2040/py/rp2040_utils/device_detector.py
new file mode 100644
index 000000000..04172c4ac
--- /dev/null
+++ b/targets/rp2040/py/rp2040_utils/device_detector.py
@@ -0,0 +1,183 @@
+#!/usr/bin/env python3
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Detects attached Raspberry Pi Pico boards."""
+
+from dataclasses import dataclass
+import logging
+from typing import Dict, List
+
+import serial.tools.list_ports # type: ignore
+import usb # type: ignore
+
+import pw_cli.log
+
+# Vendor/device ID to search for in USB devices.
+_RASPBERRY_PI_VENDOR_ID = 0x2E8A
+_PICO_USB_SERIAL_DEVICE_ID = 0x000A
+_PICO_BOOTLOADER_DEVICE_ID = 0x0003
+_PICO_DEVICE_IDS = (
+ _PICO_USB_SERIAL_DEVICE_ID,
+ _PICO_BOOTLOADER_DEVICE_ID,
+)
+
+_LOG = logging.getLogger('pi_pico_detector')
+
+
+@dataclass
+class BoardInfo:
+ """Information about a connected Pi Pico board."""
+
+ serial_port: str
+ bus: int
+ port: int
+
+ # As a board is flashed and reset, the USB address can change. This method
+ # uses the USB bus and port to try and find the desired device. Using the
+ # serial number sounds appealing, but unfortunately the application's serial
+ # number is different from the bootloader's.
+ def address(self) -> int:
+ devices = usb.core.find(
+ find_all=True,
+ idVendor=_RASPBERRY_PI_VENDOR_ID,
+ )
+ for device in devices:
+ if device.idProduct not in _PICO_DEVICE_IDS:
+ raise ValueError(
+ 'Unknown device type on bus %d port %d'
+ % (self.bus, self.port)
+ )
+ if device.port_number == self.port:
+ return device.address
+ raise ValueError(
+ (
+ 'No Pico found, it may have been disconnected or flashed with '
+ 'an incompatible application'
+ )
+ )
+
+
+@dataclass
+class _BoardSerialInfo:
+ """Information that ties a serial number to a serial com port."""
+
+ serial_port: str
+ serial_number: str
+
+
+@dataclass
+class _BoardUsbInfo:
+ """Information that ties a serial number to a USB information"""
+
+ serial_number: str
+ bus: int
+ port: int
+
+
+def _detect_pico_usb_info() -> Dict[str, _BoardUsbInfo]:
+ """Finds Raspberry Pi Pico devices and retrieves USB info for each one."""
+ boards: Dict[str, _BoardUsbInfo] = {}
+ devices = usb.core.find(
+ find_all=True,
+ idVendor=_RASPBERRY_PI_VENDOR_ID,
+ )
+
+ if not devices:
+ return boards
+
+ for device in devices:
+ if device.idProduct == _PICO_USB_SERIAL_DEVICE_ID:
+ boards[device.serial_number] = _BoardUsbInfo(
+ serial_number=device.serial_number,
+ bus=device.bus,
+ port=device.port_number,
+ )
+ elif device.idProduct == _PICO_BOOTLOADER_DEVICE_ID:
+ _LOG.error(
+ 'Found a Pi Pico in bootloader mode on bus %d address %d',
+ device.bus,
+ device.address,
+ )
+ _LOG.error(
+ (
+ 'Please flash and reboot the Pico into an application '
+ 'utilizing USB serial to properly detect it'
+ )
+ )
+
+ else:
+ _LOG.error('Unknown/incompatible Raspberry Pi detected')
+ _LOG.error(
+ (
+ 'Make sure your Pi Pico is running an application '
+ 'utilizing USB serial'
+ )
+ )
+ return boards
+
+
+def _detect_pico_serial_ports() -> Dict[str, _BoardSerialInfo]:
+ """Finds the serial com port associated with each Raspberry Pi Pico."""
+ boards = {}
+ all_devs = serial.tools.list_ports.comports()
+ for dev in all_devs:
+ if (
+ dev.vid == _RASPBERRY_PI_VENDOR_ID
+ and dev.pid == _PICO_USB_SERIAL_DEVICE_ID
+ ):
+ if dev.serial_number is None:
+ raise ValueError('Found pico with no serial number')
+ boards[dev.serial_number] = _BoardSerialInfo(
+ serial_port=dev.device,
+ serial_number=dev.serial_number,
+ )
+ return boards
+
+
+def detect_boards() -> List[BoardInfo]:
+ """Detects attached Raspberry Pi Pico boards in USB serial mode.
+
+ Returns:
+ A list of all found boards as BoardInfo objects.
+ """
+ serial_devices = _detect_pico_serial_ports()
+ pico_usb_info = _detect_pico_usb_info()
+ boards = []
+ for serial_number, usb_info in pico_usb_info.items():
+ if serial_number in serial_devices:
+ serial_info = serial_devices[serial_number]
+ boards.append(
+ BoardInfo(
+ serial_port=serial_info.serial_port,
+ bus=usb_info.bus,
+ port=usb_info.port,
+ )
+ )
+ return boards
+
+
+def main():
+ """Detects and then prints all attached Raspberry Pi Picos."""
+ pw_cli.log.install()
+
+ boards = detect_boards()
+ if not boards:
+ _LOG.info('No attached boards detected')
+ for idx, board in enumerate(boards):
+ _LOG.info('Board %d:', idx)
+ _LOG.info(' %s', board)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/targets/rp2040/py/rp2040_utils/py.typed b/targets/rp2040/py/rp2040_utils/py.typed
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/targets/rp2040/py/rp2040_utils/py.typed
diff --git a/targets/rp2040/py/rp2040_utils/unit_test_client.py b/targets/rp2040/py/rp2040_utils/unit_test_client.py
new file mode 100644
index 000000000..dfbc31208
--- /dev/null
+++ b/targets/rp2040/py/rp2040_utils/unit_test_client.py
@@ -0,0 +1,58 @@
+#!/usr/bin/env python3
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Launch a pw_target_runner client that sends a test request."""
+
+import argparse
+import subprocess
+import sys
+
+try:
+ from rp2040_utils import unit_test_server
+except ImportError:
+ # Load from this directory if rp2040_utils is not available.
+ import unit_test_server # type: ignore
+
+_TARGET_CLIENT_COMMAND = 'pw_target_runner_client'
+
+
+def parse_args():
+ """Parses command-line arguments."""
+
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('binary', help='The target test binary to run')
+ parser.add_argument(
+ '--server-port',
+ type=int,
+ default=unit_test_server.DEFAULT_PORT,
+ help='Port the test server is located on',
+ )
+
+ return parser.parse_args()
+
+
+def launch_client(binary: str, server_port: int) -> int:
+ """Sends a test request to the specified server port."""
+ cmd = (_TARGET_CLIENT_COMMAND, '-binary', binary, '-port', str(server_port))
+ return subprocess.call(cmd)
+
+
+def main() -> int:
+ """Launch a test by sending a request to a pw_target_runner_server."""
+ args = parse_args()
+ return launch_client(args.binary, args.server_port)
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/targets/rp2040/py/rp2040_utils/unit_test_runner.py b/targets/rp2040/py/rp2040_utils/unit_test_runner.py
new file mode 100644
index 000000000..a53a9500c
--- /dev/null
+++ b/targets/rp2040/py/rp2040_utils/unit_test_runner.py
@@ -0,0 +1,195 @@
+#!/usr/bin/env python3
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""This script flashes and runs unit tests on Raspberry Pi Pico boards."""
+
+import argparse
+import logging
+import subprocess
+import sys
+import time
+from pathlib import Path
+
+import serial # type: ignore
+
+import pw_cli.log
+from pw_unit_test import serial_test_runner
+from pw_unit_test.serial_test_runner import (
+ SerialTestingDevice,
+ DeviceNotFound,
+ TestingFailure,
+)
+from rp2040_utils import device_detector
+from rp2040_utils.device_detector import BoardInfo
+
+
+_LOG = logging.getLogger("unit_test_runner")
+
+
+def parse_args():
+ """Parses command-line arguments."""
+
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ 'binary', type=Path, help='The target test binary to run'
+ )
+ parser.add_argument(
+ '--usb-bus',
+ type=int,
+ help='The bus this Pi Pico is on',
+ )
+ parser.add_argument(
+ '--usb-port',
+ type=int,
+ help='The port of this Pi Pico on the specified USB bus',
+ )
+ parser.add_argument(
+ '--serial-port',
+ type=str,
+ help='The name of the serial port to connect to when running tests',
+ )
+ parser.add_argument(
+ '-b',
+ '--baud',
+ type=int,
+ default=115200,
+ help='Baud rate to use for serial communication with target device',
+ )
+ parser.add_argument(
+ '--test-timeout',
+ type=float,
+ default=5.0,
+ help='Maximum communication delay in seconds before a '
+ 'test is considered unresponsive and aborted',
+ )
+ parser.add_argument(
+ '--verbose',
+ '-v',
+ dest='verbose',
+ action='store_true',
+ help='Output additional logs as the script runs',
+ )
+
+ return parser.parse_args()
+
+
+class PiPicoTestingDevice(SerialTestingDevice):
+ """A SerialTestingDevice implementation for the Pi Pico."""
+
+ def __init__(self, board_info: BoardInfo, baud_rate=115200):
+ self._board_info = board_info
+ self._baud_rate = baud_rate
+
+ def load_binary(self, binary: Path) -> None:
+ cmd = (
+ 'picotool',
+ 'load',
+ '-x',
+ str(binary),
+ '--bus',
+ str(self._board_info.bus),
+ '--address',
+ str(self._board_info.address()),
+ '-F',
+ )
+ process = subprocess.run(
+ cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
+ )
+ if process.returncode:
+ err = (
+ 'Command failed: ' + ' '.join(cmd),
+ str(self._board_info),
+ process.stdout.decode('utf-8', errors='replace'),
+ )
+ raise TestingFailure('\n\n'.join(err))
+ # Wait for serial port to enumerate. This will retry forever.
+ while True:
+ try:
+ serial.Serial(
+ baudrate=self.baud_rate(), port=self.serial_port()
+ )
+ except serial.serialutil.SerialException:
+ time.sleep(0.001)
+ else:
+ break
+
+ def serial_port(self) -> str:
+ return self._board_info.serial_port
+
+ def baud_rate(self) -> int:
+ return self._baud_rate
+
+
+def _run_test(
+ device: PiPicoTestingDevice, binary: Path, test_timeout: float
+) -> bool:
+ return serial_test_runner.run_device_test(device, binary, test_timeout)
+
+
+def run_device_test(
+ binary: Path,
+ test_timeout: float,
+ serial_port: str,
+ baud_rate: int,
+ usb_bus: int,
+ usb_port: int,
+) -> bool:
+ """Flashes, runs, and checks an on-device test binary.
+
+ Returns true on test pass.
+ """
+ board = BoardInfo(serial_port, usb_bus, usb_port)
+ return _run_test(
+ PiPicoTestingDevice(board, baud_rate), binary, test_timeout
+ )
+
+
+def detect_and_run_test(binary: Path, test_timeout: float, baud_rate: int):
+ _LOG.debug('Attempting to automatically detect dev board')
+ boards = device_detector.detect_boards()
+ if not boards:
+ error = 'Could not find an attached device'
+ _LOG.error(error)
+ raise DeviceNotFound(error)
+ return _run_test(
+ PiPicoTestingDevice(boards[0], baud_rate), binary, test_timeout
+ )
+
+
+def main():
+ """Set up runner, and then flash/run device test."""
+ args = parse_args()
+ log_level = logging.DEBUG if args.verbose else logging.INFO
+ pw_cli.log.install(level=log_level)
+
+ test_passed = False
+ if not args.serial_port:
+ test_passed = detect_and_run_test(
+ args.binary, args.test_timeout, args.baud
+ )
+ else:
+ test_passed = run_device_test(
+ args.binary,
+ args.test_timeout,
+ args.serial_port,
+ args.baud,
+ args.usb_bus,
+ args.usb_port,
+ )
+
+ sys.exit(0 if test_passed else 1)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/targets/rp2040/py/rp2040_utils/unit_test_server.py b/targets/rp2040/py/rp2040_utils/unit_test_server.py
new file mode 100644
index 000000000..62c5b9461
--- /dev/null
+++ b/targets/rp2040/py/rp2040_utils/unit_test_server.py
@@ -0,0 +1,139 @@
+#!/usr/bin/env python3
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Launch a pw_target_runner server to use for multi-device testing."""
+
+import argparse
+import logging
+import sys
+import tempfile
+from typing import IO, List, Optional
+
+import pw_cli.process
+import pw_cli.log
+
+try:
+ from rp2040_utils import device_detector
+except ImportError:
+ # Load from this directory if rp2040_utils is not available.
+ import device_detector # type: ignore
+
+_LOG = logging.getLogger('unit_test_server')
+
+DEFAULT_PORT = 34172
+
+_TEST_RUNNER_COMMAND = 'rp2040_unit_test_runner'
+_TEST_SERVER_COMMAND = 'pw_target_runner_server'
+
+
+def parse_args():
+ """Parses command-line arguments."""
+
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ '--server-port',
+ type=int,
+ default=DEFAULT_PORT,
+ help='Port to launch the pw_target_runner_server on',
+ )
+ parser.add_argument(
+ '--server-config',
+ type=argparse.FileType('r'),
+ help='Path to server config file',
+ )
+ parser.add_argument(
+ '--verbose',
+ '-v',
+ dest='verbose',
+ action="store_true",
+ help='Output additional logs as the script runs',
+ )
+
+ return parser.parse_args()
+
+
+def generate_runner(command: str, arguments: List[str]) -> str:
+ """Generates a text-proto style pw_target_runner_server configuration."""
+ # TODO(amontanez): Use a real proto library to generate this when we have
+ # one set up.
+ for i, arg in enumerate(arguments):
+ arguments[i] = f' args: "{arg}"'
+ runner = ['runner {', f' command:"{command}"']
+ runner.extend(arguments)
+ runner.append('}\n')
+ return '\n'.join(runner)
+
+
+def generate_server_config() -> IO[bytes]:
+ """Returns a temporary generated file for use as the server config."""
+ boards = device_detector.detect_boards()
+ if not boards:
+ _LOG.critical('No attached boards detected')
+ sys.exit(1)
+ config_file = tempfile.NamedTemporaryFile()
+ _LOG.debug('Generating test server config at %s', config_file.name)
+ _LOG.debug('Found %d attached devices', len(boards))
+
+ # TODO: b/290245354 - Multi-device flashing doesn't work due to limitations
+ # of picotool. Limit to one device even if multiple are connected.
+ if boards:
+ boards = boards[:1]
+
+ for board in boards:
+ test_runner_args = [
+ '--usb-bus',
+ str(board.bus),
+ '--usb-port',
+ str(board.port),
+ '--serial-port',
+ board.serial_port,
+ ]
+ config_file.write(
+ generate_runner(_TEST_RUNNER_COMMAND, test_runner_args).encode(
+ 'utf-8'
+ )
+ )
+ config_file.flush()
+ return config_file
+
+
+def launch_server(
+ server_config: Optional[IO[bytes]], server_port: Optional[int]
+) -> int:
+ """Launch a device test server with the provided arguments."""
+ if server_config is None:
+ # Auto-detect attached boards if no config is provided.
+ server_config = generate_server_config()
+
+ cmd = [_TEST_SERVER_COMMAND, '-config', server_config.name]
+
+ if server_port is not None:
+ cmd.extend(['-port', str(server_port)])
+
+ return pw_cli.process.run(*cmd, log_output=True).returncode
+
+
+def main():
+ """Launch a device test server with the provided arguments."""
+ args = parse_args()
+
+ log_level = logging.DEBUG if args.verbose else logging.INFO
+ pw_cli.log.install(level=log_level)
+
+ exit_code = launch_server(args.server_config, args.server_port)
+ sys.exit(exit_code)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/targets/rp2040/py/setup.cfg b/targets/rp2040/py/setup.cfg
new file mode 100644
index 000000000..d49b4a20b
--- /dev/null
+++ b/targets/rp2040/py/setup.cfg
@@ -0,0 +1,34 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+[metadata]
+name = rp2040_utils
+version = 0.0.1
+author = Pigweed Authors
+author_email = pigweed-developers@googlegroups.com
+description = Target-specific python scripts for the rp2040 target
+
+[options]
+packages = find:
+zip_safe = False
+install_requires =
+ pyserial~=3.5.0
+ pyusb
+
+[options.entry_points]
+console_scripts =
+ rp2040_unit_test_runner = rp2040_utils.unit_test_runner:main
+ rp2040_detector = rp2040_utils.device_detector:main
+
+[options.package_data]
+rp2040_utils = py.typed
diff --git a/targets/rp2040/target_docs.rst b/targets/rp2040/target_docs.rst
index 201c7366c..04c77c46c 100644
--- a/targets/rp2040/target_docs.rst
+++ b/targets/rp2040/target_docs.rst
@@ -4,9 +4,9 @@
Raspberry Pi Pico
-----------------
.. warning::
- This target is in an early state and is under active development. Usability
- is not very polished, and many features/configuration options that work in
- upstream Pi Pico CMake build have not yet been ported to the GN build.
+ This target is in an early state and is under active development. Usability
+ is not very polished, and many features/configuration options that work in
+ upstream Pi Pico CMake build have not yet been ported to the GN build.
Setup
=====
@@ -14,27 +14,27 @@ To use this target, Pigweed must be set up to build against the Raspberry Pi
Pico SDK. This can be downloaded via ``pw package``, and then the build must be
manually configured to point to the location of the downloaded SDK.
-.. code:: sh
+.. code-block:: sh
- pw package install pico_sdk
+ pw package install pico_sdk
- gn args out
- # Add this line.
- PICO_SRC_DIR = getenv("PW_PACKAGE_ROOT") + "/pico_sdk"
+ gn args out
+ # Add this line.
+ PICO_SRC_DIR = getenv("PW_PACKAGE_ROOT") + "/pico_sdk"
Linux
-----
On linux, you may need to update your udev rules at
``/etc/udev/rules.d/49-pico.rules`` to include the following:
-.. code:: none
+.. code-block:: none
- SUBSYSTEMS=="usb", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="0004", MODE:="0666"
- KERNEL=="ttyACM*", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="0004", MODE:="0666"
- SUBSYSTEMS=="usb", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="0003", MODE:="0666"
- KERNEL=="ttyACM*", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="0003", MODE:="0666"
- SUBSYSTEMS=="usb", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="000a", MODE:="0666"
- KERNEL=="ttyACM*", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="000a", MODE:="0666"
+ SUBSYSTEMS=="usb", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="0004", MODE:="0666"
+ KERNEL=="ttyACM*", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="0004", MODE:="0666"
+ SUBSYSTEMS=="usb", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="0003", MODE:="0666"
+ KERNEL=="ttyACM*", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="0003", MODE:="0666"
+ SUBSYSTEMS=="usb", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="000a", MODE:="0666"
+ KERNEL=="ttyACM*", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="000a", MODE:="0666"
Usage
=====
@@ -44,9 +44,9 @@ baud rate of 115200.
Once the pico SDK is configured, the Pi Pico will build as part of the default
GN build:
-.. code:: sh
+.. code-block:: sh
- ninja -C out
+ ninja -C out
Pigweed's build will produce ELF and UF2 files for each unit test built for the
Pi Pico.
@@ -66,43 +66,43 @@ Unlike some other targets, the RP2040 does not automatically run tests on boot.
To run a test, flash it to the RP2040 and connect to the serial port and then
press the spacebar to start the test:
-.. code:: none
-
- $ python -m serial.tools.miniterm --raw /dev/ttyACM0 115200
- --- Miniterm on /dev/cu.usbmodem142401 115200,8,N,1 ---
- --- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---INF [==========] Running all tests.
- INF [ RUN ] Status.Default
- INF [ OK ] Status.Default
- INF [ RUN ] Status.ConstructWithStatusCode
- INF [ OK ] Status.ConstructWithStatusCode
- INF [ RUN ] Status.AssignFromStatusCode
- INF [ OK ] Status.AssignFromStatusCode
- INF [ RUN ] Status.Ok_OkIsTrue
- INF [ OK ] Status.Ok_OkIsTrue
- INF [ RUN ] Status.NotOk_OkIsFalse
- INF [ OK ] Status.NotOk_OkIsFalse
- INF [ RUN ] Status.Code
- INF [ OK ] Status.Code
- INF [ RUN ] Status.EqualCodes
- INF [ OK ] Status.EqualCodes
- INF [ RUN ] Status.IsError
- INF [ OK ] Status.IsError
- INF [ RUN ] Status.IsNotError
- INF [ OK ] Status.IsNotError
- INF [ RUN ] Status.Strings
- INF [ OK ] Status.Strings
- INF [ RUN ] Status.UnknownString
- INF [ OK ] Status.UnknownString
- INF [ RUN ] Status.Update
- INF [ OK ] Status.Update
- INF [ RUN ] StatusCLinkage.CallCFunctionWithStatus
- INF [ OK ] StatusCLinkage.CallCFunctionWithStatus
- INF [ RUN ] StatusCLinkage.TestStatusFromC
- INF [ OK ] StatusCLinkage.TestStatusFromC
- INF [ RUN ] StatusCLinkage.TestStatusStringsFromC
- INF [ OK ] StatusCLinkage.TestStatusStringsFromC
- INF [==========] Done running all tests.
- INF [ PASSED ] 15 test(s).
+.. code-block:: none
+
+ $ python -m serial.tools.miniterm --raw /dev/ttyACM0 115200
+ --- Miniterm on /dev/cu.usbmodem142401 115200,8,N,1 ---
+ --- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---INF [==========] Running all tests.
+ INF [ RUN ] Status.Default
+ INF [ OK ] Status.Default
+ INF [ RUN ] Status.ConstructWithStatusCode
+ INF [ OK ] Status.ConstructWithStatusCode
+ INF [ RUN ] Status.AssignFromStatusCode
+ INF [ OK ] Status.AssignFromStatusCode
+ INF [ RUN ] Status.Ok_OkIsTrue
+ INF [ OK ] Status.Ok_OkIsTrue
+ INF [ RUN ] Status.NotOk_OkIsFalse
+ INF [ OK ] Status.NotOk_OkIsFalse
+ INF [ RUN ] Status.Code
+ INF [ OK ] Status.Code
+ INF [ RUN ] Status.EqualCodes
+ INF [ OK ] Status.EqualCodes
+ INF [ RUN ] Status.IsError
+ INF [ OK ] Status.IsError
+ INF [ RUN ] Status.IsNotError
+ INF [ OK ] Status.IsNotError
+ INF [ RUN ] Status.Strings
+ INF [ OK ] Status.Strings
+ INF [ RUN ] Status.UnknownString
+ INF [ OK ] Status.UnknownString
+ INF [ RUN ] Status.Update
+ INF [ OK ] Status.Update
+ INF [ RUN ] StatusCLinkage.CallCFunctionWithStatus
+ INF [ OK ] StatusCLinkage.CallCFunctionWithStatus
+ INF [ RUN ] StatusCLinkage.TestStatusFromC
+ INF [ OK ] StatusCLinkage.TestStatusFromC
+ INF [ RUN ] StatusCLinkage.TestStatusStringsFromC
+ INF [ OK ] StatusCLinkage.TestStatusStringsFromC
+ INF [==========] Done running all tests.
+ INF [ PASSED ] 15 test(s).
This is done because the serial port enumerated by the Pi Pico goes away on
reboot, so it's not safe to run tests until the port has fully enumerated and
@@ -111,3 +111,50 @@ receives the space character (0x20) as a signal to start running the tests.
The RP2040 does not yet provide an automated test runner with build system
integration.
+
+Automated test runner
+---------------------
+This target supports automatically running on-device tests as part of the GN
+build thanks to a custom ``pw_unit_test_AUTOMATIC_RUNNER`` script.
+
+Step 1: Start test server
+^^^^^^^^^^^^^^^^^^^^^^^^^
+To allow Ninja to properly serialize tests to run on device, Ninja will send
+test requests to a server running in the background. The first step is to launch
+this server. By default, the script will attempt to automatically detect an
+attached Pi Pico running an application with USB serial enabled, then using
+it for testing. To override this behavior, provide a custom server configuration
+file with ``--server-config``.
+
+.. code-block:: sh
+
+ $ python -m rp2040_utils.unit_test_server
+
+.. tip::
+
+ If the server can't find any attached devices, ensure your Pi Pico is
+ already running an application that utilizes USB serial.
+
+.. Warning::
+
+ If you connect or disconnect any boards, you'll need to restart the test
+ server for hardware changes to take effect.
+
+Step 2: Configure GN
+^^^^^^^^^^^^^^^^^^^^
+By default, this hardware target has incremental testing disabled. Enabling the
+``pw_targets_ENABLE_RP2040_TEST_RUNNER`` build arg tells GN to send requests to
+a running ``rp2040_utils.unit_test_server``.
+
+.. code-block:: sh
+
+ $ gn args out
+ # Modify and save the args file to use pw_target_runner.
+ pw_targets_ENABLE_RP2040_TEST_RUNNER = true
+
+Step 3: Build changes
+^^^^^^^^^^^^^^^^^^^^^
+Now, whenever you run ``ninja -C out pi_pico``, all tests affected by changes
+since the last build will be rebuilt and then run on the attached device.
+Alternatively, you may use ``pw watch`` to set up Pigweed to trigger
+builds/tests whenever changes to source files are detected.