Init. "BLE+SPP MIDI controller with cs-midi, encoders, and dual-transport routing"
This commit is contained in:
parent
6e78d816b5
commit
1ae9b58eba
|
|
@ -1,3 +1,6 @@
|
||||||
[submodule "lib/Control-Surface"]
|
[submodule "lib/Control-Surface"]
|
||||||
path = lib/Control-Surface
|
path = lib/Control-Surface
|
||||||
url = https://github.com/tttapa/Control-Surface.git
|
url = https://github.com/tttapa/Control-Surface.git
|
||||||
|
[submodule "lib/cs-midi"]
|
||||||
|
path = lib/cs-midi
|
||||||
|
url = https://git.else-if.org/jess/cs-midi.git
|
||||||
|
|
|
||||||
|
|
@ -15,18 +15,30 @@ add_compile_definitions(PICO_RP2350A=0)
|
||||||
|
|
||||||
pico_sdk_init()
|
pico_sdk_init()
|
||||||
|
|
||||||
|
add_subdirectory(lib/cs-midi)
|
||||||
|
|
||||||
|
# cs_midi's BTstack sources need project-level btstack_config.h and lwipopts.h
|
||||||
|
target_include_directories(cs_midi PRIVATE ${CMAKE_CURRENT_LIST_DIR})
|
||||||
|
|
||||||
add_executable(fractional_looper
|
add_executable(fractional_looper
|
||||||
main.c
|
main.cpp
|
||||||
|
src/encoder.cpp
|
||||||
|
src/spp_midi.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(fractional_looper PRIVATE ${CMAKE_CURRENT_LIST_DIR})
|
target_include_directories(fractional_looper PRIVATE
|
||||||
|
${CMAKE_CURRENT_LIST_DIR}
|
||||||
|
${CMAKE_CURRENT_LIST_DIR}/src
|
||||||
|
)
|
||||||
|
|
||||||
target_link_libraries(fractional_looper
|
target_link_libraries(fractional_looper
|
||||||
pico_stdlib
|
pico_stdlib
|
||||||
pico_cyw43_arch_lwip_threadsafe_background
|
pico_cyw43_arch_lwip_threadsafe_background
|
||||||
pico_btstack_ble
|
pico_btstack_ble
|
||||||
|
pico_btstack_classic
|
||||||
pico_btstack_cyw43
|
pico_btstack_cyw43
|
||||||
hardware_adc
|
hardware_adc
|
||||||
|
cs_midi
|
||||||
)
|
)
|
||||||
|
|
||||||
pico_enable_stdio_usb(fractional_looper 1)
|
pico_enable_stdio_usb(fractional_looper 1)
|
||||||
|
|
|
||||||
11
Makefile
11
Makefile
|
|
@ -9,7 +9,16 @@ $(BUILD_DIR)/Makefile: CMakeLists.txt
|
||||||
@cd $(BUILD_DIR) && PICO_SDK_PATH=$$HOME/Staging/pico-sdk cmake ..
|
@cd $(BUILD_DIR) && PICO_SDK_PATH=$$HOME/Staging/pico-sdk cmake ..
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
|
@rm -f $(BUILD_DIR)/CMakeFiles/$(TARGET).dir/main.cpp.o
|
||||||
|
@rm -f $(BUILD_DIR)/CMakeFiles/$(TARGET).dir/src/*.o
|
||||||
|
@rm -f $(BUILD_DIR)/$(TARGET).elf $(BUILD_DIR)/$(TARGET).uf2
|
||||||
|
@rm -f $(BUILD_DIR)/$(TARGET).hex $(BUILD_DIR)/$(TARGET).bin
|
||||||
|
@rm -f $(BUILD_DIR)/$(TARGET).dis $(BUILD_DIR)/$(TARGET).map
|
||||||
|
@echo "Project objects cleaned (libraries + SDK preserved)"
|
||||||
|
|
||||||
|
distclean:
|
||||||
@rm -rf $(BUILD_DIR)
|
@rm -rf $(BUILD_DIR)
|
||||||
|
@echo "Full clean (rebuild everything with make)"
|
||||||
|
|
||||||
flash: all
|
flash: all
|
||||||
@echo "Waiting for RP2350 volume..."
|
@echo "Waiting for RP2350 volume..."
|
||||||
|
|
@ -18,4 +27,4 @@ flash: all
|
||||||
@cp $(BUILD_DIR)/$(TARGET).uf2 /Volumes/RP2350/
|
@cp $(BUILD_DIR)/$(TARGET).uf2 /Volumes/RP2350/
|
||||||
@echo "Done."
|
@echo "Done."
|
||||||
|
|
||||||
.PHONY: all clean flash
|
.PHONY: all clean distclean flash
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,16 @@
|
||||||
#ifndef _BTSTACK_CONFIG_H
|
#ifndef _BTSTACK_CONFIG_H
|
||||||
#define _BTSTACK_CONFIG_H
|
#define _BTSTACK_CONFIG_H
|
||||||
|
|
||||||
|
// BLE
|
||||||
#define ENABLE_LE_PERIPHERAL
|
#define ENABLE_LE_PERIPHERAL
|
||||||
#define ENABLE_LE_CENTRAL
|
#define ENABLE_LE_CENTRAL
|
||||||
#define ENABLE_L2CAP_LE_CREDIT_BASED_FLOW_CONTROL_MODE
|
#define ENABLE_L2CAP_LE_CREDIT_BASED_FLOW_CONTROL_MODE
|
||||||
|
|
||||||
|
// Classic BT
|
||||||
|
#ifndef ENABLE_CLASSIC
|
||||||
|
#define ENABLE_CLASSIC
|
||||||
|
#endif
|
||||||
|
#define ENABLE_LOG_INFO
|
||||||
#define ENABLE_PRINTF_HEXDUMP
|
#define ENABLE_PRINTF_HEXDUMP
|
||||||
|
|
||||||
#define HCI_OUTGOING_PRE_BUFFER_SIZE 4
|
#define HCI_OUTGOING_PRE_BUFFER_SIZE 4
|
||||||
|
|
@ -12,9 +19,9 @@
|
||||||
|
|
||||||
#define MAX_NR_BTSTACK_LINK_KEY_DB_MEMORY_ENTRIES 2
|
#define MAX_NR_BTSTACK_LINK_KEY_DB_MEMORY_ENTRIES 2
|
||||||
#define MAX_NR_GATT_CLIENTS 1
|
#define MAX_NR_GATT_CLIENTS 1
|
||||||
#define MAX_NR_HCI_CONNECTIONS 2
|
#define MAX_NR_HCI_CONNECTIONS 4
|
||||||
#define MAX_NR_L2CAP_CHANNELS 4
|
#define MAX_NR_L2CAP_CHANNELS 6
|
||||||
#define MAX_NR_L2CAP_SERVICES 3
|
#define MAX_NR_L2CAP_SERVICES 4
|
||||||
#define MAX_NR_SM_LOOKUP_ENTRIES 3
|
#define MAX_NR_SM_LOOKUP_ENTRIES 3
|
||||||
#define MAX_NR_WHITELIST_ENTRIES 1
|
#define MAX_NR_WHITELIST_ENTRIES 1
|
||||||
#define MAX_NR_LE_DEVICE_DB_ENTRIES 4
|
#define MAX_NR_LE_DEVICE_DB_ENTRIES 4
|
||||||
|
|
@ -28,6 +35,14 @@
|
||||||
#define HCI_HOST_SCO_PACKET_LEN 120
|
#define HCI_HOST_SCO_PACKET_LEN 120
|
||||||
#define HCI_HOST_SCO_PACKET_NUM 3
|
#define HCI_HOST_SCO_PACKET_NUM 3
|
||||||
|
|
||||||
|
// RFCOMM for SPP
|
||||||
|
#define MAX_NR_RFCOMM_MULTIPLEXERS 1
|
||||||
|
#define MAX_NR_RFCOMM_SERVICES 1
|
||||||
|
#define MAX_NR_RFCOMM_CHANNELS 1
|
||||||
|
|
||||||
|
// SDP
|
||||||
|
#define MAX_NR_SERVICE_RECORD_ITEMS 2
|
||||||
|
|
||||||
#define NVM_NUM_DEVICE_DB_ENTRIES 16
|
#define NVM_NUM_DEVICE_DB_ENTRIES 16
|
||||||
#define NVM_NUM_LINK_KEYS 16
|
#define NVM_NUM_LINK_KEYS 16
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit e215f7686c92a5588907164ac2c7e61258abc039
|
||||||
23
main.c
23
main.c
|
|
@ -1,23 +0,0 @@
|
||||||
#include <stdio.h>
|
|
||||||
#include "pico/stdlib.h"
|
|
||||||
#include "pico/cyw43_arch.h"
|
|
||||||
|
|
||||||
int main(void) {
|
|
||||||
stdio_init_all();
|
|
||||||
|
|
||||||
if (cyw43_arch_init()) {
|
|
||||||
printf("CYW43 init failed\n");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
printf("FractionalLooper: CYW43 initialized\n");
|
|
||||||
|
|
||||||
while (1) {
|
|
||||||
cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 1);
|
|
||||||
sleep_ms(500);
|
|
||||||
cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 0);
|
|
||||||
sleep_ms(500);
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
#include <cstdio>
|
||||||
|
#include "pico/stdlib.h"
|
||||||
|
#include "pico/cyw43_arch.h"
|
||||||
|
|
||||||
|
#include <cs_midi.h>
|
||||||
|
#include "encoder.h"
|
||||||
|
#include "spp_midi.h"
|
||||||
|
|
||||||
|
// Two CC sets, toggled by SW1 button.
|
||||||
|
// Set A: encoders CC 16-19, buttons CC 20-22 (SW2-SW4)
|
||||||
|
// Set B: encoders CC 24-27, buttons CC 28-30 (SW2-SW4)
|
||||||
|
static constexpr uint8_t SET_A_ENC_CC = 16;
|
||||||
|
static constexpr uint8_t SET_A_BTN_CC = 20;
|
||||||
|
static constexpr uint8_t SET_B_ENC_CC = 24;
|
||||||
|
static constexpr uint8_t SET_B_BTN_CC = 28;
|
||||||
|
static constexpr uint8_t TOGGLE_CC = 23;
|
||||||
|
|
||||||
|
static uint8_t enc_cc_base = SET_A_ENC_CC;
|
||||||
|
static uint8_t btn_cc_base = SET_A_BTN_CC;
|
||||||
|
static bool set_b_active = false;
|
||||||
|
|
||||||
|
// LED state
|
||||||
|
enum LedState { LED_IDLE_BLINK, LED_CONNECT_FLASH, LED_OFF };
|
||||||
|
static LedState led_state = LED_IDLE_BLINK;
|
||||||
|
static uint16_t led_counter = 0;
|
||||||
|
static uint8_t led_flash_count = 0;
|
||||||
|
static bool was_connected = false;
|
||||||
|
|
||||||
|
static void led_set(bool on) {
|
||||||
|
cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, on);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
stdio_init_all();
|
||||||
|
|
||||||
|
if (cyw43_arch_init()) {
|
||||||
|
printf("CYW43 init failed\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("FractionalLooper: CYW43 initialized\n");
|
||||||
|
|
||||||
|
cs::BluetoothMIDI_Interface ble;
|
||||||
|
ble.begin();
|
||||||
|
printf("BLE MIDI started\n");
|
||||||
|
|
||||||
|
SPPStreamMIDI_Interface spp;
|
||||||
|
spp.begin();
|
||||||
|
|
||||||
|
cs::BidirectionalMIDI_PipeFactory<2> pipes;
|
||||||
|
ble | pipes | spp;
|
||||||
|
|
||||||
|
encoders_init();
|
||||||
|
printf("Encoders initialized\n");
|
||||||
|
|
||||||
|
ble.setAsDefault();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
bool connected = ble.isConnected() || spp.isConnected();
|
||||||
|
if (connected && !was_connected) {
|
||||||
|
led_state = LED_CONNECT_FLASH;
|
||||||
|
led_flash_count = 0;
|
||||||
|
led_counter = 0;
|
||||||
|
} else if (!connected && was_connected) {
|
||||||
|
led_state = LED_IDLE_BLINK;
|
||||||
|
led_counter = 0;
|
||||||
|
}
|
||||||
|
was_connected = connected;
|
||||||
|
|
||||||
|
switch (led_state) {
|
||||||
|
case LED_IDLE_BLINK:
|
||||||
|
led_set(led_counter < 500);
|
||||||
|
if (++led_counter >= 1000) led_counter = 0;
|
||||||
|
break;
|
||||||
|
case LED_CONNECT_FLASH:
|
||||||
|
led_set(led_counter < 50);
|
||||||
|
if (++led_counter >= 100) {
|
||||||
|
led_counter = 0;
|
||||||
|
if (++led_flash_count >= 6) {
|
||||||
|
led_state = LED_OFF;
|
||||||
|
led_set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case LED_OFF:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < NUM_ENCODERS; i++) {
|
||||||
|
int32_t delta = encoder_get_delta(i);
|
||||||
|
if (delta != 0) {
|
||||||
|
int32_t clamped = delta;
|
||||||
|
if (clamped > 63) clamped = 63;
|
||||||
|
if (clamped < -63) clamped = -63;
|
||||||
|
uint8_t val = (clamped > 0) ? (uint8_t)clamped
|
||||||
|
: (uint8_t)(128 + clamped);
|
||||||
|
ble.sendControlChange({enc_cc_base + i, cs::Channel_1}, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (button_pressed(i)) {
|
||||||
|
if (i == 0) {
|
||||||
|
set_b_active = !set_b_active;
|
||||||
|
enc_cc_base = set_b_active ? SET_B_ENC_CC : SET_A_ENC_CC;
|
||||||
|
btn_cc_base = set_b_active ? SET_B_BTN_CC : SET_A_BTN_CC;
|
||||||
|
ble.sendControlChange({TOGGLE_CC, cs::Channel_1},
|
||||||
|
set_b_active ? 127 : 0);
|
||||||
|
} else {
|
||||||
|
ble.sendControlChange({btn_cc_base + (i - 1), cs::Channel_1}, 127);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ble.update();
|
||||||
|
spp.update();
|
||||||
|
|
||||||
|
sleep_ms(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
#include "encoder.h"
|
||||||
|
|
||||||
|
#include "hardware/gpio.h"
|
||||||
|
#include "hardware/structs/sio.h"
|
||||||
|
#include "hardware/timer.h"
|
||||||
|
#include "pico/stdlib.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
|
// Interrupt-driven quadrature decoder using the same state machine
|
||||||
|
// as Control Surface's AHEncoder. Fires on every edge of both A and B
|
||||||
|
// pins, reads both pins directly from SIO register, and updates
|
||||||
|
// position atomically.
|
||||||
|
//
|
||||||
|
// new_B new_A old_B old_A Result
|
||||||
|
// 0 0 0 1 +1
|
||||||
|
// 0 0 1 0 -1
|
||||||
|
// 0 0 1 1 +2
|
||||||
|
// 0 1 0 0 -1
|
||||||
|
// 0 1 1 0 -2
|
||||||
|
// 0 1 1 1 +1
|
||||||
|
// 1 0 0 0 +1
|
||||||
|
// 1 0 0 1 -2
|
||||||
|
// 1 0 1 1 -1
|
||||||
|
// 1 1 0 0 +2
|
||||||
|
// 1 1 0 1 -1
|
||||||
|
// 1 1 1 0 +1
|
||||||
|
|
||||||
|
static std::atomic<int32_t> enc_position[NUM_ENCODERS];
|
||||||
|
static uint8_t enc_state[NUM_ENCODERS];
|
||||||
|
|
||||||
|
static inline void encoder_update(int i) {
|
||||||
|
uint32_t gpio_all = sio_hw->gpio_in;
|
||||||
|
uint8_t s = enc_state[i] & 0b11;
|
||||||
|
if (gpio_all & (1u << ENC_PIN_A[i])) s |= 4;
|
||||||
|
if (gpio_all & (1u << ENC_PIN_B[i])) s |= 8;
|
||||||
|
enc_state[i] = (s >> 2);
|
||||||
|
switch (s) {
|
||||||
|
case 1: case 7: case 8: case 14:
|
||||||
|
enc_position[i].fetch_add(1, std::memory_order_relaxed); return;
|
||||||
|
case 2: case 4: case 11: case 13:
|
||||||
|
enc_position[i].fetch_add(-1, std::memory_order_relaxed); return;
|
||||||
|
case 3: case 12:
|
||||||
|
enc_position[i].fetch_add(2, std::memory_order_relaxed); return;
|
||||||
|
case 6: case 9:
|
||||||
|
enc_position[i].fetch_add(-2, std::memory_order_relaxed); return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Button state
|
||||||
|
static volatile bool btn_flags[NUM_ENCODERS];
|
||||||
|
static volatile uint32_t btn_debounce_time[NUM_ENCODERS];
|
||||||
|
#define DEBOUNCE_MS 25
|
||||||
|
|
||||||
|
// Single callback for all encoder edges and button presses
|
||||||
|
static void gpio_irq_callback(uint gpio, uint32_t events) {
|
||||||
|
for (int i = 0; i < NUM_ENCODERS; i++) {
|
||||||
|
if (gpio == ENC_PIN_A[i] || gpio == ENC_PIN_B[i]) {
|
||||||
|
encoder_update(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (events & GPIO_IRQ_EDGE_FALL) {
|
||||||
|
uint32_t now = to_ms_since_boot(get_absolute_time());
|
||||||
|
for (int i = 0; i < NUM_ENCODERS; i++) {
|
||||||
|
if (gpio == BTN_PINS[i]) {
|
||||||
|
if (now - btn_debounce_time[i] > DEBOUNCE_MS) {
|
||||||
|
btn_debounce_time[i] = now;
|
||||||
|
btn_flags[i] = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void encoders_init() {
|
||||||
|
for (int i = 0; i < NUM_ENCODERS; i++) {
|
||||||
|
enc_position[i].store(0, std::memory_order_relaxed);
|
||||||
|
enc_state[i] = 0;
|
||||||
|
btn_flags[i] = false;
|
||||||
|
btn_debounce_time[i] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up encoder pins with pull-ups and edge interrupts
|
||||||
|
for (int i = 0; i < NUM_ENCODERS; i++) {
|
||||||
|
gpio_init(ENC_PIN_A[i]);
|
||||||
|
gpio_set_dir(ENC_PIN_A[i], GPIO_IN);
|
||||||
|
gpio_pull_up(ENC_PIN_A[i]);
|
||||||
|
|
||||||
|
gpio_init(ENC_PIN_B[i]);
|
||||||
|
gpio_set_dir(ENC_PIN_B[i], GPIO_IN);
|
||||||
|
gpio_pull_up(ENC_PIN_B[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up button pins
|
||||||
|
for (int i = 0; i < NUM_ENCODERS; i++) {
|
||||||
|
gpio_init(BTN_PINS[i]);
|
||||||
|
gpio_set_dir(BTN_PINS[i], GPIO_IN);
|
||||||
|
gpio_pull_up(BTN_PINS[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow RC filters to settle
|
||||||
|
sleep_ms(2);
|
||||||
|
|
||||||
|
// Read initial encoder states
|
||||||
|
uint32_t gpio_all = sio_hw->gpio_in;
|
||||||
|
for (int i = 0; i < NUM_ENCODERS; i++) {
|
||||||
|
uint8_t s = 0;
|
||||||
|
if (gpio_all & (1u << ENC_PIN_A[i])) s |= 1;
|
||||||
|
if (gpio_all & (1u << ENC_PIN_B[i])) s |= 2;
|
||||||
|
enc_state[i] = s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register single callback, enable IRQs on all encoder and button pins
|
||||||
|
gpio_set_irq_enabled_with_callback(ENC_PIN_A[0],
|
||||||
|
GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL, true, gpio_irq_callback);
|
||||||
|
|
||||||
|
for (int i = 0; i < NUM_ENCODERS; i++) {
|
||||||
|
if (i > 0)
|
||||||
|
gpio_set_irq_enabled(ENC_PIN_A[i],
|
||||||
|
GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL, true);
|
||||||
|
gpio_set_irq_enabled(ENC_PIN_B[i],
|
||||||
|
GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL, true);
|
||||||
|
gpio_set_irq_enabled(BTN_PINS[i],
|
||||||
|
GPIO_IRQ_EDGE_FALL, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t encoder_get_delta(int index) {
|
||||||
|
return enc_position[index].exchange(0, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool button_pressed(int index) {
|
||||||
|
if (btn_flags[index]) {
|
||||||
|
btn_flags[index] = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#define NUM_ENCODERS 4
|
||||||
|
|
||||||
|
// A/B swapped on encoders 0-2 to correct rotation direction.
|
||||||
|
// Encoder 0 rewired to GPIO 17/18. Encoder 3 left as-is (already correct).
|
||||||
|
static constexpr uint8_t ENC_PIN_A[NUM_ENCODERS] = {17, 5, 8, 9};
|
||||||
|
static constexpr uint8_t ENC_PIN_B[NUM_ENCODERS] = {18, 3, 6, 11};
|
||||||
|
static constexpr uint8_t BTN_PINS[NUM_ENCODERS] = {1, 4, 7, 10};
|
||||||
|
|
||||||
|
void encoders_init();
|
||||||
|
|
||||||
|
// Returns accumulated delta since last call. Positive = clockwise.
|
||||||
|
int32_t encoder_get_delta(int index);
|
||||||
|
|
||||||
|
// Returns true if button was pressed since last call (edge-triggered).
|
||||||
|
bool button_pressed(int index);
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
.pio_version 0
|
||||||
|
|
||||||
|
; Quadrature encoder decoder for non-adjacent A/B pins.
|
||||||
|
;
|
||||||
|
; Pin layout per encoder: A = in_base+0, (switch = in_base+1), B = in_base+2.
|
||||||
|
; On each B transition, samples A and B into the ISR and pushes to RX FIFO.
|
||||||
|
; Software decodes direction from the state table.
|
||||||
|
;
|
||||||
|
; Set clock divider for ~10 kHz to filter mechanical bounce.
|
||||||
|
|
||||||
|
.program encoder
|
||||||
|
.wrap_target
|
||||||
|
wait 1 pin 2 ; wait for B (base+2) to go high
|
||||||
|
in pins, 3 ; sample A, switch, B (3 bits from base)
|
||||||
|
push noblock
|
||||||
|
wait 0 pin 2 ; wait for B to go low
|
||||||
|
in pins, 3 ; sample again
|
||||||
|
push noblock
|
||||||
|
.wrap
|
||||||
|
|
||||||
|
% c-sdk {
|
||||||
|
#include "hardware/clocks.h"
|
||||||
|
#include "hardware/gpio.h"
|
||||||
|
|
||||||
|
static inline void encoder_program_init(PIO pio, uint sm, uint offset,
|
||||||
|
uint pin_a) {
|
||||||
|
// Pin layout: A = pin_a, switch = pin_a+1, B = pin_a+2
|
||||||
|
uint pin_sw = pin_a + 1;
|
||||||
|
uint pin_b = pin_a + 2;
|
||||||
|
|
||||||
|
pio_gpio_init(pio, pin_a);
|
||||||
|
pio_gpio_init(pio, pin_b);
|
||||||
|
gpio_pull_up(pin_a);
|
||||||
|
gpio_pull_up(pin_b);
|
||||||
|
pio_sm_set_consecutive_pindirs(pio, sm, pin_a, 3, false);
|
||||||
|
|
||||||
|
pio_sm_config c = encoder_program_get_default_config(offset);
|
||||||
|
sm_config_set_in_pins(&c, pin_a);
|
||||||
|
sm_config_set_in_shift(&c, false, false, 32);
|
||||||
|
|
||||||
|
float div = (float)clock_get_hz(clk_sys) / 10000.0f;
|
||||||
|
sm_config_set_clkdiv(&c, div);
|
||||||
|
|
||||||
|
pio_sm_init(pio, sm, offset, &c);
|
||||||
|
pio_sm_set_enabled(pio, sm, true);
|
||||||
|
}
|
||||||
|
%}
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
#include "spp_midi.h"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#include "btstack.h"
|
||||||
|
|
||||||
|
#define RFCOMM_SERVER_CHANNEL 1
|
||||||
|
|
||||||
|
static SPPStreamMIDI_Interface *spp_instance = nullptr;
|
||||||
|
static uint16_t rfcomm_cid = 0;
|
||||||
|
static uint16_t rfcomm_mtu = 0;
|
||||||
|
static uint8_t spp_service_buffer[150];
|
||||||
|
static btstack_packet_callback_registration_t hci_event_cb_reg;
|
||||||
|
|
||||||
|
// Ring buffer storage
|
||||||
|
volatile uint8_t SPPStreamMIDI_Interface::rx_buf[RX_BUF_SIZE];
|
||||||
|
volatile uint16_t SPPStreamMIDI_Interface::rx_head = 0;
|
||||||
|
volatile uint16_t SPPStreamMIDI_Interface::rx_tail = 0;
|
||||||
|
|
||||||
|
SPPStreamMIDI_Interface *SPPStreamMIDI_Interface::instance() {
|
||||||
|
return spp_instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SPPStreamMIDI_Interface::push_rx_data(const uint8_t *data, uint16_t len) {
|
||||||
|
for (uint16_t i = 0; i < len; i++) {
|
||||||
|
uint16_t next = (rx_head + 1) % RX_BUF_SIZE;
|
||||||
|
if (next == rx_tail) break; // overflow — drop
|
||||||
|
rx_buf[rx_head] = data[i];
|
||||||
|
rx_head = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SPPStreamMIDI_Interface::RxPuller::pull(uint8_t &byte) {
|
||||||
|
if (rx_head == rx_tail) return false;
|
||||||
|
byte = rx_buf[rx_tail];
|
||||||
|
rx_tail = (rx_tail + 1) % RX_BUF_SIZE;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BTstack packet handler for RFCOMM events
|
||||||
|
void spp_packet_handler(uint8_t packet_type, uint16_t channel,
|
||||||
|
uint8_t *packet, uint16_t size) {
|
||||||
|
(void)channel;
|
||||||
|
bd_addr_t event_addr;
|
||||||
|
|
||||||
|
switch (packet_type) {
|
||||||
|
case HCI_EVENT_PACKET:
|
||||||
|
switch (hci_event_packet_get_type(packet)) {
|
||||||
|
case HCI_EVENT_PIN_CODE_REQUEST:
|
||||||
|
hci_event_pin_code_request_get_bd_addr(packet, event_addr);
|
||||||
|
gap_pin_code_response(event_addr, "0000");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HCI_EVENT_USER_CONFIRMATION_REQUEST:
|
||||||
|
hci_event_user_confirmation_request_get_bd_addr(packet, event_addr);
|
||||||
|
gap_ssp_confirmation_response(event_addr);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RFCOMM_EVENT_INCOMING_CONNECTION:
|
||||||
|
rfcomm_cid = rfcomm_event_incoming_connection_get_rfcomm_cid(packet);
|
||||||
|
rfcomm_accept_connection(rfcomm_cid);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RFCOMM_EVENT_CHANNEL_OPENED:
|
||||||
|
if (rfcomm_event_channel_opened_get_status(packet)) {
|
||||||
|
printf("SPP: channel open failed 0x%02x\n",
|
||||||
|
rfcomm_event_channel_opened_get_status(packet));
|
||||||
|
rfcomm_cid = 0;
|
||||||
|
} else {
|
||||||
|
rfcomm_cid = rfcomm_event_channel_opened_get_rfcomm_cid(packet);
|
||||||
|
rfcomm_mtu = rfcomm_event_channel_opened_get_max_frame_size(packet);
|
||||||
|
if (spp_instance) spp_instance->connected = true;
|
||||||
|
printf("SPP: connected, cid 0x%02x mtu %u\n", rfcomm_cid, rfcomm_mtu);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RFCOMM_EVENT_CHANNEL_CLOSED:
|
||||||
|
if (spp_instance) spp_instance->connected = false;
|
||||||
|
printf("SPP: disconnected\n");
|
||||||
|
rfcomm_cid = 0;
|
||||||
|
gap_discoverable_control(1);
|
||||||
|
gap_connectable_control(1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RFCOMM_DATA_PACKET:
|
||||||
|
SPPStreamMIDI_Interface::push_rx_data(packet, size);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SPPStreamMIDI_Interface::begin() {
|
||||||
|
spp_instance = this;
|
||||||
|
|
||||||
|
// Classic BT init
|
||||||
|
l2cap_init();
|
||||||
|
rfcomm_init();
|
||||||
|
rfcomm_register_service(spp_packet_handler, RFCOMM_SERVER_CHANNEL, 0xffff);
|
||||||
|
|
||||||
|
sdp_init();
|
||||||
|
memset(spp_service_buffer, 0, sizeof(spp_service_buffer));
|
||||||
|
spp_create_sdp_record(spp_service_buffer,
|
||||||
|
sdp_create_service_record_handle(),
|
||||||
|
RFCOMM_SERVER_CHANNEL,
|
||||||
|
"FractionalLooper MIDI");
|
||||||
|
sdp_register_service(spp_service_buffer);
|
||||||
|
|
||||||
|
hci_event_cb_reg.callback = &spp_packet_handler;
|
||||||
|
hci_add_event_handler(&hci_event_cb_reg);
|
||||||
|
|
||||||
|
gap_set_local_name("FractionalLooper 00:00:00:00:00:00");
|
||||||
|
gap_ssp_set_io_capability(SSP_IO_CAPABILITY_DISPLAY_YES_NO);
|
||||||
|
gap_discoverable_control(1);
|
||||||
|
gap_connectable_control(1);
|
||||||
|
|
||||||
|
printf("SPP MIDI: initialized\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
cs::MIDIReadEvent SPPStreamMIDI_Interface::read() {
|
||||||
|
RxPuller puller;
|
||||||
|
return parser.pull(puller);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SPPStreamMIDI_Interface::send_bytes(const uint8_t *data, size_t len) {
|
||||||
|
if (rfcomm_cid == 0 || len == 0) return;
|
||||||
|
rfcomm_send(rfcomm_cid, (uint8_t *)data, (uint16_t)len);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SPPStreamMIDI_Interface::sendChannelMessageImpl(cs::ChannelMessage msg) {
|
||||||
|
uint8_t buf[3] = {msg.header, msg.data1, msg.data2};
|
||||||
|
size_t len = msg.hasTwoDataBytes() ? 3 : 2;
|
||||||
|
send_bytes(buf, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SPPStreamMIDI_Interface::sendSysCommonImpl(cs::SysCommonMessage msg) {
|
||||||
|
uint8_t buf[3] = {msg.header, msg.data1, msg.data2};
|
||||||
|
size_t len = msg.getNumberOfDataBytes() + 1;
|
||||||
|
send_bytes(buf, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SPPStreamMIDI_Interface::sendSysExImpl(cs::SysExMessage msg) {
|
||||||
|
send_bytes(msg.data, msg.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SPPStreamMIDI_Interface::sendRealTimeImpl(cs::RealTimeMessage msg) {
|
||||||
|
uint8_t buf[1] = {msg.message};
|
||||||
|
send_bytes(buf, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SPPStreamMIDI_Interface::sendNowImpl() {
|
||||||
|
// RFCOMM sends immediately, nothing to flush
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cs_midi.h>
|
||||||
|
#include <MIDI_Parsers/SerialMIDI_Parser.hpp>
|
||||||
|
|
||||||
|
// Classic Bluetooth SPP MIDI interface.
|
||||||
|
// Carries raw MIDI bytes over RFCOMM serial port.
|
||||||
|
|
||||||
|
class SPPStreamMIDI_Interface : public cs::MIDI_Interface {
|
||||||
|
public:
|
||||||
|
SPPStreamMIDI_Interface() = default;
|
||||||
|
|
||||||
|
void begin() override;
|
||||||
|
void update() override { cs::MIDI_Interface::updateIncoming(this); }
|
||||||
|
void handleStall() override { cs::MIDI_Interface::handleStall(this); }
|
||||||
|
|
||||||
|
cs::MIDIReadEvent read();
|
||||||
|
cs::ChannelMessage getChannelMessage() const { return parser.getChannelMessage(); }
|
||||||
|
cs::SysCommonMessage getSysCommonMessage() const { return parser.getSysCommonMessage(); }
|
||||||
|
cs::RealTimeMessage getRealTimeMessage() const { return parser.getRealTimeMessage(); }
|
||||||
|
cs::SysExMessage getSysExMessage() const { return parser.getSysExMessage(); }
|
||||||
|
|
||||||
|
static void push_rx_data(const uint8_t *data, uint16_t len);
|
||||||
|
static SPPStreamMIDI_Interface *instance();
|
||||||
|
bool isConnected() const { return connected; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void sendChannelMessageImpl(cs::ChannelMessage msg) override;
|
||||||
|
void sendSysCommonImpl(cs::SysCommonMessage msg) override;
|
||||||
|
void sendSysExImpl(cs::SysExMessage msg) override;
|
||||||
|
void sendRealTimeImpl(cs::RealTimeMessage msg) override;
|
||||||
|
void sendNowImpl() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
cs::SerialMIDI_Parser parser;
|
||||||
|
|
||||||
|
struct RxPuller {
|
||||||
|
bool pull(uint8_t &byte);
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr size_t RX_BUF_SIZE = 256;
|
||||||
|
static volatile uint8_t rx_buf[RX_BUF_SIZE];
|
||||||
|
static volatile uint16_t rx_head;
|
||||||
|
static volatile uint16_t rx_tail;
|
||||||
|
|
||||||
|
bool connected = false;
|
||||||
|
void send_bytes(const uint8_t *data, size_t len);
|
||||||
|
friend void spp_packet_handler(uint8_t, uint16_t, uint8_t *, uint16_t);
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue