rforth1 pour tous

par Jean-Baptiste Mayer

Avant-propos

Cette série d'articles est un tutoriel d'utilisation et de programmation des PIC18F avec la chaine de développement rforth1. Le but de ces articles est d'amener le lecteur à maitriser le langage forth et l'architecture des microcontrôlleurs PIC au travers d'une série d'exemples gradués.

Pourquoi un autre langage?

Nous voulions un langage qui soit adapté à l'embarqué. Aussi, certaines fonctionnalité de langages de plus haut niveau (pensons au C par exemple, et à son opérateur 'addresse' "&") nécessitent d'avoir un adressage uniforme de la mémoire. La définition locale de variables, difficilement contrôlable, est aussi un bon moyen de faire exploser une pile d'appels. Enfin, l'allocation dynamique de mémoire (malloc) était à proscrire vu le peu de ressources disponibles.
En résumé, il nous fallait:

Pourquoi forth?

forth est un des langages les plus simples qu'il soit. Il est extrêmement simple d'écrire un compilateur ou un interprêteur forth, et le faible nombre de d'adressages rend les optimisations de code beaucoup plus simple à détecter. Enfin, le langage ne définit presque pas de structures de contrôle: la liberté est totale.
forth est également utilisé dans l'industrie dans les applications nécessitant un langage assez complet et léger: dans certains BIOS d'ordinateurs (Sun, Apple), dans des consoles de débuggage (WinDbg), ou encore des applications financières.

Pourquoi rforth1?

Nous avions commencé le développement de nos applications avec un autre compilateur, Pic18Forth. Il utilise une machine virtuelle, qui nous pénalisait beaucoup dans les applications lourdes en calcul. Nous avons en plus remarqué un problème d'architecture dans ce compilateur, qui rendait souvent le programme faux. Plutot que de modifier Pic18Forth, nous avons choisi d'écrire un autre compilateur, qui génère du code natif.
Sinon, rforth1 signifie 'robotics forth, version 1'.

1. Démarrage

Installons des logiciels

Avant de commencer à jouer avec rforth1, il faut quelques logiciels:

Python

Python est un langage interprété orienté objet. Très efficace (il mêle compilation et interprêtation), il est assez bien adapté à l'écriture de compilateurs (mais pas autant que OCaml, mais c'est une autre histoire)
Python est un logiciel libre.

Un éditeur de texte

Les accoutumés de Visual Studio, Borland Developper ou autre KDevelop vont être un peu déçus: nous n'avons pas d'Environnement de Développement Intégré pour rforth1.
Heureusement, il existe pas mal d'éditeurs de texte qui permettront d'écrire le code avec une mise en évidence de la syntaxe.

Kate, Vim et emacs sont des logiciels libres.

L'assembleur PIC

rforth1 génère un code assembleur, et utilise un assembleur pour générer le code de programmation du processeur.
Il a été testé et developpé avec les GPUtils: http://gputils.sourceforge.net/.
Les GPUtils sont un logiciel libre.

Une console décente...

Là je ne peux pas trop vous aider.

Récupérer rforth1

rforth1, écrit par Samuel Tardieu, est disponible depuis le site de l'auteur.

Placer un bootloader dans le pic

Une habitude personnelle, que je vous conseille: placer un chargeur de boot dans le PIC avant de commencer à developper.

 

Le bootloader, ou chargeur d'amorce, est un programme de base qui est placé dans la puce lors de son montage sur la carte. Au démarrage de la puce, le bootloader est chargé en place du programme principal, puis au bout d'un temps prédéterminé passera la main au programme principal.

 

Ce temps peut en particulier servir à:

 

En un mot, le bootloader va permettre de simplifier grandement le développement et la mise au point du programme embarqué.

 

Voici le notre, il est minimaliste, ne fait pas le café, mais fait bien son travail: il se trouve dans l'archive, dans le répertoire bootloader, fichier monitor.hex.

 

Il est prévu pour un pic fonctionnant à 40MHz (quartz 10MHz + PLL)

 

On le met dans le PIC de façon assez classique: avec un programmateur de puces, un programmateur in-situ...
La FAQ de T-Bot explique en détail comment faire. A défaut, demandez à quelqu'un d'autre de vous placer le bootloader, ça prend 2 minutes.

 

Une fois que c'est fait... on est prêt à partir!

2. Bonjour Monde

Premier passage obligé, le "Bonjour Monde" (ou 'hello world')

Avant toute chose, lire la page sur l'utilisation de rforth1 et du moniteur

Hello :)

 needs lib/tty-rs232.fs

: main ( -- )

    ." Bonjour Monde :)" cr

    begin again ;

Compilons

On va sauver le fichier sous le nom 'hello.fs', dans c:\rforth1 ou dans ~/work/rforth1. On compile dans une console:  python rforth1.py hello.fs On obtient (entre autres) un fichier hello.hex, que l'on va pouvoir charger dans le PIC avec le moniteur:  python utils/monitor.py --program --port /dev/ttyS0 hello.hex La première étape a transformé (compilé exactement) le programme décrit dans hello.fs en un code binaire exécutable par le PIC. Ce code exécutable est ensuite chargé dans le PIC par le moniteur: le moniteur communique avec le PIC (et en particulier le bootloader) pour placer le code dans la flash du PIC. Ça change beaucoup des programmateurs classiques :)
On peut alors redémarrer le pic avec un terminal sur le port série, et hop! le programme marche :).
On verra page suivant comment on se sert du terminal et du moniteur en pratique.

En détails

Bibliothèques

 needs lib/tty-rs232.fs Notre programme va utiliser un port série - on inclut donc le fichier de définition du port série, qui s'appelle tty-rs232.fs. Ce fichier est dans le répertoire lib, et définit le moyen d'envoyer des caractères sur le port série standard du PIC. On examinera en détail son fonctionnement par la suite.
On pourrait vouloir utiliser le port USB via un chip FTDI245BM, en incluant tty-usb.fs par exemple...

Une fonction? un mot!

 : main ( -- ) [...] ; On définit ici le mot principal du programme (remarquons que l'on ne parle pas de fonctions, mais de mots). La définition d'un mot commence par : [nom] et se termine toujours par ;. Tout le code entre le nom et ; constitue le contenu du mot, et sera exécuté lorsque le mot sera appelé.
Par convention, on place juste après le nom du mot l'action du mot sur la pile: ( -- ) signifie ici que le mot ne dépile rien et n'empile rien. On y reviendra par la suite.

Invoquer un mot

     tty-init Comme on se sert du port série, on l'initialise au tout début du programme.
Vu que l'on utilise un bootloader, ce n'est pas nécessaire de le mettre ici: le bootloader l'initialisera pour nous.
On invoque donc le mot tty-init en écrivant son nom, tout simplement.

Envoyer du texte sur la console

     ." Bonjour Monde :)" cr Ici on envoie Bonjour Monde :) sur le port série: une chaine envoyée sur le port série commence par le mot ." (attention à l'espace entre le " et le début de la chaine...), et se termine par " (attention, pas d'espace entre la chaine et le "). Et comme nous sommes polis, on envoie un retour à la ligne:cr .

Bloquer le processeur

     begin again On a fini la partie utile, on bloque le contrôleur dans boucle infinie en attendant un reset: la boucle commence par begin et se termine par une répétion infinie: again

Résumons...

On utilise un mot en marquant son nom.
On déclare un mot en commençant par : [mot] et en terminant par un ;. Tous les mots marqués entre le nom du mot déclaré et le ; constitueront le corps de la fonction.
Tout ce qui est placé entre ( parenthèses ) sera ignoré par le compilateur: c'est un commentaire.
On envoie du texte sur le port série en écrivant ce texte entre guillemets précédés d'un point:." texte"

3. Console, terminal, moniteur: WTF?

Un terminal?

Dans cette section nous évoquons les différents moyens de communiquer avec un processeur. Ce n'est bien entendu pas spécifique à rforth1 ni aux PIC - même si certains éléments évoqués ici le sont...

Le port série

Les communications en série sont des communications très simples - qui consistent à envoyer bit à bit sur un fil des niveaux hauts ou bas qui sont la représentation de chaque bit des octets que l'on veut envoyer.

Protocole électrique

Un des problèmes que l'on rencontre avec les ports série vient de l'incompatibilité de ses niveaux électriques avec la logique CMOS. Comme la plupart des circuits intégrés, le PIC18F est réalisé en logique CMOS. Il faut donc effectuer une adaptation de niveaux si jamais on veut le faire parler aver un PC... qui suit la norme RS232.

Conséquences opérationnelles

Pour faire parler un PIC avec un PC, il faut un circuit d'adapation de niveaux de type MAX232. TODO: Mettre le schéma de notre cable RS232<->PIC

Protocole logique

Signal d'horloge

Les protocoles série existent en deux familles : les protocoles synchrones et les protocole asynchrones.

Dans le cas du PIC, on va faire le plus simple possible au niveau matériel : donc pas de contrôle de flux matériel, pas de signal d'horloge. C'est d'autant plus aisé que l'UART intégré ne gère pas le contrôle de flux ni le synchrone.

Protocole de communication

L'envoi d'un octet sur le port série s'effectue de la façon suivante:

Dans le cas de l'asynchrone, faute de signal d'horloge il faut en plus une vitesse de transmission approximative, qui sera affinée automatiquement par le matériel de réception. On passera sous silence les contrôles de flux matériel (avec des fils supplémentaires) et logiciels (XON, XOFF).

So what?

En résumé: la transmission série est une des plus simple qu'il soit - au moins au niveau matériel. Par contre, cette simplicité se fait au prix d'une grosse contrainte: l'utilisateur doit paramétrer exactement de la même façon les deux extrémitées de la ligne.

Réglons la cible

Le processeur n'a en général aucune notion de sa vitesse de fonctionnement. Il n'a donc aucun moyen de savoir comment il doit diviser son horloge interne afin de générer (ou de recevoir) les données. Pour ça, pas de secret, il faut se palucher la notice technique.

4. K2000

Notre prochain objectif: faire clignoter des diodes façon K2000!

J'ai une carte avec un pic18f256 et 3 diodes sur le port C (et plus exactement les pattes RC0, RC1 et RC2). Mais attention, comme je n'avais pas assez de place sur la carte, j'ai branché l'anode sur le pic et la cathode sur le VCC. Rien de grave, on en tiendra compte dans le programme...

Accèdons à l'extérieur

L'architecture PIC et les registres de contrôle

Ceux qui sont familiers avec les pics peuvent passer directement au paragraphe suivant, les autres ont le droit de ne pas s'affoler... et de lire la suite.

Des ports, des latches et des tristates

Le pic est un microcontrôleur difficile d'accès, car chaque patte peut avoir plusieurs fonctions différentes. On va faire simple, pour l'instant on ne va pas utiliser les fonctions avancées. Chaque patte peut basiquement servir d'entrée ou de sortie. Lorsque la patte est configurée en sortie, le PIC peut connecter via un transistor (en fait 2) soit à la masse soit à l'alimentation. La patte est dite soit à 1 soit à 0, ou en LowZ. Lorsque la patte est configurée en entrée, la patte n'est connectée par le PIC ni à l'alimentation ni à la masse: La patte est dite en haute impédance (HighZ) , car la résistance entre la patte et les alimentations est suffisament forte pour qu'un montage extérieur puisse contrôler la tension sur la patte. En résumé :

Le contrôle de tout cela est fait au travers de trois registres pour chaque bloc de pattes (un port):

(Attention, il y a parfois des gags dans l'architecture du PIC à ce propos: par exemple, certaines pattes sont en collecteur ouvert, i.e. elles n'ont que l'état HighZ et 0; d'autres pattes sont communes avec des convertisseurs analogique/numérique activé par défaut - ça pose parfois problème quand on l'oublie)

Modifier un registre avec rforth1

Les registres sont définis dans rforth1 sous le même nom que dans la datasheet du PIC. Donc en cas de doute, on regarde la datasheet, et on connaît le nom du registre. On peut bien entendu écrire le registre d'un coup:  c! ( valeur_8bits registre -- ) Et de même, pour les registres 16 bits (comme les timers)  ! ( valeur_16bits registre -- ) Comme l'architeture du PIC permet de positionner les bits indépendemment, on peut manipuler les bits un par un.  LATA 0 bit-set Permet de placer à 1 ("set") le bit 0 du Port A. À condition que le tristate correspondant soit en basse impédance, cette instruction met la patte RA0 à +5V... Les instructions pour modifier un bit d'un registre sont:  bit-set ( registre bit -- ) bit-clr ( registre bit -- ) bit-toggle ( registre bit -- ) Comme les anglophones l'auront compris directement, bit-set place le bit à 1, bit-clr le passe à 0, et bit-toggle le passe à 1 s'il était à 0, et à 0 s'il était à 1.  

Application

Soyons simple

Ma première version est très bête: on a 3 leds, donc 4 étapes dans le chenillard K2000.

( Définition d'une attente de 10000 cycles de 10 instructions )
( soit environ 0.01 seconde )
 
: wait ( -- )
  10000
  begin
    1 -
  dup 0= until
  drop ;
 
( On fait une attente un peu plus longue, grace à l'attente courte )
 
: wait-long ( -- )
  100
  begin
    wait 1-
  dup 0= until
  drop ;
 
: main ( -- )
  ( Initialisation des bits 0 à 2 du port C en sortie )
  0xf8 TRISC c!
  begin
    0x01 LATC c!
    wait-long
    0x02 LATC c!
    wait-long
    0x04 LATC c!
    wait-long
    0x02 LATC c!
    wait-long
  again ;

5. La pile

Si vous avez bien lu le passage sur K2000, vous avez du voir cette construction étrange:  1000 begin    1 - dup 0= until drop Cette construction étrange compte de 1000 à 0... sans jamais nommer de variable locale!

En réalité, nous utilisons des variables locales allouées sur la pile.

 

Pas à pas

Code Action Etat de la pile après
1000 Pousse la valeur '1000' en haut de la pile 1000
begin Démarre une boucle 1000
1 Pousse la valeur '1' sur le haut de la pile 1000,1
- Soustrait la valeur en haut de la pile à la valeur immédiatement en dessous. A la première itération, ce code soustrait '1' à '1000'. Les deux opérandes sont retirés de la pile et le résultat placé en haut de la pile. 999
dup DUPlique la valeur en haut de la pile. Elle y est donc 2 fois... 999,999
0= Teste la valeur en haut de la pile pour égalité à 0, dépile l'opérande, met sur la pile la valeur 'vrai' si l'égalité est réalisée et 'faux' sinon. 999,faux
until Continue la boucle tant que faux est en haut de la pile 0 (voir note)
drop Elimine la valeur en haut de la pile  

Note: En sortie de boucle, la valeur testée est nécessairement 0. A la première itération, la pile est à 999 ...

Le modèle standard de pile, le modèle de rforth1

Une pile est un espace de stockage (en théorie) illimité, que l'on manipule avec 3 opérations:

Et alors?

Imaginons que les mots puissent modifier cette pile: par exemple, prendre deux valeurs en haut de la pile, et en placer deux autres toujours en haut. Ça ne vous dit rien? Appellons plutôt les deux mots dépilés des 'paramètres' et ceux empilés des 'valeurs de retour'. Vous l'aurez compris, la pile permet de faire des fonctions auxquelles on peut passer des paramètres.

Une convention

Vous avez surement vu le commentaire à coté du nom des fonctions: : main ( -- ). Ce commentaire est bien entendu conventionnel: il veut dire que la fonction ne dépile pas de mot ni n'en empile. Par exemple, le fichier lib/tty-rs232.fs, qui décrit le port série, contient 4 fonctions:  : tty-init ( -- ) 0b00100000 TXSTA c! 64 SPBRG c! TRISC 6 bit-clr ; : emit ( c -- ) begin TXIF bit-set? until TXREG c! ; : key ( -- c ) begin RCIF bit-set? until RCREG c@ ; : key? ( -- f ) RCIF bit-set? ; En pratique, on dipose d'opérations arithmétiques qui peuvent agir sur cette pile:

Ceux qui connaissent la notation polonaise inversée (mais si, les calculatrices de chez HP...) ne seront pas perdus, les autres auront un peu plus de mal...

 

6. Logiciels

L'éditeur de texte

N'importe quel éditeur de texte convient; toutefois, nous avons l'habitude de Vim (sous Linux et ses amis); ou de UltraEdit sous Windows.

Le compilateur

Le compilateur et ses bibliothèques sont disponibles depuis le site de l'auteur. rforth1 utilise gpasm pour générer le code binaire: vous devrer l'installer pour pouvoir générer du code pour votre PIC favori. Enfin, rforth1 est écrit en python: vous aurez besoin de python pour l'utiliser.

Le bootloader et le moniteur

rforth1 vient avec 2 outils très pratiques pour le développement:

Le bootloader

rforth1 fournit 2 bootloaders qui répondent à deux besoins distincts:

Le moniteur

Le moniteur est le compagnon du bootloader: il permet de contrôler simplement les fonctions du bootloader, comme le chargement/vérification de programme en flash, les tests du matériel... Le moniteur fourni avec rforth1 est compatible avec les deux bootloaders, et sont configurés pour une transmission série à 115kbps.