Arduino interrupts in C: implementing a time switch

Posted on 2014/12/15

0


This post continues the series of simple Arduino applications written in C instead of the official Arduino language and IDE. See the previous posts about the basics, using a buzzer and a LED matrix. The goals of using C are mainly to understand better the microcontroller, to reduce the needed resources in terms of code memory, RAM and clock cycles, and to use a widespread language.

As usual I am using avr-gcc compiler and its avr-libc library, and this time I wanted to use the microcontroller interrupts. One simple application that came to mind is a time switch composed by a button and a LED with the following behavior:

  • the light is normally off
  • when the button is pressed, it turns on the light
  • when the button is released, the light remains on for a while
  • after a timeout, the light is turned off.

This application will use two interrupts: one to monitor the level change on the pin attached to the button, and the other to manage the timeout. The information I used are present in the complete datasheet of the ATMega328P, which is the microcontroller mounted on my Arduino Uno.

There are three Pin Change Interrupts Requests: PCINT0, PCINT1 and PCINT2, that correspond to 3 interrupt lines internal to the microcontroller. Then there are external I/O pins that are sensitive to change and can generate the interrupts, and they can be seen in the pinout as PCINT0, PCINT1, … PCINT23. I decided to use PCINT0 interrupt request and PCINT4 pin (Port B, pin 4). From Arduino Uno schematics I can trace the PB4 pin to the pin 12, so I attached a button between pin 12 and ground. The pin will stay high when the button is not pressed thanks to the internal pull-up, and will go low when the user presses the button connecting it to ground.

Arduino Uno with button for time switch application.

Arduino Uno with button for time switch application.

For the timeout, I am going to use the Timer/Counter1 of ATMega328P, mainly because it’s the only 16bit timer while the others are 8bit, so I can manage longer time intervals. This timer is able to generate interrupts for various events, and I am going to use the TIMER1 OVF, that triggers when the counter overflows. Timer1 will be configured in normal mode, then set to a value, and then started; the counter will increase with a frequency given by the main clock and a divisor, and then when it reaches 0xFFFF it will overflow and generate the interrupt I need. So in order to count 100 I have to set the counter to 0xFFFF - 100, start it and wait for the overflow. The maximum divisor that can be configured is 1024, the clock is 16MHz and the maximum value that Timer1 can count is around 2^16, so the maximum time is 2^16*1024/16e6 ~= 4.2 seconds. In my case I am going to use a timeout of 2 seconds.

For the LED, I am using the one mounted directly on the board of my Arduino Uno, attached to pin 13 which is the pin 5 of port B (PB5) of ATMega328P.

The Arduino environment offers some functions to attach to interrupts and enable/disable them (http://arduino.cc/en/Reference/AttachInterrupt), while avr-libc library has a different method (here the online manual pages): the interrupt service routine should be defined with the ISR macro and the correct interrupt request name, such as “ISR(PCINT0_vect)” for my case. Then there are “sei()” and “cli()” macros to enable and disable interrupts globally. I also use “sleep_mode()” to implement a main loop that does nothing and consumes less power.

Here isthe code that implements all of this:

#include <stdbool.h>
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>

#define T1_MAX 0xFFFFUL
#define T1_PRESCALER 1024
#define T1_TICK_US (T1_PRESCALER/(F_CPU/1000000UL)) /* 64us @ 16MHz */
#define T1_MAX_US (T1_TICK_US * T1_MAX) /* ~4.2s @ 16MHz */

static void led_on(void)
{
    PORTB |= _BV(PORTB5);
}

static void led_off(void)
{
    PORTB &= ~_BV(PORTB5);
}

static void led_init(void)
{
	DDRB |= _BV(DDB5); /* PORTB5 as output */
    led_off();
}

static void timer_stop(void)
{
    TCCR1B &= ~(_BV(CS10)|_BV(CS11)|_BV(CS12)); /* stop timer clock */
    TIMSK1 &= ~_BV(TOIE1); /* disable interrupt */
    TIFR1 |= _BV(TOV1); /* clear interrupt flag */
}

static void timer_init(void)
{
    /* normal mode */
    TCCR1A &= ~(_BV(WGM10)|_BV(WGM11));
    TCCR1B &= ~(_BV(WGM13)|_BV(WGM12));
    timer_stop();
}

static void timer_start(unsigned long us)
{
    unsigned long ticks_long;
    unsigned short ticks;

    ticks_long = us / T1_TICK_US;
    if (ticks_long >= T1_MAX)
    {
        ticks = T1_MAX;
    }
    else
    {
        ticks = ticks_long;
    }
    TCNT1 = T1_MAX - ticks; /* overflow in ticks*1024 clock cycles */

    TIMSK1 |= _BV(TOIE1); /* enable overflow interrupt */
    /* start timer clock */
    TCCR1B &= ~(_BV(CS10)|_BV(CS11)|_BV(CS12));
    TCCR1B |= _BV(CS10)|_BV(CS12); /* prescaler: 1024 */
}

static void timer_start_ms(unsigned short ms)
{
    timer_start(ms * 1000UL);
}

ISR(TIMER1_OVF_vect) /* timer 1 interrupt service routine */
{
    timer_stop();
    led_off(); /* timeout expired: turn off LED */
}

ISR(PCINT0_vect) /* pin change interrupt service routine */
{
    led_on();
    timer_stop();
    if (bit_is_set(PINB, PINB4)) /* button released */
    {
        timer_start_ms(2000); /* timeout to turn off LED */
    }
}

static void button_init(void)
{
    DDRB &= ~_BV(DDB4); /* PORTB4 as input */
    PORTB |= _BV(PORTB4); /* enable pull-up */
    PCICR |= _BV(PCIE0); /* enable Pin Change 0 interrupt */
    PCMSK0 |= _BV(PCINT4); /* PORTB4 is also PCINT4 */
}

int main (void)
{
    led_init();
    button_init();
    timer_init();
    sei(); /* enable interrupts globally */
    while(true)
    {
        sleep_mode();
    }
}

The main function initializes the hardware, enables the interrupts and loops in sleep mode. When an interrupt request arrives, the CPU is woken up and the associated interrupt service routine is called.

First, the PCINT0 ISR is called when the button is pressed. Since the ISR fires on both rising edges and falling edges, I need to check with PINB register the level of the pin connected to the button. If the value is high, the button is released and I have to start the timer. There’s a complication here due to the fact that the button bounces a lot during state changes, so there are many interrupt requests for a single button press or release, but overall this implementation manages the bounces quite effectively. When the timer overflows, the TIMER1 OVF ISR is called and the LED is turned off.

I compiled the program and uploaded it to the Arduino Uno connected to USB with the following commands (note that I develop on a Linux Debian machine, but these commands could be used on other operating systems with some modifications):

avr-gcc -g -Os -DF_CPU=16000000UL -mmcu=atmega328p -c -o timeswitch.o timeswitch.c
avr-gcc  -mmcu=atmega328p timeswitch.o   -o timeswitch
avr-objcopy -O ihex -R .eeprom timeswitch timeswitch.hex
avrdude -F -V -c arduino -p ATMEGA328P -P /dev/ttyACM0 -b 115200 -U flash:w:timeswitch.hex

On pressing the reset button, the application begins to run.

Posted in: Hardware