cancel
Showing results for 
Search instead for 
Did you mean: 

Best practices: How to work with interrupts and low power modes when using the HAL?

WGall.1
Associate II

NOTE: I have edited this post after some of the replies, to fix some typos in the code and to use "insert code" instead of "Preformated" to get syntax highlighting.

 

Hello,

I would like to understand what is the recommended approach when working with interrupts and low power modes, when using the HAL.

The approach I'm following right now is:

  • Use a volatile variable to indicate if there is an interrupt. One bit for each interrupt.
  • Using masking,  the corresponding bit is set in each interrupt callback. The callback does not perform any other action.
  • In the main infinite loop, go to sleep if there are no interrupts. When woken up, check each bit of the flags variable to serve the pending interrupt(s).

Here is a simplified code sample:

const uint8_t ISR_1 = 0x01;
const uint8_t ISR_2 = 0x02;
const uint8_t ISR_3 = 0x04;

volatile uint8_t isr_flags = 0;

main (){
  // Init stuff
  
  while(1){
    if (isr_flags == 0){
      HAL_SuspendTick(); // Is this call here ok? Could it lead to missing an interrupt?
      HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON,PWR_SLEEPENTRY_WFI);
      HAL_ResumeTick();
    }
    if (isr_flags & ISR_1){
      isr_flags &= ~ISR_1;
      // do stuff 
    } 
    if (isr_flags & ISR_2){
       isr_flags &= ~ISR_2;
       // do stuff
     }
   }
}

void HAL_ISR1_Callback(...){
  isr_flags |= ISR_1;
}

void HAL_ISR1_Callback(...){
  isr_flags |= ISR_2;
}

 My main questions are:

  • Is this the recommended approach by ST when using the HAL?
  • Is there a possibility of missing an interrupt due to a race condition?

Thanks and BR

Walter

1 ACCEPTED SOLUTION

Accepted Solutions
Piranha
Chief II

The user "davidshel3193" is a bot and it has regurgitated the "average Joe" on the internet. Part of the information is true, but in many points it is critically incomplete and therefore is a disinformation.

Generally there are tree levels, where interrupts can be disabled - peripheral, NVIC and CPU core. You can learn about it, reading this discussion, but even that one is slightly in correct. ARM CPU wakes up from the sleep when the interrupt becomes pending in NVIC, regardless of whether interrupts can actually be executed. Therefore disabling interrupts at NVIC will not wake up CPU from the sleep. In addition NVIC is "per peripheral" and that is not what you need here. Disabling interrupts in CPU core sets a flag in PRIMASK register, which disables just the execution of all interrupts, but the CPU still wakes up from the sleep, when an interrupt becomes pending in NVIC. And, if the interrupt becomes pending before or at the time of executing the WFI instruction, the WFI instruction will act as a NOP instruction and the CPU will not go into sleep mode.

Generally one should not disable the system tick, when going into sleep mode. If you disable it, the system time will not advance. If you are using those ticks only in blocking functions, that can be OK, but, if you are saving timestamps and using those later, that will obviously fail. For extreme power saving by disabling system ticks typically then an RTC with an additional synchronization code is involved.

Taking all that into account, the correct code can be seen in this topic about CPU load measurement. But the generic template is this:

 

for (;;) {
	__disable_irq();
 	if (/* Test if there are no tasks to execute */) {
		__DSB();
		__WFI();
 		__enable_irq();
	} else {
		// Select the task to execute
		// Remove the active flag for the selected task
 		__enable_irq();
 		// Execute the selected task
	}
}

 

For a task selection I recommend using the __CLZ() macro (instruction) instead of inefficiently testing all bits. Though it's not available on M0(+) cores, but even then a simple loop with a binary search algorithm with O(log n) time complexity is generally better.

 

On 32-bit ARM all data types up to the size of 32 bits are atomic, therefore uint32_t, uint_fast16_t, uint_fast8_t, all of which on 32-bit ARM are defined as 32-bit unsigned integers, are often the preferred types, that can often be faster than the smaller types. But, the RMW operations, of course, are not atomic.

Effectively, what you are developing here, is not just an interrupt processing code, but a cooperative task scheduler. As the tasks can be activated (flag set) from interrupts with different preempt priorities or thread (non-interrupt) code, the flag setting code cannot just re-enable the interrupt unconditionally. Instead this approach must be used:

 

void Scheduler_TaskActivate(scheduler_task_t idTask)
{
	uint32_t rPRIMASK = __get_PRIMASK();
	__disable_irq();

	isr_flags |= idTask;

	__set_PRIMASK(rPRIMASK);
}

 

View solution in original post

5 REPLIES 5
TDK
Guru

A read-modify-write on a uint_8 or anything else is not atomic. The read is atomic, and the write is atomic, but if an interrupt happens between the read and the write, you will lose the bit that got set within the interrupt. (Side note: this is in contrast to some other micros like TI's MSP430 where bit sets/resets are atomic.)

You can either disable interrupts around the clearing of bits, or you can use separate variables, or you can perform the action within the interrupt itself.

 

> isr_flags &= ISR_1;

Probably meant to write:

 

isr_flags &= ~ISR_1;

 

 

Other than those issues, the code seems fine. Suspending ticks won't cause interrupts to be missed.

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

Hi @TDK, @davidshel3193 

Thanks for your answers, they clarify some doubts that I had but I still have some questions.

I feel like I'm still missing something. In the following code:

__disable_irq();
HAL_SuspendTick(); // What if interrupt arrives here?
HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI);
HAL_ResumeTick();
__enable_irq();

after the interrupts are disabled, but before going into Sleep, is in it possible that an action that could generate an interrupt happens (like user pressing an EXTI button) and we lose it anyway? Is this unavoidable? Or if the action happens, say while calling HAL_SuspendTick(), will the uc wake up immediately after entering sleep mode, because we have not served the interrupt?

 

Regarding the use of the HAL, I could not find an equivalent for __disable_irq() or __enable_irq(). I did find the HALs for enabling/disabling a specific interrupt, and tried this:

HAL_NVIC_DisableIRQ(USART2_IRQn);
HAL_NVIC_DisableIRQ(TIM1_UP_TIM10_IRQn);

HAL_SuspendTick();
HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI);
HAL_ResumeTick();

HAL_UART_Transmit(&huart2, (uint8_t*)"UP\r\n", 4, 1000); // Never prints

HAL_NVIC_EnableIRQ(USART2_IRQn);
HAL_NVIC_EnableIRQ(TIM1_UP_TIM10_IRQn);

But the device never wakes up (message "UP" is never printed)

So I suppose disabling specific interrupts in the NVIC is incompatible with entering sleep mode and waiting for interrupts?

Thanks in advance.

TDK
Guru

> Regarding the use of the HAL, I could not find an equivalent for __disable_irq() or __enable_irq().

The functions you should use are __disable_irq() and __enable_irq().

> But the device never wakes up (message "UP" is never printed)

Because the advice you took was from a bot who doesn't know how to code.

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

I did try with __disable_irq() and __enable_irq(), and it actually works (or at least the execution does not get stuck as when using the HAL functions)

> Because the advice you took was from a bot who doesn't know how to code.

I did not notice it initially but you are right, the reply is suspiciously similar to that given by chatgpt.

Piranha
Chief II

The user "davidshel3193" is a bot and it has regurgitated the "average Joe" on the internet. Part of the information is true, but in many points it is critically incomplete and therefore is a disinformation.

Generally there are tree levels, where interrupts can be disabled - peripheral, NVIC and CPU core. You can learn about it, reading this discussion, but even that one is slightly in correct. ARM CPU wakes up from the sleep when the interrupt becomes pending in NVIC, regardless of whether interrupts can actually be executed. Therefore disabling interrupts at NVIC will not wake up CPU from the sleep. In addition NVIC is "per peripheral" and that is not what you need here. Disabling interrupts in CPU core sets a flag in PRIMASK register, which disables just the execution of all interrupts, but the CPU still wakes up from the sleep, when an interrupt becomes pending in NVIC. And, if the interrupt becomes pending before or at the time of executing the WFI instruction, the WFI instruction will act as a NOP instruction and the CPU will not go into sleep mode.

Generally one should not disable the system tick, when going into sleep mode. If you disable it, the system time will not advance. If you are using those ticks only in blocking functions, that can be OK, but, if you are saving timestamps and using those later, that will obviously fail. For extreme power saving by disabling system ticks typically then an RTC with an additional synchronization code is involved.

Taking all that into account, the correct code can be seen in this topic about CPU load measurement. But the generic template is this:

 

for (;;) {
	__disable_irq();
 	if (/* Test if there are no tasks to execute */) {
		__DSB();
		__WFI();
 		__enable_irq();
	} else {
		// Select the task to execute
		// Remove the active flag for the selected task
 		__enable_irq();
 		// Execute the selected task
	}
}

 

For a task selection I recommend using the __CLZ() macro (instruction) instead of inefficiently testing all bits. Though it's not available on M0(+) cores, but even then a simple loop with a binary search algorithm with O(log n) time complexity is generally better.

 

On 32-bit ARM all data types up to the size of 32 bits are atomic, therefore uint32_t, uint_fast16_t, uint_fast8_t, all of which on 32-bit ARM are defined as 32-bit unsigned integers, are often the preferred types, that can often be faster than the smaller types. But, the RMW operations, of course, are not atomic.

Effectively, what you are developing here, is not just an interrupt processing code, but a cooperative task scheduler. As the tasks can be activated (flag set) from interrupts with different preempt priorities or thread (non-interrupt) code, the flag setting code cannot just re-enable the interrupt unconditionally. Instead this approach must be used:

 

void Scheduler_TaskActivate(scheduler_task_t idTask)
{
	uint32_t rPRIMASK = __get_PRIMASK();
	__disable_irq();

	isr_flags |= idTask;

	__set_PRIMASK(rPRIMASK);
}