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

How to access the TIMER3 in wireless timer sync?

Hello,

I found this great blog entry about synchronizing timers on different devices: https://devzone.nordicsemi.com/nordic/short-range-guides/b/bluetooth-low-energy/posts/wireless-timer-synchronization-among-nrf5-devices

It works just fine on a nRF52840DK and a nRF52DK. Timers on multiple nRF5 devices are synced by one device (master) sending sync packets containing captured timer values to other devices (nodes). (TIMER3 is free running, TIMER2 counts TIMER3 overflows).

At the moment I am struggeling to access this synchronized TIMER3 on the two boards. I am familiar with setting up a TIMER without wireless timer sync. 

So here are my questions:

1. How can I define CC, Prescaler, Mode, Bitmode, the interrupt handler and so forth when using wireless timer sync?

2. Can anybody provide me some advice how to access this synchronized TIMER3 properly? 

Thank you very much in advance.

Parents
  • Hello,

    can you share some more details regarding your use case? The following suggestions may be helpful for your case, otherwise we can see what options there are for you needs.

    If you want to get a timestamp value, you can use the timestamp function (either 32 or 64-bit version): https://github.com/nordic-auko/nRF5-ble-timesync-demo/blob/master/nRF5_SDK_16.0.0_98a08e2/examples/common/time_sync.h#L203

    If you want to trigger something at a given time, the ts_set_trigger function might be helpful. This lets you set up a PPI to be triggered at a given time in the future.

    There are two TIMERs involved in this code, TIMER2 is used in timer mode, while TIMER3 is used in counter mode. By default, TIMER2 has a quite short CC/reset of 40000 (=40000/16 MHz = 2.5 ms wraparound). The counter is incremented for every wraparound (2.5 ms). This means that the timestamp function uses both of these TIMER values to calculate the current time with 16 MHz tick resolution.

    Note that the ts_set_trigger function operates on the counter (TIMER3), which by default has a resolution of 2.5 ms. If you need finer resolution, you could reduce the TIME_SYNC_TIMER_MAX_VAL value.

    Prescaler, bitmode, etc. is currently hardcoded. I would generally not recommend making changes to those.

    Accessing the TIMERs directly is also not something I recommend, because it is quite tricky to handle the corner cases of TIMER wraparound and adjustment

  • Hello Audun,

    First of all, thank you so much for your reply and your great blog entry about wireless timer sync. Of course, I will check your suggestions out.

    Here is my use case:

    I have two development kits. A nRF52840DK (central) and a nRF52DK (peripheral). I flashed the provided "wireless timer sync" code from github on them. It works just fine.


    My goal is to have a similar counter on the central and the peripheral that increments every 1ms. The counter on central and peripheral should always contain the same value.

    I already found this (https://devzone.nordicsemi.com/f/nordic-q-a/53521/two-timers-with-same-value). I am not sure, but I think there was someone trying to do the same.

    What is the smartest way to achieve this goal?

    I appreciate any suggestions.

    Thank you very much in advance.

    Michael

  • Hi Michael,

    thanks for your kind words! Always happy to hear the blog post and code is being read and used.

    Regarding the 1 ms counter in your use case, what will you do with this counter? I'm trying to understand if the timestamp or trigger functions in the current version of time_sync.h can support the functionality you are looking for.

    TIMER3 does almost cover the 1 ms timer function you are describing. There are two exceptions:

    1) The counter is currently running at 2.5ms, but this should be easy to adjust by setting TIME_SYNC_TIMER_MAX_VAL  to 16000 instead of 40000

    2) Although the TIMER3 counting rate will happen in a synchronized fashion on the Central and Peripheral, there will be an offset between the two counters. This offset is handled in software and accounted for when you use the timestamp and trigger functions, but not if you were to read out the NRF_TIMER3 registers directly.

    If you wanted to, for example toggle a GPIO for every 1 ms tick, this can be achieved by changing the TIME_SYNC_TIMER_MAX_VAL  as described, and using the ts_set_trigger() function. Similar to what is done here

    Hope this helps!

    Audun

Reply
  • Hi Michael,

    thanks for your kind words! Always happy to hear the blog post and code is being read and used.

    Regarding the 1 ms counter in your use case, what will you do with this counter? I'm trying to understand if the timestamp or trigger functions in the current version of time_sync.h can support the functionality you are looking for.

    TIMER3 does almost cover the 1 ms timer function you are describing. There are two exceptions:

    1) The counter is currently running at 2.5ms, but this should be easy to adjust by setting TIME_SYNC_TIMER_MAX_VAL  to 16000 instead of 40000

    2) Although the TIMER3 counting rate will happen in a synchronized fashion on the Central and Peripheral, there will be an offset between the two counters. This offset is handled in software and accounted for when you use the timestamp and trigger functions, but not if you were to read out the NRF_TIMER3 registers directly.

    If you wanted to, for example toggle a GPIO for every 1 ms tick, this can be achieved by changing the TIME_SYNC_TIMER_MAX_VAL  as described, and using the ts_set_trigger() function. Similar to what is done here

    Hope this helps!

    Audun

Children
  • Hello Audun,

    thank you for your reply.

    Regarding the 1 ms counter in your use case, what will you do with this counter?

    I use the SAADC on the peripheral. The SAADC readings are sent from the peripheral to the central via NUS. Each NUS-paket gets a timestamp. The timestamp is the actual value of the 1ms counter.

    On the central, I read out two GPIOs as digital inputs and save them in an array. So in my application the SAADC on the peripheral samples at the same time as the central reads out the digital inputs.

    With the synchronized 1ms counter, I can restore the incoming NUS-pakets to the readings of the digital inputs. (Because incoming NUS-pakets are not time consistent)

    If you wanted to, for example toggle a GPIO for every 1 ms tick

    So I do not need to toggle a GPIO with a 1ms tick (But it would work if I change TIME_SYNC_TIMER_MAX_VAL to 16000).

    Instead I need to access this synchronized 1 ms counter in my main.c on my central and my peripheral.

    A offset up to +-0,5ms between the the counter on central and peripheral is okey. But the two counters should not start to drift away from each other.

    In conclusion, I need a 1ms counter on central and peripheral with the same value, and which are accessible in my main.c.

    Do you have any suggestions how to implement this properly?

    Thank you very much in advance.

  • Thanks for the details!

    It sounds like the timestamp functions would cover your need.

    When the time_sync is enabled, ts_timestamp_get_ticks() operates on the shared time base. If you call this function at exactly the same time on both peripheral and central, they should return the same value. (or, in reality almost the same value as the clocks are always drifting).

    Specifically, on the peripheral side call ts_timestamp_get_ticks() either when you make the SAADC measurement, or right before you call the NUS send() function. If you want millisecond resolution, you should round the timestamp value to the closest milliseconds (from the 16 MHz tick format). Include the rounded value in the NUS packet.

    On the Central side, call ts_timestamp_get_ticks() when you read the GPIOs, round the timestamp to the closest millisecond, and store in the array.

    When the Central receives the NUS packets, it can iterate through the array to figure out what the GPIO state was at the time of the NUS packet timestamp.

    You wont have to make any changes to TIME_SYNC_TIMER_MAX_VAL with this approach.

    Does this make sense?

    To achieve millisecond synchronization accuracy you could use a fairly slow sync packet transmit rate (1 Hz is probably more than enough). For example with the 10 ppm crystals on the Devkit the clocks should not drift more than 2 x 10 microseconds per second.

    Also, I wanted to mention that you might get the desired accuracy by simply keeping track of the BLE connection events. BLE is a synchronized protocol, where the Central always dictates the timing.

    Using native BLE connection event counting would save you some power, if this is a concern. Specifically, one can use the radio notification functionality to trigger an interrupt in the application at a fixed offset from every connection event. For example, with a 10 ms connection interval, you would get an interrupt every 10 ms. By keeping track of the number of these interrupts from the beginning of the BLE connection, and starting a timer in the interrupt, you can calculate the timestamp by n * 10 ms + timer value, where n is the connection event count.

    Audun

  • Hello Audun,

    thank you very much for your detailed answer.

    You already helped me a lot.

    Can you please answer my following questions:

    1. The ts_timestamp_get_ticks() function ist defined as 

    uint32_t ts_timestamp_get_ticks_u32(uint8_t ppi_chn);

    At the moment I call this function with ppi_chn = 0. With time_sync activated the return values on central and peripheral look equal. Is it correct to use ppi_chn = 0?

    2. Maybe a silly question, but I still do not really know how to handle the overflow when 2^32 is reached (After ~268s). I need a counter that runs up to 8 hours without overflow. Do you have any suggestions how to handle this?

    3. Where can I configure the sync paket transmit rate? (1Hz or lower would be enough)

    you can calculate the timestamp by n * 10 ms + timer value, where n is the connection event count.

    I will give that a try too. If I do this on peripheral and central, I should get also a synchronized time base? I already worked with radio_notifications and they can be a powerful tool.

    However, in the near future, there will be up to 4 peripherals and one central in this project. So I think the wireless timer sync is the approach, which is more reliable and futureproof. 

    4. Do you have any information or experience when the wireless timer sync is used with one central and more than one peripheral?

    Thank you very much in advance.

    Michael

  • No problem!

    I'll try to answer these questions:

    1) I recommend using the latest version of the time_sync code. Among other improvements, the timestamp functions are updated to not have this parameter: https://github.com/nordic-auko/nRF5-ble-timesync-demo/blob/master/nRF5_SDK_16.0.0_98a08e2/examples/common/time_sync.h#L216 . If not, you can use the nrfx_ppi_channel_alloc() function to allocate an unused PPI channel, and use this channel when calling the old version of the timestamp function.

    2) It would be easiest to use the 64-bit version of the timestamp function in this case, I think. This way you wont have to count overflows. Something along the lines of:

    uint64_t timestamp_ticks;
    uint32_t timestamp_ms;
    
    timestamp_ticks = ts_timestamp_get_ticks_u64();
    timestamp_ms = (uint32_t) ROUNDED_DIV(timestamp_ticks, 16000);

    3) Use ts_tx_start(1) to set the lowest possible rate. As this API is in Hz, you cannot go below 1 Hz here

    Regarding using the connection event to calculate a shared time, this does get more complicated with multiple connections. I was thinking maybe this function might be more useful than the radio notifications: 

    https://infocenter.nordicsemi.com/topic/com.nordic.infocenter.s140.api.v7.2.0/group___b_l_e___g_a_p___f_u_n_c_t_i_o_n_s.html?cp=4_7_4_1_2_1_3_10#ga6a2d8c4cd863ac538eb0a51d594e0dd6

    You could have the softdevice trigger a timer capture for each link.

    Either way, the time_sync code should work fine too. In this topology you can have any number of timing receivers (the receivers only listen, they never reply to the timing transmitter).

    4) You can have any number of receivers with the time_sync code. The transmitter will transmit at the rate you start it with, and the receivers will listen for the sync packets from the transmitter. Note that in its current version the receivers will run the radio as much as possible in receive mode, which is not great for power consumption. An improved implementation would have the receivers only turn on the radio when it expects the transmitter to send a packet.

  • Hello Audun,

    thank you for your great answer. Sorry for my late reply, I was out of office.

    I continued my work on this and I figured something weird out.

    I tried to add a continuous data transmission to the ble_app_uart example of the timesync-demo. To achieve this I implemented a timer which calls the ble_nus_data_send function every 40ms in its timer handler. So I added the following code to the ble_app_uart example: 

    static const nrfx_timer_t   m_timer = NRFX_TIMER_INSTANCE(4);
    
    static uint32_t counter = 0;
    static uint8_t array[200];
    
    void timer_handler(nrf_timer_event_t event_type, void* p_context)
    {
        ret_code_t err_code;
        counter++;
        uint16_t length_array = 200;
    
        err_code = ble_nus_data_send(&m_nus, array, &length_array, m_conn_handle);
        if ((err_code != NRF_ERROR_INVALID_STATE) &&
            (err_code != NRF_ERROR_RESOURCES) &&
            (err_code != NRF_ERROR_NOT_FOUND))
        {
            APP_ERROR_CHECK(err_code);
        }
    }
    
    void event_timer_init(void)
    {
        ret_code_t err_code;
        
        nrfx_timer_config_t timer_config = NRFX_TIMER_DEFAULT_CONFIG;
        timer_config.frequency = NRF_TIMER_FREQ_125kHz;
        err_code = nrfx_timer_init(&m_timer, &timer_config, timer_handler);
        APP_ERROR_CHECK(err_code);
    
        uint32_t ticks = nrfx_timer_ms_to_ticks(&m_timer,40);
        nrfx_timer_extended_compare(&m_timer, NRF_TIMER_CC_CHANNEL0, ticks, NRF_TIMER_SHORT_COMPARE0_CLEAR_MASK, true);
        nrfx_timer_enable(&m_timer);
    }
    
    void init_array (void)
    {
        int i;
        for(i = 0; i < 200; i++)
        {
            array[i] = 33;
        }
    }
    
    int main(void)
    {
        ...
        event_timer_init();
        init_array();
        ...
    }

    I use TIMER4. 

    Here is my problem: The timer handler (and the ble_nus_data_send() function) gets called every ~12s instead of every 40ms.

    If I add the code above into the normal ble_app_uart example without timer sync it works like it should.

    Here are my questions:

    1. Do you know what causes this weird behaviour?

    2. Is TIMER4 available in general? (TIMER0 is Softdevice, TIMER2 and TIMER3 for timesync)

    3. Can you please provide me some proper guidance how to fix this issue?

    Thank you very much in advance.

    #Update:

    It looks like If I use TIMER1 it works like it should. Can you please still answer my questions. Thank you very much.

Related