C programming on the Raspberry Pi remains the gold standard for deterministic timing and low-latency hardware control. While Python dominates rapid prototyping, C eliminates the garbage collection pauses that cause missed interrupts or jittery PWM signals. This guide targets the Raspberry Pi 5 8GB running Raspberry Pi OS (Bookworm or later), using the pigpio library to read a Bosch BME280 environmental sensor over I2C and trigger a 5V relay based on temperature thresholds.
By the end of this build, you will have a bare-metal C application that polls I2C registers, performs basic data conversion, and toggles a GPIO pin with microsecond precision. We will also cover the exact failure modes you will encounter when the I2C bus locks up on the Pi 5's new RP1 southbridge chip.
Library Selection for Pi 5 Hardware Interfacing
Before writing a single line of C, you must choose your hardware abstraction layer. The Raspberry Pi 5 uses the custom RP1 chip for GPIO and I2C, which deprecated several legacy access methods. Below is a comparison of the primary C libraries available for Pi hardware control in 2026.
| Library | GPIO Latency | I2C/SPI Support | Maintenance Status | Best For |
|---|---|---|---|---|
| pigpio | ~1 µs | Native (Hardware) | Active / Stable | Combined GPIO, PWM, and I2C/SPI projects |
| libgpiod | ~5 µs | None (GPIO only) | Official Standard | Pure GPIO toggling, character device API |
| wiringPi | ~2 µs | Native (Legacy) | Deprecated / Forked | Legacy Pi 3/4 codebases only |
| sysfs | >500 µs | None (GPIO only) | Removed in Kernel 6.x | None (Do not use) |
For this project, we use pigpio because it provides a unified C API for both the I2C bus and GPIO outputs without requiring complex file descriptor management for the I2C character devices.
Parts List and Pin Mapping
Ensure you have the exact variants listed below. Substituting a 5V logic sensor for the 3.3V BME280 will destroy the Pi 5's RP1 chip.
- Microcontroller: Raspberry Pi 5 8GB (with Active Cooler)
- Power Supply: Official 27W USB-C PD Power Supply (5V/5A)
- Sensor: BME280 I2C Breakout (Adafruit 2652 or equivalent 3.3V Bosch BME280)
- Actuator: 5V Relay Module with Optocoupler (Songle SRD-05VDC-SL-C based, active LOW trigger)
- Wiring: Female-to-female and female-to-male jumper wires (22 AWG)
Pin Mapping Table
| Pi 5 Pin (Physical) | BCM GPIO | Function | Connected To |
|---|---|---|---|
| 1 | 3.3V | Power | BME280 VIN |
| 3 | GPIO 2 (SDA1) | I2C Data | BME280 SDI |
| 5 | GPIO 3 (SCL1) | I2C Clock | BME280 SCK |
| 6 | GND | Ground | BME280 GND & Relay GND |
| 4 | 5V | Power | Relay VCC |
| 37 | GPIO 26 | Digital Out | Relay IN (Control) |
The Build: Step-by-Step Wiring
- Power Down: Disconnect the USB-C power supply from the Raspberry Pi. Never wire I2C or GPIO pins while the board is energized.
- Wire the BME280 Sensor: Connect Pin 1 (3.3V) to VIN, Pin 3 (SDA) to SDI, Pin 5 (SCL) to SCK, and Pin 6 (GND) to GND. Ensure the I2C address jumper on the breakout is set to 0x77 (the Adafruit default).
- Wire the Relay Module: Connect Pin 4 (5V) to the Relay VCC. Connect Pin 6 (GND) to the Relay GND. Connect Pin 37 (GPIO 26) to the Relay IN pin.
- Enable I2C: Boot the Pi, open a terminal, and run
sudo raspi-config. Navigate to Interface Options > I2C and enable it. Reboot the Pi. - Verify I2C Bus: Run
i2cdetect -y 1. You should see77in the grid. If you see76, update the#define BME_ADDRin the C code below. - Install pigpio: Run
sudo apt update && sudo apt install pigpio libpigpio-dev.
Complete C Code with Error Handling
Save the following code as env_monitor.c. This script initializes the pigpio daemon, configures the BME280 for continuous temperature reading, and toggles the relay if the temperature exceeds 25.0°C.
Note: The temperature compensation math below is a simplified approximation for demonstration. Production firmware should import the official Bosch BME280 integer compensation API from the Adafruit BME280 Learning Guide.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pigpio.h>
#define RELAY_PIN 26
#define I2C_BUS 1
#define BME_ADDR 0x77
#define REG_TEMP_MSB 0xFA
#define REG_CTRL_MEAS 0xF4
#define TEMP_THRESHOLD 25.0
int main() {
// Initialize pigpio library
if (gpioInitialise() < 0) {
fprintf(stderr, "Error: pigpio init failed. Are you running as root?\n");
return 1;
}
// Setup GPIO for Relay (Active LOW)
gpioSetMode(RELAY_PIN, PI_OUTPUT);
gpioWrite(RELAY_PIN, PI_HIGH); // HIGH = Relay OFF
// Open I2C bus
int i2c_handle = i2cOpen(I2C_BUS, BME_ADDR, 0);
if (i2c_handle < 0) {
fprintf(stderr, "Error: I2C open failed. Check address and wiring.\n");
gpioTerminate();
return 1;
}
// Configure BME280: Temp oversampling x1, Mode = Normal
if (i2cWriteByteData(i2c_handle, REG_CTRL_MEAS, 0x27) != 0) {
fprintf(stderr, "Error: Failed to write config to BME280.\n");
i2cClose(i2c_handle);
gpioTerminate();
return 1;
}
usleep(100000); // Allow sensor to take first reading
printf("Monitoring started. Threshold: %.1f C\n", TEMP_THRESHOLD);
while(1) {
char buf[3];
// Read 3 bytes of temperature data
if (i2cReadI2CBlockData(i2c_handle, REG_TEMP_MSB, buf, 3) != 3) {
fprintf(stderr, "I2C read failed: Remote I/O error (errno 121)\n");
break; // Exit loop on I2C bus failure
}
// Combine bytes into raw 20-bit integer
int32_t raw_temp = (buf[0] << 12) | (buf[1] << 4) | (buf[2] >> 4);
// Simplified approximation (Production requires Bosch compensation)
float temp_c = (raw_temp / 16384.0) * 25.0;
printf("Temp: %.2f C\n", temp_c);
// Toggle Relay based on threshold
if (temp_c > TEMP_THRESHOLD) {
gpioWrite(RELAY_PIN, PI_LOW); // Turn ON (Active LOW)
} else {
gpioWrite(RELAY_PIN, PI_HIGH); // Turn OFF
}
sleep(2);
}
// Cleanup
i2cClose(i2c_handle);
gpioTerminate();
return 0;
}
Compile and Run:
gcc -o env_monitor env_monitor.c -lpigpio -lpthread
sudo ./env_monitor
Debugging: First Three Things to Check When It Fails
When working with C on the Pi's I2C bus, you will inevitably encounter the following exact error string in your terminal:
I2C read failed: Remote I/O error (errno 121)
This maps to the Linux EREMOTEIO error, meaning the Pi's RP1 chip sent a clock pulse but the sensor failed to acknowledge (NACK) or pulled the SDA line low indefinitely (clock stretching timeout). If your code crashes or hangs, check these three things in order:
- Verify the I2C Address and Pull-ups: Run
i2cdetect -y 1. If the grid is entirely blank or showsUU, your sensor is either unpowered, wired to the wrong pins, or lacks pull-up resistors. The Pi 5 has internal 1.8kΩ pull-ups on SDA/SCL, which are usually sufficient for short jumper wires. If your wires exceed 12 inches, you must solder external 4.7kΩ pull-up resistors to the 3.3V line. - Check for RP1 Clock Stretching Quirks: The Pi 5's RP1 southbridge has a known hardware quirk where it struggles with sensors that aggressively use I2C clock stretching (holding SCL low to buy processing time). If
i2cdetectsees the sensor butpigpiothrowserrno 121during reads, add a 10ms delay (usleep(10000)) between your I2C write and read commands to give the sensor time to process without holding the clock. - Inspect Power Domain Mismatches: If you accidentally wired the BME280 VCC to Pin 2 (5V) instead of Pin 1 (3.3V), the sensor's internal logic level shifters may have locked up, or the chip may be dead. Disconnect power, measure the voltage at the sensor's VCC pin with a multimeter (it must read 3.3V ± 0.1V), and reset the Pi.
Extending and Simplifying the Build
Once you have the baseline C application running, you can adapt the architecture to fit your specific project constraints.
How to Simplify
If you only need to toggle the relay and don't care about microsecond I2C latency, drop pigpio and switch to libgpiod for the GPIO control, while using a standard USB environmental sensor (like the Adafruit Si7021 USB stick) for temperature. This eliminates all I2C bus debugging and reduces your C code to pure character-device GPIO toggling via the gpiod_line_set_value() API.
How to Extend
To turn this local monitor into an IoT node, integrate libmosquitto to publish the temperature data to an MQTT broker. You will need to install the dev headers (sudo apt install libmosquitto-dev) and link it during compilation (-lmosquitto). By wrapping the gpioWrite logic in a separate POSIX thread (pthread_create), you can publish sensor data to your broker every second while maintaining a high-speed local GPIO feedback loop that reacts to thermal runaway in under 50 microseconds.
For more details on configuring the Pi 5's hardware interfaces, refer to the Raspberry Pi Official Configuration Documentation.






