| #!/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 |