Embarqué · IoT · Python

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èleRAMUsage typique
Pi Zero 2W512 MoCapteurs, IoT minimal
Pi 3B+1 GoProjets polyvalents
Pi 4B2–8 GoDesktop, ML embarqué
Pi 54–8 GoHautes performances
Pi Pico (RP2040)264 KoMicrocontrô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.

GPIO

Entrées/sorties numériques générales — LEDs, boutons, relais

I2C

Bus 2 fils — capteurs de température, accéléromètres, afficheurs

SPI

Bus 4 fils, rapide — ADC, mémoires flash, écrans

UART

Série asynchrone — GPS, modules radio, Arduino

Installation & setup

Premier démarrage — Raspberry Pi OS
# 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
Installation des bibliothèques Python
# 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.

Fonction
L
R
Fonction
3V3
1
2
5V
SDA1 (GPIO2)
3
4
5V
SCL1 (GPIO3)
5
6
GND
GPIO4
7
8
TXD (GPIO14)
GND
9
10
RXD (GPIO15)
GPIO17
11
12
MOSI (GPIO10)
GPIO27
13
14
GND
GPIO22
15
16
CE0 (GPIO8)
3.3V / 5V GND GPIO I2C SPI UART
⚠️

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érotationDescriptionExemple
BCM (GPIO)Numéro chip Broadcom — utilisé en PythonGPIO17 = broche 11
BOARDPosition physique sur le connecteur11 = troisième col gauche
pinoutCommande Linux pour afficher le brochagepinout dans le terminal
gpio info
pi@rpi:~ $ pinout
# Affiche le brochage complet du modèle
pi@rpi:~ $ gpio readall
# État de toutes les broches
pi@rpi:~ $ sudo i2cdetect -y 1
0 1 2 3 4 5 6 7
40: -- -- -- -- -- -- -- --
48: 48 -- -- -- -- -- -- --

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.

gpiozero — LED + bouton
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
gpiozero — PWM et capteurs
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")
RPi.GPIO — bas niveau
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)BrochePériphériqueCouleur type
GPIO2 (SDA1)3SDABleu
GPIO3 (SCL1)5SCLJaune
3.3V1VCCRouge
GND6GNDNoir
🔍

Toujours lancer sudo i2cdetect -y 1 pour vérifier l'adresse du périphérique avant de coder.

BMP280 — pression & température (I2C)
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")
smbus2 bas niveau — lecture registre
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)BrocheSignal SPI
GPIO10 (MOSI)19MOSI — données vers périph.
GPIO9 (MISO)21MISO — données vers RPi
GPIO11 (SCLK)23Horloge
GPIO8 (CE0)24Chip Enable 0
GPIO7 (CE1)26Chip Enable 1
MCP3008 — ADC 8 canaux (SPI)
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)BrocheSignal
GPIO14 (TXD)8Transmission (vers l'autre appareil RX)
GPIO15 (RXD)10Réception (depuis l'autre appareil TX)
GND6Masse 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).

pyserial — communication UART
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

DHT22
GPIO 1 fil
Température (−40→+80°C ±0.5°C) et humidité (0–100% ±2%). Lecture toutes les 2s minimum.
import adafruit_dht, board

dht = adafruit_dht.DHT22(board.D4)
print(dht.temperature, dht.humidity)
BMP280
I2C 0x76 / 0x77
Pression barométrique (300–1100 hPa), température, altitude calculée. Très précis.
import bmp280, smbus2
bus = smbus2.SMBus(1)
b = bmp280.BMP280(i2c_dev=bus)
print(b.get_temperature(), b.get_pressure())
HC-SR04
GPIO Trigger + Echo
Distance ultrasonique 2–400 cm. Echo en 5V → level shifter vers GPIO 3.3V requis.
from gpiozero import DistanceSensor
s = DistanceSensor(echo=24, trigger=23)
print(f"{s.distance * 100:.1f} cm")
MCP3008
SPI CE0/CE1
ADC 8 canaux 10 bits. Convertit des signaux analogiques (capteurs résistifs, photorésistances) en valeurs numériques.
from gpiozero import MCP3008
adc = MCP3008(channel=0)
print(f"V={adc.value * 3.3:.2f}V")
MPU-6050
I2C 0x68
Accéléromètre + gyroscope 6 axes. Idéal pour robotique, détection de mouvement, équilibrage.
import smbus2
bus = smbus2.SMBus(1)
# Lire accél X,Y,Z depuis 0x3B
data = bus.read_i2c_block_data(0x68, 0x3B, 6)
SSD1306 OLED
I2C 0x3C
Afficheur OLED 128×64 pixels. Bibliothèque Adafruit avec support texte, formes et images.
from luma.oled.device import ssd1306
from luma.core.interface.serial import i2c
serial = i2c(port=1, address=0x3C)
device = ssd1306(serial)

Projets types

Station météo — architecture recommandée
# 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é"
Envoi vers API / MQTT
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.

Mock d'un capteur DHT22
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")
Mock complet smbus2 (I2C)
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

/etc/systemd/system/station.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
systemctl
pi@rpi:~ $ sudo systemctl enable station
Created symlink /etc/systemd/system/multi-user.target.wants/station.service
pi@rpi:~ $ sudo systemctl start station
pi@rpi:~ $ sudo systemctl status station
● station.service - Station météo IoT
Loaded: loaded (/etc/systemd/system/station.service)
Active: active (running) since Thu 2025-03-06
pi@rpi:~ $ journalctl -fu station
# Suivi des logs en temps réel
🔄

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/3I2C SDA/SCL (broche 3/5)
GPIO10/9/11/8SPI MOSI/MISO/SCLK/CE0
GPIO14/15UART TX/RX (broche 8/10)
GPIO18PWM matériel principal
Broche 13.3V (max 50mA total)
Broche 25V alimentation
Broches 6/9/14/20/25/30/34/39GND

🐍 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

pinoutBrochage du modèle
i2cdetect -y 1Adresses I2C détectées
gpio readallÉtat de toutes les GPIO
vcgencmd measure_tempTempérature CPU
ls /dev/spi*Interfaces SPI dispos
ls /dev/ttyS*Ports série disponibles

⚠️ Règles de sécurité

3.3V logiqueNe JAMAIS mettre 5V sur GPIO
Résistances330Ω minimum sur LEDs
GPIO.cleanup()Toujours en fin de script
Level shifterPour signaux 5V ↔ 3.3V
Courant GPIOMax 8mA par broche, 50mA total