feat: debug probe support
This commit is contained in:
parent
6ba735bd2c
commit
29cf1459d8
@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
|
|
||||||
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
|
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
|
||||||
runner = "elf2uf2-rs --deploy --serial"
|
runner = "probe-rs run --chip RP2040"
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
target = "thumbv6m-none-eabi" # Cortex-M0 and Cortex-M0+
|
target = "thumbv6m-none-eabi" # Cortex-M0 and Cortex-M0+
|
||||||
|
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -772,7 +772,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "geode_piano"
|
name = "geode_piano"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"byte-slice-cast 1.2.2",
|
"byte-slice-cast 1.2.2",
|
||||||
"cortex-m",
|
"cortex-m",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "geode_piano"
|
name = "geode_piano"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
|
|
||||||
|
38
README.md
38
README.md
@ -21,11 +21,20 @@ https://github.com/dogeystamp/geode-piano/assets/61116261/2a5f732a-5d3e-4b5f-946
|
|||||||
|
|
||||||
## installation
|
## installation
|
||||||
|
|
||||||
|
- Follow the materials and wiring sections below.
|
||||||
- Clone project.
|
- Clone project.
|
||||||
- Go into project directory.
|
- Go into project directory.
|
||||||
- Install the `thumbv6m-none-eabi` target using rustup.
|
- Install the `thumbv6m-none-eabi` target using rustup.
|
||||||
|
|
||||||
|
You now have two choices for installation.
|
||||||
|
You can either use a debug probe such as the [Raspberry Pi Debug Probe](https://www.raspberrypi.com/products/debug-probe/), or install over USB.
|
||||||
|
The debug probe is more convenient
|
||||||
|
for iterating quickly,
|
||||||
|
but is harder to set up.
|
||||||
|
|
||||||
|
### no debug probe
|
||||||
- Install `elf2uf2-rs`.
|
- Install `elf2uf2-rs`.
|
||||||
- Follow the materials and wiring sections below.
|
- Set `runner = "elf2uf2-rs --deploy --serial"` in `.cargo/config`.
|
||||||
- Set the Pico into BOOTSEL mode:
|
- Set the Pico into BOOTSEL mode:
|
||||||
- Hold down the BOOTSEL button on the Pico. Keep holding it during the following step.
|
- Hold down the BOOTSEL button on the Pico. Keep holding it during the following step.
|
||||||
- Reset the Pico: either replug the power, or short Pin 30 (RUN) to GND through a button or wire.
|
- Reset the Pico: either replug the power, or short Pin 30 (RUN) to GND through a button or wire.
|
||||||
@ -33,8 +42,28 @@ https://github.com/dogeystamp/geode-piano/assets/61116261/2a5f732a-5d3e-4b5f-946
|
|||||||
- `cargo run --release --bin [binary]`
|
- `cargo run --release --bin [binary]`
|
||||||
- `[binary]` can be any binary under `src/bin/`. Run `cargo run --bin` to list them.
|
- `[binary]` can be any binary under `src/bin/`. Run `cargo run --bin` to list them.
|
||||||
|
|
||||||
|
### with debug probe
|
||||||
|
|
||||||
|
- Install `probe-rs`.
|
||||||
|
- Follow the wiring instructions in the [Pico Getting Started Guide](https://datasheets.raspberrypi.com/pico/getting-started-with-pico.pdf), at _Appendix A: Using Picoprobe_ in the Picoprobe Wiring section.
|
||||||
|
You only need to wire GND, SWCLK and SWDIO.
|
||||||
|
- If you are using a second Pico as a debug probe,
|
||||||
|
you must use a second USB data wire to communicate with both the debug probe and the geode-piano board.
|
||||||
|
- `cargo run --release --bin [binary]`
|
||||||
|
- `[binary]` can be any binary under `src/bin/`. Run `cargo run --bin` to list them.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
Note that essential program output (e.g. pin scanner output)
|
||||||
|
goes through Embassy's USB serial logger rather than the defmt log that the debug probe has access to.
|
||||||
|
To see this output, install picocom and run
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo picocom -b 115200 /dev/ttyACM0
|
||||||
|
```
|
||||||
|
|
||||||
|
changing `ttyACM0` to whichever serial device your Pico may be using.
|
||||||
|
|
||||||
## usage
|
## usage
|
||||||
|
|
||||||
The intended usage is to first plug the device into the piano keyboard, then use the `pin_scanner` binary to
|
The intended usage is to first plug the device into the piano keyboard, then use the `pin_scanner` binary to
|
||||||
@ -60,8 +89,7 @@ Copy the keymap, as well as the `col_pins` and `row_pins` generated into this.
|
|||||||
|
|
||||||
Once the keymap is done, run the `piano_firmware` binary and plug the USB cable to your computer.
|
Once the keymap is done, run the `piano_firmware` binary and plug the USB cable to your computer.
|
||||||
Open up a DAW and select Geode-Piano as a MIDI input device.
|
Open up a DAW and select Geode-Piano as a MIDI input device.
|
||||||
I use LMMS with the [Maestro Concert Grand v2](https://www.linuxsampler.org/instruments.html) samples.
|
If you don't need a full DAW, you can use `qsampler` with, for example, the [Maestro Concert Grand v2](https://www.linuxsampler.org/instruments.html) samples.
|
||||||
If you don't need all of LMMS's features, `qsampler` can work too.
|
|
||||||
You should be able to play now.
|
You should be able to play now.
|
||||||
|
|
||||||
Optionally, you can also hook up a speaker to the computer for better sound quality.
|
Optionally, you can also hook up a speaker to the computer for better sound quality.
|
||||||
@ -78,6 +106,10 @@ Optionally, you can also hook up a speaker to the computer for better sound qual
|
|||||||
- Many jumper cables (40 male-to-female, ? male-to-male)
|
- Many jumper cables (40 male-to-female, ? male-to-male)
|
||||||
- Two alligator clips
|
- Two alligator clips
|
||||||
- Breadboard
|
- Breadboard
|
||||||
|
- (optional) 1 debug probe, could be a second Raspberry Pi Pico
|
||||||
|
- and necessary wires to use it, for example
|
||||||
|
- 3-pin JST-SH cable
|
||||||
|
- data micro-USB to USB cable
|
||||||
|
|
||||||
For the ribbon cable sockets, open up your piano and find the ribbon cables.
|
For the ribbon cable sockets, open up your piano and find the ribbon cables.
|
||||||
Unplug them from the PCB, and count the amount of pins on them.
|
Unplug them from the PCB, and count the amount of pins on them.
|
||||||
|
@ -460,7 +460,7 @@ async fn main(_spawner: Spawner) {
|
|||||||
unwrap(_spawner.spawn(usb_task(driver, log::LevelFilter::Debug))).await;
|
unwrap(_spawner.spawn(usb_task(driver, log::LevelFilter::Debug))).await;
|
||||||
unwrap(_spawner.spawn(blinky::blink_task(p.PIN_25.into()))).await;
|
unwrap(_spawner.spawn(blinky::blink_task(p.PIN_25.into()))).await;
|
||||||
|
|
||||||
log::debug!("main: init i2c");
|
defmt::debug!("main: init i2c");
|
||||||
let sda = p.PIN_16;
|
let sda = p.PIN_16;
|
||||||
let scl = p.PIN_17;
|
let scl = p.PIN_17;
|
||||||
|
|
||||||
@ -469,7 +469,7 @@ async fn main(_spawner: Spawner) {
|
|||||||
i2c_config.frequency = freq;
|
i2c_config.frequency = freq;
|
||||||
let i2c = i2c::I2c::new_blocking(p.I2C0, scl, sda, i2c_config);
|
let i2c = i2c::I2c::new_blocking(p.I2C0, scl, sda, i2c_config);
|
||||||
|
|
||||||
log::debug!("main: starting transparent pin driver");
|
defmt::debug!("main: starting transparent pin driver");
|
||||||
let pin_driver = unwrap(pins::TransparentPins::new(
|
let pin_driver = unwrap(pins::TransparentPins::new(
|
||||||
i2c,
|
i2c,
|
||||||
[0x20, 0x27],
|
[0x20, 0x27],
|
||||||
@ -481,15 +481,15 @@ async fn main(_spawner: Spawner) {
|
|||||||
))
|
))
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
log::info!("main: starting piano task");
|
defmt::info!("main: starting piano task");
|
||||||
_spawner.spawn(piano_task(pin_driver)).unwrap();
|
_spawner.spawn(piano_task(pin_driver)).unwrap();
|
||||||
|
|
||||||
log::info!("main: starting sustain pedal task");
|
defmt::info!("main: starting sustain pedal task");
|
||||||
_spawner
|
_spawner
|
||||||
.spawn(matrix::pedal(
|
.spawn(matrix::pedal(
|
||||||
midi::Controller::SustainPedal,
|
midi::Controller::SustainPedal,
|
||||||
p.PIN_8.into(),
|
p.PIN_8.into(),
|
||||||
false,
|
true,
|
||||||
))
|
))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
@ -13,11 +13,13 @@ pub mod midi;
|
|||||||
pub mod pins;
|
pub mod pins;
|
||||||
pub mod usb;
|
pub mod usb;
|
||||||
|
|
||||||
/// Unwrap, but log before panic
|
/// Wrapper over unwrap.
|
||||||
///
|
///
|
||||||
/// Waits a bit to give time for the logger to flush before halting.
|
/// Logs over usb instead of instantly panicking.
|
||||||
/// This exists because I do not own a debug probe 😎
|
/// If you don't have a debug probe, comment out the first line.
|
||||||
pub async fn unwrap<T, E: core::fmt::Debug>(res: Result<T, E>) -> T {
|
pub async fn unwrap<T, E: core::fmt::Debug>(res: Result<T, E>) -> T {
|
||||||
|
return res.unwrap();
|
||||||
|
#[allow(unreachable_code)]
|
||||||
match res {
|
match res {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
@ -19,10 +19,10 @@ pub async fn pedal(pedal: midi::Controller, pin: gpio::AnyPin, norm_open: bool)
|
|||||||
let off_val = if norm_open { 0 } else { 64 };
|
let off_val = if norm_open { 0 } else { 64 };
|
||||||
inp.wait_for_low().await;
|
inp.wait_for_low().await;
|
||||||
chan.controller(pedal, on_val).await;
|
chan.controller(pedal, on_val).await;
|
||||||
log::debug!("{pedal:?} set to {on_val}");
|
defmt::debug!("{} set to {}", pedal, on_val);
|
||||||
inp.wait_for_high().await;
|
inp.wait_for_high().await;
|
||||||
chan.controller(pedal, off_val).await;
|
chan.controller(pedal, off_val).await;
|
||||||
log::debug!("{pedal:?} set to {off_val}");
|
defmt::debug!("{} set to {}", pedal, off_val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,18 +73,27 @@ impl<const N_ROWS: usize, const N_COLS: usize> KeyMatrix<N_ROWS, N_COLS> {
|
|||||||
let mut note_first: [Option<Instant>; MAX_NOTES] = [None; MAX_NOTES];
|
let mut note_first: [Option<Instant>; MAX_NOTES] = [None; MAX_NOTES];
|
||||||
|
|
||||||
let mut counter = 0;
|
let mut counter = 0;
|
||||||
|
let mut prof_col_idx = 0;
|
||||||
|
|
||||||
|
defmt::debug!("using {} columns", N_COLS);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
let profile: bool = counter == 0;
|
||||||
counter += 1;
|
counter += 1;
|
||||||
counter %= 50;
|
counter %= 50;
|
||||||
let profile = counter == 0;
|
|
||||||
let prof_start = Instant::now();
|
let prof_start = Instant::now();
|
||||||
|
let mut prof_time_last_col = prof_start;
|
||||||
|
let mut prof_dur_col = Duration::from_ticks(0);
|
||||||
|
|
||||||
for (i, col) in self.col_pins.iter().enumerate() {
|
for (i, col) in self.col_pins.iter().enumerate() {
|
||||||
unwrap(pin_driver.set_output(*col)).await;
|
unwrap(pin_driver.set_output(*col)).await;
|
||||||
let input = unwrap(pin_driver.read_all()).await;
|
let input = unwrap(pin_driver.read_all()).await;
|
||||||
unwrap(pin_driver.set_input(*col)).await;
|
unwrap(pin_driver.set_input(*col)).await;
|
||||||
|
|
||||||
|
if profile && i == prof_col_idx {
|
||||||
|
prof_dur_col = prof_time_last_col.elapsed();
|
||||||
|
}
|
||||||
|
|
||||||
// values that are logical ON
|
// values that are logical ON
|
||||||
let mask = input ^ (((1 << pin_driver.n_usable_pins()) - 1) ^ (1 << col));
|
let mask = input ^ (((1 << pin_driver.n_usable_pins()) - 1) ^ (1 << col));
|
||||||
for (j, row) in self.row_pins.iter().enumerate() {
|
for (j, row) in self.row_pins.iter().enumerate() {
|
||||||
@ -111,7 +120,7 @@ impl<const N_ROWS: usize, const N_COLS: usize> KeyMatrix<N_ROWS, N_COLS> {
|
|||||||
} else {
|
} else {
|
||||||
(127 - min(dur, 250) / 5 - 70) as u8
|
(127 - min(dur, 250) / 5 - 70) as u8
|
||||||
};
|
};
|
||||||
log::debug!("{note:?} velocity {velocity} from dur {dur}ms");
|
defmt::debug!("{} velocity {} from dur {}ms", note, velocity, dur);
|
||||||
note_on[note as usize] = true;
|
note_on[note as usize] = true;
|
||||||
chan.note_on(note, velocity).await;
|
chan.note_on(note, velocity).await;
|
||||||
}
|
}
|
||||||
@ -134,11 +143,20 @@ impl<const N_ROWS: usize, const N_COLS: usize> KeyMatrix<N_ROWS, N_COLS> {
|
|||||||
midi::KeyAction::NOP => {}
|
midi::KeyAction::NOP => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
prof_time_last_col = Instant::now();
|
||||||
|
}
|
||||||
|
if profile {
|
||||||
|
let time_total = prof_start.elapsed();
|
||||||
|
prof_col_idx += 1;
|
||||||
|
prof_col_idx %= N_COLS;
|
||||||
|
defmt::debug!(
|
||||||
|
"profile: total scan took {}us, {}-th column {}us",
|
||||||
|
time_total.as_micros(),
|
||||||
|
prof_col_idx,
|
||||||
|
prof_dur_col.as_micros()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if profile {
|
|
||||||
log::trace!("profile: scan took {}ms", prof_start.elapsed().as_millis())
|
|
||||||
}
|
|
||||||
ticker.next().await;
|
ticker.next().await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
use embassy_rp::usb::{Driver, Instance};
|
use embassy_rp::usb::{Driver, Instance};
|
||||||
use embassy_sync::{blocking_mutex::raw::ThreadModeRawMutex, channel::Channel};
|
use embassy_sync::{blocking_mutex::raw::ThreadModeRawMutex, channel::Channel};
|
||||||
use embassy_usb::{class::midi::MidiClass, driver::EndpointError};
|
use embassy_usb::{class::midi::MidiClass, driver::EndpointError};
|
||||||
|
use defmt::Format;
|
||||||
|
|
||||||
////////////////////////////////
|
////////////////////////////////
|
||||||
////////////////////////////////
|
////////////////////////////////
|
||||||
@ -42,7 +43,7 @@ impl NoteMsg {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug, Format)]
|
||||||
pub enum Controller {
|
pub enum Controller {
|
||||||
SustainPedal = 64,
|
SustainPedal = 64,
|
||||||
}
|
}
|
||||||
@ -86,7 +87,7 @@ impl MidiMsg {
|
|||||||
/// Note identifiers
|
/// Note identifiers
|
||||||
///
|
///
|
||||||
/// See src/midi/note_def.py for how this is generated
|
/// See src/midi/note_def.py for how this is generated
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug, Format)]
|
||||||
pub enum Note {
|
pub enum Note {
|
||||||
A0 = 21,
|
A0 = 21,
|
||||||
AS0 = 22,
|
AS0 = 22,
|
||||||
@ -225,13 +226,13 @@ pub async fn midi_session<'d, T: Instance + 'd>(
|
|||||||
let status: u8 = (if note.on { 0b1001_0000 } else { 0b1000_0000 }) | msg.channel;
|
let status: u8 = (if note.on { 0b1001_0000 } else { 0b1000_0000 }) | msg.channel;
|
||||||
// i'll be honest i have no idea where the first number here comes from
|
// i'll be honest i have no idea where the first number here comes from
|
||||||
let packet = [8, status, note.note as u8, note.velocity];
|
let packet = [8, status, note.note as u8, note.velocity];
|
||||||
log::trace!("midi_session: note {:?}", packet);
|
defmt::trace!("midi_session: note {:?}", packet);
|
||||||
midi.write_packet(&packet).await?
|
midi.write_packet(&packet).await?
|
||||||
}
|
}
|
||||||
MsgType::Controller(ctrl) => {
|
MsgType::Controller(ctrl) => {
|
||||||
let status: u8 = (0b1011_0000) | msg.channel;
|
let status: u8 = (0b1011_0000) | msg.channel;
|
||||||
let packet = [8, status, ctrl.controller as u8, ctrl.value];
|
let packet = [8, status, ctrl.controller as u8, ctrl.value];
|
||||||
log::trace!("midi_session: control {:?}", packet);
|
defmt::trace!("midi_session: control {:?}", packet);
|
||||||
midi.write_packet(&packet).await?
|
midi.write_packet(&packet).await?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -209,7 +209,7 @@ impl TransparentPins {
|
|||||||
ret.pins.n_usable = ret.usable_extended_pins + N_REGULAR_PINS;
|
ret.pins.n_usable = ret.usable_extended_pins + N_REGULAR_PINS;
|
||||||
}
|
}
|
||||||
ret.disable_unsafe_pins = true;
|
ret.disable_unsafe_pins = true;
|
||||||
log::debug!("TransparentPins: {} usable pins", ret.pins.n_usable)
|
defmt::debug!("TransparentPins: {} usable pins", ret.pins.n_usable)
|
||||||
}
|
}
|
||||||
Ok(ret)
|
Ok(ret)
|
||||||
}
|
}
|
||||||
@ -221,7 +221,7 @@ impl TransparentPins {
|
|||||||
// ports are flipped from what it should be
|
// ports are flipped from what it should be
|
||||||
let port_a = (val & (0xff00)) >> 8;
|
let port_a = (val & (0xff00)) >> 8;
|
||||||
let port_b = val & (0x00ff);
|
let port_b = val & (0x00ff);
|
||||||
log::trace!("raw_to_usable: raw {val:016b} a {port_a:08b} b {port_b:08b}");
|
defmt::trace!("raw_to_usable: raw {:016b} a {:08b} b {:08b}", val, port_a, port_b);
|
||||||
(port_a & 0x7f) | ((port_b & 0x7f) << 7)
|
(port_a & 0x7f) | ((port_b & 0x7f) << 7)
|
||||||
} else {
|
} else {
|
||||||
val
|
val
|
||||||
@ -239,7 +239,7 @@ impl TransparentPins {
|
|||||||
|
|
||||||
/// Write all pins from a single 64-bit value.
|
/// Write all pins from a single 64-bit value.
|
||||||
pub fn write_all(&mut self, val: u64) -> Result<(), Error> {
|
pub fn write_all(&mut self, val: u64) -> Result<(), Error> {
|
||||||
log::trace!("write_all: called with val {}", val);
|
defmt::trace!("write_all: called with val {}", val);
|
||||||
for i in 0..N_PIN_EXTENDERS {
|
for i in 0..N_PIN_EXTENDERS {
|
||||||
// value for this extender
|
// value for this extender
|
||||||
let ext_val = (val >> (i * self.usable_pins_per_extender))
|
let ext_val = (val >> (i * self.usable_pins_per_extender))
|
||||||
@ -259,7 +259,7 @@ impl TransparentPins {
|
|||||||
|
|
||||||
/// Read all pins into a single 64-bit value.
|
/// Read all pins into a single 64-bit value.
|
||||||
pub fn read_all(&mut self) -> Result<u64, Error> {
|
pub fn read_all(&mut self) -> Result<u64, Error> {
|
||||||
log::trace!("read_all: called");
|
defmt::trace!("read_all: called");
|
||||||
let mut ret: u64 = 0;
|
let mut ret: u64 = 0;
|
||||||
for i in 0..N_PIN_EXTENDERS {
|
for i in 0..N_PIN_EXTENDERS {
|
||||||
let mut ext = extender!(self, i)?;
|
let mut ext = extender!(self, i)?;
|
||||||
|
@ -47,10 +47,10 @@ pub async fn usb_task(
|
|||||||
log_level: log::LevelFilter,
|
log_level: log::LevelFilter,
|
||||||
) {
|
) {
|
||||||
// Create embassy-usb Config
|
// Create embassy-usb Config
|
||||||
let mut config = Config::new(0xc0de, 0xcafe);
|
let mut config = Config::new(0xdead, 0xbeef);
|
||||||
config.manufacturer = Some("dogeystamp");
|
config.manufacturer = Some("dogeystamp");
|
||||||
config.product = Some("Geode-Piano MIDI keyboard");
|
config.product = Some("Geode-Piano MIDI keyboard");
|
||||||
config.serial_number = Some("alpha-12345");
|
config.serial_number = Some("0.2.0");
|
||||||
config.max_power = 100;
|
config.max_power = 100;
|
||||||
config.max_packet_size_0 = 64;
|
config.max_packet_size_0 = 64;
|
||||||
|
|
||||||
@ -97,9 +97,9 @@ pub async fn usb_task(
|
|||||||
let midi_fut = async {
|
let midi_fut = async {
|
||||||
loop {
|
loop {
|
||||||
midi_class.wait_connection().await;
|
midi_class.wait_connection().await;
|
||||||
log::info!("Connected");
|
defmt::info!("Connected");
|
||||||
let _ = midi_session(&mut midi_class).await;
|
let _ = midi_session(&mut midi_class).await;
|
||||||
log::info!("Disconnected");
|
defmt::info!("Disconnected");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user