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 00000000..2dc3c52f Binary files /dev/null and b/gba/screenshot.png differ 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(); +}