Very High Speed Integrated Circuit Hardware Description Language/Embarquer un PIC 16F84

Début de la boite de navigation du chapitre

Nous allons mettre en œuvre dans ce chapitre un processeur softcore compatible avec le PIC® 16F84 de Microchip. Il s'agit d'un processeur RISC sur 8 bits. Après avoir exploré le processeur original, nous nous concentrerons sur le modèle VHDL du système mono-puce. Ce projet, bien que relativement simple, nécessitera une exploration avancée du cœur pour y réaliser certaines modifications : ajout d'un troisième PORT et ajout du signal "strobe" associé. Ce travail ne sera pas présenté dans ce chapitre.

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 : Embarquer un PIC 16F84
Very High Speed Integrated Circuit Hardware Description Language/Embarquer un PIC 16F84
 », n'a pu être restituée correctement ci-dessus.


Choix du cœur

modifier

Il existe au moins trois versions de descriptions de cœur PIC® 16F84 sur Internet. La plus à jour me semble être un projet en Verilog, appelé risc16F84 dans la suite, et disponible sur le site d'opencores avec des mises à jour récentes (2006). Mais ayant des étudiants qui ne pratiquent que VHDL, nous avons décidé d’utiliser le projet CQPIC, plus ancien certes, mais à l'origine de ce cœur risc16F84.

Nous avons déjà eu l’occasion d’utiliser un cœur de processeur 16C57 dans un autre chapitre de ce cours. Cet ancien projet, dénommé silicore1657 dans la suite de ce document, nous a permis d'acquérir un peu d'expérience mise à profit pour le choix du cœur. Il existe en effet un autre cœur VHDL PIC16F84, appelé PPX par la suite, de Daniel Wallner qui a été utilisé et débogué par Patrice Nouel, Maître de Conférence à l’ENSEIRB (à la retraite maintenant). Mais mon choix s'est porté sur le CQPIC car il n'utilise pas de PORT bidirectionnel mais décompose les PORTS (comme expliqué plus loin dans ce document). La documentation du Silicore1657 nous a alerté à ce sujet : elle attire l'attention de l’utilisateur sur l’utilisation des PORTs dans un cœur. Un cœur bien réalisé doit proposer tout sauf la ROM la RAM et les PORTS parce que leur implémentation est trop dépendante du fabricant du FPGA ciblé. Il n'y a, en effet, aucune norme pour implanter ceux-ci.

Le CQPIC est un cœur de processeur (ou processeur softcore) compatible avec le PIC® 16F84 de microchip. On pouvait le trouver sur Internet sous la forme d'un fichier "cqpic100d.zip". Comme il devient difficile de le trouver nous avons mis une version (améliorée ?) sur Internet : CQPICStart.zip. Il a été développé par Sumio Morioka (Japon) et publié en décembre 1999 dans "Transistor Gijutsu Magazine" (sa dernière mise à jour datait de 2004 avant la notre en 2010)

  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 le lien CQPICStart et l'ensemble des corrections qui utilisent mon site perso seront indisponibles à partir de cette date. SergeMoutou (discuter)

Nous allons commencer par présenter la partie hardware du cœur en nous basant sur la documentation de Microchip. Vous pouvez lire aussi comment démarrer avec un PIC16F84 ainsi qu'un cours complet de la wikiversité dédié à la programmation des 16F et 18F avec un peu d'assembleur et beaucoup de C.

Architecture du PIC® 16F84

modifier

Il s'agit d'un processeur 8 bits avec des instructions codées avec un opcode sur 14 bits. La version originale de ce PIC avait peu de RAM (68 octets). Une autre de ses limitations est la taille de sa pile : avec une pile de huit niveaux seulement, il doit être difficile de réaliser un compilateur C.

Ce processeur gère un certain nombre d'interruptions.

Nous allons commencer par présenter le plus difficile, l'architecture des registres mémoire (Register File dans la terminologie anglo-saxonne que l’on pourrait traduire par dossier/classeur de registres (j'ai trouvé aussi fichier de registres), mais nous préférons registres mémoire ou mémoire de registres ou banc de registres). Nous parlons de difficultés parce qu’il y a plusieurs banques mémoires ce qui est une caractéristique des architectures 8 bits Microchip (jusqu'aux séries 18FXXX) qui disparaissent seulement avec les architectures 16/32 bits (24FXXX et autres)

Architecture des registres SFR et de la mémoire

modifier

Les registres SFR (Registres internes à Fonctions Spéciales) sont répartis en deux banques. Cette partie de mémoire est suivie par 68 registres d’usage général pouvant être utilisés comme mémoire simple qui sont identiques sur les deux banques. Ensuite vient une série de registres généraux qui eux dépendent de la banque mémoire choisie et qui ne sont pas implémenté dans le 16F84 original.

La mémoire du 16F84 est organisée en banques sélectionnées par le(s) bit(s) RP0 (et RP1) du registre STATUS. Seul RP0 est utilisé pour le 16F84, soit deux banques.

RAM et Registres du 16F84
Adr. Banque0 Banque1 Adr.
00h Indirect addr. Indirect addr. 80h
01h TMR0 OPTION_REG
02h PCL PCL 82h
03h STATUS STATUS
04h FSR FSR 84h
05h PORTA TRISA
06h PORTB TRISB 86h
07h -- --
08h EEDATA EECON1 88h
09h EEADR EECON2
0Ah PCLATH PCLATH 8Ah
0Bh INTCON INTCON
0Ch – 4Fh 68 cases mémoires idem banque 0 8Ch – CFH
50h – 7Fh inutilisé inutilisé D0H – FFH

La première partie de cette mémoire (les registres) est parfois appelée banc de registres (ou fichier de registres). La partie marquée inutilisée est naturellement inutilisée dans le PIC16F84 original mais sera en fait utilisée par notre cœur CQPIC. Remarquez aussi l'adresse 07h et 87h inutilisée dans le PIC16F84 mais utilisée par notre cœur comme PORTC.

Vous pouvez lire aussi comment démarrer avec un PIC16F84 et/ou Utiliser les PIC 16F et 18F.

Les instructions

modifier
 

Cette section est difficile à comprendre. Même si elle ne fait intervenir que des notions du niveau indiqué, il est conseillé d'avoir du recul sur les notions présentées pour bien assimiler ce qui suit. Cependant, ce contenu n'est pas fondamental et peut être sauté en première lecture.

Il est temps de présenter maintenant l’ensemble des 35 instructions du PIC® 16F84 codées sur 14 bits. Vous trouverez en colonne de gauche le code binaire de l'instruction se trouvant au centre puis une courte explication de ce que fait l'instruction.

Les opérandes peuvent être de plusieurs types:

  • f : adresse mémoire de registres (register file address) de 00 à 7F
  • W : registre de travail
  • d : sélection de destination : d=0 vers W, d=1 vers f
  • pp : numéro de PORT entre 1 et 3 sur deux bits
  • bbb : adresse de bit dans un registre 8 bits (sur 3 bits)
  • k : champ littéral (8, ou 11 bits)
  • PC compteur programme
Jeu d'instructions 14-bit pour PIC 16F84
Opcode (binary) Mnemonic Description
00 0000 0xx0 0000 NOP Pas d'opération
00 0000 0110 0011 SLEEP arrête le processeur
00 0000 0110 0100 CLRWDT Reset du timer watchdog
00 1000 dfff ffff MOVF f,d Recopie de W dans f (adressage direct)
00 0000 1fff ffff MOVWF déplacement de W vers f
00 0001 0xxx xxxx CLRW Positionne W à 0 (idem à CLR x, W)
00 0001 1fff ffff CLRF f Positionne f à 0 (idem à CLR f, F)
00 0010 dfff ffff SUBWF f, d Soustrait W de (d = f − W)
00 0011 dfff ffff DECF f, d Décrément f (d = f − 1)
00 0100 dfff ffff IORWF f, d OU Inclusif W avec F (d = f OR W)
00 0101 dfff ffff ANDWF f, d ET entre W et F (d = f AND W)
00 0110 dfff ffff XORWF f, d OU Exclusif W avec F (d = f XOR W)
00 0111 dfff ffff ADDWF f, d Additionne W avec F (d = f + W)
00 1000 dfff ffff MOVF f, d recopie F (d = f)
00 1001 dfff ffff COMF f, d Complement f (d = NOT f)
00 1010 dfff ffff INCF f, d Incrément f (d = f + 1)
00 1011 dfff ffff DECFSZ f, d Decrément f (d = f − 1) et saut si zero
00 1100 dfff ffff RRF f, d Rotation droite F (rotation droite avec la retenue)
00 1101 dfff ffff RLF f, d Rotation gauche F (rotation gauche avec la retenue)
00 1110 dfff ffff SWAPF f, d échange de groupes 4-bit de f (d = lsb:f msb:f)
00 1111 dfff ffff INCFSZ f, d Incrément f (d = f + 1) et saut si zero
01 00bb bfff ffff BCF f, b RaZ d'un bit de f (Clear bit b of f)
01 01bb bfff ffff BSF f, b Mise à 1 d'un bit de f (Set bit b of f)
01 10bb bfff ffff BTFSC f, b test de bit de f, saute si zéro (Test bit b of f)
01 11bb bfff ffff BTFSS f, b test de bit de f, saute si un (Test bit b of f)
11 01xx kkkk kkkk RETLW k Positionne W à k et retour
10 0kkk kkkk kkkk CALL k Sauve l'adresse de retour, charge PC avec k
00 0000 0000 1001 RETFIE retour d'interruption
00 0000 0000 1000 RETURN retour de sous-programme
10 1kkk kkkk kkkk GOTO k saut à l'adresse k (9 bits!)
11 111x kkkk kkkk ADDLW k Addition de W et k
11 110x kkkk kkkk SUBLW k Soustraction de W et k
11 00xx kkkk kkkk MOVLW k Chargement littéral de W (W = k)
11 1000 kkkk kkkk IORLW k OU Inclusif littéral avec W (W = k OR W)
11 1001 kkkk kkkk ANDLW k ET littéral avec W (W = k AND W)
11 1010 kkkk kkkk XORLW k OU exclusif or littéral avec W (W = k XOR W)

Dans la terminologie Microchip, littéral signifie en adressage immédiat.

Vous pouvez lire aussi comment démarrer avec un PIC16F84 et/ou Utiliser les PIC 16F et 18F.

Architecture matérielle du PIC 16F84

modifier

L'architecture matérielle du PIC 16F84 est relativement simple : elle peut être présentée sous forme de schéma.

 
Architecture matérielle du PIC 16F84

Évidemment, une description matérielle de cette architecture en VHDL laisse de côté un certain nombre de fonctions comme la réalisation d'une horloge, la gestion analogique des ports avec la possibilité de résistances de tirages par exemple …

Notre cœur ou processeur softcore

modifier

Le cœur original CQPIC a été réalisé de manière à être le plus proche du composant PIC® 16F84 original. Voici maintenant une description du cœur sous forme de programme VHDL.

component piccore
	generic (
		STACK_SIZE	: integer;
		WDT_SIZE	: integer
	);
	port (
		progdata	: in std_logic_vector(13 downto 0);
		progadr		: out std_logic_vector(12 downto 0);
		ramdtin		: in std_logic_vector(7 downto 0);
		ramdtout	: out std_logic_vector(7 downto 0);
		ramadr		: out std_logic_vector(8 downto 0);
		readram		: out std_logic;
		writeram	: out std_logic;
		existeeprom	: in std_logic;
		eepdtin		: in std_logic_vector(7 downto 0);
		eepdtout	: out std_logic_vector(7 downto 0);
		eepadr		: out std_logic_vector(7 downto 0);
		readeepreq	: out std_logic;
		readeepack	: in std_logic;
		writeeepreq	: out std_logic;
		writeeepack	: in std_logic;
--> changed ver1.10a, 2010/05/13 (Serge moutou)
--		porta_in	: in std_logic_vector(4 downto 0);
--		porta_out	: out std_logic_vector(4 downto 0);
--		porta_dir	: out std_logic_vector(4 downto 0);
		porta_in	: in std_logic_vector(7 downto 0);
		porta_out	: out std_logic_vector(7 downto 0);
		porta_dir	: out std_logic_vector(7 downto 0);
--<
		portb_in	: in std_logic_vector(7 downto 0);
		portb_out	: out std_logic_vector(7 downto 0);
		portb_dir	: out std_logic_vector(7 downto 0);
		rbpu		: out std_logic;
--> added ver1.10b May 22 2010 (Serge Moutou)
		portc_in	: in std_logic_vector(7 downto 0);	-- PORT-C input data
		portc_out	: out std_logic_vector(7 downto 0);	-- PORT-C output data
		portc_dir	: out std_logic_vector(7 downto 0);	-- TRISC: PORT-C signal direction (H:input, L:output)
		portc_strobe: out std_logic;				-- PORT_C strobe
--<
		int0		: in std_logic;
		int4		: in std_logic;
		int5		: in std_logic;
		int6		: in std_logic;
		int7		: in std_logic;
		t0cki		: in std_logic;
		wdtena		: in std_logic;
		wdtclk		: in std_logic;
		wdtfull		: out std_logic;
		powerdown	: out std_logic;
		startclkin	: out std_logic;
		ponrst_n	: in std_logic;
		mclr_n		: in std_logic;
		clkin		: in std_logic;
		clkout		: out std_logic
	);
end component;

À noter que contrairement au PIC® 16F84, le composant CQPIC utilise une entrée spécifique "t0cki" pour le timer0 (pour le PIC® c’est le bit b4 T0CKl du PORTA). Il y a encore bien d'autres différences : comparez au brochage original du PIC® 16F84 décrit dans un autre projet qui fait apparaître 18 broches seulement.


Avant de passer aux périphériques nécessaires au bon fonctionnement, il nous faut aborder le problème très spécifique des ports.

Les deux ports du CQPIC

modifier

Nous avons fait des modifications sur la gestion des PORTS dans le cœur original qui seront présentées plus loin. Nous nous contenterons dans cette section de généralités.

Les ports sont des entités très spécifiques dans un micro-contrôleur car ils sont bidirectionnels. Ce côté bidirectionnel est difficile à implanter de manière générale dans un FPGA puisqu’il dépend du fabricant (du FPGA) et donc de l'environnement d'utilisation. Ainsi, la description VHDL des ports est laissée à l'utilisateur du cœur CQPIC.

Un coup d'œil sur le cœur nous montre que chaque port est décomposé en trois ports distincts. Par exemple, pour le PORTA on trouve porta_dir, porta_in et porta_out. Cette façon de faire est assez courante dans les "soft core" et j’ai déjà eu l’occasion de la rencontrer lors d'un projet précédent, le silicore1657. La bidirectionnalité des PORTs est gérée par un registre spécial TRIS associé à chacun des ports. Ces registres spécifiques existent dans le cœur, ils s'appellent porta_dir et portb_dir. Ils sont tous les deux sur 8 bits contrairement au PIC® 16F84 pour lequel le PORTA ne possède que 5 bits et peuvent être utilisés soit comme ports généraux en sortie, soit pour la gestion bidirectionnelle. Les ports proprement dit se décomposent alors en port d'entrée porta_in et portb_in ainsi qu'en ports de sortie porta_out et portb_out.




La description des PORTs dans cette section n’est pas complète et sera reprise quand nous aurons besoin de les relier à une logique externe.

Abordons maintenant les deux autres périphériques nécessaires, la RAM et la ROM. Ces deux composants ne font pas partie du cœur tout simplement parce que les implantations de ceux-ci sont spécifiques aux FPGA cibles.

Il existe peu de modèles de mémoire standard dans le monde des FPGA et ASIC. En fait le type le plus commun de mémoire que l’on peut trouver est ce que la norme WISHBONE appelle 'FASM', ou FPGA and ASIC Subset Model. Pour la RAM, sa spécificité est qu’il s'agit d'une mémoire à écriture et lecture synchrone. Il est facile de trouver des exemples d'une telle mémoire en VHDL. Nous avons choisi :

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity dataram is
   generic(
      ADDR_WIDTH: integer:=9;
      DATA_WIDTH: integer:=8
   );
   port(
      clk: in std_logic;
      write: in std_logic;
      read: in std_logic;
      addr: in std_logic_vector(ADDR_WIDTH-1 downto 0);
      datain: in std_logic_vector(DATA_WIDTH-1 downto 0);
      dataout: out std_logic_vector(DATA_WIDTH-1 downto 0)
    );
end dataram;

architecture RTL of dataram is
   type ram_type is array (2**ADDR_WIDTH-1 downto 0)
        of std_logic_vector (DATA_WIDTH-1 downto 0);
   signal ram: ram_type;
begin
   process (clk)
   begin
      if (clk'event and clk = '1') then
         if (write='1') then
            ram(to_integer(unsigned(addr))) <= datain;
          end if;
          addr_reg <= addr;
      end if;
   end process;
	dataout <= ram(to_integer(unsigned(addr_reg)));
end RTL;

La gestion de la RAM dans un système mono-puce n’est pas un problème aussi simple qu’il n'y parait. Examinons de plus près ce problème et ce qui fait sa difficulté.

Un coup d'œil sur le programme ci-dessus nous indique immédiatement que les adresses mémoires sont sur 9 bits alors que la documentation du PIC16F84 donne des plages inutilisées à partir de FFh soit sur 8 bits. En VHDL, donner plus de mémoire à un cœur se fait très facilement s'il y a de la place dans le FPGA pour cela. Comme nous avons décidé d’utiliser un compilateur C, notre problème est de savoir comment faire comprendre au compilateur C que l’on dispose de plus de mémoire que prévu ?

C'est là que se situe le programme à exécuter. Puisque tout compilateur ou assembleur génère un fichier hex, il nous est nécessaire de transformer ce fichier hex en fichier VHDL. Ceci est laissé à un programme C décrit un peu plus loin. Commençons à présenter un exemple de ROM.

-- This file was generated with hex2rom written by Daniel Wallner 
library IEEE; 
use IEEE.std_logic_1164.all; 
use IEEE.numeric_std.all; 

entity progrom is 
	port( 
		Clk	: in std_logic; 
		romaddr	: in std_logic_vector(12 downto 0); 
		romout	: out std_logic_vector(13 downto 0) 
	); 
end progrom; 

architecture rtl of progrom is 
	signal A_r : std_logic_vector(12 downto 0); 
begin 
	process (Clk) 
	begin 
		if Clk'event and Clk = '1' then 
			A_r <= romaddr; 
		end if; 
	end process; 
	process (A_r) 
	begin 
		case to_integer(unsigned(A_r)) is 
		when 000000 => romout <= "00000100101000";	-- 0x0000 
		when 000001 => romout <= "00001100010110";	-- 0x0002 
		when 000002 => romout <= "11111100110000";	-- 0x0004 
		when 000003 => romout <= "00010100000000";	-- 0x0006 
		when 000004 => romout <= "00001100010000";	-- 0x0008 
		when 000005 => romout <= "00000000110000";	-- 0x000A 
		when 000006 => romout <= "00001100011000";	-- 0x000C 
		when 000007 => romout <= "00000100110000";	-- 0x000E 
		when 000008 => romout <= "00011000000000";	-- 0x0010 
		when 000009 => romout <= "00001100010010";	-- 0x0012 
		when 000010 => romout <= "00010100001000";	-- 0x0014 
		when 000011 => romout <= "00011000000000";	-- 0x0016 
		when 000012 => romout <= "00100100101000";	-- 0x0018 
		when 000013 => romout <= "00000000101000";	-- 0x001A 
		when others => romout <= "--------------"; 
		end case; 
	end process; 
end;

Cet exemple montre un contenu mais chaque programme donnera une ROM différente. Il est à noter que ce que l’on présente est du VHDL alors que chaque compilateur ou assembleur ne délivre qu'un fichier au format HEX. Il faudra donc un utilitaire pour transformer le fichier HEX en fichier VHDL car il s'agit d'une opération qui peut se se faire automatiquement. Nous utiliserons un programme en C++ que nous donnons en annexe 1 et qui se trouve dans le répertoire "\CQPICStart\ROM" du fichier CQPICStart.zip.

Nos premiers programmes en C

modifier

Nous n'allons pas présenter le langage C maintenant puisque d'autres projets le font :

La programmation du cœur se fait à l'aide de l'environnement MPLAB. Ce n’est pas le seul environnement de développement pour les PICs, mais c’est probablement un des plus utilisés et pour ne rien gâcher, il est gratuit. Le téléchargement de MPLAB nous permet d’utiliser l'assembleur ainsi que le compilateur C HiTech. Puisque nous avons décidé pour ce projet de n'utiliser que des outils gratuits, le compilateur C n’est pas optimisé, pour cela il faut payer.


Notre premier programme en C a été réalisé pour tester le fonctionnement du cœur CQPIC sur notre carte d'application Digilent.

Notre architecture CQPIC étant une architecture 8 bits avec peu de mémoire RAM, il peut sembler intéressant de commencer par présenter un programme en C mais qui ne contient que de l'assembleur.

Le premier programme simple en C avec assembleur

modifier

Le programme présenté montre comment inclure de l'assembleur dans un programme C.

#include <pic1684.h> 
//#include <htc.h> serait-il mieux ?
main(void) 
{ 
#asm 
Start: bcf _STATUS,5 ; select memory bank 0 
movf _PORTA, W ;read PORTA 
bsf _STATUS,5 ; select memory bank 1 
movwf _TRISA ;Recopie de PORTA 
goto Start 
#endasm 
}

Ne prenez pas ce programme à la lettre. Il est donné pour exemple mais fonctionne de manière surprenante.

Le premier programme simple en C pur

modifier

Un programme tout simple de test est présenté maintenant : il s'agit tout simplement de recopier le PORTA sur le PORTB. Remarquons aussi que ce programme fonctionne complètement si l’on a pris soin de prendre la version que j’ai réalisée.

#include <pic1684.h> 
//#include <htc.h> serait-il mieux ?
main(void) 
{ // La gestion de TRISA et TRISB semble importante dans ce cœur
  TRISA = 0xFF; // 8 entrees pour A 
  TRISB = 0x00; // 8 sorties pour B 
  while(1) 
  PORTB = PORTA; // recopie du PORTA dans le PORTB qui allume les LEDs	 
}

Contrairement au Silicore1657 déjà évoqué (projet de l'année précédente), le cœur CQPIC dispose du mécanisme d'interruption que nous allons détailler maintenant avec un exemple simple.

Interruption

modifier

Pour tester plus en avant notre cœur CQPIC, je décide d’utiliser une interruption. La plus simple des interruptions à mettre en œuvre étant celle du timer0, nous allons examiner maintenant la documentation correspondante.

La documentation du timer0

modifier

Nous présentons maintenant le schéma complet correspondant à ce timer0.

 
Le timer 0 du 16F84

Comme on peut le voir dans la documentation schématique, la gestion du timer0 est essentiellement réalisée par le registre OPTION. Seuls les trois bits de poids faibles de ce registre sont à positionner à 1 pour avoir une division par 256. Comme on le verra dans le programme, cette division ne suffit pas il faudra encore lui ajouter une division par 16 dans l'interruption même, pour que notre chenillard soit visible.

Mettre en route le timer0 est une chose, mais déclencher une interruption en est une autre. Il nous faut maintenant documenter ce mécanisme.

 
L'interruption du timer 0 dans le 16F84

Pour réaliser l'interruption, il faut réaliser un front montant dans l'ellipse rouge : on en déduit immédiatement les bits à mettre à 1 dans INTCON, à savoir T0IE et GIE.


Le programme

modifier

Le programme correspondant est donné maintenant.

#include <pic1684.h> 
void interrupt decalage(void);

unsigned char nb; 
main(void) 
{ 
  TRISA = 0xF9; // 6 entrees, 2 sorties pour A 
  TRISB = 0x00; // 8 sorties pour B 
  OPTION = 0x07; // prescaler 256 , entree sur quartz 
  INTCON = 0xA0;  // autorise l'interruption timer 
  PORTB = 0x01; // une seule diode allumee		 
  TMR0 = 0x00 ; 
  nb=0; 
  while(1) 
    { 
   // on ne fait rien que recopier sur 2 segments la valeur de SW1 
      if ((PORTA & 0x01) == 1) PORTA = 0x06; 
    } 
} 

void interrupt decalage(void) 
{ 
  nb++; 
  //TMR0 = 0x00; //c'est fait car overflow
  if (!(nb % 16)) 
  PORTB = (PORTB << 1) ; 
  if (PORTB == 0x00)   PORTB = 0x01; 
  T0IF = 0;  // acquittement interruption 
}

Remarquez le "if (!(nb % 16))" qui réalise la division par 16 dans l'interruption. C'est absolument nécessaire pour notre cœur qui fonctionne à 50 Mhz pour voir les LEDs allumées se déplacer.


Il est maintenant grand temps de revenir sur notre problème original, à savoir interfacer un écran VGA.

Assemblage du cœur, des mémoires et du module VGA

modifier
  Avant d'aller plus loin, relisez Interfaces_VGA_et_PS2.

À la fin de ce précédent chapitre Interfaces VGA, nous avons présenté un module capable de dessiner une balle et deux raquettes, ces trois objets étant mobiles.

Utile aussi de lire ou de relire l'article sur le VGA.

Le problème des PORTs

modifier

Nous avons besoin de deux fois 10 bits pour piloter nos coordonnées de balles. Une autre façon de dire les choses est qu’il nous faut 4 ports de 8 bits en sortie. Or, nous ne disposons que de deux PORTs de 8 bits en sortie (et encore parce que j’ai étendu le PORTA originel. C'est insuffisant. Il nous faut donc réfléchir sur la façon dont on va pouvoir connecter toutes ces données en sortie.

Pourquoi seulement deux PORTs en sortie ? L'entité du piccore présentée en section précédente ne montre-t-elle pas porta_dir et portb_dir en sortie ?

La modification du cœur CQPIC pour augmenter le nombre de bits du PORTA, m'a amené à lire son code VHDL et je suis tombé sur :

-- VHDL
for I in 0 to 7 loop 
  if (trisa_reg(I) = '1') then 
    ramin_node(I) := portain_sync_reg(I);-- PORT A (when input mode) 
  else 
    ramin_node(I) := portaout_reg(I);-- PORT A (when output mode) 
  end if; 
end loop;

Ce code montre que TRISA est pris en compte en interne dans le cœur CQPIC. Ceci peut sembler normal, mais n’est pas conforme à la seule expérience que nous avions avec le Silicore1657 : puisque TRISA était sorti à l'extérieur il pouvait servir de PORT supplémentaire. Si vous avez l’idée d’utiliser TRISA comme PORT supplémentaire ici, vous aurez un fonctionnement étrange d'un programme C :

//langage C 
TRISA = PORTA ;

Physiquement, on relie PORTA (ra_out) à des interrupteurs et TRISA (ra_dir) à des LEDs. Si PORTA est à OxFF (par les interrupteurs extérieurs), tout interrupteur mis à 0 éteindra correctement la LED correspondante (reliée à TRISA). Mais l'inverse ne marche plus : si ce même interrupteur est remis à 1 on n'allume plus la LED correspondante.


Comment contourner cet obstacle ?

Ajouter un PORTC dans le CQPIC

modifier

(Le cœur correspondant se trouve dans le répertoire "\CQPICStart\PORTC" du fichier CQPICStart.zip).

Après avoir passé plusieurs heures à étendre à 8 bits le PORTA (au lieu des 5 initiaux), nous avons repéré à peu près tous les endroits à modifier pour ajouter un PORTC au CQPIC. Le jeu en vaut-il la chandelle ? Cela ne peut pas suffire à résoudre nos problèmes, puisqu'on aura alors en sortie que trois PORTs sur les huit qu’il nous faut. Nous avons finalement réalisé ce changement dans le cœur et en avons profité pour ajouter un signal "strobe" qui accompagne ce port supplémentaire (voir section suivante). C'est ce signal strobe qui va nous permettre de résoudre notre problème d'extension du nombre de PORTs assez facilement.

Notez que cette solution impose de déclarer PORTC et TRISC dans les programmes C. Je déconseille une modification du fichier pic1684.h Faites plutôt comme ceci :

#include <pic1684.h> // ou #include <hct.h>
volatile unsigned char	PORTC	@ 0x07; 
volatile unsigned char TRISC	@ 0x87;

C'est simple, mais cela ne permet pas un accès à des bits individuels (mais nous n'avons pas besoin de cela dans notre projet).

Approfondissons cette solution.

Ajouter un "strobe" au PORTC et augmenter le nombre de ports

modifier

Un signal "strobe" est une sortie qui indique une écriture dans le PORTC. Ajouter ce signal strobe a été en fait le plus délicat du travail. L'intérêt d'un tel signal est de permettre facilement la démultiplication des PORTs (jusqu'à 256). Nous présentons ci-dessous un exemple qui montre comment étendre à huit le nombre de PORTs. On met la valeur du port véritable de destination dans PORTA puis on met dans PORTC la valeur à écrire : le "strobe" lui sera alors envoyé et il recopiera ce qu’il y a dans PORTC.

 
Comment ajouter des PORTs avec le signal "strobe"

Il est peut être plus facile de raisonner à partir de ce schéma. Le PORTC (a gauche) est relié à tous les ports ajoutés (à droite) et la logique de décodage envoie le strobe dans le port qui doit recopier ce qu’il y a dans PORTC.


Le programme VHDL correspondant au schéma de la page précédente est présenté maintenant. Il comporte deux entrées PORTA et PORTC pour réaliser quatre ports de sorties pA, pB, pC, pD.

-- increase the number of available ports with the new portC and its strobe 
-- ver1.10b, 2010/05/22 (Serge Moutou)	 
library ieee; 
use ieee.std_logic_1164.all; 
entity ports is 
	port( 
	 clk : in std_logic; 
	 strobeC_in : in std_logic; 
	 portA : in std_logic_vector(7 downto 0); 
	 portC : in std_logic_vector(7 downto 0); 
	 pA,pB,pC,pD : out std_logic_vector(7 downto 0) --new ports 
	); 
end ports; 
architecture BehPorts of ports is 
signal Internal_strobes : std_logic_vector(7 downto 0); 

begin	 
  dmux_Strobe:process(portA,strobeC_in) begin 
    case portA is 
	when "00000001" => Internal_strobes(0) <= strobeC_in; 
        when "00000010" => Internal_strobes(1) <= strobeC_in; 
        when "00000100" => Internal_strobes(2) <= strobeC_in; 
	when "00001000" => Internal_strobes(3) <= strobeC_in; 
	when others => internal_strobes <= (others =>'0'); 
	end case; 
  end process;		 
  
  port_A:process(clk)begin 
    if clk'event and clk='1' then 
	 if Internal_strobes="00000001" then 
		 pA<=portC; 
		end if; 
    end if; 
  end process;	 
  port_B:process(clk)begin 
    if clk'event and clk='1' then 
      if Internal_strobes="00000010" then 
		 pB<=portC; 
		end if; 
    end if; 
  end process; 
  port_C:process(clk)begin 
    if clk'event and clk='1' then 
      if Internal_strobes="00000100" then 
		 pC<=portC; 
		end if; 
    end if; 
  end process; 
  port_D:process(clk)begin 
    if clk'event and clk='1' then 
      if Internal_strobes="00001000" then 
		 pD<=portC; 
		end if; 
    end if; 
  end process;	  
end BehPorts;

Remarquez qu'on a utilisé des "process" au lieu de "components" pour simplifier le programme.

Exercice

modifier

Le programme VHDL ci-dessus permet de gérer 4 ports à partir de deux initiaux : le PORTA et le PORTC. Modifier ce programme VHDL pour qu’il gère les 6 ports en sortie conformément à la figure ci-dessus.

Arrivé à ce point, il nous faut certainement donner une image simple de ce que l’on cherche à faire.

Nous allons traiter ce problème en commençant à gérer la balle seulement puis en ajoutant les raquettes.

Programmation du nouveau cœur avec écran VGA

modifier

(L'ensemble se trouve dans le répertoire "\CQPICStart\VGA_Pong" du fichier CQPICStart.zip). Nous avons déjà présenté quelques programmes en C, mais nous allons examiner ce qu’il faut changer dans ces programmes pour gérer les nouveaux PORTs.

Programmation en C

modifier

Nous commençons par présenter un sous-programme qui écrit une valeur 16 bits dans deux des nouveaux PORTs.

Le sous-programme "setX"

modifier

Une version en C pur est montrée ci-dessous :

void setX(unsigned int x){ 
  TRISA=0x00;TRISC=0x00; 
  PORTA=1; 
  PORTC=x; //poids faible 
  PORTA=2; 
  PORTC=x>>8;//poids fort 
}

Le contenu de ce programme est complètement déterminé par la partie matérielle.

Déplacement horizontal en continu de notre balle/rectangle

modifier

Nous allons présenter maintenant un programme fonctionnel qui déplace sans arrêt le rectangle sur une horizontale.

#include <pic1684.h>
volatile unsigned char	PORTC	@ 0x07; 
volatile unsigned char TRISC	@ 0x87; 
void setX(unsigned int x); 
void setY(unsigned int y); 
void wait(int tempo); 

void main(){ 
int i; 
	while(1){	 
		setY(321); 
		for(i=0;i<600;i++){ 
			setX(i); 
			wait(255); 
			wait(255); 
		} 
	} 
} 

void setX(unsigned int x){
  TRISA=0x00;TRISC=0x00;  
  PORTA=1; //poids faible
  PORTC=x;
  PORTA=2;//poids fort
  PORTC=x>>8;
} 

void setY(unsigned int y){ 
  TRISA=0x00;TRISC=0x00; 
  PORTA=4; //poids faible
  PORTC=x;
  PORTA=8;//poids fort
  PORTC=x>>8;
} 

void wait(unsigned char tempo){ 
OPTION=0x07; // div 256 et source=quartz 
TMR0 =0; 
while(TMR0<tempo); 
}

Remarquez l’utilisation du timer0 sans interruption pour faire la temporisation.

Programme complet de gestion simple de la balle

modifier

Nous présentons maintenant un programme simple de gestion de la balle avec des rebonds :

#include <pic1684.h> 
volatile unsigned char	PORTC	@ 0x07; 
volatile unsigned char TRISC	@ 0x87; 
void setX(unsigned int x); 
void setY(unsigned int y); 
void wait(unsigned char tempo); 
void main(){ 
int posX=0,posY=0; 
signed char deltaX=1,deltaY=1;
	while(1){	 
		if ((posX>=620) && (deltaX>0)) deltaX= -deltaX; 
		if ((posX<=40) && (deltaX<0)) deltaX= -deltaX; 
		posX=posX+deltaX; 
		setX(posX); 
		if ((posY>=460) && (deltaY>0)) deltaY= -deltaY; 
		if ((posY<=10) && (deltaY<0)) deltaY= -deltaY; 
		posY=posY+deltaY; 
		setY(posY); 
		wait(250); 
		wait(250);
	} 
} 

void setX(unsigned int x){ 
  TRISA=0x00;TRISC=0x00; 
  PORTA=1; //poids faible
  PORTC=x;
  PORTA=2;//poids fort
  PORTC=x>>8;
} 

void setY(unsigned int y){ 
  TRISA=0x00;TRISC=0x00; 
  PORTA=4; //poids faible
  PORTC=x;
  PORTA=8;//poids fort
  PORTC=x>>8;
} 

void wait(unsigned char tempo){ 
OPTION=0x07; // div 256 et source=quartz 
TMR0 =0; 
while(TMR0<tempo); 
}

L'inconvénient de ce programme est le petit nombre des trajectoires gérées, ce qui peut devenir lassant pour un jeu.

Ajouter les bords, et les raquettes

modifier

Comme nous voulons deux raquettes, il nous faut deux coordonnées verticales sur 9 bits (soit 4 ports 8 bits). Nous voulons aussi deux coordonnées de balles soit encore 4 ports de 8 bits. Cela fait en tout 8 ports. Nous avons déjà eu l’occasion de montrer comment on pouvait augmenter le nombre de PORTs.

Solution simple sans bord

modifier

Nous avons l'intention dans cette section de mettre en œuvre le programme VHDL de la section Ajouter un "strobe" au PORTC et augmenter le nombre de ports. Puisque ce programme VHDL ne proposait que quatre PORTs de sortie nous allons présenter un ensemble fonctionnant mais avec des positions de raquette fixes. Si l’on numérote les quatre ports de sortie de 0 à 3 on choisit

choix technologiques
E/S module VGA E/S CQPIC
x_rect<9:8> PORT1<1:0>
x_rect<7:0> PORT0<7:0>
y_rect<9:8> PORT3<1:0>
y_rect<7:0> PORT2<7:0>

Pour pouvoir gérer des rectangles de tailles différentes et de couleur différentes, on complique un peu la partie destinée à dessiner un rectangle en lui donnant une couleur de rectangle une largeur et une hauteur. Voici donc notre nouveau composant :

COMPONENT rect IS PORT( 
  row,col,x_rec,y_rec,delta_x,delta_y :in STD_LOGIC_VECTOR(9 DOWNTO 0); 
  colorRGB : in  STD_LOGIC_VECTOR(2 DOWNTO 0); 
  red1,green1,blue1 : out std_logic); 
END component;

L'intanciation des rectangles pour la balle et les raquettes se fera alors de la manière suivante :

balle:rect port map(row=>srow, col=>scol,r ed1=>sred, green1=>sgreen, blue1=>sblue, colorRGB=>"111", delta_x=>"0000001010", delta_y=>"0000001100", 				x_rec => x_rect, y_rec => y_rect); 
raquetteG:rect port map(row=>srow, col=>scol, red1=>sred1, green1=>sgreen1,
         blue1=>sblue1, colorRGB=>"100", delta_x=>"0000001010",
         delta_y=>"0000111010", 	x_rec => "0000010110", 
         y_rec(8 downto 1) => y_raquG, y_rec(9)=>'0',y_rec(0)=>'0');	 
raquetteD:rect port map(row=>srow, col=>scol, red1=>sred2, green1=>sgreen2,
          blue1=>sblue2,colorRGB=>"100", delta_x=>"0000001010",
          delta_y=>"0000111010", x_rec => "1001001000", 
          y_rec(8 downto 1) => y_raquD,y_rec(9)=>'0',y_rec(0)=>'0');							 
  red <= sred or sred1 or sred2; 
  green <= sgreen or sgreen1 or sgreen2; 
  blue <= sblue or sblue1 or sblue2;

Les déclarations des signaux dans ce morceau de programme sont omises. Voici maintenant le programme C permettant le rebond sur les raquettes. Vous pouvez remarquer que les coordonnées des raquettes sont calculées en fonction des interrupteurs sur le PORTB, mais ne sont pas mises à jour dans le matériel puisque le matériel ne peut pas le faire.

//#include <pic1684.h> // Programme pour Hitech C dans MPLAB
#include <htc.h> //***** Hitech C *******
volatile unsigned char	PORTC	@ 0x07; 
volatile unsigned char TRISC	@ 0x87; 
void setX(unsigned int x); 
void setY(unsigned int y); 
void wait(unsigned char tempo); 
unsigned int posRaqu_16; 
void main(){ 
int posX,posY; 
unsigned char raqD_y=0,raqG_y=0; 
signed char deltaX=1,deltaY=1; 
	while(1){	 
		posX=113;		 
		posY=101; 
		setX(posX); 
		setY(posY); 
		while(RB2==0); // attente départ 
		while( (posX>30) && (posX<580)){ 
			posRaqu_16=raqD_y<<1; 
			if ((posX>=574) && (posY<posRaqu_16+58) &&
                                                 (posY+10 > posRaqu_16) && (deltaX>0)) deltaX= -deltaX; 
			posRaqu_16=raqG_y<<1; 
			if ((posX<=32) && (posY<posRaqu_16+58) &&
                                                (posY+10 > posRaqu_16) && (deltaX<0)) deltaX= -deltaX; 
			posX=posX+deltaX; 
			setX(posX); 
			if ((posY>=460) && (deltaY>0)) deltaY= -deltaY; 
			if ((posY<=10) && (deltaY<0)) deltaY= -deltaY; 
			posY=posY+deltaY; 
			setY(posY); 
// gestion des raquettes {{Unité|2|bits}} PORTB/raquette 
		 if (RB6) if (raqG_y<215) raqG_y++; 
			if (RB7) if (raqG_y>0) raqG_y--; 
		//PORT4=raqG_y; 
			if (RB0) if (raqD_y<215) raqD_y++; 
			if (RB1) if (raqD_y>0) raqD_y--; 
		//PORT5=raqD_y; 
			wait(250); 
			wait(250); 
		} 
	} 
}

Comme on peut le voir nous avons choisi le PORTB connecté aux interrupteurs sw0,sw1,sw6 et sw7 de la carte, pour faire descendre et monter les raquettes et sw2 (RB2) pour le départ.

Travail à réaliser

modifier

Analyse

modifier

Comprendre la partie matérielle. Vous devez être capable de dessiner les composants et leurs liaisons à partir des fichiers VHDL donnés dans le répertoire "\CQPICStart\VGA_Pong" du fichier CQPICStart.zip.

Synthèse

modifier

Étendre la partie matérielle pour gérer le déplacement des deux raquettes. On choisira une coordonnée Y de chacune des raquettes, au choix, sur 8 bits (comme dans le projet silicore1657 de l'année dernière) ou sur 9/10 bits. Pour un choix sur 8 bits on pourra prendre :

choix technologiques
E/S module VGA E/S CQPIC
x_rect<9:8> PORT1<1:0>
x_rect<7:0> PORT0<7:0>
y_rect<9:8> PORT3<1:0>
y_rect<7:0> PORT2<7:0>
y_raqG<7:0> PORT4<7:0>
y_raqD<7:0> PORT5<7:0>

Programmation

modifier

Explorer s'il n’est pas possible d’utiliser l'algorithme de tracé de segment de droite Bresenham pour les trajectoires de balles. Cet algorithme est expliqué dans le WIKI : Algorithme de tracé de segment de Bresenham. Il est possible de trouver directement une version en C : tapez Bresenham en C dans google.

Synthèse matérielle et programmation

modifier

Gérer un affichage des scores dans la partie basse de l'écran, soit avec une mémoire de caractères, soit avec des afficheurs sept segments à construire.

 
Gestion du jeu de Pong sur un écran VGA

Voila ce que cela donne avec un affichage sept segments simulé.

Annexe 1

modifier

Le programme C++ pour transformer le fichier HEX en fichier VHDL n’est pas donné car un petit peu trop long. Il se trouve dans le répertoire "\CQPICStart\ROM" du fichier CQPICStart.zip

  Ce n’est pas le fichier original hex2vhd.c proposé avec le cœur CQPIC qui est utilisé. C'est le fichier hex2rom.cpp présent dans un cœur concurrent, à savoir le cœur PPX. Ce fichier a été légèrement modifié pour que le programme généré puisse aller directement dans notre projet VHDL.

Un fois compilé mon utilitaire s’appelle hex2rom.exe (sous windows) ou hex2rom sous Linux. L'utilisation est faite par la ligne de commande :

	hex2rom demo16F84.hex progrom 13l14s >progrom.vhd

si le fichier à convertir s’appelle demo16F84.hex.

Le fichier généré s’appelle progrom.vhd et a "progrom" comme nom d'entité. 13 est le nombre de bit d'adresses, l désigne le fait que l’on utilise la convention little indian (b désignerait big indian) et 14 est le nombre de bits de données dans la ROM.

Si l’on utilise le fichier VHDL directement sans modifier les options de synthèse, nous nous trouvons avec de la ROM distribuée. C'est à vous de décider si c’est grave ou pas.

 
Les blocs mémoires distribués utilisés dans le FPGA

L'ensemble des blocs utilisés se trouve en blanc et cet exemple montre bien pourquoi on appelle cela de la RAM distribuée (points blancs répartis dans le FPGA). Les blocs RAM (rectangles rouges) sont donc inutilisés (en fait ils sont partiellement utilisés par la RAM du 16F84). Vous pourrez noter que cette façon de faire est très consommatrice de ressources.

La différence entre RAM distribuée et bloc RAM (BRAM) est expliquée dans un autre projet.


Annexe II : le fichier ucf

modifier

Le fichier ucf dépend à la fois du FPGA utilisé et de la carte de développement.

#PORTB sur leds
net "rb0_out" loc="K12";
net "rb1_out" loc="P14";
net "rb2_out" loc="L12";
net "rb3_out" loc="N14";
net "rb4_out" loc="P13";
net "rb5_out" loc="N12";
net "rb6_out" loc="P12";
net "rb7_out" loc="P11";
#PORTB sur interrupteurs
net "rb_in<7>" loc="k13"; 
net "rb_in<6>" loc="k14"; 
net "rb_in<5>" loc="j13"; 
net "rb_in<4>" loc="j14"; 
net "rb_in<3>" loc="h13"; 
net "rb_in<2>" loc="h14";  
net "rb_in<1>" loc="g12"; 
net "rb_in<0>" loc="f12";
# clock
net "clkin" loc="T9";
net "clkin" TNM_NET = "clkin";
TIMESPEC "TS_mclk" = PERIOD "clkin" 20 ns HIGH 50 %;
# reset
net "mclr_n" loc="M13"; 
net "ponrst_n" loc="M14";
#VGA
net "hsynch" loc="R9";
net "vsynch" loc="T10";
net "red" loc="R12";
net "blue" loc="R11";
net "green" loc="T12"; 	

Ce fichier ucf fonctionne correctement avec la version 9.2 de l'IDE mais la version 11.1 nécessite le rajout de :

NET "rb_in<0>" CLOCK_DEDICATED_ROUTE = FALSE;
NET "rb_in<4>" CLOCK_DEDICATED_ROUTE = FALSE;
NET "rb_in<5>" CLOCK_DEDICATED_ROUTE = FALSE;
NET "rb_in<6>" CLOCK_DEDICATED_ROUTE = FALSE;
NET "rb_in<7>" CLOCK_DEDICATED_ROUTE = FALSE;

en tout début de fichier.

Voir aussi

modifier