Test Types & Methods

Monter les gaz en rampe sur un banc moteur

Apprenez à monter les gaz d'un moteur brushless en rampe sur banc avec du DShot piloté par microcontrôleur, des limites d'arrêt et un teardown fiable.

JJulien Buteau
intermediate10 min de lecture27 octobre 2026

Un test de moteur brushless qui saute d'un niveau de gaz au suivant produit des pics de courant, une surtension du bus à la décélération et des lectures qui dépendent du timing plutôt que du moteur. Apprenez à piloter les gaz en rampes depuis un microcontrôleur en DShot, à enregistrer gaz, courant et vitesse dans une seule courbe, à interrompre le test sur des limites, et à garantir l'arrêt des moteurs sur chaque chemin de sortie d'une procédure TofuPilot.

Pourquoi des rampes et pas des marches

Une marche de gaz demande au moteur de changer de vitesse instantanément. L'ESC répond avec le courant dont le moteur a besoin pour accélérer son rotor et son hélice, et à la descente il freine le moteur et renvoie l'énergie récupérée dans le bus.

Valeurs d'exemple mesurées sur un moteur de taille 3115, 1250 KV, avec un ESC 4-en-1 BLHeli_32 sur un bus 6S, sans hélice :

ProfilEffet observé
70 % de gaz constant2,65 A, 22 600 tr/min
Marches de gazPics de courant jusqu'à environ 10 A par moteur
Marche descendante depuis un gaz élevéBus porté à 36,6 V sur un bus 6S nominal (22,2 V), alimenté par une alimentation de banc à découpage

Les pics valent presque quatre fois le courant établi à 70 %, sans hélice. La surtension dépasse les 25,2 V d'une batterie 6S pleine, et une alimentation de banc à découpage ne peut pas absorber le courant renvoyé, donc rien ne retient le bus.

Les commandes de test moteur des contrôleurs de vol aggravent le problème. La commande MAV_CMD_DO_MOTOR_TEST d'ArduPilot fixe un gaz et une durée, et le gaz ne peut pas être modifié pendant un test. Un profil en marches devient donc une suite d'arrêts et de redémarrages, chacun étant une accélération complète depuis l'arrêt. Générer le DShot directement depuis un microcontrôleur retire le contrôleur de vol de la boucle et permet une vraie rampe.

Architecture du banc

Répartissez le travail pour que chaque élément fasse ce qu'il sait faire :

ÉlémentResponsabilité
PC du poste avec le CLI TofuPilotSéquence le test, envoie les consignes de rampe, enregistre les mesures, applique les limites
Microcontrôleur en série USBGénère le DShot300 en continu, exécute les rampes avec un timing déterministe, gère un watchdog, renvoie le gaz appliqué et l'eRPM
ESC 4-en-1Pilote les moteurs, renvoie l'eRPM (DShot bidirectionnel) ou des trames de télémétrie
INA228 sur l'entrée DC de l'ESCMesure le courant
Arrêt d'urgence câbléCoupe l'alimentation de l'ESC, indépendamment de tout logiciel

La rampe elle-même s'exécute sur le microcontrôleur. Une boucle Python sur un système d'exploitation de bureau ne peut pas garantir une trame chaque milliseconde ; une interruption de timer, si. L'hôte n'envoie que des consignes et des pentes.

Le DShot en bref

Une trame DShot fait 16 bits : une valeur sur 11 bits, un bit de demande de télémétrie et une somme de contrôle sur 4 bits. Les valeurs de 0 à 2047 se répartissent en 0 (moteur arrêté), 1 à 47 (commandes) et 48 à 2047 (gaz). Le DShot300 transmet à 300 kbit/s, donc une trame dure environ 53 µs. L'ESC attend des trames en continu et s'arme après avoir reçu la valeur 0 pendant un court instant ; le firmware envoie donc 0 dès qu'aucun moteur n'est commandé.

Le gaz en pourcentage se convertit en valeur DShot par 48 + round(pct / 100 x 1999).

Étape 1 : définir le contrat du firmware

Le firmware du microcontrôleur est à écrire par vos soins. Ce qui compte pour le test, c'est un petit protocole texte explicite sur la liaison série USB. Ce guide utilise celui-ci comme exemple :

CommandeRéponseEffet
RAMP <motor> <target_pct> <rate_pct_s>OKMonte ou descend un moteur depuis son gaz actuel jusqu'à la consigne, à la pente donnée
STOP <rate_pct_s>OKRamène tous les moteurs à 0 à la pente donnée
STATUS <motor>OK <throttle_pct> <erpm>Gaz actuellement appliqué et dernier eRPM reçu

Deux comportements sont obligatoires dans le firmware, pas optionnels :

  • Watchdog. Si aucune commande n'arrive pendant 500 ms, le firmware ramène tous les moteurs à 0. Un hôte planté, un CLI tué ou un câble USB débranché se terminent alors par un arrêt, pas par un moteur qui reste à 70 %.
  • Limite de pente. Le firmware refuse une pente au-dessus d'un maximum fixe, pour qu'une faute de frappe dans une procédure ne transforme pas une rampe en marche.

Étape 2 : écrire le plug moteur

Le plug encapsule le protocole série, convertit l'eRPM en tr/min mécaniques et arrête les moteurs à sa destruction :

requirements.txt
pyserial>=3.5smbus2>=0.4
plugs/motor.py
39 lines
import timeimport serialclass MotorDriver:    def __init__(self, port, pole_pairs, baudrate=115200):        self.pole_pairs = pole_pairs        self.ser = serial.Serial(port, baudrate, timeout=0.2)        time.sleep(2.0)  # beaucoup de cartes redémarrent à l'ouverture du port        self.ser.reset_input_buffer()        self._cmd("STOP 50")    def __del__(self):        if hasattr(self, "ser") and self.ser.is_open:            try:                self._cmd("STOP 50")            finally:                self.ser.close()    def _cmd(self, line):        self.ser.write((line + "").encode())        reply = self.ser.readline().decode().strip()        if not reply.startswith("OK"):            raise RuntimeError(f"{line!r}: unexpected reply {reply!r}")        return reply[2:].strip()    def ramp_to(self, motor, target_pct, rate_pct_s):        self._cmd(f"RAMP {motor} {target_pct} {rate_pct_s}")    def stop(self, rate_pct_s):        self._cmd(f"STOP {rate_pct_s}")    def status(self, motor):        """Renvoie [throttle_pct, rpm] tels qu'appliqués par le firmware."""        throttle, erpm = self._cmd(f"STATUS {motor}").split()        # L'eRPM compte des tours électriques : diviser par le nombre de paires de pôles        return [float(throttle), int(erpm) / self.pole_pairs]

Un moteur 12N14P a 14 aimants, soit 7 paires de pôles : 158 200 eRPM correspondent à 22 600 tr/min. Chaque interrogation STATUS compte aussi comme une commande pour le watchdog, donc une phase qui enregistre des données maintient le firmware actif.

Étape 3 : enregistrer la rampe dans une seule courbe

Gaz, courant et vitesse partagent un axe de temps. Enregistrez-les comme une seule mesure multidimensionnelle à trois axes y, et posez les limites de validation sur des agrégations :

procedure.yaml
72 lines
name: Motor Ramp Testversion: 0.1.0execution:  workers: 1plugs:  - name: Motor Driver    key: motor    python: plugs.motor:MotorDriver    scope: station    with:      port: /dev/ttyACM0      pole_pairs: 7  - name: Current Monitor    key: current_monitor    python: plugs.ina228:INA228    scope: station    with:      bus: 1      address: 0x40      shunt_ohm: 0.0005setup:  - name: Motors Stopped    python: phases.ramp:stop_allmain:  - name: Ramp Motor 1 to 70 %    key: ramp_m1    python: phases.ramp:ramp_profile    timeout: 60s    with:      motor_index: 1      target_pct: 70      rate_pct_s: 20      hold_s: 3.0      abort_current_a: 6.0      min_rpm_per_pct: 200    measurements:      - name: Ramp Motor 1        key: ramp        description: 0 to 70 % at 20 %/s, 3 s hold, ramp down at 20 %/s. No propeller.        x_axis:          legend: Time          unit: s        y_axis:          - legend: Throttle            key: throttle            unit: "%"          - legend: Current            key: current            unit: A            aggregations:              - type: max                validators:                  - operator: "<="                    expected_value: 4.0          - legend: Speed            key: rpm            unit: rpm            aggregations:              - type: max                validators:                  - operator: ">="                    expected_value: 21000                  - operator: "<="                    expected_value: 24500teardown:  - name: Stop All Motors    python: phases.ramp:stop_all

workers: 1 garde les phases séquentielles. TofuPilot exécute par défaut les phases indépendantes en parallèle, ce qui, sur un banc moteur, revient à deux phases qui commandent le même ESC.

La phase distingue deux types de limites. Les limites d'arrêt protègent le banc et sont vérifiées dans la boucle : la phase arrête le moteur et échoue immédiatement. Les limites de validation jugent le moteur et sont évaluées sur la courbe enregistrée par les validateurs.

phases/ramp.py
53 lines
import timedef ramp_profile(motor, current_monitor, measurements, phase, log,                 motor_index, target_pct, rate_pct_s, hold_s, abort_current_a, min_rpm_per_pct):    times, throttle, current, rpm = [], [], [], []    t0 = time.monotonic()    def save():        m = measurements.ramp        m.x_axis = times        m.y_axis.throttle = throttle        m.y_axis.current = current        m.y_axis.rpm = rpm        m.y_axis.current.aggregations.max = max(current, default=0.0)        m.y_axis.rpm.aggregations.max = max(rpm, default=0.0)    def abort(reason):        motor.stop(rate_pct_s)        save()        phase.fail(reason)    motor.ramp_to(motor_index, target_pct, rate_pct_s)    ramp_down_at = target_pct / rate_pct_s + hold_s    ramping_down = False    while True:        elapsed = time.monotonic() - t0        if not ramping_down and elapsed >= ramp_down_at:            motor.ramp_to(motor_index, 0, rate_pct_s)            ramping_down = True        thr, speed = motor.status(motor_index)        amps = current_monitor.read_current()        times.append(round(elapsed, 3))        throttle.append(thr)        current.append(amps)        rpm.append(speed)        if amps > abort_current_a:            abort(f"{amps:.2f} A above abort limit {abort_current_a} A at {thr:.0f} %")        if not ramping_down and thr > 10 and speed < thr * min_rpm_per_pct:            abort(f"{speed:.0f} rpm at {thr:.0f} % throttle: stalled or desynchronized")        if ramping_down and thr == 0:            break        time.sleep(0.02)    save()    log.info(f"Peak {max(current):.2f} A, top speed {max(rpm):.0f} rpm")def stop_all(motor):    motor.stop(50)

Le contrôle de calage utilise le rapport observé sur le moteur d'exemple : 22 600 tr/min à 70 % font 323 tr/min par pourcent, donc 200 tr/min par pourcent laisse de la marge au retard de montée en vitesse tout en détectant un moteur qui a perdu la synchronisation. save() s'exécute avant phase.fail() parce que phase.fail() sort immédiatement de la fonction ; sans cela, la courbe qui explique l'arrêt serait perdue.

Sur le dashboard, la courbe montre où le courant a monté par rapport au gaz, et la carte de contrôle de l'agrégation de courant max montre la dérive d'une unité à l'autre.

Étape 4 : arrêter les moteurs sur chaque chemin de sortie

Listez toutes les façons dont un test peut se terminer et vérifiez ce qui arrête le moteur dans chaque cas :

Chemin de sortieCe qui arrête le moteur
La phase passeLa phase redescend elle-même
Limite d'arrêt ou phase.fail()La rampe d'arrêt, puis la phase de teardown
Timeout de la phaseLa phase de teardown (le teardown s'exécute toujours)
L'opérateur arrête le runLa phase de teardown
Exception Python dans la phaseLa phase de teardown
Plug détruit (arrêt du poste, nouveau déploiement)MotorDriver.__del__
CLI tué, plantage de l'hôte, USB débranchéWatchdog du firmware après 500 ms
Emballement, incendie, défaillance mécaniqueArrêt d'urgence câblé

La dernière ligne est la seule qui soit une fonction de sécurité. Les phases de teardown, les destructeurs de plug et les watchdogs de firmware sont du logiciel et tombent avec le logiciel. Un banc avec des pièces en rotation a besoin d'un arrêt d'urgence câblé qui coupe la puissance (ISO 13850, catégorie d'arrêt 0 ou 1), d'un protecteur autour des pièces en rotation (ISO 14120) avec un verrouillage s'il peut être ouvert (ISO 14119), et d'une appréciation du risque selon l'ISO 12100 au titre de la Directive Machines 2006/42/CE, remplacée par le Règlement Machines (UE) 2023/1230 à partir du 20 janvier 2027. Travailler sous 60 V DC limite le risque de choc électrique, pas les risques liés à l'énergie, à l'incendie ou aux projections.

Redescendez en rampe dans le teardown, à une pente bornée. Un teardown qui écrit un gaz de 0 instantanément fait de l'arrêt la plus grande marche descendante du test, avec la surtension de freinage régénératif qui l'accompagne.

Sans hélice et avec hélice

La même structure de procédure sert les deux cas. Les valeurs, le matériel autour et les risques changent :

Sans héliceAvec hélice
Courant par moteur0,1 A au ralenti à 3,5 à 4,5 A à 100 % (mesuré)Dizaines d'ampères, 60 à 80 A plein gaz en 6S (estimation pour une hélice typique)
Puissance pour quatre moteurs plein gazMoins de 500 W22,2 V x 80 A x 4 = 7,1 kW (estimation)
Source d'énergieAlimentation de banc, avec batterie en parallèle ou écrêteur pour la régénérationBatterie, avec précharge ou connecteur anti-étincelle
Pente de rampe20 à 50 %/sPlus lente, dimensionnée d'après le dépassement de courant mesuré
MécaniqueSupport moteur seulBanc de poussée dimensionné pour la poussée, protecteur contre l'éjection de pale
Ce que le test prouveSanté électrique et roulements, voie d'ESCPoussée, rendement, comportement thermique en charge

Gardez deux procédures distinctes, ou des phases distinctes avec leurs propres valeurs with: et leurs limites. Une limite réglée sans hélice fait échouer toutes les unités avec hélice, et un courant d'arrêt dimensionné pour une hélice ne protège pas un moteur nu.

Pièges courants

Faire tourner la boucle de rampe en Python. La gigue du système d'exploitation rend la rampe irrégulière et le timing non reproductible. Faites la rampe sur le microcontrôleur ; envoyez les consignes depuis l'hôte.

Utiliser le test moteur du contrôleur de vol pour des profils. MAV_CMD_DO_MOTOR_TEST ne peut pas changer le gaz en cours de test, donc les profils deviennent des suites d'arrêts et de redémarrages avec un pic à chaque redémarrage.

Pas de watchdog dans le firmware. Un CLI tué laisse le dernier gaz appliqué. Le watchdog est l'arrêt pour toutes les défaillances que l'hôte ne peut pas gérer.

Écrire un gaz de 0 dans le teardown. Un arrêt instantané est une marche descendante, avec la surtension du freinage régénératif. Redescendez en rampe.

Appeler phase.fail() avant d'enregistrer la mesure. La fonction sort et la courbe est perdue. Enregistrez d'abord.

Prendre le teardown pour l'arrêt d'urgence. C'est du logiciel. L'arrêt d'urgence est un circuit câblé qui coupe la puissance.

Laisser les phases en parallèle. Réglez workers: 1 sur un banc moteur.

Points clés

  • Les marches de gaz produisent des pics de courant et une surtension régénérative ; les rampes évitent les deux et rendent les lectures reproductibles.
  • Générez le DShot et exécutez les rampes sur un microcontrôleur ; le plug TofuPilot envoie les consignes et relit le gaz et l'eRPM.
  • Divisez l'eRPM par le nombre de paires de pôles (7 pour un moteur 12N14P) pour obtenir les tr/min mécaniques.
  • Enregistrez gaz, courant et tr/min dans une seule mesure multidimensionnelle ; vérifiez les limites d'arrêt dans la boucle et les limites de validation sur les agrégations.
  • Le teardown, le destructeur du plug et le watchdog du firmware arrêtent le moteur par logiciel ; seul un arrêt d'urgence câblé est une fonction de sécurité.

Pour une vue d'ensemble des bancs de test de moteurs et d'actionneurs, consultez Motor and Actuator Testing with TofuPilot.

Plus de guides

Mettez ce guide en pratique