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

Create accurate microsecond timer without interrupts

Hey guys,

I want to create an accurate timer without interrupts. My timer has to be very accurate because I have to send HIGH and LOW signals for 1us. I have read this post to know that I need high-frequency clock source (HFCLK, 16 MHz), which means better resolution (62.5 ns) and higher power consumption (typ. 5 or 70 uA depending on HFCLK source). But I don't know how to program my code so I followed this post and this post. But I have still some problems with it. I post my code below.

My timer should only replace the unaccurate nrf_delay_us.

#include <stdint.h>

#include "nrf_delay.h"
#include "app_error.h"
#include "nrf_gpio.h"
#include "nrf_drv_timer.h"

#define OUTPUT_PIN  18


/** @brief Function for Timer 1 initialization.
 */
void TIMEOUT_TIMER1_Init(void)
{
    NRF_TIMER1->TASKS_STOP = 1;           /**< Stop Timer 1 */
    NRF_TIMER1->TASKS_CLEAR = 1;          /**< Clear Timer 1 */

    NRF_TIMER1->MODE      = 0;            /**< Mode 0: Timer */
    NRF_TIMER1->BITMODE   = 3;            /**< Bitmode 3: 32 bit (for what?, What happens if I choose 16 bit?) */
    NRF_TIMER1->PRESCALER = 4;            /**< Prescaler 4: 1us Timer -> f(TIMER) = 16MHz / (2^PRESCALER) = 1MHz -> t(TIMER) = 1us */
    NRF_TIMER1->INTENCLR = TIMER_INTENCLR_COMPARE0_Disabled
                            | TIMER_INTENCLR_COMPARE1_Disabled
                            | TIMER_INTENCLR_COMPARE2_Disabled
                            | TIMER_INTENCLR_COMPARE3_Disabled
                            | TIMER_INTENCLR_COMPARE4_Disabled
                            | TIMER_INTENCLR_COMPARE5_Disabled; /**< Disable all interrupts */
}


void TIMEROUT_TIMER1_Start(uint32_t timeout_us)
{
    NRF_TIMER1->TASKS_STOP = 1;
    NRF_TIMER1->TASKS_CLEAR = 1;

    // this line: set TIMER1 to 0 (start value)
    // this line: hand over the time "timeout_us" to TIMER1 (end time)
    // this line: stop TIMER1 if end time ("timeout_us") is reached

    NRF_TIMER1->TASKS_START = 1;
}


void Write_Byte(uint8_t byte)
{
    int walk;

    for (walk = 0; walk < 8; ++walk)
    {
        nrf_gpio_pin_clear(OUTPUT_PIN);    
        if (byte & 0x01)
        {
            TIMEROUT_TIMER1_Start(1);       //instead of nrf_delay_us()                      
            nrf_gpio_pin_set(OUTPUT_PIN);                
            TIMEROUT_TIMER1_Start(60);      //instead of nrf_delay_us()                   
        }
        else
        {
            TIMEROUT_TIMER1_Start(60);       //instead of nrf_delay_us()                      
            nrf_gpio_pin_set(OUTPUT_PIN);                 
            TIMEROUT_TIMER1_Start(1);        //instead of nrf_delay_us()                     
        }
        byte >>= 1;                                       
    }
}


/**
 * @brief Function for application main entry.
 */
int main(void)
{
    uint32_t err_code;
    TIMEOUT_TIMER1_Init;
    
    uint8_t AA = 0xAA;
    nrf_gpio_cfg_output(OUTPUT_PIN);

    while (true)
    {
        Write_Byte(AA);
        nrf_delay_us(20);
    }
}

In line 19: (Init) Why should I use 32 bits and not 16 bits? where is the difference?

In lines 35 - 37: (time function) I hope you can help me here. I don't know how I can hand over my timeout value to my function and to my timer, so that the timer can work with it...

I hope, you can help me.

Thanks in advance,

Christoph

Parents
  • Hi,

     

    Since the prescaler is already setup with 4 (1 us resolution), you can set the specific CC directly with the input timeout_us.

    Something like this maybe?

    #define CC_USED 0
    
    if (timeout_us == 0) return;
    
    NRF_TIMERx->TASKS_STOP = 1;
    NRF_TIMERx->TASKS_CLEAR = 1;
    
    NRF_TIMERx->CC[CC_USED] = timeout_us;
    NRF_TIMERx->TASKS_START = 1;
    while(NRF_TIMERx->EVENTS_COMPARE[CC_USED] == 0) 
    {
      /* Call __WFE() if loop is long */
    }
    NRF_TIMERx->EVENTS_COMPARE[CC_USED] = 0;

     

    In line 19: (Init) Why should I use 32 bits and not 16 bits? where is the difference?

    16 bit = 0 to 65535 us

    32 bit = 0 to 4 294 seconds

     

    Depends on the range you'd like.

     

    Kind regards,

    Håkon

  • Hey Håkon,

    thank you for your fast reply. I have some questions for my understanding (1,2,3,4) and another question (5) for my time issue.

    1) In your Code:

    while(NRF_TIMERx->EVENTS_COMPARE[CC_USED] == 0)    //e.g. timeout_us = 40us

    the events_compare[CC_USED] is as long as zero until the 40us are reached, right? And when the 40us are reached, the counter is incremented which causes the compare event (EVENTS_COMPARE[CC_USED]) to be generated and the EVENTS_COMPARE becomes equal to the value specified in CC_USED (40us), right?  This leads to question 2.

    2) In your Code:

    NRF_TIMERx->EVENTS_COMPARE[CC_USED] = 0;

    I have to delete the value because of the EVENTS_COMPARE[CC_USED] have the value "40us" now?

    3) After NRF_TIMERx->EVENTS_COMPARE[CC_USED] = 0; why I don't set at the end of the function the stop and clear command (NRF_TIMERx->TASKS_STOP = 1; and NRF_TIMERx->TASKS_CLEAR = 1;)? I ask because if I don't set the stop command at the end of the function the timer will expire the whole time, not? Why it's better to set the stop and clear commands at the beginning of the function?

    4)

    32 bit = 0 to 4 294 seconds

    For example I set timeout_us = 4400 seconds. This timeout_us is never reached because after 4294 seconds there will happen an overflow and the timer will restart at 0seconds, right? This will be an endless loop, right?

    5) I tried our code (inclusive your idea) and I get the same problem like nrf_delay_us. I messure with an oscilloscope the high and low signals of my output pin and the time is as accurate as nrf_delay_us ...

    void Write_Byte(uint8_t byte)
    {
        int walk;
    
        for (walk = 0; walk < 8; ++walk)
        {
            nrf_gpio_pin_clear(OUTPUT_PIN);    
            if (byte & 0x01)
            {
                TIMEROUT_TIMER1_Start(1);       //instead of nrf_delay_us()                      
                nrf_gpio_pin_set(OUTPUT_PIN);                
                TIMEROUT_TIMER1_Start(60);      //instead of nrf_delay_us()                   
            }
            ....

    In line 10 I want a timeout of 1us and in oscilloscope the timout is nearly 3us.. and in line 12 i want a timeout of 60us and get a timeout between 66 and 67us... Do you know what it can be? Maybe it's my board which causes my issue? I'm using the DVK-BL652 from Laird.

    Kind regards,

    Christoph

  • Hi,

     

    I see what you mean. I get a delay of ~2.3 us when using the timer, and approx. 1.2 us when using nrf_delay_us().

    When dealing with such low delays, the overhead with handling the actual timer tasks and events will give a significant error in your GPIO toggling.

     

    For the 1 us specific delay, I think you have to use a busy-loop, and not the timer.

    I have played a bit around and tried to create a function that can give a more reliable delay.

    1 us input:

    2 us input:

    10 us input:

     

    Could you try this and see if it works similarly on your end?

    /**
     * Copyright (c) 2014 - 2019, Nordic Semiconductor ASA
     *
     * 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, except as embedded into a Nordic
     *    Semiconductor ASA integrated circuit in a product or a software update for
     *    such product, 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 Nordic Semiconductor ASA nor the names of its
     *    contributors may be used to endorse or promote products derived from this
     *    software without specific prior written permission.
     *
     * 4. This software, with or without modification, must only be used with a
     *    Nordic Semiconductor ASA integrated circuit.
     *
     * 5. Any software provided in binary form under this license must not be reverse
     *    engineered, decompiled, modified and/or disassembled.
     *
     * THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS
     * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
     * OF MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE
     * DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA 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.
     *
     */
    /** @file
     *
     * @defgroup blinky_example_main main.c
     * @{
     * @ingroup blinky_example
     * @brief Blinky Example Application main file.
     *
     * This file contains the source code for a sample application to blink LEDs.
     *
     */
    
    #include <stdbool.h>
    #include <stdint.h>
    #include "nrf_delay.h"
    #include "boards.h"
    
    #define CC_USED 0
    
    void timer_setup(void)
    {
        NRF_TIMER1->BITMODE = TIMER_BITMODE_BITMODE_32Bit;
        NRF_TIMER1->SHORTS = TIMER_SHORTS_COMPARE0_CLEAR_Enabled << TIMER_SHORTS_COMPARE0_CLEAR_Pos| TIMER_SHORTS_COMPARE0_STOP_Enabled << TIMER_SHORTS_COMPARE0_STOP_Pos;
        NRF_TIMER1->PRESCALER = 0;
    }
    
    /* Just a test function*/
    static inline void sleep_us(uint32_t timeout_us)
    {
        for (register uint32_t i = 0; i < timeout_us; i++)
        {
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop(); __nop(); __nop(); 
        }
    }
    
    #pragma push
    #pragma O3
    void timer_sleep(uint32_t timeout_us)
    {
    
        if (timeout_us == 0) 
        {
            return;
        }
        if (timeout_us == 1)
        {
            /* Adjust the amount of NOPs here */
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop();
            return;
        }
    
        NRF_TIMER1->CC[CC_USED] = (timeout_us << 4) - 18;
        NRF_TIMER1->TASKS_START = 1;
        while(NRF_TIMER1->EVENTS_COMPARE[CC_USED] == 0) 
        {
          /* Call __WFE() if loop is long */
        }
        NRF_TIMER1->EVENTS_COMPARE[CC_USED] = 0;
    }
    #pragma pop
    
    /**
     * @brief Function for application main entry.
     */
    int main(void)
    {
        /* Configure board. */
        bsp_board_init(BSP_INIT_LEDS);
        NRF_CLOCK->TASKS_HFCLKSTART = 1;
        while (NRF_CLOCK->EVENTS_HFCLKSTARTED == 0);
        NRF_NVMC->ICACHECNF = NVMC_ICACHECNF_CACHEEN_Msk;
        timer_setup();
        /* Toggle LEDs. */
        while (true)
        {
            timer_sleep(2);
            NRF_P0->OUT ^= (1 << LED_1);
    
        }
    }
    
    /**
     *@}
     **/
    

     

    Christoph_I said:
    For example I set timeout_us = 4400 seconds. This timeout_us is never reached because after 4294 seconds there will happen an overflow and the timer will restart at 0seconds, right? This will be an endless loop, right?

     a uint32_t will overflow in this scenario, resulting in a lower timeout. If you set the timer to 0x 12 0000 1000, the timer will count to 0x1000, and give an event.

     

    Kind regards,

    Håkon

  • Hey Håkon,

    thank you, the times have become much more accurate. This is a big step forward!

    With your function timer_sleep without adjust the amount of NOPs I'm getting 2us instead of 1us on my end. But deleting all NOPs in this function I'm getting 1.340us (LOW signal, HIGH: 1.370) instead of 1us and that's very close to what I want.

    void timer_sleep(...)
    {
        ...
        if (timeout_us == 1)
            {
                /* NO NOPs here */
                return;
            }
        ...
    }

    Do you know if there are any other options to reduce the inaccuracy more and more?

    Kind regards,

    Christoph

    ______________________________________

    Edit:

    P.S.:

    Without NOPs I'm getting this times:

    2us -> HIGH: 3.08us; LOW: 2.840

    10us -> HIGH: 11.10us; LOW: 10.70us

    60us --> LOW: 60.80us

  • Hi Christoph,

     

    Did you run the example as-is to see if you are able replicate my measurements? Note the ICACHE is enabled, and external HFCLK is running, and the function itself is with O3 optimization (Keil syntax). Which compiler are you using?

     

    Kind regards,

    Håkon

Reply Children
  • Ok Håkon,

    Did you run the example as-is to see if you are able replicate my measurements?

    that is a good objection. I run your example and reduced the amount of __NOP() a little bit.

    #include <stdbool.h>
    #include <stdint.h>
    #include "nrf_delay.h"
    #include "boards.h"
    
    #define OUTPUT_PIN 18
    #define CC_USED 0
    
    void timer_setup(void)
    {
        NRF_TIMER1->BITMODE = TIMER_BITMODE_BITMODE_32Bit;
        NRF_TIMER1->SHORTS = TIMER_SHORTS_COMPARE0_CLEAR_Enabled << TIMER_SHORTS_COMPARE0_CLEAR_Pos| TIMER_SHORTS_COMPARE0_STOP_Enabled << TIMER_SHORTS_COMPARE0_STOP_Pos;
        NRF_TIMER1->PRESCALER = 0;
    }
    
    
    #pragma push
    #pragma O3
    void timer_sleep(uint32_t timeout_us)
    {
    
        if (timeout_us == 0) 
        {
            return;
        }
        if (timeout_us == 1)
        {
            /* Adjust the amount of NOPs here */
            __NOP(); __NOP(); __NOP(); __NOP(); 
            __NOP(); __NOP(); __NOP(); __NOP(); 
            __NOP(); __NOP(); __NOP(); __NOP(); 
            __NOP(); __NOP(); __NOP(); __NOP(); 
            __NOP(); __NOP(); __NOP(); __NOP(); 
            __NOP(); __NOP(); __NOP(); __NOP(); 
            __NOP(); __NOP(); __NOP(); __NOP(); 
            __NOP(); __NOP(); __NOP(); __NOP(); 
            __NOP(); __NOP(); __NOP(); __NOP(); 
            /*__nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop();*/
            return;
        }
    
        NRF_TIMER1->CC[CC_USED] = (timeout_us << 4) - 18;
        NRF_TIMER1->TASKS_START = 1;
        while(NRF_TIMER1->EVENTS_COMPARE[CC_USED] == 0) 
        {
          /* Call __WFE() if loop is long */
        }
        NRF_TIMER1->EVENTS_COMPARE[CC_USED] = 0;
    }
    #pragma pop
    
    /**
     * @brief Function for application main entry.
     */
    int main(void)
    {
        /* Configure board. */
        //bsp_board_init(BSP_INIT_LEDS);
        bsp_board_leds_init();
        NRF_CLOCK->TASKS_HFCLKSTART = 1;
        while (NRF_CLOCK->EVENTS_HFCLKSTARTED == 0);
        NRF_NVMC->ICACHECNF = NVMC_ICACHECNF_CACHEEN_Msk;
        timer_setup();
        /* Toggle LEDs. */
        while (true)
        {
            timer_sleep(2);
            NRF_P0->OUT ^= (1 << OUTPUT_PIN);
    
        }
    }
    
    /**
     *@}
     **/
    

    Now I'm getting nearly the 1us! :)

    But why is there a delay when I'm using:

    nrf_gpio_pin_set() / clear() ?

    Can you please check if my adoptions are true:

    To set the output I'm using: NRF_P0->OUT |= (1 << OUTPUT_PIN);

    To clear the output: NRF_P0->OUT &= ~(1 << OUTPUT_PIN);

    To read from input: NRF_P0->IN = (1 << INPUT_PIN);

    I'm using SES and SDK 14.2.0

    Kind regards,

    Christoph

  • Hi Christoph,

    Christoph_I said:
    Now I'm getting nearly the 1us! :)

    That is good news!

    Christoph_I said:
    To read from input: NRF_P0->IN = (1 << INPUT_PIN);

    Try this: uint32_t pin =  (NRF_P0->IN & (1 << INPUT_PIN));

    Others look OK.

    Christoph_I said:
    nrf_gpio_pin_set() / clear() ?

    Because they are function calls, although static inlined, which use stacked memory to access the register. If you look at the assembly output, you'll likely see more instructions than just directly accessing the register. The output depends on the compiler used as well.

     

    For GCC, you need to force the optimization like this:

    #pragma GCC push_options
    #pragma GCC optimize ("O3")
    void timer_sleep(uint32_t timeout_us)
    {
    
        if (timeout_us == 0) 
        {
            return;
        }
        if (timeout_us == 1)
        {
            /* Adjust the amount of NOPs here */
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop(); __nop(); __nop(); 
            __nop(); __nop();
            return;
        }
    
        NRF_TIMER1->CC[CC_USED] = (timeout_us << 4) - 18;
        NRF_TIMER1->TASKS_START = 1;
        while(NRF_TIMER1->EVENTS_COMPARE[CC_USED] == 0) 
        {
          /* Call __WFE() if loop is long */
        }
        NRF_TIMER1->EVENTS_COMPARE[CC_USED] = 0;
    }
    #pragma GCC pop_options

    Your overall settings for optimization will be overridden for this specific function only.

     

    *edit*

    Tested with "O3" optimization on SES v4.22, and got:

    1 us input = 1.05 to 1.1 us

    2 us input = 1.9 us

    10 us input = 9.9 us

     

    Please check with these pragma's for GCC optimization and see if you also see similar behavior.

      

    Kind regards,

    Håkon

  • Hey Håkon,

    it's the last day before weekend. Slight smile

    uint32_t pin =  (NRF_P0->IN & (1 << INPUT_PIN));

    As note: when I'm trying this I had to set the INPUT_PIN as input as follows:

    nrf_gpio_cfg_input(INPUT_PIN, NRF_GPIO_PIN_NOPULL);

    Because they are function calls,

    Thanks for your explanation.

    What is doing the ICACHE?

    Your overall settings for optimization will be overridden for this specific function only.

    Does the pragma set the priority for this function at the highest level or which settings for optimization will be overritten by pragma?

    Please check with these pragma's for GCC optimization and see if you also see similar behavior.

    I've check the input with the new GCC optimization and your amount of __NOP() (50 i think) and I'm getting the same values as you.

    1 us input = 1.08 us

    2 us input = 1.88 us

    10 us input = 9.9 us

    60 us input = 60 us

    But first I have to thank you for your patience and devotiation. Without your help I would be very helpless. Thanks Håkon, you are a very nice and smart one! I really enjoyed working with you.

    Kind regards,

    Christoph

  • Hi Christoph

     

    Christoph_I said:
    As note: when I'm trying this I had to set the INPUT_PIN as input as follows

    Ah, yes, that is a pre-requisite. Same goes for GPIO outputs, which needs to be configured (nrf_gpio_cfg_output(my_pin_number); for instance), but those you set/clear via NRF_GPIO->OUTSET, OUTCLR, or OUT (or any other "helper" functions, like nrf_gpio_pin_toggle()).

     

    Christoph_I said:
    What is doing the ICACHE?

     This is the instruction cache. This cache reads instructions from flash, assuming that the program execution is incremental, thus speeding up your program. Its sped up as flash is actually a bit slower than the frequency the CPU run on. The implementation is explained more in detail here: https://infocenter.nordicsemi.com/topic/com.nordic.infocenter.nrf52832.ps.v1.1/nvmc.html?cp=4_2_0_10_5#concept_dh3_cyy_vr

    But, for generic cache explanations, wiki would help you out as well.

     

    Christoph_I said:
    Does the pragma set the priority for this function at the highest level or which settings for optimization will be overritten by pragma?

     The pragma sets the optimization level to the compiler. Let's say you want to optimize your firmware for size (-Os flag to the compiler), but you have certain functions that you want to run as fast as possible, you can wrap that option around those functions. You can even optimize a full .c file individually if you'd like.

     

    Christoph_I said:
    I've check the input with the new GCC optimization and your amount of __NOP() (50 i think) and I'm getting the same values as you

    This is good news! You can ofcourse add or remove amount of NOPs to see if you're able to fine-tune it even further.

     

    Christoph_I said:
    But first I have to thank you for your patience and devotiation. Without your help I would be very helpless. Thanks Håkon, you are a very nice and smart one! I really enjoyed working with you.

     Thank you for the kind words! I am very glad to hear that I was of good use! If you run into issues or questions in the future, you know where to ask Slight smile

    Hope you have a nice weekend, Christoph!

     

    Kind regards,

    Håkon

      

  • Hey Håkon,

    Same goes for GPIO outputs, which needs to be configured

    that's interesting because for toggling the output like NRF_P0->OUT ^= (1 << OUTPUT_PIN); I don't need to configure the output pin as output (in my project)...

    But thanks again for your detailed explanations. It helps me a lot. Slight smile

    If I have some problems again, I hope we can work together again. Slight smile

    Have a nice weekend too, Håkon!

    Best wishes and kind regards,

    Christoph

Related