logo

 

L'idée

Toujours en cours d'écriture...

Test imagePour vous décevoir des le début, je dois indiquer que le principe de la Super NeoGeo Pocket n'est pas d'améliorer la NeoGeo Pocket (que j'appellerai NGP/NGPC), mais de simplement pouvoir y jouer sur un écran de télévision, via une NeoGeo AES. Cette idée (assez débile, je l'admet), m'est venue par la constatation que plusieurs consoles bien connues avaient des accessoires permettant de jouer aux jeux portables de la même firme. Par exemple, il y avait une cartouche de Super NES appelée la Super Gameboy, qui permettait de jouer aux jeux noir et blanc sur sa télé. Pareil chez Sega avec le Power Base Converter de la Megadrive, qui permettait de faire tourner les jeux GameGear via un second adaptateur.

Qu'en est-il chez SNK ? Que dalle. Même pas l'ombre d'un prototype. Certainement du au fait que l'AES ne s'est pas si bien vendue et que les graphismes Gameboyens de la NGP allaient peut être entacher la réputation de bête de course de l'AES...

 

 

Le fonctionnement

Chez Sega, il n'y a pas eu grand chose a faire pour permettre aux jeux GameGear de tourner sur Megadrive, car ce ne sont qu'au final des jeux Master System avec du son en stéréo (et une définition des couleurs un peu plus précise, si mes souvenirs sont bons). C'est pas de l'émulation, le processeur vidéo de la Megadrive possédait un mode retrocompatible pouvait faire tourner nativement les jeux Master System/GameGear, et le Z80 normalement utilise pour le son était utilise comme processeur principal. Le Power Base Converter est complètement passif, aucun chip a l'intérieur, simplement une conversion du format des cartouches.

Chez Nintendo, les choses étaient bien plus complexes car la Super NES n'avait pas été prévue pour faire fonctionner des jeux NES, et de toutes façons, l'architecture de la Gameboy n'est pas du tout compatible avec celle de la NES.
L'émulation ? Avec un processeur si lent et avec un jeu d'instruction tout aussi incompatible (Z80 vs. 65C816), ce n'était tout simplement pas possible. En plus, il aurait fallu tester chaque jeu un par un pour faire une liste de compatibilité, qui n'aurait certainement pas atteint les 100%.

La solution a été pour eux d'intégrer le processeur d'une vraie Gameboy dans la cartouche Super Gameboy, et de simuler son environnement normal grâce a des E/S depuis la Super NES et un système a RAM pour récupérer l'image depuis les connections qui servaient a l'afficheur LCD. Dingue ? Pas tant que ca, c'est une solution qui permet d'assurer la plus grande compatibilité (le processeur est en tout points identique, a l'exception du bootstrap ROM), et qui ne devait pas coûter bien cher. Il leur a "seulement" fallu développer un circuit capable d'inscrire dans une RAM les données graphiques et de la copier par DMA chaque image que la Super NES réclame pour la passer a l'écran de télévision.
Les commandes sont relayées via des ports spéciaux depuis la console vers le CPU Gameboy.

Comme je me trouvais dans le même cas que Nintendo, j'en ai conclu que je devais moi aussi utiliser une vraie NGPC et créer un petit system de mémoire tampon et de formattage des données pour fournir une image a afficher a l'AES !

Élaboration

Pour afficher une image avec l'AES, on a deux choix correspondant a des ROMs et des bus distincts:

Pourquoi donc ne pas faire le rendu dans le fix et profiter d'un bus plus étroit ? Simplement parce que la ram dual port est très dure a trouver a des prix corrects, et que je n'ai pu mettre la main que sur un lot de 4ko 8bits (pour $8 pièce tout de même !).

La NeoGeo Pocket est faite pour faire le rendu de son image sur un écran de 160x152 pixels avec 12 bits de couleur par pixel.
La NeoGeo de salon peut dessiner des sprites que par blocs de 16x16 pixels avec 4 bits de couleur. J'aurai pu compliquer les choses et faire de la génération de palettes a la volée, mais comme il pouvait techniquement avoir plus de 16 couleurs a la fois par bloc de 16x16 sur la NeoGeo Pocket, j'ai laisse tomber l'idée de récupérer les couleurs.

Les blocs de 16x16 sont stockes en mémoire par lignes de 8 pixels. Dans une cartouche normale, deux ROMs 16 bits sont mis cote-a-cote pour former un bus 32 bits qui permet de fournir les 8 pixels * 4 bits de couleur pour une adresse de ligne donnée. Chaque ROM contient la moitie des bitplanes de chaque ligne (0,1 et 2,3). Les lignes sont stockées dans l'ordre suivant:

Test image

On constate que la colonne de droite est stockée avant la colonne de gauche, ce qui est un peu contre-intuitif, mais qui devait certainement simplifier les choses dans la console.

La NeoGeo Pocket donne les pixels a l'écran un par un, en partant du haut gauche, jusqu'au bas droit. Il y a pour cela 3 signaux intéressants et 3 groupes:

Quand l'écran est active, DCLK est une rafale de... rafales. La période est fixe: 324ns, soit 3.06MHz (fCPU/2).
Les premières rafales mesurent 52us (une ligne de 160 pixels) et sont espacées de 31.2us (hblank). Il y a 161 fronts, le premier ne comptant pas comme un pixel. Impulsion sur LP a chaque fin.
Les rafales de ces rafales mesurent 12.8ms (une image complète) et sont espacées de 3.8ms (vblank). Impulsion sur SPS a chaque fin.
Ceci donne une fréquence de rafraîchissement d'environ 1/(0.0128+0.0038) = 60Hz.

Le problème avec ces deux types de données, c'est non seulement qu'elles sont pas écrites et lues a la même vitesse, mais en plus, les tailles varient.
Comme il n'est pas possible simplement d'écrire dans les RAMs bit par bit au fur et a mesure qu'on récupère les pixels depuis la NGPC, il faut mettre en buffer chaque ligne de 8 pixels, c'est a dire 4 bitplanes soit 32 bits, puis les écrire les uns après les autres.

Il va donc y avoir un décalage temporel de 8 pixels entre ce que la NGPC donne, et ce qui est écrit dans les RAMs. Ce n'est pas un problème puisqu'on est pas a 2.6us près, et que ca bouclera sans problème entre les 8 derniers pixels (en bas a droite, je le rappelle), et les 8 premiers (en haut a gauche). Il risque simplement d'y avoir 8 pixels non-initialises durant la toute première image, glitch qui sera quasiment invisible.

En plus de devoir convertir les valeurs RGB en luminance, il faut traduire.

 

Organisation de l'écran virtuel du cote NG:

Test image

Pour simplifier les choses, il y a en interne 3 compteurs:

L'adresse en RAM peut ainsi être formée plus simplement. Si on se réfère a la façon dont sont stockées les lignes normalement en ROM, on voit que les 16 premières sont les unes a la suite des autres, on peut donc déjà utiliser Lineaddress comme début. Ensuite, on passe aux colonnes (2 par tiles), comme on met celle de droite avant celle de gauche mais qu'on les récupère depuis la NGPC dans l'ordre inverse, on va inverser le bit 0 de Coladdress, puis utiliser le reste des bits (1~4) normalement. Finalement, on mettra

000CCCCcLLLL
TTTTTTT000000

address[11:0] = {3'b000,coladdress[4:1],~coladdress[0],lineaddress[3:0]};
address[11:5] = address[11:5] + tileaddress[6:0];

 

Ordre d'écriture, commençant par le pixel 0:

Tile 0, colonne B, ligne 0.
...
Tile 0, colonne A, ligne 0.

Tile 1, colonne B, ligne 0.
...
Tile 1, colonne A, ligne 0.

Tile 2, colonne B, ligne 0.
......

Tile 9, colonne A, ligne 0.
Tile 0, colonne B, ligne 1.

 

1 tile = 2 colonnes = 32 lignes = x00000

Test image

On ne voit que 2 RAMs, mais il y en a bien 2 paires montees a cheval pour economiser les fils. La carte CPLD est en bleu, le reste est d'origine.

 

J'ai coupe la carte prog pour virer les roms V (audio), qui ne serviront plus, afin de laisser de la place pour la carte CPLD. Le ROM P1 (programme 68k) a ete dessoude et remplace par un support pour accueillir une EPROM. Pour recuperer les boutons appuyes sur le joypad de l'AES, le programme les lit via le BIOS comme la plupart des jeux, et transmet Haut, Bas, Gauche, Droite, A, B et C (pour le bouton Option) ainsi qu'une commande interne pour le bouton Power a l'adresse $200001. On peut donc recuperer cet octet dans la zone P2 grace au signal /PORTWEL sur un latch LS373 (LS car on aura qu'une ecriture par image, c'est a dire seulement a 60Hz). Pour adapter en tension, ce latch est alimente depuis le 3V de la NGPC et une diode est posee sur sa broche d'alimentation pour empecher le 5V de l'AES de remonter dans l'alim.
Un cote du LS373 est donc sur les LSB du bus de donnees 68k, et l'autre cote sur les contacts des boutons de la NGPC.

Test image

 

Comme avec les jeux noir et blanc sur Gameboy Color, et comme ca ne mangeait pas de pain, j'ai ajoute la possibilite de choisir a tout moment la palette de couleurs avec le bouton Select. La couleur 1 etant toujours la plus sombre, et la 15 le blanc. Je trouve la grise, verte et orange les meilleures, les autres sont fatigantes.

Test image

Code Verilog
module SuperNGPC
(
	input dotclk,			// DCLK
	input vsync,
	input hsync,
	input [3:0] R,
	input [3:0] G,
	input [3:0] B,
	output reg [3:0] weram,	// Signal /WE commun aux RAMs
	output reg ceram,		// Signal /CE commun aux RAMs
	output reg [7:0] ramdata,
	output reg [11:0] address,
	output reg led			// LED de debug
);

reg [5:0] pixel;	// Luminance calculee du pixel
reg [7:0] bpa;		// Registre a decalage live bitplane A
reg [7:0] bpb;		// ...B
reg [7:0] bpc;		// ...C
reg [7:0] bpd;		// et D
reg [7:0] bpaw;	// Buffer d'ecriture bitplane A
reg [7:0] bpbw;	// ...B
reg [7:0] bpcw;	// ...C
reg [7:0] bpdw;	// et D
reg [2:0] clkc;	// Compteur de cycles de DCLK
reg [4:0] coladdress;
reg [3:0] lineaddress;
reg [6:0] tileaddress;
reg gothsync;		// Flag indiquant si on vient d'avoir un hsync
La liste de sensibilite est un peu horrible, mais j'ai pas trouve de methode plus simple... On doit reagir sur le front montant de DCLK, sur celui de Hsync et sur le descendant de Vsync:
always @(posedge dotclk or posedge hsync or negedge vsync)
Le code suivant permet -seulement- de generer les motifs de test illustes ensuite, pas d'adapter l'image de la NGPC.
Il faut d'abord differencier les fronts. J'ai commence par Vsync, qui indique a tous les pointeurs de retourner a zero, et de desactiver la RAM tant qu'on a pas recu minimum 8 pixels.
	if (vsync == 0)
	begin
		coladdress = 0;
		lineaddress = 0;
		tileaddress = 0;
		clkc = 0;
		ceram = 1;
	end
Ensuite pour le Hsync, on doit pas faire grand chose a part mettre un flag pour ignorer le front suivant sur DCLK.
	else if (hsync == 1)	// Posedge on hsync, set flag to ignore next clock cycle
	begin
		gothsync = 1;
	end
Et finalement, la generation des lignes de 8 pixels se fait sur le front montant de DCLK:
	else if (dotclk == 1)	// Front montant sur DCLK, generer un pixel
	begin

		ceram = 0;			// Activer les RAMs
		
		if (clkc[2:0] == 0)	// Phase 0, descendre /WE0 et mettre les donnees sur le bus
		begin
			weram = {4'b1110};
			if (address[5] == 0) ramdata = 8'b00000000;
			if (address[5] == 1) ramdata = 8'b11111111;
		end
		if (clkc[2:0] == 1)	// Phase 1, remonter /WE0 (ecrire)
			weram = {4'b1111};
		if (clkc[2:0] == 2)	// Phase 2, descendre /WE1 et mettre les donnees sur le bus
		begin
			weram = {4'b1101};
			if (address[6] == 0) ramdata = 8'b00000000;
			if (address[6] == 1) ramdata = 8'b11111111;
		end
		if (clkc[2:0] == 3)	// Phase 3, remonter /WE1 (ecrire)
			weram = {4'b1111};
		
		if (clkc[2:0] == 4)	// Phase 4, descendre /WE2 et mettre les donnees sur le bus
		begin
			weram = {4'b1011};
			if (address[3] == 0) ramdata = 8'b00000000;
			if (address[3] == 1) ramdata = 8'b11111111;
		end
		if (clkc[2:0] == 5)	// Phase 5, remonter /WE2 (ecrire)
			weram = {4'b1111};
		if (clkc[2:0] == 6)	// Phase 6, descendre /WE3 et mettre les donnees sur le bus
		begin
			weram = {4'b0111};
			if (address[4] == 0) ramdata = 8'b00000000;
			if (address[4] == 1) ramdata = 8'b11111111;
		end
		if (clkc[2:0] == 7)	// Phase 7, remonter /WE3 (ecrire) et incrementer l'adresse
		begin
			weram = {4'b1111};
			address = address + 1;
		end

		clkc = clkc + 1;	// Incrementer clkc (phase) a chaque front montant de DCLK
	end
end

On constate qu'avec ce code, il n'y a pas de gestion des registres a decalage pour ecrire chaque pixel independement. Pour chaque adresse, on a 8 pixels ecrits d'un seul coup, ca suffit pour tester les RAMs.

Voyons maintenant le code pour le front de DCLK pour effectivement adapter les pixels recus depuis la NGPC pour les traduire dans le format sprites de l'AES:

		if (gothsync == 1)	// Ignorer le front si on vient juste d'avoir un hsync
		begin
			gothsync = 0;
		end
		else
		begin
			
			bpa[6:0] = bpa[7:1];	// Decaler tous les registres bitplanes a droite
			bpb[6:0] = bpb[7:1];
			bpc[6:0] = bpc[7:1];
			bpd[6:0] = bpd[7:1];
			
			// Calculer la luminance du pixel depuis les valeurs RGB
			pixel[5:0] = R[3:0] + G[3:0] + B[3:0];		// Somme RGB, plage: 000000~101101
			pixel[5:0] = pixel[5:0] + pixel[5:2] + 4;	// Mul 1.25, plage: 000100~111100
			 											// >>2, plage: 0001~1111, parfait !

			bpa[7] = pixel[5];			// Remplissage des MSB des registres bitplanes
			bpb[7] = pixel[4];
			bpc[7] = pixel[3];
			bpd[7] = pixel[2];
			
			if (clkc == 7)
			begin
				ceram = 0;		// Activer la RAM des qu'on a recu 8 pixels
				bpaw = bpa;		// Copier les registres a decalage vers les buffers d'ecriture
				bpbw = bpb;		// Pour ne pas qu'ils soient modifies pendant les prochaines pixels
				bpcw = bpc;
				bpdw = bpd;
			end
			
			// Prochaine colonne apres qu'on ait ecrit les 4 bitplanes en RAM (phase = 4)
			if ((clkc == 4) && (ceram == 0)) coladdress = coladdress + 1;
			
			if (coladdress == 20)
			begin
				coladdress = 0;			// Retour a la colonne 0 si on arrive a la 20eme
				if (lineaddress == 15)
				begin
					tileaddress = tileaddress + 10;	// Passage a la ligne de tiles suivante
					lineaddress = 0;	// Retour a la ligne 0 si on arrive a la 16eme
				end
				else
				begin
					lineaddress = lineaddress + 1;	// Sinon, passage a la ligne de pixels suivante
				end
			end
			
			clkc = clkc + 1;	// Incrementer la phase
			
			if (clkc == 0) ramdata[7:0] = bpaw[7:0];	// Choisir le bitplane a mettre sur le bus de donnees
			if (clkc == 1) ramdata[7:0] = bpbw[7:0];
			if (clkc == 2) ramdata[7:0] = bpcw[7:0];
			if (clkc == 3) ramdata[7:0] = bpdw[7:0];
			
			// Generation de l'adresse d'apres les 3 compteurs
			address[11:0] = {3'b000,coladdress[4:1],~coladdress[0],lineaddress[3:0]};
			address[11:5] = address[11:5] + tileaddress[6:0];
		end

Essai des 2 premiers bitplanes

Test des 2 bitplanes de poids fort, images simulées comme si les 2 bitplanes de poids faible étaient coinces a "1".
La valeur inscrite provient des 2 bits d'adresse indiques.

Test image
1 et 0 (toutes les lignes de pixels)
Test image
2 et 1 (toutes les 2 lignes de pixels)
Test image
3 et 2 (toutes les 4 lignes de pixels)
Test image
4 et 3 (toutes les 8 lignes de pixels). On commence a voir l'organisation droite/gauche des sous-tiles.

Test image
5 et 4 (toutes les 16 lignes de pixels)
Test image
6 et 5 (tous les tiles)
Test image
7 et 6 (tous les 2 tiles)
Test image
8 et 7 (tous les 4 tiles)


Test image
9 et 8 (tout les 8 tiles)
Test image
10 et 9 (tout les 16 tiles)
Test image
11 et 10 (tout les 32 tiles)




Essai de tous les bitplanes

Le magenta symbolise la couleur 0 (transparence).
La valeur inscrite provient des4 bits d'adresse indiques.

Test image
3,2,1, et 0 (toutes les 16 lignes)
Test image
4,3,2, et 1 (tout les tiles). On commence a voir l'organisation droite/gauche des sous-tiles.
Test image
5,4,3, et 2 (tout les 2 tiles)
Test image
6,5,4, et 3 (tout les 4 tiles)
Test image
7,6,5, et 4 (tout les 8 tiles)
Test image
8,7,6, et 5 (tout les 16 tiles)
Test image
9,8,7, et 6 (tout les 32 tiles)
Test image
10,9,8, et 7 (tout les 64 tiles)
Test image
11,10,9, et 8 (tout les 128 tiles)

 

 

Réalisation

A venir...

ROM

 

footer
symbol symbol symbol symbol symbol