def test_load_grouped_register_value_compatibility(data_dir): """Simply test to handle load of individual registers into grouped from YML.""" regs = Registers(TEST_DEVICE_NAME) group = [{"name": "TestRegA"}] regs.load_registers_from_xml(data_dir + "/grp_regs.xml", grouped_regs=group) yaml = YAML() with open(data_dir + "/group_none_reg.yml", "r") as yml_file: data = yaml.load(yml_file) regs.load_yml_config(data) reg = regs.find_reg("TestRegA") assert reg.get_hex_value() == "0x01020304111213142122232431323334" assert regs.find_reg( "TestRegA0", include_group_regs=True).get_hex_value() == "0x01020304" assert regs.find_reg( "TestRegA1", include_group_regs=True).get_hex_value() == "0x11121314" assert regs.find_reg( "TestRegA2", include_group_regs=True).get_hex_value() == "0x21222324" assert regs.find_reg( "TestRegA3", include_group_regs=True).get_hex_value() == "0x31323334"
class BaseConfigArea: """Base for CMPA and CFPA classes.""" CONFIG_DIR = PFR_DATA_FOLDER CONFIG_FILE = "database.json" BINARY_SIZE = 512 ROTKH_SIZE = 32 ROTKH_REGISTER = "ROTKH" MARK = b"SEAL" DESCRIPTION = "Base Config Area" IMAGE_PREFILL_PATTERN = 0 def __init__(self, device: str = None, revision: str = None, user_config: PfrConfiguration = None) -> None: """Initialize an instance. :param device: device to use, list of supported devices is available via 'devices' method :param revision: silicon revision, if not specified, the latest is being used :param user_config: PfrConfiguration with user configuration to use with initialization :raises SPSDKError: When no device is provided :raises SPSDKError: When no device is not supported :raises SPSDKError: When there is invalid revision """ if not (device or user_config): raise SPSDKError("No device provided") self.config = self._load_config() # either 'device' or 'user_config' IS defined! Mypy doesn't understand the check above self.device = device or user_config.device # type: ignore if self.device not in self.config.get_devices(): raise SPSDKError(f"Device '{self.device}' is not supported") self.revision = revision or (user_config.revision if user_config else "latest") if not self.revision or self.revision == "latest": self.revision = self.config.get_latest_revision(self.device) logger.warning( f"The silicon revision is not specified, the latest: '{self.revision}' has been used." ) if self.revision not in self.config.get_revisions(self.device): raise SPSDKError( f"Invalid revision '{self.revision}' for '{self.device}'") self.registers = Registers(self.device) self.registers.load_registers_from_xml( xml=self.config.get_data_file(self.device, self.revision), filter_reg=self.config.get_ignored_registers(self.device), grouped_regs=self.config.get_grouped_registers(self.device), ) # Set the computed field handler for reg, fields in self.config.get_computed_fields( self.device).items(): reg_obj = self.registers.find_reg(reg) reg_obj.add_setvalue_hook(self.reg_computed_fields_handler, fields) self.user_config = PfrConfiguration( config=user_config, device=self.device, revision=self.revision, cfg_type=self.__class__.__name__, ) if self.user_config.settings: self.set_config(self.user_config, raw=False) def reg_computed_fields_handler(self, val: bytes, context: Any) -> bytes: """Recalculate all fields for given register value. :param val: Input register value. :param context: The method context (fields). :return: recomputed value. :raises SPSDKPfrError: Raises when the computing routine is not found. """ fields: dict = context for method in fields.values(): if hasattr(self, method): method_ref = getattr(self, method, None) val = method_ref(val) else: raise SPSDKPfrError( f"The '{method}' compute function doesn't exists.") return val @staticmethod def pfr_reg_inverse_high_half(val: int) -> int: """Function that inverse low 16-bits of register value to high 16 bits. :param val: Input current reg value. :return: Returns the complete register value with updated higher half field. """ ret = val & 0xFFFF ret |= (ret ^ 0xFFFF) << 16 return ret @classmethod def _load_config(cls) -> RegConfig: """Loads the PFR block configuration file. :return: PFR block configuration database. """ return RegConfig(os.path.join(cls.CONFIG_DIR, cls.CONFIG_FILE)) @classmethod def devices(cls) -> List[str]: """Classmethod to get list of supported devices. :return: List of supported devices. """ config = cls._load_config() return config.get_devices() def _get_registers(self) -> List[RegsRegister]: """Get a list of all registers. :return: List of PFR configuration registers. """ exclude = self.config.get_ignored_registers(self.device) return self.registers.get_registers(exclude) def set_config(self, config: PfrConfiguration, raw: bool = False) -> None: """Apply configuration from file. :param config: PFR configuration. :param raw: When set all (included computed fields) configuration will be applied. :raises SPSDKError: When device is not provided. :raises SPSDKError: When revision is not provided. :raises SPSDKPfrConfigError: Invalid config file. """ if not self.device: raise SPSDKError("No device provided") if not self.revision: raise SPSDKError("No revision provided") if config.device != self.device: raise SPSDKPfrConfigError( f"Invalid device in configuration. {self.device} != {config.device}" ) if not config.revision or config.revision in ("latest", ""): config.revision = self.config.get_latest_revision(self.device) logger.warning( f"The configuration file doesn't contains silicon revision, \ the latest: '{config.revision}' has been used.") if config.revision != self.revision: raise SPSDKPfrConfigError( f"Invalid revision in configuration. {self.revision} != {config.revision}" ) if config.type and config.type.upper() != self.__class__.__name__: raise SPSDKPfrConfigError( f"Invalid configuration type. {self.__class__.__name__} != {config.type}" ) if not config.settings: raise SPSDKPfrConfigError("Missing configuration of PFR fields!") computed_regs = [] computed_regs.extend(self.config.get_ignored_registers(self.device)) if not raw: computed_regs.extend( self.config.get_computed_registers(self.device)) computed_fields = None if raw else self.config.get_computed_fields( self.device) self.registers.load_yml_config(config.settings, computed_regs, computed_fields) if not raw: # # Just update only configured registers exclude_hooks = [] if not self.config.get_value("mandatory_computed_regs", self.device): exclude_hooks.extend( list( set(self.registers.get_reg_names()) - set(config.settings.keys()))) self.registers.run_hooks(exclude_hooks) def get_yaml_config(self, exclude_computed: bool = True, diff: bool = False, indent: int = 0) -> CM: """Return YAML configuration from loaded registers. :param exclude_computed: Omit computed registers and fields. :param diff: Get only configuration with difference value to reset state. :param indent: YAML start indent. :return: YAML PFR configuration in commented map(ordered dict). """ computed_regs = (None if not exclude_computed else self.config.get_computed_registers(self.device)) computed_fields = (None if not exclude_computed else self.config.get_computed_fields(self.device)) ignored_fields = self.config.get_ignored_fields(self.device) data = self.registers.create_yml_config(computed_regs, computed_fields, ignored_fields, diff, indent + 2) return self.user_config.get_yaml_config(data, indent) def generate_config(self, exclude_computed: bool = True) -> CM: """Generate configuration structure for user configuration. :param exclude_computed: Exclude computed fields, defaults to True. :return: YAML commented map with PFR configuration in reset state. """ # Create own copy to keep self as is and get reset values by standard YML output copy_of_self = copy.deepcopy(self) copy_of_self.registers.reset_values() return copy_of_self.get_yaml_config(exclude_computed) def _calc_rotkh(self, keys: List[PublicKey]) -> bytes: """Calculate ROTKH (Root Of Trust Key Hash). :param keys: List of Keys to compute ROTKH. :return: Value of ROTKH with right width. :raises SPSDKPfrError: Algorithm width doesn't fit into ROTKH field. """ # the data structure use for computing final ROTKH is 4*32B long # 32B is a hash of individual keys # 4 is the max number of keys, if a key is not provided the slot is filled with '\x00' # The LPC55S3x has two options to compute ROTKH, so it's needed to be # detected the right algorithm and mandatory warn user about this selection because # it's MUST correspond to settings in eFuses! reg_rotkh = self.registers.find_reg("ROTKH") width = reg_rotkh.width if isinstance(keys[0], rsa.RSAPublicKey): algorithm_width = 256 else: algorithm_width = keys[0].key_size if algorithm_width > width: raise SPSDKPfrError( "The ROTKH field is smaller than used algorithm width.") key_hashes = [ calc_pub_key_hash(key, openssl_backend, algorithm_width) for key in keys ] data = [ key_hashes[i] if i < len(key_hashes) else bytes(algorithm_width // 8) for i in range(4) ] return openssl_backend.hash(bytearray().join(data), f"sha{algorithm_width}").ljust( width // 8, b"\x00") def _get_seal_start_address(self) -> int: """Function returns start of seal fields for the device. :return: Start of seals fields. :raises SPSDKError: When 'seal_start_address' in database.json can not be found """ start = self.config.get_seal_start_address(self.device) if not start: raise SPSDKError( "Can't find 'seal_start_address' in database.json") return self.registers.find_reg(start).offset def _get_seal_count(self) -> int: """Function returns seal count for the device. :return: Count of seals fields. :raises SPSDKError: When 'seal_count' in database.json can not be found """ count = self.config.get_seal_count(self.device) if not count: raise SPSDKError("Can't find 'seal_count' in database.json") return value_to_int(count) def export(self, add_seal: bool = False, keys: List[PublicKey] = None) -> bytes: """Generate binary output. :param add_seal: The export is finished in the PFR record by seal. :param keys: List of Keys to compute ROTKH field. :return: Binary block with PFR configuration(CMPA or CFPA). :raises SPSDKPfrRotkhIsNotPresent: This PFR block doesn't contains ROTKH field. :raises SPSDKError: The size of data is {len(data)}, is not equal to {self.BINARY_SIZE}. """ if keys: try: # ROTKH may or may not be present, derived class defines its presense rotkh_reg = self.registers.find_reg(self.ROTKH_REGISTER) rotkh_data = self._calc_rotkh(keys) rotkh_reg.set_value(rotkh_data, True) except SPSDKRegsErrorRegisterNotFound as exc: raise SPSDKPfrRotkhIsNotPresent( "This device doesn't contain ROTKH register!") from exc data = bytearray([self.IMAGE_PREFILL_PATTERN] * self.BINARY_SIZE) for reg in self._get_registers(): data[reg.offset:reg.offset + reg.width // 8] = reg.get_bytes_value() if add_seal: seal_start = self._get_seal_start_address() seal_count = self._get_seal_count() data[seal_start:seal_start + seal_count * 4] = self.MARK * seal_count if len(data) != self.BINARY_SIZE: raise SPSDKError( f"The size of data is {len(data)}, is not equal to {self.BINARY_SIZE}" ) return bytes(data) def parse(self, data: bytes) -> None: """Parse input binary data to registers. :param data: Input binary data of PFR block. """ for reg in self._get_registers(): value = bytearray(data[reg.offset:reg.offset + reg.width // 8]) # don't change endian if register is meant to be used in 'reverse' (array of bytes) reg.set_value(value if reg.reverse else change_endianism(value), raw=True)
class ShadowRegisters: """SPSDK support to control the shadow registers.""" def __init__( self, debug_probe: DebugProbe, config: RegConfig, device: str, revision: str = "latest", ) -> None: """Initialization of Shadow register class.""" self.probe = debug_probe self.config = config self.device = device self.offset = int( self.config.get_address(self.device, remove_underscore=True), 16) self.regs = Registers(self.device) rev = revision or "latest" rev = rev if rev != "latest" else config.get_latest_revision( self.device) self.regs.load_registers_from_xml( config.get_data_file(self.device, rev), grouped_regs=config.get_grouped_registers(self.device), ) # Set the computed field handler for reg, fields in self.config.get_computed_fields( self.device).items(): reg_obj = self.regs.find_reg(reg) reg_obj.add_setvalue_hook(self.reg_computed_fields_handler, fields) # Set the antipolize handler for reg, antipole_reg in self.config.get_antipole_regs( self.device).items(): src = self.regs.find_reg(reg) dst = self.regs.find_reg(antipole_reg) src.add_setvalue_hook(self.reg_antipolize_src_handler, dst) dst.add_setvalue_hook(self.reg_antipolize_dst_handler, src) def _write_shadow_reg(self, addr: int, data: int, verify: int = True) -> None: """The function write a shadow register. The function writes shadow register in to MCU and verify the write if requested. param addr: Shadow register address. param data: Shadow register data to write. param verify: If True the write is read back and compare, otherwise no check is done raises IoVerificationError """ self.probe.mem_reg_write(addr, data) if verify: readback = self.probe.mem_reg_read(addr) if readback != data: raise IoVerificationError( f"The written data 0x{data:08X} to 0x{addr:08X} address are invalid." ) def reload_registers(self) -> None: """Reload all the values in managed registers.""" for reg in self.regs.get_registers(): self.reload_register(reg) def sets_all_registers(self) -> None: """Update all shadow registers in target by local values.""" for reg in self.regs.get_registers(): self.set_register(reg.name, reg.get_value()) def reload_register(self, reg: RegsRegister) -> None: """Reload the value in requested register. :param reg: The register to reload from the HW. """ reg.set_value(self.get_register(reg.name)) def set_register(self, reg_name: str, data: Any) -> None: """The function sets the value of the specified register. param reg: The register name. param data: The new data to be stored to shadow register. raises SPSDKDebugProbeError: The debug probe is not specified. """ if self.probe is None: raise SPSDKDebugProbeError("There is no debug probe.") try: reg = self.regs.find_reg(reg_name) value = value_to_bytes(data) start_address = self.offset + reg.offset width = reg.width if width < len(value) * 8: raise SPSDKError( "Invalid length of data for shadow register write.") width = max(width, 32) data_aligned = bytearray(math.ceil(width / 8)) data_aligned[len(data_aligned) - len(value):len(data_aligned)] = value end_address = start_address + math.ceil(width / 8) addresses = range(start_address, end_address, 4) for i, addr in enumerate(addresses): val = data_aligned[i * 4:i * 4 + 4] self._write_shadow_reg( addr, int.from_bytes( change_endianism(val) if reg.reverse else val, "big"), ) reg.set_value(value, raw=True) except SPSDKError as exc: raise SPSDKError( f"The get shadow register failed({str(exc)}).") from exc def get_register(self, reg_name: str) -> bytes: """The function returns value of the requested register. param reg: The register name. return: The value of requested register in bytes raises SPSDKDebugProbeError: The debug probe is not specified. """ if self.probe is None: raise SPSDKDebugProbeError("There is no debug probe.") array = bytearray() try: reg = self.regs.find_reg(reg_name) start_address = self.offset + reg.offset width = max(reg.width, 32) if width == 32: array.extend( self.probe.mem_reg_read(start_address).to_bytes(4, "big")) else: end_address = start_address + math.ceil(width / 8) addresses = range(start_address, end_address, 4) for addr in addresses: array.extend( self.probe.mem_reg_read(addr).to_bytes(4, "big")) result = reverse_bytes_in_longs( bytes(array)) if reg.reverse else bytes(array) except SPSDKError as exc: raise SPSDKError( f"The get shadow register failed({str(exc)}).") from exc return result def create_yml_config(self, file_name: str, raw: bool = False) -> None: """The function creates the configuration YML file. :param file_name: The file_name (without extension) of stored configuration. :param raw: Raw output of configuration (including computed fields and anti-pole registers) """ antipole_regs = None if raw else list( self.config.get_antipole_regs(self.device).values()) computed_fields = None if raw else self.config.get_computed_fields( self.device) yaml = YAML() yaml.indent(sequence=SPSDK_YML_INDENT * 2, offset=SPSDK_YML_INDENT) data = CM() description = CM() description.yaml_set_start_comment( f"NXP {self.device.upper()} Shadow registers configuration", indent=2) description.insert(1, "device", self.device, comment="The NXP device name.") description.insert(2, "version", __version__, comment="The SPSDK Shadow register tool version.") description.insert(3, "author", __author__, comment="The author of the configuration.") description.insert(4, "release", __release__, comment="The SPSDK release.") data["description"] = description data["registers"] = self.regs.create_yml_config( exclude_regs=antipole_regs, exclude_fields=computed_fields, indent=2) with open(file_name, "w", encoding="utf8") as out_file: yaml.dump(data, out_file) def load_yml_config(self, file_name: str, raw: bool = False) -> None: """The function loads the configuration from YML file. :param file_name: The file_name (without extension) of stored configuration. :param raw: Raw input of configuration (including computed fields and anti-pole registers) :raises SPSDKError: When the configuration file not found. """ antipole_regs = None if raw else list( self.config.get_antipole_regs(self.device).values()) computed_fields = None if raw else self.config.get_computed_fields( self.device) try: with open(file_name, "r", encoding="utf8") as yml_config_file: yaml = YAML() yaml.indent(sequence=4, offset=2) data = yaml.load(yml_config_file) except FileNotFoundError as exc: raise SPSDKError( "File with YML configuration doesn't exists.") from exc self.regs.load_yml_config(data["registers"], antipole_regs, computed_fields) if not raw: # Just update only configured registers exclude_hooks = list( set(self.regs.get_reg_names()) - set(data["registers"].keys())) self.regs.run_hooks(exclude_hooks) logger.debug( "The shadow registers has been loaded from configuration.") @staticmethod def reg_antipolize_src_handler(val: int, context: Any) -> int: """Antipolize given register value. :param val: Input register value. :param context: The method context. :return: Antipolized value. """ dst_reg: RegsRegister = context dst_reg.set_value(val ^ 0xFFFFFFFF, raw=True) return val @staticmethod def reg_antipolize_dst_handler(val: int, context: Any) -> int: """Keep same antipolized register value in computed register. :param val: Input register value. :param context: The method context. :return: Antipolized value. """ src_reg: RegsRegister = context val = src_reg.get_value() new_val = val ^ 0xFFFFFFFF return new_val def reg_computed_fields_handler(self, val: bytes, context: Any) -> bytes: """Recalculate all fields for given register value. :param val: Input register value. :param context: The method context (fields). :return: recomputed value. :raises SPSDKError: Raises when the computing routine is not found. """ fields: dict = context for method in fields.values(): if hasattr(self, method): method_ref = getattr(self, method, None) val = method_ref(val) else: raise SPSDKError( f"The '{method}' compute function doesn't exists.") return val # CRC8 - ITU @staticmethod def crc_update(data: bytes, crc: int = 0, is_final: bool = True) -> int: """The function compute the CRC8 ITU method from given bytes. :param data: Input data to compute CRC. :param crc: The seed for CRC. :param is_final: The flag the the function should return final result. :return: The CRC result. """ k = 0 data_len = len(data) while data_len != 0: data_len -= 1 carry = data[k] k += 1 for i in range(8): bit = (crc & 0x80) != 0 if (carry & (0x80 >> i)) != 0: bit = not bit crc <<= 1 if bit: crc ^= 0x07 crc &= 0xFF if is_final: return (crc & 0xFF) ^ 0x55 return crc & 0xFF @staticmethod def comalg_dcfg_cc_socu_crc8(val: int) -> int: """Function that creates the crc for DCFG_CC_SOCU. :param val: Input DCFG_CC_SOCU Value. :return: Returns the value of DCFG_CC_SOCU with computed CRC8 field. """ in_val = bytearray(3) for i in range(3): in_val[i] = (val >> (8 + i * 8)) & 0xFF val &= ~0xFF val |= ShadowRegisters.crc_update(in_val) return val @staticmethod def comalg_dcfg_cc_socu_rsvd(val: int) -> int: """Function fill up the DCFG_CC_SOCU RSVD filed by 0x80 to satisfy MCU needs. :param val: Input DCFG_CC_SOCU Value. :return: Returns the value of DCFG_CC_SOCU with computed CRC8 field. """ new_val = val | 0x80000000 return new_val @staticmethod def comalg_do_nothing(val: int) -> int: """Function that do nothing. :param val: Input Value. :return: Returns same value as it get. """ return val