配合b站教程https://www.bilibili.com/video/BV1fZ4y1H7xN
学习
GBA工具安装
1:下载安装devkitPro。直接下载的免安装压缩包,解压之后需要修改msys2里的配置文件:具体路径:devkitPro/msys2/etc/profile
。在里面修改DEVKITPRO='devkitPro文件夹路径'
,DEVKITARM='devkitARM路径'
。
在PATH=''
中添加’:devkitPro路径/tools/bin:devkitARM路径/bin
完成之后就可以使用msys2了。
2:访问www.coranac.com
,project选项卡/tonc/tonc downloads/example code
。project选项卡/usenti/usenti-17.10.zip
。
下载完成后,解压在devkitPro文件夹内,运行msys2进入code文件夹,输入make build_all
编译code里所有的代码,产生.gba文件。
##帮助文件下载地址
http://www.cs.rit.edu/~tjh8300/CowBite/CowBiteSpec.zip
http://problemkaputt.de/gbatek.htm
GBA的内存
GBA没有系统,要直接对硬件进行操作
每“8位”组成“1字节”
0x00000000 开始 16K 为系统 ROM
0x02000000 开始 256K 为 CUP 外部工作内存
0x03000000 开始 32K CUP内部工作内存
0x04000000 I/O 寄存器,控制屏幕输入输出
0x05000000 存储色板,颜色信息
0x06000000 背景图层,存储地图等
0x07000000 存储精灵
0x08000000 存储 ROM
统一编址,直接对内存操作。
GBA 的 IO 显示控制
GBA 采用小端存储,因此内存是从高位到低位进行读取,每8位算一个读取单位。
0x 0000 0000 0000 0000 从右开始为第 0 位
选择 Mode 3 ,240 x 160 的像素屏幕,所以第 0 , 1 位,置1;0000 0000 0000 0011
Mode 3 只支持 BG2 ,所以第 10 位,置 1;0000 01000 0000 0011
确定了内存中的数值,接着赋值给 0x04000000 这个地址位。
(unsigned short*)0x04000000 = 0x0403;
这样就设置好了显示模式。
再将第 80 行, 100 列像素点显示红色。0000 0000 0000 0000,从右向左,每5位分别控制红,绿,蓝。
寻找地址,显示地址为 0x06000000 + (240 * 80 + 100) * 2
。
设置一个红点 为 0x001f
。0000 0000 0001 1111 。
*(unsigned short*)(0x06000000 + (240 * 80 + 100) * 2) = 0x001f;
完成代码:创建main.c ,复制以下代码
int main(){
//声明常量const
const unsigned MODE3 = 0x0403;//0000 0100 0000 0011
//指针变量,表示所在地址的指针,指针类变量和指针所指地址的内容一致。
unsigned short* SCREEN = (unsigned short*)0x04000000;
//设置视频模式和图层3
//*为取址符,将指针所指内容取出。
*SCREEN = MODE3;
//第80行,100列像素点显示红色
//0000 0000 0000 0000,从右向左,每5位分别控制红,绿,蓝
*(unsigned short*)(0x06000000 + (240 * 80 + 100) * 2) = 0x001f;
return 0;
}
使用 msys2 进入文件所在位置,执行以下代码。
arm-none-eabi-gcc -c main.c -o main.o
arm-none-eabi-gcc main.o -specs=gba.specs -omain.elf
arm-none-eabi-objcopy -O binary main.elf main.gba
gbafix main.gba -t first
或者将其写入.sh
文件中,在msys2中用sh build.sh
进行编译,为防止编译失败不能发现,可在开头加入rm *.o *.elf *.gba
将以前的文件删除掉。
位运算
int main(){
/*位运算
与 或 异或 取反 位移
a:0x46a5 0100 0110 1010 0101
b:0x82e7 1000 0010 1110 0111
与运算:a & b a 与 b 每一位相与,同为 1 则得 1 剩下都为 0
0100 0110 1010 0101
1000 0010 1110 0111
0000 0010 1010 0101
或运算:a | b a 与 b 每一位相或,只要有 1 则为 1 没有 1 则为 0
0100 0110 1010 0101
1000 0010 1110 0111
1100 0110 1110 0111
异或运算:a ^ b a 与 b 每一位相异或,不同为 1 相同则为 0
0100 0110 1010 0101
1000 0010 1110 0111
1100 0100 0100 0010
取反运算:~a a的每一位取反
移位运算:左移:<< 右移:>>
a << 5:a 左移 5 位,空出补 0
a >> 5:a 右移 5 位,空出补 0
*/
//可以单独设置每个模式的 16 位常量,需要用哪些模式将他们进行“或运算”即可
//定义常量,define 可不带“;”
//#define MODE3 0x0403
//可以把define内容放入“.h”文件中,然后在main.c文件中“#include ".h"”即可。
//声明常量const
const unsigned MODE3 = 0x0403;
//指针变量,表示所在地址的指针,指针类变量和指针所指地址的内容一致。
unsigned short* SCREEN = (unsigned short*)0x04000000;
//设置视频模式和图层3
//*为取址符,将指针所指内容取出。
*SCREEN = MODE3;//0000 0100 0000 0011
//第80行,100列像素点显示红色
//0000 0000 0000 0000,从右向左,每5位分别控制红,绿,蓝
//偏移了 (240 * 80 + 100) * 2 个像素点,一个像素点是2字节,所以要 x2
//内存地址以 1 字节为单位变化。
//所以偏移量,加上基础值 0x06000000 得到的才是内存地址。060096c8
for(int i = 0 ; i < 38400 ; i ++){
*((unsigned short*)0x06000000 + i) = 0x001f;
}
return 0;
}
将各种 Mode 分别 define ,再写在".h"文件中,用 include 包含(参照 IO 的图片)如下:
#define MODE0 0
#define MODE1 1
#define MODE2 2
#define MODE4 4
#define MODE5 5
#define BG0 0x0100
#define BG1 0x0200
#define BG2 0x0400
#define BG3 0x0800
//左移一位
#define DISPCNT *(unsigned short*)0x04000000
#define VRAM (unsigned short*)0x06000000
使用时相"或"即可MODE3 | BG2
GBA显示图片
需要使用工具usenti
可在http://www.coranac.com/projects/usenti/
下载
首先需要了解显示图片的原理:
1:一个数组中全是 RGB 的 16 位数字的数据,代表了一个图片的所有数据。
2:使用循环将每一个值赋值给内存的每一个位置,即可在显示屏上显示。
###第一步:usenti 处理图片
注意只能显示 240 x 160 的图片,需先将图片进行处理,可使用 ps - 图像 - 图像大小
进行修改
usenti 打开图片"kinoGBA",点击image - Export - 保存类型 GBA source -保存
(注意名字不要用汉字)出现如下:
按照图中设置即可,图片无法显示则按照以下:
1:左上角方框内,Gfx 下第一个选项改为 bitmap(GBA),第二个选项改为 16
2:右下角方框内,选择 u16,之后点击左下角 ok 即可,出来选项框选什么都无所谓。
之后保存的文件夹内会出现 kinoGBA.c 和 kinoGBA.h 接下来在main.c 文件中引入kinoGBA.h
#include kinoGBA.h
打开 kinoGBA.c 会发现,里面是一个数组 kinoGBABitmap[38400] ,这个就是需要遍历赋值的数组。数组里是 16 位的数值,所以计算字节数时,需要将大小x2。
遍历代码如下:
#include "mygba.h"
#include "kinoGBA.h"
//写入图片“kino.png”
//图片像素只能是 240 x 160
//需要使用 usenti 工具
int main(){
for (int i = 0;i < 38400 ;i ++ ){
*(VRAM + i) = kinoGBABitmap[i];
}
DISPCNT = MODE3 | BG2;
return 0;
}
接下来需要编译文件,kinoGBA.c 和 main.c 都需要编译,build.sh 文件如下:
#传入参数为文件夹名称,不用带“/”
#$1 代表传入的参数,./代表当前文件夹,如果不需要切换文件夹,就可以删掉"./$1/"或者参数输入"."
rm ./$1/*.o ./$1/*.elf ./$1/*.gba
#kinoGBA 也要编译
arm-none-eabi-gcc -O3 -c ./$1/kinoGBA.c -o ./$1/kinoGBA.o
#arm 代表使用 ARM 处理器进行编译,none 代表没有操作系统,e 代表嵌入式
#a 代表应用,b 代表二进制,i 代表接口。
arm-none-eabi-gcc -O3 -c ./$1/main.c -o ./$1/main.o
#这个地方也要加 kinoGBA.o 文件
arm-none-eabi-gcc ./$1/kinoGBA.o ./$1/main.o -specs=gba.specs -o ./$1/main.elf
arm-none-eabi-objcopy -O binary ./$1/main.elf ./$1/main.gba
gbafix ./$1/main.gba -t first
之后在 msys2 中使用sh build.sh .
即可编译文件。然后将 main.gba 拖入 VBA 模拟器中即可显示图片。
帧缓冲
GBA 显示图片是一帧一帧显示的,切换帧时需要时间。显示图片的原理是一层一层像素点从上到下,从左到右扫描。
240 x 160 的像素点扫描时,会有间隔,如下图
图片描述如下:
每行像素点扫描完 240 个之后,会有 68 个空白像素点。160 行像素点扫描完之后,会有 68 行空白像素点。
这些空白像素点扫描时间,被称为:垂直扫描空白。一般需要在此段时间内,进行换帧。
换帧的模式为"模式4"。
模式4:
介绍:
1:色板数据部分,为索引值,是图像色板,定义好的颜色,存储在 0x05000000 之后。共 256 色。
2:图形部分,显示图像的部分,存储在 0x06000000 之后,因为直接引用色板索引,所以一个像素只占8位,38400 -> 19200 可以存储2个帧
3:设置好 2 个帧之后,需要在 0x04000000 上第 4 位控制显示第几帧,可以在头文件".h"中定义
#define FRAME1 0x00 //显示第 1 张图
#define FRAME2 0x10; //0000 0000 0001 0000 显示第 2 张图
4:0x04000006 所在的数据可以查看屏幕扫描的当前位置。在".h"文件中定义:
#define VCOUNT *(unsigned short*)0x04000006
5:第一帧的内存地址为:0x06000000 - 0x06009FFF 40KB的内存
第二帧的内存地址为:0x0600A000 - 0x06013FFF 40KB的内存
在".h"文件中添加:
#define VRAM2 (unsigned short*)0x0600A000
当 VCOUNT < 160 时,说明还在扫描图片,反之,则在垂直扫描空白。
下面为帧缓冲实例,
1:准备一张 240 x 320 的图,使用 usenti 导出,导出时,需注意:
1)左上角方框的 bpp 需为 8;
2)右上角方框最上方的 Pal 需打钩;
这时,导出的文件中,发现".c"文件中是两个数组。色板数据和图像数据。
图像数据因为是 2 帧的图,所以还是 38400 的下标。色板数据为 256 下标。
此时,需将色板数据导入 0x05000000 ,将图像数据导入 0x06000000 。
因此,在"mygba.h"中,加入:
#define PAL *(unsigned short*)0x05000000
然后使用循环,将数组放入 PAL 中。
代码如下
// 这里是 19200 ,要一次放入两个帧。
// VRAM 是 0x06000000 - 0x06009FFF 40KB的内存
// VRAM2 是 0x0600A000 - 0x06013FFF 40KB的内存
// 需要在".h"文件中定义
for(int i = 0;i < 19200; i ++){
if(i < 256){
*(PAL + i) = gbaPicPal[i];
}
*(VRAM + i) = gbaPicBitmap[i];
*(VRAM2 + i) = gbaPicBitmap[i + 19200];
}
色板数据和图片数据载入完成后,现在图片数据区有两帧图。使用 FRAME1 和 FRAME2 进行切换,代码如下:
// 优化编译 -O3 会优化掉死循环内的代码,所以要把"build.h"中的编译选项"-O3"删掉,或改为"-O0"。
while(1){
//设置模式 4 背景 2 第 1 个帧
DISPCNT = MODE4 | BG2 | FRAME1;
//等待一次垂直扫描空白,当 VCOUNT >= 160 时,在空白,所以要等下一次空白。
//不然会跳帧。
while(VCOUNT >= 160);
while(VCOUNT < 160);
//设置第 2 个帧
DISPCNT = MODE4 | BG2 | FRAME2;
while(VCOUNT >= 160);
while(VCOUNT < 160);
}
"build.h"文件进行相应修改即可。
模式5
介绍:
颜色与模式 3 一样丰富,但因为颜色数据太多,所以图片数据内存变小,只能显示 160 x 128 的图片。要全屏显示需要拉伸图片。因为使用了 32768 色,所以图片数据是 16 位
实例:
1:找一张图片处理成 160 x 128 的规格,然后 usenti 导出,此时==左上方框内 bpp 为 16 ,右下方框要选 u16 。
2:打开".c"文件发现,其中数组角标为 40960 。
3:两帧图片存储位置与模式 4 相同:0x06000000 - 0x06009FFF 和 0x0600A000 - 0x0601 3FFF。
4:将循环条件改为 40960 的一半 20480 ,循环里面数组角标加的数字也是 20480。
5:把模式全改成 MODE5 ,还有".sh"文件内的文件名,以及各种需要修改的名称 修改完成,编译即可。
代码如下:
".c"文件
#include "mygba.h"
#include "GBAPic.h"
//写入图片“GBAPic.png”
//图片像素只能是 160 x 128
//需要使用 usenti 工具
int main(){
DISPCNT = MODE5 | BG2;
//*(unsigned short*)0x4000020=(255/1.5);
//*(unsigned short*)0x4000022=0;
//*(unsigned short*)0x4000024=0;
//*(unsigned short*)0x4000026=(255/1.25);
//循环写入图片数据
for (int i = 0;i < 20480 ;i ++ ){
*(VRAM + i) = GBAPicBitmap[i];
*(VRAM2 + i) = GBAPicBitmap[i + 20480];
}
while(1){
//设置模式 4 背景 2 第 1 个帧
DISPCNT = MODE5 | BG2 | FRAME1;
//等待一次垂直扫描空白,当 VCOUNT >= 160 时,在空白,所以要等下一次空白。
//不然会跳帧。
while(VCOUNT >= 160);
while(VCOUNT < 160);
//设置第 2 个帧
DISPCNT = MODE5 | BG2 | FRAME2;
while(VCOUNT >= 160);
while(VCOUNT < 160);
}
return 0;
}
"mygba.h"文件
#define MODE0 0
#define MODE1 1
#define MODE2 2
#define MODE3 3
#define MODE4 4
#define MODE5 5
#define BG0 0x0100
#define BG1 0x0200
#define BG2 0x0400
#define BG3 0x0800
//显示第 1 张图
#define FRAME1 0x00
//0000 0000 0001 0000 显示第 2 张图
#define FRAME2 0x10
//模式设置地址
#define DISPCNT *(unsigned short*)0x04000000
//查看屏幕扫描到哪一行
#define VCOUNT *(unsigned short*)0x04000006
//画板数据地址
#define PAL (unsigned short*)0x05000000
//第一帧地址
#define VRAM (unsigned short*)0x06000000
//第二帧图的地址
#define VRAM2 (unsigned short*)0x0600A000
//相或
"build.sh"文件 注意:一定要改"main.c"的"-O3"不然会将死循环优化
#传入参数为文件夹名称,不用带“/”
#$1 代表传入的参数,./代表当前文件夹,如果不需要切换文件夹,就可以删掉"./$1/"或者参数输入"."
rm ./$1/*.o ./$1/*.elf ./$1/*.gba
#GBAPic 也要编译
arm-none-eabi-gcc -O3 -c ./$1/GBAPic.c -o ./$1/GBAPic.o
#arm 代表使用 ARM 处理器进行编译,none 代表没有操作系统,e 代表嵌入式
#a 代表应用,b 代表二进制,i 代表接口。
#此处要改 -O0
arm-none-eabi-gcc -O0 -c ./$1/main.c -o ./$1/main.o
#链接 GBAPic.o
arm-none-eabi-gcc ./$1/GBAPic.o ./$1/main.o -specs=gba.specs -o ./$1/main.elf
arm-none-eabi-objcopy -O binary ./$1/main.elf ./$1/main.gba
#修复 gba 文件头,使其可以在掌机上运行。
gbafix ./