Writing C on a single-board computer bridges the gap between high-level Python scripting and bare-metal microcontroller firmware. When you need deterministic timing, minimal memory overhead, or direct hardware register manipulation, C programming in Raspberry Pi is the definitive approach. However, the Linux kernel abstracts hardware, meaning you cannot simply write to memory addresses like you would on an Arduino. You need the right userspace library to translate your C code into kernel GPIO and I2C commands.
This guide walks through a complete, decision-forward build: reading a BME280 environmental sensor over I2C and driving a status LED, using the pigpio C API. We will cover the library selection framework, exact wiring, compilable code with robust error handling, and the specific debugging steps when the hardware refuses to cooperate.
The 2026 Stack: Choosing Your C GPIO Library
The biggest mistake makers make when starting C programming in Raspberry Pi is picking a deprecated or overly verbose library. The Linux kernel deprecated the legacy sysfs GPIO interface years ago, and the once-popular wiringPi library is officially unmaintained and incompatible with modern Pi OS releases. Here is the decision matrix for selecting your GPIO abstraction layer.
| Library | Status & Architecture | Best For | Verdict |
|---|---|---|---|
| wiringPi | Deprecated. Uses legacy sysfs or direct /dev/mem (blocked in modern kernels). | Legacy Pi 1/2/3 projects only. | Avoid. Will fail to compile or run on Pi OS Bookworm/Trixie. |
| libgpiod (v2) | Official Linux character device API. Native, secure, but highly verbose C structs. | Pi 5 (RP1 chip) production daemons, strict security environments. | Use on Pi 5. Steep learning curve for simple sensor toggles. |
| pigpio | Active. Uses a daemon (pigpiod) or direct mmap. Includes native I2C/SPI/PWM wrappers. |
Pi 4 hardware projects, sensor polling, servo control, rapid prototyping. | DEFAULT PICK. Use for Pi 4 Model B builds requiring I2C and GPIO. |
libgpiod v2; pigpio's mmap approach is blocked on Pi 5.
Parts List & Pin Mapping for the Pi 4 Build
Before writing code, verify your bench inventory. This build assumes standard 22 AWG solid-core jumper wires for breadboard connections.
Bill of Materials (BOM)
- Compute: Raspberry Pi 4 Model B (4GB or 8GB) — Approx. $55-$75 USD
- Sensor: BME280 Breakout Board (Adafruit 2652 or generic equivalent with 3.3V logic) — Approx. $10 USD
- Indicator: Standard 5mm LED (any color) + 330Ω current-limiting resistor
- Wiring: Half-size breadboard, 6x male-to-female jumper wires
Pin Mapping Table
The Raspberry Pi 4 uses the BCM (Broadcom) numbering scheme for software definitions. Always verify physical pin locations against the 40-header diagram.
| Component | Function | BCM GPIO | Physical Pin | Wire Color (Suggested) |
|---|---|---|---|---|
| BME280 | VIN (3.3V) | N/A | Pin 1 | Red |
| BME280 | GND | N/A | Pin 6 | Black |
| BME280 | I2C SDA | GPIO 2 | Pin 3 | Blue |
| BME280 | I2C SCL | GPIO 3 | Pin 5 | Yellow |
| LED | Anode (via 330Ω) | GPIO 18 | Pin 12 | Green |
| LED | Cathode | N/A | Pin 14 | Black |
Environment Setup & Compilation
Install the pigpio development headers and enable the I2C bus via the Raspberry Pi configuration tool:
sudo apt update
sudo apt install pigpio libpigpio-dev i2c-tools
sudo raspi-config # Navigate to Interface Options -> I2C -> Enable
Verify the BME280 is physically detected on the bus before compiling C code:
i2cdetect -y 1
# You should see '76' or '77' in the output grid.
Complete C Code: I2C Sensor Reading & GPIO Control
The following code initializes the pigpio library, configures the LED pin as an output, opens the I2C bus, and reads the BME280 Chip ID register (0xD0) to verify communication. It includes strict error handling at every hardware syscall.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pigpio.h>
// --- Pin & Hardware Definitions ---
#define LED_PIN 18 // BCM GPIO 18 (Physical Pin 12)
#define I2C_BUS 1 // /dev/i2c-1
#define BME280_ADDR 0x76 // Default I2C address (check i2cdetect)
#define REG_CHIP_ID 0xD0 // BME280 WHO_AM_I register
#define EXPECTED_ID 0x60 // Expected return value for BME280
int main() {
// 1. Initialize pigpio library
if (gpioInitialise() < 0) {
fprintf(stderr, "FATAL: pigpio initialisation failed. Check daemon/permissions.\n");
return EXIT_FAILURE;
}
// 2. Configure GPIO
gpioSetMode(LED_PIN, PI_OUTPUT);
gpioWrite(LED_PIN, PI_LOW); // Ensure LED starts OFF
// 3. Open I2C Bus
int i2c_handle = i2cOpen(I2C_BUS, BME280_ADDR, 0);
if (i2c_handle < 0) {
fprintf(stderr, "FATAL: Failed to open I2C bus %d at addr 0x%X. Error code: %d\n",
I2C_BUS, BME280_ADDR, i2c_handle);
gpioTerminate();
return EXIT_FAILURE;
}
// 4. Verify Sensor Identity
int chip_id = i2cReadByteData(i2c_handle, REG_CHIP_ID);
if (chip_id < 0) {
fprintf(stderr, "FATAL: I2C read failed. Check wiring and pull-ups. Error: %d\n", chip_id);
i2cClose(i2c_handle);
gpioTerminate();
return EXIT_FAILURE;
}
if (chip_id != EXPECTED_ID) {
fprintf(stderr, "WARNING: Unexpected Chip ID. Got 0x%X, expected 0x%X. Wrong sensor?\n",
chip_id, EXPECTED_ID);
} else {
printf("SUCCESS: BME280 verified on I2C bus %d.\n", I2C_BUS);
}
// 5. Main Loop
printf("Entering main loop. Press Ctrl+C to exit.\n");
while (1) {
gpioWrite(LED_PIN, PI_HIGH);
printf("[STATUS] LED ON | Sensor polling...\n");
sleep(1);
gpioWrite(LED_PIN, PI_LOW);
sleep(1);
}
// 6. Cleanup (Unreachable in this infinite loop, but required for clean exits via signals)
i2cClose(i2c_handle);
gpioTerminate();
return EXIT_SUCCESS;
}
Compile the file (
main.c) using GCC, explicitly linking the pigpio library and the pthread library (required by pigpio's background timing threads):gcc -o sensor_node main.c -lpigpio -lpthreadRun with:
sudo ./sensor_node (pigpio requires root or specific udev rules to access /dev/mem or /dev/gpiomem).
Debugging: When the Build Fails (First 3 Checks)
Hardware programming in C fails silently or with cryptic kernel error codes. If your program exits immediately or hangs, follow this exact decision path. These are the first three things to check, ranked by probability.
1. The I2C Bus is Silent
Exact Error String: FATAL: I2C read failed. Check wiring and pull-ups. Error: -82 (or Remote I/O error in dmesg).
- Cause A (Most Likely): The I2C address in your
#defineis wrong. BME280 modules default to0x76or0x77depending on the manufacturer. Runi2cdetect -y 1and updateBME280_ADDRto match the hex value shown. - Cause B: You wired SDA/SCL to the wrong physical pins, or you are missing I2C pull-up resistors. (The Pi 4 has onboard 1.8kΩ pull-ups for I2C1, but cheap breakout boards sometimes require external 4.7kΩ pull-ups to 3.3V if the trace is cut).
- Cause C: I2C is disabled in the OS. Re-run
sudo raspi-configand verify the interface is active.
2. pigpio Daemon Conflict
Exact Error String: Can't lock /var/run/pigpio.pid or gpioInitialise: mmap failed.
- Cause A: You are running the
pigpiodbackground daemon via systemd, AND trying to run the C library's direct memory mapping simultaneously. They cannot both hold the hardware lock. Stop the daemon (sudo systemctl stop pigpiod) before running your compiled C binary, or switch your C code to use the pigpio socket interface instead of direct calls. - Cause B: You forgot to run the binary with
sudo. While/dev/gpiomemallows non-root GPIO access, pigpio's advanced features and I2C wrappers often require root privileges to map the peripheral addresses.
3. GPIO Pin Mapping Mismatch
Symptom: Code compiles and runs without errors, but the LED never turns on.
- Cause: You confused Physical Pin numbering with BCM GPIO numbering. Physical Pin 12 is BCM GPIO 18. If you pass
12intogpioSetMode(), pigpio will silently toggle BCM GPIO 12 (Physical Pin 32), which is unconnected on your breadboard. Always use the BCM numbers defined in the pin mapping table above.
Extending or Simplifying the Build
Once the baseline I2C read and GPIO toggle are stable, you need to decide how to scale the project based on your end goal.
How to Simplify (The Bare-Minimum Blink)
If you are just learning C syntax on the Pi and the I2C sensor is causing compilation headaches, strip the build down to a pure GPIO blinker.
Remove all i2cOpen and i2cReadByteData calls. Delete the BME280_ADDR defines. This isolates the environment to just gpioInitialise(), gpioSetMode(), and gpioWrite(). If the simplified code fails, your issue is OS-level permissions or a dead LED, not I2C protocol timing.
How to Extend (MQTT Telemetry Publishing)
To turn this bench test into a production IoT node, you need to push the sensor data off the Pi.
1. Install the Eclipse Paho C library: sudo apt install libpaho-mqtt-dev.
2. Extend the C code to parse the raw BME280 compensation registers (detailed in the Bosch BME280 Datasheet).
3. Format the temperature and humidity floats into a JSON string using snprintf.
4. Use MQTTClient_publish() to send the payload to a local Mosquitto broker.
This transforms your C program from a local hardware driver into a high-performance, low-latency edge gateway capable of polling at 100Hz without the garbage collection pauses inherent in Python scripts.
For deeper API references on the functions used here, consult the official pigpio C API documentation and the Raspberry Pi OS configuration guides.






