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)
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.
- Enable I2C on the Pi: SSH into your Pi and run
sudo raspi-config. Navigate to Interface Options > I2C and enable it. Reboot. - Install Build Tools: Run
sudo apt update && sudo apt install build-essential cmake linux-libc-dev. Thelinux-libc-devpackage is mandatory for the<linux/i2c-dev.h>headers. - Connect VS Code: Open VS Code, press
F1, typeRemote-SSH: Connect to Host, and enterpi@<your-pi-ip>. - Initialize CMake: In your project root on the Pi, create a
CMakeLists.txtfile:cmake_minimum_required(VERSION 3.16) project(bme280_reader CXX) set(CMAKE_CXX_STANDARD 17) add_executable(bme280_reader main.cpp) - Configure VS Code Tasks: Install the CMake Tools extension in the remote window. Press
F1and 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, ®, 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:
Failed to read chip ID: Remote I/O errorUnder 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:
- 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 see0x77instead of0x76, update the#define BME280_ADDR 0x77macro in your code. - 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.
- 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
ioctlcall. Checkdmesg | grep -i voltagefor 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.






