This post is older than 2 years and might not be relevant anymore
More Info: Consider searching for newer posts

App Timer (RTC1) drift

Hi, we have the need to develop an application that needs a precise second counter over time.

Our firmware is using the S110 SoftDevice (v8.0.0) and SDK v8.0.0 to perform scannable advertising (the second counter value is sent in the advertising data along with other custom data).

The firmware is loaded on a custom NRF51822 board that uses an external 20PPM LFCLK oscillator.

The SoftDevice is initialized in the main.c with this code:

static void ble_stack_init(void)
{
    uint32_t err_code;

    // Initialize the SoftDevice handler module.
    SOFTDEVICE_HANDLER_INIT(NRF_CLOCK_LFCLKSRC_XTAL_20_PPM, NULL);

    // Enable BLE stack 
    ble_enable_params_t ble_enable_params;
    memset(&ble_enable_params, 0, sizeof(ble_enable_params));
    err_code = sd_ble_enable(&ble_enable_params);
    APP_ERROR_CHECK(err_code);

    // Register with the SoftDevice handler module for BLE events.
    err_code = softdevice_ble_evt_handler_set(ble_evt_dispatch);
    APP_ERROR_CHECK(err_code);

    // Register with the SoftDevice handler module for SYS events.
    err_code = softdevice_sys_evt_handler_set(sys_evt_dispatch);
    APP_ERROR_CHECK(err_code);
}

The code that performs the seconds counting has been taken from the Nordic Eddystone firmware and is the following:

static void update_timestamp(uint32_t *p_sec_cnt)
{
    static uint32_t previous_tick = 0;
    static uint32_t ms_remainder = 0;
    static uint32_t ns_remainder = 0;
    static uint32_t le_time = 0; //Little endian time
    uint32_t        be_time = 0; //Big endian
    uint32_t        current_tick = 0;
    uint32_t        tick_diff;
    uint32_t        ms = 0;


    APP_ERROR_CHECK(app_timer_cnt_get(&current_tick));

    if (current_tick > previous_tick)
    {
        //ticks since last update
        tick_diff = current_tick - previous_tick;
    }
    else if (current_tick < previous_tick)
    {
        //RTC counter has overflown
        tick_diff = current_tick + (RTC1_TICKS_MAX - previous_tick);
    }

    uint32_t ns;
    const uint32_t NS_PER_MS = 1000000;

    ns = tick_diff*NS_PER_TICK;
    ms = ns/NS_PER_MS;
    ns_remainder += ns % NS_PER_MS;

    if (ns_remainder >= NS_PER_MS)
    {
        ms += ns_remainder/NS_PER_MS;
        ns_remainder = ns_remainder % NS_PER_MS;
    }

    const uint32_t RESOLUTION = 1000;
    //The spec requires 1s resolution, so only update the time when ms > 1000 ms
    if (ms >= RESOLUTION)
    {
        ms_remainder += ms % RESOLUTION;
        if (ms_remainder >= RESOLUTION)
        {
            ms += ms_remainder/RESOLUTION;
            ms_remainder = ms_remainder % RESOLUTION;
        }
        //For very first function call, ms will be >> 1000, since previous_tick = 0. 
        //So only increment the time by 1.
        if (le_time == 0)
        {
            le_time++;
        }
        else
        {
            le_time += ((ms)/RESOLUTION);
        }
        be_time = BYTES_REVERSE_32BIT(le_time);
        // Write second counter in function parameter
    *p_sec_cnt = be_time;
    }
    else
    {
        ms_remainder += ms;
    }
    previous_tick = current_tick;
}

And the app_timer library is initialized in the main.c using:

APP_TIMER_APPSH_INIT(APP_TIMER_PRESCALER, APP_TIMER_MAX_TIMERS, APP_TIMER_OP_QUEUE_SIZE, true);

and

#define APP_TIMER_PRESCALER          0
#define APP_TIMER_MAX_TIMERS         8
#define APP_TIMER_OP_QUEUE_SIZE      16

Our firmware starts advertising with a timeout of 1s and the advertising data are updated each time with the current value of the second counter (in this way I can see the increment of the second counter at each second on the advertising data using a BLE-enabled phone).

static void advertising_start(bool restart)
{
    uint32_t err_code;
    ble_gap_adv_params_t adv_params;
    uint32_t sec_counter;
    ble_advdata_t adv;
    uint8_t adv_data[ADV_SIZE];
    ble_advdata_t sr;
    uint8_t sr_data[SR_SIZE];

    ...

    // Set advertising parameters
    memset(&adv_params, 0, sizeof(adv_params));
    adv_params.type = BLE_GAP_ADV_TYPE_ADV_IND;
    adv_params.p_peer_addr = NULL;
    adv_params.fp = BLE_GAP_ADV_FP_ANY;
    adv_params.interval = MSEC_TO_UNITS(ADV_INTERVAL, UNIT_0_625_MS);
    adv_params.timeout  = 1;
    

    // Get the current second timestamp value
    update_timestamp(&sec_counter);

    // Set the second timestamp value in the scan response data 
    memcpy(&sr_data[idx], &sec_counter, sizeof(uint32_t));

    ...

    // Set the advertising and scan response data
    err_code = ble_advdata_set(&adv, &sr);
    APP_ERROR_CHECK(err_code);

    // Start Advertising
    err_code = sd_ble_gap_adv_start(&adv_params);
    APP_ERROR_CHECK(err_code);
}

The problem we are currently experiencing is that the seconds timekeeping is drifting and in 3 days we have detected around 5 minutes of drift, and this is too big for a 20PPM oscillator.

What could be the cause of this drift? Are there any others SoftDevice settings that I could try to improve the performance of the RTC1 timekeeping?

  • My first thought would be that there is some inaccuracy in your Xtal circuit design. Have you measures your PCB.s parasitic capacitance and factored it into the tuning capacitor values?

  • Yes, we have checked the circuit but it seems everything ok... In the Nordic Eddystone firmware, I have found this piece of code inside a timer handler function:

    ...
    // For every 1 second interrupt, there is 30 us delay with timer prescaler set at 0.
    us_delay += 30;
    
    if (APP_TIMER_PRESCALER != 0)
    {
        //If the prescaler is not 0, then a new us_delay increment needs to be calculated...
        //Trigger a run time error here to prevent developers from blindly changing the prescaler
        APP_ERROR_CHECK(NRF_ERROR_INVALID_PARAM);
    }
    

    From what I understand from the code, it seems that the timer handler looses 30us each time is called (with a PRESCALER set to 0), but I'm not able to find on the documentation anything regarding this...

  • 5 minutes over 3 days is about 5/(3 * 24 * 60) = 1157ppm, which is way too high. Is the clock slower or faster than the real time, and is it always like this?

    Do you always have a timer running? The app_timer module will stop the RTC if no timers are running.

  • 30 us is about the resolution when prescaler is 0 (1/32768Hz = 30.5us). I don't know why this is added, but you don't have to worry about this. You are reading the value of the RTC (app_timer_cnt_get(&current_tick)) directly, not assuming that a callback was called at exactly one second interval.

  • Sorry, in the meanwhile I have also coded a version of the firmware that uses a 1s timer (using the app_timer library), and it seems that the accuracy is better than the firmware that reads the RTC counter directly (but I can't explain why)...

    In the firmware there are other timers (always in the range of seconds, not us nor ms timers), so I believe the RTC is kept active from the SoftDevice.

    Yes, with a PRESCALER set to 0 the tick interval is around 30.5us, but I can't explain why the timer loose 1 tick every second... it is documented somewhere in the docs? If I change the PRESCALER to 4095 (125ms tick interval), will the timer loose 125ms every second?

Related