CPU event handlers
This is an advanced topic. Most programs will never need to implement a custom CPU event handler.
The Chombit microprocessor responds to CPU events by temporarily jumping to an event handler memory address, which contains assembly language instructions that perform some action. Afterwards, the handler can optionally restore the entire CPU state and resume executing the original program. The memory address of the handlers are stored in io::cpu_fail_handler, io::cpu_trace_handler, and io::cpu_request_handler.
Hybrix debugger handlers
When running your program in the Hybrix website development environment, the CPU event handlers can be intercepted by the debugger:
-
io::cpu_fail_handler: The debugger pauses the program before the event is triggered. If program is unpaused, the handler will be invoked. The Hybrix framework kernel's handler displays an error message and terminates the program. -
io::cpu_trace_handler: The debugger writes the message to the website console, then triggers the CPU event. The Hybrix framework kernel's handler simply returns control to the program. -
io::cpu_request_handler: If the request number is a debugger function, the action will be performed, then control is returned to the program without triggering the CPU event at all;io::cpu_request_handleris bypassed entirely. Otherwise, theio::cpu_request_handleris invoked normally. The Hybrix framework kernel's handler doesn't implement anyrequestfunctions; it simply terminates the program with an error.
Event handler invocation
In order to manage critical problems such as a stack fault, the handler gets invoked without reliance on the CPU stack. Instead, the CPU state is saved in four special-purpose MMIO locations, in the following sequence:
- A parameter distinguishing the type of event is written to
io::cpu_event_value. - The CPU flags byte is written to
io::cpu_event_flags. - The
ipregister is written toio::cpu_event_ip. In the case of a CPU fault,cpu_event_ippoints to the instruction that failed; in all other cases, it points to the subsequent instruction. - The
fpregister is written toio::cpu_event_fp. - The WF and SF flags are cleared, so their states will not affect the event handler.
- The
ipregister then jumps to the address read fromio::cpu_fail_handler,io::cpu_trace_handler, orio::cpu_request_handler.
The steps above take 3 clock cycles in total: one to prepare the values to be saved, one to write them to MMIO, and one to read the handler address, clear the control flags, and jump.
Writing an event handler
The code example below illustrates a typical recipe for implementing an event handler:
@example_event_handler:
move fp, $d0_0000
# check what kind of event we need to handle
compare i:28, 123 # $d0_001c = cpu_event_value
. . .
(your code here)
. . .
# restore the original CPU state
move fp, sp
push fp # saved sp
move fp, $d0_0000
push b:19 # $d0_0013 = cpu_event_flags
push i:20 # $d0_0014 = cpu_event_ip
push i:24 # $d0_0018 = cpu_event_fp
pop fp
with context
pop ip # pop ip, flags, sp (then jump)
Important points:
- The
$d0_0080–$d0_00bfmemory range is regular RAM designated as the kernel scratchpad. - By pointing the
fpregister to$d0_0000, the MMIO locations can be accessed directly without need forload/store. - The with context
pop ipinstruction enables the CPU state to be completely restored.
I/O definitions
module io
. . .
# Custom handling for "fail", "trace", "request" and other CPU events
var cpu_event_flags: byte located at $d0_0013
var cpu_event_ip: int located at $d0_0014
var cpu_event_fp: int located at $d0_0018
var cpu_event_value: int located at $d0_001c
var cpu_fail_handler: int located at $d0_0020
var cpu_trace_handler: int located at $d0_0024
var cpu_request_handler: int located at $d0_0028
. . .
end module