STM32F030C8T6 - Best way to measure frequency on GPIO pin without timer? - Page 1
Forum section: electronics / general

author: daqq
author_rank: Super Contributor
timestamp: August 11, 2021, 07:18:16 pm
content: 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:CountingDone_flag = 0; //Reset global volatile flagCounter = 0; //Reset global volatile 32 bit counterCounterCapture = 0; //Reset global volatile 32 bit counter capture registerStartTimer(); //Start a timerdo{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:volatile uint32_t CounterCapture = 0;void TIM3_IRQHandler(void){CounterCapture = Counter; //Copy the current state of the counter hereCountingDone_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

author: Kleinstein
author_rank: Super Contributor
timestamp: August 11, 2021, 09:07:40 pm
content: 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.

author: langwadt
author_rank: Super Contributor
timestamp: August 11, 2021, 09:27:30 pm
content: if it runs off a crystal how much do you expect it to be off?

author: PCB.Wiz
author_rank: Super Contributor
timestamp: August 11, 2021, 10:06:19 pm
content: Quote from: daqq on August 11, 2021, 07:18:16 pmExternal 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 moduleBetter 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)Youcanuse a timer interrupt to extend the 16b, but keep in mindyou 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 gives48M = 48000000 = 0x02DC_6C0048M*(1+20u) = 0x02DC_6FC048M*(1-20u) = 0x02DC_6840Notice here that upper 16 bits do not change, and the ±20ppm span is from 0x6840 to 0x6FC0, comfortably less than the 16b timer resolutionYoucoulduse more than a single 1pps to calibrate, but that would only really be needed if you had a TCXO sysclk.

author: daqq
author_rank: Super Contributor
timestamp: August 15, 2021, 05:48:07 pm
content: 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 agoThe crystal should not be miles off, that is true.Quote20ppm 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.

author: DavidAlfa
author_rank: Super Contributor
timestamp: August 15, 2021, 06:33:05 pm
content: Use the DWT counter, runs at CPU speed, so has the maximum precision available, and it's 32-bit.DWT->CTRL |= 1 ; // enable the counterDWT->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.

author: thm_w
author_rank: Super Contributor
timestamp: August 16, 2021, 09:18:04 pm
content: Quote from: DavidAlfa on August 15, 2021, 06:33:05 pmF0 doesn't have it, but interesting feature for me to try out.https://stackoverflow.com/questions/36378280/stm32-how-to-enable-dwt-cycle-counterhttps://community.st.com/s/question/0D50X0000BddEDG/stm32f030-microsecond-delayhttps://visualgdb.com/tutorials/arm/chronometer/

author: langwadt
author_rank: Super Contributor
timestamp: August 16, 2021, 09:50:48 pm
content: 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

author: abyrvalg
author_rank: Frequent Contributor
timestamp: August 19, 2021, 11:55:24 am
content: 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.

author: DavidAlfa
author_rank: Super Contributor
timestamp: August 19, 2021, 09:39:03 pm
content: 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?

author: abyrvalg
author_rank: Frequent Contributor
timestamp: August 19, 2021, 11:07:04 pm
content: Who says itmustbe 10MHz? There are suggestions like 1pps above.Measuring a 10MHzperiodon a 48MHz MCU would be a bad idea even with a timer.

author: PCB.Wiz
author_rank: Super Contributor
timestamp: August 19, 2021, 11:54:18 pm
content: Quote from: daqq on August 15, 2021, 05:48:07 pmIf 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

author: DavidAlfa
author_rank: Super Contributor
timestamp: August 20, 2021, 10:30:12 am
content: Quote from: abyrvalg on August 19, 2021, 11:07:04 pmRead 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.

author: abyrvalg
author_rank: Frequent Contributor
timestamp: August 20, 2021, 10:44:46 am
content: I would advise the same, tioThe calibration reference signal is not built into the system, OP will applysomethingto 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.

author: DavidAlfa
author_rank: Super Contributor
timestamp: August 20, 2021, 12:29:53 pm
content: 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.

author: daqq
author_rank: Super Contributor
timestamp: August 29, 2021, 07:09:20 pm
content: 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 ~90ppbbetween 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:#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;NVIC->ICER = 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 = 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:#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