src/pins.rs: make TransparentPins struct

this interfaces with an MCP23017
This commit is contained in:
dogeystamp 2024-04-09 21:31:39 -04:00
parent 448b8e232c
commit 05668f56d6
Signed by: dogeystamp
GPG Key ID: 7225FE3592EFFA38
7 changed files with 265 additions and 62 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+

11
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,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"

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,7 @@ static_cell = "2"
portable-atomic = { version = "1.5", features = ["critical-section"] }
log = "0.4"
mcp23017 = { version = "1.0.0" }
[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

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

View File

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

162
src/pins.rs Normal file
View File

@ -0,0 +1,162 @@
/*
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.
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<i2c::I2c<'static, I2C0, Blocking>>,
//ext1: MCP23017<i2c::I2c<'static, I2C1, Blocking>>,
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<u64, i2c::Error> {
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(())
}
}