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
  • 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.

Reply
  • 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.

Children
  • 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