├── .cargo-ok ├── .cargo └── config ├── .gitignore ├── .rustfmt.toml ├── Cargo.toml ├── LICENSE ├── README.md ├── build.rs ├── memory.x ├── openocd.cfg ├── openocd.gdb └── src ├── main.rs ├── state.rs ├── stm32f1xx.rs ├── switch.rs └── usb.rs /.cargo-ok: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btrepp/rust-midi-stomp/f17fff89a8fd81625eac92ee833244d38c87292f/.cargo-ok -------------------------------------------------------------------------------- /.cargo/config: -------------------------------------------------------------------------------- 1 | [target.thumbv7m-none-eabi] 2 | # uncomment this to make `cargo run` execute programs on QEMU 3 | # runner = "qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel" 4 | 5 | [target.'cfg(all(target_arch = "arm", target_os = "none"))'] 6 | # uncomment ONE of these three option to make `cargo run` start a GDB session 7 | # which option to pick depends on your system 8 | runner = "arm-none-eabi-gdb -q -x openocd.gdb" 9 | # runner = "gdb-multiarch -q -x openocd.gdb" 10 | # runner = "gdb -q -x openocd.gdb" 11 | 12 | rustflags = [ 13 | # LLD (shipped with the Rust toolchain) is used as the default linker 14 | "-C", "link-arg=-Tlink.x", 15 | 16 | # if you run into problems with LLD switch to the GNU linker by commenting out 17 | # this line 18 | # "-C", "linker=arm-none-eabi-ld", 19 | 20 | # if you need to link to pre-compiled C libraries provided by a C toolchain 21 | # use GCC as the linker by commenting out both lines above and then 22 | # uncommenting the three lines below 23 | # "-C", "linker=arm-none-eabi-gcc", 24 | # "-C", "link-arg=-Wl,-Tlink.x", 25 | # "-C", "link-arg=-nostartfiles", 26 | ] 27 | 28 | [build] 29 | # Pick ONE of these compilation targets 30 | # target = "thumbv6m-none-eabi" # Cortex-M0 and Cortex-M0+ 31 | target = "thumbv7m-none-eabi" # Cortex-M3 32 | # target = "thumbv7em-none-eabi" # Cortex-M4 and Cortex-M7 (no FPU) 33 | # target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU) 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/*.rs.bk 2 | .#* 3 | .gdb_history 4 | Cargo.lock 5 | target/ 6 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 80 -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["beau trepp "] 3 | edition = "2018" 4 | readme = "README.md" 5 | name = "rust-midi-stomp" 6 | version = "0.1.0" 7 | 8 | [dependencies] 9 | cortex-m = "0.6.0" 10 | cortex-m-rt = "0.6.13" 11 | cortex-m-rtic = { version= "0.5.5"} 12 | cortex-m-semihosting = "0.3.5" 13 | panic-halt = "0.2.0" 14 | panic-semihosting = "0.5.4" 15 | usb-device = {version = "0.2.3"} 16 | stm32f1xx-hal = {version="0.7.0", features = ["rt", "stm32f103", "stm32-usbd"]} 17 | stm32-usbd = { version = "0.5.1", features = ["ram_access_1x16"] } 18 | usbd-midi = { version= "0.2.0" } 19 | embedded-hal = "0.2.4" 20 | heapless = "0.5.6" 21 | 22 | # this lets you use `cargo fix`! 23 | [[bin]] 24 | name = "rust-midi-stomp" 25 | test = false 26 | bench = false 27 | 28 | [profile.release] 29 | codegen-units = 1 # better optimizations 30 | debug = true # symbols are nice and they don't increase the size on Flash 31 | lto = true # better optimizations 32 | 33 | [profile.dev] 34 | opt-level = "z" 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Beau Trepp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Usb Midi stompbox in rust 2 | 3 | This project is an embedded firmware to create a 'stompbox' that 4 | sends midi signals over usb. 5 | 6 | This is similar in concept to 7 | https://www.behringer.com/Categories/Behringer/Accessories/Midi-Foot-Controllers/FCB1010/p/P0089#googtrans(en|en) 8 | 9 | # Design 10 | 11 | This is currently using RTFM to provide task scheduling. 12 | The usb drivers are implemented using the usb-hal. 13 | Ultimately the bulk of the logic is in the usbd-midi crate. 14 | 15 | Hopefully this should be compatible with other devices that support the HAL. 16 | 17 | While there is existing products and even firmwares that achieve this, this 18 | project aims to use embedded rust to explore how useful rust is in an 19 | embedded context. 20 | 21 | # Priorities 22 | 23 | This currently uses RTFM's priorities to schedule 'tasks' as follows. 24 | 1. Processing usb 25 | 2. Sending Midi 26 | 3. Reading IO/Status LEd 27 | 28 | Processing usb needs to be done at the highest priority, otherwise the device 29 | can 'malfunction' in the usb stack. This is also interrupt driven, 30 | so we are handling the USB side fast. 31 | 32 | Then the sending of MIDI messages into the usb stack is prioritized. 33 | 34 | The actually IO is currently ran with the lowest priority, as if things 35 | are missed here it is not the end of the world. 36 | 37 | 38 | # What works 39 | 40 | Currently will send a midi message when the state of PA4 (bluepill) 41 | changes. Will send a MIDI ON message on the rising edge, and a MIDI OFF message 42 | on the falling edge. 43 | 44 | The whole system is almost entirely interrupt driven, only the LED blinking is 45 | not, but this may be possible using PWM instead, and changing the LED based on 46 | panics/other tasks. 47 | 48 | # Contributions 49 | 50 | This is my first endevaour in rust and embedded rust, feedback is welcome. 51 | Modifying the build process to support multiple boards would 52 | be an awesome contribution. 53 | 54 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::fs::File; 3 | use std::io::Write; 4 | use std::path::PathBuf; 5 | 6 | fn main() { 7 | // Put the linker script somewhere the linker can find it 8 | let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); 9 | File::create(out.join("memory.x")) 10 | .unwrap() 11 | .write_all(include_bytes!("memory.x")) 12 | .unwrap(); 13 | println!("cargo:rustc-link-search={}", out.display()); 14 | 15 | // Only re-run the build script when memory.x is changed, 16 | // instead of when any part of the source code changes. 17 | println!("cargo:rerun-if-changed=memory.x"); 18 | } 19 | -------------------------------------------------------------------------------- /memory.x: -------------------------------------------------------------------------------- 1 | MEMORY 2 | { 3 | /* NOTE 1 K = 1 KiBi = 1024 bytes */ 4 | /* TODO Adjust these memory regions to match your device memory layout */ 5 | /* These values correspond to the LM3S6965, one of the few devices QEMU can emulate */ 6 | FLASH : ORIGIN = 0x08000000, LENGTH = 64K 7 | RAM : ORIGIN = 0x20000000, LENGTH = 20K 8 | } 9 | 10 | /* This is where the call stack will be allocated. */ 11 | /* The stack is of the full descending type. */ 12 | /* You may want to use this variable to locate the call stack and static 13 | variables in different memory regions. Below is shown the default value */ 14 | /* _stack_start = ORIGIN(RAM) + LENGTH(RAM); */ 15 | 16 | /* You can use this symbol to customize the location of the .text section */ 17 | /* If omitted the .text section will be placed right after the .vector_table 18 | section */ 19 | /* This is required only on microcontrollers that store some configuration right 20 | after the vector table */ 21 | /* _stext = ORIGIN(FLASH) + 0x400; */ 22 | 23 | /* Example of putting non-initialized variables into custom RAM locations. */ 24 | /* This assumes you have defined a region RAM2 above, and in the Rust 25 | sources added the attribute `#[link_section = ".ram2bss"]` to the data 26 | you want to place there. */ 27 | /* Note that the section will not be zero-initialized by the runtime! */ 28 | /* SECTIONS { 29 | .ram2bss (NOLOAD) : ALIGN(4) { 30 | *(.ram2bss); 31 | . = ALIGN(4); 32 | } > RAM2 33 | } INSERT AFTER .bss; 34 | */ 35 | -------------------------------------------------------------------------------- /openocd.cfg: -------------------------------------------------------------------------------- 1 | # Sample OpenOCD configuration for the STM32F3DISCOVERY development board 2 | 3 | # Depending on the hardware revision you got you'll have to pick ONE of these 4 | # interfaces. At any time only one interface should be commented out. 5 | 6 | # Revision C (newer revision) 7 | #source [find interface/stlink-v2-1.cfg] 8 | 9 | # Revision A and B (older revisions) 10 | source [find interface/stlink-v2.cfg] 11 | 12 | source [find target/stm32f1x.cfg] 13 | -------------------------------------------------------------------------------- /openocd.gdb: -------------------------------------------------------------------------------- 1 | target extended-remote :3333 2 | 3 | # print demangled symbols 4 | set print asm-demangle on 5 | 6 | # set backtrace limit to not have infinite backtrace loops 7 | set backtrace limit 32 8 | 9 | # detect unhandled exceptions, hard faults and panics 10 | break DefaultHandler 11 | break HardFault 12 | break rust_begin_unwind 13 | # # run the next few lines so the panic message is printed immediately 14 | # # the number needs to be adjusted for your panic handler 15 | # commands $bpnum 16 | # next 4 17 | # end 18 | 19 | # *try* to stop at the user entry point (it might be gone due to inlining) 20 | break main 21 | 22 | monitor arm semihosting enable 23 | 24 | # # send captured ITM to the file itm.fifo 25 | # # (the microcontroller SWO pin must be connected to the programmer SWO pin) 26 | # # 8000000 must match the core clock frequency 27 | # monitor tpiu config internal itm.txt uart off 8000000 28 | 29 | # # OR: make the microcontroller SWO pin output compatible with UART (8N1) 30 | # # 8000000 must match the core clock frequency 31 | # # 2000000 is the frequency of the SWO pin 32 | # monitor tpiu config external uart off 8000000 2000000 33 | 34 | # # enable ITM port 0 35 | # monitor itm port 0 on 36 | 37 | load 38 | 39 | # start the process but immediately halt the processor 40 | stepi 41 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | mod state; 5 | mod stm32f1xx; 6 | mod switch; 7 | mod usb; 8 | 9 | extern crate panic_semihosting; 10 | 11 | use crate::state::midi_events; 12 | use crate::state::{ApplicationState, Button, Message}; 13 | use crate::stm32f1xx::read_input_pins; 14 | use crate::stm32f1xx::{initialize_usb, Inputs}; 15 | use crate::usb::{configure_usb, usb_poll}; 16 | use stm32f1xx_hal::{ 17 | gpio::gpioc::PC13, 18 | gpio::{Output, PushPull}, 19 | pac::TIM1, 20 | prelude::*, 21 | timer::{CountDownTimer, Event, Timer}, 22 | usb::{UsbBus, UsbBusType}, 23 | }; 24 | use usb_device::{ 25 | bus, 26 | prelude::{UsbDevice, UsbDeviceState}, 27 | }; 28 | use usbd_midi::{ 29 | data::usb_midi::usb_midi_event_packet::UsbMidiEventPacket, 30 | midi_device::MidiClass, 31 | }; 32 | 33 | #[rtic::app(device = stm32f1xx_hal::stm32, 34 | peripherals = true)] 35 | const APP: () = { 36 | struct Resources { 37 | midi: MidiClass<'static, UsbBusType>, 38 | usb_dev: UsbDevice<'static, UsbBusType>, 39 | inputs: Inputs, 40 | led: PC13>, 41 | timer: CountDownTimer, 42 | state: ApplicationState, 43 | } 44 | 45 | #[init()] 46 | fn init(cx: init::Context) -> init::LateResources { 47 | // This is a bit hacky, but gets us the static lifetime for the 48 | // allocator. Even when based on hardware initialization.. 49 | static mut USB_BUS: Option> = None; 50 | 51 | // Take ownership of IO devices 52 | let mut rcc = cx.device.RCC.constrain(); 53 | let mut flash = cx.device.FLASH.constrain(); 54 | let mut gpioa = cx.device.GPIOA.split(&mut rcc.apb2); 55 | let mut gpiob = cx.device.GPIOB.split(&mut rcc.apb2); 56 | let mut gpioc = cx.device.GPIOC.split(&mut rcc.apb2); 57 | let pa12 = gpioa.pa12; 58 | let pa11 = gpioa.pa11; 59 | let pb11 = gpiob.pb11.into_pull_up_input(&mut gpiob.crh); 60 | let pb12 = gpiob.pb12.into_pull_up_input(&mut gpiob.crh); 61 | let pb13 = gpiob.pb13.into_pull_up_input(&mut gpiob.crh); 62 | let pb14 = gpiob.pb14.into_pull_up_input(&mut gpiob.crh); 63 | let pb15 = gpiob.pb15.into_pull_up_input(&mut gpiob.crh); 64 | let led = gpioc.pc13.into_push_pull_output(&mut gpioc.crh); 65 | let usb = cx.device.USB; 66 | 67 | // Configure clocks 68 | let clocks = rcc 69 | .cfgr 70 | .use_hse(8.mhz()) 71 | .sysclk(48.mhz()) 72 | .pclk1(24.mhz()) 73 | .freeze(&mut flash.acr); 74 | 75 | assert!(clocks.usbclk_valid()); 76 | 77 | //Timer that will be used to read IO 78 | let mut timer = Timer::tim1(cx.device.TIM1, &clocks, &mut rcc.apb2) 79 | .start_count_down(100.hz()); 80 | timer.listen(Event::Update); 81 | 82 | // Initialize usb resources 83 | // This is a bit tricky due to lifetimes in RTFM/USB playing 84 | // difficultly 85 | let usb = initialize_usb(&clocks, pa12, pa11, &mut gpioa.crh, usb); 86 | *USB_BUS = Some(UsbBus::new(usb)); 87 | let midi = MidiClass::new(USB_BUS.as_ref().unwrap()); 88 | let usb_dev = configure_usb(USB_BUS.as_ref().unwrap()); 89 | 90 | let inputs = Inputs { 91 | pb11: pb11, 92 | pb12: pb12, 93 | pb13: pb13, 94 | pb14: pb14, 95 | pb15: pb15, 96 | }; 97 | 98 | // Resources for RTFM 99 | init::LateResources { 100 | usb_dev: usb_dev, 101 | midi: midi, 102 | inputs: inputs, 103 | led: led, 104 | state: ApplicationState::init(), 105 | timer: timer, 106 | } 107 | } 108 | 109 | /// Will be called periodically. 110 | #[task(binds = TIM1_UP, 111 | spawn = [update], 112 | resources = [inputs,timer], 113 | priority = 1)] 114 | fn read_inputs(cx: read_inputs::Context) { 115 | // There must be a better way to bank over 116 | // these below checks 117 | 118 | let values = read_input_pins(cx.resources.inputs); 119 | 120 | let _ = cx.spawn.update((Button::One, values.pin1)); 121 | let _ = cx.spawn.update((Button::Two, values.pin2)); 122 | let _ = cx.spawn.update((Button::Three, values.pin3)); 123 | let _ = cx.spawn.update((Button::Four, values.pin4)); 124 | let _ = cx.spawn.update((Button::Five, values.pin5)); 125 | 126 | cx.resources.timer.clear_update_interrupt_flag(); 127 | } 128 | 129 | #[task( spawn = [send_midi], 130 | resources = [state], 131 | priority = 1, 132 | capacity = 5)] 133 | fn update(cx: update::Context, message: Message) { 134 | let old = cx.resources.state.clone(); 135 | ApplicationState::update(&mut *cx.resources.state, message); 136 | let mut effects = midi_events(&old, cx.resources.state); 137 | let effect = effects.next(); 138 | 139 | match effect { 140 | Some(midi) => { 141 | let _ = cx.spawn.send_midi(midi); 142 | } 143 | _ => (), 144 | } 145 | } 146 | 147 | /// Sends a midi message over the usb bus 148 | /// Note: this runs at a lower priority than the usb bus 149 | /// and will eat messages if the bus is not configured yet 150 | #[task(priority=2, resources = [usb_dev,midi])] 151 | fn send_midi(cx: send_midi::Context, message: UsbMidiEventPacket) { 152 | let mut midi = cx.resources.midi; 153 | let mut usb_dev = cx.resources.usb_dev; 154 | 155 | // Lock this so USB interrupts don't take over 156 | // Ideally we may be able to better determine this, so that 157 | // it doesn't need to be locked 158 | usb_dev.lock(|usb_dev| { 159 | if usb_dev.state() == UsbDeviceState::Configured { 160 | midi.lock(|midi| { 161 | let _ = midi.send_message(message); 162 | }) 163 | } 164 | }); 165 | } 166 | 167 | // Process usb events straight away from High priority interrupts 168 | #[task(binds = USB_HP_CAN_TX,resources = [usb_dev, midi], priority=3)] 169 | fn usb_hp_can_tx(mut cx: usb_hp_can_tx::Context) { 170 | usb_poll(&mut cx.resources.usb_dev, &mut cx.resources.midi); 171 | } 172 | 173 | // Process usb events straight away from Low priority interrupts 174 | #[task(binds= USB_LP_CAN_RX0, resources = [usb_dev, midi], priority=3)] 175 | fn usb_lp_can_rx0(mut cx: usb_lp_can_rx0::Context) { 176 | usb_poll(&mut cx.resources.usb_dev, &mut cx.resources.midi); 177 | } 178 | 179 | // Required for software tasks 180 | extern "C" { 181 | // Uses the DMA1_CHANNELX interrupts for software 182 | // task scheduling. 183 | fn DMA1_CHANNEL1(); 184 | fn DMA1_CHANNEL2(); 185 | } 186 | }; 187 | -------------------------------------------------------------------------------- /src/state.rs: -------------------------------------------------------------------------------- 1 | use heapless::consts::U5; 2 | use heapless::LinearMap; 3 | use usbd_midi::data::byte::u7::U7; 4 | use usbd_midi::data::midi::channel::Channel; 5 | use usbd_midi::data::midi::message::Message as MidiMessage; 6 | use usbd_midi::data::midi::notes::Note; 7 | use usbd_midi::data::usb_midi::cable_number::CableNumber; 8 | use usbd_midi::data::usb_midi::usb_midi_event_packet::UsbMidiEventPacket; 9 | 10 | /// The buttons the user can press 11 | #[derive(Clone, Copy, PartialEq, Eq)] 12 | pub enum Button { 13 | One, 14 | Two, 15 | Three, 16 | Four, 17 | Five, 18 | } 19 | 20 | type ButtonMap = LinearMap; 21 | 22 | /// States the buttons emit 23 | #[derive(Clone, Copy, PartialEq, Eq)] 24 | pub enum State { 25 | On, 26 | Off, 27 | } 28 | 29 | impl From for State { 30 | fn from(value: bool) -> State { 31 | match value { 32 | true => State::On, 33 | false => State::Off, 34 | } 35 | } 36 | } 37 | 38 | impl From