diff --git a/Cargo.lock b/Cargo.lock index e7776e1..1416664 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,24 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "az" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" + [[package]] name = "bare-metal" version = "0.2.5" @@ -17,6 +35,18 @@ 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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "byteorder" version = "1.5.0" @@ -28,11 +58,28 @@ name = "cmforth" version = "0.1.0" dependencies = [ "cortex-m", - "heapless", + "heapless 0.9.3", "log", "thiserror", ] +[[package]] +name = "cmforth-picocalc" +version = "0.1.0" +dependencies = [ + "cmforth", + "cortex-m", + "cortex-m-rt", + "defmt", + "defmt-rtt", + "embedded-graphics", + "embedded-hal 1.0.0", + "heapless 0.9.3", + "panic-probe", + "rp235x-hal", + "static_cell", +] + [[package]] name = "cmforth-repl" version = "0.1.0" @@ -40,7 +87,7 @@ dependencies = [ "cmforth", "cortex-m", "cortex-m-rt", - "heapless", + "heapless 0.9.3", "log", "static_cell", ] @@ -52,8 +99,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ec610d8f49840a5b376c69663b6369e71f4b34484b9b2eb29fb918d92516cb9" dependencies = [ "bare-metal", - "bitfield", - "embedded-hal", + "bitfield 0.13.2", + "embedded-hal 0.2.7", "volatile-register", ] @@ -77,6 +124,91 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] + +[[package]] +name = "defmt-rtt" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05144944c117db54127a8ac17a3c9a28d55e7047bfddb950bcd20b4a94c79b10" +dependencies = [ + "critical-section", + "defmt", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "embedded-dma" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "994f7e5b5cb23521c22304927195f236813053eb9c065dd2226a32ba64695446" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "embedded-graphics" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e8da660bb0c829b34a56a965490597f82a55e767b91f9543be80ce8ccb416fe" +dependencies = [ + "az", + "byteorder", + "embedded-graphics-core", + "float-cmp", + "micromath", +] + +[[package]] +name = "embedded-graphics-core" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95743bef3ff70fcba3930246c4e6872882bbea0dcc6da2ca860112e0cd4bd09f" +dependencies = [ + "az", + "byteorder", +] + [[package]] name = "embedded-hal" version = "0.2.7" @@ -87,6 +219,106 @@ dependencies = [ "void", ] +[[package]] +name = "embedded-hal" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89" + +[[package]] +name = "embedded-hal-async" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4c685bbef7fe13c3c6dd4da26841ed3980ef33e841cddfa15ce8a8fb3f1884" +dependencies = [ + "embedded-hal 1.0.0", +] + +[[package]] +name = "embedded-hal-nb" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fba4268c14288c828995299e59b12babdbe170f6c6d73731af1b4648142e8605" +dependencies = [ + "embedded-hal 1.0.0", + "nb 1.1.0", +] + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + +[[package]] +name = "frunk" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28aef0f9aa070bce60767c12ba9cb41efeaf1a2bc6427f87b7d83f11239a16d7" +dependencies = [ + "frunk_core 0.4.4", + "frunk_derives", +] + +[[package]] +name = "frunk_core" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "476eeaa382e3462b84da5d6ba3da97b5786823c2d0d3a0d04ef088d073da225c" + +[[package]] +name = "frunk_core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bd3c9ba2e323e8b19e77f15873f60974a7d82f89b80e50c53be44b8b92927c1" + +[[package]] +name = "frunk_derives" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0b4095fc99e1d858e5b8c7125d2638372ec85aa0fe6c807105cf10b0265ca6c" +dependencies = [ + "frunk_proc_macro_helpers", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "frunk_proc_macro_helpers" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b70229a1347a20d4af9c06116cc452acef34f798668c6b69e97dd5c8a88052bd" +dependencies = [ + "frunk_core 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "fugit" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e639847d312d9a82d2e75b0edcc1e934efcc64e6cb7aa94f0b1fbec0bc231d6" +dependencies = [ + "gcd", +] + +[[package]] +name = "gcd" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d758ba1b47b00caf47f24925c0074ecb20d6dfcffe7f6d53395c0465674841a" + [[package]] name = "hash32" version = "0.3.1" @@ -96,6 +328,16 @@ dependencies = [ "byteorder", ] +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + [[package]] name = "heapless" version = "0.9.3" @@ -106,12 +348,27 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "log" version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +[[package]] +name = "micromath" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c8dda44ff03a2f238717214da50f65d5a53b45cd213a7370424ffdb6fae815" + [[package]] name = "nb" version = "0.1.3" @@ -127,6 +384,62 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "panic-probe" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd402d00b0fb94c5aee000029204a46884b1262e0c443f166d86d2c0747e1a1a" +dependencies = [ + "cortex-m", + "defmt", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pio" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76e09694b50f89f302ed531c1f2a7569f0be5867aee4ab4f8f729bbeec0078e3" +dependencies = [ + "arrayvec", + "num_enum", + "paste", +] + [[package]] name = "portable-atomic" version = "1.12.0" @@ -151,6 +464,117 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "riscv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f5c1b8bf41ea746266cdee443d1d1e9125c86ce1447e1a2615abd34330d33a9" +dependencies = [ + "critical-section", + "embedded-hal 1.0.0", +] + +[[package]] +name = "riscv-rt" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0d35e32cf1383183e8885d8a9aa4402a087fd094dc34c2cb6df6687d0229dfe" +dependencies = [ + "riscv", + "riscv-rt-macros", +] + +[[package]] +name = "riscv-rt-macros" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30f19a85fe107b65031e0ba8ec60c34c2494069fe910d6c297f5e7cb5a6f76d0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "rp-binary-info" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f582261945fa215d40e2988b4df595d11c0908c0fff97a0fe23df766d117b790" + +[[package]] +name = "rp-hal-common" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8288358786b1458fb2caac8c4b40fb529ef4200d6c46467e2695b7a8ba573ae8" +dependencies = [ + "fugit", +] + +[[package]] +name = "rp235x-hal" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2939c82776b0b4ae110168b4298b5adf831e6cff249b057bf2a2187453b959c" +dependencies = [ + "bitfield 0.14.0", + "cortex-m", + "cortex-m-rt", + "critical-section", + "embedded-dma", + "embedded-hal 0.2.7", + "embedded-hal 1.0.0", + "embedded-hal-async", + "embedded-hal-nb", + "embedded-io", + "frunk", + "fugit", + "gcd", + "itertools", + "nb 1.1.0", + "paste", + "pio", + "rand_core", + "riscv", + "riscv-rt", + "rp-binary-info", + "rp-hal-common", + "rp235x-hal-macros", + "rp235x-pac", + "sha2-const-stable", + "usb-device", + "vcell", + "void", +] + +[[package]] +name = "rp235x-hal-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74edd7a5979e9763bbb98e9746e711bac7464ee3397af7288e6c288ff0d3c764" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "rp235x-pac" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ffcb6931deee4242886b5a1df62db5e2555b0eb6ae1e8be101f3ea3e58e65c6" +dependencies = [ + "cortex-m", + "cortex-m-rt", + "critical-section", + "vcell", +] + [[package]] name = "rustc_version" version = "0.2.3" @@ -175,6 +599,12 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" +[[package]] +name = "sha2-const-stable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f179d4e11094a893b82fff208f74d448a7512f99f5a0acbd5c679b705f83ed9" + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -190,6 +620,17 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.111" @@ -238,6 +679,16 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "usb-device" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98816b1accafbb09085168b90f27e93d790b4bfa19d883466b5e53315b5f06a6" +dependencies = [ + "heapless 0.8.0", + "portable-atomic", +] + [[package]] name = "vcell" version = "0.1.3" diff --git a/Cargo.toml b/Cargo.toml index f22714f..9565064 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,12 @@ heapless = "0.9.2" log = "0.4.29" cortex-m-rt = "0.7.5" static_cell = "2.1.1" +embedded-hal = "1.0.0" +embedded-graphics = "0.8.1" +defmt = "1" +defmt-rtt = "1" +panic-probe = "1" +rp235x-hal = "0.3" [profile.dev] debug = 1 diff --git a/flake.nix b/flake.nix index 29ec0e6..0ab4f9e 100644 --- a/flake.nix +++ b/flake.nix @@ -9,6 +9,7 @@ systems = [ "x86_64-linux" "aarch64-linux" + "aarch64-darwin" ]; forAllSystems = nixpkgs.lib.genAttrs systems; @@ -23,6 +24,7 @@ pkgs.probe-rs pkgs.flip-link pkgs.gcc-arm-embedded-14 + pkgs.picotool ]; }; diff --git a/platforms/cmforth-picocalc/.cargo/config.toml b/platforms/cmforth-picocalc/.cargo/config.toml new file mode 100644 index 0000000..5dbbcea --- /dev/null +++ b/platforms/cmforth-picocalc/.cargo/config.toml @@ -0,0 +1,32 @@ +[target.thumbv8m.main-none-eabihf] +# Choose a default "cargo run" tool (see README for more info) +# - `probe-rs` provides flashing and defmt via a hardware debugger, and stack unwind on panic +# - `picotool` loads an elf, skipping unchanged flash sectors, verify it, and execute it +#runner = "probe-rs run --chip RP235x --protocol swd" +# runner = "picotool load -f -u -v -x -t elf" +runner = "probe-rs run --chip RP235x --protocol swd" + +linker = "flip-link" + +# Pass some extra options to rustc, some of which get passed on to the linker. +# +# * linker argument --nmagic turns off page alignment of sections (which saves +# flash space) +# * linker argument -Tlink.x tells the linker to use link.x as a linker script. +# This is usually provided by the cortex-m-rt crate, and by default the +# version in that crate will include a file called `memory.x` which describes +# the particular memory layout for your specific chip. +# * linker argument -Tdefmt.x also tells the linker to use `defmt.x` as a +# secondary linker script. This is required to make defmt_rtt work. +rustflags = [ + "-C", "link-arg=--nmagic", + "-C", "link-arg=-Tlink.x", + "-C", "link-arg=-Tdefmt.x", + "-C", "target-cpu=cortex-m33", +] + +[build] +target = "thumbv8m.main-none-eabihf" + +[env] +DEFMT_LOG = "debug" diff --git a/platforms/cmforth-picocalc/Cargo.toml b/platforms/cmforth-picocalc/Cargo.toml new file mode 100644 index 0000000..af5c871 --- /dev/null +++ b/platforms/cmforth-picocalc/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "cmforth-picocalc" +version = "0.1.0" +edition = "2024" +license = "MIT OR Apache-2.0" + +[dependencies] +cmforth.workspace = true +cortex-m-rt.workspace = true +cortex-m.workspace = true +defmt-rtt.workspace = true +defmt.workspace = true +embedded-graphics.workspace = true +embedded-hal.workspace = true +heapless.workspace = true +panic-probe = { workspace = true, features = ["print-defmt"] } +rp235x-hal = { workspace = true, features = [ + "rt", + "critical-section-impl", + "binary-info", +] } +static_cell.workspace = true diff --git a/platforms/cmforth-picocalc/build.rs b/platforms/cmforth-picocalc/build.rs new file mode 100644 index 0000000..879dc97 --- /dev/null +++ b/platforms/cmforth-picocalc/build.rs @@ -0,0 +1,16 @@ +use std::{env, fs::File, io::Write, path::Path}; + +fn main() { + println!("cargo:rerun-if-changed=memory.x"); + println!("cargo:rerun-if-changed=build.rs"); + copy_linker_script() +} + +fn copy_linker_script() { + let out_dir = env::var("OUT_DIR").expect("No OUT_DIR"); + let dest_path = Path::new(&out_dir); + let mut f = File::create(dest_path.join("memory.x")).unwrap(); + f.write_all(include_bytes!("memory.x")).unwrap(); + + println!("cargo:rustc-link-search={}", dest_path.display()); +} diff --git a/platforms/cmforth-picocalc/memory.x b/platforms/cmforth-picocalc/memory.x new file mode 100644 index 0000000..c803896 --- /dev/null +++ b/platforms/cmforth-picocalc/memory.x @@ -0,0 +1,75 @@ +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 = 2048K + /* + * 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 +} + +SECTIONS { + /* ### Boot ROM info + * + * Goes after .vector_table, to keep it in the first 4K of flash + * where the Boot ROM (and picotool) can find it + */ + .start_block : ALIGN(4) + { + __start_block_addr = .; + KEEP(*(.start_block)); + KEEP(*(.boot_info)); + } > FLASH + +} INSERT AFTER .vector_table; + +/* move .text to start /after/ the boot info */ +_stext = ADDR(.start_block) + SIZEOF(.start_block); + +SECTIONS { + /* ### Picotool 'Binary Info' Entries + * + * Picotool looks through this block (as we have pointers to it in our + * header) to find interesting information. + */ + .bi_entries : ALIGN(4) + { + /* We put this in the header */ + __bi_entries_start = .; + /* Here are the entries */ + KEEP(*(.bi_entries)); + /* Keep this block a nice round size */ + . = ALIGN(4); + /* We put this in the header */ + __bi_entries_end = .; + } > FLASH +} INSERT AFTER .text; + +SECTIONS { + /* ### Boot ROM extra info + * + * Goes after everything in our program, so it can contain a signature. + */ + .end_block : ALIGN(4) + { + __end_block_addr = .; + KEEP(*(.end_block)); + } > FLASH + +} INSERT AFTER .uninit; + +PROVIDE(start_to_end = __end_block_addr - __start_block_addr); +PROVIDE(end_to_start = __start_block_addr - __end_block_addr); diff --git a/platforms/cmforth-picocalc/src/display.rs b/platforms/cmforth-picocalc/src/display.rs new file mode 100644 index 0000000..041291b --- /dev/null +++ b/platforms/cmforth-picocalc/src/display.rs @@ -0,0 +1,225 @@ +//! Driver for the PicoCalc's 4-inch, 320 × 320 IPS TFT display. +//! +//! The panel uses an ST7365P controller with an ILI9488-compatible command +//! set. ClockworkPi's reference firmware identifies it as an ILI9488. This +//! module implements a write-only [`DrawTarget`] using SPI mode 0 and the +//! reference initialization sequence. The controller is configured for +//! 18-bit RGB666 color, represented by three bytes per [`Rgb888`] pixel. +//! +//! # Pinout +//! +//! The display is connected to the RP2350's SPI1 peripheral: +//! +//! | Pico GPIO | Display signal | Expected function | +//! |-----------|----------------|-------------------| +//! | GP10 | SCK | SPI1 serial-clock output. | +//! | GP11 | MOSI/SDI | SPI1 data output carrying commands, parameters, and pixels. | +//! | GP12 | MISO/SDO | SPI1 data input for controller and display-memory reads; configured by the board setup but unused by this write-only driver. | +//! | GP13 | CS | Active-low chip select. It must remain high for at least 40 ns between transactions. | +//! | GP14 | D/C | Command/data selection: low for commands, high for parameters and pixel data. | +//! | GP15 | RESET | Active-low hardware reset for the display controller. | +//! +//! The LCD backlight is not connected to an RP2350 GPIO. It is controlled by +//! the PicoCalc's STM32 system controller over I2C1 on GP6 (SDA) and GP7 (SCL) +//! and must be enabled separately from this display driver. + +use core::convert::Infallible; + +mod control; +mod traits; + +use control::HardwareControls; +use embedded_graphics::{pixelcolor::Rgb888, prelude::*, primitives::Rectangle}; +use embedded_hal::{delay::DelayNs, digital::OutputPin, spi::SpiBus}; +pub use traits::{BatchedPixelWriteOp, DisplayControls}; + +pub const WIDTH: u16 = 320; +pub const HEIGHT: u16 = 320; + +const GRAM_HEIGHT: u16 = 480; +const VERTICAL_SCROLLING_DEFINITION: u8 = 0x33; +const VERTICAL_SCROLLING_START_ADDRESS: u8 = 0x37; + +/// Write-only driver for the PicoCalc display. +pub struct PicoCalcDisplay { + controls: C, + scroll_offset: u16, +} + +impl PicoCalcDisplay> +where + SPI: SpiBus, + CS: OutputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + pub fn new(spi: SPI, cs: CS, dc: DC, reset: RST, delay: DELAY) -> Self { + Self { + controls: HardwareControls::new(spi, cs, dc, reset, delay), + scroll_offset: 0, + } + } +} + +impl PicoCalcDisplay { + /// Reset and initialize the panel using ClockworkPi's reference sequence. + pub fn init(&mut self) -> Result<(), C::Error> { + self.controls.hardware_reset(); + + self.controls.send_command_with_data( + 0xe0, + &[ + 0x00, 0x03, 0x09, 0x08, 0x16, 0x0a, 0x3f, 0x78, 0x4c, 0x09, 0x0a, 0x08, 0x16, 0x1a, + 0x0f, + ], + )?; + self.controls.send_command_with_data( + 0xe1, + &[ + 0x00, 0x16, 0x19, 0x03, 0x0f, 0x05, 0x32, 0x45, 0x46, 0x04, 0x0e, 0x0d, 0x35, 0x37, + 0x0f, + ], + )?; + self.controls.send_command_with_data(0xc0, &[0x17, 0x15])?; + self.controls.send_command_with_data(0xc1, &[0x41])?; + self.controls + .send_command_with_data(0xc5, &[0x00, 0x12, 0x80])?; + self.controls.send_command_with_data(0x36, &[0x48])?; // MX and BGR order + self.controls.send_command_with_data(0x3a, &[0x66])?; // 18-bit RGB over SPI + self.controls.send_command_with_data(0xb0, &[0x00])?; + self.controls.send_command_with_data(0xb1, &[0xa0])?; + self.controls.send_command_with_data(0x21, &[])?; // Display inversion on + self.controls.send_command_with_data(0xb4, &[0x02])?; + self.controls + .send_command_with_data(0xb6, &[0x02, 0x02, 0x3b])?; + self.controls.send_command_with_data(0xb7, &[0xc6])?; + self.controls.send_command_with_data(0xe9, &[0x00])?; + self.controls + .send_command_with_data(0xf7, &[0xa9, 0x51, 0x2c, 0x82])?; + + self.controls.send_command_with_data(0x11, &[])?; // Sleep out + self.controls.delay_ms(120); + self.controls.send_command_with_data(0x29, &[])?; // Display on + self.controls.delay_ms(120); + self.controls.send_command_with_data(0x36, &[0x48])?; + + // The controller has 480 rows of GRAM behind the 320-row panel. Make + // the entire GRAM area scrollable so it can be used as a circular + // backing store for the terminal viewport. + let [scroll_height_high, scroll_height_low] = GRAM_HEIGHT.to_be_bytes(); + self.controls.send_command_with_data( + VERTICAL_SCROLLING_DEFINITION, + &[ + 0x00, + 0x00, + scroll_height_high, + scroll_height_low, + 0x00, + 0x00, + ], + )?; + self.write_scroll_offset(0)?; + self.scroll_offset = 0; + + Ok(()) + } + + /// Scrolls the visible viewport up and clears the newly exposed rows. + pub(crate) fn scroll_up(&mut self, rows: u16, color: Rgb888) -> Result<(), C::Error> { + if rows == 0 { + return Ok(()); + } + + let old_offset = self.scroll_offset; + let offset = (self.scroll_offset + rows) % GRAM_HEIGHT; + // Use the next mapping to clear the rows that are about to enter the + // viewport while the controller still keeps them off-screen. + self.scroll_offset = offset; + let area = Rectangle::new( + Point::new(0, (HEIGHT - rows) as i32), + Size::new(WIDTH as u32, rows as u32), + ); + if let Err(error) = self.fill_solid(&area, color) { + self.scroll_offset = old_offset; + return Err(error); + } + + if let Err(error) = self.write_scroll_offset(offset) { + self.scroll_offset = old_offset; + return Err(error); + } + + Ok(()) + } + + fn write_scroll_offset(&mut self, offset: u16) -> Result<(), C::Error> { + self.controls + .send_command_with_data(VERTICAL_SCROLLING_START_ADDRESS, &offset.to_be_bytes()) + } +} + +impl OriginDimensions for PicoCalcDisplay { + fn size(&self) -> Size { + Size::new(WIDTH as u32, HEIGHT as u32) + } +} + +impl DrawTarget for PicoCalcDisplay { + type Color = Rgb888; + type Error = C::Error; + + fn draw_iter(&mut self, pixels: I) -> Result<(), Self::Error> + where + I: IntoIterator>, + { + let scroll_offset = self.scroll_offset; + let mut iter = pixels.into_iter().peekable(); + + while let Some(&Pixel(first_point, _)) = iter.peek() { + if first_point.x < 0 + || first_point.y < 0 + || first_point.x >= WIDTH as i32 + || first_point.y >= HEIGHT as i32 + { + iter.next(); + continue; + } + + let physical_y = (scroll_offset + first_point.y as u16) % GRAM_HEIGHT; + let window_start_x = first_point.x; + + let mut op = self.controls.begin_pixel_write( + first_point.x as u16, + physical_y, + WIDTH - 1, + GRAM_HEIGHT - 1, + )?; + + // Consume pixels while they form a contiguous run. + let mut expected = Some(first_point); + while let Some(&Pixel(point, color)) = iter.peek() { + if expected != Some(point) { + break; + } + + op.push_pixel(color)?; + + let py = (scroll_offset + point.y as u16) % GRAM_HEIGHT; + expected = if point.x < WIDTH as i32 - 1 { + Some(Point::new(point.x + 1, point.y)) + } else if point.y < HEIGHT as i32 - 1 && py < GRAM_HEIGHT - 1 { + Some(Point::new(window_start_x, point.y + 1)) + } else { + None + }; + + iter.next(); + } + + op.finish()?; + } + + Ok(()) + } +} diff --git a/platforms/cmforth-picocalc/src/display/control.rs b/platforms/cmforth-picocalc/src/display/control.rs new file mode 100644 index 0000000..5e032ff --- /dev/null +++ b/platforms/cmforth-picocalc/src/display/control.rs @@ -0,0 +1,257 @@ +//! Hardware-backed implementation of the display-control traits. +//! +//! This module provides [`HardwareControls`], a concrete implementation of +//! [`DisplayControls`] that talks to the ST7365P display controller over an +//! embedded-hal SPI bus plus chip-select (CS), data/command (D/C), and reset +//! GPIO pins. It handles the low-level transaction details: asserting CS for +//! the duration of each transfer, toggling D/C to distinguish command bytes +//! from parameter and pixel data, and honoring the controller's minimum +//! 40 ns CS-high time between transactions. +//! +//! Pixel writes are performed through [`HardwareWriteOp`], which programs +//! the controller's column/page address window, issues a memory-write +//! command, and then streams pixel data while CS stays asserted. Dropping +//! the operation without finishing it still flushes and deselects the +//! display, so an abandoned write never leaves the bus selected. + +use core::convert::Infallible; + +use embedded_graphics::{pixelcolor::Rgb888, prelude::*}; +use embedded_hal::{delay::DelayNs, digital::OutputPin, spi::SpiBus}; + +use super::traits::{BatchedPixelWriteOp, DisplayControls}; + +const COLUMN_ADDRESS_SET: u8 = 0x2a; +const PAGE_ADDRESS_SET: u8 = 0x2b; +const MEMORY_WRITE: u8 = 0x2c; + +// The ST7365P requires CS to remain high for at least 40 ns between +// transactions. +const CS_HIGH_DELAY_NS: u32 = 40; + +/// Hardware-backed implementation of [`DisplayControls`]. +pub struct HardwareControls { + spi: SPI, + cs: CS, + dc: DC, + reset: RST, + delay: DELAY, +} + +impl HardwareControls +where + SPI: SpiBus, + CS: OutputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + pub fn new(spi: SPI, cs: CS, dc: DC, reset: RST, delay: DELAY) -> Self { + Self { + spi, + cs, + dc, + reset, + delay, + } + } + + /// Sends the given bytes as a command or data transaction + fn send_command_or_data(&mut self, data: bool, bytes: &[u8]) -> Result<(), SPI::Error> { + if data { + self.dc.set_high().unwrap(); + } else { + self.dc.set_low().unwrap(); + } + + self.select(); + let result = self.spi.write(bytes).and_then(|()| self.spi.flush()); + self.deselect(); + result + } + + /// Engages chip select. + fn select(&mut self) { + self.cs.set_low().unwrap(); + } + + /// Disengages chip select. + fn deselect(&mut self) { + self.cs.set_high().unwrap(); + self.delay.delay_ns(CS_HIGH_DELAY_NS); + } +} + +/// An active pixel-write operation on the hardware display. +/// +/// Buffers pixels internally and flushes them to the SPI bus in +/// scanline-sized chunks to amortize transfer overhead. +pub struct HardwareWriteOp<'a, SPI, CS, DC, RST, DELAY> +where + SPI: SpiBus, + CS: OutputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + controls: &'a mut HardwareControls, +} + +impl<'a, SPI, CS, DC, RST, DELAY> HardwareWriteOp<'a, SPI, CS, DC, RST, DELAY> +where + SPI: SpiBus, + CS: OutputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + fn initiate( + controls: &'a mut HardwareControls, + x_start: u16, + y_start: u16, + x_end: u16, + y_end: u16, + ) -> Result { + let columns = [ + (x_start >> 8) as u8, + x_start as u8, + (x_end >> 8) as u8, + x_end as u8, + ]; + let pages = [ + (y_start >> 8) as u8, + y_start as u8, + (y_end >> 8) as u8, + y_end as u8, + ]; + + controls.select(); + + let result = (|| -> Result<(), SPI::Error> { + controls.dc.set_low().unwrap(); + controls.spi.write(&[COLUMN_ADDRESS_SET])?; + controls.spi.flush()?; + controls.dc.set_high().unwrap(); + controls.spi.write(&columns)?; + controls.spi.flush()?; + + controls.dc.set_low().unwrap(); + controls.spi.write(&[PAGE_ADDRESS_SET])?; + controls.spi.flush()?; + controls.dc.set_high().unwrap(); + controls.spi.write(&pages)?; + controls.spi.flush()?; + + controls.dc.set_low().unwrap(); + controls.spi.write(&[MEMORY_WRITE])?; + controls.spi.flush()?; + controls.dc.set_high().unwrap(); + + Ok(()) + })(); + + result.inspect_err(|_| { + controls.deselect(); + })?; + + Ok(Self { controls }) + } + + fn finish_impl(&mut self) -> Result<(), SPI::Error> { + let result = self.controls.spi.flush(); + self.controls.deselect(); + result + } +} + +impl BatchedPixelWriteOp for HardwareWriteOp<'_, SPI, CS, DC, RST, DELAY> +where + SPI: SpiBus, + CS: OutputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + type Error = SPI::Error; + + fn push_pixel(&mut self, color: Rgb888) -> Result<(), Self::Error> { + let pix_buffer = &[color.r(), color.g(), color.b()]; + let result = self.controls.spi.write(pix_buffer); + if result.is_err() { + self.controls.deselect(); + } + Ok(()) + } + + fn finish(mut self) -> Result<(), Self::Error> { + let result = self.finish_impl(); + let _ = core::mem::ManuallyDrop::new(self); + result + } +} + +impl Drop for HardwareWriteOp<'_, SPI, CS, DC, RST, DELAY> +where + SPI: SpiBus, + CS: OutputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + fn drop(&mut self) { + let _ = self.finish_impl(); + } +} + +impl DisplayControls for HardwareControls +where + SPI: SpiBus, + CS: OutputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + type Error = SPI::Error; + + type WriteOp<'a> + = HardwareWriteOp<'a, SPI, CS, DC, RST, DELAY> + where + Self: 'a; + + fn send_command(&mut self, command: u8) -> Result<(), Self::Error> { + self.send_command_or_data(false, &[command]) + } + + fn send_data(&mut self, data: &[u8]) -> Result<(), Self::Error> { + if data.is_empty() { + return Ok(()); + } + self.send_command_or_data(true, data) + } + + fn begin_pixel_write( + &mut self, + x_start: u16, + y_start: u16, + x_end: u16, + y_end: u16, + ) -> Result, Self::Error> { + HardwareWriteOp::initiate(self, x_start, y_start, x_end, y_end) + } + + fn hardware_reset(&mut self) { + self.cs.set_high().unwrap(); + self.dc.set_high().unwrap(); + + self.reset.set_high().unwrap(); + self.delay.delay_ms(10); + self.reset.set_low().unwrap(); + self.delay.delay_ms(10); + self.reset.set_high().unwrap(); + self.delay.delay_ms(200); + } + + fn delay_ms(&mut self, ms: u32) { + self.delay.delay_ms(ms); + } +} diff --git a/platforms/cmforth-picocalc/src/display/traits.rs b/platforms/cmforth-picocalc/src/display/traits.rs new file mode 100644 index 0000000..d7640b5 --- /dev/null +++ b/platforms/cmforth-picocalc/src/display/traits.rs @@ -0,0 +1,79 @@ +//! Abstractions over the display controller's low-level hardware interface. +//! +//! This module defines the two traits that decouple [`PicoCalcDisplay`](super::PicoCalcDisplay)'s +//! display logic from the SPI bus, GPIO pins, and delay timer it runs on: +//! +//! - [`DisplayControls`] is the main interface: single command and data +//! transfers, hardware reset, delays, and starting pixel-write operations. +//! - [`BatchedPixelWriteOp`] is the handle returned by +//! [`DisplayControls::begin_pixel_write`], streaming pixels into an +//! address window in batches until the write is finished. +//! +//! Splitting the hardware access behind these traits keeps +//! [`PicoCalcDisplay`](super::PicoCalcDisplay) non-generic over the concrete bus and pin types and +//! allows the display logic to be tested against mock implementations. See +//! the `control` submodule for the hardware-backed implementation. + +use embedded_graphics::pixelcolor::Rgb888; + +/// An incremental pixel-write operation on the display. +/// +/// Returned by [`DisplayControls::begin_pixel_write`]. Buffers pixel data +/// internally, flushing it to the display in batches, until the operation +/// is finalized. +pub trait BatchedPixelWriteOp { + type Error; + + /// Push one 18-bit RGB pixel into the operation, flushing the internal + /// buffer to the display when it fills up. + fn push_pixel(&mut self, pixel: Rgb888) -> Result<(), Self::Error>; + + /// Flush any buffered pixels and finalize the write operation, + /// deselecting the display. + fn finish(self) -> Result<(), Self::Error>; +} + +/// Low-level display hardware controls. +/// +/// This trait abstracts the SPI bus, GPIO pins, and delay timer needed to +/// communicate with the display controller. Implementors handle the +/// hardware-specific details so that [`PicoCalcDisplay`](super::PicoCalcDisplay) can focus on +/// display logic without being generic over the hardware types. +pub trait DisplayControls { + type Error: core::fmt::Debug; + type WriteOp<'a>: BatchedPixelWriteOp + where + Self: 'a; + + /// Send a single command byte to the display controller. + fn send_command(&mut self, command: u8) -> Result<(), Self::Error>; + + /// Send parameter or pixel data bytes to the display controller. + fn send_data(&mut self, data: &[u8]) -> Result<(), Self::Error>; + + /// Send a command byte followed by its parameter data. + fn send_command_with_data(&mut self, command: u8, data: &[u8]) -> Result<(), Self::Error> { + self.send_command(command)?; + if !data.is_empty() { + self.send_data(data)?; + } + Ok(()) + } + + /// Begin an incremental pixel-write operation for the given address + /// window. Returns a handle that can be used to write pixel data in + /// batches. + fn begin_pixel_write( + &mut self, + x_start: u16, + y_start: u16, + x_end: u16, + y_end: u16, + ) -> Result, Self::Error>; + + /// Perform a hardware reset of the display controller. + fn hardware_reset(&mut self); + + /// Busy-wait for the given number of milliseconds. + fn delay_ms(&mut self, ms: u32); +} diff --git a/platforms/cmforth-picocalc/src/keyboard.rs b/platforms/cmforth-picocalc/src/keyboard.rs new file mode 100644 index 0000000..c362d47 --- /dev/null +++ b/platforms/cmforth-picocalc/src/keyboard.rs @@ -0,0 +1,62 @@ +//! Driver for the PicoCalc keyboard controller. +//! +//! The keyboard is scanned by the PicoCalc's STM32 system controller. Key +//! events are read over I2C1 on GP6 (SDA) and GP7 (SCL), which is the same bus +//! used to control the LCD backlight. + +use embedded_hal::i2c::I2c; + +const CONTROLLER_ADDRESS: u8 = 0x1f; +const KEY_EVENT_REGISTER: u8 = 0x09; +const CONTROL_KEY: u8 = 0x7e; + +const KEY_PRESSED: u8 = 0x01; +const CONTROL_PRESSED: u8 = 0x02; +const CONTROL_RELEASED: u8 = 0x03; + +/// Polling driver for the PicoCalc keyboard. +pub struct PicoCalcKeyboard { + i2c: I2C, + control_held: bool, +} + +impl PicoCalcKeyboard +where + I2C: I2c, +{ + pub fn new(i2c: I2C) -> Self { + Self { + i2c, + control_held: false, + } + } + + /// Polls the controller once and returns a newly pressed key, if any. + /// + /// The controller returns two bytes: an event type followed by the key's + /// ASCII value. Control key transitions are consumed by the driver, and + /// Ctrl+A through Ctrl+Z are returned as ASCII control characters. + pub fn read_key(&mut self) -> Result, I2C::Error> { + let mut event = [0; 2]; + self.i2c + .write_read(CONTROLLER_ADDRESS, &[KEY_EVENT_REGISTER], &mut event)?; + + match event { + [CONTROL_PRESSED, CONTROL_KEY] => { + self.control_held = true; + Ok(None) + } + [CONTROL_RELEASED, CONTROL_KEY] => { + self.control_held = false; + Ok(None) + } + [KEY_PRESSED, mut key] => { + if self.control_held && key.is_ascii_lowercase() { + key = key - b'a' + 1; + } + if key >= 128 { Ok(None) } else { Ok(Some(key)) } + } + _ => Ok(None), + } + } +} diff --git a/platforms/cmforth-picocalc/src/main.rs b/platforms/cmforth-picocalc/src/main.rs new file mode 100644 index 0000000..76a18c3 --- /dev/null +++ b/platforms/cmforth-picocalc/src/main.rs @@ -0,0 +1,152 @@ +//! cmforth interpreter for the PicoCalc. +#![no_std] +#![no_main] + +mod display; +mod keyboard; +mod teletype; + +use cmforth::{ + Forth, + io::{CombinedIo, StringReader, Writer}, + stack::{Stack, StackStorage}, + types::{Address, Word}, +}; +use core::fmt::Write; +use defmt::*; +use defmt_rtt as _; +use display::PicoCalcDisplay; +use embedded_graphics::{pixelcolor::Rgb888, prelude::*}; +use embedded_hal::i2c::I2c; +use embedded_hal::spi::MODE_0; +use keyboard::PicoCalcKeyboard; +use panic_probe as _; +use rp235x_hal::clocks::init_clocks_and_plls; +use rp235x_hal::fugit::RateExtU32; +use rp235x_hal::gpio::{FunctionSpi, PinState}; +use rp235x_hal::{self as hal, entry}; +use rp235x_hal::{Clock, pac}; +use static_cell::StaticCell; +use teletype::Teletype; + +const DATA_STACK_WORDS: usize = 512; +const RETURN_STACK_ADDRESSES: usize = 128; +const COMPILE_AREA_WORDS: usize = 2048; + +static DATA_STACK_STORAGE: StaticCell> = StaticCell::new(); +static RETURN_STACK_STORAGE: StaticCell> = + StaticCell::new(); +static COMPILE_AREA_STORAGE: StaticCell> = StaticCell::new(); + +/// Tell the Boot ROM about our application +#[unsafe(link_section = ".start_block")] +#[used] +pub static IMAGE_DEF: hal::block::ImageDef = hal::block::ImageDef::secure_exe(); + +#[entry] +fn main() -> ! { + info!("Program start"); + let mut pac = pac::Peripherals::take().unwrap(); + let mut watchdog = hal::Watchdog::new(pac.WATCHDOG); + let sio = hal::Sio::new(pac.SIO); + + // External high-speed crystal on the pico board is 12Mhz + let external_xtal_freq_hz = 12_000_000u32; + let clocks = init_clocks_and_plls( + external_xtal_freq_hz, + pac.XOSC, + pac.CLOCKS, + pac.PLL_SYS, + pac.PLL_USB, + &mut pac.RESETS, + &mut watchdog, + ) + .ok() + .unwrap(); + + let delay = hal::Timer::new_timer0(pac.TIMER0, &mut pac.RESETS, &clocks); + + let pins = hal::gpio::Pins::new( + pac.IO_BANK0, + pac.PADS_BANK0, + sio.gpio_bank0, + &mut pac.RESETS, + ); + + // The LCD backlight is controlled by the PicoCalc's STM32, independently + // of the LCD controller. Set it explicitly instead of relying on its + // retained/default value. + let mut system_i2c = hal::I2C::i2c1( + pac.I2C1, + pins.gpio6.reconfigure(), + pins.gpio7.reconfigure(), + 10_000u32.Hz(), + &mut pac.RESETS, + clocks.system_clock.freq(), + ); + if system_i2c.write(0x1f_u8, &[0x85, 0xf0]).is_err() { + warn!("Could not set LCD backlight"); + } + let keyboard = PicoCalcKeyboard::new(system_i2c); + + // PicoCalc display: SPI1 SCK=GP10, MOSI=GP11, MISO=GP12, CS=GP13, + // DC=GP14, RESET=GP15. + let sck = pins.gpio10.into_function::(); + let mosi = pins.gpio11.into_function::(); + let miso = pins.gpio12.into_function::(); + let cs = pins.gpio13.into_push_pull_output_in_state(PinState::High); + let dc = pins.gpio14.into_push_pull_output_in_state(PinState::High); + let reset = pins.gpio15.into_push_pull_output_in_state(PinState::High); + + let spi = hal::Spi::<_, _, _, 8>::new(pac.SPI1, (mosi, miso, sck)).init( + &mut pac.RESETS, + clocks.peripheral_clock.freq(), + 25_000_000u32.Hz(), + MODE_0, + ); + + let mut display = PicoCalcDisplay::new(spi, cs, dc, reset, delay); + display.init().unwrap(); + display.clear(Rgb888::BLACK).unwrap(); + + let data_stack_storage = DATA_STACK_STORAGE.init_with(StackStorage::new); + let return_stack_storage = RETURN_STACK_STORAGE.init_with(StackStorage::new); + let compile_area_storage = COMPILE_AREA_STORAGE.init_with(StackStorage::new); + let mut forth = Forth::new( + Stack::new_with(data_stack_storage), + Stack::new_with(return_stack_storage), + Stack::new_with(compile_area_storage), + ); + + let teletype = Teletype::new(keyboard, display); + let mut initial_io = CombinedIo::new(StringReader::new(cmforth::FORTH_SOURCE), teletype); + while !initial_io.reader.is_eof() { + unsafe { forth.interpret_one(&mut initial_io) }.unwrap(); + } + + info!("Forth interpreter initialized"); + let mut teletype = initial_io.writer; + loop { + unsafe { + let _ = forth.run(&mut teletype).inspect_err(|err| { + let mut string: heapless::String<256> = heapless::String::new(); + let _ = core::write!(string, "{err}"); + teletype.write(string.as_bytes()); + defmt::error!("Error running forth interpreter: {}", string.as_str()); + }); + } + } +} + +/// Program metadata for `picotool info` +#[unsafe(link_section = ".bi_entries")] +#[used] +pub static PICOTOOL_ENTRIES: [rp235x_hal::binary_info::EntryAddr; 5] = [ + rp235x_hal::binary_info::rp_cargo_bin_name!(), + rp235x_hal::binary_info::rp_cargo_version!(), + rp235x_hal::binary_info::rp_program_description!(c"cmforth interpreter"), + rp235x_hal::binary_info::rp_cargo_homepage_url!(), + rp235x_hal::binary_info::rp_program_build_attribute!(), +]; + +// End of file diff --git a/platforms/cmforth-picocalc/src/teletype.rs b/platforms/cmforth-picocalc/src/teletype.rs new file mode 100644 index 0000000..170c924 --- /dev/null +++ b/platforms/cmforth-picocalc/src/teletype.rs @@ -0,0 +1,221 @@ +use embedded_graphics::{ + mono_font::{MonoTextStyleBuilder, ascii::FONT_6X12}, + pixelcolor::Rgb888, + prelude::*, + text::{Baseline, Text}, +}; +use embedded_hal::i2c::I2c; +use heapless::string::String; +use heapless::{Deque, Vec}; + +use crate::display::{DisplayControls, HEIGHT, PicoCalcDisplay, WIDTH}; +use crate::keyboard::PicoCalcKeyboard; +use cmforth::io::{Reader, ReaderWriter, Writer}; + +const CHARACTER_WIDTH: u32 = FONT_6X12.character_size.width + FONT_6X12.character_spacing; +const CHARACTER_HEIGHT: u32 = FONT_6X12.character_size.height; +const MAX_LINES: usize = HEIGHT as usize / CHARACTER_HEIGHT as usize; +const MAX_WIDTH: usize = WIDTH as usize / CHARACTER_WIDTH as usize; + +pub struct Teletype { + read_line: Vec, + read_index: usize, + lines: Deque, MAX_LINES>, + keyboard: PicoCalcKeyboard, + display: PicoCalcDisplay, +} + +impl Teletype { + pub fn new(keyboard: PicoCalcKeyboard, display: PicoCalcDisplay) -> Self { + Self { + read_line: Vec::new(), + read_index: 0, + lines: Deque::new(), + keyboard, + display, + } + } +} + +impl Teletype +where + I2C: I2c, + C: DisplayControls, +{ + fn read_key(&mut self) -> u8 { + loop { + if let Ok(Some(key)) = self.keyboard.read_key() { + return key; + } + } + } + + fn newline(&mut self) { + if self.lines.is_full() { + self.lines.pop_front(); + self.display + .scroll_up(CHARACTER_HEIGHT as u16, Rgb888::BLACK) + .unwrap(); + } + self.lines.push_back(String::new()).unwrap(); + } + + fn update_text(&mut self, line: usize, column: usize, text: &str) { + let text_style = MonoTextStyleBuilder::new() + .font(&FONT_6X12) + .text_color(Rgb888::WHITE) + .background_color(Rgb888::BLACK) + .build(); + + Text::with_baseline( + text, + Point::new( + column as i32 * CHARACTER_WIDTH as i32, + line as i32 * CHARACTER_HEIGHT as i32, + ), + text_style, + Baseline::Top, + ) + .draw(&mut self.display) + .unwrap(); + } + + fn update_character(&mut self, line: usize, column: usize, character: u8) { + let bytes = [character]; + let text = core::str::from_utf8(&bytes).unwrap(); + self.update_text(line, column, text); + } + + fn buffer_line(&mut self) { + if self.lines.back().is_none_or(|line| !line.is_empty()) { + self.newline(); + } + let last_line = self.lines.back_mut().unwrap(); + last_line.push_str("ok ").unwrap(); + let line_number = self.lines.len() - 1; + self.update_text(line_number, 0, "ok "); + + self.read_line.clear(); + self.read_index = 0; + + let mut exit = false; + while !exit { + let v = self.read_key(); + match v { + b'\x08' | b'\x7f' => { + if self + .read_line + .pop() + .is_some_and(|v| v.is_ascii_graphic() || v == b' ') + { + let last_line = self.lines.back_mut().unwrap(); + last_line.pop(); + let line_number = self.lines.len() - 1; + let column = self.lines.back().unwrap().len(); + self.update_character(line_number, column, b' '); + } + } + b'\r' | b'\n' => { + self.newline(); + exit = true; + } + v => { + let printable = v.is_ascii_graphic() || v == b' '; + if !self.read_line.is_full() + && (!printable + || self.lines.back().unwrap().len() + < self.lines.back().unwrap().capacity()) + { + self.read_line.push(v).unwrap(); + if printable { + let line_number = self.lines.len() - 1; + let column = self.lines.back().unwrap().len(); + self.lines.back_mut().unwrap().push(v as char).unwrap(); + self.update_character(line_number, column, v); + } + } + } + } + } + } +} + +impl Reader for Teletype +where + I2C: I2c, + C: DisplayControls, +{ + fn read(&mut self) -> u8 { + while self.read_index >= self.read_line.len() { + self.buffer_line(); + } + + let value = self.read_line[self.read_index]; + self.read_index += 1; + value + } + + fn read_word(&mut self) -> &[u8] { + loop { + while self + .read_line + .get(self.read_index) + .is_some_and(|v| v.is_ascii_whitespace()) + { + self.read_index += 1; + } + + if self.read_index < self.read_line.len() { + break; + } + + self.buffer_line(); + } + + let start = self.read_index; + while self + .read_line + .get(self.read_index) + .is_some_and(|v| !v.is_ascii_whitespace()) + { + self.read_index += 1; + } + &self.read_line[start..self.read_index] + } +} + +impl Writer for Teletype +where + I2C: I2c, + C: DisplayControls, +{ + fn write(&mut self, data: &[u8]) { + for &v in data { + match v { + b'\n' => self.newline(), + b'\r' => {} + b' '..=b'~' => { + if self + .lines + .back() + .is_none_or(|line| line.len() == line.capacity()) + { + self.newline(); + } + let line_number = self.lines.len() - 1; + let column = self.lines.back().unwrap().len(); + self.lines.back_mut().unwrap().push(v as char).unwrap(); + self.update_character(line_number, column, v); + } + _ => {} + } + } + } +} + +impl ReaderWriter for Teletype +where + I2C: I2c, + C: DisplayControls, +{ +} diff --git a/platforms/cmforth-qemu/src/main.rs b/platforms/cmforth-qemu/src/main.rs index e87f4c5..2187ecc 100644 --- a/platforms/cmforth-qemu/src/main.rs +++ b/platforms/cmforth-qemu/src/main.rs @@ -23,7 +23,7 @@ static COMPILE_AREA_STORAGE: StaticCell> use core::fmt::Write; use core::panic::PanicInfo; -struct SemihostingWriter {} +struct SemihostingWriter; impl Write for SemihostingWriter { fn write_str(&mut self, s: &str) -> core::fmt::Result { @@ -51,7 +51,7 @@ impl log::Log for SemihostingLogger { fn log(&self, record: &log::Record) { if record.metadata().level() <= self.level { let _ = writeln!( - &mut SemihostingWriter {}, + &mut SemihostingWriter, "{} - {}: {}", record.target(), record.level(), @@ -70,7 +70,7 @@ impl log::Log for SemihostingLogger { #[inline(never)] #[panic_handler] fn panic(info: &PanicInfo) -> ! { - let _ = writeln!(&mut SemihostingWriter {}, "{}", info.message()); + let _ = writeln!(&mut SemihostingWriter, "{}", info.message()); loop {} }