import math from ScEpTIC.emulator.energy import energy_utils from ScEpTIC.emulator.energy.mcu.options import MCUClockCycleAction, ADCPowerState, MCUPowerState from ScEpTIC.emulator.energy.mcu.lookup_tables import LookupTable from ScEpTIC.emulator.energy.mcu_peripheral import MCUPeripheral from ScEpTIC.emulator.energy.mcu_peripheral.external_nvm import ExternalNVM from ScEpTIC.emulator.energy.mcu_peripheral.options import NVMPowerState from ScEpTIC.emulator.energy.power_state_event import PowerStateEvent from ScEpTIC.emulator.energy.voltage_drawner import VoltageDrawner from ScEpTIC.exceptions import ConfigurationException class MCUEnergyModel(VoltageDrawner): """ MCU Energy Model """ # min, max, avg ADC_I_TO_CONSIDER = 'min' def __init__(self, mcu_name, load_lookup_table=False, instruction_cache_hit_ratio=0.75): try: datasheet_module = __import__(f'ScEpTIC.emulator.energy.mcu.datasheets.{mcu_name}', fromlist=['']) except ModuleNotFoundError: mcu_module_name = mcu_name.split('-')[0] datasheet_module = __import__(f'ScEpTIC.emulator.energy.mcu.datasheets.{mcu_module_name}', fromlist=['']) self.datasheet = getattr(datasheet_module, 'datasheet') self.family = self.datasheet['family'] self._check_mcu_family(mcu_name) # program cache hit rate self.cache_hit = energy_utils.str_to_float(instruction_cache_hit_ratio) # general purpose regs self.registers = self.datasheet['registers'] self.reserved_registers = self.datasheet['reserved_registers'] # dimensions self.register_size = math.ceil(self.datasheet['register_size'] / 8) self.memory_cell_size = math.ceil(self.datasheet['memory_cell_size'] / 8) self.volatile_memory_size = energy_utils.str_to_int(self.datasheet['volatile_memory_size']) self.non_volatile_memory_size = energy_utils.str_to_int(self.datasheet['non_volatile_memory_size']) self.has_adc = self.datasheet['has_adc'] self.has_nvm = self.datasheet['has_nvm'] # Non-volatile memory wait cycles (i.e., extra clock cycles to complete the memory access) self.nvm_wait_cycles = {} # MCU equivalent resistance self._mcu_equivalent_r = {} # MCU minimum voltage per frequency self.mcu_min_v = {} # MCU supported frequencies self.frequencies = {} # Default frequency self.default_frequency = self.datasheet['default_frequency'] # ADC wait cycles (i.e., clock cycles to turn adc on and access it) self.adc_wait_cycles = {} # ADC instructions (ADC on, get data, ADC off) self.adc_instructions = {"on": 0, "transfer": 0, "off": 0} # ADC equivalent resistance self.adc_equivalent_r = 0.0 # ADC minimum voltage self.adc_min_v = 0.0 # LPM data self.lpm_data = {} self.lpm_r = 0.0 self.lpm_set = False self.lpm_v_min = 0.0 self.lpm_wait_cycles = {} # Populate variables with datasheet data self._populate_data() self.adc_power_state = ADCPowerState.OFF self.mcu_power_state = MCUPowerState.OFF self.frequency = None self.peripherals = set() self.has_exeternal_nvm = False self.v_on = 0.0 # Custom nominal frequency (e.g. when frequency scaling is enabled) self.dfs_enabled = False self.custom_frequency_name = None if load_lookup_table: lookup_table = LookupTable(mcu_name) self.set_lookup_table(lookup_table) else: self.set_lookup_table(None) # ticks and time tracking since latest power failure self.__ticks = 0 self.__time = 0.0 def get_clock_cycles(self): """ Returns current active cycle clock cycles count """ return self.__ticks def get_time(self): """ Returns the mcu time """ return self.__time def set_time(self, t): """ Sets the mcu time """ self.__time = t def power_failure_callback(self): """ Sets the last time and ticks when a power failure occurred """ self.__ticks = 0 self.__time = 0.0 def update_stats(self, time, ticks): """ Update stats """ if self.mcu_power_state == MCUPowerState.ON: self.__time += time self.__ticks += ticks def reset(self): """ Resets the MCU state """ self._last_power_failure_ticks = 0 self._last_power_failure_time = 0.0 def set_lookup_table(self, lookup_table): """ Sets the lookup table for the MCU energy model. :param lookup_table: instance of LookupTable """ if lookup_table is None: self.get_mcu_equivalent_r = self._get_mcu_equivalent_r_datasheet self.lookup_table = None elif isinstance(lookup_table, LookupTable): self.get_mcu_equivalent_r = self._get_mcu_equivalent_r_lookup_table self.lookup_table = lookup_table else: raise ConfigurationException(f"set_lookup_table() requires a LookupTable, {lookup_table.__class__.__name__} was given.") def set_v_on(self, v_on): """ Sets the minimum voltage to turn the MCU on :param v_on: minimum voltage to turn the MCU on """ self.v_on = v_on def get_power_state_events(self): """ :return: a list of PowerStateEvent events that are occurring """ events = [] voltage = self.get_voltage() # ADC ON but voltage < min voltage if self.adc_power_state == ADCPowerState.ON and voltage < self.adc_min_v: events.append(PowerStateEvent.ADC_OFF) self.adc_power_state = ADCPowerState.OFF # MCU ON but voltage < min voltage if (self.mcu_power_state == MCUPowerState.ON or self.mcu_power_state == MCUPowerState.LPM) and voltage < self.get_min_v(): events.append(PowerStateEvent.MCU_OFF) self.mcu_power_state = MCUPowerState.OFF return events def set_target_lpm(self, lpm_name): """ Sets the parameters of the MCU LPM used during the simulation :param lpm_name: the name of the MCU LPM """ if lpm_name not in self.lpm_data: raise ConfigurationException(f"LPM '{lpm_name}' not found.") self.lpm_set = True self.lpm_r = self.lpm_data[lpm_name]['R'] self.lpm_v_min = self.lpm_data[lpm_name]['V_min'] # Wakeup wait cycles (t_wait * MCU frequency) for f_name, f_val in self.frequencies.items(): self.lpm_wait_cycles[f_name] = math.ceil(float(self.lpm_data[lpm_name]['t_wakeup']) * float(f_val)) def set_frequency(self, f): """ Sets MCU frequency :param f: current frequency (string) """ # set default frequency if f is None: self.frequency = self.default_frequency return if f not in self.frequencies: raise ConfigurationException(f"Wrong frequency value {f}") self.frequency = f def get_frequency(self): """ :return: current MCU clock frequency """ return self.frequencies[self.frequency] def get_nominal_frequency(self): """ :return: a string representation of the MCU frequency """ if self.custom_frequency_name is None: return self.frequency return self.custom_frequency_name def get_voltage(self): """ :return: the voltage applied to the MCU """ return self.voltage_source.get_voltage() def get_min_v(self, ignore_current_frequency=False): """ :param ignore_current_frequency: returns the absolute minimum voltage, regardless of current frequency :return: the minimum voltage required to compute with current frequency """ if self.mcu_power_state == MCUPowerState.LPM: return self.lpm_v_min if ignore_current_frequency: return min(self.mcu_min_v.values()) return self.mcu_min_v[self.frequency] def get_mcu_cycle_time(self): """ :return: the time of a single clock cycle (seconds) """ return 1.0 / self.frequencies[self.frequency] def get_ADC_wait_cycles(self): """ :return: ADC wait cycles (turn ADC on + wait for sample) """ return self.adc_wait_cycles[self.frequency] def get_LPM_wait_cycles(self): """ :return: LPM to ON wait cycles """ return self.lpm_wait_cycles[self.frequency] def get_NVM_wait_cycles(self): """ :return: NVM wait cycles """ return self.nvm_wait_cycles[self.frequency] def get_action_wait_cycles(self, mcu_clock_action): """ :param mcu_clock_action: Clock cycle action :return: the number of wait cycles that the MCU executed due to the execution of mcu_clock_action """ # NVM wait cycles if mcu_clock_action == MCUClockCycleAction.NON_VOLATILE_MEMORY_ACCESS: return self.get_NVM_wait_cycles() # LPM exit wait cycles if mcu_clock_action == MCUClockCycleAction.LPM_EXIT: return self.get_LPM_wait_cycles() return 0 def _get_mcu_equivalent_r_lookup_table(self, mcu_clock_action): """ Implementation of get_mcu_equivalent_r that uses a LookupTable :param mcu_clock_action: current MCUCLockCycleAction :return: the equivalent resistance """ return self.lookup_table.get_value(self.frequency, mcu_clock_action, self.get_voltage()) def _get_mcu_equivalent_r_datasheet(self, mcu_clock_action): """ :param mcu_clock_action: current MCUCLockCycleAction :return: the equivalent resistance """ if mcu_clock_action == MCUClockCycleAction.PHYSICAL_MEMORY_ACCESS: mcu_clock_action = MCUClockCycleAction.NO_MEMORY_ACCESS try: return self._mcu_equivalent_r[self.frequency][mcu_clock_action] except KeyError: raise ConfigurationException(f"This MCU does not support {mcu_clock_action} clock cycle action") def get_drained_energy(self, mcu_clock_action, elapsed_time=None): """ Calculates the energy consumed by the MCU. :param mcu_clock_action: Clock cycle action :return: the consumed energy """ if mcu_clock_action == MCUClockCycleAction.NOP_OFF_RECHARGE: return 0.0 if self.mcu_power_state == MCUPowerState.OFF: raise Exception(f"Cannot execute {mcu_clock_action} - MCU is off!") if self.mcu_power_state == MCUPowerState.LPM: # When MCU in LPM -> No instruction can be executed (only NOP) if mcu_clock_action != MCUClockCycleAction.LPM_NOP and mcu_clock_action != MCUClockCycleAction.LPM_EXIT: raise Exception(f"Operation {mcu_clock_action} not supported while MCU is in LPM!") r_mcu = self.lpm_r else: r_mcu = self.get_mcu_equivalent_r(mcu_clock_action) # Compute energy voltage = self.get_voltage() if elapsed_time is None: e_mcu = energy_utils.energy_from_R_f(voltage, r_mcu, self.frequencies[self.frequency]) else: e_mcu = energy_utils.energy_from_R_t(voltage, r_mcu, elapsed_time) # If ADC on -> add its energy consumption if self.adc_power_state == ADCPowerState.ON: r_adc = self.adc_equivalent_r if elapsed_time is None: e_adc = energy_utils.energy_from_R_f(voltage, r_adc, self.frequencies[self.frequency]) else: e_adc = energy_utils.energy_from_R_t(voltage, r_adc, elapsed_time) e_mcu += e_adc # Enter LPM if mcu_clock_action == MCUClockCycleAction.LPM_ENTER: self.set_mcu_state(MCUPowerState.LPM) # Exit LPM if mcu_clock_action == MCUClockCycleAction.LPM_EXIT: self.set_mcu_state(MCUPowerState.LPM_WAKEUP) return e_mcu def set_adc_state(self, adc_state): """ Sets the current ADC state to adc_state :param adc_state: ADCPowerState state """ if not self.has_adc: raise ConfigurationException("MCU does not have an ADC!") if not isinstance(adc_state, ADCPowerState): raise ConfigurationException("Variable adc_state must be an ADCPowerState enum!") self.adc_power_state = adc_state def get_adc_state(self): """ :return: The ADC state (instance of ADCPowerState) """ return self.adc_power_state def set_mcu_state(self, mcu_state): """ Sets the current MCU state to mcu_state :param mcu_state: MCUPowerState state """ if not isinstance(mcu_state, MCUPowerState): raise ConfigurationException("Variable mcu_state must be a MCUPowerState enum!") self.mcu_power_state = mcu_state # Turn off ADC if mcu_state != MCUPowerState.ON and self.has_adc: self.set_adc_state(ADCPowerState.OFF) # Update external NVM power state, if present if self.has_exeternal_nvm: nvm_state = NVMPowerState.ON if mcu_state == MCUPowerState.ON else NVMPowerState.OFF self.get_external_nvm().set_power_state(nvm_state) def get_mcu_state(self): """ :return: The mcu state (instance of MCUPowerState) """ return self.mcu_power_state def attach_peripheral(self, peripheral): """ Attaches a peripheral to the MCU :param peripheral: the peripheral """ if not isinstance(peripheral, MCUPeripheral): raise ConfigurationException(f"Wrong peripheral model {peripheral.__class__.__name__}. MCUPeripheral expected") # Only one NVM supported (for the moment) if isinstance(peripheral, ExternalNVM): for p in self.peripherals: if isinstance(p, ExternalNVM): raise ConfigurationException(f"ScEpTIC system model currently supports only one external NVM attached to the MCU") self.has_exeternal_nvm = True self.peripherals.add(peripheral) def get_external_nvm(self): """ :return: an external NVM, if attached to the MCU; otherwise None """ for peripheral in self.peripherals: if isinstance(peripheral, ExternalNVM): return peripheral return None def _check_mcu_family(self, mcu_name): """ Verifies the correctness of the MCU family """ used_family = str(self.__class__.__name__).replace('EnergyModel', '') if used_family != self.family: raise ConfigurationException(f"Wrong EnergyModel: {mcu_name} has family {self.datasheet['family']}, but you are using the EnergyModel of {used_family} family!") def _populate_data(self): """ Populates the datasheet-related variables in a correct order. """ if self.has_adc: adc_data = self.datasheet['ADC'] self.adc_instructions = self._get_ADC_instructions(adc_data) self.adc_min_v = self._get_ADC_min_v(adc_data) self.adc_equivalent_r = self._calculate_ADC_equivalent_r(adc_data) # Frequency data for various operating modes for frequency, frequency_data in self.datasheet['frequencies'].items(): self.mcu_min_v[frequency] = self._get_MCU_min_v(frequency_data) self.frequencies[frequency] = self._get_MCU_frequency(frequency_data) self._mcu_equivalent_r[frequency] = self._calculate_MCU_equivalent_r(frequency, frequency_data) if self.has_nvm: self.nvm_wait_cycles[frequency] = self._get_NVM_wait_cycles(frequency_data) if self.has_adc: self.adc_wait_cycles[frequency] = self._calculate_ADC_wait_cycles(self.datasheet['ADC'], self.frequencies[frequency]) # LPM data for lpm_name, lpm_data in self.datasheet['LPM'].items(): self.lpm_data[lpm_name] = { 'R': self._calculate_MCU_LPM_R(lpm_data), 'V_min': self._get_MCU_LPM_V_min(lpm_data), 't_wakeup': self._get_MCU_LPM_t_wakeup(lpm_data), } def _calculate_MCU_equivalent_r(self, frequency_name, frequency_data): """ :param frequency_name: operating frequency name :param frequency_data: MCU datasheet information of a specific clock frequency :return: the equivalent resistance of the MCU in the various operating conditions. """ raise NotImplementedError(f"{self.__class__.__name__} does not implement _calculate_MCU_equivalent_r()") def _get_MCU_frequency(self, frequency_data): """ :param frequency_data: MCU datasheet information of a specific clock frequency :return: clock frequency """ raise NotImplementedError(f"{self.__class__.__name__} does not implement _get_MCU_frequency()") def _get_MCU_min_v(self, frequency_data): """ :param frequency_data: MCU datasheet information of a specific clock frequency :return: the minimum operating voltage of the MCU at a given clock frequency """ raise NotImplementedError(f"{self.__class__.__name__} does not implement _get_MCU_min_v()") def _get_NVM_wait_cycles(self, frequency_data): """ :param frequency_data: MCU datasheet information of a specific clock frequency :return: the wait cycles of NVM at a given clock frequency """ raise NotImplementedError(f"{self.__class__.__name__} does not implement _get_NVM_wait_cycles()") def _calculate_MCU_LPM_R(self, lpm_data): """ :param lpm_data: LPM datasheet information :return: the equivalent resistance of the MCU in LPM """ raise NotImplementedError(f"{self.__class__.__name__} does not implement _calculate_MCU_LPM_R()") def _get_MCU_LPM_V_min(self, lpm_data): """ :param lpm_data: LPM datasheet information :return: the minimum voltage required in LPM """ raise NotImplementedError(f"{self.__class__.__name__} does not implement _get_MCU_LPM_V_min()") def _get_MCU_LPM_t_wakeup(self, lpm_data): """ :param lpm_data: LPM datasheet information :return: the wakeup time from LPM to active mode """ raise NotImplementedError(f"{self.__class__.__name__} does not implement _get_MCU_LPM_t_wakeup()") def _calculate_ADC_equivalent_r(self, adc_data): """ :param adc_data: ADC datasheet information :return: the equivalent resistance of the ADC """ raise NotImplementedError(f"{self.__class__.__name__} does not implement _calculate_ADC_equivalent_r()") def _get_ADC_min_v(self, adc_data): """ :param adc_data: ADC datasheet information :return: the minimum operating voltage of the ADC """ raise NotImplementedError(f"{self.__class__.__name__} does not implement _get_ADC_min_v()") def _calculate_ADC_wait_cycles(self, adc_data, frequency): """ Calculates the number of cycles to activate the ADC, wait for its operativity, retrieve data, and turn it off :param adc_data: ADC datasheet information :param frequency: MCU frequency :return: the wait cycles """ raise NotImplementedError(f"{self.__class__.__name__} does not implement _calculate_ADC_wait_cycles()") def _get_ADC_instructions(self, adc_data): """ :param adc_data: ADC datasheet information :return: the instructions executed to turn on the ADC, retrieve data, and turn it off """ raise NotImplementedError(f"{self.__class__.__name__} does not implement _get_ADC_instructions()") def ALFRED_n_min(self, n_writes): """ Function to calculate the minimum number of read instructions required to create a volatile copy of a memory location. Necessary for virtual_memory transformation (ALFRED). :param n_writes: number of writes required for creating a volatile copy :return: number of minimum reads """ volatile_access = self.get_drained_energy(MCUClockCycleAction.VOLATILE_MEMORY_ACCESS) non_volatile_access = self.get_drained_energy(MCUClockCycleAction.NON_VOLATILE_MEMORY_ACCESS) numerator = volatile_access * float(n_writes) denominator = non_volatile_access * float(1 + self.nvm_wait_cycles[self.frequency]) - volatile_access value = float(numerator) / float(denominator) return math.floor(value)