496 lines
17 KiB
ReStructuredText
496 lines
17 KiB
ReStructuredText
.. _docs-os_abstraction_layers:
|
||
|
||
=====================
|
||
OS Abstraction Layers
|
||
=====================
|
||
Pigweed’s operating system abstraction layers are portable and configurable
|
||
building blocks, giving users full control while maintaining high performance
|
||
and low overhead.
|
||
|
||
Although we primarily target smaller-footprint MMU-less 32-bit microcontrollers,
|
||
the OS abstraction layers are written to work on everything from single-core
|
||
bare metal low end microcontrollers to asymmetric multiprocessing (AMP) and
|
||
symmetric multiprocessing (SMP) embedded systems using Real Time Operating
|
||
Systems (RTOS). They even fully work on your developer workstation on Linux,
|
||
Windows, or MacOS!
|
||
|
||
Pigweed has ports for the following systems:
|
||
|
||
.. list-table::
|
||
|
||
* - **Environment**
|
||
- **Status**
|
||
* - STL (Mac, Window, & Linux)
|
||
- **✔ Supported**
|
||
* - `FreeRTOS <https://www.freertos.org/>`_
|
||
- **✔ Supported**
|
||
* - `Azure RTOS (formerly ThreadX) <https://azure.microsoft.com/en-us/services/rtos/>`_
|
||
- **✔ Supported**
|
||
* - `SEGGER embOS <https://www.segger.com/products/rtos/embos/>`_
|
||
- **✔ Supported**
|
||
* - Baremetal
|
||
- *In Progress*
|
||
* - `Zephyr <https://www.zephyrproject.org/>`_
|
||
- Planned
|
||
* - `CMSIS-RTOS API v2 & RTX5 <https://www.keil.com/pack/doc/CMSIS/RTOS2/html/index.html>`_
|
||
- Planned
|
||
|
||
Pigweed's OS abstraction layers are divided by the **functional grouping of the
|
||
primitives**. Many of our APIs are similar or **nearly identical to C++'s
|
||
Standard Template Library (STL)** with the notable exception that we do not
|
||
support exceptions. We opted to follow the STL's APIs partially because they
|
||
are relatively well thought out and many developers are already familiar with
|
||
them, but also because this means they are compatible with existing helpers in
|
||
the STL; for example, ``std::lock_guard``.
|
||
|
||
---------------
|
||
Time Primitives
|
||
---------------
|
||
The :ref:`module-pw_chrono` module provides the building blocks for expressing
|
||
durations, timestamps, and acquiring the current time. This in turn is used by
|
||
other modules, including :ref:`module-pw_sync` and :ref:`module-pw_thread` as
|
||
the basis for any time bound APIs (i.e. with timeouts and/or deadlines). Note
|
||
that this module is optional and bare metal targets may opt not to use this.
|
||
|
||
.. list-table::
|
||
|
||
* - **Supported On**
|
||
- **SystemClock**
|
||
* - FreeRTOS
|
||
- :ref:`module-pw_chrono_freertos`
|
||
* - ThreadX
|
||
- :ref:`module-pw_chrono_threadx`
|
||
* - embOS
|
||
- :ref:`module-pw_chrono_embos`
|
||
* - STL
|
||
- :ref:`module-pw_chrono_stl`
|
||
* - Zephyr
|
||
- Planned
|
||
* - CMSIS-RTOS API v2 & RTX5
|
||
- Planned
|
||
* - Baremetal
|
||
- Planned
|
||
|
||
|
||
System Clock
|
||
============
|
||
For RTOS and HAL interactions, we provide a ``pw::chrono::SystemClock`` facade
|
||
which provides 64 bit timestamps and duration support along with a C API. For
|
||
C++ there is an optional virtual wrapper, ``pw::chrono::VirtualSystemClock``,
|
||
around the singleton clock facade to enable dependency injection.
|
||
|
||
.. code-block:: cpp
|
||
|
||
#include <chrono>
|
||
|
||
#include "pw_thread/sleep.h"
|
||
|
||
using namespace std::literals::chrono_literals;
|
||
|
||
void ThisSleeps() {
|
||
pw::thread::sleep_for(42ms);
|
||
}
|
||
|
||
Unlike the STL's time bound templated APIs which are not specific to a
|
||
particular clock, Pigweed's time bound APIs are strongly typed to use the
|
||
``pw::chrono::SystemClock``'s ``duration`` and ``time_points`` directly.
|
||
|
||
.. code-block:: cpp
|
||
|
||
#include "pw_chrono/system_clock.h"
|
||
|
||
bool HasThisPointInTimePassed(const SystemClock::time_point timestamp) {
|
||
return SystemClock::now() > timestamp;
|
||
}
|
||
|
||
--------------------------
|
||
Synchronization Primitives
|
||
--------------------------
|
||
The :ref:`module-pw_sync` provides the building blocks for synchronizing between
|
||
threads and/or interrupts through signaling primitives and critical section lock
|
||
primitives.
|
||
|
||
Critical Section Lock Primitives
|
||
================================
|
||
Pigweed's locks support Clang's thread safety lock annotations and the STL's
|
||
RAII helpers.
|
||
|
||
.. list-table::
|
||
|
||
* - **Supported On**
|
||
- **Mutex**
|
||
- **TimedMutex**
|
||
- **InterruptSpinLock**
|
||
* - FreeRTOS
|
||
- :ref:`module-pw_sync_freertos`
|
||
- :ref:`module-pw_sync_freertos`
|
||
- :ref:`module-pw_sync_freertos`
|
||
* - ThreadX
|
||
- :ref:`module-pw_sync_threadx`
|
||
- :ref:`module-pw_sync_threadx`
|
||
- :ref:`module-pw_sync_threadx`
|
||
* - embOS
|
||
- :ref:`module-pw_sync_embos`
|
||
- :ref:`module-pw_sync_embos`
|
||
- :ref:`module-pw_sync_embos`
|
||
* - STL
|
||
- :ref:`module-pw_sync_stl`
|
||
- :ref:`module-pw_sync_stl`
|
||
- :ref:`module-pw_sync_stl`
|
||
* - Zephyr
|
||
- Planned
|
||
- Planned
|
||
- Planned
|
||
* - CMSIS-RTOS API v2 & RTX5
|
||
- Planned
|
||
- Planned
|
||
- Planned
|
||
* - Baremetal
|
||
- Planned, not ready for use
|
||
- ✗
|
||
- Planned, not ready for use
|
||
|
||
|
||
Thread Safe Mutex
|
||
-----------------
|
||
The ``pw::sync::Mutex`` protects shared data from being simultaneously accessed
|
||
by multiple threads. Optionally, the ``pw::sync::TimedMutex`` can be used as an
|
||
extension with timeout and deadline based semantics.
|
||
|
||
.. code-block:: cpp
|
||
|
||
#include <mutex>
|
||
|
||
#include "pw_sync/mutex.h"
|
||
|
||
pw::sync::Mutex mutex;
|
||
|
||
void ThreadSafeCriticalSection() {
|
||
std::lock_guard lock(mutex);
|
||
NotThreadSafeCriticalSection();
|
||
}
|
||
|
||
Interrupt Safe InterruptSpinLock
|
||
--------------------------------
|
||
The ``pw::sync::InterruptSpinLock`` protects shared data from being
|
||
simultaneously accessed by multiple threads and/or interrupts as a targeted
|
||
global lock, with the exception of Non-Maskable Interrupts (NMIs). Unlike global
|
||
interrupt locks, this also works safely and efficiently on SMP systems.
|
||
|
||
.. code-block:: cpp
|
||
|
||
#include <mutex>
|
||
|
||
#include "pw_sync/interrupt_spin_lock.h"
|
||
|
||
pw::sync::InterruptSpinLock interrupt_spin_lock;
|
||
|
||
void InterruptSafeCriticalSection() {
|
||
std::lock_guard lock(interrupt_spin_lock);
|
||
NotThreadSafeCriticalSection();
|
||
}
|
||
|
||
Signaling Primitives
|
||
====================
|
||
Native signaling primitives tend to vary more compared to critical section locks
|
||
across different platforms. For example, although common signaling primitives
|
||
like semaphores are in most if not all RTOSes and even POSIX, it was not in the
|
||
STL before C++20. Likewise many C++ developers are surprised that conditional
|
||
variables tend to not be natively supported on RTOSes. Although you can usually
|
||
build any signaling primitive based on other native signaling primitives,
|
||
this may come with non-trivial added overhead in ROM, RAM, and execution
|
||
efficiency.
|
||
|
||
For this reason, Pigweed intends to provide some simpler signaling primitives
|
||
which exist to solve a narrow programming need but can be implemented as
|
||
efficiently as possible for the platform that it is used on. This simpler but
|
||
highly portable class of signaling primitives is intended to ensure that a
|
||
portability efficiency tradeoff does not have to be made up front.
|
||
|
||
.. list-table::
|
||
|
||
* - **Supported On**
|
||
- **ThreadNotification**
|
||
- **TimedThreadNotification**
|
||
- **CountingSemaphore**
|
||
- **BinarySemaphore**
|
||
* - FreeRTOS
|
||
- :ref:`module-pw_sync_freertos`
|
||
- :ref:`module-pw_sync_freertos`
|
||
- :ref:`module-pw_sync_freertos`
|
||
- :ref:`module-pw_sync_freertos`
|
||
* - ThreadX
|
||
- :ref:`module-pw_sync_threadx`
|
||
- :ref:`module-pw_sync_threadx`
|
||
- :ref:`module-pw_sync_threadx`
|
||
- :ref:`module-pw_sync_threadx`
|
||
* - embOS
|
||
- :ref:`module-pw_sync_embos`
|
||
- :ref:`module-pw_sync_embos`
|
||
- :ref:`module-pw_sync_embos`
|
||
- :ref:`module-pw_sync_embos`
|
||
* - STL
|
||
- :ref:`module-pw_sync_stl`
|
||
- :ref:`module-pw_sync_stl`
|
||
- :ref:`module-pw_sync_stl`
|
||
- :ref:`module-pw_sync_stl`
|
||
* - Zephyr
|
||
- Planned
|
||
- Planned
|
||
- Planned
|
||
- Planned
|
||
* - CMSIS-RTOS API v2 & RTX5
|
||
- Planned
|
||
- Planned
|
||
- Planned
|
||
- Planned
|
||
* - Baremetal
|
||
- Planned
|
||
- ✗
|
||
- TBD
|
||
- TBD
|
||
|
||
Thread Notification
|
||
-------------------
|
||
Pigweed intends to provide the ``pw::sync::ThreadNotification`` and
|
||
``pw::sync::TimedThreadNotification`` facades which permit a singler consumer to
|
||
block until an event occurs. This should be backed by the most efficient native
|
||
primitive for a target, regardless of whether that is a semaphore, event flag
|
||
group, condition variable, or direct task notification with a critical section
|
||
something else.
|
||
|
||
Counting Semaphore
|
||
------------------
|
||
The ``pw::sync::CountingSemaphore`` is a synchronization primitive that can be
|
||
used for counting events and/or resource management where receiver(s) can block
|
||
on acquire until notifier(s) signal by invoking release.
|
||
|
||
.. code-block:: cpp
|
||
|
||
#include "pw_sync/counting_semaphore.h"
|
||
|
||
pw::sync::CountingSemaphore event_semaphore;
|
||
|
||
void NotifyEventOccurred() {
|
||
event_semaphore.release();
|
||
}
|
||
|
||
void HandleEventsForever() {
|
||
while (true) {
|
||
event_semaphore.acquire();
|
||
HandleEvent();
|
||
}
|
||
}
|
||
|
||
Binary Semaphore
|
||
----------------
|
||
The ``pw::sync::BinarySemaphore`` is a specialization of the counting semaphore
|
||
with an arbitrary token limit of 1, meaning it's either full or empty.
|
||
|
||
.. code-block:: cpp
|
||
|
||
#include "pw_sync/binary_semaphore.h"
|
||
|
||
pw::sync::BinarySemaphore do_foo_semaphore;
|
||
|
||
void NotifyResultReady() {
|
||
result_ready_semaphore.release();
|
||
}
|
||
|
||
void BlockUntilResultReady() {
|
||
result_ready_semaphore.acquire();
|
||
}
|
||
|
||
--------------------
|
||
Threading Primitives
|
||
--------------------
|
||
The :ref:`module-pw_thread` module provides the building blocks for creating and
|
||
using threads including yielding and sleeping.
|
||
|
||
.. list-table::
|
||
|
||
* - **Supported On**
|
||
- **Thread Creation**
|
||
- **Thread Id/Sleep/Yield**
|
||
* - FreeRTOS
|
||
- :ref:`module-pw_thread_freertos`
|
||
- :ref:`module-pw_thread_freertos`
|
||
* - ThreadX
|
||
- :ref:`module-pw_thread_threadx`
|
||
- :ref:`module-pw_thread_threadx`
|
||
* - embOS
|
||
- :ref:`module-pw_thread_embos`
|
||
- :ref:`module-pw_thread_embos`
|
||
* - STL
|
||
- :ref:`module-pw_thread_stl`
|
||
- :ref:`module-pw_thread_stl`
|
||
* - Zephyr
|
||
- Planned
|
||
- Planned
|
||
* - CMSIS-RTOS API v2 & RTX5
|
||
- Planned
|
||
- Planned
|
||
* - Baremetal
|
||
- ✗
|
||
- ✗
|
||
|
||
Thread Creation
|
||
===============
|
||
The ``pw::thread::Thread``’s API is C++11 STL ``std::thread`` like. Unlike
|
||
``std::thread``, the Pigweed's API requires ``pw::thread::Options`` as an
|
||
argument for creating a thread. This is used to give the user full control over
|
||
the native OS's threading options without getting in your way.
|
||
|
||
.. code-block:: cpp
|
||
|
||
#include "pw_thread/detached_thread.h"
|
||
#include "pw_thread_freertos/context.h"
|
||
#include "pw_thread_freertos/options.h"
|
||
|
||
pw::thread::freertos::ContextWithStack<42> example_thread_context;
|
||
|
||
void StartDetachedExampleThread() {
|
||
pw::thread::DetachedThread(
|
||
pw::thread::freertos::Options()
|
||
.set_name("static_example_thread")
|
||
.set_priority(kFooPriority)
|
||
.set_static_context(example_thread_context),
|
||
example_thread_function);
|
||
}
|
||
|
||
Controlling the current thread
|
||
==============================
|
||
Beyond thread creation, Pigweed offers support for sleeping, identifying, and
|
||
yielding the current thread.
|
||
|
||
.. code-block:: cpp
|
||
|
||
#include "pw_thread/yield.h"
|
||
|
||
void CooperativeBusyLooper() {
|
||
while (true) {
|
||
DoChunkOfWork();
|
||
pw::this_thread::yield();
|
||
}
|
||
}
|
||
|
||
------------------
|
||
Execution Contexts
|
||
------------------
|
||
Code runs in *execution contexts*. Common examples of execution contexts on
|
||
microcontrollers are **thread context** and **interrupt context**, though there
|
||
are others. Since OS abstactions deal with concurrency, it's important to
|
||
understand what API primitives are safe to call in what contexts. Since the
|
||
number of execution contexts is too large for Pigweed to cover exhaustively,
|
||
Pigweed has the following classes of APIs:
|
||
|
||
**Thread Safe APIs** - These APIs are safe to use in any execution context where
|
||
one can use blocking or yielding APIs such as sleeping, blocking on a mutex
|
||
waiting on a semaphore.
|
||
|
||
**Interrupt (IRQ) Safe APIs** - These APIs can be used in any execution context
|
||
which cannot use blocking and yielding APIs. These APIs must protect themselves
|
||
from preemption from maskable interrupts, etc. This includes critical section
|
||
thread contexts in addition to "real" interrupt contexts. Our definition
|
||
explicitly excludes any interrupts which are not masked when holding a SpinLock,
|
||
those are all considered non-maskable interrupts. An interrupt safe API may
|
||
always be safely used in a context which permits thread safe APIs.
|
||
|
||
**Non-Maskable Interrupt (NMI) Safe APIs** - Like the Interrupt Safe APIs, these
|
||
can be used in any execution context which cannot use blocking or yielding APIs.
|
||
In addition, these may be used by interrupts which are not masked when for
|
||
example holding a SpinLock like CPU exceptions or C++/POSIX signals. These tend
|
||
to come with significant overhead and restrictions compared to regular interrupt
|
||
safe APIs as they **cannot rely on critical sections**, instead
|
||
only atomic signaling can be used. An interrupt safe API may always be
|
||
used in a context which permits interrupt safe and thread safe APIs.
|
||
|
||
On naming
|
||
=========
|
||
Instead of having context specific APIs like FreeRTOS's ``...FromISR()``,
|
||
Pigweed has a single API which validates the context requirements through
|
||
``DASSERT`` and ``DCHECK`` in the backends (user configurable). We did this for
|
||
a few reasons:
|
||
|
||
#. **Too many contexts** - Since there are contexts beyond just thread,
|
||
interrupt, and NMI, having context-specefic APIs would be a hard to
|
||
maintain. The proliferation of postfixed APIs (``...FromISR``,
|
||
``...FromNMI``, ``...FromThreadCriticalSection``, and so on) would also be
|
||
confusing for users.
|
||
|
||
#. **Must verify context anyway** - Backends are requried to enforce context
|
||
requirements with ``DHCECK`` or related calls, so we chose a simple API
|
||
which happens to match both the C++'s STL and Google's Abseil.
|
||
|
||
#. **Multi-context code** - Code running in multiple contexts would need to be
|
||
duplicated for each context if the APIs were postfixed, or duplicated with
|
||
macros. The authors chose the duplication/macro route in previous projects
|
||
and found it clunky and hard to maintain.
|
||
|
||
-----------------------------
|
||
Construction & Initialization
|
||
-----------------------------
|
||
**TL;DR: Pigweed OS primitives are initialized through C++ construction.**
|
||
|
||
We have chosen to go with a model which initializes the synchronization
|
||
primitive during C++ object construction. This means that there is a requirement
|
||
in order for static instantiation to be safe that the user ensures that any
|
||
necessary kernel and/or platform initialization is done before the global static
|
||
constructors are run which would include construction of the C++ synchronization
|
||
primitives.
|
||
|
||
In addition this model for now assumes that Pigweed code will always be used to
|
||
construct synchronization primitives used with Pigweed modules. Note that with
|
||
this model the backend provider can decide if they want to statically
|
||
preallocate space for the primitives or rely on dynamic allocation strategies.
|
||
If we discover at a later point that this is not sufficiently portable than we
|
||
can either produce an optional constructor that takes in a reference to an
|
||
existing native synchronization type and wastes a little bit RAM or we can
|
||
refactor the existing class into two layers where one is a StaticMutex for
|
||
example and the other is a Mutex which only holds a handle to the native mutex
|
||
type. This would then permit users who cannot construct their synchronization
|
||
primitives to skip the optional static layer.
|
||
|
||
Kernel / Platform Initialization Before C++ Global Static Constructors
|
||
======================================================================
|
||
What is this kernel and/or platform initialization that must be done first?
|
||
|
||
It's not uncommon for an RTOS to require some initialization functions to be
|
||
invoked before more of its API can be safely used. For example for CMSIS RTOSv2
|
||
``osKernelInitialize()`` must be invoked before anything but two basic getters
|
||
are called. Similarly, Segger's embOS requires ``OS_Init()`` to be invoked first
|
||
before any other embOS API.
|
||
|
||
.. Note::
|
||
To get around this one should invoke these initialization functions earlier
|
||
and/or delay the static C++ constructors to meet this ordering requirement. As
|
||
an example if you were using :ref:`module-pw_boot_cortex_m`, then
|
||
``pw_boot_PreStaticConstructorInit()`` would be a great place to invoke kernel
|
||
initialization.
|
||
|
||
-------
|
||
Roadmap
|
||
-------
|
||
Pigweed is still actively expanding and improving its OS Abstraction Layers.
|
||
That being said, the following concrete areas are being worked on and can be
|
||
expected to land at some point in the future:
|
||
|
||
1. We'd like to offer a system clock based timer abstraction facade which can be
|
||
used on either an RTOS or a hardware timer.
|
||
2. We are evaluating a less-portable but very useful portability facade for
|
||
event flags / groups. This would make it even easier to ensure all firmware
|
||
can be fully executed on the host.
|
||
3. Cooperative cancellation thread joining along with a ``std::jthread`` like
|
||
wrapper is in progress.
|
||
4. We'd like to add support for queues, message queues, and similar channel
|
||
abstractions which also support interprocessor communication in a transparent
|
||
manner.
|
||
5. We're interested in supporting asynchronous worker queues and worker queue
|
||
pools.
|
||
6. Migrate HAL and similar APIs to use deadlines for the backend virtual
|
||
interfaces to permit a smaller vtable which supports both public timeout and
|
||
deadline semantics.
|
||
7. Baremetal support is partially in place today, but it's not ready for use.
|
||
8. Most of our APIs today are focused around synchronous blocking APIs, however
|
||
we would love to extend this to include asynchronous APIs.
|