Compare commits
3 Commits
448b8e232c
...
2bbabaff5c
Author | SHA1 | Date | |
---|---|---|---|
2bbabaff5c | |||
760a390680 | |||
05668f56d6 |
@ -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+
|
||||
|
22
Cargo.lock
generated
22
Cargo.lock
generated
@ -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,8 +799,10 @@ dependencies = [
|
||||
"futures",
|
||||
"heapless 0.8.0",
|
||||
"log",
|
||||
"mcp23017",
|
||||
"panic-probe",
|
||||
"portable-atomic",
|
||||
"shared-bus",
|
||||
"smart-leds",
|
||||
"static_cell",
|
||||
"usbd-hid 0.7.0",
|
||||
@ -984,6 +987,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"
|
||||
@ -1373,6 +1385,16 @@ dependencies = [
|
||||
"syn 2.0.58",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shared-bus"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6b8d3f0e34309c22ca4a9a27d24fa493e31573485f3493802b75b9d706756a6"
|
||||
dependencies = [
|
||||
"embedded-hal 0.2.7",
|
||||
"nb 1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "0.3.11"
|
||||
|
@ -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,8 @@ static_cell = "2"
|
||||
portable-atomic = { version = "1.5", features = ["critical-section"] }
|
||||
log = "0.4"
|
||||
|
||||
mcp23017 = { version = "1.0.0" }
|
||||
shared-bus = "0.3.1"
|
||||
|
||||
[profile.release]
|
||||
debug = 2
|
||||
|
33
README.md
33
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.
|
||||
|
121
src/main.rs
121
src/main.rs
@ -1,26 +1,42 @@
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
#![deny(rust_2018_idioms)]
|
||||
|
||||
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;
|
||||
use geode_usb::usb_task;
|
||||
use gpio::{Level, Output};
|
||||
use usb::usb_task;
|
||||
use {defmt_rtt as _, panic_probe as _};
|
||||
|
||||
mod geode_midi;
|
||||
mod geode_usb;
|
||||
mod midi;
|
||||
mod pins;
|
||||
mod usb;
|
||||
|
||||
bind_interrupts!(struct Irqs {
|
||||
USBCTRL_IRQ => InterruptHandler<USB>;
|
||||
});
|
||||
|
||||
/// 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<T, E: core::fmt::Debug>(res: Result<T, E>) -> 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 +46,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 +63,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, 0x27], []);
|
||||
|
||||
log::info!("main: setting pins as input");
|
||||
for i in 0..pins::N_EXTENDED_PINS {
|
||||
log::debug!("main: setting pin {} as input, pull up", i);
|
||||
unwrap(pin_driver.set_input(i as u8)).await;
|
||||
unwrap(pin_driver.set_pull(i as u8, 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();
|
||||
}
|
||||
|
183
src/pins.rs
Normal file
183
src/pins.rs
Normal file
@ -0,0 +1,183 @@
|
||||
/*
|
||||
geode-piano
|
||||
Copyright (C) 2024 dogeystamp <dogeystamp@disroot.org>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! Manage I²C and provide a transparent pin interface for both onboard and MCP23017 pins.
|
||||
|
||||
use embassy_rp::{
|
||||
gpio::{AnyPin, Flex, Pull},
|
||||
i2c::{self, Blocking},
|
||||
peripherals::I2C0,
|
||||
};
|
||||
|
||||
use mcp23017;
|
||||
use mcp23017::MCP23017;
|
||||
|
||||
/// Number of pins driven by each MCP23017 pin extender.
|
||||
pub const PINS_PER_EXTENDER: usize = 16;
|
||||
/// Number of MCP23017 chips used.
|
||||
pub const N_PIN_EXTENDERS: usize = 2;
|
||||
/// Number of pins driven directly by the board.
|
||||
pub const N_REGULAR_PINS: usize = 0;
|
||||
/// Number of total extended pins
|
||||
pub const N_EXTENDED_PINS: usize = PINS_PER_EXTENDER * N_PIN_EXTENDERS;
|
||||
|
||||
type I2cPeripheral = i2c::I2c<'static, I2C0, Blocking>;
|
||||
type I2cBus = shared_bus::BusManagerSimple<I2cPeripheral>;
|
||||
|
||||
/// GPIO extender pin
|
||||
struct ExtendedPin {
|
||||
/// Index of extender being used
|
||||
ext_id: usize,
|
||||
/// 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),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
InvalidPin(u8),
|
||||
I2cError(i2c::Error),
|
||||
ExtenderError,
|
||||
}
|
||||
|
||||
impl From<i2c::Error> for Error {
|
||||
fn from(err: i2c::Error) -> Error {
|
||||
Error::I2cError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> From<mcp23017::Error<E>> for Error {
|
||||
fn from(_err: mcp23017::Error<E>) -> Error {
|
||||
Error::ExtenderError
|
||||
}
|
||||
}
|
||||
|
||||
/// "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.
|
||||
/// `ext[0]` is 0-15, `ext[1]` is 16-31, regular pins are 32-63.
|
||||
pub struct TransparentPins {
|
||||
addrs: [u8; N_PIN_EXTENDERS],
|
||||
pins: [Flex<'static, AnyPin>; N_REGULAR_PINS],
|
||||
i2c_bus: I2cBus,
|
||||
}
|
||||
|
||||
/// Create a new short-lived MCP23017 struct.
|
||||
///
|
||||
/// This is needed because our bus proxy uses references to the bus,
|
||||
/// and having long-lived references angers the borrow-checker
|
||||
macro_rules! extender {
|
||||
($self:ident,$ext_id:expr) => {
|
||||
MCP23017::new($self.i2c_bus.acquire_i2c(), $self.addrs[$ext_id])
|
||||
};
|
||||
}
|
||||
|
||||
impl TransparentPins {
|
||||
fn get_pin(&mut self, pin: u8) -> Result<TransparentPin, Error> {
|
||||
if pin as usize >= N_EXTENDED_PINS + N_REGULAR_PINS {
|
||||
return Err(Error::InvalidPin(pin));
|
||||
}
|
||||
if pin < (N_EXTENDED_PINS as u8) {
|
||||
let ext_id = (pin as usize) / PINS_PER_EXTENDER;
|
||||
let loc_pin = pin % (PINS_PER_EXTENDER as u8);
|
||||
Ok(TransparentPin::Extended(ExtendedPin { ext_id, loc_pin }))
|
||||
} else {
|
||||
Ok(TransparentPin::Onboard(pin as usize - N_EXTENDED_PINS))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
i2c: i2c::I2c<'static, I2C0, Blocking>,
|
||||
addrs: [u8; N_PIN_EXTENDERS],
|
||||
pins: [AnyPin; N_REGULAR_PINS],
|
||||
) -> Self {
|
||||
TransparentPins {
|
||||
addrs,
|
||||
pins: pins.map(|x| Flex::new(x)),
|
||||
i2c_bus: shared_bus::BusManagerSimple::new(i2c),
|
||||
}
|
||||
}
|
||||
|
||||
/// Read all pins into a single 64-bit value.
|
||||
///
|
||||
/// For a given extender's range, port B is in the lower byte and port A in the upper byte.
|
||||
pub fn read_all(&mut self) -> Result<u64, Error> {
|
||||
log::trace!("read_all: called");
|
||||
let mut ret: u64 = 0;
|
||||
for i in 0..N_PIN_EXTENDERS {
|
||||
let mut ext = extender!(self, i)?;
|
||||
ret |= (ext.read_gpioab()? as u64) << (i * PINS_PER_EXTENDER);
|
||||
}
|
||||
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<(), Error> {
|
||||
let pin = self.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."),
|
||||
};
|
||||
extender!(self, p.ext_id)?.pull_up(p.loc_pin, pull_on)?
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_input(&mut self, pin: u8) -> Result<(), Error> {
|
||||
let pin = self.get_pin(pin)?;
|
||||
match pin {
|
||||
TransparentPin::Onboard(p) => self.pins[p].set_as_input(),
|
||||
TransparentPin::Extended(p) => {
|
||||
extender!(self, p.ext_id)?.pin_mode(p.loc_pin, mcp23017::PinMode::INPUT)?
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_output(&mut self, pin: u8) -> Result<(), Error> {
|
||||
let pin = self.get_pin(pin)?;
|
||||
match pin {
|
||||
TransparentPin::Onboard(p) => self.pins[p].set_as_output(),
|
||||
TransparentPin::Extended(p) => {
|
||||
extender!(self, p.ext_id)?.pin_mode(p.loc_pin, mcp23017::PinMode::OUTPUT)?
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -33,7 +33,7 @@ DEALINGS IN THE SOFTWARE.
|
||||
use embassy_futures::join::join;
|
||||
use embassy_rp::{peripherals::USB, usb::Driver};
|
||||
|
||||
use crate::geode_midi::midi_session;
|
||||
use crate::midi::midi_session;
|
||||
use embassy_usb::class::cdc_acm::CdcAcmClass;
|
||||
use embassy_usb::class::cdc_acm::State;
|
||||
use embassy_usb::class::midi::MidiClass;
|
||||
@ -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();
|
Loading…
Reference in New Issue
Block a user