I'm still following this thread, and I still feel that it would be really helpful if someone could share a specific example from one of their projects and explain the reasoning behind the design choice. I think that would benefit not just me, but anyone else reading this thread.
People could then ask follow-up questions, and the person sharing the example could answer based on their own experience. Right now, most of the discussion is at a general level, and I personally find it difficult to draw any concrete conclusions from that.
That said, this is just my opinion. I'm not trying to tell anyone how they should answer or asking anyone to share anything they're not comfortable sharing. If you'd rather keep your reply general, that's completely fine. I'm simply saying that, for me, a real example with the reasoning behind the design choice would be much easier to understand.
That's the main reason I started this thread I wanted to understand the kinds of real project requirements
Sure, here's my example: a handheld custom texting/locator radio with a monochrome display, a LoRa radio with a fully custom radio stack, and a GNSS receiver.
The high-level purpose is to be able to track other people that also have these radios, text each-other, and do basic navigation. The original firmware was a main 100ms ISR loop with a extra USART Rx ISR for GNSS data reception.
The main loop was doing:
- The full radio stack with outgoing message FIFOs and reading incoming messages off the LoRa IC directly
- Synchronizing itself to GNSS PPS pulse by simply reading it as a GPIO
- Drawing the display graphics into framebuffer and configuring DMA transfer over SPI to display
- Reading the GPIO buttons for user UI
- Maintaining the full UI state machine in the menus/submenus
The protocol itself was TDMA and needed to be synchronized to the 1 second PPS, which in this architecture can be done to a +-100ms jitter level since the ISR itself runs every 100ms. There are guard times around radio transmit and receive that tolerate up to 300ms of jitter, so that particular hard realtime constraint was respected.
The UART RXNE ISR was a separate asynchronous ISR that would parse the incoming GNSS data from the UART and populate atomic values such as latitude, longitude, time, etc. Those are then read directly by the 100ms loop ISR without mutexes or any other primitives.
Notice the limitations that need to enforced:
- The main screen render loop *has* to finish in under 100ms (well 100ms - whatever other time the code needs).
- The 100ms jitter for radio stack vs absolute GPS time means it eats a bit into the total guard time budget, but 300ms allocated to it is very large overall so not a problem
- The large guard time reduces the total on-air time of the radio artificially
- The buttons can only be sampled every 100ms, so if you press and release one super quickly, the input may be missed, which is a minor annoyance
Now with the limitations in mind, I wanted to upgrade a few things and enable less guard time (more on-air time), rendering of maps that may take more than 100ms, and fix some UI input annoyances. Instead of the 2-ISR architecture I made it 3-ISR/context:
- Periodic 50ms ISR running the radio stack that is hardware-synchronized to the PPS 1Hz pulse via a timer repetition counter: reduces jitter from 100ms to ~single digit us
- Periodic 100ms UI ISR: deadline no longer critical
- Asynchronous USART RXNE ISR doing the GNSS data reception, unchanged
Note that the split here is pretty intuitive: the radio stack does the radio, the UI context does UI,
HOWEVER the amount of extra complexity that I had to introduce here is a bit disproportionate:
The radio stack doesn't run when there is no GNSS PPS pulse, which in some ways that is nice, but it needs extra attention since the actual code execution is no longer a continuous loop.
The data is no longer inherently atomic: the radio messages, both incoming and outgoing are now transferred via lock-free FIFOs, while the GNSS data is still inherently atomic. No mutexes are required here, and neither are critical sections (i.e. disabling ISRs).
Note how if I used an RTOS to make a similar architecture, very little would actually change: you still need FIFOs to transfer the data, you still need some way to maintain GNSS data consistency from the USART context to the others. FreeRTOS would not help me synchronize my 50ms radio stack to the PPS input, that still has to be purely hardware for zero-added-jitter. Perhaps one value it would add is maybe automatic time/performance measurement of the main UI loop as well as the radio stack loop, but that is like 3 C code lines for each context here that I have to store a freerunning counter value for performance validation anyway.
FreeRTOS specifically does not have lock-free FIFOs from what I remember, it uses critical sections to maintain data consistency in its' _from_ISR API implementations, so technically that would add some extra hard to measure jitter to the UART ISR (that *still* has to be an ISR, it cannot be a task), however it's certainly not a huge issue in this particular example, just an extra annoyance. In any case with FreeRTOS, you still have to maintain context relationships, keep track of deadlines, and make sure the state machines don't have any weird edge cases. I didn't use FreeRTOS here because it's a shitload of extra code and API to learn and get good at for very little gain, it would provide no abstraction of anything on its' own (that's what ST HAL would do in my case anyway), and of course overall it would be *less* deterministic.
One thing that I wouldn't have to write with FreeRTOS is a queue/FIFO implementation, but the whole thing is like 30 lines of C in a header-only library, infinitely reusable, and again lock-free which has tangible benefits for the extra-hard jitter/deadlines.