As described in the previous article, JeeH’s “tasks” implement a limited form of concurrency. This includes “async” drivers to deal with interrupts. But asynchronous I/O requests are somewhat inconvenient: once such a request has been started, you need to return from the task to process completion events (to guarantee atomicity, incoming events for a task can only be picked up when that task is not running).
Another option is to use JeeH’s “sync” driver variants. This uses the same DMA-based transfer mechanism, but then blocks until the request finishes. Blocking is based on the ARM Cortex “Wait For Event” (WFE) instruction, which does not depend on interrupt handlers. This avoids the overhead of setting up, entering, and returning from IRQs, as would be required for async mode drivers. Sync mode is slightly more involved than “polled” mode, because it needs to set up and start the associated DMA channel(s). The benefit over polled I/O is that the CPU enters sleep mode during the transfer, with a substantially lower power consumption.
The implementation of sync mode is based on the fact that you can enable an IRQ in the hardware peripheral without enabling it in the NVIC. When fired, the result is a pending IRQ which never triggers the CPU’s exception handling mechanism. IOW, the IRQ is ready to go but the CPU won’t act on it, other than setting an “event flag” which causes the current (or next) WFE instruction to resume processing.
There is a small gotcha in that you can’t tell what caused the event flag to be set. Presumably, most interrupt sources will be fully enabled in the NVIC and will therefore mmediately fire their associated IRQ handlers. All other interrupt sources will stay pending until cleared in the hardware peripheral. Which is precisely how sync drivers work: they enable the interrupt in the peripheral (but not in the NVIC), start the request, and then loop on a WFE until the interrupt flag is set (i.e. pending). Once that happens, the driver clears the interrupt in the peripheral, clears the “pending” state in the NVIC, and then returns to the caller.
This has been implemented for I2C, SPI, and UART and works really well. The setup overhead compared to fully CPU-driven polled mode is insignificant, the attainable trhoughput is excellent, and power consumption is minimal. JeeH’s sync mode drivers are probably the best choice for most use cases.
There’s still a slight problem: if an interrupt wakes up a higher priority task, that task could in principle start another sync mode transfer while the lower-priority task is still processing its sync mode request. At this point, the CPU will be inside that second WFE loop, checking for completion of that second interrupt. If the original request now completes, it sets the event flag, but the nested loop won’t see this (it doesn’t know there was a sync request in progress). The effect is that the WFE loop now degenerates into a busy loop, because each WFE will return with the event flag still set. This is not a critical issue: the nested loop will eventually detect the nested completion and return to the original WFE loop. The bad news is that the CPU can’t re-enter sleep mode once the original sync request is done. The good news is that everything still works.
Note that there’s little point trying to enter an even lower-power “stop” or “standby” mode in a sync driver: most comms hardware requires a clock source, so sleep (or “low-power sleep”) mode is usually as low as you can go. It’s ok to stop the CPU clock, but there is still a DMA-driven I/O transfer going on, after all. The only opportunity to reach lowest power consumption modes, is when no CPU or I/O activity is taking place (and the only way to wake up again is through an interrupt of some kind or a level change on a hardware wake-up pin).