一个简单的编程示例
ARM7代码:
我们首先需要一个ARM7代码。ARM7是唯一一个可以控制触摸屏的CPU。 'libnds' 库的例子用一些“样板文件”('boilerplate')代码来实现一个“垂直空白中断”('Vertical Blank Interrupt')。这个中断可以为画屏幕产生一个规则基础。(保证各硬件功能顺序进行)
在一个中断处理中,“样板文件”( 'boilerplate' )代码获取触摸屏的参数值并储存以便于ARM9代码使用。记住只有ARM7可以访问触摸屏,所以需要读取(硬件设计如此)。但将它储存到一个内部ARM9可访问数据结构中就可以使得CPU使用这个数据了。
这是中断代码:
void InterruptHandler(void) {
static int heartbeat = 0;
if (REG_IF & IRQ_VBLANK) {
uint16 but=0, x=0, y=0, xpx=0, ypx=0, z1=0, z2=0, batt=0, aux=0;
int t1=0, t2=0;
uint32 temp=0;
uint8 ct[sizeof(IPC->curtime)];
// Update the heartbeat
heartbeat++;
// Read the X/Y buttons and the /PENIRQ line
but = REG_KEYXY;;
if (!(but & 0x40)) {
// Read the touch screen
x = touchRead(TSC_MEASURE_X);
y = touchRead(TSC_MEASURE_Y);
xpx = ( ((SCREEN_WIDTH -60) * x) / TOUCH_WIDTH ) - TOUCH_OFFSET_X;
ypx = ( ((SCREEN_HEIGHT-60) * y) / TOUCH_HEIGHT ) - TOUCH_OFFSET_Y;
z1 = touchRead(TSC_MEASURE_Z1);
z2 = touchRead(TSC_MEASURE_Z2);
}
batt = touchRead(TSC_MEASURE_BATTERY);
aux = touchRead(TSC_MEASURE_AUX);
// Read the time
rtcGetTime((uint8 *)ct);
BCDToInteger((uint8 *)&(ct[1]), 7);
// Read the temperature
temp = touchReadTemperature(&t1, &t2);
// Update the IPC struct
IPC->heartbeat = heartbeat;
IPC->buttons = but;
IPC->touchX = x;
IPC->touchY = y;
IPC->touchXpx = xpx;
IPC->touchYpx = ypx;
IPC->touchZ1 = z1;
IPC->touchZ2 = z2;
IPC->battery = batt;
IPC->aux = aux;
for(u32 i=0; i<sizeof(ct); i++) {
IPC->curtime[i] = ct[i];
}
IPC->temperature = temp;
IPC->tdiode1 = t1;
IPC->tdiode2 = t2;
}
// Acknowledge interrupts
REG_IF = REG_IF;
}
ARM7代码中有一个类似于C编程的“MAIN”函数。它设置一个中断处理所以上面的代码会在“垂直空白中断”(Vertical Blank Interrupt )时被执行并进入一个无限循环等待它。
int main(int argc, char ** argv) {
// Reset the clock if needed
rtcReset();
//enable sound
SOUND_CR = SCHANNEL_ENABLE | SOUND_VOL(0x7F);
IPC->soundData = 0;
// Set up the interrupt handler
REG_IME = 0;
IRQ_HANDLER = &InterruptHandler;
REG_IE = IRQ_VBLANK;
REG_IF = ~0;
DISP_SR = DISP_VBLANK_IRQ;
REG_IME = 1;
// Keep the ARM7 out of main RAM
while (1) swiWaitForVBlank();
return 0;
}
我把这段代码写入一个 'arm7_main.cpp' 文件。ARM7 G++编译器会将其编译成一个包含ARM7代码的 'arm7.o' 文件。G++需要很多的选项来编译ARM7代码。
arm-elf-g++ -g -Wall -O2 -mcpu=arm7tdmi -mtune=arm7tdmi \
-fomit-frame-pointer -ffast-math -mthumb-interwork \
-Id:\devkitpro\ndslib\include -DARM7 -c arm7_main.cpp -oarm7_main.o
编译完之后我们便有了 'arm7_main.o' 文件,它需要用LIBNDS提供的DS ARM7标准库文件连接。这和用C标准库连接一个普通C程序一样。这步用 'objcopy'可产生一个DS ARM7可执行的二进制的'elf'格式文件:
arm-elf-g++ -g -mthumb-interwork -mno-fpu \
-specs=ds_arm7.specs arm7_main.o -Ld:\devkitpro\ndslib\lib -lnds7 -oarm7.elf
arm-elf-objcopy -O binary arm7.elf arm7.bin
ARM9代码:
在这个例子中ARM9代码是担任主要工作的。它会显示一个简单的文本消息和输出从触摸屏获得的X,Y坐标。记住是ARM7代码获取这个信息并储存在一个我们在这可以访问的数据结构中。
ARM9代码是一个单一的 'main' 函数。首先设置显示模式以让我们使用NDS的一个显示屏。在这种情况下是主屏。我们还需要设置显示文本、字体颜色等等:
powerON(POWER_ALL);
// Use the main screen for output
videoSetMode(MODE_0_2D | DISPLAY_BG0_ACTIVE);
vramSetBankA(VRAM_A_MAIN_BG);
BG0_CR = BG_MAP_BASE(31);
// Set the colour of the font to White.
BG_PALETTE[255] = RGB15(31,31,31);
consoleInitDefault((u16*)SCREEN_BASE_BLOCK(31), (u16*)CHAR_BASE_BLOCK(0), 16);
文本在这里输出并进入一个显示触摸屏数据的循环。标准C函数 'printf' 用来输出文本到NDS的显示屏,当我显示触摸屏的X、Y坐标时我用ASCII码转换以避免光标的连续位置:
printf("\n\n\tHello World!\n");
while(1) {
touchPosition touchXY = touchReadXY();
printf("\x1b[10;0H");
printf("Touch x = %d \n", touchXY.px);
printf("Touch y = %d \n", touchXY.py);
}
这段代码在'arm9_main.cpp' 中会被像ARM7一样被连接,只须改变一下连接程序的ARM9选项:
arm-elf-g++ -g -Wall -O2 -mcpu=arm9tdmi -mtune=arm9tdmi \
-fomit-frame-pointer -ffast-math -mthumb-interwork \
-Id:\devkitpro\ndslib\include -DARM9 -c arm9_main.cpp -oarm9_main.o
arm-elf-g++ -g -mthumb-interwork -mno-fpu \
-specs=ds_arm9.specs arm9_main.o -Ld:\devkitpro\ndslib\lib -lnds9 -o arm9.elf
arm-elf-objcopy -O binary arm9.elf arm9.bin
创建DS可执行文件:
现在我们有了ARM7和ARM9的.bin 格式代码,我们现在需要创建一个包含这些文件,以及位图、图标、资源等的可执行“小文件系统”('mini-filesystem' )。使用的工具是'ndstool':
ndstool -c demo1.nds -9 arm9.bin -7 arm7.bin
以上的'ndstool'命令会创建一个'demo1.nds'文件,可以真正在硬件上运行。它包含一个ARM9 CPU 运行的代码和一个ARM7 CPU 运行的代码。
这一部分包括如何利用NDS的“帧缓冲”(framebuffer)模式在NDS的一个显示屏上绘图。NDS的每一个显示屏都可以被设置成很多种模式。每一种模式有其优缺点,但我们这里只使用“帧缓冲”模式来实现一个最简单的绘图功能。
“帧缓冲”(framebuffer):
“帧缓冲”是一种显示屏映射到一部分内存的模式。向内存中写数据会导致数据显示在显示屏上。在这个模式中,显示屏上的一个象素由2字节数据表示。这相当于C语言中的16位无符号整型数据类型(16 bit unsigned integer)。我们写入这部分内存的数据是以555格式显现的象素颜色。
所以我们不需要人为转换555格式,这有一个方便的宏'RGB15'让我们确定每一个象素的红、绿、蓝色的数量。每一个红、绿、蓝元素是一个从0-31的值。0表示无颜色,31表示最大色。(通过不同组合来显现不同颜色)。这是一个例子:
RGB15 Color
RGB15(31,0,0) Red
RGB15(0,31,0) Green
RGB15(0,0,31) Blue
RGB15(0,0,0) Black
RGB15(31,31,31) White
下面的代码片是示范如何通过给定一个指向“帧缓冲”区内存开始部分的指针来用蓝色填充屏幕的:
uint16* framebuffer = ...;
for(int i = 0; i < SCREEN_WIDTH * SCREEN_HEIGHT; ++i)
*framebuffer++ = RGB15(0,0,31);
向“帧缓冲”写的数据会立刻被画在相应象素上。“帧缓冲”模式的好处是你可以在屏幕任意地方画任何你想画的东西,它直接使用NDS的2D硬件加速。
“帧缓冲”的缺点是你必须自己做所有事。没有“精灵”('sprites')、贴图、卷动等等,除非你自己写代码实现。NDS的其他模式也许会适合做这些事,它们会在以后的教程中介绍。同时,“帧缓冲”模式有很多灵活性,并让我们更加接近硬件。
屏幕:
从硬件角度来看NDS上有两个屏幕。一个在上一个在下。下屏是唯一的触摸屏。
从编程角度来看这也有两个屏。一个主屏和一个副屏。每一个程序设计中的屏是和硬件对应的。在这个例子中我们只使用一个屏,主屏,它所对应的是硬件的上屏幕。
我们用一个叫'videoSetMode'的函数来设置屏的模式。有很多“帧缓冲”内存可以被映射到一个屏幕。这就允许我们实现双缓冲、特殊页之类的功能。我们现在只使用一个“帧缓冲”,所以我们使用“MODE_FB0”;
videoSetMode(MODE_FB0);
“帧缓冲”的内存区域是一部分带有一个字母“A”的名叫'VRAM'的内存。对每一部分,我们需要告诉显示系统那一部分'VRAM' 内存我们用来做 “帧缓冲”。我们可以像这样使用第一个,VRAM_A:
vramSetBankA(VRAM_A_LCD);
画一个图形:
我们要在显示屏上画的是一个单色矩形。我们写了一个函数来实现它,函数参数包括矩形在屏幕上的位置的X、Y坐标、颜色、以及一个指向“帧缓冲”区的指针:
void draw_shape(int x, int y, uint16* buffer, uint16 color)
{
buffer += y * SCREEN_WIDTH + x;
for(int i = 0; i < shape_height; ++i) {
uint16* line = buffer + (SCREEN_WIDTH * i);
for(int j = 0; j < shape_width; ++j) {
*line++ = color;
}
}
}
“帧缓冲”在内存中以行排列,所以如果屏幕有200象素宽,那麽“帧缓冲”开始的200个uint16(一个象素由一个uint16数据表示)数据单元就代表显示屏的第一行。第二个200个uint16数据单元就表示第二行,等等。
“draw_shape”函数首先通过对X、Y坐标的计算来确定第一个象素在“帧缓冲”中的位置。注意SCREEN_WIDTH 和 SCREEN_HEIGHT 是“LIBNDS”提供的返回屏幕宽、高的宏。
函数接下来通过向“帧缓冲”中的正确位置写入颜色数据来绘制图形的每一行。
shape_height 和 shape_width 是静态变量,做测试时可改变其值:
static int shape_width = 10;
static int shape_height = 10;
移动图形:
让图形能在屏幕上移动我们需要擦除现在位置的图形并在新位置重画。这可以通过保存运动前后X、Y坐标的轨迹坐标来实现:
static int old_x = 0;
static int old_y = 0;
static int shape_x = 0;
static int shape_y = 0;
接下来绘图变的简单了,我们只需通过调用'draw_shape' 函数和old_x、old_y坐标将其变为背景色来擦除它,再调用一次用新 'x'、'y'坐标来绘制新位置的图形:
draw_shape(old_x, old_y, VRAM_A, RGB15(0, 0, 0));
draw_shape(shape_x, shape_y, VRAM_A, RGB15(31, 0, 0));
注意在'draw_shape'函数中的“帧缓冲”参数位置是'VRAM_A',这是我们之前映射到主屏的“帧缓冲”。
并不太正确:
一个简单的画图和计算位置的 'main' 函数可以是这样:
int main(void)
{
powerON(POWER_ALL);
videoSetMode(MODE_FB0);
vramSetBankA(VRAM_A_LCD);
while(1) {
old_x = shape_x;
old_y = shape_y;
shape_x++;
if(shape_x + shape_width >= SCREEN_WIDTH) {
shape_x = 0;
shape_y += shape_height;
if(shape_y + shape_height >= SCREEN_HEIGHT) {
shape_y = 0;
}
}
draw_shape(old_x, old_y, VRAM_A, RGB15(0, 0, 0));
draw_shape(shape_x, shape_y, VRAM_A, RGB15(31, 0, 0));
}
如果你运行的话你会看见一个“倾斜”的图形在屏幕上运动。(不是矩形)。
“垂直间隔中断”(Vertical Blank Interrupt):
出现一个倾斜矩形的原因是屏幕显示的工作方法。硬件设备刷新屏幕的频率是1/60秒。它通过访问每一个象素来实现,一行接一行,复制“帧缓冲”的内容到显示屏。
同时,在 main函数中,我们在改变帧缓冲中的内容。所以如果硬件在我们擦除之前绘制图形,它不会被马上擦除。如果我们在硬件绘制之前改变数据,它会绘制一部分新数据和一部分旧数据。
幸好硬件有一种方法告诉我们它绘制完成。这就叫“垂直间隔中断”('Vertical Blank Interrupt')我们可以注册一个函数让它在这种情况发生时被调用。
中断是一个硬件机制,它中断我们当前正在做的事并调用一些函数做一些其他的事。当中断函数返回时,之前的活动就像没有中断一样继续。
为了防止我们之前看到的画图问题,我们希望在向帧缓冲写入数据时硬件不要刷新屏幕。做这的最好时候就是在垂直间隔中断时。
设置中断:
首先我们要告诉NDS在中断时我们要调用什麽函数:
void InitInterruptHandler()
{
REG_IME = 0;
IRQ_HANDLER = on_irq;
REG_IE = IRQ_VBLANK;
REG_IF = ~0;
DISP_SR = DISP_VBLANK_IRQ;
REG_IME = 1;
}
在这个代码片中我们只是让垂直间隔中断发生并在发生的同时调用'on_irq' 函数。
'on_irq' 函数会像我们之前在 'main'函数中做的一样向帧缓冲中写入数据:
void on_irq()
{
if(REG_IF & IRQ_VBLANK) {
draw_shape(old_x, old_y, VRAM_A, RGB15(0, 0, 0));
draw_shape(shape_x, shape_y, VRAM_A, RGB15(31, 0, 0));
// Tell the DS we handled the VBLANK interrupt
VBLANK_INTR_WAIT_FLAGS |= IRQ_VBLANK;
REG_IF |= IRQ_VBLANK;
}
else {
// Ignore all other interrupts
REG_IF = REG_IF;
}
}
这个函数主要是向缓冲中写入数据,其余部分是做一些中断操作。
我们也需要告诉NDS我们处理了垂直同步中断。这是我们以后要调用的 'swiWaitForVBlank' 需要的,我会在那时解释。代码是这样的:
// Tell the DS we handled the VBLANK interrupt
VBLANK_INTR_WAIT_FLAGS |= IRQ_VBLANK;
REG_IF |= IRQ_VBLANK;
依然不太对:
我们可以将写入代码从'main' 函数中移出:
int main(void)
{
powerON(POWER_ALL);
videoSetMode(MODE_FB0);
vramSetBankA(VRAM_A_LCD);
InitInterruptHandler();
while(1) {
old_x = shape_x;
old_y = shape_y;
shape_x++;
if(shape_x + shape_width >= SCREEN_WIDTH) {
shape_x = 0;
shape_y += shape_height;
if(shape_y + shape_height >= SCREEN_HEIGHT) {
shape_y = 0;
}
}
}
不幸的是运行结果依然有问题。矩形在屏幕上多次出现并最终形成一个棋盘样子。
幸亏问题很清楚,当垂直间隔中断发生时硬件每1/60秒调用一次on_irq 函数,这时图形正在被擦除和重画。
不幸的是 'main' 函数中的'while'循环和帧率并不同步。它快速的运行使得图形的坐标不断增加,这可能在'on_irq' 程序调用时运行了50次。结果绘图程序擦除了已经被更新了50次的图形坐标。所以错误的区域被擦除了。
纠正:
我们可以通过让'while'循环等待直到中断发生。这可以通过让循环慢下来的侧面效果来实现。ARM9处理器可以有效的减慢直到中断发生。它也可以使我们的帧率维持在60帧每秒。实现这个功能的函数是'swiWaitForVBlank'。
你可能记得之前在'on_irq' 函数中我们设置了一些寄存器,说是用来处理垂直间隔中断的。这是'swiWaitForVBlank'工作所必须的。如果不设置的话'swiWaitForVBlank'会挂起硬件并等待一个永不会发生的中断消息。
加入这些行使得我们的程序正确运行:
int main(void)
{
powerON(POWER_ALL);
videoSetMode(MODE_FB0);
vramSetBankA(VRAM_A_LCD);
InitInterruptHandler();
while(1) {
old_x = shape_x;
old_y = shape_y;
shape_x++;
if(shape_x + shape_width >= SCREEN_WIDTH) {
shape_x = 0;
shape_y += shape_height;
if(shape_y + shape_height >= SCREEN_HEIGHT) {
shape_y = 0;
}
}
swiWaitForVBlank();
}
return 0;
}
交换屏幕:
我之前提到主屏是可以映射到硬件的上屏或下屏的。当前程序的主屏是映射到上屏的。我们可以通过使用'lcdSwap'函数来改变它。它可以在任何时候调用来交换屏幕。下面的函数会使得程序运行在触摸屏上:
int main(void)
{
powerON(POWER_ALL);
videoSetMode(MODE_FB0);
vramSetBankA(VRAM_A_LCD);
InitInterruptHandler();
lcdSwap();
while(1) {
old_x = shape_x;
old_y = shape_y;
shape_x++;
if(shape_x + shape_width >= SCREEN_WIDTH) {
shape_x = 0;
shape_y += shape_height;
if(shape_y + shape_height >= SCREEN_HEIGHT) {
shape_y = 0;
}
}
swiWaitForVBlank();
}
return 0;
}
这一节,我们介绍如何处理用户的按键操作。
REG_KEYINPUT 寄存器:
NDS有一个硬件寄存器,当按键被按下时,寄存器的值会改变。寄存器REG_KEYINPUT 位于内存地址的0x4000130处。它是只读寄存器。当按键时,值会改变:
KEYS Bit Key 'ndslib' define Down if Up if
0 A KEY_A Cleared Set
1 B KEY_B Cleared Set
2 Select KEY_SELECT Cleared Set
3 Start KEY_START Cleared Set
4 Directional Right KEY_RIGHT Cleared Set
5 Directional Left KEY_LEFT Cleared Set
6 Directional Up KEY_UP Cleared Set
7 Directional Down KEY_DOWN Cleared Set
8 Right Alternate Button KEY_R Cleared Set
9 Left Alternate Button KEY_L Cleared Set
REG_KEYXY 寄存器:
你会注意到这里漏掉了两个键, 'X' 和 'Y'。
这两个键是从不同的寄存器读取的,REG_KEYXY寄存器,在内存地址0x04000136处。不幸的是这个寄存器只能被ARM7读取。
为了让它能被ARM9读取,在LIBNDS的ARM7样板代码中,ARM7在垂直间隔中断时读取寄存器值并储存在IPC数据结构中,IPC可被ARM9读取。这是相关代码片:
void InterruptHandler(void) {
[...]
but = REG_KEYXY;
[...]
IPC->heartbeat = heartbeat;
IPC->buttons = but;
IPC->touchX = x;
[...]
}
REG_KEYXY 寄存器不仅包含X、Y键状态位,它还包括触控笔和NDS屏幕和上或打开的状态位:
XKEYS Bit Key 'ndslib' define Down if... Up if ...
0 X (1 << 0) Cleared Set
1 Y (1 << 1) Cleared Set
2 Pen Down (1 << 6) Cleared Set
3 Hinge (1 << 7) Set Cleared
读键:
使读取 REG_KEYINPUT 寄存器变的简单一点,我取它值的补码。这允许使用'&' 操作符来判断键是否被按下。我们可以这样写代码:
uint16 keysPressed = ~(REG_KEYINPUT);
if(keys_pressed & KEY_UP)
--shape_y;
来取代这个不直观的
if(!(REG_KEYINPUT & KEY_UP))
--shape_y;
同样的事ARM7可以这样做:
uint16 specialKeysPressed = ~IPC->buttons;
// Y Key
if(specialKeysPressed & (1 << 1))
shape_color = RGB15(7, 7, 7);
// X Key
if(specialKeysPressed & (1 << 0))
shape_color = RGB15(0, 15, 15);
// Pen Down
if(specialKeysPressed & (1 << 6))
shape_color = RGB15(0, 31, 31);
// Hinge closed
if(!(specialKeysPressed & (1 << 7)))
shape_color = RGB15(0, 0, 0);
通过键来控制图形移动:
我们对上一篇教程中的示例做一下轻微改动,以通过键来控制。
我们用方向键控制来代替图形的自动移动。其他的键控制图形的颜色。合上屏幕会使它消失,打开再按一个键又会让它重现。
我们使用一个公共变量来储存颜色值:
static uint16 shape_color = RGB15(31, 0, 0);
绘图代码也须改变:
void on_irq()
{
if(REG_IF & IRQ_VBLANK) {
draw_shape(old_x, old_y, VRAM_A, RGB15(0, 0, 0));
draw_shape(shape_x, shape_y, VRAM_A, shape_color);
// Tell the DS we handled the VBLANK interrupt
VBLANK_INTR_WAIT_FLAGS |= IRQ_VBLANK;
REG_IF |= IRQ_VBLANK;
}
else {
// Ignore all other interrupts
REG_IF = REG_IF;
}
}
图形通过方向键移动:
uint16 keysPressed = ~(REG_KEYINPUT);
// Based on the key pressed, move the shape.
if(keysPressed & KEY_UP)
--shape_y;
if(keysPressed & KEY_DOWN)
++shape_y;
if(keysPressed & KEY_LEFT)
--shape_x;
if(keysPressed & KEY_RIGHT)
++shape_x;
颜色通过测试IPC->buttons的值来改变,但首先取它的补码使其更加直观:
uint16 specialKeysPressed = ~IPC->buttons;
// Change the color of the shape if the relevant key was pressed.
if(keysPressed & KEY_A)
shape_color = RGB15(31, 0, 0);
if(keysPressed & KEY_B)
shape_color = RGB15(0, 31, 0);
if(keysPressed & KEY_SELECT)
shape_color = RGB15(0, 0, 31);
if(keysPressed & KEY_START)
shape_color = RGB15(31, 31, 31);
if(keysPressed & KEY_R)
shape_color = RGB15(15, 0, 15);
if(keysPressed & KEY_L)
shape_color = RGB15(7, 15, 7);
// Y Key
if(specialKeysPressed & (1 << 1))
shape_color = RGB15(7, 7, 7);
// X Key
if(specialKeysPressed & (1 << 0))
shape_color = RGB15(0, 15, 15);
// Pen Down
if(specialKeysPressed & (1 << 6))
shape_color = RGB15(0, 31, 31);
// Hinge closed
if(!(specialKeysPressed & (1 << 7)))
shape_color = RGB15(0, 0, 0);