Skip to content

Instantly share code, notes, and snippets.

@idwpan
Created April 22, 2025 17:06
Show Gist options
  • Save idwpan/a8002e446eba3e6e48e9ca5d3b58bbed to your computer and use it in GitHub Desktop.
Save idwpan/a8002e446eba3e6e48e9ca5d3b58bbed to your computer and use it in GitHub Desktop.
Arduino Digital Tempo Controller
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
// --- LCD Setup ---
LiquidCrystal_I2C lcd(0x27, 20, 4); // 20x4 LCD at address 0x27
// --- Pins ---
const int ENC_CLK = 2; // Encoder A (interrupt)
const int ENC_DT = 3; // Encoder B
const int ENC_SW = 10; // Encoder push button
const int SW2_PIN = 11; // Step size switch 1
const int SW1_PIN = 12; // Step size switch 2
const int TAP_OUT = 13; // Tap tempo output (to pedal)
const unsigned long MIN_DELAY = 67;
const unsigned long MAX_DELAY = 10000;
unsigned long delay_ms = 450;
volatile int encoderDirection = 0;
volatile bool encoderMoved = false;
bool synced = true;
unsigned long lastEncoderActivity = 0;
int stepSize = 1;
// Tap control
bool tapInProgress = false;
unsigned long tapTimer = 0;
int tapStage = 0;
void setup() {
// Initialize LCD
lcd.init();
lcd.backlight();
// Setup pins
pinMode(ENC_CLK, INPUT_PULLUP);
pinMode(ENC_DT, INPUT_PULLUP);
pinMode(ENC_SW, INPUT_PULLUP);
pinMode(SW1_PIN, INPUT_PULLUP);
pinMode(SW2_PIN, INPUT_PULLUP);
pinMode(TAP_OUT, OUTPUT);
digitalWrite(TAP_OUT, LOW);
// Setup encoder interrupt
attachInterrupt(digitalPinToInterrupt(ENC_CLK), encoderISR, FALLING);
updateStepSize();
updateDisplay();
}
void loop() {
unsigned long now = millis();
// Update step size if needed
static bool lastSW1 = HIGH;
static bool lastSW2 = HIGH;
bool currSW1 = digitalRead(SW1_PIN);
bool currSW2 = digitalRead(SW2_PIN);
if (currSW1 != lastSW1 || currSW2 != lastSW2) {
updateStepSize();
updateDisplay();
lastSW1 = currSW1;
lastSW2 = currSW2;
}
// Handle encoder movement
if (encoderMoved) {
encoderMoved = false;
lastEncoderActivity = now;
synced = false;
if (encoderDirection > 0) {
delay_ms = min(delay_ms + stepSize, MAX_DELAY);
} else if (encoderDirection < 0) {
delay_ms = max(delay_ms - stepSize, MIN_DELAY);
}
updateDisplay();
}
// Check pushbutton (encoder press)
static bool lastButtonState = HIGH;
bool currButtonState = digitalRead(ENC_SW);
if (lastButtonState == HIGH && currButtonState == LOW) {
requestTap();
}
lastButtonState = currButtonState;
// Handle tap state machine
if (tapInProgress) {
handleTap(now);
}
}
void encoderISR() {
if (digitalRead(ENC_DT) == LOW) {
encoderDirection = -1; // CCW
} else {
encoderDirection = 1; // CW
}
encoderMoved = true;
}
void updateStepSize() {
bool s1 = digitalRead(SW1_PIN) == LOW;
bool s2 = digitalRead(SW2_PIN) == LOW;
if (!s1 && !s2) {
stepSize = 10;
} else if (s1 && !s2) {
stepSize = 1;
} else if (!s1 && s2) {
stepSize = 100;
}
}
void updateDisplay() {
lcd.clear();
// Line 1: Tempo Controller (centered)
lcd.setCursor(0, 0);
lcd.print(centerText("Tempo Controller"));
// Line 2: Delay ms (centered)
lcd.setCursor(0, 1);
String delayText = String(delay_ms) + " ms";
lcd.print(centerText(delayText));
// Line 3: SYNCED / *UNSYNCED* (centered)
lcd.setCursor(0, 2);
String statusText = synced ? "SYNCED" : "*UNSYNCED*";
lcd.print(centerText(statusText));
// Line 4: Step info (left and right aligned)
lcd.setCursor(0, 3);
lcd.print("Step:");
String stepText = String(stepSize) + " ms";
lcd.setCursor(20 - stepText.length(), 3);
lcd.print(stepText);
}
String centerText(String text) {
int spaces = (20 - text.length()) / 2;
String result = "";
for (int i = 0; i < spaces; i++) {
result += " ";
}
result += text;
return result;
}
void requestTap() {
if (tapInProgress) return;
tapInProgress = true;
tapStage = 1;
tapTimer = millis();
}
void handleTap(unsigned long now) {
switch (tapStage) {
case 1: // Tap 1 ON
digitalWrite(TAP_OUT, HIGH);
tapStage = 2;
tapTimer = now + 50;
break;
case 2: // Tap 1 OFF
if (now >= tapTimer) {
digitalWrite(TAP_OUT, LOW);
tapStage = 3;
tapTimer = now + (delay_ms - 50);
}
break;
case 3: // Tap 2 ON
if (now >= tapTimer) {
digitalWrite(TAP_OUT, HIGH);
tapStage = 4;
tapTimer = now + 50;
}
break;
case 4: // Tap 2 OFF
if (now >= tapTimer) {
digitalWrite(TAP_OUT, LOW);
tapInProgress = false;
synced = true;
updateDisplay();
}
break;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment