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

    … или как выстрелить себе в ногу на 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. Немного прошлогодних игр
    Поделиться публикацией
    Комментарии 14
      0
      Ваша статья мне напомнила про вот это — habr.com/post/163627 В нем тоже были развлечения с обращием к памяти =)
        0
        Спасибо. Прекрасная история об отладке.
        0
        Рекомендую почитать, как такие ограничения обходились на 6502 и, в частности, на NES.
          0
          В NES в каждом картридже кастомная аппаратура, а тут железо фиксированное. Но способ похожий — у AVR есть регистр, который используется для выбора нужного куска памяти программ.
          0
          В компьютерах с процессором Z80 при добавлении памяти организовывали их в страницы в верхних 16кб, и переключали их записывая в определенный порт число страницы.
            +1
            Так делалось не только для Z80, но и для x86, но это все процессоры с внешней памятью и разработчику системы не представляло сложности воткнуть схему коммутации страниц между процессором и памятью. В статье же речь идет об однокристаллке со встроенной памятью, где вмешаться в адресную шину между ЦПУ и памятью не представляется возможным.
            +1
            Попробуйте, к примеру, пройти вот этот:

            Он не проходим. В классическом сокобане ящики нельзя тянуть на себя, а здесь два из трёх ящиков иначе от стены не оторвать.
              +1
              Он точно проходим. И один человек даже сделал это без написания программы для перебора :)
              Кстати, ящики жёлтые.
                0
                Не вижу решения :) Ну если только ящики можно двигать поверх уже установленных на места.
                  0
                  А, вот в чём прикол! Я подумал, что наоборот — синие ящики, а жёлтые — места для них.
                  0

                  Проходится, весь секрет что через места для ящиков (синие клетки) можно проходить.
                  Первый ящик вниз потом в сторону и так далее.

                  +2
                  Попробуйте, к примеру, пройти вот этот:
                  А собранная версия где-то есть?
                    0
                    Если речь про поиграть, то достаточно легко собирается эмулятор на базе qt. Он живёт в game/qt_emulator.
                    +1
                    Классная у вас платформа для экспериментов.

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

                    Самое читаемое