chord controller

This commit is contained in:
Adam Gausmann 2025-10-31 09:14:40 -05:00
parent 8e96e70cfd
commit 4d4d39c7a1

View file

@ -7,7 +7,6 @@ mod led_matrix;
use core::panic::PanicInfo; use core::panic::PanicInfo;
use embassy_executor::Spawner; use embassy_executor::Spawner;
use embassy_futures::select::select_array;
use embassy_rp::bind_interrupts; use embassy_rp::bind_interrupts;
use embassy_rp::gpio::{Input, Level, Output, Pull}; use embassy_rp::gpio::{Input, Level, Output, Pull};
use embassy_rp::peripherals::PIO0; 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::pwm::{self, Pwm};
use embassy_rp::rom_data::reset_to_usb_boot; use embassy_rp::rom_data::reset_to_usb_boot;
use embassy_rp::spi::{self, Blocking, Spi}; use embassy_rp::spi::{self, Blocking, Spi};
use embassy_time::{Delay, Duration, Instant, Ticker}; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embedded_graphics::image::{Image, ImageRawLE}; use embassy_sync::signal::Signal;
use embedded_graphics::mono_font::MonoTextStyle; use embassy_time::{Delay, Duration, Instant, Ticker, Timer};
use embedded_graphics::mono_font::ascii::FONT_10X20; use embedded_graphics::pixelcolor::Rgb888;
use embedded_graphics::pixelcolor::{Rgb565, Rgb888}; use embedded_graphics::prelude::{DrawTarget, Drawable, RgbColor, Size};
use embedded_graphics::prelude::*;
use embedded_graphics::primitives::{PrimitiveStyleBuilder, Rectangle};
use embedded_graphics::text::Text;
use embedded_hal_bus::spi::ExclusiveDevice; use embedded_hal_bus::spi::ExclusiveDevice;
use heapless::String;
use mipidsi::interface::SpiInterface; use mipidsi::interface::SpiInterface;
use mipidsi::models::ST7789; use mipidsi::models::ST7789;
use mipidsi::options::Orientation; use mipidsi::options::Orientation;
@ -45,24 +40,22 @@ const DISPLAY_FREQ: u32 = 64_000_000;
async fn main(spawner: Spawner) -> ! { async fn main(spawner: Spawner) -> ! {
let p = embassy_rp::init(Default::default()); let p = embassy_rp::init(Default::default());
// Setup buttons let buttons = Buttons {
let _btn_a = Input::new(p.PIN_12, Pull::Up); f1: Input::new(p.PIN_11, Pull::Up),
let _btn_b = Input::new(p.PIN_13, Pull::Up); f2: Input::new(p.PIN_14, Pull::Up),
// let _btn_x = Input::new(p.PIN_14, Pull::Up); f3: Input::new(p.PIN_9, Pull::Up),
let btn_y = Input::new(p.PIN_15, Pull::Up); f4: Input::new(p.PIN_3, Pull::Up),
a: Input::new(p.PIN_12, Pull::Up),
let mut btn_f1 = Input::new(p.PIN_11, Pull::Up); b: Input::new(p.PIN_13, Pull::Up),
let mut btn_f2 = Input::new(p.PIN_14, Pull::Up); // x = f2
let mut btn_f3 = Input::new(p.PIN_9, Pull::Up); y: Input::new(p.PIN_15, Pull::Up),
let mut btn_f4 = Input::new(p.PIN_3, Pull::Up); };
spawner.spawn(dfu_button(btn_y)).ok();
// display SPI // display SPI
let mut display_buffer = [0u8; 512]; let mut display_buffer = [0u8; 512];
let mut display = { let mut _display = {
let mut config = spi::Config::default(); let mut config = spi::Config::default();
config.frequency = DISPLAY_FREQ; config.frequency = DISPLAY_FREQ;
config.phase = spi::Phase::CaptureOnSecondTransition; config.phase = spi::Phase::CaptureOnSecondTransition;
@ -85,17 +78,8 @@ async fn main(spawner: Spawner) -> ! {
.init(&mut Delay) .init(&mut Delay)
.expect("display init") .expect("display init")
}; };
let _display_backlight = Output::new(p.PIN_20, Level::High); // Blank display - unused at the moment
let _display_backlight = Output::new(p.PIN_20, Level::Low);
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,
);
// setup RGB LED // setup RGB LED
let mut rg_config = pwm::Config::default(); 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 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 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_a = 0x101 * (255 - r as u16);
rg_config.compare_b = 0x101 * (255 - g as u16); rg_config.compare_b = 0x101 * (255 - g as u16);
b_config.compare_a = 0x101 * (255 - b 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(led_task(led_surface)).ok();
spawner.spawn(controller_task(buttons)).ok();
let mut render = |f_keys: [bool; 4]| { // go to sleep
display.clear(Rgb565::BLACK).ok(); loop {
ferris.draw(&mut display).ok(); Timer::after_secs(1).await
hello_text.draw(&mut display).ok(); }
}
let mut f_key_string: String<16> = String::new(); struct Buttons {
for (f, s) in f_keys.into_iter().zip(["1", "2", "3", "4"]) { f1: Input<'static>,
f_key_string.push_str(f.then_some(s).unwrap_or(" ")).ok(); 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) #[derive(Clone, Copy, PartialEq)]
.draw(&mut display) struct ButtonStates {
.ok(); 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<CriticalSectionRawMutex, LedConfig> = 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 { loop {
let state = [ let state = buttons.read();
btn_f1.is_low(), let new_chord = state.chord();
btn_f2.is_low(),
btn_f3.is_low(), if state.y {
btn_f4.is_low(), reset_to_usb_boot(0, 0);
];
if Some(state) != last_state {
render(state);
last_state = Some(state);
continue;
} }
select_array([
btn_f1.wait_for_any_edge(), if new_chord != pending_chord {
btn_f2.wait_for_any_edge(), // Track the most recent chord key state.
btn_f3.wait_for_any_edge(), pending_chord = new_chord;
btn_f4.wait_for_any_edge(), chord_debounce = 10;
]) } else if chord_debounce > 0 {
.await; // 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 mut ticker = Ticker::every(Duration::from_hz(30));
let start = Instant::now(); let start = Instant::now();
let mut led_config = LedConfig::Blank;
loop { loop {
let elapsed = start.elapsed(); let elapsed = start.elapsed();
rainbow.elapsed = elapsed; rainbow.elapsed = elapsed;
if let Some(new_config) = LED_CONFIG_SIGNAL.try_take() {
led_config = new_config;
}
led_surface.clear(Rgb888::BLACK).ok(); led_surface.clear(Rgb888::BLACK).ok();
eyes.draw(&mut RainbowFilter { match led_config {
inner_target: &mut led_surface, LedConfig::Blank => {}
rainbow: &rainbow, LedConfig::Hal => {
}) hal.draw(&mut led_surface).ok();
.ok(); }
LedConfig::Happy => {
eyes.draw(&mut RainbowFilter {
inner_target: &mut led_surface,
rainbow: &rainbow,
})
.ok();
}
}
led_surface.sync().await; led_surface.sync().await;
ticker.next().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] #[panic_handler]
fn panic(_info: &PanicInfo) -> ! { fn panic(_info: &PanicInfo) -> ! {
reset_to_usb_boot(0, 0); reset_to_usb_boot(0, 0);