#include <avr/io.h>
#include <avr/interrupt.h>
/**
Enhanced ATTiny85 Selectable Waveform LFO
This code generates different LFO waveforms (triangle, ramp up, ramp down, square)
and allows control over the LFO speed, waveform type, and duty cycle.
The LFO speed and waveform type are controlled by two potentiometers connected to analog pins.
The duty cycle is controlled by another potentiometer connected to pin PB1.
*/
// ATTiny85 pin configuration
// +-\/-+
// Reset 1| |8 VCC
// Rate control A3 PB3 2| |7 PB2 Intensity control A1 (LFO duty cycle)
// Waveform select A2 PB4 3| |6 PB1 Tap tempo footwsitch (22nF cap debouncing)
// GND 4| |5 PB0 (PWM LFO out)
// +----+
// Pins assignments
#define RATE_POT_PIN A3
#define WAVE_POT_PIN A2
#define INT_POT_PIN A1
#define TEMPO_SW_PIN PB1
#define LFO_OUT_PIN PB0
#define LFO_MIN 50 // 50ms
#define LFO_MAX 5000 // 5000ms
#define POT_REFRESH_INTERVAL 20 // ms
#define TAP_TEMPO_TIMEOUT 1000 // ms
#define MIN_TAPS 3
#define TAP_DEBOUNCE_TIME 25 // ms
#define RATE_CHANGE_THRESHOLD 11 // about 1% of 0-1023 range
// Global Variables
int lastTapState = HIGH;
unsigned long lastDebounceTime = 0;
unsigned long averageTapInterval = 0;
int tapCount = 0;
unsigned long lastTapTime = 0;
bool useTapTempo = false;
volatile uint8_t loopCount = 255;
volatile int oscFreq1 = LFO_MIN;
volatile int oscCounter1 = 0;
int lastRateValue = -1;
int waveState = 0;
int minAmplitude = 0;
unsigned long lastPotCheck = 0;
void setup() {
configurePins();
configureTimers();
sei(); // Enable global interrupts
}
// Main loop for reading potentiometer values and updating LFO speed, waveform, and duty cycle
void loop() {
checkTapTempo();
checkPotsValues();
}
// Timer1 compare match interrupt for waveform generation
ISR(TIMER1_COMPA_vect) {
if (++oscCounter1 > oscFreq1) {
oscCounter1 = 0;
loopCount = (loopCount + 1) % 256;
generateWaveform();
}
}
void configurePins() {
pinMode(RATE_POT_PIN, INPUT);
pinMode(WAVE_POT_PIN, INPUT);
pinMode(INT_POT_PIN, INPUT);
pinMode(TEMPO_SW_PIN, INPUT);
PORTB |= (1 << TEMPO_SW_PIN); // Enable pull-up
DDRB |= (1 << LFO_OUT_PIN);
}
void configureTimers() {
// Configure Timer0 for PWM
TCCR0A = _BV(WGM00) | _BV(WGM01) | _BV(COM0A1);
TCCR0B = _BV(CS00);
// Configure Timer1 for interrupt handling
TCCR1 = 0;
TCNT1 = 0;
OCR1A = 100;
OCR1C = 100;
TIMSK |= (1 << OCIE1A);
TCCR1 = _BV(CTC1) | _BV(CS10) | _BV(CS11);
}
// Checks potentiometers analog values and updates LFO.
void checkPotsValues() {
unsigned long currentMillis = millis();
if (currentMillis - lastPotCheck >= POT_REFRESH_INTERVAL) {
lastPotCheck = currentMillis;
updateOscFreqFromPot();
updateWaveStateFromPot();
updateMinAmplitudeFromPot();
}
}
void updateOscFreqFromPot() {
int rateValue = analogRead(RATE_POT_PIN);
bool isPotRateChanged = abs(lastRateValue - rateValue) > RATE_CHANGE_THRESHOLD;
if (isPotRateChanged) {
oscFreq1 = map(rateValue, 0, 1023, LFO_MIN, LFO_MAX);
useTapTempo = false;
}
lastRateValue = rateValue;
}
void updateWaveStateFromPot() {
int shapeValue = analogRead(WAVE_POT_PIN);
waveState = map(shapeValue, 0, 1023, 0, 40);
}
void updateMinAmplitudeFromPot() {
int intensityValue = analogRead(INT_POT_PIN);
minAmplitude = map(intensityValue, 0, 1023, 0, 255);
}
void checkTapTempo() {
int currentTapState = digitalRead(TEMPO_SW_PIN);
unsigned long currentTime = millis();
// Check if the button state has changed
if (currentTapState != lastTapState) {
lastDebounceTime = currentTime;
}
// Check if the current state has been stable for longer than the debounce time
if ((currentTime - lastDebounceTime) > TAP_DEBOUNCE_TIME) {
// Only trigger a tap if the button went from not pressed to pressed
if (currentTapState == LOW && lastTapState == HIGH) {
if (tapCount > 0) {
// Calculate weighted average for tap interval
long currentInterval = currentTime - lastTapTime;
averageTapInterval = (0.4 * averageTapInterval) + (0.6 * currentInterval);
}
lastTapTime = currentTime;
tapCount++;
if (tapCount >= MIN_TAPS) {
oscFreq1 = constrain(averageTapInterval, LFO_MIN, LFO_MAX); // Set oscFreq1
useTapTempo = true; // Enable tap tempo control
}
}
}
if (currentTime - lastTapTime > TAP_TEMPO_TIMEOUT && tapCount >= MIN_TAPS) {
// Timeout reached, reset for upcoming new taps
tapCount = 0;
averageTapInterval = 0;
}
lastTapState = currentTapState; // Update the last tap state
}
void generateWaveform() {
switch (waveState / 10) {
case 0: generateTriangleWave(); break;
case 1: generateRampUpWave(); break;
case 2: generateRampDownWave(); break;
case 3: generateSquareWave(); break;
default: break;
}
}
// Generates triangle waveform
void generateTriangleWave() {
static int dir = 1;
static int currentAmplitude = minAmplitude; // Use a separate variable for current amlitude
// Update the current amplitude in the direction
currentAmplitude += dir;
// Reverse direction at extremes
if (currentAmplitude >= 255 || currentAmplitude <= minAmplitude) {
dir = -dir;
}
// Clamp current amplitude to the range between amplitude and 255
currentAmplitude = constrain(currentAmplitude, minAmplitude, 255);
OCR0A = currentAmplitude; // Set PWM to current amplitude
}
// Generate ramp up waveform.
void generateRampUpWave() {
OCR0A++;
if (OCR0A > 255) {
OCR0A = minAmplitude;
}
}
// Generate ramp down waveform.
void generateRampDownWave() {
OCR0A--;
if (OCR0A < minAmplitude) {
OCR0A = 255;
}
}
// Generate square waveform.
void generateSquareWave() {
static int currentState = 0; // 0 for LOW, 1 for HIGH
// Calculate the time to switch state (simple square wave for illustration)
int switchPoint = oscFreq1 / 2;
if (oscCounter1 > switchPoint) {
currentState = 1 - currentState;
oscCounter1 = 0; // Reset counter
// Set the output based on the current state and minimum brightness
OCR0A = currentState ? 255 : minAmplitude;
}
}