Souris et centrale sans fil

. Par : tony04. URL : https://www.locoduino.org/spip.php?article237

Cet article peut paraitre surprenant surtout à la suite du salon d’Orléans ou les solutions "tout automatique" ou "tout informatisé" présentées au magnifique stand Locoduino ont eu un énorme succès.
Il est vrai qu’il est très plaisant de voir circuler des trains en toute sécurité en les suivant à l’écran de son Smartphone, de sa tablette ou de son ordinateur, mais on peut aussi trouver beaucoup de plaisir à "jouer" manuellement avec une ou plusieurs machines, tout au moins sur une partie de son réseau.
L’originalité du projet tient entre autre dans le stockage maximum de 59 locos dans la souris, ce qui permet de travailler avec sa propre souris à la maison ou dans son club en ayant toutes ses machines sous la main (sous conditions d’avoir la même centrale au club).

Historique

En fréquentant des clubs de modélisme ferroviaire il est apparu que les souris utilisées ne convenaient pas toujours à tout le monde. L’idée d’en faire une sur mesure a germé et c’est en prenant en main le boitier HAMMOND que j’ai décidé de me lancer dans ce projet, tant ce boitier est agréable à manipuler. Le cahier des charges a été défini avec l’aide des membres de mon club. La demande était, entre autres, de pouvoir suivre en toute liberté son convoi tout au long du réseau, qui peut être très vaste dans certains clubs, ce qui m’a orienté vers le sans fil.
Après de longues semaines de tests avec les "pointures" du club, le projet est enfin abouti et prêt à être mis en ligne.
Depuis que j’utilise ce type de souris "liberté" il me serait difficile de revenir à une souris classique.
De plus aucun article sur ce sujet n’a jamais été présenté sur le site Locoduino (à ma connaissance en tous cas).
Cette souris/centrale sera facilement intégrable dans n’importe quelle configuration présentée sur Locoduino grâce à la présence d’un bus CAN dans la centrale.

Voici celle que je vous propose de réaliser :

Ma souris

En quoi consiste le projet ?
Il s’agit de réaliser une centrale 100% DCC (pas d’analogique) commandée par une ou plusieurs souris avec liaison radio (30 et plus sont tout à fait envisageables).

Les caractéristiques sont les suivantes :

La souris :

  • Boitier ergonomique, robuste et esthétique équipé d’un clavier alphanumérique.
  • Afficheur OLED 1,54’’ très lisible.
  • Mise en mémoire (dans la souris) de 59 machines avec leur adresse et leur nom sur 12 caractères.
  • Une "liste du jour" de 10 locos sélectionnables directement par appui de la touche * suivi de 0 à 9 du clavier. Chaque loco garde sa vitesse et ses fonctions en mémoire.
  • 28 fonctions standards vers DCC et 72 fonctions programmables envoyées sur le bus CAN de la centrale. La fonction lumière affiche une petite ampoule, les autres sont affichées dans la dernière ligne du bas.
  • Menu complet avec Liste triée sur nom, Liste triée sur adresse, Création nouvelle loco, Programmation de CVs.
  • Programmation automatique des adresses longues et courtes (ce qui modifie automatiquement les CV1, CV29, 17 et 18).
  • Programmation en mode POM possible.
  • Utilisation 24H en continu sans recharge selon la batterie installée.

La mémoire EEPROM de la souris comporte une "bibliothèque" de 59 locos avec un N° d’ordre, une adresse DCC et un nom. Il est possible de créer très rapidement une "liste du jour" de 10 locos maximum (parmi les 59), c’est avec cette liste qu’on va "jouer". Si le N° d’ordre de la loco est inférieur à 10 la loco est dans la liste du jour.
Je vous propose de feuilleter le mode d’emploi ci-dessous pour vous donner une idée plus précise de ses caractéristiques.

Mode d’emploi de la souris V1.21

La centrale :

La centrale est équipée de base :

  • d’un module Bluetooth (commande par Smartphone possible, programme Android disponible)
  • d’un module radio acceptant 6 souris (extensible par modules de 6 souris)
  • d’un bus CAN permettant toutes les extensions possibles
  • d’une sortie DCC voie MAIN 43A protégée selon les besoins du réseau
  • d’une sortie DCC voie de PROGRAMMATION de 3A.
    La centrale garde en mémoire les vitesses et fonctions même en cas de coupure de la tension DCC.
    Aucune commande ne se trouvant sur la centrale, celle-ci peut être cachée sous le réseau.

Est-il compliqué à réaliser ?

La réalisation de cet ensemble, et principalement de la souris, demande une grande rigueur mécanique, principalement au niveau de la découpe du boitier. Tous les gabarits de découpe, les plans, les schémas, les films CI, les sketchs et la liste des composants sont disponibles sur le site à la fin de cet article. Un circuit imprimé double face+trous métallisés+vernis épargne (pour la souris et pour la centrale) est également disponible sur simple demande.
Au niveau des sketchs, beaucoup "d’anciens" (ayant pratiqué l’assembleur) vont retrouver leur manière de programmer car je ne maîtrise absolument pas, à mon grand regret, le "monde des objets" si cher à Locoduino (à juste titre). Ce sera donc une programmation pas à pas avec un maximum de commentaires pour guider au mieux les utilisateurs.
Le coût de l’ensemble se situe aux alentours de 50€ pour la centrale et 40€ pour la souris.

Réalisation de la souris :

Commençons par le plus difficile, la souris.

Choix du matériel :

  • Ecran Oled pour sa qualité d’affichage.
  • Codeur incrémental avec bouton poussoir pour garder les vitesses de chaque loco en mémoire.
  • Clavier alphanumérique pour saisir facilement les noms des locos.
  • Arduino PRO/MINI 3,3V pour pouvoir utiliser une batterie de 3,7V très économique.
  • Module radio NRF24L01 pour ses 6 tampons mémoires émission/réception.

Voici le schéma complet de la souris ; ne figure pas les condensateurs de découplages conseillés sur l’alimentation du NRF24L01 ni les liaisons GND de certains composants :

Schéma souris

L’écran Oled et le NRF24L01 sont tous les deux contrôlés par le bus SPI du PRO/MINI, avec les bits A1, A2 et A3 pour sélectionner l’écran et les bits 9 et 10 pour sélectionner le NRF.
On utilise l’interruption 0 pour lire les rotations du codeur.
L’entrée A6 a la double fonction de lire le niveau de tension de la batterie à travers le pont de résistances 10K/20K et de lire l’état du bouton d’arrêt d’urgence Stop.
Je me suis gardé les bits 1 et A5 pour pouvoir éventuellement ajouter une mémoire EEPROM (SPI) extérieure de grosse capacité pour les personnes qui possèdent plus de 59 machines à mettre en mémoire.

Le programme de la souris :

Sketch V1.21 de la souris

J’ai laissé en tête de croquis plusieurs commentaires et liens qui m’ont permis de concevoir ce dernier. N’hésitez pas à consulter ces liens très enrichissants.

Voici les liens pour télécharger les différentes librairies :
Pour l’afficheur : https://github.com/olikraus/u8glib
Pour le NRF24L01 : https://github.com/nRF24/RF24
Pour l’anti-rebonds des touches : https://github.com/thomasfredericks...
Pour le clavier : http://www.arduino.cc/playground/up...

Je ne vais pas commenter le sketch complet, ce serait très vite rébarbatif, je vais juste présenter les blocs principaux avec leurs fonctions.

Les dix premières lignes permettent de modifier rapidement quelques comportements du programme (Attention ! Les N° de lignes ne correspondent pas forcément au programme).

const char version[] = "1.10";    // changer ce numéro pour version
#define tempo_alpha   700         // millisecondes pour touches alphanumériques (700)
#define tempo_touche  500         // millisecondes pour touches fonctions (500)
#define tempo_loco    1000        // millisecondes pour touches locos (500)
#define palier_1      9           // 1er palier de changement d'incrément
#define inc_palier_1  2           // incrément après 1er palier
#define palier_2      20          // 2è palier de changement d'incrément
#define inc_palier_2  5           // incrément après 2è palier
#define palier_3      50          // 3è palier de changement d'incrément
#define inc_palier_3  12          // incrément après 3è palier
// si on veut autoriser la programmation en mode POM, dé-commenter la ligne suivante
// #define aff_pom  1						

tempo_alpha permet de modifier la tempo entre 2 appuis successifs des touches alphanumériques
tempo_touche est la tempo maximum pour la saisie de la seconde valeur des touches de fonction qui se fait sur 1 ou 2 chiffres.
tempo_loco idem mais pour la saisie d’une loco dans la liste du jour avec la touche *
Les différents paliers permettent d’incrémenter ou de décrémenter plus rapidement la vitesse de la loco selon la valeur des 3 paliers. Si on ne veut pas utiliser de palier il suffit de mettre palier_1, palier_2 et palier_3 à 128.
Si l’on désire autoriser la fonction "POM" (programmation sur la voie principale), il faut dé-commenter la ligne qui définit "aff_pom". Cela rajoute une option sur la page de programmation des CVs.

Les #include :
#include <Arduino.h>
#include "U8glib.h"
#include <nRF24L01.h>
#include <RF24.h>
#include <Bounce2.h>
#include <EEPROM.h>
#include <Keypad.h> 

Si vous avez déjà travaillé avec un écran Oled, vous serez certainement surpris du choix de la librairie U8glib.h qui est une ancienne librairie qui n’est plus maintenue et dont l’auteur propose de se rabattre sur la nouvelle version qui est U8g2lib.h.
Ce choix s’est imposé à moi car, d’après mes essais, c’est la librairie qui consomme le moins de mémoire vive et qui permet malgré tout de beaux graphismes. Par contre cela impose une gymnastique un peu particulière pour l’affichage d’une page car à chaque ajout de texte il faut entièrement rafraîchir cette dernière. Une petite explication (en anglais) ici : https://github.com/olikraus/u8glib/...

Les constantes :
// menu
#define MENU_ITEMS 5
const char *menu_strings[MENU_ITEMS] = { "Liste par nom", "Liste par adresse", "Nouvelle Loco", "Gestion CVs", "Quitter" };

C’est le menu qui vous est proposé en appuyant la touche # .

Les canaux radio :

// il y a 10 canaux possibles
const uint64_t pipes[] = {0xB3B4B5B601LL, 0xB3B4B5B612LL, 0xB3B4B5B623LL, 0xB3B4B5B634LL, 0xB3B4B5B645LL, 0xB3B4B5B656LL, 0xB3B4B5B667LL, 0xB3B4B5B678LL, 0xB3B4B5B689LL, 0xB3B4B5B69ALL};

Pourquoi 10 canaux et non pas 6 comme l’exige le NRF24L01 ?
Au club la centrale est équipée d’une carte radio supplémentaire avec 4 canaux en plus ce qui permet d’utiliser 10 souris en même temps.
Le choix du canal se fait en fonction de la valeur num_souris lue dans l’EEPROM et modifiable par appui de la touche grise (mise en route DCC) au moment de l’allumage de la souris puis la saisie d’un chiffre de 0 à 9. Ce N° s’affiche pendant 2s à l’allumage ainsi que la version du programme.

Les touches alpha-numériques :

const char lettre[10][4] = {
  {'Z','Q','-','0'},
  {' ',':','+','1'},
  {'A','B','C','2'},
  {'D','E','F','3'},
  {'G','H','I','4'},
  {'J','K','L','5'},
  {'M','N','O','6'},
  {'P','R','S','7'},
  {'T','U','V','8'},
  {'W','X','Y','9'},
};

Ce tableau permet de modifier éventuellement les caractères utilisés pour les noms des locos ; chaque ligne correspond à son chiffre en fin de ligne.

Avant le setup se trouvent les différents sous-programmes d’affichage de pages et quelques fonctions utilisées plus loin dont la très courte routine d’interruption du codeur :

// routine interruption du codeur
void routineInterruption ()  {
  if (digitalRead(PinA)) {
    up = !digitalRead(PinB);
  }
  else {
    up = digitalRead(PinB);
  }
  mouvement = true;
}

ou la routine d’affichage de la page "jeu" assez complexe :

void draw_Jeux() {                     // affiche la page en position JEU
  u8g.setFont(u8g_font_6x13);
  u8g.setFontRefHeightText();
  u8g.setFontPosTop();
  // Afficher la ligne d'état
  sprintf(ligne1, " Ordre:%d     Adr:%d", ordre, adresse_loco);
  u8g.drawStr(0, 0, ligne1);
  // Afficher le nom de la loco ou Loco ? si choix loco en cours
  u8g.setFont(u8g_font_helvB12);       // hauteur 12 pixels GRAS 64%
  if (choix == 0) {
    for (i = 0; i < 16; i++) {
      ligne1[i] = nom_loco[i];         // initialiser avec la loco 0
    }
    d = ((128 - u8g.getStrWidth(ligne1)) / 2);
    u8g.drawStr(d, 28, ligne1);
  }
  else {
    u8g.drawStr(42, 28, "Loco ?");     // Afficher le nom de la loco ou Loco ? si choix loco en cours
  }
  // Afficher ampoule si fonction 0 activée
  if (fonction[ordre][0]) {
    u8g.drawCircle(centre_co, centre_li, 4);
    u8g.drawLine(15, centre_li - 8, 15, centre_li - 6);
    u8g.drawLine(15, centre_li + 6, 15, centre_li + 8);
    u8g.drawLine(centre_co - 8, centre_li, centre_co - 6, centre_li);
    u8g.drawLine(centre_co + 6, centre_li, centre_co + 8, centre_li);
    u8g.drawLine(centre_co - 7, centre_li - 6, centre_co - 5, centre_li - 4); // "\"
    u8g.drawLine(centre_co + 5, centre_li - 4, centre_co + 7, centre_li - 6); // "/"
    u8g.drawLine(centre_co - 7, centre_li + 6, centre_co - 5, centre_li + 4); // "/"
    u8g.drawLine(centre_co + 5, centre_li + 4, centre_co + 7, centre_li + 6); // "\"
  }
  // Afficher la vitesse
  if (tension == 1) {
    cpt = crans[ordre];                 // crans[] = crans réels envoyés, cpt = crans affichés à l'écran
    sens[ordre] = 1;
    if (crans[ordre] > 0) {
      sprintf(buf, "%3d  >", cpt + 2);
      d = ((128 - u8g.getStrWidth(buf)) / 2) + 10;
    }
    else if (crans[ordre] == 0) {
      sprintf(buf, "%3d", 0);
      d = ((128 - u8g.getStrWidth(buf)) / 2);
    }
    else if (crans[ordre] < 0) {
      cpt = - crans[ordre];
      sprintf(buf, "<  %d", cpt + 2);
      sens[ordre] = 0;
      d = ((128 - u8g.getStrWidth(buf)) / 2) - 6;
      if (d < 0) {
        d = 0;
      }
    }
  }
  else if (tension == 0) {
    sprintf(buf, "%s", "STOP");
    d = 40;
  }
  else {
    sprintf(buf, "%s", "DISJONCTE");
    d = 9;
  }
  u8g.drawStr(d, 46, buf);
  // afficher niveau batterie
  u8g.setFont(u8g_font_6x13);
  u8g.setFontRefHeightText();
  u8g.setFontPosTop();
  u8g.drawFrame(106, 38, 22, 9);
  if (moyenne > 0) {
    byte volt = 0;            //(0 = 1 barre, 5 = 6 barres)
    if (moyenne > 850) {      // 6 barres
      volt = 5;
    }
    else if (moyenne > 829) { // 5 barres
      volt = 4;
    }
    else if (moyenne > 808) {    // 4 barres
      volt = 3;
    }
    else if (moyenne > 787) { // 3 barres
      volt = 2;
    }
    else if (moyenne > 766) { // 2 barres
      volt = 1;
    }
    else if (moyenne > 745) { // 1 barre
      volt = 0;
    }
    for (byte i = 0; i <= volt; i++) {
      u8g.drawLine((i * 3) + 109, 40, (i * 3) + 109, 44);
    }
  }
  // Afficher les fonctions
  d = 0;                      // au départ afficher à partir de gauche
  if (w > 128) {
    d = -(w - 128);           // si ligne trop longue, décaler à gauche
  }
  u8g.drawStr(d, 54, ligne2); // n'afficher que la fin de la ligne des fonctions
}
Le setup :
//Serial.begin (9600);               // Initialisation du port série

ordre = EEPROM.read(1020);           // lire la dernière loco utilisée en EEPROM
contraste = EEPROM.read(1021);       // récupérer dernier contraste en EEPROM
num_souris = EEPROM.read(1022);      // lire le numéro de souris en EEPROM
PTXpipe = pipes[num_souris];         // récupérer le canal correspondant
  
rech_loco_ordre(ordre);              // chercher les infos de la dernière loco utilisée  
  
attachInterrupt (0,routineInterruption,CHANGE);   

pinMode(btn_STOP,INPUT_PULLUP);   
pinMode(btn_START,INPUT_PULLUP);  
pinMode(PinA,INPUT_PULLUP);       
pinMode(PinB,INPUT_PULLUP);       
pinMode(btn_codeur,INPUT_PULLUP);

boun_btn_codeur.attach(btn_codeur);
boun_btn_codeur.interval(10);
  
radio.begin();
radio.setPALevel(RF24_PA_MAX);      //RF24_PA_MIN, RF24_PA_LOW, RF24_PA_HIGH and RF24_PA_MAX
radio.setChannel(108);              //canal 108 au dessus des canaux WiFi  (de 0 à 125)
radio.setDataRate( RF24_250KBPS ) ; // 250Kbps pour max distance
radio.openReadingPipe(0, PTXpipe);  // ouvrir le canal PTXpipe en réception
radio.openWritingPipe(PTXpipe);     // ouvrir un canal en émission avec même adresse
radio.stopListening();              // se mettre en émission
  
crans[ordre] = 0;
mouvement = false;    
// Lecture niveau batterie, RAZ du tableau d'échantillons
for (int i = 0; i < nEchantillons; i++) {
    echantillon[i] = 0;
}
u8g.begin();
u8g.setContrast(contraste);         // mettre le contraste qui a été lu en EEPROM   
dateDernierAffichage = - 52000;     // forcer affichage niveau batterie après 8s
  
// créer la liste des locos non triée
for (int i = 0; i < nb_max_enr; i++) {
  listeTriee[i] = i;
}
// Test si bouton gris appuyé au démarrage pour réglage numéro souris
if (analogRead(btn_START) < 20) { 
  type_affichage = 8;
  menu = 8;
}

Le Serial.begin est supprimé pour gagner de la place en mémoire flash et RAM.
Il peut être remis en service à des fins de débogage en supprimant momentanément la police "u8g_font_helvB12" en mettant toutes les lignes qui l’utilisent en commentaire ce qui libère pas mal de mémoire.
Le NRF24L01 est utilisé de façon un peu particulière en lui attribuant le même canal pour l’émission et la réception. C’est ce qui permet d’utiliser 6 souris (avec une centrale de base sans l’extension radio).
Le setup configure l’interruption pour le codeur, les bits E/S, le module radio NRF24L01 et récupère toutes les infos sauvegardées en EEPROM : Le N° de canal de la souris, la dernière loco utilisée et le dernier contraste affiché.

La loop :

Désolé pour les explications un peu (voire très) succinctes de la partie la plus conséquente du programme mais vue la longueur (encore le monde des objets qui manque) je ne me vois pas commenter ces 748 lignes de code, et n’en faire qu’une petite partie ne vous apporterait pas grand chose. Je suis à votre disposition pour répondre à toutes vos demandes à la fin de cet article ou via le Forum.
En voici en tous cas les blocs principaux :
Ligne 636 à 673 : Boucle d’affichage selon la valeur de la variable "type_affichage".
Ligne 676 à 699 : On teste si une trame radio est détectée, suivi de l’envoi d’une éventuelle réponse.
Ligne 701 à 836 : Test et actions en fonction de la rotation du codeur incrémental. Les commentaires vous orienteront si besoin.
Ligne 837 à 863 : On n’envoie les commandes DCC que toutes les 500 ms.
Ligne 869 à 929 : Test des boutons STOP (rouge).
Ligne 931 à 974 : Test des boutons START (gris).
Ligne 977 à 1200 : Très long test du bouton du codeur avec actions correspondantes selon la page dans laquelle on se trouve.
Ligne 1204 à 1343 : Test du clavier alphanumérique et actions selon page.
Ligne 1345 à 1355 : Test de la tempo pour la saisie des différents caractères des touches du clavier.
Ligne 1357 à 1360 : Test de la tempo pour les touches de fonction.
Ligne 1362 à 1370 : Test de la tempo pour les touches de choix de loco.
Ligne 1373 à 1384 : Lecture du niveau batterie avec moyenne glissante, sujet très intéressant à voir ici: : http://arduino.blaisepascal.fr/inde...

Les ligne 977 à 1343 représentent Le gros du programme avec une tripotée de "if" que j’ai essayé de remplacer par des "case" mais sans que cela ne fasse gagner de la mémoire, donc abandonné. C’est dans cette partie que tout se passe pour gérer l’appui des touches, afficher un "pseudo" curseur ( il n’existe pas de curseur dans la bibliothèque U8glib.h), limiter le nombre de caractères saisis, annuler ou valider une action, etc. L’orientation se fait principalement en fonction de la variable "menu" qui est définie par la validation d’une des lignes du menu.
La aussi les commentaires sont nombreux et peuvent éclairer votre route.

La fabrication de la souris

Voici les dimensions du boitier :

Dimensions boitier

Un des objectifs pour la fabrication de la souris était de mettre tous les composants sur un seul circuit imprimé avec le clavier, l’écran et le microcontrôleur sur picots enfichables pour en faciliter le remplacement en cas de panne. Cela impose 2 types de picots pour une question d’encombrement vertical et que vous retrouverez sur la liste des pièces.
La partie la plus difficile car la plus précise, sera la préparation du boitier avec toutes les découpes nécessaires pour accueillir cet unique circuit imprimé qui sera bloqué entre les 2 demis-coques du boitier sans aucune fixation. Il faudra suivre scrupuleusement le gabarit de découpe fourni.

Gabarit de découpe

Gabarit de perçage

Pour la fabrication proprement dite j’avais commencé par mettre toutes les photos et tous les commentaires directement dans cet article mais pour une plus grande lisibilité j’ai préféré tout intégrer dans le document PDF ci-dessous.
Attention ! Très important. Il faut impérativement souder une capa de 10µF/16V directement sur les pins du NRF24L01, si vous la mettez sous le PCB il n’y a aucune amélioration. Cette modification est également valable pour la centrale.

Montage mécanique souris

Le câblage du circuit imprimé

Il faut bien respecter les hauteurs des différents composants pour que le boitier se ferme sans forcer. Pour cela le microprocesseur PRO/MINI 3,3V ne peut pas être monté avec les picots mâles prévus pour le support bas profil.

Câblage PRO/MINI

Dans le document PDF ci-après vous trouverez l’explication pour son montage un peu particulier. Je voulais surtout éviter de le souder directement sur le CI pour faciliter son remplacement en cas de panne.

Mise en place des composants

Liste des pièces

J’ai mis la liste des pièces dans un fichier Excel qui facilite la récupération des liens correspondants aux pièces principales.

Le circuit imprimé est disponible dans ce zip au format Gerber et Excellon mais aussi au format Sprint-Layout6.0 pour lequel il existe un lecteur gratuit ici : https://www.electronic-software-sho...
Pour ma part je travaille depuis pas mal de temps avec https://jlcpcb.com/ pour la qualité des circuits et leur prix, sauf pour les frais de port où il ne faut surtout pas choisir DHL sinon gare aux frais de douane. J’ai toujours utilisé l’option ePacket avec 0€ de frais de douane.

Circuit imprimé souris

Je peux également vous faire parvenir par courriel les fichiers au format bmp.
Vous pouvez aussi obtenir le circuit imprimé en qualité professionnelle en me contactant en MP (messagerie privée). Si le montage vous semble hors de portée vous pouvez me contacter de la même manière.

La Centrale

Nous abordons ici la partie la plus facile et la plus libre de l’article.
Comme je vous l’ai déjà précisé plus haut, cette centrale tel qu’elle est conçue, ne fonctionne qu’en DCC avec le logiciel DCCpp de Thierry dont vous trouverez un article ici : https://www.locoduino.org/spip.php?.... Libre à vous de la modifier pour l’utiliser en analogique.
Vous trouverez une documentation complète de cette bibliothèque dans le dossier \libraries\DCCpp\extras\Doc\ et en cliquant sur index.html.

Voici son schéma :

Schéma de la centrale V1.20

Elle est basée sur un Arduino MÉGA car il est impossible d’utiliser le bus SPI d’un UNO puisque le bit 12 (Miso) est utilisé pour le DIR du booster de la voie principale, voir ici : http://www.locoduino.org/spip.php?a...
Ce MÉGA gère une carte NRF24L01 pour l’émission/réception radio, une carte CAN pour l’ouvrir à toutes sortes d’extensions, une carte Bluetooth et commande les 2 boosters DCC.

Pour la voie principale j’ai fait le choix d’un module BTS7960B qui est capable de supporter 43A mais que le programme va bien sûr limiter à une valeur bien plus réaliste et que vous pourrez adapter à votre réseau, ceci grâce à ses sorties R_IS et L_IS envoyées sur l’entrée analogique A0 et qui donne une valeur proportionnelle au courant consommé.
Cette entrée n’est plus géré par le logiciel DCCpp (qui est le cœur du programme) mais par une routine personnelle qui me permet de traiter les courts-circuits d’une façon un peu particulière. A la détection d’un court-circuit, la centrale va se rallumer automatiquement 3 fois de suite avec un intervalle de 200ms (ajustable avec une constante en début de programme) afin d’absorber les petits courts-circuits qui peuvent se produire au passage de certaines aiguilles selon les locos ou wagons. Cette spécificité a trouvé un grand succès auprès des membres du club qui étaient obligés de remettre en marche très souvent leur ancienne centrale du fait de l’usage de machines parfois assez exotiques.
Attention ! Il semblerait que la tension renvoyé pour la mesure de courant du BTS7960B soit très variable d’un module à un autre. Le mieux est de décommenter la ligne 642 pour afficher les valeurs réelles lues puis d’adapter la consigne "Courant_Max" en ligne 2 avec une marge confortable.

Pour la voie de programmation le booster est un produit beaucoup plus traditionnel puisqu’il s’agit du LMD18200 suivi d’un MAX471 pour la lecture du courant. Je précise, pour les débutants, que cette mesure de courant a énormément d’importance car c’est grâce à elle que la centrale peut lire les informations des CVs.
L’Arrêt d’urgence peut vous paraitre assez originale puisque je force le MÉGA à faire un reset. J’ai fait ce choix car il permet dans tous les cas, même après un "plantage" du programme de repartir avec les sorties DCC à 0.
Attention ! Très important. Il faut impérativement souder une capa de 10µF/16V directement sur les pins du NRF24L01, si vous la mettez sous le PCB il n’y a aucune amélioration.

Le programme

Sketch V1.20 de la centrale

Attention ! Le cœur du programme est DCCpp comme déjà annoncé, mais j’ai du faire une modification au niveau de la librairie que vous trouverez ici : https://github.com/Locoduino/DCCpp/...
Dans config.h ligne 23 : MAX_MAIN_REGISTERS passe de 12 à 24

En tête du programme vous trouverez quelques paramètres pour adapter le programme à votre usage.

// Version = 1.00;
#define Courant_Max 1000			// courant maximum avant disjonction 
#define Boucle_Lec_Courant 2		// boucle de lecture courant
#define tempo_CC 200			// nb de millisecondes avant rétablissement tension DCC
#define YESICAN 				// à commenter si pas de module CAN installé

Courant_Max comme son nom l’indique est la valeur maximum autorisée avant de couper le DCC.
Boucle_Lec_Courant est le nombre de boucle que l’on autorise avec une valeur de courant dépassée avant de couper le DCC.
tempo_CC est le temps avant rétablissement automatique de la tension DCC (3 fois)
Il faudra jongler avec ces 3 paramètres pour adapter la gestion des courts-circuits à votre réseau.

Les #include :
#include "DCCpp.h"

#ifndef USE_TEXTCOMMAND
#error To be able to compile this sample,the line #define USE_TEXTCOMMAND must be uncommented in DCCpp.h
#endif

#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>

#ifdef YESICAN
#include <mcp_can.h>
#endif

Où trouver les librairies :
Pour la carte CAN : https://github.com/Locoduino/CAN_BU...
Pour le NRF24L01 : https://github.com/nRF24/RF24

Les constantes :
const unsigned int ad_reception_carte1= 0x100;		
const unsigned int ad_reception_carte2= 0x101;		
const unsigned int ad_emission_carte1	= 0x200;		
const unsigned int ad_emission_carte2	= 0x300;
const unsigned int nb_char_recu		= 1;	// nombre de caractères à reçevoir
const unsigned int nb_char_emis		= 1;	// nombre de caractères à emettre
boolean _dumpCan = true;								// mode débogage

Ce sont les constantes utilisées pour les échanges avec le bus CAN. Il faudra bien sûr les adapter à votre "philosophie" personnelle.
const uint64_t pipes[] = {0xB3B4B5B601LL, 0xB3B4B5B612LL, 0xB3B4B5B623LL, 0xB3B4B5B634LL, 0xB3B4B5B645LL, 0xB3B4B5B656LL};
Contrairement à la souris, la centrale n’autorise que 6 canaux radio différents. Si l’on désire augmenter le nombre de canaux il faudra ajouter une carte optionnelle qui va gérer les 4 canaux supplémentaires. Description et CI sur simple demande.

// locos
#define nb_max_locos 12		// Nb de locos max mises en mémoire

Cette valeur ne doit pas être modifiée car elle est liée à la valeur MAX_MAIN_REGISTERS de DCCpp. Nous utilisons 12 registres pour la mise en mémoire des vitesses de 12 locos et 12 registres pour la mise en mémoire des fonctions de ces 12 locos. Alors pourquoi MAX_MAIN_REGISTERS = 24 ce qui ferait 25 registres ? Tout simplement parce que le registre 0 ne peut pas être utilisé de la même façon. Je vous reparlerai de ce sujet (sensible) plus loin.

Le setup :
void setup()
{
  Serial.begin(9600);
  Serial1.begin(9600);
  Serial.println("OK pour connecter CAN!");

START_INIT:
  if (CAN_OK == CAN.begin(CAN_250KBPS)) {       // init can bus : baudrate = 250k
    if (_dumpCan) {
      Serial.println("CAN BUS Shield init ok!");
    }
  }
  else  {
    if (_dumpCan) {
      Serial.println("CAN BUS Shield init fail");
    }
    delay(100);
    goto START_INIT;
  }

  /*
    configurer mask & filter du bus CAN
  */
  CAN.init_Mask(0, 0, 0b11111111111);         // Il y a 2 masques à initialiser dans le mcp2111
  CAN.init_Mask(1, 0, 0b11111111111);         // on teste tous les bits

  CAN.init_Filt(0, 0, ad_reception_carte1);   // Réception possible : que 2 adresses
  CAN.init_Filt(1, 0, ad_reception_carte1);   // idem
  CAN.init_Filt(2, 0, ad_reception_carte1);   // idem
  CAN.init_Filt(3, 0, ad_reception_carte2);   // idem
  CAN.init_Filt(4, 0, ad_reception_carte2);   // idem
  CAN.init_Filt(5, 0, ad_reception_carte2);   // Idem

  // configurer l'interruption
  attachInterrupt(2, MCP2515_ISR, FALLING);   // interrupt 2 (pin 21)

Si vous n’installez pas la carte CAN, il faudra commenter la ligne (5 dans le programme) pour éviter de boucler sur la configuration du module CAN.

// lancement radio
radio.begin();
radio.setPALevel(RF24_PA_MAX);    //RF24_PA_MIN, RF24_PA_LOW, RF24_PA_HIGH and RF24_PA_MAX
radio.setChannel(108);            //canal 108 au dessus des canaux WiFi  (de 0 à 125)
radio.setDataRate( RF24_250KBPS ) ;

// ouvrir 6 canaux de réception radio
radio.openReadingPipe(0, pipes[0]);
radio.openReadingPipe(1, pipes[1]);
radio.openReadingPipe(2, pipes[2]);
radio.openReadingPipe(3, pipes[3]);
radio.openReadingPipe(4, pipes[4]);
radio.openReadingPipe(5, pipes[5]);

radio.startListening();          // passer en réception

On configure les 6 canaux radio

  // Lancement DCC
  DCCpp::begin();
  DCCpp::beginMain(UNDEFINED_PIN, DCC_SIGNAL_PIN_MAIN, 3, UNDEFINED_PIN );	//en mettant UNDEFINED_PIN  à la place de A0,plus de test de courant par DCC, il y a une routine spécifique dans le loop
  DCCpp::beginProg(UNDEFINED_PIN, DCC_SIGNAL_PIN_PROG, 11, A1);
  Serial.println("SerialDcc OK");

On lance DCCpp en supprimant le test de courant par A0 avec le UNDEFINED_PIN puisque nous voulons utiliser notre propre routine de lecture de courant.

  // mise à 0 des tableaux de fonctions et adresses locos
  for (int nb_loc = 0; nb_loc < nb_max_locos; nb_loc++) {	
	adr_loc[nb_loc] = 0;
	tab_fonc_B0_4[nb_loc] = 128;
	tab_fonc_B5_8[nb_loc] = 176;
	tab_fonc_B9_12[nb_loc] = 160;
	tab_fonc_B0_43_2[nb_loc] = 0;
	tab_fonc_B0_28_2[nb_loc] = 0;
  }

Nous initialisons 1 tableau pour les adresses des 12 locos et 5 tableaux pour la sauvegarde des fonctions de ces 12 machines.
Peut-être l’avez-vous remarqué si vous utilisez déjà DCCpp, mais en cas de mauvais contacts sur les rails ou de coupure de la tension DCC, les fonctions de toutes les locos sont remises à 0 ce qui est très gênant surtout si l’on fait circuler plusieurs convois en même temps.

  // envoyer l'info tension coupée sur les souris
  //start_DCC();	// si demande de tension DCC à l'allumage, dé-commenter cette ligne
  envoi_etat();
}

Dans l’état actuel du programme, à l’allumage de la centrale, la tension DCC est à 0. Si vous préférez avoir du DCC à l’allumage il suffit de dé-commenter la ligne "start_DCC() ;".

La loop :

La ligne 283 lance le rafraîchissement des routines DCCpp
De 288 à 389 on teste la réception radio qui appelle quelques commentaires.

if (radio.available(&pipeNum))  {                       // tester si une trame est arrivée, 
char c_cv[2];
  radio.read(&dcc_receive, sizeof(dcc_receive));        // sizeof(dcc_receive) est toujours à 8
if(dcc_receive[0] == '<') {                             // ne traiter que si trame commence par <
 if(dcc_receive[1] == 1) {                              // "1" = trame allumer DCC
  start_DCC();                                          // utiliser de ce programme pour allumer DCC
  cpt_cc = 0;                                           // permettre à nouveau 3 CC de 0,5s
  old_etat = 10;                                        // pour forcer l'envoi de l'état
  delay(100);                                           // laisser le temps à la souris de passer en réception
 }
 else if(dcc_receive[1] == 0) {                         // "0" = trame couper DCC
  stop_DCC();                                           // utiliser de ce programme pour couper DCC
  old_etat = 10;                                        // pour forcer l'envoi de l'état
  delay(100);                                           // laisser le temps à la souris de passer en réception
 }
 else {
  if(dcc_receive[1] == 't') {                           // 't' = trame vitesse
   adr_loco_H = dcc_receive[2];
   adr_loco_L = dcc_receive[3];
   adresse_loco = restaure_adr();                       // restaure l'adresse complète à partir de HI et LOW
   crans = dcc_receive[4];                              // non utilisé pour l'instant
   sens = bitRead(dcc_receive[5], 7);                   // le sens est déterminé par le bit 7 de vitesse
   vitesse = dcc_receive[5] & 0b01111111 ;              // mettre bit 7 à 0
   enregistre_loc();                                    // enregistre la nouvelle loco ou récupère son ID si existe
   // ici on change de registre de vitesse selon loco, ne pas utiliser registre 0 donc +1
   DCCpp::setSpeedMain(ordre_loc + 1, adresse_loco, 128, vitesse, sens);  
  }
  else if(dcc_receive[1] == 'W') {                      // "W" = trame écriture CV sur voie de programmation
   envoi[pipeNum] = true;                               // pour forcer un retour du résultat
   adr_cv[pipeNum] = dcc_receive[2];
   // assembler les 2 bytes pour refaire valeur INT
   adr_loco_H = dcc_receive[3];
   adr_loco_L = dcc_receive[4];
   don_cv = restaure_adr();                             // restaure valeur complète à partir de HI et LOW
   if(adr_cv[pipeNum] == 1 and don_cv < 128) { 
    // c'est une adresse courte pour cv1
    DCCpp::writeCvProg(adr_cv[pipeNum], don_cv, 100, 200); // écriture de CV1
    // mettre bit 5 de CV29 à 0 pour adresse courte
    byte cv_29 = DCCpp::readCvProg(29, 100, 200);
    bitWrite(cv_29, 5, 0);
    DCCpp::writeCvProg(29, cv_29, 100, 200); 
   }
   else if(adr_cv[pipeNum] == 1) { 
    // c'est une adresse longue pour cv1, donc on calcul les valeurs de CV17 et CV18
    int deb=0;
    int fin=0;
    int res=0;
    int cv17=192;                                       // laisser cv17 à 192 pour calcul
    int cvres=0;
    for (int i = 0;i <= 9984;i = i + 256) {
     deb = i;
     fin= i + 255;
     if ((don_cv >= deb)&&(don_cv <= fin)){
     res = don_cv - i;
     cvres = cv17;
     }
     cv17 = cv17 + 1;
    }
    // mettre bit 5 de CV29 à 1 pour adresse longue
    byte cv_29 = DCCpp::readCvProg(29, 100, 200);
    bitWrite(cv_29, 5, 1);
    DCCpp::writeCvProg(29, cv_29, 100, 200); 
    DCCpp::writeCvProg(17, cvres, 100, 200);            // écriture de CV17
    DCCpp::writeCvProg(18, res, 100, 200);              // écriture de CV18
   }
   else { // pour tous les autres cv, envoyer trame
    DCCpp::writeCvProg(adr_cv[pipeNum], don_cv, 100, 200);
   }
  }
  else if(dcc_receive[1] == 'R') {                     // "R" = trame lecture CV
   adr_cv[pipeNum] = dcc_receive[2];
   envoi[pipeNum] = true;                              // la lecture se fait dans test envoi[pipeNum]
  }
  else if(dcc_receive[1] == 'f') {                     // "f" = trame fonctions locos
   adr_loco_H = dcc_receive[2];
   adr_loco_L = dcc_receive[3];
   adresse_loco = restaure_adr();                      // restaure l'adresse complète à partir de HI et LOW
   etat_fct = bitRead(dcc_receive[4], 7);              // le sens est déterminé par le bit 7 de vitesse
   num_fct = dcc_receive[4] & 0b01111111 ;             // mettre bit 7 à 0
   if(num_fct < 29) {                                  // n'envoyer sur loco que les 28 premières fonctions
    enregistre_loc();                                  // enregistre la nouvelle loco ou récupère son ID si existe
    // envoyer la fonction
    if (etat_fct) {
     gLocoFunctions[ordre_loc].activate(num_fct);
    } else {
     gLocoFunctions[ordre_loc].inactivate(num_fct);
    }
    // MAJ registre loco
    DCCpp::setFunctionsMain(ordre_loc + 13, adresse_loco, gLocoFunctions[ordre_loc]);
   }
   else { // les fonctions > 28 sont envoyées sur bus CAN avec bit 7 en l'état
    ordres_carte1 = dcc_receive[4];
   }
  }
  else if(dcc_receive[1] == 119) {                    // "w" = trame programmation POM
   adr_cv[pipeNum] = dcc_receive[2];
   don_cv = dcc_receive[3];
   DCCpp::writeCvMain(adr_cv[pipeNum], don_cv, 100, 200);
  }
 }
}
}

Tout d’abord on n’accepte que des trames délimitées par < et >

Les trames <0> et <1> coupent ou mettent en route la tension DCC.

Les trames commençants par "t" sont les trames de réglage de vitesse que l’on va sauvegarder dans les registres 1 à 12. Pour ce faire on va enregistrer l’adresse de chaque nouvelle loco mise sur les rails (dont on reçoit une commande) dans un tableau dont le pointeur va s’incrémenter de 0 à 11, c’est le tableau adr_loc[]. Si on dépasse les 12 locos sans avoir initialisé la centrale les 6 tableaux (adresses et fonctions) sont remis à 0.
Puis on utilise ce pointeur (ordre_loc) pour enregistrer l’adresse, les crans, la vitesse et le sens dans les registres 1 à 12 :
DCCpp::setSpeedMain(ordre_loc + 1, adresse_loco, 128, vitesse, sens);

Les trames commençants par "W" sont les trames d’écriture de CV sur la voie de programmation. On oriente le programme différemment s’il s’agit d’une adresse longue (supérieure à 127) ou courte. Pour le CV1 on réécrit le CV29 puis les CV17 et 18 selon les normes NMRA et on positionne un drapeau (envoi[]) pour signaler qu’un retour d’information est à envoyer à la souris :

envoi[pipeNum] = true;	// pour forcer un retour du résultat
DCCpp::writeCvProg(29, cv_29, 100, 200); 
DCCpp::writeCvProg(17, cvres, 100, 200);	// écriture de CV17
DCCpp::writeCvProg(18, res, 100, 200);		// écriture de CV18

Pour les autres CVs on positionne le même drapeau et on écrit la valeur demandée :

envoi[pipeNum] = true;	// pour forcer un retour du résultat
DCCpp::writeCvProg(adr_cv[pipeNum], don_cv, 100, 200);

Les trames commençants par "w" sont les trames d’écriture de CV sur la voie principale en mode "POM". Cette programmation ne permet pas de retour d’information :
DCCpp::writeCvMain(adr_cv[pipeNum], don_cv, 100, 200);

Les trames commençants par "R" sont les trames de lecture de CV sur la voie de programmation. Cela se fait ici en positionnant aussi le drapeau de retour :

adr_cv[pipeNum] = dcc_receive[2];
envoi[pipeNum] = true;			// la lecture se fait dans test envoi[pipeNum]

Les trames commençants par "f" sont les trames de fonctions que l’on va sauvegarder dans l’un des 5 registres.
Si ce sont des fonctions standards de 0 à 28, on les envoie à DCCpp par :
DCCpp::setFunctionsMain(ordre_loc + 13, adresse_loco, gLocoFunctions[ordre_loc]);

Si la fonction est supérieure à 28 on va l’envoyer sur le bus CAN, information qui sera récupérée par l’une des cartes connectée sur le bus et qui fera l’action que vous aurez programmé ; cela peut être une animation quelconque, un son (parmi plusieurs) fourni par une carte que je suis entrain de développer, une commande d’aiguille etc...

Retour d’information

if (envoi[pipeNum] and etat) { // récupérer la donnée à lire à l'adresse adr_cv et l'envoyer par radio mais que si loco sous tension (etat = true)
  char text[7];
  if (adr_cv[pipeNum] == 1) {                                    // si on lit le CV1, tester si adresse longue ou non
    int CV_29 = DCCpp::readCvProg(29, 101, 101); 
    int CV_17 = DCCpp::readCvProg(17, 101, 101); 
    int CV_18 = DCCpp::readCvProg(18, 101, 101); 
    if (bitRead(CV_29, 5) == 1) {                                // on est en adresse longue
      sav_don_cv[pipeNum] = ((CV_17 - 192) * 256 ) + CV_18;      // reconstruire adresse longue
      sprintf(text, "R%d", sav_don_cv[pipeNum]);                 // on renvoi l'adresse longue lue
    }
    else {
      int monINT = DCCpp::readCvProg(adr_cv[pipeNum], 101, 101); // lecture CV1 (adr courte)
      sprintf(text, "R%d", monINT);                              // réponse bonne ou fausse
    }
  }
  else { // si on lit les autres CV, retourner simplement sa lecture
    int monINT = DCCpp::readCvProg(adr_cv[pipeNum], 101, 101);   // lecture du CV demandé
    sprintf(text, "R%d", monINT);                                // réponse bonne ou fausse
  }
  radio.stopListening();                                         // passer en émission
  radio.openWritingPipe(pipes[pipeNum]);                         // utiliser le canal de celui qui a envoyé la demande
  radio.write(&text, sizeof(text));
  delay(5);
  radio.startListening();                                        // passer en réception
  envoi[pipeNum] = false;
}

Si le drapeau relatif à la loco N° [pipeNum] est à true on renvoie la lecture du ou des CVs vers la souris.

Gestion du Bluetooth
while (Serial1.available()>0) {                   // Tant que données série arrivent
char c = Serial1.read();
if (c == '<') {                                   // début de la trame
 cmdBlue[0] = 0;
}
else if (c == '>') {                              // fin de la trame
 TextCommand::parse(cmdBlue);                     // on envoie traiter la trame
}
else if (strlen(cmdBlue) < MAX_COMMAND_LENGTH) {  // s'il reste de la place dans le tableau
 sprintf(cmdBlue, "%s%c", cmdBlue, c);            // continuer jusqu'au >
}

Le module Bluetooth est relié sur l’entrée Serial1 du MÉGA. Si une information arrive par ce biais on l’envoie directement vers DCCpp.
Pour l’instant je ne traite pas la mise en mémoire des informations venues par le Bluetooth mais il est tout à fait possible de les gérer de la même manière que les informations venants de la souris. Avis aux amateurs.

Le reste du programme me semble suffisamment commenté pour ne pas rallonger inutilement cet article. N’hésitez pas à me contacter à la fin de cet article ou sur le forum si vous avez des questions.

La fabrication de la centrale

Pour la fabrication de la centrale toutes les possibilités existent selon le boitier, le budget, la présentation que vous souhaitez etc...

Pour câbler les différents composants vous pouvez utiliser un Protoshield MÉGA sur lequel vous implantez le module NRF24L01, le module Bluetooth, le transistor inverseur, éventuellement la carte CAN ainsi que tous les connecteurs nécessaires pour relier le 0 et 5V, les boosters, les entrées lecture courant, le bouton STOP et la sortie CAN.
Les boosters et le MAX471 sont montés séparément.

Pour ma part j’ai fait faire un circuit imprimé qui regroupe tous ces éléments ce qui facilite énormément le montage. Ce circuit est disponible de la même manière que celui de la souris.

JPEG - 256.2 kio

J’ai facilement intégré tous les éléments dans ce boitier dont le lien est dans la liste des pièces et qui fait 180x135x55mm :

Boitier centrale

Option
Comme je vous l’ai signalé plus haut, il est possible d’étendre le nombre de souris avec un module additionnel.
Ce module comporte un Arduino NANO, une carte NRF24L01 et un module CAN et voici son circuit imprimé (également disponible en fichiers ou physiquement) :

Module 4 canaux radio

Je vous laisse approfondir cet article et me poser toutes les questions que vous jugerez utiles ; je suis également ouvert à toutes les améliorations ou suggestions qui feraient évoluer ce produit.

Plusieurs personnes ayant eu des soucis avec la librairie de l’afficheur U8glib, voici celle que j’utilise te qui est fonctionnelle.

Bibliothèque u8g