Our emulator needs to have ways to read user inputs, know when a V-Blank is reached, when the LCD enters a certain mode, or when a timer has expired (to be able to have time-related behavior in the code). This is achieved by interrupts.
Note: V-Blank is the period, right after the screen drew a complete frame, and before starting the next frame. This period often is used to change VRAM, calculate enemy movement etc.
Interrupts allow the developer to actually react to certain events (a button press, a timer expiring etc.). So, we have to be able to handle those interrupts. In general, when a interrupt is fired and handled, it will jump to a predefined location (e.g. 0x40 for a V-Blank interrupt) and run the code at that location, before finally returning to it’s original location, through a RET.
RET returns to the last address that is pushed on the stack, then removes the address from the stack. Firing an interrupt, pushes the address of PC + 1 to the stack, then handles the interrupt, finally returns, and proceeds normally.
The interrupts are a bit more complex. There is a general Flag, that enables and disables interrupts in general (usually inside the CPU itself, and directly accessible), it’s called interrupt_enabled in my case. This flag is accessed by the opcodes EI (0xFB) and DI (0xF3).
// EI / DI
case 0xf3: return op_set_ime(interrupts_enabled, 0, pc); break;
case 0xfb: return op_set_ime(interrupts_enabled, 1, pc); break;
Following that, interrupts have a register, that shows that the condition for a interrupt was met (user pressed a button, for example), this register is the Interrupt Flag register – IF at 0xFF0F.
Only having interrupts_enabled == true and the appropriate bit set in the IF isn’t enough to handle a interrupt, though. We still have another register. The Interrupt Enable register – IE at 0xFFFF. Only interrupts that are enabled in this register will be handled, once they are flagged in IF.
Now that we know the general way how interrupts work, we can implement them:
// handle interrupts
if (interrupts_enabled) {
// some interrupt is enabled and allowed
if (readFromMem(0xffff) & readFromMem(0xff0f)) {
// handle interrupts by priority (starting at bit 0 - vblank)
// v-blank interrupt
if ((readFromMem(0xffff) & 1) & (readFromMem(0xff0f) & 1)) {
sp--;
writeToMem(sp, pc >> 8);
sp--;
writeToMem(sp, pc & 0xff);
pc = 0x40;
writeToMem(0xff0f, readFromMem(0xff0f) & ~1);
}
...
As you can see in line 14, we are supposed to turn off the interrupt, after we are done rearranging the PC.
Another important feature of the CPU is the timer. The timer makes it possible to start / stop things after a certain set period. The timer can be run at 4 different frequencies:
4096 Hz 262144 Hz 65536 Hz 16384 Hz
Basically the timer ticks depending on the set frequency. Once the timer overflows its register, a timer interrupt is flagged (that we already implemented the handling of)
void handleTimer(int cycle) {
// set divider
div_clocksum += cycle;
if (div_clocksum >= 256) {
div_clocksum -= 256;
aluToMem(0xff04, 1);
}
// check if timer is on
if ((readFromMem(0xff07) >> 2) & 0x1) {
// increase helper counter
timer_clocksum += cycle * 4;
// set frequency
int freq = 4096; // Hz
if ((readFromMem(0xff07) & 3) == 1) // mask last 2 bits
freq = 262144;
else if ((readFromMem(0xff07) & 3) == 2) // mask last 2 bits
freq = 65536;
else if ((readFromMem(0xff07) & 3) == 3) // mask last 2 bits
freq = 16384;
// increment the timer according to the frequency (synched to the processed opcodes)
while (timer_clocksum >= (4194304 / freq)) {
// increase TIMA
aluToMem(0xff05, 1);
// check TIMA for overflow
if (readFromMem(0xff05) == 0x00) {
// set timer interrupt request
writeToMem(0xff0f, readFromMem(0xff0f) | 4);
// reset timer to timer modulo
writeToMem(0xff05, readFromMem(0xff06));
}
timer_clocksum -= (4194304 / freq);
}
}
}
As you can see, to make this timer work, we need to know how many cycles each operation takes.
Comments