Timers¶
Timers are fundamental components commonly found in processors. A timer operates by periodically generating an interrupt request (IRQ) after a specified number of instructions have executed. These IRQs can typically be masked or configured to control their trigger frequency. In real-time operating systems (RTOS), timers are essential for task switching, while in general-purpose operating systems, they are used for time tracking and scheduling.
Styx provides several approaches for implementing timers, each with different trade-offs between accuracy, performance, and implementation complexity.
We distinuquish between target timer registers being memory mapped I/O
for peripherals (e.g. arm) or CPU registers (e.g. PowerPC’s TBL and
TBU).
A naive implementation of a timer IRQ may use an asyncronous timer that triggers every 100ms, or other constant duration. However, you will find that most of these implementations listed here rely on triggering timer IRQs after a constant number of instructions, rather than duration of time. This is to ensure target emulation is deterministic across executions. This is becase instruction cycles are not linked to a specific timestep so the host machines CPU speed and load will change how many instructions are executed in a given 100ms wall clock time interval from one run to the next. What this materializes into is that a bug may occur if a timer is triggered at a specific instruction type or in a critical point in a task. To investigate further, you rerun the test and the next run, your host machine is slightly faster and instead of triggering at the critical point, the timer is triggered at a benign instruction, thus missing the bug.
Without deterministic execution, debugging becomes difficult. It is recommended your timer implementation follows this advice.
See more about the Async Implementation and the example processor that uses it below.
Code Hook Implementation¶
The first approach to implementing a timer in Styx is to use a Code Hook on
every address. Code Hooks execute before the instruction at their registered
address. By creating a code hook on every address using StyxHook::code(..,
my_hook), the hook will trigger on every instruction. This hook can increment
an internal counter and/or update a register (either memory-mapped or CPU
register) to maintain the timer state. When the timer condition is met, the
code hook has access to the EventController to latch the appropriate timer
IRQ.
Pros:
Timer state is updated every instruction, ensuring hardware accuracy.
Simple to implement.
Works with all timer implementations (memory-mapped registers, CPU registers).
Cons:
Significant performance cost.
Examples:
ppc4xx/styx/processors/ppc/styx-ppc4xx-processor/src/timers.rs
Tick-Based Implementation¶
The second approach is to update timers in the tick() function of a
peripheral or event controller. This method is similar to the code hook approach but executes timer logic less frequently,
usually once every 1000 instructions [1].
Operation¶
At every tick(), the EventController or Peripheral will write the
CPU or memory mapped timer register with the current timer state and latch the
timer IRQ as configured.
A Peripheral’s tick() receives a PeripheralTickCtx which exposes
shared physical memory through ctx.memory, but no CPU registers and no
per-vCPU MMU. Because of this, Peripheral can only write Memory Mapped timer
registers, not CPU registers. Use hooks registered in Peripheral::init() if
CPU state is needed.
Instead of latching, a Peripheral returns the IRQs it wants raised as a
RaisedIrqs and the EventDistributor routes them to the appropriate
vCPU.
Timer Configuration¶
In order to configure the timer, this method may involve reading timer
configuration registers periodically or creating a Memory Write hook on the
MMIO registers used to configure the timers. The tick() function reduces
the performance penalties compared to the Code Hook method (though not entirely)
while maintaining reasonably accurate emulated state.
Time in Styx¶
Critically, the tick() function supplies timing information for the
previous stride.
A Peripheral receives a GlobalDelta in ctx.delta, where
simulated_time is the number of cycles the processor executed this round
and wall_time is the elapsed wall clock time. An EventController
receives the per-vCPU Delta, where count is the number of instructions
executed and time is the elapsed wall clock time. Note that wall_time
and delta.time are wall clock time (real-world duration), not any
processor-specific time. The cycle and instruction counts can be used to
properly calculate the progress to the next timer trigger ensuring each timer
interrupt is raised after the same number of instructions.
Optionally, this method can be used without writing to any timer registers, if the target program doesn’t read from the timer and only operates off of the IRQs.
Pros and Cons¶
Pros:¶
Hardware accuracy is maintained to a reasonable degree (registers are updated).
Works with all timer implementations (memory-mapped registers, CPU registers).
More performant than the code hook approach.
Cons:¶
Performance cost from register/memory operations approximately every 1000 instructions.
Target timer state not up to date between ticks.
Requires scaling logic for tick-based implementation.
Examples:¶
kinetis21/styx/processors/arm/styx-kinetis21-processor/src/systick.rsNote: this implementation does not write to the actual register, it only triggers IRQs.
Memory Read Hook Implementation¶
Another approach is to use tick() to maintain an internal counter of the
target timer status and create a Memory Read Hook on the timer register for
target operations. This method only works with timers that have memory-mapped
registers. It offers better performance than writing to memory/registers on
every tick() since updates occur only when the target actually reads the
register. The timer IRQ can be raised at the appropiate time in the tick()
implementation.
Pros:
Good performance, as memory/register is written only when needed by the target.
Cons:
Only applicable to memory-mapped registers.
Target timer state may not be up to date between ticks.
Requires scaling logic for tick-based implementation.
Timers are only updated via memory hooks, so GDB and Styx
read_memory()operations will not reflect current timer register status.
Register Read Hook Implementation¶
This approach adds a Register Read hook to the timer register. It is similar to
the Memory Read Hook solution but relies upon the Register Read Hook, which is
currently exclusive to the Pcode Backend. This method has similar advantages and
disadvantages to the Memory Read Hook approach. The timer IRQ can be raised at
the appropiate time in the tick() implementation.
Pros:
Good performance, as memory/register is written only when needed by the target.
Cons:
Only applicable to CPU timer registers.
System status may not be up to date between ticks.
Requires scaling logic for tick-based implementation.
Timers are only updated via memory hooks, so GDB and Styx
read_memory()operations will not reflect current timer register status.Requires the Pcode Backend.
PC Manager Implementation¶
Another approach is to add timer logic directly in the PC manager of the Pcode Backend. This is a specialized solution that requires your processor to use the Pcode Backend. It offers good performance since the PC manager is already executing (no additional hooks or branches required). This method updates every instruction and can leverage RegisterHandlers to provide completely accurate timer register values for CPU registers. Memory-mapped registers will require either a memory read hook or direct memory writes.
Pros:
Good performance.
Target emulation accurate (updated every instruction).
CPU and Memory Mapped registers.
Cons:
Strongly tied to the Pcode Backend.
Memory-mapped registers still require a memory write.
Async Implementation¶
In this approach we use the available async runtime to run a timer that is
triggered after a duration. Once the async timer is triggered, a syncronization
method is used (AtomicBool, channel, etc.) to raise the IRQ on the next
tick().
This method is not recommended because IRQs will not be triggered deterministrically which makes debugging difficult. This also causes certain plugins/executors to alter emulation by slowing emulation down, thus increasing timer IRQ frequency.
With a sufficently stable processor, async based timer implementations may be a useful alternative in performance critical testing.
Pros:
Easy to implement.
Performant.
Cons:
Indeterministic execution.
Examples:
bfin/styx/processors/bfin/styx-blackfin-processor/src/timers/mod.rs