Архитектура и программирование Sony Playstation 1

в 17:42, , рубрики: intro, mips, psx, r3000, RISC, Sony PlayStation, ассемблер, Демосцена, ретро-компьютеры
Unirom запущенный на PSX (про 64K RAM - это шутка)

Unirom запущенный на PSX (про 64K RAM - это шутка)

По сравнению с другими, ранее описанными мной архитектурами, архитектура Sony Playstation 1 (PSX) - сравнительно современная. И дело даже не в годе выпуска (1994) - скорее это общее ощущение сочетания новых возможностей и исчезновения привычных старых, которые были типичными для компьютеров и приставок предыдущей эпохи.

Уже то, что минимальный заголовок стандартного исполняемого файла - 2кб, говорит о многом. Подобную расточительность сложно было представить в недавнем ещё прошлом.

PSX (это сокращение пошло от первоначального названия проекта - Playstation X) имеет в качестве центрального процессора MIPS R3000, работающий на частоте 33МГц. Причём, Sony отказалось от сопроцессора для вычислений с плавающей точкой и вместо него сопроцессором в PSX является так называемый GTE (Geometry Transformation Engine), выполняющий различные операции с фиксированной точкой над векторами и матрицами. Другими отдельными блоками являются:

GPU (Graphics Processing Unit) - отвечает за отрисовку на экране примитивов: треугольников, прямоугольников, линий и т.п.

SPU (Sound Processing Unit) - отвечает, преимущественно, за проигрывание сэмплов с различной постобработкой.

Кроме того, есть ещё MDEC (Macroblock Decoder), отвечающий за декодирование сжатых (DCT+RLE) изображений, контроллеры CDROM, DMA, прерываний, разные порты ввода-вывода.

Процессор адресует 2MB ОЗУ, но ни видео ни аудио контроллеры непосредственного доступа к нему не имеют. У них собственные ОЗУ, обмен с которыми происходит по DMA.

В ПЗУ объёмом 512KB находится BIOS. В нём имеется набор подпрограмм, преимущественно общего назначения - типа работы с файлами, выделения памяти и пр,, а также CD Player и оболочка для работы с Memory Cards.

Карточки Memory cards содержат NVRAM объемом 128кб. Они предназначены для сохранения и восстановления состояния игры, плюс уязвимость BIOS позволяет использовать их ещё и для начальной загрузки произвольного кода.

CD-ROM тоже содержит своё ОЗУ (256кб) и даже процессор MC68HC05, хотя свой код в него загрузить нельзя.

Возможности платформы позволяет оценить следующий ролик:

Процессор MIPS R3000

В PSX используется микропроцессор MIPS R3000, работающий на тактовой частоте 33МГц.
Это типичный RISC процессор, со всеми свойственными RISC прелестями - и в кавычках и без. R3000 - один из ранних MIPS процессоров (классифицируется как MIPS I), хотя Mongoose-V - 12 MHz микроконтроллер в радиационно-стойком исполнении на основе R3000 - не так уж давно летал на New Horizon к Плутону.

Документации на MIPS процессоры в сети предостаточно, поскольку они много где использовались (в компьютерах Silicon Graphics, микроконтроллер PIC32 - тоже MIPS и пр.) но я всё же остановлюсь на некоторых существенных моментах.

В процессоре тридцать два 32-разрядных регистра r0 - r31 и два регистра hi, lo для использования в инструкциях mul/div.
Взависимости от ассемблера и ситуации, 32 регистра имеют названия r0-r31, $0-31 или $zero, $at, $v0-$v1, $a0-$a3, $t0-$t7, $s0-$s7, $t8-$t9, $k0-$k1, $gp, $sp, $fp, $ra.

Из регистров для своих нужд нельзя использовать r0 (zero) - в нём всегда ноль и r31 (ra) - в него сохраняется адрес возврата при вызове подпрограммы инструкцией jal (возврат из подпрограммы выполняется как jr ra).
Кроме того, лучше избегать r1 (at) - он может использоваться ассемблером как промежуточный при трансляции псевдоинструкций.

Регистр флагов отсутствует. Соответственно, нет необходимости во всяких там cmp и всё происходит прямо в инструкции условного перехода. Например beq s0,s1,label - переход на метку, если s0 == s1.

Любая инструкция занимает 32 разряда (4 байта). Поэтому, например, загрузка в регистр 32-битной константы требует более одной инструкции.

Поскольку RISC процессоры стали распространяться в ту эпоху, когда на ассемблере программировали всё реже, набор инструкций процессора заточен не под человека, а под компиляторы. Например, загрузка числа 2 в регистр r5 выглядит так:

addiu $5, $0, 0x00000002

т.е. складываем число 2 с нулём (в r0 всегда ноль) и помещаем в r5.

Даже очень большой фанат ассемблера уже через несколько часов с трудом сможет понять, что он сам имел ввиду собственным подобным кодом. Чтобы как-то смягчить страдания людей, существует такая вещь как псевдоинструкции.

Вместо того, что выше, можно написать:

li $v0, 2

при ассемблировании эта псевдоинструкция превращается в вышеприведённую addiu (или ori - зависит от ассемблера).

Не любая псевдоинструкция превращается в одну инструкцию процессора. Например, если вы напишите:

li $v0, 0x012345678

то это превратится в следующее:

lui $1, 0x00001234
ori $2, $1, 0x00005678

причина очевидна - длинную константу вместе с кодом операции проблематично утрамбовать в 32 бита.

Или, к примеру, циклический сдвиг влево содержимого t2 на t3 разряда с помещением результата в t1 делается псевдоинструкцией:

rol $t1,$t2,$t3

если вы пишите tiny intro, результат ассемблирования такой псевдоинструкции может вас неприятно удивить:

subu $1,$0,$11
srlv $1,$10,$1
sllv $9,$10,$11
or $9,$9,$1

или, скажем, вы захотели вычесть из регистра число и написали:

subi $t0, $t1, 3

а потом обнаруживаете в полученном коде:

addi $1, $0, 3
sub $8,$9,$1

(это сделал MARS)

или

addi t0,t1,-3

(это сделал nvasm)

всё потому, что в MIPS нет инструкций вычитания константы (subi, subiu). Хотите вычитать - складывайте с отрицательными числами и не морочьте ассемблеру голову!

Короче говоря, различных псевдоинструкций много, причём их набор и способы интерпретации от ассемблера к ассемблеру варьируются. Если хотите быть уверенным в результате, лучше жертвовать читаемостью и псевдоинструкций избегать.

Load/delay slots

В коде для многих RISC процессоров часто можно встретить последовательность вида:

	beq	label	; или любой другой переход
	nop		; эта инструкция будет исполнена в любом случае

Это называется "branch delay slot".

Из-за особенностей логики работы конвейера, следующая после команды перехода инструкция выполняется всегда (т.е. неважно, произошёл переход или нет), поэтому туда вставляют либо nop, либо, если важна оптимизация по размеру, какую-то полезную инструкцию.

/ шутка про инструкцию перехода, вставленную сразу после инструкции перехода /

Ещё есть так называемый "load delay slot" при загрузке из памяти в регистр:

	lb	v0, data(t0)
	nop		; в v0 ещё ничего нет, содержимое будет доступно только следующей инструкции

Аналогичная проблема есть с mfhi/mult. Вроде где-то в районе MIPS R4000 - R8000 проблемы с load delay slots решили - т.е. при попытке слишком рано достать что-то из этого регистра процессор тормозит и ждёт, пока там появится верное значение, после чего отдаёт его (хотя, я не очень понимаю, как поступили при этом с обратной совместимостью).

При использовании инструкций GTE load/store тоже есть delay slots - целых две (!) инструкции. Хотя, тут показания расходятся - кое-где пишут, что не для любых таких инструкций это верно - проверять это очень муторно.

Выравнивание

Адреса, по которым происходит обращение для загрузки слова должны быть выровнены на 4 (и на 2 для полуслов).

Т.е.:

	lw	$s0, 0x10010000	;  всё хорошо
	lw	$s0, 0x10010001	; exception 

	lb	$s0, 0x10010000	;  всё хорошо
	lb	$s0, 0x10010001	;  тоже всё хорошо (потому что загружаем один байт)

Поэтому перед dw, откуда собираемся брать значение, надо писать align:

.align 4

label:
	dw	0x1234

Ещё советую погуглить про lwl и lwr - перед вами разверзнется бездна.

Сопроцессоры

У MIPS R3000 может быть четыре сопроцессора.

cop0 - зарезервирован для обработки исключений, прерываний и т.п.
cop1 - обычно сопроцессор для вычислений с плавающей точкой, но в PSX на этом сэкономили - его нет.
cop2 - GTE (Geometry Transformation Engine) - см. далее
cop3 - отсутствует в PSX

Для работы с сопроцессорами есть ряд инструкций:

LWCn cop_reg, offset(base) - загрузка значения из памяти в регистр данных сопроцессора n
SWCn cop_reg, offset(base) - сохранение значения из регистра данных сопроцессора n в память

MTCn cpu_reg, cop_reg - запись значения из регистра процессора в регистр данных сопроцессора n
MFCn cpu_reg, cop_reg - запись значения из регистра данных сопроцессора n в регистр процессора

CTCn cpu_reg, cop_reg - запись значения из регистра процессора в управляющий регистр сопроцессора n
CFCn cpu_reg, cop_reg - запись значения из управляющего регистра сопроцессора n в регистр процессора

COPn opcode - выполнение операции opcode (25 разрядный 0..1FFFFFFh) сопроцессором n

GTE

В качестве cop2 сопроцессора для MIPS в PSX выступает GTE - Geometry Transformation Engine. По сути , это некий вариант SIMD - для операций над векторами и матрицами (умножение, сложение, различные операции с RGB и пр.). Операции выполняются с фиксированной точкой. Сопроцессора для вычислений с плавающей точкой в PSX нет, с чем, к слову, связаны специфические искажения в играх.

GTE имеет шестьдесят четыре 32-разрядных регистра - 32 регистра данных и 32 управляющих.

Загрузка в регистры GTE осуществляется инструкциями mtc2, lwc2 и ctc2
Загрузка из регистров GTE инструкциями mfc2 , swc2 и cfc2

Сами команды GTE представляют собой инструкцию cop2 и 25 бит кода инструкции GTE.
Т.е., например, команда SQR будет выглядеть так:

cop2 0x0a00428

машинный код для неё: 0x4aa00428

Каждая инструкция GTE выполняется за несколько тактов.

В примерах кода для GTE (ниже) я использую синтаксис gcc inline assembler, поскольку смотреть значения в отладчике удобнее всего именно таким способом.
На мнемоники инструкций команд связанных с сопроцессорами и их регистрами нет чётких стандартов - авторы ассемблеров, отладчиков и дизассемблеров дают волю фантазии.

Общий смысл в следующем:

1.Нужно включить GTE (cop2). Для этого нужно сначала записать магическое число в регистр состояния SR cop0 (иначе ничего связанное с GTE работать не будет - даже загрузка в его регистры):

	"li    $t0, 0x40000000;"	// 30-й разряд в единицу, загружаем константу в регистр t0 процессора
	"mtc0  $t0, $12;"	// переписываем из t0 в r12 cop0 (SR)

Т.е. в инструкциях mtc, mfc первый операнд это регистр процессора, второй - регистр сопроцессора.

Если вам, по какой-то причине, дороги остальные разряды регистра состояния и не жалко памяти, можно сделать красивее:

	"mfc0  $t0, $12;"
	"li    $t1, 0x40000000;"
	"or    $t0, $t0, $t1;"
	"mtc0  $t0, $12;"

2.Загружаем что-нибудь в регистры GTE (cop2)

	"li $a0, 0x1;" // загружаем 1 в регистр a0 процессора
	"li $a1, 0x2;" // ... 2
	"li $a2, 0x3;" // ... 3

	"mtc2 $a0, $9;" // загружаем содержимое регистра a0 процессора в регистр IR1 GTE (он же r9 cop2)
	"mtc2 $a1, $10;" // ... IR2
	"mtc2 $a2, $11;" // ... IR3
	"nop;" // delay slot
	"nop;" 

3.Выполняем операцию. В данном случае это SQR - т.е. значения в регистрах IR1, IR2, IR3 возводятся в квадрат

	"cop2 0x0a00428;" // SQR 
	"nop;"
	"nop;"

4.Загружаем результат из регистров сопроцессора обратно в регистры процессора

	"mfc2 $a0, $9;" // загружаем содержимое IR1 GTE в регистр a0 процессора
	"mfc2 $a1, $10;" // ... IR2
	"mfc2 $a2, $11;" // ... IR3
	"nop;"
	"nop;"

Таким образом, на выходе получаем в регистрах a0, a1, a2 числа 1,4,9 (квадраты 1, 2, 3)

И ещё один момент - результат в регистрах IR1, IR2, IR3 обрезается (saturation) до 15 бит. Т.е. если на входе SQR будет 100, 200, 300, то на выходе будет 10000, 32767, 32767. Чтобы получить необрезанные значения, надо брать их из регистров MAC1, MAC2, MAC3 (r25, r26, r27 cop2). Там будет 10000, 40000, 90000.

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

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

GPU

GPU (Graphics Processing Unit) выполняет команды на отрисовку примитивов, список которых ему пересылается [обычно] через DMA из основной памяти. Команды должны быть перечислены в связном списке отображения (display list), где для каждого элемента (команды) перечислены:

  1. ссылка на следующую команду, либо $ffffff, если команда последняя в списке

  2. размер текущей команды (включая параметры)

  3. код команды

  4. параметры (количество зависит от конкретной команды)

Основные команды - это рисование примитивов. Примитивы бывают следующие:

  1. Закрашенный треугольник

  2. Закрашенный четырёхугольник (на самом деле состоящий из двух треугольников, что влияет на качество заливки)

  3. Прямая линия

  4. Закрашенный прямоугольник (рисуется быстрее четырёхугольника)

Примитивы могут иметь либо простую заливку цветом, либо заливку Гуро (градиентом), либо заливку текстурой.

Выглядит это примерно так:

            la a0, list    
            jal SendList                ; там десяток инструкций для настройки DMA

[...]

list

; треугольник, залитый цветом
poly    
    db line, line>>8, line>>16, $4 ; ссылка на следующую команду и размер команды
    dw $2000ff00    ; $20 - polygon. CCBBGGRR (C - command, b,g,r - color)
    dw $00100010  ; y0=16, x0=16
    dw $00e8027f    ;  y1=232, x1=639
    dw $01e000a0   ;  y2=480, x2=160

; линия одного цвета
line    
    db poly_g, poly_g>>8, poly_g>>16, $3 ; ссылка на следующую команду и размер команды
    dw $400000ff    ; $40 - линия одного цвета. CCBBGGRR (C - command, b,g,r - color)
    dw $00000000 ; y0=0, x0=0  
    dw $00f0013f ; y1=240, x1=319

; треугольник с заливкой Гуро
poly_g    
    db block, block>>8, block>>16, $6 ; ссылка на следующую команду и размер команды
    dw $30ff00ff    ; $30 - треугольник с заливкой Гуро. CCBBGGRR (C - command, b,g,r - color)
    dw $00300010 ; y0=48, x0=16
    dw $30ff0000    ; color 2
    dw $0028013f    ; y1=40, x1=319
    dw $300000ff    ; color 3
    dw $00f00080    ; y2=240, x2=128

; прямоугольник
block   
    db $ff, $ff, $ff, $3 ; ссылка на следующую команду и размер команды
    dw $02ff0000     ; $02 - прямоугольник. CCBBGGRR (C - command, b,g,r - color)
    dw $00500050   ; y0=80, x0=80
    dw $00550060   ; y1=80, x1=96
Несколько примитивов GPU на экране PSX

Несколько примитивов GPU на экране PSX

Как я уже упоминал, GPU имеет собственную память объемом 1мб. Она организована в виде фрейм-буфера размером 1024 x 512 пикселов. Каждый пиксел - одно слово (16 бит).
Доступ к этой памяти осуществляется не по адресам, а по координатам (см. параметры команд в display list).
Записью в регистры можно задать, какая именно часть этого фрейм-буфера должна отображаться. Достаточно типичным подходом является выделение в этом фрейм-буфере двух страниц, размером 320x240, на одной из которых рисуем, а вторую показываем (стандартный способ обеспечить вывод без мерцания).
При этом оставшаяся часть фрейм-буфера используется для текстур, шрифтов, спрайтов и т.п. (помимо команд примитивов, есть команды для перемещения блоков между основной памятью и видеопамятью, в любых сочетаниях).

Содержимое GPU VRAM (слева - две страницы 320x240 каждая)

Содержимое GPU VRAM (слева - две страницы 320x240 каждая)

Кстати, понятие "спрайт" (sprite), которое фигурирует в документации на PSX, спрайтом, в традиционном смысле этого слова, не является. Это просто прямоугольник с текстурой.

Хотя, как я сказал, типичным является использование разрешения 320x240, это не единственный вариант. Если не нужны переключения страниц, можно использовать, например, режим 640x480. Единственно, в этом случае нужно включить развертку с чередованием строк (interlaced), иначе обычный композитный монитор не сможет такое разрешение отобразить. Соответственно, изображение будет заметно мерцать.
Что касается цветов, то отображается 15 бит (32768 цветов) на пиксел. Режим 24 бит, который есть в документации, касается только MDEC декодирования видеопотока с CD-ROM.

Что касается отображения 3D объектов, то GPU этого не умеет. Всё это надо делать вручную, полигонами, пользуясь для расчётов GTE. При этом сортировку тоже надо делать самому с учётом того, что каждый следующий примитив списка рисуется поверх предыдущего.

SPU

Основная функция SPU (Sound Processing Unit) - по сути, проигрывание ADPCM сэмплов (16 бит со знаком) независимо по каждому из 24 каналов с формированием ADSR огибающей. SPU имеет свою память - буфер размером 512кб, в который сэмплы загружаются через DMA из основной памяти.

Настройка громкости, огибающей, реверберации и прочих вещей осуществляется записью по определённым адресам ОЗУ, на которые отображаются регистры SPU (все они 16-разрядные).

Примеры проигрывания сэмплов ищутся довольно легко, я же в качестве примера покажу более редкий режим использования SPU - генерация шума. Этот режим не требует сэмплов, а значит обеспечивает наименьший возможный размер программы (в данном случае - около 80 байт):

        org $80010000
            
        lui r27,$1F80           ; I/O base
	
	    li r8,$ea30	            ; 1100000100110000 enable noise (no adpcm)
        sh r8,$1DAA(r27)        ; SPU_CONTROL

        li r8,$3fff             ; master volume = 011111111111111

        sh r8,$1d80(r27)        ; master volume left
        sh r8,$1d82(r27)        ; master volume right

        li r8,$1010		        ; SPU buffer address

        sh r8,$1C06(r27)        ; set SPU buffer address 

        li r8,$3fff             ; volume = 011111111111111

        sh r8,$1C00(r27)        ; volume left
        sh r8,$1C02(r27)        ; volume right
	
  	    li r8,$bf3f		        ; 1011111100111111
        sh r8,$1c08(r27)        ; SPU_CH_ADSR1

	    li r8,$cfff		        ; 1100111111111111
        sh r8,$1c0a(r27)        ; SPU_CH_ADSR2

	    li r8,1
        sh r8,$1d94(r27)        ; SPU_NOISE_MODE1

	    li r8,1
        sh r8,$1d88(r27)        ; SPU_KEY_ON1

	    jr ra
	    nop

Для шума здесь указывается частота (в SPU_CONTROL), громкость, ADSR. Проигрывание идёт в канале 0.

Ещё раз подчеркну, что режим генерации шума и режим проигрывания ADPCM сэмплов - взаимоисключающие (бит в SPU_CONTROL). Не всё, что работает для сэмплов, будет работать для шума.

Порты

Parallel port - есть только в старых моделях PSX (у меня в SCHP-7502 есть). Полезен разве что для PSIO и всяких картриджей типа Action Replay. Ну и есть какие-то совсем экзотические устройства, типа приставки для просмотра VCD.

Serial port - используется для многопользовательских игр. Через этот порт можно соединить две PSX кабелем. Впрочем, игр которые это поддерживали - было не очень много. А позднее, в PS One, Sony убрала и этот порт.

Memory card - в этот разъём вставляются memory cards, который предназначены для сохранения состояния игр. Их ёмкость 128кб, хотя встречаются левые китайские на 64кб. Благодаря уязвимости, с memory cards может быть запущен код (см. UNIROM).

Video - композитный видео выход + звук. Кое-где пишут, что на этом разъеме якобы есть и S-Video, но это не точно (возможно, зависит от модели PSX).

Game pads - для подключения геймпадов

Эмуляторы

Существует несколько эмуляторов, из которых у разработчиков популярны no$psx и pcsx-redux.

no$psx у меня работает крайне нестабильно - т.е. очень многие нормальные exe'шники не работают, причём на двух разных PC (Win10). Это довольно странно, т.к. вроде бы этот эмулятор считается совершенно рабочим.

Эмулятор NO$PSX

Эмулятор NO$PSX

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

Эмулятор PCSX-Redux

Эмулятор PCSX-Redux

Из командной строки exe файлы запускаются так:

pcsx-redux.exe -exe filename.exe -run

Из популярных можно ещё упомянуть Duckstation.

К слову, ни один из известных мне эмуляторов не умеет записывать в видеофайл.

С эмуляторами поставляется некий open bios, так что рекомендуется ещё найти и скачать оригинальный bios от PSX (впрочем, это необязательно).

В дополнение упомяну, что существует несколько online и offline эмуляторов отдельно MIPS процессоров, что полезно для быстрой проверки фрагментов кода и обучения. Я использовал MARS - он мне показался наиболее удобным.

Эмулятор MIPS процессора - MARS

Эмулятор MIPS процессора - MARS

Разработка

Хотя были выпущены аж две версии PSX для разработчиков, в настоящее время они представляют собой скорее коллекционную ценность, нежели практическую. Теперь есть более современные и удобные средства разработки.

Assembler

Можно использовать gcc от PSn00bSDK, если нравится его синтаксис (регистры начинающиеся с $ и прочие прелести), либо один из двух ассемблеров:

nv-spasm, это современная, работающая под Win10, вариация на тему spasm (exe'шник можно найти здесь), который на выходе даёт готовый PSX exe, либо, если указать соответствующий параметр, бинарник.
Бинарник без заголовка может быть полезен, если вы хотите написать маленькое интро, т.к. заголовок exe'шника добавляет к коду несколько килобайт.

У nv-spasm есть ряд проблем. К примеру, там очень слабый препроцессор - он не понимает скобки, не поддерживаются двоичные литералы (%xxxx), cop2 почему-то ассемблирует в инструкцию соответствующую cop0, для neg s3,s3 вообще не генерит кода (хотя по логике должно быть sub s3,zero,s3 ) и т.д.

Есть более мощный ассемблер armips (к примеру, там есть макросы), но с ним придётся делать exe из бинарника отдельным скриптом.

Мне первым попался nv-spasm и, купившись на то, что он генерит сразу нормальные PSX exe/bin я с ним изрядно намучался. Рекомендую не повторять моей ошибки и попробовать armips.

C

Есть несколько вариантов SDK, но современный, не требующий экзотики типа Windows 95/XP - фактически только PSn00bSDK. Он представляет собой обычный GCC с библиотеками и примерами. Под Win10 и Linux работает.

В документации ( psn00bsdksharepsn00bsdkdoc ) всё более-менее разжевано. Вкратце: ставите дополнительно cmake, прописываете путь к bin и переменную PSN00BSDK_LIBS, после чего берёте пример из psn00bsdksharepsn00bsdkexamples и запускаете в его директории:

cmake -S . -B ./build -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=C:/work/psx/psn00bsdk/lib/libpsn00b/cmake/sdk.cmake
cmake --build ./build

Получаете .exe в build/

Если нужен не просто exe, а CD образ, посмотрите тут. psn00bsdksharepsn00bsdktemplate

Для отладки в vscode можно использовать VSCodePSX. Там ставится Native Debug расширение, потом gdb-multiarch, настраивается PCSX-Redux и создаётся .vscode/launch.json со ссылками на gdb и исполняемый файл. Всё достаточно просто.

Отладка непосредственно на железном PSX тоже возможна, в том числе есть отдельный PSn00b-Debugger, хотя я не пробовал - отлаживал на эмуляторе, а на PSX проверял. Постоянно проверять на железе необходимо, т.к. сплошь и рядом код работал на эмуляторе, но напрочь не работал (или глючил) на PSX.

PSn00b Debugger

PSn00b Debugger

Замечу, что для ассемблера есть работающий проигрыватель .MOD (hitmod), а вот для C нет. Точнее есть, но старый - на psn00bsdk его так просто не переделать. Приделать же к C плеер написанный на ассемблере у меня сходу не вышло, ввиду нехватки опыта работы с gcc/ld. Если у кого-то есть готовое решение, просьба написать.

Загрузка кода в PSX

Есть как минимум два способа, каждый под свою задачу. Первый больше подходит для тех, кто планирует писать полноценную большую игру и часто запускать код на PSX. Это PSIO - аппаратный эмулятор CD привода.

В этом варианте образ CD, записанный на SD карту, PSX видит как диск. У устройства есть ряд недостатков - например, заливать код по USB (а не через SD карту) умеют вроде как не все прошивки этого устройства, причём в китайских клонах прошивки не обновляются, а оригинальных PSIO на момент написания этой статьи у изготовителя в наличии нет.
Кроме того, это устройство вставляется в параллельный порт PSX, который есть только в старых моделях (уже начиная с SCPH-9001 он в некоторых есть, в некоторых нет).

Второй, более гуманный и бюджетный способ заключается в использовании Serial port. Он позволяет заливать прямо с PC в память PSX код по RS232. Недостаток в том, что залить так образ диска не получится. Зато для проверки, работает ли отлаженный вами на эмуляторе EXE или бинарник - вполне годится.

Для того, чтобы PSX смогла принимать данные через Serial port, вначале необходима ещё одна процедура - нужно, чтобы на стороне PSX был запущен Unirom - он будет принимать данные и выполнять команды.

UniROM

UniROM
  1. Скачиваете unirom boot disk;

  2. Записываете uniroom boot disk на CD-R (на наименьшей возможной скорости, для надёжности. CD-RW не подойдёт!). Например, через imgburn;

  3. Вставляете диск в PSX. После загрузки появится меню. Вставляете в любой слот memory card. Выбираете install to memory card. Производите запись. Можно конечно не записывать на memory card и каждый раз грузиться с boot disk-а, но это неудобно.

  4. Теперь уже без диска вставляете этот memory card во ВТОРОЙ слот и при включении PSX выбираете "memory card" в главном меню приставки. Загрузится unirom basic.

PSX готова к приёму данных.

Далее в Serial port PSX втыкается (точнее подпаивается, т.к. вряд ли вы найдёте такой разъем) FT232RL FTDI USB to TTL адаптер, который должен быть переключен перемычкой в 3.3 вольта. С другой стороны в него втыкается USB. Минимум проблем - у меня всё заработало со второго раза, т.к. в первый раз я перепутал RX с TX. Win10 сходу опознала адаптер и поставила драйвер, даже ничего не спросив. Адаптеры продаются свободно, хотя бы на Озоне. Главное смотрите, чтобы было переключение в 3.3 вольта.

FT232RL FTDI USB to TTL адаптер

FT232RL FTDI USB to TTL адаптер

Теперь скачиваете nops.exe (пакет NOTPSXSerial) и запускаете его с параметрами вида:

nops /exe /fast helloworld.exe com7

Код загружается в PSX и тут же запускается. Можно загрузить бинарник и запустить его с заданного адреса:

nops /fast /bin 0x80010000 test.bin com7
nops /fast /jmp 0x80010000 com7

Замечу, что nops /ping у меня не работает, однако при этом проблем с передачей данных нет - имейте ввиду.
Ключ /fast увеличивает скорость до 518400 (вместо 115200).
Там ещё много всяких параметров, в частности для отладки. Но мне это не было нужно.

Добавлю, что для всех этих операций PSX должна быть чипованная. Т.е. доработанная так, чтобы могла читать не только фирменные диски, но и записанные кем угодно.
В принципе, у нас нечипованные наверное и не продаёт никто, но всё же при покупке нужно спрашивать.
Чипование визуально выглядит как небольшой чип с проводами, прилепленный на обратную сторону платы. Есть сложносочинённые способы обойтись без чипования, но проблем будет больше, чем экономии.

Так выглядит "чипование" PSX

Так выглядит "чипование" PSX

Интро

Как обычно, чтобы на практике прочувствовать платформу, я написал небольшое (812 байт) простенькое интро - что-то типа синей плазмы под шум волн.
Про реализацию шума я уже писал (в данном случае он ещё периодически перезапускается по счетчику), а что касается плазмы, то использована возможность аппаратно рисовать четырёхугольники с градиентной Гуро заливкой.
Углы четырёхугольника соответствуют углам экрана и в каждом из углов цвет циклически меняется от черного до синего и обратно. Причём, фаза и скорость изменения цвета в разных углах отличаются, таким образом изменения кажутся более или менее случайными.

Полагаю, что если сильно повозиться, можно уменьшить объем кода в полтора-два раза, но всё равно - для столь примитивного эффекта, к тому же выполняемого GPU, MIPS архитектура требует непомерно много байт.

Эпилог

Как-то я писал, что создание программ на ассемблере для старого железа как бы устанавливает связь с людьми, которые его делали. Так вот, конкретно с PSX этого уже не ощущается. Программирование для него больше похоже на программирование STM32, чем скажем TI 99/4a или Commodore 64.

В частности, везде нужно инициализировать множество регистров, без чего вообще ничего не заработает. В системе ни для чего не существует разумных настроек "по умолчанию". Очень ощущается, что разработчики железа вообще не переживали насчёт тех, кто будет его программировать напрямую - ожидается, что люди будут пользоваться фирменным SDK и писать на C.

Собственно, подавляющее большинство демо, интро и игр, созданных уже после того, как PSX "сошла со сцены" - написаны именно на C. Правда, делалось это в те годы, когда Win95/98/XP была актуальной и, соответственно, можно было использовать старые SDK.

Напоследок, коллекция Cracktro для PSX:

Автор: Пётр Соболев

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js