Programming an AVR Tap Tempo
When I first started AVR coding I wanted to make a nice Tap Tempo. At that time, I didn't find an easy to understand AVR code for it.
So today, we are going to make a LED blink in sync with the rhythm you tapped. It may not seem like much, but it's an important step in the making of a Tap Tempo, and a cool coding exercise.
Why AVR? I don't really know, to me it seemed a bit more friendly than PIC. With the popularity of Arduino it has a lot of support and I liked the idea of knowing a bit of C.
Keep in mind this isn't an Arduino project (even though it will probably work on an Arduino), coding an AVR chip without Arduino, allows a deeper understanding and control of the micro-controller in my opinion.
So What Do We Need?
- You will need to install Atmel Studio.
- Download AVRDUDE
- Have an AVR programmer installed (I use USBasp, there's a lot of others available)
- An AVR micro-controller, I'll use an ATtiny84A
- A breadboard with a LED, a 1kΩ resistor and a momentary switch.
- You'll probably need the complete datasheet of the AVR you're using
Setting up for AVR programming can be a hassle, but there's a lot of help out there. I can recommend this series of tutorials.
You can use AVRDUDE via the external tools in Atmel Studio, here's my command line:
-c usbasp -p attiny84 -U flash:w:"$(ProjectDir)Debug\$(TargetName).hex":i
The Circuit
As you can see, the circuit is really simple. You can connect the resistor and switch to any pin with General Input/Output capacities.
Pin setup
Let's begin by avoiding any confusion. Take a look at the pin configuration of you AVR chip and identify the name of the pins connected to the Switch et LED.
In my case, Tap Switch is PB0 and LED is PB1. Let's indicate that to the compiler by using the #define command, before our program.
Now we need to set the LED pin as an output by updating its Data Direction Register. We also need to set the Tap Switch pin to high state (+5V) by updating PORT register (this connects a pull-up resistor between the pin and Vcc). This way, the AVR will be able to sense when the pressed switch is forcing the pin to ground. This is done before in the setup before the endless loop.
Here's a little reminder of how to use logic gates to update a single bit in a register. This is detailed in this LED blink tutorial.
#include <avr/io.h> #define TAP_PIN PINB0 #define LED_PIN PINB1 int main(void) { while (1) { } }
#include <avr/io.h> #define TAP_PIN PINB0 #define LED_PIN PINB1 int main(void) { DDRB |= (1<<LED_PIN); //Setting LED PIN as an output PORTB |= (1<<TAP_PIN); //Setting TAP PIN high while (1) //Infinite Loop { } }
//Setting a bit in a register is done this way: PORTA |= (1<<PINA0); //Clearing a bit in a register is done this way: PORTA &= ~(1<<PINA0); //Toggling a bit in a register is done this way: PORTA ^= (1<<PINA0);
Debouncing
Debouncing is an important part of programs containing mechanical switches. This is because mechanical switches aren't perfect and there's a flickering between on and off for a short time while the button is pressed or released. Here's an in-depth article about debouncing.
There's a lot of ways to do debouncing, but one of the most common is to just wait a bit and then check again. To do that easily we will need to use the "delay.h" library. We can access that library with the "#include" statement. You also need to indicate your CPU frequency (1MHz by default).
To check on the Tap Switch state we will be using "bit_is_clear()" function. Normally this function is included in "io.h". "bit_is_clear" returns 1 if the pin selcted is connected to ground, it returns 0 in any other case.
To check on the pin state, let's create a function that we can call at any time. This function will return 1 if the switch is pressed and 0 if the button is released.
We aslo #define a debounce time number in µs. For a precise Tap Tempo it's best not to debounce too much, a millisecond will do.
Here's our code at this point:
#include <avr/io.h> #define F_CPU 1000000UL //indicate the CPU clock frequency #include <util/delay.h> //including the delay header
#include <avr/io.h> #define F_CPU 1000000UL //indicate the CPU clock frequency #include <util/delay.h> //including the delay header #include <avr/sfr_defs.h> //including special function register #define TAP_PIN PINB0 #define LED_PIN PINB1 #define DEBOUNCE_TIME 1000 //define debounce time in µs uint8_t debounce(void) //declaring a debounce function { if (bit_is_clear(PINB,0)) //if TAP_PIN to ground { _delay_us(DEBOUNCE_TIME); //wait for debounce time if (bit_is_clear(PINB,0)) //if TAP_PIN still to ground { return(1); //function returns 1 } else //if TAP_PIN not to ground at any time, function returns 0 { return(0); } } else { return(0); } } //end of debounce function int main(void) { DDRB |= (1<<LED_PIN); //Setting LED PIN as an output PORTB |= (1<<TAP_PIN); //Setting TAP PIN high while (1) //infinite loop { } }
Setting up the Timer
Now that we can sense the footswitch, let's start counting time between those taps. This is done by setting up a ms timer.
Most AVR chip have a 8bit timer/counter, usually it's Timer 0. We want that timer to count up constantly and to return to 0 when a ms has passed.
Our internal clock is set to 1MHz, so logically we need to count 1000 clock ticks. The problem with that is that a 8bit counter only counts up to 255. We can use a prescaler to avoid that problem, if we use a prescaler of 8 we only need to count 1000/8 = 125 ticks. To select the 8 prescaler we need to set CS01 in the TCCR0B register.
In order to have a counter that counts constantly up to 124 we need to use Fast PWM with a custom TOP value (TOP is 255 by default). The new TOP value will be OCR0A in the mode we choose. This table from the datasheet (p.81) shows us which Waveform Generation Mode to use. We'll change WGMXX bits in TCCR0A and TCCR0B in order to select the WGM mode we need.
To count the ms that has passed we need to update a variable every time that the counter reaches its TOP value. To do that we need to enable interrupts. This is done with 3 lines. First, we #include the "interrupts.h" header. Then we enable the timer 0 overflow interrupt, and finally we enable global interrupts with the sei() command.
Our counter is now set up, the last thing to do is to write what to do in our overflow interrupt. Let's create a volatile global variable "ms" to increment every time the overflow interrupt is executed. The vector for our interrupt is "TIM0_OVF_vect".
Our code now looks like that:
#include <avr/io.h> #define F_CPU 1000000UL //indicate the CPU clock frequency #include <util/delay.h> //including the delay header #include <avr/sfr_defs.h> //including special feature registers #include <avr/interrupt.h> //including interrupts #define TAP_PIN PINB0 #define LED_PIN PINB1 #define DEBOUNCE_TIME 500 //define debounce time in µs volatile uint16_t ms = 0; //declaring uint8_t debounce(void) //declaring a debounce function { if (bit_is_clear(PINB,0)) //if TAP_PIN to ground { _delay_us(DEBOUNCE_TIME); //wait for debounce time if (bit_is_clear(PINB,0)) //if TAP_PIN still to ground { return(1); //function returns 1 } else //if TAP_PIN not to ground at any time, function returns 0 { return(0); } } else { return(0); } } int main(void) { DDRB |= (1<<LED_PIN); //Setting LED PIN as an output PORTB |= (1<<TAP_PIN); //Setting TAP PIN high /*timer0 setup*/ OCR0A = 124; //counter max TCNT0 = 0; //Counter start TCCR0A |= (1<<WGM00) | (1<<WGM01); //Fast PWM mode OCR1A as TOP TIMSK0 |= (1<<TOIE0); //Overflow interrupt enable TCCR0B |= (1<<WGM02) | (1<<CS01); //Fast PWM mode, No prescaler sei(); //Activate interrupts while (1) { } } ISR(TIM0_OVF_vect) //Timer overflow interrupt { ms++; //increment ms every ms }
The Tap Tempo
Now that peripherals are set up, let's code that Tap Tempo!
We'll need to keep track of some things, so let's create variables for each one :
- Has the button been released ?: if the program doesn't know it will count hundreds of taps if we keep the button pressed. So we create a uint8_t variable buttonstate. 1 if pressed 0 if released. It is initialised as 0.
- How many taps did we do ?: you don't want to do the same thing for the first and 30th tap, so lets keep track of that. We create a uint8_t variable called nbtap.
- What's the maximum delay allowed ?: If we don't limit the maximum tempo the AVR will never stop counting and that can be a problem between tap sequences. Let's create a uint16_t variable called maxtempo. We'll choose 2000 ms.
- What tempo did we just tap? : The most obious variable needed, the tempo we want the LED to blink to. Let's create a uint16_t variable called tempo. With an initial tempo of 120bpm (500ms).
We need to declare these in the main function :
Now let's start at the beginning, what do we do at the first press of the button?
Well, we need to start counting the ms in case of a second tap. We also need to indicate that the button has been pressed once, and not released yet.
The code will be an "if" statement in the main loop:
Now let's notify the program when the button is released :
Let's add another "if" for the case if we exceed the maximum tempo. We'll need to reset the nbtap variables in order to have a normal tap sequence next time.
And finally, when we tap more than once, let's update the tempo with the ms counted between the two taps. Not forgatting to update, buttonstate and nbtap variable. We will also reset the counter to count for next tap too.
int main(void) { uint8_t buttonstate = 0; //Was the button released? uint8_t nbtap = 0; //Number of taps in sequence uint16_t maxtempo = 2000; //Maximum tempo allowed in ms uint16_t tempo = 500; //The current tempo
if (debounce() == 1 && nbtap == 0 && buttonstate == 0) //first tap { TCNT0 = 0; //starts counting ms = 0; nbtap++; //the button was tapped once buttonstate = 1; //the button isn't released yet }
if (debounce() == 0 && buttonstate == 1) // if button released { buttonstate = 0; }
if (ms >= maxtempo) // ms exceed maximum tempo { nbtap = 0; //reset tap sequence }
if (debounce() == 1 && nbtap != 0 && buttonstate == 0) //not first tap { tempo = ms; //update tempo nbtap++; buttonstate = 1; TCNT0 = 0; //reset counter ms = 0; }
Making the LED Blink
Everything has been taken care of, we just need to make the LED blink now. What I propose to do is to add another ms counter for the LED. This way, the LED blinking is independant of the Tap tempo ms count.
So we add a uint16_t volatile general variable, called "ledms". We need to declare it the same way than "ms". This variable will be incremented in the timer overflow interrupt, just like "ms".
Now that our led variable is set up, let's make the LED blink with an "if" statement in the main loop.
This way the LED wil be on on the downbeat and stay on for 8ms, then turn off until the next downbeat.
volatile uint16_t ms = 0; //declaring ms counting variables volatile uint16_t ledms = 0; ISR(TIM0_OVF_vect) //Timer overflow interrupt { ms++; //increment every ms ledms++; }
if (ledms >= tempo) { ledms = 0; PORTB |= (1<<LED_PIN); _delay_ms(8); PORTB &= ~(1<<LED_PIN); }
The Final Code
#include <avr/io.h> #define F_CPU 1000000UL //indicate the CPU clock frequency #include <util/delay.h> //including the delay header #include <avr/interrupt.h> //including interrupts #define TAP_PIN PINB0 #define LED_PIN PINB1 #define DEBOUNCE_TIME 500 //define debounce time in us volatile uint16_t ms = 0; //declaring ms counting variables volatile uint16_t ledms = 0; uint8_t debounce(void) //declaring a debounce function { if (bit_is_clear(PINB,0)) //if TAP_PIN to ground { _delay_us(DEBOUNCE_TIME); //wait for debounce time if (bit_is_clear(PINB,0)) //if TAP_PIN still to ground (switch still pressed) { return(1); //function returns 1 } else //if TAP_PIN not to ground at any time, function returns 0 { return(0); } } else { return(0); } } int main(void) { DDRB |= (1<<LED_PIN); //Setting LED PIN as an output PORTB |= (1<<TAP_PIN); //Setting TAP PIN high /*timer0 setup*/ OCR0A = 124; //counter max TCNT0 = 0; //Counter start TCCR0A |= (1<<WGM00) | (1<<WGM01); //Fast PWM mode OCR1A as TOP TIMSK0 |= (1<<TOIE0); //Overflow interrupt enable TCCR0B |= (1<<WGM02) | (1<<CS01); //Fast PWM mode, No prescaler uint8_t buttonstate = 0; //Was the button released? uint8_t nbtap; // Number of Taps in sequence uint16_t maxtempo = 2000; //Maximum tempo allowed in ms uint16_t tempo = 500; sei(); //Activate interrupts while (1) { if (debounce() == 1 && nbtap == 0 && buttonstate == 0) //first tap { TCNT0 = 0; //starts counting ms = 0; nbtap++; buttonstate = 1; } if (debounce() == 0 && buttonstate == 1) // if button released { buttonstate = 0; } if (ms >= maxtempo) //if maxtempo exeeded { nbtap = 0; //reset tap sequence } if (debounce() == 1 && nbtap != 0 && buttonstate == 0) //not first tap { tempo = ms; //update tempo nbtap++; buttonstate = 1; TCNT0 = 0; //reset counter ms = 0; } if (ledms >= tempo) { ledms = 0; PORTB |= (1<<LED_PIN); _delay_ms(8); PORTB &= ~(1<<LED_PIN); } } } ISR(TIM0_OVF_vect) //Timer overflow interrupt { ms++; //increment ms every ms ledms++; }
Final Thoughts
This tutorial only presents to you a skeleton of a program. There's a lot of room for improvment. I'll leave that up to you.
Remember if there's a part you don't understand, always refer to the datasheet, it's very well explained.
All there's left to say is "Happy Coding!" and hit me up if you need help.
Legal
About