From b882e2f5b1632a75e3f57f2213bb987f1da74756 Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Fri, 24 Feb 2023 16:27:11 +0000 Subject: [PATCH 01/37] Working on first draft. --- .gitignore | 6 ++ Cargo.toml | 10 +++ LICENSE-APACHE | 176 +++++++++++++++++++++++++++++++++++++++++ LICENSE => LICENSE-MIT | 2 +- README.md | 23 ++++++ src/lib.rs | 151 +++++++++++++++++++++++++++++++++++ 6 files changed, 367 insertions(+), 1 deletion(-) create mode 100644 Cargo.toml create mode 100644 LICENSE-APACHE rename LICENSE => LICENSE-MIT (93%) create mode 100644 README.md create mode 100644 src/lib.rs diff --git a/.gitignore b/.gitignore index 088ba6b..b7e28fc 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,9 @@ Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk + + +# Added by cargo + +/target +/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ccc9ddf --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "neotron-sdk" +version = "0.1.0" +edition = "2021" +description = "SDK for writing applications for Neotron OS" +license = "MIT OR Apache-2.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..1b5ec8b --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/LICENSE b/LICENSE-MIT similarity index 93% rename from LICENSE rename to LICENSE-MIT index 706fadd..4de72dc 100644 --- a/LICENSE +++ b/LICENSE-MIT @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 Neotron - the Rust based home computer platform +Copyright (c) The Neotron Developers, 2022 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md new file mode 100644 index 0000000..0a7b80e --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# Neotron SDK + +The Neotron SDK defines the API that applications receive when they run on the +[Neotron OS](https://github.com/neotron-compute/neotron-os). + +## Changelog + +### Unreleased Changes + +* First Version + +## Licence + +Copyright (c) The Neotron Developers, 2022 + +Licensed under either [MIT](./LICENSE-MIT) or [Apache-2.0](./LICENSE-APACHE) at +your option. + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you shall be licensed as above, without any +additional terms or conditions. diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f9da82b --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,151 @@ +//! The Neotron SDK +//! +//! Defines the API supplied to applications that run on Neotron OS + +#![no_std] + +// ============================================================================ +// Imports +// ============================================================================ + +// None + +// ============================================================================ +// Constants +// ============================================================================ + +/// Maximum length of a filename (with no directory components), including the +/// extension. +const MAX_FILENAME_LEN: usize = 11; + +// ============================================================================ +// Types +// ============================================================================ + +/// The type of the function which starts up the Operating System. The BIOS +/// finds and calls this function. +pub type OsStartFn = extern "C" fn(&crate::Api) -> !; + +#[repr(C)] +pub struct Api { + /// Open a file, given a path as UTF-8 string. + /// + /// Path may be relative to current directory, or it may be an absolute + /// path. + open: fn(path: *const u8, path_len: usize, flags: i32) -> Result, + /// Close a previously opened file. + close: fn(fd: FileHandle) -> Result<()>, + /// Write to an open file, returning how much was actually written. + write: fn(fd: FileHandle, buffer: *const u8, buffer_len: usize) -> Result, + /// Read from an open file, returning how much was actually read. + read: fn(fd: FileHandle, buffer: *mut u8, buffer_len: usize) -> Result, + /// Move the file offset (for the given file handle) to the given position + seek_set: fn(fd: FileHandle, position: u64) -> Result<()>, + /// Move the file offset (for the given file handle) relative to the current position + seek_cur: fn(fd: FileHandle, offset: i64) -> Result<()>, + /// Move the file offset (for the given file handle) to the end of the file + seek_end: fn(fd: FileHandle) -> Result<()>, + /// Rename a file + rename: fn( + old_path: *const u8, + old_path_len: usize, + new_path: *const u8, + new_path_len: usize, + ) -> Result<()>, + /// Perform a special I/O control operation. + ioctl: fn(fd: FileHandle, command: u64, value: u64) -> Result, + /// Open a directory, given a path as a UTF-8 string. + opendir: fn(path: *const u8, path_len: usize) -> Result, + /// Close a previously opened directory. + closedir: fn(dir: DirHandle) -> Result<()>, + /// Read from an open directory + readdir: fn(dir: DirHandle, dir_entry: *mut DirEntry) -> Result<()>, + /// Get information about a file + stat: fn(path: *const u8, path_len: usize, stat: *mut Stat) -> Result<()>, + /// Get information about an open file + fstat: fn(fd: FileHandle, stat: *mut Stat) -> Result<()>, + /// Change the current directory + chdir: fn(path: *const u8, path_len: usize) -> Result<()>, + /// Change the current directory to the open directory + dchdir: fn(dir: DirHandle) -> Result<()>, + /// Allocate some memory + malloc: fn(size: usize, alignment: u8) -> Result<*mut ()>, + /// Free some previously allocated memory + free: fn(ptr: *mut (), size: usize, alignment: u8), +} + +/// Represents a time on a clock with millisecond accuracy. +#[repr(C)] +pub struct SystemTime(u64); + +/// Represents an open file +#[repr(C)] +pub struct FileHandle(u8); + +/// Represents an open directory +#[repr(C)] +pub struct DirHandle(u8); + +/// Describes how something has failed +#[repr(C)] +pub struct Error(u32); + +/// Describes an entry in a directory. +/// +/// This is set up for 8.3 filenames on MS-DOS FAT32 partitions currently. +#[repr(C)] +pub struct DirEntry { + /// The name and extension of the file + pub name: [u8; MAX_FILENAME_LEN], + /// File attributes (Directory, Volume, etc) + pub attr: u8, + /// How big is this file + pub file_size: u64, + /// When was the file created + pub ctime: SystemTime, + /// When was the last modified + pub mtime: SystemTime, +} + +/// Describes a file on disk. +/// +/// This is set up for 8.3 filenames on MS-DOS FAT32 partitions currently. +#[repr(C)] +pub struct Stat { + /// Which volume is this file on + pub volume: u8, + /// File attributes (Directory, Volume, etc) + pub attr: u8, + /// How big is this file + pub file_size: u64, + /// When was the file created + pub ctime: SystemTime, + /// When was the last modified + pub mtime: SystemTime, +} + +/// All API functions which can fail return this type. We don't use the +/// `Result` type from the standard library because that isn't FFI safe and +/// may change layout between compiler versions. +#[repr(C)] +pub enum Result { + /// The operation succeeded (the same as `core::result::Result::Ok`). + Ok(T), + /// The operation failed (the same as `core::result::Result::Err`). + Err(Error), +} + +/// All API functions which take/return optional values return this type. We +/// don't use the `Option` type from the standard library because that isn't +/// FFI safe and may change layout between compiler versions. +#[repr(C)] +pub enum Option { + /// There is some data (the same as `core::option::Option::Some`) + Some(T), + /// There is no data (the same as `core::option::Option::None`) + None, +} + +// ============================================================================ +// Impls +// ============================================================================ From e791ac490ba81cee91c841c393a07c3c33c60000 Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Fri, 26 May 2023 11:35:05 +0100 Subject: [PATCH 02/37] Use neotron-ffi crate. --- Cargo.toml | 14 +++- src/lib.rs | 231 ++++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 178 insertions(+), 67 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ccc9ddf..5b38a16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,18 @@ [package] -name = "neotron-sdk" -version = "0.1.0" -edition = "2021" description = "SDK for writing applications for Neotron OS" +edition = "2021" license = "MIT OR Apache-2.0" +name = "neotron-sdk" +version = "0.1.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +bitflags = "2" +neotron-ffi = "0.1" + +[features] +# Enables functions the OS needs but an application does not +os = [] +# Enables functions an application needs but the OS does not +application = [] diff --git a/src/lib.rs b/src/lib.rs index f9da82b..ea89230 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,9 @@ // Imports // ============================================================================ -// None +use bitflags::bitflags; + +use neotron_ffi::{FfiBuffer, FfiByteSlice, FfiString}; // ============================================================================ // Constants @@ -16,7 +18,12 @@ /// Maximum length of a filename (with no directory components), including the /// extension. -const MAX_FILENAME_LEN: usize = 11; +pub const MAX_FILENAME_LEN: usize = 11; + +/// Maximum length of a path to a file. +/// +/// This is shorter than on MS-DOS, to save on memory. +pub const MAX_PATH_LEN: usize = 64; // ============================================================================ // Types @@ -26,126 +33,222 @@ const MAX_FILENAME_LEN: usize = 11; /// finds and calls this function. pub type OsStartFn = extern "C" fn(&crate::Api) -> !; +/// The result type for any SDK API function. +/// +/// Like a [`neotron_ffi::FfiResult`] but the error type is [`Error`]. +pub type Result = neotron_ffi::FfiResult; + +/// The syscalls provided by the Neotron OS to a Neotron Application. #[repr(C)] pub struct Api { /// 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. - open: fn(path: *const u8, path_len: usize, flags: i32) -> Result, + pub open: fn(path: FfiString, flags: FileFlags) -> Result, /// Close a previously opened file. - close: fn(fd: FileHandle) -> Result<()>, - /// Write to an open file, returning how much was actually written. - write: fn(fd: FileHandle, buffer: *const u8, buffer_len: usize) -> Result, + pub close: fn(fd: FileHandle) -> Result<()>, + /// Write to an open file handle, blocking until everything is written. + /// + /// Some files do not support writing and will produce an error. + pub write: fn(fd: FileHandle, buffer: FfiByteSlice) -> Result<()>, /// Read from an open file, returning how much was actually read. - read: fn(fd: FileHandle, buffer: *mut u8, buffer_len: usize) -> Result, - /// Move the file offset (for the given file handle) to the given position - seek_set: fn(fd: FileHandle, position: u64) -> Result<()>, + /// + /// If you hit the end of the file, you might get less data than you asked for. + pub read: fn(fd: FileHandle, buffer: FfiBuffer) -> Result, + /// Move the file offset (for the given file handle) to the given position. + /// + /// Some files do not support seeking and will produce an error. + pub seek_set: fn(fd: FileHandle, position: u64) -> Result<()>, /// Move the file offset (for the given file handle) relative to the current position - seek_cur: fn(fd: FileHandle, offset: i64) -> Result<()>, + /// + /// Some files do not support seeking and will produce an error. + pub seek_cur: fn(fd: FileHandle, offset: i64) -> Result<()>, /// Move the file offset (for the given file handle) to the end of the file - seek_end: fn(fd: FileHandle) -> Result<()>, + /// + /// Some files do not support seeking and will produce an error. + pub seek_end: fn(fd: FileHandle) -> Result<()>, /// Rename a file - rename: fn( - old_path: *const u8, - old_path_len: usize, - new_path: *const u8, - new_path_len: usize, - ) -> Result<()>, + pub rename: fn(old_path: FfiString, new_path: FfiString) -> Result<()>, /// Perform a special I/O control operation. - ioctl: fn(fd: FileHandle, command: u64, value: u64) -> Result, + pub ioctl: fn(fd: FileHandle, command: u64, value: u64) -> Result, /// Open a directory, given a path as a UTF-8 string. - opendir: fn(path: *const u8, path_len: usize) -> Result, + pub opendir: fn(path: FfiString) -> Result, /// Close a previously opened directory. - closedir: fn(dir: DirHandle) -> Result<()>, + pub closedir: fn(dir: DirHandle) -> Result<()>, /// Read from an open directory - readdir: fn(dir: DirHandle, dir_entry: *mut DirEntry) -> Result<()>, + pub readdir: fn(dir: DirHandle) -> Result, /// Get information about a file - stat: fn(path: *const u8, path_len: usize, stat: *mut Stat) -> Result<()>, + pub stat: fn(path: FfiString) -> Result, /// Get information about an open file - fstat: fn(fd: FileHandle, stat: *mut Stat) -> Result<()>, + pub fstat: fn(fd: FileHandle) -> Result, + /// Delete a file or directory + /// + /// If the file is currently open, or the directory has anything in it, this + /// will give an error. + pub delete: fn(path: FfiString) -> Result<()>, /// Change the current directory - chdir: fn(path: *const u8, path_len: usize) -> Result<()>, + /// + /// 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. + pub chdir: fn(path: FfiString) -> Result<()>, /// Change the current directory to the open directory - dchdir: fn(dir: DirHandle) -> Result<()>, + /// + /// 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. + pub dchdir: fn(dir: DirHandle) -> Result<()>, /// Allocate some memory - malloc: fn(size: usize, alignment: u8) -> Result<*mut ()>, + pub malloc: fn(size: usize, alignment: usize) -> Result>, /// Free some previously allocated memory - free: fn(ptr: *mut (), size: usize, alignment: u8), + pub free: fn(ptr: core::ptr::NonNull<[u8]>, size: usize, alignment: usize), } -/// Represents a time on a clock with millisecond accuracy. +/// Describes how something has failed #[repr(C)] -pub struct SystemTime(u64); +pub enum Error { + FileNotFound, + FileReadOnly, + EndOfFile, +} /// Represents an open file #[repr(C)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct FileHandle(u8); +impl FileHandle { + /// Construct a new `FileHandle` from an integer. + /// + /// Only the OS should call this. + /// + /// # Safety + /// + /// The integer given must be a valid index for an open File. + #[cfg(feature = "os")] + pub const fn new(value: u8) -> FileHandle { + FileHandle(value) + } + + /// Get the numeric value of this File Handle + pub const fn value(&self) -> u8 { + self.0 + } +} + /// Represents an open directory #[repr(C)] +#[derive(Debug, PartialEq, Eq)] pub struct DirHandle(u8); -/// Describes how something has failed -#[repr(C)] -pub struct Error(u32); +impl DirHandle { + /// Construct a new `DirHandle` from an integer. + /// + /// Only the OS should call this. + /// + /// # Safety + /// + /// The integer given must be a valid index for an open Directory. + #[cfg(feature = "os")] + pub const fn new(value: u8) -> DirHandle { + DirHandle(value) + } + + /// Get the numeric value of this Directory Handle + pub const fn value(&self) -> u8 { + self.0 + } +} /// Describes an entry in a directory. /// /// This is set up for 8.3 filenames on MS-DOS FAT32 partitions currently. #[repr(C)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct DirEntry { /// The name and extension of the file pub name: [u8; MAX_FILENAME_LEN], - /// File attributes (Directory, Volume, etc) - pub attr: u8, - /// How big is this file - pub file_size: u64, - /// When was the file created - pub ctime: SystemTime, - /// When was the last modified - pub mtime: SystemTime, + /// File properties + pub properties: Stat, } /// Describes a file on disk. /// /// This is set up for 8.3 filenames on MS-DOS FAT32 partitions currently. #[repr(C)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Stat { - /// Which volume is this file on - pub volume: u8, - /// File attributes (Directory, Volume, etc) - pub attr: u8, /// How big is this file pub file_size: u64, /// When was the file created - pub ctime: SystemTime, + pub ctime: FileTime, /// When was the last modified - pub mtime: SystemTime, + pub mtime: FileTime, + /// File attributes (Directory, Volume, etc) + pub attr: FileAttributes, } -/// All API functions which can fail return this type. We don't use the -/// `Result` type from the standard library because that isn't FFI safe and -/// may change layout between compiler versions. -#[repr(C)] -pub enum Result { - /// The operation succeeded (the same as `core::result::Result::Ok`). - Ok(T), - /// The operation failed (the same as `core::result::Result::Err`). - Err(Error), +bitflags! { + #[repr(C)] + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + /// Describes the attributes of a file. + pub struct FileFlags: u8 { + /// Enable write support for this file. + const WRITE = 0x01; + /// Create the file if it doesn't exist. + const CREATE = 0x02; + /// Truncate the file to zero length upon opening. + const TRUNCATE = 0x04; + } +} + +bitflags! { + #[repr(C)] + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + /// The attributes a file on disk can have.alloc + /// + /// Based on that supported by the FAT32 file system. + pub struct FileAttributes: u8 { + /// File is read-only + const READ_ONLY = 0x01; + /// File should not appear in directory listing + const HIDDEN = 0x02; + /// File should not be moved on disk + const SYSTEM = 0x04; + /// File is a volume label + const VOLUME = 0x08; + /// File is a directory + const DIRECTORY = 0x10; + /// File has not been backed up + const ARCHIVE = 0x20; + /// File is actually a device + const DEVICE = 0x40; + } } -/// All API functions which take/return optional values return this type. We -/// don't use the `Option` type from the standard library because that isn't -/// FFI safe and may change layout between compiler versions. +/// Represents an instant in time, in the local time zone. #[repr(C)] -pub enum Option { - /// There is some data (the same as `core::option::Option::Some`) - Some(T), - /// There is no data (the same as `core::option::Option::None`) - None, +#[derive(Debug, Copy, Clone, PartialOrd, Ord, PartialEq, Eq)] +pub struct FileTime { + /// Add 1970 to this file to get the calendar year + pub year_since_1970: u8, + /// Add one to this value to get the calendar month + pub zero_indexed_month: u8, + /// Add one to this value to get the calendar day + pub zero_indexed_day: u8, + /// The number of hours past midnight + pub hours: u8, + /// The number of minutes past the hour + pub minutes: u8, + /// The number of seconds past the minute + pub seconds: u8, } // ============================================================================ -// Impls +// End of File // ============================================================================ From 1915bf9e427f714d80196ad5b5713f668018e486 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Mon, 29 May 2023 22:26:49 +0100 Subject: [PATCH 03/37] Add some helper code for starting up applications. --- Cargo.toml | 3 +- samples/README.md | 32 +++++++++ samples/hello/.cargo/config.toml | 11 +++ samples/hello/.gitignore | 1 + samples/hello/Cargo.toml | 16 +++++ samples/hello/README.md | 18 +++++ samples/hello/neotron-cortex-m.ld | 107 ++++++++++++++++++++++++++++ samples/hello/src/main.rs | 19 +++++ src/lib.rs | 111 +++++++++++++++++++++++------- 9 files changed, 293 insertions(+), 25 deletions(-) create mode 100644 samples/README.md create mode 100644 samples/hello/.cargo/config.toml create mode 100644 samples/hello/.gitignore create mode 100644 samples/hello/Cargo.toml create mode 100644 samples/hello/README.md create mode 100644 samples/hello/neotron-cortex-m.ld create mode 100644 samples/hello/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index 5b38a16..7184202 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,8 +4,7 @@ edition = "2021" license = "MIT OR Apache-2.0" name = "neotron-sdk" version = "0.1.0" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +authors = ["Jonathan 'theJPster' Pallant "] [dependencies] bitflags = "2" diff --git a/samples/README.md b/samples/README.md new file mode 100644 index 0000000..7f0b1e0 --- /dev/null +++ b/samples/README.md @@ -0,0 +1,32 @@ +# Neotron Sample Applications + +Here are some sample applications that use the Neotron API. + +## Building the Applications + +Build the application as follows: + +```console +$ cargo build --release --target=thumbv6m-none-eabi +$ cargo objcopy --release --target=thumbv6m-none-eabi -- -O binary hello.bin +``` + +Then copy the resulting `hello.bin` file to an SD card and insert it into your Neotron system. You can load the application with something like: + +```text +> load hello.bin +> run +``` + +If you don't have `cargo-binutils` installed (which adds the `objcopy` sub-command), install it with: + +```console +$ cargo install cargo-binutils +``` + +## List of Sample Applications + +## [`hello`](./hello) + +This is a basic "Hello World" application. It prints the string "Hello, world" to *standard output* and then exits with an exit code of 0. + diff --git a/samples/hello/.cargo/config.toml b/samples/hello/.cargo/config.toml new file mode 100644 index 0000000..b92398d --- /dev/null +++ b/samples/hello/.cargo/config.toml @@ -0,0 +1,11 @@ +[target.thumbv7em-none-eabihf] +rustflags = ["-C", "link-arg=-Tneotron-cortex-m.ld"] + +[target.thumbv7em-none-eabi] +rustflags = ["-C", "link-arg=-Tneotron-cortex-m.ld"] + +[target.thumbv7m-none-eabi] +rustflags = ["-C", "link-arg=-Tneotron-cortex-m.ld"] + +[target.thumbv6m-none-eabi] +rustflags = ["-C", "link-arg=-Tneotron-cortex-m.ld"] diff --git a/samples/hello/.gitignore b/samples/hello/.gitignore new file mode 100644 index 0000000..eb5a316 --- /dev/null +++ b/samples/hello/.gitignore @@ -0,0 +1 @@ +target diff --git a/samples/hello/Cargo.toml b/samples/hello/Cargo.toml new file mode 100644 index 0000000..d045e96 --- /dev/null +++ b/samples/hello/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "hello" +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" +authors = ["Jonathan 'theJPster' Pallant "] +description = "Hello World for Neotron systems" + +[dependencies] +neotron-sdk = { path = "../..", features = ["application"] } + +[profile.dev] +panic = "abort" + +[profile.release] +panic = "abort" diff --git a/samples/hello/README.md b/samples/hello/README.md new file mode 100644 index 0000000..1fda2ac --- /dev/null +++ b/samples/hello/README.md @@ -0,0 +1,18 @@ +# Hello, World! + +A basic *Hello World* application for Neotron systems. + +See the general sample application [README](../README.md) for compilation instructions. + +## Licence + +Copyright (c) The Neotron Developers, 2023 + +Licensed under either [MIT](../../LICENSE-MIT) or [Apache-2.0](../../LICENSE-APACHE) at +your option. + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you shall be licensed as above, without any +additional terms or conditions. diff --git a/samples/hello/neotron-cortex-m.ld b/samples/hello/neotron-cortex-m.ld new file mode 100644 index 0000000..1f5547b --- /dev/null +++ b/samples/hello/neotron-cortex-m.ld @@ -0,0 +1,107 @@ +/* + * Neotron Application linker script for Arm Cortex-M systems. + * + * Based on the [Rust Embedded Cortex-M crate](https://github.com/rust-embedded/cortex-m). + * + * Copyright (c) 2016 Jorge Aparicio. + * Copyright (c) 2023 Jonathan 'theJPster' Pallant and the Neotron Developers. + */ + +MEMORY +{ + /* + * 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 = 0x20001000, LENGTH = 256K +} + +/* # Entry point = what the BIOS calls to start the OS */ +ENTRY(app_entry); + +/* # Sections */ +SECTIONS +{ + + /* ### .entry_point */ + .entry_point ORIGIN(RAM) : + { + KEEP(*(.entry_point)) + } > RAM + + PROVIDE(_stext = ADDR(.entry_point) + SIZEOF(.entry_point)); + + /* ### .text */ + .text _stext : + { + *(.text .text.*); + *(.HardFaultTrampoline); + *(.HardFault.*); + } + + /* ### .rodata */ + .rodata : ALIGN(4) + { + *(.rodata .rodata.*); + + /* 4-byte align the end (VMA) of this section. + This is required by LLD to ensure the LMA of the following .data + section will have the correct alignment. */ + . = ALIGN(4); + } + + /* ### .data */ + .data : ALIGN(4) + { + . = ALIGN(4); + __sdata = .; + *(.data .data.*); + . = ALIGN(4); /* 4-byte align the end (VMA) of this section */ + __edata = .; + } + + /* LMA of .data */ + __sidata = LOADADDR(.data); + + /* ### .bss */ + .bss : ALIGN(4) + { + . = ALIGN(4); + __sbss = .; + *(.bss .bss.*); + . = ALIGN(4); /* 4-byte align the end (VMA) of this section */ + __ebss = .; + } + + /* ### .uninit */ + .uninit (NOLOAD) : ALIGN(4) + { + . = ALIGN(4); + *(.uninit .uninit.*); + . = ALIGN(4); + } + + /* Place the heap right after `.uninit` */ + . = ALIGN(4); + __sheap = .; + + /* ## .got */ + /* Dynamic relocations are unsupported. This section is only used to detect relocatable code in + the input files and raise an error if relocatable code is found */ + .got (NOLOAD) : + { + KEEP(*(.got .got.*)); + } + + /* ## Discarded sections */ + /DISCARD/ : + { + /* Unused exception related info that only wastes space */ + *(.ARM.exidx); + *(.ARM.exidx.*); + *(.ARM.extab.*); + } +} diff --git a/samples/hello/src/main.rs b/samples/hello/src/main.rs new file mode 100644 index 0000000..097925c --- /dev/null +++ b/samples/hello/src/main.rs @@ -0,0 +1,19 @@ +#![no_std] +#![no_main] + +extern crate neotron_sdk; + +#[no_mangle] +extern "C" fn main() -> i32 { + let stdout = neotron_sdk::FileHandle::new_stdout(); + neotron_sdk::write(stdout, b"Hello, world\n"); + 0 +} + +#[inline(never)] +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop { + // Spin + } +} diff --git a/src/lib.rs b/src/lib.rs index ea89230..1257717 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,7 @@ use bitflags::bitflags; -use neotron_ffi::{FfiBuffer, FfiByteSlice, FfiString}; +pub use neotron_ffi::{FfiBuffer, FfiByteSlice, FfiString}; // ============================================================================ // Constants @@ -25,13 +25,19 @@ pub const MAX_FILENAME_LEN: usize = 11; /// This is shorter than on MS-DOS, to save on memory. pub const MAX_PATH_LEN: usize = 64; +#[cfg(feature = "application")] +extern "C" { + fn main() -> i32; +} + // ============================================================================ // Types // ============================================================================ -/// The type of the function which starts up the Operating System. The BIOS -/// finds and calls this function. -pub type OsStartFn = extern "C" fn(&crate::Api) -> !; +/// The type of the application entry-point. +/// +/// The OS calls a function of this type. +pub type AppStartFn = extern "C" fn(*mut crate::Api) -> i32; /// The result type for any SDK API function. /// @@ -47,74 +53,85 @@ pub struct Api { /// /// Path may be relative to current directory, or it may be an absolute /// path. - pub open: fn(path: FfiString, flags: FileFlags) -> Result, + pub open: extern "C" fn(path: FfiString, flags: FileFlags) -> Result, /// Close a previously opened file. - pub close: fn(fd: FileHandle) -> Result<()>, + pub close: extern "C" fn(fd: FileHandle) -> Result<()>, /// Write to an open file handle, blocking until everything is written. /// /// Some files do not support writing and will produce an error. - pub write: fn(fd: FileHandle, buffer: FfiByteSlice) -> Result<()>, + pub write: extern "C" fn(fd: FileHandle, buffer: FfiByteSlice) -> Result<()>, /// 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. - pub read: fn(fd: FileHandle, buffer: FfiBuffer) -> Result, + pub read: extern "C" fn(fd: FileHandle, buffer: FfiBuffer) -> Result, /// Move the file offset (for the given file handle) to the given position. /// /// Some files do not support seeking and will produce an error. - pub seek_set: fn(fd: FileHandle, position: u64) -> Result<()>, + pub seek_set: extern "C" fn(fd: FileHandle, position: u64) -> Result<()>, /// 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. - pub seek_cur: fn(fd: FileHandle, offset: i64) -> Result<()>, + pub seek_cur: extern "C" fn(fd: FileHandle, offset: i64) -> Result<()>, /// 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. - pub seek_end: fn(fd: FileHandle) -> Result<()>, + pub seek_end: extern "C" fn(fd: FileHandle) -> Result<()>, /// Rename a file - pub rename: fn(old_path: FfiString, new_path: FfiString) -> Result<()>, + pub rename: extern "C" fn(old_path: FfiString, new_path: FfiString) -> Result<()>, /// Perform a special I/O control operation. - pub ioctl: fn(fd: FileHandle, command: u64, value: u64) -> Result, + pub ioctl: extern "C" fn(fd: FileHandle, command: u64, value: u64) -> Result, /// Open a directory, given a path as a UTF-8 string. - pub opendir: fn(path: FfiString) -> Result, + pub opendir: extern "C" fn(path: FfiString) -> Result, /// Close a previously opened directory. - pub closedir: fn(dir: DirHandle) -> Result<()>, + pub closedir: extern "C" fn(dir: DirHandle) -> Result<()>, /// Read from an open directory - pub readdir: fn(dir: DirHandle) -> Result, + pub readdir: extern "C" fn(dir: DirHandle) -> Result, /// Get information about a file - pub stat: fn(path: FfiString) -> Result, + pub stat: extern "C" fn(path: FfiString) -> Result, /// Get information about an open file - pub fstat: fn(fd: FileHandle) -> Result, + pub fstat: extern "C" fn(fd: FileHandle) -> Result, /// Delete a file or directory /// /// If the file is currently open, or the directory has anything in it, this /// will give an error. - pub delete: fn(path: FfiString) -> Result<()>, + pub delete: extern "C" fn(path: FfiString) -> Result<()>, /// 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. - pub chdir: fn(path: FfiString) -> Result<()>, + pub chdir: extern "C" fn(path: FfiString) -> Result<()>, /// 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. - pub dchdir: fn(dir: DirHandle) -> Result<()>, + pub dchdir: extern "C" fn(dir: DirHandle) -> Result<()>, /// Allocate some memory - pub malloc: fn(size: usize, alignment: usize) -> Result>, + pub malloc: extern "C" fn(size: usize, alignment: usize) -> Result<*mut core::ffi::c_void>, /// Free some previously allocated memory - pub free: fn(ptr: core::ptr::NonNull<[u8]>, size: usize, alignment: usize), + pub free: extern "C" fn(ptr: *mut core::ffi::c_void, size: usize, alignment: usize), } /// Describes how something has failed #[repr(C)] pub enum Error { + /// The given file path was not found FileNotFound, + /// Tried to write to a read-only file FileReadOnly, + /// Reached the end of the file EndOfFile, + /// The API has not been implemented + Unimplemented, + /// An invalid argument was given to the API + InvalidArg, + /// A bad file handle was given to the API + BadFileHandle, + /// An device-specific error occurred. Look at the BIOS source for more details. + DeviceSpecific, } /// Represents an open file @@ -123,6 +140,9 @@ pub enum Error { pub struct FileHandle(u8); impl FileHandle { + /// The magic file ID for Standard Output + const STDOUT: u8 = 0; + /// Construct a new `FileHandle` from an integer. /// /// Only the OS should call this. @@ -135,6 +155,11 @@ impl FileHandle { FileHandle(value) } + /// Create a file handle for Standard Output + pub const fn new_stdout() -> FileHandle { + FileHandle(Self::STDOUT) + } + /// Get the numeric value of this File Handle pub const fn value(&self) -> u8 { self.0 @@ -249,6 +274,46 @@ pub struct FileTime { pub seconds: u8, } +// ============================================================================ +// Functions +// ============================================================================ + +#[cfg(feature = "application")] +mod application { + use super::*; + use core::sync::atomic::{AtomicPtr, Ordering}; + + #[link_section = ".entry_point"] + #[used] + pub static APP_ENTRY: AppStartFn = app_entry; + + static API: AtomicPtr = AtomicPtr::new(core::ptr::null_mut()); + + /// The function the OS calls to start the application. + /// + /// Will jump to the application entry point, and `extern "C"` function + /// called `main`. + extern "C" fn app_entry(api: *mut crate::Api) -> i32 { + API.store(api as *mut Api, Ordering::Relaxed); + unsafe { main() } + } + + /// Write to a file handle. + pub fn write(fd: FileHandle, data: &[u8]) -> Result<()> { + let api = get_api(); + (api.write)(fd, FfiByteSlice::new(data)) + } + + /// Get the API structure so you can call APIs manually. + fn get_api() -> &'static Api { + let ptr = API.load(Ordering::Relaxed); + unsafe { ptr.as_ref().unwrap() } + } +} + +#[cfg(feature = "application")] +pub use application::*; + // ============================================================================ // End of File // ============================================================================ From bdb76d62b81648f4fd80268cb4474e57ff4dd036 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Mon, 29 May 2023 22:32:45 +0100 Subject: [PATCH 04/37] Set up Github Actions. --- .github/FUNDING.yml | 13 ++++++++++++ .github/workflows/build.yml | 36 ++++++++++++++++++++++++++++++++++ .github/workflows/clippy.yml | 29 +++++++++++++++++++++++++++ .github/workflows/format.yml | 25 +++++++++++++++++++++++ samples/{hello => }/.gitignore | 0 samples/Cargo.toml | 8 ++++++++ samples/hello/Cargo.toml | 13 ++++++------ 7 files changed, 118 insertions(+), 6 deletions(-) create mode 100644 .github/FUNDING.yml create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/clippy.yml create mode 100644 .github/workflows/format.yml rename samples/{hello => }/.gitignore (100%) create mode 100644 samples/Cargo.toml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..dd26823 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,13 @@ +# These are supported funding model platforms + +github: Neotron-Compute +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..01f74aa --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,36 @@ +on: [push, pull_request] +name: Build (and Release) + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + name: Build (and Release) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: true + fetch-depth: 0 + + - name: Add targets + run: | + rustup target add thumbv6m-none-eabi + + - name: Build lib (native) + run: | + cargo build --verbose + + - name: Test lib (native) + run: | + cargo test --verbose + + - name: Build lib (Cortex-M0), OS mode + run: | + cargo build --verbose --target=thumbv6m-none-eabi --features=os + + - name: Build samples (Cortex-M0) + run: | + cd samples && cargo build --verbose --target=thumbv6m-none-eabi diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml new file mode 100644 index 0000000..46b9900 --- /dev/null +++ b/.github/workflows/clippy.yml @@ -0,0 +1,29 @@ +name: Clippy + +on: [push, pull_request] + +env: + CARGO_TERM_COLOR: always + +jobs: + clippy-check: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v3 + with: + submodules: true + fetch-depth: 0 + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + components: clippy + target: thumbv6m-none-eabi + + - name: Run Clippy + uses: actions-rs/clippy-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: --all-features --target=thumbv6m-none-eabi diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml new file mode 100644 index 0000000..1fc3748 --- /dev/null +++ b/.github/workflows/format.yml @@ -0,0 +1,25 @@ +name: Format + +on: [push, pull_request] + +jobs: + format-check: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v3 + with: + submodules: true + fetch-depth: 0 + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + components: rustfmt + + - name: Check Format + run: cargo fmt -- --check + + - name: Check Format + run: cd samples && cargo fmt -- --check diff --git a/samples/hello/.gitignore b/samples/.gitignore similarity index 100% rename from samples/hello/.gitignore rename to samples/.gitignore diff --git a/samples/Cargo.toml b/samples/Cargo.toml new file mode 100644 index 0000000..52b2ccd --- /dev/null +++ b/samples/Cargo.toml @@ -0,0 +1,8 @@ +[workspace] +members = ["hello"] + +[profile.dev] +panic = "abort" + +[profile.release] +panic = "abort" diff --git a/samples/hello/Cargo.toml b/samples/hello/Cargo.toml index d045e96..58a5230 100644 --- a/samples/hello/Cargo.toml +++ b/samples/hello/Cargo.toml @@ -8,9 +8,10 @@ description = "Hello World for Neotron systems" [dependencies] neotron-sdk = { path = "../..", features = ["application"] } - -[profile.dev] -panic = "abort" - -[profile.release] -panic = "abort" +# +# NB: This is in a workspace, so these settings are in the workspace file. +#[profile.dev] +#panic = "abort" +# +#[profile.release] +#panic = "abort" From 231b7b62629693671df08abc78dd2640be783ce2 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Thu, 8 Jun 2023 22:01:15 +0100 Subject: [PATCH 05/37] Use API crate for API. Also adds a File struct with methods. --- Cargo.toml | 1 + src/lib.rs | 432 +++++++++++++++++++++++++---------------------------- 2 files changed, 205 insertions(+), 228 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7184202..3046638 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ authors = ["Jonathan 'theJPster' Pallant "] [dependencies] bitflags = "2" neotron-ffi = "0.1" +neotron-api = { git = "https://github.com/neotron-compute/neotron-api.git", branch = "seek-updates" } [features] # Enables functions the OS needs but an application does not diff --git a/src/lib.rs b/src/lib.rs index 1257717..9da71f4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,28 +8,41 @@ // Imports // ============================================================================ -use bitflags::bitflags; +use core::sync::atomic::{AtomicPtr, Ordering}; pub use neotron_ffi::{FfiBuffer, FfiByteSlice, FfiString}; +pub use neotron_api::{dir, file, Api, Error}; + // ============================================================================ // Constants // ============================================================================ -/// Maximum length of a filename (with no directory components), including the -/// extension. -pub const MAX_FILENAME_LEN: usize = 11; - /// Maximum length of a path to a file. /// /// This is shorter than on MS-DOS, to save on memory. pub const MAX_PATH_LEN: usize = 64; -#[cfg(feature = "application")] extern "C" { - fn main() -> i32; + /// This is what the user's application entry point must be called. + /// + /// They can return `0` for success, or anything else to indicate an error. + fn neotron_main() -> i32; } +// ============================================================================ +// Static Variables +// ============================================================================ + +#[link_section = ".entry_point"] +#[used] +pub static APP_ENTRY: AppStartFn = app_entry; + +/// Holds a pointer to the OS API provided by the OS on start-up. +/// +/// Once you've hit the application `main()`, this will be non-null. +static API: AtomicPtr = AtomicPtr::new(core::ptr::null_mut()); + // ============================================================================ // Types // ============================================================================ @@ -39,280 +52,243 @@ extern "C" { /// The OS calls a function of this type. pub type AppStartFn = extern "C" fn(*mut crate::Api) -> i32; -/// The result type for any SDK API function. +/// The result type for any SDK function. /// -/// Like a [`neotron_ffi::FfiResult`] but the error type is [`Error`]. -pub type Result = neotron_ffi::FfiResult; +/// Like a [`core::result::Result`] but the error type is [`Error`]. +pub type Result = core::result::Result; -/// The syscalls provided by the Neotron OS to a Neotron Application. -#[repr(C)] -pub struct Api { +/// Represents an open File +pub struct File(file::Handle); + +impl File { /// 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. - pub open: extern "C" fn(path: FfiString, flags: FileFlags) -> Result, - /// Close a previously opened file. - pub close: extern "C" fn(fd: FileHandle) -> Result<()>, - /// Write to an open file handle, blocking until everything is written. /// - /// Some files do not support writing and will produce an error. - pub write: extern "C" fn(fd: FileHandle, buffer: FfiByteSlice) -> Result<()>, - /// 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. - pub read: extern "C" fn(fd: FileHandle, buffer: FfiBuffer) -> Result, - /// Move the file offset (for the given file handle) to the given position. - /// - /// Some files do not support seeking and will produce an error. - pub seek_set: extern "C" fn(fd: FileHandle, position: u64) -> Result<()>, - /// 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. - pub seek_cur: extern "C" fn(fd: FileHandle, offset: i64) -> Result<()>, - /// 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. - pub seek_end: extern "C" fn(fd: FileHandle) -> Result<()>, - /// Rename a file - pub rename: extern "C" fn(old_path: FfiString, new_path: FfiString) -> Result<()>, - /// Perform a special I/O control operation. - pub ioctl: extern "C" fn(fd: FileHandle, command: u64, value: u64) -> Result, - /// Open a directory, given a path as a UTF-8 string. - pub opendir: extern "C" fn(path: FfiString) -> Result, - /// Close a previously opened directory. - pub closedir: extern "C" fn(dir: DirHandle) -> Result<()>, - /// Read from an open directory - pub readdir: extern "C" fn(dir: DirHandle) -> Result, - /// Get information about a file - pub stat: extern "C" fn(path: FfiString) -> Result, - /// Get information about an open file - pub fstat: extern "C" fn(fd: FileHandle) -> Result, - /// Delete a file or directory + /// # Limitations /// - /// If the file is currently open, or the directory has anything in it, this - /// will give an error. - pub delete: extern "C" fn(path: FfiString) -> Result<()>, - /// Change the current directory + /// * You cannot open a file if it is currently open. + /// * Paths must confirm to the rules for the filesystem for the given drive. + /// * Relative paths are taken relative to the current directory (see `Api::chdir`). + pub fn open(path: file::Path, flags: file::Flags) -> Result { + let api = get_api(); + match (api.open)(path, flags) { + neotron_ffi::FfiResult::Ok(handle) => Ok(File(handle)), + neotron_ffi::FfiResult::Err(e) => Err(e), + } + } + + /// Write to an open file handle, blocking until everything is written. /// - /// Relative file paths are taken to be relative to the current directory. + /// Some files do not support writing and will produce an error. You will + /// also get an error if you run out of disk space. /// - /// Unlike on MS-DOS, there is only one current directory for the whole - /// system, not one per drive. - pub chdir: extern "C" fn(path: FfiString) -> Result<()>, - /// Change the current directory to the open directory + /// The `buffer` is only borrowed for the duration of the function call and + /// is then forgotten. + pub fn write(&self, buffer: &[u8]) -> Result<()> { + let api = get_api(); + match (api.write)(self.0, FfiByteSlice::new(buffer)) { + neotron_ffi::FfiResult::Ok(_) => Ok(()), + neotron_ffi::FfiResult::Err(e) => Err(e), + } + } + + /// Read from an open file, returning how much was actually read. /// - /// Relative file paths are taken to be relative to the current directory. + /// You might get less data than you asked for. If you do an `Api::read` and + /// you are already at the end of the file you will get + /// `Err(Error::EndOfFile)`. /// - /// Unlike on MS-DOS, there is only one current directory for the whole - /// system, not one per drive. - pub dchdir: extern "C" fn(dir: DirHandle) -> Result<()>, - /// Allocate some memory - pub malloc: extern "C" fn(size: usize, alignment: usize) -> Result<*mut core::ffi::c_void>, - /// Free some previously allocated memory - pub free: extern "C" fn(ptr: *mut core::ffi::c_void, size: usize, alignment: usize), -} - -/// Describes how something has failed -#[repr(C)] -pub enum Error { - /// The given file path was not found - FileNotFound, - /// Tried to write to a read-only file - FileReadOnly, - /// Reached the end of the file - EndOfFile, - /// The API has not been implemented - Unimplemented, - /// An invalid argument was given to the API - InvalidArg, - /// A bad file handle was given to the API - BadFileHandle, - /// An device-specific error occurred. Look at the BIOS source for more details. - DeviceSpecific, -} - -/// Represents an open file -#[repr(C)] -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub struct FileHandle(u8); + /// Data is stored to the given `buffer. The `buffer` is only borrowed for + /// the duration of the function call and is then forgotten. + pub fn read(&self, buffer: &mut [u8]) -> Result { + let api = get_api(); + match (api.read)(self.0, FfiBuffer::new(buffer)) { + neotron_ffi::FfiResult::Ok(n) => Ok(n), + neotron_ffi::FfiResult::Err(e) => Err(e), + } + } -impl FileHandle { - /// The magic file ID for Standard Output - const STDOUT: u8 = 0; + /// Close a file + pub fn close(self) -> Result<()> { + let api = get_api(); + // We could panic on error, but let's silently ignore it for now + let result = (api.close)(self.0); + core::mem::forget(self); + result.into() + } - /// Construct a new `FileHandle` from an integer. - /// - /// Only the OS should call this. - /// - /// # Safety + /// Move the file offset (for the given file handle) to the given position. /// - /// The integer given must be a valid index for an open File. - #[cfg(feature = "os")] - pub const fn new(value: u8) -> FileHandle { - FileHandle(value) + /// Some files do not support seeking and will produce an error. + pub fn seek_set(&self, position: u64) -> Result<()> { + let api = get_api(); + match (api.seek_set)(self.0, position) { + neotron_ffi::FfiResult::Ok(_) => Ok(()), + neotron_ffi::FfiResult::Err(e) => Err(e), + } } - /// Create a file handle for Standard Output - pub const fn new_stdout() -> FileHandle { - FileHandle(Self::STDOUT) + /// Move the file offset (for the given file handle) relative to the current position. + /// + /// Returns the new position, or an error. + /// + /// Some files do not support seeking and will produce an error. + pub fn seek_cur(&self, offset: i64) -> Result { + let api = get_api(); + match (api.seek_cur)(self.0, offset) { + neotron_ffi::FfiResult::Ok(new_offset) => Ok(new_offset), + neotron_ffi::FfiResult::Err(e) => Err(e), + } } - /// Get the numeric value of this File Handle - pub const fn value(&self) -> u8 { - self.0 + /// Move the file offset (for the given file handle) to the end of the file + /// + /// Returns the new position, or an error. + /// + /// Some files do not support seeking and will produce an error. + pub fn seek_end(&self) -> Result { + let api = get_api(); + match (api.seek_end)(self.0) { + neotron_ffi::FfiResult::Ok(new_offset) => Ok(new_offset), + neotron_ffi::FfiResult::Err(e) => Err(e), + } } -} -/// Represents an open directory -#[repr(C)] -#[derive(Debug, PartialEq, Eq)] -pub struct DirHandle(u8); - -impl DirHandle { - /// Construct a new `DirHandle` from an integer. + /// Rename a file. /// - /// Only the OS should call this. + /// # Limitations /// - /// # Safety + /// * You cannot rename a file if it is currently open. + /// * You cannot rename a file where the `old_path` and the `new_path` are + /// not on the same drive. + /// * Paths must confirm to the rules for the filesystem for the given drive. + pub fn rename(old_path: file::Path, new_path: file::Path) -> Result<()> { + let api = get_api(); + match (api.rename)(old_path, new_path) { + neotron_ffi::FfiResult::Ok(_) => Ok(()), + neotron_ffi::FfiResult::Err(e) => Err(e), + } + } + + /// Perform a special I/O control operation. /// - /// The integer given must be a valid index for an open Directory. - #[cfg(feature = "os")] - pub const fn new(value: u8) -> DirHandle { - DirHandle(value) + /// The allowed values of `command` and `value` are TBD. + pub fn ioctl(&self, command: u64, value: u64) -> Result { + let api = get_api(); + match (api.ioctl)(self.0, command, value) { + neotron_ffi::FfiResult::Ok(output) => Ok(output), + neotron_ffi::FfiResult::Err(e) => Err(e), + } } - /// Get the numeric value of this Directory Handle - pub const fn value(&self) -> u8 { - self.0 + /// Get information about this file. + pub fn stat(&self) -> Result { + let api = get_api(); + match (api.fstat)(self.0) { + neotron_ffi::FfiResult::Ok(output) => Ok(output), + neotron_ffi::FfiResult::Err(e) => Err(e), + } } } -/// Describes an entry in a directory. -/// -/// This is set up for 8.3 filenames on MS-DOS FAT32 partitions currently. -#[repr(C)] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct DirEntry { - /// The name and extension of the file - pub name: [u8; MAX_FILENAME_LEN], - /// File properties - pub properties: Stat, +impl core::ops::Drop for File { + fn drop(&mut self) { + let api = get_api(); + // We could panic on error, but let's silently ignore it for now. + // If you care, call `file.close()`. + let _ = (api.close)(self.0); + } } -/// Describes a file on disk. -/// -/// This is set up for 8.3 filenames on MS-DOS FAT32 partitions currently. -#[repr(C)] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Stat { - /// How big is this file - pub file_size: u64, - /// When was the file created - pub ctime: FileTime, - /// When was the last modified - pub mtime: FileTime, - /// File attributes (Directory, Volume, etc) - pub attr: FileAttributes, -} +/// Represents an open directory that we are iterating through. +pub struct ReadDir(dir::Handle); -bitflags! { - #[repr(C)] - #[derive(Clone, Copy, Debug, PartialEq, Eq)] - /// Describes the attributes of a file. - pub struct FileFlags: u8 { - /// Enable write support for this file. - const WRITE = 0x01; - /// Create the file if it doesn't exist. - const CREATE = 0x02; - /// Truncate the file to zero length upon opening. - const TRUNCATE = 0x04; +impl ReadDir { + pub fn open(path: file::Path) -> Result { + let api = get_api(); + match (api.opendir)(path) { + neotron_ffi::FfiResult::Ok(output) => Ok(ReadDir(output)), + neotron_ffi::FfiResult::Err(e) => Err(e), + } } } -bitflags! { - #[repr(C)] - #[derive(Clone, Copy, Debug, PartialEq, Eq)] - /// The attributes a file on disk can have.alloc - /// - /// Based on that supported by the FAT32 file system. - pub struct FileAttributes: u8 { - /// File is read-only - const READ_ONLY = 0x01; - /// File should not appear in directory listing - const HIDDEN = 0x02; - /// File should not be moved on disk - const SYSTEM = 0x04; - /// File is a volume label - const VOLUME = 0x08; - /// File is a directory - const DIRECTORY = 0x10; - /// File has not been backed up - const ARCHIVE = 0x20; - /// File is actually a device - const DEVICE = 0x40; +impl Iterator for ReadDir { + type Item = Result; + + fn next(&mut self) -> Option { + None } } -/// Represents an instant in time, in the local time zone. -#[repr(C)] -#[derive(Debug, Copy, Clone, PartialOrd, Ord, PartialEq, Eq)] -pub struct FileTime { - /// Add 1970 to this file to get the calendar year - pub year_since_1970: u8, - /// Add one to this value to get the calendar month - pub zero_indexed_month: u8, - /// Add one to this value to get the calendar day - pub zero_indexed_day: u8, - /// The number of hours past midnight - pub hours: u8, - /// The number of minutes past the hour - pub minutes: u8, - /// The number of seconds past the minute - pub seconds: u8, +impl Drop for ReadDir { + fn drop(&mut self) { + let api = get_api(); + let _ = (api.closedir)(self.0); + } } // ============================================================================ // Functions // ============================================================================ -#[cfg(feature = "application")] -mod application { - use super::*; - use core::sync::atomic::{AtomicPtr, Ordering}; +/// The function the OS calls to start the application. +/// +/// Will jump to the application entry point, and `extern "C"` function +/// called `main`. +extern "C" fn app_entry(api: *mut Api) -> i32 { + API.store(api, Ordering::Relaxed); + unsafe { neotron_main() } +} - #[link_section = ".entry_point"] - #[used] - pub static APP_ENTRY: AppStartFn = app_entry; +/// Get information about a file on disk. +pub fn stat(_path: file::Path) -> Result { + todo!() +} - static API: AtomicPtr = AtomicPtr::new(core::ptr::null_mut()); +/// Delete a file from disk +pub fn delete(_path: file::Path) -> Result<()> { + todo!() +} - /// The function the OS calls to start the application. - /// - /// Will jump to the application entry point, and `extern "C"` function - /// called `main`. - extern "C" fn app_entry(api: *mut crate::Api) -> i32 { - API.store(api as *mut Api, Ordering::Relaxed); - unsafe { main() } - } +/// Change the current working directory to the given path. +pub fn chdir(_path: file::Path) -> Result<()> { + todo!() +} - /// Write to a file handle. - pub fn write(fd: FileHandle, data: &[u8]) -> Result<()> { - let api = get_api(); - (api.write)(fd, FfiByteSlice::new(data)) - } +/// Change the current working directory to that given by the handle. +pub fn dchdir(_dir: dir::Handle) -> Result<()> { + todo!() +} - /// Get the API structure so you can call APIs manually. - fn get_api() -> &'static Api { - let ptr = API.load(Ordering::Relaxed); - unsafe { ptr.as_ref().unwrap() } - } +/// Get the current working directory. +/// +/// Provided as a call-back, so the caller doesn't need to allocate storage space for the string. +pub fn pwd)>(callback: F) { + callback(Err(Error::FileNotFound)) } -#[cfg(feature = "application")] -pub use application::*; +/// Alllocate some memory +pub fn malloc(_size: usize, _alignment: usize) -> Result<*mut core::ffi::c_void> { + todo!() +} + +/// Free some previously allocated memory. +pub fn free(_ptr: *mut core::ffi::c_void, _size: usize, _alignment: usize) { + todo!() +} + +/// Get the API structure so we can call APIs manually. +/// +/// If you managed to not have `app_entry` called on start-up, this will panic. +fn get_api() -> &'static Api { + let ptr = API.load(Ordering::Relaxed); + unsafe { ptr.as_ref().unwrap() } +} // ============================================================================ // End of File From 1fc07583f7e0a5bf64d193eb89c3a1e2fa59aad3 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Thu, 8 Jun 2023 22:09:38 +0100 Subject: [PATCH 06/37] Fix the sample app. --- samples/hello/src/main.rs | 4 ++-- src/lib.rs | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/samples/hello/src/main.rs b/samples/hello/src/main.rs index 097925c..ea3f69a 100644 --- a/samples/hello/src/main.rs +++ b/samples/hello/src/main.rs @@ -5,8 +5,8 @@ extern crate neotron_sdk; #[no_mangle] extern "C" fn main() -> i32 { - let stdout = neotron_sdk::FileHandle::new_stdout(); - neotron_sdk::write(stdout, b"Hello, world\n"); + let stdout = neotron_sdk::stdout(); + stdout.write(b"Hello, world\n").unwrap(); 0 } diff --git a/src/lib.rs b/src/lib.rs index 9da71f4..86dedcf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -282,6 +282,21 @@ pub fn free(_ptr: *mut core::ffi::c_void, _size: usize, _alignment: usize) { todo!() } +/// Get a handle for Standard Input +pub fn stdin() -> File { + File(file::Handle::new_stdin()) +} + +/// Get a handle for Standard Output +pub fn stdout() -> File { + File(file::Handle::new_stdout()) +} + +/// Get a handle for Standard Error +pub fn stderr() -> File { + File(file::Handle::new_stderr()) +} + /// Get the API structure so we can call APIs manually. /// /// If you managed to not have `app_entry` called on start-up, this will panic. From f640a6e172c9177d0ad1b31cc7f42466c8515e00 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Thu, 8 Jun 2023 22:20:49 +0100 Subject: [PATCH 07/37] Get sample to build using linker script. --- samples/{hello => }/.cargo/config.toml | 0 samples/hello/build.rs | 21 +++++++++++++++++++++ samples/hello/src/main.rs | 2 +- samples/{hello => }/neotron-cortex-m.ld | 0 4 files changed, 22 insertions(+), 1 deletion(-) rename samples/{hello => }/.cargo/config.toml (100%) create mode 100644 samples/hello/build.rs rename samples/{hello => }/neotron-cortex-m.ld (100%) diff --git a/samples/hello/.cargo/config.toml b/samples/.cargo/config.toml similarity index 100% rename from samples/hello/.cargo/config.toml rename to samples/.cargo/config.toml diff --git a/samples/hello/build.rs b/samples/hello/build.rs new file mode 100644 index 0000000..b0576cb --- /dev/null +++ b/samples/hello/build.rs @@ -0,0 +1,21 @@ +use std::env; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; + +fn main() { + // Put `neotron-cortex-m.ld` in our output directory and ensure it's + // on the linker search path. + let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); + File::create(out.join("neotron-cortex-m.ld")) + .unwrap() + .write_all(include_bytes!("../neotron-cortex-m.ld")) + .unwrap(); + println!("cargo:rustc-link-search={}", out.display()); + + // By default, Cargo will re-run a build script whenever + // any file in the project changes. By specifying `neotron-cortex-m.ld` + // here, we ensure the build script is only re-run when + // `neotron-cortex-m.ld` is changed. + println!("cargo:rerun-if-changed=neotron-cortex-m.ld"); +} diff --git a/samples/hello/src/main.rs b/samples/hello/src/main.rs index ea3f69a..5dd2993 100644 --- a/samples/hello/src/main.rs +++ b/samples/hello/src/main.rs @@ -4,7 +4,7 @@ extern crate neotron_sdk; #[no_mangle] -extern "C" fn main() -> i32 { +extern "C" fn neotron_main() -> i32 { let stdout = neotron_sdk::stdout(); stdout.write(b"Hello, world\n").unwrap(); 0 diff --git a/samples/hello/neotron-cortex-m.ld b/samples/neotron-cortex-m.ld similarity index 100% rename from samples/hello/neotron-cortex-m.ld rename to samples/neotron-cortex-m.ld From 05f27dfad77ecd986b71b0f661f828e136f050ad Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Fri, 9 Jun 2023 20:38:56 +0100 Subject: [PATCH 08/37] Use new path type from neotron-api. --- src/lib.rs | 45 +++++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 86dedcf..8c17595 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,7 +12,9 @@ use core::sync::atomic::{AtomicPtr, Ordering}; pub use neotron_ffi::{FfiBuffer, FfiByteSlice, FfiString}; -pub use neotron_api::{dir, file, Api, Error}; +pub use neotron_api::{path, Api, Error}; + +use neotron_api as api; // ============================================================================ // Constants @@ -58,7 +60,7 @@ pub type AppStartFn = extern "C" fn(*mut crate::Api) -> i32; pub type Result = core::result::Result; /// Represents an open File -pub struct File(file::Handle); +pub struct File(api::file::Handle); impl File { /// Open a file, given a path as UTF-8 string. @@ -73,9 +75,9 @@ impl File { /// * You cannot open a file if it is currently open. /// * Paths must confirm to the rules for the filesystem for the given drive. /// * Relative paths are taken relative to the current directory (see `Api::chdir`). - pub fn open(path: file::Path, flags: file::Flags) -> Result { + pub fn open(path: path::Path, flags: api::file::Flags) -> Result { let api = get_api(); - match (api.open)(path, flags) { + match (api.open)(FfiString::new(path.as_str()), flags) { neotron_ffi::FfiResult::Ok(handle) => Ok(File(handle)), neotron_ffi::FfiResult::Err(e) => Err(e), } @@ -166,9 +168,12 @@ impl File { /// * You cannot rename a file where the `old_path` and the `new_path` are /// not on the same drive. /// * Paths must confirm to the rules for the filesystem for the given drive. - pub fn rename(old_path: file::Path, new_path: file::Path) -> Result<()> { + pub fn rename(old_path: path::Path, new_path: path::Path) -> Result<()> { let api = get_api(); - match (api.rename)(old_path, new_path) { + match (api.rename)( + FfiString::new(old_path.as_str()), + FfiString::new(new_path.as_str()), + ) { neotron_ffi::FfiResult::Ok(_) => Ok(()), neotron_ffi::FfiResult::Err(e) => Err(e), } @@ -186,7 +191,7 @@ impl File { } /// Get information about this file. - pub fn stat(&self) -> Result { + pub fn stat(&self) -> Result { let api = get_api(); match (api.fstat)(self.0) { neotron_ffi::FfiResult::Ok(output) => Ok(output), @@ -205,12 +210,12 @@ impl core::ops::Drop for File { } /// Represents an open directory that we are iterating through. -pub struct ReadDir(dir::Handle); +pub struct ReadDir(api::dir::Handle); impl ReadDir { - pub fn open(path: file::Path) -> Result { + pub fn open(path: path::Path) -> Result { let api = get_api(); - match (api.opendir)(path) { + match (api.opendir)(FfiString::new(path.as_str())) { neotron_ffi::FfiResult::Ok(output) => Ok(ReadDir(output)), neotron_ffi::FfiResult::Err(e) => Err(e), } @@ -218,7 +223,7 @@ impl ReadDir { } impl Iterator for ReadDir { - type Item = Result; + type Item = Result; fn next(&mut self) -> Option { None @@ -246,30 +251,30 @@ extern "C" fn app_entry(api: *mut Api) -> i32 { } /// Get information about a file on disk. -pub fn stat(_path: file::Path) -> Result { +pub fn stat(_path: path::Path) -> Result { todo!() } /// Delete a file from disk -pub fn delete(_path: file::Path) -> Result<()> { +pub fn delete(_path: path::Path) -> Result<()> { todo!() } /// Change the current working directory to the given path. -pub fn chdir(_path: file::Path) -> Result<()> { +pub fn chdir(_path: path::Path) -> Result<()> { todo!() } /// Change the current working directory to that given by the handle. -pub fn dchdir(_dir: dir::Handle) -> Result<()> { +pub fn dchdir(_dir: api::dir::Handle) -> Result<()> { todo!() } /// Get the current working directory. /// /// Provided as a call-back, so the caller doesn't need to allocate storage space for the string. -pub fn pwd)>(callback: F) { - callback(Err(Error::FileNotFound)) +pub fn pwd)>(callback: F) { + callback(Err(Error::NotFound)) } /// Alllocate some memory @@ -284,17 +289,17 @@ pub fn free(_ptr: *mut core::ffi::c_void, _size: usize, _alignment: usize) { /// Get a handle for Standard Input pub fn stdin() -> File { - File(file::Handle::new_stdin()) + File(api::file::Handle::new_stdin()) } /// Get a handle for Standard Output pub fn stdout() -> File { - File(file::Handle::new_stdout()) + File(api::file::Handle::new_stdout()) } /// Get a handle for Standard Error pub fn stderr() -> File { - File(file::Handle::new_stderr()) + File(api::file::Handle::new_stderr()) } /// Get the API structure so we can call APIs manually. From 0302e0f40f0bfb5df5dbfed6841e9d34ca8133e8 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Fri, 9 Jun 2023 21:50:50 +0100 Subject: [PATCH 09/37] Use published crate. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 3046638..b620053 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ authors = ["Jonathan 'theJPster' Pallant "] [dependencies] bitflags = "2" neotron-ffi = "0.1" -neotron-api = { git = "https://github.com/neotron-compute/neotron-api.git", branch = "seek-updates" } +neotron-api = "0.1" [features] # Enables functions the OS needs but an application does not From 41fd1376bfb2b885c2080cde581876d51afe89c2 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sat, 10 Jun 2023 22:23:38 +0100 Subject: [PATCH 10/37] Add a fancy panic handler, and some more samples. * File (and &File) now support core::fmt::Write. * neotron-sdk now supplies a panic handler. --- .github/workflows/build.yml | 15 +++++++++++---- Cargo.toml | 6 ++---- samples/.gitignore | 3 ++- samples/Cargo.toml | 8 -------- samples/README.md | 13 ++++++++++--- samples/build.sh | 11 +++++++++++ samples/fault/.gitignore | 1 + samples/fault/Cargo.toml | 18 ++++++++++++++++++ samples/fault/README.md | 19 +++++++++++++++++++ samples/fault/build.rs | 21 +++++++++++++++++++++ samples/fault/src/main.rs | 14 ++++++++++++++ samples/hello/.gitignore | 1 + samples/hello/Cargo.toml | 17 +++++++++-------- samples/hello/build.rs | 2 +- samples/hello/src/main.rs | 13 ++++--------- samples/panic/.gitignore | 1 + samples/panic/Cargo.toml | 18 ++++++++++++++++++ samples/panic/README.md | 19 +++++++++++++++++++ samples/panic/build.rs | 21 +++++++++++++++++++++ samples/panic/src/main.rs | 11 +++++++++++ src/lib.rs | 32 ++++++++++++++++++++++++++++++++ 21 files changed, 226 insertions(+), 38 deletions(-) delete mode 100644 samples/Cargo.toml create mode 100755 samples/build.sh create mode 100644 samples/fault/.gitignore create mode 100644 samples/fault/Cargo.toml create mode 100644 samples/fault/README.md create mode 100644 samples/fault/build.rs create mode 100644 samples/fault/src/main.rs create mode 100644 samples/hello/.gitignore create mode 100644 samples/panic/.gitignore create mode 100644 samples/panic/Cargo.toml create mode 100644 samples/panic/README.md create mode 100644 samples/panic/build.rs create mode 100644 samples/panic/src/main.rs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 01f74aa..e9b4f9e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,6 +18,9 @@ jobs: - name: Add targets run: | rustup target add thumbv6m-none-eabi + rustup target add thumbv7m-none-eabi + rustup target add thumbv7em-none-eabi + cargo install cargo-binutils - name: Build lib (native) run: | @@ -27,10 +30,14 @@ jobs: run: | cargo test --verbose - - name: Build lib (Cortex-M0), OS mode + - name: Build samples (Cortex-M0) run: | - cargo build --verbose --target=thumbv6m-none-eabi --features=os + cd samples && ./build.sh thumbv6m-none-eabi - - name: Build samples (Cortex-M0) + - name: Build samples (Cortex-M3) + run: | + cd samples && ./build.sh thumbv7m-none-eabi + + - name: Build samples (Cortex-M4) run: | - cd samples && cargo build --verbose --target=thumbv6m-none-eabi + cd samples && ./build.sh thumbv7em-none-eabi diff --git a/Cargo.toml b/Cargo.toml index b620053..38339af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,5 @@ neotron-ffi = "0.1" neotron-api = "0.1" [features] -# Enables functions the OS needs but an application does not -os = [] -# Enables functions an application needs but the OS does not -application = [] +# Prints panic info. Costs you about 14K of code. +fancy-panic = [] diff --git a/samples/.gitignore b/samples/.gitignore index eb5a316..936164a 100644 --- a/samples/.gitignore +++ b/samples/.gitignore @@ -1 +1,2 @@ -target +*.bin + diff --git a/samples/Cargo.toml b/samples/Cargo.toml deleted file mode 100644 index 52b2ccd..0000000 --- a/samples/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[workspace] -members = ["hello"] - -[profile.dev] -panic = "abort" - -[profile.release] -panic = "abort" diff --git a/samples/README.md b/samples/README.md index 7f0b1e0..826ed45 100644 --- a/samples/README.md +++ b/samples/README.md @@ -8,7 +8,7 @@ Build the application as follows: ```console $ cargo build --release --target=thumbv6m-none-eabi -$ cargo objcopy --release --target=thumbv6m-none-eabi -- -O binary hello.bin +$ rust-objcopy -O binary ./target/thumbv6m-none-eabi/release/hello hello.bin ``` Then copy the resulting `hello.bin` file to an SD card and insert it into your Neotron system. You can load the application with something like: @@ -18,10 +18,10 @@ Then copy the resulting `hello.bin` file to an SD card and insert it into your N > run ``` -If you don't have `cargo-binutils` installed (which adds the `objcopy` sub-command), install it with: +If you don't have `rust-objcopy` installed, install it with: ```console -$ cargo install cargo-binutils +$ rustup component add llvm-tools ``` ## List of Sample Applications @@ -30,3 +30,10 @@ $ cargo install cargo-binutils This is a basic "Hello World" application. It prints the string "Hello, world" to *standard output* and then exits with an exit code of 0. +## [`panic`](./panic) + +This application panics, printing a nice panic message. + +## [`fault`](./fault) + +This application generates a Hard Fault. diff --git a/samples/build.sh b/samples/build.sh new file mode 100755 index 0000000..c816fdd --- /dev/null +++ b/samples/build.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +set -euo pipefail + +TARGET=${1:-thumbv6m-none-eabi} + +echo "Building for ${TARGET}" +for program in panic hello fault; do + ( cd ${program} && cargo build --target=${TARGET} --release ) + rust-objcopy -O binary ./${program}/target/${TARGET}/release/${program} ${program}.bin +done diff --git a/samples/fault/.gitignore b/samples/fault/.gitignore new file mode 100644 index 0000000..eb5a316 --- /dev/null +++ b/samples/fault/.gitignore @@ -0,0 +1 @@ +target diff --git a/samples/fault/Cargo.toml b/samples/fault/Cargo.toml new file mode 100644 index 0000000..9789263 --- /dev/null +++ b/samples/fault/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "fault" +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" +authors = ["Jonathan 'theJPster' Pallant "] +description = "Hello World for Neotron systems" + +[dependencies] +neotron-sdk = { path = "../.." } + +[profile.dev] +panic = "abort" + +[profile.release] +panic = "unwind" +opt-level = "z" +lto = "fat" diff --git a/samples/fault/README.md b/samples/fault/README.md new file mode 100644 index 0000000..e907151 --- /dev/null +++ b/samples/fault/README.md @@ -0,0 +1,19 @@ +# Fault + +A basic *faulting* application for Neotron systems. This program will jump to +the address 0xDEADC0DE, which will cause a Hard Fault. + +See the general sample application [README](../README.md) for compilation instructions. + +## Licence + +Copyright (c) The Neotron Developers, 2023 + +Licensed under either [MIT](../../LICENSE-MIT) or [Apache-2.0](../../LICENSE-APACHE) at +your option. + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you shall be licensed as above, without any +additional terms or conditions. diff --git a/samples/fault/build.rs b/samples/fault/build.rs new file mode 100644 index 0000000..759eb7d --- /dev/null +++ b/samples/fault/build.rs @@ -0,0 +1,21 @@ +use std::env; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; + +fn main() { + // Put `neotron-cortex-m.ld` in our output directory and ensure it's + // on the linker search path. + let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); + File::create(out.join("neotron-cortex-m.ld")) + .unwrap() + .write_all(include_bytes!("../neotron-cortex-m.ld")) + .unwrap(); + println!("cargo:rustc-link-search={}", out.display()); + + // By default, Cargo will re-run a build script whenever + // any file in the project changes. By specifying `neotron-cortex-m.ld` + // here, we ensure the build script is only re-run when + // `neotron-cortex-m.ld` is changed. + println!("cargo:rerun-if-changed=../neotron-cortex-m.ld"); +} diff --git a/samples/fault/src/main.rs b/samples/fault/src/main.rs new file mode 100644 index 0000000..f0de2e9 --- /dev/null +++ b/samples/fault/src/main.rs @@ -0,0 +1,14 @@ +#![no_std] +#![no_main] + +use core::fmt::Write; + +#[no_mangle] +extern "C" fn neotron_main() -> i32 { + let stdout = neotron_sdk::stdout(); + writeln!(&stdout, "About to fault...\n").unwrap(); + let bad_address: u32 = 0xDEAD_C0DE; + let bad_fn: fn() = unsafe { core::mem::transmute(bad_address) }; + bad_fn(); + 0 +} diff --git a/samples/hello/.gitignore b/samples/hello/.gitignore new file mode 100644 index 0000000..eb5a316 --- /dev/null +++ b/samples/hello/.gitignore @@ -0,0 +1 @@ +target diff --git a/samples/hello/Cargo.toml b/samples/hello/Cargo.toml index 58a5230..f42b6ba 100644 --- a/samples/hello/Cargo.toml +++ b/samples/hello/Cargo.toml @@ -7,11 +7,12 @@ authors = ["Jonathan 'theJPster' Pallant "] description = "Hello World for Neotron systems" [dependencies] -neotron-sdk = { path = "../..", features = ["application"] } -# -# NB: This is in a workspace, so these settings are in the workspace file. -#[profile.dev] -#panic = "abort" -# -#[profile.release] -#panic = "abort" +neotron-sdk = { path = "../.." } + +[profile.dev] +panic = "abort" + +[profile.release] +panic = "unwind" +opt-level = "z" +lto = "fat" diff --git a/samples/hello/build.rs b/samples/hello/build.rs index b0576cb..759eb7d 100644 --- a/samples/hello/build.rs +++ b/samples/hello/build.rs @@ -17,5 +17,5 @@ fn main() { // any file in the project changes. By specifying `neotron-cortex-m.ld` // here, we ensure the build script is only re-run when // `neotron-cortex-m.ld` is changed. - println!("cargo:rerun-if-changed=neotron-cortex-m.ld"); + println!("cargo:rerun-if-changed=../neotron-cortex-m.ld"); } diff --git a/samples/hello/src/main.rs b/samples/hello/src/main.rs index 5dd2993..36add0a 100644 --- a/samples/hello/src/main.rs +++ b/samples/hello/src/main.rs @@ -6,14 +6,9 @@ extern crate neotron_sdk; #[no_mangle] extern "C" fn neotron_main() -> i32 { let stdout = neotron_sdk::stdout(); - stdout.write(b"Hello, world\n").unwrap(); - 0 -} - -#[inline(never)] -#[panic_handler] -fn panic(_info: &core::panic::PanicInfo) -> ! { - loop { - // Spin + if stdout.write(b"Hello, world\n").is_ok() { + 0 + } else { + 1 } } diff --git a/samples/panic/.gitignore b/samples/panic/.gitignore new file mode 100644 index 0000000..eb5a316 --- /dev/null +++ b/samples/panic/.gitignore @@ -0,0 +1 @@ +target diff --git a/samples/panic/Cargo.toml b/samples/panic/Cargo.toml new file mode 100644 index 0000000..c5db1f4 --- /dev/null +++ b/samples/panic/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "panic" +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" +authors = ["Jonathan 'theJPster' Pallant "] +description = "Hello World for Neotron systems" + +[dependencies] +neotron-sdk = { path = "../..", features = ["fancy-panic"] } + +[profile.dev] +panic = "abort" + +[profile.release] +panic = "unwind" +opt-level = "z" +lto = "fat" diff --git a/samples/panic/README.md b/samples/panic/README.md new file mode 100644 index 0000000..37b2064 --- /dev/null +++ b/samples/panic/README.md @@ -0,0 +1,19 @@ +# Hello, World! + +A basic *panicking* application for Neotron systems. This program will cause a +panic, and the panic handler will print to stdout. + +See the general sample application [README](../README.md) for compilation instructions. + +## Licence + +Copyright (c) The Neotron Developers, 2023 + +Licensed under either [MIT](../../LICENSE-MIT) or [Apache-2.0](../../LICENSE-APACHE) at +your option. + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you shall be licensed as above, without any +additional terms or conditions. diff --git a/samples/panic/build.rs b/samples/panic/build.rs new file mode 100644 index 0000000..759eb7d --- /dev/null +++ b/samples/panic/build.rs @@ -0,0 +1,21 @@ +use std::env; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; + +fn main() { + // Put `neotron-cortex-m.ld` in our output directory and ensure it's + // on the linker search path. + let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); + File::create(out.join("neotron-cortex-m.ld")) + .unwrap() + .write_all(include_bytes!("../neotron-cortex-m.ld")) + .unwrap(); + println!("cargo:rustc-link-search={}", out.display()); + + // By default, Cargo will re-run a build script whenever + // any file in the project changes. By specifying `neotron-cortex-m.ld` + // here, we ensure the build script is only re-run when + // `neotron-cortex-m.ld` is changed. + println!("cargo:rerun-if-changed=../neotron-cortex-m.ld"); +} diff --git a/samples/panic/src/main.rs b/samples/panic/src/main.rs new file mode 100644 index 0000000..885e9bc --- /dev/null +++ b/samples/panic/src/main.rs @@ -0,0 +1,11 @@ +#![no_std] +#![no_main] + +use core::fmt::Write; + +#[no_mangle] +extern "C" fn neotron_main() -> i32 { + let stdout = neotron_sdk::stdout(); + writeln!(&stdout, "About to panic...\n").unwrap(); + panic!("Oh no, I panicked!"); +} diff --git a/src/lib.rs b/src/lib.rs index 8c17595..e3f3aa3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -209,6 +209,18 @@ impl core::ops::Drop for File { } } +impl core::fmt::Write for File { + fn write_str(&mut self, s: &str) -> core::fmt::Result { + self.write(s.as_bytes()).map_err(|_| core::fmt::Error) + } +} + +impl core::fmt::Write for &File { + fn write_str(&mut self, s: &str) -> core::fmt::Result { + self.write(s.as_bytes()).map_err(|_| core::fmt::Error) + } +} + /// Represents an open directory that we are iterating through. pub struct ReadDir(api::dir::Handle); @@ -310,6 +322,26 @@ fn get_api() -> &'static Api { unsafe { ptr.as_ref().unwrap() } } +#[cfg(feature = "fancy-panic")] +#[inline(never)] +#[panic_handler] +fn panic(info: &core::panic::PanicInfo) -> ! { + use core::fmt::Write; + let stdout = stdout(); + let _ = writeln!(&stdout, "Panic:\n{:#?}", info); + loop {} +} + +#[cfg(not(feature = "fancy-panic"))] +#[inline(never)] +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + use core::fmt::Write; + let stdout = stdout(); + let _ = writeln!(&stdout, "Panic!"); + loop {} +} + // ============================================================================ // End of File // ============================================================================ From 8c4fe8d372dcc355869237462585820054448434 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sat, 10 Jun 2023 22:36:30 +0100 Subject: [PATCH 11/37] Don't set panic handlers when testing. --- src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e3f3aa3..a06f512 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -322,7 +322,7 @@ fn get_api() -> &'static Api { unsafe { ptr.as_ref().unwrap() } } -#[cfg(feature = "fancy-panic")] +#[cfg(all(feature = "fancy-panic", not(test)))] #[inline(never)] #[panic_handler] fn panic(info: &core::panic::PanicInfo) -> ! { @@ -332,7 +332,7 @@ fn panic(info: &core::panic::PanicInfo) -> ! { loop {} } -#[cfg(not(feature = "fancy-panic"))] +#[cfg(all(not(feature = "fancy-panic"), not(test)))] #[inline(never)] #[panic_handler] fn panic(_info: &core::panic::PanicInfo) -> ! { From 671aae16b8d5b7e1f081b576d658ca4578856532 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sat, 10 Jun 2023 23:05:43 +0100 Subject: [PATCH 12/37] Oh, you need llvm-tools as well. --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e9b4f9e..51b739d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,6 +20,7 @@ jobs: rustup target add thumbv6m-none-eabi rustup target add thumbv7m-none-eabi rustup target add thumbv7em-none-eabi + rustup component add llvm-tools cargo install cargo-binutils - name: Build lib (native) From 29b4e1570e0015f34d048ad4ce27a2c8a0889a51 Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Fri, 16 Jun 2023 21:56:37 +0100 Subject: [PATCH 13/37] Set --nmagic to avoid weirdness, and skip entry u32. --- samples/.cargo/config.toml | 8 ++++---- samples/neotron-cortex-m.ld | 32 ++++++-------------------------- src/lib.rs | 5 +---- 3 files changed, 11 insertions(+), 34 deletions(-) diff --git a/samples/.cargo/config.toml b/samples/.cargo/config.toml index b92398d..5dd2536 100644 --- a/samples/.cargo/config.toml +++ b/samples/.cargo/config.toml @@ -1,11 +1,11 @@ [target.thumbv7em-none-eabihf] -rustflags = ["-C", "link-arg=-Tneotron-cortex-m.ld"] +rustflags = ["-C", "link-arg=-Tneotron-cortex-m.ld"] # , "-C", "link-arg=--nmagic"] [target.thumbv7em-none-eabi] -rustflags = ["-C", "link-arg=-Tneotron-cortex-m.ld"] +rustflags = ["-C", "link-arg=-Tneotron-cortex-m.ld"] # , "-C", "link-arg=--nmagic"] [target.thumbv7m-none-eabi] -rustflags = ["-C", "link-arg=-Tneotron-cortex-m.ld"] +rustflags = ["-C", "link-arg=-Tneotron-cortex-m.ld"] # , "-C", "link-arg=--nmagic"] [target.thumbv6m-none-eabi] -rustflags = ["-C", "link-arg=-Tneotron-cortex-m.ld"] +rustflags = ["-C", "link-arg=-Tneotron-cortex-m.ld"] # , "-C", "link-arg=--nmagic"] diff --git a/samples/neotron-cortex-m.ld b/samples/neotron-cortex-m.ld index 1f5547b..205e921 100644 --- a/samples/neotron-cortex-m.ld +++ b/samples/neotron-cortex-m.ld @@ -25,31 +25,19 @@ ENTRY(app_entry); /* # Sections */ SECTIONS { - - /* ### .entry_point */ - .entry_point ORIGIN(RAM) : - { - KEEP(*(.entry_point)) - } > RAM - - PROVIDE(_stext = ADDR(.entry_point) + SIZEOF(.entry_point)); - /* ### .text */ - .text _stext : + .text : ALIGN(4) { + . = ALIGN(4); *(.text .text.*); - *(.HardFaultTrampoline); - *(.HardFault.*); + . = ALIGN(4); } /* ### .rodata */ .rodata : ALIGN(4) { + . = ALIGN(4); *(.rodata .rodata.*); - - /* 4-byte align the end (VMA) of this section. - This is required by LLD to ensure the LMA of the following .data - section will have the correct alignment. */ . = ALIGN(4); } @@ -57,10 +45,8 @@ SECTIONS .data : ALIGN(4) { . = ALIGN(4); - __sdata = .; *(.data .data.*); - . = ALIGN(4); /* 4-byte align the end (VMA) of this section */ - __edata = .; + . = ALIGN(4); } /* LMA of .data */ @@ -70,10 +56,8 @@ SECTIONS .bss : ALIGN(4) { . = ALIGN(4); - __sbss = .; *(.bss .bss.*); - . = ALIGN(4); /* 4-byte align the end (VMA) of this section */ - __ebss = .; + . = ALIGN(4); } /* ### .uninit */ @@ -84,10 +68,6 @@ SECTIONS . = ALIGN(4); } - /* Place the heap right after `.uninit` */ - . = ALIGN(4); - __sheap = .; - /* ## .got */ /* Dynamic relocations are unsupported. This section is only used to detect relocatable code in the input files and raise an error if relocatable code is found */ diff --git a/src/lib.rs b/src/lib.rs index a06f512..555c621 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,10 +36,6 @@ extern "C" { // Static Variables // ============================================================================ -#[link_section = ".entry_point"] -#[used] -pub static APP_ENTRY: AppStartFn = app_entry; - /// Holds a pointer to the OS API provided by the OS on start-up. /// /// Once you've hit the application `main()`, this will be non-null. @@ -257,6 +253,7 @@ impl Drop for ReadDir { /// /// Will jump to the application entry point, and `extern "C"` function /// called `main`. +#[no_mangle] extern "C" fn app_entry(api: *mut Api) -> i32 { API.store(api, Ordering::Relaxed); unsafe { neotron_main() } From cde4309f9b0b8ac30c9003f0dc92473dad46da73 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sun, 25 Jun 2023 21:54:14 +0100 Subject: [PATCH 14/37] Update build script --- samples/build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/build.sh b/samples/build.sh index c816fdd..0f32b75 100755 --- a/samples/build.sh +++ b/samples/build.sh @@ -7,5 +7,5 @@ TARGET=${1:-thumbv6m-none-eabi} echo "Building for ${TARGET}" for program in panic hello fault; do ( cd ${program} && cargo build --target=${TARGET} --release ) - rust-objcopy -O binary ./${program}/target/${TARGET}/release/${program} ${program}.bin + cp ./${program}/target/${TARGET}/release/${program} ${program}.elf done From a2f224840c4cd5076c767e0df2322b10bde24945 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sun, 25 Jun 2023 21:54:40 +0100 Subject: [PATCH 15/37] Ignore ELF binaries --- .gitignore | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index b7e28fc..e87a591 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,5 @@ Cargo.lock **/*.rs.bk -# Added by cargo +samples/*.elf -/target -/Cargo.lock From 0705187970249ae4b068472f1f4c5bd885ad8ee4 Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Fri, 21 Jul 2023 16:49:24 +0100 Subject: [PATCH 16/37] Adds new input test sample program. --- samples/README.md | 10 +++++++--- samples/input_test/.gitignore | 1 + samples/input_test/Cargo.toml | 18 ++++++++++++++++++ samples/input_test/README.md | 18 ++++++++++++++++++ samples/input_test/build.rs | 21 +++++++++++++++++++++ samples/input_test/src/main.rs | 32 ++++++++++++++++++++++++++++++++ 6 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 samples/input_test/.gitignore create mode 100644 samples/input_test/Cargo.toml create mode 100644 samples/input_test/README.md create mode 100644 samples/input_test/build.rs create mode 100644 samples/input_test/src/main.rs diff --git a/samples/README.md b/samples/README.md index 826ed45..13eef8f 100644 --- a/samples/README.md +++ b/samples/README.md @@ -8,13 +8,13 @@ Build the application as follows: ```console $ cargo build --release --target=thumbv6m-none-eabi -$ rust-objcopy -O binary ./target/thumbv6m-none-eabi/release/hello hello.bin +$ cp ./target/thumbv6m-none-eabi/release/hello /my/sdcard/hello.elf ``` -Then copy the resulting `hello.bin` file to an SD card and insert it into your Neotron system. You can load the application with something like: +Then copy the resulting `hello.elf` file to an SD card and insert it into your Neotron system. You can load the application with something like: ```text -> load hello.bin +> load hello.elf > run ``` @@ -30,6 +30,10 @@ $ rustup component add llvm-tools This is a basic "Hello World" application. It prints the string "Hello, world" to *standard output* and then exits with an exit code of 0. +## [`input-test`](./input-test) + +This reports any bytes received on Standard Input. Press Ctrl-X to quit. + ## [`panic`](./panic) This application panics, printing a nice panic message. diff --git a/samples/input_test/.gitignore b/samples/input_test/.gitignore new file mode 100644 index 0000000..eb5a316 --- /dev/null +++ b/samples/input_test/.gitignore @@ -0,0 +1 @@ +target diff --git a/samples/input_test/Cargo.toml b/samples/input_test/Cargo.toml new file mode 100644 index 0000000..e75e1eb --- /dev/null +++ b/samples/input_test/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "input-test" +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" +authors = ["Jonathan 'theJPster' Pallant "] +description = "Hello World for Neotron systems" + +[dependencies] +neotron-sdk = { path = "../.." } + +[profile.dev] +panic = "abort" + +[profile.release] +panic = "unwind" +opt-level = "z" +lto = "fat" diff --git a/samples/input_test/README.md b/samples/input_test/README.md new file mode 100644 index 0000000..8db7538 --- /dev/null +++ b/samples/input_test/README.md @@ -0,0 +1,18 @@ +# Hello, World! + +A basic standard input test application for Neotron systems. + +See the general sample application [README](../README.md) for compilation instructions. + +## Licence + +Copyright (c) The Neotron Developers, 2023 + +Licensed under either [MIT](../../LICENSE-MIT) or [Apache-2.0](../../LICENSE-APACHE) at +your option. + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you shall be licensed as above, without any +additional terms or conditions. diff --git a/samples/input_test/build.rs b/samples/input_test/build.rs new file mode 100644 index 0000000..759eb7d --- /dev/null +++ b/samples/input_test/build.rs @@ -0,0 +1,21 @@ +use std::env; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; + +fn main() { + // Put `neotron-cortex-m.ld` in our output directory and ensure it's + // on the linker search path. + let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); + File::create(out.join("neotron-cortex-m.ld")) + .unwrap() + .write_all(include_bytes!("../neotron-cortex-m.ld")) + .unwrap(); + println!("cargo:rustc-link-search={}", out.display()); + + // By default, Cargo will re-run a build script whenever + // any file in the project changes. By specifying `neotron-cortex-m.ld` + // here, we ensure the build script is only re-run when + // `neotron-cortex-m.ld` is changed. + println!("cargo:rerun-if-changed=../neotron-cortex-m.ld"); +} diff --git a/samples/input_test/src/main.rs b/samples/input_test/src/main.rs new file mode 100644 index 0000000..9dc9891 --- /dev/null +++ b/samples/input_test/src/main.rs @@ -0,0 +1,32 @@ +#![no_std] +#![no_main] + +extern crate neotron_sdk; + +use core::fmt::Write; + +#[no_mangle] +extern "C" fn neotron_main() -> i32 { + let mut stdout = neotron_sdk::stdout(); + let stdin = neotron_sdk::stdin(); + let _ = stdout.write(b"Type some things, press Ctrl-X to quit...\n"); + loop { + let mut buffer = [0u8; 16]; + match stdin.read(&mut buffer) { + Err(_) => { + return 1; + } + Ok(0) => { + // Do nothing + } + Ok(n) => { + for b in &buffer[0..n] { + let _ = writeln!(stdout, "0x{:02x}", b); + if *b == 0x18 { + return 0; + } + } + } + } + } +} From 1ee28615460dd54c0bfd5b8f4318e98ad7be4f80 Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Fri, 28 Jul 2023 14:04:34 +0100 Subject: [PATCH 17/37] Clean up existing build script. --- samples/.gitignore | 3 ++- samples/build.sh | 18 ++++++++++++++++-- samples/fault/src/main.rs | 2 +- samples/{input_test => input-test}/.gitignore | 0 samples/{input_test => input-test}/Cargo.toml | 0 samples/{input_test => input-test}/README.md | 0 samples/{input_test => input-test}/build.rs | 0 samples/{input_test => input-test}/src/main.rs | 0 8 files changed, 19 insertions(+), 4 deletions(-) rename samples/{input_test => input-test}/.gitignore (100%) rename samples/{input_test => input-test}/Cargo.toml (100%) rename samples/{input_test => input-test}/README.md (100%) rename samples/{input_test => input-test}/build.rs (100%) rename samples/{input_test => input-test}/src/main.rs (100%) diff --git a/samples/.gitignore b/samples/.gitignore index 936164a..da6d86c 100644 --- a/samples/.gitignore +++ b/samples/.gitignore @@ -1,2 +1,3 @@ -*.bin +release/ + diff --git a/samples/build.sh b/samples/build.sh index 0f32b75..e424a76 100755 --- a/samples/build.sh +++ b/samples/build.sh @@ -1,11 +1,25 @@ #!/bin/bash +# +# Builds all the Neotron SDK sample binaries. +# +# Specify the target as the first argument. Defaults to "thumbv6m-none-eabi" if +# not given. +# +# ```console +# $ ./build.sh thumbv7em-none-eabi +# $ ls *.elf +# ``` +# + set -euo pipefail TARGET=${1:-thumbv6m-none-eabi} +mkdir -p ./release + echo "Building for ${TARGET}" -for program in panic hello fault; do +for program in panic hello fault input-test; do ( cd ${program} && cargo build --target=${TARGET} --release ) - cp ./${program}/target/${TARGET}/release/${program} ${program}.elf + cp ./${program}/target/${TARGET}/release/${program} ./release/${program}.elf done diff --git a/samples/fault/src/main.rs b/samples/fault/src/main.rs index f0de2e9..66d2c3d 100644 --- a/samples/fault/src/main.rs +++ b/samples/fault/src/main.rs @@ -7,7 +7,7 @@ use core::fmt::Write; extern "C" fn neotron_main() -> i32 { let stdout = neotron_sdk::stdout(); writeln!(&stdout, "About to fault...\n").unwrap(); - let bad_address: u32 = 0xDEAD_C0DE; + let bad_address: usize = 0xDEAD_C0DE; let bad_fn: fn() = unsafe { core::mem::transmute(bad_address) }; bad_fn(); 0 diff --git a/samples/input_test/.gitignore b/samples/input-test/.gitignore similarity index 100% rename from samples/input_test/.gitignore rename to samples/input-test/.gitignore diff --git a/samples/input_test/Cargo.toml b/samples/input-test/Cargo.toml similarity index 100% rename from samples/input_test/Cargo.toml rename to samples/input-test/Cargo.toml diff --git a/samples/input_test/README.md b/samples/input-test/README.md similarity index 100% rename from samples/input_test/README.md rename to samples/input-test/README.md diff --git a/samples/input_test/build.rs b/samples/input-test/build.rs similarity index 100% rename from samples/input_test/build.rs rename to samples/input-test/build.rs diff --git a/samples/input_test/src/main.rs b/samples/input-test/src/main.rs similarity index 100% rename from samples/input_test/src/main.rs rename to samples/input-test/src/main.rs From af2d7ad84e87ef9cd081048c3d6a6668b2e08887 Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Fri, 28 Jul 2023 14:05:33 +0100 Subject: [PATCH 18/37] Add VSCode config. Gives you r-a support in the sample projects. --- .vscode/settings.json | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c05c971 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "rust-analyzer.linkedProjects": [ + "./samples/input-test/Cargo.toml", + "./samples/panic/Cargo.toml", + "./samples/hello/Cargo.toml", + "./samples/fault/Cargo.toml", + "./Cargo.toml" + ] +} \ No newline at end of file From 8a3c3ea46a7c50be0785daaf0ecee7d982394dfc Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Fri, 28 Jul 2023 21:07:55 +0100 Subject: [PATCH 19/37] Set it up to build for the host too. --- Cargo.toml | 7 +- samples/.cargo/config.toml | 8 +- samples/.gitignore | 5 +- samples/Cargo.toml | 11 ++ samples/build.sh | 12 +- samples/fault/.gitignore | 1 - samples/fault/Cargo.toml | 8 +- samples/fault/src/main.rs | 9 +- samples/hello/.gitignore | 1 - samples/hello/Cargo.toml | 8 +- samples/hello/src/main.rs | 9 +- samples/input-test/.gitignore | 1 - samples/input-test/Cargo.toml | 8 +- samples/input-test/src/main.rs | 9 +- samples/panic/.gitignore | 1 - samples/panic/Cargo.toml | 8 +- samples/panic/src/main.rs | 9 +- src/fake_os_api.rs | 224 +++++++++++++++++++++++++++++++++ src/lib.rs | 18 ++- 19 files changed, 300 insertions(+), 57 deletions(-) create mode 100644 samples/Cargo.toml delete mode 100644 samples/fault/.gitignore delete mode 100644 samples/hello/.gitignore delete mode 100644 samples/input-test/.gitignore delete mode 100644 samples/panic/.gitignore create mode 100644 src/fake_os_api.rs diff --git a/Cargo.toml b/Cargo.toml index 38339af..7ee0af7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,10 +7,15 @@ version = "0.1.0" authors = ["Jonathan 'theJPster' Pallant "] [dependencies] -bitflags = "2" neotron-ffi = "0.1" neotron-api = "0.1" +[target.'cfg(unix)'.dependencies] +crossterm = "0.26" + +[target.'cfg(windows)'.dependencies] +crossterm = "0.26" + [features] # Prints panic info. Costs you about 14K of code. fancy-panic = [] diff --git a/samples/.cargo/config.toml b/samples/.cargo/config.toml index 5dd2536..55e0e6c 100644 --- a/samples/.cargo/config.toml +++ b/samples/.cargo/config.toml @@ -1,11 +1,11 @@ [target.thumbv7em-none-eabihf] -rustflags = ["-C", "link-arg=-Tneotron-cortex-m.ld"] # , "-C", "link-arg=--nmagic"] +rustflags = ["-C", "link-arg=-Tneotron-cortex-m.ld", "-C", "link-arg=--nmagic"] [target.thumbv7em-none-eabi] -rustflags = ["-C", "link-arg=-Tneotron-cortex-m.ld"] # , "-C", "link-arg=--nmagic"] +rustflags = ["-C", "link-arg=-Tneotron-cortex-m.ld", "-C", "link-arg=--nmagic"] [target.thumbv7m-none-eabi] -rustflags = ["-C", "link-arg=-Tneotron-cortex-m.ld"] # , "-C", "link-arg=--nmagic"] +rustflags = ["-C", "link-arg=-Tneotron-cortex-m.ld", "-C", "link-arg=--nmagic"] [target.thumbv6m-none-eabi] -rustflags = ["-C", "link-arg=-Tneotron-cortex-m.ld"] # , "-C", "link-arg=--nmagic"] +rustflags = ["-C", "link-arg=-Tneotron-cortex-m.ld", "-C", "link-arg=--nmagic"] diff --git a/samples/.gitignore b/samples/.gitignore index da6d86c..62f6a6c 100644 --- a/samples/.gitignore +++ b/samples/.gitignore @@ -1,3 +1,2 @@ -release/ - - +target +release diff --git a/samples/Cargo.toml b/samples/Cargo.toml new file mode 100644 index 0000000..3c0d82b --- /dev/null +++ b/samples/Cargo.toml @@ -0,0 +1,11 @@ +[workspace] +members = [ + "fault", + "hello", + "input-test", + "panic", +] + +[profile.release] +opt-level = "z" +lto = "fat" diff --git a/samples/build.sh b/samples/build.sh index e424a76..6bcaec6 100755 --- a/samples/build.sh +++ b/samples/build.sh @@ -8,7 +8,7 @@ # # ```console # $ ./build.sh thumbv7em-none-eabi -# $ ls *.elf +# $ ls ./release/*.elf # ``` # @@ -18,8 +18,12 @@ TARGET=${1:-thumbv6m-none-eabi} mkdir -p ./release +echo "Building for host" +cargo build + echo "Building for ${TARGET}" -for program in panic hello fault input-test; do - ( cd ${program} && cargo build --target=${TARGET} --release ) - cp ./${program}/target/${TARGET}/release/${program} ./release/${program}.elf +cargo build --target ${TARGET} --release + +for program in panic hello fault input-test snake; do + cp ./target/${TARGET}/release/${program} ./release/${program}.elf done diff --git a/samples/fault/.gitignore b/samples/fault/.gitignore deleted file mode 100644 index eb5a316..0000000 --- a/samples/fault/.gitignore +++ /dev/null @@ -1 +0,0 @@ -target diff --git a/samples/fault/Cargo.toml b/samples/fault/Cargo.toml index 9789263..491502b 100644 --- a/samples/fault/Cargo.toml +++ b/samples/fault/Cargo.toml @@ -9,10 +9,4 @@ description = "Hello World for Neotron systems" [dependencies] neotron-sdk = { path = "../.." } -[profile.dev] -panic = "abort" - -[profile.release] -panic = "unwind" -opt-level = "z" -lto = "fat" +# See workspace for profile settings diff --git a/samples/fault/src/main.rs b/samples/fault/src/main.rs index 66d2c3d..e2772d6 100644 --- a/samples/fault/src/main.rs +++ b/samples/fault/src/main.rs @@ -1,5 +1,10 @@ -#![no_std] -#![no_main] +#![cfg_attr(target_os = "none", no_std)] +#![cfg_attr(target_os = "none", no_main)] + +#[cfg(not(target_os = "none"))] +fn main() { + neotron_sdk::init(); +} use core::fmt::Write; diff --git a/samples/hello/.gitignore b/samples/hello/.gitignore deleted file mode 100644 index eb5a316..0000000 --- a/samples/hello/.gitignore +++ /dev/null @@ -1 +0,0 @@ -target diff --git a/samples/hello/Cargo.toml b/samples/hello/Cargo.toml index f42b6ba..5a2a25c 100644 --- a/samples/hello/Cargo.toml +++ b/samples/hello/Cargo.toml @@ -9,10 +9,4 @@ description = "Hello World for Neotron systems" [dependencies] neotron-sdk = { path = "../.." } -[profile.dev] -panic = "abort" - -[profile.release] -panic = "unwind" -opt-level = "z" -lto = "fat" +# See workspace for profile settings diff --git a/samples/hello/src/main.rs b/samples/hello/src/main.rs index 36add0a..8a8e2c9 100644 --- a/samples/hello/src/main.rs +++ b/samples/hello/src/main.rs @@ -1,7 +1,10 @@ -#![no_std] -#![no_main] +#![cfg_attr(target_os = "none", no_std)] +#![cfg_attr(target_os = "none", no_main)] -extern crate neotron_sdk; +#[cfg(not(target_os = "none"))] +fn main() { + neotron_sdk::init(); +} #[no_mangle] extern "C" fn neotron_main() -> i32 { diff --git a/samples/input-test/.gitignore b/samples/input-test/.gitignore deleted file mode 100644 index eb5a316..0000000 --- a/samples/input-test/.gitignore +++ /dev/null @@ -1 +0,0 @@ -target diff --git a/samples/input-test/Cargo.toml b/samples/input-test/Cargo.toml index e75e1eb..b66acfb 100644 --- a/samples/input-test/Cargo.toml +++ b/samples/input-test/Cargo.toml @@ -9,10 +9,4 @@ description = "Hello World for Neotron systems" [dependencies] neotron-sdk = { path = "../.." } -[profile.dev] -panic = "abort" - -[profile.release] -panic = "unwind" -opt-level = "z" -lto = "fat" +# See workspace for profile settings diff --git a/samples/input-test/src/main.rs b/samples/input-test/src/main.rs index 9dc9891..89db53d 100644 --- a/samples/input-test/src/main.rs +++ b/samples/input-test/src/main.rs @@ -1,7 +1,10 @@ -#![no_std] -#![no_main] +#![cfg_attr(target_os = "none", no_std)] +#![cfg_attr(target_os = "none", no_main)] -extern crate neotron_sdk; +#[cfg(not(target_os = "none"))] +fn main() { + neotron_sdk::init(); +} use core::fmt::Write; diff --git a/samples/panic/.gitignore b/samples/panic/.gitignore deleted file mode 100644 index eb5a316..0000000 --- a/samples/panic/.gitignore +++ /dev/null @@ -1 +0,0 @@ -target diff --git a/samples/panic/Cargo.toml b/samples/panic/Cargo.toml index c5db1f4..25a1ef0 100644 --- a/samples/panic/Cargo.toml +++ b/samples/panic/Cargo.toml @@ -9,10 +9,4 @@ description = "Hello World for Neotron systems" [dependencies] neotron-sdk = { path = "../..", features = ["fancy-panic"] } -[profile.dev] -panic = "abort" - -[profile.release] -panic = "unwind" -opt-level = "z" -lto = "fat" +# See workspace for profile settings diff --git a/samples/panic/src/main.rs b/samples/panic/src/main.rs index 885e9bc..c73203a 100644 --- a/samples/panic/src/main.rs +++ b/samples/panic/src/main.rs @@ -1,5 +1,10 @@ -#![no_std] -#![no_main] +#![cfg_attr(target_os = "none", no_std)] +#![cfg_attr(target_os = "none", no_main)] + +#[cfg(not(target_os = "none"))] +fn main() { + neotron_sdk::init(); +} use core::fmt::Write; diff --git a/src/fake_os_api.rs b/src/fake_os_api.rs new file mode 100644 index 0000000..3397e4e --- /dev/null +++ b/src/fake_os_api.rs @@ -0,0 +1,224 @@ +//! Fake API implementation +//! +//! Allows Neotron SDK applications to run using libstd instead of Neotron OS + +use std::io::Write; + +static FAKE_API: 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, +}; + +/// Get an Api pointer that uses libstd. +pub fn get_ptr() -> *const neotron_api::Api { + &FAKE_API as *const neotron_api::Api +} + +/// 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() { + let mut stdout = std::io::stdout(); + let buffer = buffer.as_slice(); + for chunk in buffer.split_inclusive(|b| *b == b'\n') { + if chunk.last() == Some(&b'\n') { + // raw terminal needs CR and LF + stdout.write_all(&chunk[0..chunk.len() - 1]).unwrap(); + stdout.write_all(b"\r\n").unwrap(); + } else { + stdout.write_all(chunk).unwrap(); + } + } + 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, + mut buffer: neotron_api::FfiBuffer, +) -> neotron_api::Result { + if fd == neotron_api::file::Handle::new_stdin() { + use std::io::Read; + let mut stdin = std::io::stdin(); + let Some(mut buffer_slice) = buffer.as_mut_slice() else { + return neotron_api::Result::Err(neotron_api::Error::InvalidArg); + }; + let count = stdin.read(&mut buffer_slice).expect("stdin read"); + neotron_api::Result::Ok(count) + } else { + neotron_api::Result::Err(neotron_api::Error::BadHandle) + } +} + +/// 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) {} diff --git a/src/lib.rs b/src/lib.rs index 555c621..d0f5ab6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,7 @@ //! //! Defines the API supplied to applications that run on Neotron OS -#![no_std] +#![cfg_attr(target_os = "none", no_std)] // ============================================================================ // Imports @@ -16,6 +16,9 @@ pub use neotron_api::{path, Api, Error}; use neotron_api as api; +#[cfg(not(target_os = "none"))] +mod fake_os_api; + // ============================================================================ // Constants // ============================================================================ @@ -319,7 +322,16 @@ fn get_api() -> &'static Api { unsafe { ptr.as_ref().unwrap() } } -#[cfg(all(feature = "fancy-panic", not(test)))] +#[cfg(not(target_os = "none"))] +pub fn init() { + API.store(fake_os_api::get_ptr() as *mut Api, Ordering::Relaxed); + crossterm::terminal::enable_raw_mode().expect("enable raw mode"); + let res = unsafe { neotron_main() }; + crossterm::terminal::disable_raw_mode().expect("disable raw mode"); + std::process::exit(res); +} + +#[cfg(all(target_os = "none", feature = "fancy-panic"))] #[inline(never)] #[panic_handler] fn panic(info: &core::panic::PanicInfo) -> ! { @@ -329,7 +341,7 @@ fn panic(info: &core::panic::PanicInfo) -> ! { loop {} } -#[cfg(all(not(feature = "fancy-panic"), not(test)))] +#[cfg(all(target_os = "none", not(feature = "fancy-panic")))] #[inline(never)] #[panic_handler] fn panic(_info: &core::panic::PanicInfo) -> ! { From 084cea38b5a1ba6f96837b72600bdf6e0834db8d Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Fri, 28 Jul 2023 21:08:34 +0100 Subject: [PATCH 20/37] Add snake program skelton. --- .vscode/settings.json | 1 + samples/Cargo.toml | 1 + samples/snake/Cargo.toml | 12 ++++++++++++ samples/snake/build.rs | 21 +++++++++++++++++++++ samples/snake/src/main.rs | 17 +++++++++++++++++ 5 files changed, 52 insertions(+) create mode 100644 samples/snake/Cargo.toml create mode 100644 samples/snake/build.rs create mode 100644 samples/snake/src/main.rs diff --git a/.vscode/settings.json b/.vscode/settings.json index c05c971..39aa490 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,6 +4,7 @@ "./samples/panic/Cargo.toml", "./samples/hello/Cargo.toml", "./samples/fault/Cargo.toml", + "./samples/snake/Cargo.toml", "./Cargo.toml" ] } \ No newline at end of file diff --git a/samples/Cargo.toml b/samples/Cargo.toml index 3c0d82b..ecc88d6 100644 --- a/samples/Cargo.toml +++ b/samples/Cargo.toml @@ -3,6 +3,7 @@ members = [ "fault", "hello", "input-test", + "snake", "panic", ] diff --git a/samples/snake/Cargo.toml b/samples/snake/Cargo.toml new file mode 100644 index 0000000..ff37eb3 --- /dev/null +++ b/samples/snake/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "snake" +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" +authors = ["Jonathan 'theJPster' Pallant "] +description = "ANSI Snake for Neotron systems" + +[dependencies] +neotron-sdk = { path = "../.." } + +# See workspace for profile settings diff --git a/samples/snake/build.rs b/samples/snake/build.rs new file mode 100644 index 0000000..759eb7d --- /dev/null +++ b/samples/snake/build.rs @@ -0,0 +1,21 @@ +use std::env; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; + +fn main() { + // Put `neotron-cortex-m.ld` in our output directory and ensure it's + // on the linker search path. + let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); + File::create(out.join("neotron-cortex-m.ld")) + .unwrap() + .write_all(include_bytes!("../neotron-cortex-m.ld")) + .unwrap(); + println!("cargo:rustc-link-search={}", out.display()); + + // By default, Cargo will re-run a build script whenever + // any file in the project changes. By specifying `neotron-cortex-m.ld` + // here, we ensure the build script is only re-run when + // `neotron-cortex-m.ld` is changed. + println!("cargo:rerun-if-changed=../neotron-cortex-m.ld"); +} diff --git a/samples/snake/src/main.rs b/samples/snake/src/main.rs new file mode 100644 index 0000000..8a8e2c9 --- /dev/null +++ b/samples/snake/src/main.rs @@ -0,0 +1,17 @@ +#![cfg_attr(target_os = "none", no_std)] +#![cfg_attr(target_os = "none", no_main)] + +#[cfg(not(target_os = "none"))] +fn main() { + neotron_sdk::init(); +} + +#[no_mangle] +extern "C" fn neotron_main() -> i32 { + let stdout = neotron_sdk::stdout(); + if stdout.write(b"Hello, world\n").is_ok() { + 0 + } else { + 1 + } +} From 9ed1d4b9c2f1b56e102f4e961aa227e59a938119 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sat, 29 Jul 2023 00:00:45 +0100 Subject: [PATCH 21/37] Snake mostly works. --- samples/snake/src/lib.rs | 430 ++++++++++++++++++++++++++++++++++++++ samples/snake/src/main.rs | 12 +- src/fake_os_api.rs | 34 ++- src/lib.rs | 19 +- 4 files changed, 481 insertions(+), 14 deletions(-) create mode 100644 samples/snake/src/lib.rs diff --git a/samples/snake/src/lib.rs b/samples/snake/src/lib.rs new file mode 100644 index 0000000..2fbadee --- /dev/null +++ b/samples/snake/src/lib.rs @@ -0,0 +1,430 @@ +//! Game logic for Snake + +#![no_std] + +use core::fmt::Write; + +#[derive(Debug)] +pub enum Error { + ScreenTooTall, + ScreenTooWide, +} + +pub struct App { + game: Game, + width: usize, + height: usize, + stdout: neotron_sdk::File, + stdin: neotron_sdk::File, +} + +impl App { + pub const fn new(width: usize, height: usize) -> App { + App { + game: Game::new(width - 2, height - 2, 1, 1), + width, + height, + stdout: neotron_sdk::stdout(), + stdin: neotron_sdk::stdin(), + } + } + + pub fn play(&mut self) -> Result<(), Error> { + // hide cursor + let _ = writeln!(self.stdout, "\u{001b}[?25l"); + self.clear_screen(); + self.title_screen(); + + let mut seed: u16 = 0x4f35; + + 'outer: loop { + 'inner: loop { + let key = self.wait_for_key(); + seed = seed.wrapping_add(1); + if key == b'q' || key == b'Q' { + break 'outer; + } + if key == b'p' || key == b'P' { + break 'inner; + } + } + + self.clear_screen(); + + let score = self.game.play(seed, &mut self.stdin, &mut self.stdout); + + self.winning_message(score); + } + + // show cursor + let _ = writeln!(self.stdout, "\u{001b}[?25h"); + self.clear_screen(); + Ok(()) + } + + fn clear_screen(&mut self) { + let _ = self.stdout.write_str("\u{001b}[2J"); + for x in 1..=self.width { + let _ = write!(self.stdout, "\u{001b}[{};{}H{}", 1, x, '-'); + let _ = write!(self.stdout, "\u{001b}[{};{}H{}", self.height, x, '-'); + } + for y in 1..=self.height { + let _ = write!(self.stdout, "\u{001b}[{};{}H{}", y, 1, '|'); + let _ = write!(self.stdout, "\u{001b}[{};{}H{}", y, self.width, '|'); + } + } + + fn title_screen(&mut self) { + let message = "ANSI Snake"; + let centre_x = (self.width - message.chars().count()) / 2; + let centre_y = self.height / 2; + let _ = writeln!( + self.stdout, + "\u{001b}[{};{}H{}", + centre_y, centre_x, message + ); + let message = "Q to Quit | 'P' to Play"; + let centre_x = (self.width - message.chars().count()) / 2; + let centre_y = (self.height / 2) + 1; + let _ = writeln!( + self.stdout, + "\u{001b}[{};{}H{}", + centre_y, centre_x, message + ); + } + + fn wait_for_key(&mut self) -> u8 { + loop { + let mut buffer = [0u8; 1]; + if let Ok(1) = self.stdin.read(&mut buffer) { + return buffer[0]; + } + neotron_sdk::delay(core::time::Duration::from_millis(10)); + } + } + + fn winning_message(&mut self, score: u32) { + let centre_x = (self.width - 13) / 2; + let mut centre_y = self.height / 2; + let _ = writeln!( + self.stdout, + "\u{001b}[{};{}HScore: {:06}", + centre_y, centre_x, score + ); + let message = "Q to Quit | 'P' to Play"; + let centre_x = (self.width - message.chars().count()) / 2; + centre_y += 1; + let _ = writeln!( + self.stdout, + "\u{001b}[{};{}H{}", + centre_y, centre_x, message + ); + } +} + +pub struct Game { + board: Board<{ Self::MAX_WIDTH }, { Self::MAX_HEIGHT }>, + width: usize, + height: usize, + offset_x: usize, + offset_y: usize, + head_x: usize, + head_y: usize, + tail_x: usize, + tail_y: usize, + direction: Direction, + seed: u16, + score: u32, + digesting: u32, +} + +impl Game { + pub const MAX_WIDTH: usize = 80; + pub const MAX_HEIGHT: usize = 25; + + const fn new(width: usize, height: usize, offset_x: usize, offset_y: usize) -> Game { + Game { + board: Board::new(), + width, + height, + offset_x, + offset_y, + head_x: 0, + head_y: 0, + tail_x: 0, + tail_y: 0, + direction: Direction::Up, + seed: 0, + score: 0, + digesting: 0, + } + } + + fn random(&mut self) -> u16 { + let lfsr = self.seed; + let bit = ((lfsr >> 0) ^ (lfsr >> 2) ^ (lfsr >> 3) ^ (lfsr >> 5)) & 0x01; + self.seed = (lfsr >> 1) | (bit << 15); + self.seed + } + + fn play( + &mut self, + seed: u16, + stdin: &mut neotron_sdk::File, + stdout: &mut neotron_sdk::File, + ) -> u32 { + // Reset score + self.score = 0; + // Init random numbers + self.seed = seed; + for _ in 0..100 { + let _ = self.random(); + } + // Wipe board + self.board.reset(); + // Add offset snake + self.head_x = self.width / 4; + self.head_y = self.height / 4; + self.tail_x = self.head_x; + self.tail_y = self.head_y; + self.board.set_dir(self.head_x, self.head_y, self.direction); + self.write_at(stdout, self.head_x, self.head_y, 'U'); + // Add random food + let (x, y) = self.random_empty_position(); + self.board.set_food(x, y); + self.write_at(stdout, x, y, 'F'); + + 'game: loop { + // Wait for frame tick + neotron_sdk::delay(core::time::Duration::from_millis(100)); + + // 1 point for not being dead + self.score += 1; + + // Read input + 'input: loop { + let mut buffer = [0u8; 1]; + if let Some(1) = stdin.read(&mut buffer).ok() { + match buffer[0] { + b'w' | b'W' => { + // Going up + if self.direction.is_horizontal() { + self.direction = Direction::Up; + } + } + b's' | b'S' => { + // Going down + if self.direction.is_horizontal() { + self.direction = Direction::Down; + } + } + b'a' | b'A' => { + // Going left + if self.direction.is_vertical() { + self.direction = Direction::Left; + } + } + b'd' | b'D' => { + // Going right + if self.direction.is_vertical() { + self.direction = Direction::Right; + } + } + b'q' | b'Q' => { + // Quit game + break 'game; + } + _ => { + // ignore + } + } + } else { + break 'input; + } + } + + // Mark which way we're going in the old head position + self.board.set_dir(self.head_x, self.head_y, self.direction); + + // Update head position + match self.direction { + Direction::Up => { + if self.head_y == 0 { + break 'game; + } + self.head_y -= 1; + } + Direction::Down => { + if self.head_y == self.height - 1 { + break 'game; + } + self.head_y += 1; + } + Direction::Left => { + if self.head_x == 0 { + break 'game; + } + self.head_x -= 1; + } + Direction::Right => { + if self.head_x == self.width - 1 { + break 'game; + } + self.head_x += 1; + } + } + + // Check what we just ate + // - Food => get longer + // - Ourselves => die + if self.board.is_food(self.head_x, self.head_y) { + // yum + self.score += 10; + self.digesting = 2; + // Add random food + let (x, y) = self.random_empty_position(); + self.board.set_food(x, y); + self.write_at(stdout, x, y, 'F'); + } else if self.board.is_body(self.head_x, self.head_y) { + // oh no + break 'game; + } + + // Write the new head + self.board.set_dir(self.head_x, self.head_y, self.direction); + self.write_at(stdout, self.head_x, self.head_y, 'U'); + + if self.digesting == 0 { + let (old_tail_x, old_tail_y) = (self.tail_x, self.tail_y); + match self.board.get_tail_dir(self.tail_x, self.tail_y) { + Some(Direction::Up) => { + self.tail_y -= 1; + } + Some(Direction::Down) => { + self.tail_y += 1; + } + Some(Direction::Left) => { + self.tail_x -= 1; + } + Some(Direction::Right) => { + self.tail_x += 1; + } + None => { + panic!("Bad game state"); + } + } + self.board.clear(old_tail_x, old_tail_y); + self.write_at(stdout, old_tail_x, old_tail_y, ' '); + } else { + self.digesting -= 1; + } + } + + self.score + } + + fn write_at(&self, console: &mut neotron_sdk::File, x: usize, y: usize, ch: char) { + let _ = write!( + console, + "\u{001b}[{};{}H{}", + self.offset_y + y + 1, + self.offset_x + x + 1, + ch + ); + } + + fn random_empty_position(&mut self) -> (usize, usize) { + loop { + // This isn't equally distributed. I don't really care. + let x = (self.random() as usize) % self.width; + let y = (self.random() as usize) % self.height; + if self.board.is_empty(x, y) { + return (x, y); + } + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +enum Direction { + Up, + Down, + Left, + Right, +} + +impl Direction { + fn is_horizontal(self) -> bool { + self == Direction::Left || self == Direction::Right + } + + fn is_vertical(self) -> bool { + self == Direction::Up || self == Direction::Down + } +} + +struct Board { + cells: [[u8; WIDTH]; HEIGHT], +} + +impl Board { + const UP: u8 = b'U'; + const DOWN: u8 = b'D'; + const LEFT: u8 = b'L'; + const RIGHT: u8 = b'R'; + const FOOD: u8 = b'F'; + + const fn new() -> Board { + Board { + cells: [[0; WIDTH]; HEIGHT], + } + } + + fn reset(&mut self) { + for y in 0..HEIGHT { + for x in 0..WIDTH { + self.cells[y][x] = 0; + } + } + } + + fn set_dir(&mut self, x: usize, y: usize, direction: Direction) { + self.cells[y][x] = match direction { + Direction::Up => Self::UP, + Direction::Down => Self::DOWN, + Direction::Left => Self::LEFT, + Direction::Right => Self::RIGHT, + } + } + + fn get_tail_dir(&self, x: usize, y: usize) -> Option { + match self.cells[y][x] { + Self::UP => Some(Direction::Up), + Self::DOWN => Some(Direction::Down), + Self::LEFT => Some(Direction::Left), + Self::RIGHT => Some(Direction::Right), + _ => None, + } + } + + fn set_food(&mut self, x: usize, y: usize) { + self.cells[y][x] = Self::FOOD; + } + + fn is_food(&mut self, x: usize, y: usize) -> bool { + self.cells[y][x] == Self::FOOD + } + + fn is_body(&mut self, x: usize, y: usize) -> bool { + let cell = self.cells[y][x]; + cell == Self::UP || cell == Self::DOWN || cell == Self::LEFT || cell == Self::RIGHT + } + + fn is_empty(&mut self, x: usize, y: usize) -> bool { + self.cells[y][x] == 0 + } + + fn clear(&mut self, x: usize, y: usize) { + self.cells[y][x] = 0; + } +} diff --git a/samples/snake/src/main.rs b/samples/snake/src/main.rs index 8a8e2c9..27a16a5 100644 --- a/samples/snake/src/main.rs +++ b/samples/snake/src/main.rs @@ -6,12 +6,16 @@ fn main() { neotron_sdk::init(); } +static mut APP: snake::App = snake::App::new(80, 25); + #[no_mangle] extern "C" fn neotron_main() -> i32 { - let stdout = neotron_sdk::stdout(); - if stdout.write(b"Hello, world\n").is_ok() { - 0 - } else { + if let Err(e) = unsafe { APP.play() } { + let mut stdout = neotron_sdk::stdout(); + use core::fmt::Write; + let _ = writeln!(stdout, "Error: {:?}", e); 1 + } else { + 0 } } diff --git a/src/fake_os_api.rs b/src/fake_os_api.rs index 3397e4e..8a9f2e7 100644 --- a/src/fake_os_api.rs +++ b/src/fake_os_api.rs @@ -3,6 +3,12 @@ //! Allows Neotron SDK applications to run using libstd instead of Neotron OS use std::io::Write; +use std::sync::{ + mpsc::{channel, Receiver}, + Mutex, +}; + +static STDIN_RX: Mutex>> = Mutex::new(None); static FAKE_API: neotron_api::Api = neotron_api::Api { open: api_open, @@ -30,6 +36,20 @@ static FAKE_API: neotron_api::Api = neotron_api::Api { /// Get an Api pointer that uses libstd. pub fn get_ptr() -> *const neotron_api::Api { + let (sender, receiver) = channel(); + *STDIN_RX.lock().unwrap() = Some(receiver); + + std::thread::spawn(move || { + use std::io::prelude::*; + let mut stdin = std::io::stdin(); + loop { + let mut buffer = [0u8; 1]; + if let Ok(1) = stdin.read(&mut buffer) { + sender.send(buffer[0]).unwrap(); + } + } + }); + &FAKE_API as *const neotron_api::Api } @@ -70,6 +90,7 @@ extern "C" fn api_write( stdout.write_all(chunk).unwrap(); } } + stdout.flush().unwrap(); neotron_api::Result::Ok(()) } else { neotron_api::Result::Err(neotron_api::Error::BadHandle) @@ -84,13 +105,12 @@ extern "C" fn api_read( mut buffer: neotron_api::FfiBuffer, ) -> neotron_api::Result { if fd == neotron_api::file::Handle::new_stdin() { - use std::io::Read; - let mut stdin = std::io::stdin(); - let Some(mut buffer_slice) = buffer.as_mut_slice() else { - return neotron_api::Result::Err(neotron_api::Error::InvalidArg); - }; - let count = stdin.read(&mut buffer_slice).expect("stdin read"); - neotron_api::Result::Ok(count) + if let Some(b) = STDIN_RX.lock().unwrap().as_mut().unwrap().try_recv().ok() { + buffer.as_mut_slice().unwrap()[0] = b; + neotron_api::Result::Ok(1) + } else { + neotron_api::Result::Ok(0) + } } else { neotron_api::Result::Err(neotron_api::Error::BadHandle) } diff --git a/src/lib.rs b/src/lib.rs index d0f5ab6..f72a819 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -300,20 +300,33 @@ pub fn free(_ptr: *mut core::ffi::c_void, _size: usize, _alignment: usize) { } /// Get a handle for Standard Input -pub fn stdin() -> File { +pub const fn stdin() -> File { File(api::file::Handle::new_stdin()) } /// Get a handle for Standard Output -pub fn stdout() -> File { +pub const fn stdout() -> File { File(api::file::Handle::new_stdout()) } /// Get a handle for Standard Error -pub fn stderr() -> File { +pub const fn stderr() -> File { File(api::file::Handle::new_stderr()) } +/// Delay for some milliseconds +pub fn delay(period: core::time::Duration) { + #[cfg(not(target_os = "none"))] + std::thread::sleep(period); + + // TODO: sleep on real hardware? + for _ in 0..period.as_micros() { + for _ in 0..50 { + unsafe { core::arch::asm!("nop") } + } + } +} + /// Get the API structure so we can call APIs manually. /// /// If you managed to not have `app_entry` called on start-up, this will panic. From 3fb5a8229e0da3f4b51c6965f24b2a196133df85 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sat, 29 Jul 2023 19:02:35 +0100 Subject: [PATCH 22/37] Add ANSI helpers and rand/srand. --- src/console.rs | 104 +++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 28 +++++++++++-- 2 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 src/console.rs diff --git a/src/console.rs b/src/console.rs new file mode 100644 index 0000000..fda1a81 --- /dev/null +++ b/src/console.rs @@ -0,0 +1,104 @@ +//! Helper functions for sending ANSI sequences + +// ============================================================================ +// Imports +// ============================================================================ + +use core::fmt::Write; + +use crate::File; + +// ============================================================================ +// Types +// ============================================================================ + +/// Represents a position on a screen. +/// +/// A position is 0-indexed. That is (0, 0) is the top-left corner. +/// +/// Translation to 1-based is performed before sending the ANSI sequences. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct Position { + pub row: u8, + pub col: u8, +} + +impl Position { + pub const fn origin() -> Position { + Position { row: 0, col: 0 } + } +} + +/// Represents a Select Graphic Rendition parameter you can send in an SGR ANSI +/// sequence. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[repr(u8)] +pub enum SgrParam { + Reset = 0, + Bold = 1, + Reverse = 7, + NotBold = 22, + NotReverse = 27, + FgBlack = 30, + FgRed = 31, + FgGreen = 32, + FgYellow = 33, + FgBlue = 34, + FgMagenta = 35, + FgCyan = 36, + FgWhite = 37, + BgBlack = 40, + BgRed = 41, + BgGreen = 42, + BgYellow = 43, + BgBlue = 44, + BgMagenta = 45, + BgCyan = 46, + BgWhite = 47, +} + +// ============================================================================ +// Functions +// ============================================================================ + +/// Erase the screen +pub fn clear_screen(f: &mut File) { + let _ = f.write_str("\u{001b}[2J"); +} + +/// Turn the cursor on +pub fn cursor_on(f: &mut File) { + let _ = f.write_str("\u{001b}[?25h"); +} + +/// Turn the cursor off +pub fn cursor_off(f: &mut File) { + let _ = f.write_str("\u{001b}[?25l"); +} + +/// Move the cursor to the given position +pub fn move_cursor(f: &mut File, pos: Position) { + let _ = write!(f, "\u{001b}[{};{}H", 1 + pos.row, 1 + pos.col); +} + +/// Change the background +/// +/// Only values 0..8 will work. +pub fn set_sgr(f: &mut File, values: T) +where + T: IntoIterator, +{ + let _ = write!(f, "\u{001b}["); + let mut iter = values.into_iter(); + if let Some(value) = iter.next() { + let _ = write!(f, "{}", value as u8); + } + while let Some(value) = iter.next() { + let _ = write!(f, ";{}", value as u8); + } + let _ = write!(f, "m"); +} + +// ============================================================================ +// End of File +// ============================================================================ diff --git a/src/lib.rs b/src/lib.rs index f72a819..88b33d1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,6 +16,8 @@ pub use neotron_api::{path, Api, Error}; use neotron_api as api; +pub mod console; + #[cfg(not(target_os = "none"))] mod fake_os_api; @@ -315,10 +317,8 @@ pub const fn stderr() -> File { } /// Delay for some milliseconds +#[cfg(target_os = "none")] pub fn delay(period: core::time::Duration) { - #[cfg(not(target_os = "none"))] - std::thread::sleep(period); - // TODO: sleep on real hardware? for _ in 0..period.as_micros() { for _ in 0..50 { @@ -327,6 +327,28 @@ pub fn delay(period: core::time::Duration) { } } +/// Delay for some milliseconds +#[cfg(not(target_os = "none"))] +pub fn delay(period: core::time::Duration) { + std::thread::sleep(period); +} + +static RAND_STATE: core::sync::atomic::AtomicU16 = core::sync::atomic::AtomicU16::new(0); + +/// Seed the 16-bit psuedorandom number generator +pub fn srand(seed: u16) { + RAND_STATE.store(seed, core::sync::atomic::Ordering::Relaxed); +} + +/// Get a 16-bit psuedorandom number +pub fn rand() -> u16 { + let mut state = RAND_STATE.load(core::sync::atomic::Ordering::Relaxed); + let bit = ((state >> 0) ^ (state >> 2) ^ (state >> 3) ^ (state >> 5)) & 0x01; + state = (state >> 1) | (bit << 15); + RAND_STATE.store(state, core::sync::atomic::Ordering::Relaxed); + state +} + /// Get the API structure so we can call APIs manually. /// /// If you managed to not have `app_entry` called on start-up, this will panic. From 3c418eff83832399203068cc1d178d448a60cd8d Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sat, 29 Jul 2023 19:03:08 +0100 Subject: [PATCH 23/37] Use new ANSI helpers in snake. --- samples/snake/src/lib.rs | 331 ++++++++++++++++++++------------------- 1 file changed, 174 insertions(+), 157 deletions(-) diff --git a/samples/snake/src/lib.rs b/samples/snake/src/lib.rs index 2fbadee..367ccf3 100644 --- a/samples/snake/src/lib.rs +++ b/samples/snake/src/lib.rs @@ -4,6 +4,8 @@ use core::fmt::Write; +use neotron_sdk::console; + #[derive(Debug)] pub enum Error { ScreenTooTall, @@ -12,16 +14,16 @@ pub enum Error { pub struct App { game: Game, - width: usize, - height: usize, + width: u8, + height: u8, stdout: neotron_sdk::File, stdin: neotron_sdk::File, } impl App { - pub const fn new(width: usize, height: usize) -> App { + pub const fn new(width: u8, height: u8) -> App { App { - game: Game::new(width - 2, height - 2, 1, 1), + game: Game::new(width - 2, height - 2, console::Position { row: 1, col: 1 }), width, height, stdout: neotron_sdk::stdout(), @@ -30,8 +32,7 @@ impl App { } pub fn play(&mut self) -> Result<(), Error> { - // hide cursor - let _ = writeln!(self.stdout, "\u{001b}[?25l"); + console::cursor_off(&mut self.stdout); self.clear_screen(); self.title_screen(); @@ -51,46 +52,68 @@ impl App { self.clear_screen(); - let score = self.game.play(seed, &mut self.stdin, &mut self.stdout); + neotron_sdk::srand(seed); + + let score = self.game.play(&mut self.stdin, &mut self.stdout); self.winning_message(score); } // show cursor - let _ = writeln!(self.stdout, "\u{001b}[?25h"); + console::cursor_on(&mut self.stdout); self.clear_screen(); Ok(()) } fn clear_screen(&mut self) { - let _ = self.stdout.write_str("\u{001b}[2J"); - for x in 1..=self.width { - let _ = write!(self.stdout, "\u{001b}[{};{}H{}", 1, x, '-'); - let _ = write!(self.stdout, "\u{001b}[{};{}H{}", self.height, x, '-'); + console::clear_screen(&mut self.stdout); + console::move_cursor(&mut self.stdout, console::Position::origin()); + let _ = self.stdout.write_char('+'); + for _ in 1..self.width - 1 { + let _ = self.stdout.write_char('-'); } - for y in 1..=self.height { - let _ = write!(self.stdout, "\u{001b}[{};{}H{}", y, 1, '|'); - let _ = write!(self.stdout, "\u{001b}[{};{}H{}", y, self.width, '|'); + let _ = self.stdout.write_char('+'); + console::move_cursor( + &mut self.stdout, + console::Position { + row: self.height - 1, + col: 0, + }, + ); + let _ = self.stdout.write_char('+'); + for _ in 1..self.width - 1 { + let _ = self.stdout.write_char('-'); + } + let _ = self.stdout.write_char('+'); + for row in 1..self.height - 1 { + console::move_cursor(&mut self.stdout, console::Position { row, col: 0 }); + let _ = self.stdout.write_char('|'); + console::move_cursor( + &mut self.stdout, + console::Position { + row, + col: self.width - 1, + }, + ); + let _ = self.stdout.write_char('|'); } } fn title_screen(&mut self) { let message = "ANSI Snake"; - let centre_x = (self.width - message.chars().count()) / 2; - let centre_y = self.height / 2; - let _ = writeln!( - self.stdout, - "\u{001b}[{};{}H{}", - centre_y, centre_x, message - ); + let pos = console::Position { + row: self.height / 2, + col: (self.width - message.chars().count() as u8) / 2, + }; + console::move_cursor(&mut self.stdout, pos); + let _ = self.stdout.write_str(message); let message = "Q to Quit | 'P' to Play"; - let centre_x = (self.width - message.chars().count()) / 2; - let centre_y = (self.height / 2) + 1; - let _ = writeln!( - self.stdout, - "\u{001b}[{};{}H{}", - centre_y, centre_x, message - ); + let pos = console::Position { + row: pos.row + 1, + col: (self.width - message.chars().count() as u8) / 2, + }; + console::move_cursor(&mut self.stdout, pos); + let _ = self.stdout.write_str(message); } fn wait_for_key(&mut self) -> u8 { @@ -104,99 +127,77 @@ impl App { } fn winning_message(&mut self, score: u32) { - let centre_x = (self.width - 13) / 2; - let mut centre_y = self.height / 2; - let _ = writeln!( - self.stdout, - "\u{001b}[{};{}HScore: {:06}", - centre_y, centre_x, score - ); + let pos = console::Position { + row: self.height / 2, + col: (self.width - 13 as u8) / 2, + }; + console::move_cursor(&mut self.stdout, pos); + let _ = writeln!(self.stdout, "Score: {:06}", score); let message = "Q to Quit | 'P' to Play"; - let centre_x = (self.width - message.chars().count()) / 2; - centre_y += 1; - let _ = writeln!( - self.stdout, - "\u{001b}[{};{}H{}", - centre_y, centre_x, message - ); + let pos = console::Position { + row: pos.row + 1, + col: (self.width - message.chars().count() as u8) / 2, + }; + console::move_cursor(&mut self.stdout, pos); + let _ = self.stdout.write_str(message); } } pub struct Game { board: Board<{ Self::MAX_WIDTH }, { Self::MAX_HEIGHT }>, - width: usize, - height: usize, - offset_x: usize, - offset_y: usize, - head_x: usize, - head_y: usize, - tail_x: usize, - tail_y: usize, + width: u8, + height: u8, + offset: console::Position, + head: console::Position, + tail: console::Position, direction: Direction, - seed: u16, score: u32, digesting: u32, + tick_interval_ms: u16, } impl Game { pub const MAX_WIDTH: usize = 80; pub const MAX_HEIGHT: usize = 25; - const fn new(width: usize, height: usize, offset_x: usize, offset_y: usize) -> Game { + const fn new(width: u8, height: u8, offset: console::Position) -> Game { Game { board: Board::new(), width, height, - offset_x, - offset_y, - head_x: 0, - head_y: 0, - tail_x: 0, - tail_y: 0, + offset, + head: console::Position { row: 0, col: 0 }, + tail: console::Position { row: 0, col: 0 }, direction: Direction::Up, - seed: 0, score: 0, digesting: 0, + tick_interval_ms: 150, } } - fn random(&mut self) -> u16 { - let lfsr = self.seed; - let bit = ((lfsr >> 0) ^ (lfsr >> 2) ^ (lfsr >> 3) ^ (lfsr >> 5)) & 0x01; - self.seed = (lfsr >> 1) | (bit << 15); - self.seed - } - - fn play( - &mut self, - seed: u16, - stdin: &mut neotron_sdk::File, - stdout: &mut neotron_sdk::File, - ) -> u32 { + fn play(&mut self, stdin: &mut neotron_sdk::File, stdout: &mut neotron_sdk::File) -> u32 { // Reset score self.score = 0; - // Init random numbers - self.seed = seed; - for _ in 0..100 { - let _ = self.random(); - } // Wipe board self.board.reset(); // Add offset snake - self.head_x = self.width / 4; - self.head_y = self.height / 4; - self.tail_x = self.head_x; - self.tail_y = self.head_y; - self.board.set_dir(self.head_x, self.head_y, self.direction); - self.write_at(stdout, self.head_x, self.head_y, 'U'); + self.head = console::Position { + row: self.height / 4, + col: self.width / 4, + }; + self.tail = self.head; + self.board.set_dir(self.head, self.direction); + self.write_at(stdout, self.head, 'U'); // Add random food - let (x, y) = self.random_empty_position(); - self.board.set_food(x, y); - self.write_at(stdout, x, y, 'F'); + let pos = self.random_empty_position(); + self.board.set_food(pos); + self.write_at(stdout, pos, 'F'); 'game: loop { // Wait for frame tick - neotron_sdk::delay(core::time::Duration::from_millis(100)); + neotron_sdk::delay(core::time::Duration::from_millis( + self.tick_interval_ms as u64, + )); // 1 point for not being dead self.score += 1; @@ -244,77 +245,84 @@ impl Game { } // Mark which way we're going in the old head position - self.board.set_dir(self.head_x, self.head_y, self.direction); + self.board.set_dir(self.head, self.direction); // Update head position match self.direction { Direction::Up => { - if self.head_y == 0 { + if self.head.row == 0 { break 'game; } - self.head_y -= 1; + self.head.row -= 1; } Direction::Down => { - if self.head_y == self.height - 1 { + if self.head.row == self.height - 1 { break 'game; } - self.head_y += 1; + self.head.row += 1; } Direction::Left => { - if self.head_x == 0 { + if self.head.col == 0 { break 'game; } - self.head_x -= 1; + self.head.col -= 1; } Direction::Right => { - if self.head_x == self.width - 1 { + if self.head.col == self.width - 1 { break 'game; } - self.head_x += 1; + self.head.col += 1; } } // Check what we just ate // - Food => get longer // - Ourselves => die - if self.board.is_food(self.head_x, self.head_y) { + if self.board.is_food(self.head) { // yum self.score += 10; self.digesting = 2; + // Drop 10% on the tick interval + self.tick_interval_ms *= 9; + self.tick_interval_ms /= 10; + if self.tick_interval_ms < 5 { + // Maximum speed + self.tick_interval_ms = 5; + } // Add random food - let (x, y) = self.random_empty_position(); - self.board.set_food(x, y); - self.write_at(stdout, x, y, 'F'); - } else if self.board.is_body(self.head_x, self.head_y) { + let pos = self.random_empty_position(); + self.board.set_food(pos); + self.write_at(stdout, pos, 'F'); + } else if self.board.is_body(self.head) { // oh no break 'game; } // Write the new head - self.board.set_dir(self.head_x, self.head_y, self.direction); - self.write_at(stdout, self.head_x, self.head_y, 'U'); + self.board.set_dir(self.head, self.direction); + self.write_at(stdout, self.head, 'U'); if self.digesting == 0 { - let (old_tail_x, old_tail_y) = (self.tail_x, self.tail_y); - match self.board.get_tail_dir(self.tail_x, self.tail_y) { + let old_tail = self.tail; + match self.board.get_tail_dir(self.tail) { Some(Direction::Up) => { - self.tail_y -= 1; + self.tail.row -= 1; } Some(Direction::Down) => { - self.tail_y += 1; + self.tail.row += 1; } Some(Direction::Left) => { - self.tail_x -= 1; + self.tail.col -= 1; } Some(Direction::Right) => { - self.tail_x += 1; + self.tail.col += 1; } None => { panic!("Bad game state"); } } - self.board.clear(old_tail_x, old_tail_y); - self.write_at(stdout, old_tail_x, old_tail_y, ' '); + self.board.clear(old_tail); + self.write_at(stdout, old_tail, ' '); } else { self.digesting -= 1; } @@ -323,23 +331,24 @@ impl Game { self.score } - fn write_at(&self, console: &mut neotron_sdk::File, x: usize, y: usize, ch: char) { - let _ = write!( - console, - "\u{001b}[{};{}H{}", - self.offset_y + y + 1, - self.offset_x + x + 1, - ch - ); + fn write_at(&self, console: &mut neotron_sdk::File, position: console::Position, ch: char) { + let adjusted_position = console::Position { + row: position.row + self.offset.row, + col: position.col + self.offset.col, + }; + console::move_cursor(console, adjusted_position); + let _ = console.write_char(ch); } - fn random_empty_position(&mut self) -> (usize, usize) { + fn random_empty_position(&mut self) -> console::Position { loop { // This isn't equally distributed. I don't really care. - let x = (self.random() as usize) % self.width; - let y = (self.random() as usize) % self.height; - if self.board.is_empty(x, y) { - return (x, y); + let pos = console::Position { + row: (neotron_sdk::rand() % self.height as u16) as u8, + col: (neotron_sdk::rand() % self.width as u16) as u8, + }; + if self.board.is_empty(pos) { + return pos; } } } @@ -363,68 +372,76 @@ impl Direction { } } +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[repr(u8)] +enum CellContents { + Empty, + Up, + Down, + Left, + Right, + Food, +} + struct Board { - cells: [[u8; WIDTH]; HEIGHT], + cells: [[CellContents; WIDTH]; HEIGHT], } impl Board { - const UP: u8 = b'U'; - const DOWN: u8 = b'D'; - const LEFT: u8 = b'L'; - const RIGHT: u8 = b'R'; - const FOOD: u8 = b'F'; - const fn new() -> Board { Board { - cells: [[0; WIDTH]; HEIGHT], + cells: [[CellContents::Empty; WIDTH]; HEIGHT], } } fn reset(&mut self) { for y in 0..HEIGHT { for x in 0..WIDTH { - self.cells[y][x] = 0; + self.cells[y][x] = CellContents::Empty; } } } - fn set_dir(&mut self, x: usize, y: usize, direction: Direction) { - self.cells[y][x] = match direction { - Direction::Up => Self::UP, - Direction::Down => Self::DOWN, - Direction::Left => Self::LEFT, - Direction::Right => Self::RIGHT, + fn set_dir(&mut self, position: console::Position, direction: Direction) { + self.cells[usize::from(position.row)][usize::from(position.col)] = match direction { + Direction::Up => CellContents::Up, + Direction::Down => CellContents::Down, + Direction::Left => CellContents::Left, + Direction::Right => CellContents::Right, } } - fn get_tail_dir(&self, x: usize, y: usize) -> Option { - match self.cells[y][x] { - Self::UP => Some(Direction::Up), - Self::DOWN => Some(Direction::Down), - Self::LEFT => Some(Direction::Left), - Self::RIGHT => Some(Direction::Right), + fn get_tail_dir(&self, position: console::Position) -> Option { + match self.cells[usize::from(position.row)][usize::from(position.col)] { + CellContents::Up => Some(Direction::Up), + CellContents::Down => Some(Direction::Down), + CellContents::Left => Some(Direction::Left), + CellContents::Right => Some(Direction::Right), _ => None, } } - fn set_food(&mut self, x: usize, y: usize) { - self.cells[y][x] = Self::FOOD; + fn set_food(&mut self, position: console::Position) { + self.cells[usize::from(position.row)][usize::from(position.col)] = CellContents::Food; } - fn is_food(&mut self, x: usize, y: usize) -> bool { - self.cells[y][x] == Self::FOOD + fn is_food(&mut self, position: console::Position) -> bool { + self.cells[usize::from(position.row)][usize::from(position.col)] == CellContents::Food } - fn is_body(&mut self, x: usize, y: usize) -> bool { - let cell = self.cells[y][x]; - cell == Self::UP || cell == Self::DOWN || cell == Self::LEFT || cell == Self::RIGHT + fn is_body(&mut self, position: console::Position) -> bool { + let cell = self.cells[usize::from(position.row)][usize::from(position.col)]; + cell == CellContents::Up + || cell == CellContents::Down + || cell == CellContents::Left + || cell == CellContents::Right } - fn is_empty(&mut self, x: usize, y: usize) -> bool { - self.cells[y][x] == 0 + fn is_empty(&mut self, position: console::Position) -> bool { + self.cells[usize::from(position.row)][usize::from(position.col)] == CellContents::Empty } - fn clear(&mut self, x: usize, y: usize) { - self.cells[y][x] = 0; + fn clear(&mut self, position: console::Position) { + self.cells[usize::from(position.row)][usize::from(position.col)] = CellContents::Empty; } } From 19feabfe6e0b31da5347c0e33ce443c5dece0d40 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sat, 29 Jul 2023 19:39:30 +0100 Subject: [PATCH 24/37] Add colour. --- samples/snake/src/lib.rs | 231 +++++++++++++++++++++++++++----------- samples/snake/src/main.rs | 10 +- 2 files changed, 165 insertions(+), 76 deletions(-) diff --git a/samples/snake/src/lib.rs b/samples/snake/src/lib.rs index 367ccf3..d7d642a 100644 --- a/samples/snake/src/lib.rs +++ b/samples/snake/src/lib.rs @@ -1,17 +1,16 @@ //! Game logic for Snake #![no_std] +#![deny(missing_docs)] +#![deny(unsafe_code)] use core::fmt::Write; use neotron_sdk::console; -#[derive(Debug)] -pub enum Error { - ScreenTooTall, - ScreenTooWide, -} - +/// Represents the Snake application +/// +/// An application can play multiple games. pub struct App { game: Game, width: u8, @@ -21,6 +20,10 @@ pub struct App { } impl App { + /// Make a new snake application. + /// + /// You can give the screen size in characters. There will be a border and + /// the board will be two units smaller in each axis. pub const fn new(width: u8, height: u8) -> App { App { game: Game::new(width - 2, height - 2, console::Position { row: 1, col: 1 }), @@ -31,7 +34,10 @@ impl App { } } - pub fn play(&mut self) -> Result<(), Error> { + /// Play multiple games of snake. + /// + /// Loops playing games and printing scores. + pub fn play(&mut self) { console::cursor_off(&mut self.stdout); self.clear_screen(); self.title_screen(); @@ -62,17 +68,22 @@ impl App { // show cursor console::cursor_on(&mut self.stdout); self.clear_screen(); - Ok(()) } + /// Clear the screen and draw the board. fn clear_screen(&mut self) { + console::set_sgr(&mut self.stdout, [console::SgrParam::Reset]); console::clear_screen(&mut self.stdout); + console::set_sgr( + &mut self.stdout, + [console::SgrParam::FgYellow, console::SgrParam::BgBlue], + ); console::move_cursor(&mut self.stdout, console::Position::origin()); - let _ = self.stdout.write_char('+'); + let _ = self.stdout.write_char('╔'); for _ in 1..self.width - 1 { - let _ = self.stdout.write_char('-'); + let _ = self.stdout.write_char('═'); } - let _ = self.stdout.write_char('+'); + let _ = self.stdout.write_char('╗'); console::move_cursor( &mut self.stdout, console::Position { @@ -80,14 +91,14 @@ impl App { col: 0, }, ); - let _ = self.stdout.write_char('+'); + let _ = self.stdout.write_char('╚'); for _ in 1..self.width - 1 { - let _ = self.stdout.write_char('-'); + let _ = self.stdout.write_char('═'); } - let _ = self.stdout.write_char('+'); + let _ = self.stdout.write_char('╝'); for row in 1..self.height - 1 { console::move_cursor(&mut self.stdout, console::Position { row, col: 0 }); - let _ = self.stdout.write_char('|'); + let _ = self.stdout.write_char('║'); console::move_cursor( &mut self.stdout, console::Position { @@ -95,12 +106,15 @@ impl App { col: self.width - 1, }, ); - let _ = self.stdout.write_char('|'); + let _ = self.stdout.write_char('║'); } + console::set_sgr(&mut self.stdout, [console::SgrParam::Reset]); } + /// Show the title screen fn title_screen(&mut self) { - let message = "ANSI Snake"; + console::set_sgr(&mut self.stdout, [console::SgrParam::Reset]); + let message = "Neotron Snake by theJPster"; let pos = console::Position { row: self.height / 2, col: (self.width - message.chars().count() as u8) / 2, @@ -116,6 +130,7 @@ impl App { let _ = self.stdout.write_str(message); } + /// Spin until a key is pressed fn wait_for_key(&mut self) -> u8 { loop { let mut buffer = [0u8; 1]; @@ -126,10 +141,12 @@ impl App { } } + /// Print the game over message with the given score fn winning_message(&mut self, score: u32) { + console::set_sgr(&mut self.stdout, [console::SgrParam::Reset]); let pos = console::Position { row: self.height / 2, - col: (self.width - 13 as u8) / 2, + col: (self.width - 13u8) / 2, }; console::move_cursor(&mut self.stdout, pos); let _ = writeln!(self.stdout, "Score: {:06}", score); @@ -143,7 +160,36 @@ impl App { } } -pub struct Game { +/// Something we can send to the ANSI console +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum Piece { + Head, + Food, + Body, +} + +impl Piece { + /// Get the Unicode char for this piece + fn get_char(self) -> char { + match self { + Piece::Body => '▓', + Piece::Head => '█', + Piece::Food => '▲', + } + } + + /// Get the ANSI colour for this piece + fn get_colour(self) -> console::SgrParam { + match self { + Piece::Body => console::SgrParam::FgMagenta, + Piece::Head => console::SgrParam::FgYellow, + Piece::Food => console::SgrParam::FgGreen, + } + } +} + +/// Represents one game of Snake +struct Game { board: Board<{ Self::MAX_WIDTH }, { Self::MAX_HEIGHT }>, width: u8, height: u8, @@ -157,9 +203,17 @@ pub struct Game { } impl Game { - pub const MAX_WIDTH: usize = 80; - pub const MAX_HEIGHT: usize = 25; - + /// The maximum width board we can handle + pub const MAX_WIDTH: usize = 78; + /// The maximum height board we can handle + pub const MAX_HEIGHT: usize = 23; + /// How many ms per tick do we start at? + const STARTING_TICK: u16 = 150; + + /// Make a new game. + /// + /// Give the width and the height of the game board, and where on the screen + /// the board should be located. const fn new(width: u8, height: u8, offset: console::Position) -> Game { Game { board: Board::new(), @@ -171,13 +225,15 @@ impl Game { direction: Direction::Up, score: 0, digesting: 0, - tick_interval_ms: 150, + tick_interval_ms: Self::STARTING_TICK, } } + /// Play a game fn play(&mut self, stdin: &mut neotron_sdk::File, stdout: &mut neotron_sdk::File) -> u32 { - // Reset score + // Reset score and speed self.score = 0; + self.tick_interval_ms = Self::STARTING_TICK; // Wipe board self.board.reset(); // Add offset snake @@ -186,12 +242,12 @@ impl Game { col: self.width / 4, }; self.tail = self.head; - self.board.set_dir(self.head, self.direction); - self.write_at(stdout, self.head, 'U'); + self.board.store_body(self.head, self.direction); + self.write_at(stdout, self.head, Some(Piece::Head)); // Add random food let pos = self.random_empty_position(); - self.board.set_food(pos); - self.write_at(stdout, pos, 'F'); + self.board.store_food(pos); + self.write_at(stdout, pos, Some(Piece::Food)); 'game: loop { // Wait for frame tick @@ -205,7 +261,7 @@ impl Game { // Read input 'input: loop { let mut buffer = [0u8; 1]; - if let Some(1) = stdin.read(&mut buffer).ok() { + if let Ok(1) = stdin.read(&mut buffer) { match buffer[0] { b'w' | b'W' => { // Going up @@ -245,7 +301,8 @@ impl Game { } // Mark which way we're going in the old head position - self.board.set_dir(self.head, self.direction); + self.board.store_body(self.head, self.direction); + self.write_at(stdout, self.head, Some(Piece::Body)); // Update head position match self.direction { @@ -291,20 +348,20 @@ impl Game { } // Add random food let pos = self.random_empty_position(); - self.board.set_food(pos); - self.write_at(stdout, pos, 'F'); + self.board.store_food(pos); + self.write_at(stdout, pos, Some(Piece::Food)); } else if self.board.is_body(self.head) { // oh no break 'game; } // Write the new head - self.board.set_dir(self.head, self.direction); - self.write_at(stdout, self.head, 'U'); + self.board.store_body(self.head, self.direction); + self.write_at(stdout, self.head, Some(Piece::Head)); if self.digesting == 0 { let old_tail = self.tail; - match self.board.get_tail_dir(self.tail) { + match self.board.remove_piece(self.tail) { Some(Direction::Up) => { self.tail.row -= 1; } @@ -321,8 +378,7 @@ impl Game { panic!("Bad game state"); } } - self.board.clear(old_tail); - self.write_at(stdout, old_tail, ' '); + self.write_at(stdout, old_tail, None); } else { self.digesting -= 1; } @@ -331,15 +387,29 @@ impl Game { self.score } - fn write_at(&self, console: &mut neotron_sdk::File, position: console::Position, ch: char) { + /// Draw a piece on the ANSI console at the given location + fn write_at( + &self, + console: &mut neotron_sdk::File, + position: console::Position, + piece: Option, + ) { let adjusted_position = console::Position { row: position.row + self.offset.row, col: position.col + self.offset.col, }; console::move_cursor(console, adjusted_position); - let _ = console.write_char(ch); + if let Some(piece) = piece { + let colour = piece.get_colour(); + let ch = piece.get_char(); + console::set_sgr(console, [colour]); + let _ = console.write_char(ch); + } else { + let _ = console.write_char(' '); + } } + /// Find a spot on the board that is empty fn random_empty_position(&mut self) -> console::Position { loop { // This isn't equally distributed. I don't really care. @@ -354,94 +424,119 @@ impl Game { } } +/// A direction in which a body piece can face #[derive(Copy, Clone, Debug, PartialEq, Eq)] enum Direction { + /// Facing up Up, + /// Facing down Down, + /// Facing left Left, + /// Facing right Right, } impl Direction { + /// Is this left/right? fn is_horizontal(self) -> bool { self == Direction::Left || self == Direction::Right } + /// Is this up/down? fn is_vertical(self) -> bool { self == Direction::Up || self == Direction::Down } } +/// Something we can put on a board. #[derive(Debug, Copy, Clone, PartialEq, Eq)] #[repr(u8)] -enum CellContents { +enum BoardPiece { + /// Nothing here Empty, + /// A body, and the next piece is up Up, + /// A body, and the next piece is down Down, + /// A body, and the next piece is left Left, + /// A body, and the next piece is right Right, + /// A piece of food Food, } +/// Tracks where the snake is in 2D space. +/// +/// We do this rather than maintain a Vec of body positions and a Vec of food +/// positions because it's fixed size and faster to see if a space is empty, or +/// body, or food. struct Board { - cells: [[CellContents; WIDTH]; HEIGHT], + cells: [[BoardPiece; WIDTH]; HEIGHT], } impl Board { + /// Make a new empty board const fn new() -> Board { Board { - cells: [[CellContents::Empty; WIDTH]; HEIGHT], + cells: [[BoardPiece::Empty; WIDTH]; HEIGHT], } } + /// Clean up the board so everything is empty. fn reset(&mut self) { for y in 0..HEIGHT { for x in 0..WIDTH { - self.cells[y][x] = CellContents::Empty; + self.cells[y][x] = BoardPiece::Empty; } } } - fn set_dir(&mut self, position: console::Position, direction: Direction) { + /// Store a body piece on the board, based on which way it is facing + fn store_body(&mut self, position: console::Position, direction: Direction) { self.cells[usize::from(position.row)][usize::from(position.col)] = match direction { - Direction::Up => CellContents::Up, - Direction::Down => CellContents::Down, - Direction::Left => CellContents::Left, - Direction::Right => CellContents::Right, + Direction::Up => BoardPiece::Up, + Direction::Down => BoardPiece::Down, + Direction::Left => BoardPiece::Left, + Direction::Right => BoardPiece::Right, } } - fn get_tail_dir(&self, position: console::Position) -> Option { - match self.cells[usize::from(position.row)][usize::from(position.col)] { - CellContents::Up => Some(Direction::Up), - CellContents::Down => Some(Direction::Down), - CellContents::Left => Some(Direction::Left), - CellContents::Right => Some(Direction::Right), - _ => None, - } - } - - fn set_food(&mut self, position: console::Position) { - self.cells[usize::from(position.row)][usize::from(position.col)] = CellContents::Food; + /// Put some food on the board + fn store_food(&mut self, position: console::Position) { + self.cells[usize::from(position.row)][usize::from(position.col)] = BoardPiece::Food; } + /// Is there food on the board here? fn is_food(&mut self, position: console::Position) -> bool { - self.cells[usize::from(position.row)][usize::from(position.col)] == CellContents::Food + self.cells[usize::from(position.row)][usize::from(position.col)] == BoardPiece::Food } + /// Is there body on the board here? fn is_body(&mut self, position: console::Position) -> bool { let cell = self.cells[usize::from(position.row)][usize::from(position.col)]; - cell == CellContents::Up - || cell == CellContents::Down - || cell == CellContents::Left - || cell == CellContents::Right + cell == BoardPiece::Up + || cell == BoardPiece::Down + || cell == BoardPiece::Left + || cell == BoardPiece::Right } + /// Is this position empty? fn is_empty(&mut self, position: console::Position) -> bool { - self.cells[usize::from(position.row)][usize::from(position.col)] == CellContents::Empty + self.cells[usize::from(position.row)][usize::from(position.col)] == BoardPiece::Empty } - fn clear(&mut self, position: console::Position) { - self.cells[usize::from(position.row)][usize::from(position.col)] = CellContents::Empty; + /// Remove a piece from the board + fn remove_piece(&mut self, position: console::Position) -> Option { + let old = match self.cells[usize::from(position.row)][usize::from(position.col)] { + BoardPiece::Up => Some(Direction::Up), + BoardPiece::Down => Some(Direction::Down), + BoardPiece::Left => Some(Direction::Left), + BoardPiece::Right => Some(Direction::Right), + _ => None, + }; + self.cells[usize::from(position.row)][usize::from(position.col)] = BoardPiece::Empty; + old } } diff --git a/samples/snake/src/main.rs b/samples/snake/src/main.rs index 27a16a5..70a974e 100644 --- a/samples/snake/src/main.rs +++ b/samples/snake/src/main.rs @@ -10,12 +10,6 @@ static mut APP: snake::App = snake::App::new(80, 25); #[no_mangle] extern "C" fn neotron_main() -> i32 { - if let Err(e) = unsafe { APP.play() } { - let mut stdout = neotron_sdk::stdout(); - use core::fmt::Write; - let _ = writeln!(stdout, "Error: {:?}", e); - 1 - } else { - 0 - } + unsafe { APP.play() } + 0 } From 21c4d3e68113ef7a2b863d8225dea75c287fbad7 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sat, 29 Jul 2023 20:08:06 +0100 Subject: [PATCH 25/37] Clean up some clippy lints --- src/console.rs | 2 +- src/fake_os_api.rs | 2 +- src/lib.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/console.rs b/src/console.rs index fda1a81..214bf20 100644 --- a/src/console.rs +++ b/src/console.rs @@ -93,7 +93,7 @@ where if let Some(value) = iter.next() { let _ = write!(f, "{}", value as u8); } - while let Some(value) = iter.next() { + for value in iter { let _ = write!(f, ";{}", value as u8); } let _ = write!(f, "m"); diff --git a/src/fake_os_api.rs b/src/fake_os_api.rs index 8a9f2e7..2fa2659 100644 --- a/src/fake_os_api.rs +++ b/src/fake_os_api.rs @@ -105,7 +105,7 @@ extern "C" fn api_read( mut buffer: neotron_api::FfiBuffer, ) -> neotron_api::Result { if fd == neotron_api::file::Handle::new_stdin() { - if let Some(b) = STDIN_RX.lock().unwrap().as_mut().unwrap().try_recv().ok() { + if let Ok(b) = STDIN_RX.lock().unwrap().as_mut().unwrap().try_recv() { buffer.as_mut_slice().unwrap()[0] = b; neotron_api::Result::Ok(1) } else { diff --git a/src/lib.rs b/src/lib.rs index 88b33d1..f47d4b1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -343,7 +343,7 @@ pub fn srand(seed: u16) { /// Get a 16-bit psuedorandom number pub fn rand() -> u16 { let mut state = RAND_STATE.load(core::sync::atomic::Ordering::Relaxed); - let bit = ((state >> 0) ^ (state >> 2) ^ (state >> 3) ^ (state >> 5)) & 0x01; + let bit = (state ^ (state >> 2) ^ (state >> 3) ^ (state >> 5)) & 0x01; state = (state >> 1) | (bit << 15); RAND_STATE.store(state, core::sync::atomic::Ordering::Relaxed); state From f82f493850f84d09efd08a5cf5ebc6bddab55f1a Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sat, 21 Oct 2023 13:36:18 +0100 Subject: [PATCH 26/37] Remove the snake application. It's now in neotron-apps. --- samples/Cargo.toml | 2 +- samples/build.sh | 2 +- samples/snake/Cargo.toml | 12 - samples/snake/build.rs | 21 -- samples/snake/src/lib.rs | 542 -------------------------------------- samples/snake/src/main.rs | 15 -- 6 files changed, 2 insertions(+), 592 deletions(-) delete mode 100644 samples/snake/Cargo.toml delete mode 100644 samples/snake/build.rs delete mode 100644 samples/snake/src/lib.rs delete mode 100644 samples/snake/src/main.rs diff --git a/samples/Cargo.toml b/samples/Cargo.toml index ecc88d6..6d844a1 100644 --- a/samples/Cargo.toml +++ b/samples/Cargo.toml @@ -3,9 +3,9 @@ members = [ "fault", "hello", "input-test", - "snake", "panic", ] +resolver = "2" [profile.release] opt-level = "z" diff --git a/samples/build.sh b/samples/build.sh index 6bcaec6..68fbc77 100755 --- a/samples/build.sh +++ b/samples/build.sh @@ -24,6 +24,6 @@ cargo build echo "Building for ${TARGET}" cargo build --target ${TARGET} --release -for program in panic hello fault input-test snake; do +for program in panic hello fault input-test; do cp ./target/${TARGET}/release/${program} ./release/${program}.elf done diff --git a/samples/snake/Cargo.toml b/samples/snake/Cargo.toml deleted file mode 100644 index ff37eb3..0000000 --- a/samples/snake/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "snake" -version = "0.1.0" -edition = "2021" -license = "MIT OR Apache-2.0" -authors = ["Jonathan 'theJPster' Pallant "] -description = "ANSI Snake for Neotron systems" - -[dependencies] -neotron-sdk = { path = "../.." } - -# See workspace for profile settings diff --git a/samples/snake/build.rs b/samples/snake/build.rs deleted file mode 100644 index 759eb7d..0000000 --- a/samples/snake/build.rs +++ /dev/null @@ -1,21 +0,0 @@ -use std::env; -use std::fs::File; -use std::io::Write; -use std::path::PathBuf; - -fn main() { - // Put `neotron-cortex-m.ld` in our output directory and ensure it's - // on the linker search path. - let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); - File::create(out.join("neotron-cortex-m.ld")) - .unwrap() - .write_all(include_bytes!("../neotron-cortex-m.ld")) - .unwrap(); - println!("cargo:rustc-link-search={}", out.display()); - - // By default, Cargo will re-run a build script whenever - // any file in the project changes. By specifying `neotron-cortex-m.ld` - // here, we ensure the build script is only re-run when - // `neotron-cortex-m.ld` is changed. - println!("cargo:rerun-if-changed=../neotron-cortex-m.ld"); -} diff --git a/samples/snake/src/lib.rs b/samples/snake/src/lib.rs deleted file mode 100644 index d7d642a..0000000 --- a/samples/snake/src/lib.rs +++ /dev/null @@ -1,542 +0,0 @@ -//! Game logic for Snake - -#![no_std] -#![deny(missing_docs)] -#![deny(unsafe_code)] - -use core::fmt::Write; - -use neotron_sdk::console; - -/// Represents the Snake application -/// -/// An application can play multiple games. -pub struct App { - game: Game, - width: u8, - height: u8, - stdout: neotron_sdk::File, - stdin: neotron_sdk::File, -} - -impl App { - /// Make a new snake application. - /// - /// You can give the screen size in characters. There will be a border and - /// the board will be two units smaller in each axis. - pub const fn new(width: u8, height: u8) -> App { - App { - game: Game::new(width - 2, height - 2, console::Position { row: 1, col: 1 }), - width, - height, - stdout: neotron_sdk::stdout(), - stdin: neotron_sdk::stdin(), - } - } - - /// Play multiple games of snake. - /// - /// Loops playing games and printing scores. - pub fn play(&mut self) { - console::cursor_off(&mut self.stdout); - self.clear_screen(); - self.title_screen(); - - let mut seed: u16 = 0x4f35; - - 'outer: loop { - 'inner: loop { - let key = self.wait_for_key(); - seed = seed.wrapping_add(1); - if key == b'q' || key == b'Q' { - break 'outer; - } - if key == b'p' || key == b'P' { - break 'inner; - } - } - - self.clear_screen(); - - neotron_sdk::srand(seed); - - let score = self.game.play(&mut self.stdin, &mut self.stdout); - - self.winning_message(score); - } - - // show cursor - console::cursor_on(&mut self.stdout); - self.clear_screen(); - } - - /// Clear the screen and draw the board. - fn clear_screen(&mut self) { - console::set_sgr(&mut self.stdout, [console::SgrParam::Reset]); - console::clear_screen(&mut self.stdout); - console::set_sgr( - &mut self.stdout, - [console::SgrParam::FgYellow, console::SgrParam::BgBlue], - ); - console::move_cursor(&mut self.stdout, console::Position::origin()); - let _ = self.stdout.write_char('╔'); - for _ in 1..self.width - 1 { - let _ = self.stdout.write_char('═'); - } - let _ = self.stdout.write_char('╗'); - console::move_cursor( - &mut self.stdout, - console::Position { - row: self.height - 1, - col: 0, - }, - ); - let _ = self.stdout.write_char('╚'); - for _ in 1..self.width - 1 { - let _ = self.stdout.write_char('═'); - } - let _ = self.stdout.write_char('╝'); - for row in 1..self.height - 1 { - console::move_cursor(&mut self.stdout, console::Position { row, col: 0 }); - let _ = self.stdout.write_char('║'); - console::move_cursor( - &mut self.stdout, - console::Position { - row, - col: self.width - 1, - }, - ); - let _ = self.stdout.write_char('║'); - } - console::set_sgr(&mut self.stdout, [console::SgrParam::Reset]); - } - - /// Show the title screen - fn title_screen(&mut self) { - console::set_sgr(&mut self.stdout, [console::SgrParam::Reset]); - let message = "Neotron Snake by theJPster"; - let pos = console::Position { - row: self.height / 2, - col: (self.width - message.chars().count() as u8) / 2, - }; - console::move_cursor(&mut self.stdout, pos); - let _ = self.stdout.write_str(message); - let message = "Q to Quit | 'P' to Play"; - let pos = console::Position { - row: pos.row + 1, - col: (self.width - message.chars().count() as u8) / 2, - }; - console::move_cursor(&mut self.stdout, pos); - let _ = self.stdout.write_str(message); - } - - /// Spin until a key is pressed - fn wait_for_key(&mut self) -> u8 { - loop { - let mut buffer = [0u8; 1]; - if let Ok(1) = self.stdin.read(&mut buffer) { - return buffer[0]; - } - neotron_sdk::delay(core::time::Duration::from_millis(10)); - } - } - - /// Print the game over message with the given score - fn winning_message(&mut self, score: u32) { - console::set_sgr(&mut self.stdout, [console::SgrParam::Reset]); - let pos = console::Position { - row: self.height / 2, - col: (self.width - 13u8) / 2, - }; - console::move_cursor(&mut self.stdout, pos); - let _ = writeln!(self.stdout, "Score: {:06}", score); - let message = "Q to Quit | 'P' to Play"; - let pos = console::Position { - row: pos.row + 1, - col: (self.width - message.chars().count() as u8) / 2, - }; - console::move_cursor(&mut self.stdout, pos); - let _ = self.stdout.write_str(message); - } -} - -/// Something we can send to the ANSI console -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -enum Piece { - Head, - Food, - Body, -} - -impl Piece { - /// Get the Unicode char for this piece - fn get_char(self) -> char { - match self { - Piece::Body => '▓', - Piece::Head => '█', - Piece::Food => '▲', - } - } - - /// Get the ANSI colour for this piece - fn get_colour(self) -> console::SgrParam { - match self { - Piece::Body => console::SgrParam::FgMagenta, - Piece::Head => console::SgrParam::FgYellow, - Piece::Food => console::SgrParam::FgGreen, - } - } -} - -/// Represents one game of Snake -struct Game { - board: Board<{ Self::MAX_WIDTH }, { Self::MAX_HEIGHT }>, - width: u8, - height: u8, - offset: console::Position, - head: console::Position, - tail: console::Position, - direction: Direction, - score: u32, - digesting: u32, - tick_interval_ms: u16, -} - -impl Game { - /// The maximum width board we can handle - pub const MAX_WIDTH: usize = 78; - /// The maximum height board we can handle - pub const MAX_HEIGHT: usize = 23; - /// How many ms per tick do we start at? - const STARTING_TICK: u16 = 150; - - /// Make a new game. - /// - /// Give the width and the height of the game board, and where on the screen - /// the board should be located. - const fn new(width: u8, height: u8, offset: console::Position) -> Game { - Game { - board: Board::new(), - width, - height, - offset, - head: console::Position { row: 0, col: 0 }, - tail: console::Position { row: 0, col: 0 }, - direction: Direction::Up, - score: 0, - digesting: 0, - tick_interval_ms: Self::STARTING_TICK, - } - } - - /// Play a game - fn play(&mut self, stdin: &mut neotron_sdk::File, stdout: &mut neotron_sdk::File) -> u32 { - // Reset score and speed - self.score = 0; - self.tick_interval_ms = Self::STARTING_TICK; - // Wipe board - self.board.reset(); - // Add offset snake - self.head = console::Position { - row: self.height / 4, - col: self.width / 4, - }; - self.tail = self.head; - self.board.store_body(self.head, self.direction); - self.write_at(stdout, self.head, Some(Piece::Head)); - // Add random food - let pos = self.random_empty_position(); - self.board.store_food(pos); - self.write_at(stdout, pos, Some(Piece::Food)); - - 'game: loop { - // Wait for frame tick - neotron_sdk::delay(core::time::Duration::from_millis( - self.tick_interval_ms as u64, - )); - - // 1 point for not being dead - self.score += 1; - - // Read input - 'input: loop { - let mut buffer = [0u8; 1]; - if let Ok(1) = stdin.read(&mut buffer) { - match buffer[0] { - b'w' | b'W' => { - // Going up - if self.direction.is_horizontal() { - self.direction = Direction::Up; - } - } - b's' | b'S' => { - // Going down - if self.direction.is_horizontal() { - self.direction = Direction::Down; - } - } - b'a' | b'A' => { - // Going left - if self.direction.is_vertical() { - self.direction = Direction::Left; - } - } - b'd' | b'D' => { - // Going right - if self.direction.is_vertical() { - self.direction = Direction::Right; - } - } - b'q' | b'Q' => { - // Quit game - break 'game; - } - _ => { - // ignore - } - } - } else { - break 'input; - } - } - - // Mark which way we're going in the old head position - self.board.store_body(self.head, self.direction); - self.write_at(stdout, self.head, Some(Piece::Body)); - - // Update head position - match self.direction { - Direction::Up => { - if self.head.row == 0 { - break 'game; - } - self.head.row -= 1; - } - Direction::Down => { - if self.head.row == self.height - 1 { - break 'game; - } - self.head.row += 1; - } - Direction::Left => { - if self.head.col == 0 { - break 'game; - } - self.head.col -= 1; - } - Direction::Right => { - if self.head.col == self.width - 1 { - break 'game; - } - self.head.col += 1; - } - } - - // Check what we just ate - // - Food => get longer - // - Ourselves => die - if self.board.is_food(self.head) { - // yum - self.score += 10; - self.digesting = 2; - // Drop 10% on the tick interval - self.tick_interval_ms *= 9; - self.tick_interval_ms /= 10; - if self.tick_interval_ms < 5 { - // Maximum speed - self.tick_interval_ms = 5; - } - // Add random food - let pos = self.random_empty_position(); - self.board.store_food(pos); - self.write_at(stdout, pos, Some(Piece::Food)); - } else if self.board.is_body(self.head) { - // oh no - break 'game; - } - - // Write the new head - self.board.store_body(self.head, self.direction); - self.write_at(stdout, self.head, Some(Piece::Head)); - - if self.digesting == 0 { - let old_tail = self.tail; - match self.board.remove_piece(self.tail) { - Some(Direction::Up) => { - self.tail.row -= 1; - } - Some(Direction::Down) => { - self.tail.row += 1; - } - Some(Direction::Left) => { - self.tail.col -= 1; - } - Some(Direction::Right) => { - self.tail.col += 1; - } - None => { - panic!("Bad game state"); - } - } - self.write_at(stdout, old_tail, None); - } else { - self.digesting -= 1; - } - } - - self.score - } - - /// Draw a piece on the ANSI console at the given location - fn write_at( - &self, - console: &mut neotron_sdk::File, - position: console::Position, - piece: Option, - ) { - let adjusted_position = console::Position { - row: position.row + self.offset.row, - col: position.col + self.offset.col, - }; - console::move_cursor(console, adjusted_position); - if let Some(piece) = piece { - let colour = piece.get_colour(); - let ch = piece.get_char(); - console::set_sgr(console, [colour]); - let _ = console.write_char(ch); - } else { - let _ = console.write_char(' '); - } - } - - /// Find a spot on the board that is empty - fn random_empty_position(&mut self) -> console::Position { - loop { - // This isn't equally distributed. I don't really care. - let pos = console::Position { - row: (neotron_sdk::rand() % self.height as u16) as u8, - col: (neotron_sdk::rand() % self.width as u16) as u8, - }; - if self.board.is_empty(pos) { - return pos; - } - } - } -} - -/// A direction in which a body piece can face -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -enum Direction { - /// Facing up - Up, - /// Facing down - Down, - /// Facing left - Left, - /// Facing right - Right, -} - -impl Direction { - /// Is this left/right? - fn is_horizontal(self) -> bool { - self == Direction::Left || self == Direction::Right - } - - /// Is this up/down? - fn is_vertical(self) -> bool { - self == Direction::Up || self == Direction::Down - } -} - -/// Something we can put on a board. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -#[repr(u8)] -enum BoardPiece { - /// Nothing here - Empty, - /// A body, and the next piece is up - Up, - /// A body, and the next piece is down - Down, - /// A body, and the next piece is left - Left, - /// A body, and the next piece is right - Right, - /// A piece of food - Food, -} - -/// Tracks where the snake is in 2D space. -/// -/// We do this rather than maintain a Vec of body positions and a Vec of food -/// positions because it's fixed size and faster to see if a space is empty, or -/// body, or food. -struct Board { - cells: [[BoardPiece; WIDTH]; HEIGHT], -} - -impl Board { - /// Make a new empty board - const fn new() -> Board { - Board { - cells: [[BoardPiece::Empty; WIDTH]; HEIGHT], - } - } - - /// Clean up the board so everything is empty. - fn reset(&mut self) { - for y in 0..HEIGHT { - for x in 0..WIDTH { - self.cells[y][x] = BoardPiece::Empty; - } - } - } - - /// Store a body piece on the board, based on which way it is facing - fn store_body(&mut self, position: console::Position, direction: Direction) { - self.cells[usize::from(position.row)][usize::from(position.col)] = match direction { - Direction::Up => BoardPiece::Up, - Direction::Down => BoardPiece::Down, - Direction::Left => BoardPiece::Left, - Direction::Right => BoardPiece::Right, - } - } - - /// Put some food on the board - fn store_food(&mut self, position: console::Position) { - self.cells[usize::from(position.row)][usize::from(position.col)] = BoardPiece::Food; - } - - /// Is there food on the board here? - fn is_food(&mut self, position: console::Position) -> bool { - self.cells[usize::from(position.row)][usize::from(position.col)] == BoardPiece::Food - } - - /// Is there body on the board here? - fn is_body(&mut self, position: console::Position) -> bool { - let cell = self.cells[usize::from(position.row)][usize::from(position.col)]; - cell == BoardPiece::Up - || cell == BoardPiece::Down - || cell == BoardPiece::Left - || cell == BoardPiece::Right - } - - /// Is this position empty? - fn is_empty(&mut self, position: console::Position) -> bool { - self.cells[usize::from(position.row)][usize::from(position.col)] == BoardPiece::Empty - } - - /// Remove a piece from the board - fn remove_piece(&mut self, position: console::Position) -> Option { - let old = match self.cells[usize::from(position.row)][usize::from(position.col)] { - BoardPiece::Up => Some(Direction::Up), - BoardPiece::Down => Some(Direction::Down), - BoardPiece::Left => Some(Direction::Left), - BoardPiece::Right => Some(Direction::Right), - _ => None, - }; - self.cells[usize::from(position.row)][usize::from(position.col)] = BoardPiece::Empty; - old - } -} diff --git a/samples/snake/src/main.rs b/samples/snake/src/main.rs deleted file mode 100644 index 70a974e..0000000 --- a/samples/snake/src/main.rs +++ /dev/null @@ -1,15 +0,0 @@ -#![cfg_attr(target_os = "none", no_std)] -#![cfg_attr(target_os = "none", no_main)] - -#[cfg(not(target_os = "none"))] -fn main() { - neotron_sdk::init(); -} - -static mut APP: snake::App = snake::App::new(80, 25); - -#[no_mangle] -extern "C" fn neotron_main() -> i32 { - unsafe { APP.play() } - 0 -} From 9c858bab4ab2e0b91b596b1d8434c843b3568dca Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Fri, 3 Nov 2023 10:50:21 +0000 Subject: [PATCH 27/37] Add C and ASM examples. --- samples/README.md | 8 + samples/asmhello/.gitignore | 2 + samples/asmhello/asmhello.S | 35 +++ samples/asmhello/build.sh | 2 + samples/build.sh | 10 + samples/chello/.gitignore | 1 + samples/chello/build.sh | 2 + samples/chello/chello.c | 19 ++ samples/chello/neotron.h | 468 ++++++++++++++++++++++++++++++++++++ 9 files changed, 547 insertions(+) create mode 100644 samples/asmhello/.gitignore create mode 100644 samples/asmhello/asmhello.S create mode 100755 samples/asmhello/build.sh create mode 100644 samples/chello/.gitignore create mode 100755 samples/chello/build.sh create mode 100644 samples/chello/chello.c create mode 100644 samples/chello/neotron.h diff --git a/samples/README.md b/samples/README.md index 13eef8f..1e726d4 100644 --- a/samples/README.md +++ b/samples/README.md @@ -41,3 +41,11 @@ This application panics, printing a nice panic message. ## [`fault`](./fault) This application generates a Hard Fault. + +## [`asmhello`](./asmhello) + +A basic "Hello, world" but written in ARM assembly. It prints the string "Hello, world" to *standard output* and then exits with an exit code of 0. + +## [`chello`](../chello) + +A basic "Hello, world" but written in C. It prints the string "Hello, world" to *standard output* and then exits with an exit code of 0. diff --git a/samples/asmhello/.gitignore b/samples/asmhello/.gitignore new file mode 100644 index 0000000..e494714 --- /dev/null +++ b/samples/asmhello/.gitignore @@ -0,0 +1,2 @@ +*.elf +*.o diff --git a/samples/asmhello/asmhello.S b/samples/asmhello/asmhello.S new file mode 100644 index 0000000..56f851e --- /dev/null +++ b/samples/asmhello/asmhello.S @@ -0,0 +1,35 @@ +.syntax unified +.thumb +.cpu cortex-m0plus +.global app_entry +.thumb_func + +.text +.section ".text" + +// r0 will contain a pointer to the syscall table +// the write function is the third entry (offset 8) +// and takes the arguments r0=handle, r1=pointer, r2=length +app_entry: + // Save registers + push {r0, r1, r2, lr} + // Fetch write function address + ldr r3, [r0, #8] + // Set up data length + movs r2, #13 + // Set up file handle + movs r0, #1 + // Set up data pointer + ldr r1, =message + // Call write function + blx r3 + // Set return value + movs r0, #0 + // Exit + pop {r1, r2, r3, pc} + +.data +.section ".rodata" + +message: +.ascii "Hello world!\n" diff --git a/samples/asmhello/build.sh b/samples/asmhello/build.sh new file mode 100755 index 0000000..de54b36 --- /dev/null +++ b/samples/asmhello/build.sh @@ -0,0 +1,2 @@ +#!/bin/sh +arm-none-eabi-gcc -mcpu=cortex-m0 -Wl,-T../neotron-cortex-m.ld -o asmhello.elf asmhello.S -nostdlib diff --git a/samples/build.sh b/samples/build.sh index 68fbc77..d0a82d4 100755 --- a/samples/build.sh +++ b/samples/build.sh @@ -24,6 +24,16 @@ cargo build echo "Building for ${TARGET}" cargo build --target ${TARGET} --release +pushd chello +./build.sh +popd + +pushd asmhello +./build.sh +popd + for program in panic hello fault input-test; do cp ./target/${TARGET}/release/${program} ./release/${program}.elf done +cp ./asmhello/asmhello.elf ./release +cp ./chello/chello.elf ./release diff --git a/samples/chello/.gitignore b/samples/chello/.gitignore new file mode 100644 index 0000000..5099253 --- /dev/null +++ b/samples/chello/.gitignore @@ -0,0 +1 @@ +*.elf diff --git a/samples/chello/build.sh b/samples/chello/build.sh new file mode 100755 index 0000000..439bd16 --- /dev/null +++ b/samples/chello/build.sh @@ -0,0 +1,2 @@ +#!/bin/sh +arm-none-eabi-gcc -Os -mcpu=cortex-m0 -Wl,-T../neotron-cortex-m.ld -o chello.elf chello.c -nostdlib diff --git a/samples/chello/chello.c b/samples/chello/chello.c new file mode 100644 index 0000000..04c2cc5 --- /dev/null +++ b/samples/chello/chello.c @@ -0,0 +1,19 @@ +/** + * Basic C sample which runs on Neotron OS. + */ + +#include "neotron.h" + +int app_entry(const NeotronApi *f) +{ + FfiByteSlice buffer = { + .data = "Hello, world", + .data_len = 12 + }; + Handle fd = { + // fd 1 is STDOUT + ._0 = 1 + }; + f->write(fd, buffer); + return 0; +} diff --git a/samples/chello/neotron.h b/samples/chello/neotron.h new file mode 100644 index 0000000..8f57c16 --- /dev/null +++ b/samples/chello/neotron.h @@ -0,0 +1,468 @@ +/** + * Header file for the Neoton OS API. + * + * Copyright (c) 2023 Jonathan Pallant and the Neotron Developers + * + * This file is licensed under the MIT or Apache 2.0 licences, at your option. + */ + +#include +#include +#include +#include + +/** + * Maximum length of a filename (with no directory components), including the + * extension. + */ +#define MAX_FILENAME_LEN 11 + +/** + * The character that separates one directory name from another directory name. + */ +#define Path_PATH_SEP '/' + +/** + * The character that separates drive specifiers from directories. + */ +#define Path_DRIVE_SEP ':' + +/** + * Describes how something has failed + */ +typedef enum Error +{ + /** + * The given file/directory path was not found + */ + NotFound, + /** + * Tried to write to a read-only file + */ + FileReadOnly, + /** + * Reached the end of the file + */ + EndOfFile, + /** + * The API has not been implemented + */ + Unimplemented, + /** + * An invalid argument was given to the API + */ + InvalidArg, + /** + * A bad handle was given to the API + */ + BadHandle, + /** + * An device-specific error occurred. Look at the BIOS source for more details. + */ + DeviceSpecific, + /** + * The OS does not have enough memory + */ + OutOfMemory, + /** + * The given path was invalid + */ + InvalidPath, +} Error; + +/** + * Represents an open directory + */ +typedef struct Handle +{ + uint8_t _0; +} Handle; + +typedef enum FfiResult_Tag +{ + /** + * The operation succeeded (like [`core::result::Result::Ok`]). + */ + FfiResult_Ok, + /** + * The operation failed (like [`core::result::Result::Err`]). + */ + FfiResult_Err, +} FfiResult_Tag; + +typedef struct FfiResult_Handle +{ + FfiResult_Tag tag; + union + { + struct + { + struct Handle ok; + }; + struct + { + enum Error err; + }; + }; +} FfiResult_Handle; + +/** + * A Rust u8 slice, but compatible with FFI. Assume the lifetime is only valid + * until the callee returns to the caller. + */ +typedef struct FfiByteSlice +{ + /** + * A pointer to the data + */ + const uint8_t *data; + /** + * The number of bytes we are pointing at + */ + uintptr_t data_len; +} FfiByteSlice; + +/** + * A Rust UTF-8 string, but compatible with FFI. + * + * Assume the lifetime is only valid until the callee returns to the caller. Is + * not null-terminated. + */ +typedef struct FfiString +{ + struct FfiByteSlice _0; +} FfiString; + +typedef struct FfiResult_usize +{ + FfiResult_Tag tag; + union + { + struct + { + uintptr_t ok; + }; + struct + { + enum Error err; + }; + }; +} FfiResult_usize; + +/** + * A Rust u8 mutable slice, but compatible with FFI. Assume the lifetime is + * only valid until the callee returns to the caller. + */ +typedef struct FfiBuffer +{ + /** + * A pointer to where the data can be put + */ + uint8_t *data; + /** + * The maximum number of bytes we can store in this buffer + */ + uintptr_t data_len; +} FfiBuffer; + +typedef struct FfiResult_u64 +{ + FfiResult_Tag tag; + union + { + struct + { + uint64_t ok; + }; + struct + { + enum Error err; + }; + }; +} FfiResult_u64; + +/** + * Represents an instant in time, in the local time zone. + */ +typedef struct Time +{ + /** + * Add 1970 to this file to get the calendar year + */ + uint8_t year_since_1970; + /** + * Add one to this value to get the calendar month + */ + uint8_t zero_indexed_month; + /** + * Add one to this value to get the calendar day + */ + uint8_t zero_indexed_day; + /** + * The number of hours past midnight + */ + uint8_t hours; + /** + * The number of minutes past the hour + */ + uint8_t minutes; + /** + * The number of seconds past the minute + */ + uint8_t seconds; +} Time; + +/** + * Describes a file on disk. + * + * This is set up for 8.3 filenames on MS-DOS FAT32 partitions currently. + */ +typedef struct Stat +{ + /** + * How big is this file + */ + uint64_t file_size; + /** + * When was the file created + */ + struct Time ctime; + /** + * When was the last modified + */ + struct Time mtime; + /** + * File attributes (Directory, Volume, etc) + */ + uint8_t attr; +} Stat; + +/** + * Describes an entry in a directory. + * + * This is set up for 8.3 filenames on MS-DOS FAT32 partitions currently. + */ +typedef struct Entry +{ + /** + * The name and extension of the file. + * + * The name and extension are separated by a single '.'. + * + * The filename will be in ASCII. Unicode filenames are not supported. + */ + uint8_t name[MAX_FILENAME_LEN]; + /** + * The properties for the file/directory this entry represents. + */ + struct Stat properties; +} Entry; + +typedef struct FfiResult_Entry +{ + FfiResult_Tag tag; + union + { + struct + { + struct Entry ok; + }; + struct + { + enum Error err; + }; + }; +} FfiResult_Entry; + +typedef struct FfiResult_Stat +{ + FfiResult_Tag tag; + union + { + struct + { + struct Stat ok; + }; + struct + { + enum Error err; + }; + }; +} FfiResult_Stat; + +typedef struct FfiResult_void +{ + FfiResult_Tag tag; + union + { + struct + { + enum Error err; + }; + }; +} FfiResult_void; + +/** + * The syscalls provided by the Neotron OS to a Neotron Application. + */ +typedef struct NeotronApi +{ + /** + * 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. + * + * # Limitations + * + * * You cannot open a file if it is currently open. + * * Paths must confirm to the rules for the filesystem for the given drive. + * * Relative paths are taken relative to the current directory (see `Api::chdir`). + */ + struct FfiResult_Handle (*open)(struct FfiString path, uint8_t flags); + /** + * Close a previously opened file. + * + * Closing a file is important, as only this action will cause the + * directory entry for the file to be updated. Crashing the system without + * closing a file may cause the directory entry to be incorrect, and you + * may need to run `CHKDSK` (or similar) on your disk to fix it. + */ + struct FfiResult_void (*close)(struct Handle fd); + /** + * Write to an open file handle, blocking until everything is written. + * + * Some files do not support writing and will produce an error. You will + * also get an error if you run out of disk space. + * + * The `buffer` is only borrowed for the duration of the function call and + * is then forgotten. + */ + struct FfiResult_void (*write)(struct Handle fd, struct FfiByteSlice buffer); + /** + * Read from an open file, returning how much was actually read. + * + * You might get less data than you asked for. If you do an `Api::read` and + * you are already at the end of the file you will get + * `Err(Error::EndOfFile)`. + * + * Data is stored to the given `buffer. The `buffer` is only borrowed for + * the duration of the function call and is then forgotten. + */ + struct FfiResult_usize (*read)(struct Handle fd, struct FfiBuffer buffer); + /** + * Move the file offset (for the given file handle) to the given position. + * + * Some files do not support seeking and will produce an error. + */ + struct FfiResult_void (*seek_set)(struct Handle fd, uint64_t position); + /** + * Move the file offset (for the given file handle) relative to the current position. + * + * Returns the new file offset. + * + * Some files do not support seeking and will produce an error. + */ + struct FfiResult_u64 (*seek_cur)(struct Handle fd, int64_t offset); + /** + * Move the file offset (for the given file handle) to the end of the file + * + * Returns the new file offset. + * + * Some files do not support seeking and will produce an error. + */ + struct FfiResult_u64 (*seek_end)(struct Handle fd); + /** + * Rename a file. + * + * # Limitations + * + * * You cannot rename a file if it is currently open. + * * You cannot rename a file where the `old_path` and the `new_path` are + * not on the same drive. + * * Paths must confirm to the rules for the filesystem for the given drive. + */ + struct FfiResult_void (*rename)(struct FfiString old_path, struct FfiString new_path); + /** + * Perform a special I/O control operation. + */ + struct FfiResult_u64 (*ioctl)(struct Handle fd, uint64_t command, uint64_t value); + /** + * Open a directory, given a path as a UTF-8 string. + */ + struct FfiResult_Handle (*opendir)(struct FfiString path); + /** + * Close a previously opened directory. + */ + struct FfiResult_void (*closedir)(struct Handle dir); + /** + * Read from an open directory + */ + struct FfiResult_Entry (*readdir)(struct Handle dir); + /** + * Get information about a file. + */ + struct FfiResult_Stat (*stat)(struct FfiString path); + /** + * Get information about an open file. + */ + struct FfiResult_Stat (*fstat)(struct Handle fd); + /** + * Delete a file. + * + * # Limitations + * + * * You cannot delete a file if it is currently open. + */ + struct FfiResult_void (*deletefile)(struct FfiString path); + /** + * Delete a directory. + * + * # Limitations + * + * * You cannot delete a root directory. + * * You cannot delete a directory that has any files or directories in it. + */ + struct FfiResult_void (*deletedir)(struct FfiString path); + /** + * Change the current directory. + * + * Relative file paths (e.g. passed to `Api::open`) 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. + */ + struct FfiResult_void (*chdir)(struct FfiString path); + /** + * Change the current directory to the given open directory. + * + * Unlike on MS-DOS, there is only one current directory for the whole + * system, not one per drive. + */ + struct FfiResult_void (*dchdir)(struct Handle dir); + /** + * Get the current directory. + * + * The current directory is stored as UTF-8 into the given buffer. The + * function returns the number of bytes written to the buffer, or an error. + * If the function did not return an error, the buffer can be assumed to + * contain a valid file path. That path will not be null terminated. + */ + struct FfiResult_usize (*pwd)(struct FfiBuffer path); + /** + * Allocate some memory. + * + * * `size` - the number of bytes required + * * `alignment` - the returned address will have this alignment, or + * better. For example, pass `4` if you are allocating an array of `u32`. + */ + struct FfiResult_void (*malloc)(uintptr_t size, uintptr_t alignment); + /** + * Free some previously allocated memory. + * + * You must pass the same `size` and `alignment` values that you passed to `malloc`. + */ + void (*free)(void *ptr, uintptr_t size, uintptr_t alignment); +} NeotronApi; From 478840899d64091e8b56fdc23a9210c402bafcd6 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sun, 12 Nov 2023 19:10:46 +0000 Subject: [PATCH 28/37] Install Arm GCC. --- .github/workflows/build.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 51b739d..dfe825e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,6 +23,10 @@ jobs: rustup component add llvm-tools cargo install cargo-binutils + - name: Add targets + run: | + sudo apt-get -y install gcc-arm-none-eabi binutils-arm-none-eabi + - name: Build lib (native) run: | cargo build --verbose @@ -31,7 +35,7 @@ jobs: run: | cargo test --verbose - - name: Build samples (Cortex-M0) + - name: Build samples (Cortex-M0+) run: | cd samples && ./build.sh thumbv6m-none-eabi From e6bad5ed711da57471a963673458e77ba6c7b18b Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Tue, 14 Nov 2023 19:56:55 +0000 Subject: [PATCH 29/37] Make artefacts in CI --- .github/workflows/build.yml | 44 ++++++++++++++++++++++++++++++++++--- describe.sh | 23 +++++++++++++++++++ 2 files changed, 64 insertions(+), 3 deletions(-) create mode 100755 describe.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dfe825e..f0c8902 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,6 +27,12 @@ jobs: run: | sudo apt-get -y install gcc-arm-none-eabi binutils-arm-none-eabi + - name: Find slug name + run: | + slug=$(./describe.sh "${GITHUB_REF}") + echo "Building with slug '${slug}'" + echo "slug=${slug}" >> "${GITHUB_ENV}" + - name: Build lib (native) run: | cargo build --verbose @@ -37,12 +43,44 @@ jobs: - name: Build samples (Cortex-M0+) run: | - cd samples && ./build.sh thumbv6m-none-eabi + cd samples + ./build.sh thumbv6m-none-eabi + mv release release-thumbv6m-none-eabi - name: Build samples (Cortex-M3) run: | - cd samples && ./build.sh thumbv7m-none-eabi + cd samples + ./build.sh thumbv7m-none-eabi + mv release release-thumbv7m-none-eabi - name: Build samples (Cortex-M4) run: | - cd samples && ./build.sh thumbv7em-none-eabi + cd samples + ./build.sh thumbv7em-none-eabi + mv release release-thumbv7em-none-eabi + + - name: Assemble Artifacts + run: | + echo "Making ./neotron-sdk-${{ env.slug }}..." + mkdir -p ./neotron-sdk-${{ env.slug }}/samples + mv ./samples/release-thumbv6m-none-eabi ./neotron-sdk-${{ env.slug }}/samples/thumbv6m-none-eabi + mv ./samples/release-thumbv7m-none-eabi ./neotron-sdk-${{ env.slug }}/samples/thumbv7m-none-eabi + mv ./samples/release-thumbv7em-none-eabi ./neotron-sdk-${{ env.slug }}/samples/thumbv7em-none-eabi + echo "Compressing ./neotron-sdk-${{ env.slug }}.zip..." + zip -r ./neotron-sdk-${{ env.slug }}.zip ./neotron-sdk-${{ env.slug }} + + - name: Upload Artifacts + uses: actions/upload-artifact@v3 + if: ${{success()}} + with: + name: Artifacts + if-no-files-found: error + path: | + ./neotron-sdk-*/ + + - name: Create and Upload Release + if: github.event_name == 'push' && startswith(github.ref, 'refs/tags/') + id: create_release + uses: ncipollo/release-action@v1 + with: + artifacts: ./neotron-sdk-${{ env.slug }}.zip diff --git a/describe.sh b/describe.sh new file mode 100755 index 0000000..eb014ac --- /dev/null +++ b/describe.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Copyright (c) Ferrous Systems, 2023 +# +# This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License. + +set -euo pipefail + +GIVEN_REF=$1 + +case "${GIVEN_REF}" in + refs/heads/*) + slug="$(git branch --show)-$(git rev-parse --short HEAD)" + ;; + refs/tags/*) + slug="$(echo "${GIVEN_REF}" | awk '{split($0,a,"/"); print a[3]}')" + ;; + refs/pull/*/merge) + slug="pr-$(echo "${GIVEN_REF}" | awk '{split($0,a,"/"); print a[3]}')-$(git rev-parse --short HEAD)" + ;; +esac + +echo "${slug}" From 76b69b579eae6319feca99449b9ff568189939b1 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Tue, 14 Nov 2023 22:17:52 +0000 Subject: [PATCH 30/37] Improve ASM and C examples. Added more comments to explain what was going on. I also expanded the C example to use newlib and show how malloc works. --- samples/asmhello/asmhello.S | 55 +++++++++++++++++++++++++++---------- samples/asmhello/build.sh | 8 +++++- samples/chello/.gitignore | 2 ++ samples/chello/build.sh | 22 ++++++++++++++- samples/chello/chello.c | 44 +++++++++++++++++++++-------- samples/chello/stubs.c | 35 +++++++++++++++++++++++ samples/neotron-cortex-m.ld | 17 ++++++------ 7 files changed, 146 insertions(+), 37 deletions(-) create mode 100644 samples/chello/stubs.c diff --git a/samples/asmhello/asmhello.S b/samples/asmhello/asmhello.S index 56f851e..a3b3b2b 100644 --- a/samples/asmhello/asmhello.S +++ b/samples/asmhello/asmhello.S @@ -1,35 +1,60 @@ +/** + * Basic ARM Unified Assembly Language sample which runs on Neotron OS. + */ + .syntax unified -.thumb -.cpu cortex-m0plus -.global app_entry -.thumb_func + +// +// Functions (.text) +// .text -.section ".text" -// r0 will contain a pointer to the syscall table -// the write function is the third entry (offset 8) -// and takes the arguments r0=handle, r1=pointer, r2=length +// Entry point for our application. +// +// * r0 will contain a pointer to the syscall table. +// * The write function is the third entry (offset 8) +// and takes the arguments r0=handle, r1=pointer, r2=length +.thumb_func +.global app_entry +.func app_entry app_entry: - // Save registers - push {r0, r1, r2, lr} + // "A subroutine must preserve the contents of the registers r4-r11 + // and SP" - and we only use R4 + push {r4, lr} // Fetch write function address ldr r3, [r0, #8] - // Set up data length - movs r2, #13 // Set up file handle movs r0, #1 // Set up data pointer ldr r1, =message + // Set up data length + ldr r4, =message_len + ldr r2, [r4] // Call write function blx r3 // Set return value movs r0, #0 // Exit - pop {r1, r2, r3, pc} + pop {r4, pc} +.endfunc + +// +// Read Only Data (.rodata) +// -.data -.section ".rodata" +.section .rodata +// The message we want to print +.type message,%object message: .ascii "Hello world!\n" + +// The length of the string with the label `message` +// +// Must come immediately after `message:` +.type message_len,%object +message_len: +.long . - message + +// End of file diff --git a/samples/asmhello/build.sh b/samples/asmhello/build.sh index de54b36..84cffd7 100755 --- a/samples/asmhello/build.sh +++ b/samples/asmhello/build.sh @@ -1,2 +1,8 @@ #!/bin/sh -arm-none-eabi-gcc -mcpu=cortex-m0 -Wl,-T../neotron-cortex-m.ld -o asmhello.elf asmhello.S -nostdlib +arm-none-eabi-gcc \ + -nostartfiles \ + -ffreestanding \ + -mcpu=cortex-m0plus \ + -Wl,-T../neotron-cortex-m.ld \ + -o asmhello.elf \ + asmhello.S \ diff --git a/samples/chello/.gitignore b/samples/chello/.gitignore index 5099253..d68e16e 100644 --- a/samples/chello/.gitignore +++ b/samples/chello/.gitignore @@ -1 +1,3 @@ *.elf +*.o + diff --git a/samples/chello/build.sh b/samples/chello/build.sh index 439bd16..da7cc4c 100755 --- a/samples/chello/build.sh +++ b/samples/chello/build.sh @@ -1,2 +1,22 @@ #!/bin/sh -arm-none-eabi-gcc -Os -mcpu=cortex-m0 -Wl,-T../neotron-cortex-m.ld -o chello.elf chello.c -nostdlib + +arm-none-eabi-gcc \ + -fdata-sections \ + -ffreestanding \ + -ffunction-sections \ + -flto \ + -mcpu=cortex-m0 \ + -nostartfiles \ + -Os \ + -Wall \ + -Wconversion \ + -Wdouble-promotion \ + -Wextra \ + -Wl,-gc-sections \ + -Wl,-T../neotron-cortex-m.ld \ + -Wshadow \ + --specs=nano.specs \ + --specs=nosys.specs \ + -o chello.elf \ + stubs.c \ + chello.c diff --git a/samples/chello/chello.c b/samples/chello/chello.c index 04c2cc5..8340db0 100644 --- a/samples/chello/chello.c +++ b/samples/chello/chello.c @@ -3,17 +3,37 @@ */ #include "neotron.h" +#include +#include +#include -int app_entry(const NeotronApi *f) -{ - FfiByteSlice buffer = { - .data = "Hello, world", - .data_len = 12 - }; - Handle fd = { - // fd 1 is STDOUT - ._0 = 1 - }; - f->write(fd, buffer); - return 0; +const NeotronApi *g_api; + +static int main(void); + +/* + * Called by Neotron OS when the binary is 'run'. + */ +int app_entry(const NeotronApi *f) { + g_api = f; + return main(); } + +/* + * Our main function. + * + * Just prints a message and exits. + */ +static int main(void) { + // allocate a buffer + void *buffer = calloc(1024, 1); + // write a string into it + snprintf(buffer, 1023, "Hello, world!\n"); + // print the buffer + printf(buffer); + // free the buffer + free(buffer); + return 0; +} + +// End of file diff --git a/samples/chello/stubs.c b/samples/chello/stubs.c new file mode 100644 index 0000000..8bfe574 --- /dev/null +++ b/samples/chello/stubs.c @@ -0,0 +1,35 @@ +/* + * Newlib stub functions we need. + */ + +#include +#include +#include + +#include "neotron.h" + +extern const NeotronApi *g_api; + +/* + * Implementation of the newlib library syscall `write` + */ +int _write(int fd, const void *data, size_t count) { + if (fd >= 255) { + return -1; + } + FfiByteSlice buffer = { + .data = data, + .data_len = count, + }; + Handle neo_fd = { + ._0 = (uint8_t)fd, + }; + FfiResult_void result = g_api->write(neo_fd, buffer); + if (result.tag == FfiResult_Ok) { + return (int)count; + } else { + return -1; + } +} + +// End of file diff --git a/samples/neotron-cortex-m.ld b/samples/neotron-cortex-m.ld index 205e921..d1b48c5 100644 --- a/samples/neotron-cortex-m.ld +++ b/samples/neotron-cortex-m.ld @@ -20,6 +20,7 @@ MEMORY } /* # Entry point = what the BIOS calls to start the OS */ +EXTERN(app_entry); ENTRY(app_entry); /* # Sections */ @@ -31,7 +32,7 @@ SECTIONS . = ALIGN(4); *(.text .text.*); . = ALIGN(4); - } + } > RAM /* ### .rodata */ .rodata : ALIGN(4) @@ -39,7 +40,7 @@ SECTIONS . = ALIGN(4); *(.rodata .rodata.*); . = ALIGN(4); - } + } > RAM /* ### .data */ .data : ALIGN(4) @@ -47,10 +48,7 @@ SECTIONS . = ALIGN(4); *(.data .data.*); . = ALIGN(4); - } - - /* LMA of .data */ - __sidata = LOADADDR(.data); + } > RAM /* ### .bss */ .bss : ALIGN(4) @@ -58,7 +56,7 @@ SECTIONS . = ALIGN(4); *(.bss .bss.*); . = ALIGN(4); - } + } > RAM /* ### .uninit */ .uninit (NOLOAD) : ALIGN(4) @@ -66,7 +64,10 @@ SECTIONS . = ALIGN(4); *(.uninit .uninit.*); . = ALIGN(4); - } + } > RAM + + . = ALIGN(4); + end = .; /* ## .got */ /* Dynamic relocations are unsupported. This section is only used to detect relocatable code in From 44398399199adc27112f9c2bf781dcf805497181 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Tue, 14 Nov 2023 22:38:09 +0000 Subject: [PATCH 31/37] Build C and ASM demo for specified target --- samples/asmhello/build.sh | 20 ++++++++++++++++++-- samples/build.sh | 4 ++-- samples/chello/build.sh | 19 +++++++++++++++++-- 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/samples/asmhello/build.sh b/samples/asmhello/build.sh index 84cffd7..6b68aa9 100755 --- a/samples/asmhello/build.sh +++ b/samples/asmhello/build.sh @@ -1,8 +1,24 @@ -#!/bin/sh +#!/bin/bash + +set -euo pipefail + +TARGET=${1:-thumbv6m-none-eabi} + +if [ "$TARGET" == "thumbv6m-none-eabi" ]; then + CPU="cortex-m0plus" +elif [ "$TARGET" == "thumbv7m-none-eabi" ]; then + CPU="cortex-m3" +elif [ "$TARGET" == "thumbv7em-none-eabi" ]; then + CPU="cortex-m4" +else + echo "Unknown target" + exit 1 +fi + arm-none-eabi-gcc \ -nostartfiles \ -ffreestanding \ - -mcpu=cortex-m0plus \ + -mcpu=$CPU \ -Wl,-T../neotron-cortex-m.ld \ -o asmhello.elf \ asmhello.S \ diff --git a/samples/build.sh b/samples/build.sh index d0a82d4..4fc0f6b 100755 --- a/samples/build.sh +++ b/samples/build.sh @@ -25,11 +25,11 @@ echo "Building for ${TARGET}" cargo build --target ${TARGET} --release pushd chello -./build.sh +./build.sh ${TARGET} popd pushd asmhello -./build.sh +./build.sh ${TARGET} popd for program in panic hello fault input-test; do diff --git a/samples/chello/build.sh b/samples/chello/build.sh index da7cc4c..2813c8b 100755 --- a/samples/chello/build.sh +++ b/samples/chello/build.sh @@ -1,11 +1,26 @@ -#!/bin/sh +#!/bin/bash + +set -euo pipefail + +TARGET=${1:-thumbv6m-none-eabi} + +if [ "$TARGET" == "thumbv6m-none-eabi" ]; then + CPU="cortex-m0plus" +elif [ "$TARGET" == "thumbv7m-none-eabi" ]; then + CPU="cortex-m3" +elif [ "$TARGET" == "thumbv7em-none-eabi" ]; then + CPU="cortex-m4" +else + echo "Unknown target" + exit 1 +fi arm-none-eabi-gcc \ -fdata-sections \ -ffreestanding \ -ffunction-sections \ -flto \ - -mcpu=cortex-m0 \ + -mcpu=$CPU \ -nostartfiles \ -Os \ -Wall \ From 08df652a3590a7093c6711fd37086559599aa67e Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Fri, 12 Apr 2024 12:08:25 +0100 Subject: [PATCH 32/37] Support CLI arguments. --- Cargo.toml | 2 +- samples/chello/chello.c | 17 +++++------------ src/lib.rs | 42 +++++++++++++++++++++++++++++++++++++---- 3 files changed, 44 insertions(+), 17 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7ee0af7..f69ff1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ authors = ["Jonathan 'theJPster' Pallant "] [dependencies] neotron-ffi = "0.1" -neotron-api = "0.1" +neotron-api = "0.2" [target.'cfg(unix)'.dependencies] crossterm = "0.26" diff --git a/samples/chello/chello.c b/samples/chello/chello.c index 8340db0..ac39602 100644 --- a/samples/chello/chello.c +++ b/samples/chello/chello.c @@ -9,22 +9,11 @@ const NeotronApi *g_api; -static int main(void); - /* * Called by Neotron OS when the binary is 'run'. */ -int app_entry(const NeotronApi *f) { +int app_entry(const NeotronApi *f, size_t argc, const FfiString* argv) { g_api = f; - return main(); -} - -/* - * Our main function. - * - * Just prints a message and exits. - */ -static int main(void) { // allocate a buffer void *buffer = calloc(1024, 1); // write a string into it @@ -33,6 +22,10 @@ static int main(void) { printf(buffer); // free the buffer free(buffer); + for(size_t i = 0; i < argc; i++) { + const FfiString* ffi_arg = &argv[i]; + printf("Arg %u: %.*s\n", (unsigned int) i, ffi_arg->_0.data_len, ffi_arg->_0.data); + } return 0; } diff --git a/src/lib.rs b/src/lib.rs index f47d4b1..adb6ea6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,7 @@ // Imports // ============================================================================ -use core::sync::atomic::{AtomicPtr, Ordering}; +use core::sync::atomic::{AtomicPtr, AtomicUsize, Ordering}; pub use neotron_ffi::{FfiBuffer, FfiByteSlice, FfiString}; @@ -46,6 +46,12 @@ extern "C" { /// Once you've hit the application `main()`, this will be non-null. static API: AtomicPtr = AtomicPtr::new(core::ptr::null_mut()); +/// Number of arguments passed +static ARG_COUNT: AtomicUsize = AtomicUsize::new(0); + +/// Start of the argument list +static ARG_PTR: AtomicPtr = AtomicPtr::new(core::ptr::null_mut()); + // ============================================================================ // Types // ============================================================================ @@ -53,7 +59,7 @@ static API: AtomicPtr = AtomicPtr::new(core::ptr::null_mut()); /// The type of the application entry-point. /// /// The OS calls a function of this type. -pub type AppStartFn = extern "C" fn(*mut crate::Api) -> i32; +pub use neotron_api::AppStartFn; /// The result type for any SDK function. /// @@ -259,11 +265,39 @@ impl Drop for ReadDir { /// Will jump to the application entry point, and `extern "C"` function /// called `main`. #[no_mangle] -extern "C" fn app_entry(api: *mut Api) -> i32 { - API.store(api, Ordering::Relaxed); +extern "C" fn app_entry(api: *const Api, argc: usize, argv: *const FfiString) -> i32 { + let _check: AppStartFn = app_entry; + API.store(api as *mut Api, Ordering::Relaxed); + ARG_COUNT.store(argc, Ordering::Relaxed); + ARG_PTR.store(argv as *mut FfiString, Ordering::Relaxed); unsafe { neotron_main() } } +/// Get a command line argument. +/// +/// Given an zero-based index, returns `Some(str)` if that argument was +/// provided, otherwise None. +/// +/// Does not return the name of the program in the first argument. +#[cfg(target_os = "none")] +pub fn arg(n: usize) -> Option<&'static str> { + let arg_count = ARG_COUNT.load(Ordering::Relaxed); + let arg_ptr = ARG_PTR.load(Ordering::Relaxed); + let arg_slice = unsafe { core::slice::from_raw_parts(arg_ptr, arg_count) }; + arg_slice.get(n).map(|ffi| ffi.as_str()) +} + +/// Get a command line argument. +/// +/// Given an zero-based index, returns `Some(str)` if that argument was +/// provided, otherwise None. +/// +/// Does not return the name of the program in the first argument. +#[cfg(not(target_os = "none"))] +pub fn arg(n: usize) -> Option { + std::env::args().skip(1).nth(n) +} + /// Get information about a file on disk. pub fn stat(_path: path::Path) -> Result { todo!() From 8e2793e423aad995add4d8bef247f19f1b4b346b Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Fri, 12 Apr 2024 12:08:51 +0100 Subject: [PATCH 33/37] Removed deleted project from vscode settings --- .vscode/settings.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 39aa490..2427850 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,7 +4,6 @@ "./samples/panic/Cargo.toml", "./samples/hello/Cargo.toml", "./samples/fault/Cargo.toml", - "./samples/snake/Cargo.toml", "./Cargo.toml" - ] + ], } \ No newline at end of file From a6803549fee2896c4bbd599062009fa16ee10eda Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Fri, 12 Apr 2024 15:44:49 +0100 Subject: [PATCH 34/37] Don't let stdin/out/err close on drop. --- src/lib.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index adb6ea6..0c472f1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -207,12 +207,17 @@ impl File { } } -impl core::ops::Drop for File { +impl Drop for File { fn drop(&mut self) { let api = get_api(); - // We could panic on error, but let's silently ignore it for now. - // If you care, call `file.close()`. + // Don't close default (in, out, err) handles on drop because we can't + // re-open them. + if self.0.value() <= 2 { + // don't close + } else { + // close it let _ = (api.close)(self.0); + } } } From 6252024ef064de8f997c9e46da44b08c126fd8b1 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Fri, 12 Apr 2024 15:58:33 +0100 Subject: [PATCH 35/37] Add hexdump demo. --- samples/Cargo.toml | 1 + samples/build.sh | 2 +- samples/hexdump/Cargo.toml | 12 +++++++ samples/hexdump/README.md | 20 +++++++++++ samples/hexdump/src/main.rs | 72 +++++++++++++++++++++++++++++++++++++ src/lib.rs | 47 ++++++++++++++++++++++-- 6 files changed, 151 insertions(+), 3 deletions(-) create mode 100644 samples/hexdump/Cargo.toml create mode 100644 samples/hexdump/README.md create mode 100644 samples/hexdump/src/main.rs diff --git a/samples/Cargo.toml b/samples/Cargo.toml index 6d844a1..8a5a153 100644 --- a/samples/Cargo.toml +++ b/samples/Cargo.toml @@ -2,6 +2,7 @@ members = [ "fault", "hello", + "hexdump", "input-test", "panic", ] diff --git a/samples/build.sh b/samples/build.sh index 4fc0f6b..0832190 100755 --- a/samples/build.sh +++ b/samples/build.sh @@ -32,7 +32,7 @@ pushd asmhello ./build.sh ${TARGET} popd -for program in panic hello fault input-test; do +for program in panic hello fault input-test hexdump; do cp ./target/${TARGET}/release/${program} ./release/${program}.elf done cp ./asmhello/asmhello.elf ./release diff --git a/samples/hexdump/Cargo.toml b/samples/hexdump/Cargo.toml new file mode 100644 index 0000000..dbf0ada --- /dev/null +++ b/samples/hexdump/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "hexdump" +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" +authors = ["Jonathan 'theJPster' Pallant "] +description = "Prints information about files" + +[dependencies] +neotron-sdk = { path = "../.." } + +# See workspace for profile settings diff --git a/samples/hexdump/README.md b/samples/hexdump/README.md new file mode 100644 index 0000000..92fd457 --- /dev/null +++ b/samples/hexdump/README.md @@ -0,0 +1,20 @@ +# Hexdump + +A basic file printing tool for Neotron systems. This program takes an argument +which is assumed to be a filename. That file is opened, and printed out as hex. +The output is similar to the UNIX command `hexdump -C`. + +See the general sample application [README](../README.md) for compilation instructions. + +## Licence + +Copyright (c) The Neotron Developers, 2024 + +Licensed under either [MIT](../../LICENSE-MIT) or [Apache-2.0](../../LICENSE-APACHE) at +your option. + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you shall be licensed as above, without any +additional terms or conditions. diff --git a/samples/hexdump/src/main.rs b/samples/hexdump/src/main.rs new file mode 100644 index 0000000..2e985e7 --- /dev/null +++ b/samples/hexdump/src/main.rs @@ -0,0 +1,72 @@ +#![cfg_attr(target_os = "none", no_std)] +#![cfg_attr(target_os = "none", no_main)] + +use core::fmt::Write; + +#[cfg(not(target_os = "none"))] +fn main() { + neotron_sdk::init(); +} + +#[no_mangle] +extern "C" fn neotron_main() -> i32 { + if let Err(e) = real_main() { + let mut stdout = neotron_sdk::stdout(); + let _ = writeln!(stdout, "Error: {:?}", e); + 1 + } else { + 0 + } +} + +fn real_main() -> Result<(), neotron_sdk::Error> { + let mut stdout = neotron_sdk::stdout(); + let Some(filename) = neotron_sdk::arg(0) else { + return Err(neotron_sdk::Error::InvalidArg); + }; + let _ = writeln!(stdout, "Dumping {:?}...", filename); + let path = neotron_sdk::path::Path::new(&filename)?; + let f = neotron_sdk::File::open(path, neotron_sdk::Flags::empty())?; + let stat = f.stat()?; + let mut bytes_remaining = stat.file_size; + let _ = writeln!(stdout, "File is {} bytes", bytes_remaining); + + let mut lines_remaining = 24; + let mut buffer = [0u8; 16]; + let mut addr = 0; + while bytes_remaining > 0 { + let this_time = f.read(&mut buffer)?; + let valid = &buffer[0..this_time]; + // print address + let _ = write!(stdout, "{:08x}: ", addr); + // print bytes (with padding) + for b in valid { + let _ = write!(stdout, "{:02x} ", b); + } + for _padding in 0..(buffer.len() - valid.len()) { + let _ = write!(stdout, ".. "); + } + let _ = write!(stdout, "| "); + // print ascii (with padding) + for b in valid { + let ch = *b as char; + let _ = write!(stdout, "{}", if !ch.is_control() { ch } else { '?' }); + } + for _padding in 0..(buffer.len() - valid.len()) { + let _ = write!(stdout, "."); + } + let _ = writeln!(stdout, "|"); + addr += this_time; + bytes_remaining = bytes_remaining.saturating_sub(this_time as u64); + if lines_remaining == 0 { + if neotron_sdk::wait_for_key() == neotron_sdk::WaitForKey::Quit { + break; + } + lines_remaining = 25; + } else { + lines_remaining -= 1; + } + } + + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index 0c472f1..75822d4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,7 +12,7 @@ use core::sync::atomic::{AtomicPtr, AtomicUsize, Ordering}; pub use neotron_ffi::{FfiBuffer, FfiByteSlice, FfiString}; -pub use neotron_api::{path, Api, Error}; +pub use neotron_api::{file::Flags, path, Api, Error}; use neotron_api as api; @@ -216,7 +216,7 @@ impl Drop for File { // don't close } else { // close it - let _ = (api.close)(self.0); + let _ = (api.close)(self.0); } } } @@ -372,6 +372,49 @@ pub fn delay(period: core::time::Duration) { std::thread::sleep(period); } +/// The result of a *Wait for Key* operation. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum WaitForKey { + /// User wants more data + More, + /// User wants to quit + Quit, +} + +/// Wait for a key +pub fn wait_for_key() -> WaitForKey { + use core::fmt::Write; + let mut ticker = "|/-\\".chars().cycle(); + let stdin = stdin(); + let mut stdout = stdout(); + let result = loop { + let _ = write!( + stdout, + "\rPress Space for more, 'q' to quit... {}", + ticker.next().unwrap() + ); + let mut buffer = [0u8; 1]; + match stdin.read(&mut buffer) { + Ok(0) => { + // No data + } + Ok(_n) => { + if buffer[0] == b' ' { + break WaitForKey::More; + } else if buffer[0] == b'q' || buffer[0] == b'Q' { + break WaitForKey::Quit; + } + } + Err(e) => { + let _ = writeln!(stdout, "Error {:?}", e); + break WaitForKey::Quit; + } + } + }; + let _ = write!(stdout, "\r \r"); + result +} + static RAND_STATE: core::sync::atomic::AtomicU16 = core::sync::atomic::AtomicU16::new(0); /// Seed the 16-bit psuedorandom number generator From 5f14e87116bc4a50dbb6360026ad769123076615 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sun, 19 May 2024 12:46:47 +0100 Subject: [PATCH 36/37] Update the docs. Preparing for a release. --- CHANGELOG.md | 5 ++++ README.md | 22 +++++++++++++--- samples/README.md | 6 ----- src/lib.rs | 65 ++++++++++++++++++++++++++++++++++------------- 4 files changed, 71 insertions(+), 27 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..40966b9 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Change Log + +## Unreleased changes ([Source](https://github.com/neotron-compute/neotron-sdk/tree/develop)) + +First version. Supports cross-platform terminal output and some basic file APIs. diff --git a/README.md b/README.md index 0a7b80e..18aeff3 100644 --- a/README.md +++ b/README.md @@ -3,15 +3,28 @@ The Neotron SDK defines the API that applications receive when they run on the [Neotron OS](https://github.com/neotron-compute/neotron-os). -## Changelog +You should use this crate when writing applications that run on Neotron OS. + +This SDK attempts to detect targets that support UNIX or Windows, and implements +some code to talk to the appropriate UNIX or Windows API. This allows some level +of portable, mainly to support application testing on those OSes. -### Unreleased Changes +On a *bare-metal* target (i.e. where the OS is `none`), the SDK expects the +Neotron OS to pass the callback table to the entry point (`app_entry()`). Once +initialised, the SDK then expects you application to provide an `extern "C"` +`no-mangle` function called `neotron_main`, which the SDK will call. -* First Version +## Samples + +Some [sample](./samples/README.md) applications are provided with this SDK. + +## Changelog + +See [CHANGELOG.md](./CHANGELOG.md) ## Licence -Copyright (c) The Neotron Developers, 2022 +Copyright (c) The Neotron Developers, 2024 Licensed under either [MIT](./LICENSE-MIT) or [Apache-2.0](./LICENSE-APACHE) at your option. @@ -21,3 +34,4 @@ your option. Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you shall be licensed as above, without any additional terms or conditions. + diff --git a/samples/README.md b/samples/README.md index 1e726d4..3c96309 100644 --- a/samples/README.md +++ b/samples/README.md @@ -18,12 +18,6 @@ Then copy the resulting `hello.elf` file to an SD card and insert it into your N > run ``` -If you don't have `rust-objcopy` installed, install it with: - -```console -$ rustup component add llvm-tools -``` - ## List of Sample Applications ## [`hello`](./hello) diff --git a/src/lib.rs b/src/lib.rs index 75822d4..03b16ba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,19 @@ //! The Neotron SDK //! //! Defines the API supplied to applications that run on Neotron OS +//! +//! You should use this crate when writing applications that run on Neotron OS. +//! +//! This SDK attempts to detect targets that support UNIX or Windows, and +//! implements some code to talk to the appropriate UNIX or Windows API. This +//! allows some level of portable, mainly to support application testing on +//! those OSes. +//! +//! On a *bare-metal* target (i.e. where the OS is `none`), the SDK expects the +//! Neotron OS to pass the callback table to the entry point +//! ([`app_entry()`](app_entry)). Once initialised, the SDK then expects you +//! application to provide an `extern "C"` `no-mangle` function called +//! `neotron_main`, which the SDK will call. #![cfg_attr(target_os = "none", no_std)] @@ -52,6 +65,9 @@ static ARG_COUNT: AtomicUsize = AtomicUsize::new(0); /// Start of the argument list static ARG_PTR: AtomicPtr = AtomicPtr::new(core::ptr::null_mut()); +/// Random number generator state +static RAND_STATE: core::sync::atomic::AtomicU16 = core::sync::atomic::AtomicU16::new(0); + // ============================================================================ // Types // ============================================================================ @@ -261,16 +277,25 @@ impl Drop for ReadDir { } } +/// The result of a *Wait for Key* operation. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum WaitForKey { + /// User wants more data + More, + /// User wants to quit + Quit, +} + // ============================================================================ // Functions // ============================================================================ /// The function the OS calls to start the application. /// -/// Will jump to the application entry point, and `extern "C"` function -/// called `main`. +/// Will initialise the SDK and then jump to the application entry point, which +/// is an `extern "C"` function called `neotron_main`. #[no_mangle] -extern "C" fn app_entry(api: *const Api, argc: usize, argv: *const FfiString) -> i32 { +pub extern "C" fn app_entry(api: *const Api, argc: usize, argv: *const FfiString) -> i32 { let _check: AppStartFn = app_entry; API.store(api as *mut Api, Ordering::Relaxed); ARG_COUNT.store(argc, Ordering::Relaxed); @@ -304,21 +329,29 @@ pub fn arg(n: usize) -> Option { } /// Get information about a file on disk. +/// +/// **Note:** This function is not implemented currently. pub fn stat(_path: path::Path) -> Result { todo!() } /// Delete a file from disk +/// +/// **Note:** This function is not implemented currently. pub fn delete(_path: path::Path) -> Result<()> { todo!() } /// Change the current working directory to the given path. +/// +/// **Note:** This function is not implemented currently. pub fn chdir(_path: path::Path) -> Result<()> { todo!() } /// Change the current working directory to that given by the handle. +/// +/// **Note:** This function is not implemented currently. pub fn dchdir(_dir: api::dir::Handle) -> Result<()> { todo!() } @@ -331,11 +364,15 @@ pub fn pwd)>(callback: F) { } /// Alllocate some memory +/// +/// **Note:** This function is not implemented currently. pub fn malloc(_size: usize, _alignment: usize) -> Result<*mut core::ffi::c_void> { todo!() } /// Free some previously allocated memory. +/// +/// **Note:** This function is not implemented currently. pub fn free(_ptr: *mut core::ffi::c_void, _size: usize, _alignment: usize) { todo!() } @@ -355,10 +392,12 @@ pub const fn stderr() -> File { File(api::file::Handle::new_stderr()) } -/// Delay for some milliseconds +/// Delay for some given duration before returning. +/// +/// Currently this does a badly calibrated nop busy-wait. #[cfg(target_os = "none")] pub fn delay(period: core::time::Duration) { - // TODO: sleep on real hardware? + // TODO: call OS sleep API? for _ in 0..period.as_micros() { for _ in 0..50 { unsafe { core::arch::asm!("nop") } @@ -366,22 +405,16 @@ pub fn delay(period: core::time::Duration) { } } -/// Delay for some milliseconds +/// Delay for some given duration before returning. #[cfg(not(target_os = "none"))] pub fn delay(period: core::time::Duration) { std::thread::sleep(period); } -/// The result of a *Wait for Key* operation. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum WaitForKey { - /// User wants more data - More, - /// User wants to quit - Quit, -} - /// Wait for a key +/// +/// Prints `Press Space for more, 'q' to quit...` with a spinner, and waits +/// for you to press the appropriate key. pub fn wait_for_key() -> WaitForKey { use core::fmt::Write; let mut ticker = "|/-\\".chars().cycle(); @@ -415,8 +448,6 @@ pub fn wait_for_key() -> WaitForKey { result } -static RAND_STATE: core::sync::atomic::AtomicU16 = core::sync::atomic::AtomicU16::new(0); - /// Seed the 16-bit psuedorandom number generator pub fn srand(seed: u16) { RAND_STATE.store(seed, core::sync::atomic::Ordering::Relaxed); From 107cbd482cef3527407473d678abad98b3d21edb Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sun, 19 May 2024 12:48:20 +0100 Subject: [PATCH 37/37] Update CHANGELOG for release. --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40966b9..bbbacf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log -## Unreleased changes ([Source](https://github.com/neotron-compute/neotron-sdk/tree/develop)) +## Unreleased changes ([Source](https://github.com/neotron-compute/neotron-sdk/tree/develop) | [Changes](https://github.com/neotron-compute/neotron-sdk/compare/v0.1.0...develop)) + +* None + +## v0.1.0 - 2024-05-19 ([Source](https://github.com/neotron-compute/neotron-sdk/tree/v0.1.0) | [Release](https://github.com/neotron-compute/neotron-sdk/releases/tag/v0.1.0)) First version. Supports cross-platform terminal output and some basic file APIs.