1er prog : son sans lumière (assembleur)

Les sites internet qui proposent comme premier programme de faire flasher une led sont innombrables. Si vous avez envie de commencer par là, vous n’aurez aucun mal à en trouver un. Comme alternative, je vous propose de tirer un son d’un petit haut-parleur.

Le matériel

  • Haut-parleur (un petit haut-parleur récupéré dans un vieux PC fera l’affaire)
  • D1 : 1N4148 ou 1N4001
  • R1 : 330 Ω

Raccordez le haut-parleur à la pin C5 par l’intermédiaire de la résistance. L’autre broche du haut-parleur va à la masse. Placez la diode aux bornes du haut parleur en respectant bien la polarité : la cathode est dirigée vers la résistance et le µC. La diode ne change rien au son mais le haut-parleur comporte une bobine dont l’effet est de s’opposer aux variations brusques de courant. Lors du passage de la pin de l’état haut à l’état bas, la bobine du haut-parleur génère une tension inverse qui pourrait détruire le µC  si elle n’était pas court-circuitée par la diode.

Le principe

Pour produire un son avec un haut-parleur, il faut faire vibrer la membrane de celui-ci. Pour obtenir un son de 1000 Hz, il faut donc attirer puis relâcher la membrane 1000 fois par seconde. L’attraction doit donc durer 1/2000 de seconde, de même que le relâchement. La membrane du HP étant attirée lorsque le courant passe et relâchée lorsque le courant est interrompu, le problème revient à alterner l’état haut et bas de la pin du HP tous les 1/2000 de seconde.

Le programme

Lancez Studio4. Vous vous trouvez devant l’écran de gauche.

Cliquez sur ‘Atmel AVR Assembler’, introduisez le nom du projet (Son), cochez les cases ‘Create initial file’ et ‘Create folder’ et choisissez le répertoire dans lequel vous allez créer le projet, puis cliquez sur ‘Next’.

 

Ceci vous amène à l’écran suivant :

Cliquez sur ‘AVR Simulator ‘ puis choisissez votre µC et terminez en cliquant sur ‘Finish

 

 

 

 

Vous êtes maintenant prêts à entrer le programme.

A l’ouverture de Studio, vous avez certainement remarqué le choix de langage entre l’assembleur et le langage C d’AVR GCC. Pour ce premier exemple, j’utiliserai successivement les deux langages de programmation. Vous aurez ainsi l’occasion de comparer et de constater les ressemblances et divergences.

Tapez le programme ci-contre dans la fenêtre d’édition.

Tout ce qui est en vert ne fait pas partie du programme mais constitue des commentaires introduits par un point virgule (pour une ligne) ou encadrés par /* et */ pour un bloc de lignes. Les mots en bleu sont les instructions du programme.

 

 

 

 

 

 

 

Après avoir tapé le programme, vous pouvez directement essayer de charger le programme en suivant les étapes décrites ci-dessous :

  • 1° Lancez la compilation du programme en cliquant sur le bouton marqué 1 ou sur le menu BUILD
  • 2° La fenêtre Build s’ouvre en bas de la page, indiquant si la compilation est réussie et , si c’est le cas, montrant le nombre d’octets occupés par le programme.
  • 3° Cliquez sur le bouton marqué 3 pour ouvrir la fenêtre de l’AVRISP mII. Si vous utilisez un autre programmateur, ouvrez le programme Burn-O-Mat.
  • 4° Indiquez le fichier .hex à flasher
  • 5° Lancez le flashage

Quelques mots d’explication du programme

 Le programme commence par

  • .NOLIST
  • .INCLUDE « m8def.inc » ; Header for ATMEGA8
  • .LIST

Les lignes commençant par un point sont des directives de compilation.

Ici, il s’agit d’inclure dans le programme le fichier des définitions et paramètres de l’Atmega8.

Chaque µC a son fichier dans le répertoire C:\Program Files\Atmel\AVR Tools\AvrAssembler2\Appnotes

La directive .INCLUDE est entourée des directives .NOLIST et .LIST pour que le fichier m8def.inc ne soit pas imprimé lorsqu’on imprime le programme.

On trouve ensuite les directives suivantes :

  • .CSEG
  • .ORG $0000

.CSEG indique que ce qui suit est le ‘Code Segment’ et .ORG $0000 signifie que le programme commence à l’adresse $0000

Le programme commence alors par une initialisation de la pile et des Ports utilisés.

Initialisation de la pile

  •             ldi R16, HIGH(RAMEND)     ; Init MSB stack
  •             out SPH,R16
  •             ldi R16, LOW(RAMEND)      ; Init LSB stack
  •             out SPL,R16

La pile est une zone de mémoire servant à garder provisoirement certaines valeurs ou adresses dont on aura besoin plus tard. La pile fonctionne un peu comme une pile d’assiettes : la dernière chose empilée est la première chose que l’on enlèvera de la pile. Elle répond à la logique LIFO (Last In, First Out). Le µC maintient un pointeur de pile qui indique toujours l’adresse de la prochaine case libre pour la pile. On peut placer la pile où on veut mais traditionnellement, la pile débute au dernier octet de la mémoire des données (SRAM) et à chaque fois qu’on y empile une donnée, le pointeur de pile diminue d’une case. Le pointeur de pile est constitué de deux octets : SPH (Stack Pointer High) et SPL (Stack Pointer Low). Le fichier des définitions m8def.inc contient l’adresse (en 2 octets) de la fin de la mémoire RAM. L’initialisation du Stack consiste à transférer cette adresse dans le pointeur de pile. Pour ce faire, chaque octet est d’abord chargé (instruction LDI) dans le registre r16 puis transféré dans le pointeur de pile. En effet, le µC est incapable de travailler directement sur des mémoires périphériques. Les valeurs doivent nécessairement transiter par l’un des 32 registres internes (R0 à R31)

Initialisation des Ports

  • ldi R16,0b00100000               ; Port C5 en output
  • out DDRC,R16

Les ports sont les regroupements des Pin d’entrée/sortie du µC. Nous n’utiliserons dans cet exemple que le port C. Sur l’Atmega8, le Port C comprend les Pin 23 à 28 + la Pin 1 (qui a un usage spécial). Nous utilisons la Pin C5 (=Pin 28) mais c’est arbitraire et on aurait pu choisir une autre Pin de n’importe quel Port.

Chaque Port est défini par 3 registres d’entrée/sortie de 8 bits (= différents des registres internes).

Pour le Port C :

  • Le registre DDRC : il sert à indiquer la direction de chaque pin : entrée ou sortie. Quand un bit de DDR est à 1, la pin correspondante est en sortie. Quand il est à 0, la pin est en sortie. Voilà pourquoi nous chargeons la valeur 0b00100000 dans r16 avant de la transférer dans DDRC. On voit que seul le bit 5 est à 1.
  • Le registre PORTC : Il sert à fixer la tension des pins du port. Lorsqu’un bit est à 1, la pin correspondante sera à +5V. Lorsqu’il est à 0, la pin sera à 0V.
  • Le registre PINC : il sert à lire le niveau des Pin du Port. Si une Pin est à l’état haut, le bit correspondant sera mis à 1. Si elle est à l’état bas, le bit sera mis à 0.

Nous abordons maintenant le programme principal

Celui-ci est constitué d’une boucle et d’une routine

La boucle principale :

  • Son:
  •             ldi r16,0b00100000                ;Pin C5 à 1
  •             out PortC,r16
  •             rcall delay                                ;Routine d’attente
  •             ldi r16,0b00000000                ;Pin C5 à 0
  •             out PortC,r16
  •             rcall delay                                ;Routine d’attente
  •             rjmp Son                                 ;Retour au début du programme

La boucle commence par « Son : ». Il s’agit d’une étiquette permettant au programme de revenir à cet endroit en fin de boucle. Remarquez le double point suivant le nom de l’étiquette. Il faut savoir que l’étiquette ne consomme pas de mémoire dans le programme car elle est enlevée au moment de la compilation.

On commence par charger la valeur 0b00100000 dans r16 puis de transférer cette valeur sur PortC. Le bit 5 étant à 1, la pin C5 sera mise à l’état haut.

Il nous faut maintenant rester dans cet état 1/2000 seconde. Ce sera l’objet de la routine d’attente que j’appelle « delay ». C’est grâce à l’instruction RCALL (Relative Call) que nous appelons cette routine.

Ce délai passé, on poursuit avec les instructions suivantes qui consistent à charger la valeur 0b00000000 dans r16 et à transférer cette valeur sur PortC. Cette fois, le bit 5 est à 0, donc la Pin C5 va passer à 0V. On fait à nouveau suivre par un appel à la routine « delay »  afin de prolonger cet état pendant 1/2000 seconde.

La boucle principale se termine par l’instruction RJMP son (Relative Jump to Son), ce qui nous ramène au début de la boucle et on est reparti pour un tour.

La routine « delay »

  • delay:
  •             ldi r17,125                               ;Choix d’une durée d’attente
  • delay1:
  •             dec r17                                   ;r17 = r17 – 1  (1 cycle)
  •             nop                                         ;Pas d’opération (1 cycle)
  •             brne delay1                             ;si r17 # 0, branchement à delay1
  •                                                            ;(2 cycles si branchement, sinon 1 cycle)
  •             ret                                           ;Retour au programme appelant

Comment « attendre » 1/2000 sec ?

On sait d’une part que le µC a une horloge qui tourne à 1 MHz, c’est-à-dire à 1 million de cycles par seconde. On sait d’autre part que beaucoup d’instructions de l’assembleur prennent 1 cycle pour être effectuées (dans le menu Help / Assembler Help, vous trouverez la liste de toutes les instructions de l’assembleur. Pour chacune de ces instructions, l’article se termine en indiquant combien de cycles cette instruction consomme). Un simple calcul indique combien de cycles d’horloge il faut pour une durée de 1/2000 seconde. Il faut 1000000 / 2000 = 500 cycles d’horloge.

On pourrait donc créer une boucle qui dure 4 cycles d’horloge et parcourir cette boucle 125 fois.. C’est ce qui est fait dans la boucle delay : on charge d’abord 125 dans le registre 17 (LDI  signifie LoaD Immediate). Ensuite on entame une boucle qui débute par DEC r17 (DECrement), ce qui retranche 1 à r17.  Cette instruction dure 1 cycle. Puis vient l’instruction NOP (No OPeration), qui consiste à ne rien faire sauf à dépenser 1 cycle. La boucle se termine par BRNE delay (BRanch if Not Equal). Cette instruction demande une petite explication. Le µC possède un registre de statut nommé SREG qui contient 8 flags ou drapeaux (page 11 de la Datasheet). L’un de ces flags est nommé Z (pour Zero). Ce flag est mis à 1 lorsque le résultat de la dernière opération est zéro ou lorsque l’on compare 2 registres égaux. Or l’instruction BRNE teste le flag Z. Si Z est à zéro, la dernière comparaison n’est pas égale et le branchement a lieu, ce qui prend 2 cycles. Par contre, si Z = 1, le programme continue sans branchement et cela prend 1 cycle. Donc, la boucle recommence tant que r17 est différent de 0.

Si on fait le compte, la boucle représente un total de 4 cycles et comme r17 était positionné à 125, la boucle consomme 125 x 4 = 500 cycles. On passe alors à l’instruction RET (RETurn) qui renvoie le programme d’où il a été appelé

En réalité, l’appel à la routine Delay prend un peu plus que 500 cycles car il faut compter quelques cycles pour l’appel de la routine (rcall), la sauvegarde sur la pile de l’adresse de retour (automatique lors de l’appel d’une routine), la fixation de la valeur de r17 (ldi) et l’instruction de retour (ret) avec rechargement de l’adresse reprise de la pile (automatique). Mais ces quelques cycles sont assez négligeables par rapport aux 500 de la boucle.

Lors de la compilation du programme, la fenêtre Build vous a montré que le programme compilé occupe 36 bytes. Cela correspond exactement à 18 instructions du programme assembleur. En effet, chaque instruction est traduite en un code machine de 2 bytes. Comme l’Atmega8 comporte plus de 8000 bytes de mémoire flash, le programme actuel est minime.

Il est néanmoins possible de l’optimiser. Voici comment. Dans le programme principal, les 3 premières lignes ressemblent beaucoup aux 3 suivantes. La seule différence est que le bit 5 du PortC est une fois à 1 et une fois à 0. Au lieu de charger deux fois de suite ces valeurs, on peut utiliser une instruction qui fait basculer le bit dans l’état opposé à chaque passage. Cette instruction est EOR (Exclusive OR). EOR effectue un « OU exclusif » entre chaque bit de 2 registres et place le résultat dans le premier registre. On va donc écrire EOR r15,r16

Voici la table de vérité du OU exclusif :

On remarque que lorsque le bit de r16 est à 1, le résultat de EOR est toujours l’opposé  du bit correspondant de r15.

Le programme principal peut maintenant être écrit :

  • Son:
  •             eor r15,r16                              ; inverse le bit 5 de r15
  •             out PortC,r15                          ; Transfert la valeur sur PortC
  •             rcall delay                                ;Routine d’attente
  •             rjmp Son                                 ;Retour au début du programme

Le comportement est identique au programme précédent, avec 3 lignes d’instructions en moins et bien sûr une compilation en 30 octets.

Retour au MENU

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s