This post is older than 2 years and might not be relevant anymore
More Info: Consider searching for newer posts

Methods to put the nRF52 to sleep in a "spinlock loop"

We recently had an issue where a race-condition was caused because we handled entry to sleep mode a bit too carefully. Therefore, I wanted to discuss different methods to make the nRF52 go to sleep - mainly without the softdevice enabled.

The goal is to be able to go to sleep on the lowest IRQ priority level based on the status of a "busy"-flag, which may be set in a higher priority IRQ handler.

I read the related post where sleep modes are explained somewhat: How do you put the nrf51822 chip to sleep?

Unfortunately user Anders Nore stated "Calling __WFE twice is basically the same as doing a __WFI." and user Carles suggested a sequence of __SEV(); __WFE(); __WFE();. So, I ended up implementing a mix of a "common" __WFI-based implementation with __WFE, which resulted in a race-condition.

WFI-based sleep function

From my understanding this should work.

void sleep_f(void)
{
  int was_masked;

  /* Disable interrupts to make sure that the busy flag does not get 
     modified between the check in the while condition and the system
     sleep */
  was_masked = __disable_irq();
  
  while (!m_busy_b)
  {
    /* If an interrupt occurrs right at this point, the interrupt is set to 
       pending, but because the __disable_irq masked the exception, the handler
       is not being executed. Therefore neither the m_busy_b flag is modified at
       this point nor is the pending bit cleared.
       
       The call to __WFI will immediately return because of the pending interrupt.
     */
    __WFI();
    
    /* Unmask exception to allow execution of IRQ handler. Thus, the m_busy_b
       flag may be set here and the pending interrupt will be cleared */
    if (!was_masked) { __enable_irq(); }
    
    /* Prevent the compiler from optimizing away enable / disable of IRQs
    __NOP();
    
    /* mask exceptions again, in case we still need to sleep */
    was_masked = __disable_irq();
  }
  
  /* unmask exceptions, system needs to wake up */
  if (!was_masked) { __enable_irq(); }

}

WFE-based sleep function (wrong)

The following excample does not work.

void sleep_f(void)
{
  int was_masked;

  /* Disable interrupts to make sure that the busy flag does not get 
     modified between the check in the while condition and the system
     sleep */
  was_masked = __disable_irq();
  
  while (!m_busy_b)
  {
    /* If an interrupt occurrs right at this point, the interrupt is set to 
       pending, but because the __disable_irq masked the exception, the handler
       is not being executed. Therefore neither the m_busy_b flag is modified at
       this point nor is the pending bit cleared.
       
       The event register will get set at this point.
       
       The call to __SEV() will set the event register.
     */
     __SEV();
     
     /* This call to __WFE will clear the event register and immediately return.
        So if an interrupt happened after the m_busy_b check, the event register
        will be cleared here no matter what, but the IRQ handler will not be
        executed.
     */
     __WFE();
     
     /* This call to __WFE will make the system sleep in any case, even if 
        an interrupt occurred right after the flag-check.
        
        If an interrupt occurred right after the flag check, the associated
        IRQn is pending and any further interrupt from the same IRQn will not
        set the event register again. Therefore the system can only wake up 
        again after an interrupt on *another* IRQn occurred!
     */
     __WFE();
    
    /* Unmask exception to allow execution of IRQ handler. Thus, the m_busy_b
       flag may be set here and the pending interrupt will be cleared */
    if (!was_masked) { __enable_irq(); }
    
    /* Prevent the compiler from optimizing away enable / disable of IRQs
    __NOP();
    
    /* mask exceptions again, in case we still need to sleep */
    was_masked = __disable_irq();
  }
  
  /* unmask exceptions, system needs to wake up */
  if (!was_masked) { __enable_irq(); }

}

Are the conclutions (within the code comments) right?

Simple WFE-based sleep function

So, I also came to the conclusion / understanding, that it's not necessary to lock interrupts when using __WFE, because the only way to clear the event register is to call __WFE, therefore the busy-flag cannot be modified before going to sleep.

void sleep_f(void)
{
  while (!m_busy_b)
  {
    /* If an interrupt occurrs right at this point, the interrupt handler 
       is executed, the busy flag may be modified, the interrupt pending 
       bit will get cleared, but the event register will get set.
       
       The call to __WFE will immediately return and clear the event register
       because the event register was set.
     */
     __WFE();
     
     /* Upon wake up, the system will - after executing any IRQ handlers - 
        first check the busy flag and then need at least two iterations of
        this loop to go back to sleep again. The first iteration will only
        clear the event register. Only the second iteration will make the
        system go to sleep.
     */
  }
}

Power-optimised WFE-based sleep function

The Nordic SDK seems to use the __WFE(); __SEV(); __WFE() sequence, which probably works as a power optimisation.

void sleep_f(void)
{
  while (!m_busy_b)
  {
    /* If an interrupt occurrs right at this point, the interrupt handler 
       is executed, the busy flag may be modified, the interrupt pending 
       bit will get cleared, but the event register will get set.
       
       The call to __WFE will immediately return and clear the event register
       because the event register was set.
     */
     __WFE();
     
     /* If the system actually was in sleep mode, the first thing done 
        (after executing any pending IRQ handlers) is setting the event
        register...
     */
     __SEV();
     
    /* ...and then clearing it immediately. This will allow the system to
       immediately go back to sleep when calling __WFE in the next loop
       iteration. Without __SEV() and second __WFE(), the system would 
       have to execute the loop body twice, first to clear the event 
       register and then to go back to sleep.
    */
     __WFE();
  }
}

Are my conclusions right?

  • An IRQn will only set the event register if it changes from "not pending" to "pending". (e.g. if a RADIO IRQ occurred but was not served (i.e. the pending bit was never cleared), and then a second RADIO IRQ occurrs, the event register will not be set.
  • __WFI-based sleep needs masking interrupts when a flag is set from the IRQ_Handler.
  • Relevant interrupts must not be masked when using __WFE-based sleep, otherwise the system may not wake up.
  • Care must be taken on the order of __WFE and __SEV calls in any case...
  • The more elaborate sleep function with __WFE(); __SEV(); __WFE(); is a power optimisation, requiring one loop iteration less to go to sleep than just calling __WFE();
Related