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


1, 2 Suivante
Lephenixnoir Hors ligne Administrateur Points: 24574 Défis: 170 Message

Citer : Posté le 08/11/2023 01:26 | #


La partie que tu es en train de découvrir c'est tout l'arrangement de la mémoire. Dans ton code tu as mis rom dans cpu_status_t, mais en vrai la mémoire n'est pas du tout dans le processeur, c'est un autre des (nombreux) composants matériels dont tu dois reproduire le comportement dans ta quête d'un émulateur.

Comment, donc, est-ce que les données de l'add-in sont chargées en mémoire au lancement ?

Le fichier g1a est chargé à l'adresse 0x00300000, genre copié-collé, et l'exécution commence donc en initialisant PC à l'adresse 0x00300200 pour sauter l'en-tête. (Note : sur Graph 90+E l'en-tête n'est pas copié donc le code commence direct à 0x00300000.)

En plus de ça, une zone de RAM est réservée à l'adresse 0x08100000. Sa taille dépend des modèles ; les détails sont documentés sur cette page de la bible.

Il y a d'autres bouts de mémoire que les add-ins utilisent plus rarement, mais comme gint s'en sert, si tu veux lancer un add-in gint un jour il faudra que tu les aies aussi. Sur SH4, la liste complète des zones mémoire est documentée sur cette page. Tu auras besoin d'émuler la XRAM, la YRAM et l'ILRAM. Il n'y a pas de difficulté particulière, il faut juste que tu crées des buffers de 8/8/4 kio et que tu les fasses correspondre aux adresses 0xe5007000, 0xe5017000 et 0xe5200000.

Il n'y a pas trop de subtilités pour toi sinon parce que le programme lui-même se charge d'initialiser la RAM (ce dont je discutais sur la shout avec Potter tout à l'heure).

Note que la plupart du travail d'émulation de la mémoire va donc se faire dans les instructions qui accèdent à la mémoire ; tu devrais regarder l'adresse utilisée, tester si elle est dans le voisinage de 0x00300000, ou de 0x08100000, ou des adresses de la XRAM/YRAM/ILRAM, et à chaque fois aller lire dans le buffer qui va bien.

Je remarque d'ailleurs un bug fourbe qui est que ton cpu->rom[x] c'est un entier de 8 bits alors que dans la plupart des instructions (comme celle dont tu donnes le code ci-dessus) tu dois lire 32 bits. Il faut que tu modifies cet accès. Et note bien que la calculatrice est en big-endian, alors que le PC sur lequel tu exécutes ça (ARM/x86) est très probablement en little-endian.
Mon graphe (11 Avril): ((Rogue Life || HH2) ; PythonExtra ; serial gint ; Boson X ; passe gint 3 ; ...) || (shoutbox v5 ; v5)
Inikiwi Hors ligne Membre Points: 594 Défis: 8 Message

Citer : Posté le 08/11/2023 09:00 | #


cpu_status_t est la première structure que j'ai implémenté dans mon émulateur, au final cette structure contient tout les variables utilisés par l'émulateur (registres, mémoires, variables internes de l'émulateur) je devrais plutôt le renommer en nemu_status_t, de plus ma gestion mémoire est pas bonne, je devrais faire un système ou on alloué des blocks mémoire dans le bus d'adresse. J'irai dans les détails quand je serais libre.
Validuser Hors ligne Membre Points: 508 Défis: 1 Message

Citer : Posté le 08/11/2023 12:22 | #


Dîtes quand on à le code source de la calculatrice c'est plus facile de faire un émulateur ? Juste une question que je me suis posée.
Votre développeur favori
Je suis en train de travailler sur TD'PC : un Tower Defense sur G35+EII (Bien sûr que oui je travaille dessus )
Ne pas cliquer
Mtn que tu à cliqué tu est obligé de tout installer
Fcalva Hors ligne Membre Points: 600 Défis: 10 Message

Citer : Posté le 08/11/2023 12:27 | #


Pas tant que ça je pense.
Enfin avec tout ce qu'on a avec les désassemblages, si on partait de 0 ça serait d'une très grande aide effectivement.
Pc master race - Apréciateur de Noctua moyen
Caltos : G35+EII, G90+E (briquée )
Drakalex007 Hors ligne Membre Points: 688 Défis: 0 Message

Citer : Posté le 08/11/2023 13:47 | # | Fichier joint


@Lephenixnoir : Merci pour ta réponse et ces infos très utiles !

Cette formulation est plus pertinente du coup:
Drakalex007 a écrit :
Et pour cela il va falloir émuler les fonctionnalités du CPU. de la calculatrice (dont le CPU).


Il y a un truc que je n'arrive pas à comprendre : d'après la doc la mémoire de la calculatrice est de 4MiB (entre autres), soit 4 * 1024 * 1024 = 4,194,304 bytes.
Cependant, la RAM est située à l'adresse 0x08100000, ce qui représente un offset de 135,266,304 bytes, bien plus que la capacité de la mémoire !
Où est-ce que je me trompe ?

Concernant l'endian, j'ai vu dans la doc qu'il s'agissait de la disposition des bytes (ou words) en mémoire. Ce passage semble indiquer qu'on peut le changer lors d'un reset ?

Et finalement, je ne suis pas sûr de ce que cela implique pour mon code. Il s'agit uniquement de positionner les bytes de gauche à droite ou inversement dans chaque longword dans les différents buffers qui émulent la mémoire?

@Inikiwi : Dans ton émulateur lorsque tu charges le fichier .g1a, tu copies directement le code de l'add-in dans la ROM en excluant le header. Pourtant, tu initialises quand même PC à 0x00300200 avec un offset de 0x200 pour passer le header. Comment ça se fait ?

    fseek(addin_file, 0L, SEEK_END);
    status->program_size = ftell(addin_file) - 0x200L;

    status->rom = malloc(status->program_size);
    fseek(addin_file, 0x200L,SEEK_SET);
    fread(status->rom, status->program_size, 1, addin_file);

    fclose(addin_file);    

    printf("%d bytes allocated\n",status->program_size);

    status->pc = 0x00300200;

Slyvtt Hors ligne Maître du Puzzle Points: 2389 Défis: 17 Message

Citer : Posté le 08/11/2023 14:29 | #


Sur les Monochrome, le code "utile" commence à 0x00300200. Tu as donc 2 manières de faire :
1/ tu copies tout comme un bourrin (header + code "utile") à partir de l'adresse 0x00300000 et comme le header fait 0x200 tu es OK (note que les 0x200 bytes de header ne servent absolument à rien et ne seront pas utilisés)
2/ tu copie seulement le code "utile" à partir de 0x00300200 en prenant soin de retirer les 0x200 premiers bytes de ton g1a

PC doit toujours être mis à 0x00300200 car c'est là que le code commence.


Pour la G90, il n'y a qu'une seule manière de faire en partant du g3a : virer le header et copier le code "utile" à partir de 0x00300000 en mettant bien le PC à cette valeur.
There are only 10 types of people in the world: Those who understand binary, and those who don't ...
Inikiwi Hors ligne Membre Points: 594 Défis: 8 Message

Citer : Posté le 08/11/2023 14:48 | #


uint32_t cpu_read32(cpu_status_t* status, uint32_t addr){
    if(addr >=0x08100000 && addr <= 0x08100000+524288){
        uint32_t ret;
        ret = decode(status->ram[addr-0x08100000+3], status->ram[addr-0x08100000+2], status->ram[addr-0x08100000+1], status->ram[addr-0x08100000]);
        return ret;
    }
    else if(addr >=0x00300200 && addr <= 0x00300200+status->program_size){
        uint32_t ret;
        ret = decode(status->rom[addr-0x00300200+3], status->rom[addr-0x00300200+2], status->rom[addr-0x00300200+1], status->rom[addr-0x00300200]);
        return ret;
    }
    else{
        log_mem_read_error(status, addr);
        return 0;
    }
}


si tu lis bien, tu peux remarquer que le code de l'add-in est mappé à 0x00300200 car je saute l'header.
Lephenixnoir Hors ligne Administrateur Points: 24574 Défis: 170 Message

Citer : Posté le 08/11/2023 15:34 | #


Pour ce qui est de la valeur 0x08100000 qui paraît excessive, c'est parce que les adresses mémoire ne vont pas de 0 à la taille de la RAM. Non seulement il y a plein d'autres types de mémoire qui ont toutes besoin d'avoir des adresses aussi, mais en plus l'espace complet (qui pourrait couvrir 4 Gio vu qu'il y a 32 bits dans chaque adresse) est découpé en morceaux d'une façon assez riche. J'ai documenté tous ces détails dans la technique suivante : https://www.planet-casio.com/Fr/forums/topic16807-2-casm-optimiser-au-cycle-pres-la-reference.html#184913

Pour l'endianness, le SuperH est en effet ce qu'on dit "bi-endian", on peut le choisir au démarrage. Dans la calculatrice le paramètre est fixé à big-endian donc tu peux (dois) traiter la calculatrice comme un système big-endian.

Ce que ça change c'est que dans ton buffer uint8_t *rom par exemple, si tu veux lire une valeur de 16 bits comme une instruction il faut faire attention. Tu peux écrire par exemple

uint16_t instruction = (rom[i] << 8) | rom[i+1];

mais si tu écris

uint16_t instruction = *(uint16_t *)&rom[i];

ie. si tu essaies de faire une lecture 16 bits sur le PC, l'endianness étant pas la même, tu récupéreras pas la bonne valeur.

Pour rajouter sur la question du 0x00300200, tu n'as pas le choix de la valeur initiale de PC : c'est 0x00300200 parce que le programme est compilé comme ça. Ce qu'il faut bien savoir c'est que le code est sensible à sa position en mémoire. Si tu déplaces le code à 0x00300000 et que tu initialises PC à 0x00300000 le programme va pas marcher. Il doit être chargé précisément à 0x00300200.

Personnellement je trouve qu'il vaut mieux charger le header. Sur la calculatrice il est là, donc sur l'émulateur il devrait être là aussi. C'est 512 octets, ie. complètement négligeable, donc y'a pas de raison de pas être fidèle à la calto.
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 09/11/2023 16:37 | #


D'accord, je comprends mieux, merci

@Lephenixnoir: Je vois pour l'adressage, d'ailleurs c'est une mine d'or le topic que tu as envoyé pour s'initier à la programmation bas niveau et au superH !

Pour l'endianness, je ne suis pas familier avec la deuxième écriture. J'aurai plutôt eu tendance à avoir écrit ça :

uint16_t instruction = (uint16_t)rom[i];

Qu'est-ce que cette syntaxe change ?

Concernant la mémoire, suite à tes précédentes explications, voici sa structure actuelle (qui ne comporte pas encore les adresses utilisées par gint) :

#define ROM_START   0x00300000
#define RAM_START   0x08100000
#define XRAM_START  0xe5007000
#define YRAM_START  0xe5017000
#define ILRAM_START 0xe5200000

#define RAM_SIZE    512 * 1024
#define XRAM_SIZE     8 * 1024
#define YRAM_SIZE     8 * 1024
#define ILRAM_SIZE    4 * 1024

typedef struct {
    uint8_t* rom;
    uint8_t ram[RAM_SIZE];
    uint8_t xram[XRAM_SIZE];
    uint8_t yram[YRAM_SIZE];
    uint8_t ilram[ILRAM_SIZE];

} memory_t;

J'ai ajouté un pointeur vers cette mémoire dans le CPU pour simplifier le code, bien qu'elle soit externe au CPU, et j'ai également créé deux fonctions pour accéder à la mémoire (les bytes sont assemblés en big-endian) : mem_read et mem_write, qui prennent en argument le nombre de bytes à traiter (1=byte, 2=word, 4=long) et s'occupent de remapper l'adresse vers le bon buffer.

uint32_t mem_read(cpu_t* cpu, uint32_t address, uint8_t bytes);
void    mem_write(cpu_t* cpu, uint32_t address, uint32_t data, uint8_t bytes);

Je changerai probablement ça plus tard lorsque je me rendrai compte que ça complique certaines choses (il vaudrait probablement mieux faire une fonction read et write pour chaque type?), mais ça fait moins de code et c'est plus simple à comprendre pour l'instant.

Au fait, la plus grande adresse valide de la ROM est-elle directement définie par la taille de l'addin ? Si l'adresse dépasse 0x00300000 + addin_size, c'est invalide ?

Avec ce système en place, j'ai pu avancer dans l'émulation du sample add-in. Premièrement, j'ai commencé à utiliser le SDK Casio pour pouvoir analyser le code désassemblé instruction par instruction ainsi que les registres de la calto.
Le seul problème, c'est lorsque j'arrive sur des instructions de type JMP ou BSR. Quand j'essaie de passer à l'instruction suivante (enfin celle d'après), le debugger ne breakpoint pas dans la branche correspondante, mais après son exécution. Est-ce possible de contourner ça ?
Aussi, est-ce qu'il existe des outils alternatifs/plus pratiques pour analyser le code instruction par instruction avec l'état des registres et de la mémoire ?

J'ai aussi remarqué que mon code crashait avec un "memory out of bounds" après quelques instructions, et effectivement, la valeur de R15 (initialisée à 0) était utilisée comme adresse pour écrire sur la mémoire.
Après avoir fait un tour sur le repo d'Inikiwi, j'ai vu que R15 était le seul registre qui avait été assigné une valeur lors de l'initialisation. Cette valeur semble représenter l'adresse de la fin de la RAM.
cpu->r[15] = RAM_START + RAM_SIZE;
(Edit: la valeur de r[15] sur l'émulateur est 0x88023F98, ce qui extrêmement éloigné de 0x08100000, donc je suis pas sur de ce calcul)

J'aimerais bien en savoir plus sur ce cas particulier (c'est le seul registre avec PC qui semble initialisé à autre chose que 0), et s'il y a d'autres "états" du CPU ou de la mémoire à prendre en compte lors de l'initialisation ?
Drakalex007 Hors ligne Membre Points: 688 Défis: 0 Message

Citer : Posté le 09/11/2023 18:26 | #


Typiquement, quatre instructions après le lancement du programme j'arrive à une instruction sts.l pr, @-r15.
Cette instruction écrit la valeur de PR à l'adresse contenue dans R15 (-4).

Mon code initialise PR à 0, donc cette instruction écrit 0 dans la RAM.
Mais dans l'émulateur Casio, PR contient une valeur non nulle au lancement de l'add-in, donc la valeur écrite est différente. (Et l'adresse également comme découvert plus haut).

Comment devrais-je initialiser PR ?
Lephenixnoir Hors ligne Administrateur Points: 24574 Défis: 170 Message

Citer : Posté le 09/11/2023 18:34 | #


Cool que le topic t'ai été utile. Concernant la ligne que j'ai citée :

uint16_t instruction = *(uint16_t *)&rom[i];

Il faut garder en tête que rom est un tableau de uint8_t, ie. d'octets individuels. Si tu veux lire une instruction il faut donc lire deux éléments de ce tableau d'un coup. Le cast que tu proposes ne fait que lire un élément et le stocker sur une valeur de 16 bits, ce qui n'accomplit pas cet objectif.

Les programmeurs C généralement voient ce problème et se disent "facile". Puisque je dois faire une lecture de 16 bits depuis le tableau rom, il suffit de cast le pointeur uint8_t * en uint16_t * comme ça la lecture me donnera 16 bits.

uint8_t *rom8 = &rom[i];
// *rom8 c'est UN octet à la position i de la ROM
uint16_t *rom16 = (uint16_t *)rom8;
// *rom16 c'est DEUX octets à la position i de la ROM
uint16_t instruction = *rom16;
// Et hop j'ai lu rom[i] et rom[i+1] d'un seul coup !

Le code au début de ce post est juste une version courte de cette séquence. Le commentaire est correct, on a bien lu rom[i] et rom[i+1] d'un coup, sauf que selon l'endianness ça peut être (rom[i] << 8) | rom[i+1] (big-endian, comme la calto, ce que tu veux) ou (rom[i+1] << 8) | rom[i] (little-endian, ce que tu récupères sur le PC).

Mais bon si cette technique ne t'était pas venue à l'esprit alors pas la peine de te tracasser avec.

(Je réponds au reste dans d'autres messages)
Mon graphe (11 Avril): ((Rogue Life || HH2) ; PythonExtra ; serial gint ; Boson X ; passe gint 3 ; ...) || (shoutbox v5 ; v5)
Lephenixnoir Hors ligne Administrateur Points: 24574 Défis: 170 Message

Citer : Posté le 09/11/2023 18:47 | #


Très bien ta gestion de la mémoire. Juste pour t'embêter, la ROM et la RAM sont en-dehors du MPU mais la XRAM, YRAM et ILRAM sont dessus. :P

Dans tes macros *_SIZE pense à mettre des parenthèses :

#define XRAM_SIZE     (8 * 1024)

sinon tu peux avoir des problèmes avec les priorités opératoires, eg.

int offset = address % XRAM_SIZE;
// devient address % 8 * 1024
// ce qui est lu (address % 8) * 1024

De façon générale si la valeur d'une macro n'est pas juste un nombre on met toujours des parenthèses autour de (1) la valeur complète de la macro, (2) chaque apparition d'un argument de macro dans la valeur, pour éviter spécifiquement ce problème.

Au fait, la plus grande adresse valide de la ROM est-elle directement définie par la taille de l'addin ? Si l'adresse dépasse 0x00300000 + addin_size, c'est invalide ?

En gros oui. En pratique c'est arrondi à 4 kio mais tu peux dire que c'est exactement la taille de l'add-in.

Le seul problème, c'est lorsque j'arrive sur des instructions de type JMP ou BSR. Quand j'essaie de passer à l'instruction suivante (enfin celle d'après), le debugger ne breakpoint pas dans la branche correspondante, mais après son exécution. Est-ce possible de contourner ça ?

La question n'est pas assez précise que pour j'arrive à déterminer si tu es au courant que le delay slot existe ou si t'es en train de le découvrir.

Aussi, est-ce qu'il existe des outils alternatifs/plus pratiques pour analyser le code instruction par instruction avec l'état des registres et de la mémoire ?

Si ton programme est assez "pur" (ie. pas un truc spécifique à la calto) le combo qemu + remote gdb est très agréable. Je peux te montrer éventuellement. Mais si t'as pas trop peur tu peux y aller un peu à l'aveugle.

J'ai aussi remarqué que mon code crashait avec un "memory out of bounds" après quelques instructions, et effectivement, la valeur de R15 (initialisée à 0) était utilisée comme adresse pour écrire sur la mémoire.
Après avoir fait un tour sur le repo d'Inikiwi, j'ai vu que R15 était le seul registre qui avait été assigné une valeur lors de l'initialisation. Cette valeur semble représenter l'adresse de la fin de la RAM.

r15 est le pointeur de pile. Il faut l'initialiser avec une valeur appropriée. Tu rentres dans des subtilités pour la raison suivante. La zone à l'adresse 0x08100000 est contrôlée par le MMU, comme tu as pu le lire sur l'autre topic. Ça veut dire que c'est un alias pour une autre adresse physique, et en l'occurrence c'est un alias de 0x8801c000 avant la Graph 35+E II et de 0x88048000 sur la Graph 35+E II.

Si tu vas sur la calto, tu constateras que la zone virtuelle à 0x08100000 fait 32 kio, et en fait le pointeur de pile est à la fin de ces 32 kio (rappel que le pointeur de pile il "recule" durant l'exécution, sa valeur initiale est la plus grande). Mais la valeur qui lui est donnée est la version physique de cette adresse (0x880#####) au lieu d'être la version virtuelle (0x08108000). Si tu veux être à peu près réalise, il faut donc que tu réserves une zone physique à l'adresse eg. 0x8801c000 comme la Graph 75 et co., et que tu en fasses un alias de 0x08100000.

Comment devrais-je initialiser PR ?

Les add-ins sur la calto ont un fonctionnement un peu inhabituel comparé à des processus sur un ordinateur. Ils sont appelés comme des fonctions. Quand la fonction principale de l'add-in return, l'add-in s'arrête. La valeur initiale de pr, en pratique, c'est une adresse quelque part dans le code de l'OS qui se charge de lancer les add-ins. Quand l'add-in return et que l'exécution reprend à cet adresse, l'OS conclut que l'add-in s'est terminé.

Dans ton émulateur, il faut que tu détectes un saut au pr initial comme marquant l'arrêt de l'add-in. Il faut donc que tu mettes une valeur spéciale dans pr, pour que quand pc est égal à ça tu en déduis que l'add-in s'est arrêté. Je te déconseille de mettre une valeur comme 0 parce que l'add-in pourrait sauter à l'adresse 0 par accident. Je mettrais plutôt une valeur invalide pour du code comme 0xfedcba91 on un autre nombre aléatoire. (Tu peux aussi envisager de rendre la détection de la fin de l'add-in plus robuste à l'aide du pointeur de pile, mais osef là tout de suite.)
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 09/11/2023 20:18 | #


Ah oui, compris pour le cast de pointeur, merci pour l'explication. Effectivement, je fais de l'assemblage de bytes et non pas du casting pour que ce soit big endian !

Lephenixnoir a écrit :
La question n'est pas assez précise que pour j'arrive à déterminer si tu es au courant que le delay slot existe ou si t'es en train de le découvrir.

Alors, j'ai justement commencé à les utiliser. Voici ce que dit la doc :

Delayed Branches: In a delayed branch, the instruction following the branch is executed before the branch destination instruction.
Delay Slot: This execution slot following a delayed branch is called a delay slot.

En (très) gros, quand on arrive sur une instruction qui crée une delayed branch, on exécute d'abord l'instruction juste après celle actuelle (pc+2) avant de "jump" à l'adresse de la branche.

L'utilisation du delay slot dans les implémentations de la doc n'est pas super intuitive, par exemple pour le JMP, PC est modifié avant le delay slot:

JMP(int n)/* JMP @Rn */
{
     unsigned int temp;
     temp = PC;
     PC = R[n];
     Delay_Slot(temp+2);
}

Tandis que pour l'implémenter en C, j'ai dû réarranger un peu les choses pour exécuter le delay slot avant de modifier PC, et ensuite appliquer les modifications à PC :

void jmp(cpu_t* cpu, uint16_t instruction) {
    uint8_t rn = (instruction >> 8) & 0x0F;

    uint32_t temp = cpu->r[rn];

    cpu->pc += 2;
    run_next_instruction(cpu);
    cpu->pc = temp;
}

Voici un autre exemple plus complexe avec BSR et JSR :
Cliquez pour découvrir
Cliquez pour recouvrir
Implementation de la doc:
BSR(int d) /* BSR disp */
{
     int disp;
     unsigned int temp;
     temp = PC;

     if((d&0x800) == 0)
          disp = (0x00000FFF & d);
     else disp = (0xFFFFF000 | d);

     if(is_32bit_instruction(temp+2))
          PR = PC + 6;
     else PR = PC + 4;

     PC = PC + 4 + (disp << 1);
     Delay_Slot(temp + 2);
}

On remarque que PC et PR sont modifiés avant le delay slot.

Dans mon implémentation, ils sont modifiés après :
void bsr(cpu_t* cpu, uint16_t instruction) {
    uint32_t disp = instruction & 0x800 == 0 ? 0x00000FFF & instruction : 0xFFFFF000 | instruction;

    uint32_t temp = cpu->pc;

    cpu->pc += 2;
    run_next_instruction(cpu);
    cpu->pc = temp;

    if (is_32_bit_instruction(cpu, cpu->pc + 2)) {
        cpu->pr = cpu->pc + 6;
    }
    else {
        cpu->pr = cpu->pc + 4;
    }

    cpu->pc += 4 + (disp << 1);
}


Et pour JSR:
JSR(int n) /* JSR @Rn */
{
     unsigned int temp;
     temp = PC;
     if(is_32bit_instruction(temp+2))
         PR = PC +6;
     else PR = PC + 4;
     PC = R[n];
     Delay_Slot(temp+2);
}


Cette fois-ci il faut cette fois rajouter une variable temporaire pour implémenter la même fonctionnalité :
void jsr(cpu_t* cpu, uint16_t instruction) {
    uint8_t rn = (instruction >> 8) & 0x0F;

    uint32_t temp = cpu->pc;
    uint32_t r = cpu->r[rn];

    cpu->pc += 2;
    run_next_instruction(cpu);
    cpu->pc = temp;

    if (is_32_bit_instruction(cpu, cpu->pc + 2)) {
        cpu->pr = cpu->pc + 6;
    }
    else {
        cpu->pr = cpu->pc + 4;
    }

    cpu->pc = r;
}
(Note: is_32_bit_instruction() retourne toujours faux pour l'instant.)


Du coup j'ai un peu commencé à toucher à ce topic. Et dans l'émulateur Casio, quand j'exécute le code instruction par instruction avec "trace into" ou "step over", si j'arrive à un JMP ou BSR, l'instruction qui la suit est lue, mais ensuite le debugger skip directement à ce que j'imagine être l'adresse de retour de la delayed branch, avec plein de changements dans les valeurs des registres indiquant qu'il s'est passé plusieurs choses.
Dans l'émulateur que je suis en train de coder, je n'ai pas ce problème car je contrôle instruction par instruction, et je vois ce qu'il se passe et a quelle adresse saute PC après l’exécution du delayed slot.

Finalement, voici la nouvelle disposition de la mémoire d'après ce que j'ai compris des alias :
RAM_START représente désormais l'adresse physique de la RAM, et RAM_START_MMU_ALIAS l'alias contrôlé par le MMU.
#define RAM_START_MMU_ALIAS 0x08100000
#define RAM_START           0x8801c000 // (35+E)

// Retourne un pointeur sur la mémoire correspondante a l'adresse passée en argument.
// `mapped` représente l'adresse correspondante à l’intérieur de ce buffer.
// `d_mem_type` c'est du debug pour afficher quel type de mémoire est utilisée.
uint8_t* get_memory_for_address(cpu_t* cpu, uint32_t address, uint32_t* mapped, uint8_t* d_mem_type) {
    if (address > ROM_START && address < RAM_START_MMU_ALIAS) {
        // TODO: Check for ROM max valid address
        *mapped = address - ROM_START;
        *d_mem_type = 0;
        return cpu->mem->rom;  
    }
    else if (address > RAM_START_MMU_ALIAS && address < RAM_START_MMU_ALIAS + RAM_SIZE) {
        // Acces RAM par alias : 0x08100000
        *mapped = address - RAM_START_MMU_ALIAS;
        *d_mem_type = 1;
        return cpu->mem->ram;
    }
    else if (address > RAM_START && address < RAM_START + RAM_SIZE) {
        // Acces RAM direct : 0x8801c000
        *mapped = address - RAM_START;
        *d_mem_type = 2;
        return cpu->mem->ram;
    }
    else {
        critical_error("Memory address 0x%08X is out of bounds\n", address);
    }
}

Cela semble correct?

Edit: Je viens de remarquer autre chose, juste après avoir écrit PR dans la RAM, une autre instruction écrit le contenu de R4 dans la RAM (sachant qu'aucune autre instruction ne touche R4 avant).
R4 a une valeur de 1 dans l’émulateur CASIO.
Vu le rôle de PR et sa proximité avec cette instruction, est-ce que c'est la valeur de retour du programme justement ?
Devrais-je m'en soucier?
Lephenixnoir Hors ligne Administrateur Points: 24574 Défis: 170 Message

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


Pour les delay slots, la doc est correcte, l'instruction dans le delay slot voit la valeur de PC d'après le saut. Mais ce n'est pas un problème que tu as vraiment besoin de gérer car les instructions dont le comportement dépend de la valeur de PC sont quasiment toutes interdites dans les delay slots, et les rares qui ne le sont pas ne sont pas générées par les compilateurs car ce serait une purge. Je t'invite si tu es curieux à faire un printf() bien visible au cas où ça se produit, et je parie que tu ne le verras pas.

Le code de gestion mémoire est très bien.

La valeur de retour d'une fonction est dans r0. r4--r7 (puis la pile) sont les paramètres. Vois la partie "Conventions d'appel" de cette technique pour les détails. r4 ici est le premier paramètre passé à la fonction en question. Si tu regardes les paramètres de int AddIn_main() ça devrait te dire ce que c'est. Enfin, je sais pas si le code d'initialisation de l'add-in (qui est fourni par le SDK) transfère ces valeurs à AddIn_main() directement, mais j'imagine que c'est le cas.
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 10/11/2023 17:06 | #


1 - Syscalls

J'avançais dans le projet lorsque je suis tombé sur une instruction qui déplace la valeur de PC à l'adresse 0x80010070, soit vers la ROM physique.

Dans mon émulateur, il n'y a rien à cet endroit car c'est au-delà de la taille de l'add-in, donc j'ai eu un beau crash. Puis j'ai appris que cette adresse était utilisée comme point d'entrée pour lancer des syscalls (system calls), qui sont des fonctions non-incluses dans le CPU mais propres à l'OS de Casio. Dans une vraie calculatrice, l'OS est inclus dans la ROM et donc cette adresse est valide.
On pourrait directement copier l'OS d'une calculatrice au bon endroit dans l'émulateur et résoudre ce problème, mais d'un point de vue légal ça empêcherait de le distribuer publiquement.

La solution est donc d'émuler ces syscalls! Cette page de la bible liste ces syscalls et nous en apprend plus sur leur fonctionnement :

The calling convention to access the system calls, is to jump to 0x80010070 with the syscall number in r0.
The code you jmp to will rts back to the last function call made.

Ainsi, lorsque PC (l'adresse de la prochaine instruction) se retrouve à l'adresse 0x80010070, on peut lancer une fonction spéciale qui extrait le numéro de la syscall grâce à r0 et lance l'implémentation correspondante. Il est aussi possible de passer des paramètres aux syscalls avec r4-r7.
Une fois la syscall terminée, on peut simplement reprendre l'exécution en mettant la valeur de PC à celle de PR :

void run_next_instruction(cpu_t* cpu) {
    // Handle system calls
    if (cpu->pc == 0x80010070) {
        syscall(cpu);
        return;
    }
    
    // builtin CPU instructions
    // ...
}

void syscall(cpu_t* cpu) {
    uint32_t syscall_code = cpu->r[0];

    if (syscall_code == 0x807) { ... }
    else if (syscall_code == 0x808) { ... }
    else if (syscall_code == 0x90F) { ... }

    cpu->pc = cpu->pr;
}

Il existe des centaines de syscalls, mais heureusement elles ne sont pas toutes nécessaires pour cet émulateur et une vingtaine devraient suffire. Je les ajouterai au fur et à mesure que je les rencontrerai. Voici les quelques syscalls sur lesquels je suis tombé lors de l'émulation du Sample Add-in :

0x3FA: Hmem_SetMMU
0x494: void SetQuitHandler( void (*callback)(void) );
0x144: Bdisp_AllClr_DDVRAM
0x807: locate
0x808: Print
0x90F: GetKey

Je sais que la première n'est pas nécessaire et peut être ignorée. Mais je ne suis pas sûr pour la deuxième, et les 4 dernières me disent grandement quelque chose !..
Où est-ce que je pourrai trouver des détails sur l'implémentation de ces syscalls?

2 - Questions sur l’implémentation

En plus de mes interrogations sur les syscalls, j'aurais aimé vérifier certaines de mes implémentations, notamment la conversion entre byte/word/long et des confusions que j'ai avec la doc.

Voici l'implémentation de la doc de MOVI. (À noter : les paramètres font 1 byte chacun et sont des int)

// 1110nnnniiiiiiii
MOVI(int i, int n) /* MOV #imm,Rn */
{
     if ((i&0x80)==0) R[n] = (0x000000FF & i);
     else R[n] = (0xFFFFFF00 | i);
}

1) Pourquoi dans l'implémentation de SUB par exemple, les deux paramètres (qui font toujours 1 byte) sont désormais de type long ? Je les définit comme des uint8_t dans mon implémentation, ils ne devraient pas dépasser 255 (et même pas dépasser 15!)

// 0011nnnnmmmm1000
SUB(long m, long n) /* SUB Rm,Rn */
{
     R[n] -= R[m];
}

A noter que dans l’implémentation de MOVI (premier snippet), i est "sign-extended" de façon à le faire passer de type int8 à int32 tout en gardant sa valeur et son signe. On a vu ensemble que l'implémentation C pouvait être plus simple et ressembler à ça :

void MOVI(cpu_t* cpu, uint16_t instruction) {
    uint8_t rn = (instruction >> 8) & 0x0F;

    cpu->r[rn] = (int8_t)instruction; // Sign-extend (automatic with int8_t -> int32_t conversion)
}

Ici on cast instruction de int16_t à int8_t pour ne garder que les 8 premiers bits qui contiennent la valeur du paramètre "i". Puis le cast de int8_t à int32_t fait automatiquement le sign extension.

2) Maintenant passons à l'implémentation de la doc de MOVWL4 :

Cliquez pour découvrir
Cliquez pour recouvrir
// 10000101mmmmdddd
MOVWL4(long m, long d) /* MOV.W @(disp,Rm),R0 */
{
     long disp;
     disp = (0x0000000F & (long)d);       // 2) Si d est déjà un long, à quoi sert (long)d ?
     R[0] = Read_Word(R[m]+(disp<<1));
     if ((R[0]&0x8000)==0) R[0] &= 0x0000FFFF;
     else R[0] |= 0xFFFF0000;
}

Voici mon implémentation C de cette fonction :

void MOVWL4(cpu_t* cpu, uint16_t instruction) {
    uint8_t rm = (instruction >> 4) & 0x0F;
    uint32_t disp = instruction & 0x0F;

    uint32_t addr = cpu->r[rm] + (disp << 1);
    cpu->r[0] = mem_read(cpu, addr, 2);
}


3) Est-ce que les lignes "long disp = (0x0000000F & (long)d);" et "uint32_t disp = instruction & 0x0F;" sont équivalentes ?

4) Finalement, dans mon implémentation on peut voir que je ne fais pas l'extension de signe. En effet, j'ai remarqué que tous les Read_Word de la doc sont suivis d'une extension de signe (sauf 1 exception). J'ai donc essayé de la faire directement dans mon mem_read.

Est-ce que cette implémentation de mem_read pour 2 bytes permet bien de faire cette extension automatiquement ?

Cliquez pour découvrir
Cliquez pour recouvrir
uint32_t mem_read(cpu_t* cpu, uint32_t address, uint8_t bytes) {
    ...
    uint16_t val = (mem[mapped] << 8) | (mem[mapped + 1]);
    return val;
}

Je crée un uint16 et je retourne sa valeur, mais la fonction est de type uint32, donc je mise sur le cast pour l'extension de signe.


5) Pour continuer avec les cast qui me paraissent étranges, les registres r0-r15 contiennent des valeurs de type uint32_t.
Il me semble que l'un des deux cast suivants n'est pas nécessaire, et je ne comprends pas la différence entre les deux :

Cliquez pour découvrir
Cliquez pour recouvrir
CMPGT(long m, long n) /* CMP_GT Rm,Rn */
{
     if ((long)R[n]>(long)R[m]) T = 1;
     else T = 0;
}

CMPHS(long m, long n) /* CMP_HS Rm,Rn */
{
     if ((unsigned long)R[n]>=(unsigned long)R[m]) T = 1;
     else T = 0;
}


Voilà, alors ça fait un sacré paquet de choses mais j'avance plus vite que prévu, si je ne dis pas de bêtise dès que j'ai implémenté les syscalls ci-dessus le sample addin est déjà fonctionnel ! (Bon hormis les inputs et l'écran !)
Lephenixnoir Hors ligne Administrateur Points: 24574 Défis: 170 Message

Citer : Posté le 10/11/2023 17:24 | #


SetQuitHandler enregistre la fonction passée en paramètre et l'appelle quand l'add-in se termine. Il n'y a pas de détails unifiés mais un certain nombre de syscalls correspond à des fonctions de fxlib qui sont documentées dans le manuel officiel du SDK : https://bible.planet-casio.com/common/casio/sdk_manuals/Libraries.pdf

1) Pour mov #imm, rn comme d'hab le langage peut faire l'extension pour toi : R[n] = (int8_t)i. Il est probable que ça aille plus vite, sauf si le compilo est vraiment intelligent et reconnaît que c'est une extension de signe (ce qui est possible, mais pas garanti).

Dans la doc long est juste un mot machine. Vois-le comme un entier générique, en gros "on s'en fout" et tu peux faire confiance à ton intuition sur quand est-ce qu'il est nécessaire de faire une extension de signe.

Ah ben tiens tu arrives à la même conclusion sur mov #imm, rn, très bien.

2) Tu peux l'ignorer, y'a pas de subtilité. Dans cette doc les extensions de signes sont explicites, les types/cast ne cachent pas de piège.

3) Oui, c'est équivalent. Faire l'extension de signe dans l'accès mémoire est valide. Je ne crois pas de tête qu'il y aura de cas où tu voudras faire un accès mémoire sans extension. (Garde un œil dessus quand même au cas où.)

4) Non car les types sont non signés donc ça fait une extension à zéro. Si tu veux une extension de signe il faut que tu prennes un type signé, ie. return (int16_t)val.

5) Si tu stockes comme des uint32_t les comparaisons sont non signées. Donc le cast est inutile dans le second cas. Si tu stockais comme des int32_t, c'est-à-dire comme le manuel (long est signé), ce serait l'inverse. Le manuel est explicite pour éviter toute confusion et c'est très bien ; tu peux l'imiter. Le compilateur est beaucoup trop intelligent et éliminera tout seul le cast inutile ; de toute façon le cast c'est un peu une vue de l'esprit pour le typage, le compilateur fait la magie qui va bien après.
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 10/11/2023 18:01 | # | Fichier joint


Merci pour ces éclaircissements!

Ah oui effectivement, le signed extension ne marche que pour des valeurs signées.
D'ailleurs je me suis trompé je stocke bien les registres r0-r15 en int32, donc c'est le premier cas qui est redondant.
De plus j'avais mal lu la doc, je ne comprenais pas pourquoi les cast étaient différents, mais c'était indiqué que CMPHS fait une comparaison entre unsigned.

Le lien que tu as envoyé est utile mais certaines fonctions restent un peu floues. Par exemple Print, ses parametres sont renseignés mais je n'ai pas l'impression d'avoir assez d'informations pour débuter son implémentation :



Concernant la VRAM, il s'agit d'un espace mémoire contenant l'état des pixels a l'écran?
Il y a 128*64 = 8192 pixels, comme chaque pixel peut être représenté avec un seul bit (allumé/éteint), il nous faut 8192 bits, soit 1024 bytes pour stocker tous les pixels, c'est correct?

uint32_t vram[1024];

Est-ce qu'il y a d'autres subtilités a savoir?
Lephenixnoir Hors ligne Administrateur Points: 24574 Défis: 170 Message

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


Pour Print(), tu dois avoir une valeur globale qui est la position du curseur à l'écran (modifiable avec locate()), qui est en fait une globale de fxlib (x entre 1 et 21, y entre 1 et 8). Print() affiche le texte donné à cette position (ie. un truc du genre 6x+1, 8y si ma mémoire est bonne) en utilisant la police par défaut du système.

Pour ta VRAM, c'est correct. Pas d'autre subtilité sur la VRAM je pense.
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 11/11/2023 18:07 | # | Fichier joint


Top c'est effectivement ce que j'avais commencé à faire.

J'ai créé une nouvelle structure qui contient les infos liées à l'écran et l'affichage, également passée par pointeur au CPU pour faciliter l'accès :

typedef struct {
    int col; // starts at 1
    int row; // starts at 1
} cursor_t;

struct display_t {
    // Pointer to virtual memory in RAM
    uint8_t* vram;

    // Current cursor position
    cursor_t cursor;
};

Comme tu m'as appris sur le chat, cette VRAM n'est pas un nouveau type de mémoire, elle pointe seulement vers une adresse en RAM.
Je ne suis pas bien sûr d'avoir compris où est-ce que je devais/pouvais mettre la VRAM. De ce que j'ai compris, je peux la mettre n'importe où avant les 256 premiers kio de la RAM ? Voici son initialisation actuelle, arbitrairement placée 64 kio après le début de la RAM :

#define VRAM_ADDRESS (64 * 1024)

cpu->display->vram = &cpu->memory->ram[VRAM_ADDRESS];

C'est valide?


Concernant les syscalls, j'ai implémenté locate :

// Sets the position of the display cursor
// x: [1-21], y: [1-8]
void syscall_locate(cpu_t* cpu, int x, int y) {
    cpu->disp->cursor.col = x;
    cpu->disp->cursor.row = y;
}

J'ai aussi implémenté syscall_Bdisp_AllClr_VRAM, mais en quoi consiste syscall_Bdisp_AllClr_DD ?

// These functions clear the specified area of VRAM and/or DD (Display Driver).

#define VRAM_SIZE (SCREEN_WIDTH * SCREEN_HEIGHT / 8)

void syscall_Bdisp_AllClr_DD(cpu_t* cpu) {
    // TODO
}

void syscall_Bdisp_AllClr_VRAM(cpu_t* cpu) {
    for (int i = 0; i < VRAM_SIZE; i++) {
        cpu->disp->vram[i] = 0;
    }
}

void syscall_Bdisp_AllClr_DDVRAM(cpu_t* cpu) {
    syscall_Bdisp_AllClr_DD(cpu);
    syscall_Bdisp_AllClr_VRAM(cpu);
}

Finalement, j'ai passé un bon moment à essayer d'implémenter la syscall Print. La première étape consistait à trouver une texture contenant les caractères utilisés par Casio. En fouillant sur PC, j'ai trouvé ton repo contenant des fonts unicodes pour fx-9860.
Est-ce que je dois implémenter tous les caractères contenus dans le dossier uf5x7? Comment conseillerais-tu d’implémenter la gestion des fonts dans un émulateur?

Pour l'instant, j'ai vu que les caractères alphanumériques standard (utilisés par le Sample Addin dans locate) se trouvaient dans la texture U+0020.
Je l'ai donc ajoutée à mon projet, j'ai également ajouté la librairie stb_image.h qui permet de lire des images en C puis j'ai convertit l'image en un tableau de 128*64 uint (1 pixel par byte pour simplifier, vu la taille de l'image c'est pas dramatique).

Ensuite, l'algorithme du syscall Print est finalement plutôt simple (bon il ne gère qu'une seule texture pour l'instant), il suffit de calculer pour chaque caractère sa position à l'écran, sa position dans la texture unicode, et copier les pixels depuis la texture vers la VRAM dans une zone délimitée.
Voici mon implémentation actuelle de la syscall Print. Je pense qu'elle pourrait être optimisée pour traiter 8 pixels à la fois, mais pour l'instant j'en suis satisfait (à noter que la dénotation "ASCII" est probablement un abus de language ici, mais plus rapide a écrire que Unicode!).

// Size of characters in ASCII texture
#define ASCII_CHAR_WIDTH 7
#define ASCII_CHAR_HEIGHT 9

// Size of characters on screen
#define CHAR_WIDTH 6
#define CHAR_HEIGHT 8

// Display a string at the current position of the display cursor
void syscall_print(cpu_t* cpu, const unsigned char* str) {
    int i = 0;
    while (1) {
        int c = str[i]; // Current character

        if (c == 0x00) break; // End of string

        int offset = c - 0x0020; // Offset from texture start

        // Character x,y in ASCII texture (16x6)
        int char_x = offset % 16;
        int char_y = offset / 16;
        // Pixel start x, y in ASCII texture (112x54)
        char_x = char_x * ASCII_CHAR_WIDTH;
        char_y = char_y * ASCII_CHAR_HEIGHT;

        // Character x,y on screen (21x7)
        int screen_x = cpu->disp->cursor.col;
        int screen_y = cpu->disp->cursor.row;
        // Pixel start x,y on screen (128x64)
        screen_x = screen_x * CHAR_WIDTH + 1;
        screen_y = screen_y * CHAR_HEIGHT;

        for (int y = 0; y < ASCII_CHAR_HEIGHT; y++) {
            for (int x = 0; x < ASCII_CHAR_WIDTH; x++) {
                // Current pixel position in ASCII texture
                int ascii_x = char_x + x;
                int ascii_y = char_y + y;
                int ascii_id = ascii_x + ascii_y * 112;      // Pixel index
                int ascii_bit = cpu->asciiTexture[ascii_id]; // Pixel value

                if (ascii_id < 0 || ascii_id > 112 * 54) critical_error("ASCII Texture Access out of bounds: %d %d (%d)", ascii_x, ascii_y, ascii_id);

                // Current pixel position on-screen
                int vram_x = screen_x + x;
                int vram_y = screen_y + y;
                int vram_id = (vram_x + vram_y * SCREEN_WIDTH) / 8; // index in VRAM
                int vram_bit = vram_x % 8;                          // Current bit

                if (vram_id < 0 || vram_id > VRAM_SIZE) critical_error("VRAM Access out of bounds: %d %d (%d)", vram_x, vram_y, vram_id);

                // Change one single bit in the VRAM
                cpu->disp->vram[vram_id] = (cpu->disp->vram[vram_id] & ~(1 << vram_bit)) | (ascii_bit << vram_bit);
            }
        }
        
        // Move the cursor the the right
        cpu->disp->cursor.col++;
        i++;
    }
}

Pas de soucis particulier dans ce code?
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?

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 ?

En tout cas, voici le résultat d'un bon gros printf de la VRAM après avoir exécuté le sample addin dans mon émulateur!



Je suis d'ailleurs ravi de vous apprendre que le sample add-in mets exactement 134 instructions avant d'arriver dans la boucle infinie du getkey, et il a besoin pour cela de 27 instructions uniques et 7 syscalls (sans compter les instructions effectuées par l’OS d’origine lors de l’appel de ces syscalls)
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
1, 2 Suivante

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 206 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