5 Wake-Up On Button Press

This example demonstrates the usage of interrupts and sleep modes. In this use case, the microcontroller exits sleep on button press, turns on an LED, and goes back to sleep. On button release, it exits sleep, turns off the LED, and goes back to sleep. The LED is ON while the button is pressed, but the microcontroller can be in sleep mode.

Pins can detect transitions from ‘0’ to ‘1’ (rising edge) and from ‘1’ to ‘0’ (falling edge). An interrupt can be triggered on one or both transitions. This is configured using the ISC bit field in the PORTx.PINnCTRL register.

Figure 5-1. ISC Bit Field Control Register

These interrupts wake the device from all sleep modes. In this example, on button press/release, the microcontroller will wake from sleep and turn an LED on/off and then go back to sleep. In this way, the LED will stay on only while the button is pressed.

First, the B2 pin, where the button is attached, is configured as input, and its interrupt is activated.

PORTB.DIR &= ~ PIN2_bm;
PORTB.PIN2CTRL |= PORT_PULLUPEN_bm | PORT_ISC_BOTHEDGES_gc;
Note: When pressed, the button will close the circuit to ground, and the pin will read ‘0’. Therefore, whenever the button is not pressed, it must be connected to VDD using a pull-up resistor. The pull-up resistor is activated using the PULLUPEN bit field in the PORTx.PINnCTRL register.

Second, the sleep mode is selected using the macros defined in the <avr/sleep.h> header. The modes of sleep are Idle, Standby and Power-Down. In this example, the deepest sleep mode is used.

set_sleep_mode(sleep_MODE_PWR_DOWN);

This only selects which sleep mode to be used. The following macro makes the microcontroller go to sleep.

sleep_mode();
Note: The sleep_mode() macro also enables sleep before sending the CPU sleep instruction and disables sleep when the microcontroller wakes up.

When going to sleep, the global interrupts must be enabled. Otherwise, there is no way to wake up. Using the sei() macro defined in the <avr/interrupt.h> header enables global interrupts. In this example, this is done in the initialization code.

Sometimes, there are blocks of instructions that need to execute without interruption. They are called atomic blocks. This is achieved by disabling interrupts at the beginning of the block and re-enabling them at the end of the block. For this, the ATOMIC_BLOCK macro in <avr/atomic.h> is used.

Finally, inside the Interrupt Service Routine (ISR), a flag is set to indicate a transition (rising or falling edge) happened. Because it is a good practice to keep the ISR short, the transition type check and LED toggling are handled in the main-line code.

ISR(PORTB_PORT_vect)
{
    if(PB2_INTERRUPT)
    {
        pb2Ioc = 1;
        PB2_CLEAR_INTERRUPT_FLAG;
    }
}

Because the same interrupt is used for all the pins in a port, the ISR code must check what pin triggered the interrupt. This is done by checking its flag in PORTx.INTFLAGS. In this example, to check whether the pin B2 triggered the interrupt, the following macro is used.

#define PB2_INTERRUPT PORTB.INTFLAGS & PIN2_bm

Clearing the interrupt flag is done by writing a ‘1’ to its location in PORTn.INTFLAGS.

#define PB2_CLEAR_INTERRUPT_FLAG PORTB.INTFLAGS &= PIN2_bm
Note: If the flag is not cleared, the interrupt will keep triggering, so the flag must always be cleared before exiting the ISR. Any algorithm that relies on not clearing the interrupt flag is highly discouraged because this effectively overloads the ISR responsibility. The resulting responsibilities of the ISR will be to handle the interrupt and to keep firing until the flag is cleared. This violates the Single Responsibility principle in software design and extreme care must be taken to avoid bugs.

The main-line code checks the pin value ten milliseconds after the interrupt to decide whether it was a falling edge or a rising edge. If the pin is low (value ‘0’), then it must have been a falling edge, otherwise it was a rising edge. The ten milliseconds delay is a denouncing method.

#define PB2_LOW !(PORTB.IN & PIN2_bm)

An MCC generated code example for AVR128DA48 with the same functionality as the one described in this section can be found here: