Encore un Pong sur FPGA
Un Pong ? Comme c’est original…
Mais après avoir mis en mouvement les raquettes, il restait à gérer les déplacements et rebonds de la p’tite balle.
Au niveau du matériel, je reprends :
- la carte FPGA DE0-nano ;
- le module PmodVGA, interface entre la carte FPGA et l’écran VGA ;
- deux encodeurs rotatifs avec des signaux en quadrature, surmontés chacun d’un magnifique bouton doré ;
- et cela va de soi, un câble VGA et un écran avec une entrée VGA.
J’ai déjà présenté l’architecture du projet au billet précédent, elle reste identique.
Les changement interviennent dans le module de dessin et d’animation drawing.sv :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
module drawing (
input logic clk25, rst,
input logic [9:0] x, y,
input logic signed [1:0] dir2,
input logic signed [1:0] dir1,
input logic inDisplayArea,
input logic hsync, vsync,
output logic [3:0] vga_r, vga_g, vga_b,
output logic vga_hsync, vga_vsync,
input logic frame
);
localparam PADDLE_WIDTH = 60;
localparam PADDLE_HEIGHT = 20;
localparam SCREEN_WIDTH = 640;
localparam SCREEN_HEIGHT = 480;
localparam INIT_POSITION = (SCREEN_WIDTH - PADDLE_WIDTH) / 2; // raquette initialement au centre
localparam X = 0;
localparam Y = 1;
logic [1:0][1:0] dir; // Tableau contenant 2 bus de 2 bits chacun, sens de rotation des encodeurs
assign dir = '{dir1, dir2};
logic [9:0] paddle [2][2] = '{
'{INIT_POSITION, 440}, // coordonnées paddle 1
'{INIT_POSITION, 20} // coordonnées paddle 2
};
integer speed = 10; // vitesse de déplacement des raquettes
localparam BALL_SIZE = 10; // Taille de la balle (carré)
localparam BALL_X = (SCREEN_WIDTH - BALL_SIZE) / 2; // Position centrale en X
localparam BALL_Y = (SCREEN_HEIGHT - BALL_SIZE) / 2; // Position centrale en Y
logic first_frame = 1; // Flag pour détecter la première exécution
logic [9:0] lfsr = 10'b1101010011; // graine du générateur pseudo-aléatoire
integer ball_x, ball_y;
integer ball_dx, ball_dy;
always_ff @(posedge clk25) begin
lfsr <= {lfsr[8:0], lfsr[9] ^ lfsr[6]}; // Mise à jour du LFSR
end
always_ff @(posedge clk25) begin // mouvement des raquettes
for (int i = 0; i < 2; i++) begin
if (dir[i] != 0) begin
if (dir[i] == 1) begin
paddle[i][X] <= (paddle[i][X] + speed < SCREEN_WIDTH - PADDLE_WIDTH) ? paddle[i][X] + speed : SCREEN_WIDTH - PADDLE_WIDTH;
end else begin
paddle[i][X] <= (paddle[i][X] > speed) ? paddle[i][X] - speed : 0;
end
end
end
end
always_ff @(posedge clk25) begin
if (rst==0) begin
first_frame <= 1; // active l'initialisation après le premier cycle
end else
begin
if (frame) begin
if (first_frame) begin
ball_x <= lfsr % (SCREEN_WIDTH - BALL_SIZE);
ball_y <= SCREEN_HEIGHT / 2; // Position centrale verticale
ball_dx <= (lfsr[1]) ? 2 : -2; // Direction pseudo-aléatoire en X
ball_dy <= (lfsr[2]) ? 2 : -2; // Direction pseudo-aléatoire en Y
first_frame <= 0; // Désactive l'initialisation après le premier cycle
end else
begin
ball_x <= ball_x + ball_dx;
ball_y <= ball_y + ball_dy;
if (ball_x > SCREEN_WIDTH - BALL_SIZE) begin // rebond sur bord droit
ball_dx <= ball_dx * (-1);
ball_x <= SCREEN_WIDTH - BALL_SIZE;
end
else if (ball_x < 0) begin // rebond sur bord gauche
ball_dx <= ball_dx * (-1);
ball_x <= 0;
end
if (ball_y > SCREEN_HEIGHT - BALL_SIZE) begin // rebond sur bord bas
ball_dy <= ball_dy * (-1);
ball_y <= SCREEN_HEIGHT - BALL_SIZE;
end
else if (ball_y < 0) begin // rebond sur bord haut
ball_dy <= ball_dy * (-1);
ball_y <= 0;
end
// rebond sur raquette
for (int i = 0; i < 2; i++) begin
if (ball_y + BALL_SIZE >= paddle[i][Y] && ball_y <= paddle[i][Y] + PADDLE_HEIGHT &&
ball_x + BALL_SIZE >= paddle[i][X] && ball_x <= paddle[i][X] + PADDLE_WIDTH) begin
ball_dy <= ball_dy *(-1); // Inversion de la direction verticale
// Correction de la position pour éviter que la balle reste collée à la raquette
ball_y <= (ball_dy > 0) ? paddle[i][Y] - BALL_SIZE - 1 : paddle[i][Y] + PADDLE_HEIGHT + 1;
end
end
end
end
end
end
// ----- Gestion de l'affichage -----
logic [3:0] r, g, b;
always_comb begin
r = 4'h0; // hors zone d'affichage active de l'écran
g = 4'h0;
b = 4'h0;
if (inDisplayArea) begin
r = 4'h0; // Couleur de fond par défaut
g = 4'h0;
b = 4'h0;
if ((y < SCREEN_HEIGHT / 2 + 4 && y > SCREEN_HEIGHT /2 - 4) && (x % 4 == 0)) begin
r = 4'hf; // Ligne blanche du terrain
g = 4'hf;
b = 4'hf;
end else begin
// dessin de la balle
if (x > ball_x && x < ball_x + BALL_SIZE &&
y > ball_y && y < ball_y + BALL_SIZE) begin
r = 4'hF; // Couleur de la balle (jaune)
g = 4'hF;
b = 4'h0;
end else begin // dessin des raquettes
for (int i = 0; i < 2; i++) begin
if (x > paddle[i][X] && x < paddle[i][X] + PADDLE_WIDTH &&
y > paddle[i][Y] && y < paddle[i][Y] + PADDLE_HEIGHT) begin
r = 4'hF; // Couleur de la raquette
g = 4'h0;
b = 4'h0;
end
end
end
end
end
end
always_ff @(posedge clk25) begin
{vga_hsync, vga_vsync} <= {hsync, vsync};
{vga_r, vga_g, vga_b} <= {r, g, b};
end
endmodule
Dans ce module, les principales difficultés à prendre en compte sont le mouvement de la balle, avec ses rebonds sur le bord du terrain, et sur la raquette. La position initiale de la balle et sa direction sont pseudo-aléatoires au démarrage ou après appui sur le bouton Reset.
Le jeu est encore incomplet pour simplifier. Les rebonds sont à 45° et la vitesse de la balle est constante, mais rien n’empêche d’ajouter du piment au jeu en accélérant progressivement la balle ou en donnant des effets avec la raquette. Plus loin encore, on pourra compter les points, et donner un vainqueur au bout de 15 points, etc.
Pas facile d’une seule main…
Vous trouverez le projet réalisé avec Intel Quartus Prime sur mon dépôt : FPGA-YetAnotherPong.