diff --git a/.cargo/config b/.cargo/config index b27f46d..303305e 100644 --- a/.cargo/config +++ b/.cargo/config @@ -26,7 +26,7 @@ [target.'cfg(all(target_arch = "arm", target_os = "none"))'] -runner = "elf2uf2-rs -d" +runner = "elf2uf2-rs --deploy --serial" [build] target = "thumbv6m-none-eabi" # Cortex-M0 and Cortex-M0+ diff --git a/Cargo.lock b/Cargo.lock index ebc037f..f52398e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -788,6 +788,7 @@ dependencies = [ "embassy-time", "embassy-usb", "embassy-usb-logger", + "embedded-hal 0.2.7", "embedded-hal 1.0.0", "embedded-hal-async", "embedded-hal-bus", @@ -798,6 +799,7 @@ dependencies = [ "futures", "heapless 0.8.0", "log", + "mcp23017", "panic-probe", "portable-atomic", "smart-leds", @@ -984,6 +986,15 @@ version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +[[package]] +name = "mcp23017" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c32fd6627e73f1cfa95c00ddcdcb5a6a6ddbd10b308d08588a502c018b6e12c" +dependencies = [ + "embedded-hal 0.2.7", +] + [[package]] name = "memchr" version = "2.7.2" diff --git a/Cargo.toml b/Cargo.toml index e934995..07e97b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ heapless = "0.8" usbd-hid = "0.7.0" embedded-hal-1 = { package = "embedded-hal", version = "1.0" } +embedded-hal-02 = { package = "embedded-hal", version = "0.2.7" } embedded-hal-async = "1.0" embedded-hal-bus = { version = "0.1", features = ["async"] } embedded-io-async = { version = "0.6.1", features = ["defmt-03"] } @@ -41,5 +42,7 @@ static_cell = "2" portable-atomic = { version = "1.5", features = ["critical-section"] } log = "0.4" +mcp23017 = { version = "1.0.0" } + [profile.release] debug = 2 diff --git a/README.md b/README.md index fa57046..33dfc50 100644 --- a/README.md +++ b/README.md @@ -10,4 +10,37 @@ This project only attempts to expose the keyboard as a MIDI device. - Install `elf2uf2-rs`. - `cargo run --bin --release geode-piano` +## materials + +- 1 Raspberry Pi Pico (preferably with pre-soldered headers) +- 2 MCP23017 I/O extender chips, DIP format +- 2 pull-up resistors for I²C (1-10kΩ), these are optional but recommended +- 1 USB to Micro-USB cable with data transfer +- Many jumper cables +- Breadboard + +## wiring + +### rails + +- Pin 3 -> GND rail +- Pin 36 (3V3OUT) -> power (positive) rail + +### i2c + +Let's call the closest MCP23017 chip to the Pico MCP A, and the further one MCP B. + +- GP16 -> MCP A SDA +- GP17 -> MCP A SCL +- Pull-up resistor from GP16 to power rail +- Pull-up resistor from GP17 to power rail + +For both MCP23017s: + +- MCP RESET -> power rail +- MCP A0, A1, A2 -> GND rail for 0, power rail for 1 + - MCP A should be 0x20 (GND, GND, GND), MCP B 0x27 (3V3, 3V3, 3V3) +- MCP VDD -> power rail +- MCP VSS -> GND rail + If you are missing dependencies, consult [Alex Wilson's guide](https://www.alexdwilson.dev/learning-in-public/how-to-program-a-raspberry-pi-pico) on Rust Pico development. diff --git a/src/geode_usb.rs b/src/geode_usb.rs index ee67a64..5007b3e 100644 --- a/src/geode_usb.rs +++ b/src/geode_usb.rs @@ -81,7 +81,7 @@ pub async fn usb_task( // Create classes on the builder. let mut midi_class = MidiClass::new(&mut builder, 1, 1, 64); let logger_class = CdcAcmClass::new(&mut builder, &mut logger_state, 64); - let log_fut = embassy_usb_logger::with_class!(1024, log::LevelFilter::Info, logger_class); + let log_fut = embassy_usb_logger::with_class!(1024, log::LevelFilter::Trace, logger_class); // The `MidiClass` can be split into `Sender` and `Receiver`, to be used in separate tasks. // let (sender, receiver) = class.split(); diff --git a/src/main.rs b/src/main.rs index 598cfa7..0074e04 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,9 +4,7 @@ use embassy_executor::Spawner; use embassy_rp::bind_interrupts; use embassy_rp::gpio; -use embassy_rp::gpio::AnyPin; -use embassy_rp::gpio::Input; -use embassy_rp::gpio::Pull; +use embassy_rp::i2c; use embassy_rp::peripherals::USB; use embassy_rp::usb::{Driver, InterruptHandler}; use embassy_time::Timer; @@ -16,11 +14,28 @@ use {defmt_rtt as _, panic_probe as _}; mod geode_midi; mod geode_usb; +mod pins; bind_interrupts!(struct Irqs { USBCTRL_IRQ => InterruptHandler; }); +/// Unwrap, but log before panic +/// +/// Waits a bit to give time for the logger to flush before halting. +/// This exists because I do not own a debug probe 😎 +async fn unwrap(res: Result) -> T { + match res { + Ok(v) => v, + Err(e) => { + log::error!("[FATAL] {:?}", e); + log::error!("HALTING DUE TO PANIC."); + Timer::after_millis(10).await; + panic!(); + } + } +} + #[embassy_executor::task] async fn blink_task(pin: embassy_rp::gpio::AnyPin) { let mut led = Output::new(pin, Level::Low); @@ -30,62 +45,15 @@ async fn blink_task(pin: embassy_rp::gpio::AnyPin) { Timer::after_millis(100).await; led.set_low(); - Timer::after_secs(5).await; + Timer::after_millis(900).await; } } -enum Note { - C, - Pedal, -} - -#[embassy_executor::task(pool_size = 2)] -async fn button(pin: AnyPin, note: Note) { - let mut button = Input::new(pin, Pull::Up); - let chan = geode_midi::MidiChannel::new(0); +#[embassy_executor::task] +async fn read_task(mut pin_driver: pins::TransparentPins) { loop { - let mut counter = 10; - button.wait_for_falling_edge().await; - loop { - Timer::after_millis(5).await; - if button.is_low() { - counter -= 1; - } else { - counter = 10; - } - if counter <= 0 { - break; - } - } - match note { - Note::C => chan.note_on(72, 64).await, - Note::Pedal => { - chan.controller(geode_midi::Controller::SustainPedal, 64) - .await - } - } - log::info!("button press"); - counter = 10; - button.wait_for_rising_edge().await; - loop { - Timer::after_millis(5).await; - if button.is_high() { - counter -= 1; - } else { - counter = 10; - } - if counter <= 0 { - break; - } - } - match note { - Note::C => chan.note_off(72, 0).await, - Note::Pedal => { - chan.controller(geode_midi::Controller::SustainPedal, 0) - .await - } - } - log::info!("button release"); + log::warn!("{:b}", unwrap(pin_driver.read_all()).await); + Timer::after_millis(1000).await; } } @@ -94,11 +62,37 @@ async fn main(_spawner: Spawner) { let p = embassy_rp::init(Default::default()); let driver = Driver::new(p.USB, Irqs); + _spawner.spawn(usb_task(driver)).unwrap(); _spawner.spawn(blink_task(p.PIN_25.into())).unwrap(); - _spawner.spawn(button(p.PIN_16.into(), Note::C)).unwrap(); - _spawner - .spawn(button(p.PIN_17.into(), Note::Pedal)) - .unwrap(); - _spawner.spawn(usb_task(driver)).unwrap(); + + Timer::after_secs(2).await; + + log::info!("main: init i2c"); + let sda = p.PIN_16; + let scl = p.PIN_17; + + let mut i2c_config = i2c::Config::default(); + let freq = 100_000; + i2c_config.frequency = freq; + let i2c = i2c::I2c::new_blocking(p.I2C0, scl, sda, i2c_config); + + log::info!("main: starting transparent pin driver"); + let mut pin_driver = pins::TransparentPins::new(i2c, [0x20], []); + + log::info!("main: setting pins as input"); + for i in 0..16 { + log::debug!("main: setting pin {} as input, pull up", i); + unwrap(pin_driver.set_input(i)).await; + unwrap(pin_driver.set_pull(i, gpio::Pull::Up)).await; + } + + // these pins are faulty as inputs + // unwrap(pin_driver.set_output(7)).await; + // unwrap(pin_driver.set_output(8 + 7)).await; + // unwrap(pin_driver.set_output(16 + 7)).await; + // unwrap(pin_driver.set_output(16 + 8 + 7)).await; + + log::debug!("main: starting read task"); + _spawner.spawn(read_task(pin_driver)).unwrap(); } diff --git a/src/pins.rs b/src/pins.rs new file mode 100644 index 0000000..e025c37 --- /dev/null +++ b/src/pins.rs @@ -0,0 +1,162 @@ +/* + geode-piano + Copyright (C) 2024 dogeystamp + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +//! Manage I²C and provide a transparent pin interface for both onboard and MCP23017 pins. + +extern crate embedded_hal_02; +use embassy_rp::{ + gpio::{AnyPin, Flex, Pull}, + i2c::{self, Blocking}, + peripherals::{I2C0, I2C1}, +}; + +extern crate mcp23017; +use embassy_time::Timer; +use mcp23017::MCP23017; + +/// Number of pins driven by each MCP23017 pin extender. +const PINS_PER_EXTENDER: usize = 16; +/// Number of MCP23017 chips used. This can not be changed without changing code. +const N_PIN_EXTENDERS: usize = 1; +/// Number of pins driven directly by the board. +const N_REGULAR_PINS: usize = 0; +/// Number of total extended pins +const N_EXTENDED_PINS: usize = PINS_PER_EXTENDER * N_PIN_EXTENDERS; + +/// "Transparent pins" to consistently interface with a GPIO extender + onboard GPIO ports. +/// +/// This interface uses a single addressing scheme for all the pins it manages. +/// ext0 is 0-15, ext1 is 16-31, regular pins are 32-63. +pub struct TransparentPins { + ext0: MCP23017>, + //ext1: MCP23017>, + pins: [Flex<'static, AnyPin>; N_REGULAR_PINS], +} + +/// GPIO extender pin +struct ExtendedPin { + /// ID (not address) of the extender being used + ext_id: u8, + /// Pin number in the extender's addressing scheme + loc_pin: u8, +} + +enum TransparentPin { + /// On-board GPIO (this is an index into `TransparentPins::pins` not the Pico numbering) + Onboard(usize), + /// Extender pin + Extended(ExtendedPin), +} + +impl TransparentPins { + fn get_pin(pin: u8) -> TransparentPin { + if pin < (N_EXTENDED_PINS as u8) { + let ext_id = pin / (PINS_PER_EXTENDER as u8); + let loc_pin = pin % (PINS_PER_EXTENDER as u8); + if ext_id >= N_PIN_EXTENDERS as u8 { + panic!("invalid pin") + } + TransparentPin::Extended(ExtendedPin { ext_id, loc_pin }) + } else { + TransparentPin::Onboard(pin as usize - N_EXTENDED_PINS) + } + } + + pub fn new( + i2c0: i2c::I2c<'static, I2C0, Blocking>, + //i2c1: i2c::I2c<'static, I2C1, Blocking>, + addrs: [u8; N_PIN_EXTENDERS], + pins: [AnyPin; N_REGULAR_PINS], + ) -> Self { + let pin_init = pins.map(|x| Flex::new(x)); + return TransparentPins { + ext0: MCP23017::new(i2c0, addrs[0]).unwrap(), + pins: pin_init, + }; + } + + /// Read all pins into a single 64-bit value. + pub fn read_all(&mut self) -> Result { + log::trace!("read_all: called"); + let mut ret: u64 = 0; + // remember here port b is in the lower byte and port a in the upper byte + ret |= self.ext0.read_gpioab()? as u64; + for pin in 0..N_REGULAR_PINS { + log::trace!("pin read: {}", pin); + ret |= (self.pins[pin].is_high() as u64) << (N_EXTENDED_PINS + pin); + } + + Ok(ret) + } + + /// Set the pull on an individual pin (0-index). + /// + /// Note: MCP23017 pins do not support pull-down. + pub fn set_pull(&mut self, pin: u8, pull: Pull) -> Result<(), i2c::Error> { + let pin = TransparentPins::get_pin(pin); + match pin { + TransparentPin::Onboard(p) => { + self.pins[p].set_pull(pull); + } + TransparentPin::Extended(p) => { + let pull_on: bool = match pull { + Pull::None => false, + Pull::Up => true, + // Extended pins don't seem to support pull-down + Pull::Down => unimplemented!("MCP23017 does not support pull-down."), + }; + match p.ext_id { + 0 => self.ext0.pull_up(p.loc_pin, pull_on)?, + //1 => self.ext1.pull_up(p.loc_pin, pull_on)?, + _ => panic!("invalid pin"), + } + } + } + Ok(()) + } + + pub fn set_input(&mut self, pin: u8) -> Result<(), i2c::Error> { + let pin = TransparentPins::get_pin(pin); + match pin { + TransparentPin::Onboard(p) => self.pins[p].set_as_input(), + TransparentPin::Extended(p) => { + match p.ext_id { + 0 => self.ext0.pin_mode(p.loc_pin, mcp23017::PinMode::INPUT)?, + //1 => self.ext1.pin_mode(p.loc_pin, mcp23017::PinMode::INPUT).unwrap(), + _ => panic!("invalid pin"), + } + } + } + Ok(()) + } + + pub fn set_output(&mut self, pin: u8) -> Result<(), i2c::Error> { + let pin = TransparentPins::get_pin(pin); + match pin { + TransparentPin::Onboard(p) => self.pins[p].set_as_output(), + TransparentPin::Extended(p) => { + match p.ext_id { + 0 => self.ext0.pin_mode(p.loc_pin, mcp23017::PinMode::OUTPUT)?, + //1 => self.ext1.pin_mode(p.loc_pin, mcp23017::PinMode::OUTPUT).unwrap(), + _ => panic!("invalid pin"), + } + } + } + Ok(()) + } +}