From 4d4d39c7a1298e12589ac692d56f55abc43cb37b Mon Sep 17 00:00:00 2001 From: Adam Gausmann Date: Fri, 31 Oct 2025 09:14:40 -0500 Subject: [PATCH] chord controller --- firmware/src/main.rs | 215 ++++++++++++++++++++++++++++--------------- 1 file changed, 140 insertions(+), 75 deletions(-) diff --git a/firmware/src/main.rs b/firmware/src/main.rs index cea649e..2604204 100644 --- a/firmware/src/main.rs +++ b/firmware/src/main.rs @@ -7,7 +7,6 @@ mod led_matrix; use core::panic::PanicInfo; use embassy_executor::Spawner; -use embassy_futures::select::select_array; use embassy_rp::bind_interrupts; use embassy_rp::gpio::{Input, Level, Output, Pull}; use embassy_rp::peripherals::PIO0; @@ -16,16 +15,12 @@ use embassy_rp::pio_programs::ws2812::{PioWs2812, PioWs2812Program}; use embassy_rp::pwm::{self, Pwm}; use embassy_rp::rom_data::reset_to_usb_boot; use embassy_rp::spi::{self, Blocking, Spi}; -use embassy_time::{Delay, Duration, Instant, Ticker}; -use embedded_graphics::image::{Image, ImageRawLE}; -use embedded_graphics::mono_font::MonoTextStyle; -use embedded_graphics::mono_font::ascii::FONT_10X20; -use embedded_graphics::pixelcolor::{Rgb565, Rgb888}; -use embedded_graphics::prelude::*; -use embedded_graphics::primitives::{PrimitiveStyleBuilder, Rectangle}; -use embedded_graphics::text::Text; +use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; +use embassy_sync::signal::Signal; +use embassy_time::{Delay, Duration, Instant, Ticker, Timer}; +use embedded_graphics::pixelcolor::Rgb888; +use embedded_graphics::prelude::{DrawTarget, Drawable, RgbColor, Size}; use embedded_hal_bus::spi::ExclusiveDevice; -use heapless::String; use mipidsi::interface::SpiInterface; use mipidsi::models::ST7789; use mipidsi::options::Orientation; @@ -45,24 +40,22 @@ const DISPLAY_FREQ: u32 = 64_000_000; async fn main(spawner: Spawner) -> ! { let p = embassy_rp::init(Default::default()); - // Setup buttons - let _btn_a = Input::new(p.PIN_12, Pull::Up); - let _btn_b = Input::new(p.PIN_13, Pull::Up); - // let _btn_x = Input::new(p.PIN_14, Pull::Up); - let btn_y = Input::new(p.PIN_15, Pull::Up); - - let mut btn_f1 = Input::new(p.PIN_11, Pull::Up); - let mut btn_f2 = Input::new(p.PIN_14, Pull::Up); - let mut btn_f3 = Input::new(p.PIN_9, Pull::Up); - let mut btn_f4 = Input::new(p.PIN_3, Pull::Up); - - spawner.spawn(dfu_button(btn_y)).ok(); + let buttons = Buttons { + f1: Input::new(p.PIN_11, Pull::Up), + f2: Input::new(p.PIN_14, Pull::Up), + f3: Input::new(p.PIN_9, Pull::Up), + f4: Input::new(p.PIN_3, Pull::Up), + a: Input::new(p.PIN_12, Pull::Up), + b: Input::new(p.PIN_13, Pull::Up), + // x = f2 + y: Input::new(p.PIN_15, Pull::Up), + }; // display SPI let mut display_buffer = [0u8; 512]; - let mut display = { + let mut _display = { let mut config = spi::Config::default(); config.frequency = DISPLAY_FREQ; config.phase = spi::Phase::CaptureOnSecondTransition; @@ -85,17 +78,8 @@ async fn main(spawner: Spawner) -> ! { .init(&mut Delay) .expect("display init") }; - let _display_backlight = Output::new(p.PIN_20, Level::High); - - let raw_image_data = ImageRawLE::new(include_bytes!("assets/ferris.raw"), 86); - let ferris = Image::new(&raw_image_data, Point::new(0, 0)); - - let style = MonoTextStyle::new(&FONT_10X20, Rgb565::GREEN); - let hello_text = Text::new( - "Hello embedded_graphics \n + embassy + RP2040!", - Point::new(0, 68), - style, - ); + // Blank display - unused at the moment + let _display_backlight = Output::new(p.PIN_20, Level::Low); // setup RGB LED let mut rg_config = pwm::Config::default(); @@ -106,7 +90,7 @@ async fn main(spawner: Spawner) -> ! { let mut rg_pwm = Pwm::new_output_ab(p.PWM_SLICE3, p.PIN_6, p.PIN_7, rg_config.clone()); let mut b_pwm = Pwm::new_output_a(p.PWM_SLICE4, p.PIN_8, b_config.clone()); - let mut set_led = |r: u8, g: u8, b: u8| { + let mut _set_led = |r: u8, g: u8, b: u8| { rg_config.compare_a = 0x101 * (255 - r as u16); rg_config.compare_b = 0x101 * (255 - g as u16); b_config.compare_a = 0x101 * (255 - b as u16); @@ -129,43 +113,116 @@ async fn main(spawner: Spawner) -> ! { ), ); spawner.spawn(led_task(led_surface)).ok(); + spawner.spawn(controller_task(buttons)).ok(); - let mut render = |f_keys: [bool; 4]| { - display.clear(Rgb565::BLACK).ok(); - ferris.draw(&mut display).ok(); - hello_text.draw(&mut display).ok(); + // go to sleep + loop { + Timer::after_secs(1).await + } +} - let mut f_key_string: String<16> = String::new(); - for (f, s) in f_keys.into_iter().zip(["1", "2", "3", "4"]) { - f_key_string.push_str(f.then_some(s).unwrap_or(" ")).ok(); +struct Buttons { + f1: Input<'static>, + f2: Input<'static>, + f3: Input<'static>, + f4: Input<'static>, + a: Input<'static>, + b: Input<'static>, + // x = f2 + y: Input<'static>, +} + +impl Buttons { + fn read(&self) -> ButtonStates { + ButtonStates { + f1: self.f1.is_low(), + f2: self.f2.is_low(), + f3: self.f3.is_low(), + f4: self.f4.is_low(), + a: self.a.is_low(), + b: self.b.is_low(), + y: self.y.is_low(), } + } +} - Text::new(&f_key_string, Point::new(0, 108), style) - .draw(&mut display) - .ok(); - }; +#[derive(Clone, Copy, PartialEq)] +struct ButtonStates { + f1: bool, + f2: bool, + f3: bool, + f4: bool, + a: bool, + b: bool, + // x = f2 + y: bool, +} - let mut last_state = None; +impl ButtonStates { + fn chord(&self) -> u8 { + (self.f1 as u8) + ((self.f2 as u8) << 1) + ((self.f3 as u8) << 2) + ((self.f4 as u8) << 3) + } +} + +enum LedConfig { + Blank, + Hal, + Happy, +} + +static LED_CONFIG_SIGNAL: Signal = Signal::new(); + +#[embassy_executor::task] +async fn controller_task(buttons: Buttons) { + let mut scan_ticker = Ticker::every(Duration::from_millis(1)); + let mut chord_debounce = 0; + let mut stable_chord = 0; + let mut pending_chord = 0; loop { - let state = [ - btn_f1.is_low(), - btn_f2.is_low(), - btn_f3.is_low(), - btn_f4.is_low(), - ]; - if Some(state) != last_state { - render(state); - last_state = Some(state); - continue; + let state = buttons.read(); + let new_chord = state.chord(); + + if state.y { + reset_to_usb_boot(0, 0); } - select_array([ - btn_f1.wait_for_any_edge(), - btn_f2.wait_for_any_edge(), - btn_f3.wait_for_any_edge(), - btn_f4.wait_for_any_edge(), - ]) - .await; + + if new_chord != pending_chord { + // Track the most recent chord key state. + pending_chord = new_chord; + chord_debounce = 10; + } else if chord_debounce > 0 { + // Wait several scans to make sure it's a stable reading. + chord_debounce -= 1; + } else if (!stable_chord & pending_chord) != 0 { + // Once it's stable, if new keys were pressed compared to + // stable_chord, replace stable_chord. + stable_chord = pending_chord; + } else if pending_chord == 0 && stable_chord != 0 { + // If a stable 0 (all keys released) is read, then process + // stable_chord as an input. + match stable_chord { + 0x1 => { + // blank display + LED_CONFIG_SIGNAL.signal(LedConfig::Blank); + } + 0x2 => { + // happy + LED_CONFIG_SIGNAL.signal(LedConfig::Happy); + } + 0x4 => { + // hal + LED_CONFIG_SIGNAL.signal(LedConfig::Hal); + } + _ => { + // unknown pattern + } + } + + stable_chord = 0; + } + + scan_ticker.next().await; } } @@ -184,27 +241,35 @@ async fn led_task(mut led_surface: LedMatrix<'static>) { let mut ticker = Ticker::every(Duration::from_hz(30)); let start = Instant::now(); + let mut led_config = LedConfig::Blank; + loop { let elapsed = start.elapsed(); rainbow.elapsed = elapsed; + if let Some(new_config) = LED_CONFIG_SIGNAL.try_take() { + led_config = new_config; + } + led_surface.clear(Rgb888::BLACK).ok(); - eyes.draw(&mut RainbowFilter { - inner_target: &mut led_surface, - rainbow: &rainbow, - }) - .ok(); + match led_config { + LedConfig::Blank => {} + LedConfig::Hal => { + hal.draw(&mut led_surface).ok(); + } + LedConfig::Happy => { + eyes.draw(&mut RainbowFilter { + inner_target: &mut led_surface, + rainbow: &rainbow, + }) + .ok(); + } + } led_surface.sync().await; ticker.next().await; } } -#[embassy_executor::task] -async fn dfu_button(mut btn_y: Input<'static>) { - btn_y.wait_for_falling_edge().await; - reset_to_usb_boot(0, 0); -} - #[panic_handler] fn panic(_info: &PanicInfo) -> ! { reset_to_usb_boot(0, 0);