Very High Speed Integrated Circuit Hardware Description Language/Améliorer l'ATMega8 avec l'ATMega16 et l'ATMega32

Leçons de niveau 16
Une page de Wikiversité, la communauté pédagogique libre.
Début de la boite de navigation du chapitre
Améliorer l'ATMega8 avec l'ATMega16 et l'ATMega32
Icône de la faculté
Chapitre no 11
Leçon : Very High Speed Integrated Circuit Hardware Description Language
Chap. préc. :Embarquer un Atmel ATMega8
Chap. suiv. :Utiliser des systèmes mono-puce en verilog
fin de la boite de navigation du chapitre
En raison de limitations techniques, la typographie souhaitable du titre, « Very High Speed Integrated Circuit Hardware Description Language : Améliorer l'ATMega8 avec l'ATMega16 et l'ATMega32
Very High Speed Integrated Circuit Hardware Description Language/Améliorer l'ATMega8 avec l'ATMega16 et l'ATMega32
 », n'a pu être restituée correctement ci-dessus.

Nous avons l'intention dans ce chapitre de reprendre ce qui a été fait dans le chapitre précédent avec l'AVR ATMega8 de manière un peu plus systématique. Il doit être clair pour les lecteurs assidus de ce précédent chapitre, qu’il a été rédigé avec aucun recul. Ainsi plusieurs versions de l'ATMega8 ont été utilisées essentiellement pour la gestion des mémoires. Voici donc présentés les défauts du précédent chapitre que nous espérons corriger.

  • Une seule version du processeur de base devra être utilisée contrairement aux deux versions du chapitre précédent (premier défaut à corriger)
  • Nous n'avons pas, d’autre part, étudié la réalisation systématique des périphériques. C'est aussi ce deuxième défaut que nous espérons corriger dans ce chapitre
  • La mémoire programme disponible dans la dernière version de l'ATMega8 était de 16 ko, donc exactement celle qui est présente dans l'ATMega16. Il y avait donc 8 ko inutilisables avec l'option de compilation ATMega8. Nous avons l'intention de les récupérer maintenant (troisième défaut à corriger)
  • Enfin, la mémoire RAM disponible dans la dernière version de l'ATMega8 était de 4 ko. Celle qui est disponible dans un ATMega16 est de 1 ko et celle de l'ATMega32 est de 2 ko. Les options de compilations pour ATMega32 nous permettrons donc de récupérer 1 ko facilement. Mais les 2 ko restants seront à gérer à la main avec des pointeurs si l’on désire les utiliser.
Panneau d’avertissement L'hébergement de mon site perso se termine le 5 septembre 2023 ! A ce jour je n'ai pas encore décidé comment je vais gérer ce problème dont je ne suis pas à l'origine. Il en résulte que l'ensemble des corrections qui utilisent mon site perso seront indisponibles à partir de cette date pour tout ce chapitre. SergeMoutou (discuter)

Passage de l'ATMega8 vers l'ATMega16[modifier | modifier le wikicode]

Comme nous l'avons déjà exprimé en introduction, tout ce qui concerne la gestion des mémoires programme et mémoires RAM est déjà réalisé. La seule grande évolution dans l'ATMega16 est l'apparition d'une instruction de saut nouvelle dans la table de vecteur d'interruptions. Nous allons donc examiner maintenant ce problème. Côté périphérique il y a des différences mais nous n'en avons pas (ou peu) dans notre processeur (des périphériques). C'est à nous de les imaginer.

Changer la gestion des vecteurs d'interruptions[modifier | modifier le wikicode]

Nous avons déjà évoqué ce problème dans la section Ajoutons une interruption du précédent chapitre.

Pour faire simple il vous faut donc regarder dans le fichier opc_deco.vhd autour de la ligne 99, et avoir :

    Q_JADR <= "0000000000" & I_OPC(4 downto 0) & "0";

au lieu de

    Q_JADR <= "00000000000" & I_OPC(4 downto 0);

pour notre ancien ATMega8.

Faites cela et vous avez un ATMega16 disponible. Cette simplicité de transformation est liée au fait que la seule nouvelle instruction dans l'ATMega16 (à part BREAK comme visible dans cette annexe d'un autre livre) est "JMP". Notre ATMega8 disposait déjà de l'instruction "jmp" et des 16 ko de mémoire programme.

Changer les options de compilation[modifier | modifier le wikicode]

La compilation a toujours été réalisée avec l'option "-mmcu=atmega8" dans le chapitre précédent. Il suffit de compiler maintenant avec l'option "-mmcu=atmega16".

Voici un moyen automatique de compiler sous Linux :

#!/bin/bash
#export PATH=$PATH:/usr/local/avr/bin:~/XILINX/Xilinx/11.1/ISE/bin/lin/
export PATH=$PATH:/usr/bin:/opt/Xilinx/14.5/ISE_DS/ISE/bin/lin64
avr-gcc -g -mmcu=atmega16 -Wall -Os -c your_program.c 
avr-gcc -g -mmcu=atmega16 -o your_program.elf -Wl,-Map,your_program.map your_program.o 
avr-objdump -h -S your_program.elf > your_program.lss
#avr-objcopy -O binary -R .eeprom cordic.elf cordic.bin
#avr-objcopy -R .eeprom -O ihex hello.elf hello.hex
data2mem -bm memoryS6.bmm -bd your_program.elf -bt atmega16.bit -o uh atmega16
# commentez ligne suivante si pas ADEPT installe
djtgcfg prog -d Nexys3 --index 0 --file atmega16_rp.bit

Quelques précisions sur ce script :

  • vous devez adapter le PATH à votre installation de l'ISE et de votre compilateur C pour l'AVR.
  • memoryS6.bmm est lié au fait que l’on utilise maintenant une carte avec un spartan6 (lire documentation data2mem dans ce livre)
  • la dernière ligne sert à programmer la carte Nexys3 en ligne de commande avec l'outil ADEPT de Digilent

Ressource disponible[modifier | modifier le wikicode]

Nous mettons à disposition un fichier contenant l’ensemble de l'ATMega16. Il a été testé sur une carte digilent Nexys 3 possédant un FPGA spartan 6 avec une horloge à 100 MHz. Nous avons gardé la division par deux des versions précédentes : il fonctionne donc à 50 MHz. Vous pouvez le faire fonctionner juste en changeant le fichier ".ucf" sur les cartes spartan3, spartan3E et Basys2 mais à 25 MHz seulement. Gardez alors les contraintes de localisation des mémoires et le fichier memory.bmm que l’on trouve dans le chapitre Programmer in situ ...

  • Téléchargez la ressource ATMega16. Les modifications futures concerneront le fichier "io.vhd". Nous mettrons dans ce fichier zip diverses versions de ce fichier "io.vhd". Si vous voulez réaliser un ATMega16 fonctionnel il faut en choisir un seul parmi ceux que vous trouverez.
    • Pour le moment il y a un fichier "io_timer.vhd" qui implante un pseudo-timer (voir plus bas dans ce chapitre) avec une interruption de débordement.

Comment donner un nom à un registre de périphérique[modifier | modifier le wikicode]

Pour comprendre ce qui va suivre, rappelez-vous que notre processeur n'a que très peu de périphérique à comparer à l'ATMega16 du commerce. Cela veut dire qu'une bonne partie des registres utilisés pour configurer et utiliser ces périphériques est libre. Ainsi deux solutions s'offrent à vous lorsque vous réalisez des périphériques :

  1. vous êtes conservateur (ce que nous avons été jusqu'ici) et vous gardez les noms des registres originaux de l'ATMega16.
  2. vous êtes révolutionnaire et vous ne voyez aucune raison pour garder les noms des registres originaux.

Examinons donc les tâches à accomplir dans les deux cas.

Garder les noms de registres de l'ATMega16[modifier | modifier le wikicode]

Dans ce cas il vous faudra lire les documentations officielles de l'ATMega16 pour connaître le nom et les adresses des registres. Alors vous pourrez automatiquement utiliser ce nom en langage C (ou autre). C'est un avantage mais vous vous trouvez alors dans la situation où vous serez amené à écrire du code C peu lisible. Voici un exemple provenant du projet pacman (et ATMega8) :

//**********************************************************************************************************
// function setenemyXY()
// purpose: put the enemy with x and y coordinates
// arguments:
// corresponding x and y coordinates
// return:
// note:
//**********************************************************************************************************
void setenemyXY(uint8_t x,unsigned char y){
// voir fichier io2.vhd pour comprendre 
  DDRA=x; 
  DDRC=y;
}

Et la question subsidiaire : quel est le rapport entre DDRA et DDRC et le pacman ? Notez qu'en plus DDRA ne fait pas partie des PORTs de l'ATMega8 du commerce.

En résumé, il nous a fallu lire la documentation officielle du processeur pour connaître nom et adresses des registres. Aucun changement dans les programmes C mais un code peu lisible pour un lecteur non averti.

Changer les noms de registre de l'ATMega16[modifier | modifier le wikicode]

L'autre technique consiste à ne pas garder le nom des registres. Le problème est que maintenant le compilateur C ne connaîtra pas vos noms. L'avantage est que cela vous évite de lire la documentation du processeur et l'inconvénient que cela vous oblige à gérer à la main vos noms de registres. Ceci est réalisé de manière simple :

/* Port A */
// Les ports ci-dessous n'existent pas dans l'ATMega16 de la vraie vie mais nous les avons ajouté dans notre cœur
// d'où leur présence ici pour ne pas modifier les fichiers d'entête !
//#define PINA _SFR_IO8(0x19)
#define DDRA _SFR_IO8(0x1A)
#define PORTA _SFR_IO8(0x1B)
//#define PORTA _SFR_MEM8(0x3B)

qui montre comment ont été ajoutés des registres inexistants dans ce cas, à savoir, DDRA et PORTA.

Des constantes pour simplifier l'écriture des adresses des registres[modifier | modifier le wikicode]

Nous n'avons pas utilisé la technique des constantes jusqu'à présent. Il est possible dans les "case" des process "io_rd" et "io_wr" d’utiliser directement les adresses des registres définies par leur nom si elles sont déclarées comme constantes comme ci-dessous.

-- constantes pour egistres des périphériques de l'ATMega16
constant TWBR : std_logic_vector(7 downto 0) := X"20";
constant TWSR : std_logic_vector(7 downto 0) := X"21";
constant TWAR : std_logic_vector(7 downto 0) := X"22";
constant TWDR : std_logic_vector(7 downto 0) := X"23";
constant ADCL : std_logic_vector(7 downto 0) := X"24";
constant ADCH : std_logic_vector(7 downto 0) := X"25";
constant UCSRB : std_logic_vector(7 downto 0) := X"2A";
constant UCSRA : std_logic_vector(7 downto 0) := X"2B";
constant UDR : std_logic_vector(7 downto 0) := X"2C";
constant PIND : std_logic_vector(7 downto 0) := X"30";
constant DDRD : std_logic_vector(7 downto 0) := X"31";
constant PORTD : std_logic_vector(7 downto 0) := X"32";
constant PINC : std_logic_vector(7 downto 0) := X"33";
constant DDRC : std_logic_vector(7 downto 0) := X"34";
constant PORTC : std_logic_vector(7 downto 0) := X"35";
constant PINB : std_logic_vector(7 downto 0) := X"36";
constant DDRB : std_logic_vector(7 downto 0) := X"37";
constant PORTB : std_logic_vector(7 downto 0) := X"38";
constant PINA : std_logic_vector(7 downto 0) := X"39";
constant DDRA : std_logic_vector(7 downto 0) := X"3A";
constant PORTA : std_logic_vector(7 downto 0) := X"3B";
constant EEDR : std_logic_vector(7 downto 0) := X"3D";
constant EEARL : std_logic_vector(7 downto 0) := X"3E";
constant EEARH : std_logic_vector(7 downto 0) := X"3F";
constant UCSRC : std_logic_vector(7 downto 0) := X"40";
constant TCNT0 : std_logic_vector(7 downto 0) := X"52";
constant TCCR0 : std_logic_vector(7 downto 0) := X"53";
constant TWCR : std_logic_vector(7 downto 0) := X"56";
constant TIMSK : std_logic_vector(7 downto 0) := X"59";
constant OCR0 : std_logic_vector(7 downto 0) := X"5C";

Voila alors à quoi pourrait ressembler un process "io_wr" :

   iowr: process(I_CLK)
    begin
        if (rising_edge(I_CLK)) then
            if (I_CLR = '1') then
                L_RX_INT_ENABLED  <= '0';
                L_TX_INT_ENABLED  <= '0';
            elsif (I_WE_IO = '1') then
                case I_ADR_IO is
                   when DDRD  => Balle_xLow <= I_DIN;  --DDRD
                   when PORTD  => Balle_xHigh <= I_DIN;  --PORTD
                   when DDRB  => Balle_yLow <= I_DIN;  --DDRB
                   when PORTB => Balle_yHigh <= I_DIN; --PORTB
                   -- when DDRC => raqD_y <= I_DIN; --DDRC
                   when PORTC => raq_x <= I_DIN; --PORTC
                   when PORTA => s_ligne1 <= I_DIN; -- PORTA
                   when DDRA => s_ligne2 <= I_DIN; -- DDRA
-- gestion écriture RAM avec registres EEPROM en fait
                   when EEDR => s_DIB <= I_DIN; -- EEDR : EEPROM Data Register
                   when EEARL => s_ADDRB <= I_DIN; --EEARL
                   when EEARH => s_ENB <= I_DIN(7);s_WEB<=I_DIN(6); --EEARH
-- gestion UART
                  when UCSRC  =>  -- handled by uart
                  when X"41"  =>  -- handled by uart
                  when X"43"  => L_RX_INT_ENABLED <= I_DIN(0);
                                 L_TX_INT_ENABLED <= I_DIN(1);
                  when others =>
                end case;
            end if;
        end if;
    end process;

où l’on voit apparaître les constantes, ce qui rend le programme un peu plus lisible. Notez que cette façon de faire impose de ne pas appeler les sorties avec ces noms. On pourra garder la notation allemande de l'auteur : préfixer par "I_" pour une entrée et par "Q_" pour une sortie.


Nos conventions sur les registres de périphérique dans ce chapitre[modifier | modifier le wikicode]

Notez avant de continuer que le nom des périphériques est choisi lors de la programmation en C et non lors de l'implantation matérielle. Ce qui est choisi lors de l'implantation matérielle est une adresse libre. Ce qui est choisi en C c’est comment vous appelez cette adresse.

Nous avons décidé de garder partiellement la première méthode et d'évoluer vers la deuxième.

Des bonnes résolutions pas tenues[modifier | modifier le wikicode]

Nous avons eu l’occasion de développer un tas de périphériques depuis l'écriture du principe de la section précédente... et n'avons pas tenu ces promesses de clarification. Nous allons donc chercher à donner des conseils aux utilisateurs mieux organisés que nous...

Bon, reprenons dans le désordre. Ce que nous faisons dans ce livre s’appelle du co-design en anglais. Il s'agit donc de développer du matériel et de le programmer par du logiciel.

En général les concepteurs hardware et software ne sont pas les mêmes. Ce qui est fait ici est donc exceptionnel (dans le sens pas très général). Regardez par exemple le chapitre sur le microBlaze si vous voulez comprendre. Dans ce processeur il n'y a pas de registres de périphériques, mais seulement un espace d'adressage pour les mettre. C'est ainsi que cela se passe toujours si vous prenez un processeur conçu par les fondeurs (microBlaze pour Xilinx, NIOS pour Altera). Mais pour faire cela, Xilinx, Altera et d'autres ont inventé un mécanisme automatique (ou semi-automatique) pour lier le matériel et le logiciel.

Il est cependant possible, pour vous aussi, de considérer l'espace des registres de périphériques comme un espace vierge à remplir par vos propres périphériques. Même pour l'ATMega16 que l'on utilise. Comme le concepteur du matériel et du logiciel est le même vous avez le choix entre plusieurs options :

  • adapter les noms de registres à vos applications et donner le même nom aux deux
    • en C cela se fait en changeant le fichier iom16.h, ce que l'on déconseille ou en redéfinissant vos propres noms avec des "_SFR_IO8" dans le programme principal ou dans un fichier d'entête spécifique ce que l'on conseille.
    • en VHDL cela se fait avec des définitions des constantes comme dans la précédente section
  • garder les noms des registres que le compilateur C connaît et les utiliser aussi en VHDL
  • garder les noms pour le C et prendre les adresses (et non des noms) pour les registres

Le dernier point est ce que l'on a utilisé au chapitre précédent, en tant que débutant, et ne nous a pas gêné. Il n'empêche que nous le déconseillons pour l'enseignement.

Le deuxième est plutôt ce que l'on utilise de manière classique maintenant.

Le premier point est pour nous l'idéal pédagogique mais demande un peu plus d'organisation (que nous en avons nous-même).

Réalisation des périphériques[modifier | modifier le wikicode]

Un certain nombre de périphériques a déjà été implémenté dans le chapitre précédent. Nous allons maintenant les examiner de façon plus systématique et résoudre ainsi tous les problèmes que vous pourrez rencontrer dans votre vie d'architecte en périphérique.

Comment interfacer un registre en écriture ?[modifier | modifier le wikicode]

Définir des PORTs en sortie se fait dans le process iowr du fichier io.vhd dont voici la version originale :

iowr: process(I_CLK) 
    begin 
      if (rising_edge(I_CLK)) then 
        if (I_CLR = '1') then 
                L_RX_INT_ENABLED  <= '0'; 
                L_TX_INT_ENABLED  <= '0'; 
        elsif (I_WE_IO = '1') then 
          case I_ADR_IO is 
            when X"38"  => -- PORTB 
                        Q_PORTB <= I_DIN; 
            when X"35" => -- PORTC 
		 	Q_PORTC <= I_DIN; 
            when X"32" => -- PORTD 
                        Q_PORTD <= I_DIN;	
            when X"2A"  => -- UCSRB 
                         L_RX_INT_ENABLED <= I_DIN(7); 
                         L_TX_INT_ENABLED <= I_DIN(6); 
            when X"2B"  => -- UCSRA:       handled by uart 
            when X"2C"  => -- UDR:         handled by uart 
            when X"40"  => -- UCSRC/UBRRH: (ignored) 
            when others => 
          end case; 
        end if; 
      end if; 
    end process;

Vous voyez apparaître une correspondance entre des PORTs (en commentaire) et des numéros. Le nom des PORTs et les numéros correspondants proviennent de la documentation officielle du processeur. Comme déjà expliqué, on n’est pas obligé de le respecter mais le faire évite de changer le fichier avr/io.h connu du compilateur.

Vous constatez donc que le cœur de départ gère un certain nombre de PORTs : PORTB, PORTC, PORTD et d'autres registres qui ne nous intéressent pas pour le moment. Nous présentons l’ensemble de ce qui vient d’être dit sur un schéma ci-dessous. Tous les registres et PORTs ne sont pas dessinés et seul le processus "iowr" est montré . La terminologie utilisée dans les micro-contrôleurs sépare la notion de PORT et la notion de registre. Un port est un registre dont les valeurs sortent vers l'extérieur, sur des broches. Ce n’est pas le cas des registres. Nous ne suivrons pas cette terminologie ici de manière rigoureuse.

Panneau d’avertissement

Pour information I_CLK est relié à I_CLK_100 divisée par deux. Nous avons en effet rebaptisé l'horloge car nous utilisons maintenant une carte spartan6 disposant de 100 MHz. Pour les cartes avec horloge à 50 MHz vous pouvez rebaptiser l'horloge ou pas.

Les dessins resteront avec l'horloge à 50 MHz, comme celui qui est ci-dessous. Une autre façon de dire les choses est que chaque fois que vous voyez "I_CLK_50" dans un dessin cela désigne l'horloge du processeur (qui peut donc être à 100 MHz)

Nous présentons maintenant un schéma où seul le processus "iowr: process(I_CLK)" permettant une sortie dans un PORT ou registre est dessiné : son objectif est de dispatcher "I_DIN[7:0]" dans les différents registres en fonction de la valeur de "I_ADR_IO".

Comment écrire dans un registre/port ?

Pour revenir un peu sur la façon utilisée pour nommer les registres, notez ici la présence des noms de PORTs. En effet ceux-ci sont directement reliés aux sorties "Q_PORTB", "Q_PORTC" et "Q_PORTD" du processeur sans aucun traitement préalable. Il est donc normal de les appeler PORT.

Comment interfacer un registre en lecture ?[modifier | modifier le wikicode]

Les PORTs et registres en entrée se gèrent dans le process iord :

iord: process(I_ADR_IO, I_PINB, 
             U_RX_DATA, U_RX_READY, L_RX_INT_ENABLED, 
             U_TX_BUSY, L_TX_INT_ENABLED) 
    begin 
 -- addresses for mega16 device (use iom16.h or #define __AVR_ATmega16__). 
 -- 
      case I_ADR_IO is 
        when X"2A"  => Q_DOUT <=             -- UCSRB: 
                         L_RX_INT_ENABLED  -- Rx complete int enabled. 
                       & L_TX_INT_ENABLED  -- Tx complete int enabled. 
                       & L_TX_INT_ENABLED  -- Tx empty int enabled. 
                       & '1'               -- Rx enabled 
                       & '1'               -- Tx enabled 
                       & '0'               -- 8 bits/char 
                       & '0'               -- Rx bit 8 
                       & '0';              -- Tx bit 8 
         when X"2B"  => Q_DOUT <=             -- UCSRA: 
                          U_RX_READY       -- Rx complete 
                        & not U_TX_BUSY    -- Tx complete 
                        & not U_TX_BUSY    -- Tx ready 
                        & '0'              -- frame error 
                        & '0'              -- data overrun 
                        & '0'              -- parity error 
                        & '0'              -- double dpeed 
                        & '0';             -- multiproc mode 
         when X"2C"  => Q_DOUT <= U_RX_DATA; -- UDR 
         when X"40"  => Q_DOUT <=            -- UCSRC 
                          '1'              -- URSEL 
                        & '0'              -- asynchronous 
                        & "00"             -- no parity 
                        & '1'              -- two stop bits 
                        & "11"             -- 8 bits/char 
                        & '0';             -- rising clock edge 

         when X"36"  => Q_DOUT <= I_PINB;  -- PINB 
         when others => Q_DOUT <= X"AA"; 
       end case; 
    end process;

Vous pouvez voir que certains registres sont implantés bit par bit et d'autres en entier.

Nous avons présenté sous forme de programme VHDL le process "iord" mais nous allons en faire un schéma de principe. Comme d'habitude, prenez du temps devant ce schéma pour bien comprendre ce que l’on cherche à présenter.

Comment lire un registre/port ?

Son principe est de former le signal "Q_DOUT[7:0]" à partir d'une adresse "I_ADR_IO[7:0]" et de données provenant de l'extérieur ou pas. Il est possible que vous soyez surpris par le sens de "Q_DOUT[7:0]" comme sortie du processus. Mais cette sortie devient une entrée du processeur, non ?


Comment interfacer un registre à la fois en lecture et écriture ?[modifier | modifier le wikicode]

Nous cherchons maintenant à implémenter des périphériques classiques, nous voulons dire du genre de ceux que l’on trouve habituellement dans un vrai processeur. Prenons le cas du timer0 dans un ATMega16. Il nous donne accès à un registre appelé TCNT0 qui peut fonctionner tout seul quand le timer0 est en route, mais dans lequel on peut aussi écrire, ou lire. Comment faire cohabiter donc toutes ces possibilités ?

Nous ferons probablement plus tard un exemple avec une entité et une architecture, mais pour le moment vous pouvez lire la section Réaliser un périphérique avec interruption pour vous en faire une idée.

Une bonne idée, par exemple, est d'interfacer les PORTs en sortie et en entrée. Nous ne l'avons jamais fait mais cela permettrait par exemple d'écrire :

// incrémentation du PORTB
  PORTB++;

On rappelle en effet que PORTB++ est équivalent à PORTB = PORTB+1

Le fait que PORTB se trouve à droite de l'affectation veut dire qu’il faut le considérer comme une entrée. Le fait qu’il apparaisse à gauche veut dire qu’il est considéré comme une sortie.

La remarque précédente impose donc le principe suivant :

Début d’un principe
Fin du principe


Comment interfacer un FIFO en écriture ?[modifier | modifier le wikicode]

Nous donnerons un exemple simple plus tard. Pour le moment, notez que ceci a déjà été fait dans un autre projet en deux étapes :

  • connecter l'entrée du FIFO à I_DIN
  • réaliser le signal d'écriture du FIFO : L_WE_UART2 <= I_WE_IO when (I_ADR_IO = X"24") else '0'; -- write UART2 ADCL

Le résultat présenté ici peut être généralisé à tout registre. Nous voulons parler d'un périphérique qui a déjà son registre d'entrée.

Comment interfacer un FIFO en lecture ?[modifier | modifier le wikicode]

Puisque nous nous intéressons à un FIFO en lecture, cela laisse entendre que c’est un périphérique (un bout de VHDL en tout cas) qui viendra écrire dedans. Mais notre processeur qui viendra le lire. Ceci n’est pas sans conséquence :

Dans cette section nous allons nous intéresser seulement à l'interface entre le FIFO et le processeur. Nous laissons tomber les explications sur le "done" et aurons certainement l’occasion de revenir dessus lors d'une implantation pratique.

Quelques informations supplémentaires sur le VHDL correspondant :

-- utilisé dans le registre PINC en lecture 
-- b7 b6 b5 
signal s_data_present, s_buffer_full, s_half_full : std_logic;

Les signaux ci-dessus sont utilisés dans le process de lecture :

iord: process(I_ADR_IO, I_SWITCH, 
              U_RX_DATA, U_RX_READY, L_RX_INT_ENABLED, 
              U_TX_BUSY, L_TX_INT_ENABLED) 
    begin 
 -- addresses for mega16 device (use iom16.h or #define __AVR_ATmega16__). 
 -- 
      case I_ADR_IO is 
        when X"33"  => Q_DOUT <=           -- PINC: 
                         s_data_present    -- data present ? 
                       & s_buffer_full     -- buffer plein ? 
                       & s_half_full       -- buffer à moitié plein ?
                       & '0'               -- non utilisé 
                       & '0'               -- non utilisé 
                       & '0'               -- non utilisé 
                       & '0'               -- non utilisé 
                       & '0';              -- non utilisé 
        when X"30"  => Q_DOUT <= PIND_DATA; -- PIND:

Nous n'avons pas mis l’ensemble du process ici mais seulement ce qu’il y a de nouveau pour cet exemple.


Il y a un autre point sur lequel il nous faut revenir : la construction du signal read qui va au FIFO. Il se fait avec le simple code :

--regardez la figure un peu plus loin pour comprendre
L_RD_FIFO <= I_RD_IO when (I_ADR_IO = X"30") else '0'; -- read PIND

Avec ce signal, toute lecture de PIND en C provoquera simultanément une évacuation de la donnée du FIFO : c’est un décalage dans le FIFO grâce à ce signal qui permet de récupérer la donnée suivante la prochaine fois qu'on viendra le lire.

Un FIFO interfacé en lecture

La figure montre qu’il faudra aussi câbler le FIFO, ce qui n’est pas montré dans nos extraits de code VHDL. Mais il faut bien laisser un peu de travail à nos lecteurs.

Pour information, nous avons eu l’occasion d’utiliser cette technique avec un module le lecture de clavier PS/2 qui remplissait le FIFO au fur et à mesure. Cette technique est utilisée aussi dans la gestion de l'accéléromètre de la manette Nunchuk du projet pacman plus bas.

Comment interfacer un compteur 32 bits ?[modifier | modifier le wikicode]

Le titre de cette section n'est probablement pas assez explicite. Soyons donc plus concret. Imaginons que nous devons compter des événements qui peuvent être nombreux et nécessiter donc 32 bits. Puisque nous avons un processeur 8 bits, il nous faudrait en principe 4 ports en sortie et 4 ports en entrées pour gérer ces 32 bits en entrée et sortie. Supposons que nous n'ayons absolument pas besoin de lire les 32 bits car une partie d'affichage (ou autre) s'en occupe. La question est alors : est-il possible par une simple écriture dans un PORT (8 bits) d'incrémenter un compteur 32 bits ? Voila la question à laquelle nous désirons nous atteler maintenant.

La technique consiste à utiliser un compteur avec un signal de validation "en" et de construire ce "en" comme on l'a fait pour le signal "read" de la FIFO.

Un exemple pour comprendre[modifier | modifier le wikicode]

Pour être concret, voici un exemple qui a été donné comme examen final à des étudiants. Il est sur 16 bits mais montre bien les problèmes à résoudre pour y parvenir. Voici le compteur proprement dit :

-- Serge Moutou juin 2013
-- fichier cmptAngle.vhd
library IEEE;
use IEEE.std_logic_1164.all;
use IEEE.std_logic_arith.all;
use IEEE.std_logic_unsigned.all;
ENTITY cmpt_Angle is 
  PORT( 
    clk,reset,en : IN std_logic;
    inc_dec_i : IN std_logic_vector(7 DOWNTO 0);
    -- 3243 < angle_o < CDBD
    angle_o : OUT std_logic_vector(15 downto 0));
END cmpt_Angle;
ARCHITECTURE Behavioural OF cmpt_Angle IS
-- =1 si incrementation =0 si decrementation
  signal increment : std_logic; 
  signal s_angle : std_logic_vector(15 downto 0);
BEGIN
  PROCESS(clk,reset) BEGIN
    IF reset='1' THEN
      s_angle <=MAX;
      increment <= '1';
      ELSIF rising_edge(clk) THEN
        IF en = '1' THEN
          IF increment = '1' THEN
            s_Angle <= s_Angle + inc_dec_i;
            IF s_Angle >= MIN and s_Angle(15)='0' THEN 
               s_angle <= MIN;
               increment <= '0';
            END IF;
          ELSE
            s_Angle <= s_Angle - inc_dec_i;
	 IF s_Angle <= MAX and s_Angle(15)='1' THEN 
               s_angle <= MAX;				
               increment <= '1';
            END IF;
          END IF;
      END IF;
    END IF;
  END PROCESS;
  angle_o <= s_angle;
END Behavioural;

Seule l'entité a un intérêt et le signal s_angle qui est sur 16 bits. Le compteur est un peu spécial car destiné à un cœur CORDIC.

Le câblage de ce composant se fait dans io.vhd par :

	cmptAngle: cmpt_Angle PORT MAP( 
               clk => I_CLK,
               reset => I_CLR,
               en => s_en,
               inc_dec_i => s_PORTA,
               angle_o(15 downto 8) => Angle_MSB,
               angle_o(7 downto 0) => Angle_LSB);

Ce code laisse entendre qu’il se passera quelque chose par une écriture dans PORTA ! Une confirmation ?

iowr: process(I_CLK)
    begin
        if (rising_edge(I_CLK)) then
            if (I_CLR = '1') then
                L_RX_INT_ENABLED  <= '0';
                L_TX_INT_ENABLED  <= '0';
            elsif (I_WE_IO = '1') then
                case I_ADR_IO is
                    when X"3B"  => -- PORTA
                       s_PORTA <= I_DIN;
                    when X"38"  => -- PORTB
                       s_PORTB <= I_DIN;
-- plusieurs lignes sont omises
end process;
    s_en <= I_WE_IO when (I_ADR_IO = X"3B") else '0'; -- write PORTA

De tout cet ensemble d'extraits de code, il est possible de déduire qu'une écriture d'une valeur X dans PORTA incrémentera le compteur 16 bits de X.

Comment interfacer une mémoire RAM en la projetant dans l'espace mémoire ?[modifier | modifier le wikicode]

Ce problème a déjà été traité dans ce livre lors de l'apparition du projet pacman. Mais nous l'avons alors traité d'une manière qui mérite d’être expliquée de nouveau.

Comment interfacer une mémoire RAM avec des PORTs ?[modifier | modifier le wikicode]

Au lieu de projeter la RAM directement dans la RAM de l'AVR, il est possible de passer par des Registres/PORTs pour ce travail. Pourquoi ne pas reprendre, par exemple, les registres de l'EEPROM ? Cette technique est cependant plus lente mais probablement plus facile à mettre en œuvre. Il nous a suffit de rajouter un PORT à un BRAM et de la connecter à nos PORTs.

Voici un exemple de programme associé :

//**************************************************************************************************************************
// function putLevel()
// purpose: put level in the screen by writing in ROM
// arguments:
// corresponding level
// return:
// note: WEB est en b6 et ENB en b7 tandis que SSRB est forcé matériellement à 0
//**************************************************************************************************************************
void putLevel(uint8_t level){
  uint8_t Data;
  Data = level & 0x0F;
  EEARH = 0;
// voici l'adresse où l’on écrit
  EEARL = 102;
// voici la donnée que l’on écrit
  EEDR = Data + '0';
// voici la commande d'écriture
  EEARH = 0xC0;
  EEARH = 0;
  Data = level & 0xF0;
  Data >>= 4;
  EEARL = 101;
  EEDR = Data + '0';
  EEARH = 0xC0;
  EEARH = 0;
}

Les fins connaisseurs de l'écriture en EEPROM auront remarqué que l’on n'a pas respecté la façon dont cela se passe avec un ATMega. Nous avons utilisé EEARH comme registre de commande alors qu'en principe c’est le registre d'adresse haute ! Cette façon de faire présente un inconvénient : il ne nous est pas possible de déclarer directement des variables dans cette mémoire alors que cela est possible avec un vrai AVR. Voici par exemple un code trouvé dans la documentation officielle de l'ATMega16 où l’on voit que le registre de commande est EECR et que 2 bits sont concernés EEWE et EEMWE.

void EEPROM_write(unsigned int uiAddress, unsigned char ucData)
{
/* Wait for completion of previous write */
  while(EECR & (1<<EEWE));
/* Set up address and data registers */
  EEAR = uiAddress;
  EEDR = ucData;
/* Write logical one to EEMWE */
  EECR |= (1<<EEMWE);
/* Start eeprom write by setting EEWE */
  EECR |= (1<<EEWE);
}

Nous nous intéresserons à ce problème un peu plus tard. Les changements sont mineurs :

  • changer de registre de commande (une adresse à changer)
  • trouver un mécanisme pour que le bit EEWE revienne à 0 tout seul en une période d'horloge
  • tester : c’est ce qui prend le plus de temps

Voir aussi : Examen final LO11 2015 qui utilise aussi une mémoire RAM accessible par des PORTs sur une architecture ATTiny861 décrite dans la partie TP de ce livre.

Pourquoi ne pas vouloir refaire les périphériques de l'ATMega16 original ?[modifier | modifier le wikicode]

Tout le monde s'accorde pour dire que l’intérêt d'enfouir un processeur dans un FPGA est tout simplement que ses périphériques peuvent être taillés sur mesure dans la partie restante du FPGA.

Quelques exemples peut être ?

  1. L'ATMega16 que nous proposons dispose d'une liaison série. Elle n’est pas complète puisqu'elle ne gère qu'une vitesse fixe. Toute instruction visant à changer cette vitesse échouera donc. Nous n'avons pas encore rencontré de situation où cela présentait un problème sérieux !
  2. La liaison i2c est gérée par l'ATMega16 avec un ensemble de registres. Nous avions comme objectif, de copier cette gestion pour gérer la manette Nunchuk (année scolaire 2012/2013) mais avons fini par abandonner cette idée par souci de simplification. Bien sûr cela aura comme conséquence qu'une gestion d'un accéléromètre i2c, par exemple, nécessitera de tout refaire... mais la simplification en valait la peine. Pour information un périphérique i2c plus générique a été réalisé en janvier 2017 un peu plus loin dans ce chapitre.
  3. Le problème des timer mérite probablement plus d'attention. En effet ces derniers peuvent être utilisés dans les systèmes multitâches. Ne pas les implémenter comme dans le processeur original nécessite donc de lire le code source du noyau multitâche que vous désirez utiliser. Dans ce cas précis, ce qui est gagné sur le matériel (simplification par la description d'un timer maison) est perdu sur le logiciel (nécessité de vérifier que le timer maison est compatible avec le logiciel). Les compétences techniques de l'équipe chargée du projet seront donc capitales pour choisir entre les deux options.

Réaliser un périphérique avec interruption[modifier | modifier le wikicode]

Le problème des interruptions a été évoqué ci-dessus et dans le chapitre sur l'ATMega8. Nous n'avons cependant jamais réalisé de périphérique qui réalise une telle interruption. C'est donc à cette tâche que nous allons nous atteler maintenant.

Essai d'interruption[modifier | modifier le wikicode]

Dans le chapitre précédent nous avons déjà donné un programme qui utilise une interruption en réception rs232. Il vous faut commencer par essayer de le faire fonctionner. Le voici donc rappelé :

#include <avr/io.h>
#include <avr/interrupt.h>
#undef F_CPU
#define F_CPU 100000000UL
#include "util/delay.h"
// interruption de réception
ISR(USART_RXC_vect) // ISR(_VECTOR(11))
{
        PORTC = UDR;
}
 
int main(int argc, char * argv[])
{
    UCSRB = (1<<RXEN)|(1<<RXCIE); // pour pouvoir déclencher interruption RS232
    sei(); // autorise interruption générale
    for (;;);
}

Lorsqu'un hyperterminal est connecté à 38400 bauds, 8 bits de données, sans parité et deux bits de stop, vous devez voir clignoter les leds au gré de vos envois par le clavier. Nous l'avons fait fonctionner à 100 MHz dans la carte Nexys 3 (architecturée autour d'un spartan6) sans problème.

Si ce programme fonctionne c’est parce que Juergen Sauermann, l'auteur du projet, a livré un exemple d'implantation matérielle d'interruption avec la rs232. Il nous faut donc lire ce code et l'adapter à nos besoins.

Un pseudo-timer pour réaliser une interruption[modifier | modifier le wikicode]

Nous avons décidé, comme premier exemple, de réaliser une interruption par débordement d'un compteur/timer. Nous avons appelé cette section pseudo-timer car nous allons implanter un timer mais de manière très simplifiée (avec une pré-division fixe par exemple).

Le timer proprement dit[modifier | modifier le wikicode]

L'idée générale est de faire un compteur sur 20 bits mais avec lequel une écriture provoquera la seule mise à jour des 8 bits de poids fort.

Il suffit d’abord d'ajouter un process de gestion du timer

timer0: process(I_CLK) begin
  if (rising_edge(I_CLK)) then
    if (I_ADR_IO = X"52" and I_WE_IO = '1') then -- écriture dans TCNT0
      pseudotimer(19 downto 12) <= I_DIN;
      L_TOV0 <= '0'; --flag remis a 0 pendant écriture
    else
      pseudotimer <= pseudotimer + 1;
      if pseudotimer = x"FFFFF" then 
        L_TOV0 <= '1'; -- timer 0 overflow
      end if;
    end if;
  end if;
end process;

où pseudotimer est un signal 20 bits à déclarer dont seuls les bits de poids forts nous intéressent vraiment. Le reste est là pour réaliser une division de l'horloge qui fonctionne à 50 MHz. Comme on peut le voir la mise à 0 de l'overflow se fait par une écriture dans le timer. Pour votre information, ce n’est pas comme cela que fonctionne l' ATMega16. Et ce dernier offre la possibilité supplémentaire de mise à 0 de TOV0 par écriture directe dans le registre TIFR. Nous n'avons pas réalisé cette deuxième fonctionnalité par souci de simplification.

Autorisation de l'interruption timer[modifier | modifier le wikicode]

C'est un bit appelé TOIE0 du registre TIMSK (adresse 0x59) qui gère cette autorisation. Si l’on veut le gérer correctement il nous faut donc ajouter une ligne au process "iowr" comme ceci :

iowr: process(I_CLK)
    begin
        if (rising_edge(I_CLK)) then
            if (I_CLR = '1') then
                L_RX_INT_ENABLED  <= '0';
                L_TX_INT_ENABLED  <= '0';
            elsif (I_WE_IO = '1') then
            case I_ADR_IO is
              when X"38"  => -- PORTB
                     Q_7_SEGMENT <= I_DIN(6 downto 0);
              when X"35" => -- PORTC
	 Q_LEDS <= I_DIN;
	 when X"32" => -- PORTD
	 Q_AN <= I_DIN(3 downto 0);								 
              when X"2A"  => -- UCSRB
                     L_RX_INT_ENABLED <= I_DIN(7);
                     L_TX_INT_ENABLED <= I_DIN(6);
              when X"2B"  => -- UCSRA:       handled by uart
              when X"2C"  => -- UDR:         handled by uart
              when X"40"  => -- UCSRC/UBRRH: (ignored)
              when X"59" => -- TIMSK ajouté pour nos besoins
                     L_TOIE0 <= I_DIN(0);		 
              when others =>
            end case;
          end if;
        end if;
end process;

Voila, nous sommes prêt, il ne reste plus qu’à modifier le process "ioint".


Une lecture possible du timer[modifier | modifier le wikicode]

Il est possible de lire la valeur des 8 bits de poids forts de TCNT0 avec le process de lecture :

-- IO read process
--
iord: process(I_ADR_IO, I_SWITCH,
              U_RX_DATA, U_RX_READY, L_RX_INT_ENABLED,
              U_TX_BUSY, L_TX_INT_ENABLED)
begin
  -- addresses for mega8 device (use iom8.h or #define __AVR_ATmega8__).
  --
  case I_ADR_IO is
     when X"2A"  => Q_DOUT <=             -- UCSRB:
                               L_RX_INT_ENABLED  -- Rx complete int enabled.
                             & L_TX_INT_ENABLED  -- Tx complete int enabled.
                             & L_TX_INT_ENABLED  -- Tx empty int enabled.
                             & '1'               -- Rx enabled
                             & '1'               -- Tx enabled
                             & '0'               -- 8 bits/char
                             & '0'               -- Rx bit 8
                             & '0';              -- Tx bit 8
     when X"2B"  => Q_DOUT <=             -- UCSRA:
                               U_RX_READY       -- Rx complete
                             & not U_TX_BUSY    -- Tx complete
                             & not U_TX_BUSY    -- Tx ready
                             & '0'              -- frame error
                             & '0'              -- data overrun
                             & '0'              -- parity error
                             & '0'              -- double dpeed
                             & '0';             -- multiproc mode
     when X"2C"  => Q_DOUT <= U_RX_DATA; -- UDR
     when X"40"  => Q_DOUT <=            -- UCSRC
                               '1'              -- URSEL
                             & '0'              -- asynchronous
                             & "00"             -- no parity
                             & '1'              -- two stop bits
                             & "11"             -- 8 bits/char
                             & '0';             -- rising clock edge

     when X"36"  => Q_DOUT <= I_SWITCH;  -- PINB
     when X"52"  => Q_DOUT <= pseudotimer(19 downto 12); --TCNT0
     when others => Q_DOUT <= X"AA";
   end case;
end process;

Process d'interruption[modifier | modifier le wikicode]

Avant toute chose, il faut connaître le numéro du vecteur d'interruption que nous tentons de réaliser. Cela peut se trouver dans la documentation officielle du micro-contrôleur ou dans le fichier "iom16.h" (/usr/lib/avr/include/avr/iom16.h avec un Linux Ubuntu) du compilateur avr-gcc. On touvre dans ce fichier (parmi bien d'autres choses) :

/* Timer/Counter0 Overflow */
#define TIMER0_OVF_vect_num		9
#define TIMER0_OVF_vect			_VECTOR(9)
#define SIG_OVERFLOW0			_VECTOR(9)

qui nous montre que le vecteur que l’on tente de réaliser porte le numéro 9.

Voici donc comment on a modifié le process "ioint" (intéressez-vous à ce qui concerne le vecteur 9) :

ioint: process(I_CLK)
begin
  if (rising_edge(I_CLK)) then
    if (I_CLR = '1') then
      L_INTVEC <= "000000";
    else
      case L_INTVEC is
	 -- vector 9
        when "101001" =>
	 if (L_TOIE0 and L_TOV0) = '0' then
	 L_INTVEC <= "000000";
           end if;	
             -- vector 11 ??
        when "101011" => -- vector 11 interrupt pending.
           if (L_RX_INT_ENABLED and U_RX_READY) = '0' then
             L_INTVEC <= "000000";
           end if;
             -- vector 12 ??
        when "101100" => -- vector 12 interrupt pending.
           if (L_TX_INT_ENABLED and not U_TX_BUSY) = '0' then
             L_INTVEC <= "000000";
           end if;
        when others   =>
                        -- no interrupt is pending.
                        -- We accept a new interrupt.
                        --
           if    (L_RX_INT_ENABLED and U_RX_READY) = '1' then
                            L_INTVEC <= "101011";            -- _VECTOR(11)
              elsif (L_TX_INT_ENABLED and not U_TX_BUSY) = '1' then
                            L_INTVEC <= "101100";            -- _VECTOR(12)
	 elsif (L_TOIE0 and L_TOV0) = '1' then
			 L_INTVEC <= "101001"; -- _VECTOR(9)	 
              else
                           L_INTVEC <= "000000";            -- no interrupt
            end if;
        end case;
      end if;
    end if;
end process;

Ce code associé au programme c suivant fonctionne parfaitement :

#include <avr/io.h>
#include <avr/interrupt.h>
#undef F_CPU
#define F_CPU 50000000UL
/* Ce programme fonctionne avec les modifications présentées plus haut */
// interruption de débordement de timer
char deb,vPORTC;

ISR(TIMER0_OVF_vect) // ISR(_VECTOR(9))
{
    deb++;
    if (deb==100) {
       deb = 0;
       vPORTC = vPORTC ^ 0xFF;
       PORTC = vPORTC;
    }
    TCNT0 = 0; // remet TOV0 à 0 !!!!
}
 
int main(int argc, char * argv[])
{
    vPORTC = 0xFF;
    deb = 0;
    TCNT0 = 0;
    TIMSK = 0x01; // TOVIE0
    sei(); // autorise interruption générale
    for (;;);
}

Pouvez-vous calculer à quelle fréquence les LEDs s'allument et s'éteignent ?

Réponse : Si la fréquence est de 100 MHz et donc la période correspondante est de 10 ns. Le débordement du timer a lieu tous les 1024x1024x10 ns soit : 10485760 ns ce qui correspond à une fréquence de 95 Hz. Si vous regardez attentivement le code de l'interruption, vous verrez qu'elle compte de 0 à 100 et seulement à ce moment là, bascule le PORTC. Il faut donc compter 200 fois pour une période soit 2,097152000 s et donc une fréquence de 0,477 Hz.

Le fonctionnement du process ioint pour l'interruption ajoutée est dangereux et mérite d’être exploré plus en avant ! Il faut que d'une manière ou d'une autre le vecteur d'interruption disparaisse. Ceci ne peut être réalisé qu'avec TOV0. Mais ...

Ressources[modifier | modifier le wikicode]

Téléchargez la ressource ATMega16. Le fichier "io_timer.vhd" implante le pseudo-timer de cette section "Réaliser un périphérique avec interruption" avec une interruption de débordement. Rappelons que ce fichier remplacera "io.vhd" dans le projet complet (ne pas mettre les deux fichiers dans un même projet même si les deux vous sont fournis !)

Mais le fonctionnement du flag TOV0 dans cette ressource ne correspond pas à ce qui se passe dans un vrai ATMega16. Nous allons donc ajouter quelques explications et la ressource correspondante.

Correspondance avec Juergen Sauermann et nouvelle ressource[modifier | modifier le wikicode]

Pour expliquer les changements réalisés, nous allons traduire la correspondance que nous avons eu avec Juergen Sauermann, l'auteur de ce cœur.

> Le vendredi 3 juillet 2015 Juergen Sauermann nous a répondu

Je vois. La phrase correspondante dans le manuel de l'AVR est probablement celle-ci :

THE OCF0A FLAG IS AUTOMATICALLY cleared when the interrupt is executed. Alternatively, the OCF0A flag can be cleared by softwar BY WRITING A LOGICAL ONE TO ITS I/O BIT LOCATION.

Ainsi la méthode dont je parlais était la seconde (WRITING A LOGICAL ONE TO ITS I/O BIT LOCATION.) tandis que toi tu te referais à la première (THE OCF0A FLAG IS AUTOMATICALLY CLEARED WHEN THE INTERRUPT IS EXECUTED).

Cela n'a pas été inventé par AVR mais déjà connu comme acquittement (ou INT_ACK) dans les Motorola 68020 et (peut-être dans des processeurs encore plus anciens). La bonne place pour générer INT_ACK est dans OPC_DECO.VHD, mais pas dans RETI. Plutôt dans le pseudo opcode d'interruption. Et cela parce que INT_ACK doit être généré au début de l'interruption et non pas à la fin.

Ajouter INT_ACK dans OPC_DECO.VHD est relativement simple:

1. Add an output INT_ACK to entity opc_deco

 entity opc_deco is
 ...
               Q_INT_ACK                    : out   std_logic;
 ...

2. mettre INT_ACK à 0 par defaut (autour de la ligne 72):

 
                 ALU <= ALU_NOP;
                 DDDDD <= Rd;
                 RRRRR <= Rr;
                 IMM <= X"0000";
                 Q_INT_ACK <= '0';
                 PC_OP <= PC_NEXT;

3. Mettre INT_ACK à 1 quant l'opcode est exécuté (LIGNE 95):

 
                     when "00" =>
                         --
                         -- 0000 0000 0000 0000 - NOP
                         -- 0000 0000 001v vvvv - INTERRUPT
                         --
                         if (I_OPC(5)) = '1' then   -- interrupt
                             Q_ALU_OP <= ALU_INTR;
                             Q_AMOD <= AMOD_ddSP;
                             Q_JADR <= "0000000000" & I_OPC(4 downto 0)  & "0";
                             Q_PC_OP <= PC_LD_I;
                             Q_WE_F <= '1';
                             Q_WE_M <= "11";
                             Q_INT_ACK <= '1';    -- ACKNOWLEDGE THE INTERRUPT
                         end if;

Cela met Q_INT_ACK à 1 pour un cycle et peut être utilisé pour mettre la source d'interruption à 0. En théorie on pourrait créer un INT_ACK par source possible d'interruption au lieu d'un seul pour toutes les interruptions mais comme la majeure partie des interruptions ne sont pas automatiques, je ne ferai pas cela ici mais plutôt dans le process IOINT.

Tu peux maintenant connecter INT_ACK au process IOINT qui doit alors vérifier l'interruption courante pour générer le bon signal INT_ACK par source.

/// Jürgen

Notez que nous ne sommes pas sûr que Juergen nous tutoie, avec l'Anglais on ne sait jamais... Nous avons fait les modifications demandées.

Pour bien comprendre ce mail, il faut avoir à l'esprit qu'une interruption est réalisée par une insertion d'un opcode spécifique qui va mettre le drapeau d'interruption général (appelé I) à 0. Et ceci pour éviter toute nouvelle interruption pendant que l’on en exécute une. Ce drapeau I sera automatiquement repositionné à 1 par l'instruction RETI. Tout ce mécanisme est dans le cœur depuis le début... la seule chose qui a changé c’est l'apparition d'une nouvelle possibilité de mise à 0 d'un drapeau de manière automatique.

Nous publions dans cette section seulement le nouveau pseudotimer pour montrer qu’il est un peu plus complexe maintenant.

	 timer0: process(I_CLK) begin
	 if (rising_edge(I_CLK)) then
		 if (I_ADR_IO = X"52" and I_WE_IO = '1') then
			 pseudotimer(19 downto 12) <= I_DIN;
--			 L_TOV0 <= '0';
			else
		 pseudotimer <= pseudotimer + 1;
			 
			end if;
		 end if;
	 end process;
	 TOV0:process(I_CLK) begin
	 if (rising_edge(I_CLK)) then
		 if pseudotimer = x"FFFFF" then 
			 L_TOV0 <= '1';
-- automatic reset of TOV0 :
			 elsif I_INT_ACK ='1' and L_INTVEC = "101001" then
			 L_TOV0 <= '0';
-- reset of TOV0 with TIFR |= (1<<TOV0); :
			 elsif I_ADR_IO = X"58" and I_DIN(0)='1' and I_WE_IO = '1' then 
			 L_TOV0 <= '0';
			 end if;
		 end if;
    end process;

Nous avions promis en début de chapitre d'arrêter avec les versions différentes mais voilà, on ne fait pas toujours comme on veut. Voici donc une version qui permet la mise à 0 du bit TOV0 exactement comme dans celui que vous achèteriez : ATMega16 avec reset automatique du TOV0. Cette nouvelle ressource a été ajoutée en Juillet 2015.

Projet pacman avec l'ATMega16 (2013/2014)[modifier | modifier le wikicode]

Le projet Pacman de l'année précédente avec l'ATMega8 était un programme C de plus de 700 lignes. On s'approchait alors des limites de l'ATMega8. Le fait d’utiliser un ATMega16 maintenant va nous donner une bouée d'oxygène pour la taille du code (16 ko de mémoire programme contre 8 ko).

Lire aussi[modifier | modifier le wikicode]

Présentons maintenant les objectifs du projets.

Objectifs[modifier | modifier le wikicode]

Il s'agit de gérer un jeu de pacman à partir d'une partie matérielle donnée. Cependant, contrairement à l'année précédente, la partie matérielle donnée n’est pas complète comme le montre la section suivante. Quant à la partie logicielle, il sera assez difficile aux étudiants de reprendre le travail de l'année précédente. L'ajout d'un ennemi change fortement la donne.

Contrairement à l'année précédente nous allons utiliser la carte platine d'évaluation spartan3e starter kit.

Matériel : gestion de l'accéléromètre de la manette Nunchuk[modifier | modifier le wikicode]

Rappelons ce qui a été fait jusqu'à présent. Nous avons commencé par utiliser un vieux Joystick avec 4 contacts pour haut, bas, gauche et droite. Nous avons remplacé ce périphérique par une manette Nunchuk mais avec comme souci de ne pas changer le programme C de gestion des directions de déplacement. Donc le périphérique développé l'année précédente lisait les valeurs analogiques retournées par Nunchuk et à partir d'un certain seuil positionnait un bit par direction. Il se comportait donc comme le joystick du départ... mais tout cela est peu adapté à la gestion des accéléromètres.

Vous allez partir de la version de l'année précédente avec une gestion du joystick analogique de la manette Nunchuk et lui ajouter une gestion des accéléromètres tout en retirant les comparaisons et les seuils associés. L'ensemble des données de la Nunchuk sera systématiquement stocké dans un FIFO lisible depuis le processeur. Fini donc la gestion matérielle pour remplacer le joystick analogique par un joystick numérique ! C'est notre processeur qui devra tout gérer !

Un module capable de gérer TOUTES les données de la Nunchuk est disponible dans un autre chapitre. C'est lui que vous allez utiliser et interfacer dans "io2.vhd". Vous utiliserez deux registres réservés à l’i2c dans l'ATMega16 :

  • TWDR en adresse 0x23 pour les données
  • TWCR en adresse 0x56 pour les commandes et états commande TWEN est en bit b2 (écriture) et l'état TWINT = data present en bit b7 (lecture)

Voici un condensé sous forme de schéma de ce qu’il y a à faire :

Comment interfacer le module I2C pour lire les données de la Nunchuk

Même si cette figure ne représente pas tous les registres et PORTs que nous utilisons dans le projet pacman complet elle nous montre l'essentiel. L'ensemble du travail consiste donc à câbler et ajouter des lignes dans les deux "case" des deux process de lecture et d'écriture. Ce qui est peut être un peu délicat à comprendre est la réalisation du signal "s_readFIFO" tout en haut de la figure. Ce signal est là pour vider le FIFO au fur et à mesure de ses lectures par le processeur. Il sera relié à l'entrée readFIFO du module NunchukTop.

Pour information, votre point de départ est le projet de l'année précédente qui peut se résumer comme présenté dans la figure ci-dessous :

Projet de l'année scolaire 2012/2013

Logiciel : lecture du FIFO pour les données Nunchuk[modifier | modifier le wikicode]

Nous allons vous donner deux sous-programmes pour vous aider à démarrer votre projet.

Lecture du FIFO et demande de données[modifier | modifier le wikicode]

Voici le code source correspondant :

//******************************************************************************************************************************
// function readFIFOandAskForNextData()
// purpose: read FIFO and put data in array and start the request of next data
// arguments: array of unsigned char to store FIFO data
// 
// return: 
// note: you cannot understand if you never read the corresponding VHDL code (in io2.vhd)
//******************************************************************************************************************************
void readFIFOandAskForNextData(unsigned char nunchukDATA[]){
  unsigned char i;
  while(!(TWCR & (1<<TWINT))); // attente données dans le FIFO 	
  for (i=0;i<6;i++) //lecture du Nunchuk du FIFO dans tableau
     nunchukDATA[i]=TWDR;
  // on demande une nouvelle lecture par le pérphérique : toutes données seront pour la prochaine fois
  TWCR |= (1<<TWEN); //depart
  TWCR &= ~(1<<TWEN);// arret pour attendre la prochaine fois : voir machine d'états pour comprendre
}

Il faudra le compléter par un code du genre :

readFIFOandAskForNextData(nunchukDATA);
if (nunchukDATA[0]>0xC0) {dxf = 2; dyf = 0;vPORTB &= 0xFC;
if (nunchukDATA[0]<0x30) {dxf = -2; dyf = 0;vPORTB = (vPORTB&0xFD)|0x01;}
if (nunchukDATA[1]>0xC0) {dyf = -4; dxf = 0;vPORTB = (vPORTB &0xFE)|0x02;} 
if (nunchukDATA[1]<0x30) {dyf = 4; dxf = 0;vPORTB |= 0x03;}

pour gérer correctement les déplacements de pacman.

Attente d'un départ[modifier | modifier le wikicode]

Voici comment on peut bloquer tout en attendant un appui sur le bouton Z :

//******************************************************************************************************************************
// function waitStart()
// purpose: wait Z button start : Doesn't work properrly at the moment !!!
// arguments:
// 
// return: 
// note: you cannot understand if you never read the corresponding VHDL code (in io2.vhd)
//******************************************************************************************************************************
void waitStart(){
  unsigned char data[6],loop=1;
  while(loop) {
    readFIFOandAskForNextData(data);
    if ((data[5] & 0x01)==0x00) loop=0;
    _delay_ms(10); // car data_present mal géré et on ne prend pas de risque
  }

On rappelle pour comprendre le code, que les deux boutons sont peésents dans le 6° octet retourné par la Nunchuk (donc la case 5 de notre tableau) que les deux boutons sont les deux poids faibles et qu’ils sont à 1 lorsqu’ils ne sont pas appuyés.

Logiciel : gestion du deuxième ennemi[modifier | modifier le wikicode]

Nous avons ajouté un deuxième ennemi pour cette année scolaire. Votre programmation en C devra obligatoirement le gérer.

Comme nous avons l'intention d'examiner, à terme, la réalisation du pacman avec un petit système multitâche, nous demandons aux étudiants de cette année scolaire de lire l’article sur un système multitâche non préemptif et d'essayer d’utiliser les techniques correspondantes pour ce jeu. Ce système écrit par Ron Kreymborg a été porté pour les AVR ICI (mais avec au autre compilateur C). Téléchargez alors "multi.zip" pour avoir le code source.

Voici un sous-programme permettant de gérer le positionnement de notre deuxième ennemi :

//******************************************************************************************************************************
// function setenemy2XY()
// purpose: put the second enemy with x and y coordinates
// arguments:
// corresponding x and y coordinates
// return:
// note:
//******************************************************************************************************************************
void setenemy2XY(uint8_t x,unsigned char y){
  ADCL=x; 
  ADCH=y;
}

Les sous-programmes donnés comme point de départ de l'année précédente peuvent être repris aussi.

Ressources pour commencer[modifier | modifier le wikicode]

On vous propose pour commencer votre projet un ensemble de ressources pour travailler. Voici ce que contient le fichier ReadMe.txt de la ressource :

Ce projet comporte :

  • un processeur ATMega16 embarqué avec peu des périphériques originaux : seule la rs232 fonctionne
  • un écran VGA gérant le pacman
  • un périphérique gérant la manette Nunchuk de Nitendo.
  • deux fichers ucf : un pour la carte spartan3 et un pour la carte spartan3E toutes deux de chez Digilent

Dans le répertoire Soft vous avez :

  • un programme c ne gérant qu'un seul fantome sur les deux : pacman.c
  • un autre programme c ne gérant qu'un seul fantome mais une poursuite plus sofistiquée du pacman par le fantome. Il a été réalisé par deux étudiants de GEII en projet 2012/2013 (Guillaume Demarquez et Pauline Skorupka) : pacmanEtu.c
  • un script shell pour mettre le programme en question dans le processeur (par l'intermédiaire du fichier .bit) puis télécharger dans le FPGA.

Notez que la façon de télécharger utilisée dans le script n'est utilisable que sur un port parallèle. Si vous utilisez adept de digilent, il vous faudra tout adapter.

L'ensemble est téléchargeable ici :

Correction partielle[modifier | modifier le wikicode]

La correction matérielle complète est téléchargeable ici : nexys3PacmanAtmega16.zip pour la carte Nexys 3. Le programme C de cette correction est le mien mais les étudiantes (KONAK Sumeyye et SAVRY Maelle) vous présentent le leur ici dans le WIKI de l'IUT de Troyes. Pour une fois nous nous demandons sérieusement si la rédaction du rapport de ce projet ne dépasse pas en qualité celui du tuteur que vous avez sous les yeux ? Allez donc lire pour comparer.


Voir aussi[modifier | modifier le wikicode]

Conception d'un périphérique i2c[modifier | modifier le wikicode]

La réalisation du périphérique Nunchuk pour le pacman de la section précédente nous a donné entière satisfaction. Mais elle est extrêmement spécialisée. Il existe beaucoup d'autres périphériques i2c mais il est impossible de les utiliser sans changer de machine d'états. Or changer une machine d'états est un travail complexe, bien plus complexe que d'utiliser un programme en C pour gérer un protocole i2c avec un microcontrôleur. C'est cette simplicité d'utilisation que nous allons rechercher dans cette section. En clair nous sommes obligés de refaire une machine d'états, mais ce sera, nous l'espérons la dernière. Toute modification du protocole i2c sera transférée sur le programme.

Dans la mesure du possible, ce que l'on va réaliser est très proche de ce que l'on trouve dans les ATMega8/16/32 du commerce. Nous allons utiliser trois registres (seulement contre 5 dans les ATMegas) pour réaliser les transferts i2c. Nous allons les nommer ici, par commodité, avec les noms qu'ils ont dans l'AVR :

  • TWBR sert à régler la division d'horloge. Nous le ferons fonctionner différemment de la façon dont il fonctionne dans les AVR. Relisez l'étude de la manette Nunchuk et i2c dans les AVR pour comprendre la différence. La formule dans les AVR est tandis que celle dans notre cœur est .
  • TWCR sert à commander le périphérique i2c. Nous allons garder les bits TWSTA, TWSTO, TWEN, TWEA et TWINT mais ajouter TWWR et TWRD respectivement pour écrire et lire.
  • TWDR sert à écrire ou lire les données à transmettre ou reçues

Dans les AVR, il y a la possibilité d'envoyer un START seul. Par exemple le sous-programme suivant fait le travail :

//send start signal
void TWIStart(void)
{
    TWCR = (1<<TWINT)|(1<<TWSTA)|(1<<TWEN);
    while ((TWCR & (1<<TWINT)) == 0);
    //TWCR &= ~(1<<TWSTA); //RAZ logiciel du bit TWSTA en commentaire avant des tests
}

Mais nous allons utiliser la partie VHDL que l'on a déjà utilisé pour la manette Nunchuk. Et elle ne nous offre pas la possibilité de séparer les START du write. Pour le STOP il en est autrement car il peut être associé à un read ou à un write. En clair ce que nous allons réaliser fonctionnera différemment de l'AVR et c'est d'ailleurs pour cela que nous avons ajouté les deux bits dans le registre TWCR, à savoir TWWR et TWRD.

Machine d'états pour commander un périphérique i2c par le coeur ATMega16

Notre cahier des charges consiste à donner aux programmeurs les sept possibilités suivantes :

  • write seul (TWEN=1 et TWWR=1 et TWSTA=0 et TWSTO=0)
  • START + write (TWEN=1 et TWWR=1 et TWSTA=1 et TWSTO=0)
  • write + STOP (TWEN=1 et TWWR=1 et TWSTA=0 et TWSTO=1)
  • START + write + STOP (TWEN=1 et TWWR=1 et TWSTA=1 et TWSTO=1)
  • read + ACK (TWEN=1 et TWRD=1 et TWEA=1 et TWSTO=0)
  • read + ACK + STOP (TWEN=1 et TWRD=1 et TWEA=1 et TWSTO=1)
  • read + NACK + STOP (TWEN=1 et TWRD=1 et TWEA=0 et TWSTO=1)

Ceci montre que notre machine d'états aura sept branches parallèles. C'est ce que montre la figure ci-contre que nous expliquerons un peu plus loin.

Exercice 0[modifier | modifier le wikicode]

Interfacer notre cœur i2c avec notre processeur

Cet exercice ne comporte pas de travail à proprement parler d'où son numéro 0.

Nous avons déjà présenté le graphe d'états. Ce qui est important à mémoriser de ce graphe est l'ensemble de ses huit branches liées aux sept possibilités données au programmeur. La huitième est là pour gérer des configurations de bits du registre TWCR incompatibles (par exemple une écriture et une lecture demandées simultanément).

Comme nous avons l'intention de réaliser un périphérique étape par étape, il faut comprendre sa liaison avec le microcontrôleur. Comme d'habitude un schéma explicatif est présenté. Il nous montre toutes les entrées du périphérique à réaliser (en jaune dans la figure). L'ensemble des exercices 1, 2, 3 et 4 se réalise dans ce rectangle jaune. Le schéma nous donne immédiatement l'entité correspondante :

entity topi2c is port(
    clk,Reset : in std_logic;
    TWWR,TWSTA,TWSTO,TWRD,TWEA,TWINT,TWEN : in std_logic;
    TWBR : in std_logic_vector(7 downto 0); -- Bit Rate Register
    IN_TWDR : in std_logic_vector(7 downto 0); -- Data Register
    OUT_TWDR : out std_logic_vector(7 downto 0); -- Data Register
    O_TWINT : out std_logic; -- pour gestion particuliere de TWINT
    -- i2c signals
    SCL : inout std_logic;
    SDA : inout std_logic
);  
end entity topi2c;

Distinguez les bits qui sont en "std_logic" des registres qui sont en "std_logic_vector(7 downto 0)".

Les bits vont servir à réaliser le premier exercice.

Exercice 1[modifier | modifier le wikicode]

Pour simplifier la programmation de la machine d'états, nous allons réaliser une fonction combinatoire qui sera capable de choisir une des huit branches présentes sur la machine d'états ci-dessus. On va pour cela utiliser quelques bits du registre de commande TWCR : la table de vérité à compléter vous précise lesquels.

1°) On vous demande de compléter la table de vérité ci-dessous capable de gérer les bits précédents et d'en sortir huit branches possibles :

Table de vérité
Entrées Sortie
TWWR TWRD TWSTA TWSTO TWEA branch1 branch2 branch3 branch4 branch5 branch6 branch7 autrement
1 0 0 0 0 1 0 0 0 0 0 0 0
1 0 1 0 0 0 1 0 0 0 0 0 0
? ? ? ? ? ? ? ? ? ? ? ? ?
Partie combinatoire à réaliser

2°) Écrire le programme VHDL correspondant à l'aide d'un "with select when". Pour cela vous allez créer un signal interne en "std_logic_vector" pour les entrées à partir des entrées sous forme de bits (concaténation en VHDL réalisée avec l'opérateur &). La sortie sera un "std_logic_vector" qui regroupe aussi les diverses sorties de la table de vérité.

Indications  :

  • Une figure vous présente le travail à réaliser. Nous utiliserons à partir de maintenant des rectangles en pointillés pour désigner un composant non réalisé par le couple entité/architecture mais directement par equation ou process sur signaux externes ou internes.
  • Les bits présentés dans cette table de vérité appartiennent tous au registre TWCR. On a :
    • TWWR demande d'écriture i2c quand il est à 1
    • TWRD demande de lecture i2c quand il est à 1
    • TWSTA demande de Start i2c quand il est à 1. Ce bit ne peut pas être séparé de TWWR. Un start est toujours associé à une écriture.
    • TWSTO demande de Stop i2c quand il est à 1. Ce bit est associé soit à TWWR soit à TWRD. Dans ce dernier cas on utilise aussi TWEA
    • TWEA demande d'acquitement i2c quand il est à 1
  • Le bit TWINT a été retiré car il a un fonctionnement très particulier et sera géré à part (voir exercice suivant)
  • Le bit TWEN a été retiré parce qu'il est géré ailleurs comme d'habitude dans le "if clk'event"
  • la gestion habituelle de ce genre de situation se fait directement dans les if des transitions de la machine d'états mais nous préférons la réaliser à l'extérieur de la machine d'état et avec une table de vérité complète, ce qui permettra de la programmer avec un "with select when"
  • tous les bits non présents dans la description du cahier des charges ci-dessus seront supposés à 0.
  • le "autrement" est à 1 quand aucune des autres branches n'est sélectionnée. Il n'y a donc que 6 lignes à compléter dans la table de vérité.
  • si un programmeur demande une écriture et une lecture simultanément, c'est donc la branche "else" qui sera exécutée, c'est-à-dire qu'il ne se passera rien.

Exercice 2 : Réalisation du bit TWINT[modifier | modifier le wikicode]

On ajoute la réalisation du bit très particulier TWINT

Attention : ce qui est présenté ici n'est pas général. C'est propre aux AVR ATMega et nous désirons réaliser un bit qui fonctionne à peu près comme cela.

Le bit TWINT du registre TWCR est donc un peu particulier. Il faut le mettre à un pour démarrer l'utilisation du cœur i2c. Mais comme il s'agit d'un drapeau, cette mise à 1 provoque en fait une mise à 0. C'est seulement quand la commande i2c demandée est réalisée que ce bit revient à 1. Ce mécanisme est là pour réaliser une interruption mais nous n'implanterons pas celle-ci.

Ce qui est à réaliser est encore une fois présenté à l'aide d'une figure dans laquelle on retrouve la partie de l'exercice 1 ainsi que la nouvelle pour l'exercice 2. Encore une fois le rectangle est en pointillés et son nom nous montre qu'il est réalisé par un process.

Réaliser ce mécanisme à l'aide d'un process qui reçoit un signal "TWINT", une horloge "clk", une entree "reset" et fabrique une sortie "s_TWINT".

Indications :

  • le schéma de principe d'intégration du périphérique dans le processeur (figure présentée plus haut), nous montre que l'entrée TWINT est réalisée par l'équation
s_TWINT_tick <= I_WE_IO when ((I_ADR_IO = TWCR) and (I_DIN(7)='1')) else '0';

Ce nom suggère un tick (signal à 1 pendant une seule durée d'horloge). Ce signal est donc réalisé par l'écriture dans TWCR d'où la présence de "I_ADR_IO = TWCR" et par une écriture d'un 1 dans TWINT d'où la présence de "(I_DIN(7)='1')'. Tout ceci doit réaliser une remise à zéro (RAZ) d'un signal que l'on appellera s_TWINT.

  • La mise à 1 (set) sera réalisée par la machine d'états (exercice suivant) qui génèrera le signal "O_set_TWINT"
Ensemble avec timer et registre

Conséquences : L'utilisation du périphérique i2c se fera toujours en C à l'aide du couple :

    TWCR = (1<<TWINT)|... a compléter...|(1<<TWEN);
    while ((TWCR & (1<<TWINT)) == 0);

La première ligne sert à choisir la commande i2c que l'on veut réaliser tandis que la deuxième sert à attendre que tout soit fini.

Avant d'entamer la construction de la machine d'états il vous faut construire un timer et un registre de donnée. Ces deux process ont déjà été publiés avec la réalisation du périphérique Nunchuk et ne seront donc pas demandés ici mais sont présentés sur la figure ci-contre. Le registre comme le timer sont sur 8 bits. La sortie du timer est donc réalisée avec le bit 7 du timer.

Exercice 3 : Machine d'états[modifier | modifier le wikicode]

Machine d'états pour commander un périphérique i2c par le coeur ATMega16

La machine d'états a été déjà présentée sous forme de figure mais nous la reproduisons encore une fois pour faciliter la lecture.

Les états de cette machine d'états sont représentés avec leur nom en partie supérieure et les action (sorties) en partie inférieure. Les sorties sont, pour tous les états, présentées dans le même ordre qui est indiqué sur la figure.

Explications :

  • l'état "s_else" sert à réaliser le set du bit TWINT : dire au programmeur que ce qu'il a demandé de réaliser est complètement terminé
  • un front montant sur TWINT permet le passage de l'état "s0" à l'état "s1". La façon dont a été réalisé ce front est expliquée dans l'exercice précédent.
  • le choix des diverses branches (y compris le "autrement") est fait par l'ensemble des bits réunis dans un signal "s_select" réalisé en exercice 1
  • les branches et donc les états ne sont pas dans l'ordre attendu (s_br1, s_br2, ... ,s_br6 et s_br7) pour simplifier le dessin
  • l'état s_stop sert à réaliser le stop de l'i2c. Donc seules les branches qui ont à réaliser ce stop sont concernées par cet état.

Réaliser la machine d'états correspondante. Encore une fois l'ensemble est présenté sous forme de figure où vous reconnaîtrez la présence de la machine d'états dans un rectangle en pointillés car implanter directement dans un process.

Ajout du cerveau : la machine d'états

Réalisation complète dans l'ATMega16[modifier | modifier le wikicode]

Voici notre choix des bits de contrôle du registre de contrôle TWCR. Ils ont été choisis pour être le plus possible en conformité aux AVR. On rappelle que TWWR et TWRD n'existent pas dans un ATMega.

TWCR
b7
b6
b5
b4
b3
b2
b1
b0
TWINT
TWEA
TWSTA
TWSTO
TWRD
TWEN
TWWR
?

La réalisation dans l'ATMega16 présentée dans ce livre nécessite de modifier 4 fichiers :

  • réaliser une machine d'états autour du cœur i2c
  • réaliser une interface au processeur
  • propager les sorties i2c comme sortie du processeur
  • modifier le fichier ucf pour prendre en compte ces modifications.

Exercice 4[modifier | modifier le wikicode]

Voici présenté sous forme de figure l'ensemble complet de ce qui est à réaliser.

Voici la réalisation finale du périphérique

Vous pouvez y distinguer le composant simple_i2c qui est celui qui a déjà été utilisé pour l'étude de la manette Nunchuk. Il n'est plus en pointillés car il est réalisé par un programme VHDL comportant une entité et une architecture.

Pour ne pas surcharger la figure, nous avons laissé quelques connexions non connectées :

  • l'entrée TWBR est reliée à clk_cnt
  • l'entrée nReset est reliée à "not reset". Le not est admis dans un port map
  • l'entree "ena" et reliée à TWEN marqué TWEN[2] sur le schéma

Réaliser l'ensemble complet du périphérique.

Exercice 5[modifier | modifier le wikicode]

Interfacer le périphérique de l'exercice 4 dans votre ATMega16.

Exercice 6[modifier | modifier le wikicode]

Adapter votre processeur au nouveau périphérique. Il suffit ici de sortir les file de l'i2c vers l'extérieur.

Exercice 7[modifier | modifier le wikicode]

Modifier le fichier ucf pour l'adapter à votre carte.


Amélioration du cœur i2c[modifier | modifier le wikicode]

La remarque précédente nous a obligé à améliorer le code de ce cœur i2c. Nous laissons pour le moment le code source de cette nouvelle version ici sans commentaire, sachant qu'elle n'a pas encore été testée.

Programme d'utilisation[modifier | modifier le wikicode]

Réaliser un programme capable de lire la manette Nunchuk.

Voir aussi[modifier | modifier le wikicode]

Problèmes matériels résolus avec ce soft processeur[modifier | modifier le wikicode]

Nous avons découvert depuis un certain temps que les calculs en nombre flottant posent des problèmes à notre cœur ATMega16.

Il y a probablement une instruction ou plusieurs instructions qui ne fonctionnent pas correctement.

Cette section est destinée à contenir l'histoire de la résolution de ce problème. Il s'agit d'une enquête minutieuse dans laquelle nous avons fait des erreurs : considérer après un test une instruction comme bonne alors qu'elle ne l'était pas !

Pour réaliser cette recherche nous avons d'abord essayé de programmer une addition de nombres flottants entièrement en c en espérant y retrouver le problème. Pourquoi cette façon de procéder ? Parce qu'autrement il faut lire entièrement la librairie de calcul flottant et y débusquer une instruction qui n'a jamais été utilisée par nos autres différents programmes. Le meilleurs moyen de réaliser cela de manière automatique aurait été de réaliser un pseudo-assembleur capable de lire les fichiers .lss et de lister toutes les instructions assembleur utilisées. En comparant les listes avec et sans calcul flottant on pouvait espérer trouver la ou les instructions coupables. Ce travail n'a tout simplement pas été réalisé malgré notre connaissance très (trop) ancienne sur la réalisation de compilateur avec les outils Lex et Yacc. Pour le problème posé, seule l'utilisation de "lex" ou plutôt "Flex" aurait été nécessaire.

Commençons par réaliser un travail sur les additions avec un compilateur standard pour PC, à savoir le GNU C. Notre objectif sera ensuite de porter ce travail dans notre SOC ATMega16 pour examiner son comportement.

Addition flottante en C sur PC[modifier | modifier le wikicode]

Il est beaucoup plus facile de travailler sur un PC que sur un microcontrôleur car nous rencontrons beaucoup moins de problème d'entrées/sorties.

Voici donc un programme d'addition de nombre flottants en C destiné à un PC. Nous sommes parti d'un travail de Mike Field Custom floating point mais l'avons complètement modifié.

Ce programme donne :

serge@Rosetta:~/Xilinx/Nexys3/Atmega16i2c/soft$ ./essai
Entrez deux nombres flotants : 1.5
1.25
Deux nombres initiaux : 3fc00000 3fa00000
c00000 7f - a00000 7f
avant normalisation : 1600000 7f
apres normalisation : 300000 80
Le resultat est 2.750000 à comparer a 2.750000
Le resultat HEXA : 40300000

mais n'est pas prévu pour fonctionner avec des nombres négatifs. Ce qui est donné ici nécessite une grande connaissance du format flottant et de l'hexadécimal correspondant si on veut en faire une analyse fine.

Addition flottante en C sur notre SOC ATMega16[modifier | modifier le wikicode]

Puisque le problème d'une addition complètement écrite en c est résolu en section précédente, nous allons tenter de porter ce code dans notre ATMega16 et comparer cette addition à celle de la librairie standard du C.

L'exécution de ce programme donne dans un moniteur série :

00C00000 - 007F
00A00000 - 007F
sum = 01600000 - 007F
sum norm = 00300000 - 0080
sum = 40300000
sum (lib) = 40400000

Si vous comparez les deux sections (celle-ci et la précédente) vous voyez que sur AVR tout se passe bien jusqu'à "sum" de l'avant dernière ligne. C'est-à-dire avec notre propre routine d'addition. Par contre avec "sum (lib)" qui est un calcul utilisant la librairie standard d' avr-gcc, on ne trouve pas exactement ce que l'on devrait trouver.

Résultats de l'enquête[modifier | modifier le wikicode]

Histoire de la recherche[modifier | modifier le wikicode]

  • Le point essentiel du code des deux sections précédentes est la définition d'une union qui permet de voir un même nombre de 32 bits soit comme un nombre flottant, soit comme un nombre entier (hexadécimal) :
 typedef union {
     float f_temp;
     uint32_t li_temp;
  } t_u;
  • On s'est relativement très vite aperçu que si l'on voulait faire une addition il fallait en fait faire une soustraction. En clair, si dans un programme dans mon AVR j'écrit : "sum = nb1-nb2;" j’obtiens dans sum la valeur nb1+nb2.
  • Quand on sait que le signe d'un nombre flottant est donné par le bit de poids fort, nous avons essayé de changer le signe du nombre flottant avec une instruction c :
  fnb1.li_temp=0x3F800000; //=1.0
  fnb1.li_temp|=0x00400000; //=1.5 maintenant
  fnb1.li_temp|=0x80000000; //=-1.5 maintenant : changement de signe ici

Le changement de signe de la troisième ligne était réalisé par le programme assembleur (c compilé) :

  set
  bld r16,7
  • Nous avions classé l'instruction BLD parmi les instructions fonctionnant correctement mais ce code assembleur ne fonctionnait pas correctement. Il donnait la valeur hexadécimale 0x808400000 au lieu de la valeur 0xBF8400000 attendue.
  • Nous venions de découvrir la première instruction fautive : BLD.
    • si r16 = XXXXXXXX bld r16,7 doit donner r16 = TXXXXXXX (T est un flag qui est mis à 1 par l'instruction "set" qui précède)
    • si r16 = XXXXXXXX notre bld r16,7 donnait r16 = T0000000 (les autres bits du registre étaient perdus)
    • la correction de cette instruction est donnée un peu plus loin. Nous pensions qu'immédiatement le calcul flottant fonctionnerait correctement mais ce n'était pas le cas !!!
  • Une autre instruction coupable était à chercher. Cette recherche a été parasitée par la découverte des instructions CBI et SBI ne fonctionnant pas correctement. Elles ne pouvaient absolument pas être coupables du dysfonctionnement de la librairie flottante car elles servent à mettre un bit à 1 ou à 0 dans les registres I/O (pas dans les registres r0,...,r31, seuls utilisés dans la librairie) !
  • le TRUC c'est que pour faire fonctionner ces CBI et SBI il fallait configurer l'ALU dans le même mode que l'instruction contraire de BLD (notre premier coupable) à savoir BST. Et celle-là n'avait encore jamais été testée et elle apparaissait une fois dans la librairie d'addition flottante.
  • Nous nous sommes vite aperçu que BST ne fonctionnait pas correctement non plus avec un programme assembleur du genre :
  ldi r16,0xF0
  bst r16,4 ; devait mettre 1 dans T mais mettait 0 !
  ;bst r16,3 ; devait mettre 0 dans T mais mettait 1 !
  bld r16,0 ; recopie de T en b0
  out PORTC,r16 ;n'allumait pas le bit b0 du PORTC
  • Puisque nous avons mis en cause les instructions CBI et SBI, il nous en faut dire un petit mot ici. Ces instructions fonctionnent en fait parfaitement à condition que la sortie des registres d' I/O soit bouclée sur l'entrée correspondante, ce que nous ne faisons pas toujours. Ce problème est déjà abordé ICI dans ce chapitre.

Correction du code de l'ATMega16[modifier | modifier le wikicode]

  • Correction de l'instruction BLD

Il faut aller dans le fichier VHDL : alu.vhd en ligne 188 (ou pas loin pour vous). Vous y trouverez le code :

when ALU_BLD =>     -- copy T flag to DOUT
                case I_BIT(2 downto 0) is
                    when "000"  => L_DOUT( 0) <= I_FLAGS(6);
                                   L_DOUT( 8) <= I_FLAGS(6);
                    when "001"  => L_DOUT( 1) <= I_FLAGS(6);
                                   L_DOUT( 9) <= I_FLAGS(6);
                    when "010"  => L_DOUT( 2) <= I_FLAGS(6);
                                   L_DOUT(10) <= I_FLAGS(6);
                    when "011"  => L_DOUT( 3) <= I_FLAGS(6);
                                   L_DOUT(11) <= I_FLAGS(6);
                    when "100"  => L_DOUT( 4) <= I_FLAGS(6);
                                   L_DOUT(12) <= I_FLAGS(6);
                    when "101"  => L_DOUT( 5) <= I_FLAGS(6);
                                   L_DOUT(13) <= I_FLAGS(6);
                    when "110"  => L_DOUT( 6) <= I_FLAGS(6);
                                   L_DOUT(14) <= I_FLAGS(6);
                    when others => L_DOUT( 7) <= I_FLAGS(6);
                                   L_DOUT(15) <= I_FLAGS(6);
                end case;

et vous devez le remplacer par :

when ALU_BLD =>     -- copy T flag to DOUT
  --Q_FLAGS(6) <= I_FLAGS(6);-- done by default far all flags
  case I_BIT(2 downto 0) is
       when "000"  => L_DOUT(0) <= I_FLAGS(6);
         L_DOUT(7 downto 1) <= I_DIN(7 downto 1);
         L_DOUT(8) <= I_FLAGS(6);
         L_DOUT(15 downto 9) <= I_DIN(7 downto 1);
       when "001"  => L_DOUT(1) <= I_FLAGS(6);
         L_DOUT(0) <= I_DIN(0);
         L_DOUT(7 downto 2) <= I_DIN(7 downto 2);
         L_DOUT(9) <= I_FLAGS(6);
         L_DOUT(8) <= I_DIN(0);
         L_DOUT(15 downto 10) <= I_DIN(7 downto 2);
       when "010"  => L_DOUT( 2) <= I_FLAGS(6);
         L_DOUT(1 DOWNTO 0) <= I_DIN(1 DOWNTO 0);
         L_DOUT(7 downto 3) <= I_DIN(7 downto 3);
         L_DOUT(10) <= I_FLAGS(6);
         L_DOUT(9 DOWNTO 8) <= I_DIN(1 DOWNTO 0);
         L_DOUT(15 downto 11) <= I_DIN(7 downto 3);
       when "011"  => L_DOUT( 3) <= I_FLAGS(6);
         L_DOUT(2 DOWNTO 0) <= I_DIN(2 DOWNTO 0);
         L_DOUT(7 downto 4) <= I_DIN(7 downto 4);
         L_DOUT(11) <= I_FLAGS(6);
         L_DOUT(10 DOWNTO 8) <= I_DIN(2 DOWNTO 0);
         L_DOUT(15 downto 12) <= I_DIN(7 downto 4);
       when "100"  => L_DOUT( 4) <= I_FLAGS(6);
         L_DOUT(3 DOWNTO 0) <= I_DIN(3 DOWNTO 0);
         L_DOUT(7 downto 5) <= I_DIN(7 downto 5);
         L_DOUT(12) <= I_FLAGS(6);
         L_DOUT(11 DOWNTO 8) <= I_DIN(3 DOWNTO 0);
         L_DOUT(15 downto 13) <= I_DIN(7 downto 5);
       when "101"  => L_DOUT( 5) <= I_FLAGS(6);
         L_DOUT(4 DOWNTO 0) <= I_DIN(4 DOWNTO 0);
         L_DOUT(7 downto 6) <= I_DIN(7 downto 6);
         L_DOUT(13) <= I_FLAGS(6);
         L_DOUT(12 DOWNTO 8) <= I_DIN(4 DOWNTO 0);
         L_DOUT(15 downto 14) <= I_DIN(7 downto 6);
       when "110"  => L_DOUT( 6) <= I_FLAGS(6);
         L_DOUT(5 DOWNTO 0) <= I_DIN(5 DOWNTO 0);
         L_DOUT(7) <= I_DIN(7);
         L_DOUT(14) <= I_FLAGS(6);
         L_DOUT(13 DOWNTO 8) <= I_DIN(5 DOWNTO 0);
         L_DOUT(15) <= I_DIN(7);
       when others => L_DOUT( 7) <= I_FLAGS(6);
         L_DOUT( 6 downto 0) <= I_DIN(6 downto 0);
         L_DOUT(15) <= I_FLAGS(6);
         L_DOUT( 14 downto 8) <= I_DIN(6 downto 0);
       end case;
  • Correction de l'instruction BST

La correction de cette instruction est beaucoup plus simple. Il faut encore aller dans le fichier VHDL : alu.vhd en ligne 237 (ou pas loin pour vous si vous avez fait la correction précédente). Vous y trouverez le code :

when ALU_BIT_CS =>  -- copy I_DIN to T flag
                Q_FLAGS(6) <= L_RBIT xor not I_BIT(3);

et vous devez le remplacer par :

when ALU_BIT_CS =>  -- copy I_DIN to T flag
                Q_FLAGS(6) <= L_RBIT; --removed 20/10/2020 : xor not I_BIT(3);

Et vous êtes maintenant capable de calculer un cosinus ou un sinus en nombre flottant.


Avancées technologiques 2019[modifier | modifier le wikicode]

Ce que nous avons réalisé dans ce chapitre peut être trouvé commercialement maintenant (2019 et probablement depuis quelques années). Nous voulons dire quelque chose à peu près équivalent mais plus facile à utiliser (et encore un peu cher à notre goût). Présentons d'abord un produit de Alorium Technology.

Au moment où nous écrivons ces lignes nous n'avons pas testé les produits présentés dans cette section. Mais il ne fait aucun doute que nous allons nous y intéresser assez rapidement.

XLR8 : FPGA Intel MAX 10[modifier | modifier le wikicode]

XLR8 est une fusion parfaite entre les vitesses des FPGA et la plateforme Arduino connue de presque tous. Cette carte vous donne accès à l'ensemble des boucliers Arduino. Des adaptateurs 5V/3.3V sont présents pour une compatibilité 5V. La mise en service ne nécessite pas de connaissances particulières en FPGA puisqu'il s'agit simplement de programmer un SOC avec la librairie Arduino.

Ce qui fait la différence avec un Arduino c'est la possibilité d'utiliser des XBs (pre-programmed hardware Xcelerator Blocks) des blocs enfouis dans le FPGA pour des fonctionnalités diverses comme :

  • le contrôle de servomoteurs
  • l'accélération du calcul flottant
  • la conversion analogique numérique étendue
  • le contrôle NeoPixel
  • la gestion des quadratures

Il est d'autre part possible de gérer ses propres fabrications de XBs et c'est là naturellement qu'intervient le FPGA et toutes les connaissances particulières qui lui sont associées.

Voir aussi[modifier | modifier le wikicode]

Alorium Technology a basé sa technologie sur les AVR 8 bits. Arduino a fait un autre pari : le 32 bits avec le MKR Vidor 4000.

Le MKR Vidor 4000 (Arduino)[modifier | modifier le wikicode]

Dans ce produit apparu en 2019, le processeur est en dur et externe au FPGA

Avec le MKR VIDOR 4000 vous pouvez le configurer de la façon que vous voulez. Il inclut l'interface classique MKR qui permet aux broches d'être reliée aux deux, le SAMD21 et le FPGA.

Le FPGA contient 16K Logic Elements, 504 KB de RAM, et 56 18x18 bit multiplieurs harware pour DSP grande vitesse. Chaque sortie peut basculer jusqu'à 150 MHz et peut être configurée pour des fonctions telles que UARTs, (Q)SPI, grande résolution haute fréquence PWM, encodeur de quadrature, I2C, I2S, Sigma Delta DAC, etc.