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:

Environment

Status

STL (Mac, Window, & Linux)

✔ Supported

FreeRTOS

✔ Supported

Azure RTOS (formerly ThreadX)

✔ Supported

SEGGER embOS

✔ Supported

Baremetal

In Progress

Zephyr

Planned

CMSIS-RTOS API v2 & RTX5

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 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 pw_sync and 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.

Supported On

SystemClock

FreeRTOS

pw_chrono_freertos

ThreadX

pw_chrono_threadx

embOS

pw_chrono_embos

STL

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.

#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.

#include "pw_chrono/system_clock.h"

bool HasThisPointInTimePassed(const SystemClock::time_point timestamp) {
  return SystemClock::now() > timestamp;
}

Synchronization Primitives

The 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.

Supported On

Mutex

TimedMutex

InterruptSpinLock

FreeRTOS

pw_sync_freertos

pw_sync_freertos

pw_sync_freertos

ThreadX

pw_sync_threadx

pw_sync_threadx

pw_sync_threadx

embOS

pw_sync_embos

pw_sync_embos

pw_sync_embos

STL

pw_sync_stl

pw_sync_stl

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.

#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.

#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.

Supported On

ThreadNotification

TimedThreadNotification

CountingSemaphore

BinarySemaphore

FreeRTOS

pw_sync_freertos

pw_sync_freertos

pw_sync_freertos

pw_sync_freertos

ThreadX

pw_sync_threadx

pw_sync_threadx

pw_sync_threadx

pw_sync_threadx

embOS

pw_sync_embos

pw_sync_embos

pw_sync_embos

pw_sync_embos

STL

pw_sync_stl

pw_sync_stl

pw_sync_stl

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.

#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.

#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 pw_thread module provides the building blocks for creating and using threads including yielding and sleeping.

Supported On

Thread Creation

Thread Id/Sleep/Yield

FreeRTOS

pw_thread_freertos

pw_thread_freertos

ThreadX

pw_thread_threadx

pw_thread_threadx

embOS

pw_thread_embos

pw_thread_embos

STL

pw_thread_stl

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.

#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.

#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:

  1. 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.

  2. 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.

  3. 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 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.