This post is part of the series about my personal BeagleBone Black project. The C part of the HAL is compiled on an x86 development machine — but the binary has to run on the ARM Cortex-A8 of the BeagleBone Black. That’s cross-compilation, and CMake makes it manageable.

Prerequisites

Installing the toolchain

On Ubuntu/Debian:

sudo apt install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf

That installs the essential tools:

  • arm-linux-gnueabihf-gcc — C compiler for ARMv7 with the hard-float ABI

  • `arm-linux-gnueabihf-g` — C compiler

  • arm-linux-gnueabihf-ld — linker

  • arm-linux-gnueabihf-strip — symbol stripper

Check the version:

arm-linux-gnueabihf-gcc --version
# arm-linux-gnueabihf-gcc (Ubuntu 12.3.0-1ubuntu1~22.04) 12.3.0

A sysroot is a copy of the target filesystem — with the correct libraries and headers for the target platform. Without a sysroot, the compiler links against the host libraries, which causes ABI mismatches.

Copy the sysroot from the BeagleBone Black:

rsync -avz \
  --exclude="/proc" --exclude="/sys" --exclude="/dev" \
  pi@beaglebone:/ \
  ~/bbb-sysroot/

For simple projects without system-specific libraries, the toolchain alone is enough. As soon as you link against libgpiod, libi2c, or other board-specific libraries, the sysroot becomes necessary.

CMake toolchain file

The centerpiece is the toolchain file — a CMake file that tells the build system which compiler, which linker, and which target system to use.

File cmake/armv7-toolchain.cmake:

# Target system
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR armv7)

# Toolchain prefix
set(TOOLCHAIN_PREFIX arm-linux-gnueabihf)

# Compiler
set(CMAKE_C_COMPILER   ${TOOLCHAIN_PREFIX}-gcc)
set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++)

# Sysroot (comment out if no sysroot is available)
# set(CMAKE_SYSROOT /home/user/bbb-sysroot)

# Only search the sysroot for libraries, not the host
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

# ARMv7 Cortex-A8 specific flags
set(CMAKE_C_FLAGS "-march=armv7-a -mfpu=neon -mfloat-abi=hard" CACHE STRING "")
set(CMAKE_CXX_FLAGS "-march=armv7-a -mfpu=neon -mfloat-abi=hard" CACHE STRING "")

The most important variables explained:

CMAKE_SYSTEM_NAME

Tells CMake that the build targets a different operating system. Disables many host checks.

CMAKE_SYSTEM_PROCESSOR

Processor architecture of the target system — influences compiler defaults.

CMAKE_C_COMPILER

Absolute path or name of the cross-compiler.

CMAKE_SYSROOT

Where CMake looks for headers and libraries.

CMAKE_FIND_ROOT_PATH_MODE_*

Prevents CMake from accidentally finding host libraries.

CMakeLists.txt specifics

The CMakeLists.txt itself barely differs from a normal configuration. A few points need attention when cross-compiling:

cmake_minimum_required(VERSION 3.20)
project(bbb_hal C)

# Disable the compiler test if no emulator is available
# (otherwise CMake tries to run the compiled binary)
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)

# Target library
add_library(bbb_drivers STATIC
    src/gpio_driver.c
    src/i2c_driver.c
    src/uart_driver.c
)

target_include_directories(bbb_drivers PUBLIC include)

# Install path for the deploy script
install(TARGETS bbb_drivers DESTINATION lib)
install(DIRECTORY include/ DESTINATION include)

CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY is crucial. Without this setting, CMake tries to link and run a test binary — which doesn’t work on the x86 host.

Running the build

# Create and configure the build directory
cmake \
  -DCMAKE_TOOLCHAIN_FILE=cmake/armv7-toolchain.cmake \
  -DCMAKE_BUILD_TYPE=Release \
  -B build-arm

# Compile
cmake --build build-arm

# Check: is it really an ARM binary?
file build-arm/libbbb_drivers.a
# build-arm/libbbb_drivers.a: current ar archive
arm-linux-gnueabihf-readelf -h build-arm/libbbb_drivers.a | grep Machine
# Machine: ARM

Integration in Drone CI

The toolchain needs to be installed in the CI container. I use plain apt-get in the pipeline to avoid maintaining my own images:

steps:
  - name: build-c
    image: ubuntu:22.04
    commands:
      - apt-get update -q
      - apt-get install -y cmake gcc-arm-linux-gnueabihf
      - cmake
          -DCMAKE_TOOLCHAIN_FILE=cmake/armv7-toolchain.cmake
          -DCMAKE_BUILD_TYPE=Release
          -B build-arm
      - cmake --build build-arm --parallel $(nproc)

Deployment to the board

After the build, the binary has to get onto the BeagleBone Black:

# Transfer the library and headers
rsync -avz build-arm/libbbb_drivers.a pi@beaglebone:/opt/bbb-hal/lib/
rsync -avz include/ pi@beaglebone:/opt/bbb-hal/include/

# Shared library (if built)
rsync -avz build-arm/libbbb_drivers.so pi@beaglebone:/usr/local/lib/
ssh pi@beaglebone "ldconfig"

In the CI deploy step:

  - name: deploy
    image: alpine
    environment:
      SSH_KEY:
        from_secret: bbb_ssh_key
    commands:
      - apk add --no-cache openssh-client rsync
      - eval $(ssh-agent -s)
      - echo "$SSH_KEY" | ssh-add -
      - rsync -avz build-arm/ pi@beaglebone:/opt/bbb-hal/
      - ssh pi@beaglebone "systemctl restart bbb-hal"
    when:
      branch: [main]

Common errors

Wrong ABI: hard vs softfp

/usr/bin/ld: skipping incompatible /usr/lib/libm.so when searching for -lm

The linker finds a library with the wrong float ABI. Fix: set the sysroot correctly, or compile the libraries explicitly with -mfloat-abi=hard.

Missing sysroot headers

fatal error: linux/i2c-dev.h: No such file or directory

The header doesn’t exist in the toolchain itself, only in the board sysroot. Fix: configure the sysroot correctly or include the header manually.

CMake finds host libraries

-- Found OpenSSL: /usr/lib/x86_64-linux-gnu/libssl.so (found version "3.0.2")

CMake found the host library instead of the ARM version. Fix: check CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY in the toolchain file.

Binary doesn’t run on the board

pi@beaglebone:~$ ./myapp
bash: ./myapp: cannot execute binary file: Exec format error

The binary was compiled for x86, not for ARM. Cause: the toolchain file wasn’t passed. Check with file myapp — it should show ARM.

Rust cross-compilation

As a bonus: here’s what cross-compilation looks like for the Rust part of the project.

# Install the target
rustup target add armv7-unknown-linux-gnueabihf

File .cargo/config.toml:

[target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"

Build:

cargo build \
  --release \
  --target armv7-unknown-linux-gnueabihf

Rust and CMake use the same toolchain (arm-linux-gnueabihf-gcc). If you’ve configured the CMake toolchain file correctly, you’ve already done the hardest part of the Rust configuration.


Next post in the series: Rust in the HAL — why and how