/* * 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/rfcore-xreg.h" #include "rtimer-arch.h" #include "lpm.h" #include "cc2538_cm3.h" #include "reg.h" #include #include #include #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 5 #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; SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk; max_pm = LPM_CONF_MAX_PM; LPM_STATS_INIT(); } /*---------------------------------------------------------------------------*/ #endif /* LPM_CONF_ENABLE != 0 */ /** @} */