Reference Manual For QP/C++ 8.1.5
Loading...
Searching...
No Matches
Interface Viewpoint

Time ViewpointObservability Viewpoint

Design Viewpoint


[SDS_QP_IFC]

Design Viewpoint: Interfaces

Purpose

The Interface Viewpoint focuses on defining and describing the interfaces between the QP/C++ Framework and the outside layers, such as the underlying operating system (OS Abstraction Layer) and the QP/C++ Application.

Model Kind

The package diagram shown in Figure SDS-OSAL illustrates the main layers with interfaces under consideration in this viewpoint.

Figure SDS-OSAL: QP Port implementing OS Abstraction Layer.


The OS Abstraction Layer (OSAL) insulates the QP/C++ Framework from the underlying Operating System (OS), such as one of the built-in real-time kernels, 3rd-party RTOS, or a General-Purpose OS. The OSAL provides both the abstract Operating System API (OSAL API), see Figure SDS-OSAL [2A], and the implementation of this API for the specific OS (Figure SDS-OSAL [2B]). The main idea of an OSAL is that the OSAL API is fixed and is used inside the QP/C++ Framework source code, while the OSAL implementation (called the "QP port") depends on the specific underlying OS. In other words, there is a separate "QP port" for every supported real-time kernel, 3rd-party RTOS, or GPOS. The primary focus of this section is to describe the design of the OSAL API, with specific examples taken from various "QP ports" implementing the OSAL API.

Remarks
The OSAL is an example of the "Facade" design pattern [GoF:94].


Design Views: Critical Section

Critical section is a non-blocking mutual exclusion mechanism that protects a sequence of instructions from preemption. QP/C++ Framework, just like any other system-level software, must internally use critical sections to guarantee concurrency-safe operation. The critical section implementation depends on the underlying OS, so the QP OSAL must provide an efficient critical section abstraction to insulate QP/C++ Framework from the specifics of the underlying OS.

For example, in single-core embedded systems, critical sections are typically implemented by disabling interrupts upon entry to the section and re-enabling them upon exit. This is effective because in such systems, interrupts are the only way preemption can be initiated (both interrupt preemption and thread preemption in a real-time kernel). However, other systems (e.g., multicore) might use a different critical section mechanism (e.g., spin-lock). Some other systems (e.g., general-purpose OS) might implement user-space critical sections with the traditional mutual-exclusion mechanism, such as a mutex. The critical section abstraction in QP OSAL must accommodate all such critical section implementations.

Note
The maximum time spent in a critical section directly affects the system's responsiveness to external events (interrupt latency and task-level response). Therefore, all critical sections inside the QP/C++ Framework source code are carefully designed to be as short as possible and are of the same order as critical sections in any commercial RTOS. Of course, the length of critical sections depends on the actual critical section mechanism and the quality of the code generated by the compiler.

[SDS_QP_CRIT]

Design view: Critical Section API.

Description

The critical section abstraction in QP OSAL consists of three parameter-less macros: critical-section status, critical-section-entry, and critical-section-exit.

#define QF_CRIT_STAT ...
#define QF_CRIT_ENTRY() ...
#define QF_CRIT_EXIT() ...

These macros support two main types of critical section implementations:

  1. Simple critical section entry/exit (empty definition of the macro QF_CRIT_STAT);
  2. Save-and-restore critical section status (not-empty definition of the macro QF_CRIT_STAT).

Internally, QP/C++ Framework uses the critical section macros in the following generic way:

. . . // outside critical section
. . . // outside critical section
[3] . . . // inside critical section
. . . // outside critical section
#define QF_CRIT_ENTRY()
Port-specific critical section entry.
Definition qp_port.hpp:57
#define QF_CRIT_EXIT()
Port-specific critical section exit.
Definition qp_port.hpp:58
#define QF_CRIT_STAT
Define the critical section status that was present before entering the critical section.
Definition qp_port.hpp:56

[1] If the macro QF_CRIT_STAT is not-empty, it allocates a local critical-section-status variable. Otherwise, if the macro is empty, the line does nothing.

[2] The macro QF_CRIT_ENTRY() enters the critical section, which saves the critical section status into the status variable (if it was defined at step [1])

[3] This code executes inside the critical section protection

[4] the macro QF_CRIT_EXIT() exits the critical section, which restores the critical section status from the status variable (if it was defined at step [1])

Examples

Example simple critical section macros without critical section status (ARM Cortex-M CPU):

#define QF_CRIT_STAT // empty
#define QF_CRIT_ENTRY() (asm volatile ("cpsid i"))
#define QF_CRIT_EXIT() (asm volatile ("cpsie i"))

Example save-and-restore critical section status macros (MSP430 CPU):

#define QF_CRIT_STAT unsigned int int_state_;
#define QF_CRIT_ENTRY() do { \
int_state_ = _get_interrupt_state(); \
_disable_interrupt(); \
} while (false)
#define QF_CRIT_EXIT() _set_interrupt_state(int_state_)

Nesting Critical Sections

Some CPUs or operating systems require nesting of one critical section in another critical section. The "save-and-restore critical section status" method enables such critical section nesting in an explicit manner. But even the "simple critical section" method can be implemented in a way that allows critical section nesting. For example, the critical section can use an internal up-down nesting counter, which will guarantee that only the last nesting level truly exits the critical section.

Forward Traceability (truncated to 2 level(s))

Active Object OSAL

In addition to the Critical Section abstraction, the QP OSAL also insulates the Active Object abstraction from the specifics of the underlying OS. As described under [SDS_QP_QActive], three attributes of an Active Object are OS-dependent and therefore delegated to the QP port: the type of its event queue, its optional operating-system object, and its optional native execution context (thread). This section describes the actual OSAL API through which a QP port defines these types, using examples taken from several existing QP ports.


[SDS_QP_OSAL]

Design view: Active Object OSAL API.

Description

A QP port defines the Active Object's OS-dependent attributes as three compile-time types:

#define QACTIVE_EQUEUE_TYPE ...
#define QACTIVE_OS_OBJ_TYPE ...
#define QACTIVE_THREAD_TYPE ...

QACTIVE_EQUEUE_TYPE (see [SRS_QP_REQ_020]) defines the concrete type of the Active Object's event queue. When QP/C++ uses one of the built-in kernels (QV, QK, QXK), this is configured to be the native QP::QEQueue, and the Framework's own event-queue implementation (qf_actq.c) provides the Active Object's event-posting and event-retrieval operations. When QP/C++ runs on a 3rd-party RTOS, the port instead typically adapts that RTOS's own native queue type (e.g., QueueHandle_t on FreeRTOS, TX_QUEUE on ThreadX) – in which case qf_actq.c is not used at all (and is excluded from the build), and the RTOS's own native queue implementation takes over event posting and retrieval for the Active Object.

QACTIVE_OS_OBJ_TYPE (see [SRS_QP_REQ_040]) defines an optional, port-specific "operating-system object" attribute, whose exact purpose depends on the port. Ports that use the native QP::QEQueue (which provides no blocking of its own) typically use this attribute to hold the OS synchronization primitive needed to block and later wake the Active Object's thread when the queue is empty – for example, the POSIX port defines it as pthread_cond_t (a condition variable), and the Win32 port defines it as void* (a Win32 event-object handle). Ports that instead adapt the RTOS's own native, already-blocking queue (such as the FreeRTOS and ThreadX ports) can repurpose this attribute differently – for example, the FreeRTOS port uses it to hold the statically-allocated queue and task control blocks required by the FreeRTOS static-object-creation API. Ports based on the built-in, non-blocking, run-to-completion kernels (QV, QK) do not define this type at all, because such kernels never block waiting for events – QV instead returns to its idle loop, and QK falls through to the next-highest-priority ready Active Object.

QACTIVE_THREAD_TYPE (see [SRS_QP_REQ_030]) defines the optional native execution-context ("thread") attribute. Depending on the port, this can be an RTOS thread handle or Thread-Control-Block type (e.g., TaskHandle_t on FreeRTOS, TX_THREAD on ThreadX), a lightweight port-specific identifier (e.g., bool on POSIX, used as a "thread-is-running" flag), or, for ports that do not need a separate OS thread for each Active Object (e.g., the built-in QV and QK kernels), storage repurposed for another port-specific purpose, such as holding MPU/memory-isolation settings.

For ports whose Active Object event queue does not already block and wake on its own – which is the case for the native QP::QEQueue – the OSAL additionally defines a matching pair of macros that customize how the Active Object waits for, and is signaled about, a newly posted event:

#define QACTIVE_EQUEUE_WAIT_(me_) ...
#define QACTIVE_EQUEUE_SIGNAL_(me_) ...

QACTIVE_EQUEUE_WAIT_() is invoked, from inside a critical section, whenever the Active Object's event queue is found empty; it must block the calling thread on the Active Object's OS-object until an event becomes available, and must return with the critical section re-established. QACTIVE_EQUEUE_SIGNAL_() is invoked whenever an event is posted to the queue; it must wake up any thread currently blocked in QACTIVE_EQUEUE_WAIT_(). Ports that adapt an RTOS's own native, already-blocking queue as QACTIVE_EQUEUE_TYPE (such as FreeRTOS and ThreadX) do not need to define these macros at all, since the RTOS's own queue-receive/queue-send calls already provide the blocking and waking. Likewise, ports based on the built-in QV and QK kernels do not define these macros, since their Active Objects are never blocked waiting for events.

Examples

Example from the POSIX port, whose Active Objects use the native QP::QEQueue (and therefore qf_actq.c) and block their pthread on an empty queue using a condition variable:

#define QACTIVE_EQUEUE_TYPE QEQueue
#define QACTIVE_OS_OBJ_TYPE pthread_cond_t
#define QACTIVE_THREAD_TYPE bool
...
#define QACTIVE_EQUEUE_WAIT_(me_) do { \
while ((me_)->eQueue.frontEvt.e == (QEvt *)0) { \
pthread_cond_wait(&(me_)->osObject, &QF_critSectMutex_); \
} \
} while (false)
#define QACTIVE_EQUEUE_SIGNAL_(me_) \
pthread_cond_signal(&(me_)->osObject)

Example from the FreeRTOS port, which adapts the native FreeRTOS queue directly as the Active Object event queue (so qf_actq.c is excluded from the build) and therefore needs neither the wait/signal macros nor an OS-object used for blocking (its OS-object instead holds the FreeRTOS static-allocation control blocks):

#define QACTIVE_EQUEUE_TYPE QueueHandle_t
#define QACTIVE_OS_OBJ_TYPE OsObject // holds StaticQueue_t + StaticTask_t
#define QACTIVE_THREAD_TYPE TaskHandle_t

Example from the built-in, non-blocking QV kernel port, which needs only the event-queue and thread types, and defines neither QACTIVE_OS_OBJ_TYPE nor the wait/signal macros:

#define QACTIVE_EQUEUE_TYPE QEQueue
#define QACTIVE_THREAD_TYPE void const * // repurposed to hold MPU settings

Backward Traceability

  • [SRS_QP_REQ_020] Active Object abstraction shall provide an event queue for each Active Object instance
  • [SRS_QP_REQ_030] Active Object abstraction may provide an optional execution context for each Active Object instance
  • [SRS_QP_REQ_040] Active Object abstraction may provide an optional "operating-system object" for each Active Object instance

Forward Traceability

Time ViewpointObservability Viewpoint