-
Notifications
You must be signed in to change notification settings - Fork 99
Plan for multi-tasking and IO #155
Description
While there already exists the skeleton of a scheduler in Zinc we need to put a bit more thought into how this will integrate with the rest of the library. In particular, the interaction between concurrency and peripheral usage needs some consideration. Most applications of concurrency in embedded systems revolve around I/O.
There are a few schools of thought on how to frame IO in the embedded world,
- Blocking, serial: Here you require the user to pause execution to wait for the I/O to finish. This is the approach largely taken by Arduino's Wiring and it is quite limiting, especially when working with slow interfaces. Blocking can either involve sitting in a busy loop (as is often done in Wiring) or waiting for an interrupt (taking advantage of low-power wait states).
- Non-blocking, queued: Here you allow execution to proceed, queueing the I/O for later execution is necessary. This is the approach taken by the Wiring's
Serial
interface for writes. Unfortunately you lose the ability to report input and/or errors back to the caller. - Non-blocking with callbacks: Allow execution to proceed, reporting I/O completion to the caller with a callback. This is the approach taken by the MC HCK library. Unfortunately completion passing tends to be very awkward, especially in imperative languages like C. Moreover, it's quite tricky to do correctly in Rust due to the transfers of object ownership that it requires.
- Blocking, concurrent: Here you allow I/O to block execution of the caller but allow other tasks to run in the interim. If no other tasks are available, the core can enter a low-power state.
In the case of Zinc, option 4 stands out as it leverages the safe concurrency enabled by Rust's type system. This, however, requires that the library is built with concurrency in mind, particularly ensuring that operations requiring interaction with peripherals are thread safe.
There are a few ways to ensure this,
- At compile time: Guarantee that no two threads have access to the same peripheral. This could be accomplished by specifying each task's peripheral requirements in the Platform Tree.
- At run time: Wrap each peripheral in a lock
I believe that (1) is far too restrictive, requiring the user to structure their program around the needs of the library and hardware instead of the semantics that they desire.
An example of an interface in the spirit of (2) can be found here. Here the user is required to acquire a Context
in order to use the peripheral. This context is responsible for grabbing a mutex and tracking transaction state.
Anyways, what do others think?