233 lines
9.1 KiB
Python
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
|