From 2907abd3b34bb4753a2dc3a2c9afbb3f27109316 Mon Sep 17 00:00:00 2001 From: bokuweb Date: Sun, 4 Oct 2026 19:07:38 +0900 Subject: [PATCH] Run r2 on the Game Boy Advance and boot Linux Add a `gba/` frontend that runs the RISC-V core on a GBA. Guest RAM is the upper 16MB of the GamePak window, which flash carts with PSRAM expose as writable memory; the kernel image and DTB are embedded in the ROM and copied there at boot. The interpreter loop runs from IWRAM as ARM code, the guest clock is derived from the instruction count, and the serial console is drawn on the Mode 3 bitmap with buttons typing commands. To make this possible the core and device_interfaces crates are now no_std; the Vec-backed Bus and the `start` loop stay behind a default `std` feature, so the existing frontends are unchanged. The LR/SC reservation is a single Option instead of a HashMap, and Cpu gains bus_mut(). Co-Authored-By: Claude Opus 5.5 --- core/Cargo.toml | 6 + core/src/bus_interface.rs | 6 +- core/src/cpu.rs | 16 ++- core/src/lib.rs | 6 + device_interfaces/src/lib.rs | 2 + gba/.cargo/config.toml | 9 ++ gba/Cargo.toml | 27 ++++ gba/Makefile | 19 +++ gba/README.md | 60 +++++++++ gba/build.rs | 11 ++ gba/gba.ld | 50 +++++++ gba/rust-toolchain.toml | 3 + gba/screenshot.png | Bin 0 -> 54516 bytes gba/src/bus.rs | 252 +++++++++++++++++++++++++++++++++++ gba/src/console.rs | 217 ++++++++++++++++++++++++++++++ gba/src/font.rs | 99 ++++++++++++++ gba/src/gba.rs | 107 +++++++++++++++ gba/src/main.rs | 158 ++++++++++++++++++++++ 18 files changed, 1039 insertions(+), 9 deletions(-) create mode 100644 gba/.cargo/config.toml create mode 100644 gba/Cargo.toml create mode 100644 gba/Makefile create mode 100644 gba/README.md create mode 100644 gba/build.rs create mode 100644 gba/gba.ld create mode 100644 gba/rust-toolchain.toml create mode 100644 gba/screenshot.png create mode 100644 gba/src/bus.rs create mode 100644 gba/src/console.rs create mode 100644 gba/src/font.rs create mode 100644 gba/src/gba.rs create mode 100644 gba/src/main.rs diff --git a/core/Cargo.toml b/core/Cargo.toml index 246c2311..78a9396f 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -5,6 +5,12 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = ["std"] +# `std` brings in the Vec-backed `Bus` and the `start` run loop. Without it the +# CPU core is `no_std` so it can be embedded on bare-metal hosts (see `gba/`). +std = [] + [dependencies] device_interfaces = { path = "../device_interfaces" } diff --git a/core/src/bus_interface.rs b/core/src/bus_interface.rs index e75b9f10..6ab1d89d 100644 --- a/core/src/bus_interface.rs +++ b/core/src/bus_interface.rs @@ -1,4 +1,4 @@ -use std::error::Error; +use core::error::Error; #[derive(Debug, Clone, Copy)] pub enum BusException { @@ -8,8 +8,8 @@ pub enum BusException { StoreAccessFault, } -impl std::fmt::Display for BusException { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Display for BusException { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::LoadAddressMisaligned => write!(f, "LoadAddressMisaligned,"), Self::LoadAccessFault => write!(f, "LoadAccessFault,"), diff --git a/core/src/cpu.rs b/core/src/cpu.rs index 6e0bf1b4..43fb5cdf 100644 --- a/core/src/cpu.rs +++ b/core/src/cpu.rs @@ -50,8 +50,8 @@ pub struct Cpu { wait_for_interrupt: bool, /// Previous privilege mode. previous_mode: PrivilegeMode, - /// This is used to reserve addresses for LR/SC - reserved_load_addresses: std::collections::HashMap, + /// Address and value reserved by the last LR.W, consumed by SC.W. + reservation: Option<(u32, u32)>, /// It is used to record exception reason for mtval cause: u32, } @@ -74,7 +74,7 @@ impl Cpu { exception: 0, wait_for_interrupt: false, previous_mode: PrivilegeMode::Machine, - reserved_load_addresses: std::collections::HashMap::new(), + reservation: None, cause: 0, } } @@ -222,6 +222,10 @@ impl Cpu { &self.bus } + pub fn bus_mut(&mut self) -> &mut B { + &mut self.bus + } + pub fn step(&mut self) -> CpuState { // Drive bus state self.bus.step(&mut self.mip); @@ -470,14 +474,14 @@ impl Cpu { // LR.W // Load-Reserved Word 0b00010 => { - self.reserved_load_addresses.insert(rs1, v); + self.reservation = Some((rs1, v)); self.write_back(rd, v) } // SC.W // Store-Conditional Word 0b00011 => { - if let Some(val) = self.reserved_load_addresses.get(&rs1) { - if *val == v { + if let Some((addr, val)) = self.reservation.take() { + if addr == rs1 && val == v { self.bus .write32(rs1, rs2) .unwrap_or_else(|e| self.record_exception(e.into(), rs1)); diff --git a/core/src/lib.rs b/core/src/lib.rs index 5cad89de..593832f5 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,11 +1,17 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(feature = "std")] pub mod bus; pub mod bus_interface; pub mod clint; pub mod cpu; +#[cfg(feature = "std")] use bus_interface::{BusController, BusReader, BusWriter}; +#[cfg(feature = "std")] use cpu::{Cpu, CpuState}; +#[cfg(feature = "std")] pub fn start( bus: B, pc: u32, diff --git a/device_interfaces/src/lib.rs b/device_interfaces/src/lib.rs index c105b3d9..2adc38e6 100644 --- a/device_interfaces/src/lib.rs +++ b/device_interfaces/src/lib.rs @@ -1,3 +1,5 @@ +#![no_std] + mod serial; mod timer; diff --git a/gba/.cargo/config.toml b/gba/.cargo/config.toml new file mode 100644 index 00000000..44752d8c --- /dev/null +++ b/gba/.cargo/config.toml @@ -0,0 +1,9 @@ +[build] +target = "armv4t-none-eabi" + +[unstable] +build-std = ["core"] +build-std-features = ["compiler-builtins-mem"] + +[target.armv4t-none-eabi] +rustflags = ["-Clink-arg=-Tgba.ld"] diff --git a/gba/Cargo.toml b/gba/Cargo.toml new file mode 100644 index 00000000..9cdd4ff7 --- /dev/null +++ b/gba/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "r2-gba" +version = "0.1.0" +edition = "2021" + +# Runs the r2 RISC-V core on a Game Boy Advance and boots Linux on it. +# Built for `thumbv4t-none-eabi` (see .cargo/config.toml), so it lives outside +# the host workspace. +[workspace] + +[features] +# Boot the tiny bare-metal test program instead of Linux. +baremetal = [] + +[dependencies] +r2_core = { path = "../core", package = "core", default-features = false } + +[profile.release] +opt-level = 3 +lto = true +codegen-units = 1 +panic = "abort" +debug = false + +[profile.dev] +opt-level = 1 +panic = "abort" diff --git a/gba/Makefile b/gba/Makefile new file mode 100644 index 00000000..ec1bd099 --- /dev/null +++ b/gba/Makefile @@ -0,0 +1,19 @@ +# Build the GBA ROM: `make` (Linux) or `make baremetal`. +SYSROOT := $(shell rustc --print sysroot) +OBJCOPY := $(wildcard $(SYSROOT)/lib/rustlib/*/bin/llvm-objcopy) +ELF := target/armv4t-none-eabi/release/r2-gba + +.PHONY: all baremetal clean FORCE + +all: target/r2-gba.gba + +baremetal: FORCE + cargo build --release --features baremetal --target-dir target/baremetal + $(OBJCOPY) -O binary target/baremetal/armv4t-none-eabi/release/r2-gba target/r2-gba-baremetal.gba + +target/r2-gba.gba: FORCE + cargo build --release + $(OBJCOPY) -O binary $(ELF) $@ + +clean: + cargo clean diff --git a/gba/README.md b/gba/README.md new file mode 100644 index 00000000..6ada6548 --- /dev/null +++ b/gba/README.md @@ -0,0 +1,60 @@ +# r2-gba + +Runs the r2 RISC-V core on a Game Boy Advance and boots Linux on it +(the same `fixtures/linux.bin` + `default.dtb` as the other frontends). + +![Linux shell on the GBA (rgba)](screenshot.png) + +## How it fits in 288KB of RAM + +It doesn't. Flash carts that run games out of PSRAM (EZ-Flash, EverDrive, ...) +expose the 32MB GamePak ROM window as writable memory, so: + +| GBA address | Use | +| ------------------------- | ----------------------------------------- | +| `0x08000000-0x08FFFFFF` | this program + embedded kernel image/DTB | +| `0x09000000-0x09FFFFFF` | guest RAM (16MB, guest `0x80000000-`) | +| IWRAM | the interpreter loop (ARM code) and stack | +| VRAM (Mode 3) | 60x26 text console (4x6 font) | + +At boot the kernel is copied into guest RAM and the DTB is placed at its end +(its memory size is patched like mini-rv32ima does). + +The guest clock is derived from the instruction count (16 instructions per +µs), so the guest sees a 16 MIPS machine and timing is deterministic. +WFI fast-forwards the clock to the next timer interrupt. + +## Build + +Needs a nightly toolchain with `rust-src` (picked up from `rust-toolchain.toml`). + +```sh +make # target/r2-gba.gba +make baremetal # target/r2-gba-baremetal.gba (tiny test program) +``` + +## Run + +On [rgba](https://github.com/bokuweb/rgba) with PSRAM emulation: + +```sh +RGBA_PSRAM=1 rgba target/r2-gba.gba +``` + +The console is also mirrored to the mGBA debug log. The login prompt is +answered automatically (`root`), and `uname -a` runs at the first prompt. Each button types a command: + +| Button | Command | +| ------ | ------------------- | +| A | `uname -a` | +| B | `ls /` | +| L | `cat /proc/cpuinfo` | +| R | `free` | +| START | `uptime` | +| SELECT | `ps` | + +## Speed + +About 70 ARM instructions per RISC-V instruction, roughly 110K guest +instructions per second on a 16.78MHz GBA. Reaching the login prompt takes +about 40M guest instructions, i.e. several minutes of GBA time. diff --git a/gba/build.rs b/gba/build.rs new file mode 100644 index 00000000..f4e5cd12 --- /dev/null +++ b/gba/build.rs @@ -0,0 +1,11 @@ +use std::{env, fs, path::PathBuf}; + +fn main() { + // Put the linker script where rust-lld can find it. + let out = PathBuf::from(env::var("OUT_DIR").unwrap()); + fs::copy("gba.ld", out.join("gba.ld")).unwrap(); + println!("cargo:rustc-link-search={}", out.display()); + println!("cargo:rerun-if-changed=gba.ld"); + println!("cargo:rerun-if-changed=../fixtures/linux.bin"); + println!("cargo:rerun-if-changed=../fixtures/default.dtb"); +} diff --git a/gba/gba.ld b/gba/gba.ld new file mode 100644 index 00000000..f2a7dfaf --- /dev/null +++ b/gba/gba.ld @@ -0,0 +1,50 @@ +/* GBA memory map: code and payload in GamePak ROM, hot code and data in IWRAM. */ +ENTRY(_start) + +MEMORY { + ewram (w!x) : ORIGIN = 0x02000000, LENGTH = 256K + iwram (w!x) : ORIGIN = 0x03000000, LENGTH = 31K /* top 1K: stacks + BIOS area */ + rom (rx) : ORIGIN = 0x08000000, LENGTH = 16M /* upper 16M is guest RAM */ +} + +SECTIONS { + .text : { + KEEP(*(.text.crt0)) + *(.text .text.*) + . = ALIGN(4); + } >rom + + .rodata : { + *(.rodata .rodata.*) + . = ALIGN(4); + } >rom + + /* Copied from ROM to IWRAM by crt0: the emulator loop and initialized data. */ + .iwram : { + . = ALIGN(4); + __iwram_start = .; + *(.iwram .iwram.*) + *(.data .data.*) + . = ALIGN(4); + __iwram_end = .; + } >iwram AT>rom + __iwram_lma = LOADADDR(.iwram); + + .bss (NOLOAD) : { + . = ALIGN(4); + __bss_start = .; + *(.bss .bss.*) + *(COMMON) + . = ALIGN(4); + __bss_end = .; + } >iwram + + .ewram (NOLOAD) : { + *(.ewram .ewram.*) + } >ewram + + /DISCARD/ : { + *(.ARM.exidx .ARM.exidx.*) + *(.ARM.extab .ARM.extab.*) + } +} diff --git a/gba/rust-toolchain.toml b/gba/rust-toolchain.toml new file mode 100644 index 00000000..f70d2254 --- /dev/null +++ b/gba/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "nightly" +components = ["rust-src"] diff --git a/gba/screenshot.png b/gba/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..2dc3c52f71b3bc479c28686793c9c696def31533 GIT binary patch literal 54516 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 literal 0 HcmV?d00001 diff --git a/gba/src/bus.rs b/gba/src/bus.rs new file mode 100644 index 00000000..a98303b0 --- /dev/null +++ b/gba/src/bus.rs @@ -0,0 +1,252 @@ +//! Guest physical bus: RAM in cartridge PSRAM, CLINT timer, syscon and an +//! 8250 UART wired to the on-screen console. + +use r2_core::bus_interface::{BusController, BusException, BusReader, BusWriter}; + +use crate::console; + +pub const RAM_START: u32 = 0x8000_0000; +/// Guest RAM lives in the upper half of the 32MB GamePak window. +pub const GUEST_RAM: *mut u32 = 0x0900_0000 as *mut u32; +pub const RAM_SIZE: u32 = 16 * 1024 * 1024; + +/// Guest instructions per microsecond of guest time. The clock is driven by +/// the instruction count, so the guest sees a 16 MIPS machine no matter how +/// slow the host really is. (At 1 MIPS, timer work starves userspace and the +/// boot never reaches the login prompt.) +const STEPS_PER_MICRO: u32 = 16; + +/// Check that stores to the GamePak window stick (PSRAM present). +pub fn probe_psram() -> bool { + let p = unsafe { GUEST_RAM.add(RAM_SIZE as usize / 4 - 1) }; + unsafe { + p.write_volatile(0x5a5a_a5a5); + let ok = p.read_volatile() == 0x5a5a_a5a5; + p.write_volatile(0xa5a5_5a5a); + ok && p.read_volatile() == 0xa5a5_5a5a + } +} + +pub struct GbaBus { + /// Instructions left until `mtime` ticks. + sub: u32, + /// Re-evaluate the timer interrupt on the next step (mtimecmp changed). + timer_dirty: bool, + mtime: u64, + mtimecmp: u64, + msip: u32, + /// 8250 line control register (bit 7 = DLAB selects the divisor latches). + lcr: u8, + divisor: u16, + pub power_off: bool, + pub reboot: bool, +} + +impl GbaBus { + pub fn new() -> Self { + Self { + sub: STEPS_PER_MICRO, + timer_dirty: false, + mtime: 0, + mtimecmp: 0, + msip: 0, + lcr: 0, + divisor: 0, + power_off: false, + reboot: false, + } + } + + /// Fast-forward guest time to the pending timer interrupt (used on WFI). + pub fn skip_to_timer(&mut self) { + self.mtime = self.mtime.max(self.mtimecmp); + self.sub = 1; + } + + #[inline(always)] + fn ram(addr: u32, size: u32) -> Option<*mut u8> { + let offset = addr.wrapping_sub(RAM_START); + (offset <= RAM_SIZE - size).then(|| unsafe { (GUEST_RAM as *mut u8).add(offset as usize) }) + } + + fn clint_read(&self, addr: u32) -> u32 { + match addr & 0xffff { + 0x0000 => self.msip, + 0x4000 => self.mtimecmp as u32, + 0x4004 => (self.mtimecmp >> 32) as u32, + 0xbff8 => self.mtime as u32, + 0xbffc => (self.mtime >> 32) as u32, + _ => 0, + } + } + + fn clint_write(&mut self, addr: u32, v: u32) { + match addr & 0xffff { + 0x0000 => self.msip = v & 1, + 0x4000 => { + self.mtimecmp = (self.mtimecmp & !0xffff_ffff) | v as u64; + self.timer_dirty = true; + } + 0x4004 => { + self.mtimecmp = (self.mtimecmp & 0xffff_ffff) | ((v as u64) << 32); + self.timer_dirty = true; + } + 0xbff8 => self.mtime = (self.mtime & !0xffff_ffff) | v as u64, + 0xbffc => self.mtime = (self.mtime & 0xffff_ffff) | ((v as u64) << 32), + _ => {} + } + } + + fn uart_read(&self, reg: u32) -> u8 { + let dlab = self.lcr & 0x80 != 0; + match reg { + 0 if dlab => self.divisor as u8, + 1 if dlab => (self.divisor >> 8) as u8, + 0 => console::read_byte().unwrap_or(0), + // LSR: transmitter empty, plus data-ready when a key is queued. + 5 => 0x60 | console::has_input() as u8, + _ => 0, + } + } + + fn uart_write(&mut self, reg: u32, v: u8) { + let dlab = self.lcr & 0x80 != 0; + match reg { + 0 if dlab => self.divisor = (self.divisor & 0xff00) | v as u16, + 1 if dlab => self.divisor = (self.divisor & 0x00ff) | ((v as u16) << 8), + 0 => console::write_byte(v), + 3 => self.lcr = v, + _ => {} + } + } + + fn mmio_read(&self, addr: u32) -> Result { + match addr { + 0x1100_bff8 | 0x1100_bffc => Ok(self.clint_read(addr)), + 0x1000_0000..=0x1000_00ff => Ok(self.uart_read(addr & 0x7) as u32), + 0x1000_0100..=0x1200_0000 => Ok(0), + _ => Err(BusException::LoadAccessFault), + } + } + + fn mmio_write(&mut self, addr: u32, v: u32) -> Result<(), BusException> { + match addr { + // syscon + 0x1110_0000 if v == 0x5555 => self.power_off = true, + 0x1110_0000 if v == 0x7777 => self.reboot = true, + 0x1110_0000 | 0x1100_4000 | 0x1100_4004 => self.clint_write(addr, v), + 0x1000_0000..=0x1000_00ff => self.uart_write(addr & 0x7, v as u8), + 0x1000_0100..=0x1200_0000 => {} + _ => return Err(BusException::StoreAccessFault), + } + Ok(()) + } +} + +impl BusController for GbaBus { + #[inline(always)] + fn step(&mut self, mip: &mut u32) { + self.sub -= 1; + if self.sub != 0 && !self.timer_dirty { + return; + } + if self.sub == 0 { + self.sub = STEPS_PER_MICRO; + self.mtime += 1; + } + self.timer_dirty = false; + if self.mtimecmp != 0 && self.mtime >= self.mtimecmp { + *mip |= 0x80; + } else { + *mip &= !0x80; + } + } + + fn power_off(&self) -> bool { + self.power_off + } + + fn reboot(&self) -> bool { + self.reboot + } +} + +// The GamePak bus is 16 bits wide and PSRAM carts generally have no byte +// lanes, so all accesses are done as halfwords (or words). +impl BusReader for GbaBus { + #[inline(always)] + fn read8(&self, addr: u32) -> Result { + match Self::ram(addr, 1) { + Some(p) => { + let hw = unsafe { (((p as usize) & !1) as *const u16).read() }; + Ok((hw >> ((addr & 1) * 8)) as u8) + } + None => self.mmio_read(addr).map(|v| v as u8), + } + } + + #[inline(always)] + fn read16(&self, addr: u32) -> Result { + if addr & 1 != 0 { + return Err(BusException::LoadAddressMisaligned); + } + match Self::ram(addr, 2) { + Some(p) => Ok(unsafe { (p as *const u16).read() }), + None => self.mmio_read(addr).map(|v| v as u16), + } + } + + #[inline(always)] + fn read32(&self, addr: u32) -> Result { + if addr & 3 != 0 { + return Err(BusException::LoadAddressMisaligned); + } + match Self::ram(addr, 4) { + Some(p) => Ok(unsafe { (p as *const u32).read() }), + None => self.mmio_read(addr), + } + } +} + +impl BusWriter for GbaBus { + #[inline(always)] + fn write8(&mut self, addr: u32, v: u8) -> Result<(), BusException> { + match Self::ram(addr, 1) { + Some(p) => { + let p = ((p as usize) & !1) as *mut u16; + let shift = (addr & 1) * 8; + unsafe { p.write((p.read() & !(0xff << shift)) | ((v as u16) << shift)) }; + Ok(()) + } + None => self.mmio_write(addr, v as u32), + } + } + + #[inline(always)] + fn write16(&mut self, addr: u32, v: u16) -> Result<(), BusException> { + if addr & 1 != 0 { + return Err(BusException::StoreAddressMisaligned); + } + match Self::ram(addr, 2) { + Some(p) => { + unsafe { (p as *mut u16).write(v) }; + Ok(()) + } + None => self.mmio_write(addr, v as u32), + } + } + + #[inline(always)] + fn write32(&mut self, addr: u32, v: u32) -> Result<(), BusException> { + if addr & 3 != 0 { + return Err(BusException::StoreAddressMisaligned); + } + match Self::ram(addr, 4) { + Some(p) => { + unsafe { (p as *mut u32).write(v) }; + Ok(()) + } + None => self.mmio_write(addr, v), + } + } +} diff --git a/gba/src/console.rs b/gba/src/console.rs new file mode 100644 index 00000000..034e56bc --- /dev/null +++ b/gba/src/console.rs @@ -0,0 +1,217 @@ +//! Guest serial console: a 60x26 text terminal on the Mode 3 bitmap (4x6 +//! font), mirrored line by line to the mGBA debug log. Input comes from the +//! GBA buttons, each of which types a canned shell command. + +use core::fmt; + +use crate::font::GLYPHS; +use crate::gba; + +const COLS: usize = 240 / 4; +const ROWS: usize = 160 / 6; +const FG: u16 = 0x6f7b; // light grey +const BG: u16 = 0x0000; + +/// What each button types into the guest. +const KEYMAP: [(u16, &[u8]); 6] = [ + (gba::KEY_A, b"uname -a\n"), + (gba::KEY_B, b"ls /\n"), + (gba::KEY_L, b"cat /proc/cpuinfo\n"), + (gba::KEY_R, b"free\n"), + (gba::KEY_START, b"uptime\n"), + (gba::KEY_SELECT, b"ps\n"), +]; + +/// Typed once the login prompt shows up. +const AUTOLOGIN: &[u8] = b"root\n"; +/// Typed at the first shell prompt (login flushes anything typed ahead). +const AUTORUN: &[u8] = b"uname -a\n"; + +struct Console { + col: usize, + row: usize, + escape: Escape, + /// Current output line, flushed to the debug log on newline. + line: [u8; 255], + line_len: usize, + /// Last few bytes written, to spot the login prompt. + tail: [u8; 7], + input: [u8; 64], + input_head: usize, + input_len: usize, + keys: u16, + autorun_done: bool, +} + +#[derive(Clone, Copy, PartialEq)] +enum Escape { + None, + Esc, + Csi, +} + +static mut CONSOLE: Console = Console { + col: 0, + row: 0, + escape: Escape::None, + line: [0; 255], + line_len: 0, + tail: [0; 7], + input: [0; 64], + input_head: 0, + input_len: 0, + keys: 0, + autorun_done: false, +}; + +fn console() -> &'static mut Console { + unsafe { &mut *core::ptr::addr_of_mut!(CONSOLE) } +} + +pub fn init() { + gba::mode3(); + gba::fill_lines(0, 160, BG); +} + +/// Print which button types which command. +pub fn print_help() { + for (key, cmd) in KEYMAP { + for b in gba::key_name(key).bytes().chain(*b": ") { + write_byte(b); + } + cmd.iter().copied().for_each(write_byte); + } +} + +pub fn write_byte(b: u8) { + let c = console(); + c.tail.copy_within(1.., 0); + c.tail[c.tail.len() - 1] = b; + if &c.tail == b"login: " { + c.push_input(AUTOLOGIN); + } + if !c.autorun_done && c.tail.ends_with(b"~ # ") { + c.autorun_done = true; + c.push_input(AUTORUN); + } + + match (c.escape, b) { + // Swallow ANSI escape sequences. + (Escape::None, 0x1b) => c.escape = Escape::Esc, + (Escape::Esc, b'[') => c.escape = Escape::Csi, + (Escape::Esc, _) => c.escape = Escape::None, + (Escape::Csi, 0x40..=0x7e) => c.escape = Escape::None, + (Escape::Csi, _) => {} + (Escape::None, b'\n') => { + c.flush_log(); + c.newline(); + } + (Escape::None, b'\r') => c.col = 0, + (Escape::None, 0x08) => c.col = c.col.saturating_sub(1), + (Escape::None, b'\t') => { + for _ in 0..(8 - c.col % 8) { + c.put(b' '); + } + } + (Escape::None, 0x20..=0x7e) => c.put(b), + _ => {} + } +} + +impl Console { + fn put(&mut self, b: u8) { + if self.col == COLS { + self.newline(); + } + draw_glyph(self.col, self.row, b); + self.col += 1; + if self.line_len < self.line.len() { + self.line[self.line_len] = b; + self.line_len += 1; + } + } + + fn newline(&mut self) { + self.col = 0; + if self.row + 1 < ROWS { + self.row += 1; + } else { + gba::scroll_up(6, ROWS * 6, BG); + } + } + + fn flush_log(&mut self) { + gba::log(&self.line[..self.line_len]); + self.line_len = 0; + } + + fn push_input(&mut self, bytes: &[u8]) { + for &b in bytes { + if self.input_len == self.input.len() { + return; + } + self.input[(self.input_head + self.input_len) % self.input.len()] = b; + self.input_len += 1; + } + } + + /// Turn newly pressed buttons into typed commands. + fn poll_keys(&mut self) { + let keys = gba::keys(); + let pressed = keys & !self.keys; + self.keys = keys; + if pressed != 0 && self.input_len == 0 { + if let Some((_, cmd)) = KEYMAP.iter().find(|(key, _)| pressed & key != 0) { + self.push_input(cmd); + } + } + } +} + +fn draw_glyph(col: usize, row: usize, b: u8) { + let glyph = &GLYPHS[(b - 0x20) as usize]; + for (y, bits) in glyph.iter().enumerate() { + let mut px = [BG; 4]; + for (x, p) in px.iter_mut().enumerate() { + if bits & (8 >> x) != 0 { + *p = FG; + } + } + gba::put_pixels(col * 4, row * 6 + y, &px); + } +} + +/// Called whenever the guest polls the UART line status. +pub fn has_input() -> bool { + let c = console(); + c.poll_keys(); + c.input_len != 0 +} + +pub fn read_byte() -> Option { + let c = console(); + if c.input_len == 0 { + return None; + } + let b = c.input[c.input_head]; + c.input_head = (c.input_head + 1) % c.input.len(); + c.input_len -= 1; + Some(b) +} + +pub struct Writer; + +impl fmt::Write for Writer { + fn write_str(&mut self, s: &str) -> fmt::Result { + s.bytes().for_each(write_byte); + Ok(()) + } +} + +#[macro_export] +macro_rules! println { + ($($arg:tt)*) => {{ + use core::fmt::Write; + let _ = writeln!($crate::console::Writer, $($arg)*); + }}; +} diff --git a/gba/src/font.rs b/gba/src/font.rs new file mode 100644 index 00000000..323bb880 --- /dev/null +++ b/gba/src/font.rs @@ -0,0 +1,99 @@ +// 4x6 glyphs for ASCII 0x20-0x7e, one byte per row (bit 3 = leftmost pixel). +// Generated from embedded-graphics' FONT_4X6 (X11 misc-fixed, public domain). +pub const GLYPHS: [[u8; 6]; 95] = [ + [0x0, 0x0, 0x0, 0x0, 0x0, 0x0], // ' ' + [0x4, 0x4, 0x4, 0x0, 0x4, 0x0], // '!' + [0xa, 0xa, 0x0, 0x0, 0x0, 0x0], // '"' + [0xa, 0xf, 0xa, 0xf, 0xa, 0x0], // '#' + [0x4, 0xe, 0xc, 0x2, 0xe, 0x4], // '$' + [0x8, 0x2, 0x4, 0x8, 0x2, 0x0], // '%' + [0x4, 0xa, 0x4, 0xa, 0x5, 0x0], // '&' + [0x4, 0x4, 0x0, 0x0, 0x0, 0x0], // '\'' + [0x2, 0x4, 0x4, 0x4, 0x4, 0x2], // '(' + [0x8, 0x4, 0x4, 0x4, 0x4, 0x8], // ')' + [0xa, 0x4, 0xe, 0x4, 0xa, 0x0], // '*' + [0x4, 0x4, 0xe, 0x4, 0x4, 0x0], // '+' + [0x0, 0x0, 0x0, 0x0, 0x4, 0x8], // ',' + [0x0, 0x0, 0xe, 0x0, 0x0, 0x0], // '-' + [0x0, 0x0, 0x0, 0x0, 0x4, 0x0], // '.' + [0x2, 0x2, 0x4, 0x8, 0x8, 0x0], // '/' + [0x4, 0xa, 0xe, 0xa, 0x4, 0x0], // '0' + [0x4, 0xc, 0x4, 0x4, 0xe, 0x0], // '1' + [0x4, 0xa, 0x2, 0x4, 0xe, 0x0], // '2' + [0xe, 0x2, 0x4, 0x2, 0xc, 0x0], // '3' + [0xa, 0xa, 0xe, 0x2, 0x2, 0x0], // '4' + [0xe, 0x8, 0xc, 0x2, 0xc, 0x0], // '5' + [0x6, 0x8, 0xc, 0xa, 0x4, 0x0], // '6' + [0xe, 0x2, 0x4, 0x8, 0x8, 0x0], // '7' + [0x6, 0xa, 0x4, 0xa, 0xc, 0x0], // '8' + [0x4, 0xa, 0x6, 0x2, 0xc, 0x0], // '9' + [0x0, 0x4, 0x0, 0x0, 0x4, 0x0], // ':' + [0x0, 0x4, 0x0, 0x0, 0x4, 0x8], // ';' + [0x2, 0x4, 0x8, 0x4, 0x2, 0x0], // '<' + [0x0, 0xe, 0x0, 0xe, 0x0, 0x0], // '=' + [0x8, 0x4, 0x2, 0x4, 0x8, 0x0], // '>' + [0xc, 0x2, 0x4, 0x0, 0x4, 0x0], // '?' + [0x6, 0xa, 0xa, 0x8, 0x6, 0x0], // '@' + [0x4, 0xa, 0xe, 0xa, 0xa, 0x0], // 'A' + [0xc, 0xa, 0xc, 0xa, 0xc, 0x0], // 'B' + [0x4, 0xa, 0x8, 0xa, 0x4, 0x0], // 'C' + [0xc, 0xa, 0xa, 0xa, 0xc, 0x0], // 'D' + [0xe, 0x8, 0xc, 0x8, 0xe, 0x0], // 'E' + [0xe, 0x8, 0xc, 0x8, 0x8, 0x0], // 'F' + [0x6, 0x8, 0xa, 0xa, 0x6, 0x0], // 'G' + [0xa, 0xa, 0xe, 0xa, 0xa, 0x0], // 'H' + [0xe, 0x4, 0x4, 0x4, 0xe, 0x0], // 'I' + [0x2, 0x2, 0x2, 0xa, 0x4, 0x0], // 'J' + [0xa, 0xa, 0xc, 0xa, 0xa, 0x0], // 'K' + [0x8, 0x8, 0x8, 0x8, 0xe, 0x0], // 'L' + [0xa, 0xe, 0xe, 0xa, 0xa, 0x0], // 'M' + [0x2, 0xa, 0xe, 0xa, 0x8, 0x0], // 'N' + [0x4, 0xa, 0xa, 0xa, 0x4, 0x0], // 'O' + [0xc, 0xa, 0xc, 0x8, 0x8, 0x0], // 'P' + [0x4, 0xa, 0xa, 0xa, 0x4, 0x2], // 'Q' + [0xc, 0xa, 0xc, 0xa, 0xa, 0x0], // 'R' + [0x6, 0x8, 0x4, 0x2, 0xc, 0x0], // 'S' + [0xe, 0x4, 0x4, 0x4, 0x4, 0x0], // 'T' + [0xa, 0xa, 0xa, 0xa, 0xe, 0x0], // 'U' + [0xa, 0xa, 0xa, 0xe, 0x4, 0x0], // 'V' + [0xa, 0xa, 0xe, 0xe, 0xa, 0x0], // 'W' + [0xa, 0xa, 0x4, 0xa, 0xa, 0x0], // 'X' + [0xa, 0xa, 0x4, 0x4, 0x4, 0x0], // 'Y' + [0xe, 0x2, 0x4, 0x8, 0xe, 0x0], // 'Z' + [0x6, 0x4, 0x4, 0x4, 0x6, 0x0], // '[' + [0x8, 0x8, 0x4, 0x2, 0x2, 0x0], // '\\' + [0xc, 0x4, 0x4, 0x4, 0xc, 0x0], // ']' + [0x4, 0xa, 0x0, 0x0, 0x0, 0x0], // '^' + [0x0, 0x0, 0x0, 0x0, 0x0, 0xe], // '_' + [0x4, 0x2, 0x0, 0x0, 0x0, 0x0], // '`' + [0x0, 0x6, 0xa, 0xa, 0x6, 0x0], // 'a' + [0x8, 0xc, 0xa, 0xa, 0xc, 0x0], // 'b' + [0x0, 0x6, 0x8, 0x8, 0x6, 0x0], // 'c' + [0x2, 0x6, 0xa, 0xa, 0x6, 0x0], // 'd' + [0x0, 0x4, 0xa, 0xc, 0x6, 0x0], // 'e' + [0x2, 0x4, 0xe, 0x4, 0x4, 0x0], // 'f' + [0x0, 0x6, 0xa, 0x6, 0x2, 0xc], // 'g' + [0x8, 0xc, 0xa, 0xa, 0xa, 0x0], // 'h' + [0x4, 0x0, 0xc, 0x4, 0xe, 0x0], // 'i' + [0x2, 0x0, 0x2, 0x2, 0x2, 0xc], // 'j' + [0x8, 0xa, 0xc, 0xa, 0xa, 0x0], // 'k' + [0xc, 0x4, 0x4, 0x4, 0xe, 0x0], // 'l' + [0x0, 0xa, 0xe, 0xa, 0xa, 0x0], // 'm' + [0x0, 0xc, 0xa, 0xa, 0xa, 0x0], // 'n' + [0x0, 0x4, 0xa, 0xa, 0x4, 0x0], // 'o' + [0x0, 0xc, 0xa, 0xc, 0x8, 0x8], // 'p' + [0x0, 0x6, 0xa, 0xa, 0x6, 0x2], // 'q' + [0x0, 0xa, 0xc, 0x8, 0x8, 0x0], // 'r' + [0x0, 0x6, 0xc, 0x2, 0xc, 0x0], // 's' + [0x4, 0xe, 0x4, 0x4, 0x2, 0x0], // 't' + [0x0, 0xa, 0xa, 0xa, 0x6, 0x0], // 'u' + [0x0, 0xa, 0xa, 0xa, 0x4, 0x0], // 'v' + [0x0, 0xa, 0xa, 0xe, 0xa, 0x0], // 'w' + [0x0, 0xa, 0x4, 0x4, 0xa, 0x0], // 'x' + [0x0, 0xa, 0xa, 0x6, 0x2, 0xc], // 'y' + [0x0, 0xe, 0x2, 0x4, 0xe, 0x0], // 'z' + [0x2, 0x4, 0xc, 0x4, 0x4, 0x2], // '{' + [0x4, 0x4, 0x4, 0x4, 0x4, 0x0], // '|' + [0x8, 0x4, 0x6, 0x4, 0x4, 0x8], // '}' + [0x5, 0xa, 0x0, 0x0, 0x0, 0x0], // '~' +]; diff --git a/gba/src/gba.rs b/gba/src/gba.rs new file mode 100644 index 00000000..f58b5225 --- /dev/null +++ b/gba/src/gba.rs @@ -0,0 +1,107 @@ +//! GBA hardware registers used by the host program. + +const REG_DISPCNT: *mut u16 = 0x0400_0000 as *mut u16; +const REG_VCOUNT: *const u16 = 0x0400_0006 as *const u16; +const REG_DMA3SAD: *mut u32 = 0x0400_00d4 as *mut u32; +const REG_DMA3DAD: *mut u32 = 0x0400_00d8 as *mut u32; +const REG_DMA3CNT: *mut u32 = 0x0400_00dc as *mut u32; +const REG_KEYINPUT: *const u16 = 0x0400_0130 as *const u16; +const REG_WAITCNT: *mut u16 = 0x0400_0204 as *mut u16; +const VRAM: *mut u16 = 0x0600_0000 as *mut u16; +const MGBA_LOG_ENABLE: *mut u16 = 0x04ff_f780 as *mut u16; +const MGBA_LOG_FLAGS: *mut u16 = 0x04ff_f700 as *mut u16; +const MGBA_LOG_BUFFER: *mut u8 = 0x04ff_f600 as *mut u8; + +const WIDTH: usize = 240; + +pub const KEY_A: u16 = 1 << 0; +pub const KEY_B: u16 = 1 << 1; +pub const KEY_SELECT: u16 = 1 << 2; +pub const KEY_START: u16 = 1 << 3; +pub const KEY_R: u16 = 1 << 8; +pub const KEY_L: u16 = 1 << 9; + +pub fn init() { + unsafe { + // GamePak WS0: 3 cycles first access, 1 cycle sequential, prefetch on. + REG_WAITCNT.write_volatile(0x4317); + // mGBA / rgba debug log. + MGBA_LOG_ENABLE.write_volatile(0xc0de); + } +} + +/// Emit one line through the mGBA debug log (no-op on real hardware). +pub fn log(line: &[u8]) { + let len = line.len().min(255); + unsafe { + for (i, &b) in line[..len].iter().enumerate() { + MGBA_LOG_BUFFER.add(i).write_volatile(b); + } + MGBA_LOG_BUFFER.add(len).write_volatile(0); + // Level 3 = info, bit 8 = send. + MGBA_LOG_FLAGS.write_volatile(0x103); + } +} + +pub fn vsync() { + unsafe { + while REG_VCOUNT.read_volatile() >= 160 {} + while REG_VCOUNT.read_volatile() < 160 {} + } +} + +/// Buttons currently held (bit set = pressed). +pub fn keys() -> u16 { + unsafe { !REG_KEYINPUT.read_volatile() & 0x3ff } +} + +pub fn key_name(key: u16) -> &'static str { + match key { + KEY_A => "A", + KEY_B => "B", + KEY_SELECT => "SELECT", + KEY_START => "START", + KEY_R => "R", + KEY_L => "L", + _ => "?", + } +} + +/// 240x160 15-bit bitmap mode with BG2 enabled. +pub fn mode3() { + unsafe { REG_DISPCNT.write_volatile(0x0403) }; +} + +pub fn put_pixels(x: usize, y: usize, px: &[u16]) { + let dst = unsafe { VRAM.add(y * WIDTH + x) }; + for (i, &p) in px.iter().enumerate() { + unsafe { dst.add(i).write_volatile(p) }; + } +} + +/// DMA3 word copy. +fn dma3(src: *const u32, dst: *mut u32, words: usize) { + let ctrl = (1 << 31) | (1 << 26); + unsafe { + REG_DMA3SAD.write_volatile(src as u32); + REG_DMA3DAD.write_volatile(dst as u32); + REG_DMA3CNT.write_volatile(ctrl | words as u32); + } +} + +pub fn fill_lines(from: usize, to: usize, color: u16) { + // Plain stores: a DMA fill would read its source word behind the + // compiler's back, so the store of that word could be optimized away. + let fill = (color as u32) << 16 | color as u32; + let dst = unsafe { VRAM.add(from * WIDTH) } as *mut u32; + for i in 0..(to - from) * WIDTH / 2 { + unsafe { dst.add(i).write_volatile(fill) }; + } +} + +/// Move lines [by, height) up by `by` lines and clear the gap at the bottom. +pub fn scroll_up(by: usize, height: usize, color: u16) { + let src = unsafe { VRAM.add(by * WIDTH) } as *const u32; + dma3(src, VRAM as *mut u32, (height - by) * WIDTH / 2); + fill_lines(height - by, height, color); +} diff --git a/gba/src/main.rs b/gba/src/main.rs new file mode 100644 index 00000000..1f75eddc --- /dev/null +++ b/gba/src/main.rs @@ -0,0 +1,158 @@ +//! r2 on the Game Boy Advance. +//! +//! The GBA only has 288KB of RAM, far too little for Linux. Flash carts that +//! run games out of PSRAM expose the GamePak ROM window as writable memory, so +//! the upper 16MB of it (0x09000000-) is used as the RISC-V guest's RAM. The +//! kernel image and DTB are embedded in the ROM and copied there at boot. +#![no_std] +#![no_main] + +mod bus; +mod console; +mod font; +mod gba; + +use core::arch::global_asm; + +use r2_core::cpu::{Cpu, CpuState}; + +use bus::{GbaBus, GUEST_RAM, RAM_SIZE, RAM_START}; + +#[repr(C, align(4))] +struct Aligned(T); + +#[cfg(not(feature = "baremetal"))] +static IMAGE: &Aligned<[u8]> = &Aligned(*include_bytes!("../../fixtures/linux.bin")); +#[cfg(not(feature = "baremetal"))] +static DTB: Option<&Aligned<[u8]>> = Some(&Aligned(*include_bytes!("../../fixtures/default.dtb"))); + +#[cfg(feature = "baremetal")] +static IMAGE: &Aligned<[u8]> = &Aligned(*include_bytes!("../../fixtures/baremetal/baremetal.bin")); +#[cfg(feature = "baremetal")] +static DTB: Option<&Aligned<[u8]>> = None; + +// Entry point: GBA cartridge header, stacks, IWRAM/BSS setup, then `main`. +global_asm!( + r#" + .section .text.crt0, "ax" + .arm + .global _start +_start: + b 1f + .space 0xbc +1: + mov r0, #0x12 + msr CPSR_c, r0 + ldr sp, =0x03007fa0 + mov r0, #0x1f + msr CPSR_c, r0 + ldr sp, =0x03007f00 + + ldr r0, =__iwram_lma + ldr r1, =__iwram_start + ldr r2, =__iwram_end +2: + cmp r1, r2 + ldrlo r3, [r0], #4 + strlo r3, [r1], #4 + blo 2b + + ldr r1, =__bss_start + ldr r2, =__bss_end + mov r3, #0 +3: + cmp r1, r2 + strlo r3, [r1], #4 + blo 3b + + ldr r0, =main + bx r0 + .ltorg + .arm +"# +); + +#[no_mangle] +extern "C" fn main() -> ! { + gba::init(); + console::init(); + println!("r2-gba: RISC-V emulator on GBA"); + + if !bus::probe_psram() { + println!("PSRAM not found: the ROM area is read-only."); + println!("Use a flash cart with PSRAM (or RGBA_PSRAM=1 on rgba)."); + halt(); + } + + load_guest(); + let dtb_ref = place_dtb(); + println!("image {} bytes, RAM {}MB", IMAGE.0.len(), RAM_SIZE >> 20); + console::print_help(); + + let mut cpu = Cpu::new(GbaBus::new()); + cpu.a0(0).a1(dtb_ref).pc(RAM_START); + run(&mut cpu); + + println!("\npower off"); + halt(); +} + +/// Copy the kernel image to the start of guest RAM. +fn load_guest() { + let src = IMAGE.0.as_ptr() as *const u32; + let words = IMAGE.0.len().div_ceil(4); + for i in 0..words { + unsafe { GUEST_RAM.add(i).write(src.add(i).read()) }; + } +} + +/// Put the DTB at the end of guest RAM and tell the kernel that RAM ends +/// where the DTB begins (same trick as mini-rv32ima). Returns its guest address. +fn place_dtb() -> u32 { + let Some(dtb) = DTB else { return 0 }; + let len = dtb.0.len().next_multiple_of(4); + let offset = RAM_SIZE as usize - len; + let src = dtb.0.as_ptr() as *const u32; + let dst = unsafe { GUEST_RAM.add(offset / 4) }; + for i in 0..len / 4 { + unsafe { dst.add(i).write(src.add(i).read()) }; + } + // /memory/reg size cell, big-endian. Only patch the default placeholder. + let size_cell = unsafe { dst.add(0x13c / 4) }; + if unsafe { size_cell.read() } == 0x00c0_ff03 { + unsafe { size_cell.write((offset as u32).swap_bytes()) }; + } + RAM_START + offset as u32 +} + +/// The emulation loop. Lives in IWRAM (32-bit, zero wait state) together with +/// the CPU core inlined into it. +#[inline(never)] +#[link_section = ".iwram.run"] +fn run(cpu: &mut Cpu) { + loop { + match cpu.step() { + CpuState::Active => { + let bus = cpu.bus(); + if bus.power_off || bus.reboot { + return; + } + } + // WFI: nothing to do until the next timer interrupt, so jump the + // clock straight to it. + CpuState::Idle => cpu.bus_mut().skip_to_timer(), + } + } +} + +fn halt() -> ! { + loop { + gba::vsync(); + } +} + +#[panic_handler] +fn panic(info: &core::panic::PanicInfo) -> ! { + println!("\npanic: {}", info); + halt(); +}