diff --git a/hwci/boards/__init__.py b/hwci/boards/__init__.py index 11375ca..0f348eb 100644 --- a/hwci/boards/__init__.py +++ b/hwci/boards/__init__.py @@ -3,5 +3,4 @@ # Copyright Tock Contributors 2024. from .tockloader_board import TockloaderBoard -from .nrf52dk import Nrf52dk from .mock_board import MockBoard diff --git a/hwci/boards/imix.py b/hwci/boards/imix.py new file mode 100644 index 0000000..b870921 --- /dev/null +++ b/hwci/boards/imix.py @@ -0,0 +1,118 @@ +# Licensed under the Apache License, Version 2.0 or the MIT License. +# SPDX-License-Identifier: Apache-2.0 OR MIT +# Copyright Tock Contributors 2024. + +import time +import os +import subprocess +import logging +from contextlib import contextmanager +import serial.tools.list_ports +from boards.tockloader_board import TockloaderBoard +from utils.serial_port import SerialPort +from gpio.gpio import GPIO +import yaml +import os +import traceback + +class Imix(TockloaderBoard): + def __init__(self): + super().__init__() + self.arch = "cortex-m4" + self.kernel_path = os.path.join( + self.base_dir, "repos/tock") + self.kernel_board_path = os.path.join( + self.kernel_path, "boards/imix") + self.uart_port = self.get_uart_port() + self.uart_baudrate = self.get_uart_baudrate() + self.board = "imix" + self.program_method = "serial_bootloader" + self.serial = self.get_serial_port() + self.gpio = self.get_gpio_interface() + self.open_serial_during_flash = False + self.app_sha256_credential = True + + def get_uart_port(self): + logging.info("Getting list of serial ports") + ports = list(serial.tools.list_ports.comports()) + for port in ports: + if "imix IoT Module" in port.description: + logging.info(f"Found imix IoT programming port: {port.device}") + return port.device + if ports: + logging.info(f"Automatically selected port: {ports[0].device}") + return ports[0].device + else: + logging.error("No serial ports found") + raise Exception("No serial ports found") + + def get_uart_baudrate(self): + return 115200 # Default baudrate for the board + + def get_serial_port(self): + logging.info( + f"Using serial port: {self.uart_port} at baudrate {self.uart_baudrate}" + ) + return SerialPort(self.uart_port, self.uart_baudrate, open_rts=False, open_dtr=True) + + def get_gpio_interface(self): + return None + + def cleanup(self): + if self.gpio: + for interface in self.gpio.gpio_interfaces.values(): + interface.cleanup() + if self.serial: + self.serial.close() + + def flash_kernel(self): + logging.info("Flashing the Tock OS kernel") + if not os.path.exists(self.kernel_path): + logging.error(f"Tock directory {self.kernel_path} not found") + raise FileNotFoundError(f"Tock directory {self.kernel_path} not found") + + # Run make program from the board directory (this uses the Tock bootloader) + subprocess.run( + ["make", "program"], cwd=self.kernel_board_path, check=True + ) + + def erase_board(self): + logging.info("Erasing the board") + # We erase all apps, but don't erase the kernel. Is there a simple way + # that we can prevent the installed kernel from starting (by + # overwriting its reset vector?) + subprocess.run(["tockloader", "erase-apps"], check=True) + + def reset(self): + if self.serial.is_open(): + logging.info("Performing a target reset by toggling RTS") + self.serial.set_rts(True) + time.sleep(0.1) + self.serial.set_rts(False) + else: + logging.info("Performing a target reset by reading address 0") + subprocess.run(["tockloader", "read", "0x0", "1"], check=True) + + # The flash_app method is inherited from TockloaderBoard + + @contextmanager + def change_directory(self, new_dir): + previous_dir = os.getcwd() + os.chdir(new_dir) + logging.info(f"Changed directory to: {os.getcwd()}") + try: + yield + finally: + os.chdir(previous_dir) + logging.info(f"Reverted to directory: {os.getcwd()}") + + +def load_target_spec(): + # Assume the target spec file is in a fixed location + target_spec_path = os.path.join(os.getcwd(), "target_spec_imix.yaml") + with open(target_spec_path, "r") as f: + target_spec = yaml.safe_load(f) + return target_spec + + +board = Imix() diff --git a/hwci/boards/litex_arty.py b/hwci/boards/litex_arty.py new file mode 100644 index 0000000..fc6108c --- /dev/null +++ b/hwci/boards/litex_arty.py @@ -0,0 +1,143 @@ +# Licensed under the Apache License, Version 2.0 or the MIT License. +# SPDX-License-Identifier: Apache-2.0 OR MIT +# Copyright Tock Contributors 2024. + +import time +import os +import subprocess +import logging +from contextlib import contextmanager +import serial.tools.list_ports +from boards.tockloader_board import TockloaderBoard +from utils.serial_port import SerialPort +from gpio.gpio import GPIO +import yaml +import os +import traceback + +class LiteXArty(TockloaderBoard): + def __init__(self): + super().__init__() + self.arch = "rv32imc" + self.tock_targets = None + self.kernel_path = os.path.join( + self.base_dir, "repos/tock") + self.kernel_board_path = os.path.join( + self.kernel_path, "boards/litex/arty") + self.uart_port = self.get_uart_port() + self.uart_baudrate = self.get_uart_baudrate() + self.board = "litex_arty" + self.program_method = "none" + self.program_args = ["--flash-file=/srv/tftp/boot.bin"] + self.serial = self.get_serial_port() + self.gpio = self.get_gpio_interface() + self.open_serial_during_flash = True + self.app_sha256_credential = False + + def get_uart_port(self): + logging.info("Getting list of serial ports") + ports = list(serial.tools.list_ports.comports()) + for port in ports: + if "Digilent USB Device" in port.description: + logging.info(f"Found LiteX Arty UART: {port.device}") + return port.device + if ports: + logging.info(f"Automatically selected port: {ports[0].device}") + return ports[0].device + else: + logging.error("No serial ports found") + raise Exception("No serial ports found") + + def get_uart_baudrate(self): + return 1000000 # Default baudrate for the board + + def get_serial_port(self): + logging.info( + f"Using serial port: {self.uart_port} at baudrate {self.uart_baudrate}" + ) + return SerialPort(self.uart_port, self.uart_baudrate, open_rts=False, open_dtr=True) + + def get_gpio_interface(self): + return None + + def cleanup(self): + if self.gpio: + for interface in self.gpio.gpio_interfaces.values(): + interface.cleanup() + if self.serial: + self.serial.close() + + def flash_kernel(self): + logging.info("Flashing the Tock OS kernel") + if not os.path.exists(self.kernel_path): + logging.error(f"Tock directory {self.kernel_path} not found") + raise FileNotFoundError(f"Tock directory {self.kernel_path} not found") + + # Run make program from the board directory + subprocess.run( + ["make"], cwd=self.kernel_board_path, check=True + ) + # Then, flash this kernel into the board's flash file: + subprocess.run( + [ + "tockloader", + "flash", + "--board=litex_arty", + "--flash-file=/srv/tftp/boot.bin", + "--address=0x40000000", + os.path.join(self.kernel_path, "target/riscv32imc-unknown-none-elf/release/litex_arty.bin"), + ], + cwd=self.kernel_board_path, + check=True, + ) + # Finally, reset the board: + + + def erase_board(self): + logging.info("Erasing the board") + subprocess.run( + [ + "truncate", + "-s0", + "/srv/tftp/boot.bin", + ], + check=True, + ) + self.reset() + + def reset(self): + if self.serial.is_open(): + self.serial.close() + time.sleep(0.1) + self.serial.open() + else: + self.serial.open() + self.serial.close() + + def wait_boot(self): + logging.info("Waiting 10sec for target to boot") + time.sleep(15) + + # The flash_app method is inherited from TockloaderBoard + + @contextmanager + def change_directory(self, new_dir): + previous_dir = os.getcwd() + os.chdir(new_dir) + logging.info(f"Changed directory to: {os.getcwd()}") + try: + yield + finally: + os.chdir(previous_dir) + logging.info(f"Reverted to directory: {os.getcwd()}") + + +def load_target_spec(): + # Assume the target spec file is in a fixed location + target_spec_path = os.path.join(os.getcwd(), "target_spec_imix.yaml") + with open(target_spec_path, "r") as f: + target_spec = yaml.safe_load(f) + return target_spec + + +board = LiteXArty() diff --git a/hwci/boards/nrf52dk.py b/hwci/boards/nrf52dk.py index 9384b85..ad088ba 100644 --- a/hwci/boards/nrf52dk.py +++ b/hwci/boards/nrf52dk.py @@ -24,13 +24,14 @@ def __init__(self): self.kernel_path, "boards/nordic/nrf52840dk") self.uart_port = self.get_uart_port() self.uart_baudrate = self.get_uart_baudrate() + self.program_method = "openocd" self.openocd_board = "nrf52dk" self.board = "nrf52dk" self.serial = self.get_serial_port() self.gpio = self.get_gpio_interface() def get_uart_port(self): - logging.info("Getting list of serial ports") + logging.info("Getting list of serial ports!") ports = list(serial.tools.list_ports.comports()) for port in ports: if "J-Link" in port.description: diff --git a/hwci/boards/tockloader_board.py b/hwci/boards/tockloader_board.py index c81c145..0dd168c 100644 --- a/hwci/boards/tockloader_board.py +++ b/hwci/boards/tockloader_board.py @@ -15,7 +15,11 @@ def __init__(self): super().__init__() self.board = None # Should be set in subclass self.arch = None # Should be set in subclass + self.tock_targets = None # Optional self.base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + self.program_method = "serial_bootloader" + self.program_args = [] + self.app_sha256_credential = False def flash_app(self, app): if type(app) == str: @@ -38,24 +42,43 @@ def flash_app(self, app): logging.error(f"App directory {app_dir} not found") raise FileNotFoundError(f"App directory {app_dir} not found") + + make_args = [ + "make", + "-j", + ] + ( + [f"TOCK_TARGETS={self.tock_targets}"] + if self.tock_targets is not None else [] + ) + # if self.app_sha256_credential: + # make_args.append("ELF2TAB_ARGS=\"--sha256\"") + # Build the app using absolute paths logging.info(f"Building app: {app_name}") if app_name != "lua-hello": - subprocess.run( - ["make", f"TOCK_TARGETS={self.arch}"], cwd=app_dir, check=True - ) + subprocess.run(make_args, cwd=app_dir, check=True) else: # if the app is lua-hello, we need to build the libtock-c submodule first so we need to change directory # into the libtock-c directory so it knows we are in a git repostiory self.change_directory(libtock_c_dir) - subprocess.run( - ["make", f"TOCK_TARGETS={self.arch}"], cwd=app_dir, check=True - ) + subprocess.run(make_args, cwd=app_dir, check=True) tab_path = os.path.join(app_dir, tab_file) if not os.path.exists(tab_path): logging.error(f"Tab file {tab_path} not found") raise FileNotFoundError(f"Tab file {tab_path} not found") + + if self.program_method == "serial_bootloader": + program_method_arg = "--serial" + elif self.program_method == "jlink": + program_method_arg = "--jlink" + elif self.program_method == "openocd": + program_method_arg = "--openocd" + elif self.program_method == "none": + program_method_arg = None + else: + raise NotImplemented(f"Unknown program method: {self.program_method}") + logging.info(f"Installing app: {app_name}") subprocess.run( [ @@ -63,11 +86,13 @@ def flash_app(self, app): "install", "--board", self.board, - "--openocd", - tab_path, - ], + ] + + ([program_method_arg] if program_method_arg is not None else []) + + self.program_args + + [tab_path], check=True, ) + self.reset() def get_uart_port(self): raise NotImplementedError @@ -78,6 +103,9 @@ def get_uart_baudrate(self): def erase_board(self): raise NotImplementedError + def wait_boot(self): + pass + def reset(self): raise NotImplementedError diff --git a/hwci/setup.sh b/hwci/setup.sh index 49819e6..ce9e506 100755 --- a/hwci/setup.sh +++ b/hwci/setup.sh @@ -62,7 +62,8 @@ sudo DEBIAN_FRONTEND=noninteractive apt install -y \ git cargo openocd python3 python3-pip python3-serial \ python3-pexpect gcc-arm-none-eabi libnewlib-arm-none-eabi \ pkg-config libudev-dev cmake libusb-1.0-0-dev udev make \ - gdb-multiarch gcc-arm-none-eabi build-essential jq || true + gdb-multiarch gcc-arm-none-eabi build-essential jq \ + gcc-riscv64-unknown-elf || true # If we don't have any of the tock or libtock-c repos checked out, clone them # here. We never want to do this for CI, as that'll want to check out specific diff --git a/hwci/target_spec_imix.yaml b/hwci/target_spec_imix.yaml new file mode 100644 index 0000000..c24019b --- /dev/null +++ b/hwci/target_spec_imix.yaml @@ -0,0 +1,3 @@ +model: imix +serial_number: 0xfoobar +pin_mappings: diff --git a/hwci/tests/console_recv_short_and_long.py b/hwci/tests/console_recv_short_and_long.py new file mode 100644 index 0000000..e8c1eee --- /dev/null +++ b/hwci/tests/console_recv_short_and_long.py @@ -0,0 +1,41 @@ +# Licensed under the Apache License, Version 2.0 OR the MIT License. +# SPDX-License-Identifier: Apache-2.0 OR MIT + +import logging +from utils.test_helpers import OneshotTest +import re + + +class StackSizeTest01(OneshotTest): + def __init__(self): + super().__init__(apps=[ + "tests/console/console_recv_short", + "tests/console/console_recv_long", + ]) + + def oneshot_test(self, board): + serial = board.serial + return + + # Wait for "Stack Test App" + output = serial.expect("Stack Test App", timeout=10) + if not output: + raise Exception("Did not receive 'Stack Test App' message") + + # Wait for "Current stack pointer: 0x..." + output = serial.expect(r"Current stack pointer: 0x[0-9a-fA-F]+", timeout=5) + if not output: + raise Exception("Did not receive 'Current stack pointer' message") + + # Optionally, you can extract and log the stack pointer value + match = re.search(r"Current stack pointer: (0x[0-9a-fA-F]+)", output.decode()) + if match: + stack_pointer = match.group(1) + logging.info(f"Stack pointer is at {stack_pointer}") + else: + raise Exception("Failed to parse stack pointer value") + + logging.info("Stack size test 01 completed successfully") + + +test = StackSizeTest01() diff --git a/hwci/tests/console_timeout.py b/hwci/tests/console_timeout.py index 851eb59..ed5aa9a 100644 --- a/hwci/tests/console_timeout.py +++ b/hwci/tests/console_timeout.py @@ -16,18 +16,22 @@ def oneshot_test(self, board): # Wait for the application to initialize logging.info("Waiting for the application to initialize...") - time.sleep(2) # Allow time for the app to start + time.sleep(1) # Allow time for the app to start # Simulate user input by writing to the serial port - test_input = b"Hello, Tock!" - serial.write(test_input) + serial.flush_buffer() + test_input = b"Hello, Tock!\r\n" + for b in test_input: + time.sleep(0.01) # imix doesn't like this being sent too quickly! + serial.write(bytes([b])) + #serial.write(test_input) logging.info(f"Sent test input: {test_input.decode('utf-8')}") - time.sleep(7) # Wait for the application to process # Wait for the expected output from the application logging.info("Waiting for the application to output the result...") pattern = r"Userspace call to read console returned: (.*)" output = serial.expect(pattern, timeout=10) + print(output) if output: received_line = output.decode("utf-8", errors="replace").strip() diff --git a/hwci/utils/serial_port.py b/hwci/utils/serial_port.py index d78b23f..bc1a125 100644 --- a/hwci/utils/serial_port.py +++ b/hwci/utils/serial_port.py @@ -12,23 +12,56 @@ class SerialPort: - def __init__(self, port, baudrate=115200): + def __init__(self, port, baudrate=115200, open_rts=None, open_dtr=None): self.port = port self.baudrate = baudrate - try: - self.ser = serial.Serial(port, baudrate=baudrate, timeout=1) - self.child = fdpexpect.fdspawn(self.ser.fileno()) - logging.info(f"Opened serial port {port} at baudrate {baudrate}") - except serial.SerialException as e: - logging.error(f"Failed to open serial port {port}: {e}") - raise + self.open_rts = open_rts + self.open_dtr = open_dtr + self.ser = None + self.child = None + + def open(self): + if self.ser is None: + try: + self.ser = serial.Serial(self.port, baudrate=self.baudrate, timeout=1, exclusive=True) + self.child = fdpexpect.fdspawn(self.ser.fileno()) + if self.open_rts is not None: + self.ser.rts = self.open_rts + if self.open_dtr is not None: + self.ser.dtr = self.open_dtr + logging.info(f"Opened serial port {self.port} at baudrate {self.baudrate}") + except serial.SerialException as e: + logging.error(f"Failed to open serial port {port}: {e}") + raise + + def is_open(self): + return self.ser is not None + + def close(self): + if self.ser is not None: + self.ser.close() + logging.info(f"Closed serial port {self.port}") + self.ser = None + self.child = None + + def set_rts(self, rts): + assert self.ser is not None, "Serial port is not open!" + self.ser.rts = rts + + def set_dtr(self, dtr): + assert self.ser is not None, "Serial port is not open!" + self.ser.dtr = dtr def flush_buffer(self): + assert self.ser is not None, "Serial port is not open!" + self.ser.reset_input_buffer() self.ser.reset_output_buffer() logging.info("Flushed serial buffers") def expect(self, pattern, timeout=10, timeout_error=True): + assert self.ser is not None, "Serial port is not open!" + try: index = self.child.expect(pattern, timeout=timeout) return self.child.after @@ -45,26 +78,31 @@ def expect(self, pattern, timeout=10, timeout_error=True): return None def write(self, data): + assert self.ser is not None, "Serial port is not open!" + logging.debug(f"Writing data: {data}") for byte in data: self.ser.write(bytes([byte])) time.sleep(0.1) - def close(self): - self.ser.close() - logging.info(f"Closed serial port {self.port}") - - class MockSerialPort: def __init__(self): self.buffer = queue.Queue() self.accumulated_data = b"" + self.open = False + + def open(self): + self.open = True def write(self, data): + assert self.open, "Serial port is not open!" + logging.debug(f"Writing data: {data}") self.buffer.put(data) def expect(self, pattern, timeout=10, timeout_error=True): + assert self.open, "Serial port is not open!" + end_time = time.time() + timeout compiled_pattern = re.compile(pattern.encode()) while time.time() < end_time: @@ -81,12 +119,15 @@ def expect(self, pattern, timeout=10, timeout_error=True): return None def flush_buffer(self): + assert self.open, "Serial port is not open!" + self.accumulated_data = b"" while not self.buffer.empty(): self.buffer.get() def close(self): - pass + assert self.open, "Serial port is not open!" + self.open = False def reset_input_buffer(self): self.flush_buffer() diff --git a/hwci/utils/test_helpers/oneshot.py b/hwci/utils/test_helpers/oneshot.py index ff7ba47..9dc8f76 100644 --- a/hwci/utils/test_helpers/oneshot.py +++ b/hwci/utils/test_helpers/oneshot.py @@ -9,12 +9,26 @@ def __init__(self, apps=[]): def test(self, board): logging.info("Starting OneshotTest") board.erase_board() - board.serial.flush_buffer() + + # For some boards, we need to open the serial console during flash to + # capture the initial messages: + if board.open_serial_during_flash: + board.serial.open() + board.flash_kernel() for app in self.apps: board.flash_app(app) + + # For other boards (such as Imix), we can only open the serial after + # the board has been flashed: + if not board.open_serial_during_flash: + board.serial.open() + + board.wait_boot() self.oneshot_test(board) + logging.info("Finished OneshotTest") + board.serial.close() def oneshot_test(self, board): pass # To be implemented by subclasses