Reconnaissance de caractères en BASIC

Sur une machine avec un « vrai » mode texte, lorsqu’on affiche du texte, le micro-processeur se contente de copier les codes ASCII de chaque caractère dans zone définie de la mémoire vidéo, et le processeur graphique se charge de convertir ces codes ASCII en caractères visibles sur l’écran.

Le ZX-Spectrum, comme plusieurs micros de la même époque, ne dispose pas d’un mode d’affichage par caractères « matériel ». Cela veut dire que lorsqu’on affiche du texte, celui-ci est affiché en mode graphique par le micro-processeur.
C’est légèrement plus lent car il faut au minimum modifier 8 octets dans la mémoire vidéo au lieu d’un seul(*), mais en général ça ne se remarque pas trop.

Un avantage est qu’on peut sans problème mélanger texte et graphiques à l’écran, mais il peut être compliqué pour un programme de retrouver quel caractère se trouve à une position donnée de l’écran.

Certains micros, comme le TRS-80 Model 100(**) conservent une « fausse » ram vidéo qui contient le code ASCII de chaque caractère affiché. Le micro-processeur doit toujours « dessiner » chaque caractère en entier en mode graphique, mais si on veut savoir quel caractère se trouve à une position donnée il suffit de lire cette mémoire vidéo pour trouver son code ASCII(***).

Sur le ZX-Spectrum ce n’est pas le cas.
Il y a pourtant une fonction BASIC appelée SCREEN$ qui renvoie le caractère affiché à une position donnée de l’écran.
Comment cette fonction procède-t-elle pour retourner cette information ?

Je soupçonnais la présence d’une zone mémoire conservant les caractères affichés comme pour le Model 100, mais cela ne fonctionnait pas avec les caractères redéfinissables.

En effet le ZX-Spectrum dispose de 21 caractères redéfinissables (codes ASCII entre 144 et 164). Après un reset, ces caractères contiennent les graphiques des caractères « A » à « U ».
Si on entre les deux lignes suivantes :

PRINT CHR$ 144
PRINT ASC SCREEN$(0,0)

nous récupérons un « A » (le graphique par défaut du caractère 144) suivi de la valeur… 65 ! (le code ASCII du « vrai » A).

Que ce passe-t-il donc ?
Pour vérifier j’ai écrit le petit programme ci-dessous :

Ce programme affiche un « F » dans la première case de l’écran (à la position 0,0), lit le code ASCII correspondant avec SCREEN$(0,0) (70, valeur ASCII de F) puis traces 1 pixel dans cette même case avant de relire son code ASCII (maintenant 0 puisque ce caractère n’existe pas) avant de tracer une droite pour transformer le caractère « F » en « E », puis de lire une dernière fois la valeur de SCREEN$(0,0), qui est maintenant 69 (c’est à dire le code ASCII du « E »).

Est-ce que cette fonction reconnaît vraiment dynamiquement tous les symboles du jeu de caractères du ZX-Spectrum ?

Je pensais qu’une simple implémentation serait d’utiliser une fonction de hashage sur les 8 octets qui composent l’image du caractère, puis de regarder dans une table de lookup pour trouver le code ASCII correspondant à l’image, mais après avoir regardé le code source de cette fonction, il semble qu’elle compare effectivement les 8 octets de l’image avec les images du jeu de caractère !

Cette reconnaissance fonctionne pour toutes les couleurs, ce qui peut poser quelques problèmes. En effet, à cause de cette fonctionnalité, SCREEN$ retourne la même valeur pour un espace et pour un bloc graphique plein, mais heureusement il y a des solutions pour contourner ce problème.

Décidément, ce bon vieux ZX-Spectrum est plein de surprises dès qu’on le regarde d’un peu plus près.

(*) Plus si l’affichage est en couleurs
(**) Sur lequel je suis en train de taper cet article :o)
(***) Ca lui sert aussi à redessiner l’écran après un scrolling vertical

Interfacer un clavier de Wyse WY-30, part VI

Maintenant que j’arrive à lire l’état du clavier sans problème, il reste à faire reconnaître l’Arduino comme un clavier, afin de pouvoir l’utiliser comme un clavier USB normal.

Spoiler Alert: J’utilise le clavier WY-30 pour taper ce billet donc il y a de grandes chances que j’ai réussi ;o)

En fait, cette partie est plutôt simple : Il suffit d’utiliser la librairie standard Keyboard, puis après avoir initialisé le clavier avec Keyboard.begin(), il n’y a qu’à appeler Keyboard.press() ou Keyboard.release() chaque fois qu’une touche est pressée ou relachée.

Il y a cependant deux petits problèmes à prendre en compte :

  • Le clavier de la WY-30 n’a pas toutes les touches d’un clavier de PC moderne, et dispose de quelques touches qui n’ont pas d’équivalent. Il a donc fallu réassigner certaines touches (par exemple, « Line Feed » sur le clavier est devenu « Num Lock », ou « Funct » est devenu « Alt »).
  • Une fois que l’Arduino appelle Keyboard.begin(), elle est reconnue par le PC comme un clavier USB. Cela a pour inconvénient de ne plus permettre de la reprogrammer (à moins d’utiliser les grands moyens). Pour éviter ce problème, j’ai ajouté un bout de code au début de la fonction setup() qui va faire une boucle tant que la GPIO 10 est reliée à GND. Pour re-programmer l’Arduino. il suffit donc de relier cette pin à GND avant de la brancher au PC (la led RX clignote lorsque la carte est dans ce mode).

Comme ce programme ne fait plus du tout la même chose que celui des billets précédents, j’en ai crée un nouveau (oui, il y a beaucoup de code en commun avec l’ancien programme :o) ):

// Scan du clavier de la WY-30
// Lit l'état de chaque touche et envoie les changements d'état au PC

#include <Keyboard.h>

// Correspondance des touches du clavier avec les touches du PC
// Les touches suivantes n'ont pas d'équivalent direct donc j'ai improvisé
//   Keypad ,   => KEY_KP_PLUS
//   Funct      => KEY_LEFT_ALT
//   Ctrl       => KEY_LEFT_CTRL
//   Line Feed  => KEY_NUM_LOCK
//   Both Shift => KEY_LEFT_SHIFT
//   Setup      => KEY_MENU
uint8_t  keysCode[] = {
  KEY_KP_1, KEY_KP_MINUS, KEY_F1, KEY_CAPS_LOCK, 'd', 'p', KEY_LEFT_ALT, KEY_ESC,
  KEY_KP_2, KEY_KP_4, KEY_F2, ' ', KEY_RETURN, '[', 'c', '1',
  KEY_KP_3, KEY_KP_5, KEY_F3, '\\', 's', ']', 'z', '2',
  KEY_MENU, KEY_BACKSPACE, '=', '-', '0', '`', '9', '\'',
  KEY_HOME, KEY_UP_ARROW, '/', '.', KEY_LEFT_CTRL, ',', 0, '3',
  'o', 'i', 'u', 't', 'y', 0, 'x', KEY_NUM_LOCK,
  ';', 'l', 'a', 'j', 0, KEY_TAB, KEY_LEFT_SHIFT, '4',
  KEY_KP_ENTER, KEY_KP_DOT, KEY_KP_0, 0, 'f', 'q', 'v', '5',
  KEY_KP_9, KEY_KP_PLUS, 0, KEY_LEFT_ARROW, 'h', 'w', 'n', '6',
  KEY_KP_8, 0, KEY_F4, KEY_DOWN_ARROW, 'g', 'e', 'b', '7',
  KEY_DELETE, KEY_KP_6, KEY_KP_7, KEY_RIGHT_ARROW, 'k', 'r', 'm', '8' };

// GPIO utilisées
#define RXLED 17  // Led RX (peut varier selon votre carte Arduino)
#define CLK 21    // Ligne /CLK du connecteur clavier
#define DATA 20   // Ligne DATA du connecteur clavier

#define delay1us __asm__("nop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\n")
#define delay10us delay1us;delay1us;delay1us;delay1us;delay1us;delay1us;delay1us;delay1us;delay1us;delay1us

uint8_t frame[11]; // frame courante
uint8_t pframe[11];// frame précédente

// Initialisation
void setup() {
  // Securité : stop l'exécution si GPIO 10 est mise à GND
  pinMode(RXLED, OUTPUT);
  pinMode(10, INPUT_PULLUP);
  while(digitalRead(10)==LOW) {
    digitalWrite(RXLED, HIGH);
    delay(100);
    digitalWrite(RXLED, LOW);
    delay(100);
    digitalWrite(RXLED, HIGH);
    delay(100);
    digitalWrite(RXLED, LOW);
    delay(300);
  }

  Keyboard.begin();
  pinMode(CLK, OUTPUT);
  pinMode(DATA, INPUT_PULLUP); // Pour éviter des lectures fantômes quand le clavier est débranché
  digitalWrite(CLK, HIGH);
  memset(pframe, 0xff, sizeof(pframe)); // Initialisation de la frame précédente pour la première itération
}

// Boucle principale
void loop() {
  byte f=0;
  memset(frame, 0, sizeof(frame));
  cli();
  PINF = 0x10; // Début de la première impulsion (io 21 = PORT F, bit 4)
  delay10us; delay10us; delay10us; delay10us; // Pause de 40us
  PINF = 0x10;
  frame[0] = (PINF&0x20)>>5; // Enregistre la première touche

  for(byte k=1;k<88;++k) {
    if((k&0x7)==0) ++f;
    PINF = 0x10;
    delay1us; // Pause de 1us;
    PINF = 0x10;
    frame[f]<<=1;
    frame[f]|=(PINF&0x20)>>5;
  }
  sei();

  // Envoie les codes des touches modifiées au PC
  uint8_t k=0; // Touche à tester
  for(f=0;f<11;++f) {
    if(frame[f]!=pframe[f]) {
      uint8_t cf = frame[f];
      uint8_t mb = pframe[f]^frame[f];
      for(uint8_t m=0x80;m;m>>=1) {
        if(mb&m) { // Ce bit a changé et...
          if(cf&m) { // ... la touche a été relachée
            Keyboard.release(keysCode[k]);
          }
          else { // ... la touche a été pressée
            Keyboard.press(keysCode[k]);
          }
        }
        ++k;
      }
      pframe[f] = cf;
    }
    else {
      k += 8;
    }
  }

  delay(20);  // Pause de 20ms (pour environ 50 scans par seconde)
}

Quelques remarques sur ce programme :

  • L’assignation de certaines touches n’est pas optimal ; il faudra sans doute que je fasse quelques modifications
  • Si vous utilisez un clavier autre que QWERTY, il faudra que vous l’indiquiez dans le programme lors de l’appel de Keyboard.begin(). Par exemple pour un clavier AZERTY ça donne Keyboard.begin(KeyboardLayout_fr_FR).
  • Ce programme passe 98.5% de son temps à attendre (j’ai mesuré). Une meilleure implémentation serait d’utiliser des interruptions. Ca permettrait de faire autre chose pendant que les fonctions du clavier s’exécutent en arrière plan, ou de mettre l’Arduino en veille entre chaque interruption (ce qui permettrait de réduire la consommation, petit détail utile si votre PC est sur batteries).
Version finale (ou pas ?)

Ce billet est le dernier de cette série pour l’instant, mais il est bien probable que je le revisite dans quelques temps car à terme je compte utiliser ce clavier dans un projet à base de Raspberry Pi Pico. J’en profiterais sans doute également pour ajouter quelques easter eggs car ce serait dommage de ne pas en avoir ;o)

Je pensais peut-être faire une PCB dédiée mais quand on voit le résultat final ce n’est pas forcément nécessaire.

Interfacer un clavier de Wyse WY-30, part V

Une fois ma carte Arduino Pro Micro remise en état, il est maintenant temps de finaliser le programme.

Pour l’instant, le programme de la 4ème partie n’affiche qu’un groupe de 22 chiffres hexadécimaux, ce qui permet de montrer que tout fonctionne mais n’est pas très utile. Dans cette 5ème partie, nous allons identifier les touches par leur nom, et surtout détecter quelles sont celles qui viennent d’être pressées ou relâchées.

J’aurais pu utiliser l’oscilloscope pour déterminer à quelle position dans le train d’impulsions se trouve chaque touches, mais j’ai utilisé une table trouvée sur un forum après avoir corrigé une petite erreur (la seconde touche « Right » est en fait la lettre « R ») :

Table des touches du clavier de la WY-30, courtesy of https://geekhack.org/index.php?topic=51079.msg1157910#msg1157910

J’ai stocké ces informations dans un tableau de chaînes de caractères keysLabel, puis comme le but est de détecter les changements de touche entre chaque scan du clavier j’utilise maintenant deux variables frame et pframe pour pouvoir les comparer (frame contient le dernier scan du clavier, tandis que pframe contient le scan précédent).

Notez que pframe est initialisée avec des 0xff (tous les bits à 1) pour éviter que lors de la première itération toutes les touches ne soient listées comme relâchées.

De même, la GPIO DATA correspondant à l’entrée venant du clavier est maintenant initialisée en INPUT_PULLUP pour éviter que certaines touches n’apparaissent pressées lorsque le clavier est débranché.

J’ai également augmenté le délais de remise à zéro de la ligne CLOCK à 40 microsecondes car 30 ne suffisaient pas (ça fonctionnait dans la version précédente, probablement parce qu’une interruption se produisait durant ce délais, mais maintenant que le programme désactive les interruptions il fallait un minimum de 36 microsecondes ; j’ai mis 40 pour avoir une petite marge de sécurité).

La pause entre deux scans du clavier est maintenant de 20 ms, ce qui devrait donner environ 50 scans par seconde.

Voici le code de cette version :

// Scan du clavier de la WY-30
// Lit l'état de chaque touche et affiche les changements d'état

// Nom des touches
char* keysLabel[] = {
  "NP1", "NP-", "F1", "CapsLock", "D", "P", "Funct", "ESC",
  "NP2", "NP4", "F2", "Space", "Return", "[", "C", "1",
  "NP3", "NP5", "F3", "\\", "S", "]", "Z", "2",
  "Setup", "BackSpace", "=", "-", "0", "`", "9", "'",
  "Home", "Up", "/", ".", "Ctrl", ",", "N/A", "3",
  "O", "I", "U", "T", "Y", "N/A", "X", "Line Feed",
  ";", "L", "A", "J", "N/A", "Tab", "Shift", "4",
  "NP Enter", "NP.", "NP0", "N/A", "F", "Q", "V", "5",
  "NP9", "NP,", "N/A", "Left", "H", "W", "N", "6",
  "NP8", "N/A", "F4", "Down", "G", "E", "B", "7",
  "Del", "NP6", "NP7", "Right", "K", "R", "M", "8" };

// GPIO utilisées
#define CLK 21
#define DATA 20

#define delay1us __asm__("nop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\n")

uint8_t frame[11]; // frame courante
uint8_t pframe[11];// frame précédente

// Initialisation
void setup() {
  pinMode(CLK, OUTPUT);
  pinMode(DATA, INPUT_PULLUP); // Pour éviter des lectures fantômes quand le clavier est débranché
  digitalWrite(CLK, HIGH);
  Serial.begin(115200);
  memset(pframe, 0xff, sizeof(pframe)); // Initialisation de la frame précédente pour la première itération
}

// Boucle principale
void loop() {
  byte f=0;
  memset(frame, 0, sizeof(frame)); // Re-initialise l'état des touches (probablement inutile :o) )
  cli(); // Interdit les interruptions
  PINF = 0x10; // Début de la première impulsion (io 21 = PORT F, bit 4)
  // Pause de 40us
  delay1us; delay1us; delay1us; delay1us; delay1us; delay1us; delay1us; delay1us; delay1us; delay1us; 
  delay1us; delay1us; delay1us; delay1us; delay1us; delay1us; delay1us; delay1us; delay1us; delay1us; 
  delay1us; delay1us; delay1us; delay1us; delay1us; delay1us; delay1us; delay1us; delay1us; delay1us; 
  delay1us; delay1us; delay1us; delay1us; delay1us; delay1us; delay1us; delay1us; delay1us; delay1us; 
  PINF = 0x10; // Fin de la première impulsion
  frame[0] = (PINF&0x20)>>5; // La première touche est enregistrée

  for(byte k=1;k<88;++k) { // Traitement des 87 touches suivantes
    if((k&0x7)==0) ++f; // Change d'octet toutes les 8 touches
    PINF = 0x10;
    delay1us; // pause de 1us;
    PINF = 0x10;
    // Enregistrement de la touche
    frame[f]<<=1;
    frame[f]|=(PINF&0x20)>>5;
  }
  sei(); // Autorise les interruptions

  // Affiche les touches qui ont changé d'état
  uint8_t k=0; // Numéro de la première touche à tester
  for(f=0;f<11;++f) {
    if(frame[f]!=pframe[f]) { // Est-ce qu'une des 8 prochaines touches a changé ?
      uint8_t cf = frame[f];
      uint8_t mb = pframe[f]^frame[f];
      uint8_t cb = 0;
      for(uint8_t m=0x80;m;m>>=1) { // Teste les 8 touches de cet octet
        if(mb&m) { // Cette touche a changé
          Serial.print(keysLabel[k+cb]);
          Serial.println((cf&m)?" relâchée":" pressée");
        }
        ++cb;
      }
      pframe[f] = cf; // Mise à jour de pframe (uniquement les octets modifiés)
    }
    k += 8; // On teste les 8 touches suivantes
  }   
  delay(20);  // Pause de 20ms (pour environ 50 scans par seconde)
} 

Nous voici presque à la fin de ce projet. La prochaine étape sera de configurer l’Arduino Pro Micro pour qu’elle soit reconnue par le PC comme un clavier, et envoie les touches pressées à celui-ci.

Dé-briquer une Arduino Pro Micro

En modifiant mon programme permettant de lire un clavier de Wyse WY-30 pour Arduino, j’ai utilisé les instructions cli() et sei() permettant d’interdire et de rétablir les interruptions.

Tout allait bien jusqu’à ce qu’en déplaçant ces instructions je fasse une erreur de copier/coller qui a effacé le sei().

Une fois le programme téléversé dans l’Arduino, le timing de lecture du clavier fonctionnait très bien lorsqu’on le regardait à l’oscilloscope mais celle-ci n’était plus reconnue comme un périphérique USB valide lors de la connexion à un PC.

Du coup, impossible de mettre à jour le programme ou de faire quoi que ce soit d’autre avec cette carte.

J’ai trouvé une page sur stack-overflow contenant beaucoup d’informations très utiles pour ramener à la vie une carte Arduino qui ne répond plus, mais la suggestion pour les cartes à base d’ATmega32u4 (garder la carte en état de RESET jusqu’au début du téléversement) ne fonctionnait pas dans mon cas.

Cependant, plusieurs suggestions pour les autres cartes utilisaient une Arduino UNO configurée comme un programmeur externe pour reflasher le bootloader. Ca m’a rappelé qu’il y a quelques mois, j’avais acheté un programmeur externe compatible USBtiny afin d’utiliser des ATtiny23A (ce microcontrôleur n’a pas assez de flash pour utiliser un bootloader), donc je me suis dit que j’allais tenter la manipulation (reflasher le bootloader efface le programme téléversé par effet de bord).

Ce type de programmeur externe se connecte en principe sur un port ICSP à 6 broches. L’Arduino Pro Micro ne dispose pas d’un tel port, mais les pins correspondantes sont accessibles sur ses GPIO donc il a suffi de faire un petit adaptateur en utilisant des câbles Dupont.

Câble ICSP pour Arduino Pro Micro réalisé avec des câbles Dupont. J’aurais pu utiliser deux connecteurs à 3 broches mais avec un seul connecteur à 12 broches il n’est pas possible de faire d’erreur en branchant l’Arduino.

Utiliser un connecteur Dupont avec le bon nombre de pins au lieu de n’avoir que des connecteurs 1×1 à brancher un par un simplifie beaucoup l’utilisation du câble. Vous pouvez acheter une prise à sertir dédiée ainsi qu’un lot de broches Dupont à sertir, et des connecteurs de la taille souhaitée, mais il y a une solution plus simple que j’ai utilisée ici :

Vous pouvez simplement acheter les connecteurs de la taille souhaitée (la partie en plastique sans les broches), un câble Dupont de 40 broches tout fait, avec des connecteurs 1×1 à chaque extrémité, puis séparer de celui-ci une nappe avec le nombre de broches souhaité, enlever les connecteurs 1×1 et insérer vos broches parfaitement serties dans vos connecteurs.

L’Arduino Micro à ressusciter, le câble ICSP et le programmeur externe USBtiny au second plan

Une fois le câble réalisé, il a suffit de connecter l’Arduino au programmeur externe, puis de connecter ce dernier au PC et d’effectuer les opérations suivantes dans l’IDE Arduino :

  • Choisir le menu Tools / Programmer / USBtinyISP
  • Choisir le menu Tools / Board / Arduino Leonardo(*)
  • Sélectionner l’option Tools / Burn Bootloader

Et après quelques secondes, la carte est comme neuve ; il ne reste plus qu’à la reconnecter et à re-téléverser votre programme (sans oublier d’ajouter le sei() manquant cette fois).

Il est probable que téléverser un programme en utilisant l’USBtiny fonctionnerait aussi, mais je n’ai pas essayé. C’est peut-être une meilleure solution si vous n’êtes pas totalement sûr du type de carte que vous utilisez.

Maintenant il ne me reste plus qu’à faire marcher mon programme de lecture de clavier sans refaire planter l’Arduino :o)

(*) Les Arduino Leonardo et Pro Micro sont très proches, et si il y avait une option « Arduino Pro Micro » j’ai préféré choisir « Arduino Leonardo » car avant l’incident ma carte était reconnue par l’IDE Arduino comme une Leonardo (c’est ce qui arrive quand on utilise des clones « no-name made in China »). Dans la pratique, la seule différence est le mapping de certaines GPIO, comme par exemple les leds RX et TX donc ça ne devrait pas poser de problème.

Interfacer un clavier de Wyse WY-30, part IV

Après avoir testé mon second programme (voir plus bas pour le code source), j’ai pu constater que ce n’était pas encore parfait mais ça commençais à fonctionner, et les traces de l’oscilloscope sont devenues beaucoup plus proches du résultat attendu :

Traces à l’oscilloscope d’une trame complète (88 bits correspondant à 82 touches plus quelques valeurs non-utilisées)

La voie 1 en jaune correspond au signal /CLK généré par l’Arduino, tandis que la voie 2 en rose correspond au signal DATA renvoyé par le clavier.

La partie en haut de l’écran contient toute la trame du clavier, et la partie en bas de l’écran est un zoom de la zone à fond noir de la partie haute.

Vous pouvez voir la première impulsion dans /CLK à gauche qui remet le compteur de touches à zéro, puis les impulsions pour chaque touches.

Lors de cette capture, j’avais pressé la seconde touche (le « – » du pavé numérique), ce qui se traduit par une sortie LOW du signal DATA dès le début de la seconde impulsion, et jusqu’au début de la 3ème.

Vous pouvez aussi remarquer si vous regardez de près la trace de /CLK qu’il y a un « trou » aux environs des deux tiers de la lecture des touches. Cela est sans doute causé par une interruption de l’Arduino. Ca n’a pas causé de problème, mais il devrait être possible d’éliminer ce petit glitch en interdisant les interruptions durant la lecture.

Enthousiasmé par ce résultat, j’ai continué à coder pour lire les codes des touches, et je n’ai pas conservé de copie de cette première version « fonctionnelle », mais en voici une recréation d’après ce dont je me souviens :

// GPIO utilisées
#define CLK 21
#define DATA 20

#define delay1us __asm__("nop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\n")

uint8_t frame[11]; // Frame contenant l'état de toutes les touches

void setup() {
  pinMode(CLK, OUTPUT);
  pinMode(DATA, INPUT);
  digitalWrite(CLK, HIGH);
  Serial.begin(115200);
}

void loop() {
  byte f=0;
  memset(frame, 0, sizeof(frame)); // Re-initialise l'état des touches
  PINF = 0x10; // Début de la première impulsion (io 21 = PORT F, bit 4)
  // Pause de 30 micro-secondes
  delay1us; delay1us; delay1us; delay1us; delay1us; delay1us; delay1us; delay1us; delay1us; delay1us; 
  delay1us; delay1us; delay1us; delay1us; delay1us; delay1us; delay1us; delay1us; delay1us; delay1us; 
  delay1us; delay1us; delay1us; delay1us; delay1us; delay1us; delay1us; delay1us; delay1us; delay1us; 
  PINF = 0x10; // Fin de la première impulsion
  frame[0] = (PINF&0x20)>>5; // La première touche est enregistrée

  for(byte k=1;k<88;++k) { // Traitement des 87 touches suivantes
    if((k&0x7)==0) ++f; // Change d'octet toutes les 8 touches
    PINF = 0x10;
    delay1us; // Pause de 1us;
    PINF = 0x10;
    // Enregistrement de la touche
    frame[f]<<=1;
    frame[f]|=(PINF&0x20)>>5;
  }

  // Affiche en hexa l'état de toutes les touches lues sur la console série
  for(f=0;f<11;++f) {
    Serial.print(frame[f],HEX);
    Serial.print(" ");
  }
  Serial.println("");
  delay(100);  // Attend un peu avant de recommencer la lecture
}

Notez que j’ai également optimisé les timings :

  • La durée de la première impulsion est maintenant de 30 micro-secondes
  • Les impulsions suivantes ne durent plus que 1 micro-seconde
  • Il n’y a plus de pause entre les impulsions, puisque l’exécution des instructions qui stockent l’état de chaque touche dure suffisamment longtemps pour que le contrôleur du clavier détecte le changement d’état de /CLK

Il devrait être possible d’aller encore plus vite, mais ces valeurs permettent de scanner tout le clavier en moins de 200 micro-secondes, ce qui permet en théorie de lire le clavier 5000 fois par secondes. Dans la pratique, 50 on 60 lectures par secondes devraient être suffisantes pour avoir un bon confort d’utilisation (en fait, si je voulais faire de ce bidule un vrai produit utilisable au quotidien, il est probable que je réduirais la vitesse du micro-contrôleur afin de réduire sa consommation électrique).

Bon, c’est un bon début, mais pour l’instant à part voir des nombres hexadécimaux défiler et des lignes bouger sur l’oscilloscope ça ne fait pas grand chose. La prochaine étape sera donc de décoder les touches pressées.

Interfacer un clavier de Wyse WY-30, part III

La première partie de cette série est disponible ici.

Une fois le clavier nettoyé et remonté, il est temps d’essayer de lui parler.

A terme, je compte ajouter le clavier à un projet tournant sur un Pi RP2040, mais pour faire mes premiers tests j’ai utilisé une Arduino Pro Micro. Le principal avantage étant que cette carte utilise des niveaux logiques en 5V, comme le clavier, ce qui me dispense d’utiliser des level shifters pour faire la conversion 3,3V vers 5V.

Premier prototype : Une Arduino Pro Micro et une breadboard. A droite, deux sondes qui vont à l’oscilloscope pour déboguer.

Le protocole de ce clavier est relativement simple :

  • Il y a un compteur permettant de parcourir chacune des 82 touches
  • Une impulsion « longue » (plus de 30 micro-secondes) sur la ligne /CLK remet le compteur à 0
  • Une impulsion courte (moins de 6 micro-secondes) sur la ligne /CLK incrémente le compteur pour lire la touche suivante
  • A chaque instant, la ligne DATA retourne l’état de la touche sélectionnée

Ca n’a pas l’air bien compliqué. Arduino disposant d’une fonction delayMicroseconds(), tout ce que j’ai à faire est :

  • Mettre la ligne /CLK à LOW
  • Attendre 35 micro-secondes
  • Mettre la ligne /CLK à HIGH
  • Lire DATA et stocker la valeur de la première touche
  • Répéter pour les 81 touches restantes :
    • Attendre 2 micro-secondes (par sécurité)
    • Mettre la ligne /CLK à LOW
    • Attendre 2 micro-secondes
    • Mettre la ligne /CLK à HIGH
    • Lire DATA et stocker la valeur de la touche
  • Afficher le résultat sur la console série

J’ai choisi 35 micro-secondes pour l’impulsion « longue » et 2 micro-secondes pour l’impulsion « courte » plus ou moins au hasard car il n’y avait pas d’indications précises, de même j’ai ajouté un délais de 2 micro-secondes entre chaque touche pour laisser le temps au clavier de détecter que la ligne est bien remontée à HIGH.

Bien évidemment, ça ne fonctionnait pas comme prévu et après vérification sur un oscilloscope, le signal /CLK n’avait pas du tout le bon timing…

Une recherche rapide sur la documentation de delayMicroseconds() (que j’aurais du lire avant de l’utiliser :o) ) m’apprend que si cette fonction fait d’une manière générale ce que son nom indique, le délais généré dure au minimum 3 micro-secondes (ou 6 selon certaines autres sources). Ca pourrait presque passer, mais l’instruction digitalWrite() utilisée pour mettre la ligne /CLK à LOW ou HIGH prend aussi du temps, ce qui fait que l’on dépasse la limite de 6 micro-secondes.

Heureusement il y a des solutions : L’instruction assembleur NOP (no-operation) s’exécute en 1 cycle CPU, qui dure approximativement 1/16 micro-seconde. Il suffit donc de l’exécuter 16 fois d’affilé pour obtenir une pause de 1 micro-seconde, ce qui peut se faire simplement grâce à la macro suivante :

#define delay1us asm("nop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\n")

De même, il est possible d’accéder directement aux entrées-sorties (GPIO) de l’Arduino via un registre. Sans rentrer trop dans les détails, les entrées sorties sont groupées par 4 ou 8 dans des ports, et chaque port dispose de plusieurs registres accessibles depuis un sketch Arduino comme une simple variable.

Par exemple, la GPIO 21 que j’utilise pour la sortie /CLK est la 5ème (bit 4) du PORT F. Il est donc possible de modifier sa valeur en modifiant le bit 4 du registre PORTF. Si cette GPIO était configurée en entrée, il serait possible de lire sa valeur en lisant le bit 4 du port PINF.

Cette méthode est bien plus rapide que digitalWrite(), surtout si on veut modifier plusieurs GPIO en même temps. Par contre si on ne veut en modifier qu’une, il faut commencer par lire la valeur du registre, puis modifier le bit correspondant à la GPIO concernée, et enfin ré-écrire le registre en entier.

Cela reste rapide, mais il est possible de faire encore mieux grâce à une fonctionnalité non-documentée des registres PINx. En effet, ce registre n’est supposé être utilisé que pour lire les GPIO en entrée, mais il est également possible d’y écrire des valeurs. Dans ce cas, pour toutes les GPIO dont le bit est à 1, la sortie est inversée (si la sortie était à LOW, elle passe à HIGH, et inversement).

Dans le cas de notre sortie /CLK (connectée à la GPIO 21, ou bit 4 du port F), il suffit donc d’écrire PINF = 32; pour l’inverser.

Comme on n’envoie que des impulsions sur cette sortie, le code deviens relativement simple. Pour générer une impulsion de 2 micro-secondes il suffit d’écrire :

PINF = 32;
delay1us; delay1us;
PINF = 32;

Bien sûr il faut connaître l’état initial de la GPIO. Pour cela il suffit de le fixer au démarrage du programme avec un appel à digitalWrite() :

#define CLK 21
setup() {
pinMode(CLK, OUTPUT);
digitalWrite(CLK, HIGH);
}

Il n’y a plus qu’à essayer de nouveau pour voir ce que ça donne.

Interfacer un clavier de Wyse WY-30, part II

La première partie de cette série est disponible ici.

Pour commencer, j’ai démonté le clavier afin de lui donner un grand nettoyage :

  • Inspecter le circuit imprimé pour identifier les problèmes potentiels
  • Enlever les touches et les faire tremper dans un mélange eau + liquide vaisselle
  • Brosser le boîtier en plastique tout en préservant les différentes étiquettes
  • Brosser la plaque d’acier sous les touches qui a accumulé toutes sortes de poussières pendant 30 ans

C’est lors de la première étape que j’ai constaté une tache suspecte sous un condensateur électrolytique. Suspectant une classique fuite, j’en ai profité pour le retirer et le remplacer mais… il s’est avéré que la tache suspecte n’était qu’une sorte de « hot glue », et après avoir testé le condensateur, je l’ai remis à sa place (ça commence bien :o) ).

L’intérieur du clavier qui avait bien besoin d’un nettoyage (notez le condensateur C1 manquant en haut à gauche)

Une fois l’inspection du circuit imprimé terminée, j’ai enlevé toutes les touches pour les faire tremper. J’ai eu une bonne surprise concernant les touches longues (Espace, Shift, Return, etc…). En principe ces touches ont une tringle métallique qui leur permet d’être actionnées quelle que soit la position où on les presse. C’est plus confortable pour la frappe mais par contre ce n’est pas toujours simple à démonter et remonter sans casser un bout de plastique qui les maintient en place.

Sur ce clavier les tringles sont sous la plaque d’acier, et on ne voit que 3 supports pour les touches concernées, ce qui rend le démontage / remontage des touches beaucoup plus simple (bon, par contre si on démonte la plaque d’acier, c’est probablement compliqué de remettre toutes les tringles en même temps lors du remontage, mais c’est plutôt rare de devoir faire ça).

Pendant que les touches baignaient dans l’eau, j’ai fait une recherche sur l’unique circuit intégré de la carte, et je suis tombé sur le manuel de maintenance de la WY-30, qui non seulement me donnait le brochage du port clavier mais détaillait également le protocole de communication de celui-ci.

Le connecteur femelle RJ11 étant particulièrement non breadboard-friendly, j’ai commencé par réaliser l’adaptateur ci-dessous afin de me simplifier la vie :

Adaptateur RJ11 vers header 0.1″ pour me faciliter la vie (ne regardez pas mes soudures de trop près :o) )

Notez l’étiquette qui me sera très utile si j’interromps de nouveau le projet pendant quelques années.

Le protocole est relativement simple puisqu’il n’utilise que 2 fils en plus de l’alimentation : en gros le host qui veut lire l’état du clavier envoie des impulsions sur la sortie /CLK, pour changer de touche et lit l’état de la touche en cours sur l’entrée DATA. Ce clavier n’ayant aucune LED du style CAPS-LOCK ou NUM-LOCK, il n’y a pas besoin de plus.

Il est donc temps de commencer un prototype à base d’Arduino, mais ce sera l’objet du prochain billet de cette série.

Interfacer un clavier de Wyse WY-30, part I

Lors de ma première année d’études en informatique, nous utilisions principalement des terminaux texte WY-30 et WY-50. J’aimais beaucoup ces terminaux, en particulier le WY-30 plus récent et légèrement plus avancé, mais surtout avec un clavier particulièrement agréable (C’est un de mes trois claviers préférés avec l’IBM Model M et celui de l’Oric Atmos).

Wyse WY-30 et son clavier (crédit photo: eBay)

L’année suivante, nous avons reçu des dizaines de stations de travail Sun, et rapidement les couloirs de l’école se sont remplis d’armoires métalliques pleines de terminaux Wyse dont plus personne ne voulait.

Lorsque j’ai quitté l’école, j’ai demandé au directeur si je ne pouvais pas en emporter une, sans trop y croire, mais à ma surprise, il m’a dit « pas de problème » :o)

Je comptais l’utiliser comme terminal série sur mon Amiga, en remplacement du minitel que j’utilisais jusque là (c’est très pratique pour déboguer en pas à pas des démos qui modifient la copper list), mais à cette époque j’utilisais surtout un PC, et je n’utilisais ce terminal qu’une fois de temps en temps.

Un jour, je le branche, et… la panne. Comme j’étais jeune et stupide (et que j’habitais un petit appart) je m’en suis débarrassé, mais j’ai gardé ce clavier en me disais qu’un jour je devrais pouvoir l’utiliser sur un autre ordinateur.

20 ans plus tard, je tombe par hasard sur un forum où un gars développe un « adaptateur universel » pour utiliser les claviers des différents terminaux Wyse (qui ne sont pas tous compatibles, même si ils utilisent plus ou moins les mêmes connecteurs). Je bookmarque l’article, en me disant que ça pourrait m’être utile et à tout hasard je commande sur eBay une dizaine de prises RJ11 femelles (connecteur utilisé pour mon clavier).

Puis rien ne se passe pendant 4 ou 5 ans, jusqu’à ce qu’en rangeant mon bureau je ne retombe sur ce clavier et son connecteur, ce qui m’a donné l’envie de relancer ce projet qui pourrait m’être utile pour un autre projet concernant mon RC2014 (et puis ce genre de projet, ça ne devrait prendre que quelques heures, non ? What could possibly go wrong? ;o) )

Enregistrer sur K7 avec un Omni 128HQ

L’Omni 128HQ est une machine très polyvalente côté émulation, et intègre des extensions bien pratiques comme des ports Joystick ou un (même deux) lecteurs de SD cards compatibles DivMMC, mais ce dernier ne peut fonctionner que lors de l’émulation d’un ZX-Spectrum (48K, 128K ou +2e, mais un Spectrum).

Cela pose problème lors de l’utilisation des autres machines comme le ZX-81 ou le Jupiter ACE. Heureusement il reste le port K7, que je vais utiliser ici.

Premier problème, il faut trouver le bon câble. Si les micro-ordinateurs d’origine avaient deux prises 3.5mm mono (une pour lire et une pour enregistrer, sur l’Omni 128HQ elles ont été remplacées par une seule prise 3.5mm stéréo (la seconde prise 3.5mm est toujours là mais elle permet d’écouter la sortie audio du chip AY-3-8910).

N’ayant pas pu trouver le bon type de câble, et ne voulant pas en faire un, j’ai utilisé un câble 3.5mm stéréo vers deux RCA mono, avec deux adaptateurs RCA vers 3.5mm mâle.

En guise de magnétophone, j’ai utilisé un TASCAM DR-05 avec la configuration suivante :

  • Format : WAV 24 bits
  • Sample : 44.1k
  • Type : Mono
  • MIC POWER : OFF
  • LOW CUT : OFF
  • PRE REC : OFF
  • AUTO-TONE : OFF
  • AUTO-REC : OFF

Il ne reste plus qu’à s’assurer que le contrôle du niveau d’enregistrement automatique est désactivé (menu « Quick » pendant que l’enregistrement est en pause) et à mettre le niveau d’enregistrement à 0 (le niveau en sortie de l’Omni 128HQ est assez élevé, mais ça fonctionne).

J’ai testé avec le mode Jupiter ACE et tout fonctionne très bien. Il m’est arrivé quelques fois que le Jupiter ACE plante à la fin de la sauvegarde, mais dans tous les cas, la sauvegarde a bien fonctionné.

Les commandes principales en Forth pour utiliser le lecteur de K7 sont :

SAVE fichier
VERIFY fichier
LOAD fichier

Notez que sur l’Omni 128HQ, la première fois que vous allez utiliser une de ces commandes, vous allez avoir un menu permettant de choisir le périphérique à utiliser. Il faut choisir l’option « Cassette » (ce menu n’apparaît pas sur un vrai Jupiter ACE).

Contrairement à l’équivalent BASIC, LOAD sans argument ne chargera pas le premier fichier trouvé, mais affichera les noms de tous les fichiers rencontrés (pressez la touche espace pour revenir au prompt). Il faut impérativement fournir le nom du fichier pour que celui soit chargé (et il n’y a pas de guillemets autour du nom, qui est limité à 8 caractères).

De plus, la commande LOAD est en fait l’équivalent de MERGE, donc si vous chargez un programme alors que d’autres sont déjà en mémoires, ceux-ci seront ajouté au dictionnaire.

Sauver une impression 3D après un problème de filament

J’ai une vieille imprimante 3D. Elle fonctionne bien, et pour les 10 à 15 trucs que j’imprime chaque année ça me suffit, mais il lui manque quelques fonctionnalités qui facilitent la vie sur les modèles plus récents.

En particulier, elle n’a pas de capteur de fin de filament, ni de reprise automatique en cas d’erreur. Ce n’est en principe pas trop gênant mais dernièrement j’ai essayé d’imprimer une « grosse » pièce (environ 12 heures d’impression ; ce n’est pas beaucoup dans l’absolu, mais mon imprimante n’ayant pas de « Thermal runaway protection » c’est rare que je fasse des impressions aussi longues).

Evidemment, après environ 4 heures le filament décide de casser et si je m’en rends compte 5 minutes après c’est déjà trop tard pour mettre en pause et enclencher un nouveau filament…

Pas de panique, j’utilise la commande manuelle de l’axe Z pour trouver la hauteur où le filament à cassé (en redescendant la tête jusqu’à ce qu’elle touche la pièce, et en notant la valeur Z affichée sur l’écran de l’imprimante), puis je génère un fichier gcode qui commence à cette hauteur.

Quelques précautions à prendre :

  • Vérifiez bien les paramètres de votre slicer : Par exemple sur Simplify3D, si vous voulez commencer à imprimer par exemple à 45.1 mm, il faut slicer à partir de 45.1 moins la hauteur de layer (c’est à dire avec des layers de 0.3mm à partir de 44.8mm)
  • Quand vous générez le nouveau gcode, enlevez toutes les options du style skirt ou raft, car vous allez commencer à imprimer dans le vide
  • Certains slicers vont commencer par imprimer un plancher à votre pièce, même si vous êtes au milieu d’un bloc. Ca fait perdre un peu de temps, mais si vous n’imprimez pas avec du filament transparent ça ne devrait pas trop se remarquer
  • Si votre profil d’impression a une hauteur différente pour le premier layer, il peut être une bonne idée de l’éditer pour qu’elle soit identique à celle des autres layers

Je lance l’impression du nouveau gcode en croisant les doigts, et celui-ci comme d’habitude commence par faire un homing sur les 3 axes pour retrouver l’origine. Et là je me dit que je suis chanceux car l’axe X de mon imprimante se retrouve à moins de 2 mm de la partie déjà imprimée ! Si le filament avait cassé deux minutes plus tard, ça aurait coincé et probablement ruiné toute possibilité de reprise…

Tout se passe bien pendant environ 10 heures de plus (oui, l’estimation à 12 heures d’impression du slicer était un peu optimiste) quand le filament casse de nouveau (à 20 minutes de la fin de l’impression)…

Je me rend compte que je ne peux pas utiliser la même méthode que pour la première cassure car il serait impossible de faire un homing de l’axe Z avec la pièce au qui prend presque tout le plateau, mais perdu pour perdu je décide de tenter le tout pour le tout.

  • Je commence par trouver la hauteur où le filament a cassé en utilisant la même méthode que précédement, puis je fait un homing manuel des axes X et Y
  • Ensuite, je génère un nouveau fichier gcode qui commence à la hauteur de cassure

Puis, ça se complique un peu ; Je sais que la tête d’impression se trouve à la position {0, 0, 133.8 mm} (c’est la hauteur à laquelle le filament a cassé) et il faut donc que j’édite le fichier gcode à la main pour que l’impression commence à cette hauteur SANS faire de homing comme c’est le cas par défaut.

Au début du fichier gcode on trouve en principe la configuration de l’impression (choix des températures de plateau et de tête), puis un homing qui permet à l’imprimante de savoir où se trouve la tête, et enfin les mouvements de la tête pour « dessiner » l’objet à imprimer.

Dans mon cas c’est un peu plus compliqué car j’ai modifié le code généré pour que l’impression démarre plus vite:

  • Je commence par fixer les températures avec les commandes M140 et M104 qui sont asynchrones et permettent donc au programme gcode de continuer pendant la montée en température
  • Puis je fais un homing (ça fait gagner du temps si la tête était éloignée de l’origine à la fin de l’impression précédente)
  • Je déplace la tête de 1 cm en hauteur et 1 cm sur l’axe X pour éviter que la tête n’endommage le plateau en restant trop longtemps trop près
  • Je re-fixe les températures avec les commandes M190 et M109 qui sont synchrone, et donc pausent l’exécution jusqu’à ce que les températures voulues soient atteintes
  • Je fais un second homing pour que l’impression puisse commencer à partir du point {0, 0, 0}
…/…
M140 S60 T0
M104 S210 T0
G28 ; home all axes
G1 Z10.0 X10.0
M190 S60 T0
M109 S210 T0
G28
; process Process1
; layer 1, Z = 133.800
…/…

Il faut donc commencer par commenter les commandes G28 qui correspondent à « Homing all axes« , ainsi que la commande G1 qui déplacent la tête de 1 cm en X et Z pendant la chauffe, puis ajouter des commandes G92 qui permettent de dire à l’imprimante où se trouve la tête, ce qui donne :

…/…
M140 S60 T0
M104 S210 T0
;G28 ; home all axes
;G1 Z10.0 X10.0
M190 S60 T0
M109 S210 T0
;G28
G92 X0.000
G92 Y0.000
G92 Z133.800
; process Process1
; layer 1, Z = 133.800
…/…

En principe je commence mes impressions par une skirt autour de la pièce afin d’amorcer l’extrusion. Ce n’est pas une option ici donc j’ai extrudé un peu de filament avec la commande manuelle pour être sûr que la tête soit prête dès le début avant de lancer l’exécution du nouveau script.

Finalement l’impression s’est bien terminée, mais avec le temps perdu ça a duré presque 16 heures (heureusement que j’avais commencé tôt :o) ).

Il y a bien une légère marque au niveau des deux coupures, mais rien qu’un peu de ponçage ne saurer fixer.

Epilogue : J’ai commandé sur AliExpress un capteur de fin de filament pour environ 1 euro, afin d’éviter d’avoir à repasser par toutes ces étapes dans le futur.