blob: ef09de8f92a501b3aaa5f7e6b90709ea23eed3e4 [file] [edit]
# Copyright 2025 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.
"""Utility to invoke opentitantool and pipe the output through the detokenizer."""
import argparse
import logging
import subprocess
import sys
import tempfile
import threading
import time
import pathlib
from pathlib import Path
from pw_tokenizer import detokenize
_LOG = logging.getLogger(__name__)
_LOG.setLevel(logging.INFO)
try:
import opentitan.opentitantool # type: ignore
import opentitan.verilator # type: ignore
import opentitan.test_rom # type: ignore
import opentitan.test_otp # type: ignore
import opentitan.rom_ext_cw310 # type: ignore
import opentitan.rom_ext_cw340 # type: ignore
import opentitan.bitstream_hyper310 # type: ignore
import opentitan.bitstream_hyper340 # type: ignore
import opentitan.fake_keys.app_prod_ecdsa # type: ignore
from python.runfiles import runfiles # type: ignore
r = runfiles.Create()
_OTTO = r.Rlocation(*opentitan.opentitantool.RLOCATION)
_VERILATOR = r.Rlocation(*opentitan.verilator.RLOCATION)
_TEST_ROM = r.Rlocation(*opentitan.test_rom.RLOCATION)
_TEST_OTP = r.Rlocation(*opentitan.test_otp.RLOCATION)
_PRODKEY = r.Rlocation(*opentitan.fake_keys.app_prod_ecdsa.RLOCATION)
_ROM_EXT = {
"hyper310": r.Rlocation(*opentitan.rom_ext_cw310.RLOCATION),
"hyper340": r.Rlocation(*opentitan.rom_ext_cw340.RLOCATION),
}
_BITSTREAM = {
"hyper310": r.Rlocation(*opentitan.bitstream_hyper310.RLOCATION),
"hyper340": r.Rlocation(*opentitan.bitstream_hyper340.RLOCATION),
}
except ImportError as e:
_LOG.fatal("runfiles could not open resources: %r", e)
def _parse_args():
"""Parse and return command line arguments."""
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--interface",
type=str,
help="opentitantool interface type",
)
parser.add_argument(
"--load-bitstream",
type=bool,
action=argparse.BooleanOptionalAction,
help="load a bitstream into the FPGA board",
)
parser.add_argument(
"--mechanism",
type=str,
default="bootstrap",
choices=["bootstrap", "rescue"],
help="mechanism used to load the image",
)
parser.add_argument(
"--elf",
type=pathlib.Path,
help="elf file ",
)
parser.add_argument(
"--bin",
type=pathlib.Path,
help="bin file",
)
parser.add_argument(
"--exit-success",
type=str,
default=None,
help="regex to determine success",
)
parser.add_argument(
"--exit-failure",
type=str,
default=None,
help="regex to determine failure",
)
return parser.parse_args()
def _detokenizer(image: Path, tokenized_file: Path, otto_finished: threading.Event):
try:
detokenizer = detokenize.Detokenizer(image)
line_buffer = ""
with open(tokenized_file, "r", buffering=1) as f:
while not otto_finished.is_set():
try:
chunk = f.readline()
if chunk:
# qemu may not write a complete line, so buffer
# the chunks until there is a complete line to
# pass to the detokenizer.
line_buffer += chunk
# Use a while loop, as there could also potentially
# be multiple lines printed in-between iterations.
while "\n" in line_buffer:
newline_pos = line_buffer.find("\n") + 1
complete_line = line_buffer[:newline_pos]
if not complete_line.endswith("\r\n"):
complete_line = complete_line.replace("\n", "\r\n")
detokenizer.detokenize_text_to_file(
complete_line, sys.stdout.buffer
)
sys.stdout.flush()
line_buffer = line_buffer[newline_pos:]
except BlockingIOError:
# If writing to stdout too fast, it's sometimes possible
# to get BlockingIOError due to the stdout buffer being
# full, so sleep and try again.
time.sleep(0.1)
# detokenize any remaining data in the buffer.
if line_buffer:
detokenizer.detokenize_text_to_file(complete_line, sys.stdout.buffer)
sys.stdout.flush()
except OSError as e:
print(f"Exception opening file {e}", file=sys.stderr)
def transport_init(interface: str):
subprocess.run(
[
_OTTO,
f"--interface={interface}",
"transport",
"init",
],
check=True,
)
def load_bitstream(interface: str):
"""Load an appropriate bitstream for the given board type."""
try:
bitstream = _BITSTREAM[interface]
except KeyError:
_LOG.error("No bitstream for board %s", interface)
sys.exit(1)
_LOG.info("Loading bitstream: %s", bitstream)
subprocess.run(
[
_OTTO,
f"--interface={interface}",
"fpga",
"load-bitstream",
bitstream,
],
check=True,
)
def load_and_run(
image: Path, interface: str, mechanism: str, exit_success: str, exit_failure: str
) -> list[str]:
"""Prepare opentitantool arguments to load an image into a board and spawn a console."""
if interface == "verilator":
# When the interface is verilator, we don't use any of the normal image
# loading mechanisms. Instead, the verilator test bench loads the image
# directly into flash memory before starting the simulation.
load_command = [
f"--verilator-bin={_VERILATOR}",
f"--verilator-rom={_TEST_ROM}",
f"--verilator-otp={_TEST_OTP}",
f"--verilator-flash={image}",
]
elif mechanism == "bootstrap":
try:
rom_ext = _ROM_EXT[interface]
except KeyError:
_LOG.error("No ROM_EXT for board %s", interface)
sys.exit(1)
# To load via bootstrap, we need to assemble a complete flash image that
# includes the ROM_EXT and the application image. Once assembled, we
# load that image into the device.
boot_img = image.with_suffix(".img")
load_command = [
"--exec",
f"image assemble --mirror=false --output={boot_img} {rom_ext}@0 {image}@0x10000",
"--exec",
f"bootstrap --clear-uart=true {boot_img}",
]
elif mechanism == "rescue":
# To load via rescue, we assume that the device already has a functional
# ROM_EXT and we trigger the serial rescue protocol to load the image.
load_command = ["--exec", f"rescue firmware {image}"]
else:
raise Exception("unknown mechanism", mechanism)
console_command = ["console", "--timestamp"]
if exit_success:
console_command.append(f"--exit-success={exit_success}")
if exit_failure:
console_command.append(f"--exit-failure={exit_failure}")
if exit_success and exit_failure:
console_command.append("--non-interactive")
return (
[
_OTTO,
f"--interface={interface}",
]
+ load_command
+ console_command
)
def simple_console(opentitantool_cmd: list[str]):
"""Invoke opentitantool for a simple (non-tokenized) console."""
_LOG.info("Invoking opentitantool: %s", opentitantool_cmd)
process = subprocess.run(opentitantool_cmd, check=False)
return process.returncode
def tokenized_console(opentitantool_cmd: list[str]):
"""Invoke opentitantool for a tokenized console."""
_LOG.info("Invoking opentitantool: %s", opentitantool_cmd)
with tempfile.NamedTemporaryFile() as f:
with subprocess.Popen(
args=otto_args,
stdout=f,
) as proc:
# Capturing the sub process stdout or stderr and then writing to
# stdout can cause deadlocks (see
# https://docs.python.org/3/library/subprocess.html#subprocess.Popen.stderr)
# due to a write buffer (child process) filling up the pipe
# buffer before the parent process can consume it.
# To work around this, write to a temp file, and have the
# detokenizer poll and detokenize the temp file.
otto_finished_event = threading.Event()
stdout_thread = threading.Thread(
target=_detokenizer,
args=(Path(args.elf), Path(f.name), otto_finished_event),
daemon=True,
)
stdout_thread.start()
out, err = proc.communicate()
otto_finished_event.set()
if out:
print(out)
if err:
print(err)
return_code = proc.returncode
stdout_thread.join()
return return_code
def _main(args) -> int:
if args.interface == "verilator":
# Verilator loads the unsigned binary. The build process for the
# kernel will detect when verilator is the target and will place
# a ROM_EXT header that will jump directly into the kernel.
pass
else:
transport_init(args.interface)
if args.load_bitstream:
load_bitstream(args.interface)
cmd = load_and_run(
args.bin, args.interface, args.mechanism, args.exit_success, args.exit_failure
)
# TODO(cfrantz): add support for the tokenized console.
return_code = simple_console(cmd)
sys.exit(return_code)
if __name__ == "__main__":
logging.basicConfig()
_main(_parse_args())