diff options
Diffstat (limited to 'targets/rp2040')
-rw-r--r-- | targets/rp2040/BUILD.bazel | 2 | ||||
-rw-r--r-- | targets/rp2040/BUILD.gn | 22 | ||||
-rw-r--r-- | targets/rp2040/pico_executable.gni | 8 | ||||
-rw-r--r-- | targets/rp2040/py/BUILD.gn | 37 | ||||
-rw-r--r-- | targets/rp2040/py/pyproject.toml | 16 | ||||
-rw-r--r-- | targets/rp2040/py/rp2040_utils/__init__.py | 0 | ||||
-rw-r--r-- | targets/rp2040/py/rp2040_utils/device_detector.py | 183 | ||||
-rw-r--r-- | targets/rp2040/py/rp2040_utils/py.typed | 0 | ||||
-rw-r--r-- | targets/rp2040/py/rp2040_utils/unit_test_client.py | 58 | ||||
-rw-r--r-- | targets/rp2040/py/rp2040_utils/unit_test_runner.py | 195 | ||||
-rw-r--r-- | targets/rp2040/py/rp2040_utils/unit_test_server.py | 139 | ||||
-rw-r--r-- | targets/rp2040/py/setup.cfg | 34 | ||||
-rw-r--r-- | targets/rp2040/target_docs.rst | 155 |
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. |