This page is at least 13 years old !
Pour vous décevoir des le début, 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.
Ce nom et cette idée (assez débile, je l'admet), m'est venue par la constatation que deux consoles bien connues ont été dotées d'accessoires permettant de jouer aux jeux d'une autre de la même firme.
Par exemple, il y avait une cartouche pour Super NES appelée la Super Gameboy, qui permettait de jouer aux jeux Gameboy sur sa télé.
Chez Sega il était possible de faire l'inverse en utilisant un adaptateur pour jouer aux jeux Master System sur Game Gear, et 30 ans plus tard, l'inverse grâce au Gear Master d'Apocalypse.
Qu'en est-il chez SNK ? Que dalle. Même pas l'ombre d'un prototype. Avec la réputation de la NeoGeo AES, le rendu primitif des jeux NGPC, et le marché très limité, l'idée n'a même pas du leur traverser l'esprit.
Le fonctionnement
Chez Sega, il n'y a pas eu grand chose a faire pour permettre aux jeux Master System de tourner sur Game Gear, car celle-ci n'est qu'une Master System modifiée. Plus tard, pour faire fonctionner les jeux Master System sur Megadrive grâce a un adaptateur complètement passif, le processeur vidéo de la Megadrive avait été conçu avec un mode rétrocompatible pour cette raison, et le Z80 normalement utilise pour le son était utilise comme processeur principal.
Chez Nintendo, les choses étaient bien plus complexes car la Super NES n'avait pas été prévue pour faire fonctionner des jeux Gameboy, a cause de l'architecture complètement différente.
L'émulation ? Avec un processeur si lent que celui de la SNES et avec un jeu d'instruction tout aussi incompatible (Z80 vs. 65C816), c'était tout simplement pas possible. En plus, il aurait fallu tester chaque jeu pour vérifier sa compatibilité, avec tous les corner cases et la gestion des bugs hardware, ce qui n'aurait eu aucune chance de mener a un produit abordable et fiable.
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 un chip additionnel permettant de récupérer l'image produite depuis les connections qui servaient a l'afficheur LCD, et de communiquer avec la SNES. 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 de la ROM de démarrage), 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 images, et de les faire copier par DMA par la SNES pour les passer a l'écran, comme si elles provenaient d'une cartouche ROM classique.
Les contrôles du joueur sont relayés via des ports spéciaux depuis la console vers le CPU Gameboy, en plus d'autres fonctionnalités étendues que certains jeux ont utilisé.
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 système de mémoire tampon et de formatage des données pour fournir une image dynamique a la console comme si elle venait d'une ROM.
Élaboration
Pour afficher une image avec la NeoGeo AES, on a deux choix correspondant a des ROMs et des bus distincts:- Soit on passe par le fix, un plan fixe avec des tiles de 8x8 pixels en 16 couleurs, dont les données viennent d'une ROM 8 bits.
- Soit on passe par les sprites, tiles de 16x16 pixels également en 16 couleurs, qui viennent de 2 ROMs 16 bits en parallèle.
La NeoGeo Pocket fait le rendu de son image sur un écran de 160x152 pixels avec 12 bits par pixel (4096 couleurs). Il y a une contrainte de couleurs par tile, mais ça n'aide pas ici car les tiles peuvent défiler, et des sprites peuvent se trouver par dessus, donc il faut assumer que n'importe quel pixel de l'écran peut avoir n'importe laquelle de ces 4096 couleurs.
Il faut donc un minimum de 160*152*12 = 291840 bits pour stocker une image de la NGPC. Si on décide de faire que du noir et blanc sur 16 niveaux, on arrive a 160*152*4 = 97280 bits, ce qui tiendrait dans 16ko.
Idéalement j'aurais voulu utiliser le fix pour faire le rendu sur console afin de profiter du bus plus étroit et réduire le travail de modification. Cependant la RAM dual port (RAM permettait deux accès aléatoires simultanés sur les mêmes données) dont j'allais avoir besoin est très dure a trouver a des prix corrects, et je n'ai pu mettre la main que sur un lot de quatre 4ko 8bits qui, avec le petit CPLD que j'allais utiliser ensuite, a contraint mon choix d'utiliser des sprites.
Les tiles de 16x16 sont stockes en mémoire par rangées 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 d'une lecture les 8 pixels * 4 bits de couleur pour une adresse donnée. Chaque ROM contient la moitie des bitplanes de chaque ligne (0, 1 et 2, 3). Les rangées sont stockées dans l'ordre suivant:

On constate que la colonne de droite est stockée avant la colonne de gauche, ce qui est un peu contre-intuitif, mais peu importe.
La NeoGeo Pocket fournit des pixels a son écran un par un, en partant du haut gauche, jusqu'au bas droit, pas de surprise. Il y a pour cela 3 signaux intéressants et 3 groupes:
- DCLK (Dot Clock), l'horloge pour indiquer sur un front montant que la couleur du pixel est valide.
- LP (Line Pulse ?), une impulsion positive indique le passage a la ligne suivante, la synchro horizontale.
- SPS (?), une impulsion négative indique le retour au début de l'écran, la synchro verticale.
- R0~3, la composante rouge du pixel.
- G0~3, la composante verte du pixel.
- B0~3, la composante bleue du pixel.
Quand l'écran est actif, 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 (blanking horizontal). Il y a 161 fronts, le premier ne comptant pas comme un pixel. Il y a une impulsion sur LP a la fin.
Les rafales de ces rafales mesurent 12.8ms (une image complète de 152 lignes) et sont espacées de 3.8ms (blanking vertical). Il y a une impulsion sur SPS a la fin.
Ceci donne une fréquence de rafraîchissement d'environ 1 / (12.8ms + 3.8ms) = 60Hz.
Le problème avec ces deux types de données sur chaque console, c'est non seulement qu'elles ne sont pas écrites et lues a la même vitesse, mais en plus, les tailles varient.
Il n'est pas possible simplement d'écrire dans les RAMs pixel par pixel depuis la NGPC car ca impliquerait de faire des relectures pour convertir ces données "blocky" en "planaire". Le plus simple pour faire la conversion a la volée rapidement est de mettre en buffer chaque ligne de 8 pixels 4bpp provenant de la NGPC, pour correspondre aux 32 bits de ce que l'AES veut lire, et écrire ce buffer séquentiellement en RAM.
Il va donc y avoir un décalage temporel de 8 pixels entre ce que la NGPC fournit, 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), et les 8 premiers (en haut a gauche) car tout reste un multiple de 8.
L'animation montre a gauche l'image rendue par la NGPC pixel par pixel, et a droite les tiles AES 16x16 avec en rouge celui dans lequel la rangée de 8 pixels est inscrite.
Le CPLD s'occupe de convertir les 12bpp en une valeur approximative de luminance 4bpp, et de générer l'adresse et les signaux d'écriture pour la RAM. Le port en lecture de la RAM est lui complètement géré par l'AES comme si c'etait une ROM.
Pour couvrir les 160x152 pixels, il faut 10 * 9.5, arrondi a 10 = 100 tiles. Ca aurait ete plus simple d'avoir un multiple de 8 tiles horizontalement (16 au lieu de 10), mais la taille de RAM ne permet pas un tel gaspillage.
Le programme cote AES disposera les sprites comme suit pour former un écran virtuel:
![]()
Pour simplifier les choses au maximum dans le CPLD et tout faire rentrer dans ses 128 blocs logiques, il y a en interne 3 compteurs:
- Un compteur de rangée dans un tile, sur 4 bits (0 a 15), lineaddress.
- Un compteur de colonne de 8 pixels sur une rangée d'écran, sur 5 bits (0 a 160/8-1=19), coladdress.
- Un compteur de tiles sur 7 bits (0 a 99), tileaddress.
Les 4 chips de RAM sont arranges pour former 4ko * 32 bits, afin que la console puisse les lire comme des ROMs.
Si on se réfère a la façon dont sont stockées les données graphiques pour les sprites, on voit que des blocs de 16 rangées verticales sont les unes a la suite des autres, on peut donc déjà utiliser lineaddress directement pour les 4 bits de poids faible.
Ensuite, on passe en horizontal avec les colonnes de 8 pixels (2 par tile), comme on met celle de droite avant celle de gauche mais qu'on les récupère depuis la NGPC dans l'ordre normal, il faut inverse le bit 0 de coladdress, puis utiliser le reste des bits normalement.
address = 000CCCCcLLLL
Comme le nombre de tiles horizontalement n'est pas un multiple de 8, il faut passer par une addition pour finir de former l'adresse avec tileaddress, qui sera incrémenté par pas de 10 a chaque rangée de tiles terminée.
address = 000CCCCcLLLL + TTTTTTT00000
address[11:0] = {3'b000,coladdress[4:1],~coladdress[0], lineaddress[3:0]};
address[11:5] = address[11:5] + tileaddress[6:0];
Si on suit chaque groupe de 8 pixels fourni par la NGPC, on a un ordre d'écriture commençant par le pixel 0:
Tile 0, colonne gauche, ligne 0
Tile 0, colonne droite, ligne 0
Tile 1, colonne gauche, ligne 0
...
Tile 9, colonne droite, ligne 0
Tile 10, colonne gauche, ligne 1
...
Tile 99, colonne droite, ligne 7
Réalisation
Vomissez pas trop fort svp.

J'ai sacrifie une cartouche AES et une NGPC. La carte PROG de la cartouche a ete découpée pour virer les ROMs V (audio), qui ne serviront plus, afin de laisser de la place pour la carte CPLD.
La pile pour l'horloge de la NGPC a ete remplacée par un accu NiCd et de quoi le charger via le 5V de la console. Le bouton power a été câblé via un transistor pour simuler un appui, car le circuit d'alim est assez complique (auto shutdown / veille pilotable par logiciel).

La ROM P (programme 68k) a ete dessoudee et remplacee par un support pour accueillir une EPROM qui contiendra le programme charge de l'initialisation et le relai des commandes joueur.
Pour recuperer ces commandes, le programme lits le joystick 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 generee au demarrage pour le bouton Power a l'adresse $200001. On peut recuperer cet octet facilement dans la cartouche grace au signal /PORTWEL sur un latch LS373. Pour adapter en tension entre les 5V console et les 3.3V NGPC, ce latch est alimente depuis le 3V de la NGPC a travers une diode pour empecher le 5V de l'AES de remonter par les diodes de clamp. C'est parfaitement degueulasse.
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.
Les RAMs sont montées par paires les unes sur les autres, avec des nappes IDE découpées en guise de bus.

Comme avec la Super Gameboy, et comme ca ne mangeait pas de pain vu l'espace disponible dans la ROM P, j'ai ajoute la possibilité de choisir a tout moment la palette de couleurs avec le bouton Select.
La couleur 1 étant toujours la plus sombre, et la 15 la plus claire. Je trouve la grise, la verte et l'orange les meilleures, les autres sont fatigantes.

Logique
J'étais en train d'apprendre le verilog, mes excuses. Je n'avais aussi aucune idée de ce qu'était Git.
Comme il n'y avait pas assez d'IO sur le CPLD, il a fallu écrire dans les RAMs séquentiellement avec un bus 8 bits et 4 signaux d'activation. Heureusement cette complexité additionnelle n'a pas consommé trop de blocs logiques.
module SuperNGPC ( input dotclk, // DCLK input vsync, // SPS input hsync, // LP 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 hsyncLa liste de sensibilité est horrible, mais j'ai pas trouve de méthode plus simple... On doit réagir 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 générer les motifs de test de RAM illustrés ensuite, pas encore d'adapter l'image de la NGPC.
Il faut d'abord différencier les fronts. J'ai commence par Vsync, qui est pratiquement un reset, met tous les compteurs a zero, et désactive la RAM en écriture tant qu'on a pas reçu au moins 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 génération 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
Il n'y a pas de gestion des registres a décalage nécessaires pour écrire chaque pixel indépendamment. Pour chaque adresse, il y a des valeurs de pixels prédefinies, ca suffit pour tester les RAMs.
Essai des 2 premiers bitplanes
Test des 2 bitplanes de poids fort, images simulées comme si les 2 bitplanes de poids faible étaient bloques a "1", donc 4 niveaux de gris.
La valeur inscrite provient des 2 bits d'adresse indiques.
![]() address[1:0] (toutes les lignes de pixels) |
![]() address[2:1] (toutes les 2 lignes de pixels) |
![]() address[3:2] (toutes les 4 lignes de pixels) |
![]() address[4:3] (toutes les 8 lignes de pixels, on commence a voir l'organisation droite/gauche des colonnes. |
![]() address[5:4] (toutes les 16 lignes de pixels) |
![]() address[6:5] (tous les tiles) |
![]() address[7:6] (tous les 2 tiles) |
![]() address[8:7] (tous les 4 tiles) |
![]() address[9:8] (tout les 8 tiles) |
![]() address[10:9] (tout les 16 tiles) |
![]() address[11:10] (tout les 32 tiles) |
Essai de chaque bitplane
Maintenant avec 16 niveaux de gris. Le magenta symbolise la couleur 0 (transparence).
La valeur inscrite provient des 4 bits d'adresse indiques.
![]() address[3:0] (toutes les 16 lignes) |
![]() address[4:1] (tous les tiles, on voit l'organisation droite/gauche des colonnes) |
![]() address[5:2] (tout les 2 tiles) |
![]() address[6:3] (tout les 4 tiles) |
![]() address[7:4] (tout les 8 tiles) |
![]() address[8:5] (tout les 16 tiles) |
![]() address[9:6] (tout les 32 tiles) |
![]() address[10:7] (tout les 64 tiles) |
![]() address[11:8] (tout les 128 tiles) |
Le code final pour le front de DCLK pour effectivement adapter les pixels reçus depuis la NGPC et les traduire dans le format sprites de l'AES:
if (gothsync == 1) // Ignorer ce 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 approx du pixel depuis les valeurs RGB
pixel[5:0] = R[3:0] + G[3:0] + B[3:0]; // Somme RGB, plage: 15+15+15 = 45 = 000000~101101
pixel[5:0] = pixel[5:0] + pixel[5:2] + 4; // *1.25+4, 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 (double buffer)
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
La gestion des weram est plus rapide qu'avec le code de test, chaque RAM est écrite pour chaque clkc, complétant l'écriture en 4 cycles au lieu de 8. Je sais plus pourquoi c'etait nécessaire, mais c'est de toutes façons plus optimisé.



















