From b422a05594e1b04f8ac8ea143ea625e17277b0c0 Mon Sep 17 00:00:00 2001 From: Javier Alvarez Date: Sun, 19 Jul 2026 19:01:59 +0200 Subject: [PATCH 1/9] Initial picocalc platform --- Cargo.lock | 397 +++++++++++++++++- Cargo.toml | 5 + flake.nix | 2 + platforms/cmforth-picocalc/.cargo/config.toml | 31 ++ platforms/cmforth-picocalc/Cargo.toml | 21 + platforms/cmforth-picocalc/build.rs | 16 + platforms/cmforth-picocalc/memory.x | 75 ++++ platforms/cmforth-picocalc/src/main.rs | 87 ++++ platforms/cmforth-qemu/src/main.rs | 6 +- 9 files changed, 633 insertions(+), 7 deletions(-) create mode 100644 platforms/cmforth-picocalc/.cargo/config.toml create mode 100644 platforms/cmforth-picocalc/Cargo.toml create mode 100644 platforms/cmforth-picocalc/build.rs create mode 100644 platforms/cmforth-picocalc/memory.x create mode 100644 platforms/cmforth-picocalc/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index e7776e1..5c720ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # 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 = "bare-metal" version = "0.2.5" @@ -17,6 +23,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 +46,25 @@ 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-hal 1.0.0", + "panic-probe", + "rp235x-hal", +] + [[package]] name = "cmforth-repl" version = "0.1.0" @@ -40,7 +72,7 @@ dependencies = [ "cmforth", "cortex-m", "cortex-m-rt", - "heapless", + "heapless 0.9.3", "log", "static_cell", ] @@ -52,8 +84,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 +109,68 @@ 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-hal" version = "0.2.7" @@ -87,6 +181,97 @@ 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 = "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 +281,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,6 +301,15 @@ 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" @@ -127,6 +331,53 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d" +[[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 +402,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 +537,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 +558,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 +617,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..8ec6362 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,11 @@ heapless = "0.9.2" log = "0.4.29" cortex-m-rt = "0.7.5" static_cell = "2.1.1" +embedded-hal = "1.0.0" +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..828f4af --- /dev/null +++ b/platforms/cmforth-picocalc/.cargo/config.toml @@ -0,0 +1,31 @@ +[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" + +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..45a02f9 --- /dev/null +++ b/platforms/cmforth-picocalc/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "cmforth-picocalc" +version = "0.1.0" +edition = "2024" +license = "MIT OR Apache-2.0" + +[dependencies] +cmforth.workspace = true +cortex-m.workspace = true +cortex-m-rt.workspace = true +embedded-hal.workspace = true + +defmt.workspace = true +defmt-rtt.workspace = true +panic-probe = { workspace = true, features = ["print-defmt"] } + +rp235x-hal = { workspace = true, features = [ + "rt", + "critical-section-impl", + "binary-info", +] } 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/main.rs b/platforms/cmforth-picocalc/src/main.rs new file mode 100644 index 0000000..7b6ed69 --- /dev/null +++ b/platforms/cmforth-picocalc/src/main.rs @@ -0,0 +1,87 @@ +//! Blinks the LED on a Pico board +//! +//! This will blink an LED attached to GP25, which is the pin the Pico uses for the on-board LED. +#![no_std] +#![no_main] + +use defmt::*; +use defmt_rtt as _; +use embedded_hal::digital::OutputPin; +use panic_probe as _; +use rp235x_hal::clocks::init_clocks_and_plls; +use rp235x_hal::{self as hal, entry}; +use rp235x_hal::{Clock, pac}; + +// Provide an alias for our BSP so we can switch targets quickly. +// Uncomment the BSP you included in Cargo.toml, the rest of the code does not need to change. +// use some_bsp; + +/// 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 core = cortex_m::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 mut delay = cortex_m::delay::Delay::new(core.SYST, clocks.system_clock.freq().to_Hz()); + + let pins = hal::gpio::Pins::new( + pac.IO_BANK0, + pac.PADS_BANK0, + sio.gpio_bank0, + &mut pac.RESETS, + ); + + // This is the correct pin on the Raspberry Pico 2 board. On other boards, even if they have an + // on-board LED, it might need to be changed. + // + // Notably, on the Pico 2 W, the LED is not connected to any of the RP2350 GPIOs but to the cyw43 module instead. + // One way to do that is by using [embassy](https://github.com/embassy-rs/embassy/blob/main/examples/rp/src/bin/wifi_blinky.rs) + // + // If you have a Pico W and want to toggle a LED with a simple GPIO output pin, you can connect an external + // LED to one of the GPIO pins, and reference that pin here. Don't forget adding an appropriate resistor + // in series with the LED. + let mut led_pin = pins.gpio25.into_push_pull_output(); + + loop { + info!("on!"); + led_pin.set_high().unwrap(); + delay.delay_ms(500); + info!("off!"); + led_pin.set_low().unwrap(); + delay.delay_ms(500); + } +} + +/// 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-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 {} } From eca1a233925ed0581f17de622fea62d494e60227 Mon Sep 17 00:00:00 2001 From: Javier Alvarez Date: Sun, 19 Jul 2026 21:39:35 +0200 Subject: [PATCH 2/9] Initial picocalc display integration --- Cargo.lock | 60 +++ Cargo.toml | 1 + platforms/cmforth-picocalc/.cargo/config.toml | 3 +- platforms/cmforth-picocalc/Cargo.toml | 1 + platforms/cmforth-picocalc/src/display.rs | 342 ++++++++++++++++++ platforms/cmforth-picocalc/src/main.rs | 116 ++++-- 6 files changed, 496 insertions(+), 27 deletions(-) create mode 100644 platforms/cmforth-picocalc/src/display.rs diff --git a/Cargo.lock b/Cargo.lock index 5c720ca..ba587ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,18 @@ 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" @@ -60,6 +72,7 @@ dependencies = [ "cortex-m-rt", "defmt", "defmt-rtt", + "embedded-graphics", "embedded-hal 1.0.0", "panic-probe", "rp235x-hal", @@ -171,6 +184,29 @@ 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" @@ -212,6 +248,15 @@ 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" @@ -316,6 +361,12 @@ 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" @@ -331,6 +382,15 @@ 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" diff --git a/Cargo.toml b/Cargo.toml index 8ec6362..9565064 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ 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" diff --git a/platforms/cmforth-picocalc/.cargo/config.toml b/platforms/cmforth-picocalc/.cargo/config.toml index 828f4af..5dbbcea 100644 --- a/platforms/cmforth-picocalc/.cargo/config.toml +++ b/platforms/cmforth-picocalc/.cargo/config.toml @@ -3,7 +3,8 @@ # - `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 = "picotool load -f -u -v -x -t elf" +runner = "probe-rs run --chip RP235x --protocol swd" linker = "flip-link" diff --git a/platforms/cmforth-picocalc/Cargo.toml b/platforms/cmforth-picocalc/Cargo.toml index 45a02f9..86a93b6 100644 --- a/platforms/cmforth-picocalc/Cargo.toml +++ b/platforms/cmforth-picocalc/Cargo.toml @@ -8,6 +8,7 @@ license = "MIT OR Apache-2.0" cmforth.workspace = true cortex-m.workspace = true cortex-m-rt.workspace = true +embedded-graphics.workspace = true embedded-hal.workspace = true defmt.workspace = true diff --git a/platforms/cmforth-picocalc/src/display.rs b/platforms/cmforth-picocalc/src/display.rs new file mode 100644 index 0000000..ac23c2c --- /dev/null +++ b/platforms/cmforth-picocalc/src/display.rs @@ -0,0 +1,342 @@ +//! 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; + +use embedded_graphics::{pixelcolor::Rgb888, prelude::*, primitives::Rectangle}; +use embedded_hal::{delay::DelayNs, digital::OutputPin, spi::SpiBus}; + +pub const WIDTH: u16 = 320; +pub const HEIGHT: u16 = 320; + +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; + +/// Write-only driver for the PicoCalc's ILI9488 display. +pub struct PicoCalcDisplay { + spi: SPI, + cs: CS, + dc: DC, + reset: RST, + delay: DELAY, +} + +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 { + spi, + cs, + dc, + reset, + delay, + } + } + + /// Reset and initialize the panel using ClockworkPi's reference sequence. + pub fn init(&mut self) -> Result<(), SPI::Error> { + 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); + + self.command( + 0xe0, + &[ + 0x00, 0x03, 0x09, 0x08, 0x16, 0x0a, 0x3f, 0x78, 0x4c, 0x09, 0x0a, 0x08, 0x16, 0x1a, + 0x0f, + ], + )?; + self.command( + 0xe1, + &[ + 0x00, 0x16, 0x19, 0x03, 0x0f, 0x05, 0x32, 0x45, 0x46, 0x04, 0x0e, 0x0d, 0x35, 0x37, + 0x0f, + ], + )?; + self.command(0xc0, &[0x17, 0x15])?; + self.command(0xc1, &[0x41])?; + self.command(0xc5, &[0x00, 0x12, 0x80])?; + self.command(0x36, &[0x48])?; // MX and BGR order + self.command(0x3a, &[0x66])?; // 18-bit RGB over SPI + self.command(0xb0, &[0x00])?; + self.command(0xb1, &[0xa0])?; + self.command(0x21, &[])?; // Display inversion on + self.command(0xb4, &[0x02])?; + self.command(0xb6, &[0x02, 0x02, 0x3b])?; + self.command(0xb7, &[0xc6])?; + self.command(0xe9, &[0x00])?; + self.command(0xf7, &[0xa9, 0x51, 0x2c, 0x82])?; + + self.command(0x11, &[])?; // Sleep out + self.delay.delay_ms(120); + self.command(0x29, &[])?; // Display on + self.delay.delay_ms(120); + self.command(0x36, &[0x48])?; + + Ok(()) + } + + fn write_init(&mut self, data: bool, bytes: &[u8]) -> Result<(), SPI::Error> { + if data { + self.dc.set_high().unwrap(); + } else { + self.dc.set_low().unwrap(); + } + + self.cs.set_low().unwrap(); + let result = self.spi.write(bytes).and_then(|()| self.spi.flush()); + self.deselect(); + result + } + + fn command(&mut self, command: u8, data: &[u8]) -> Result<(), SPI::Error> { + // Keep command and data in separate transactions so CS returns high + // before D/C changes, but send all parameter bytes in one transfer. + self.write_init(false, &[command])?; + if !data.is_empty() { + self.write_init(true, data)?; + } + Ok(()) + } + + fn begin_write( + &mut self, + x_start: u16, + y_start: u16, + x_end: u16, + y_end: u16, + ) -> Result<(), SPI::Error> { + 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, + ]; + + self.cs.set_low().unwrap(); + + let result = (|| -> Result<(), SPI::Error> { + self.dc.set_low().unwrap(); + self.spi.write(&[COLUMN_ADDRESS_SET])?; + self.spi.flush()?; + self.dc.set_high().unwrap(); + self.spi.write(&columns)?; + self.spi.flush()?; + + self.dc.set_low().unwrap(); + self.spi.write(&[PAGE_ADDRESS_SET])?; + self.spi.flush()?; + self.dc.set_high().unwrap(); + self.spi.write(&pages)?; + self.spi.flush()?; + + self.dc.set_low().unwrap(); + self.spi.write(&[MEMORY_WRITE])?; + self.spi.flush()?; + self.dc.set_high().unwrap(); + + Ok(()) + })(); + + if result.is_err() { + self.deselect(); + } + + result + } + + fn end_write(&mut self) -> Result<(), SPI::Error> { + let result = self.spi.flush(); + self.deselect(); + result + } + + fn write_color(&mut self, color: Rgb888) -> Result<(), SPI::Error> { + self.spi.write(&[color.r(), color.g(), color.b()]) + } + + fn abort_write(&mut self) { + self.deselect(); + } + + fn deselect(&mut self) { + self.cs.set_high().unwrap(); + self.delay.delay_ns(CS_HIGH_DELAY_NS); + } +} + +impl OriginDimensions for PicoCalcDisplay { + fn size(&self) -> Size { + Size::new(WIDTH as u32, HEIGHT as u32) + } +} + +impl DrawTarget for PicoCalcDisplay +where + SPI: SpiBus, + CS: OutputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + type Color = Rgb888; + type Error = SPI::Error; + + fn draw_iter(&mut self, pixels: I) -> Result<(), Self::Error> + where + I: IntoIterator>, + { + let mut selected = false; + let mut expected = None; + let mut window_start_x = 0; + + for Pixel(point, color) in pixels { + if point.x < 0 || point.y < 0 || point.x >= WIDTH as i32 || point.y >= HEIGHT as i32 { + continue; + } + + if expected != Some(point) { + if selected { + self.end_write()?; + } + + window_start_x = point.x; + self.begin_write(point.x as u16, point.y as u16, WIDTH - 1, HEIGHT - 1)?; + selected = true; + } + + if let Err(error) = self.write_color(color) { + self.abort_write(); + return Err(error); + } + + expected = if point.x < WIDTH as i32 - 1 { + Some(Point::new(point.x + 1, point.y)) + } else if point.y < HEIGHT as i32 - 1 { + Some(Point::new(window_start_x, point.y + 1)) + } else { + None + }; + } + + if selected { + self.end_write()?; + } + + Ok(()) + } + + fn fill_contiguous(&mut self, area: &Rectangle, colors: I) -> Result<(), Self::Error> + where + I: IntoIterator, + { + let drawable = area.intersection(&self.bounding_box()); + let Some(bottom_right) = drawable.bottom_right() else { + return Ok(()); + }; + + self.begin_write( + drawable.top_left.x as u16, + drawable.top_left.y as u16, + bottom_right.x as u16, + bottom_right.y as u16, + )?; + + for (point, color) in area.points().zip(colors) { + if drawable.contains(point) + && let Err(error) = self.write_color(color) + { + self.abort_write(); + return Err(error); + } + } + + self.end_write() + } + + fn fill_solid(&mut self, area: &Rectangle, color: Self::Color) -> Result<(), Self::Error> { + const CHUNK_PIXELS: usize = 32; + + let drawable = area.intersection(&self.bounding_box()); + let Some(bottom_right) = drawable.bottom_right() else { + return Ok(()); + }; + + self.begin_write( + drawable.top_left.x as u16, + drawable.top_left.y as u16, + bottom_right.x as u16, + bottom_right.y as u16, + )?; + + let mut buffer = [0; CHUNK_PIXELS * 3]; + for pixel in buffer.chunks_exact_mut(3) { + pixel.copy_from_slice(&[color.r(), color.g(), color.b()]); + } + + let pixel_count = drawable.size.width as usize * drawable.size.height as usize; + let full_chunks = pixel_count / CHUNK_PIXELS; + let remaining_bytes = pixel_count % CHUNK_PIXELS * 3; + + for _ in 0..full_chunks { + if let Err(error) = self.spi.write(&buffer) { + self.abort_write(); + return Err(error); + } + } + + if remaining_bytes != 0 + && let Err(error) = self.spi.write(&buffer[..remaining_bytes]) + { + self.abort_write(); + return Err(error); + } + + self.end_write() + } +} diff --git a/platforms/cmforth-picocalc/src/main.rs b/platforms/cmforth-picocalc/src/main.rs index 7b6ed69..915c4ac 100644 --- a/platforms/cmforth-picocalc/src/main.rs +++ b/platforms/cmforth-picocalc/src/main.rs @@ -1,21 +1,28 @@ -//! Blinks the LED on a Pico board -//! -//! This will blink an LED attached to GP25, which is the pin the Pico uses for the on-board LED. +//! PicoCalc display bring-up. #![no_std] #![no_main] +mod display; + use defmt::*; use defmt_rtt as _; -use embedded_hal::digital::OutputPin; +use display::PicoCalcDisplay; +use embedded_graphics::{ + mono_font::{MonoTextStyleBuilder, ascii::FONT_10X20}, + pixelcolor::Rgb888, + prelude::*, + primitives::{PrimitiveStyle, Rectangle}, + text::{Baseline, Text}, +}; +use embedded_hal::i2c::I2c; +use embedded_hal::spi::MODE_0; 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}; -// Provide an alias for our BSP so we can switch targets quickly. -// Uncomment the BSP you included in Cargo.toml, the rest of the code does not need to change. -// use some_bsp; - /// Tell the Boot ROM about our application #[unsafe(link_section = ".start_block")] #[used] @@ -25,7 +32,6 @@ pub static IMAGE_DEF: hal::block::ImageDef = hal::block::ImageDef::secure_exe(); fn main() -> ! { info!("Program start"); let mut pac = pac::Peripherals::take().unwrap(); - let core = cortex_m::Peripherals::take().unwrap(); let mut watchdog = hal::Watchdog::new(pac.WATCHDOG); let sio = hal::Sio::new(pac.SIO); @@ -43,7 +49,7 @@ fn main() -> ! { .ok() .unwrap(); - let mut delay = cortex_m::delay::Delay::new(core.SYST, clocks.system_clock.freq().to_Hz()); + let delay = hal::Timer::new_timer0(pac.TIMER0, &mut pac.RESETS, &clocks); let pins = hal::gpio::Pins::new( pac.IO_BANK0, @@ -52,24 +58,82 @@ fn main() -> ! { &mut pac.RESETS, ); - // This is the correct pin on the Raspberry Pico 2 board. On other boards, even if they have an - // on-board LED, it might need to be changed. - // - // Notably, on the Pico 2 W, the LED is not connected to any of the RP2350 GPIOs but to the cyw43 module instead. - // One way to do that is by using [embassy](https://github.com/embassy-rs/embassy/blob/main/examples/rp/src/bin/wifi_blinky.rs) - // - // If you have a Pico W and want to toggle a LED with a simple GPIO output pin, you can connect an external - // LED to one of the GPIO pins, and reference that pin here. Don't forget adding an appropriate resistor - // in series with the LED. - let mut led_pin = pins.gpio25.into_push_pull_output(); + // 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 backlight_i2c = hal::I2C::i2c1( + pac.I2C1, + pins.gpio6.reconfigure(), + pins.gpio7.reconfigure(), + 10_000u32.Hz(), + &mut pac.RESETS, + clocks.system_clock.freq(), + ); + if backlight_i2c.write(0x1f_u8, &[0x85, 0xf0]).is_err() { + warn!("Could not set LCD backlight"); + } + + // 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(), + 12_000_000u32.Hz(), + MODE_0, + ); + + let mut display = PicoCalcDisplay::new(spi, cs, dc, reset, delay); + display.init().unwrap(); + display.clear(Rgb888::WHITE).unwrap(); + + Rectangle::new(Point::new(8, 8), Size::new(304, 304)) + .into_styled(PrimitiveStyle::with_stroke(Rgb888::WHITE, 3)) + .draw(&mut display) + .unwrap(); + + for (x, color) in [Rgb888::RED, Rgb888::GREEN, Rgb888::BLUE] + .into_iter() + .enumerate() + { + Rectangle::new(Point::new(45 + x as i32 * 80, 75), Size::new(70, 50)) + .into_styled(PrimitiveStyle::with_fill(color)) + .draw(&mut display) + .unwrap(); + } + + let text_style = MonoTextStyleBuilder::new() + .font(&FONT_10X20) + .text_color(Rgb888::WHITE) + .background_color(Rgb888::BLACK) + .build(); + Text::with_baseline( + "Hello, PicoCalc!", + Point::new(80, 165), + text_style, + Baseline::Top, + ) + .draw(&mut display) + .unwrap(); + Text::with_baseline( + "cmforth is running", + Point::new(70, 195), + text_style, + Baseline::Top, + ) + .draw(&mut display) + .unwrap(); + + info!("Display initialized"); loop { - info!("on!"); - led_pin.set_high().unwrap(); - delay.delay_ms(500); - info!("off!"); - led_pin.set_low().unwrap(); - delay.delay_ms(500); + cortex_m::asm::wfi(); } } From 6b3f865191420cd3d567fc3f0094dbd403870694 Mon Sep 17 00:00:00 2001 From: Javier Alvarez Date: Tue, 21 Jul 2026 19:19:15 +0200 Subject: [PATCH 3/9] Add keyboard --- platforms/cmforth-picocalc/src/keyboard.rs | 62 ++++++++++++ platforms/cmforth-picocalc/src/main.rs | 110 ++++++++++++++++++++- 2 files changed, 167 insertions(+), 5 deletions(-) create mode 100644 platforms/cmforth-picocalc/src/keyboard.rs diff --git a/platforms/cmforth-picocalc/src/keyboard.rs b/platforms/cmforth-picocalc/src/keyboard.rs new file mode 100644 index 0000000..3e6ab9b --- /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; + } + Ok(Some(key)) + } + _ => Ok(None), + } + } +} diff --git a/platforms/cmforth-picocalc/src/main.rs b/platforms/cmforth-picocalc/src/main.rs index 915c4ac..be362a5 100644 --- a/platforms/cmforth-picocalc/src/main.rs +++ b/platforms/cmforth-picocalc/src/main.rs @@ -1,8 +1,9 @@ -//! PicoCalc display bring-up. +//! PicoCalc display and keyboard bring-up. #![no_std] #![no_main] mod display; +mod keyboard; use defmt::*; use defmt_rtt as _; @@ -16,6 +17,7 @@ use embedded_graphics::{ }; 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; @@ -61,7 +63,7 @@ fn main() -> ! { // 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 backlight_i2c = hal::I2C::i2c1( + let mut system_i2c = hal::I2C::i2c1( pac.I2C1, pins.gpio6.reconfigure(), pins.gpio7.reconfigure(), @@ -69,9 +71,10 @@ fn main() -> ! { &mut pac.RESETS, clocks.system_clock.freq(), ); - if backlight_i2c.write(0x1f_u8, &[0x85, 0xf0]).is_err() { + if system_i2c.write(0x1f_u8, &[0x85, 0xf0]).is_err() { warn!("Could not set LCD backlight"); } + let mut keyboard = PicoCalcKeyboard::new(system_i2c); // PicoCalc display: SPI1 SCK=GP10, MOSI=GP11, MISO=GP12, CS=GP13, // DC=GP14, RESET=GP15. @@ -130,10 +133,107 @@ fn main() -> ! { .draw(&mut display) .unwrap(); - info!("Display initialized"); + const INPUT_LEFT: i32 = 10; + const INPUT_TOP: i32 = 258; + const INPUT_BOTTOM: i32 = 308; + const CHARACTER_WIDTH: i32 = 10; + const LINE_HEIGHT: i32 = 20; + + Rectangle::new(Point::new(8, 230), Size::new(304, 82)) + .into_styled(PrimitiveStyle::with_fill(Rgb888::BLACK)) + .draw(&mut display) + .unwrap(); + Text::with_baseline( + "Keyboard input:", + Point::new(INPUT_LEFT, 234), + text_style, + Baseline::Top, + ) + .draw(&mut display) + .unwrap(); + + info!("Display and keyboard initialized"); + + let mut cursor = Point::new(INPUT_LEFT, INPUT_TOP); + let mut keyboard_error = false; loop { - cortex_m::asm::wfi(); + let key = match keyboard.read_key() { + Ok(key) => { + if keyboard_error { + info!("Keyboard communication restored"); + keyboard_error = false; + } + key + } + Err(_) => { + if !keyboard_error { + warn!("Could not read keyboard"); + keyboard_error = true; + } + None + } + }; + + let Some(key) = key else { + continue; + }; + debug!("Key pressed: {=u8}", key); + + match key { + b'\r' | b'\n' => { + cursor.x = INPUT_LEFT; + cursor.y += LINE_HEIGHT; + } + b'\x08' | b'\x7f' => { + if cursor.x > INPUT_LEFT { + cursor.x -= CHARACTER_WIDTH; + Rectangle::new( + cursor, + Size::new(CHARACTER_WIDTH as u32, LINE_HEIGHT as u32), + ) + .into_styled(PrimitiveStyle::with_fill(Rgb888::BLACK)) + .draw(&mut display) + .unwrap(); + } + } + b' '..=b'~' => { + if cursor.x + CHARACTER_WIDTH > 310 { + cursor.x = INPUT_LEFT; + cursor.y += LINE_HEIGHT; + } + + if cursor.y + LINE_HEIGHT > INPUT_BOTTOM { + Rectangle::new( + Point::new(INPUT_LEFT, INPUT_TOP), + Size::new(300, (INPUT_BOTTOM - INPUT_TOP) as u32), + ) + .into_styled(PrimitiveStyle::with_fill(Rgb888::BLACK)) + .draw(&mut display) + .unwrap(); + cursor = Point::new(INPUT_LEFT, INPUT_TOP); + } + + let bytes = [key]; + let character = core::str::from_utf8(&bytes).unwrap(); + Text::with_baseline(character, cursor, text_style, Baseline::Top) + .draw(&mut display) + .unwrap(); + cursor.x += CHARACTER_WIDTH; + } + _ => {} + } + + if cursor.y + LINE_HEIGHT > INPUT_BOTTOM { + Rectangle::new( + Point::new(INPUT_LEFT, INPUT_TOP), + Size::new(300, (INPUT_BOTTOM - INPUT_TOP) as u32), + ) + .into_styled(PrimitiveStyle::with_fill(Rgb888::BLACK)) + .draw(&mut display) + .unwrap(); + cursor = Point::new(INPUT_LEFT, INPUT_TOP); + } } } From c9d296a74b27d9e42248e20c75b4a521b895340e Mon Sep 17 00:00:00 2001 From: Javier Alvarez Date: Tue, 28 Jul 2026 19:42:57 +0200 Subject: [PATCH 4/9] Minimalistic forth demo --- Cargo.lock | 2 + platforms/cmforth-picocalc/Cargo.toml | 10 +- platforms/cmforth-picocalc/src/keyboard.rs | 2 +- platforms/cmforth-picocalc/src/main.rs | 191 +++++------------- platforms/cmforth-picocalc/src/teletype.rs | 221 +++++++++++++++++++++ 5 files changed, 275 insertions(+), 151 deletions(-) create mode 100644 platforms/cmforth-picocalc/src/teletype.rs diff --git a/Cargo.lock b/Cargo.lock index ba587ba..1416664 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -74,8 +74,10 @@ dependencies = [ "defmt-rtt", "embedded-graphics", "embedded-hal 1.0.0", + "heapless 0.9.3", "panic-probe", "rp235x-hal", + "static_cell", ] [[package]] diff --git a/platforms/cmforth-picocalc/Cargo.toml b/platforms/cmforth-picocalc/Cargo.toml index 86a93b6..af5c871 100644 --- a/platforms/cmforth-picocalc/Cargo.toml +++ b/platforms/cmforth-picocalc/Cargo.toml @@ -6,17 +6,17 @@ license = "MIT OR Apache-2.0" [dependencies] cmforth.workspace = true -cortex-m.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 - -defmt.workspace = true -defmt-rtt.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/src/keyboard.rs b/platforms/cmforth-picocalc/src/keyboard.rs index 3e6ab9b..c362d47 100644 --- a/platforms/cmforth-picocalc/src/keyboard.rs +++ b/platforms/cmforth-picocalc/src/keyboard.rs @@ -54,7 +54,7 @@ where if self.control_held && key.is_ascii_lowercase() { key = key - b'a' + 1; } - Ok(Some(key)) + 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 index be362a5..7d612c8 100644 --- a/platforms/cmforth-picocalc/src/main.rs +++ b/platforms/cmforth-picocalc/src/main.rs @@ -1,20 +1,21 @@ -//! PicoCalc display and keyboard bring-up. +//! cmforth interpreter for the PicoCalc. #![no_std] #![no_main] mod display; mod keyboard; +mod teletype; +use cmforth::{ + Forth, + io::{CombinedIo, StringReader}, + stack::{Stack, StackStorage}, + types::{Address, Word}, +}; use defmt::*; use defmt_rtt as _; use display::PicoCalcDisplay; -use embedded_graphics::{ - mono_font::{MonoTextStyleBuilder, ascii::FONT_10X20}, - pixelcolor::Rgb888, - prelude::*, - primitives::{PrimitiveStyle, Rectangle}, - text::{Baseline, Text}, -}; +use embedded_graphics::{pixelcolor::Rgb888, prelude::*}; use embedded_hal::i2c::I2c; use embedded_hal::spi::MODE_0; use keyboard::PicoCalcKeyboard; @@ -24,6 +25,17 @@ 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")] @@ -74,7 +86,7 @@ fn main() -> ! { if system_i2c.write(0x1f_u8, &[0x85, 0xf0]).is_err() { warn!("Could not set LCD backlight"); } - let mut keyboard = PicoCalcKeyboard::new(system_i2c); + let keyboard = PicoCalcKeyboard::new(system_i2c); // PicoCalc display: SPI1 SCK=GP10, MOSI=GP11, MISO=GP12, CS=GP13, // DC=GP14, RESET=GP15. @@ -94,145 +106,34 @@ fn main() -> ! { let mut display = PicoCalcDisplay::new(spi, cs, dc, reset, delay); display.init().unwrap(); - display.clear(Rgb888::WHITE).unwrap(); - - Rectangle::new(Point::new(8, 8), Size::new(304, 304)) - .into_styled(PrimitiveStyle::with_stroke(Rgb888::WHITE, 3)) - .draw(&mut display) - .unwrap(); - - for (x, color) in [Rgb888::RED, Rgb888::GREEN, Rgb888::BLUE] - .into_iter() - .enumerate() - { - Rectangle::new(Point::new(45 + x as i32 * 80, 75), Size::new(70, 50)) - .into_styled(PrimitiveStyle::with_fill(color)) - .draw(&mut display) - .unwrap(); - } - - let text_style = MonoTextStyleBuilder::new() - .font(&FONT_10X20) - .text_color(Rgb888::WHITE) - .background_color(Rgb888::BLACK) - .build(); - Text::with_baseline( - "Hello, PicoCalc!", - Point::new(80, 165), - text_style, - Baseline::Top, - ) - .draw(&mut display) - .unwrap(); - Text::with_baseline( - "cmforth is running", - Point::new(70, 195), - text_style, - Baseline::Top, - ) - .draw(&mut display) - .unwrap(); - - const INPUT_LEFT: i32 = 10; - const INPUT_TOP: i32 = 258; - const INPUT_BOTTOM: i32 = 308; - const CHARACTER_WIDTH: i32 = 10; - const LINE_HEIGHT: i32 = 20; - - Rectangle::new(Point::new(8, 230), Size::new(304, 82)) - .into_styled(PrimitiveStyle::with_fill(Rgb888::BLACK)) - .draw(&mut display) - .unwrap(); - Text::with_baseline( - "Keyboard input:", - Point::new(INPUT_LEFT, 234), - text_style, - Baseline::Top, - ) - .draw(&mut display) - .unwrap(); - - info!("Display and keyboard initialized"); + 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 mut cursor = Point::new(INPUT_LEFT, INPUT_TOP); - let mut keyboard_error = false; + 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 { - let key = match keyboard.read_key() { - Ok(key) => { - if keyboard_error { - info!("Keyboard communication restored"); - keyboard_error = false; - } - key - } - Err(_) => { - if !keyboard_error { - warn!("Could not read keyboard"); - keyboard_error = true; - } - None - } - }; - - let Some(key) = key else { - continue; - }; - debug!("Key pressed: {=u8}", key); - - match key { - b'\r' | b'\n' => { - cursor.x = INPUT_LEFT; - cursor.y += LINE_HEIGHT; - } - b'\x08' | b'\x7f' => { - if cursor.x > INPUT_LEFT { - cursor.x -= CHARACTER_WIDTH; - Rectangle::new( - cursor, - Size::new(CHARACTER_WIDTH as u32, LINE_HEIGHT as u32), - ) - .into_styled(PrimitiveStyle::with_fill(Rgb888::BLACK)) - .draw(&mut display) - .unwrap(); - } - } - b' '..=b'~' => { - if cursor.x + CHARACTER_WIDTH > 310 { - cursor.x = INPUT_LEFT; - cursor.y += LINE_HEIGHT; - } - - if cursor.y + LINE_HEIGHT > INPUT_BOTTOM { - Rectangle::new( - Point::new(INPUT_LEFT, INPUT_TOP), - Size::new(300, (INPUT_BOTTOM - INPUT_TOP) as u32), - ) - .into_styled(PrimitiveStyle::with_fill(Rgb888::BLACK)) - .draw(&mut display) - .unwrap(); - cursor = Point::new(INPUT_LEFT, INPUT_TOP); - } - - let bytes = [key]; - let character = core::str::from_utf8(&bytes).unwrap(); - Text::with_baseline(character, cursor, text_style, Baseline::Top) - .draw(&mut display) - .unwrap(); - cursor.x += CHARACTER_WIDTH; - } - _ => {} - } - - if cursor.y + LINE_HEIGHT > INPUT_BOTTOM { - Rectangle::new( - Point::new(INPUT_LEFT, INPUT_TOP), - Size::new(300, (INPUT_BOTTOM - INPUT_TOP) as u32), - ) - .into_styled(PrimitiveStyle::with_fill(Rgb888::BLACK)) - .draw(&mut display) - .unwrap(); - cursor = Point::new(INPUT_LEFT, INPUT_TOP); + unsafe { + let _ = forth.run(&mut teletype).inspect_err(|err| { + use core::fmt::Write; + use core::write; + let mut string: heapless::String<256> = heapless::String::new(); + let _ = write!(string, "{err}"); + defmt::error!("Error running forth interpreter: {}", string.as_str()); + }); } } } diff --git a/platforms/cmforth-picocalc/src/teletype.rs b/platforms/cmforth-picocalc/src/teletype.rs new file mode 100644 index 0000000..c5866fd --- /dev/null +++ b/platforms/cmforth-picocalc/src/teletype.rs @@ -0,0 +1,221 @@ +use core::convert::Infallible; +use embedded_graphics::{ + mono_font::{MonoTextStyle, ascii::FONT_10X20}, + pixelcolor::Rgb888, + prelude::*, + text::{Baseline, Text}, +}; +use embedded_hal::{delay::DelayNs, digital::OutputPin, i2c::I2c, spi::SpiBus}; +use heapless::string::String; +use heapless::{Deque, Vec}; + +use crate::display::{HEIGHT, PicoCalcDisplay, WIDTH}; +use crate::keyboard::PicoCalcKeyboard; +use cmforth::io::{Reader, ReaderWriter, Writer}; + +const MAX_LINES: usize = HEIGHT as usize / FONT_10X20.character_size.height as usize; +const MAX_WIDTH: usize = + WIDTH as usize / (FONT_10X20.character_size.width + FONT_10X20.character_spacing) 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, + SPI: SpiBus, + CS: OutputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + 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.lines.push_back(String::new()).unwrap(); + } + + fn update_display(&mut self) { + let text_style = MonoTextStyle::new(&FONT_10X20, Rgb888::WHITE); + + self.display.clear(Rgb888::BLACK).unwrap(); + for (line_number, line) in self.lines.iter().enumerate() { + Text::with_baseline( + line, + Point::new( + 0, + line_number as i32 * FONT_10X20.character_size.height as i32, + ), + text_style, + Baseline::Top, + ) + .draw(&mut self.display) + .unwrap(); + } + } + + 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(); + + self.read_line.clear(); + self.read_index = 0; + self.update_display(); + + 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(); + } + } + b'\r' | b'\n' => { + self.newline(); + exit = true; + } + v => { + let last_line = self.lines.back_mut().unwrap(); + let printable = v.is_ascii_graphic() || v == b' '; + if !self.read_line.is_full() + && (!printable || last_line.len() < last_line.capacity()) + { + self.read_line.push(v).unwrap(); + if printable { + last_line.push(v as char).unwrap(); + } + } + } + } + self.update_display(); + } + } +} + +impl Reader for Teletype +where + I2C: I2c, + SPI: SpiBus, + CS: OutputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + 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, + SPI: SpiBus, + CS: OutputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + 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(); + } + self.lines.back_mut().unwrap().push(v as char).unwrap(); + } + _ => {} + } + } + self.update_display(); + } +} + +impl ReaderWriter for Teletype +where + I2C: I2c, + SPI: SpiBus, + CS: OutputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ +} From dacd1e86f7f752bada6971c2d607b91b16169e79 Mon Sep 17 00:00:00 2001 From: Javier Alvarez Date: Tue, 28 Jul 2026 22:51:06 +0200 Subject: [PATCH 5/9] Speed up display update --- platforms/cmforth-picocalc/src/display.rs | 87 +++++++++++++++-------- platforms/cmforth-picocalc/src/main.rs | 2 +- 2 files changed, 60 insertions(+), 29 deletions(-) diff --git a/platforms/cmforth-picocalc/src/display.rs b/platforms/cmforth-picocalc/src/display.rs index ac23c2c..aca2961 100644 --- a/platforms/cmforth-picocalc/src/display.rs +++ b/platforms/cmforth-picocalc/src/display.rs @@ -27,6 +27,7 @@ use core::convert::Infallible; use embedded_graphics::{pixelcolor::Rgb888, prelude::*, primitives::Rectangle}; use embedded_hal::{delay::DelayNs, digital::OutputPin, spi::SpiBus}; +use heapless::Vec; pub const WIDTH: u16 = 320; pub const HEIGHT: u16 = 320; @@ -35,6 +36,12 @@ const COLUMN_ADDRESS_SET: u8 = 0x2a; const PAGE_ADDRESS_SET: u8 = 0x2b; const MEMORY_WRITE: u8 = 0x2c; +// A display-width transfer buffer amortizes the SpiBus call overhead while +// keeping stack usage bounded. This matches the scanline-sized buffer used by +// ClockworkPi's reference driver. +const TRANSFER_PIXELS: usize = WIDTH as usize; +const TRANSFER_BYTES: usize = TRANSFER_PIXELS * 3; + // The ST7365P requires CS to remain high for at least 40 ns between // transactions. const CS_HIGH_DELAY_NS: u32 = 40; @@ -196,8 +203,16 @@ where result } - fn write_color(&mut self, color: Rgb888) -> Result<(), SPI::Error> { - self.spi.write(&[color.r(), color.g(), color.b()]) + fn write_pixels(&mut self, pixels: &[u8]) -> Result<(), SPI::Error> { + if pixels.is_empty() { + return Ok(()); + } + + let result = self.spi.write(pixels); + if result.is_err() { + self.abort_write(); + } + result } fn abort_write(&mut self) { @@ -231,9 +246,16 @@ where where I: IntoIterator>, { - let mut selected = false; + #[derive(PartialEq, Eq)] + enum DrawState { + Idle, + Writing, + } + + let mut state = DrawState::Idle; let mut expected = None; let mut window_start_x = 0; + let mut buffer: Vec = Vec::new(); for Pixel(point, color) in pixels { if point.x < 0 || point.y < 0 || point.x >= WIDTH as i32 || point.y >= HEIGHT as i32 { @@ -241,18 +263,25 @@ where } if expected != Some(point) { - if selected { + if state == DrawState::Writing { + self.write_pixels(buffer.as_slice())?; + buffer.clear(); self.end_write()?; } window_start_x = point.x; self.begin_write(point.x as u16, point.y as u16, WIDTH - 1, HEIGHT - 1)?; - selected = true; + state = DrawState::Writing; } - if let Err(error) = self.write_color(color) { - self.abort_write(); - return Err(error); + // The capacity is divisible by three and a full buffer is cleared + // after every RGB pixel, so these three bytes always fit. + buffer + .extend_from_slice(&[color.r(), color.g(), color.b()]) + .expect("Unable to extend buffer"); + if buffer.is_full() { + self.write_pixels(buffer.as_slice())?; + buffer.clear(); } expected = if point.x < WIDTH as i32 - 1 { @@ -264,7 +293,8 @@ where }; } - if selected { + if state == DrawState::Writing { + self.write_pixels(buffer.as_slice())?; self.end_write()?; } @@ -287,21 +317,28 @@ where bottom_right.y as u16, )?; + let mut buffer = [0; TRANSFER_BYTES]; + let mut buffered_bytes = 0; for (point, color) in area.points().zip(colors) { - if drawable.contains(point) - && let Err(error) = self.write_color(color) - { - self.abort_write(); - return Err(error); + if drawable.contains(point) { + buffer[buffered_bytes..buffered_bytes + 3].copy_from_slice(&[ + color.r(), + color.g(), + color.b(), + ]); + buffered_bytes += 3; + if buffered_bytes == buffer.len() { + self.write_pixels(&buffer)?; + buffered_bytes = 0; + } } } + self.write_pixels(&buffer[..buffered_bytes])?; self.end_write() } fn fill_solid(&mut self, area: &Rectangle, color: Self::Color) -> Result<(), Self::Error> { - const CHUNK_PIXELS: usize = 32; - let drawable = area.intersection(&self.bounding_box()); let Some(bottom_right) = drawable.bottom_right() else { return Ok(()); @@ -314,27 +351,21 @@ where bottom_right.y as u16, )?; - let mut buffer = [0; CHUNK_PIXELS * 3]; + let mut buffer = [0; TRANSFER_BYTES]; for pixel in buffer.chunks_exact_mut(3) { pixel.copy_from_slice(&[color.r(), color.g(), color.b()]); } let pixel_count = drawable.size.width as usize * drawable.size.height as usize; - let full_chunks = pixel_count / CHUNK_PIXELS; - let remaining_bytes = pixel_count % CHUNK_PIXELS * 3; + let full_chunks = pixel_count / TRANSFER_PIXELS; + let remaining_bytes = pixel_count % TRANSFER_PIXELS * 3; for _ in 0..full_chunks { - if let Err(error) = self.spi.write(&buffer) { - self.abort_write(); - return Err(error); - } + self.write_pixels(&buffer)?; } - if remaining_bytes != 0 - && let Err(error) = self.spi.write(&buffer[..remaining_bytes]) - { - self.abort_write(); - return Err(error); + if remaining_bytes != 0 { + self.write_pixels(&buffer[..remaining_bytes])?; } self.end_write() diff --git a/platforms/cmforth-picocalc/src/main.rs b/platforms/cmforth-picocalc/src/main.rs index 7d612c8..8fb01d4 100644 --- a/platforms/cmforth-picocalc/src/main.rs +++ b/platforms/cmforth-picocalc/src/main.rs @@ -100,7 +100,7 @@ fn main() -> ! { let spi = hal::Spi::<_, _, _, 8>::new(pac.SPI1, (mosi, miso, sck)).init( &mut pac.RESETS, clocks.peripheral_clock.freq(), - 12_000_000u32.Hz(), + 25_000_000u32.Hz(), MODE_0, ); From 0e01ec2e2b37c48f89fcc1b4089b95b6320a7b85 Mon Sep 17 00:00:00 2001 From: Javier Alvarez Date: Tue, 28 Jul 2026 22:56:59 +0200 Subject: [PATCH 6/9] Partial drawing when possible --- platforms/cmforth-picocalc/src/teletype.rs | 91 ++++++++++++++++------ 1 file changed, 69 insertions(+), 22 deletions(-) diff --git a/platforms/cmforth-picocalc/src/teletype.rs b/platforms/cmforth-picocalc/src/teletype.rs index c5866fd..a77fd46 100644 --- a/platforms/cmforth-picocalc/src/teletype.rs +++ b/platforms/cmforth-picocalc/src/teletype.rs @@ -1,6 +1,6 @@ use core::convert::Infallible; use embedded_graphics::{ - mono_font::{MonoTextStyle, ascii::FONT_10X20}, + mono_font::{MonoTextStyleBuilder, ascii::FONT_6X12}, pixelcolor::Rgb888, prelude::*, text::{Baseline, Text}, @@ -13,9 +13,10 @@ use crate::display::{HEIGHT, PicoCalcDisplay, WIDTH}; use crate::keyboard::PicoCalcKeyboard; use cmforth::io::{Reader, ReaderWriter, Writer}; -const MAX_LINES: usize = HEIGHT as usize / FONT_10X20.character_size.height as usize; -const MAX_WIDTH: usize = - WIDTH as usize / (FONT_10X20.character_size.width + FONT_10X20.character_spacing) as usize; +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, @@ -57,24 +58,28 @@ where } } - fn newline(&mut self) { - if self.lines.is_full() { + /// Adds a line and reports whether the visible contents scrolled. + fn newline(&mut self) -> bool { + let scrolled = self.lines.is_full(); + if scrolled { self.lines.pop_front(); } self.lines.push_back(String::new()).unwrap(); + scrolled } fn update_display(&mut self) { - let text_style = MonoTextStyle::new(&FONT_10X20, Rgb888::WHITE); + let text_style = MonoTextStyleBuilder::new() + .font(&FONT_6X12) + .text_color(Rgb888::WHITE) + .background_color(Rgb888::BLACK) + .build(); self.display.clear(Rgb888::BLACK).unwrap(); for (line_number, line) in self.lines.iter().enumerate() { Text::with_baseline( line, - Point::new( - 0, - line_number as i32 * FONT_10X20.character_size.height as i32, - ), + Point::new(0, line_number as i32 * CHARACTER_HEIGHT as i32), text_style, Baseline::Top, ) @@ -83,16 +88,43 @@ where } } + 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(); + if self.lines.back().is_none_or(|line| !line.is_empty()) && self.newline() { + self.update_display(); } 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; - self.update_display(); let mut exit = false; while !exit { @@ -106,26 +138,34 @@ where { 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(); + if self.newline() { + self.update_display(); + } exit = true; } v => { - let last_line = self.lines.back_mut().unwrap(); let printable = v.is_ascii_graphic() || v == b' '; if !self.read_line.is_full() - && (!printable || last_line.len() < last_line.capacity()) + && (!printable + || self.lines.back().unwrap().len() + < self.lines.back().unwrap().capacity()) { self.read_line.push(v).unwrap(); if printable { - last_line.push(v as char).unwrap(); + 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); } } } } - self.update_display(); } } } @@ -190,22 +230,29 @@ where fn write(&mut self, data: &[u8]) { for &v in data { match v { - b'\n' => self.newline(), + b'\n' => { + if self.newline() { + self.update_display(); + } + } b'\r' => {} b' '..=b'~' => { if self .lines .back() .is_none_or(|line| line.len() == line.capacity()) + && self.newline() { - self.newline(); + self.update_display(); } + 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); } _ => {} } } - self.update_display(); } } From db0fdb6143e37a35b7b846cce30c1a3ae8a9e8f1 Mon Sep 17 00:00:00 2001 From: Javier Alvarez Date: Wed, 29 Jul 2026 23:06:00 +0200 Subject: [PATCH 7/9] Handle scrolling --- platforms/cmforth-picocalc/src/display.rs | 154 +++++++++++++++++---- platforms/cmforth-picocalc/src/teletype.rs | 47 ++----- 2 files changed, 139 insertions(+), 62 deletions(-) diff --git a/platforms/cmforth-picocalc/src/display.rs b/platforms/cmforth-picocalc/src/display.rs index aca2961..46acc07 100644 --- a/platforms/cmforth-picocalc/src/display.rs +++ b/platforms/cmforth-picocalc/src/display.rs @@ -32,9 +32,12 @@ use heapless::Vec; pub const WIDTH: u16 = 320; pub const HEIGHT: u16 = 320; +const GRAM_HEIGHT: u16 = 480; const COLUMN_ADDRESS_SET: u8 = 0x2a; const PAGE_ADDRESS_SET: u8 = 0x2b; const MEMORY_WRITE: u8 = 0x2c; +const VERTICAL_SCROLLING_DEFINITION: u8 = 0x33; +const VERTICAL_SCROLLING_START_ADDRESS: u8 = 0x37; // A display-width transfer buffer amortizes the SpiBus call overhead while // keeping stack usage bounded. This matches the scanline-sized buffer used by @@ -53,6 +56,7 @@ pub struct PicoCalcDisplay { dc: DC, reset: RST, delay: DELAY, + scroll_offset: u16, } impl PicoCalcDisplay @@ -70,6 +74,7 @@ where dc, reset, delay, + scroll_offset: 0, } } @@ -119,9 +124,63 @@ where self.delay.delay_ms(120); self.command(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.command( + 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<(), SPI::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<(), SPI::Error> { + self.command(VERTICAL_SCROLLING_START_ADDRESS, &offset.to_be_bytes()) + } + + fn physical_y(&self, logical_y: u16) -> u16 { + (self.scroll_offset + logical_y) % GRAM_HEIGHT + } + fn write_init(&mut self, data: bool, bytes: &[u8]) -> Result<(), SPI::Error> { if data { self.dc.set_high().unwrap(); @@ -215,6 +274,36 @@ where result } + fn fill_solid_physical( + &mut self, + x_start: u16, + y_start: u16, + x_end: u16, + y_end: u16, + color: Rgb888, + ) -> Result<(), SPI::Error> { + self.begin_write(x_start, y_start, x_end, y_end)?; + + let mut buffer = [0; TRANSFER_BYTES]; + for pixel in buffer.chunks_exact_mut(3) { + pixel.copy_from_slice(&[color.r(), color.g(), color.b()]); + } + + let pixel_count = (x_end - x_start + 1) as usize * (y_end - y_start + 1) as usize; + let full_chunks = pixel_count / TRANSFER_PIXELS; + let remaining_bytes = pixel_count % TRANSFER_PIXELS * 3; + + for _ in 0..full_chunks { + self.write_pixels(&buffer)?; + } + + if remaining_bytes != 0 { + self.write_pixels(&buffer[..remaining_bytes])?; + } + + self.end_write() + } + fn abort_write(&mut self) { self.deselect(); } @@ -261,6 +350,7 @@ where if point.x < 0 || point.y < 0 || point.x >= WIDTH as i32 || point.y >= HEIGHT as i32 { continue; } + let physical_y = self.physical_y(point.y as u16); if expected != Some(point) { if state == DrawState::Writing { @@ -270,7 +360,7 @@ where } window_start_x = point.x; - self.begin_write(point.x as u16, point.y as u16, WIDTH - 1, HEIGHT - 1)?; + self.begin_write(point.x as u16, physical_y, WIDTH - 1, GRAM_HEIGHT - 1)?; state = DrawState::Writing; } @@ -286,7 +376,7 @@ where expected = if point.x < WIDTH as i32 - 1 { Some(Point::new(point.x + 1, point.y)) - } else if point.y < HEIGHT as i32 - 1 { + } else if point.y < HEIGHT as i32 - 1 && physical_y < GRAM_HEIGHT - 1 { Some(Point::new(window_start_x, point.y + 1)) } else { None @@ -310,17 +400,36 @@ where return Ok(()); }; + let height = drawable.size.height as u16; + let physical_y = self.physical_y(drawable.top_left.y as u16); + let first_height = height.min(GRAM_HEIGHT - physical_y); self.begin_write( drawable.top_left.x as u16, - drawable.top_left.y as u16, + physical_y, bottom_right.x as u16, - bottom_right.y as u16, + physical_y + first_height - 1, )?; + let wraps = first_height < height; + let wrap_y = drawable.top_left.y + first_height as i32; + let mut wrapped = false; let mut buffer = [0; TRANSFER_BYTES]; let mut buffered_bytes = 0; for (point, color) in area.points().zip(colors) { if drawable.contains(point) { + if wraps && !wrapped && point.y >= wrap_y { + self.write_pixels(&buffer[..buffered_bytes])?; + buffered_bytes = 0; + self.end_write()?; + self.begin_write( + drawable.top_left.x as u16, + 0, + bottom_right.x as u16, + height - first_height - 1, + )?; + wrapped = true; + } + buffer[buffered_bytes..buffered_bytes + 3].copy_from_slice(&[ color.r(), color.g(), @@ -344,30 +453,25 @@ where return Ok(()); }; - self.begin_write( - drawable.top_left.x as u16, - drawable.top_left.y as u16, - bottom_right.x as u16, - bottom_right.y as u16, + let x_start = drawable.top_left.x as u16; + let x_end = bottom_right.x as u16; + let height = drawable.size.height as u16; + let physical_y = self.physical_y(drawable.top_left.y as u16); + let first_height = height.min(GRAM_HEIGHT - physical_y); + + self.fill_solid_physical( + x_start, + physical_y, + x_end, + physical_y + first_height - 1, + color, )?; - let mut buffer = [0; TRANSFER_BYTES]; - for pixel in buffer.chunks_exact_mut(3) { - pixel.copy_from_slice(&[color.r(), color.g(), color.b()]); + let remaining_height = height - first_height; + if remaining_height != 0 { + self.fill_solid_physical(x_start, 0, x_end, remaining_height - 1, color)?; } - let pixel_count = drawable.size.width as usize * drawable.size.height as usize; - let full_chunks = pixel_count / TRANSFER_PIXELS; - let remaining_bytes = pixel_count % TRANSFER_PIXELS * 3; - - for _ in 0..full_chunks { - self.write_pixels(&buffer)?; - } - - if remaining_bytes != 0 { - self.write_pixels(&buffer[..remaining_bytes])?; - } - - self.end_write() + Ok(()) } } diff --git a/platforms/cmforth-picocalc/src/teletype.rs b/platforms/cmforth-picocalc/src/teletype.rs index a77fd46..2ba58de 100644 --- a/platforms/cmforth-picocalc/src/teletype.rs +++ b/platforms/cmforth-picocalc/src/teletype.rs @@ -58,34 +58,14 @@ where } } - /// Adds a line and reports whether the visible contents scrolled. - fn newline(&mut self) -> bool { - let scrolled = self.lines.is_full(); - if scrolled { + 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(); - scrolled - } - - fn update_display(&mut self) { - let text_style = MonoTextStyleBuilder::new() - .font(&FONT_6X12) - .text_color(Rgb888::WHITE) - .background_color(Rgb888::BLACK) - .build(); - - self.display.clear(Rgb888::BLACK).unwrap(); - for (line_number, line) in self.lines.iter().enumerate() { - Text::with_baseline( - line, - Point::new(0, line_number as i32 * CHARACTER_HEIGHT as i32), - text_style, - Baseline::Top, - ) - .draw(&mut self.display) - .unwrap(); - } } fn update_text(&mut self, line: usize, column: usize, text: &str) { @@ -115,8 +95,8 @@ where } fn buffer_line(&mut self) { - if self.lines.back().is_none_or(|line| !line.is_empty()) && self.newline() { - self.update_display(); + 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(); @@ -144,9 +124,7 @@ where } } b'\r' | b'\n' => { - if self.newline() { - self.update_display(); - } + self.newline(); exit = true; } v => { @@ -230,20 +208,15 @@ where fn write(&mut self, data: &[u8]) { for &v in data { match v { - b'\n' => { - if self.newline() { - self.update_display(); - } - } + b'\n' => self.newline(), b'\r' => {} b' '..=b'~' => { if self .lines .back() .is_none_or(|line| line.len() == line.capacity()) - && self.newline() { - self.update_display(); + self.newline(); } let line_number = self.lines.len() - 1; let column = self.lines.back().unwrap().len(); From 66221ad559e51882678d77f16690f51bf98b1694 Mon Sep 17 00:00:00 2001 From: Javier Alvarez Date: Thu, 30 Jul 2026 19:42:09 +0200 Subject: [PATCH 8/9] Separation of responsibilities in display driver --- platforms/cmforth-picocalc/src/display.rs | 416 ++++-------------- .../cmforth-picocalc/src/display/control.rs | 257 +++++++++++ .../cmforth-picocalc/src/display/traits.rs | 79 ++++ platforms/cmforth-picocalc/src/teletype.rs | 48 +- 4 files changed, 432 insertions(+), 368 deletions(-) create mode 100644 platforms/cmforth-picocalc/src/display/control.rs create mode 100644 platforms/cmforth-picocalc/src/display/traits.rs diff --git a/platforms/cmforth-picocalc/src/display.rs b/platforms/cmforth-picocalc/src/display.rs index 46acc07..041291b 100644 --- a/platforms/cmforth-picocalc/src/display.rs +++ b/platforms/cmforth-picocalc/src/display.rs @@ -25,41 +25,28 @@ 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}; -use heapless::Vec; +pub use traits::{BatchedPixelWriteOp, DisplayControls}; pub const WIDTH: u16 = 320; pub const HEIGHT: u16 = 320; const GRAM_HEIGHT: u16 = 480; -const COLUMN_ADDRESS_SET: u8 = 0x2a; -const PAGE_ADDRESS_SET: u8 = 0x2b; -const MEMORY_WRITE: u8 = 0x2c; const VERTICAL_SCROLLING_DEFINITION: u8 = 0x33; const VERTICAL_SCROLLING_START_ADDRESS: u8 = 0x37; -// A display-width transfer buffer amortizes the SpiBus call overhead while -// keeping stack usage bounded. This matches the scanline-sized buffer used by -// ClockworkPi's reference driver. -const TRANSFER_PIXELS: usize = WIDTH as usize; -const TRANSFER_BYTES: usize = TRANSFER_PIXELS * 3; - -// The ST7365P requires CS to remain high for at least 40 ns between -// transactions. -const CS_HIGH_DELAY_NS: u32 = 40; - -/// Write-only driver for the PicoCalc's ILI9488 display. -pub struct PicoCalcDisplay { - spi: SPI, - cs: CS, - dc: DC, - reset: RST, - delay: DELAY, +/// Write-only driver for the PicoCalc display. +pub struct PicoCalcDisplay { + controls: C, scroll_offset: u16, } -impl PicoCalcDisplay +impl PicoCalcDisplay> where SPI: SpiBus, CS: OutputPin, @@ -69,66 +56,59 @@ where { pub fn new(spi: SPI, cs: CS, dc: DC, reset: RST, delay: DELAY) -> Self { Self { - spi, - cs, - dc, - reset, - delay, + 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<(), SPI::Error> { - 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); + pub fn init(&mut self) -> Result<(), C::Error> { + self.controls.hardware_reset(); - self.command( + self.controls.send_command_with_data( 0xe0, &[ 0x00, 0x03, 0x09, 0x08, 0x16, 0x0a, 0x3f, 0x78, 0x4c, 0x09, 0x0a, 0x08, 0x16, 0x1a, 0x0f, ], )?; - self.command( + self.controls.send_command_with_data( 0xe1, &[ 0x00, 0x16, 0x19, 0x03, 0x0f, 0x05, 0x32, 0x45, 0x46, 0x04, 0x0e, 0x0d, 0x35, 0x37, 0x0f, ], )?; - self.command(0xc0, &[0x17, 0x15])?; - self.command(0xc1, &[0x41])?; - self.command(0xc5, &[0x00, 0x12, 0x80])?; - self.command(0x36, &[0x48])?; // MX and BGR order - self.command(0x3a, &[0x66])?; // 18-bit RGB over SPI - self.command(0xb0, &[0x00])?; - self.command(0xb1, &[0xa0])?; - self.command(0x21, &[])?; // Display inversion on - self.command(0xb4, &[0x02])?; - self.command(0xb6, &[0x02, 0x02, 0x3b])?; - self.command(0xb7, &[0xc6])?; - self.command(0xe9, &[0x00])?; - self.command(0xf7, &[0xa9, 0x51, 0x2c, 0x82])?; - - self.command(0x11, &[])?; // Sleep out - self.delay.delay_ms(120); - self.command(0x29, &[])?; // Display on - self.delay.delay_ms(120); - self.command(0x36, &[0x48])?; + 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.command( + self.controls.send_command_with_data( VERTICAL_SCROLLING_DEFINITION, &[ 0x00, @@ -146,7 +126,7 @@ where } /// Scrolls the visible viewport up and clears the newly exposed rows. - pub(crate) fn scroll_up(&mut self, rows: u16, color: Rgb888) -> Result<(), SPI::Error> { + pub(crate) fn scroll_up(&mut self, rows: u16, color: Rgb888) -> Result<(), C::Error> { if rows == 0 { return Ok(()); } @@ -173,303 +153,71 @@ where Ok(()) } - fn write_scroll_offset(&mut self, offset: u16) -> Result<(), SPI::Error> { - self.command(VERTICAL_SCROLLING_START_ADDRESS, &offset.to_be_bytes()) - } - - fn physical_y(&self, logical_y: u16) -> u16 { - (self.scroll_offset + logical_y) % GRAM_HEIGHT - } - - fn write_init(&mut self, data: bool, bytes: &[u8]) -> Result<(), SPI::Error> { - if data { - self.dc.set_high().unwrap(); - } else { - self.dc.set_low().unwrap(); - } - - self.cs.set_low().unwrap(); - let result = self.spi.write(bytes).and_then(|()| self.spi.flush()); - self.deselect(); - result - } - - fn command(&mut self, command: u8, data: &[u8]) -> Result<(), SPI::Error> { - // Keep command and data in separate transactions so CS returns high - // before D/C changes, but send all parameter bytes in one transfer. - self.write_init(false, &[command])?; - if !data.is_empty() { - self.write_init(true, data)?; - } - Ok(()) - } - - fn begin_write( - &mut self, - x_start: u16, - y_start: u16, - x_end: u16, - y_end: u16, - ) -> Result<(), SPI::Error> { - 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, - ]; - - self.cs.set_low().unwrap(); - - let result = (|| -> Result<(), SPI::Error> { - self.dc.set_low().unwrap(); - self.spi.write(&[COLUMN_ADDRESS_SET])?; - self.spi.flush()?; - self.dc.set_high().unwrap(); - self.spi.write(&columns)?; - self.spi.flush()?; - - self.dc.set_low().unwrap(); - self.spi.write(&[PAGE_ADDRESS_SET])?; - self.spi.flush()?; - self.dc.set_high().unwrap(); - self.spi.write(&pages)?; - self.spi.flush()?; - - self.dc.set_low().unwrap(); - self.spi.write(&[MEMORY_WRITE])?; - self.spi.flush()?; - self.dc.set_high().unwrap(); - - Ok(()) - })(); - - if result.is_err() { - self.deselect(); - } - - result - } - - fn end_write(&mut self) -> Result<(), SPI::Error> { - let result = self.spi.flush(); - self.deselect(); - result - } - - fn write_pixels(&mut self, pixels: &[u8]) -> Result<(), SPI::Error> { - if pixels.is_empty() { - return Ok(()); - } - - let result = self.spi.write(pixels); - if result.is_err() { - self.abort_write(); - } - result - } - - fn fill_solid_physical( - &mut self, - x_start: u16, - y_start: u16, - x_end: u16, - y_end: u16, - color: Rgb888, - ) -> Result<(), SPI::Error> { - self.begin_write(x_start, y_start, x_end, y_end)?; - - let mut buffer = [0; TRANSFER_BYTES]; - for pixel in buffer.chunks_exact_mut(3) { - pixel.copy_from_slice(&[color.r(), color.g(), color.b()]); - } - - let pixel_count = (x_end - x_start + 1) as usize * (y_end - y_start + 1) as usize; - let full_chunks = pixel_count / TRANSFER_PIXELS; - let remaining_bytes = pixel_count % TRANSFER_PIXELS * 3; - - for _ in 0..full_chunks { - self.write_pixels(&buffer)?; - } - - if remaining_bytes != 0 { - self.write_pixels(&buffer[..remaining_bytes])?; - } - - self.end_write() - } - - fn abort_write(&mut self) { - self.deselect(); - } - - fn deselect(&mut self) { - self.cs.set_high().unwrap(); - self.delay.delay_ns(CS_HIGH_DELAY_NS); + 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 { +impl OriginDimensions for PicoCalcDisplay { fn size(&self) -> Size { Size::new(WIDTH as u32, HEIGHT as u32) } } -impl DrawTarget for PicoCalcDisplay -where - SPI: SpiBus, - CS: OutputPin, - DC: OutputPin, - RST: OutputPin, - DELAY: DelayNs, -{ +impl DrawTarget for PicoCalcDisplay { type Color = Rgb888; - type Error = SPI::Error; + type Error = C::Error; fn draw_iter(&mut self, pixels: I) -> Result<(), Self::Error> where I: IntoIterator>, { - #[derive(PartialEq, Eq)] - enum DrawState { - Idle, - Writing, - } - - let mut state = DrawState::Idle; - let mut expected = None; - let mut window_start_x = 0; - let mut buffer: Vec = Vec::new(); - - for Pixel(point, color) in pixels { - if point.x < 0 || point.y < 0 || point.x >= WIDTH as i32 || point.y >= HEIGHT as i32 { + 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 = self.physical_y(point.y as u16); - if expected != Some(point) { - if state == DrawState::Writing { - self.write_pixels(buffer.as_slice())?; - buffer.clear(); - self.end_write()?; + 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; } - window_start_x = point.x; - self.begin_write(point.x as u16, physical_y, WIDTH - 1, GRAM_HEIGHT - 1)?; - state = DrawState::Writing; - } - - // The capacity is divisible by three and a full buffer is cleared - // after every RGB pixel, so these three bytes always fit. - buffer - .extend_from_slice(&[color.r(), color.g(), color.b()]) - .expect("Unable to extend buffer"); - if buffer.is_full() { - self.write_pixels(buffer.as_slice())?; - buffer.clear(); - } - - expected = if point.x < WIDTH as i32 - 1 { - Some(Point::new(point.x + 1, point.y)) - } else if point.y < HEIGHT as i32 - 1 && physical_y < GRAM_HEIGHT - 1 { - Some(Point::new(window_start_x, point.y + 1)) - } else { - None - }; - } - - if state == DrawState::Writing { - self.write_pixels(buffer.as_slice())?; - self.end_write()?; - } + op.push_pixel(color)?; - Ok(()) - } + 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 + }; - fn fill_contiguous(&mut self, area: &Rectangle, colors: I) -> Result<(), Self::Error> - where - I: IntoIterator, - { - let drawable = area.intersection(&self.bounding_box()); - let Some(bottom_right) = drawable.bottom_right() else { - return Ok(()); - }; - - let height = drawable.size.height as u16; - let physical_y = self.physical_y(drawable.top_left.y as u16); - let first_height = height.min(GRAM_HEIGHT - physical_y); - self.begin_write( - drawable.top_left.x as u16, - physical_y, - bottom_right.x as u16, - physical_y + first_height - 1, - )?; - - let wraps = first_height < height; - let wrap_y = drawable.top_left.y + first_height as i32; - let mut wrapped = false; - let mut buffer = [0; TRANSFER_BYTES]; - let mut buffered_bytes = 0; - for (point, color) in area.points().zip(colors) { - if drawable.contains(point) { - if wraps && !wrapped && point.y >= wrap_y { - self.write_pixels(&buffer[..buffered_bytes])?; - buffered_bytes = 0; - self.end_write()?; - self.begin_write( - drawable.top_left.x as u16, - 0, - bottom_right.x as u16, - height - first_height - 1, - )?; - wrapped = true; - } - - buffer[buffered_bytes..buffered_bytes + 3].copy_from_slice(&[ - color.r(), - color.g(), - color.b(), - ]); - buffered_bytes += 3; - if buffered_bytes == buffer.len() { - self.write_pixels(&buffer)?; - buffered_bytes = 0; - } + iter.next(); } - } - - self.write_pixels(&buffer[..buffered_bytes])?; - self.end_write() - } - - fn fill_solid(&mut self, area: &Rectangle, color: Self::Color) -> Result<(), Self::Error> { - let drawable = area.intersection(&self.bounding_box()); - let Some(bottom_right) = drawable.bottom_right() else { - return Ok(()); - }; - - let x_start = drawable.top_left.x as u16; - let x_end = bottom_right.x as u16; - let height = drawable.size.height as u16; - let physical_y = self.physical_y(drawable.top_left.y as u16); - let first_height = height.min(GRAM_HEIGHT - physical_y); - - self.fill_solid_physical( - x_start, - physical_y, - x_end, - physical_y + first_height - 1, - color, - )?; - let remaining_height = height - first_height; - if remaining_height != 0 { - self.fill_solid_physical(x_start, 0, x_end, remaining_height - 1, color)?; + 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/teletype.rs b/platforms/cmforth-picocalc/src/teletype.rs index 2ba58de..170c924 100644 --- a/platforms/cmforth-picocalc/src/teletype.rs +++ b/platforms/cmforth-picocalc/src/teletype.rs @@ -1,15 +1,14 @@ -use core::convert::Infallible; use embedded_graphics::{ mono_font::{MonoTextStyleBuilder, ascii::FONT_6X12}, pixelcolor::Rgb888, prelude::*, text::{Baseline, Text}, }; -use embedded_hal::{delay::DelayNs, digital::OutputPin, i2c::I2c, spi::SpiBus}; +use embedded_hal::i2c::I2c; use heapless::string::String; use heapless::{Deque, Vec}; -use crate::display::{HEIGHT, PicoCalcDisplay, WIDTH}; +use crate::display::{DisplayControls, HEIGHT, PicoCalcDisplay, WIDTH}; use crate::keyboard::PicoCalcKeyboard; use cmforth::io::{Reader, ReaderWriter, Writer}; @@ -18,19 +17,16 @@ 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 { +pub struct Teletype { read_line: Vec, read_index: usize, lines: Deque, MAX_LINES>, keyboard: PicoCalcKeyboard, - display: PicoCalcDisplay, + display: PicoCalcDisplay, } -impl Teletype { - pub fn new( - keyboard: PicoCalcKeyboard, - display: PicoCalcDisplay, - ) -> Self { +impl Teletype { + pub fn new(keyboard: PicoCalcKeyboard, display: PicoCalcDisplay) -> Self { Self { read_line: Vec::new(), read_index: 0, @@ -41,14 +37,10 @@ impl Teletype { } } -impl Teletype +impl Teletype where I2C: I2c, - SPI: SpiBus, - CS: OutputPin, - DC: OutputPin, - RST: OutputPin, - DELAY: DelayNs, + C: DisplayControls, { fn read_key(&mut self) -> u8 { loop { @@ -148,14 +140,10 @@ where } } -impl Reader for Teletype +impl Reader for Teletype where I2C: I2c, - SPI: SpiBus, - CS: OutputPin, - DC: OutputPin, - RST: OutputPin, - DELAY: DelayNs, + C: DisplayControls, { fn read(&mut self) -> u8 { while self.read_index >= self.read_line.len() { @@ -196,14 +184,10 @@ where } } -impl Writer for Teletype +impl Writer for Teletype where I2C: I2c, - SPI: SpiBus, - CS: OutputPin, - DC: OutputPin, - RST: OutputPin, - DELAY: DelayNs, + C: DisplayControls, { fn write(&mut self, data: &[u8]) { for &v in data { @@ -229,13 +213,9 @@ where } } -impl ReaderWriter for Teletype +impl ReaderWriter for Teletype where I2C: I2c, - SPI: SpiBus, - CS: OutputPin, - DC: OutputPin, - RST: OutputPin, - DELAY: DelayNs, + C: DisplayControls, { } From 2abc3f08a3d8aba0b110578746dbfa0c6b1ac4b9 Mon Sep 17 00:00:00 2001 From: Javier Alvarez Date: Thu, 30 Jul 2026 19:47:10 +0200 Subject: [PATCH 9/9] Display errors in teletype --- platforms/cmforth-picocalc/src/main.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/platforms/cmforth-picocalc/src/main.rs b/platforms/cmforth-picocalc/src/main.rs index 8fb01d4..76a18c3 100644 --- a/platforms/cmforth-picocalc/src/main.rs +++ b/platforms/cmforth-picocalc/src/main.rs @@ -8,10 +8,11 @@ mod teletype; use cmforth::{ Forth, - io::{CombinedIo, StringReader}, + io::{CombinedIo, StringReader, Writer}, stack::{Stack, StackStorage}, types::{Address, Word}, }; +use core::fmt::Write; use defmt::*; use defmt_rtt as _; use display::PicoCalcDisplay; @@ -128,10 +129,9 @@ fn main() -> ! { loop { unsafe { let _ = forth.run(&mut teletype).inspect_err(|err| { - use core::fmt::Write; - use core::write; let mut string: heapless::String<256> = heapless::String::new(); - let _ = write!(string, "{err}"); + let _ = core::write!(string, "{err}"); + teletype.write(string.as_bytes()); defmt::error!("Error running forth interpreter: {}", string.as_str()); }); }