OS Support#

Pigweed AI summary: Pigweed's operating system abstraction layers provide portable and configurable building blocks that offer full control while maintaining high performance and low overhead. These layers are designed to work on a wide range of systems, from small microcontrollers to embedded systems using real-time operating systems (RTOS) and even developer workstations. Pigweed supports various operating systems, including STL, FreeRTOS, Azure RTOS, SEGGER embOS, and Zephyr. The OS abstraction layers are divided into functional groups, such as time

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

In Progress

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#

Pigweed AI summary: The pw_chrono module provides the building blocks for expressing durations, timestamps, and acquiring the current time, which is used by other modules as the basis for any time bound APIs. The module is optional and bare metal targets may opt not to use it. For RTOS and HAL interactions, the module provides a pw::chrono::SystemClock facade which provides 64 bit timestamps and duration support along with a C API. Unlike the STL’s time bound templated APIs which are not specific to a

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#

Pigweed AI summary: The article discusses the use of Pigweed's SystemClock and VirtualSystemClock facades for RTOS and HAL interactions, providing 64 bit timestamps and duration support along with a C API. The article also highlights the difference between Pigweed's time bound APIs and the STL's time bound templated APIs, with Pigweed's APIs being strongly typed to use the SystemClock's duration and time_points directly. Code examples are provided to illustrate the usage of these APIs.

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#

Pigweed AI summary: The pw_sync library provides synchronization primitives for threads and interrupts through signaling primitives and critical section lock primitives. The library supports various platforms such as FreeRTOS, ThreadX, embOS, and STL. The library also provides simpler signaling primitives that are highly portable and efficient for the platform used. These include ThreadNotification, TimedThreadNotification, CountingSemaphore, and BinarySemaphore. The library intends to use the most efficient native primitive for a target, regardless of whether that is a semaphore, event flag group

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 AI summary: The Pigweed locks support Clang's thread safety lock annotations and the STL's RAII helpers. The supported platforms include FreeRTOS, ThreadX, embOS, and STL. The pw::sync::Mutex protects shared data from being simultaneously accessed by multiple threads, while 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).

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#

Pigweed AI summary: The pw::sync::Mutex is used to protect shared data from being accessed by multiple threads at the same time. The pw::sync::TimedMutex can also be used with timeout and deadline based semantics. The code example shows how to use the std::lock_guard with the pw::sync::Mutex to ensure thread safety.

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#

Pigweed AI summary: The Interrupt Safe InterruptSpinLock is a global lock that protects shared data from being accessed simultaneously by multiple threads and interrupts, except for Non-Maskable Interrupts. It is safe and efficient on SMP systems and can be implemented using the pw::sync::InterruptSpinLock library. The InterruptSafeCriticalSection function demonstrates how to use this lock in a critical section.

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#

Pigweed AI summary: The article discusses the challenges of using native signaling primitives across different platforms and the added overhead that comes with building signaling primitives based on other native signaling primitives. Pigweed aims to provide simpler and highly portable signaling primitives that can be implemented efficiently for the platform they are used on. The article also provides information on the different signaling primitives supported by Pigweed on various platforms, including Thread Notification, Counting Semaphore, and Binary Semaphore. Code examples are provided for each primitive.

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 AI summary: Pigweed plans to offer the pw::sync::ThreadNotification and pw::sync::TimedThreadNotification tools, which allow a single user to wait for an event to occur. These tools will be supported by the most efficient native primitive for a given target, such as a semaphore, event flag group, or condition variable.

Pigweed intends to provide the pw::sync::ThreadNotification and pw::sync::TimedThreadNotification facades which permit a single 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, direct task notification with a critical section, or something else.

Counting Semaphore#

Pigweed AI summary: The pw::sync::CountingSemaphore is a synchronization tool that can be used for counting events and managing resources. It allows receivers to block on acquire until notifiers signal by invoking release. The code example provided shows how to use the CountingSemaphore in C++.

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#

Pigweed AI summary: The Binary Semaphore is a type of counting semaphore with a token limit of 1, making it either full or empty. It is used in programming and can be implemented using the pw::sync::BinarySemaphore specialization. The code example provided shows how to use the Binary Semaphore to release and acquire a result ready 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#

Pigweed AI summary: The pw_thread module provides the necessary tools for creating and using threads, including yielding and sleeping. The module supports various operating systems, including FreeRTOS, ThreadX, embOS, and STL. The Pigweed API requires pw::thread::Options as an argument for creating a thread, giving the user full control over the native OS's threading options. Pigweed also offers support for sleeping, identifying, and yielding the current thread.

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#

Pigweed AI summary: The Pigweed thread creation API is similar to C++11 STL std::thread, but requires pw::thread::Options as an argument for creating a thread. This allows for full control over the native OS's threading options. The provided code example shows how to start a detached thread with a specified name, priority, and static context.

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#

Pigweed AI summary: Pigweed offers support for controlling the current thread beyond thread creation, including sleeping, identifying, and yielding. The code example provided shows how to use the yield function in a busy looper.

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#

Pigweed AI summary: The article discusses execution contexts in microcontrollers and the importance of understanding which API primitives are safe to call in different contexts. Pigweed offers three classes of APIs: Thread Safe, Interrupt Safe, and Non-Maskable Interrupt Safe. Instead of having context-specific APIs, Pigweed has a single API that validates context requirements through DASSERT and DCHECK in the backends. This approach was chosen because there are too many contexts to maintain specific APIs, backends must verify context requirements anyway, and multi-context code

Code runs in execution contexts. Common examples of execution contexts on microcontrollers are thread context and interrupt context, though there are others. Since OS abstractions 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#

Pigweed AI summary: The article discusses the benefits of using a single API for context requirements validation in Pigweed, rather than having context-specific APIs like FreeRTOS's "...FromISR()". The reasons for this include the difficulty of maintaining multiple context-specific APIs, the need to verify context requirements anyway, and the challenges of duplicating code for multiple contexts. The authors found that using macros or duplicated code for context-specific APIs was clunky and hard to maintain.

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-specific 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 required to enforce context requirements with DCHECK 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#

Pigweed AI summary: Pigweed OS primitives are initialized through C++ construction, which means that users must ensure that any necessary kernel and/or platform initialization is done before the global static constructors are run. This model assumes that Pigweed code will always be used to construct synchronization primitives used with Pigweed modules, and the backend provider can decide whether to statically preallocate space for the primitives or rely on dynamic allocation strategies. If necessary, an optional constructor can be produced that takes in a reference to an existing native synchronization type.

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#

Pigweed AI summary: This article discusses the importance of initializing the kernel and/or platform before using certain API functions in an RTOS. Initialization functions such as osKernelInitialize() and OS_Init() must be invoked before other API functions can be safely used. To meet this ordering requirement, one can invoke these initialization functions earlier and/or delay the static C++ constructors. The article suggests using pw_boot_PreStaticConstructorInit() to invoke kernel initialization if using pw_boot_cortex_m.

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 AI summary: Pigweed is actively expanding and improving its OS Abstraction Layers. They are working on several areas such as a system clock based timer abstraction facade, a portability facade for event flags/groups, cooperative cancellation thread joining, support for queues and message queues, asynchronous worker queues and worker queue pools, migrating HAL and similar APIs to use deadlines, and adding support for baremetal and asynchronous APIs. These improvements are expected to be implemented in the future.

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.