M0AGX / LB9MG

Amateur radio and embedded systems

XMEGA power down mode for battery powered devices

This post describes how to implement firmware-controlled device power switching on an XMEGA. I am working on a portable device that is powered from a Li-Ion cell, has an USB socket for charging, MCU, couple of LEDs and a button. I wanted to keep the design as simple as possible so there is just one button connected to the MCU and no separate power switch. Most of the information applies equally to all AVR MCUs.

XMEGA has the following sleep modes: xmega power mode table The deeper the sleep mode - the lower power consumption and less clocks (and peripherals) running. In this case a button wake up happens via the asynchronous port interrupt so no clocks are necessary and "power down" mode can be used.

Schematic is not necessary to understand everything - just imagine an XMEGA32A4U, 3.3V LDO from battery to the MCU and a single button from any XMEGA IO pin to GND.

The single button has the following functions:

  • Power on - short press
  • Power off - long hold (until all LEDs light up) and release (all LEDs go off)
  • Software function - short press (while the device is running, not relevant for the power control driver)

This single-button approach has the benefit that the device is very simple to operate, no extra switches, nor PCB tracks are necessary.

Pin definitions

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#pragma once
#include <avr/io.h>

#define BUTTON_DOWN     !(PORTA.IN&PIN4_bm)
#define BUTTON_UP         PORTA.IN&PIN4_bm

#define BUTTON_init() do {   
                           PORTA.DIRCLR = PIN4_bm;   
                           PORTA.PIN4CTRL = PORT_OPC_PULLUP_gc;   
                         } while (0)

#define LED_init() do {   
                         PORTA.DIRSET = PIN1_bm;   
                         PORTD.DIRSET = PIN0_bm | PIN1_bm;   
                       } while (0)

#define LED_red_on()    PORTD.OUTCLR = PIN0_bm
#define LED_red_off()   PORTD.OUTSET = PIN0_bm
#define LED_green_on()  PORTD.OUTCLR = PIN1_bm
#define LED_green_off() PORTD.OUTSET = PIN1_bm
#define LED_blue_on()   PORTA.OUTCLR = PIN1_bm
#define LED_blue_off()  PORTA.OUTSET = PIN1_bm

Button is wired to PA4, LEDs are wired to PD0, PD1 and PA1. Button init macro configures the pin as an input with a pullup. LED init macro configures the pins as outputs.

Power control driver

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>
#include <avr/wdt.h>
#include "pins.h"
#include <util/delay.h>
#include <LUFA/Drivers/USB/USB.h>

void power_down(void){
    //disable all possible external components here
    GPS_DISABLE(); //power down GPS module

    wdt_reset();
    CCP = CCP_IOREG_gc; //unclock watchdog access
    WDT.CTRL = WDT_CEN_bm; //disable watchdog

    USB_Detach();

    LED_blue_on();
    LED_red_on();
    LED_green_on();

    while (BUTTON_DOWN){ } //wait for button release
    _delay_ms(100); //allow some time for bounce

    LED_blue_off();
    LED_red_off();
    LED_green_off();

    set_sleep_mode(SLEEP_MODE_PWR_DOWN);
    PORTA.INTCTRL = PORT_INT0LVL_HI_gc; //enable PORTA interrupt
    PORTA.PIN4CTRL = PORT_OPC_PULLUP_gc | PORT_ISC_LEVEL_gc; //low-level triggered interrupt
    PORTA.INT0MASK = PIN4_bm;

    sleep_enable();
    sei();
    sleep_cpu(); //go to power down and wait for the button ISR
                 //to reset the chip via watchdog
}

//this ISR is called when button is pressed in power down mode, it resets the MCU
ISR(PORTA_INT0_vect){
    _delay_ms(1500); //allow some time for bounce, so the bootloader will not start

    CCP = CCP_IOREG_gc; //unclock watchdog access
    WDT.CTRL = WDT_ENABLE_bm | WDT_CEN_bm | WDT_PER_64CLK_gc; //64ms watchdog timeout
    while(1){} //wait for reset
}

Main loop (or wherever the button driver is placed) has to count how long a button is held down and call power_down() at the appropriate moment. Then the following sequence takes place:

  1. All extra hardware is disabled (in my case the enable line of GPS LDO is pulled low to turn it off)
  2. Watchdog is disabled (I am not sure if it is necessary, as all clocks in power down mode are disabled so there should be no way for the watchdog to run - to be verified...)
  3. XMEGA detaches itself from USB (it is always good to make a clean detach, than rely on the PC to time out - at least PC software can be aware of the disconnection)
  4. All LEDs are lit
  5. Because the button is being pressed, now the function has to wait until it is released. Without waiting for button release the code would enter sleep mode, detect that the button is down (normal press for power on) and immediately wake up
  6. After the button is released there is a delay to allow it to bounce (again to avoid an unwanted wakeup)
  7. All LEDs are turned off
  8. Pin interrupt is enabled (active level low)
  9. XMEGA enters power down mode

Now with the MCU in power down mode when a button is pressed the pin interrupt is executed. The press can be brief as the interrupt is sensitive to the edge. Interrupt enables the watchdog and then busy waits for it to reset the MCU. It is a very safe way to assure that the device will always start in a clean state. If it was not reset for a long time then some software problems could appear (example: timer overflow counting how long the device was running) and a not easily removable battery would certainly not help.

There is a 1.5 second delay, because the device has a bootloader. The bootloader is executed at every reset and checks if the button is pressed. If there was no delay, the button press would always lead to bootloader activation (as the whole reset operation takes just ~64ms).