diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 847660d..72f3418 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -22,7 +22,7 @@ jobs: rustup component add llvm-tools-preview cargo install cargo-binutils - name: Build - run: ./build.sh + run: ./build.sh --verbose - name: Upload files to Release if: github.event_name == 'push' && startswith(github.ref, 'refs/tags/') uses: softprops/action-gh-release@v1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 710b428..0d1c301 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased changes +## v0.4.0 + +* The `load` command now takes ELF binaries, not raw binaries. +* Neotron OS can now be used as a dependency within an application, if desired. + ## v0.3.3 * Add `dir` command diff --git a/Cargo.lock b/Cargo.lock index 5372c17..d5a352f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "bitflags" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6776fc96284a0bb647b615056fc496d1fe1644a7ab01829818a6d91cae888b84" + [[package]] name = "byteorder" version = "1.4.3" @@ -119,6 +125,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d" +[[package]] +name = "neotron-api" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82c00f842e7006421002e67a53866b90ddd6f7d86137b52d2147f5e38b35e82f" +dependencies = [ + "bitflags", + "neotron-ffi", +] + [[package]] name = "neotron-common-bios" version = "0.8.0" @@ -129,14 +145,28 @@ dependencies = [ "pc-keyboard", ] +[[package]] +name = "neotron-ffi" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d37886e73d87732421aaf5da617eead9d69a7daf6b0d059780f76157d9ce5372" + +[[package]] +name = "neotron-loader" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9b8634a088b9d5b338a96b3f6ef45a3bc0b9c0f0d562c7d00e498265fd96e8f" + [[package]] name = "neotron-os" -version = "0.3.3" +version = "0.4.0" dependencies = [ "chrono", "embedded-sdmmc", "menu", + "neotron-api", "neotron-common-bios", + "neotron-loader", "pc-keyboard", "postcard", "r0", diff --git a/Cargo.toml b/Cargo.toml index 8629cf3..1bbb4af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,10 @@ [package] name = "neotron-os" -version = "0.3.3" -authors = ["Jonathan 'theJPster' Pallant ", "The Neotron Developers"] +version = "0.4.0" +authors = [ + "Jonathan 'theJPster' Pallant ", + "The Neotron Developers" +] edition = "2018" description = "The Neotron Operating System" license = "GPL-3.0-or-later" @@ -46,3 +49,8 @@ serde = { version = "1.0", default-features = false } menu = "0.3" chrono = { version = "0.4", default-features = false } embedded-sdmmc = { version = "0.5", default-features = false } +neotron-api = "0.1" +neotron-loader = "0.1" + +[features] +lib-mode = [] diff --git a/build.sh b/build.sh index d235682..4b17559 100755 --- a/build.sh +++ b/build.sh @@ -11,9 +11,9 @@ for TARGET_ARCH in thumbv6m-none-eabi thumbv7m-none-eabi thumbv7em-none-eabi; do echo "TARGET is ${TARGET_ARCH}" for BINARY in flash0002 flash0802 flash1002; do echo "BINARY is ${BINARY}" - cargo build --verbose --release --target=${TARGET_ARCH} --bin ${BINARY} + cargo build $* --release --target=${TARGET_ARCH} --bin ${BINARY} # objcopy would do the build for us first, but it doesn't have good build output - cargo objcopy --verbose --release --target=${TARGET_ARCH} --bin ${BINARY} -- -O binary ${RELEASE_DIR}/${TARGET_ARCH}-${BINARY}-libneotron_os.bin + cargo objcopy $* --release --target=${TARGET_ARCH} --bin ${BINARY} -- -O binary ${RELEASE_DIR}/${TARGET_ARCH}-${BINARY}-libneotron_os.bin # Keep the ELF file too (for debugging) cp ./target/${TARGET_ARCH}/release/${BINARY} ${RELEASE_DIR}/${TARGET_ARCH}-${BINARY}-libneotron_os.elf done diff --git a/neotron-flash-0002.ld b/neotron-flash-0002.ld index f7d3a28..a7f56ea 100644 --- a/neotron-flash-0002.ld +++ b/neotron-flash-0002.ld @@ -32,7 +32,11 @@ MEMORY /* # Entry point = what the BIOS calls to start the OS */ ENTRY(main); -EXTERN(__RESET_VECTOR); + +/* +Where the Transient Program Area starts. +*/ +_tpa_start = ORIGIN(RAM) + LENGTH(RAM); /* # Sections */ SECTIONS diff --git a/neotron-flash-0802.ld b/neotron-flash-0802.ld index 2859cbe..cd36166 100644 --- a/neotron-flash-0802.ld +++ b/neotron-flash-0802.ld @@ -32,7 +32,11 @@ MEMORY /* # Entry point = what the BIOS calls to start the OS */ ENTRY(main); -EXTERN(__RESET_VECTOR); + +/* +Where the Transient Program Area starts. +*/ +_tpa_start = ORIGIN(RAM) + LENGTH(RAM); /* # Sections */ SECTIONS diff --git a/neotron-flash-1002.ld b/neotron-flash-1002.ld index 9a09f2c..d2e0f2a 100644 --- a/neotron-flash-1002.ld +++ b/neotron-flash-1002.ld @@ -23,16 +23,24 @@ MEMORY { /* The first 128 KiB is for the BIOS. We get the rest. */ FLASH (rx) : ORIGIN = 0x10020000, LENGTH = 128K + /* - * We get the bottom 4KB of RAM. Anything above that is for applications - * (up to wherever the BIOS tells us we can use.) + * The RAM reserved for the OS. Above this is the Transient Program Area. + * + * This is defined by the Neotron specification for a given platform. On this + * Cortex-M based platform, it's the start of Cortex-M SRAM, plus 4 KiB, or + * 0x2000_1000. */ RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 4K } /* # Entry point = what the BIOS calls to start the OS */ ENTRY(main); -EXTERN(__RESET_VECTOR); + +/* +Where the Transient Program Area starts. +*/ +_tpa_start = ORIGIN(RAM) + LENGTH(RAM); /* # Sections */ SECTIONS diff --git a/src/bin/flash0002.rs b/src/bin/flash0002.rs index 289dca3..de2fc0f 100644 --- a/src/bin/flash0002.rs +++ b/src/bin/flash0002.rs @@ -13,4 +13,4 @@ /// of our portion of Flash. #[link_section = ".entry_point"] #[used] -pub static ENTRY_POINT_ADDR: extern "C" fn(*const neotron_common_bios::Api) -> ! = neotron_os::main; +pub static ENTRY_POINT_ADDR: extern "C" fn(&neotron_common_bios::Api) -> ! = neotron_os::os_main; diff --git a/src/bin/flash0802.rs b/src/bin/flash0802.rs index ab8e020..340707b 100644 --- a/src/bin/flash0802.rs +++ b/src/bin/flash0802.rs @@ -13,4 +13,4 @@ /// of our portion of Flash. #[link_section = ".entry_point"] #[used] -pub static ENTRY_POINT_ADDR: extern "C" fn(*const neotron_common_bios::Api) -> ! = neotron_os::main; +pub static ENTRY_POINT_ADDR: extern "C" fn(&neotron_common_bios::Api) -> ! = neotron_os::os_main; diff --git a/src/bin/flash1002.rs b/src/bin/flash1002.rs index 9e44849..33fa480 100644 --- a/src/bin/flash1002.rs +++ b/src/bin/flash1002.rs @@ -13,4 +13,4 @@ /// of our portion of Flash. #[link_section = ".entry_point"] #[used] -pub static ENTRY_POINT_ADDR: extern "C" fn(*const neotron_common_bios::Api) -> ! = neotron_os::main; +pub static ENTRY_POINT_ADDR: extern "C" fn(&neotron_common_bios::Api) -> ! = neotron_os::os_main; diff --git a/src/commands/config.rs b/src/commands/config.rs index 18b5f8d..fedf676 100644 --- a/src/commands/config.rs +++ b/src/commands/config.rs @@ -22,7 +22,7 @@ pub static COMMAND_ITEM: menu::Item = menu::Item { /// Called when the "config" command is executed. fn command(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], ctx: &mut Ctx) { - let command = args.get(0).cloned().unwrap_or("print"); + let command = args.first().cloned().unwrap_or("print"); match command { "reset" => match config::Config::load() { Ok(new_config) => { diff --git a/src/commands/fs.rs b/src/commands/fs.rs index fae0138..e750d20 100644 --- a/src/commands/fs.rs +++ b/src/commands/fs.rs @@ -1,9 +1,8 @@ //! File Systems related commands for Neotron OS -use chrono::{Datelike, Timelike}; use embedded_sdmmc::VolumeIdx; -use crate::{bios, print, println, Ctx, API}; +use crate::{bios, print, println, Ctx}; pub static DIR_ITEM: menu::Item = menu::Item { item_type: menu::ItemType::Callback { @@ -14,7 +13,6 @@ pub static DIR_ITEM: menu::Item = menu::Item { help: Some("Dir the root directory on block device 0"), }; -#[cfg(target_os = "none")] pub static LOAD_ITEM: menu::Item = menu::Item { item_type: menu::ItemType::Callback { function: load, @@ -27,89 +25,12 @@ pub static LOAD_ITEM: menu::Item = menu::Item { help: Some("Load a file into the application area"), }; -struct BiosBlock(); - -impl embedded_sdmmc::BlockDevice for BiosBlock { - type Error = bios::Error; - - fn read( - &self, - blocks: &mut [embedded_sdmmc::Block], - start_block_idx: embedded_sdmmc::BlockIdx, - _reason: &str, - ) -> Result<(), Self::Error> { - let api = API.get(); - let byte_slice = unsafe { - core::slice::from_raw_parts_mut( - blocks.as_mut_ptr() as *mut u8, - blocks.len() * embedded_sdmmc::Block::LEN, - ) - }; - match (api.block_read)( - 0, - bios::block_dev::BlockIdx(u64::from(start_block_idx.0)), - blocks.len() as u8, - bios::ApiBuffer::new(byte_slice), - ) { - bios::Result::Ok(_) => Ok(()), - bios::Result::Err(e) => Err(e), - } - } - - fn write( - &self, - blocks: &[embedded_sdmmc::Block], - start_block_idx: embedded_sdmmc::BlockIdx, - ) -> Result<(), Self::Error> { - let api = API.get(); - let byte_slice = unsafe { - core::slice::from_raw_parts( - blocks.as_ptr() as *const u8, - blocks.len() * embedded_sdmmc::Block::LEN, - ) - }; - match (api.block_write)( - 0, - bios::block_dev::BlockIdx(u64::from(start_block_idx.0)), - blocks.len() as u8, - bios::ApiByteSlice::new(byte_slice), - ) { - bios::Result::Ok(_) => Ok(()), - bios::Result::Err(e) => Err(e), - } - } - - fn num_blocks(&self) -> Result { - let api = API.get(); - match (api.block_dev_get_info)(0) { - bios::Option::Some(info) => Ok(embedded_sdmmc::BlockCount(info.num_blocks as u32)), - bios::Option::None => Err(bios::Error::InvalidDevice), - } - } -} - -struct BiosTime(); - -impl embedded_sdmmc::TimeSource for BiosTime { - fn get_timestamp(&self) -> embedded_sdmmc::Timestamp { - let time = API.get_time(); - embedded_sdmmc::Timestamp { - year_since_1970: (time.year() - 1970) as u8, - zero_indexed_month: time.month0() as u8, - zero_indexed_day: time.day0() as u8, - hours: time.hour() as u8, - minutes: time.minute() as u8, - seconds: time.second() as u8, - } - } -} - /// Called when the "dir" command is executed. fn dir(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: &mut Ctx) { fn work() -> Result<(), embedded_sdmmc::Error> { println!("Listing files on Block Device 0, /"); - let bios_block = BiosBlock(); - let time = BiosTime(); + let bios_block = crate::fs::BiosBlock(); + let time = crate::fs::BiosTime(); let mut mgr = embedded_sdmmc::VolumeManager::new(bios_block, time); // Open the first partition let volume = mgr.get_volume(VolumeIdx(0))?; @@ -165,33 +86,12 @@ fn dir(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: & } /// Called when the "load" command is executed. -#[cfg(target_os = "none")] -fn load(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], _ctx: &mut Ctx) { - fn work(args: &[&str]) -> Result<(), embedded_sdmmc::Error> { - println!("Loading /{} from Block Device 0", args[0]); - let bios_block = BiosBlock(); - let time = BiosTime(); - let mut mgr = embedded_sdmmc::VolumeManager::new(bios_block, time); - // Open the first partition - let mut volume = mgr.get_volume(VolumeIdx(0))?; - let root_dir = mgr.open_root_dir(&volume)?; - let mut file = mgr.open_file_in_dir( - &mut volume, - &root_dir, - args[0], - embedded_sdmmc::Mode::ReadOnly, - )?; - let file_length = file.length(); - // Application space starts 4K into Cortex-M SRAM - const APPLICATION_START_ADDR: usize = 0x2000_1000; - let application_ram: &'static mut [u8] = unsafe { - core::slice::from_raw_parts_mut(APPLICATION_START_ADDR as *mut u8, file_length as usize) - }; - mgr.read(&mut volume, &mut file, application_ram)?; - Ok(()) - } - - match work(args) { +fn load(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], ctx: &mut Ctx) { + let Some(filename) = args.first() else { + println!("Need a filename"); + return; + }; + match ctx.tpa.load_program(filename) { Ok(_) => {} Err(e) => { println!("Error: {:?}", e); diff --git a/src/commands/mod.rs b/src/commands/mod.rs index c316eb3..2b83f3a 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -23,9 +23,8 @@ pub static OS_MENU: menu::Menu = menu::Menu { &fs::DIR_ITEM, &hardware::LSHW_ITEM, &ram::HEXDUMP_ITEM, - #[cfg(target_os = "none")] &ram::RUN_ITEM, - #[cfg(target_os = "none")] + &ram::LOAD_ITEM, &fs::LOAD_ITEM, &screen::CLEAR_ITEM, &screen::BENCH_ITEM, diff --git a/src/commands/ram.rs b/src/commands/ram.rs index 5527bfd..99d191c 100644 --- a/src/commands/ram.rs +++ b/src/commands/ram.rs @@ -20,7 +20,6 @@ pub static HEXDUMP_ITEM: menu::Item = menu::Item { help: Some("Dump the contents of RAM as hex"), }; -#[cfg(target_os = "none")] pub static RUN_ITEM: menu::Item = menu::Item { item_type: menu::ItemType::Callback { function: run, @@ -30,13 +29,31 @@ pub static RUN_ITEM: menu::Item = menu::Item { help: Some("Jump to start of application area"), }; +pub static LOAD_ITEM: menu::Item = menu::Item { + item_type: menu::ItemType::Callback { + function: loadf, + parameters: &[ + menu::Parameter::Mandatory { + parameter_name: "address", + help: Some("The address to load from"), + }, + menu::Parameter::Mandatory { + parameter_name: "length", + help: Some("The number of bytes to load"), + }, + ], + }, + command: "loadf", + help: Some("Copy a program from ROM/RAM into the application area"), +}; + fn parse_usize(input: &str) -> Result { if let Some(digits) = input.strip_prefix("0x") { // Parse as hex usize::from_str_radix(digits, 16) } else { // Parse as decimal - usize::from_str_radix(input, 10) + input.parse::() } } @@ -47,7 +64,7 @@ fn parse_usize(input: &str) -> Result { fn hexdump(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], _ctx: &mut Ctx) { const BYTES_PER_LINE: usize = 16; - let Some(address_str) = args.get(0) else { + let Some(address_str) = args.first() else { println!("No address"); return; }; @@ -81,43 +98,51 @@ fn hexdump(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], _ctx println!(); } -#[allow(unused)] -#[repr(C)] -pub struct Api { - pub print: extern "C" fn(data: *const u8, len: usize), -} - -#[allow(unused)] -static CALLBACK_TABLE: Api = Api { print: print_fn }; - -#[allow(unused)] -extern "C" fn print_fn(data: *const u8, len: usize) { - let slice = unsafe { core::slice::from_raw_parts(data, len) }; - if let Ok(s) = core::str::from_utf8(slice) { - print!("{}", s); - } else { - // Ignore App output - not UTF-8 +/// Called when the "run" command is executed. +fn run(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], ctx: &mut Ctx) { + match ctx.tpa.execute() { + Ok(0) => { + println!(); + } + Ok(n) => { + println!("\nError Code: {}", n); + } + Err(e) => { + println!("\nFailed to execute: {:?}", e); + } } } -/// Called when the "run" command is executed. -#[cfg(target_os = "none")] -fn run(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: &mut Ctx) { - use core::convert::TryInto; - const APPLICATION_START_ADDR: usize = 0x2000_1000; - const APPLICATION_LEN: usize = 4096; - // Application space starts 4K into Cortex-M SRAM - let application_ram: &'static mut [u8] = unsafe { - core::slice::from_raw_parts_mut(APPLICATION_START_ADDR as *mut u8, APPLICATION_LEN) +/// Called when the "loadf" command is executed. +/// +/// If you ask for an address that generates a HardFault, the OS will crash. So +/// don't. +fn loadf(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], ctx: &mut Ctx) { + let Some(address_str) = args.first() else { + println!("No address"); + return; }; - let start_word: [u8; 4] = (&application_ram[0..4]).try_into().unwrap(); - let start_ptr = usize::from_le_bytes(start_word) as *const (); - let result = unsafe { - let code: extern "C" fn(*const Api) -> u32 = ::core::mem::transmute(start_ptr); - code(&CALLBACK_TABLE) + let Ok(address) = parse_usize(address_str) else { + println!("Bad address"); + return; }; - println!(); - if result != 0 { - println!("Got error code {}", result); + let Some(len_str) = args.get(1) else { + println!("No length"); + return; + }; + let Ok(len) = parse_usize(len_str) else { + println!("Bad length"); + return; + }; + + let src_slice = unsafe { core::slice::from_raw_parts(address as *const u8, len) }; + + match ctx.tpa.copy_program(src_slice) { + Ok(_) => { + println!("Ok"); + } + Err(e) => { + println!("Error: {:?}", e); + } } } diff --git a/src/fs.rs b/src/fs.rs new file mode 100644 index 0000000..129a65f --- /dev/null +++ b/src/fs.rs @@ -0,0 +1,83 @@ +//! Filesystem related types + +use chrono::{Datelike, Timelike}; +use neotron_common_bios as bios; + +use crate::API; + +pub struct BiosBlock(); + +impl embedded_sdmmc::BlockDevice for BiosBlock { + type Error = bios::Error; + + fn read( + &self, + blocks: &mut [embedded_sdmmc::Block], + start_block_idx: embedded_sdmmc::BlockIdx, + _reason: &str, + ) -> Result<(), Self::Error> { + let api = API.get(); + let byte_slice = unsafe { + core::slice::from_raw_parts_mut( + blocks.as_mut_ptr() as *mut u8, + blocks.len() * embedded_sdmmc::Block::LEN, + ) + }; + match (api.block_read)( + 0, + bios::block_dev::BlockIdx(u64::from(start_block_idx.0)), + blocks.len() as u8, + bios::ApiBuffer::new(byte_slice), + ) { + bios::Result::Ok(_) => Ok(()), + bios::Result::Err(e) => Err(e), + } + } + + fn write( + &self, + blocks: &[embedded_sdmmc::Block], + start_block_idx: embedded_sdmmc::BlockIdx, + ) -> Result<(), Self::Error> { + let api = API.get(); + let byte_slice = unsafe { + core::slice::from_raw_parts( + blocks.as_ptr() as *const u8, + blocks.len() * embedded_sdmmc::Block::LEN, + ) + }; + match (api.block_write)( + 0, + bios::block_dev::BlockIdx(u64::from(start_block_idx.0)), + blocks.len() as u8, + bios::ApiByteSlice::new(byte_slice), + ) { + bios::Result::Ok(_) => Ok(()), + bios::Result::Err(e) => Err(e), + } + } + + fn num_blocks(&self) -> Result { + let api = API.get(); + match (api.block_dev_get_info)(0) { + bios::Option::Some(info) => Ok(embedded_sdmmc::BlockCount(info.num_blocks as u32)), + bios::Option::None => Err(bios::Error::InvalidDevice), + } + } +} + +pub struct BiosTime(); + +impl embedded_sdmmc::TimeSource for BiosTime { + fn get_timestamp(&self) -> embedded_sdmmc::Timestamp { + let time = API.get_time(); + embedded_sdmmc::Timestamp { + year_since_1970: (time.year() - 1970) as u8, + zero_indexed_month: time.month0() as u8, + zero_indexed_day: time.day0() as u8, + hours: time.hour() as u8, + minutes: time.minute() as u8, + seconds: time.second() as u8, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index dbdb67d..2f75c17 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,8 +14,12 @@ use neotron_common_bios as bios; mod commands; mod config; +mod fs; +mod program; mod vgaconsole; +pub use config::Config as OsConfig; + // =========================================================================== // Global Variables // =========================================================================== @@ -48,12 +52,12 @@ static IS_PANIC: AtomicBool = AtomicBool::new(false); #[macro_export] macro_rules! print { ($($arg:tt)*) => { - if let Some(ref mut console) = unsafe { &mut crate::VGA_CONSOLE } { + if let Some(ref mut console) = unsafe { &mut $crate::VGA_CONSOLE } { #[allow(unused)] use core::fmt::Write as _; write!(console, $($arg)*).unwrap(); } - if let Some(ref mut console) = unsafe { &mut crate::SERIAL_CONSOLE } { + if let Some(ref mut console) = unsafe { &mut $crate::SERIAL_CONSOLE } { #[allow(unused)] use core::fmt::Write as _; write!(console, $($arg)*).unwrap(); @@ -64,10 +68,10 @@ macro_rules! print { /// Prints to the screen and puts a new-line on the end #[macro_export] macro_rules! println { - () => (crate::print!("\n")); + () => ($crate::print!("\n")); ($($arg:tt)*) => { - crate::print!($($arg)*); - crate::print!("\n"); + $crate::print!($($arg)*); + $crate::print!("\n"); }; } @@ -129,6 +133,25 @@ impl Api { /// Represents the serial port we can use as a text input/output device. struct SerialConsole(u8); +impl SerialConsole { + fn write_bstr(&mut self, data: &[u8]) -> core::fmt::Result { + let api = API.get(); + let is_panic = IS_PANIC.load(Ordering::SeqCst); + let res = (api.serial_write)( + // Which port + self.0, + // Data + bios::ApiByteSlice::new(data), + // No timeout + bios::Option::None, + ); + if !is_panic { + res.unwrap(); + } + Ok(()) + } +} + impl core::fmt::Write for SerialConsole { fn write_str(&mut self, data: &str) -> core::fmt::Result { let api = API.get(); @@ -151,6 +174,7 @@ impl core::fmt::Write for SerialConsole { pub struct Ctx { config: config::Config, keyboard: pc_keyboard::EventDecoder, + tpa: program::TransientProgramArea, } impl core::fmt::Write for Ctx { @@ -166,7 +190,7 @@ impl core::fmt::Write for Ctx { /// Initialise our global variables - the BIOS will not have done this for us /// (as it doesn't know where they are). -#[cfg(target_os = "none")] +#[cfg(all(target_os = "none", not(feature = "lib-mode")))] unsafe fn start_up_init() { extern "C" { @@ -183,7 +207,7 @@ unsafe fn start_up_init() { r0::init_data(&mut __sdata, &mut __edata, &__sidata); } -#[cfg(not(target_os = "none"))] +#[cfg(any(not(target_os = "none"), feature = "lib-mode"))] unsafe fn start_up_init() { // Nothing to do } @@ -195,7 +219,7 @@ unsafe fn start_up_init() { /// This is the function the BIOS calls. This is because we store the address /// of this function in the ENTRY_POINT_ADDR variable. #[no_mangle] -pub extern "C" fn main(api: *const bios::Api) -> ! { +pub extern "C" fn os_main(api: &bios::Api) -> ! { unsafe { start_up_init(); API.store(api); @@ -242,14 +266,40 @@ pub extern "C" fn main(api: *const bios::Api) -> ! { println!("Welcome to {}!", OS_VERSION); println!("Copyright © Jonathan 'theJPster' Pallant and the Neotron Developers, 2022"); - let ctx = Ctx { + let (tpa_start, tpa_size) = match (api.memory_get_region)(0) { + bios::Option::None => { + panic!("No TPA offered by BIOS!"); + } + bios::Option::Some(tpa) => { + if tpa.length < 256 { + panic!("TPA not large enough"); + } + let offset = tpa.start.align_offset(4); + ( + unsafe { tpa.start.add(offset) as *mut u32 }, + tpa.length - offset, + ) + } + }; + + let mut ctx = Ctx { config, keyboard: pc_keyboard::EventDecoder::new( pc_keyboard::layouts::AnyLayout::Uk105Key(pc_keyboard::layouts::Uk105Key), pc_keyboard::HandleControl::MapLettersToUnicode, ), + tpa: unsafe { + // We have to trust the values given to us by the BIOS. If it lies, we will crash. + program::TransientProgramArea::new(tpa_start, tpa_size) + }, }; + println!( + "TPA: {} bytes @ {:p}", + ctx.tpa.as_slice_u8().len(), + ctx.tpa.as_slice_u8().as_ptr() + ); + let mut buffer = [0u8; 256]; let mut menu = menu::Runner::new(&commands::OS_MENU, &mut buffer, ctx); @@ -296,6 +346,25 @@ pub extern "C" fn main(api: *const bios::Api) -> ! { println!("Failed to get HID events: {:?}", e); } } + if let Some((uart_dev, _serial_conf)) = menu.context.config.get_serial_console() { + loop { + let mut buffer = [0u8; 8]; + let wrapper = neotron_common_bios::ApiBuffer::new(&mut buffer); + match (api.serial_read)(uart_dev, wrapper, neotron_common_bios::Option::None) { + neotron_common_bios::Result::Ok(n) if n == 0 => { + break; + } + neotron_common_bios::Result::Ok(n) => { + for b in &buffer[0..n] { + menu.input_byte(*b); + } + } + _ => { + break; + } + } + } + } (api.power_idle)(); } } @@ -303,6 +372,7 @@ pub extern "C" fn main(api: *const bios::Api) -> ! { /// Called when we have a panic. #[inline(never)] #[panic_handler] +#[cfg(not(feature = "lib-mode"))] fn panic(info: &core::panic::PanicInfo) -> ! { IS_PANIC.store(true, Ordering::SeqCst); println!("PANIC!\n{:#?}", info); diff --git a/src/program.rs b/src/program.rs new file mode 100644 index 0000000..c629091 --- /dev/null +++ b/src/program.rs @@ -0,0 +1,475 @@ +//! Program Loading and Execution + +use embedded_sdmmc::File; + +use crate::{ + fs::{BiosBlock, BiosTime}, + print, println, +}; + +#[allow(unused)] +static CALLBACK_TABLE: neotron_api::Api = neotron_api::Api { + open: api_open, + close: api_close, + write: api_write, + read: api_read, + seek_set: api_seek_set, + seek_cur: api_seek_cur, + seek_end: api_seek_end, + rename: api_rename, + ioctl: api_ioctl, + opendir: api_opendir, + closedir: api_closedir, + readdir: api_readdir, + stat: api_stat, + fstat: api_fstat, + deletefile: api_deletefile, + deletedir: api_deletedir, + chdir: api_chdir, + dchdir: api_dchdir, + pwd: api_pwd, + malloc: api_malloc, + free: api_free, +}; + +/// Ways in which loading a program can fail. +#[derive(Debug)] +pub enum Error { + /// The file was too large for RAM. + ProgramTooLarge, + /// A filesystem error occurred + Filesystem(embedded_sdmmc::Error), + /// An ELF error occurred + Elf(neotron_loader::Error>), + /// Tried to run when nothing was loaded + NothingLoaded, +} + +impl From> for Error { + fn from(value: embedded_sdmmc::Error) -> Self { + Error::Filesystem(value) + } +} + +impl From>> for Error { + fn from( + value: neotron_loader::Error>, + ) -> Self { + Error::Elf(value) + } +} + +/// Something the ELF loader can use to get bytes off the disk +struct FileSource { + mgr: core::cell::RefCell>, + volume: embedded_sdmmc::Volume, + file: core::cell::RefCell, + buffer: core::cell::RefCell<[u8; Self::BUFFER_LEN]>, + offset_cached: core::cell::Cell>, +} + +impl FileSource { + const BUFFER_LEN: usize = 128; + + fn new( + mgr: embedded_sdmmc::VolumeManager, + volume: embedded_sdmmc::Volume, + file: File, + ) -> FileSource { + FileSource { + mgr: core::cell::RefCell::new(mgr), + file: core::cell::RefCell::new(file), + volume, + buffer: core::cell::RefCell::new([0u8; 128]), + offset_cached: core::cell::Cell::new(None), + } + } + + fn uncached_read( + &self, + offset: u32, + out_buffer: &mut [u8], + ) -> Result<(), embedded_sdmmc::Error> { + println!("Reading from {}", offset); + self.file.borrow_mut().seek_from_start(offset).unwrap(); + self.mgr + .borrow_mut() + .read(&self.volume, &mut self.file.borrow_mut(), out_buffer)?; + Ok(()) + } +} + +impl neotron_loader::traits::Source for &FileSource { + type Error = embedded_sdmmc::Error; + + fn read(&self, mut offset: u32, out_buffer: &mut [u8]) -> Result<(), Self::Error> { + for chunk in out_buffer.chunks_mut(FileSource::BUFFER_LEN) { + if let Some(offset_cached) = self.offset_cached.get() { + let cached_range = offset_cached..offset_cached + FileSource::BUFFER_LEN as u32; + if cached_range.contains(&offset) + && cached_range.contains(&(offset + chunk.len() as u32 - 1)) + { + // Do a fast copy from the cache + let start = (offset - offset_cached) as usize; + let end = start + chunk.len(); + chunk.copy_from_slice(&self.buffer.borrow()[start..end]); + return Ok(()); + } + } + + println!("Reading from {}", offset); + self.file.borrow_mut().seek_from_start(offset).unwrap(); + self.mgr.borrow_mut().read( + &self.volume, + &mut self.file.borrow_mut(), + self.buffer.borrow_mut().as_mut_slice(), + )?; + self.offset_cached.set(Some(offset)); + chunk.copy_from_slice(&self.buffer.borrow()[0..chunk.len()]); + + offset += chunk.len() as u32; + } + + Ok(()) + } +} + +/// Represents the Transient Program Area. +/// +/// This is a piece of memory that can be used for loading and executing programs. +/// +/// Only one program can be executed at a time. +pub struct TransientProgramArea { + memory_bottom: *mut u32, + memory_top: *mut u32, + last_entry: u32, +} + +extern "C" { + #[cfg(all(target_os = "none", target_arch = "arm"))] + static mut _tpa_start: u32; +} + +impl TransientProgramArea { + /// Construct a new [`TransientProgramArea`]. + pub unsafe fn new(start: *mut u32, length_in_bytes: usize) -> TransientProgramArea { + let mut tpa = TransientProgramArea { + memory_bottom: start, + memory_top: start.add(length_in_bytes / core::mem::size_of::()), + last_entry: 0, + }; + + // You have to take the address of a linker symbol to find out where + // points to, as the linker can only invent symbols pointing at + // addresses; it cannot actually put values in RAM. + #[cfg(all(target_os = "none", target_arch = "arm"))] + let official_tpa_start: Option<*mut u32> = Some((&mut _tpa_start) as *mut u32); + + #[cfg(not(all(target_os = "none", target_arch = "arm")))] + let official_tpa_start: Option<*mut u32> = None; + + if let Some(tpa_start) = official_tpa_start { + let range = tpa.as_slice_u32().as_ptr_range(); + if !range.contains(&(tpa_start as *const u32)) { + panic!("TPA doesn't contain system start address"); + } + let offset = tpa_start.offset_from(tpa.memory_bottom); + tpa.memory_bottom = tpa.memory_bottom.offset(offset); + } + + tpa + } + + /// Borrow the TPA region as a slice of words + pub fn as_slice_u32(&mut self) -> &mut [u32] { + unsafe { + core::slice::from_raw_parts_mut( + self.memory_bottom, + self.memory_top.offset_from(self.memory_bottom) as usize, + ) + } + } + + /// Borrow the TPA region as a slice of bytes + pub fn as_slice_u8(&mut self) -> &mut [u8] { + unsafe { + core::slice::from_raw_parts_mut( + self.memory_bottom as *mut u8, + (self.memory_top.offset_from(self.memory_bottom) as usize) + * core::mem::size_of::(), + ) + } + } + + /// Loads a program from disk into the Transient Program Area. + /// + /// The program must be in the Neotron Executable format. + pub fn load_program(&mut self, file_name: &str) -> Result<(), Error> { + println!("Loading /{} from Block Device 0", file_name); + let bios_block = crate::fs::BiosBlock(); + let time = crate::fs::BiosTime(); + let mut mgr = embedded_sdmmc::VolumeManager::new(bios_block, time); + // Open the first partition + let mut volume = mgr.get_volume(embedded_sdmmc::VolumeIdx(0))?; + let root_dir = mgr.open_root_dir(&volume)?; + let file = mgr.open_file_in_dir( + &mut volume, + &root_dir, + file_name, + embedded_sdmmc::Mode::ReadOnly, + )?; + + let source = FileSource::new(mgr, volume, file); + let loader = neotron_loader::Loader::new(&source)?; + + let mut iter = loader.iter_program_headers(); + while let Some(Ok(ph)) = iter.next() { + if ph.p_vaddr() as *mut u32 >= self.memory_bottom + && ph.p_type() == neotron_loader::ProgramHeader::PT_LOAD + { + println!("Loading {} bytes to 0x{:08x}", ph.p_memsz(), ph.p_vaddr()); + let ram = unsafe { + core::slice::from_raw_parts_mut(ph.p_vaddr() as *mut u8, ph.p_memsz() as usize) + }; + // Zero all of it. + for b in ram.iter_mut() { + *b = 0; + } + // Replace some of those zeros with bytes from disk. + if ph.p_filesz() != 0 { + source.uncached_read(ph.p_offset(), &mut ram[0..ph.p_filesz() as usize])?; + } + } + } + + self.last_entry = loader.e_entry(); + + Ok(()) + } + + /// Copy a program from memory into the Transient Program Area. + /// + /// The program must be in the Neotron Executable format. + pub fn copy_program(&mut self, program: &[u8]) -> Result<(), Error> { + let application_ram = self.as_slice_u8(); + if program.len() > application_ram.len() { + return Err(Error::ProgramTooLarge); + } + let application_ram = &mut application_ram[0..program.len()]; + application_ram.copy_from_slice(program); + Ok(()) + } + + /// Execute a program. + /// + /// If the program returns, you get `Ok()`. The program returning + /// an exit code that is non-zero is not considered a failure from the point + /// of view of this API. You wanted to run a program, and the program was + /// run. + pub fn execute(&mut self) -> Result { + if self.last_entry == 0 { + return Err(Error::NothingLoaded); + } + + let result = unsafe { + let code: extern "C" fn(*const neotron_api::Api) -> i32 = + ::core::mem::transmute(self.last_entry as *const ()); + code(&CALLBACK_TABLE) + }; + + self.last_entry = 0; + Ok(result) + } +} + +/// Application API to print things to the console. +#[allow(unused)] +extern "C" fn print_fn(data: *const u8, len: usize) { + let slice = unsafe { core::slice::from_raw_parts(data, len) }; + if let Ok(s) = core::str::from_utf8(slice) { + print!("{}", s); + } else { + // Ignore App output - not UTF-8 + } +} + +/// Open a file, given a path as UTF-8 string. +/// +/// If the file does not exist, or is already open, it returns an error. +/// +/// Path may be relative to current directory, or it may be an absolute +/// path. +extern "C" fn api_open( + _path: neotron_api::FfiString, + _flags: neotron_api::file::Flags, +) -> neotron_api::Result { + neotron_api::Result::Err(neotron_api::Error::Unimplemented) +} + +/// Close a previously opened file. +extern "C" fn api_close(_fd: neotron_api::file::Handle) -> neotron_api::Result<()> { + neotron_api::Result::Err(neotron_api::Error::Unimplemented) +} + +/// Write to an open file handle, blocking until everything is written. +/// +/// Some files do not support writing and will produce an error. +extern "C" fn api_write( + fd: neotron_api::file::Handle, + buffer: neotron_api::FfiByteSlice, +) -> neotron_api::Result<()> { + if fd == neotron_api::file::Handle::new_stdout() { + if let Some(ref mut console) = unsafe { &mut crate::VGA_CONSOLE } { + console.write_bstr(buffer.as_slice()); + } + if let Some(ref mut console) = unsafe { &mut crate::SERIAL_CONSOLE } { + if let Err(_e) = console.write_bstr(buffer.as_slice()) { + return neotron_api::Result::Err(neotron_api::Error::DeviceSpecific); + } + } + neotron_api::Result::Ok(()) + } else { + neotron_api::Result::Err(neotron_api::Error::BadHandle) + } +} + +/// Read from an open file, returning how much was actually read. +/// +/// If you hit the end of the file, you might get less data than you asked for. +extern "C" fn api_read( + _fd: neotron_api::file::Handle, + _buffer: neotron_api::FfiBuffer, +) -> neotron_api::Result { + neotron_api::Result::Err(neotron_api::Error::Unimplemented) +} + +/// Move the file offset (for the given file handle) to the given position. +/// +/// Some files do not support seeking and will produce an error. +extern "C" fn api_seek_set( + _fd: neotron_api::file::Handle, + _position: u64, +) -> neotron_api::Result<()> { + neotron_api::Result::Err(neotron_api::Error::Unimplemented) +} + +/// Move the file offset (for the given file handle) relative to the current position +/// +/// Some files do not support seeking and will produce an error. +extern "C" fn api_seek_cur( + _fd: neotron_api::file::Handle, + _offset: i64, +) -> neotron_api::Result { + neotron_api::Result::Err(neotron_api::Error::Unimplemented) +} + +/// Move the file offset (for the given file handle) to the end of the file +/// +/// Some files do not support seeking and will produce an error. +extern "C" fn api_seek_end(_fd: neotron_api::file::Handle) -> neotron_api::Result { + neotron_api::Result::Err(neotron_api::Error::Unimplemented) +} + +/// Rename a file +extern "C" fn api_rename( + _old_path: neotron_api::FfiString, + _new_path: neotron_api::FfiString, +) -> neotron_api::Result<()> { + neotron_api::Result::Err(neotron_api::Error::Unimplemented) +} + +/// Perform a special I/O control operation. +extern "C" fn api_ioctl( + _fd: neotron_api::file::Handle, + _command: u64, + _value: u64, +) -> neotron_api::Result { + neotron_api::Result::Err(neotron_api::Error::Unimplemented) +} + +/// Open a directory, given a path as a UTF-8 string. +extern "C" fn api_opendir( + _path: neotron_api::FfiString, +) -> neotron_api::Result { + neotron_api::Result::Err(neotron_api::Error::Unimplemented) +} + +/// Close a previously opened directory. +extern "C" fn api_closedir(_dir: neotron_api::dir::Handle) -> neotron_api::Result<()> { + neotron_api::Result::Err(neotron_api::Error::Unimplemented) +} + +/// Read from an open directory +extern "C" fn api_readdir( + _dir: neotron_api::dir::Handle, +) -> neotron_api::Result { + neotron_api::Result::Err(neotron_api::Error::Unimplemented) +} + +/// Get information about a file +extern "C" fn api_stat( + _path: neotron_api::FfiString, +) -> neotron_api::Result { + neotron_api::Result::Err(neotron_api::Error::Unimplemented) +} + +/// Get information about an open file +extern "C" fn api_fstat( + _fd: neotron_api::file::Handle, +) -> neotron_api::Result { + neotron_api::Result::Err(neotron_api::Error::Unimplemented) +} + +/// Delete a file. +/// +/// If the file is currently open this will give an error. +extern "C" fn api_deletefile(_path: neotron_api::FfiString) -> neotron_api::Result<()> { + neotron_api::Result::Err(neotron_api::Error::Unimplemented) +} + +/// Delete a directory +/// +/// If the directory has anything in it, this will give an error. +extern "C" fn api_deletedir(_path: neotron_api::FfiString) -> neotron_api::Result<()> { + neotron_api::Result::Err(neotron_api::Error::Unimplemented) +} + +/// Change the current directory +/// +/// Relative file paths are taken to be relative to the current directory. +/// +/// Unlike on MS-DOS, there is only one current directory for the whole +/// system, not one per drive. +extern "C" fn api_chdir(_path: neotron_api::FfiString) -> neotron_api::Result<()> { + neotron_api::Result::Err(neotron_api::Error::Unimplemented) +} + +/// Change the current directory to the open directory +/// +/// Relative file paths are taken to be relative to the current directory. +/// +/// Unlike on MS-DOS, there is only one current directory for the whole +/// system, not one per drive. +extern "C" fn api_dchdir(_dir: neotron_api::dir::Handle) -> neotron_api::Result<()> { + neotron_api::Result::Err(neotron_api::Error::Unimplemented) +} + +/// Obtain the current working directory. +extern "C" fn api_pwd(_path: neotron_api::FfiBuffer) -> neotron_api::Result { + neotron_api::Result::Err(neotron_api::Error::Unimplemented) +} + +/// Allocate some memory +extern "C" fn api_malloc( + _size: usize, + _alignment: usize, +) -> neotron_api::Result<*mut core::ffi::c_void> { + neotron_api::Result::Err(neotron_api::Error::Unimplemented) +} + +/// Free some previously allocated memory +extern "C" fn api_free(_ptr: *mut core::ffi::c_void, _size: usize, _alignment: usize) {} + +// =========================================================================== +// End of file +// ===========================================================================