import math from ScEpTIC.emulator.energy import energy_utils from ScEpTIC.emulator.energy.mcu import MCUPowerState from ScEpTIC.emulator.energy.mcu_peripheral import MCUPeripheral from ScEpTIC.emulator.energy.mcu_peripheral.options import ExternalNVMState, NVMPowerState, MCUPeripheralPowerStateBehaviour from ScEpTIC.emulator.energy.mcu_peripheral.protocol_models.external_nvm_protocol import ExternalNVMProtocolAction from ScEpTIC.emulator.energy.power_state_event import PowerStateEvent from ScEpTIC.emulator.energy.voltage_regulator import NoRegulator from ScEpTIC.exceptions import ConfigurationException class ExternalNVM(MCUPeripheral): """ External NVM chip """ def __init__(self, chip_name): super().__init__() datasheet_module = __import__(f'ScEpTIC.emulator.energy.mcu_peripheral.datasheets.{chip_name}', fromlist=['']) self.datasheet = getattr(datasheet_module, 'datasheet') self.min_v = energy_utils.str_to_float(self.datasheet['min_v']) self.peak_frequency = energy_utils.str_to_float(self.datasheet['peak_frequency']) measurement_voltage = energy_utils.str_to_float(self.datasheet['measure_voltage']) standby_current_draw = energy_utils.str_to_float(self.datasheet['standby_current_draw']) # Divide by frequency to recalculate w.r.t. MCU frequency later operating_current_draw = energy_utils.str_to_float(self.datasheet['operating_current_draw']) / self.peak_frequency self.equivalent_resistance = { 'standby': energy_utils.equivalent_resistance(measurement_voltage, standby_current_draw), 'operating': energy_utils.equivalent_resistance(measurement_voltage, operating_current_draw) } if self.datasheet['has_regulator']: self.internal_regulator = self.datasheet['regulator_type']() v_out = energy_utils.str_to_float(self.datasheet['regulator_voltage']) self.internal_regulator.set_output_voltage(v_out) else: self.internal_regulator = NoRegulator() self.address_size = math.ceil(self.datasheet['address_size'] / 8) self.cell_size = math.ceil(self.datasheet['cell_size'] / 8) self.protocol = self.datasheet['protocol'](self.address_size) self.power_state = NVMPowerState.OFF self.power_state_behaviour = MCUPeripheralPowerStateBehaviour.FOLLOW_MCU # Default state: standby self.state = ExternalNVMState.STANDBY def _follow_mcu_power_state(self): """ Checks the MCU power state and updates the peripheral power state accordingly """ mcu_state = self.mcu.get_mcu_state() if mcu_state == MCUPowerState.ON: if self.power_state != NVMPowerState.ON: self.set_power_state(NVMPowerState.ON) self.set_state(ExternalNVMState.STANDBY) else: if self.power_state != NVMPowerState.OFF: self.set_state(ExternalNVMState.STANDBY) self.set_power_state(NVMPowerState.OFF) def attach_voltage_source(self, source): """ :param source: a voltage source """ self.internal_regulator.attach_voltage_source(source) super().attach_voltage_source(self.internal_regulator) def get_min_v(self): """ :return: the minimum voltage required by the external NVM to operate """ return self.min_v def set_state(self, state): """ Sets the NVM state (STANDBY / OPERATING) :param state: the NVM state (instance of ExternalNVMState) """ if self.power_state == NVMPowerState.OFF: raise Exception(f"NVM is off!") if not isinstance(state, ExternalNVMState): raise ConfigurationException(f"{self.__class__.__name__}.set_state() requires a ExternalNVMState but {state.__class__.__name__} was given") self.state = state def get_state(self): """ :return: the power state and the current state """ return self.power_state, self.state def set_power_state(self, power_state): """ Sets the NVM power state (ON / OFF) :param power_state: the NVM power state (instance of NVMPowerState) """ if not isinstance(power_state, NVMPowerState): raise ConfigurationException(f"{self.__class__.__name__}.set_power_state() requires a NVMPowerState but {power_state.__class__.__name__} was given") self.power_state = power_state def _get_drained_energy(self, t): """ Calculates the energy consumed by the component. :param t: elapsed time :return: the consumed energy """ if self.power_state == NVMPowerState.OFF: return 0.0 voltage = self.voltage_source.get_voltage() if self.state == ExternalNVMState.STANDBY: equivalent_resistance = self.equivalent_resistance['standby'] else: # we need to account for the operating frequency to calculate current draw in operating mode max_frequency = min(self.mcu.get_frequency(), self.peak_frequency) equivalent_resistance = self.equivalent_resistance['operating'] * max_frequency energy_draw = energy_utils.energy_from_R_t(voltage, equivalent_resistance, t) return self.voltage_source.get_drained_energy(energy_draw, t) def get_power_state_events(self): """ :return: a list of PowerStateEvent events that are occurring """ events = [] voltage = self.voltage_source.get_voltage() if voltage < self.min_v: self.power_state = NVMPowerState.OFF events.append(PowerStateEvent.NVM_OFF) return events def get_wait_cycles(self): """ :return: the cycles the MCU needs to wait to send the next command / bit """ mcu_frequency = self.mcu.get_frequency() if mcu_frequency <= self.peak_frequency: return 0 cycles = math.ceil(mcu_frequency / self.peak_frequency) # 1 cycle already used to send the command return cycles - 1 def execute_action(self, operation_code, system_model, n_bytes=0, additional_op_mode_name=None): """ Executes a peripheral action :param operation_code: the operation code (ExternalNVMProtocolAction) :param system_model: the system model :param n_bytes: the number of bytes, if the operation code is READ_BYTE or WRITE_BYTE :param additional_op_mode_name: additional operation mode name for metrics collection :return: the number of executed clock cycles """ bytes_multiplier = 1 if operation_code == ExternalNVMProtocolAction.READ_INIT or operation_code == ExternalNVMProtocolAction.WRITE_INIT: self.set_state(ExternalNVMState.OPERATING) elif operation_code == ExternalNVMProtocolAction.STOP: self.set_state(ExternalNVMState.STANDBY) elif operation_code == ExternalNVMProtocolAction.READ_BYTE or ExternalNVMProtocolAction.WRITE_BYTE: if self.state != ExternalNVMState.OPERATING: raise Exception(f"Cannot execute {operation_code}: {self.__class__.__name__} was not initialized!") if n_bytes == 0: raise Exception(f"Cannot execute {operation_code}: n_bytes must be > 0! ({n_bytes} provided)") bytes_multiplier = max(math.ceil(n_bytes / self.cell_size), 1) # calculate the number of clock cycles (MCU and peripheral may have different clock speeds) peripheral_ticks = self.protocol.get_operation_ticks(operation_code) * bytes_multiplier mcu_waits_per_peripheral_tick = self.get_wait_cycles() total_ticks = peripheral_ticks * (1 + mcu_waits_per_peripheral_tick) #print(f"Ticks: {peripheral_ticks}; MCU: {mcu_waits_per_peripheral_tick}; Total: {total_ticks}") n_ticks = 0 for _ in range(total_ticks): n_ticks += system_model.run_step(self.protocol.clock_cycle_action, self.protocol.op_mode_name, additional_op_mode_name=additional_op_mode_name) return n_ticks