To develop and debug native C++ hardware projects on a Raspberry Pi 5 using VS Code, use the Remote-SSH extension paired with a CMake toolchain and the native Linux i2c-dev interface. This approach bypasses the latency of SFTP sync plugins, compiles directly on the Pi's ARM64 architecture, and allows you to hit hardware breakpoints using gdbserver. In this guide, we will wire a Bosch BME280 environmental sensor, write a robust C++ I2C driver, and troubleshoot the most common I2C bus faults.

Project Spec Sheet & Hardware Requirements

Difficulty: Intermediate (Requires basic Linux CLI and C++ knowledge)
Time to Complete: 45 minutes
Estimated Cost: $95 - $115 USD

Parts List

  • Microcontroller: Raspberry Pi 5 (8GB RAM variant, BCM2712 SoC, ASM3001 PMIC)
  • Sensor: Adafruit BME280 I2C/SPI Breakout (Product ID: 2652) or equivalent generic module with 3.3V logic
  • Power Supply: Official 27W USB-C PD Power Supply (Required for Pi 5 peripheral headroom)
  • Wiring: 4x Female-to-Female Dupont Jumper Wires (20cm length)
  • Storage: 32GB+ MicroSD card (A2 class recommended for fast CMake indexing)
Hardware Warning: The Raspberry Pi 5 GPIO pins operate at 3.3V logic and are not 5V tolerant. Connecting a 5V I2C sensor module without a logic level shifter will permanently damage the BCM2712 SoC's GPIO pad ring.

Raspberry Pi 5 vs Pi 4: I2C & GPIO Specifications

Before wiring, understand how the Pi 5's updated power management and I/O routing differ from the Pi 4. This data is critical when calculating I2C pull-up requirements and bus capacitance.

Feature Raspberry Pi 5 (BCM2712) Raspberry Pi 4B (BCM2711)
Default I2C Bus /dev/i2c-1 (BCM 2/3) /dev/i2c-1 (BCM 2/3)
I2C Clock Stretching Hardware supported (RP1 chip) Known silicon bug (requires software workaround)
3.3V Rail Max Current ~1.2A (Dedicated PMIC buck converter) ~1.2A (Shared system rail)
Default Pull-up Resistance 50kΩ (Internal, configurable) 50kΩ (Internal, fixed)
I2C Max Bus Capacitance 400 pF (Standard Fast Mode) 400 pF (Standard Fast Mode)

Pin Mapping & Physical Wiring

The BME280 breakout board uses standard I2C. We will map it to the Pi 5's primary I2C bus (Bus 1). Ensure the Pi is completely powered down and the USB-C cable is disconnected before making physical connections.

Pi 5 Physical Pin BCM GPIO Function BME280 Breakout Pin
1 N/A 3.3V Power VIN / 3V3
6 N/A Ground GND
3 GPIO 2 I2C1 SDA SDI / SDA
5 GPIO 3 I2C1 SCL SCK / SCL

Note: If your specific BME280 module has the SDO pin tied to VCC by default, its I2C address will be 0x77 instead of 0x76. The code below defaults to 0x76; update the macro if i2cdetect shows otherwise.

VS Code Remote-SSH & CMake Toolchain Setup

Do not use SFTP extensions to upload files. They break file permissions and fail to sync hidden build directories. Use the official VS Code Remote-SSH extension to treat the Pi as a native development machine.

  1. Enable I2C on the Pi: SSH into your Pi and run sudo raspi-config. Navigate to Interface Options > I2C and enable it. Reboot.
  2. Install Build Tools: Run sudo apt update && sudo apt install build-essential cmake linux-libc-dev. The linux-libc-dev package is mandatory for the <linux/i2c-dev.h> headers.
  3. Connect VS Code: Open VS Code, press F1, type Remote-SSH: Connect to Host, and enter pi@<your-pi-ip>.
  4. Initialize CMake: In your project root on the Pi, create a CMakeLists.txt file:
    cmake_minimum_required(VERSION 3.16)
    project(bme280_reader CXX)
    set(CMAKE_CXX_STANDARD 17)
    add_executable(bme280_reader main.cpp)
  5. Configure VS Code Tasks: Install the CMake Tools extension in the remote window. Press F1 and select CMake: Configure. This generates the build directory and configures IntelliSense to read the Pi's ARM64 system headers, eliminating false red squiggles on your local monitor.

Compilable C++ Source Code (I2C Native)

Below is the complete, compilable C++ code to read the BME280 Chip ID register. This acts as a hardware handshake; if the sensor returns 0x60, your I2C wiring and power are correct. This code uses the native Linux I2C dev-interface, requiring zero external libraries like wiringPi or pigpio.

#include <iostream>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/i2c-dev.h>
#include <unistd>
#include <cerrno>
#include <cstring>
#include <iomanip>

// BME280 I2C Address (SDO to GND = 0x76, SDO to VCC = 0x77)
#define I2C_BUS "/dev/i2c-1"
#define BME280_ADDR 0x76
#define REG_CHIP_ID 0xD0
#define EXPECTED_CHIP_ID 0x60

int main() {
    // 1. Open the I2C bus
    int file = open(I2C_BUS, O_RDWR);
    if (file < 0) {
        std::cerr << "Failed to open I2C bus: " << strerror(errno) << std::endl;
        return EXIT_FAILURE;
    }

    // 2. Set the I2C slave address
    if (ioctl(file, I2C_SLAVE, BME280_ADDR) < 0) {
        std::cerr << "Failed to acquire I2C address 0x" << std::hex << BME280_ADDR 
                  << ": " << strerror(errno) << std::endl;
        close(file);
        return EXIT_FAILURE;
    }

    // 3. Write the register address we want to read
    __u8 reg = REG_CHIP_ID;
    if (write(file, &reg, 1) != 1) {
        std::cerr << "Failed to write register address: " << strerror(errno) << std::endl;
        close(file);
        return EXIT_FAILURE;
    }

    // 4. Read the response byte
    __u8 chip_id = 0;
    if (read(file, &chip_id, 1) != 1) {
        std::cerr << "Failed to read chip ID: " << strerror(errno) << std::endl;
        close(file);
        return EXIT_FAILURE;
    }

    // 5. Validate the hardware handshake
    std::cout << "BME280 Chip ID: 0x" << std::hex << std::setfill('0') << std::setw(2) << (int)chip_id << std::endl;
    
    if (chip_id == EXPECTED_CHIP_ID) {
        std::cout << "Success: BME280 sensor verified and communicating." << std::endl;
    } else {
        std::cerr << "Error: Unexpected Chip ID. Check wiring or I2C address." << std::endl;
    }

    close(file);
    return EXIT_SUCCESS;
}

Build and run this via the VS Code terminal: cmake --build build && ./build/bme280_reader.

Debugging "Remote I/O Error" & I2C Faults

If your build succeeds but the runtime fails, you will likely encounter this exact error string in the VS Code terminal:

Exact Error String: Failed to read chip ID: Remote I/O error
Under the hood, this is Linux errno 121 (EREMOTEIO), triggered when the I2C controller sends a clock pulse but receives a NACK (Not Acknowledged) from the slave device.

When this happens, do not rewrite your code. The C++ I2C implementation is correct; the physical layer is failing. Here are the first three things to check, ranked by probability:

  1. Verify the Address with i2cdetect: Open a separate terminal and run i2cdetect -y 1. If the grid is empty, your sensor is unpowered or SDA/SCL are swapped. If you see 0x77 instead of 0x76, update the #define BME280_ADDR 0x77 macro in your code.
  2. Check for SDA/SCL Swap: The "Remote I/O error" is the classic symptom of crossing the data and clock lines. The Pi is sending clock pulses on the SDA line, which the sensor ignores, resulting in a bus timeout. Swap the physical wires on pins 3 and 5.
  3. Inspect 3.3V Rail Brownouts: The Pi 5's PMIC will throttle I2C clocks if the 3.3V rail sags below 3.1V. If you are powering the Pi via a generic USB-C phone charger instead of the official 27W PD supply, the sensor may brownout during the ioctl call. Check dmesg | grep -i voltage for under-voltage warnings.

Extending and Simplifying the Build

Once the Chip ID handshake passes, you have a verified physical link. From here, you can adapt the architecture based on your project's end goals.

How to Extend the Project

To read actual temperature and pressure data, you must read the sensor's compensation parameters (registers 0x88 to 0xA1) and apply the Bosch calibration formulas. Furthermore, if you want to add an SSD1306 OLED display to show the readings, do not put it on the same I2C bus. Long wires and multiple modules increase bus capacitance past the 400 pF limit, causing intermittent Remote I/O errors. Instead, wire the OLED to the Pi 5's SPI0 bus (Physical pins 19, 21, 23, 24, 25) and use a C++ library like U8g2.

How to Simplify the Build

If you do not need microsecond-level sampling latency or multi-threaded C++ performance, C++ is overkill for basic environmental logging. Simplify the stack by switching to Python using the smbus2 and RPi.bme280 libraries. You can still use VS Code Remote-SSH to edit the Python scripts, and you can debug them using the debugpy extension. However, for production daemons, systemd services, and strict memory-constrained environments, the native C++ approach detailed above remains the most robust path for the Raspberry Pi 5 hardware.