This discussion has been locked.
You can no longer post new replies to this discussion. If you have a question you can start a new discussion

Timer as frequency measurement: issue

Hello everyone,

I am using the STM32F407ZET6, amongst other things, for a frequency measurement.

The incoming sine signal is converted to a (positive only) square wave signal via hardware, and is then fed to a GPIO port, configured as external interrupt on rising edge.
To measure its cycle duration, I am starting a microsecond-increment timer on the first rising edge and disable its counter after, let’s say, 5 rising edges. On every timer interrupt a variable is incremented to keep count of the passed microseconds. Eventually I am using the mean value over those 5 cycles to determine the signal frequency
(I can’t use the input capture function of the timers because the square-signal lies on an unfitting port.)

Now this works very well. I get exact results that fit their purpose by all means.
But only up to the point where I add several delays (to control a LCD display) before the frequency measurement. For some reason this seems to interfere with the correct timer execution. The more or larger delay functions have been placed, the greater the error during the frequency measurement.

I’ve done the delays with the same timer I use for the frequency measurement (timer 3) and also by simple usage of NOP loops. Both create said error. I feel like, for some reason, it messes with the system ticks.

Any inputs on this topic would be greatly appreciated.

Kind regards,
Alain

Parents
  • So how do you implement the delays?

    As busy-loops against a free-running timer (should be safe to also measure time with same timer)

    By resetting the timer on every delay - which means the timer can't at the same time measure the external signal.

    By programming the timer to generate an interrupt after x cycles? Also not compatible with the other measurement unless you can adapt the interrupt trig value based on the current value so leaving the timer free-running.

    The best way for you is to never reset that timer when measuring the frequency or when using it as a delay. Just let it run freely and just sample the current value on start. As long as you treat the timer as an unsigned value and you use unsigned variables of the same size, you can just take do:
    if (a-b) >= my_delay then delay done
    and:

    start = timer_value;
    ...
    stop = timer_value;
    


    Number of ticks for 5 periods is (stop - start) -- assuming it's an incrementing timer.

    Just that with unsigned integers of same size, you don't have issues with the timer turning around, as long as you know that you never try to measure a too long time so it can turn around multiple times. And that can also be handled by specifically looking for how many times it did turn around.

Reply
  • So how do you implement the delays?

    As busy-loops against a free-running timer (should be safe to also measure time with same timer)

    By resetting the timer on every delay - which means the timer can't at the same time measure the external signal.

    By programming the timer to generate an interrupt after x cycles? Also not compatible with the other measurement unless you can adapt the interrupt trig value based on the current value so leaving the timer free-running.

    The best way for you is to never reset that timer when measuring the frequency or when using it as a delay. Just let it run freely and just sample the current value on start. As long as you treat the timer as an unsigned value and you use unsigned variables of the same size, you can just take do:
    if (a-b) >= my_delay then delay done
    and:

    start = timer_value;
    ...
    stop = timer_value;
    


    Number of ticks for 5 periods is (stop - start) -- assuming it's an incrementing timer.

    Just that with unsigned integers of same size, you don't have issues with the timer turning around, as long as you know that you never try to measure a too long time so it can turn around multiple times. And that can also be handled by specifically looking for how many times it did turn around.

Children
  • The timers have an input capture mode to accurately time-stamp edges, use that.

  • Hello Per, thank you very much for your reply.

    To answer your questions i will try to describe the basic structure of the delay as well as the time measurement.

    The timer is set up as usual:

    void ZCDD_Delay_TIM3_Init(void){
            // Enable peripheral clock
            RCC->APB1ENR |= RCC_APB1ENR_TIM3EN;
    
            // Enable Timer 3 Interrupt
            NVIC_EnableIRQ(TIM3_IRQn);
    
            TIM3->PSC    = 0;            // prescaler Register 1-1 = 0 (84/1 MHz --> 84 MHz)
            TIM3->CNT    = 0x0;          // Counter Value Register (start-value)
            TIM3->ARR = 83;              // Counter Auto Relaoad Register: 84 - 1 = 83 (84 MHz / 84 = 1 MHz) => 1 usec increments
    
            TIM3->CR1    &=~TIM_CR1_CKD_1;   // Clock Division [1:0] = 00 => no division
            TIM3->CR1    &=~TIM_CR1_CKD_0;
            TIM3->CR1    &=~TIM_CR1_DIR;             // Counter direction: up
            TIM3->CR1 |= TIM_CR1_URS;            // Update Request Source: Only counter overflow/underflow generates an update interrupt or DMA request
    
            TIM3->DIER |= TIM_DIER_UIE; //Update interrupt enable
    
            //TIM2->CR1  |= TIM_CR1_CEN;         // Counter Enable/Disable, will be set via seperate function
    }
    

    This is the interrupt service routine of the timer:

    void TIM3_IRQHandler(void){
            TIM3->SR &=~TIM_SR_UIF; // clear pending interrupt flag
            TIM3_usec_elapsed++; // increase external counter variable
    }
    

    As you can see there is a counter variable being increased. This will be used to determine the elapsed microsecounds.

    To enable the Timer i use following function:

    void ZCDD_TIM3_CounterEnable(void){
            TIM3_usec_elapsed = 0;  // clear usec counter
            TIM3->CR1    |= TIM_CR1_CEN;
    }
    


    Hence the interrupt counter will be reset on every start timer command to avoid wrong counter values at different usage.

    The timer disable function is just this

    void ZCDD_TIM3_CounterDisable(void){
            TIM3->CR1    &=~TIM_CR1_CEN;
    }
    

    The counter value can be read out at any time by usage of the following function:

    uint32_t ZCDD_TIM3_GetTickCount(void){
            return TIM3_usec_elapsed;
    }
    

    The delay function itself works like this:

    void Delay_usec(float delayCounter){
            ZCDD_TIM3_CounterEnable();
            while (ZCDD_TIM3_GetTickCount() < delayCounter){__NOP();}
            ZCDD_TIM3_CounterDisable();
    }
    


    After starting the timer there is a busy loop that, as leave condition, compares the forwarded amount of microseconds to be delayed and the current interrupt counter of the timer. if the condition is met, the counter will be disabled.

    For the frequency measurement an external GPIO interrupt is used:

    void EXTI0_IRQHandler (void) {
    
            if (EXTI->PR & (1<<0)) {                        // EXTI0 interrupt pending?
        EXTI->PR |= (1<<0);                           // clear pending interrupt
            }
    
            if (ZCD_Counter == 0) {
                    ZCDD_TIM3_CounterEnable();
            }
    
            ZCD_Counter++;
            // No function needed. Purpose is to break while loop in ZCD_Go();
    }
    


    On the first edge the timer will be enabled, therefore the interrupt counter will be reset as well. Other than that, the ISR only counts the amount of detected rising edges by incrementing the ZCD_Counter variable.

    The start of teh actual measurement is controlled by the following function:

    float ZCD_Go(uint32_t n_periods){
            float frequency;
    
            // Enable external interrupt Service Routine
            NVIC_EnableIRQ(EXTI0_IRQn); //Cortex M4F Core function - Enable External Interrupt Line 0 (for Px1)
            //Start ZCDD_TIM3 in interrupt handler
            while(n_periods >= ZCD_Counter){__NOP();}//__WFI();} // Wait until n_periods cycles have passed (=>amount of rising edges)
            ZCDD_TIM3_CounterDisable();     // Disable Time Measurement
            NVIC_DisableIRQ(EXTI0_IRQn); //Cortex M4F Core function - Disable External Interrupt Line 0 (for Px1), will not be used again till restart of measurement
            frequency = ((float)(1000000*n_periods))/(float)ZCDD_TIM3_GetTickCount();
    
            return frequency;
    }
    


    It enables the external interrupt and then keeps a busy loop running which compares the requested amount of periodes with the number of currently detected of rising edges. If the condition is met, first the timer and then the external interrupt will be disabled. Eventually the frequency is determined out of the timer variable and used as the return value of the function.

    As you can see, the timer always runs straight through every delay or measurement function without being disabled or disrupted (except for the external interrupt). The delay function and the measurement function never take place at the same time. in fact, between the last delay function and the frequency measurement there is a whole bunch of other functions.

    I apologize for this a little extensive amount of text.

  • Hello Pier,

    Thank you for your reply. As mentioned above i cant use the input capture function because the signal is wired to an unfitting pin. The design is already finalized and manufactured, so there is not much capability of changing it all. Well except for some quite long and unpleasant work of hand.

    Kind regards,
    Alain

  • "The timers have an input capture mode to accurately time-stamp edges, use that."

    But he already noted that the pin the signal arrives on doesn't support this.

  • You should clear the external interrupt flag before you start measuring 5 periods.

    Else you'll get a new period start while you are busy with LCD delay.
    And this period start sets the external interrupt flag.
    When you then decides to start your measurement you instantly get an interrupt even if it was a long time since the last pulse.

    Note that it's often nice to keep the timer free-running forever.

    The reason for this is that you can then use an interrupt-drive delay for longer delays. But you can also busy-loop short delays with little overhead by quickly sample the current timer value and then repeatedly sample again and again until the difference between the two samples represents your delay. This means you could have a timer tick with 100ns period and busy-loop a 1.2us delay (with a 0.1us jitter error and a bit of setup error) by waiting 12 ticks. Or you could have the main loop order a 500ms delay by requesting an interrupt (that sets a flag) after 5000000 ticks. Or the main loop could poll multiple concurrent software timers with:

    for (;;) {
        if ((t0 - sw_timer0_start) >= TIMER0_DELAY) {
            ...
        }
        if ((t0 - sw_timer1_start) >= TIMER1_DELAY) {
            ...
        }
        if (hw_timer_flag) {
            hw_timer_flag = false;
            ...
        }
        if (uptime >= uptime_last + 30) {
            ...
        }
    }
    


    And if that free-running timer generates an interrupt on every overflow, then it could update an "uptime" counter.

    So a single free-running timer can really solve lots of different problems all at the same time. Short polled delays. Multiple concurrently polled long delays. System uptime.

  • A 1us interrupt isn't a good plan (1 MHz!)

    TIM3->SR &=~TIM_SR_UIF; // clear pending interrupt flag

    No need to mask it on, just write it to the register

    TIM3->SR =~TIM_SR_UIF;

    Also try using DWT_CYCCNT as a sub-microsecond time stamp, it doesn't need interrupts, and has 32-bit precision, and multi-second rollover

    There's the SYSTICK counter too, but it's only 24-bit wide as I recall

  • Even if it's not connected to a pin you could use any timer (ideally a 32-bit one) to count at 1 MHz, and use the HW count, and not a SW count. If you had it wrap at 1000 ticks your interrupt would be 1 KHz, and not 1 MHz. Remember a 1 MHz interrupt will divert the processor every 168 cycles (at 168 MHz), which is rather excessive.

    16-bit (TIM3 and TIM4) or 32-bit (TIM2 and TIM5)

  • Thank you very much for your help, everyone. The issue is solved.

    Per, it was indeed a pending external interrupt which was set during the LCD Setup time. I wasn't aware of the fact that the microcontroller sets interrupt flags as soon as the port is accordingly configured, and not only after the interrupt service routine itself was activated.
    However i do not seem to be able to reset the interrupt flag outside the service routine, so i simply ignore the first call of the ISR. It's not pretty but it works.

    In this application a free running usec timer is out of question due to time sensitive operations following the frequency measurement. To keep system performance high and the time measurement increments small, i am forced to toggle a usec interrupt timer. But i will keep this in the back of my head for later use. Thanks a lot for the input!

    Pier, i am aware that a 1 usec timer is very demanding for the uC, you are right in your pleads to avoid the likes. Unfortunately i am forced to use such small time increments to get an exact frequency value since all following calculations and measurements are based around the signal frequency. But to your reassurance: i only activate the timer on a few occasions when there is nothing much else to do for the processor. Also the measurement interrupts have a higher priority than the timer interrupt, which should secure its the proper execution.

    I used the SysTick counter before, subtracting an end and a start value and thus calculating the passed time. But i later on changed it to the toggling usec timer because i felt it is cleaner coding.

    I will definitely look into DWT_CYCCNT and HW Counter. Thank you very much for your advice.

    Best regards,
    Alain