cancel
Showing results for 
Search instead for 
Did you mean: 

Creating a "Delay by x microseconds" function using TIM6

kj.obara
Associate III

Hi, I'm trying to understand what I'm doing wrong that the following function seems to delay by anywhere between 667ns and 5.67us even though it's being called as Delay_us(4)

I'm using Nucleo STM32F446RE

void KJO_TIM6_Init(uint16_t arel)
{
	LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_TIM6);
	LL_TIM_InitTypeDef tim6init = {0};
 
	//clock on APB1 is 40MHz so we need to reduce it by 40
	//tim6init.ClockDivision = LL_TIM_CLOCKDIVISION_DIV1;
	tim6init.Prescaler = 40;
	tim6init.Autoreload = arel;
	//tim6init.CounterMode = LL_TIM_COUNTERMODE_DOWN;
 
	LL_TIM_Init(TIM6, &tim6init);
	LL_TIM_SetOnePulseMode(TIM6, LL_TIM_ONEPULSEMODE_SINGLE);
	LL_TIM_ClearFlag_UPDATE(TIM6);
}
 
void KJO_Delay_us(uint16_t delay)
{
	LL_TIM_SetAutoReload(TIM6, delay);
	LL_TIM_SetPrescaler(TIM6, 40); //it's cleared on each reload
	LL_TIM_EnableCounter(TIM6); //one-pulse mode disables the counter
	while(!LL_TIM_IsActiveFlag_UPDATE(TIM6));
	LL_TIM_ClearFlag_UPDATE(TIM6);
}

The SysClk clock was configured to 160MHz using code generated with CubeMX, so as I understand TIM6 should take APB1 frequency which is 40MHz, prescale it by 40 to effectively give 1 MHz or 1us delay.

void SystemClock_Config(void)
{
  LL_FLASH_SetLatency(LL_FLASH_LATENCY_5);
 
  if(LL_FLASH_GetLatency() != LL_FLASH_LATENCY_5)
  {
  Error_Handler();
  }
  LL_PWR_SetRegulVoltageScaling(LL_PWR_REGU_VOLTAGE_SCALE1);
  LL_PWR_DisableOverDriveMode();
  LL_RCC_HSI_SetCalibTrimming(16);
  LL_RCC_HSI_Enable();
 
   /* Wait till HSI is ready */
  while(LL_RCC_HSI_IsReady() != 1)
  {
 
  }
  LL_RCC_PLL_ConfigDomain_SYS(LL_RCC_PLLSOURCE_HSI, LL_RCC_PLLM_DIV_8, 160, LL_RCC_PLLP_DIV_2);
  LL_RCC_PLL_Enable();
 
   /* Wait till PLL is ready */
  while(LL_RCC_PLL_IsReady() != 1)
  {
 
  }
  LL_RCC_SetAHBPrescaler(LL_RCC_SYSCLK_DIV_1);
  LL_RCC_SetAPB1Prescaler(LL_RCC_APB1_DIV_8);
  LL_RCC_SetAPB2Prescaler(LL_RCC_APB2_DIV_2);
  LL_RCC_SetSysClkSource(LL_RCC_SYS_CLKSOURCE_PLL);
 
   /* Wait till System clock is ready */
  while(LL_RCC_GetSysClkSource() != LL_RCC_SYS_CLKSOURCE_STATUS_PLL)
  {
 
  }
  LL_SetSystemCoreClock(160000000);
 
   /* Update the time base */
  if (HAL_InitTick (TICK_INT_PRIORITY) != HAL_OK)
  {
    Error_Handler();
  };
  LL_RCC_SetTIMPrescaler(LL_RCC_TIM_PRESCALER_TWICE);
}

My function is used to toggle a GPIO PIN to enable an LCD, but according to logic analyser it never time right.

Pin is configured this way

  GPIO_InitStruct.Pin = LCD_RS_PIN|LCD_RW_PIN|LCD_EN_PIN;
  GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
  GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_MEDIUM;
  GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
  //GPIO_InitStruct.Pull = LL_GPIO_PULL_DOWN;
  LL_GPIO_Init(GPIOB, &GPIO_InitStruct);

Does this approach even makes sense? I've seen other suggestions for creating microsecond delay functions, but I'm just trying to understand here how timers work. I used to use Atmel AVR chips which were much simpler than this one and I'm just lost.

Thanks in advance.

1 ACCEPTED SOLUTION

Accepted Solutions
kj.obara
Associate III

Hi guys,

So I made a small change to SystemClock_Config() to replace HSI with HSE

That includes modifying line 19 in that function to LL_RCC_PLL_ConfigDomain_SYS(LL_RCC_PLLSOURCE_HSE, LL_RCC_PLLM_DIV_4, 160, LL_RCC_PLLP_DIV_2);

Since HSI was 16MHz and HSE is just 8MHz.

void KJO_TIM6_Init()
{
	LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_TIM6);
	LL_TIM_InitTypeDef tim6init = {0};
 
	tim6init.Prescaler = 0;
	tim6init.Autoreload = 1;
	LL_TIM_Init(TIM6, &tim6init);
	LL_TIM_SetOnePulseMode(TIM6, LL_TIM_ONEPULSEMODE_SINGLE);
	LL_TIM_ClearFlag_UPDATE(TIM6);
}
 
void KJO_Delay_us(uint16_t delay)
{
	if (delay > 1638) //1638*40 < 2^16-1
	{
		HAL_Delay(2);
		return;
	}
 
//clock on APB1 is 20 MHz and timers are 2x that so 40MHz so need to reduce it by 40
	LL_TIM_SetAutoReload(TIM6, delay  * 40 - 1);
	LL_TIM_EnableCounter(TIM6); //one-pulse mode disables the counter
	while(!LL_TIM_IsActiveFlag_UPDATE(TIM6));
	LL_TIM_ClearFlag_UPDATE(TIM6);
}

Here's the status of TIM6 registers on first entry to Delay_us function with delay = 5 [us]0693W000001qHSAQA2.png

On the exit from the function (before executing clear UIF) the registers are as I would expect them - with CNT zeroed, UIF set and ARR = 199 (5us*40 -1) :

0693W000001qHSFQA2.png

This seems to give me 5.75us which is good enough I think. 0.75us or 30 commands seems about right for overhead coming from the function call, I think.

Well, that might be as good as it gets 🙂

View solution in original post

11 REPLIES 11
TDK
Guru
        LL_TIM_SetAutoReload(TIM6, delay);
	LL_TIM_SetPrescaler(TIM6, 39); // <--- divide clock by 40
	LL_TIM_ClearFlag_UPDATE(TIM6); // <--- clear flags
        TIM6->CNT = 0; // <-- reset counter
	LL_TIM_EnableCounter(TIM6);
	while(!LL_TIM_IsActiveFlag_UPDATE(TIM6));
	LL_TIM_ClearFlag_UPDATE(TIM6);

Seems fine. Note that you want a prescaler of 39, not 40, to divide the clock by 40.

Possibly the update flag is set before you start the timer, which defeats your plan.

Note that it'll be difficult to get us precision given the overhead with calling the timer, but it should be close/consistent.

If you feel a post has answered your question, please click "Accept as Solution".
TDK
Guru

Oh, and the clock timer is twice the APB clock, if the APB prescaler is not 1. So you really want a timer prescaler of 79.

If you feel a post has answered your question, please click "Accept as Solution".
kj.obara
Associate III

Hi TDK,

In TIM6_Init I'm doing this: LL_TIM_SetOnePulseMode(TIM6, LL_TIM_ONEPULSEMODE_SINGLE); and clearing the UPDATE/UIF flag after that. So my assumption was that this one pulse mode is preserved by the timer, so there shouldn't be any chance that UPDATE flag is set at the beginning of the Delay_us function. And the CNT counter should also be zero after each UIF set event. So the counter should stay at 0 in single pulse mode shouldn't it?

And yes, I should have mentioned that I'm not aiming at exact precision. It's rather this function should take AT LEAST X microseconds. It's fine if it takes a little longer (looking at it and the main clock being 160MHz this should never be more than X+1 microseconds, I just need it not to be less than Xus).

I accounted for APB1 prescaler: In SystemClock_Config you'll see the SYSCLK is 160MHz and APB1 prescaler is 8, so APB1 is 20MHz and timers on APB1 are twice that so 40MHz. So the prescaler needs to be 40 or 39.

Wouldn't it be the same if I kept prescaler at 40 and subtract 1 from ARR? In my head it just easier thinking about it this way. But again maybe the image of how this works in my mind is wrong from the start.

It's just that the first few invocations of Delay_us(4) give me around 0.6us and all the later ones 5.5us and this inconsistency is what bothers me. If it was consistent throughout I could at least get it to work by trial and error.

Read out and check/post the TIM6 registers content.

JW

TDK
Guru

> So the counter should stay at 0 in single pulse mode shouldn't it?

What you're saying makes sense. But the results indicate something unexpected is occurring. Rechecking your assumptions can often reveal what the problem is.

> I accounted for APB1 prescaler: In SystemClock_Config you'll see the SYSCLK is 160MHz and APB1 prescaler is 8, so APB1 is 20MHz and timers on APB1 are twice that so 40MHz. So the prescaler needs to be 40 or 39.

Okay. I was just going off the comments in the code which said APB1 is 40 MHz.

> Wouldn't it be the same if I kept prescaler at 40 and subtract 1 from ARR? In my head it just easier thinking about it this way. But again maybe the image of how this works in my mind is wrong from the start.

No. (x - 1) * y is not the same as x * (y - 1).

You should be doing "tim6init.Autoreload = arel - 1" as well. The period of the counter is the timer tick period multiplied by (ARR + 1) * (PSC + 1).

If you feel a post has answered your question, please click "Accept as Solution".

Personally I wouldn't reset/start the counter on each use, but rather start once, and then delta the TIM->CNT.

For more accurate timing, I'd not prescale

Note that other interrupt can take context away from foreground code, for quite significant periods.

Tips, Buy me a coffee, or three.. PayPal Venmo
Up vote any posts that you find helpful, it shows what's working..

Yes, I'll check the registers. JW above also suggested that. I looked at them but I will post them later if other suggestions fail.

Sorry about the misleading comment. It should say that APB1 is 20MHz and timers connected to APB1 are 40MHz.

And you're right, the prescaler is 0-based as everything. I should have thought about that.

Hi Clive, the thing is I use that function a couple of times during startup, but later on it won't be used more than once a minute, so it would be a shame to keep the timer running. In my case energy consumption doesn't really matter, but using the timer in single pulse mode still made most sense to me.

So is there any difference in accuracy based on wether I choose to ARR over PSC? As TDK wrote above the timer tick would be proportional to (ARR+1)*(PSC+1), so why would (3+1)*(39+1) be less precise than (159+1)*(0+1)?

Yeah, interrupts in STM32 are not my strong suit yet. So far I only have the systick interrupt every 1ms as per HAL settings, so I just depend (in this particular case) on the fact that at most only 1 of my half a dozen 1us delays could happen at the same time as HAL tick update. So anything else shouldn't be interfering in this learning project I'm doing.

kj.obara
Associate III

Hi guys,

So I made a small change to SystemClock_Config() to replace HSI with HSE

That includes modifying line 19 in that function to LL_RCC_PLL_ConfigDomain_SYS(LL_RCC_PLLSOURCE_HSE, LL_RCC_PLLM_DIV_4, 160, LL_RCC_PLLP_DIV_2);

Since HSI was 16MHz and HSE is just 8MHz.

void KJO_TIM6_Init()
{
	LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_TIM6);
	LL_TIM_InitTypeDef tim6init = {0};
 
	tim6init.Prescaler = 0;
	tim6init.Autoreload = 1;
	LL_TIM_Init(TIM6, &tim6init);
	LL_TIM_SetOnePulseMode(TIM6, LL_TIM_ONEPULSEMODE_SINGLE);
	LL_TIM_ClearFlag_UPDATE(TIM6);
}
 
void KJO_Delay_us(uint16_t delay)
{
	if (delay > 1638) //1638*40 < 2^16-1
	{
		HAL_Delay(2);
		return;
	}
 
//clock on APB1 is 20 MHz and timers are 2x that so 40MHz so need to reduce it by 40
	LL_TIM_SetAutoReload(TIM6, delay  * 40 - 1);
	LL_TIM_EnableCounter(TIM6); //one-pulse mode disables the counter
	while(!LL_TIM_IsActiveFlag_UPDATE(TIM6));
	LL_TIM_ClearFlag_UPDATE(TIM6);
}

Here's the status of TIM6 registers on first entry to Delay_us function with delay = 5 [us]0693W000001qHSAQA2.png

On the exit from the function (before executing clear UIF) the registers are as I would expect them - with CNT zeroed, UIF set and ARR = 199 (5us*40 -1) :

0693W000001qHSFQA2.png

This seems to give me 5.75us which is good enough I think. 0.75us or 30 commands seems about right for overhead coming from the function call, I think.

Well, that might be as good as it gets 🙂