Compare commits

...

3 Commits

Author SHA1 Message Date
2bbabaff5c
feat: support multiple pin extenders on the I²C bus 2024-04-14 16:07:42 -04:00
760a390680
chore: reformat, remove unused things 2024-04-14 10:16:22 -04:00
05668f56d6
src/pins.rs: make TransparentPins struct
this interfaces with an MCP23017
2024-04-14 10:13:59 -04:00
8 changed files with 303 additions and 66 deletions

View File

@ -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
View File

@ -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"

View File

@ -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

View File

@ -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.

View File

@ -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
View 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(())
}
}

View File

@ -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();