osd-contiki/cpu/cc2538/lpm.c
Benoît Thébaudeau 5261bb861d cc2538: lpm: Fix RTIMER_NOW() upon wake-up
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.

Because of this RTIMER_NOW() fixup, lpm_exit() has to be called at the very
beginning of ISRs waking up the SoC. This also ensures that all clocks and
timers are enabled at the correct frequency and updated before using them
following wake-up.

Without this fix, etimers could sometimes (randomly, depending on timings)
become ultra slow (observed from 10x to 40x slower than normal) if the system
exited PM1/2 very often. This issue occurred more often with PM1.

Signed-off-by: Benoît Thébaudeau <benoit.thebaudeau@advansee.com>
2013-12-05 20:23:29 +01:00

381 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 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 PM1/2 to adjust the system clock, which
* stops ticking while in those PMs
*/
static rtimer_clock_t sleep_enter_time;
#define RTIMER_CLOCK_TICK_RATIO (RTIMER_SECOND / CLOCK_SECOND)
void clock_adjust(clock_time_t ticks);
/*---------------------------------------------------------------------------*/
/* 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 2
#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_OFF(ENERGEST_TYPE_CPU);
ENERGEST_ON(ENERGEST_TYPE_LPM);
/* We are only interested in IRQ energest while idle or in LPM */
ENERGEST_IRQ_RESTORE(irq_energest);
/*
* After PM0 we don't need to adjust the system clock. Thus, saving the time
* we enter Deep Sleep is only required if we are keeping stats.
*/
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_ON(ENERGEST_TYPE_CPU);
ENERGEST_OFF(ENERGEST_TYPE_LPM);
}
/*---------------------------------------------------------------------------*/
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. */
REG(SYS_CTRL_CLOCK_CTRL) |= 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.
*/
REG(SYS_CTRL_CLOCK_CTRL) &= ~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 and
* this will cost us some accuracy */
clock_adjust((clock_time_t)
((RTIMER_NOW() - sleep_enter_time) / RTIMER_CLOCK_TICK_RATIO));
/* 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_ON(ENERGEST_TYPE_CPU);
ENERGEST_OFF(ENERGEST_TYPE_LPM);
}
/*---------------------------------------------------------------------------*/
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_OFF(ENERGEST_TYPE_CPU);
ENERGEST_ON(ENERGEST_TYPE_LPM);
/* Remember the current time so we can adjust the clock when we wake up */
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_ON(ENERGEST_TYPE_CPU);
ENERGEST_OFF(ENERGEST_TYPE_LPM);
} 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();
}
/*---------------------------------------------------------------------------*/
/** @} */