5 Commits

Author SHA1 Message Date
aa4cc58b42 expand doom memory for pimoroni 2025-10-31 17:59:48 -06:00
85e888193d Merge branch 'main' into doom 2025-10-31 17:51:18 -06:00
beee2392ae Merge branch 'main' into doom 2025-10-31 14:15:53 -06:00
46bfa89e18 doom compiles but is BIG 2025-10-31 13:39:16 -06:00
970d021a9a layout 2025-10-29 17:49:14 -06:00
54 changed files with 1743 additions and 2190 deletions

4
.gitignore vendored
View File

@@ -1,6 +1,4 @@
/target
*.uf2
userlib_sys.h
abi_sys.h
assets/gif/bad_apple.gif
picolibc
user-apps/gboy/Peanut-GB

9
.gitmodules vendored
View File

@@ -1,6 +1,3 @@
[submodule "picolibc"]
path = picolibc
url = https://github.com/picolibc/picolibc
[submodule "user-apps/gboy/Peanut-GB"]
path = user-apps/gboy/Peanut-GB
url = https://github.com/deltabeard/Peanut-GB
[submodule "user-apps/doom/doomgeneric"]
path = user-apps/doom/doomgeneric
url = https://github.com/ozkl/doomgeneric

625
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,14 +2,13 @@
resolver = "3"
members = [
"kernel",
"userlib_sys",
"userlib",
"selection_ui",
"user_apps/calculator",
"user_apps/snake",
"user_apps/gallery",
"user_apps/gif",
"user_apps/wav_player",
"abi_sys",
"abi",
"user-apps/calculator",
"user-apps/snake",
"user-apps/gallery",
"user-apps/gif",
"user-apps/doom",
]
[profile.release]
@@ -24,8 +23,8 @@ codegen-units = 1
lto = true
debug = false
opt-level = "z"
panic = "unwind"
panic = "abort"
[profile.dev]
lto = true
opt-level = "s"
opt-level = "z"

View File

@@ -1,27 +1,26 @@
# PicoCalc OS (Rust)
A simple kernel and applications for the **Clockwork PicoCalc**, written in Rust.
This project provides a minimal kernel, syscall table, and user-space applications to experiment with kernel development on constrained hardware.
A simple operating system for the **Clockwork PicoCalc**, written in Rust.
This project provides a minimal kernel, ABI, and user-space applications to experiment with OS development on constrained hardware.
## Status
Basic synchronous applications are working great.
Current focus is on exanding applications and porting software, finding bugs in ffi, and making sure the kernel is as stable as possible.
Current focus is on **expanding the ABI syscalls** and **fixing the MSC/USB-SCSI driver** to make application development easier and smoother.
## Project Structure
- **`kernel/`** The core OS kernel
- **`userlib_sys/`** C FFI bindings for kernel syscall
- **`userlib/`** Rust wrapper on top of `userlib_sys`
- **`picolib/`** Built with ```just newlib```, and provides libc symbols when linking with C libraries
- **`user-apps/`** Collection of userspace programs (gif player, wav player, calculator, snake, etc.)
- **`kernel/`** The core OS kernel (task scheduling, drivers, memory, etc.)
- **`abi_sys/`** Shared application binary interface definitions for kernel ↔ userspace (Repr "C")
- **`abi/`** Rust focused ABI helpers and abstractions for easier development
- **`user-apps/`** Collection of userspace programs (calculator, snake, etc.)
## Features
- Minimal Rust-based kernel targeting the PicoCalc
- Custom ABI for *Mostly* safe communication between kernel and applications
- Custom ABI for safe communication between kernel and applications
- Support for multiple user-space applications
- Hardware drivers tailored for the PicoCalc( Audio, Display, Keyboard, ans Storage )
- Hardware drivers tailored for the PicoCalc
## Getting Started

View File

@@ -1,10 +1,11 @@
[package]
name = "userlib"
name = "abi"
version = "0.1.0"
edition = "2024"
[dependencies]
userlib_sys = { path = "../userlib_sys" }
embedded-sdmmc = { version = "0.9.0", default-features = false }
embedded-graphics = "0.8.1"
abi_sys = { path = "../abi_sys" }
once_cell = { version = "1", default-features = false }
rand_core = "0.9.3"

162
abi/src/lib.rs Normal file
View File

@@ -0,0 +1,162 @@
#![no_std]
#![allow(static_mut_refs)]
extern crate alloc;
pub use abi_sys::{self, keyboard};
use abi_sys::{RngRequest, alloc, dealloc, keyboard::KeyEvent};
pub use alloc::format;
use core::alloc::{GlobalAlloc, Layout};
use rand_core::RngCore;
#[global_allocator]
static ALLOC: Alloc = Alloc;
struct Alloc;
unsafe impl GlobalAlloc for Alloc {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
alloc(layout.into())
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
dealloc(ptr, layout.into());
}
}
#[macro_export]
macro_rules! print {
($($arg:tt)*) => {{
let s = $crate::format!($($arg)*);
$crate::abi_sys::print(s.as_ptr(), s.len());
}};
}
pub fn sleep(ms: u64) {
abi_sys::sleep(ms);
}
pub fn get_ms() -> u64 {
abi_sys::get_ms()
}
pub fn get_key() -> KeyEvent {
abi_sys::keyboard::get_key().into()
}
pub mod display {
use abi_sys::CPixel;
use embedded_graphics::{
Pixel,
geometry::{Dimensions, Point},
pixelcolor::Rgb565,
prelude::{DrawTarget, Size},
primitives::Rectangle,
};
pub const SCREEN_WIDTH: usize = 320;
pub const SCREEN_HEIGHT: usize = 320;
pub type Pixel565 = Pixel<Rgb565>;
const BUF_SIZE: usize = 15 * 1024; // tune this for performance
static mut BUF: [CPixel; BUF_SIZE] = [CPixel::new(); BUF_SIZE];
// const BUF_SIZE: usize = 250 * 1024; // tune this for performance
// static mut BUF: Lazy<Vec<CPixel>> = Lazy::new(|| vec![const { CPixel::new() }; BUF_SIZE]);
pub struct Display;
impl Dimensions for Display {
fn bounding_box(&self) -> Rectangle {
Rectangle {
top_left: Point { x: 0, y: 0 },
size: Size {
width: SCREEN_WIDTH as u32,
height: SCREEN_HEIGHT as u32,
},
}
}
}
impl DrawTarget for Display {
type Color = Rgb565;
type Error = ();
fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
where
I: IntoIterator<Item = Pixel<Self::Color>>,
{
let mut count = 0;
for p in pixels {
unsafe { BUF[count] = p.into() };
count += 1;
if count == BUF_SIZE - 1 {
abi_sys::draw_iter(unsafe { BUF.as_ptr() }, count);
count = 0;
}
}
if count > 0 {
abi_sys::draw_iter(unsafe { BUF.as_ptr() }, count);
}
Ok(())
}
}
}
fn gen_rand(req: &mut RngRequest) {
abi_sys::gen_rand(req);
}
pub struct Rng;
impl RngCore for Rng {
fn next_u32(&mut self) -> u32 {
let mut req = RngRequest::U32(0);
gen_rand(&mut req);
if let RngRequest::U32(i) = req {
return i;
};
0
}
fn next_u64(&mut self) -> u64 {
let mut req = RngRequest::U64(0);
gen_rand(&mut req);
if let RngRequest::U64(i) = req {
return i;
};
0
}
fn fill_bytes(&mut self, dst: &mut [u8]) {
let mut req = RngRequest::Bytes {
ptr: dst.as_mut_ptr(),
len: dst.len(),
};
gen_rand(&mut req);
}
}
pub mod fs {
use embedded_sdmmc::DirEntry;
pub fn read_file(file: &str, read_from: usize, buf: &mut [u8]) -> usize {
abi_sys::read_file(
file.as_ptr(),
file.len(),
read_from,
buf.as_mut_ptr(),
buf.len(),
)
}
pub fn list_dir(path: &str, files: &mut [Option<DirEntry>]) -> usize {
abi_sys::list_dir(path.as_ptr(), path.len(), files.as_mut_ptr(), files.len())
}
pub fn file_len(str: &str) -> usize {
abi_sys::file_len(str.as_ptr(), str.len())
}
}

View File

@@ -1,5 +1,5 @@
[package]
name = "userlib_sys"
name = "abi_sys"
version = "0.1.0"
edition = "2024"
@@ -12,6 +12,7 @@ defmt = ["dep:defmt"]
strum = { version = "0.27.2", default-features = false, features = ["derive"] }
bitflags = "2.9.4"
embedded-graphics = "0.8.1"
embedded-sdmmc = { version = "0.9.0", default-features = false }
defmt = { version = "0.3", optional = true }
[build-dependencies]

View File

@@ -1,23 +1,24 @@
#![no_std]
#[cfg(feature = "alloc")]
use core::alloc::Layout;
use core::ffi::c_char;
use embedded_graphics::{
Pixel,
pixelcolor::{Rgb565, raw::RawU16},
prelude::{IntoStorage, Point},
};
use embedded_sdmmc::DirEntry;
use strum::{EnumCount, EnumIter};
pub type EntryFn = fn();
pub const SYS_CALL_TABLE_COUNT: usize = 14;
const _: () = assert!(SYS_CALL_TABLE_COUNT == SyscallTable::COUNT);
pub const ABI_CALL_TABLE_COUNT: usize = 11;
const _: () = assert!(ABI_CALL_TABLE_COUNT == CallTable::COUNT);
#[derive(Clone, Copy, EnumIter, EnumCount)]
#[repr(u8)]
pub enum SyscallTable {
pub enum CallTable {
Alloc = 0,
Dealloc = 1,
PrintString = 2,
@@ -28,15 +29,12 @@ pub enum SyscallTable {
GenRand = 7,
ListDir = 8,
ReadFile = 9,
WriteFile = 10,
FileLen = 11,
AudioBufferReady = 12,
SendAudioBuffer = 13,
FileLen = 10,
}
#[unsafe(no_mangle)]
#[unsafe(link_section = ".syscall_table")]
pub static mut SYS_CALL_TABLE: [usize; SYS_CALL_TABLE_COUNT] = [0; SYS_CALL_TABLE_COUNT];
pub static mut CALL_ABI_TABLE: [usize; ABI_CALL_TABLE_COUNT] = [0; ABI_CALL_TABLE_COUNT];
#[cfg(feature = "alloc")]
#[repr(C)]
@@ -46,9 +44,9 @@ pub struct CLayout {
}
#[cfg(feature = "alloc")]
impl From<CLayout> for Layout {
fn from(val: CLayout) -> Self {
unsafe { Layout::from_size_align_unchecked(val.size, val.alignment) }
impl Into<Layout> for CLayout {
fn into(self) -> Layout {
unsafe { Layout::from_size_align_unchecked(self.size, self.alignment) }
}
}
@@ -62,51 +60,51 @@ impl From<Layout> for CLayout {
}
}
pub type Alloc = extern "C" fn(layout: CLayout) -> *mut u8;
pub type AllocAbi = extern "C" fn(layout: CLayout) -> *mut u8;
#[unsafe(no_mangle)]
pub extern "C" fn alloc(layout: CLayout) -> *mut u8 {
let f: Alloc = unsafe { core::mem::transmute(SYS_CALL_TABLE[SyscallTable::Alloc as usize]) };
let f: AllocAbi = unsafe { core::mem::transmute(CALL_ABI_TABLE[CallTable::Alloc as usize]) };
f(layout)
}
pub type Dealloc = extern "C" fn(ptr: *mut u8, layout: CLayout);
pub type DeallocAbi = extern "C" fn(ptr: *mut u8, layout: CLayout);
#[unsafe(no_mangle)]
pub extern "C" fn dealloc(ptr: *mut u8, layout: CLayout) {
let f: Dealloc =
unsafe { core::mem::transmute(SYS_CALL_TABLE[SyscallTable::Dealloc as usize]) };
let f: DeallocAbi =
unsafe { core::mem::transmute(CALL_ABI_TABLE[CallTable::Dealloc as usize]) };
f(ptr, layout)
}
pub type Print = extern "C" fn(ptr: *const u8, len: usize);
pub type PrintAbi = extern "C" fn(ptr: *const u8, len: usize);
#[unsafe(no_mangle)]
pub extern "C" fn print(ptr: *const u8, len: usize) {
let f: Print =
unsafe { core::mem::transmute(SYS_CALL_TABLE[SyscallTable::PrintString as usize]) };
let f: PrintAbi =
unsafe { core::mem::transmute(CALL_ABI_TABLE[CallTable::PrintString as usize]) };
f(ptr, len);
}
pub type SleepMs = extern "C" fn(ms: u64);
pub type SleepMsAbi = extern "C" fn(ms: u64);
#[unsafe(no_mangle)]
pub extern "C" fn sleep(ms: u64) {
let f: SleepMs =
unsafe { core::mem::transmute(SYS_CALL_TABLE[SyscallTable::SleepMs as usize]) };
let f: SleepMsAbi =
unsafe { core::mem::transmute(CALL_ABI_TABLE[CallTable::SleepMs as usize]) };
f(ms);
}
pub type GetMs = extern "C" fn() -> u64;
pub type GetMsAbi = extern "C" fn() -> u64;
#[unsafe(no_mangle)]
pub extern "C" fn get_ms() -> u64 {
let f: GetMs = unsafe { core::mem::transmute(SYS_CALL_TABLE[SyscallTable::GetMs as usize]) };
let f: GetMsAbi = unsafe { core::mem::transmute(CALL_ABI_TABLE[CallTable::GetMs as usize]) };
f()
}
#[repr(C)]
#[derive(Default, Copy, Clone)]
#[derive(Copy, Clone)]
pub struct CPixel {
pub x: i32,
pub y: i32,
@@ -123,36 +121,33 @@ impl CPixel {
}
}
impl From<CPixel> for Pixel<Rgb565> {
fn from(value: CPixel) -> Self {
Pixel(
Point::new(value.x, value.y),
RawU16::new(value.color).into(),
)
}
}
impl From<Pixel<Rgb565>> for CPixel {
fn from(value: Pixel<Rgb565>) -> Self {
impl Into<CPixel> for Pixel<Rgb565> {
fn into(self) -> CPixel {
CPixel {
x: value.0.x,
y: value.0.y,
color: value.1.into_storage(),
x: self.0.x,
y: self.0.y,
color: self.1.into_storage(),
}
}
}
pub type DrawIter = extern "C" fn(ptr: *const CPixel, len: usize);
impl Into<Pixel<Rgb565>> for CPixel {
fn into(self) -> Pixel<Rgb565> {
Pixel(Point::new(self.x, self.y), RawU16::new(self.color).into())
}
}
pub type DrawIterAbi = extern "C" fn(ptr: *const CPixel, len: usize);
#[unsafe(no_mangle)]
pub extern "C" fn draw_iter(ptr: *const CPixel, len: usize) {
let f: DrawIter =
unsafe { core::mem::transmute(SYS_CALL_TABLE[SyscallTable::DrawIter as usize]) };
let f: DrawIterAbi =
unsafe { core::mem::transmute(CALL_ABI_TABLE[CallTable::DrawIter as usize]) };
f(ptr, len);
}
pub mod keyboard {
use crate::{SYS_CALL_TABLE, SyscallTable};
use crate::{CALL_ABI_TABLE, CallTable};
bitflags::bitflags! {
#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)]
@@ -174,12 +169,12 @@ pub mod keyboard {
pub mods: Modifiers,
}
impl From<KeyEventC> for KeyEvent {
fn from(val: KeyEventC) -> Self {
impl Into<KeyEvent> for KeyEventC {
fn into(self) -> KeyEvent {
KeyEvent {
key: val.key.into(),
state: val.state,
mods: val.mods,
key: self.key.into(),
state: self.state,
mods: self.mods,
}
}
}
@@ -191,12 +186,12 @@ pub mod keyboard {
pub mods: Modifiers,
}
impl From<KeyEvent> for KeyEventC {
fn from(val: KeyEvent) -> Self {
impl Into<KeyEventC> for KeyEvent {
fn into(self) -> KeyEventC {
KeyEventC {
key: val.key.into(),
state: val.state,
mods: val.mods,
key: self.key.into(),
state: self.state,
mods: self.mods,
}
}
}
@@ -270,9 +265,9 @@ pub mod keyboard {
Unknown(u8),
}
impl From<KeyCode> for u8 {
fn from(val: KeyCode) -> Self {
match val {
impl Into<u8> for KeyCode {
fn into(self) -> u8 {
match self {
KeyCode::JoyUp => 0x01,
KeyCode::JoyDown => 0x02,
KeyCode::JoyLeft => 0x03,
@@ -370,12 +365,12 @@ pub mod keyboard {
}
}
pub type GetKey = extern "C" fn() -> KeyEventC;
pub type GetKeyAbi = extern "C" fn() -> KeyEventC;
#[unsafe(no_mangle)]
pub extern "C" fn get_key() -> KeyEventC {
let f: GetKey =
unsafe { core::mem::transmute(SYS_CALL_TABLE[SyscallTable::GetKey as usize]) };
let f: GetKeyAbi =
unsafe { core::mem::transmute(CALL_ABI_TABLE[CallTable::GetKey as usize]) };
f()
}
}
@@ -392,7 +387,7 @@ pub type GenRand = extern "C" fn(req: &mut RngRequest);
#[unsafe(no_mangle)]
pub extern "C" fn gen_rand(req: &mut RngRequest) {
unsafe {
let ptr = SYS_CALL_TABLE[SyscallTable::GenRand as usize];
let ptr = CALL_ABI_TABLE[CallTable::GenRand as usize];
let f: GenRand = core::mem::transmute(ptr);
f(req)
}
@@ -401,23 +396,21 @@ pub extern "C" fn gen_rand(req: &mut RngRequest) {
pub type ListDir = extern "C" fn(
str: *const u8,
len: usize,
entries: *mut *mut c_char,
files: *mut Option<DirEntry>,
file_len: usize,
max_entry_str_len: usize,
) -> usize;
#[unsafe(no_mangle)]
pub extern "C" fn list_dir(
str: *const u8,
len: usize,
entries: *mut *mut c_char,
entry_count: usize,
max_entry_str_len: usize,
files: *mut Option<DirEntry>,
file_len: usize,
) -> usize {
unsafe {
let ptr = SYS_CALL_TABLE[SyscallTable::ListDir as usize];
let ptr = CALL_ABI_TABLE[CallTable::ListDir as usize];
let f: ListDir = core::mem::transmute(ptr);
f(str, len, entries, entry_count, max_entry_str_len)
f(str, len, files, file_len)
}
}
@@ -438,62 +431,19 @@ pub extern "C" fn read_file(
buf_len: usize,
) -> usize {
unsafe {
let ptr = SYS_CALL_TABLE[SyscallTable::ReadFile as usize];
let ptr = CALL_ABI_TABLE[CallTable::ReadFile as usize];
let f: ReadFile = core::mem::transmute(ptr);
f(str, len, read_from, buf, buf_len)
}
}
pub type WriteFile =
extern "C" fn(str: *const u8, len: usize, write_from: usize, buf: *const u8, buf_len: usize);
#[unsafe(no_mangle)]
pub extern "C" fn write_file(
str: *const u8,
len: usize,
write_from: usize,
buf: *const u8,
buf_len: usize,
) {
unsafe {
let ptr = SYS_CALL_TABLE[SyscallTable::WriteFile as usize];
let f: WriteFile = core::mem::transmute(ptr);
f(str, len, write_from, buf, buf_len)
}
}
pub type FileLen = extern "C" fn(str: *const u8, len: usize) -> usize;
#[unsafe(no_mangle)]
pub extern "C" fn file_len(str: *const u8, len: usize) -> usize {
unsafe {
let ptr = SYS_CALL_TABLE[SyscallTable::FileLen as usize];
let ptr = CALL_ABI_TABLE[CallTable::FileLen as usize];
let f: FileLen = core::mem::transmute(ptr);
f(str, len)
}
}
pub type AudioBufferReady = extern "C" fn() -> bool;
#[allow(unused)]
pub fn audio_buffer_ready() -> bool {
unsafe {
let ptr = SYS_CALL_TABLE[SyscallTable::AudioBufferReady as usize];
let f: AudioBufferReady = core::mem::transmute(ptr);
f()
}
}
pub const AUDIO_BUFFER_SAMPLES: usize = 1024;
pub const AUDIO_BUFFER_LEN: usize = AUDIO_BUFFER_SAMPLES * 2;
pub type SendAudioBuffer = extern "C" fn(ptr: *const u8, len: usize);
#[allow(unused)]
pub fn send_audio_buffer(buf: *const u8, len: usize) {
unsafe {
let ptr = SYS_CALL_TABLE[SyscallTable::SendAudioBuffer as usize];
let f: SendAudioBuffer = core::mem::transmute(ptr);
f(buf, len)
}
}

View File

@@ -1,37 +1,13 @@
target := "thumbv8m.main-none-eabihf"
kernel-dev board:
cargo run --bin kernel --features {{board}} --features fps --features defmt
kernel-release-probe board:
cargo run --bin kernel --profile release --features {{board}} --features fps
cargo run --bin kernel --features {{board}}
kernel-release board:
cargo build --bin kernel --release --no-default-features --features {{board}}
elf2uf2-rs -d target/{{target}}/release/kernel
elf2uf2-rs -d target/thumbv8m.main-none-eabihf/release/kernel
binary-args := "RUSTFLAGS=\"-C link-arg=-pie -C relocation-model=pic\""
cbindgen:
cbindgen userlib_sys --output userlib_sys.h -q
newlib:
#!/bin/bash
cd picolibc
mkdir build
cd build
CONFIG_PICOLIBC=true ../scripts/do-configure thumbv8m_main_fp-none-eabi \
--buildtype=minsize \
-Dtests=true \
-Dtinystdio=false \
-Dsingle-thread=true \
-Db_pie=true \
-Ddefault_library=static \
-Dtinystdio=false \
-Dnewlib-nano-malloc=true \
-Dmultilib=false \
-Dpicolib=true \
"$@"
DESTDIR=./install meson install
ninja
cbindgen abi_sys --output abi_sys.h -q
userapp app:
{{binary-args}} cargo build --bin {{app}} --profile release-binary
@@ -41,20 +17,4 @@ userapps: cbindgen
just userapp snake
just userapp gallery
just userapp gif
just userapp wav_player
copy-userapp app:
cp ./target/{{target}}/release-binary/{{app}} /run/media/$(whoami)/PICOCALC/{{app}}.bin
copy-userapps:
#!/bin/bash
just userapps
just copy-userapp calculator
just copy-userapp snake
just copy-userapp gallery
just copy-userapp gif
just copy-userapp wav_player
DEV=$(lsblk -o LABEL,NAME -nr | awk -v L="PICOCALC" '$1==L {print "/dev/" $2}')
udisksctl unmount -b "$DEV"
udisksctl power-off -b "$DEV"
just userapp doom

View File

@@ -11,14 +11,11 @@ doctest = false
bench = false
[features]
default = ["rp235x"]
pimoroni2w = ["rp235x", "psram"]
# rp2040 = ["embassy-rp/rp2040"] # unsupported, ram too small for fb
default = ["rp235x", "defmt"]
pimoroni2w = ["rp235x"]
rp2040 = ["embassy-rp/rp2040"]
rp235x = ["embassy-rp/rp235xb"]
trouble = ["dep:bt-hci", "dep:cyw43", "dep:cyw43-pio", "dep:trouble-host"]
psram = ["dep:embedded-alloc"]
overclock = []
fps = []
defmt = [
"dep:defmt",
"panic-probe/print-defmt",
@@ -73,17 +70,16 @@ panic-probe = "0.3"
portable-atomic = { version = "1.11", features = ["critical-section"] }
assign-resources = "0.5.0"
defmt = { version = "1.0.1", optional = true }
defmt-rtt = "1.1.0"
defmt = { version = "0.3", optional = true }
defmt-rtt = "0.4.2"
embedded-sdmmc = { version = "0.9", default-features = false }
st7365p-lcd = { git = "https://github.com/legitcamper/st7365p-lcd-rs", rev = "a784b9e6df0769371dfc522528e770cf8fc6403a" } # async branch
embedded-graphics = { version = "0.8.1" }
embedded-text = "0.7.2"
embedded-layout = "0.4.2"
kolibri-embedded-gui = "0.1.0"
micromath = "2.1.0"
fixed = "1.29.0"
strum = { version = "0.27.2", default-features = false }
rand = { version = "0.9.0", default-features = false }
once_cell = { version = "1.21.3", default-features = false }
@@ -94,9 +90,7 @@ spin = "0.10.0"
num_enum = { version = "0.7.4", default-features = false }
goblin = { version = "0.10.1", default-features = false, features = ["elf32"] }
talc = "4.4.3"
embedded-alloc = { version = "0.6.0", features = [
"allocator_api",
], optional = true }
embedded-alloc = { version = "0.6.0", features = ["allocator_api"] }
bumpalo = "3.19.0"
userlib_sys = { path = "../userlib_sys" }
abi_sys = { path = "../abi_sys" }

View File

@@ -14,7 +14,7 @@ use std::io::Write;
use std::path::PathBuf;
#[cfg(all(feature = "rp235x", not(feature = "pimoroni2w")))]
const MEMORY: &[u8] = include_bytes!("rp2350.x");
const MEMORY: &'static [u8] = include_bytes!("rp2350.x");
#[cfg(feature = "pimoroni2w")]
const MEMORY: &'static [u8] = include_bytes!("rp2350.x");

View File

@@ -1,60 +1,46 @@
use abi_sys::{
AllocAbi, CLayout, CPixel, DeallocAbi, DrawIterAbi, FileLen, GenRand, GetMsAbi, ListDir,
PrintAbi, ReadFile, RngRequest, SleepMsAbi, keyboard::*,
};
use alloc::{string::ToString, vec::Vec};
use core::{ffi::c_char, ptr, sync::atomic::Ordering};
use core::{alloc::GlobalAlloc, sync::atomic::Ordering};
use embassy_rp::clocks::{RoscRng, clk_sys_freq};
use embassy_time::Instant;
use embedded_graphics::draw_target::DrawTarget;
use embedded_sdmmc::LfnBuffer;
use embedded_sdmmc::{DirEntry, LfnBuffer};
use heapless::spsc::Queue;
use userlib_sys::{
AUDIO_BUFFER_SAMPLES, Alloc, AudioBufferReady, CLayout, CPixel, Dealloc, DrawIter, FileLen,
GenRand, GetMs, ListDir, Print, ReadFile, RngRequest, SendAudioBuffer, SleepMs, WriteFile,
keyboard::*,
};
#[cfg(feature = "psram")]
use crate::heap::HEAP;
#[cfg(feature = "psram")]
use core::alloc::GlobalAlloc;
use crate::{
audio::{AUDIO_BUFFER, AUDIO_BUFFER_READY, AUDIO_BUFFER_WRITTEN},
display::FRAMEBUFFER,
framebuffer::FB_PAUSED,
storage::{Dir, File, SDCARD},
};
const _: Alloc = alloc;
const _: AllocAbi = alloc;
pub extern "C" fn alloc(layout: CLayout) -> *mut u8 {
// SAFETY: caller guarantees layout is valid
unsafe {
#[cfg(feature = "psram")]
{
return HEAP.alloc(layout.into());
}
#[cfg(not(feature = "psram"))]
{
if cfg!(feature = "pimoroni2w") {
crate::heap::HEAP.alloc(layout.into())
} else {
alloc::alloc::alloc(layout.into())
}
}
}
const _: Dealloc = dealloc;
const _: DeallocAbi = dealloc;
pub extern "C" fn dealloc(ptr: *mut u8, layout: CLayout) {
// SAFETY: caller guarantees ptr and layout are valid
#[cfg(feature = "psram")]
{
unsafe { HEAP.dealloc(ptr, layout.into()) }
unsafe {
if cfg!(feature = "pimoroni2w") {
crate::heap::HEAP.dealloc(ptr, layout.into())
} else {
alloc::alloc::dealloc(ptr, layout.into())
}
#[cfg(not(feature = "psram"))]
{
unsafe { alloc::alloc::dealloc(ptr, layout.into()) }
}
}
const _: Print = print;
const _: PrintAbi = print;
pub extern "C" fn print(ptr: *const u8, len: usize) {
// SAFETY: caller guarantees `ptr` is valid for `len` bytes
let slice = unsafe { core::slice::from_raw_parts(ptr, len) };
@@ -68,7 +54,7 @@ pub extern "C" fn print(ptr: *const u8, len: usize) {
}
}
const _: SleepMs = sleep;
const _: SleepMsAbi = sleep;
pub extern "C" fn sleep(ms: u64) {
let cycles_per_ms = clk_sys_freq() / 1000;
let total_cycles = ms * cycles_per_ms as u64;
@@ -80,14 +66,14 @@ pub extern "C" fn sleep(ms: u64) {
pub static mut MS_SINCE_LAUNCH: Option<Instant> = None;
const _: GetMs = get_ms;
const _: GetMsAbi = get_ms;
pub extern "C" fn get_ms() -> u64 {
Instant::now()
.duration_since(unsafe { MS_SINCE_LAUNCH.unwrap() })
.as_millis()
}
const _: DrawIter = draw_iter;
const _: DrawIterAbi = draw_iter;
pub extern "C" fn draw_iter(cpixels: *const CPixel, len: usize) {
// SAFETY: caller guarantees `ptr` is valid for `len` bytes
let cpixels = unsafe { core::slice::from_raw_parts(cpixels, len) };
@@ -101,7 +87,7 @@ pub extern "C" fn draw_iter(cpixels: *const CPixel, len: usize) {
pub static mut KEY_CACHE: Queue<KeyEvent, 32> = Queue::new();
const _: GetKey = get_key;
const _: GetKeyAbi = get_key;
pub extern "C" fn get_key() -> KeyEventC {
if let Some(event) = unsafe { KEY_CACHE.dequeue() } {
event.into()
@@ -130,27 +116,11 @@ pub extern "C" fn gen_rand(req: &mut RngRequest) {
}
}
unsafe fn copy_entry_to_user_buf(name: &[u8], dest: *mut c_char, max_str_len: usize) {
if !dest.is_null() {
let len = name.len().min(max_str_len - 1);
unsafe {
ptr::copy_nonoverlapping(name.as_ptr(), dest, len);
*dest.add(len) = 0; // nul terminator
}
}
}
unsafe fn get_dir_entries(dir: &Dir, entries: &mut [*mut c_char], max_str_len: usize) -> usize {
let mut b = [0; 25];
let mut buf = LfnBuffer::new(&mut b);
fn get_dir_entries(dir: &Dir, files: &mut [Option<DirEntry>]) -> usize {
let mut i = 0;
dir.iterate_dir_lfn(&mut buf, |entry, lfn_name| {
if i < entries.len() {
if let Some(name) = lfn_name {
unsafe { copy_entry_to_user_buf(name.as_bytes(), entries[i], max_str_len) };
} else {
unsafe { copy_entry_to_user_buf(entry.name.base_name(), entries[i], max_str_len) };
}
dir.iterate_dir(|entry| {
if i < files.len() {
files[i] = Some(entry.clone());
i += 1;
}
})
@@ -158,30 +128,24 @@ unsafe fn get_dir_entries(dir: &Dir, entries: &mut [*mut c_char], max_str_len: u
i
}
unsafe fn recurse_dir(
dir: &Dir,
dirs: &[&str],
entries: &mut [*mut c_char],
max_str_len: usize,
) -> usize {
fn recurse_dir(dir: &Dir, dirs: &[&str], files: &mut [Option<DirEntry>]) -> usize {
if dirs.is_empty() {
return unsafe { get_dir_entries(dir, entries, max_str_len) };
return get_dir_entries(dir, files);
}
let dir = dir.open_dir(dirs[0]).unwrap();
unsafe { recurse_dir(&dir, &dirs[1..], entries, max_str_len) }
recurse_dir(&dir, &dirs[1..], files)
}
const _: ListDir = list_dir;
pub extern "C" fn list_dir(
dir: *const u8,
len: usize,
entries: *mut *mut c_char,
files: *mut Option<DirEntry>,
files_len: usize,
max_entry_str_len: usize,
) -> usize {
// SAFETY: caller guarantees `ptr` is valid for `len` bytes
let files = unsafe { core::slice::from_raw_parts_mut(entries, files_len) };
let files = unsafe { core::slice::from_raw_parts_mut(files, files_len) };
// SAFETY: caller guarantees `ptr` is valid for `len` bytes
let dir = unsafe { core::str::from_raw_parts(dir, len) };
let dirs: Vec<&str> = dir.split('/').collect();
@@ -191,13 +155,11 @@ pub extern "C" fn list_dir(
let mut wrote = 0;
sd.access_root_dir(|root| {
if dirs[0].is_empty() && dirs.len() >= 2 {
unsafe {
if dirs[0] == "" && dirs.len() >= 2 {
if dir == "/" {
wrote = get_dir_entries(&root, files, max_entry_str_len);
wrote = get_dir_entries(&root, files);
} else {
wrote = recurse_dir(&root, &dirs[1..], files, max_entry_str_len);
}
wrote = recurse_dir(&root, &dirs[1..], files);
}
}
});
@@ -214,14 +176,13 @@ fn recurse_file<T>(
let mut buf = LfnBuffer::new(&mut b);
let mut short_name = None;
dir.iterate_dir_lfn(&mut buf, |entry, name| {
if let Some(name) = name
&& (name == dirs[0] || entry.name.to_string().as_str() == dirs[0])
{
if let Some(name) = name {
if name == dirs[0] || entry.name.to_string().as_str() == dirs[0] {
short_name = Some(entry.name.clone());
}
}
})
.expect("Failed to iterate dir");
.unwrap();
if let Some(name) = short_name {
let mut file = dir
.open_file_in_dir(name, embedded_sdmmc::Mode::ReadWriteAppend)
@@ -245,19 +206,9 @@ pub extern "C" fn read_file(
) -> usize {
// SAFETY: caller guarantees `ptr` is valid for `len` bytes
let file = unsafe { core::str::from_raw_parts(str, len) };
let mut components: [&str; 8] = [""; 8];
let mut count = 0;
for part in file.split('/') {
if count >= components.len() {
break;
}
components[count] = part;
count += 1;
}
let file: Vec<&str> = file.split('/').collect();
// SAFETY: caller guarantees `ptr` is valid for `len` bytes
let buf = unsafe { core::slice::from_raw_parts_mut(buf, buf_len) };
let mut buf = unsafe { core::slice::from_raw_parts_mut(buf, buf_len) };
let mut read = 0;
@@ -265,9 +216,9 @@ pub extern "C" fn read_file(
let sd = guard.as_mut().unwrap();
if !file.is_empty() {
sd.access_root_dir(|root| {
if let Ok(result) = recurse_file(&root, &components[1..count], |file| {
file.seek_from_start(start_from as u32).unwrap_or(());
file.read(buf).unwrap()
if let Ok(result) = recurse_file(&root, &file[1..], |file| {
file.seek_from_start(start_from as u32).unwrap();
file.read(&mut buf).unwrap()
}) {
read = result
};
@@ -276,46 +227,9 @@ pub extern "C" fn read_file(
read
}
const _: WriteFile = write_file;
pub extern "C" fn write_file(
str: *const u8,
len: usize,
start_from: usize,
buf: *const u8,
buf_len: usize,
) {
// SAFETY: caller guarantees str ptr is valid for `len` bytes
let file = unsafe { core::str::from_raw_parts(str, len) };
let mut components: [&str; 8] = [""; 8];
let mut count = 0;
for part in file.split('/') {
if count >= components.len() {
break;
}
components[count] = part;
count += 1;
}
// SAFETY: caller guarantees buf ptr is valid for `buf_len` bytes
let buf = unsafe { core::slice::from_raw_parts(buf, buf_len) };
let mut guard = SDCARD.get().try_lock().expect("Failed to get sdcard");
let sd = guard.as_mut().unwrap();
if !file.is_empty() {
sd.access_root_dir(|root| {
recurse_file(&root, &components[1..count], |file| {
file.seek_from_start(start_from as u32).unwrap();
file.write(buf).unwrap()
})
.unwrap_or(())
});
};
}
const _: FileLen = file_len;
pub extern "C" fn file_len(str: *const u8, len: usize) -> usize {
// SAFETY: caller guarantees str ptr is valid for `len` bytes
// SAFETY: caller guarantees `ptr` is valid for `len` bytes
let file = unsafe { core::str::from_raw_parts(str, len) };
let file: Vec<&str> = file.split('/').collect();
@@ -332,31 +246,3 @@ pub extern "C" fn file_len(str: *const u8, len: usize) -> usize {
}
len as usize
}
const _: AudioBufferReady = audio_buffer_ready;
pub extern "C" fn audio_buffer_ready() -> bool {
AUDIO_BUFFER_READY.load(Ordering::Acquire)
}
const _: SendAudioBuffer = send_audio_buffer;
pub extern "C" fn send_audio_buffer(ptr: *const u8, len: usize) {
// SAFETY: caller guarantees `ptr` is valid for `len` bytes
let buf = unsafe { core::slice::from_raw_parts(ptr, len) };
while !AUDIO_BUFFER_READY.load(Ordering::Acquire) {
core::hint::spin_loop();
}
if buf.len() == AUDIO_BUFFER_SAMPLES * 2 {
AUDIO_BUFFER_READY.store(false, Ordering::Release);
unsafe { AUDIO_BUFFER.copy_from_slice(buf) };
AUDIO_BUFFER_WRITTEN.store(true, Ordering::Release);
} else {
#[cfg(feature = "defmt")]
defmt::warn!(
"user audio stream was wrong size: {} should be {}",
buf.len(),
AUDIO_BUFFER_SAMPLES * 2
)
}
}

View File

@@ -1,174 +0,0 @@
use crate::Audio;
use core::sync::atomic::{AtomicBool, Ordering};
use embassy_futures::{join::join, yield_now};
use embassy_rp::{
Peri,
clocks::clk_sys_freq,
dma::{AnyChannel, Channel},
gpio::Level,
pio::{
Common, Config, Direction, FifoJoin, Instance, LoadedProgram, PioPin, ShiftConfig,
StateMachine, program::pio_asm,
},
};
use fixed::traits::ToFixed;
pub const SAMPLE_RATE_HZ: u32 = 22_050;
const AUDIO_BUFFER_SAMPLES: usize = 1024;
const _: () = assert!(AUDIO_BUFFER_SAMPLES == userlib_sys::AUDIO_BUFFER_SAMPLES);
const SILENCE: u8 = u8::MAX / 2;
// 8bit stereo interleaved PCM audio buffers
pub static mut AUDIO_BUFFER: [u8; AUDIO_BUFFER_SAMPLES * 2] = [SILENCE; AUDIO_BUFFER_SAMPLES * 2];
static mut AUDIO_BUFFER_1: [u8; AUDIO_BUFFER_SAMPLES * 2] = [SILENCE; AUDIO_BUFFER_SAMPLES * 2];
pub static AUDIO_BUFFER_READY: AtomicBool = AtomicBool::new(true);
pub static AUDIO_BUFFER_WRITTEN: AtomicBool = AtomicBool::new(false);
pub fn clear_audio_buffers() {
unsafe {
AUDIO_BUFFER.fill(SILENCE);
AUDIO_BUFFER_1.fill(SILENCE);
}
}
#[embassy_executor::task]
pub async fn audio_handler(mut audio: Audio) {
let prg = PioPwmAudioProgram8Bit::new(&mut audio.pio);
let mut pwm_pio_left =
PioPwmAudio::new(audio.dma0, &mut audio.pio, audio.sm0, audio.left, &prg);
let mut pwm_pio_right =
PioPwmAudio::new(audio.dma1, &mut audio.pio, audio.sm1, audio.right, &prg);
loop {
unsafe {
if AUDIO_BUFFER_WRITTEN.load(Ordering::Acquire) {
write_samples(&mut pwm_pio_left, &mut pwm_pio_right, &AUDIO_BUFFER_1).await;
AUDIO_BUFFER_1.fill(SILENCE);
core::mem::swap(&mut AUDIO_BUFFER, &mut AUDIO_BUFFER_1);
AUDIO_BUFFER_READY.store(true, Ordering::Release)
} else {
yield_now().await;
}
}
}
}
async fn write_samples<PIO: Instance>(
left: &mut PioPwmAudio<'static, PIO, 0>,
right: &mut PioPwmAudio<'static, PIO, 1>,
buf: &[u8],
) {
// pack two samples per word
static mut PACKED_BUF_L: [u32; AUDIO_BUFFER_SAMPLES / 2] = [0; AUDIO_BUFFER_SAMPLES / 2];
static mut PACKED_BUF_R: [u32; AUDIO_BUFFER_SAMPLES / 2] = [0; AUDIO_BUFFER_SAMPLES / 2];
unsafe {
for ((pl, pr), sample) in PACKED_BUF_L
.iter_mut()
.zip(PACKED_BUF_R.iter_mut())
.zip(buf.chunks(4))
{
*pl = pack_u8_samples(sample[0], sample[2]);
*pr = pack_u8_samples(sample[1], sample[3]);
}
let left_fut = left
.sm
.tx()
.dma_push(left.dma.reborrow(), &PACKED_BUF_L, false);
let right_fut = right
.sm
.tx()
.dma_push(right.dma.reborrow(), &PACKED_BUF_R, false);
join(left_fut, right_fut).await;
}
}
struct PioPwmAudioProgram8Bit<'d, PIO: Instance>(LoadedProgram<'d, PIO>);
/// Writes one sample to pwm as high and low time
impl<'d, PIO: Instance> PioPwmAudioProgram8Bit<'d, PIO> {
fn new(common: &mut Common<'d, PIO>) -> Self {
// only uses 16 bits top for pwm high, bottom for pwm low
// allows storing two samples per word
let prg = pio_asm!(
".side_set 1",
"set x, 0 side 1",
"set y, 0 side 0",
".wrap_target",
"check:",
"pull ifempty noblock side 1", // gets new osr or loads 0 into x, gets second sample if osr not empty
"out x, 8 side 0", // pwm high time
"out y, 8 side 1", // pwm low time
"jmp x!=y play_sample side 0", // x & y are never equal unless osr was empty
"set x, 1 side 1",
"set y, 1 side 0",
"play_sample:"
"loop_high:",
"jmp x-- loop_high side 1", // keep pwm high, decrement X until 0
"loop_low:",
"jmp y-- loop_low side 0", // keep pwm low, decrement Y until 0
".wrap"
);
let prg = common.load_program(&prg.program);
Self(prg)
}
}
struct PioPwmAudio<'d, PIO: Instance, const SM: usize> {
dma: Peri<'d, AnyChannel>,
sm: StateMachine<'d, PIO, SM>,
}
impl<'d, PIO: Instance, const SM: usize> PioPwmAudio<'d, PIO, SM> {
fn new(
dma: Peri<'d, impl Channel>,
pio: &mut Common<'d, PIO>,
mut sm: StateMachine<'d, PIO, SM>,
pin: Peri<'d, impl PioPin>,
prg: &PioPwmAudioProgram8Bit<'d, PIO>,
) -> Self {
let pin = pio.make_pio_pin(pin);
sm.set_pins(Level::High, &[&pin]);
sm.set_pin_dirs(Direction::Out, &[&pin]);
let mut cfg = Config::default();
cfg.fifo_join = FifoJoin::TxOnly;
let shift_cfg = ShiftConfig {
auto_fill: false,
..Default::default()
};
cfg.shift_out = shift_cfg;
cfg.use_program(&prg.0, &[&pin]);
sm.set_config(&cfg);
let target_clock = (u8::MAX as u32 + 1) * SAMPLE_RATE_HZ;
let divider = (clk_sys_freq() / target_clock).to_fixed();
sm.set_clock_divider(divider);
sm.set_enable(true);
Self {
dma: dma.into(),
sm,
}
}
}
/// packs two u8 samples into 32bit word
fn pack_u8_samples(sample1: u8, sample2: u8) -> u32 {
(u8_pcm_to_pwm(sample1) as u32) << 16 | u8_pcm_to_pwm(sample2) as u32
}
fn u8_pcm_to_pwm(sample: u8) -> u16 {
((sample as u16) << 8) | ((u8::MAX - sample) as u16)
}

View File

@@ -1,27 +1,21 @@
use crate::framebuffer::{self, AtomicFrameBuffer, FB_PAUSED};
use core::alloc::{GlobalAlloc, Layout};
use core::sync::atomic::Ordering;
use embassy_futures::yield_now;
use embassy_rp::{
Peri,
gpio::{Level, Output},
peripherals::{PIN_13, PIN_14, PIN_15, SPI1},
spi::{Async, Spi},
};
use embassy_time::Delay;
use embassy_time::{Delay, Timer};
use embedded_graphics::{
pixelcolor::Rgb565,
prelude::{DrawTarget, RgbColor},
};
use embedded_hal_bus::spi::ExclusiveDevice;
use st7365p_lcd::ST7365P;
#[cfg(feature = "psram")]
use crate::heap::HEAP;
#[cfg(feature = "psram")]
use core::alloc::{GlobalAlloc, Layout};
#[cfg(feature = "psram")]
use embedded_graphics::{draw_target::DrawTarget, pixelcolor::Rgb565, prelude::RgbColor};
#[cfg(feature = "fps")]
pub use framebuffer::fps::{FPS_CANVAS, FPS_COUNTER};
type Display = ST7365P<
type DISPLAY = ST7365P<
ExclusiveDevice<Spi<'static, SPI1, Async>, Output<'static>, Delay>,
Output<'static>,
Output<'static>,
@@ -35,21 +29,18 @@ pub static mut FRAMEBUFFER: Option<AtomicFrameBuffer> = None;
fn init_fb() {
unsafe {
#[cfg(feature = "psram")]
{
let slab = HEAP.alloc(Layout::array::<u16>(framebuffer::SIZE).unwrap()) as *mut u16;
FRAMEBUFFER = Some(if cfg!(not(feature = "pimoroni2w")) {
static mut BUF: [u16; framebuffer::SIZE] = [0; framebuffer::SIZE];
AtomicFrameBuffer::new(&mut BUF)
} else {
let slab = crate::heap::HEAP.alloc(Layout::array::<u16>(framebuffer::SIZE).unwrap())
as *mut u16;
let buf = core::slice::from_raw_parts_mut(slab, framebuffer::SIZE);
let mut fb = AtomicFrameBuffer::new(buf);
fb.clear(Rgb565::BLACK).unwrap();
FRAMEBUFFER = Some(fb);
}
#[cfg(not(feature = "psram"))]
{
static mut BUF: [u16; framebuffer::SIZE] = [0; framebuffer::SIZE];
FRAMEBUFFER = Some(AtomicFrameBuffer::new(&mut BUF));
}
fb
});
}
}
@@ -58,7 +49,7 @@ pub async fn init_display(
cs: Peri<'static, PIN_13>,
data: Peri<'static, PIN_14>,
reset: Peri<'static, PIN_15>,
) -> Display {
) -> DISPLAY {
init_fb();
let spi_device = ExclusiveDevice::new(spi, Output::new(cs, Level::Low), Delay).unwrap();
@@ -86,38 +77,20 @@ pub async fn init_display(
}
#[embassy_executor::task]
pub async fn display_handler(mut display: Display) {
use embassy_time::{Instant, Timer};
// Target ~60 Hz refresh (≈16.67 ms per frame)
const FRAME_TIME_MS: u64 = 1000 / 60;
pub async fn display_handler(mut display: DISPLAY) {
loop {
let start = Instant::now();
#[cfg(feature = "fps")]
unsafe {
if FPS_COUNTER.should_draw() {
FPS_CANVAS.draw_fps().await;
}
}
if !FB_PAUSED.load(Ordering::Acquire) {
unsafe {
FRAMEBUFFER
.as_mut()
.unwrap()
.partial_draw(&mut display)
.safe_draw(&mut display)
.await
.unwrap();
}
.unwrap()
};
}
let elapsed = start.elapsed().as_millis();
if elapsed < FRAME_TIME_MS {
Timer::after_millis(FRAME_TIME_MS - elapsed).await;
} else {
yield_now().await;
}
// small yield to allow other tasks to run
Timer::after_nanos(500).await;
}
}

View File

@@ -1,7 +1,9 @@
use crate::{
abi,
storage::{File, SDCARD},
syscalls,
};
use abi_sys::CallTable;
use abi_sys::EntryFn;
use alloc::{vec, vec::Vec};
use bumpalo::Bump;
use core::ptr;
@@ -16,7 +18,6 @@ use goblin::{
elf32::{header, reloc::Rel, section_header::SectionHeader, sym::Sym},
};
use strum::IntoEnumIterator;
use userlib_sys::{EntryFn, SyscallTable};
const ELF32_HDR_SIZE: usize = 52;
@@ -39,7 +40,7 @@ pub async unsafe fn load_binary(name: &ShortFileName) -> Option<(EntryFn, Bump)>
let mut ph_buf = vec![0_u8; elf_header.e_phentsize as usize];
let (total_size, min_vaddr, _max_vaddr) =
total_loadable_size(&mut file, elf_header, &mut ph_buf);
total_loadable_size(&mut file, &elf_header, &mut ph_buf);
let bump = Bump::with_capacity(total_size);
let base = bump.alloc_slice_fill_default::<u8>(total_size);
@@ -52,22 +53,25 @@ pub async unsafe fn load_binary(name: &ShortFileName) -> Option<(EntryFn, Bump)>
let ph = cast_phdr(&ph_buf);
let seg_offset = (ph.p_vaddr - min_vaddr) as usize;
let segment = &mut base[seg_offset..seg_offset + ph.p_memsz as usize];
let mut segment = &mut base[seg_offset..seg_offset + ph.p_memsz as usize];
if ph.p_type == PT_LOAD {
load_segment(&mut file, &ph, segment).unwrap();
load_segment(&mut file, &ph, &mut segment).unwrap();
}
}
for i in 0..elf_header.e_shnum {
let sh = read_section(&mut file, elf_header, i.into());
if sh.sh_type == SHT_REL {
match sh.sh_type {
SHT_REL => {
apply_relocations(&sh, min_vaddr, base.as_mut_ptr(), &mut file).unwrap();
}
_ => {}
}
}
patch_syscalls(elf_header, base.as_mut_ptr(), min_vaddr, &mut file).unwrap();
patch_abi(&elf_header, base.as_mut_ptr(), min_vaddr, &mut file).unwrap();
// entry pointer is base_ptr + (entry - min_vaddr)
let entry_ptr: EntryFn = unsafe {
@@ -147,14 +151,14 @@ fn apply_relocations(
Ok(())
}
fn patch_syscalls(
fn patch_abi(
elf_header: &Header,
base: *mut u8,
min_vaddr: u32,
file: &mut File,
) -> Result<(), ()> {
for i in 1..=elf_header.e_shnum {
let sh = read_section(file, elf_header, i.into());
let sh = read_section(file, &elf_header, i.into());
// find the symbol table
if sh.sh_type == SHT_SYMTAB {
@@ -169,7 +173,7 @@ fn patch_syscalls(
&symtab_buf[i * sh.sh_entsize as usize..(i + 1) * sh.sh_entsize as usize];
let sym = cast_sym(sym_bytes);
let str_sh = read_section(file, elf_header, sh.sh_link);
let str_sh = read_section(file, &elf_header, sh.sh_link);
let mut name = Vec::new();
file.seek_from_start(str_sh.sh_offset + sym.st_name)
@@ -185,30 +189,27 @@ fn patch_syscalls(
}
let symbol_name = core::str::from_utf8(&name).unwrap();
if symbol_name == stringify!(SYS_CALL_TABLE) {
if symbol_name == "CALL_ABI_TABLE" {
let table_base =
unsafe { base.add((sym.st_value as usize) - min_vaddr as usize) }
as *mut usize;
for (idx, call) in SyscallTable::iter().enumerate() {
for (idx, call) in CallTable::iter().enumerate() {
let ptr = match call {
SyscallTable::Alloc => syscalls::alloc as usize,
SyscallTable::Dealloc => syscalls::dealloc as usize,
SyscallTable::PrintString => syscalls::print as usize,
SyscallTable::SleepMs => syscalls::sleep as usize,
SyscallTable::GetMs => syscalls::get_ms as usize,
SyscallTable::DrawIter => syscalls::draw_iter as usize,
SyscallTable::GetKey => syscalls::get_key as usize,
SyscallTable::GenRand => syscalls::gen_rand as usize,
SyscallTable::ListDir => syscalls::list_dir as usize,
SyscallTable::ReadFile => syscalls::read_file as usize,
SyscallTable::WriteFile => syscalls::write_file as usize,
SyscallTable::FileLen => syscalls::file_len as usize,
SyscallTable::AudioBufferReady => syscalls::audio_buffer_ready as usize,
SyscallTable::SendAudioBuffer => syscalls::send_audio_buffer as usize,
CallTable::Alloc => abi::alloc as usize,
CallTable::Dealloc => abi::dealloc as usize,
CallTable::PrintString => abi::print as usize,
CallTable::SleepMs => abi::sleep as usize,
CallTable::GetMs => abi::get_ms as usize,
CallTable::DrawIter => abi::draw_iter as usize,
CallTable::GetKey => abi::get_key as usize,
CallTable::GenRand => abi::gen_rand as usize,
CallTable::ListDir => abi::list_dir as usize,
CallTable::ReadFile => abi::read_file as usize,
CallTable::FileLen => abi::file_len as usize,
};
unsafe {
table_base.add(idx).write(ptr);
table_base.add(idx as usize).write(ptr);
}
}
return Ok(());
@@ -230,7 +231,7 @@ fn total_loadable_size(
file.seek_from_start(elf_header.e_phoff + (elf_header.e_phentsize * i) as u32)
.unwrap();
file.read(ph_buf).unwrap();
let ph = cast_phdr(ph_buf);
let ph = cast_phdr(&ph_buf);
if ph.p_type == PT_LOAD {
if ph.p_vaddr < min_vaddr {

View File

@@ -1,5 +1,6 @@
use crate::display::{SCREEN_HEIGHT, SCREEN_WIDTH};
use core::sync::atomic::{AtomicBool, Ordering};
use embassy_sync::lazy_lock::LazyLock;
use embedded_graphics::{
draw_target::DrawTarget,
pixelcolor::{
@@ -13,40 +14,36 @@ use embedded_hal_2::digital::OutputPin;
use embedded_hal_async::{delay::DelayNs, spi::SpiDevice};
use st7365p_lcd::ST7365P;
#[cfg(feature = "fps")]
use fps::{FPS_CANVAS, FPS_CANVAS_HEIGHT, FPS_CANVAS_WIDTH, FPS_CANVAS_X, FPS_CANVAS_Y};
pub const TILE_SIZE: usize = 16; // 16x16 tile
pub const TILE_COUNT: usize = (SCREEN_WIDTH / TILE_SIZE) * (SCREEN_HEIGHT / TILE_SIZE); // 400 tiles
const TILE_SIZE: usize = 16; // 16x16 tile
const TILE_COUNT: usize = (SCREEN_WIDTH / TILE_SIZE) * (SCREEN_HEIGHT / TILE_SIZE); // 400 tiles
const NUM_TILE_ROWS: usize = SCREEN_WIDTH / TILE_SIZE;
const NUM_TILE_COLS: usize = SCREEN_WIDTH / TILE_SIZE;
const MAX_BATCH_TILES: usize = (SCREEN_WIDTH / TILE_SIZE) * 2;
type BatchTileBuf = [u16; MAX_BATCH_TILES * TILE_SIZE * TILE_SIZE];
// Group of tiles for batching
pub const MAX_META_TILES: usize = (SCREEN_WIDTH / TILE_SIZE) * 2; // max number of meta tiles in buffer
type MetaTileVec = heapless::Vec<Rectangle, { TILE_COUNT / MAX_META_TILES }>;
pub const SIZE: usize = SCREEN_HEIGHT * SCREEN_WIDTH;
pub static FB_PAUSED: AtomicBool = AtomicBool::new(false);
#[allow(dead_code)]
pub struct AtomicFrameBuffer<'a> {
fb: &'a mut [u16],
dirty_tiles: [AtomicBool; TILE_COUNT],
batch_tile_buf: BatchTileBuf,
static mut DIRTY_TILES: LazyLock<heapless::Vec<AtomicBool, TILE_COUNT>> = LazyLock::new(|| {
let mut tiles = heapless::Vec::new();
for _ in 0..TILE_COUNT {
tiles.push(AtomicBool::new(true)).unwrap();
}
tiles
});
#[allow(dead_code)]
pub struct AtomicFrameBuffer<'a>(&'a mut [u16]);
impl<'a> AtomicFrameBuffer<'a> {
pub fn new(buffer: &'a mut [u16]) -> Self {
assert!(buffer.len() == SIZE);
Self {
fb: buffer,
dirty_tiles: core::array::from_fn(|_| AtomicBool::new(true)),
batch_tile_buf: [0; MAX_BATCH_TILES * TILE_SIZE * TILE_SIZE],
}
Self(buffer)
}
fn mark_tiles_dirty(&mut self, rect: Rectangle) {
let tiles_x = SCREEN_WIDTH.div_ceil(TILE_SIZE);
let tiles_x = (SCREEN_WIDTH + TILE_SIZE - 1) / TILE_SIZE;
let start_tx = (rect.top_left.x as usize) / TILE_SIZE;
let end_tx = ((rect.top_left.x + rect.size.width as i32 - 1) as usize) / TILE_SIZE;
let start_ty = (rect.top_left.y as usize) / TILE_SIZE;
@@ -55,7 +52,7 @@ impl<'a> AtomicFrameBuffer<'a> {
for ty in start_ty..=end_ty {
for tx in start_tx..=end_tx {
let tile_idx = ty * tiles_x + tx;
self.dirty_tiles[tile_idx].store(true, Ordering::Release);
unsafe { DIRTY_TILES.get_mut()[tile_idx].store(true, Ordering::Release) };
}
}
}
@@ -81,7 +78,7 @@ impl<'a> AtomicFrameBuffer<'a> {
for y in sy..=ey {
for x in sx..=ex {
if let Some(color) = color_iter.next() {
self.fb[(y as usize * SCREEN_WIDTH) + x as usize] = color;
self.0[(y as usize * SCREEN_WIDTH) + x as usize] = color;
} else {
return Err(()); // Not enough data
}
@@ -96,17 +93,60 @@ impl<'a> AtomicFrameBuffer<'a> {
Ok(())
}
// Checks if a full draw would be faster than individual tile batches
fn should_full_draw(&self) -> bool {
let threshold_pixels = SIZE * 80 / 100;
let mut dirty_pixels = 0;
// walk the dirty tiles and mark groups of tiles(meta-tiles) for batched updates
fn find_meta_tiles(&mut self, tiles_x: usize, tiles_y: usize) -> MetaTileVec {
let mut meta_tiles: MetaTileVec = heapless::Vec::new();
self.dirty_tiles.iter().any(|tile| {
if tile.load(Ordering::Acquire) {
dirty_pixels += TILE_SIZE * TILE_SIZE;
for ty in 0..tiles_y {
let mut tx = 0;
while tx < tiles_x {
let idx = ty * tiles_x + tx;
if !unsafe { DIRTY_TILES.get()[idx].load(Ordering::Acquire) } {
tx += 1;
continue;
}
dirty_pixels >= threshold_pixels
})
// Start meta-tile at this tile
let mut width_tiles = 1;
let height_tiles = 1;
// Grow horizontally, but keep under MAX_TILES_PER_METATILE
while tx + width_tiles < tiles_x
&& unsafe {
DIRTY_TILES.get()[ty * tiles_x + tx + width_tiles].load(Ordering::Acquire)
}
&& (width_tiles + height_tiles) <= MAX_META_TILES
{
width_tiles += 1;
}
// TODO: for simplicity, skipped vertical growth
for x_off in 0..width_tiles {
unsafe {
DIRTY_TILES.get()[ty * tiles_x + tx + x_off]
.store(false, Ordering::Release);
};
}
// new meta-tile pos
let rect = Rectangle::new(
Point::new((tx * TILE_SIZE) as i32, (ty * TILE_SIZE) as i32),
Size::new(
(width_tiles * TILE_SIZE) as u32,
(height_tiles * TILE_SIZE) as u32,
),
);
if meta_tiles.push(rect).is_err() {
return meta_tiles;
};
tx += width_tiles;
}
}
meta_tiles
}
/// Sends the entire framebuffer to the display
@@ -125,104 +165,71 @@ impl<'a> AtomicFrameBuffer<'a> {
0,
self.size().width as u16 - 1,
self.size().height as u16 - 1,
&self.fb[..],
&self.0[..],
)
.await?;
for tile in self.dirty_tiles.iter() {
unsafe {
for tile in DIRTY_TILES.get_mut().iter() {
tile.store(false, Ordering::Release);
}
};
#[cfg(feature = "fps")]
unsafe {
crate::display::FPS_COUNTER.measure()
Ok(())
}
pub async fn safe_draw<SPI, DC, RST, DELAY>(
&mut self,
display: &mut ST7365P<SPI, DC, RST, DELAY>,
) -> Result<(), ()>
where
SPI: SpiDevice,
DC: OutputPin,
RST: OutputPin,
DELAY: DelayNs,
{
let tiles_x = SCREEN_WIDTH / TILE_SIZE;
let _tiles_y = SCREEN_HEIGHT / TILE_SIZE;
let tiles = unsafe { DIRTY_TILES.get_mut() };
let mut pixel_buffer: heapless::Vec<u16, { TILE_SIZE * TILE_SIZE }> = heapless::Vec::new();
for tile_idx in 0..TILE_COUNT {
if tiles[tile_idx].swap(false, Ordering::AcqRel) {
let tx = tile_idx % tiles_x;
let ty = tile_idx / tiles_x;
let x_start = tx * TILE_SIZE;
let y_start = ty * TILE_SIZE;
let x_end = (x_start + TILE_SIZE).min(SCREEN_WIDTH);
let y_end = (y_start + TILE_SIZE).min(SCREEN_HEIGHT);
pixel_buffer.clear();
for y in y_start..y_end {
let start = y * SCREEN_WIDTH + x_start;
let end = y * SCREEN_WIDTH + x_end;
pixel_buffer.extend_from_slice(&self.0[start..end]).unwrap();
}
display
.set_pixels_buffered(
x_start as u16,
y_start as u16,
(x_end - 1) as u16,
(y_end - 1) as u16,
&pixel_buffer,
)
.await
.unwrap();
}
}
Ok(())
}
// used when doing a full screen refresh fps must be drawn into fb
// unfortunately it is not garenteed to not be drawn over before
// being pushed to the display
#[cfg(feature = "fps")]
pub fn draw_fps_into_fb(&mut self) {
unsafe {
let canvas = &FPS_CANVAS.canvas;
for y in 0..FPS_CANVAS_HEIGHT {
let fb_y = FPS_CANVAS_Y + y;
let fb_row_start = fb_y * SCREEN_WIDTH + FPS_CANVAS_X;
let canvas_row_start = y * FPS_CANVAS_WIDTH;
self.fb[fb_row_start..fb_row_start + FPS_CANVAS_WIDTH].copy_from_slice(
&canvas[canvas_row_start..canvas_row_start + FPS_CANVAS_WIDTH],
);
}
}
}
// copy N tiles horizontally to the right into batch tile buf
fn append_tiles_to_batch(
&mut self,
tile_x: u16,
tile_y: u16,
total_tiles: u16, // number of tiles being written to buf
) {
debug_assert!(total_tiles as usize <= NUM_TILE_COLS);
for batch_row_num in 0..TILE_SIZE {
let batch_row_offset = batch_row_num * total_tiles as usize * TILE_SIZE;
let batch_row = &mut self.batch_tile_buf
[batch_row_offset..batch_row_offset + (total_tiles as usize * TILE_SIZE)];
let fb_row_offset = (tile_y as usize * TILE_SIZE + batch_row_num) * SCREEN_WIDTH
+ tile_x as usize * TILE_SIZE;
let fb_row =
&self.fb[fb_row_offset..fb_row_offset + (total_tiles as usize * TILE_SIZE)];
batch_row.copy_from_slice(fb_row);
// override fps pixel region with fps
// avoids writing to fps, and having it overridden before draw
#[cfg(feature = "fps")]
{
let global_y = tile_y as usize * TILE_SIZE + batch_row_num;
if global_y >= FPS_CANVAS_Y && global_y < FPS_CANVAS_Y + FPS_CANVAS_HEIGHT {
let start_x = tile_x as usize * TILE_SIZE;
let end_x = start_x + (total_tiles as usize * TILE_SIZE);
// horizontal overlap check
let fps_x0 = FPS_CANVAS_X;
let fps_x1 = FPS_CANVAS_X + FPS_CANVAS_WIDTH;
let x0 = start_x.max(fps_x0);
let x1 = end_x.min(fps_x1);
if x1 > x0 {
let row_in_fps = global_y - FPS_CANVAS_Y;
let fps_off = row_in_fps
.checked_mul(FPS_CANVAS_WIDTH)
.and_then(|v| v.checked_add(x0 - fps_x0));
let batch_off = x0 - start_x;
let len = x1 - x0;
if let Some(fps_off) = fps_off {
let fps_len_ok = fps_off + len <= unsafe { FPS_CANVAS.canvas.len() };
let batch_len_ok = batch_off + len <= batch_row.len();
if fps_len_ok && batch_len_ok {
batch_row[batch_off..batch_off + len].copy_from_slice(unsafe {
&FPS_CANVAS.canvas[fps_off..fps_off + len]
});
}
}
}
}
}
}
}
// Pushes tiles to the display in batches to avoid full frame pushes (unless needed)
/// Sends only dirty tiles (16x16px) in batches to the display
pub async fn partial_draw<SPI, DC, RST, DELAY>(
&mut self,
display: &mut ST7365P<SPI, DC, RST, DELAY>,
@@ -233,78 +240,56 @@ impl<'a> AtomicFrameBuffer<'a> {
RST: OutputPin,
DELAY: DelayNs,
{
if self.should_full_draw() {
#[cfg(feature = "fps")]
self.draw_fps_into_fb();
return self.draw(display).await;
if unsafe { DIRTY_TILES.get().iter().any(|p| p.load(Ordering::Acquire)) } {
let tiles_x = (SCREEN_WIDTH + TILE_SIZE - 1) / TILE_SIZE;
let tiles_y = (SCREEN_HEIGHT + TILE_SIZE - 1) / TILE_SIZE;
let meta_tiles = self.find_meta_tiles(tiles_x, tiles_y);
// buffer for copying meta tiles before sending to display
let mut pixel_buffer: heapless::Vec<u16, { MAX_META_TILES * TILE_SIZE * TILE_SIZE }> =
heapless::Vec::new();
for rect in meta_tiles {
let rect_width = rect.size.width as usize;
let rect_height = rect.size.height as usize;
let rect_x = rect.top_left.x as usize;
let rect_y = rect.top_left.y as usize;
pixel_buffer.clear();
for row in 0..rect_height {
let y = rect_y + row;
let start = y * SCREEN_WIDTH + rect_x;
let end = start + rect_width;
// Safe: we guarantee buffer will not exceed MAX_META_TILE_PIXELS
pixel_buffer.extend_from_slice(&self.0[start..end]).unwrap();
}
#[cfg(feature = "fps")]
{
let fps_tile_x = FPS_CANVAS_X / TILE_SIZE;
let fps_tile_y = FPS_CANVAS_Y / TILE_SIZE;
let fps_tile_w = (FPS_CANVAS_WIDTH + TILE_SIZE - 1) / TILE_SIZE;
let fps_tile_h = (FPS_CANVAS_HEIGHT + TILE_SIZE - 1) / TILE_SIZE;
for ty in fps_tile_y..fps_tile_y + fps_tile_h {
for tx in fps_tile_x..fps_tile_x + fps_tile_w {
self.dirty_tiles[ty * NUM_TILE_COLS + tx].store(true, Ordering::Release);
}
}
}
for tile_row in 0..NUM_TILE_ROWS {
let row_start_idx = tile_row * NUM_TILE_COLS;
let mut col = 0;
while col < NUM_TILE_COLS {
// Check for dirty tile
if self.dirty_tiles[row_start_idx + col].swap(false, Ordering::Acquire) {
let run_start = col;
let mut scan_col = col;
let mut run_len = 1;
// Extend run while contiguous dirty tiles and within MAX_BATCH_TILES
while scan_col + 1 < NUM_TILE_COLS
&& self.dirty_tiles[row_start_idx + col + 1].load(Ordering::Acquire)
&& run_len < MAX_BATCH_TILES
{
scan_col += 1;
run_len += 1;
}
// Copy the whole horizontal run into the batch buffer in one call
let tile_x = run_start;
let tile_y = tile_row;
self.append_tiles_to_batch(tile_x as u16, tile_y as u16, run_len as u16);
// Compute coordinates for display write
let start_x = tile_x * TILE_SIZE;
let end_x = start_x + run_len * TILE_SIZE - 1;
let start_y = tile_y * TILE_SIZE;
let end_y = start_y + TILE_SIZE - 1;
// Send batch to display
display
.set_pixels_buffered(
start_x as u16,
start_y as u16,
end_x as u16,
end_y as u16,
&self.batch_tile_buf[..run_len * TILE_SIZE * TILE_SIZE],
rect_x as u16,
rect_y as u16,
(rect_x + rect_width - 1) as u16,
(rect_y + rect_height - 1) as u16,
&pixel_buffer,
)
.await?;
col = scan_col;
}
// walk the meta-tile and set as clean
let start_tx = rect_x / TILE_SIZE;
let start_ty = rect_y / TILE_SIZE;
let end_tx = (rect_x + rect_width - 1) / TILE_SIZE;
let end_ty = (rect_y + rect_height - 1) / TILE_SIZE;
col += 1;
for ty in start_ty..=end_ty {
for tx in start_tx..=end_tx {
let tile_idx = ty * tiles_x + tx;
unsafe { DIRTY_TILES.get_mut()[tile_idx].store(false, Ordering::Release) };
}
}
}
#[cfg(feature = "fps")]
unsafe {
crate::display::FPS_COUNTER.measure()
}
Ok(())
@@ -324,14 +309,14 @@ impl<'a> DrawTarget for AtomicFrameBuffer<'a> {
for Pixel(coord, color) in pixels {
if coord.x >= 0 && coord.y >= 0 {
let x = coord.x;
let y = coord.y;
let x = coord.x as i32;
let y = coord.y as i32;
if (x as usize) < SCREEN_WIDTH && (y as usize) < SCREEN_HEIGHT {
let idx = (y as usize) * SCREEN_WIDTH + (x as usize);
let raw_color = RawU16::from(color).into_inner();
if self.fb[idx] != raw_color {
self.fb[idx] = raw_color;
if self.0[idx] != raw_color {
self.0[idx] = raw_color;
changed = true;
}
@@ -349,9 +334,11 @@ impl<'a> DrawTarget for AtomicFrameBuffer<'a> {
}
}
if changed && let Some(rect) = dirty_rect {
if changed {
if let Some(rect) = dirty_rect {
self.mark_tiles_dirty(rect);
}
}
Ok(())
}
@@ -378,8 +365,8 @@ impl<'a> DrawTarget for AtomicFrameBuffer<'a> {
if let Some(color) = colors.next() {
let idx = (p.y as usize * SCREEN_WIDTH) + (p.x as usize);
let raw_color = RawU16::from(color).into_inner();
if self.fb[idx] != raw_color {
self.fb[idx] = raw_color;
if self.0[idx] != raw_color {
self.0[idx] = raw_color;
changed = true;
}
} else {
@@ -403,7 +390,7 @@ impl<'a> DrawTarget for AtomicFrameBuffer<'a> {
fn fill_solid(&mut self, area: &Rectangle, color: Self::Color) -> Result<(), Self::Error> {
self.fill_contiguous(
area,
core::iter::repeat_n(color, (self.size().width * self.size().height) as usize),
core::iter::repeat(color).take((self.size().width * self.size().height) as usize),
)
}
@@ -413,13 +400,11 @@ impl<'a> DrawTarget for AtomicFrameBuffer<'a> {
0,
self.size().width as u16 - 1,
self.size().height as u16 - 1,
core::iter::repeat_n(
RawU16::from(color).into_inner(),
(self.size().width * self.size().height) as usize,
),
core::iter::repeat(RawU16::from(color).into_inner())
.take((self.size().width * self.size().height) as usize),
)?;
for tile in self.dirty_tiles.iter() {
for tile in unsafe { DIRTY_TILES.get_mut() }.iter() {
tile.store(true, Ordering::Release);
}
@@ -432,146 +417,3 @@ impl<'a> OriginDimensions for AtomicFrameBuffer<'a> {
Size::new(SCREEN_WIDTH as u32, SCREEN_HEIGHT as u32)
}
}
#[cfg(feature = "fps")]
pub mod fps {
use crate::display::SCREEN_WIDTH;
use core::fmt::Write;
use embassy_time::{Duration, Instant};
use embedded_graphics::{
Drawable, Pixel,
draw_target::DrawTarget,
geometry::Point,
mono_font::{MonoTextStyle, ascii::FONT_8X13},
pixelcolor::Rgb565,
prelude::{IntoStorage, OriginDimensions, RgbColor, Size},
text::{Alignment, Text},
};
pub static mut FPS_COUNTER: FpsCounter = FpsCounter::new();
pub static mut FPS_CANVAS: FpsCanvas = FpsCanvas::new();
// "FPS: 120" = 8 len
const FPS_LEN: usize = 8;
pub const FPS_CANVAS_WIDTH: usize = (FONT_8X13.character_size.width + 4) as usize * FPS_LEN;
pub const FPS_CANVAS_HEIGHT: usize = FONT_8X13.character_size.height as usize;
// puts canvas in the top right of the display
// top left point of canvas
pub const FPS_CANVAS_X: usize = SCREEN_WIDTH - FPS_CANVAS_WIDTH;
pub const FPS_CANVAS_Y: usize = 0;
pub struct FpsCanvas {
pub canvas: [u16; FPS_CANVAS_HEIGHT * FPS_CANVAS_WIDTH],
}
impl FpsCanvas {
const fn new() -> Self {
Self {
canvas: [0; FPS_CANVAS_HEIGHT * FPS_CANVAS_WIDTH],
}
}
fn clear(&mut self) {
for p in &mut self.canvas {
*p = 0;
}
}
pub async fn draw_fps(&mut self) {
let mut buf: heapless::String<FPS_LEN> = heapless::String::new();
let fps = unsafe { FPS_COUNTER.smoothed };
let _ = write!(buf, "FPS: {}", fps as u8);
self.clear();
let text_style = MonoTextStyle::new(&FONT_8X13, Rgb565::WHITE);
Text::with_alignment(
buf.as_str(),
Point::new(
FPS_CANVAS_WIDTH as i32 / 2,
(FPS_CANVAS_HEIGHT as i32 + 8) / 2,
),
text_style,
Alignment::Center,
)
.draw(self)
.unwrap();
}
}
impl DrawTarget for FpsCanvas {
type Error = ();
type Color = Rgb565;
fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
where
I: IntoIterator<Item = Pixel<Self::Color>>,
{
for Pixel(point, color) in pixels {
if point.x < 0
|| point.x >= FPS_CANVAS_WIDTH as i32
|| point.y < 0
|| point.y >= FPS_CANVAS_HEIGHT as i32
{
continue;
}
let index = (point.y as usize) * FPS_CANVAS_WIDTH + point.x as usize;
self.canvas[index] = color.into_storage();
}
Ok(())
}
}
impl OriginDimensions for FpsCanvas {
fn size(&self) -> Size {
Size::new(FPS_CANVAS_WIDTH as u32, FPS_CANVAS_HEIGHT as u32)
}
}
pub struct FpsCounter {
last_frame: Option<Instant>,
smoothed: f32,
last_draw: Option<Instant>,
}
impl FpsCounter {
pub const fn new() -> Self {
Self {
last_frame: None,
smoothed: 0.0,
last_draw: None,
}
}
// Is called once per frame or partial frame to update FPS
pub fn measure(&mut self) {
let now = Instant::now();
if let Some(last) = self.last_frame {
let dt_us = (now - last).as_micros() as f32;
if dt_us > 0.0 {
let current = 1_000_000.0 / dt_us;
self.smoothed = if self.smoothed == 0.0 {
current
} else {
0.9 * self.smoothed + 0.1 * current
};
}
}
self.last_frame = Some(now);
}
pub fn should_draw(&mut self) -> bool {
let now = Instant::now();
match self.last_draw {
Some(last) if now - last < Duration::from_millis(200) => false,
_ => {
self.last_draw = Some(now);
true
}
}
}
}
}

View File

@@ -22,8 +22,8 @@ impl Region {
fn contains(&self, address: usize) -> bool {
let start = self.start.load(Ordering::Relaxed);
let end = start + self.size.load(Ordering::Relaxed);
(start..end).contains(&address)
let end = self.start.load(Ordering::Relaxed);
(start..start + end).contains(&address)
}
fn new(start: usize, size: usize) -> Self {

View File

@@ -8,58 +8,50 @@
extern crate alloc;
mod audio;
mod abi;
mod display;
mod elf;
mod framebuffer;
// TODO: NEED TO UPDATE MCU TO TEST BATTERY READS
#[allow(unused)]
mod heap;
mod peripherals;
#[allow(unused)]
mod psram;
mod scsi;
mod storage;
mod syscalls;
mod ui;
#[allow(unused)]
mod usb;
mod utils;
#[cfg(feature = "psram")]
#[allow(unused)]
mod heap;
#[cfg(feature = "psram")]
#[allow(unused)]
mod psram;
#[cfg(feature = "psram")]
#[cfg(feature = "pimoroni2w")]
use crate::{heap::init_qmi_psram_heap, psram::init_psram_qmi};
use crate::{
audio::{AUDIO_BUFFER_WRITTEN, audio_handler, clear_audio_buffers},
abi::{KEY_CACHE, MS_SINCE_LAUNCH},
display::{FRAMEBUFFER, display_handler, init_display},
peripherals::{conf_peripherals, keyboard::read_keyboard_fifo},
peripherals::{
conf_peripherals,
keyboard::{KeyState, read_keyboard_fifo},
},
psram::init_psram,
scsi::MSC_SHUTDOWN,
storage::{SDCARD, SdCard},
syscalls::{KEY_CACHE, MS_SINCE_LAUNCH},
ui::{SELECTIONS, clear_selection, ui_handler},
};
use abi_sys::EntryFn;
use bumpalo::Bump;
use core::sync::atomic::{AtomicBool, Ordering};
use embassy_executor::{Executor, Spawner};
use embassy_futures::{join::join, select::select};
use embassy_rp::{
Peri,
clocks::ClockConfig,
config::Config,
gpio::{Input, Level, Output, Pull},
i2c::{self, I2c},
multicore::{Stack, spawn_core1},
peripherals::{
DMA_CH0, DMA_CH1, DMA_CH3, DMA_CH4, I2C1, PIN_2, PIN_3, PIN_6, PIN_7, PIN_10, PIN_11,
PIN_12, PIN_13, PIN_14, PIN_15, PIN_16, PIN_17, PIN_18, PIN_19, PIN_20, PIN_21, PIN_22,
PIN_26, PIN_27, PIO0, SPI0, SPI1, USB, WATCHDOG,
PIO0, SPI0, SPI1, USB, WATCHDOG,
},
pio::{self, Common, Pio, StateMachine},
pio,
spi::{self, Spi},
usb as embassy_rp_usb,
watchdog::{ResetReason, Watchdog},
@@ -76,7 +68,6 @@ use embedded_hal_bus::spi::ExclusiveDevice;
use embedded_sdmmc::SdCard as SdmmcSdCard;
use static_cell::StaticCell;
use talc::*;
use userlib_sys::EntryFn;
use {defmt_rtt as _, panic_probe as _};
embassy_rp::bind_interrupts!(struct Irqs {
@@ -106,7 +97,7 @@ async fn watchdog_task(mut watchdog: Watchdog) {
ResetReason::Forced => "forced",
ResetReason::TimedOut => "timed out",
};
#[cfg(feature = "defmt")]
#[cfg(feature = "debug")]
defmt::error!("Watchdog reset reason: {}", _reason);
}
@@ -122,17 +113,9 @@ async fn watchdog_task(mut watchdog: Watchdog) {
static ENABLE_UI: AtomicBool = AtomicBool::new(true);
static UI_CHANGE: Signal<CriticalSectionRawMutex, ()> = Signal::new();
const OVERCLOCK: u32 = 300_000_000;
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
let p = if cfg!(feature = "overclock") {
let clocks = ClockConfig::system_freq(OVERCLOCK).unwrap();
let config = Config::new(clocks);
embassy_rp::init(config)
} else {
embassy_rp::init(Default::default())
};
let p = embassy_rp::init(Default::default());
spawn_core1(
p.CORE1,
@@ -154,19 +137,6 @@ async fn main(_spawner: Spawner) {
data: p.PIN_14,
reset: p.PIN_15,
};
let Pio {
common, sm0, sm1, ..
} = Pio::new(p.PIO0, Irqs);
let audio = Audio {
pio: common,
sm0,
dma0: p.DMA_CH3,
left: p.PIN_26,
sm1,
dma1: p.DMA_CH4,
right: p.PIN_27,
};
let sd = Sd {
spi: p.SPI0,
clk: p.PIN_18,
@@ -175,15 +145,15 @@ async fn main(_spawner: Spawner) {
cs: p.PIN_17,
det: p.PIN_22,
};
// let psram = Psram {
// pio: p.PIO0,
// sclk: p.PIN_21,
// mosi: p.PIN_2,
// miso: p.PIN_3,
// cs: p.PIN_20,
// dma1: p.DMA_CH3,
// dma2: p.DMA_CH4,
// };
let psram = Psram {
pio: p.PIO0,
sclk: p.PIN_21,
mosi: p.PIN_2,
miso: p.PIN_3,
cs: p.PIN_20,
dma1: p.DMA_CH3,
dma2: p.DMA_CH4,
};
let mcu = Mcu {
i2c: p.I2C1,
clk: p.PIN_7,
@@ -193,7 +163,7 @@ async fn main(_spawner: Spawner) {
executor0.run(|spawner| {
spawner
.spawn(kernel_task(
spawner, p.WATCHDOG, display, audio, sd, mcu, p.USB,
spawner, p.WATCHDOG, display, sd, psram, mcu, p.USB,
))
.unwrap()
});
@@ -226,9 +196,6 @@ async fn userland_task() {
// enable kernel ui
{
AUDIO_BUFFER_WRITTEN.store(false, Ordering::Release);
clear_audio_buffers();
ENABLE_UI.store(true, Ordering::Release);
UI_CHANGE.signal(());
unsafe { FRAMEBUFFER.as_mut().unwrap().clear(Rgb565::BLACK).unwrap() };
@@ -250,15 +217,6 @@ struct Display {
data: Peri<'static, PIN_14>,
reset: Peri<'static, PIN_15>,
}
struct Audio {
pio: Common<'static, PIO0>,
dma0: Peri<'static, DMA_CH3>,
sm0: StateMachine<'static, PIO0, 0>,
left: Peri<'static, PIN_26>,
dma1: Peri<'static, DMA_CH4>,
sm1: StateMachine<'static, PIO0, 1>,
right: Peri<'static, PIN_27>,
}
struct Sd {
spi: Peri<'static, SPI0>,
clk: Peri<'static, PIN_18>,
@@ -267,7 +225,6 @@ struct Sd {
cs: Peri<'static, PIN_17>,
det: Peri<'static, PIN_22>,
}
#[allow(dead_code)]
struct Psram {
pio: Peri<'static, PIO0>,
sclk: Peri<'static, PIN_21>,
@@ -293,7 +250,7 @@ async fn setup_mcu(mcu: Mcu) {
async fn setup_display(display: Display, spawner: Spawner) {
let mut config = spi::Config::default();
config.frequency = 192_000_000;
config.frequency = 64_000_000;
let spi = Spi::new(
display.spi,
display.clk,
@@ -309,26 +266,26 @@ async fn setup_display(display: Display, spawner: Spawner) {
// psram is kind of useless on the pico calc
// ive opted to use the pimoroni with on onboard xip psram instead
// async fn setup_psram(psram: Psram) {
// let psram = init_psram(
// psram.pio, psram.sclk, psram.mosi, psram.miso, psram.cs, psram.dma1, psram.dma2,
// )
// .await;
async fn setup_psram(psram: Psram) {
let psram = init_psram(
psram.pio, psram.sclk, psram.mosi, psram.miso, psram.cs, psram.dma1, psram.dma2,
)
.await;
// #[cfg(feature = "defmt")]
// defmt::info!("psram size: {}", psram.size);
#[cfg(feature = "defmt")]
defmt::info!("psram size: {}", psram.size);
// if psram.size == 0 {
// #[cfg(feature = "defmt")]
// defmt::info!("\u{1b}[1mExternal PSRAM was NOT found!\u{1b}[0m");
// }
// }
if psram.size == 0 {
#[cfg(feature = "defmt")]
defmt::info!("\u{1b}[1mExternal PSRAM was NOT found!\u{1b}[0m");
}
}
#[cfg(feature = "psram")]
#[cfg(feature = "pimoroni2w")]
async fn setup_qmi_psram() {
Timer::after_millis(250).await;
let psram_qmi_size = init_psram_qmi(&embassy_rp::pac::QMI, &embassy_rp::pac::XIP_CTRL);
#[cfg(feature = "defmt")]
#[cfg(feature = "debug")]
defmt::info!("size: {}", psram_qmi_size);
Timer::after_millis(100).await;
@@ -360,9 +317,8 @@ async fn kernel_task(
spawner: Spawner,
watchdog: Peri<'static, WATCHDOG>,
display: Display,
audio: Audio,
sd: Sd,
// _psram: Psram,
psram: Psram,
mcu: Mcu,
usb: Peri<'static, USB>,
) {
@@ -370,25 +326,15 @@ async fn kernel_task(
.spawn(watchdog_task(Watchdog::new(watchdog)))
.unwrap();
#[cfg(feature = "defmt")]
defmt::info!("Clock: {}", embassy_rp::clocks::clk_sys_freq());
setup_mcu(mcu).await;
#[cfg(feature = "defmt")]
defmt::info!("setting up psram");
Timer::after_millis(100).await;
// setup_psram(psram).await;
#[cfg(feature = "psram")]
#[cfg(feature = "pimoroni2w")]
setup_qmi_psram().await;
Timer::after_millis(100).await;
setup_display(display, spawner).await;
setup_sd(sd).await;
spawner.spawn(audio_handler(audio)).unwrap();
let _usb = embassy_rp_usb::Driver::new(usb, Irqs);
// spawner.spawn(usb_handler(usb)).unwrap();
@@ -408,8 +354,7 @@ async fn prog_search_handler() {
let mut guard = SDCARD.get().lock().await;
let sd = guard.as_mut().unwrap();
let mut files = sd.list_files_by_extension(".bin").unwrap();
files.sort();
let files = sd.list_files_by_extension(".bin").unwrap();
let mut select = SELECTIONS.lock().await;
if *select.selections() != files {
@@ -424,10 +369,12 @@ async fn prog_search_handler() {
async fn key_handler() {
loop {
if let Some(event) = read_keyboard_fifo().await {
if let KeyState::Pressed = event.state {
unsafe {
let _ = KEY_CACHE.enqueue(event);
}
}
}
Timer::after_millis(50).await;
}
}

View File

@@ -1,5 +1,5 @@
use crate::peripherals::PERIPHERAL_BUS;
pub use userlib_sys::keyboard::{KeyCode, KeyEvent, KeyState, Modifiers};
pub use abi_sys::keyboard::{KeyCode, KeyEvent, KeyState, Modifiers};
const REG_ID_KEY: u8 = 0x04;
const REG_ID_FIF: u8 = 0x09;

View File

@@ -526,7 +526,7 @@ pub fn init_psram_qmi(
let psram_size = detect_psram_qmi(qmi);
if psram_size == 0 {
#[cfg(feature = "defmt")]
#[cfg(feature = "debug")]
defmt::error!("qmi psram size 0");
return 0;
}
@@ -549,13 +549,13 @@ pub fn init_psram_qmi(
let min_deselect: u32 = ((18 * 1_000_000 + (clock_period_fs - 1)) / clock_period_fs
- u64::from(divisor + 1) / 2) as u32;
// #[cfg(feature = "defmt")]
// defmt::info!(
// "clock_period_fs={} max_select={} min_deselect={}",
// clock_period_fs,
// max_select,
// min_deselect
// );
#[cfg(feature = "defmt")]
defmt::info!(
"clock_period_fs={} max_select={} min_deselect={}",
clock_period_fs,
max_select,
min_deselect
);
qmi.direct_csr().write(|w| {
w.set_clkdiv(10);

View File

@@ -30,7 +30,7 @@ pub struct MassStorageClass<'d, D: Driver<'d>> {
bulk_in: D::EndpointIn,
}
impl<'d, D: Driver<'d>> MassStorageClass<'d, D> {
impl<'d, 's, D: Driver<'d>> MassStorageClass<'d, D> {
pub fn new(builder: &mut Builder<'d, D>, temp_sd: Option<SdCard>) -> Self {
let mut function = builder.function(0x08, SUBCLASS_SCSI, 0x50); // Mass Storage class
let mut interface = function.interface();
@@ -72,10 +72,9 @@ impl<'d, D: Driver<'d>> MassStorageClass<'d, D> {
async fn handle_cbw(&mut self) {
let mut cbw_buf = [0u8; 31];
if let Ok(n) = self.bulk_out.read(&mut cbw_buf).await
&& n == 31
&& let Some(cbw) = CommandBlockWrapper::parse(&cbw_buf[..n])
{
if let Ok(n) = self.bulk_out.read(&mut cbw_buf).await {
if n == 31 {
if let Some(cbw) = CommandBlockWrapper::parse(&cbw_buf[..n]) {
// Take sdcard to increase speed
if self.temp_sd.is_none() {
let mut guard = SDCARD.get().lock().await;
@@ -95,13 +94,15 @@ impl<'d, D: Driver<'d>> MassStorageClass<'d, D> {
self.send_csw_fail(cbw.dCBWTag).await
}
if self.pending_eject
&& let ScsiCommand::Write { lba: _, len: _ } = command
{
if self.pending_eject {
if let ScsiCommand::Write { lba: _, len: _ } = command {
MSC_SHUTDOWN.signal(());
}
}
}
}
}
}
async fn handle_command(&mut self, command: ScsiCommand) -> Result<(), ()> {
let mut response: Vec<u8, BULK_ENDPOINT_PACKET_SIZE> = Vec::new();
@@ -237,7 +238,7 @@ impl<'d, D: Driver<'d>> MassStorageClass<'d, D> {
let block_size = SdCard::BLOCK_SIZE as u64;
let total_blocks = self.temp_sd.as_ref().unwrap().size() / block_size;
let last_lba = total_blocks.saturating_sub(1);
let last_lba = total_blocks.checked_sub(1).unwrap_or(0);
response.extend_from_slice(&(last_lba as u32).to_be_bytes())?;
response.extend_from_slice(&(block_size as u32).to_be_bytes())?;
@@ -248,7 +249,7 @@ impl<'d, D: Driver<'d>> MassStorageClass<'d, D> {
let block_size = SdCard::BLOCK_SIZE as u64;
let total_blocks = self.temp_sd.as_ref().unwrap().size() / block_size;
let last_lba = total_blocks.saturating_sub(1);
let last_lba = total_blocks.checked_sub(1).unwrap_or(0);
response.extend_from_slice(&last_lba.to_be_bytes())?; // 8 bytes last LBA
response.extend_from_slice(&(block_size as u32).to_be_bytes())?; // 4 bytes block length
@@ -268,7 +269,7 @@ impl<'d, D: Driver<'d>> MassStorageClass<'d, D> {
sdcard.read_blocks(block_buf, BlockIdx(idx as u32))?;
for block in &mut *block_buf {
for chunk in block.contents.chunks(BULK_ENDPOINT_PACKET_SIZE) {
for chunk in block.contents.chunks(BULK_ENDPOINT_PACKET_SIZE.into()) {
self.bulk_in.write(chunk).await.map_err(|_| ())?;
}
}
@@ -280,7 +281,7 @@ impl<'d, D: Driver<'d>> MassStorageClass<'d, D> {
.read_blocks(&mut block_buf[..blocks as usize], BlockIdx(idx as u32))?;
for block in &block_buf[..blocks as usize] {
for chunk in block.contents.chunks(BULK_ENDPOINT_PACKET_SIZE) {
for chunk in block.contents.chunks(BULK_ENDPOINT_PACKET_SIZE.into()) {
self.bulk_in.write(chunk).await.map_err(|_| ())?;
}
}
@@ -300,7 +301,8 @@ impl<'d, D: Driver<'d>> MassStorageClass<'d, D> {
while blocks > 0 {
if blocks >= block_buf.len() as u64 {
for block in block_buf.as_mut() {
for chunk in block.contents.chunks_mut(BULK_ENDPOINT_PACKET_SIZE) {
for chunk in block.contents.chunks_mut(BULK_ENDPOINT_PACKET_SIZE.into())
{
self.bulk_out.read(chunk).await.map_err(|_| ())?;
}
}
@@ -311,7 +313,8 @@ impl<'d, D: Driver<'d>> MassStorageClass<'d, D> {
idx += block_buf.len() as u64;
} else {
for block in block_buf[..blocks as usize].as_mut() {
for chunk in block.contents.chunks_mut(BULK_ENDPOINT_PACKET_SIZE) {
for chunk in block.contents.chunks_mut(BULK_ENDPOINT_PACKET_SIZE.into())
{
self.bulk_out.read(chunk).await.map_err(|_| ())?;
}
}

View File

@@ -1,7 +1,7 @@
use num_enum::TryFromPrimitive;
/// THE CODE BELOW ORIGINATES FROM: https://github.com/apohrebniak/usbd-storage/blob/master/usbd-storage/src/subclass/scsi.rs
///
/// SCSI device subclass code
pub const SUBCLASS_SCSI: u8 = 0x06; // SCSI Transparent command set
@@ -91,7 +91,6 @@ pub enum ScsiCommand {
},
}
#[allow(clippy::enum_variant_names)]
#[repr(u8)]
#[derive(Copy, Clone, Debug, TryFromPrimitive)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]

View File

@@ -9,8 +9,8 @@ use embassy_sync::mutex::Mutex;
use embassy_time::Delay;
use embedded_hal_bus::spi::ExclusiveDevice;
use embedded_sdmmc::{
Block, BlockDevice, BlockIdx, Directory, SdCard as SdmmcSdCard, TimeSource, Timestamp,
VolumeIdx, VolumeManager, sdcard::Error,
Block, BlockCount, BlockDevice, BlockIdx, Directory, SdCard as SdmmcSdCard, TimeSource,
Timestamp, Volume, VolumeIdx, VolumeManager, sdcard::Error,
};
use embedded_sdmmc::{File as SdFile, LfnBuffer, Mode, ShortFileName};
@@ -21,6 +21,7 @@ pub const MAX_VOLUMES: usize = 1;
type Device = ExclusiveDevice<Spi<'static, SPI0, Blocking>, Output<'static>, embassy_time::Delay>;
type SD = SdmmcSdCard<Device, Delay>;
type VolMgr = VolumeManager<SD, DummyTimeSource, MAX_DIRS, MAX_FILES, MAX_VOLUMES>;
type Vol<'a> = Volume<'a, SD, DummyTimeSource, MAX_DIRS, MAX_FILES, MAX_VOLUMES>;
pub type Dir<'a> = Directory<'a, SD, DummyTimeSource, MAX_DIRS, MAX_FILES, MAX_VOLUMES>;
pub type File<'a> = SdFile<'a, SD, DummyTimeSource, MAX_DIRS, MAX_FILES, MAX_VOLUMES>;
@@ -34,24 +35,12 @@ impl TimeSource for DummyTimeSource {
}
}
#[derive(Clone, PartialEq, Eq)]
#[derive(Clone, PartialEq)]
pub struct FileName {
pub long_name: String,
pub short_name: ShortFileName,
}
impl PartialOrd for FileName {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for FileName {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
self.long_name.cmp(&other.long_name)
}
}
pub struct SdCard {
det: Input<'static>,
volume_mgr: VolMgr,
@@ -66,7 +55,10 @@ impl SdCard {
DummyTimeSource {},
5000,
);
Self { det, volume_mgr }
Self {
det: det,
volume_mgr,
}
}
/// Returns true if an SD card is inserted.
@@ -86,6 +78,17 @@ impl SdCard {
result
}
pub fn num_blocks(&self) -> u32 {
let mut result = 0;
self.volume_mgr.device(|sd| {
result = sd.num_blocks().unwrap_or(BlockCount(0)).0;
DummyTimeSource {}
});
result
}
pub fn read_blocks(&self, blocks: &mut [Block], start_block_idx: BlockIdx) -> Result<(), ()> {
let mut res: Result<(), Error> = Ok(());
self.volume_mgr.device(|sd| {

View File

@@ -2,6 +2,7 @@ use crate::{
BINARY_CH, display::FRAMEBUFFER, elf::load_binary, framebuffer::FB_PAUSED,
peripherals::keyboard, storage::FileName,
};
use abi_sys::keyboard::{KeyCode, KeyState};
use alloc::{str::FromStr, string::String, vec::Vec};
use core::sync::atomic::Ordering;
use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex};
@@ -19,16 +20,14 @@ use embedded_layout::{
prelude::*,
};
use embedded_text::TextBox;
use userlib_sys::keyboard::{KeyCode, KeyState};
pub static SELECTIONS: Mutex<CriticalSectionRawMutex, SelectionList> =
Mutex::new(SelectionList::new());
pub async fn ui_handler() {
loop {
if let Some(event) = keyboard::read_keyboard_fifo().await
&& let KeyState::Pressed = event.state
{
if let Some(event) = keyboard::read_keyboard_fifo().await {
if let KeyState::Pressed = event.state {
match event.key {
KeyCode::Up => {
let mut selections = SELECTIONS.lock().await;
@@ -53,6 +52,7 @@ pub async fn ui_handler() {
_ => (),
}
}
}
let changed = SELECTIONS.lock().await.changed;
if changed {
@@ -75,7 +75,7 @@ pub async fn clear_selection() {
async fn draw_selection() {
let mut guard = SELECTIONS.lock().await;
let file_names = guard.selections.clone();
let file_names = &guard.selections.clone();
let text_style = MonoTextStyle::new(&FONT_10X20, Rgb565::WHITE);
let display_area = unsafe { FRAMEBUFFER.as_mut().unwrap().bounding_box() };
@@ -99,7 +99,7 @@ async fn draw_selection() {
} else {
let mut views: alloc::vec::Vec<Text<MonoTextStyle<Rgb565>>> = Vec::new();
for i in &file_names {
for i in file_names {
views.push(Text::new(&i.long_name, Point::zero(), text_style));
}

Submodule picolibc deleted from d664643068

View File

@@ -1,10 +0,0 @@
[package]
name = "selection_ui"
version = "0.1.0"
edition = "2024"
[dependencies]
userlib = { path = "../userlib" }
embedded-graphics = "0.8.1"
embedded-layout = "0.4.2"
embedded-text = "0.7.3"

View File

@@ -1,153 +0,0 @@
#![no_std]
extern crate alloc;
use alloc::vec::Vec;
use embedded_graphics::{
mono_font::{ascii::FONT_10X20, MonoTextStyle},
pixelcolor::Rgb565,
prelude::{Dimensions, DrawTarget, Point, Primitive, RgbColor},
primitives::{PrimitiveStyle, Rectangle},
text::{renderer::TextRenderer, Alignment, Text},
Drawable,
};
use embedded_layout::{
align::{horizontal, vertical},
layout::linear::{FixedMargin, LinearLayout},
prelude::*,
};
use userlib::{
display::Display,
get_key,
keyboard::{KeyCode, KeyState},
};
#[derive(Debug)]
pub enum SelectionUiError<DisplayError> {
SelectionListEmpty,
DisplayError(DisplayError),
}
pub struct SelectionUi<'a> {
selection: usize,
items: &'a [&'a str],
last_bounds: Option<Rectangle>,
}
impl<'a> SelectionUi<'a> {
pub fn new(items: &'a [&'a str]) -> Self {
Self {
selection: 0,
items,
last_bounds: None,
}
}
pub fn run_selection_ui(
&mut self,
display: &mut Display,
) -> Result<Option<usize>, SelectionUiError<<Display as DrawTarget>::Error>> {
self.draw(display)?;
let selection;
loop {
let key = get_key();
if key.state == KeyState::Pressed
&& let Some(s) = self.update(display, key.key)? {
selection = Some(s);
display
.clear(Rgb565::BLACK)
.map_err(SelectionUiError::DisplayError)?;
break;
}
}
Ok(selection)
}
/// updates the display with a new keypress.
/// returns selection idx if selected
pub fn update(
&mut self,
display: &mut Display,
key: KeyCode,
) -> Result<Option<usize>, SelectionUiError<<Display as DrawTarget>::Error>> {
match key {
KeyCode::Down => {
self.selection = (self.selection + 1).min(self.items.len() - 1);
}
KeyCode::Up => {
self.selection = self.selection.saturating_sub(1);
}
KeyCode::Enter | KeyCode::Right => return Ok(Some(self.selection)),
_ => return Ok(None),
};
self.draw(display)?;
Ok(None)
}
fn draw(
&mut self,
display: &mut Display,
) -> Result<(), SelectionUiError<<Display as DrawTarget>::Error>> {
let text_style = MonoTextStyle::new(&FONT_10X20, Rgb565::WHITE);
let display_area = display.bounding_box();
if self.items.is_empty() {
return Err(SelectionUiError::SelectionListEmpty);
}
if let Some(bounds) = self.last_bounds {
Rectangle::new(bounds.top_left, bounds.size)
.into_styled(PrimitiveStyle::with_fill(Rgb565::BLACK))
.draw(display)
.map_err(SelectionUiError::DisplayError)?;
}
let mut views: Vec<Text<MonoTextStyle<Rgb565>>> = Vec::new();
for i in self.items {
views.push(Text::new(i, Point::zero(), text_style));
}
let views_group = Views::new(views.as_mut_slice());
let layout = LinearLayout::vertical(views_group)
.with_alignment(horizontal::Center)
.with_spacing(FixedMargin(5))
.arrange()
.align_to(&display_area, horizontal::Center, vertical::Center);
layout
.draw(display)
.map_err(SelectionUiError::DisplayError)?;
// draw selected box
if let Some(selected_bounds) = layout.inner().get(self.selection) {
let selected_bounds = selected_bounds.bounding_box();
Rectangle::new(selected_bounds.top_left, selected_bounds.size)
.into_styled(PrimitiveStyle::with_stroke(Rgb565::WHITE, 1))
.draw(display)
.map_err(SelectionUiError::DisplayError)?;
self.last_bounds = Some(selected_bounds);
}
Ok(())
}
}
pub fn draw_text_center<S>(
display: &mut Display,
text: &str,
style: S,
) -> Result<Point, <Display as DrawTarget>::Error>
where
S: TextRenderer<Color = <Display as DrawTarget>::Color>,
{
Text::with_alignment(
text,
display.bounding_box().center(),
style,
Alignment::Center,
)
.draw(display)
}

View File

@@ -4,6 +4,6 @@ version = "0.1.0"
edition = "2024"
[dependencies]
userlib = { path = "../../userlib" }
abi = { path = "../../abi" }
embedded-graphics = "0.8.1"
embedded-layout = "0.4.2"

View File

@@ -2,6 +2,12 @@
#![no_main]
extern crate alloc;
use abi::{
display::Display,
get_key,
keyboard::{KeyCode, KeyState},
print,
};
use alloc::{format, string::String, vec, vec::Vec};
use core::panic::PanicInfo;
use embedded_graphics::{
@@ -19,16 +25,10 @@ use embedded_layout::{
layout::linear::LinearLayout,
prelude::Chain,
};
use userlib::{
display::Display,
get_key,
keyboard::{KeyCode, KeyState},
println,
};
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("user panic: {} @ {:?}", info.message(), info.location(),);
print!("user panic: {} @ {:?}", info.message(), info.location(),);
loop {}
}
@@ -38,8 +38,8 @@ pub extern "Rust" fn _start() {
}
pub fn main() {
println!("Starting Calculator app");
let mut display = Display::take().unwrap();
print!("Starting Calculator app");
let mut display = Display;
let mut input = vec!['e', 'x', 'p', 'r', ':', ' '];
let input_min = input.len();
@@ -83,7 +83,7 @@ pub fn main() {
.align_to(&display.bounding_box(), horizontal::Left, vertical::Center);
let result = if let Ok(result) = evaluate(&input[input_min..]) {
&format!(" = {result}")
&format!(" = {}", result)
} else {
" = Error"
};
@@ -104,7 +104,7 @@ pub fn main() {
}
let event = get_key();
if event.state == KeyState::Released {
if event.state != KeyState::Idle {
match event.key {
KeyCode::Char(ch) => {
input.push(ch);

12
user-apps/doom/Cargo.toml Normal file
View File

@@ -0,0 +1,12 @@
[package]
name = "doom"
version = "0.1.0"
edition = "2024"
[dependencies]
abi = { path = "../../abi" }
embedded-graphics = "0.8.1"
[build-dependencies]
bindgen = "0.71.0"
cc = "1.0"

126
user-apps/doom/build.rs Normal file
View File

@@ -0,0 +1,126 @@
//! This build script copies the `memory.x` file from the crate root into
//! a directory where the linker can always find it at build time.
//! For many projects this is optional, as the linker always searches the
//! project root directory -- wherever `Cargo.toml` is. However, if you
//! are using a workspace or have a more complicated build setup, this
//! build script becomes required. Additionally, by requesting that
//! Cargo re-run the build script whenever `memory.x` is changed,
//! updating `memory.x` ensures a rebuild of the application with the
//! new memory settings.
use std::env;
use std::fs::File;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command;
fn main() {
build_newlib();
build_doom();
// Put `memory.x` 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("memory.x"))
.unwrap()
.write_all(include_bytes!("memory.x"))
.unwrap();
println!("cargo:rustc-link-search={}", out.display());
println!("cargo:rerun-if-changed=memory.x");
println!("cargo:rustc-link-arg-bins=-Tmemory.x");
}
// TODO: use target to change newlib build - only workds for rp2350 right now
fn build_newlib() {
let out_dir = std::env::var("OUT_DIR").unwrap();
let picolibc_dir = format!("{}/picolibc", out_dir);
if !Path::new(&picolibc_dir).exists() {
let status = Command::new("git")
.args(&[
"clone",
"https://github.com/picolibc/picolibc",
&picolibc_dir,
])
.status()
.expect("Failed to clone picolibc");
assert!(status.success());
}
let build_dir = format!("{}/build", picolibc_dir);
std::fs::create_dir_all(&build_dir).unwrap();
let status = Command::new(&format!("{picolibc_dir}/scripts/do-configure"))
.current_dir(&build_dir)
.args(&[
"thumbv8m_main_fp-none-eabi",
"--buildtype=minsize",
"-Dtests=true",
"-Dtinystdio=false",
"-Dsingle-thread=true",
"-Db_pie=true",
"-Ddefault_library=static",
"-Dtinystdio=false",
"-Dmultilib=false",
".",
])
.status()
.expect("Failed to run Meson configure");
assert!(status.success());
let status = Command::new("ninja")
.current_dir(&build_dir)
.status()
.expect("Failed to run Ninja build");
assert!(status.success());
println!("cargo:rustc-link-search={}/newlib", build_dir);
println!("cargo:rustc-link-lib=c");
println!("cargo:rustc-link-lib=m");
}
fn build_doom() {
let ref dg_src_dir = std::path::PathBuf::from("doomgeneric/doomgeneric");
let mut dg_c_paths = vec![];
let mut dg_h_paths = vec![];
// Find most c and h files
for entry in std::fs::read_dir(dg_src_dir).unwrap() {
let entry = entry.unwrap();
if let Some(filename) = entry.file_name().to_str() {
if filename.starts_with("doomgeneric")
|| filename.contains("_allegro")
|| filename.contains("_sdl")
|| filename.contains("net_")
|| filename == "i_main.c"
{
continue;
}
if filename.ends_with(".h") {
dg_h_paths.push(dg_src_dir.join(filename));
} else if filename.ends_with(".c") {
dg_c_paths.push(dg_src_dir.join(filename));
}
}
}
dg_c_paths
.iter()
.chain(dg_h_paths.iter())
.for_each(|path| println!("cargo:rerun-if-changed={}", path.to_str().unwrap()));
cc::Build::new()
.compiler("arm-none-eabi-gcc")
.flag("-w") // silence warnings
.flag("-Os") // optimize for size
.flag("-ffunction-sections")
.flag("-fdata-sections")
.define("CMAP256", None)
.define("DOOMGENERIC_RESX", Some("320"))
.define("DOOMGENERIC_RESY", Some("200"))
.flag_if_supported("-std=gnu99")
.define("_POSIX_C_SOURCE", Some("200809L"))
.files(dg_c_paths)
.compile("doomgeneric");
}

31
user-apps/doom/memory.x Normal file
View File

@@ -0,0 +1,31 @@
MEMORY
{
RAM : ORIGIN = 0x0, LENGTH = 400K
}
SECTIONS
{
.text : ALIGN(4)
{
*(.text .text.*);
*(.rodata .rodata.*);
} > RAM
.data : ALIGN(4)
{
*(.data .data.*);
} > RAM
.bss : ALIGN(4)
{
*(.bss .bss.*);
*(COMMON);
} > RAM
.syscall_table (NOLOAD) : ALIGN(4)
{
__user_reloc_start = .;
KEEP(*(.user_reloc*));
__user_reloc_end = .;
} > RAM
}

112
user-apps/doom/src/doom.rs Normal file
View File

@@ -0,0 +1,112 @@
use abi::{
abi_sys::{get_ms, sleep},
display::Display,
};
use core::ffi::{c_int, c_uchar};
use embedded_graphics::{
Drawable, Pixel,
draw_target::DrawTarget,
pixelcolor::Rgb565,
prelude::{Point, RgbColor},
};
#[unsafe(no_mangle)]
#[allow(non_snake_case)]
static mut DG_ScreenBuffer: *const u8 = core::ptr::null();
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct RGBA {
pub b: u8,
pub g: u8,
pub r: u8,
pub a: u8,
}
unsafe extern "C" {
fn D_DoomMain();
fn doomgeneric_Tick();
fn M_FindResponseFile();
pub static colors: [RGBA; 256];
}
pub fn tick() {
unsafe { doomgeneric_Tick() };
}
pub struct ScreenBuffer<const RESX: usize, const RESY: usize, const SIZE: usize>(pub [u8; SIZE]);
impl<const RESX: usize, const RESY: usize, const SIZE: usize> ScreenBuffer<RESX, RESY, SIZE> {
#[allow(dead_code)]
const NA: () = assert!(SIZE == RESX * RESY);
pub const fn new() -> Self {
Self([0_u8; SIZE])
}
}
#[unsafe(no_mangle)]
extern "C" fn DG_SetWindowTitle() {}
pub fn create<const RESX: usize, const RESY: usize, const SIZE: usize>(
screenbuffer: &ScreenBuffer<RESX, RESY, SIZE>,
) {
unsafe {
M_FindResponseFile();
DG_ScreenBuffer = screenbuffer.0.as_ptr();
D_DoomMain();
}
}
const X: usize = 320;
const Y: usize = 200;
const SIZE: usize = X * Y;
pub static mut DISPLAY: Option<Display> = None;
pub static mut SCREEN_BUFFER: ScreenBuffer<X, Y, SIZE> = ScreenBuffer::new();
#[unsafe(no_mangle)]
extern "C" fn DG_DrawFrame() {
let palette565: [Rgb565; 256] = unsafe {
colors.map(|c| {
Rgb565::new(
((c.r as u16 * 31) / 255) as u8, // red 5 bits
((c.g as u16 * 63) / 255) as u8, // green 6 bits
((c.b as u16 * 31) / 255) as u8, // blue 5 bits
)
})
};
let buf = unsafe { &SCREEN_BUFFER.0 };
let display = unsafe { DISPLAY.as_mut().unwrap() };
display.clear(Rgb565::BLACK).unwrap();
for y in 0..Y {
for x in 0..X {
let idx = y * X + x;
let color = palette565[buf[idx] as usize];
Pixel(Point::new(x as i32, y as i32), color)
.draw(display)
.unwrap();
}
}
}
#[unsafe(no_mangle)]
extern "C" fn DG_GetTicksMs() -> u32 {
get_ms() as u32
}
#[unsafe(no_mangle)]
extern "C" fn DG_GetKey(pressed: *mut c_int, key: *mut c_uchar) -> c_int {
0
}
#[unsafe(no_mangle)]
extern "C" fn DG_SleepMs(ms: u32) {
sleep(ms.into());
}

118
user-apps/doom/src/libc.rs Normal file
View File

@@ -0,0 +1,118 @@
use core::ffi::{c_char, c_int, c_uchar, c_void};
use core::ptr;
#[unsafe(no_mangle)]
pub static _ctype_: [c_uchar; 256] = [0u8; 256];
#[repr(C)]
pub struct Reent {
pub errno: c_int,
_reserved: [u8; 32],
}
#[unsafe(no_mangle)]
pub static mut _reent_data: Reent = Reent {
errno: 0,
_reserved: [0; 32],
};
#[unsafe(no_mangle)]
pub static mut _impure_ptr: *mut Reent = unsafe { &mut _reent_data as *mut Reent };
#[unsafe(no_mangle)]
pub extern "C" fn __errno() -> *mut c_int {
unsafe { &mut (*_impure_ptr).errno as *mut c_int }
}
#[unsafe(no_mangle)]
pub extern "C" fn exit(_status: i32) -> ! {
loop {}
}
#[unsafe(no_mangle)]
pub extern "C" fn close(_file: i32) -> i32 {
-1
}
#[repr(C)]
pub struct Stat {
st_mode: u32,
}
#[unsafe(no_mangle)]
pub extern "C" fn fstat(_file: i32, st: *mut Stat) -> i32 {
unsafe {
if !st.is_null() {
(*st).st_mode = 0x2000; // S_IFCHR
}
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn isatty(_file: i32) -> i32 {
1
}
#[unsafe(no_mangle)]
pub extern "C" fn lseek(_file: i32, _ptr: i32, _dir: i32) -> i32 {
0
}
#[unsafe(no_mangle)]
pub extern "C" fn lseek64(_fd: c_int, _offset: i64, _whence: c_int) -> i64 {
0
}
#[unsafe(no_mangle)]
pub extern "C" fn open(_name: *const u8, _flags: i32, _mode: i32) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn read(_file: i32, _ptr: *mut u8, len: usize) -> i32 {
len as i32
}
#[unsafe(no_mangle)]
pub extern "C" fn write(_file: i32, _ptr: *const u8, len: usize) -> i32 {
len as i32
}
#[unsafe(no_mangle)]
pub extern "C" fn rename(_old: *const c_char, _new: *const c_char) -> c_int {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn kill(_pid: i32, _sig: i32) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn getpid() -> i32 {
1
}
#[unsafe(no_mangle)]
pub extern "C" fn link(_old: *const u8, _new: *const u8) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn unlink(_name: *const u8) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn mkdir(_path: *const u8, _mode: u32) -> i32 {
0
}
#[unsafe(no_mangle)]
pub extern "C" fn sbrk(incr: isize) -> *mut c_void {
core::ptr::null_mut()
}
#[unsafe(no_mangle)]
pub extern "C" fn fini() {}

View File

@@ -0,0 +1,43 @@
#![no_std]
#![no_main]
#![allow(static_mut_refs)]
#![feature(c_variadic)]
extern crate alloc;
use abi::{
display::Display,
fs::{file_len, read_file},
get_key, get_ms,
keyboard::{KeyCode, KeyState},
print, sleep,
};
use alloc::vec;
use core::panic::PanicInfo;
use embedded_graphics::{pixelcolor::Rgb565, prelude::Point};
mod doom;
use crate::doom::{DISPLAY, SCREEN_BUFFER, create, tick};
mod libc;
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
print!("user panic: {} @ {:?}", info.message(), info.location(),);
loop {}
}
#[unsafe(no_mangle)]
pub extern "Rust" fn _start() {
main()
}
pub fn main() {
print!("Starting Doom app");
let display = Display;
unsafe { DISPLAY = Some(display) };
unsafe { create(&SCREEN_BUFFER) };
loop {
tick();
}
}

View File

@@ -4,6 +4,6 @@ version = "0.1.0"
edition = "2024"
[dependencies]
userlib = { path = "../../userlib" }
abi = { path = "../../abi" }
embedded-graphics = "0.8.1"
tinybmp = "0.6.0"

View File

@@ -0,0 +1,102 @@
#![no_std]
#![no_main]
#![allow(static_mut_refs)]
extern crate alloc;
use abi::{
display::{Display, SCREEN_HEIGHT, SCREEN_WIDTH},
fs::{list_dir, read_file},
get_key,
keyboard::{KeyCode, KeyState},
print,
};
use alloc::{format, string::ToString, vec};
use core::panic::PanicInfo;
use embedded_graphics::{
Drawable, image::Image, mono_font::MonoTextStyle, mono_font::ascii::FONT_6X10,
pixelcolor::Rgb565, prelude::*, text::Text,
};
use tinybmp::Bmp;
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
print!("user panic: {} @ {:?}", info.message(), info.location());
loop {}
}
#[unsafe(no_mangle)]
pub extern "Rust" fn _start() {
main()
}
pub fn main() {
print!("Starting Gallery app");
let mut bmp_buf = vec![0_u8; 100_000];
let mut display = Display;
// Grid parameters
let grid_cols = 3;
let grid_rows = 3;
let cell_width = SCREEN_WIDTH as i32 / grid_cols;
let cell_height = SCREEN_HEIGHT as i32 / grid_rows;
let mut images_drawn = 0;
let mut files = [const { None }; 18];
let files_num = list_dir("/images", &mut files);
for file in &files[2..files_num] {
if images_drawn >= grid_cols * grid_rows {
break; // only draw 3x3
}
if let Some(f) = file {
print!("file: {}", f.name);
if f.name.extension() == b"bmp" || f.name.extension() == b"BMP" {
let file = format!("/images/{}", f.name);
let read = read_file(&file, 0, &mut &mut bmp_buf[..]);
if read > 0 {
let bmp = Bmp::from_slice(&bmp_buf).expect("failed to parse bmp");
let row = images_drawn / grid_cols;
let col = images_drawn % grid_cols;
let cell_x = col * cell_width;
let cell_y = row * cell_height;
// Center image inside cell
let bmp_w = bmp.size().width as i32;
let bmp_h = bmp.size().height as i32;
let x = cell_x + (cell_width - bmp_w) / 2;
let y = cell_y + 5; // 5px top margin
Image::new(&bmp, Point::new(x, y))
.draw(&mut display)
.unwrap();
let text_style = MonoTextStyle::new(&FONT_6X10, Rgb565::WHITE);
let text_y = y + bmp_h + 2; // 2px gap under image
Text::new(
f.name.to_string().as_str(),
Point::new(cell_x + 2, text_y),
text_style,
)
.draw(&mut display)
.unwrap();
images_drawn += 1;
}
}
}
}
loop {
let event = get_key();
if event.state != KeyState::Idle {
match event.key {
KeyCode::Esc => return,
_ => (),
}
};
}
}

View File

@@ -4,7 +4,6 @@ version = "0.1.0"
edition = "2024"
[dependencies]
userlib = { path = "../../userlib" }
selection_ui = { path = "../../selection_ui" }
abi = { path = "../../abi" }
embedded-graphics = "0.8.1"
tinygif = { git = "https://github.com/LegitCamper/tinygif" }

66
user-apps/gif/src/main.rs Normal file
View File

@@ -0,0 +1,66 @@
#![no_std]
#![no_main]
extern crate alloc;
use abi::{
display::Display,
fs::{file_len, read_file},
get_key, get_ms,
keyboard::{KeyCode, KeyState},
print, sleep,
};
use alloc::vec;
use core::panic::PanicInfo;
use embedded_graphics::{
image::ImageDrawable, pixelcolor::Rgb565, prelude::Point, transform::Transform,
};
use tinygif::Gif;
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
print!("user panic: {} @ {:?}", info.message(), info.location(),);
loop {}
}
#[unsafe(no_mangle)]
pub extern "Rust" fn _start() {
main()
}
pub fn main() {
print!("Starting Gif app");
let mut display = Display;
let size = file_len("/gifs/bad_apple.gif");
let mut buf = vec![0_u8; size];
let read = read_file("/gifs/bad_apple.gif", 0, &mut buf);
print!("read: {}, file size: {}", read, size);
assert!(read == size);
let gif = Gif::<Rgb565>::from_slice(&buf).unwrap();
let height = gif.height();
let mut frame_num = 0;
loop {
for mut frame in gif.frames() {
let start = get_ms();
frame
.translate_mut(Point::new(0, (320 - height as i32) / 2))
.draw(&mut display)
.unwrap();
frame_num += 1;
if frame_num % 100 == 0 {
let event = get_key();
if event.state != KeyState::Idle {
match event.key {
KeyCode::Esc => return,
_ => (),
};
};
}
sleep(((frame.delay_centis as u64) * 10).saturating_sub(start));
}
}
}

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2024"
[dependencies]
userlib = { path = "../../userlib" }
abi = { path = "../../abi" }
embedded-graphics = "0.8.1"
embedded-snake = { git = "https://github.com/LegitCamper/embedded-snake-rs" }
rand = { version = "0.9.0", default-features = false }

View File

@@ -2,20 +2,20 @@
#![no_main]
extern crate alloc;
use core::panic::PanicInfo;
use embedded_graphics::{pixelcolor::Rgb565, prelude::RgbColor};
use embedded_snake::{Direction, SnakeGame};
use userlib::{
use abi::{
Rng,
display::{Display, SCREEN_HEIGHT, SCREEN_WIDTH},
get_key,
keyboard::{KeyCode, KeyState},
println, sleep,
print, sleep,
};
use core::panic::PanicInfo;
use embedded_graphics::{pixelcolor::Rgb565, prelude::RgbColor};
use embedded_snake::{Direction, SnakeGame};
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("user panic: {} @ {:?}", info.message(), info.location(),);
print!("user panic: {} @ {:?}", info.message(), info.location(),);
loop {}
}
@@ -27,8 +27,8 @@ pub extern "Rust" fn _start() {
const CELL_SIZE: usize = 8;
pub fn main() {
println!("Starting Snake app");
let mut display = Display::take().unwrap();
print!("Starting Snake app");
let mut display = Display;
let mut game = SnakeGame::<100, Rgb565, Rng>::new(
SCREEN_WIDTH as u16,

View File

@@ -1,92 +0,0 @@
#![no_std]
#![no_main]
#![allow(static_mut_refs)]
extern crate alloc;
use alloc::{format, vec};
use core::panic::PanicInfo;
use embedded_graphics::{
Drawable, image::Image, mono_font::MonoTextStyle, mono_font::ascii::FONT_6X10,
pixelcolor::Rgb565, prelude::*, text::Text,
};
use tinybmp::Bmp;
use userlib::{
display::{Display, SCREEN_HEIGHT, SCREEN_WIDTH},
fs::{Entries, list_dir, read_file},
get_key,
keyboard::{KeyCode, KeyState},
println,
};
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("user panic: {} @ {:?}", info.message(), info.location());
loop {}
}
#[unsafe(no_mangle)]
pub extern "Rust" fn _start() {
main()
}
pub fn main() {
println!("Starting Gallery app");
let mut bmp_buf = vec![0_u8; 100_000];
let mut display = Display::take().unwrap();
let grid_cols = 3;
let grid_rows = 3;
let cell_width = SCREEN_WIDTH as i32 / grid_cols;
let cell_height = SCREEN_HEIGHT as i32 / grid_rows;
let mut images_drawn = 0;
let mut entries = Entries::new();
let files_num = list_dir("/images", &mut entries);
for file in &entries.entries()[2..files_num] {
if images_drawn >= grid_cols * grid_rows {
break; // only draw 3x3
}
println!("file: {}", file);
if file.extension().unwrap_or("") == "bmp" || file.extension().unwrap_or("") == "BMP" {
let file_path = format!("/images/{file}");
let read = read_file(&file_path, 0, &mut bmp_buf[..]);
if read > 0 {
let bmp = Bmp::from_slice(&bmp_buf).expect("failed to parse bmp");
let row = images_drawn / grid_cols;
let col = images_drawn % grid_cols;
let cell_x = col * cell_width;
let cell_y = row * cell_height;
// Center image inside cell
let bmp_w = bmp.size().width as i32;
let bmp_h = bmp.size().height as i32;
let x = cell_x + (cell_width - bmp_w) / 2;
let y = cell_y + 5; // 5px top margin
Image::new(&bmp, Point::new(x, y))
.draw(&mut display)
.unwrap();
let text_style = MonoTextStyle::new(&FONT_6X10, Rgb565::WHITE);
let text_y = y + bmp_h + 2; // 2px gap under image
Text::new(file.base(), Point::new(cell_x + 2, text_y), text_style)
.draw(&mut display)
.unwrap();
images_drawn += 1;
}
}
}
loop {
let event = get_key();
if event.state != KeyState::Idle && event.key == KeyCode::Esc {
return;
}
}
}

View File

@@ -1,98 +0,0 @@
#![no_std]
#![no_main]
extern crate alloc;
use alloc::{format, vec, vec::Vec};
use core::panic::PanicInfo;
use embedded_graphics::{
image::ImageDrawable,
mono_font::{MonoTextStyle, ascii::FONT_6X10},
pixelcolor::Rgb565,
prelude::{Point, RgbColor},
transform::Transform,
};
use selection_ui::{SelectionUi, SelectionUiError, draw_text_center};
use tinygif::Gif;
use userlib::{
display::{Display, SCREEN_HEIGHT, SCREEN_WIDTH},
fs::{Entries, file_len, list_dir, read_file},
get_key, get_ms,
keyboard::{KeyCode, KeyState},
println, sleep,
};
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("user panic: {} @ {:?}", info.message(), info.location(),);
loop {}
}
#[unsafe(no_mangle)]
pub extern "Rust" fn _start() {
main()
}
pub fn main() {
println!("Starting Gif app");
let mut display = Display::take().unwrap();
let mut entries = Entries::new();
list_dir("/gifs", &mut entries);
let mut files = entries.entries();
files.retain(|e| e.extension().unwrap_or("") == "gif");
let mut gifs = files.iter().map(|e| e.full_name()).collect::<Vec<&str>>();
gifs.sort();
let mut selection_ui = SelectionUi::new(&gifs);
let selection = match selection_ui.run_selection_ui(&mut display) {
Ok(maybe_sel) => maybe_sel,
Err(e) => match e {
SelectionUiError::SelectionListEmpty => {
draw_text_center(
&mut display,
"No Gifs were found in /gifs",
MonoTextStyle::new(&FONT_6X10, Rgb565::RED),
)
.expect("Display Error");
None
}
SelectionUiError::DisplayError(_) => panic!("Display Error"),
},
};
assert!(selection.is_some());
let file_name = format!("/gifs/{}", gifs[selection.unwrap()]);
let size = file_len(&file_name);
let mut buf = vec![0_u8; size];
let read = read_file(&file_name, 0, &mut buf);
println!("read: {}, file size: {}", read, size);
assert!(read == size);
let gif = Gif::<Rgb565>::from_slice(&buf).expect("Failed to parse gif");
let translation = Point::new(
(SCREEN_WIDTH as i32 - gif.width() as i32) / 2,
(SCREEN_HEIGHT as i32 - gif.height() as i32) / 2,
);
let mut frame_num = 0;
loop {
for mut frame in gif.frames() {
let start = get_ms();
frame.translate_mut(translation).draw(&mut display).unwrap();
frame_num += 1;
if frame_num % 5 == 0 {
let event = get_key();
if event.state != KeyState::Idle && event.key == KeyCode::Esc {
drop(buf);
return;
};
}
sleep(((frame.delay_centis as u64) * 10).saturating_sub(start));
}
}
}

View File

@@ -1,11 +0,0 @@
[package]
name = "wav_player"
version = "0.1.0"
edition = "2024"
[dependencies]
userlib = { path = "../../userlib" }
selection_ui = { path = "../../selection_ui" }
embedded-graphics = "0.8.1"
rand = { version = "0.9.0", default-features = false }
embedded-audio = { git = "https://github.com/LegitCamper/embedded-audio" }

View File

@@ -1,28 +0,0 @@
//! This build script copies the `memory.x` file from the crate root into
//! a directory where the linker can always find it at build time.
//! For many projects this is optional, as the linker always searches the
//! project root directory -- wherever `Cargo.toml` is. However, if you
//! are using a workspace or have a more complicated build setup, this
//! build script becomes required. Additionally, by requesting that
//! Cargo re-run the build script whenever `memory.x` is changed,
//! updating `memory.x` ensures a rebuild of the application with the
//! new memory settings.
use std::env;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
fn main() {
// Put `memory.x` 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("memory.x"))
.unwrap()
.write_all(include_bytes!("../memory.x"))
.unwrap();
println!("cargo:rustc-link-search={}", out.display());
println!("cargo:rerun-if-changed=memory.x");
println!("cargo:rustc-link-arg-bins=-Tmemory.x");
}

View File

@@ -1,143 +0,0 @@
#![no_std]
#![no_main]
extern crate alloc;
use alloc::{string::String, vec::Vec};
use core::panic::PanicInfo;
use embedded_audio::{AudioFile, PlatformFile, PlatformFileError, wav::Wav};
use embedded_graphics::{
mono_font::{MonoTextStyle, ascii::FONT_6X10},
pixelcolor::Rgb565,
prelude::RgbColor,
};
use selection_ui::{SelectionUi, SelectionUiError, draw_text_center};
use userlib::{
audio::{AUDIO_BUFFER_LEN, audio_buffer_ready, send_audio_buffer},
display::Display,
format,
fs::{Entries, file_len, list_dir, read_file},
get_key,
keyboard::{KeyCode, KeyState},
println,
};
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("user panic: {} @ {:?}", info.message(), info.location(),);
loop {}
}
#[unsafe(no_mangle)]
pub extern "Rust" fn _start() {
main()
}
pub fn main() {
println!("Starting Wav player app");
let mut display = Display::take().unwrap();
loop {
let mut entries = Entries::new();
list_dir("/music", &mut entries);
let mut files = entries.entries();
files.retain(|e| e.extension().unwrap_or("") == "wav");
let mut wavs = files.iter().map(|e| e.full_name()).collect::<Vec<&str>>();
wavs.sort();
let mut selection_ui = SelectionUi::new(&wavs);
let selection = match selection_ui.run_selection_ui(&mut display) {
Ok(maybe_sel) => maybe_sel,
Err(e) => match e {
SelectionUiError::SelectionListEmpty => {
draw_text_center(
&mut display,
"No Wavs were found in /music",
MonoTextStyle::new(&FONT_6X10, Rgb565::RED),
)
.expect("Display Error");
None
}
SelectionUiError::DisplayError(_) => panic!("Display Error"),
},
};
assert!(selection.is_some());
draw_text_center(
&mut display,
&format!("Now playing {}", wavs[selection.unwrap()]),
MonoTextStyle::new(&FONT_6X10, Rgb565::WHITE),
)
.expect("Display Error");
let file_name = format!("/music/{}", wavs[selection.unwrap()]);
let file = File::new(file_name);
let mut wav = Wav::new(file).unwrap();
println!("sample rate: {}", wav.sample_rate());
println!("channels: {:?}", wav.channels() as u8);
let mut buf = [0_u8; AUDIO_BUFFER_LEN];
loop {
if audio_buffer_ready() {
if wav.is_eof() {
break;
}
let _read = wav.read(&mut buf).unwrap();
send_audio_buffer(&buf);
}
let event = get_key();
if event.state == KeyState::Released && event.key == KeyCode::Esc {
return;
}
}
}
}
struct File {
current_pos: usize,
file: String,
}
impl File {
fn new(file: String) -> Self {
Self {
current_pos: 0,
file,
}
}
}
impl PlatformFile for File {
fn read(&mut self, buf: &mut [u8]) -> Result<usize, PlatformFileError> {
let read = read_file(&self.file, self.current_pos, buf);
self.current_pos += read;
Ok(read)
}
fn seek_from_current(&mut self, offset: i64) -> Result<(), PlatformFileError> {
if offset.is_positive() {
self.current_pos += offset as usize;
} else {
self.current_pos -= offset as usize;
}
Ok(())
}
fn seek_from_start(&mut self, offset: usize) -> Result<(), PlatformFileError> {
self.current_pos = offset;
Ok(())
}
fn seek_from_end(&mut self, offset: usize) -> Result<(), PlatformFileError> {
self.current_pos = self.length() - offset;
Ok(())
}
fn length(&mut self) -> usize {
file_len(&self.file)
}
}

View File

@@ -1,283 +0,0 @@
#![no_std]
#![allow(static_mut_refs)]
extern crate alloc;
pub use alloc::format;
use core::alloc::{GlobalAlloc, Layout};
use rand_core::RngCore;
use userlib_sys::{RngRequest, keyboard::KeyEvent};
pub use userlib_sys::{keyboard, print};
#[global_allocator]
static ALLOC: Alloc = Alloc;
struct Alloc;
unsafe impl GlobalAlloc for Alloc {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
userlib_sys::alloc(layout.into())
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
userlib_sys::dealloc(ptr, layout.into());
}
}
#[macro_export]
macro_rules! println {
($($arg:tt)*) => {{
let s = $crate::format!($($arg)*);
$crate::print(s.as_ptr(), s.len());
}};
}
pub fn sleep(ms: u64) {
userlib_sys::sleep(ms);
}
pub fn get_ms() -> u64 {
userlib_sys::get_ms()
}
pub fn get_key() -> KeyEvent {
userlib_sys::keyboard::get_key().into()
}
pub mod display {
use core::sync::atomic::{AtomicBool, Ordering};
use embedded_graphics::{
Pixel,
geometry::{Dimensions, Point},
pixelcolor::Rgb565,
prelude::{DrawTarget, Size},
primitives::Rectangle,
};
use userlib_sys::CPixel;
pub const SCREEN_WIDTH: usize = 320;
pub const SCREEN_HEIGHT: usize = 320;
pub type Pixel565 = Pixel<Rgb565>;
const BUF_SIZE: usize = 1024;
static mut BUF: [CPixel; BUF_SIZE] = [CPixel::new(); BUF_SIZE];
static DISPLAY_TAKEN: AtomicBool = AtomicBool::new(false);
pub struct Display {
_private: (),
}
impl Display {
/// Only one instance of Display can be taken
pub fn take() -> Option<Display> {
if DISPLAY_TAKEN
.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
.is_ok()
{
Some(Self { _private: () })
} else {
None
}
}
}
impl Dimensions for Display {
fn bounding_box(&self) -> Rectangle {
Rectangle {
top_left: Point { x: 0, y: 0 },
size: Size {
width: SCREEN_WIDTH as u32,
height: SCREEN_HEIGHT as u32,
},
}
}
}
impl DrawTarget for Display {
type Color = Rgb565;
type Error = ();
fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
where
I: IntoIterator<Item = Pixel<Self::Color>>,
{
let mut count = 0;
for p in pixels {
unsafe { BUF[count] = p.into() };
count += 1;
if count == BUF_SIZE - 1 {
userlib_sys::draw_iter(unsafe { BUF.as_ptr() }, count);
count = 0;
}
}
if count > 0 {
userlib_sys::draw_iter(unsafe { BUF.as_ptr() }, count);
}
Ok(())
}
}
}
fn gen_rand(req: &mut RngRequest) {
userlib_sys::gen_rand(req);
}
pub struct Rng;
impl RngCore for Rng {
fn next_u32(&mut self) -> u32 {
let mut req = RngRequest::U32(0);
gen_rand(&mut req);
if let RngRequest::U32(i) = req {
return i;
};
0
}
fn next_u64(&mut self) -> u64 {
let mut req = RngRequest::U64(0);
gen_rand(&mut req);
if let RngRequest::U64(i) = req {
return i;
};
0
}
fn fill_bytes(&mut self, dst: &mut [u8]) {
let mut req = RngRequest::Bytes {
ptr: dst.as_mut_ptr(),
len: dst.len(),
};
gen_rand(&mut req);
}
}
pub mod fs {
use alloc::vec::Vec;
use core::fmt::Display;
pub fn read_file(file: &str, start_from: usize, buf: &mut [u8]) -> usize {
userlib_sys::read_file(
file.as_ptr(),
file.len(),
start_from,
buf.as_mut_ptr(),
buf.len(),
)
}
pub fn write_file(file: &str, start_from: usize, buf: &[u8]) {
userlib_sys::write_file(
file.as_ptr(),
file.len(),
start_from,
buf.as_ptr(),
buf.len(),
)
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct FileName<'a> {
full: &'a str,
base: &'a str,
ext: Option<&'a str>,
}
impl<'a> FileName<'a> {
pub fn full_name(&self) -> &str {
self.full
}
pub fn base(&self) -> &str {
self.base
}
pub fn extension(&self) -> Option<&str> {
self.ext
}
}
impl<'a> Display for FileName<'a> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}", self.full_name())
}
}
impl<'a> From<&'a str> for FileName<'a> {
fn from(s: &'a str) -> FileName<'a> {
let full = s;
// Split on last dot for extension
let (base, ext) = match s.rfind('.') {
Some(idx) => (&s[..idx], Some(&s[idx + 1..])),
None => (s, None),
};
FileName { full, base, ext }
}
}
const MAX_ENTRY_NAME_LEN: usize = 25;
const MAX_ENTRIES: usize = 25;
#[derive(Clone, Copy, Debug)]
pub struct Entries([[u8; MAX_ENTRY_NAME_LEN]; MAX_ENTRIES]);
impl Default for Entries {
fn default() -> Self {
Self::new()
}
}
impl Entries {
pub fn new() -> Self {
Self([[0; MAX_ENTRY_NAME_LEN]; MAX_ENTRIES])
}
/// Get list of file names after listing
pub fn entries<'a>(&'a self) -> Vec<FileName<'a>> {
self.0
.iter()
.filter_map(|buf| {
let nul_pos = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
Some(core::str::from_utf8(&buf[..nul_pos]).ok()?.into())
})
.collect()
}
fn as_ptrs(&mut self) -> [*mut u8; MAX_ENTRIES] {
let mut ptrs: [*mut u8; MAX_ENTRIES] = [core::ptr::null_mut(); MAX_ENTRIES];
for (i, buf) in self.0.iter_mut().enumerate() {
ptrs[i] = buf.as_mut_ptr();
}
ptrs
}
}
pub fn list_dir(path: &str, entries: &mut Entries) -> usize {
userlib_sys::list_dir(
path.as_ptr(),
path.len(),
entries.as_ptrs().as_mut_ptr(),
MAX_ENTRIES,
MAX_ENTRY_NAME_LEN,
)
}
pub fn file_len(str: &str) -> usize {
userlib_sys::file_len(str.as_ptr(), str.len())
}
}
pub mod audio {
pub use userlib_sys::{AUDIO_BUFFER_LEN, AUDIO_BUFFER_SAMPLES, audio_buffer_ready};
pub fn send_audio_buffer(buf: &[u8]) {
userlib_sys::send_audio_buffer(buf.as_ptr(), buf.len())
}
}