Login
Utilisateur :
Mot de passe :
Nouveau ?
Accueil
 
Sasfépu
sasfepu
Matériels
Logiciels
Littérature
Articles de presse
Publicités
Téléchargements
 
Interviews
Pierre Lafitte
Jean Fontayne
 
Emulateurs
MESS
dchector
VB Hector
 
Projets
wav2hec
hec2wav
hecdump
émulateur
cable péritel
Hectorduino
Hectorduino & vidéos
 
Webmestre
 Laisser une Bafouille

Liens
 


Creation d'un emulateur

Un émulateur

0. Introduction

Ce document n'a pas de grandes prétentions, il existe uniquement parce que n'ayant jamais développé d'émulateur auparavant, j'aurais bien aimé en trouver un comme celui-ci sur la toile.

Il ne détient pas l'unique vérité et contient uniquement ma démarche de débutant dans le domaine pour construire mon premier émulateur.

1. Qu'est ce qu'un émulateur ?

Un programme qui reproduit le comportement d'une machine et de ses périphériques sur une autre dans ses moindres détails, ceci incluant les éventuels bogues.

Très important aussi, une émulation réussie simule le fonctionnement de la machine à sa vitesse d'origine exacte. Un émulateur ZX Spectrum qui tourne à la vitesse d'une GameCube ne sera pas parfait !

 

2. La machine

Il faut choisir une machine à émuler et uniquement une. Choisir une gamme (par exemple : tous les hector) complique beaucoup trop la tâche ! Toutes similaires qu'elles soient en apparence, elles ont des différences qu'il est difficile d'intégrer au premier jet.

2.1 Le processeur

Coeur de l'ordinateur, c'est lui qui va animer les programmes inséré en mémoire par l'utilisateur.

Un processeur est toujours constitué de registres qui permettent de manipuler des données et des registres donnant l'état logique du processeur comme l'instruction en cours d'exécution ou le résultat de l'opération précédente (retenue, résultat nul, etc...). Chaque registre a une taille donnée qui l'autorise à coder un nombre maximal. Par exemple: Le z80 a des registres pouvant contenir chacun la valeur d'un octet assemblables deux à deux pour obtenir si on le désire des valeurs codées sur deux octets (16 bits).

2.2 La mémoire

Dans tout ordinateur, on trouve de la mémoire RAM (vive et volatile), et ROM (morte uniquement lisible et persistante). Cette mémoire est accédée par le processeur qui exécute les instructions qu'on lui soumet.

2.3 Le son

Ou plutôt l'équipement qui va permettre à l'ordinateur de faire du bruit, de la musique pour les plus doués.

Il se caractérise sur HECTOR par une puce reliée et controlée par le processeur.

 

2.4 Les périphériques

Tout ce qui est connecté à l'ordinateur est un périphérique !

2.4.1 Le Clavier

Il permet à l'utilisateur de transmettre des données à l'ordinateur pour qu'il les traite. Le fait d'appuyer sur une ou plusieurs touches "envoie" des informations à la machine qui va réagir en fonction de ce que l'électronicien qui a construit le circuit afait ert de ce qui a été programmé dans la ROM par le constructeur.

Le clavier a l'air banal mais représente bien un module à part, bien souvent, il ne se contente pas uniquement de transmettre le code de la touche pressée.

2.4.2 L'écran

Ici nous parlons plutôt tu tube cathodique et des circuits reliés au processeur pour le gérer.

En effet, pour, par exemple, afficher un point à l'écran plusieurs circuits interviennent. Le canon à électrons qui projette son faisceau sur la paroi phosporée du tube est controlé par les signaux envoyés par l'ordinateur qui a du exécuter plusieurs instructions avant d'avoir pu obtenir un résultat visible.

2.4.3 Les joysticks

Les joysticks sont assez particuliers sur hector, ils disposent de potentiomètres, en plus des traditionnels boutons "feu"

2.4.4 Le lecteur de cassettes

Ce périphérique-ci fait l'interface entre l'ordinateur et la cassette, media de stockage de masse, persistant. On stocke des données sous forme analogique (son) que l'on relit et convertit au format numérique compréhensible par la machine.

2.4.5 Le lecteur de disquettes

Le lecteur de disquettes est presque comme le lecteru de cassettes à la différence de son organisation et qu'il dispose souvent d'un processeur et mémoire propre (Ceci est vrai pour les lecteurs commodore et hector)

2.4.6 Les cartouches

Les cartouches comportent une mémoire (ROM) dans laquelle est enregistré un programme. Pour le hector la seule cartouche référencé jusqu'à présent est celle qui contient le basic. Sur hector, on la branche sur le port paralèlle.

2.5 Les prises (RS232, etc...)

Les différents ports qui équipe la machine servent à brancher des périphériques pour communiquer avec l'ordinateur. Sur Hector, on a, pour les derniers modèles, l'interface parallèle et les prises joystick.

3.0 La légalité

L'émulateur, le programme en lui même, ne fait que reproduire le comportement d'une machine, il ne possède pas de code protégé commercialement. Par contre, les roms et logiciels sont propriétés de la société qui détient les droits. Vous devez posséder l'original pour pouvoir utiliser la rom ou le jeu et vous n'avez pas le droit de désassembler la ROM ou les logiciels, mais juste d'utiliser.

 

4. Choix techniques

4.1 Le système d'exploitation

Pourquoi pas Windows ???? Deux réponses possibles au choix:

1. J'aime pas Windows :) et il y a déjà beaucoup de choses fonctionnant dessus.

2. Le développer sur U*ix ne permet pas forcément de s'adresser à une énorme population mais cette plateforme à l'avantage d'accroitre la portabilité. En effet, développer pour U*ix permet de voir fonctionner son émulateur sur tous les linux, AIX, Solaris, HP-UX, Mac OS-X, etc... sans adaptations majeures.

4.2 Le langage de programmation

Le langage utilisé ici est le C. Pourquoi ? Parce qu'un langage comme l'assembleur est rapide mais pas du tout portable et difficilement maintenable, le C++ trop lent, Java encore plus lent. Le C par contre offre de bonnes possibilités de portage et d'évolution du code sans être trop lent à exécuter.

4.3 L'interface graphique

Unix et C, il faut aussi une interface graphique commune ou disponible gratuitement sur toutes les variantes de ce système d'exploitation. Gtk2 ou Kde auraient été possibles, mais compiler KDE ou Gtk2 sur AIX 4.3.3, "c'est l'aventure" ! Motif v2 a l'avantage d'être livré avec les Unix commerciaux et d'être disponible gratuitement sur les autres plateformes.

5.0 Principes de l'émulateur (comment faire ?)

5.1 Par quoi commencer ?

La machine commence à fonctionner dès qu'on la branche sur le secteur et qu'on appuie sur le bouton "Marche". Pour un émulateur c'est pareil, on lance l'émulateur et il commence à exécuter des instructions jusqu'à ce qu'on en sorte.

En gros cela revient à avoir une boucle principale ou l'on effectue l'émulation de la machine.

int main(int argc,char **argv)
{
  BOOL fin=FALSE;

  Fonctions_d_initialisation();

  while(!fin)
  {
    Traitement_d_une instruction();
    Réactualisation_de_l_ecran_si_besoin();
    Temporisation();
    }

  Fonctions_de_nettoyage();
}

Maintenant la grande question est: "Par quel bout commencer la réalisation de l'émulateur ?"

Sans conteste, la première chose à faire est lire ! Effectivement, comment émuler une machine dont on ne connait rien ? Il faut se documenter sur les internes de la machine, le type de microprocesseur, la taille mémoire, son fonctionenement interne. Personnellement, je pense qu'il ne sert à rien de démarrer sans avoir au minimum une connaissance au moins basique de l'assembleur de la machine que l'on veut émuler et du type de processeur de l'ordinateur.

Une fois que l'on dispose de ces informations, il faut savoir programmer dans un langage adapté à ce que l'on veut faire, connaitre le logo ou comment coder une macro excel ne vous permettra pas de réaliser un émulateur (ou alors vous êtes très très fort et je désire faire votre connaissance )

Une derniere chose à savoir est comment programmer la partie graphique de l'émulateur. Si vous aller développer sur Windows, il vous faudra surement maîtriser la programmation de DirectX, sous Unix celle de X11 (ou tout autre API graphique).

Ah oui, j'oubliais: Vouloir commencer à programmer un émulateur de Console dernier cri n'est pas une bonne idée .

5.1 Comment emuler le processeur ?

L'émulation du processeur est en théorie simple, c'est la partie qui prend le plus de temps si vous décidez de tout coder depuis le début car il faut vous procurer une documentation qui décrit l'ensemble des instructions comprises par le processeur et les effets qu'elles ont sur lui.

Pour le Hector, le processeur est un Z80 de la société Zilog il comprend les registres A,F,B,C,D,E,H,L et leurs images ,IY,IX,R, un pointeur de pile (SP ou Stack Pointer) et un compteur de programme (PC ou Program Counter). Le processeur exécute l'instruction pointée par le registre PC (16 bits).

Exemple:

Valeur de PC
(Addresse en mémoire)
Instruction
0x5FE2 LD A,0
0x5FE3 OR A

Le processeur va d'abord traiter l'instruction stockée en mémoire à l'adresse 0x5FE2 (nombre en base 16) qui es LD A,0 qui lui indique qu'il faut mettre la valeur 0 dans le registre A. Le PC passe alors à l'instruction suivante et exécute un XOR A, un OU logique. Cette opération est l'équivalente à A = A | A soit un résultat de 0. A ce moment là, le Z80 va aussi mettre à jour le registre F qui est un registre spécial qui contient plusieurs indicateurs (flags), dans le cas d'une opération retournant un résultat nul, le "flag" (en fait un bit) correspondant est positionné à 1.

Il faut savoir que chaque instruction met du temps à être exécutée par le microprocesseur, celui-ci varie en fonction de la complexité de l'instruction.

Donc on se retrouve a créer une structure qui regroupe plusieurs choses: Les différents registres et d'autres informations de contrôle.

 

typedef union
{
 u16 r16;
 struct {
      u8 lsb;
      u8 msb;
       } r8;
} paire;


typedef struct
{
 paire _af1, _af2;
 paire _bc1, _bc2;
 paire _de1, _de2;
 paire _hl1, _hl2;
 paire _ix , _iy;
 paire _sp;     /* SP = Stack Pointer */
 paire _pc;     /* PC = Program Counter */
 paire _ir;
 u16 IFF1, IFF2;

 char *cpu_name;       // Nom du cpu
 int state;            // Etat du CPU
 u32 frequence;        // MHz
 u16 interrupt;        // Frequence 50Hz
 u16 IPeriod;          // Periode (nombre de cycles) apres la quelle une interruption est generee
 u16 ICount;           // Nombre de cycles avant la generation d'interruption
 u16 IRequest;         // Type d'intérruption, si interruption il y a
  } Z80;

 

On voit bien que chaque registre de 16 bits est composé d'une "paire" d'octet, structure définie avant, ceci uniquement pour des facilités d'accès pour le programmeur. On aurait pu écrire

unsigned short _hl1;

ou

unsigned char _hl1[2];

mais à l'usage, c'est beaucoup moins pratique.

Après on peut aussi définir quelques macros pour pouvoir accéder aux registres facilement sans taper des kilomètres de code:

#define PC _pc.r16
#define A  _af1.r8.msb
#define F  _af1.r8.lsb
#define AF _af1.r16
...

Ceci fait, on regarde combien d'instructions comprend le microprocesseur. Chaque instruction est représenté par une valeur ou plus, ci-dessous un bout de code assembleur Z80. A droite les instructions "décodées", lisibles par le commun des mortels, à gauche se trouvent les valeurs qui représentent l'instruction :

B7   OR A
C8   RET Z
07   RLCA
...

On peut donc les classer par leur valeur respective. On fait un structure "enum" qui va permettre de les référencer facilement:

enum OpCodes
{
NOP      , LD_BC_WORD , LD_xBC_A    , INC_BC , INC_B , DEC_B , LD_B_BYTE , RLCA,
EX_AF_AF , ADD_HL_BC  , LD_A_xBC    , DEC_BC , INC_C , DEC_C , LD_C_BYTE , RRCA,
DJNZ     , LD_DE_WORD , LD_xDE_A    , INC_DE , INC_D , DEC_D , LD_D_BYTE , RLA,
JR       , ADD_HL_DE  , LD_A_xDE    , DEC_DE , INC_E , DEC_E , LD_E_BYTE , RRA,
JR_NZ    , LD_HL_WORD , LD_xWORD_HL , INC_HL , INC_H , DEC_H , LD_H_BYTE , DAA,
....
}

Certaines de ces instructions positionnent des flags dans le registre F lorsque elles sont exécutées par exemple le flag de signe ou le flag Zéro lorsque le résultat de l'opération est 0, on pourrait créer une routine qui en fonction du résultat stocké dans A, retourne ou positionne les bon flags, mais ce ne serait pas très optimisé ! Le plus simple est de créer un tableau de la taille du nombre de valeurs que peut prendre le registre A et d'y mettre les flags Zero et Signe correspondants à chaque valeur:

#define Z_FLAG 0x40
#define S_FLAG 0x80

unsigned char ZSTable[256] =
{
Z_FLAG,0     ,0     ,0     ,0     ,0     ,0     ,0     ,
0     ,0     ,0     ,0     ,0     ,0     ,0     ,0     ,
0     ,0     ,0     ,0     ,0     ,0     ,0     ,0     ,
0     ,0     ,0     ,0     ,0     ,0     ,0     ,0     ,
0     ,0     ,0     ,0     ,0     ,0     ,0     ,0     ,
0     ,0     ,0     ,0     ,0     ,0     ,0     ,0     ,
0     ,0     ,0     ,0     ,0     ,0     ,0     ,0     ,
0     ,0     ,0     ,0     ,0     ,0     ,0     ,0     ,
0     ,0     ,0     ,0     ,0     ,0     ,0     ,0     ,
0     ,0     ,0     ,0     ,0     ,0     ,0     ,0     ,
0     ,0     ,0     ,0     ,0     ,0     ,0     ,0     ,
0     ,0     ,0     ,0     ,0     ,0     ,0     ,0     ,
0     ,0     ,0     ,0     ,0     ,0     ,0     ,0     ,
0     ,0     ,0     ,0     ,0     ,0     ,0     ,0     ,
0     ,0     ,0     ,0     ,0     ,0     ,0     ,0     ,
0     ,0     ,0     ,0     ,0     ,0     ,0     ,0     ,
S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,
S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,
S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,
S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,
S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,
S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,
S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,
S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,
S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,
S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,
S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,
S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,
S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,
S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,
S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,
S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG,S_FLAG
};

On peut ainsi, lors de l'exécution d'une instruction qui peut avoir un effet sur le registre d'état du processeur, savoir quelle est la valeur est à utiliser pour ces deux flags. Pour le temps (Tstate) que prend chaque instruction lors de son traitement, on fait de même: un tableau qui contient les temps de chacune d'elles.

Les flags zéro et signe ne sont pas les seuls existant, d'autres comme la retenue ou le bit de parité existent aussi, tout ceci est lié au processeur que vous tentez d'emuler et est contenu dans ses spécifications.

Une fois que l'on a tous les tableaux nécessaires, on peut continuer et Créer plusieurs fonctions qui vont simuler le fonctionnement du processeur:

Il me vient à l'esprit trois fonctions différentes : Step, Init et, Interruption.

Step va exécuter une instruction et retourner le nombre d'octets pris par l'instruction, Init va initialiser à 0 tous les registres du processeur et Interruption va être appelé lors de la génération d'une interruption.

La fonction Step se résume à un switch() géant !:

i=RDMEM(cpu->PC++); /* On lit la mémoire ou est positionnéle Program Counter */
cpu->ICount-=NbCycles[i]; /* On met à jour le nombre de Tstates restants avant */
                            /* génération d'une interruption*/

switch(i) /* On traite l'instruction lue en mémoire */
{
   case JR_NZ: if(cpu->F&Z_FLAG) cpu->PC++; else { cpu->ICount-=5;JR; } break;
   case JR_NC: if(cpu->F&C_FLAG) cpu->PC++; else { cpu->ICount-=5;JR; } break;
   case JR_Z: if(cpu->F&Z_FLAG) { cpu->ICount-=5;JR; } else cpu->PC++; break;
   case JR_C: if(cpu->F&C_FLAG) { cpu->ICount-=5;JR; } else cpu->PC++; break;

   case JP_NZ: if(cpu->F&Z_FLAG) cpu->PC+=2; else { JP; } break;
   case JP_NC: if(cpu->F&C_FLAG) cpu->PC+=2; else { JP; } break;
   case JP_PO: if(cpu->F&P_FLAG) cpu->PC+=2; else { JP; } break;
   case JP_P: if(cpu->F&S_FLAG) cpu->PC+=2; else { JP; } break;
   case JP_Z: if(cpu->F&Z_FLAG) { JP; } else cpu->PC+=2; break;
   case JP_C: if(cpu->F&C_FLAG) { JP; } else cpu->PC+=2; break;
   case JP_PE: if(cpu->F&P_FLAG) { JP; } else cpu->PC+=2; break;
   case JP_M: if(cpu->F&S_FLAG) { JP; } else cpu->PC+=2; break;
...

Pour la fonction "Interruption" on vérifie plusieurs choses: Les interruptions sont-elles désactivées, est-ce une interruption non maskable que l'on doit traiter, et si oui, quel vecteur "adresse" d'interruption dois-je utiliser ?

if((cpu->IFF1&0x01) || (vecteur == INT_NMI))// Les interruptions sont elles activees ?
{                                           // ou est-ce une interruption non masquable ?
  PUSH(cpu->PC); // On sauvegarde la valeur du PC
  cpu->IFF1&=0xFE; // on remet a 0 IFF1

  if(cpu->IFF1 & 0x80) // Est-on en mode HALT
  { cpu->PC++; } // Si oui on increment le PC
    cpu->IFF1=cpu->IFF1 & 0x9F; // on sort du mode HALT
    switch(cpu->IFF1&0xF9)
   {
    case 0x00: // IM 0
        break;
    case 0x02: // IM1
        cpu->PC=INT_IRQ; // 0x0038
        break;
    case 0x04: // IM2
        vecteur=(vecteur&0xFF)|((u16)(cpu->I)<<8);
        cpu->PCl=RDMEM(vecteur++);
        cpu->PCh=RDMEM(vecteur);
        break;
    default:
        cpu->PC=vecteur;
        break;
    }
}

Ca peut paraitre compliqué au départ mais c'est une fonction qui, tout en restant indispensable, n'est pas forcément utile tout au départ de votre développement !

A ces trois fonctions on peut en rajouter d'autres qui facilitent par la suite la manipulation du processeur virtuel. Des fonctions qui permettent de changer l'état du processeur (qui modifient la variable "state") par exemple.

 

5.2 Comment emuler la mémoire (RAM et ROM)

Les ordinateurs actuels ont une quantité astronomique de mémoire lorsqu'on la compare à la quantité des machines construites dans les années 80. Ceci nous facilite la tâche car il nous suffit de créer un tableau d'octets et de le considérer comme l'espace d'adressage de l'ordinateur.

unsigned char memoire[64*1024];

Voila pour l'espace d'adressage, maintenant , la ROM ! Ce programme inséré sur une eprom est généralement adressé par le processeur à partir de l'adresse 0x0000, dans le cas du hector 1 cette rom a une taille de 4 kilo-octets. Il suffit de la mettre dans le tableau créé plus tôt.

if( read(descripteur, memoire, 4096) != 4096)
{
    fprintf(stderr, "ERREUR - Erreur de lecture du fichier '%s' \n", filename);
    close(descripteur);
    return 4;
}

Sur certaines machines comme le hector1, les périphériques sont accessibles d'une manière simple: on peut leur "parler" en écrivant ou lisant à des adresses en mémoire (différentes suivant la machine). Une autre raison pour laquelle il est nécessaure de vérifier les écritures en mémoire est que certaines adresses ont des miroirs à d'autres adresses, ou ne sont que lisibles ,etc... Il faut donc contrôler toutes les lectures ou écritures que l'on fait en mémoire. Pour cela on crée deux fonctions: WRMEM pour écrire et RDMEM pour lire.

 

5.3 Comment emuler l'écran (enfin le circuit vidéo )

Sur hector l'écran correspond à une zone mémoire qui court de 0x4000 à 0x49A0 en basse résolution. Tout ce que l'on écrit dans cette zone la se retrouve affiché à l'écran !

L'octet écrit là, se retrouve affiché à l'écran mais pas comme on se l'imagine: Sur hector un octet code pour 4 pixels différents et contigus.

Les Hector ont huit couleurs à leur disposition et peuvent en utiliser quatre simultanément. Cette palette que est stockée sur deux octets (0x1000 et 0x1800) indique au hector quelle est la couleur de la gamme a utiliser pour le numéro d'index 0, 1, 2 et 3. L'octet écrit dans la mémoire vidéo est composés d'un numéro d'index pour chaque pixel "contenu"dans l'octet.

Un exemple:

La palette comporte quatre couleurs et est initialisée comme suit:

  • Couleur 0 = Noir
  • Couleur 1 = Rouge
  • Couleur 2 = Jaune
  • Couleur 3 = Blanc

Si l'on veut afficher quatre pixels Jaune,noir,blanc et rouge si faut écrire en mémoire vidéo un octet rempli de la façon suivante :

7
6
5
4
3
2
1
0
Bit
1
0
0
0
1
1
0
1
Couleur Obtenue (et valeur format binaire)
02
00
03
01
Valeur (format decimal)

 

Il se peut parfois qu'on tombe sur des particularités de la machines qui ne sont pas référencées dans les documentations et ce n'est que lors de la programmation de l'émulateur que l'on s'en aperçoit!

En effet, sur hector, en basse résolution nous avons 0x49A0 - 0x4000 = 0x9A0 octets. Un octet codant pour 4 pixels, nous devrions voir 0x9A0 * 4 = 9856 pixels. Mais dans la documentation de la machine on lit que la résolution est de 77 lignes par 113 pixels ce qui ne fait que 8701 pixels !

Mais où sont donc passés les 1155 pixels manquants ? La première version de l'émulateur montrait tous les pixels:

On voit sur le coté droit de l'écran des pixels sans organisation cohérente, en réalité, sur un vrai hector, ils sont masqués.

Il faut que la fonction de mise à jour de la mémoire de notre émulateur en tienne compte : mettre à jour la mémoire mais ne pas afficher ces pixels.

Sans entrer dans les détails, lorsque l'on dessine l'écran, on effectue juste un modulo sur l'adresse de l'octet à tracer et si il se trouve dans cette zone on ne fait rien.

if( (adresse - 0x4000) % 32 < 28) AffichePixel();

La version de l'émulateur qui n'affiche pas cette zone ressemble à ça:


5.4 Comment emuler le clavier

Le clavier n'est pas forcément "banal" et il faut aussi connaitre son fonctionnement. Sur hector le fait de presser une touche met un bit à 0 et non pas à 1 comme on aurait tendance à le penser. Le code de la touche pressée est aussi stockée en mémoire et un autre octet est modifié pour signaler que l'on a bien pressé une touche !

Sous X-Window, cette routine d'émulation reste assez simple, on attache à l'évènement "une touche est pressée" et à "une touche est relachée" un bout de code qui va réaliser toutes les actions décrites ci-dessus.

void motif_keypress(Widget w, XtPointer client_data, XEvent * event, Boolean *conttodispatch)
{
 KeySym keysym;
 char buffer[128];
 XComposeStatus keyboard_status;

 XLookupString((XKeyEvent *) event, buffer, 128, &keysym, &keyboard_status);

On récupère le code des touches récemment pressées et on regarde ensuite quel type d'évènement nous arrive. Si c'est un éventement "touche enfoncée" (KeyPress) ou touche relachée (KeyRelease), on agit !

switch (event->type)
{
 ,...

 case KeyPress:
    switch (keysym)
    {
     case XK_c: /* 0x063 */
     case XK_d: /* 0x064 */
     case XK_e: /* 0x065 */
     case XK_f: /* */
     case XK_g: /* */
     case XK_h: /* */
     case XK_i: /* */
     case XK_j: /* 0x06a */ /* 0x3804 = adresse clavier touches C J */
          WRMEM(0x3804,RDMEM(0x3804) & ~(u8)(0x80 >> (keysym - XK_c)) );
          WRMEM(0x5FD0,(u8)0x01); /* Char Ready, 0x01 == une touche est prete */
          WRMEM(0x5FD1,(u8)keysym); /* Code du Caractere ASCII */
     break;
     ....
    }


 case KeyRelease:
    switch (keysym)
    {
     case XK_c: /* 0x063 */
     case XK_d: /* 0x064 */
     case XK_e: /* 0x065 */
     case XK_f: /* */
     case XK_g: /* */
     case XK_h: /* */
     case XK_i: /* */
     case XK_j: /* 0x06a */ /* On remet à 1 le bit correspondant à la touche */
          WRMEM(0x3804,RDMEM(0x3804)|(u8)(0x80 >> (keysym - XK_c)) );
          break;
     ...
    }
    break;
...
}

Toujours sur hector, les touches ne modifient pas toutes un bit aux mêmes adresses mémoires, en effet, elles sont organisées par "lignes" de huit touches.

Adresse/Bit
7
6
5
4
3
2
1
0
0x3800
Shift
Control
Rep
Back
Tab
Return
Espace
*
0x3801
+
,
-
.
/
0
1
2
0x3802
3
4
5
6
7
8
9
0x3803
;
=
?
A
B
0x3804
C
D
E
F
G
H
I
J
0x3805
K
L
M
N
O
P
Q
R
0x3806
S
T
U
V
W
X
Y
Z

 

Par exemple, la pression sur la touche N fera changer d'état le bit numéro 4 de l'octet 0x3805 (0xF7).


5.5 Comment emuler le lecteur de cassettes

5.5.1 Le chargement des jeux sur hector

Le chargement d'un jeu compose de plusieurs phases: La bande contient des données sous format analogique qui sont converties au format numérique par le circuit relié au magnétophone, ceci fait, le hector va reconstruire les différents octets lus et les stocker en mémoire avant de les exécuter.

Sur hector c'est à l'adresse 0x3000 que ça se passe ! Cet octet est composé de deux parties: La première longue d'un bit (bit 7) change d'état à chaque passage de la courbe en 0 (voir pour plus d'informations la page sur la conversion des cassettes hector en fichiers binaires) et les 7 autres bits font office de compteur matériel.

Lorsque le bit 7 change d'état on regarde à quel stade est le compteur matériel, suivant la valeur on sait alors si on a affaire à un 1, un 0 ou un cycle de synchronisation "gap".

L'émulation consiste à faire "croire" à la ROM que cette adresse est bien reliée au circuit de conversion. Dans la fonction RDMEM vue précédemment on ajoute un case

switch(adresse)
{
 ...
  case 0x3000:
         return(GetBit());
         break;
}

La fonction GetBit nous permet de lire le dump du logiciel que l'on a sélectionné auparavant et de fournir la bonne valeur lorsque l'on vient lire le contenu de l'adresse 0x3000

6. Conclusion et remerciements

50Ko de texte pour juste survoler le sujet! En effet, d'autre types d'émulations existent et il y a encore beaucoup à dire dessus. Merci à Jean Fontayne pour l'aide et les indispensables informations procurées sur les hector et merci à Romuald Liné pour les conseils, les corrections et les bonnes idées !

Si vous désirez apporter commentaires, remarques ou désirez que telle ou telle section soit étoffée, n'hésitez pas écrivez.

7. Postscriptum - Hemulector , le premier émulateur de Hector 1 !

La version "alpha" de cet émulateur est disponible pour les systèmes d'exploitation suivants:

  • AIX 4.3.x    : Vous devez être connecté pour télécharger
  • Linux (intel) : Vous devez être connecté pour télécharger
  • Solaris 2.x  : Vous devez être connecté pour télécharger
Note: Vous devez disposer de Motif (libXm.so.2) pour faire fonctionner la version Linux.

Framework PHP ©2002-2003 Stéphane Vanlierde