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