Les membres ayant 30 points peuvent parler sur les canaux annonces, projets et hs du chat.
La shoutbox n'est pas chargée par défaut pour des raisons de performances. Cliquez pour charger.

Forum Casio - Projets de programmation


Index du Forum » Projets de programmation » Emulateur fx-9860 SH4
Drakalex007 Hors ligne Membre Points: 688 Défis: 0 Message

Emulateur fx-9860 SH4

Posté le 08/11/2023 01:03

Salut !

Update: L’émulateur est testable en ligne! N’hésitez pas a me signaler si vous rencontrez des problèmes !

https://sh4.vercel.app/

Je tente actuellement d'en apprendre davantage sur les étapes nécessaires à la création d'un émulateur pour les calculatrices Casio monochromes (type Graph 35+/75) SH4.

Je suis tombé sur le projet d'IniKiwi qui est déjà bien avancé. Il peut émuler des add-ins écrits avec le SDK, mais ne gère pas encore les inputs: https://gitea.planet-casio.com/IniKiwi/nemu.
Ce repo est très utile pour avoir un apercu de la structure d'un tel émulateur.

Au vu de l'avancement du projet, je préfère repartir de zéro pour comprendre en détail le fonctionnement du code.
Je n'ai que très peu de connaissances bas niveau et sur le fonctionnement interne des CPUs et le meilleur moyen de vraiment comprendre ce qu'il se passe est de tenter de l’implémenter par moi-même.

C'est évidemment un projet assez gargantuesque, et je ne prévois pas de le terminer de sitôt, ni même de le terminer tout court. Mais je suis réellement intéressé par le défi!

Voici ce que j'ai réussi à comprendre jusqu'à présent :

Ce document décrit la structure d'un fichier G1A. On y apprend que le code d'un add-in commence à l'adresse 0x200 après un en-tête et s'étend jusqu'à la fin du fichier. C'est le point de départ de l'émulateur: extraire le code de l'add-in.

Ensuite, il faut convertir ce code en instructions. Pour cela, on a besoin de la documentation superH (le CPU utilisé par les calculatrices SH4).
Ce document nous indique que chaque instruction est codée sur 2 octets (16 bits) et contient la liste de 375 instructions, leur nom, leur fonction, leur encodage et leur implémentation. Super !
Il ne "reste plus qu'à" convertir chacune de ces instructions en code.

Et pour cela il va falloir émuler les fonctionnalités du CPU.
Je ne suis pas encore certain de tout ce qui est nécessaire en détail, mais pour mes premiers tests, j'ai isolé ces composants :

- 16 registres généraux R0-R15
- PC (Program Counter), qui indique l'adresse de l'instruction à exécuter. Sauf cas spécial, cette variable augmente de 2 octets (une instruction) à la fin de chaque instruction.
- Une "ROM" (read-only memory) qui sert de mémoire, dans laquelle on stocke le code initial.

Voici la structure C d'un tel CPU:

typedef struct {
    // General registers R0 - R15
    int32_t r[16];

    // System registers
    uint32_t pc; // Program counter

    // Memory
    uint8_t* rom;

} cpu_t;


Il manque encore un tas de choses. En lisant la doc il faudrait au moins ajouter tous ces registres:

Cliquez pour découvrir
Cliquez pour recouvrir
typedef struct{
    // General registers
    int32_t r[16]; // R0 - R15

    // Control registers
    uint32_t gbr; // Global base register
    uint32_t ssr;  // Saved status register
    uint32_t spc;  // Saved program counter

    uint32_t vbr; // Vector base register
    uint32_t sgr; // Saved general register 15
    uint32_t dbr; // Debug base register

    // System registers
    uint32_t mach; // Multiply-accumulate high
    uint32_t macl; // Multiply-accumulate low
    uint32_t pr; // Procedure register
    uint32_t pc; // Program counter

    // +
    // A0, A1: 40-bit registers
    // A0G, A1G: Guard bits for A0, A1
    // M0, M1, X0, X1, Y0, Y1: 32-bit registers

    // + VRAM

    // + inputs

    // + display

    // + timers
} cpu_status_t;


Mais chaque chose en son temps.
Je fais mes tests à partir de l'add-in de base "Sample Add-in" du fxsdk.

La toute première instruction de cet add-in est 0xD3 0x01 (1101001100000001), qui correspond selon la documentation à l'instruction MOV.L @(disp,PC), Rn.

Et là vient la première interrogation: cette instruction copie une valeur depuis la ROM vers un registre. Cependant, je ne suis pas certain de savoir comment initialiser le contenu de la ROM.

Doit-on copier le code de l'add-in dans la ROM dès le début, initialiser PC à 0 et lire la première instruction ? Y a-t-il des décalages à prendre en compte ? Quelles sont les "subtilités" à savoir pour mener à bien cette première instruction?

Voici l'implémentation actuelle de cette instruction:
// MOV.L @(disp,PC), Rn - 1101nnnndddddddd
void mov_l_disp_pc_rn(cpu_t* cpu, uint16_t instruction) {
    uint8_t rn = (instruction >> 8) & 0x0F;
    int8_t disp = instruction & 0xFF;

    int32_t addr = (int32_t)(disp << 2) + (cpu->pc & 0xFFFFFFFC) + 4;
    cpu->r[rn] = (int32_t)cpu->rom[addr];
    cpu->pc += 2;
}


Je suis en train de créer un depot sur Gitea pour que ce soit plus simple d'accéder au code entier.

Merci pour vos retours!

Fichier joint


Redcmd Hors ligne Membre Points: 380 Défis: 7 Message

Citer : Posté le 11/11/2023 20:10 | #


Je ne suis pas bien sûr d'avoir compris où est-ce que je devais/pouvais mettre la VRAM

the VRAM address is in a different position on almost every OS
0x88004991 on the sdk

mais en quoi consiste syscall_Bdisp_AllClr_DD ?

VRAM and the display are two different things
syscall_Bdisp_AllClr_VRAM clears the VRAM
while syscall_Bdisp_AllClr_DD clears the display memory

there should be a Bdisp_putDD or something syscall
it copies the VRAM to the display
GetKey internally runs that syscall
Lephenixnoir Hors ligne Administrateur Points: 24657 Défis: 170 Message

Citer : Posté le 11/11/2023 22:06 | #


Oui ton choix de mettre la VRAM après 64 kio est valide (faut juste pas que ça intersecte la RAM de l'add-in).

Locate très bien. Les syscalls en DD affectent directement l'écran sans passer par la VRAM. Tu dois donc dessiner "sur l'écran" sans modifier la mémoire associée à la VRAM.

Pour la police, la référence c'est le Character Set.pdf des manuels du SDK : https://bible.planet-casio.com/common/casio/sdk_manuals/ La police uf5x7 utilise à peu près les mêmes glyphes mais n'a pas tous les caractères, et l'encodage est différent. D'une lecture en diagonale ton code a l'air bon. Note que tu dois pas dépasser la position 21 en x, il me semble. Je sais plus ce qui arrive à la position du curseur quand tu sors, mais tu peux le tester en faisant un add-in basique dans le SDK.

Dans cette implémentation, le pixel 0 de la VRAM correspond au coin en haut à gauche de l'écran. Au sein des bytes, les bits sont stockés dans l'ordre inverse qu'affichés à l'écran, donc pour allumer le premier pixel de la première rangée, il faut mettre la valeur de vram[0] à 0b00000001, et pour le 8ème pixel ce serait 0b10000000.
Est-ce correct de stocker l'état des pixels dans la VRAM dans cet ordre? Ou est-ce que je devrais suivre un format particulier documenté quelque part?

Non, il faut que tu fasses l'inverse. MonochromeLib s'appuie sur le fait que c'est l'inverse. Même si, je te l'accorde, ce format-là est plus pratique.

Et dernière question, ce while(1) me crispe un peu, ce serait pertinent de le transformer en for(int i = 0; i < 999; i++) pour eviter des boucles infinies ?

Non, la boucle n'est pas infinie si la chaîne est bien formée. Mais bon, mieux encore, une fois que tu as dépassé la colonne 21 tu peux break.

Joli boulot !
Mon graphe (11 Avril): ((Rogue Life || HH2) ; PythonExtra ; serial gint ; Boson X ; passe gint 3 ; ...) || (shoutbox v5 ; v5)
Drakalex007 Hors ligne Membre Points: 688 Défis: 0 Message

Citer : Posté le 12/11/2023 21:00 | #


Pour l'historique, j'ai fait une grosse update concernant l'automatisation de l'extraction de toutes les instructions de la doc.

J'ai également créé un repo contenant le code source de l'émulateur.

----------------------------

@Lephenixnoir: Merci pour ces infos.

Avec cette avancée, j'ai rencontré de nouveaux syscalls :

if (syscall_code == 0x9AD) { // PrintXY
if (syscall_code == 0xACD) { // malloc
if (syscall_code == 0x462) { // GetAppName

PrintXY fut assez simple comme j'avais déjà Print, j'ai simplement créé une fonction qui dessine un caractère à l'écran, que j'utilise dans Print et PrintXY avec les paramètres appropriés.

Pour GetAppName, cette fonction est documentée dans la bible :

// The APPNAME of an addin can be found at offset 0x20 in the file,
// preceded by an '@'-character, and spans 8 characters, excluding '@'.
void syscall_GetAppName(cpu_t* cpu, char* dest) {
    // The app names is stored in 9 bytes
    dest = malloc(9 * sizeof(char));

    // Copies the app name to the new buffer
    memcpy(dest, &cpu->mem->rom[0x20], 9);

    // Return the buffer
    cpu->r[0] = dest;
}
Ça semble correct ?

Ensuite, pour malloc, je ne suis pas sûr de l'implémentation. Je n'ai pas trouvé dans la bible, mais j'ai repris l'implémentation d'Inikiwi qui ressemble à celle-ci :

#define MALLOC_MEM_LOW RAM_START_MMU_ALIAS + (32 * 1042)
#define MALLOC_MEM_HIGH RAM_START_MMU_ALIAS + RAM_SIZE - (16 * 1024)
#define MALLOC_MARGIN 0x200

typedef struct{
    uint32_t size;
    uint32_t addr;
} malloc_info_t;

// Allocate memory and return the address of the allocated memory in r0
void syscall_Malloc(cpu_t* cpu, uint32_t size) {
    memory_t* mem = cpu->mem;

    // Initialize with the lowest address
    int addr = MALLOC_MEM_LOW;

    // If there are mallocs, start at the end of the last malloc
    if (mem->mallocCount > 0) {
        addr = mem->mallocs[mem->mallocCount - 1].addr +
               mem->mallocs[mem->mallocCount - 1].size + MALLOC_MARGIN;
    }
    
    // Update malloc manager
    mem->mallocCount++;
    mem->mallocs = realloc(mem->mallocs, mem->mallocCount * sizeof(malloc_info_t));

    if (mem->mallocs == 0) {
        critical_error("syscall_malloc(): Could not allocate memory");
    }

    mem->mallocs[mem->mallocCount - 1] = (malloc_info_t){size, addr};

    // Return the address of the allocated memory
    cpu->r[0] = addr;
}

Est-ce que cette implémentation est valide ? Où est-ce que je pourrais trouver des infos, notamment sur la valeur des define et du processus interne?

Il y a aussi d'autres syscalls dont je ne suis pas sûr de la fonction ou de l'implémentation, qui semblent être liés à la lecture/écriture de fichiers :

if (syscall_code == 0x43B) // Bfile_FindFirst
if (syscall_code == 0x434) // Bfile_CreateEntry_OS
if (syscall_code == 0x42C) // Bfile_OpenFile_OS
if (syscall_code == 0x82B) // MCSPutVar2
if (syscall_code == 0x840) // MCSGetDlen2

J'imagine que Bfile_FindFirst cherche un fichier, Bfile_CreateEntry_OS crée un ficher et Bfile_OpenFile_OS ouvre un fichier.
Cependant c'est plus flou pour MCSPutVar2 et MCSGetDlen2, a quoi servent-elles?

Jusqu'ici, j'ai simplement ignoré ces syscalls. Cependant, elles semblent essentielles pour émuler un addin qui dispose d'un système de sauvegarde, ce qui est très fréquent. Comment pourrais-je m'y prendre pour implémenter ces syscalls, et potentiellement les autres syscalls nécessaires pour la lecture/écriture de fichiers dans mon émulateur?

Finalement, j'ai aussi ignoré ces syscalls, sont-elles nécessaires ?

if (syscall_code == 0x014) // GlibGetAddinLibInf
if (syscall_code == 0x015) // GlibGetOSVersionInfo

Lephenixnoir Hors ligne Administrateur Points: 24657 Défis: 170 Message

Citer : Posté le 13/11/2023 15:23 | #


Ton GetAppName ne va pas non - l'usage du pointeur crée un mélange très inconsistent entre tes pointeurs hôte (ceux de ton programme, qui font 64 bits d'ailleurs) et les pointeurs du système émulé. Tu dois avoir un warning sur cpu->r[0] = dest, à juste titre. Tu n'as pas besoin d'allouer de mémoire dans cette fonction parce que dest est déjà l'adresse (dans la mémoire émulée) d'un buffer réservé par l'appelant. Le code devrait ressembler à ça :

void syscall_GetAppName(cpu_t* cpu, uint32_t dest) {
    // Address translation like in mem_read() and mem_write()
    char *dest_ptr = mem_translate(dest);
    if(!dest_ptr) { /* segfault */ }

    memcpy(dest_ptr, &cpu->mem->rom[0x20], 9);
    cpu->r[0] = dest;
}

Note que les pointeurs du programme émulé ne sont jamais des pointeurs pour toi. Le type de données doit être différent et la conversion doit passer par mem_translate() ou une fonction du même type si tu en as une.

Cette implémentation de malloc() ne convient pas non plus : elle ne fait qu'avancer en ligne droite et ne réutilise pas la mémoire. Au mieux pour debugger au début et voir si ça marchotte, mais je trouve personnellement malloc() trop important et cette approche simple trop bancale pour faire le taf. Il faut que tu réserves une zone pour le tas, de 48 kio si ma mémoire est bonne, à une adresse arbitraire comme la VRAM. Et ensuite ton syscall malloc() doit la découper et la gérer. Comme tu n'utilises pas une copie de l'OS, tu n'as pas accès au code de malloc() de l'OS. Je ne conseille pas trop d'en écrire un à partir de zéro parce que c'est un peu difficile ; je te proposerais d'utiliser celui de gint (kmalloc.h, src/kmalloc) mais quelques ajustements seraient sans doutes nécessaires (endianness, taille des types, ce genre de trucs).

Pour Bfile, la doc de SimLo est disponible, celle du SDK complète, et il y a celle de gint si vraiment t'as un doute. MCS c'est la mémoire principale et je n'ai aucune idée de comment ça marche pour être franc. SimLo a documenté ça, mais je considère ça une fonctionnalité "avancée" et je serais surpris que tu en aies besoin pour la plupart des add-ins bêtes et méchants.

Pour implémenter Bfile, il faut que tu prennes un dossier sur ton ordinateur qui corresponde à la mémoire de stockage, et ensuite tu implémentes les interfaces de Bfile avec les fonctions standard sur les fichiers. Bfile_FindFirst() utilisera par exemple opendir() pour explorer les fichiers. Là un peu de flexibilité sera nécessaire car le système de fichiers de ton ordinateur n'est pas une réplique exacte de la mémoire de stockage.

Les deux syscalls Glib sont des trivialités, ils font pas grand-chose mais sont pas durs à implémenter non plus. Le deuxième demande littéralement juste la version de l'OS. Le premier accède aux infos dans le header de l'add-in, je crois. Ils sont documentés dans le chm de SimLo.
Mon graphe (11 Avril): ((Rogue Life || HH2) ; PythonExtra ; serial gint ; Boson X ; passe gint 3 ; ...) || (shoutbox v5 ; v5)
Drakalex007 Hors ligne Membre Points: 688 Défis: 0 Message

Citer : Posté le 15/11/2023 18:55 | #


Effectivement, j'ai eu des confusions sur le fait que certains pointeurs pointent vers la mémoire de mon PC tandis que d'autres pointent vers une adresse de mon émulateur. C'est plus clair maintenant ! J'ai changé GetAppName comme ceci :

// Copies the registered name for the running application into the character array dest.
// dest must be able to hold 9 bytes. dest is returned.
void syscall_GetAppName(cpu_t* cpu, char* dest) {
    // Copies the app name to the new buffer
    for (int i = 0; i < 9; i++) {
        mem_write(cpu, (uint32_t)dest + i, cpu->mem->rom[0x20 + i], 1);
    }

    R0 = (int32_t)dest; // Return the buffer
}

Je vais aussi effectivement devoir revoir malloc. Merci pour ces informations

Concernant l'émulateur, j'ai intégré emscripten comme le fait circuit10 sur le sien, ce qui me permet de le compiler en webassembly et de permettre de tester l'avancée directement depuis un navigateur !

Il est disponible à ce lien : https://sh4.vercel.app/

Par défaut, le nombre d'instructions par frame est très bas, donc les inputs mettent des secondes à s'enregistrer, mais il est possible de l'augmenter. Sur Jetpack, il faut l'augmenter à plus de 10,000 instructions/frame pour du 1fps. J'affiche également directement le contenu de la VRAM, mais il y a une option pour afficher uniquement le contenu de l'écran LCD !

Pour la gestion des inputs, j'ai réussi grâce à ce document de la bible à reverse-engineer le comportement du périphérique qui gère le clavier. Ce périphérique permet entre autres de renseigner sur l'état des touches de la calculatrice (appuyées ou non). À noter qu'il renvoie les informations par rangées de touches, encodées dans 1 byte avec 1=relâché, 0=enfoncé.

Le document de la bible inclue un code détaillant l'utilisation de ce périphérique.
Il est possible d'interagir avec ce périphérique via les addresses suivantes (sur SH3):
short*PORTB_CTRL=(void*)0xA4000102;
short*PORTM_CTRL=(void*)0xA4000118;
char*PORTB=(void*)0xA4000122;
char*PORTM=(void*)0xA4000138;
char*PORTA=(void*)0xA4000120;

Ici, il y a 3 ports : A, B et M.
B et M sont des ports de configuration, on va écrire des données à ces adresses pour configurer le périphérique qui gère le clavier, notamment sur quelle rangée on veut récupérer une valeur.
A est le port de sortie et contient le résultat d'après la configuration des ports B et M. Une fois A récupéré (l'état d'une rangée), il est possible de retrouver la valeur de la colonne qui nous intéresse en faisant du bit shift.

Sauf que sur mon émulateur, il n'y a rien derrière ces adresses. Aucun périphérique ne va recevoir les inputs de B et M, puis écrire sur A. Il va falloir que je l'émule. Tout d'abord, il faut une structure qui permette de stocker l'état de chaque rangée. Il y a 10 rangées, et chaque rangée est encodée dans un byte, on a donc :
uint8_t keyboard[10];

Ensuite, j'ai créé une zone mémoire a l'adresse du port de contrôle de B. Je n'ai pas besoin de toutes les autres valeurs qui sont redondantes: elles encodent la meme chose mais différemment.
Finalement, la seule opération que l'on va avoir besoin d'émuler est la lecture du résultat, soit une lecture à l'adresse A (0xA4000120). Lorsque je détecte une lecture à cette adresse, un algorithme va lire les valeurs des ports B et M, et les reverse engineer de façon à trouver 1) la valeur de la variable smask puis 2) la valeur originale de row passée en argument. Une fois que j'ai trouvé la valeur originelle de row, je peux juste retourner la rangée correspondante encodée sous forme de byte !

// Version simplifiee du code de la bible pour interagir avec les registres du clavier
// Probleme: Depuis la valeur de PORTB_CTRL, comment retrouver la valeur originelle de row ?
int CheckKeyRow( int row ) {
  smask = 0x0003 << ((row%8)*2);
  *PORTB_CTRL = 0xAAAA ^ smask;  
  ...
  result = ~(*PORTA);
}

// Solution:
uint16_t PORTB_CTRL = mem_read(cpu, 0xA4000102, 2);
uint16_t smask = PORTB_CTRL ^ 0xAAAA; // Reverse-engineer smask
uint8_t row = 0;
while (smask >> (row*2) > 0b11) row++; // On a reverse-engineer row!

result = cpu->keyboard->rows[row]; // On retourne le resultat

A noter que ce code ne prend en compte que les 8 premieres rangées, il y a une toute petite variation pour les deux dernières.
Pour la gestion de l'écran LCD, le principe est assez similaire mais avec quelques modifications et l'utilisation de variables statiques.

Cela me permet d'en venir à la suite des choses : Je peux désormais me balader dans les menus de Jetpack, sauf qu'il semble y avoir une boucle infinie lorsque je lance une partie. Je soupçonne le fait d'avoir omis la gestion de fichier de causer des problèmes, donc j'ai commencé à m'y pencher. Mais je fais face à un problème :

Voici à quoi ressemble le code de Jetpack pour la lecture du fichier de sauvegarde (oui c'est horrible mais au final très pratique pour tester la bonne implémentation d'un file system ! )
Save *save = (Save*)malloc(sizeof(Save));

handle = Bfile_OpenFile(PathName,_OPENMODE_READ); // on ouvre le fichier en mode lecture

if (handle < 0) { // si pas de fichier
     Bfile_CreateFile(PathName,sizeof(Save));           // CreateFile
     handle = Bfile_OpenFile(PathName,_OPENMODE_WRITE); // OpenFile
     Bfile_WriteFile(handle,save,sizeof(Save));         // WriteFile
     Bfile_CloseFile(handle);                           // CloseFile
     handle = Bfile_OpenFile(PathName,_OPENMODE_READ);  // OpenFile
}

Bfile_ReadFile(handle,save,sizeof(Save),0); // on charge la sauvegarde
Bfile_CloseFile(handle); // on ferme le fichier

Dans mon émulateur, jusqu'ici j'ignorais complètement toute syscall relative à des fichiers. Notamment Bfile_OpenFile_OS est censé retourner une valeur négative en cas d'erreur, dans mon cas ça retourne la valeur précédente du registre donc n'importe quoi.
Je m'attends donc à voir un log du style OpenFile -> ReadFile -> CloseFile, et avoir n'importe quoi dans ma save.
Sauf que j'observe à la place OpenFile -> GetAppName -> MCSGetDlen2. Aucune autre syscall de type File n'est appelé.

Run Syscall: Malloc #0 (size: 128)
Syscall not implemented, skipping: Bfile_OpenFile_OS
Run Syscall: GetAppName (@JETPACK)
Syscall not implemented, skipping: MCSGetDlen2
Run Syscall: GlibGetAddinLibInf

Du coup, j'ai essayé de commencer à implémenter OpenFile selon la doc de simlo pour avoir un comportement correct. Elle retourne désormais -1 vu que le fichier n'existe pas, et donc on arrive dans le "if" du code précédent.
Et cette fois-ci, le comportement est différent et crash:

Run Syscall: Malloc #0 (size: 128)
Run Syscall: Bfile_OpenFile_OS    (JETPACK.sav, 0x1, 0x0) handle: 0x0
Run Syscall: Bfile_CreateEntry_OS (JETPACK.sav, 0x1) size: 128, handle: 0x14AEF8
Run Syscall: Bfile_OpenFile_OS    (JETPACK.sav, 0x2, 0x0) handle: 0x14AEF8
Run Syscall: GetAppName (@JETPACK)
Syscall not implemented, skipping: MCSPutVar2
[Error] Request for memory at address 0x09ADAD98 is out of bounds // Arrive 9 instructions apres MCSPutVar2

Voici mes implémentations actuelles de OpenFile_OS et CreateEntryOS :

// mode: 1 = read, 2 = write
void syscall_Bfile_OpenFile_OS(cpu_t* cpu, const char* filename, int mode, int mode2) {
    const char* name = convertFileName(filename);

    FILE* handle = fopen(name, mode == 1 ? "rb" : "wb");

    printf("Run Syscall: Bfile_OpenFile_OS (%s, 0x%X, 0x%X) handle: 0x%X\n", name, mode, mode2, (uint32_t)handle);

    R0 = handle == NULL ? -1 : (int32_t)handle;
}

void syscall_Bfile_CreateEntry_OS(cpu_t* cpu, const char* filename, int mode, int size_ptr) {
    const char* name = convertFileName(filename);

    FILE* handle;

    if (mode == 0x1) { // Create file
        handle = fopen(name, "wb");
        fclose(handle);
    }
    else printf("[Warning] CreateEntry mode not implemented\n");

    printf("Run Syscall: Bfile_CreateEntry_OS (%s, 0x%X) size: %d, handle: 0x%X\n", name, mode, mem_read(cpu, size_ptr, 4), (uint32_t)handle);

    R0 = handle == NULL ? -1 : (int32_t)handle;
}


Elles utilisent convertFileName qui transforment une chaine de type = {'\\','\\','f','l','s','0','\\','J','E','T','P','A','C','K','.','s','a','v',0}; en "JETPACK.sav":

convertFileName()
Cliquer pour enrouler
const char* convertFileName(const char* filename) {
    char name[40];
    char ch;
    int i;

    // Convert filename from { 0x00, 0xXX, 0x00, 0xXX, ... } to { 0xXX, 0xXX, ... }
    for (i = 0; i < 40; i++) {
        uint16_t ch = *(uint16_t*) &filename[i * 2 + 1];
        name[i] = ch;
        if (ch == 0x00) break;
    }

    // Remove '\\fls0\'
    i -= 6;
    char* final_name = malloc(sizeof(char) * i);
    memcpy(final_name, name + 7, i);

    return (const char*)final_name;
}


Une idée de ce qu'il pourrait se passer ? Comment devrais-je implémenter MCSPutVar2, est-ce que ça pourrait être la cause du memory out of bounds ?
Drakalex007 Hors ligne Membre Points: 688 Défis: 0 Message

Citer : Posté le 15/11/2023 20:59 | #


Petite mise à jour concernant le crash lorsqu'on lance une partie. En fait, c'était dû à mon initialisation des registres qui était un copié collé de ce que je voyais sur l'émulateur du Casio SDK :

J'ai tout enlevé et remplacé par ce que j'utilisais avant, et maintenant une partie se lance correctement !
La nouvelle version disponible en ligne!

#define PC_PROGRAM_START 0x00300200 // Execution starting address
#define PR_INIT_VALUE    0xffeeeeee // Random value used to check for app return
#define SR_INIT_VALUE    0x700000f0 // Status Register initialization value (from doc)
#define RAM_START        0x8801c000
#define RAM_SIZE   (512 * 1024)

cpu->pc = PC_PROGRAM_START;
cpu->pr = PR_INIT_VALUE;
cpu->sr = SR_INIT_VALUE;
cpu->r[15] = RAM_START + RAM_SIZE;

// Mimic Casio SDK (Removed -> fixed crash)
// cpu->r[0] = 0x00300200;
// cpu->r[1] = 0x00000003;
// cpu->r[2] = 0xA002A67C;
// cpu->r[3] = 0x0000009D;
// cpu->r[4] = 0x00000001;
// cpu->r[5] = 0x00000000;
// cpu->r[6] = 0x00000020;
// cpu->r[7] = 0x00300000;
// cpu->r[8] = 0xA0035988;
// cpu->r[9] = 0x00000000;
// cpu->r[10] = 0x00000000;
// cpu->r[11] = 0x880068CC;
// cpu->r[12] = 0xA00D2B38;
// cpu->r[13] = 0x00005D9D;
// cpu->r[14] = 0x00000000;
// cpu->r[15] = 0x88023F98;
// cpu->sr = 0x40000001;
// cpu->gbr = 0xFFFFFFE0;

Une idée de ce qui pouvait poser des problèmes lors du lancement d'une partie avec le précédent code d'initialisation?

De plus, j'ai ajouté l'optimisation -O2 lors de la compilation. C'est bien plus rapide ! J'arrive sur du temps réel désormais (la vidéo n'est pas accélérée, au contraire ralentie par l'enregistrement! Mais j'ai un très bon PC)
La video s’arrête sur une syscall non implémentée




Je reste tout de meme a l’écoute en ce qui concerne le file system!
Slyvtt En ligne Maître du Puzzle Points: 2408 Défis: 17 Message

Citer : Posté le 15/11/2023 21:55 | #


Ah superbe, bravo.
Ca c'est du lourd.
Tu nous fais le même en SH4 après pour émuler les G35 plus récentes ?
There are only 10 types of people in the world: Those who understand binary, and those who don't ...
Drakalex007 Hors ligne Membre Points: 688 Défis: 0 Message

Citer : Posté le 15/11/2023 23:09 | #


Oui à la base je visais du SH4, mais au final comme j'utilise l’émulateur du Casio SDK qui est SH3 pour mes tests c’était plus simple de prendre les adresses SH3.
J'ai tenté de bien documenter les endroits où il y a une difference entre SH3 et SH4 pour pouvoir facilement convertir plus tard, notamment lorsque j'essayerai de supporter gint!
Drakalex007 Hors ligne Membre Points: 688 Défis: 0 Message

Citer : Posté le 18/11/2023 11:27 | #


Grosse mise à jour concernant l'émulateur !

- Importez vos propres G1A!
- Contrôles claviers
- Mode debug (affichage des registres + instruction par instruction)
- Plus rapide!

===> Lien de l’émulateur: https://sh4.vercel.app/

J'ai enfin réussi à comprendre comment les fichiers marchaient. En fait, ce n'était pas bien compliqué, c'est juste qu'il y a un détail très important qui m'échappait.
La valeur de retour des fonctions comme Bfile_OpenFile_OS est un handle. Au départ, je retournais simplement le handle de fopen en me disant qu'il servirait juste comme identifiant dans le reste des syscalls. Mais en faisant ça, je me retrouvais avec des memory out of bound. J'ai ensuite essayé de retourner uniquement un index qui représente le numéro du fichier en partant de zéro. Ça a résolu mes problèmes de memory out of bounds, mais je me retrouvais dans la situation de mon message un peu plus haut à devoir gérer des MCS handles complexes et moins documentés, qui menaient tôt ou tard à des crashs.

Ce que j'ai donc fait, c'est d'examiner la valeur de retour de cette syscall dans le debugger du Casio SDK. Et là, j'ai compris ce qu'il se passait : un handle représente bien un index de fichier, mais il est composé de la sorte : 0x01000000 | index. De plus, j'ai appris que l'OS officiel ne pouvait pas ouvrir plus de 4 fichiers en même temps. Ainsi, il ne peut y avoir que 4 handles distincts : 0x01000000 pour le premier fichier, puis 0x01000001, 0x01000010 et 0x01000011.

Et en modifiant mon code pour me conformer à ce pattern, comme par magie, toutes les instructions obscures relatives au MCS qui me posaient souci (MCSGetDlen2, MCSGetData1, MCSPutVar2, MCSOvwDat2) ont tout simplement disparu de la liste d'appels. Apparemment, la valeur du handle avait un impact direct sur la suite du code et redirigeait vers des sets de syscalls différents. J'ai donc pu implémenter sans encombre les fonctions relatives à Bfile (Create/Open/Close/Read/Write/Seek), que j'ai pu copieusement documenter grâce à ce gist très intéressant trouvé sur le net qui détaille en profondeur toutes les fonctions de fxlib.

Ensuite, je me suis heurté à un souci sur plusieurs add-ins: certains éléments étaient visuellement incorrects mais de façon assez minime. Par exemple, l'ombre projetée au sol dans Jetpack n'apparaissait pas dans mon émulateur. En examinant les parties de code en questions, j'ai pu associer ce phénomène aux opérations qui utilisent des float. L'ombre était calculée avec ML_ellipse de MonochromeLib, qui est la seule fonction de la librairie qui déclare des float. En poussant plus loin, je me suis rendu compte que lorsque j'affichais des float avec sprintf, ça affichait des résultats totalement faux. J'ai mis pas mal de temps à isoler le problème, j'ai même ajouté un debugger qui permet d'afficher la valeur de tous les registres et d'avancer instruction par instruction dans ma demo web, et j'ai finalement mis le doigt sur un souci avec le Status Register (SR). Il contient 32 bits dont 4 (T, Q, S et M) sont utilisés pour stocker des résultats intermédiaires dans plusieurs instructions, et leur position est importante. Pour éviter d'avoir à faire des bit shift pour isoler ces 4 composants, la doc officielle propose cette construction :

struct SR0 {
     unsigned long dummy0:22;
     unsigned long M0:1;
     unsigned long Q0:1;
     unsigned long I0:4;
     unsigned long dummy1:2;
     unsigned long S0:1;
     unsigned long T0:1;
};

#define M ((*(struct SR0 *)(&SR)).M0)
#define Q ((*(struct SR0 *)(&SR)).Q0)
#define S ((*(struct SR0 *)(&SR)).S0)
#define T ((*(struct SR0 *)(&SR)).T0)

Je me suis rendu compte que les instructions qui manipulaient ces bits donnaient des valeurs étranges à SR, et effectivement les positions des bits manipulés étaient inversées par rapport à la doc.
Comme je n'étais pas familier avec cette syntaxe, je n'avais pas vérifié l'ordre avant, en faisant confiance à la doc. Mais certaines instructions dépendent directement de la position de ces bits et ne passent pas par les #define, par exemple SR & 0x00000001.

J'imagine que c'est ça la première "subtilitée" de la méthode que j'ai utilisée ! Avec ce changement, l'ombre s'affiche correctement, les valeurs des float également et ça a résolu des problèmes dans plusieurs addins que j'ai testés. C'est à se demander comment tout pouvait marcher aussi bien jusqu'ici avec un status register inversé !

Finalement, j'ai ajouté des contrôles claviers pour les touches replay (flèches), exit (échap), exe (entrée) et shift, et la possibilité d'importer et tester ses propres add-ins. Il y a aussi 5 add-ins inclus dans la démo qui ont l'air de fonctionner parfaitement jusqu'ici (Jetpack, Gravity Duck, Deadlabs, 2048, et Fruit Ninja, ce dernier étant assez amusant avec la souris!).

Concernant la compatibilité, c'est un peu confus au final. J'ai testé une dizaine de jeux du top 20, à la fois les versions SH3, SH4 et 35+EII, mais impossible de faire un verdict. L'émulateur fonctionne pour certains SH3 mais pas d'autres, et c'est pareil pour les versions SH4 et 35+EII, sans pouvoir isoler un élément commun à chaque fois.

La plus grosse incompatibilité pour l'instant est l'appel de la syscall 0x3ED : Interrupt_SetOrClrStatusFlags. Cet appel provoque une boucle infinie certaine que je n'ai pas réussi à résoudre jusqu'ici. Je n'ai absolument aucune idée non plus de ce qui déclenche cette syscall, elle n'est pas reliée à une instruction précise. Il s'agit d'un topic entier qui est la gestion des interruptions (et des exceptions ?), et j'ai l'impression d'arriver au bout de ce que je suis capable de faire de ce côté, après avoir lu la doc, la bible et même survolé le code de gint je ne suis pas bien avancé.

Je n'ai pas eu l'occasion de tester des addins gint donc je ne sais pas si certains fonctionnent, en tout cas je sais qu'il manque plusieurs choses dans mon émulateur pour pouvoir entièrement supporter gint (instructions DSP, X/Y/ILRAM et potentiellement d'autres).

Pour finir, il reste encore pas mal de place côté optimisation. La plus notable serait de transformer la longue liste de "if" qui sert à déterminer l'instruction à partir de son code (instructions.c) en une table de saut qui permettrait de "sauter" directement à une sous-fonction en prenant les 8 premiers bits du code d'une instruction (256 sauts possibles) pour avoir moins de conditions à checker à chaque itération. J'ai essayé de faire ça avec un tableau de 256 pointeurs de fonctions mais ça n'a pas marché (probablement que je m'y suis mal pris).

En tout cas, n'hésitez pas à tester vos addins et à me faire des retours. Si vous observez des crashs, vous pouvez me partager l'erreur qui s'affiche dans la console et le G1A en question, je pourrai m'y pencher !
Yannis300307 Hors ligne Membre Points: 297 Défis: 0 Message

Citer : Posté le 19/11/2023 09:13 | #


C'est génial ! ça marche parfaitement bien malgré quelques ralentissements au moment des syscall ! Est ce que tu penses sortir une version desktop Windows et Linux ? Et si oui, est ce que je pourrais les intégrer dans Casio Dev Tools ?

Je viens de remarquer que sur Gravity Duck, le bouton "exit" fait freeze l'add-in jusqu'à se qu'on appuie sur "exe". Je pense qu'il est censé ouvrir le menu pause.
WOW ! Mais qu'est-ce-que je vois ??!! Une extension VS Code qui permet de simplifier le développement sur calculatrices ??!! C'est ici : Casio Dev Tools. C'est incroyable ! C'est prodigieux !
Drakalex007 Hors ligne Membre Points: 688 Défis: 0 Message

Citer : Posté le 29/11/2023 18:38 | #


@Yannis300307: Ce n'est pas prévu pour tout de suite car j'ai envie de créer une suite d'outil entièrement en ligne pour que ce soit le plus simple d'accès possible. Je n'ai pas trop envie de m'embêter avec SDL pour l'instant, mais le repo étant sur Gitea, il est tout à fait possible pour quelqu'un de motivé de s'y attaquer et de créer une PR !

Pour le bug sur Gravity Duck, en effet, la syscall Getkey avait en fait une fonction cachée qui est de copier la VRAM à l'écran. Je l'ai ajoutée et ça marche bien désormais !

@RDP:
Article pour la prochaine RDP
Cliquer pour enrouler
Salut tout le monde!
Vous avez peut-être vu passer dans le forum mon projet du moment, qui est une tentative de créer un émulateur pour calculatrices monochrome SH4.
(Dans le futur, j'aimerais pouvoir supporter également les add-ins SH3 et 35+EII en parallèle.)

Le but ultime de ce projet serait de pouvoir tester tous les add-ins monochromes directement depuis planet-casio avant de les télécharger, et de proposer un environnement de développement en ligne pour la compilation, l’émulation et le debugging d'add-ins.

Grâce à la précieuse aide de Lephe et du forum, j'ai pu avancer très vite dans ce projet et publier la première version fonctionnelle directement testable en ligne : https://sh4.vercel.app/.

Cette démo comporte les jeux suivants qui marchent sans soucis : Jetpack Joyride, Gravity Duck, Fruit Ninja, 2048, Test Andropov, Orton and the Princess, Dead Labs et Hardest Game.
Il est aussi possible d'importer ses propres fichiers .G1A. S'ils sont SH4 et n'utilisent pas gint, il y a de grandes chances qu'ils marchent !
Finalement, si l'émulation est trop lente, il est possible d'augmenter le nombre d'instructions dans le slider en haut à droite.

La prochaine étape est désormais d'intégrer gint à l'émulateur, ce qui s'avère être une tâche tout aussi complexe.
Vous pouvez suivre l'avancement du projet sur le topic officiel, n’hésitez pas a me signaler les add-ins incompatibles ainsi que les erreurs rencontrées !
Le code source est également disponible sur Gitea : https://gitea.planet-casio.com/Drakalex007/fx9860-emulator-playground

Drakalex007 Hors ligne Membre Points: 688 Défis: 0 Message

Citer : Posté le 29/11/2023 18:58 | # | Fichier joint


Maintenant que je suis arrivé à un émulateur qui est fonctionnel avec plusieurs add-ins compatibles SH4, j'aimerais m'attaquer à une étape qui semble être de complexité équivalente à tout ce que je viens de faire jusqu'ici : l'intégration de gint *musique démoniaque*.

En effet, en essayant d'intégrer gint à mon émulateur, je me suis rendu compte à quel point le SDK de base utilise peu de fonctionnalités du CPU et du MMU. Il manque actuellement un tas de choses (interruptions, timers, x/y/ilram, dsp, et j'en passe), pourtant de nombreux add-ins créés avant la publication de gint semblent marcher parfaitement sans ces éléments.

Pour supporter gint, il faut tout d'abord que je choisisse un modèle de calculatrice spécifique à émuler pour retourner des valeurs consistantes lorsque gint essaiera d'identifier les caractéristiques de l'OS et du hardware. J'ai choisi la Graph 75+E SH4 car c'est celle qui semble avoir le moins de limites en termes de mémoire disponible.

La première chose que fait gint est de détecter le MPU de la calculatrice (src/kernel/hardware.c#L32). Pour me conformer au modèle que j'ai choisi, je retourne 0x10300b00 lorsque gint accède au PVR et 0x00002c00 lorsque gint accède au PRR, ce qui flag le MPU comme étant HWMPU_SH7305.

Ensuite, gint essaie de détecter le modèle de l'OS à l'adresse 0x80010021 (hardware.c#L81). Il semble que '3' désigne les Graph 35+EII qui utilisent le nouveau moteur de rendu HWFS_FUGUE, et toutes les autres valeurs utilisent HWFS_CASIOWIN, ce qui est mon cas car je retourne '2' (c'est correct pour la 75+E ?).

Après ça, gint va essayer de détecter si la calculatrice dispose d'une extension de RAM en changeant la valeur à l'adresse 0x88040000 et en regardant si cela affecte la valeur à l'adresse 0x88000000 (hardware.c#L92). Sur mon émulateur, ces deux adresses sont bien différentes, donc gint détecte une extension de RAM.

Finalement, gint va effectuer un scan aux adresses 0xf6000000 et 0xf7000000 afin de récupérer l'adresse physique de la RAM je crois (src/mmu/mmu.c#L133). Ces adresses serviront aussi à calculer la taille de la RAM. J'ai pu trouver une explication très détaillée de Lephe qui répondait à Inikiwi face au même problème un an plus tôt, ce qui m'a été très utile pour émuler le comportement a ces adresses.

Toutes ces étapes étaient nécessaires à l'initialisation du cœur de gint, maintenant passons aux drivers.

De ce que j'ai pu comprendre du code, gint prend le contrôle (presque) total de l'OS en définissant plusieurs drivers qui vont chacun agir sur des composants spécifiques connectés au MMU. Au lancement de gint, chaque driver sauvegarde l'état des registres importants pour chaque composant, afin de pouvoir les restaurer à l'identique à la sortie du programme. Ces registres ont tous une adresse spécifique qui peut être trouvée dans la documentation du MMU SH7724.

La première étape va être de créer des zones de mémoire pour toutes ces adresses qui sont pour l'instant non-attribuées dans mon émulateur. Pour l'instant, je ne cherche pas directement à émuler chaque composant, je le ferai au fur et à mesure quand ce sera nécessaire.

J'ai d'abord créé des zones de mémoire aux adresses pour les drivers INTC et MMU, ce qui m'a permis d'émuler cet add-in très simple créé avec gint et de l'afficher à l'écran :
#include <gint/display.h>

int main(void) {
    dclear(C_WHITE);
    dtext(1, 1, C_BLACK, "Sample fxSDK add-in.");
    dupdate();

    return 1;
}

Ensuite, j'ai ajouté l'instruction getkey :

#include <gint/display.h>
#include <gint/keyboard.h>

int main(void) {
    dclear(C_WHITE);
    dtext(1, 1, C_BLACK, "Sample fxSDK add-in.");
    dupdate();
    getkey();

    return 1;
}


Et c'est là que les choses se compliquent. J'ai dû ajouter les zones mémoires pour les drivers CPG, BSC, TMU, ETMU et KEYSC. Mais l'appel de getkey invoque une instruction que je n'avais pas encore rencontrée ni implémentée jusqu'ici : SLEEP. La trace de cette instruction est getkey() -> getkey_opt() -> keydev_read() -> sleep() -> __asm__("sleep") (src/keysc/keydev.c#L272).

Je me suis documenté sur ce que fait sleep, et selon le manuel, cela met en pause l'exécution des instructions du CPU. Seule une interruption peut sortir le CPU de cet état. Les revoilà ! Cette fois-ci, je me suis un peu plus documenté sur la gestion des exceptions et interruptions.

Lorsqu'une interruption apparaît, le CPU va arrêter l'exécution du code dès que possible pour gérer l'interruption. Il y a plusieurs types d'exceptions et d'interruptions, mais il me semble que je suis ici uniquement intéressé par les "general interrupt request" qui peuvent être émises par les composants connectés au MMU. Voici la procédure à suivre dans ce cas en se basant sur la doc du CPU SuperH :



INTEVT est un registre dans lequel va être stocké le code de l'interruption. La toute dernière ligne place PC à l'adresse VBR + 0x600 où se trouve du code qui correspond à l "interrupt handling routine" pour réagir à l'interruption.
Lors de son initialisation, gint va changer l'adresse du VBR afin de pointer vers des interrupt handlers customisés définis ici : src/kernel/inth.S#L65.

Ainsi, j'ai ajouté deux flags à la structure de mon CPU :

uint8_t interrupt_pending;
uint8_t sleep_mode;

À la lecture d'une instruction sleep, le flag sleep_mode est mis à 1, ce qui empêche le CPU d'exécuter les prochaines instructions, mais il peut toujours réagir aux interruptions.

Maintenant, il faut lever une interruption pour sortir du mode sleep et continuer le programme. Dans la doc du MMU SH7724, il y a un périphérique nommé KEYSC pour Key Scan qui lève une interruption lors de l'appui sur une touche (code : 0xBE0). Étant donné que le sleep intervient lors d'un getkey, et que le fichier s'appelle keysc.c, je me suis dit que je pouvais tenter d'utiliser ce code pour créer une exception personnalisée :

#define INTEVT_REGISTER 0xff000028

void handle_interrupt(cpu_t* cpu) {
    cpu->interrupt_pending = 0;
    cpu->sleep_mode = 0;

    cpu->ssr = cpu->sr;
    cpu->spc = cpu->pc;
    cpu->sgr = cpu->r[15];

    // KEYSC interrupt code: 0xBE0
    mem_write(cpu, INTEVT_REGISTER, 0xBE0, 4);

    MD = 1;
    RB = 1;
    BL = 1;
    // if (cond) IMASK = level_of_accepted_interrupt() // TODO

    cpu->pc = cpu->vbr + 0x600;
}

Lors de l'exécution de ce code, le CPU sort bien du mode sleep et saute bien au VBR custom de gint. Seulement dès le premier jump (bsrf), PC se retrouve sur une instruction invalide.

Après avoir cherché un peu plus sur le site, je suis tombé sur ce commentaire :

Lephenixnoir a écrit :
L'interruption de code be0 (de mémoire) se produit quand on appuie sur une touche, mais essentiellement tu ne peux pas détecter le relâchement à moins d'avoir un timer. Donc c'est un peu inutile, personnellement je la désactive.
.
Il semble que cette interruption soit désactivée par gint, il faut donc chercher ailleurs.

J'ai essayé d'analyser ce qu'il se passait sur l'émulateur officiel du SDK lors de l'arrivée à cette instruction sleep. J'ai pu détecter un saut vers VBR + 0x600, et trouver que le code de l'interruption était en fait 0xF00. Malheureusement, ce code n'a pas marché non plus. J'ai donc fouillé un peu plus les sources de gint et j'ai trouvé ce commentaire : (src/intc/intc.c#L86) :

gint/intc.c a écrit :
[...] 0xf00 (the SH3 event code for ETMU0 underflow) is [...] where 0x9e0 (the SH4 event code for the same event)
.
L'émulateur casio étant SH3, 0xF00 n'était pas le bon code pour l'interruption. Son équivalent SH4 est 0x9E0.

Avec ce nouveau code, l'exécution du programme a pu continuer sans arriver sur une instruction invalide!

Mais ce fut de courte durée : toujours dans le code de l'interrupt handler, dans la routine _gint_inth_callback_reloc, lors de l'arrivée à l'instruction mov.l @(4, r0), r4 (src/kernel/inth.S#L250), r0 vaut 0x2 ce qui cause une lecture à une adresse invalide.

À noter que pour arriver jusqu'à cette instruction, j'ai dû ajouter 8 registres à mon CPU, R0_BANK-R7_BANK qui sont des registres séparés de R0-R15, et implémenter les instructions relatives à ces registres.

J'aimerais en apprendre plus sur ce qu'il se passe lors de l'appel à la fonction getkey de gint. Elle attend une interruption, mais laquelle ?
Qu'est-ce qui manque actuellement pour pouvoir faire fonctionner getkey, est-ce que j'ai fait une erreur qui pourrait expliquer le comportement incorrect dans l'interrupt handler ?
Est-ce qu'il faut savoir quelque chose en particulier concernant les registres R_BANK?

Toute piste pouvant m’éclairer sur le lien entre le keyboard et les interruptions dans gint est la bienvenue!
Lephenixnoir Hors ligne Administrateur Points: 24657 Défis: 170 Message

Citer : Posté le 05/05/2024 22:37 | #


J'ai choisi la Graph 75+E SH4 car c'est celle qui semble avoir le moins de limites en termes de mémoire disponible.

Ok pour ça. Note juste que le système de fichiers est plus gros et un peu plus flexible sur la 35+E II donc certains add-ins seront peut-être pas au top de leurs fonctionnalités. Mais c'est un problème pour plus tard.

La première chose que fait gint est de détecter le MPU de la calculatrice (src/kernel/hardware.c#L32). Pour me conformer au modèle que j'ai choisi, je retourne 0x10300b00 lorsque gint accède au PVR et 0x00002c00 lorsque gint accède au PRR, ce qui flag le MPU comme étant HWMPU_SH7305.

Parfait.

Ensuite, gint essaie de détecter le modèle de l'OS à l'adresse 0x80010021 (hardware.c#L81). Il semble que '3' désigne les Graph 35+EII qui utilisent le nouveau moteur de rendu HWFS_FUGUE, et toutes les autres valeurs utilisent HWFS_CASIOWIN, ce qui est mon cas car je retourne '2' (c'est correct pour la 75+E ?).

C'est ça, l'OS 3.xx c'est la Graph 35+E II, donc il faut renvoyer un OS 2. Je te conseille de charger dans la mémoire de l'émulateur un numéro de version complet, du type "02.05.0000", c'est pas super rare que les add-ins ou que gint lui-même aille lire le numéro complet (e.g. pour s'identifier par USB). De la même façon, à 0x8000ffd0 tu as un numéro de série de 8 octets pour lequel tu peux mettre 8*0xff.

Après ça, gint va essayer de détecter si la calculatrice dispose d'une extension de RAM en changeant la valeur à l'adresse 0x88040000 et en regardant si cela affecte la valeur à l'adresse 0x88000000 (hardware.c#L92). Sur mon émulateur, ces deux adresses sont bien différentes, donc gint détecte une extension de RAM.

Impeccable.

Finalement, gint va effectuer un scan aux adresses 0xf6000000 et 0xf7000000 afin de récupérer l'adresse physique de la RAM je crois (src/mmu/mmu.c#L133). Ces adresses serviront aussi à calculer la taille de la RAM. J'ai pu trouver une explication très détaillée de Lephe qui répondait à Inikiwi face au même problème un an plus tôt, ce qui m'a été très utile pour émuler le comportement a ces adresses.

Très bien. Ça ne cherche pas l'adresse physique de "la RAM" dans sa totalité, ça cherche en fait l'adresse physique associée à une page de RAM spécifique (celle dont l'adresse virtuelle est 0x08100000).

De ce que j'ai pu comprendre du code, gint prend le contrôle (presque) total de l'OS en définissant plusieurs drivers qui vont chacun agir sur des composants spécifiques connectés au MMU. Au lancement de gint, chaque driver sauvegarde l'état des registres importants pour chaque composant, afin de pouvoir les restaurer à l'identique à la sortie du programme. Ces registres ont tous une adresse spécifique qui peut être trouvée dans la documentation du MMU SH7724.

Correct. (Techniquement gint prend le contrôle du matériel pas de l'OS, et fait aussi d'autres trucs que les drivers, mais toi y'a que les drivers dont tu dois te soucier.)

Et c'est là que les choses se compliquent. J'ai dû ajouter les zones mémoires pour les drivers CPG, BSC, TMU, ETMU et KEYSC.

Quand tu dis zone mémoire tu t'assures bien d'émuler le comportement des modules aussi ?

Mais l'appel de getkey invoque une instruction que je n'avais pas encore rencontrée ni implémentée jusqu'ici : SLEEP. (...) Lorsqu'une interruption apparaît, le CPU va arrêter l'exécution du code dès que possible pour gérer l'interruption. Il y a plusieurs types d'exceptions et d'interruptions, mais il me semble que je suis ici uniquement intéressé par les "general interrupt request" qui peuvent être émises par les composants connectés au MMU.

Pour référence, la procédure est exactement la même pour les exceptions et TLB miss, à part l'offset (VBR+0x100, 0x400, 0x600 selon le type). Les exceptions t'auras aucun mal à les gérer parce que c'est juste les cas d'erreur (e.g. accès mémoire non aligné). Les TLB miss c'est pareil mais elles n'apparaissent que si tu émules le MMU en détail, ce que tu n'es pas obligé de faire. Les interruptions sont la partie chiante parce qu'elles sont asynchrones donc elles vont apparaître quand tu vas commencer à émuler le comportement du matériel en parallèle du processeur.

Ainsi, j'ai ajouté deux flags à la structure de mon CPU :

uint8_t interrupt_pending;
uint8_t sleep_mode;

À la lecture d'une instruction sleep, le flag sleep_mode est mis à 1, ce qui empêche le CPU d'exécuter les prochaines instructions, mais il peut toujours réagir aux interruptions.

Oui pas mal. L'idée du "interrupt pending" est extrêmement correcte mais il te faut un flag pour chaque source d'interruption, du style "keyboard interrupt pending", "timer 0 interrupt pending", etc.

Maintenant, il faut lever une interruption pour sortir du mode sleep et continuer le programme. Dans la doc du MMU SH7724, il y a un périphérique nommé KEYSC pour Key Scan qui lève une interruption lors de l'appui sur une touche (code : 0xBE0).

Note que dans gint, de façon contre-intuititive (et sans doute pas idéale), l'usage du clavier n'est pas détecté par l'interruption clavier. gint assigne une priorité 0 ou un masque sur cette interruption (donc IPR ou IMR, je sais plus lequel) ce qui fait que l'interruption n'est jamais acceptée. Donc même si tu l'émules ça n'aidera pas gint, qui utilise un timer à 128 Hz.

Étant donné que le sleep intervient lors d'un getkey, et que le fichier s'appelle keysc.c, je me suis dit que je pouvais tenter d'utiliser ce code pour créer une exception personnalisée :

#define INTEVT_REGISTER 0xff000028

void handle_interrupt(cpu_t* cpu) {
    cpu->interrupt_pending = 0;
    cpu->sleep_mode = 0;

    cpu->ssr = cpu->sr;
    cpu->spc = cpu->pc;
    cpu->sgr = cpu->r[15];

    // KEYSC interrupt code: 0xBE0
    mem_write(cpu, INTEVT_REGISTER, 0xBE0, 4);

    MD = 1;
    RB = 1;
    BL = 1;
    // if (cond) IMASK = level_of_accepted_interrupt() // TODO

    cpu->pc = cpu->vbr + 0x600;
}

Looks good.

Lors de l'exécution de ce code, le CPU sort bien du mode sleep et saute bien au VBR custom de gint. Seulement dès le premier jump (bsrf), PC se retrouve sur une instruction invalide.

En effet parce que la façon dont les interruptions sont gérées est très optimisée, ça fait essentiellement PC += INTEVT en utilisant le fait que les valeurs de INTEVT sont des multiples de 32, ce qui laisse 32 octets de code pour gérer chaque interruption ou appeler un handler plus compliqué. Ici, comme le code 0xbe0 n'est pas géré ça saute dans le vide. Donc c'est pas un problème avec ton sleep ni ta gestion de base des 'interruptions, c'est que tu as déclenché une interruption clavier alors que le programme avait désactivé cette source. Il faudra que tu prennes en compte les IPR/IMR pour savoir quelle interruption laisser passer ou pas.

gint/intc.c a écrit :
[...] 0xf00 (the SH3 event code for ETMU0 underflow) is [...] where 0x9e0 (the SH4 event code for the same event)
.
L'émulateur casio étant SH3, 0xF00 n'était pas le bon code pour l'interruption. Son équivalent SH4 est 0x9E0.

Avec ce nouveau code, l'exécution du programme a pu continuer sans arriver sur une instruction invalide!

En effet le timer du clavier sous gint est ETMU0. Mais note que ce n'est pas lié au clavier à proprement parler : gint configurer ETMU0 pour avoir 128 interruptions par seconde, donc ton travail à toi c'est plutôt de lire les registres du TMU et de configurer un signal régulier de ton côté (les timers haute précision c'est chiant à faire sur un ordi je sais pas ce que ça donnerait, mais bon tu as le temps de voir). La gestion du clavier à proprement parler avec l'interruption 0xbe0 est toujours une fonctionnalité utile pour ton émulateur, mais aucune version de gint ne s'en sert (tu en aurais besoin, par exemple, pour Vhex).

Mais ce fut de courte durée : toujours dans le code de l'interrupt handler, dans la routine _gint_inth_callback_reloc, lors de l'arrivée à l'instruction mov.l @(4, r0), r4 (src/kernel/inth.S#L250), r0 vaut 0x2 ce qui cause une lecture à une adresse invalide.

C'est bien t'es à fond dans le code subtil ! Y'a pas beaucoup de code qui soit plus compliqué dans gint que gint_inth_callback() en termes de subtilités du processeur.

Le code que tu as sous les yeux exécute un gint_call_t (une structure composée d'un pointeur de fonction + 4 arguments). Le mov.l @(4, r0), r4 qui plante est le bout qui charge le premier argument dans r4. Puisqu'il plante c'est que r0 ne doit pas avoir la bonne valeur. Sa valeur vient du ldc r4, r0_bank un peu au-dessus (on change de banque au ldc r1, sr).

Mon premier soupçon serait : est-ce que tu as bien vu que rN et rN_bank sont échangés selon la valeur de SR.RB ?

J'aimerais en apprendre plus sur ce qu'il se passe lors de l'appel à la fonction getkey de gint. Elle attend une interruption, mais laquelle ?

N'importe laquelle. sleep() se fait réveiller quand il y a une interruption, et getkey() se rendort après ça sauf si les fonctions clavier (genre pollevent()) donnent des trucs nouveaux. Donc essentiellement getkey() attend n'importe quelle interruption qui fait bouger le clavier, et en l'occurrence vu comment gint est codé c'est ETMU0. La raison pour laquelle on appelle sleep est pour économiser la batterie pendant que le programme ne fait rien.

Qu'est-ce qui manque actuellement pour pouvoir faire fonctionner getkey, est-ce que j'ai fait une erreur qui pourrait expliquer le comportement incorrect dans l'interrupt handler ?

On peut commencer par la gestion des deux banques de registres, comme tu l'as deviné. Sinon tu m'as l'air bien parti. Mais je note un peu de dette technique sur deux points : (1) tu dois consulter les registers IPR et IMR de l'INTC pour savoir quelles interruptions donner ou pas, et (2) tu délivres actuellement une interruption timer sur la base de ce qui se passe au clavier, ce qu'il faudra corriger à un moment.
Mon graphe (11 Avril): ((Rogue Life || HH2) ; PythonExtra ; serial gint ; Boson X ; passe gint 3 ; ...) || (shoutbox v5 ; v5)
Drakalex007 Hors ligne Membre Points: 688 Défis: 0 Message

Citer : Posté le 06/05/2024 15:59 | #


Merci pour ces éclaircissements !

Content de savoir que mon intégration de gint est bien partie.

Lephenixnoir a écrit :
Quand tu dis zone mémoire tu t'assures bien d'émuler le comportement des modules aussi ?

Non, pour l'instant je parle uniquement de définir les zones en mémoire correspondant à ces registres pour ne pas avoir de memory out of bound. J'ai conscience qu'il faudra également émuler le comportement asynchrone des composants correspondants, je le ferai au fur et à mesure !

Lephenixnoir a écrit :
Mon premier soupçon serait : est-ce que tu as bien vu que rN et rN_bank sont échangés selon la valeur de SR.RB ?

Ah tiens non je n'avais pas vu ! Il y a de grande chances que ce soit ça le problème, je vais rajouter ce comportement merci.

Les prochaines étapes pour faire fonctionner getkey semblent donc être la gestion du timer ETMU et la lecture des registres IPR/IMR.

Je ferai une update sur mes avancées

LienAjouter une imageAjouter une vidéoAjouter un lien vers un profilAjouter du codeCiterAjouter un spoiler(texte affichable/masquable par un clic)Ajouter une barre de progressionItaliqueGrasSoulignéAfficher du texte barréCentréJustifiéPlus petitPlus grandPlus de smileys !
Cliquez pour épingler Cliquez pour détacher Cliquez pour fermer
Alignement de l'image: Redimensionnement de l'image (en pixel):
Afficher la liste des membres
:bow: :cool: :good: :love: ^^
:omg: :fusil: :aie: :argh: :mdr:
:boulet2: :thx: :champ: :whistle: :bounce:
valider
 :)  ;)  :D  :p
 :lol:  8)  :(  :@
 0_0  :oops:  :grr:  :E
 :O  :sry:  :mmm:  :waza:
 :'(  :here:  ^^  >:)

Σ π θ ± α β γ δ Δ σ λ
Veuillez donner la réponse en chiffre
Vous devez activer le Javascript dans votre navigateur pour pouvoir valider ce formulaire.

Si vous n'avez pas volontairement désactivé cette fonctionnalité de votre navigateur, il s'agit probablement d'un bug : contactez l'équipe de Planète Casio.

Planète Casio v4.3 © créé par Neuronix et Muelsaco 2004 - 2024 | Il y a 51 connectés | Nous contacter | Qui sommes-nous ? | Licences et remerciements

Planète Casio est un site communautaire non affilié à Casio. Toute reproduction de Planète Casio, même partielle, est interdite.
Les programmes et autres publications présentes sur Planète Casio restent la propriété de leurs auteurs et peuvent être soumis à des licences ou copyrights.
CASIO est une marque déposée par CASIO Computer Co., Ltd