| # 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 time |
| import hashlib |
| from functools import partial |
| from collections import namedtuple |
| |
| |
| 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. |
| FRAME_SIZE: Size of the SPI Flash frame, depends on the Boot ROM, see spiflash_frame.h. |
| is_second_frame: Indicates if this is the second 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 |
| FRAME_SIZE = 2048 |
| PAYLOAD_SIZE = FRAME_SIZE - 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_second_frame(self): |
| """Indicates if this is the second frame.""" |
| return _le_to_uint(self._frame_number) == 1 |
| |
| @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 SAM3X/U on CW310/305. |
| |
| Initializes pins, resets OpenTitan, transmits frames, and receives acknowledgements. |
| |
| To use: |
| >>> with _Bootstrap(fpga) as bootstrap: |
| >>> ... |
| >>> ack = bootstrap.transfer(frame) |
| >>> ... |
| """ |
| # Pin mappings for CW305/CW310 boards. |
| PinMapping = namedtuple('PinMapping', ['PIN_SCK', |
| 'PIN_SDI', |
| 'PIN_SDO', |
| 'PIN_CS', |
| 'PIN_TRST', |
| 'PIN_SRST', |
| 'PIN_JTAG_SPI', |
| 'PIN_BOOTSTRAP']) |
| _PIN_MAPPINGS = {} |
| _PIN_MAPPINGS['CW305'] = PinMapping('USB_A13', |
| 'USB_A14', |
| 'USB_A15', |
| 'USB_A16', |
| 'USB_A17', |
| 'USB_A18', |
| 'USB_A19', |
| 'USB_A20') |
| _PIN_MAPPINGS['CW310'] = PinMapping('USB_SPI_SCK', |
| 'USB_SPI_COPI', |
| 'USB_SPI_CIPO', |
| 'USB_SPI_CS', |
| 'USB_A17', |
| 'USB_A18', |
| 'USB_A19', |
| 'USB_A16') |
| _board = 'CW305' |
| |
| # Delays below are in seconds. |
| _BOOTSTRAP_DELAY = 0.1 |
| _SECOND_FRAME_DELAY = 0.2 |
| _INTER_FRAME_DELAY = 0.02 |
| |
| def __init__(self, fpga, board='CW305'): |
| """Inits a _Bootstrap with a CW310/305. |
| |
| Args: |
| fpga: CW310/305 to be programmed, a ``cw.capture.targets.CW310/305`` instance. |
| board: can be ``CW310`` or ``CW305`` to distinguish the different board types. |
| """ |
| |
| # Check board type. |
| if board in ['CW305', 'CW310']: |
| self._board = board |
| else: |
| raise ValueError('board must be either CW305 or CW310, got ' + board) |
| |
| # Configure JTAG and bootstrap pins. |
| self._fpga_io = fpga.gpio_mode() |
| self._fpga_io.pin_set_output(self._PIN_MAPPINGS[self._board].PIN_TRST) |
| self._fpga_io.pin_set_output(self._PIN_MAPPINGS[self._board].PIN_SRST) |
| self._fpga_io.pin_set_output(self._PIN_MAPPINGS[self._board].PIN_JTAG_SPI) |
| self._fpga_io.pin_set_output(self._PIN_MAPPINGS[self._board].PIN_BOOTSTRAP) |
| self._fpga_io.pin_set_state(self._PIN_MAPPINGS[self._board].PIN_TRST, 1) |
| self._fpga_io.pin_set_state(self._PIN_MAPPINGS[self._board].PIN_SRST, 1) |
| self._fpga_io.pin_set_state(self._PIN_MAPPINGS[self._board].PIN_JTAG_SPI, 1) |
| self._fpga_io.pin_set_state(self._PIN_MAPPINGS[self._board].PIN_BOOTSTRAP, 0) |
| # Initialize SPI pins. |
| self._fpga_io.spi1_setpins(sck=self._PIN_MAPPINGS[self._board].PIN_SCK, |
| sdo=self._PIN_MAPPINGS[self._board].PIN_SDI, |
| sdi=self._PIN_MAPPINGS[self._board].PIN_SDO, |
| cs=self._PIN_MAPPINGS[self._board].PIN_CS) |
| self._fpga_io.spi1_enable(True) |
| |
| def _reset_opentitan(self): |
| self._fpga_io.pin_set_state(self._PIN_MAPPINGS[self._board].PIN_JTAG_SPI, 1) |
| self._fpga_io.pin_set_state(self._PIN_MAPPINGS[self._board].PIN_SRST, 0) |
| time.sleep(self._BOOTSTRAP_DELAY) |
| self._fpga_io.pin_set_state(self._PIN_MAPPINGS[self._board].PIN_SRST, 1) |
| time.sleep(self._BOOTSTRAP_DELAY) |
| |
| def __enter__(self): |
| """Starts bootstrapping.""" |
| self._fpga_io.pin_set_state(self._PIN_MAPPINGS[self._board].PIN_BOOTSTRAP, 1) |
| self._reset_opentitan() |
| self._fpga_io.pin_set_state(self._PIN_MAPPINGS[self._board].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_MAPPINGS[self._board].PIN_BOOTSTRAP, 0) |
| self._fpga_io.pin_set_state(self._PIN_MAPPINGS[self._board].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_second_frame: |
| time.sleep(self._SECOND_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 SPIProgrammer: |
| """Class for programming OpenTitan over the SPI interface of SAM3X/U on CW310/305.""" |
| def __init__(self, firmware_path, board='CW305'): |
| """Inits SPIProgrammer with the path of a firmware image and board name.""" |
| self._firmware_path = firmware_path |
| self._board = board |
| |
| def run(self, fpga): |
| """Programs OpenTitan over the SPI interface of SAM3X/U on CW310/305. |
| |
| This implementation has two improvements over the `spiflash` tool under |
| sw/host/spiflash: |
| * 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: CW310/305 to be programmed, a ``cw.capture.targets.CW310/305`` instance. |
| """ |
| with _Bootstrap(fpga, self._board) 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'')) |