Résolu sur le forum : Borne Wallbox Pulsar Plus 7,4 kW sur abonnement 6 kVA : quel délestage avant accord copro (urgent)Ma panne diagnostiquée — réponse sous 48 h29 €

Arduino delay vs millis : quelle méthode choisir et pourquoi ?

Arduinomis à jour le 26 février 2026

Sur Arduino, le choix entre delay() et millis() change complètement la manière dont un projet réagit, gère plusieurs tâches et reste stable dans le temps. Derrière ces deux fonctions très simples en apparence se cachent des différences fortes en termes de blocage, de précision et d’architecture logicielle.

Dans cet article, on analyse en détail le fonctionnement interne de chaque approche, leurs limites chiffrées, leurs impacts sur le multitâche et la réactivité, ainsi que des exemples concrets de mise en œuvre. De quoi revoir en profondeur la façon de gérer le temps dans vos programmes Arduino.

Situation Méthode recommandée Pourquoi Niveau d’impact sur le programme
Temporisation simple et unique delay() Le code est plus lisible et rapide à écrire pour un besoin ponctuel. Impact fort : le microcontrôleur est bloqué.
Projet multitâche (LED + capteurs + communication…) millis() Permet d’exécuter plusieurs actions sans bloquer le programme. Impact faible : boucle loop() reste active.
Gestion précise d’événements répétés millis() Assure des intervalles constants, même si d’autres tâches sont exécutées entre-temps. Très faible : comportement régulier et stable.
Débogage rapide ou prototype simple delay() Idéal pour tester rapidement sans structure complexe. Modéré : acceptable pour les micro‑projets.
Besoin d’une réactivité quasi immédiate (boutons, interruptions…) millis() Évite les blocages qui feraient rater des actions utilisateur. Très faible : réactivité optimale.

Comprendre le fonctionnement de delay() sur Arduino

La fonction delay() est souvent la première utilisée lorsqu’on écrit un programme Arduino débutant. Elle paraît simple : on lui passe une durée en millisecondes, et le microcontrôleur attend avant de continuer. Derrière cette simplicité se cache un comportement très bloquant pour le reste du code.

Concrètement, delay() stoppe l’exécution de la boucle loop() pendant la durée demandée. Le microcontrôleur reste occupé à attendre, sans exécuter d’autres tâches utilisateur. Cette attente bloquante interfère directement avec la réactivité d’un système, surtout en présence de capteurs, de communication série ou réseau, ou de moteurs à piloter en continu.

La fonction delay() utilise le timer interne de la carte. Elle convertit le nombre de millisecondes en cycles d’horloge, puis boucle jusqu’à ce que le temps estimé soit écoulé. Pendant cette période, aucun traitement de haut niveau n’est réalisé dans votre code, à part les interruptions matérielles automatiques gérées par le microcontrôleur lui-même.

Effets concrets du blocage provoqué par delay()

L’effet principal de delay() est clair : le microcontrôleur est totalement bloqué pendant toute la durée de l’attente. Aucun traitement dans loop() ne peut avancer. Aucune décision logique n’est prise. Aucune tâche parallèle ne progresse.

En pratique, cela signifie :

  • Multitâche impossible en structure séquentielle simple.
  • Réactivité très faible face à un appui bouton imprévu.
  • Lecture capteur en retard ou carrément ratée si l’événement est trop court.
  • Gestion moteur ou servomoteur irrégulière si la logique repose sur des délais bloquants.

Une dérive de temps a aussi été observée : sur certains projets, l’utilisation intensive de delay() conduit à une erreur d’environ 13 secondes après 100 minutes. Cette dérive provient de la manière dont les boucles d’attente sont gérées, combinée à de légers décalages accumulés dans le temps.

« Sur un projet de datalogger, j’ai perdu régulièrement plusieurs secondes par heure en basant la temporisation uniquement sur delay(). Le passage à millis() a stabilisé totalement la chronologie des enregistrements. »

Les bibliothèques et services bas niveau (interruptions, timers matériels) continuent cependant de fonctionner, ce qui explique pourquoi certaines fonctions système restent réactives malgré le blocage de la boucle principale. Mais ce fonctionnement reste peu adapté dès que l’on souhaite une architecture réactive.

Conseil pratique :

Réservez delay() aux prototypes très simples, aux tests rapides ou à des pauses très courtes lors de séquences d’initialisation. Pour tout projet évolutif, orientez votre logique vers millis() et les machines à états.

La fonction millis() : une base pour des programmes Arduino non bloquants

La fonction millis() renvoie le nombre de millisecondes écoulées depuis le démarrage de la carte Arduino. Elle ne prend aucun paramètre et retourne un unsigned long 32 bits (4 octets). Cette valeur s’incrémente en continu, indépendamment de votre code, grâce à un timer matériel.

ArduinoArduino et domotique : automatiser sa maison pour pas cher

Contrairement à delay(), millis() n’arrête jamais le programme. Elle se contente de fournir un horodatage interne, que votre code utilise ensuite pour décider quand exécuter une action. Cette approche transforme complètement la structure des programmes Arduino : au lieu d’attendre passivement, le code vérifie régulièrement si un certain temps s’est écoulé et agit en conséquence.

La valeur renvoyée par millis() déborde après environ 4 294 967 295 ms, soit un peu plus de 50 jours de fonctionnement continu. Au moment de l’overflow, la valeur revient à zéro par effet de débordement sur le type unsigned long. Un code correctement écrit continue alors à fonctionner sans erreur.

Principe d’utilisation de millis() pour la temporisation

La méthode classique consiste à mémoriser l’instant de référence dans une variable, puis à comparer ce temps à la valeur courante de millis(). Dès que la différence atteint l’intervalle souhaité, l’action est exécutée et l’instant de référence mis à jour.

Ce schéma permet de réaliser des minuteries, clignotements, rafraîchissements de capteurs ou séquences moteur sans bloquer le déroulement général du programme. La boucle loop() s’exécute en continu, vérifie plusieurs minuteries virtuelles et déclenche les actions au bon moment.

Le même principe sert de base à des structures plus avancées : machines à états, gestion d’événements, systèmes de tâches pseudo-concurrentes. Tout repose sur le temps système fourni par millis() et des comparaisons régulières.

Astuce de conception :

Traitez chaque temporisation comme une « alarme » indépendante. Une variable stocke le dernier déclenchement, une constante définit l’intervalle, et une condition if (millis() - dernier >= intervalle) active l’action. Cette structure se réutilise presque à l’identique pour toutes les tâches.

Comparatif détaillé : delay() vs millis()

Un tableau comparatif aide à visualiser les différences entre les deux approches, tant sur le plan technique que sur le plan architectural.

Critère delay() millis()
Type de comportement Blocage complet de la boucle loop() Lecture du temps, aucun blocage direct
Multitâche Quasi impossible sans architecture complexe Géré via minuteries logicielles et états
Précision temporelle Dérive observée d’environ 13 s / 100 min Basée sur timer matériel, stable dans la durée
Lisibilité du code Très simple sur de courts exemples Structure un peu plus élaborée mais claire
Scalabilité Chaque délai bloque tout le reste Ajout de tâches supplémentaires très simple
Gestion des événements imprévus Réactivité faible pendant un delay() Réactivité constante, boucle active en continu
Type de variable associé Généralement unsigned long pour stocker les durées Retour natif en unsigned long 32 bits
Débordement (overflow) Sensibilité au débordement des compteurs intermédiaires Overflow après ~50 jours, bien géré en soustraction
Cas d’usage typique Programmes d’exemple simples, tests rapides Projets complets, systèmes réactifs, IoT, robots

Le principal avantage de millis() tient à la suppression du blocage. Le programme reste actif, la fonction loop() tourne en continu, et chaque tâche se déclenche en fonction du temps écoulé. Cette organisation facilite la mise en place de structures en machine d’état et d’architectures orientées événements.

Avec delay(), la scalabilité reste limitée. Ajouter une nouvelle tâche qui doit s’exécuter à intervalle régulier entraîne souvent des enchaînements de delay() imbriqués ou successifs. Le code devient difficile à maintenir et à faire évoluer.

Gestion des overflow : millis(), delay() et types numériques

La gestion du temps sur Arduino repose fortement sur des types numériques non signés et sur le phénomène d’overflow. Comprendre ces phénomènes évite des bogues discrets, surtout dans des projets qui tournent de manière prolongée.

Overflow de millis() autour de 50 jours

millis() utilise un unsigned long 32 bits. La valeur maximale de ce type est 4 294 967 295. Une fois ce seuil atteint, la valeur repasse à zéro. Ce cycle dure environ 50 jours de fonctionnement continu avant chaque réinitialisation implicite.

La clé pour rester robuste consiste à toujours travailler en soustraction :

  • Stocker l’instant de référence : unsigned long t0 = millis();
  • Tester l’intervalle : if (millis() - t0 >= intervalle)

Grâce aux propriétés de l’arithmétique modulaire des entiers non signés, cette soustraction reste correcte même lorsque millis() déborde. Le débordement se produit de la même manière pour la valeur courante et la valeur mémorisée, si bien que la différence reflète toujours le temps écoulé.

« Les projets Arduino qui tournent en continu pendant des semaines s’appuient systématiquement sur la soustraction de millis() pour gérer les temporisations. Cette méthode encaisse sans incident chaque overflow de 50 jours. »

Overflow des entiers sur Arduino Uno

Sur Arduino Uno, le type unsigned int sur 16 bits déborde au-delà de 65 535. Pour une variable qui compte des millisecondes, ce plafond correspond à environ 65 secondes. Au-delà, retour à zéro et comportement inattendu si le code ne prend pas en compte ce phénomène.

Pour toutes les temporisations basées sur des millisecondes ou sur la valeur de millis(), l’usage de unsigned long est fortement recommandé. Ce type garantit une plage suffisamment large pour les systèmes qui restent alimentés sur de longues périodes.

Point de vigilance :

Évitez d’utiliser int pour les minuteries ou les compteurs de temps sur Arduino Uno. Préférez systématiquement unsigned long pour refléter correctement l’échelle de temps gérée par millis().

Impact réel de delay() sur les performances et la réactivité

L’utilisation répétée de delay() affecte directement les performances perçues d’un système Arduino. Le processeur n’est pas saturé en termes de cycles d’horloge, mais il reste inactif du point de vue de votre logique applicative.

On observe alors :

  • Une latence élevée entre un événement externe (bouton, capteur) et la réaction du programme.
  • Une impossibilité de gérer proprement plusieurs boucles temporelles différentes.
  • Une impression de lenteur globale, même sur un microcontrôleur pourtant rapide pour des tâches embarquées.

Il n’existe pas encore, à l’horizon 2026, de séries de mesures publiques exhaustives sur la dégradation de performances liée à delay() (latence moyenne, temps de réaction précis, charge CPU perçue). Les retours de terrain convergent cependant vers la même constatation : plus les délais bloquants sont longs, moins le système semble réactif.

Dans les architectures avec communication série, WiFi ou Ethernet, l’emploi massif de delay() provoque aussi des pertes de paquets, des tampons saturés, voire des resets inopinés lorsque certains watchdogs ne reçoivent pas de rafraîchissement à temps.

Pourquoi millis() favorise des machines à états réactives

La force de millis() ne tient pas seulement à l’absence de blocage. Elle pousse à structurer le code autour de machines à états. Chaque fonctionnalité du programme se décrit alors comme un enchaînement d’états, avec des transitions déclenchées par le temps ou par des événements externes.

Par exemple, un feu tricolore se décrit par les états Rouge, Orange, Vert. Au lieu d’écrire :

  • digitalWrite(rouge, HIGH); delay(5000);
  • digitalWrite(orange, HIGH); delay(1000);
  • digitalWrite(vert, HIGH); delay(5000);

On définit un état courant et un instant de changement. La boucle principale vérifie régulièrement si le temps pour passer à l’état suivant est écoulé. Pendant ce temps, le programme reste libre de gérer d’autres tâches : lecture capteurs, communication série, interface utilisateur.

Ce paradigme d’états + temps s’adapte très bien aux systèmes embarqués :

  • Menus d’interface avec boutons physiques.
  • Robot mobile avec plusieurs comportements (suivi de ligne, évitement d’obstacles, pause).
  • Système de contrôle domotique réagissant à des consignes et à des horaires.
Conseil d’architecture :

Identifiez clairement les états fonctionnels de votre système, puis associez à chacun : les actions à réaliser, la durée minimale à passer dans cet état, et les événements déclenchant le passage à l’état suivant. millis() devient alors la référence temporelle commune à toute la logique.

Exemples concrets d’utilisation de millis() à la place de delay()

Le passage de delay() à millis() se traduit surtout par un changement de style de code. Voici plusieurs exemples typiques où millis() offre un gain de flexibilité.

Minuteur multitâche gérant plusieurs actions simultanées

Un seul programme Arduino peut gérer plusieurs minuteries indépendantes grâce à millis() :

  • Une LED qui clignote toutes les 200 ms.
  • Un affichage LCD mis à jour toutes les 500 ms.
  • Une mesure de température toutes les 2 s.
  • Une sauvegarde sur carte SD toutes les 60 s.

Chaque fonctionnalité dispose de sa propre variable dernierTemps et de son intervalle. La boucle principale parcourt ces tâches à chaque itération et déclenche celles dont l’intervalle est dépassé. L’ajout d’une nouvelle tâche se fait alors en quelques lignes, sans risquer de bloquer le reste.

Ce type de minuterie multitâche serait extrêmement difficile à maintenir avec une série de delay(), car chaque attente figerait tout le système.

Contrôle de servomoteur sans bloquer le reste du code

La gestion d’un servomoteur nécessite parfois des pauses entre deux positions successives. Avec delay(), ces pauses bloquent tout le programme. Avec millis(), le servomoteur change de position toutes les 3 secondes, pendant que le reste du code continue de tourner.

ArduinoPiloter un relais 230 V avec Arduino en sécurité : 5 erreurs à éviter

Par exemple :

  • Le servo se déplace de 0° à 90° toutes les 3 s.
  • Un capteur de distance est lu toutes les 200 ms.
  • Les données sont envoyées à un serveur local toutes les 5 s.

Chaque action repose sur sa propre temporisation par millis(). Aucun blocage global n’intervient, ce qui améliore la fluidité globale du système.

Système multi-capteurs et serveur web embarqué

Un même Arduino peut gérer plusieurs capteurs avec des fréquences d’échantillonnage différentes, tout en servant une page web embarquée. Par exemple :

  • Capteur LDR (luminosité) lu à un certain intervalle.
  • Capteur de température LM34 lu à un autre intervalle.
  • Serveur web envoyant les données toutes les 3 minutes.

Chaque capteur possède son timer basé sur millis(). Pendant que l’Arduino attend 3 minutes avant le prochain envoi sur le serveur, il continue de rafraîchir et de traiter les mesures des capteurs à un rythme bien plus rapide. Aucun delay(180000) ne vient figer le projet.

Pilotage de moteur pas à pas avec intervalles non bloquants

Un moteur pas à pas nécessite une succession de pas espacés dans le temps. Avec delay(), la boucle enchaîne les pas et attend entre chaque, en bloquant tout. Avec millis(), chaque pas est déclenché quand l’intervalle souhaité est atteint, pendant que la boucle loop() reste libre.

Ce fonctionnement simplifie :

  • L’adaptation dynamique de la vitesse.
  • La gestion simultanée de plusieurs moteurs.
  • L’intégration de capteurs de fin de course et de sécurité.

Chaque moteur peut ainsi disposer de son propre intervalle de pas, géré par une variable nextStepTime fondée sur millis(). Le code reste lisible, réactif, et évolutif.

Clignotement de LEDs à fréquences indépendantes

Un cas pédagogique fréquent consiste à faire clignoter plusieurs LEDs à des fréquences différentes. Avec delay(), il devient vite difficile de combiner plusieurs rythmes. Avec millis(), on définit pour chaque LED :

  • Un intervalle de clignotement spécifique.
  • Une variable dernierBasculage.
  • Un état courant (allumé / éteint).

La boucle teste chaque LED à chaque passage et met à jour seulement celles dont l’intervalle est écoulé. L’ajout d’une nouvelle LED revient simplement à ajouter une nouvelle entrée dans cette logique. Le code n’augmente pas en complexité de manière disproportionnée.

Pour approfondir la mise en œuvre de ces schémas temporels, une ressource dédiée comme ce guide sur millis() et les timers Arduino fournit une base solide pour professionnaliser la gestion du temps dans vos projets.

Gestion des temporisations avancées : combiner millis() et interruptions

Dans certaines applications exigeantes, l’usage exclusif de millis() ne suffit pas. Des impératifs de latence très courte, de synchronisation précise avec des signaux externes, ou de comptage à haute fréquence conduisent à combiner millis() avec des interruptions matérielles.

Les interruptions réagissent à des événements matériels (front montant sur une broche, débordement de timer matériel, etc.) et interrompent l’exécution courante pour exécuter une routine spécifique. Cette approche complète bien millis() lorsque certains événements doivent être traités immédiatement, quelle que soit la portion de code en cours d’exécution.

Dans ce contexte, millis() sert souvent à :

  • Mesurer des durées globales entre deux événements gérés par interruption.
  • Programmer des temporisations non critiques, tandis que les interruptions se chargent des tâches prioritaires.
  • Surveiller le bon fonctionnement de routines d’interruption (détection d’absence d’événements, timeout, etc.).

Une compréhension fine des interruptions et de leur interaction avec les temporisations améliore encore la stabilité et la précision des projets temps réel. Un guide spécifique comme ce tutoriel sur les interruptions Arduino permet d’affiner cette partie de l’architecture.

Note technique :

La valeur retournée par millis() s’appuie déjà sur un timer matériel et sur une interruption système. Dans la plupart des cas, cette précision suffit largement. Les interruptions personnalisées se réservent aux besoins très spécifiques (mesure de fréquence élevée, protocoles temps réel stricts, etc.).

Limites pratiques de millis() et points d’attention

Malgré ses nombreux avantages, millis() exige quelques précautions pour rester fiable dans toutes les situations. Une structure mal pensée peut introduire des dérives logiques, même si la base temporelle reste stable.

Organisation du code et lisibilité

Un grand nombre de tâches temporisées dans une seule fonction loop() peut rendre le code difficile à lire. Pour conserver une bonne structure, plusieurs stratégies aident :

  • Regrouper les tâches par thème dans des fonctions dédiées.
  • Utiliser des structures ou des classes pour encapsuler chaque « tâche temporelle ».
  • Documenter clairement les intervalles et les états associés.

Sans cette discipline, la logique basée sur millis() risque de se disperser et de perdre sa clarté initiale. La lisibilité reste un enjeu central dès que le projet grossit.

Interactions avec analogRead() et délais entre mesures

Lorsqu’on enchaîne des lectures analogiques sur des broches différentes avec analogRead(), un délai recommandé de l’ordre de 100 µs entre deux lectures permet au multiplexeur interne de se stabiliser. Cette contrainte temporelle reste faible par rapport aux millisecondes gérées par millis(), mais elle entre dans la conception globale du timing.

On peut ainsi combiner :

  • Une boucle principale cadencée par millis() pour la logique globale.
  • Des délais très courts (quelques dizaines de microsecondes) pour la stabilité des conversions analogiques.

Ces deux échelles de temps coexistent sans difficulté, à condition de ne pas transformer ces petites pauses techniques en longues attentes bloquantes injustifiées.

Absence de mesures publiques détaillées sur certaines métriques

Pour l’instant, les données publiques récentes ne fournissent pas encore une cartographie précise de la latence réelle et de la réactivité microseconde par microseconde de projets complexes utilisant delay() ou millis(). Les mesures disponibles se concentrent surtout sur :

  • Les limites théoriques des types numériques.
  • Les dérives de temps observées sur des périodes données (par exemple 13 s / 100 min pour delay()).
  • Les comportements empiriques dans des projets concrets.

Malgré cette absence de benchs exhaustifs, le retour d’expérience concorde largement : les architectures basées sur millis() et sur des machines à états procurent une meilleure stabilité à long terme, une réactivité plus maîtrisée, et une marge de manœuvre plus large pour faire évoluer les projets Arduino.

Donnez votre avis

Soyez le 1er à noter cet article


Partagez cet article maintenant !


Ça ne règle pas votre cas ?

Décrivez la panne sur le forum, avec une photo si vous pouvez : les membres qui ont eu la même répondent, et le sujet reste pour les suivants.

Avatar photoAvatar photoAvatar photoAvatar photoAvatar photoRejoindre les 54 membres — gratuit, 44 pannes résolues

Dans le même tiroir

Photo réaliste d’un Arduino connecté à un module Bluetooth HC‑06 sur un établi avec outils et smartphone.
Arduino

Arduino HC-06 code : Bluetooth esclave

Connecter un module Bluetooth HC‑06 à une carte Arduino ouvre la voie au pilotage sans fil, au contrôle à distance…

Portrait de Inès DameronInès Dameron · 28 Fév 2026
La lettre du club

Une lettre par mois, accrochée au panneau

Les sujets du forum résolus dans le mois, un guide, un chiffre qu’on peut vérifier chez soi. Rien d’autre, jamais plus souvent — et on se désabonne d’un clic.

Ce qu’il y a dedans

Votre adresse

Une lettre par mois. Désabonnement en un clic.