日別アーカイブ: 2026年1月18日

Raspberry Pi PICOでシンセサイザー

Raspberry Pi Pico 2 (RP2350) とタッチパネル液晶、そしてUSB MIDIキーボードを組み合わせた自作シンセサイザー開発における、デバッグの記録を記事にまとめました。


【RP2350】Pico 2でタッチパネル液晶シンセを作る:CST328の座標ズレとUSB Host MIDI認識エラーとの闘い

背景

Raspberry Pi Pico 2 (RP2350) に Waveshareの2.8インチタッチLCDを接続し、ポリフォニック・シンセサイザーを作成していました。

音の生成やSDカードからのMIDIファイル再生までは順調でしたが、「タッチパネルの座標が正しく取れない」 問題と、「USB MIDIキーボードを接続しても認識しない(USBホスト機能)」 という2つの大きな壁にぶつかりました。

この記事は、その解決までの試行錯誤のログです。


エラーとの戦い:試行錯誤のプロセス

Round 1: タッチ座標がノコギリ波になる

最初に書いたコードでは、タッチコントローラー(CST328)からI2Cで単純に座標バイトを読み込んでいました。

Step 1: 最初のコード(抜粋)

C++

// よくあるI2C読み込み
Wire1.requestFrom(CST_ADDR, 4);
uint8_t x_h = Wire1.read();
uint8_t x_l = Wire1.read();
// ... 単純結合 ...

Step 2: 発生した現象

エラーメッセージは出ませんでしたが、シリアルモニタで座標を見ると以下の挙動を示しました。

  • 横方向(X軸)に指を動かすと、値が 0 -> 300 -> 1 ... のようにループする(ノコギリ波状)。
  • 画面の右に行くほど値が減る(逆転している)。

Step 3: 原因の解説

WaveshareのPythonサンプルコードを確認したところ、このタッチパネルは 12ビットのデータを変則的にパッキングして送信 しており、しかもレジスタアドレスが8bitではなく 16bit (0xD000) でアクセスする必要がありました。単純なバイト読み込みでは、上位ビットと下位ビットが正しく結合されていませんでした。

Step 4: 修正したコード(ビット演算の修正)

C++

// レジスタポインタを0xD000にセット
Wire1.beginTransmission(CST_ADDR);
Wire1.write(0xD0);
Wire1.write(0x00);
Wire1.endTransmission(false); // Repeated Start

// 変則的な12bitデコード(Pythonコードを移植)
// buf[3]にXとYの下位4ビットが混ざっている
int raw_x = ((int)buf[1] << 4) | ((buf[3] & 0xF0) >> 4);
int raw_y = ((int)buf[2] << 4) | (buf[3] & 0x0F);

Round 2: ボタンが連打されてしまう(ゴーストタッチ)

座標は取れるようになりましたが、画面上の「NEXT」ボタンを一回押したつもりが、ページが2つ3つ進んでしまう現象が発生しました。

Step 1: 問題のコード

C++

// タッチされた瞬間だけ反応するつもりだったが...
if (touch.touched) {
    // 処理
}

Step 2: 発生した現象

指を離しても、タッチパネルのレジスタに「最後の座標」が残り続け、プログラムが「まだ押されている」と誤認して処理をループさせていました。

Step 3: 修正案

「静止画検知(ジッターフィルタ)」と「リリース待ちフラグ」を導入しました。

  1. 静止検知: 人間の指は微妙に震えるため、座標が完全に一致し続ける場合は「指がない(レジスタのゴミ)」と判定して無視。
  2. リリース待ち: ボタンを押した後、一度指を離す(!touched になる)まで次の入力を受け付けないようにしました。

Round 3: MIDIコールバック関数のコンパイルエラー

次に、USB MIDIキーボードを接続するためのコードを追加したところでコンパイルエラーが発生しました。

Step 1: 追加したコード

C++

// 古いライブラリ仕様に基づいた記述
void tuh_midi_mount_cb(uint8_t d, uint8_t in_ep, uint16_t in_packet_size, uint8_t out_ep, uint16_t out_packet_size, void *ptr) {
    // ...
}

Step 2: 発生したエラー

Plaintext

error: conflicting declaration of C function 'void tuh_midi_mount_cb(...)'
note: previous declaration 'void tuh_midi_mount_cb(uint8_t, const tuh_midi_mount_cb_t*)'

Step 3: 原因の解説

使用している Adafruit TinyUSB ライブラリのバージョンが上がり、コールバック関数の引数の仕様が変わっていました。エラーログの note に正解が書いてありました。

Step 4: 修正したコード

C++

// 最新の仕様に合わせた引数
void tuh_midi_mount_cb(uint8_t idx, const tuh_midi_mount_cb_t *mount_cb_data) {
    usb_mounted = true;
}

Round 4: MIDIキーボードを認識しない(最大の山場)

コンパイルは通りましたが、Pico 2 WにUSBキーボードを繋いでも全く反応しません(Lチカによるデバッグでも反応なし)。しかし、単純なサンプルコードでは動作しました。

Step 1: 失敗していた構成

  • Arduino IDE設定: USB Stack: Adafruit TinyUSB
  • コード: USBDevice.attach()(PC接続用)と USBHost.begin(0)(キーボード接続用)を混在させていた。
  • コード: Serial1 (ハードウェアMIDI) を初期化していた。

Step 2: 発生した現象

プログラムは起動するが、USBポートにキーボードを挿しても tuh_midi_mount_cb が呼ばれない。

Step 3: 原因の解説

Pico 2 (RP2350) でUSBホスト機能を使う場合、以下の条件が必須でした。

  1. IDEの設定: メニューの「USB Stack」で “Adafruit TinyUSB (Host)” を明示的に選ぶ必要がある(無印のTinyUSBではデバイスモードが優先されるためNG)。
  2. 初期化順序: USBHost.begin(0)setup()一番最初に呼ぶ必要がある。

Step 4: 最終的な修正

IDEの設定を “Adafruit TinyUSB (Host)” に変更し、コードもUSBホスト専用に特化させました。


完成コード

これら全ての問題を解決し、タッチ操作とUSB MIDIキーボード演奏が両立した最終コードです。

<details>

<summary>クリックしてコードを展開: PolySynth_RP2350_LCD2_Touch_v16_00.ino</summary>

C++

// PolySynth_RP2350_LCD2_Touch_v16_00
// Target: Raspberry Pi Pico 2 / Pico 2 W (RP2350)
// REQUIRED IDE SETTING: Tools > USB Stack > "Adafruit TinyUSB (Host)"
//
// Fixes:
// 1. Validated for "TinyUSB (Host)" build option.
// 2. P4 (Visualizer) Touch Fix: Touch is polled continuously, independent of FFT framerate.
// 3. MIDI: Host Mode only.

#define ENABLE_TOUCH 

#include <Arduino.h>
#include <I2S.h>
#include <SPI.h> 
#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>
#include <Adafruit_TinyUSB.h> // ★ Must use "Adafruit TinyUSB (Host)" setting
#include <Wire.h> 
#include "hardware/watchdog.h" 
#include <LittleFS.h>
#include "arduinoFFT.h"
#include "hardware/vreg.h"
#include "hardware/clocks.h"

const char* VERSION_STR = "v16.00 (Host/P4Fix)";

// --- Configuration ---
#define SAMPLE_RATE 44100
#define POLYPHONY 14
#define SINE_SIZE 1024
#define AUDIO_BLOCK 64 
#define FFT_SAMPLES 512 
#define SCOPE_SAMPLES 320
#define MAX_MIDI_FILES 20
#define MAX_TRACKS 16
#define MAX_PAGES 6 

#define DELAY_LEN 16537
#define COMB1_LEN 1601
#define COMB2_LEN 1811
#define AP_LEN    499

// --- Colors ---
#define C_BLACK   0x0000
#define C_WHITE   0xFFFF
#define C_CYAN    0x07FF 
#define C_MAGENTA 0xF81F 
#define C_ORANGE  0xFD20 
#define C_GREEN   0x07E0
#define C_YELLOW  0xFFE0
#define C_RED     0xF800
#define C_GRAY    0x4208
#define C_DARK    0x1082
#define C_BLUE    0x001F

// --- Pins ---
#define TFT_BL   16  
#define TFT_DC   14
#define TFT_CS   13  
#define TFT_SCLK 10 
#define TFT_MOSI 11  
#define TFT_RST  15  

#define TOUCH_SDA 6
#define TOUCH_SCL 7
#define TOUCH_RST 17 

#define I2S_BCLK 2  
#define I2S_DOUT 4  

#define CST_ADDR 0x1A

// --- USB Objects ---
Adafruit_USBH_Host USBHost;
volatile bool midi_active = false;

// --- Prototypes ---
void triggerNoteOn(uint8_t note, uint8_t velocity, bool is_drum);
void triggerNoteOff(uint8_t note, bool is_drum);
void apply_selection(int idx);

class CST328 {
public:
    int x = 0, y = 0;
    int raw_x = 0, raw_y = 0;
    bool touched = false;
    
    int last_raw_x = -1, last_raw_y = -1;
    int static_count = 0;

    void begin() {
        Wire1.setSDA(TOUCH_SDA);
        Wire1.setSCL(TOUCH_SCL);
        Wire1.begin(); 
        Wire1.setClock(400000);
        
        pinMode(TOUCH_RST, OUTPUT);
        digitalWrite(TOUCH_RST, HIGH); delay(50);
        digitalWrite(TOUCH_RST, LOW);  delay(20);
        digitalWrite(TOUCH_RST, HIGH); delay(100);
    }

    bool read() {
        Wire1.beginTransmission(CST_ADDR);
        Wire1.write(0x01); 
        if (Wire1.endTransmission() != 0) return false;

        Wire1.requestFrom(CST_ADDR, 6);
        if (Wire1.available() < 6) return false;

        uint8_t buf[6];
        for(int i=0; i<6; i++) buf[i] = Wire1.read();

        int val_A = ((uint16_t)buf[0] << 4) | ((buf[2] & 0xF0) >> 4); 
        int val_B = ((uint16_t)buf[1] << 4) | (buf[2] & 0x0F);        

        bool valid_read = (val_A > 0 || val_B > 0);
        
        if (valid_read) {
            if (val_A == last_raw_y && val_B == last_raw_x) {
                static_count++;
            } else {
                static_count = 0; 
            }
            last_raw_y = val_A;
            last_raw_x = val_B;

            if (static_count > 20) {
                touched = false;
            } else {
                touched = true;
                raw_y = val_A; 
                raw_x = val_B; 
                
                // Calibration (Proven)
                x = map(raw_x, 317, 1, 0, 320); 
                y = map(raw_y, 1, 239, 0, 240);
                
                if (x < 0) x = 0; if (x > 320) x = 320;
                if (y < 0) y = 0; if (y > 240) y = 240;
            }
        } else {
            touched = false;
            static_count = 0;
        }
        return touched;
    }
} touch;

// --- Globals ---
const char* ALL_NAMES[] = {"SINE", "SAW", "TRI", "SQR", "NOISE", "PIANO", "ORGAN", "VIOLIN", "MBOX", "SEA", "WIND"};
const uint8_t total_presets = 11;

struct Voice {
    int note = -1; float freq = 0, target_freq = 0, ph = 0, mod_ph = 0, env = 0;
    int stage = 0; bool active = false; bool is_drum = false;
    float low = 0, band = 0, f_coeff = 0; 
};

struct MidiTrackState {
    uint32_t start_offset; uint32_t cursor; uint32_t next_tick; uint8_t running_st; bool active;           
};

struct {
    volatile int page = 0, wave = 0, preset = 0, selected_file_idx = 0;
    int total_files = 0;
    volatile float gain = 0.8f, lfo_f = 0.2f, lfo_d = 0.0f, lfo_ph = 0;
    volatile float a=0.01f, d=0.2f, s=0.6f, r=0.5f, cut=8000.0f, res=0.1f;
    volatile float glide = 0.0f, fm_idx = 0.0f, chorus = 0.0f;
    volatile float delay_mix = 0.0f; volatile float reverb_mix = 0.0f;
    volatile float play_speed = 1.0f;
    volatile bool playing = false, dirty = true; 
    volatile bool note_active = false;
    uint32_t current_tick = 0; uint32_t us_per_tick = 1000;
    volatile bool req_file_reload = false; volatile bool req_panic = false;
} p;

Voice voices[POLYPHONY];
float sineTbl[SINE_SIZE];
MidiTrackState tracks[MAX_TRACKS];
int num_tracks_active = 0;

float delayBuf[DELAY_LEN]; int delay_idx = 0;
float comb1Buf[COMB1_LEN]; int c1_idx = 0;
float comb2Buf[COMB2_LEN]; int c2_idx = 0;
float apBuf[AP_LEN];       int ap_idx = 0;
volatile int16_t scope_buf[SCOPE_SAMPLES]; 
float vReal[FFT_SAMPLES], vImag[FFT_SAMPLES];
volatile bool fft_ready = false;
volatile int s_ptr = 0; volatile int f_ptr = 0;
int last_touch_note = -1;
uint32_t last_btn_action = 0;

bool finger_released = true;

I2S i2s(OUTPUT);
Adafruit_ST7789 tft = Adafruit_ST7789(&SPI1, TFT_CS, TFT_DC, TFT_RST);
ArduinoFFT<float> FFT = ArduinoFFT<float>(vReal, vImag, FFT_SAMPLES, SAMPLE_RATE);

// --- File System ---
char midi_filenames[MAX_MIDI_FILES][32];
File midiFile;

String formatMidiName(const char* name) {
    String s = String(name); s.replace(".mid", ""); s.replace(".MID", "");
    if(s.length() > 12) s = s.substring(0, 12);
    return s;
}

void scanMidiFiles() {
    p.total_files = 0;
    Dir dir = LittleFS.openDir("/midi");
    if (!dir.next()) dir = LittleFS.openDir("/"); 
    dir.rewind();
    while (dir.next() && p.total_files < MAX_MIDI_FILES) {
        String n = dir.fileName();
        if (n.endsWith(".mid") || n.endsWith(".MID")) {
            strncpy(midi_filenames[p.total_files], n.c_str(), 31);
            p.total_files++;
        }
    }
    p.dirty = true;
}

uint32_t xorshift32() {
    static uint32_t x = 123456789;
    x ^= x << 13; x ^= x >> 17; x ^= x << 5;
    return x;
}

// --- SOUND ENGINE ---
void triggerNoteOn(uint8_t note, uint8_t velocity, bool is_drum) {
    float tf = 440.0f * powf(2.0f, (note - 69.0f) / 12.0f);
    for(int i=0; i<POLYPHONY; i++) {
        if(voices[i].active && voices[i].note == note && voices[i].is_drum == is_drum) voices[i].active = false; 
    }
    for(int i=0; i<POLYPHONY; i++) if(!voices[i].active) {
        voices[i].note = note; voices[i].target_freq = tf; 
        if(p.glide == 0 || is_drum) voices[i].freq = tf; 
        if(p.glide > 0 && !is_drum) voices[i].freq = tf; 
        voices[i].ph = 0; voices[i].stage = 1; voices[i].env = 0; 
        voices[i].active = true; voices[i].low = 0; voices[i].band = 0;
        voices[i].is_drum = is_drum;
        p.note_active = true; break;
    }
}

void triggerNoteOff(uint8_t note, bool is_drum) {
    bool any_active = false;
    for(int i=0; i<POLYPHONY; i++) {
        if(voices[i].note == note && voices[i].active && voices[i].is_drum == is_drum) voices[i].stage = 4;
        if(voices[i].active && voices[i].stage != 4) any_active = true;
    }
    p.note_active = any_active;
}

void panic() { 
    for(int i=0; i<POLYPHONY; i++) {
        voices[i].active = false; voices[i].env = 0.0f; voices[i].stage = 0;
    }
    p.note_active = false;
}

// --- SEQUENCER ---
uint32_t readVarLen() {
    uint32_t val = 0; uint8_t c;
    do { c = midiFile.read(); val = (val << 7) | (c & 0x7F); } while (c & 0x80);
    return val;
}
void init_sequencer() {
    if (midiFile) midiFile.close();
    if (p.total_files == 0) { p.playing = false; p.dirty = true; return; }
    String path = "/midi/"; path += midi_filenames[p.selected_file_idx];
    midiFile = LittleFS.open(path, "r");
    if (!midiFile) midiFile = LittleFS.open("/" + String(midi_filenames[p.selected_file_idx]), "r");
    if (!midiFile) { p.playing = false; p.dirty = true; return; }
    midiFile.seek(0); char chunk[4]; midiFile.readBytes(chunk, 4);
    if (strncmp(chunk, "MThd", 4) != 0) { p.playing = false; p.dirty = true; return; }
    midiFile.seek(10); uint16_t numTrks; midiFile.readBytes((char*)&numTrks, 2); numTrks = __builtin_bswap16(numTrks);
    uint16_t timeDiv; midiFile.readBytes((char*)&timeDiv, 2); timeDiv = __builtin_bswap16(timeDiv);
    p.us_per_tick = 500000 / timeDiv; 
    num_tracks_active = 0; midiFile.seek(14); 
    while(midiFile.available() && num_tracks_active < MAX_TRACKS && num_tracks_active < numTrks) {
        uint32_t chunkStart = midiFile.position(); midiFile.readBytes(chunk, 4);
        uint32_t len; midiFile.readBytes((char*)&len, 4); len = __builtin_bswap32(len);
        if (strncmp(chunk, "MTrk", 4) == 0) {
            tracks[num_tracks_active].start_offset = midiFile.position();
            tracks[num_tracks_active].cursor = midiFile.position();
            tracks[num_tracks_active].active = true;
            tracks[num_tracks_active].running_st = 0;
            tracks[num_tracks_active].next_tick = readVarLen();
            tracks[num_tracks_active].cursor = midiFile.position(); 
            num_tracks_active++;
        }
        midiFile.seek(chunkStart + 8 + len);
    }
    p.current_tick = 0;
}
void update_sequencer() {
    if (!p.playing) return;
    if (!midiFile) { init_sequencer(); if(!p.playing) return; }
    static uint32_t last_time = 0;
    if (micros() - last_time < (uint32_t)(p.us_per_tick / p.play_speed)) return;
    last_time = micros();
    p.current_tick++;
    bool any_active = false;
    for (int i = 0; i < num_tracks_active; i++) {
        if (!tracks[i].active) continue;
        any_active = true;
        while (tracks[i].next_tick <= p.current_tick) {
            midiFile.seek(tracks[i].cursor); 
            uint8_t b = midiFile.read();
            if (b >= 0x80) { tracks[i].running_st = b; b = midiFile.read(); }
            uint8_t status = tracks[i].running_st;
            uint8_t type = status & 0xF0; 
            if (type == 0xF0) { 
                if (status == 0xFF) { 
                    uint8_t metaType = b; uint32_t len = readVarLen();
                    if (metaType == 0x2F) tracks[i].active = false; else midiFile.seek(midiFile.position() + len);
                } else if (status == 0xF0 || status == 0xF7) { uint32_t len = readVarLen(); midiFile.seek(midiFile.position() + len); }
            } else {
                uint8_t d1 = b; uint8_t d2 = 0; if (type != 0xC0 && type != 0xD0) d2 = midiFile.read();
                uint8_t ch = status & 0x0F; bool is_drum = (ch == 9);
                if (type == 0x90 && d2 > 0) triggerNoteOn(d1, d2, is_drum);
                else if (type == 0x80 || (type == 0x90 && d2 == 0)) triggerNoteOff(d1, is_drum);
            }
            if (tracks[i].active) { tracks[i].next_tick += readVarLen(); tracks[i].cursor = midiFile.position(); } else { break; }
        }
    }
    if (!any_active) { p.playing = false; p.req_panic = true; p.dirty = true; }
}

void apply_selection(int idx) {
    p.preset = constrain(idx, 0, (int)total_presets - 1);
    p.delay_mix = 0.0f; p.reverb_mix = 0.0f;
    if (p.preset < 5) {
        p.wave = p.preset; p.a=0.01; p.d=0.3; p.s=0.8; p.r=0.3; p.cut=8000.0f; p.res=0.1f; p.lfo_d=0.0f; p.glide=0.0f;
    } else {
        switch(p.preset) {
            case 5: p.wave=1; p.a=0.01; p.d=0.5; p.s=0.0; p.r=0.4; p.cut=2800; p.res=0.1; break;
            case 6: p.wave=3; p.a=0.02; p.d=0.1; p.s=1.0; p.r=0.1; p.cut=4500; p.res=0.0; break;
            case 7: p.wave=1; p.a=0.30; p.d=0.3; p.s=0.7; p.r=0.6; p.cut=2200; p.res=0.3; p.lfo_f=0.3; p.lfo_d=0.3; p.glide=0.06; break;
            case 8: p.wave=0; p.a=0.01; p.d=1.5; p.s=0.0; p.r=1.0; p.cut=3500; p.res=0.2; break;
            case 9: p.wave=4; p.a=2.5; p.d=2.0; p.s=0.4; p.r=2.5; p.cut=600; p.res=0.1; break;
            case 10: p.wave=4; p.a=1.5; p.d=1.5; p.s=0.5; p.r=2.0; p.cut=1000; p.res=0.88; break;
        }
    }
    if (p.preset == 7) p.reverb_mix = 0.3f;
    if (p.preset == 9) p.delay_mix = 0.4f;
    p.dirty = true;
}

// UI
void drawCell(int col, int row, const char* label, String valStr, float val, float maxVal, uint16_t color) {
    int x = (col == 0) ? 5 : 165; int y = 35 + row * 50; int w = 150;
    tft.setTextColor(C_YELLOW, C_BLACK); tft.setTextSize(1);
    tft.setCursor(x, y); tft.print("K"); tft.print(col == 0 ? row + 1 : row + 5); tft.print(" "); tft.print(label);
    tft.setTextColor(C_WHITE, C_BLACK); tft.setTextSize(2);
    tft.setCursor(x, y + 12); tft.print(valStr);
    int barY = y + 36;
    if (maxVal <= 1.0f) { // Toggle
        uint16_t btnColor = (val > 0.5f) ? color : C_DARK;
        tft.fillRect(x, barY - 4, w, 14, btnColor);
        tft.drawRect(x, barY - 4, w, 14, C_WHITE);
    } else { // Slider
        tft.drawRect(x, barY, w, 6, C_GRAY);
        int fillW = constrain((int)((val / maxVal) * (w - 2)), 0, w - 2);
        tft.fillRect(x + 1, barY + 1, fillW, 4, color);
    }
}

void drawKeyboard() {
    int wk_w = 40; int bk_w = 26; int bk_h = 130;
    int y_start = 40; int h = 200;
    for(int i=0; i<8; i++) {
        tft.fillRect(i*wk_w, y_start, wk_w-1, h, C_WHITE);
        tft.drawRect(i*wk_w, y_start, wk_w-1, h, C_GRAY); 
    }
    int bk_pos[] = {1, 2, 4, 5, 6}; 
    for(int i=0; i<5; i++) {
        int cx = bk_pos[i] * wk_w;
        tft.fillRect(cx - (bk_w/2), y_start, bk_w, bk_h, C_BLACK);
    }
    tft.setTextColor(C_BLACK); tft.setTextSize(1); 
    tft.setCursor(12, 220); tft.print("C4"); tft.setCursor(292, 220); tft.print("C5");
}

void drawSystemPage() {
    tft.setTextColor(C_WHITE, C_BLACK);
    tft.setCursor(10, 40); tft.setTextSize(2); tft.print("SYSTEM MENU");
    
    tft.setCursor(10, 70); tft.setTextSize(1); tft.setTextColor(C_GRAY); tft.print("USB MODE:");
    tft.setCursor(160, 70); tft.setTextColor(C_GREEN); tft.print("HOST (KEYS)");
    
    // Version Display
    tft.setCursor(80, 210); tft.setTextSize(1); tft.setTextColor(C_GRAY);
    tft.print("FIRMWARE: "); tft.print(VERSION_STR);
}

void handle_touch() {
    touch.read();
    if (!touch.touched) {
        if (last_touch_note != -1) { triggerNoteOff(last_touch_note, false); last_touch_note = -1; }
        finger_released = true;
        return;
    }
    int tx = touch.x; int ty = touch.y;
    if (tx == 0 && ty == 0) return;
    bool can_trigger_btn = (millis() - last_btn_action > 300);

    if (ty < 50 && tx > 200) { // Global Nav
        if (finger_released) {
            p.page = (p.page + 1) % MAX_PAGES;
            if(p.page >= MAX_PAGES) p.page = 0; 
            p.dirty = true; finger_released = false;
            if (last_touch_note != -1) { triggerNoteOff(last_touch_note, false); last_touch_note = -1; }
        }
        return; 
    }

    if (p.page == 4) { // Keyboard
        if (tx < 0 || tx > 320 || ty < 40) return; 
        int wk_w = 40; int bk_w = 26; int bk_h = 130 + 40; 
        int base_note = 60; int note = -1;
        int bk_centers[] = {40, 80, 160, 200, 240}; int bk_notes[] = {1, 3, 6, 8, 10}; 
        bool is_black = false;
        if (ty < bk_h) { 
            for(int i=0; i<5; i++) {
                if (tx >= (bk_centers[i] - bk_w/2) && tx <= (bk_centers[i] + bk_w/2)) {
                    note = base_note + bk_notes[i]; is_black = true; break;
                }
            }
        }
        if (!is_black) {
            int wk_idx = tx / wk_w; int wk_notes[] = {0, 2, 4, 5, 7, 9, 11, 12};
            if(wk_idx >= 0 && wk_idx < 8) note = base_note + wk_notes[wk_idx];
        }
        if (note != last_touch_note) {
            if (last_touch_note != -1) triggerNoteOff(last_touch_note, false);
            if (note != -1) triggerNoteOn(note, 100, false);
            last_touch_note = note;
        }
    }
    else if (p.page < 4) { // Main UI
        if (tx < 0 || tx > 320 || ty < 35 || ty > 235) return;
        int row = (ty - 35) / 50; int col = (tx < 160) ? 0 : 1;
        float cellX = (col == 0) ? tx - 5 : tx - 165;
        float normVal = constrain(cellX / 150.0f, 0.0f, 1.0f);

        if (p.page == 0) {
            if(col==0 && row==0) apply_selection((int)(normVal * total_presets));
            if(col==0 && row==1 && p.total_files > 0) {
                int new_idx = constrain((int)(normVal * p.total_files), 0, p.total_files - 1);
                if (p.selected_file_idx != new_idx) { p.selected_file_idx = new_idx; p.req_file_reload = true; p.dirty = true; }
            }
            if(col==0 && row==2 && finger_released) { p.playing = !p.playing; if(!p.playing) p.req_panic = true; p.dirty = true; finger_released = false; }
            if(col==0 && row==3) { p.play_speed = 0.5f + normVal * 1.5f; p.dirty = true; }
            if(col==1 && row==0) { p.wave = (int)(normVal * 5); p.dirty = true; }
            if(col==1 && row==1) { p.chorus = normVal; p.dirty = true; }
            if(col==1 && row==2 && finger_released) { p.page = (p.page + 1) % MAX_PAGES; p.dirty = true; finger_released = false; }
            if(col==1 && row==3) { p.gain = normVal; p.dirty = true; }
        }
        else if (p.page == 1) { 
            if(col==0 && row==0) p.glide=normVal; if(col==0 && row==1) p.cut=normVal*8000;
            if(col==0 && row==2) p.res=normVal; if(col==0 && row==3) p.delay_mix=normVal;
            if(col==1 && row==0) p.reverb_mix=normVal; if(col==1 && row==1) p.fm_idx=normVal*5.0f;
            if(col==1 && row==2 && finger_released) { p.page = (p.page + 1) % MAX_PAGES; p.dirty = true; finger_released = false; }
            if(col==1 && row==3) p.gain=normVal; p.dirty=true;
        }
        else if (p.page == 2) {
            if(col==0 && row==0) p.a=normVal*2.0; if(col==0 && row==1) p.d=normVal*2.0;
            if(col==0 && row==2) p.s=normVal; if(col==0 && row==3) p.r=normVal*2.0;
            if(col==1 && row==0) p.lfo_f=normVal; if(col==1 && row==1) p.lfo_d=normVal;
            if(col==1 && row==2 && finger_released) { p.page = (p.page + 1) % MAX_PAGES; p.dirty = true; finger_released = false; }
            if(col==1 && row==3) p.gain=normVal; p.dirty=true;
        }
    }
}

void core1_entry() {
    pinMode(TFT_BL, OUTPUT); digitalWrite(TFT_BL, HIGH);
    pinMode(TFT_RST, OUTPUT); digitalWrite(TFT_RST, LOW); delay(50); digitalWrite(TFT_RST, HIGH); delay(50);

    tft.init(240, 320); tft.setRotation(1); tft.fillScreen(C_BLACK);
    tft.setCursor(40, 100); tft.setTextSize(3); tft.setTextColor(C_CYAN); tft.print("PolySynth");
    tft.setCursor(100, 140); tft.setTextSize(2); tft.setTextColor(C_WHITE); tft.print(VERSION_STR);
    delay(2000); 

    int last_pg = -1; uint32_t last_draw = 0; 
    touch.begin();

    while (1) {
        // ★ Polling touch frequently is key
        handle_touch(); 
        
        int cur_pg = p.page;
        uint16_t theme = (cur_pg==0)?C_CYAN : (cur_pg==1)?C_MAGENTA : (cur_pg==2)?C_ORANGE : (cur_pg==3)?C_GREEN : (cur_pg==4)?C_WHITE : C_RED;
        
        if (cur_pg != last_pg) { tft.fillScreen(C_BLACK); last_pg = cur_pg; p.dirty = true; }

        if (p.dirty && (millis() - last_draw > 50)) {
            last_draw = millis();
            tft.fillRect(0, 0, 320, 25, C_DARK);
            tft.setTextColor(theme, C_DARK); tft.setTextSize(1); tft.setCursor(10, 8);
            tft.print("P"); tft.print(cur_pg + 1); tft.print(" "); 
            
            // ★ USB DIAGNOSTIC DISPLAY ★
            tft.setCursor(150, 8); 
            if(usb_mounted) {
                tft.setTextColor(C_GREEN, C_DARK); tft.print("USB:OK ");
            } else {
                tft.setTextColor(C_RED, C_DARK); tft.print("USB:-- ");
            }
            
            if(midi_rx_activity) {
                tft.setTextColor(C_YELLOW, C_RED); tft.print("MIDI!");
                midi_rx_activity = false; // Reset flash
            }

            tft.fillRect(260, 0, 60, 25, C_GRAY);
            tft.setCursor(270, 8); tft.setTextColor(C_WHITE, C_GRAY); tft.print("NEXT");

            if (cur_pg < 3) {
                 // ... Same drawing logic as before ...
                 if(cur_pg==0) {
                     drawCell(0,0,"PRESET",ALL_NAMES[p.preset],p.preset,10,theme);
                     String fName = "NO FILES"; if(p.total_files>0) fName = formatMidiName(midi_filenames[p.selected_file_idx]);
                     drawCell(0,1,"FILE",fName,1,1,(p.total_files>0?theme:C_RED));
                     drawCell(0,2,"PLAY",(p.playing?"ON":"OFF"),p.playing,1,theme);
                     drawCell(0,3,"SPEED",String((int)(p.play_speed*100))+"%",p.play_speed,2.0,theme);
                     drawCell(1,0,"WAVE",ALL_NAMES[p.wave],p.wave,10,theme);
                     drawCell(1,1,"CHORUS",String((int)(p.chorus*100))+"%",p.chorus,1.0,theme);
                     drawCell(1,2,"PAGE","NEXT",0,1,C_GRAY);
                     drawCell(1,3,"VOL",String((int)(p.gain*100)),p.gain,1.0,C_WHITE);
                 } 
                 else if(cur_pg==1) {
                     drawCell(0,0,"GLIDE",String(p.glide,2),p.glide,1.0,theme); drawCell(0,1,"CUTOFF",String((int)p.cut),p.cut,12000,theme);
                     drawCell(0,2,"RESON",String(p.res,2),p.res,1.0,theme); drawCell(0,3,"DELAY",String((int)(p.delay_mix*100))+"%",p.delay_mix,1.0,theme);
                     drawCell(1,0,"REVERB",String((int)(p.reverb_mix*100))+"%",p.reverb_mix,1.0,theme); drawCell(1,1,"FM IDX",String(p.fm_idx,1),p.fm_idx,5.0,theme);
                     drawCell(1,2,"PAGE","NEXT",0,1,C_GRAY); drawCell(1,3,"VOL",String((int)(p.gain*100)),p.gain,1.0,C_WHITE);
                 }
                 else if(cur_pg==2) {
                     drawCell(0,0,"ATTACK",String(p.a,2),p.a,2.0,theme); drawCell(0,1,"DECAY",String(p.d,2),p.d,2.0,theme);
                     drawCell(0,2,"SUSTAIN",String(p.s,2),p.s,1.0,theme); drawCell(0,3,"RELEASE",String(p.r,2),p.r,2.0,theme);
                     drawCell(1,0,"LFO F",String(p.lfo_f*20,1),p.lfo_f,1.0,theme); drawCell(1,1,"LFO D",String(p.lfo_d,1),p.lfo_d,1.0,theme);
                     drawCell(1,2,"PAGE","NEXT",0,1,C_GRAY); drawCell(1,3,"VOL",String((int)(p.gain*100)),p.gain,1.0,C_WHITE);
                 }
            } else if (cur_pg == 4) { drawKeyboard(); } 
            else if (cur_pg == 5) { drawSystemPage(); }
            p.dirty = false;
        }

        if (cur_pg == 3) { 
            // ★ Throttled to 66ms (15 FPS) to allow Touch priority
            if(millis() - last_draw > 66) { 
                tft.fillRect(0, 42, 320, 78, 0); 
                int cy = 81; for (int i = 0; i < SCOPE_SAMPLES - 1; i++) { int y1 = constrain(cy + (scope_buf[i] / 500), 42, 118); int y2 = constrain(cy + (scope_buf[i+1] / 500), 42, 118); tft.drawLine(i, y1, i+1, y2, C_CYAN); }
                if (fft_ready) {
                    tft.fillRect(0, 120, 320, 100, 0); 
                    FFT.windowing(FFTWindow::Hamming, FFTDirection::Forward); FFT.compute(FFTDirection::Forward); FFT.complexToMagnitude();
                    for (int i=1; i < 161; i++) { int h = (int)constrain(13.0f * log10f(vReal[i] + 1.0f), 0, 90); uint16_t barColor = C_GREEN; if(h>40) barColor=C_YELLOW; if(h>70) barColor=C_RED; tft.fillRect((i-1)*2, 210-h, 2, h, barColor); }
                    fft_ready = false;
                }
            }
        }
        // No heavy delay
    }
}

// ... (Setup/Loop) ...
void setup() {
    vreg_set_voltage(VREG_VOLTAGE_1_20); delay(10);
    set_sys_clock_khz(250000, true);
    Serial1.setTX(0); Serial1.begin(31250); 
    SPI1.setSCK(TFT_SCLK); SPI1.setTX(TFT_MOSI); SPI1.begin();
    
    pinMode(TOUCH_RST, OUTPUT); digitalWrite(TOUCH_RST, HIGH); delay(50); digitalWrite(TOUCH_RST, LOW);  delay(20); digitalWrite(TOUCH_RST, HIGH); delay(100);
    Wire1.setSDA(TOUCH_SDA); Wire1.setSCL(TOUCH_SCL); Wire1.begin(); Wire1.setClock(400000);

    if(LittleFS.begin()) { scanMidiFiles(); }
    
    // ★ USB HOST START (Always ON) ★
    USBHost.begin(0); 
    
    for (int i=0; i<SINE_SIZE; i++) sineTbl[i] = sinf(2.0f * PI * i / SINE_SIZE);
    i2s.setBCLK(I2S_BCLK); i2s.setDATA(I2S_DOUT); i2s.begin(SAMPLE_RATE);
    apply_selection(0); 
    multicore_launch_core1(core1_entry); 
}

void loop() {
    // ★ USB HOST TASK ★
    USBHost.task();
    
    update_sequencer();
    if (p.req_panic) { panic(); p.req_panic = false; }
    if (p.req_file_reload) { if (midiFile) midiFile.close(); p.playing = false; num_tracks_active = 0; p.req_file_reload = false; }

    float lfo_val = sinf(p.lfo_ph) * p.lfo_d * 10.0f;
    p.lfo_ph += (2.0f * PI * (p.lfo_f * 20.0f)) / SAMPLE_RATE;
    if(p.lfo_ph >= 2.0f * PI) p.lfo_ph -= 2.0f * PI;
    float g_factor = powf(0.001f, 1.0f / (max(p.glide, 0.001f) * SAMPLE_RATE));
    float q = 1.0f - p.res;

    for (int s=0; s<AUDIO_BLOCK; s++) {
        float mix = 0;
        for (int i=0; i<POLYPHONY; i++) {
            if (voices[i].active) {
                if (!voices[i].is_drum && p.glide > 0) voices[i].freq = voices[i].target_freq + (voices[i].freq - voices[i].target_freq) * g_factor;
                else voices[i].freq = voices[i].target_freq;
                float mod = 0;
                if(!voices[i].is_drum && p.fm_idx > 0) mod = voices[i].freq * p.fm_idx * sinf(voices[i].ph * 2.0f * PI);
                float target_cut = (p.wave == 4) ? voices[i].freq : p.cut;
                if (s == 0) voices[i].f_coeff = 2.0f * sinf(PI * constrain(target_cut, 50, 15000) / SAMPLE_RATE);
                
                float cur_ph = voices[i].ph; float osc_out = 0;
                if (voices[i].is_drum) { osc_out = (voices[i].note < 40) ? sinf(cur_ph * 20.0f * PI) + (((int32_t)xorshift32()) / 2147483648.0f) * 0.3f : ((int32_t)xorshift32()) / 2147483648.0f; } 
                else {
                    if(p.wave==0) osc_out = sineTbl[(int)(cur_ph*SINE_SIZE)%SINE_SIZE];
                    else if(p.wave==1) osc_out = 2.0f*(cur_ph-0.5f);
                    else if(p.wave==2) osc_out = (cur_ph < 0.5f) ? (4.0f * cur_ph - 1.0f) : (3.0f - 4.0f * cur_ph);
                    else if(p.wave==3) osc_out = (cur_ph<0.5f)?0.5f:-0.5f;
                    else osc_out = ((int32_t)xorshift32()) / 2147483648.0f;
                }
                voices[i].low += voices[i].f_coeff * voices[i].band;
                voices[i].band += voices[i].f_coeff * (osc_out - voices[i].low - q * voices[i].band);
                float env = voices[i].env;
                if(voices[i].is_drum) env *= (voices[i].note < 40) ? 0.9f : 0.6f;
                mix += voices[i].low * env * 0.20f;
                voices[i].ph += (voices[i].freq + mod + lfo_val)/SAMPLE_RATE;
                if(voices[i].ph >= 1.0f) voices[i].ph -= 1.0f;
                
                float stp = 1.0f / SAMPLE_RATE;
                float atk = voices[i].is_drum ? 0.001f : p.a;
                float dec = voices[i].is_drum ? 0.1f : p.d;
                float sus = voices[i].is_drum ? 0.0f : p.s;
                float rel = voices[i].is_drum ? 0.1f : p.r;

                if (voices[i].stage == 1) { voices[i].env += stp/max(atk,0.001f); if(voices[i].env>=1.0f) voices[i].stage=2; }
                else if (voices[i].stage == 2) { voices[i].env -= (stp/max(dec,0.001f))*(1.0 - sus); if(voices[i].env<=sus) voices[i].stage=3; }
                else if (voices[i].stage == 4) { voices[i].env -= stp/max(rel,0.001f); if(voices[i].env<=0) { voices[i].active=false; voices[i].note=-1; } }
            }
        }
        float d_out = delayBuf[delay_idx]; delayBuf[delay_idx] = mix + d_out * (0.5f + p.chorus * 0.2f); delay_idx = (delay_idx + 1) % DELAY_LEN;
        float dry_plus_delay = mix + d_out * p.delay_mix;
        float c1 = comb1Buf[c1_idx]; comb1Buf[c1_idx] = dry_plus_delay + c1 * 0.7f; c1_idx = (c1_idx + 1) % COMB1_LEN;
        float c2 = comb2Buf[c2_idx]; comb2Buf[c2_idx] = dry_plus_delay + c2 * 0.65f; c2_idx = (c2_idx + 1) % COMB2_LEN;
        float rev_in = c1 + c2;
        float ap_out = apBuf[ap_idx]; float ap_new = rev_in + ap_out * 0.5f; apBuf[ap_idx] = ap_new; ap_idx = (ap_idx + 1) % AP_LEN;
        float final_rev = ap_new - rev_in; 
        
        float output = (dry_plus_delay + final_rev * p.reverb_mix) * p.gain * 12000.0f;
        int16_t dry_int = (int16_t)constrain(output, -32000, 32000);
        i2s.write(dry_int); i2s.write(dry_int);
        
        if (p.page == 3) {
            if(s_ptr < SCOPE_SAMPLES) scope_buf[s_ptr++] = dry_int; else s_ptr = 0;
            if (!fft_ready) { vReal[f_ptr] = (float)dry_int; vImag[f_ptr] = 0; if(++f_ptr >= FFT_SAMPLES) { f_ptr = 0; fft_ready = true; } }
        }
    }
}

// ★ FIXED CALLBACKS (Updated for new library) ★
extern "C" {
// ★ FIXED SIGNATURE: use const tuh_midi_mount_cb_t *
void tuh_midi_mount_cb(uint8_t idx, const tuh_midi_mount_cb_t *mount_cb_data) {
    usb_mounted = true;
}
void tuh_midi_unmount_cb(uint8_t idx) {
    usb_mounted = false;
}
void tuh_midi_rx_cb(uint8_t d, uint32_t n_p) {
    uint8_t pkt[4];
    while (tuh_midi_packet_read(d, pkt)) {
        midi_rx_activity = true; 
        uint8_t st = pkt[1] & 0xF0, d1 = pkt[2], d2 = pkt[3];
        float val = d2 / 127.0f;
        
        if (st == 0x90 && d2 > 0) {
            float tf = 440.0f * powf(2.0f, (d1 - 69.0f) / 12.0f);
            for(int i=0; i<POLYPHONY; i++) if(!voices[i].active) {
                voices[i].note=d1; voices[i].target_freq=tf; 
                if(p.glide==0) voices[i].freq=tf;
                voices[i].ph=0; voices[i].stage=1; voices[i].env=0; voices[i].active=true;
                voices[i].low=0; voices[i].band=0; 
                p.note_active = true; break;
            }
        } else if (st == 0x80 || (st == 0x90 && d2 == 0)) {
            bool any_active = false;
            for(int i=0; i<POLYPHONY; i++) {
                if(voices[i].note==d1) voices[i].stage=4;
                if(voices[i].active && voices[i].stage!=4) any_active = true;
            }
            p.note_active = any_active;
        }
        
        if (st == 0xB0) {
            if (d1 == 7) { p.page = (d2 * 4) / 128; p.dirty = true; }
            else if (d1 == 8) { p.gain = val; p.dirty = true; }
            else {
                switch(p.page) {
                    case 0:
                        if(d1==1) apply_selection((d2 * total_presets) / 128);
                        if(d1==2 && p.total_files>0) { int nx=(d2*p.total_files)/128; if(p.selected_file_idx!=nx) { p.selected_file_idx=nx; p.req_file_reload=true; p.dirty=true; }}
                        if(d1==3) { bool q=(d2>64); if(p.playing!=q){p.playing=q; if(!q)p.req_panic=true; p.dirty=true;} }
                        if(d1==4) { p.play_speed = 0.5f+val*1.5f; p.dirty=true; }
                        if(d1==5) { p.wave=(d2*5)/128; p.dirty=true; }
                        if(d1==6) { p.chorus=val; p.dirty=true; }
                        break;
                    case 1:
                        if(d1==1)p.glide=val; if(d1==2)p.cut=val*8000.0f; 
                        if(d1==3)p.res=val; if(d1==4)p.delay_mix=val; 
                        if(d1==5)p.reverb_mix=val; if(d1==6)p.fm_idx=val*5.0f;
                        p.dirty = true; break;
                    case 2:
                        if(d1==1)p.a=val; if(d1==2)p.d=val; if(d1==3)p.s=val; if(d1==4)p.r=val;
                        if(d1==5)p.lfo_f=val; if(d1==6)p.lfo_d=val; p.dirty = true; break;
                }
            }
        }
    }
}
}

</details>

教訓

  • タッチパネルのデータ仕様はデータシートを読むか、サンプルコードを徹底的に解析する: 8bitだと思い込んでいたら、実は12bit変則パッキングだった。
  • ゴーストタッチ対策: 静電容量式タッチパネルでも、ドライバレベルでのチャタリング除去や静止画検知が必要な場合がある。
  • USBホスト機能はIDE設定が命: コードが正しくても、コンパイラの設定(USB Stack)が間違っていればハードウェアは動かない。これが今回の最大の落とし穴でした。
  • ライブラリの更新履歴: エラーメッセージの conflicting declaration は、API仕様変更の証拠。ヘッダファイルを確認するのが一番の近道。

USB 接続のmidi keyboardの認識に苦労

Grokの次のアドバイスで救われました。

Arduino IDEのライブラリフォルダを開く Windowsの場合: C:\Users\[ユーザー名]\AppData\Local\Arduino15\packages\rp2040\hardware\rp2040\[バージョン]\libraries\Adafruit_TinyUSB_Arduino\src\arduino\ports\rp2040\

そこに tusb_config_rp2040.h というファイルがあります。

そのファイルをテキストエディタで開き、以下の2箇所を修正:

① MIDI Hostを有効にする 以下の行を探して(なければ追加):

#define CFG_TUH_MIDI 1

(0 になっていたら1に変更)

② 列挙バッファを大きくする(MPK mini mk3の記述子が長いため必須) 以下の行を探して:

#define CFG_TUH_ENUMERATION_BUFSIZE 512

Waveshare RP2350-Touch-LCD-2

機能グループGPIO番号用途 / 接続先インターフェース備考
LCD (ST7789T3)GPIO15LCD_BL (バックライト PWM制御)GPIO (PWM)占有
LCDGPIO16LCD_DC (Data/Command)SPI占有
LCDGPIO17LCD_CS (Chip Select)SPI占有
LCDGPIO18LCD_CLK (SCK / Clock)SPI占有
LCDGPIO19LCD_DIN (MOSI / Data In)SPI占有
LCDGPIO20LCD_RST (Reset)GPIO占有
Touch (CST816D) + IMU (QMI8658)GPIO12TP_SDA / IMU_SDA (I2C データ)I2C共有
Touch / IMUGPIO13TP_SCL / IMU_SCL (I2C クロック)I2C共有
Touch / IMUGPIO14TP_INT / IMU_INT1 (割り込み)GPIO共有(割り込み)
TFカード (SD)GPIO24SD_SCLK (Clock)SPI占有
TFカードGPIO25SD_CS (Chip Select)SPI占有
TFカードGPIO26SD_MISO (DO / Data Out)SPI占有
TFカードGPIO27SD_MOSI (DI / Data In)SPI占有
カメラ (DVP)GPIO0CAM_D0 (Data 0)Parallel DVP占有
カメラGPIO1CAM_D1Parallel DVP占有
カメラGPIO2CAM_D2Parallel DVP占有
カメラGPIO3CAM_D3Parallel DVP占有
カメラGPIO4CAM_D4Parallel DVP占有
カメラGPIO5CAM_D5Parallel DVP占有
カメラGPIO6CAM_D6Parallel DVP占有
カメラGPIO7CAM_D7Parallel DVP占有
カメラGPIO8CAM_VSYNC (Vertical Sync)DVP占有
カメラGPIO9CAM_HREF (Horizontal Reference)DVP占有
カメラGPIO10CAM_PCLK (Pixel Clock)DVP占有
カメラGPIO11CAM_XCLK (Master Clock出力)GPIO (クロック)占有
カメラGPIO21CAM_PWDN (Power Down)GPIO占有
バッテリー監視GPIO28BAT_ADC (電池電圧 ADC入力)ADC占有