Why Remote VS Code is the Standard for Raspberry Pi 5 GPIO

Writing embedded C++ directly on a Raspberry Pi using a desktop environment like Thonny or Geany is a bottleneck. The Raspberry Pi 5 (4GB variant, currently ~$60) has the compute power to run a local IDE, but you still lose the advanced IntelliSense, CMake integration, and remote debugging capabilities of a full desktop workstation. Using Raspberry Pi Visual Studio Code via the Remote - SSH extension bridges this gap: you write, compile, and step through breakpoints on your host machine while the code executes natively on the Pi's RP1 southbridge chip.

Difficulty Rating: Intermediate (Requires basic Linux CLI and C++ familiarity)
Time to Complete: 45 minutes

Parts List & Exact Variants

  • Microcontroller: Raspberry Pi 5 (4GB RAM, 2023/2024 revision) running Raspberry Pi OS Bookworm (64-bit)
  • Input: KY-040 Rotary Encoder module (breakout board with built-in pull-ups)
  • Output: 5mm Diffused Blue LED with 330Ω 1/4W carbon film resistor
  • Wiring: 27-point solderless breadboard, 6x Female-to-Male 20AWG jumper wires
  • Host Machine: Windows 11, macOS, or Ubuntu desktop with VS Code Remote - SSH extension installed

Hardware Wiring and Pin Mapping for the Pi 5

The Raspberry Pi 5 uses the new RP1 I/O controller. Unlike the Pi 4, the physical pins map to a different internal GPIO chip device tree node. We are using BCM (Broadcom) numbering for the software definitions.

Component PinPi 5 Physical PinBCM GPIO NumberFunction in Code
KY-040 CLK1117Encoder Clock (Input with Pull-up)
KY-040 DT1327Encoder Data (Input with Pull-up)
KY-040 GND9N/ASystem Ground
LED Anode (+)3212PWM Output (via 330Ω Resistor)
LED Cathode (-)14N/ASystem Ground
Safety Callout: Always de-energize the Pi (unplug the USB-C PD power supply) before moving jumper wires on the GPIO header. Shorting the 3.3V pin (Physical Pin 1) to ground will permanently damage the RP1 chip's voltage regulator.

Configuring Visual Studio Code for Remote C++ Compilation

To compile C++ on the Pi from your host, we use the lgpio library. The older pigpio and wiringPi libraries have severe compatibility issues with the Pi 5's RP1 chip. lgpio is the modern, officially recommended GPIO library for Bookworm.

  1. Install Dependencies on the Pi: Open your VS Code integrated terminal (connected via SSH to the Pi) and run:
    sudo apt update && sudo apt install build-essential cmake liblgpio-dev
  2. Initialize CMake: In your project root, create a CMakeLists.txt file:
    cmake_minimum_required(VERSION 3.16)
    project(pi5_encoder CXX)
    set(CMAKE_CXX_STANDARD 17)
    add_executable(encoder_main main.cpp)
    target_link_libraries(encoder_main PRIVATE lgpio)
  3. Configure VS Code Launch: Create a .vscode/launch.json to enable hardware breakpoints. Use the gdb debugger targeting the remote ARM64 architecture.

The Complete Compilable C++ Source Code

This code targets the Raspberry Pi 5 (4GB). It reads the quadrature signals from the rotary encoder and adjusts the PWM duty cycle of the LED. It includes explicit error handling for GPIO claims, which is critical when debugging hardware faults.

#include <iostream>
#include <lgpio.h>
#include <unistd.h>
#include <csignal>

// --- Pin Definitions (BCM) ---
// Pi 5 RP1 southbridge maps the main 40-pin header to gpiochip4
#define GPIO_CHIP 4  
#define ENC_CLK 17   // Encoder Clock
#define ENC_DT 27    // Encoder Data
#define LED_PWM 12   // PWM LED output

static int gpio_handle = -1;
volatile bool keep_running = true;

void signal_handler(int signum) {
    keep_running = false;
}

int main() {
    signal(SIGINT, signal_handler);
    std::cout << "Initializing Pi 5 GPIO via lgpio...\n";

    // Open the GPIO chip (Chip 4 on Pi 5)
    gpio_handle = lgGpiochipOpen(GPIO_CHIP);
    if (gpio_handle < 0) {
        std::cerr << "Failed to open GPIO chip " << GPIO_CHIP 
                  << ". Error code: " << gpio_handle << std::endl;
        return 1;
    }

    // Claim pins with error checking
    if (lgGpioClaimInput(gpio_handle, 0, ENC_CLK) < 0) {
        std::cerr << "Failed to claim ENC_CLK (Pin " << ENC_CLK << ")\n";
        lgGpiochipClose(gpio_handle);
        return 1;
    }
    if (lgGpioClaimInput(gpio_handle, 0, ENC_DT) < 0) {
        std::cerr << "Failed to claim ENC_DT (Pin " << ENC_DT << ")\n";
        lgGpiochipClose(gpio_handle);
        return 1;
    }
    if (lgGpioClaimOutput(gpio_handle, 0, LED_PWM, 0) < 0) {
        std::cerr << "Failed to claim LED_PWM (Pin " << LED_PWM << ")\n";
        lgGpiochipClose(gpio_handle);
        return 1;
    }

    int last_clk = lgGpioRead(gpio_handle, ENC_CLK);
    int current_clk, current_dt;
    int duty_cycle = 50; // Start at 50% brightness

    std::cout << "Reading encoder. Ctrl+C to exit.\n";

    while (keep_running) {
        current_clk = lgGpioRead(gpio_handle, ENC_CLK);
        
        // Detect falling edge on Clock pin
        if (current_clk == 0 && last_clk == 1) {
            current_dt = lgGpioRead(gpio_handle, ENC_DT);
            
            if (current_dt != current_clk) {
                duty_cycle += 5; // Clockwise
            } else {
                duty_cycle -= 5; // Counter-clockwise
            }
            
            // Clamp duty cycle between 0 and 100
            if (duty_cycle > 100) duty_cycle = 100;
            if (duty_cycle < 0) duty_cycle = 0;
            
            // Apply PWM (lgpio uses 0-1000000 for duty cycle in microseconds, 
            // but lgGpioWrite is digital. For true hardware PWM on Pi 5, 
            // we use lgTxPwm)
            lgTxPwm(gpio_handle, LED_PWM, 1000.0, duty_cycle, 0, 0);
            std::cout << "Duty Cycle: " << duty_cycle << "%\n";
        }
        last_clk = current_clk;
        usleep(2000); // 2ms debounce delay
    }

    // Cleanup
    std::cout << "\nCleaning up GPIO...\n";
    lgTxPwm(gpio_handle, LED_PWM, 0, 0, 0, 0); // Stop PWM
    lgGpiochipClose(gpio_handle);
    return 0;
}

Troubleshooting: Exact Error Strings and Ranked Causes

When bridging host IDEs and remote ARM hardware, compilation and runtime errors often look cryptic. Here are the exact error strings you will encounter and how to fix them.

Error 1: "lgGpiochipOpen: ERROR: /dev/gpiochip4: Permission denied"

Ranked Causes:

  1. User not in the gpio group: By default, Pi OS Bookworm restricts raw GPIO access. Run sudo usermod -aG gpio $USER and reboot.
  2. Wrong Chip Number: You are running this code on a Pi 4 (which uses chip 0) instead of a Pi 5. Change #define GPIO_CHIP 4 to 0.
  3. Udev rules missing: The liblgpio-dev package failed to trigger the udev reload. Run sudo udevadm control --reload-rules && sudo udevadm trigger.

Error 2: "/usr/bin/ld: cannot find -llgpio: No such file or directory"

Ranked Causes:

  1. Missing Dev Package: You installed liblgpio but not the development headers. Run sudo apt install liblgpio-dev.
  2. CMake Cache Stale: VS Code's CMake integration cached the old environment. Delete the build/ directory and run cmake .. again.

Error 3: "Cannot connect to the remote debugger. Timeout after 10000 ms"

Ranked Causes:

  1. GDB Server Missing: The Pi doesn't have the debug server installed. Run sudo apt install gdbserver.
  2. SSH Keep-Alive Dropping: The debug port tunnel is timing out. Add ServerAliveInterval 60 to your ~/.ssh/config on the host machine.
The First Three Things to Check When It Fails:
  1. Verify the GPIO Chip: Run ls /dev/gpiochip* in the Pi terminal. If you don't see gpiochip4, your kernel or board variant is different than expected.
  2. Check Group Permissions: Run groups. If gpio is missing, the hardware access layer will block your binary at runtime.
  3. Validate SSH Tunnel: Ensure VS Code isn't trying to bind the debug port to a local process. Check the "Ports" tab in the VS Code bottom panel to confirm the remote gdbserver port is forwarded.

Extending and Simplifying the Build

How to Simplify: If C++ and CMake feel like overkill for a simple blink or read task, strip the build down to Python. Install gpiozero (sudo apt install python3-gpiozero) and use the VS Code Python extension. You lose hardware-level PWM precision and microsecond interrupt latency, but you eliminate the CMake compilation step entirely. Change the launch.json to use debugpy instead of gdb.

How to Extend: To turn this into a robust industrial-style input, add hardware debouncing via the RP1's internal filters. The lgpio library supports lgGpioSetDebounce(). Add lgGpioSetDebounce(gpio_handle, ENC_CLK, 5000); (5000 microseconds) immediately after claiming the pin. This offloads the debounce logic from the CPU to the hardware level, freeing up your main loop to handle MQTT telemetry or display rendering without missing encoder ticks.

Frequently Asked Questions

How do I run Raspberry Pi Visual Studio Code without a monitor?

You must enable headless SSH before disconnecting the display. On the Pi, run sudo raspi-config, navigate to Interface Options > SSH, and enable it. On your host machine, generate an SSH key (ssh-keygen -t ed25519) and copy it to the Pi using ssh-copy-id pi@raspberrypi.local. In VS Code, press F1, type Remote-SSH: Connect to Host, and enter pi@raspberrypi.local. This allows full IDE access, including GUI forwarding for X11 apps if you install an X-server like VcXsrv on Windows.

Is Raspberry Pi Visual Studio Code better than Thonny for Python?

For beginners writing simple gpiozero scripts, Thonny is superior because it includes a built-in micro-stepper and requires zero configuration. However, for any project exceeding 300 lines of code, requiring external API integrations, or utilizing C/C++ for performance-critical loops, VS Code is vastly better. VS Code provides Git integration, multi-file refactoring, and remote debugging that Thonny simply cannot match in a production or advanced hobbyist environment.

Why does my Raspberry Pi Visual Studio Code IntelliSense show red squiggles for GPIO?

Red squiggles under headers like #include <lgpio.h> occur because VS Code's IntelliSense engine runs on your host machine (Windows/Mac), which doesn't have the ARM64 Linux headers installed locally. To fix this, install the C/C++ Extension Pack and configure the c_cpp_properties.json file. Set the compilerPath to the remote GCC path (/usr/bin/aarch64-linux-gnu-gcc) and ensure the configurationProvider is set to ms-vscode.cmake-tools so it reads the include directories directly from your CMake configuration.