912 lines
34 KiB
Python
912 lines
34 KiB
Python
import gc
|
|
from collections import defaultdict
|
|
|
|
from ScEpTIC.emulator.custom_devices import CustomDevice
|
|
from ScEpTIC.emulator.energy import energy_utils
|
|
from ScEpTIC.emulator.energy.buffer import EnergyBufferModel
|
|
from ScEpTIC.emulator.energy.energy_source import EnergySourceModel
|
|
from ScEpTIC.emulator.energy.energy_source.synthetic_energy_source import SyntheticEnergySource
|
|
from ScEpTIC.emulator.energy.energy_harvester import EnergyHarvesterModel
|
|
from ScEpTIC.emulator.energy.energy_harvester.generic import GenericEnergyHarvester
|
|
from ScEpTIC.emulator.energy.memory.physical_memory_energy_model import PhysicalMemoryEnergyModel
|
|
from ScEpTIC.emulator.energy.mcu import MCUEnergyModel, MCUPowerState, MCUClockCycleAction
|
|
from ScEpTIC.emulator.energy.mcu_peripheral import MCUPeripheral
|
|
from ScEpTIC.emulator.energy.options import ComponentVoltageSource, PowerOffCondition
|
|
from ScEpTIC.emulator.energy.power_state_event import PowerStateEvent
|
|
from ScEpTIC.emulator.energy.state_retention import StateRetentionEnergyModel
|
|
from ScEpTIC.emulator.energy.timekeeper import TimekeeperModel
|
|
from ScEpTIC.emulator.energy.voltage_regulator import NoRegulator, VoltageRegulatorModel
|
|
from ScEpTIC.exceptions import ConfigurationException
|
|
|
|
|
|
class SystemEnergyModel:
|
|
"""
|
|
Class to model the system energy consumption
|
|
"""
|
|
|
|
def __init__(self):
|
|
# index 0 -> operation mode; index 1 -> MCU_CLOCK_ACTION
|
|
# {'total': {'total': 0.0, MCU_ACTION_1: 0.0,} , 'op_mode_1': {...}}
|
|
self.elapsed_time = defaultdict(lambda: defaultdict(float))
|
|
self.elapsed_ticks = defaultdict(lambda: defaultdict(int))
|
|
|
|
# index 0 -> component; index 1 -> operation mode; index 2 -> MCU_CLOCK_ACTION
|
|
# {'total': {'total': {'total': 0.0, MCU_ACTION1: 0.0,}, 'component1': {...}}
|
|
self.used_energy = defaultdict(lambda: defaultdict(lambda: defaultdict(float)))
|
|
self.vreg_energy_loss = defaultdict(lambda: defaultdict(lambda: defaultdict(float)))
|
|
|
|
# index 0 -> operation mode; index 1-> MCUPowerState
|
|
self.harvested_energy = defaultdict(lambda: defaultdict(float))
|
|
|
|
# standard components
|
|
self.energy_buffer = None
|
|
self.voltage_regulator = None
|
|
|
|
self.voltage_regulator_buffer = None
|
|
self.voltage_regulator_mcu = None
|
|
|
|
self.mcu = None
|
|
self.timekeeper = None
|
|
|
|
# additional components of the system
|
|
self.additional_components = {
|
|
ComponentVoltageSource.ENERGY_BUFFER: {},
|
|
ComponentVoltageSource.VOLTAGE_REGULATOR: {}
|
|
}
|
|
|
|
# custom devices - elements with their own logic and setup (e.g., the model of the D2VFS board)
|
|
self.custom_devices = []
|
|
|
|
# mcu_peripheral
|
|
self.mcu_peripherals = []
|
|
self.physical_memories = []
|
|
|
|
# state retention model
|
|
self.state_retention = None
|
|
|
|
# Power off condition
|
|
self.power_off_condition = PowerOffCondition.POWER_STATE_EVENT
|
|
self.power_off_events = []
|
|
self.power_off_voltage_threshold = 0.0
|
|
|
|
# Power failure
|
|
self.automatic_check_for_power_failures = False
|
|
self.cached_power_failure = False
|
|
self.cached_energy = 0.0
|
|
self.cached_harvested_energy = 0.0
|
|
self.power_failures = 0
|
|
|
|
# Instant values
|
|
self.instant_V = 0.0
|
|
self.instant_E = 0.0
|
|
self.instant_EH = 0.0
|
|
|
|
# Energy source model
|
|
self.energy_source = None
|
|
self.energy_harvester = None
|
|
|
|
# Collect signals
|
|
self.collect_signals = False
|
|
self.signals = []
|
|
self.custom_signals_keys = []
|
|
self.custom_signals_strings = []
|
|
self.custom_signals = {}
|
|
self._init_signals()
|
|
self.total_time = 0.0
|
|
|
|
self.lpm_precise_ticks = False
|
|
|
|
def enable_lpm_precise_ticks(self):
|
|
"""
|
|
Enables LPM simulation of t = mcu tick
|
|
"""
|
|
self.lpm_precise_ticks = True
|
|
|
|
def disable_lpm_precise_ticks(self):
|
|
"""
|
|
Revers LPM simulation of t = source_sample_time
|
|
"""
|
|
self.lpm_precise_ticks = False
|
|
|
|
def get_simulation_time(self):
|
|
"""
|
|
Returns total elapsed time in s
|
|
"""
|
|
return self.elapsed_time['total']['total']
|
|
|
|
def reset_simulation_time(self):
|
|
self.elapsed_time = defaultdict(lambda: defaultdict(float))
|
|
|
|
def get_elapsed_ticks(self):
|
|
"""
|
|
Returns total elapsed clock ticks
|
|
"""
|
|
return self.elapsed_ticks['total']['total']
|
|
|
|
|
|
def get_used_energy(self):
|
|
"""
|
|
Returns total used energy
|
|
"""
|
|
return self.used_energy['total']['total']['total']
|
|
|
|
def reset_used_energy(self):
|
|
"""
|
|
Resets the total energy usage
|
|
"""
|
|
self.used_energy['total']['total']['total'] = 0.0
|
|
|
|
|
|
def disable_stats(self):
|
|
"""
|
|
Disables statistics collection
|
|
"""
|
|
self.__update_stats, self.__disabled_update_stats = self.__disabled_update_stats, self.__update_stats
|
|
|
|
|
|
def init_custom_signals(self, data):
|
|
"""
|
|
Set the strings and keys of custom signals collected during the simulation
|
|
:param data: a list of tuples (key, string)
|
|
"""
|
|
if not isinstance(data, list):
|
|
raise ConfigurationException(f"set_custom_signal_strings() requires a list of tuples.")
|
|
|
|
del self.custom_signals_strings
|
|
del self.custom_signals_keys
|
|
self.custom_signals_strings = []
|
|
self.custom_signals_keys = []
|
|
|
|
for key, name in data:
|
|
self.custom_signals_keys.append(key)
|
|
self.custom_signals_strings.append(name)
|
|
self.custom_signals[key] = None
|
|
|
|
self._init_signals()
|
|
|
|
|
|
def set_custom_signal(self, name, val):
|
|
"""
|
|
Set the custom signals collected during the simulation
|
|
:param name: the signal name
|
|
:param val: the signal value
|
|
"""
|
|
self.custom_signals[name] = val
|
|
|
|
|
|
def get_stats(self):
|
|
"""
|
|
:return: the simulation statistics and the name of each label
|
|
"""
|
|
names = {
|
|
'time': 'Elapsed Time',
|
|
'ticks': 'Simulated Ticks',
|
|
'energy': 'Energy Consumption',
|
|
'harvested_energy': 'Harvested Energy',
|
|
'voltage_regulator_loss': 'Energy - Voltage Regulator Loss',
|
|
'power_failures': 'Simulated Power Failures',
|
|
'n_state_saves': 'Number of State Saves',
|
|
'n_restores': 'Number of State Restores',
|
|
'state_max_regs': 'Max Registers Saved',
|
|
'state_max_cells': 'Max Memory Cells Saved',
|
|
}
|
|
|
|
stats = {
|
|
'time': self.elapsed_time,
|
|
'ticks': self.elapsed_ticks,
|
|
'energy': self.used_energy,
|
|
'harvested_energy': self.harvested_energy,
|
|
'voltage_regulator_loss': self.vreg_energy_loss,
|
|
'power_failures': self.power_failures,
|
|
'n_state_saves': self.state_retention.n_state_save,
|
|
'n_restores': self.state_retention.n_state_restore,
|
|
'state_max_regs': self.state_retention.max_registers,
|
|
'state_max_cells': self.state_retention.max_memory_cells,
|
|
}
|
|
|
|
return names, stats
|
|
|
|
|
|
def get_collected_signals(self):
|
|
"""
|
|
:return: the collected signals
|
|
"""
|
|
return self.signals
|
|
|
|
|
|
def set_power_off_condition(self, power_off_condition):
|
|
"""
|
|
Sets the power off condition for the MCU
|
|
:param power_off_condition: a PowerOffCondition
|
|
"""
|
|
if not isinstance(power_off_condition, PowerOffCondition):
|
|
raise ConfigurationException(f"Wrong power off condition: {power_off_condition.__class__.__name__} is not a PowerOffCondition")
|
|
|
|
self.power_off_condition = power_off_condition
|
|
|
|
|
|
def set_power_off_voltage(self, voltage):
|
|
"""
|
|
Sets the power off voltage (considered only when power off condition = PowerOffCondition.VOLTAGE_THRESHOLD)
|
|
:param voltage: the power off voltage
|
|
"""
|
|
self.power_off_voltage_threshold = float(voltage)
|
|
|
|
|
|
def set_power_failures_automatic_check(self, val):
|
|
"""
|
|
Sets how power failures are controlled
|
|
:param val: True/False
|
|
"""
|
|
self.automatic_check_for_power_failures = val
|
|
|
|
|
|
def add_power_off_event(self, event):
|
|
"""
|
|
Adds a PowerStateEvent to the list of events that will cause the system to power off
|
|
:param event: a PowerStateEvent
|
|
"""
|
|
if not isinstance(event, PowerStateEvent):
|
|
raise ConfigurationException(f"add_power_off_event() requires a PowerStateEvent, {event.__class__.__name__} provided.")
|
|
|
|
self.power_off_events.append(event)
|
|
|
|
|
|
def attach_energy_buffer(self, energy_buffer):
|
|
"""
|
|
Attaches an energy buffer to the system model
|
|
:param energy_buffer: the energy buffer (must inherit from EnergyBufferModel)
|
|
"""
|
|
if self.energy_buffer is not None:
|
|
raise ConfigurationException("Energy buffer already atteched!")
|
|
|
|
if not isinstance(energy_buffer, EnergyBufferModel):
|
|
raise ConfigurationException("Wrong energy buffer model!")
|
|
|
|
self.energy_buffer = energy_buffer
|
|
|
|
if self.voltage_regulator is not None:
|
|
self.voltage_regulator.attach_voltage_source(self.energy_buffer)
|
|
|
|
for component in self.additional_components[ComponentVoltageSource.ENERGY_BUFFER]:
|
|
component.attach_voltage_source(self.energy_buffer)
|
|
|
|
|
|
def attach_energy_harvester(self, energy_harvester):
|
|
"""
|
|
Attaches an energy harvester to the system model
|
|
:param energy_harvester: the energy harvester
|
|
"""
|
|
if self.energy_harvester is not None:
|
|
raise ConfigurationException("Energy harvester already attached!")
|
|
|
|
if not isinstance(energy_harvester, EnergyHarvesterModel):
|
|
raise ConfigurationException("Wrong energy harvester model!")
|
|
|
|
self.energy_harvester = energy_harvester
|
|
|
|
if self.energy_source is not None:
|
|
self.energy_harvester.attach_energy_source(self.energy_source)
|
|
|
|
|
|
def attach_mcu(self, mcu):
|
|
"""
|
|
Attaches a MCU to the system model
|
|
:param mcu: the MCU (must inherit from MCUEnergyModel)
|
|
"""
|
|
if self.mcu is not None:
|
|
raise ConfigurationException("MCU already attached!")
|
|
|
|
if not isinstance(mcu, MCUEnergyModel):
|
|
raise ConfigurationException("Wrong mcu energy model!")
|
|
|
|
self.mcu = mcu
|
|
|
|
if self.voltage_regulator is not None:
|
|
self.mcu.attach_voltage_source(self.voltage_regulator)
|
|
|
|
for peripheral in self.mcu_peripherals:
|
|
peripheral.attach_mcu(self.mcu)
|
|
self.mcu.attach_peripheral(peripheral)
|
|
|
|
def attach_timekeeper(self, timekeeper):
|
|
"""
|
|
Attaches a timekeeper to the system model
|
|
:param timekeeper: the timekeeper
|
|
"""
|
|
|
|
if self.timekeeper is not None:
|
|
raise ConfigurationException("Timekeeper already attached!")
|
|
|
|
if not isinstance(timekeeper, TimekeeperModel):
|
|
raise ConfigurationException("Wrong timekeeper model!")
|
|
|
|
self.timekeeper = timekeeper
|
|
self.timekeeper.attach_system_model(self)
|
|
|
|
|
|
def attach_voltage_regulator(self, voltage_regulator):
|
|
"""
|
|
Attaches a voltage regulator to the system model
|
|
:param voltage_regulator: the voltage regulator (must inherit from VoltageRegulatorModel)
|
|
"""
|
|
if self.voltage_regulator is not None:
|
|
raise ConfigurationException("Voltage regulator already attached!")
|
|
|
|
if voltage_regulator is not None and not isinstance(voltage_regulator, VoltageRegulatorModel):
|
|
raise ConfigurationException("Wrong voltage regulator energy model!")
|
|
|
|
# Crate dummy regulator to preserve overall logic
|
|
if voltage_regulator is None:
|
|
voltage_regulator = NoRegulator()
|
|
|
|
self.voltage_regulator = voltage_regulator
|
|
|
|
if self.energy_buffer is not None:
|
|
self.voltage_regulator.attach_voltage_source(self.energy_buffer)
|
|
|
|
if self.mcu is not None:
|
|
self.mcu.attach_voltage_source(self.voltage_regulator)
|
|
|
|
for component in self.additional_components[ComponentVoltageSource.VOLTAGE_REGULATOR]:
|
|
component.attach_voltage_source(self.voltage_regulator)
|
|
|
|
|
|
def attach_component(self, name, component, target):
|
|
"""
|
|
Attaches an additional component to the energy buffer, the voltage regulator, or the MCU.
|
|
:param name: component name for stats
|
|
:param component: the component
|
|
:param target: ComponentVoltageSource enum specifying the voltage source for the device
|
|
"""
|
|
if not isinstance(target, ComponentVoltageSource):
|
|
raise ConfigurationException(f"Wrong target {target.__class__.__name__} for component {component.__class__.__name__}")
|
|
|
|
if isinstance(component, PhysicalMemoryEnergyModel):
|
|
self.physical_memories.append(component)
|
|
name = component.get_name()
|
|
|
|
self.additional_components[target][name] = component
|
|
|
|
if target == ComponentVoltageSource.ENERGY_BUFFER:
|
|
if self.energy_buffer is not None:
|
|
component.attach_voltage_source(self.energy_buffer)
|
|
|
|
elif target == ComponentVoltageSource.VOLTAGE_REGULATOR:
|
|
if self.voltage_regulator is not None:
|
|
component.attach_voltage_source(self.voltage_regulator)
|
|
|
|
else:
|
|
raise ConfigurationException(f"Wrong target '{target.__class__.__name__}' for component {name}")
|
|
|
|
if hasattr(component, 'attach_system_model') and callable(getattr(component, 'attach_system_model')):
|
|
component.attach_system_model(self)
|
|
|
|
# attach peripheral to MCU
|
|
if isinstance(component, MCUPeripheral):
|
|
self.mcu_peripherals.append(component)
|
|
|
|
if self.mcu is not None:
|
|
component.attach_mcu(self.mcu)
|
|
self.mcu.attach_peripheral(component)
|
|
|
|
|
|
def attach_custom_device(self, custom_device):
|
|
"""
|
|
Attaches a custom device to the system model and initializes it.
|
|
Note that a custom device consists in a model of an ad-hoc device, which may include multiple components and additional logic.
|
|
:param custom_device: the custom device (must inherit from CustomDevice)
|
|
"""
|
|
if not isinstance(custom_device, CustomDevice):
|
|
raise ConfigurationException(f"Wrong custom device model! {custom_device.__class__.__name__} is not a valid CustomDevice")
|
|
|
|
if custom_device in self.custom_devices:
|
|
raise ConfigurationException(f"{custom_device.__class__.__name__} already attached!")
|
|
|
|
self.custom_devices.append(custom_device)
|
|
custom_device.setup(self)
|
|
|
|
|
|
def attach_state_retention_model(self, state_retention):
|
|
"""
|
|
Attaches a state retention model to the system model
|
|
:param state_retention: the state retention model (must inherit from StateRetentionEnergyModel)
|
|
"""
|
|
if self.state_retention is not None:
|
|
raise ConfigurationException("State retention model already attached!")
|
|
|
|
if not isinstance(state_retention, StateRetentionEnergyModel):
|
|
raise ConfigurationException("Wrong state retention energy model!")
|
|
|
|
self.state_retention = state_retention
|
|
self.state_retention.attach_system_model(self)
|
|
|
|
|
|
def attach_energy_source_model(self, energy_source):
|
|
"""
|
|
Attaches an energy source model to the system model
|
|
:param energy_source: the energy source model
|
|
"""
|
|
if self.energy_source is not None:
|
|
raise ConfigurationException("Energy source model already attached!")
|
|
|
|
if energy_source is None:
|
|
energy_source = SyntheticEnergySource(5.0)
|
|
|
|
if not isinstance(energy_source, EnergySourceModel):
|
|
raise ConfigurationException("Wrong energy source model!")
|
|
|
|
self.energy_source = energy_source
|
|
|
|
if self.energy_harvester is not None:
|
|
self.energy_harvester.attach_energy_source(self.energy_source)
|
|
|
|
|
|
def get_power_state_events(self):
|
|
"""
|
|
:return: a list of the power state events occurred
|
|
"""
|
|
events = self.mcu.get_power_state_events()
|
|
|
|
for peripheral in self.mcu_peripherals:
|
|
events.extend(peripheral.get_power_state_events())
|
|
|
|
return events
|
|
|
|
|
|
def _init_signals(self, run_gc_collect=True):
|
|
"""
|
|
Inserts signals names onto the signal collection list
|
|
:param run_gc_collect: runs the garbage collector after deleting the saved signals
|
|
"""
|
|
|
|
if self.collect_signals:
|
|
del self.signals
|
|
|
|
if run_gc_collect:
|
|
gc.collect()
|
|
|
|
self.signals = []
|
|
|
|
data = [
|
|
'Time',
|
|
'Energy Source Voltage',
|
|
'Energy Buffer Voltage',
|
|
'Voltage Regulator Output Voltage',
|
|
'Energy Buffer Available Energy',
|
|
'Energy Harvested',
|
|
'Energy Consumed',
|
|
'MCU Frequency',
|
|
'MCU Power State',
|
|
'ADC Power State',
|
|
]
|
|
|
|
for device in self.custom_devices:
|
|
for signal_string in device.get_signals_strings():
|
|
data.append(signal_string)
|
|
|
|
for custom_signal in self.custom_signals_strings:
|
|
data.append(custom_signal)
|
|
|
|
self.signals.append(data)
|
|
|
|
|
|
def _collect_signals(self, t):
|
|
"""
|
|
Collect the voltage trace
|
|
:param t: elapsed time
|
|
"""
|
|
if self.collect_signals:
|
|
# Simulation signals
|
|
data = [
|
|
self.total_time,
|
|
self.energy_source.get_voltage(),
|
|
self.energy_buffer.get_voltage(),
|
|
self.voltage_regulator.get_voltage(),
|
|
self.energy_buffer.get_energy(),
|
|
self.instant_EH,
|
|
self.instant_E,
|
|
self.mcu.frequency,
|
|
str(self.mcu.mcu_power_state),
|
|
str(self.mcu.adc_power_state),
|
|
]
|
|
|
|
# Devices signals
|
|
for device in self.custom_devices:
|
|
for signal in device.get_signals():
|
|
data.append(signal)
|
|
|
|
# Custom signals
|
|
for key in self.custom_signals_keys:
|
|
data.append(self.custom_signals[key])
|
|
|
|
self.signals.append(data)
|
|
|
|
self.total_time += t
|
|
|
|
|
|
def __disabled_update_stats(self, op_mode_name, additional_op_mode_name, is_wait_cycle, mcu_clock_action, elapsed_time, n_ticks, energy_draws, e_harvested):
|
|
"""
|
|
Disabled method
|
|
"""
|
|
return
|
|
|
|
def __update_stats(self, op_mode_name, additional_op_mode_name, is_wait_cycle, mcu_clock_action, elapsed_time, n_ticks, energy_draws, e_harvested):
|
|
"""
|
|
Updates internal statistics
|
|
:param op_mode_name: current operation identifier (string) for metrics identification
|
|
:param additional_op_mode_name: additional operation identifier (string) for metrics identification
|
|
:param is_wait_cycle: if current cycle is a wait cycle
|
|
:param mcu_clock_action: Clock cycle action of the MCU
|
|
:param elapsed_time: time elapsed in the simulation
|
|
:param n_ticks: number of ticks simulated
|
|
:param energy_draws: energy drawn by the entire circuit calculated with calculate_energy_draws()
|
|
:param e_harvested: energy harvested from the environment
|
|
"""
|
|
wait_cycle_mask = "_WAIT" if is_wait_cycle else ""
|
|
|
|
op_mode_name = "{}{}".format(str(op_mode_name), wait_cycle_mask)
|
|
mcu_clock_action = "{}{}".format(str(mcu_clock_action), wait_cycle_mask)
|
|
mcu_state = "{}{}".format(str(self.mcu.get_mcu_state()), wait_cycle_mask)
|
|
|
|
# Unpack dict
|
|
energy_drawn = energy_draws['energy_drawn']
|
|
vreg_energy_loss = energy_draws['vreg_energy_loss']
|
|
energy_components = energy_draws['energy_components']
|
|
energy_loss_components = energy_draws['energy_loss_components']
|
|
|
|
# Operation mode names
|
|
op_modes = ['total', op_mode_name]
|
|
if additional_op_mode_name is not None:
|
|
additional_op_mode_name = "{}{}".format(str(additional_op_mode_name), wait_cycle_mask)
|
|
op_modes.append(additional_op_mode_name)
|
|
|
|
for op_mode in op_modes:
|
|
for mcu_action in ['total', mcu_clock_action]:
|
|
self.elapsed_time[op_mode][mcu_action] += elapsed_time
|
|
self.elapsed_ticks[op_mode][mcu_action] += n_ticks
|
|
self.used_energy['total'][op_mode][mcu_action] += energy_drawn
|
|
self.vreg_energy_loss['total'][op_mode][mcu_action] += vreg_energy_loss
|
|
|
|
for component in energy_components.keys():
|
|
energy = energy_components[component]
|
|
vreg_loss = energy_loss_components[component]
|
|
self.used_energy[component][op_mode][mcu_action] += energy
|
|
self.vreg_energy_loss[component][op_mode][mcu_action] += vreg_loss
|
|
|
|
for mcu_st in ['total', mcu_state]:
|
|
self.harvested_energy[op_mode][mcu_st] += e_harvested
|
|
|
|
|
|
def get_elapsed_time(self):
|
|
"""
|
|
:return: the time elapsed during a simulation tick / step
|
|
"""
|
|
mcu_state = self.mcu.get_mcu_state()
|
|
|
|
# If energy source is set and MCU in LPM / OFF -> recharging energy buffer from harvested energy
|
|
# -> fast forward to next sampled interval
|
|
if self.energy_source is not None and (mcu_state == MCUPowerState.OFF or (mcu_state == MCUPowerState.LPM and not self.lpm_precise_ticks)):
|
|
return self.energy_source.get_current_sample_remaining_time()
|
|
|
|
return self.mcu.get_mcu_cycle_time()
|
|
|
|
|
|
def calculate_energy_draws(self, mcu_clock_action, elapsed_time):
|
|
"""
|
|
Calculates the energy draw of the simulated circuit and returns it
|
|
:param mcu_clock_action: Clock cycle action of the MCU
|
|
:param elapsed_time: the time elapsed
|
|
:return: a dictionary with all the energy draws types
|
|
"""
|
|
energy_components = {}
|
|
energy_loss_components = {}
|
|
|
|
# Energy consumed from the output of the voltage regulator
|
|
e = self.mcu.get_drained_energy(mcu_clock_action, elapsed_time)
|
|
|
|
energy_components['mcu'] = e
|
|
energy_from_regulator = e
|
|
energy_loss_components['mcu'] = 0.0
|
|
|
|
# Energy consumed by devices attached to voltage regulator
|
|
for name, component in self.additional_components[ComponentVoltageSource.VOLTAGE_REGULATOR].items():
|
|
e = component.get_drained_energy(elapsed_time)
|
|
energy_components[name] = e
|
|
energy_from_regulator += e
|
|
energy_loss_components[name] = 0.0
|
|
|
|
# energy drawn by voltage regulator from energy buffer
|
|
energy_drawn = self.voltage_regulator.get_drained_energy(energy_from_regulator, elapsed_time)
|
|
|
|
# energy loss due to voltage regulator (in)efficiency
|
|
vreg_energy_loss = energy_drawn - energy_from_regulator
|
|
|
|
if energy_from_regulator > 0.0:
|
|
# Calculate the energy lost due to voltage regulator for each connected component
|
|
for name in energy_components.keys():
|
|
e_ratio = energy_components[name] / energy_from_regulator
|
|
energy_loss_components[name] = vreg_energy_loss * e_ratio
|
|
|
|
# Energy consumed by devices attached to energy buffer
|
|
for name, component in self.additional_components[ComponentVoltageSource.ENERGY_BUFFER].items():
|
|
e = component.get_drained_energy(elapsed_time)
|
|
energy_components[name] = e
|
|
energy_drawn += e
|
|
energy_loss_components[name] = 0.0
|
|
|
|
retval = {
|
|
'energy_drawn': energy_drawn,
|
|
'vreg_energy_loss': vreg_energy_loss,
|
|
'energy_from_regulator': energy_from_regulator,
|
|
'energy_components': energy_components,
|
|
'energy_loss_components': energy_loss_components,
|
|
}
|
|
|
|
return retval
|
|
|
|
def run_step(self, mcu_clock_action, op_mode_name, is_wait_cycle=False, from_custom_device=None, additional_op_mode_name=None):
|
|
"""
|
|
Runs a simulation step
|
|
:param mcu_clock_action: Clock cycle action of the MCU
|
|
:param op_mode_name: current operation identifier (string) for metrics identification
|
|
:param is_wait_cycle: specifies if the execution happens due to a wait cycle
|
|
:param from_custom_device: name of the cust device that is triggering the step execution
|
|
:param additional_op_mode_name: additional op_mode_name for metrics identification
|
|
:return: the number of clock cycles executed by the MCU
|
|
"""
|
|
# Automatic check enabled -> return 0 ticks if power failure occurred
|
|
if self.automatic_check_for_power_failures:
|
|
|
|
# Check for a possible power failure (do not reset cache)
|
|
self.cached_power_failure = self.power_failure_occurring(False)
|
|
|
|
if self.cached_power_failure:
|
|
return 0
|
|
|
|
devices_ticks = self.run_custom_devices_logic(from_custom_device)
|
|
|
|
n_ticks = 1
|
|
|
|
# Set ticks to 0 if current operation is a NOP
|
|
if mcu_clock_action == MCUClockCycleAction.LPM_NOP or mcu_clock_action == MCUClockCycleAction.NOP_OFF_RECHARGE:
|
|
n_ticks = 0
|
|
|
|
elapsed_time = self.get_elapsed_time()
|
|
self._collect_signals(elapsed_time)
|
|
|
|
energy_draws = self.calculate_energy_draws(mcu_clock_action, elapsed_time)
|
|
energy_drawn = energy_draws['energy_drawn']
|
|
|
|
# Instantaneous Values (pre-recharge)
|
|
self.instant_V = self.energy_buffer.get_voltage()
|
|
self.instant_E = energy_drawn
|
|
|
|
# Energy source voltage intervals -> [(voltage, time),...]
|
|
voltage_intervals = self.energy_harvester.get_voltage_intervals(elapsed_time)
|
|
|
|
# Get equivalent resistances
|
|
charge_r = self.energy_harvester.get_equivalent_resistance()
|
|
e_harvested = self.energy_buffer.update(voltage_intervals, energy_drawn, charge_r, elapsed_time, self.cached_power_failure)
|
|
|
|
# Instant EH = e_harvested if charge + e_drawn
|
|
self.instant_EH = e_harvested
|
|
|
|
# if a cached power failure occurred, it means that power failures are checked manually, as otherwise the
|
|
# execution would not reach this point
|
|
# -> save the consumed energy and decrement it later
|
|
# Note: self.energy_buffer.update() keeps the voltage constant if self.cached_power_failure is set
|
|
if self.cached_power_failure:
|
|
self.cached_energy += energy_drawn
|
|
self.cached_harvested_energy += e_harvested
|
|
|
|
# Collect metrics
|
|
self.__update_stats(op_mode_name, additional_op_mode_name, is_wait_cycle, mcu_clock_action, elapsed_time, n_ticks, energy_draws, e_harvested)
|
|
self.mcu.update_stats(elapsed_time, n_ticks)
|
|
|
|
# Account for wait cycles
|
|
if not is_wait_cycle:
|
|
wait_cycles = self.mcu.get_action_wait_cycles(mcu_clock_action)
|
|
for _ in range(wait_cycles):
|
|
n_ticks += self.run_step(mcu_clock_action, op_mode_name, True, from_custom_device, additional_op_mode_name)
|
|
|
|
# Physical memory access latency
|
|
if mcu_clock_action == MCUClockCycleAction.PHYSICAL_MEMORY_ACCESS:
|
|
for memory in self.physical_memories:
|
|
name = memory.get_name()
|
|
wait_cycles = memory.get_wait_cycles(self.mcu.get_frequency())
|
|
|
|
# simulate wait cycles
|
|
for _ in range(wait_cycles):
|
|
n_ticks += self.run_step(mcu_clock_action, op_mode_name, True, from_custom_device, name)
|
|
|
|
# reset memories' operations queue
|
|
memory.reset_operations_queue()
|
|
|
|
# LPM exit
|
|
if mcu_clock_action == MCUClockCycleAction.LPM_EXIT:
|
|
# Turn on MCU and init custom devices
|
|
n_ticks += self.init()
|
|
|
|
return n_ticks + devices_ticks
|
|
|
|
|
|
def get_drained_energy(self):
|
|
"""
|
|
:return: the used energy
|
|
"""
|
|
return self.used_energy['total']['total']['total']
|
|
|
|
|
|
def init(self, custom_devices_print_enabled=True):
|
|
"""
|
|
Initializes the MCU and custom devices
|
|
:param custom_devices_print_enabled: enables/disables custom devices messages
|
|
:return: the number of clock cycles executed by the MCU
|
|
"""
|
|
n_ticks = 0
|
|
|
|
self.mcu.set_mcu_state(MCUPowerState.ON)
|
|
|
|
for device in self.custom_devices:
|
|
n_ticks += device.init(custom_devices_print_enabled)
|
|
|
|
return n_ticks
|
|
|
|
|
|
def run_custom_devices_logic(self, skip_device=None):
|
|
"""
|
|
Runs all custom device logic
|
|
:param skip_device: device to skip
|
|
:return: the number of clock cycles executed by the MCU during custom device operations
|
|
"""
|
|
n_ticks = 0
|
|
|
|
for device in self.custom_devices:
|
|
if device.name != skip_device:
|
|
n_ticks += device.run_logic()
|
|
|
|
return n_ticks
|
|
|
|
|
|
def power_failure_occurred(self):
|
|
"""
|
|
:return: if a power failure occurred during previous operations
|
|
"""
|
|
return self.cached_power_failure
|
|
|
|
|
|
def power_failure_occurring(self, reset_cache=True):
|
|
"""
|
|
:param reset_cache: reset the cache if a cached power failure occurred
|
|
:return: if the MCU needs to be shut down / a power failure is occurring
|
|
"""
|
|
if self.cached_power_failure:
|
|
# reset cache (the manual control is happening)
|
|
if reset_cache:
|
|
# e_harvested - e_drawn -> energy drawn (this is negative if e_harvested < e_drawn)
|
|
net_e_drawn = self.cached_harvested_energy - self.cached_energy
|
|
self.energy_buffer.increment_energy(net_e_drawn)
|
|
self.cached_power_failure = False
|
|
self.cached_energy = 0.0
|
|
self.cached_harvested_energy = 0.0
|
|
|
|
return True
|
|
|
|
# keep this call here, as it updates also all the power state events
|
|
events = self.get_power_state_events()
|
|
|
|
# voltage threshold
|
|
if self.power_off_condition == PowerOffCondition.ENERGY_BUFFER_VOLTAGE_THRESHOLD:
|
|
voltage = self.energy_buffer.get_voltage()
|
|
return voltage < self.power_off_voltage_threshold
|
|
|
|
# event-based
|
|
elif self.power_off_condition == PowerOffCondition.POWER_STATE_EVENT:
|
|
if len(events) == 0:
|
|
return False
|
|
|
|
for poff_event in self.power_off_events:
|
|
if poff_event in events:
|
|
return True
|
|
|
|
return False
|
|
|
|
else:
|
|
raise ConfigurationException("Power off condition not set.")
|
|
|
|
|
|
def record_power_failure(self):
|
|
"""
|
|
Records the occurrence of a power failure
|
|
"""
|
|
self.power_failures += 1
|
|
self.mcu.power_failure_callback()
|
|
|
|
|
|
def reset(self, run_gc_collect=True):
|
|
"""
|
|
Resets statistics
|
|
:param run_gc_collect: runs the garbage collector after deleting the saved signals
|
|
"""
|
|
del self.elapsed_time
|
|
self.elapsed_time = defaultdict(lambda: defaultdict(float))
|
|
del self.elapsed_ticks
|
|
self.elapsed_ticks = defaultdict(lambda: defaultdict(int))
|
|
del self.used_energy
|
|
self.used_energy = defaultdict(lambda: defaultdict(lambda: defaultdict(float)))
|
|
del self.vreg_energy_loss
|
|
self.vreg_energy_loss = defaultdict(lambda: defaultdict(lambda: defaultdict(float)))
|
|
|
|
# remove garbage
|
|
if run_gc_collect:
|
|
gc.collect()
|
|
|
|
self.power_failures = 0
|
|
self.cached_power_failure = False
|
|
self.cached_energy = 0.0
|
|
self.cached_harvested_energy = 0.0
|
|
|
|
self.instant_V = 0.0
|
|
self.instant_E = 0.0
|
|
self.instant_EH = 0.0
|
|
|
|
self._init_signals(run_gc_collect)
|
|
self.total_time = 0.0
|
|
|
|
self.state_retention.reset()
|
|
self.mcu.reset()
|
|
|
|
|
|
def circuit_equivalent_resistance(self, mcu_clock_action, energy_draws=None, elapsed_time=None):
|
|
"""
|
|
Calculates the circuit equivalent resistance (excluding the energy harvester)
|
|
:param mcu_clock_action: Clock cycle action of the MCU
|
|
:param energy_draws: pre-computed energy draws
|
|
:param elapsed_time: elapsed time in the pre-computed energy draws
|
|
:return: the circuit equivalent resistance
|
|
"""
|
|
if energy_draws is None:
|
|
energy_drawn = self.calculate_energy_draws(mcu_clock_action, elapsed_time)['energy_drawn']
|
|
else:
|
|
energy_drawn = energy_draws['energy_drawn']
|
|
|
|
return energy_utils.R_from_energy_t(self.energy_buffer.get_voltage(), energy_drawn, elapsed_time)
|
|
|
|
def execute_full_recharge(self, mcu_clock_action, op_mode_name, v_target, v_supply):
|
|
"""
|
|
Executes a full recharge of the energy buffer
|
|
:param mcu_clock_action: Clock cycle action of the MCU
|
|
:param op_mode_name: current operation identifier (string) for metrics identification
|
|
:param v_target: target voltage
|
|
:param v_supply: voltage of the power supply
|
|
"""
|
|
v_buff = self.energy_buffer.get_voltage()
|
|
|
|
# Check non-charging condition and return
|
|
if v_buff > v_target or v_buff > v_supply or v_target > v_supply:
|
|
return
|
|
|
|
# Equivalent resistance
|
|
charge_r = self.energy_harvester.get_equivalent_resistance()
|
|
|
|
# Calculate time required to recharge
|
|
t_recharge = self.energy_buffer.calculate_recharge_time_to_voltage(v_target, v_supply, charge_r)
|
|
energy_draws = self.calculate_energy_draws(MCUClockCycleAction.NOP_OFF_RECHARGE, t_recharge)
|
|
|
|
# Calculate harvested energy
|
|
e_harvested = self.energy_buffer.calculate_recharge_energy_to_voltage(v_target)
|
|
e_harvested += energy_draws['energy_drawn']
|
|
|
|
self.__update_stats(op_mode_name, None, False, mcu_clock_action, t_recharge, 0, energy_draws, e_harvested)
|
|
|
|
# Refill energy buffer to v_supply
|
|
self.energy_buffer.set_voltage(v_target)
|
|
|
|
|
|
def get_component(self, name):
|
|
for components in self.additional_components.values():
|
|
for c_name, component in components.items():
|
|
if c_name == name:
|
|
return component
|