Подключаем геймпад от PS1/PS2 к Raspberry pi

    На дворе майские праздники. А значит время есть шашлыки и употреблять различные разливные и бутилированные напитки. А я занимаюсь всякой ерундой. Мысль о данном проекте пришла, ещё до нового года. Но я долго не мог его реализовать. Прикупив первый геймпад (который я выкинул в последствии), и попробовав опросить его, я понял, что тут уже не обойтись простым чтением мануалов, хотя инфы достаточно. Благо, на это 23 февраля я получил в подарок не пену для бритья, упакованную в носки, а 600 р. с пожеланием не отказывать себе ни в чём на aliexpress. Где и была куплена китайская копия логического анализатора Saleae Logic. Те, кому интересно, что из этого получилось, могут ткнуть мышкой по кнопке ниже. А те, кому лень читать могут сразу посмотреть результат вот тут.



    Прикрутив логический анализатор к незамысловатой конструкции, я увидел, что геймпаду отправляется откровенный шлак.

    Минутка теории.

    Дело в том, что консоль с геймпадом общаются по интерфейсу SPI. И есть регламентированные режимы работы этого интерфейса. В данном случае должен быть MODE 3, передача младшим битом вперёд, и логический уровень на линии Chip enable или Select slave при обращении к геймпаду «0». Частота на линии clk 250 kHz. Но я сделал 100 kHz, и нормально работает. Всё в принципе наглядно описано вот здесь. А система команд геймпаду, и расшифровка ответов вот здесь. Так же на портале «Радиокот», есть публикация, но там есть ошибки в командах. Проще говоря, чтобы опросить стандартный геймпад, нужно дать ему команду:

    0x1 0x42 0x0 0x0 0x0
    и он вернёт, что-то вроде
    0xff 0x41 0x5a 0xff 0xff
    

    Где 0x41 это тип геймпада, а последние 2 байта — это состояние кнопок. С аналоговым всё аналогично, только надо добавить в посылку ещё 4 нулевых байта.

    0x1, 0x42, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0
    и он вернёт, что-то вроде
    0xff 0x73 0x5a 0xff 0xff 0x80 0x80 0x80 0x80
    

    Здесь 0x73 это обозначает, что геймпад — аналоговый, последние 4 байта — состояние аналогов.

    Важно, что геймпад работает от 3.3 В., что позволяет его питать напрямую от Raspberry pi. В стандартном режиме (работают только кнопки), потребление порядка 2 мА, в аналоговом режиме 12 мА. И то эти 10 мА добавляет светодиод на геймпаде. А на линию 3.3 В Raspberry pi можно сажать нагрузку до 50 мА.

    Собственно, для подключения геймпада, понадобится любые 4 порта GPIO и питание. Всего 6 проводов.

    Так вот. Я понял, что библиотеки для работы с GPIO, что bcm2835, что wiringPi, очень плохо работают с SPI. Они в принципе не умеют передавать посылки младшим битом вперёд. В доках к одной из них это честно описано, но где-то очень глубоко. Да и режимы они толком соблюдать не умеют.

    Но ничего не мешает воспроизвести посылки самому, и читать данные от геймпада. Сказано, сделано. Берём библиотеку wiringPi и пишем:

    #include <stdio.h>
    #include <unistd.h>
    #include <wiringPi.h>
    #define mosi 12
    #define miso 13
    #define clk 14
    #define ce 5
    unsigned char i, b;
    unsigned char cmd[9] = {0x1, 0x42, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0};
    int main()
    {
    	wiringPiSetup () ; //инициализация библиотеки
    	pinMode (mosi, OUTPUT);
    	pinMode (clk, OUTPUT);
    	pinMode (ce, OUTPUT);
    	pinMode (miso, INPUT);
    	digitalWrite (clk, HIGH);
    	digitalWrite (mosi, LOW);
    	digitalWrite (ce, HIGH);
    	while(1) 
    	{
    		unsigned char data[9] = {0};
    		digitalWrite (ce, LOW);
    		delayMicroseconds(20);
    		for (i = 0; i < 9; i++)
    		{
    			for (b = 0; b < 8; b++)
    			{
    				if (((cmd[i] >> b)&1) == 1)
    				{
    					digitalWrite (mosi, HIGH);
    					digitalWrite (clk, LOW);
    					delayMicroseconds(5);
    					digitalWrite (clk, HIGH);
    					data[i] ^= digitalRead(miso) << b;
    					delayMicroseconds(5);
    					digitalWrite (mosi, LOW);
    				}
    				else
    				{
    					digitalWrite (clk, LOW);
    					delayMicroseconds(5);
    					digitalWrite (clk, HIGH);
    					data[i] ^= digitalRead(miso) << b;
    					delayMicroseconds(5);
    				}
    			}
    			delayMicroseconds(40);
    		}
    		digitalWrite (ce, HIGH);
    		printf("%x %x %x %x %x %x\n", data[3], data[4], data[5], data[6], data[7], data[8]);
    		delayMicroseconds(100);
    	}
    }
    

    Всё классически, объявляем порты GPIO, переменные и массив команды. Далее настраиваются режимы работы портов, и приводятся в исходное состояния линии clk, mosi и ce (chip enable). Потом формируются 2 цикла. В первом перебираются байты команды, а во втором перебираются побитно сами байты, для того, что бы подать логическую единицу или ноль на линию mosi в зависимости от значения бита. И одновременно с этим читаем ответ. Когда увидел в ответ заветные:

    0xff 0x41 0x5a 0xff 0xff
    

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

    ff ff 80 80 ff 80
    ff ff 80 80 ff 80
    ff ff 80 80 ff 80
    ff ff 80 80 ff 80
    ff ff 80 80 ff 80
    ff ff 80 80 ff 80
    ff ff 80 80 ff 40
    ff ff 80 80 ff 4b
    ff ff 80 80 ff 4b
    ff ff 80 80 ff 4b
    ff ff 80 80 ff 4b
    ff ff 80 80 ff 4b
    ff ff 80 80 ff 1a
    ff ff 80 80 ff 1a
    ff ff 80 80 ff 1a
    ff ff 80 80 ff 1a
    ff ff 80 80 ff 1a
    ff ff 80 80 ff 0
    

    Ну а далее предстояло уже интегрировать этот код в эмулятор. В данном случае вопрос с выбором эмулятора не стоял. Однозначно pcsx_rearmed. Так же, как и в предыдущий раз, необходимо было найти функцию, которая запускается одной из первых, чтобы в ней проинициализировать библиотеку. Такой функцией оказалась psxInit, она располагается в файле r3000a.c, который эмулирует работу центрального процессора. В этот файл так же добавил заголовочный файл библиотеки и объявил порты GPIO. В функции psxInit я произвожу инициализацию библиотеки и выставляю исходные уровни на портах GPIO. Внизу приведены первые строки этого файла с изменениями.

    #include "r3000a.h"
    #include "cdrom.h"
    #include "mdec.h"
    #include "gte.h"
    #include <wiringPi.h>
    #define mosi 12
    #define miso 13
    #define clk 14
    #define ce 5
    R3000Acpu *psxCpu = NULL;
    psxRegisters psxRegs;
    
    int psxInit() 
    {
    	SysPrintf(_("Running PCSX Version %s (%s).\n"), PACKAGE_VERSION, __DATE__);
    	wiringPiSetup () ; //инициализация библиотеки
    	pinMode (mosi, OUTPUT);
    	pinMode (clk, OUTPUT);
    	pinMode (ce, OUTPUT);
    	pinMode (miso, INPUT);
    	digitalWrite (clk, HIGH);
    	digitalWrite (mosi, LOW);
    	digitalWrite (ce, HIGH);
    #ifdef PSXREC
    	if (Config.Cpu == CPU_INTERPRETER) {
    		psxCpu = &psxInt;
    	} else psxCpu = &psxRec;
    #else
    	psxCpu = &psxInt;
    #endif
    
    	Log = 0;
    
    	if (psxMemInit() == -1) return -1;
    
    	return psxCpu->Init();
    }
    

    Далее предстояло разобраться с тем как эмулятор имитирует работу геймпада. Есть заголовочный файл psemu_plugin_defs.h, он располагается в каталоге include, вот в нём находится структура, в которой определены переменные, в которых хранятся – тип геймпада – стандартный, аналоговый, переменная состояния кнопок, переменные состояния аналогов, и переменные управления вибрацией.

    typedef struct
    {
    	// controler type - fill it withe predefined values above
    	unsigned char controllerType;
    
    	// status of buttons - every controller fills this field
    	unsigned short buttonStatus;
    
    	// for analog pad fill those next 4 bytes
    	// values are analog in range 0-255 where 127 is center position
    	unsigned char rightJoyX, rightJoyY, leftJoyX, leftJoyY;
    
    	// for mouse fill those next 2 bytes
    	// values are in range -128 - 127
    	unsigned char moveX, moveY;
    
    	unsigned char Vib[2];
    	unsigned char VibF[2];
    
    	unsigned char reserved[87];
    
    } PadDataS;
    

    Так вот основная задача записать полученные данные в эти переменные. У этого эмулятора имеются плагины. В том числе и плагин геймпадов, в каталоге plugins имеется подкаталог dfinput, в котором располагается искомый файл pad.c, в котором и реализовано чтение состояния геймпада, а так же его настройки. Вот в него и был перенесён код из тестовой программы. Так же объявляются порты, и заголовочный файл библиотеки. Так же в нём имеются переменные, которые хранят код команд посылаемых геймпаду. Эти переменные используются в функциях обращения к геймпаду. Ниже этот участок кода:

    #include <stdint.h>
    #include "../include/psemu_plugin_defs.h"
    #include "main.h"
    #include <wiringPi.h>
    #define mosi 12
    #define miso 13
    #define clk 14
    #define ce 5
    unsigned char a, b;
    unsigned char cmd[9] = {0x1, 0x42, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0};
    enum {
    	ANALOG_LEFT = 0,
    	ANALOG_RIGHT,
    
    	ANALOG_TOTAL
    };
    
    enum {
    	CMD_READ_DATA_AND_VIBRATE = 0x42,
    	CMD_CONFIG_MODE = 0x43,
    	CMD_SET_MODE_AND_LOCK = 0x44,
    	CMD_QUERY_MODEL_AND_MODE = 0x45,
    	CMD_QUERY_ACT = 0x46, // ??
    	CMD_QUERY_COMB = 0x47, // ??
    	CMD_QUERY_MODE = 0x4C, // QUERY_MODE ??
    	CMD_VIBRATION_TOGGLE = 0x4D,
    };
    

    Нас интересует самая первая, у которой значение 42. Вот при вызове этой переменной и будет выполняться код. Тем более, что в оригинальном файле она пустая. И в функции перебора команд do_cmd, я вставил основной код:

    static uint8_t do_cmd(void)
    {
    	PadDataS *pad = &padstate[CurPad].pad;
    	int pad_num = CurPad;
    	unsigned char data[9] = {0};
    	CmdLen = 8;
    	switch (CurCmd) 
    	{
    		case CMD_SET_MODE_AND_LOCK:
    			buf = stdmode[pad_num];
    			return 0xF3;
    
    		case CMD_QUERY_MODEL_AND_MODE:
    			buf = stdmodel[pad_num];
    			buf[4] = padstate[pad_num].PadMode;
    			return 0xF3;
    
    		case CMD_QUERY_ACT:
    			buf = unk46[pad_num];
    			return 0xF3;
    
    		case CMD_QUERY_COMB:
    			buf = unk47[pad_num];
    			return 0xF3;
    
    		case CMD_QUERY_MODE:
    			buf = unk4c[pad_num];
    			return 0xF3;
    
    		case CMD_VIBRATION_TOGGLE:
    			buf = unk4d[pad_num];
    			return 0xF3;
    
    		case CMD_CONFIG_MODE:
    			if (padstate[pad_num].ConfigMode)
    			 {
    				buf = stdcfg[pad_num];
    				return 0xF3;
    			}
    			// else FALLTHROUGH
    
    		case CMD_READ_DATA_AND_VIBRATE:
    		digitalWrite (ce, LOW);
    		delayMicroseconds(20);
    		for (a = 0; a < 9; a++)
    		{
    			for (b = 0; b < 8; b++)
    			{
    				if (((cmd[a] >> b)&1) == 1)
    				{
    					digitalWrite (mosi, HIGH);
    					digitalWrite (clk, LOW);
    					delayMicroseconds(5);
    					digitalWrite (clk, HIGH);
    					data[a] ^= digitalRead(miso) << b;
    					delayMicroseconds(5);
    					digitalWrite (mosi, LOW);
    				}
    				else
    				{
    					digitalWrite (clk, LOW);
    					delayMicroseconds(5);
    					digitalWrite (clk, HIGH);
    					data[a] ^= digitalRead(miso) << b;
    					delayMicroseconds(5);
    				}
    			}
    			delayMicroseconds(40);
    		}
    		digitalWrite (ce, HIGH);
    				
    		pad->buttonStatus = data[4];
    		pad->buttonStatus = pad->buttonStatus << 8;
    		pad->buttonStatus |= data[3];
    		pad->rightJoyX = data[5];
    		pad->rightJoyY = data[6];
    		pad->leftJoyX = data[7];
    		pad->leftJoyY = data[8];
    		
    		default:
    			buf = stdpar[pad_num];
    
    			buf[2] = pad->buttonStatus;
    			buf[3] = pad->buttonStatus >> 8;
    
    			if (padstate[pad_num].PadMode == 1) 
    			{
    				buf[4] = pad->rightJoyX;
    				buf[5] = pad->rightJoyY;
    				buf[6] = pad->leftJoyX;
    				buf[7] = pad->leftJoyY;
    			} 
    			else 
    			{
    				CmdLen = 4;
    			}
    
    			return padstate[pad_num].PadID;
    	}
    }
    

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

    Ну и всё, осталось только скомпилировать проект. Для того, чтобы компилятор подхватил библиотеку wirigPi нужно в Makefile проекта вставить ссылку на неё. Это достаточно сделать в самом начале.

    # Makefile for PCSX ReARMed
    
    # default stuff goes here, so that config can override
    TARGET ?= pcsx
    CFLAGS += -Wall -ggdb -Ifrontend -ffast-math -I/usr/include -I/usr/include/SDL
    LDLIBS += -lpthread -lSDL -lpng -lwiringPi
    ifndef DEBUG
    CFLAGS += -DNDEBUG -g
    endif
    CXXFLAGS += $(CFLAGS)
    #DRC_DBG = 1
    #PCNT = 1
    

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

    У меня не так много игр для PS1, я проверил где-то штук 7. Не заработало на Tomb Raider2 и Nuclear strike, но это видимо потому, что эти игры не знают, что такое аналоговый геймпад. Наверно надо выбрать в настройках стандартный и попробовать.

    P.S. Сам эмулятор лучше собирать с флагами оптимизации. Это хорошо описано вот здесь.
    Метки:
    Поделиться публикацией
    Похожие публикации
    Комментарии 14
    • 0
      Задержки какие?
      • 0
        Задержки где, в чём?
        • 0
          Между нажатием клавиши на геймпаде и реакцией персонажа
          • 0
            Да я и не заметил. Вроде комфортный отклик.
      • 0
        А вот это разве не делает это автоматом?
        • +2
          Ну, да это конечно проще. Подключить любой usb-шный гемпад, это каждый сможет. Ну во-первых, я не хочу ставить какую-то отдельную систему под игры, мне и Raspbian нормально, во-вторых, так я учусь программировать, в-третьих хотелось играть при помощи оригинального железа, в-четвёртых — просто fun. А вот кстати геймпад xbox one неплохо было бы подключить, он у меня есть. ИМХО самый удобный из современных геймпадов.
          • 0
            Если Вы учитесь программировать, попробуйте получить feedback от разработчиков, оформив изменения в виде патчей и отправив их в upstream.
            • 0
              Ох, что они скажут, если увидят код с wiringpi…
              • 0
                Я бы сказал, что это тоже часть обучения.
                • 0

                  Так эта библиотека же просто упрощает доступ к GPIO для простого пользователя. Так, что ничего такого в этом нет. Чем писать 20 строк кода, можно написать одну.

                • 0

                  Это тонкий троллинг или совет)?

              • 0
                В Retropie есть поддержка геймпадов и для соньки и для ящика, нужно просто чуть доставить пакеты и сконфигурить геймпады.
                • 0

                  Для геймпадов от первой и второй соньки? Я просто не в курсе. То, что для ящикового геймпада есть поддержка, это понятно. Самый популярный геймпад. Я знаю, что у Adafruit, есть приблуда для подключения геймпада от PS2, но она какая-то большая и многослойная.

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