blob: 4610cebd0b88740e28845873286f792566170b78 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright lowRISC contributors.
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0
r"""OTP memory map class, used to create the associated RTL and
documentation, and to create OTP memory images for preloading.
"""
import logging as log
import random
from math import ceil, log2
from lib.common import check_bool, check_int, random_or_hexvalue
from mubi.prim_mubi import is_width_valid, mubi_value_as_int
from tabulate import tabulate
DIGEST_SUFFIX = "_DIGEST"
DIGEST_SIZE = 8
# Seed diversification constant for OtpMemMap (this enables to use
# the same seed for different classes)
OTP_SEED_DIVERSIFIER = 177149201092001677687
# This must match the rtl parameter ScrmblBlockWidth / 8
SCRAMBLE_BLOCK_WIDTH = 8
def _validate_otp(otp):
'''Validate OTP entry'''
otp.setdefault("depth", "1024")
otp.setdefault("width", "2")
otp["depth"] = check_int(otp["depth"])
otp["width"] = check_int(otp["width"])
otp["size"] = otp["depth"] * otp["width"]
otp["addr_width"] = ceil(log2(check_int(otp["depth"])))
otp["byte_addr_width"] = ceil(log2(otp["size"]))
def _validate_scrambling(scr):
'''Validate SCrambling entry'''
scr.setdefault("key_size", "16")
scr.setdefault("iv_size", "8")
scr.setdefault("cnst_size", "16")
scr["key_size"] = check_int(scr["key_size"])
scr["iv_size"] = check_int(scr["iv_size"])
scr["cnst_size"] = check_int(scr["cnst_size"])
if "keys" not in scr:
raise RuntimeError("Missing key configuration.")
if "digests" not in scr:
raise RuntimeError("Missing digest configuration.")
for key in scr["keys"]:
key.setdefault("name", "unknown_key_name")
key.setdefault("value", "<random>")
random_or_hexvalue(key, "value", scr["key_size"] * 8)
for dig in scr["digests"]:
dig.setdefault("name", "unknown_key_name")
dig.setdefault("iv_value", "<random>")
dig.setdefault("cnst_value", "<random>")
random_or_hexvalue(dig, "iv_value", scr["iv_size"] * 8)
random_or_hexvalue(dig, "cnst_value", scr["cnst_size"] * 8)
# if remaining number of bytes are not perfectly aligned, truncate
def _avail_blocks(size):
return int(size / SCRAMBLE_BLOCK_WIDTH)
# distribute number of blocks among partitions
def _dist_blocks(num_blocks, parts):
num_parts = len(parts)
if not num_parts:
return
# Very slow looping
for i in range(num_blocks):
parts[i % num_parts]['size'] += SCRAMBLE_BLOCK_WIDTH
# distribute unused otp bits
def _dist_unused(config, allocated):
# determine how many aligned blocks are left
# unaligned bits are not used
leftover_blocks = _avail_blocks(config['otp']['size'] - allocated)
# sponge partitions are partitions that will accept leftover allocation
sponge_parts = [part for part in config['partitions'] if part['absorb']]
# spread out the blocks
_dist_blocks(leftover_blocks, sponge_parts)
# return aligned partition size
def _calc_size(part, size):
size = SCRAMBLE_BLOCK_WIDTH * \
int((size + SCRAMBLE_BLOCK_WIDTH - 1) / SCRAMBLE_BLOCK_WIDTH)
if part["sw_digest"] or part["hw_digest"]:
size += DIGEST_SIZE
return size
def _validate_part(part, key_names):
'''Validates a partition within the OTP memory map'''
part.setdefault("name", "unknown_name")
part.setdefault("variant", "Unbuffered")
part.setdefault("secret", False)
part.setdefault("sw_digest", False)
part.setdefault("hw_digest", False)
part.setdefault("write_lock", "none")
part.setdefault("read_lock", "none")
part.setdefault("key_sel", "NoKey")
part.setdefault("absorb", False)
log.info("Validating partition {}".format(part["name"]))
# Make sure these are boolean types (simplifies the mako templates)
part["secret"] = check_bool(part["secret"])
part["sw_digest"] = check_bool(part["sw_digest"])
part["hw_digest"] = check_bool(part["hw_digest"])
part["bkout_type"] = check_bool(part["bkout_type"])
part["ecc_fatal"] = check_bool(part["ecc_fatal"])
# basic checks
if part["variant"] not in ["Unbuffered", "Buffered", "LifeCycle"]:
raise RuntimeError("Invalid partition type {}".format(part["variant"]))
if part["key_sel"] not in (["NoKey"] + key_names):
raise RuntimeError("Invalid key sel {}".format(part["key_sel"]))
if check_bool(part["secret"]) and part["key_sel"] == "NoKey":
raise RuntimeError(
"A secret partition needs a key select value other than NoKey")
if part["write_lock"].lower() not in ["digest", "csr", "none"]:
raise RuntimeError("Invalid value for write_lock")
if part["read_lock"].lower() not in ["digest", "csr", "none"]:
raise RuntimeError("Invalid value for read_lock")
if part["sw_digest"] and part["hw_digest"]:
raise RuntimeError(
"Partition cannot support both a SW and a HW digest at the same time."
)
if part["variant"] == "Unbuffered" and not part["sw_digest"]:
raise RuntimeError(
"Unbuffered partitions without digest are not supported at the moment."
)
if not part["sw_digest"] and not part["hw_digest"]:
if part["write_lock"].lower() == "digest" or part["read_lock"].lower(
) == "digest":
raise RuntimeError(
"A partition can only be write/read lockable if it has a hw or sw digest."
)
if not isinstance(part['items'], list):
raise RuntimeError('the "items" key must contain a list')
if len(part["items"]) == 0:
log.warning("Partition does not contain any items.")
# validate items and calculate partition size if necessary
size = 0
for item in part["items"]:
_validate_item(item)
size += item["size"]
# if size not previously defined, set it
if "size" not in part:
part["size"] = _calc_size(part, size)
# Make sure this has integer type.
part["size"] = check_int(part["size"])
# Make sure partition size is aligned.
if part["size"] % SCRAMBLE_BLOCK_WIDTH:
raise RuntimeError("Partition size must be 64bit aligned")
def _validate_item(item):
'''Validates an item within a partition'''
item.setdefault("name", "unknown_name")
item.setdefault("size", "0")
item.setdefault("isdigest", "false")
item.setdefault("ismubi", "false")
# make sure these have the correct types
item["isdigest"] = check_bool(item["isdigest"])
item["ismubi"] = check_bool(item["ismubi"])
item["size"] = check_int(item["size"])
item_width = item["size"] * 8
# defaults are handled differently in case of mubi
if item["ismubi"]:
if not is_width_valid(item_width):
raise RuntimeError("Mubi value {} has invalid width".format(
item["name"]))
# Convert default to correct mubi value
item.setdefault("inv_default", "false")
item["inv_default"] = check_bool(item["inv_default"])
item["inv_default"] = mubi_value_as_int(item["inv_default"],
item_width)
else:
# Generate random constant to be used when partition has
# not been initialized yet or when it is in error state.
random_or_hexvalue(item, "inv_default", item_width)
def _validate_mmap(config):
'''Validate the memory map configuration'''
# Get valid key names.
key_names = []
for key in config["scrambling"]["keys"]:
key_names.append(key["name"])
if not isinstance(config['partitions'], list):
raise RuntimeError('the "partitions" key must contain a list')
# validate inputs before use
allocated = 0
for part in config["partitions"]:
_validate_part(part, key_names)
allocated += part['size']
# distribute unallocated bits
_dist_unused(config, allocated)
# Determine offsets and generation dicts
offset = 0
part_dict = {}
for j, part in enumerate(config["partitions"]):
if part['name'] in part_dict:
raise RuntimeError('Partition name {} is not unique'.format(
part['name']))
part['offset'] = offset
if check_int(part['offset']) % SCRAMBLE_BLOCK_WIDTH:
raise RuntimeError(
"Partition {} offset must be 64bit aligned".format(
part['name']))
log.info("Partition {} at offset {} size {}".format(
part["name"], part["offset"], part["size"]))
# Loop over items within a partition
item_dict = {}
for k, item in enumerate(part["items"]):
if item['name'] in item_dict:
raise RuntimeError('Item name {} is not unique'.format(
item['name']))
item['offset'] = offset
log.info("> Item {} at offset {} with size {}".format(
item["name"], offset, item["size"]))
offset += check_int(item["size"])
item_dict[item['name']] = k
# Place digest at the end of a partition.
if part["sw_digest"] or part["hw_digest"]:
digest_name = part["name"] + DIGEST_SUFFIX
if digest_name in item_dict:
raise RuntimeError(
'Digest name {} is not unique'.format(digest_name))
item_dict[digest_name] = len(part["items"])
part["items"].append({
"name":
digest_name,
"size":
DIGEST_SIZE,
"offset":
check_int(part["offset"]) + check_int(part["size"]) -
DIGEST_SIZE,
"ismubi":
False,
"isdigest":
True,
"inv_default":
"<random>"
})
# Randomize the digest default.
random_or_hexvalue(part["items"][-1], "inv_default",
DIGEST_SIZE * 8)
# We always place the digest into the last 64bit word
# of a partition.
canonical_offset = (check_int(part["offset"]) +
check_int(part["size"]) - DIGEST_SIZE)
if offset > canonical_offset:
raise RuntimeError(
"Not enough space in partition "
"{} to accommodate a digest. Bytes available "
"= {}, bytes allocated to items = {}".format(
part["name"], part["size"], offset - part["offset"]))
offset = canonical_offset
log.info("> Adding digest {} at offset {} with size {}".format(
digest_name, offset, DIGEST_SIZE))
offset += DIGEST_SIZE
# check offsets and size
if offset > check_int(part["offset"]) + check_int(part["size"]):
raise RuntimeError("Not enough space in partition "
"{} to accommodate all items. Bytes available "
"= {}, bytes allocated to items = {}".format(
part["name"], part["size"],
offset - part["offset"]))
offset = check_int(part["offset"]) + check_int(part["size"])
part_dict.setdefault(part['name'], {'index': j, 'items': item_dict})
if offset > config["otp"]["size"]:
raise RuntimeError(
"OTP is not big enough to store all partitions. "
"Bytes available {}, bytes required {}", config["otp"]["size"],
offset)
log.info("Total number of partitions: {}".format(len(
config["partitions"])))
log.info("Bytes available in OTP: {}".format(config["otp"]["size"]))
log.info("Bytes required for partitions: {}".format(offset))
# return the partition/item index dict
return part_dict
class OtpMemMap():
# This holds the config dict.
config = {}
# This holds the partition/item index dict for fast access.
part_dict = {}
def __init__(self, config):
log.info('')
log.info('Parse and translate OTP memory map.')
log.info('')
if "seed" not in config:
raise RuntimeError("Missing seed in configuration.")
config["seed"] = check_int(config["seed"])
# Initialize RNG.
random.seed(OTP_SEED_DIVERSIFIER + int(config['seed']))
log.info('Seed: {0:x}'.format(config['seed']))
log.info('')
if "otp" not in config:
raise RuntimeError("Missing otp configuration.")
if "scrambling" not in config:
raise RuntimeError("Missing scrambling configuration.")
if "partitions" not in config:
raise RuntimeError("Missing partition configuration.")
# Validate OTP info.
_validate_otp(config["otp"])
# Validate scrambling info.
_validate_scrambling(config["scrambling"])
# Validate memory map.
self.part_dict = _validate_mmap(config)
self.config = config
log.info('')
log.info('Successfully parsed and translated OTP memory map.')
log.info('')
def create_partitions_table(self):
header = [
"Partition", "Secret", "Buffered", "WR Lockable", "RD Lockable",
"ECC Fatal Alert", "Description"
]
table = [header]
colalign = ("center", ) * len(header)
for part in self.config["partitions"]:
is_secret = "yes" if check_bool(part["secret"]) else "no"
is_buffered = "yes" if part["variant"] in [
"Buffered", "LifeCycle"
] else "no"
wr_lockable = "no"
if part["write_lock"].lower() in ["csr", "digest"]:
wr_lockable = "yes (" + part["write_lock"] + ")"
rd_lockable = "no"
if part["read_lock"].lower() in ["csr", "digest"]:
rd_lockable = "yes (" + part["read_lock"] + ")"
ecc_fatal = "no"
if part["ecc_fatal"]:
ecc_fatal = "yes"
# remove newlines
desc = ' '.join(part["desc"].split())
row = [
part["name"], is_secret, is_buffered, wr_lockable, rd_lockable,
ecc_fatal, desc
]
table.append(row)
return tabulate(table,
headers="firstrow",
tablefmt="pipe",
colalign=colalign)
def create_mmap_table(self):
header = [
"Index", "Partition", "Size [B]", "Access Granule", "Item",
"Byte Address", "Size [B]"
]
table = [header]
colalign = ("center", ) * len(header)
for k, part in enumerate(self.config["partitions"]):
for j, item in enumerate(part["items"]):
granule = "64bit" if check_bool(part["secret"]) else "32bit"
if check_bool(item["isdigest"]):
granule = "64bit"
name = "[{}](#Reg_{}_0)".format(item["name"],
item["name"].lower())
else:
name = item["name"]
if j == 0:
row = [str(k), part["name"], str(part["size"]), granule]
else:
row = ["", "", "", granule]
row.extend([
name, "0x{:03X}".format(check_int(item["offset"])),
str(item["size"])
])
table.append(row)
return tabulate(table,
headers="firstrow",
tablefmt="pipe",
colalign=colalign)
def create_digests_table(self):
header = ["Digest Name", " Affected Partition", "Calculated by HW"]
table = [header]
colalign = ("center", ) * len(header)
for part in self.config["partitions"]:
if check_bool(part["hw_digest"]) or check_bool(part["sw_digest"]):
is_hw_digest = "yes" if check_bool(part["hw_digest"]) else "no"
for item in part["items"]:
if check_bool(item["isdigest"]):
name = "[{}](#Reg_{}_0)".format(
item["name"], item["name"].lower())
row = [name, part["name"], is_hw_digest]
table.append(row)
break
else:
raise RuntimeError(
"Partition with digest does not contain a digest item")
return tabulate(table,
headers="firstrow",
tablefmt="pipe",
colalign=colalign)
def get_part(self, part_name):
''' Get partition by name, return None if it does not exist'''
entry = self.part_dict.get(part_name)
return (None if entry is None else
self.config['partitions'][entry['index']])
def get_item(self, part_name, item_name):
''' Get item by name, return None if it does not exist'''
entry = self.part_dict.get(part_name)
if entry is not None:
idx = entry['items'].get(item_name, None)
return (None if idx is None else
self.config['partitions'][entry['index']]['items'][idx])
else:
return None