3
votes

Hello I would like to know what is the proper way to put ARM Cortex M0+ to deep sleep. Particularly I'm using CMSIS-RTOS RTX.

The way my IRQ is handled is that ISR just set OS Signal and clear IRQ. Eg.:

void ISR_A(){
  osSignalSet(ID_Task_Handling_IRQ_A, IRQ_A_SIGNAL_CODE);
  DisableIRQ_A();
}

Then in my idle loop

void os_idle_demon(void) {
...
timeToSleep = os_suspend(); // get from OS how long I can sleep and also stop OS scheduling
LPTMR_Init(timeToSleep,...) // set wakeup timer
POWER_EnterLLS(void)        // enter deep sleep. Set registers and calls WFI instruction
// after wakup compute actual slpetTime
os_resume(sleptTime); // enable OS scheduling
}

The problem is that my ISR does not handles IRQ fully (it just set signal in OS and some thread will handle it according to priority and scheduling - I would like to keep it this way). But when IRQ comes in between os_suspend() and __wfi() instruction then IRQ is cleared but task can not be scheduled (because os_suspend()). When CPU get to WFI it goes to sleep and thus OS thread that shall handle signal from ISR never executes. But CPU is also not woken up by (pad) IRQ because that is already handled.

The question is how to atomically do the check that there is no task pending and start WFI.

Something like

if( ! OS_Signal_Is_rised) {  
  // only do it atomically because what if IRQ would come here?
  wfi; 
}
2

2 Answers

2
votes

So I had time to do some test on ARM M0+ in chip MKL17Z256VFT4. Using CMSIS-RTOS RTX (v 4.75).

It works like this:

void os_idle_demon(void) { // task with lowest priority - scheduled by 
  //system when there is no action to do
  for (;;) {
    timeToSleep = os_suspend(); // stop OS from switching tasks and get maximum allowed sleep time
    __disable_irq();  
    LPTMR_Init(timeToSleep...); // set Low Power sleep timer 
    SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk;//set DeepSleep
    GPIO(pin=0,val=1);  // signalize on GPIO pad that CPU is (almost) in sleep
    __enable_irq();
    __wfi();   // go to DeepSleep
    GPIO(pin=0,val=0); // signalize on GPIO pad that CPU just wakeup
    sleptTime = LPTMR_GetCounterValue();  // get sleepTime after wakeup
    os_resume(sleptTime); // set system to schedule tasks and give os info about sleep time
  }  

I did observe what happens when I stimulate interrupt at different places of code execution. I used NVIC_SetPendingIRQ(PORTCD_IRQn); for enforcing IRQ. I observed which task is running by logic analyzer observing GPIO pins.

case 1) easy case is: IRQ is triggered before os_suspend() the ISR is called and I use in ISR system signalling osSignalSet(ID_Thread1, SIGNAL_X). Since every thread has higher priority than os_idle_demon the thread ID_Thread1 which is waiting in event = osSignalWait(ANY_SIGNAL, osWaitForever); is switched to (by RTOS) and signal is processed. After that thread starts to waiting again for any signal and os_idle_demon task is scheduled and ARM goes to sleep.

case 2) Another case is: IRQ is set between os_suspend() and __disable_irq(). I found that when IRQ is called just before __disable_irq() the ARM is not quick enough to process IRQ and actually __disable_irq() is executed first. So IRQ waits until __enable_irq() is called. And so all goes into another case.

case 3) IRQ is set before __enable_irq(). Right after enabling IRQ and before CPU do DeepsSleep (__wfi();) the ISR is executed. Signal is set. But system can not switch thread (as we called os_suspend()). But apparently WFI is somehow magically (I'm still looking into spec why) not executed. The deep sleep is not eneterd and code continues to os_resume(). Then OS switch tasks and signal is processed correctly.

So the only buggy case is when you put something between instructions:

__enable_irq();
// do not put anything here
__wfi();

If you put there anything then case 3 would react like this: the ISR is executed right after __enable_irq(). ISR set OS signal but signaled task is not scheduled (because we called os_suspend() before). Then deep sleep is entered by __wfi(). System then sleeps forever or until LPTMR. But that is error because there is signal which shall be handled as soon as possible and it is not!

So conclusion is that it seems that sequence depicted in response is safe. As long as you do not put any instruction in between __enable_irq(); and __wfi();. Also you shall not put any instruction in between: os_suspend(); and __disable_irq();. This is valid at least for MKL17Z256VFT4. Dunno about other chip. But you can test yourself by enforcing the IRQ flag using function NVIC_SetPendingIRQ()

--- EDIT ---

So my friend showed me also documentation where is written that even when you disable interrupt CPSID the ARM wake up from WFI. So maybe safer sequence is

__wfi();   // go to DeepSleep
// optionally enable peripherals that might been disabled
__enable_irq();

Do not forget to call __enable_irq(); at latest before you call os_resume(sleptTime); otherwise on my chip I get HardFault.

--- EDIT 2 ---

Also I found that we could use __WFE(); instruction to be sure there is no racing condition. WFE is Wait For Event. It puts CPU to same sleep mode as WFI. But it also checks "event register". This register is set on every ISR (at the end). When there was IRQ before WFE then WFE will not go to sleep. You can optionally set "event register" by calling instruction __SEV();. "event register" is not accessible from SW. If you want to be sure to clear it you can call

__SEV(); // set event register if it was not set 
__WFE(); // clear event register and don't goto sleep because we set event register just before

But note that this instruction has slightly different behavior from WFI. For example WFE can wake up also on pending IRQ if SEVONPEND is set (see WFE spec). (Note interrupt is pending if has less priority than configured in Base Priority Mask Register)See also Entering Sleep Mode. Here is also very nice table about difference WFI vs WFE

1
votes

I recommend one of two approaches.

1) When I use wfi() in OS context switching, I enable the SysTick interrupt so that in the rare instance the interrupt arrives between os_suspend() and wfi(), the system will sleep only for the duration of the SysTick interrupt and then wake to check the OS status. This approach works in most situations.

2) If you have hard real-time requirements, you can use the sleep-on-exit feature documented here: http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0552a/BABHHGEB.html. This will probably be more complicated to implement, but using interrupt priorities you can guarantee an atomic operation between os_suspend() and entering sleep.