在GBA上跑的linux

来源: 作者:wheel
  本修改都以Linux kernel 2.4.24+uclinux针对其的补丁为基础做的。
在把补丁打好后,将我提供的linux-2.4.24中的文件覆盖进目录。然后将norfs目录中的linux-2.4.24目录中覆盖Kernel目录中相应文件就可以。然后make menuconfig,在System Type中设置ARM system type为GBA,打开General setup中的Support uClinux FLAT format binaries,打开File systems中的NOR file system support。然后make dep,make clean,make,将生成的elf文件linux拷贝到gbarom目录中。
然后进入norfs目录中的gennorfs目录,输入make,make install,将生成norfs镜象的gennorfs文件装进系统中。
最后进入make,则将生成linux.gba,这就是给GBA模拟器使用的2进制文件。

3.GBA的简单介绍以及其优点和缺点
GBA是一种掌上游戏机,用的ARM7TDMI,256K内存(有的介绍将其32K内存也算在其中的,这是完全不对的,因为那些内存是有专门作用的),显存、声道、多用串口等等一堆游戏机需要的东西,rom是nor flash,详细的介绍网上非常多,我也就不详细介绍了。
GBA的优点:开发资源丰富。因为对GBA游戏开发有兴趣的人不少,所以对其介绍的资料相当多,还有不少专门的论坛对其的开发进行讨论。同时GBA有多种模拟器可以使用,而且功能都很不错。
GBA的缺点:有点BT的环境。256K内存是无法将整个KERNEL放入其中的。最主要的问题是GBA将头16K写入了他自己的代码,也就是说GBA自己处理了svc到fiq,其中irq的处理会在GBA自己处理过后跳到指定的代码(这将在后面做详细介绍),其他的以我现有的认识都是无法取到的,GBA都用自己的代码处理掉了。

4.介绍一下我写的bootloader
GBA的执行是从0x8000000开始的,这也是flash的开始地址,但是实际上从0x8000004一直到0x80000c0以前的部分是有一套格式的,有什么图标字符、校验位等等一大堆东西,不过这些东西我都没能够在我的GBA镜象生成程序creatrom中完成,只是简单的按照规矩在bootloader中直接从0x8000000跳到了0x80000c0开始执行具体的bootloader代码。在bootloader中我主要做的工作是将存储在flash中的init节和data节放到内存中。下面我就来介绍一下bootloader代码bl.s:
首先将mess的地址设置到r4,这个mess中存储了init和data在flash中的位置、长度、应在内存中的位置以及text的开始地址,这个部分是在creatrom程序中填写的。
然后就是先init然后data的拷贝过程,其中具体的拷贝用的是gba_dma3_copy这个函数,这里用的是GBA的DMA3,GBA共有4个DMA,在实际的kernel中我还没能将这几个东西用上。
在2次拷贝结束的时候,有这么2句:
mov r0,#0
mov r1,#210
这是设置启动参数,在arch/armnommu/kernel/head-armv.S中是这样写的
/*
* Kernel startup entry point.
*
* The rules are:
* r0 - should be 0
* r1 - unique architecture number
* MMU - off
* I-cache - on or off
* D-cache - off
*
* See linux/arch/arm/tools/mach-types for the complete list of numbers
* for r1.
*/
注意这个210是我后加的,具体我将在下面介绍对kernel的修改的时候进行介绍。
设置好启动参数以后就是将mess中的text开始位置读出,跳到那个位置开始执行。

5.我对kernel的修改的介绍
我对kernel的修改除了我自己增加的目录arch/armnommu/mach-gba/和include/asm-armnommu/arch-gba/以外,我都用类似
//teawater add for gba 2004.2.28------------------------------------------------
#ifdef CONFIG_ARCH_GBA
#else
#endif
//AJ2D--------------------------------------------------------------------------
标记了出来。
5.1.编译相关的修改
arch/armnommu/Makefile,这里增加了一个GBA相关的条目,主要是设置连接脚本,其他也跟别的项目没什么不同,TEXTADDR这个部分本来是在连接脚本用指定kernel开始执行地址的地方,不过在我修改的代码中没什么作用,只是原来临时用保留下来的。
arch/armnommu/config.in,这里是menuconfig的时候生成菜单以及进行编译配置的地方,在这其中:
DRAM_BASE 内存的起始地址
DRAM_SIZE 内存的长度
FLASH_MEM_BASE flash的起始地址
FLASH_SIZE flash的长度
CONFIG_NO_PGT_CACHE 表示Disable pgtable cache
CONFIG_CPU_WITH_CACHE 表示CPU有CACHE 启动的时候要clean
CONFIG_CPU_WITH_MCR_INSTRUCTION 表示系统是否有MCR指令
arch/armnommu/tools/mach-types,实际上这个文件是用来生成文件include/asm-armnommu/mach-types.h的,不过因为关联的问题,修改过这个文件后注意删除一下mach-types.h这个文件,保证修改的生效。
5.2.连接脚本
我使用了单独的连接脚本arch/armnommu/mach-gba/romlinux.lds。
在这里比较主要的就是设置init和data的开始地址设置在了0x2001000,内存开始的部分在0x2000000,而这0x1000是交给kernel让其写中断处理代码的。而在0x8040d00是text的开始地址。
如果仔细观察的话你可以发现我将*(.text.init)也放进了text,这样的目的就是为了节省内存。
其中有1个地方比较关键:
. = ALIGN(4096);
__init_end = .;
因为init节实际上在init函数中将要被释放掉的,而且其后跟着的就是init_task_union,所以这里一定要保证init节的长度跟页长度对齐,同时要保证这个__init_end生成的地址是跟task的长度对齐。而前面将开始地址设置在0x2001000也是出于这个目的。
5.3.kernel在start_kernel以前的代码
arch/armnommu/kernel/head-armv.S,这是kernel最开始启动的代码,这里体系相关的代码很多,通用的启动部分在448行,也就是我修改后代码的第464行。
一上来就是函数__lookup_processor_type,这个函数是用来根据芯片的类型ID从.proc.info中取出相应的proc_info_list结构,这个结构定义在include/asm-armnommu/procinfo.h文件中,这个段中的数据是arch/armnommu/mm/proc-*.S文件中的,如果你的芯片比较特殊,可以定义自己的芯片结构以及编写自己的文件,arm7tdmi在arch/armnommu/mm/proc-arm6,7.S文件中。这个函数中,我们可以看到在arch/armnommu/config.in中定义的CONFIG_CPU_WITH_MCR_INSTRUCTION,我也尝试跑过mrc指令取得芯片ID,不过代码马上就跑到了未定义指令上去了,所以我估计用这个是不行的,就定义了不支持mcr。在下面也写明了
# warning "FIXME: Get Processor ID without MCR Instruction"
还举出了使用的例子
@ A possible code
@ldr r9, PROCESSOR_ID_MEM_LOCATION
@ldr r9, [r9]
看一下arch/armnommu/mm/proc-arm6,7.S文件,就可以找到定义arm7tdmi的结构的是729行开始的,所以代码应该写成
ldr r9, __arm7tdmi_proc_info
ldr r9, [r9]
不过我开始是没仔细看这段注释的,直接就
@set r9 to arm7tdmi id(0x41007700)
mov r9,#0x7700
add r9,r9,#0x41000000
结果都是一样的,当然建议大家按uclinux提供的方式来弄比较好,我嘛,以后再改。
然后就是一大段根据这个ID比较的过程,最后返回,一些查找结构失败的判断,然后就是函数__lookup_architecture_type。
__lookup_architecture_type这个函数的作用是在.arch.info段中寻找相应的machine_desc结构,这个结构定义在include/asm-armnommu/mach/arch.h中,具体针对每个arch的定义就在arch/armnommu/kernel/arch.c以及arch/armnommu/mach-*/arch.c中,而GBA的就在arch/armnommu/mach-gba/arch.c中。其中具体的每个宏都在include/asm-armnommu/mach/arch.h中,都很明确我就不详细介绍了。
往后还有一些初始化工作,不过因为跟没有什么改动,就不详细介绍了。
5.3.start_kernel
5.3.1.setup_arch
这个函数用来做体系相关的初始化的,armnommu的在arch/armnommu/kernel/setup.c。注意一下540行,这里是默认的对物理内存结构meminfo的初始化,这个结构将在后面的内存初始化中起很重要的作用。在这其中,nr_banks指定了内存块的数量,bank是指定了每块内存的范围。而在这里用来指定块开始和长度的PAGE_OFFSET和MEM_SIZE,都定义在include/asm-armnommu/arch-gba/memory.h中,PAGE_OFFSET是内存的开始地址,我设置这个值为0x2001000,就是前面介绍的init和data的开始地址,结束地址自然就是内存的结束地址。后面函数就将根据meminfo进行内存结构初始化。
其中还有一个地方的修改,bootmem_init函数中调用的函数reserve_node_zero,这个函数的作用是保留一些内存以便使值不能被动态分配。看一下这个函数的内部,原来的代码是:
reserve_bootmem_node(pgdat, __pa(&_stext), &_end - &_stext);
这行的意思就是从_stext到_end都保留起来不做动态分配,而看我写的连接脚本就能知道这么写是不行的,因为_stext在flash中,所以我将这段代码改为:
reserve_bootmem_node(pgdat, __pa(&__init_begin), &_end - &__init_begin);
后面对init_arch_irq进行了初始化,这个将在后面用到。
5.3.2.parse_options和calibrate_delay
我的代码跑到这2行以后进去就跑不出来了,正好看一个文章写有什么问题,而且2个函数也没什么作用,所以我就直接将其去掉了。
5.3.3.trap_init
这个函数用来做体系相关的中断处理的初始化,armnommu的在arch/armnommu/kernel/traps.c。vectors_base的值是比较关键的,因为其的写中断向量的开始地址,他的设置在include/asm-armnommu/proc-armv/system.h,地址0因为GBA的BIOS的原因自然是写不进去的,所以我将其设置为0x2000000。然后就调用arch/armnommu/kernel/entry-armv.S中的函数__trap_init将中断处理代码写入vectors_base指定地址。
5.3.4.init_IRQ
这个函数用来做体系相关的irq处理的初始化,armnommu的在arch/armnommu/kernel/irq.c。一开始就是对根据定义在include/asm-armnommu/arch-gba/irqs.h中断数量NR_IRQS对中断结构irq_desc的初始化。
在默认的初始化完成后调用前面初始化的函数init_arch_irq,先到了arch/armnommu/kernel/irq-arch.c中的函数genarch_init_irq,然后就执行include/asm-armnommu/arch-gba/irq.h中的inline函数irq_init_irq,在这里对irq_desc进行了实质的初始化。其中mask用阻塞中断;unmask用来取消阻塞;mask_ack的作用是第一是阻塞中断,同时还发ack给板表示这个中断已经被处理了,否则板将会自动再次发生同一个中断,当然不是所有板需要这个ack,所以有的扳子的mask_ack跟mask用的是一个函数。后面3行语句,第1行设置对irq全部mask,第2行是对irq的另一个标志设置为irq可以发生,第3行是设置irq向量处理的位置,GBA将在接到irq自己处理后跳到这里指定的位置执行。
下面执行的函数是init_dma,前面我已经介绍过我没有在kernel使用GBA的DMA,所以就设置include/asm-armnommu/arch-gba/dma.h中的MAX_DMA_CHANNELS为0,这样在arch/armnommu/kernel/dma.c文件中你可以看到根据这个定义的不同会使用不同的函数。
5.3.5.time_init
这个函数用来做体系相关的timer的初始化,armnommu的在arch/armnommu/kernel/time.c。这里调用了在include/asm-armnommu/arch-gba/time.h中的inline函数setup_timer,首先设置timer的中断处理函数为gba_timer_interrupt,然后设置已经初始化好的结构timer_irq用setup_arm_irq为GBA的timer3的中断处理结构。
然后调用gba_enable_timer3启动timer3,其中LATCH是每个时钟中断用的时钟滴答的次数,这个值定义在include/linux/timex.h中:
#define LATCH ((CLOCK_TICK_RATE + HZ/2) / HZ)
其中CLOCK_TICK_RATE是定义在include/asm-armnommu/arch-gba/timex.h中,这个值是输入时钟脉冲的频率。
5.3.6.fork_init
因为GBA的内存太小,计算出的max_threads数量太小,所以我将其改成我定义的GBA_MAX_THREADS。
5.3.7.kernel_thread
这里调用了arch/armnommu/kernel/process.c中的函数arch_kernel_thread,在这个函数中kernel第一次使用了swi指令,在:
__syscall(clone)
我将其换成了
bl gba_sys_clone
gba_sys_clone这个函数定义在arch/armnommu/mach-gba/entry_gba.S中,sys_clone函数的最后一个参数是struct pt_regs *regs,这是一个寄存器的列表,原来是在swi的处理过程中存储进栈的,然后在返回的时候再将寄存器恢复成原来的值,在gba_sys_clone中模仿了这个过程。gba_save_user_regs是前面定义的宏,是模仿save_user_regs写的,作用就是将寄存器存入栈。然后将sp值放进r2,也就是作为sys_clone函数的最后一个参数。函数sys_clone执行结束后,将r0的值放入寄存器列表中r0的位置,这样做是保证函数的返回值最后可以返回,最后 用宏gba_restore_user_regs将寄存器值从栈中恢复到寄存器中。
其实如果是一般的系统调用,直接bl到相应函数就可以了,只是因为sys_clone函数还需要一个寄存器列表的函数,所以使用的上面的方法,类似的还有下面用到的sys_execve,只是
5.4.init
5.4.1.do_basic_setup
在这里的sock_init我觉得占内存太多,所以就把这个函数的执行去掉了。
5.4.2.blkmem相关的修改
下面这段在使用norfs的时候不需要,不过我还是保留了下来。
我将include/linux/blkdev.h中的MAX_NR_REQUESTS从1024换成了16,在blkmem_init中,调用了blk_init_queue,这里调用了blk_init_free_list,这里调用blk_grow_request_list,在这个函数中会kmem_cache_alloc出nr_requests个request结构,而nr_requests的值为MAX_NR_REQUESTS,如果是1024个request结构则将用掉过多的内存,实际GBA的全部内存也不够,所以我将其替换成了16。
在include/asm-armnommu/arch-gba/blkmem.h中的一行
#define FIXUP_ARENAS arena[0].address = (unsigned long)0x8240d1c;
其中0x8240d1c就是romfs在flash中的地址。
5.4.3.free_initmem
这个函数在arch/armnommu/mm/init.c中,就是前面提到的对init节的释放,如果你的体系不想释放init,可以用其中其他体系类似的方式指定不释放。
5.4.4.run_init_process
在这其中有一个execve,我将其换成了gba_sys_execve,基本过程跟上面的gba_sys_clone过程类似。不同的地方在sys_execve返回后,先判断r0的值是否大于等于0;如果不是则表示sys_execve的执行发生了错误,则将r0的值设置进积存器列表中;如果是则设置USR_MODE进寄存器列表,可能你要奇怪我为什么这么做了,因为寄存器列表应该已经在sys_execve被设置好了的,我这么做的原因是这样的:
先看一下实际对寄存器列表进行设置的宏start_thread
#define start_thread(regs,pc,sp)
({
unsigned long *stack = (unsigned long *)sp;
set_fs(USER_DS);
memzero(regs->uregs, sizeof(regs->uregs));
if (current->personality & ADDR_LIMIT_32BIT)
regs->ARM_cpsr = USR_MODE;
else
regs->ARM_cpsr = USR26_MODE;
regs->ARM_pc = pc; /* pc */
regs->ARM_sp = sp; /* sp */
regs->ARM_r2 = stack[2]; /* r2 (envp) */
regs->ARM_r1 = stack[1]; /* r1 (argv) */
regs->ARM_r0 = stack[0]; /* r0 (argc) */
})
可以很清楚的看到,依赖了current->personality才将cpsr设置为32位user模式,不然就是26位user模式。
而personality来自哪里呢,继承自init_task,但是在其的初始化宏INIT_TASK并没这个元素,应该是被设置为了0,这样最后被设成了 USR26_MODE,我觉得有点莫名其妙。
然后我试图用一种通用的办法解决这个问题:
发现在 CONFIG_ARCH_C5471 中有一个
static void __init
fixup_c5471(struct machine_desc *desc, struct param_struct *params,
char **cmdline, struct meminfo *mi)
{
/* This is probably not necessary for the init process. But, all
* normal C5471 processes must be marked as 32-bit processes. */
init_task_union.task.personality = PER_LINUX_32BIT;
}
俺一看马上模仿了写了一个,一跑还是不行,开始还都一直PER_LINUX_32BIT跑的满好,可最后到了binfmt_flat.c中,load_flat_file函数中有一句,set_personality(PER_LINUX); 被清成0了,偶延着这个set_personality走了一下,发现被设置成0是肯定的。
这下偶就晕了,别人的代码应该是跑的好好的,可为什么到我这里就不行了呢?只好在最后做了个不久工作,如果哪位知道指点一下我,多谢了。
5.5.irq的处理
刚才介绍了irq的初始化,这里来介绍发生irq后的处理。
发生中断以后,首先是GBA的BIOS对中断进行处理,然后到咱们前面在irq初始化时候指定的地址继续执行,那些代码在arch/armnommu/kernel/entry-armv.S中的第1360行开始也就是我修改过代码的1382行开始。在一上来我就增加了一行:
ldmia r13!,{r0-r3,r12,r14}
因为GBA的BIOS会将这几个寄存器存入栈,为了让这些寄存器保持一个irq才发生时候的值。而在后面设置r13为0x3007fa0,这样BIOS处理irq的时候的栈指针,这样可以保证下次再发生irq以后BIOS可以做正常处理。
这里的处理结束后就会根据当前进程所处的情况分别执行__irq_usr或者__irq_svc,在这其中get_irqnr_and_base就是取得当前中断的体系相关的取得当前中断号的宏。这个宏的几个参数代表的意思为:
irqnr: 中断号码,也就是取得返回的中断号码。
irqstat:用来存储irq当前状态的临时寄存器。
base:中断优先级别,不过在这里好象没起实际作用,所以我也不太了解具体做什么的。
tmp:给宏中使用的临时寄存器。
宏中整个的过程就是将GBA_IRQ_STAT位置中的中断状况,然后循环比较,最后将返回。
5.6.对page和task大小的修改
因为GBA内存的问题,实际还没运行到init函数内存就不够了,我采取的解决办法是将page和task改小。
首先我将include/asm-armnommu/proc-armv/page.h中页的大小改小。
然后我修改了include/asm-armnommu/processor.h中的THREAD_SIZE的页大小改小。不过在代码中实际几乎没有人使用THREAD_SIZE,而都用了8192,所以我都将他们改成了THREAD_SIZE,包括一些别的体系的代码,个人还是希望kernel中的东西多用宏,方便查看也能方便修改。还有修改了include/asm-armnommu/proc-armv/processor.h中的ll_alloc_task_struct宏和ll_free_task_struct宏,这两个宏是在do_fork的时候alloc_task_struct调用的,用来分配task的空间,既然同时改变了page和task的大小,自然也要修改。
还有就是要修改根据sp取得task开始地址的函数,这里一共有2个。一个是include/asm-armnommu/current.h中的inline函数get_current,主要是将sp的值最后的几位设置为0,所以这也是前面一直要内存对齐的原因。还有一个是arch/armnommu/kernel/entry-header.S中的宏get_current_task,刚才那个函数是给c代码用的,而这个是给asm代码用的。它的作用跟刚才那个函数一样,也是将最后几位设置为0,不过他使用了移位的方法,而前面的函数使用了位清除,这2段代码的作用是一样的,希望对这里有理解的朋友指点这样使用各自的好处。
5.7.关于大量使用宏ifdef的时候需要注意的东西
移植的代码为了保证能不影响别的体系代码的使用,我大量的使用了
#ifdef CONFIG_ARCH_GBA
#else
#endif
但是如果前面没有
#include
则将导致不管定义了什么选项,编译中只使用#else后面的部分,这自然是要发生错误的,我使用的检查方法是,在打开了GBA选项以后,在#else后定义一个#error,这样在发生定义错误的时候编译马上会终止,这样就能找到需要增加#include 的位置,在include/asm-armnommu/proc-armv/page.h中就是这样的情况。
5.8.为了让Kernel支持norfs作为根文件系统对Kernel进行的修改
在do_mounts.c文件中的mount_block_root函数是用来mount根分区的函数,但是因为norfs不是FS_REQUIRES_DEV类型的文件系统,所以不会在其循环mount中出现,我为了方便在其全部失败后的位置增加了单独的mount norfs的代码

时间:2005-07-11 11:40 来源: 作者:wheel 原文链接

好文,顶一下
(1)
100%
文章真差,踩一下
(0)
0%
------分隔线----------------------------


把开源带在你的身边-精美linux小纪念品
无觅相关文章插件,快速提升流量