cc2538: clock: Fix time drift occurring with PM1/2

The clock adjustments made when waking up from PM1/2 were very inaccurate. If
relying on ContikiMAC's rtimer to sleep, this led to Contiki's software clock
time, seconds and etimers to be 2.5 s slower after each min, i.e. 1 hour slower
after each day, which is a show stopper issue for most real-life applications.

This was caused by a lack of accuracy in several pieces of code during sleep
entry and wake-up:
 - It was difficult to synchronize the calls to RTIMER_NOW() before and after
   sleep with the deactivation and activation of the SysTick peripheral caused
   by PM1/2. This caused an inaccuracy in the corrective number of ticks passed
   to clock_adjust().
 - The value passed to clock_adjust() was truncated from an rtimer_clock_t
   value, but the accumulated error caused by these truncated bits was ignored.
 - The SysTick peripheral had to be stopped during the call to clock_adjust().

Rather than creating even more complicated clock adjustment mechanisms that
would probably still have mixed results as to accuracy, this change simply uses
the Sleep Timer counter as a base value for Contiki's clock and seconds
counters. The tick from the Systick peripheral is still used as the interrupt
source to update Contiki's clocks and timers. When running, the SysTick
peripheral and the Sleep Timer are synchronized, so combining both is not an
issue, and this allows not to alter the rtimer interrupt mechanism using the
Sleep Timer. The purpose of the Sleep Timer is to be an RTC, so it is the
perfect fit for the clock module, all the more it can not be disturbed by PM1/2.
If the 32-kHz XOSC is used, the Sleep Timer is also very accurate. If the
32-kHZ RCOSC is used, it is calibrated from the 32-MHz XOSC, so it is also
accurate, and the 32753-Hz vs. 32768-Hz systematic error in that case is
negligible, all the more one would use the 32-kHz XOSC for better accuracy.

Besides fixing this time drift issue, this change has several benefits:
 - clock_time(), clock_seconds() and RTIMER_NOW() start synchronized, and they
   change at the same source pace.
 - If clock_set_seconds() is called, then clock_seconds() indicates one more
   second almost exactly one second later, then exactly each second. Before this
   change, clock_seconds() was not synchronized with clock_set_seconds(), so the
   value returned by the former could be incremented immediately after the call
   to the latter in some cases.
 - The code tied to the clock module is simpler and more robust.

Signed-off-by: Benoît Thébaudeau <benoit.thebaudeau@advansee.com>
This commit is contained in:
Benoît Thébaudeau 2013-12-04 16:17:14 +01:00
parent b7515004da
commit d86b8275ec
2 changed files with 74 additions and 64 deletions

View file

@ -38,7 +38,11 @@
*
* To implement the clock functionality, we use the SysTick peripheral on the
* cortex-M3. We run the system clock at 16 MHz and we set the SysTick to give
* us 128 interrupts / sec
* us 128 interrupts / sec. However, the Sleep Timer counter value is used for
* the number of elapsed ticks in order to avoid a significant time drift caused
* by PM1/2. Contrary to the Sleep Timer, the SysTick peripheral is indeed
* frozen during PM1/2, so adjusting upon wake-up a tick counter based on this
* peripheral would hardly be accurate.
* @{
*
* \file
@ -52,14 +56,15 @@
#include "dev/sys-ctrl.h"
#include "sys/energest.h"
#include "sys/etimer.h"
#include "sys/rtimer.h"
#include <stdint.h>
/*---------------------------------------------------------------------------*/
#define RTIMER_CLOCK_TICK_RATIO (RTIMER_SECOND / CLOCK_SECOND)
#define RELOAD_VALUE (125000 - 1) /** Fire 128 times / sec */
static volatile clock_time_t count;
static volatile unsigned long secs = 0;
static volatile uint8_t second_countdown = CLOCK_SECOND;
static volatile uint64_t rt_ticks_startup = 0, rt_ticks_epoch = 0;
/*---------------------------------------------------------------------------*/
/**
* \brief Arch-specific implementation of clock_init for the cc2538
@ -75,8 +80,6 @@ static volatile uint8_t second_countdown = CLOCK_SECOND;
void
clock_init(void)
{
count = 0;
REG(SYSTICK_STRELOAD) = RELOAD_VALUE;
/* System clock source, Enable */
@ -109,19 +112,19 @@ clock_init(void)
CCIF clock_time_t
clock_time(void)
{
return count;
return rt_ticks_startup / RTIMER_CLOCK_TICK_RATIO;
}
/*---------------------------------------------------------------------------*/
void
clock_set_seconds(unsigned long sec)
{
secs = sec;
rt_ticks_epoch = (uint64_t)sec * RTIMER_SECOND;
}
/*---------------------------------------------------------------------------*/
CCIF unsigned long
clock_seconds(void)
{
return secs;
return rt_ticks_epoch / RTIMER_SECOND;
}
/*---------------------------------------------------------------------------*/
void
@ -160,43 +163,40 @@ clock_delay(unsigned int i)
}
/*---------------------------------------------------------------------------*/
/**
* \brief Adjust the clock by moving it forward by a number of ticks
* \param ticks The number of ticks
* \brief Update the software clock ticks and seconds
*
* This function is useful when coming out of PM1/2, during which the system
* clock is stopped. We adjust the clock by moving it forward by a number of
* ticks equal to the deep sleep duration. We update the seconds counter if
* we have to and we also do some housekeeping so that the next second will
* increment when it is meant to.
* This function is used to update the software tick counters whenever the
* system clock might have changed, which can occur upon a SysTick ISR or upon
* wake-up from PM1/2.
*
* \note This function is only meant to be used by lpm_exit(). Applications
* should really avoid calling this
* For the software clock ticks counter, the Sleep Timer counter value is used
* as the base tick value, and extended to a 64-bit value thanks to a detection
* of wraparounds.
*
* For the seconds counter, the changes of the Sleep Timer counter value are
* added to the reference time, which is either the startup time or the value
* passed to clock_set_seconds().
*
* This function polls the etimer process if an etimer has expired.
*/
void
clock_adjust(clock_time_t ticks)
static void
update_ticks(void)
{
/* Halt the SysTick while adjusting */
REG(SYSTICK_STCTRL) &= ~SYSTICK_STCTRL_ENABLE;
rtimer_clock_t now;
uint64_t prev_rt_ticks_startup, cur_rt_ticks_startup;
uint32_t cur_rt_ticks_startup_hi;
/* Moving forward by more than a second? */
secs += ticks >> 7;
now = RTIMER_NOW();
prev_rt_ticks_startup = rt_ticks_startup;
/* Increment tick count */
count += ticks;
/*
* Update internal second countdown so that next second change will actually
* happen when it's meant to happen.
*/
second_countdown -= ticks & 127;
if(second_countdown == 0 || second_countdown > 128) {
secs++;
second_countdown -= 128;
cur_rt_ticks_startup_hi = prev_rt_ticks_startup >> 32;
if(now < (rtimer_clock_t)prev_rt_ticks_startup) {
cur_rt_ticks_startup_hi++;
}
cur_rt_ticks_startup = (uint64_t)cur_rt_ticks_startup_hi << 32 | now;
rt_ticks_startup = cur_rt_ticks_startup;
/* Re-Start the SysTick */
REG(SYSTICK_STCTRL) |= SYSTICK_STCTRL_ENABLE;
rt_ticks_epoch += cur_rt_ticks_startup - prev_rt_ticks_startup;
/*
* Inform the etimer library that the system clock has changed and that an
@ -208,23 +208,39 @@ clock_adjust(clock_time_t ticks)
}
/*---------------------------------------------------------------------------*/
/**
* \brief The clock Interrupt Service Routine. It polls the etimer process
* if an etimer has expired. It also updates the software clock tick and
* seconds counter since reset.
* \brief Adjust the clock following missed SysTick ISRs
*
* This function is useful when coming out of PM1/2, during which the system
* clock is stopped. We adjust the clock counters like after any SysTick ISR.
*
* \note This function is only meant to be used by lpm_exit(). Applications
* should really avoid calling this
*/
void
clock_adjust(void)
{
/* Halt the SysTick while adjusting */
REG(SYSTICK_STCTRL) &= ~SYSTICK_STCTRL_ENABLE;
update_ticks();
/* Re-Start the SysTick */
REG(SYSTICK_STCTRL) |= SYSTICK_STCTRL_ENABLE;
}
/*---------------------------------------------------------------------------*/
/**
* \brief The clock Interrupt Service Routine
*
* It polls the etimer process if an etimer has expired. It also updates the
* software clock tick and seconds counter.
*/
void
clock_isr(void)
{
ENERGEST_ON(ENERGEST_TYPE_IRQ);
count++;
if(etimer_pending()) {
etimer_request_poll();
}
if(--second_countdown == 0) {
secs++;
second_countdown = CLOCK_SECOND;
}
update_ticks();
ENERGEST_OFF(ENERGEST_TYPE_IRQ);
}
/*---------------------------------------------------------------------------*/

View file

@ -86,14 +86,11 @@ rtimer_clock_t lpm_stats[3];
/*---------------------------------------------------------------------------*/
/*
* 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
* This is used when coming out of PM0/1/2 to keep stats
*/
static rtimer_clock_t sleep_enter_time;
#define RTIMER_CLOCK_TICK_RATIO (RTIMER_SECOND / CLOCK_SECOND)
void clock_adjust(clock_time_t ticks);
void clock_adjust(void);
/*---------------------------------------------------------------------------*/
/* Stores the currently specified MAX allowed PM */
static uint8_t max_pm;
@ -135,10 +132,7 @@ enter_pm0(void)
/* 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.
*/
/* Remember the current time so we can keep stats when we wake up */
if(LPM_CONF_STATS) {
sleep_enter_time = RTIMER_NOW();
}
@ -213,10 +207,8 @@ lpm_exit()
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));
* 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();
@ -304,8 +296,10 @@ lpm_enter()
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();
/* 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.