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

233 lines
9.1 KiB
Python

import math
from ScEpTIC.emulator.energy import energy_utils
from ScEpTIC.emulator.energy.mcu import MCUEnergyModel
from ScEpTIC.emulator.energy.mcu.options import MCUClockCycleAction
class MSP430FREnergyModel(MCUEnergyModel):
"""
MCU energy model for MSP430-FR series from Texas Instruments
"""
def _get_MCU_frequency(self, frequency_data):
"""
:param frequency_data: MCU datasheet information of a specific clock frequency
:return: clock frequency
"""
return energy_utils.str_to_float(frequency_data["frequency"])
def _get_MCU_nominal_v(self, frequency_data):
"""
:param frequency_data: MCU datasheet information of a specific clock frequency
:return: the nominal voltage at which the current draws of the MCU were measured
"""
return energy_utils.str_to_float(frequency_data['V'])
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
"""
return energy_utils.str_to_float(frequency_data['V_min'])
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
"""
return energy_utils.str_to_int(frequency_data["n_waits"])
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.
"""
data = {}
nominal_v = self._get_MCU_nominal_v(frequency_data)
current_draws = self._calculate_MCU_I(frequency_data)
for operating_mode, current_draw in current_draws.items():
data[operating_mode] = energy_utils.equivalent_resistance(nominal_v, current_draw)
return data
def __get_MCU_I_am_fram(self, frequency_data):
if str(self.cache_hit) in frequency_data["I_am_fram"]:
return frequency_data["I_am_fram"][str(self.cache_hit)]["I"]
else:
lower_bound = None
upper_bound = None
for hit_rate in frequency_data["I_am_fram"].keys():
if float(hit_rate) > self.cache_hit:
upper_bound = hit_rate
break
lower_bound = hit_rate
hit_lower_bound = float(lower_bound)
hit_upper_bound = float(upper_bound)
I_lower_bound = energy_utils.str_to_float(frequency_data["I_am_fram"][lower_bound]["I"])
I_upper_bound = energy_utils.str_to_float(frequency_data["I_am_fram"][upper_bound]["I"])
data = I_lower_bound + (self.cache_hit - hit_lower_bound) / (hit_upper_bound - hit_lower_bound) * (I_upper_bound - I_lower_bound)
#print(f"{hit_upper_bound} = {I_upper_bound}; {hit_lower_bound} = {I_lower_bound}; => {self.cache_hit} = {data}")
return data
def _calculate_MCU_I(self, frequency_data):
"""
Calculates the current draw of the MCU in the various operating conditions.
:param frequency_data: MCU datasheet information of a specific clock frequency
"""
# Program -> SRAM; Data -> SRAM
I_am_ram = energy_utils.str_to_float(frequency_data["I_am_ram"])
# Program -> FRAM; Data -> FRAM
I_am_fram_uni = energy_utils.str_to_float(frequency_data["I_am_fram_uni"])
# Program -> FRAM; Data -> SRAM
#I_am_fram = energy_utils.str_to_float(frequency_data["I_am_fram"][str(self.cache_hit)]["I"])
I_am_fram = energy_utils.str_to_float(self.__get_MCU_I_am_fram(frequency_data))
# I_am_ram and I_am_fram_uni consider 2 accesses per clock cycle (instruction and data)
I_volatile_access = I_am_ram / 2.0
I_non_volatile_access = I_am_fram_uni / 2.0
"""
I_am_fram considers 1 non-volatile access (program) and 1 volatile access (data) + normal clock cycle overhead
To identify the clock cycle current draw when no data is accessed, we need to subtract I_volatile_access (volatile access)
from I_am_fram, as data resides in SRAM when I_am_fram is measured.
-> I_cycle = I_am_fram - I_volatile_access
Then, we need to account for cache hit/miss -> when a cache miss occurs, we need to retrieve the program from FRAM and not from cache.
When a cache miss occurrs, we need to account for the current consumption of the extra wait cycle to access FRAM.
E.g., with a cache hit rate of 0.75, 75% of the times we require only 1 cycle and 25% of the times we require 1 cycle + the extra fram wait cycles, if any.
-> I_cycle = I_cycle * (hit_rate * 1 + (1-hit_rate) * (1 + fram_wait_cycle)
"""
wait_cycles = energy_utils.str_to_int(frequency_data["n_waits"])
I_no_access = (I_am_fram - I_volatile_access) * (self.cache_hit + (1-self.cache_hit) * (1 + wait_cycles))
data = {
MCUClockCycleAction.NO_MEMORY_ACCESS: I_no_access,
MCUClockCycleAction.I2C_ACCESS: I_no_access,
MCUClockCycleAction.SPI_ACCESS: I_no_access,
MCUClockCycleAction.LPM_ENTER: I_no_access,
MCUClockCycleAction.LPM_EXIT: I_no_access,
MCUClockCycleAction.VOLATILE_MEMORY_ACCESS: I_no_access + I_volatile_access,
MCUClockCycleAction.NON_VOLATILE_MEMORY_ACCESS: I_no_access + I_non_volatile_access,
}
return data
def _calculate_MCU_LPM_R(self, lpm_data):
"""
:param lpm_data: LPM datasheet information
:return: the equivalent resistance of the MCU in LPM
"""
v_lpm = energy_utils.str_to_float(lpm_data['V'])
i_lpm = energy_utils.str_to_float(lpm_data['I'])
return energy_utils.equivalent_resistance(v_lpm, i_lpm)
def _get_MCU_LPM_V_min(self, lpm_data):
"""
:param lpm_data: LPM datasheet information
:return: the minimum voltage required in LPM
"""
return energy_utils.str_to_float(lpm_data['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
"""
return energy_utils.str_to_float(lpm_data['t_wakeup'])
def _calculate_ADC_equivalent_r(self, adc_data):
"""
:param adc_data: ADC datasheet information
:return: the equivalent resistance of the ADC
"""
nominal_v = self._get_ADC_nominal_v(adc_data)
current_draw = self._get_ADC_I(adc_data)
return energy_utils.equivalent_resistance(nominal_v, current_draw)
def _get_ADC_nominal_v(self, adc_data):
"""
:param adc_data: ADC datasheet information
:return: the nominal voltage at which the current draw of the ADC was measured
"""
return energy_utils.str_to_float(adc_data['V'])
def _get_ADC_min_v(self, adc_data):
"""
:param adc_data: ADC datasheet information
:return: the minimum operating voltage of the ADC
"""
return energy_utils.str_to_float(adc_data['V_min'])
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
"""
# Time to start the ADC
t_off_on = energy_utils.str_to_float(adc_data['T_off_on'])
# Time to sample data
t_sample = energy_utils.str_to_float(adc_data['T_sampling'])
n_off_on = math.ceil(t_off_on * frequency)
n_sample = math.ceil(t_sample * frequency)
return int(n_off_on + n_sample)
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
"""
# Instructions to init the ADC
n_init = energy_utils.str_to_int(adc_data['N_init'])
# Instructions to transfer data
n_transfer = energy_utils.str_to_int(adc_data['N_transfer_ops'])
# Instructions to turn off the ADC
n_off = energy_utils.str_to_int(adc_data['N_off'])
return {"on": n_init, "transfer": n_transfer, "off": n_off}
def _get_ADC_I(self, adc_data):
"""
:param adc_data: ADC datasheet information
:return: the current draw of the ADC
"""
if self.ADC_I_TO_CONSIDER == 'min':
return energy_utils.str_to_float(adc_data['I_min'])
elif self.ADC_I_TO_CONSIDER == 'max':
return energy_utils.str_to_float(adc_data['I_max'])
elif self.ADC_I_TO_CONSIDER == 'avg':
I_min = energy_utils.str_to_float(adc_data['I_min'])
I_max = energy_utils.str_to_float(adc_data['I_max'])
return (I_min + I_max) / 2.0