PS1 (também conhecido como PSX, também conhecido como PS One) é a primeira geração de consoles de jogos PlayStation da Sony e pertence à quinta geração de consoles de jogos em geral. Ele usa uma unidade de velocidade 2x para ler CDs. Uma quantidade tão grande de dados, pelos padrões da época atual para o console, permitiu que os desenvolvedores de jogos não olhassem particularmente para as restrições na criação de conteúdo para jogos, o que tornou este último de maior qualidade em comparação com os jogos da geração anterior de consoles. Além disso, os jogos agora podem ser longos. E se algum jogo, com raras exceções, em consoles de gerações anteriores pudesse ser concluído em uma sessão de jogo, então com os jogos PS1 tudo era diferente. Para salvar o progresso, o PlayStation tem cartões de memória: pequenos módulos de memória removíveis e não voláteis.
Se você está se perguntando como funciona exatamente o cartão de memória do PlayStation 1, como funciona e como você pode criar o seu próprio, bem-vindo ao gato.
Portanto, o cartão de memória PS1 é um dispositivo periférico padrão, como todo o zoológico de joypads, joysticks e outros acessórios. Para entender exatamente como funciona, primeiro você precisa ver o que ele contém.
Foto da placa de circuito impresso de um cartão de memória de 15 blocos padrão
Como você pode ver pela foto, o dispositivo do cartão é muito simples: um controlador que atende as solicitações do sistema, e, na verdade, memória não volátil, que é representada pelo padrão NOR FLASH. Logicamente, o cartão de memória é dividido em 15 blocos que os jogos podem usar. Pode parecer que 15 não é lógico para um sistema binário, mas não há contradição aqui: um bloco é dado para o sistema de arquivos, nomes de arquivos e até ícones animados são armazenados lá, assim como fluxos NTFS. Cada bloco tem o tamanho de 8 KiB, 16 blocos no total são 128 KiB, o que pode ser visto na marcação da memória FLASH na foto acima.
No início, isso bastava para todos, mas depois começaram a aparecer jogos que usavam mais de um bloco por vez. Por exemplo, alguns simuladores, como Sega GT, usam 4-5 blocos, enquanto o Construtorentão, em geral, todo o cartão de memória tem 15 blocos. Isso obrigou a comprar mais cartões e a situação ameaçou se tornar como disquetes ou cartuchos. Mas então os piratas pararam e começaram a emitir cartões de 2, 4 ou 8 páginas de uma vez. E as páginas foram trocadas por uma combinação inteligente no joypad ou por um botão explícito no próprio cartão de memória. No entanto, em cartões com mais de 2 páginas, a compressão era usada, e o número real de páginas era muito menor, e alguns cartões podiam ser bloqueados de forma estúpida. E foi muito difícil tirá-los desse estado, mas o que os jogadores não fizeram por causa de suas defesas. Aqui estão os representantes típicos de cartões de memória de várias páginas:
À esquerda está um cartão de memória com 2 páginas, à direita com 8. O da direita tem um botão de hardware para virar as páginas e um indicador mostrando o número de 1 a 8, que é escondido atrás de um vidro escuro
Pequena digressão lírica
Tudo começou em 2001, quando comprei um disco milagroso para PC chamado "All Emulators", no qual havia emuladores PS1 incluindo: era Bleem! e o ePSXe anterior. E meu computador era até capaz de reproduzir meus discos PS1 de forma jogável! E um pouco depois eu ganhei um modem e aprendi sobre o DirectPad Pro . Conectar um joystick nativo a um computador (embora via LPT) custa muito. E este sistema funcionou tanto no 9x quanto no XP! E um pouco depois, já em 2002, conheci o Memory Card Capture Sakura! Este programa permitiu trabalhar com cartões de memória reais usando o mesmo esquema de conexão do DirectPad Pro. Foi então que tive a ideia de fazer um cartão de memória "infinito" que me permitisse trocar informações com um computador sem a necessidade de dispositivos adicionais. Mas naquela época eu não tinha informações suficientes e uma base de elementos disponível, e a ideia permaneceu apenas uma ideia, brilhando em algum lugar no quintal de minha consciência.
Quase 9 anos se passaram desde que percebi que já sei o suficiente e tenho a oportunidade de implementar pelo menos alguma versão de um cartão de memória sem fim. No entanto, outro fator entrou em jogo aqui - a idade e tudo o que está relacionado a ela. Há menos tempo para hobbies, mais e mais preocupações. E só agora posso fornecer ao público pelo menos algum resultado, uma Prova de Conceito completa.
Interface física
Portanto, o cartão de memória e os joypads funcionam por meio de uma interface comum. O número de sinais nele é 6, aqui estão seus nomes e objetivos:
- SEL0 - Primeiro sinal de seleção de porta, nível ativo baixo
- SEL1 - Sinal de seleção da segunda porta, nível ativo baixo;
- CLK - Sinal de clock da interface, estado passivo de nível alto, na borda descendente, travando na borda;
- CMD - Sinal de dados do console para a periferia;
- DAT - Sinal de dados da periferia para o console;
- ACK - handshake de hardware, ativo baixo.
Existem também duas tensões de alimentação diferentes na interface: 3,3v e 7,6v. Todos os sinais, exceto SEL0 e SEL1, são comuns a todos os dispositivos conectados. É por isso que um cartão de memória não funcional ou um joypad no segundo slot afetou os trabalhadores no primeiro, embora depois de consoles de 16 bits parecesse estranho. Acho que muitos já reconheceram o SPI padrão na interface - está tudo correto, está. Apenas adicionado um sinal ACK para confirmar a operação de E / S. Aqui estão as atribuições dos sinais nos contatos do cartão de memória:
Cartão de memória reparado com FLASH de 5 volts As
características técnicas da interface são as seguintes:
___ ___________________________ ____
\ / \ /
X X
___/ \___________________________/ \____
___ ____________
\ / \
\ / \
\____________/ \____
| |
| tck |
|<--------------------------->|
+-------+-------+------+-------+
| | . | . | . |
+-------+-------+------+-------+
| tck | 1 | 4 | - |
+-------+-------+------+-------+
ACK:
____
SEL- |______________________________________________
______ __________ ___________
CLK |||||||| |||||||| ||||||||
| |
ACK- -----------------------|_|-------------|_|---------
| ta1 | | | ta2 |
|<------->| | |<----->|
| | ap
>|-|<-----
+-----+------+-------+--------+
| | . | . | . |
+-----+------+-------+--------+
| ta1 | 0 | - | 100 | -
+-----+------+-------+--------+
| ta2 | | 10 | 1 |
+-----+------+-------+--------+
| ap | 2 | | | ACK
+-----+------+-------+--------+
A frequência medida do sinal CLK é 250 kHz, que é 4 µs por ciclo. Com os parâmetros físicos da interface resolvidos, agora a camada de transporte. Um engenheiro experiente já notou que o joypad e o cartão de memória estão conectados completamente em paralelo e podem entrar em conflito um com o outro. Na verdade, há arbitragem de software para isso. Depois que o sinal SELn é ativado, a periferia permanece em silêncio, mas escuta o primeiro byte enviado. Se este byte for igual a 0x01, o joypad é ativado e o cartão de memória permanece em silêncio até que o sinal de seleção seja desativado. E se o byte era 0x81, então o oposto é verdadeiro: o cartão de memória é ativado e o joypad fica em silêncio. Naturalmente, o host está esperando por um sinal ACKneste byte de arbitragem e não espere muito. Isso é necessário para ter tempo de interrogar o resto da periferia, se parte dessa periferia estiver ausente. O fato é que o sistema operacional pesquisa controladores e cartões de memória estritamente de acordo com o sinal do caminho reverso do feixe, ou mais conhecido como VBlank . Aceita-se que os jogos em consoles até a 5ª geração estejam vinculados a esse tempo, que é igual à taxa de quadros. E a taxa de quadros é estritamente estável e normalizada: 50 Hz para PAL e 60 Hz para NTSC. Ou seja, o período de votação para joysticks e cartões de memória é de 20 ms para PAL ou 16 ms para NTSC.
Então, descobrimos a arbitragem, agora o nível superior real. Que comandos o cartão de memória PS1 padrão entende? Sim, na verdade, existem apenas 3 deles.
- R - 0x52 ou Leitura . Lendo um setor de um cartão de memória;
- W - 0x57 ou Write . Gravação do setor do cartão de memória;
- S - 0x53 ou Status . Lendo o status do cartão de memória.
Todo o cartão de memória está dividido em setores. Um setor de 128 bytes. Assim, 128 KiB se encaixa 0x400 ou 1024 setores. Neste caso, você não precisa apagar o setor antes de gravar. Mas o sistema tem a garantia de dar tempo para o próximo quadro inteiro durante a gravação. Ou seja, ele pode ler o cartão de memória a cada quadro, mas grava após um. A propósito, todos os tipos de "Codebreakers" não aderem a esses horários para aumentar a velocidade. Vamos analisar cada comando com mais detalhes.
Protocolo de cartão de memória
A ordem dos dados transmitidos em cada comando é assim:
Leitura:
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
Legenda:
CMD - Dados que o host envia ao cartão.
DAT - Dados que o cartão envia ao host.
FLAG - Sinalizadores atuais do estado do mapa e o resultado do comando anterior.
PRV - Dados anteriores recebidos, resultado da simplificação do circuito no mapa.
MSB - Byte alto do número do setor.
LSB - Byte menos significativo do número do setor.
DADOS - Carga útil.
CHK - Dígito verificador do bloco.
ACK - Sinalizador de confirmação.
O byte FLAG usa os seguintes bits:
- D5 – Sony. .
- D3 – . .
- D2 – , .
Depois de ligar, FLAG é 0x08. E após o primeiro registro, ele é zerado. O sistema operacional PS1 sempre grava no setor 0x003F para isso, causando desgaste nesse setor. Mas no âmbito da marcação do cartão de memória pelo sistema, não há informações úteis neste setor. Número do setor MSB : LSB 10 bits e varia de 0x0000 a 0x03FF. A soma de verificação CHK é o XOR usual de todos os 128 bytes de dados + MSB e LSB . A confirmação ACK pode ter apenas 3 valores: G 0x47, E 0x43 e 0xFF. G = Bom ou OK. E = Erro . Na verdade, ao ler o cartão, ACK é sempre igual a G , e ao escrever G = OK, E = erro de checksum e 0xFF significa um número de setor incorreto. É verdade que a maioria das placas simplesmente descarta os bits não utilizados no byte alto do número do setor e, portanto, nunca responde com 0xFF. Os números 0x0400 e 0x0080 no comando de status sugerem certos pensamentos de que este é o número de setores e o tamanho do setor em bytes, mas isso não é conhecido com certeza. Bem, aqui estamos e chegamos ao ponto principal:
Percebendo o seu cartão de memória
Portanto, esta é toda a informação de que você precisa para criar seu cartão de memória PS1. Os gargalos potenciais são os seguintes:
- Durante a leitura, leva tempo para atualizar os dados. Entre o número do setor e a transferência de dados real, temos 4 bytes dos quais podemos esticar um pouco o ACK . A propósito, para o cartão de memória original em NOR FLASH, todos os ACKs vão uniformemente, para cartões de memória com SPI FLASH, após a transmissão de LSB , há um atraso de ACK , durante o qual o controlador configura o comando para SPI FLASH e lê o primeiro byte e lê o resto durante a troca.
- Ao gravar, após a transferência de todo o pacote e o início da gravação para o array, leva tempo, mas aqui o próprio sistema dá o atraso necessário.
Quanto à fonte de alimentação, os joypads de 3,3 V são usados para lógica e 7,6 V para alimentar os motores. Os cartões de memória geralmente usam apenas uma fonte de alimentação. Se houver FLASH de 5v dentro, então 7,6v e um estabilizador são usados. Se houver FLASH de 3,3v, então 3,3v será usado imediatamente.
A primeira versão que montei no STM32F407VG, que é alimentado por 3,3V, tem SPI para PSIO, SDIO rápido e memória suficiente para armazenar toda a imagem dentro dela mesma, resolvendo os problemas mencionados. Foto do dispositivo acabado:
a primeira versão do meu cartão de memória no STM32F407
Ficou rápido, confiável, mas caro. Você pode fazer isso mais barato? Bem, bem, o desafio é aceito. Considerando as especificidades da tarefa, escolhi STM32F042F6. Aqui está o que aconteceu:
Segunda versão do meu cartão de memória em STM32F042
Nossa placa é acionada, então a estabilização de frequência com um ressonador de quartzo externo não é necessária, um oscilador interno é suficiente. Este controlador tem um SPI de hardware, então eu o entreguei ao cartão SD para reduzir os atrasos no transporte. PSIO será o software aqui.
Implementação de software
A primeira coisa a fazer é trabalhar com um cartão SD no modo SPI. Não vou me alongar muito sobre isso, há muito que foi mastigado e espalhado pela Internet. O código de inicialização, leitura e gravação do setor é fornecido abaixo.
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; }
A placa inicializa a 375 kHz (PCLK / 128) e opera a 24 MHz (PCLK / 2). Em tais velocidades, as medições mostraram que SDv1 e SDHC fornecem um setor dentro de 2,8 ms para toda a transação. Isso deve ser lembrado porque importante para a operação de leitura PSIO.
Agora vamos dar uma olhada em PSIO. Como mencionado acima, nós o temos no software de qualquer maneira. Existem apenas dois sinais para rastrear: SEL e CLK . Vamos rastrear o primeiro em ambas as frentes e fazer os preparativos para a troca de dados:
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; }
Capturaremos apenas o sinal CLK na frente. O fato é que STM32F042 opera apenas a 48 MHz e seu desempenho é muito pequeno para nossa tarefa. E se você fizer uma interrupção nas duas frentes, então durante a transferência de um byte, praticamente não sai do manipulador de interrupções e tudo funciona bem no limite da possibilidade, às vezes falhando. E se você reage apenas na frente, e o trabalho que precisa ser feito no declínio é feito no final da interrupção, então está tudo bem em menos de 55% do período CLK , porque vários cheques podem ser rejeitados . Tenho certeza de que, se esse manipulador for escrito em assembler da forma mais otimizada possível, ele será capaz de funcionar até mesmo em ambos os saltos. Aqui está o código do manipulador:
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; } }
O temporizador TIM3 será o responsável por gerar o ACK . Isso é necessário para que o kernel esteja livre para trabalhar com o cartão SD durante este atraso. O manipulador de interrupção do timer é assim:
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; } }
O código já está bastante comentado e acho que não precisa de muita análise. Observarei apenas que, após receber o segundo byte do número do setor no comando de leitura, definimos o sinalizador para a operação de leitura do cartão SD para o código que gira no loop eterno da função main (). E imediatamente depois disso, os próximos 4 ACKs são emitidos com um tempo estendido. Na interface, tem a seguinte aparência:
Captura de tela do programa analisador lógico, 4 grandes atrasos na transação são destacados
No total, cerca de 3,5ms são digitados, e isso é mais do que suficiente para o algoritmo no código principal ler o setor. Além disso, este código só pode funcionar quando não há interrupção, ou seja, apenas nessas grandes pausas. Durante a gravação, o sinalizador é colocado bem no final e devido ao fato de o sistema permitir que o cartão de memória conclua a gravação, o código principal funciona sem interferência de interrupções. Agora, vamos dar uma olhada no código do loop principal.
a Principal ()
// 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; } } }
Em um loop eterno, o sinal de inserção do cartão SD é constantemente analisado. Se você retirá-lo em trânsito, o código desabilitará o PSIO e o PS1 "perderá" o cartão. Se o cartão for inserido de volta (ou apenas ligar com o cartão inserido), então inicialmente haverá uma tentativa de inicializar o cartão com a função Card_Init (), que retornará o tipo do cartão detectado. Isso é importante porque SDv1 e outros métodos de endereçamento SDHC / SDXC são diferentes. O código de inicialização em si não contém nenhum segredo e foi espiado em uma pilha de exemplos disponíveis na Internet sobre FatFS e projetos semelhantes.
Após a inicialização do cartão, a função complicada Card_FSInit () é chamada. Esta é a característica mais importante deste projeto. O fato é que o STM32F042 é modesto em recursos e não será capaz de obter suporte completo do FatFS na velocidade necessária. Portanto, eu vim com este método: o arquivo de imagem é sempre 128 KiB, portanto, você precisa saber apenas 256 setores de 512 bytes, cada um dos quais terá exatamente 4 setores de nosso cartão de memória PS1. Assim, fazemos o seguinte:
- Analisamos o setor LBA = # 0 para MBR. Se este for realmente o MBR, então temos um novo setor onde o MBS está localizado.
- Tendo recebido o endereço do suposto MBS (pode ser # 0, se não houver MBR, ou algum número, se houver MBR), passamos a analisá-lo por pertencer a um dos FATs: FAT12, FAT16, FAT32 ou vFAT .
- Se o setor foi aprovado na verificação, obtemos informações sobre a estrutura dele e procuramos um elemento com o nome do arquivo no diretório raiz. Nesse caso, é 'MEMCRD00.BIN'.
- Se esse arquivo for encontrado, verificamos seu tamanho - ele deve ser estritamente fixado em 0x20000 bytes. Se tudo estiver certo, obtemos o número do primeiro cluster.
- Se chegamos a esse ponto, já temos todas as informações necessárias para construir uma lista dos setores físicos do LBA onde está localizada nossa imagem. Percorrendo a cadeia FAT e usando as informações da estrutura do MBS, preenchemos uma tabela de 256 números de setor do LBA.
Se for bem-sucedido, o PSIO será iniciado e o PS1 verá a placa como sua placa normal de 15 blocos. Se ocorrer um erro em qualquer estágio, a operação é interrompida, ambos os LEDs acendem e tudo permanece neste estado até que a alimentação seja removida ou o cartão SD seja substituído. Aqui está o código para este procedimento:
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; }
Para ser honesto, como se trata apenas de PoC, apenas a pesquisa FAT16 é implementada aqui. Provavelmente, o FAT12 não precisa ser suportado - microSD de volumes tão pequenos não existe. Mas o FAT32 ou o vFAT podem ser adicionados se alguém precisar deles no futuro.
O nome da imagem 'MEMCRD00.BIN' não foi escolhido por acaso. O fato é que, no futuro, pretendo adicionar a seleção de imagens por meio de uma combinação padrão de botões no joypad para cartões de memória de várias páginas: quando SELECT é pressionado, um único toque em L1 / R1 segue. E alterando os últimos 2 caracteres, você pode suportar 100 imagens no diretório raiz, de 'MEMCRD00.BIN' a 'MEMCRD99.BIN'. Há uma base para isso no manipulador de interrupção SCKna interface PSIO, o ramal onde a chamada para o joypad é analisada. Não há problema em fazer um farejador, mas os periféricos do controlador PS1 são ricos e você terá que oferecer suporte a quase todos eles.
Como resultado, o dispositivo acabou sendo eficiente e todos podem repeti-lo se quiserem. O link para todo o projeto está aqui. Terei o maior prazer em ajudar todos os interessados nos comentários ao artigo.
PS Eu realmente gostaria de indicar aqui uma lista de todas as fontes de informação que usei na criação deste projeto, mas, infelizmente, isso é muito difícil. Muito foi ouvido por acaso. Algo veio na forma de arquivos TXT com informações gerais sobre o PS1 há mais de 15 anos, para quem queria escrever seu próprio emulador. E agora tudo existe como alguns arquivos de texto no meu disco rígido. Podemos dizer que toda a Internet é fonte de informação há 15 anos.