PS1 (alias PSX, alias PS One) est la première génération de consoles de jeux PlayStation de Sony et appartient à la cinquième génération de consoles de jeux en général. Il utilise un lecteur de vitesse 2x pour lire les CD. Une telle quantité de données, selon les normes de l'heure actuelle pour la console, permettait aux développeurs de jeux de ne pas particulièrement se pencher sur les restrictions lors de la création de contenu pour les jeux, ce qui rendait ce dernier de meilleure qualité par rapport aux jeux de la génération précédente. de consoles. De plus, les jeux peuvent désormais être longs. Et si un jeu, à de rares exceptions près, sur les consoles des générations précédentes pouvait bien être terminé en une seule session de jeu, alors avec les jeux PS1, tout était différent. Pour enregistrer les progrès, PlayStation dispose de cartes mémoire: de petits modules de mémoire non volatile amovibles.
Si vous vous demandez exactement comment fonctionne la carte mémoire PlayStation 1, comment elle fonctionne et comment vous pouvez créer la vôtre, bienvenue dans cat.
Ainsi, la carte mémoire PS1 est un périphérique standard, comme tout le zoo de joypads, joysticks et autres accessoires. Pour comprendre exactement comment cela fonctionne, vous devez d'abord regarder ce qu'il contient.
Photo de la carte de circuit imprimé d'une carte mémoire standard de 15 blocs
Comme vous pouvez le voir sur la photo, le dispositif de la carte est très simple: un contrôleur qui sert les requêtes du système, et, en fait, une mémoire non volatile, qui est représentée par le standard NOR FLASH. Logiquement, la carte mémoire est divisée en 15 blocs que les jeux peuvent utiliser. Il peut sembler que 15 n'est pas logique pour un système binaire, mais il n'y a pas de contradiction ici: un bloc est donné pour le système de fichiers, les noms de fichiers et même les icônes animées y sont stockés, tout comme les flux NTFS. Chaque bloc a une taille de 8 Kio, 16 blocs au total soit 128 Kio, ce qui peut être vu à partir du marquage de la mémoire FLASH sur la photo ci-dessus.
Au début, c'était suffisant pour tout le monde, mais ensuite, des jeux ont commencé à apparaître qui utilisaient plus d'un bloc à la fois. Par exemple, certains simulateurs, comme Sega GT, utilisent 4-5 blocs, tandis que ConstructorDonc, en général, la carte mémoire entière est de 15 blocs. Cela a obligé à acheter plus de cartes et la situation a menacé de devenir comme des disquettes ou des cartouches. Mais alors les pirates se sont arrêtés et ont commencé à émettre des cartes pour 2, 4 ou 8 pages à la fois. Et les pages étaient permutées soit par une combinaison intelligente sur le joypad, soit par un bouton explicite sur la carte mémoire elle-même. Cependant, dans les cartes de plus de 2 pages, la compression était utilisée, et le nombre réel de pages était beaucoup moins élevé, et certaines cartes pouvaient être bêtement bloquées. Et il était très difficile de les sortir de cet état, mais ce que les joueurs n'ont pas fait pour leurs arrêts. Voici des représentants typiques des cartes mémoire multipages: à
gauche, une carte mémoire avec 2 pages, à droite avec 8. La droite a un bouton de rotation de page matérielle et un indicateur indiquant le nombre de 1 à 8, qui est caché derrière un verre sombre
Petite digression lyrique
Tout a commencé en 2001, lorsque j'ai acheté un disque miracle pour PC appelé "All Emulators", sur lequel il y avait des émulateurs PS1 dont: c'était Bleem! et au début de l'ePSXe. Et mon ordinateur de l'époque était même capable de lire mes disques PS1 de manière jouable! Et un peu plus tard, j'ai eu un modem et j'ai découvert DirectPad Pro . Connecter un joystick natif à un ordinateur (bien que via LPT) coûte cher. Et ce système fonctionnait à la fois sur 9x et XP! Et un peu plus tard, déjà en 2002, j'ai découvert Memory Card Capture Sakura! Ce programme permettait de travailler avec de vraies cartes mémoire en utilisant le même schéma de connexion DirectPad Pro. C'est alors que j'ai eu l'idée de créer une carte mémoire "sans fin" qui me permettrait d'échanger des informations avec un ordinateur sans avoir besoin d'appareils supplémentaires. Mais à ce moment-là, je n'avais pas assez d'informations et d'éléments disponibles, et l'idée est restée juste une idée, scintillant quelque part dans l'arrière-cour de ma conscience.
Presque 9 ans se sont écoulés depuis que j'ai réalisé que j'en savais déjà assez et que j'avais l'opportunité d'implémenter au moins une version d'une carte mémoire sans fin. Cependant, un autre facteur est entré en jeu ici - l'âge et tout ce qui y est lié. Il y a moins de temps pour les loisirs, de plus en plus de soucis. Et ce n'est que maintenant que je peux fournir au public au moins un résultat, une preuve de concept à part entière.
Interface physique
Ainsi, la carte mémoire et les joypads fonctionnent via une interface commune. Le nombre de signaux qu'il contient est de 6, voici leurs noms et objectifs:
- SEL0 - Premier signal de sélection de port, niveau actif bas
- SEL1 - Deuxième signal de sélection de port, niveau actif bas;
- CLK - Signal d'horloge de l'interface, état passif niveau haut, sur front descendant, verrouillage sur front;
- CMD - Signal de données de la console vers la périphérie;
- DAT - Signal de données de la périphérie vers la console;
- ACK - Prise de contact matérielle, active faible.
Il existe également deux tensions d'alimentation différentes sur l'interface: 3,3 V et 7,6 V. Tous les signaux sauf SEL0 et SEL1 sont communs à tous les appareils connectés. C'est pourquoi une carte mémoire qui ne fonctionnait pas ou un joypad dans le deuxième emplacement affectait les travailleurs du premier, bien qu'après les consoles 16 bits, cela semblait étrange. Je pense que beaucoup ont déjà reconnu le SPI standard dans l'interface - tout est correct, c'est vrai. Ajout d' un signal ACK uniquement pour confirmer l'opération d'E / S. Voici les affectations des signaux sur les contacts de la carte mémoire:
Carte mémoire réparée avec FLASH 5 volts Les
caractéristiques techniques de l'interface sont les suivantes:
___ ___________________________ ____
\ / \ /
X X
___/ \___________________________/ \____
___ ____________
\ / \
\ / \
\____________/ \____
| |
| tck |
|<--------------------------->|
+-------+-------+------+-------+
| | . | . | . |
+-------+-------+------+-------+
| tck | 1 | 4 | - |
+-------+-------+------+-------+
ACK:
____
SEL- |______________________________________________
______ __________ ___________
CLK |||||||| |||||||| ||||||||
| |
ACK- -----------------------|_|-------------|_|---------
| ta1 | | | ta2 |
|<------->| | |<----->|
| | ap
>|-|<-----
+-----+------+-------+--------+
| | . | . | . |
+-----+------+-------+--------+
| ta1 | 0 | - | 100 | -
+-----+------+-------+--------+
| ta2 | | 10 | 1 |
+-----+------+-------+--------+
| ap | 2 | | | ACK
+-----+------+-------+--------+
La fréquence mesurée du signal CLK est de 250 kHz, soit 4 µs par cycle. Avec les paramètres physiques de l'interface triés, maintenant la couche de transport. Un ingénieur expérimenté a déjà remarqué que le joypad et la carte mémoire sont connectés complètement en parallèle et peuvent entrer en conflit. En effet, il existe un arbitrage logiciel pour cela. Une fois le signal SELn activé, la périphérie reste silencieuse, mais écoute le premier octet envoyé. Si cet octet est égal à 0x01, alors le joypad est activé et la carte mémoire reste silencieuse jusqu'à ce que le signal de sélection soit désactivé. Et si l'octet était 0x81, alors le contraire est vrai: la carte mémoire est activée et le joypad est silencieux. Naturellement, l'hôte attend un signal ACKsur cet octet d'arbitrage et n'attend pas longtemps. Ceci est nécessaire pour avoir le temps d'interroger le reste de la périphérie, si une partie de cette périphérie est absente. Le fait est que le système d'exploitation interroge les contrôleurs et les cartes mémoire strictement en fonction du signal du trajet inverse du faisceau, ou mieux connu sous le nom de VBlank . Il est ainsi admis que les jeux sur consoles jusqu'à la 5e génération sont liés à ce timing, qui est égal à la fréquence d'images. Et la fréquence d'images est strictement stable et normalisée: 50Hz pour PAL et 60Hz pour NTSC. Autrement dit, la période d'interrogation pour les joysticks et les cartes mémoire est de 20 ms pour PAL ou de 16 ms pour NTSC.
Donc, nous avons compris l'arbitrage, maintenant le niveau le plus élevé. Quelles commandes la carte mémoire PS1 standard comprend-elle? Oui, en fait, il n'y en a que 3.
- R - 0x52 ou lecture . Lecture d'un secteur d'une carte mémoire;
- W - 0x57 ou écriture . Enregistrement du secteur de la carte mémoire;
- S - 0x53 ou état . Lecture de l'état de la carte mémoire.
La carte mémoire entière est divisée en secteurs. Un secteur de 128 octets. Ainsi, 128KiB correspond à 0x400 ou 1024 secteurs. Dans ce cas, vous n'avez pas besoin d'effacer le secteur avant l'enregistrement. Mais le système est garanti pour donner du temps pour la prochaine image entière lors de l'enregistrement. Autrement dit, il peut lire la carte mémoire à chaque image, mais l'écrit après une. En passant, toutes sortes de "Codebreakers" ne respectent pas ces délais pour accélérer. Analysons chaque commande plus en détail.
Protocole de la carte mémoire
L'ordre des données transmises dans chaque commande ressemble à ceci:
En train de lire:
CMD 0x81 0x52 0x00 0x00 MSB LSB 0x00 0x00 0x00 0x00 0x00 ... 0x00 0x00 0x00 DAT ---- FLAG 0x5A 0x5D PRV PRV 0x5C 0x4D MSB LSB DATA ... DATA CHK ACK
:
CMD 0x81 0x57 0x00 0x00 MSB LSB DATA ... DATA CHK 0x00 0x00 0x00 DAT ---- FLAG 0x5A 0x5D PRV PRV PRV ... PRV PRV 0x5C 0x5D ACK
:
CMD 0x81 0x53 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 DAT ---- FLAG 0x5A 0x5D 0x5C 0x5D 0x04 0x00 0x00 0x80
Légende:
CMD - Données que l'hôte envoie à la carte.
DAT - Données que la carte envoie à l'hôte.
FLAG - Indicateurs actuels de l'état de la carte et du résultat de la commande précédente.
PRV - Données reçues précédentes, résultat de la simplification du circuit dans la carte.
MSB - Octet de poids fort du numéro de secteur.
LSB - Octet le moins significatif du numéro de secteur.
DATA - Charge utile.
CHK - Somme de contrôle du bloc.
ACK - Indicateur d' accusé de réception.
L'octet FLAG utilise les bits suivants:
- D5 – Sony. .
- D3 – . .
- D2 – , .
Après la mise sous tension, FLAG est 0x08. Et après le premier enregistrement, il est remis à zéro. Le système d'exploitation PS1 écrit toujours dans le secteur 0x003F pour cela, provoquant ainsi une usure de ce secteur. Mais dans le cadre du marquage de la carte mémoire par le système, il n'y a aucune information utile dans ce secteur. Numéro de secteur MSB : LSB 10 bits et va de 0x0000 à 0x03FF. La somme de contrôle CHK est le XOR habituel des 128 octets de données + MSB et LSB . La confirmation ACK ne peut prendre que 3 valeurs: G 0x47, E 0x43 et 0xFF. G = Bon ou OK. E = Erreur . En fait, lors de la lecture de la carte, ACK est toujours égal à G , et lors de l'écriture de G = OK, E = erreur de somme de contrôle et 0xFF signifie un numéro de secteur incorrect. Certes, la plupart des cartes rejettent simplement les bits inutilisés dans l'octet de poids fort du numéro de secteur et ne répondent donc jamais avec 0xFF. Les nombres 0x0400 et 0x0080 dans la commande d'état suggèrent certaines pensées qu'il s'agit du nombre de secteurs et de la taille du secteur en octets, mais cela n'est pas connu avec certitude. Eh bien, nous y sommes et nous arrivons à l'essentiel:
Réaliser votre carte mémoire
Donc, ce sont toutes les informations dont vous avez besoin pour créer votre carte mémoire PS1. Les goulots d'étranglement potentiels sont les suivants:
- Lors de la lecture, la mise à jour des données prend du temps. Entre le numéro de secteur et le transfert de données réel, nous avons 4 octets à partir desquels nous pouvons étirer un peu l' ACK . À propos, pour la carte mémoire d'origine sur NOR FLASH, tous les ACK vont de manière uniforme, pour les cartes mémoire avec SPI FLASH, après la transmission LSB , il y a un délai ACK , pendant lequel le contrôleur définit la commande sur SPI FLASH et lit le premier octet , et lit le reste pendant l'échange.
- Lors de l'enregistrement, après le transfert du paquet entier et le début de l'enregistrement vers la matrice, cela prend du temps, mais ici le système lui-même donne le retard nécessaire.
En ce qui concerne l'alimentation, les joypads 3,3V sont utilisés pour la logique et 7,6V sont utilisés pour alimenter les moteurs. Les cartes mémoire n'utilisent généralement qu'une seule alimentation. S'il y a un FLASH 5 V à l'intérieur, alors 7,6 V et un stabilisateur sont utilisés. S'il y a 3,3 V FLASH, alors 3,3 V est utilisé immédiatement.
La première version que j'ai construite sur STM32F407VG, qui est alimentée par 3,3 V, a SPI pour PSIO, SDIO rapide et suffisamment de mémoire pour stocker l'image entière à l'intérieur d'elle-même, résolvant les problèmes susmentionnés. Photo de l'appareil fini:
La première version de ma carte mémoire sur STM32F407
Cela s'est avéré rapide, fiable, mais cher. Pouvez-vous le faire moins cher? Eh bien, le défi est accepté. Compte tenu des spécificités de la tâche, j'ai choisi STM32F042F6. Voici ce qui s'est passé:
Deuxième version de ma carte mémoire sur STM32F042
Notre carte est entraînée, donc la stabilisation de fréquence avec un résonateur à quartz externe n'est pas nécessaire, un oscillateur interne suffit. Ce contrôleur a un SPI matériel, donc je l'ai donné à la carte SD pour réduire les délais de transport. PSIO sera un logiciel ici.
Implémentation logicielle
La première chose à faire est de travailler avec une carte SD en mode SPI. Je ne m'attarderai pas trop sur cela, il a longtemps été mâché et dispersé sur Internet. Le code d'initialisation, de lecture et d'écriture du secteur est donné ci-dessous.
Card_Init ()
// TCardType Card_Init( void ) { // TCardType Res; uint32_t Cnt,OCR; uint8_t Dat, Resp; // CARD_OFF; Res = ctNone; // SPI PCLK/128: 48/128 = 0,375 SPI1->CR1 &= ~SPI_CR1_SPE; SPI1->CR1 = SPI_CR1_MSTR | SPI_LOW_SPEED; SPI1->CR1 |= SPI_CR1_SPE; // HAL_Delay( 1 ); // 256 for (Cnt = 0;Cnt < 256;Cnt++ ) { // Card_SPI( 0xFF ); } // CARD_ON; // do { // 0xFF Dat = Card_SPI( 0xFF ); } while ( Dat != 0xFF ); // CMD0: GO_IDLE_STATE Card_SendCMD( &CARD_CMD0[ 0 ], CMD_LENGTH ); Resp = Card_WaitResp( &OCR, DISABLE, 128 ); // ? if ( Resp == 0x01 ) { // IDLE_STATE, CMD8: SEND_IF_COND Card_SendCMD( &CARD_CMD8[ 0 ], CMD_LENGTH ); Resp = Card_WaitResp( &OCR, ENABLE, 128 ); // if ( Resp != 0x01 ) { // SDv1/MMC do { // ACMD41: APP_SEND_OP_COND Card_SendCMD( &CARD_ACMD41[ 0 ], CMD_LENGTH ); Resp = Card_WaitResp( &OCR, ENABLE, 128 ); } while ( Resp == 0x01 ); // ? if ( Resp == 0x00 ) { // SD v1 Res = ctSD1; } else { // MMC, Res = ctUnknown; } } else { // SDv2 if ( (OCR & 0x0001FF) == 0x0001AA ) { // SDv2 do { // ACMD55: APP_CMD Card_SendCMD( &CARD_CMD55[ 0 ], CMD_LENGTH ); Resp = Card_WaitResp( &OCR, DISABLE, 128 ); // if ( Resp == 0x01 ) { // ACMD41: APP_SEND_OP_COND Card_SendCMD( &CARD_ACMD41[ 0 ], CMD_LENGTH ); Resp = Card_WaitResp( &OCR, ENABLE, 128 ); } } while ( Resp == 0x01 ); // ? if ( Resp == 0x00 ) { // CMD58: READ_OCR Card_SendCMD( &CARD_CMD58[ 0 ], CMD_LENGTH ); Resp = Card_WaitResp( &OCR, ENABLE, 128 ); // ? if ( Resp == 0x00 ) { // OCR if ( (OCR & 0x40000000) == 0x00000000 ) { // Res = ctSD2; } else { // Res = ctSD3; } } else { // Res = ctUnknown; } } else { // Res = ctUnknown; } } else { // Res = ctUnknown; } } } else { // if ( Res != 0xFF ) { Res = ctUnknown; } } // if ( (Res == ctSD1) || (Res == ctSD2) ) { // 512 // CMD16: SET_BLOCKLEN Card_SendCMD( &CARD_CMD16[ 0 ], CMD_LENGTH ); Resp = Card_WaitResp( &OCR, DISABLE, 128 ); // ? if ( Resp != 0x00 ) { // Res = ctUnknown; } } // while ( (SPI1->SR & SPI_SR_BSY) != 0x0000 ) { } CARD_OFF; // if ( (Res != ctNone) && (Res != ctUnknown) ) { // SPI PCLK/2: 48/2 = 24 SPI1->CR1 &= ~SPI_CR1_SPE; SPI1->CR1 = SPI_CR1_MSTR; SPI1->CR1 |= SPI_CR1_SPE; } // return Res; }
Card_Read ()
// DMA FunctionalState Card_Read( TCardType CardType, uint8_t *Buf, uint32_t *Loaded, uint32_t Addr ) { // FunctionalState Res; uint8_t Cmd[ 6 ]; uint8_t Dat,Resp; uint32_t Cnt; // Res = DISABLE; // , ? if ( *(Loaded) != Addr ) { // *(Loaded) = Addr; // if ( (CardType == ctSD1) || (CardType == ctSD2) ) { // LBA Addr *= 0x00000200; } // while ( 1 ) { // - if ( CardType == ctNone ) { break; } if ( CardType == ctUnknown ) { break; } // Cmd[ 0 ] = CARD_CMD17; Cmd[ 1 ] = Addr >> 24; Cmd[ 2 ] = Addr >> 16; Cmd[ 3 ] = Addr >> 8; Cmd[ 4 ] = Addr; Cmd[ 5 ] = 0xFF; // CARD_ON; // do { // 0xFF Dat = Card_SPI( 0xFF ); } while ( Dat != 0xFF ); // Card_SendCMD( &Cmd[ 0 ], CMD_LENGTH ); Resp = Card_WaitResp( (uint32_t *)&Cmd[ 0 ], DISABLE, 128 ); // if ( Resp != 0x00 ) { break; } // Cnt = 2048; do { // Dat = Card_SPI( 0xFF ); // Cnt--; } while ( (Dat == 0xFF) && (Cnt > 0) ); // ? if ( Cnt == 0 ) { break; } // ? if ( Dat != CARD_DATA_TOKEN ) { break; } // , for (Cnt = 0;Cnt < 512;Cnt++) { // *(Buf) = Card_SPI( 0xFF ); Buf++; } // CRC Cmd[ 0 ] = Card_SPI( 0xFF ); Cmd[ 1 ] = Card_SPI( 0xFF ); // Res = ENABLE; // break; } } else { // Res = ENABLE; } // while ( (SPI1->SR & SPI_SR_BSY) != 0x0000 ) { } CARD_OFF; // , if ( Res == DISABLE ) { *(Loaded) = 0xFFFFFFFF; } // return Res; }
Card_Write ()
// DMA FunctionalState Card_Write( TCardType CardType, uint8_t *Buf, uint32_t *Loaded, uint32_t Addr ) { // FunctionalState Res; uint8_t Cmd[ 6 ]; uint8_t Dat,Resp; uint32_t Cnt; // Res = DISABLE; // if ( (CardType == ctSD1) || (CardType == ctSD2) ) { // LBA Addr *= 0x00000200; } // while ( 1 ) { // - if ( CardType == ctNone ) { break; } if ( CardType == ctUnknown ) { break; } // Cmd[ 0 ] = CARD_CMD24; Cmd[ 1 ] = Addr >> 24; Cmd[ 2 ] = Addr >> 16; Cmd[ 3 ] = Addr >> 8; Cmd[ 4 ] = Addr; Cmd[ 5 ] = 0xFF; // CARD_ON; // do { // 0xFF Dat = Card_SPI( 0xFF ); } while ( Dat != 0xFF ); // Card_SendCMD( &Cmd[ 0 ], CMD_LENGTH ); Resp = Card_WaitResp( (uint32_t *)&Cmd[ 0 ], DISABLE, 128 ); // if ( Resp != 0x00 ) { break; } // Card_SPI( CARD_DATA_TOKEN ); // // , for (Cnt = 0;Cnt < 512;Cnt++) { // Card_SPI( *(Buf) ); Buf++; } // CRC Card_SPI( 0xFF ); Card_SPI( 0xFF ); // Res = ENABLE; // break; } // while ( (SPI1->SR & SPI_SR_BSY) != 0x0000 ) { } CARD_OFF; // ? if ( Res == ENABLE ) { // *(Loaded) = Addr; } else { // *(Loaded) = 0xFFFFFFFF; } // return Res; }
La carte s'initialise à 375 kHz (PCLK / 128) et fonctionne à 24 MHz (PCLK / 2). À de telles vitesses, les mesures ont montré que SDv1 et SDHC donnent un secteur à moins de 2,8 ms pour l'ensemble de la transaction. Il faut s'en souvenir car important pour l'opération de lecture PSIO.
Regardons maintenant PSIO. Comme mentionné ci-dessus, nous l'avons de toute façon dans le logiciel. Il n'y a que deux signaux à suivre: SEL et CLK . Nous suivrons le premier sur les deux fronts et préparerons l'échange de données:
EXTI2_3_IRQHandler ()
// SEL void EXTI2_3_IRQHandler( void ) { // EXTI->PR = 0x00000004; // SEL if ( MEM_SEL ) { // SEL = 1 EXTI->IMR &= 0xFFFFFFFE; State.PSIO.Mode = mdSync; // LED_GREEN_OFF; } else { // SEL = 0 EXTI->IMR |= 0x00000001; State.PSIO.Bits = 7; // LED_GREEN_OFF; LED_RED_OFF; } // MEM_DAT1; MEM_nACK; }
Nous n'attraperons le signal CLK que le long du front. Le fait est que le STM32F042 ne fonctionne qu'à 48 MHz et que ses performances sont trop faibles pour notre tâche. Et si vous faites une interruption sur les deux fronts, alors pendant le transfert d'un octet, il ne sort pratiquement pas du gestionnaire d'interruption et tout fonctionne à la limite du possible, échouant parfois. Et si vous ne réagissez qu'au front, et que le travail à faire sur le déclin se fait à la fin de l'interruption, alors tout va bien en moins de 55% de la période CLK , car plusieurs chèques peuvent être rejetés . Je suis sûr que si ce gestionnaire est écrit en assembleur de la manière la plus optimale possible, il serait capable de fonctionner même sur les deux sauts. Voici le code du gestionnaire:
EXTI0_1_IRQHandler ()
// CLK void EXTI0_1_IRQHandler( void ) { // EXTI->PR = 0x00000001; // uint16_t AckTime; // AckTime = 0; // State.PSIO.DataIn >>= 1; if ( MEM_CMD ) { // 1 State.PSIO.DataIn |= 0x80; } else { // 0 State.PSIO.DataIn &= 0x7F; } // if ( State.PSIO.Bits > 0 ) { // State.PSIO.Bits--; } else { // ? if ( State.PSIO.Bits == 0 ) { // State.PSIO.Bits = 7; // State.PSIO.DataOut = State.PSIO.DataIn; // switch ( State.PSIO.Mode ) { // case mdSync : { // if ( State.PSIO.DataIn == 0x81 ) { // State.PSIO.Mode = mdCmd; // State.PSIO.DataOut = State.MemCard.Status; // ACK AckTime = AckNormal; } else if ( State.PSIO.DataIn == 0x01 ) { // , . State.PSIO.Mode = mdDone; } // break; } // case mdCmd : { // State.PSIO.Mode = mdParam; // State.MemCard.Cmd = State.PSIO.DataIn; State.MemCard.Bytes = 0; // State.PSIO.DataOut = 0x5A; // ACK AckTime = AckNormal; // break; } // case mdParam : { // ACK AckTime = AckNormal; // switch ( State.MemCard.Cmd ) { // : R case 0x52 : { // switch ( State.MemCard.Bytes ) { // case 0 : { State.PSIO.DataOut = 0x5D; break; } case 1 : { break; } case 2 : { State.MemCard.Sector = State.PSIO.DataIn * 0x0100; State.MemCard.Check = State.PSIO.DataIn; break; } case 3 : { State.MemCard.Sector += State.PSIO.DataIn; State.MemCard.Check ^= State.PSIO.DataIn; State.PSIO.DataOut = 0x5C; State.SDCard.CardOp = coRead; AckTime = AckDelayed; break; } case 4 : { State.PSIO.DataOut = 0x5D; AckTime = AckDelayed; break; } case 5 : { State.PSIO.DataOut = State.MemCard.Sector >> 8; AckTime = AckDelayed; break; } case 6 : { State.PSIO.DataOut = State.MemCard.Sector; AckTime = AckDelayed; State.PSIO.Mode = mdRdData; State.MemCard.Bytes = 0; break; } default : { State.PSIO.Mode = mdDone; AckTime = 0; break; } } // LED_GREEN_ON; // break; } // : W case 0x57 : { // switch ( State.MemCard.Bytes ) { // case 0 : { State.PSIO.DataOut = 0x5D; break; } case 1 : { break; } case 2 : { State.MemCard.Sector = State.PSIO.DataIn * 0x0100; State.MemCard.Check = State.PSIO.DataIn; break; } case 3 : { State.MemCard.Sector += State.PSIO.DataIn; State.MemCard.Check ^= State.PSIO.DataIn; // break; } State.PSIO.Mode = mdWrData; State.MemCard.Bytes = 0; break; } default : { State.PSIO.Mode = mdDone; AckTime = 0; break; } } // LED_RED_ON; // break; } // : S case 0x53 : { // switch ( State.MemCard.Bytes ) { // case 0 : { State.PSIO.DataOut = 0x5D; break; } case 1 : { State.PSIO.DataOut = 0x5C; break; } case 2 : { State.PSIO.DataOut = 0x5D; break; } case 3 : { State.PSIO.DataOut = 0x04; break; } case 4 : { State.PSIO.DataOut = 0x00; break; } case 5 : { State.PSIO.DataOut = 0x00; break; } case 6 : { State.PSIO.DataOut = 0x80; break; } default : { State.PSIO.Mode = mdDone; AckTime = 0; break; } } // break; } // default : { State.PSIO.Mode = mdDone; break; } } // if ( State.PSIO.Mode == mdParam ) { State.MemCard.Bytes++; } // break; } // case mdRdData : { // ACK AckTime = AckNormal; // if ( State.MemCard.Bytes < 128 ) { // State.PSIO.DataOut = State.MemCard.Data[ State.MemCard.Bytes ]; State.MemCard.Check ^= State.PSIO.DataOut; } else { // switch ( State.MemCard.Bytes ) { // case 128 : { State.PSIO.DataOut = State.MemCard.Check; break; } // case 129 : { State.PSIO.DataOut = 0x47; break; } // default : { State.PSIO.Mode = mdDone; AckTime = 0; LED_GREEN_OFF; break; } } } // State.MemCard.Bytes++; // break; } // case mdWrData : { // ACK AckTime = AckNormal; // if ( State.MemCard.Bytes < 128 ) { // State.MemCard.Data[ State.MemCard.Bytes ] = State.PSIO.DataIn; State.MemCard.Check ^= State.PSIO.DataIn; } else { // switch ( State.MemCard.Bytes ) { // case 128 : { // if ( State.MemCard.Check == State.PSIO.DataIn ) { State.MemCard.Check = 0x47; } else { State.MemCard.Check = 0x4E; } // State.PSIO.DataOut = 0x5C; // break; } // case 129 : { State.PSIO.DataOut = 0x5D; break; } // case 130 : { // , if ( State.MemCard.Sector < 0x4000 ) { // , State.PSIO.DataOut = State.MemCard.Check; // ? if ( State.MemCard.Check == 0x47 ) { // State.SDCard.CardOp = coWrite; // State.MemCard.Status &= ~StateNew; } } else { // , State.PSIO.DataOut = 0xFF; } // break; } // default : { State.PSIO.Mode = mdDone; AckTime = 0; break; } } } // State.MemCard.Bytes++; // break; } // , case mdDone : { break; } // - default : { State.PSIO.Mode = mdSync; break; } } } } // if ( State.PSIO.Mode != mdSync ) { // if ( State.PSIO.DataOut & 0x01 ) { // 1 MEM_DAT1; } else { // 0 MEM_DAT0; } // State.PSIO.DataOut >>= 1; } // ACK? if ( AckTime > 0 ) { // CNT TIM3->CNT = AckTime; // State.PSIO.Ack = DISABLE; // TIM3->SR = 0x0000; // TIM3->CR1 |= TIM_CR1_CEN; } }
Le temporisateur TIM3 sera responsable de la génération de l' ACK . Ceci est nécessaire pour que le noyau soit libre de fonctionner avec la carte SD pendant ce délai. Le gestionnaire d'interruption du minuteur est comme ceci:
TIM3_IRQHandler ()
// TIM3 void TIM3_IRQHandler( void ) { // TIM3->SR = 0x0000; // if ( State.PSIO.Ack == ENABLE ) { // ACK MEM_nACK; } else { // ACK MEM_ACK; // State.PSIO.Ack = ENABLE; // TIM3->CNT = 0; // TIM3->CR1 |= TIM_CR1_CEN; } }
Le code est suffisamment commenté et je pense qu'il ne nécessite pas beaucoup d'analyse. Je noterai seulement qu'après avoir reçu le deuxième octet du numéro de secteur dans la commande de lecture, nous définissons le drapeau pour l'opération de lecture à partir de la carte SD pour le code qui tourne dans la boucle éternelle de la fonction main (). Et immédiatement après cela, les 4 ACK suivants sont émis avec une durée prolongée. Dans l'interface, cela ressemble à ceci:
Capture d'écran du programme de l'analyseur logique, 4 grands retards dans la transaction sont mis en évidence
Au total, environ 3,5 ms sont tapés, ce qui est largement suffisant pour que l'algorithme du code principal lise le secteur. De plus, ce code ne peut fonctionner que lorsqu'il n'y a pas d'interruption, c'est-à-dire juste dans ces grandes pauses. Pendant l'enregistrement, le drapeau est placé à la toute fin et du fait que le système permet à la carte mémoire de terminer l'enregistrement, le code principal fonctionne sans interférence des interruptions. Jetons maintenant un œil au code de la boucle principale.
principale ()
// while ( 1 ) { // if ( CARD_nCD == 0 ) { // if ( State.SDCard.CardType == ctNone ) { // LED_GREEN_ON; LED_RED_OFF; // , State.SDCard.CardType = Card_Init(); // ? if ( State.SDCard.CardType != ctUnknown ) { // if ( Card_FSInit( &State.SDCard, &CARD_IMAGE[ 0 ] ) == ENABLE ) { // , EXTI->IMR |= 0x00000004; // LED_GREEN_OFF; LED_RED_OFF; } else { // State.SDCard.CardType = ctUnknown; // LED_GREEN_ON; LED_RED_ON; } } else { // LED_GREEN_ON; LED_RED_ON; } } } else { // if ( State.SDCard.CardType != ctNone ) { // , PSIO EXTI->IMR &= 0xFFFFFFFA; // State.PSIO.Mode = mdSync; State.PSIO.Bits = 0; State.PSIO.DataIn = 0x00; State.PSIO.DataOut = 0; State.PSIO.Ack = DISABLE; State.MemCard.Status = StateNew; State.SDCard.CardType = ctNone; State.SDCard.CardOp = coIdle; State.SDCard.LoadedLBA = 0xFFFFFFFF; } // LED_GREEN_OFF; LED_RED_OFF; } // if ( (State.SDCard.CardType != ctNone) && (State.SDCard.CardType != ctUnknown) ) { // ? if ( State.SDCard.CardOp == coWrite ) { // Ofs = State.MemCard.Sector & 0x03FF; LBA = (Ofs >> 2) & 0x000000FF; Ofs = (Ofs << 7) & 0x00000180; // Card_Read( State.SDCard.CardType, &State.SDCard.CardBuf[ 0 ], &State.SDCard.LoadedLBA, State.SDCard.CardList[ LBA ] ); // for (Cnt = 0;Cnt < 128;Cnt++) { // State.SDCard.CardBuf[ Ofs + Cnt ] = State.MemCard.Data[ Cnt ]; } // Card_Write( State.SDCard.CardType, &State.SDCard.CardBuf[ 0 ], &State.SDCard.LoadedLBA, State.SDCard.CardList[ LBA ] ); // LED_RED_OFF; // State.SDCard.CardOp = coIdle; } // ? if ( State.SDCard.CardOp == coRead ) { // Ofs = State.MemCard.Sector & 0x03FF; LBA = (Ofs >> 2) & 0x000000FF; Ofs = (Ofs << 7) & 0x00000180; // Card_Read( State.SDCard.CardType, &State.SDCard.CardBuf[ 0 ], &State.SDCard.LoadedLBA, State.SDCard.CardList[ LBA ] ); // for (Cnt = 0;Cnt < 128;Cnt++) { // State.MemCard.Data[ Cnt ] = State.SDCard.CardBuf[ Ofs + Cnt ]; } // State.SDCard.CardOp = coIdle; } } }
Dans une boucle éternelle, le signal d'insertion de la carte SD est constamment analysé. Si vous le retirez lors de vos déplacements, le code désactivera le PSIO et la PS1 «perdra» la carte. Si la carte est réinsérée (ou juste allumée avec la carte insérée), alors dans un premier temps, il y aura une tentative d'initialisation de la carte avec la fonction Card_Init (), qui retournera le type de la carte détectée. Ceci est important car SDv1 et les autres méthodes d'adressage SDHC / SDXC sont différentes. Le code d'initialisation lui-même ne porte aucun secret et a été espionné dans un tas d'exemples disponibles sur Internet à propos de FatFS et de projets similaires.
Suite à l'initialisation de la carte, la fonction délicate Card_FSInit () est appelée. C'est la caractéristique la plus importante de ce projet. Le fait est que STM32F042 a des capacités modestes et ne sera pas en mesure de tirer le support complet de FatFS à la vitesse requise. Par conséquent, j'ai proposé cette méthode: le fichier image est toujours de 128 Ko, par conséquent, vous n'avez besoin de connaître que 256 secteurs de 512 octets, dont chacun aura exactement 4 secteurs de notre carte mémoire PS1. Ainsi, nous faisons ce qui suit:
- Nous analysons le secteur LBA = # 0 pour MBR. S'il s'agit bien du MBR, alors nous obtenons un nouveau secteur où se trouve le MBS.
- Après avoir reçu l'adresse du MBS supposé (cela peut être # 0, s'il n'y a pas de MBR, ou un certain nombre, s'il y a MBR), nous commençons à l'analyser pour l'appartenance à l'un des FAT: FAT12, FAT16, FAT32 ou vFAT .
- Si le secteur a réussi le contrôle, nous en prenons des informations sur la structure et cherchons un élément avec le nom de fichier dans le répertoire racine. Dans ce cas, il s'agit de «MEMCRD00.BIN».
- Si un tel fichier est trouvé, nous vérifions sa taille - il doit être strictement fixé à 0x20000 octets. Si tout est ainsi, nous obtenons le numéro du premier cluster.
- Si nous avons atteint ce point, alors nous avons déjà toutes les informations nécessaires pour construire une liste de secteurs LBA physiques où se trouve notre image. En parcourant la chaîne FAT et en utilisant les informations de structure du MBS, nous remplissons un tableau de 256 numéros de secteur LBA.
En cas de succès, PSIO démarre et PS1 verra la carte comme sa carte habituelle à 15 blocs. Si une erreur se produit à un stade quelconque, le fonctionnement est interrompu, les deux voyants s'allument et tout reste dans cet état jusqu'à ce que l'alimentation soit coupée ou que la carte SD soit remplacée. Voici le code de cette procédure:
Card_FSInit ()
// , FAT16 FunctionalState Card_FSInit( TSDCard *SDCard, const uint8_t *FName ) { // FunctionalState Res; uint8_t *Buf; uint8_t Pos; uint16_t ClustSize,Reserv,RootSize,FATSize,Cluster; uint32_t Cnt,LBA,SysOrg,FATOrg,RootOrg,DataOrg; int Compare; // Res = DISABLE; SysOrg = 0; Cluster = 0xFFFF; // while ( 1 ) { // 0 if ( Card_Read( SDCard->CardType, &SDCard->CardBuf[ 0 ], &SDCard->LoadedLBA, SysOrg ) == DISABLE ) { break; } // #0 MBR if ( *((uint16_t *)&SDCard->CardBuf[ 0x01FE ]) != 0xAA55 ) { break; } // MBR if ( ((SDCard->CardBuf[ 0x01BE ] == 0x00) || (SDCard->CardBuf[ 0x01BE ] == 0x80)) && ((SDCard->CardBuf[ 0x01CE ] == 0x00) || (SDCard->CardBuf[ 0x01CE ] == 0x80)) && ((SDCard->CardBuf[ 0x01DE ] == 0x00) || (SDCard->CardBuf[ 0x01DE ] == 0x80)) && ((SDCard->CardBuf[ 0x01EE ] == 0x00) || (SDCard->CardBuf[ 0x01EE ] == 0x80)) ) { // MBR, for (Cnt = 0;Cnt < 4;Cnt++) { // if ( (SDCard->CardBuf[ (Cnt * 0x0010) + 0x01C2 ] == 0x01) || // 0x01: FAT12 (SDCard->CardBuf[ (Cnt * 0x0010) + 0x01C2 ] == 0x04) || // 0x04: FAT16 (SDCard->CardBuf[ (Cnt * 0x0010) + 0x01C2 ] == 0x06) || // 0x06: Big FAT16 (SDCard->CardBuf[ (Cnt * 0x0010) + 0x01C2 ] == 0x0E) ) // 0x0E: vFAT { // , MBS SysOrg = SDCard->CardBuf[ (Cnt * 0x0010) + 0x01C6 ]; SysOrg += (SDCard->CardBuf[ (Cnt * 0x0010) + 0x01C7 ] * 0x00000100); SysOrg += (SDCard->CardBuf[ (Cnt * 0x0010) + 0x01C8 ] * 0x00010000); SysOrg += (SDCard->CardBuf[ (Cnt * 0x0010) + 0x01C9 ] * 0x01000000); // break; } } } // MBS if ( Card_Read( SDCard->CardType, &SDCard->CardBuf[ 0 ], &SDCard->LoadedLBA, SysOrg ) == DISABLE ) { break; } // MBS if ( *((uint16_t *)&SDCard->CardBuf[ 0x01FE ]) != 0xAA55 ) { break; } if ( SDCard->CardBuf[ 0x000D ] == 0x00 ) { break; } if ( (SDCard->CardBuf[ 0x0010 ] == 0x00) || (SDCard->CardBuf[ 0x0010 ] > 0x02) ) { break; } if ( SDCard->CardBuf[ 0x0015 ] != 0xF8 ) { break; } if ( *((uint32_t *)&SDCard->CardBuf[ 0x001C ]) != SysOrg ) { break; } if ( SDCard->CardBuf[ 0x0026 ] != 0x29 ) { break; } if ( *((uint16_t *)&SDCard->CardBuf[ 0x0036 ]) != 0x4146 ) { break; } if ( *((uint16_t *)&SDCard->CardBuf[ 0x0038 ]) != 0x3154 ) { break; } if ( SDCard->CardBuf[ 0x003A ] != 0x36 ) { break; } // , ClustSize = SDCard->CardBuf[ 0x000D ]; Reserv = *((uint16_t *)&SDCard->CardBuf[ 0x000E ]); RootSize = (SDCard->CardBuf[ 0x0012 ] * 0x0100) + SDCard->CardBuf[ 0x0011 ]; FATSize = *((uint16_t *)&SDCard->CardBuf[ 0x0016 ]); // FAT ROOT FATOrg = SysOrg + Reserv; RootOrg = FATOrg + (FATSize * 2); DataOrg = RootOrg + (RootSize / 16 ); // , for (LBA = 0;LBA < (RootSize / 16);LBA++) { // if ( Card_Read( SDCard->CardType, &SDCard->CardBuf[ 0 ], &SDCard->LoadedLBA, RootOrg + LBA ) == ENABLE ) { // 16 , for (Cnt = 0;Cnt < 16;Cnt++) { // Compare = memcmp( &SDCard->CardBuf[ Cnt * 32 ], &CARD_IMAGE[ 0 ], 11 ); if ( Compare == 0 ) { // , if ( *((uint32_t *)&SDCard->CardBuf[ (Cnt * 32) + 0x001C ]) == 0x00020000 ) { // , Cluster = *((uint16_t *)&SDCard->CardBuf[ (Cnt * 32) + 0x001A ]); // Res = ENABLE; // break; } } } // - if ( Res == ENABLE ) { break; } } else { // - break; } } // , , if ( Res == ENABLE ) { // , Pos = 0; do { // if ( Cluster < 0x0002 ) { // , Res = DISABLE; break; } // LBA LBA = DataOrg + ((Cluster - 2) * ClustSize); // for (Cnt = 0;Cnt < ClustSize;Cnt++) { // LBA SDCard->CardList[ Pos ] = LBA + Cnt; // Pos++; if ( Pos == 0 ) { break; } } // , // , if ( Pos != 0 ) { // LBA = FATOrg; Reserv = Cluster; while ( Reserv > 256 ) { LBA++; Reserv -= 256; } // if ( Card_Read( SDCard->CardType, &SDCard->CardBuf[ 0 ], &SDCard->LoadedLBA, LBA ) == ENABLE ) { // Cluster = *((uint16_t *)&SDCard->CardBuf[ Reserv * 2 ]); } else { // Res = DISABLE; break; } } } while ( (Cluster != 0xFFFF) && (Pos != 0) ); } // break; } // return Res; }
Pour être honnête, puisqu'il ne s'agit que de PoC, seule la recherche FAT16 est implémentée ici. FAT12 n'a probablement pas besoin d'être pris en charge - la microSD de si petits volumes n'existe pas. Mais FAT32 ou vFAT peuvent être ajoutés si quelqu'un en a besoin à l'avenir.
Le nom de l'image 'MEMCRD00.BIN' n'a pas été choisi par hasard. Le fait est qu'à l'avenir, je prévois d'ajouter une sélection d'image via une combinaison standard de boutons sur le joypad pour les cartes mémoire multipages: lorsque SELECT est maintenu enfoncé, une simple pression sur L1 / R1 suit. Et en changeant les 2 derniers caractères, vous pouvez prendre en charge 100 images dans le répertoire racine, de «MEMCRD00.BIN» à «MEMCRD99.BIN». Il y a une base pour cela dans le gestionnaire d'interruption SCKdans l'interface PSIO, la branche où l'appel au joypad est analysé. Il n'y a pas de problème pour faire un renifleur, mais les périphériques du contrôleur PS1 sont riches et vous devrez les supporter presque tous.
En conséquence, l'appareil s'est avéré efficace et chacun peut le répéter s'il le souhaite. Le lien vers l'ensemble du projet est ici. Je serai heureux d'aider toutes les personnes intéressées par les commentaires de l'article.
PS Je voudrais bien indiquer ici une liste de toutes les sources d'informations que j'ai utilisées pour créer ce projet, mais hélas, c'est très difficile. Beaucoup de choses ont été entendues par hasard. Quelque chose est venu sous la forme de fichiers TXT avec des informations générales sur la PS1 il y a plus de 15 ans, pour ceux qui voulaient écrire leur propre émulateur. Et maintenant, tout existe sous forme de quelques fichiers texte sur mon disque dur. On peut dire que tout Internet est la source d'informations depuis 15 ans.