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