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

377 lines
17 KiB
Python

from ScEpTIC.emulator.custom_devices import CustomDevice
from ScEpTIC.emulator.custom_devices.FBTCLogicOnly.changepoint_detector import ChangepointDischargeDetectorLogic, ChangepointChargeDetectorLogic
from ScEpTIC.emulator.energy.mcu.options import MCUClockCycleAction, MCUPowerState
from ScEpTIC.emulator.energy.options import ComponentVoltageSource, OpModeName
from ScEpTIC.emulator.energy.voltage_regulator.ideal_regulator import IdealStepDownRegulator
class FBTCSystemModelLogicOnly(CustomDevice):
"""
FBTC system model - Energy model included in lookup table (voltage regulator and FBTC components)
"""
# Operational zones extracted from paper
operational_zones = {
3.3: '16MHz',
2.8: '12MHz',
2.2: '8MHz',
1.8: '1MHz',
}
voltage_dividers = {
'charge': {'R1': '2M', 'R2': '8M'},
'discharge': {'R1': '150K', 'R2': '10M'},
}
# Voltage init sensitivity
sensitivity = 0.005
op_mode_name = OpModeName.FBTC_ISR
name = 'FBTC-real'
def __init__(self):
# voltages of the operational zones (reverse ordered for logic purposes)
self.v_operational_zones = sorted(list(self.operational_zones.keys()), reverse=True)
# current operational zone
self.current_operational_zone_id = None
self.interrupt_enabled = {"charge": True, "discharge": True}
self.current_signal = {'charge': None, 'charge_enabled': None, 'discharge': None, 'discharge_enabled': None, 'state': None}
self.vreg_voltage = 0.0
def _check_lookup_table(self):
"""
Checks if the correct lookup table is applied to the system
"""
if self.system_model.mcu.lookup_table is None:
raise ConfigurationException(f"FBTCSystemModelLookupTable expects an MCU with a lookup table configured.")
if "-fbtc" not in self.system_model.mcu.lookup_table.system_name:
raise ConfigurationException(f"FBTCSystemModelLookupTable expects an MCU with a lookup table for FBTC configured.")
def setup(self, system_model):
"""
Setup the FBTC device. Note that this is a callback called by the system model
:param system_model: the system model
"""
super().setup(system_model)
self._check_lookup_table()
# The measures in the lookup table already include the voltage regulator at different input voltages and frequencies
# -> use no regulator
system_model.attach_voltage_regulator(None)
# Attach to energy manager
system_model.mcu.custom_frequency_name = self.name
system_model.mcu.dfs_enabled = True
# Changepoint Discharge Detector
d_R1 = self.voltage_dividers['discharge']['R1']
d_R2 = self.voltage_dividers['discharge']['R2']
self.changepoint_detector_discharge = ChangepointDischargeDetectorLogic(d_R1, d_R2, system_model)
# Changepoint Charge Detector
c_R1 = self.voltage_dividers['charge']['R1']
c_R2 = self.voltage_dividers['charge']['R2']
self.changepoint_detector_charge = ChangepointChargeDetectorLogic(c_R1, c_R2, system_model)
def init(self, print_enabled=True):
"""
Initializes the FBTC circuit
:param print_enabled: enables/disables print messages
:return: the number of clock cycles executed by the MCU during the initialization of custom device operations
"""
# Init to default frequency
self.system_model.mcu.set_frequency(None)
voltage = self.system_model.energy_buffer.get_voltage()
# Hardcoded first operational zone
# Adjust hardcoded first operational zone w.r.t. different system init settings
for zone_id, zone_voltage in enumerate(self.v_operational_zones):
if voltage >= zone_voltage + self.sensitivity:
self.current_operational_zone_id = zone_id
break
# Low voltage (may happen in voltage identifier analysis)
if self.current_operational_zone_id is None:
min_v = min(self.v_operational_zones)
self.current_operational_zone_id = self.v_operational_zones.index(min_v)
first_zone = self.v_operational_zones[self.current_operational_zone_id]
# On start -> voltage regulator outputs the energy buffer voltage
self.vreg_voltage = voltage
# Set voltage (hardcoded)
n_ticks = self._simulate_set_new_voltage(first_zone, False)
# Set frequency (hardcoded)
n_ticks += self._simulate_set_new_frequency(first_zone, False, print_enabled=print_enabled)
# Enable/disable interrupts (hardcoded)
n_ticks += self._simulate_enable_disable_interrupts(first_zone, False)
# 01. Set frequency identifier to max value
# frequency_id = max_frequency
n_ticks += self.system_model.run_step(MCUClockCycleAction.VOLATILE_MEMORY_ACCESS, self.op_mode_name, from_custom_device=self.name)
self._update_signals("INIT")
return n_ticks
def run_logic(self):
"""
Runs the FBTC logic (detects if a changepoint is reached and changes voltage and frequency accordingly
:return: the number of clock cycles executed by the MCU during custom device operations
"""
n_ticks = 0
# No logic when MCU off or in LPM
if self.system_model.mcu.get_mcu_state() != MCUPowerState.ON:
self._update_signals("MCU_OFF")
return n_ticks
self._update_signals(None)
# Charge detected
if self.interrupt_enabled["charge"] and self.changepoint_detector_charge.output(self.vreg_voltage):
self.current_operational_zone_id = max(self.current_operational_zone_id - 1, 0)
new_voltage = self.v_operational_zones[self.current_operational_zone_id]
n_ticks = self._simulate_interrupt_for_increase(new_voltage)
#print(f"FBTC - Increasing {new_voltage} {self.system_model.energy_buffer.get_voltage()} {self.interrupt_enabled}")
# Discharge detected
elif self.interrupt_enabled["discharge"] and self.changepoint_detector_discharge.output(self.vreg_voltage):
self.current_operational_zone_id = min(self.current_operational_zone_id + 1, len(self.v_operational_zones) - 1)
new_voltage = self.v_operational_zones[self.current_operational_zone_id]
n_ticks = self._simulate_interrupt_for_decrease(new_voltage)
#print(f"FBTC - Decreasing {new_voltage} {self.system_model.energy_buffer.get_voltage()} {self.interrupt_enabled}")
return n_ticks
def _simulate_interrupt_for_increase(self, new_vreg_voltage):
"""
Simulates the interrupt that signals the MCU to increase its clock frequency
The MCU changes first sets the new output voltage of the voltage regulator and then changes its clock frequency.
:param new_vreg_voltage: the new operational zone voltage to be set as output of voltage regulator
"""
n_ticks = self._simulate_isr_call()
n_ticks += self._simulate_set_new_voltage(new_vreg_voltage)
n_ticks += self._simulate_set_new_frequency(new_vreg_voltage)
n_ticks += self._simulate_enable_disable_interrupts(new_vreg_voltage)
n_ticks += self._simulate_isr_return()
return n_ticks
def _simulate_interrupt_for_decrease(self, new_vreg_voltage):
"""
Simulates the interrupt that signals the MCU to decrease its clock frequency.
The MCU changes first its clock frequency and then sets the new output voltage of the voltage regulator.
:param new_vreg_voltage: the new operational zone voltage to be set as output of voltage regulator
"""
n_ticks = self._simulate_isr_call()
n_ticks += self._simulate_set_new_frequency(new_vreg_voltage)
n_ticks += self._simulate_set_new_voltage(new_vreg_voltage)
n_ticks += self._simulate_enable_disable_interrupts(new_vreg_voltage)
n_ticks += self._simulate_isr_return()
return n_ticks
def _simulate_set_new_frequency(self, new_vreg_voltage, retrieve_val=True, print_enabled=True):
"""
Simulates the execution of the instructions that change the MCU frequency
:param new_vreg_voltage: the new operational zone voltage to be set as output of voltage regulator
:param retrieve_val: True if the frequency value is not hardcoded in the simulated operation and must be loaded from memory
:param print_enabled: enables/disables print messages
:return: the number of executed clock cycles
"""
# Get the new frequency value
# R1 = operating_frequency[frequency_id] (frequency_id in R0)
# 01. LOAD R1, R0(operating_frequency)
n_ticks = 0
if retrieve_val:
n_ticks = self.system_model.run_step(MCUClockCycleAction.VOLATILE_MEMORY_ACCESS, self.op_mode_name, from_custom_device=self.name)
# Set the operating frequency
# MSP430FR:
# 02. CSCTL1 = R1
# 03. CSCTL2 = SELA__VLOCLK | SELS__DCOCLK | SELM__DCOCLK; // Set SMCLK = MCLK = DCO
# 04. CSCTL3 = DIVA__1 | DIVS__1 | DIVM__1; // Set all dividers
# MSP430G:
# 02. DCOCTL = 0; // Select lowest DCOx and MODx settings
# 03. BCSCTL1 = R1; // Set range
# 04. DCOCTL = R1; // Set DCO step + modulation */
n_ticks += self.system_model.run_step(MCUClockCycleAction.NO_MEMORY_ACCESS, self.op_mode_name, from_custom_device=self.name)
n_ticks += self.system_model.run_step(MCUClockCycleAction.NO_MEMORY_ACCESS, self.op_mode_name, from_custom_device=self.name)
n_ticks += self.system_model.run_step(MCUClockCycleAction.NO_MEMORY_ACCESS, self.op_mode_name, from_custom_device=self.name)
if print_enabled:
print(f"FBTCLogicOnly: frequency {self.system_model.mcu.frequency} -> {self.operational_zones[new_vreg_voltage]} / voltage set to {new_vreg_voltage}V (buffer: {self.system_model.energy_buffer.get_voltage():.3}V)")
# Update MCU frequency for the simulation
self.system_model.mcu.set_frequency(self.operational_zones[new_vreg_voltage])
# MSP430FR - Set FRAM WAIT CYCLES
if self.system_model.mcu.family == 'MSP430FR':
# Get the number of wait cycles
# R1 = fram_wait_cycles[frequency_id]
# 05. LOAD R1, R0(fram_wait_cycles)
n_ticks += self.system_model.run_step(MCUClockCycleAction.VOLATILE_MEMORY_ACCESS, self.op_mode_name, from_custom_device=self.name)
# Set NWAITS
# 06. FRCTL0 = R1
n_ticks += self.system_model.run_step(MCUClockCycleAction.NO_MEMORY_ACCESS, self.op_mode_name, from_custom_device=self.name)
return n_ticks
def _simulate_set_new_voltage(self, new_vreg_voltage, retrieve_val=True):
"""
Simulates the execution of the instructions that change the Voltage Regulator output voltage
:param new_vreg_voltage: the new operational zone voltage to be set as output of voltage regulator
:param retrieve_val: True if the voltage value is not hardcoded in the simulated operation and must be loaded from memory
:return: the number of executed clock cycles
"""
# Get the new voltage value
# R1 = operating_voltage[frequency_id]
# 01. LOAD R1, R0(operating_voltage)
n_ticks = 0
if retrieve_val:
n_ticks += self.system_model.run_step(MCUClockCycleAction.VOLATILE_MEMORY_ACCESS, self.op_mode_name, from_custom_device=self.name)
# Set the voltage to the output pin group to change voltage
# 02. MOV R1, PIN_GROUP_1
n_ticks += self.system_model.run_step(MCUClockCycleAction.NO_MEMORY_ACCESS, self.op_mode_name, from_custom_device=self.name)
# Update output of voltage regulator for the simulation
self.vreg_voltage = new_vreg_voltage
return n_ticks
def _simulate_enable_disable_interrupts(self, new_vreg_voltage, retrieve_val=True):
"""
Simulates the execution of the instructions that enable/disable the charge/discharge interrupts (required for de-bouncing)
:param new_vreg_voltage: the new operational zone voltage
:param retrieve_val: True if the enable/disable value is not hardcoded in the simulated operation and must be loaded from memory
:return: the number of executed clock cycles
"""
n_ticks = 0
# Get the enable/disable status of the charge interrupt
# R1 = interrupt_charge_state[frequency_id] (frequency_id in R0)
# 01. LOAD R1, R0(interrupt_charge_state)
if retrieve_val:
n_ticks += self.system_model.run_step(MCUClockCycleAction.VOLATILE_MEMORY_ACCESS, self.op_mode_name, from_custom_device=self.name)
# Enable/disable the interrupt
# 02. MOV IE1, R1
n_ticks += self.system_model.run_step(MCUClockCycleAction.NO_MEMORY_ACCESS, self.op_mode_name, from_custom_device=self.name)
# Charge interrupt enabled only if new_vreg_voltage < operational zone 0 (max voltage reached)
self.interrupt_enabled["charge"] = new_vreg_voltage < self.v_operational_zones[0]
# Get the enable/disable status of the discharge interrupt
# R1 = interrupt_discharge_state[frequency_id] (frequency_id in R0)
# 03. LOAD R1, R0(interrupt_discharge_state)
if retrieve_val:
n_ticks += self.system_model.run_step(MCUClockCycleAction.VOLATILE_MEMORY_ACCESS, self.op_mode_name, from_custom_device=self.name)
# Enable/disable the interrupt
# 04. MOV IE2, R1
n_ticks += self.system_model.run_step(MCUClockCycleAction.NO_MEMORY_ACCESS, self.op_mode_name, from_custom_device=self.name)
# Discharge interrupt enabled only if new_vreg_voltage > operational zone n (min voltage reached)
self.interrupt_enabled["discharge"] = new_vreg_voltage > self.v_operational_zones[-1]
return n_ticks
def _simulate_isr_call(self):
"""
Simulates the execution of the ISR call
:return: the number of executed clock cycles
"""
# Interrupt fires -> call ISR
# 01. save PC
# 02. save EBP
# 03. jump ISR
n_ticks = self.system_model.run_step(MCUClockCycleAction.VOLATILE_MEMORY_ACCESS, self.op_mode_name, from_custom_device=self.name)
n_ticks += self.system_model.run_step(MCUClockCycleAction.VOLATILE_MEMORY_ACCESS, self.op_mode_name, from_custom_device=self.name)
n_ticks += self.system_model.run_step(MCUClockCycleAction.NO_MEMORY_ACCESS, self.op_mode_name, from_custom_device=self.name)
# Increment frequency identifier
# frequency_id = frequency_id + 1
# 04. LOAD frequency_id, R0
# 05. ADD R0, R0, 1
# 06. STORE R0, frequency_id
n_ticks += self.system_model.run_step(MCUClockCycleAction.VOLATILE_MEMORY_ACCESS, self.op_mode_name, from_custom_device=self.name)
n_ticks += self.system_model.run_step(MCUClockCycleAction.NO_MEMORY_ACCESS, self.op_mode_name, from_custom_device=self.name)
n_ticks += self.system_model.run_step(MCUClockCycleAction.VOLATILE_MEMORY_ACCESS, self.op_mode_name, from_custom_device=self.name)
return n_ticks
def _simulate_isr_return(self):
"""
Simulates the execution of the return from ISR
:return: the number of executed clock cycles
"""
# Return from ISR
# 01. pop EBP
# 02. pop PC
n_ticks = self.system_model.run_step(MCUClockCycleAction.VOLATILE_MEMORY_ACCESS, self.op_mode_name, from_custom_device=self.name)
n_ticks += self.system_model.run_step(MCUClockCycleAction.VOLATILE_MEMORY_ACCESS, self.op_mode_name, from_custom_device=self.name)
return n_ticks
def _update_signals(self, state):
"""
Updates the internal signals state
:param state: current state
"""
self.current_signal['charge'] = self.changepoint_detector_charge.output(self.vreg_voltage)
self.current_signal['charge_enabled'] = self.interrupt_enabled['charge']
self.current_signal['discharge'] = self.changepoint_detector_charge.output(self.vreg_voltage)
self.current_signal['discharge_enabled'] = self.interrupt_enabled['discharge']
self.current_signal['state'] = state
def get_signals_strings(self):
"""
:return: a list with the names of the signals that may be collected from this device
"""
return [
'FBTC State',
'FBTC Charge Detector',
'FBTC/MCU Charge Interrupt Enabled',
'FBTC Discharge Detector',
'FBTC/MCU Discharge Interrupt Enabled',
]
def get_signals(self):
"""
:return: a list with the signals collected from this device
"""
return [
self.current_signal['state'],
self.current_signal['charge'],
self.current_signal['charge_enabled'],
self.current_signal['discharge'],
self.current_signal['discharge_enabled'],
]