361 lines
16 KiB
Python
361 lines
16 KiB
Python
from ScEpTIC.emulator.custom_devices import CustomDevice
|
|
from ScEpTIC.emulator.custom_devices.FBTC.changepoint_detector import ChangepointDischargeDetector, ChangepointChargeDetector
|
|
from ScEpTIC.emulator.custom_devices.FBTC.fbtc_energy_model import FBCTEnergyModel
|
|
from ScEpTIC.emulator.energy.mcu.options import MCUClockCycleAction, MCUPowerState
|
|
from ScEpTIC.emulator.energy.options import ComponentVoltageSource, OpModeName
|
|
from ScEpTIC.emulator.energy.voltage_regulator.TPS62740x import TPS62740x
|
|
|
|
|
|
class FBTCSystemModel(CustomDevice):
|
|
"""
|
|
FBTC system model
|
|
"""
|
|
|
|
# Operational zones extracted from paper
|
|
operational_zones = {
|
|
3.3: '16MHz',
|
|
2.8: '12MHz',
|
|
2.2: '8MHz',
|
|
1.8: '1MHz',
|
|
}
|
|
|
|
# Voltage init sensitivity
|
|
sensitivity = 0.005
|
|
|
|
op_mode_name = OpModeName.FBTC_ISR
|
|
|
|
name = 'FBTC'
|
|
|
|
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}
|
|
|
|
|
|
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)
|
|
|
|
# Initialize devices
|
|
fbtc_model = FBCTEnergyModel()
|
|
voltage_regulator = TPS62740x()
|
|
|
|
# Attach to energy manager
|
|
system_model.attach_voltage_regulator(voltage_regulator)
|
|
system_model.attach_component('FBTC', fbtc_model, ComponentVoltageSource.ENERGY_BUFFER)
|
|
system_model.mcu.custom_frequency_name = 'FBTC'
|
|
system_model.mcu.dfs_enabled = True
|
|
|
|
# Changepoint Discharge Detector
|
|
d_R1 = fbtc_model.circuit_components['CHANGEPOINT_DETECTION']['R']['voltage_divider_discharge']['R1']
|
|
d_R2 = fbtc_model.circuit_components['CHANGEPOINT_DETECTION']['R']['voltage_divider_discharge']['R2']
|
|
self.changepoint_detector_discharge = ChangepointDischargeDetector(d_R1, d_R2, system_model)
|
|
|
|
# Changepoint Charge Detector
|
|
c_R1 = fbtc_model.circuit_components['CHANGEPOINT_DETECTION']['R']['voltage_divider_charge']['R1']
|
|
c_R2 = fbtc_model.circuit_components['CHANGEPOINT_DETECTION']['R']['voltage_divider_charge']['R2']
|
|
self.changepoint_detector_charge = ChangepointChargeDetector(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.system_model.voltage_regulator.set_output_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.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.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"FBTC: 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.system_model.voltage_regulator.set_output_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.current_signal['charge_enabled'] = self.interrupt_enabled['charge']
|
|
|
|
self.current_signal['discharge'] = self.changepoint_detector_charge.output()
|
|
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'],
|
|
]
|