The Decision: Which C Library for Raspberry Pi GPIO and I2C?
When programming the Raspberry Pi and C together, your first major roadblock is choosing the hardware abstraction library. Historically, makers relied on pigpio or bcm2835. However, the release of the Raspberry Pi 5 changed the hardware landscape entirely. The Pi 5 offloads peripheral control to a dedicated RP1 southbridge chip, rendering legacy memory-mapped libraries useless or unstable.
Here is the decision matrix for selecting your C library in 2026. The definitive pick is lgpio.
| Library | Pi 4 Support | Pi 5 Support | I2C/SPI Built-in | Verdict |
|---|---|---|---|---|
sysfs (/sys/class/gpio) |
Deprecated | Deprecated | No | Reject: Removed from modern Linux kernels due to race conditions. |
| bcm2835 | Yes | No (Hard fails) | Yes | Reject: Relies on BCM2711 memory maps; incompatible with Pi 5 RP1. |
| pigpio | Yes | Partial/Buggy | Yes | Reject: No longer actively maintained; daemon architecture adds latency. |
| lgpio | Yes | Yes (Native) | Yes | SELECT: Official recommendation, uses standard Linux gpiochar and spidev/i2c-dev. |
lgpio for GPIO control and the standard Linux i2c-dev kernel interface for I2C communication. This combination guarantees forward compatibility with future Pi hardware revisions while keeping your C code close to the metal.
Parts List and Pin Mapping for the Pi 5 Build
This project targets the Raspberry Pi 5 (4GB variant). We will read temperature/pressure data from a BME280 sensor over I2C and trigger a 5V relay based on a temperature threshold.
Exact Parts List
- Microcontroller: Raspberry Pi 5 (4GB RAM) — ~$60.00
- Power Supply: Official Raspberry Pi 27W USB-C PD Power Supply — ~$12.00 (Crucial: Pi 5 will throttle GPIO/current limits on non-PD 5V/5A supplies).
- Sensor: Adafruit BME280 I2C/SPI Breakout (Product ID: 2652) — ~$19.95
- Actuator: SainSmart 5V 1-Channel Relay Module (Optocoupler isolated) — ~$8.50
- Wiring: 22 AWG silicone jumper wires (Female-to-Female and Male-to-Female).
Pin Mapping Table (40-Pin Header)
The Pi 5 maintains the standard 40-pin layout, but remember that all GPIO pins are strictly 3.3V logic. Never feed 5V back into a Pi 5 GPIO pin, or you will destroy the RP1 chip.
| Component | Pin Function | Pi 5 Physical Pin | BCM / GPIO Number |
|---|---|---|---|
| BME280 | VIN (3.3V) | Pin 1 | N/A (Power) |
| BME280 | GND | Pin 6 | N/A (Ground) |
| BME280 | SDA | Pin 3 | GPIO 2 (I2C1 SDA) |
| BME280 | SCL | Pin 5 | GPIO 3 (I2C1 SCL) |
| Relay Module | VCC | Pin 2 | N/A (5V Power) |
| Relay Module | GND | Pin 9 | N/A (Ground) |
| Relay Module | IN (Signal) | Pin 37 | GPIO 26 |
Wiring and Compiling the C Environment
Before writing code, ensure your OS environment is configured for low-level hardware access. We are using Raspberry Pi OS (64-bit, Bookworm or later).
- Enable I2C: Run
sudo raspi-config, navigate to Interface Options -> I2C, and enable it. Reboot. - Install C Toolchain and lgpio: Open your terminal and install the necessary development headers.
sudo apt update sudo apt install build-essential liblgpio-dev i2c-tools - Verify Hardware: Run
i2cdetect -y 1. You should see77(or76) in the grid, confirming the BME280 is on the I2C bus. - Set Permissions: To run your C program without
sudo, ensure your user is in thei2candgpiogroups:sudo usermod -aG i2c,gpio $USER newgrp i2c
The Complete C Code: I2C Sensor Reading and GPIO Relay Control
This code uses lgpio for the relay and standard Linux ioctl calls for the I2C sensor. It includes robust error handling, file descriptor cleanup, and explicit pin definitions.
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/i2c-dev.h>
#include <lgpio.h>
#include <unistd.h>
#include <errno.h>
// --- PIN & ADDRESS DEFINITIONS ---
#define I2C_DEV_FILE "/dev/i2c-1"
#define BME280_ADDR 0x77 // Adafruit default. Use 0x76 for generic clones.
#define BME280_REG_ID 0xD0 // Chip ID register (should return 0x60)
#define RELAY_PIN 26 // BCM GPIO 26 (Physical Pin 37)
// Cleanup function to prevent GPIO lockups on exit
void cleanup(int gpio_handle, int i2c_fd) {
if (gpio_handle >= 0) {
lgGpioWrite(gpio_handle, RELAY_PIN, 0); // Turn off relay
lgGpiochipClose(gpio_handle);
}
if (i2c_fd >= 0) close(i2c_fd);
}
int main() {
int h = -1;
int i2c_fd = -1;
// 1. Initialize GPIO via lgpio
// Pi 5 uses chip 4 (RP1), Pi 4 uses chip 0. We try 4 first.
h = lgGpiochipOpen(4);
if (h < 0) h = lgGpiochipOpen(0);
if (h < 0) {
fprintf(stderr, "Failed to open GPIO chip: %s\n", lguErrorText(h));
return EXIT_FAILURE;
}
// Claim GPIO 26 as output, default LOW (relay off)
int claim = lgGpioClaimOutput(h, 0, RELAY_PIN, 0);
if (claim < 0) {
fprintf(stderr, "Failed to claim GPIO %d: %s\n", RELAY_PIN, lguErrorText(claim));
cleanup(h, i2c_fd);
return EXIT_FAILURE;
}
// 2. Initialize I2C Bus
i2c_fd = open(I2C_DEV_FILE, O_RDWR);
if (i2c_fd < 0) {
perror("Failed to open I2C device file");
cleanup(h, i2c_fd);
return EXIT_FAILURE;
}
if (ioctl(i2c_fd, I2C_SLAVE, BME280_ADDR) < 0) {
perror("Failed to acquire I2C bus access (ioctl)");
cleanup(h, i2c_fd);
return EXIT_FAILURE;
}
// 3. Verify Sensor Connection by reading Chip ID
char reg = BME280_REG_ID;
if (write(i2c_fd, ®, 1) != 1) {
perror("I2C write failed");
cleanup(h, i2c_fd);
return EXIT_FAILURE;
}
char chip_id = 0;
if (read(i2c_fd, &chip_id, 1) != 1) {
perror("ioctl I2C_RDWR failed: Remote I/O error");
cleanup(h, i2c_fd);
return EXIT_FAILURE;
}
if (chip_id != 0x60) {
fprintf(stderr, "Invalid BME280 Chip ID: 0x%02X (Expected 0x60)\n", chip_id);
cleanup(h, i2c_fd);
return EXIT_FAILURE;
}
printf("BME280 detected successfully.\n");
// 4. Main Control Loop
printf("Starting thermal control loop. Press Ctrl+C to exit.\n");
while(1) {
// Note: Full BME280 compensation math omitted for brevity.
// Here we simulate a temperature read of 28.5C.
float simulated_temp = 28.5;
if (simulated_temp > 25.0) {
lgGpioWrite(h, RELAY_PIN, 1); // Trigger relay (Active LOW relays need 0, adjust if needed)
printf("[RELAY ON] Temp: %.1fC - Cooling engaged.\n", simulated_temp);
} else {
lgGpioWrite(h, RELAY_PIN, 0);
printf("[RELAY OFF] Temp: %.1fC - System idle.\n", simulated_temp);
}
sleep(2);
}
cleanup(h, i2c_fd);
return EXIT_SUCCESS;
}
Compile Command: gcc -o thermal_relay thermal_relay.c -llgpio
Execute: ./thermal_relay
Debugging: "Remote I/O error" and the First Three Checks
When writing C for embedded Linux, the most notorious and frustrating error you will encounter on the I2C bus is:
ioctl I2C_RDWR failed: Remote I/O error (errno 121)
This is a generic kernel-level rejection meaning the master (Pi) sent a clock pulse, but the slave (sensor) did not acknowledge (ACK) the address. Here are the ranked causes and fixes:
- Wrong I2C Address (Most Common): Adafruit BME280s default to
0x77. Cheap generic Amazon/AliExpress clones almost always default to0x76. Fix: Runi2cdetect -y 1and change the#define BME280_ADDRin your C code to match the grid output. - Missing or Weak Pull-Up Resistors: The Pi has 1.8kΩ internal pull-ups on SDA/SCL, but if your wires exceed 12 inches, capacitance degrades the signal edges. Fix: Solder 4.7kΩ physical pull-up resistors between SDA/3.3V and SCL/3.3V on the breakout board.
- Bus Lockup from Previous Crash: If your C code crashed while the Pi was driving SDA low, the slave might be holding the bus hostage. Fix: Power cycle both the Pi and the sensor completely. Unplug the 5V/3.3V rail for 10 seconds to drain parasitic capacitance.
1. Run
i2cdetect -y 1 in the terminal. If the grid is empty, it is a hardware/wiring issue, not a C code issue.2. Check
ls /dev/gpiochip*. If gpiochip4 is missing, your Pi OS is outdated and lacks RP1 southbridge drivers.3. Verify your relay module logic. Many 5V relay modules are Active LOW. If your relay clicks on when the code says OFF, change
lgGpioWrite(h, RELAY_PIN, 1) to 0.
Extending or Simplifying the Build
Depending on your end goal, you should adapt this baseline architecture rather than starting from scratch.
How to Simplify (The 'Just Blink an LED' Route)
If you do not need environmental sensing and only want to toggle GPIO pins (e.g., turning on a grow light via a cron job), strip out all <linux/i2c-dev.h> and ioctl code. Keep only the lgpio block. You can reduce the entire program to 20 lines of C, compile it, and call the binary directly from a bash script or systemd timer.
How to Extend (Production-Ready Data Logging)
To turn this into a robust data logger:
- Add MQTT: Integrate the Eclipse Paho C MQTT library. Publish the compensated BME280 temperature/humidity payloads to a local Mosquitto broker for Home Assistant ingestion.
- Implement True BME280 Math: The BME280 outputs raw ADC bytes that require factory-calibration compensation. Import the Bosch Sensortec official C API and link it during compilation (
-lbme280) to get exact floating-point Celsius and hPa readings. - Add Watchdog Timers: Use the Linux
<linux/watchdog.h>API to ping the hardware watchdog. If your C loop hangs due to an I2C bus lockup, the Pi will automatically hard-reboot itself, which is mandatory for remote off-grid deployments.
By anchoring your Raspberry Pi and C projects to the lgpio standard and native Linux I2C interfaces, you bypass the fragility of deprecated Python wrappers and legacy memory-mapped libraries, ensuring your embedded systems survive the next generation of hardware upgrades.






