From 0d654032bbf513937a2cd14cd5ee7883c7661045 Mon Sep 17 00:00:00 2001 From: Ian Pickering Date: Wed, 13 Feb 2019 14:42:24 -0800 Subject: [PATCH] Add module for interrupt request (IRQ) handling --- crt0.s | 56 +++++++++++++++ examples/irq.rs | 146 +++++++++++++++++++++++++++++++++++++ src/bios.rs | 17 ++--- src/io.rs | 1 + src/io/display.rs | 18 ++++- src/io/irq.rs | 180 ++++++++++++++++++++++++++++++++++++++++++++++ src/io/keypad.rs | 5 +- 7 files changed, 411 insertions(+), 12 deletions(-) create mode 100644 examples/irq.rs create mode 100644 src/io/irq.rs diff --git a/crt0.s b/crt0.s index 9b06b47..53437f1 100644 --- a/crt0.s +++ b/crt0.s @@ -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 diff --git a/examples/irq.rs b/examples/irq.rs new file mode 100644 index 0000000..c38bc81 --- /dev/null +++ b/examples/irq.rs @@ -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)); +} diff --git a/src/bios.rs b/src/bios.rs index cd98d7f..03f6c8e 100644 --- a/src/bios.rs +++ b/src/bios.rs @@ -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")))] diff --git a/src/io.rs b/src/io.rs index 076813b..23e4d47 100644 --- a/src/io.rs +++ b/src/io.rs @@ -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; diff --git a/src/io/display.rs b/src/io/display.rs index deecb88..f924701 100644 --- a/src/io/display.rs +++ b/src/io/display.rs @@ -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 {} } diff --git a/src/io/irq.rs b/src/io/irq.rs new file mode 100644 index 0000000..2f3a9bb --- /dev/null +++ b/src/io/irq.rs @@ -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 = 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 = 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 = 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 = 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; diff --git a/src/io/keypad.rs b/src/io/keypad.rs index 477dcaa..a86c130 100644 --- a/src/io/keypad.rs +++ b/src/io/keypad.rs @@ -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)]