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

213 lines
8.1 KiB
Python

import math
from ScEpTIC.emulator.energy import energy_utils
from ScEpTIC.emulator.energy.mcu import MCUPowerState
from ScEpTIC.emulator.energy.mcu_peripheral import MCUPeripheral
from ScEpTIC.emulator.energy.mcu_peripheral.options import ExternalNVMState, NVMPowerState, MCUPeripheralPowerStateBehaviour
from ScEpTIC.emulator.energy.mcu_peripheral.protocol_models.external_nvm_protocol import ExternalNVMProtocolAction
from ScEpTIC.emulator.energy.power_state_event import PowerStateEvent
from ScEpTIC.emulator.energy.voltage_regulator import NoRegulator
from ScEpTIC.exceptions import ConfigurationException
class ExternalNVM(MCUPeripheral):
"""
External NVM chip
"""
def __init__(self, chip_name):
super().__init__()
datasheet_module = __import__(f'ScEpTIC.emulator.energy.mcu_peripheral.datasheets.{chip_name}', fromlist=[''])
self.datasheet = getattr(datasheet_module, 'datasheet')
self.min_v = energy_utils.str_to_float(self.datasheet['min_v'])
self.peak_frequency = energy_utils.str_to_float(self.datasheet['peak_frequency'])
measurement_voltage = energy_utils.str_to_float(self.datasheet['measure_voltage'])
standby_current_draw = energy_utils.str_to_float(self.datasheet['standby_current_draw'])
# Divide by frequency to recalculate w.r.t. MCU frequency later
operating_current_draw = energy_utils.str_to_float(self.datasheet['operating_current_draw']) / self.peak_frequency
self.equivalent_resistance = {
'standby': energy_utils.equivalent_resistance(measurement_voltage, standby_current_draw),
'operating': energy_utils.equivalent_resistance(measurement_voltage, operating_current_draw)
}
if self.datasheet['has_regulator']:
self.internal_regulator = self.datasheet['regulator_type']()
v_out = energy_utils.str_to_float(self.datasheet['regulator_voltage'])
self.internal_regulator.set_output_voltage(v_out)
else:
self.internal_regulator = NoRegulator()
self.address_size = math.ceil(self.datasheet['address_size'] / 8)
self.cell_size = math.ceil(self.datasheet['cell_size'] / 8)
self.protocol = self.datasheet['protocol'](self.address_size)
self.power_state = NVMPowerState.OFF
self.power_state_behaviour = MCUPeripheralPowerStateBehaviour.FOLLOW_MCU
# Default state: standby
self.state = ExternalNVMState.STANDBY
def _follow_mcu_power_state(self):
"""
Checks the MCU power state and updates the peripheral power state accordingly
"""
mcu_state = self.mcu.get_mcu_state()
if mcu_state == MCUPowerState.ON:
if self.power_state != NVMPowerState.ON:
self.set_power_state(NVMPowerState.ON)
self.set_state(ExternalNVMState.STANDBY)
else:
if self.power_state != NVMPowerState.OFF:
self.set_state(ExternalNVMState.STANDBY)
self.set_power_state(NVMPowerState.OFF)
def attach_voltage_source(self, source):
"""
:param source: a voltage source
"""
self.internal_regulator.attach_voltage_source(source)
super().attach_voltage_source(self.internal_regulator)
def get_min_v(self):
"""
:return: the minimum voltage required by the external NVM to operate
"""
return self.min_v
def set_state(self, state):
"""
Sets the NVM state (STANDBY / OPERATING)
:param state: the NVM state (instance of ExternalNVMState)
"""
if self.power_state == NVMPowerState.OFF:
raise Exception(f"NVM is off!")
if not isinstance(state, ExternalNVMState):
raise ConfigurationException(f"{self.__class__.__name__}.set_state() requires a ExternalNVMState but {state.__class__.__name__} was given")
self.state = state
def get_state(self):
"""
:return: the power state and the current state
"""
return self.power_state, self.state
def set_power_state(self, power_state):
"""
Sets the NVM power state (ON / OFF)
:param power_state: the NVM power state (instance of NVMPowerState)
"""
if not isinstance(power_state, NVMPowerState):
raise ConfigurationException(f"{self.__class__.__name__}.set_power_state() requires a NVMPowerState but {power_state.__class__.__name__} was given")
self.power_state = power_state
def _get_drained_energy(self, t):
"""
Calculates the energy consumed by the component.
:param t: elapsed time
:return: the consumed energy
"""
if self.power_state == NVMPowerState.OFF:
return 0.0
voltage = self.voltage_source.get_voltage()
if self.state == ExternalNVMState.STANDBY:
equivalent_resistance = self.equivalent_resistance['standby']
else:
# we need to account for the operating frequency to calculate current draw in operating mode
max_frequency = min(self.mcu.get_frequency(), self.peak_frequency)
equivalent_resistance = self.equivalent_resistance['operating'] * max_frequency
energy_draw = energy_utils.energy_from_R_t(voltage, equivalent_resistance, t)
return self.voltage_source.get_drained_energy(energy_draw, t)
def get_power_state_events(self):
"""
:return: a list of PowerStateEvent events that are occurring
"""
events = []
voltage = self.voltage_source.get_voltage()
if voltage < self.min_v:
self.power_state = NVMPowerState.OFF
events.append(PowerStateEvent.NVM_OFF)
return events
def get_wait_cycles(self):
"""
:return: the cycles the MCU needs to wait to send the next command / bit
"""
mcu_frequency = self.mcu.get_frequency()
if mcu_frequency <= self.peak_frequency:
return 0
cycles = math.ceil(mcu_frequency / self.peak_frequency)
# 1 cycle already used to send the command
return cycles - 1
def execute_action(self, operation_code, system_model, n_bytes=0, additional_op_mode_name=None):
"""
Executes a peripheral action
:param operation_code: the operation code (ExternalNVMProtocolAction)
:param system_model: the system model
:param n_bytes: the number of bytes, if the operation code is READ_BYTE or WRITE_BYTE
:param additional_op_mode_name: additional operation mode name for metrics collection
:return: the number of executed clock cycles
"""
bytes_multiplier = 1
if operation_code == ExternalNVMProtocolAction.READ_INIT or operation_code == ExternalNVMProtocolAction.WRITE_INIT:
self.set_state(ExternalNVMState.OPERATING)
elif operation_code == ExternalNVMProtocolAction.STOP:
self.set_state(ExternalNVMState.STANDBY)
elif operation_code == ExternalNVMProtocolAction.READ_BYTE or ExternalNVMProtocolAction.WRITE_BYTE:
if self.state != ExternalNVMState.OPERATING:
raise Exception(f"Cannot execute {operation_code}: {self.__class__.__name__} was not initialized!")
if n_bytes == 0:
raise Exception(f"Cannot execute {operation_code}: n_bytes must be > 0! ({n_bytes} provided)")
bytes_multiplier = max(math.ceil(n_bytes / self.cell_size), 1)
# calculate the number of clock cycles (MCU and peripheral may have different clock speeds)
peripheral_ticks = self.protocol.get_operation_ticks(operation_code) * bytes_multiplier
mcu_waits_per_peripheral_tick = self.get_wait_cycles()
total_ticks = peripheral_ticks * (1 + mcu_waits_per_peripheral_tick)
#print(f"Ticks: {peripheral_ticks}; MCU: {mcu_waits_per_peripheral_tick}; Total: {total_ticks}")
n_ticks = 0
for _ in range(total_ticks):
n_ticks += system_model.run_step(self.protocol.clock_cycle_action, self.protocol.op_mode_name, additional_op_mode_name=additional_op_mode_name)
return n_ticks