Dans l’article L’Arduino au coeur des systèmes de pilotage analogiques ou numériques, on explique l’architecture d’un système de pilotage complet.
Dans mon projet, j’ai choisi de réaliser la gestion complète de mon réseau en utilisant des Arduinos.
Voici donc une présentation de mon programme de gestion du réseau SGDD (Système de Gestion Denis D).
J’insiste sur le fait qu’il s’agit d’une étude, qui permet d’identifier les principaux problèmes et de leur trouver "une" solution.
En particulier, elle n’est pas terminée et je me réserve la possibilité de résoudre les problèmes identifiés d’une façon différente.
Je recommande donc de ne pas démarrer une réalisation personnelle sur la base de cet article, pour le moment.
Mais il est très important de voir quelles questions se posent au niveau des itinéraires complexes et la signalisation. C’est l’intérêt premier de cette série d’articles.
Tout d’abord, voyons un peu la philosophie de ce projet, étape essentielle.
Le but :
Le but de ce système de gestion étant de commander tout un réseau avec des Arduino communiquants, je suis bien conscient que c’est extrêmement ambitieux...
Il s’agit d’une étude prospective dans le cadre de la réalisation de mon futur réseau. Pour le moment cette réalisation se limite au développement du logiciel de gestion présenté ici.
L’intégration avec le réseau réel nécessitera probablement quelques aménagements dont je vous ferai part ultérieurement.
Je me suis "limité" à 100 aiguilles et 155 cantons pour que le pointeur vers l’un de ces éléments tienne dans un octet.
Un élément peut être un canton (index de 1 à 100) ou une aiguille (index de 101 à 255), mais c’est déjà énorme. La plupart du temps, on n’utilisera qu’une infime partie des possibilités.
La longueur du programme ne dépendant pas du nombre d’éléments (on verra comment), pourquoi se priver ?
Concernant la limite pour les itinéraires, il n’y en a pas vraiment. Cela dépend de la configuration du réseau.
Ma gare, représentée sur la figure ci-dessous, compte 180 itinéraires et l’itinéraire le plus long voit 16 aiguilles à la file. Il y a 21 entrées-sorties, 19 TJD (Traversée Jonction Double) et 11 aiguilles.
Figure 1 : Ma future gare
Philosophie du projet :
Un système qui gère tout un réseau, ça existe déjà : si on y met le prix, on peut faire un magnifique réseau tout en DCC en achetant tous les modules qui vont bien chez un ou plusieurs fournisseurs.
On peut même y adjoindre un programme du type "Train Controller" pour améliorer certaines fonctions, mais on rajoute au DCC le prix du logiciel et de l’ordinateur…
Mon idée, c’est d’en faire autant, mais avec un budget très nettement moindre, avec les éléments suivants :
1°) Le chef d’orchestre :
J’ai évidemment pris ce qui se fait de mieux en Arduino : le DUE.
Non seulement il est le plus rapide à cause de son processeur 32 bits, mais il a une autre particularité unique : il possède nativement 2 bus CAN.
Le bus CAN fait l’objet d’un article Mise en oeuvre du Bus CAN entre modules Arduino (1) dans ce site.
2°) Les instrumentistes :
Il faut évidemment des modules pour interfacer le réseau au gestionnaire "chef d’orchestre".
L’idée force est de mettre les modules au plus près du terrain pour que les fils (susceptibles de récupérer des parasites) soient les plus court possibles. Disons 10 à 20 cm comme ordre de grandeur.
Les modules sont ensuite reliés entre eux par un bus CAN.
3°) Les différents systèmes de traction doivent être gérés : DCC ou analogique avec PWM
A) Commençons par le DCC.
Figure 2 : Architecture de l’électronique en mode DCC
On garde, bien sûr, le DCC pour les locos. Mais c’est tout.
La centrale est un Arduino DUE qui reçoit toutes les infos et donne les ordres. Le schéma indique la structure de fonctionnement. D’autres articles en développeront les détails.
Prenons l’exemple d’un itinéraire :
Un train "T", conduit automatiquement, a, en mémoire de la carte DCC, la dernière consigne que le pilote (vous) lui a donné.
Cette consigne c’est "à fond".
Mais l’itinéraire n’est pas formé. Il est donc sagement à l’arrêt devant un carré.
Vous appuyez sur le bouton-poussoir (BP) du TCO correspondant à la voie où il est à l’arrêt.
Le gestionnaire (le DUE) enregistre cette info qui transite par le bus CAN, info qui vient d’un des modules BP du TCO.
Vous appuyez maintenant sur le BP de la voie de destination. Le gestionnaire enregistre aussi cette info. Il a maintenant tout pour calculer l’itinéraire demandé.
Constatant que cet itinéraire est possible, il va commander aux modules aiguilles de mettre les aiguilles en position. Il recevra au fur et à mesure de la construction de l’itinéraire réel une info du module aiguille qui lui dira "je suis en position", jusqu’à ce que cette info vienne de la dernière aiguille concernée par cet itinéraire.
Au passage, il saura que certaines aiguilles de cet itinéraire sont prises en position déviée et limitées à 30 km/h. Il va donc demander au module signal d’afficher ralentissement 30km/h.
Cette info "30 km/h pour le train T" est aussi envoyée à la carte DCC, carte qui va choisir la plus petite des valeurs ("à fond" = consigne DCC demandée) ou ("30 km/h").
Ce minimum est bien évidemment 30 km/h et la carte DCC enverra l’ordre de démarrage du train jusqu’à une vitesse de 30 km/h, avec une accélération donnée par les CV du décodeur de la loco.
Malgré une vitesse donnée comme maximale par la position du potentiomètre de vitesse de la carte DCC, le gestionnaire assure la sécurité.
On note sur le schéma qu’il y a en DCC un rail commun (en noir) et que la présence des trains est détectée par les cartes cantons ET par les cartes aiguilles. C’est indispensable si on veut un vrai PRS, géré également par le système.
Comme on est en DCC, c’est le bus DCC qui commande le courant traction et qui alimente les cantons et les aiguilles.
B) Cas de l’analogique :
Il y aura quelques différences que j’entrevois sans les détailler.
La première différence concerne l’alimentation des rails où il faut maintenant faire des coupures des 2 côtés pour que chaque canton puisse avoir des sens et des alimentations différentes.
Comme la gestion de la vitesse des trains est maintenant directement effectuée par le gestionnaire, mon avis est qu’il faudra probablement utiliser le deuxième bus CAN de la carte DUE, avec un débit plus élevé (c’est du temps réel !)
Enfin, comme il faut alimenter les aiguilles à la bonne tension, même celles qui sont isolées (par exemple les TJD au milieu des grills), il faudra utiliser une astuce pour ne le faire qu’avec un seul contact de relais par aiguille. On y reviendra.
Sinon, l’essentiel du programme doit être sensiblement le même, qu’on soit en DCC ou en analogique. L’alimentation traction étant évidemment différente.
4°) Le programme :
La philosophie du programme est novatrice. C’est même la principale innovation de cette gestion des trains.
Au lieu de raisonner localement, en se demandant quels sont les différents cas qui se posent pour un canton donné, j’applique successivement une règle à tous les cantons via une boucle "for". Ainsi, chaque règle n’est écrite qu’une fois pour tout le réseau, quelle que soit sa taille.
Si mon nombre de cantons évolue, je n’ai qu’un indice à changer. Comme je vais avoir 60 cantons, je ne veux pas écrire 60 fois chaque règle…
Autre innovation fondamentale : je ne décris nulle part les itinéraires possibles (et heureusement, j’en ai 180 !) et encore moins les incompatibilités.
Si on compte que chaque itinéraire est incompatible avec la moitié des itinéraires, on arrive à 180*90 = 16 200 !!
J’ai donc un programme qui se veut universel. Je n’ai qu’à l’adapter au réseau une fois en rentrant quelques données descriptives à l’écriture du programme et tout en découle.
C’est le but de la suite de cet article.
Entrons un peu plus dans les détails :
Dans le gestionnaire, on a des listes d’objets qui définissent des cantons, des aiguilles, des signaux, des itinéraires, un TCO, ...
Dans chaque liste d’objet, on a une partie fixe, définie une fois pour toutes : c’est la description du réseau. On le fait au début, une seule fois, et on n’a plus à y toucher.
Exemple :
Si je marque dans une constante que le canton 3 suit le canton 2, dans le sens horaire, c’est suffisant ... et ça n’évoluera plus.
Puis une autre partie, que je qualifierai de variable, qui évolue avec la position des trains, la position des aiguilles, etc...
Au départ, dans la liste des objets, tous les cantons sont libres et la position des aiguilles est "tout droit".
Je fais une différence entre le contenu initial des variables et la position réelle des aiguilles.
Comme la carte aiguilles va fournir au gestionnaire la vraie position des aiguilles, cet état va se mettre à jour automatiquement.
Puis le gestionnaire interroge les modules :
- Des modules "canton", il récupère l’info d’occupation.
- Des modules "aiguilles", il récupère la position réelle des aiguilles
- Des modules "TCO", il récupère la volonté du pilote (nous !) qui demande, par exemple, un itinéraire.
En fait, le gestionnaire déroule son schéma en récupérant via le bus CAN toutes les infos correspondant aux occupations et aux positions d’aiguilles.
Une fois que c’est fait, on a des listes d’objets à jour.
A ce stade, le gestionnaire peut s’occuper des itinéraires.
Il interroge donc le TCO pour connaître la position des boutons poussoirs.
En connaissant la volonté du pilote (les BP), la position réelle des aiguilles, l’occupation des cantons, il peut alors calculer les itinéraires, les lancer et commander la couleur de tous les signaux du réseau.
Et la couleur des signaux fournit alors une information essentielle : la vitesse maxi que chaque train doit respecter. La sécurité est donc entièrement gérée.
Initialisation :
La définition de ces objets permet de décrire complètement le réseau, via des listes d’objets :
- La liste d’objet "canton" ("Block" en anglais)
- La liste d’objet "aiguille" ("Turnout" en anglais)
- Etc...
Comme c’est un programme assez complexe, je vais décrire sommairement son fonctionnement.
Il n’est pas terminé. Il y manque la partie relations avec les autres modules via le bus CAN, mais je l’ai simulée pour vérifier que le noyau du programme fonctionne bien.
J’ai dû faire un certain nombre de choix (définition d’un canton, choix des itinéraires par boutons poussoir, ...) pour mener à bien ce projet. On peut, bien sûr, en avoir d’autres, mais cela supposera d’adapter le programme à ces choix différents.
Commençons l’étude :
J’utilise, comme la SNCF, la signalisation pour définir la vitesse maximale d’un train.
Il y a donc un objet "Train" dans lequel on sait sur quel canton il est et les 2 cantons devant lui.
Et dans cet objet, le signal qu’il "voit".
Évidemment, il ne voit pas le signal et, d’ailleurs, ce signal est virtuel : c’est une variable.
On peut installer à côté de la voie un vrai signal, parce que ça fait joli, mais ça ne change rien du point de vue du programme.
J’ai écrit mon programme en anglais, un peu comme la plupart des bibliothèques. Une version française suivra peut-être.
Pour mon réseau, la mise à jour des listes d’objets dure 6 ms sur un Arduino DUE, non compris les échanges d’infos via les bus CAN.
Quand j’aurais fini mes essais, une partie des traitements qui sont actuellement dans ce programme migreront au niveau du canton. On en reparlera.
Pour moi, un canton est formé de 3 zones :
- Une zone d’arrêt dans le sens horaire
- Une zone de pleine voie
- Une zone d’arrêt dans le sens trigo (anti-horaire) Il n’y a jamais d’aiguilles dans un canton.
Un objet canton comprend 20 variables :
// Members variables
// Defined once and for all :
byte previous_element; // # of previous element (clockwise)
byte current_block; // # of current block
byte next_element; // # of next element (clockwise)
byte block_level; // level of the block in the station map
byte block_length; // block length (in cm)
byte stop_length; // stop length (clockwise, in cm)
byte conter_stop_length; // stop length (conterclockwise, in cm)
// Depending of trains and turnouts positions :
byte previous_block; // # of previous block (clockwise)
byte next_block; // # of next block (clockwise)
byte signal; // color of the signal (clockwise)
byte conter_signal; // color of the signal (conterclockwise)
boolean busy; // busy (true) or not (false)
boolean train_direction; // train direction (clockwise (false) or conterclockwise (true))
boolean clw_stop_zone_status; // busy (true) or not (false)
boolean plain_track_status; // busy (true) or not (false)
boolean cclw_stop_zone_status; // busy (true) or not (false)
boolean clw_stop_zone_status_0; // busy (true) or not (false)
boolean plain_track_status_0; // busy (true) or not (false)
boolean cclw_stop_zone_status_0; // busy (true) or not (false)
Définies une fois pour toutes (elles correspondent à la topographie du réseau) :
- L’élément précédent. Ce n’est pas le canton précédent puisque ça peut être une aiguille.
- Le canton courant.
- L’élément suivant. Même remarque que pour l’élément précédent.
- La longueur de la zone d’arrêt dans le sens horaire
- La longueur de la pleine voie
- La longueur de la zone d’arrêt dans le sens trigo
- Le niveau du canton. Je développerai plus tard cette notion fondamentale.
Les variables calculées (vie du réseau) :
- Le canton précédent. Le vrai, celui qui peut être au delà d’un grill d’aiguilles
- Le canton suivant. Le vrai.
- Le signal sens horaire
- Le signal sens trigo
- La direction du train sur ce canton
- Les 6 autres variables seront à mettre au niveau du canton. Ce sont les occupations des 3 zones (mémo, puis courante). J’y reviendrai.
Un objet aiguille comprend 10 variables :
// Members variables
// Defined once and for all :
byte native_side; // # previous element on the native side
byte current_turnout; // # of current turnout
byte straight_path; // # of next element after straight path
byte diverging_path; // # of next element after diverging path
byte turnout_level_straight; // level of the turnout to the straight path in the station map
byte turnout_level_diverging; // level of the turnout to the diverging path in the station map
byte turnout_max_speed; // max speed on diverging path : 255, 160, 60, 30 (km/h)
// Depending of trains and turnouts positions :
boolean turnout_path; // turnout path : STRAIGHT (false) or DIVERGING (true)
byte turnout_status; // turnout status : FREE (0), RESERVED (1), BUSY (2)
boolean train_direction; // train direction : CLOCKWISE (false) or CONTERCLOCKWISE (true)
Définies une fois pour toutes (elles correspondent à la topographie du réseau) :
- L’élément précédant l’aiguille en pointe
- L’aiguille courante
- L’élément suivant en position tout droit
- L’élément suivant en position déviée
- La vitesse maxi admissible en position déviée
- Le niveau en position tout droit
- Le niveau en position déviée
Les variables calculées (vie du réseau) :
- La position de l’aiguille (TD ou DV)
- Le statut de l’aiguille (Libre, réservée, occupée)
- La direction du train sur cette aiguille
Et c’est tout.
Il y aura d’autres objets, mais plus besoin de description du réseau.
En particulier, aucune définition des itinéraires, aucune définition des incompatibilités.
J’ajoute une autre notion, rare : la définition des priorités d’itinéraires.
Cela correspond à la notion intuitive que la voie vers Paris est plus souvent sollicitée que la voie vers Souains-Perthes-les-Hurlus (51600).
Après avoir défini les premiers objets de mon programme, je vais indiquer la méthode de remplissage pour l’initialisation.
Voici un exemple de réseau à décrire :
(Plan téléchargeable pour une meilleure lisibilité)
Figure 3 : Un réseau exemple
On remarque plusieurs choses :
- Les aiguilles sont numérotés de 1 à 100 maxi (ici : 21 aiguilles).
- Les cantons sont numérotés de 101 à 255 (ici : 31 cantons)
- La flèche rouge indique le sens horaire
Chaque aiguille est isolée et une TJD est traitée comme 2 aiguilles tête-bêche. C’est d’ailleurs le cas chez PECO, par exemple.
Donc on a sur le réseau une TJD et sur ce plan 2 aiguilles (21-14, 13-12, 11-10 et 9-8)
Et, en rouge, les niveaux qui me serviront pour les itinéraires.
Commençons par remplir les objets cantons :
Pour définir "précédent" et "suivant", on respectera le sens horaire et ce, pour tous les cantons.
- Canton 101 : son élément précédent est 0 (un butoir) et son élément suivant 102.
- Canton 102 : son élément précédent est 101 et son élément suivant 103.
- Canton 103 : son élément précédent est 102 et son élément suivant 19 (c’est une aiguille).
- Canton 104 : son élément précédent est 19 et son élément suivant 0 (un butoir).
Puis 1 — (105) — 3 et 1 — (106) — 4 etc.... et on finit par 17 — (121) — 0.
C’est là qu’on voit bien la différence entre "élément" et "canton" (voir le début de l’article).
Remplissons maintenant les objets aiguilles :
Là, on ne s’occupe pas de sens horaire ou autre : toutes les aiguilles sont décrites dans le même ordre :
Aiguille 1 :
- côté pointe, c’est l’élément 130.
- côté talon, direction tout droit, c’est l’élément 106.
- côté talon, direction déviée, c’est l’élément 105.
Puis 131 — (2) — 107 — 108 et 105 — (3) — 4 — 19 etc ... jusqu’à 14 — (21) — 130 — 20.
Maintenant, venons-en aux niveaux, clé de voûte des itinéraires
Quand le plan du réseau est dessiné sous cette forme, c’est presque évident :
On part de 0 (en bas à droite, en rouge) et on monte de 2 en 2, en suivant les niveaux des voies, jusqu’à 10.
Les niveaux sont des vrais niveaux, c’est à dire que la voie la plus "en bas" est au niveau 0.
La voie dessinée juste au dessus est au niveau 2
La voie "du dessus" est au niveau 4
Etc...
Pour aller du niveau 0 au niveau 6, il faut monter. On verra comment avec les aiguilles.
Et si le canton 131 a le niveau 8 d’un côté, il aura le niveau 8 de l’autre.
De même le canton 130 a le niveau 6 des 2 côtés. On en déduit les derniers niveaux manquants en haut.
Dans cet exemple, on a fini la description des niveaux.
Il faut simplement les inscrire dans les objets.
Dans les cantons, c’est évident, il n’y a qu’un niveau.
Pour les aiguilles, je prends un exemple :
Aiguille 14 : Le niveau tout droit vaut 6, le niveau dévié vaut 5 (en fait, entre 4 et 6).
Dit autrement, si on est au niveau 6 et qu’on veut aller au niveau 6, on ira tout droit.
Si on est au niveau 6 et qu’on veut aller au niveau 4, il faut "descendre", c’est à dire prendre la voie déviée vers le bas.
Évidemment, pour la gare du haut il faudrait retourner la feuille pour qu’on ait des niveaux dans le bon sens :
Exemple de l’aiguille 1 : Le niveau tout droit vaut 4 et le niveau dévié vaut 3.
Si je suis au niveau 4 et que je veux aller au niveau 4, je vais tout droit
Si je suis au niveau 4 et que je veux aller voie 105 (niveau 2), je dois "descendre", c’est à dire la voie déviée.
A noter que si la gare du haut avait été plus large que la gare du bas, mon précédent raisonnement aurait amené des niveaux négatifs, ce qui n’a pas de sens.
Il faut alors démarrer de plus haut.
On pourrait fort bien démarrer pour le canton 121 par un niveau 10, décalant ainsi tous les niveaux d’autant. Cela ne changerait rien sur le fonctionnement.
On a une grande latitude pour choisir les bons niveaux SAUF qu’il faut que la notion de niveau soit respectée : un niveau 6 est au-dessus d’un niveau 4, lui même au dessus d’un niveau 2.
Et avec 100 aiguilles maximum, on ne dépassera pas les 256 niveaux permis par un byte.
Dernière remarque sur les cantons et aiguilles :
Vous avez certainement remarqué que les aiguilles et cantons peuvent être numérotés dans le désordre.
On a, là aussi, toute latitude quant à la numérotation SAUF qu’il ne faut pas que ces numérotations aient des trous. C’est impératif.
Voilà donc le résultat complet du réseau.
Liste des objets Block (= canton) du réseau :
Block blockList[MAX_BLOCK] =
{ Block( 0,101,102, 2),
Block(101,102,103, 2),
Block(102,103, 19, 2),
Block( 19,104, 0, 2),
Block( 1,105, 3, 4),
Block( 1,106, 4, 6),
Block( 2,107, 5, 8),
Block( 2,108, 5,10),
Block( 4,109,111, 6),
Block( 5,110,112, 8),
Block(109,111,113, 6),
Block(110,112,114, 8),
Block(111,113,116, 6),
Block(112,114,115, 8),
Block(114,115,117, 8),
Block(113,116,118, 6),
Block(115,117, 6, 8),
Block(116,118, 8, 6),
Block( 0,119, 18, 4),
Block( 0,120, 18, 2),
Block( 0,121, 17, 0),
Block( 17,122, 10, 4),
Block( 7,123, 15,10),
Block( 7,124, 16, 8),
Block( 9,125, 14, 6),
Block( 11,126, 12, 4),
Block( 11,127, 12, 2),
Block( 15,128, 0,10),
Block( 13,129, 0, 4),
Block( 21,130, 1, 6),
Block( 20,131, 2, 8)
};
MAX_BLOCK est à mettre à 31.
Liste des objets Turnout (= aiguille) du réseau :
Turnout turnoutList[MAX_TURNOUT] =
{ Turnout(130, 1,106,105, 6, 5, 30),
Turnout(131, 2,107,108, 8, 9, 60),
Turnout(105, 3, 4, 19, 4, 3, 30),
Turnout(109, 4,106, 3, 6, 5, 30),
Turnout(110, 5,107,108, 8, 9, 60),
Turnout(117, 6, 7, 8, 8, 7, 30),
Turnout( 6, 7,124,123, 8, 9, 30),
Turnout( 9, 8,118, 6, 6, 7, 30),
Turnout( 8, 9,125, 10, 6, 5, 30),
Turnout( 11, 10,122, 9, 4, 5, 30),
Turnout( 10, 11,126,127, 4, 3, 30),
Turnout( 13, 12,126,127, 4, 3, 30),
Turnout( 12, 13,129, 14, 4, 5, 30),
Turnout( 21, 14,125, 13, 6, 5, 30),
Turnout(123, 15,128, 16,10, 9, 30),
Turnout( 20, 16,124, 15, 8, 9, 30),
Turnout(122, 17, 18,121, 4, 0, 30),
Turnout( 17, 18,119,120, 4, 2, 30),
Turnout(104, 19,103, 3, 2, 3, 30),
Turnout(131, 20, 16, 21, 8, 7, 30),
Turnout( 14, 21,130, 20, 6, 7, 30)
};
MAX_TURNOUT est à mettre à 21.
J’ai, par ailleurs, supposé que, pour toutes les aiguilles, la vitesse maxi en voie déviée était à 30 km/h, sauf les aiguilles 2 et 5 qui ont une vitesse maxi de 60 km/h en voie déviée.
On notera l’aspect compact des données spécifiques puisqu’il ne reste que 4 nombres à définir pour que le programme soit complètement adapté au réseau !