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();