Pull to refresh

Когда в gcc 16-битные адреса, а памяти внезапно 256к

Reading time 3 min
Views 6K

… или как выстрелить себе в ногу на 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к. Это маловероятно, потому что кода всё-таки больше, чем спрайтов, а значит скорее закончится память вообще.

Бонус


Этим летом мы написали игру в стиле Сокобана. Некоторые уровни получились довольно сложными. Попробуйте, к примеру, пройти вот этот:



Ссылки


  1. Страница проекта на github
  2. Arduino и светодиодный дисплей
  3. Arduino и философский музыкальный камень
  4. Немного прошлогодних игр
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+18
Comments 14
Comments Comments 14

Articles