Tuesday 27 September 2011

Using the Watch Dog on an ATmega1281 as lock-up detector and sleep timer

Overview
In this post I will be describing how to use the Watchdog Timer of the ATmega1281 in order to detect and handle code lock-ups and at the same time, as a timer to wake the device from low power sleep mode.
Note this same approach should work with the ATmega640, ATmega1280, ATmega2560 and ATmega2561 micro-controllers.
This entry assumes the reader has some experience developing for the ATmega1281 micro-controller, understands how a typical Watchdog Timer works and understands the sleep modes of he ATmega1281.


Atmega1281 Watchdog Timer (WDT)
Functionality
The WDT has three modes of operation:

  • Interrupt - The WDT_vect interrupt sub-routing will be called when the WDT times out. Can be used to wake the micro from sleep modes, including the lowest power sleep mode (SLEEP_MODE_PWR_DOWN), where other timers are not available.
  • System Reset - A watchdog time-out reset will occur, i.e. the micro-controller will be restarted. For use in handling code lockups.
  • Interrupt and System Reset - First the WDT_vect interrupt sub-routing will be called, when completed a watchdog time-out reset will occur.
The WDT has it's own internal clock, that gives us time-out resolutions from 16ms to 8s.

Note, after a System Reset the WDT is still enabled, also the configuration will have reverted to default, i.e. 16ms time-out, If the user doesn't re-configure or disable the WDT before that time-out you can end up with the micro in a continuous System Reset loop. The following will disable the WDT before user code is executed:

/**
 * @brief Disables the watchdog at startup, otherwise after a timeout reset 
 *        it will continue to timeout with it's default value (<50mS).
 */
uint8_t mcusr_mirror __attribute__ ((section (".noinit")));
void get_mcusr(void) \
__attribute__((naked)) \
__attribute__((section(".init3")));
void get_mcusr(void)
{
    mcusr_mirror = MCUSR;
    MCUSR = 0;
    /* We always need to make sure the WDT is disabled immediately after a 
     * reset, otherwise it will continue to operate with default values.
     */
    wdt_disable();
}


Control & Registers
There are two sets of registers to be used when controlling the WDT:
  • WDTCSR - Watchdog timer Control Register, used for configuring the time-out, mode of operation, etc.
  • MCUSR - MCU Status Register, used to tell the cause of the last reset, such as brown-out reset, watchdog reset, etc. NOTE: for security reasons, there is a timed sequence for clearing the WDE and changing the time-out configuration. If you don't use this sequence properly, you'll get unexpected results.
For further detail on those registers and the timed sequence please see the ATmega1281 Reference Manual.


AVR Libraries
Atmel provide a set of macros for controlling the WDT. If you simply want to use the WDT to detect code lock-ups, that library will be fine. If you want to do anything more advanced, you will also have to manually configure the above registers.


Code
We want to use the WDT for two purposes simultaneously; detect/handle code lock-ups and as a timer to bring us out of sleep mode.
For this we will use the WDT in interrupt mode only. Depending on why the interrupt has entered, we will either force a watchdog system reset or wake the main application from it's sleep mode.
#include <stdbool.h>
#include <avr/wdt.h>
#include <avr/interrupt.h>

/**
 * @brief   Flag stating if sleep has been entered or not.
 */
volatile bool sleep_entered;


/**
 * @brief We need to enable the WDT for Interrupt mode only, so we can use it 
 *        as a sleep wakeup timer as well as for detecting lockups.
 * @note  Inline function!
 */
inline void configure_wdt(void)
{
    /* A 'timed' sequence is required for configuring the WDT, so we need to 
     * disable interrupts here.
     */
    cli(); 
    wdt_reset();
    MCUSR &= ~_BV(WDRF);
    /* Start the WDT Config change sequence. */
    WDTCSR |= _BV(WDCE) | _BV(WDE);
    /* Configure the prescaler and the WDT for interrupt mode only*/
    WDTCSR =  _BV(WDP0) | _BV(WDP3) | _BV(WDIE);
    sei();
}

/**
 * @brief Configures and enables the watchdog for the specified period. If this
 *        is not required please update the power management module.
 */
void app_sleep_init(void)
{
    /* Setup the flag. */
    sleep_entered = false;
    configure_wdt();
}


 
/**
 * @brief   Enters the module into low-power sleep mode. Execution is blocked 
 *          here until sleep is exited.
 */
void app_sleep_enter(void)
{
    set_sleep_mode(SLEEP_MODE_PWR_DOWN);
    sleep_entered = true;
    sleep_enable();
    sei();
    sleep_mode(); 
    /* Execution will resume here. */
}


/**
 * @brief   The watchdog interrupt. The watchdog has two functions: 1/ to detect 
 *          if the application code has locked up. 2/ to wake up the module
 *          from sleep.
 */
ISR(WDT_vect)
{
    /* Check if we are in sleep mode or it is a genuine WDR. */
    if(sleep_entered == false)
    {
        /* The app has locked up, force a WDR. */
        wdt_enable(WDTO_15MS);
        while(1);
    }
    else
    {
        wdt_reset();
        /* Service the timer if necessary. */
        sleep_entered = false;
        sleep_disable();
        
        /* Inline function so OK to call from here. */
        configure_wdt();
    }
}




References