Very High Speed Integrated Circuit Hardware Description Language/Embarquer un Atmel ATMega8
Ce projet consiste à développer un jeu de pong sur un écran VGA dans un FPGA. C'est un sujet assez classique pour lequel plusieurs versions existent sur Internet. Ce qui fait son originalité est l’utilisation d'un cœur (libre) de micro contrôleur 8 bits en interaction avec de la logique externe réalisée en VHDL. Le processeur soft core sera appelé CoreAtMega8 dans la suite de ce document et a été conçu par Juergen Sauermann. Il est compatible avec un AVR ATMEGA8® de chez Atmel. La programmation du cœur devra si possible, se faire en langage C.
Pour accompagner ce document, télécharger le fichier : ATMega8_pong_VGA.zip. En effet, nous avons préparé ce fichier pour faciliter le démarrage de projets pour des étudiants. Les choses ne se passant pas toujours comme prévu aujourd’hui ce fichier dispose des répertoires suivants :
- "/AVRComplet16_S4_S4/25MHzAVR/" qui contient la version complète fonctionnant à 25 MHz (à partir d'une horloge 50 MHz), gérant correctement les interruptions avec une compilation utilisant le modèle ATMega8 qui permet de démarrer n’importe quel projet,
- "/CorrProjet2010/" qui contient la correction complète de ce chapitre mais avec une version antérieure du cœur.
Le plus formateur me semble cependant de commencer par télécharger le code de départ chez OpenCore à partir de SVN (et non le fichier zippé) pour avoir la toute dernière version des fichiers.
Dans une console, lancer la commande :
svn export http://opencores.org/ocsvn/cpu_lecture/cpu_lecture/trunk/ cpu_lecture
qui créera le répertoire cpu_lecture et y déposera la dernière version. Pour réaliser cela il faut s'inscrire chez opencores puisque l’on vous demandera un mot de passe.
La version OpenCore ne contient pas l'interface avec l'écran VGA.
Le contenu de ce paragraphe peut sembler redondant avec les chapitres silicore1657 et CQPIC 16F84. Je rappelle que tous ces chapitres sont des projets qui font exactement la même chose, gérer un jeu de pong, d'où cette redondance. Il vous est demandé de choisir votre architecture et de lire le chapitre correspondant. Désolé, mais il en faut pour tous les goûts !
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 resources et corrections de ce chapitre seront indisponibles à partir de cette date. SergeMoutou (discuter) |
Choix du cœur
modifierIl existe plusieurs versions de descriptions de cœurs compatibles avec les RISC de chez Atmel sur Internet. Nous avons tendance à choisir les cœurs de processeur en fonction de l'épaisseur de leur documentation. Cela avait déjà été le cas avec le cœur de processeur PIC16C57 l'année précédente (voir autre chapitre de ce cours, ou le rapport complet SiliCore1657.pdf).
Même s'il existe d’autre cœurs concurrents chez Opencores AVR core ou encore un autre Atmel AVR ATtiny261/461/861, nous avons choisi celui-ci car il est accompagné d'un cours. Le CoreAtMega8 est un cœur de processeur (ou processeur softcore) compatible avec le ATMEGA8® de chez Atmel. Il a été développé par Dr. Juergen Sauermann (Allemagne) et publié en janvier 2010 chez OpenCore sous forme d'un cours pour apprendre à développer un "soft processor" en VHDL (CPU lecture).
Un cœur bien réalisé doit proposer tout sauf la ROM la RAM et les PORTs parce que leur implémentation est très dépendante du fabricant du FPGA ciblé. Il n'y a, en effet, aucune norme pour implanter ceux-ci. Ici ce n’est pas le cas.
Ce cœur est réalisé avec des mémoires Xilinx, c'est-à-dire que dans son code source vous avez des éléments de mémoire qui ne pourront pas être compilés tels quels pour Altera. Cela ne veut pas dire que c’est impossible. Cela veut dire qu’il y a quelques petits changements à réaliser.
Nous allons commencer par présenter la partie hardware du cœur en nous basant sur la documentation de Atmel.
Architecture d'ATMEGA8® de chez Atmel
modifierIl s'agit d'un processeur 8 bits avec des instructions codées sur 16 bits ou 32 bits. Ce processeur gère un certain nombre d'interruptions.
Architecture des registres mémoires
modifierIl existe 3 espaces distincts en RAM :
- Les 32 premières adresses correspondent aux 32 registres. Ces registres sont directement reliés à l’UAL et tous les calculs ne peuvent être effectués qu’à partir de ces registres (addition, soustraction, multiplication, opérations logiques, tests ).
- L'espace IO de l'adresse 0x20 à 0x5F. Dans cet espace se trouvent beaucoup de registres de gestion du matériels (les ports, Timers, UART,...). Pour accéder à ces registres les instructions in et out utilisent l'adresse 0 pour le premier registre et non l'adresse 0x20. Les instructions cli et sbi permettent de mettre à 0 ou à 1 un bit d'un registre
- Enfin à partir de l'adresse 0x60 se trouve la mémoire à proprement parler (le ATMEGA8 possède 1ko de RAM). Pour accéder à tout l'espace mémoire (Registre et zone I/O incluse) les instructions ST,LD et STS et LDS peuvent être utilisées. L'adressage peut être direct (STS ou LDS) ou indirect (ST et LD). Un adressage indirect utilise les registres X, Y ou Z (respectivement les registres R27:R26, R29:R28 et R31:R30) pour pointer sur une zone mémoire.
Les deux premiers espaces sont désignés en anglais par "register file". On utilisera indistinctement : mémoire de registres, fichier de registres et banc de registres pour les désigner en français.
Quelques registres importants du banc de registres
modifierCe tableau (partiel) des registres respecte les fichiers d'inclusion du compilateur avr-gcc. Par exemple, dans la documentation officielle, le bit b0 du PORTB s’appelle PORTB0 tandis que dans le fichier d'inclusion du GNU-C il s’appelle PB0.
Même si une majorité de ces registres sont implantés dans notre architecture, ce n’est pas le cas pour tous :
- les registres gris clair sont implantés dans notre architecture initiale, celle que l’on télécharge chez OpenCores
- les registres un peu plus foncés seront implantés dans le projet PONG de ce chapitre
- les registres blancs ne seront pas implantés dans ce chapitre
Addr | Name | Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
0x3F(0x5F) | SREG | I
|
T
|
H
|
S
|
V
|
N
|
Z
|
C
|
0x32(0x52) | TCNT0 | Timer0 8 bits
| |||||||
0x2D(0x4D) | TCNT1H | Timer1 8 bits de poids fort
| |||||||
0x2C(0x4C) | TCNT1L | Timer1 8 bits de poids faible
| |||||||
0x23(0x43) | OCR2 | Timer/Counter2 output compare register
| |||||||
0x21(0x41) | WDTCR | -
|
-
|
-
|
WDCE
|
WDE
|
WDP2
|
WDP1
|
WDP0
|
0x20(0x40) | UCSRC | URSEL | UMSEL | UPM1 | UPM0 | USBS | UCSZ1 | UCSZ0 | UCPOL |
0x1B(0x3B) | PORTA | PA7 | PA6 | PA5 | PA4 | PA3 | PB2 | PA1 | PA0 |
0x1A(0x3A) | DDRA | DDA7 | DDA6 | DDA5 | DDA4 | DDA3 | DDA2 | DDA1 | DDA0 |
0x18(0x38) | PORTB | PB7 | PB6 | PB5 | PB4 | PB3 | PB2 | PB1 | PB0 |
0x17(0x37) | DDRB | DDB7 | DDB6 | DDB5 | DDB4 | DDB3 | DDB2 | DDB1 | DDB0 |
0x16(0x36) | PINB | PINB7 | PINB6 | PINB5 | PINB4 | PINB3 | PINB2 | PINB1 | PINB0 |
0x15(0x35) | PORTC | PC7 | PC6 | PC5 | PC4 | PC3 | PC2 | PC1 | PC0 |
0x14(0x37) | DDRC | DDC7 | DDC6 | DDC5 | DDC4 | DDC3 | DDC2 | DDC1 | DDC0 |
0x13(0x33) | PINC | PINC7 | PINC6 | PINC5 | PINC4 | PINC3 | PINC2 | PINC1 | PINC0 |
0x12(0x32) | PORTD | PD7 | PD6 | PD5 | PD4 | PD3 | PD2 | PD1 | PD0 |
0x11(0x31) | DDRD | DDD7 | DDD6 | DDD5 | DDD4 | DDD3 | DDD2 | DDD1 | DDD0 |
0x10(0x30) | PIND | PIND7 | PIND6 | PIND5 | PIND4 | PIND3 | PIND2 | PIND1 | PIND0 |
0x0C(0x2C) | UDR | Registre de données USART I/O
| |||||||
0x0B(0x2B) | UCSRA | RXC | TXC | UDRE | FE | DOR | PE | U2X | MPCM |
0x0A(0x2A) | UCSRB | RXCIE | TXCIE | UDRIE | RXEN | TXEN | UCSZ2 | RXB8 | TXB8 |
Les connaisseurs de l'ATMega8 sont invités à détailler ce tableau pour apprendre ce que l’on peut faire avec notre cœur.
Il est facile de constater qu'aucun timer n'est implanté. Cela a comme conséquence qu’il nous sera impossible de faire tourner un petit système temps réel sans modifier ce cœur.
Les instructions
modifierCette 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 n'est absolument pas nécessaire de connaître la liste des instructions pour continuer ce projet. Mais n'ayant trouvé cette liste nulle part dans Wikipédia, nous la donnons ici sans plus d'explications.
- Instructions arithmétiques et logiques
Mnemonics | Operands | Description | Opération | Flags | #Clocks |
ADD | Rd, Rr | Additionne deux registres | Rd ←Rd+Rr | Z,C,N,V,H | 1
|
ADC | Rd, Rr | Additionne deux registres avec la retenue | Rd← Rd+Rr+C | Z,C,N,V,H | 1
|
ADIW | Rdl, K | Addition immédiate de mots | Rdh:Rdl←Rdh:Rdl + K | Z,C,N,V,S | 2
|
SUB | Rd, Rr | Soustraire deux registres | Rd←Rd - Rr | Z,C,N,V,H | 1
|
SUBI | Rd, K | Soustraire les constantes de deux registres | Rd←Rd - K | Z,C,N,V,H | 1
|
SBC | Rd, Rr | Soustraire deux registres avec une retenue | Rd←Rd - Rr - C | Z,C,N,V,H | 1
|
SBCI | Rd, K | Soustraire une constante du registre avec retenue | Rd←Rd - K - C | Z,C,N,V,H | 1
|
SBIW | Rdl, K | Soustraction immédiate du mot | Rdh:Rdl←Rdh:Rdl - K | Z,C,N,V,S | 2
|
AND | Rd, Rr | Registres du ET logique | Rd ← Rd * Rr | Z,N,V | 1
|
ANDI | Rd, K | Registres du ET logique et constante | Rd ← Rd * K | Z,N,V | 1
|
OR | Rd, Rr | OU logique entre registres | Rd ← Rd v K | Z,N,V | 1
|
ORI | Rd, K | OU logique entre Registre et constante | Rd ← Rd v K | Z,N,V | 1
|
EOR | Rd, Rr | OU exclusif entre registres | Rd ← Rd Rr | Z,N,V | 1
|
COM | Rd | Complément à 1 | Rd ← 0xFF - Rd | Z,C,N,V | 1
|
NEG | Rd | Complément à 2 | Rd ← 0x00 - Rd | Z,C,N,V,H | 1
|
SBR | Rd, K | Mettre le(s) bit(s) dans un registre | Rd ← Rd v K | Z,N,V | 1
|
CBR | Rd, K | Effacer le(s) bit(s) dans un registre | Rd ← Rd ET (0xFF-K) | Z,N,V | 1
|
INC | Rd | Incremente | Rd ← Rd + 1 | Z,N,V | 1
|
DEC | Rd | Décremente un registre | Rd ← Rd - 1 | Z,N,V | 1
|
TST | Rd | Test si zero ou négatif | Rd ← Rd * Rd | Z,N,V | 1
|
CLR | Rd | Effacer le registre | Rd ← Rd XOR Rd | Z,N,V | 1
|
SER | Rd | Registre tout à 1 | Rd ← 0xFF | None | 1
|
MUL | Rd, Rr | Multiplication (non signée) | R1:R0← Rd * Rr | Z,C | 2
|
MULS | Rd, Rr | Multiplication (signée) | R1:R0← Rd * Rr | Z,C | 2
|
MULSU | Rd, Rr | Multiplication signé par non signé | R1:R0← (Rd * Rr) <<1 | Z,C | 2
|
FMUL | Rd, Rr | Multiplication fractionnaire non signée | R1:R0 ← (Rd * Rr) << 1 | Z,C | 2
|
FMULS | Rd, Rr | Multiplication fractionnaire signée | R1:R0← (Rd * Rr) << 1 | Z,C | 2
|
FMULSU | Rd, Rr | Multiplication fractionnaire non signée et non signée | R1:R0← (Rd * Rr) << 1 | Z,C | 2
|
- Instructions de sauts
Mnemonics | Operands | Description | Opération | Flags | #Clocks |
RJMP | K | Saut relatif | PC ← PC + k + 1 | None | 2 |
IJMP | Saut indirect vers (Z) | PC ← Z | None | 2 | |
RCALL | K | Appel du sous-programme en relatif | PC ← PC + k + 1 | None | 3 |
ICALL | Appel indirect de (Z) | PC ← Z | None | 3 | |
RET | Retour de sous-programme | PC ← STACK | None | 4 | |
RETI | Retour d'interruption | PC ← STACK | I | 4 | |
CPSE | Rd,Rr | Compare et saute si égal | if(Rd=Rr) PC← PC + 2 or 3 | None | |
CP | Rd, Rr | Compare | Rd - Rr | Z,N,V,C,H | 1 |
CPC | Rd, Rr | Compare avec la retenue | Rd – Rr - C | Z,N,V,C,H | 1 |
CPI | Rd, K | Comparaison en immédiat | Rd - K | Z,N,V,C,H | 1 |
SBRC | Rr, b | Saute si le bit du registre est effacé | if(Rr(b)=0) PC← PC + 2 or 3 | None | 1/2/3 |
SBRS | Rr, b | Saute si le bit du registre et positionné à 1 | if(Rr(b)=1) PC← PC + 2 or 3 | None | |
SBIC | P, b | Saute si bit registre d'entrées/sorties est à 0 | if(P(b)=0) PC← PC + 2 or 3 | None | |
SBIS | S, K | Saute si bit registre d'entrées/sorties est à 1 | if(P(b)=1) PC← PC + 2 or 3 | None | |
BRBS | S, K | Brancher si le drapeau est mis | if(SREG(s)=1) then PC PC+K+1 | None | |
BRBC | S,K | Brancher si le drapeau est effacé | if(SREG(s)=0) then PC PC+K+1 | None | |
BREQ | k | Brancher si égalité | if(Z=1) then PC ← PC + k +1 | None | |
BRNE | k | Brancher si non égal | if(Z=0) then PC← PC + k +1 | None | |
BRCS | k | Brancher si la retenue est mise | if(C=1) then PC ← PC + k +1 | None | |
BRCC | k | Brancher si la retenue est effacée | if(C=0) then PC ← PC + k +1 | None | |
BRSH | k | Brancher si idem ou supérieur | if(C=0) then PC ← PC + k +1 | None | |
BRLO | k | Brancher si inférieur | if(C=1) then PC ← PC + k +1 | None | |
BRMI | k | Brancher si négatif | if(N=1) then PC ← PC + k +1 | None | |
BRPL | k | Brancher si positif | if(N=0) then PC ← PC + k +1 | None | |
BRGE | k | Brancher si supérieur ou égal (signé) | if(N V=0) then PC ← PC + k +1 | None | |
BRLT | k | Brancher si inférieur à 0 (signé) | if(N V=1) then PC ← PC + k +1 | None | |
BRHS | k | Brancher si toutes les retenues du drapeau sont mises | if(H=1) then PC ← PC + k +1 | None | |
BRHC | k | Brancher si toutes les retenues du drapeau sont effacées | if(H=0) then PC ← PC + k +1 | None | |
BRTS | k | Brancher si T flag est mis | if(T=1) then PC ← PC + k +1 | None | |
BRTC | k | Brancher si T flag est | if(T=0) then PC ← PC + k +1 | None | |
BRVS | k | Brancher si l'Overflow est à un | if(V=1) then PC ← PC + k +1 | None | |
BRVC | k | Brancher si l'Overflow est effacé | if(T=0) then PC ← PC + k +1 | None | |
BRIE | k | Brancher si l'interruption est permise | if(I=1) then PC ← PC + k +1 | None | |
BRID | k | Brancher si l'interruption n’est pas permise | if(I=0) then PC ← PC + k +1 | None |
- Instructions de transfert de données
Mnemonics | Operands | Description | Opération | Flags | #Clocks |
MOV | Rd, Rr | Copier un registre dans un autre | Rd← Rr | None | 1 |
MOVW | Rd, Rr | Copier un mot dans un autre | Rd+1:Rd← Rr+1:Rr | None | 1 |
LDI | Rd, K | Charger en immédiat | Rd← Knone1 | ||
LD | Rd, X | Charger en indirect | Rd← (x) | None | 2 |
LD | Rd, X+ | Chargement indirect et Post-inc. | Rd← (x), x← x+1 | None | 2 |
LD | Rd, - X | Chargement indirect et Pré-décr. | x← x-1, Rd← (x) | None | 2 |
LD | Rd, Y | Chargement indirect | Rd← (y) | None | 2 |
LD | Rd, Y+ | Chargement indirect et Post-inc | Rd← (y), y← y+1 | None | 2 |
LD | Rd, - Y | Chargement indirect et Pré-décr. | y← y-1, Rd← (y) | None | 2 |
LDD | Rd, Y+q | Chargement indirect avec déplacement | Rd← (y +q ) | None | 2 |
LD | Rd, Z | Chargement indirect | Rd← (z) | None | 2 |
LD | Rd, Z+ | Chargement indirect et Post-inc | Rd← (z), z← z+1 | None | 2 |
LD | Rd, -Z | Chargement indirect et Pré-décr. | z← z-1, Rd← (z) | None | 2 |
LDD | Rd, Z+q | Chargement indirect avec déplacement | Rd← (z +q ) | None | 2 |
LDS | Rd, K | Chargement direct avec SRAM | Rd← (k) | None | 2 |
ST | X, Rr | Stockage indirect | (x) ← Rr | None | 2 |
ST | X+, Rr | Stockage indirect et Post-inc | (x) ← Rr, x← x+1 | None | 2 |
ST | -X, Rr | Stockage indirect et Pré-décr. | x← x-1, (x)← Rr | None | 2 |
ST | Y, Rr | Stockage indirect | (y) ← Rr | None | 2 |
ST | Y+, Rr | Stockage indirect et Post-inc. | (y) ← Rr, y← y+1 | None | 2 |
ST | -Y, Rr | Stockage indirect et Pré-décr. | y← y-1, (y)← Rr | None | 2 |
STD | Y+q, Rr | Stockage indirect avec déplacement | (y + q) ← Rr | None | 2 |
ST | Z, Rr | Stockage indirect | (z) ← Rr | None | 2 |
ST | Z+, Rr | Stockage indirect et Post-inc | (z) ← Rr, z← z+1 | None | 2 |
ST | -Z, Rr | Stockage indirect et Pré-décr. | z← z-1, (z)← Rr | None | 2 |
STD | Z+q, Rr | Stockage indirect avec déplacement | (z + q) ← Rr | None | 2 |
STS | K, Rr | Stockage direct de SRAM | (k) ← Rr | None | 2 |
LPM | Chargement de la mémoire programme | R0← (z) | None | 3 | |
LPM | Rd, Z | Chargement de la mémoire programme | Rd← (z) | None | 3 |
LPM | Rd, Z+ | Chargement de la mémoire programme et Post-inc | Rd← (z), z← z+1 | None | 3 |
SPM | Stockage dans la mémoire programme | (z)← R1:R0 | None | - | |
IN | Rd, P | IN Port P | Rd← P | None | 1 |
OUT | P, Rr | OUT Port P | P← Rr | None | 1 |
PUSH | Rr | Pousse le registre dans la pile | STACK ← Rr | None | 2 |
POP | Rd | Enlever le registre de la pile | Rd←STACK | None | 2 |
- Instructions sur bits et tests sur bit
Mnemonics | Operands | Description | Opération | Flags | #Clocks |
SBI | P,b | Positionne un bit à 1 | I/O(p,b)← 1 | None | 2 |
CBI | P,b | Positionne un bit à 0 | I/O(p,b)← 0 | None | 2 |
LSL | Rd | décalage vers la gauche | Rd(n+1)←Rd(n),Rd(0)← 0 | Z,C,N,V | 1 |
LSR | Rd | décalage vers la droite | Rd(n)←Rd(n+1),Rd(7)← 0 | Z,C,N,V | 1 |
ROL | Rd | décalage circulaire gauche | Rd(0)←C,Rd(n+1)←Rd(n),C← Rd(7) | Z,C,N,V | 1 |
ROR | Rd | décalage circulaire droite | Rd(7)←C,Rd(n)←Rd(n+1),C← Rd(0) | Z,C,N,V | 1 |
ASR | Rd | décalage arithmétique droit | Rd(n)←Rd(n+1), n=0..6 | Z,C,N,V | 1 |
SWAP | Rd | échange poids/fort/faible | Rd(3..0)←Rd(7..4),Rd(7..4),← Rd(3..0) | None | 1 |
BSET | s | SREG(s)← 1 | SREG(s) | 1 | |
BCLR | s | SREG(s)← 0 | SREG(s) | 1 | |
BST | Rr, b | T← Rr(b) | T | 1 | |
BLD | Rd, b | Rd(b)←T | None | 1 | |
SEC | Met la retenue à 1 | C←1 | C | 1 | |
CLC | Met la retenue à 0 | C←0 | C | 1 | |
SEN | N←1 | C | 1 | ||
CLN | N←0 | N | 1 | ||
SEZ | Met Z à 1 | Z←1 | C | 1 | |
CLZ | Met Z à 0 | Z←0 | Z | 1 | |
SEI | Autorisation des interruptions globales | I←1 | I | 1 | |
CLI | Désactivation des interruptions globales | I←0 | I | 1 | |
SES | Met le bit de signe à 1 | S←1 | S | 1 | |
CLS | Efface le bit de signe | S←0 | S | 1 | |
SEV | Met à 1 le dépassement en complément à deux | v←1 | V | 1 | |
CLV | Efface le dépassement en complément à deux | V←0 | V | 1 | |
SET | Set T in SREG | T←1 | T | 1 |
Le cœur CoreAtMega8
modifierNous allons commencer par aborder dans ce chapitre ce qu’il y a de spécifique pour notre système mono-puce, à savoir les PORTs, la RAM et la ROM. Avant de commencer quoi que ce soit vous devez donc vous poser trois questions :
- comment utiliser les ports existants et plus tard, comment en ajouter d'autres ?
- comment implanter la mémoire RAM ?
- comment générer de manière automatique la ROM ?
Nous ne prétendons pas répondre de manière exhaustive à ces trois questions simultanément, mais nous allons y revenir petit à petit.
Les ports du cœur CoreAtMega8
modifierLa gestion des PORTs dans ce cœur ressemble à cette même gestion dans le PicoBlaze. Il est facile d'étendre le nombre de PORTs jusqu'à 64 dans l'espace d'entrées/sorties. Nous nous contenterons cependant de l'existant, à savoir un PORT : PORTB dans le cœur initial comme cela est expliqué plus loin. Le coreATMega8 est conçu pour une réalisation de PORTs bidirectionnels mais à l'extérieur : ces PORTs bidirectionnels ne sont pas réalisés dans le cœur car trop dépendants du fabricant du FPGA qui accueillera ce processeur softcore.
Tous les ports sont accessibles de manière normale. L'écriture dans DDRA se fait par l'instruction :
// en langage C
DDRA=0xFF; // 1 <=> output
tandis qu'en assembleur on écrira :
; assembleur ldi r10,0xFF out DDRA,r10
Attention, comme on le verra plus loin, l'écriture dans DDRA n'a pas l'effet escompté ! Cela ne peut servir à choisir la direction du PORTA puisqu’il n'y a pas de PORT bidirectionnel d'implémenté.
Ma deuxième remarque sera pour compléter notre description. On doit écrire dans un programme C :
PORTA = PINA; // recopie du PORTA dans le PORTA
et non pas PORTA=PORTA.
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.
La RAM
modifierOn rappelle qu’il s'agit de l'espace à partir de l'adresse 0x60. C'est là que se trouve la mémoire à proprement parler.
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 WHISBONE 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. Le cœur CoreAtMega8 propose un exemple de mémoire en VHDL que nous utiliserons sans changement.
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;
entity data_mem is
port ( I_CLK : in std_logic;
I_ADR : in std_logic_vector(10 downto 0);
I_DIN : in std_logic_vector(15 downto 0);
I_WE : in std_logic_vector( 1 downto 0);
Q_DOUT : out std_logic_vector(15 downto 0));
end data_mem;
architecture Behavioral of data_mem is
constant zero_256 : bit_vector := X"00000000000000000000000000000000"
& X"00000000000000000000000000000000";
constant nine_256 : bit_vector := X"99999999999999999999999999999999"
& X"99999999999999999999999999999999";
component RAMB4_S4_S4
generic(INIT_00 : bit_vector := zero_256;
INIT_01 : bit_vector := zero_256;
INIT_02 : bit_vector := zero_256;
INIT_03 : bit_vector := zero_256;
INIT_04 : bit_vector := zero_256;
INIT_05 : bit_vector := zero_256;
INIT_06 : bit_vector := zero_256;
INIT_07 : bit_vector := zero_256;
INIT_08 : bit_vector := zero_256;
INIT_09 : bit_vector := zero_256;
INIT_0A : bit_vector := zero_256;
INIT_0B : bit_vector := zero_256;
INIT_0C : bit_vector := zero_256;
INIT_0D : bit_vector := zero_256;
INIT_0E : bit_vector := zero_256;
INIT_0F : bit_vector := zero_256);
port( DOA : out std_logic_vector(3 downto 0);
DOB : out std_logic_vector(3 downto 0);
ADDRA : in std_logic_vector(9 downto 0);
ADDRB : in std_logic_vector(9 downto 0);
CLKA : in std_ulogic;
CLKB : in std_ulogic;
DIA : in std_logic_vector(3 downto 0);
DIB : in std_logic_vector(3 downto 0);
ENA : in std_ulogic;
ENB : in std_ulogic;
RSTA : in std_ulogic;
RSTB : in std_ulogic;
WEA : in std_ulogic;
WEB : in std_ulogic);
end component;
signal L_ADR_0 : std_logic;
signal L_ADR_E : std_logic_vector(10 downto 1);
signal L_ADR_O : std_logic_vector(10 downto 1);
signal L_DIN_E : std_logic_vector( 7 downto 0);
signal L_DIN_O : std_logic_vector( 7 downto 0);
signal L_DOUT_E : std_logic_vector( 7 downto 0);
signal L_DOUT_O : std_logic_vector( 7 downto 0);
signal L_WE_E : std_logic;
signal L_WE_O : std_logic;
begin
sr_0 : RAMB4_S4_S4 ---------------------------------------------------------
generic map(INIT_00 => nine_256, INIT_01 => nine_256, INIT_02 => nine_256,
INIT_03 => nine_256, INIT_04 => nine_256, INIT_05 => nine_256,
INIT_06 => nine_256, INIT_07 => nine_256, INIT_08 => nine_256,
INIT_09 => nine_256, INIT_0A => nine_256, INIT_0B => nine_256,
INIT_0C => nine_256, INIT_0D => nine_256, INIT_0E => nine_256,
INIT_0F => nine_256)
port map( ADDRA => L_ADR_E, ADDRB => "0000000000",
CLKA => I_CLK, CLKB => I_CLK,
DIA => L_DIN_E(3 downto 0), DIB => "0000",
ENA => '1', ENB => '0',
RSTA => '0', RSTB => '0',
WEA => L_WE_E, WEB => '0',
DOA => L_DOUT_E(3 downto 0), DOB => open);
-- ************* une partie est retirée : voir le fichier original : data_mem.vhd ***********
sr_3 : RAMB4_S4_S4 ---------------------------------------------------------
generic map(INIT_00 => nine_256, INIT_01 => nine_256, INIT_02 => nine_256,
INIT_03 => nine_256, INIT_04 => nine_256, INIT_05 => nine_256,
INIT_06 => nine_256, INIT_07 => nine_256, INIT_08 => nine_256,
INIT_09 => nine_256, INIT_0A => nine_256, INIT_0B => nine_256,
INIT_0C => nine_256, INIT_0D => nine_256, INIT_0E => nine_256,
INIT_0F => nine_256)
port map( ADDRA => L_ADR_O, ADDRB => "0000000000",
CLKA => I_CLK, CLKB => I_CLK,
DIA => L_DIN_O(7 downto 4), DIB => "0000",
ENA => '1', ENB => '0',
RSTA => '0', RSTB => '0',
WEA => L_WE_O, WEB => '0',
DOA => L_DOUT_O(7 downto 4), DOB => open);
-- remember ADR(0)
--
adr0: process(I_CLK)
begin
if (rising_edge(I_CLK)) then
L_ADR_0 <= I_ADR(0);
end if;
end process;
-- we use two memory blocks _E and _O (even and odd).
-- This gives us a memory with ADR and ADR + 1 at th same time.
-- The second port is currently unused, but may be used later,
-- e.g. for DMA.
--
L_ADR_O <= I_ADR(10 downto 1);
L_ADR_E <= I_ADR(10 downto 1) + ("000000000" & I_ADR(0));
L_DIN_E <= I_DIN( 7 downto 0) when (I_ADR(0) = '0') else I_DIN(15 downto 8);
L_DIN_O <= I_DIN( 7 downto 0) when (I_ADR(0) = '1') else I_DIN(15 downto 8);
L_WE_E <= I_WE(1) or (I_WE(0) and not I_ADR(0));
L_WE_O <= I_WE(1) or (I_WE(0) and I_ADR(0));
Q_DOUT( 7 downto 0) <= L_DOUT_E when (L_ADR_0 = '0') else L_DOUT_O;
Q_DOUT(15 downto 8) <= L_DOUT_E when (L_ADR_0 = '1') else L_DOUT_O;
end Behavioral;
La mémoire est construite à partir de composants Xilinx "RAMB4_S4_S4" qui est une mémoire double ports. Cette façon de faire est peu portable et il vous faudra l'adapter si vous désirez faire tourner ce soft processor dans un FPGA concurrent.
La ROM
modifierC'est là que se situe le programme à exécuter. Commençons à présenter un exemple de ROM.
-- VHDL File : prog_mem.vhd
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;
-- the content of the program memory.
--
use work.prog_mem_content.all;
entity prog_mem is
port ( I_CLK : in std_logic;
I_WAIT : in std_logic;
I_PC : in std_logic_vector(15 downto 0); -- word address
I_PM_ADR : in std_logic_vector(11 downto 0); -- byte address
Q_OPC : out std_logic_vector(31 downto 0);
Q_PC : out std_logic_vector(15 downto 0);
Q_PM_DOUT : out std_logic_vector( 7 downto 0));
end prog_mem;
architecture Behavioral of prog_mem is
-- *************** retiré pour question de place ****************
end Behavioral;
Cet exemple incomplet ne montre aucun contenu. 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 (Intel). Il faudra donc un utilitaire pour transformer le fichier HEX en fichier VHDL car il s'agit d'une opération qui peut se faire automatiquement. Nous utiliserons un programme en C++ que nous donnons en annexe 1 et qui s’appelle "make_mem.cc" qui est fourni avec le cœur. Compilez donc ce programme pour en faire un exécutable. Une fois l'exécutable réalisé, lancer
make_mem demo.hex prog_mem_content.vhd
ce qui vous génèrera un package dans un fichier prog_mem_content.vhd, qui sera utilisé dans le projet. La mémoire est construite à partir de composants Xilinx "RAMB4_S4_S4" qui est une mémoire double ports. Cette façon de faire est peu portable et il vous faudra l'adapter si vous désirez faire tourner ce soft processor dans un FPGA Altera ou ACTEL.
Mes premiers programmes en C
modifierNous ne présentons pas le langage C dans ce document. Si vous n'êtes pas familier, lisez :
- Le langage C chez Wikipédia
- La programmation C et ses exercices chez WikiBook
- Le Introduction au langage C chez Wikiversité
La programmation du cœur se fait dans l'environnement AVRStudio. Le téléchargement d'AVRStudio nous permet d’utiliser l'assembleur ainsi que le compilateur C GNU (si l’on a installé winAVR). Puisque nous avons décidé pour ce projet de n'utiliser que des outils gratuits, cela nous convient parfaitement. Mon premier programme en C a été réalisé pour tester le fonctionnement du cœur CoreAtMega8 sur notre carte d'application Digilent.
Pour tester l'exécution d'un programme dans un cœur il faut naturellement que celui-ci fasse quelque chose de visible, autrement dit qu’il utilise les PORTs. C'est là que le travail sérieux commence. En effet le cœur a été développé et testé avec un spartan2 de chez Xilinx et nous désirons l'essayer sur une carte Digilent starter Board équipée d'un spartan3. Ceci nécessite donc quelques modifications de notre cœur sur au moins deux fichiers :
- le fichier de contraintes ucf qui relie les noms symboliques des entrées/sorties VHDL à des broches physiques du FPGA
- le fichier de description des PORTs que nous allons lire mais pas modifier pour le moment.
Connaître les PORTS implantés
modifierJuergen Sauermann qui a développé le cœur utilisé dans ce chapitre (que nous appelons CoreAtMega8) le décrit comme compatible à un ATMega8. Cette compatibilité ne peut pas être complète :
- les fusibles ne sont pas implantés
- beaucoup de périphériques et leurs fonctionnalités ne sont pas implantées.
En général la compatibilité se fait plutôt au niveau des instructions : les seules instructions non implantées sont souvent celles qui mettent le micro-contrôleur en mode repos et éventuellement celles qui écrivent dans l’EEPROM.
Si l’on veut connaître lesPORTs il nous faut lire le fichier io.vhd du projet original dont on présente quelques extraits. Côté ports d'entrée on trouve :
-- 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 others => Q_DOUT <= X"AA";
end case;
end process;
Ce qui nous intéresse est à la fin : la ligne when X"36" montre que le PORTB (plus exactement PINB) est implanté en lecture.
Cherchons la même chose en écriture :
-- IO write process
--
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);
L_LEDS <= not L_LEDS;
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;
Les nouvelles sont moins bonnes : le portB est bien implanté mais seulement sur 7 bits ! C'est une des choses que l’on changera dans la suite du projet, mais qu'on laisse tel quel pour le moment.
Notre architecture CoreAtMega8 é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
modifierLe programme présenté montre comment inclure de l’assembleur dans un programme C.
#include <avr/io.h>
main(void)
{
asm volatile (
debut: in r24, 0x16 ; 22 : PINB
com r24 ; one's complement
out 0x18, r24 ; 24 : PORTB
rjmp debut ; infinite loop
)
}
Ne prenez pas ce programme à la lettre, il est juste donné comme exemple et n'a pas été testé.
Le premier programme simple en C pur
modifierUn programme tout simple de test est présenté maintenant : il s'agit tout simplement de recopier le PORTB sur le PORTB. Remarquons aussi que ce programme fonctionne complètement si l’on a pris soin de prendre une version modifiée parce que la version originale sera incapable d'allumer la LED poids fort.
#include <avr/io.h>
main(void)
{ // La gestion de DDRB et DDRC est inutile pour ce cœur
// DDRB = 0xFF; // 8 sorties pour B retiré car non implanté (voir ci-dessus)
// DDRC = 0x00; // 8 entrees pour C retiré car non implanté
while(1)
PORTB = PINB; // recopie du PORTB dans le PORTB qui allume les LEDs
}
Ce programme est très simple et ne fait pas grand chose à part être pratique pour tester le fonctionnement correct.
Compilation
modifierTout programme C doit être compilé avec le compilateur GNU C facilement utilisable avec AVRStudio. Quel que soit l'environnement, quel que soit le langage, votre compilation donnera un fichier hex Intel. Ce fichier devra être transformé en fichier VHDL comme expliqué en Annexe 1. À ce point, seul un détail vous manque avant de compiler votre projet VHDL, la création du fichier de contraintes ucf.
Fichier ucf
modifierLe fichier ucf doit être construit à partir de l'entité globale du projet que voici :
entity avr_fpga is
port ( I_CLK_100 : in std_logic;
I_SWITCH : in std_logic_vector(9 downto 0);
I_RX : in std_logic;
Q_7_SEGMENT : out std_logic_vector(6 downto 0);
Q_LEDS : out std_logic_vector(3 downto 0);
Q_TX : out std_logic);
end avr_fpga;
Nous ne disposons que d'une horloge 50 MHz sur notre carte, mais nous garderons le nom "I_CLK_100" pour ne rien changer dans les fichiers VHDL.
NET I_CLK_100 PERIOD = 20 ns; NET I_CLK_100 TNM_NET = I_CLK_100; NET "I_CLK_100" LOC = "T9"; #liaison série #net "serial_out" loc = "r13"; #net "serial_in" loc = "t13"; NET I_RX LOC = T13; NET Q_TX LOC = R13; # 7 LED display # NET Q_7_SEGMENT<0> LOC = "K12"; NET Q_7_SEGMENT<1> LOC = "P14"; NET Q_7_SEGMENT<2> LOC = "L12"; NET Q_7_SEGMENT<3> LOC = "N14"; NET Q_7_SEGMENT<4> LOC = "P13"; NET Q_7_SEGMENT<5> LOC = "N12"; NET Q_7_SEGMENT<6> LOC = "P12"; # single LEDs # #NET Q_LEDS<0> LOC = "K12"; #NET Q_LEDS<1> LOC = "P14"; #NET Q_LEDS<2> LOC = "L12"; #NET Q_LEDS<3> LOC = "N14"; # DIP switch(0 ... 7) and two pushbuttons (8, 9) # NET I_SWITCH<0> LOC = "f12"; NET I_SWITCH<1> LOC = "g12"; NET I_SWITCH<2> LOC = "h14"; NET I_SWITCH<3> LOC = "h13"; NET I_SWITCH<4> LOC = "j14"; NET I_SWITCH<5> LOC = "j13"; NET I_SWITCH<6> LOC = "k14"; NET I_SWITCH<7> LOC = "k13"; NET I_Reset<0> LOC = "L13"; NET I_Reset<1> LOC = "L14";
Vous avez certainement remarqué à ce stade que la liaison série est connectée. Nous l’utiliserons plus loin.
En résumé
modifierNous avons décrit comment modifier le cœur original pour l'adapter à une autre carte que celle pour lequel il a été conçu.
- le minimum à faire est de changer le fichier ucf
- ensuite écrire un fichier programme à compiler et à transformer en vhdl avec les outils appropriés.
Ce qui n'est présenté qu'en trame de fond ici, est la démarche à suivre pour interfacer ce cœur à de la logique externe :
- modifier le fichier io.vhd pour ajouter des registres internes au cœur et/ou des PORTs
- amener les entrées et sorties correspondantes au niveau de l'entité globale.
Nous allons voir dans la suite une série d'exemples qui fixera les idées sur ces points.
Un petit peu plus loin en C
modifierNous décidons de faire tourner le programme d'exemple donné avec le cœur. Ce programme utilise le seul afficheur sept segments de la carte spartan2 ainsi que la liaison série. Avec notre carte spartan3 nous disposons de 4 afficheurs sept segments et aussi de la liaison série. Une autre façon de dire les choses c’est qu’il nous faudra très légèrement changer la partie matérielle pour faire tourner le programme. Tout ce qui concerne la liaison série ne sera pas modifié (ce qui aura des conséquences sur la vitesse de transmission), seule la gestion de l'afficheur devra changer.
Programme de départ
modifierVoici donc le programme de départ de Juergen Sauermann, donné dans le répertoire "app"
/* Port A */
// Les ports ci-dessous n'existent pas dans l'ATMega8 de la vraie vie mais nous les avons ajouté dans notre cœur
// d'où leur présence ici : 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)
#include "stdint.h"
#include "avr/io.h"
#include "avr/pgmspace.h"
#undef F_CPU
#define F_CPU 25000000UL
#include "util/delay.h"
//----------------------------------------------------------------------//
// //
// print char cc on UART. //
// return number of chars printed (i.e. 1). //
// //
//----------------------------------------------------------------------//
uint8_t uart_putc(uint8_t cc) {
while ((UCSRA & (1 << UDRE)) == 0) ;
UDR = cc;
return 1;
}
//----------------------------------------------------------------------//
// //
// print char cc on 7 segment display. //
// return number of chars printed (i.e. 1). //
// //
//----------------------------------------------------------------------//
// The segments of the display are encoded like this:
//
// segment PORT B
// name Bit number
// ----A---- ----0----
// | | | |
// F B 5 1
// | | | |
// ----G---- ----6----
// | | | |
// E C 4 2
// | | | |
// ----D---- ----3----
//
//-----------------------------------------------------------------------------
#define SEG7(G, F, E, D, C, B, A) (~(G<<6|F<<5|E<<4|D<<3|C<<2|B<<1|A))
uint8_t seg7_putc(uint8_t cc) {
uint16_t t;
switch(cc) { // G F E D C B A
case ' ': PORTB = SEG7(0,0,0,0,0,0,0); break;
case 'E': PORTB = SEG7(1,1,1,1,0,0,1); break;
case 'H': PORTB = SEG7(1,1,1,0,1,1,0); break;
case 'L': PORTB = SEG7(0,1,1,1,0,0,0); break;
case 'O': PORTB = SEG7(0,1,1,1,1,1,1); break;
default: PORTB = SEG7(1,0,0,1,0,0,1); break;
}
// wait 800 + 200 ms. This can be quite boring in simulations,
// so we wait only if DIP switch 6 is closed.
if (!(PINB & 0x20)) for (t = 0; t < 800; ++t) _delay_ms(1);
PORTB = SEG7(0,0,0,0,0,0,0);
if (!(PINB & 0x20)) for (t = 0; t < 200; ++t) _delay_ms(1);
return 1;
}
//----------------------------------------------------------------------//
// //
// print string s on UART. //
// return number of chars printed. //
// //
//----------------------------------------------------------------------//
uint16_t uart_puts(const char * s) {
const char * from = s;
uint8_t cc;
while ((cc = pgm_read_byte(s++))) uart_putc(cc);
return s - from - 1;
}
//----------------------------------------------------------------------//
// //
// print string s on 7 segment display. //
// return number of chars printed. //
// //
//----------------------------------------------------------------------//
uint16_t seg7_puts(const char * s) {
const char * from = s;
uint8_t cc;
while ((cc = pgm_read_byte(s++))) seg7_putc(cc);
return s - from - 1;
}
//-----------------------------------------------------------------------------
int main(int argc, char * argv[]) {
for (;;) {
if (PINB & 0x40) { // DIP switch 7 open.
// print 'Hello world' on UART.
uart_puts(PSTR("Hello, World!\r\n"));
}
else { // DIP switch 7 closed.
// print 'HELLO' on 7-segment display
seg7_puts(PSTR("HELLO "));
}
}
}
//-----------------------------------------------------------------------------
Si l’on regarde le main, on voit que ce programme teste le bit b6 du PORTB : s'il est à un il envoie "Hello Word" sur la liaison série autrement il envoie "HELLO" sur l’afficheur sept segments.
La directive #define F_CPU 25000000UL sert à définir la fréquence d'horloge. Elle est importante pour les sous-programmes _delay_ms. Elle est pourtant fausse pour notre carte. En effet ce cœur utilise un diviseur par 4 en entrée sur une fréquence originelle de 100 MHz. Nous, nous devons nous contenter de 50 MHz il faudrait donc diviser la fréquence par deux... Comme nous avons choisi l'option de modifier le moins de choses possible dans le CoreATMega8, nous laissons cela de côté. Par contre la liaison série fonctionne elle aussi deux fois moins vite : elle sera réglée à 19200 bauds 8 bits parité paire deux bits de stop. Tout ce qui vient d’être dit dans cette remarque concerne le cœur téléchargeable chez OpenCore. Si vous téléchargez le fichier ATMega8_pong_VGA.zip et allez dans le répertoire "/AVRComplet16_S4_S4/25MHzAVR/" vous n'aurez plus l’affichage sept segments mais la liaison série fonctionne bien à 38400 bauds.
Nous allons redire les choses autrement de peur de perdre des lecteurs en route :
- le répertoire /AVRComplet16_S4_S4/25MHzAVR/ du fichier ATMega8_pong_VGA.zip fonctionne bien à 38400 bauds,
- le répertoire /CorrProjet2010 contient la correction de ce chapitre branché sur 50 MHz d'horloge mais avec une division par 4 qui réduit l'horloge effective à 12,5 MHz et aussi la vitesse de la liaison série à 19200 bauds.
Modification matérielle en conséquence
modifierNos afficheurs sont multiplexés et il nous faut donc gérer cette situation. Voir TP 2 pour plus de détails sur les afficheurs.
Voici donc le nouveau fichier pour lequel nous avons décidé d'ajouter le reset et la gestion des afficheurs multiplexés pour nous adapter à notre carte spartan 3 :
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;
entity avr_fpga is
port ( I_CLK_100 : in std_logic;
I_SWITCH : in std_logic_vector(9 downto 0);
I_RX : in std_logic;
--> added 30/06/2011
I_CLR : in std_logic;
aff : out std_logic_vector(3 downto 0);
--<
Q_7_SEGMENT : out std_logic_vector(6 downto 0);
Q_LEDS : out std_logic_vector(3 downto 0);
Q_TX : out std_logic);
end avr_fpga;
architecture Behavioral of avr_fpga is
component cpu_core
port ( I_CLK : in std_logic;
I_CLR : in std_logic;
I_INTVEC : in std_logic_vector( 5 downto 0);
I_DIN : in std_logic_vector( 7 downto 0);
Q_OPC : out std_logic_vector(15 downto 0);
Q_PC : out std_logic_vector(15 downto 0);
Q_DOUT : out std_logic_vector( 7 downto 0);
Q_ADR_IO : out std_logic_vector( 7 downto 0);
Q_RD_IO : out std_logic;
Q_WE_IO : out std_logic);
end component;
signal C_PC : std_logic_vector(15 downto 0);
signal C_OPC : std_logic_vector(15 downto 0);
signal C_ADR_IO : std_logic_vector( 7 downto 0);
signal C_DOUT : std_logic_vector( 7 downto 0);
signal C_RD_IO : std_logic;
signal C_WE_IO : std_logic;
component io
port ( I_CLK : in std_logic;
I_CLR : in std_logic;
I_ADR_IO : in std_logic_vector( 7 downto 0);
I_DIN : in std_logic_vector( 7 downto 0);
I_RD_IO : in std_logic;
I_WE_IO : in std_logic;
I_SWITCH : in std_logic_vector( 7 downto 0);
I_RX : in std_logic;
Q_7_SEGMENT : out std_logic_vector( 6 downto 0);
Q_DOUT : out std_logic_vector( 7 downto 0);
Q_INTVEC : out std_logic_vector(5 downto 0);
Q_LEDS : out std_logic_vector( 1 downto 0);
Q_TX : out std_logic);
end component;
signal N_INTVEC : std_logic_vector( 5 downto 0);
signal N_DOUT : std_logic_vector( 7 downto 0);
signal N_TX : std_logic;
signal N_7_SEGMENT : std_logic_vector( 6 downto 0);
component segment7
port ( I_CLK : in std_logic;
I_CLR : in std_logic;
I_OPC : in std_logic_vector(15 downto 0);
I_PC : in std_logic_vector(15 downto 0);
Q_7_SEGMENT : out std_logic_vector( 6 downto 0));
end component;
signal S_7_SEGMENT : std_logic_vector( 6 downto 0);
signal L_CLK : std_logic := '0';
signal L_CLK_CNT : std_logic_vector( 2 downto 0) := "000";
signal L_CLR : std_logic; -- reset, active low
signal L_CLR_N : std_logic := '0'; -- reset, active low
signal L_C1_N : std_logic := '0'; -- switch debounce, active low
signal L_C2_N : std_logic := '0'; -- switch debounce, active low
begin
cpu : cpu_core
port map( I_CLK => L_CLK,
--> modified 30/06/2011
-- I_CLR => L_CLR,
I_CLR => I_CLR,
-->
I_DIN => N_DOUT,
I_INTVEC => N_INTVEC,
Q_ADR_IO => C_ADR_IO,
Q_DOUT => C_DOUT,
Q_OPC => C_OPC,
Q_PC => C_PC,
Q_RD_IO => C_RD_IO,
Q_WE_IO => C_WE_IO);
ino : io
port map( I_CLK => L_CLK,
--> modified 30/06/2011
-- I_CLR => L_CLR,
I_CLR => I_CLR,
-->
I_ADR_IO => C_ADR_IO,
I_DIN => C_DOUT,
I_RD_IO => C_RD_IO,
I_RX => I_RX,
I_SWITCH => I_SWITCH(7 downto 0),
I_WE_IO => C_WE_IO,
Q_7_SEGMENT => N_7_SEGMENT,
Q_DOUT => N_DOUT,
Q_INTVEC => N_INTVEC,
Q_LEDS => Q_LEDS(1 downto 0),
Q_TX => N_TX);
seg : segment7
port map( I_CLK => L_CLK,
--> modified 30/06/2011
-- I_CLR => L_CLR,
I_CLR => I_CLR,
-->
I_OPC => C_OPC,
I_PC => C_PC,
Q_7_SEGMENT => S_7_SEGMENT);
-- input clock scaler
--
clk_div : process(I_CLK_100)
begin
if (rising_edge(I_CLK_100)) then
L_CLK_CNT <= L_CLK_CNT + "001";
if (L_CLK_CNT = "001") then
L_CLK_CNT <= "000";
L_CLK <= not L_CLK;
end if;
end if;
end process;
-- reset button debounce process
--
deb : process(L_CLK)
begin
if (rising_edge(L_CLK)) then
-- switch debounce
if ((I_SWITCH(8) = '0') or (I_SWITCH(9) = '0')) then -- pushed
L_CLR_N <= '0';
L_C2_N <= '0';
L_C1_N <= '0';
else -- released
L_CLR_N <= L_C2_N;
L_C2_N <= L_C1_N;
L_C1_N <= '1';
end if;
end if;
end process;
--> modified 30/06/2011
aff <="1110"; -- selection un seul afficheur
--L_CLR <= not L_CLR_N;
L_CLR_N <= I_CLR;
--<
Q_LEDS(2) <= I_RX;
Q_LEDS(3) <= N_TX;
Q_7_SEGMENT <= N_7_SEGMENT when (I_SWITCH(7) = '1') else S_7_SEGMENT;
Q_TX <= N_TX;
end Behavioral;
À ce niveau tout fonctionne correctement sauf ....
Le fonctionnement correct nécessite que l'interrupteur sw7 soit à un ! Nous n'avons pas vraiment d'idée sur la raison de cela pour le moment.. sauf peut être que ce n'est probablement pas une cause logicielle... mais plutôt matérielle. Est-ce lié au fait que la lecture du portB se fait sur 7 bits ?
Et si l’on voulait recevoir de l'information par la RS232...
modifierIl n’est pas difficile de recevoir des données par la RS232 : il suffit d'attendre qu'un certain bit soit positionné puis de lire. Voici un programme complet qui fonctionne correctement mais avec l’affichage sur 7 segments si vous n'avez rien changé par rapport à la section précédente.
#include "avr/io.h"
#undef F_CPU
#define F_CPU 25000000UL
int main(int argc, char * argv[])
{
for (;;) {
// 2 lignes ci-dessous pour recevoir
while (!(UCSRA & (1<<RXC))); //attente donnée RS232
PORTB = UDR;
}
}
Contrairement au Silicore1657 déjà évoqué, le cœur CoreAtMega8 dispose du mécanisme d'interruption que nous allons détailler maintenant.
Ajoutons une interruption
modifierNous avons appris à nos dépend que la gestion des interruptions nécessite d’utiliser un protocole précis si l’on ne veut pas se retrouver avec des programmes ne fonctionnant pas.
Gestion des Interuptions (Juergen Sauermann)
modifierSuite à un problème avec les interruptions, nous avons été amené à échanger avec Juergen Sauermann, auteur de ce cœur coreATMega8. Il nous a très gentiment répondu le mail suivant qui est suffisamment pédagogique pour être traduit sans transformation.
Il y a des différences subtiles dans les entrées/sorties (E/S dans la suite) des différents composants ATMega. Quand je dis que le cœur est (principalement) compatible mega8 cela vaut pour le cœur mais pas pour les E/S (comme les registres pour l'UART les bits d'autorisation d'interruption etc.) Ainsi beaucoup de choses de io.h ne peuvent pas être utilisés et ces quelques différences sur les composants ont un impact sur mon cœur.
Laissez-moi décrire en premier comment les interruptions sont supposées fonctionner et vous donner quelques astuces pour réparer cela. Aussi loin que je me rappelle j’ai utilisé les interruptions pour UART Rx, UART Tx, et timer dans quelques uns de mes projets.
Il y a des pré requis pour que les interruptions soient prises en charge:
- les interruptions doivent être autorisées par l'intermédiaire du registre status (instruction EI), et
- les interruptions individuelles doivent avoir été autorisées elles aussi (notant une différence entre le code donné dans le fichier html/pdf et le code VHDL dans le répertoire src, dans la suite je me réfèrerai uniquement au code VHDL qui est plus proche de l'ATMega)
Quand une interruption arrive alors L_INTVEC est positionné tant qu'aucune autre interruption est déjà en cours (io.vhd lignes 161-180).
Cela a pour conséquence le remplacement par opc_fetch.vhd de l'instruction courante par un pseudo opcode 00000000001vvvvv où vvvvv sont les 5 bits de poids faible de INTVEC.
Quand le pseudo-opcode est decodé il exécute une instruction jump vers le début du programme source. À ce point les différences entre les ATMegas entrent en jeu. Si vous générez du code pour de modèles mémoire "small" (jusqu'à, je crois, ATMega 8) alors avr_gcc génère une instruction (deux octets) RJMP par vecteur d'interruption. Pour des modèles plus grands, une instruction 2x2 octets JMP est générée.
Si vous regardez dans le fichier opc_deco.vhd autour de la ligne 99, alors vous voyez:
Q_JADR <= "0000000000" & I_OPC(4 downto 0) & "0";
qui est pour un modèle de mémoire large (instruction 2 mots JMP). Si vous utilisez une instruction sur un mot RJMP, alors changez en :
Q_JADR <= "00000000000" & I_OPC(4 downto 0);
Il faut faire aussi attention que avr_gcc peut ou pas changer les numéros de vecteurs d'interruption en fonction des composants.
Vous pouvez déclarer un service d'interruption de deux manières: par un numéro de vecteur ou par un nom. Si vous déclarez par nom (comme USART_RXC_vect) alors le compilateur pourra générer différents vecteurs pour différents composants. Si vous déclarez par des nombres (comme _VECTOR(13)) alors vous obtenez toujours le même vecteur quel que soit le composant spécifié comme cible. Déclarer par nom est certainement mieux pour des programmes qui seront portés dans plusieurs catégories de ATMegas mais en ce qui nous concerne déclarer avec des nombres est certainement plus approprié.
Ayant expliqué tout cela, la manière de procéder est ainsi:
- vérifier que les interrutions sont autoridées globalement
- vérifier que l'interruption en question est autorisée (attention aux incompatibilités déjà mentionnées plus haut)
- compiler le code et générer un listing assembleur (avec avr-objdump "avr-objdump -d hello.out")
- tester si les instructions JMP ou RJMP sont générées et changer le fichier opc_deco.vhd si nécessaire
- tester si le numéro de vecteur en assembleur correspond au numéro de vecteur dans io.vhd.
That hopefully does it. Good luck,
Juergen Sauermann
Pour votre information nous avons toujours compilé nos programmes en ciblant l'ATMega8 (voir début de l’Annexe III). Ainsi nous avons été obligé de faire le changement présenté par Juergen Sauermann dans le mail ci-dessus.
Mise en application
modifierNous présentons un petit programme d'interruption qui affiche ce qui arrive du PORT RS232 sur le PORTB
/* Ce programme fonctionne avec les modifications présentées plus haut */
#include "avr/io.h"
#include <avr/interrupt.h>
#undef F_CPU
#define F_CPU 25000000UL
// interruption de réception
ISR(USART_RXC_vect) // ISR(_VECTOR(11))
{
PORTB = 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 (;;);
}
Le bit "RXCIE" qui autorise les interruptions est implanté dans notre cœur mais pas le bit qui autorise la réception. Ainsi il est en fait inutile d'écrire (1<<RXEN) dans le registre UCSRB.
L'autorisation des interruptions par sei() est implantée dans le coreATMega8.
Dans toutes les architectures connues par nous (PIC16F et 68HC11), les flags (drapeaux en français) étaient mis à 1 par le matériel mais à 0 par le logiciel. Une autre manière de dire les choses est que les interruptions étaient responsables de cette mise à 0 de manière explicite. Il semblerait que pour les AVR ce ne soit pas explicitement le cas, parce qu’il n'y aurait pas de notion de drapeau. Nous avons eu l’occasion de lire des programmes avec interruptions mais jamais cette remise à 0 du flag était présente ! Regardez le programme ci-dessus : la remise à 0 du bit RXC n’est pas présente ! Cela semble logique dans le cas présent : la lecture de UDR positionne RXC à 0. Mais qu'en est-il du timer ? Voici par exemple un programme d'interruption du timer0
unsigned int cnt;
ISR(TIMER0_OVF_vect) {
cnt++; // increment counter
TCTNT0 = 96;
}
}
void main() {
unsigned char vPORTB=0x00;
TCCR0 = (1<<CS01) | (1<<CS00); // Assign prescaler to TMR0
TRISB = 0xFF; // PORTB is output
PORTB = 0xFF; // Initialize PORTB
TCNT0 = 96; // Timer0 initial value
TIMSK=(1<<TOIE0); // Enable TMRO interrupt
sei();
cnt = 0; // Initialize cnt
do {
if (cnt >= 400) {
PORTB = ~PORTB; // Toggle PORTB LEDs
cnt = 0; // Reset cnt
}
} while(1);
}
où vous ne voyez pas apparaître de mise à 0 du bit TOV0 dans l'interruption. La question qu’il nous vient est : est-ce l'instruction "TCTNT0 = 96" ou l'interruption elle-même qui met TOV0 à 0 ? Pour y répondre, nous avons lu le code source du cœur AVR qui nous a conforté dans l’idée que c’était l'entrée dans l'interruption qui opérait cette remise à 0.
Les périphériques avec interruption seront examinés dans le chapitre suivant...
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
modifierArrivé à ce point, il nous faut certainement rappeler simplement ce que l’on cherche à faire.
La gestion de l'écran VGA peut se présenter comme une boite, certes complexe, mais qui sait dessiner :
- une balle
- deux raquettes
La seule chose qui sera demandé au processeur sera de gérer toutes les coordonnées de ces éléments.
Toute interface entre un processeur et de la logique externe passe par des ports.
À la fin d'un 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 : nous avions 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 pour notre cœur rien que pour gérer les positions X et Y d'une balle sur l'écran. Il faut naturellement ajouter un port par raquette.
Retour sur la gestion des PORTs dans le CoreAtMega8
modifierLa gestion des ports du CoreAtMege8 est relativement simple. Il suffit de modifier le fichier io.vhd du projet initial. Les PORTs existants (je veux dire ceux connus du compilateur C) sont en nombre suffisant pour ce projet. Les raquettes sont finalement gérées sur 8 bits seulement, ce qui fait en tout, seulement 6 PORTs à trouver.
Le fichier VHDL qui fait la gestion s’appelle io2.vhd et remplace le fichier io.vhd du projet initial. Nous en donnons le contenu maintenant. (On a laissé le composant qui gère la communication RS232 du cœur initial, on a ajouté le composant VGA_Top et modifié le "IO Write" process). Ce fichier un peu long est, par conséquent, mis dans une boîte déroulante :
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;
entity io is
port ( I_CLK : in std_logic;
--> added for VGA synchro 18 June 2010
I_CLK_50 : in std_logic;
--< end added
I_CLR : in std_logic;
I_ADR_IO : in std_logic_vector( 7 downto 0);
I_DIN : in std_logic_vector( 7 downto 0);
I_SWITCH : in std_logic_vector( 7 downto 0);
I_RD_IO : in std_logic;
I_RX : in std_logic;
I_WE_IO : in std_logic;
Q_7_SEGMENT : out std_logic_vector( 6 downto 0);
Q_DOUT : out std_logic_vector( 7 downto 0);
Q_INTVEC : out std_logic_vector( 5 downto 0);
Q_LEDS : out std_logic_vector( 1 downto 0);
Q_TX : out std_logic;
hsynch,vsynch,red,green,blue : out STD_LOGIC);
end io;
architecture Behavioral of io is
component uart
generic(CLOCK_FREQ : std_logic_vector(31 downto 0);
BAUD_RATE : std_logic_vector(27 downto 0));
port( I_CLK : in std_logic;
I_CLR : in std_logic;
I_RD : in std_logic;
I_WE : in std_logic;
I_RX : in std_logic;
I_TX_DATA : in std_logic_vector(7 downto 0);
Q_RX_DATA : out std_logic_vector(7 downto 0);
Q_RX_READY : out std_logic;
Q_TX : out std_logic;
Q_TX_BUSY : out std_logic);
end component;
signal U_RX_READY : std_logic;
signal U_TX_BUSY : std_logic;
signal U_RX_DATA : std_logic_vector( 7 downto 0);
signal L_INTVEC : std_logic_vector( 5 downto 0);
signal L_LEDS : std_logic;
signal L_RD_UART : std_logic;
signal L_RX_INT_ENABLED : std_logic;
signal L_TX_INT_ENABLED : std_logic;
signal L_WE_UART : std_logic;
component VGAtop
PORT (clk_50 : in STD_LOGIC;
x_rect, y_rect: IN STD_LOGIC_VECTOR(9 DOWNTO 0);
y_raquG, y_raquD: IN STD_LOGIC_VECTOR(7 DOWNTO 0);
hsynch,vsynch,red,green,blue : out STD_LOGIC);
END component;
signal Balle_xLow : STD_LOGIC_VECTOR(7 DOWNTO 0);
signal Balle_xHigh : STD_LOGIC_VECTOR(7 DOWNTO 0);
signal Balle_yLow : STD_LOGIC_VECTOR(7 DOWNTO 0);
signal Balle_yHigh : STD_LOGIC_VECTOR(7 DOWNTO 0);
signal raqD_y : STD_LOGIC_VECTOR(7 DOWNTO 0);
signal raqG_y : STD_LOGIC_VECTOR(7 DOWNTO 0);
begin
urt: uart
generic map(CLOCK_FREQ => std_logic_vector(conv_unsigned(25000000, 32)),
BAUD_RATE => std_logic_vector(conv_unsigned( 38400, 28)))
port map( I_CLK => I_CLK,
I_CLR => I_CLR,
I_RD => L_RD_UART,
I_WE => L_WE_UART,
I_TX_DATA => I_DIN(7 downto 0),
I_RX => I_RX,
Q_TX => Q_TX,
Q_RX_DATA => U_RX_DATA,
Q_RX_READY => U_RX_READY,
Q_TX_BUSY => U_TX_BUSY);
vga:VGAtop
port map (clk_50 => I_CLK_50,
x_rect(7 downto 0) => Balle_xLow,
x_rect(9 downto 8) => Balle_xHigh(1 downto 0),
y_rect(7 downto 0) => Balle_yLow,
y_rect(9 downto 8) => Balle_yHigh(1 downto 0),
y_raquG => raqG_y,
y_raquD => raqD_y,
hsynch => hsynch,
vsynch => vsynch,
red => red,
green => green,
blue => blue);
-- 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 others => Q_DOUT <= X"AA";
end case;
end process;
-- IO write process
--
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"31" => Balle_xLow <= I_DIN; --DDRD
when X"32" => Balle_xHigh <= I_DIN; --PORTD
when X"37" => Balle_yLow <= I_DIN; --DDRB
when X"38" => Balle_yHigh <= I_DIN; --PORTB
when X"34" => raqD_y <= I_DIN; --DDRC
when X"35" => raqG_y <= I_DIN; --PORTC
when X"40" => -- 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;
-- interrupt process
--
ioint: process(I_CLK)
begin
if (rising_edge(I_CLK)) then
if (I_CLR = '1') then
L_INTVEC <= "000000";
else
if (L_RX_INT_ENABLED and U_RX_READY) = '1' then
if (L_INTVEC(5) = '0') then -- no interrupt pending
L_INTVEC <= "101011"; -- _VECTOR(11)
end if;
elsif (L_TX_INT_ENABLED and not U_TX_BUSY) = '1' then
if (L_INTVEC(5) = '0') then -- no interrupt pending
L_INTVEC <= "101100"; -- _VECTOR(12)
end if;
else -- no interrupt
L_INTVEC <= "000000";
end if;
end if;
end if;
end process;
L_WE_UART <= I_WE_IO when (I_ADR_IO = X"2C") else '0'; -- write UART UDR
L_RD_UART <= I_RD_IO when (I_ADR_IO = X"2C") else '0'; -- read UART UDR
Q_LEDS(1) <= L_LEDS;
Q_LEDS(0) <= not L_LEDS;
Q_INTVEC <= L_INTVEC;
end Behavioral;
La lecture de ce programme VHDL nous montre immédiatement les choix technologiques effectués pour la connexion des PORTs aux coordonnées des balles et raquettes.
- choix technologiques
E/S module VGA E/S AVR x_rect<9:8> PORTD<1:0> x_rect<7:0> DDRD<7:0> y_rect<9:8> PORTB<1:0> y_rect<7:0> DDRB<7:0> y_raqG<7:0> PORTC<7:0> y_raqD<7:0> DDRC<7:0>
Il peut être choquant de voir utiliser les ports de direction DDRB, DDRC et DDRD comme ports de sortie. Il est impossible de faire cela avec un processeur réel. Pour les processeurs softcore, c’est parfois possible comme ici et pour le silicore1657 mais pas pour le CQPIC 16F84. Il n'y a donc aucune règle générale à ce sujet ! Seuls des tests permettent de trancher !
Nous sommes prêt pour la programmation en C de notre ensemble cœur plus gestion écran VGA.
Programmation du nouveau cœur avec écran VGA
modifierNous 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
modifierNous commençons par présenter un sous-programme qui écrit une valeur 16 bits dans deux des nouveaux PORTs pour changer la position en X de la balle. Le sous-programme "setX" Une version en C pur est montrée ci-dessous :
void setX(uint16_t x){
DDRD=x; //poids faible
PORTD=x>>8;//poids fort
}
Le contenu de ce programme est complètement déterminé par la partie matérielle.
Recopie des interrupteurs pour déplacer la balle
modifierNous allons présenter maintenant un programme fonctionnel qui déplace la balle dans une position déterminée par les interrupteurs.
#include <avr/io.h>
void setX(uint16_t x);
void setY(unsigned int x);
unsigned int posRaqu_16;
void main (void)
{
unsigned int posX,posY;
unsigned char raqD_y=0,raqG_y=0;
signed char deltaX=1,deltaY=1;
posX=113;
posY=101;
setX(posX);
setY(posY);
while(1){
//setY directement
DDRB=PINB;
setX(PINB);
}
}
void setX(uint16_t x){
DDRD=x; //poids faible
PORTD=x>>8;//poids fort
}
void setY(unsigned int y){
DDRB=y; //poids faible
PORTB=y>>8;//poids fort
}
Remarquez qu'au lieu d’utiliser le sous-programme setY on a réalisé l'affectation directe dans DDRB. Remarquez aussi les deux types distincts pour travailler sur 16 bits: uint16_t et unsigned int.
Programme complet de gestion simple de la balle
modifierNous présentons maintenant un programme simple de gestion de la balle avec des rebonds :
#include <avr/io.h>
#include <util/delay.h>
void setX(uint16_t x);
void setY(unsigned int x);
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);
_delay_loop_2(30000);
}
}
void setX(uint16_t x){
DDRD=x; //poids faible
PORTD=x>>8;//poids fort
}
void setY(unsigned int y){
DDRB=y; //poids faible
PORTB=y>>8;//poids fort
}
Ce programme réalise une balle rebondissant sur les bords. Le petit nombre des trajectoires gérées, peut devenir lassant pour un jeu. Mais à ce stade personne ne peut jouer car les raquettes sont invisibles.
Ajouter les bords, et rendre les raquettes mobiles
modifierNous avons déjà eu l’occasion de présenter les choix technologiques réalisés pour la connexion des deux raquettes. Le hardware doit permettre de voir les raquettes, ce sera le logiciel qui les fera bouger.
Solution simple sans bord
modifierNous allons présenter un ensemble fonctionnant mais avec des positions de raquette fixes. Ce sera aux étudiants de les faire bouger. 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 :
--VHDL
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'instanciation des rectangles pour la balle et les raquettes se fera alors de la manière suivante :
-- VHDL
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. Rappelons que les raquettes sont fixes, il vous faudra modifier ce programme pour les faire bouger. Le matériel est lui prévu pour les faire bouger comme on l'a vu dans la section "La gestion des PORTs dans le CoreAtMega8".
#include <avr/io.h>
#include "util/delay.h"
/* Port A */
// Les ports ci-dessous n'existent pas dans l'ATMega8 de la vraie vie mais nous les avons parfois 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)
void setX(uint16_t x);
void setY(unsigned int x);
//void wait(unsigned char tempo);
//void wait(int tempo);
unsigned int posRaqu_16;
void main (void)
{
unsigned int posX,posY;
unsigned char raqD_y=0,raqG_y=0;
signed char deltaX=1,deltaY=1;
while(1) {
posX=130;
posY=301;
setX(posX);
setY(posY);
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);
// manque la gestion des raquettes ici
_delay_loop_2(30000);
}
}
}
void setX(uint16_t x){
DDRD=x; //poids faible
PORTD=x>>8;//poids fort
}
void setY(unsigned int x){
DDRB=x; //poids faible
PORTB=x>>8;//poids fort
}
Comme on peut le remarquer en lisant les commentaires de ce programme, la gestion des raquettes avec leurs déplacement n’est pas réalisée dans ce programme. Ce travail est présenté un peu plus loin avec sa correction.
Travail à réaliser
modifierLe contenu de cette section est lié au fait qu'un des auteurs a utilisé cette page comme énoncé de projet avec ses étudiants (année scolaire 2010/2011). Le fait qu'on y trouve les solutions est lié aussi à l'avancement du projet qui est terminé maintenant. Si vous utilisez cette page aussi avec vos étudiants, il vous faudra certainement adapter complètement cette section "Travail à réaliser".
Comprendre la partie matérielle (projet tutoré)
modifierVous devez être capable de dessiner les composants et leurs liaisons à partir des fichiers VHDL donnés dans le projet initial.
Vous allez traduire en français le chapitre 8 sur les Entrées/Sorties du cours (lecture.pdf) et me refaire le tableau des instructions.
Pour information le fichier lecture.pdf est un cours en anglais, fourni avec le projet original.
Développer en C
modifierÉtendre la partie logicielle pour gérer le déplacement des deux raquettes. Tests de bits des PORTs (pour le déplacement des raquettes) Chaque port est défini dans le fichier "avr/io.h". Par exemple PORTB est défini comme #define PORTB _SFR_IO8(0x18) car il est en position 0x18=24 dans l'espace d'entrées/sorties (soit en adresse 0x38). Chacun des bits des PORTs est aussi prédéfini :
//********** langage C ********
/* PORTB */
#define PB7 7
#define PB6 6
#define PB5 5
#define PB4 4
#define PB3 3
#define PB2 2
#define PB1 1
#define PB0 0
/* DDRB */
#define DDB7 7
#define DDB6 6
#define DDB5 5
#define DDB4 4
#define DDB3 3
#define DDB2 2
#define DDB1 1
#define DDB0 0
/* PINB */
#define PINB7 7
#define PINB6 6
#define PINB5 5
#define PINB4 4
#define PINB3 3
#define PINB2 2
#define PINB1 1
#define PINB0 0
Muni de ces informations, vous pouvez comprendre que pour tester que le bit PINB1 du port PINB est à un il suffit d'écrire en C :
//********** langage C ********
/* Port B */
if ((PINB &(1<<PINB1))==(1<<PINB1))
/* ou mieux encore */
if (bit_is_set(PINB, PINB1))
Nous avons choisi le PINB connecté aux interrupteurs sw0,sw1,sw6 et sw7 de la carte, pour faire descendre et monter les raquettes mais si vous disposez de joysticks, tout autre choix peut être fait. Ajouter un PORT dans notre cœur (inutile si raquettes sur 8 bits) Par défaut le PORTA n'existe pas dans l'ATMega8. Mais pour nous ce n’est pas un problème pour l'ajouter matériellement (dans le fichier io2.vhd) et logiciellement. Il nous suffit d'ajouter :
//********** langage C ********
/* Port A */
#define PINA _SFR_IO8(0x19)
#define DDRA _SFR_IO8(0x1A)
#define PORTA _SFR_IO8(0x1B)
//#define PORTA _SFR_MEM8(0x3B)
dans nos programmes en C. Notre fichier "io2.vhd" (voir la section La gestion des PORTs dans le CoreAtMega8) doit être modifié en conséquence (en utilisant les adresses ci-dessus auxquelles on ajoute 0x20) :
--************ extraits de io2.vhd ************
case I_ADR_IO
when X"31" => Balle_xLow <= I_DIN; --DDRD
when X"32" => Balle_xHigh <= I_DIN; --PORTD
when X"37" => Balle_yLow <= I_DIN; --DDRB
when X"38" => Balle_yHigh <= I_DIN; --PORTB
when X"34" => raqD_y <= I_DIN; --DDRC
when X"35" => raqG_y <= I_DIN; --PORTC
when X"3B" => s_scored <= I_DIN; -- PORTA
when X"3A" => s_scoreg <= I_DIN; -- DDRA
when X"40" => -- 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;
On ne donne que le contenu à compléter dans le programme qui est donné plus haut :
// gestion des raquettes 2bits PORTB/raquette
if (bit_is_set(PINB,PINB6)) if (raqG_y<215) raqG_y++;
if (bit_is_set(PINB,PINB7)) if (raqG_y>0) raqG_y--;
PORTC=raqG_y; //positionnement de raquette gauche en PORTC
if ((PINB &(1<<PINB0))==(1<<PINB0)) if (raqD_y<215) raqD_y++;
if ((PINB &(1<<PINB1))==(1<<PINB1)) if (raqD_y>0) raqD_y--;
DDRC=raqD_y; //positionnement de raquette droite en DDRC
Tracé de Bresenham
modifierExplorer s'il n’est pas possible d’utiliser l'algorithme de tracé de segment de droite de 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.
Premièrement : modifier le programme pour utiliser setXY en lieu et place de setX et setY
/* Port A */
// Les ports ci-dessous n'existent pas dans l'ATMega8 de la vraie vie mais nous les avons parfois 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)
#include <avr/io.h>
#undef F_CPU
#define F_CPU 25000000UL
#include "util/delay.h"
void setXY(uint16_t x,unsigned int y);
unsigned int posRaqu_16;
void main (void){
unsigned int posX,posY;
unsigned char raqD_y=0,raqG_y=0;
signed char deltaX=1,deltaY=1;
while(1) {
posX=130;
posY=301;
setXY(posX,posY);
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;
if ((posY>=460) && (deltaY>0)) deltaY= -deltaY;
if ((posY<=10) && (deltaY<0)) deltaY= -deltaY;
posY=posY+deltaY;
setXY(posX,posY);
// gestion des raquettes 2bits PORTB/raquette
if (bit_is_set(PINB,PINB6)) if (raqG_y<215) raqG_y++;
if (bit_is_set(PINB,PINB7)) if (raqG_y>0) raqG_y--;
PORTC=raqG_y;
if ((PINB &(1<<PINB0))==(1<<PINB0)) if (raqD_y<215) raqD_y++;
if ((PINB &(1<<PINB1))==(1<<PINB1)) if (raqD_y>0) raqD_y--;
DDRC=raqD_y;
_delay_loop_2(30000);
}
}
}
void setXY(uint16_t x,unsigned int y){
DDRD=x; //poids faible
PORTD=x>>8;//poids fort
DDRB=y; //poids faible
PORTB=y>>8;//poids fort
}
Pour l’algorithme de tracé de segment de Bresenham, le programme le plus compact trouvé sur internet est :
//********* http://rosettacode.org/wiki/Bitmap/Bresenham's_line_algorithm
void line(int x0, int y0, int x1, int y1) {
int dx = abs(x1-x0), sx = x0<x1 ? 1 : -1;
int dy = abs(y1-y0), sy = y0<y1 ? 1 : -1;
int err = (dx>dy ? dx : -dy)/2, e2;
for(;;){
setPixel(x0,y0);
if (x0==x1 && y0==y1) break;
e2 = err;
if (e2 >-dx) { err -= dy; x0 += sx; }
if (e2 < dy) { err += dx; y0 += sy; }
}
}
La première chose que fait l'algorithme est de calculer dx et dy. En ce qui nous concerne, nous ne ferons pas ce calcul puisque nous gérerons directement ce dx et ce dy. Entre parenthèse, cela nous évite de calculer des points d'intersections de notre trajectoire avec les bords de l'écran.
En remarquant que x0 est notre posX, y0 est notre posY, sx est notre deltaX et donc sy notre delatY, il vient :
#include <avr/io.h>
#include "util/delay.h"
void setXY(uint16_t x,unsigned int y);
unsigned int posRaqu_16;
void main (void) {
unsigned int posX,posY;
unsigned char raqD_y=0,raqG_y=0;
signed char deltaX=1,deltaY=1;
// int err = (dx>dy ? dx : -dy)/2, e2; montre comment calculer err
int dx=10,dy=10,err=-5,e2; //Pour Bresenham
while(1) {
posX=130;
posY=301;
setXY(posX,posY);
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;
//tempo -=10000;
}
posRaqu_16=raqG_y<<1;
if ((posX<=32) && (posY<posRaqu_16+58) && (posY+10>posRaqu_16) && (deltaX<0)) {
deltaX= -deltaX;
//tempo -=10000;
}
//posX=posX+deltaX; Retiré pour Bresenham
if ((posY>=460) && (deltaY>0)) deltaY= -deltaY;
if ((posY<=10) && (deltaY<0)) deltaY= -deltaY;
//*********** Début de Brensenham
e2 = err;
if (e2 >-dx) { err -= dy; posX += deltaX; }
if (e2 < dy) { err += dx; posY += deltaY; }
// posY=posY+deltaY;
setXY(posX,posY);
//*********** fin de Bresenham
// gestion des raquettes 2bits PORTB/raquette
if (bit_is_set(PINB,PINB6)) if (raqG_y<215) raqG_y++;
if (bit_is_set(PINB,PINB7)) if (raqG_y>0) raqG_y--;
PORTC=raqG_y;
if ((PINB &(1<<PINB0))==(1<<PINB0)) if (raqD_y<215) raqD_y++;
if ((PINB &(1<<PINB1))==(1<<PINB1)) if (raqD_y>0) raqD_y--;
DDRC=raqD_y;
_delay_loop_2(30000);
}
}
}
void setXY(uint16_t x,unsigned int y){
DDRD=x; //poids faible
PORTD=x>>8;//poids fort
DDRB=y; //poids faible
PORTB=y>>8;//poids fort
}
Certains se demandent peut-être ce qu’il y a de Bresenham dans le programme ci-dessus. Je vous rappelle que l'algorithme original permet de calculer les points d'une droite sans multiplication et sans division, c’est ce que l’on fait ici. Ce qui caractérise cet algorithme c’est que la pente de la droite tracée est dy/dx, c'est-à-dire un nombre rationnel, c’est ce que l’on a ici encore une fois. Changez la ligne
// int err = (dx>dy ? dx : -dy)/2, e2; montre comment calculer err
int dx=10,dy=10,err=-5,e2; //Pour Bresenham
en
// int err = (dx>dy ? dx : -dy)/2, e2; montre comment calculer err
int dx=10,dy=5,err=+5,e2; //Pour Bresenham
ou encore en
// int err = (dx>dy ? dx : -dy)/2, e2; montre comment calculer err
int dx=10,dy=15,err=-7,e2; //Pour Bresenham
pour voir les différentes pentes du déplacement de la balle.
Aller plus loin consiste à traiter les rebonds sur les raquettes, par exemple, de la manière suivante :
- dx est fixé à 10 et ne change jamais
- si les raquettes sont immobiles au moment du rebond dy ne change pas
- si le déplacement de la balle (suivant y) et de la raquette ont même signe on augmente dy en ne dépassant jamais 15
- si le déplacement de la raquette et de la balle (suivant y) sont en sens contraires on diminue dy en ne dépassant jamais 5
Le problème c’est qu'un bug (matériel ou logiciel ?) fait que dès que je rajoute quelque chose après le test de rebond sur la raquette droite la balle traverse cette raquette !!! Et cela ne se passe pas sur la raquette gauche, de quoi en perdre son latin !
Nous donnons une version exagérée (du point de vue du changement de pente) qui satisfait les conditions de rebonds sur la raquette gauche. (Rien sur la droite tant qu'aucune correction du bug n'est réalisée).
#include <avr/io.h>
#include "util/delay.h"
void setXY(uint16_t x,unsigned int y);
unsigned int posRaqu_16;
void main (void)
{
unsigned int posX,posY;
unsigned char raqD_y=0,raqG_y=0;
signed char deltaX=1,deltaY=1,delta_raqG_y,delta_raqD_y;
// int err = (dx>dy ? dx : -dy)/2, e2; montre comment calculer err
int dx=10,dy=5,err=-5,e2; //Pour Bresenham
while(1) {
posX=130;
posY=301;
setXY(posX,posY);
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; // rebond sur raquette droite
//tempo -=10000;
}
posRaqu_16=raqG_y<<1;
if ((posX<=32) && (posY<posRaqu_16+58) &&
(posY+10>posRaqu_16) && (deltaX<0)) {
deltaX= -deltaX; // rebond sur raquette gauche
if (delta_raqG_y) { // delta_raqG_y!=0 on change la pente
if ((delta_raqG_y >0 && deltaY >0) ||(delta_raqG_y <0 && deltaY <0)) {
dy+=5; if (dy > 15) dy=15; // ne peut dépasser 15
} else {
dy-=5; if (dy < 5) dy=5; // ne peut dépasser 5
}
}
}
//posX=posX+deltaX; Retiré pour Bresenham
if ((posY>=460) && (deltaY>0)) deltaY= -deltaY;
if ((posY<=10) && (deltaY<0)) deltaY= -deltaY;
//*********** Début de Brensenham
e2 = err;
if (e2 >-dx) { err -= dy; posX += deltaX; }
if (e2 < dy) { err += dx; posY += deltaY; }
// posY=posY+deltaY;
setXY(posX,posY);
//*********** fin de Bresenham
// gestion des raquettes 2bits PORTB/raquette
delta_raqG_y=0;
if (bit_is_set(PINB,PINB6)) if (raqG_y<215) delta_raqG_y=1;
if (bit_is_set(PINB,PINB7)) if (raqG_y>0) delta_raqG_y=-1;
raqG_y = raqG_y + delta_raqG_y;
PORTC=raqG_y;
delta_raqD_y=0;
if ((PINB &(1<<PINB0))==(1<<PINB0)) if (raqD_y<215) delta_raqD_y=1;
if ((PINB &(1<<PINB1))==(1<<PINB1)) if (raqD_y>0) delta_raqD_y=-1;
raqD_y = raqD_y + delta_raqD_y;
DDRC=raqD_y;
_delay_loop_2(30000);
}
}
}
Gestion des scores
modifierGérer un affichage des scores dans le moniteur RS232. Chaque fois qu'un nouveau score est réalisé, il est envoyé sur la liaison série qui est affichée à l'aide d'un hyperterminal. C'est pour répondre à cette question que l’on a laissé la partie concernant la RS232 dans le cœur initial.
Correction complète
modifierLa correction complète est dans le fichier ATMega8_pong_VGA.zip et se trouve dans le répertoire "CorrProjet2010". Dans cette correction, nous n'avons pas respecté le cahier des charges pour la gestion des scores : ils sont affichés directement sur l'écran VGA et non envoyés par la liaison série, comme le montre la figure. Bien sûr les entrées présentées dans la figure sont reliées à des PORTs du processeur embarqué que l’on a pris soin de relier comme suit :
- choix technologiques
E/S module VGA E/S AVR x_rect<9:8> PORTD<1:0> x_rect<7:0> DDRD<7:0> y_rect<9:8> PORTB<1:0> y_rect<7:0> DDRB<7:0> y_raqG<7:0> PORTC<7:0> y_raqD<7:0> DDRC<7:0> scoreD<7:0> PORTA<7:0> scoreG<7:0> DDRA<7:0>
Ainsi l’affichage d'une valeur sur le score droit se fait simplement par PORTA = valeur; en prenant bien soin d'y mettre une valeur BCD.
Le tableau ci-dessus est à mettre en correspondance avec le "IO wite process" (extrait du fichier io2.vhd) donné plus haut si vous voulez adapter ce travail à vos propres périphériques. Vous y trouvez dans les deux cas la même information vous donnant comment faire bouger un score ou une raquette.
Une version légèrement différente est donnée sous forme de figure dans Autres projets pour ATMEL ATMega8 si vous avez besoin d'explications supplémentaires.
Recherche de bogue dans ce cœur
modifierQuand ce projet a débuté, nous avons travaillé avec des anciens fichiers montrant un bogue. En fait les dernières versions et la version que nous proposons à télécharger (ATMega8_pong_VGA.zip) sont corrigées. Ne sachant pas qu'une correction avait été faite, nous nous sommes mis en quête de corriger le bogue et il nous semble utile d’en raconter l'histoire.
Nous partons de la situation suivante : nous connaissons mal l'assembleur de l'AVR. Nous allons donc travailler en C et montrer qu’il est plus difficile traquer un bogue en langage C qu'en assembleur.
Erreur complètement incompréhensible en C
modifierVoici présenté succinctement ce qui nous arrive. Le morceau de programme ci-dessous fonctionne correctement.
//******** petit bout de code qui fonctionne ****************
if ((posX>=574) && (posY<posRaqu_16+58) && (posY+10>posRaqu_16) && (deltaX>0)){
deltaX= -deltaX; // rebond sur raquette droite
}
On en déduit qu’il est capable de faire les tests du if correctement. Mais dès que l’on ajoute quelque chose dans ce if, après le deltaX=-deltaX, comme par exemple :
//******** petit bout de code qui ne fonctionne pas ****************
if ((posX>=574) && (posY<posRaqu_16+58) && (posY+10>posRaqu_16) && (deltaX>0)){
deltaX= -deltaX; // rebond sur raquette droite
if (delta_raqD_y) { // delta_raqD_y!=0 on change la pente
if ((delta_raqD_y >0 && deltaY >0) ||(delta_raqD_y <0 && deltaY <0)) {
dy+=5; if (dy > 15) dy=15; // ne peut dépasser 15
} else {
dy-=5; if (dy < 5) dy=5; // ne peut dépasser 5
}
}
}
eh bien cela ne fonctionne plus correctement : en fait l'instruction deltaX = - deltaX n'est jamais réalisée !!!
Si l’on reste à ce niveau, il n'y a aucune chance de trouver pourquoi. Comment ajouter des instructions peut-il perturber le test puisqu’il est déjà fait là où l’on ajoute ces instructions ?
On descend donc au niveau de l'assembleur pour voir ce qui s'y passe.
Même erreur en assembleur
modifierIl est possible de visualiser le programme assembleur généré. C'est un fichier qui a l'extension lss et voici les deux extraits incriminés. (La commande qui permet d’avoir le fichier lss est : avr-objdump -h -S hello.out > hello.lss)
Commençons par celui qui fonctionne :
if ((posX>=574) && (posY<posRaqu_16+58) && (posY+10>posRaqu_16) && (deltaX>0)){ 96: 82 e0 ldi r24, 0x02 ; 2 98: ee 33 cpi r30, 0x3E ; 62 9a: f8 07 cpc r31, r24 9c: 68 f0 brcs .+26 ; 0xb8 <main+0x70> 9e: c9 01 movw r24, r18 a0: ca 96 adiw r24, 0x3a ; 58 a2: 48 17 cp r20, r24 a4: 59 07 cpc r21, r25 a6: 40 f4 brcc .+16 ; 0xb8 <main+0x70> a8: ca 01 movw r24, r20 aa: 0a 96 adiw r24, 0x0a ; 10 ac: 28 17 cp r18, r24 ae: 39 07 cpc r19, r25 b0: 18 f4 brcc .+6 ; 0xb8 <main+0x70> b2: 17 fd sbrc r17, 7 b4: 11 95 neg r17 b6: 11 95 neg r17
Puis maintenant celui qui ne fonctionne pas :
if ((posX>=574) && (posY<posRaqu_16+58) && (posY+10>posRaqu_16) && (deltaX>0)){ 9c: 82 e0 ldi r24, 0x02 ; 2 9e: ee 33 cpi r30, 0x3E ; 62 a0: f8 07 cpc r31, r24 a2: 28 f1 brcs .+74 ; 0xee <main+0xa6> a4: c9 01 movw r24, r18 a6: ca 96 adiw r24, 0x3a ; 58 a8: 68 17 cp r22, r24 aa: 79 07 cpc r23, r25 ac: 00 f5 brcc .+64 ; 0xee <main+0xa6> ae: cb 01 movw r24, r22 b0: 0a 96 adiw r24, 0x0a ; 10 b2: 28 17 cp r18, r24 b4: 39 07 cpc r19, r25 b6: d8 f4 brcc .+54 ; 0xee <main+0xa6> b8: 11 16 cp r1, r17 ba: cc f4 brge .+50 ; 0xee <main+0xa6> deltaX= -deltaX; // rebond sur raquette droite bc: 11 95 neg r17
Comme on peut le voir le fait d'ajouter des instructions dans le if du test a changé sa façon de faire le test. Pour les développeurs de compilateurs cela doit paraître évident mais pour les simples utilisateurs que nous sommes, c’était difficile à imaginer.
Si dans le deuxième cas on sait que l’on n'a jamais delaX=-deltaX de réalisé, on en déduit que c’est l'instruction brge qui ne fonctionne pas correctement. Après 24 heures de réflexion nous n'étions plus aussi catégorique mais il fallait plutôt dire : c'est l'instruction brge qui ne fonctionne pas correctement quand elle est précédée de cp ou cpc et donc qu'en final c’est cp et cpc qui ne positionnent pas correctement le bit utilisé par brge, à savoir le drapeau S.
Nous prévoyons dans un futur proche (d'ici fin 2011) de tester l’ensemble des instructions de test, mais en assembleur cette fois-ci... mais il y a tellement de choses à faire... d'ici la fin 2011...
Le bogue ci-dessus a été en fait corrigé par l'auteur original du cœur (Juergen Sauermann). Si vous allez chez Opencores préférez le téléchargement par SVN plutôt que le fichier zip, ce premier étant plus à jour.
Les bogues matériels de ce chapitre ne sont pas les seules instructions à poser problème. Deux autres instructions seront corrigées dans le chapitre suivant qui passe notre ATMega8 en ATMega16. Nous vous conseillons d'utiliser plutôt l'ATMega16 pour vos projets. |
Conclusion
modifierCette mésaventure de bogue nous montre clairement l’intérêt de disposer d'un cœur libre : on peut toujours lire et modifier son code source. Certains nous rétorqueront que lorsqu'on achète, on a automatiquement des processeurs softcore sans bogue. Nous laissons au lecteur le soin de se faire sa propre opinion à partir de ses expériences passées dans le domaine du logiciel...
Rappel sur les versions de l'AVR
modifierLe projet décrit dans ce chapitre se trouve dans le répertoire /CorrProjet2010 de ATMega8_pong_VGA.zip. C'est une version sans bogue dans le processeur mais n'utilise pas la dernière version du processeur comme l'indique la remarque suivante.
L'écriture du chapitre Programmer in Situ et déboguer nous a obligé à faire évoluer le cœur embarqué coreATMega8. Même si la gestion d'interruptions, l'implantation de l'instruction SPM ne sont pas nécessaires pour le projet étudiant de PONG de cette section, le simple fait de vouloir utiliser data2mem (décrit lui aussi dans Programmer in Situ et déboguer) nous oblige à utiliser la dernière version de notre processeur. Cette version n’est pas disponible chez Opencores pour le moment mais nous la mettons à disposition dans le répertoire /AVRComplet16_S4_S4/25MHzAVR/ de notre ATMega8_pong_VGA.zip. C'est la version la plus à jour pour le moment, version pour laquelle nous avons vérifié les interruptions RS232 et l'instruction SPM.
Nous proposons depuis le 21 octobre 2013 une nouvelle ressource : arduinoMega8 encore plus à jour et discutée un peu plus loin.
Pour dire les choses autrement, vous ne pourrez pas utiliser data2mem avec la correction de ce chapitre (dans /CorrProjet2010 de ATMega8_pong_VGA.zip). Peut être qu'un jour viendra où tout sera parfait.... En tout cas la correction du projet 2011 sera complète de ce point de vue.
Un autre projet pour l'année suivante
modifierPour ne pas trop alourdir ce chapitre, le contenu de cette section a été déplacé en Autres projets pour Atmel ATMega8.
Et si l’on faisait un Arduino
modifierLa première version de l’Arduino était réalisée avec un ATMega8. On peut donc envisager de le réaliser avec le cœur présent dans ce chapitre. Pour ce faire il suffit d'envisager de réaliser un bootloader qui rendrait transparent le FPGA. Ceci est discuté dans un autre chapitre de ce livre. Pour information nous n'avons pas réussi à faire fonctionner correctement le bootloader mais notez que la ressource arduinoMega8 correspondante est la plus à jour de celles trouvées dans ce chapitre. Nous la prendrons comme point de départ de tout notre enseignement à partir de maintenant (Octobre 2013).
Quelques précautions d’emploi
modifierNous ne pouvons pas fournir une ressource universelle qui fonctionnerait avec un simple clic. Celle de cette section a les fonctionnalités suivantes :
- Les entrées et sorties du cœur ATMega8 ont été renommées. Cela vous impose de faire de même dans le fichier ucf !
- l'architecture de "io.vhd" commence par :
generic map(CLOCK_FREQ => std_logic_vector(conv_unsigned(50000000, 32)), -- Quartz frequency
BAUD_RATE => std_logic_vector(conv_unsigned( 38400, 28))) -- Baud rate
qui permet de régler la vitesse de transmission de la RS232 à partir de la fréquence d'horloge. La fréquence d'horloge est positionnée ici à 50 MHz car c’est la fréquence d'horloge de la carte (100 MHz pour Nexys 3) divisée par deux. Adaptez cette ligne à votre fréquence d'horloge.
- vous disposez d'un deuxième fichier "io_timer.vhd" pour les entrées sorties dans lequel il y a un timer très simple mais qui peut déclencher une interruption. Rappelons qu'un projet correct doit choisir entre "io.vhd" minimum et "io_timer.vhd". Ne prenez pas les deux fichiers en même temps !
- dans l'architecture de "opc_fetch.vhd", apparition d'une constante "constant BootLoader : boolean := FALSE;" au tout début de l'architecture. Celle-ci a pour objectif de choisir entre un fonctionnement avec et sans bootloader mais elle ne fonctionne correctement que positionnée à FALSE, c'est-à-dire sans bootloader !
Qui dit Arduino dit bootloader... À ce jour nous n'avons pas réussi à faire fonctionner correctement le bootloader ! Ceci est lié au fait que nous n'avons pas réussi à initialiser le compteur programme à autre chose que zéro ! malgré beaucoup d'essais. Peut être qu'un jour cela fonctionnera correctement !
- vous disposez d'un répertoire "/soft" dans lequel vous trouverez les ressources pour programmer ce cœur. Elles sont à jour pour une carte Nexys3 mais lisez la section projet coreATMega8 (du chapitre programmer in situ de ce livre) pour trouver les ressources correspondantes memory.bmm et ses contraintes dans l'ucf que nous avons testé pour les cartes spartan3 et spartan3E. Cela n'a pas été testé avec les Basys2 et Nexys2 mais nous pensons que cela doit fonctionner sans problème.
Et pour conclure
modifierCe chapitre est marqué comme terminé. Il y a pourtant bien des précisions à apporter, mais nous avons choisi de refaire un chapitre qui améliore le cœur ATMega8 en ATMega16 et étudie de manière plus systématique la réalisation des périphériques : voir Améliorer l'ATMega8 avec l'ATMega16 et l'ATMega32
Annexe I (transformer un fichier HEX en VHDL)
modifierPrésentation de la chaîne de compilation
modifierLa compilation d'un cœur embarqué proposé par un fondeur de FPGA nécessite l'EDK et le SDK chez Xilinx ainsi que l'ISE. L'EDK n’est pas gratuit mais l'ISE l'est à condition de s'inscrire sur le site de Xilinx. Il faut alors utiliser une licence de type webpack (gratuite). L'utilisation d'un processeur softcore libre représente l'énorme avantage de n'utiliser que l'ISE pour sa synthèse.
Nous présentons la chaîne de compilation pour savoir à quel gendre de problème nous devrons nous attaquer pour enfin exécuter un programme compilé par un compilateur du GNU dans notre FPGA.
À gauche nous avons présenté sans détail la chaîne de compilation Xilinx qui part de fichiers VHDL pour réaliser un fichier BIT avec l'ISE. Ce fichier sera téléchargé par Impact (ou adept ou autre) dans le composant. À droite nous avons la chaine de compilation de notre (ou nos) programme(s) C. L'obtention d'un fichier HEX n’est pas obligatoire, un fichier ELF est parfois suffisant.
Voici un script qui fait l’ensemble du travail :
#!/bin/bash
# changer ou commenter la ligne ci-dessous :
export PATH=$PATH:/usr/local/avr/bin:~/XILINX/Xilinx/11.1/ISE/bin/lin/
avr-gcc -g -mmcu=atmega8 -Wall -Os -c prog.c
avr-gcc -g -mmcu=atmega8 -o prog.elf -Wl,-Map,prog.map prog.o
#avr-objdump -h -S prog.elf > prog.lss
#avr-objcopy -O binary -R .eeprom prog.elf prog.bin
avr-objcopy -R .eeprom -O ihex prog.elf prog.hex
Utilisation de make_mem
modifierSi l’on veut que l'ISE nous génère un processeur susceptible d'exécuter un programme, il nous faut trouver un moyen de relier ces deux chaines de compilation. J'espère que vous aviez remarqué qu’elles sont séparées, aucune interaction entre les deux ! Il nous faudra en effet d'une manière ou d'une autre mettre le programme compilé dans le FPGA. Il y a plusieurs moyens de faire cela que nous allons présenter encore à l'aide de figures.
La première façon est présentée ci-dessus. On a ajouté un programme externe "make_mem" capable de transformer un fichier HEX en fichier VHDL qui sera inséré dans le projet (pour l'exemple avec le fichier mem_content.vhd). Cette façon de faire est à mon avis la plus simple et tout projet devrait la proposer au moins pour les débutants. Mais elle est cependant loin d’être un must. En effet tout changement dans le programme C nécessite de réaliser la compilation des programmes C ce qui prend quelques secondes puis de transformer le fichier HEX en fichier VHD ce qui ne prend aussi que quelques secondes et enfin de compiler le projet VHDL en entier ce qui peut prendre un certain temps (10 - 15 min sur un vieux PC). Lancer ISE pour compiler un projet important est toujours consommateur de temps. Il nous faut donc explorer d'autres solutions : ce sera fait après la section suivante.
Voici un script qui fait l’ensemble du travail :
#!/bin/bash
# changer ou commenter la ligne ci-dessous :
export PATH=$PATH:/usr/local/avr/bin:~/XILINX/Xilinx/11.1/ISE/bin/lin/
avr-gcc -g -mmcu=atmega8 -Wall -Os -c prog.c
avr-gcc -g -mmcu=atmega8 -o prog.elf -Wl,-Map,prog.map prog.o
#avr-objdump -h -S prog.elf > prog.lss
#avr-objcopy -O binary -R .eeprom prog.elf prog.bin
avr-objcopy -R .eeprom -O ihex prog.elf prog.hex
make_mem prog.hex CheminProjetISE/prog_mem_content.vhd
où vous devez définir le chemin de votre projet pour une mise à jour automatique dans votre projet matériel du fichier "prog_mem_content.vhd".
Source de make_mem
modifierLe programme C++ pour transformer le fichier HEX en fichier VHDL est présenté ci-dessous.
Une fois compilé mon utilitaire s’appelle make_mem.exe (sous windows) ou make_mem sous Linux.
L'utilisation est faite par la ligne de commande :
make_mem demo1.hex prog_mem_content.vhd
si le fichier à convertir s’appelle demo1.hex. Le fichier généré s’appelle prog_mem_content.vhd et se trouve sous la forme d'un package.
#include "assert.h"
#include "stdio.h"
#include "stdint.h"
#include "string.h"
uint8_t buffer[0x10000]; // 64 k is max. for Intel hex.
uint8_t slice [0x10000]; // 16 k is max. for Xilinx bram
//-----------------------------------------------------------------------------
//
// get a byte (from cp pointing into Intel hex file).
//
uint32_t
get_byte(const char * cp)
{
uint32_t value;
const char cc[3] = { cp[0], cp[1], 0 };
const int cnt = sscanf(cc, "%X", &value);
assert(cnt == 1);
return value;
}
//-----------------------------------------------------------------------------
//
// read an Intel hex file into buffer
void
read_file(FILE * in)
{
memset(buffer, 0xFF, sizeof(buffer));
char line[200];
for (;;)
{
const char * s = fgets(line, sizeof(line) - 2, in);
if (s == 0) return;
assert(*s++ == ':');
const uint32_t len = get_byte(s);
const uint32_t ah = get_byte(s + 2);
const uint32_t al = get_byte(s + 4);
const uint32_t rectype = get_byte(s + 6);
const char * d = s + 8;
const uint32_t addr = ah << 8 | al;
uint32_t csum = len + ah + al + rectype;
assert((addr + len) <= 0x10000);
for (uint32_t l = 0; l < len; ++l)
{
const uint32_t byte = get_byte(d);
d += 2;
buffer[addr + l] = byte;
csum += byte;
}
csum = 0xFF & -csum;
const uint32_t sum = get_byte(d);
assert(sum == csum);
}
}
//-----------------------------------------------------------------------------
//
// copy a slice from buffer into slice.
// buffer is organized as 32-bit x items.
// slice is organized as bits x items.
//
void copy_slice(uint32_t slice_num, uint32_t port_bits, uint32_t mem_bits)
{
assert(mem_bits == 0x1000 || mem_bits == 0x4000);
const uint32_t items = mem_bits/port_bits;
const uint32_t mask = (1 << port_bits) - 1;
const uint8_t * src = buffer;
memset(slice, 0, sizeof(slice));
for (uint32_t i = 0; i < items; ++i)
{
// read one 32-bit value;
const uint32_t v0 = *src++;
const uint32_t v1 = *src++;
const uint32_t v2 = *src++;
const uint32_t v3 = *src++;
const uint32_t v = (v3 << 24 |
v2 << 16 |
v1 << 8 |
v0 ) >> (slice_num*port_bits) & mask;
if (port_bits == 16)
{
assert(v < 0x10000);
slice[2*i] = v;
slice[2*i + 1] = v >> 8;
}
else if (port_bits == 8)
{
assert(v < 0x100);
slice[i] = v;
}
else if (port_bits == 4)
{
assert(v < 0x10);
slice[i >> 1] |= v << (4*(i & 1));
}
else if (port_bits == 2)
{
assert(v < 0x04);
slice[i >> 2] |= v << (2*(i & 3));
}
else if (port_bits == 1)
{
assert(v < 0x02);
slice[i >> 3] |= v << ((i & 7));
}
else assert(0 && "Bad aspect ratio.");
}
}
//-----------------------------------------------------------------------------
//
// write one initialization vector
//
void
write_vector(FILE * out, uint32_t mem, uint32_t vec, const uint8_t * data)
{
fprintf(out, "constant p%u_%2.2X : BIT_VECTOR := X\"", mem, vec);
for (int32_t d = 31; d >= 0; --d)
fprintf(out, "%2.2X", data[d]);
fprintf(out, "\";\r\n");
}
//-----------------------------------------------------------------------------
//
// write one memory
//
void
write_mem(FILE * out, uint32_t mem, uint32_t bytes)
{
fprintf(out, "-- content of p_%u --------------------------------------"
"--------------------------------------------\r\n", mem);
const uint8_t * src = slice;
for (uint32_t v = 0; v < bytes/32; ++v)
write_vector(out, mem, v, src + 32*v);
fprintf(out, "\r\n");
}
//-----------------------------------------------------------------------------
//
// write the entire memory_contents file.
//
void
write_file(FILE * out, uint32_t bits)
{
fprintf(out,
"\r\n"
"library IEEE;\r\n"
"use IEEE.STD_LOGIC_1164.all;\r\n"
"\r\n"
"package prog_mem_content is\r\n"
"\r\n");
const uint32_t mems = 16/bits;
for (uint32_t m = 0; m < 2*mems; ++m)
{
copy_slice(m, bits, 0x1000);
write_mem(out, m, 0x200);
}
fprintf(out,
"end prog_mem_content;\r\n"
"\r\n");
}
//-----------------------------------------------------------------------------
int
main(int argc, char * argv[])
{
uint32_t bits = 4;
const char * prog = *argv++; --argc;
if (argc && !strcmp(*argv, "-1")) { bits = 1; ++argv; --argc; }
else if (argc && !strcmp(*argv, "-2")) { bits = 2; ++argv; --argc; }
else if (argc && !strcmp(*argv, "-4")) { bits = 4; ++argv; --argc; }
else if (argc && !strcmp(*argv, "-8")) { bits = 8; ++argv; --argc; }
else if (argc && !strcmp(*argv, "-16")) { bits = 16; ++argv; --argc; }
const char * hex_file = 0;
const char * vhdl_file = 0;
if (argc) { hex_file = *argv++; --argc; }
if (argc) { vhdl_file = *argv++; --argc; }
assert(argc == 0);
FILE * in = stdin;
if (hex_file) in = fopen(hex_file, "r");
assert(in);
read_file(in);
fclose(in);
FILE * out = stdout;
if (vhdl_file) out = fopen(vhdl_file, "w");
write_file(out, bits);
assert(out);
}
//-----------------------------------------------------------------------------
Ce programme (une fois compilé) génèrera une description en BRAM (Block RAM) du programme (à exécuter par l'AVR), comme le montre la figure ci-dessous :
Conventions pour lire la figure :
- les 12 rectangles rouges en deux rangées non contigües représentent la BRAM du FPGA
- les parties en blancs (sous parties de certains rectangles rouges) représentent l’ensemble des blocs mémoires utilisés pour le programme.
Les blocs rouges non utilisés le sont en fait avec la mémoire RAM de ce cœur.
Puisque les rectangles blancs sont plus petit que les rectangles rouges qui les contiennent, cela veut dire qu’il est possible d'augmenter la taille programme de ce coreATMega8.
Examiner d'autres solutions
modifierCette solution est sur certains points plus détaillée dans le chapitre Programmer in Situ et déboguer de ce livre.
Voici donc cette autre solution. Comme le montre le dessin, elle utilise un utilitaire Xilinx qui s’appelle data2mem. Cette solution ne présente plus l'inconvénient précédent puisqu'elle ne nécessite plus une recompilation complète du projet VHDL : elle modifie directement le fichier BIT en y mettant le programme compilé.
Il nous faut retenir que data2mem est capable de traiter un fichier ELF. C'est le format de sortie par défaut des compilateurs du GNU et, je l'espère, d'autres compilateurs. Donc tout cœur embarqué qui utilise un compilateur du GNU peut utiliser cette technique. Elle est par exemple utilisée avec le microBlaze (de manière complètement transparente), et openMSP430 par exemple. Par contre, elle devient difficile à utiliser avec un PIC de la famille 16F qui a souvent une chaîne de compilation qui génère un fichier HEX !
La mauvaise nouvelle, il y en faut bien une, est que si l’on regarde attentivement la figure on voit qu'un nouveau fichier BMM est nécessaire (ici memory.bmm). En gros, si vous utilisez les cœurs embarqués estampillés Xilinx (picoBlaze ou microBlaze) vous obtiendrez ce fichier BMM de manière automatique (plus ou moins facilement) mais si vous sortez des sentiers battus vous devrez le créer de toute pièce et donc lire la documentation de 50 pages de data2mem. Nous avons lu quelque part que PlanAhead savait le créer automatiquement mais nous n'avons pas réussi à l’utiliser pour cela mais seulement pour trouver l'information nécessaire à la création de ce fichier BMM. Peut être qu'un lecteur sait le faire ?
Avant de terminer, nous présentons un script qui fait tout ce travail automatiquement.
Pour la carte Nexys3
modifierCe script (ci-dessous) peut être lancé sous Linux à partir du moment où vous avez vos trois fichiers disponibles :
- fichier source en c appelé ici your_program.c
- fichier memoryS6.bmm (décrit dans le chapitre Programmer in Situ et deboguer, section Projet coreATMega8)
- fichier atmega8.bit résultant d'une compilation complète de la partie matérielle (compilée avec Family=Spartan6, Device=XC6SLC16,Package=CSG324).
#!/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=atmega8 -Wall -Os -c your_program.c
avr-gcc -g -mmcu=atmega8 -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 atmega8.bit -o uh atmega8
# commentez ligne suivante si pas ADEPT installe
djtgcfg prog -d Nexys3 --index 0 --file atmega8_rp.bit
Nous avons déjà eu l’occasion de réaliser ce genre de script sous Windows avec un fichier "batch", ce qui ne pose pas de problème spécifique. En voici un exemple :
:: **** fichier pour terminal DOS sous Windows **********
set path = C:\WinAVR-20100110\bin
cd C:\Users\Benjamin\Desktop\"projet TR"\ANDROID
avr-gcc -g -mmcu=atmega8 -Wall -Os -c your_program.c
avr-gcc -g -mmcu=atmega8 -o your_program.elf -Wl,-Map,your_program.map your_program.o
C:\Xilinx\13.1\ISE_DS\ISE\bin\nt64\data2mem.exe -bm memoryS6.bmm -bd your_program.elf -bt atmega8.bit -o uh atmega8
djtgcfg prog -d Nexys3 --index 0 --file atmega8_rp.bit
cd c:\users\Benjamin
Évidemment les répertoires donnés ici sont à adapter. La présence du répertoire WinAVR montre que l’on a installé le compilateur C à partie de winAVR.
Quelques années après l'écriture de ces lignes, nous ne conseillerions plus d'installer WinAVR pour avoir le compilateur C pour AVR sous windows. Nous pensons qu'il est bien plus simple d'installer un environnement Arduino. Vous pouvez éventuellement utiliser l'environnement Arduino pour compiler vos programmes mais pas pour les télécharger. Il vous suffit pour cela de choisir une carte Arduino ancienne comportant un ATMega8 (Arduino NG or older → ATMega8).
Pour la carte Basys2
modifier#!/bin/bash
export PATH=$PATH:/usr/bin:/opt/Xilinx/14.5/ISE_DS/ISE/bin/lin64
avr-gcc -g -mmcu=atmega8 -Wall -Os -c your_program.c
avr-gcc -g -mmcu=atmega8 -o your_program.elf -Wl,-Map,your_program.map your_program.o
data2mem -bm memory.bmm -bd your_program.elf -bt atmega8.bit -o uh atmega8
# commentez ligne suivante si pas ADEPT installe
djtgcfg prog -d Basys2 --index 0 --file atmega8_rp.bit
Ce script peut être lancé sous Linux à partir du moment où vous avez vos trois fichiers disponibles :
- fichier source en c appelé ici your_program.c
- fichier memory.bmm (décrit dans le chapitre Programmer in Situ et deboguer, section Projet coreATMega8)
- fichier atmega8.bit résultant d'une compilation complète de la partie matérielle (compilée avec Family=Spartan3E, Device=XC3s250E, Package=CP132).
Pour la carte Spartan 3E Starter Board
modifierUn seul petit changement à la fin à faire quand nous aurons une carte sous la main. (Nous les laissons sous la forme ????? pour le moment)
#!/bin/bash
export PATH=$PATH:/usr/bin:/opt/Xilinx/14.5/ISE_DS/ISE/bin/lin64
avr-gcc -g -mmcu=atmega8 -Wall -Os -c your_program.c
avr-gcc -g -mmcu=atmega8 -o your_program.elf -Wl,-Map,your_program.map your_program.o
data2mem -bm memory.bmm -bd your_program.elf -bt atmega8.bit -o uh atmega8
# commentez ligne suivante si pas ADEPT installe
djtgcfg prog -d DCabUsb --index 0 --file atmega8_rp.bit
Ce script peut être lancé sous Linux à partir du moment où vous avez vos trois fichiers disponibles :
- fichier source en c appelé ici your_program.c
- fichier memory.bmm (décrit dans le chapitre Programmer in Situ et deboguer, section Projet coreATMega8)
- fichier atmega8.bit résultant d'une compilation complète de la partie matérielle (compilée avec Family=Spartan3E, Device=xc3s500e, Package=FG320).
Pour la carte Spartan-3 Starter Board
modifierUn seul petit changement à la fin du fichier. Ceci nécessite un câble USB.
#!/bin/bash
export PATH=$PATH:/usr/bin:/opt/Xilinx/14.5/ISE_DS/ISE/bin/lin64
avr-gcc -g -mmcu=atmega8 -Wall -Os -c your_program.c
avr-gcc -g -mmcu=atmega8 -o your_program.elf -Wl,-Map,your_program.map your_program.o
data2mem -bm memory.bmm -bd your_program.elf -bt atmega8.bit -o uh atmega8
# commentez ligne suivante si pas ADEPT installe
djtgcfg prog -d DCabUsb --index 0 --file atmega8_rp.bit
Ce script peut être lancé sous Linux à partir du moment où vous avez vos trois fichiers disponibles :
- fichier source en c appelé ici your_program.c
- fichier memory.bmm (décrit dans le chapitre Programmer in Situ et deboguer, section Projet coreATMega8)
- fichier atmega8.bit résultant d'une compilation complète de la partie matérielle (compilée avec Family=Spartan3, Device=xc3s200, Package=FT256).
Depuis un certain temps nous n'utilisons plus la carte ci-dessus et du coup nous ne sommes pas très sûr du "DCabUsb". À vérifier donc ! Pour le trouver il suffit de faire un "djtgcfg enum".
Annexe II : le fichier ucf
modifierVoici sans commentaires le fichier ucf :
NET I_CLK_50 PERIOD = 20 ns; NET L_CLK PERIOD = 35 ns; NET I_CLK_50 TNM_NET = I_CLK_100; NET L_CLK TNM_NET = L_CLK; NET "I_CLK_50" LOC = "T9"; #NET I_RX LOC = M3; #NET Q_TX LOC = M4; # single LEDs # NET Q_LEDS<0> LOC = "K12"; NET Q_LEDS<1> LOC = "P14"; NET Q_LEDS<2> LOC = "L12"; NET Q_LEDS<3> LOC = "N14"; # DIP switch(0 ... 7) and two pushbuttons (8, 9) # NET I_SWITCH<0> LOC = "f12"; NET I_SWITCH<1> LOC = "g12"; NET I_SWITCH<2> LOC = "h14"; NET I_SWITCH<3> LOC = "h13"; NET I_SWITCH<4> LOC = "j14"; NET I_SWITCH<5> LOC = "j13"; NET I_SWITCH<6> LOC = "k14"; NET I_SWITCH<7> LOC = "k13"; NET I_Reset<0> LOC = "L13"; NET I_Reset<1> LOC = "L14"; #NET I_SWITCH<*> PULLUP; #VGA net "hsynch" loc="R9"; net "vsynch" loc="T10"; net "red" loc="R12"; net "blue" loc="R11"; net "green" loc="T12";
Annexe III : Le GNU C
modifierPour compiler manuellement vous lancez :
avr-gcc -g -mmcu=atmega8 -Wall -Os -c hello.c avr-gcc -g -mmcu=atmega8 -o hello.out -Wl,-Map,hello.map hello.o avr-objcopy -R .eeprom -O ihex hello.out hello.hex
Si vous voulez connaitre le code source assembleur :
avr-objdump -S hello.out
puisqu' hello.out a été créé avec la deuxième commande à partir de hello.o
Quelques fonctions non standard de la librairie du C pour AVR sont données :
Fonction | Définition | Exemple |
---|---|---|
_BV(x) | positionne un bit spécifique | char result = _BV(PINA6); // met 64 dans result
|
void sbi (uint8_t port, uint8_t bit) | positionne un bit spécifique à 1 | sbi (PORTB, 3); //#define PORTB 0x18
sbi (PORTB, PINA3);
|
void cbi (uint8_t port, uint8_t bit) | positionne un bit spécifique à 0 | cbi (PORTB, 3); //#define PORTB 0x18
cbi (PORTB, PINA3);
|
uint8_t bit_is_set (uint8_t port, uint8_t bit); | teste si un bit spécifique est à 1 | //#define PINB 0x16
uint8_t result = bit_is_set (PINB, PINB3);
|
uint8_t bit_is_clear (uint8_t port, uint8_t bit); | teste si un bit spécifique est à 0 | //#define PINB 0x16
uint8_t result = bit_is_clear (PINB, PINB3);
|
uint8_t inp (uint8_t port); | lit un registre spécifique et retourne le résultat | //#define SREG 0x3F
uint8_t res = inp (SREG);
|
uint16_t __inw (uint8_t port); | lit un registre spécifique 16 bits et retourne le résultat | //#define TCNT1 0x2C
uint16_t res = __inw (TCNT1);
|
uint16_t __inw_atomic (uint8_t port); | lit un registre spécifique 16 bits et retourne le résultat sans interruption possible | //#define TCNT1 0x2C
uint16_t res = __inw (TCNT1);
|
outp (uint8_t val, uint8_t port); | sort une valeur spécifique sur un port spécifique | //#define PORTB 0x18
outp(0xFF, PORTB);
|
__outw (uint16_t val, uint8_t port); | sort une valeur spécifique 16 bits sur un port spécifique | //#define OCR1A 0x2A
__outw(0xAAAA, OCR1A);
|
__outw_atomic (uint16_t val, uint8_t port); | sort une valeur spécifique 16 bits sur un port spécifique sans interruption | //#define OCR1A 0x0A
__outw_atomic(0xAAAA, OCR1A);
|
Ressource Juillet 2015
modifierNous avons abandonné la rédaction de ce chapitre depuis quelques années maintenant. Mais comme nous avons eu l’occasion de travailler sur le bootloader de type Arduino pour l'ATMega8 en Juillet 2015, nous proposons une ressource beaucoup plus à jour. C'est la version la plus à jour, c’est-à-dire la plus proche de l'ATMega8 du commerce.
Le fichier arduinoMega8.zip contient la version avec la nouvelle instruction SPM. Le fichier progmemcontent.vhd contient le bootloader (qui ne fonctionne pas correctement). Cette version a été réalisée avec l'option bootloader à false, ce qui veut dire que vous pouvez l’utiliser normalement (avec data2mem). Nous y avons aussi inséré la possibilité de réaliser une mise à 0 automatique d'un drapeau d'interruption, comme pour l'ATMega16 d'un autre chapitre. Ceci est mis en œuvre dans le fichier io_timer.vhd. Rappelons encore que c’est la version la plus à jour.
Voir aussi
modifier- La version pdf, une version plus ancienne de ce document.
- Micro contrôleurs AVR, autre projet de la Wikiversité
- AVR Atmel et le WIKIBOOK (en) Atmel AVR ainsi que (en) jeu d'instruction des AVRs.
- la carte Spartan-3 Starter Board
- Le langage C chez Wikipédia
- La programmation C et ses exercices chez WikiBook
- Introduction au langage C chez Wikiversité
- ISE Xilinx pour développer en VHDL ou plus exactement le WebPack gratuit
- AVRStudio pour développer les programmes pour les AVRs, avec soit l'assembleur par défaut, soit le compilateur C du GNU qui est fourni avec AVRStudio. Si ce n’est pas le cas, il est facile à trouver et installer.
- Un autre ISE KamAVR pour développer autour de l’AVR Atmel.
AVR studio et KamAVR ne semblent pas fonctionner sous wine sous Linux contrairement à winAVR. En fait AVR studio devrait marcher à condition de lancer wine en utilisateur "root" pour son installation, ce que nous ne savons pas faire sous notre Linux ! L'environnement concurrent MPLAB (pour les PICs) fonctionne correctement sous Linux/wine et la nouvelle version MPLABX fonctionne parfaitement sous Linux sans passer par wine.