Aller au contenu principal

Projet guidé : le carrefour

Ce que ce chapitre apporte

  • Construire un programme par étapes, en gardant à chaque étape un programme qui fonctionne.
  • Traduire un cahier des charges en diagramme d'états, puis en programme.
  • Faire réagir un feu à un appel piéton à tout instant, sans attendre la fin d'un cycle.
  • Mémoriser une demande et la servir au moment autorisé.
  • Vérifier des règles de sécurité sur le comportement observé, et pas seulement à la lecture du code.

Un feu de circulation avec bouton piéton réunit tout le parcours : des sorties, une entrée qui rebondit, des durées, et surtout un comportement qui doit réagir à tout instant. On ne l'écrit pas d'un bloc. On le fait grandir en trois étapes, chacune donnant un programme qui fonctionne, et chacune ajoutant une exigence. La première version qui vient à l'esprit, une suite de delay, échoue à la deuxième étape, et comprendre pourquoi est l'objet même du projet.

Le montage

ComposantBrocheRemarque
Feu voitures rouge13LED et résistance de 220 Ω vers GND
Feu voitures orange12idem
Feu voitures vert11idem
Feu piétons rouge10idem, ajouté à l'étape 3
Feu piétons vert9idem, ajouté à l'étape 3
Bouton d'appel2entre la broche et GND, configuré en INPUT_PULLUP

Étape 1 : le feu seul

Le feu tourne de lui-même : vert quatre secondes, orange une seconde, rouge quatre secondes. Pas encore de bouton, mais déjà la structure définitive : une machine à états et millis().

Graphe orienté et pondéré3 sommets, 3 arêtes
4 s1 s4 sVERTORANGEROUGE
Le feu seul. Chaque transition est déclenchée par une durée écoulée dans l'état.
Arduino
const int ROUGE = 13, ORANGE = 12, VERT = 11;
const unsigned long DUREE_VERT = 4000, DUREE_ORANGE = 1000, DUREE_ROUGE = 4000;
enum Etat { FEU_VERT, FEU_ORANGE, FEU_ROUGE };
Etat etat = FEU_VERT;
unsigned long debutEtat = 0; // instant d'entrée dans l'état courant
void allumerSeul(int broche) {
digitalWrite(ROUGE, broche == ROUGE);
digitalWrite(ORANGE, broche == ORANGE);
digitalWrite(VERT, broche == VERT);
}
void changerEtat(Etat nouvel) {
etat = nouvel;
debutEtat = millis();
}
void setup() {
pinMode(ROUGE, OUTPUT);
pinMode(ORANGE, OUTPUT);
pinMode(VERT, OUTPUT);
}
void loop() {
unsigned long ecoule = millis() - debutEtat;
switch (etat) {
case FEU_VERT:
allumerSeul(VERT);
if (ecoule >= DUREE_VERT) changerEtat(FEU_ORANGE);
break;
case FEU_ORANGE:
allumerSeul(ORANGE);
if (ecoule >= DUREE_ORANGE) changerEtat(FEU_ROUGE);
break;
case FEU_ROUGE:
allumerSeul(ROUGE);
if (ecoule >= DUREE_ROUGE) changerEtat(FEU_VERT);
break;
}
}

Deux fonctions portent toute la mécanique. allumerSeul garantit qu'un seul feu est allumé, quel que soit l'état précédent. changerEtat note l'instant d'entrée dans le nouvel état : chaque durée se mesure alors par millis() - debutEtat, sans delay.

La même étape écrite avec delay
Écrire vert, delay(4000), orange, delay(1000), rouge, delay(4000) donne le même feu, plus court à écrire. Le défaut apparaît à l'étape suivante. Une version de ce type, qui lit le bouton au début de chaque cycle, a été simulée : sans aucun appui, le feu passe au rouge à 4, 11 puis 18 secondes, alors que le cahier des charges veut qu'il reste au vert ; et un appui donné à 8 secondes, pendant une pause, est tout simplement perdu.

Étape 2 : le bouton qui appelle

Le cahier des charges change : le feu reste au vert tant que personne n'appelle. Un appui déclenche immédiatement l'orange, puis le rouge pendant cinq secondes, puis le retour au vert, où le feu attend de nouveau.

Graphe orienté et pondéré3 sommets, 3 arêtes
appui1 s5 sVERTORANGEROUGE
Le feu à la demande. L'état VERT n'a plus de durée : seul un appui l'en fait sortir.
Arduino
const int ROUGE = 13, ORANGE = 12, VERT = 11, BOUTON = 2;
const unsigned long DUREE_ORANGE = 1000, DUREE_ROUGE = 5000, STABLE_MS = 30;
enum Etat { FEU_VERT, FEU_ORANGE, FEU_ROUGE };
Etat etat = FEU_VERT;
unsigned long debutEtat = 0;
int lectureBrute = HIGH, etatStable = HIGH;
unsigned long dernierChangement = 0;
bool appuiDetecte() {
int lecture = digitalRead(BOUTON);
if (lecture != lectureBrute) {
lectureBrute = lecture;
dernierChangement = millis();
}
if (millis() - dernierChangement >= STABLE_MS && lectureBrute != etatStable) {
etatStable = lectureBrute;
return etatStable == LOW;
}
return false;
}
void allumerSeul(int broche) {
digitalWrite(ROUGE, broche == ROUGE);
digitalWrite(ORANGE, broche == ORANGE);
digitalWrite(VERT, broche == VERT);
}
void changerEtat(Etat nouvel) {
etat = nouvel;
debutEtat = millis();
}
void setup() {
pinMode(ROUGE, OUTPUT);
pinMode(ORANGE, OUTPUT);
pinMode(VERT, OUTPUT);
pinMode(BOUTON, INPUT_PULLUP);
}
void loop() {
bool appui = appuiDetecte(); // lu à chaque tour, dans tous les états
unsigned long ecoule = millis() - debutEtat;
switch (etat) {
case FEU_VERT: // reste vert tant que personne n'appelle
allumerSeul(VERT);
if (appui) changerEtat(FEU_ORANGE);
break;
case FEU_ORANGE:
allumerSeul(ORANGE);
if (ecoule >= DUREE_ORANGE) changerEtat(FEU_ROUGE);
break;
case FEU_ROUGE:
allumerSeul(ROUGE);
if (ecoule >= DUREE_ROUGE) changerEtat(FEU_VERT);
break;
}
}

Le bouton est lu à chaque tour, dans tous les états, par l'anti-rebond du chapitre 5. Simulé, ce programme reste au vert indéfiniment sans appui ; un appui donné à 8 s fait passer à l'orange à 8,036 s, les 36 ms venant des rebonds puis des 30 ms de stabilité exigées, au rouge à 9,036 s, et rend le vert à 14,036 s.

Étape 3 : les piétons et la sécurité

Le cahier des charges complet ajoute un feu piéton et quatre règles.

  • Le feu piéton n'est jamais vert tant que les voitures ont le vert ou l'orange.
  • Les voitures gardent le vert au moins quatre secondes : un appel trop précoce attend.
  • Un appel donné à n'importe quel moment, même pendant une traversée, est mémorisé et servi au cycle suivant.
  • La fin de traversée est signalée par un vert piéton clignotant, puis une seconde où tous les feux sont rouges avant de rendre le vert aux voitures.
Graphe orienté et pondéré5 sommets, 5 arêtes
demande et 4 s1 s4 s2 s1 sVOITURES_VERTVOITURES_ORANGEPIETONS_VERTPIETONS_CLIGNOTANTDEGAGEMENT
Le carrefour complet. La demande est une variable à part, levée par un appui dans n'importe quel état, et consommée à la sortie du vert voitures.
Arduino
const int V_ROUGE = 13, V_ORANGE = 12, V_VERT = 11; // feu des voitures
const int P_ROUGE = 10, P_VERT = 9; // feu des piétons
const int BOUTON = 2;
const unsigned long VERT_MINIMUM = 4000; // les voitures gardent le vert au moins 4 s
const unsigned long DUREE_ORANGE = 1000;
const unsigned long DUREE_TRAVERSEE = 4000;
const unsigned long DUREE_CLIGNOTEMENT = 2000;
const unsigned long DUREE_DEGAGEMENT = 1000; // tous au rouge avant de rendre le vert
const unsigned long DEMI_PERIODE = 250;
const unsigned long STABLE_MS = 30;
enum Etat { VOITURES_VERT, VOITURES_ORANGE, PIETONS_VERT, PIETONS_CLIGNOTANT, DEGAGEMENT };
Etat etat = VOITURES_VERT;
unsigned long debutEtat = 0;
bool demande = false; // un appel piéton en attente
int lectureBrute = HIGH, etatStable = HIGH;
unsigned long dernierChangement = 0;
bool appuiDetecte() {
int lecture = digitalRead(BOUTON);
if (lecture != lectureBrute) {
lectureBrute = lecture;
dernierChangement = millis();
}
if (millis() - dernierChangement >= STABLE_MS && lectureBrute != etatStable) {
etatStable = lectureBrute;
return etatStable == LOW;
}
return false;
}
void feux(int voiture, bool pietonVert) {
digitalWrite(V_ROUGE, voiture == V_ROUGE);
digitalWrite(V_ORANGE, voiture == V_ORANGE);
digitalWrite(V_VERT, voiture == V_VERT);
digitalWrite(P_VERT, pietonVert);
digitalWrite(P_ROUGE, !pietonVert);
}
void changerEtat(Etat nouvel) {
etat = nouvel;
debutEtat = millis();
}
void setup() {
for (int b = P_VERT; b <= V_ROUGE; b++) pinMode(b, OUTPUT); // broches 9 à 13
pinMode(BOUTON, INPUT_PULLUP);
}
void loop() {
if (appuiDetecte()) demande = true; // mémorisé, quel que soit l'état
unsigned long ecoule = millis() - debutEtat;
switch (etat) {
case VOITURES_VERT:
feux(V_VERT, false);
if (demande && ecoule >= VERT_MINIMUM) {
demande = false; // l'appel est servi
changerEtat(VOITURES_ORANGE);
}
break;
case VOITURES_ORANGE:
feux(V_ORANGE, false);
if (ecoule >= DUREE_ORANGE) changerEtat(PIETONS_VERT);
break;
case PIETONS_VERT:
feux(V_ROUGE, true);
if (ecoule >= DUREE_TRAVERSEE) changerEtat(PIETONS_CLIGNOTANT);
break;
case PIETONS_CLIGNOTANT: // le vert piéton clignote : il faut finir de traverser
feux(V_ROUGE, (ecoule / DEMI_PERIODE) % 2 == 0);
if (ecoule >= DUREE_CLIGNOTEMENT) changerEtat(DEGAGEMENT);
break;
case DEGAGEMENT:
feux(V_ROUGE, false);
if (ecoule >= DUREE_DEGAGEMENT) changerEtat(VOITURES_VERT);
break;
}
}

La demande est la clé de cette étape. Un appui ne provoque plus directement une transition : il lève un drapeau, demande = true, quel que soit l'état. Seul l'état VOITURES_VERT le consulte, et seulement une fois le vert minimum écoulé. C'est ce qui permet de ne perdre aucun appel sans jamais écourter le vert des voitures.

250 ms par bit4 bauds48 bits, 12 s en tout
boutonvoitures vertvoitures orangevoitures rougepiétons vertpiétons rougeappuitraverséefin de traverséedégagementvert voitures02 s4 s6 s8 s10 s12 s
Un cycle d'appel simulé, de 9,5 s à 21,5 s, par tranches de 250 ms. Aucun instant ne montre le vert piéton en même temps que le vert ou l'orange des voitures.
Vérifier le comportement, pas seulement le code
Les règles de sécurité ont été vérifiées sur le comportement simulé, toutes les 5 ms pendant 40 secondes, avec des appuis donnés au pire moment : exactement un feu voitures allumé à tout instant, jamais de vert piéton avec le vert ou l'orange des voitures, jamais le rouge et le vert piétons ensemble. Relire le code ne suffit pas à s'en assurer : une règle de sécurité se vérifie sur ce que fait le programme, instant par instant.

Exercices type

Au démarrage de l'étape 3, un piéton appuie au bout d'une seconde. Quand les voitures passent-elles à l'orange ?

À 4 secondes. L'appui lève la demande dès qu'il est validé, vers 1,03 s, mais l'état VOITURES_VERT ne la sert qu'une fois le vert minimum de quatre secondes écoulé depuis son entrée, à l'instant 0.

La simulation le confirme : orange à 4 000 ms exactement.

Un piéton appuie pendant la traversée d'un autre. Son appel est-il perdu ?

Non. L'appui lève demande, qui n'est consultée ni pendant la traversée, ni pendant le dégagement. Au retour du vert voitures, la demande est toujours levée : les voitures passent à l'orange quatre secondes après le retour au vert, dès que le vert minimum est écoulé.

Dans la simulation, un premier appel à 10 s donne l'orange à 10,036 s ; un second, donné à 13 s pendant la traversée, donne un nouvel orange à 22,036 s, soit quatre secondes après le retour du vert à 18,036 s.

Combien de temps les voitures sont-elles arrêtées à chaque appel ?

De la fin du vert au retour du vert : orange 1 s, traversée 4 s, clignotement 2 s, dégagement 1 s, soit 8 secondes.

Les voitures ne sont vraiment à l'arrêt, feu rouge, que pendant les sept dernières. Toutes ces durées sont des constantes en tête du programme : un réglage ne demande de toucher aucune ligne de la machine à états.

Pourquoi l'étape 3 exige-t-elle une seconde où tous les feux sont rouges ?

Un piéton engagé au dernier moment du clignotement est encore sur la chaussée quand celui-ci se termine. Rendre le vert aux voitures à cet instant précis les ferait démarrer vers lui.

L'état DEGAGEMENT laisse une marge. C'est une règle de sécurité, et elle s'écrit comme un état à part entière du diagramme, et non comme un delay glissé entre deux lignes : elle reste ainsi visible, réglable, et vérifiable.

Vérification

Vérification rapideon peut se reprendre

1.Pourquoi une version à base de delay ne peut-elle pas satisfaire l'étape 2 ?

2.À quoi sert la variable demande de l'étape 3 ?

3.Pourquoi l'état VOITURES_VERT attend-il quatre secondes avant de servir une demande ?

4.Comment vérifier qu'un vert piéton ne s'allume jamais avec le vert des voitures ?

5.Que fait la fonction changerEtat ?

La méthode

  1. Écrire le cahier des charges en règles courtes et vérifiables avant tout diagramme.
  2. Dessiner le diagramme d'états, une flèche par transition, avec son événement.
  3. Construire par étapes : un programme qui fonctionne à chaque étape, une exigence de plus à la suivante.
  4. Lire les entrées à chaque tour, dans tous les états, et mémoriser les événements qui ne peuvent pas être servis tout de suite.
  5. Regrouper les durées en constantes, et les états dans un enum.
  6. Vérifier les règles de sécurité sur le comportement, en provoquant les cas défavorables : appui trop tôt, appui pendant la traversée, appuis répétés.

Synthèse

  • Un projet se construit par étapes, chacune donnant un programme qui fonctionne.
  • Le feu est une machine à états ; chaque durée se mesure par millis() - debutEtat.
  • Une version à delay perd les appuis survenus pendant les pauses, et ne peut pas attendre un appel.
  • Le bouton se lit à chaque tour, avec un anti-rebond qui ne bloque pas.
  • Une demande mémorisée découple l'instant de l'appui de l'instant où il est servi.
  • Les règles de sécurité deviennent des états ou des conditions explicites, et se vérifient sur le comportement observé.