Every Raspberry Pi tutorial starts with blinking an LED and ends with a DHT11 temperature reading. That is the right starting point — but if you want to build real projects, you need to go deeper. Hardware PWM for precise motor control, SPI for high-speed displays and ADCs, I2C for sensor networks, and the RP2040’s programmable I/O for bit-banging custom protocols — these are the tools that separate functional projects from impressive ones.
This guide assumes you are comfortable with basic GPIO (digital read/write, simple sensor interfacing) and takes you through the advanced capabilities of the Raspberry Pi’s GPIO system with practical Python examples and real component hookups.
GPIO Architecture: Hardware Versus Software
The Raspberry Pi’s BCM283x/BCM2712 SoC exposes 40 GPIO pins, but not all pins are equal. Understanding the hardware capabilities of specific pins avoids frustrating bugs later.
Pin Numbering Systems
Three numbering systems coexist in Pi documentation, and confusing them is a constant source of errors:
- BCM (Broadcom) numbers: The GPIO chip’s internal numbering. Most Python libraries (RPi.GPIO, gpiozero, lgpio) use BCM by default. GPIO 18 = BCM pin 18.
- Physical (board) numbers: The literal pin position on the 40-pin header, 1–40. Pin 12 on the board = BCM GPIO 18.
- WiringPi numbers: A third scheme from the wiringPi library. Largely deprecated — avoid it for new projects.
Always specify your numbering scheme explicitly in code. In RPi.GPIO: GPIO.setmode(GPIO.BCM) or GPIO.setmode(GPIO.BOARD). In gpiozero, all pin numbers are BCM by default.
GPIO Electrical Characteristics
Pi GPIO operates at 3.3V logic — not 5V tolerant. Connecting a 5V signal directly to a Pi GPIO pin risks permanent damage. Always use a level shifter (e.g., TXS0108E) between 5V microcontrollers/sensors and the Pi. Maximum current per GPIO pin is 16 mA, total GPIO bank limit is 51 mA. Drive LEDs through resistors — never direct-connect without current limiting.
Hardware PWM: Precision Servo and Motor Control
Software PWM (bit-banged by the CPU) works for LEDs but fails for servos and ESCs because Linux’s multitasking introduces timing jitter that causes servo jitter. Hardware PWM uses dedicated silicon timers and is not affected by CPU load.
Raspberry Pi exposes hardware PWM on four channels:
- PWM0: GPIO 12 (physical pin 32) and GPIO 18 (physical pin 12)
- PWM1: GPIO 13 (physical pin 33) and GPIO 19 (physical pin 35)
Only one pin per channel can be active at a time (GPIO 12 OR GPIO 18 for PWM0). Enable hardware PWM via the device tree overlay in /boot/config.txt:
dtoverlay=pwm-2chan,pin=18,func=2,pin2=13,func2=4
Reboot, then control PWM via the sysfs interface or the pigpio library. The pigpio daemon provides the best hardware PWM interface in Python:
import pigpio
import time
pi = pigpio.pi()
# Set servo pulse on GPIO 18
# Pulse width 1000–2000 microseconds = 0–180 degrees
for angle in range(0, 181, 10):
pulse_width = 1000 + int(angle / 180 * 1000)
pi.set_servo_pulsewidth(18, pulse_width)
time.sleep(0.05)
pi.set_servo_pulsewidth(18, 0) # Stop PWM
pi.stop()
This produces jitter-free servo movement. For motor speed control (DC motor via L298N or DRV8833), use pi.hardware_PWM(18, 1000, 500000) — frequency 1 kHz, 50% duty cycle.
SPI Deep Dive: High-Speed Peripheral Communication
SPI (Serial Peripheral Interface) is a synchronous four-wire protocol capable of multi-megabit speeds. It is used by displays (ILI9341, ST7789, SSD1351), ADCs (MCP3008, ADS1256), DACs, SD card interfaces, and many sensors requiring fast data transfer.
SPI Pins on Raspberry Pi
- MOSI (Master Out Slave In): GPIO 10 (SPI0) / GPIO 20 (SPI1)
- MISO (Master In Slave Out): GPIO 9 (SPI0) / GPIO 19 (SPI1)
- SCLK (Clock): GPIO 11 (SPI0) / GPIO 21 (SPI1)
- CE0 (Chip Enable 0): GPIO 8 (SPI0) / GPIO 18 (SPI1)
- CE1 (Chip Enable 1): GPIO 7 (SPI0)
Enable SPI in raspi-config → Interface Options → SPI, or add dtparam=spi=on to /boot/config.txt.
Reading an MCP3008 ADC via SPI
The MCP3008 is an 8-channel 10-bit ADC that connects to the Pi via SPI, providing analogue inputs that the Pi’s GPIO lacks. This is essential for reading potentiometers, analogue sensors (LDR, soil moisture), and other non-digital signals.
import spidev
spi = spidev.SpiDev()
spi.open(0, 0) # Bus 0, device 0 (CE0)
spi.max_speed_hz = 1000000
spi.mode = 0
def read_channel(channel):
# MCP3008 single-ended read
adc = spi.xfer2([1, (8 + channel) << 4, 0])
data = ((adc[1] & 3) << 8) + adc[2]
return data
def read_voltage(channel, vref=3.3):
raw = read_channel(channel)
return (raw / 1023.0) * vref
print(f'CH0 voltage: {read_voltage(0):.3f}V')
spi.close()
Driving an SPI Display (ST7789)
The ST7789 240×240 colour LCD uses SPI at up to 80 MHz. Install the luma.lcd library and the Pillow image library, then you can draw text, shapes, and images to the display from Python. For the Raspberry Pi, the st7789 Python library (by Pimoroni) provides the simplest interface.
I2C Deep Dive: Building Sensor Networks
I2C (Inter-Integrated Circuit) uses just two wires — SDA (data) and SCL (clock) — to connect up to 128 devices on the same bus. This makes it ideal for sensor networks where you want multiple sensors without using many GPIO pins.
I2C Pins on Raspberry Pi
- I2C0: GPIO 0 (SDA) / GPIO 1 (SCL) — reserved for HAT identification, avoid for user devices
- I2C1: GPIO 2 (SDA) / GPIO 3 (SCL) — the standard user I2C bus
Enable I2C: raspi-config → Interface Options → I2C.
Scanning the I2C Bus
Always scan first to confirm your devices are detected and note their addresses:
sudo i2cdetect -y 1
This prints a grid showing which addresses have responding devices. Common addresses: SSD1306 OLED at 0x3C, BMP280 at 0x76 or 0x77, MPU6050 IMU at 0x68, PCF8574 I/O expander at 0x20–0x27.
Reading BMP280 Pressure and Temperature via I2C
import smbus2
import bmp280
bus = smbus2.SMBus(1) # I2C bus 1
sensor = bmp280.BMP280(i2c_dev=bus)
print(f'Temperature: {sensor.get_temperature():.2f}°C')
print(f'Pressure: {sensor.get_pressure():.2f} hPa')
print(f'Altitude: {sensor.get_altitude():.2f} m')
Install the library: pip3 install bmp280 smbus2. The BMP280 is excellent for weather stations and altitude measurement projects.
I2C Bus at Higher Speeds
The default I2C speed is 100 kHz. Many modern I2C devices support 400 kHz (Fast Mode) or even 1 MHz. Speed up the bus by adding to /boot/config.txt:
dtparam=i2c_arm_baudrate=400000
Higher speed matters when reading multiple sensors in a tight loop — a 10-sensor read at 100 kHz might take 10 ms; at 400 kHz it takes 2.5 ms. Always check your specific sensors’ maximum I2C speed before increasing — some older sensors only support 100 kHz.
Combining SPI and I2C in a Single Project
Real projects often need both buses simultaneously. For example: an I2C sensor network (BMP280 + SHT31 + MPU6050) feeding data to an SPI display (ST7789) with PWM-controlled RGB LED status indicators. This is entirely achievable on a single Pi — the hardware handles all three protocols independently.
A practical pattern for multi-peripheral projects:
- Initialise all buses in a setup function
- Use a main loop with a time budget — sensor reads on a 100 ms tick, display update on a 500 ms tick
- Handle I2C errors gracefully (some sensors time out) — wrap reads in try/except IOError
- Log data with timestamps to a CSV file for analysis
Advanced GPIO Techniques
Interrupt-Driven GPIO
Polling GPIO pins in a loop wastes CPU time. Use hardware interrupts instead — the GPIO hardware detects an edge (rising, falling, or both) and calls your callback function immediately:
import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM)
GPIO.setup(23, GPIO.IN, pull_up_down=GPIO.PUD_UP)
def button_callback(channel):
print(f'Button pressed on GPIO {channel}')
GPIO.add_event_detect(23, GPIO.FALLING,
callback=button_callback,
bouncetime=200) # 200ms debounce
input('Press Enter to quitn')
GPIO.cleanup()
GPIO with asyncio
For modern Python async projects, the gpiozero library integrates with asyncio through its when_pressed/when_released event model. This allows GPIO monitoring alongside async network operations (MQTT, HTTP server) in a single-threaded event loop.
Using lgpio for GPIO on Pi 5
The Raspberry Pi 5 uses a new RP1 I/O controller chip. The legacy RPi.GPIO library has compatibility issues on Pi 5. Use lgpio or gpiozero (which uses lgpio as its backend on Pi 5) for forward-compatible code:
pip3 install lgpio
Frequently Asked Questions
Can I use more than two I2C devices on the same bus?
Yes — I2C supports up to 128 devices on one bus (7-bit addressing). The practical limit is lower because most sensors come in only two or three address variants. If you need more identical sensors, use an I2C multiplexer like the TCA9548A, which provides 8 separate I2C bus segments switchable via the same two-wire interface.
What is the maximum SPI speed on Raspberry Pi?
The Pi’s SPI hardware supports up to 125 MHz theoretically, but practical limits depend on wire length, capacitance, and the slave device. For most displays and ADCs, 4–32 MHz is the working range. Long wires reduce the maximum safe speed significantly — keep SPI connections short (under 30 cm) for high speeds.
Why does my servo jitter even with hardware PWM?
Check that you are using the pigpio daemon-based approach rather than sysfs PWM or software PWM. Also verify your power supply — servos draw significant current and a weak USB supply causes voltage drops that affect the Pi’s GPIO output voltage and thus servo behaviour. Use a dedicated servo power supply and share only ground with the Pi.
Can I run SPI and I2C on the same GPIO pins?
No — SPI and I2C use different pin functions. SPI0 uses GPIO 7–11; I2C1 uses GPIO 2–3. They do not conflict. You can run both buses simultaneously. If you need more SPI devices, use separate CE (chip enable) lines — the Pi supports CE0 and CE1 on SPI0, and you can use any GPIO as a software CE for additional devices.
How do I handle 5V sensors with the Pi’s 3.3V GPIO?
Use a bidirectional logic level shifter module (TXS0108E or similar). For simple one-directional signals (sensor output → Pi input), a voltage divider (two resistors) works: a 10kΩ + 20kΩ divider on the signal line shifts 5V down to 3.3V. Never connect a 5V output directly to a Pi GPIO input.
Conclusion
Mastering hardware PWM, SPI, and I2C unlocks the full potential of Raspberry Pi GPIO for professional-grade electronics projects. Hardware PWM delivers jitter-free servo and motor control. SPI enables high-speed displays and precision ADCs. I2C makes it easy to connect a network of sensors with minimal wiring. Together, these three protocols cover the vast majority of real-world peripheral interfacing you will ever encounter.
The key to success is understanding which pins have hardware support for each function, choosing the right Python library for your Pi model (lgpio/gpiozero for Pi 5, RPi.GPIO or pigpio for older models), and handling errors gracefully so your long-running projects remain stable.
Find sensors, displays, and Raspberry Pi boards for your next GPIO project at Zbotic.in — India’s maker electronics store with fast delivery and expert technical support.
Add comment