From dc2127b2ce9ac2af03703a40733fc8ce222bd3e3 Mon Sep 17 00:00:00 2001 From: Ian Pickering Date: Wed, 13 Feb 2019 02:21:47 -0800 Subject: [PATCH 1/3] Fix incorrect opcode of VBlankIntrWait BIOS function It should be `swi 0x05` instead of `swi 0x04`. --- src/bios.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bios.rs b/src/bios.rs index 111aa99..cd98d7f 100644 --- a/src/bios.rs +++ b/src/bios.rs @@ -225,7 +225,7 @@ pub fn vblank_interrupt_wait() { #[cfg(all(target_vendor = "nintendo", target_env = "agb"))] { unsafe { - asm!(/* ASM */ "swi 0x04" + asm!(/* ASM */ "swi 0x05" :/* OUT */ // none :/* INP */ // none :/* CLO */ "r0", "r1" // both set to 1 by the routine From 0d654032bbf513937a2cd14cd5ee7883c7661045 Mon Sep 17 00:00:00 2001 From: Ian Pickering Date: Wed, 13 Feb 2019 14:42:24 -0800 Subject: [PATCH 2/3] 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)] From d15168068bcad6dc0e7fd3f744ae0b148bfbc03a Mon Sep 17 00:00:00 2001 From: Ian Pickering Date: Wed, 13 Feb 2019 18:27:27 -0800 Subject: [PATCH 3/3] Add __IRQ_HANDLER symbol to hello_magic example The hello_magic example does not depend on the gba crate, but the crt0 now assumes that the symbol for the interrupt handler which is defined in it will be present, as interrupts ought to be handled in some manner. If neither the symbol or the crate are added then the linker will give an error, but if anything in the gba crate is used also then the symbol will be brought in, so defining it manually also would cause a duplicate definition error. In the future something like cortex-m-rt's `exception!` macro could be used to better document how to define this symbol (all their examples depend on at least one symbol from their runtime library, so they don't have this problem). --- examples/hello_magic.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/examples/hello_magic.rs b/examples/hello_magic.rs index 75eb6bc..8aa3c55 100644 --- a/examples/hello_magic.rs +++ b/examples/hello_magic.rs @@ -16,3 +16,8 @@ fn main(_argc: isize, _argv: *const *const u8) -> isize { loop {} } } + +#[no_mangle] +static __IRQ_HANDLER: extern "C" fn() = irq_handler; + +extern "C" fn irq_handler() {}