diff --git a/README.md b/README.md index ba5cc22..ff88225 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ do this stuff. dots as well as other support files: * crt0.s * linker.ld - * thumbv4-nintendo-agb.json + * thumbv4-none-agb.json * build.rs 5) Run `arm-none-eabi-as crt0.s -o crt0.o` to build the `crt0.s` into a `crt0.o` @@ -38,7 +38,7 @@ do this stuff. `build.bat` file it's set to simply run every single time because it's a cheap enough operation. -6) Build with `cargo xbuild --target thumbv4-nintendo-agb.json` +6) Build with `cargo xbuild --target thumbv4-none-agb.json` * The file extension is significant, and `cargo xbuild` takes it as a flag to compile dependencies with the same sysroot, so you can include crates normally. Well, crates that can run inside a GBA at least (Which means they @@ -47,7 +47,7 @@ do this stuff. helpful because it has debug symbols). 7) Also you can patch up the output to be a "real" ROM file: - * `arm-none-eabi-objcopy -O binary target/thumbv4-nintendo-agb/debug/gbatest target/output.gba` + * `arm-none-eabi-objcopy -O binary target/thumbv4-none-agb/debug/gbatest target/output.gba` * `gbafix target/output.gba` 8) Alternately, you can use the provided `build.bat` file (or write a similar diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 835ff6e..0afee8f 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -8,3 +8,4 @@ * [IO Registers](ch1/io_registers.md) * [The Display Control](ch1/the_display_control.md) * [Video Memory Intro](ch1/video_memory_intro.md) + * [hello2](ch1/hello2.md) diff --git a/book/src/ch0/index.md b/book/src/ch0/index.md index 4bda9fd..4084be7 100644 --- a/book/src/ch0/index.md +++ b/book/src/ch0/index.md @@ -37,10 +37,12 @@ Now you'll need some particular files each time you want to start a new project. You can find them in the root of the [rust-console/gba repo](https://github.com/rust-console/gba). -* `thumbv4-nintendo-agb.json` describes the overall GBA to cargo-xbuild so it - knows what to do. This is actually a somewhat made up target name since - there's no official target name, but this is the best name to pick based on - [how target names are decided](https://wiki.osdev.org/Target_Triplet). +* `thumbv4-none-agb.json` describes the overall GBA to cargo-xbuild so it knows + what to do. This is actually a somewhat made up target name since there's no + official target name. The GBA is essentially the same as a normal + `thumbv4-none-eabi` device, but we give it the "agb" signifier so that later + on we'll be able to use rust's `cfg` ability to allow our code to know if it's + specifically targeting a GBA or some other similar device (like an NDS). * `crt0.s` describes some ASM startup stuff. If you have more ASM to place here later on this is where you can put it. You also need to build it into a `crt0.o` file before it can actually be used, but we'll cover that below. @@ -57,7 +59,7 @@ Once you've got something to build, you perform the following steps: might as well do it every time so that you never forget to because it's a practically instant operation. -* `cargo xbuild --target thumbv4-nintendo-agb.json` +* `cargo xbuild --target thumbv4-none-agb.json` * This builds your Rust source. It accepts _most of_ the normal options, such as `--release`, and options, such as `--bin foo` or `--examples`, that you'd expect `cargo` to accept. @@ -83,7 +85,7 @@ emulators can also do it. However, if you want a "real" ROM that works in all emulators and that you could transfer to a flash cart there's a little more to do. -* `arm-none-eabi-objcopy -O binary target/thumbv4-nintendo-agb/MODE/BIN_NAME target/ROM_NAME.gba` +* `arm-none-eabi-objcopy -O binary target/thumbv4-none-agb/MODE/BIN_NAME target/ROM_NAME.gba` * This will perform an [objcopy](https://linux.die.net/man/1/objcopy) on our program. Here I've named the program `arm-none-eabi-objcopy`, which is what devkitpro calls their version of `objcopy` that's specific to the GBA in the @@ -97,7 +99,7 @@ transfer to a flash cart there's a little more to do. `cargo` arranges stuff in the `target/` directory, and between RLS and `cargo doc` and stuff it gets kinda crowded, so it goes like this: * Since our program was built for a non-local target, first we've got a - directory named for that target, `thumbv4-nintendo-agb/` + directory named for that target, `thumbv4-none-agb/` * Next, the "MODE" is either `debug/` or `release/`, depending on if we had the `--release` flag included. You'll probably only be packing release mode programs all the way into GBA roms, but it works with either mode. diff --git a/book/src/ch1/hello1.md b/book/src/ch1/hello1.md index 042cdf5..eb6ca6f 100644 --- a/book/src/ch1/hello1.md +++ b/book/src/ch1/hello1.md @@ -177,3 +177,8 @@ If you ever use volatile stuff on other platforms it's important to note that volatile doesn't make things thread-safe, you still need atomic for that. However, the GBA doesn't have threads, so we don't have to worry about thread safety concerns. + +Accordingly, our first bit of code for our library will be a _newtype_ over a +normal `*mut T` so that it has volatile reads and writes as the default. We'll +cover the details later on when we try writing a `hello2` program, once we know +more of what's going on. diff --git a/book/src/ch1/hello2.md b/book/src/ch1/hello2.md new file mode 100644 index 0000000..1273a69 --- /dev/null +++ b/book/src/ch1/hello2.md @@ -0,0 +1,114 @@ +# hello2 + +Okay so let's have a look again: + +`hello1` + +```rust +#![feature(start)] +#![no_std] + +#[cfg(not(test))] +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +} + +#[start] +fn main(_argc: isize, _argv: *const *const u8) -> isize { + unsafe { + (0x04000000 as *mut u16).write_volatile(0x0403); + (0x06000000 as *mut u16).offset(120 + 80 * 240).write_volatile(0x001F); + (0x06000000 as *mut u16).offset(136 + 80 * 240).write_volatile(0x03E0); + (0x06000000 as *mut u16).offset(120 + 96 * 240).write_volatile(0x7C00); + loop {} + } +} +``` + +Now let's clean this up so that it's clearer what's going on. + +First we'll label that display control stuff: + +```rust +pub const DISPCNT: *mut u16 = 0x04000000 as *mut u16; +pub const MODE3: u16 = 3; +pub const BG2: u16 = 0b100_0000_0000; +``` + +Next we make some const values for the actual pixel drawing + +```rust +pub const VRAM: usize = 0x06000000; +pub const SCREEN_WIDTH: isize = 240; +``` + +And then we want a small helper function for putting together a color value. + +Happily, this one can even be declared as a const function. At the time of +writing, we've got the "minimal const fn" support in nightly. It really is quite +limited, but I'm happy to let rustc and LLVM pre-compute as much as they can +when it comes to the GBA's tiny CPU. + +```rust +pub const fn rgb16(red: u16, green: u16, blue: u16) -> u16 { + blue << 10 | green << 5 | red +} +``` + +Finally, we'll make a function for drawing a pixel in Mode 3. Even though it's +just a one-liner, having the "important parts" be labeled as function arguments +usually helps you think about it a lot better. + +```rust +pub unsafe fn mode3_pixel(col: isize, row: isize, color: u16) { + (VRAM as *mut u16).offset(col + row * SCREEN_WIDTH).write_volatile(color); +} +``` + +So now we've got this: + +`hello2` + +```rust +#![feature(start)] +#![no_std] + +#[cfg(not(test))] +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +} + +#[start] +fn main(_argc: isize, _argv: *const *const u8) -> isize { + unsafe { + DISPCNT.write_volatile(MODE3 | BG2); + mode3_pixel(120, 80, rgb16(31, 0, 0)); + mode3_pixel(136, 80, rgb16(0, 31, 0)); + mode3_pixel(120, 96, rgb16(0, 0, 31)); + loop {} + } +} + +pub const DISPCNT: *mut u16 = 0x04000000 as *mut u16; +pub const MODE3: u16 = 3; +pub const BG2: u16 = 0b100_0000_0000; + +pub const VRAM: usize = 0x06000000; +pub const SCREEN_WIDTH: isize = 240; + +pub const fn rgb16(red: u16, green: u16, blue: u16) -> u16 { + blue << 10 | green << 5 | red +} + +pub unsafe fn mode3_pixel(col: isize, row: isize, color: u16) { + (VRAM as *mut u16).offset(col + row * SCREEN_WIDTH).write_volatile(color); +} +``` + +Exact same program that we started with, but much easier to read. + +Of course, in the full `gba` crate that this book is a part of we have these and +other elements all labeled and sorted out for you. Still, for educational +purposes it's often best to do it yourself at least once. diff --git a/book/src/ch1/the_display_control.md b/book/src/ch1/the_display_control.md index 3109c5f..5a8cb8a 100644 --- a/book/src/ch1/the_display_control.md +++ b/book/src/ch1/the_display_control.md @@ -27,9 +27,10 @@ own demos and chapters later on, so that's all we'll say about them for now. Modes 3, 4, and 5 are "Bitmap" modes. These let you write individual pixels to locations on the screen. -* **Mode 3** is full resolution (240w x 160h) RBG15 color. You might not be used to - RGB15, since modern computers have 24 or 32 bit colors. In RGB15, there's 5 - bits for each color channel, and the highest bit is totally ignored. +* **Mode 3** is full resolution (240w x 160h) RBG15 color. You might not be used + to RGB15, since modern computers have 24 or 32 bit colors. In RGB15, there's 5 + bits for each color channel stored within a `u16` value, and the highest bit is + simply ignored. * **Mode 4** is full resolution paletted color. Instead of being a `u16` color, each pixel value is a `u8` palette index entry, and then the display uses the palette memory (which we'll talk about later) to store the actual color data. @@ -99,3 +100,6 @@ First let's [convert that to binary](https://www.wolframalpha.com/input/?i=0x0403), and we get `0b100_0000_0011`. So, that's setting Mode 3 with background 2 enabled and nothing else special. + +However, I think we can do better than that. This is a prime target for more +newtyping as we attempt a `hello2` program. diff --git a/book/src/ch1/video_memory_intro.md b/book/src/ch1/video_memory_intro.md index 29569ea..07832ac 100644 --- a/book/src/ch1/video_memory_intro.md +++ b/book/src/ch1/video_memory_intro.md @@ -18,20 +18,96 @@ don't know to follow the special rules. ## RGB15 -TODO +As I said before, RGB15 stores a color within a `u16` value using 5 bits for +each color channel. + +```rust +pub const RED: u16 = 0b0_00000_00000_11111; +pub const GREEN: u16 = 0b0_00000_11111_00000; +pub const BLUE: u16 = 0b0_11111_00000_00000; +``` + +In Mode 3 and Mode 5 we write direct color values into VRAM, and in Mode 4 we +write palette index values, and then the color values go into the PALRAM. ## Mode 3 -TODO +Mode 3 is pretty easy. We have a full resolution grid of rgb15 pixels. There's +160 rows of 240 pixels each, with the base address being the top left corner. A +particular pixel uses normal "2d indexing" math: + +```rust +let row_five_col_seven = 5 + (7 * SCREEN_WIDTH); +``` + +To draw a pixel, we just write a value at the address for the row and col that +we want to draw to. ## Mode 4 -TODO +Mode 4 introduces page flipping. Instead of one giant page at `0x0600_0000`, +there's Page 0 at `0x0600_0000` and then Page 1 at `0x0600_A000`. The resolution +for each page is the same as above, but instead of writing `u16` values, the +memory is treated as `u8` indexes into PALRAM. The PALRAM starts at +`0x0500_0000`, and there's enough space for 256 palette entries (each a `u16`). + +To set the color of a palette entry we just do a normal `u16` write_volatile. + +```rust +(0x0500_0000 as *mut u16).offset(target_index).write_volatile(new_color) +``` + +To draw a pixel we set the palette entry that we want the pixel to use. However, +we must remember the "minimum size" write limitation that applies to VRAM. So, +if we want to change just a single pixel at a time we must + +1) Read the full `u16` it's a part of. +2) Clear the half of the `u16` we're going to replace +3) Write the half of the `u16` we're going to replace with the new value +4) Write that result back to the address. + +So, the math for finding a byte offset is the same as Mode 3 (since they're both +a 2d grid). If the byte offset is EVEN it'll be the high bits of the `u16` at +half the byte offset rounded down. If the offset is ODD it'll be the low bits of +the `u16` at half the byte. + +Does that make sense? + +* If we want to write pixel (0,0) the byte offset is 0, so we change the high + bits of `u16` offset 0. Then we want to write to (1,0), so the byte offset is + 1, so we change the low bits of `u16` offset 0. The pixels are next to each + other, and the target bytes are next to each other, good so far. +* If we want to write to (5,6) that'd be byte `5 + 6 * 240 = 1445`, so we'd + target the low bits of `u16` offset `floor(1445/2) = 722`. + +As you can see, trying to write individual pixels in Mode 4 is mostly a bad +time. Fret not! We don't _have_ to write individual bytes. If our data is +arranged correctly ahead of time we can just write `u16` or `u32` values +directly. The video hardware doesn't care, it'll get along just fine. ## Mode 5 -TODO +Mode 5 is also a two page mode, but instead of compressing the size of a pixel's +data to fit in two pages, we compress the resolution. + +Mode 5 is full `u16` color, but only 160w x 128h per page. ## In Conclusion... -TODO +So what got written into VRAM in `hello1`? + +```rust + (0x06000000 as *mut u16).offset(120 + 80 * 240).write_volatile(0x001F); + (0x06000000 as *mut u16).offset(136 + 80 * 240).write_volatile(0x03E0); + (0x06000000 as *mut u16).offset(120 + 96 * 240).write_volatile(0x7C00); +``` + +So at pixels `(120,80)`, `(136,80)`, and `(120,96)` we write three values. Once +again we probably need to [convert them](https://www.wolframalpha.com/) into +binary to make sense of it. + +* 0x001F: 0b11111 +* 0x03E0: 0b11111_00000 +* 0x7C00: 0b11111_00000_00000 + +Ah, of course, a red pixel, a green pixel, and a blue pixel. diff --git a/build.bat b/build.bat index 01f4336..739d3f8 100644 --- a/build.bat +++ b/build.bat @@ -3,12 +3,12 @@ arm-none-eabi-as crt0.s -o crt0.o @rem Build all examples, both debug and release -cargo xbuild --examples --target thumbv4-nintendo-agb.json -cargo xbuild --examples --target thumbv4-nintendo-agb.json --release +cargo xbuild --examples --target thumbv4-none-agb.json +cargo xbuild --examples --target thumbv4-none-agb.json --release @echo Packing examples into ROM files... @for %%I in (.\examples\*.*) do @( echo %%~nI - arm-none-eabi-objcopy -O binary target/thumbv4-nintendo-agb/release/examples/%%~nI target/example-%%~nI.gba >nul + arm-none-eabi-objcopy -O binary target/thumbv4-none-agb/release/examples/%%~nI target/example-%%~nI.gba >nul gbafix target/example-%%~nI.gba >nul ) diff --git a/docs/ch0/index.html b/docs/ch0/index.html index 5cba0db..f07c2a3 100644 --- a/docs/ch0/index.html +++ b/docs/ch0/index.html @@ -72,7 +72,7 @@
@@ -169,8 +169,12 @@ cargo will figure it all out for you.

You can find them in the root of the rust-console/gba repo.

  • -

    cargo xbuild --target thumbv4-nintendo-agb.json

    +

    cargo xbuild --target thumbv4-none-agb.json