Raspberry Pi
& IoT
GPIO, protocoles I2C/SPI/UART, capteurs courants, projets Python et stratégies de test pour systèmes embarqués — du câblage au déploiement.
Introduction au Raspberry Pi
Le Raspberry Pi est un nano-ordinateur ARM sous Linux, idéal pour l'IoT, la domotique, la robotique et les projets embarqués. On programme en Python grâce aux bibliothèques gpiozero, RPi.GPIO, smbus2, spidev et serial.
| Modèle | RAM | Usage typique |
|---|---|---|
| Pi Zero 2W | 512 Mo | Capteurs, IoT minimal |
| Pi 3B+ | 1 Go | Projets polyvalents |
| Pi 4B | 2–8 Go | Desktop, ML embarqué |
| Pi 5 | 4–8 Go | Hautes performances |
| Pi Pico (RP2040) | 264 Ko | Microcontrôleur, MicroPython |
Pour développer sans RPi physique, utiliser un mock matériel en Python — on simule GPIO, I2C, SPI avec unittest.mock. Toute la logique métier est testable sur PC.
Entrées/sorties numériques générales — LEDs, boutons, relais
Bus 2 fils — capteurs de température, accéléromètres, afficheurs
Bus 4 fils, rapide — ADC, mémoires flash, écrans
Série asynchrone — GPS, modules radio, Arduino
Installation & setup
# 1. Flash avec Raspberry Pi Imager
# → Raspberry Pi OS Lite (sans bureau) pour IoT
# → Configurer SSH + WiFi dans l'imager
# 2. Connexion SSH (première connexion)
ssh pi@raspberrypi.local
# ou avec IP : ssh pi@192.168.1.xx
# 3. Mise à jour
sudo apt update && sudo apt upgrade -y
# 4. Activer les interfaces (raspi-config)
sudo raspi-config
# → Interface Options → I2C → Enable
# → Interface Options → SPI → Enable
# → Interface Options → Serial Port → Enable
# Environnement virtuel recommandé
python3 -m venv venv
source venv/bin/activate
# GPIO
pip install gpiozero RPi.GPIO
# I2C
pip install smbus2 adafruit-circuitpython-busdevice
# SPI
pip install spidev
# UART / Serial
pip install pyserial
# Capteurs courants (Adafruit)
pip install adafruit-circuitpython-dht # DHT11/22
pip install adafruit-circuitpython-bmp280 # BMP280
pip install adafruit-circuitpython-mcp3xxx # MCP3008
# Vérifier I2C
sudo i2cdetect -y 1
GPIO — Brochage
Le Pi 4/5 dispose de 40 broches GPIO. Les numéros BCM (Broadcom) sont ceux utilisés en Python. Les numéros BOARD correspondent à la position physique.
Le RPi fonctionne en 3.3V logique. Ne jamais connecter directement un signal 5V sur une broche GPIO — utiliser un level shifter. Toujours limiter le courant sur GPIO avec des résistances (330Ω pour les LEDs).
| Numérotation | Description | Exemple |
|---|---|---|
| BCM (GPIO) | Numéro chip Broadcom — utilisé en Python | GPIO17 = broche 11 |
| BOARD | Position physique sur le connecteur | 11 = troisième col gauche |
pinout | Commande Linux pour afficher le brochage | pinout dans le terminal |
gpiozero & RPi.GPIO
gpiozero est la bibliothèque moderne recommandée — API haut niveau, objets intuitifs (LED, Button, MotionSensor…). Utiliser RPi.GPIO pour un contrôle bas niveau ou la compatibilité legacy.
from gpiozero import LED, Button
from signal import pause
led = LED(17) # GPIO17 = broche 11
bouton = Button(2) # GPIO2 = broche 3
# Allumer / éteindre
led.on()
led.off()
led.toggle()
led.blink(on_time=0.5, off_time=0.5)
# Réagir au bouton
bouton.when_pressed = led.on
bouton.when_released = led.off
pause() # attendre les événements
from gpiozero import PWMLED, DistanceSensor, MCP3008
# LED gradable (PWM)
led_pwm = PWMLED(18)
led_pwm.value = 0.5 # 50% intensité
# Capteur ultrasonique HC-SR04
capteur = DistanceSensor(echo=24, trigger=23)
print(f"Distance: {capteur.distance * 100:.1f} cm")
# ADC MCP3008 (via SPI)
adc = MCP3008(channel=0)
print(f"Tension: {adc.value * 3.3:.2f} V")
import RPi.GPIO as GPIO
import time
GPIO.setmode(GPIO.BCM) # numérotation BCM
GPIO.setup(17, GPIO.OUT) # sortie
GPIO.setup(27, GPIO.IN, pull_up_down=GPIO.PUD_UP) # entrée
try:
while True:
GPIO.output(17, GPIO.HIGH)
time.sleep(0.5)
GPIO.output(17, GPIO.LOW)
time.sleep(0.5)
finally:
GPIO.cleanup() # TOUJOURS appeler cleanup
I2C
I2C utilise 2 fils — SDA (données) et SCL (horloge). Chaque périphérique a une adresse unique (0x00–0x7F). Jusqu'à 127 appareils sur le même bus.
| RPi (BCM) | Broche | Périphérique | Couleur type |
|---|---|---|---|
| GPIO2 (SDA1) | 3 | SDA | Bleu |
| GPIO3 (SCL1) | 5 | SCL | Jaune |
| 3.3V | 1 | VCC | Rouge |
| GND | 6 | GND | Noir |
Toujours lancer sudo i2cdetect -y 1 pour vérifier l'adresse du périphérique avant de coder.
import smbus2
import bmp280
bus = smbus2.SMBus(1) # bus I2C 1
capteur = bmp280.BMP280(i2c_dev=bus)
# Lecture
temp = capteur.get_temperature() # °C
pression = capteur.get_pressure() # hPa
print(f"Temp: {temp:.1f}°C Pression: {pression:.1f} hPa")
import smbus2
bus = smbus2.SMBus(1)
ADDR = 0x76 # adresse BMP280
# Lire 1 octet depuis le registre 0xD0 (chip_id)
chip_id = bus.read_byte_data(ADDR, 0xD0)
print(f"Chip ID: {chip_id:#04x}") # 0x60 = BMP280
# Lire 2 octets depuis registre
data = bus.read_i2c_block_data(ADDR, 0xF7, 3)
print(data) # [msb, lsb, xlsb]
SPI
SPI utilise 4 fils — MOSI, MISO, SCLK et CE (chip enable). Plus rapide que I2C, mais chaque périphérique nécessite sa propre ligne CE.
| RPi (BCM) | Broche | Signal SPI |
|---|---|---|
| GPIO10 (MOSI) | 19 | MOSI — données vers périph. |
| GPIO9 (MISO) | 21 | MISO — données vers RPi |
| GPIO11 (SCLK) | 23 | Horloge |
| GPIO8 (CE0) | 24 | Chip Enable 0 |
| GPIO7 (CE1) | 26 | Chip Enable 1 |
import spidev
spi = spidev.SpiDev()
spi.open(0, 0) # bus 0, CE0
spi.max_speed_hz = 1350000
def lire_canal(canal: int) -> float:
"""Lit un canal ADC, retourne 0.0–1.0"""
assert 0 <= canal <= 7
r = spi.xfer2([1, (8 + canal) << 4, 0])
valeur = ((r[1] & 3) << 8) + r[2]
return valeur / 1023.0
# Lire potentiomètre sur canal 0
tension = lire_canal(0) * 3.3
print(f"Tension: {tension:.2f} V")
spi.close()
UART / Serial
| RPi (BCM) | Broche | Signal |
|---|---|---|
| GPIO14 (TXD) | 8 | Transmission (vers l'autre appareil RX) |
| GPIO15 (RXD) | 10 | Réception (depuis l'autre appareil TX) |
| GND | 6 | Masse commune obligatoire |
Le port UART principal (/dev/ttyAMA0) est utilisé par défaut pour la console. Désactiver la console série dans raspi-config → Interface Options → Serial Port (désactiver login shell, activer hardware).
import serial
import time
# Port UART du RPi
port = serial.Serial(
port='/dev/ttyS0', # ou /dev/ttyAMA0
baudrate=9600,
timeout=1.0
)
# Envoyer des données
port.write(b'Hello Arduino\r\n')
# Lire une ligne
ligne = port.readline().decode('utf-8').strip()
print(f"Reçu: {ligne}")
# Lire en continu
while True:
if port.in_waiting > 0:
data = port.readline().decode().strip()
print(f"GPS: {data}")
time.sleep(0.1)
port.close()
Capteurs courants
import adafruit_dht, board
dht = adafruit_dht.DHT22(board.D4)
print(dht.temperature, dht.humidity)import bmp280, smbus2
bus = smbus2.SMBus(1)
b = bmp280.BMP280(i2c_dev=bus)
print(b.get_temperature(), b.get_pressure())from gpiozero import DistanceSensor
s = DistanceSensor(echo=24, trigger=23)
print(f"{s.distance * 100:.1f} cm")from gpiozero import MCP3008
adc = MCP3008(channel=0)
print(f"V={adc.value * 3.3:.2f}V")import smbus2
bus = smbus2.SMBus(1)
# Lire accél X,Y,Z depuis 0x3B
data = bus.read_i2c_block_data(0x68, 0x3B, 6)from luma.oled.device import ssd1306
from luma.core.interface.serial import i2c
serial = i2c(port=1, address=0x3C)
device = ssd1306(serial)Projets types
# capteurs/dht22.py — couche d'abstraction
class DHT22Sensor:
def __init__(self, pin: int):
import adafruit_dht, board
self._dev = adafruit_dht.DHT22(
getattr(board, f'D{pin}')
)
def read(self) -> dict:
return {
'temperature': self._dev.temperature,
'humidity': self._dev.humidity,
}
# app.py — logique métier (testable)
class WeatherStation:
def __init__(self, sensor):
self.sensor = sensor # injection dépendance
def get_status(self) -> str:
data = self.sensor.read()
t = data['temperature']
h = data['humidity']
if t > 30: return "Chaud"
elif t < 10: return "Froid"
else: return "Tempéré"
import requests, time
def publier_mesure(temperature, humidite):
try:
r = requests.post(
'https://api.monserveur.com/mesures',
json={'temp': temperature, 'hum': humidite},
headers={'Authorization': 'Bearer TOKEN'},
timeout=5
)
r.raise_for_status()
except requests.RequestException as e:
print(f"Erreur réseau: {e}")
# MQTT avec paho-mqtt
import paho.mqtt.client as mqtt
client = mqtt.Client()
client.connect("broker.local", 1883)
client.publish("capteurs/temp", f"{temperature:.1f}")
Tests & mocks matériel
Le matériel physique ne peut pas être testé automatiquement sur PC. La solution : séparer la logique métier du matériel avec une couche d'abstraction, puis mocker cette couche dans les tests.
import unittest
from unittest.mock import MagicMock, patch
from app import WeatherStation
class TestWeatherStation(unittest.TestCase):
def test_chaud_au_dessus_30(self):
sensor_mock = MagicMock()
sensor_mock.read.return_value = {
'temperature': 35,
'humidity': 60
}
station = WeatherStation(sensor=sensor_mock)
self.assertEqual(station.get_status(), "Chaud")
def test_froid_en_dessous_10(self):
sensor_mock = MagicMock()
sensor_mock.read.return_value = {
'temperature': 5,
'humidity': 80
}
station = WeatherStation(sensor=sensor_mock)
self.assertEqual(station.get_status(), "Froid")
from unittest.mock import patch, MagicMock
class TestBMP280(unittest.TestCase):
@patch('smbus2.SMBus')
def test_lecture_temperature(self, mock_bus_class):
mock_bus = MagicMock()
mock_bus_class.return_value = mock_bus
# Simuler la réponse brute du registre
mock_bus.read_i2c_block_data.return_value = [
0x80, 0x00, 0x00 # données brutes BMP280
]
from mon_capteur import LectureBMP280
capteur = LectureBMP280(adresse=0x76)
result = capteur.lire()
# Vérifier que la lecture a bien été appelée
mock_bus.read_i2c_block_data.assert_called_once_with(
0x76, 0xF7, 3
)
Déploiement & service
[Unit]
Description=Station météo IoT
After=network.target
[Service]
Type=simple
User=pi
WorkingDirectory=/home/pi/station
ExecStart=/home/pi/station/venv/bin/python app.py
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
Restart=always redémarre automatiquement le service si le script plante (erreur capteur, coupure réseau…). Indispensable pour un système IoT autonome.
Cheat Sheet RPi / IoT
🔌 Broches essentielles
| GPIO2/3 | I2C SDA/SCL (broche 3/5) |
| GPIO10/9/11/8 | SPI MOSI/MISO/SCLK/CE0 |
| GPIO14/15 | UART TX/RX (broche 8/10) |
| GPIO18 | PWM matériel principal |
| Broche 1 | 3.3V (max 50mA total) |
| Broche 2 | 5V alimentation |
| Broches 6/9/14/20/25/30/34/39 | GND |
🐍 gpiozero — objets
LED(pin) | LED on/off/blink |
PWMLED(pin) | LED gradable 0–1 |
Button(pin) | Bouton when_pressed |
DistanceSensor(e,t) | HC-SR04 .distance |
MCP3008(ch) | ADC SPI .value (0–1) |
MotionSensor(pin) | PIR .motion_detected |
Buzzer(pin) | Buzzer .on/.beep() |
🔍 Diagnostic terminal
pinout | Brochage du modèle |
i2cdetect -y 1 | Adresses I2C détectées |
gpio readall | État de toutes les GPIO |
vcgencmd measure_temp | Température CPU |
ls /dev/spi* | Interfaces SPI dispos |
ls /dev/ttyS* | Ports série disponibles |
⚠️ Règles de sécurité
| 3.3V logique | Ne JAMAIS mettre 5V sur GPIO |
| Résistances | 330Ω minimum sur LEDs |
| GPIO.cleanup() | Toujours en fin de script |
| Level shifter | Pour signaux 5V ↔ 3.3V |
| Courant GPIO | Max 8mA par broche, 50mA total |