Navigating the world of Android Automotive and its deep connectivity to in-vehicle systems can be a maze, but the Vehicle Abstraction Layer (VHAL) promises to bridge this gap. Delving into this article, you’ll uncover the essence of Android Automotive, its interaction with vehicle hardware and the potential of an approach to VHAL implementation using ROS 2. Through a case study, the article sheds light on how teams can efficiently parallelise work, reduce cost, and improve product quality, all while navigating the challenges of cross-compilation in the Android ecosystem.

Introduction to Android Automotive

Android Automotive is a base Android platform that runs pre-installed IVI system Android applications, as well as optional second- and third-party Android Applications. It runs directly on the in-vehicle hardware and can physically interact with the car; to do so, Android Automotive requires access to the platform specifics, hardware peripherals and networks (e.g., CAN).

From the perspective of Android Automotive applications, the access to the car-specific functionality is realised through the Car API. Behind the Car API, there are multiple Hardware Abstraction Layers, including the Vehicle Abstraction Layer (VHAL), that is designed specifically to match the vehicle functionality, e.g., HVAC, Door, or Seat configuration control.

The VHAL defines a set of properties OEMs can implement and contains property metadata (for example, whether the property is an int and which change modes are allowed). The VHAL interface is based on accessing (read, write, subscribe) a property, which is an abstraction for a specific function.

android automotive vhal ros

Implementing VHAL: low-level connection

To connect the Car API with the physical car, OEMs must implement the VHAL. Typically, this is done by using low-level languages like C or C++ and hardware-specific libraries to connect it with the vehicle networks (CAN, LIN, etc.) or system peripherals connected with I2C or SPI.

This approach makes the system tightly coupled to the hardware and less portable.

android automotive vhal ros

Implementing VHAL: high-level communication

Another way to implement the VHAL would be to decouple it from the underlying physical system and bridge the data flow with a higher-level communication protocol, such as ROS 2 / DDS. This way, the hardware-specific implementation could be separated into ROS Nodes and the vehicle data provided on ROS Topics and Services. That would bring plenty of benefits, like modularisation, standardised communication with the hardware and several ROS 2 tools for simulation, testing, debugging and data visualisation!

Below is an example of the VHAL Node that communicates with the OEM IVI system via ROS Topics/Services.

android automotive vhal ros

Case study: Connecting VHAL to Android Automotive car API on top of ROS 2

There are three factors that make product development successful:

  • time-to-market,
  • price
  • and quality.

And the second approach described above leads us to even more possibilities in those three factors. To reduce price and accelerate product development, teams may want to parallelise the work. But that can only be achieved  if there are no strict dependencies between the parts of the system. Here, thanks to the decoupling of the VHAL implementation from the OEM system, development teams can start the work immediately in parallel without waiting for the OEM system to be ready for use and testing. Some parts of the OEM system may not even exist yet, and that would not block the development as long as the ROS 2 data interfaces (topics, services) are defined.

Decoupling from the OEM system also adds to overall product quality, as it makes the whole system more testable. Android applications can be tested early on in a complete system from the app’s perspective but without the need to assemble every underlying component. Vehicle ECUs can be simply mocked as ROS 2 test nodes:

android automotive vhal ros

Realisation

All implementation and testing in this article were done on ROS 2 Iron and AOSP version 13.0.

How do you bring the project to life? It needs to be implemented one or the other way. Let us explore the possibilities and which one can be picked as the most suitable.

We assume that the VHAL will be a ROS 2 node. One could think of implementing merely a DDS node that communicates over a DDS topic with the OEM system. But later, we will see that ROS 2 brings the necessary abstraction on top of the DDS protocol to add service-oriented communication (besides a rich set of tools and other APIs).

Cross-compilation of ROS 2

ROS 2 typically comes precompiled for a specific Linux distribution, like Ubuntu, but can also be compiled from the sources. However, the cross-compilation of ROS 2 for unsupported systems doesn’t seem to be an easy task. And Android is not supported in our case.

Some projects aimed to make the cross-compilation of ROS 2 simple, like cross_compile, but as of the writing of this article, that project is already deprecated. What’s more, it does not even provide a true cross-compilation, but rather an emulated build within QEMU (that would require running the build within the emulated Android system).

Because Android is not just another GNU/Linux distribution, we not only need to build the ROS 2 itself, but also all the dependencies. We cannot just install them with system package managers, e.g., with:

apt install <package>

Android is also distinct in developing native platform applications. While user / 3rd party applications may be built with many external build systems, the platform code is built and assembled with Android-specific build tools.

In AOSP, there are three build systems:

  • the legacy Make-based build system,
  • Soong,
  • and the upcoming Bazel-based build system.

As the Make-based build is gradually replaced by Soong, it won’t be considered here. And the Bazel isn’t yet fully available for AOSP, so we will only focus on the Soong build system.

ROS 2, on the other hand, uses CMake as the base build-system, which is extended with ament_cmake and colcon. It relies on system packages as dependencies for building its code base. As it uses CMake, providing a CMake Toolchain definition for cross-compilation should be fairly straightforward. We just need to use the proper Android cross-toolchain for our target.

How do you integrate an external project with Soong?

One way could be adding Soong build scripts for all required ROS 2 packages, but that would just be too much work and would require regular maintenance. Luckily, Soong supports integrating externally built libraries as its targets and lets Android modules link them, both dynamically and statically.

As an example, to include an external static library, a developer must define a module with the following config:

package {
}

cc_prebuilt_library_static {
    name: "vendor.spyrosoft.libexample",
    vendor: true,
    export_include_dirs: [
        "include/libexample"
    ],
    srcs: ["prebuilt/arm64/libexample.a"],
    strip: { none:true, },
}

In the above static library, the prebuilt library archive file is located in the prebuilt/arm64/libexample.a, , and its public API headers location is exported with export_include_dirs, so other modules will inherit the included path automatically.

Static or dynamic linking?

ROS 2 supports building with both dynamic and static linking. Because each dynamic library built for ROS 2 would require a separate Soong build definition, the static linking seems to be a better fit. To build the ROS 2 packages, a CMake option BUILD_SHARED_LIBS has to be set to OFF. It can be simply added to the colcon as one of the CMake args:

$ colcon build --cmake-args "BUILD_SHARED_LIBS=OFF" ...

Where do we get the Android cross-toolchain?

One could think of using Android’s NDK, which supports using CMake to build native applications. But there’s a catch! As we plan to implement a platform service – VHAL, it will link to the system’s native libraries, and NDK cannot be used to build it. The reason for that is the introduction of Namespaces for Native Libraries. As a result, if we used NDK, attempting to build a platform code with it will most likely end up in linker errors. The NDK native libraries are incompatible on the ABI level with system native libraries.

android automotive vhal ros

As of AOSP version 13.0_r61 the toolchain for native platform code is located in:

prebuilts/clang/host/linux-x86/clang-r450784d/

and the sysroot is located in:

prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.17-4.8/sysroot

Unlike the GNU compiler collection, where every host/target combination has its set of binaries, headers, libraries, etc., Clang/LLVM is natively a cross-compiler, meaning that one set of programs can compile to all targets by setting the -target option.

To get the set of options specific to the target platform, we can use the fact that AOSP is not fully migrated to Soong, and some modules still rely on Make. To sustain compatibility with Make, AOSP uses a tool called Kati, which is a Make clone. Soong communicates with Kati by generating Make Vars and generates .mk files in the output directory out/soong/{Android, late, make_vars}-<product>.mk

The file out/soong/make_vars-<product-suffix>.mk is where all the compiler flags and options are stored. We just need to parse the data and generate the CMake toolchain file. Let’s implement it in Python.

To locate the target specific out/soong/make_vars-<product-suffix>.mk file, a ‘product-suffix’ is needed. Luckily, Soong stores it in out/soong/soong.variables under “Make_suffix” key, e.g.,

"Make_suffix": "-aosp_rpi4",

To get the make_vars.mk file path, we can do the following:

def get_makefile_path():
    with open('out/soong/soong.variables', 'r') as soong_vars:
        vars = json.load(soong_vars)
        make_suffix = vars['Make_suffix']
        return os.path.abspath(f'out/soong/make_vars{vars['Make_suffix']}.mk')

To get all the variables as an indexable list, we can use a relatively simple regex pattern:

def parse_makefile(file_path):
    variables = {}
    variable_assignment_regex = re.compile(r'^([a-zA-Z0-9_]+)\s*:=\s*(.*)$')

    with open(file_path, 'r') as makefile:
        for line in makefile:
            # Remove comments
            line = line.split('#')[0].strip()

            # Match the line against the regular expression
            match = variable_assignment_regex.match(line)
            if match:
                variables[match.group(1)] = match.group(2)

    return variables

Once we have all the information, we can generate the CMake Toolchain file:

toolchain_file_content = f"""#

# Without that flag CMake is not able to pass test compilation check
set(CMAKE_TRY_COMPILE_TARGET_TYPE   STATIC_LIBRARY)

set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR ARM)
set(CMAKE_SYSTEM_VERSION 31)

set(CMAKE_SYSROOT                         {root_path + find_in('--sysroot ', mk_vars['SOONG_CLANG_HOST_GLOBAL_CFLAGS'])})
set(CMAKE_ANDROID_STANDALONE_TOOLCHAIN    {root_path + find_in('--gcc-toolchain=', mk_vars['SOONG_CLANG_HOST_GLOBAL_CFLAGS'])})
set(TOOLCHAIN_TRIPLE                      {mk_vars['SOONG_CLANG_CONFIG_arm64_TARGET_TRIPLE']})

# specify the cross compiler
set(CMAKE_ASM_COMPILER                    {root_path + mk_vars['SOONG_CLANG']} CACHE STRING "")
SET(CMAKE_ASM_COMPILER_TARGET             ${{TOOLCHAIN_TRIPLE}})
set(CMAKE_C_COMPILER                      {root_path + mk_vars['SOONG_CLANG']} CACHE STRING "")
set(CMAKE_C_COMPILER_TARGET               ${{TOOLCHAIN_TRIPLE}})
set(CMAKE_C_COMPILER_EXTERNAL_TOOLCHAIN   ${{CMAKE_ANDROID_STANDALONE_TOOLCHAIN}})
set(CMAKE_CXX_COMPILER                    {root_path + mk_vars['SOONG_CLANG_CXX']} CACHE STRING "")
set(CMAKE_CXX_COMPILER_TARGET             ${{TOOLCHAIN_TRIPLE}})
set(CMAKE_CXX_COMPILER_EXTERNAL_TOOLCHAIN ${{CMAKE_ANDROID_STANDALONE_TOOLCHAIN}})


# specify the linker
set(CMAKE_C_LINK_EXECUTABLE     {root_path + mk_vars['SOONG_TARGET_LD']} CACHE STRING "")
set(CMAKE_CXX_LINK_EXECUTABLE   {root_path + mk_vars['SOONG_TARGET_LD']} CACHE STRING "")
set(CMAKE_LINKER                {root_path + mk_vars['SOONG_TARGET_LD']} CACHE STRING "")

set(CMAKE_C_FLAGS_INIT          "{mk_vars['SOONG_CLANG_TARGET_GLOBAL_CFLAGS']}" CACHE STRING "")
set(CMAKE_CXX_FLAGS_INIT        "{mk_vars['SOONG_CLANG_TARGET_GLOBAL_CPPFLAGS']}" CACHE STRING "")

set(CMAKE_C_FLAGS_DEBUG         "-O0 -g" CACHE INTERNAL "")
set(CMAKE_C_FLAGS_RELEASE       "-Os -DNDEBUG" CACHE INTERNAL "")
set(CMAKE_CXX_FLAGS_DEBUG       "${{CMAKE_C_FLAGS_DEBUG}}" CACHE INTERNAL "")
set(CMAKE_CXX_FLAGS_RELEASE     "${{CMAKE_C_FLAGS_RELEASE}}" CACHE INTERNAL "")

set(CMAKE_C_STANDARD_INCLUDE_DIRECTORIES
    {root_path + "bionic/libc/include"}
    {root_path + "bionic/libc/kernel/uapi"}
    {root_path + "bionic/libc/kernel/android/scsi"}
    {root_path + "bionic/libc/kernel/android/uapi"}
    {root_path + "bionic/libc/kernel/uapi/asm-arm64"}
    {root_path + "external/libcxx/include"}
)

set(CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES
    ${{CMAKE_C_STANDARD_INCLUDE_DIRECTORIES}}
    ${{CMAKE_SYSROOT}}/usr/include/x86_64-linux-gnu/
    {root_path + '/synergycar/aosp/external/libcxx/include'}
)

add_compile_options(-fPIC)
add_compile_definitions(__ANDROID_API__=31 __ANDROID__)
add_link_options({mk_vars['SOONG_CLANG_TARGET_GLOBAL_LLDFLAGS']})
link_directories({root_path + "prebuilts/runtime/mainline/runtime/sdk/android/arm/lib/"})

# don't search for programs in the build host directories
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)

# search for libraries and headers in the target directories
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
"""

# Write the CMake toolchain file
with open('android_toolchain.cmake', 'w') as toolchain_file:
    toolchain_file.write(toolchain_file_content)

Some details needed to define the CMake toolchain are buried a bit deeper, e.g., the CMAKE_SYSROOT path needs to be extracted from SOONG_CLANG_HOST_GLOBAL_CFLAGS and some additional settings have to be added manually, e.g., the Android’s C library – Bionic include directories or some global defines.

For the details, please refer to the full script, which can be found here.

The generated toolchain file can be easily used with colcon to build the ROS 2:

$ colcon build --cmake-args "BUILD_SHARED_LIBS=OFF -DCMAKE_TOOLCHAIN_FILE=aosp_toolchain.cmake"

Summary

While the theoretical approach to cross-compiling ROS 2 for Android is promising, the practical application can present certain challenges, especially when it comes to compiling all the required dependencies. Of course, there are many potential paths that researchers and engineers can take to achieve full compatibility and functionality.

However, we cannot overlook the existence of a more elegant and potentially more efficient solution for those looking to integrate ROS 2 with unsupported systems: micro-ROS. This innovative project offers a much more tailored approach to cross-compiling across various platforms. In the next instalment of this series, we’ll delve deeper into micro-ROS, exploring its capabilities and attempting to build and run it on Android.

Start your next robotics project with Spyrosoft

Are you looking for a company to which you can entrust your product? Here you can find more about the scope of our professional robotics services and how we bring ideas to life.

About the author

Connectivity in Robotics with Staex

Slawomir Cielepak

Lead C++ Software Engineer