Wobbling signal generated by Output Compare output - Page 1
Forum section: electronics / general

author: ppTRN
author_rank: Regular Contributor
timestamp: April 24, 2025, 09:18:30 am
content: Hi everyone,I'm working on generating two signals: one that is in phase, and another that's delayed by 90° relative to a reference input signal. I am using an STM32F303K8.The code technically works, but the output signals are unstable. I've attached a video showing the issue.I’ve tried reorganizing the code to make it as efficient as possible, but unfortunately, I haven’t seen any improvement. I even considered implementing the logic in assembly, as mentioned in a previous thread, but that approach clearly isn’t feasible.Has anyone encountered similar behavior? I understand I'm not working with a real-time processor or any high-end hardware, but a 400ns jitter at 72MHz suggests a timing uncertainty of about 30 clock cycles—which seems significant.Could the issue be related to the PLL? I'm using a 6MHz crystal oscillator that's boosted to 72MHz. Or could it be tied to the code running inside the ISR? After all, 30 clock cycles can be consumed pretty quickly in there.I'd really appreciate any insights or suggestions on how to improve this. Thanks in advance!CODE THAT GENERATES THE TWO SQUARE WAVES:Code:void TIM1_CC_IRQHandler(){if(TIM1->SR & 1<<1){TIM1->SR = ~(1<<1);// Clear flagif(GPIOA->IDR & 1<<8)GPIOA->BSRR = 1<<(8+16);// Toggle pinelse GPIOA->BSRR = 1<<8;TIM1->CCR1 = TIM1->CCR1 + TXpulseWidth/2;// Update compare match}else if(TIM1->SR & 1<<2){TIM1->SR = ~(1<<2);// Clear flagif(GPIOA->IDR & 1<<8)GPIOA->BSRR = 1<<(9+16);// Toggle pinelse GPIOA->BSRR = 1<<9;TIM1->CCR2 = TIM1->CCR2 + TXpulseWidth/2;// Update compare match}}DISASSEMBLED CODE:Code:0x08000A7C F6424010 MOVW r0,#0x2C100x08000A80 F2C40001 MOVT r0,#0x400153: if(TIM1->SR & 1<<1){54: TIM1->SR = ~(1<<1); // Clear flag55: if(GPIOA->IDR & 1<<8) GPIOA->BSRR = 1<<(8+16); // Toggle pin56: else GPIOA->BSRR = 1<<8;57: TIM1->CCR1 = TIM1->CCR1 + TXpulseWidth/2; // Update compare match58: }0x08000A84 6801 LDR r1,[r0,#0x00]0x08000A86 0789 LSLS r1,r1,#300x08000A88 F2400110 MOVW r1,#0x100x08000A8C F6C40100 MOVT r1,#0x48000x08000A90 D410 BMI 0x08000AB459: else if(TIM1->SR & 1<<2){60: TIM1->SR = ~(1<<2); // Clear flag61: if(GPIOA->IDR & 1<<8) GPIOA->BSRR = 1<<(9+16); // Toggle pin62: else GPIOA->BSRR = 1<<9;63: TIM1->CCR2 = TIM1->CCR2 + TXpulseWidth/2; // Update compare match64: }0x08000A92 6802 LDR r2,[r0,#0x00]0x08000A94 0752 LSLS r2,r2,#2965: }0x08000A96 BF58 IT PL0x08000A98 4770 BX lr0x08000A9A F06F0204 MVN r2,#0x0460: TIM1->SR = ~(1<<2); // Clear flag0x08000A9E F8402B28 STR r2,,#0x2861: if(GPIOA->IDR & 1<<8) GPIOA->BSRR = 1<<(9+16); // Toggle pin62: else GPIOA->BSRR = 1<<9;63: TIM1->CCR2 = TIM1->CCR2 + TXpulseWidth/2; // Update compare match64: }0x08000AA2 680A LDR r2,[r1,#0x00]0x08000AA4 05D2 LSLS r2,r2,#230x08000AA6 F04F7200 MOV r2,#0x20000000x08000AAA BF58 IT PL0x08000AAC F44F7200 MOV r2,#0x2000x08000AB0 E00B B 0x08000ACA0x08000AB2 BF00 NOP0x08000AB4 F06F0202 MVN r2,#0x0254: TIM1->SR = ~(1<<1); // Clear flag0x08000AB8 F8402B24 STR r2,,#0x2455: if(GPIOA->IDR & 1<<8) GPIOA->BSRR = 1<<(8+16); // Toggle pin56: else GPIOA->BSRR = 1<<8;57: TIM1->CCR1 = TIM1->CCR1 + TXpulseWidth/2; // Update compare match58: }59: else if(TIM1->SR & 1<<2){60: TIM1->SR = ~(1<<2); // Clear flag61: if(GPIOA->IDR & 1<<8) GPIOA->BSRR = 1<<(9+16); // Toggle pin62: else GPIOA->BSRR = 1<<9;63: TIM1->CCR2 = TIM1->CCR2 + TXpulseWidth/2; // Update compare match64: }0x08000ABC 680A LDR r2,[r1,#0x00]0x08000ABE 05D2 LSLS r2,r2,#230x08000AC0 F04F7280 MOV r2,#0x10000000x08000AC4 BF58 IT PL0x08000AC6 F44F7280 MOV r2,#0x1000x08000ACA 608A STR r2,[r1,#0x08]0x08000ACC F2400280 MOVW r2,#0x800x08000AD0 F2C20200 MOVT r2,#0x20000x08000AD4 6801 LDR r1,[r0,#0x00]0x08000AD6 6812 LDR r2,[r2,#0x00]0x08000AD8 EB0272D2 ADD r2,r2,r2,LSR #310x08000ADC EB010162 ADD r1,r1,r2,ASR #10x08000AE0 6001 STR r1,[r0,#0x00]65: }

author: ppTRN
author_rank: Regular Contributor
timestamp: April 24, 2025, 09:21:58 am
content: It appears that the GIF is not moving at all. Here's a link to the video:https://youtu.be/5fmkvsD-KEA

author: mikerj
author_rank: Super Contributor
timestamp: April 24, 2025, 10:27:29 am
content: Do you have any other interrupts running, or any code in the main loop that may be disabling interrupts for a brief period?

author: ppTRN
author_rank: Regular Contributor
timestamp: April 24, 2025, 10:57:08 am
content: There are no other interrupts, but I managed to fix it. I now use the toggle on match feature in order to automatically toggle the pin on match and in the ISR I just update the compare values. Also I increased the interrupt priority reducing the jitter to just 50ns.Code:/*This code generates two square waves, one in phase and one in quadraturewith the received signal. They can be shifted by adding a delay, stored in K.TEST: PASSED.PERFORMANCE: 50ns gitter*/#include "demodulatorClocks.h"extern int K;extern int TXpulseWidth;void demodulatorClocksInit(){RCC->APB2ENR |= RCC_APB2ENR_TIM1EN;// TIM1 clock enableRCC->AHBENR |= RCC_AHBENR_GPIOAEN;// GPIOA clock enableGPIOA->MODER &= ~(1<<16);// GPIOA8 in AF modeGPIOA->MODER |= 1<<17;GPIOA->MODER &= ~(1<<18);// GPIOA9 in AF modeGPIOA->MODER |= 1<<19;GPIOA->AFR &= ~(0xF << 0); // Clear PA8GPIOA->AFR |= (0x6 << 0); // AF6 for TIM1_CH1GPIOA->AFR &= ~(0xF << 4); // Clear PA9GPIOA->AFR |= (0x6 << 4); // AF6 for TIM1_CH2TIM1->CR1 |= 0;// Up counterTIM1->CR2 |= 0;// No master modeTIM1->SMCR |= 0;// No slave modeTIM1->DIER |= 1<<1// Interrupt enable on CCR1| 1<<2// Interrupt enable on CCR2| 1<<3// Interrupt enable on CCR3| 1<<4;// Interrupt enable on CCR4TIM1->SR |= 0;// Interrupt flag registerTIM1->EGR |= 0;// Software event generationTIM1->CCMR1 |= (0b011 << 4);// CH1 toggle on matchTIM1->CCMR1 |= (0b011 << 12);// CH2 toggle on matchTIM1->CCMR2 |= 0;// CC2 and CC3 are in output mode with no effect on pinsTIM1->CCER |= 1<<0// CC1 enable| 1<<4// CC2 enable| 1<<8// CC3 enable| 1<<12;// CC4 enableTIM1->CNT = 0;// TIM1 counter valueTIM1->PSC = 0;// Prescaler 1TIM1->ARR = 0xFFFF;// Auto reload register at MAXTIM1->RCR = 0;// Repetition counterTIM1->CCR1 = TXpulseWidth/2;// When to lower ITIM1->CCR2 = TXpulseWidth/4;// When to rise QTIM1->CCR3 = 3*TXpulseWidth/4;// When to lower QTIM1->BDTR |= TIM_BDTR_MOE;// OUTPUT COMPARE OUTPUT ENABLENVIC_SetPriority(TIM1_CC_IRQn, 1);NVIC_EnableIRQ(TIM1_CC_IRQn);}void TIM1_CC_IRQHandler(){if(TIM1->SR & 1<<1){TIM1->SR &= ~(1<<1);// Clear flagTIM1->CCR1 = TIM1->CCR1 + TXpulseWidth/2;// Update compare match}else if(TIM1->SR & 1<<2){TIM1->SR &= ~(1<<2);// Clear flagTIM1->CCR2 = TIM1->CCR2 + TXpulseWidth/2;// Update compare match}}

author: radiolistener
author_rank: Super Contributor
timestamp: April 24, 2025, 12:32:46 pm
content: try something like this:Code:void handleCC1IF(void) {uint32_t shiftAdd = (GPIOA->IDR >> 4) & 0x10; // Based on PA8 stateGPIOA->BSRR = 1 << (8 + shiftAdd); // Toggle PA8TIM1->CCR1 += TXpulseWidth / 2;TIM1->SR &= ~1; // Clear CC1IF}void handleCC2IF(void) {uint32_t shiftAdd = (GPIOA->IDR >> 4) & 0x10; // Based on PA8 stateGPIOA->BSRR = 1 << (9 + shiftAdd); // Toggle PA9 in sync with PA8TIM1->CCR2 += TXpulseWidth / 2;TIM1->SR &= ~2; // Clear CC2IF}void handleError(void) {// Handle the unexpected case (maybe log or assert)}void TIM1_CC_IRQHandler() {// Function pointer table for handling CC1IF and CC2IFvoid (*handlers[])(void) = { handleError, handleCC1IF, handleCC2IF, handleError };handlers[TIM1->SR & 3](); // Call the corresponding handler}

author: wek
author_rank: Frequent Contributor
timestamp: April 24, 2025, 09:47:48 pm
content: As you've found out already, it's always better to use hardware when it comes to timing.Also, not directly related to your initial question, but don't RMW TIM_SR. Strangely enough, you had it correctly in your first post... (moreover, |= into SR is straight wrong).JW

author: PCB.Wiz
author_rank: Super Contributor
timestamp: April 24, 2025, 10:06:58 pm
content: Quote from: ppTRN on April 24, 2025, 10:57:08 amYes, this is the least jitter / 'correct' approach.Jitter occurs in any software pin toggle as the time to arrive at that line of code varies.If you have other interrupts that are long, and the next-compare update is high frequency, then interrupt priority can be required.The granularity should be 1 Peripheral CLK, and there should not be any added jitter? What is your sampling jitter here?eg if your edge distance is 2000 perclks, it should always be exactly 2000 perclks, as HW sets the toggle distance.

author: ozcar
author_rank: Frequent Contributor
timestamp: April 25, 2025, 06:29:04 am
content: Quote from: ppTRN on April 24, 2025, 10:57:08 amThe code as presented does not reference K. Maybe that part is yet to be added, but how is the first output kept in phase with the received signal at all?

author: ppTRN
author_rank: Regular Contributor
timestamp: April 25, 2025, 08:25:36 am
content: In the code I posted K and TXpulseWidth are not shown because are defined and updated in another file. The first output is in phase with the reference signal because one of its edges triggers the timer 1, which then provides the two square signals. K gives me a way to delay the generation of both in phase and quadrature signals in respect to the reference signal, while TXpulseWidth gives me infos about the precise period of the reference signal.In the video I posted in a previous answare you can see the two generated signal. The reference signal is not shown because I have just a 2 ch oscilloscope, but I used it as esternal trigger. The lag between the trigger and the square wave generation is due to K, which in fact allows me to shift both square waves at the same time.

author: ozcar
author_rank: Frequent Contributor
timestamp: April 25, 2025, 10:45:58 am
content: A two channel scope - what luxury! For years, well decades actually, I got by with a single channel CRO, so I learned all the tricks making use of the trigger input. At least you are able to see the received signal and one of your generated signals at the same time.It must be doing what you want, but I'm always keen to learn something new.How does the external signal "trigger" timer 1? I can see that demodulatorClocksInit() does not enable the TIM1 counter (via CEN bit in CR1), so presumably something must turn that on. You also said that you don't have any other interrupt routines, so that leaves one less possible way of doing that.

author: ppTRN
author_rank: Regular Contributor
timestamp: April 26, 2025, 08:03:02 am
content: Hi,in order to make it clearer to you I draw a simple schematic of how the system works. Also, vou can find the whole code attached.Code:/*File name: TXfrequencyMeasure.cThis code reads the TX signal and automatically updates TXpulseWidthwith the period of the signal. It is possible to evaluate the TXfrequency with a desired step, stored in REP. It means that theactual measurement is taken only after REP periods. It is uselessto measure it every cycle since if it changes, it changes very slowly.TEST: PASSED.*/#include "TXfrequencyMeasure.h"#define REP 100#define CAPTURE_BUFFER_SIZE 2// One for rising and one for falling edgevolatile uint32_t capture_buffer;// DMA bufferextern int TXpulseWidth;// Pulse width in ticksextern int K;extern int nextI, nextQ;int n = 0;void TXfrequencyMeasureInit(void){RCC->AHBENR |= RCC_AHBENR_GPIOAEN;// GPIOA clock enableRCC->APB1ENR |= RCC_APB1ENR_TIM2EN;// TIM2 clock enableRCC->AHBENR |= RCC_AHBENR_DMA1EN;// DMA clock enableGPIOA->MODER &= ~(3U << (0 * 2));// GPIOA0 in alternate functionGPIOA->MODER |= (2U << (0 * 2));GPIOA->AFR &= ~(0xF << (0 * 4));// Alternate function is TIM2 CHANNEL 1GPIOA->AFR |= (1U << (0 * 4));TIM2->PSC = 0;// No prescaler for maximum resolutionTIM2->ARR = 0xFFFFFFFF;// Auto reload set to MAXTIM2->CCMR1 &= ~TIM_CCMR1_CC1S;// CC1 channel is input, mapped on TI1TIM2->CCMR1 |= TIM_CCMR1_CC1S_0;TIM2->CCER &= ~(TIM_CCER_CC1P);// Sensitive to rising edgeTIM2->CCER |= TIM_CCER_CC1E;// Enable Input Capture CH1TIM2->DIER |= TIM_DIER_CC1DE;// Enable DMA request on CH1 captureTIM2->CR1 |= TIM_CR1_CEN;// TIM2 starts// --------------------------// Configure DMA1 Channel 5// TIM2_CH1 DMA is on DMA1_Channel5// --------------------------DMA1_Channel5->CPAR = (uint32_t)&TIM2->CCR1;DMA1_Channel5->CMAR = (uint32_t)capture_buffer;DMA1_Channel5->CNDTR = CAPTURE_BUFFER_SIZE;DMA1_Channel5->CCR =DMA_CCR_MINC | // Memory incrementDMA_CCR_PSIZE_1 | // Peripheral size: 32-bitDMA_CCR_MSIZE_1 | // Memory size: 32-bitDMA_CCR_CIRC | // Circular modeDMA_CCR_PL_1 | // High priorityDMA_CCR_EN; // Enable DMA channelDMA1_Channel5->CCR |= DMA_CCR_TCIE;// Enable transfer complete interruptNVIC_EnableIRQ(DMA1_Channel5_IRQn);}void DMA1_Channel5_IRQHandler(void){if(DMA1->ISR & DMA_ISR_TCIF5){DMA1->IFCR |= DMA_IFCR_CTCIF5;// Clear transfer complete flagif(n>=REP){TIM1->CCR1 = K;// When to rise ITIM1->CCR2 = TXpulseWidth/4+K;// When to rise Quint32_t t1 = capture_buffer;// Assign the values to two variablesuint32_t t2 = capture_buffer;if (t2 >= t1) TXpulseWidth = t2 - t1;// If we did not overflow the timer the width is the difference of timeselse TXpulseWidth = (0xFFFFFFFF - t1 + t2 + 1);// If we overflew it, we fix it}else{if(n<REP) n++;else n = 0;}TIM1->CNT = 0;// Reset TIM1TIM1->CR1 |= 1<<0;// Start TIM1}}Code:/*File name: demodulatorClocks.cThis code generates two square waves, one in phase and one in quadraturewith the received signal. They can be shifted by adding a delay, stored in K.TEST: PASSED.PERFORMANCE: 50ns gitter*/#include "demodulatorClocks.h"extern int K;extern int TXpulseWidth;void demodulatorClocksInit(){RCC->APB2ENR |= RCC_APB2ENR_TIM1EN;// TIM1 clock enableRCC->AHBENR |= RCC_AHBENR_GPIOAEN;// GPIOA clock enableGPIOA->MODER &= ~(1<<16);// GPIOA8 in AF modeGPIOA->MODER |= 1<<17;GPIOA->MODER &= ~(1<<18);// GPIOA9 in AF modeGPIOA->MODER |= 1<<19;GPIOA->AFR &= ~(0xF << 0); // Clear PA8GPIOA->AFR |= (0x6 << 0); // AF6 for TIM1_CH1GPIOA->AFR &= ~(0xF << 4); // Clear PA9GPIOA->AFR |= (0x6 << 4); // AF6 for TIM1_CH2TIM1->CR1 |= 0;// Up counterTIM1->CR2 |= 0;// No master modeTIM1->SMCR |= 0;// No slave modeTIM1->DIER |= 1<<1 | 1<<2;// Interrupt enable on CC1 and CC2TIM1->SR |= 0;// Interrupt flag registerTIM1->EGR |= 0;// Software event generationTIM1->CCMR1 |= (0b011 << 4);// CH1 toggle on matchTIM1->CCMR1 |= (0b011 << 12);// CH2 toggle on matchTIM1->CCMR2 |= 0;// CC2 and CC3 are in output mode with no effect on pinsTIM1->CCER |= 1<<0 | 1<<4;// CC1 and CC2 channel enableTIM1->CNT = 0;// TIM1 counter valueTIM1->PSC = 0;// Prescaler 1TIM1->ARR = 0xFFFF;// Auto reload register at MAXTIM1->RCR = 0;// Repetition counterTIM1->CCR1 = TXpulseWidth/2;// When to lower ITIM1->CCR2 = TXpulseWidth/4;// When to rise QTIM1->CCR3 = 3*TXpulseWidth/4;// When to lower QTIM1->BDTR |= TIM_BDTR_MOE;// OUTPUT COMPARE OUTPUT ENABLENVIC_SetPriority(TIM1_CC_IRQn, 1);NVIC_EnableIRQ(TIM1_CC_IRQn);}void TIM1_CC_IRQHandler(){if(TIM1->SR & 1<<1){TIM1->SR &= ~(1<<1);// Clear flagTIM1->CCR1 = TIM1->CCR1 + TXpulseWidth/2;// Update compare match}else if(TIM1->SR & 1<<2){TIM1->SR &= ~(1<<2);// Clear flagTIM1->CCR2 = TIM1->CCR2 + TXpulseWidth/2;// Update compare match}}As you can see TIM1 is started by the DMA interrupt that provides the measure of the input signal frequency

author: ozcar
author_rank: Frequent Contributor
timestamp: April 27, 2025, 06:26:26 am
content: Thanks for providing that.So, you do have another interrupt routine, but if things work correctly, the DMA and timer interrupts should be separated by your "K" offset, so should not interfere with each other.I tried it on a STM32F303 "Black Pill" board, with CPU and timers running at 72MHz. Not knowing what frequency you are working with, I used a 20kHz input. It looked like it was working, but I did notice a few things.Occasionally, the "in phase" output on PA8 would get inverted for a while. I managed to capture it changing over from being "OK" to "NOT OK" - see below. But then maybe I messed something up in getting it to work on my board, or maybe just running at different cycle time upsets something.Secondly, the comments in the code say that your intention is to measure the period of the input waveform only every "REP" times, with REP set at 100 in the code as posted. However, "n" will count up to 100 and then get stuck at that value:Code:if(n>=REP){(calculate new TXpulseWidth etc)}else{if(n<REP) n++;else n = 0;}So from then onwards, it does the calculation every time. I tried changing it so that it really only does that processing every 100th time through, and that totally messed things up! It was just not working at all for even values of REP much smaller than 100. I think that setting the timer counter to zero after it has been free-running for a few cycles could be to blame. Note that the DMA interrupt only occurs every second cycle of the input, so if you did want to measure the period of each cycle of the input signal, you'd have to turn on the interrupt for DMA transfer half-complete as well.So, it may appear to be working, but I've got a feeling that there could be something there that will come out to bite you at some point.

author: ppTRN
author_rank: Regular Contributor
timestamp: April 27, 2025, 12:51:23 pm
content: Thank you for taking such interest.1. Yes there is actually another ISR, and several other will be implemented. Given the timing syncronization of the system, they are expected to fire at certain time instant such that they should not interfere to much with one another. That's why I neglected the DMA interrupt.2. The typical frequencies goes from 3 to 25kHz;3. I also noticed the strange inversion of the in phase signal. I have no idea why it happends but after resetting it behaves correctly. I suspect this happend when the input signals is subjected to a big frequency change, which will not normally happend;4. Yes, there is a bug that makes the measurement of the input period continous. If correctly implemented it does not work because it does not set the first threshold for the compare register of TIM1. Thank you for spotting it, I just fixed it and it works quite well, expecially for REP = 100 to 500. For higher values i find that it takes too much to adapt to the changes in the signalCode:void DMA1_Channel5_IRQHandler(void){if(DMA1->ISR & DMA_ISR_TCIF5){DMA1->IFCR |= DMA_IFCR_CTCIF5;// Clear transfer complete flagTIM1->CCR1 = K;// When to rise ITIM1->CCR2 = TXpulseWidth/4+K;// When to rise Qif(n>=REP){n = 0;uint32_t t1 = capture_buffer;// Assign the values to two variablesuint32_t t2 = capture_buffer;if (t2 >= t1) TXpulseWidth = t2 - t1;// If we did not overflow the timer the width is the difference of timeselse TXpulseWidth = (0xFFFFFFFF - t1 + t2 + 1);// If we overflew it, we fix it}else n++;TIM1->CNT = 0;// Reset TIM1TIM1->CR1 |= 1<<0;// Start TIM1}}Yet another change I made was to make te input capture for the input frequency measure sensitive to the falling edge. It is just more convenient for the kind of proccess that is made afterward. So now the Inphase signal is actually out of phase with the input one

author: ozcar
author_rank: Frequent Contributor
timestamp: April 28, 2025, 08:17:53 pm
content: I look at things like this wondering "How would I do that?". Or maybe just as a puzzle in the same way some people might try to solve Sudoku, but the difference with something like this is that when you start you don't know that there even is a solution.Using the toggle mode with the interrupt routine just continually incrementing the CCR values looks like a good way to keep two (or more) output signals in locked phase, as long as the frequency is not too high, as is the case here. I had not seen that before, but when I go looking now, I do see this suggested elsewhere. It does worry me a bit that when using toggle mode, maybe you could lose track of whether the output is high or low, and I was wondering if that could somehow be why the one output occasionally got inverted.For what it is worth, my test signal was generated by PWM on another STM32, and seems pretty stable. If I set TIM1 going and never touch it again in the DMA interrupt, I can see the two outputs maintain the phase difference for as long as I like, but with nothing to maintain the phase relationship with the input signal, the DSO shows the two output signals drifting slowly relative to the input. The DSO said that the input signal was 20001.1Hz, and the generated signals were 20001.5Hz.Locking the phase of the outputs to the input signal seems to be the tricky bit. For me, even with the DMA interrupt adjusting the TIM1 CNT and CCR values every time (so once for every two cycles of the input), I was seeing strange things happening.I was thinking about trying to gradually steer the outputs in the right direction. To that end, I was contemplating using another channel on TIM1 to capture the input signal instead of using another timer. That might make it a bit easier to know what CCR values are required to lock the outputs to the input. I have not had a chance to look at that though.