import json import math import time from ScEpTIC.AST.transformations.base.instructions.enter_mcu_lpm import EnterMCULPM from ScEpTIC.analysis.utils.saved_state_calculator import SavedStateCalculator from ScEpTIC.analysis.utils.voltage_thresholds_identifier import VoltageThresholdsIdentifier from ScEpTIC.emulator.energy.options import OpModeName from ScEpTIC.exceptions import ConfigurationException from ScEpTIC.analysis.base_analysis import ScEpTICAnalysis from ScEpTIC.AST.elements.instructions.memory_operations import StoreOperation, LoadOperation from ScEpTIC.AST.elements.instructions.other_operations import CallOperation from ScEpTIC.AST.elements.instructions.termination_instructions import ReturnOperation from ScEpTIC.AST.transformations.base.instructions.simulate_clock_cycles import SimulateClockCycles from ScEpTIC.analysis.options import AnalysisResultFormat from ScEpTIC.emulator.energy.mcu import MCUClockCycleAction, MCUPowerState from ScEpTIC.tools import fancy_dict_to_str, fancy_list_to_str from ScEpTIC.emulator.energy import energy_utils from ScEpTIC.AST.transformations.base.instructions.printf import Printf class EnergyAnalysis(ScEpTICAnalysis): """ Energy analysis """ def __init__(self, vm): super().__init__(vm) self._check_unsupported_conf() # Configuration params self.system_model = self.vm.state.config.analysis.energy.system_model self.power_failures_during_state_save = not self.vm.state.config.analysis.energy.ignore_power_failures_during_state_save self.max_simulation_time = self.vm.state.config.analysis.energy.max_simulation_time # State-save params self.use_checkpoints = self.vm.state_retention.use_checkpoints self.static_placement = self.vm.state_retention.static_placement self.probe_energy_buffer = self.vm.state_retention.probe_energy_buffer self.probe_with_adc = self.vm.state_retention.probe_with_adc # Hibernate after save self.hibernation_enabled = self.vm.state_retention.hibernate_after_save # State save calculator self.state_calc = SavedStateCalculator(self.vm) self.stdout_enabled = Printf.stdout_enabled # Threshold voltage for saving the state if self.use_checkpoints and (not self.static_placement or self.probe_energy_buffer): # Identify optimal voltage if self.vm.state_retention.calculate_v_save_state: v_identifier = VoltageThresholdsIdentifier(self.vm, self.system_model) v_identifier.identify() self.v_save_state = v_identifier.v_save_state self.v_resume = v_identifier.v_resume # Fixed in configuration else: self.v_save_state = self.vm.state_retention.v_save_state self.v_resume = self.vm.state_retention.v_resume # Probe with ADC if self.probe_energy_buffer and self.probe_with_adc: if not self.system_model.mcu.has_adc: raise Exception("MCU does not have an ADC (probe set to use ADC)") # ADC min voltage may be higher than optimal v_state_save # -> ADC must be on to probe energy buffer self.v_save_state = max(self.v_save_state, self.system_model.mcu.adc_min_v) self.v_resume = max(self.v_resume, self.system_model.mcu.adc_min_v) custom_signals = [('probe', 'Probe Energy Buffer'), ('save', 'Save State'), ('restore', 'Restore State')] self.system_model.init_custom_signals(custom_signals) if self.vm.state.config.analysis.energy.calculate_cache_only: import sys sys.exit(0) def _check_unsupported_conf(self): """ Cheks for unsupported configurations """ if self.vm.state_retention.restore_non_volatile_gst: raise Exception(f"Unsupported configuration: restore non-volatile gst") if self.vm.state_retention.restore_stack and self.vm.state.memory.memory_positions['stack'] == 'non_volatile': raise Exception(f"Unsupported configuration: restore non-volatile stack") if self.vm.state_retention.restore_heap and self.vm.state.memory.memory_positions['heap'] == 'non_volatile': raise Exception(f"Unsupported configuration: restore non-volatile heap") def mmu_log_energy_failure(self): for mmu in self.vm.state.memory.mmus.values(): mmu.log_data('ENERGY_FAILURE', None, None, None, None, None) def get_result(self, result_format): """ Returns the analysis result :param result_format: the result format """ names, result = self.system_model.get_stats() names['global_clock'] = 'Global CLock (ScEpTIC VM)' result['global_clock'] = self.vm.state.global_clock signals = self.system_model.get_collected_signals() results = {} if result_format == AnalysisResultFormat.JSON: results['analysis'] = json.dumps(result) if len(signals) > 1: results['signals'] = json.dumps(signals) elif result_format == AnalysisResultFormat.TEXT: retstr = '[General Stats]\n' for name, val in result.items(): if not isinstance(val, dict) and not isinstance(val, list): retstr += f' {names[name]}: {val}\n' retstr += '\n' for name, val in result.items(): if isinstance(val, dict): retstr += f'[{names[name]}]\n' retstr += fancy_dict_to_str(val) retstr += '\n\n' elif isinstance(val, list): retstr += f'[{names[name]}]\n' retstr += fancy_list_to_str(val) retstr += '\n\n' results['analysis'] = retstr if len(signals) > 1: retstr = '' for data in signals: data = ', '.join([str(x) for x in data]) retstr += f"{data}\n" results['signals'] = retstr else: raise ConfigurationException(f"Invalid result format {result_format}") return results def run(self): """ Runs the energy analysis simulation """ state_save_function_name = self.vm.state_retention.routine_names['save'] op_mode_name = OpModeName.PROGRAM_EXECUTION adc_op_name = OpModeName.PROBE_ENERGY_BUFFER self.vm.reset() self.system_model.reset() self.system_model.energy_buffer.set_voltage(self.system_model.mcu.v_on) self.system_model.mcu.set_mcu_state(MCUPowerState.ON) # init MCU and custom devices; set clock to custom devices clock cycles self.vm.state.global_clock += self.system_model.init() save_id = 0 restore_id = 0 # Flag to always save the state (V < v_adc_min) probe_adc_low_voltage_flag = False self.system_model.set_custom_signal('probe', False) self.system_model.set_custom_signal('save', False) self.system_model.set_custom_signal('restore', False) print("Starting simulation") t = time.time() last_lpm_print_tick = 0 print_lpm_out = False self.vm.set_termination_reason("SIMULATION_COMPLETED") while not self.vm.state.program_end_reached: # current instruction current_instruction = self.vm.state.current_instruction #print(current_instruction) #print(self.vm.state.register_file.pc.pc_tree()) op_ticks = current_instruction.tick_count sysmodel_ticks = 0 power_failure_occurred = False save_state = False # Function name function_name = None if isinstance(current_instruction, CallOperation): function_name = current_instruction.resolve_function_name() # Execute instruction self.vm.state.run_step() ticks_elapsed = self.system_model.elapsed_ticks['total']['total'] if ticks_elapsed % 1500000 == 0: elapsed = time.time() - t print(f"Still running {self.vm.state.register_file.pc} - TICKS={energy_utils.float_to_str(ticks_elapsed, omit_dot_zero=True)} - elapsed {math.floor(elapsed)}s ({math.floor(ticks_elapsed/elapsed)} op/s) - Cap={self.system_model.energy_buffer.get_voltage()}V; Harvester={self.system_model.energy_harvester.get_voltage()}V; Source={self.system_model.energy_source.get_voltage()}V; T={round(self.system_model.energy_source.elapsed_time, 3)}s") if self.max_simulation_time > 0: if self.system_model.get_simulation_time() >= self.max_simulation_time: self.vm.set_termination_reason("MAX_SIMULATION_TIME_REACHED") print(f"MAX SIMULATION TIME REACHED") break # Simulate cycles if isinstance(current_instruction, SimulateClockCycles): op_type = current_instruction.get_instruction_type() sysmodel_ticks += self.system_model.run_step(op_type, OpModeName.SIMULATE_CYCLES) ### COSTS # Operations not corresponding to a machine-code instruction has op_tick = 0 and need not to execute an # energy-simulation cycle # e.g., a "useless branch" (targets next instruction only; used by llvm to split the program into basic blocks) elif op_ticks == 0: sysmodel_ticks += 0 # Load / Store elif isinstance(current_instruction, LoadOperation) or isinstance(current_instruction, StoreOperation): op_type, additional_op_mode_name = self.state_calc.get_memory_access_mcu_action(current_instruction) sysmodel_ticks += self.system_model.run_step(op_type, op_mode_name, additional_op_mode_name=additional_op_mode_name) # Call / Return elif isinstance(current_instruction, CallOperation) or isinstance(current_instruction, ReturnOperation): mem_op_type, additional_op_mode_name = self.state_calc.get_call_ret_mcu_action(current_instruction) memory_ticks = current_instruction.memory_tick_count normal_ticks = op_ticks - memory_ticks for _ in range(memory_ticks): sysmodel_ticks += self.system_model.run_step(mem_op_type, op_mode_name, additional_op_mode_name=additional_op_mode_name) for _ in range(normal_ticks): sysmodel_ticks += self.system_model.run_step(MCUClockCycleAction.NO_MEMORY_ACCESS, op_mode_name) # All the other instructions else: sysmodel_ticks += self.system_model.run_step(MCUClockCycleAction.NO_MEMORY_ACCESS, op_mode_name) ### CUSTOM ACTIONS # Enter LPM if isinstance(current_instruction, EnterMCULPM): lpm_time = current_instruction.get_lpm_time() # - If power failure occurs -> stop (power failure simulated after the else branch) # - If voltage raises above v_resume -> resume from LPM sysmodel_ticks += self.system_model.run_step(MCUClockCycleAction.LPM_ENTER, OpModeName.LPM_ENTER) if self.stdout_enabled: ticks_elapsed = self.system_model.elapsed_ticks['total']['total'] if last_lpm_print_tick < (ticks_elapsed - 50000): print(f"Entering LPM at {self.system_model.energy_buffer.get_voltage()}V") print_lpm_out = True last_lpm_print_tick = ticks_elapsed # Start timekeeper if self.system_model.timekeeper is not None: self.system_model.timekeeper.start_tracking() lpm_start_time = self.system_model.get_simulation_time() lpm_end_time = lpm_start_time + lpm_time lpm_wakeup_using_time = lpm_time > 0 i = 0 while True: if i % 50000 == 0: if self.stdout_enabled: print(f"LPM Recharging - Cap={self.system_model.energy_buffer.get_voltage()}V; Harvester={self.system_model.energy_harvester.get_voltage()}V; Source={self.system_model.energy_source.get_voltage()}V; T={round(self.system_model.energy_source.elapsed_time, 3)}s") if self.max_simulation_time > 0: if self.system_model.get_simulation_time() >= self.max_simulation_time: self.vm.set_termination_reason("MAX_SIMULATION_TIME_REACHED", "LPM_RECHARGING") print(f"MAX SIMULATION TIME REACHED DURING LPM RECHARGING") break i += 1 sysmodel_ticks += self.system_model.run_step(MCUClockCycleAction.LPM_NOP, OpModeName.LPM) # Energy failure (do not stop timekeeper) if self.system_model.power_failure_occurring(): power_failure_occurred = True break # Resume when: # - Time lpm_time elapsed (if lpm_time > 0) # - v_on reached (if lpm_time = 0) if (lpm_wakeup_using_time and lpm_end_time < self.system_model.get_simulation_time()) or (not lpm_wakeup_using_time and self.system_model.energy_buffer.get_voltage() >= self.system_model.mcu.v_on): # Exit from LPM (automatically wait for t_wakeup + re-initialize custom devices) sysmodel_ticks += self.system_model.run_step(MCUClockCycleAction.LPM_EXIT, OpModeName.LPM_EXIT) if print_lpm_out: print(f"LPM out {self.system_model.harvested_energy['total']['total']}") print_lpm_out = False # Stop timekeeper, as we are exiting LPM if self.system_model.timekeeper is not None: self.system_model.timekeeper.stop_tracking() # Exit from while loop and continue execution break if print_lpm_out: print(f"Exiting LPM at {self.system_model.energy_buffer.get_voltage()}V") print_lpm_out = False if self.use_checkpoints: # Save state if static placement and a call to a state-save operation is being executed # Note: the cost of executing the function call is already considered if self.static_placement and function_name == state_save_function_name: save_state = True # set automatic check for power failures # if self.power_failures_during_state_save is set to False, the analysis considers the state-saving # operation to always complete, even if in a real-world scenario the device would turn off. # Note that the additional energy is considered. self.system_model.set_power_failures_automatic_check(self.power_failures_during_state_save) # Probe if self.probe_energy_buffer: self.system_model.set_custom_signal('probe', True) sysmodel_ticks += self._simulate_save_restore_call_ret(0, OpModeName.PROBE_ENERGY_BUFFER) energy_buffer_voltage = self.system_model.energy_buffer.get_voltage() # Assumption: when voltage < minimum operating voltage for the ADC, the MCU always save the state if self.probe_with_adc and energy_buffer_voltage <= self.system_model.mcu.adc_min_v: # Set flag if not probe_adc_low_voltage_flag: sysmodel_ticks += self.system_model.run_step(MCUClockCycleAction.VOLATILE_MEMORY_ACCESS, adc_op_name) probe_adc_low_voltage_flag = True # Read flag sysmodel_ticks += self.system_model.run_step(MCUClockCycleAction.VOLATILE_MEMORY_ACCESS, adc_op_name) # Flag not set if not probe_adc_low_voltage_flag: # OR operation on flag (flag is false) sysmodel_ticks += self.system_model.run_step(MCUClockCycleAction.NO_MEMORY_ACCESS, adc_op_name) # Probe the energy buffer sysmodel_ticks += self.system_model.state_retention.execute_probe_energy_buffer() # Check if state must be saved save_state = self.system_model.energy_buffer.get_voltage() <= self.v_save_state if not save_state: # unset automatic check for power failures self.system_model.set_power_failures_automatic_check(False) self.system_model.set_custom_signal('probe', False) # print(f"Probed{'-forced' if probe_adc_low_voltage_flag else ''}: {save_state} - {self.system_model.energy_buffer.get_voltage()}V vs {self.v_save_state}") #else: # print(f"Not Probed: {save_state} - {self.system_model.energy_buffer.get_voltage()}V vs {self.v_save_state}") # Save state if interrupt-based state-saving and energy buffer voltage lower than the v_save_state threshold if not self.static_placement and self.system_model.energy_buffer.get_voltage() <= self.v_save_state: save_state = True # set automatic check for power failures # if self.power_failures_during_state_save is set to False, the analysis considers the state-saving # operation to always complete, even if in a real-world scenario the device would turn off. # Note that the additional energy is considered. self.system_model.set_power_failures_automatic_check(self.power_failures_during_state_save) # Save the state if save_state: self.system_model.set_custom_signal('save', True) #print(f"Going to save the state at {self.system_model.energy_buffer.get_voltage()}") # get state size save_only_pc, n_registers = self.state_calc.get_saved_registers(current_instruction) n_memory_cells = self.state_calc.get_saved_memory_cells(current_instruction) state_save_ticks = self.system_model.state_retention.execute_save_state(save_only_pc, n_registers, n_memory_cells) # simulate call/ret + update global clock self._simulate_save_restore_call_ret(state_save_ticks, OpModeName.STATE_SAVE) # unset automatic check for power failures self.system_model.set_power_failures_automatic_check(False) #print(f"Reached {self.system_model.energy_buffer.get_voltage()}") if not self.system_model.power_failure_occurred(): self.system_model.set_custom_signal('save', False) save_id += 1 t = time.time() self.vm.state_retention.execute_state_save() # Account for physical memory energy consumption if self.vm.state_retention.physical_mmu is not None: self.system_model.run_step(MCUClockCycleAction.PHYSICAL_MEMORY_ACCESS, OpModeName.STATE_SAVE, additional_op_mode_name='STATE_RETENTION_MEMORY') #print(f"State-save {self.system_model.elapsed_ticks['total']['total']} - saved in {time.time() - t}s") if self.vm.state_retention.hibernate_after_save: sysmodel_ticks += self.system_model.run_step(MCUClockCycleAction.LPM_ENTER, OpModeName.LPM_ENTER) if self.stdout_enabled: print(f"Entering LPM at {self.system_model.energy_buffer.get_voltage()}V") # Start timekeeper if self.system_model.timekeeper is not None: self.system_model.timekeeper.start_tracking() # Simulate hibernation ticks # - If power failure occurs -> stop (power failure simulated after the else branch) # - If voltage raises above v_resume -> resume from LPM i = 0 while True: if i % 50000 == 0: if self.stdout_enabled: print(f"LPM Recharging - Cap={self.system_model.energy_buffer.get_voltage()}V; Harvester={self.system_model.energy_harvester.get_voltage()}V; Source={self.system_model.energy_source.get_voltage()}V; T={round(self.system_model.energy_source.elapsed_time, 3)}s") if self.max_simulation_time > 0: if self.system_model.get_simulation_time() >= self.max_simulation_time: self.vm.set_termination_reason("MAX_SIMULATION_TIME_REACHED", "LPM_RECHARGING") print(f"MAX SIMULATION TIME REACHED DURING LPM RECHARGING") break i += 1 sysmodel_ticks += self.system_model.run_step(MCUClockCycleAction.LPM_NOP, OpModeName.LPM) if self.system_model.power_failure_occurring(): power_failure_occurred = True break # Check if voltage >= V_resume if self.system_model.energy_buffer.get_voltage() >= self.v_resume: # Exit from LPM (automatically wait for v_wakeup + re-initialize custom devices) sysmodel_ticks += self.system_model.run_step(MCUClockCycleAction.LPM_EXIT, OpModeName.LPM_EXIT) if self.stdout_enabled: print("LPM out") # Stop timekeeper, as we are exiting from LPM if self.system_model.timekeeper is not None: self.system_model.timekeeper.stop_tracking() # Reset ADC flag if probe set if self.probe_energy_buffer and self.probe_with_adc: probe_adc_low_voltage_flag = False sysmodel_ticks += self.system_model.run_step(MCUClockCycleAction.VOLATILE_MEMORY_ACCESS, adc_op_name) # Exit from while loop and continue execution break if self.stdout_enabled: print(f"Exiting LPM at {self.system_model.energy_buffer.get_voltage()}V") self.system_model.set_custom_signal('save', False) # additional ticks due to wait cycles / state-save / etc additional_ticks = max(0, sysmodel_ticks - op_ticks) self.vm.state.global_clock += additional_ticks # Simulate a power failure if power_failure_occurred or self.system_model.power_failure_occurring(): if self.stdout_enabled: print(f"Energy failure: {self.system_model.elapsed_ticks['total']['total']} {self.system_model.energy_buffer.get_voltage()}V") self.system_model.record_power_failure() self.mmu_log_energy_failure() # Start timekeeper (automatically ignored if previously started) if self.system_model.timekeeper is not None: self.system_model.timekeeper.start_tracking() # reset volatile state self.vm.state.reset() # On power failure -> full recharge if self.system_model.energy_source.power_off_full_recharge: v_target = self.system_model.mcu.v_on v_supply = self.system_model.energy_source.full_recharge_voltage self.system_model.execute_full_recharge(MCUClockCycleAction.NOP_OFF_RECHARGE, OpModeName.ENERGY_BUFFER_RECHARGE, v_target, v_supply) # On power failure -> simulate energy source recharge else: i = 0 while self.system_model.energy_buffer.get_voltage() < self.system_model.mcu.v_on: if i % 1000000 == 0: if self.stdout_enabled: print(f"Recharging to {self.system_model.mcu.v_on}V - Cap={self.system_model.energy_buffer.get_voltage()}V; Harvester={self.system_model.energy_harvester.get_voltage()}V; Source={self.system_model.energy_source.get_voltage()}V; T={round(self.system_model.energy_source.elapsed_time, 3)}s") if self.max_simulation_time > 0: if self.system_model.get_simulation_time() >= self.max_simulation_time: self.vm.set_termination_reason("MAX_SIMULATION_TIME_REACHED", "POWER_OFF_RECHARGE") self.system_model.run_step(MCUClockCycleAction.NOP_OFF_RECHARGE, OpModeName.ENERGY_BUFFER_RECHARGE) if self.stdout_enabled: print(f"Recharged to {self.system_model.energy_buffer.get_voltage()}V") # Stop timekeeper tracking if self.system_model.timekeeper is not None: self.system_model.timekeeper.stop_tracking() # restore saved state self.vm.state_retention.execute_state_restore() self.system_model.set_custom_signal('restore', True) # Detect non-terminating path bugs if self.use_checkpoints and restore_id == save_id: # TODO: add a way to check non-termination for task-based systemds raise Exception("Error: unable to reach next state-saving operation (non-terminating path bug)!") restore_id = save_id # re-initialize components (turn on MCU / reset custom devices) restore_ticks = self.system_model.init() # run restore # Note: here we do not check for power failures, as a power failure happening during restore triggers # the non-terminating path bug check during the subsequent iteration of the simulation. # In fact, when a power failure occurs during restore, the next iteration executes the instruction and # re-enters this portion of code. As restore_id == save_id, the simulation stops. restore_ticks += self.system_model.state_retention.execute_restore_state() # Account for physical memory energy consumption if self.vm.state_retention.physical_mmu is not None: self.system_model.run_step(MCUClockCycleAction.PHYSICAL_MEMORY_ACCESS, OpModeName.STATE_RESTORE, additional_op_mode_name='STATE_RETENTION_MEMORY') # simulate call/ret + update global clock n_ticks = self._simulate_save_restore_call_ret(restore_ticks, OpModeName.STATE_RESTORE) # restore ADC probe flag probe_adc_low_voltage_flag = False self.system_model.set_custom_signal('restore', False) if self.stdout_enabled: print(f"Restore: {self.system_model.elapsed_ticks['total']['total']} {self.system_model.energy_buffer.get_voltage()}V") self.system_model._collect_signals(0.0) def _simulate_save_restore_call_ret(self, n_ticks, op_mode_name): """ Simulates the execution of a function call for state-saving and state-restoring operations. :param n_ticks: number of executed clock cycles to save/restore the state """ # state-saving/state-restoring call/ret execute on volatile stack ticks = CallOperation.tick_count + ReturnOperation.tick_count memory_ticks = CallOperation.memory_tick_count + ReturnOperation.memory_tick_count normal_ticks = ticks - memory_ticks for _ in range(memory_ticks): n_ticks += self.system_model.run_step(MCUClockCycleAction.VOLATILE_MEMORY_ACCESS, op_mode_name) for _ in range(normal_ticks): n_ticks += self.system_model.run_step(MCUClockCycleAction.NO_MEMORY_ACCESS, op_mode_name) self.vm.state.global_clock += n_ticks return n_ticks