From d24abc6db2f810d98f9f8a92761e21625e454352 Mon Sep 17 00:00:00 2001 From: Lukrecja Date: Sun, 28 Dec 2025 19:13:12 +0100 Subject: [PATCH] implement basic keyboard firmware for my 40% ortho prototype, along with debug display info --- .gitignore | 3 +- Cargo.lock | 159 +++++++++++++++++++++++++- Cargo.toml | 6 + src/display.rs | 85 ++++++++++++++ src/keymap.rs | 172 ++++++++++++++++++++++++++++ src/main.rs | 295 ++++++++++++++++++------------------------------- src/matrix.rs | 78 +++++++++++++ src/usb.rs | 138 +++++++++++++++++++++++ 8 files changed, 745 insertions(+), 191 deletions(-) create mode 100644 src/display.rs create mode 100644 src/keymap.rs create mode 100644 src/matrix.rs create mode 100644 src/usb.rs diff --git a/.gitignore b/.gitignore index a1f3e00..80459a8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ # Cargo build artifacts /target/ -**/.idea \ No newline at end of file +**/.idea +*.uf2 \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index c9f7ad4..c03849a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -116,6 +116,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "byte-slice-cast" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" + [[package]] name = "bytemuck" version = "1.24.0" @@ -318,6 +324,35 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "display-interface" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba2aab1ef3793e6f7804162debb5ac5edb93b3d650fbcc5aeb72fcd0e6c03a0" + +[[package]] +name = "display-interface-i2c" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d964fa85bbbb5a6ecd06e58699407ac5dc3e3ad72dac0ab7e6b0d00a1cd262d" +dependencies = [ + "display-interface", + "embedded-hal 1.0.0", + "embedded-hal-async", +] + +[[package]] +name = "display-interface-spi" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86b9ec30048b1955da2038fcc3c017f419ab21bb0001879d16c0a3749dc6b7a" +dependencies = [ + "byte-slice-cast", + "display-interface", + "embedded-hal 1.0.0", + "embedded-hal-async", +] + [[package]] name = "document-features" version = "0.2.12" @@ -565,6 +600,29 @@ dependencies = [ "embedded-io-async", ] +[[package]] +name = "embedded-graphics" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0649998afacf6d575d126d83e68b78c0ab0e00ca2ac7e9b3db11b4cbe8274ef0" +dependencies = [ + "az", + "byteorder", + "embedded-graphics-core", + "float-cmp", + "micromath", +] + +[[package]] +name = "embedded-graphics-core" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba9ecd261f991856250d2207f6d8376946cd9f412a2165d3b75bc87a0bc7a044" +dependencies = [ + "az", + "byteorder", +] + [[package]] name = "embedded-hal" version = "0.2.7" @@ -669,6 +727,15 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" +[[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 = "fnv" version = "1.0.7" @@ -799,12 +866,17 @@ dependencies = [ "embassy-sync 0.6.2", "embassy-time", "embassy-usb", + "embedded-graphics", "embedded-hal 1.0.0", "embedded-hal-async", "embedded-io", "embedded-io-async", "embedded-storage", + "heapless", "panic-probe", + "portable-atomic", + "ssd1306", + "static_cell", "usbd-hid", ] @@ -876,12 +948,31 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "maybe-async-cfg" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e083394889336bc66a4eaf1011ffbfa74893e910f902a9f271fa624c61e1b2" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "pulldown-cmark", + "quote", + "syn 1.0.109", +] + [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[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" @@ -1065,9 +1156,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" [[package]] name = "precomputed-hash" @@ -1075,6 +1166,30 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -1106,6 +1221,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pulldown-cmark" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "679341d22c78c6c649893cbd6c3278dcbe9fc4faa62fea3a9296ae2b50c14625" +dependencies = [ + "bitflags 2.10.0", + "memchr", + "unicase", +] + [[package]] name = "quote" version = "1.0.42" @@ -1304,6 +1430,20 @@ dependencies = [ "rgb", ] +[[package]] +name = "ssd1306" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ea6aac2d078bbc71d9b8ac3f657335311f3b6625e9a1a96ccc29f5abfa77c56" +dependencies = [ + "display-interface", + "display-interface-i2c", + "display-interface-spi", + "embedded-graphics-core", + "embedded-hal 1.0.0", + "maybe-async-cfg", +] + [[package]] name = "ssmarshal" version = "1.0.0" @@ -1320,6 +1460,15 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "static_cell" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0530892bb4fa575ee0da4b86f86c667132a94b74bb72160f58ee5a4afec74c23" +dependencies = [ + "portable-atomic", +] + [[package]] name = "string_cache" version = "0.8.9" @@ -1404,6 +1553,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + [[package]] name = "unicode-ident" version = "1.0.22" diff --git a/Cargo.toml b/Cargo.toml index d76571f..58fb569 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,12 @@ embassy-rp = { version = "0.4", features = ["defmt", "unstable-pac", "time-drive embassy-usb = "0.4.0" usbd-hid = "0.8.2" embassy-futures = "0.1.2" +ssd1306 = "0.10.0" +embedded-graphics = "0.8.1" +heapless = "0.8" +static_cell = "2.1.1" +portable-atomic = { version = "1.13.0", features = ["unsafe-assume-single-core"] } + [profile.release] debug = 2 lto = true diff --git a/src/display.rs b/src/display.rs new file mode 100644 index 0000000..60d4653 --- /dev/null +++ b/src/display.rs @@ -0,0 +1,85 @@ +use embedded_graphics::{ + mono_font::{MonoTextStyleBuilder, ascii::FONT_6X10}, + pixelcolor::BinaryColor, + prelude::Point, + text::{Baseline, Text}, + draw_target::DrawTarget, + Drawable, +}; +use ssd1306::{prelude::*, I2CDisplayInterface, Ssd1306, mode::BufferedGraphicsMode}; +use embassy_rp::i2c::I2c; +use core::fmt::Write; + +/// Display type encapsulating the I2C interface, its size and graphics mode +pub type Display = Ssd1306< + I2CInterface>, + DisplaySize128x32, + BufferedGraphicsMode +>; + +/// Function that initializes the display with given settings +pub fn init_display(bus: I2c<'static, embassy_rp::peripherals::I2C0, embassy_rp::i2c::Async>) -> Option { + let interface = I2CDisplayInterface::new(bus); + let mut display = Ssd1306::new( + interface, + DisplaySize128x32, + DisplayRotation::Rotate180, + ) + .into_buffered_graphics_mode(); + + match display.init() { + Ok(_) => Some(display), + Err(_) => None, + } +} + +/// Temp function which updates the display with currently pressed keys +pub fn update_display( + display: &mut Option, + pressed_keys: &[(usize, usize, crate::keymap::KeyCode)], +) -> Option<()> { + if let Some(display) = display { + display.clear(BinaryColor::Off).ok()?; + + let text_style = MonoTextStyleBuilder::new() + .font(&FONT_6X10) + .text_color(BinaryColor::On) + .build(); + + if pressed_keys.is_empty() { + Text::with_baseline("No keys", Point::zero(), text_style, Baseline::Top) + .draw(display) + .ok()?; + } else { + let mut y_pos = 0; + for (row_idx, col_idx, keycode) in pressed_keys.iter().take(3) { + let mut msg = heapless::String::<32>::new(); + write!(&mut msg, "R{}C{}: 0x{:02X}", row_idx, col_idx, keycode.as_u8()).ok(); + + Text::with_baseline(&msg, Point::new(0, y_pos), text_style, Baseline::Top) + .draw(display) + .ok()?; + + y_pos += 10; + } + } + + display.flush().ok()?; + Some(()) + } else { + None + } +} + +/// Helper function to clear the display +pub fn clear_display( + display: &mut Option, +) -> Option<()> { + if let Some(display) = display { + display.clear(BinaryColor::Off).ok()?; + display.flush().ok()?; + Some(()) + } else { + None + } +} \ No newline at end of file diff --git a/src/keymap.rs b/src/keymap.rs new file mode 100644 index 0000000..4b27c3c --- /dev/null +++ b/src/keymap.rs @@ -0,0 +1,172 @@ +/// USB HID Keyboard scancodes +/// Reference: https://www.usb.org/sites/default/files/documents/hut1_12v2.pdf +/// Section 10: Keyboard/Keypad Page (0x07) + +#[allow(dead_code)] + +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum KeyCode { + No = 0x00, + + // Letters + A = 0x04, + B = 0x05, + C = 0x06, + D = 0x07, + E = 0x08, + F = 0x09, + G = 0x0A, + H = 0x0B, + I = 0x0C, + J = 0x0D, + K = 0x0E, + L = 0x0F, + M = 0x10, + N = 0x11, + O = 0x12, + P = 0x13, + Q = 0x14, + R = 0x15, + S = 0x16, + T = 0x17, + U = 0x18, + V = 0x19, + W = 0x1A, + X = 0x1B, + Y = 0x1C, + Z = 0x1D, + + // Numbers + Kb1 = 0x1E, + Kb2 = 0x1F, + Kb3 = 0x20, + Kb4 = 0x21, + Kb5 = 0x22, + Kb6 = 0x23, + Kb7 = 0x24, + Kb8 = 0x25, + Kb9 = 0x26, + Kb0 = 0x27, + + // Special keys + Enter = 0x28, + Escape = 0x29, + Backspace = 0x2A, + Tab = 0x2B, + Space = 0x2C, + Minus = 0x2D, + Equal = 0x2E, + LeftBracket = 0x2F, + RightBracket = 0x30, + Backslash = 0x31, + Semicolon = 0x33, + Quote = 0x34, + Grave = 0x35, + Comma = 0x36, + Dot = 0x37, + Slash = 0x38, + CapsLock = 0x39, + + // Function keys + F1 = 0x3A, + F2 = 0x3B, + F3 = 0x3C, + F4 = 0x3D, + F5 = 0x3E, + F6 = 0x3F, + F7 = 0x40, + F8 = 0x41, + F9 = 0x42, + F10 = 0x43, + F11 = 0x44, + F12 = 0x45, + + // Navigation + PrintScreen = 0x46, + ScrollLock = 0x47, + Pause = 0x48, + Insert = 0x49, + Home = 0x4A, + PageUp = 0x4B, + Delete = 0x4C, + End = 0x4D, + PageDown = 0x4E, + Right = 0x4F, + Left = 0x50, + Down = 0x51, + Up = 0x52, + + // Modifiers + LeftCtrl = 0xE0, + LeftShift = 0xE1, + LeftAlt = 0xE2, + LeftGui = 0xE3, + RightCtrl = 0xE4, + RightShift = 0xE5, + RightAlt = 0xE6, // AltGr + RightGui = 0xE7, +} + +impl KeyCode { + pub const fn as_u8(self) -> u8 { + self as u8 + } + + pub const fn is_modifier(self) -> bool { + matches!(self, + KeyCode::LeftCtrl | KeyCode::LeftShift | KeyCode::LeftAlt | KeyCode::LeftGui | + KeyCode::RightCtrl | KeyCode::RightShift | KeyCode::RightAlt | KeyCode::RightGui + ) + } + + pub const fn modifier_bit(self) -> u8 { + match self { + KeyCode::LeftCtrl => 0x01, + KeyCode::LeftShift => 0x02, + KeyCode::LeftAlt => 0x04, + KeyCode::LeftGui => 0x08, + KeyCode::RightCtrl => 0x10, + KeyCode::RightShift => 0x20, + KeyCode::RightAlt => 0x40, // AltGr + KeyCode::RightGui => 0x80, + _ => 0, + } + } +} + +/// Keymap for my 4x12 ortholinear keyboard +/// Each position [row][col] maps to a USB HID scancode +pub const KEYMAP: [[KeyCode; 12]; 4] = [ + [ + KeyCode::Escape, KeyCode::Q, KeyCode::W, KeyCode::E, + KeyCode::R, KeyCode::T, KeyCode::Y, KeyCode::U, + KeyCode::I, KeyCode::O, KeyCode::P, KeyCode::Backspace, + ], + [ + KeyCode::Tab, KeyCode::A, KeyCode::S, KeyCode::D, + KeyCode::F, KeyCode::G, KeyCode::H, KeyCode::J, + KeyCode::K, KeyCode::L, KeyCode::Quote, KeyCode::Enter, + ], + [ + KeyCode::LeftShift, KeyCode::Z, KeyCode::X, KeyCode::C, + KeyCode::V, KeyCode::B, KeyCode::N, KeyCode::M, + KeyCode::Comma, KeyCode::Dot, KeyCode::Up, KeyCode::Slash, + ], + [ + KeyCode::LeftCtrl, KeyCode::No, KeyCode::No, KeyCode::LeftAlt, + KeyCode::No, KeyCode::Space, KeyCode::No, KeyCode::RightAlt, + KeyCode::No, KeyCode::Left, KeyCode::Down, KeyCode::Right, + ], +]; + +/// Get keycode from given row and column +pub fn get_keycode(row: usize, col: usize) -> Option { + if row < 4 && col < 12 { + let keycode = KEYMAP[row][col]; + if keycode != KeyCode::No { + return Some(keycode); + } + } + None +} diff --git a/src/main.rs b/src/main.rs index 39efbac..cd5207c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,222 +1,141 @@ #![no_std] #![no_main] -use core::sync::atomic::{AtomicBool, Ordering}; +mod matrix; +mod keymap; +mod display; +mod usb; use defmt::{info, warn}; use embassy_executor::Spawner; use embassy_futures::join::join; -use embassy_rp::{bind_interrupts, gpio::{Input, Level, Output, Pull}, peripherals::USB, usb::{Driver, InterruptHandler}}; +use embassy_rp::{ + bind_interrupts, + i2c::{Config as I2cConfig, I2c, InterruptHandler as I2cInterruptHandler}, + peripherals::USB, + usb::{Driver, InterruptHandler as UsbInterruptHandler}, +}; use embassy_time::{Duration, Timer}; -use embassy_usb::{Builder, Config, Handler, class::hid::{HidReaderWriter, ReportId, RequestHandler, State}, control::OutResponse}; -use usbd_hid::descriptor::{KeyboardReport, SerializedDescriptor}; +use usbd_hid::descriptor::KeyboardReport; use {defmt_rtt as _, panic_probe as _}; -pub struct Debouncer<'a> { - input: Input<'a>, - debounce: Duration, -} - -impl<'a> Debouncer<'a> { - pub fn new(input: Input<'a>, debounce: Duration) -> Self { - Self { input, debounce } - } - - pub async fn debounce_low(&mut self) -> Level { - loop { - let l1 = self.input.get_level(); - - self.input.wait_for_low().await; - - Timer::after(self.debounce).await; - - let l2 = self.input.get_level(); - if l1 != l2 { - break l2; - } - } - } - - pub async fn debounce_high(&mut self) -> Level { - loop { - let l1 = self.input.get_level(); - - self.input.wait_for_high().await; - - Timer::after(self.debounce).await; - - let l2 = self.input.get_level(); - if l1 != l2 { - break l2; - } - } - } -} +use keymap::get_keycode; bind_interrupts!(struct Irqs { - USBCTRL_IRQ => InterruptHandler; + I2C0_IRQ => I2cInterruptHandler; + USBCTRL_IRQ => UsbInterruptHandler; }); + #[embassy_executor::main] async fn main(_spawner: Spawner) -> () { let p = embassy_rp::init(Default::default()); - let driver = Driver::new(p.USB, Irqs); + info!("Firmware starting"); - let mut config = Config::new(0x1234, 0xabcd); - config.manufacturer = Some("Lukrecja"); - config.product = Some("kb-prototype"); - config.serial_number = Some("00000000"); - config.composite_with_iads = false; - config.device_class = 0x00; - config.device_sub_class = 0x00; - config.device_protocol = 0x00; + // Setup USB HID + let usb_driver = Driver::new(p.USB, Irqs); + let mut usb_keyboard = usb::setup_usb(usb_driver); + let usb_fut = usb_keyboard.usb.run(); + info!("USB configured"); - let mut config_descriptor = [0; 256]; - let mut bos_descriptor = [0; 256]; - let mut msos_descriptor = [0; 256]; - let mut control_buf = [0; 64]; - let mut request_handler = KbRequestHandler {}; - let mut device_handler = KbDeviceHandler::new(); + // Setup I2C display + info!("Initializing display..."); + let i2c_config = I2cConfig::default(); + let bus = I2c::new_async(p.I2C0, p.PIN_17, p.PIN_16, Irqs, i2c_config); + let mut display = display::init_display(bus); + match display { + Some(_) => info!("Display initialized"), + None => warn!("Can't initialize display"), + } - let mut state = State::new(); - - let mut builder = Builder::new( - driver, - config, - &mut config_descriptor, - &mut bos_descriptor, - &mut msos_descriptor, - &mut control_buf, + // Setup matrix + info!("Initializing matrix..."); + let mut matrix = matrix_4x12!(p, + rows: [PIN_0, PIN_1, PIN_2, PIN_3], + cols: [PIN_4, PIN_5, PIN_6, PIN_7, PIN_8, PIN_9, PIN_10, PIN_11, PIN_12, PIN_13, PIN_14, PIN_15] ); + info!("Matrix initialized"); - builder.handler(&mut device_handler); + let mut writer = usb_keyboard.writer; + let reader = usb_keyboard.reader; + let request_handler = usb_keyboard.request_handler; + info!("Starting main loop..."); - let config = embassy_usb::class::hid::Config { - report_descriptor: KeyboardReport::desc(), - request_handler: None, - poll_ms: 60, - max_packet_size: 64, - }; - - let hid = HidReaderWriter::<_, 1, 8>::new(&mut builder, &mut state, config); - - let mut usb = builder.build(); - - let usb_fut = usb.run(); - - let mut led = Output::new(p.PIN_25, Level::Low); - let mut switch_input = Debouncer::new(Input::new(p.PIN_0, Pull::Up), Duration::from_millis(20)); - switch_input.input.set_schmitt(true); - - let (reader, mut writer) = hid.split(); + display::clear_display(&mut display); + // Keyboard scanning task let in_fut = async { + let mut previous_keycodes: [u8; 6] = [0; 6]; + let mut previous_modifier: u8 = 0; + loop { - defmt::info!("waiting for low"); - led.set_low(); - - switch_input.debounce_low().await; - - info!("got low"); - led.set_high(); - - let report = KeyboardReport { - keycodes: [4, 0, 0, 0, 0, 0], - leds: 0, - modifier: 0, - reserved: 0, - }; - - match writer.write_serialize(&report).await { - Ok(()) => info!("report sent"), - Err(e) => warn!("failed to send: {:?}", e), - }; - - switch_input.debounce_high().await; - info!("got high"); - - let report = KeyboardReport { - keycodes: [0, 0, 0, 0, 0, 0], - leds: 0, - modifier: 0, - reserved: 0, - }; - - match writer.write_serialize(&report).await { - Ok(()) => info!("report sent"), - Err(e) => warn!("failed to send: {:?}", e), - }; + let scan_result = matrix.scan().await; + + // Collect all pressed keys and their scancodes + let mut pressed_keys: heapless::Vec<(usize, usize, keymap::KeyCode), 48> = heapless::Vec::new(); + + for (row_idx, row) in scan_result.iter().enumerate() { + for (col_idx, &is_pressed) in row.iter().enumerate() { + if is_pressed { + if let Some(keycode) = get_keycode(row_idx, col_idx) { + let _ = pressed_keys.push((row_idx, col_idx, keycode)); + } + } + } + } + + // Build modifier byte and keycodes array + let mut modifier: u8 = 0; + let mut keycodes = [0u8; 6]; + let mut keycode_index = 0; + + for (_, _, keycode) in pressed_keys.iter() { + if keycode.is_modifier() { + // Add to modifier byte + modifier |= keycode.modifier_bit(); + } else if keycode_index < 6 { + // Add to keycodes array (max 6 regular keys) + keycodes[keycode_index] = keycode.as_u8(); + keycode_index += 1; + } + } + + // Update display + display::update_display(&mut display, &pressed_keys); + + // Send HID report if keycodes or modifiers changed + if keycodes != previous_keycodes || modifier != previous_modifier { + if keycodes != [0; 6] || modifier != 0 { + info!("Keys: {:?}, Mods: 0x{:02X}", keycodes, modifier); + } else { + info!("All keys released"); + } + + let report = KeyboardReport { + keycodes, + leds: 0, + modifier, + reserved: 0, + }; + + match writer.write_serialize(&report).await { + Ok(()) => {}, + Err(e) => warn!("Failed to send report: {:?}", e), + }; + + previous_keycodes = keycodes; + previous_modifier = modifier; + } + + Timer::after(Duration::from_millis(10)).await; } }; + // USB reader task let out_fut = async { - reader.run(false, &mut request_handler).await; + reader.run(false, request_handler).await; }; + join(usb_fut, join(in_fut, out_fut)).await; } - -struct KbRequestHandler {} - -impl RequestHandler for KbRequestHandler { - fn get_report(&mut self, _id: ReportId, _buf: &mut [u8]) -> Option { - info!("get report"); - None - } - - fn set_report(&mut self, _id: ReportId, data: &[u8]) -> OutResponse { - info!("set report: {=[u8]}", data); - OutResponse::Accepted - } - - fn set_idle_ms(&mut self, _id: Option, dur: u32) { - info!("set idle rate to {}", dur); - } - - fn get_idle_ms(&mut self, _id: Option) -> Option { - info!("get idle rate"); - None - } -} - -struct KbDeviceHandler { - configured: AtomicBool, -} - -impl KbDeviceHandler { - fn new() -> Self { - Self { - configured: AtomicBool::new(false), - } - } -} - -impl Handler for KbDeviceHandler { - fn enabled(&mut self, enabled: bool) { - self.configured.store(false, Ordering::Relaxed); - if enabled { - info!("device enabled"); - } else { - info!("device disabled"); - } - } - - fn reset(&mut self) { - self.configured.store(false, Ordering::Relaxed); - info!("bus reset, Vbus current limit is 100mA"); - } - - fn addressed(&mut self, addr: u8) { - self.configured.store(false, Ordering::Relaxed); - info!("USB address set to: {}", addr); - } - - fn configured(&mut self, configured: bool) { - self.configured.store(configured, Ordering::Relaxed); - if configured { - info!("device configured") - } else { - info!("device no longer configured"); - } - } -} diff --git a/src/matrix.rs b/src/matrix.rs new file mode 100644 index 0000000..50a8447 --- /dev/null +++ b/src/matrix.rs @@ -0,0 +1,78 @@ +use embassy_rp::gpio::{Input, Output}; +use embassy_time::{Duration, Timer}; + +/// Matrix struct with row output pins, column input pins and the key mask +pub struct Matrix { + rows: [Output<'static>; R], + cols: [Input<'static>; C], + key_mask: [[bool; C]; R], +} + +impl Matrix { + /// Create a new `Matrix` with a given mask + pub const fn new_with_mask( + rows: [Output<'static>; R], + cols: [Input<'static>; C], + key_mask: [[bool; C]; R], + ) -> Self { + Self { rows, cols, key_mask } + } + + /// Scan all the keys in the `Matrix` and output current state + pub async fn scan(&mut self) -> [[bool; C]; R] { + let mut state = [[false; C]; R]; + + for (row_idx, row_pin) in self.rows.iter_mut().enumerate() { + row_pin.set_low(); + Timer::after(Duration::from_micros(1)).await; + + for col_idx in 0..C { + if !self.key_mask[row_idx][col_idx] { + continue; + } + + state[row_idx][col_idx] = self.cols[col_idx].is_low(); + } + + row_pin.set_high(); + } + + state + } +} + +// 12x4 mask +pub const MASK: [[bool; 12]; 4] = [ + [true; 12], + [true; 12], + [true; 12], + [true; 12], +]; + +/// Generic macro to create a matrix with any pins +#[macro_export] +macro_rules! create_matrix { + ( + $p:expr, + rows: [$($row:tt),+ $(,)?], + cols: [$($col:tt),+ $(,)?], + mask: $mask:expr + ) => {{ + use embassy_rp::gpio::{Input, Level, Output, Pull}; + let rows = [ + $(Output::new($p.$row, Level::High)),+ + ]; + let cols = [ + $(Input::new($p.$col, Pull::Up)),+ + ]; + $crate::matrix::Matrix::new_with_mask(rows, cols, $mask) + }}; +} + +/// Convenience macro for my 4x12 ortholinear matrix +#[macro_export] +macro_rules! matrix_4x12 { + ($p:expr, rows: [$($row:tt),+ $(,)?], cols: [$($col:tt),+ $(,)?]) => { + $crate::create_matrix!($p, rows: [$($row),+], cols: [$($col),+], mask: $crate::matrix::MASK) + }; +} diff --git a/src/usb.rs b/src/usb.rs new file mode 100644 index 0000000..4d5cc17 --- /dev/null +++ b/src/usb.rs @@ -0,0 +1,138 @@ +/// Reference: https://github.com/embassy-rs/embassy/blob/main/examples/rp/src/bin/usb_hid_keyboard.rs + +use core::sync::atomic::{AtomicBool, Ordering}; + +use defmt::info; +use embassy_rp::peripherals::USB; +use embassy_rp::usb::Driver; +use embassy_usb::{ + Builder, Config, Handler, + class::hid::{HidReaderWriter, ReportId, RequestHandler, State}, + control::OutResponse, +}; +use static_cell::StaticCell; +use usbd_hid::descriptor::{KeyboardReport, SerializedDescriptor}; + +pub struct UsbKeyboard { + pub usb: embassy_usb::UsbDevice<'static, Driver<'static, USB>>, + pub reader: embassy_usb::class::hid::HidReader<'static, Driver<'static, USB>, 1>, + pub writer: embassy_usb::class::hid::HidWriter<'static, Driver<'static, USB>, 8>, + pub request_handler: &'static mut KbRequestHandler, +} + +static CONFIG_DESC: StaticCell<[u8; 256]> = StaticCell::new(); +static BOS_DESC: StaticCell<[u8; 256]> = StaticCell::new(); +static MSOS_DESC: StaticCell<[u8; 256]> = StaticCell::new(); +static CONTROL_BUF: StaticCell<[u8; 64]> = StaticCell::new(); +static REQUEST_HANDLER: StaticCell = StaticCell::new(); +static DEVICE_HANDLER: StaticCell = StaticCell::new(); +static STATE: StaticCell = StaticCell::new(); + +pub fn setup_usb(driver: Driver<'static, USB>) -> UsbKeyboard { + let mut usb_config = Config::new(0x1209, 0x0001); + usb_config.manufacturer = Some("Lukrecja"); + usb_config.product = Some("kb-ortho-40"); + usb_config.serial_number = Some("00000000"); + usb_config.composite_with_iads = false; + usb_config.device_class = 0x00; + usb_config.device_sub_class = 0x00; + usb_config.device_protocol = 0x00; + + let config_descriptor = CONFIG_DESC.init([0; 256]); + let bos_descriptor = BOS_DESC.init([0; 256]); + let msos_descriptor = MSOS_DESC.init([0; 256]); + let control_buf = CONTROL_BUF.init([0; 64]); + let request_handler = REQUEST_HANDLER.init(KbRequestHandler {}); + let device_handler = DEVICE_HANDLER.init(KbDeviceHandler::new()); + let state = STATE.init(State::new()); + + let mut builder = Builder::new( + driver, + usb_config, + config_descriptor, + bos_descriptor, + msos_descriptor, + control_buf, + ); + + builder.handler(device_handler); + + let hid_config = embassy_usb::class::hid::Config { + report_descriptor: KeyboardReport::desc(), + request_handler: None, + poll_ms: 60, + max_packet_size: 64, + }; + + let hid = HidReaderWriter::<_, 1, 8>::new(&mut builder, state, hid_config); + + let usb = builder.build(); + let (reader, writer) = hid.split(); + + UsbKeyboard { usb, reader, writer, request_handler } +} + +pub struct KbRequestHandler {} + +impl RequestHandler for KbRequestHandler { + fn get_report(&mut self, _id: ReportId, _buf: &mut [u8]) -> Option { + info!("get report"); + None + } + + fn set_report(&mut self, _id: ReportId, data: &[u8]) -> OutResponse { + info!("set report: {=[u8]}", data); + OutResponse::Accepted + } + + fn set_idle_ms(&mut self, _id: Option, dur: u32) { + info!("set idle rate to {}", dur); + } + + fn get_idle_ms(&mut self, _id: Option) -> Option { + info!("get idle rate"); + None + } +} + +pub struct KbDeviceHandler { + configured: AtomicBool, +} + +impl KbDeviceHandler { + pub fn new() -> Self { + Self { + configured: AtomicBool::new(false), + } + } +} + +impl Handler for KbDeviceHandler { + fn enabled(&mut self, enabled: bool) { + self.configured.store(false, Ordering::Relaxed); + if enabled { + info!("device enabled"); + } else { + info!("device disabled"); + } + } + + fn reset(&mut self) { + self.configured.store(false, Ordering::Relaxed); + info!("bus reset, Vbus current limit is 100mA"); + } + + fn addressed(&mut self, addr: u8) { + self.configured.store(false, Ordering::Relaxed); + info!("USB address set to: {}", addr); + } + + fn configured(&mut self, configured: bool) { + self.configured.store(configured, Ordering::Relaxed); + if configured { + info!("device configured") + } else { + info!("device no longer configured"); + } + } +}