Author Topic: STM32F030C8T6 - Best way to measure frequency on GPIO pin without timer?  (Read 2115 times)

0 Members and 1 Guest are viewing this topic.

Offline daqqTopic starter

  • Super Contributor
  • ***
  • Posts: 2302
  • Country: sk
    • My site
Hi guys,

I kinda screwed up on a design - I forgot that the need for self adjustment of the device clock, so I'm stuck with PA13, PA14 (SWD programmer) and PC13 as the only pins that are on a reasonably accessible place.

None of these have any timer functionalities that I know of, though PC13 does have a bunch of RTC functions. I am not using the RTC, so that's not important.

I'm stuck with doing a calibration measurement by software. It doesn't need to be hyper precise, but it should let me know the frequency of the device compared to a reference signal (hoping for 10MHz, will settle for 1MHz). The device will run on 48MHz (8MHz crystal scaled up by PLL).


There's a few approaches I can think about, the simplest would be to simulate a timer with capture for pulse counting via software:
1. I stop all other interrupts aside from one timer interrupt.

2. In the main program I'll do something like:
Code: [Select]
CountingDone_flag = 0; //Reset global volatile flag
Counter = 0; //Reset global volatile 32 bit counter
CounterCapture = 0; //Reset global volatile 32 bit counter capture register
StartTimer(); //Start a timer
do
{
 PrevState = State;
 State = GetPinState(PC13);
 if(State != State_prev) Counter++;
}while(!CountingDone_flag);
3. The interrupt will be triggered by a real timer and will trigger precisely after 100s (bit of an overkill, but it should minimize any errors from the software triggering and overhead). The code for it will be simple:
Code: [Select]
volatile uint32_t CounterCapture = 0;
void TIM3_IRQHandler(void)
{
  CounterCapture  = Counter; //Copy the current state of the counter here
  CountingDone_flag = 1;
  ClearInterrupts();
  DisableTimer();
}
4. After this is done, knowing the exact reference frequency being applied onto PC13, it's a simple matter of calculating the actual internal frequency.

Another way would be the reverse of this, where I apply a 100s single pulse onto PC13 and start and stop a timer from the pin change interrupt. This however is problematic, because all of the available timers are 16 bit and would overflow many times. Since I do not have access to the internal prescaler, it's not possible to get a 32 bit value from this and I would have to have two interrupts. I could probably use one timer as a prescaler for another timer, but I'm not sure that's possible, nor that it would be superior to the first approach.

I am hoping to avoid extra instrumentation - I could probably generate a square signal via software and hook it up to a high res frequency counter, but I do not have one and I would like to avoid making the process more complicated than it needs to be.

Is there a more elegant way? Some clever way of using any GPIO to trigger timers?

Thanks,

David

Believe it or not, pointy haired people do exist!
+++Divide By Cucumber Error. Please Reinstall Universe And Reboot +++
 

Online Kleinstein

  • Super Contributor
  • ***
  • Posts: 14208
  • Country: de
One could get around the 16 bit timer limit by applying a slightly faster external test signal, so that the 16 bit timer is sufficient. The µC can than count the pulses in SW and check for timer overflows. So only 1 interrupt needed.

If the measurement needs to be fast the extra data from pulses in between could be used to get a high resolution. Instead of only using the first and last time, one could do a linear regression (there is a close formula for this). This reduced the jitter error from the ISR delay.
 
The following users thanked this post: daqq

Offline langwadt

  • Super Contributor
  • ***
  • Posts: 4427
  • Country: dk
if it runs off a crystal how much do you expect it to be off?
 

Offline PCB.Wiz

  • Super Contributor
  • ***
  • Posts: 1545
  • Country: au
I'm stuck with doing a calibration measurement by software. It doesn't need to be hyper precise, but it should let me know the frequency of the device compared to a reference signal (hoping for 10MHz, will settle for 1MHz). The device will run on 48MHz (8MHz crystal scaled up by PLL).
Another way would be the reverse of this, where I apply a 100s single pulse onto PC13 and start and stop a timer from the pin change interrupt. This however is problematic, because all of the available timers are 16 bit and would overflow many times. Since I do not have access to the internal prescaler, it's not possible to get a 32 bit value from this and I would have to have two interrupts. I could probably use one timer as a prescaler for another timer, but I'm not sure that's possible, nor that it would be superior to the first approach.

Is there a more elegant way? Some clever way of using any GPIO to trigger timers?
External clocking is a weakness in many MCUs but it does sound like you need 'good precision' as you have a crystal, and mention 32b capture and 100s capture times.
It also sounds like this is a one-off factory-offset capture and a good source for calibrate of that would be a 1pps from a GPS module

Better Crystals should be ok within ±10ppm, and then they vary ±10ppm, or ±20ppm over temperature.

You probably need to resolve to about 1ppm-or-better ballpark, anything finer will be lost in temperature changes anyway.

What I would do is use a pin change interrupt, and start/stop timers using that, just as you mention above.
On some MCUs, if the main loop sits in IDLE, you get less interrupt jitter (no opcode to finish before it vectors)

You can use a timer interrupt to extend the 16b, but keep in mind you know with a crystal it will not be 'miles-off'

If we take a rough guess of 5 sysclks on jitter in the interrupt, for timer Start/stop,  and a 48Mhz sysclk, that's ~100ns of jitter or 0.1ppm in one second.

Using that 1pps, we find a 48MHz timer gives
 48M = 48000000 = 0x02DC_6C00
 48M*(1+20u)      = 0x02DC_6FC0
 48M*(1-20u)       = 0x02DC_6840
Notice here that upper 16 bits do not change, and the ±20ppm span is from 0x6840 to 0x6FC0, comfortably less than the 16b timer resolution :)

You could use more than a single 1pps to calibrate, but that would only really be needed if you had a TCXO sysclk.



« Last Edit: August 11, 2021, 10:09:42 pm by PCB.Wiz »
 
The following users thanked this post: daqq

Offline daqqTopic starter

  • Super Contributor
  • ***
  • Posts: 2302
  • Country: sk
    • My site
Thanks for all of the hints, much appreciated!

The suggestion by PCB.Wiz is an interesting and elegant approach - at the moment though I'm settled on a 1MHz signal source, got myself a rubidium clock some time ago :) The crystal should not be miles off, that is true.

Quote
if it runs off a crystal how much do you expect it to be off?
20ppm base tolerance, gives you around 1 minute error per month. Which is too much. There's also the tempco, but since the device is intended for room usage and is temperature mostly stable, it should not have more than a few ppm from ambient temperature variations.

Believe it or not, pointy haired people do exist!
+++Divide By Cucumber Error. Please Reinstall Universe And Reboot +++
 

Offline DavidAlfa

  • Super Contributor
  • ***
  • Posts: 5912
  • Country: es
Use the DWT counter, runs at CPU speed, so has the maximum precision available, and it's 32-bit.
DWT->CTRL |= 1 ; // enable the counter
DWT->CYCCNT = 0; // clear the counter (Or read It)
If the frequency is relatively low, you could use interrupt on pin change, read the counter, reset it and wait for the next edge.

Although I'm not sure i's available on F0 devices.
In F1 and upper, for sure.
« Last Edit: August 15, 2021, 06:37:54 pm by DavidAlfa »
Hantek DSO2x1x            Drive        FAQ          DON'T BUY HANTEK! (Aka HALF-MADE)
Stm32 Soldering FW      Forum      Github      Donate
 
The following users thanked this post: daqq, thm_w

Offline thm_w

  • Super Contributor
  • ***
  • Posts: 6389
  • Country: ca
  • Non-expert
Use the DWT counter, runs at CPU speed, so has the maximum precision available, and it's 32-bit.
DWT->CTRL |= 1 ; // enable the counter
DWT->CYCCNT = 0; // clear the counter (Or read It)
If the frequency is relatively low, you could use interrupt on pin change, read the counter, reset it and wait for the next edge.

Although I'm not sure i's available on F0 devices.
In F1 and upper, for sure.

F0 doesn't have it, but interesting feature for me to try out.

https://stackoverflow.com/questions/36378280/stm32-how-to-enable-dwt-cycle-counter
https://community.st.com/s/question/0D50X0000BddEDG/stm32f030-microsecond-delay
https://visualgdb.com/tutorials/arm/chronometer/
Profile -> Modify profile -> Look and Layout ->  Don't show users' signatures
 

Offline langwadt

  • Super Contributor
  • ***
  • Posts: 4427
  • Country: dk
could you add a short between PC13 and PC14?  PC14 is the OSC32 input for the RTC and afaict can run at up to 1MHz 
 

Offline abyrvalg

  • Frequent Contributor
  • **
  • Posts: 825
  • Country: es
If this is a dedicated one time calibration process (so you can suspend all other activities) you can just disable interrupts and count loop execution cycles between pin state changes. This should have lower latency than controlling a timer from a pin change interrupt and will give you a 32-bit count. Ok, it is not so obvious how to convert the result into metric units w/o looking into asm and instruction cycles, but you can take a reference measurement on one MCU running from a known stable clock and calculate the conversion factor instead. Or just hardcode that measured reference count into the calibration code and trim the oscillator in steps until you are close to that value.
 
The following users thanked this post: daqq

Offline DavidAlfa

  • Super Contributor
  • ***
  • Posts: 5912
  • Country: es
I doubt sampling 10MHz using software is doable at all.
With 1MHz, probably, but disabling all interrupts and running a tight loop, you'll have 48 clocks to read the signal, increase a counter, check if your loop is done...
Why can't you use any other pin? Only PA13, PA14 and PC13 are free?
Hantek DSO2x1x            Drive        FAQ          DON'T BUY HANTEK! (Aka HALF-MADE)
Stm32 Soldering FW      Forum      Github      Donate
 

Offline abyrvalg

  • Frequent Contributor
  • **
  • Posts: 825
  • Country: es
Re: STM32F030C8T6 - Best way to measure frequency on GPIO pin without timer?
« Reply #10 on: August 19, 2021, 11:07:04 pm »
Who says it must be 10MHz? There are suggestions like 1pps above.
Measuring a 10MHz period on a 48MHz MCU would be a bad idea even with a timer.
 

Offline PCB.Wiz

  • Super Contributor
  • ***
  • Posts: 1545
  • Country: au
Re: STM32F030C8T6 - Best way to measure frequency on GPIO pin without timer?
« Reply #11 on: August 19, 2021, 11:54:18 pm »
The suggestion by PCB.Wiz is an interesting and elegant approach - at the moment though I'm settled on a 1MHz signal source, got myself a rubidium clock some time ago :) The crystal should not be miles off, that is true.

If you have 1.0000Mhz, you are probably best to simply divide that externally, if you have only GPIO-pin and no timer access.
There are devices like 74HC4060 or 74AHC1G4215GW that can scale the 1us down to something pin-change interrupts and sw can better handle.
Two cascaded 74AHC1G4210GW would get you down to 1.048576 second, for the same ballpark as GPS 1pps.
Software could auto-select between 1MHz/2^20 and GPS_1pps

(1u*2^20)/((1/48M)*(1+10u)) = 0x02FFFE08
(1u*2^20)/(1/48M)           = 0x03000000
(1u*2^20)/((1/48M)*(1-10u)) = 0x030001F7

 

Offline DavidAlfa

  • Super Contributor
  • ***
  • Posts: 5912
  • Country: es
Re: STM32F030C8T6 - Best way to measure frequency on GPIO pin without timer?
« Reply #12 on: August 20, 2021, 10:30:12 am »
Who says it must be 10MHz? There are suggestions like 1pps above.
Measuring a 10MHz period on a 48MHz MCU would be a bad idea even with a timer.
Read the first post. You don't read the period directly, that would lack a lot of accuracy, instead you count the pulses for ex. 1 mcu second (48MHz) and then compute the deviation.
« Last Edit: August 20, 2021, 10:32:57 am by DavidAlfa »
Hantek DSO2x1x            Drive        FAQ          DON'T BUY HANTEK! (Aka HALF-MADE)
Stm32 Soldering FW      Forum      Github      Donate
 

Offline abyrvalg

  • Frequent Contributor
  • **
  • Posts: 825
  • Country: es
Re: STM32F030C8T6 - Best way to measure frequency on GPIO pin without timer?
« Reply #13 on: August 20, 2021, 10:44:46 am »
I would advise the same, tio :D
The calibration reference signal is not built into the system, OP will apply something to one of externalized pins, so the choice of signal parameters is open. If you read a bit further you’ll see suggestions like much slower but stable 1pps from GPS.
 

Offline DavidAlfa

  • Super Contributor
  • ***
  • Posts: 5912
  • Country: es
Re: STM32F030C8T6 - Best way to measure frequency on GPIO pin without timer?
« Reply #14 on: August 20, 2021, 12:29:53 pm »
I don't see where he said he's using a GPS module... That was just PCB.Wiz's suggestion.

There're very accurate 16MHz oscillators,  1ppm accuracy and 1ppm temperature drift.
In the worst case the drift will be 5s per month.
« Last Edit: August 20, 2021, 01:03:21 pm by DavidAlfa »
Hantek DSO2x1x            Drive        FAQ          DON'T BUY HANTEK! (Aka HALF-MADE)
Stm32 Soldering FW      Forum      Github      Donate
 

Offline daqqTopic starter

  • Super Contributor
  • ***
  • Posts: 2302
  • Country: sk
    • My site
Re: STM32F030C8T6 - Best way to measure frequency on GPIO pin without timer?
« Reply #15 on: August 29, 2021, 07:09:20 pm »
Okay, finished the base testing, need to tidy it up, but the code works and the results are actually very good!

So, through external means I got the following: Reference frequency was 1.000 003 6 MHz, actual MCU frequency was 48.003 801 2 MHz. I ran the calibration with the exact reference frequency. The calibration determined the internal frequency to be 48.003 805 488 MHz . A difference from the measurement of only ~90 ppb between the real real and the determined real. So the technique works.

Included is the source code. It's a bit of a mess, a work in progress and will cause your eyes to bleed.

Other info:
- The software 'timer' maxes out a little above 1MHz. The code could probably be further optimized to go higher, but it's not necessary for me now.
- With 60 seconds of integration time, there seems to be little enough overhead to not be a concern
- The code is a tad quirky and will max out at different frequencies depending on the optimization settings and probably the phase of the moon.

The code works as follows:
Disable all interrupts aside from the one (store the interrupt state).
Preload the timer counter with a small value to create a delay.
Wait for the CounterState to increment to CounterState_Running.
After that happens, every time the pin changes its value (this will give you a count on both the rising and falling edge), increment the CalibrationCounter. Do this until CounterState is not CounterState_Done.
Everytime the real timer interrupt occurs, latch the last value of CalibrationCounter into CalibrationCounter_latch and increment CounterState .
Once done, do some simple math and print out the result.
Restore other interrupts.

That's it.

ClockCalibration.c:
Code: [Select]
#include "ClockCalibration.h"

#include <stdio.h>

#include "Miscellany.h"

#include "stm32f0xx_ll_bus.h"
#include "stm32f0xx_ll_tim.h"
#include "stm32f0xx_ll_gpio.h"



//*************************************************************************************************




enum ECounterStates
{
CounterState_NotStarted = 0,
CounterState_Starting,
CounterState_Running,
CounterState_Done
};

static volatile uint32_t CalibrationCounter = 0;
static volatile uint32_t CalibrationCounter_latch = 0;

static volatile uint8_t CounterState = CounterState_NotStarted;

void __ClockCalibration_TimerCallbackFunction(void)
{
LL_TIM_ClearFlag_UPDATE(__ClockCalibration_Timer);
CounterState++;

CalibrationCounter_latch = CalibrationCounter;
}



//*************************************************************************************************

void ClockCalibration_Init(void)
{
LL_TIM_InitTypeDef TIM_InitStruct = {0};


LL_GPIO_InitTypeDef GPIO_InitStruct;

LL_GPIO_StructInit(&GPIO_InitStruct);
GPIO_InitStruct.Mode = LL_GPIO_MODE_INPUT;
GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Pull = LL_GPIO_PULL_UP;
GPIO_InitStruct.Alternate = LL_GPIO_AF_0;


GPIO_InitStruct.Pin = GPIO_AUXIO_Pin;
LL_GPIO_Init(GPIO_AUXIO, &GPIO_InitStruct);

__ClockCalibration_Timer_EnableClock();


NVIC_SetPriority(__ClockCalibration_TimerIRQN, 0);
NVIC_EnableIRQ(__ClockCalibration_TimerIRQN);

TIM_InitStruct.Prescaler = __ClockCalibration_Timer_Prescaler;
TIM_InitStruct.CounterMode = LL_TIM_COUNTERMODE_DOWN;
TIM_InitStruct.Autoreload = __ClockCalibration_Timer_Reload;
TIM_InitStruct.ClockDivision = LL_TIM_CLOCKDIVISION_DIV1;


LL_TIM_Init(__ClockCalibration_Timer, &TIM_InitStruct);
LL_TIM_DisableARRPreload(__ClockCalibration_Timer);
LL_TIM_SetClockSource(__ClockCalibration_Timer, LL_TIM_CLOCKSOURCE_INTERNAL);
LL_TIM_SetTriggerInput(__ClockCalibration_Timer, LL_TIM_TS_ITR0);
LL_TIM_SetSlaveMode(__ClockCalibration_Timer, LL_TIM_SLAVEMODE_DISABLED);
LL_TIM_DisableIT_TRIG(__ClockCalibration_Timer);
LL_TIM_DisableDMAReq_TRIG(__ClockCalibration_Timer);
LL_TIM_SetTriggerOutput(__ClockCalibration_Timer, LL_TIM_TRGO_RESET);
LL_TIM_DisableMasterSlaveMode(__ClockCalibration_Timer);
}

//*************************************************************************************************


uint8_t ClockCalibration_Execute(double ReferenceFrequency)
{
printf("Starting calibration.\r\n");

LL_TIM_SetCounter(__ClockCalibration_Timer, __ClockCalibration_Timer_InitialPause);
LL_TIM_ClearFlag_UPDATE(__ClockCalibration_Timer);
LL_TIM_EnableCounter(__ClockCalibration_Timer);
LL_TIM_EnableIT_UPDATE(__ClockCalibration_Timer);


uint32_t PrevInterruptEnable = NVIC->ISER[0];
NVIC->ICER[0] = 0xFFFFFFFF;

NVIC_EnableIRQ(__ClockCalibration_TimerIRQN);

CounterState = CounterState_Starting;



CalibrationCounter = 0;
uint32_t PinStat = GPIO_AUXIO->IDR & GPIO_AUXIO_Pin;
uint32_t PinStat_prev = PinStat;

while(CounterState != CounterState_Running);

do
{
PinStat = GPIO_AUXIO->IDR & GPIO_AUXIO_Pin;
if(PinStat != PinStat_prev) CalibrationCounter++;
PinStat_prev = PinStat;
}while(CounterState != CounterState_Done);

LL_TIM_DisableCounter(__ClockCalibration_Timer);
LL_TIM_DisableIT_UPDATE(__ClockCalibration_Timer);

NVIC->ISER[0] = PrevInterruptEnable;

printf("Got pulses: %ld .\r\n", CalibrationCounter_latch);


double Frequency_Measured = CalibrationCounter_latch;
Frequency_Measured /= 2.0;
Frequency_Measured /= __ClockCalibration_Duration;

double FreqRatio = ReferenceFrequency / Frequency_Measured;

double FrequencyLocal_Nominal = __ClockCore_FreqNom;
double FrequencyLocal_Real = __ClockCore_FreqNom * FreqRatio;

printf("Results: %f\r\n", Frequency_Measured);
printf("Results: %f\r\n", FreqRatio);
printf("Results: %f\r\n", FrequencyLocal_Nominal);
printf("Results: %f\r\n", FrequencyLocal_Real);
printf("Done.\r\n");


CounterState = CounterState_NotStarted;

return TRUE;

}


//*************************************************************************************************


ClockCalibration.h:
Code: [Select]
#ifndef INC_CLOCKCALIBRATION_H_
#define INC_CLOCKCALIBRATION_H_


//*************************************************************************************************

#include <stdint.h>

//*************************************************************************************************

extern uint8_t ClockCalibration_Execute(double ReferenceFrequency);
extern void ClockCalibration_Init(void);

//*************************************************************************************************


#define GPIO_AUXIO GPIOC
#define GPIO_AUXIO_Pin LL_GPIO_PIN_13


//*************************************************************************************************


#define __ClockCalibration_TimerIRQN TIM3_IRQn
#define __ClockCalibration_Timer_EnableClock() {LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_TIM3);}
#define __ClockCalibration_TimerCallbackFunction TIM3_IRQHandler
#define __ClockCalibration_Timer TIM3

#define __ClockCalibration_Timer_Prescaler 47999 //Timer will be fed 1kHz
#define __ClockCalibration_Timer_Reload 59999 //59999 //Timer will count one minute
#define __ClockCalibration_Timer_InitialPause 100 //Actual counting will commence 100ms after command

#define __ClockCalibration_Duration 60.0


#define __ClockCore_FreqNom 48E6
//*************************************************************************************************



#endif

« Last Edit: August 29, 2021, 07:11:08 pm by daqq »
Believe it or not, pointy haired people do exist!
+++Divide By Cucumber Error. Please Reinstall Universe And Reboot +++
 
The following users thanked this post: thm_w


Share me

Digg  Facebook  SlashDot  Delicious  Technorati  Twitter  Google  Yahoo
Smf