blob: 032169d287a6b77344b5db744af4118b05e8e6bf [file] [log] [blame]
# 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''))