Коли в gcc 16-бітові адреси, а пам’яті раптово 256к
… або як вистрілити собі в ногу на Arduino
У літній комп’ютерної школі ми використовуємо для навчання розробці ігор власноруч зроблений старий комп’ютер.
Зараз в ньому встановлена плата Arduino Mega з процесором ATmega2560, в якому цілих 256 кілобайт флеш-пам’яті. Передбачалося, що цього вистачить дуже надовго, адже ігри виходять прості (екран-то всього лише 64×64 пікселів). В реальності ми зіткнулися з деякими проблемами вже по досягненні прошивкою розміру приблизно 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 і філософський музичний камінь
- Трохи минулорічних ігор