...或者如何在Arduino上射击自己

在夏季的计算机学校中,我们使用自己
制造的老式计算机来教授游戏开发。
现在,它具有一个带有ATmega2560处理器的Arduino Mega板,其中可容纳多达256 KB的闪存。 假定这将持续很长时间,因为游戏很简单(屏幕只有64x64像素)。 实际上,当固件达到约128 KB时,我们遇到了一些问题。
在程序存储器中,尽管有其名称,除了游戏的可执行代码外,还存储了各种不变的数据,例如精灵和关卡表。 这个数据不是很多。
但是,当我们
将YM2149F声音芯片连接到我们的计算机 ,然后将几十首乐曲下载到同一程序存储器中时,问题就开始了。
尝试播放旋律或在游戏菜单中造成一些垃圾时,前缀崩溃。 目前尚不清楚如何进行调试,因为处理器不仅处理游戏逻辑,而且还显示图像和声音。 结果,事实证明gcc-avr编译器使用大小为两个字节的变量来存储指针。 但是仅用两个字节来寻址256 KB是不可能的! 他如何下车?
代码指针
首先,函数调用指令和跳转可以使用三字节地址。 因此,链接程序在这样的指令中替换完整地址就足够了,它将起作用。 如果函数的地址是通过指针传递的,那么这样的数字将不会传递-因为我们有一个双字节指针。
在这种情况下,gcc将一个“跳板”插入到较低的64kb中-jmp指令,该指令切换到所需的功能。 然后,该跳板的地址将用作需要存储在变量中的函数的地址-毕竟,它被放置在两个字节中。 当被调用时,将在必要时进行过渡。
数据指针
但是我们不仅将可执行代码存储在程序存储器中。 因此,这里的跳转将无济于事-我们正在取消引用指针,但不准备使用它们。
AVR库甚至具有函数/宏,例如pgm_read_byte_far(addr),以取消对完整指针的引用(它们传递了四个字节的值)。 但是gcc不知道如何使用C语言获得这些指针。
幸运的是,有一个宏pgm_get_far_address(var)可获取变量的完整地址。 这是使用内置的汇编程序完成的(汇编程序比编译器更聪明的情况)。
仍然需要重写所有使用ROM中数据的代码。 即,音乐播放器,渲染精灵,文本输出……不是很愉快的体验。 是的,代码将变得更具约束力,对于图形来说,这非常关键。 因此,
我们在ROM上分发数据
链接器正在非常努力地将程序存储器的数据放在较低的64k中。 如果有太多数据,则无法使用。 但是我们拥有的最大数据是音乐文件。 因此,如果仅删除它们,则其他所有内容都将放入较低的内存中,而无需重做代码的主要部分。
为此,我们将利用链接描述文件的功能。 链接器放在ROM中的最后一部分之一称为.fini7。 保存本节中的所有音乐数组:
#define MUSICMEM __attribute__((section(".fini7"))) const uint8_t tetris2[] MUSICMEM = { ... };
现在,avr-nm告诉我们一切都井井有条-带有精灵和水平的数据竟然位于ROM的底部,而音乐则位于ROM的顶部。
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
世界末日推迟到精灵到达底部64k为止。 这是不太可能的,因为仍然有比sprites更多的代码,这意味着内存通常会终止。
红利
今年夏天,我们以推箱子的风格编写了一款游戏。 事实证明,有些水平很难。 例如,尝试通过以下方法:

参考文献
- Github项目页面
- Arduino和LED显示屏
- Arduino和
哲学家的音乐之石 - 去年的几场比赛