Native Compilation on Pi
The simplest workflow builds directly on the Raspberry Pi itself. Modern Pi 4/5 boards with 4–8GB RAM are capable development machines for moderate C++ projects. Install the standard build tools and run CMake natively.
# Install build tools on Raspberry Pi OS (Bookworm)
sudo apt update
sudo apt install -y cmake ninja-build g++ git pkg-config
# Verify versions
cmake --version # 3.25+ on Bookworm
g++ --version # GCC 12+ on Bookworm
uname -m # aarch64 on 64-bit Pi OS
# Configure and build (same as any Linux system)
cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -S . -B build
cmake --build build -j$(nproc)
# Install system-wide
sudo cmake --install build
-j$(nproc) to parallelize — the Pi 4 has 4 cores.
Cross-Compilation from x86
Cross-compiling from an x86 workstation dramatically accelerates builds. The toolchain produces aarch64 binaries that run on 64-bit Raspberry Pi OS.
# Install cross-compilation toolchain on Ubuntu x86
sudo apt install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
# Verify the cross-compiler
aarch64-linux-gnu-g++ --version
# cmake/rpi-toolchain.cmake
# Cross-compilation toolchain for Raspberry Pi (aarch64)
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
# Cross-compiler
set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++)
# Sysroot — extracted from the Pi or from official image
set(CMAKE_SYSROOT /opt/rpi-sysroot)
# Where to search for libraries and headers
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
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)
# Pi 4/5 CPU flags (Cortex-A72/A76)
set(CMAKE_C_FLAGS_INIT "-march=armv8-a+crc -mtune=cortex-a72")
set(CMAKE_CXX_FLAGS_INIT "-march=armv8-a+crc -mtune=cortex-a72")
# Configure with cross-toolchain
cmake -G Ninja \
-DCMAKE_TOOLCHAIN_FILE=cmake/rpi-toolchain.cmake \
-DCMAKE_BUILD_TYPE=Release \
-S . -B build-rpi
# Build (uses host CPU — much faster than building on Pi)
cmake --build build-rpi -j$(nproc)
# Deploy to Pi via SSH
scp build-rpi/myapp pi@raspberrypi.local:~/
ssh pi@raspberrypi.local ./myapp
Sysroot Configuration
The sysroot provides headers and libraries from the target system. Without it, the cross-compiler cannot find Pi-specific libraries (pigpio, libcamera, etc.).
# Method 1: rsync sysroot from a running Pi
mkdir -p /opt/rpi-sysroot
rsync -avz pi@raspberrypi.local:/usr/include /opt/rpi-sysroot/usr/
rsync -avz pi@raspberrypi.local:/usr/lib/aarch64-linux-gnu /opt/rpi-sysroot/usr/lib/
rsync -avz pi@raspberrypi.local:/lib/aarch64-linux-gnu /opt/rpi-sysroot/lib/
# Fix absolute symlinks to be relative
cd /opt/rpi-sysroot
find . -type l | while read link; do
target=$(readlink "$link")
if [[ "$target" == /* ]]; then
ln -sf "/opt/rpi-sysroot$target" "$link"
fi
done
# Method 2: Mount Pi SD card image (offline)
sudo losetup -Pf 2024-03-15-raspios-bookworm-arm64.img
sudo mount /dev/loop0p2 /mnt/rpi-root
# Copy /usr/include, /usr/lib, /lib from mount
/usr/lib (e.g., libpthread.so → /lib/aarch64-linux-gnu/libpthread.so.0). After copying the sysroot, these point nowhere on your host. Fix them with the symlink conversion script above, or the linker will report "cannot find -lpthread".
GPIO Libraries
Raspberry Pi GPIO access requires user-space libraries. The modern standard is libgpiod (character device interface), though pigpio remains popular for its simplicity and PWM support.
cmake_minimum_required(VERSION 3.21)
project(GPIOApp LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
add_executable(gpio_app src/main.cpp)
# Method 1: libgpiod (modern, kernel character device API)
find_package(PkgConfig REQUIRED)
pkg_check_modules(GPIOD REQUIRED libgpiod)
target_include_directories(gpio_app PRIVATE ${GPIOD_INCLUDE_DIRS})
target_link_libraries(gpio_app PRIVATE ${GPIOD_LIBRARIES})
# Method 2: pigpio (daemon-based, supports PWM/servo)
# find_library(PIGPIO_LIB pigpio)
# find_path(PIGPIO_INCLUDE pigpio.h)
# target_include_directories(gpio_app PRIVATE ${PIGPIO_INCLUDE})
# target_link_libraries(gpio_app PRIVATE ${PIGPIO_LIB} pthread)
// src/main.cpp — Blink LED with libgpiod v2
#include <gpiod.hpp>
#include <chrono>
#include <thread>
#include <iostream>
int main() {
// Open GPIO chip (Pi 4: gpiochip0, Pi 5: gpiochip4)
auto chip = gpiod::chip("/dev/gpiochip0");
// Request GPIO 17 as output
auto settings = gpiod::line_settings();
settings.set_direction(gpiod::line::direction::OUTPUT);
auto request = chip.prepare_request()
.set_consumer("blink-app")
.add_line_settings(17, settings)
.do_request();
std::cout << "Blinking LED on GPIO 17..." << std::endl;
for (int i = 0; i < 10; ++i) {
request.set_value(17, gpiod::line::value::ACTIVE);
std::this_thread::sleep_for(std::chrono::milliseconds(500));
request.set_value(17, gpiod::line::value::INACTIVE);
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
return 0;
}
I2C/SPI/UART Hardware Access
Hardware peripherals on the Pi are accessed through Linux device files. CMake projects link against standard Linux I/O headers — no special libraries needed for basic access.
cmake_minimum_required(VERSION 3.21)
project(I2CApp LANGUAGES CXX)
add_executable(sensor_reader src/main.cpp src/i2c_device.cpp)
# i2c-dev is a kernel header — no external library needed
target_compile_definitions(sensor_reader PRIVATE
I2C_BUS="/dev/i2c-1"
SPI_DEVICE="/dev/spidev0.0"
)
// src/i2c_device.cpp — Read temperature from BME280 via I2C
#include <cstdint>
#include <cstdio>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/i2c-dev.h>
class I2CDevice {
public:
I2CDevice(const char* bus, uint8_t address) {
fd_ = open(bus, O_RDWR);
if (fd_ < 0) { perror("open i2c"); return; }
if (ioctl(fd_, I2C_SLAVE, address) < 0) {
perror("ioctl I2C_SLAVE");
}
}
~I2CDevice() { if (fd_ >= 0) close(fd_); }
int readRegister(uint8_t reg, uint8_t* buf, size_t len) {
if (write(fd_, ®, 1) != 1) return -1;
return read(fd_, buf, len);
}
int writeRegister(uint8_t reg, uint8_t value) {
uint8_t data[2] = {reg, value};
return write(fd_, data, 2);
}
private:
int fd_ = -1;
};
Camera Module Integration
Raspberry Pi cameras are accessed through libcamera on modern Pi OS (replacing the legacy MMAL API). CMake finds it via pkg-config.
cmake_minimum_required(VERSION 3.21)
project(CameraApp LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
find_package(PkgConfig REQUIRED)
pkg_check_modules(LIBCAMERA REQUIRED libcamera)
add_executable(camera_app src/main.cpp)
target_include_directories(camera_app PRIVATE ${LIBCAMERA_INCLUDE_DIRS})
target_link_libraries(camera_app PRIVATE ${LIBCAMERA_LIBRARIES})
target_compile_options(camera_app PRIVATE ${LIBCAMERA_CFLAGS_OTHER})
Hardware Acceleration
Raspberry Pi 4/5 supports ARM NEON SIMD instructions for vectorized computation. CMake can enable architecture-specific optimizations for data-intensive workloads.
cmake_minimum_required(VERSION 3.21)
project(NEONApp LANGUAGES CXX)
add_executable(dsp_app src/main.cpp src/fir_filter.cpp)
# Enable NEON (ARM SIMD) — enabled by default on aarch64 but explicit is clearer
target_compile_options(dsp_app PRIVATE
-march=armv8-a+simd # NEON is part of ARMv8 SIMD
-O3 # Enables auto-vectorization
-ftree-vectorize # Explicit vectorization pass
-ffast-math # Allow FP reordering for SIMD
)
# For Pi 4 specifically (Cortex-A72)
# target_compile_options(dsp_app PRIVATE -mcpu=cortex-a72)
# For Pi 5 (Cortex-A76)
# target_compile_options(dsp_app PRIVATE -mcpu=cortex-a76)
// src/fir_filter.cpp — NEON-optimized FIR filter
#include <arm_neon.h>
#include <vector>
void fir_filter_neon(const float* input, float* output,
const float* coeffs, int num_taps, int length) {
for (int i = 0; i < length - num_taps; ++i) {
float32x4_t sum = vdupq_n_f32(0.0f);
// Process 4 taps at a time using NEON
int j = 0;
for (; j <= num_taps - 4; j += 4) {
float32x4_t samples = vld1q_f32(&input[i + j]);
float32x4_t coefs = vld1q_f32(&coeffs[j]);
sum = vfmaq_f32(sum, samples, coefs); // fused multiply-add
}
// Horizontal sum of the 4 lanes
float result = vaddvq_f32(sum);
// Handle remaining taps
for (; j < num_taps; ++j) {
result += input[i + j] * coeffs[j];
}
output[i] = result;
}
}
A 256-tap FIR filter processing 48kHz audio on a Pi 4 runs at ~12ms per buffer (1024 samples) with scalar code. With NEON intrinsics and -O3 -ffast-math, the same filter completes in ~3ms — a 4× speedup matching the 4-lane SIMD width. For real-time audio, this is the difference between marginal and comfortable latency budgets.
Distribution with DEB Packages
CPack generates .deb packages for easy installation on Raspberry Pi OS. This handles dependencies, file permissions, and system service registration.
cmake_minimum_required(VERSION 3.21)
project(PiService VERSION 1.2.0 LANGUAGES CXX)
add_executable(pi-sensor-daemon src/main.cpp src/sensor.cpp)
install(TARGETS pi-sensor-daemon RUNTIME DESTINATION bin)
install(FILES config/sensor.conf DESTINATION /etc/pi-sensor/)
install(FILES systemd/pi-sensor.service
DESTINATION /lib/systemd/system/)
# CPack DEB configuration
set(CPACK_GENERATOR "DEB")
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Your Name")
set(CPACK_DEBIAN_PACKAGE_DESCRIPTION
"Raspberry Pi sensor monitoring daemon")
set(CPACK_DEBIAN_PACKAGE_SECTION "embedded")
set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "arm64")
set(CPACK_DEBIAN_PACKAGE_DEPENDS
"libgpiod2 (>= 1.6), libstdc++6 (>= 12)")
# Post-install script (enable systemd service)
set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA
"${CMAKE_SOURCE_DIR}/packaging/postinst")
include(CPack)
# Build the DEB package
cmake --build build --target package
# Install on Pi
sudo dpkg -i pi-sensor-daemon-1.2.0-arm64.deb
sudo systemctl enable --now pi-sensor
Remote Build/Debug
For iterative development, build on your workstation and deploy to the Pi automatically. CMake custom targets streamline the deploy-run-debug cycle.
# Remote deployment targets
set(PI_HOST "pi@raspberrypi.local")
set(PI_DEPLOY_DIR "/home/pi/app")
# Deploy binary to Pi
add_custom_target(deploy
COMMAND scp $ ${PI_HOST}:${PI_DEPLOY_DIR}/
DEPENDS myapp
COMMENT "Deploying to ${PI_HOST}:${PI_DEPLOY_DIR}"
)
# Deploy and run
add_custom_target(run-remote
COMMAND ssh ${PI_HOST} "${PI_DEPLOY_DIR}/myapp"
DEPENDS deploy
COMMENT "Running on Pi"
USES_TERMINAL
)
# Remote GDB debug (requires gdbserver on Pi)
add_custom_target(debug-remote
COMMAND ssh ${PI_HOST}
"gdbserver :2345 ${PI_DEPLOY_DIR}/myapp" &
COMMAND sleep 1
COMMAND gdb-multiarch $
-ex "target remote ${PI_HOST}:2345"
DEPENDS deploy
COMMENT "Remote debugging on Pi"
USES_TERMINAL
)
# Full workflow: cross-compile → deploy → run
cmake --build build-rpi --target run-remote
# Debug remotely
cmake --build build-rpi --target debug-remote