Back to Technology

Raspberry Pi for Embedded Prototyping

April 1, 2026 Wasil Zafar 55 min read

Build embedded systems with Raspberry Pi — GPIO programming in Python and C, SPI/I2C/UART peripheral communication, camera and display interfaces, bare-metal development, Linux device drivers, and complete IoT project walkthroughs.

Table of Contents

  1. History of Raspberry Pi
  2. Hardware Overview
  3. GPIO Programming
  4. SPI, I2C & UART Communication
  5. Camera & Display Interfaces
  6. Linux Device Drivers on Pi
  7. Bare-Metal Pi Programming
  8. IoT Project Walkthroughs
  9. Performance & Power Management
  10. Case Studies
  11. Exercises
  12. Raspberry Pi Project Plan Generator
  13. Conclusion & Resources

History of Raspberry Pi

Key Insight: Since its launch in 2012, the Raspberry Pi has sold over 60 million units worldwide, making it one of the best-selling computers of all time and one of the most impactful tools in embedded systems education and prototyping.

The Raspberry Pi story begins at the University of Cambridge's Computer Laboratory. In the mid-2000s, Eben Upton, Rob Mullins, Jack Lang, and Alan Mycroft noticed a troubling decline in the number and skill level of students applying to study computer science. Where previous generations had grown up programming BBC Micros and Amigas, the new applicants had primarily used computers as appliances — browsing the web and writing documents, but never writing code or understanding hardware.

Upton and his colleagues set out to build an affordable, programmable computer that could reignite the spirit of hands-on computing. The Raspberry Pi Foundation was established as a UK-registered charity in 2009, and development began on what would become the original Raspberry Pi Model B. The first prototype used an Atmel ATmega644 microcontroller, but the team quickly pivoted to Broadcom's BCM2835 system-on-chip (SoC), which offered a 700 MHz ARM11 core, a VideoCore IV GPU capable of 1080p video, and — critically — a price point that could hit the target of $35.

On February 29, 2012, the Raspberry Pi Model B went on sale. Demand was so overwhelming that both the RS Components and Farnell/element14 websites crashed within minutes. The initial production run of 10,000 units sold out in hours. Within its first year, over one million Pi boards had shipped — a staggering number for what was conceived as a niche educational tool.

Evolution of the Platform

The evolution of the Raspberry Pi tracks the rapid advancement of ARM-based SoCs. The original Model B (2012) featured a single-core ARM11 at 700 MHz with 256 MB of RAM. The Pi 2 Model B (February 2015) jumped to a quad-core Cortex-A7 at 900 MHz with 1 GB RAM — a dramatic leap in compute power. The Pi 3 Model B (February 2016) upgraded to a 64-bit Cortex-A53 and added integrated WiFi and Bluetooth, eliminating the need for USB dongles.

The Pi 4 Model B (June 2019) was a generational leap: a Cortex-A72 quad-core at 1.5 GHz, up to 8 GB RAM, USB 3.0, Gigabit Ethernet with no shared bus, and dual micro-HDMI outputs supporting 4K. For the first time, a Raspberry Pi could genuinely serve as a desktop replacement for everyday tasks.

In October 2023, the Raspberry Pi 5 arrived with a custom-designed RP1 I/O controller chip — the first time the Raspberry Pi Foundation had designed its own silicon for I/O management. The Pi 5 features a quad-core Cortex-A76 at 2.4 GHz, delivering 2-3x the performance of the Pi 4, with PCIe 2.0 x1 for NVMe storage, a dedicated UART connector for debugging, and a real-time clock with battery backup support.

Meanwhile, the Raspberry Pi Pico (January 2021) marked the Foundation's entry into microcontrollers with the custom-designed RP2040 chip — dual Cortex-M0+ cores at 133 MHz with programmable I/O (PIO) state machines. At $4, it opened up real-time, low-power applications that Linux-based Pis cannot efficiently address.

Milestone Date Significance
Foundation established2009UK charity formed to promote CS education
Model B launchFeb 2012First $35 single-board computer, 10K units sold out in hours
Model A launchFeb 2013$25 stripped-down version, 256 MB RAM
Pi 2 Model BFeb 2015Quad-core Cortex-A7, 1 GB RAM
Pi ZeroNov 2015$5 computer, single-core 1 GHz
Pi 3 Model BFeb 201664-bit Cortex-A53, onboard WiFi/BLE
Pi 4 Model BJun 2019Cortex-A72, up to 8 GB, USB 3.0, 4K dual display
Pi PicoJan 2021RP2040 microcontroller, $4, PIO state machines
Pi Zero 2 WOct 2021Quad-core Cortex-A53 at $15
Pi 5Oct 2023Cortex-A76, custom RP1 southbridge, PCIe 2.0
60M units sold2024One of the best-selling computers of all time

The Raspberry Pi's impact extends far beyond education. It has become the de facto prototyping platform for IoT products, a popular choice for home automation servers, a staple in industrial edge computing, and even found its way into space. Its combination of Linux capability, GPIO access, camera interfaces, and community support makes it uniquely suited for embedded prototyping where rapid iteration matters more than real-time determinism.

Hardware Overview

Choosing the right Raspberry Pi model is the first decision in any embedded project. Each model targets a different balance of processing power, I/O capability, power consumption, and cost. Understanding the hardware differences lets you match the board to your application requirements.

Model Comparison

Feature Pi 5 Pi 4 Model B Pi Zero 2 W Pi Pico / Pico W
CPUCortex-A76 x4 @ 2.4 GHzCortex-A72 x4 @ 1.5 GHzCortex-A53 x4 @ 1 GHzCortex-M0+ x2 @ 133 MHz
RAM4/8 GB LPDDR4X1/2/4/8 GB LPDDR4512 MB LPDDR2264 KB SRAM
GPIO Pins40404026 (30 total)
USB2x USB 3.0, 2x USB 2.02x USB 3.0, 2x USB 2.01x micro-USB (OTG)1x micro-USB
NetworkingGbE, WiFi 5, BLE 5.0GbE, WiFi 5, BLE 5.0WiFi 4, BLE 4.2None / WiFi 4 (W)
StoragemicroSD + PCIe NVMemicroSDmicroSD2 MB Flash
Display2x HDMI (4Kp60)2x micro-HDMI (4Kp30)mini-HDMINone (SPI/I2C displays)
Camera2x MIPI CSI-21x MIPI CSI-21x MIPI CSI-2 (mini)None
Power5V/5A USB-C (PD)5V/3A USB-C5V/2.5A micro-USB1.8-5.5V via VSYS
Price$60 / $80$35-$75$15$4 / $6
Best ForEdge AI, desktop, NVMeGeneral prototypingCompact IoT, wearablesReal-time, low-power, sensors
Key Insight: The Pi 5 introduced the RP1 custom I/O controller, meaning GPIO, SPI, I2C, UART, and USB are no longer directly on the SoC. This improves performance and timing consistency but changes how device tree overlays and low-level access work compared to Pi 4 and earlier.

The 40-Pin GPIO Header

All full-sized Raspberry Pi models share a standardized 40-pin GPIO header (the Pi 1 Model A/B had 26 pins). This header provides access to:

  • 28 GPIO pins — configurable as digital input/output with internal pull-up/down resistors
  • 2x SPI buses — SPI0 (CE0, CE1) and SPI1 (CE0, CE1, CE2)
  • 2x I2C buses — I2C1 (GPIO 2/3) for user devices, I2C0 (GPIO 0/1) for HAT EEPROM
  • 2x UART — UART0 (GPIO 14/15) and UART2-5 via alt functions
  • 2x PWM channels — hardware PWM on GPIO 12/13/18/19
  • Power pins — 5V (pins 2/4), 3.3V (pins 1/17), Ground (pins 6/9/14/20/25/30/34/39)
Warning: Raspberry Pi GPIO pins operate at 3.3V logic levels. Connecting 5V signals directly to GPIO pins will permanently damage the BCM/RP1 chip. Always use level shifters (e.g., TXB0108 or voltage dividers) when interfacing with 5V peripherals.

Power Architecture

Understanding power delivery is critical for reliable embedded projects. The Pi 5 uses a dedicated PMIC (power management IC) for the first time — the DA9091. This enables proper power sequencing, voltage monitoring, and the ability to power via the new PCIe connector. The Pi 4 and earlier use discrete regulators.

A fully loaded Pi 5 with NVMe drive, camera, and USB peripherals can draw up to 25W. The official 27W USB-C PD power supply is recommended. For battery-powered applications, the Pi Zero 2 W at approximately 350 mA typical (1.75W) or the Pi Pico at under 50 mA is far more practical.

GPIO Programming

GPIO (General Purpose Input/Output) programming is the foundation of all Raspberry Pi embedded projects. The Pi ecosystem offers multiple libraries at different abstraction levels, from high-level Python wrappers to direct register manipulation in C.

Library Comparison

Library Language Level Pi 5 Support Notes
gpiozeroPythonHighYes (via lgpio)Official, beginner-friendly, device-oriented API
RPi.GPIOPythonMediumNo (deprecated)Legacy, direct /dev/mem access, Pi 4 and earlier only
lgpioPython/CMediumYesUses /dev/gpiochip, Pi 5 compatible, replaces RPi.GPIO
pigpioPython/CLowNo (daemon-based)Sub-microsecond timing via daemon, Pi 4 and earlier
libgpiodC/PythonLowYesLinux kernel GPIO character device interface
WiringPiCLowUnofficial forkOriginal deprecated 2019, community fork exists

Digital Output with gpiozero

The gpiozero library is the official recommended approach for Python GPIO programming. It provides a device-oriented abstraction where you work with LEDs, buttons, motors, and sensors rather than raw pin numbers.

#!/usr/bin/env python3
"""LED blink example using gpiozero - works on Pi 3/4/5."""

from gpiozero import LED, Button, PWMLED
from time import sleep
from signal import pause

# Simple LED blink on GPIO 17
led = LED(17)

print("Blinking LED on GPIO 17...")
for i in range(10):
    led.on()
    sleep(0.5)
    led.off()
    sleep(0.5)

# PWM LED (fade in/out) on GPIO 18
pwm_led = PWMLED(18)

print("Fading LED on GPIO 18...")
for brightness in range(0, 101, 5):
    pwm_led.value = brightness / 100.0
    sleep(0.05)
for brightness in range(100, -1, -5):
    pwm_led.value = brightness / 100.0
    sleep(0.05)

# Button with callback on GPIO 27
button = Button(27, pull_up=True, bounce_time=0.05)

def on_press():
    print("Button pressed!")
    led.toggle()

def on_release():
    print("Button released!")

button.when_pressed = on_press
button.when_released = on_release

print("Waiting for button presses... (Ctrl+C to exit)")
pause()

Digital I/O with lgpio (Pi 5 Compatible)

For Pi 5, lgpio is the recommended low-level library. It accesses GPIO through the Linux /dev/gpiochip interface rather than direct memory mapping, which is necessary because the Pi 5's RP1 chip manages GPIO.

#!/usr/bin/env python3
"""GPIO programming with lgpio - Pi 5 compatible."""

import lgpio
import time

# Open gpiochip4 on Pi 5 (gpiochip0 on Pi 4 and earlier)
CHIP = 4  # Change to 0 for Pi 4/3/Zero

h = lgpio.gpiochip_open(CHIP)

# Configure GPIO 17 as output
LED_PIN = 17
lgpio.gpio_claim_output(h, LED_PIN)

# Configure GPIO 27 as input with pull-up
BUTTON_PIN = 27
lgpio.gpio_claim_input(h, BUTTON_PIN, lgpio.SET_PULL_UP)

# Blink LED
print("Blinking LED...")
for i in range(10):
    lgpio.gpio_write(h, LED_PIN, 1)
    time.sleep(0.3)
    lgpio.gpio_write(h, LED_PIN, 0)
    time.sleep(0.3)

# Read button state
print("Reading button (press Ctrl+C to stop)...")
try:
    while True:
        state = lgpio.gpio_read(h, BUTTON_PIN)
        if state == 0:  # Active low with pull-up
            print("Button pressed!")
            lgpio.gpio_write(h, LED_PIN, 1)
        else:
            lgpio.gpio_write(h, LED_PIN, 0)
        time.sleep(0.05)
except KeyboardInterrupt:
    pass
finally:
    lgpio.gpiochip_close(h)
    print("GPIO cleanup complete.")

Hardware PWM in C

For precise PWM control — servo motors, motor speed control, or LED dimming — C provides direct access to the hardware PWM peripheral. The Pi has two hardware PWM channels that can be mapped to specific GPIO pins.

/* hardware_pwm.c - Direct hardware PWM control via sysfs
 * Compile: gcc -o hardware_pwm hardware_pwm.c
 * Run: sudo ./hardware_pwm
 */

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

#define PWM_CHIP    "/sys/class/pwm/pwmchip0"
#define PWM_CHANNEL "0"  /* PWM0 = GPIO 18 */

static void write_to_file(const char *path, const char *value) {
    int fd = open(path, O_WRONLY);
    if (fd < 0) { perror(path); exit(1); }
    write(fd, value, strlen(value));
    close(fd);
}

int main(void) {
    char path[256];

    /* Export PWM channel */
    snprintf(path, sizeof(path), "%s/export", PWM_CHIP);
    write_to_file(path, PWM_CHANNEL);
    usleep(100000); /* Wait for sysfs to create files */

    /* Set period to 20ms (50Hz - standard servo) */
    snprintf(path, sizeof(path), "%s/pwm%s/period", PWM_CHIP, PWM_CHANNEL);
    write_to_file(path, "20000000"); /* 20ms in nanoseconds */

    /* Set duty cycle to 1.5ms (servo center position) */
    snprintf(path, sizeof(path), "%s/pwm%s/duty_cycle", PWM_CHIP, PWM_CHANNEL);
    write_to_file(path, "1500000"); /* 1.5ms in nanoseconds */

    /* Enable PWM */
    snprintf(path, sizeof(path), "%s/pwm%s/enable", PWM_CHIP, PWM_CHANNEL);
    write_to_file(path, "1");

    printf("PWM active on GPIO 18 (50Hz, 1.5ms pulse)\n");
    printf("Sweeping servo...\n");

    /* Sweep servo from 1ms to 2ms */
    for (int duty_us = 1000; duty_us <= 2000; duty_us += 50) {
        char duty_str[32];
        snprintf(duty_str, sizeof(duty_str), "%d", duty_us * 1000);
        snprintf(path, sizeof(path), "%s/pwm%s/duty_cycle",
                 PWM_CHIP, PWM_CHANNEL);
        write_to_file(path, duty_str);
        usleep(100000); /* 100ms between steps */
    }

    /* Disable and unexport */
    snprintf(path, sizeof(path), "%s/pwm%s/enable", PWM_CHIP, PWM_CHANNEL);
    write_to_file(path, "0");
    snprintf(path, sizeof(path), "%s/unexport", PWM_CHIP);
    write_to_file(path, PWM_CHANNEL);

    printf("PWM disabled.\n");
    return 0;
}

Interrupts and Edge Detection

Polling GPIO pins wastes CPU cycles. Edge-triggered interrupts allow the kernel to wake your program only when a pin changes state. The lgpio library supports callbacks on rising, falling, or both edges.

#!/usr/bin/env python3
"""GPIO interrupt handling with lgpio - efficient edge detection."""

import lgpio
import time

CHIP = 4  # Pi 5 (use 0 for Pi 4)
BUTTON_PIN = 27
LED_PIN = 17

h = lgpio.gpiochip_open(CHIP)
lgpio.gpio_claim_output(h, LED_PIN)
lgpio.gpio_claim_alert(h, BUTTON_PIN, lgpio.FALLING_EDGE, lgpio.SET_PULL_UP)

press_count = 0
last_press_time = 0

def button_callback(chip, gpio, level, timestamp):
    global press_count, last_press_time
    now = time.time()
    # Software debounce: ignore presses within 200ms
    if now - last_press_time < 0.2:
        return
    last_press_time = now
    press_count += 1
    print(f"Button press #{press_count} at {timestamp}")
    # Toggle LED
    current = lgpio.gpio_read(h, LED_PIN)
    lgpio.gpio_write(h, LED_PIN, 1 - current)

# Register callback
cb = lgpio.callback(h, BUTTON_PIN, lgpio.FALLING_EDGE, button_callback)

print("Interrupt-driven button handler active. Press Ctrl+C to stop.")
try:
    while True:
        time.sleep(1)
except KeyboardInterrupt:
    cb.cancel()
    lgpio.gpiochip_close(h)
    print(f"\nTotal presses: {press_count}")

SPI, I2C & UART Communication

The Raspberry Pi's serial interfaces — SPI, I2C, and UART — connect it to the vast ecosystem of embedded sensors, displays, memory chips, and communication modules. Each protocol has distinct characteristics that make it suited for different applications.

Enabling Interfaces

# Enable SPI, I2C, and serial via raspi-config
sudo raspi-config nonint do_spi 0      # Enable SPI
sudo raspi-config nonint do_i2c 0      # Enable I2C
sudo raspi-config nonint do_serial_hw 0 # Enable hardware UART

# Or edit /boot/firmware/config.txt directly (Pi 5)
# dtparam=spi=on
# dtparam=i2c_arm=on
# enable_uart=1

# Verify interfaces are active
ls /dev/spidev*   # Should show /dev/spidev0.0, /dev/spidev0.1
ls /dev/i2c*      # Should show /dev/i2c-1
ls /dev/ttyAMA*   # Should show /dev/ttyAMA0 (Pi 5) or /dev/ttyS0

# Install Python libraries
pip install spidev smbus2 pyserial

# Scan I2C bus for connected devices
sudo i2cdetect -y 1

I2C: Reading a BME280 Sensor

I2C (Inter-Integrated Circuit) is the most common protocol for connecting sensors to the Pi. It uses just two wires (SDA + SCL) and supports multiple devices on the same bus, each with a unique 7-bit address.

#!/usr/bin/env python3
"""BME280 temperature, pressure, humidity sensor via I2C.
Wiring: VCC->3.3V, GND->GND, SDA->GPIO2, SCL->GPIO3
"""

import smbus2
import time
import struct

BME280_ADDR = 0x76  # or 0x77 if SDO is pulled high

class BME280:
    def __init__(self, bus_num=1, addr=BME280_ADDR):
        self.bus = smbus2.SMBus(bus_num)
        self.addr = addr
        self._read_calibration()
        # Set oversampling: temp x2, pressure x16, humidity x1
        self.bus.write_byte_data(addr, 0xF2, 0x01)  # ctrl_hum
        self.bus.write_byte_data(addr, 0xF4, 0x57)  # ctrl_meas (normal mode)
        self.bus.write_byte_data(addr, 0xF5, 0xA0)  # config (1000ms standby)

    def _read_calibration(self):
        """Read factory calibration data from sensor registers."""
        cal1 = self.bus.read_i2c_block_data(self.addr, 0x88, 26)
        cal2 = self.bus.read_i2c_block_data(self.addr, 0xE1, 7)
        self.dig_T1 = struct.unpack_from('<H', bytes(cal1), 0)[0]
        self.dig_T2 = struct.unpack_from('<h', bytes(cal1), 2)[0]
        self.dig_T3 = struct.unpack_from('<h', bytes(cal1), 4)[0]
        self.dig_P1 = struct.unpack_from('<H', bytes(cal1), 6)[0]
        self.dig_P2 = struct.unpack_from('<h', bytes(cal1), 8)[0]
        self.dig_H1 = cal1[25]
        self.dig_H2 = struct.unpack_from('<h', bytes(cal2), 0)[0]
        self.dig_H3 = cal2[2]

    def read(self):
        """Read compensated temperature, pressure, humidity."""
        data = self.bus.read_i2c_block_data(self.addr, 0xF7, 8)
        raw_p = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4)
        raw_t = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4)
        raw_h = (data[6] << 8) | data[7]

        # Temperature compensation
        var1 = ((raw_t / 16384.0) - (self.dig_T1 / 1024.0)) * self.dig_T2
        var2 = (((raw_t / 131072.0) - (self.dig_T1 / 8192.0)) ** 2) * self.dig_T3
        t_fine = var1 + var2
        temperature = t_fine / 5120.0

        # Pressure compensation (simplified)
        var1 = t_fine / 2.0 - 64000.0
        var2 = var1 * var1 * self.dig_P2 / 32768.0 + var1 * self.dig_P2 * 2.0
        pressure = raw_p / (1048576.0 - self.dig_P1) if self.dig_P1 != 0 else 0
        pressure = max(pressure, 300.0)  # Sanity clamp

        return {
            'temperature': round(temperature, 2),
            'pressure': round(pressure, 2),
            'humidity': round(min(max(raw_h / 1024.0, 0), 100), 2)
        }

if __name__ == '__main__':
    sensor = BME280()
    print("BME280 sensor readings (Ctrl+C to stop):\n")
    while True:
        data = sensor.read()
        print(f"  Temp: {data['temperature']}C  "
              f"Press: {data['pressure']} hPa  "
              f"Humid: {data['humidity']}%")
        time.sleep(2)

SPI: Driving an MCP3008 ADC

SPI (Serial Peripheral Interface) is faster than I2C and commonly used for ADCs, DACs, displays, and high-speed sensors. The MCP3008 is a popular 10-bit ADC that provides 8 analog input channels — something the Pi lacks natively.

#!/usr/bin/env python3
"""MCP3008 10-bit ADC via SPI - 8 analog input channels.
Wiring: VDD/VREF->3.3V, AGND/DGND->GND,
        CLK->GPIO11(SCLK), DOUT->GPIO9(MISO),
        DIN->GPIO10(MOSI), CS->GPIO8(CE0)
"""

import spidev
import time

spi = spidev.SpiDev()
spi.open(0, 0)         # Bus 0, CE0
spi.max_speed_hz = 1350000  # 1.35 MHz (MCP3008 max at 3.3V)
spi.mode = 0           # CPOL=0, CPHA=0

def read_adc(channel):
    """Read single-ended value from MCP3008 channel (0-7).
    Returns 10-bit value (0-1023).
    """
    if channel < 0 or channel > 7:
        raise ValueError("Channel must be 0-7")
    # MCP3008 protocol: start bit, single/diff, channel bits
    cmd = [0x01, (0x08 | channel) << 4, 0x00]
    resp = spi.xfer2(cmd)
    # Extract 10-bit result from response
    value = ((resp[1] & 0x03) << 8) | resp[2]
    return value

def read_voltage(channel, vref=3.3):
    """Read voltage from MCP3008 channel."""
    return read_adc(channel) * vref / 1023.0

# Read all 8 channels
print("MCP3008 ADC Readings:")
print("-" * 40)
for ch in range(8):
    raw = read_adc(ch)
    voltage = read_voltage(ch)
    print(f"  CH{ch}: {raw:4d} ({voltage:.3f}V)")

# Continuous monitoring of channel 0 (e.g., potentiometer)
print("\nMonitoring CH0 (Ctrl+C to stop):")
try:
    while True:
        v = read_voltage(0)
        bar = '#' * int(v / 3.3 * 40)
        print(f"\r  CH0: {v:.3f}V [{bar:<40s}]", end='')
        time.sleep(0.1)
except KeyboardInterrupt:
    spi.close()
    print("\nDone.")

UART: Serial Communication

#!/usr/bin/env python3
"""UART serial communication - GPS module example.
Wiring: GPS TX -> GPIO 15 (RXD), GPS RX -> GPIO 14 (TXD)
"""

import serial
import time

# Open UART - /dev/ttyAMA0 on Pi 5, /dev/ttyS0 on Pi 3/4
ser = serial.Serial(
    port='/dev/ttyAMA0',
    baudrate=9600,
    parity=serial.PARITY_NONE,
    stopbits=serial.STOPBITS_ONE,
    bytesize=serial.EIGHTBITS,
    timeout=1
)

def parse_gpgga(sentence):
    """Parse NMEA GPGGA sentence for position data."""
    parts = sentence.split(',')
    if len(parts) < 15 or parts[0] != '$GPGGA':
        return None
    try:
        lat = float(parts[2][:2]) + float(parts[2][2:]) / 60
        if parts[3] == 'S': lat = -lat
        lon = float(parts[4][:3]) + float(parts[4][3:]) / 60
        if parts[5] == 'W': lon = -lon
        alt = float(parts[9]) if parts[9] else 0
        sats = int(parts[7]) if parts[7] else 0
        return {'lat': lat, 'lon': lon, 'alt': alt, 'satellites': sats}
    except (ValueError, IndexError):
        return None

print("Reading GPS data via UART (Ctrl+C to stop)...")
try:
    while True:
        line = ser.readline().decode('ascii', errors='replace').strip()
        if line.startswith('$GPGGA'):
            pos = parse_gpgga(line)
            if pos and pos['satellites'] > 0:
                print(f"  Lat: {pos['lat']:.6f}  Lon: {pos['lon']:.6f}  "
                      f"Alt: {pos['alt']:.1f}m  Sats: {pos['satellites']}")
except KeyboardInterrupt:
    ser.close()
    print("\nUART closed.")

Camera & Display Interfaces

The Raspberry Pi's CSI (Camera Serial Interface) and DSI (Display Serial Interface) connectors provide high-bandwidth, low-latency connections to camera modules and touchscreen displays. The Pi 5 adds a second CSI/DSI port, enabling dual-camera setups for stereo vision.

Camera Module with picamera2

The picamera2 library replaced the legacy picamera and provides a Python interface to the libcamera stack. It supports the official Camera Module 3 (12.3 MP, autofocus), Camera Module 2 (8 MP), and the HQ Camera (12.3 MP, interchangeable lenses).

#!/usr/bin/env python3
"""Camera capture and streaming with picamera2."""

from picamera2 import Picamera2, Preview
from libcamera import controls
import time
import numpy as np

picam2 = Picamera2()

# Configure for still capture (full resolution)
still_config = picam2.create_still_configuration(
    main={"size": (4056, 3040), "format": "RGB888"},
    controls={"AfMode": controls.AfModeEnum.Continuous}
)

# Configure for video/preview (lower resolution)
video_config = picam2.create_video_configuration(
    main={"size": (1920, 1080), "format": "RGB888"},
    controls={"FrameRate": 30}
)

# --- Still Capture ---
picam2.configure(still_config)
picam2.start()
time.sleep(2)  # Allow auto-exposure to settle

# Capture high-resolution still
picam2.capture_file("capture.jpg")
print("Saved capture.jpg (4056x3040)")

# Capture as numpy array for processing
array = picam2.capture_array()
print(f"Array shape: {array.shape}, dtype: {array.dtype}")
avg_brightness = np.mean(array)
print(f"Average brightness: {avg_brightness:.1f}")

picam2.stop()

# --- Video Recording ---
picam2.configure(video_config)
encoder = picam2.create_encoder("h264")
picam2.start_recording(encoder, "video.h264", duration=10)
print("Recording 10 seconds of 1080p video...")
time.sleep(10)
picam2.stop_recording()
print("Saved video.h264")

# --- Time-lapse ---
picam2.configure(still_config)
picam2.start()

print("Starting time-lapse (1 frame every 30 seconds)...")
for i in range(10):
    filename = f"timelapse_{i:04d}.jpg"
    picam2.capture_file(filename)
    print(f"  Captured {filename}")
    time.sleep(30)

picam2.stop()
print("Time-lapse complete.")

Framebuffer Display Output

For headless embedded projects, you can drive small SPI/I2C displays through the Linux framebuffer or directly via SPI. Common choices include 128x64 OLED displays (SSD1306 via I2C) and 320x240 or 480x320 TFT displays (ILI9341 via SPI).

#!/usr/bin/env python3
"""SSD1306 OLED display via I2C using Pillow + adafruit_ssd1306."""

import board
import busio
import adafruit_ssd1306
from PIL import Image, ImageDraw, ImageFont
import psutil
import time

# Initialize I2C and OLED
i2c = busio.I2C(board.SCL, board.SDA)
oled = adafruit_ssd1306.SSD1306_I2C(128, 64, i2c, addr=0x3C)

# Clear display
oled.fill(0)
oled.show()

font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 10)

def draw_system_stats():
    """Draw CPU, memory, temperature, and disk usage on OLED."""
    image = Image.new("1", (128, 64))
    draw = ImageDraw.Draw(image)

    cpu = psutil.cpu_percent(interval=0.5)
    mem = psutil.virtual_memory().percent
    disk = psutil.disk_usage('/').percent

    # Read CPU temperature
    with open('/sys/class/thermal/thermal_zone0/temp') as f:
        temp = float(f.read()) / 1000.0

    draw.text((0, 0),  f"CPU:  {cpu:5.1f}%", font=font, fill=255)
    draw.text((0, 14), f"Mem:  {mem:5.1f}%", font=font, fill=255)
    draw.text((0, 28), f"Disk: {disk:5.1f}%", font=font, fill=255)
    draw.text((0, 42), f"Temp: {temp:5.1f}C", font=font, fill=255)

    # CPU usage bar
    draw.rectangle((80, 2, 127, 10), outline=255)
    draw.rectangle((80, 2, 80 + int(cpu * 0.47), 10), fill=255)

    oled.image(image)
    oled.show()

print("Displaying system stats on OLED (Ctrl+C to stop)...")
try:
    while True:
        draw_system_stats()
        time.sleep(2)
except KeyboardInterrupt:
    oled.fill(0)
    oled.show()
    print("Display cleared.")
Key Insight: For computer vision projects, the Camera Module 3 with autofocus and HDR at $25 provides remarkably capable imaging. Combined with OpenCV or TensorFlow Lite on the Pi 5, you can build real-time object detection systems that process 15-30 fps at 1080p.

Linux Device Drivers on Pi

The Raspberry Pi runs a full Linux kernel, and its peripherals are managed through the Linux device driver framework. Understanding device trees, kernel modules, and the /dev filesystem gives you precise control over hardware that high-level libraries abstract away.

Device Tree Overlays

The device tree is a data structure that describes hardware to the Linux kernel. Instead of hardcoding peripheral addresses and configurations in the kernel, the device tree tells the kernel what hardware exists, where it is mapped, and which driver to use. Overlays let you modify the device tree at boot to enable or configure peripherals.

# List available overlays
ls /boot/firmware/overlays/*.dtbo | head -20

# View current device tree configuration
cat /boot/firmware/config.txt | grep -E "^dtoverlay|^dtparam"

# Common overlays for embedded projects
# Add to /boot/firmware/config.txt:

# Enable SPI with additional chip selects
dtoverlay=spi0-2cs

# Enable I2C with custom bus speed (400 kHz fast mode)
dtparam=i2c_arm=on
dtparam=i2c_arm_baudrate=400000

# Enable hardware PWM on GPIO 18 and 19
dtoverlay=pwm-2chan,pin=18,func=2,pin2=19,func2=2

# Enable a DS18B20 one-wire temperature sensor on GPIO 4
dtoverlay=w1-gpio,gpiopin=4

# Enable an additional UART on GPIO 0/1
dtoverlay=uart2

# Enable SPI1 with 3 chip selects
dtoverlay=spi1-3cs

# Verify device tree at runtime
dtc -I fs /proc/device-tree 2>/dev/null | head -50

# Check which overlays are loaded
vcdbg log msg 2>&1 | grep -i overlay

The /dev/gpiochip Interface

Modern Linux GPIO access uses the GPIO character device interface (/dev/gpiochipN) rather than the deprecated /sys/class/gpio sysfs interface. On the Pi 5, /dev/gpiochip4 controls the 40-pin header; on Pi 4 and earlier, it is /dev/gpiochip0.

# List available GPIO chips
gpiodetect
# Output on Pi 5:
# gpiochip0 [pinctrl-bcm2712] (32 lines)
# gpiochip4 [pinctrl-rp1]    (54 lines)  <-- 40-pin header

# Show GPIO line info
gpioinfo gpiochip4 | head -20

# Read a GPIO line (e.g., button on line 27)
gpioget gpiochip4 27

# Set a GPIO line (e.g., LED on line 17)
gpioset gpiochip4 17=1    # Turn on
gpioset gpiochip4 17=0    # Turn off

# Monitor GPIO events (edge detection)
gpiomon --falling-edge gpiochip4 27

Writing a Simple Kernel Module

/* gpio_led_module.c - Minimal kernel module to blink an LED.
 * Build: make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
 * Load:  sudo insmod gpio_led_module.ko
 * Remove: sudo rmmod gpio_led_module
 */

#include <linux/module.h>
#include <linux/gpio.h>
#include <linux/timer.h>

#define LED_GPIO 17
#define BLINK_PERIOD_MS 500

static struct timer_list blink_timer;
static bool led_state = false;

static void blink_callback(struct timer_list *t)
{
    led_state = !led_state;
    gpio_set_value(LED_GPIO, led_state);
    mod_timer(&blink_timer,
              jiffies + msecs_to_jiffies(BLINK_PERIOD_MS));
}

static int __init led_init(void)
{
    int ret;

    ret = gpio_request(LED_GPIO, "blink_led");
    if (ret) {
        pr_err("gpio_led: failed to request GPIO %d\n", LED_GPIO);
        return ret;
    }
    gpio_direction_output(LED_GPIO, 0);

    timer_setup(&blink_timer, blink_callback, 0);
    mod_timer(&blink_timer,
              jiffies + msecs_to_jiffies(BLINK_PERIOD_MS));

    pr_info("gpio_led: blinking LED on GPIO %d\n", LED_GPIO);
    return 0;
}

static void __exit led_exit(void)
{
    del_timer_sync(&blink_timer);
    gpio_set_value(LED_GPIO, 0);
    gpio_free(LED_GPIO);
    pr_info("gpio_led: module removed\n");
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Wasil Zafar");
MODULE_DESCRIPTION("GPIO LED blink kernel module");
Warning: Kernel modules run in kernel space with full hardware access. A bug in a kernel module can crash the entire system or corrupt the filesystem. Always test on a development Pi with no important data, and keep a backup SD card ready.

Bare-Metal Pi Programming

Bare-metal programming means running code directly on the processor without an operating system. On a Raspberry Pi, this involves writing to hardware registers directly, managing your own interrupt vector table, and bootstrapping the ARM core from the GPU bootloader. While impractical for most projects (use the Pi Pico instead), bare-metal Pi programming is an excellent educational exercise for understanding ARM architecture.

The Boot Process

The Raspberry Pi boot sequence is unique. Unlike most ARM boards where the CPU starts first, the Pi's VideoCore GPU boots first:

  1. GPU ROM loads bootcode.bin from the SD card's FAT32 partition
  2. bootcode.bin loads start.elf (the GPU firmware)
  3. start.elf reads config.txt for configuration parameters
  4. start.elf loads kernel8.img (or kernel.img) to memory address 0x80000 (Pi 3/4) or 0x0 (Pi 1/2)
  5. GPU releases the ARM CPU from reset, which begins executing at the kernel load address

Bare-Metal UART Output

/* bare_metal_uart.c - Bare-metal UART on Raspberry Pi 3/4.
 * Cross-compile: aarch64-none-elf-gcc -ffreestanding -nostdlib
 *                -o kernel8.img bare_metal_uart.c boot.S
 */

#include <stdint.h>

/* BCM2837 peripheral base (Pi 3) - use 0xFE000000 for Pi 4 */
#define MMIO_BASE       0x3F000000

/* GPIO registers */
#define GPFSEL1         (*(volatile uint32_t *)(MMIO_BASE + 0x00200004))
#define GPPUD           (*(volatile uint32_t *)(MMIO_BASE + 0x00200094))
#define GPPUDCLK0       (*(volatile uint32_t *)(MMIO_BASE + 0x00200098))

/* Mini UART (UART1) registers */
#define AUX_ENABLES     (*(volatile uint32_t *)(MMIO_BASE + 0x00215004))
#define AUX_MU_IO_REG   (*(volatile uint32_t *)(MMIO_BASE + 0x00215040))
#define AUX_MU_IER_REG  (*(volatile uint32_t *)(MMIO_BASE + 0x00215044))
#define AUX_MU_IIR_REG  (*(volatile uint32_t *)(MMIO_BASE + 0x00215048))
#define AUX_MU_LCR_REG  (*(volatile uint32_t *)(MMIO_BASE + 0x0021504C))
#define AUX_MU_MCR_REG  (*(volatile uint32_t *)(MMIO_BASE + 0x00215050))
#define AUX_MU_LSR_REG  (*(volatile uint32_t *)(MMIO_BASE + 0x00215054))
#define AUX_MU_CNTL_REG (*(volatile uint32_t *)(MMIO_BASE + 0x00215060))
#define AUX_MU_BAUD_REG (*(volatile uint32_t *)(MMIO_BASE + 0x00215068))

static void delay(uint32_t count) {
    while (count--) { asm volatile("nop"); }
}

void uart_init(void) {
    AUX_ENABLES = 1;        /* Enable mini UART */
    AUX_MU_CNTL_REG = 0;   /* Disable TX/RX during config */
    AUX_MU_IER_REG = 0;    /* Disable interrupts */
    AUX_MU_LCR_REG = 3;    /* 8-bit mode */
    AUX_MU_MCR_REG = 0;    /* RTS high */
    AUX_MU_BAUD_REG = 270;  /* 115200 baud at 250 MHz */

    /* Map GPIO 14/15 to alt5 (mini UART) */
    uint32_t ra = GPFSEL1;
    ra &= ~(7 << 12);     /* Clear GPIO 14 */
    ra |= (2 << 12);       /* Alt5 for GPIO 14 */
    ra &= ~(7 << 15);     /* Clear GPIO 15 */
    ra |= (2 << 15);       /* Alt5 for GPIO 15 */
    GPFSEL1 = ra;

    /* Disable pull-up/down for GPIO 14, 15 */
    GPPUD = 0;
    delay(150);
    GPPUDCLK0 = (1 << 14) | (1 << 15);
    delay(150);
    GPPUDCLK0 = 0;

    AUX_MU_CNTL_REG = 3;   /* Enable TX and RX */
}

void uart_putc(char c) {
    while (!(AUX_MU_LSR_REG & 0x20)) {}  /* Wait for TX ready */
    AUX_MU_IO_REG = (uint32_t)c;
}

void uart_puts(const char *s) {
    while (*s) {
        if (*s == '\n') uart_putc('\r');
        uart_putc(*s++);
    }
}

/* Entry point called from boot.S */
void kernel_main(void) {
    uart_init();
    uart_puts("Hello from bare-metal Raspberry Pi!\n");
    uart_puts("No OS, no drivers, just raw hardware.\n");

    while (1) {
        uart_putc(AUX_MU_IO_REG & 0xFF);  /* Echo received chars */
    }
}
Key Insight: For real bare-metal and real-time applications, the Raspberry Pi Pico (RP2040) is far more practical than bare-metal on a Pi 4/5. The Pico boots in microseconds, has deterministic timing, costs $4, and its PIO state machines can bit-bang virtually any protocol at exact cycle-level precision.

IoT Project Walkthroughs

Project 1: Weather Station with BME280 + MQTT

This project combines a BME280 environmental sensor with MQTT messaging to create a wireless weather station that publishes data to a broker (e.g., Mosquitto) for dashboarding with Grafana or Home Assistant.

#!/usr/bin/env python3
"""IoT Weather Station - BME280 sensor + MQTT publishing.
Publishes temperature, humidity, and pressure every 60 seconds.
"""

import paho.mqtt.client as mqtt
import smbus2
import json
import time
import logging
from datetime import datetime

logging.basicConfig(level=logging.INFO,
                    format='%(asctime)s %(levelname)s %(message)s')

# MQTT Configuration
MQTT_BROKER = "192.168.1.100"
MQTT_PORT = 1883
MQTT_TOPIC = "home/weather/outdoor"
MQTT_CLIENT_ID = "pi-weather-station"

# Sensor Configuration
BME280_ADDR = 0x76
I2C_BUS = 1
PUBLISH_INTERVAL = 60  # seconds

class WeatherStation:
    def __init__(self):
        self.bus = smbus2.SMBus(I2C_BUS)
        self.client = mqtt.Client(client_id=MQTT_CLIENT_ID)
        self.client.on_connect = self._on_connect
        self.client.on_disconnect = self._on_disconnect
        self.client.will_set(
            f"{MQTT_TOPIC}/status", "offline", qos=1, retain=True
        )

    def _on_connect(self, client, userdata, flags, rc):
        logging.info(f"Connected to MQTT broker (rc={rc})")
        client.publish(f"{MQTT_TOPIC}/status", "online",
                       qos=1, retain=True)

    def _on_disconnect(self, client, userdata, rc):
        logging.warning(f"Disconnected from broker (rc={rc})")

    def read_sensor(self):
        """Read BME280 and return dict with readings."""
        # Force a measurement
        self.bus.write_byte_data(BME280_ADDR, 0xF4, 0x25)
        time.sleep(0.05)
        data = self.bus.read_i2c_block_data(BME280_ADDR, 0xF7, 8)
        raw_t = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4)
        raw_h = (data[6] << 8) | data[7]
        raw_p = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4)
        temp = raw_t / 5120.0 - 4.0  # Simplified compensation
        return {
            "temperature": round(temp, 1),
            "humidity": round(raw_h / 1024.0, 1),
            "pressure": round(raw_p / 25600.0 + 900, 1),
            "timestamp": datetime.utcnow().isoformat() + "Z",
            "station_id": MQTT_CLIENT_ID
        }

    def run(self):
        self.client.connect(MQTT_BROKER, MQTT_PORT, keepalive=120)
        self.client.loop_start()
        logging.info("Weather station started.")

        try:
            while True:
                reading = self.read_sensor()
                payload = json.dumps(reading)
                result = self.client.publish(
                    MQTT_TOPIC, payload, qos=1, retain=True
                )
                if result.rc == mqtt.MQTT_ERR_SUCCESS:
                    logging.info(f"Published: {reading['temperature']}C, "
                                 f"{reading['humidity']}%, "
                                 f"{reading['pressure']} hPa")
                else:
                    logging.error(f"Publish failed: {result.rc}")
                time.sleep(PUBLISH_INTERVAL)
        except KeyboardInterrupt:
            logging.info("Shutting down...")
        finally:
            self.client.publish(f"{MQTT_TOPIC}/status", "offline",
                                qos=1, retain=True)
            self.client.loop_stop()
            self.client.disconnect()

if __name__ == "__main__":
    station = WeatherStation()
    station.run()

Project 2: Security Camera with Motion Detection

#!/usr/bin/env python3
"""Motion-detecting security camera with email alerts.
Uses picamera2 + OpenCV for motion detection.
"""

from picamera2 import Picamera2
import cv2
import numpy as np
import time
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.image import MIMEImage
from datetime import datetime
import os

MOTION_THRESHOLD = 5000    # Minimum contour area to trigger
SENSITIVITY = 25           # Pixel difference threshold
SAVE_DIR = "/home/pi/security_captures"
COOLDOWN = 30              # Seconds between alerts

os.makedirs(SAVE_DIR, exist_ok=True)

picam2 = Picamera2()
config = picam2.create_video_configuration(
    main={"size": (1280, 720), "format": "RGB888"}
)
picam2.configure(config)
picam2.start()
time.sleep(2)

prev_frame = None
last_alert_time = 0
motion_count = 0

print("Security camera active. Monitoring for motion...")

try:
    while True:
        frame = picam2.capture_array()
        gray = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY)
        gray = cv2.GaussianBlur(gray, (21, 21), 0)

        if prev_frame is None:
            prev_frame = gray
            continue

        # Compute frame difference
        delta = cv2.absdiff(prev_frame, gray)
        thresh = cv2.threshold(delta, SENSITIVITY, 255,
                               cv2.THRESH_BINARY)[1]
        thresh = cv2.dilate(thresh, None, iterations=2)

        contours, _ = cv2.findContours(
            thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
        )

        motion_detected = False
        for c in contours:
            if cv2.contourArea(c) > MOTION_THRESHOLD:
                motion_detected = True
                (x, y, w, h) = cv2.boundingRect(c)
                cv2.rectangle(frame, (x, y), (x+w, y+h),
                              (0, 255, 0), 2)

        if motion_detected:
            now = time.time()
            if now - last_alert_time > COOLDOWN:
                motion_count += 1
                ts = datetime.now().strftime("%Y%m%d_%H%M%S")
                filename = f"{SAVE_DIR}/motion_{ts}.jpg"
                cv2.imwrite(filename,
                            cv2.cvtColor(frame, cv2.COLOR_RGB2BGR))
                print(f"[{ts}] Motion detected! Saved: {filename}")
                last_alert_time = now

        prev_frame = gray
        time.sleep(0.1)

except KeyboardInterrupt:
    picam2.stop()
    print(f"\nTotal motion events: {motion_count}")

Project 3: Home Automation Controller

{
    "homeassistant": true,
    "mqtt": {
        "broker": "192.168.1.100",
        "port": 1883,
        "base_topic": "home/pi_controller"
    },
    "relays": [
        {"name": "Living Room Light", "gpio": 17, "type": "toggle"},
        {"name": "Fan", "gpio": 27, "type": "toggle"},
        {"name": "Garage Door", "gpio": 22, "type": "momentary", "pulse_ms": 500}
    ],
    "sensors": [
        {"name": "Temperature", "type": "bme280", "i2c_addr": "0x76"},
        {"name": "Motion PIR", "type": "pir", "gpio": 4},
        {"name": "Door Contact", "type": "contact", "gpio": 5}
    ]
}
Key Insight: For production IoT deployments, combine a Raspberry Pi as the local gateway/hub running Home Assistant or Node-RED with ESP32 or Pi Pico W nodes as sensor endpoints. The Pi handles data aggregation, visualization, and cloud connectivity while the microcontrollers handle real-time sensor acquisition at a fraction of the power cost.

Performance & Power Management

Embedded applications often need to balance computational performance against thermal limits and power budgets. The Raspberry Pi provides several mechanisms for managing both.

CPU Governor and Frequency Scaling

# Check current CPU frequency and governor
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor

# Available governors
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors
# ondemand performance powersave conservative schedutil

# Set performance governor (max clock always)
echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor

# Set conservative governor (slowly ramp up)
echo conservative | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor

# Check thermal throttling status
vcgencmd measure_temp       # Current temperature
vcgencmd get_throttled      # Throttle flags (0x0 = no throttling)
# Bit 0: under-voltage detected
# Bit 1: ARM frequency capped
# Bit 2: currently throttled
# Bit 3: soft temperature limit active

# Monitor temperature and frequency in real-time
watch -n 1 'echo "Temp: $(vcgencmd measure_temp) | Freq: $(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq) kHz | Throttle: $(vcgencmd get_throttled)"'

Power Budget Reference

Configuration Idle Power Load Power Notes
Pi 5 (no peripherals)3.5W12WActive cooling recommended
Pi 5 + NVMe + Camera5W15-18W27W PD supply required
Pi 4 (no peripherals)2.7W6.4WPassive heatsink sufficient for light loads
Pi Zero 2 W0.4W1.8WGood for battery projects
Pi Pico0.005W0.15WExcellent for battery/solar
Pi Pico (dormant)0.0006WN/ARTC wake with 1 year on 2xAA

For battery-powered projects, disable unused interfaces to reduce power consumption:

# Disable HDMI (saves ~25mA on Pi 4)
sudo tvservice -o

# Disable onboard LEDs
echo 0 | sudo tee /sys/class/leds/ACT/brightness
echo none | sudo tee /sys/class/leds/ACT/trigger

# Disable WiFi and Bluetooth
sudo rfkill block wifi
sudo rfkill block bluetooth

# Disable USB controller (Pi 4 - saves ~100mA)
echo '1-1' | sudo tee /sys/bus/usb/drivers/usb/unbind

# Reduce CPU frequency
echo 600000 | sudo tee /sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq

Case Studies

Case Study 1: NASA JPL Ingenuity Mars Helicopter

Perhaps the most extraordinary Raspberry Pi deployment is aboard NASA's Ingenuity helicopter, which flew on Mars starting April 19, 2021. While the flight computer uses a Qualcomm Snapdragon processor, the navigation camera and image processing pipeline rely on a Linux-based compute element built on the same architecture principles as the Raspberry Pi ecosystem — specifically, the use of standard Linux, Python scripts, and open-source computer vision libraries on ARM hardware.

The JPL team chose to use commercial off-the-shelf (COTS) ARM processors running Linux because the development velocity was dramatically higher than traditional radiation-hardened space computers. They managed radiation exposure through software redundancy, watchdog timers, and ECC memory rather than specialized hardware. The Ingenuity project demonstrated that the embedded Linux + ARM approach — exactly what Raspberry Pi popularized — is viable even for interplanetary missions.

Case Study 2: Industrial Pi Deployments at Festo and Siemens

Festo, the German industrial automation company, developed the Festo CPX-IoT Gateway powered by a Raspberry Pi Compute Module. It bridges OT (operational technology) networks running Profinet, EtherNet/IP, and IO-Link to IT cloud platforms. In production facilities, it collects real-time sensor data from pneumatic actuators and predictive maintenance sensors, processes it locally, and forwards aggregated insights to Festo's cloud analytics dashboard.

Siemens uses Raspberry Pi-class hardware in its SIMATIC IOT2050 gateway, which runs a TI AM6548 (similar ARM Cortex-A53 architecture). However, many Siemens partners prototype their industrial IoT solutions on standard Raspberry Pis before porting to the IOT2050 for production. The software compatibility between the Pi's Debian-based OS and the IOT2050's Debian-based OS makes this transition straightforward.

Case Study 3: Oracle Pi Supercomputer

At Oracle OpenWorld 2019, Oracle demonstrated a Raspberry Pi supercomputer cluster consisting of 1,060 Raspberry Pi 3B+ boards. The cluster ran Oracle Autonomous Linux and was designed to demonstrate the scalability of Oracle's cloud-native infrastructure stack. Each Pi ran a Kubernetes node, collectively forming a cluster that could schedule and manage containerized workloads.

While primarily a demonstration piece, Pi clusters have genuine educational and research value. Universities like the University of Southampton (where Professor Simon Cox built one of the first documented Pi clusters with his son in 2012 using 64 Pi Model Bs and Lego cases) use them to teach distributed computing, MPI programming, and cluster management at a fraction of the cost of traditional server hardware.

Key Insight: The Raspberry Pi's real strength in industry is not as a production platform (use a Compute Module or industrial gateway for that) but as the world's fastest path from idea to working prototype. A team that can demonstrate a working sensor fusion pipeline on a Pi in a week will win the budget to build the production version.

Exercises

Exercise 1 Beginner

LED Traffic Light Controller

Build a traffic light simulator using three LEDs (red, yellow, green) connected to GPIO pins. Program the standard traffic light sequence: green (30s) -> yellow (5s) -> red (30s) -> green. Add a pedestrian button on a fourth GPIO that triggers an interrupt to change the light to red after completing the current green phase. Use gpiozero for the implementation, then rewrite it using lgpio for Pi 5 compatibility.

gpiozero GPIO Interrupts State Machine
Exercise 2 Intermediate

I2C Multi-Sensor Data Logger

Connect a BME280 (temperature/pressure/humidity) and an MPU6050 (accelerometer/gyroscope) to the I2C bus. Write a Python program that reads both sensors at 10 Hz, stores data in a SQLite database, and serves a real-time web dashboard using Flask with Chart.js graphs. Implement proper I2C error handling with retry logic and bus recovery. Add MQTT publishing so Home Assistant can subscribe to the sensor feeds.

I2C BME280 MPU6050 SQLite Flask MQTT
Exercise 3 Advanced

Computer Vision Object Counter

Build a real-time object counting system using the Camera Module 3 and TensorFlow Lite on a Pi 5. Load a pre-trained MobileNet SSD model and count specific objects (e.g., people, cars, or packages) passing through a defined region of interest in the camera frame. Implement a line-crossing algorithm to count objects moving in each direction. Log counts to InfluxDB and display on a Grafana dashboard. The system should handle at least 15 fps at 720p resolution and survive network disconnections by buffering data locally.

picamera2 TensorFlow Lite OpenCV InfluxDB Grafana Edge AI

Raspberry Pi Project Plan Generator

Use this tool to document your Raspberry Pi embedded project plan — board selection, GPIO assignments, peripheral connections, operating system, and power requirements. Download as Word, Excel, PDF, or PowerPoint for project documentation, team handoff, or hardware procurement.

Raspberry Pi Project Plan Generator

Document your Raspberry Pi project configuration for review and export. All data stays in your browser — nothing is sent to any server.

Draft auto-saved

All data stays in your browser. Nothing is sent to or stored on any server.

Conclusion & Resources

The Raspberry Pi has transformed embedded prototyping from a specialized discipline requiring expensive development kits and proprietary tools into an accessible field where anyone with a $35 board and a text editor can build sophisticated embedded systems. From GPIO basics to Linux device drivers, from camera-based computer vision to MQTT-connected IoT networks, the Pi provides a complete platform for learning and building.

Key Takeaways:
  • Choose the right Pi model — Pi 5 for compute-heavy tasks, Pi Zero 2 W for compact IoT, Pi Pico for real-time and battery-powered
  • Use gpiozero/lgpio for Python GPIO programming (Pi 5 requires lgpio or gpiozero with lgpio backend)
  • I2C, SPI, and UART connect the Pi to thousands of sensors, displays, and communication modules
  • Device tree overlays configure hardware interfaces without recompiling the kernel
  • For production IoT, combine Pi as gateway with ESP32/Pico as sensor endpoints
  • Always account for power budgets and thermal management in embedded designs

Further Resources

Technology