Реактивное введение в программирование Game Boy Advance (часть 1 из 8)
Оглавление
Часть 1: Инструментарий, основы, кнопки, таймеры
Часть 2: Пиксельные видеорежимы
Часть 3: Тайловые видеорежимы
Часть 4: Спрайты
Часть 5: Вращающиеся фоны и прозрачность
Часть 6: Прерывания, DMA
Часть 7: Звук (Direct Sound)
Часть 8: Сохранения
Game Boy Advance — довольно интересный аппарат. Уже, конечно, устаревший и физически и морально он находится в достаточно интересной позиции на шкале соотношения архитектурной сложности к имеющимся возможностям. ЦП в нём это 32-битный ARM с частотой 16МГц, а видеоадаптер по своим возможностям несколько лучше видеоадаптера 16-битной консоли от Nintendo же — Super Nintendo Entertaiment System (SNES). В результате программировать под платформу становится возможно на вполне привычном современном компиляторе (GCC), но при этом она насквозь пропитана аппаратными решениями 8/16-битных консолей, особенно что касается видеосистемы. При этом сложность аппаратной начинки еще сильно далека от ускорителей 3D-графики, поэтому я и написал это краткое и быстрое введение в программирование под Game Boy Advance, чтобы можно было вникнуть в него буквально за один вечер (ну может два) на уровне достаточном, чтобы написать полноценную игру.
Это, однако, не будет поэтому уроком программирования — ни в коей мере — предполагается что вы уже знаете архитектуру компьютера и язык(и) C/C++ в достаточном объёме чтобы не буксовать на том что такое порты ввода-вывода, volatile или прямой доступ в память.
Таким образом, если подобные знания у вас уже есть, то буквально за один вечер вы сможете почувствовать себя настоящим разработчиком под настоящую портативную консоль. При этом, чтобы увидеть результат не обязательно иметь настоящую консоль и flash-картридж к ней. Для проверки трудов можно использовать любой из многочисленных эмуляторов системы.
Программировать мы будем под Windows. По идее это должно получаться даже замороченнее, нежели если вести разработку под Linux, ибо все утилиты пакетов заточены под последнее, даже после переноса под Windows там предполагается использование так называемого msys. Для простоты в начале уроков я полностью отказываюсь от предполагаемого тулчейна (как раз через msys) и делаю всё на адских батниках. Как работать хотя бы через make можно посмотреть в исходниках Contra Force Advance (cfa) в папке с исходниками (см. ниже).
В ходе написания этой серии статей были использованы материалы из следующих источников:
1. Книга «Неофициальное введение в создание игр под Game Boy Advance» (англ.): www.freeinfosociety.com/media/pdf/2901.pdf
2. Сайт с уроками и ссылками (англ.): www.loirak.com/gameboy/gbatutor.php
3. Сайт с техническими спецификациями (предпочтительный) (англ.): problemkaputt.de/gbatek.htm
4. Сайт с техническими спецификациями (для кросс-сверок сведений) (англ.): www.zap.pe.kr/projects/arm7tdmi/docs/gba_spec.htm
Последний пакет со всеми использованными исходниками и ресурсами находится здесь: yadi.sk/d/djA-upj1-oc1hA
1. Среда разработки и Hello, world!
Для компиляции примеров из этих исходников нужно установить среду разработки Dev Kit Pro.
На момент выкладывания данной статьи (2018-09-26) это можно сделать следуя следующим инструкциям:
1) Идём на devkitpro.org/wiki/Getting_Started
2) Переходим на скачку графического инсталлятора для Windows: github.com/devkitPro/installer/releases/tag/v3.0.3
3) Качаем и запускаем последнюю его версию (на момент выкладывания статьи это 3.0.3) github.com/devkitPro/installer/releases/download/v3.0.3/devkitProUpdater-3.0.3.exe
(путешествие по ссылкам расписано так подробно на случай изменения ссылок в будущем создателем Dev Kit Pro)
4) На первом вопросе в установщике выбрать первый пункт (Download and install).
5) На втором вопросе первый пункт (Keep...) сохранит скачанные установочные
файлы после установки для возможности повторно установить среду, второй же (Remove...)
удалит их, если они в дальнейшем не нужны (я выбираю второе).
6) На третьем вопросе оставить галочки напротив пунктов:
— Minimal System
— GBA Development
остальные пункты можно убрать.
7) На следующем вопросе выбираем путь для установки — желательно выбрать папку не содержащую
путей с русскими буквами и пробелами в названиях папок, например C:\DevKitPro.
При установке возможно появление ошибок скачки — нажимаем кнопку «Повтор» («Retry»).
Эта ошибка довольно назойлива и не исчезает у этого пакета годами, поэтому не отчаивайтесь даже после десятого раза в разных местах установки — нажимайте на «Повтор» еще раз. Иногда помогает повторить процесс через пару часов.
Если ошибка начинается с первых же секунд и процесс решительно не продвигается вперёд, то может помочь следующее — откройте Internet Explorer (32 бит), в меню Сервис->Свойства обозревателя зайдите в закладку «Дополнительно» и там поставьте галочки напротив всех пунктов SSL и TLS в первой же ветке «Безопасность». Звучит бредово, но похоже инсталлятор качает файлы с помощью программной компоненты IE и похоже в старых его версиях могут появляться невнятные ошибки с обновлёнными протоколами SSL с разными серверами в облаке. Как то так.
Опыт показывает, что со временем менялось и место где выложен DevKitPro и внутреннее его устройство, отчего прошлая редакция руководства просто устарела в течении (полу)года. Поэтому если у вас возникают какие то непреодолимые трудности, то можете попробовать связаться со мной на этом сайте или, например, по почтовому адресу aa-dav на основном почтовом сервере яндекса с одноимённым названием.
Hello, world!
1. Создайте папку где будут находится ваши первые пробы пера (или распакуйте папку с исходниками по ссылке выше).
2. Если создаёте папку с нуля, то создайте в ней файл build_gba.bat со следующим содержимым:
@REM Укажите в следующей строке путь до папки DevKitPro!!!
@set DEVKITPRO=d:\devel\devkitpro
@set DEVKITARM=%DEVKITPRO%\devkitarm
del %PROGNAME%.gba
@set PATH=%DEVKITARM%\bin;%DEVKITARM%\arm-none-eabi\bin;%PATH%
@set CXX=arm-none-eabi-g++.exe
@REM Скомпонуем параметры командной строки для компилятора
@set PARAMS=-specs=gba.specs -mcpu=arm7tdmi -mtune=arm7tdmi
@set PARAMS=%PARAMS% -fomit-frame-pointer -ffast-math -fno-rtti -fno-exceptions
@set PARAMS=%PARAMS% -mthumb -mthumb-interwork -g0 -O2
@REM Если нужны библиотеки из DevKitArm - раскомментировать следующие 2 строки
@REM set PARAMS=%PARAMS% -I%DEVKITPRO%\libgba\include
@REM set PARAMS=%PARAMS% -L%DEVKITPRO%\libgba\lib
%CXX% %PARAMS% %PROGNAME%.cpp %MODULES% -o %PROGNAME%.elf
objcopy -O binary %PROGNAME%.elf %PROGNAME%.gba
%DEVKITPRO%\tools\bin\gbafix %PROGNAME%.gba
@REM Если вы не работаете через консоль Windows, то уберите '@REM' перед
@REM командой pause ниже, чтобы видеть результаты работы компилятора и линкера
@REM во всплывающем окне, иначе оно будет мгновенно исчезать.
@REM pause
Во второй строчке, где написано set DEVKITPRO=d:\devel\devkitpro исправьте путь d:\devel\devkitpro на путь к папке куда вы установили DevKitPro. Если вы не работаете в настоящей консоли Windows, то уберите «REM в начале последней строки build_gba.bat чтобы видеть результаты работы компилятора и линкера во всплывающем окне, иначе оно будет мгновенно исчезать.
Данный файл будет использоваться нами как минималистичный „тулчейн“ под Windows для сборки простейших проектов для GBA. Для реальной разработки он слишком минималистичен, но для быстрого старта более чем достаточен. Помните, что запускать этот файл напрямую никогда не надо.
3. Тут же создаём файл 01_hello.cpp со следующим содержимым (взят со страницы www.loirak.com/gameboy/gbatutor.php):
/* hello.c - Gameboy Advance Tutorial - Loirak Development */
#define RGB16(r,g,b) (r+(g<<5)+(b<<10))
int main()
{
char x,y;
unsigned short* Screen = (unsigned short*)0x6000000;
*(unsigned long*)0x4000000 = 0x403; // Режим 0x3
// очистить экран
for(x = 0; x<240;x++) // цикл по колонкам
{
for(y = 0; y<160; y++) // цикл по строкам
{
Screen[x+y*240] = RGB16(0,0,31); // закрашиваем пиксель в координатах (x,y)
}
}
// нарисуем слово HI на заднем фоне
for(x = 20; x<=60; x+=15)
for(y = 30; y<50; y++)
Screen[x+y*240] = RGB16(31,31,31);
for (x = 20; x < 35; x++)
Screen[x+40*240] = RGB16(31,31,31);
while(1){} // бесконечный цикл
}
4. Далее создайте файл build_01_hello.bat со следующими строками:
@set PROGNAME=01_hello
@set MODULES=
@call build_gba.bat
Данный простой файл будет с помощью build_gba.bat производить сборку нашего „Hello, world!“. В первой строке указывается имя основного файла с исходным кодом (без расширения .cpp), во второй строке после знака равно указываются все второстепенные модули которые им используются (с расширением .cpp) разделённые пробелом, если они есть. С этим мы столкнёмся в следующих уроках.
5. Запускаем файл make_01_hello.bat (например двойным щелчком) — в папке должен появится файл 01_hello.gba.
6. Запускаем Visual Boy Advance M, открываем им файл hello.gba и видим:
Наш „привет мир“ заработал, с чем вас и поздравляю!
2. Основы
DevKitPro включает в себя компилятор GCC (в рамках DevKitARM), настроенный под платформу, плюс содержит C Runtime Library (CRT), заточенную под эту консоль.
Заточка в основном заключалась в обрезании всего лишнего и подгонке под реалии разметки памяти и самой памяти.
Т.к. мы имеем дело с консолью с картриджем, то всё что мы создаём в среде разработки — это по сути бинарный образ этого картриджа.
На самом деле в бинарном образе должны быть: соответствующий заголовок с названием игры и производителя, опционально логотип нинтендо, определенные правила куда BIOS передаёт управление, как обрабатываются прерывания и т.п. — очень многие эти вещи CRT берет на себя и мы о них сразу же можем не задумываться и писать код как привыкли в C/C++ с main и так далее. Тем не менее важно знать раскладку памяти девайса. Опишем её сразу кодом, расписывая стартовые адреса соответствующих блоков и их размеры.
// Внешняя для ЦП память, 256Кб, но с большими таймингами, нежели внутренняя.
// CRT располагает в ней кучу.
#define EW_RAM_START 0x02000000
#define EW_RAM_SIZE 262144
// Внутренняя память в ЦП, 32Кб, но с минимальной задержкой и макс. пропускной способностью.
// Линкер располагает здесь глобальные переменные и в самом конце стек.
#define IW_RAM_START 0x03000000
#define IW_RAM_SIZE 32768
// Порты ввода-вывода.
#define IO_RAM_START 0x04000000
#define IO_RAM_SIZE 1024
// Палитры задников и спрайтов по 512 байт каждая.
#define BGR_PAL_RAM_START 0x05000000
#define BGR_PAL_RAM_SIZE 512
#define SPR_PAL_RAM_START 0x05000200
#define SPR_PAL_RAM_SIZE 512
// Видеопамять, 96Кб.
#define VID_RAM_START 0x06000000
#define VID_RAM_SIZE 98304
// Память спрайтов, 1Кб.
#define OAM_RAM_START 0x07000000
#define OAM_RAM_SIZE 1024
// Начало ROM картриджа.
// Линкер располагает здесь код и константы.
#define ROM_START 0x08000000
Наглядно видно, что по сути самый старший байт 32-битного адреса является в консоли своеобразным селектором из той или иной области памяти. Глобальные переменные и стек располагаются в IW_RAM, объём которой всего 32Кб и если вы за него выйдете — будет просто зависон и некому будет даже рассказать о сегфолте.
Немного более медленная EW_RAM объёмом 256Кб отдана под кучу, причём адрес первой же аллокации начинается с 8-ого байта от ER_RAM_START, т.е. видно что прямо вся видимо отдана под это дело.
Код и константные данные сразу же линкуются в ROM начиная с адреса ROM_START и выше. Пенальти за доступ в эти области в сущности не такой уж и огромный, особенно для исполняющегося кода, который при последовательном выполнении получит оптимизацию 1/2WS последовательного доступа к ROM.
Немного технической информации здесь (этот параграф сейчас можно спокойно пропустить до конца) о скорости доступа к банкам памяти: все они, кроме IW_RAM сидит на 16-битной шине, в то время как у IW_RAM полноценная 32-битная шина. Уже за счёт этого она побыстрее будет на чтение собственно int-ов.
Во вторых самые быстрые банки — IW_RAM и VID_RAM читаются за 1 цикл. Другие памяти требуют дополнительных циклов на чтение (так называемые wait state-ы).
Так, EW_RAM нуждается еще в 2-ух дополнительных циклах для чтения (2WS), таким образом всего чтение из неё требует 3 цикла. Т.е. IW_RAM и VID_RAM являются 0WS (0 wait states). С ROM картриджа еще интереснее — бывает ПЗУ с двумя скоростями: быстрая 3/1WS и медленная 4/2WS.
Первое число это чтение случайного адреса. Но контроллер ROM имеет фичу — считывание кеширует следующую ячейку и последовательное чтение занимает меньше wait state-ов — это и есть второе число.
Вообще всё с чем имеет дело программа — это огромное адресное пространство и порты ввода-вывода отображены на него и RAM и ROM и видеопамять — вообще всё. Поэтому зачастую самый просто способ поначалу подключить в игру какой то двоичный ресурс является просто утилитка перегоняющая его в исходник на C/C++ с кодом вида:
const unsigned char DataNameXXXX = { 0x00, 0x01, 0x02 ... };
… и просто адресацией его из программы как собственно массива — всё. Никаких файлов, никаких потоков ввода-вывода, хотя в CRT реализации этих вещей даже присутствуют, но состоят как правило из одной строчки: return E_NOTIMPL;
В системе присутствует BIOS в котором даже присутствуют некие функции, которые можно вызывать посредством софтварных прерываний — как int 21h в DOS, но по первому взгляду они довольно бестолковы и ничего что нужно истинному игроделу, по крайней мере в самом начале творческого пути, там нет. Никакого привычного так называемого API здесь нет.
3. Взаимодействие с аппаратурой, кнопки и таймеры
Итак, как же осуществляется тогда общение с аппаратурой? Напрямую записью в порты ввода-вывода, которые просто отображены на адресное пространство процессора.
Вот типичный заголовок для общения с геймпадными кнопками:
// Подсократим названия базисных типов на будущее:
typedef signed char i8;
typedef unsigned char u8;
typedef signed short i16;
typedef unsigned short u16;
typedef signed int i32;
typedef unsigned int u32;
// KEYS IO
#define REG_KEYS (*((volatile u16 *) 0x04000130))
#define REG_KEYCNT (*((volatile u16 *) 0x04000132))
#define KEY_A 1
#define KEY_B 2
#define KEY_SELECT 4
#define KEY_START 8
#define KEY_RIGHT 16
#define KEY_LEFT 32
#define KEY_UP 64
#define KEY_DOWN 128
#define KEY_R 256
#define KEY_L 512
Итак, вот здесь и виден главный способ общаться с железом — закрепляем в макросе адрес в котором лежит нужный нам порт ввода-вывода и просто его читаем или записываем как обычно. Не надо заботится ни о каких временных промежутках или паузах между записями в порты — аппаратура GBA сама все такие тонкости разруливает (даже удивительно), так что остаётся только не забывать объявлять эти участки памяти как volatile.
Здесь порт REG_KEYS — порт на чтение хранящий текущее состояние битовой маски нажатых кнопок. Т.е. STATE кнопок. Свободно читается из любого места программы и так же спокойно по маскам проверяется состояние любой кнопки. Надо только помнить, что в системе GBA 0 означает что кнопка нажата, а 1 — отжата.
А вот порт REG_KEYCNT — порт на запись, которым можно управлять EVENT-ами кнопок, т.е. прерываниями, но они нам пока не нужны и эту тему подниму в будущем.
Вот код использования:
if ( !(REG_KEYS & KEY_A) ) // помним, что 0 - нажато и 1 - отжато.
wait_for_vblank = true;
if ( !(REG_KEYS & KEY_B) )
wait_for_vblank = false;
Второе, после вышеописанных кнопок, что можно рассказать о программировании для GBA это таймеры.
Платформа имеет 4 аппаратных таймера, каждый из которых представляет собой 16-битный счётчик.
Они имеют номера от 0 до 3. Каждый из них может быть выключен или включен.
Каждый независимо может работать на одной из четырёх частот: системной (частота шины) и дробления её на 64, 256 или 1024, таймеры соответственно инкрементируются.
Помимо инкремента по таймеру любой из них можно перевести в режим инкремента по переполнению предыдущего (по номеру) таймера (таким образом для нулевого этот режим не имеет смысла), что можно трактовать как таймеры повышенной разрядности (до 64 бит).
Можно так же включить запуск прерывания по переполнению любого из таймеров, но про прерывания я напишу много позднее.
Кроме того, при переполнении, таймер заполняется значением „по умолчанию“, которое можно переопределить.
Интерфейс таймеров всецело описывается следующими дефайнами на C/C++:
// Timers
#define TIMER_FREQUENCY_SYSTEM 0x0 // 16.78 МГц (системная частота)
#define TIMER_FREQUENCY_64 0x1 // 262.21 кГц
#define TIMER_FREQUENCY_256 0x2 // 65,536 кГц (т.е. 65536 раз в секунду)
#define TIMER_FREQUENCY_1024 0x3 // 16,384 кГц
#define TIMER_OVERFLOW 0x4 // инкремент по переполнению предыдущего по номеру таймера (отменяет инкремент по частоте!)
#define TIMER_IRQ_ENABLE 0x40 // включает генерацию прерываний
#define TIMER_ENABLE 0x80 // вкл/выкл таймера как такового
#define REG_TM0D (*((volatile u16 *) 0x4000100))
#define REG_TM0CNT (*((volatile u16 *) 0x4000102))
#define REG_TM1D (*((volatile u16 *) 0x4000104))
#define REG_TM1CNT (*((volatile u16 *) 0x4000106))
#define REG_TM2D (*((volatile u16 *) 0x4000108))
#define REG_TM2CNT (*((volatile u16 *) 0x400010A))
#define REG_TM3D (*((volatile u16 *) 0x400010C))
#define REG_TM3CNT (*((volatile u16 *) 0x400010E))
Порты заканчивающиеся на CNT (CoNTrol, это вообще правило для консоли — все управляющие (на запись) порты дополнять суффиксом 'CNT') управляют каждым из таймеров. Записываются, по сути, битовые комбинации перечисленных флагов.
Порты TM*D при чтении выдают текущее значение каждого из таймеров, а по записи вменяют таймеру начальное значение после переполнения. По сути начальное значение начинает работать только со следующего переполнения таймера, а по умолчанию (после рестарта консоли) равно нулю.
В следующий раз напишу код как раз с выводом на экран таймеров и реакцией на клавиатуру, закрепляя вышенаписанное.
Продолжение...
6 комментариев
Кажется, у нас новая платформа-кандидат на победу в ZX Spectrum 640k demo compo 2019!
Например вот:
Качество видео отстойное и что-то не могу найти лучше на ютубе. Лучше посмотреть на эмуляторе хотя бы взяв образ картриджа отсюда: www.pouet.net/prod.php?which=19030