High Performance OPC UA Server SDK  1.1.0.158
SDK Porting Guide

Introduction

The SDK supports for multiple platforms. When buying the SDK you choose one or more of the existing platforms, which are ready to use (e.g. Linux or Windows). If your platform is not supported you can choose any platform and port the SDK yourself to your target platform. Typically the Linux PL is a better example for porting, because many systems share the C and POSIX functions used by Linux PL, whereas Windows uses a very special API, that is normally not available on other systems. It may also be important where you are developing the main time. Many customers are developing on a Windows (or Linux) PC and are cross compiling only for testing. In this case it may be wise to choose the operating system of your PC to simplify the development process.

Important to understand is that the platform layer in src/platform is just a part of the whole platform adaption. The reason for that is that the platform adaption not only encapsulates operating system specific details, but also other software APIs which may exist on multiple platforms (e.g. OpenSSL), and so are platform independent.

highperformancesdk_platformlayer.png
Platform Layer Architecture

The following table gives an overview about all platform adaption modules and the available implementations.

Module Description Available Implementations
platform operating system adaption win32, linux, SeggerembOS
network network API adaption berkeley, epoll
trace trace adaption stdout, file
timer timer adaption soft
crypto crypto API adaption openssl, mbedtls, Mocana NanoCrypto
pki PKI API adaption openssl, mbedtls, Mocana NanoCrypto

The following table gives a more detailed explanation about the available platform backend modules.

Backend Implementation Description
platform/win32 Platform Layer for Windows Win32 API
platform/linux Platform Layer for Linux
platform/embos Platform Layer for Segger embOS
network/berkeley Network implementation using Berkeley Sockets (works on Linux, Windows, and embOS/IP)
network/epoll Network implementation using Linux epoll instead of select
trace/stdout Simply prints the trace using fprintf to stdout/stderr
trace/file Trace to file using fopen/fprintf/fclose
timer/soft Software timer derived from Berkeley select timeouts. Based on platform_tickcount()
crypto/openssl Crypto API implementation using OpenSSL (libcrypto.so, no libssl.so required)
crypto/mbedtls Crypto API implementation using mbedTLS
crypto/nanocrypto Crypto API implementation using Mocana NanoCrypto
pki/openssl PKI API implementation using OpenSSL (libcrypto.so, no libssl.so required)
pki/mbedtls PKI API implementation using mbedTLS
pki/nanocrypto PKI API implementation using Mocana NanoCrypto

Tip: To get started you should disable Crypto and PKI so you don't need to care about these modules. Trace can also be disabled completely, however a working printf is normally not a problem and the trace can help you in analyzing problems. If a Berkeley sockets API is available on your system you should start with network/berkeley. Later on the network backend can be replaced with a more efficient version.

Platform Layer

Each platform contains an own backend folder located in SDK/src/platform. These folders contain platform specific implementations which will be needed to be adapted for new platforms. The rest of the SDK is generic C code and must not be changed. The SDK platform is split into two main parts. The platform independent frontend files and the backend files, which are platform specific.
This tutorial is based on an ARM compiler but most of its content is generic. Just replace the example compiler with your compiler and configuration.

Frontend Files

The frontend files are located in SDK/src/platform. Theses files are containing generic code and wrapper functions for the platform specific functions. These files must no be changed.

Backend Files

The backend files, all with a "p" prefix, contain platform dependent functions. The backend files are used by the frontend functions only and won't be called directly by applications. To add support for a new platform, these backend functions need to be created for the new platform.

  • pfile.c
  • pguid.c
  • pipcmem.c
  • pmemory.c
  • pmutex.c
  • pplatform.c
  • pprocess.c
  • psemaphore.c
  • pshutdown.c
  • ptime.c

CMake

The general procedure of cross compiling using CMake is the same as when compiling locally. The main difference is that you add the parameter -DCMAKE_TOOLCHAIN_FILE=<path to file> to your CMake arguments. This tells CMake to use the cross compiler toolchain specified in the given toolchain file when generating a make file. When the make file was generated you simply type make as usual to compile the code for the target. The advantage is obvious. You don’t need any platform specific parts in your CMake projects (CMakeList.txt files). The toolchain parts are separated from the generic project definitions like what files to compile, what libraries to link or what defines to add. All platform specific settings like system libraries, compiler and linker flags are defined in the toolchain file.

Building

Building can be done manually by invoking CMake and Make or by using the build.sh script. The advantage of the build.sh script is that it can load configurations from different files which makes it easy to maintain different build configurations. For that reason the following examples will use the build.sh script. There are two important options for this script:
• -p <profile>: Profile selection. This configures a set of different optional functionalities. The build.sh will source the file profile_config_<profile>.
• -t <target>: Target selection. The build.sh will source the file target_config_<target>. This file specifies what toolchain file to use and what build folder and installation prefix should be used.

Prepare Files for a New Platform

At first the preparation folder structure for the new platform is necessary.

Create Backend Folder

Create a copy of SDK/src/platform/template named demoport.
This template contains all platform backend files with empty functions. All functions will return a negative result, so all unit tests will fail.
Open platform_backend.cmake and replace "template" with "demoport":

# collect sources
set(PLATFORM_SOURCES
demoport/pplatform.c
demoport/ptime.c
demoport/pmemory.c
demoport/pguid.c
demoport/pipcmem.c
demoport/pshutdown.c
demoport/psemaphore.c
demoport/pprocess.c
)
# collect headers
set(PLATFORM_HEADERS
demoport/pplatform.h
demoport/ptime.h
demoport/pmemory.h
demoport/pguid.h
demoport/pipcmem.h
demoport/pshutdown.h
demoport/psemaphore.h
demoport/psemaphore_types.h
demoport/pprocess.h
demoport/pprocess_types.h
)
if (CONFIG_THREADSAFE)
set(PLATFORM_SOURCES ${PLATFORM_SOURCES} demoport/pmutex.c)
set(PLATFORM_HEADERS ${PLATFORM_HEADERS} demoport/pmutex.h demoport/pmutex_types.h)
endif()
if (SUPPORT_FILE_IO)
set(PLATFORM_SOURCES ${PLATFORM_SOURCES} demoport/pfile.c)
set(PLATFORM_HEADERS ${PLATFORM_HEADERS} demoport/pfile.h demoport/pfile_types.h)
endif()

Create Config File

Create a file called target_config_demoport in SDK root folder with the following content:

# This file gets sourced by build.sh to configure cross compilation
# Run ./build.sh -t demoport
# build directory, here all temp objects will be created (out-of-source-build)
BLDDIR=$PWD/bldDemoport
# destination directory for 'make install', this folder can be distributed as
# binary SDK.
DISTDIR=$PWD/distDemoport
# Compiler toolchain configuration for CMake
TOOLCHAIN=$PWD/toolchains/demoport/demoport.cmake
# Default build type: Debug/Release,MinSizeRelease,RelWithDebInfo
BUILD_TYPE=Debug
# Further CMake build options for this target
# disable security
OPTIONS="$OPTIONS -DTEST_TARGET_DIR=$TARGET_DIR"
# minimal provider
OPTIONS="$OPTIONS -DSERVER1_DEMO_PROVIDER_ENABLED=on SERVER1_MINIMAL_PROVIDER_ENABLED=off"

Prepare Cross Compiling

The following example, shows how to create a toolchain file and compile a minimal project for the desired platform.

Create a Toolchain File

Add a minimal toolchain file to cross compile projects for desired target system. The following values are mandatory:

Variable Description
cmake_system_name set system_name to "generic" to enable cross compiling
platform_name the platform name
toolchain_root path to the toolchain root (containing libs and headers)
cmake_c_compiler set the c cross compiler
cmake_cxx_compiler set th cpp cross compiler

For this example, the toolchain file looks like this:

#set system name to generic to enable cross compiling
set(CMAKE_SYSTEM_NAME Generic)
#set the name of the desired platform
set(PLATFORM_NAME "port_demo")
#set toolchain root
if (EXISTS $ENV{TOOLCHAIN_ROOT})
set (TOOLCHAIN_ROOT "$ENV{TOOLCHAIN_ROOT}")
else()
message(FATAL_ERROR "toolchain root not set!")
endif()
#set the cross compiler
#replace this part with your compiler
set(CMAKE_C_COMPILER ${TOOLCHAIN_ROOT}/bin/arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER ${TOOLCHAIN_ROOT}/bin/arm-none-eabi-g++)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --specs=nosys.specs" CACHE INERNAL "")

Cross Compile a Minimal Project

To check if the created toolchain file is correct, a minimal test program is created, which will be built without any SDK sources.
A simple hello world program is located in SDK/src/hello.
It contains a main.c, which simply prints "Hello World" and a CMakeLists.txt.

Generate Program

To generate and compile the applications, the following steps are necessary:

  1. Set TOOLCHAIN_ROOT
    Location of the installed toolchain containing libs, headers
  2. Set INSTALL_PREFIX
    The install prefix defines the location of the output (e.g. .bin, .hex)
  3. Set TOOLCHAIN_FILE
    Location of the toolchain file created above (if this step is skipped, the project will be compiled natively)
  4. generate makefiles
    call cmake with the parameters specified above to create makefile
  5. build project
    build the actual project
  6. install project
    copies created file to install destination

These steps are processed with following shell script.

#!/bin/bash
#(1) set TOOLCHAIN_ROOT to specify compiler location
export TOOLCHAIN_ROOT=~/work/toolchains/gcc-arm-none-eabi-5_3-2016q1
#(2) use the current directory as installation folder
INSTALL_PREFIX=$PWD
#(3) specify toolchain file to enable cross compiling
TOOLCHAIN_FILE=$PWD/toolchainfile.cmake
#create bld folder, if it doesn't exists
if [ ! -d "bld" ]; then
mkdir bld
fi
#cmake output will be stored in bld folder
cd bld
#(4) generate makefiles
cmake -DCMAKE_INSTALL_PREFIX=$INSTALL_PREFIX -DCMAKE_TOOLCHAIN_FILE=$TOOLCHAIN_FILE . ..
#(5) build project
make
#(6)install the project, copies binary in install folder
make install

After running build.sh, the generated files should be found in SDK/examples/minimal/bin

Test Generated File

Now load the resulting executable to the target system to test the created binary.

Cross Compile SDK

Use the previously created configuration file with the nano profile to build the SDK with a minimal configuration. This profile disables a lot of functionality of the SDK.

#build minimal SDK
cd SDK
./build.sh -p nano -t demoport

Implement Backends

Now the SDK should compile for the desired target system, but it won't work yet. The platform specific functions have to be implemented in the backend files. And in addition, its possible and recommended to add a security backend. To ensure that these functions work properly, the unit tests can be used.

Mandatory Functions

The following files contain function stubs and it is necessary to implement these to get the SDK working correctly. There are already fully implemented and tested platform folders in SDK/platform like linux and win32 which are great sources to check how the backend functions are implemented for other platforms.

Inter Process Communication Memory – pipcmem

For single process systems, only ua_p_ipc_mem_create and ua_p_ipc_mem_clear is necessary. The create function is called on startup and has to provide a memory block which will be used by the SDK. The size of the requested block is configured in the appconfig.
If the platform should be multi process capable, ua_p_ipc_mem_open is used to open already allocated memory. For more information about inter process communication in the SDK see IPC .

Semapore – psemaphore

The semaphore implementation is always necessary. If you operate on a single process system, it can be implemented with a simple counter variable, otherwise a full semaphore implementation is needed.

Time – ptime

The ptime module has to provide the following functions:

function Description
ua_p_time_tickcount has to return a monotonous system tick
ua_p_time_datetime the current date
ua_p_time_timestamp the current time stamp
ua_p_time_usleep a sleep function

Memory – pmemory

Functions in pmemory are used for memory allocation at runtime. The default implementations are mapped to std functions like malloc, calloc, free etc.
Replace this functions with your platform functions.

Platform Configuration – pplatform

The pplatform.h contains global platform configuration defines. This file is included in all files and so makes these defines available in the whole SDK.

pplatform.h:

  • Configure the system endianess
  • Marcos for byte swapping
  • countof macro: returns the array size for array variables
  • UA_STATIC_ASSERT: compile time assertions if supported (e.g. C11)
  • UA_UNUSED: for suppressing unused variable warnings
  • more optional macros for static code analysis

The pplatform.c file provides only three simple functions.

pplatform.c:

  • platform_init: initializes the platform layer
  • platform_cleanup: undo plaform_init code
  • platform_panic: a panic print useful for debugging

Optional Functions

There remaining files contains optional implementations, depending on your project environment. For a new platform port, these functions can be skipped and implemented later, depending if particular functions are needed.

File Access – pfile

Contains the implementation for file access with the well-known functions like open, close, read, write

GUID pguid

This file contains only one function for UUID creation: ua_p_guid_create(). The Linux implementation uses libuuid if available and uses a software implementation based on rand() as a fallback. This fallback can be used on all systems with a working rand() function. If other system functions are available for this you should prefer that. For example on Windows the UuidCreate API function of rpcrt4.dll is used.

Process - pprocess

The implementation is only necessary if multiple processes are needed. The default implementation of ua_p_process_support_forking() returns false, so process forking is disabled and no further code is needed.

Sandbox – psandbox

Used to switch into sandbox mode where operating system calls are no longer possible.

Shutdown – pshutdown

This part is used if its necessary to shutdown the application. The default implementation of ua_p_shutdown_should_shutdown returns 0, so it will work on systems without shutdown feature.

Mutex – pmutex

Not used on single process systems. For multi process systems a standard mutex implementation is needed.

Security Backend

To achieve a first working solution for a new platform, it is possible to build the SDK without security. It is highly recommended to add security for a final product.

Trace Backend

The default trace backend uses stderr. If you need a custom trace e.G via UART communication, add a new trace backend to SDK/src/trace/<your backend>.
Use SDK/src/trace/stderr/trace.c as template.
The following functions are mandatory: