diff --git a/.gitignore b/.gitignore index ea8c4bf..f73c1bf 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /target +*.uf2 +abi_sys.h diff --git a/.helix/languages.toml b/.helix/languages.toml new file mode 100644 index 0000000..9d6eb0f --- /dev/null +++ b/.helix/languages.toml @@ -0,0 +1,5 @@ +[[language]] +name = "rust" + +[language-server.rust-analyzer.config.check] +allTargets = false diff --git a/Cargo.lock b/Cargo.lock index 0473f8c..5469205 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12,6 +12,42 @@ dependencies = [ "regex", ] +[[package]] +name = "abi" +version = "0.1.0" +dependencies = [ + "abi_sys", + "embedded-graphics", + "embedded-sdmmc", + "rand_core 0.9.3", + "spin", + "talc", +] + +[[package]] +name = "abi_sys" +version = "0.1.0" +dependencies = [ + "bitflags 2.9.4", + "cbindgen", + "defmt 0.3.100", + "embedded-graphics", + "embedded-sdmmc", + "strum", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -45,6 +81,12 @@ dependencies = [ "term 1.0.2", ] +[[package]] +name = "assign-resources" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "840ad5d907de7448d88a3f22b4a7b5d326c6eb3deeb9f94cfaaec7354a80b305" + [[package]] name = "atomic-polyfill" version = "1.0.3" @@ -54,6 +96,17 @@ dependencies = [ "critical-section", ] +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -75,27 +128,6 @@ dependencies = [ "rustc_version", ] -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - -[[package]] -name = "bisync" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5020822f6d6f23196ccaf55e228db36f9de1cf788052b37992e17cbc96ec41a7" -dependencies = [ - "bisync_macros", -] - -[[package]] -name = "bisync_macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d21f40d350a700f6aa107e45fb26448cf489d34794b2ba4522181dc9f1173af6" - [[package]] name = "bit-set" version = "0.5.3" @@ -126,12 +158,24 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + [[package]] name = "bitfield" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46afbd2983a5d5a7bd740ccb198caf5b82f45c40c09c0eed36052d91cb92e719" +[[package]] +name = "bitfield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d7e60934ceec538daadb9d8432424ed043a904d8e0243f3c6446bce549a46ac" + [[package]] name = "bitflags" version = "1.3.2" @@ -140,9 +184,21 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] [[package]] name = "block-buffer" @@ -160,7 +216,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f377753756ec12e76b52d2dd657437be0448cc9736402ffadd0b8b8b9602c8a1" dependencies = [ "embassy-sync 0.6.2", - "embassy-time", + "embassy-time 0.4.0", "embedded-io", "embedded-io-async", "futures-intrusive", @@ -186,12 +242,64 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "calculator" +version = "0.1.0" +dependencies = [ + "abi", + "embedded-graphics", + "embedded-layout", +] + +[[package]] +name = "cbindgen" +version = "0.24.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b922faaf31122819ec80c4047cc684c6979a087366c069611e33649bf98e18d" +dependencies = [ + "clap", + "heck 0.4.1", + "indexmap 1.9.3", + "log", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 1.0.109", + "tempfile", + "toml", +] + [[package]] name = "cfg-if" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +[[package]] +name = "clap" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +dependencies = [ + "atty", + "bitflags 1.3.2", + "clap_lex", + "indexmap 1.9.3", + "strsim 0.10.0", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -202,12 +310,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "const-default" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b396d1f76d455557e1218ec8066ae14bba60b4b36ecd55577ba979f5db7ecaa" - [[package]] name = "cortex-m" version = "0.7.7" @@ -215,7 +317,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ec610d8f49840a5b376c69663b6369e71f4b34484b9b2eb29fb918d92516cb9" dependencies = [ "bare-metal", - "bitfield", + "bitfield 0.13.2", "embedded-hal 0.2.7", "volatile-register", ] @@ -292,7 +394,7 @@ dependencies = [ "embassy-futures", "embassy-net-driver-channel", "embassy-sync 0.6.2", - "embassy-time", + "embassy-time 0.4.0", "embedded-hal 1.0.0", "embedded-io-async", "futures", @@ -333,7 +435,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim", + "strsim 0.11.1", "syn 2.0.104", ] @@ -459,13 +561,48 @@ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "embassy-embedded-hal" -version = "0.3.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fea5ef5bed4d3468dfd44f5c9fa4cda8f54c86d4fb4ae683eacf9d39e2ea12" +checksum = "8c62a3bf127e03832fb97d8b01a058775e617653bc89e2a12c256485a7fb54c1" dependencies = [ + "embassy-embedded-hal 0.4.0", "embassy-futures", "embassy-sync 0.6.2", - "embassy-time", + "embassy-time 0.4.0", + "embedded-hal 0.2.7", + "embedded-hal 1.0.0", + "embedded-hal-async", + "embedded-storage", + "embedded-storage-async", + "nb 1.1.0", +] + +[[package]] +name = "embassy-embedded-hal" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1611b7a7ab5d1fbed84c338df26d56fd9bded58006ebb029075112ed2c5e039" +dependencies = [ + "embassy-futures", + "embassy-hal-internal 0.3.0", + "embassy-sync 0.7.2", + "embedded-hal 0.2.7", + "embedded-hal 1.0.0", + "embedded-hal-async", + "embedded-storage", + "embedded-storage-async", + "nb 1.1.0", +] + +[[package]] +name = "embassy-embedded-hal" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "554e3e840696f54b4c9afcf28a0f24da431c927f4151040020416e7393d6d0d8" +dependencies = [ + "embassy-futures", + "embassy-hal-internal 0.3.0", + "embassy-sync 0.7.2", "embedded-hal 0.2.7", "embedded-hal 1.0.0", "embedded-hal-async", @@ -476,22 +613,23 @@ dependencies = [ [[package]] name = "embassy-executor" -version = "0.7.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90327bcc66333a507f89ecc4e2d911b265c45f5c9bc241f98eee076752d35ac6" +checksum = "06070468370195e0e86f241c8e5004356d696590a678d47d6676795b2e439c6b" dependencies = [ "cortex-m", "critical-section", - "defmt 0.3.100", + "defmt 1.0.1", "document-features", "embassy-executor-macros", + "embassy-executor-timer-queue", ] [[package]] name = "embassy-executor-macros" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3577b1e9446f61381179a330fc5324b01d511624c55f25e3c66c9e3c626dbecf" +checksum = "dfdddc3a04226828316bf31393b6903ee162238576b1584ee2669af215d55472" dependencies = [ "darling", "proc-macro2", @@ -500,10 +638,16 @@ dependencies = [ ] [[package]] -name = "embassy-futures" -version = "0.1.1" +name = "embassy-executor-timer-queue" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f878075b9794c1e4ac788c95b728f26aa6366d32eeb10c7051389f898f7d067" +checksum = "2fc328bf943af66b80b98755db9106bf7e7471b0cf47dc8559cd9a6be504cc9c" + +[[package]] +name = "embassy-futures" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc2d050bdc5c21e0862a89256ed8029ae6c290a93aecefc73084b3002cdebb01" [[package]] name = "embassy-hal-internal" @@ -513,7 +657,18 @@ checksum = "0ef3bac31ec146321248a169e9c7b5799f1e0b3829c7a9b324cb4600a7438f59" dependencies = [ "cortex-m", "critical-section", - "defmt 0.3.100", + "num-traits", +] + +[[package]] +name = "embassy-hal-internal" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95285007a91b619dc9f26ea8f55452aa6c60f7115a4edc05085cd2bd3127cd7a" +dependencies = [ + "cortex-m", + "critical-section", + "defmt 1.0.1", "num-traits", ] @@ -525,13 +680,13 @@ checksum = "524eb3c489760508f71360112bca70f6e53173e6fe48fc5f0efd0f5ab217751d" [[package]] name = "embassy-net-driver-channel" -version = "0.3.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4818c32afec43e3cae234f324bad9a976c9aa7501022d26ff60a4017a1a006b7" +checksum = "b7b2739fbcf6cd206ae08779c7d709087b16577d255f2ea4a45bc4bbbf305b3f" dependencies = [ "embassy-futures", "embassy-net-driver", - "embassy-sync 0.6.2", + "embassy-sync 0.7.2", ] [[package]] @@ -546,12 +701,12 @@ dependencies = [ "cortex-m-rt", "critical-section", "document-features", - "embassy-embedded-hal", + "embassy-embedded-hal 0.3.2", "embassy-futures", - "embassy-hal-internal", + "embassy-hal-internal 0.2.0", "embassy-sync 0.6.2", - "embassy-time", - "embassy-usb-driver", + "embassy-time 0.4.0", + "embassy-usb-driver 0.1.0", "embedded-hal 0.2.7", "embedded-hal 1.0.0", "embedded-hal-async", @@ -564,7 +719,7 @@ dependencies = [ "nb 1.1.0", "pio 0.2.1", "pio-proc 0.2.2", - "rand_core", + "rand_core 0.6.4", "rp-pac", "rp2040-boot2", "sha2-const-stable", @@ -573,25 +728,25 @@ dependencies = [ [[package]] name = "embassy-rp" -version = "0.4.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1a063d8baccdc5c7752840f4c7931f17bcd7de1ffe1efa2109e68113fe42612" +checksum = "1a284935af0a869de3fa14af74b5f932389dd66d7048012f1083b06f38d05399" dependencies = [ "atomic-polyfill", "cfg-if", "cortex-m", "cortex-m-rt", "critical-section", - "defmt 0.3.100", + "defmt 1.0.1", "document-features", - "embassy-embedded-hal", + "embassy-embedded-hal 0.5.0", "embassy-futures", - "embassy-hal-internal", - "embassy-sync 0.6.2", - "embassy-time", + "embassy-hal-internal 0.3.0", + "embassy-sync 0.7.2", + "embassy-time 0.5.0", "embassy-time-driver", "embassy-time-queue-utils", - "embassy-usb-driver", + "embassy-usb-driver 0.2.0", "embedded-hal 0.2.7", "embedded-hal 1.0.0", "embedded-hal-async", @@ -603,8 +758,8 @@ dependencies = [ "fixed", "nb 1.1.0", "pio 0.3.0", - "rand_core", - "rp-binary-info", + "rand_core 0.6.4", + "rand_core 0.9.3", "rp-pac", "rp2040-boot2", "sha2-const-stable", @@ -627,16 +782,16 @@ dependencies = [ [[package]] name = "embassy-sync" -version = "0.7.0" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cef1a8a1ea892f9b656de0295532ac5d8067e9830d49ec75076291fd6066b136" +checksum = "73974a3edbd0bd286759b3d483540f0ebef705919a5f56f4fc7709066f71689b" dependencies = [ "cfg-if", "critical-section", "defmt 1.0.1", "embedded-io-async", + "futures-core", "futures-sink", - "futures-util", "heapless", ] @@ -648,7 +803,6 @@ checksum = "f820157f198ada183ad62e0a66f554c610cdcd1a9f27d4b316358103ced7a1f8" dependencies = [ "cfg-if", "critical-section", - "defmt 0.3.100", "document-features", "embassy-time-driver", "embedded-hal 0.2.7", @@ -658,43 +812,72 @@ dependencies = [ ] [[package]] -name = "embassy-time-driver" -version = "0.2.0" +name = "embassy-time" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d45f5d833b6d98bd2aab0c2de70b18bfaa10faf661a1578fd8e5dfb15eb7eba" +checksum = "f4fa65b9284d974dad7a23bb72835c4ec85c0b540d86af7fc4098c88cff51d65" +dependencies = [ + "cfg-if", + "critical-section", + "defmt 1.0.1", + "document-features", + "embassy-time-driver", + "embassy-time-queue-utils", + "embedded-hal 0.2.7", + "embedded-hal 1.0.0", + "embedded-hal-async", + "futures-core", +] + +[[package]] +name = "embassy-time-driver" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0a244c7dc22c8d0289379c8d8830cae06bb93d8f990194d0de5efb3b5ae7ba6" dependencies = [ "document-features", ] [[package]] name = "embassy-time-queue-utils" -version = "0.1.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc55c748d16908a65b166d09ce976575fb8852cf60ccd06174092b41064d8f83" +checksum = "80e2ee86063bd028a420a5fb5898c18c87a8898026da1d4c852af2c443d0a454" dependencies = [ - "embassy-executor", + "embassy-executor-timer-queue", "heapless", ] +[[package]] +name = "embassy-usb" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc4462e48b19a4f401a11901bdd981aab80c6a826608016a0bdc73cbbab31954" +dependencies = [ + "embassy-futures", + "embassy-net-driver-channel", + "embassy-sync 0.7.2", + "embassy-usb-driver 0.2.0", + "embedded-io-async", + "heapless", + "ssmarshal", + "usbd-hid", +] + [[package]] name = "embassy-usb-driver" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fc247028eae04174b6635104a35b1ed336aabef4654f5e87a8f32327d231970" -dependencies = [ - "defmt 0.3.100", -] [[package]] -name = "embedded-alloc" -version = "0.6.0" +name = "embassy-usb-driver" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f2de9133f68db0d4627ad69db767726c99ff8585272716708227008d3f1bddd" +checksum = "17119855ccc2d1f7470a39756b12068454ae27a3eabb037d940b5c03d9c77b7a" dependencies = [ - "const-default", - "critical-section", - "linked_list_allocator", - "rlsf", + "defmt 1.0.1", + "embedded-io-async", ] [[package]] @@ -768,6 +951,18 @@ dependencies = [ "nb 1.1.0", ] +[[package]] +name = "embedded-iconoir" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c52b9899b636b56d4e66834f7a90766d0bc6600c0f067d91ed0711b11fa3f5c8" +dependencies = [ + "bit_field", + "embedded-graphics", + "paste", + "static_assertions", +] + [[package]] name = "embedded-io" version = "0.6.1" @@ -784,21 +979,47 @@ dependencies = [ ] [[package]] -name = "embedded-sdmmc" -version = "0.8.0" -source = "git+https://github.com/Be-ing/embedded-sdmmc-rs?branch=bisync#835b2e4f9d3482b6287f674d7ecf6ae5d0618c18" +name = "embedded-layout" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a90553247f2b05c59ac7894ea13d830636c2b1203fa03bff400eddbd1fa9f52" +dependencies = [ + "embedded-graphics", + "embedded-layout-macros", +] + +[[package]] +name = "embedded-layout-macros" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f6e621fe4c7e05b695274b722dc0a60bacd1c8696b58191baa0154713d52400" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "embedded-sdmmc" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce3c7f9ea039eeafc4a49597b7bd5ae3a1c8e51b2803a381cb0f29ce90fe1ec6" dependencies = [ - "bisync", "byteorder", "defmt 0.3.100", - "embassy-futures", "embedded-hal 1.0.0", - "embedded-hal-async", "embedded-io", - "embedded-io-async", "heapless", ] +[[package]] +name = "embedded-snake" +version = "0.0.3" +dependencies = [ + "embedded-graphics", + "rand_core 0.9.3", +] + [[package]] name = "embedded-storage" version = "0.3.1" @@ -814,6 +1035,17 @@ dependencies = [ "embedded-storage", ] +[[package]] +name = "embedded-text" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cf5c72c52db2f7dbe4a9c1ed81cd21301e8d66311b194fa41c04fb4f71843ba" +dependencies = [ + "az", + "embedded-graphics", + "object-chain", +] + [[package]] name = "ena" version = "0.14.3" @@ -823,12 +1055,34 @@ dependencies = [ "log", ] +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "fixed" version = "1.29.0" @@ -868,6 +1122,18 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.31" @@ -951,6 +1217,15 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "gallery" +version = "0.1.0" +dependencies = [ + "abi", + "embedded-graphics", + "tinybmp", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -972,6 +1247,28 @@ dependencies = [ "wasi", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "goblin" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be320f077239a0361c20d608b4768c62a1b77aa4946dd76395aa4cd502cba7d" +dependencies = [ + "plain", + "scroll", +] + [[package]] name = "half" version = "2.6.0" @@ -991,6 +1288,21 @@ dependencies = [ "byteorder", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash", +] + [[package]] name = "hashbrown" version = "0.15.4" @@ -1004,9 +1316,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" dependencies = [ "hash32", + "serde", "stable_deref_trait", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "hermit-abi" version = "0.5.2" @@ -1028,6 +1362,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + [[package]] name = "indexmap" version = "2.9.0" @@ -1035,7 +1379,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.4", ] [[package]] @@ -1044,7 +1388,7 @@ version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ - "hermit-abi", + "hermit-abi 0.5.2", "libc", "windows-sys", ] @@ -1067,6 +1411,12 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "js-sys" version = "0.3.77" @@ -1086,6 +1436,64 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "kernel" +version = "0.0.1" +dependencies = [ + "abi_sys", + "assign-resources", + "bitflags 2.9.4", + "bt-hci", + "bumpalo", + "cortex-m", + "cortex-m-rt", + "cyw43", + "cyw43-pio", + "defmt 0.3.100", + "defmt-rtt", + "embassy-embedded-hal 0.3.2", + "embassy-executor", + "embassy-futures", + "embassy-rp 0.8.0", + "embassy-sync 0.7.2", + "embassy-time 0.5.0", + "embassy-usb", + "embedded-graphics", + "embedded-hal 0.2.7", + "embedded-hal 1.0.0", + "embedded-hal-async", + "embedded-hal-bus", + "embedded-layout", + "embedded-sdmmc", + "embedded-text", + "goblin", + "heapless", + "kolibri-embedded-gui", + "num_enum 0.7.4", + "once_cell", + "panic-probe", + "portable-atomic", + "rand", + "spin", + "st7365p-lcd", + "static_cell", + "strum", + "talc", + "trouble-host", +] + +[[package]] +name = "kolibri-embedded-gui" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "011f8f415e8c2f03e4ad752afcf1bb156a18926250401b1fe29d8feda644c140" +dependencies = [ + "embedded-graphics", + "embedded-iconoir", + "foldhash", + "heapless", +] + [[package]] name = "lalrpop" version = "0.19.12" @@ -1167,15 +1575,15 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1580801010e535496706ba011c15f8532df6b42297d2e471fec38ceadd8c0638" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "libc", ] [[package]] -name = "linked_list_allocator" -version = "0.10.5" +name = "linux-raw-sys" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afa463f5405ee81cdb9cc2baf37e08ec7e4c8209442b5d72c04cfb2cd6e6286" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litrs" @@ -1282,12 +1690,24 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "object-chain" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41af26158b0f5530f7b79955006c2727cd23d0d8e7c3109dc316db0a919784dd" + [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "os_str_bytes" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" + [[package]] name = "panic-probe" version = "0.3.2" @@ -1334,7 +1754,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset 0.4.2", - "indexmap", + "indexmap 2.9.0", ] [[package]] @@ -1344,7 +1764,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset 0.5.7", - "indexmap", + "indexmap 2.9.0", ] [[package]] @@ -1362,39 +1782,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" -[[package]] -name = "picocalc-os-rs" -version = "0.1.0" -dependencies = [ - "bitflags 2.9.1", - "bt-hci", - "cortex-m", - "cortex-m-rt", - "critical-section", - "cyw43", - "cyw43-pio", - "defmt 0.3.100", - "defmt-rtt", - "embassy-embedded-hal", - "embassy-executor", - "embassy-futures", - "embassy-rp 0.4.0", - "embassy-sync 0.7.0", - "embassy-time", - "embedded-alloc", - "embedded-graphics", - "embedded-hal 0.2.7", - "embedded-hal-async", - "embedded-hal-bus", - "embedded-sdmmc", - "fixed", - "panic-probe", - "portable-atomic", - "st7365p-lcd", - "static_cell", - "trouble-host", -] - [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1495,6 +1882,12 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "portable-atomic" version = "1.11.1" @@ -1574,19 +1967,46 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_core 0.9.3", +] + [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" + [[package]] name = "redox_syscall" version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", ] [[package]] @@ -1595,7 +2015,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom", + "getrandom 0.2.16", "libredox", "thiserror 1.0.69", ] @@ -1644,24 +2064,6 @@ dependencies = [ "bytemuck", ] -[[package]] -name = "rlsf" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222fb240c3286247ecdee6fa5341e7cdad0ffdf8e7e401d9937f2d58482a20bf" -dependencies = [ - "cfg-if", - "const-default", - "libc", - "svgbobdoc", -] - -[[package]] -name = "rp-binary-info" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ed2051a0bf2c726df01cfce378ed8a367be2a6e402fc183857f429a346d429" - [[package]] name = "rp-pac" version = "7.0.0" @@ -1690,12 +2092,31 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags 2.9.4", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "rustversion" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + [[package]] name = "same-file" version = "1.0.6" @@ -1711,6 +2132,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scroll" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1257cd4248b4132760d6524d6dda4e053bc648c9070b960929bf50cfb1e7add" + [[package]] name = "semver" version = "0.9.0" @@ -1726,6 +2153,49 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + [[package]] name = "sha2-const-stable" version = "0.1.0" @@ -1773,12 +2243,44 @@ dependencies = [ ] [[package]] -name = "st7365p-lcd" -version = "0.10.0" -source = "git+https://github.com/legitcamper/st7365p-lcd-rs#d751e8d30f1a3f964ffe05e4bb16f82112fbefce" +name = "snake" +version = "0.1.0" dependencies = [ + "abi", + "embedded-graphics", + "embedded-snake", + "rand", +] + +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" +dependencies = [ + "lock_api", +] + +[[package]] +name = "ssmarshal" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3e6ad23b128192ed337dfa4f1b8099ced0c2bf30d61e551b65fda5916dbb850" +dependencies = [ + "encode_unicode", + "serde", +] + +[[package]] +name = "st7365p-lcd" +version = "0.11.0" +source = "git+https://github.com/legitcamper/st7365p-lcd-rs?rev=a784b9e6df0769371dfc522528e770cf8fc6403a#a784b9e6df0769371dfc522528e770cf8fc6403a" +dependencies = [ + "bitvec", "embedded-graphics-core", "embedded-hal 1.0.0", + "embedded-hal-async", + "heapless", "nb 1.1.0", ] @@ -1788,6 +2290,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "static_cell" version = "2.1.1" @@ -1809,6 +2317,12 @@ dependencies = [ "precomputed-hash", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "strsim" version = "0.11.1" @@ -1816,16 +2330,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] -name = "svgbobdoc" -version = "0.3.0" +name = "strum" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2c04b93fc15d79b39c63218f15e3fdffaa4c227830686e3b7c5f41244eb3e50" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ - "base64", + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck 0.5.0", "proc-macro2", "quote", - "syn 1.0.109", - "unicode-width", + "syn 2.0.104", ] [[package]] @@ -1850,6 +2372,34 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "talc" +version = "4.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3ae828aa394de34c7de08f522d1b86bd1c182c668d27da69caadda00590f26d" +dependencies = [ + "lock_api", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys", +] + [[package]] name = "term" version = "0.7.0" @@ -1880,6 +2430,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" + [[package]] name = "thiserror" version = "1.0.69" @@ -1929,6 +2485,24 @@ dependencies = [ "crunchy", ] +[[package]] +name = "tinybmp" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df43af2cb7b369009aa14144959bb4f2720ab62034c9073242f2d3a186c2edb6" +dependencies = [ + "embedded-graphics", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "trouble-host" version = "0.1.0" @@ -1938,11 +2512,11 @@ dependencies = [ "bt-hci", "embassy-futures", "embassy-sync 0.6.2", - "embassy-time", + "embassy-time 0.4.0", "embedded-io", "futures", "heapless", - "rand_core", + "rand_core 0.6.4", "static_cell", "trouble-host-macros", "zerocopy", @@ -1986,6 +2560,53 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "usb-device" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98816b1accafbb09085168b90f27e93d790b4bfa19d883466b5e53315b5f06a6" +dependencies = [ + "heapless", + "portable-atomic", +] + +[[package]] +name = "usbd-hid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6f291ab53d428685cc780f08a2eb9d5d6ff58622db2b36e239a4f715f1e184c" +dependencies = [ + "serde", + "ssmarshal", + "usb-device", + "usbd-hid-macros", +] + +[[package]] +name = "usbd-hid-descriptors" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee54712c5d778d2fb2da43b1ce5a7b5060886ef7b09891baeb4bf36910a3ed" +dependencies = [ + "bitfield 0.14.0", +] + +[[package]] +name = "usbd-hid-macros" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb573c76e7884035ac5e1ab4a81234c187a82b6100140af0ab45757650ccda38" +dependencies = [ + "byteorder", + "hashbrown 0.13.2", + "log", + "proc-macro2", + "quote", + "serde", + "syn 1.0.109", + "usbd-hid-descriptors", +] + [[package]] name = "uuid" version = "1.17.0" @@ -2039,6 +2660,15 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -2201,6 +2831,21 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "zerocopy" version = "0.8.26" diff --git a/Cargo.toml b/Cargo.toml index c6ffc9d..5f3e4be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,79 +1,25 @@ -[package] -name = "picocalc-os-rs" -version = "0.1.0" -edition = "2024" +[workspace] +resolver = "3" +members = [ + "kernel", + "abi", + "user-apps/calculator", + "user-apps/snake", + "user-apps/gallery", +] [profile.release] -debug = 2 +debug = true +opt-level = "z" +lto = true +codegen-units = 1 + +[profile.release-binary] +inherits = "release" +lto = true +debug = false +opt-level = "s" [profile.dev] lto = true opt-level = "z" - -[features] -default = ["rp235x", "defmt"] -rp2040 = ["embassy-rp/rp2040"] -rp235x = ["embassy-rp/rp235xb"] -trouble = ["dep:bt-hci", "dep:cyw43", "dep:cyw43-pio", "dep:trouble-host"] -defmt = [ - "dep:defmt", - "panic-probe/print-defmt", - "embassy-executor/defmt", - "embassy-time/defmt", - "embassy-time/defmt-timestamp-uptime", - "embassy-rp/defmt", - "embassy-sync/defmt", - "embedded-graphics/defmt", - "embedded-sdmmc/defmt-log", - # "bt-hci/defmt", - # "cyw43/defmt", - # "cyw43-pio/defmt", -] - -[dependencies] -embassy-executor = { version = "0.7", features = [ - "arch-cortex-m", - "executor-interrupt", - "executor-thread", - "nightly", -] } -embassy-rp = { version = "0.4.0", features = [ - "critical-section-impl", - "unstable-pac", - "time-driver", - "binary-info", -] } -embassy-futures = "0.1.1" -embassy-time = "0.4.0" -embassy-embedded-hal = "0.3.0" -embassy-sync = { version = "0.7" } -trouble-host = { version = "0.1", features = [ - "derive", - "scan", -], optional = true } -bt-hci = { version = "0.2", default-features = false, optional = true } -cyw43 = { version = "0.3.0", features = [ - "firmware-logs", - "bluetooth", -], optional = true } -cyw43-pio = { version = "0.3.0", optional = true } - -embedded-hal-bus = { version = "0.3.0", features = ["async"] } -embedded-hal = "0.2.7" -embedded-hal-async = "1.0.0" -cortex-m = { version = "0.7.7" } -cortex-m-rt = "0.7.5" -panic-probe = "0.3" -portable-atomic = { version = "1.11", features = ["critical-section"] } -static_cell = "2.1.1" -bitflags = "2.9.1" -embedded-alloc = "0.6.0" -fixed = "1.29.0" -critical-section = "1.2.0" - -defmt = { version = "0.3", optional = true } -defmt-rtt = "0.4.2" - -embedded-graphics = { version = "0.8.1" } -embedded-sdmmc = { git = "https://github.com/Be-ing/embedded-sdmmc-rs", branch = "bisync", default-features = false } -st7365p-lcd = { git = "https://github.com/legitcamper/st7365p-lcd-rs" } diff --git a/README.md b/README.md new file mode 100644 index 0000000..9bffec0 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# PicoCalc OS (Rust) + +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 **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 (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 safe communication between kernel and applications +- Support for multiple user-space applications +- Hardware drivers tailored for the PicoCalc + +## Getting Started + +```bash +git clone https://github.com/LegitCamper/picocalc-os-rs.git +cd picocalc-os-rs +just userapps +# copy the build applications from target/thumbv8m.main-none-eabihf/release-binary/application to the sdcard and rename them to app.bin +just kernel-release # keep in mind that https://github.com/StripedMonkey/elf2uf2-rs version is required until https://github.com/JoNil/elf2uf2-rs/pull/41 is merged diff --git a/abi/Cargo.toml b/abi/Cargo.toml new file mode 100644 index 0000000..935faa5 --- /dev/null +++ b/abi/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "abi" +version = "0.1.0" +edition = "2024" + +[dependencies] +embedded-sdmmc = { version = "0.9.0", default-features = false } +embedded-graphics = "0.8.1" +abi_sys = { path = "../abi_sys" } +talc = "4.4.3" +spin = "0.10.0" +rand_core = "0.9.3" diff --git a/abi/src/lib.rs b/abi/src/lib.rs new file mode 100644 index 0000000..0e775a5 --- /dev/null +++ b/abi/src/lib.rs @@ -0,0 +1,147 @@ +#![no_std] + +pub use abi_sys::keyboard; +use abi_sys::{RngRequest, keyboard::KeyEvent}; +use rand_core::RngCore; +use talc::*; + +extern crate alloc; + +static mut ARENA: [u8; 10000] = [0; 10000]; + +#[global_allocator] +static ALLOCATOR: Talck, ClaimOnOom> = + Talc::new(unsafe { ClaimOnOom::new(Span::from_array(core::ptr::addr_of!(ARENA).cast_mut())) }) + .lock(); + +pub fn print(msg: &str) { + abi_sys::print(msg.as_ptr(), msg.len()); +} + +pub fn sleep(ms: u64) { + abi_sys::sleep(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; + + pub fn lock_display(lock: bool) { + abi_sys::lock_display(lock); + } + + const BUF_SIZE: usize = 1024; // tune this for performance + + 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(&mut self, pixels: I) -> Result<(), Self::Error> + where + I: IntoIterator>, + { + let mut buf: [CPixel; BUF_SIZE] = [CPixel::new(); BUF_SIZE]; + + let mut count = 0; + for p in pixels { + buf[count] = p.into(); + count += 1; + + if count == BUF_SIZE { + abi_sys::draw_iter(buf.as_ptr(), count); + count = 0; + } + } + + if count > 0 { + abi_sys::draw_iter(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]) -> 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()) + } +} diff --git a/abi_sys/Cargo.toml b/abi_sys/Cargo.toml new file mode 100644 index 0000000..a64b5e5 --- /dev/null +++ b/abi_sys/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "abi_sys" +version = "0.1.0" +edition = "2024" + +[features] +default = [] +defmt = ["dep:defmt"] + +[dependencies] +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] +cbindgen = "0.24.0" diff --git a/abi_sys/src/lib.rs b/abi_sys/src/lib.rs new file mode 100644 index 0000000..b7ac704 --- /dev/null +++ b/abi_sys/src/lib.rs @@ -0,0 +1,412 @@ +#![no_std] + +use embedded_graphics::{ + Pixel, + pixelcolor::{Rgb565, raw::RawU16}, + prelude::{IntoStorage, Point}, +}; +use embedded_sdmmc::DirEntry; +use strum::EnumIter; + +pub const ABI_CALL_TABLE_COUNT: usize = 10; + +#[derive(Clone, Copy, EnumIter)] +#[repr(u8)] +pub enum CallAbiTable { + PrintString = 0, + SleepMs = 1, + GetMs = 2, + LockDisplay = 3, + DrawIter = 4, + GetKey = 5, + GenRand = 6, + ListDir = 7, + ReadFile = 8, + FileLen = 9, +} + +pub type EntryFn = fn(); + +#[unsafe(no_mangle)] +#[unsafe(link_section = ".syscall_table")] +pub static mut CALL_ABI_TABLE: [usize; ABI_CALL_TABLE_COUNT] = [0; ABI_CALL_TABLE_COUNT]; + +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: PrintAbi = + unsafe { core::mem::transmute(CALL_ABI_TABLE[CallAbiTable::PrintString as usize]) }; + f(ptr, len); +} + +pub type SleepMsAbi = extern "C" fn(ms: u64); + +#[unsafe(no_mangle)] +pub extern "C" fn sleep(ms: u64) { + let f: SleepMsAbi = + unsafe { core::mem::transmute(CALL_ABI_TABLE[CallAbiTable::SleepMs as usize]) }; + f(ms); +} + +pub type GetMsAbi = extern "C" fn() -> u64; + +#[unsafe(no_mangle)] +pub extern "C" fn get_ms() -> u64 { + let f: GetMsAbi = unsafe { core::mem::transmute(CALL_ABI_TABLE[CallAbiTable::GetMs as usize]) }; + f() +} + +pub type LockDisplay = extern "C" fn(lock: bool); + +#[unsafe(no_mangle)] +pub extern "C" fn lock_display(lock: bool) { + let f: LockDisplay = + unsafe { core::mem::transmute(CALL_ABI_TABLE[CallAbiTable::LockDisplay as usize]) }; + f(lock); +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub struct CPixel { + pub x: i32, + pub y: i32, + pub color: u16, +} + +impl CPixel { + pub fn new() -> Self { + Self { + x: 0, + y: 0, + color: 0, + } + } +} + +impl Into for Pixel { + fn into(self) -> CPixel { + CPixel { + x: self.0.x, + y: self.0.y, + color: self.1.into_storage(), + } + } +} + +impl Into> for CPixel { + fn into(self) -> Pixel { + 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: DrawIterAbi = + unsafe { core::mem::transmute(CALL_ABI_TABLE[CallAbiTable::DrawIter as usize]) }; + f(ptr, len); +} + +pub mod keyboard { + use crate::{CALL_ABI_TABLE, CallAbiTable}; + + bitflags::bitflags! { + #[derive(Default, Debug, PartialEq, Eq, Clone, Copy)] + #[repr(C)] + pub struct Modifiers: u8 { + const NONE = 0; + const CTRL = 1; + const ALT = 2; + const LSHIFT = 4; + const RSHIFT = 8; + const SYM = 16; + } + } + + #[repr(C)] + pub struct KeyEventC { + pub key: u8, + pub state: KeyState, + pub mods: Modifiers, + } + + impl Into for KeyEventC { + fn into(self) -> KeyEvent { + KeyEvent { + key: self.key.into(), + state: self.state, + mods: self.mods, + } + } + } + + #[derive(Debug)] + pub struct KeyEvent { + pub key: KeyCode, + pub state: KeyState, + pub mods: Modifiers, + } + + impl Into for KeyEvent { + fn into(self) -> KeyEventC { + KeyEventC { + key: self.key.into(), + state: self.state, + mods: self.mods, + } + } + } + + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + #[repr(C)] + pub enum KeyState { + Idle = 0, + Pressed = 1, + Hold = 2, + Released = 3, + } + + impl From for KeyState { + fn from(value: u8) -> Self { + match value { + 1 => KeyState::Pressed, + 2 => KeyState::Hold, + 3 => KeyState::Released, + 0 | _ => KeyState::Idle, + } + } + } + + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + #[repr(u8)] + pub enum KeyCode { + JoyUp = 0x01, + JoyDown = 0x02, + JoyLeft = 0x03, + JoyRight = 0x04, + JoyCenter = 0x05, + BtnLeft1 = 0x06, + BtnRight1 = 0x07, + BtnLeft2 = 0x11, + BtnRight2 = 0x12, + Backspace = 0x08, + Tab = 0x09, + Enter = 0x0A, + ModAlt = 0xA1, + ModShiftLeft = 0xA2, + ModShiftRight = 0xA3, + ModSym = 0xA4, + ModCtrl = 0xA5, + Esc = 0xB1, + Left = 0xB4, + Up = 0xB5, + Down = 0xB6, + Right = 0xB7, + Break = 0xD0, + Insert = 0xD1, + Home = 0xD2, + Del = 0xD4, + End = 0xD5, + PageUp = 0xD6, + PageDown = 0xD7, + CapsLock = 0xC1, + F1 = 0x81, + F2 = 0x82, + F3 = 0x83, + F4 = 0x84, + F5 = 0x85, + F6 = 0x86, + F7 = 0x87, + F8 = 0x88, + F9 = 0x89, + F10 = 0x90, + Char(char), + Unknown(u8), + } + + impl Into for KeyCode { + fn into(self) -> u8 { + match self { + KeyCode::JoyUp => 0x01, + KeyCode::JoyDown => 0x02, + KeyCode::JoyLeft => 0x03, + KeyCode::JoyRight => 0x04, + KeyCode::JoyCenter => 0x05, + KeyCode::BtnLeft1 => 0x06, + KeyCode::BtnRight1 => 0x07, + KeyCode::BtnLeft2 => 0x11, + KeyCode::BtnRight2 => 0x12, + KeyCode::Backspace => 0x08, + KeyCode::Tab => 0x09, + KeyCode::Enter => 0x0A, + KeyCode::ModAlt => 0xA1, + KeyCode::ModShiftLeft => 0xA2, + KeyCode::ModShiftRight => 0xA3, + KeyCode::ModSym => 0xA4, + KeyCode::ModCtrl => 0xA5, + KeyCode::Esc => 0xB1, + KeyCode::Left => 0xB4, + KeyCode::Up => 0xB5, + KeyCode::Down => 0xB6, + KeyCode::Right => 0xB7, + KeyCode::Break => 0xD0, + KeyCode::Insert => 0xD1, + KeyCode::Home => 0xD2, + KeyCode::Del => 0xD4, + KeyCode::End => 0xD5, + KeyCode::PageUp => 0xD6, + KeyCode::PageDown => 0xD7, + KeyCode::CapsLock => 0xC1, + KeyCode::F1 => 0x81, + KeyCode::F2 => 0x82, + KeyCode::F3 => 0x83, + KeyCode::F4 => 0x84, + KeyCode::F5 => 0x85, + KeyCode::F6 => 0x86, + KeyCode::F7 => 0x87, + KeyCode::F8 => 0x88, + KeyCode::F9 => 0x89, + KeyCode::F10 => 0x90, + KeyCode::Char(char) => char as u8, + KeyCode::Unknown(i) => i, + } + } + } + + impl From for KeyCode { + fn from(value: u8) -> Self { + match value { + 0x01 => Self::JoyUp, + 0x02 => Self::JoyDown, + 0x03 => Self::JoyLeft, + 0x04 => Self::JoyRight, + 0x05 => Self::JoyCenter, + 0x06 => Self::BtnLeft1, + 0x07 => Self::BtnRight1, + 0x08 => Self::Backspace, + 0x09 => Self::Tab, + 0x0A => Self::Enter, + 0x11 => Self::BtnLeft2, + 0x12 => Self::BtnRight2, + 0xA1 => Self::ModAlt, + 0xA2 => Self::ModShiftLeft, + 0xA3 => Self::ModShiftRight, + 0xA4 => Self::ModSym, + 0xA5 => Self::ModCtrl, + 0xB1 => Self::Esc, + 0xB4 => Self::Left, + 0xB5 => Self::Up, + 0xB6 => Self::Down, + 0xB7 => Self::Right, + 0xC1 => Self::CapsLock, + 0xD0 => Self::Break, + 0xD1 => Self::Insert, + 0xD2 => Self::Home, + 0xD4 => Self::Del, + 0xD5 => Self::End, + 0xD6 => Self::PageUp, + 0xD7 => Self::PageDown, + 0x81 => Self::F1, + 0x82 => Self::F2, + 0x83 => Self::F3, + 0x84 => Self::F4, + 0x85 => Self::F5, + 0x86 => Self::F6, + 0x87 => Self::F7, + 0x88 => Self::F8, + 0x89 => Self::F9, + 0x90 => Self::F10, + _ => match char::from_u32(value as u32) { + Some(c) => Self::Char(c), + None => Self::Unknown(value), + }, + } + } + } + + pub type GetKeyAbi = extern "C" fn() -> KeyEventC; + + #[unsafe(no_mangle)] + pub extern "C" fn get_key() -> KeyEventC { + let f: GetKeyAbi = + unsafe { core::mem::transmute(CALL_ABI_TABLE[CallAbiTable::GetKey as usize]) }; + f() + } +} + +#[repr(C)] +pub enum RngRequest { + U32(u32), + U64(u64), + Bytes { ptr: *mut u8, len: usize }, +} + +pub type GenRand = extern "C" fn(req: &mut RngRequest); + +#[unsafe(no_mangle)] +pub extern "C" fn gen_rand(req: &mut RngRequest) { + unsafe { + let ptr = CALL_ABI_TABLE[CallAbiTable::GenRand as usize]; + let f: GenRand = core::mem::transmute(ptr); + f(req) + } +} + +pub type ListDir = extern "C" fn( + str: *const u8, + len: usize, + files: *mut Option, + file_len: usize, +) -> usize; + +#[unsafe(no_mangle)] +pub extern "C" fn list_dir( + str: *const u8, + len: usize, + files: *mut Option, + file_len: usize, +) -> usize { + unsafe { + let ptr = CALL_ABI_TABLE[CallAbiTable::ListDir as usize]; + let f: ListDir = core::mem::transmute(ptr); + f(str, len, files, file_len) + } +} + +pub type ReadFile = extern "C" fn( + str: *const u8, + len: usize, + read_from: usize, + buf: *mut u8, + buf_len: usize, +) -> usize; + +#[unsafe(no_mangle)] +pub extern "C" fn read_file( + str: *const u8, + len: usize, + read_from: usize, + buf: *mut u8, + buf_len: usize, +) -> usize { + unsafe { + let ptr = CALL_ABI_TABLE[CallAbiTable::ReadFile as usize]; + let f: ReadFile = core::mem::transmute(ptr); + f(str, len, read_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 = CALL_ABI_TABLE[CallAbiTable::FileLen as usize]; + let f: FileLen = core::mem::transmute(ptr); + f(str, len) + } +} diff --git a/assets/gallery/Sprite-0001.bmp b/assets/gallery/Sprite-0001.bmp new file mode 100644 index 0000000..8e24bd6 Binary files /dev/null and b/assets/gallery/Sprite-0001.bmp differ diff --git a/assets/gallery/Sprite-0002.bmp b/assets/gallery/Sprite-0002.bmp new file mode 100644 index 0000000..b2678b5 Binary files /dev/null and b/assets/gallery/Sprite-0002.bmp differ diff --git a/assets/gallery/Sprite-0003.bmp b/assets/gallery/Sprite-0003.bmp new file mode 100644 index 0000000..ee8e1ea Binary files /dev/null and b/assets/gallery/Sprite-0003.bmp differ diff --git a/assets/gallery/Sprite-0004.bmp b/assets/gallery/Sprite-0004.bmp new file mode 100644 index 0000000..1988ce5 Binary files /dev/null and b/assets/gallery/Sprite-0004.bmp differ diff --git a/assets/gallery/Sprite-0005.bmp b/assets/gallery/Sprite-0005.bmp new file mode 100644 index 0000000..172ddc8 Binary files /dev/null and b/assets/gallery/Sprite-0005.bmp differ diff --git a/cbindgen.toml b/cbindgen.toml new file mode 100644 index 0000000..56fe889 --- /dev/null +++ b/cbindgen.toml @@ -0,0 +1,4 @@ +language = "C" + +[macro_expansion] +bitflags = true diff --git a/justfile b/justfile new file mode 100644 index 0000000..0f1a419 --- /dev/null +++ b/justfile @@ -0,0 +1,18 @@ +kernel-dev: + cargo run --bin kernel +kernel-release: + cargo build --bin kernel --release + elf2uf2-rs -d target/thumbv8m.main-none-eabihf/release/kernel + +binary-args := "RUSTFLAGS=\"-C link-arg=-pie -C relocation-model=pic\"" + +cbindgen: + cbindgen abi_sys --output abi_sys.h -q + +userapp app: + {{binary-args}} cargo build --bin {{app}} --profile release-binary + +userapps: cbindgen + just userapp calculator + just userapp snake + just userapp gallery diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml new file mode 100644 index 0000000..2db6363 --- /dev/null +++ b/kernel/Cargo.toml @@ -0,0 +1,93 @@ +[package] +name = "kernel" +version = "0.0.1" +edition = "2024" + +[[bin]] +name = "kernel" +path = "src/main.rs" +test = false +doctest = false +bench = false + +[features] +default = ["rp235x", "defmt"] +rp2040 = ["embassy-rp/rp2040"] +rp235x = ["embassy-rp/rp235xb"] +trouble = ["dep:bt-hci", "dep:cyw43", "dep:cyw43-pio", "dep:trouble-host"] +defmt = [ + "dep:defmt", + "panic-probe/print-defmt", + "embassy-executor/defmt", + "embassy-time/defmt", + "embassy-time/defmt-timestamp-uptime", + "embassy-rp/defmt", + "embassy-sync/defmt", + "embedded-graphics/defmt", + "embedded-sdmmc/defmt-log", + # "bt-hci/defmt", + # "cyw43/defmt", + # "cyw43-pio/defmt", +] + +[dependencies] +embassy-executor = { version = "0.9", features = [ + "arch-cortex-m", + "executor-interrupt", + "executor-thread", + "nightly", +] } +embassy-rp = { version = "0.8.0", features = [ + "critical-section-impl", + "unstable-pac", + "time-driver", +] } +embassy-usb = "0.5.1" +embassy-futures = "0.1.2" +embassy-time = { version = "0.5.0", features = ["generic-queue-8"] } +embassy-embedded-hal = "0.3.2" +embassy-sync = { version = "0.7" } +trouble-host = { version = "0.1", features = [ + "derive", + "scan", +], optional = true } +bt-hci = { version = "0.2", default-features = false, optional = true } +cyw43 = { version = "0.3.0", features = [ + "firmware-logs", + "bluetooth", +], optional = true } +cyw43-pio = { version = "0.3.0", optional = true } + +embedded-hal-bus = { version = "0.3.0", features = ["async"] } +embedded-hal = "0.2.7" +embedded-hal_2 = { package = "embedded-hal", version = "1.0.0" } +embedded-hal-async = "1.0.0" +cortex-m = { version = "0.7.7" } +cortex-m-rt = "0.7.5" +panic-probe = "0.3" +portable-atomic = { version = "1.11", features = ["critical-section"] } +assign-resources = "0.5.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" + +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 } +static_cell = "2.1.1" +bitflags = "2.9.4" +heapless = "0.8.0" +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" +bumpalo = "3.19.0" + +abi_sys = { path = "../abi_sys" } diff --git a/build.rs b/kernel/build.rs similarity index 100% rename from build.rs rename to kernel/build.rs diff --git a/memory.x b/kernel/memory.x similarity index 73% rename from memory.x rename to kernel/memory.x index 5bdbb38..a2edb86 100644 --- a/memory.x +++ b/kernel/memory.x @@ -1,22 +1,7 @@ MEMORY { - /* - * The RP2350 has either external or internal flash. - * - * 2 MiB is a safe default here, although a Pico 2 has 4 MiB. - */ FLASH : ORIGIN = 0x10000000, LENGTH = 4096K - /* - * RAM consists of 8 banks, SRAM0-SRAM7, with a striped mapping. - * This is usually good for performance, as it distributes load on - * those banks evenly. - */ + RAM : ORIGIN = 0x20000000, LENGTH = 512K - /* - * RAM banks 8 and 9 use a direct mapping. They can be used to have - * memory areas dedicated for some specific job, improving predictability - * of access times. - * Example: Separate stacks for core0 and core1. - */ SRAM4 : ORIGIN = 0x20080000, LENGTH = 4K SRAM5 : ORIGIN = 0x20081000, LENGTH = 4K } diff --git a/kernel/src/abi.rs b/kernel/src/abi.rs new file mode 100644 index 0000000..3bed7c0 --- /dev/null +++ b/kernel/src/abi.rs @@ -0,0 +1,224 @@ +use abi_sys::{ + CPixel, DrawIterAbi, FileLen, GenRand, GetMsAbi, ListDir, LockDisplay, PrintAbi, ReadFile, + RngRequest, SleepMsAbi, keyboard::*, +}; +use alloc::{string::ToString, vec::Vec}; +use core::sync::atomic::Ordering; +use embassy_rp::clocks::{RoscRng, clk_sys_freq}; +use embassy_time::Instant; +use embedded_graphics::draw_target::DrawTarget; +use embedded_sdmmc::{DirEntry, LfnBuffer}; +use heapless::spsc::Queue; + +use crate::{ + display::{FB_PAUSED, FRAMEBUFFER}, + storage::{Dir, File, SDCARD}, +}; + +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) }; + + if let Ok(msg) = core::str::from_utf8(slice) { + defmt::info!("print: {}", msg); + } else { + defmt::warn!("print: "); + } +} + +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; + + for _ in 0..total_cycles { + cortex_m::asm::nop(); + } +} + +pub static mut MS_SINCE_LAUNCH: Option = None; + +const _: GetMsAbi = get_ms; +pub extern "C" fn get_ms() -> u64 { + Instant::now() + .duration_since(unsafe { MS_SINCE_LAUNCH.unwrap() }) + .as_millis() +} + +const _: LockDisplay = lock_display; +pub extern "C" fn lock_display(lock: bool) { + FB_PAUSED.store(lock, Ordering::Relaxed); +} + +const _: DrawIterAbi = draw_iter; +// TODO: maybe return result +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) }; + + let iter = cpixels.iter().copied().map(|c: CPixel| c.into()); + unsafe { FRAMEBUFFER.draw_iter(iter).unwrap() } +} + +pub static mut KEY_CACHE: Queue = Queue::new(); + +const _: GetKeyAbi = get_key; +pub extern "C" fn get_key() -> KeyEventC { + if let Some(event) = unsafe { KEY_CACHE.dequeue() } { + event.into() + } else { + KeyEvent { + key: KeyCode::Unknown(0), + state: KeyState::Idle, + mods: Modifiers::empty(), + } + .into() + } +} + +const _: GenRand = gen_rand; +pub extern "C" fn gen_rand(req: &mut RngRequest) { + let mut rng = RoscRng; + + match req { + RngRequest::U32(i) => *i = rng.next_u32(), + RngRequest::U64(i) => *i = rng.next_u64(), + RngRequest::Bytes { ptr, len } => { + // SAFETY: caller guarantees `ptr` is valid for `len` bytes + let slice: &mut [u8] = unsafe { core::slice::from_raw_parts_mut(*ptr, *len) }; + rng.fill_bytes(slice); + } + } +} + +fn get_dir_entries(dir: &Dir, files: &mut [Option]) -> usize { + let mut i = 0; + dir.iterate_dir(|entry| { + if i < files.len() { + files[i] = Some(entry.clone()); + i += 1; + } + }) + .unwrap(); + i +} + +fn recurse_dir(dir: &Dir, dirs: &[&str], files: &mut [Option]) -> usize { + if dirs.is_empty() { + return get_dir_entries(dir, files); + } + + let dir = dir.open_dir(dirs[0]).unwrap(); + recurse_dir(&dir, &dirs[1..], files) +} + +const _: ListDir = list_dir; +pub extern "C" fn list_dir( + dir: *const u8, + len: usize, + files: *mut Option, + files_len: usize, +) -> usize { + // SAFETY: caller guarantees `ptr` is valid for `len` bytes + 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(); + + let mut guard = SDCARD.get().try_lock().expect("Failed to get sdcard"); + let sd = guard.as_mut().unwrap(); + + let mut wrote = 0; + sd.access_root_dir(|root| { + if dirs[0] == "" && dirs.len() >= 2 { + if dir == "/" { + wrote = get_dir_entries(&root, files); + } else { + wrote = recurse_dir(&root, &dirs[1..], files); + } + } + }); + wrote +} + +fn recurse_file( + dir: &Dir, + dirs: &[&str], + mut access: impl FnMut(&mut File) -> T, +) -> Result { + if dirs.len() == 1 { + let mut b = [0_u8; 50]; + 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 { + if name == dirs[0] || entry.name.to_string().as_str() == dirs[0] { + short_name = Some(entry.name.clone()); + } + } + }) + .unwrap(); + if let Some(name) = short_name { + let mut file = dir + .open_file_in_dir(name, embedded_sdmmc::Mode::ReadWriteAppend) + .map_err(|_| ())?; + return Ok(access(&mut file)); + } + return Err(()); + } + + let dir = dir.open_dir(dirs[0]).unwrap(); + recurse_file(&dir, &dirs[1..], access) +} + +const _: ReadFile = read_file; +pub extern "C" fn read_file( + str: *const u8, + len: usize, + start_from: usize, + buf: *mut u8, + buf_len: usize, +) -> usize { + // 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(); + // SAFETY: caller guarantees `ptr` is valid for `len` bytes + let mut buf = unsafe { core::slice::from_raw_parts_mut(buf, buf_len) }; + + let mut read = 0; + + 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| { + 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 + }; + }); + } + read +} + +const _: FileLen = file_len; +pub extern "C" fn file_len(str: *const u8, len: usize) -> usize { + // 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(); + + let mut len = 0; + + 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| { + if let Ok(result) = recurse_file(&root, &file[1..], |file| file.length()) { + len = result + } + }); + } + len as usize +} diff --git a/kernel/src/display.rs b/kernel/src/display.rs new file mode 100644 index 0000000..ee7996b --- /dev/null +++ b/kernel/src/display.rs @@ -0,0 +1,64 @@ +use core::sync::atomic::{AtomicBool, Ordering}; + +use crate::framebuffer::AtomicFrameBuffer; +use embassy_rp::{ + Peri, + gpio::{Level, Output}, + peripherals::{PIN_13, PIN_14, PIN_15, SPI1}, + spi::{Async, Spi}, +}; +use embassy_time::{Delay, Timer}; +use embedded_hal_bus::spi::ExclusiveDevice; +use st7365p_lcd::ST7365P; + +type DISPLAY = ST7365P< + ExclusiveDevice, Output<'static>, Delay>, + Output<'static>, + Output<'static>, + Delay, +>; + +pub const SCREEN_WIDTH: usize = 320; +pub const SCREEN_HEIGHT: usize = 320; + +pub static mut FRAMEBUFFER: AtomicFrameBuffer = AtomicFrameBuffer::new(); +pub static FB_PAUSED: AtomicBool = AtomicBool::new(false); + +pub async fn init_display( + spi: Spi<'static, SPI1, Async>, + cs: Peri<'static, PIN_13>, + data: Peri<'static, PIN_14>, + reset: Peri<'static, PIN_15>, +) -> DISPLAY { + let spi_device = ExclusiveDevice::new(spi, Output::new(cs, Level::Low), Delay).unwrap(); + let mut display = ST7365P::new( + spi_device, + Output::new(data, Level::Low), + Some(Output::new(reset, Level::High)), + false, + true, + Delay, + ); + display.init().await.unwrap(); + display.set_custom_orientation(0x40).await.unwrap(); + unsafe { FRAMEBUFFER.draw(&mut display).await.unwrap() } + display.set_on().await.unwrap(); + + display +} + +#[embassy_executor::task] +pub async fn display_handler(mut display: DISPLAY) { + loop { + if !FB_PAUSED.load(Ordering::Acquire) { + unsafe { + FRAMEBUFFER + .partial_draw_batched(&mut display) + .await + .unwrap() + } + } + + Timer::after_millis(10).await; + } +} diff --git a/kernel/src/elf.rs b/kernel/src/elf.rs new file mode 100644 index 0000000..2fcec1b --- /dev/null +++ b/kernel/src/elf.rs @@ -0,0 +1,277 @@ +use crate::{ + abi, + storage::{File, SDCARD}, +}; +use abi_sys::CallAbiTable; +use abi_sys::EntryFn; +use alloc::{vec, vec::Vec}; +use bumpalo::Bump; +use core::ptr; +use embedded_sdmmc::ShortFileName; +use goblin::{ + elf::{ + header::header32::Header, + program_header::program_header32::{PT_LOAD, ProgramHeader}, + reloc::R_ARM_RELATIVE, + section_header::{SHT_REL, SHT_SYMTAB}, + }, + elf32::{header, reloc::Rel, section_header::SectionHeader, sym::Sym}, +}; +use strum::IntoEnumIterator; + +const ELF32_HDR_SIZE: usize = 52; + +pub async unsafe fn load_binary(name: &ShortFileName) -> Option<(EntryFn, Bump)> { + let mut sd_lock = SDCARD.get().lock().await; + let sd = sd_lock.as_mut().unwrap(); + + let mut header_buf = [0; ELF32_HDR_SIZE]; + + let (entry, bump) = sd + .read_file(name, |mut file| { + file.read(&mut header_buf).unwrap(); + let elf_header = Header::from_bytes(&header_buf); + + // reject non-PIE + if elf_header.e_type != header::ET_DYN { + return None; + } + + 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); + + let bump = Bump::with_capacity(total_size); + let base = bump.alloc_slice_fill_default::(total_size); + + // load each segment into bump, relative to base_ptr + for i in 0..elf_header.e_phnum { + file.seek_from_start(elf_header.e_phoff + (elf_header.e_phentsize * i) as u32) + .unwrap(); + file.read(&mut ph_buf).unwrap(); + let ph = cast_phdr(&ph_buf); + + let seg_offset = (ph.p_vaddr - min_vaddr) 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, &mut segment).unwrap(); + } + } + + for i in 0..elf_header.e_shnum { + let sh = read_section(&mut file, elf_header, i.into()); + + match sh.sh_type { + SHT_REL => { + apply_relocations(&sh, min_vaddr, base.as_mut_ptr(), &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 { + core::mem::transmute(base.as_ptr().add((elf_header.e_entry - min_vaddr) as usize)) + }; + + Some((entry_ptr, bump)) + }) + .await + .expect("Failed to read file")?; + + Some((entry, bump)) +} + +fn load_segment(file: &mut File, ph: &ProgramHeader, segment: &mut [u8]) -> Result<(), ()> { + let filesz = ph.p_filesz as usize; + let memsz = ph.p_memsz as usize; + + // read file contents + let mut remaining = filesz; + let mut dst_offset = 0; + let mut file_offset = ph.p_offset; + let mut buf = [0u8; 512]; + + while remaining > 0 { + let to_read = core::cmp::min(remaining, buf.len()); + file.seek_from_start(file_offset).unwrap(); + file.read(&mut buf[..to_read]).unwrap(); + + segment[dst_offset..dst_offset + to_read].copy_from_slice(&buf[..to_read]); + + remaining -= to_read; + dst_offset += to_read; + file_offset += to_read as u32; + } + + // zero BSS if needed + if memsz > filesz { + segment[filesz..].fill(0); + } + + Ok(()) +} + +fn apply_relocations( + sh: &SectionHeader, + min_vaddr: u32, + base: *mut u8, + file: &mut File, +) -> Result<(), ()> { + let mut reloc = [0_u8; 8]; + + let num_relocs = sh.sh_size as usize / sh.sh_entsize as usize; + + for i in 0..num_relocs { + file.seek_from_start(sh.sh_offset + (i as u32 * 8)).unwrap(); + file.read(&mut reloc).unwrap(); + + let rel = cast_rel(&reloc); + + let reloc_type = rel.r_info & 0xff; + let reloc_addr = unsafe { base.add((rel.r_offset - min_vaddr) as usize) as *mut u32 }; + + match reloc_type { + R_ARM_RELATIVE => { + // REL: add base to the word already stored there + unsafe { + let val = ptr::read_unaligned(reloc_addr); + ptr::write_unaligned(reloc_addr, val.wrapping_add(base as u32)); + } + } + _ => { + return Err(()); + } + } + } + Ok(()) +} + +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()); + + // find the symbol table + if sh.sh_type == SHT_SYMTAB { + let mut symtab_buf = vec![0u8; sh.sh_size as usize]; + file.seek_from_start(sh.sh_offset).unwrap(); + file.read(&mut symtab_buf).unwrap(); + + // Cast buffer into symbols + let sym_count = sh.sh_size as usize / sh.sh_entsize as usize; + for i in 0..sym_count { + let sym_bytes = + &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 mut name = Vec::new(); + file.seek_from_start(str_sh.sh_offset + sym.st_name) + .unwrap(); + + loop { + let mut byte = [0u8; 1]; + file.read(&mut byte).unwrap(); + if byte[0] == 0 { + break; + } + name.push(byte[0]); + } + + let symbol_name = core::str::from_utf8(&name).unwrap(); + 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 CallAbiTable::iter().enumerate() { + let ptr = match call { + CallAbiTable::PrintString => abi::print as usize, + CallAbiTable::SleepMs => abi::sleep as usize, + CallAbiTable::GetMs => abi::get_ms as usize, + CallAbiTable::LockDisplay => abi::lock_display as usize, + CallAbiTable::DrawIter => abi::draw_iter as usize, + CallAbiTable::GetKey => abi::get_key as usize, + CallAbiTable::GenRand => abi::gen_rand as usize, + CallAbiTable::ListDir => abi::list_dir as usize, + CallAbiTable::ReadFile => abi::read_file as usize, + CallAbiTable::FileLen => abi::file_len as usize, + }; + unsafe { + table_base.add(idx as usize).write(ptr); + } + } + return Ok(()); + } + } + } + } + Err(()) +} + +fn total_loadable_size( + file: &mut File, + elf_header: &Header, + ph_buf: &mut [u8], +) -> (usize, u32, u32) { + let mut min_vaddr = u32::MAX; + let mut max_vaddr = 0u32; + for i in 0..elf_header.e_phnum { + 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); + + if ph.p_type == PT_LOAD { + if ph.p_vaddr < min_vaddr { + min_vaddr = ph.p_vaddr; + } + if ph.p_vaddr + ph.p_memsz > max_vaddr { + max_vaddr = ph.p_vaddr + ph.p_memsz; + } + } + } + + let total_size = (max_vaddr - min_vaddr) as usize; + (total_size, min_vaddr, max_vaddr) +} + +fn read_section(file: &mut File, elf_header: &Header, section: u32) -> SectionHeader { + let mut sh_buf = vec![0_u8; elf_header.e_shentsize as usize]; + + file.seek_from_start(elf_header.e_shoff + (elf_header.e_shentsize as u32 * section)) + .unwrap(); + file.read(&mut sh_buf).unwrap(); + + cast_shdr(&sh_buf) +} + +fn cast_phdr(buf: &[u8]) -> ProgramHeader { + assert!(buf.len() >= core::mem::size_of::()); + unsafe { core::ptr::read(buf.as_ptr() as *const ProgramHeader) } +} + +fn cast_shdr(buf: &[u8]) -> SectionHeader { + assert!(buf.len() >= core::mem::size_of::()); + unsafe { core::ptr::read(buf.as_ptr() as *const SectionHeader) } +} + +fn cast_sym(buf: &[u8]) -> Sym { + assert!(buf.len() >= core::mem::size_of::()); + unsafe { core::ptr::read(buf.as_ptr() as *const Sym) } +} + +fn cast_rel(buf: &[u8]) -> Rel { + assert!(buf.len() >= core::mem::size_of::()); + unsafe { core::ptr::read(buf.as_ptr() as *const Rel) } +} diff --git a/kernel/src/framebuffer.rs b/kernel/src/framebuffer.rs new file mode 100644 index 0000000..04823f8 --- /dev/null +++ b/kernel/src/framebuffer.rs @@ -0,0 +1,367 @@ +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::{ + Rgb565, + raw::{RawData, RawU16}, + }, + prelude::*, + primitives::Rectangle, +}; +use embedded_hal_2::digital::OutputPin; +use embedded_hal_async::{delay::DelayNs, spi::SpiDevice}; +use heapless::Vec; +use st7365p_lcd::ST7365P; + +pub const TILE_SIZE: usize = 16; // 16x16 tile +pub const TILE_COUNT: usize = (SCREEN_WIDTH / TILE_SIZE) * (SCREEN_HEIGHT / TILE_SIZE); // 400 tiles + +// Group of tiles for batching +pub const MAX_META_TILES: usize = SCREEN_WIDTH / TILE_SIZE; // max number of meta tiles in buffer +type MetaTileVec = heapless::Vec; + +const SIZE: usize = SCREEN_HEIGHT * SCREEN_WIDTH; + +static mut BUFFER: [u16; SIZE] = [0; SIZE]; + +static mut DIRTY_TILES: LazyLock> = LazyLock::new(|| { + let mut tiles = Vec::new(); + for _ in 0..TILE_COUNT { + tiles.push(AtomicBool::new(true)).unwrap(); + } + tiles +}); + +#[allow(dead_code)] +pub struct AtomicFrameBuffer; + +impl AtomicFrameBuffer { + pub const fn new() -> Self { + Self + } + + fn mark_tiles_dirty(&mut self, rect: Rectangle) { + 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; + let end_ty = ((rect.top_left.y + rect.size.height as i32 - 1) as usize) / TILE_SIZE; + + 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(true, Ordering::Relaxed) }; + } + } + } + + fn set_pixel(&mut self, x: u16, y: u16, color: u16) -> Result<(), ()> { + unsafe { BUFFER[(y as usize * SCREEN_WIDTH) + x as usize] = color }; + + Ok(()) + } + + fn set_pixels_buffered>( + &mut self, + sx: u16, + sy: u16, + ex: u16, + ey: u16, + colors: P, + ) -> Result<(), ()> { + if sx >= self.size().width as u16 + || ex >= self.size().width as u16 + || sy >= self.size().height as u16 + || ey >= self.size().height as u16 + { + return Err(()); // Bounds check + } + + let mut color_iter = colors.into_iter(); + + for y in sy..=ey { + for x in sx..=ex { + if let Some(color) = color_iter.next() { + unsafe { BUFFER[(y as usize * SCREEN_WIDTH) + x as usize] = color }; + } else { + return Err(()); // Not enough data + } + } + } + + // Optional: check that we consumed *exactly* the right amount + if color_iter.next().is_some() { + return Err(()); // Too much data + } + + Ok(()) + } + + // 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(); + + 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; + } + + // 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::Relaxed) + } + && (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 + pub async fn draw( + &mut self, + display: &mut ST7365P, + ) -> Result<(), ()> + where + SPI: SpiDevice, + DC: OutputPin, + RST: OutputPin, + { + display + .set_pixels_buffered( + 0, + 0, + self.size().width as u16 - 1, + self.size().height as u16 - 1, + unsafe { &BUFFER }, + ) + .await?; + + unsafe { + for tile in DIRTY_TILES.get_mut().iter() { + tile.store(false, Ordering::Release); + } + }; + + Ok(()) + } + + /// Sends only dirty tiles (16x16px) in batches to the display + pub async fn partial_draw_batched( + &mut self, + display: &mut ST7365P, + ) -> Result<(), ()> + where + SPI: SpiDevice, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, + { + 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 = + 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(unsafe { &BUFFER[start..end] }) + .unwrap(); + } + + display + .set_pixels_buffered( + 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?; + + // 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; + + 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) }; + } + } + } + } + + Ok(()) + } +} + +impl DrawTarget for AtomicFrameBuffer { + type Error = (); + type Color = Rgb565; + + fn draw_iter(&mut self, pixels: I) -> Result<(), Self::Error> + where + I: IntoIterator>, + { + let mut dirty_rect: Option = None; + + for Pixel(coord, color) in pixels { + if coord.x >= 0 && coord.y >= 0 { + let x = coord.x as i32; + let y = coord.y as i32; + + if (x as usize) < SCREEN_WIDTH && (y as usize) < SCREEN_HEIGHT { + unsafe { + BUFFER[(y as usize) * SCREEN_WIDTH + (x as usize)] = + RawU16::from(color).into_inner() + }; + + if let Some(ref mut rect) = dirty_rect { + rect.top_left.x = rect.top_left.x.min(x); + rect.top_left.y = rect.top_left.y.min(y); + let max_x = (rect.top_left.x + rect.size.width as i32 - 1).max(x); + let max_y = (rect.top_left.y + rect.size.height as i32 - 1).max(y); + rect.size.width = (max_x - rect.top_left.x + 1) as u32; + rect.size.height = (max_y - rect.top_left.y + 1) as u32; + } else { + dirty_rect = Some(Rectangle::new(Point::new(x, y), Size::new(1, 1))); + } + } + } + } + + if let Some(rect) = dirty_rect { + self.mark_tiles_dirty(rect); + } + + Ok(()) + } + + fn fill_contiguous(&mut self, area: &Rectangle, colors: I) -> Result<(), Self::Error> + where + I: IntoIterator, + { + let drawable_area = area.intersection(&Rectangle::new(Point::zero(), self.size())); + + if drawable_area.size != Size::zero() { + // We assume that `colors` iterator is in row-major order for the original `area` + // So we must skip rows/pixels that are clipped + let area_width = area.size.width; + let area_height = area.size.height; + let mut colors = colors.into_iter(); + + for y in 0..area_height { + for x in 0..area_width { + let p = area.top_left + Point::new(x as i32, y as i32); + + if drawable_area.contains(p) { + if let Some(color) = colors.next() { + self.set_pixel( + p.x as u16, + p.y as u16, + RawU16::from(color).into_inner(), + )?; + } else { + break; + } + } else { + // Still need to consume the color even if not used! + let _ = colors.next(); + } + } + } + + self.mark_tiles_dirty(*area); + } + + Ok(()) + } + + fn fill_solid(&mut self, area: &Rectangle, color: Self::Color) -> Result<(), Self::Error> { + self.fill_contiguous( + area, + core::iter::repeat(color).take((self.size().width * self.size().height) as usize), + ) + } + + fn clear(&mut self, color: Self::Color) -> Result<(), Self::Error> { + self.set_pixels_buffered( + 0, + 0, + self.size().width as u16 - 1, + self.size().height as u16 - 1, + core::iter::repeat(RawU16::from(color).into_inner()) + .take((self.size().width * self.size().height) as usize), + )?; + + for tile in unsafe { DIRTY_TILES.get_mut() }.iter() { + tile.store(true, Ordering::Release); + } + + Ok(()) + } +} + +impl OriginDimensions for AtomicFrameBuffer { + fn size(&self) -> Size { + Size::new(SCREEN_WIDTH as u32, SCREEN_HEIGHT as u32) + } +} diff --git a/src/heap.rs b/kernel/src/heap.rs similarity index 100% rename from src/heap.rs rename to kernel/src/heap.rs diff --git a/kernel/src/main.rs b/kernel/src/main.rs new file mode 100644 index 0000000..57490ec --- /dev/null +++ b/kernel/src/main.rs @@ -0,0 +1,280 @@ +#![feature(impl_trait_in_assoc_type)] +#![feature(str_from_raw_parts)] +#![cfg_attr(not(test), no_std)] +#![cfg_attr(not(test), no_main)] +#![allow(static_mut_refs)] + +extern crate alloc; + +mod abi; +mod display; +mod elf; +mod framebuffer; +mod peripherals; +mod scsi; +mod storage; +mod ui; +mod usb; +mod utils; + +use crate::{ + abi::{KEY_CACHE, MS_SINCE_LAUNCH}, + display::{FRAMEBUFFER, display_handler, init_display}, + peripherals::{ + conf_peripherals, + keyboard::{KeyState, read_keyboard_fifo}, + }, + scsi::MSC_SHUTDOWN, + storage::{SDCARD, SdCard}, + ui::{SELECTIONS, clear_selection, ui_handler}, +}; +use abi_sys::EntryFn; +use bumpalo::Bump; +use embedded_graphics::{ + pixelcolor::Rgb565, + prelude::{DrawTarget, RgbColor}, +}; + +use {defmt_rtt as _, panic_probe as _}; + +use core::sync::atomic::{AtomicBool, Ordering}; +use defmt::unwrap; +use embassy_executor::{Executor, Spawner}; +use embassy_futures::{join::join, select::select}; +use embassy_rp::{ + Peri, + gpio::{Input, Level, Output, Pull}, + i2c::{self, I2c}, + multicore::{Stack, spawn_core1}, + peripherals::{ + DMA_CH0, DMA_CH1, I2C1, 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_22, SPI0, SPI1, USB, + }, + spi::{self, Spi}, + usb as embassy_rp_usb, +}; +use embassy_sync::{ + blocking_mutex::raw::CriticalSectionRawMutex, channel::Channel, signal::Signal, +}; +use embassy_time::{Delay, Instant, Timer}; +use embedded_hal_bus::spi::ExclusiveDevice; +use embedded_sdmmc::SdCard as SdmmcSdCard; +use static_cell::StaticCell; +use talc::*; + +embassy_rp::bind_interrupts!(struct Irqs { + I2C1_IRQ => i2c::InterruptHandler; + USBCTRL_IRQ => embassy_rp_usb::InterruptHandler; +}); + +static mut CORE1_STACK: Stack<16384> = Stack::new(); +static EXECUTOR0: StaticCell = StaticCell::new(); +static EXECUTOR1: StaticCell = StaticCell::new(); + +static mut ARENA: [u8; 200 * 1024] = [0; 200 * 1024]; + +#[global_allocator] +static ALLOCATOR: Talck, ClaimOnOom> = + Talc::new(unsafe { ClaimOnOom::new(Span::from_array(core::ptr::addr_of!(ARENA).cast_mut())) }) + .lock(); + +static ENABLE_UI: AtomicBool = AtomicBool::new(true); +static UI_CHANGE: Signal = Signal::new(); + +#[embassy_executor::main] +async fn main(_spawner: Spawner) { + let p = embassy_rp::init(Default::default()); + + spawn_core1( + p.CORE1, + unsafe { &mut *core::ptr::addr_of_mut!(CORE1_STACK) }, + move || { + let executor1 = EXECUTOR1.init(Executor::new()); + executor1.run(|spawner| unwrap!(spawner.spawn(userland_task()))); + }, + ); + + let display = Display { + spi: p.SPI1, + clk: p.PIN_10, + mosi: p.PIN_11, + miso: p.PIN_12, + dma1: p.DMA_CH0, + dma2: p.DMA_CH1, + cs: p.PIN_13, + data: p.PIN_14, + reset: p.PIN_15, + }; + let sd = Sd { + spi: p.SPI0, + clk: p.PIN_18, + mosi: p.PIN_19, + miso: p.PIN_16, + cs: p.PIN_17, + det: p.PIN_22, + }; + let mcu = Mcu { + i2c: p.I2C1, + clk: p.PIN_7, + data: p.PIN_6, + }; + let executor0 = EXECUTOR0.init(Executor::new()); + executor0.run(|spawner| unwrap!(spawner.spawn(kernel_task(spawner, display, sd, mcu, p.USB)))); +} + +// One-slot channel to pass EntryFn from core1 +static BINARY_CH: Channel = Channel::new(); + +// runs dynamically loaded elf files +#[embassy_executor::task] +async fn userland_task() { + let recv = BINARY_CH.receiver(); + loop { + let (entry, _bump) = recv.receive().await; + + // disable kernel ui + { + ENABLE_UI.store(false, Ordering::Release); + UI_CHANGE.signal(()); + + clear_selection().await; + + MSC_SHUTDOWN.signal(()); + } + + unsafe { MS_SINCE_LAUNCH = Some(Instant::now()) }; + defmt::info!("Executing Binary"); + entry(); + + // enable kernel ui + { + ENABLE_UI.store(true, Ordering::Release); + UI_CHANGE.signal(()); + unsafe { FRAMEBUFFER.clear(Rgb565::BLACK).unwrap() }; + + let mut selections = SELECTIONS.lock().await; + selections.set_changed(true); + } + } +} + +struct Display { + spi: Peri<'static, SPI1>, + clk: Peri<'static, PIN_10>, + mosi: Peri<'static, PIN_11>, + miso: Peri<'static, PIN_12>, + dma1: Peri<'static, DMA_CH0>, + dma2: Peri<'static, DMA_CH1>, + cs: Peri<'static, PIN_13>, + data: Peri<'static, PIN_14>, + reset: Peri<'static, PIN_15>, +} +struct Sd { + spi: Peri<'static, SPI0>, + clk: Peri<'static, PIN_18>, + mosi: Peri<'static, PIN_19>, + miso: Peri<'static, PIN_16>, + cs: Peri<'static, PIN_17>, + det: Peri<'static, PIN_22>, +} +struct Mcu { + i2c: Peri<'static, I2C1>, + clk: Peri<'static, PIN_7>, + data: Peri<'static, PIN_6>, +} + +async fn setup_mcu(mcu: Mcu) { + // MCU i2c bus for peripherals( keyboard) + let mut config = i2c::Config::default(); + config.frequency = 400_000; + let i2c1 = I2c::new_async(mcu.i2c, mcu.clk, mcu.data, Irqs, config); + conf_peripherals(i2c1).await; +} + +async fn setup_display(display: Display, spawner: Spawner) { + let mut config = spi::Config::default(); + config.frequency = 16_000_000; + let spi = Spi::new( + display.spi, + display.clk, + display.mosi, + display.miso, + display.dma1, + display.dma2, + config, + ); + let display = init_display(spi, display.cs, display.data, display.reset).await; + spawner.spawn(display_handler(display)).unwrap(); +} + +async fn setup_sd(sd: Sd) { + let mut config = spi::Config::default(); + config.frequency = 400_000; + let spi = Spi::new_blocking(sd.spi, sd.clk, sd.mosi, sd.miso, config.clone()); + let cs = Output::new(sd.cs, Level::High); + let det = Input::new(sd.det, Pull::None); + + let device = ExclusiveDevice::new(spi, cs, Delay).unwrap(); + let sdcard = SdmmcSdCard::new(device, Delay); + + config.frequency = 32_000_000; + sdcard.spi(|dev| dev.bus_mut().set_config(&config)); + SDCARD.get().lock().await.replace(SdCard::new(sdcard, det)); +} + +#[embassy_executor::task] +async fn kernel_task( + spawner: Spawner, + display: Display, + sd: Sd, + mcu: Mcu, + usb: Peri<'static, USB>, +) { + setup_mcu(mcu).await; + Timer::after_millis(250).await; + setup_display(display, spawner).await; + setup_sd(sd).await; + + let _usb = embassy_rp_usb::Driver::new(usb, Irqs); + // spawner.spawn(usb_handler(usb)).unwrap(); + + loop { + let ui_enabled = ENABLE_UI.load(Ordering::Relaxed); + if ui_enabled { + select(join(ui_handler(), prog_search_handler()), UI_CHANGE.wait()).await; + } else { + select(key_handler(), UI_CHANGE.wait()).await; + } + } +} + +async fn prog_search_handler() { + loop { + { + let mut guard = SDCARD.get().lock().await; + let sd = guard.as_mut().unwrap(); + + let files = sd.list_files_by_extension(".bin").unwrap(); + let mut select = SELECTIONS.lock().await; + + if *select.selections() != files { + select.update_selections(files); + select.reset(); + } + } + Timer::after_secs(5).await; + } +} + +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; + } +} diff --git a/kernel/src/peripherals/keyboard.rs b/kernel/src/peripherals/keyboard.rs new file mode 100644 index 0000000..28b65e7 --- /dev/null +++ b/kernel/src/peripherals/keyboard.rs @@ -0,0 +1,59 @@ +use crate::peripherals::PERIPHERAL_BUS; +pub use abi_sys::keyboard::{KeyCode, KeyEvent, KeyState, Modifiers}; + +const REG_ID_KEY: u8 = 0x04; +const REG_ID_FIF: u8 = 0x09; + +const KEY_CAPSLOCK: u8 = 1 << 5; +const KEY_NUMLOCK: u8 = 1 << 6; +const KEY_COUNT_MASK: u8 = 0x1F; // 0x1F == 31 + +pub async fn read_keyboard_fifo() -> Option { + let mut i2c = PERIPHERAL_BUS.get().lock().await; + let i2c = i2c.as_mut().unwrap(); + + let mut key_status = [0_u8; 1]; + + if i2c + .write_read_async(super::MCU_ADDR, [REG_ID_KEY], &mut key_status) + .await + .is_ok() + { + let _caps = key_status[0] & KEY_CAPSLOCK == KEY_CAPSLOCK; + let _num = key_status[0] & KEY_NUMLOCK == KEY_NUMLOCK; + let fifo_count = key_status[0] & KEY_COUNT_MASK; + + if fifo_count >= 1 { + let mut event = [0_u8; 2]; + + if i2c + .write_read_async(super::MCU_ADDR, [REG_ID_FIF], &mut event) + .await + .is_ok() + { + return Some(KeyEvent { + state: KeyState::from(event[0]), + key: KeyCode::from(event[1]), + mods: Modifiers::NONE, + }); + } + } + } + None +} + +const REG_ID_DEB: u8 = 0x06; +const REG_ID_FRQ: u8 = 0x07; + +pub async fn configure_keyboard(debounce: u8, poll_freq: u8) { + let mut i2c = PERIPHERAL_BUS.get().lock().await; + let i2c = i2c.as_mut().unwrap(); + + let _ = i2c + .write_read_async(super::MCU_ADDR, [REG_ID_DEB], &mut [debounce]) + .await; + + let _ = i2c + .write_read_async(super::MCU_ADDR, [REG_ID_FRQ], &mut [poll_freq]) + .await; +} diff --git a/src/peripherals/mod.rs b/kernel/src/peripherals/mod.rs similarity index 80% rename from src/peripherals/mod.rs rename to kernel/src/peripherals/mod.rs index b3e7d1a..25a436e 100644 --- a/src/peripherals/mod.rs +++ b/kernel/src/peripherals/mod.rs @@ -6,42 +6,36 @@ use embassy_rp::{ peripherals::I2C1, }; use embassy_sync::{ - blocking_mutex::raw::NoopRawMutex, channel::Sender, lazy_lock::LazyLock, mutex::Mutex, + blocking_mutex::raw::CriticalSectionRawMutex, lazy_lock::LazyLock, mutex::Mutex, }; -use embassy_time::{Duration, Timer}; +use embassy_time::Timer; pub mod keyboard; -use keyboard::{KeyCode, KeyEvent, KeyState}; use crate::peripherals::keyboard::{configure_keyboard, read_keyboard_fifo}; const MCU_ADDR: u8 = 0x1F; type I2CBUS = I2c<'static, I2C1, Async>; -pub static PERIPHERAL_BUS: LazyLock>> = +pub static PERIPHERAL_BUS: LazyLock>> = LazyLock::new(|| Mutex::new(None)); const REG_ID_VER: u8 = 0x01; const REG_ID_RST: u8 = 0x08; const REG_ID_INT: u8 = 0x03; -#[embassy_executor::task] -pub async fn peripherals_task( - i2c: I2CBUS, - mut keyboard_channel: Sender<'static, NoopRawMutex, KeyEvent, 10>, -) { +pub async fn conf_peripherals(i2c: I2CBUS) { Timer::after(embassy_time::Duration::from_millis(100)).await; PERIPHERAL_BUS.get().lock().await.replace(i2c); configure_keyboard(200, 100).await; - set_lcd_backlight(255).await; - set_key_backlight(0).await; - loop { - Timer::after(Duration::from_millis(200)).await; - read_keyboard_fifo(&mut keyboard_channel).await; - } + // empty keys + while read_keyboard_fifo().await.is_some() {} + + // set_lcd_backlight(255).await; + set_key_backlight(0).await; } /// return major & minor mcu version diff --git a/src/psram.rs b/kernel/src/psram.rs similarity index 100% rename from src/psram.rs rename to kernel/src/psram.rs diff --git a/kernel/src/scsi/mod.rs b/kernel/src/scsi/mod.rs new file mode 100644 index 0000000..b61acee --- /dev/null +++ b/kernel/src/scsi/mod.rs @@ -0,0 +1,412 @@ +use embassy_futures::select::select; +use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; +use embassy_sync::lazy_lock::LazyLock; +use embassy_sync::signal::Signal; +use embassy_usb::Builder; +use embassy_usb::driver::{Driver, EndpointIn, EndpointOut}; +use embedded_sdmmc::{Block, BlockIdx}; +use heapless::Vec; + +mod scsi_types; +use scsi_types::*; + +use crate::storage::{SDCARD, SdCard}; + +const BULK_ENDPOINT_PACKET_SIZE: usize = 64; + +pub static MSC_SHUTDOWN: Signal = Signal::new(); + +// number of blocks to read from sd at once +// higher is better, but is larger. Size is BLOCKS * 512 bytes +const BLOCKS: usize = 32; +static mut BLOCK_BUF: LazyLock<[Block; BLOCKS]> = + LazyLock::new(|| core::array::from_fn(|_| Block::new())); + +pub struct MassStorageClass<'d, D: Driver<'d>> { + temp_sd: Option, // temporary owns sdcard when scsi is running + ejected: bool, + pending_eject: bool, + bulk_out: D::EndpointOut, + bulk_in: D::EndpointIn, +} + +impl<'d, 's, D: Driver<'d>> MassStorageClass<'d, D> { + pub fn new(builder: &mut Builder<'d, D>, temp_sd: Option) -> Self { + let mut function = builder.function(0x08, SUBCLASS_SCSI, 0x50); // Mass Storage class + let mut interface = function.interface(); + let mut alt = interface.alt_setting(0x08, SUBCLASS_SCSI, 0x50, None); + + let bulk_out = alt.endpoint_bulk_out(None, BULK_ENDPOINT_PACKET_SIZE as u16); + let bulk_in = alt.endpoint_bulk_in(None, BULK_ENDPOINT_PACKET_SIZE as u16); + + Self { + temp_sd, + pending_eject: false, + ejected: false, + bulk_out, + bulk_in, + } + } + + pub async fn poll(&mut self) { + loop { + if !self.ejected { + select(self.handle_cbw(), MSC_SHUTDOWN.wait()).await; + + if MSC_SHUTDOWN.signaled() { + defmt::info!("MSC shutting down"); + + if self.temp_sd.is_some() { + let mut guard = SDCARD.get().lock().await; + guard.replace(self.temp_sd.take().unwrap()).unwrap(); + } + + self.ejected = true; + + return; + } + } + } + } + + async fn handle_cbw(&mut self) { + let mut cbw_buf = [0u8; 31]; + 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; + if let Some(sd) = guard.take() { + self.temp_sd = Some(sd); + } else { + defmt::warn!("Tried to take SDCARD but it was already taken"); + return; + } + } + + let command = parse_cb(&cbw.CBWCB); + if self.handle_command(command).await.is_ok() { + self.send_csw_success(cbw.dCBWTag).await + } else { + self.send_csw_fail(cbw.dCBWTag).await + } + + 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 = Vec::new(); + + match command { + ScsiCommand::Unknown => Err(()), + ScsiCommand::Inquiry { + evpd, + page_code, + alloc_len, + } => { + if !evpd { + response.push(0x00).map_err(|_| ())?; // Direct-access block device + response.push(0x80).map_err(|_| ())?; // Removable + response.push(0x05).map_err(|_| ())?; // SPC-3 compliance + response.push(0x02).map_err(|_| ())?; // Response data format + response.push(0x00).map_err(|_| ())?; // Additional length - edited later + response.push(0x00).map_err(|_| ())?; // FLAGS + response.push(0x00).map_err(|_| ())?; // FLAGS + response.push(0).map_err(|_| ())?; // FLAGS + assert!(response.len() == 8); + + let vendor = b"LEGTCMPR"; + assert!(vendor.len() == 8); + response.extend_from_slice(vendor)?; + + let product = b"Pico Calc Sdcard"; + assert!(product.len() == 16); + response.extend_from_slice(product)?; + + let version = b"1.00"; + assert!(version.len() == 4); + response.extend_from_slice(version)?; // 4-byte firmware version + + let addl_len = response.len() - 5; + response[4] = addl_len as u8; + assert!(response.len() == 36); + } else { + match page_code { + 0x00 => { + response + .extend_from_slice(&[ + 0x00, // Peripheral Qualifier + Peripheral Device Type (0x00 = Direct-access block device) + 0x00, // Page Code (same as requested: 0x00) + 0x00, 0x03, // Page Length: 3 bytes follow + 0x00, // Supported VPD Page: 0x00 (this one — the "Supported VPD Pages" page itself) + 0x80, // Supported VPD Page: 0x80 (Unit Serial Number) + 0x83, // Supported VPD Page: 0x83 (Device Identification) + ]) + .map_err(|_| ())? + } + 0x80 => { + let serial = b"Pico Calc"; + response.extend_from_slice(&[ + 0x00, // Peripheral Qualifier & Device Type + 0x80, // Page Code = 0x80 (Unit Serial Number) + 0x00, // Reserved + serial.len() as u8, + ])?; + response.extend_from_slice(serial)?; + } + 0x83 => { + let id = b"SdCard"; + response.extend_from_slice(&[ + 0x00, + 0x83, // Page code + 0x00, + (4 + id.len()) as u8, // Length + 0x02, // ASCII identifier + 0x01, // Identifier type + 0x00, // Reserved + id.len() as u8, + ])?; + response.extend_from_slice(id)?; + } + _ => (), + } + }; + + let len = core::cmp::min(alloc_len as usize, response.len()); + self.bulk_in.write(&response[..len]).await.map_err(|_| ()) + } + ScsiCommand::TestUnitReady => { + if self.temp_sd.as_ref().unwrap().is_attached() { + Ok(()) + } else { + Err(()) + } + } + ScsiCommand::RequestSense { + desc: _, + alloc_len: _, + } => Ok(()), + ScsiCommand::ModeSense6 { + dbd: _, + page_control: _, + page_code: _, + subpage_code: _, + alloc_len, + } => { + // DBD=0, no block descriptors; total length = 4 + let response = [ + 0x03, // Mode data length (excluding this byte): 3 + 0x00, // Medium type + 0x00, // Device-specific parameter + 0x00, // Block descriptor length = 0 (DBD = 1) + ]; + + let len = alloc_len.min(response.len() as u8) as usize; + + self.bulk_in.write(&response[..len]).await.map_err(|_| ()) + } + ScsiCommand::ModeSense10 { + dbd: _, + page_control: _, + page_code: _, + subpage_code: _, + alloc_len, + } => { + let response = [ + 0x00, 0x06, // Mode data length = 6 + 0x00, // Medium type + 0x00, // Device-specific parameter + 0x00, 0x00, // Reserved + 0x00, 0x00, // Block descriptor length = 0 + ]; + + let len = alloc_len.min(response.len() as u16) as usize; + + self.bulk_in.write(&response[..len]).await.map_err(|_| ()) + } + ScsiCommand::ReadCapacity10 => { + 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.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())?; + + self.bulk_in.write(&response).await.map_err(|_| ()) + } + ScsiCommand::ReadCapacity16 { alloc_len } => { + 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.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 + response.extend_from_slice(&[0u8; 20])?; // 20 reserved bytes zeroed + + let len = alloc_len.min(response.len() as u32) as usize; + self.bulk_in.write(&response[..len]).await.map_err(|_| ()) + } + ScsiCommand::Read { lba, len } => { + let sdcard = self.temp_sd.as_ref().unwrap(); + let block_buf = unsafe { &mut *BLOCK_BUF.get_mut() }; + let mut blocks = len; + let mut idx = lba; + + while blocks > 0 { + if blocks >= block_buf.len() as u64 { + 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.into()) { + self.bulk_in.write(chunk).await.map_err(|_| ())?; + } + } + + blocks -= block_buf.len() as u64; + idx += block_buf.len() as u64; + } else { + sdcard + .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.into()) { + self.bulk_in.write(chunk).await.map_err(|_| ())?; + } + } + + idx += blocks; + blocks = 0; + } + } + Ok(()) + } + ScsiCommand::Write { lba, len } => { + let sdcard = self.temp_sd.as_ref().unwrap(); + let block_buf = unsafe { &mut *BLOCK_BUF.get_mut() }; + let mut blocks = len; + let mut idx = lba; + + 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.into()) + { + self.bulk_out.read(chunk).await.map_err(|_| ())?; + } + } + + sdcard.read_blocks(block_buf, BlockIdx(idx as u32))?; + + blocks -= block_buf.len() as u64; + 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.into()) + { + self.bulk_out.read(chunk).await.map_err(|_| ())?; + } + } + + sdcard.write_blocks( + &mut block_buf[..blocks as usize], + BlockIdx(idx as u32), + )?; + + idx += blocks; + blocks = 0; + } + } + Ok(()) + } + ScsiCommand::ReadFormatCapacities { alloc_len } => { + let block_size = SdCard::BLOCK_SIZE as u32; + let num_blocks = (self.temp_sd.as_ref().unwrap().size() / block_size as u64) as u32; + + let mut response = [0u8; 12]; + + // Capacity List Length (8 bytes follows) + response[3] = 8; + + // Descriptor + response[4..8].copy_from_slice(&num_blocks.to_be_bytes()); + response[8] = 0x03; // formatted media + response[9..12].copy_from_slice(&block_size.to_be_bytes()[1..4]); // only 3 bytes + + let response_len = alloc_len.min(response.len() as u16) as usize; + self.bulk_in + .write(&response[..response_len]) + .await + .map_err(|_| ()) + } + ScsiCommand::PreventAllowMediumRemoval { prevent: _prevent } => Ok(()), + ScsiCommand::StartStopUnit { start, load_eject } => { + if !start && load_eject { + self.pending_eject = true; + } + Ok(()) + } + } + } + + pub async fn send_csw_success(&mut self, tag: u32) { + self.send_csw(tag, 0x00, 0).await; + } + + pub async fn send_csw_fail(&mut self, tag: u32) { + defmt::error!("Command Failed: {}", tag); + self.send_csw(tag, 0x01, 0).await; // 0x01 = Command Failed + } + + pub async fn send_csw(&mut self, tag: u32, status: u8, residue: u32) { + let mut csw = [0u8; 13]; + csw[0..4].copy_from_slice(&0x53425355u32.to_le_bytes()); // Signature "USBS" + csw[4..8].copy_from_slice(&tag.to_le_bytes()); + csw[8..12].copy_from_slice(&residue.to_le_bytes()); + csw[12] = status; + let _ = self.bulk_in.write(&csw).await; + } +} + +#[repr(C, packed)] +#[allow(non_snake_case)] +struct CommandBlockWrapper { + dCBWSignature: u32, + dCBWTag: u32, + dCBWDataTransferLength: u32, + bmCBWFlags: u8, + bCBWLUN: u8, + bCBWCBLength: u8, + CBWCB: [u8; 16], +} + +#[allow(non_snake_case)] +impl CommandBlockWrapper { + fn parse(buf: &[u8]) -> Option { + if buf.len() < 31 { + return None; + } + let dCBWSignature = u32::from_le_bytes(buf[0..4].try_into().ok()?); + if dCBWSignature != 0x43425355 { + return None; // invalid signature + } + Some(Self { + dCBWSignature, + dCBWTag: u32::from_le_bytes(buf[4..8].try_into().ok()?), + dCBWDataTransferLength: u32::from_le_bytes(buf[8..12].try_into().ok()?), + bmCBWFlags: buf[12], + bCBWLUN: buf[13], + bCBWCBLength: buf[14], + CBWCB: buf[15..31].try_into().ok()?, + }) + } +} diff --git a/kernel/src/scsi/scsi_types.rs b/kernel/src/scsi/scsi_types.rs new file mode 100644 index 0000000..76bbb8a --- /dev/null +++ b/kernel/src/scsi/scsi_types.rs @@ -0,0 +1,159 @@ +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 + +/* SCSI codes */ + +/* SPC */ +const TEST_UNIT_READY: u8 = 0x00; +const REQUEST_SENSE: u8 = 0x03; +const INQUIRY: u8 = 0x12; +const MODE_SENSE_6: u8 = 0x1A; +const MODE_SENSE_10: u8 = 0x5A; + +/* SBC */ +const READ_10: u8 = 0x28; +const READ_16: u8 = 0x88; +const READ_CAPACITY_10: u8 = 0x25; +const READ_CAPACITY_16: u8 = 0x9E; +const WRITE_10: u8 = 0x2A; + +/* MMC */ +const READ_FORMAT_CAPACITIES: u8 = 0x23; + +const PREVENT_ALLOW_MEDIUM_REMOVAL: u8 = 0x1E; +const START_STOP_UNIT: u8 = 0x1B; + +/// SCSI command +/// +/// Refer to specifications (SPC,SAM,SBC,MMC,etc.) +#[derive(Copy, Clone, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +pub enum ScsiCommand { + Unknown, + + /* SPC */ + Inquiry { + evpd: bool, + page_code: u8, + alloc_len: u16, + }, + TestUnitReady, + RequestSense { + desc: bool, + alloc_len: u8, + }, + ModeSense6 { + dbd: bool, + page_control: PageControl, + page_code: u8, + subpage_code: u8, + alloc_len: u8, + }, + ModeSense10 { + dbd: bool, + page_control: PageControl, + page_code: u8, + subpage_code: u8, + alloc_len: u16, + }, + + /* SBC */ + ReadCapacity10, + ReadCapacity16 { + alloc_len: u32, + }, + Read { + lba: u64, + len: u64, + }, + Write { + lba: u64, + len: u64, + }, + + /* MMC */ + ReadFormatCapacities { + alloc_len: u16, + }, + + PreventAllowMediumRemoval { + prevent: bool, + }, + + StartStopUnit { + start: bool, + load_eject: bool, + }, +} + +#[repr(u8)] +#[derive(Copy, Clone, Debug, TryFromPrimitive)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum PageControl { + CurrentValues = 0b00, + ChangeableValues = 0b01, + DefaultValues = 0b10, + SavedValues = 0b11, +} + +#[allow(dead_code)] +pub fn parse_cb(cb: &[u8]) -> ScsiCommand { + match cb[0] { + TEST_UNIT_READY => ScsiCommand::TestUnitReady, + INQUIRY => ScsiCommand::Inquiry { + evpd: (cb[1] & 0b00000001) != 0, + page_code: cb[2], + alloc_len: u16::from_be_bytes([cb[3], cb[4]]), + }, + REQUEST_SENSE => ScsiCommand::RequestSense { + desc: (cb[1] & 0b00000001) != 0, + alloc_len: cb[4], + }, + READ_CAPACITY_10 => ScsiCommand::ReadCapacity10, + READ_CAPACITY_16 => ScsiCommand::ReadCapacity16 { + alloc_len: u32::from_be_bytes([cb[10], cb[11], cb[12], cb[13]]), + }, + READ_10 => ScsiCommand::Read { + lba: u32::from_be_bytes([cb[2], cb[3], cb[4], cb[5]]) as u64, + len: u16::from_be_bytes([cb[7], cb[8]]) as u64, + }, + READ_16 => ScsiCommand::Read { + lba: u64::from_be_bytes((&cb[2..10]).try_into().unwrap()), + len: u32::from_be_bytes((&cb[10..14]).try_into().unwrap()) as u64, + }, + WRITE_10 => ScsiCommand::Write { + lba: u32::from_be_bytes([cb[2], cb[3], cb[4], cb[5]]) as u64, + len: u16::from_be_bytes([cb[7], cb[8]]) as u64, + }, + MODE_SENSE_6 => ScsiCommand::ModeSense6 { + dbd: (cb[1] & 0b00001000) != 0, + page_control: PageControl::try_from_primitive(cb[2] >> 6).unwrap(), + page_code: cb[2] & 0b00111111, + subpage_code: cb[3], + alloc_len: cb[4], + }, + MODE_SENSE_10 => ScsiCommand::ModeSense10 { + dbd: (cb[1] & 0b00001000) != 0, + page_control: PageControl::try_from_primitive(cb[2] >> 6).unwrap(), + page_code: cb[2] & 0b00111111, + subpage_code: cb[3], + alloc_len: u16::from_be_bytes([cb[7], cb[8]]), + }, + READ_FORMAT_CAPACITIES => ScsiCommand::ReadFormatCapacities { + alloc_len: u16::from_be_bytes([cb[7], cb[8]]), + }, + PREVENT_ALLOW_MEDIUM_REMOVAL => ScsiCommand::PreventAllowMediumRemoval { + prevent: (cb[1] & 0b00000001) != 0, + }, + START_STOP_UNIT => ScsiCommand::StartStopUnit { + start: (cb[4] & 0b00000001) != 0, + load_eject: (cb[4] & 0b00000010) != 0, + }, + _ => ScsiCommand::Unknown, + } +} diff --git a/kernel/src/storage.rs b/kernel/src/storage.rs new file mode 100644 index 0000000..15093d8 --- /dev/null +++ b/kernel/src/storage.rs @@ -0,0 +1,161 @@ +use alloc::{string::String, vec::Vec}; +use core::str::FromStr; +use embassy_rp::gpio::{Input, Output}; +use embassy_rp::peripherals::SPI0; +use embassy_rp::spi::{Blocking, Spi}; +use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; +use embassy_sync::lazy_lock::LazyLock; +use embassy_sync::mutex::Mutex; +use embassy_time::Delay; +use embedded_hal_bus::spi::ExclusiveDevice; +use embedded_sdmmc::{ + Block, BlockCount, BlockDevice, BlockIdx, Directory, SdCard as SdmmcSdCard, TimeSource, + Timestamp, Volume, VolumeIdx, VolumeManager, sdcard::Error, +}; +use embedded_sdmmc::{File as SdFile, LfnBuffer, Mode, ShortFileName}; + +pub const MAX_DIRS: usize = 4; +pub const MAX_FILES: usize = 5; +pub const MAX_VOLUMES: usize = 1; + +type Device = ExclusiveDevice, Output<'static>, embassy_time::Delay>; +type SD = SdmmcSdCard; +type VolMgr = VolumeManager; +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>; + +pub static SDCARD: LazyLock>> = + LazyLock::new(|| Mutex::new(None)); + +pub struct DummyTimeSource {} +impl TimeSource for DummyTimeSource { + fn get_timestamp(&self) -> Timestamp { + Timestamp::from_calendar(2022, 1, 1, 0, 0, 0).unwrap() + } +} + +#[derive(Clone, PartialEq)] +pub struct FileName { + pub long_name: String, + pub short_name: ShortFileName, +} + +pub struct SdCard { + det: Input<'static>, + volume_mgr: VolMgr, +} + +impl SdCard { + pub const BLOCK_SIZE: u16 = 512; + + pub fn new(sdcard: SD, det: Input<'static>) -> Self { + let volume_mgr = VolumeManager::<_, _, MAX_DIRS, MAX_FILES, MAX_VOLUMES>::new_with_limits( + sdcard, + DummyTimeSource {}, + 5000, + ); + Self { + det: det, + volume_mgr, + } + } + + /// Returns true if an SD card is inserted. + /// The DET pin is active-low via mechanical switch in the socket. + pub fn is_attached(&self) -> bool { + self.det.is_low() + } + + pub fn size(&self) -> u64 { + let mut result = 0; + + self.volume_mgr.device(|sd| { + result = sd.num_bytes().unwrap_or(0); + DummyTimeSource {} + }); + + 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| { + res = sd.read(blocks, start_block_idx); + DummyTimeSource {} + }); + res.map_err(|_| ()) + } + + pub fn write_blocks(&self, blocks: &mut [Block], start_block_idx: BlockIdx) -> Result<(), ()> { + let mut res: Result<(), Error> = Ok(()); + self.volume_mgr.device(|sd| { + res = sd.write(blocks, start_block_idx); + DummyTimeSource {} + }); + res.map_err(|_| ()) + } + + pub fn access_root_dir(&mut self, mut access: impl FnMut(Dir)) { + let volume0 = self.volume_mgr.open_volume(VolumeIdx(0)).unwrap(); + let root_dir = volume0.open_root_dir().unwrap(); + + access(root_dir); + } + + pub async fn read_file( + &mut self, + name: &ShortFileName, + mut access: impl FnMut(File) -> T, + ) -> Result { + let mut res = Err(()); + self.access_root_dir(|root_dir| { + if let Ok(file) = root_dir.open_file_in_dir(name, Mode::ReadOnly) { + res = Ok(access(file)); + } + }); + + res + } + + /// Returns a Vec of file names (long format) that match the given extension (e.g., "BIN") + pub fn list_files_by_extension(&mut self, ext: &str) -> Result, ()> { + let mut result = Vec::new(); + + // Only proceed if card is inserted + if !self.is_attached() { + return Ok(result); + } + + let mut lfn_storage = [0; 50]; + let mut lfn_buffer = LfnBuffer::new(&mut lfn_storage); + + self.access_root_dir(|dir| { + dir.iterate_dir_lfn(&mut lfn_buffer, |entry, name| { + if let Some(name) = name { + let name = String::from_str(name).unwrap(); + if name.contains(ext) { + result.push(FileName { + long_name: name, + short_name: entry.name.clone(), + }); + } + } + }) + .unwrap() + }); + + Ok(result) + } +} diff --git a/kernel/src/ui.rs b/kernel/src/ui.rs new file mode 100644 index 0000000..1c91c40 --- /dev/null +++ b/kernel/src/ui.rs @@ -0,0 +1,188 @@ +use crate::{ + BINARY_CH, + display::{FB_PAUSED, FRAMEBUFFER}, + elf::load_binary, + 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}; +use embedded_graphics::{ + Drawable, + mono_font::{MonoTextStyle, ascii::FONT_10X20}, + pixelcolor::Rgb565, + prelude::{Dimensions, Point, Primitive, RgbColor, Size}, + primitives::{PrimitiveStyle, Rectangle}, + text::Text, +}; +use embedded_layout::{ + align::{horizontal, vertical}, + layout::linear::{FixedMargin, LinearLayout}, + prelude::*, +}; +use embedded_text::TextBox; + +pub static SELECTIONS: Mutex = + Mutex::new(SelectionList::new()); + +pub async fn ui_handler() { + loop { + 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; + selections.up(); + } + KeyCode::Down => { + let mut selections = SELECTIONS.lock().await; + selections.down(); + } + KeyCode::Enter | KeyCode::Right => { + let selections = SELECTIONS.lock().await; + let selection = + selections.selections[selections.current_selection as usize].clone(); + + let entry = unsafe { + load_binary(&selection.short_name) + .await + .expect("unable to load binary") + }; + BINARY_CH.send(entry).await; + } + _ => (), + } + } + } + + let changed = SELECTIONS.lock().await.changed; + if changed { + clear_selection().await; + draw_selection().await; + } + } +} + +pub async fn clear_selection() { + let sel = SELECTIONS.lock().await; + + if let Some(area) = sel.last_bounds { + Rectangle::new(area.top_left, area.size) + .into_styled(PrimitiveStyle::with_fill(Rgb565::BLACK)) + .draw(unsafe { &mut FRAMEBUFFER }) + .unwrap(); + } +} + +async fn draw_selection() { + let mut guard = SELECTIONS.lock().await; + let file_names = &guard.selections.clone(); + + let text_style = MonoTextStyle::new(&FONT_10X20, Rgb565::WHITE); + let display_area = unsafe { FRAMEBUFFER.bounding_box() }; + + const NO_BINS: &str = "No Programs found on SD Card. Ensure programs end with '.bin', and are located in the root directory"; + let no_bins = String::from_str(NO_BINS).unwrap(); + + FB_PAUSED.store(true, Ordering::Release); // ensure all elements show up at once + + if file_names.is_empty() { + TextBox::new( + &no_bins, + Rectangle::new( + Point::new(25, 25), + Size::new(display_area.size.width - 50, display_area.size.width - 50), + ), + text_style, + ) + .draw(unsafe { &mut FRAMEBUFFER }) + .unwrap(); + } else { + let mut views: alloc::vec::Vec>> = Vec::new(); + + for i in file_names { + views.push(Text::new(&i.long_name, 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); + + // draw selected box + let selected_bounds = layout + .inner() + .get(guard.current_selection as usize) + .expect("Selected binary missing") + .bounding_box(); + Rectangle::new(selected_bounds.top_left, selected_bounds.size) + .into_styled(PrimitiveStyle::with_stroke(Rgb565::WHITE, 1)) + .draw(unsafe { &mut FRAMEBUFFER }) + .unwrap(); + + guard.last_bounds = Some(layout.bounds()); + + layout.draw(unsafe { &mut FRAMEBUFFER }).unwrap(); + } + + guard.changed = false; + FB_PAUSED.store(false, Ordering::Release); // ensure all elements show up at once +} + +#[derive(Clone)] +pub struct SelectionList { + // allows easy clearing of selection ui, + // based on previous bounds + last_bounds: Option, + current_selection: u16, + selections: Vec, + changed: bool, +} + +impl SelectionList { + pub const fn new() -> Self { + Self { + last_bounds: None, + selections: Vec::new(), + current_selection: 0, + changed: false, + } + } + + pub fn set_changed(&mut self, changed: bool) { + self.changed = changed + } + + pub fn update_selections(&mut self, selections: Vec) { + self.selections = selections; + self.changed = true; + } + + pub fn selections(&self) -> &Vec { + &self.selections + } + + pub fn reset(&mut self) { + self.current_selection = 0; + self.changed = true; + } + + fn up(&mut self) { + if self.current_selection > 0 { + self.current_selection -= 1; + self.changed = true; + } + } + + fn down(&mut self) { + if self.current_selection + 1 < self.selections.len() as u16 { + self.current_selection += 1; + self.changed = true; + } + } +} diff --git a/kernel/src/usb.rs b/kernel/src/usb.rs new file mode 100644 index 0000000..65c4fb0 --- /dev/null +++ b/kernel/src/usb.rs @@ -0,0 +1,45 @@ +use crate::{scsi::MassStorageClass, storage::SdCard}; +use core::sync::atomic::{AtomicBool, Ordering}; +use embassy_futures::select::select; +use embassy_rp::{peripherals::USB, usb::Driver}; +use embassy_usb::{Builder, Config, UsbDevice}; + +pub static USB_ACTIVE: AtomicBool = AtomicBool::new(false); + +#[embassy_executor::task] +pub async fn usb_handler(driver: Driver<'static, USB>) { + let mut config = Config::new(0xc0de, 0xbabe); + config.manufacturer = Some("LegitCamper"); + config.product = Some("PicoCalc"); + config.serial_number = Some("01001100"); + config.max_power = 100; + config.max_packet_size_0 = 64; + + let mut config_descriptor = [0; 256]; + let mut bos_descriptor = [0; 64]; + let mut control_buf = [0; 64]; + + let mut builder = Builder::new( + driver, + config, + &mut config_descriptor, + &mut bos_descriptor, + &mut [], + &mut control_buf, + ); + + let temp_sd: Option = None; + let mut scsi = MassStorageClass::new(&mut builder, temp_sd); + let usb = builder.build(); + + select(run(usb), scsi.poll()).await; +} + +async fn run<'d>(mut usb: UsbDevice<'d, Driver<'d, USB>>) -> ! { + loop { + usb.wait_resume().await; + USB_ACTIVE.store(true, Ordering::Release); + usb.run_until_suspend().await; + USB_ACTIVE.store(false, Ordering::Release); + } +} diff --git a/kernel/src/utils.rs b/kernel/src/utils.rs new file mode 100644 index 0000000..62683c6 --- /dev/null +++ b/kernel/src/utils.rs @@ -0,0 +1,11 @@ +#[macro_export] +macro_rules! format { + ($len:literal, $($arg:tt)*) => {{ + use heapless::String; + use core::fmt::Write; + + let mut s: String<$len> = String::new(); + let _ = write!(&mut s, $($arg)*); + s + }} +} diff --git a/src/display.rs b/src/display.rs deleted file mode 100644 index fe3f2b0..0000000 --- a/src/display.rs +++ /dev/null @@ -1,113 +0,0 @@ -use embassy_rp::{ - gpio::{Level, Output}, - peripherals::{PIN_13, PIN_14, PIN_15, SPI1}, - spi::{Blocking, Spi}, -}; -use embassy_time::{Delay, Timer}; -use embedded_graphics::{ - Drawable, Pixel, - pixelcolor::{Rgb565, raw::RawU16}, - prelude::{DrawTarget, OriginDimensions, Point, Primitive, RawData, RgbColor, Size}, - primitives::{PrimitiveStyle, Rectangle}, -}; -use embedded_hal_bus::spi::ExclusiveDevice; -use st7365p_lcd::{Orientation, ST7365P}; - -#[embassy_executor::task] -pub async fn display_task( - spi: Spi<'static, SPI1, Blocking>, - cs: PIN_13, - data: PIN_14, - reset: PIN_15, -) { - let spi_device = ExclusiveDevice::new(spi, Output::new(cs, Level::Low), Delay).unwrap(); - let mut display = ST7365P::new( - spi_device, - Output::new(data, Level::Low), - Some(Output::new(reset, Level::High)), - true, - false, - 320, - 320, - ); - display.init(&mut Delay).unwrap(); - display.set_orientation(&Orientation::Landscape).unwrap(); - let mut virtual_display = VirtualDisplay::new(display, 320 / 2, 320 / 2); - - let thin_stroke = PrimitiveStyle::with_stroke(Rgb565::RED, 20); - - Rectangle::new(Point::new(10, 10), Size::new(100, 100)) - .into_styled(thin_stroke) - .draw(&mut virtual_display) - .unwrap(); - - loop { - Timer::after_millis(500).await; - } -} - -/// simple abstraction over real display & resolution to reduce frame buffer size -/// by cutting the resolution by 1/4 -struct VirtualDisplay { - display: ST7365P< - ExclusiveDevice, Output<'static>, Delay>, - Output<'static>, - Output<'static>, - >, - width: u32, - height: u32, -} - -impl VirtualDisplay { - pub fn new( - display: ST7365P< - ExclusiveDevice, Output<'static>, Delay>, - Output<'static>, - Output<'static>, - >, - new_width: u32, - new_height: u32, - ) -> Self { - Self { - display, - width: new_width, - height: new_height, - } - } -} - -impl DrawTarget for VirtualDisplay { - type Color = Rgb565; - type Error = (); - - fn draw_iter(&mut self, pixels: I) -> Result<(), Self::Error> - where - I: IntoIterator>, - { - for Pixel(coord, color) in pixels.into_iter() { - // Check bounds on the *virtual* (already reduced) resolution - if coord.x >= 0 - && coord.y >= 0 - && coord.x < self.width as i32 - && coord.y < self.height as i32 - { - let px = coord.x as u16 * 2; - let py = coord.y as u16 * 2; - let raw_color = RawU16::from(color).into_inner(); - - // Draw the 2x2 block on the underlying hardware - self.display.set_pixel(px, py, raw_color)?; - self.display.set_pixel(px + 1, py, raw_color)?; - self.display.set_pixel(px, py + 1, raw_color)?; - self.display.set_pixel(px + 1, py + 1, raw_color)?; - } - } - Ok(()) - } -} - -impl OriginDimensions for VirtualDisplay { - fn size(&self) -> Size { - Size::new(self.width, self.height) - } -} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 921dc45..0000000 --- a/src/main.rs +++ /dev/null @@ -1,66 +0,0 @@ -#![feature(impl_trait_in_assoc_type)] -#![no_std] -#![no_main] - -#[cfg(feature = "defmt")] -use defmt::*; -use {defmt_rtt as _, panic_probe as _}; - -extern crate alloc; - -use embassy_executor::Spawner; -use embassy_rp::peripherals::{I2C1, PIO1}; -use embassy_rp::{ - bind_interrupts, - gpio::{Level, Output}, - i2c, - i2c::I2c, - spi, -}; -use embassy_rp::{pio, spi::Spi}; -use embassy_sync::blocking_mutex::raw::NoopRawMutex; -use embassy_sync::channel::Channel; -use embassy_time::Timer; -use embedded_hal_bus::spi::ExclusiveDevice; -use embedded_sdmmc::asynchronous::{File, SdCard, ShortFileName, VolumeIdx, VolumeManager}; -use static_cell::StaticCell; - -mod peripherals; -use peripherals::{keyboard::KeyEvent, peripherals_task}; -mod display; -use display::display_task; -mod heap; -use heap::{HEAP, init_heap}; -mod psram; - -embassy_rp::bind_interrupts!(struct Irqs { - I2C1_IRQ => i2c::InterruptHandler; - PIO1_IRQ_0 => pio::InterruptHandler; -}); - -const MAX_SPI_FREQ: u32 = 62_500_000; - -#[embassy_executor::main] -async fn main(spawner: Spawner) { - let p = embassy_rp::init(Default::default()); - init_heap(); - - static KEYBOARD_EVENTS: StaticCell> = StaticCell::new(); - let keyboard_events = KEYBOARD_EVENTS.init(Channel::new()); - - // configure keyboard event handler - let mut config = i2c::Config::default(); - config.frequency = 100_000; - let i2c1 = I2c::new_async(p.I2C1, p.PIN_7, p.PIN_6, Irqs, config); - spawner - .spawn(peripherals_task(i2c1, keyboard_events.sender())) - .unwrap(); - - // configure display handler - let mut config = spi::Config::default(); - config.frequency = MAX_SPI_FREQ; - let spi1 = spi::Spi::new_blocking(p.SPI1, p.PIN_10, p.PIN_11, p.PIN_12, config); - spawner - .spawn(display_task(spi1, p.PIN_13, p.PIN_14, p.PIN_15)) - .unwrap(); -} diff --git a/src/peripherals/keyboard.rs b/src/peripherals/keyboard.rs deleted file mode 100644 index eaa07c7..0000000 --- a/src/peripherals/keyboard.rs +++ /dev/null @@ -1,207 +0,0 @@ -use defmt::{info, warn}; -use embassy_rp::{ - i2c::{Async, I2c}, - peripherals::I2C1, -}; -use embassy_sync::{ - blocking_mutex::raw::{CriticalSectionRawMutex, NoopRawMutex}, - channel::Sender, -}; - -use crate::peripherals::PERIPHERAL_BUS; - -const REG_ID_KEY: u8 = 0x04; -const REG_ID_FIF: u8 = 0x09; - -const KEY_CAPSLOCK: u8 = 1 << 5; -const KEY_NUMLOCK: u8 = 1 << 6; -const KEY_COUNT_MASK: u8 = 0x1F; // 0x1F == 31 - -pub async fn read_keyboard_fifo(channel: &mut Sender<'static, NoopRawMutex, KeyEvent, 10>) { - let mut i2c = PERIPHERAL_BUS.get().lock().await; - let i2c = i2c.as_mut().unwrap(); - - let mut key_status = [0_u8; 1]; - - if i2c - .write_read_async(super::MCU_ADDR, [REG_ID_KEY], &mut key_status) - .await - .is_ok() - { - let _caps = key_status[0] & KEY_CAPSLOCK == KEY_CAPSLOCK; - let _num = key_status[0] & KEY_NUMLOCK == KEY_NUMLOCK; - let fifo_count = key_status[0] & KEY_COUNT_MASK; - - if fifo_count >= 1 { - let mut event = [0_u8; 2]; - - if i2c - .write_read_async(super::MCU_ADDR, [REG_ID_FIF], &mut event) - .await - .is_ok() - { - channel - .try_send(KeyEvent { - state: KeyState::from(event[0]), - key: KeyCode::from(event[1]), - mods: Modifiers::NONE, - }) - .expect("Failed to push key"); - } - } - } -} - -const REG_ID_DEB: u8 = 0x06; -const REG_ID_FRQ: u8 = 0x07; - -pub async fn configure_keyboard(debounce: u8, poll_freq: u8) { - let mut i2c = PERIPHERAL_BUS.get().lock().await; - let i2c = i2c.as_mut().unwrap(); - - let _ = i2c - .write_read_async(super::MCU_ADDR, [REG_ID_DEB], &mut [debounce]) - .await; - - let _ = i2c - .write_read_async(super::MCU_ADDR, [REG_ID_FRQ], &mut [poll_freq]) - .await; -} - -bitflags::bitflags! { - #[derive(Default, Debug, PartialEq, Eq, Clone, Copy)] - pub struct Modifiers: u8 { - const NONE = 0; - const CTRL = 1; - const ALT = 2; - const LSHIFT = 4; - const RSHIFT = 8; - const SYM = 16; - } -} - -#[derive(Debug)] -pub struct KeyEvent { - pub key: KeyCode, - pub state: KeyState, - pub mods: Modifiers, -} - -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum KeyState { - Idle = 0, - Pressed = 1, - Hold = 2, - Released = 3, -} - -impl From for KeyState { - fn from(value: u8) -> Self { - match value { - 1 => KeyState::Pressed, - 2 => KeyState::Hold, - 3 => KeyState::Released, - 0 | _ => KeyState::Idle, - } - } -} - -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[repr(u8)] -pub enum KeyCode { - JoyUp = 0x01, - JoyDown = 0x02, - JoyLeft = 0x03, - JoyRight = 0x04, - JoyCenter = 0x05, - BtnLeft1 = 0x06, - BtnRight1 = 0x07, - BtnLeft2 = 0x11, - BtnRight2 = 0x12, - Backspace = 0x08, - Tab = 0x09, - Enter = 0x0A, - ModAlt = 0xA1, - ModShiftLeft = 0xA2, - ModShiftRight = 0xA3, - ModSym = 0xA4, - ModCtrl = 0xA5, - Esc = 0xB1, - Left = 0xB4, - Up = 0xB5, - Down = 0xB6, - Right = 0xB7, - Break = 0xD0, - Insert = 0xD1, - Home = 0xD2, - Del = 0xD4, - End = 0xD5, - PageUp = 0xD6, - PageDown = 0xD7, - CapsLock = 0xC1, - F1 = 0x81, - F2 = 0x82, - F3 = 0x83, - F4 = 0x84, - F5 = 0x85, - F6 = 0x86, - F7 = 0x87, - F8 = 0x88, - F9 = 0x89, - F10 = 0x90, - Char(char), - Unknown(u8), -} - -impl From for KeyCode { - fn from(value: u8) -> Self { - match value { - 0x01 => Self::JoyUp, - 0x02 => Self::JoyDown, - 0x03 => Self::JoyLeft, - 0x04 => Self::JoyRight, - 0x05 => Self::JoyCenter, - 0x06 => Self::BtnLeft1, - 0x07 => Self::BtnRight1, - 0x08 => Self::Backspace, - 0x09 => Self::Tab, - 0x0A => Self::Enter, - 0x11 => Self::BtnLeft2, - 0x12 => Self::BtnRight2, - 0xA1 => Self::ModAlt, - 0xA2 => Self::ModShiftLeft, - 0xA3 => Self::ModShiftRight, - 0xA4 => Self::ModSym, - 0xA5 => Self::ModCtrl, - 0xB1 => Self::Esc, - 0xB4 => Self::Left, - 0xB5 => Self::Up, - 0xB6 => Self::Down, - 0xB7 => Self::Right, - 0xC1 => Self::CapsLock, - 0xD0 => Self::Break, - 0xD1 => Self::Insert, - 0xD2 => Self::Home, - 0xD4 => Self::Del, - 0xD5 => Self::End, - 0xD6 => Self::PageUp, - 0xD7 => Self::PageDown, - 0x81 => Self::F1, - 0x82 => Self::F2, - 0x83 => Self::F3, - 0x84 => Self::F4, - 0x85 => Self::F5, - 0x86 => Self::F6, - 0x87 => Self::F7, - 0x88 => Self::F8, - 0x89 => Self::F9, - 0x90 => Self::F10, - _ => match char::from_u32(value as u32) { - Some(c) => Self::Char(c), - None => Self::Unknown(value), - }, - } - } -} diff --git a/user-apps/calculator/Cargo.toml b/user-apps/calculator/Cargo.toml new file mode 100644 index 0000000..19115f3 --- /dev/null +++ b/user-apps/calculator/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "calculator" +version = "0.1.0" +edition = "2024" + +[dependencies] +abi = { path = "../../abi" } +embedded-graphics = "0.8.1" +embedded-layout = "0.4.2" diff --git a/user-apps/calculator/build.rs b/user-apps/calculator/build.rs new file mode 100644 index 0000000..332a55b --- /dev/null +++ b/user-apps/calculator/build.rs @@ -0,0 +1,28 @@ +//! 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"); +} diff --git a/user-apps/calculator/src/main.rs b/user-apps/calculator/src/main.rs new file mode 100644 index 0000000..2e0bfec --- /dev/null +++ b/user-apps/calculator/src/main.rs @@ -0,0 +1,207 @@ +#![no_std] +#![no_main] + +extern crate alloc; +use abi::{ + display::{Display, lock_display}, + get_key, + keyboard::{KeyCode, KeyState}, + print, +}; +use alloc::{format, string::String, vec, vec::Vec}; +use core::panic::PanicInfo; +use embedded_graphics::{ + Drawable, + geometry::{Dimensions, Point}, + mono_font::{MonoTextStyle, ascii::FONT_7X14, iso_8859_1::FONT_10X20}, + pixelcolor::Rgb565, + prelude::{Primitive, RgbColor}, + primitives::{PrimitiveStyle, Rectangle}, + text::Text, +}; +use embedded_layout::{ + View, + align::{Align, horizontal, vertical}, + layout::linear::LinearLayout, + prelude::Chain, +}; + +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + print(&format!( + "user panic: {} @ {:?}", + info.message(), + info.location(), + )); + loop {} +} + +#[unsafe(no_mangle)] +pub extern "Rust" fn _start() { + main() +} + +pub fn main() { + print("Starting Calculator app"); + let mut display = Display; + + let mut input = vec!['e', 'x', 'p', 'r', ':', ' ']; + let input_min = input.len(); + let mut dirty = true; + let mut last_area: Option<(Rectangle, Rectangle)> = None; + + LinearLayout::vertical(Chain::new(Text::new( + "Calculator!", + Point::zero(), + MonoTextStyle::new(&FONT_10X20, Rgb565::WHITE), + ))) + .arrange() + .align_to(&display.bounding_box(), horizontal::Center, vertical::Top) + .draw(&mut display) + .expect("Failed to draw title"); + + loop { + if dirty { + lock_display(true); + + let style = PrimitiveStyle::with_fill(Rgb565::BLACK); + if let Some(area) = last_area { + Rectangle::new(area.0.top_left, area.0.size) + .into_styled(style) + .draw(&mut display) + .unwrap(); + + Rectangle::new(area.1.top_left, area.1.size) + .into_styled(style) + .draw(&mut display) + .unwrap(); + } + + let text = input.iter().cloned().collect::(); + + let style = MonoTextStyle::new(&FONT_7X14, Rgb565::WHITE); + let expr_layout = LinearLayout::vertical(Chain::new(Text::new( + &text, + display.bounding_box().center(), + style, + ))) + .arrange() + .align_to(&display.bounding_box(), horizontal::Left, vertical::Center); + + let result = if let Ok(result) = evaluate(&input[input_min..]) { + &format!(" = {}", result) + } else { + " = Error" + }; + + let eq_layout = LinearLayout::vertical(Chain::new(Text::new( + result, + display.bounding_box().center(), + style, + ))) + .arrange() + .align_to(&display.bounding_box(), horizontal::Right, vertical::Center); + + last_area = Some((expr_layout.bounds(), eq_layout.bounds())); + expr_layout.draw(&mut display).unwrap(); + eq_layout.draw(&mut display).unwrap(); + + dirty = false; + lock_display(false); + } + + let event = get_key(); + if event.state != KeyState::Idle { + match event.key { + KeyCode::Char(ch) => { + input.push(ch); + } + KeyCode::Del => { + input.truncate(input_min); + } + KeyCode::Backspace => { + if input.len() > input_min { + input.pop(); + } + } + KeyCode::Esc => return, + _ => (), + } + dirty = true; + } + } +} + +fn get_int(int: &[char]) -> Result { + let mut output: i32 = 0; + for &c in int { + let digit = c.to_digit(10).ok_or(())? as i32; + output = output + .checked_mul(10) + .and_then(|v| v.checked_add(digit)) + .ok_or(())?; + } + Ok(output) +} + +fn primary(input: &[char], pos: &mut usize) -> Result { + let mut digits = Vec::new(); + while *pos < input.len() && input[*pos].is_ascii_digit() { + digits.push(input[*pos]); + *pos += 1; + } + if digits.is_empty() { + return Err(()); + } + get_int(&digits) +} + +fn mul_div(input: &[char], pos: &mut usize) -> Result { + let mut value = primary(input, pos)?; + while *pos < input.len() { + let op = input[*pos]; + if op != '*' && op != '/' { + break; + } + *pos += 1; + let rhs = primary(input, pos)?; + value = match op { + '*' => value.checked_mul(rhs).ok_or(())?, + '/' => { + if rhs == 0 { + return Err(()); + } + value.checked_div(rhs).ok_or(())? + } + _ => unreachable!(), + }; + } + Ok(value) +} + +fn add_sub(input: &[char], pos: &mut usize) -> Result { + let mut value = mul_div(input, pos)?; + while *pos < input.len() { + let op = input[*pos]; + if op != '+' && op != '-' { + break; + } + *pos += 1; + let rhs = mul_div(input, pos)?; + value = match op { + '+' => value.checked_add(rhs).ok_or(())?, + '-' => value.checked_sub(rhs).ok_or(())?, + _ => unreachable!(), + }; + } + Ok(value) +} + +fn evaluate(input: &[char]) -> Result { + let mut pos = 0; + let result = add_sub(input, &mut pos)?; + if pos != input.len() { + return Err(()); + } + Ok(result) +} diff --git a/user-apps/gallery/Cargo.toml b/user-apps/gallery/Cargo.toml new file mode 100644 index 0000000..610805a --- /dev/null +++ b/user-apps/gallery/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "gallery" +version = "0.1.0" +edition = "2024" + +[dependencies] +abi = { path = "../../abi" } +embedded-graphics = "0.8.1" +tinybmp = "0.6.0" diff --git a/user-apps/gallery/build.rs b/user-apps/gallery/build.rs new file mode 100644 index 0000000..332a55b --- /dev/null +++ b/user-apps/gallery/build.rs @@ -0,0 +1,28 @@ +//! 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"); +} diff --git a/user-apps/gallery/src/main.rs b/user-apps/gallery/src/main.rs new file mode 100644 index 0000000..be73636 --- /dev/null +++ b/user-apps/gallery/src/main.rs @@ -0,0 +1,109 @@ +#![no_std] +#![no_main] +#![allow(static_mut_refs)] + +extern crate alloc; +use abi::{ + display::{Display, SCREEN_HEIGHT, SCREEN_WIDTH, lock_display}, + fs::{list_dir, read_file}, + get_key, + keyboard::{KeyCode, KeyState}, + print, +}; +use alloc::{format, string::ToString}; +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(&format!( + "user panic: {} @ {:?}", + info.message(), + info.location(), + )); + loop {} +} + +#[unsafe(no_mangle)] +pub extern "Rust" fn _start() { + main() +} + +pub fn main() { + print("Starting Gallery app"); + static mut BMP_BUF: [u8; 100_000] = [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(&format!("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 unsafe { &mut BMP_BUF[..] }); + if read > 0 { + let bmp = Bmp::from_slice(unsafe { &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 + + lock_display(true); + 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(); + + lock_display(false); + + images_drawn += 1; + } + } + } + } + + loop { + let event = get_key(); + if event.state != KeyState::Idle { + match event.key { + KeyCode::Esc => return, + _ => (), + } + }; + } +} diff --git a/user-apps/memory.x b/user-apps/memory.x new file mode 100644 index 0000000..5517509 --- /dev/null +++ b/user-apps/memory.x @@ -0,0 +1,31 @@ +MEMORY +{ + RAM : ORIGIN = 0x0, LENGTH = 150K +} + +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 +} diff --git a/user-apps/snake/Cargo.toml b/user-apps/snake/Cargo.toml new file mode 100644 index 0000000..5ce4e74 --- /dev/null +++ b/user-apps/snake/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "snake" +version = "0.1.0" +edition = "2024" + +[dependencies] +abi = { path = "../../abi" } +embedded-graphics = "0.8.1" +embedded-snake = { path = "../../../embedded-snake-rs" } +rand = { version = "0.9.0", default-features = false } diff --git a/user-apps/snake/build.rs b/user-apps/snake/build.rs new file mode 100644 index 0000000..332a55b --- /dev/null +++ b/user-apps/snake/build.rs @@ -0,0 +1,28 @@ +//! 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"); +} diff --git a/user-apps/snake/src/main.rs b/user-apps/snake/src/main.rs new file mode 100644 index 0000000..a979b33 --- /dev/null +++ b/user-apps/snake/src/main.rs @@ -0,0 +1,72 @@ +#![no_std] +#![no_main] + +extern crate alloc; +use abi::{ + Rng, + display::{Display, SCREEN_HEIGHT, SCREEN_WIDTH, lock_display}, + get_key, + keyboard::{KeyCode, KeyState}, + print, sleep, +}; +use alloc::format; +use core::panic::PanicInfo; +use embedded_graphics::{pixelcolor::Rgb565, prelude::RgbColor}; +use embedded_snake::{Direction, SnakeGame}; + +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + print(&format!( + "user panic: {} @ {:?}", + info.message(), + info.location(), + )); + loop {} +} + +#[unsafe(no_mangle)] +pub extern "Rust" fn _start() { + main() +} + +const CELL_SIZE: usize = 8; + +pub fn main() { + print("Starting Snake app"); + let mut display = Display; + + let mut game = SnakeGame::<100, Rgb565, Rng>::new( + SCREEN_WIDTH as u16, + SCREEN_HEIGHT as u16, + CELL_SIZE as u16, + CELL_SIZE as u16, + Rng, + Rgb565::BLACK, + Rgb565::GREEN, + Rgb565::RED, + 50, + ); + + loop { + let event = get_key(); + if event.state != KeyState::Idle { + let direction = match event.key { + KeyCode::Up => Direction::Up, + KeyCode::Down => Direction::Down, + KeyCode::Right => Direction::Right, + KeyCode::Left => Direction::Left, + KeyCode::Esc => return, + _ => Direction::None, + }; + game.set_direction(direction); + }; + + // ensure all draws show up at once + lock_display(true); + game.pre_draw(&mut display); + game.draw(&mut display); + lock_display(false); + + sleep(15); + } +}