From 502f681c09074bbab421cad25436d53c4fe14c75 Mon Sep 17 00:00:00 2001 From: Lokathor Date: Mon, 13 May 2024 17:52:10 -0600 Subject: [PATCH] asm startup incomplete, but can boot a tiny example. --- .cargo/config.toml | 2 - .github/workflows/ci-builds.yml | 3 + Cargo.toml | 21 +- dump.bat | 2 + examples/do_nothing.rs | 9 + linker_scripts/mono_boot.ld | 28 +-- src/asm_runtime.rs | 360 ++++++++++++++++++++++++++++++++ src/critical_section.rs | 37 ++-- src/gba_cell.rs | 16 ++ src/lib.rs | 91 +++++--- src/mmio.rs | 10 +- src/panic_handlers.rs | 7 + src/per_project_setup.rs | 110 ++++++++++ src/per_system_setup.rs | 39 ++++ 14 files changed, 673 insertions(+), 62 deletions(-) create mode 100644 dump.bat create mode 100644 examples/do_nothing.rs create mode 100644 src/asm_runtime.rs create mode 100644 src/gba_cell.rs create mode 100644 src/panic_handlers.rs create mode 100644 src/per_project_setup.rs create mode 100644 src/per_system_setup.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index 9b9b1ce2..99678165 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -3,12 +3,10 @@ target = "thumbv4t-none-eabi" [unstable] build-std = ["core"] -build-std-features = ["compiler-builtins-weak-intrinsics"] [target.thumbv4t-none-eabi] runner = "mgba-qt" rustflags = [ "-Clinker=arm-none-eabi-ld", "-Clink-arg=-Tlinker_scripts/mono_boot.ld", - "--emit=mir", ] diff --git a/.github/workflows/ci-builds.yml b/.github/workflows/ci-builds.yml index c99bd205..d2791fcc 100644 --- a/.github/workflows/ci-builds.yml +++ b/.github/workflows/ci-builds.yml @@ -34,3 +34,6 @@ jobs: - name: Build The Examples (Link With LLD, strong compiler intrinsics) run: cargo build --examples --target=thumbv4t-none-eabi -Zbuild-std=core -Clink-arg=-Tlinker_scripts/mono_boot.ld + + - name: Build The Crate With No Default Features (build script usage simulation) + run: cargo build --no-default-features diff --git a/Cargo.toml b/Cargo.toml index d0e89852..b31b6d32 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,12 +7,27 @@ edition = "2021" license = "Zlib OR Apache-2.0 OR MIT" [features] -default = ["on_gba", "critical-section", "doc_cfg"] +default = ["on_gba"] # SEE THE CRATE DOCS FOR SAFETY RELATED INFO REGARDING THIS FEATURE. on_gba = [] -# utilize `doc_cfg` where appropriate. requires nightly. +# utilize `doc_cfg` where appropriate. requires nightly. intended mostly for use +# during docs.rs documentation generation. doc_cfg = [] +# Activates the `track_caller` attribute on various functions. Use of the +# `track_caller` attribute on a function adds a "secret" extra argument for the +# `Location` of the caller, which can reduce performance if the function is not +# inlined (meaning `Location` is passed via the stack). This is only needed for +# debugging, and so it's off by default. track_caller = [] +# The assembly runtime's irq handler will take extra steps to allow nested +# interrupts during calls to the Rust interrupt handler, and also the Rust +# interrupt handler will be called with the CPU in System mode, allowing for +# full stack usage. If you do not use this feature then the Rust IRQ handler +# will be called with the CPU in IRQ mode with nested interrupts disabled at the +# CPU level. IRQ mode still has enough stack space to do any *reasonable* IRQ +# handling, so you do NOT normally need to use this unless you need nested +# interrupt support or extremely large stack usage during the handler. +robust_irq_handler = [] [dependencies] voladdress = "1.3.0" @@ -20,6 +35,7 @@ bitfrob = "1" critical-section = { version = "1.1.2", features = [ "restore-state-bool", ], optional = true } +bytemuck = { version = "1.16.0", optional = true } [profile.dev] opt-level = 3 @@ -38,3 +54,4 @@ incremental = false # on the GBA. This is the closest target that docs.rs supports by default and # which *also* supports the `instruction_set` attribute to avoid build errors. targets = ["armv5te-unknown-linux-gnueabi"] +features = ["doc_cfg"] diff --git a/dump.bat b/dump.bat new file mode 100644 index 00000000..eb915322 --- /dev/null +++ b/dump.bat @@ -0,0 +1,2 @@ +cargo build --examples +arm-none-eabi-objdump --headers --disassemble --demangle --architecture=armv4t --no-show-raw-insn -Mreg-names-std target/thumbv4t-none-eabi/debug/examples/do_nothing >target/ex-do_nothing.txt diff --git a/examples/do_nothing.rs b/examples/do_nothing.rs new file mode 100644 index 00000000..b20e9a88 --- /dev/null +++ b/examples/do_nothing.rs @@ -0,0 +1,9 @@ +#![no_std] +#![no_main] + +gba::panic_handler!(empty_loop); + +#[no_mangle] +pub extern "C" fn main() -> ! { + loop {} +} diff --git a/linker_scripts/mono_boot.ld b/linker_scripts/mono_boot.ld index ff0233b8..045323a1 100644 --- a/linker_scripts/mono_boot.ld +++ b/linker_scripts/mono_boot.ld @@ -1,6 +1,6 @@ /* THIS LINKER SCRIPT FILE IS RELEASED TO THE PUBLIC DOMAIN (SPDX: CC0-1.0) */ -ENTRY(__start) +ENTRY(_start) MEMORY { ewram (w!x) : ORIGIN = 0x2000000, LENGTH = 256K @@ -11,7 +11,7 @@ MEMORY { SECTIONS { .text : { /* be sure that the ROM header is the very first */ - *(.text.gba_rom_header); + *(.text._start); *(.text .text.*); . = ALIGN(4); } >rom = 0x00 @@ -22,42 +22,42 @@ SECTIONS { } >rom = 0x00 . = ALIGN(4); - __iwram_position_in_rom = .; + _iwram_position_in_rom = .; .data : { - __iwram_start = ABSOLUTE(.); + _iwram_start = ABSOLUTE(.); *(.data .data.*); *(.iwram .iwram.*); . = ALIGN(4); - __iwram_end = ABSOLUTE(.); + _iwram_end = ABSOLUTE(.); } >iwram AT>rom = 0x00 . = ALIGN(4); - __ewram_position_in_rom = __iwram_position_in_rom + (__iwram_end - __iwram_start); + _ewram_position_in_rom = _iwram_position_in_rom + (_iwram_end - _iwram_start); .ewram : { - __ewram_start = ABSOLUTE(.); + _ewram_start = ABSOLUTE(.); *(.ewram .ewram.*); . = ALIGN(4); - __ewram_end = ABSOLUTE(.); + _ewram_end = ABSOLUTE(.); } >ewram AT>rom = 0x00 . = ALIGN(4); - __bss_position_in_rom = __ewram_position_in_rom + (__ewram_end - __ewram_start); + _bss_position_in_rom = _ewram_position_in_rom + (_ewram_end - _ewram_start); .bss : { - __bss_start = ABSOLUTE(.); + _bss_start = ABSOLUTE(.); *(.bss .bss.*); . = ALIGN(4); - __bss_end = ABSOLUTE(.); + _bss_end = ABSOLUTE(.); } >iwram - __iwram_word_copy_count = (__iwram_end - __iwram_start) / 4; - __ewram_word_copy_count = (__ewram_end - __ewram_start) / 4; - __bss_word_clear_count = (__bss_end - __bss_start) / 4; + _iwram_word_copy_count = (_iwram_end - _iwram_start) / 4; + _ewram_word_copy_count = (_ewram_end - _ewram_start) / 4; + _bss_word_clear_count = (_bss_end - _bss_start) / 4; /* rust-lld demands we keep the `section header string table` */ .shstrtab 0 : { *(.shstrtab) } diff --git a/src/asm_runtime.rs b/src/asm_runtime.rs new file mode 100644 index 00000000..c3ed7445 --- /dev/null +++ b/src/asm_runtime.rs @@ -0,0 +1,360 @@ +#![allow(unused_macros)] + +//! Assembly runtime and support functions for the GBA. + +// Note(Lokathor): Functions here will *definitely* panic without the `on_gba` +// cargo feature enabled, and so they should all have the `track_caller` +// attribute set whenever the `on_gba` feature is *disabled* + +use crate::gba_cell::GbaCell; + +macro_rules! on_gba_or_unimplemented { + ($($token_tree:tt)*) => { + #[cfg(feature="on_gba")] + { + $($token_tree)* + } + #[cfg(not(feature="on_gba"))] + unimplemented!() + } +} + +/// Inserts a `nop` instruction. +#[inline(always)] +#[cfg_attr(not(feature = "on_gba"), track_caller)] +pub fn nop() { + on_gba_or_unimplemented! { + unsafe { + core::arch::asm! { + "nop", + } + } + } +} + +/// Atomically swap `x` and the 32-bit value stored at `ptr`. +/// +/// ## Safety +/// This both reads and writes `ptr`, so all the usual rules of that apply. +#[inline] +#[cfg_attr(feature = "on_gba", instruction_set(arm::a32))] +#[cfg_attr(not(feature = "on_gba"), track_caller)] +pub unsafe fn swp(mut ptr: *mut u32, x: u32) -> u32 { + on_gba_or_unimplemented! { + let output: u32; + // Note(Lokathor): This won't actually alter the pointer register, but we + // *tell* LLVM that it will because the pointer register can't be used as + // the output of the swapping operation. + #[allow(unused_assignments)] + unsafe { + core::arch::asm! { + "swp {output}, {input}, [{addr}]", + output = lateout(reg) output, + input = in(reg) x, + addr = inlateout(reg) ptr, + } + } + output + } +} + +/// Atomically swap `x` and the 8-bit value stored at `ptr`. +/// +/// ## Safety +/// This both reads and writes `ptr`, so all the usual rules of that apply. +#[inline] +#[cfg_attr(feature = "on_gba", instruction_set(arm::a32))] +#[cfg_attr(not(feature = "on_gba"), track_caller)] +pub unsafe fn swpb(mut ptr: *mut u8, x: u8) -> u8 { + on_gba_or_unimplemented! { + let output: u8; + // Note(Lokathor): This won't actually alter the pointer register, but we + // *tell* LLVM that it will because the pointer register can't be used as + // the output of the swapping operation. + #[allow(unused_assignments)] + unsafe { + core::arch::asm! { + "swpb {output}, {input}, [{addr}]", + output = lateout(reg) output, + input = in(reg) x, + addr = inlateout(reg) ptr, + } + } + output + } +} + +#[cfg(target_feature = "thumb-mode")] +macro_rules! a32_code { + ($($asm_line:expr),+ $(,)?) => { + concat!( + ".code 32\n", + + $( concat!($asm_line, "\n") ),+ , + + ".code 16\n", + ) + } +} +#[cfg(not(target_feature = "thumb-mode"))] +macro_rules! a32_code { + ($($asm_line:expr),+ $(,)?) => { + concat!( + $( concat!($asm_line, "\n") ),+ , + ) + } +} + +/// If `on_gba` is enabled, makes a `global_asm` for the function given. +/// +/// If `on_gba` is disabled, this does nothing. +macro_rules! global_a32_fn { + ( + $name:ident [iwram=true] { + $($asm_line:expr),+ $(,)? + } + ) => { + #[cfg(feature = "on_gba")] + core::arch::global_asm!{ + a32_code! { + concat!(".section .iwram.text.", stringify!($name), ", \"x\" "), + concat!(".global ",stringify!($name)), + concat!(stringify!($name),":"), + $( concat!($asm_line, "\n") ),+ , + ".pool", + } + } + }; + ( + $name:ident [] { + $($asm_line:expr),+ $(,)? + } + ) => { + #[cfg(feature = "on_gba")] + core::arch::global_asm!{ + a32_code! { + concat!(".section .text.", stringify!($name), ", \"x\" "), + concat!(".global ",stringify!($name)), + concat!(stringify!($name),":"), + $( concat!($asm_line, "\n") ),+ , + ".pool", + } + } + }; +} + +macro_rules! while_swapped { + ( + ptr=$ptr:literal, val=$val:literal { + $($asm_line:expr),+ $(,)? + } + ) => { + concat!( + concat!("swp ",$val,", ",$val,", [",$ptr,"]\n"), + + $( concat!($asm_line, "\n") ),+ , + + concat!("swp ",$val,", ",$val,", [",$ptr,"]\n"), + ) + } +} + +macro_rules! with_spsr_held_in { + ($reg:literal, { + $($asm_line:expr),* $(,)? + }) => { + concat!( + concat!("mrs ", $reg, ", SPSR\n"), + $( concat!($asm_line, "\n") ),* , + concat!("msr SPSR, ", $reg, "\n"), + ) + } +} + +macro_rules! set_cpu_control { + // CPSR control bits are: `I F T MMMMM`, and T must always be left as 0. + // * 0b10011: Supervisor (SVC) + // * 0b11111: System (SYS) + (System, irq_masked: false, fiq_masked: false) => { + "msr CPSR_c, #0b00011111\n" + }; + (Supervisor, irq_masked: true, fiq_masked: false) => { + "msr CPSR_c, #0b10010010\n" + }; +} + +macro_rules! when { + ($reg:literal == $op2:literal [label_id=$label:literal] { + $($asm_line:expr),* $(,)? + }) => { + concat!( + concat!("cmp ", $reg, ", ", $op2, "\n"), + concat!("bne ", $label, "f\n"), + $( concat!($asm_line, "\n") ),* , + concat!($label, ":\n"), + ) + }; + ($reg:literal != $op2:literal [label_id=$label:literal] { + $($asm_line:expr),* $(,)? + }) => { + concat!( + concat!("cmp ", $reg, ", ", $op2, "\n"), + concat!("beq ", $label, "f\n"), + $( concat!($asm_line, "\n") ),* , + concat!($label, ":\n"), + ) + }; + ($reg:literal >=u $op2:literal [label_id=$label:literal] { + $($asm_line:expr),* $(,)? + }) => { + concat!( + concat!("cmp ", $reg, ", ", $op2, "\n"), + concat!("bcc ", $label, "f\n"), // cc: Unsigned LT + $( concat!($asm_line, "\n") ),* , + concat!($label, ":\n"), + ) + }; + ($reg:literal <=u $op2:literal [label_id=$label:literal] { + $($asm_line:expr),* $(,)? + }) => { + concat!( + concat!("cmp ", $reg, ", ", $op2, "\n"), + concat!("bhi ", $label, "f\n"), // hi: Unsigned GT + $( concat!($asm_line, "\n") ),* , + concat!($label, ":\n"), + ) + }; +} + +/// Sets `lr` properly and then uses `bx` on the register given. +macro_rules! a32_fake_blx { + (reg=$reg_name:expr, label_id=$label:expr) => { + concat!( + concat!("adr lr, ", $label, "f\n"), + concat!("bx ", $reg_name, "\n"), + concat!($label, ":\n"), + ) + }; +} + +macro_rules! with_pushed_registers { + ($reglist:expr, { + $($asm_line:expr),* $(,)? + }) => { + concat!( + concat!("push ", $reglist, "\n"), + $( concat!($asm_line, "\n") ),* , + concat!("pop ", $reglist, "\n"), + ) + } +} + +global_a32_fn! {_start [] { + "b 1f", + ".space 0xE0", + "1:", + + "mov r12, #0x04000000", + + // Configure WAITCNT to the GBATEK suggested default + "add r0, r12, #0x204", + "ldr r1, =0x4317", + "strh r1, [r0]", + + // TODO: iwram copying + + // TODO: ewram copying + + // TODO: bss zeroing + + // Tell the BIOS about our irq handler + "ldr r0, =_asm_runtime_irq_handler", + "str r0, [r12, #-4]", + + // Note(Lokathor): we do a `bx` instead of a `b` because it saves 4 *entire* + // bytes (!), since `main` will usually be a t32 function and thus usually + // requires a linker shim to call. + "ldr r0, =main", + "bx r0", + + // TODO: should we soft reset or something if `main` returns? +}} + +#[cfg(not(feature = "robust_irq_handler"))] +global_a32_fn! {_asm_runtime_irq_handler [iwram=true] { + // handle MMIO interrupt system + "ldr r0, [r12, #-8] /* read IE_IF */", + "and r0, r0, r0, LSR #16 /* r0 = IE & IF */", + "strh r0, [r12, #-6] /* write IF */", + + // Now the interrupt bits are in r0 as a `u16` + + // handle BIOS interrupt system + "sub r2, r12, #(0x208+8) /* BIOS_IF address */", + "ldrh r1, [r2] /* read the `has_occurred` flags */", + "orr r1, r1, r0 /* activate the new bits, if any */", + "strh r1, [r2] /* update the value */", + + // Get the user handler fn pointer, call it if non-null. + "ldr r1, =USER_IRQ_HANDLER", + "ldr r1, [r1]", + when!("r1" != "#0" [label_id=9] { + with_pushed_registers!("{{r0, lr}}", { + a32_fake_blx!(reg="r1", label_id=1), + }), + }), + + // return to the BIOS + "bx lr", +}} + +#[cfg(feature = "robust_irq_handler")] +global_a32_fn! {_asm_runtime_irq_handler [iwram=true] { + // Suppress IME while this is running. If the user wants to allow for + // interrupts *during* other interrupts they can enable IME in their handler. + "add r12, r0, #0x208", + "mov r3, #0", + while_swapped! { ptr="r12", val="r3" { + // handle MMIO interrupt system + "ldr r0, [r12, #-8] /* read IE_IF */", + "and r0, r0, r0, LSR #16 /* r0 = IE & IF */", + "strh r0, [r12, #-6] /* write IF */", + + // Now the interrupt bits are in r0 as a `u16` + + // handle BIOS interrupt system + "sub r2, r12, #(0x208+8) /* BIOS_IF address */", + "ldrh r1, [r2] /* read the `has_occurred` flags */", + "orr r1, r1, r0 /* activate the new bits, if any */", + "strh r1, [r2] /* update the value */", + + // Get the user handler fn pointer, call it if non-null. + "ldr r1, =USER_IRQ_HANDLER", + "ldr r1, [r1]", + when!("r1" != "#0" [label_id=9] { + with_spsr_held_in!("r2", { + // We have to preserve: + // * r2: spsr + // * r3: old IME value + // * r12: IME address + // * lr: our handler return address + with_pushed_registers!("{{r2, r3, r12, lr}}", { + // Note(Lokathor): LLVM won't ever leave the stack alignment as less + // than 8 so we skip trying to align it to 8 by hand. + set_cpu_control!(System, irq_masked: false, fiq_masked: false), + a32_fake_blx!(reg="r1", label_id=1), + set_cpu_control!(Supervisor, irq_masked: true, fiq_masked: false), + }), + }) + }), + }}, + + // return to the BIOS + "bx lr", +}} + +/// The user-provided interrupt request handler function. +#[no_mangle] +#[cfg(feature = "on_gba")] +pub static USER_IRQ_HANDLER: GbaCell> = + GbaCell::new(None); diff --git a/src/critical_section.rs b/src/critical_section.rs index abd33e6b..4976e2fc 100644 --- a/src/critical_section.rs +++ b/src/critical_section.rs @@ -5,20 +5,33 @@ use critical_section::{set_impl, Impl, RawRestoreState}; use crate::mmio::IME; struct GbaCriticalSection; +#[cfg(feature = "on_gba")] set_impl!(GbaCriticalSection); #[cfg(feature = "on_gba")] unsafe impl Impl for GbaCriticalSection { - /// # Safety - /// This function has no pre-conditions. + /// ## Safety + /// * This function has no pre-conditions. + /// * This uses `IME` to disable interrupts. Technically there's a 2 CPU cycle + /// delay between `IME` being disabled and interrupts actually being unable + /// to run. This function is marked `inline(never)`, so just the time it + /// takes the CPU to return from the function should be enough to prevent + /// any problems. Technically that's "only a hint" though. Even then, any + /// code running in ROM will generally be slow enough for is to not matter + /// just because of ROM access speeds. If your code is running in IWRAM and + /// you wanted to be absolutely paranoid you could insert two calls to + /// [`nop`][crate::asm_runtime::nop] after calling this function. + /// Personally, even then I wouldn't bother. + #[inline(never)] unsafe fn acquire() -> RawRestoreState { let restore = IME.read(); IME.write(false); restore } - /// # Safety - /// This function has no pre-conditions. + /// ## Safety + /// * This function has no pre-conditions. + #[inline] unsafe fn release(restore: RawRestoreState) { IME.write(restore); } @@ -26,17 +39,17 @@ unsafe impl Impl for GbaCriticalSection { #[cfg(not(feature = "on_gba"))] unsafe impl Impl for GbaCriticalSection { - /// # Safety - /// This function will always panic, so I guess you could say it's always - /// safe. + /// ## Safety + /// * This function will always panic. + #[track_caller] unsafe fn acquire() -> RawRestoreState { - panic!() + unimplemented!() } - /// # Safety - /// This function will always panic, so I guess you could say it's always - /// safe. + /// ## Safety + /// * This function will always panic. + #[track_caller] unsafe fn release(restore: RawRestoreState) { - panic!() + unimplemented!() } } diff --git a/src/gba_cell.rs b/src/gba_cell.rs new file mode 100644 index 00000000..102bc634 --- /dev/null +++ b/src/gba_cell.rs @@ -0,0 +1,16 @@ +//! Provides the [`GbaCell`] type. + +/// A "cell" type suitable to hold a global on the GBA. +#[repr(transparent)] +pub struct GbaCell(core::cell::UnsafeCell); + +#[cfg(feature = "on_gba")] +#[cfg_attr(feature = "doc_cfg", doc(cfg(feature = "on_gba")))] +unsafe impl Sync for GbaCell {} + +impl GbaCell { + /// Constructs a new cell with the value given + pub const fn new(t: T) -> Self { + Self(core::cell::UnsafeCell::new(t)) + } +} diff --git a/src/lib.rs b/src/lib.rs index 66072722..271dbe66 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ #![no_std] +#![cfg_attr(not(feature = "on_gba"), allow(unused))] #![warn(missing_docs)] #![warn(unsafe_op_in_unsafe_fn)] #![cfg_attr(feature = "doc_cfg", feature(doc_cfg))] @@ -11,41 +12,73 @@ //! type system by passing around zero-sized token types, try the //! [agb](https://docs.rs/agb) crate instead. //! -//! # Crate Features +//! # This Is Intended For The Game Boy Advance //! -//! * `on_gba` (**Default:** enabled): When this feature is used, the crate -//! assumes that you're building the crate for, and running the code on, the -//! Game Boy Advance. The build target is expected to be `thumbv4t-none-eabi` -//! or `armv4t-none-eabi`, any other targets may have a build error. Further, -//! the specific device is assumed to be the GBA, which is used to determine -//! the safety of all direct hardware access using MMIO. This feature is on by -//! default because the primary purpose of this crate is to assist in the -//! building of GBA games, but you *can* disable the feature and build the -//! crate anyway, such as if you want to use any of the crate's data type -//! definitions within a build script on your host machine. When this feature -//! is disabled, GBA specific internals of functions *may* be replaced with -//! runtime panics when necessary. How much of this crate actually works on -//! non-GBA platforms is **not** covered by our SemVer! -//! * `critical-section` (**Default:** enabled): activates an implementation to -//! support for the [critical-section](https://docs.rs/critical-section) -//! crate. -//! * `track_caller` (**Default:** disabled): Causes some functions that can -//! panic to add the [track_caller][ref-track-caller] attribute. The attribute -//! adds a "secret" function argument to pass the `Location` of the call, so -//! it can reduce performance when a function is not inlined (more data has to -//! be pushed onto the stack per function call). Suggested for debugging only. +//! When the `on_gba` crate feature is used, the crate assumes that you're +//! building the crate for, and also running the code on, the Game Boy Advance. +//! The build target is expected to be `thumbv4t-none-eabi` or +//! `armv4t-none-eabi`, and any other targets might have a build error. Further, +//! the specific device you run the code on is assumed to be the GBA (or a GBA +//! emulator). These facts are used by the `unsafe` code in this crate. //! -//! [ref-track-caller]: -//! https://doc.rust-lang.org/reference/attributes/codegen.html#the-track_caller-attribute +//! This crate feature is **on by default** because the primary purpose of this +//! crate is to assist in the building of GBA games, but you *can* disable the +//! feature and build the crate anyway. How much of this crate actually works on +//! non-GBA platforms is **not** covered by our SemVer! Building and using the +//! crate without the `on_gba` feature is intended for non-GBA code that wants +//! the data type definitions the crate provides, such as a build script running +//! on your development machine. Without the `on_gba` feature enabled, any GBA +//! specific functions that "don't make sense" outside of a GBA context (such as +//! functions using inline assembly) will just be `unimplemented!()`, and +//! calling them will trigger a panic. //! -//! # Additional Information -//! -//! * Development Environment Setup -//! * Project Setup -//! * Learning GBA Programming +//! If you're not familiar with GBA programming some explanations are provided +//! on separate pages: +//! * [Per System Setup][`per_system_setup`] +//! * [Per Project Setup][`per_project_setup`] +pub mod asm_runtime; +pub mod gba_cell; pub mod mmio; +pub mod panic_handlers; +pub mod per_project_setup; +pub mod per_system_setup; #[cfg(feature = "critical-section")] #[cfg_attr(feature = "doc_cfg", doc(cfg(feature = "critical-section")))] pub mod critical_section; + +/// Declares one of the functions in the [`panic_handlers`] module to be the +/// handler for your program. +/// +/// Valid inputs are the name of any of the functions in that module: +/// * [`empty_loop`][crate::panic_handlers::empty_loop] +/// +/// There's no special magic here, it just saves you on typing it all out +/// yourself. +#[macro_export] +macro_rules! panic_handler { + ($i:ident) => { + #[panic_handler] + fn panic_handler(info: &core::panic::PanicInfo) -> ! { + gba::panic_handlers::$i(info) + } + }; +} + +/// A color value. +/// +/// This is a bit-packed linear RGB color value with 5 bits per channel: +/// ```text +/// 0bX_BBBBB_GGGGG_RRRRR +/// ``` +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct Color(pub u16); + +#[cfg(feature = "bytemuck")] +#[cfg_attr(feature = "doc_cfg", doc(cfg(feature = "bytemuck")))] +unsafe impl bytemuck::Zeroable for Color {} +#[cfg(feature = "bytemuck")] +#[cfg_attr(feature = "doc_cfg", doc(cfg(feature = "bytemuck")))] +unsafe impl bytemuck::Pod for Color {} diff --git a/src/mmio.rs b/src/mmio.rs index d1455378..d151e3fc 100644 --- a/src/mmio.rs +++ b/src/mmio.rs @@ -1,13 +1,14 @@ //! Definitions for Memory-mapped IO (hardware control). -use voladdress::VolAddress; +#[allow(unused_imports)] +use voladdress::{Safe, Unsafe, VolAddress}; /// "safe on GBA", which is either Safe or Unsafe according to the `on_gba` /// cargo feature. #[cfg(feature = "on_gba")] -type SOGBA = voladdress::Safe; +type SOGBA = Safe; #[cfg(not(feature = "on_gba"))] -type SOGBA = voladdress::Unsafe; +type SOGBA = Unsafe; type PlainAddr = VolAddress; @@ -19,4 +20,7 @@ type PlainAddr = VolAddress; /// pending until this is again set to `true`. /// /// This defaults to `false`. +/// +/// Technically there's a two CPU cycle delay between this being written and +/// interrupts actually being enabled/disabled. In practice, it doesn't matter. pub const IME: PlainAddr = unsafe { VolAddress::new(0x0400_0208) }; diff --git a/src/panic_handlers.rs b/src/panic_handlers.rs new file mode 100644 index 00000000..d9f37da1 --- /dev/null +++ b/src/panic_handlers.rs @@ -0,0 +1,7 @@ +//! Various panic handler functions that you might find useful. + +/// Just performs an empty `loop` +#[inline] +pub fn empty_loop(_: &core::panic::PanicInfo) -> ! { + loop {} +} diff --git a/src/per_project_setup.rs b/src/per_project_setup.rs new file mode 100644 index 00000000..101f3780 --- /dev/null +++ b/src/per_project_setup.rs @@ -0,0 +1,110 @@ +//! Explanation of suggested setup per GBA project. +//! +//! ## Cargo Configuration +//! +//! We need to use cargo's unstable `build-std` ability to build the +//! `thumbv4t-none-eabi` target, and we'll want to have that be the default +//! target for basically everything we do. +//! +//! You'll want a GBA emulator for testing your GBA rom. I use +//! [mGBA](https://mgba.io/), but others also have success with +//! [No$GBA](https://www.nogba.com/). Whatever you pick, you should set it as +//! the `runner` under `[target.thumbv4t-none-eabi]` +//! +//! As discussed in the [Per System Setup][crate::per_system_setup], only the +//! GNU `objdump` will be able to correctly dump a GBA program. It's possible to +//! use the LLVM linker and only GNU for `objdump`, but the GNU `objdump` won't +//! be able to show all info if you mix utilities like this, because (i guess?) +//! the GNU `objdump` doesn't understand LLVM linker info format. Using the GNU +//! linker will make sure you can see all the debug info. I've never noticed a +//! linking speed difference between LLVM and GNU on the GBA, programs just +//! don't get that big. Put `-Clinker=arm-none-eabi-ld` into the `rustflags` for +//! our target and we'll get the GNU linker. +//! +//! Regardless of if you're using the LLVM or GNU linker, you'll need a linker +//! script. The [Github Repo][github_script] for this project has a usable +//! linker script. You should save this file into your own project, such as in a +//! folder called `linker_scripts/` under the name `mono_boot.ld`. Then we add a +//! `rustflags` argument with `-Clink-arg=-T`, using the script's path +//! relative to our project root. +//! +//! [github_script]: +//! https://github.com/rust-console/gba/blob/main/linker_scripts/mono_boot.ld +//! +//! Your `.cargo/config.toml` should probably look like the following: +//! ```toml +//! [build] +//! target = "thumbv4t-none-eabi" +//! +//! [unstable] +//! build-std = ["core"] +//! +//! [target.thumbv4t-none-eabi] +//! runner = "mgba-qt" +//! rustflags = [ +//! "-Clinker=arm-none-eabi-ld", +//! "-Clink-arg=-Tlinker_scripts/mono_boot.ld", +//! ] +//! ``` +//! +//! * If you want the `compiler_builtins` crate to provide weak intrinsics so +//! that you can override them yourself, you can set `build-std-features = +//! ["compiler-builtins-weak-intrinsics"]` in the `[unstable]` section. This +//! is not normally needed, but if you some day try to write your own +//! intrinsics impls you'll want to know it's available. +//! +//! ## Rust Analyzer +//! +//! The `test` crate won't be available when using `build-std` like this. To +//! make Rust Analyzer work without constantly complaining about a missing +//! `test` crate, add this to your `.vscode/settings.json` (or whatever similar +//! file in your editor of choice) +//! +//! ```json +//! { +//! "rust-analyzer.cargo.allTargets": false, +//! "rust-analyzer.check.command": "build", +//! "rust-analyzer.check.extraArgs": [ +//! "--lib", +//! "--bins", +//! "--examples" +//! ] +//! } +//! ``` +//! +//! ## `no_std` and `no_main` +//! +//! The full standard library isn't available on the GBA, it's a [bare metal] +//! environment, so we'll only have access to the [`core`] crate. +//! +//! * You'll need to use the [`#![no_std]`][no_std] and [`#![no_main]`][no_main] +//! attributes on your binary. +//! * You'll also need to declare a [panic handler][panic_handler]. You can do +//! this yourself or you can use the [`panic_handler!`][crate::panic_handler] +//! macro from this crate to type a little less. +//! * You need to provide a correct `main` function for the assembly runtime to +//! call once it has made the GBA ready for your Rust program. It needs to be +//! an `extern "C" fn` named `main` (using [`#![no_mangle]`][no_mangle]), and +//! which takes no arguments and never returns (`-> !`). +//! +//! Here's a minimal example: +//! ```rust +//! #![no_std] +//! #![no_main] +//! +//! gba::panic_handler!(empty_loop); +//! +//! #[no_mangle] +//! pub fn main() -> ! { +//! loop {} +//! } +//! ``` +//! +//! [bare metal]: +//! https://docs.rust-embedded.org/book/intro/no-std.html#bare-metal-environments +//! [no_std]: +//! https://doc.rust-lang.org/reference/names/preludes.html#the-no_std-attribute +//! [no_main]: +//! https://doc.rust-lang.org/reference/crates-and-source-files.html#the-no_main-attribute +//! [no_mangle]: https://doc.rust-lang.org/reference/abi.html#the-no_mangle-attribute +//! [panic_handler]: https://doc.rust-lang.org/nomicon/panic-handler.html diff --git a/src/per_system_setup.rs b/src/per_system_setup.rs new file mode 100644 index 00000000..d8d86601 --- /dev/null +++ b/src/per_system_setup.rs @@ -0,0 +1,39 @@ +//! Explanation of required setup per development machine. +//! +//! ## GNU Binutils +//! +//! The linker that comes with Rust (part of the LLVM utils) is capable of +//! linking a GBA rom with no special steps. However, the *other* LLVM binutils +//! do not all understand the "interworking" code that exists on the GBA and +//! similar old ARM targets. +//! +//! If you want to be able to dump the assembly of a compiled file and have it +//! all be readable, you will need to get the GNU objdump that's available as +//! part of the GNU binutils. If you just use the LLVM objdump then any +//! functions using `a32` code will get disassembled as complete nonsense. +//! +//! The GNU binutils are configured for each target family separately, and in +//! this case we want the binutils for `arm-none-eabi`. To get an appropriate +//! set of utilities you can go to the [ARM GNU Toolchain] website (Windows, +//! Mac, and Linux downloads), or if you're on Linux you can probably find it in +//! your package manager under a name like `binutils-arm-none-eabi` or something +//! like that. +//! +//! [ARM GNU Toolchain]: +//! https://developer.arm.com/Tools%20and%20Software/GNU%20Toolchain +//! +//! ## Nightly Rust Toolchain +//! +//! You'll need a [Nightly channel][channels] of Rust available, because we'll +//! need to use the `build-std` nightly ability of `cargo`. +//! +//! [channels]: https://rust-lang.github.io/rustup/concepts/channels.html +//! +//! ## Rust Source +//! +//! You'll need to have the `rust-src` component from `rustup` installed. This +//! is also used by `build-std`. +//! +//! ```sh +//! rustup component add rust-src +//! ```