Skip to main content
Associate III
January 27, 2026
Solved

Timer synchronisation

  • January 27, 2026
  • 15 replies
  • 598 views

I am using a STM32G4 MCU and STM32CubeIDE. 

I want to synchronise two timers (TIM3 and TIM15) that are generating PWM signals so that they both start at the same time. 

TIM3 is set to 250 kHz and the PWM is modulated via DMA on channel 1.  The modulated signal is a 10 kHz sine wave.

TIM15 is set to 10 kHz and the PWM is a simple 50% duty cycle.

I need the timers to start together to ensure that the PW Modulated 10 kHz sine wave (TIM3) is synchronised to the 10 kHz PWM (TIM15).

I thought it would be straight forward enough. 

I have configured TIM15 as master.  In CubeMX I have set Master/Slave Mode (MSM bit) to "Enable (Trigger delayed for master/slaves simultaneous start)" and Trigger Event Selection to "Enable (CNT_EN)".

I have configured TIM3 as a slave.  In CubeMX I have set Slave Mode to "Trigger Mode" and Trigger Source to "ITR6" (per the reference manual).

To the CubeIDE generated code I have added:

 /* USER CODE BEGIN 2 */
 HAL_TIM_PWM_Start_DMA(&htim3, TIM_CHANNEL_1, BDPos_CCRValue_Buffer, CCRValue_BufferSize);
 HAL_TIM_PWM_Start(&htim15, TIM_CHANNEL_1);

 /* USER CODE END 2 */

BDPos_CCRValue_Buffer and CCRValue_BufferSize are a pointer to the sine wave data buffer and the buffer size.

When I run the code the timers are not synchronised.  After debugging through the code I found that TIM3 starts immediately on executing HAL_TIM_PWM_Start_DMA and does not wait for TIM15 to be started.  TIM15 starts when this code is executed:

 /* Enable the Capture compare channel */
 TIM_CCxChannelCmd(htim->Instance, Channel, TIM_CCx_ENABLE);
void TIM_CCxChannelCmd(TIM_TypeDef *TIMx, uint32_t Channel, uint32_t ChannelState)
{
 uint32_t tmp;

 /* Check the parameters */
 assert_param(IS_TIM_CC1_INSTANCE(TIMx));
 assert_param(IS_TIM_CHANNELS(Channel));

 tmp = TIM_CCER_CC1E << (Channel & 0x1FU); /* 0x1FU = 31 bits max shift */

 /* Reset the CCxE Bit */
 TIMx->CCER &= ~tmp;

 /* Set or reset the CCxE Bit */
 TIMx->CCER |= (uint32_t)(ChannelState << (Channel & 0x1FU)); /* 0x1FU = 31 bits max shift */
}

TIM15 starts when the last line is executed (ie after /* Set or reset the CCxE Bit */).

Is there a way to make TIM3 wait for TIM15 to start?

Thanks.

 

Best answer by Repwoc

In summary, when using CubeMX + HAL or LL:

1. To ensure that a slave timer starts when the master timer starts, the master timer must be initialised before the slave timer is initialised (ie MX_TIMmaster_Init() must run before MX_TIMslave_Init()) and the slave timer must be started before the master timer (eg with HAL_TIM_PWM_Start());

2. To align the 10 kHz PWM from TIM15 (slave) with the 10 kHz PW Modulated sine wave from TIM3 (master)  I set the TIM3 (master) Trigger Event Selection TRGO to "Update Event" to ensure that the slave timer started at the same time as the first DMA request to get sine wave data.

15 replies

RepwocAuthor
Associate III
January 27, 2026

Sorry there is a typo - should be:

TIM3 starts when this code is executed:

waclawek.jan
Super User
January 27, 2026

The main point of synchronization is, that in the slave timer, you are not supposed to set TIMx_CR1.CEN in software, but you should leave it cleared (i.e. at 0), and it has to be set by the hardware through the slave-mode controller set to Trigger mode, when TRGI arrives to it from the master.

Also, the master timer won't generate TRGO, if it's set to generate TRGO upon Trigger in TIMx_CR2.MMS, but it is not Triggered (through its slave-mode controller, from some other master).

I don' use Cube/HAL. Those functions may or may not do what you need; they are open source so you can find out yourself.

JW

RepwocAuthor
Associate III
January 27, 2026

-- in the slave timer, you are not supposed to set TIMx_CR1.CEN in software

I'm not.

TDK
Super User
January 27, 2026

Should be working. The correct checks are done in HAL. The timer is only enabled if slave mode is not enabled.

stm32g4xx-hal-driver/Src/stm32g4xx_hal_tim.c at 788fe2faa8b83e730bd7baf1024c9aa05be1d657 · STMicroelectronics/stm32g4xx-hal-driver

 

After HAL_TIM_PWM_Start_DMA is called, is the CEN bit set?

If you comment out "HAL_TIM_PWM_Start(&htim15" you still see a PWM signal on TIM3?

"If you feel a post has answered your question, please click ""Accept as Solution""."
RepwocAuthor
Associate III
January 27, 2026

>> After HAL_TIM_PWM_Start_DMA is called, is the CEN bit set?

Yes.  It is set before running HAL_TIM_PWM_Start_DMA and after, ie it doesn't change.

>> If you comment out "HAL_TIM_PWM_Start(&htim15" you still see a PWM signal on TIM3?

Yes.  The signal on TIM3 starts before HAL_TIM_PWM_Start(&htim15 executes.

When HAL_TIM_PWM_Start_DMA(&htim3 is executed the signal starts on TIM3 when the  TIMx->CCER bit 0 (CC1E) is set in TIM_CCxChannelCmd().

 

TDK
Super User
January 27, 2026

> It is set before running HAL_TIM_PWM_Start_DMA

So where does it get set? Step through your code and find out. It is not enabled at reset.

"If you feel a post has answered your question, please click ""Accept as Solution""."
RepwocAuthor
Associate III
January 27, 2026

If I reset the CEN bit before HAL_TIM_PWM_Start_DMA executes then the signal from TIM3 does not start until TIM15 is started.  However it is not synchronised with TIM 15.

 

RepwocAuthor
Associate III
January 27, 2026

>> So where does it get set? Step through your code and find out.

Do you mean where in the HAL code?  I don't know yet.  The only code I have added are the two lines to start the timers.  

RepwocAuthor
Associate III
January 27, 2026

Weirdly the CEN bit for TIM3 gets set in MX_TIM15_Init().

It happens during the call to 

HAL_TIM_PWM_Init(&htim15)

- TIM_Base_SetConfig(htim->Instance, &htim->Init)

-- TIM_Base_SetConfig()

/**
 * @brief Time Base configuration
 * @PAram TIMx TIM peripheral
 * @PAram Structure TIM Base configuration structure
 * @retval None
 */
void TIM_Base_SetConfig(TIM_TypeDef *TIMx, const TIM_Base_InitTypeDef *Structure)
{
 uint32_t tmpcr1;
 tmpcr1 = TIMx->CR1;

 /* Set TIM Time Base Unit parameters ---------------------------------------*/
 if (IS_TIM_COUNTER_MODE_SELECT_INSTANCE(TIMx))
 {
 /* Select the Counter Mode */
 tmpcr1 &= ~(TIM_CR1_DIR | TIM_CR1_CMS);
 tmpcr1 |= Structure->CounterMode;
 }

 if (IS_TIM_CLOCK_DIVISION_INSTANCE(TIMx))
 {
 /* Set the clock division */
 tmpcr1 &= ~TIM_CR1_CKD;
 tmpcr1 |= (uint32_t)Structure->ClockDivision;
 }

 /* Set the auto-reload preload */
 MODIFY_REG(tmpcr1, TIM_CR1_ARPE, Structure->AutoReloadPreload);

 TIMx->CR1 = tmpcr1;

 /* Set the Autoreload value */
 TIMx->ARR = (uint32_t)Structure->Period ;

 /* Set the Prescaler value */
 TIMx->PSC = Structure->Prescaler;

 if (IS_TIM_REPETITION_COUNTER_INSTANCE(TIMx))
 {
 /* Set the Repetition Counter value */
 TIMx->RCR = Structure->RepetitionCounter;
 }

 /* Generate an update event to reload the Prescaler
 and the repetition counter (only for advanced timer) value immediately */
 TIMx->EGR = TIM_EGR_UG;

 /* Check if the update flag is set after the Update Generation, if so clear the UIF flag */
 if (HAL_IS_BIT_SET(TIMx->SR, TIM_FLAG_UPDATE))
 {
 /* Clear the update flag */
 CLEAR_BIT(TIMx->SR, TIM_FLAG_UPDATE);
 }
}

TIM3->CR1.CEN is set at line 46.

 

TDK
Super User
January 27, 2026

For anything but the most basic timer setups, I avoid using HAL. You'll have to tweak the initialization if it isn't quite doing what you want. That said, if you reset CEN and CNT, it may start up how you want. Hard to know what you're seeing vs what you expect to see.

"If you feel a post has answered your question, please click ""Accept as Solution""."
RepwocAuthor
Associate III
January 27, 2026

Well I expected (hoped) that the slave timer would start at the same time as the master timer so that my signals would be aligned.  Is that to much to expect?  If it is then I'll stop trying now and find a different solution.

RepwocAuthor
Associate III
January 28, 2026

I tried switching to the LL libraries instead of HAL.  It does exactly the same thing - sets the TIM3 CEN bit - during MX_TIM15_Init():

MX_TIM15_Init(void)

-> LL_TIM_Init(TIM15, &TIM_InitStruct)

--> LL_TIM_GenerateEvent_UPDATE(TIMx):

__STATIC_INLINE void LL_TIM_GenerateEvent_UPDATE(TIM_TypeDef *TIMx)
{
 SET_BIT(TIMx->EGR, TIM_EGR_UG);
}

This one line (SET_BIT(TIMx->EGR, TIM_EGR_UG)) updates several TIM3 registers but not any TIM15 registers.  It updates:

TIM3->CR1.CEN bit set

TIM3->SR.TIF, SR.CC4IF, SR.CC3IF, SR.CC2IF, SR.CC1IF bits set

TIM3->CNT.CNT set to 0x10c

TIM3->DMAR.DMAR bit set

Strangely it seems not to change the TIM15->EGR which is referenced in this line.  TIM_EGR_UG is an unsigned long with value = 1.

I'm not sure what is going on here.

 

RepwocAuthor
Associate III
January 28, 2026

Eventually I found (with the help of my AI buddy) the definition for the SET_BIT macro:

#define SET_BIT(REG, BIT) ((REG) |= (BIT))

So all it's doing is ORing the referenced register with the 2nd argument.  The referenced register is definitely TIM15->EGR, TIM_EGR_UG is 1 so I would expect bit 0 of TIM15->EGR to be 1 after this executes. But it isn't and several registers in TIM3 get updated instead.  What's going on?

RepwocAuthor
Associate III
January 28, 2026

According to the reference manual (RM0440):

30.7.6 TIM15 event generation register (TIM15_EGR)

Bit 0 UG: Update generation
This bit can be set by software, it is automatically cleared by hardware.
0:No action
1:Reinitialize the counter and generates an update of the registers. Note that the prescaler
counter is cleared too (anyway the prescaler ratio is not affected).

Does anyone know what "update of the registers" means in practice?

It doesn't say anything about updating the registers of other timers.