Dieser Post ist Teil der Serie über mein privates BeagleBone-Black-Projekt. Der C-Teil des HAL wird auf einem x86-Entwicklungsrechner kompiliert — das Binary muss aber auf dem ARM Cortex-A8 des BeagleBone Black laufen. Das ist Cross-Kompilierung, und CMake macht das handhabbar.

Voraussetzungen

Toolchain installieren

Auf Ubuntu/Debian:

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

Das installiert die wichtigsten Tools:

  • arm-linux-gnueabihf-gcc — C-Compiler für ARMv7 mit Hard-Float ABI

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

  • arm-linux-gnueabihf-ld — Linker

  • arm-linux-gnueabihf-strip — Symbol-Stripper

Version prüfen:

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

Sysroot (optional aber empfohlen)

Ein Sysroot ist eine Kopie des Ziel-Dateisystems — mit den korrekten Libraries und Headern für die Zielplattform. Ohne Sysroot linkt der Compiler gegen die Host-Libraries, was zu ABI-Mismatches führt.

Sysroot vom BeagleBone Black kopieren:

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

Für einfache Projekte ohne systemspezifische Libraries reicht die Toolchain alleine. Sobald man gegen libgpiod, libi2c oder andere Board-spezifische Libraries linkt, ist das Sysroot nötig.

CMake Toolchain-File

Das Herzstück ist das Toolchain-File — eine CMake-Datei die dem Build-System mitteilt, welchen Compiler, welchen Linker und welches Zielsystem es nutzen soll.

Datei cmake/armv7-toolchain.cmake:

# Zielsystem
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 (auskommentieren wenn kein Sysroot vorhanden)
# set(CMAKE_SYSROOT /home/user/bbb-sysroot)

# Nur im Sysroot nach Libraries suchen, nicht auf dem 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 spezifische 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 "")

Die wichtigsten Variablen erklärt:

CMAKE_SYSTEM_NAME

Teilt CMake mit, dass für ein anderes Betriebssystem gebaut wird. Deaktiviert viele Host-Checks.

CMAKE_SYSTEM_PROCESSOR

Prozessor-Architektur des Zielsystems — beeinflusst Compiler-Defaults.

CMAKE_C_COMPILER

Absoluter Pfad oder Name des Cross-Compilers.

CMAKE_SYSROOT

Wo CMake nach Headers und Libraries sucht.

CMAKE_FIND_ROOT_PATH_MODE_*

Verhindert dass CMake versehentlich Host-Libraries findet.

CMakeLists.txt-Besonderheiten

Die CMakeLists.txt selbst unterscheidet sich kaum von einer normalen Konfiguration. Ein paar Punkte sind beim Cross-Compile zu beachten:

cmake_minimum_required(VERSION 3.20)
project(bbb_hal C)

# Compiler-Test deaktivieren wenn kein Emulator vorhanden
# (CMake versucht sonst das kompilierte Binary auszuführen)
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)

# Ziel-Bibliothek
add_library(bbb_drivers STATIC
    src/gpio_driver.c
    src/i2c_driver.c
    src/uart_driver.c
)

target_include_directories(bbb_drivers PUBLIC include)

# Installationspfad für Deploy-Script
install(TARGETS bbb_drivers DESTINATION lib)
install(DIRECTORY include/ DESTINATION include)

CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY ist entscheidend. Ohne diese Einstellung versucht CMake ein Test-Binary zu linken und auszuführen — was auf dem x86-Host nicht funktioniert.

Build ausführen

# Build-Verzeichnis erstellen und konfigurieren
cmake \
  -DCMAKE_TOOLCHAIN_FILE=cmake/armv7-toolchain.cmake \
  -DCMAKE_BUILD_TYPE=Release \
  -B build-arm

# Kompilieren
cmake --build build-arm

# Prüfen: ist es wirklich ein 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

Im CI-Container muss die Toolchain installiert sein. Ich verwende in der Pipeline direktes apt-get um keine eigenen Images pflegen zu müssen:

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 auf das Board

Nach dem Build muss das Binary auf den BeagleBone Black:

# Library und Header übertragen
rsync -avz build-arm/libbbb_drivers.a pi@beaglebone:/opt/bbb-hal/lib/
rsync -avz include/ pi@beaglebone:/opt/bbb-hal/include/

# Shared Library (wenn gebaut)
rsync -avz build-arm/libbbb_drivers.so pi@beaglebone:/usr/local/lib/
ssh pi@beaglebone "ldconfig"

Im 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]

Häufige Fehler

Falscher ABI: hard vs softfp

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

Der Linker findet eine Library mit falschem Float-ABI. Lösung: Sysroot korrekt setzen oder Libraries explizit mit -mfloat-abi=hard kompilieren.

Fehlende Sysroot-Header

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

Der Header existiert nicht in der Toolchain selbst, nur im Board-Sysroot. Lösung: Sysroot korrekt konfigurieren oder Header manuell einbinden.

CMake findet Host-Libraries

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

CMake hat die Host-Library gefunden statt der ARM-Version. Lösung: CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY im Toolchain-File prüfen.

Binary läuft nicht auf dem Board

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

Das Binary wurde für x86 kompiliert, nicht für ARM. Ursache: Toolchain-File wurde nicht übergeben. Prüfen mit file myapp — sollte ARM zeigen.

Rust Cross-Compilation

Als Bonus: so sieht Cross-Compilation für den Rust-Teil des Projekts aus.

# Target installieren
rustup target add armv7-unknown-linux-gnueabihf

Datei .cargo/config.toml:

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

Build:

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

Rust und CMake nutzen dieselbe Toolchain (arm-linux-gnueabihf-gcc). Wer das Toolchain-File für CMake richtig konfiguriert hat, hat den schwersten Teil der Rust-Konfiguration schon hinter sich.


Nächster Post in der Serie: Rust in der HAL — warum und wie