2026-07-10 10:38:57 +02:00

612 lines
21 KiB
Python

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)