mirror of
https://github.com/italicsjenga/gba.git
synced 2024-12-24 11:11:31 +11:00
sensible goals
This commit is contained in:
parent
5bb6a927fd
commit
b927a348bd
|
@ -2,26 +2,17 @@
|
||||||
|
|
||||||
So, what's this book actually gonna teach you?
|
So, what's this book actually gonna teach you?
|
||||||
|
|
||||||
I'm _not_ gonna tell you how to use a crate that already exists.
|
My goal is certainly not just showing off the crate. Programming for the GBA is
|
||||||
|
weird enough that I'm trying to teach you all the rest of the stuff you need to
|
||||||
|
know along the way. If I do my job right then you'd be able to write your own
|
||||||
|
crate for GBA stuff just how you think it should all go by the end.
|
||||||
|
|
||||||
Don't get me wrong, there _is_ a [gba](https://crates.io/crates/gba) crate, and
|
Overall the book is sorted more for easy review once you're trying to program
|
||||||
it's on crates.io and all that jazz.
|
something. The GBA has a few things that can stand on their own and many other
|
||||||
|
things are a mass of interconnected concepts, so some parts of the book end up
|
||||||
However, unlike most crates that come with a tutorial book, I don't want to just
|
having to refer you to portions that you haven't read yet. The chapters and
|
||||||
teach you how to use the crate. What I want is to teach you what you need to
|
sections are sorted so that _minimal_ future references are required, but it's
|
||||||
know so that you could build the crate yourself, from scratch, if it didn't
|
unavoidable that it'll happen sometimes.
|
||||||
already exist for you. Let's call it the [Handmade
|
|
||||||
Hero](https://handmadehero.org/) school of design. Much more than you might find
|
|
||||||
in other Rust crate books, I'll be attempting to show a lot of the _why_ in
|
|
||||||
addition to just the _how_. Once you know how to do it all on your own, you can
|
|
||||||
decide for yourself if the `gba` crate does it well, or if you think you can
|
|
||||||
come up with something that suits your needs better.
|
|
||||||
|
|
||||||
Overall the book is sorted for easy review once you're trying to program
|
|
||||||
something, and the GBA has a few interconnected concepts, so some parts of the
|
|
||||||
book end up having to refer you to portions that you haven't read yet. The
|
|
||||||
chapters and sections are sorted so that _minimal_ future references are
|
|
||||||
required, but it's unavoidable that it'll happen sometimes.
|
|
||||||
|
|
||||||
The actual "tutorial order" of the book is the
|
The actual "tutorial order" of the book is the
|
||||||
[Examples](../05-examples/00-index.md) chapter. Each section of that chapter
|
[Examples](../05-examples/00-index.md) chapter. Each section of that chapter
|
||||||
|
|
|
@ -402,9 +402,9 @@ silly but it's so much faster than doing an actual division).
|
||||||
Also, again signed values can be annoying, because if the value _just happens_
|
Also, again signed values can be annoying, because if the value _just happens_
|
||||||
to be `i32::MIN` then when you negate it you'll have... _still_ a negative
|
to be `i32::MIN` then when you negate it you'll have... _still_ a negative
|
||||||
value. I'm not 100% on this, but I think the correct thing to do at that point
|
value. I'm not 100% on this, but I think the correct thing to do at that point
|
||||||
is to give `$t::MIN` as out output num value.
|
is to give `$t::MIN` as the output num value.
|
||||||
|
|
||||||
Did you get all that? Good, because this is involves casting, we will need to
|
Did you get all that? Good, because this involves casting, so we will need to
|
||||||
implement it three times, which calls for another macro.
|
implement it three times, which calls for another macro.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
|
|
|
@ -14,3 +14,8 @@ for full details of how the `WAITCNT` register works.
|
||||||
The game pak SRAM also has only an 8-bit bus, so have fun with that.
|
The game pak SRAM also has only an 8-bit bus, so have fun with that.
|
||||||
|
|
||||||
The GBA Direct Memory Access (DMA) unit cannot access SRAM.
|
The GBA Direct Memory Access (DMA) unit cannot access SRAM.
|
||||||
|
|
||||||
|
Also, you [should not write to SRAM with code executing from
|
||||||
|
ROM](https://problemkaputt.de/gbatek.htm#gbacartbackupsramfram). Instead, you
|
||||||
|
should move the code to WRAM and execute the save code from there. We'll cover
|
||||||
|
how to handle that eventually.
|
||||||
|
|
|
@ -3,4 +3,81 @@
|
||||||
It's all well and good to just show a picture, even to show an animation, but if
|
It's all well and good to just show a picture, even to show an animation, but if
|
||||||
we want a game we have to let the user interact with something.
|
we want a game we have to let the user interact with something.
|
||||||
|
|
||||||
TODO
|
## Key Input Register
|
||||||
|
|
||||||
|
* KEYINPUT, `0x400_0130`, `u16`, read only
|
||||||
|
|
||||||
|
This little `u16` stores the status of _all_ the buttons on the GBA, all at
|
||||||
|
once. There's only 10 of them, and we have 16 bits to work with, so that sounds
|
||||||
|
easy. However, there's a bit of a catch. The register follows a "low-active"
|
||||||
|
convention, where pressing a button _clears_ that bit until it's released.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
const NO_BUTTONS_PRESSED: u16 = 0b0000_0011_1111_1111;
|
||||||
|
```
|
||||||
|
|
||||||
|
The buttons are, going up in order from the 0th bit:
|
||||||
|
|
||||||
|
* A
|
||||||
|
* B
|
||||||
|
* Select
|
||||||
|
* Start
|
||||||
|
* Right
|
||||||
|
* Left
|
||||||
|
* Up
|
||||||
|
* Down
|
||||||
|
* R
|
||||||
|
* L
|
||||||
|
|
||||||
|
Bits above that are not used. However, since the arrow left and right can never
|
||||||
|
be pressed at the same time, and the arrows up and down can never be pressed at
|
||||||
|
the same time, this register will never read as zero.
|
||||||
|
|
||||||
|
When programming, we usually are thinking of what buttons we want to have _be
|
||||||
|
pressed_ instead of buttons we want to have _not be pressed_. This means that we
|
||||||
|
need an inversion to happen somewhere along the line. The easiest moment of
|
||||||
|
inversion is immediately as you read in from the register and wrap the value up
|
||||||
|
in a newtype.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub fn read_key_input() -> KeyInput {
|
||||||
|
KeyInput(KEYINPUT.read() ^ 0b0000_0011_1111_1111)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now the KeyInput you get can be checked for what buttons are pressed by checking
|
||||||
|
for a set bit like you'd do anywhere else.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
impl KeyInput {
|
||||||
|
pub fn a_pressed(self) -> bool {
|
||||||
|
(self.0 & A_BIT) > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that the current `KEYINPUT` value changes in real time as the user presses
|
||||||
|
or releases the buttons. To account for this, it's best to read the value just
|
||||||
|
once per game frame and then use that single value as if it was the input across
|
||||||
|
the whole frame. If you've worked with polling input before that should sound
|
||||||
|
totally normal, but if not just always remember to gather the input once per
|
||||||
|
frame and then use that value across the whole frame.
|
||||||
|
|
||||||
|
## Key Interrupt Control
|
||||||
|
|
||||||
|
* KEYCNT, `0x400_0132`, `u16`, read/write
|
||||||
|
|
||||||
|
This lets you control what keys will trigger a keypad interrupt. Of course, for
|
||||||
|
the actual interrupt to fire you also need to set the `IME` and IE` registers
|
||||||
|
properly. See the [Interrupts](05-interrupts.md) section for details there.
|
||||||
|
|
||||||
|
The main thing to know about this register is that the keys are in _the exact
|
||||||
|
same order_ as the key input order. However, with this register they use a
|
||||||
|
high-active convention instead (eg: the bit is active when the button should be
|
||||||
|
pressed as part of the interrupt).
|
||||||
|
|
||||||
|
In addition to simply having the bits for the buttons, bit 14 is a flag for
|
||||||
|
enabling keypad interrupts (in addition to the flag in the `IE` register), and
|
||||||
|
bit 15 decides how having more than one button works. If bit 15 is disabled,
|
||||||
|
it's an OR combination (eg: "press any key to continue"). If bit 15 is enabled
|
||||||
|
it's an AND combination (eg: "press A+B+Start+Select to reset").
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
# hello_magic
|
|
|
@ -1 +0,0 @@
|
||||||
# hello_world
|
|
|
@ -1 +0,0 @@
|
||||||
# light_cycle
|
|
|
@ -1 +0,0 @@
|
||||||
# bg_demo
|
|
|
@ -35,7 +35,3 @@
|
||||||
* [Link Cable](04-non-video/06-link_cable.md)
|
* [Link Cable](04-non-video/06-link_cable.md)
|
||||||
* [Game Pak](04-non-video/07-game_pak.md)
|
* [Game Pak](04-non-video/07-game_pak.md)
|
||||||
* [Examples](05-examples/00-index.md)
|
* [Examples](05-examples/00-index.md)
|
||||||
* [hello_magic](05-examples/01-hello_magic.md)
|
|
||||||
* [hello_world](05-examples/02-hello_world.md)
|
|
||||||
* [light_cycle](05-examples/03-light_cycle.md)
|
|
||||||
* [bg_demo](05-examples/04-bg_demo.md)
|
|
||||||
|
|
|
@ -1,243 +0,0 @@
|
||||||
#![no_std]
|
|
||||||
#![feature(start)]
|
|
||||||
#![feature(underscore_const_names)]
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! newtype {
|
|
||||||
($(#[$attr:meta])* $new_name:ident, $old_name:ident) => {
|
|
||||||
$(#[$attr])*
|
|
||||||
#[repr(transparent)]
|
|
||||||
pub struct $new_name($old_name);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! const_assert {
|
|
||||||
($condition:expr) => {
|
|
||||||
#[deny(const_err)]
|
|
||||||
#[allow(dead_code)]
|
|
||||||
const _: usize = 0 - !$condition as usize;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Constructs an RGB value with a `const_assert!` that the input is in range.
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! const_rgb {
|
|
||||||
($r:expr, $g:expr, $b:expr) => {{
|
|
||||||
const_assert!($r <= 31);
|
|
||||||
const_assert!($g <= 31);
|
|
||||||
const_assert!($b <= 31);
|
|
||||||
Color::new($r, $g, $b)
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
mod vol_address {
|
|
||||||
#![allow(unused)]
|
|
||||||
use core::{cmp::Ordering, iter::FusedIterator, marker::PhantomData, num::NonZeroUsize};
|
|
||||||
/// VolAddress
|
|
||||||
#[derive(Debug)]
|
|
||||||
#[repr(transparent)]
|
|
||||||
pub struct VolAddress<T> {
|
|
||||||
address: NonZeroUsize,
|
|
||||||
marker: PhantomData<*mut T>,
|
|
||||||
}
|
|
||||||
impl<T> Clone for VolAddress<T> {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
*self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T> Copy for VolAddress<T> {}
|
|
||||||
impl<T> PartialEq for VolAddress<T> {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.address == other.address
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T> Eq for VolAddress<T> {}
|
|
||||||
impl<T> PartialOrd for VolAddress<T> {
|
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
||||||
Some(self.address.cmp(&other.address))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T> Ord for VolAddress<T> {
|
|
||||||
fn cmp(&self, other: &Self) -> Ordering {
|
|
||||||
self.address.cmp(&other.address)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T> VolAddress<T> {
|
|
||||||
pub const unsafe fn new_unchecked(address: usize) -> Self {
|
|
||||||
VolAddress {
|
|
||||||
address: NonZeroUsize::new_unchecked(address),
|
|
||||||
marker: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub const unsafe fn cast<Z>(self) -> VolAddress<Z> {
|
|
||||||
VolAddress {
|
|
||||||
address: self.address,
|
|
||||||
marker: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub unsafe fn offset(self, offset: isize) -> Self {
|
|
||||||
// TODO: const this
|
|
||||||
VolAddress {
|
|
||||||
address: NonZeroUsize::new_unchecked(self.address.get().wrapping_add(offset as usize * core::mem::size_of::<T>())),
|
|
||||||
marker: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn is_aligned(self) -> bool {
|
|
||||||
// TODO: const this
|
|
||||||
self.address.get() % core::mem::align_of::<T>() == 0
|
|
||||||
}
|
|
||||||
pub const unsafe fn iter_slots(self, slots: usize) -> VolAddressIter<T> {
|
|
||||||
VolAddressIter { vol_address: self, slots }
|
|
||||||
}
|
|
||||||
pub fn read(self) -> T
|
|
||||||
where
|
|
||||||
T: Copy,
|
|
||||||
{
|
|
||||||
unsafe { (self.address.get() as *mut T).read_volatile() }
|
|
||||||
}
|
|
||||||
pub unsafe fn read_non_copy(self) -> T {
|
|
||||||
(self.address.get() as *mut T).read_volatile()
|
|
||||||
}
|
|
||||||
pub fn write(self, val: T) {
|
|
||||||
unsafe { (self.address.get() as *mut T).write_volatile(val) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// VolAddressIter
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct VolAddressIter<T> {
|
|
||||||
vol_address: VolAddress<T>,
|
|
||||||
slots: usize,
|
|
||||||
}
|
|
||||||
impl<T> Clone for VolAddressIter<T> {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
VolAddressIter {
|
|
||||||
vol_address: self.vol_address,
|
|
||||||
slots: self.slots,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T> PartialEq for VolAddressIter<T> {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.vol_address == other.vol_address && self.slots == other.slots
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T> Eq for VolAddressIter<T> {}
|
|
||||||
impl<T> Iterator for VolAddressIter<T> {
|
|
||||||
type Item = VolAddress<T>;
|
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
|
||||||
if self.slots > 0 {
|
|
||||||
let out = self.vol_address;
|
|
||||||
unsafe {
|
|
||||||
self.slots -= 1;
|
|
||||||
self.vol_address = self.vol_address.offset(1);
|
|
||||||
}
|
|
||||||
Some(out)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T> FusedIterator for VolAddressIter<T> {}
|
|
||||||
/// VolAddressBlock
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct VolAddressBlock<T> {
|
|
||||||
vol_address: VolAddress<T>,
|
|
||||||
slots: usize,
|
|
||||||
}
|
|
||||||
impl<T> Clone for VolAddressBlock<T> {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
VolAddressBlock {
|
|
||||||
vol_address: self.vol_address,
|
|
||||||
slots: self.slots,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T> PartialEq for VolAddressBlock<T> {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.vol_address == other.vol_address && self.slots == other.slots
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T> Eq for VolAddressBlock<T> {}
|
|
||||||
impl<T> VolAddressBlock<T> {
|
|
||||||
pub const unsafe fn new_unchecked(vol_address: VolAddress<T>, slots: usize) -> Self {
|
|
||||||
VolAddressBlock { vol_address, slots }
|
|
||||||
}
|
|
||||||
pub const fn iter(self) -> VolAddressIter<T> {
|
|
||||||
VolAddressIter {
|
|
||||||
vol_address: self.vol_address,
|
|
||||||
slots: self.slots,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub unsafe fn index_unchecked(self, slot: usize) -> VolAddress<T> {
|
|
||||||
// TODO: const this
|
|
||||||
self.vol_address.offset(slot as isize)
|
|
||||||
}
|
|
||||||
pub fn index(self, slot: usize) -> VolAddress<T> {
|
|
||||||
if slot < self.slots {
|
|
||||||
unsafe { self.vol_address.offset(slot as isize) }
|
|
||||||
} else {
|
|
||||||
panic!("Index Requested: {} >= Bound: {}", slot, self.slots)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn get(self, slot: usize) -> Option<VolAddress<T>> {
|
|
||||||
if slot < self.slots {
|
|
||||||
unsafe { Some(self.vol_address.offset(slot as isize)) }
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
use self::vol_address::*;
|
|
||||||
|
|
||||||
newtype! {
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
Color, u16
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Color {
|
|
||||||
/// Combines the Red, Blue, and Green provided into a single color value.
|
|
||||||
pub const fn new(red: u16, green: u16, blue: u16) -> Color {
|
|
||||||
Color(blue << 10 | green << 5 | red)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
newtype! {
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
DisplayControlSetting, u16
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const DISPLAY_CONTROL: VolAddress<DisplayControlSetting> = unsafe { VolAddress::new_unchecked(0x0400_0000) };
|
|
||||||
pub const JUST_MODE3: DisplayControlSetting = DisplayControlSetting(3);
|
|
||||||
pub const JUST_BG2: DisplayControlSetting = DisplayControlSetting(0b100_0000_0000);
|
|
||||||
pub const JUST_MODE3_AND_BG2: DisplayControlSetting = DisplayControlSetting(JUST_MODE3.0 | JUST_BG2.0);
|
|
||||||
|
|
||||||
pub struct Mode3;
|
|
||||||
impl Mode3 {
|
|
||||||
const SCREEN_WIDTH: isize = 240;
|
|
||||||
const SCREEN_HEIGHT: isize = 160;
|
|
||||||
const PIXELS: VolAddressBlock<Color> =
|
|
||||||
unsafe { VolAddressBlock::new_unchecked(VolAddress::new_unchecked(0x600_0000), (Self::SCREEN_WIDTH * Self::SCREEN_HEIGHT) as usize) };
|
|
||||||
|
|
||||||
pub unsafe fn draw_pixel(col: usize, row: usize, color: Color) {
|
|
||||||
Self::PIXELS.index(col + row * Self::SCREEN_WIDTH as usize).write(color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[panic_handler]
|
|
||||||
fn panic(_info: &core::panic::PanicInfo) -> ! {
|
|
||||||
loop {}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[start]
|
|
||||||
fn main(_argc: isize, _argv: *const *const u8) -> isize {
|
|
||||||
unsafe {
|
|
||||||
DISPLAY_CONTROL.write(JUST_MODE3_AND_BG2);
|
|
||||||
Mode3::draw_pixel(120, 80, const_rgb!(31, 0, 0));
|
|
||||||
Mode3::draw_pixel(136, 80, const_rgb!(0, 31, 0));
|
|
||||||
Mode3::draw_pixel(120, 96, const_rgb!(0, 0, 31));
|
|
||||||
loop {}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -65,11 +65,21 @@ pub extern "C" fn __clzsi2(mut x: usize) -> usize {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn __clzsi2_test() {
|
fn __clzsi2_test() {
|
||||||
let mut i = 1 << 63;
|
let mut i: usize = core::usize::MAX;
|
||||||
while i > 0 {
|
while i > 0 {
|
||||||
assert_eq!(__clzsi2(i), i.leading_zeros() as usize);
|
assert_eq!(__clzsi2(i) as u32, i.leading_zeros());
|
||||||
i >>= 1;
|
i >>= 1;
|
||||||
}
|
}
|
||||||
|
// check 0 also
|
||||||
|
i = 0;
|
||||||
|
assert_eq!(__clzsi2(i) as u32, i.leading_zeros());
|
||||||
|
// double check for bit patterns that aren't solid 1s
|
||||||
|
i = 1;
|
||||||
|
for _ in 0 .. 63 {
|
||||||
|
assert_eq!(__clzsi2(i) as u32, i.leading_zeros());
|
||||||
|
i <<= 2;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: add some shims
|
// TODO: add some shims
|
||||||
|
|
Loading…
Reference in a new issue