Ce guide suppose que vous n'avez jamais monté de quad et que vous en voulez un qui vole, puis qui réfléchit. Il est écrit dans l'ordre de construction, pas dans l'ordre théorique : rien n'apparaît avant que vous en ayez besoin, et chaque étape nomme l'erreur qu'elle sert à éviter.
Le crédit revient à ceux qui l'ont mérité. La moitié châssis suit la méthode sur laquelle la communauté FPV a convergé — les carnets de build d'Oscar Liang pour le câblage et l'ordre d'assemblage, les tutoriels Betaflight de Joshua Bardwell pour la configuration et le dépannage, Painless360 pour tout ce qui touche à ArduPilot. Ce sont mes sources d'apprentissage et celles à regarder si vous préférez la vidéo. Ce que j'ajoute, c'est la partie qu'ils ne couvrent pas : rendre l'aéronef autonome, et faire tourner un détecteur à bord.
Deux builds en un. Les parties A–B donnent un quad 5 pouces FPV classique, piloté à la radio. Les parties C–D en font une plateforme de reconnaissance. Vous pouvez vous arrêter après la partie B et avoir un drone qui vole — c'est le bon jalon, et le sauter est la façon dont les projets finissent en tas de pièces et dépôt GitHub.
La décision qui conditionne chaque achat
Betaflight ou ArduPilot ? Ce n'est pas une préférence, c'est ce qui rend le projet possible ou non. Betaflight est un excellent firmware de vol manuel, sans vraie couche de mission autonome. ArduPilot donne le mode GUIDED, le maintien de position, l'upload de mission et une API MAVLink — exactement ce dont un ordinateur compagnon a besoin.
| Objectif | Firmware | Matériel en plus |
|---|---|---|
| FPV manuel, tournage | Betaflight 4.x | VTX + caméra FPV ou air unit numérique |
| Relevé autonome + détection | ArduPilot Copter 4.6+ | GPS + boussole, Pi 5, liaison télémétrie |
| Les deux | ArduPilot (il vole très bien à la main) | GPS, Pi 5, liaison RC |
Outils, et les trois non négociables
Une LiPo, c'est un incendie tenu poliment. Jamais de charge sans surveillance, jamais de perforation, jamais de soudure pack branché. Une 6S 1500 mAh débite plus de 100 A dans un court-circuit franc — le smoke stopper existe pour que cette erreur coûte 10 € au lieu de votre build.
A1 · Assemblage du châssis
Étalez toutes les pièces et recomptez-les avec la notice avant de toucher une vis. Les châssis existent en variantes avec des bras et des entretoises différents, et la notice est la seule chose qui dit quelle vis va où.
Plaque inférieure d'abord
Fixez les quatre bras avec deux vis chacun, frein filet, serrage régulier. Sur la plupart des châssis 5 pouces les bras sont pris en sandwich : ne serrez pas à fond avant d'avoir posé les quatre.
Le sens de rotation compte plus que la position
Configuration « props-in » standard : moteurs 1 et 3 tournent en sens horaire vu de dessus, 2 et 4 en antihoraire. Vous le réglerez dans le firmware ; se tromper ici ne coûte qu'une hélice.
Entretoises
25 mm est la hauteur courante pour une pile qui doit recevoir un FC, un ESC puis un Pi 5 au-dessus. Mesurez votre carte la plus haute plus les connecteurs avant de figer.
Plaques caméra et plaque supérieure en dernier
Laissez la plaque du haut de côté. Vous rouvrirez ce châssis au moins deux fois.
A2 · Moteurs, et la vis qui les tue
La façon la plus courante de détruire un moteur, c'est une vis 2 mm trop longue. Elle traverse le bras, la base du moteur, et atteint le bobinage du stator — où tout semble parfait et fonctionne pendant un vol.
Montez les quatre moteurs sur les bras avant de monter les bras
Beaucoup plus facile d'accéder aux vis. Je le sais pour l'avoir fait dans l'autre sens.
À la main, puis au couple
Démarrez chaque vis à la main. Si ça force, arrêtez — vous êtes de travers ou trop long. Faites tourner la cloche à la main après la dernière vis ; le moindre frottement se corrige maintenant.
Pas d'hélices
Les hélices se montent après le test firmware, pas une minute avant.
A3 · Chaîne d'énergie et pads
Soudez dans l'ordre ci-dessous. Chaque étape est testable avant la suivante, c'est tout l'intérêt : vous voulez l'échec le moins cher possible.
XT60 + condensateur
Étamez les pads, puis le fil, puis assemblez avec une panne large à 400 °C. Pattes de condensateur courtes, soudé entre BAT+ et BAT− : il absorbe le pic de commutation qui arrive sinon dans votre vidéo et dans votre Pi.
Contrôle de continuité avant tout le reste
Multimètre en continuité entre BAT+ et BAT− : ça doit être ouvert. Un bip signifie un court-circuit, et vous venez d'épargner un ESC.
Pads de phases moteurs
Trois fils par moteur, ordre indifférent pour un moteur seul — inverser deux fils inverse le sens. Ce qui compte, c'est que les groupes M1…M4 correspondent aux sorties du FC.
Première mise sous tension avec le smoke stopper
Sans hélices, FC non connecté. Si le stopper déclenche, arrêtez et cherchez le court-circuit. S'il passe, vous avez un étage de puissance vivant et un ESC qui fonctionne.
Le brownout qui tue les projets : le Pi 5 veut 5 V jusqu'à 5 A et ne pardonne aucun affaissement. Le BEC intégré de l'ESC s'écroule dès que les moteurs montent en régime. Utilisez un UBEC 5 V/5 A dédié alimenté depuis la batterie, et prouvez-le : montez les gaz au banc en surveillant vcgencmd measure_volts. Sous ~4,85 V, corrigez avant de voler.
A4 · Monter la pile
Supports souples sous le FC
Gommettes silicone sur un entraxe 30,5×30,5. Elles isolent le gyro de la résonance du châssis — sans elles, votre boucle PID combat des vibrations qu'elle ne voit pas.
Flèche vers l'avant
Chaque FC a une flèche d'orientation. Montez-la vers l'avant et croyez-la. Si la carte est physiquement tournée, corrigez-le en logiciel plus tard — ne compensez pas mentalement, vous oublierez.
Nappe FC ↔ ESC
Connecteur sur la plupart des piles. Vérifiez le sens du brochage deux fois ; l'inverser tue le FC.
Gardez la place au-dessus pour le Pi
Environ 45 mm de hauteur de pile pour FC + ESC + Pi 5 + ventilateur. Prévoyez les sorties de câbles maintenant, pas une fois tout vissé.
A5 · Récepteur, GPS, vidéo
- Récepteur ELRS — soudez sur un UART libre, bindez avant de le cacher sous une plaque. Binder après assemblage est un hobby à part entière.
- GPS + boussole — sur un mât, le plus loin possible du VTX et du Pi. L'interférence de boussole est la cause n°1 du « toilet-bowling » en maintien de position. Calibration en extérieur, loin du métal et des aimants.
- VTX — alimenté depuis un rail 5 V/9 V filtré, jamais depuis le Pi. Antenne branchée avant la mise sous tension, à chaque fois.
- Caméra — pour le build IA, sautez la caméra analogique et montez la Camera Module 3 sur une plaque amortie. Deux caméras, c'est du poids inutile.
B1 · La séquence de flash
B2 · Voie Betaflight (vol manuel)
Passer en mode DFU / bootloader
Maintenir le bouton BOOT en branchant l'USB, ou pont les pads de boot. La carte apparaît comme un périphérique STM32, pas comme un port série. Batterie débranchée pendant toute l'opération.
Installer le pilote (Windows uniquement)
ImpulseRC Driver Fixer est la voie en un clic ; Zadig avec WinUSB la voie manuelle. Linux et macOS n'ont besoin de rien. Si le configurateur dit « no serial port », c'est la raison 9 fois sur 10.
Flasher la cible exacte
Choisissez le nom de carte imprimé sur le FC — pas un nom proche. Full chip erase sur une carte neuve ; jamais sur un build existant sous peine de refaire calibration et PID.
Ports et récepteur
Activez Serial RX sur l'UART du récepteur ELRS, puis Receiver Mode sur Serial et le fournisseur sur CRSF dans l'onglet Receiver.
Sens des moteurs
BLHeli Configurator ou l'onglet Motors : vérifiez que chaque moteur tourne dans le sens attendu par les hélices. Inversez en logiciel, pas en ressoudant sauf obligation.
Failsafe et modes
Un interrupteur pour l'armement, un pour le beeper, et un failsafe stage 2 qui fait tomber le quad plutôt que de le laisser partir. Testez en coupant la radio au banc, sans hélices.
B3 · Voie ArduPilot (autonomie)
C'est la voie de ce build : ArduPilot expose MAVLink, la langue que parle le Pi.
Flasher le .apj de votre carte
Le plus simple : Mission Planner ou QGroundControl installe le firmware par USB en un clic. Voie manuelle : télécharger le .apj sur firmware.ardupilot.org pour la carte exacte, puis flasher avec STM32CubeProgrammer en mode DFU.
Classe de châssis et ordre moteurs
Choisissez Quad X et suivez exactement l'ordre du test moteurs. C'est l'étape où une mauvaise réponse retourne l'appareil à l'armement.
Port série MAVLink
Réglez l'UART relié au Pi en MAVLink2 à 57600 bauds. Notez le numéro : le Pi sera configuré avec le même port et le même débit.
GPS, boussole, batterie
Calibration boussole en extérieur, moniteur de batterie avec les bonnes cellules et la bonne capacité, failsafe basse tension activé. Ne désactivez pas les vérifications de pré-armement pour « que ça vole » — chacune dit quelque chose.
Failsafe : perte RC → RTL
Trois secondes, retour au point de décollage, puis atterrissage. Testez au banc en coupant la radio.
GUIDED sur un interrupteur
Vous voulez un retour matériel au manuel à tout instant. C'est la fonction de sécurité la plus importante d'un aéronef autonome.
# Paramètres ArduPilot à fixer explicitement (relisez-les — les noms bougent entre versions)
PARAM_SET FRAME_CLASS 1 # 1 = Quad
PARAM_SET FRAME_TYPE 1 # 1 = X
PARAM_SET SERIAL2_PROTOCOL 2 # 2 = MAVLink2 (UART relié au Pi)
PARAM_SET SERIAL2_BAUD 57 # 57600
PARAM_SET ARMING_CHECK 1 # 1 = tous les contrôles. Jamais 0.
B4 · Prouver avant de sortir du banc
Porte de banc — ne sautez aucune ligne. Capteurs à niveau dans le HUD quand le châssis est à plat · ordre moteurs correct dans l'onglet de test, sans hélices · chaque moteur tourne librement à la main · failsafe déclenché quand on coupe la radio · verrou GPS ≥ 10 satellites en extérieur · offsets de boussole sains · tension batterie à 0,2 V près du multimètre.
C1 · Alimentation
UBEC séparé, masse séparée, 5 A disponibles. Sinon le Pi 5 brownout à l'accélération, et un brownout en vol ressemble exactement à un crash logiciel.
# Surveillez le rail en mettant les gaz au banc
watch -n 0.2 'vcgencmd measure_volts; vcgencmd measure_temp; vcgencmd get_throttled'
# 0x0 = propre. Tout le reste = sous-tension ou throttling thermique. À corriger avant de voler.
C2 · UART vers le contrôleur de vol
# /boot/firmware/config.txt — active l'UART primaire, libéré du bluetooth
enable_uart=1
dtoverlay=disable-bt
# /boot/firmware/cmdline.txt — supprimez ce jeton, sinon une console de login occupe le port
# console=serial0,115200
sudo reboot
ls -l /dev/serial0 # -> ttyAMA0
# Toute la liaison, testée en quatre lignes. Un heartbeat = câblage, débit et protocole corrects.
from pymavlink import mavutil
m = mavutil.mavlink_connection("/dev/serial0", baud=57600)
m.wait_heartbeat(timeout=10)
print("heartbeat from system", m.target_system)
Pas de heartbeat ? Dans cet ordre : inversez TX/RX (30 secondes), vérifiez une masse commune, vérifiez que console=serial0 a disparu de cmdline.txt, vérifiez que le port du FC est en MAVLink2 à 57600. L'ordre est délibéré : il est trié par fréquence de réponse.
C3 · Caméra
# Camera Module 3 sur CSI-0. Testez le capteur avant d'écrire du code d'inférence.
from picamera2 import Picamera2
import time
cam = Picamera2()
cam.configure(cam.create_still_configuration(main={"size": (2304, 1296)}))
cam.start(); time.sleep(2)
cam.capture_file("test.jpg") # ouvrez-la — mise au point et exposition se règlent ici
cam.stop()
Montez-la sur silicone ou mousse, pas vissée dans le carbone. Les vibrations ne floutent pas visiblement une image — elles rendent chaque image légèrement différente, ce qui ruine le tracker et donc la médiane de géolocalisation.
C4 · Thermique
Le Pi 5 throttle vers 80–85 °C, et dans une coque sans air il y arrive en six minutes. Ventilateur actif, une entrée d'air de 10 mm, et la température dans le journal de vol. Si le fps chute en vol, c'est presque toujours ça.
D1 · Quel modèle, sur quel matériel
| Modèle | Taille | Où il tourne | À utiliser quand |
|---|---|---|---|
| YOLOv8n / YOLO11n | ~6 Mo int8 | CPU Pi 5, 10–14 fps | Par défaut. Pas d'accélérateur, pas de pilote, pas de surprise. |
| YOLOv8s | ~22 Mo int8 | Hailo-8L / Hailo-8 | Petits objets à distance, avec un AI HAT. |
| YOLO11l | ~50 Mo | Hailo-8, ~21 fps NPU | Précision maximale sur un HAT 26 TOPS. |
| MobileNet-SSD | ~27 Mo | CPU Pi 5, plus rapide mais plus faible | Classes grossières à courte distance seulement. |
| RT-DETR | > 60 Mo | Bureau uniquement | Pas cet aéronef. Listé pour couper court. |
Un accélérateur ne vaut le coup qu'une fois la voie CPU fonctionnelle. Le Raspberry Pi AI HAT+ (Hailo-8L, 13 TOPS) utilise la voie PCIe et fait passer 12 fps à 60+ sur les petits modèles — mais ajoute une pile de pilotes, 15 g et 2–3 W. Faites d'abord la version CPU ; ajoutez le HAT quand c'est le modèle, pas la cadence, qui vous limite.
D2 · Affiner sur vos propres 500 images
Un détecteur COCO générique est mauvais sur exactement ce qui vous intéresse : un type de véhicule précis, un conteneur, un animal, du matériel sur un site. Cinq cents images annotées de votre caméra battent un jeu générique de 500 000 images, parce que le domaine correspond.
Collecter
Volez ou tenez la caméra aux altitudes et angles d'exploitation. Variez la lumière : matin, midi, couvert, fin d'après-midi. 500 à 1500 images est la plage utile.
Annoter
Roboflow, CVAT ou Label Studio. Une classe par chose sur laquelle vous devez agir, pas plus. Export au format YOLO avec le split train/val/test appliqué.
Entraîner
N'importe quel GPU, 100 époques, 640 px. Sur un Colab gratuit c'est une soirée ; sur un CPU portable c'est une semaine — louez le GPU.
Valider sur des vols, pas sur des images
C'est la précision sur vidéo tenue à l'écart qui compte. Un modèle à 0,85 mAP qui scintille entre images est pire qu'un 0,75 qui tient une piste.
pip install ultralytics
yolo detect train data=dataset/data.yaml model=yolo11n.pt epochs=100 imgsz=640 batch=16
yolo detect val model=runs/detect/train/weights/best.pt data=dataset/data.yaml
# Deux exports : ONNX pour le CPU, int8 pour l'edge
yolo export model=best.pt format=onnx imgsz=640 simplify=True
yolo export model=best.pt format=onnx imgsz=640 int8=True data=dataset/data.yaml
Quantifiez volontairement. L'int8 exige un jeu de calibration d'environ 300 images réelles, pas du bruit. Exportez en FP32, mesurez, puis int8, mesurez à nouveau. Sur ce Pi l'écart est 110–140 ms contre 70–95 ms par image — assez pour changer ce que l'aéronef peut faire.
D3 · La boucle d'inférence, complète
Trois threads. Le thread de capture ne bloque jamais sur l'inférence ; celui d'inférence ne bloque jamais sur la télémétrie. Les files « drop-oldest » gardent la latence honnête.
import queue, threading, time
from picamera2 import Picamera2
from ultralytics import YOLO
frames = queue.Queue(maxsize=2) # on jette l'ancienne : la latence prime
results = queue.Queue(maxsize=2)
def capture_loop():
cam = Picamera2()
cam.configure(cam.create_video_configuration(
main={"size": (1280, 720), "format": "RGB888"}, buffer_count=4))
cam.start()
while True:
buf = cam.capture_array("main")
if frames.full():
try: frames.get_nowait()
except queue.Empty: pass
frames.put(buf)
def inference_loop(model):
while True:
frame = frames.get()
r = model.predict(frame, imgsz=640, conf=0.35, verbose=False)[0]
dets = [{"box": b.xyxy[0].tolist(), "cls": model.names[int(b.cls)],
"conf": float(b.conf)} for b in r.boxes]
if results.full():
try: results.get_nowait()
except queue.Empty: pass
results.put((time.time(), frame, dets))
threading.Thread(target=capture_loop, daemon=True).start()
model = YOLO("models/recon_int8.pt")
threading.Thread(target=inference_loop, args=(model,), daemon=True).start()
Le suivi, pour que la sortie arrête de scintiller
def iou(a, b):
ax2, ay2 = a[0]+a[2], a[1]+a[3]
bx2, by2 = b[0]+b[2], b[1]+b[3]
ix = max(0, min(ax2,bx2) - max(a[0],b[0]))
iy = max(0, min(ay2,by2) - max(a[1],b[1]))
inter = ix*iy
return inter / (a[2]*a[3] + b[2]*b[3] - inter + 1e-9)
class Tracker:
def __init__(self, iou_thr=0.3, max_age=5):
self.tracks, self.iou_thr, self.max_age, self._id = {}, iou_thr, max_age, 0
def update(self, dets, t):
for tr in self.tracks.values(): tr["age"] += 1
for d in dets:
w, h = d["box"][2]-d["box"][0], d["box"][3]-d["box"][1]
box = (d["box"][0], d["box"][1], w, h)
best, score = None, self.iou_thr
for tid, tr in self.tracks.items():
if tr["label"] != d["cls"]: continue
s = iou(tr["box"], box)
if s > score: best, score = tid, s
if best is None:
self._id += 1; best = self._id
self.tracks[best] = {"label": d["cls"], "box": box, "age": 0, "first": t}
self.tracks[best].update(box=box, age=0, last=t, conf=d["conf"])
for tid in [k for k, v in self.tracks.items() if v["age"] > self.max_age]:
del self.tracks[tid]
return self.tracks
Du pixel à la coordonnée
Relèvement depuis les paramètres intrinsèques, rayon tourné dans le repère monde par l'attitude, intersection avec le plan du sol à l'altitude sol.
import numpy as np
def pixel_to_world(u, v, intr, R_body, R_cam, alt_m):
"""u,v = bas-centre de la boîte (le point de contact au sol).
R_body = rotation corps->ENU donnée par le pilote. alt_m = AGL, pas MSL."""
ray_c = np.array([(u - intr["cx"]) / intr["fx"],
(v - intr["cy"]) / intr["fy"], 1.0])
ray_w = R_body @ (R_cam @ ray_c)
ray_w /= np.linalg.norm(ray_w)
if ray_w[2] >= -1e-6: return None # horizon : pas d'intersection
t = -alt_m / ray_w[2]
if t < 0 or t > 500: return None
return (t * ray_w)[0], (t * ray_w)[1] # est, nord
def to_latlon(east, north, lat0, lon0):
R = 6378137.0
return (lat0 + np.degrees(north / R),
lon0 + np.degrees(east / (R * np.cos(np.radians(lat0)))))
Quatre choses décident d'une précision de 3 m ou de 30 : le bas-centre de la boîte, l'altitude AGL et non MSL, la calibration des intrinsèques au damier, et la médiane des dix dernières estimations pondérée par la confiance.
D4 · Lancer en service
# /etc/systemd/system/recon.service
[Unit]
Description=Recon perception service
After=network-online.target
[Service]
ExecStart=/home/pi/recon/bin/python /opt/recon/main.py
WorkingDirectory=/opt/recon
Restart=always
RestartSec=3
User=pi
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload && sudo systemctl enable --now recon
journalctl -u recon -f # suivez les détections en direct par SSH
E1 · Sept vols, dans cet ordre
Stationnaire, 30 secondes, sans charge
À 1 m, sans vent. Confirmez qu'il tient l'attitude et se pose proprement. Vérifiez le clipping de vibrations dans les logs.
Stationnaire avec le Pi en marche
Idem, inférence active. Surveillez le courant et la tension du Pi. Les brownouts apparaissent ici.
Maintien de position
Compensation du vent et santé de la boussole. S'il décrit des cercles, il y a interférence — posez et corrigez le mât.
GUIDED sans mouvement
Laissez le Pi envoyer un ordre de maintien. Rien ne doit bouger. Cela prouve que la liaison MAVLink est correcte dans un sens.
Petite translation commandée par le Pi
Deux mètres, et retour. Une main sur l'interrupteur de mode en permanence.
Détection à 20 m
Un objet connu, une altitude, mesurez l'écart entre la coordonnée rapportée et celle relevée.
Détection à l'altitude d'exploitation
Répétez à 40–50 m. L'erreur va grandir ; notez-la, c'est la résolution réelle de votre mission.
E2 · Contraintes légales et éthiques
Un drone qui reconnaît des personnes est un système de surveillance, et il est réglementé comme tel. Dans l'UE : catégorie ouverte EASA, sous 120 m, en vue directe, enregistrement opérateur si l'appareil porte une caméra, pas de survol de personnes ni de proximité d'aéroports. Le vol hors vue exige une autorisation que vous n'avez probablement pas. Au titre du RGPD, des images de personnes identifiables sont des données personnelles — si elles arrivent sur un serveur que vous exploitez, vous êtes responsable de traitement.
- Voler sur terrain privé avec autorisation, avec un observateur.
- Pas de détection de personnes sur le domaine public. Détectez ce pour quoi vous avez un motif légitime.
- Garder les pixels sur l'aéronef, avec une durée de conservation défendable.
- Journaliser chaque vol — zone, finalité, durée. Une réponse vaut mieux qu'un haussement d'épaules.
E3 · Liste de matériel
| Élément | Spec | Poids | ≈ € |
|---|---|---|---|
| Châssis | 5", 220 mm, bras 5 mm | 120 g | 55 |
| Moteurs ×4 | 2207 · 1960KV | 132 g | 80 |
| ESC | 4-en-1 · 45 A | 14 g | 55 |
| Contrôleur de vol | H743 · 7 UART | 10 g | 65 |
| Hélices | 5.1×4.6×3 | 24 g | 6 |
| RX ELRS | 2,4 GHz | 3 g | 20 |
| GPS + boussole | M10 | 13 g | 30 |
| VTX + antenne | 5,8 GHz 800 mW | 22 g | 35 |
| Pi 5 | 8 Go + ventilo + NVMe | 87 g | 135 |
| Camera Module 3 | IMX708 · 84° | 8 g | 35 |
| UBEC | 5 V / 5 A | 12 g | 12 |
| Modem 4G | USB + antennes | 38 g | 45 |
| Batterie | 6S 1500 mAh | 265 g | 45 |
| Divers | fils, supports, XT60 | 55 g | 25 |
Environ 640 € plus un chargeur et une radio. La meilleure amélioration du confort de vol est un meilleur chargeur ; la deuxième, un jeu de bras de rechange.
E4 · Ce qui a cassé sur le mien
- Throttling thermique à 6 minutes — canopée fermée, pas d'air, chute à 4 fps. Ventilateur actif + entrée d'air.
- Interférence de boussole — maintien de position instable ; mât GPS relevé de 40 mm et câble NVMe rerouté.
- Nappe CSI fissurée au connecteur après un atterrissage dur. Silicone sur le connecteur, serre-câble, et une nappe d'avance.
- Brownouts à la montée en gaz jusqu'à l'ajout de l'UBEC dédié.
- Un moteur détruit par une vis de 10 mm. Voir A2. 20 € et une soirée.