diff --git a/Cargo.lock b/Cargo.lock index 031c0f8..b034a16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -70,6 +70,16 @@ version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +[[package]] +name = "bitmask-enum" +version = "2.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6cbbb8f56245b5a479b30a62cdc86d26e2f35c2b9f594bc4671654b03851380" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -101,6 +111,8 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" name = "cardputer-bsc-nostd" version = "0.1.0" dependencies = [ + "bitflags", + "bitmask-enum", "embedded-hal-bus", "esp-hal", "mipidsi", @@ -784,6 +796,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "keyboard_test" +version = "0.1.0" +dependencies = [ + "cardputer-bsc-nostd", + "esp-bootloader-esp-idf", + "esp-hal", + "esp-println", +] + [[package]] name = "libc" version = "0.2.174" diff --git a/Cargo.toml b/Cargo.toml index cf9d368..2cb70eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,8 @@ debug = true # Symbols are nice and they don't increase the size on Flash opt-level = "z" [dependencies] +bitflags = "2.9.1" +bitmask-enum = "2.2.5" embedded-hal-bus = "0.3.0" esp-hal = {version = "=1.0.0-beta.1", features = ["esp32s3", "unstable"]} mipidsi = "0.9.0" diff --git a/examples/keyboard_test/Cargo.toml b/examples/keyboard_test/Cargo.toml new file mode 100644 index 0000000..90dcbb1 --- /dev/null +++ b/examples/keyboard_test/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "keyboard_test" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "keyboard_test" +harness = false # do not use the built in cargo test harness -> resolve rust-analyzer errors + +[profile.release] +opt-level = "s" +debug = true +panic = "abort" + +[profile.dev] +debug = true # Symbols are nice and they don't increase the size on Flash +opt-level = "z" +panic = "abort" + +[dependencies] +cardputer-bsc-nostd = { version = "0.1.0", path = "../.." } +esp-hal = {version = "=1.0.0-beta.1", features = ["esp32s3", "unstable"]} +esp-println = { version = "0.14.0", features = ["esp32s3"] } +esp-bootloader-esp-idf = "0.1.0" diff --git a/examples/keyboard_test/build.rs b/examples/keyboard_test/build.rs new file mode 100644 index 0000000..a76b496 --- /dev/null +++ b/examples/keyboard_test/build.rs @@ -0,0 +1,52 @@ +fn main() { + linker_be_nice(); + // make sure linkall.x is the last linker script (otherwise might cause problems with flip-link) + println!("cargo:rustc-link-arg=-Tlinkall.x"); +} + +fn linker_be_nice() { + let args: Vec = std::env::args().collect(); + if args.len() > 1 { + let kind = &args[1]; + let what = &args[2]; + + match kind.as_str() { + "undefined-symbol" => match what.as_str() { + "_defmt_timestamp" => { + eprintln!(); + eprintln!("💡 `defmt` not found - make sure `defmt.x` is added as a linker script and you have included `use defmt_rtt as _;`"); + eprintln!(); + } + "_stack_start" => { + eprintln!(); + eprintln!("💡 Is the linker script `linkall.x` missing?"); + eprintln!(); + } + "esp_wifi_preempt_enable" + | "esp_wifi_preempt_yield_task" + | "esp_wifi_preempt_task_create" => { + eprintln!(); + eprintln!("💡 `esp-wifi` has no scheduler enabled. Make sure you have the `builtin-scheduler` feature enabled, or that you provide an external scheduler."); + eprintln!(); + } + "embedded_test_linker_file_not_added_to_rustflags" => { + eprintln!(); + eprintln!("💡 `embedded-test` not found - make sure `embedded-test.x` is added as a linker script for tests"); + eprintln!(); + } + _ => (), + }, + // we don't have anything helpful for "missing-lib" yet + _ => { + std::process::exit(1); + } + } + + std::process::exit(0); + } + + println!( + "cargo:rustc-link-arg=-Wl,--error-handling-script={}", + std::env::current_exe().unwrap().display() + ); +} diff --git a/examples/keyboard_test/readme.md b/examples/keyboard_test/readme.md new file mode 100644 index 0000000..75aad77 --- /dev/null +++ b/examples/keyboard_test/readme.md @@ -0,0 +1,11 @@ +# keyboard_test example + +This is a simple test of the cardputer's keyboard. It logs key presses and releases over the USB serial. + +## Run this example + +With this repo cloned, run the following from either the root or this directory: + +```shell +cargo espflash flash --release --package keyboard_test --monitor +``` diff --git a/examples/keyboard_test/src/main.rs b/examples/keyboard_test/src/main.rs new file mode 100644 index 0000000..6a6203e --- /dev/null +++ b/examples/keyboard_test/src/main.rs @@ -0,0 +1,63 @@ +#![no_std] +#![no_main] + +use core::panic::PanicInfo; + +use esp_hal::main; +use esp_println::println; + +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + println!("{info}"); + esp_hal::system::software_reset() +} + +esp_bootloader_esp_idf::esp_app_desc!(); + +#[main] +fn entrypoint() -> ! { + _main(); + println!("main exited, entering infinite loop"); + loop {} +} + +#[allow(static_mut_refs)] +fn _main() { + let peripherals = esp_hal::init(esp_hal::Config::default()); + + let mut keyboard = cardputer_bsc_nostd::keyboard::Keyboard::new( + peripherals.GPIO8, + peripherals.GPIO9, + peripherals.GPIO11, + peripherals.GPIO13, + peripherals.GPIO15, + peripherals.GPIO3, + peripherals.GPIO4, + peripherals.GPIO5, + peripherals.GPIO6, + peripherals.GPIO7, + ); + + loop { + keyboard.scan(); + let key_set = keyboard.pressed_keys(); + if !key_set.is_none() { + for &(key_name, key) in cardputer_bsc_nostd::keyboard::Key::flags() { + if key_set.contains(key) { + println!("Pressed {key_name}"); + } + } + } + keyboard.clear_pressed_keys(); + let key_set = keyboard.released_keys(); + if !key_set.is_none() { + for &(key_name, key) in cardputer_bsc_nostd::keyboard::Key::flags() { + if key_set.contains(key) { + println!("Released {key_name}"); + } + } + } + keyboard.clear_released_keys(); + esp_hal::delay::Delay::new().delay_millis(10); + } +} diff --git a/src/display.rs b/src/display.rs index a0ce08a..9f690d6 100644 --- a/src/display.rs +++ b/src/display.rs @@ -1,6 +1,3 @@ -//! Create and initialize ST7789 display driver -// use display_interface_spi::SPIInterface; - use core::ops::{Deref, DerefMut}; use embedded_hal_bus::spi::ExclusiveDevice; diff --git a/src/keyboard.rs b/src/keyboard.rs index e69de29..fad3323 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -0,0 +1,293 @@ +use bitmask_enum::bitmask; + +/// A set of physical keys on the cardputer's keyboard +/// +/// This type is a bit flag (i.e. each bit corresponds to a different key). +#[bitmask(u64)] +#[bitmask_config(flags_iter)] +pub enum Key { + LeftOpt, + Z, + C, + B, + M, + Period, + Space, + _Dummy0, + LeftShift, + S, + F, + H, + K, + SemiColon, + Enter, + _Dummy1, + Q, + E, + T, + U, + O, + OpenSquareBracket, + Backslash, + _Dummy2, + One, + Three, + Five, + Seven, + Nine, + Minus, + Backspace, + _Dummy3, + LeftCtrl, + LeftAlt, + X, + V, + N, + Comma, + Slash, + _Dummy4, + LeftFn, + A, + D, + G, + J, + L, + Quote, + _Dummy5, + Tab, + W, + R, + Y, + I, + P, + CloseSquareBracket, + _Dummy6, + Backquote, + Two, + Four, + Six, + Eight, + Zero, + Equal, + _Dummy7, +} + +/// The type for controlling the cardputer's keyboard +/// +/// `Keyboard` tracks and buffers key presses and releases, +/// making it simple to react to only the changes in a key's state. +/// +/// Example usage: +/// ```rust +/// let mut keyboard = cardputer_bsc_nostd::keyboard::Keyboard::new( +/// peripherals.GPIO8, +/// peripherals.GPIO9, +/// peripherals.GPIO11, +/// peripherals.GPIO13, +/// peripherals.GPIO15, +/// peripherals.GPIO3, +/// peripherals.GPIO4, +/// peripherals.GPIO5, +/// peripherals.GPIO6, +/// peripherals.GPIO7, +/// ); +/// loop { +/// keyboard.scan(); +/// let key_set = keyboard.pressed_keys(); +/// if !key_set.is_none() { +/// for &(key_name, key) in cardputer_bsc_nostd::keyboard::Key::flags() { +/// if key_set.contains(key) { +/// println!("Pressed {key_name}"); +/// } +/// } +/// } +/// keyboard.clear_pressed_keys(); +/// let key_set = keyboard.released_keys(); +/// if !key_set.is_none() { +/// for &(key_name, key) in cardputer_bsc_nostd::keyboard::Key::flags() { +/// if key_set.contains(key) { +/// println!("Released {key_name}"); +/// } +/// } +/// } +/// keyboard.clear_released_keys(); +/// esp_hal::delay::Delay::new().delay_millis(10); +/// } +/// ``` +pub struct Keyboard<'a> { + a0: esp_hal::gpio::Output<'a>, + a1: esp_hal::gpio::Output<'a>, + a2: esp_hal::gpio::Output<'a>, + y0: esp_hal::gpio::Input<'a>, + y1: esp_hal::gpio::Input<'a>, + y2: esp_hal::gpio::Input<'a>, + y3: esp_hal::gpio::Input<'a>, + y4: esp_hal::gpio::Input<'a>, + y5: esp_hal::gpio::Input<'a>, + y6: esp_hal::gpio::Input<'a>, + last_state: Key, + pressed: Key, + released: Key, +} + +impl<'a> Keyboard<'a> { + /// Initialises the cardputer's keyboard. + pub fn new( + a0: esp_hal::peripherals::GPIO8<'a>, + a1: esp_hal::peripherals::GPIO9<'a>, + a2: esp_hal::peripherals::GPIO11<'a>, + y0: esp_hal::peripherals::GPIO13<'a>, + y1: esp_hal::peripherals::GPIO15<'a>, + y2: esp_hal::peripherals::GPIO3<'a>, + y3: esp_hal::peripherals::GPIO4<'a>, + y4: esp_hal::peripherals::GPIO5<'a>, + y5: esp_hal::peripherals::GPIO6<'a>, + y6: esp_hal::peripherals::GPIO7<'a>, + ) -> Self { + let a0 = esp_hal::gpio::Output::new( + a0, + esp_hal::gpio::Level::Low, + esp_hal::gpio::OutputConfig::default() + .with_drive_mode(esp_hal::gpio::DriveMode::PushPull), + ); + let a1 = esp_hal::gpio::Output::new( + a1, + esp_hal::gpio::Level::Low, + esp_hal::gpio::OutputConfig::default() + .with_drive_mode(esp_hal::gpio::DriveMode::PushPull), + ); + let a2 = esp_hal::gpio::Output::new( + a2, + esp_hal::gpio::Level::Low, + esp_hal::gpio::OutputConfig::default() + .with_drive_mode(esp_hal::gpio::DriveMode::PushPull), + ); + let y0 = esp_hal::gpio::Input::new( + y0, + esp_hal::gpio::InputConfig::default().with_pull(esp_hal::gpio::Pull::Up), + ); + let y1 = esp_hal::gpio::Input::new( + y1, + esp_hal::gpio::InputConfig::default().with_pull(esp_hal::gpio::Pull::Up), + ); + let y2 = esp_hal::gpio::Input::new( + y2, + esp_hal::gpio::InputConfig::default().with_pull(esp_hal::gpio::Pull::Up), + ); + let y3 = esp_hal::gpio::Input::new( + y3, + esp_hal::gpio::InputConfig::default().with_pull(esp_hal::gpio::Pull::Up), + ); + let y4 = esp_hal::gpio::Input::new( + y4, + esp_hal::gpio::InputConfig::default().with_pull(esp_hal::gpio::Pull::Up), + ); + let y5 = esp_hal::gpio::Input::new( + y5, + esp_hal::gpio::InputConfig::default().with_pull(esp_hal::gpio::Pull::Up), + ); + let y6 = esp_hal::gpio::Input::new( + y6, + esp_hal::gpio::InputConfig::default().with_pull(esp_hal::gpio::Pull::Up), + ); + + Self { + a0, + a1, + a2, + y0, + y1, + y2, + y3, + y4, + y5, + y6, + last_state: Key::none(), + pressed: Key::none(), + released: Key::none(), + } + } + + /// Scans the keyboard matrix and tracks which keys have been pressed and released. + /// + /// This function should be called often (100Hz) otherwise it may miss key presses. + pub fn scan(&mut self) { + let mut key_flags = 0_u64; + for i in 0..8 { + self.a0 + .set_level(esp_hal::gpio::Level::from(i & 0b00000001 != 0)); + self.a1 + .set_level(esp_hal::gpio::Level::from(i & 0b00000010 != 0)); + self.a2 + .set_level(esp_hal::gpio::Level::from(i & 0b00000100 != 0)); + + let inputs: [esp_hal::gpio::Level; 7] = [ + self.y0.level(), + self.y1.level(), + self.y2.level(), + self.y3.level(), + self.y4.level(), + self.y5.level(), + self.y6.level(), + ]; + + let major_offset = i * 8; + + for (minor_offset, decoded) in inputs.iter().enumerate() { + let bit = match decoded { + esp_hal::gpio::Level::Low => 1_u64, + esp_hal::gpio::Level::High => 0_u64, + }; + key_flags |= bit << (major_offset + minor_offset); + } + } + let key = Key { bits: key_flags }; + + let changes = key.xor(self.last_state); + + let new_presses = changes.and(key); + self.pressed |= new_presses; + + let new_releases = changes.and(!key); + self.released |= new_releases; + + self.last_state = key; + } + + /// Returns the keys that were held when [scan][Self::scan] was last called. + pub fn held_keys(&self) -> Key { + self.last_state + } + + /// Returns buffered key presses that haven't already been cleared with + /// [clear_pressed_keys][Self::clear_pressed_keys] or [clear_some_pressed_keys][Self::clear_some_pressed_keys] + pub fn pressed_keys(&self) -> Key { + self.pressed + } + + /// Clears the key press buffer + pub fn clear_pressed_keys(&mut self) { + self.pressed = Key::none() + } + + /// Clears specific keys from the key press buffer + pub fn clear_some_pressed_keys(&mut self, keys_to_clear: Key) { + self.pressed &= !keys_to_clear; + } + + /// Returns buffered key releases that haven't already been cleared with + /// [clear_released_keys][Self::clear_released_keys] or [clear_some_released_keys][Self::clear_some_released_keys] + pub fn released_keys(&self) -> Key { + self.released + } + + /// Clears the key release buffer + pub fn clear_released_keys(&mut self) { + self.released = Key::none() + } + + /// Clears specific keys from the key release buffer + pub fn clear_some_released_keys(&mut self, keys_to_clear: Key) { + self.released &= !keys_to_clear; + } +}