Table of Contents

  1. Native Compilation on Pi
  2. Cross-Compilation from x86
  3. Sysroot Configuration
  4. GPIO Libraries
  5. I2C/SPI/UART Access
  6. Camera Module Integration
  7. Hardware Acceleration
  8. Distribution with DEB Packages
  9. Remote Build/Debug
Back to CMake Mastery Series

Raspberry Pi

June 4, 2026 Wasil Zafar 10 min read

Build and deploy C++ applications on Raspberry Pi with CMake — native and cross-compilation, GPIO libraries (pigpio, libgpiod), I2C/SPI hardware access, NEON optimization, camera integration, and DEB package distribution.

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
Build Performance: On a Pi 4 (4GB), medium C++ projects (50–100 source files) build in 2–5 minutes. For larger projects, cross-compile from your desktop (10–50× faster) and deploy the binary to the Pi. Use -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
Symlink Fix Required: Raspberry Pi OS uses absolute symlinks in /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_, &reg, 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;
    }
}
SBC NEON vs Scalar Performance

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.

NEON SIMD Audio DSP Cortex-A72

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
VS Code Remote-SSH: For the best development experience, use VS Code's Remote-SSH extension to connect directly to the Pi. This gives you native compilation with full IDE support (IntelliSense, debugging, terminal) — no toolchain file needed, and CMake runs natively on the Pi.