Porting

MARTe2 has a friendly structure which allows easy porting on different platforms. Essentially, porting comes down to a sequence of basic steps, some of them are mandatory while others are optional, to accomplish the whole porting process.

Porting fundamentals

Starting from fundamentals, the MARTe2 structure will be again represented from the porting point-of-view. MARTe2 porting consists essentially in adaptations on the outer layers (lowermost and uppermost) of its layered structure. Specifically:

  • Core/BareMetal

    • L0Types (Architecture)

    • L1Portability (Architecture/Environment)

    • L6App (Environment)

  • Core/FileSystem

    • L1Portability (Environment)

    • L6App (Environment)

  • Core/Scheduler

    • L1Portability (Environment)

As described above, layers may contain one or both Architecture and Environment subdirectories. While the first contains hardware related porting code, the latter contains software and “operating system” code. The term “operating system” is intentionally quoted. Actually the term is abused as MARTe2 can also be ported on bare metal, directly on hardware without an operating system but with an hardware abstraction layer of software instead, which wraps calls to the underlying platform.

Each of the Architecture and Environment subfolders may contain other folders in turn, which represent the specific platform and operating system for the porting couple. Their naming must follow a convention and must encourage code reusation across combinations. One fundamental advice is to avoid binding Architectures to Environment and to maintain clean and streamlined versions of the two. Should this be impossible, a scheme to avoid direct binding and leave room for code re-utilisation will be further explained (see Architecture/Environment unbinding section). This is done to avoid ARCH x ENV combinations proliferation of (mostly) copy-paste code.

Bundled Architecture and Environment follow this principle, although specific platforms have some exceptions which are handled in a further descripted section (Architecture/Environment unbinding).

Bundled Architecture/Environment folders must reside in a specific directory, located inside MARTe2 directory tree (e.g. Source/Core/BareMetal/L0Types/Architecture/x86_gcc). In order to avoid cluttering, have clear separation and allow independent “unofficial” portings, their placement can be customised by using environment settings.

The aforementioned mechanism works leveraging a Makefile-based structure where
  • The MakeDefaults directory contains definitions (MakeStdLibDefs) and rules (MakeStdLibRules) for each couple of Architecture and Environment

  • The MakeDefaults directory can be placed inside the MARTe2 one for bundled configuration or moved outside for customisation

  • Each top-level Makefile knows its own MARTe2 tier (BareMetal/FileSystem/Scheduler) and level (L0, L1, …, L6). This is particulary true in L0, where TIER and LAYER symbols are needed to build include paths

  • The TARGET environment variable is used to derive the Architecture and Environment couple (i.e. their directory name)

  • Makefile are defaulting to MARTe2 Architecture/Environment path but two environment variables allow them to be placed elsewhere

For instance, a complete workflow for an existing target configuration is (e.g. Ultrascale+ porting running with FreeRTOS)

TARGET=armv8_gcc_a53-freertos_us

This translates Architecture to armv8_gcc_a53 and Environment to freertos_us which means going into the MakeDefaults directory and search for the following two files:

MakeStdLibDefs.armv8_gcc_a53-freertos_us
MakeStdLibRules.armv8_gcc_a53-freertos_us

The MakeStdLibDefs file defines two new variables (i.e. ENVIRONMENT=FreeRTOS and ARCHITECTURE=armv8_gcc) which are used for the subsequent Makefiles to search and find the right Environment and Architecture subdirectories. Please note that the files may have further naming convention, which can be used to do subdivisions. In the specific example, the _a53 suffix stands for the eventual CPU specialisation and _us stands for Ultrascale+ environment.

Base folder customisation

In order to have independent portings, without affecting the cloned MARTe2 repository, the MakeDefaults directory can be customised. The MakeDefaults directory can be configured by means of the MARTe2_MAKEDEFAULT_DIR

Once the TARGET environment variable is set, inside that folder you must have (at least) the MakeStdLibDefs.arch-env and the MakeStdLibRules.arch-env files.

At this point, these two files may combine already existing Architecture/Environment or specify new ones. Using the MARTe2_PORTABLE_ENV_DIR and MARTe2_PORTABLE_ARCH_DIR environment variables, you can also move outside the MARTe2 directory tree your own porting implementation, leaving unaffected the original folder content and structure, given however they remain hierarchically structured as their bundled counterpart.

Architecture/Environment unbinding

In some scenarios it may be difficult a total separation between architecture and environment. When this happens, it is advisable to use the following pattern. This approach can be already found inside some of the actual MARTe2 portings.

  • Create a standard porting file, where instead of the real implementation you only put a #define guarded include

  • Only in the included file, use the needed “mixed” approach

  • In your MakeStdLibDefs.arch-env add the ENV_ARCH_CUSTOMIZATION variable

For example, the CompilerTypes.h file for the Xilinx Ultrascale+ implementation has a non-generic armv8 gcc porting. To achieve that, a standard CompilerTypes.h is produced:

#ifdef XILINX_ULTRASCALE
#include "CompilerTypesXil.h"
#define CUSTOMIZED_COMPILERTYPES_HEADER
#endif
#ifndef CUSTOMIZED_COMPILERTYPES_HEADER
#include "CompilerTypesDefault.h"
#endif

As shown above, the ENV_ARCH_CUSTOMIZATION is brought inside the CompilerTypes.h file and its value is used to distinguish between clean and targeted implementation. Note also the approach allows multiple implementations and a fallback CompilerTypesDefault.h implementation. The fallback implementation is, effectively, the default clean one.

Portable test environment

MARTe2 default test suite is based on GTest. Another option, which comes handy when porting MARTe2 is the availability of a portable test environment. The portable test environment is based on a mechanism that offers an interface compatible to the one offered by GTest but way simpler in terms of implementation and dependencies. Standard makefiles default to the GTest suite, the setting of the MARTe2_TEST_ENVIRONMENT variable to "Portable" instead, switches to the Portable. The portable environment, used in conjunction with the static test mode, produces a whole test executable which can be executed on the ported environment.

To reduce the size of the executable or the scope of the tests, a test link filter is available. It is enabled only when MARTe2_TEST_ENVIRONMENT is set to "Portable" and enables another two options: MARTe2_TEST_LINK_FILTER_MODE and MARTe2_FILTER_ITEMS.

The MARTe2_TEST_LINK_FILTER_MODE environment variable controls the level at which the filter has to operate while the MARTe2_FILTER_ITEMS controls what, also according to the mode, has to be included or excluded from the linking step in the executable generation.

The MARTe2_TEST_LINK_FILTER_MODE can be: - Layer, the filtering happens at whole Tier/Layer level, meaning that the MARTe2_FILTER_ITEMS will contain the name of the layers which have to be included/excluded; - Library, the filtering happens at single test object level, meaning that the MARTe2_FILTER_ITEMS will contain the name of the test objects that have to be included/excluded;

The MARTe2_FILTER_ITEMS environment variable must contain a colon (:) separated list of the names of the layers/library to include or exclude. When only the name is specified (e.g. L1PortabilityBGTest.a) the linking operates in whitelist mode and includes only the specified library. When the name is preceded by a minus sign (-) (e.g. -L1PortabilityBGTest.a) the linking operates in blacklist mode, a whole list of the available layers will be built and, if and only if the specified name is found, it will be removed from the list of the linked objects.

When operating in Layer mode, the MARTe2 naming convention helps filtering in/out the right tier as a capital letter (B for BareMetal, S for Scheduler and F for FileSystem) is appended to the layer name itself (e.g. L1PortabilityBGTest.a or L2ObjectsBGTest.a)

Note that the filter cannot operate in mixed black/whitelist mode (e.g. you cannot mix L1PortabilityBGTest.a:-L2ObjectsBGTest.a). If you both include and exclude an error will be generated.

MARTe2 startup hooks

Some embedded deployment scenarios require that hardware initialisation functions start before any other code is executed. This is particularly useful when deploying the full MARTe2 Application, which owns already a main() entry point. MARTe2 has currently two (2) hooks which are called during the startup phase, before the GlobalObjectsDatabase constructor begins its initialisation loop:

InitHardware() {[...]}
InitPlatform() {[...]}

Their declaration stays inside BareMetal/L1Portability/HardwareI.h and you should provide an implementation (at least an empty one if not needed). Implementation of these methods are intended to initialise the underlying hardware platform before the MARTe initialisation process begins, like in embedded or microcontroller-based platforms, where peripherals (e.g. U[S]ART) should be initialised beforehand (e.g. console output functions). They are called subsequently, one after the other without any other intermediate call (e.g. InitHardware(); InitPlatform();).

Bootstrapping advices

If the platform initialisation does not require to be executed before the GlobalObjectsDatabase construction, it can be accomplished at a later stage in the ported Bootstrap.cpp. This is the case of the current FreeRTOS implementation, where a MARTe2HardwareInitialise() extern function is expected to be implemented from the hardware platform side.

In the specific aforementioned FreeRTOS implementation, as the platform hardware initialisation may be required to run in a task execution environment, MARTe2 waits for a vTaskNotification to resume its operations. In that scenario, the minimal MARTe2HardwareInitialise() call must notify the caller task to start MARTe2.

Walkthrough

The porting guide is supported by the following walkthrough, in order to speedup the boilerplate operations needed to begin the MARTe2 porting process.

Step 1 - Makefile Rules and Definitions

In the first step you will need to create a suitable directory to hold the two Makefile Rules and Definitions for your architecture. Makefile naming convention needs for these two to be named with the following scheme [architecture]-[environment] where

  • [architecture] can also be split into subarchitectures (e.g. armv8_gcc or x86_gcc or x86_cl)

  • [environment] can also be split into subenvironments (e.g. freertos_us or bare_us)

Step 2 - Change MakeStdLibDefs file

Inherit an existing MakeStdLibDefs with default needed compiler defines by changing the first inclusion. You can use a shipped one or start from one of them to adapt to the ported platform.

Update the ENVIRONMENT and ARCHITECTURE (see above) with the names your architecture and environment combination should have. Note that these two names will match the two Architecture/Environment subdirectories under each ported layer For tidyness, their name should be given accordingly to their internally set ARCHITECTURE``and ``ENVIRONMENT variables.

Update the MARTe2_LINK_MODE variable to suit your environment. Allowed values are Static or anything else (also none or undefined). This value will affect the MARTeApp linking step.

Update the MARTe2_TEST_ENVIRONMENT variable to suit your needs. Allowed values are Portable and GTest. This value will affect the MARTe2 test suite generation. Undefined defaults to GTest.

Append, by using INCLUDES+=, eventual additional directories which are needed during the compilation process. Keep in mind that these are the ones where both ported architecture and environment code will source. Usually here goes the BSP (Board Support Package) path where all the includable headers for the platform reside. To keep the code clean and independent from the path, you can use your own environment variable to point the includes to the right place.

Update the (a) COMPILERPREFIX, (b) COMPILER, (c) CCOMPILER and (d) LINKER with your
  1. prefix for all the names (e.g. aarch64-none-elf);

  2. C++ compiler (e.g. -g++);

  3. C compiler (e.g. -gcc) and (d) linker (e.g. -ld). The COMPILERPREFIX could be omitted, as its purpose is solely aestethic, avoiding repetition.

Update the CPUSPECIFICFLAG, ADDITIONALFLAGS, OPTIM and CFLAGS according to your own platform specifications. Note that the only significant variable is CFLAGS, which is built appending the others.

Step 3 - Change MakeStdLibRules file

Inherit an existing MakeStdLibRules with default needed compiler defines by changing the first inclusion. You can use a shipped one or start from one of them to adapt to the ported platform. Please note here that rules are also named with a convention and they inherit/override settings (e.g. gcc-dynamic vs gcc-static).

Step 4 - Set the right environment variables

In contrast to standard shipped MARTe2, the MARTe2_DIR environment variable must be set in order for the makefiles chain to become able to pick MARTe2 and own ported files. This becomes true especially if using the external Makedefaults/Architecture/Environment directories configuration.

At a bare minimum, the following environment variables must be set:

  • MARTe2_MAKEDEFAULT_DIR (where aforementioned MakeStdLibDefs and MakeStdLibRules reside)

  • MARTe2_PORTABLE_ENV_DIR (where the environment-related porting files reside)

  • MARTe2_PORTABLE_ARCH_DIR (where the architecture-related porting files reside)

Plus, setting the environment variable MARTe2_TEST_ENVIRONMENT, sets the right test environment to be compiled for MARTe. As above stated, this variable can be GTest or Portable. If planning to port for embedded architecture, prefer the Portable environment, unless sure that the GTest suite is able to be compiled. Also take into account the link filter previously explained. Note that the MARTe2_TEST_ENVIRONMENT defaults to GTest. To avoid test compilation, which is useful in the first development phases, you can disable their compilation by issuing

"make -f Makefile.gcc core"

After the bare minimum variables, you must also consider the BSP files location which will be needed for your own platform porting. This variable will be used in the aforementioned INCLUDES+= line inside the MakeStdLibDefs file previously written.

Step 5 - Create the architecture and environment files

Use the provided stubs to speedup the development process or take them from an existing implementation. Consider that the stubs or, alternatively, your own must provide an implementation for every function defined (no __weaks symbols) eventually returning a failure or fail-safe value.

Also consider that, given that the Architecture and Environment directories are different, porting may be only oriented to just one of them. This could be achieved by setting accordingly the MARTe2_PORTABLE_ARCH_DIR and MARTe2_PORTABLE_ENV_DIR variables.

Warning

If other ancillary files are needed, aside the mandatory porting ones, keep in mind to add their .x line in the Makefile.inc.

Quick reference on files to change

Files which are needed are (1)

  • BareMetal - L0Types - Architecture

    • CompilerTypes.h

  • BareMetal - L1Portability - Architecture

    • AtomicA.h (2)

    • EndianityA.h (3)

    • FastMathA.h

    • HardwareIA.h (4)

    • HighResolutionTimerA.h (5)

    • ProcessorA.h (6)

  • BareMetal - L1Portability - Environment

    • BasicConsole.cpp (7)

    • GeneralDefinitions.h (8)

    • HardwareI.cpp (9)

    • HighResolutionTimer.h (10) + HighResolutionTimerCalibrator.h/.cpp

    • LoadableLibrary.cpp (11)

    • Sleep.cpp (12)

    • StandardHeap.cpp (13)

    • Timestamp.cpp (14)

  • BareMetal - L6App - Environment

    • Bootstrap.cpp (15)

  • FileSystem - L1Portability - Environment

    • BasicFile.cpp (16)

    • BasicFileProperties.h (17)

    • BasicSocket.cpp (18)

    • BasicTCPSocket.cpp

    • BasicUART.cpp (19)

    • BasicUARTProperties.h

    • Directory.cpp (20)

    • DirectoryCore.h

    • DirectoryScanner.cpp

    • InternetHost.cpp (21)

    • InternetHostCore.h

    • InternetMulticastCore.h

    • InternetService.cpp (22)

    • InternetServiceCore.h

    • Select.cpp (23)

    • SelectProperties.h

    • SocketCore.h (24)

  • Scheduler - L1Portability - Environment

    • EventSem.cpp

    • MutexSem.cpp

    • Processor.cpp

    • Threads.cpp (25)

    • ThreadDatabase.h (26)

Notes

Based on the file listing above, some notes are provided about specific files porting instructions.

  • (1): provided stubs are empty do-nothing implementation of the described methods.

  • (2): when porting AtomicA.h, prefer intrinsics over inline assembly where possible (e.g. __atomic_test_and_set)

  • (3): when porting EndianityA.h, ensure your platform endianess and implement accordingly the To/From functions. Some of them may result in do-nothing functions (e.g. the ToLittleEndian in a little endian architecture). Consider that some architectures may be both configured as big and little. Prefer intrinsics to achieve endiannes swap (e.g. __bswap/16/32/64).

  • (4): HardwareIA.h holds the function that will be called at startup, which is intended to be called as hardware initialisation.

  • (5): Native microseconds-granular counters are preferred, to suit internal calculations (delays, etc).

  • (6): Can also be implemented to return a fixed compile-time value if cpuid mechanism is unwanted/unnecessary as not fundamental.

  • (7): Can also be empty-implemented like the stub, if the console is unavailable (e.g. headless or unsupervised) or unneeded.

  • (8): Catch all container for all baseline stuff (e.g. handles and constants).

  • (9): Contains the code which must be run before MARTe2 GlobalObjectsDatabase construction, see HardwareIA.h and notes above.

  • (10): If the vanilla 32/64 bit counter is available without further initialisations, the implementation can be straightforwardly included in HighResolutionTimer.h (stateless, no class, direct inline methods). Else, if the HRT needs a stateful implementation or a peripheral initialisation beforehand, the HRT calibrator approach can be used. This means that you have an instance of the calibrator, which is a class in charge of the initialisation and preparation of the timer/counter which lies statically and externally referenced from the HighResolutionTimer.h file inside the inlined methods. This approach can be translated anywhere else, to suit similar needs.

  • (11): If the porting is based on dynamic linking and runtime module loading, place here suitable functions (see Linux dlopen())

  • (12): Sleep functions here are meant to be, if available, a non-busy sleep alternative from the OS (e.g. yielding like the FreeRTOS vTaskDelay()).

  • (13): Must implement own heap management function (alloc/free/realloc/…). Use OS-aware primitives when available (e.g. FreeRTOS vPortMalloc/vPortFree)

  • (14): Timestamp functions can be left empty if the platform has not RTC

  • (15): Functions can be semantically split between BareMetal/L6App and FileSystem/L6App bootstraps

  • (16): Handle variable, used across FileSystem implementations, stays in GeneralDefinitions.h. It assumes a POSIX style handle.

  • (17): Place here convenient structure to hold basic file information (see Linux implementation).

  • (18): Needs aformentioned Handle and InternetHost class to be defined, see below.

  • (19): Use BasicUARTProperties.h to define a suitable structure for UART configuration data (bps, data, stop, parity bits, …)

  • (20): Use DirectoryCore.h to define a suitable structure to hold directory stat.

  • (21): Use InternetHostCore.h to define a suitable structure to hold IP/host data.

  • (22): Use InternetServiceCore.h to define a suitable structure to hold IP/service data.

  • (23): Use SelectProperties.h to define a suitable structure to hold select data (POSIX fd_set style).

  • (24): Used by BasicSocket as base handle.

  • (25): If the OS/BM does not include a thread database, the ThreadDatabase can be used instead, see ThreadDatabase.h

  • (26): Defaults to a MARTe2 based GenericThreadDatabase, which can be used to store/retrieve currently spawned threads. Be aware that, if not using the database, that some informations may be different between the real number of running threads and the MARTe2 perceived one. For example, if bypassing the ThreadsDatabase, asking directly to the OS the number of threads returns a number that may be inconsistent for the MARTe2 point-of-view. This is especially true in some tests, which expect a zero (0) number of threads still running after the completion. The zero (0) value has to be intended as “MARTe2 has 0 threads running” and not as “The system has 0 threads running”. (See socket test suite).