nes-proj/cpu/cc2538/lpm.c

391 lines
13 KiB
C

/*
* Copyright (c) 2013, Texas Instruments Incorporated - http://www.ti.com/
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
* \addtogroup cc2538-lpm
* @{
*
* \file
* Implementation of low power modes ofr the cc2538
*/
#include "contiki-conf.h"
#include "sys/energest.h"
#include "sys/process.h"
#include "dev/sys-ctrl.h"
#include "dev/scb.h"
#include "dev/rfcore-xreg.h"
#include "rtimer-arch.h"
#include "lpm.h"
#include "reg.h"
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#if LPM_CONF_ENABLE != 0
/*---------------------------------------------------------------------------*/
#if ENERGEST_CONF_ON
static unsigned long irq_energest = 0;
#define ENERGEST_IRQ_SAVE(a) do { \
a = energest_type_time(ENERGEST_TYPE_IRQ); } while(0)
#define ENERGEST_IRQ_RESTORE(a) do { \
energest_type_set(ENERGEST_TYPE_IRQ, a); } while(0)
#else
#define ENERGEST_IRQ_SAVE(a) do {} while(0)
#define ENERGEST_IRQ_RESTORE(a) do {} while(0)
#endif
/*---------------------------------------------------------------------------*/
/*
* Deep Sleep thresholds in rtimer ticks (~30.5 usec)
*
* If Deep Sleep duration < DEEP_SLEEP_PM1_THRESHOLD, simply enter PM0
* If duration < DEEP_SLEEP_PM2_THRESHOLD drop to PM1
* else PM2.
*/
#define DEEP_SLEEP_PM1_THRESHOLD 10
#define DEEP_SLEEP_PM2_THRESHOLD 100
/*---------------------------------------------------------------------------*/
#define assert_wfi() do { asm("wfi"::); } while(0)
/*---------------------------------------------------------------------------*/
#if LPM_CONF_STATS
rtimer_clock_t lpm_stats[3];
#define LPM_STATS_INIT() \
do { memset(lpm_stats, 0, sizeof(lpm_stats)); } while(0)
#define LPM_STATS_ADD(pm, val) do { lpm_stats[pm] += val; } while(0)
#else
#define LPM_STATS_INIT()
#define LPM_STATS_ADD(stat, val)
#endif
/*---------------------------------------------------------------------------*/
/*
* Remembers what time it was when went to deep sleep
* This is used when coming out of PM0/1/2 to keep stats
*/
static rtimer_clock_t sleep_enter_time;
void clock_adjust(void);
/*---------------------------------------------------------------------------*/
/* Stores the currently specified MAX allowed PM */
static uint8_t max_pm;
/*---------------------------------------------------------------------------*/
/* Buffer to store peripheral PM1+ permission FPs */
#ifdef LPM_CONF_PERIPH_PERMIT_PM1_FUNCS_MAX
#define LPM_PERIPH_PERMIT_PM1_FUNCS_MAX LPM_CONF_PERIPH_PERMIT_PM1_FUNCS_MAX
#else
#define LPM_PERIPH_PERMIT_PM1_FUNCS_MAX 4
#endif
static lpm_periph_permit_pm1_func_t
periph_permit_pm1_funcs[LPM_PERIPH_PERMIT_PM1_FUNCS_MAX];
/*---------------------------------------------------------------------------*/
static bool
periph_permit_pm1(void)
{
int i;
for(i = 0; i < LPM_PERIPH_PERMIT_PM1_FUNCS_MAX &&
periph_permit_pm1_funcs[i] != NULL; i++) {
if(!periph_permit_pm1_funcs[i]()) {
return false;
}
}
return true;
}
/*---------------------------------------------------------------------------*/
/*
* Routine to put is in PM0. We also need to do some housekeeping if the stats
* or the energest module is enabled
*/
static void
enter_pm0(void)
{
ENERGEST_SWITCH(ENERGEST_TYPE_CPU, ENERGEST_TYPE_LPM);
/* We are only interested in IRQ energest while idle or in LPM */
ENERGEST_IRQ_RESTORE(irq_energest);
/* Remember the current time so we can keep stats when we wake up */
if(LPM_CONF_STATS) {
sleep_enter_time = RTIMER_NOW();
}
assert_wfi();
/* We reach here when the interrupt context that woke us up has returned */
LPM_STATS_ADD(0, RTIMER_NOW() - sleep_enter_time);
/* Remember IRQ energest for next pass */
ENERGEST_IRQ_SAVE(irq_energest);
ENERGEST_SWITCH(ENERGEST_TYPE_LPM, ENERGEST_TYPE_CPU);
}
/*---------------------------------------------------------------------------*/
static void
select_32_mhz_xosc(void)
{
/* First, make sure there is no ongoing clock source change */
while((REG(SYS_CTRL_CLOCK_STA) & SYS_CTRL_CLOCK_STA_SOURCE_CHANGE) != 0);
/* Turn on the 32 MHz XOSC and source the system clock on it. */
REG(SYS_CTRL_CLOCK_CTRL) &= ~SYS_CTRL_CLOCK_CTRL_OSC;
/* Wait for the switch to take place */
while((REG(SYS_CTRL_CLOCK_STA) & SYS_CTRL_CLOCK_STA_OSC) != 0);
/* Power down the unused oscillator and restore divisors (silicon errata) */
REG(SYS_CTRL_CLOCK_CTRL) = (REG(SYS_CTRL_CLOCK_CTRL)
#if SYS_CTRL_SYS_DIV == SYS_CTRL_CLOCK_CTRL_SYS_DIV_32MHZ
& ~SYS_CTRL_CLOCK_CTRL_SYS_DIV
#endif
#if SYS_CTRL_IO_DIV == SYS_CTRL_CLOCK_CTRL_IO_DIV_32MHZ
& ~SYS_CTRL_CLOCK_CTRL_IO_DIV
#endif
) | SYS_CTRL_CLOCK_CTRL_OSC_PD;
}
/*---------------------------------------------------------------------------*/
static void
select_16_mhz_rcosc(void)
{
/*
* Power up both oscillators in order to speed up the transition to the 32-MHz
* XOSC after wake up. In addition, consider CC2538 silicon errata:
* "Possible Incorrect Value of Clock Dividers after PM2 and PM3" and
* set system clock divisor / I/O clock divisor to 16 MHz in case they run
* at full speed (=32 MHz)
*/
REG(SYS_CTRL_CLOCK_CTRL) = (REG(SYS_CTRL_CLOCK_CTRL)
#if SYS_CTRL_SYS_DIV == SYS_CTRL_CLOCK_CTRL_SYS_DIV_32MHZ
| SYS_CTRL_CLOCK_CTRL_SYS_DIV_16MHZ
#endif
#if SYS_CTRL_IO_DIV == SYS_CTRL_CLOCK_CTRL_IO_DIV_32MHZ
| SYS_CTRL_CLOCK_CTRL_IO_DIV_16MHZ
#endif
) & ~SYS_CTRL_CLOCK_CTRL_OSC_PD;
/*First, make sure there is no ongoing clock source change */
while((REG(SYS_CTRL_CLOCK_STA) & SYS_CTRL_CLOCK_STA_SOURCE_CHANGE) != 0);
/* Set the System Clock to use the 16MHz RC OSC */
REG(SYS_CTRL_CLOCK_CTRL) |= SYS_CTRL_CLOCK_CTRL_OSC;
/* Wait till it's happened */
while((REG(SYS_CTRL_CLOCK_STA) & SYS_CTRL_CLOCK_STA_OSC) == 0);
}
/*---------------------------------------------------------------------------*/
void
lpm_exit()
{
if((REG(SYS_CTRL_PMCTL) & SYS_CTRL_PMCTL_PM3) == SYS_CTRL_PMCTL_PM0) {
/* We either just exited PM0 or we were not sleeping in the first place.
* We don't need to do anything clever */
return;
}
/*
* When returning from PM1/2, the sleep timer value (used by RTIMER_NOW()) is
* not up-to-date until a positive edge on the 32-kHz clock has been detected
* after the system clock restarted. To ensure an updated value is read, wait
* for a positive transition on the 32-kHz clock by polling the
* SYS_CTRL_CLOCK_STA.SYNC_32K bit, before reading the sleep timer value.
*/
while(REG(SYS_CTRL_CLOCK_STA) & SYS_CTRL_CLOCK_STA_SYNC_32K);
while(!(REG(SYS_CTRL_CLOCK_STA) & SYS_CTRL_CLOCK_STA_SYNC_32K));
LPM_STATS_ADD(REG(SYS_CTRL_PMCTL) & SYS_CTRL_PMCTL_PM3,
RTIMER_NOW() - sleep_enter_time);
/* Adjust the system clock, since it was not counting while we were sleeping
* We need to convert sleep duration from rtimer ticks to clock ticks */
clock_adjust();
/* Restore system clock to the 32 MHz XOSC */
select_32_mhz_xosc();
/* Restore PMCTL to PM0 for next pass */
REG(SYS_CTRL_PMCTL) = SYS_CTRL_PMCTL_PM0;
/* Remember IRQ energest for next pass */
ENERGEST_IRQ_SAVE(irq_energest);
ENERGEST_SWITCH(ENERGEST_TYPE_LPM, ENERGEST_TYPE_CPU);
}
/*---------------------------------------------------------------------------*/
void
lpm_enter()
{
rtimer_clock_t lpm_exit_time;
rtimer_clock_t duration;
/*
* If either the RF or the registered peripherals are on, dropping to PM1/2
* would equal pulling the rug (32MHz XOSC) from under their feet. Thus, we
* only drop to PM0. PM0 is also used if max_pm==0.
*/
if((REG(RFCORE_XREG_FSMSTAT0) & RFCORE_XREG_FSMSTAT0_FSM_FFCTRL_STATE) != 0
|| !periph_permit_pm1() || max_pm == 0) {
enter_pm0();
/* We reach here when the interrupt context that woke us up has returned */
return;
}
/*
* Registered peripherals were off. Radio was off: Some Duty Cycling in place.
* rtimers run on the Sleep Timer. Thus, if we have a scheduled rtimer
* task, a Sleep Timer interrupt will fire and will wake us up.
* Choose the most suitable PM based on anticipated deep sleep duration
*/
lpm_exit_time = rtimer_arch_next_trigger();
duration = lpm_exit_time - RTIMER_NOW();
if(duration < DEEP_SLEEP_PM1_THRESHOLD || lpm_exit_time == 0) {
/* Anticipated duration too short or no scheduled rtimer task. Use PM0 */
enter_pm0();
/* We reach here when the interrupt context that woke us up has returned */
return;
}
/* If we reach here, we -may- (but may as well not) be dropping to PM1+. We
* know the registered peripherals and RF are off so we can switch to the
* 16MHz RCOSC. */
select_16_mhz_rcosc();
/*
* Switching the System Clock from the 32MHz XOSC to the 16MHz RC OSC may
* have taken a while. Re-estimate sleep duration.
*/
duration = lpm_exit_time - RTIMER_NOW();
if(duration < DEEP_SLEEP_PM1_THRESHOLD) {
/*
* oops... The clock switch took some time and now the remaining sleep
* duration is too short. Restore the clock source to the 32MHz XOSC and
* abort the LPM attempt altogether. We can't drop to PM0,
* we need to yield to main() since we may have events to service now.
*/
select_32_mhz_xosc();
return;
} else if(duration >= DEEP_SLEEP_PM2_THRESHOLD && max_pm == 2) {
/* Long sleep duration and PM2 is allowed. Use it */
REG(SYS_CTRL_PMCTL) = SYS_CTRL_PMCTL_PM2;
} else {
/*
* Anticipated duration too short for PM2 but long enough for PM1 and we
* are allowed to use PM1
*/
REG(SYS_CTRL_PMCTL) = SYS_CTRL_PMCTL_PM1;
}
/* We are only interested in IRQ energest while idle or in LPM */
ENERGEST_IRQ_RESTORE(irq_energest);
ENERGEST_SWITCH(ENERGEST_TYPE_CPU, ENERGEST_TYPE_LPM);
/* Remember the current time so we can keep stats when we wake up */
if(LPM_CONF_STATS) {
sleep_enter_time = RTIMER_NOW();
}
/*
* Last chance to abort entering Deep Sleep.
*
* - There is the slight off-chance that a SysTick interrupt fired while we
* were trying to make up our mind. This may have raised an event.
* - The Sleep Timer may have fired
*
* Check if there is still a scheduled rtimer task and check for pending
* events before going to Deep Sleep
*/
if(process_nevents() || rtimer_arch_next_trigger() == 0) {
/* Event flag raised or rtimer inactive.
* Turn on the 32MHz XOSC, restore PMCTL and abort */
select_32_mhz_xosc();
REG(SYS_CTRL_PMCTL) = SYS_CTRL_PMCTL_PM0;
/* Remember IRQ energest for next pass */
ENERGEST_IRQ_SAVE(irq_energest);
ENERGEST_SWITCH(ENERGEST_TYPE_LPM, ENERGEST_TYPE_CPU);
} else {
/* All clear. Assert WFI and drop to PM1/2. This is now un-interruptible */
assert_wfi();
}
/*
* We reach here after coming back from PM1/2. The interrupt context that
* woke us up has returned. lpm_exit() has run, it has switched the system
* clock source back to the 32MHz XOSC, it has adjusted the system clock,
* it has restored PMCTL and it has done energest housekeeping
*/
return;
}
/*---------------------------------------------------------------------------*/
void
lpm_set_max_pm(uint8_t pm)
{
max_pm = pm > LPM_CONF_MAX_PM ? LPM_CONF_MAX_PM : pm;
}
/*---------------------------------------------------------------------------*/
void
lpm_register_peripheral(lpm_periph_permit_pm1_func_t permit_pm1_func)
{
int i;
for(i = 0; i < LPM_PERIPH_PERMIT_PM1_FUNCS_MAX; i++) {
if(periph_permit_pm1_funcs[i] == permit_pm1_func) {
break;
} else if(periph_permit_pm1_funcs[i] == NULL) {
periph_permit_pm1_funcs[i] = permit_pm1_func;
break;
}
}
}
/*---------------------------------------------------------------------------*/
void
lpm_init()
{
/*
* The main loop calls lpm_enter() when we have no more events to service.
* By default, we will enter PM0 unless lpm_enter() decides otherwise
*/
REG(SYS_CTRL_PMCTL) = SYS_CTRL_PMCTL_PM0;
REG(SCB_SYSCTRL) |= SCB_SYSCTRL_SLEEPDEEP;
max_pm = LPM_CONF_MAX_PM;
LPM_STATS_INIT();
}
/*---------------------------------------------------------------------------*/
#endif /* LPM_CONF_ENABLE != 0 */
/** @} */