Auteur Sujet: Projet Dominique  (Lu 6668 fois)

Dominique

  • Global Moderator
  • Hero Member
  • *****
  • Messages: 1209
  • 100% Arduino et N
    • Voir le profil
Re : Projet Dominique
« Réponse #30 le: novembre 25, 2017, 11:25:47 am »
Le temps passe et le projet continue :

- Le modéliste est réparé (la hanche est refaite à neuf, pour 30 ans au moins).
- Sur les conseils de Pierre, j'ai ajouté 2 aiguilles pour permettre des manoeuvres entre la voie de programmation (qui fait partie intégrante de la zone de jeu) et la zone de dépôt.
- Du coup j'ai ajouté 4 relais sur le module de commande d'aiguilles.
- Du coup j'ai intégré ces 2 aiguilles dans le Gestionnaire avec 2 boutons tactiles, ce qui permet de les commander en l'absence de boutons réels sur le TCO qui, d'ailleurs, devra subir une rénovation drastique comme son modéliste.
- Je dois encore ajouter des connexions entre des capteurs de consommation récemment installés et le TCO pour permettre encore plus d'itinéraires que ce qui était possible au départ.

Mais, surtout, je dois maintenant refaire le module de traction, simplement récupéré sur mon précédent réseau, dont il manque une fonction essentielle : le pilotage par le gestionnaire et bien d'autres fonctions que je vais détailler.

Une remarque quand même au passage : toutes ces modifications n'interrompent à aucun moment la disponibilité du réseau pour jouer (sauf le temps d'une mise à jour logicielle d'un module ou le branchement de quelque fil). Cela est dû en grande partie à l'architecture choisie, découpée en modules dédiés à un seul type de fonction.

C'est pour cela que :
- j'ai pu ajouter 2 aiguilles supplémentaires sans remettre en cause la disponibilité du réseau;
- j'ai changé d'écran sur le gestionnaire pour passer de 2 écrans 2,8' à un écran 5' en attendant de passer au 7' grâce à Jean-Luc (on verra cela prochainement);
- je vais maintenant refaire complètement le module de traction sur la base de DCC++, mais avec la bibliothèque de Thierry et une ergonomie plus adaptée au jeu à plusieurs et à la circulation de nombreuses machines.

Vous voyez, nous travaillons tous ensemble et chacun profite des compétences des autres, pour le meilleur du modélisme ferroviaire.

Amicalement
A bientôt
Dominique
« Modifié: novembre 25, 2017, 01:16:29 pm par Dominique »

Dominique

  • Global Moderator
  • Hero Member
  • *****
  • Messages: 1209
  • 100% Arduino et N
    • Voir le profil
Re : Projet Dominique
« Réponse #31 le: novembre 26, 2017, 01:44:34 pm »
Voici maintenant le module de traction dernière version.

Comme on s'en doutait il est équipé du logiciel DCC++ et en particulier de sa version sous forme de bibliothèque concoctée par Thierry.

Comme j'aime bien les systèmes à l'ancienne, j'ai récupéré un bloc de 12 potentiomètres de 10K professionnels issus d'une table de mixage.



Ensuite, il faut une électronique que j'ai voulue totalement sans PC, donc un panneau avec des boutons et afficheur :



Ce panneau est couplé à un Mega2560 qui va gérer :
  • L'afficheur 4x20 I2C
  • Le clavier tactile 12 touches I2C
  • L'encodeur en quadrature
  • Le LMD18200 pour fournir la puissance en DCC
  • L'interface CAN en SPI
  • Une interface radio NRF24L01
  • Une manette sans fil reliée en radio

Il y aura aussi 12 Leds rouges et 12 Leds verte en face de chaque potentiomètre.

Je vais expliquer ensuite comment je compte utiliser ce module qui doit assurer la traction jusqu'à 12 locos, la configuration de tout ce qu'il faut configurer et le pilotage à partir du Gestionnaire via l'interface CAN.

La réalisation n'est pas terminée donc je distillerai les détails au fur et à mesure des mises au point lorsqu'elles seront couronnés de succès  8).

Mais en attendant, je viens de faire un essai sur le réseau et, sans les potentiomètres ni le Gestionnaire, j'ai bien piloté 4 machines simultanément, rien qu'avec le claver tactile pour les sélectionner et l'encodeur rotatif (gros bouton à droite) pour la vitesse et la direction.

L'intensité affichée en mA se situait autour de 500 mA, ça dépendait si la voie était à plat ou en côtes, soit environ 100 à 150 mA par loco et plus s'il y a des wagons et voitures derrière. Sur la photo ci-dessus, ma loco dite "Cabane" consomme 140 mA ;)

A suivre...
« Modifié: novembre 26, 2017, 05:05:26 pm par Dominique »

Dominique

  • Global Moderator
  • Hero Member
  • *****
  • Messages: 1209
  • 100% Arduino et N
    • Voir le profil
Re : Projet Dominique
« Réponse #32 le: novembre 26, 2017, 05:45:37 pm »
Voilà comment j'ai conçu l'ergonomie du module, avec les fonctions de chaque élément sur cette photo de la face avant :
.

A gauche les fonctions DCC :
  • Les 2 leds jaunes sont montées tête-bêche et reliées aux rails via une résistance de 1500 Ω.
  • La led rouge indique si le DCC est en service (éteinte, avec les leds jaunes allumées) ou hors service (en cas de court-circuit notamment, ce que j'ai bien testé avec une valeur seuil de 2000 mA).
  • Le bouton rouge "Panic" stoppe le DCC sur les rails, ou le remet en service. Le gestionnaire est informé des coupures du DCC et valide la remise en service par message CAN
  • Les boutons F0 (lumières), F1 et F2 actionnent les fonctions de la loco sélectionnée et indiquée à l'écran.
  • Le clavier tactile a 12 touches pour les valeurs 0 à 9 et les 2 valeurs suivantes 10 et 11 pour la sélection des locos (12 locos maximum). Sinon ces 2 touches sont affectées en mode saisie de paramètre de configuration, à la correction (back space) et la validation (enter). A cause de cela, j'ai numéroté les locos de 0 à 11, au lieu de 1 à 12 qui aurait été plus humain. Mais bon, c'est comme ça !
  • Les 3 leds à droite de l'écran renseignent sur l'état en mode configuration (sélection de fonction, saisie de paramètre, retour en mode conduite.
  • L'encodeur rotatif permet de faire défiler une valeur en avant ou en arrière, toujours à partir de la valeur qu'elle a à l'écran. Il sert à choisir les paramètres de configuration (après appui sur son bouton) ou à faire varier la vitesse de la loco sélectionnée à l'écran (quand on est sorti du mode configuration). Pour cette vitesse, le passage par zéro se traduit par le changement de sens de la loco (et du symbole < ou >). Ainsi on accélère en marche arrière en tournant à gauche et en marche avant en tournant à droite, pour rester logique.

Avec ces quelques éléments on peut faire beaucoup de choses !

Bien entendu tout fonctionne en même temps : les locos continuent de tourner même en mode configuration, le gestionnaire peut piloter les locos, ainsi que les 12 potentiomètres à coté de ce panneau.

Cet aspect "multi-tâche" sera décrit en détail pour vous donner un exemple à réutiliser dans votre projet.

Ajout

Je reviendrai en détail plus loin sur l'afficheur LCD 4x20, mais je précise tout de suite les contenus des 4 lignes :
- ligne 0 (en haut) : nom de fa fonction de configuration
- ligne 1 : question/saisie pour la configuration ou message hors configuration (par exemple le contenu d'un message CAN reçu ou la vitesse telle d’une loco mesurée sur la voie de parade juste devant)
- ligne 2 : nom de la loco sélectionnée
- ligne 3 : paramètres de conduite de cette loco (N°, adresse DCC, vitesse, direction et lumières) et le courant consommé total en mA.
« Modifié: décembre 21, 2017, 09:17:23 pm par Dominique »

Dominique

  • Global Moderator
  • Hero Member
  • *****
  • Messages: 1209
  • 100% Arduino et N
    • Voir le profil
Re : Projet Dominique
« Réponse #33 le: novembre 26, 2017, 07:51:37 pm »
Juste pour voir l'envers du décor, j'ai travaillé sur un petit chassis en bois qui permet de déplacer le projet facilement :



A l'interieur il y a le Mega2560 sur lequel est monté une plaque d’extension pour y raccorder les fils de liaison vers ses accessoires tout autour :



Et même l'envers de la face avant, dont le chassis m'a bien aidé pour faire les soudures.



C'est bidouille mais c'est solide, modifiable et fait pour durer.

A suivre...
« Modifié: décembre 21, 2017, 09:20:41 pm par Dominique »

Dominique

  • Global Moderator
  • Hero Member
  • *****
  • Messages: 1209
  • 100% Arduino et N
    • Voir le profil
Re : Projet Dominique
« Réponse #34 le: décembre 01, 2017, 12:51:28 pm »
Voici les schémas :

L'Arduino Mega 2560 et ses connexions

La broche A0 (Max471) est reliée à la sortie "courant" du Max471 (figure suivante).

La puissance DCC et l'alimentation 9V de l'Arduino (la tension d'alim du DCC étant trop élevée pour l'Arduino)


Les potentiomètres, leds et boutons de direction (sur les même pins que les leds vertes)


Les éléments de la face avant


Les télécom CAN et NRF24L01+ vers la manette sans fil


Pour le moment j’ai mis de côté le NRF24 pour bien mettre au point le reste car chaque loco doit pouvoir être commandée par le bouton rotatif, un potentiomètre, un message en provenance du CAN (ça fait 3 sources), puis en radio via le NRF24 (ça fait 4 sources), en enfin, plus tard, par une interface WiFi et mon iPhone (ça fera 5 sources).

Rassurez-vous, DCCpp supporte tout ça, la complexité réside dans l’ecriture multitâche du programme.

« Modifié: décembre 31, 2017, 10:15:00 am par Dominique »

Dominique

  • Global Moderator
  • Hero Member
  • *****
  • Messages: 1209
  • 100% Arduino et N
    • Voir le profil
Re : Projet Dominique
« Réponse #35 le: décembre 09, 2017, 04:54:28 pm »
Premier assemblages des éléments : ça marche du 1er coup, avec 4 trains : la conduite est douce et réaliste, du fait de la grande longueur des potentiomètres et de leur haute qualité.

« Modifié: décembre 31, 2017, 10:21:39 am par Dominique »

Dominique

  • Global Moderator
  • Hero Member
  • *****
  • Messages: 1209
  • 100% Arduino et N
    • Voir le profil
Re : Projet Dominique
« Réponse #36 le: décembre 15, 2017, 11:26:42 pm »
Voici une petite photo d'étape, un panoramique (pas terrible) avec presque tous les éléments côte à côte :



De gauche à droite :
- la centrale de programmation
- le gestionnaire avec son écran graphique tactile (version 2, pas la dernière !) et le controlleur d'aiguilles (face blanche)
- le TCO à boutons et leds
- la centrale va-et-vient pour le métro
- le module de traction avec ses 12 potars

Il manque le contrôleur des signaux et le contrôleur du décor, ainsi qu'un écran vidéo pour les caméras.

De quoi s'amuser encore un certain temps ....
C'est tout en plexi et Arduino (il y en a 6 pour le moment, un Uno, un Due et 4 Megas)
« Modifié: décembre 15, 2017, 11:38:05 pm par Dominique »

Dominique

  • Global Moderator
  • Hero Member
  • *****
  • Messages: 1209
  • 100% Arduino et N
    • Voir le profil
Re : Projet Dominique
« Réponse #37 le: décembre 21, 2017, 07:16:10 pm »
Le logiciel du module de traction

Je ne vais pas recopier tout le logiciel d'un coup, 1700 lignes, ce serait imbuvable !

Mais je vais distiller des morceaux que vous pourrez récupérer et re-utiliser dans vos projets.

D'abord un numéro et une date de version : ça permet d'éviter de se mélanger les pinceaux quand le projet s'étale dans le temps :

const char VersTRACTION[20] =  "DCCppTract08 141217"; // 19 caractères + \0
Les bibliothèques utilisées :

#include "DCCpp.h"              // La bibliothèque de Thierry

#include <Wire.h>               // bus I2C
#include <Streaming.h>          // http://arduiniana.org/libraries/streaming/
#include <LiquidCrystal_I2C.h>  // I2C adresse 0x27
#include <SPI.h>                // bus SPI
#include <mcp_can.h>            // bus CAN
#include "Adafruit_MPR121.h"    // I2C adresse 0x5A
#include <Encoder.h>            // encodeur
#include <Bounce2.h>            // boutons poussoir
#include <EEPROM.h>             // Eeprom integree
#include "Classes.h"            // Ma classe CmdLoco pour commander les locos

Pour utiliser la bibliothèque DCCpp de Therry, rien de plus simple :

#define MOTOR_SHIELD_TYPE   0           // LMD18200
#define COMM_INTERFACE   0

#define LMD_DIR 12                      // DCC_MAIN = Arduino Mega - uses OC1B - DIR LMD18200
#define DCC_MAIN 12
#define LMD_PWM 3                       // DCC_ENABLE = PWM LMD18200
#define DCC_ENABLE 3
#define Max471  A0                      // current sensor

Je redéfini dans mon programme les valeurs qui sont dans config.h et dans DCCpp.h et il ne me reste plus qu'à démarrer DCCpp dans le Setup :

DCCpp::begin();
DCCpp::beginMain(UNDEFINED_PIN, LMD_DIR, LMD_PWM, Max471);        // Dc: Dir, Pwm, current sensor

Recherche des adresses des terminaux I2C

J'installe le sketch I2C_Scanner et j'obtient les adresses (je recopie le résultat dans mon programme, mais en commentaire :

//////////// I2C ///////////
// I2C device found at address 0x23  = NRF24L01
// I2C device found at address 0x27  = lcd
// I2C device found at address 0x5A  = clavier tactile

Cela me permet de déclarer l'écran LCD et le clavier tactile (qui fonctionne en 3,3V, attention, il ne faut pas oublier d'ajouter un circuit convertisseur de niveau)

///////////// LCD a l'adresse I2C = 0x27 ////////////
LiquidCrystal_I2C lcd(0x27,20,4);  // set the LCD address to 0x27 for a 20 chars and 4 line display

///////////// RETRO-ECLAIRAGE LCD ///////////////////
#define eclairage  180000   // extinction auto du retro-eclairage au bout de 3 minutes
unsigned long retrotime;
unsigned long lblinktime;

///////////// Touchpad à l'adresse I2C = 0x5A ///////
Adafruit_MPR121 cap = Adafruit_MPR121();
uint16_t lasttouched = 0;
uint16_t currtouched = 0;
int Key;
// Attach the MPR121's IRQ pin to digital pin 4
const int PIN_TOUCH_IRQ = 4;

Puis on installe ces périphériques dans le setup :

  //--- init Serial et LCD
  pinMode(PIN_TOUCH_IRQ, INPUT);// pour l'interruption du clavier tactile
Serial.begin(115200);
  Serial.flush();
  Serial.println(VersTRACTION); // affichage du N° de version sur le Terminal
  lcd.init();                   // initialize the lcd
  lcd.clear();
  lcd .backlight();
  lcd.noBlink();

Le bus CAN

Je fais carrément un copier-coller depuis mes autres programmes (ceux des autres modules) :
- Constructeur de l'objet CAN
- Définitions et variables de la memoire tampon circulaire pour y stocker immédiatement tout message reçu
- routine d'interruption du CAN, qui ne fait que monter un flag
- liste des identifiants utilisés dans mon réseau, qui concernent ce module de traction

///////////// CAN BUS //////////////
MCP_CAN CAN(53);                    // Set CS to pin 53 (MEGA)
volatile byte Flag_Recv = 0;        // flag pour l'interruption IRQ

//--- Message recu
byte IdR;                           // Id pour la routine CAN_recup()
unsigned char lenR = 0;             // Longueur "    "       "
unsigned char bufR[8];              // buffer reception      "

//--- Message emis
unsigned char bufS[8];              // buffer emission

//--- Memoire circulaire pour le stockage rapide des messages recus
unsigned char _Circule[512];          // recepteur circulaire des messages CAN sous IT
unsigned int _indexW, _indexR, _Ncan; // index d'ecriture et lecture, nb d'octets a lire
byte _CANoverflow = 0;                // flag overflow (buffer _Circule plein)

int curIndex;                         // loco désignée dans un message CAN
bool DCC_OK_Central = false;

//--- Interruption (ISR) CAN
void MCP2515_ISR()
{
     Flag_Recv = 1;
}

//--- Ids des messages
#define RId_LOCO        0x30        // commande central pour un train (Index, Vit, Dir, F0)
#define RId_F0          0x31        // commande central F0
#define RId_URGENT      0x32        // arrêt urgence (0x8F) et reprise (0x0F)
#define RId_PARAM       0x33        // configuration (index, @dcc, Vmin, Vmax, cran30, cran60)
#define RId_POSTE       0x38        // affichage sur poste de conduite (0x00..0x3F)
#define TId_LOCO        0x13        // envoi des paramêtres de conduite (Index, Vit, Dir, F0)
#define TId_F0          0x14        // envoi F0 d'une loco (index, F0)
#define TId_DCC         0x15        // envoi état DCC (0x80 = on; 0x00 = off)
#define TId_ICC         0X16        // envoi intensité (mA/16) et alarme CC (bit 7)
#define TId_VV          0x17        // envoi consignes V&V (vitesses aller et retour)
#define TId_VITESSE     0x18        // envoi mesure de vitesse d'un train
#define TId_TRACT_CAN   0x1E        // envoi alarme CAN overflow

Et puis la routine appelée chaque fois que le Flag_Recv est monté :

//--- Routine de récuperation des messages CAN dans la memoire circulaire _Circule
//--- appelee par LOOP lorsque Flag_Recv = 1;
 
void CAN_recup()
{
  unsigned char len = 0;                // nombre d'octets du message
  unsigned char buf[8];                 // message
  unsigned char Id;                     // Id

  while (CAN_MSGAVAIL == CAN.checkReceive())  {
    CAN.readMsgBuf(&len, buf);          // read data, len: data length, buf: data buf
    Id = CAN.getCanId();
    if ((_Ncan+len+2) < sizeof(_Circule))  { // il reste de la place dans _Circule
      _Circule[_indexW] = Id;           // enregistrement de Id
      _indexW++;
      _Ncan++;
      if (_indexW == sizeof(_Circule))  {_indexW = 0;}
      _Circule[_indexW] = len;          // enregistrement de len
      _indexW++;
      _Ncan++;
      if (_indexW == sizeof(_Circule))  {_indexW = 0;}
      for (byte z = 0; z<len; z++)  {
        _Circule[_indexW] = buf[z];      // enregistrement du message
        _indexW++;
        _Ncan++;
        if (_indexW == sizeof(_Circule))  {_indexW = 0;}
      }
    } else {
      _CANoverflow = 1;  // depassement de la capacite de Circule
    }
  }
}

On remarque que cette routine assure la gestion des débordements (overflow) éventuels de cette mémoire tampon, ce qui ne m'est jamais arrivé.

On trouvera plus d'explications sur le CAN dans l'article ici : http://www.locoduino.org/spip.php?article148 et un peu plus loin dans ce sujet.

Petite pause pour se détendre, ensuite on verra un certain nombre de routines utilitaires écrites pour décharger la loop et la rendre plus lisible.

A suivre ...
« Modifié: décembre 31, 2017, 10:23:22 am par Dominique »

bricoleau

  • Jr. Member
  • **
  • Messages: 51
    • Voir le profil
Re : Projet Dominique
« Réponse #38 le: décembre 21, 2017, 10:14:38 pm »
Super intéressant

Je te soumets une petite analyse personnelle, en ce qui concerne le LCD texte à interface I2C :

Au travers de la bibliothèque <LiquidCrystal_I2C.h>, l'arduino envoie des données via le bus I2C, à un io expander PCF8574, lui même connecté aux entrées du contrôleur HD44780 de l'écran LCD.
Le PCF8574 a 8 sorties parallèles :
  • 1 est utilisée pour commander directement le rétro-éclairage du LCD (en on/off)
  • 4 sont utilisées pour le transfert de données vers le HD44780, qui est mis en mode 4bits (chargement demi-octet par demi-octet).
  • 2 sont utilisées pour les entrées de commande E et RS du HD44780
  • 1 est non utilisée ou cablée sur l'entrée de commande R/W (pas très utile)
Le principe de fonctionnement du PCF8574 est très simple : l'arduino lui transmet un octet, dont les 8 bits correspondent aux 8 sorties du PCF8574.

Le bus I2C fonctionne au max à 400 kb/s sur arduino (100 kb/s en standard)
Ce qui, déduction faite des bits de contrôle START, ACK etc. permet de faire passer grosso modo un débit de 40 octets par milliseconde, adresse incluse (elle prend aussi un octet).

Pour chaque octet utile à transmettre depuis l'arduino vers le LCD (= un caractère à afficher, ou une commande d'effacement écran, ou une commande de positionnement de curseur), il y a 2x6=12 octets qui passent sur le bus I2C :
  • adresse 0x27 + premier demi-octet
  • adresse 0x27 + premier demi-octet avec activation de la commande "enable"
  • adresse 0x27 + premier demi-octet avec relâchement de la commande "enable"
et pareil pour le second demi-octet

Au niveau du programme, l'instruction de base LCD.print(chaine) ne rend la main qu'à la fin des émissions sur le bus.
Dans le pire de cas, la chaîne fait 20 caractères, soit 20x12 = 240 octets de trafic sur le bus I2C, ce qui va durer 6 ms.

En supposant que je ne me suis pas trompé dans le calcul (de toute manière facile à vérifier par mesure de temps dans le programme), cela fait 6 millisecondes pendant lesquelles l'arduino ne fait aucune lecture des buffers de réception du MCP2515.

Si l'écosystème fait qu'il n'y a aucune chance de recevoir plusieurs messages CAN pendant ce laps de temps, alors pas de problème.
Sinon, il y a un risque de perte de message au niveau du mcp2515, pour cause de buffers de réception pleins.


Pour réduire le risque :
N'afficher que les caractères qui ont changé
Effectuer des LCD.print caractère par caractère, en testant la réception MCP2515 entre chaque, cela réduit le tunnel à 300 µs.

On peut faire mieux, mais il faut réécrire <LiquidCrystal_I2C.h>
« Modifié: décembre 21, 2017, 10:30:04 pm par bricoleau »

Dominique

  • Global Moderator
  • Hero Member
  • *****
  • Messages: 1209
  • 100% Arduino et N
    • Voir le profil
Re : Projet Dominique
« Réponse #39 le: décembre 21, 2017, 11:32:17 pm »
Je n’ai pas fait ce calcul du temps de blocage du cpu pendant un lcd.print, mais je m’en doutais.

Il y a normalement tout ce qu’il faut dans le MCP2515 pour ne pas perdre de message : le protocole contient un accusé de réception qui force l’emetteur à répéter une transmission quand il reçoit un  «acknowledge error». Le MCP2515 contient 3 buffers de réception, un avant les filtres et 2 après les filtres. Un message est d’abord reçu dans le 1er buffer et sera transmis dans l’un des 2 autres si le filtre l'autorise et si l’un des buffers est disponible. Sinon il y aura une «acknowledge error» et la transmission sera répétée.

Pour qu’un message soit répété il faut qu’il ne soit pas reçu, ni par le récepteur prévu pour cela, ni par un espion placé sur le bus Can. J’ai réalisé un tel outil pour voir le trafic sur mon bus et là j’ai commencé à perdre des messages parce qu’ils étaient réputés reçus.

C’est pour ça aussi que le choix des identifiants et des filtres est si important !

Donc, pour revenir à notre problème d’ecran LCD/i2c, je ne m’inquiète pas trop car le bus CAN peut attendre. Mais ce serait mieux d’améliorer l’afficheur, théoriquement.

Pratiquement, comme j’ai choisi le DCC, le bus CAN a relativement peu de messages à transmettre et il est toujours possible d’ajouter un peu de contrôle de flux applicatif, et encore ce n’est pas certain.

Personnellement je suis confiant pour avoir un réseau qui tourne depuis un bon moment.

En cas de problème il y a des solutions ,
- doubler le bus CAN pour alléger le trafic (mais ça fait 2 fils de plus a brancher)
- remplacer l’interface I2C du LCD par un Arduino mini qui servira de buffet d’affichage. C’est pas plus cher !
- remplacer l’i2c par du SPI.

Jean-Luc a plus d’expérience que moi sur le CAN et un bus CAN plus chargé. Il pourra éventuellement compléter ces explications.


« Modifié: décembre 21, 2017, 11:38:58 pm par Dominique »

Dominique

  • Global Moderator
  • Hero Member
  • *****
  • Messages: 1209
  • 100% Arduino et N
    • Voir le profil
Re : Projet Dominique
« Réponse #40 le: décembre 30, 2017, 07:32:35 pm »
Puisqu'on aborde les communications par le bus CAN, je vais décrire ce que j'ai réalisé pour le moment.

D'abord un petit rappel de ce que j'ai compris du CAN :

Le bus CAN possède plusieurs caractéristiques intéressantes. Il est :
• Temps-réel. C’est à dire que les temps de transmission sont déterministes et peuvent être calculés à priori.
• Peu coûteux. Les contrôleurs CAN sont soit disponibles en tant que composant destiné à être connecté à un micro-contrôleur, soit intégrés directement dans un micro-contrôleur. La quantité de logiciel nécessaire pour le faire fonctionner est relativement succincte. Les composants CAN sont fabriqués en grand nombre, ce qui abaisse leur coût.
• Multi-maître asynchrone. pas d’arbitre centralisé, pas d’horloge globale, une station peut émettre dès que le bus est libre.
• Priorité entre les messages. Les collisions, quand plusieurs contrôleurs CAN émettent sur le bus simultanément, sont résolues sans destruction du message le plus prioritaire.
• Messages de taille limitée. 8 octets de données au plus.
Le bus CAN est fiable :
• Une détection d’erreur est incluse. La probabilité qu’une erreur reste non détectée est inférieure à 4,7 10-11.
• Le protocole définit une surveillance très stricte du bus.
• La cohérence des données est garantie. Un message est soit accepté par tous les contrôleurs CAN du réseau, soit rejeté par tous.
• Acquittement des messages par toutes les stations, le signalement d’erreur émis par une station est reçu par toutes les stations.
• Retransmission automatique des messages après signalement d’erreur.
• Le confinement des erreurs. Un contrôleur CAN gère de façon autonome les erreurs temporaires qu’il détecte.
Les performances
Le bus CAN propose des débits allant de 100kbits/s à 1Mbits/s.
La distance maximale entre 2 noeuds dépend de cette vitesse : 100 m à 500kb/s, ou 500 m à 125 kb/s, ce qui ne pose aucun problème pour nos réseaux, même excessivement grands !

Qu’est-ce qu’un message ?
Quand on construit un système avec un bus CAN, on définit une messagerie : l’ensemble des messages qui vont transiter et leur identifiant.
Les messages CAN sont courts, 94 bits maximum. Il n’y a pas d’adresse explicite donc aucun système de routage de messages entre un émetteur et un récepteur. Tout le monde reçoit tout et doit filtrer pour ne traiter que ce qui le concerne.
La partie "données" d’un message contient un identifiant de 11 bits (mode standard) ou 29 bits (mode étendu que je n'utilise pas dans cette application) et des données de 0 à 8 octets. L’identifiant est indispensable, les données non.
Dans mon application, je n’utilise pratiquement qu’un seul octet de données, en plus de l’identifiant. Seuls quelques cas spécifiques comme les messages de configuration utilisent plusieurs octets.
Comme dans tout système de transmission, il y a d’autres bits de contrôle et de calcul d’erreur qui servent uniquement au circuit d’interface MCP2515, donc il n’est pas besoin d’en parler, « ça se débrouille tout seul » !

L’identifiant joue un rôle particulièrement important
Il sert à plusieurs choses :
• l’arbitrage en cas de contention, c’est à dire quand 2 ou plusieurs messages sont émis sur le bus simultanément,
• le filtrage, c’est à dire la possibilité pour le récepteur de ne recevoir que ce qui l’intéresse,
• l’adressage qui n’est pas un adressage classique de messagerie, mais qui est utilisé dans ce but, d’abord au moyen du filtrage, puis par le récepteur pour déterminer à quel fonction le message est destiné.

En cas de contention, chaque émetteur regarde si un identifiant émis est plus prioritaire que le sien. Si oui, il stoppe d’émettre et attend son tour. Celui qui a l’identifiant le plus prioritaire continue d’émettre comme si de rien n’était. Il n’y a donc pas de perte de temps !
La règle de priorité est simple : le plus prioritaire est le plus petit !
Une autre règle en découle, qui est que 2 émetteurs différents ne doivent pas émettre de messages avec un même identifiant.

Par exemple, supposons que le Central du réseau veuille envoyer des ordres à des moteurs d’aiguillage (donc à la carte de commande d’aiguilles) et d’autres ordres à des signaux (donc à la carte de commande des signaux) et d’autres encore à la traction (donc la carte générateur du DCC ou du PWM ).
On va donc définir une série d’identifiants pour les messages destinés exclusivement aux commandes d’aiguille, par exemple la série de 16 identifiants de Id = 0x40 à 0x4F, une autre série d’identifiants pour les commandes de signaux, par exemple Id = 0x50 à 0x5F, et une autre pour la traction, par exemple Id 0x30 à 0x3F.
On remarque au passage qu’on ne s’intéresse qu’aux messages reçus par la carte CAN dont il faut définir les masques et les filtres.

On va rester en trame standard, donc les identifiants font 11 bits. Les valeurs de 1 à 2047 sont largement suffisantes pour tous les types de messages possibles. Et même en gardant uniquement les 8 bits de poids faible, 256 types de messages me semblent suffisants.

Supposons qu’au maximum il y ait 22 moteurs d’aiguillage et quelques dételeurs : 5 bits suffiront dans un unique octet de message pour désigner un moteur d’aiguilles ou un dételeur, les 3 bits supplémentaires permettant de définir les types de commande qui s’appliquent aux aiguilles.

Les réponses de la carte d’aiguilles vers le gestionnaire de réseau et/ou d’autres cartes étant filtrées par leur destinataire, les Id des messages émis seront donc pris dans la liste des Id qu’elles acceptent de recevoir.

Par exemple, pour gérer mes aiguilles, les commandes reçues seront décodées en fonction de l’octet de données qui a la forme suivante :
• les bits 0 à 5 définissent le numéro de l’aiguille ;
• les bits 6 et 7 portent la consigne à appliquer.
- bit 7 = 1, bit 6 = 1 : sens « droit »
- bit 7 = 1, bit 6 = 0 : sens « dévié »


Comme la carte de commande d’aiguilles ne doit réagir qu’aux ordres émis par le central (avec l’Id = 0x40) et comme le TCO permet de positionner les aiguilles "à la main" avec des clés, chaque fois qu’un opérateur tournera une clé d’aiguille sur le TCO, la demande du TCO sera d’abord adressée au Central. Ce dernier devra la valider avant de l’envoyer à l’aiguille si l’opération est possible.
Dans ce schéma, le TCO enverra les même données que dans les commandes ci-dessus mais avec un Id = 0x11 (dans la plage des Id acceptés par le Central). Dès que le Central les aura validées, ce dernier les renverra alors avec l’Id = 0x40 et elles seront exécutées par la carte de commande d’aiguilles. Ensuite les réponses de la carte d’aiguilles avec l’Id = 0x12 seront reçues par le central (pour la mise à jour des tables de positions ou des objets aiguilles) et ce dernier enverra un message vers le TCO avec l’Id 0x20 pour l’allumage de la diode verte qui représente la position d’aiguille (et l’extinction de l’autre Led) à coté de la clé de commande.

Le schéma des échanges est le suivant pour une commande d’aiguille :
Demande TCO -> Id=0x11-> Central -> Commande -> Id=0x40 -> Aiguilles
Réponse état Aiguilles -> Id=0x12 -> Central -> Etat -> Id=0x20 -> TCO

Dans le projet de réseau, il faut donc étudier tous les échanges possibles et dresser le tableau des Id et des données de tous les messages.
Rappel : ce qui compte, c’est de rassembler les identifiants des messages reçus dans une plage qui permet de définir les masques et filtres de façon pratique.

Et les filtres alors ?
On l’a entrevu dans la routine de setup : il y a 2 masques et 6 filtres.
Les masques définissent les bits de l’identifiant qui sont significatifs (sinon ils sont ignorés par le filtre).
Les filtres font que seuls les identifiants présentés sont acceptés Les bits du masque travaille avec les bits des filtres de même position.
Un bit « 0 » du masque permet d’accepter n’importe quel bit de l’identifiant (pas de filtre). Cela permet d’éviter de filtrer tous les bits et donc d’accepter des plages entières d’identifiants.
Un bit « 1 » du masque autorise le filtrage : pour qu’un bit de l’identifiant soit accepté, il doit être identique à celui du filtre. Tous les bits de l’identifiant doivent être acceptés pour que l’identifiant soit accepté.

Exemple :
Filtrage total pour accepter seulement l’Id = 0x317 dans un buffer
•   - Masque = 111 1111 1111 •   - Filtre = 011 0001 0111
Filtrage partiel pour accepter une plage d’Id de 0x310 à 0x317
•   - Masque = 111 1111 1000 •   - Filtre = 011 0001 0xxx
Pas de filtrage du tout
•   - Masque = 000 0000 0000 •   - Filtre = xxx xxxx xxxx

Dans mon cas, je n’ai qu’une carte de commande d’aiguilles, le masque est fixé à 0x7F0 et les filtres définissent les identifiants des seuls messages attendus : 0x40 à 0x4F.

Dominique

  • Global Moderator
  • Hero Member
  • *****
  • Messages: 1209
  • 100% Arduino et N
    • Voir le profil
Re : Projet Dominique
« Réponse #41 le: décembre 30, 2017, 07:54:29 pm »
Voici la liste des identifiants que j'ai prévu : une plage de 16 identifiants est réservée à chaque module (32 pour le Gestionnaire). Les valeurs sont données en hexadécimal.

Gestionnaire : 00..1F
Gestionnaire global du réseau, il reçoit toutes les demandes du TCO (cles d’aiguilles, occupations/libérations de zones), de la traction (commandes des pilotes, état des générateurs DCC), des Aiguilles (positions), tous les autres capteurs.
Il assure l’IHM, l’interface avec le WiFI, les commandes sans fil, la configuration, la gestion du temps, etc..

TCO, occupations de zones : 20..2F
Affiche (leds rouges) et transmet au Gestionnaire les occupations/libérations de zones et les positions des clés d’aiguilles (actions opérateurs). Reçoit des Aiguilles les positions réelles d’aiguilles affichées par led vertes.

Traction DCC - réseau principal - va-et-vient : 30..3F
Transmet au Gestionnaire les consignes de conduite (potentiomètre de vitesse, clé direction et lumière FL et fonctions Fx), reçoit du gestionnaire les commandes de vitesse/direction/Fonction pour chaque train. Gère la génération DCC et l’interface au Booster, ainsi que les relais de protection des zones Va-et-Vient et Programmation.

Aiguilles, dételeurs : 40..4F
Reçoit les ordres de positionnement des aiguilles et dételeurs du Gestionnaire, la sauvegarde en EEPROM des positions à l’arrêt du réseau pour les retrouver au démarrage. Transmet au Gestionnaire et au TCO les positions d’aiguilles.

Signalisation : 50..5F
Reçoit du Gestionnaire les états des signaux et commande les signaux en conséquence.

Autres capteurs : 60..6F
Gèrent les capteurs complémentaires comme les zones d’arrêt spécifiques (quai), les capteurs RFID, etc.. Transmet les événements au Gestionnaire.

Autres actionneurs : 70..7F
Reçoit les ordres du Gestionnaire pour actionner des organes comme le passage à niveau

Animation décor : 80..FF
Ensemble de capteurs et d’actionneurs liés à l’animation du décor

Pour le moment je n'ai pas réalisé le module de commande du décor, et je lui ai attribué la moitié des identifiants.
Je pense évoluer en créant un second bus CAN dédié aux éléments du décor. Ainsi je pourrai étendre les capacités du bus principal et disposer d'un bus décor très étendu, tout en gardant seulement 8 bits significatifs pour les identifiants.


Dominique

  • Global Moderator
  • Hero Member
  • *****
  • Messages: 1209
  • 100% Arduino et N
    • Voir le profil
Re : Projet Dominique
« Réponse #42 le: décembre 31, 2017, 10:57:01 am »
Ce qui m'amène à traiter les messages CAN de la manière suivante :

En entrée, j'ai donc des messages possédant un identifiant RId et un ou deux octets de données (mais potentiellement jusqu'à huit) dans la table Rbuf[], avec sa longueur Rlen.

Je commence donc à récupérer chaque message dans le buffer circulaire :
Un message existe si le nombre d'octets présent dans le buffer est au moins égal à 3.


  while (_Ncan > 2)  {                // messages dans _Circule : au moins 3 bytes
    _Ncan--;
    RId = _Circule[_indexR];          // recuperation de l' Identifiant
    _indexR++;
    if (_indexR == sizeof(_Circule))  {_indexR = 0;}
    _Ncan--;
    Rlen = _Circule[_indexR];         // recup longueur
    _indexR++;
    if (_indexR == sizeof(_Circule))  {_indexR = 0;}
    for (int k = 0; k < Rlen; k++)  {
      _Ncan--;
      Rbuf[k] = _Circule[_indexR];    // recuperation des octets du message
      _indexR++;
      if (_indexR == sizeof(_Circule))  {_indexR = 0;}
    } // le message est dans les globales RId, Rlen et Rbuf[..]

    //-- Traitement des messages CAN (commandes du gestionnaire)

  } // fin traitement messages CAN

Ensuite le traitement de chaque message (à ajouter sous le commentaire dans le code ci-dessus) se résume à un switch - case :

    //-- Traitement des messages CAN (commandes du gestionnaire)
 
    curIndex = Rbuf[0] & 0x3F;       // ne garder que les bits 0..5 = index loco
    if (curIndex < MaxLocos) {
    switch (RId)  {
      case RId_LOCO:                 // 0x30 commande central pour un train (vitesse et direction)
      CanSpeed = Rbuf[1] & 0x7F;
      switch (CanSpeed) {
        case 30:
        gLocoSpeed[curIndex] = gConfigLocos[curIndex].cran30;
        break;
        case 60:
        gLocoSpeed[curIndex] = gConfigLocos[curIndex].cran60;
        break;
        case 90:
        gLocoSpeed[curIndex] = gConfigLocos[curIndex].cran90;
        break;
        default:
        gLocoSpeed[curIndex] = CanSpeed;
        break;
      }
      DccDirection = bitRead(Rbuf[1], 7);
      gLocoDirection[curIndex] = DccDirection;
      if (gConfigLocos[curIndex].inverse) {DccDirection = !DccDirection;}
      DCCpp::setSpeedMain(gConfigLocos[curIndex].registre, gConfigLocos[curIndex].dccAddress, gLocoStepsNumber, gLocoSpeed[curIndex], DccDirection);
      gChange = true;
      break;
     
      case RId_F0:                  // 0x31 commande central F0
      if (bitRead(Rbuf[0], 7)) {gLocoFunctions[curIndex].activate(0);} else {gLocoFunctions[curIndex].inactivate(0);}
      DCCpp::setFunctionsMain(gConfigLocos[curIndex].registre, gConfigLocos[curIndex].dccAddress, gLocoFunctions[curIndex]);
      gChange = true;
      break;
     
      case RId_URGENT:              // 0x32 arrêt urgence et reprise
      if (Rbuf[0] == 0x8F) {                  // arret immediat
        stop_DCC();
        DCC_OK_Central = false;
      }
      if (Rbuf[0] == 0x0F) {                  // reprise
        start_DCC();
        DCC_OK_Central = true;
      }
      break;
     
      case RId_PARAM:                 // 0x33 configuration
      //curManette = Rbuf[0] >> 4;    // manette = quartet poids fort,
      break;
     
      case RId_POSTE:                 // 0x38 affichage du central vers poste de conduite (ligne 1)
      // afficher l'adresse DCC sur la manette, l'indication F0, ...
      break; 
      } 
    } 

Je n'ai pas mis tous les cas à traiter car je ne les ai pas encore étudiés mais l'ajout ultérieur de ces cas sera simple : il suffit d'ajouter le traitement présenté en commentaire.

Quelques explications sur les variables apparaissant dans le code :

curIndex désigne une loco parmi 12 (MaxLocos).
gLocoSpeed[curIndex] désigne la vitesse de cette loco dans une table de 12 valeurs.
gConfigLocos[curIndex] désigne une structure contenant les paramètres permanents d'une loco, dans ue table de 12 structures.
Cette structure est :

struct LocoCONFIG {
  byte registre;                      // numero de registre = pot+1
  byte active;                        // loco active (1=oui)
  int  dccAddress;                    // adresse DCC loco courte ou longue
  byte cran30;                        // cran DCC pour 30 km/h
  byte cran60;                        // cran DCC pour 60 km/h
  byte cran90;                        // cran DCC pour 90 km/h
  byte inverse;                       // inversion avant (0) /arriere (1=oui)
  byte lumiere;                       // lumiere FL (1=oui)
  byte MaxSpeed;                      // vitesse maxi souhaitable en cran DCC
  byte MinSpeed;                      // vitesse mini possible
};

L'analyse de la valeur de vitesse transmise dans le message CAN par un autre switch montre que je m'intéresse particulièrement aux vitesses 30, 60 et 90 (km/h à l'échelle 1/160 pour le N).
L'interface utilisateur du module de traction permet de configurer les crans DCC qui correspondent aux vitesses réelles 30, 60 et 90 km/h.
Quand une de ces vitesses est demandée par le Gestionnaire, c'est cette valeur qui est envoyée à la commande DCC, sinon, c'est la valeur contenue dans le message (qui le Gestionnaire n'a aucune raison d'envoyer).

DccDirection est la direction indiquée dans le message CAN (bit 7 du premier octet).
Ensuite on teste la valeur inverse configurable, dans la structure, pour tenir compte du sens réel de la machine.

Et enfin, le commande DCC est envoyée grâce à la bibliothèque DCCpp :
DCCpp::setSpeedMain(gConfigLocos[curIndex].registre, gConfigLocos[curIndex].dccAddress, gLocoStepsNumber, gLocoSpeed[curIndex], DccDirection);
On continue ensuite par le traitement des fonctions de la loco, l'arrêt d'urgence et la reprise (en cas de court-circuit) et d'autres fonctions que nous verrons plus tard.

Dominique

  • Global Moderator
  • Hero Member
  • *****
  • Messages: 1209
  • 100% Arduino et N
    • Voir le profil
Re : Projet Dominique
« Réponse #43 le: décembre 31, 2017, 11:20:19 am »
Maintenant que nous avons une commande des trains par le bus CAN, je vais décrire la commande des trains par les potentiomètres.
Nous avons ce schéma :


Chaque train est piloté par un potentiomètre et un bouton poussoir, avec une led verte indiquant la marche avant et une led rouge indiquant la marche arrière.

Le challenge qui se présentait était multiple :
- écrire une seule fois le code pour les 12 commandes;
- assurer la gestion des anti-rebonds pour les 12 boutons (l'utilisation de la bibliothèque Bounce2 n'étant pas facile dans ce cas);
- gérer un ensemble de variables indépendantes pour chaque commande.

J'ai donc créé une classe pour gérer ces commandes, la classe permettant d'encapsuler les variables et les fonctions :
class CmdLoco {
private :
  int pinLedVerte; // et bouton
  int pinLedRouge;
  int pinPotar;
  unsigned long previousTime ;
  unsigned int intervalTime;
  int etat; // 0 : attente, 1 : 1er changement, 2 : confirmation
  bool buttonVState;
  bool marcheAvant;
  int potValue;
 
public :
  CmdLoco(int pLV, int pLR, int pPot, int pIt) {           // constructeur
    pinLedVerte = pLV;
    pinLedRouge = pLR;
    pinPotar = pPot;
    intervalTime = pIt;
  }

  void CmdInit() {
    marcheAvant=true;
    pinMode(pinLedVerte, OUTPUT);
    pinMode(pinLedRouge, OUTPUT);
    digitalWrite(pinLedVerte, !marcheAvant);        // allume si false
    digitalWrite(pinLedRouge, marcheAvant);         // eteinte
    previousTime = millis();
    this->etat = 0;
    buttonVState = true;   
  }

  void MajLed(bool d) {
    digitalWrite(pinLedRouge, d);
    digitalWrite(pinLedVerte, !d);      // allume si false
  }

  bool CmdMaj( bool d) {
    bool dir = d;
    bool change = false;
    bool buttonValue;
    if (millis()-previousTime >= intervalTime) {   
      previousTime = millis();   
      pinMode(pinLedVerte, INPUT);
      buttonValue = digitalRead(pinLedVerte);
      if (buttonValue != buttonVState) {    // changement d'etat de la pin
        switch (this->etat) {
          case 0:
          if (!buttonValue) {               // 1er appui
          this->etat = 1;                   // attente confirmation
          }
          break;
          case 1:
          if (!buttonValue) {               // confirmation d'appui
            buttonVState = buttonValue;     // nouvel ancien etat de la pin
            dir = !dir;                     // inversion sens de marche
            this->etat = 2;                 // appui en cours, do nothing
            change = true;                  // job done !
          }
          break;
          case 2:
          if (buttonValue) {                // relaché
            this->etat = 3;                 // attente confirmation
          }
          break;
          case 3:
          if (buttonValue) {                // relaché
            buttonVState = buttonValue;     // nouvel etat
            this->etat = 0;                 // fin process
          }
        }
      }
      pinMode(pinLedVerte, OUTPUT);
      digitalWrite(pinLedRouge, dir);
      digitalWrite(pinLedVerte, !dir);      // allume si false
    }
    return (change);
  }
 
}; // fin classe CmdLoco

Ainsi nous aurons regroupé :
- la fonction CmdInit() qui doit initialiser chaque objet dans le setup();
- la fonction MajLed(bool d) qui allume l'une des 2 leds verte et rouge;
- la fonction CmdMaj( bool d) qui va tester l'état du bouton-poussoir et retourner un true si le bouton est enfoncé, tout en gérant l'anti-rebond  :D :D

Pour mettre en place ces objets, il faut créer une table en variables globales :
//--- TABLE DES LOCOS
CmdLoco * gTableCloco[MaxLocos];          // Table des locos

et l'initialiser dans le setup() :

  //--- creation des objets Cloco et de la table
  for (int L=0; L<MaxLocos; L++) {
    gTableCloco[L] = new CmdLoco(ledVerte[L],ledRouge[L],gPotPin[L], 25); // ledVerte, ledRouge, pot, intervalTime)
  }
  //--- initialisation des objets Cloco
  for (int L=0;L<MaxLocos;L++) {
    gTableCloco[L]->CmdInit();   
  }

Pour gérer les potentiomètres j'ai quelques variables globales que j'aurais très bien pu intégrer à la classe CmdLoco :

int gSensorValue = 0;           // value read from the pot
int gOutputValue;
int gPot[MaxLocos];
bool gPotChange = false;

Enfin, la gestion de toute cette interface, dans la loop(), se résume à ces quelques lignes de code :

  // potentiometres

  for (int i = 0; i < 12; i++) {
    gSensorValue = analogRead(gPotPin[i]);
    gOutputValue = map(gSensorValue, 0, 1023, 0, gConfigLocos[i].MaxSpeed);
    if (gOutputValue != gPot[i]) {
      gPot[i] = gOutputValue;
      gLocoSpeed[i] = gPot[i];
      gPotChange = true;
      DccDirection = gLocoDirection[i];
      if (gConfigLocos[i].inverse) {DccDirection = !DccDirection;}
      DCCpp::setSpeedMain(gConfigLocos[i].registre, gConfigLocos[i].dccAddress, gLocoStepsNumber, gLocoSpeed[i], DccDirection);     
    }
  }
 
  // boutons et leds de direction

  for (int u=0;u<MaxLocos;u++) {
    if (gLocoSpeed[u] == 0) {
      if (gTableCloco[u]->CmdMaj(gLocoDirection[u])) {     
        gLocoDirection[u] = !gLocoDirection[u];
      }
    }     
  }

« Modifié: décembre 31, 2017, 11:25:08 am par Dominique »

Dominique

  • Global Moderator
  • Hero Member
  • *****
  • Messages: 1209
  • 100% Arduino et N
    • Voir le profil
Re : Projet Dominique
« Réponse #44 le: décembre 31, 2017, 11:34:58 am »
Pour un 31 décembre, c'est déjà pas mal : Nous avons deux exemples de pilotage par la bibliothèque DCCpp :

  • Un pilotage par le bus CAN
  • Un pilotage par potentiomètre

Je vous souhaites à tous un excellent réveillon de la Saint Sylvestre et une bonne année 2018  ;)



Amicalement

Dominique