Sometimes, we want to enable an interrupt in NVIC, source of which has been active during the time it has been disabled. It means, that the given interrupt is set as pending in NVIC (as can be observed in NVIC_ISPRx registers), and as soon as it is enabled, it will "fire" and run the associated ISR.
If we don't want this, rather, we want an interrupt to occur only when the interrupt source gets active in the future, we need to clear the "old", pending interrupt, Using the NVIC_ICPRx register is intended exactly for this purpose1.
However, we might not know, when was the interrupt source removed - it might've been just before we want to enable the interrupt. Or, it might've been our deliberate action which removed the interrupt source (e.g. clearing some status bit in a peripheral), just before enabling it. And, as we already know, it takes time until changes of interrupt source propagate to NVIC, so we might expect that clearing the pending bit too soon may not work.
It may seem that this is simple to work around: it should be enough to clear the NVIC pending bit in a loop until it reads as being cleared, right?
Unfortunately, this is not the case. After clearing the pending bit only a few cycles after removing the interrupt source,
it reads zero for a couple more cycles, but then it gets set again. The following snippet has been run on an STM32F407 just after reset (i.e. with no clock tree setup) (see part 2 below):
int main(void) { RCC->APB1ENR |= RCC_APB1ENR_TIM3EN; // enable TIM3 clock TIM3->DIER |= TIM_DIER_UIE; // enable interrupt from update event testA[0] = NVIC_GetPendingIRQ(TIM3_IRQn); // there's no pending IRQ at the moment testA[1] = TIM3->SR; TIM3->EGR = TIM_EGR_UG; // generate an update event testA[2] = NVIC_GetPendingIRQ(TIM3_IRQn); // pending IRQ in NVIC? not yet testA[3] = TIM3->SR; // but we should see status register being set __asm("nop"); // okay, it may take time until the interrupt flags "propagate" to NVIC __asm("nop"); testA[4] = NVIC_GetPendingIRQ(TIM3_IRQn); // there should already be a pending IRQ in the NVIC testA[5] = TIM3->SR; // and also status register of the timerTIM3->SR &= ~TIM_SR_UIF; // this clears the status bit-- see part 2 below TIM3->SR = ~TIM_SR_UIF; // this clears the status bit // the key to the problem is the number of cycles elapsed between these two events // - more than 6 is needed to remove the problem in this snippet __asm("nop"); __asm("nop"); __asm("nop"); __asm("nop"); __asm("nop"); __asm("nop"); // __asm("nop"); // uncommenting this line makes delay long enough to remove the problem NVIC_ClearPendingIRQ(TIM3_IRQn); // and this should clear the pending interrupt testA[6] = NVIC_GetPendingIRQ(TIM3_IRQn); // there should be no pending IRQ in the NVIC by now testA[7] = TIM3->SR; // __asm("nop"); __asm("nop"); __asm("nop"); __asm("nop"); testA[8] = NVIC_GetPendingIRQ(TIM3_IRQn); // and it should remain so testA[9] = TIM3->SR; // while(1) { __asm("nop"); } }
Running this code results in the following content of testA array:
(gdb) p /x testA $26 = {0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x0}As witnessed by nonzero testA[8], the pending interrupt "reappeared" and would cause ISR to be entered immediately after enabling the interrupt in NVIC. Only after the 7th nop was uncommented before clearing the pending bit, it did not reappear again.
This is one more of those pesky timing problems, which are not documented at all, and even if experimentally determined, given we don't know what do they exactly depend upon (e.g. APB prescalers), they have the nasty potential to backfire if not handled with utmost caution.
Soon after this article was published, I was contacted by Herman Roebbers, who pointed out, that the code I posted violates the rule not to use RMW on status bytes. He also suggested, that that the operation in question, TIM3->SR &= ~TIM_SR_UIF; might've been reponsible for the observed effect.
I've corrected the status register bit clear, and then also tested on an 'L476 Disco (which has unfortunately been discontinued by ST). I found out that with RCC left completely in reset state, the testA array was:
(gdb) p /x testA $23 = {0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0}which means, that the interrupt did not "come back", and this remained so even when I removed every nop between TIM3->SR = ~TIM_SR_UIF; and NVIC_ClearPendingIRQ(TIM3_IRQn);.
Does this mean that my original test was incorrect? Turns out, that it's enough to set the APB1 divider to 4, and the described effect is back (and remains with insertion of up to 4 nop).
(gdb) p /x testA $26 = {0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x1, 0x0}(Note, that the first TIM3_SR readback after TIM3_EGR.UG being set indicates, that UIF hasn't been set yet, due to slower APB bus thus TIMx operation.)
Conclusion?
My original test was done quite a couple of years ago and the original code and its compilation environment and binary did not survive. As we are dealing with issues related to a few clock cycles, the exact machine code generated by compiler, and possible minute differences in the bus/clock structure between 'F4 and 'L4, do matter. I now might spend time on digging deep and trying to reconstruct the original setup under which the interrupt "reappearance" occured back then, but that was not my point.
In demonstrating, that there *are* circumstances under which the cleared interrupt might "reappear", I wanted to alert the users on this possibility, so that they are not caught by surprise when attempting to perform the disable-clear-reenable sequence with interrupts in Cortex-M3/4/7-based mcus.