Add module for interrupt request (IRQ) handling

This commit is contained in:
Ian Pickering 2019-02-13 14:42:24 -08:00
parent dc2127b2ce
commit 0d654032bb
7 changed files with 411 additions and 12 deletions

56
crt0.s
View file

@ -6,6 +6,11 @@ __start:
.fill 188, 1, 0
.Linit:
@ Set address of user IRQ handler
ldr r0, =MainIrqHandler
ldr r1, =0x03FFFFFC
str r0, [r1]
@ set IRQ stack pointer
mov r0, #0x12
msr CPSR_cf, r0
@ -31,4 +36,55 @@ __start:
@ jump to user code
ldr r0, =main
bx r0
.arm
.global MainIrqHandler
.align 4, 0
MainIrqHandler:
@ Load base I/O register address
mov r2, #0x04000000
add r2, r2, #0x200
@ Save IRQ stack pointer and IME
mrs r0, spsr
ldrh r1, [r2, #8]
stmdb sp!, {r0-r2,lr}
@ Disable all interrupts by writing to IME
mov r0, #0
strh r0, [r2, #8]
@ Acknowledge all received interrupts that were enabled in IE
ldr r3, [r2, #0]
and r0, r3, r3, lsr #16
strh r0, [r2, #2]
@ Switch to system mode
mrs r2, cpsr
bic r2, r2, #0x1F
orr r2, r2, #0x1F
msr cpsr_cf, r2
@ Jump to user specified IRQ handler
ldr r2, =__IRQ_HANDLER
ldr r1, [r2]
stmdb sp!, {lr}
adr lr, MainIrqHandler_Return
bx r1
MainIrqHandler_Return:
ldmia sp!, {lr}
@ Switch to IRQ mode
mrs r2, cpsr
bic r2, r2, #0x1F
orr r2, r2, #0x92
msr cpsr_cf, r2
@ Restore IRQ stack pointer and IME
ldmia sp!, {r0-r2,lr}
strh r1, [r2, #8]
msr spsr_cf, r0
@ Return to BIOS IRQ handler
bx lr
.pool

146
examples/irq.rs Normal file
View file

@ -0,0 +1,146 @@
#![no_std]
#![feature(start)]
use gba::{
io::{
display::{DisplayControlSetting, DisplayMode, DisplayStatusSetting, DISPCNT, DISPSTAT},
irq::{self, IrqEnableSetting, IrqFlags, BIOS_IF, IE, IME},
keypad::read_key_input,
timers::{TimerControlSetting, TimerTickRate, TM0CNT_H, TM0CNT_L, TM1CNT_H, TM1CNT_L},
},
vram::bitmap::Mode3,
Color,
};
const BLACK: Color = Color::from_rgb(0, 0, 0);
const RED: Color = Color::from_rgb(31, 0, 0);
const GREEN: Color = Color::from_rgb(0, 31, 0);
const BLUE: Color = Color::from_rgb(0, 0, 31);
const YELLOW: Color = Color::from_rgb(31, 31, 0);
const PINK: Color = Color::from_rgb(31, 0, 31);
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
loop {}
}
fn start_timers() {
let init_val: u16 = u32::wrapping_sub(0x1_0000, 64) as u16;
const TIMER_SETTINGS: TimerControlSetting = TimerControlSetting::new().with_overflow_irq(true).with_enabled(true);
TM0CNT_L.write(init_val);
TM0CNT_H.write(TIMER_SETTINGS.with_tick_rate(TimerTickRate::CPU1024));
TM1CNT_L.write(init_val);
TM1CNT_H.write(TIMER_SETTINGS.with_tick_rate(TimerTickRate::CPU64));
}
#[start]
fn main(_argc: isize, _argv: *const *const u8) -> isize {
DISPCNT.write(DisplayControlSetting::new().with_mode(DisplayMode::Mode3).with_bg2(true));
Mode3::clear_to(BLACK);
// Set the IRQ handler to use.
irq::set_irq_handler(irq_handler);
// Enable all interrupts that are set in the IE register.
IME.write(IrqEnableSetting::UseIE);
// Request that VBlank, HBlank and VCount will generate IRQs.
const DISPLAY_SETTINGS: DisplayStatusSetting = DisplayStatusSetting::new()
.with_vblank_irq_enable(true)
.with_hblank_irq_enable(true)
.with_vcounter_irq_enable(true);
DISPSTAT.write(DISPLAY_SETTINGS);
// Start two timers with overflow IRQ generation.
start_timers();
loop {
let this_frame_keys = read_key_input();
// The VBlank IRQ must be enabled at minimum, or else the CPU will halt
// at the call to vblank_interrupt_wait() as the VBlank IRQ will never
// be triggered.
let mut flags = IrqFlags::new().with_vblank(true);
// Enable interrupts based on key input.
if this_frame_keys.a() {
flags = flags.with_hblank(true);
}
if this_frame_keys.b() {
flags = flags.with_vcounter(true);
}
if this_frame_keys.l() {
flags = flags.with_timer0(true);
}
if this_frame_keys.r() {
flags = flags.with_timer1(true);
}
IE.write(flags);
// Puts the CPU into low power mode until a VBlank IRQ is received. This
// will yield considerably better power efficiency as opposed to spin
// waiting.
gba::bios::vblank_interrupt_wait();
}
}
static mut PIXEL: usize = 0;
fn write_pixel(color: Color) {
unsafe {
Mode3::write_pixel(PIXEL, 0, color);
PIXEL = (PIXEL + 1) % Mode3::SCREEN_PIXEL_COUNT;
}
}
extern "C" fn irq_handler(flags: IrqFlags) {
if flags.vblank() {
vblank_handler();
}
if flags.hblank() {
hblank_handler();
}
if flags.vcounter() {
vcounter_handler();
}
if flags.timer0() {
timer0_handler();
}
if flags.timer1() {
timer1_handler();
}
}
fn vblank_handler() {
write_pixel(BLUE);
// When using `interrupt_wait()` or `vblank_interrupt_wait()`, IRQ handlers must acknowledge
// the IRQ on the BIOS Interrupt Flags register.
BIOS_IF.write(BIOS_IF.read().with_vblank(true));
}
fn hblank_handler() {
write_pixel(GREEN);
BIOS_IF.write(BIOS_IF.read().with_hblank(true));
}
fn vcounter_handler() {
write_pixel(RED);
BIOS_IF.write(BIOS_IF.read().with_vcounter(true));
}
fn timer0_handler() {
write_pixel(YELLOW);
BIOS_IF.write(BIOS_IF.read().with_timer0(true));
}
fn timer1_handler() {
write_pixel(PINK);
BIOS_IF.write(BIOS_IF.read().with_timer1(true));
}

View file

@ -11,6 +11,7 @@
#![cfg_attr(not(all(target_vendor = "nintendo", target_env = "agb")), allow(unused_variables))]
use super::*;
use io::irq::IrqFlags;
//TODO: ALL functions in this module should have `if cfg!(test)` blocks. The
//functions that never return must panic, the functions that return nothing
@ -184,16 +185,16 @@ pub fn stop() {
/// * The first argument controls if you want to ignore all current flags and
/// wait until a new flag is set.
/// * The second argument is what flags you're waiting on (same format as the
/// IE/IF registers).
/// [`IE`](io::irq::IE)/[`IF`](io::irq::IF) registers).
///
/// If you're trying to handle more than one interrupt at once this has less
/// overhead than calling `halt` over and over.
///
/// When using this routing your interrupt handler MUST update the BIOS
/// Interrupt Flags `0x300_7FF8` in addition to the usual interrupt
/// acknowledgement.
/// Interrupt Flags at [`BIOS_IF`](io::irq::BIOS_IF) in addition to
/// the usual interrupt acknowledgement.
#[inline(always)]
pub fn interrupt_wait(ignore_current_flags: bool, target_flags: u16) {
pub fn interrupt_wait(ignore_current_flags: bool, target_flags: IrqFlags) {
#[cfg(not(all(target_vendor = "nintendo", target_env = "agb")))]
{
unimplemented!()
@ -203,19 +204,19 @@ pub fn interrupt_wait(ignore_current_flags: bool, target_flags: u16) {
unsafe {
asm!(/* ASM */ "swi 0x04"
:/* OUT */ // none
:/* INP */ "{r0}"(ignore_current_flags), "{r1}"(target_flags)
:/* INP */ "{r0}"(ignore_current_flags), "{r1}"(target_flags.0)
:/* CLO */ // none
:/* OPT */ "volatile"
);
}
}
}
//TODO(lokathor): newtype this flag business.
/// (`swi 0x05`) "VBlankIntrWait", VBlank Interrupt Wait.
///
/// This is as per `interrupt_wait(true, 1)` (aka "wait for a new vblank"). You
/// must follow the same guidelines that `interrupt_wait` outlines.
/// This is as per `interrupt_wait(true, IrqFlags::new().with_vblank(true))`
/// (aka "wait for a new vblank"). You must follow the same guidelines that
/// [`interrupt_wait`](interrupt_wait) outlines.
#[inline(always)]
pub fn vblank_interrupt_wait() {
#[cfg(not(all(target_vendor = "nintendo", target_env = "agb")))]

View file

@ -12,6 +12,7 @@ pub mod background;
pub mod color_blend;
pub mod display;
pub mod dma;
pub mod irq;
pub mod keypad;
pub mod sound;
pub mod timers;

View file

@ -133,14 +133,28 @@ pub fn vcount() -> u16 {
}
/// Performs a busy loop until VBlank starts.
///
/// NOTE: This method isn't very power efficient, since it is equivalent to
/// calling "halt" repeatedly. The recommended way to wait for a VBlank or VDraw
/// is to set an IRQ handler with
/// [`io::irq::set_irq_handler`](`io::irq::set_irq_handler`) and using
/// [`bios::vblank_intr_wait`](bios::vblank_interrupt_wait) to sleep the CPU
/// until a VBlank IRQ is generated. See the [`io::irq`](io::irq) module for
/// more details.
pub fn spin_until_vblank() {
// TODO: make this the better version with BIOS and interrupts and such.
while vcount() < VBLANK_SCANLINE {}
}
/// Performs a busy loop until VDraw starts.
///
/// NOTE: This method isn't very power efficient, since it is equivalent to
/// calling "halt" repeatedly. The recommended way to wait for a VBlank or VDraw
/// is to set an IRQ handler with
/// [`io::irq::set_irq_handler`](`io::irq::set_irq_handler`) and using
/// [`bios::vblank_intr_wait`](bios::vblank_interrupt_wait) to sleep the CPU
/// until a VBlank IRQ is generated. See the [`io::irq`](io::irq) module for
/// more details.
pub fn spin_until_vdraw() {
// TODO: make this the better version with BIOS and interrupts and such.
while vcount() >= VBLANK_SCANLINE {}
}

180
src/io/irq.rs Normal file
View file

@ -0,0 +1,180 @@
//! Module containing a wrapper for interrupt request (IRQ) handling.
//!
//! When an interrupt is executed, the CPU will be set to IRQ mode and code
//! execution will jump to the physical interrupt vector, located in BIOS. The
//! BIOS interrupt handler will then save several registers to the IRQ stack
//! pointer and execution will jump to the user interrupt handler starting at
//! `0x0300_7FFC`, in ARM mode.
//!
//! Currently, the user interrupt handler is defined in `crt0.s`. It is set up
//! to execute a user-specified interrupt handler after saving some registers.
//! This handler is declared as a static function pointer on the Rust side, and
//! can be set by using [`set_irq_handler`](irq::set_irq_handler).
//!
//! ## Notes
//! * The interrupt will only be triggered if [`IME`](irq::IME) is enabled, the
//! flag corresponding to the interrupt is enabled on the [`IE`](irq::IE)
//! register, and the "IRQ Enable" flag is set on the register related to the
//! interrupt, which varies. For example, to enable interrupts on VBlank you
//! would set the
//! [`vblank_irq_enable`](io::display::DisplayStatusSetting::vblank_irq_enable)
//! flag on the [`DISPSTAT`](io::display::DISPCNT) register.
//! * If you intend to use [`interrupt_wait`](bios::interrupt_wait) or
//! [`vblank_interrupt_wait`](bios::vblank_interrupt_wait) to wait for an
//! interrupt, your interrupt handler MUST update the BIOS Interrupt Flags at
//! [`BIOS_IF`](irq::BIOS_IF) in addition to the usual interrupt
//! acknowledgement (which is handled for you by the user interrupt handler).
//! This is done by setting the corresponding IRQ flag on
//! [`BIOS_IF`](irq::BIOS_IF) at the end of the interrupt handler.
//! * You can change the low-level details of the interrupt handler by editing
//! the `MainIrqHandler` routine in `crt0.s`. For example, you could declare
//! an external static variable in Rust holding a table of interrupt function
//! pointers and jump directly into one of them in assembly, without the need
//! to write the branching logic in Rust. However, note that the main
//! interrupt handler MUST acknowledge all interrupts received by setting
//! their corresponding bits to `1` in the [`IF`](irq::IF) register.
//! * If you wait on one or more interrupts, be sure at least one of them is
//! able to be triggered or the call to wait will never return.
//! * If you wait on multiple interrupts and those interrupts fire too quickly,
//! it is possible that the call to wait will never return as interrupts will
//! be constantly received before control is returned to the caller. This
//! usually only happens when waiting on multiple timer interrupts with very
//! fast overflow rates.
//!
//! ## Example
//!
//! ```rust
//! extern "C" fn irq_handler(flags: IrqFlags) {
//! if flags.vblank() {
//! // Run drawing logic here.
//!
//! // Acknowledge the IRQ on the BIOS Interrupt Flags register.
//! BIOS_IF.write(BIOS_IF.read().with_vblank(true));
//! }
//! }
//!
//! fn main_loop() {
//! // Set the IRQ handler to use.
//! irq::set_irq_handler(irq_handler);
//!
//! // Handle only the VBlank interrupt.
//! const FLAGS: IrqFlags = IrqFlags::new().with_vblank(true);
//! IE.write(flags);
//!
//! // Enable all interrupts that are set in the IE register.
//! IME.write(IrqEnableSetting::UseIE);
//!
//! // Enable IRQ generation during VBlank.
//! const DISPLAY_SETTINGS: DisplayStatusSetting = DisplayStatusSetting::new()
//! .with_vblank_irq_enable(true);
//! DISPSTAT.write(DISPLAY_SETTINGS);
//!
//! loop {
//! // Sleep the CPU until a VBlank IRQ is generated.
//! bios::vblank_interrupt_wait();
//! }
//! }
//! ```
//!
//! ## Implementation Details
//!
//! This is the setup the provided user interrupt handler in `crt0.s` will do
//! when an interrupt is received, in order. It is based on the _Recommended
//! User Interrupt Handling_ portion of the GBATEK reference.
//!
//! 1. Save the status of [`IME`](irq::IME).
//! 2. Save the IRQ stack pointer and change to system mode to use the user
//! stack instead of the IRQ stack (to prevent stack overflow).
//! 3. Disable interrupts by setting [`IME`](irq::IME) to 0, so other interrupts
//! will not preempt the main interrupt handler.
//! 4. Acknowledge all IRQs that occurred and were enabled in the
//! [`IE`](irq::IE) register by writing the bits to the [`IF`](irq::IF)
//! register.
//! 5. Save the user stack pointer, switch to Thumb mode and jump to the
//! user-specified interrupt handler. The IRQ flags that were set are passed
//! as an argument in `r0`.
//! 6. When the handler returns, restore the user stack pointer and switch back
//! to IRQ mode.
//! 7. Restore the IRQ stack pointer and the status of [`IME`](irq::IME).
//! 8. Return to the BIOS interrupt handler.
use super::*;
newtype!(
/// A newtype over all interrupt flags.
IrqFlags, pub u16
);
impl IrqFlags {
phantom_fields! {
self.0: u16,
vblank: 0,
hblank: 1,
vcounter: 2,
timer0: 3,
timer1: 4,
timer2: 5,
timer3: 6,
serial: 7,
dma0: 8,
dma1: 9,
dma2: 10,
dma3: 11,
keypad: 12,
game_pak: 13,
}
}
/// Interrupt Enable Register. Read/Write.
///
/// After setting up interrupt handlers, set the flags on this register type corresponding to the
/// IRQs you want to handle.
pub const IE: VolAddress<IrqFlags> = unsafe { VolAddress::new(0x400_0200) };
/// Interrupt Request Flags / IRQ Acknowledge. Read/Write.
///
/// The main user interrupt handler will acknowledge the interrupt that was set
/// by writing to this register, so there is usually no need to modify it.
/// However, if the main interrupt handler in `crt0.s` is changed, then the
/// handler must write a `1` bit to all bits that are enabled on this register
/// when it is called.
pub const IF: VolAddress<IrqFlags> = unsafe { VolAddress::new(0x400_0200) };
newtype_enum! {
/// Setting to control whether interrupts are enabled.
IrqEnableSetting = u32,
/// Disable all interrupts.
DisableAll = 0,
/// Enable interrupts according to the flags set in the [`IE`](irq::IE) register.
UseIE = 1,
}
/// Interrupt Master Enable Register. Read/Write.
pub const IME: VolAddress<IrqEnableSetting> = unsafe { VolAddress::new(0x400_0208) };
/// BIOS Interrupt Flags. Read/Write.
///
/// When using either [`interrupt_wait`](bios::interrupt_wait) or
/// [`vblank_interrupt_wait`](bios::vblank_interrupt_wait), the corresponding
/// interrupt handler MUST set the flag of the interrupt it has handled on this
/// register in addition to the usual interrupt acknowledgement.
pub const BIOS_IF: VolAddress<IrqFlags> = unsafe { VolAddress::new(0x0300_7FF8) };
/// A function pointer for use as an interrupt handler.
pub type IrqHandler = extern "C" fn(IrqFlags);
/// Sets the function to run when an interrupt is executed. The function will
/// receive the interrupts that were acknowledged by the main interrupt handler
/// as an argument.
pub fn set_irq_handler(handler: IrqHandler) {
unsafe {
__IRQ_HANDLER = handler;
}
}
extern "C" fn default_handler(_flags: IrqFlags) {}
// Inner definition of the interrupt handler. It is referenced in `crt0.s`.
#[doc(hidden)]
#[no_mangle]
static mut __IRQ_HANDLER: IrqHandler = default_handler;

View file

@ -101,8 +101,9 @@ newtype! {
/// of the interrupt firing.
///
/// NOTE: This _only_ configures the operation of when keypad interrupts can
/// fire. You must still set the `IME` to have interrupts at all, and you must
/// further set `IE` for keypad interrupts to be possible.
/// fire. You must still set the [`IME`](irq::IME) to have interrupts at all,
/// and you must further set [`IE`](irq::IE) for keypad interrupts to be
/// possible.
KeyInterruptSetting, u16
}
#[allow(missing_docs)]