[fpga] Carry over ChipWhisperer spiflash tool form ot-sca repo This commit carries over the spiflash tool for ChipWhisperer FPGA boards from the ot-sca repository. This tool allows flashing firmware images into OpenTitan using the SAM3X/U MCUs on the ChipWhisperer FPGA boards. Signed-off-by: Pirmin Vogel <vogelpi@lowrisc.org>
diff --git a/util/fpga/cw_spiflash.py b/util/fpga/cw_spiflash.py new file mode 100644 index 0000000..09fbae8 --- /dev/null +++ b/util/fpga/cw_spiflash.py
@@ -0,0 +1,244 @@ +# Copyright lowRISC contributors. +# Licensed under the Apache License, Version 2.0, see LICENSE for details. +# SPDX-License-Identifier: Apache-2.0 +"""Utilities used to download firmware images into the OpenTitan system.""" + +import subprocess +import time +import hashlib +from functools import partial + +def _uint_to_le(val, length): + """Returns a byte array that represents an unsigned integer in little-endian format. + + Args: + val: Unsigned integer to convert. + length: Number of bytes. + + Returns: + A byte array of ``length`` bytes that represents ``val`` in little-endian format. + """ + return val.to_bytes(length=length, byteorder='little') + +def _le_to_uint(val): + """Returns the unsigned integer represented by the given byte array in little-endian format. + + Args: + val: Byte array in little-endian format that represents an unsigned integer. + + Returns: + The unsigned integer represented by the byte array ``val``. + """ + return int.from_bytes(val, byteorder='little') + +class _SpiFlashFrame: + """A frame used for programming OpenTitan over the SPI interface. + + In order to program OpenTitan with a binary image, the image is broken into ``PAYLOAD_SIZE`` + byte chunks, each of which is transmitted as a frame over the SPI interface. A frame consists + of four parts: + * Digest: A ``DIGEST_SIZE`` byte digest of the rest of the frame, + * Frame number: A ``FRAME_NUMBER_SIZE`` byte little-endian representation of the frame number. + MSB of the last frame's frame number is set to 1 to indicate the end of the image. + * Flash offset: A ``FLASH_OFFSET_SIZE`` byte little-endian representation of the flash offset + to start writing the payload of the frame. + * Payload: ``PAYLOAD_SIZE`` byte actual binary payload in little-endian format. + + OpenTitan signals correct transmission of frame N-1 by transmitting its digest (computed over + the entire frame, i.e. including the digest field of the frame) while it receives frame N. + + Attributes: + HASH_FUNCTION: Hash function to use for digest computations. + DIGEST_SIZE: Size of digests (inferred from ``HASH_FUNCTION``). + FRAME_NUMBER_SIZE: Size of the frame number field in bytes. + FLASH_OFFSET_SIZE: Size of the flash offset field in bytes. + PAYLOAD_SIZE: Size of the frame payload in bytes. + is_first_frame: Indicates if this is the first frame. + expected_ack: Expected acknowledgement value for a frame. + """ + HASH_FUNCTION = hashlib.sha256 + DIGEST_SIZE = HASH_FUNCTION().digest_size + FRAME_NUMBER_SIZE = 4 + FLASH_OFFSET_SIZE = 4 + PAYLOAD_SIZE = 2048 - DIGEST_SIZE - FRAME_NUMBER_SIZE - FLASH_OFFSET_SIZE + + def __init__(self, frame_number, flash_offset, payload): + """Inits a _SpiFlashFrame. + + Also pads frame payload to ``PAYLOAD_SIZE`` bytes if needed. + + Args: + frame_numer: Frame number. + flash_offset: Flash offset. + payload: Frame payload. + """ + self._frame_number = _uint_to_le(frame_number, self.FRAME_NUMBER_SIZE) + self._flash_offset = _uint_to_le(flash_offset, self.FLASH_OFFSET_SIZE) + self._payload = payload + padding = self.PAYLOAD_SIZE - len(self._payload) + if padding > 0: + self._payload = b''.join([self._payload, b'\xFF'*padding]) + self._digest = self.HASH_FUNCTION(b''.join([ + self._frame_number, + self._flash_offset, + self._payload, + ])).digest() + + def __bytes__(self): + return b''.join([ + self._digest, + self._frame_number, + self._flash_offset, + self._payload, + ]) + + def __repr__(self): + return f'frame 0x{_le_to_uint(self._frame_number):08X} @ 0x{_le_to_uint(self._flash_offset):08X}' + + @property + def is_first_frame(self): + """Indicates if this is the first frame.""" + return _le_to_uint(self._frame_number) == 0 + + @property + def expected_ack(self): + """Expected acknowledgement value for the frame.""" + return self.HASH_FUNCTION(bytes(self)).digest() + +class _Bootstrap: + """Handles low-level details during programming OpenTitan using SAM3U on CW305. + + Initializes pins, resets OpenTitan, transmits frames, and receives acknowledgements. + + To use: + >>> with _Bootstrap(fpga) as bootstrap: + >>> ... + >>> ack = bootstrap.transfer(frame) + >>> ... + """ + _PIN_SCK='USB_A13' + _PIN_SDI='USB_A14' + _PIN_SDO='USB_A15' + _PIN_CS='USB_A16' + _PIN_TRST='USB_A17' + _PIN_SRST='USB_A18' + _PIN_JTAG_SPI='USB_A19' + _PIN_BOOTSTRAP='USB_A20' + # Delays below are in seconds. + _BOOTSTRAP_DELAY=0.1 + _FIRST_FRAME_DELAY=0.2 + _INTER_FRAME_DELAY=0.02 + + def __init__(self, fpga): + """Inits a _Bootstrap with a CW305. + + Args: + fpga: CW305 to be programmed, a ``cw.capture.targets.CW305`` instance. + """ + # Configure JTAG and bootstrap pins. + self._fpga_io = fpga.gpio_mode() + self._fpga_io.pin_set_output(self._PIN_TRST) + self._fpga_io.pin_set_output(self._PIN_SRST) + self._fpga_io.pin_set_output(self._PIN_JTAG_SPI) + self._fpga_io.pin_set_output(self._PIN_BOOTSTRAP) + self._fpga_io.pin_set_state(self._PIN_TRST, 1) + self._fpga_io.pin_set_state(self._PIN_SRST, 1) + self._fpga_io.pin_set_state(self._PIN_JTAG_SPI, 1) + self._fpga_io.pin_set_state(self._PIN_BOOTSTRAP, 0) + # Initialize SPI pins. + self._fpga_io.spi1_setpins(sck=self._PIN_SCK, sdo=self._PIN_SDI, sdi=self._PIN_SDO, cs=self._PIN_CS) + self._fpga_io.spi1_enable(True) + + def _reset_opentitan(self): + self._fpga_io.pin_set_state(self._PIN_JTAG_SPI, 1) + self._fpga_io.pin_set_state(self._PIN_SRST, 0) + time.sleep(self._BOOTSTRAP_DELAY) + self._fpga_io.pin_set_state(self._PIN_SRST, 1) + time.sleep(self._BOOTSTRAP_DELAY) + + def __enter__(self): + """Starts bootstrapping.""" + self._fpga_io.pin_set_state(self._PIN_BOOTSTRAP, 1) + self._reset_opentitan() + self._fpga_io.pin_set_state(self._PIN_JTAG_SPI, 0) + time.sleep(self._BOOTSTRAP_DELAY) + return self + + def __exit__(self, exc_type, exc_value, traceback): + """Ends bootstrapping.""" + self._fpga_io.pin_set_state(self._PIN_BOOTSTRAP, 0) + self._fpga_io.pin_set_state(self._PIN_JTAG_SPI, 1) + time.sleep(self._BOOTSTRAP_DELAY) + + def transfer(self, frame): + """Transmits a frame over SPI and receives the acknowledgement for the previous frame.""" + # Wait longer after the first frame since it triggers a flash erase operation. + if frame.is_first_frame: + time.sleep(self._FIRST_FRAME_DELAY) + else: + time.sleep(self._INTER_FRAME_DELAY) + print(f'Transferring {frame}.') + # Acknowledgement is the same size as digests. + return bytes(self._fpga_io.spi1_transfer(bytes(frame))[:_SpiFlashFrame.DIGEST_SIZE]) + + +class SAM3UProgrammer: + """Class for programming OpenTitan over the SPI interface of SAM3U on CW305.""" + def __init__(self, firmware_path): + """Inits SAM3UProgrammer with the path of a firmware image.""" + self._firmware_path = firmware_path + + def run(self, fpga): + """Programs OpenTitan over the SPI interface of SAM3U on CW305. + + This implementation has two improvements over the `spiflash` tool in + lowRISC/opentitan: + * Optimizes the happy path by checking the acknowledgement of frame N-1 + after transmitting frame N instead of transmitting an extra frame to check + each frame's acknowledgement. + * Sends an all 0xFF frame as the last frame to avoid cases where the last + frame gets corrupted and halts the bootstrapping process. + + Args: + fpga: CW305 to be programmed, a ``cw.capture.targets.CW305`` instance. + """ + with _Bootstrap(fpga) as bootstrap: + frame_number = 0 + flash_offset = 0 + prev_frame = None + print(f'Programming OpenTitan with "{self._firmware_path}"...') + with open(self._firmware_path, mode='rb') as fw: + # Read fixed-size blocks from the firmware image. + # Note: The second argument ``b''`` to ``iter`` below is the sentinel value that + # ends the loop, i.e. the value returned by ``fw.read`` at EOF. + for payload in iter(partial(fw.read, _SpiFlashFrame.PAYLOAD_SIZE), b''): + frame = _SpiFlashFrame(frame_number, flash_offset, payload) + # We need to make sure that the previous frame is transmitted correctly before + # proceeding with the next frame. + while True: + # Transmit frame N, receive acknowledgement for frame N-1. + ack = bootstrap.transfer(frame) + if prev_frame and prev_frame.expected_ack != ack: + # When OpenTitan encounters a transmission error or an out of + # sequence frame, it ignores the current frame and sends the ack of + # the last successfully received frame. Since transmission errors + # can occur in both directions, i.e. the previous frame sent by + # SAM3U or the ack sent by OpenTitan, and we can't tell them + # apart, we should re-transmit the previous frame and check the + # ack sent by OpenTitan. + print(f'Error transferring {prev_frame}.') + ack = bootstrap.transfer(prev_frame) + if ack == frame.expected_ack: + # Ack sent by OpenTitan was corrupted. Proceed with the next frame. + break + else: + # Previous frame sent by SAM3U was corrupted. Send current frame again. + pass + else: + # Correct acknowledgement received or this is the first frame. + break + prev_frame = frame + frame_number += 1 + flash_offset += _SpiFlashFrame.PAYLOAD_SIZE + # Transmit an all 0xFF frame with the expected frame number to finish bootstrapping. + bootstrap.transfer(_SpiFlashFrame(frame_number | (1<<31), flash_offset, b''))