Atelier du 12 février 2008
La programmation c'est si simple : atelier 3
6-Faire un ordinogramme
Qu'est-ce qu'un ordinogramme? Le terme anglais "flowchart" nous en dis plus sur cet outil indispensable. Ce diagramme définira le squelette de notre programme en indiquant clairement le déroulement des étapes à suivre pour résoudre le problème posé. Il peut être très sommaire au début, pour nous mettre en tête, l'ordre des choses à accomplir. Par la suite nous pouvons ajouter des branches, ou des commandes spécifiques au matériel lui-même.
Si nous tentons de faire un ordinogramme pour le cas qui nous préoccupe, il pourrait ressembler à ceci:

Tableau 10

Pour l'instant tout semble être parfait, cet ordinogramme comporte quand même des lacunes. Il représente notre conception du programme, mais pour le CPU cela n'est pas suffisant. La première case A nous demande de lire un nombre sur le port B, est-ce que le CPU sait que nous avons besoin des "pullups", est-ce qu'il sait que le port B doit-être en entrée? Nous devrons ajouter des cases, avant la case A, pour transmettre ces informations au CPU. La case B conserve la valeur non-modifiée en mémoire, est-ce qu'on a réservé un espace mémoire pour cette valeur? Toutes variables doivent être déclarées avant leur utilisation, donc une autre boîte s'ajoute avant la case A. La case C inverse le nombre, à cause de l'arrangement de nos commutateurs, il est plus logique pour l'utilisateur de faire cette inversion.
La case D affiche le nombre sur la première ligne? Comment pouvons nous être sûrs qu'il s'agit bien de la première ligne? Est-ce que l'on avait définit notre port A en sortie? Est-ce qu'on avait initialisé notre display? Est-ce qu'on a effacé le display et effectué la commande "home". OUPS! Il semble que bien des choses nous ont échappé, mais ce n'est pas grave on est encore sur papier! Donc il faudra définir notre port A en sortie, cela devrait être fait au début (avant la case A) Une fois le port A défini en sortie, mais pas avant, nous pourrons initialiser le display (avant A) Pour s'assurer que l'on est bien sur la première ligne, on effacera le display (home & clear) avant la case D. Il est possible que l'affichage d'un nombre binaire, nous demandent un peut de travail supplémentaire. En effet nous avons choisi d'afficher BIN: suivi de deux espaces, le premier nibble, deux espaces et le dernier nibble. Cette mise en forme nous demandera quelques lignes
de codes supplémentaires.
Les cases E et F effectuent des calculs de conversion et utiliseront deux nouvelles
variables qui devront être définies, elles aussi avant la case A préférablement. La case G affiche les résultats obtenus, nous devrons s'assurer que le résultat s'affichera au début de la deuxième ligne et le format choisi DEC:_085 HEX:_55 nous demanderas un peu de mise en forme.
La case H relit le port B pour accepter une nouvelle valeur. La case I vérifie si les commutateurs ont changé de position, si le chiffre présent à l'entrée du port B est le même que celui conservé en mémoire, nous n'avons pas de changement à faire, l'affichage est valide. Si par contre la valeur présente à l'entrée est différente, notre affichage ne représente pas la réalité et on doit refaire l'affichage.
Ce programme roulera donc sans fin, en boucle, de toute façon nous n'avons pas d'autre tâche à lui faire exécuter.
Vous voyez maintenant l'utilité d'un ordinogramme, même très primitif, il nous dicte l'ordre dans lequel les choses doivent se produire. Il nous restera à le compléter pour s'assurer que les blocs sont bien au bon endroit. Par la suite nous devrons analyser chacun de ces blocs pour trouver des redondances, c.-à-d. des endroits qui se répète dans le code. Ces blocs pourront être déplacés dans des sous-routines, ou fonctions, qui allègeront l'ordinogramme. La dernière étape consiste à coder ces blocs en code machine, en fonction du CPU choisi et de l'interpréteur utilisé.

Tableau 11

7-Est-ce possible de faire des modules?
Nous remarquons que nous devons envoyer, tout au long du programme, des commandes et des caractères au LCD. Chaque écriture au display nous demandera d'activer la ligne RA6 (E) pour une écriture de texte et d'activer les lignes RA7 (RS) et RA6 (E) pour écrire une commande. En plus nous devrons mettre le "data" dans une forme acceptée par l'afficheur, sur les lignes, RA3-RA0. Chacune de ces écritures nous obligeras de répéter à chaque fois qu'on exploite le LCD, de recommencer cette mise en forme. Serait-ce plus pratique de créer un sous-programme qui s'occuperait de tous les aspects de contrôle de l'afficheur? Ou encore mieux pourquoi ne pas rafraîchir toutes les données de l'afficheur en une seule séquence?
La création d'un module nous permettra "d'aérer" notre programme principal et ce module pourra être réutilisé dans d'autres programmes ultérieurement. A cette étape deux choix se propose à nous, une routine qui fait l'ensemble de l'œuvre, ou une routine qui acceptera une commande à la fois, caractères par caractères. Pour les besoins de la cause nous allons choisir la plus complète. Ce petit module, n'a qu'un travail à accomplir; rafraîchir l'affichage. La première étape sera d'effacer le LCD. Cette commande requiert l'envoi de la commande $01 sur l'afficheur et sera transmis au LCD de la façon suivante, puisqu'il s'agit d'une commande la patte RS doit rester à 0. Le data transmis à l'affichage est accepté quand nous effectuons une transition sur la ligne E, c.-à-d. que la ligne E, doit être amenée au niveau 1, pendant un certain temps et remise à zéro. Puisque nous utilisons un bus de 4 bits, nous devrons envoyer le MSB en premier suivi du LSB. Donc
voici la séquence:

Tableau 12

Fin de l'envoi de la première commande!
On vient juste d'effacer notre afficheur et on se rend compte qu'une multitude de lignes de code seront nécessaires pour effectuer cette simple tâche. Chaque fois que nous voulons envoyer une commande, nous devrons refaire la même séquence. Est-ce que notre mémoire sera suffisante? Je crois qu'il serait approprié de faire deux autres modules, qui s'occuperont des détails fastidieux. Un module pour les commandes et un pour les données à afficher. Le premier module, appelons SendCommand, recevras le code $01 (home and clear en hex) et effectuera la tâche de contrôler les bits RS et E, et s'occuperas des délais impliqués. Le deuxième, appelons le SendData, acceptera le caractère à afficher (en code ASCII) et il s'occupera de la routine d'affichage.
Ces deux modules se ressembleront tellement, que je serais tenté de vous proposer d'en faire un seul. Ce module pourrait s'appeler SendLCD et il tient compte d'une variable externe appelée DataBit, qui serait à 0 pour les commandes et à 1 pour le data. Notre module saurait maintenant si on lui passe une commande ou du data, et ajusterais le bit RS en conséquence. Vous remarquerez aussi que quand on doit choisir des noms pour des modules, ou des variables, il est préférable de leur donner des noms significatifs. Le programme fonctionnerait quand même si les modules s'appelleraient titi, tata ou tutu, mais qu'est que ce nom nous dit sur ce qu'il fait vraiment, par contre le choix d'un nom comme SendLCD, SendCommand ou SendData sont clair et ils nous aiderons plus tard si nous devons modifier le code.
Regardons maintenant à quoi ressemble ce module: (Tableau 13)
Premièrement il sera important de sauvegarder la commande dans une nouvelle variable, qui devra être déclarée. Pourquoi devons nous la sauvegarder?, parce que durant la routine, elle subira des transformations "bitwise" et qu"il seras impossible de la reconstruire. Cette première transformation est le décalage à droite des données de quatre positions. Cette opération nous permets d'aligner le nibble MSB, à la position LSB dans le registre. Cette opération peut s'effectuer en utilisant la commande "shift right", ou en divisant cette valeur par 16. Ensuite nous regardons la valeur de la variable DataBit, si il est à un nous additionnons $80 à notre commande décalée. Cette addition placera le bit RS à la valeur 1, ce qui est recommandé par le fabricant du LCD. Le résultat des transformations sera alors déposé dans le port A, qui transmettra ces données au
LCD. Après que ces données sont transmises il faut attendre quelques microseconde pour stabiliser le bus. Par la suite nous additionnons $40, ce qui aura pour effet de placer le bit E à 1. Après un certain délai, cette ligne du LCD doit être remise à 0. Ceci est accompli par la soustraction de $40 au résultat précédent, et ce nouveau résultat est emmagasiné dans le port A. Par la suite nous rechargeons le data/commande original et cette fois nous masquons les MSB, cette opération est facilement réalisée avec la fonction AND, encore une fois nous vérifions le DataBit pour faire la différence entre du data et des données. Nous ajoutons en conséquence le bit RS et on repart pour l'activation du bit E (clock) tout en respectant les délais du manufacturier. Finalement le travail est accompli, cette routine pourra être appelée en tout temps, même pour l'initialisation du LCD, au début du programme. Cette routine qui comportera environ trente-cinq case mémoires dans notre programme, si nous ne l'avions pas créé, il aurait fallu ajouter ces 35 lignes de codes, à chaque fois que l'on aurait voulu écrire un caractère. (environ 1120 lignes)
Il est à remarquer que la fonction de délai se représente six fois dans notre routine, on devra donc l'écrire six fois. Bon je sais vous avez hâte d'essayer votre programme, mais si jamais il nous manque de place dans la mémoire, on se rappellera que l'on peut sauver quelques précieuses cases mémoires en ajoutant ce nouveau module. (DoDelay).
Il n'y a aucun problème à utiliser des programmes, qui appellent des modules, qui appellent d'autres modules, qui appellent d'autres modules. L'ordinateur grâce à sa mémoire "stack" ou mémoire pile ne peut se perdre dans ce dédale. Cette mémoire de type "LIFO" (last-in first-out) permettra au CPU de retrouver son chemin, au travers de ce spaghetti, sans problème. Il est à noter toutefois que nos modules doivent respecter quelques conditions pour être parfaitement transparent; ces modules ne devrait pas changer l'état des registre de contrôle. C'est-à-dire que les modules devraient sauvegarder les valeurs des registres, avant de les modifier et les rétablir avant de redonner le contrôle au programme appelant.

Tableau 13