Integrating middleware into an ECU
Overview
This section covers the ECU integration work that starts before service-specific middleware artifacts have been generated (for example generated proxies, skeletons, cluster connection code and message types).
The remaining work in this guide is integration code, not generator output: platform bindings (time provider, logger, OS task ID and concurrency lock types), runtime object wiring, startup ordering and cyclic processing setup.
At a high level, middleware integration has six parts:
Provide platform bindings required by middleware.
Add the middleware deployment configuration.
Add the generated middleware code to the ECU build.
Provide the shared runtime objects used by middleware.
Initialize middleware during ECU startup.
Run the generated cluster processing entrypoints cyclically.
Required inputs and platform bindings
Before starting ECU integration, make sure these upstream inputs already exist:
Generated middleware headers and sources for the service deployment.
Predefined queue and allocator configuration parameters for the target ECU (for example queue counts and allocator capacities).
For terminology and definitions used throughout this guide, see the Glossary.
Middleware time interface
The middleware evaluates timeouts through the platform time interface. The platform integrator must provide an implementation of these functions and link it into the ECU build:
CMake: add a source file defining the functions to any library that links
middleware/middlewareInterfaces(seeexecutables/referenceApp/middlewareConfiguration/platform_integration/time/src/SystemTimeProvider.cppfor the reference implementation).
The interface consists of two functions:
namespace middleware::time
{
uint32_t getCurrentTimeInMs();
uint32_t getCurrentTimeInUs();
}
Integration requirements:
getCurrentTimeInMs()must be monotonic for timeout handling.getCurrentTimeInUs()must be monotonic and provide a microsecond-resolution timestamp for consumers that require finer granularity.Both functions are 32-bit and may wrap around. Implementations must preserve deterministic wrap behavior.
A typical bare-metal implementation reads the platform timer service:
#include "bsp/timer/SystemTimer.h" // platform specific header for getSystemTimeMs32Bit and getSystemTimeUs32Bit
#include "middleware/time/SystemTimerProvider.h"
namespace middleware::time
{
uint32_t getCurrentTimeInMs()
{
return getSystemTimeMs32Bit();
}
uint32_t getCurrentTimeInUs()
{
return getSystemTimeUs32Bit();
}
} // namespace middleware::time
Logger platform interface
The middleware emits diagnostic and error records through a platform logger interface. These functions must be implemented by the platform integrator and linked into the ECU build:
CMake: add a source file defining the functions to any library that links
middleware/middlewareInterfaces(seeexecutables/referenceApp/middlewareConfiguration/platform_integration/logger/src/LoggerImpl.cppfor the reference implementation).
All logger functions below must be defined. Missing definitions will produce linker errors for unresolved logger symbols.
namespace middleware::logger
{
void log(const LogLevel level, const char* const format, ...);
void logBinary(const LogLevel level, const etl::span<const uint8_t> data);
uint32_t getMessageId(const Error id);
} // namespace middleware::logger
Recommended behavior:
Map
LogLevelto your platform severity levels.Keep logging payloads bounded and deterministic in size and processing cost.
Prefer structured, machine-readable payloads for diagnostics and analytics.
Return stable message IDs from
getMessageId(...)for log decoding.
Example implementation:
#include <array>
#include <cstdarg>
#include <cstdio>
#include <etl/span.h>
#include "middleware/logger/Logger.h"
#include "Logger.h" // platform specific header toPlatFormLevel, platformWrite, platformBinaryWrite
namespace middleware::logger
{
namespace
{
constexpr std::size_t kTextBufferSize = 256U;
uint8_t toPlatformLevel(const LogLevel level)
{
switch (level)
{
case LogLevel::Critical: return 1U;
case LogLevel::Error: return 2U;
case LogLevel::Info: return 3U;
// ...add remaining log levels used by your platform.
case LogLevel::None:
default: return 0U;
}
}
void platformWrite(const uint8_t level, const char* text)
{
// Replace this with the target platform sink (DLT/UART/trace backend).
}
void platformBinaryWrite(const uint8_t level, const etl::span<const uint8_t> data)
{
// Replace this with the target platform binary sink (ex. DLT).
}
} // namespace
void log(const LogLevel level, const char* const format, ...)
{
std::array<char, kTextBufferSize> buffer{};
va_list args;
va_start(args, format);
(void)std::vsnprintf(buffer.data(), buffer.size(), format, args);
va_end(args);
platformWrite(toPlatformLevel(level), buffer.data());
}
void logBinary(const LogLevel level, const etl::span<const uint8_t> data)
{
platformBinaryWrite(toPlatformLevel(level), data);
}
uint32_t getMessageId(const Error id)
{
switch (id)
{
case Error::Allocation: return 0x1001U;
case Error::Deallocation: return 0x1002U;
case Error::ProxyInitialization: return 0x1003U;
// ...add stable IDs for all remaining error codes.
default: return 0x10FFU;
}
}
} // namespace middleware::logger
Note
A simulation logger implementation is available at
//libs/bsw/middleware/simulation/ folder for reference implementations.
It is a useful reference when creating a target-specific integration.
OS platform interface
The middleware uses an OS abstraction to identify the current task context. This ID is used for crossthread checks in generated proxies and skeletons.
The function is provided by the platform integrator and linked into the ECU build:
CMake: add a source file defining the function to any library that links
middleware/middlewareInterfaces(seeexecutables/referenceApp/middlewareConfiguration/platform_integration/os/src/OsDefinitions.cppfor the reference implementation).
The platform integrator must provide the following function:
namespace middleware::os
{
uint32_t getProcessId();
} // namespace middleware::os
If this symbol is not defined, linking fails with unresolved
middleware::os::getProcessId.
The function should return a stable identifier for the currently executing task context.
Example implementation:
#include <cstdint>
#include "middleware/os/TaskIdProvider.h"
#include "TaskId.h" // platform specific header for Os::GetCurrentTaskId()
namespace middleware::os
{
uint32_t getProcessId()
{
// Replace this with the target OS API and return a deterministic
// uint32_t identifier for the current execution task context.
return static_cast<uint32_t>(Os::GetCurrentTaskId());
}
} // namespace middleware::os
Note
A simulation reference implementation is available at:
//libs/bsw/middleware/simulation/platform_integration/os/
The function declaration is defined in
libs/bsw/middleware/interfaces/include/middleware/os/TaskIdProvider.h.
Concurrency lock types
The middleware uses two platform-provided lock guards to protect critical
sections in core-local and ECU-shared paths. These types are exposed via
middleware/concurrency/LockStrategies.h and provided by the platform
integrator:
CMake: provide a
middleware/concurrency/lock_types.hheader on the include path of any library that linksmiddleware/middlewareInterfaces(seeexecutables/referenceApp/middlewareConfiguration/include/platform_integration/middleware/concurrency/lock_types.hfor the reference implementation).
The platform integrator must provide both lock types in
middleware/concurrency/lock_types.h under this namespace:
namespace middleware::concurrency::integration
{
struct ScopedCoreLock;
struct ScopedECULock;
} // namespace middleware::concurrency::integration
Usage and ordering guidance:
Use
ScopedCoreLockonly for short core-local critical sections.Use
ScopedECULockfor shared-memory objects visible across cores (queues, allocators, and related shared metadata).
Example implementation shape:
#include <cstdint>
namespace middleware::concurrency::integration
{
struct ScopedCoreLock
{
ScopedCoreLock()
{
// Replace with platform primitive, e.g. disable interrupts.
Platform::DisableInterrupts();
}
~ScopedCoreLock()
{
// Restore previous core state.
Platform::RestoreInterrupts();
}
ScopedCoreLock(ScopedCoreLock const&) = delete;
ScopedCoreLock& operator=(ScopedCoreLock const&) = delete;
ScopedCoreLock(ScopedCoreLock&&) = delete;
ScopedCoreLock& operator=(ScopedCoreLock&&) = delete;
};
struct ScopedECULock
{
explicit ScopedECULock(uint8_t volatile* const mutex)
: mutex_(mutex)
{
// Replace with platform primitive, e.g. spinlock/mutex lock.
Platform::LockMutex(mutex_);
}
~ScopedECULock()
{
Platform::UnlockMutex(mutex_);
}
ScopedECULock(ScopedECULock const&) = delete;
ScopedECULock& operator=(ScopedECULock const&) = delete;
ScopedECULock(ScopedECULock&&) = delete;
ScopedECULock& operator=(ScopedECULock&&) = delete;
private:
uint8_t volatile* mutex_;
};
} // namespace middleware::concurrency::integration
For single-threaded or simulation targets, no-op implementations are valid as long as they preserve the same API and RAII behavior.
Note
Reference implementations are available at:
//libs/bsw/middleware/test/platform_integration/concurrency///libs/bsw/middleware/simulation/platform_integration/concurrency/
The interface aliases are defined in
libs/bsw/middleware/interfaces/include/middleware/concurrency/LockStrategies.h.
Add the generated middleware code to the ECU build
The generated code should be integrated as a normal ECU dependency instead of being used directly from the generator output folder. In practice, the ECU usually defines:
One library containing the generated middleware headers and sources (for example
middleware_generatedwith generated cluster connection and proxy/skeleton code).One library containing shared-memory setup code (for example
middleware_shmwith shared-memory layout types and accessors).
CMake build integration
For CMake-based ECUs, generated code is produced by invoking the code
generator (libs/bsw/middleware/tools/cpp_generator/jinja2cpp.py) from a
custom command driven by the deployment YAML, then compiled into a per-ECU
configuration library together with the platform binding sources.
The reference integration
(executables/referenceApp/middlewareConfiguration/CMakeLists.txt) follows
this pattern:
set(_GEN_SCRIPT ${CMAKE_SOURCE_DIR}/libs/bsw/middleware/tools/cpp_generator/jinja2cpp.py)
set(_GEN_INPUT ${CMAKE_SOURCE_DIR}/libs/bsw/middleware/tools/cpp_generator)
set(_DEPLOYMENT_YAML ${CMAKE_CURRENT_SOURCE_DIR}/model/deployment.yaml)
set(_GEN_DIR ${CMAKE_CURRENT_BINARY_DIR})
add_custom_command(
OUTPUT ${_GENERATED_HEADERS} ${_GENERATED_SRCS}
COMMAND ${Python3_EXECUTABLE} ${_GEN_SCRIPT}
--input ${_GEN_INPUT} --output ${_GEN_DIR}
--deployment-yaml ${_DEPLOYMENT_YAML}
DEPENDS ${_GEN_SCRIPT} ${_DEPLOYMENT_YAML}
COMMENT "Generating middleware C++ code from deployment.yaml"
VERBATIM)
add_library(middlewareConfiguration
${_GENERATED_SRCS}
src/platform_integration/logger/LoggerImpl.cpp
src/platform_integration/os/OsDefinitions.cpp
src/platform_integration/time/SystemTimeProvider.cpp
src/MemoryLayout.cpp)
target_include_directories(middlewareConfiguration
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include
${_GEN_DIR}/include/generated_code)
target_link_libraries(middlewareConfiguration PUBLIC asyncBinding middleware logger)
Key points:
add_custom_commandregenerates code whenever the deployment YAML, templates, or generator script change. List all of them asDEPENDS.Platform binding sources (time, logger, OS, concurrency) are compiled into this same configuration library, not into
middlewareitself. The concrete implementation is an additional source file.Link the resulting configuration library into the final ECU executable. Linking
middlewarealone is not sufficient because it only provides the generic core, not the generated service code or platform bindings.
Initialize middleware during ECU startup
Middleware initialization should be part of the ECU startup sequence, before the application starts executing its regular lifecycle.
For the owner core, the reference order is:
Prepare/attach the shared memory region used by middleware runtime objects.
Construct
middleware::shm::MemoryLayoutexactly once in shared memory.Initialize the generated cluster connections for the local cluster.
Continue with the remaining application startup.
These examples intentionally show only middleware-related responsibilities in
main() and omit non-middleware system and lifecycle setup.
For an owner core, a minimal main.cpp sketch looks like:
#include <etl/typed_storage.h>
#include "shm.h" // Platform specific header for the shared memory access
#include "generated/cluster_connections.h"
int main()
{
// Shared-memory mapping is platform-specific and done before middleware init.
middlewareData.create(); // middlewareData is declared and instantiated in the shm lib
// Initialize local generated cluster connections.
middleware::initializeFooBarClusterConnection();
// ...rest of application startup / scheduler...
return 0;
}
For an attached core, the order is slightly different because shared objects already exist:
Attach to the shared memory region.
Bind to the existing
middleware::shm::MemoryLayoutinstance.Initialize the generated cluster connections for the local cluster.
Continue with the remaining application startup.
For an attached core, a minimal main.cpp sketch looks like:
#include <etl/typed_storage.h>
#include "shm.h" // Platform specific header for the shared memory access
#include "generated/cluster_connections.h"
int main()
{
// Shared-memory mapping is platform-specific and done before middleware init.
// Owner core constructs shared data once; attached core waits and binds.
platform::waitForMiddlewareShmReady();
// Initialize local generated cluster connections.
middleware::initializeFooBarClusterConnection();
// ...rest of application startup / scheduler...
return 0;
}
For 3 or more participating cores, apply the same ownership rule:
owner core maps shared memory and constructs
MemoryLayout.owner core signals construction completion through a platform barrier.
attached core instances wait for that signal and only then bind to
value().Each core initializes only local cluster connection objects and wrappers.
Attached cores can then continue in parallel after the construction-complete barrier has been observed. In general, each core should initialize only the generated cluster connection objects that belong to the middleware cluster executed on that core.
Run cluster processing cyclically in the application lifecycle
Generated proxies and skeletons do not make progress on their own. Each local middleware cluster must be driven by a periodic or otherwise well-defined application lifecycle hook.
In the reference integration, the application lifecycle calls the generated cluster processing function from its cyclic execution path:
void App::cyclic()
{
// Application logic...
middleware::processFooBarCluster();
}
That generated function performs the middleware work for the local cluster:
It takes a snapshot of the incoming queue.
Dispatches messages according to the source cluster.
Calls the generated cluster connection routing logic.
Updates timeouts when requested.
If this cluster processing entrypoint is not called, requests, responses, attribute updates and timeout handling will not progress.
Example mapping: reference ECU flow
A reference ECU can use a compact flow with a provider on one core and a consumer on another:
The middleware build target collects generated code, shared-memory support and wrapper code.
Core 0 prepares shared memory, constructs
middleware::shm::MemoryLayoutand initializes the server-side cluster connection.Core 1 attaches to shared memory, binds to the existing
middleware::shm::MemoryLayoutinstance and initializes the client-side cluster connection.The server application initializes its generated skeleton wrapper.
The lifecycle periodically calls the generated cluster processing function.
This is a good reference pattern, but it should be treated as an example of integration order rather than as the only valid ECU structure.
Extracting statistics from the middleware system
Middleware exposes runtime statistics for queues and memory allocation paths. These statistics are useful during bring-up and can also be exported to diagnostics for continuous health monitoring.
What can be extracted
For each queue instance, you can extract queue statistics including
processedMessages, lostMessages, maxLoad, startupLoad, and
maxFillRate.
For memory behavior, you can read allocator totals via
AllocatorBase::getStats(), which provides allocations,
deallocations, and unknownPtrsError. You can also inspect per-pool
metrics through PoolStats, including successfulAllocations,
failedAllocations, delegatedAllocations, internalFragmentation,
and maxLoad.
How to read and reset metrics
Queue API:
Read current values with
queue->getStats()Start a new measurement window with
queue->resetStats()
Memory API:
Read allocator totals with
allocator.getStats()Read a specific pool via
pool.getPoolStats()Collect all pools via
Aggregator::collectStats(collector)Reset queue statistics explicitly with
queue->resetStats(). Pool statistics should be reset explicitly by the integration if a new pool measurement window is required.
Where to call it in ECU lifecycle
Collect metrics in the same periodic path that already calls the generated cluster processing entrypoint. Use a slower sampling interval than the cluster processing period (for example every 1-15 seconds) to keep overhead bounded.
Recommended pattern:
Call
process<LocalCluster>Cluster()every cycle.Every N cycles read queue and memory statistics.
Publish/log values.
Optionally reset the counters to start a new observation window.
Example extraction loop
void App::cyclic()
{
static uint32_t cycleCounter = 0U;
middleware::processFooBarCluster();
// Example: collect metrics every 100 cycles.
if ((cycleCounter % 100U) == 0U)
{
const auto& qStats = middleware::shm::getQueueToCore0()->getStats();
middleware::logger::log(middleware::logger::LogLevel::Info,
"Q processed=%u lost=%u maxLoad=%u maxFillRate=%u",
qStats.processedMessages,
qStats.lostMessages,
qStats.maxLoad,
qStats.maxFillRate);
const auto& aStats = middleware::shm::getAllocatorCore0()->getStats();
middleware::logger::log(middleware::logger::LogLevel::Info,
"A alloc=%u dealloc=%u invalidPtr=%u",
aStats.allocations,
aStats.deallocations,
aStats.unknownPtrsError);
// Optional window reset.
middleware::shm::getQueueToCore0()->resetStats();
}
++cycleCounter;
}