Arduino: LFO Generator

here’s a treat for anyone that’s into the audio side of arduino. it’s an 8-bit two-timer based LFO Generator using timer 0/2 on an ATMega328p. i’m only using timer0 for output in this code. i’ve started implementing the LFO code into my 8-bit melotronium where timer2 is dedicated for the audio output. in the larger code base, timer0 just stores values in an unsigned integer that the other timer grabs and modulates the output mathematically. for now, this should be a good reference to anyone looking for the outline of an LFO with seven different wave forms.

#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))

uint8_t sineTable[] = {
  127,130,133,136,139,143,146,149,152,155,158,161,164,167,170,173,176,178,181,184,187,190,
  192,195,198,200,203,205,208,210,212,215,217,219,221,223,225,227,229,231,233,234,236,238,
  239,240,242,243,244,245,247,248,249,249,250,251,252,252,253,253,253,254,254,254,254,254,254,
  254,253,253,253,252,252,251,250,249,249,248,247,245,244,243,242,240,239,238,236,234,
  233,231,229,227,225,223,221,219,217,215,212,210,208,205,203,200,198,195,192,190,187,184,
  181,178,176,173,170,167,164,161,158,155,152,149,146,143,139,136,133,130,127,124,121,118,
  115,111,108,105,102,99,96,93,90,87,84,81,78,76,73,70,67,64,62,59,56,54,51,49,46,44,
  42,39,37,35,33,31,29,27,25,23,21,20,18,16,15,14,12,11,10,9,7,6,5,5,4,3,2,2,1,1,1,0,0,0,
  0,0,0,0,1,1,1,2,2,3,4,5,5,6,7,9,10,11,12,14,15,16,18,20,21,23,25,27,29,31, 33,35,37,39,42,
  44,46,49,51,54,56,59,62,64,67,70,73,76,78,81,84,87,90,93,96,99,102,105,108,111,115,118,121,124
};

uint8_t tWave = 128;
uint8_t sWave = 255;
uint8_t ruWave = 128;
uint8_t rdWave = 128;
uint8_t rWave = 128;

int   i = 0;
int   rate;
int   waveform;
byte  d = HIGH;

void setup() {
  pinMode(3, OUTPUT);
  setupTimer();
  OCR0A = 128;
}

void loop() {
  waveform = map(analogRead(0),0,1023,1,7);
  rate = map(analogRead(1),0,1023,255,0);
  OCR0A = rate;
}

ISR(TIMER0_COMPA_vect) {
  if(i == 255) i = 0;
  switch(waveform) {
    case 1:
      analogWrite(3,sine(i));
    break;
    case 2:
      analogWrite(3,triangle(i));
    break;
    case 3:
      analogWrite(3, square(i));
    break;
    case 4:
      analogWrite(3, rampUp(i));
    break;
    case 5:
      analogWrite(3, rampDown(i));
    break;
    case 6:
      analogWrite(3, rand(i));
    break;
    case 7:
      analogWrite(3, white(i));
    break;
  }
  i++;
}

void setupTimer() {
  cli();
/*--- TIMER2 CONFIG ---*/
  sbi(TCCR2A,WGM20);
  sbi(TCCR2A,WGM21);
  cbi(TCCR2A,WGM22);
 
  sbi(TCCR2B, CS20);
  cbi(TCCR2B, CS21);
  cbi(TCCR2B, CS22);

  sbi(TCCR2A,COM2B1);
  cbi(TCCR2A,COM2B0);
   
 /*--- TIMER0 CONFIG ---*/ 
  cbi(TCCR0B,CS00);
  cbi(TCCR0B,CS01);
  sbi(TCCR0B,CS02);
 
  sbi(TCCR0A, COM0A1);
  cbi(TCCR0A, COM0A0);
    
  cbi(TCCR0A, WGM00);
  sbi(TCCR0A, WGM01);
  cbi(TCCR0A, WGM02);

  cbi(TIFR0,OCF0A);
  sbi(TIMSK0,OCIE0A);
  sei(); 
}

int sine(int i) {
  return sineTable[i];
}

int triangle(int i) {
  if(tWave >= 255) d = LOW;
  if(tWave <= 0) d = HIGH;
  if(d == HIGH) tWave++;
  if(d == LOW) tWave--;
  return tWave; 
}

int rampUp(int i) {
  ruWave++;
  if(ruWave > 255) ruWave = 0; 
  return ruWave;
}

int rampDown(int i) {
  rdWave--;
  if(rdWave < 0) rdWave = 255;
  return rdWave;
}

int square(int i) {
  if(i >= 128) sWave = 255;
  if(i <= 127) sWave = 0;
  return sWave;
}

int rand(int i) {
  if(i == rWave) rWave = random(255);
  return rWave;
}

int white(int i) {
  return random(255);
}

Update: Here's my current code for the ATTiny85. It's not exactly bug-free as there seems to be some issue with latch-up when the speed of the LFO is high. This of course could be remedied by limiting the map() function's minimum value. I can't remember off hand if there are any other issues. Good power supply and output filtering is definitely needed to reduce noise in any additional stages of the circuit. At some point, I'll just make a separate post detailing my tremolo effect circuit in detail (so many projects and so little time!). Until then:

/*
 * Title: LFO Generator for ATTiny85 v0.12
 * Author: Abram Morphew
 * Date: 09.19.2016
 * Purpose: Low frequency oscillator with wave output controls
 */

#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))

uint8_t sineTable[] = {
  127,130,133,136,139,143,146,149,152,155,158,161,164,167,170,173,176,178,181,184,187,190,
  192,195,198,200,203,205,208,210,212,215,217,219,221,223,225,227,229,231,233,234,236,238,
  239,240,242,243,244,245,247,248,249,249,250,251,252,252,253,253,253,254,254,254,254,254,254,
  254,253,253,253,252,252,251,250,249,249,248,247,245,244,243,242,240,239,238,236,234,
  233,231,229,227,225,223,221,219,217,215,212,210,208,205,203,200,198,195,192,190,187,184,
  181,178,176,173,170,167,164,161,158,155,152,149,146,143,139,136,133,130,127,124,121,118,
  115,111,108,105,102,99,96,93,90,87,84,81,78,76,73,70,67,64,62,59,56,54,51,49,46,44,
  42,39,37,35,33,31,29,27,25,23,21,20,18,16,15,14,12,11,10,9,7,6,5,5,4,3,2,2,1,1,1,0,0,0,
  0,0,0,0,1,1,1,2,2,3,4,5,5,6,7,9,10,11,12,14,15,16,18,20,21,23,25,27,29,31, 33,35,37,39,42,
  44,46,49,51,54,56,59,62,64,67,70,73,76,78,81,84,87,90,93,96,99,102,105,108,111,115,118,121,124
};

uint8_t tWave = 128;
uint8_t sWave = 255;
uint8_t ruWave = 128;
uint8_t rdWave = 128;
uint8_t rWave = 128;

int   i = 0;
int   rate;
int   waveform;
byte  d = HIGH;
const uint8_t output = 1;                       // output pin #6 on ATTiny85

void setup() {
  setupTimer();
  pinMode(3, INPUT);
  pinMode(2, INPUT);
  pinMode(output, OUTPUT);
  OCR0A = 128;
}

void loop() {
  waveform = map(analogRead(2),0,1023,1,6);
  rate = map(analogRead(3),0,1023,255,0);
  OCR0A = rate;
}

ISR(TIMER0_COMPA_vect) {
  if(i == 255) i = 0;
  switch(waveform) {
    case 1:
      analogWrite(output,sine(i));
    break;
    case 2:
      analogWrite(output,triangle(i));
    break;
    case 3:
      analogWrite(output, square(i));
    break;
    case 4:
      analogWrite(output, rampUp(i));
    break;
    case 5:
      analogWrite(output, rampDown(i));
    break;
    case 6:
      analogWrite(output, rand(i));
    break;
    case 7:
      analogWrite(output, white(i));
    break;
  }
  i++;
}

void setupTimer() {
  cli(); 
 /*--- TIMER0 CONFIG ---*/  
  TCCR0A = 0b10000011;
  TCCR0B = 0b00001010;    // last 3 bits set prescalar for Timer0

  cbi(TIFR,OCF0A);
  sbi(TIMSK,OCIE0A);
  sei(); 
}

int sine(int i) {
  return sineTable[i];
}

int triangle(int i) {
  if(tWave >= 255) d = LOW;
  if(tWave <= 0) d = HIGH;
  if(d == HIGH) tWave++;
  if(d == LOW) tWave--;
  return tWave; 
}

int rampUp(int i) {
  ruWave++;
  if(ruWave > 255) ruWave = 0; 
  return ruWave;
}

int rampDown(int i) {
  rdWave--;
  if(rdWave < 0) rdWave = 255;
  return rdWave;
}

int square(int i) {
  if(i >= 128) sWave = 255;
  if(i <= 127) sWave = 0;
  return sWave;
}

int rand(int i) {
  if(i == rWave) rWave = random(255);
  return rWave;
}

int white(int i) {
  return random(255);
}
  • Nacho Consolani

    Hey, this code gives me the following error when checking de syntaxis before compiling..
    LFO.ino:25:7: error: ‘int waveform’ redeclared as different kind of symbol
    listing_2.ino:57:6: note: previous declaration ‘void waveform(byte)’
    LFO.ino: In function ‘void setup()’:
    LFO.ino:28:6: error: redefinition of ‘void setup()’
    listing_2.ino:20:6: note: ‘void setup()’ previously defined here
    LFO.ino: In function ‘void loop()’:
    LFO.ino:36:6: error: redefinition of ‘void loop()’
    listing_2.ino:45:6: note: ‘void loop()’ previously defined here
    LFO.ino:37:12: error: assignment of function ‘void waveform(byte)’
    LFO.ino:37:12: error: cannot convert ‘long int’ to ‘void(byte) {aka void(unsigned char)}’ in assignment
    LFO.ino: In function ‘void __vector_14()’:
    LFO.ino:44:18: error: switch quantity not an integer
    Did you check it yourself that it was running and ready for flashing into arduino?
    Thanks and good job!

    • Abram Morphew

      Hi Nacho,
      I went ahead and grabbed the code using the copy icon in the upper right hand corner of the code block. I dropped it into a new sketch and it compiles just fine for me. The declaration errors seems like a part of the code may appear twice or alongside some other declarations of the same name in yours. I know others have downloaded and compiled the code without a hitch. Maybe you can setup a new sketch and copy it using that copy icon up the top. Thanks!

      • Nacho Consolani

        Thanks for your reply Abram! i am really interested in your work! I am make a research and learning some coding in arduino to make an LFO for compiling it in a ATtinny85, in order to introducing it as part of a tremolo or a modulated delay.. I will try again copying the code just as you explained and then I tell you how it goes.
        EDIT: I have just coppied it again, it worked! thanks again and expect to stay in contact!
        EDIT2: Another question, why did you declare PINS 5 & 6 as OUTPUTS if they are not being used in the code?

        • Abram Morphew

          that’s awesome! i actually have a very similar project that i’m working on right now using the ATTiny85. i’ve already adapted this code for it and i’ll try to post it in the next few days just in case anyone wants to do something similar. it’s not exactly bug free, but i haven’t had much time to do the necessary debugging,

          as for those pin declarations, this code was adapted from a more comprehensive Arduino synth project i was working on. i just left those declarations in there by mistake which is pretty common on this site. hah. i’ll edit the code to remove them. thanks for catching it!

          • Nacho Consolani

            Wow! thats realley great to hear somebody having the same idea! I am really struggling with TImers and interrupt concepts now, so i’m thinking that will be the real deal when passing from arduino to Attiny, to adapt de Timers set up and config..

        • Abram Morphew

          updates have been made, FYI.

          • Nacho Consolani

            Hey, I don’t know how the expresion is in english cause my native language is spanish… But your really went too far helping! in spanish we would say (specially in argentina)… “Te pasaste!!”

          • Abram Morphew

            No hay de que! El ATTiny85 es un poco mas sencillo que el ATMega. Hah. Tu ingles es mejor que mi espanol!

  • Nacho Consolani

    Hi again Abram and I am really sorry for bothering you so much. I am trying to use the ATtiny code yo posted, and it works like a charm in the chip, though i’ve been trying to control a Digital Pot via ISP with ATtiny, with no succes.. The Attiny uses Port PB0 as MOSI/DO for comunicating via ISP. I think that the timer you used in the code, (Timer 0) is assigned to that Pin (pin#5) and it is conflicting with the data sending of the ISP. Is it there any chance to change the Timer configuration to Port PB1?? With the digital Pot I don’t need an Output pin anymore.. my new “output” is the MOSI/DO in pin #5, so I only need to free that pin from any timer activity and give another pin the timer function. I hope you understand this shitty explanation. I am very newbie at this things son i am sorry for any missunderstanding. Cheers!!

    • Abram Morphew

      no hay problemas, Ignacio. i’ll try and update the code this weekend providing i get some time to make sure it works. i should just have to update the TCCRx lines in order to make that work, but i’ll have to check the datasheet to figure out what needs to happen exactly. and i’ll want to make sure that it works correctly before i update it. i’ll let you know once i have it working.

      • Nacho Consolani

        Abram i didnt mean you to waste your time in changing the code, just wanted to know if you had a clue of what the problem could be and then i’ll manage to change it, and also i may learn that timers config stuff which i have no idea how they work. But please, i dont want you to take part of your to time to solve my problem.

        • Nacho Consolani

          Better than that, if you have some time this week we could chat via facebook or Gmail to talk about it. Let me know.

          • Abram Morphew

            Sounds good, Nacho. send me an email on gmail and we’ll figure out a time that works. my username is just my last name.

  • Synthetech

    Hi Abram, thanks for this bit if code. It helped me get a simple LFO control of my Korg NJM2069 VCF chip. Here’s a vid of using your sketch with the Korg VCF…

    https://youtu.be/ZAUnktgci9Y

    Check out some other vids in my list of a Teensy 3.2+ board turned into a virtual analog synth project I have been working on, if you are interested.

    As for your LFO.. Cool that you did a ATtiny version! I may give it a try with a few extra Tinys I have.
    Is there a simple way to make the speed much slower?
    I was thinking I would probably have to use the 16 bit timer to get a nice and smooth slowwww LFO for really long sweeps.

    Cheers!
    /Blaine

    • Abram Morphew

      hi Blaine. that thing sounds like a beast! great work there. glad that you found the code useful. i’ll definitely check out your other videos as well as it looks like you’ve got some cool stuff happening there.

      and yeah, i think the 16-bit timer would give you much finer resolution on the sine wave output. this was originally a piece of a larger project that was already using the 16-bit timer. i think though that as a consequence you would need more resolution in the wav table as well. with the ATMega328, i don’t think that would be a problem. in the current code, there are 256 sample points for the sine wave. due to the symmetry of the waveform, you could actually get away with using this many samples for just one quarter of the wave (say from the midpoint to the first crest) and using some simple conditional logic statements to create the entire wave. this would give you 1024 samples without needing a larger table. of course, the 16-bit wave will require twice the memory. there is a way though that you can use the flash memory on the chip if you need to store the wave there instead of using the available RAM. a lot of options really.

      just a note on the square wave, the reason it doesn’t appear so square is due to some capacitive coupling somewhere in your circuit. with a cap in series with the output, the square wave is oscillating so slowly that it just charges and discharges the cap. LFOs typically need big meaty caps to allow the signal to pass, but this trade off distorts the square wave appearance on the oscope. to my ears though, you get the square wave sound which is all the truly matters i guess. hah.

      anyhow, thanks for the video! i’ll give you a heads up when i revisit this project again.