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


    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? Да собственно, их всего три.

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

    Легенда:

    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. При чтении необходимо время на актуализацию данных. Между номером сектора и фактической передачей данных у нас есть четыре байта, у которых мы можем немного растянуть 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
    	{	// Посылаем 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;
    }

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

    Теперь посмотрим на PSIO. Как было уже сказано выше, он у нас в любом случае программный. Отслеживать надо только два сигнала: SEL и CLK. Первый мы будем отслеживать по обоим фронтам и делать приготовления к обмену данными:

    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;
    }

    Сигнал CLK будем ловить только по фронту. Дело в том, что STM32F042 работает всего лишь на 48 МГц и его производительность маловата для нашей задачи. И если делать прерывание по обоим фронтам, то во время пересылки байта он практически не вылезает из обработчика прерывания и всё работает прямо на грани возможности, иногда давая сбои. А если реагировать только на фронт, а ту работу, что должна быть сделана по спаду сделать в конце прерывания, то всё отлично успевает меньше, чем за 55% от периода CLK, ведь несколько проверок при этом можно выкинуть. Уверен, что если этот обработчик написать на ассемблере максимально оптимально, то он смог бы работать даже по обоим перепадам. Вот код обработчика:

    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;
    	}
    }

    Таймер TIM3 будет отвечать за генерацию ACK. Это нужно для того, чтобы во время этой задержки ядро было свободно для работы с SD картой. Обработчик прерывания от таймера вот такой:
    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;
    	}
    }
    


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

    В вечном цикле постоянно анализируется сигнал вставления SD карты. Если её вытащить на ходу, то код отключит PSIO и PS1 «потеряет» карту. Если же карту вставить обратно (или просто подать питание со вставленной картой), то сначала будет попытка инициализировать карту функцией Card_Init(), которая вернёт тип обнаруженной карты. Это важно, потому что у SDv1 и остальных SDHC/SDXC адресация идёт различными методами. Сам код инициализации никаких секретов не несёт и подсмотрен в куче доступных в интернете примеров про FatFS и подобных проектов.

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

    1. Анализируем сектор с адресом LBA равным нулю на предмет 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 увидит карту как свою обычную на 15 блоков. Если на каком-либо этапе произошла ошибка, то работа прерывается, загораются оба светодиода и всё остаётся в таком состоянии до снятия питания или замены SD карты. Вот код этой процедуры:

    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;
    }

    Скажу честно, так как это всего лишь 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 лет.

    Средняя зарплата в IT

    120 000 ₽/мес.
    Средняя зарплата по всем IT-специализациям на основании 7 559 анкет, за 1-ое пол. 2021 года Узнать свою зарплату
    Реклама
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее

    Комментарии 46

      0
      Ох жалко не могу плюс поставить.
      PS1 ковырять интересное занятие.
      PS. Недавно тоже ковырял SD карту через SPI пришлось переводить его в режим 32 бита, чтобы выиграть немного скорости за счет меньшего «межбайтного» пространства в SPI. При этом казалось что вроде STM не подвержены такой напасти, но по логу вижу, что всё также.
        +1
        В логе что в статье запись PSIO, там по определению должны быть гапы. В SPI что на карту гапов нет. Более того, конкретно у STM32F042 SPI 16ти битный и имеет FIFO на 3 слова в каждую сторону. И установка размера меньше 9 бит (4-8 бит) автоматически включает пакинг. Если посмотреть в код, что по ссылке на гитхаб статье, то видно, Card_SendCMD() посылает 6 байт команды как 3 слова а затем ждёт сначала опустошения FIFO передачи, а потом ещё и снятия BUSY. А вот Card_SPI() работает с байтами и даже в SPI1_DR пишет как в байт:
        *((__IO uint8_t *)&SPI1->DR) = Data;

        Если сделать обычное приравнивание, то произойдёт запись слова и пересылка станет 16 бит. Так что пересылка идёт достаточно плотно и между байтами если и есть задержка, то она очень маленькая. И, кстати, DMA в данном проекте особо не поможет: скорости это точно не прибавит, разве что будет гарантировать, что все 512 байт прилетят без прерывания, а значит сэкономится немного времени. Возможно, позже я доработаю до DMA.
          0
          Упс, недоглядел это лог PSIO.
          В SPI что на карту гапов нет.

          Значит правильно помню что на STM проблем не было, у NIOS II размер гапа примерно как размер передачи байта, с упреждающей посылкой следующего можно чуть сократить но всё равно неприятно получалось.
            0
            При правильной настройке SPI, при достаточной частоте ядра (F4/H7) для использования прерываний или правильной настройке DMA сигнал SCK вообще в логе выглядит как непрерывная стабильная частота. Для этого в блоке SPI есть разные флаги: TXE/BSY/FTLV/FRLV и прочие. И именно при полном понимании какой флаг для чего и грамотном использовании этих флагом добивается гладкая работа интерфейса. И примеров этому вы не найдёте в интернете, т.к. там обычно школьные примеры с программным поллингом. Ну и практически любой SPI у STM имеет второй режим I2S, а он не терпит гапов по определению.
        0
        Вы в jlcpcb отсылали те файлы, которые на гитхабе? И вам такую сверловку сделали? У вас там 0.005 дюйма сверло указано. Это слегка маловато. Для вашего проекта было бы логично использовать минимальное 0.3мм с внешним диаметром площадки 0.7мм.
          0

          Не знаю, как китайцы, но Резонит пишет, что могут сделать переходное отверстие 0,1 мм за совсем дорого (точнее, с ценой «по запросу»). Не вижу, где было бы написано, что использовался jlcpcb.

            0
            Не вижу, где было бы написано, что использовался jlcpcb
            Номер на платах похож.
            +1
            Файлы что на гитхабе в обработке на грязных платках, ниразу не юзал jlcpcb. Но говорят, что они через них и работают. Всегда без проблем принимали и изготавливали. Вот состояние на сейчас конкретно файлов с гитхаба:
            image
            Фотографии готовых устройств в статье так же были изготовлены там же.
              –1
              Всегда без проблем принимали и изготавливали.
              Это не значит, что сделано правильно. Платы ваши простые, китайцы могут и без спроса поменять диаметры/рисунок. Впрочем, дело ваше.
                +1
                Спасибо за разъяснения. На всякий случай подправил гербера согласно вашей рекомендации.
            +1
            Сейчас не каждый и вспомнит, что такое NOR FLASH.
              +3
              Чего это? используются везде. Просто интерфейс стал последовательным, и корпус 8ногим.
              25-я серия горячо любимая…
                +1
                Ну обычно подразумевается, что NOR FLASH это память с прямым доступом в каждую ячейку. И никто не задумывался о NOR как таковом, пока не пришла NAND. Вот тогда и стали различать. Т.е., в быту обычно это использовали чтобы обозначить именно интерфейс доступа, а не физическую организацию ячеек. Ну а ваш пример это уже, естественно, SPI FLASH.
                  +1
                  она всё еще NOR, и с произвольным доступом. Уж не знаю какой у вас быт, но у нас ее так и называют. Больше того, если стать осциллографом на шину такой памяти в разных устройствах — часто можно обнаружить что выполнение программы из нее не кешируется, а выполняется Direct, прям как из настоящей параллельной.
                    0
                    Интересно, а как решен вопрос со временем доступа то? С последовательным доступом ещё понятно, а полностью рандомный? Там же оверхед на установку адреса и команды минимум 24 такта (для двухбайтовых адресов). Каким бы быстрым SPI не был, он всё равно не сможет полностью заменить параллельную шину.
                      +2
                      image
                        0
                        Ах да, я совсем упустил из виду QSPI. Помноженный на бешеные частоты он вполне может работать почти как настоящая память с параллельной шиной.
                    +2

                    (минутка занудства) тёплое — это тёплое, не надо путать его с мягким.
                    Флешки с SPI-интерфейсом бывают и с NOR внутри, и с NAND (у этих объемы от 0.5 до нескольких гигабит). Так что SPI FLASH — штука несколько абстрактная.

                      +3
                      Ну разницу между интерфейсом и организацией ячеек памяти я осознаю и понимаю. Так же понимаю какие ограничения есть у NOR матрицы или NAND матрицы. В любом случае благодарю за уточнение, возможно кто-то может и запутаться, читая мой предыдущий пост.
                  +1

                  Вы о чем, параллельные NOR Flash вполне живы, S29AL016J там, например. Используются в критичных местах (типа загрузчиков, как подсказывает даташит), где недопустимы единичные проблемы битых ячеек, присущие NAND.

                  0
                  Похоже используемый на фото КС156А старше чем PS1
                    0
                    О, да. Эту карту мне отдали как нерабочую в 1996м году. Я тогда уже имел некоторое представление об устройствах карт памяти, т.к. часто занимался ремонтом среди знакомых. Попробовал поставить — сработало. И карта до сих пор работает! Там даже сохранёнки какие-то из Music2000, Tekken 3 и ещё пары игр.
                    0
                    Флешка для БЕСКОНЕЧНОЙ карты памяти? это не модно, надо втыкать wi-fi и складывать сохранения в облако.
                      +2
                      Технически, уже давно есть полноразмерные SD/MMC карты с WiFi. Они предназначены для фотографов и выгружают фото сразу в сеть на NAS, точнее, линкуются к открытой сетевой шаре. Так что да, меняем все кишки на избитую ESPxxx и понеслась. Только вот корпус будет почти как у 8ми страничной на фото из статьи.
                      +8
                      Я человек простой — вижу STM вместо ардуины — ставлю плюс.
                        +1
                        К сожалению, повторить хоть и хочется, но не сильно получится:
                        STM32F042F6 недоступен по вменяемой цене с вменяемыми сроками: в магазинах города нет, под заказ сроки от 40 недель, на Ali — от 500 рублей за штуку с неизвестным качеством.
                          0
                          А какие есть варианты в быстром и адекватном доступе? Быть может есть смысл перейти на какую-то альтернативу? Предлагайте.
                            0
                            По STM'овскому Product Finder'у есть 7 вариантов в том же корпусе и с теми же объемами RAM/ROM. И их всех нет в наличии.
                            Да и с blue-pill'ным STM32F103C8T6 тоже как-то не очень.
                            Похоже кризис компонентов и до DIY-щиков добрался.
                              0
                              есть 7 вариантов в том же корпусе и с теми же объемами RAM/ROM

                              Ну а если что-то совсем другое подобрать? RAM надо больше либо равно 2КиБ (1,6КиБ уходит на буферы и таблицу), с ROM всё более свободно, текущая прошивка занимает около 18КиБ. Вот со скоростью уже туже: 48МГц минимум, если нет ОЗУ на кеширование всего образа.

                              Да и с blue-pill'ным STM32F103C8T6 тоже как-то не очень.

                              У него 72МГц и 48 ног в корпусе. С ним уже можно прикрутить экранчик на верхнюю сторону и получить понятную пользователю обратную связь. Но он и стоит 4х от 042.

                              А в целом согласен, кризис есть. Но ходят слухи что конкретно с STM ситуация выправится через 1-2 года.
                                0
                                Не специалист по ассортименту STM, но пока наиболее доступный по наличию и цене
                                STM32F102R4T6A. Но он вообще 64-LQFP.
                                  0
                                  Он 4х от цены 042, но при этом FLASH всего 16КиБ. Туда этот код уже не влезет. Ну или всё переписывать без использования С.
                                    +1
                                    А больше нет ничего.
                                    Все остальное — от 300 рублей и доступно в единичных количествах.
                                    Так что будем ждать лучших времен.
                          –1
                          в сумме это 128 КиБ, что и видно по маркировке FLASH памяти на фото выше.

                          То ли лыжи не едут… Мне не видно. Никаких «128k» не вижу.
                            +2
                            Маркировка FLASH на картинке гласит: SST29VE010. Выделенные цифры означают объём в битах (в данном случае — один мегабит, что равно 128 КиБ).
                              –1
                              Ну вот так и надо было сразу написать в статье, для тех, кто не в курсе (это на будущее замечание в том числе).
                                +2
                                Предполагается, что читатель уже имеет некоторую базовую информацию об электронике, компонентах и их маркировках. Должен ли я добавить сей дисклаймер в начала статьи, которая публикуется в хаб DIY?
                                  –2
                                  Кодировка объёма флеш памяти в обозначении = базовая информация об электронике?
                                    +2
                                    Кодировка объёма флеш памяти является частью обозначений элементов. Встречный вопрос: является ли обозначение элементов базовой информацией об электронике?
                                    Ну и прочтите моё сообщение после запятой тоже.
                                      –1
                                      Ваша отсылка к маркировке выглядит примерно так:
                                      «микросхема имеет 39600 логических элементов, что и видно по маркировке»
                                      [маркировка] = EP4CE40F29C8N
                                        +1
                                        Ну и где противоречие то? Вы же привели в пример EP4CE40F29C8N, а не, скажем, EP4CE6F29C8N. Ваша придирка звучит примерно так: шильдик «Lada 1600» это просто красивое число рядом с названием, а не объём двигателя в кубических сантиметрах.

                                        Я, конечно, понимаю, что каждый производитель обозначает свои элементы каждый во что горазд, но с памятью всё обстоит более-менее стандартно и адекватно. К тому же есть объединяющий стандарт JEDEC. Если используются 2 цифры, это однозначно кибибиты: 16, 32, 64. Если используются следующие 3 цифры, это всё равно кибибиты: 128, 256 и 512. Следующие 3 цифры означают уже мибибиты: 010, 020, 040 и 080. Если используются 4 цифры, то это только мимбибиты. И это знает каждый, кто работает с SRAM, EPROM или FLASH памятью.
                                          0
                                          Интересная инфа, буду знать. С паматью не работал, потому не знаю.
                                      0
                                      Вы «Инженер-схемотехник-ПЛИСовод», и не знаете, как расшифровывается маркировка микросхем памяти? Мда.
                              0
                              Давно ждал такого проекта. Похожая мысль была, но не с SD картой, а с USB на другой стороне, чтоб удобно было выгружать на комп. Как-то года два назад попробовал через ардуину подцепить Memorycard, а на компе прога MemcardRex, которая подключается к ардуине по COM порту. Таким образом читать/записывать сейвы. Но так и оставил эту затею пересобрать все не на соплях «на потом». Буду следить за проектом, попробую повторить, но не силен в STM.
                              Планируете в дальнейшем сделать программу для чтения/редактирования образов MEMCRDXX.BIN?
                                0
                                Такая программа уже существует и про неё сказано в статье: MCCSE. Но, во-первых она старая, а во вторых оригинал был японский, а английский перевод какой-то кустарный. Так что да, идея менеджера образов карт памяти так же есть и, вероятно, приложение я тоже сделаю, с возможностью общего хранилища, куда можно будет скидывать отдельные сейвы, а не просто набор стандартных образов как у MCCSE.
                                  0
                                  Показалось, что MCCSE читает напрямую с карты, через LPT, не сами дампы карты. В исходниках где-то есть описание структуры выходного образа сейвов? В статье я не увидел, может пропустил.

                                  P.S. добавить потом в прогу возможность конвертации сейвов с RetroPi например )))
                                    0
                                    MCCSE работает с образами карт памяти. В том числе и вычитывая реальную карту через физическое подключение. Логическая организация карт памяти намеренно не озвучена в статье, так как это тема другой статьи и, собственно, не относится к данной теме.

                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                              Самое читаемое