Compare commits
No commits in common. "4c6a4bfbc18c95aa36f49cf9a7590f29f7c1ac74" and "6fc0a7240610a67eecc68e1f756fbecaf50d09ec" have entirely different histories.
4c6a4bfbc1
...
6fc0a72406
60
README.md
60
README.md
@ -1,16 +1,6 @@
|
||||
# RP2040 MIDI Tracker
|
||||
|
||||
A simple MIDI step sequencer using a Raspberry Pi Pico (or Pico W), an OLED display, a rotary encoder, and an 8x8 NeoPixel matrix.
|
||||
|
||||
## Software Requirements
|
||||
|
||||
* **Board Core**: This project uses the **Raspberry Pi Pico/RP2040** core by **Earle F. Philhower, III**.
|
||||
1. Add this URL to your Additional Boards Manager URLs in Preferences:
|
||||
`https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json`
|
||||
2. Go to **Tools > Board > Boards Manager**, search for **"pico"**, and install **Raspberry Pi Pico/RP2040**.
|
||||
3. Select **Raspberry Pi Pico** (or **Pico W**) from the board menu.
|
||||
* **Libraries**: Install via Library Manager:
|
||||
* `Adafruit GFX Library`, `Adafruit SSD1306`, `Adafruit NeoPixel`
|
||||
A simple MIDI step sequencer using a Raspberry Pi Pico, an OLED display, a rotary encoder, and an 8x8 NeoPixel matrix.
|
||||
|
||||
## Connections & Wiring
|
||||
|
||||
@ -19,10 +9,10 @@ Properly wiring the components is crucial, especially for power.
|
||||
### Power
|
||||
|
||||
The project is best powered in two parts:
|
||||
1. **Raspberry Pi Pico W**: Power the Pico via its Micro-USB port from a computer or a USB wall adapter.
|
||||
1. **Raspberry Pi Pico**: Power the Pico via its Micro-USB port from a computer or a USB wall adapter.
|
||||
2. **NeoPixel 8x8 Matrix**: This component is power-hungry and **requires a separate, external 5V power supply**. A power supply capable of delivering at least 2A is recommended.
|
||||
|
||||
> **WARNING**: Do NOT power the NeoPixel matrix from the Pico's 3.3V or VBUS pins. VBUS is connected directly to the USB port, which is typically limited to 500mA. The matrix can draw over 2A, which could overload and damage your host computer's USB port.
|
||||
> **WARNING**: Do NOT power the NeoPixel matrix from the Pico's 3.3V or VBUS pins. Doing so can draw too much current and damage your Pico and/or the host computer's USB port.
|
||||
|
||||
### Component Wiring
|
||||
|
||||
@ -46,49 +36,5 @@ Make sure to establish a **common ground** by connecting the ground from your ex
|
||||
| 5V / VCC | External 5V Supply `+` | **External 5V Power** |
|
||||
| GND | External 5V Supply `-` | **External Power Ground** |
|
||||
| | GND (Pin 18) | **Common Ground with Pico** |
|
||||
| **MIDI DIN (Serial)** | | |
|
||||
| TX (MIDI OUT) | GP0 (Pin 1) | To DIN Pin 5 (via 220Ω) |
|
||||
|
||||
**MIDI Hardware Note**:
|
||||
* **MIDI OUT**: Connect **GP0** to DIN Pin 5 via a 220Ω resistor. Connect DIN Pin 4 to +5V via a 220Ω resistor. Connect DIN Pin 2 to GND.
|
||||
|
||||
Once everything is wired up, you can upload the code and your tracker should be ready to go!
|
||||
|
||||
## Making it Portable
|
||||
|
||||
To run this project without being tethered to a computer, you'll need a portable power source that can supply both the Pico and the power-hungry NeoPixel matrix.
|
||||
|
||||
### Option 1: Using a USB Power Bank (Easiest)
|
||||
|
||||
This is the simplest and safest method.
|
||||
|
||||
* **What you'll need:** A standard USB power bank with at least two outputs, capable of supplying a total of 2.5A or more.
|
||||
* **Pico Power:** Connect a standard Micro-USB cable from one of the power bank's outputs to the Pico's USB port.
|
||||
* **Matrix Power:** Use a second USB cable to power the NeoPixel matrix. You will likely need to cut the end of a USB cable and connect the 5V (usually red) and GND (usually black) wires to the matrix's power input.
|
||||
* **Common Ground:** The common ground is handled automatically through the USB connections to the same power bank. However, it is still best practice to run a dedicated wire from the matrix's GND to one of the Pico's GND pins.
|
||||
|
||||
### Option 2: LiPo Battery with a 5V Booster (More Compact)
|
||||
|
||||
For a more integrated build that can fit inside an enclosure, you can use a Lithium Polymer (LiPo) battery and a voltage-boosting board.
|
||||
|
||||
* **What you'll need:**
|
||||
* A single-cell (3.7V) LiPo battery.
|
||||
* A 5V booster board, such as the Adafruit PowerBoost 1000C. These boards can charge the LiPo battery and provide a stable 5V output.
|
||||
|
||||
> **LIPO BATTERY WARNING**: LiPo batteries are powerful but require careful handling.
|
||||
> * **Never** use a LiPo battery without a dedicated protection circuit, which prevents over-charge, over-discharge, and short-circuits. Boards like the PowerBoost series have this built-in.
|
||||
> * Do not puncture, bend, or short-circuit a LiPo battery.
|
||||
> * Always charge them with a proper LiPo charger.
|
||||
|
||||
* **Wiring:**
|
||||
1. Connect the **LiPo battery** to the battery input terminals on the **PowerBoost board**.
|
||||
2. Connect the **PowerBoost's 5V output** to the **NeoPixel Matrix's 5V/VCC input**.
|
||||
3. Connect the **PowerBoost's 5V output** to the **Pico's VBUS pin (Pin 40)**. This will power the Pico.
|
||||
4. Connect the **PowerBoost's GND** to **both** the **NeoPixel Matrix's GND** and one of the **Pico's GND pins**. This creates the essential common ground.
|
||||
|
||||
## Usage
|
||||
|
||||
* **Navigation**: Rotate the encoder to move between steps.
|
||||
* **Edit Mode**: Short press the encoder button to toggle Edit Mode. Rotate to change the note.
|
||||
* **Playback**: Long press the encoder button (> 0.6s) to Start/Stop playback.
|
||||
* **MIDI**: The device sends MIDI notes via the Hardware Serial (DIN) port on GP0.
|
||||
@ -1,327 +0,0 @@
|
||||
#include <SPI.h>
|
||||
#include <Wire.h>
|
||||
#include <Adafruit_GFX.h>
|
||||
#include <Adafruit_SSD1306.h>
|
||||
#include <Adafruit_NeoPixel.h>
|
||||
|
||||
// --- HARDWARE CONFIGURATION ---
|
||||
#define SCREEN_WIDTH 128
|
||||
#define SCREEN_HEIGHT 64
|
||||
#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin)
|
||||
#define SCREEN_ADDRESS 0x3C // See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32
|
||||
|
||||
// Pin Definitions for Raspberry Pi Pico (RP2040)
|
||||
#define PIN_SDA 4
|
||||
#define PIN_SCL 5
|
||||
|
||||
#define ENC_CLK 12
|
||||
#define ENC_DT 13
|
||||
#define ENC_SW 14
|
||||
|
||||
// MIDI UART Pins (GP0/GP1) -- OUT only so far
|
||||
#define PIN_MIDI_TX 0
|
||||
|
||||
// NeoPixel Pin (any GPIO is fine, I've chosen 16)
|
||||
#define PIN_NEOPIXEL 16
|
||||
#define NUM_PIXELS 64 // For 8x8 WS2812B matrix
|
||||
|
||||
// --- TRACKER DATA ---
|
||||
#define NUM_STEPS 16
|
||||
|
||||
struct Step {
|
||||
int8_t note; // MIDI Note (0-127), -1 for OFF
|
||||
};
|
||||
|
||||
Step sequence[NUM_STEPS];
|
||||
|
||||
// --- STATE ---
|
||||
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
|
||||
Adafruit_NeoPixel pixels(NUM_PIXELS, PIN_NEOPIXEL, NEO_GRB + NEO_KHZ800);
|
||||
|
||||
int currentStep = 0;
|
||||
bool isEditing = false;
|
||||
int scrollOffset = 0;
|
||||
bool isPlaying = false;
|
||||
unsigned long lastStepTime = 0;
|
||||
int tempo = 120; // BPM
|
||||
|
||||
|
||||
// Encoder State
|
||||
volatile int encoderDelta = 0;
|
||||
static uint8_t prevNextCode = 0;
|
||||
static uint16_t store = 0;
|
||||
|
||||
// Button State
|
||||
bool lastButtonState = HIGH;
|
||||
unsigned long lastDebounceTime = 0;
|
||||
bool buttonActive = false;
|
||||
bool buttonConsumed = false;
|
||||
unsigned long buttonPressTime = 0;
|
||||
|
||||
// --- ENCODER INTERRUPT ---
|
||||
// Robust Rotary Encoder reading
|
||||
void readEncoder() {
|
||||
static int8_t rot_enc_table[] = {0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0};
|
||||
|
||||
prevNextCode <<= 2;
|
||||
if (digitalRead(ENC_DT)) prevNextCode |= 0x02;
|
||||
if (digitalRead(ENC_CLK)) prevNextCode |= 0x01;
|
||||
prevNextCode &= 0x0f;
|
||||
|
||||
// If valid state
|
||||
if (rot_enc_table[prevNextCode]) {
|
||||
store <<= 4;
|
||||
store |= prevNextCode;
|
||||
if ((store & 0xff) == 0x2b) encoderDelta--;
|
||||
if ((store & 0xff) == 0x17) encoderDelta++;
|
||||
}
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(5000);
|
||||
Serial.println(F("Starting."));
|
||||
|
||||
// 1. Setup Encoder
|
||||
pinMode(ENC_CLK, INPUT_PULLUP);
|
||||
pinMode(ENC_DT, INPUT_PULLUP);
|
||||
pinMode(ENC_SW, INPUT_PULLUP);
|
||||
|
||||
attachInterrupt(digitalPinToInterrupt(ENC_CLK), readEncoder, CHANGE);
|
||||
attachInterrupt(digitalPinToInterrupt(ENC_DT), readEncoder, CHANGE);
|
||||
|
||||
// 2. Setup Display
|
||||
// Note: Using default I2C pins (SDA=GP4, SCL=GP5) which works on both cores.
|
||||
Wire.begin();
|
||||
|
||||
// SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
|
||||
if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
|
||||
Serial.println(F("SSD1306 allocation failed"));
|
||||
for(;;); // Don't proceed, loop forever
|
||||
}
|
||||
|
||||
// 3. Setup NeoPixel Matrix
|
||||
pixels.begin();
|
||||
pixels.setBrightness(40); // Set brightness to a medium-low value (0-255)
|
||||
pixels.clear();
|
||||
pixels.show();
|
||||
|
||||
// 4. Init Sequence
|
||||
for(int i=0; i<NUM_STEPS; i++) {
|
||||
sequence[i].note = -1; // Default to empty
|
||||
}
|
||||
// Add a simple C-Major scale for testing
|
||||
sequence[0].note = 60; // C4
|
||||
sequence[2].note = 62; // D4
|
||||
sequence[4].note = 64; // E4
|
||||
sequence[6].note = 65; // F4
|
||||
sequence[8].note = 65; // F4
|
||||
sequence[9].note = 62; // D4
|
||||
sequence[12].note = 64; // E4
|
||||
|
||||
display.clearDisplay();
|
||||
display.display();
|
||||
|
||||
// 5. Setup MIDI Serial
|
||||
Serial1.setTX(PIN_MIDI_TX);
|
||||
Serial1.begin(31250);
|
||||
Serial.println(F("MIDI Serial initialized on GP0/GP1"));
|
||||
|
||||
Serial.println(F("Started."));
|
||||
}
|
||||
|
||||
void sendMidi(uint8_t status, uint8_t note, uint8_t velocity) {
|
||||
Serial1.write(status);
|
||||
Serial1.write(note);
|
||||
Serial1.write(velocity);
|
||||
}
|
||||
|
||||
void handleInput() {
|
||||
// Handle Encoder Rotation
|
||||
int delta = 0;
|
||||
noInterrupts();
|
||||
delta = encoderDelta;
|
||||
encoderDelta = 0;
|
||||
interrupts();
|
||||
|
||||
if (delta != 0) {
|
||||
if (isEditing) {
|
||||
// Change Note
|
||||
int newNote = sequence[currentStep].note + delta;
|
||||
if (newNote < -1) newNote = -1;
|
||||
if (newNote > 127) newNote = 127;
|
||||
sequence[currentStep].note = newNote;
|
||||
Serial.print(F("Note changed: ")); Serial.println(newNote);
|
||||
} else {
|
||||
// Move Cursor
|
||||
currentStep += (delta > 0 ? 1 : -1);
|
||||
if (currentStep < 0) currentStep = NUM_STEPS - 1;
|
||||
if (currentStep >= NUM_STEPS) currentStep = 0;
|
||||
|
||||
// Adjust Scroll to keep cursor in view
|
||||
if (currentStep < scrollOffset) scrollOffset = currentStep;
|
||||
if (currentStep >= scrollOffset + 6) scrollOffset = currentStep - 5;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Button
|
||||
int reading = digitalRead(ENC_SW);
|
||||
|
||||
if (reading != lastButtonState) {
|
||||
lastDebounceTime = millis();
|
||||
}
|
||||
|
||||
if ((millis() - lastDebounceTime) > 50) {
|
||||
if (reading == LOW && !buttonActive) {
|
||||
// Button Pressed
|
||||
buttonActive = true;
|
||||
buttonPressTime = millis();
|
||||
buttonConsumed = false;
|
||||
Serial.println(F("Button Down"));
|
||||
}
|
||||
|
||||
if (reading == HIGH && buttonActive) {
|
||||
// Button Released
|
||||
buttonActive = false;
|
||||
if (!buttonConsumed) {
|
||||
isEditing = !isEditing;
|
||||
Serial.print(F("Mode toggled: ")); Serial.println(isEditing ? F("EDIT") : F("NAV"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Long Press (Start/Stop Playback)
|
||||
if (buttonActive && !buttonConsumed && (millis() - buttonPressTime > 600)) {
|
||||
isPlaying = !isPlaying;
|
||||
buttonConsumed = true; // Prevent short press action
|
||||
Serial.print(F("Playback: ")); Serial.println(isPlaying ? F("ON") : F("OFF"));
|
||||
if (!isPlaying) {
|
||||
// Send All Notes Off on stop (CC 123)
|
||||
sendMidi(0xB0, 123, 0);
|
||||
}
|
||||
}
|
||||
|
||||
lastButtonState = reading;
|
||||
}
|
||||
|
||||
void handlePlayback() {
|
||||
if (!isPlaying) return;
|
||||
|
||||
unsigned long interval = 15000 / tempo; // 16th notes (60000 / tempo / 4)
|
||||
if (millis() - lastStepTime > interval) {
|
||||
lastStepTime = millis();
|
||||
|
||||
// Note Off for current step (before advancing)
|
||||
if (sequence[currentStep].note != -1) {
|
||||
sendMidi(0x80, sequence[currentStep].note, 0);
|
||||
}
|
||||
|
||||
currentStep++;
|
||||
if (currentStep >= NUM_STEPS) currentStep = 0;
|
||||
|
||||
// Note On for new step
|
||||
if (sequence[currentStep].note != -1) {
|
||||
sendMidi(0x90, sequence[currentStep].note, 100);
|
||||
}
|
||||
|
||||
// Auto-scroll logic is handled in drawUI based on currentStep
|
||||
if (currentStep < scrollOffset) scrollOffset = currentStep;
|
||||
if (currentStep >= scrollOffset + 6) scrollOffset = currentStep - 5;
|
||||
}
|
||||
}
|
||||
|
||||
void drawUI() {
|
||||
display.clearDisplay();
|
||||
display.setTextSize(1);
|
||||
display.setTextColor(SSD1306_WHITE);
|
||||
display.setCursor(0, 0);
|
||||
|
||||
// Header
|
||||
display.print(F("TRACKER "));
|
||||
display.print(isEditing ? F("[EDIT]") : F("[NAV]"));
|
||||
display.println();
|
||||
display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
|
||||
|
||||
// Steps
|
||||
int y = 10;
|
||||
for (int i = scrollOffset; i < min(scrollOffset + 6, NUM_STEPS); i++) {
|
||||
|
||||
// Draw Cursor
|
||||
if (i == currentStep) {
|
||||
display.fillRect(0, y, 128, 8, SSD1306_WHITE);
|
||||
display.setTextColor(SSD1306_BLACK, SSD1306_WHITE); // Invert text
|
||||
} else {
|
||||
display.setTextColor(SSD1306_WHITE);
|
||||
}
|
||||
|
||||
display.setCursor(2, y);
|
||||
|
||||
// Step Number
|
||||
if (i < 10) display.print(F("0"));
|
||||
display.print(i);
|
||||
display.print(F(" | "));
|
||||
|
||||
// Note Value
|
||||
int n = sequence[i].note;
|
||||
if (n == -1) {
|
||||
display.print(F("---"));
|
||||
} else {
|
||||
// Basic Note to String conversion
|
||||
const char* noteNames[] = {"C-", "C#", "D-", "D#", "E-", "F-", "F#", "G-", "G#", "A-", "A#", "B-"};
|
||||
display.print(noteNames[n % 12]);
|
||||
display.print(n / 12 - 1); // Octave
|
||||
}
|
||||
|
||||
y += 9;
|
||||
}
|
||||
|
||||
display.display();
|
||||
}
|
||||
|
||||
// Helper to convert X,Y to pixel index for an 8x8 matrix.
|
||||
// Assumes row-major wiring (NOT serpentine).
|
||||
// If your matrix is wired differently, you'll need to change this function.
|
||||
int getPixelIndex(int x, int y) {
|
||||
return y * 8 + x;
|
||||
}
|
||||
|
||||
void updateLeds() {
|
||||
pixels.clear(); // Clear buffer
|
||||
|
||||
for (int s = 0; s < NUM_STEPS; s++) {
|
||||
int blockX = (s % 4) * 2;
|
||||
int blockY = (s / 4) * 2;
|
||||
|
||||
uint32_t color;
|
||||
|
||||
if (s == currentStep) {
|
||||
if (isEditing) {
|
||||
color = pixels.Color(50, 0, 0); // Dim Red for editing
|
||||
} else {
|
||||
color = pixels.Color(40, 40, 40); // Dim White for current step
|
||||
}
|
||||
} else {
|
||||
if (sequence[s].note != -1) {
|
||||
color = pixels.Color(0, 0, 50); // Dim Blue for step with note
|
||||
} else {
|
||||
color = 0; // Off
|
||||
}
|
||||
}
|
||||
|
||||
// Set the 4 pixels for the 2x2 block
|
||||
pixels.setPixelColor(getPixelIndex(blockX, blockY), color);
|
||||
pixels.setPixelColor(getPixelIndex(blockX + 1, blockY), color);
|
||||
pixels.setPixelColor(getPixelIndex(blockX, blockY + 1), color);
|
||||
pixels.setPixelColor(getPixelIndex(blockX + 1, blockY + 1), color);
|
||||
}
|
||||
|
||||
pixels.show();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
handleInput();
|
||||
handlePlayback();
|
||||
drawUI();
|
||||
updateLeds();
|
||||
delay(10); // Small delay to prevent screen tearing/excessive refresh
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user