Archives pour la catégorie Électronique

Réseau hivernal

Il y a quelques temps, j’ai commencé un nouveau projet de réseau, toujours à l’échelle N, mais de plus petite taille que le premier, soit 120 * 75 cm.

Le réseau représentera une ligne secondaire ainsi qu’une ligne de bus pour atteindre le village depuis la gare. Pour la ligne de bus, j’ai choisi d’utiliser le système Magnorail développé par des modélistes Hollandais (www.magnorail.com). Il s’agit d’une chaîne comportant des aimants pour entraîner n’importe quel véhicule. Ma première idée était d’implanter une ligne de train à crémaillère pour atteindre le village, mais mon premier réseau en a déjà une.

Enfin, le paysage sera hivernal.

Base et menuiserie

Le plan est tracé sur du papier, les éléments de profil dans du carton.

imag0180

Grâce à l’aide précieuse d’un ami menuisier, l’infrastructure prend rapidement forme.

imag0181

Les voies sont posées, le premier train peut circuler. On distingue la gare cachée en arrière-plan.

petitreseau_02

Bâtiments et système Magnorail

Les bâtiments proviennent de Manfred Feiss, un artiste Bâlois qui réalise de magnifiques réseaux à l’échelle N avec des bâtiments en carton. On peut télécharger quelques PDF pour tester et acheter d’autres bâtiments imprimés sur papier rigide 200 g / m2. Pour plus de détails:  www.9-mm.ch, en allemand.

Les photos ci-après permettent aussi de voir les premiers travaux d’installation du système Magnorail. Initialement, j’avais pensé implanter 2 boucles avec un tunnel au milieu. Il n’y aura finalement qu’une seule boucle entraînée par 2 moteurs.

dsc_3172_forumdsc_3174_forum

L’un des 2 moteurs d’entraînement.

dsc_3178_forum

De petits aimants sont insérés dans l’un des maillons.

dsc_3181_forum

Un patin comportant aussi des aimants est fixé sous le véhicule.

dsc_3184_forum

Des tasseaux de bois seront fixés de part et d’autre du guide chaîne pour arriver au niveau de ce dernier. Je fixerai ensuite une route en papier rigide qui cachera la chaîne.

dsc_3183_forum

Arrêts de bus

Il y aura 2 arrêts de bus, l’un à la gare, l’autre au village. Pour réaliser les arrêts de bus, j’ai choisi d’alimenter les moteurs à l’aide d’un décodeur DCC initialement prévu pour les locomotives N. L’important est qu’il dispose de la fonction ABC (Automatic Brake Control) inventée par Lenz, mais aussi disponible sur d’autres décodeurs. J’ai choisi un modèle ESU.

Vu de la centrale, une Trix MS1, le système Magnorail est comme une loco supplémentaire. La consommation des moteurs Magnorail, 50 mA chacun et la circulation de 2 trains, permettront de rester dans les limites permises par la centrale MS1 capable de délivrer 1.9 A !

Le schéma du montage est le suivant. Un monostable à 555 est déclenché par l’un  des 2 ILS placés aux arrêts. Le 555 commande un relais qui, à l’état de repos, court circuite les 5 diodes et, à l’état actif, les met en fonction, créant ainsi l’asymétrie requise par le décodeur. Ce dernier interprète l’asymétrie du signal DCC comme un ordre de freinage et d’arrêt. Les valeurs du condensateur et des résistances, dont un potentiomètre, donnent des durées d’arrêt comprises entre ~15 et 40 secondes.

monostable_bus

La vidéo ci-après montre le résultat obtenu. À noter qu’il a été nécessaire d’insérer un aimant plus fort dans l’un des maillons précédant le bus pour activer les ILS, les aimants d’origine n’y parvenant pas.

Publicités

Mon réseau: panneau de contrôle (TCO)

Introduction

Bien qu’exploitant mon réseau en analogique, un peu d’électronique et un microcontrôleur permettent d’assurer une exploitation en toute sécurité, pratiquement sans aucun risque de collision !

Le panneau de contrôle ou TCO dans le jargon (tableau de contrôle optique) se compose des éléments suivants:

reseau10

  • À gauche: la commande des aiguillages et des alimentations de la gare.
  • À droite: 4 régulateurs traction pour: la voie à crémaillère, la zone de manoeuvre, la boucle extérieure, la boucle intérieure.
  • En haut: la commande va-et-vient pour la voie à crémaillère.

Commande des aiguillages et alimentations

États du système

Le réseau est constitué de 2 boucles: intérieure et extérieure (cf. Mon réseau: plan et conception). La boucle intérieure est alimentée par le régulateur rouge, celui le plus à droite, la boucle extérieure par le régulateur bleu, 2ème en partant de la droite. Le régulateur jaune, 3ème en partant de la droite, peut alimenter la zone de manoeuvre.

État initial

tco110

  • Les traversées jonction double (TJD) 1 et 2 sont en position droite.
  • Les aiguilles  1 et 2 (AIG) sont en position droite.
  • La voie 2 est alimentée par le régulateur de la boucle intérieure.
  • La voie 3 est alimentée par le régulateur de la boucle extérieure.

Train de la boucle intérieure en voie 1

tco210

  • Les TJD sont en position déviée.
  • Les aiguilles sont toujours en position droite.
  • La voie 1 est alimentée par le régulateur de la boucle intérieure.
  • La voie 2 n’est pas alimentée, on peut donc y laisser un train à l’arrêt.
  • La voie 3 est toujours alimentée par le régulateur de la boucle extérieure.

Train de la boucle extérieure en voie 2

tco310

  • Les TJD sont en position déviée.
  • Les AIG sont en position déviée.
  • La voie 1 n’est pas alimentée.
  • La voie 2 est alimentée par le régulateur de la boucle extérieure.
  • La voie 3 est alimentée par le régulateur de la zone de manoeuvre.

La zone de manoeuvre est isolée du reste du réseau pour éviter les collisions. Les trains arrivant de la boucle intérieure sont bloqués à l’entrée de la gare.

Train de la boucle extérieure sur la voie 1

tco410

  • Les TJD sont en position droite.
  • Les aiguilles sont en position déviée.
  • La voie 1 est alimentée par le régulateur de la boucle extérieure.
  • La voie 2 n’est pas alimentée.
  • La voie 3 est alimentée par le régulateur de la zone de manoeuvre.

Dans ce cas aussi la zone de manoeuvre est isolée et les trains de la boucle intérieure sont bloqués à l’entrée en gare.

Les 2 derniers états

Envoyons le train de la boucle intérieure en voie 2 tout en continuant à manoeuvrer avec le régulateur jaune. Cela permet d’avoir un 3ème train en mouvement sur la boucle extérieure.

tco510

  • Les TJD sont en position droite.
  • Les aiguilles sont en position droite.
  • La voie 1 n’est pas alimentée.
  • La voie 2 est alimentée par le régulateur de la boucle intérieure.
  • La voie 3 est alimentée par le régulateur de la zone de manoeuvre.

Comme dans les 2 cas précédents, La zone de manoeuvre est isolée. Par contre, les trains venant de la boucle extérieure sont bloqués à l’entrée de la gare.

Le dernier cas, vous l’aurez certainement deviné, consiste à envoyer le train de la boucle intérieure en voie 1.

tco610

  • Les TJD sont en position déviée.
  • Les aiguilles sont en position droite.
  • La voie 1 est alimentée par le régulateur de la boucle intérieure.
  • La voie 2 n’est pas alimentée.
  • La voie 3 est alimentée par le régulateur de la zone de manoeuvre.

Avec cette manière de procéder, les trains ne passent jamais d’une alimentation à une autre car on change dynamiquement l’alimentation des voies de gare selon les positions des TJD et aiguilles. Les modélistes anglophones parlent de cab control et les germanophones de Z-Schaltung. Je n’ai pas trouvé de terme français équivalent !

Réalisation

La base du TCO est une plaque d’aluminium. La partie blanche est produite à l’aide de PowerPoint (certainement que tout autre logiciel de dessin conviendrait aussi) puis imprimée. Le dessus est un plastique transparent (plexiglas ou autre). Le plus difficile était de percer la plaque d’aluminium et celle de plastique à l’identique. J’ai procédé comme suit:

  • Faire une version spéciale du dessin comportant les endroits où percer.
  • Percer la plaque d’aluminium en servant du dessin avec le plan de perçage.
  • Fixer le plastique transparent sur la plaque d’aluminium à l’aide de scotch.
  • Retourner le tout et percer le plastique transparent en servant de la plaque d’aluminium comme gabarit de perçage.

L’électronique est implantée sur 2 plaques à bandes (Veroboard).

reseau11

  • sur la platine de gauche, un microcontrôleur Attiny2313 et les relais (petits boîtiers bleus) de commande des aiguillages,
  • sur la platine de droite, les relais de distribution des alimentations en gare et zone de manoeuvre.

Les schémas correspondants sont:

Microcontrôleur

Les 6 poussoirs sur la droite correspondent aux poussoirs de couleurs du TCO avec la convention suivante:

  • A1: poussoir rouge voie 1.
  • A2: poussoir rouge voie 2.
  • B1: poussoir bleu voie 1.
  • B2: poussoir bleu voie 2.
  • B3: poussoir bleu voie 3.
  • C3: poussoir jaune voie 3.

tco_savignyexpressscan-161009-0001

Leds du TCO

Pour éviter d’utiliser des sorties supplémentaires pour les leds qui affichent l’état des voies de gare au TCO, le montage suivant permet d’allumer les leds en fonction des 3 sorties de commande des relais.

tco_savignyexpressscan-161009-0002

Relais

L’alimentation A correspond au régulateur de la boucle intérieure. L’alimentation B à celui de la boucle extérieure et l’alimentation C à celui de la zone manoeuvre. Cette convention est reprise dans le programme C.

Les 2 voies de chaque côté de la gare comportent des zones d’arrêt. Dans le cas de la voie extérieure, l’aiguille est incluse dans la zone afin de simplifier le câblage. Ces zones d’arrêt permettent d’arrêter les trains entrants selon l’état de la gare (cf la description des états ci-avant). Ces zones d’arrêt ne fonctionnent bien qu’avec des trains classiques, tirés par une locomotive, car la prise de courant se fait en tête de train. Pour des trains réversibles (loco en pousse) ou des rames telles que la prise de courant se fait sur tous les essieux, ces zones d’arrêt ne conviennent pas. Ce n’est pas un problème sur mon réseau époque III / IV où ne circulent que des trains classiques. 

tco_savignyexpressscan-161009-0003

Programme en C

Dans le programme en C du microcontrôleur, les conventions suivantes sont suivies:

  • 1, 2, 3: numéros des voies en partant de la gare.
  • A, B: alimentations traction du circuit intérieur et du circuit extérieur. En rouge, respectivement bleu sur le TCO. Couleurs correspodant aussi aux boutons poussoir.
  • C: alimentation traction de la zone marchandise, peut être commutée avec l’alimentation B. En jaune sur la description des états du TCO. Couleur correspondant aussi au bouton poussoir.
  • B_A1, B_A2, etc: pressions sur le poussoir A1, A2, etc. Le bouton A1 alimente la voie 1 avec le circuit A (rouge) et ainsi de suite.
  • Les états sont nommés: AXB, XAB, AXC, etc. Il s’agit des états possibles des 3 voies de gare:
    • AXB: voie 1 alimentée par A, voie 2 non alimentée, voie 3 alimentée par B.
    • XAB: voie 1 non alimentée, voie 2 alimentée par A, voie 3 alimentée par B.
    • AXC: voie 1 alimentée par A, voie 2 non alimentée, voie 3 alimentée par C.
  • La commande des aiguillages se fait par la fonction CommanderTdjAig dont le but est de commander séparément les TJDs et aiguillages de la gare, ce qui évite de tirer trop de courant de l’alimentation.
  • Il y a aussi quelques fonctions qui ne servent qu’à simuler le programme en le faisant tourner sur PC: GetTexteEvt, GetTexteEtat, GetTexteAiguillage, AfficherEtat.

La structure générale du programme est une boucle infinie, comme toujours sur un microcontrôleur où un programme ne se termine jamais, dont la structure est la suivante:

  • Lire les entrées, les boutons poussoirs en l’occurrence.
  • Déterminer le nouvel état du système et des sorties.
  • Mettre à jour les sorties.
  • Attendre quelques dizaines de millisecondes.
  • Revenir à la lecture des entrées.

Les fonctions principales du programme, appelées dans la boucle principale, sont:

  • TraiterEtat: gestion de la machine d’états pour les 6 états du TCO décrits plus haut dans ce post. Cette fonction appelle ensuite DefinirSorties.
  • DefinirSorties: détermination des sorties en fonction de l’état et mise à jour des sorties. S’il faut commander les TJD et les aiguillages, cette fonction initialise la machine d’états implantée dans la fonction CommanderTjdAig décrite ci-après.
  • CommanderTjdAig: gestion de la machine d’états pour la commande des TJD et aiguillages. Elle commande d’abord les 2 TJD, puis les 2 aiguillages afin d’éviter que les électro-aimants ne tirent trop de courant de l’alimentation accessoires.

Ce programme comporte des fonctions utilisées que pour la mise au point sur PC. À cet effet, 2 directives de compilation sont utilisées:

  • PC compile les appels d’affichage sur écran lorsque le programme est compilé pour PC avec gcc.
  • AVR permet de compiler tout ce qui est spécifique à l’AVR, principalement les entrées-sorties, lorsque le programme est compilé avec avr-gcc.

Avec cette astuce, il est possible de mettre au point la logique du programme en grande partie sur le PC avant de télécharger le programme en version AVR vers le microcontrôleur.

Cliquer sur expand source ci-après pour voir le programme complet.

//
//  Maquette.c
//
//  Programme de contrôle de la maquette "Savigny Express".
//
//  2009.09.11  MHP  Création.
//  2009.10.06  MHP  Simplificatin de la machine d'états.
//  2009.10.10  MHP  Début des tests avec les E/S de l'AVR.
//  2009.10.14  MHP  Commande momentanée des aiguillages.
//  2009.11.04  MHP  Debug amélioré de la commande des aiguillages.
//  2009.11.08  MHP  Ajout de la commande du bit bSela12, oubli !
//  2010.02.11  MHP  Correction des reactions a l'evenement C3.
//
//  Directives de compilation: PC ou AVR.
//

#ifdef AVR
#include <inttypes.h>
#include <avr/io.h>

#define F_CPU 1000000UL
#include <util/delay.h>

// Période en ms.
#define  PERIODE  50

// Boutons poussoirs.
#define BOUTONS  PINB
#define B_A1  _BV(PD6)
#define  B_A2  _BV(PB0)
#define B_B1  _BV(PB1)
#define B_B2  _BV(PB2)
#define  B_B3  _BV(PB3)
#define  B_C3  _BV(PB4)
#define P_A1  (!(PIND & B_A1))
#define P_A2  (!(PINB & B_A2))
#define P_B1  (!(PINB & B_B1))
#define P_B2  (!(PINB & B_B2))
#define P_B3  (!(PINB & B_B3))
#define P_C3  (!(PINB & B_C3))

// Sorties.
#define  SetTJD_D  (PORTD |= _BV(PD0))
#define ClrTJD_D  (PORTD &= ~_BV(PD0))
#define  SetTJD_N  (PORTD |= _BV(PD1))
#define  ClrTJD_N  (PORTD &= ~_BV(PD1))

#define SetAIG_D  (PORTA |= _BV(PA1))
#define ClrAIG_D  (PORTA &= ~_BV(PA1))
#define  SetAIG_N  (PORTA |= _BV(PA0))
#define ClrAIG_N  (PORTA &= ~_BV(PA0))

#define SetSelAB  (PORTD |= _BV(PD2))
#define ClrSelAB  (PORTD &= ~_BV(PD2))
#define SetSel12  (PORTD |= _BV(PD3))
#define ClrSel12  (PORTD &= ~_BV(PD3))
#define SetSel3      (PORTD |= _BV(PD4))
#define ClrSel3      (PORTD &= ~_BV(PD4))
#define  SetSela12  (PORTD |= _BV(PD5))
#define  ClrSela12  (PORTD &= ~_BV(PD5))

// Configuration des ports
#define CONFIG_DDRA  (_BV(PA0)|_BV(PA1))

#define  CONFIG_DDRB  ~(B_A2|B_B1|B_B2|B_B3|B_C3);
#define CONFIG_PULLUPB  (B_A2|B_B1|B_B2|B_B3|B_C3);

#define CONFIG_DDRD  ~(B_A1)
#define CONFIG_PULLUPD  B_A1
#endif

#ifdef PC
#include  <stdio.h>
#endif

#define  NB_PERIODES  4

// Etats: nommés à partir de l'état
// des voies de gares 1, 2 et 3.
enum tEtat {AXB, XAB, AXC, XAC, BXC, XBC};
enum tEtat eEtat;

// Evénements: nommés à partir de la source à connecter à chaque voie.
enum tEvt {A1, B1, A2, B2, B3, C3};

// Commandes des relais.
short bSelAB;
short bSel12;
short bSel3;
short bSela12;

// Positions et commandes des aiguillages.
enum tPosition {NORMAL, DEVIE};
enum tPosition ePosJonctions;
enum tPosition ePosAiguillages;

enum tCommandeAig {IDLE, TJD, AIG} eCommandeAig;
short nNbPeriodes;

// Pour compter les tests en mode PC.
#ifdef PC
int nTest;
#endif

char *GetTexteEvt(enum tEvt xEvt)
{
    #ifdef PC
    switch(xEvt)
    {
        case A1: return("A1");
        case B1: return("B1");
        case A2: return("A2");
        case B2: return("B2");
        case B3: return("B3");
        case C3: return("C3");
    } // switch
    #endif
} // GetTexteEvt

char *GetTexteEtat(enum tEtat xEtat, short xVoie)
{
    #ifdef PC
    switch(xEtat)
    {
        case AXB:
            if(xVoie == 1)
                return("A");
            if(xVoie == 2)
                return("X");
            if(xVoie == 3)
                return("B");

        case XAB:
            if(xVoie == 1)
                return("X");
            if(xVoie == 2)
                return("A");
            if(xVoie == 3)
                return("B");

        case AXC:
            if(xVoie == 1)
                return("A");
            if(xVoie == 2)
                return("X");
            if(xVoie == 3)
                return("C");

        case XAC:
            if(xVoie == 1)
                return("X");
            if(xVoie == 2)
                return("A");
            if(xVoie == 3)
                return("C");

        case BXC:
            if(xVoie == 1)
                return("B");
            if(xVoie == 2)
                return("X");
            if(xVoie == 3)
                return("C");

        case XBC:
            if(xVoie == 1)
                return("X");
            if(xVoie == 2)
                return("B");
            if(xVoie == 3)
                return("C");
    } // switch
    #endif
} // GetTexteEtat

char *GetTexteAiguillage(enum tPosition xPos)
{
    #ifdef PC
    switch (xPos)
    {
        case NORMAL:  return("N");
        case DEVIE:  return("D");
    } // switch
    #endif
} // GetTexteAiguillage

void AfficherEtat(void)
{
#ifdef PC
#define V1 GetTexteEtat(eEtat, 1)
#define V2 GetTexteEtat(eEtat, 2)
#define V3 GetTexteEtat(eEtat, 3)
#define J (bSelAB ? "B" : "A")
#define A (bSela12 ? "X" : "B")
#define P2 (bSelAB ? "X" : "A")
#define P3 (bSel3 ? "X" : "B")
#define PJ GetTexteAiguillage(ePosJonctions)
#define PA GetTexteAiguillage(ePosAiguillages)
#define FA (bSelAB ? "R" : "V")
#define FB (bSela12 ? "R" : "V")

    printf("          /------%s------\\      \n", V1);
    printf("%s----%s--%s%s-------%s------%s%s--%s--%s\n", FA, P2, J, PJ, V2, PJ, J, P2, FA);
    printf("%s------%s%s%s-------%s-------%s%s%s---%s\n", FB, A, PA, P3, V3, P3, PA, A, FB);
    printf("\n");
#endif
} // AfficherEtat

void CommanderTjdAig(void)
{
    switch (eCommandeAig)
    {
        case TJD:
            if (nNbPeriodes == 0)
            {
                #ifdef AVR
                ClrTJD_D;
                ClrTJD_N;
                ClrAIG_D;
                ClrAIG_N;
                (ePosJonctions == DEVIE ? SetTJD_D : SetTJD_N);
                #endif

                #ifdef PC
                (ePosJonctions == DEVIE ? printf("\tTJD DEV\n") : printf("\tTJD NOR\n"));
                #endif
            }
            else if (nNbPeriodes == (NB_PERIODES / 2))
            {
                #ifdef AVR
                ClrTJD_D;
                ClrTJD_N;
                (ePosAiguillages == DEVIE ? SetAIG_D : SetAIG_N);
                #endif

                #ifdef PC
                printf("\tTJD --\n");
                (ePosAiguillages == DEVIE ? printf("\tAIG DEV\n") : printf("\tAIG NOR\n"));
                #endif

                eCommandeAig = AIG;
            } // if
            nNbPeriodes++;
        break;

        case AIG:
            if (nNbPeriodes == NB_PERIODES)
            {
                #ifdef AVR
                ClrTJD_D;
                ClrTJD_N;
                ClrAIG_D;
                ClrAIG_N;
                #endif

                eCommandeAig = IDLE;

                #ifdef PC
                printf("\tAIG --\n");
                #endif

                nNbPeriodes = 0;
            }
            else
                nNbPeriodes++;
        break;

        case IDLE:
        break;
    } // switch
} // CommanderTjdAig

void DefinirSorties(void)
{
    switch(eEtat)
    {
        case AXB :
            bSelAB  = 0;
            bSel12  = 1;
            bSela12  = 0;
            bSel3  = 0;
            ePosJonctions  = DEVIE;
            ePosAiguillages  = NORMAL;
        break;

        case XAB:
           bSelAB  = 0;
           bSel12  = 0;
           bSela12  = 0;
           bSel3  = 0;
           ePosJonctions  = NORMAL;
           ePosAiguillages  = NORMAL;
        break;

        case AXC:
            bSelAB  = 0;
            bSel12  = 1;
            bSela12  = 1;
            bSel3  = 1;
            ePosJonctions  = DEVIE;
            ePosAiguillages  = NORMAL;
        break;

        case XAC:
            bSelAB  = 0;
            bSel12  = 0;
            bSela12  = 1;
            bSel3  = 1;
            ePosJonctions  = NORMAL;
            ePosAiguillages  = NORMAL;
        break;

        case BXC:
            bSelAB  = 1;
            bSel12  = 1;
            bSela12  = 0;
            bSel3  = 1;
            ePosJonctions  = NORMAL;
            ePosAiguillages  = DEVIE;
        break;

        case XBC:
            bSelAB  = 1;
            bSel12  = 0;
            bSela12  = 0;
            bSel3  = 1;
            ePosJonctions  = DEVIE;
            ePosAiguillages  = DEVIE;
        break;
    } // switch

    #ifdef AVR
    (bSelAB ? SetSelAB : ClrSelAB);
    (bSel12 ? SetSel12 : ClrSel12);
    (bSel3 ? SetSel3 : ClrSel3);
    (bSela12 ? SetSela12 : ClrSela12);
    #endif
    eCommandeAig = TJD;
} // DefinirSorties

void SimulerTimeouts(void)
{
#ifdef PC
int nNbTimeouts;

    // On simule quelques timeouts
    for(nNbTimeouts = 0; nNbTimeouts < 8; nNbTimeouts++)
        CommanderTjdAig();
#endif
} // SimulerTimeouts

void TraiterEtat(enum tEvt xEvt)
{
enum tEtat  eProchEtat;

    #ifdef PC
    printf("%02d: Evénement: %s\n", ++nTest, GetTexteEvt(xEvt));
    #endif

    switch(eEtat)
    {
        case AXB:
            if (xEvt == A1) eProchEtat = AXB;
            if (xEvt == B1) eProchEtat = BXC;
            if (xEvt == A2) eProchEtat = XAB;
            if (xEvt == B2) eProchEtat = XBC;
            if (xEvt == B3) eProchEtat = AXB;
            if (xEvt == C3) eProchEtat = AXC;
        break;

        case XAB:
            if (xEvt == A1) eProchEtat = AXB;
            if (xEvt == B1) eProchEtat = BXC;
            if (xEvt == A2) eProchEtat = XAB;
            if (xEvt == B2) eProchEtat = XBC;
            if (xEvt == B3) eProchEtat = XAB;
            if (xEvt == C3) eProchEtat = XAC;
        break;

        case AXC:
            if (xEvt == A1) eProchEtat = AXC;
            if (xEvt == B1) eProchEtat = BXC;
            if (xEvt == A2) eProchEtat = XAC;
            if (xEvt == B2) eProchEtat = XBC;
            if (xEvt == B3) eProchEtat = AXB;
            if (xEvt == C3) eProchEtat = AXC;
        break;

        case XAC:
            if (xEvt == A1) eProchEtat = AXC;
            if (xEvt == B1) eProchEtat = BXC;
            if (xEvt == A2) eProchEtat = XAC;
            if (xEvt == B2) eProchEtat = XBC;
            if (xEvt == B3) eProchEtat = XAB;
            if (xEvt == C3) eProchEtat = XAC;
        break;

        case BXC:
            if (xEvt == A1) eProchEtat = AXC;
            if (xEvt == B1) eProchEtat = BXC;
            if (xEvt == A2) eProchEtat = XAC;
            if (xEvt == B2) eProchEtat = XBC;
            if (xEvt == B3) eProchEtat = AXB;
            if (xEvt == C3) eProchEtat = BXC;
        break;

        case XBC:
            if (xEvt == A1) eProchEtat = AXC;
            if (xEvt == B1) eProchEtat = BXC;
            if (xEvt == A2) eProchEtat = XAC;
            if (xEvt == B2) eProchEtat = XBC;
            if (xEvt == B3) eProchEtat = XAB;
            if (xEvt == C3) eProchEtat = XBC;
        break;

        default:
        break;
    } // switch

    // Ne changer les sorties que si l'état a changé.
    if (eProchEtat != eEtat)
    {
        eEtat = eProchEtat;
        DefinirSorties();
    } // if

    AfficherEtat();
    SimulerTimeouts();
 } // TraiterEtat

void main(void)
{
    eEtat = XAB;
    eCommandeAig = IDLE;
    nNbPeriodes = 0;
    DefinirSorties();

    #ifdef AVR
    // Entrées à droite du chip avec pull-ups.
    // Sorties à gauche du chip.
    DDRA = CONFIG_DDRA;
    DDRB = CONFIG_DDRB;
    PORTB = CONFIG_PULLUPB;
    DDRD = CONFIG_DDRD;
    PORTD = CONFIG_PULLUPD;

    while (1)
    {
        if (P_A1) TraiterEtat(A1);
        if (P_A2) TraiterEtat(A2);
        if (P_B1) TraiterEtat(B1);
        if (P_B2) TraiterEtat(B2);
        if (P_B3) TraiterEtat(B3);
        if (P_C3) TraiterEtat(C3);

        CommanderTjdAig();
        _delay_ms(PERIODE);
    } // while
    #endif

    #ifdef PC
    nTest = 0;
    printf("*** Etat initial ***\n");
    AfficherEtat();
    SimulerTimeouts();

    TraiterEtat(C3);
    TraiterEtat(A1);
    TraiterEtat(B1);
    TraiterEtat(B2);
    TraiterEtat(B3);
    TraiterEtat(A1);
    TraiterEtat(B1);
    TraiterEtat(B3);
    TraiterEtat(B2);
    TraiterEtat(A1);
    TraiterEtat(A2);
    TraiterEtat(B3);
    TraiterEtat(B1);
    TraiterEtat(A2);
    TraiterEtat(B3);
    TraiterEtat(A1);
    TraiterEtat(C3);
    TraiterEtat(B3);
    TraiterEtat(A2);
    TraiterEtat(C3);
    TraiterEtat(B1);
    TraiterEtat(B3);
    #endif
} // main

Régulateurs traction

Les régulateurs traction font partie d’un bloc de 4 régulateurs trouvé d’occasion sur Internet. Chaque régulateur comporte:

  • Un régulateur linéaire LM317.
  • Un potentiomètre linéaire de commande.
  • Une résistance ajustable pour adapter la tension maximale.
  • Un inverseur de sens de marche.

Les 4 régulateurs sont alimentés en amont par une source de courant unique. Lors du câblage, il est important de garder cela à l’esprit et toujours prévoir des éclisses isolantes sur les 2 rails. En aucun cas de tels régulateurs ne doivent être câblés avec un retour commun sous peine de court-circuit. Un câblage avec retour commun n’est possible qu’avec 2 transfos train séparés.

reseau12

Zone manoeuvre

Les aiguilles de la zone manoeuvre sont actionnées à l’aide d’une matrice de diodes ce qui permet de choisir l’une des 3 voies de garage ou la voie principale à l’aide d’un seul poussoir. Le schéma ci-après montre le principe. Les poussoirs G1, G2 et S (pour la scierie) sélectionnent l’une des 3 voies, le poussoir V permet de poursuivre sur la voie normale. Dans ce schéma: N = normal, D = dévié.

tco_savignyexpressscan-161009-0004

Cette 1ère réalisation repose sur la capacité des aiguilles de n’alimenter que la voie correspondant à la direction courante, pour autant que l’on ait supprimé les contacts aux emplacements montrés ci-dessous.

aiguil10

Hélas la fiabilité n’était pas au rendez-vous et j’ai choisi de doubler chaque aiguille par un relais bistable pour assurer la réalimentation des voies de garage en fonction de la position des aiguilles.

On retrouve la matrice de diodes et 3 bistables 7, 8, 9 alimentés en parallèle avec l’aiguille correspondante.

manoeuvre

Gare du réseau d’un ami: états et modes de fonctionnement

États et modes de fonctionnement

Les événements détectés par le protothread LireEntrees sont interprétés par le protothread GererEtats pour modifier l’état du système . Ensuite, en fonction de l’état, les relais seront activés par le protothread EtablirItineraires.

Le protothread GererEtats n’a aucune interaction directe avec les entrées-sorties du micro-contrôleur. Cela permet de se concentrer sur la gestion des états et des modes de fonctionnement, sans se préoccuper (du moins pour l’instant) des relais à activer.

Représentation de l’état du système

En vue de la traduction de l’état du système en activation de relais par le protothread EtablirItineraires, il a paru judicieux de choisir une représentation par voie de gare. Chaque voie est représentée par une structure et les structures sont regroupées dans un tableau global accessible par le protothread EtablirItineraires:

struct tEtatVoie {
    uint8_t    nZoneLsne;
    uint8_t    nZoneBern;
    uint8_t    eEtablirLsne;
    uint8_t    eEtablirBern;
};
...
#define	NB_VOIES  6
...
struct tEtatVoie agVoies[NB_VOIES];

Les champs de la structure tEtatVoie ont les significations suivantes:

  • nZoneLsne: numéro de la zone à laquelle la voie est connectée côté Lausanne.
  • nZoneBern: numéro de la zone à laquelle la voie est connectée côté Berne.

Les valeurs possibles pour les numéros de zones sont les mêmes que les numéros des poussoirs d’entrées correspondants. La valeur 255 (-1) indique que la voie n’est connectée à aucune zone.

#define	Z_LSNE1  BP_DIR_LSNE
#define	Z_LSNE2  BP_PROV_LSNE
#define	Z_BERN1A BP_DIR_BERN_A
#define	Z_BERN1B BP_DIR_BERN_B
#define	Z_BERN2  BP_PROV_BERN
...
#define	BLOQUEE  255
  • eEtablirLsne: état de l’établissement de l’itinéraire côté Lausanne.
  • eEtablirBern: état de l’établissement de l’itinéraire côté Berne.

Les valeurs possibles pour ces états sont:

  • ETAB_IDLE: pas d’itinéraire à établir.
  • ETAB_DEMANDE: l’établissement de l’itinéraire est demandé.
  • ETAB_EN_COURS: l’établissement de l’itinéraire est en cours (état géré dans EtablirItineraires).

Structure du protothread

Le protothread est structuré comme suit:

PT_THREAD(GererEtats(struct pt *ppt))
{
    PT_BEGIN(ppt);

    while(1)
    {
        GererTransition(B_SAUVER_EEPROM);
        PT_YIELD(ppt);
    } // while

    PT_END(ppt);
} // GererEtats

Il appelle la fonction GererTransition qui gère effectivement les changements d’état. Le paramètre B_SAUVER_EEPROM permet de choisir s’il faut sauver le nouvel état dans l’EEPROM ou pas, cette fonction étant appelée aussi au démarrage pour restaurer l’état à partir de ce qui est sauvé en EEPROM.

La fonction GererTransition permet de coordonner les appels aux fonctions de gestion des états et des modes.

void GererTransition(uint8_t bxSauverEEPROM)
{
    if(bgEvtReinit)
    {
        InitialiserRelais();
        InitModuleEtats();
    }
    else
    {
        bNouvelEtat = 0;

        GererModeManoeuvre(ngEvtPoussoirEntreeMan);

        if((ngEvtPoussoirVoie != PAS_DE_POUSSOIR) &&
           (ngEvtPoussoirEntreeMan != PAS_DE_POUSSOIR) &&
           (ngEvtPoussoirEntreeMan != BP_MANOEUVRE))
        {
            if((ngEvtPoussoirVoie == BP_VOIE_1) || (ngEvtPoussoirVoie == BP_VOIE_2))
                GererVoie1_2(ngEvtPoussoirVoie, ngEvtPoussoirEntreeMan, bgSortieLsneLibre);
            if((ngEvtPoussoirVoie >= BP_VOIE_3) && (ngEvtPoussoirVoie <= BP_VOIE_6))
                GererVoie3_6(ngEvtPoussoirVoie, ngEvtPoussoirEntreeMan, bgSortieLsneLibre, bgSortieBernLibre);
        } // if

        // Blocage des cantons d'entrée au passage de l'ILS.
        if(bgEvtTrainEntreeLsne)
        {
            bgCantonEntreeLsne = 0;
            bNouvelEtat = 1;
        } // if
        if(bgEvtTrainEntreeBern)
        {
            bgCantonEntreeBern = 0;
            bNouvelEtat = 1;
        } // if

        // En mode transit, on détecte la ré-ouverture des cantons de sortie pour libérer
        // les cantons d'entréee.
        if(bgTransitLsneBern && bgEvtSortieBernLibere && !bgManoeuvre)
        {
            bgCantonEntreeLsne = 1;
            bNouvelEtat = 1;
        } // if
        if(bgTransitBernLsne && bgEvtSortieLsneLibere && !bgManoeuvre)
        {
            bgCantonEntreeBern = 1;
            bNouvelEtat = 1;
        } // if

        if(bNouvelEtat && bxSauverEEPROM) SauverEtatCourant();
    } // if(bgEvtReinit)

    // Réinitialiser les variables d'événements.
    ngEvtPoussoirVoie       = PAS_DE_POUSSOIR;
    ngEvtPoussoirEntreeMan  = PAS_DE_POUSSOIR;
    bgEvtTrainEntreeLsne    = 0;
    bgEvtTrainEntreeBern    = 0;
    bgEvtSortieLsneLibere   = 0;
    bgEvtSortieBernLibere   = 0;
    bgEvtReinit             = 0;
} // GererTransition

Cette fonction commence par détecter si une ré-initialisation du système a été demandée. Sinon, elle gère le mode manoeuvre, puis détermine quelle sous-fonction appeler selon le poussoir de voie pressé. Cette décomposition a uniquement pour but de simplifier les fonctions qui sont déjà suffisamment complexes. Ensuite ce sont les entrées de trains qui sont gérées ainsi que le mode transit décrit plus tard.

La variable bNouvelEtat est mise à jour chaque fois qu’un changement d’état est effectué. Son but est de déterminer ensuite si l’état doit être sauvegardé en EEPROM. La description de la sauvegarde / restauration de l’état et le fonctionnement de l’EEPROM seront décrits dans un autre article.

Changements d’état

A priori, le changement d’état est une opération simple. Pour une voie côté Lausanne par exemple, il suffit de coder:

static void GererVoie3_6(uint8_t nxPoussoirVoie, uint8_t nxPoussoirEntreeMan, uint8_t bxSortieLsneLibre, uint8_t bxSortieBernLibre)
{
uint8_t nVoie;

    switch(nxPoussoirEntreeMan)
    {
        case BP_PROV_LSNE:
            agVoies[nxPoussoirVoie].nZoneLsne = Z_LSNE1;
            agVoies[nxPoussoirVoie].eEtablirLsne = ETAB_DEMANDE;
            bNouvelEtat = 1;
            ....
        break;

        case BP_DIR_LSNE:
            if(bxSortieLsneLibre || bgManoeuvre)
            {
                agVoies[nxPoussoirVoie].nZoneLsne = Z_LSNE2;
                agVoies[nxPoussoirVoie].eEtablirLsne = ETAB_DEMANDE;
                bNouvelEtat = 1;
	    } // if
        break;

        ...
        default:
        break;
    } // switch
} // GererVoie3_6

Mais ce n’est pas aussi simple car les états des voies ne sont pas indépendants. Plusieurs cas doivent être distingués:

Blocage des autres voies

Quelques exemples:

  • Lorsque l’on relie la voie 3 à la zone Lsne1, il faut bloquer les voies 4, 5 et 6-9.
  • Lorsque l’on relie la voie 4 à la zone Lsne2, il faut bloquer les voies 3, 5, 6-9 mais aussi les voies 1 et 2.

La programmation de ces blocages se fait après le changement d’état principal dans la fonction GererVoie1_2.

// Bloquer l'autre voie côté Lausanne.
if((nxPoussoirVoie == BP_VOIE_1) && (agVoies[nxPoussoirVoie].eEtablirLsne != ETAB_IDLE))
{
    agVoies[BP_VOIE_2].nZoneLsne = BLOQUEE;
    agVoies[BP_VOIE_2].eEtablirLsne = ETAB_IDLE;
} // if

if((nxPoussoirVoie == BP_VOIE_2) && (agVoies[nxPoussoirVoie].eEtablirLsne != ETAB_IDLE))
{
    agVoies[BP_VOIE_1].nZoneLsne = BLOQUEE;
    agVoies[BP_VOIE_1].eEtablirLsne = ETAB_IDLE;
} // if

// On doit aussi bloquer l'autre voie côté Berne même s'il n'y a pas de zone d'arrêt
// car cette info est utilisée pour la commande des alimentations traction.
if((nxPoussoirVoie == BP_VOIE_1) && (agVoies[nxPoussoirVoie].eEtablirBern != ETAB_IDLE))
{
    agVoies[BP_VOIE_2].nZoneBern = BLOQUEE;
    agVoies[BP_VOIE_2].eEtablirBern = ETAB_IDLE;
} // if

if((nxPoussoirVoie == BP_VOIE_2) && (agVoies[nxPoussoirVoie].eEtablirBern != ETAB_IDLE))
{
    agVoies[BP_VOIE_1].nZoneBern = BLOQUEE;
    agVoies[BP_VOIE_1].eEtablirBern = ETAB_IDLE;
} // if

// Bloquer les voies 3 à 6 qui pourraient être reliées à LSNE2.
if(agVoies[nxPoussoirVoie].eEtablirLsne != ETAB_IDLE)
{
    for(nVoie = BP_VOIE_3; nVoie <= BP_VOIE_6; nVoie++)
    {
        if(agVoies[nVoie].nZoneLsne == Z_LSNE2)
        {
            agVoies[nVoie].nZoneLsne = BLOQUEE;
            agVoies[nVoie].eEtablirLsne = ETAB_IDLE;
        } // if
    } // for
} // if

// Bloquer les voies 3 à 5 qui pourraient être reliées à BERN2.
if(agVoies[nxPoussoirVoie].eEtablirBern != ETAB_IDLE)
{
    for(nVoie = BP_VOIE_3; nVoie <= BP_VOIE_5; nVoie++)
    {
        if(agVoies[nVoie].nZoneBern == Z_BERN2)
        {
            agVoies[nVoie].nZoneBern = BLOQUEE;
            agVoies[nVoie].eEtablirBern = ETAB_IDLE;
        } // if
    } // for
} // if

Ainsi que dans la fonction GererVoie3_6.

// Bloquer les autre voies.
for(nVoie = BP_VOIE_3; nVoie <= BP_VOIE_6; nVoie++)
{
    if(nVoie != nxPoussoirVoie)
    {
        if(agVoies[nxPoussoirVoie].eEtablirLsne != ETAB_IDLE)
        {
            agVoies[nVoie].nZoneLsne = BLOQUEE;
            agVoies[nVoie].eEtablirLsne = ETAB_IDLE;
        } // if
        if(agVoies[nxPoussoirVoie].eEtablirBern != ETAB_IDLE)
        {
            agVoies[nVoie].nZoneBern = BLOQUEE;
            agVoies[nVoie].eEtablirBern = ETAB_IDLE;
        } // if
    } // if
} // for

if(agVoies[nxPoussoirVoie].nZoneLsne == Z_LSNE2)
{
    agVoies[BP_VOIE_1].nZoneLsne = BLOQUEE;
    agVoies[BP_VOIE_2].nZoneLsne = BLOQUEE;
    agVoies[BP_VOIE_1].eEtablirLsne = ETAB_IDLE;
    agVoies[BP_VOIE_2].eEtablirLsne = ETAB_IDLE;
    if((nxPoussoirVoie >= BP_VOIE_3) && (nxPoussoirVoie <= BP_VOIE_6) && !bgManoeuvre)
    {
        bgCantonEntreeLsne = 0;
        bNouvelEtat = 1;
    } // if
 } // if

Voie reliée des 2 côtés au même sens de circulation

Ce cas se produit par exemple lorsque la voie 5 est reliée à Lsne1, puis à Bern1A. Elle est alors ouverte des 2 côtés. Ce cas correspond à la condition de transit.

Dans la fonction GererVoie1_2, il suffit de détecter la condition de transit et la mémoriser dans la variable globale bgTransitBernLsne pour plus de commodité.

// Détecter la condition de transit Bern -> Lausanne.
bgTransitBernLsne = (agVoies[nxPoussoirVoie].nZoneLsne == Z_LSNE2) &&
                    (agVoies[nxPoussoirVoie].nZoneBern == Z_BERN2);

Dans la fonction GererVoie3_6 on doit aussi bloquer le côté opposé d’une voie s’il n’est pas relié au même sens de circulation.

// Cas des voies reliées des 2 côtés à la même direction de circulation.
if((nxPoussoirVoie >= BP_VOIE_3) && (nxPoussoirVoie <= BP_VOIE_5))
{
    if(agVoies[nxPoussoirVoie].eEtablirLsne != ETAB_IDLE)
    {
        if((agVoies[nxPoussoirVoie].nZoneLsne == Z_LSNE1) && !B_BERN1(agVoies[nxPoussoirVoie].nZoneBern))
        {
            agVoies[nxPoussoirVoie].nZoneBern = BLOQUEE;
            agVoies[nxPoussoirVoie].eEtablirBern = ETAB_IDLE;
        } // if

        if((agVoies[nxPoussoirVoie].nZoneLsne == Z_LSNE2) &&
        (agVoies[nxPoussoirVoie].nZoneBern != Z_BERN2))
        {
            agVoies[nxPoussoirVoie].nZoneBern = BLOQUEE;
            agVoies[nxPoussoirVoie].eEtablirBern = ETAB_IDLE;
        } // if
    } // if

    if(agVoies[nxPoussoirVoie].eEtablirBern != ETAB_IDLE)
    {
        if(B_BERN1(agVoies[nxPoussoirVoie].nZoneBern) && (agVoies[nxPoussoirVoie].nZoneLsne != Z_LSNE1))
        {
            agVoies[nxPoussoirVoie].nZoneLsne = BLOQUEE;
            agVoies[nxPoussoirVoie].eEtablirLsne = ETAB_IDLE;
        } // if

        if((agVoies[nxPoussoirVoie].nZoneBern == Z_BERN2) && (agVoies[nxPoussoirVoie].nZoneLsne != Z_LSNE2))
        {
            agVoies[nxPoussoirVoie].nZoneLsne = BLOQUEE;
            agVoies[nxPoussoirVoie].eEtablirLsne = ETAB_IDLE;
        } // if
    } // if
} // if

// Détection de la condition de transit.
bgTransitLsneBern = (agVoies[nxPoussoirVoie].nZoneLsne == Z_LSNE1) && B_BERN1(agVoies[nxPoussoirVoie].nZoneBern) ||
                    (agVoies[nxPoussoirVoie].nZoneLsne == Z_LSNE2) && (agVoies[nxPoussoirVoie].nZoneBern == Z_BERN2);

Mode manoeuvre

La fonction GererModeManoeuvre ne fait que changer l’état de la variable bgManoeuvre. Cette fonction est la seule de tout ce module à interagir avec le matériel, en l’occurence la led qui indique que le mode manoeuvre est actif !

static void GererModeManoeuvre(uint8_t nxPoussoirEntreeMan)
{
    if(nxPoussoirEntreeMan == BP_MANOEUVRE) bgManoeuvre = !bgManoeuvre;

    if(bgManoeuvre)
    {
        SET_LED(LED_MANOEUVRE);
        bgCantonEntreeLsne = 0;
        bgCantonEntreeBern = 0;
    }
    else
        CLR_LED(LED_MANOEUVRE);
} // GererModeManoeuvre

Le mode manoeuvre est utilisé pour déterminer si on doit tenir compte de l’état du canton de sortie, comme par exemple:

    case BP_DIR_LSNE:
        if(bxSortieLsneLibre || bgManoeuvre)
        {
            agVoies[nxPoussoirVoie].nZoneLsne = Z_LSNE2;
            agVoies[nxPoussoirVoie].eEtablirLsne = ETAB_DEMANDE;
            bNouvelEtat = 1;
        } // if

La condition booléenne est vraie si la sortie est libre ou si on est en mode manoeuvre, auquel cas l’état de la sortie n’importe pas.

Il est aussi utilisé pour autoriser les trains à quitter les cantons d’entrée, comme par exemple:

    case BP_PROV_BERN:
        agVoies[nxPoussoirVoie].nZoneBern = Z_BERN2;
        agVoies[nxPoussoirVoie].eEtablirBern = ETAB_DEMANDE;
        bNouvelEtat = 1;

        if(!bgManoeuvre) bgCantonEntreeBern = 1;

Si le mode manoeuvre est actif, le canton n’est pas libéré.

Mode transit

Le mode transit permet d’intégrer la gare au reste du réseau et de laisser les trains la traverser sans aucune intervention manuelle.

Le mode transit s’active indépendamment pour chaque direction de circulation (Lausanne – Berne ou vice-versa) lorsqu’une voie de gare est reliée à la même direction de circulation de chaque côté. Les possibilités sont (voir le code au chapitre Voie reliée des 2 côtés au même sens de circulation ci-dessus) :

  • Voies 1 et 2: provenance Berne / direction Lausanne.
  • Voies 3 à 5: provenance Lausanne / direction Berne ou provenance Berne / direction Lausanne.

C’est dans la fonction GererTransition que l’on gère l’automatisme.

// En mode transit, on détecte la ré-ouverture des cantons de sortie pour libérer
// les cantons d'entréee.
if(bgTransitLsneBern && bgEvtSortieBernLibere && !bgManoeuvre)
{
    bgCantonEntreeLsne = 1;
    bNouvelEtat = 1;
} // if
if(bgTransitBernLsne && bgEvtSortieLsneLibere && !bgManoeuvre)
{
    bgCantonEntreeBern = 1;
    bNouvelEtat = 1;
} // if

Cela peut fonctionner car les variables d’événements bgEvtSortieBernLibere et bgEvtSortieLsneLibere détectent les transitions canton fermé vers canton ouvert, soit la ré-ouverture !
 

Retour vers la table des matières

Gare du réseau d’un ami: lecture des entrées

Lecture des entrées

Le protothread LireEntrees est en charge de la lecture:

  • du clavier analogique (cf. l’article lecture des entrées: clavier analogique),
  • de l’état des cantons de sortie côtés Lausanne et Berne,
  • des ILS détectant l’entrée des trains en provenance de Lausanne et Berne.

Lecture des boutons poussoirs

La lecture du bouton pressé, soit parmi les voies de gare, soit parmi les entrées et le mode manoeuvre est effectuée par la fonction nLirePoussoir. Pour s’affranchir des rebonds sur les poussoirs, la transition de l’état non pressé à l’état pressé est détectée. Pour cela, les déclarations suivantes sont nécessaires:

static uint8_t nPoussoirVoie, nPoussoirEntreeMan, nPoussoirVoieNouv, nPoussoirEntreeManNouv;

2 variables sont définies pour chaque chaînes de poussoirs. Par exemple, pour les poussoirs de choix de la voie: nPoussoirVoie, nPoussoirVoieNouv (pour le nouvel état du poussoir). Ces variables, toutes initialisées à la valeur PAS_DE_POUSSOIR, s’utilisent comme suit dans le corps principal du protothread LireEntrees:

while(1)
{
    nTimerRebond = DELAI_REBOND;
    PT_WAIT_UNTIL(ppt, nTimerRebond == 0);
    ...
    // Lire les poussoirs.
    nPoussoirVoieNouv = nLirePoussoir(BP_VOIES);
    nPoussoirEntreeManNouv = nLirePoussoir(BP_ENTREES_MAN);

    // Détection des transitions des poussoirs.
    if((nPoussoirVoieNouv != PAS_DE_POUSSOIR) && (nPoussoirVoie == PAS_DE_POUSSOIR))
        nPoussoirVoiePress = nPoussoirVoieNouv;
    else
        nPoussoirVoiePress = PAS_DE_POUSSOIR;

    if((nPoussoirEntreeManNouv != PAS_DE_POUSSOIR) && (nPoussoirEntreeMan == PAS_DE_POUSSOIR))
        nPoussoirEntreeManPress = nPoussoirEntreeManNouv;
    else
        nPoussoirEntreeManPress = PAS_DE_POUSSOIR;
    nPoussoirVoie = nPoussoirVoieNouv;
    nPoussoirEntreeMan = nPoussoirEntreeManNouv;
    ...

Le timer nTimerRebond est utilisé pour insérer un temps d’attente de 50 ms entre chaque lecture des poussoirs (cf. l’article programmation pour la description du fonctionnement des timers).

Lorsqu’une transition de l’état PAS_DE_POUSSOIR à une valeur correspondant à la pression d’un poussoir, la variable nPoussoirVoiePress, respectivement nPoussoirEntreeManPress est mise à jour avec la valeur du bouton pressé.

Il faut encore gérer les pressions sur les 2 poussoirs qui doivent être effectuées dans un délai de 5 secondes. À cet effet, les états suivants sont définis (les valeurs sont absolument arbitraires):

#define E_IDLE             0
#define E_BP_ENTREE_MAN    1
#define E_BP_VOIE          2
#define E_BP_COMPLET       3

Ces états ont les significations suivantes:

  • E_IDLE: état initial du système, en attente d’une pression sur un poussoir de gare ou d’entrée. Une pression sur un bouton d’entrée ou de manoeuvre passe dans l’état E_BP_ENTREE_MAN et enclenche le timer. De même une pression sur un bouton de voie passe dans l’état E_BP_VOIE et enclenche le timer.
  • E_BP_ENTREE_MAN: un bouton d’entrée ou le bouton de manoeuvre a été pressé. Une pression sur un bouton de voie passe dans l’état E_BP_COMPLET tandis que l’occurence du timeout repasse dans l’état E_IDLE.
  • E_BP_VOIE: un bouton de voie a été pressé. Une pression sur un bouton d’entrée ou le bouton de manoeuvre passe dans l’état E_BP_COMPLET tandis que l’occurence du timeout repasse dans l’état E_IDLE.
  • E_BP_COMPLET: les 2 poussoirs voie et entrée/manoeuvre ont été pressé dans le délai imparti. Après avoir recopié les valeurs des poussoirs dans les variables globales ngEvtPoussoirVoie et ngEvtPoussoirEntreeMan pour les rendre disponibles au reste du système, retour à l’état E_BP_IDLE.

Lecture des ILS: cantons d’entrée

Les 2 cantons d’entrée côtés Lausanne et Berne doivent être refermé une fois que le train correspondant est entré dans la gare. Pour cela, les trains doivent être détectés à l’entrée de la gare. Comme la détection des trains sur ce réseau est déjà basée sur des ILS, le matériel roulant est déjà équipé d’aimants. Il est donc naturel d’utiliser 2 ILS supplémentaires pour cette détection.

Ces 2 ILS sont câblés comme des poussoirs sur 2 entrées numériques du micro-contrôleur qu’ils tirent vers la masse lorsqu’ils sont activés. Tout comme les poussoirs, les ILS produisent des rebonds. À l’instar des poussoirs, on s’intéresse aux transitions de l’état non pressé vers l’état pressé. Le code suivant, appelé dans la boucle principale du protothread toutes les 40 ms, effectue la détection de transition.

#define	B_TRAIN_ENTREE_LSNE	(!(PIND & _BV(TRAIN_LSNE)))
#define	B_TRAIN_ENTREE_BERN	(!(PIND & _BV(TRAIN_BERN)))
...
bTrainEntreeLsneNouv	= B_TRAIN_ENTREE_LSNE;
if((bTrainEntreeLsneNouv == 1) && (bTrainEntreeLsne == 0))
    bgEvtTrainEntreeLsne = bTrainEntreeLsneNouv;
else
    bgEvtTrainEntreeLsne = 0;
bTrainEntreeLsne = bTrainEntreeLsneNouv;

bTrainEntreeBernNouv	= B_TRAIN_ENTREE_BERN;
if((bTrainEntreeBernNouv == 1) && (bTrainEntreeBern == 0))
    bgEvtTrainEntreeBern = bTrainEntreeBernNouv;
else
    bgEvtTrainEntreeBern = 0;
bTrainEntreeBern = bTrainEntreeBernNouv;

Les 2 macros B_TRAIN_ENTREE_LSNE et B_TRAIN_ENTREE_BERN effectuent la lecture des ports du micro-contrôleur. Ensuite, les variables bTrainEntreeLsneNouv, bTrainEntreeLsne, respectivement bTrainEntreeBernNouv, bTrainEntreeBern permettent de comparer l’état courant avec l’état précédent.

Ce sont ensuite les variables globales bgEvtTrainEntreeLsne et bgEvtTrainEntreeBern qui exportent les événements correspondant au reste du programme.

Aux noms de variables près, c’est exactement le même principe qui est utilisé pour lire l’état des cantons de sortie, information nécessaire pour déterminer si on peut laisser sortir un train.

#define B_SORTIE_LSNE_LIBRE	(!(PIND & _BV(CANTON_LSNE)))
#define	B_SORTIE_BERN_LIBRE	(!(PIND & _BV(CANTON_BERN)))
...
bSortieLsneLibreNouv	= B_SORTIE_LSNE_LIBRE;
if((bSortieLsneLibreNouv == 1) && (bgSortieLsneLibre == 0))
    bgEvtSortieLsneLibere = bSortieLsneLibreNouv;
else
    bgEvtSortieLsneLibere = 0;
bgSortieLsneLibre = bSortieLsneLibreNouv;

bSortieBernLibreNouv	= B_SORTIE_BERN_LIBRE;
if((bSortieBernLibreNouv == 1) && (bgSortieBernLibre == 0))
    bgEvtSortieBernLibere = bSortieBernLibre;
else
    bgEvtSortieBernLibere = 0;
bgSortieBernLibre = bSortieBernLibreNouv;

Retour vers la table des matières

Gare du réseau d’un ami: lecture des entrées: clavier analogique

Lecture des entrées: clavier analogique

Après la présentation de la structure générale du programme et du concept de protothread mis en oeuvre (cf. l’article programmation), passons à la description du protothread LireEntrees. Cet article décrit la lecture du clavier analogique, les autres tâches de ce protothread feront l’objet d’un autre article.

Lecture des boutons poussoir

La lecture d’un bouton poussoir d’une entrée ou d’une voie se fait à l’aide du convertisseur analogique-numérique intégré au micro-contrôleur. La chaîne de résistances correspondant aux boutons d’entrées (2 côté Lausanne, 3 côté Berne) ainsi qu’au bouton du mode manoeuvre est reliée à l’une des entrées du multiplexeur d’entrée du convertisseur analogique-numérique. La chaîne de résistances correspondant aux 6 boutons de voie est reliée à l’autre entrée.

Les 2 chaînes de résistances comportent les mêmes valeurs de résistances, calculées à l’aide de la feuille Excel décrite dans l’article sur la connexion de boutons poussoirs. .

Les déclarations nécessaires sont:

#define	PAS_DE_POUSSOIR	255
// Codes des touches pour les entrées en gare, convertisseur ADC0.
#define	NB_POUSSOIRS_ENTREES    6
#define BP_DIR_LSNE             0
#define BP_PROV_LSNE            1
#define	BP_DIR_BERN_A           2
#define	BP_DIR_BERN_B           3
#define	BP_PROV_BERN            4
#define	BP_MANOEUVRE            5

// Codes des touches pour les voies de gare, convertisseur ADC 1.
#define	NB_POUSSOIRS_VOIES      6
#define	BP_VOIE_1               0
#define	BP_VOIE_2               1
#define	BP_VOIE_3               2
#define	BP_VOIE_4               3
#define	BP_VOIE_5               4
#define	BP_VOIE_6               5  // Voies 6 à 9.

// Configuration du convertisseur ADC.
#define	CONFIG_ADMUX_AD_REF    (_BV(REFS0))
#define	SELECT_ADC0            (ADMUX &= ~(_BV(MUX0)|_BV(MUX1)|_BV(MUX2)))
#define	SELECT_ADC1            (ADMUX |= _BV(MUX0))
#define	CONFIG_ADCSRA          (_BV(ADEN)|_BV(ADPS1)|_BV(ADPS0))
#define	ENABLE_ADC             (ADCSRA |= _BV(ADEN))
#define	START_CONVERSION       (ADCSRA |= _BV(ADSC))
#define CONVERSION_RUNNING     (ADCSRA & _BV(ADSC))
#define	NB_POUSSOIRS	6

uint16_t aSeuilsTouches[NB_POUSSOIRS] PROGMEM = {
79, 248, 431, 606, 772, 939 };

#define	BP_ENTREES_MAN	0
#define	BP_VOIES	1

Le convertisseur analogique-numérique doit être initialisé pour utiliser la tension d’alimentation comme référence et prendre la valeur de l’entrée 0 ou 1 du multiplexeur d’entrée.

ADMUX  = CONFIG_ADMUX_AD_REF;
ADCSRA = CONFIG_ADCSRA;

Toutes ces déclarations permettent d’écrire la fonction de lecture d’un poussoir de l’une des 2 chaînes.

static uint8_t nLirePoussoir (uint8_t ePoussoir)
{
uint16_t n, seuil;
uint16_t nAdValeur;

    if(ePoussoir == BP_VOIES) SELECT_ADC1;
    else if(ePoussoir == BP_ENTREES_MAN) SELECT_ADC0;

    ENABLE_ADC;
    START_CONVERSION;
    while(CONVERSION_RUNNING);

    // Il est indispensable de lire ADCL avant ADCH, sinon la conversion ne peut pas être répétée !
    nAdValeur  = ADCL|(ADCH << 8);

    for (n = 0; n < NB_POUSSOIRS; n++)
    {
        seuil = (uint16_t)pgm_read_word(&(aSeuilsTouches[n]));

        if (nAdValeur < seuil)
        {
            return(n);
         } // if
    } // for
    return(PAS_DE_POUSSOIR);
} // nLirePoussoir

En fonction de la valeur du paramètre, on commence par sélectionner l’entrée du multiplexeur à lire. Ensuite, le convertisseur analogique-numérique est enclenché par l’appel ENABLE_ADC, la conversion lancée par l’appel START_CONVERSION. On attend ensuite la fin de la conversion par l’instruction while(CONVERSION_RUNNING).

La valeur, entre 0 et 1023 est lue par l’instruction nAdValeur = ADCL|(ADCH << 8);.

Ensuite, on parcourt les valeurs de seuils du tableau aSeuilsTouches pour déterminer quelle poussoir a été pressé. Les valeurs de ce tableau proviennent de la feuille Excel décrite dans l’article sur la connexion de plusieurs poussoirs.

Le tableau aSeuilsTouches est déclaré à l’aide de la directive PROGMEN pour qu’il soit stocké en mémoire programme plutôt qu’en RAM. Le principal avantage est l’économie de RAM qui en résulte. Pour lire une valeur de ce tableau, on procède comme suit:

  • Ecrire l’instruction d’accès au tableau: aSeuilsTouches[n];
  • Prendre son adresse: &(aSeuilsTouches[n]);
  • Passer le tout à une fonction d’accès à la mémoire programme: seuil = (uint16_t)pgm_read_word(&(aSeuilsTouches[n]));
  • D’autres fonctions permettant de lire des bytes ou des chaînes de caractères sont définies dans la librairie progmen.h.

Si la valeur lue est plus petite que le seuil courant, la position dans le tableau donne le numéro du poussoir pressé, d’où les définitions des constantes correspondant aux poussoirs ci-dessus. On peut alors sortir de la fonction.

Si aucun bouton n’est pressé la dernière instruction est exécutée:
return(PAS_DE_POUSSOIR);

Cette fonction s’inspire largement de l’exemple donné par C. Tavernier dans son livre: Arduino, maîtrisez sa programmation et ses cartes d’interfaces (shields). La principale différence ici est la déclaration du tableau de constantes en mémoire programme.

Retour vers la table des matières

Gare du réseau d’un ami: programmation

Structure générale

La technique mise en oeuvre est la programmation synchrone qui permet tout à la fois d’avoir plusieurs tâches donnant l’illusion de se dérouler en parallèle, d’autre part de se passer des interruptions. La structure du programme principal est simplement:

while(1)
{
   Tâche1;
   Tâche2;
   ...
   TâcheN;

   // Attendre que le timer0 atteigne la valeur correspondant à 10ms,
   // puis le réinitialiser.
   while(TCNT0 < MAX_TIMER0);
   TCNT0 = 0;
} // while

Les tâches s’exécutent l’une après l’autre, puis on attend la fin de la période de 10 ms. Pour compter 10 ms avec le timer0, les définitions suivantes sont nécessaires:

#define	CONFIG_TIMER0	(_BV(CS01)|_BV(CS00))
#define	MAX_TIMER0	156

...
    // À placer avant d'entrer dans la boucle principale.
    TCCR0 = CONFIG_TIMER0;

La valeur CONFIG_TIMER0, écrite dans le registre de contrôle TCCR0 du timer0 divise la fréquence d’horloge de 1 MHz par 64. Avec ce paramétrage du pre-scaler, la valeur MAX_TIMER0 de 156, utilisée dans la boucle principale compte 10 ms. Il aurait été possible d’utiliser une instruction _delay_ms() ou delay dans l’environnement Arduino, mais ce n’est applicable que lorsque le temps d’exécution des tâches est beaucoup plus petit que 10 ms car le temps d’exécution s’ajoute au temps d’attente, l’instruction _delay_ms() ne s’exécutant pas en parallèle. Il y a donc une dérive temporelle plus ou moins importante et plus ou moins gênante.

Le timer intégré au micro-contrôleur ne présente pas ce problème car il tourne parallèlement à l’exécution des instructions par le micro-contrôleur. Ainsi, les périodes font exactement 10 ms.

Ce principe impose toutefois 2 contraintes:

  • Les tâches ne doivent pas monopoliser le processeur et donc rendre la main rapidement. Nous verrons plus loin quelles techniques permettent de garantir cela.
  • Le temps d’exécution cumulé des N tâches ne doit pas excéder 10 ms. Si cette contrainte ne peut pas être satisfaite, on peut soit augmenter la période, soit configurer le processeur pour que son oscillateur interne tourne plus vite: 2, 4 et 8 MHz peuvent aussi être choisi en plus de 1 MHz qui est la valeur par défaut.

Utilisation des protothreads

Les tâches peuvent être implantées sous formes de machines à états finis ou, lorsque la séquence d’instructions est largement séquentielle, sous forme de protothreads.

Les protothreads, un concept proposé par Adam Dunkels (cf sa page: protothreads qui décrit le principe et propose la librairie en téléchargement), permettent un style de programmation quasi concurrente même sur des micro-contrôleurs 8 bits tels que les Atmega et Attiny. C’est aussi utilisable dans l’environnement Arduino ainsi que le montre le tutorial suivant: Threading in Arduino.

Un premier exemple de protothread

Ce premier exemple de protothread fait clignoter une LED. Rien de bien nouveau par rapport au premier programme que tout amateur de micro-contrôleur développe, si ce n’est que la LED pourra clignoter tandis que le micro-contrôleur déroulera d’autres tâches.

Le programme développé pour le contrôle de la gare a un protothread qui fait clignoter la LED présente sur la carte micro-contrôleur pour indiquer que tout se déroule bien et que le programme n’est pas parti dans une boucle infinie, auquel cas la LED ne clignoterait plus.

Après avoir téléchargé les fichiers permettant d’utiliser les protothreads (cf. le lien plus haut), ajouter l’instruction suivante dans le programme:

#include <pt.h>

Les protothreads sont implantés uniquement sous formes de macros du préprocesseur C, il n’y a donc que des fichiers .h à recopier dans le même répertoires que les fichiers .c et .h du programme.

Il faut ensuite écrire les déclarations suivantes:

#define	LED_CARTE	PB7
...
#define	SET_LED(led)	(PORTB |= _BV(led))
#define CLR_LED(led)	(PORTB &= ~_BV(led))
...
#define PERIODE	10
#define	DELAI_WATCHDOG	(500/PERIODE)
...
static struct pt ptWatchdogLed;
static uint16_t	nTimerWatchdog;
...
nTimerWatchdog = 0;

PT_THREAD(WatchdogLed(struct pt *ppt))
{
    if(nTimerWatchdog > 0) nTimerWatchdog--;

    PT_BEGIN(ppt);
    while(1)
    {
        SET_LED(LED_CARTE);

        nTimerWatchdog = DELAI_WATCHDOG;
        PT_WAIT_UNTIL(ppt, nTimerWatchdog == 0);

        CLR_LED(LED_CARTE);

        nTimerWatchdog = DELAI_WATCHDOG;
        PT_WAIT_UNTIL(ppt, nTimerWatchdog == 0);
    } // while
    PT_END(ppt);
} // WatchdogLed

Une petite explications quant au nom de cette fonction s’impose. Un watchdog ou chien de garde en français est un mécanisme surveillant le bon fonctionnement d’un système informatique.

Le paramètre passé à cette fonction permet de mémoriser l’endroit où reprendre lors du prochain appel, cette fonction est appelée dans la boucle principale toutes les 10 ms.

Le fonctionnement est le suivant:

  • Le code entre le début et l’instruction PT_BEGIN(ppt) est exécuté à chaque appel. Il s’agit ici de décrémenter le timer nTimerWatchdog qui est en fait un compteur du nombre d’appels de la fonction. Comme la fonction est appelée toutes les 10 ms, le compteur permet de mesurer le temps par tranches de 10 ms.
  • Lors du premier appel, on entre dans la fonction pour appeler SET_LED(LED_CARTE) une macro qui met à 1 la sortie correspondant à la LED de la carte. Cette macro a le même effet que l’appel digitalWrite(LED_CARTE, HIGH) de l’Arduino.
  • Le compteur nTimerWatchdog est ensuite initialisé avec la valeur DELAI_WATCHDOG, cette constante étant définie comme 500/PERIODE, soit 500 ms / 10 ms = 50 itérations.
  • L’instruction PT_WAIT_UNTIL(ppt, nTimerWatchdog == 0) est très intéressante. Son premier paramètre permet de mémoriser l’endroit courant du code. Son second paramètre est une condition booléenne.
  • Si la condition booléenne est vraie, on passe à l’instruction suivante.
  • Si elle est fausse, on saute directement à l’instruction PT_END(ppt).
  • À l’appel suivant de la fonction, le compteur nTimerWatchdog est décrémenté car le code avant l’instruction PT_BEGIN(ppt) est exécuté à tous les appels.
  • À partir de l’instruction PT_BEGIN(ppt), on saute directement à l’instruction PT_WAIT_UNTIL(ppt, nTimerWatchdog == 0) où on était resté lors du précédent appel.
  • À nouveau la condition est testée. Si maintenant le compteur nTimerWatchdog vaut zéro, cela signifie que 500 ms se sont écoulées depuis l’initialisation de nTimerWatchdog.
  • On passe à l’instruction suivante: CLR_LED(LED_CARTE) qui a pour effet d’éteindre la LED, ce qui correspond à digitalWrite(LED_CARTE, LOW) pour Arduino.
  • On arrive ensuite sur la seconde initialisation du compteur nTimerWatchdog pour attendre pendant que la LED est enclenchée.
  • L’instruction PT_WAIT_UNTIL(ppt, nTimerWatchdog == 0) qui suit a exactement le même comportement. Au bout de 50 appels de la fonction, on continue. Comme on est dans une boucle infinie, on revient au début du bloc while et on allume la LED.

La structure de la fonction WatchdogLed est très proche du programme de clignotement de LED que tout le monde connaît. Les principales différences sont la présence des instructions PT_BEGIN(ppt) et PT_END(ppt) pour délimiter la zone de code où on souhaite mémoriser la dernière instruction exécutée à chaque appels, ainsi que l’utilisation du bloc suivant à la place d’une instruction _delay_ms ou delay (Arduino):

nTimerWatchdog = DELAI_WATCHDOG;
PT_WAIT_UNTIL(ppt, nTimerWatchdog == 0);

Ce qui fait tout l’intérêt de ce mécanisme c’est que l’appel à PT_WAIT_UNTIL ne bloque pas, il retourne tout de suite à l’appelant si la condition est fausse. Ce qui n’est évidemment pas le cas avec les fonctions _delay_ms ou delay.

L’utilisation de cette fonction est très simple.

PT_INIT(&ptWatchdogLed);
...
while(1)
{
   WatchdogLed(&ptWatchdogLed);

   Tâche2;
   ...
   TâcheN;

   // Attendre que le timer0 atteigne la valeur correspondant à 10ms,
   // puis le réinitialiser.
   while(TCNT0 < MAX_TIMER0);
   TCNT0 = 0;
} // while

Ainsi, comme le protothread WatchdogLed ne bloque plus, il « rend la main » au micro-contrôleur ce qui lui permet d’appeler les autres tâches dans la boucle principale.

En plus de l’appel PT_WAIT_UNTIL, il existe aussi PT_WAIT_WHILE qui permet d’attendre (et donc de sauter au PT_END tant qu’une condition est vraie et poursuivre dès que la condition est fausse. Mentionnons aussi PT_YIELD qui permet de sauter au PT_END sans aucune condition, ce qui peut parfois s’avérer utile.

Remarques importantes

Quelques précautions sont indispensables pour bien utiliser les protothreads.

  • Ne jamais employer l’instruction switch dans un protothread. La raison est que l’instruction switch est utilisée par les macros implantant les protothreads pour permettre la mémorisation du dernier emplacement et les sauts.
  • Les variables déclarées localement dans la fonction sont perdues entre chaque appel. Si on a besoin de conserver une valeur entre 2 appels, il est impératif de déclarer une variable hors de la fonction du protothread. Il existe un cas particulier que nous verrons dans un autre article.
  • Il n’est pas possible d’appeler PT_WAIT_UNTIL, PT_WAIT_WHILE et PT_YIELD dans une fonction appelée depuis un protothread. Il faut utiliser la notion de sous-protothread, utilisée dans ce projet par le protothread EtablirItineraires.

Utilisation dans le cadre de ce projet

Pour le système gérant la gare, 4 protothreads ont été développés:

  • WatchdogLed qui a fait l’objet de l’explication ci-dessus.
  • LireEntrees pour lire les poussoirs ainsi que les autres entrées du système.
  • GererEtats pour gérer la machine à états finis de la gare.
  • EtablirItineraires pour commander les relais en fonction de l’état.

Le programme principal a donc la structure suivante:

...
PT_INIT(&ptWatchdogLed);
PT_INIT(&ptLireEntrees);
PT_INIT(&ptGererEtats);
PT_INIT(&ptEtablirItineraires);
...
while(1)
{
    // Ce protothread permet de voir qu'aucun autre protothread
    // ne monopolise le processeur.
    WatchdogLed(&ptWatchdogLed);

    LireEntrees(&ptLireEntrees);
    GererEtats(&ptGererEtats);
    EtablirItineraires(&ptEtablirItineraires);
    CommanderRelais();

    // Attendre la fin de la période et passer à la suivante.
    while(TCNT0 < MAX_TIMER0);
    TCNT0 = 0;
} // while

Les 4 protothreads communiquent entre eux par des variables globales. Selon les bonnes pratiques de développement logiciel, ce n’est pas ce qu’il y a de mieux, mais c’est plus optimal sur un micro-contrôleur 8 bits!

La fonction CommanderRelais est appelée dans la boucle principale pour rafraîchir le contenu des registres à décalage. Comme cette fonction n’a pas besoin de mémoriser son état, elle n’est pas implantée sous forme de protothread. Elle est par ailleurs aussi appelée dans le protothread EtablirItineraires.

Retour à la table des matières