Processor Emulation
The processor emulation capability in TEMU is based on an instruction level simulation engine powered by LLVM. At present the processor emulation is interpreted, but does reach hundreds of MIPS (Millions of emulated Instructions Per wall-clock Second) on modern hardware.
The processor models provide static instruction timing which is useful in order to predict performance in certain cases. Timing does not take pipeline dependencies into account, so there is no simulation of branch prediction, pipeline stalls or superscalar execution. It is possible to insert user provided cache models in the memory space object such models can add more timing accuracy to the emulation at the expense of performance.
A processor object can be embedded inside a machine object. The machine objects can be used in order to control multiple processors as a group. This is the primary way that multi-core, and multi-computer systems are supported.
When running a machine with multiple processors, the processors are temporally decoupled, and the machine synchronizes the processors at various time points. These time points include a mandatory time-quanta, and synchronized events.
Running a CPU or Machine
For a simulator it is important to understand the flow and state transitions of a CPU core and when it terminates and the distinction between stepping and running.
CPU States
A CPU can be in three different states:
-
Nominal
-
Idling
-
Halted
The nominal state indicates that the CPU is executing instructions.
Idling indicates that the CPU is not executing instructions but is advancing the CPU cycle counter and event queue. Idle mode is exited when IRQs are raised or the CPU is reset. Idle mode normally indicates either an idle loop (unconditional branch to itself) or power-down mode. In both cases, the CPU will simply forward time to the next event (or if no events are pending return from the core).
Halted mode indicates that the CPU is halted as would happen when a critical error is detected, on the SPARC the halted state corresponds to the SPARC error mode. When entering halted state the CPU core will return and the CPU will remain in halted state until it is reset. It is possible to run a halted core to advance time and execute events (e.g. if there are death event handlers or watchdogs that should reset the system).
CPU Exits
A CPU can exit (return from its step / run function) due to a number of reasons.
-
Normal exit (step or cycle counter reach its target time)
-
Transition to halted mode
-
Breakpoint / watchpoint hit
-
Early exit (other reason which can be forced by event handlers or others)
Stepping
When a CPU is stepping (e.g. calling its step function), it will execute a fixed number of instructions. When a CPU enters idle mode, a step is seen as advancing to the next event. Except for the event advancement in idle mode, a step can be seen as executing a single instruction. Stepping is not normally done in a simulator, but is often done while debugging software. When the core is in error mode, a step will not advance time however.
When a machine is stepping, it is not the machine that is stepping, but one of its CPUs, thus the step
command takes an optional parameter cpuidx
which can be set when one do not wish to step the default CPU which is the current CPU.
As the "current" CPU can change (e.g. when the CPU finishes its scheduling quanta), it is advisable to set this parameter.
Running
When a CPU is running, it is set to run UINT64_MAX steps, and a special end-run-event is posted at the target cycle time. When this end-run event is triggered, the core will stop executing after any stacked events have finished executing. Running a CPU is done in cycles (or in seconds, which is converted to an exact number of cycles).
When machines are run, the CPUs part of the machine will all advance for the time given to the machine. In this case, it is not possible to specify time in the unit "cycles" as each CPU in a machine may have a different clock frequency. Instead, the machine is executed for a given number of nanoseconds.
Instruction Behavior
The emulator is interpreted (at present), in the current release an instruction is executed in the following order:
-
Fetch and decode instruction (may call fetch memory access handler)
-
Execute instruction semantics (may call memory access handlers, raise traps etc)
-
Increment program, step and cycle counters
-
Execute any pending events
This means that in an I/O model, if the model wants to terminate
with an emergency stop, the step, cycle and program counters
will not be updated.
To leave the core after this, you need to post a stacked event, which will be executed in step 4.
In particular you need to be careful with raiseTrap() and the exitEmuCore() functions defined in the CPU interface.
Although, the raiseTrap() function will in general adjust the PC, step and cycle counters and also ensure pending events are executed, the exact results of doing this in a memory handler and an event handler does obviously have different behavior.
|
If a memory event handler calls enterIdleMode() , this will be entered after the program, step and cycle counters have been incremented.
Thus, if you write to a power-down register, then the CPU will continue at the next instruction when returning from the interrupt handler that wakes the CPU.
If the power-down system needs to be triggered at the current PC, then you need to use exitEmuCore() .
|
Event System
A processor is the primary keeper of time in the emulator. The processor keeps track of the progress of time, by maintaining a cycle counter.
Some device models need to be able to post timed events on the CPUs event queue to simulate items such as DMA and bus message timing.
There is a standard API for event posting on CPU models. Timed events are fired at their expiration time, while stack posted events goes on a special event stack. The event will then be triggered after the current instruction has finished executing.
Events are tracked by an event ID which is associated with a function/object pair. Meaning that each object (e.g. an UART instance may have the same function posted as an event), however a single object should not post the same function multiple times while the event is still in-flight. Re-posting an event while in flight, will result in a the existing event being descheduled automatically and warning printed in the log.
Multi-Core Emulation and Events
Multi-core processors are simulated by creating a machine object, and adding multiple CPU cores to it, and associating a single memory space object which all the cores (in fact, a non shared memory multi-computer system is a machine object with separate memory spaces for each CPU).
Multi-core processors are temporally decoupled and emulated by scheduling each core for a number of cycles on a single CPU (this window is called a CPU scheduling quanta). This method guarantees full determinism even when emulating multi-core processors. The quanta length can be configured as low as a single nanosecond for the fastest processor, but this has a significant performance impact. The best value need to be experimentally determined for the relevant application, but something corresponding to 10 kCycles is probably a good start. Note that too long quantum means that Inter-Processor Interrupts (IPIs) and spinlocks may have a long response time.
Also, IPIs are typically raised as soon as the destination CPU is scheduled, this is either at the start of the next quanta (i.e. later in time) in case the destination CPU already being scheduled, or at the start of the current quanta (earlier in time) in case the destination CPU has not yet been scheduled.
Set the time quanta to 10 kCycles initially, this is a good starting point. This is also the default value. |
The quanta length is set in whole nanoseconds. The quanta property can be set in the machine state object, and it will automatically be converted to cycles based on the individual processor’s clock frequency. Thus it is even possible to provide different CPUs with different clock frequencies.
The fact that processors are temporally decoupled does have impact on low level multi-threaded code, such as spin locks and lock free algorithms, where a CPU-core may have to wait excessively long for a spin lock if the owning CPU finishes its quanta before releasing the lock. However, it also ensures that the emulation is deterministic. |
IPIs are delivered at the start of either the current quanta or the next depending on whether the destination CPU has already been scheduled. |
It is possible to manipulate the machine’s time-quanta during execution. |
One variant for debugging locking issues is to run with a longer quanta at first and when approaching the locking code, reduce the quanta size to home in on the bug.
As the CPUs usually do not agree on time, the quanta length has an impact on the event system. When posting an event, it normally goes to a single CPU. However, in some cases it is needed to have the different cores agree on time. For these cases, the machine object allows for the posting of synchronized events. These will ensure that the CPU scheduling window is aborted before the quanta is finished and all processor will agree on time (within the granularity of the worst case instruction time).
Synchronized events should always be posted with a firing time in at least the next CPU scheduling quanta. If it isn’t the event will be delayed until the next quanta and a warning noted in the log. |
CPU Interface
The CPU interface provides a way to run processor cores and to access CPU state such as registers and the program counter.
typedef struct temu_CpuIface {
void (*reset)(void *Cpu, int ResetType);
uint64_t (*run)(void *Cpu, uint64_t Cycles);
uint64_t (*step)(void *Cpu, uint64_t Steps);
void __attribute__((noreturn))
(*raiseTrap)(void *Obj, int Trap);
void (*enterIdleMode)(void *Obj);
void __attribute__((noreturn))
(*exitEmuCore)(void *Cpu, temu_CpuExitReason Reason);
uint64_t (*getFreq)(void *Cpu);
temu_CpuState (*getState)(void *Cpu);
void (*setPc)(void *Cpu,
uint64_t Pc);
uint64_t (*getPc)(void *Cpu);
void (*setGpr)(void *Cpu,
int Reg,
uint64_t Value);
uint64_t (*getGpr)(void *Cpu,
unsigned Reg);
void (*setFpr32)(void *Cpu,
unsigned Reg,
uint32_t Value);
uint32_t (*getFpr32)(void *Cpu,
unsigned Reg);
void (*setFpr64)(void *Cpu,
unsigned Reg,
uint64_t Value);
uint64_t (*getFpr64)(void *Cpu,
unsigned Reg);
uint64_t (*getSpr)(void *Cpu,
unsigned Reg);
int (*getRegId)(void *Cpu,
const char *RegName);
uint32_t (*assemble)(void *Cpu,
const char *AsmStr);
const char* (*disassemble)(void *Cpu,
uint32_t Instr);
void (*enableTraps)(void *Cpu);
void (*disableTraps)(void *Cpu);
void (*invalidateAtc)(void *Obj,
uint64_t Addr,
uint64_t Pages,
uint32_t Flags);
} temu_CpuIface;