… или как выстрелить себе в ногу на Arduino
В летней компьютерной школе мы используем для обучения разработке игр собственноручно сделанный старый компьютер.
Сейчас в нём установлена плата Arduino Mega с процессором ATmega2560, в котором целых 256 килобайт флеш-памяти. Предполагалось, что этого хватит очень надолго, ведь игры получаются простые (экран-то всего лишь 64x64 пикселя). В реальности мы столкнулись с некоторыми проблемами уже по достижении прошивкой размера примерно 128 килобайт.
В памяти программ, несмотря на её название, кроме исполняемого кода игр хранятся всякие неизменные данные типа спрайтов и таблиц уровней. Этих данных не так уж и много.
Но когда мы подключили к нашему компьютеру звуковой чип YM2149F, и загрузили пару десятков мелодий в ту же программную память, начались проблемы.
Приставка зависала при попытке проиграть мелодию, либо рисовала какой-то мусор в меню игры. Непонятно было как это вообще отлаживать, ведь процессор не только занимается логикой игры, но и выводит изображение и звук. В итоге оказалось, что компилятор gcc-avr использует для хранения указателей переменные размером в два байта. Но адресовать 256 килобайт всего двумя байтами невозможно! Как же он выкручивается?
Указатели на код
Во-первых, инструкции вызова функций и переходы могут использовать трёхбайтовые адреса. Поэтому линкеру достаточно подставить полный адрес в такую инструкцию и всё заработает. Если же адрес функции передаётся через указатель, то такой номер не пройдёт — ведь указатель-то у нас двухбайтовый.
В такой ситуации gcc вставляет в нижних 64кб «трамплин» — инструкцию jmp, которая переходит на нужную функцию. Тогда в качестве адреса функции, который надо хранить в переменной, будет выступать адрес этого трамплина — ведь он же помещается в два байта. А при вызове будет происходить переход куда надо.
Указатели на данные
Но мы-то храним в памяти программ не только исполняемый код. А значит трамплины здесь не помогут — мы разыменовываем указатели, а не переходим на них.
В библиотеке AVR даже есть функции/макросы типа pgm_read_byte_far(addr), чтобы разыменовать полный указатель (им передаются четырёхбайтовые значения). Но gcc не умеет добывать эти указатели средствами языка Си.
К счастью, есть макрос pgm_get_far_address(var) для получения полного адреса переменной. Это делается с помощью встроенного ассемблера (тот случай, когда ассемблер умнее компилятора).
Осталось переписать весь код, который использует данные в ПЗУ. То есть музыкальный проигрыватель, отрисовку спрайтов, вывод текста,… Не очень приятное занятие. Да ещё и код станет более тормозным, а для вывода графики это очень критично. Поэтому,
Распределяем данные по ПЗУ
Линкер очень старается разместить данные для программной памяти в нижних 64к. Это не срабатывает, если данных слишком много. Но ведь самые большие данные у нас — это музыкальные файлы. А значит если убрать только их, то всё остальное влезет в нижнюю память и основную часть кода переделывать не придётся.
Для этого будем эксплуатировать особенности линкерного скрипта. Одна из последних секций, которые линкер размещает в ПЗУ, называется .fini7. Сохраним все массивы с музыкой в этой секции:
#define MUSICMEM __attribute__((section(".fini7")))
const uint8_t tetris2[] MUSICMEM = { ... };
Теперь avr-nm говорит нам, что всё в порядке — данные со спрайтами и уровнями оказались в нижней части ПЗУ, а музыка в верхней.
00002f9c t _ZL10level_menu
00002e0f t _ZL10rope_lines
000006de t _ZL10ShipSprite
00023a09 t tetris2
00024714 T the_last_v8
Остаётся переделать проигрыватель на использование четырёхбайтовых указателей и вместо указателя на массив с кодом мелодии использовать функцию для получения её адреса. Функции нужны, потому что у нас есть приложение-проигрыватель, где можно слушать все мелодии по выбору. В нём теперь хранятся указатели на функции подобного вида:
00006992 <_Z12tetris2_addrv>:
6992: 61 ef ldi r22, 0xF1 ; 241
6994: 7a e3 ldi r23, 0x3A ; 58
6996: 82 e0 ldi r24, 0x02 ; 2
6998: 99 27 eor r25, r25
699a: 08 95 ret
Конец света откладывается до момента, когда спрайты забьют нижние 64к. Это маловероятно, потому что кода всё-таки больше, чем спрайтов, а значит скорее закончится память вообще.
Бонус
Этим летом мы написали игру в стиле Сокобана. Некоторые уровни получились довольно сложными. Попробуйте, к примеру, пройти вот этот:
Ссылки
- Страница проекта на github
- Arduino и светодиодный дисплей
- Arduino и
философскиймузыкальный камень - Немного прошлогодних игр
Автор: Dovgaluk