Русский
Русский
English
Статистика
Реклама

Ps1

Делаем бесконечную карту памяти для PS1

22.04.2021 20:09:52 | Автор: admin

PS1 (она же PSX, она же PS One) это первое поколение игровых консолей PlayStation от Sony и относится к пятому поколению игровых консолей вообще. Она использует 2х скоростной привод для чтения CD. Такой большой объём данных по меркам актуального для приставки времени позволял игроделам особо не оглядываться на ограничения при создании контента для игр, что делало последних более качественными, по сравнению с играми предыдущего поколения приставок. А ещё, игры теперь могут быть длинными. И если любая игра за редким исключением на консолях предыдущих поколений вполне себе могла быть пройдена за одну игровую сессию, то с играми PS1 всё обстояло иначе. Для сохранения прогресса у PlayStation предусмотрены карты памяти: маленькие сменные модули энергонезависимой памяти.

Если вам интересно, как именно устроена карта памяти PlayStation 1, как она работает и как можно создать свою добро пожаловать под кат.

Итак, карта памяти PS1 является стандартным периферийным устройством, как и весь зоопарк джойпадов, джойстиков и прочих аксессуаров. Чтобы понять, как именно она работает, нужно для начала посмотреть, что у неё внутри.


Фотография печатной платы стандартной карты памяти на 15 блоков

Как видно из фото, устройство карты очень простое: контроллер, который обслуживает запросы системы, и, собственно, энергонезависимая память, которая представлена стандартной NOR FLASH. Логически, карта памяти разбита на 15 блоков, которые игры могут использовать. Может показаться, что 15 не логично для двоичной системы, но тут противоречия нет: один блок отдан под файловую систему, там хранятся имена файлов и даже анимированные иконки, прям как потоки у NTFS. Каждый блок имеет размер 8 КиБ, 16 блоков в сумме это 128 КиБ, что и видно по маркировке FLASH памяти на фото выше.

На первых порах этого хватало всем, но потом стали появляться игры, которые использовали более одного блока за раз. Например, некоторые симуляторы, вроде Sega GT, используют 4-5 блоков, а Constructor так вообще всю карту памяти на 15 блоков. Это вынуждало покупать больше карт и ситуация грозила стать как с дискетами или картриджами. Но потом подтянулись пираты и стали выпускать карты на 2, 4 или 8 страниц разом. И переключались страницы либо по хитрой комбинации на джойпаде, либо явной кнопкой на самой карте памяти. Правда, в картах более 2х страниц применялось сжатие, и фактическое число страниц было значительно меньше, а некоторые карты могли тупо заблокироваться. И вывести их из этого состояния было очень трудно, но на что только не шли игроки ради своих сохранений. Вот типичные представители многостраничных карт памяти:


Слева карта памяти на 2 страницы, справа на 8. У правой есть аппаратная кнопка перелистывания страниц и индикатор, отображающий число от 1 до 8, который скрыт за тёмным стеклом

Небольшое лирическое отступление.


Всё началось в 2001м году, когда я купил чудо диск для ПК под названием Все эмуляторы, на котором были эмуляторы PS1 в том числе: это были Bleem! и ранний ePSXe. И мой тогдашний комп даже смог играбельно запускать мои диски от PS1! А чуть позже у меня появился модем и я узнал про DirectPad Pro. Подключение родного джойстика к компьютеру (пусть и через LPT) многого стоит. И работала эта система как на 9х так и на XP! А ещё чуть позже, уже в 2002м я узнал про Memory Card Capture Sakura! Эта программа позволяла работать с настоящими картами памяти, используя всё ту же схему подключения DirectPad Pro. Именно тогда у меня появилась идея сделать бесконечную карту памяти, которая бы позволяла обмениваться информацией с компьютером без необходимости дополнительных устройств. Но на тот момент у меня не было достаточно информации и доступной элементной базы, и идея оставалась лишь идеей, теплясь где-то на задворках сознания.

Прошло почти 9 лет как я осознал, что уже знаю достаточно и имею возможность, чтобы реализовать хоть какой-то вариант бесконечной карты памяти. Однако тут вступил уже другой фактор возраст и всё что с этим связано. Времени на хобби всё меньше, забот всё больше. И вот только сейчас я могу предоставить общественности хоть какой-то результат, полноценный Proof of Concept.

Физический интерфейс.


Итак, карта памяти и джойпады работают через общий интерфейс. Количество сигналов в нём 6, вот их названия и назначения:
  • SEL0 Сигнал выбора первого порта, активный уровень низкий
  • SEL1 Сигнал выбора второго порта, активный уровень низкий;
  • CLK Тактовый сигнал интерфейса, пассивное состояние высокий уровень, по спаду сдвиг, по фронту защёлкивание;
  • CMD Сигнал данных от консоли к периферии;
  • DAT Сигнал данных от периферии к консоли;
  • ACK Аппаратный хэндшейк, активный уровень низкий.

Так же на интерфейсе присутствует два разных напряжения питания: 3.3в и 7.6в. Все сигналы, кроме SEL0 и SEL1 являются общими для всех подключаемых устройств. Именно поэтому нерабочая карта памяти или джойпад во втором слоту влияли на рабочие в первом, хотя после 16ти битных приставок это казалось странным. Я думаю, что многие уже узнали в интерфейсе стандартный SPI всё верно, так и есть. Только добавлен сигнал ACK для подтверждения операции ввода/вывода. Вот назначения сигналов на контактах карты памяти:


Отремонтированная карта памяти с 5ти вольтовым FLASH

Технические характеристики интерфейса такие:
        ___   ___________________________   ____Данные     \ /                           \ /     или        X                             XКоманда ___/ \___________________________/ \____        ___                  ____________                  \                /            \      Такты       \              /              \                  \____________/                \____            |                             |            |           tck               |            |<--------------------------->|+-------+-------+------+-------+|       | мин.  | тип. | макс. |+-------+-------+------+-------+| tck   | 1мкс  | 4мкс |   -   |+-------+-------+------+-------+Тайминг ACK:     ____                                               SEL-     |______________________________________________     ______        __________        ___________        CLK        ||||||||          ||||||||           ||||||||                  |                 |ACK- -----------------------|_|-------------|_|---------                  |   ta1   | |     |  ta2  |                  |<------->| |     |<----->|                            | |  ap                           >|-|<-----+-----+------+-------+--------+|     | мин. |  тип. |  макс. |+-----+------+-------+--------+| ta1 | 0мкс |   -   | 100мкс | Первый байт-подтверждение+-----+------+-------+--------+| ta2 |      | 10мкс |   1мс  | Остальные+-----+------+-------+--------+|  ap | 2мкс |       |        | Длительность ACK+-----+------+-------+--------+

Измеренная частота сигнала CLK является 250кГц, что составляет 4 мкс на период. С физическими параметрами интерфейса разобрались, теперь транспортный уровень. Опытный инженер уже заметил, что джойпад и карта памяти подключены полностью параллельно и могут конфликтовать между собой. Так и есть, для этого присутствует программный арбитраж. После активации сигнала SELn периферия продолжает молчать, но слушает первый присланный байт. Если этот байт равен 0x01, то далее активируется джойпад, а карта памяти продолжает молчать до деактивации сигнала выбора. А если байт был 0x81, то всё наоборот: карта памяти активируется, а джойпад молчит. Естественно, что хост ждёт сигнала ACK на этот байт арбитража и ждёт недолго. Это нужно для того, чтобы успеть опросить остальную периферию, если часть этой периферии отсутствует. Дело в том, что операционная система опрашивает контроллеры и карты памяти строго по сигналу обратного хода луча, или более известного как VBlank. Так принято, что игры в приставках до 5-го поколения завязаны на этот тайминг, который равен частоте кадров. А частота кадров строго стабильна и нормирована: 50Гц для PAL и 60Гц для NTSC. Т.е., период опроса джойстиков и карт памяти равен 20мс для PAL или 16мс для NTSC.

Итак, с арбитражем разобрались, теперь собственно верхний уровень. Какие команды понимает стандартная карта памяти PS1? Да, собственно, их всего 3.
  • R 0x52 или Read. Чтение сектора карты памяти;
  • W 0x57 или Write. Запись сектора карты памяти;
  • S 0x53 или Status. Чтение статуса карты памяти.

Вся карта памяти разбита на сектора. Один сектор 128 байт. Таким образом, в 128КиБ помещается 0x400 или 1024 сектора. При этом стирать сектор перед записью не нужно. Но система гарантированно даёт время на целый следующий кадр при записи. Т.е., читать карту памяти она может каждый кадр, а записывает через один. К слову, всякие Взломщики кодов для ускорения не придерживаются данных таймингов. Разберём каждую команду более детально.

Протокол работы с картой памяти.


Порядок передаваемых данных в каждой команде выглядит вот так:
Чтение:
CMD 0x81 0x52 0x00 0x00 MSB LSB 0x00 0x00 0x00 0x00 0x00 ... 0x00 0x00 0x00DAT ---- 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 0s5A 0x5D PRV PRV  PRV ...  PRV PRV 0x5C 0x5D  ACK

Статус:
CMD 0x81 0x53 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00DAT ---- FLAG 0x5A 0x5D 0x5C 0x5D 0x04 0x00 0x00 0x80

Легенда:
CMD Данные, которые хост посылает карте.
DAT Данные, которые карта посылает хосту.
FLAG Текущие флаги состояния карты и результат предыдущей команды.
PRV Предыдущие принятые данные, результат упрощения схемы в карте.
MSB Старший байт номера сектора.
LSB Младший байт номера сектора.
DATA Полезные данные.
CHK Контрольная сумма блока.
ACK Флаг подтверждения.

Байт флагов FLAG использует следующие биты:
  • D5 Устанавливается некоторыми картами памяти не от Sony. Назначение неизвестно.
  • D3 Устанавливается при подаче питания и сбрасывается при любой записи. Используется для обнаружения смены карты памяти.
  • D2 Устанавливается при ошибках записи, актуален на следующее обращение после самой операции.

После подачи питания FLAG равен 0x08. И после первой же записи он обнуляется. Операционная система PS1 всегда делает запись в сектор 0x003F для этого, тем самым вызывая износ этого сектора. Но в рамках разметки карты памяти системой какой-либо полезной информации в этом секторе нет. Номер сектора MSB:LSB 10 бит и составляет число от 0x0000 до 0x03FF. Контрольная сумма CHK это обычный XOR всех 128 байт данных + MSB и LSB. Подтверждение ACK может принимать всего 3 значения: G 0x47, E 0x43 и 0xFF. G = Good или ОК. E = Error. Собственно, при чтении из карты ACK всегда равен G, а при записи G = ОК, E = ошибка контрольной суммы а 0xFF означает неправильный номер сектора. Правда, большинство карт просто откидывают неиспользуемые биты в старшем байте номера сектора и поэтому никогда не отвечают 0xFF. Числа 0x0400 и 0x0080 в команде статуса наводят на определённые мысли, что это количество секторов и размер сектора в байтах, но доподлинно это не известно. Ну вот мы и мы подошли к главному:

Реализация своей карты памяти.


Итак, эта вся информация, которая необходима для создания своей карты памяти для PS1. Потенциальные узкие места следующие:
  1. При чтении необходимо время на актуализацию данных. Между номером сектора и фактической передачей данных у нас есть 4 байта, у которых мы можем немного растянуть ACK. К слову, у оригинальной карты памяти на NOR FLASH все ACK идут равномерно, у карт памяти с SPI FLASH после передачи LSB происходит задержка ACK, во время которой контроллер выставляет команду в SPI FLASH и вычитывает первый байт, а остальные он вычитывает по ходу обмена.
  2. При записи после передачи всего пакета и начала самой записи в массив требуется время, но тут система сама даёт необходимую задержку.

Что касается питания, то у джойпадов 3,3в используется для логики а 7,6в для питания моторчиков. У карт памяти обычно используется только одно питание. Если внутри стоит 5в FLASH, то используется 7,6в и стабилизатор. Если стоит 3,3в FLASH, то используется сразу 3,3в.

Первый вариант я собрал на STM32F407VG, который питается от 3,3в, имеет SPI для PSIO, быстрый SDIO и достаточно памяти, чтобы хранить весь образ внутри себя, решая вышеупомянутые проблемы. Фотография готового устройства:


Первая версия моей карты памяти на STM32F407

Получилось быстро, надежно, но дорого. А можно сделать дешевле? Ну, что-ж, вызов принят. Учитывая специфику задачи, я выбрал STM32F042F6. Вот что получилось:


Вторая версия моей карты памяти на STM32F042

Карта у нас ведомая, поэтому стабилизация частоты внешним кварцевым резонатором не нужна, достаточно внутреннего генератора. Аппаратный SPI у этого контроллера один, поэтому я его отдал SD карте, чтобы снизить задержки на транспорт. PSIO тут будет программный.

Программная реализация.


Первое, что надо сделать это работу с SD картой в режиме SPI. Я не буду особо останавливаться на этом, это уже давно разжёвано и растаскано по интернету. Код инита, чтения и записи сектора приведён ниже.
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{// Посылаем 0xFFDat = Card_SPI( 0xFF );} while ( Dat != 0xFF );// CMD0: GO_IDLE_STATECard_SendCMD( &CARD_CMD0[ 0 ], CMD_LENGTH ); Resp = Card_WaitResp( &OCR, DISABLE, 128 );// Какой ответ получен?if ( Resp == 0x01 ){// Карта вошла в IDLE_STATE, посылаем CMD8: SEND_IF_CONDCard_SendCMD( &CARD_CMD8[ 0 ], CMD_LENGTH ); Resp = Card_WaitResp( &OCR, ENABLE, 128 );// Если был дан адекватный респонсif ( Resp != 0x01 ){// Это ветка SDv1/MMCdo{// Посылаем ACMD41: APP_SEND_OP_CONDCard_SendCMD( &CARD_ACMD41[ 0 ], CMD_LENGTH ); Resp = Card_WaitResp( &OCR, ENABLE, 128 );} while ( Resp == 0x01 );// Каков был ответ?if ( Resp == 0x00 ){// Обнаружена карта SD v1Res = ctSD1;}else{// Это ветка MMC, нам её некуда втыкатьRes = ctUnknown;}}else{// Это ветка SDv2if ( (OCR & 0x0001FF) == 0x0001AA ){// Это карта SDv2do{// Посылаем ACMD55: APP_CMDCard_SendCMD( &CARD_CMD55[ 0 ], CMD_LENGTH ); Resp = Card_WaitResp( &OCR, DISABLE, 128 );// Если ответ правильныйif ( Resp == 0x01 ){// Посылаем ACMD41: APP_SEND_OP_CONDCard_SendCMD( &CARD_ACMD41[ 0 ], CMD_LENGTH ); Resp = Card_WaitResp( &OCR, ENABLE, 128 );}} while ( Resp == 0x01 );// Каков был ответ?if ( Resp == 0x00 ){// Посылаем CMD58: READ_OCRCard_SendCMD( &CARD_CMD58[ 0 ], CMD_LENGTH ); Resp = Card_WaitResp( &OCR, ENABLE, 128 );// Каков ответ?if ( Resp == 0x00 ){// Анализируем OCRif ( (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_BLOCKLENCard_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()
// Чтение сектора карты памяти без DMAFunctionalState 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) ){// У старых карт адрес вместо LBAAddr *= 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{// Посылаем 0xFFDat = 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++;}// Дочитываем CRCCmd[ 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()
// Запись сектора карты памяти без DMAFunctionalState 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) ){// У старых карт адрес вместо LBAAddr *= 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{// Посылаем 0xFFDat = 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++;}// Досылаем CRCCard_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;}

Карта инициализируется на скорости 375кГц (PCLK/128), а работает на 24МГц (PCLK/2). При таких скоростях замеры показали, что SDv1 и SDHC отдают сектор в рамках 2,8мс на всю транзакцию полностью. Это следует запомнить, т.к. важно для операции чтения PSIO.

Теперь посмотрим на PSIO. Как было уже сказано выше, он у нас в любом случае программный. Отслеживать надо только два сигнала: SEL и CLK. Первый мы будем отслеживать по обоим фронтам и делать приготовления к обмену данными:
EXTI2_3_IRQHandler()
// Прерывание по перепаду SELvoid EXTI2_3_IRQHandler( void ){// Подтверждаем прерываниеEXTI->PR = 0x00000004;// Анализируем состояние SELif ( MEM_SEL ){// SEL = 1EXTI->IMR &= 0xFFFFFFFE;State.PSIO.Mode = mdSync;// Тушим зелёную лампочкуLED_GREEN_OFF;}else{// SEL = 0EXTI->IMR |= 0x00000001;State.PSIO.Bits = 7;// Тушим лампочкиLED_GREEN_OFF; LED_RED_OFF;}// ОбесточиваемMEM_DAT1; MEM_nACK;}

Сигнал CLK будем ловить только по фронту. Дело в том, что STM32F042 работает всего лишь на 48МГц и его производительность маловата для нашей задачи. И если делать прерывание по обоим фронтам, то во время пересылки байта он практически не вылезает из обработчика прерывания и всё работает прямо на грани возможности, иногда давая сбои. А если реагировать только на фронт, а ту работу, что должна быть сделана по спаду сделать в конце прерывания, то всё отлично успевает меньше, чем за 55% от периода CLK, ведь несколько проверок при этом можно выкинуть. Уверен, что если этот обработчик написать на ассемблере максимально оптимально, то он смог бы работать даже по обоим перепадам. Вот код обработчика:
EXTI0_1_IRQHandler()
// Прерывание по фронту CLKvoid EXTI0_1_IRQHandler( void ){// Подтверждаем прерываниеEXTI->PR = 0x00000001;// Локальные переменныеuint16_t AckTime;// ИнитAckTime = 0;// Считываем данныеState.PSIO.DataIn >>= 1;if ( MEM_CMD ){// Принята 1State.PSIO.DataIn |= 0x80;}else{// Принят 0State.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;// Посылаем ACKAckTime = AckNormal;}elseif ( State.PSIO.DataIn == 0x01 ){// Команда активации джойстика}// Выходbreak;}// Получаем командуcase mdCmd : {// Меняем режимState.PSIO.Mode = mdParam;// Сохраняем байт в команду и подготовим буферState.MemCard.Cmd = State.PSIO.DataIn;State.MemCard.Bytes = 0;// ОтвечаемState.PSIO.DataOut = 0x5A;// Посылаем ACKAckTime = AckNormal;// Выходbreak;}// Режим получения параметровcase mdParam : {// Почти каждый ответ требует ACKAckTime = AckNormal;// Принимаем параметрыswitch ( State.MemCard.Cmd ){// Команда чтения: Rcase 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;}// Команда записи: Wcase 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;}// Команда параметров: Scase 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 : {// Почти каждый ответ требует ACKAckTime = 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 : {// Почти каждый ответ требует ACKAckTime = 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 ){// Выставляем 1MEM_DAT1;}else{// Выставляем 0MEM_DAT0;}// Сдвигаем данныеState.PSIO.DataOut >>= 1;}// Требуется ACK?if ( AckTime > 0 ){// Установим CNTTIM3->CNT = AckTime;// Устанавливаем флагState.PSIO.Ack = DISABLE;// Сбросим событияTIM3->SR = 0x0000;// Включаем таймерTIM3->CR1 |= TIM_CR1_CEN;}}

Таймер TIM3 буде отвечать за генерацию ACK. Это нужно для того, чтобы во время этой задержки ядро было свободно для работы с SD картой. Обработчик прерывания от таймера вот такой:
TIM3_IRQHandler()
// Прерывание таймера TIM3void TIM3_IRQHandler( void ){// Снимаем флагTIM3->SR = 0x0000;// Анализируем режимif ( State.PSIO.Ack == ENABLE ){// Выключаем сигнал ACKMEM_nACK;}else{// Включаем сигнал ACKMEM_ACK;// Перекидываем режимState.PSIO.Ack = ENABLE;// Новый таймаутTIM3->CNT = 0;// Включаем таймерTIM3->CR1 |= TIM_CR1_CEN;}}


Код достаточно комментирован и я думаю, что в особом разборе не нуждается. Отмечу лишь тот момент, что после получения второго байта номера сектора в команде чтения мы устанавливаем флаг для операции чтения с SD карты для кода, который крутится в вечном цикле функции main(). И сразу после этого 4 следующих ACK выдаются с удлинённым временем. В интерфейсе это выглядит вот так:


Скриншот из программы логического анализатора, выделяются 4 большие задержки в транзакции

В сумме набирается порядка 3,5мс и этого с запасом хватает, чтобы код в основном коде успел считать сектор. Более того, тот код может работать только когда нет прерывания, т.е. как раз в эти большие паузы. Во время записи флаг устанавливается в самом конце и из-за того, что система даёт карте памяти отработать запись, основной код работает без помех со стороны прерываний. А теперь глянем в код основного цикла.
main()
// Основной цикл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 ){// Только вытащили, отключаем PSIOEXTI->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;}}}

В вечном цикле постоянно анализируется сигнал вставления SD карты. Если её вытащить на ходу, то код отключит PSIO и PS1 потеряет карту. Если же карту вставить обратно (или просто подать питание со вставленной картой), то сначала будет попытка инициализировать карту функцией Card_Init(), которая вернёт тип обнаруженной карты. Это важно, потому что у SDv1 и остальных SDHC/SDXC адресация идёт различными методами. Сам код инициализации никаких секретов не несёт и подсмотрен в куче доступных в интернете примеров про FatFS и подобных проектов.
Следом за инициализацией карты вызывается хитрая функция Card_FSInit(). Это самая главная фишка данного проекта. Дело в том, что STM32F042 скромный по возможностям и потянуть полную поддержку FatFS на необходимой скорости не сможет. Поэтому, я придумал такой метод: файл образа у нас всегда 128КиБ, поэтому, необходимо знать только 256 секторов по 512 байт, в каждом из которых будет ровно 4 сектора нашей карты памяти PS1. Таким образом, мы делаем следующее:

  1. Анализируем сектор LBA=#0 на предмет MBR. Если это действительно MBR, то получаем новый сектор, где находится MBS.
  2. Получив адрес предполагаемого MBS (это может быть #0, если нет MBR или какое-то число, если MBR есть) мы начинаем его анализ на предмет принадлежности одной из FAT: FAT12, FAT16, FAT32 или vFAT.
  3. Если сектор прошёл проверку, то мы забираем из него информацию о структуре и в корневом каталоге ищем элемент с именем файла. В данном случае это MEMCRD00.BIN.
  4. Если такой файл находится, то проверяем его размер он должен быть строго фиксирован 0x20000 байт. Если всё так получаем номер первого кластера.
  5. Если мы дошли до этого пункта, то у нас уже есть вся необходимая информации для построения списка физических LBA секторов, где расположен наш образ. Пробегая по цепочке FAT и используя информацию о структуре из MBS, заполняем таблицу из 256 номеров LBA секторов.

В случае успеха запускается PSIO и PS1 увидит свою карту как обычную. Если на каком-либо этапе произошла ошибка, то работа прерывается, загораются оба светодиода и всё остаётся в таком состоянии до снятия питания или замены SD карты. Вот код этой процедуры:
Card_FSInit()
// Инициализация таблицы секторов по имени файла, поддерживается пока только FAT16FunctionalState 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 ){// Вычитываем сектор 0if ( Card_Read( SDCard->CardType, &SDCard->CardBuf[ 0 ], &SDCard->LoadedLBA, SysOrg ) == DISABLE ) { break; }// Анализируем сектор #0 на MBRif ( *((uint16_t *)&SDCard->CardBuf[ 0x01FE ]) != 0xAA55 ) { break; }// Проверим косвенные признаки MBRif ( ((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;}}}// Загружаем сектор предполагаемого MBSif ( Card_Read( SDCard->CardType, &SDCard->CardBuf[ 0 ], &SDCard->LoadedLBA, SysOrg ) == DISABLE ) { break; }// Анализируем сектор на MBSif ( *((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 и ROOTFATOrg = 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;}

Скажу честно, так как это всего лишь PoC, то здесь реализован поиск только у FAT16. FAT12, наверное, и не надо поддерживать microSD таких малых объёмов не бывает. А вот FAT32 или vFAT добавить возможно, если это кому-нибудь понадобится в будущем.

Имя образа MEMCRD00.BIN выбрано не случайно. Дело в том, что в будущем я планирую добавить выбор образа через стандартную для многостраничных карт памяти комбинацию кнопок на джойпаде: при зажатом SELECT следует однократное нажатие на L1/R1. И меняя последние 2 символа можно поддержать 100 образов в корневой директории, от MEMCRD00.BIN до MEMCRD99.BIN. Для этого есть задел в обработчике прерывания по SCK в интерфейса PSIO, ветка где анализируется обращение к джойпаду. Сделать сниффер проблем нет, но периферия контроллеров у PS1 богатая и придётся практически всех их поддерживать.

В итоге, устройство получилось работоспособно и его может повторить каждый, если захочет. Ссылка на весь проект тут. Буду рад помочь всем заинтересовавшимся в комментариях к статье.

P.S. Я бы очень хотел указать здесь список всех источников информации, которые я использовал в создании этого проекта, но увы это очень затруднительно. Многое было подслушано случайно. Кое-что ходило в виде TXT файлов с общей информацией про PS1 более 15 лет назад, для тех, кто хотел написать свой эмулятор. И теперь всё это существует в виде нескольких текстовых файлов на моём жёстком диске. Можно сказать, что источником информации служил весь интернет на протяжении последних 15 лет.
Подробнее..

Перевод Создаём модчипы для PlayStation 1

09.12.2020 10:19:32 | Автор: admin

В детстве у меня была PlayStation 1 (PS1). Одна из первых, если не самая первая моя консоль, сильно повлиявшая на моё будущее.

Я увлёкся компьютерами благодаря тому, что играл в видеоигры и занимался их моддингом. Хотя в детстве я не был достаточно умён, чтобы модифицировать игровые консоли, мой отец с этим неплохо справлялся. Он модифицировал мою PS1, добавив на заднюю панель небольшой переключатель, позволявший запускать записанные на болванки игры. Как говорится, яблоко от яблони недалеко падает.

Перенесёмся в день сегодняшний: у меня уже нет моей первой PS1. Припоминаю, что некоторые игры перестали загружаться и я купил себе PS2. Поэтому зачем хранить старую консоль? Оказалось, это нужно из-за ностальгии и воспоминаний. Мы скучаем о том, чего больше нет.

Недавно друг подарил мне PS1. И так началось моё приключение: как мне модифицировать эту штуку?

DRM консоли Playstation 1


Если упростить, то принципы работы DRM консоли PS1 таковы:

  1. Sony запекла в официальные игры PS1 строки (например, SCEA, SCEI, SCEE, или, в редких случаях, SCEW); они находятся на дорожках, которые не может воссоздать обычное устройство считывания. В этом видео и этом посте подробно рассказывается о процессе.
  2. Контроллер CD-привода ищет эти строки, чтобы подтвердить официальность диска. Когда контроллер принимает решение о том, является ли диск официальным, основной процессор считывает это решение и действует соответствующим образом.
  3. Если строка не найдена или повреждена, то PS1 понимает, что диск не является подлинной игрой для PS1. Однако поскольку PS1 должна учитывать ошибки считывания с диска, есть значительный коридор возможностей для того, чтобы пройти проверку подлинности.
  4. Поскольку регион (A Америка, I Япония, E Европа и W комплект разработки Net Yaroze), а также подлинность записаны в одну строку, Sony убила одним выстрелом двух зайцев, реализовав разделение по регионам вместе с защитой от копирования.

Если вам нужно подробное исследование, то можно почитать посты The Old Crow, страницу документации No$PSX или этот Modchip FAQ

Принцип работы модчипов PSX заключается в электрическом препятствовании выходному сигналу, сгенерированному находящимся в приводе CD, и инъекции нового, фальшивого сигнала в микроконтроллер CD. Благодаря этому PS1 начинает верить, что любой вставленный диск является подлинным, и продолжает загрузку.

Позже Sony добавила более сложные проверки, например:

  • Проверка магических строк во время самой игры, а не при запуске
  • Изменение всего процесса, чтобы его было сложнее обойти простой передачей правильных строк.

Однако современные модчипы способны с этим справляться. Модчипы, работающие в условиях такой обновлённой защиты, называют stealth modchips, потому что консоль вообще не может их обнаружить.

Существующие типы модчипов


Первый модчип, ставший, так сказать, open source, был получен реверс-инжинирингом человеком по имени The Old Crow. Любопытно, что The Old Crow специализируется в электронных музыкальных синтезаторах, а не хакинге видеоигровых консолей. В каком-то смысле, от этого модчипа пошло большинство остальных модчипов. Изначально он выполнил реверс-инжиниринг коммерческого модчипа PS1, спроектированного западным инженером, работавшим в китайской компании.

Сегодня сообщество использует три основных модчипа:


Каждый из них имеет свои плюсы и минусы, но в целом их можно описать так:

  • MM3 самый распространённый модчип PS1, используемый сегодня. Его единственный недостаток заключается в том, что в нём применён встроенный генератор, который может рассинхронизоваться с генератором колебаний, находящимся в CD-приводе. В таком случае достаточно просто перезагрузить консоль, чтобы попробовать считывание заново.
  • Mayumi v4 пытается использовать генератор колебаний самого CD-привода. Это снижает вероятность возникновения рассинхронизации; однако считается, что Mayumi v4 довольно сложно устанавливать.
  • PSNee это модчип с открытым исходным кодом, первым автором которого был TheFrietMan. Его разработка позже была продолжена другими людьми. Похоже, он довольно неплохо работает на всех моделях Playstation 1/PSOne. Судя по коду, могу предположить, что он пытается определить, на каком этапе процесса загрузки находится PS1, чтобы начать инъекции фальшивых строк SCEX. К сожалению, PSNee сложен в установке. Схемы его установки ужасны и рядом не стоят со схемами для MM3 и Mayumi. Я создал разводку выводов Attiny45, но в конечном итоге решил работать с MM3 и Mayumi, потому что это проще.

Создание модчипа


[Прим. пер.: здесь в оригинале статьи была ссылка на страницу продажи модчипа автора на Ebay, но торговая площадка, похоже, не любит товаров такого рода, и страница уже закрыта.]

При наличии нужных инструментов создать модчип PS1 довольно просто. В этом туториале мы сосредоточимся на создании модчипов MM3 или Mayumi v4.

Хотя у меня есть Arduino, я предпочитаю использовать чипы MM3 и Mayumi, а не PSNee. Если вы хотите создать модчип PSNee, то можете воспользоваться инструкциями по прошивке файла .ino в Attiny.

Вам потребуются:

  • Микрочип PIC12F508
  • PICkit 3
    • Также скачать и установить MPLAB IPE из пакета MPLAB X IDE.
  • Какие-нибудь провода и монтажная плата (breadboard) для соединения PIC с PICkit.
  • HEX-коды для выбранного вами модчипа (представлены ниже).

Во многих туториалах рекомендуется PIC12C508. Это старая модель, и если у вас нет её запасов, то продолжать её использовать необязательно. HEX-коды, работающие с 12C, подходят и для 12F.

Для начала можно взглянуть на ИС и определить, какая нога за что отвечает. Нога рядом с вдавленным кружком это Pin 1. Противоположная нога это Pin 8.


Это DIP-чип. Я случайно приобрёл SOIC-8 (т.е. чип с поверхностным монтажом), поэтому мне пришлось припаять его к монтажной плате, но обычно этого можно избежать.

Можно подключить его к монтажной плате, а затем соединить с Pickit в соответствии со схемами ниже. Нужно обеспечить следующее соединение (остальные контакты пока не используются):

  • PICKit 1 IC 4 (VPP)
  • PICKit 2 IC 1 (VDD)
  • PICKit 3 IC 8 (VSS)
  • PICKit 4 IC 7 (ICSPDAT)
  • PICKit 5 IC 6 (ICSPCLK)


PICKit 3 слева, PIC12F508 справа.

После подсоединения чипа подключите PICKit к компьютеру и запустите MPLAB IPE. В разделе Device выберите PIC12F508.

Перейдите в Settings > Advanced Mode. По умолчанию в Advanced Mode используется пароль microchip. Не рекомендую его менять, даже не знаю, зачем вообще есть такая возможность.

Перейдите во вкладку Power слева и включите Power Target Circuit from Tool.


Вернитесь на вкладку Operate и нажмите Connect.


Скачайте соответствующий HEX-код для вашего чипа и консоли. Для каждого региона консоли он свой.


Также при желании можно использовать Mayumi v4 на PIC12F508.


В файле Source выберите своей HEX-код.

Нажмите большую кнопку Program.

Вы должны увидеть нечто подобное:

2018-06-20 20:48:02 -0400 - Loading hex file. Please wait...  Loading code from /Users/kchung/Repositories/PsNeePy.wiki/hexcodes/mm3/MM3USA.HEX...  2018-06-20 20:48:03 -0400 - Hex file loaded successfully.2018-06-20 20:48:20 -0400 - Programming...Device Erased...Programming...The following memory area(s) will be programmed:  program memory: start address = 0x0, end address = 0x1e7  configuration memory  Programming/Verify complete  2018-06-20 20:48:25 -0400 - Programming complete

При желании можно нажать на кнопку Verify, чтобы убедиться в правильности прошивки. Вывод должен выглядеть примерно так:

2018-06-20 20:50:10 -0400 - Verifying...Verifying...The following memory areas(s) will be verified:  program memory: start address = 0x0, end address = 0x1ff  configuration memory  User Id MemoryVerification successful.  2018-06-20 20:50:13 -0400 - Verify complete

Далее можно припаять чип к вашей PSX в соответствии со схемами из Интернета. Лично я пользовался превосходными схемами Уильями Куэда и рекомендую их вам.

Создание нового модчипа PS1


Изучая все эти модчипы, я подумал, что было бы здорово читать код на Python вместо ассемблера и C, поэтому приступил к портированию PSNee на Python.

При помощи MicroPython и ESP8266 мы можем создать модчип, который способен дистанционно обновляться и модифицироваться через WiFi чипа ESP8266.


По сути, это самая первая PS1 с функцией WiFi!

На фотографии выше (SCPH-7501) ESP8266 находится в верхнем левом углу, его разъёмы направлены вверх. Провода подключены к разъёмам и идут под CD-приводом к монтажной плате в нижнем правом углу.

К монтажной плате подключены провода, припаянные к нужным точкам MM3 и помеченные соответствующими номерами контактов. Благодаря использованию этой монтажной платы я могу тестировать создаваемые мной модчипы. Это гораздо быстрее, чем каждый раз припаиваться к плате.

Мой модчип (названный PsNeePy) выложен на Github: https://github.com/ColdHeat/PsNeePy.

Хотя моя тестовая консоль довольно плохо читает диски и редко загружает игры, модчип работает.

Чаще всего можно понять, что модчип работает (если не считать функций сокрытия от консоли), если он доходит до чёрного экрана с логотипом Playstation, поскольку это означает, что процессор посчитал игру подлинной.

Однако поскольку код основан на старой версии PSNee, стелс-функции, похоже, не работают на некоторых более новых версиях PS1.

В первую очередь я создал этот модчип как proof of concept и, скорее всего, не буду особо его поддерживать. Хоть я и не рекомендую его использовать, надеюсь, он понравится сообществу, и оно поможет совершенствовать его. Несмотря на то, что PSX уже мертва, её сообщество довольно активно.

С помощью PsNeePy можно удалённо обновлять ESP8266 по WiFi, выполнять удалённую отладку, а также производить сброс чипа. Раньше я много работал с PS1, и такой современный способ оказался довольно удобным.


Удалённое управление модчипом.

Спасибо анонимному другу за мою PS1, Sharan за ремонт моей PS1, Уильяму Куэду за превосходные схемы, AssemblerGames за качественную информацию, хоть они и не приняли меня на свой форум, а также PSXDEV за ответы на мои вопросы.
Подробнее..

Видео гид по эмулятору Omega Red

13.07.2020 10:06:40 | Автор: admin
Привет всем читателям!

Я продолжаю тему программного эмулятора для PlayStation 1, PlayStation 2 и PlayStation Portable Omega Red. Более подробно:


Данную публикацию я написал в связи с включением поддержки 7z архивов для коллекций БИОСов и изменения частоты обновления картинки рендеринга эмулятора. Дополнительно, после ряда вопросов, я записал видео гид по настройке и запуску игр с нуля.

Добро пожаловать под кат.





Данный проект использует WPF C# фреймворк как промежуточный уровень между рендером эмулятора и графическим контекстом дисплея: рендерер прорисовывает кадр в текстуру и WPF фреймворк встраивает эту текстуру в пользовательский интерфейс программы. Именно встраивание DirectX текстуры в пользовательский интерфейс является бутылочным горлышком WPF графического драйвера WPF происходит от Windows XP SP2 и имеет в основе DirectX9 layer, и встраивание DirectX9 текстуры задействует процессорное время. Как результат, загрузка процессора увеличивается что может идти в ущерб работы самого эмулятора в аудио потоке возникают паузы в воспроизведения синтезированного результата.
Разгрузить процессор можно через установки паттерна пропуска встраивания DirectX9 текстуры. Текущую частоту обновления текстуры можно мониторить через опцию Показать число кадров в секунду:
image
Уменьшить загрузку процессора можно через пропуск кадров по опции Режим пропуска кадров::

image

В настоящий момент программа доступна по ссылке: Omega Red и представлена на GitHub: OmegaRed.

Поддержать проект и задать вопросы можно по ссылкам:
www.facebook.com/Omega-Red-269237843996260
Buy Me A Coffee
Подробнее..

4К (2160р) разрешение для игр PS1, PS2, PSP в эмуляторе Omega Red

01.08.2020 10:15:58 | Автор: admin
Привет всем читателям!

Я продолжаю тему программного эмулятора для PlayStation 1, PlayStation 2 и PlayStation Portable Omega Red. Более подробно:


Данную публикацию я написал в связи с включением поддержки 4К разрешения рендеринга при эмуляции игр PS1, PS2, PSP.

Добро пожаловать под кат.



Особенностью программной архитектуры эмуляторов является рендерин полигонов игровых сцен через нативный графический API как результат, эмуляторы имеют в своём составе графический движок различной степени функционала. И один из функционалов это рендеринг в разрешение окна экрана. В эмуляторе OmegaRed рендеринг происходит в отдельную видео текстуру, и размер этой текстуры определяет размер виртуальной камеры рендерера.

Установка текущего разрешения производится из панели настройки конфигурации:

image

Сравнить качество рендеринга при разрешениях 720р / 2160р можно на следующих видео:


В настоящий момент программа доступна по ссылке: Omega Red и представлена на GitHub: OmegaRed.
Подробнее..

Редизайн пользовательского интерфейса эмулятора Omega Red

09.08.2020 14:18:19 | Автор: admin
Привет всем читателям!

Я продолжаю тему программного эмулятора для PlayStation 1, PlayStation 2 и PlayStation Portable Omega Red. Более подробно:


Данную новость я написал в связи решением изменить дизайн пользовательского интерфейса моего проекта.



Добро пожаловать под кат.

С момента начала публикации моего проекта я регулярно принимал сообщения от комментаторов о плохом дизайне интерфейса программы. Особенно в одном из последних сообщений:



Особенно доставило удовольствие: avoid switching between North American English and British English.

Но, как говорится Vox populi глас народа. И я принял решение сделать редизайн интерфейса программы. Программа написана на C# в WPF с использованием паттерна MVVM как результат, сделать набросок нового дизайна это дело пары свободных часов.






Какое мнение у читателей по новому редизайну?

По вопросам схожести интерфейса с Deus Ex: Human Revolution и Нинтендо Свитч отвечу не спи л, а позаимствовал.

В настоящий момент программа доступна по ссылке: Omega Red и представлена на GitHub: OmegaRed.
Подробнее..

Редизайн пользовательского интерфейса эмулятора Omega Red (Финал)

25.01.2021 10:05:20 | Автор: admin

Привет всем читателям!

Я продолжаю тему программного эмулятора для PlayStation 1, PlayStation 2 и PlayStation Portable Omega Red. Более подробно:

Данную новость я написал в связи редизайном разрабатываемого эмулятора (а заодно и ребридингом ребрендингом).

Добро пожаловать под кат.

Данная версия проекта, под названием Golden Phi, представляет переработанный пользовательский интерфейс. По странной причине первоначальный интерфейс не понравился некоторым пользователям. Что же, редизайн интерфейса интересная задача и хорошая причина попрактиковаться в MVVM проектировании.

Первоначально идея обновления интерфейса ограничевалась только изменением визуального стиля. Но в дальнейшем пришлось значительно переработать структуру и пользовательского интерфейса, и большую часть кода.

Приложение открывается списком трёх последних запущенных игр:

Ограничение списка только тремя играми связано с рекомендациями читателей по исключению скролинга списка. Это и логично - три игры для трёх эмуляторов (PS1, PS2, PSP). Для большей презентабельности списка игр каждый образ диска отображается скрином игрового экрана с автоматического сохранения. Таким образом, "обои" образов дисков постоянно обновляются в течении игрового процесса.

Каждая панель игры в дополнение к "обоям" имеет кнопку сохранений для отображения панели текущих сохранений для данной игры. Данная панель позволяет загрузить одно из сохранений или удалить одно из них.

Выбор образа игры из списка всех игр, добавление нового образа или удаление существующего можно сделать из отдельного меню по нажатию на панель с иконкой диска. Список имеет полоску скролинга и позволяет легко управлять большим списком образов игр.

Для игр на PS1 и PS2 потребуется загрузить и выбрать образ соответствующего БИОСа.

Конфигурацию эмулятора можно изменять из опции по кнопке. Панель конфигурации включает настройки графики, геймпада, языка интерфейса.

Панель скриншотов позволяет просмотреть сделанные скриншоты из игр и в случае необходимости удалить скриншоты.

Выход из программы осуществляется через панель пользователя.

А теперь к игровому процессу. Запуск игры осуществляется двойным нажатием на плитку желаемого образа игры. Экран игры в режиме точпада имеет следующий вид:

При переводе эмулятора в паузу программа переходит на главный экран, перемещая текущую игру в левую крайнею позицию с отображением анимированной иконкой активного состояния игры. Нажатие на иконку активного состояния останавливает текущую игру с сохранением текущего игрового процесса.

При подключении геймпада программа автоматически находит первый геймпад и подключает его автоматически - подключил и играй. Для управления эмулятором с геймпада определены три команды:

Быстрое сохранение

Быстрая загрузка

Быстрое возобновление игры (аналог Quick Resume Xbox Series X)

При переходе к панели быстрого возобновление игры список образов игр для возобновления представлен в форме вертикальной очереди текущих обложек образов игр:

В настоящий момент Windows OS версия программы доступна по ссылке: Omega Red и представлена на GitHub: Golden .

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru