Os/Generic/docs/sdd.md
This package provides generic implementations of various OSAL modules. These are implemented using generic data structures, other OSAL types, and C++ code. These specifically avoid calls down into any underlying operating system.
Available implementations:
Os::PriorityQueue is an in-memory implementation of Os::Queue. It allows projects that desire in-memory queue support to use Os::Queues. Os::PriorityQueue allocates memory for its underlying data structures through the registered Fw::MemAllocator and deallocates it through the same allocator. These actions are taken during create and object destruction. This implies that Os::PriorityQueue should be instantiated and initialized during system initialization.
For memory protection, Os::PriorityQueue delegates to Os::Mutex and Os::ConditionVariable.
[!WARNING] This Queue implementation is insufficient to be used for sending messages in ISR context due to the use of Os::Mutex as mentioned in above.
Priority Ordering: Larger priority numbers have higher priority than lower priority numbers (i.e., priority 0 is the lowest priority, there is no upper bound on priority value).
The requirements for Os::Generic::PriorityQueue are as follows:
| Requirement | Description | Verification Method |
|---|---|---|
| PQ-001 | The PriorityQueue shall implement the Os::QueueInterface for compatibility with F´ components | Inspection, Unit Test |
| PQ-002 | The PriorityQueue shall support message prioritization with higher priority messages dequeued before lower priority messages | Unit Test |
| PQ-003 | The PriorityQueue shall allocate memory during create and deallocate it during destruction using the registered Fw::MemAllocator | Inspection |
| PQ-004 | The PriorityQueue shall use a single shared memory pool for all message priorities | Inspection |
| PQ-005 | The PriorityQueue shall support blocking and non-blocking send operations | Unit Test |
| PQ-006 | The PriorityQueue shall support blocking and non-blocking receive operations | Unit Test |
| PQ-007 | The PriorityQueue shall use Os::Mutex for thread-safe access to queue data structures | Inspection |
| PQ-008 | The PriorityQueue shall use Os::ConditionVariable to implement blocking send/receive | Inspection, Unit Test |
| PQ-009 | The PriorityQueue shall track high water marks for message queue depth | Unit Test |
| PQ-010 | The PriorityQueue shall validate message sizes against the configured maximum | Unit Test |
| PQ-011 | The PriorityQueue shall use the F´ memory allocator registry for all dynamic allocations | Inspection |
| PQ-012 | The PriorityQueue shall return appropriate error codes for queue full, empty, and size mismatch conditions | Unit Test |
| PQ-013 | The PriorityQueue shall use a max-heap data structure for O(log n) priority ordering | Inspection |
| PQ-014 | The PriorityQueue shall NOT be ISR-safe due to mutex usage | Inspection |
[!NOTE] Os::PriorityQueue is simpler than Os::PriorityMemQueue but lacks ISR safety and per-priority configuration capabilities.
Os::PriorityQueue stores messages in a set of dynamically allocated unordered parallel arrays. These arrays store: message data, and message data size respectively. There is also an index-free list that stores the indices that are available for storage in the fixed size arrays.
In order to prioritize messages, a Types::MaxHeap data structure is used.
When a message is received from a calling sender, find_index returns a free index from the free list. The data is copied into the message data array, and the size into the size array using this free index via store_data. The index is then inserted into the max heap structure for prioritization. When the queue is full and the BLOCKING option was supplied, the sender will block on the m_full condition variable until notified of a dequeue.
When a message is dequeued, the highest priority index is removed from the max heap. The data is copied out from the data array, and the size from the size array using that index via load_data and sent to the calling receiver. The index is then returned to the free list via return_index to indicate that it may be reused. When the queue is empty and the BLOCKING option was supplied, the receiver will block on the m_empty condition variable until notified of a enqueue.
If the queue is empty and data was received, the m_empty condition variable is notified to unblock waiting receivers. If the queue is full and data was dequeued, the m_full condition variable is notified to unblock waiting receivers.
The Types::MaxHeap data structure is used to prioritize a list of indices using the given priority. This heap uses a dynamically allocated maximum-length array to back a binary tree storage structure. The first element is the root of the tree, left children are calculated using 2x + 1 and right children using 2x + 2. A node's parent is at (x - 1)/2.
When an index is pushed into this structure, it starts at the first unused element (first free child of some node) and iteratively swaps with its parent as long as the new nodes priority is larger than the parent. This ensures that the higher-priority elements are closer to the root of the tree. Specifically, the root is the highest priority element.
When an index is pulled from this structure, the root is removed as it is the highest priority data structure. The last leaf of the tree is elevated to the root and heapify is then called to restore the max-heap invariant of the data structure.
heapify starts at the newly ill-ordered root. It iteratively swaps this node with the highest-priority child until this node is the largest of the three (parent, left child, and right child) or until this node is swapped into a leaf position without children. The max-heap invariant is now restored.
Os::LocklessPriorityQueue is an ISR-safe, lockless implementation of Os::Queue that provides strict-priority delivery without requiring any operating-system lock. It is intended for flight-software contexts where a producer or consumer may run in interrupt context and therefore cannot block on an OS-level mutex or condition variable.
All memory is allocated exactly once during create through the registered Fw::MemAllocator. No allocation occurs during send, receive, getMessagesAvailable, or getMessageHighWaterMark. The non-blocking variants of send and receive use only lock-free atomic operations and bounded memcpy, making them safe to invoke from ISR context.
[!NOTE] The blocking variants (
BlockingType::BLOCKING) spin-wait and must not be invoked from ISR context.
The queue stores messages in a fixed pool of pre-allocated slots. Each slot is governed by a four-state atomic state machine (FREE -> WRITING -> READY -> READING -> FREE) with an embedded ABA tag that prevents the ABA problem across concurrent producers and consumers.
Producers scan the slot array for a FREE slot, claim it via compare-exchange, populate the message data, and publish with a release store transitioning to READY. Consumers scan for the highest-priority READY slot (with FIFO tiebreak by sequence number), claim it via compare-exchange, copy the message out, and release back to FREE.
All non-blocking control paths are bounded by depth * MAX_RETRY_PASSES. The detailed algorithm, memory-ordering rationale, and requirements traceability are documented in sdd-lockless-queue.md.
Os::PriorityMemQueue is an ISR-safe and SMP-safe, priority-based memory queue implementation for F´ using lock-free atomic circular buffers (AtomicQueue). Each priority level has its own dedicated AtomicQueue, providing O(1) enqueue and dequeue without mutexes or interrupt disable.
The key components are:
The requirements for Os::Generic::PriorityMemQueue are as follows:
| Requirement | Description | Verification Method |
|---|---|---|
| PMQ-001 | The PriorityMemQueue shall implement the Os::QueueInterface for compatibility with F´ components | Inspection, Unit Test |
| PMQ-002 | The PriorityMemQueue shall support up to 32 priority levels | Inspection, Unit Test |
| PMQ-003 | The PriorityMemQueue shall allocate memory pools only for configured priority levels (sparse allocation) | Inspection, Unit Test |
| PMQ-004 | The PriorityMemQueue shall support per-priority configuration of message size and depth | Inspection, Unit Test |
| PMQ-005 | The PriorityMemQueue shall support blocking and non-blocking send operations | Unit Test |
| PMQ-006 | The PriorityMemQueue shall support blocking and non-blocking receive operations | Unit Test |
| PMQ-007 | The PriorityMemQueue shall dequeue messages in priority order, with the highest enabled priority serviced first | Unit Test |
| PMQ-008 | The PriorityMemQueue shall support dynamic enable/disable of individual priority levels | Unit Test |
| PMQ-009 | The PriorityMemQueue shall be configurable to be ISR-safe for message enqueue and dequeue operations | Platform dependent |
| PMQ-010 | The PriorityMemQueue shall track high water marks for message queue depth | Unit Test |
| PMQ-011 | The PriorityMemQueue shall validate message sizes against priority-specific limits | Unit Test |
| PMQ-012 | The PriorityMemQueue shall use the F´ memory allocator registry for all dynamic allocations | Inspection |
| PMQ-013 | The PriorityMemQueue shall return appropriate error codes for queue full, empty, and invalid priority conditions | Unit Test |
| PMQ-014 | The PriorityMemQueue shall provide per-priority O(1) enqueue and dequeue operations | Inspection |
[!WARNING] Send/receive operations from ISR context must not use blocking behavior
[!NOTE] Os::PriorityMemQueue provides ISR safety and per-priority configuration at the cost of increased complexity and memory usage compared to Os::PriorityQueue.
PriorityMemQueue uses AtomicQueue — a lock-free MPMC (multi-producer, multi-consumer) circular buffer — for message storage at each priority level. This provides:
std::atomic with acquire/release ordering on slot sequence numbersstd::atomicEach priority level has its own dedicated AtomicQueue, allocated via the F´ memory allocator:
struct PriorityMemQueueHandle {
I8 m_priorityMap[32]; // Priority→index mapping (-1 = unused)
Types::AtomicQueue* m_atomicQueues; // Array sized to configured priorities
FwSizeType m_numActivePriorities; // Number of configured priorities
std::atomic<U32> m_priorityMask; // Bit mask of enabled priorities
Os::CountingSemaphore* m_notEmptySem; // Semaphore for blocking receive
std::atomic<U32>* m_highWaterMarks; // Per-priority peak depth
};
index = m_priorityMap[priority]AtomicQueue at mapped index (or fallback to default priority 0)atomicQueue->enqueue(buffer, size)m_notEmptySem->post()memory_order_acquireindex = m_priorityMap[priority]index < 0), skip to nextAtomicQueue at mapped indexgetSize() as a cheap pre-filter (2 relaxed loads)getSize() > 0, attempt dequeue() via lock-free CASwait() on semaphore, then re-scanEMPTYLiveness is guaranteed by the semaphore — a spurious getSize() miss simply causes a re-scan after the next semaphore post.
When using non-consecutive priorities (e.g., {0, 15, 31}), allocating arrays sized to maxPriority + 1 wastes significant memory on unused entries.
To mitigate that, the priority queue uses a sparse priority map:
m_priorityMap[32]: Maps priority value → array index (-1 for unconfigured)m_atomicQueues: Sized to numActivePriorities (not maxPriority + 1)m_highWaterMarks: Sized to numActivePrioritiesMemory Savings Example (3 priorities: {0, 15, 31}):
For deployments with many queues using sparse priorities, this approach significantly reduces memory footprint and reduces the complexity of understanding how adding new queue priorities will impact memory use (each priority adds 84 bytes + message memory).
Performance Impact: Negligible (<6 cycles for write operations, <2% overhead for worst case receive)
PriorityMemQueue is the main class that implements Os::QueueInterface. It provides a multi-priority message queue with configurable ISR-safe operations and flexible per-priority configuration.
Priority Ordering: Larger priority numbers have higher priority than lower priority numbers (i.e., priority 0 is the lowest priority, priority 31 is the highest).
Key Features:
PriorityMemQueue uses a counting semaphore for blocking receive operations:
post() called after successful enqueue to signal message availabilitywait() blocks receiver when count reaches 0 (queue empty)wait() returns, receiver dequeues from highest priority with messagesA semaphore is used (as opposed to a condition variable & mutex) because:
The static configuration system allows per-component, per-priority queue sizing. This enables fine-grained memory allocation tailored to each component's messaging patterns.
If a configuration is not provided for a given queue instance ID, then create() will use the message max size and depth arguments for priority Os::Queue::DEFAULT_PRIORITY (0).
priority_buffer_analyzer.py for Message Size CalculationF´ provides priority_buffer_analyzer.py to automatically calculate the maximum message sizes for each priority level based on the topology's port connections. This eliminates manual calculation and ensures correct buffer sizing.
Script Location: ${FPRIME_FRAMEWORK_PATH}/cmake/autocoder/scripts/priority_buffer_analyzer.py
Integration Steps:
Add CMake hook in Deployment CMakeLists.txt:
# Add hook for port priority analyzer
set(PRIORITY_BUFFER_HEADER_DIR "${CMAKE_BINARY_DIR}/${FPRIME_CURRENT_MODULE}/Top")
set(PRIORITY_BUFFER_HEADER "${PRIORITY_BUFFER_HEADER_DIR}/PriorityBufferSizesAc.hpp")
add_custom_command(
OUTPUT "${PRIORITY_BUFFER_HEADER}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${PRIORITY_BUFFER_HEADER_DIR}"
COMMAND ${PYTHON}
"${FPRIME_FRAMEWORK_PATH}/cmake/autocoder/scripts/priority_buffer_analyzer.py"
--build-dir "${CMAKE_BINARY_DIR}"
--topology-path "${PRIORITY_BUFFER_HEADER_DIR}"
--output "${PRIORITY_BUFFER_HEADER}"
COMMENT "Generating PriorityBufferSizesAc.hpp for ${FPRIME_CURRENT_MODULE} deployment"
VERBATIM
)
add_custom_target(priority_buffer_header DEPENDS "${PRIORITY_BUFFER_HEADER}")
# Ensure priority buffer header is generated before building deployment
add_dependencies(${FPRIME_CURRENT_MODULE} priority_buffer_header)
Include generated header in Main.cpp:
#include <Deployment/Top/PriorityBufferSizesAc.hpp>
Use generated constants for configuration:
// The script analyzes topology and generates constants per component/priority
Os::Generic::PriorityMemQueue::QueuePriorityConfig cmdDispPriorityConfigs[] = {
{Os::Generic::Queue::DEFAULT_PRIORITY,
PriorityBufferConfig::F_Prime_Svc_CmdDispatcher::PRIORITY_0, 10},
{2, PriorityBufferConfig::F_Prime_Svc_CmdDispatcher::PRIORITY_2, 20},
{3, PriorityBufferConfig::F_Prime_Svc_CmdDispatcher::PRIORITY_3, 20},
{10, PriorityBufferConfig::F_Prime_Svc_CmdDispatcher::PRIORITY_10, 4},
};
Os::Generic::PriorityMemQueue::QueueConfig queueConfigs[] = {
{Deployment::InstanceIds::CdhCore_cmdDisp,
FW_NUM_ARRAY_ELEMENTS(cmdDispPriorityConfigs),
&cmdDispPriorityConfigs[0]},
};
Os::Generic::PriorityMemQueue::configure(queueConfigs,
FW_NUM_ARRAY_ELEMENTS(queueConfigs),
false,
allocatorId);
Generated Header Structure:
The script generates PriorityBufferSizesAc.hpp with constants for each component and priority level:
namespace PriorityBufferConfig {
constexpr FwSizeType DATA_OFFSET = sizeof(FwEnumStoreType) + sizeof(FwIndexType);
namespace F_Prime_Svc_CmdDispatcher {
// Maximum size for priority 0 messages (Cmd port)
static constexpr FwSizeType PRIORITY_0 =
Fw::CmdPortBuffer::CAPACITY + DATA_OFFSET;
// Maximum size for priority 2 messages (CmdResponse port)
static constexpr FwSizeType PRIORITY_2 =
Fw::CmdResponsePortBuffer::CAPACITY + DATA_OFFSET;
// ... etc
}
}
How It Works:
DATA_OFFSET (priority + port ID overhead) to each sizeBenefits:
DATA_OFFSET)The AtomicQueue-based implementation provides ISR safety through:
std::atomic<U32> with acquire/release ordering — no spinlock neededNONBLOCKING, send and receive never call any blocking OS primitiveISR Usage Pattern:
// From ISR context - use NONBLOCKING
queue.send(data, size, priority, QueueInterface::BlockingType::NONBLOCKING);
queue.receive(dest, capacity, QueueInterface::BlockingType::NONBLOCKING, size, pri);
[!WARNING] Blocking operations (
BlockingType::BLOCKING) use a counting semaphore (post()/wait()) and must NOT be called from ISR context. Semaphore blocking from ISR context is undefined behavior on most RTOSs.
[!NOTE] ISR safety of
post()(called bysend()) depends on the platform's counting semaphore implementation. VerifyOs_CountingSemaphoreISR safety for your target platform before usingsend()from ISR context.