共3条
1/1 1 跳转至页
Porting,uClinux,to,Samsung,S3C44B0X,Board [转]Porting uClinux to Samsung S3C44B0X
问
一.Bootloader
理论上,uClinux引导时并非一定需要一个独立于Kernel Image的Bootloader Image。然而,将Bootloader与Kernel分开设计能够使软件架构更加清晰,也有助于灵活地支持多种引导方式,实现一些有用的辅助功能。Bootloader的主要任务可以概括如下:
1.硬件初始化和系统引导;
2.加载uClinux Kernel Image (如果需要);
3.设置需要传递给Kernel的启动参数(如果需要);
4.调用uClinux Kernel;
5.辅助功能:从主机下载新的Image;
6.辅助功能:烧写Flash Memory;
7.辅助功能:支持功能5和6所需的人机界面,如串行终端上的命令行接口。
对于常见的几类处理器内核,现在一般都找得到现成的Bootloader可用,不过需要针对具体的Board做些移植。在实现上述功能的前提下,也可以选择自行开发。由于Bootloader Image在物理上独立于Kernel Image,因此不一定选择GNU作为开发工具。对于以ARM7TDMI为内核的S3C44B0X处理器,完全可以使用ADS来开发Bootloader。
1.硬件初始化和系统引导
完整的Bootloader引导流程可描述如下:
硬件初始化阶段一 -> 复制二级Exception Vector Table -> 初始化各种处理器模式 -> 复制RO和RW,清零ZI -> (跳转到C代码入口函数) -> 初始化Exception/Interrupt Handler Entry Table -> 初始化Device Drivers -> 硬件初始化阶段二 -> 建立人机界面
下面对上述各步骤逐一加以说明。
1.1 硬件初始化阶段一
板子上电或复位后,程序从位于地址0x0的Reset Exception Vector处开始执行,因此需要在这里放置Bootloader的第一条指令:b ResetHandler,跳转到标号为ResetHandler处进行第一阶段的硬件初始化,主要内容为:关Watchdog Timer,关中断,初始化PLL和时钟,初始化Memory Controller。比较重要的是PLL的输出频率要算正确,这里把它设置为50MHz;后面在计算SDRAM的Refresh Count和UART的Baud Rate等参数时还要用到。
1.2 复制二级Exception Vector Table
Exception Vector Table是Bootloader与uClinux Kernel发生联系的地方之一(另两处是加载and/or调用Kernel,以及向Kernel传递启动参数)。ARM7规定Exception Vector Table的基地址是0x0,所以Flash Memory的基地址也必须是0x0;而S3C44B0X处理器又不支持Memory Remap,这意味着无论运行什么样的上层软件,一旦发生中断,程序就得到Flash Memory中的Exception Vector Table里去打个转(中断Interrupt是异常Exception的一种)。对于uClinux而言,它会在RAM中建立自己的二级Exception Vector Table(后面将提到基地址被设为0x0C000000),所以在编写Bootloader时,地址0x0处的一级Exception Vector Table只需简单地包含向二级Exception Vector Table跳转的内容:
b ResetHandler ;Reset Handler
ldr pc,=0x0c000004 ;Undefined Instruction Handler
ldr pc,=0x0c000008 ;Software Interrupt Handler
ldr pc,=0x0c00000c ;Prefetch Abort Handler
ldr pc,=0x0c000010 ;Data Abort Handler
b .
ldr pc,=0x0c000018 ;IRQ Handler
ldr pc,=0x0c00001c ;FIQ Handler
LTORG
如果在Bootloader执行的全过程中都不必响应中断,那么上面的设置已能满足要求。但如果某些Bootloader功能要求使用中断(例如用Timer Interrupt实现精确定时,或利用External Interrupt支持Ethernet Driver以实现TFTP下载),那么Bootloader必须在同样的地址处配置自己的二级Exception Vector Table,以便同uClinux兼容。这张表事先存放在Flash Memory里,引导过程中由Bootloader将其复制到RAM地址0x0C000000:
存放:
RelocatedExceptionVectorStart
mov pc,#0
b HandlerUndef
b HandlerSWI
b HandlerPAbort
b HandlerDAbort
b .
b HandlerIRQ
b HandlerFIQ
HandlerUndef HANDLER HandleUndef
HandlerSWI HANDLER HandleSWI
HandlerPAbort HANDLER HandlePAbort
HandlerDAbort HANDLER HandleDAbort
HandlerIRQ HANDLER HandleIRQ
HandlerFIQ HANDLER HandleFIQ
LTORG
RelocatedExceptionVectorEnd
复制:
adr r0, RelocatedExceptionVectorStart
ldr r2, =0x0c000000
adr r3, RelocatedExceptionVectorEnd
0
cmp r0, r3
ldrcc r1, [r0], #4
strcc r1, [r2], #4
bcc %B0
其中HANDLER是一个宏,用于查找Exception Handler Routines的入口地址。这些地址存放在由HandleXXX指向的表项中,该表定位在RAM高端,基地址为_ISR_STARTADDRESS:
^ _ISR_STARTADDRESS
HandleReset # 4
HandleUndef # 4
HandleSWI # 4
HandlePAbort # 4
HandleDAbort # 4
HandleReserved # 4
HandleIRQ # 4
HandleFIQ # 4
该表的内容将在步骤1.5:“初始化Exception/Interrupt Handler Entry Table”中被填写为各Exception Handler Routine的入口地址。
1.3 初始化各种处理器模式
ARM7TDMI支持7种Operation Mode:User,FIQ,IRQ,Supervisor,Abort,System和Undefined。Bootloader需要依次切换到每种模式,初始化其程序状态寄存器(SPSR)和堆栈指针(SP)。S3C44B0X处理器在上电或复位后处于Supervisor模式,本步骤中把对Supervisor模式的初始化放在最后,也就是说Bootloader的后续部份仍将运行在Supervisor模式下。
1.4 复制RO和RW,清零ZI
对于ADS开发工具而言,一个ARM程序由RO,RW和ZI三个段组成,其中RO为代码段,RW是已初始化的全局变量,ZI是未初始化的全局变量(对于GNU工具,对应的概念是TEXT,DATA和BSS)。RO段既可以在Flash Memory中运行,也可以在RAM中运行。考虑到Bootloader中可能需要烧写Flash Memory,因此在引导阶段应当将RO和RW段复制到RAM中,并将ZI段清零。当RO段复制完毕之后,程序就可以跳转到RAM中运行。若不考虑烧写Flash Memory,则可以不复制RO段,程序始终在Flash Memory中运行。ADS使用下列Linker Symbols来记录各段的起始和结束地址:
|Image$$RO$$Base| :RO段起始地址
|Image$$RO$$Limit| :RO段结束地址加1
|Image$$RW$$Base| :RW段起始地址
|Image$$RW$$Limit| :ZI段结束地址加1
|Image$$ZI$$Base| :ZI段起始地址
|Image$$ZI$$Limit| :ZI段结束地址加1
可以在程序中引用这些标号。需要注意的是,这些标号的值是根据ARM Linker中RO Base和RW Base的设置来计算的,属于“Linker Address”或“Execution Address”,并不一定代表这些段存放在Flash Memory中的地址,在编写复制程序时需要根据具体情况作相应的计算。
1.5 初始化Exception/Interrupt Handler Entry Table
在步骤1.2里已经提到,需要在这一步中填写各Exception Handler Routine的入口地址。由于IRQ Exception为全部的中断所共用,因此必须在IRQ Exception Handler Routine中根据中断状态寄存器判断中断源,并调用相应的Interrupt Handler Routine。各Interrupt Handler Routine的入口地址也存放在上述的Exception/Interrupt Handler Entry Table中(紧接在HandleFIQ之后),需要在这一步中填写,这里就不一一列出了。
另外,S3C44B0X处理器的Interrupt Controller支持两种中断处理模式:Vectored Mode和Non-Vectored Mode,其中前者可能是Samsung特有的模式,并不被其它ARM7内核所支持。考虑到代码的可移植性,以上讨论仅针对这里所使用的Non-Vectored Mode。
1.6 初始化Device Drivers
在这一步中需要为Bootloader用到的一些关键Device Drivers建立必要的数据结构,主要包括用于精确定时的Watchdog Timer Driver和用于支持串行终端的UART Driver。
1.7 硬件初始化阶段二
继续对硬件进行初始化,主要包括:GPIO,Cache,Interrupt Controller,Watchdog Timer和UARTs。S3C44B0X处理器内置data/instruction合一的8KB Cache,且允许按地址范围设置两个Non-Cacheable区间。合理的配置是打开对RAM区间的Cache,关闭对其它地址区间(包含Flash Memory区间)的Cache。所有硬件初始化完毕之后,开中断。
在步骤1.6和1.7中,仍然遵循“必要”的原则对硬件和Device Drivers进行初始化。在目前阶段没有涉及的设备(如Ethernet Controller),可以留待使用它们之前再进行初始化。
1.8 建立人机界面
引导过程的最后一步是在串行终端上建立人机界面,并等待用户输入命令。合理的做法是先等待固定的秒数,若在此期间未接收到用户输入,则直接从Flash Memory中加载and/or调用uClinux Kernel。若接收到用户输入,则显示菜单模式或命令行模式的交互界面,等待用户进一步的命令。这里就不对此详细讨论了。
2.加载Kernel
Bootloader是否需要执行加载操作,取决于uClinux Kernel Image的类型。根据不同的配置,可以生成下面几种uClinux Kernel Image:
2.1 非压缩,非XIP
XIP(eXecute In Place)是指不对代码段重新定位,在存放代码段的位置就地运行程序。该类型的uClinux Kernel Image以非压缩格式存放在Flash Memory中,由Bootloader加载到RAM中并直接调用。
2.2 非压缩,XIP
该类型的uClinux Kernel Image以非压缩格式存放在Flash Memory中,不需加载,由Bootloader直接调用。复制init段和data段,清零bss段的工作由Kernel自行完成。
2.3 RAM自解压
压缩格式的uClinux Kernel Image都是由开头的一段自解压代码和后面的压缩数据部分组成。对于Kernel而言,由于是以压缩格式存放,因次只能以非XIP方式执行。RAM自解压类型的uClinux Kernel Image存放在Flash Memory中,由Bootloader加载到RAM中的一段临时空间,然后调用其自解压代码。可执行的uClinux Kernel被解压到最终的执行空间,然后运行。压缩格式Image所占据的临时RAM空间可在随后由uClinux回收利用。
2.4 ROM自解压
解压缩操作也可以在Flash Memory中完成。实际上,这意味着以XIP方式执行自解压代码。ROM自解压类型的uClinux Kernel Image存放在Flash Memory中,不需加载,由Bootloader直接调用其自解压代码。自解压代码自行复制其data段,清零bss段,然后将可执行的uClinux Kernel解压到最终的执行空间并运行之。与RAM自解压相比,用ROM自解压方式引导uClinux并不真正节省RAM,而且在Flash Memory中解压缩速度较慢,因此实用价值不大。
2.5 Memory Map
下面给出Bootloader和uClinux在存储和运行时的Memory Map。系统存储器由NOR Flash Memory (2MB)和SDRAM(32MB)组成,Flash Memory的地址范围从0x0到0x00200000,SDRAM的地址范围从0x0C000000到0x0E000000。
Flash Memory
0x00000000 ~ 0x00020000: 存放Bootloader
0x00020000 ~ 0x00200000: 存放所有类型的uClinux Kernel Image
运行2.2类型的uClinux Kernel
运行2.4类型的自解压代码
SDRAM
0x0C008000 ~ xxxxxxxx: 运行Bootloader
0x0C200000 ~ xxxxxxxx: 运行2.1,2.3,2.4类型uClinux Kernel
0x0C400000 ~ xxxxxxxx: 临时存放2.3类型压缩Image,并运行自解压
3.设置内核启动参数
Linux 2.4.x以后的内核都期望以标记列表(tagged list)的形式来传递启动参数。每个标记存放在一个tag结构中,每个tag结构由标识被传递参数的tag_header结构以及随后存放的参数值组成。数据结构tag、tag_header以及各种参数的数据结构都定义在Linux内核源码的头文件linux/include/asm-ARMnommu/setup.h中。
通常需要由Bootloader设置的启动参数有:ATAG_MEM、ATAG_CMDLINE、ATAG_RAMDISK、ATAG_SERIAL、ATAG_INITRD等。启动参数的标记列表以ATAG_CORE开始,以ATAG_NONE结束,代码示例如下。其中0x0C000100是内核启动参数在RAM中的基地址,指针params的类型是struct tag。宏tag_next()以指向当前标记的指针为参数,计算下一个标记的起始地址。
params = (struct tag *)0x0C000100;
params->hdr.tag = ATAG_CORE;
params->hdr.size = tag_size(tag_core);
params->u.core.flags = 0;
params->u.core.pagesize = 0;
params->u.core.rootdev = 0;
params = tag_next(params);
……
params->hdr.tag = ATAG_NONE;
params->hdr.size = 0;
在Linux内核源码的linux/arch/ARMnommu/mach-s3c44b0/arch.c中设置内核启动参数在RAM中的基地址。如果Kernel不需要从Bootloader接收启动参数,下面代码中的“BOOT_PARAMS(0x0C000100)”这一行可以不写。
MACHINE_START(S3C44B0, "44B0EVAL")
MAINTAINER("XXX YYY")
BOOT_MEM(DRAM_BASE,0x00000000,0x00000000)
BOOT_PARAMS(0x0C000100)
INITIRQ(genarch_init_irq)
MACHINE_END
uClinux Kernel处理启动参数时的代码调用关系可查阅linux/init/main.c和linux/arch/ARMnommu/kernel/setup.c:start_kernel()àsetup_arch()àparse_tags()。parse_tags()函数中调用parse_tag()函数依次处理每个标记。parse_tag()函数先判断tag_header结构中的标记类型,然后调用相应的处理函数。例如,调用parse_tag_cmdline()处理ATAG_CMDLINE标记,调用parse_tag_initrd()处理ATAG_INITRD标记,等等。对应关系如下:
static const struct tagtable core_tagtable[] __init = {
{ ATAG_CORE, parse_tag_core},
{ ATAG_MEM, parse_tag_mem32},
{ ATAG_VIDEOTEXT, parse_tag_videotext},
{ ATAG_RAMDISK, parse_tag_ramdisk},
{ ATAG_INITRD, parse_tag_initrd},
{ ATAG_SERIAL, parse_tag_serialnr},
{ ATAG_REVISION, parse_tag_revision},
{ ATAG_CMDLINE, parse_tag_cmdline}
};
对于Kernel Command Line,parse_tag_cmdline()函数将用内核参数表中的命令字符串来覆盖default_command_line[]变量。如果Kernel不从Bootloader接收启动参数,也可以有两种方法来初始化Kernel Command Line。在linux/arch/ARMnommu/kernel/setup.c中有:
static char default_command_line[COMMAND_LINE_SIZE] __initdata = CONFIG_CMDLINE;
因此一种方法是在make menuconfig时通过修改“General Setup”子菜单中的“Default kernel command string”选项来定义linux/include/linux/autoconf.h头文件中的CONFIG_CMDLINE宏,另一种方法是在linux/arch/ARMnommu/kernel/setup.c中直接把default_command_line[]写死。
4.调用Kernel
Bootloader调用uClinux Kernel的方法是直接跳转到Kernel的第一条指令处。在跳转时要满足下列条件:
CPU寄存器r0=0;
CPU寄存器r1=Machine Type ID(S3C44B0X的Machine Type ID定义在include/asm-ARM/mach-types.h中:#define MACH_TYPE_S3C44B0 178。寄存器r1也可以在Kernel启动之初的head-ARMv.S中设置);
禁止中断(IRQs和FIQs);
CPU运行在SVC模式;
MMU必须关闭(S3C44B0X没有MMU);
指令Cache可以打开也可以关闭,数据Cache必须关闭(S3C44B0X的Cache是指令与数据合一的,因此只能选择关闭)。
C代码调用Kernel的示例如下,其中r0和r1的值通过参数传递:
void (*CallKernel)(int zero, int mach) = (void (*)(int, int))KERNEL_ADDR;
CallKernel(0, 178);
5.辅助功能
完整的Bootloader还应该支持从主机下载文件到目标板的RAM;用RAM中的数据烧写Flash Memory;以及上述功能所需的人机交互接口。文件下载途径视目标板所提供的物理通讯接口而定,比较简单的方法一般是通过串口,用Xmodem或Ymodem协议下载,但速度较慢。目标板上只需要实现协议的接收部份,主机上可以用HyperTerminal等工具来发送文件。如果目标板提供Ethernet等快速接口,也可以移植一个简单的TCP/IP栈,用TFTP等标准文件传输协议下载。目前Flash Memory一般都是用NOR Flash,烧写是非常简单的;需要注意的是对于多数Flash芯片,在Erase/Program之前需要先Unprotect。人机交互接口不难在串行终端上实现,这里就不赘述了。
二. uClinux Kernel的调试
在移植uClinux的过程中,较为困难的是如何发现并解决Kernel启动阶段的问题。本节的内容主要取自网上的一篇文档《用AXD + Multi-ICE调试uClinux内核》,说明如何使用ADS和Multi-ICE工具对Kernel进行源代码级的调试。
首先是设置ARM-elf-gcc编译器,使其能够输出dwarf-2格式的调试信息。修改linux/Makefile,将CFLAGS_KERNEL设置为-gdwarf-2,然后重新编译uClinux Kernel:
CFLAGS_KERNEL = -gdwarf-2
把以下文件复制到Windows下:image.ram,System.map,linux以及一份编译时所用的内核源码。image.ram是上面提到过的非压缩/非XIP模式的Kernel Image;System.map提供了各symbol所对应的地址;linux是一个ELF格式的文件,包含有AXD Debugger所需的调试信息。
开发板上电,启动Multi-ICE Server,启动AXD Debugger。使用FileLoad Memory From File读入image.ram,其中基地址应设为Kernel的TEXTADDR,这里是0x0C200000。然后使用FileLoad Debug Symbols读入linux文件。把PC寄存器设置为image.ram的起始地址0x0C200000,此时在AXD Debugger的Disassembly窗口中已能看到一些符号的名称。
在AXD Debugger中,Processor ViewsSource,选择要调试的源码文件。例如,选择linux/init/main.c,在Kernel的入口函数start_kernel()中设置断点。也可以在System.map中查找到start_kernel的地址,然后在Disassembly窗口中跳到该地址并设置断点。最后Go,就可以从断点处开始调试了。
三. uClinux Kernel的移植
1. 相关的目录结构
Machine-Specific源代码: linux/arch/ARMnommu/mach-s3c44b0
Machine-Specific头文件: linux/include/asm-ARMnommu/arch-s3c44b0
通用的设备驱动程序: linux/drivers/block
linux/drivers/char
linux/drivers/mtd
linux/drivers/net
文件系统: linux/fs
网络协议栈: linux/net
其它linux/arch/ARMnommu 子目录下的Architecture-Specific源代码:
kernel: 核心内核代码;
mm: 内存管理代码;
lib: ARM-Specific或经过优化的内部库函数代码;
nwfpe/fastfpe: 浮点库的两种实现;
boot: 用于生成压缩内核镜像的代码;
tools: 用于自动生成头文件的配置脚本;
def-configs: 各Machine-Specific的缺省配置文件。
其它linux/include/asm-ARMnommu子目录下的Architecture-Specific头文件:
hardware: ARM-Specific芯片或设备的头文件;
mach: 多数Machine共有的一般性接口定义(如DMA/IRQ/PCI);
proc-ARMo: 26-bit版本的ARM处理器相关头文件;
proc-ARMv: 32-bit版本的ARM处理器相关头文件。
2. 相关的源码文件和移植要点
现在结合源码文件,对一些移植过程中的要点进行讨论,目的是得到能够在Samsung S3C44B0X开发板上运行的非压缩,非XIP模式的uClinux Kernel Image。该Image由Bootloader加载并调用。
2.1 内核配置系统
Linux的内核配置系统由Makefile,配置脚本(config.in)和配置工具组成。当用户Make config,Make menuconfig或Make xconfig时,相应的配置工具按照配置脚本config.in的内容显示可用的配置选项。用户完成配置并存盘退出时,配置信息保存在配置文件.config中,原有的.config文件被更名为.config.old。Makefile根据.config中的配置信息,构造出需要编译的源文件列表,然后分别编译;并根据Makefile中指定的链接器脚本,把目标代码链接到一起,最终形成Linux Kernel Image。
在配置脚本linux/arch/ARMnommu/config.in中,应给出可供用户选择S3C44B0X开发板的选项,并定义基于该处理器的开发板的一些重要参数,如存储器空间等:
comment 'System Type'
choice 'ARM system type' …… ……
S3C44B0 CONFIG_ARCH_S3C44B0 …… ……
if [ "$CONFIG_ARCH_S3C44B0" = "y" ]; then
define_bool CONFIG_NO_PGT_CACHE y
define_bool CONFIG_CPU_32 y
define_bool CONFIG_CPU_26 n
define_bool CONFIG_CPU_ARM710 y
define_bool CONFIG_CPU_WITH_CACHE y
define_bool CONFIG_CPU_WITH_MCR_INSTRUCTION n
define_bool CONFIG_SERIAL_S3C44B0 y
define_hex DRAM_BASE 0x0C000000
define_hex DRAM_SIZE 0x02000000
define_hex FLASH_MEM_BASE 0x00000000
define_hex FLASH_SIZE 0x00200000
fi
在针对ARM Architecture (NO MMU)的linux/arch/ARMnommu/Makefile中,应为S3C44B0X开发板定义处理器类型(26-bit或32-bit),Machine名称和代码段基地址,并指定链接器脚本为linux/arch/ARMnommu/vmlinux-ARMv.lds.in:
LINKFLAGS := -p -X -T arch/ARMnommu/vmlinux.lds
……
ifeq ($(CONFIG_CPU_32),y)
PROCESSOR = ARMv
endif
……
ifeq ($(CONFIG_ARCH_S3C44B0), y)
TEXTADDR = 0x0C200000
MACHINE = s3c44b0
endif
……
arch/ARMnommu/vmlinux.lds: arch/ARMnommu/vmlinux-$(PROCESSOR).lds.in dummy
2.2 内核启动入口
在linux/Makefile中可以找到生成uClinux Kernel Image的规则为:
ifdef CONFIG_UCLINUX
LINUX=linux
endif
……
$(LINUX): $(CONFIGURATION) init/main.o init/version.o init/do_mounts.o linuxsubdirs
$(LD) $(LINKFLAGS) $(HEAD) init/main.o init/version.o init/do_mounts.o --start-group $(CORE_FILES) $(DRIVERS) $(NETWORKS) $(LIBS) --end-group -o $(LINUX)
可见linux是由HEAD、main.o、version.o、do_mounts.o、CORE_FILES、DRIVERS、NETWORKS和LIBS组成的。这些变量定义了用于链接生成linux的目标文件和库文件列表。其中HEAD在linux/arch/ARMnommu/Makefile中定义,用来确定最先被链接进linux的文件:
HEAD := arch/ARMnommu/kernel/head-$(PROCESSOR).o arch/ARMnommu/kernel/init_task.o
因此,可以确定入口代码是linux/arch/ARMnommu/kernel/head-ARMv.S。在前面介绍Bootloader时曾经提到,uClinux Kernel在启动时要求把Machine Type ID存放在寄存器r1中。如果Bootloader在调用Kernel时没有以参数传递的方式设置r1,则必须在head-ARMv.S中先对r1进行初始化:
……
#elif defined(CONFIG_ARCH_S3C44B0)
mov r1, #MACH_TYPE_S3C44B0
……
#endif
其中,宏MACH_TYPE_S3C44B0定义在头文件linux/include/asm-ARMnommu/mach-types.h中:
#define MACH_TYPE_S3C44B0 178
……
#ifdef CONFIG_ARCH_S3C44B0
#ifdef machine_arch_type
#undef machine_arch_type
#define machine_arch_type __machine_arch_type
#else
#define machine_arch_type MACH_TYPE_S3C44B0
#endif
#define machine_is_s3c44b0() (machine_arch_type==MACH_TYPE_S3C44B0)
#else
#define machine_is_s3c44b0() (0)
#endif
该头文件是由脚本linux/arch/ARMnommu/tools/gen-mach-types根据linux/arch/ARMnommu/tools/mach-types文件中的对应信息自动生成的:
#machine_is_xxx CONFIG_xxxx MACH_TYPE_xxx number
…… …… …… ……
s3c44b0 ARCH_S3C44B0 S3C44B0 178
在head-ARMv.S中,接下来就可以初始化Processor ID和Machine Type ID,清除BSS段,并跳转到C代码的入口函数start_kernel()了:
#if defined(CONFIG_ARCH_S3C44B0)
adr r5, LC0
ldmia r5, {r5, r6, r8, r9, sp}
/* clear BSS */
mov r4, #0
1: cmp r5, r8
strcc r4, [r5], #4
bcc 1b
ldr r2, S3C44B0_PROCESSOR_TYPE
str r2, [r6]
mov r2, #MACH_TYPE_S3C44B0
str r2, [r9]
/* call start_kernel() */
mov fp, #0
b start_kernel
LC0: .long __bss_start @ r5
.long processor_id @ r6
.long _end @ r8
.long __machine_arch_type @ r9
.long init_task_union+8192 @ sp
S3C44B0_PROCESSOR_TYPE:
.long 0x36366036
#endif
其中标号__bss_start和_end分别代表了BSS段的起始地址和结束地址,它们都定义在链接器脚本linux/arch/ARMnommu/vmlinux-ARMv.lds.in中。而全局变量processor_id和__machine_arch_type则是定义在linux/arch/ARMnommu/kernel/setup.c中。
2.3 异常处理和中断处理
uClinux Kernel的C代码入口函数start_kernel()定义在linux/init/main.c中。该函数中,有关调用setup_arch()处理内核启动参数等内容在前面已有提及。此外,较为重要的还有对异常和中断的处理:
asmlinkage void __init start_kernel(void)
{
……
trap_init();
init_IRQ();
……
}
linux/arch/ARMnommu/kernel/traps.c中的trap_init()函数调用linux/arch/ARMnommu/kernel/entry-ARMv.S中的__trap_init()函数,把二级Exception Vector Table设置到基地址vectors_base()处:
void __init trap_init(void)
{
……
extern void __trap_init(void *);
__trap_init((void *)vectors_base());
……
}
其中宏vectors_base()被设置为系统中SDRAM的起始地址0x0C000000,定义在linux/include/asm-ARMnommu/proc-ARMv/system.h中:
#ifdef CONFIG_ARCH_S3C44B0
#define vectors_base() (0x0C000000)
#endif
__trap_init()函数首先从标号.LCvectors处复制二级Exception Vector Table,其中参数vectors_base()通过寄存器r0传递:
.equ __real_stubs_start, .LCvectors + 0x200
.LCvectors:
swi SYS_ERROR0
b __real_stubs_start + (vector_undefinstr - __stubs_start)
ldr pc, __real_stubs_start + (.LCvswi - __stubs_start)
b __real_stubs_start + (vector_prefetch - __stubs_start)
b __real_stubs_start + (vector_data - __stubs_start)
b __real_stubs_start + (vector_addrexcptn - __stubs_start)
b __real_stubs_start + (vector_IRQ - __stubs_start)
b __real_stubs_start + (vector_FIQ - __stubs_start)
…… …… ……
adr r1, .LCvectors
ldmia r1, {r2, r3, r4, r5, r6, r7, r8, r9}
stmia r0, {r2, r3, r4, r5, r6, r7, r8, r9}
然后__trap_init()函数把各Exception Handler Routine的代码从__stubs_start复制到vectors_base()+0x200,也就是0x0C000200处,以便与上述二级Exception Vector Table中的跳转指令相匹配。原先存放该段代码的地址区间由__stubs_start和__stubs_end标记:
add r2, r0, #0x200
adr r0, __stubs_start
adr r1, __stubs_end
1: ldr r3, [r0], #4
str r3, [r2], #4
cmp r0, r1
blt 1b
中断的初始化函数init_IRQ()定义在linux/arch/ARMnommu/kernel/irq.c中:
struct irqdesc irq_desc[NR_IRQS];
void (*init_arch_irq)(void) __initdata = NULL;
……
void __init init_IRQ(void)
{
……
for (irq = 0; irq < NR_IRQS; irq++) {
irq_desc[irq].probe_ok = 0;
irq_desc[irq].valid = 0;
irq_desc[irq].noautoenable = 0;
irq_desc[irq].mask_ack = dummy_mask_unmask_irq;
irq_desc[irq].mask = dummy_mask_unmask_irq;
irq_desc[irq].unmask = dummy_mask_unmask_irq;
}
init_arch_irq();
……
}
irq_desc[]数组用于存放IRQ请求描述符。每个中断号对应一个irq_desc结构,NR_IRQS代表中断总数。S3C44B0X开发板共有26个中断,中断号和NR_IRQS都定义在linux/include/asm-ARMnommu/arch-s3c44b0/irqs.h中。函数指针init_arch_irq()初始时为空,在前面提到的linux/arch/ARMnommu/kernel/setup.c里的setup_arch()函数中初始化:
struct machine_desc *mdesc;
……
mdesc = setup_architecture(machine_arch_type);
……
init_arch_irq = mdesc->init_irq;
其中结构类型machine_desc定义在linux/include/asm-ARMnommu/mach/arch.h中,而对应的结构体定义在前面提到过的linux/arch/ARMnommu/mach-s3c44b0/arch.c中:
MACHINE_START(S3C44B0, "44B0EVAL")
MAINTAINER("XXX YYY")
BOOT_MEM(DRAM_BASE,0x00000000,0x00000000)
BOOT_PARAMS(0x0C000100)
INITIRQ(genarch_init_irq)
MACHINE_END
setup_architecture()函数根据Machine Type ID在.arch.info段中搜索与之匹配的machine_desc结构,故mdesc->init_irq实际上将指向genarch_init_irq()函数。该函数定义在linux/arch/ARMnommu/kernel/irq-arch.c中:
void __init genarch_init_irq(void)
{
irq_init_irq();
}
而真正对irq_desc[]数组进行初始化的irq_init_irq()函数定义在linux/include/asm-ARMnommu/arch-s3c44b0/irq.h中,该头文件被linux/arch/ARMnommu/mach-s3c44b0/irq.c所包含:#include <asm/mach/irq.h>
在s3c44b0x_int_init()函数中将初始化S3C44B0X中断控制器的各寄存器。s3c44b0x_int_init()、s3c4510b_mask_ack_irq()、s3c4510b_mask_irq()和s3c4510b_unmask_irq()等函数也都定义在linux/arch/ARMnommu/mach-s3c44b0/irq.c中。
static __inline__ void irq_init_irq(void)
{
……
s3c4510b_int_init();
……
for (irq = 0; irq < NR_IRQS; irq++) {
irq_desc[irq].valid = 1;
irq_desc[irq].probe_ok = 1;
irq_desc[irq].mask_ack = s3c4510b_mask_ack_irq;
irq_desc[irq].mask = s3c4510b_mask_irq;
irq_desc[irq].unmask = s3c4510b_unmask_irq;
}
}
2.4 系统时钟
在linux/init/main.c有对系统时钟的初始化:
asmlinkage void __init start_kernel(void)
{
……
time_init();
……
}
函数time_init()定义在linux/arch/ARMnommu/kernel/time.c中:
#include <asm/arch/time.h>
void __init time_init(void)
{
……
setup_timer();
……
}
函数setup_timer()属于architecture-specific代码,可以在头文件linux/include/asm-ARMnommu/arch-s3c44b0/time.h里实现:
extern struct irqaction timer_irq;
extern void samsung_timer_interrupt(int, void *, struct pt_regs *);
void __inline__ setup_timer (void)
{
rTCON &= 0xf0ffffff;
rTCFG0 &= 0xff00ffff;
rTCFG0 |= (16-1)<<16;
rTCFG1 &= 0xff0fffff;
rTCFG1 |= 0<<20;
rTCNTB5 = fMCLK_MHz/(100*16*2);
rTCON |= 0x02000000;
rTCON &= 0xf0ffffff;
rTCON |= 0x05000000;
CLEAR_PEND_INT(IRQ_TIMER);
INT_ENABLE(IRQ_TIMER);
timer_irq.handler = samsung_timer_interrupt;
setup_ARM_irq(IRQ_TIMER, &timer_irq);
}
该函数首先初始化S3C44B0X PWM Timer 5,作为系统时钟源。Timer 5的输入时钟频率由以下公式计算:
Timer Input Clock Frequency = fMCLK_MHz / (Prescaler Value + 1) / (Divider Value)
其中Prescaler Value和Divider Value在TCFG0和TCFG1寄存器中设置。fMCLK_MHz通常等于PLL的输出频率,定义在linux/include/asm-ARMnommu/arch-s3c44b0/hardware.h中。PLL是在Bootloader里初始化的,该频率值应与当初的设置相匹配,假设为50MHz:
#define MHz 1000000
#define fMCLK_MHz (50 * MHz)
对寄存器TCNTB5的设置决定了uClinux系统时钟的频率,这里为100Hz,即Timer 5的计时周期为(1/100)秒。然后通过设置TCON寄存器,更新TCNTB5的值,并启动Timer 5定时器,使其工作在Auto Reload模式下。
setup_timer()函数最后清除中断标记,开Timer 5中断,挂接中断服务例程samsung_timer_interrupt(),并通过linux/arch/ARMnommu/kernel/irq.c中的setup_ARM_irq()函数用已经赋过初值的timer_irq结构来初始化Timer 5的中断处理数据结构。samsung_timer_interrupt()函数定义在linux/arch/ARMnommu/mach-s3c44b0/time.c中,只是简单地调用linux/kernel/timer.c中的do_timer()函数:
void samsung_timer_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
do_timer(regs);
}
答 1: 顶!
理论上,uClinux引导时并非一定需要一个独立于Kernel Image的Bootloader Image。然而,将Bootloader与Kernel分开设计能够使软件架构更加清晰,也有助于灵活地支持多种引导方式,实现一些有用的辅助功能。Bootloader的主要任务可以概括如下:
1.硬件初始化和系统引导;
2.加载uClinux Kernel Image (如果需要);
3.设置需要传递给Kernel的启动参数(如果需要);
4.调用uClinux Kernel;
5.辅助功能:从主机下载新的Image;
6.辅助功能:烧写Flash Memory;
7.辅助功能:支持功能5和6所需的人机界面,如串行终端上的命令行接口。
对于常见的几类处理器内核,现在一般都找得到现成的Bootloader可用,不过需要针对具体的Board做些移植。在实现上述功能的前提下,也可以选择自行开发。由于Bootloader Image在物理上独立于Kernel Image,因此不一定选择GNU作为开发工具。对于以ARM7TDMI为内核的S3C44B0X处理器,完全可以使用ADS来开发Bootloader。
1.硬件初始化和系统引导
完整的Bootloader引导流程可描述如下:
硬件初始化阶段一 -> 复制二级Exception Vector Table -> 初始化各种处理器模式 -> 复制RO和RW,清零ZI -> (跳转到C代码入口函数) -> 初始化Exception/Interrupt Handler Entry Table -> 初始化Device Drivers -> 硬件初始化阶段二 -> 建立人机界面
下面对上述各步骤逐一加以说明。
1.1 硬件初始化阶段一
板子上电或复位后,程序从位于地址0x0的Reset Exception Vector处开始执行,因此需要在这里放置Bootloader的第一条指令:b ResetHandler,跳转到标号为ResetHandler处进行第一阶段的硬件初始化,主要内容为:关Watchdog Timer,关中断,初始化PLL和时钟,初始化Memory Controller。比较重要的是PLL的输出频率要算正确,这里把它设置为50MHz;后面在计算SDRAM的Refresh Count和UART的Baud Rate等参数时还要用到。
1.2 复制二级Exception Vector Table
Exception Vector Table是Bootloader与uClinux Kernel发生联系的地方之一(另两处是加载and/or调用Kernel,以及向Kernel传递启动参数)。ARM7规定Exception Vector Table的基地址是0x0,所以Flash Memory的基地址也必须是0x0;而S3C44B0X处理器又不支持Memory Remap,这意味着无论运行什么样的上层软件,一旦发生中断,程序就得到Flash Memory中的Exception Vector Table里去打个转(中断Interrupt是异常Exception的一种)。对于uClinux而言,它会在RAM中建立自己的二级Exception Vector Table(后面将提到基地址被设为0x0C000000),所以在编写Bootloader时,地址0x0处的一级Exception Vector Table只需简单地包含向二级Exception Vector Table跳转的内容:
b ResetHandler ;Reset Handler
ldr pc,=0x0c000004 ;Undefined Instruction Handler
ldr pc,=0x0c000008 ;Software Interrupt Handler
ldr pc,=0x0c00000c ;Prefetch Abort Handler
ldr pc,=0x0c000010 ;Data Abort Handler
b .
ldr pc,=0x0c000018 ;IRQ Handler
ldr pc,=0x0c00001c ;FIQ Handler
LTORG
如果在Bootloader执行的全过程中都不必响应中断,那么上面的设置已能满足要求。但如果某些Bootloader功能要求使用中断(例如用Timer Interrupt实现精确定时,或利用External Interrupt支持Ethernet Driver以实现TFTP下载),那么Bootloader必须在同样的地址处配置自己的二级Exception Vector Table,以便同uClinux兼容。这张表事先存放在Flash Memory里,引导过程中由Bootloader将其复制到RAM地址0x0C000000:
存放:
RelocatedExceptionVectorStart
mov pc,#0
b HandlerUndef
b HandlerSWI
b HandlerPAbort
b HandlerDAbort
b .
b HandlerIRQ
b HandlerFIQ
HandlerUndef HANDLER HandleUndef
HandlerSWI HANDLER HandleSWI
HandlerPAbort HANDLER HandlePAbort
HandlerDAbort HANDLER HandleDAbort
HandlerIRQ HANDLER HandleIRQ
HandlerFIQ HANDLER HandleFIQ
LTORG
RelocatedExceptionVectorEnd
复制:
adr r0, RelocatedExceptionVectorStart
ldr r2, =0x0c000000
adr r3, RelocatedExceptionVectorEnd
0
cmp r0, r3
ldrcc r1, [r0], #4
strcc r1, [r2], #4
bcc %B0
其中HANDLER是一个宏,用于查找Exception Handler Routines的入口地址。这些地址存放在由HandleXXX指向的表项中,该表定位在RAM高端,基地址为_ISR_STARTADDRESS:
^ _ISR_STARTADDRESS
HandleReset # 4
HandleUndef # 4
HandleSWI # 4
HandlePAbort # 4
HandleDAbort # 4
HandleReserved # 4
HandleIRQ # 4
HandleFIQ # 4
该表的内容将在步骤1.5:“初始化Exception/Interrupt Handler Entry Table”中被填写为各Exception Handler Routine的入口地址。
1.3 初始化各种处理器模式
ARM7TDMI支持7种Operation Mode:User,FIQ,IRQ,Supervisor,Abort,System和Undefined。Bootloader需要依次切换到每种模式,初始化其程序状态寄存器(SPSR)和堆栈指针(SP)。S3C44B0X处理器在上电或复位后处于Supervisor模式,本步骤中把对Supervisor模式的初始化放在最后,也就是说Bootloader的后续部份仍将运行在Supervisor模式下。
1.4 复制RO和RW,清零ZI
对于ADS开发工具而言,一个ARM程序由RO,RW和ZI三个段组成,其中RO为代码段,RW是已初始化的全局变量,ZI是未初始化的全局变量(对于GNU工具,对应的概念是TEXT,DATA和BSS)。RO段既可以在Flash Memory中运行,也可以在RAM中运行。考虑到Bootloader中可能需要烧写Flash Memory,因此在引导阶段应当将RO和RW段复制到RAM中,并将ZI段清零。当RO段复制完毕之后,程序就可以跳转到RAM中运行。若不考虑烧写Flash Memory,则可以不复制RO段,程序始终在Flash Memory中运行。ADS使用下列Linker Symbols来记录各段的起始和结束地址:
|Image$$RO$$Base| :RO段起始地址
|Image$$RO$$Limit| :RO段结束地址加1
|Image$$RW$$Base| :RW段起始地址
|Image$$RW$$Limit| :ZI段结束地址加1
|Image$$ZI$$Base| :ZI段起始地址
|Image$$ZI$$Limit| :ZI段结束地址加1
可以在程序中引用这些标号。需要注意的是,这些标号的值是根据ARM Linker中RO Base和RW Base的设置来计算的,属于“Linker Address”或“Execution Address”,并不一定代表这些段存放在Flash Memory中的地址,在编写复制程序时需要根据具体情况作相应的计算。
1.5 初始化Exception/Interrupt Handler Entry Table
在步骤1.2里已经提到,需要在这一步中填写各Exception Handler Routine的入口地址。由于IRQ Exception为全部的中断所共用,因此必须在IRQ Exception Handler Routine中根据中断状态寄存器判断中断源,并调用相应的Interrupt Handler Routine。各Interrupt Handler Routine的入口地址也存放在上述的Exception/Interrupt Handler Entry Table中(紧接在HandleFIQ之后),需要在这一步中填写,这里就不一一列出了。
另外,S3C44B0X处理器的Interrupt Controller支持两种中断处理模式:Vectored Mode和Non-Vectored Mode,其中前者可能是Samsung特有的模式,并不被其它ARM7内核所支持。考虑到代码的可移植性,以上讨论仅针对这里所使用的Non-Vectored Mode。
1.6 初始化Device Drivers
在这一步中需要为Bootloader用到的一些关键Device Drivers建立必要的数据结构,主要包括用于精确定时的Watchdog Timer Driver和用于支持串行终端的UART Driver。
1.7 硬件初始化阶段二
继续对硬件进行初始化,主要包括:GPIO,Cache,Interrupt Controller,Watchdog Timer和UARTs。S3C44B0X处理器内置data/instruction合一的8KB Cache,且允许按地址范围设置两个Non-Cacheable区间。合理的配置是打开对RAM区间的Cache,关闭对其它地址区间(包含Flash Memory区间)的Cache。所有硬件初始化完毕之后,开中断。
在步骤1.6和1.7中,仍然遵循“必要”的原则对硬件和Device Drivers进行初始化。在目前阶段没有涉及的设备(如Ethernet Controller),可以留待使用它们之前再进行初始化。
1.8 建立人机界面
引导过程的最后一步是在串行终端上建立人机界面,并等待用户输入命令。合理的做法是先等待固定的秒数,若在此期间未接收到用户输入,则直接从Flash Memory中加载and/or调用uClinux Kernel。若接收到用户输入,则显示菜单模式或命令行模式的交互界面,等待用户进一步的命令。这里就不对此详细讨论了。
2.加载Kernel
Bootloader是否需要执行加载操作,取决于uClinux Kernel Image的类型。根据不同的配置,可以生成下面几种uClinux Kernel Image:
2.1 非压缩,非XIP
XIP(eXecute In Place)是指不对代码段重新定位,在存放代码段的位置就地运行程序。该类型的uClinux Kernel Image以非压缩格式存放在Flash Memory中,由Bootloader加载到RAM中并直接调用。
2.2 非压缩,XIP
该类型的uClinux Kernel Image以非压缩格式存放在Flash Memory中,不需加载,由Bootloader直接调用。复制init段和data段,清零bss段的工作由Kernel自行完成。
2.3 RAM自解压
压缩格式的uClinux Kernel Image都是由开头的一段自解压代码和后面的压缩数据部分组成。对于Kernel而言,由于是以压缩格式存放,因次只能以非XIP方式执行。RAM自解压类型的uClinux Kernel Image存放在Flash Memory中,由Bootloader加载到RAM中的一段临时空间,然后调用其自解压代码。可执行的uClinux Kernel被解压到最终的执行空间,然后运行。压缩格式Image所占据的临时RAM空间可在随后由uClinux回收利用。
2.4 ROM自解压
解压缩操作也可以在Flash Memory中完成。实际上,这意味着以XIP方式执行自解压代码。ROM自解压类型的uClinux Kernel Image存放在Flash Memory中,不需加载,由Bootloader直接调用其自解压代码。自解压代码自行复制其data段,清零bss段,然后将可执行的uClinux Kernel解压到最终的执行空间并运行之。与RAM自解压相比,用ROM自解压方式引导uClinux并不真正节省RAM,而且在Flash Memory中解压缩速度较慢,因此实用价值不大。
2.5 Memory Map
下面给出Bootloader和uClinux在存储和运行时的Memory Map。系统存储器由NOR Flash Memory (2MB)和SDRAM(32MB)组成,Flash Memory的地址范围从0x0到0x00200000,SDRAM的地址范围从0x0C000000到0x0E000000。
Flash Memory
0x00000000 ~ 0x00020000: 存放Bootloader
0x00020000 ~ 0x00200000: 存放所有类型的uClinux Kernel Image
运行2.2类型的uClinux Kernel
运行2.4类型的自解压代码
SDRAM
0x0C008000 ~ xxxxxxxx: 运行Bootloader
0x0C200000 ~ xxxxxxxx: 运行2.1,2.3,2.4类型uClinux Kernel
0x0C400000 ~ xxxxxxxx: 临时存放2.3类型压缩Image,并运行自解压
3.设置内核启动参数
Linux 2.4.x以后的内核都期望以标记列表(tagged list)的形式来传递启动参数。每个标记存放在一个tag结构中,每个tag结构由标识被传递参数的tag_header结构以及随后存放的参数值组成。数据结构tag、tag_header以及各种参数的数据结构都定义在Linux内核源码的头文件linux/include/asm-ARMnommu/setup.h中。
通常需要由Bootloader设置的启动参数有:ATAG_MEM、ATAG_CMDLINE、ATAG_RAMDISK、ATAG_SERIAL、ATAG_INITRD等。启动参数的标记列表以ATAG_CORE开始,以ATAG_NONE结束,代码示例如下。其中0x0C000100是内核启动参数在RAM中的基地址,指针params的类型是struct tag。宏tag_next()以指向当前标记的指针为参数,计算下一个标记的起始地址。
params = (struct tag *)0x0C000100;
params->hdr.tag = ATAG_CORE;
params->hdr.size = tag_size(tag_core);
params->u.core.flags = 0;
params->u.core.pagesize = 0;
params->u.core.rootdev = 0;
params = tag_next(params);
……
params->hdr.tag = ATAG_NONE;
params->hdr.size = 0;
在Linux内核源码的linux/arch/ARMnommu/mach-s3c44b0/arch.c中设置内核启动参数在RAM中的基地址。如果Kernel不需要从Bootloader接收启动参数,下面代码中的“BOOT_PARAMS(0x0C000100)”这一行可以不写。
MACHINE_START(S3C44B0, "44B0EVAL")
MAINTAINER("XXX YYY")
BOOT_MEM(DRAM_BASE,0x00000000,0x00000000)
BOOT_PARAMS(0x0C000100)
INITIRQ(genarch_init_irq)
MACHINE_END
uClinux Kernel处理启动参数时的代码调用关系可查阅linux/init/main.c和linux/arch/ARMnommu/kernel/setup.c:start_kernel()àsetup_arch()àparse_tags()。parse_tags()函数中调用parse_tag()函数依次处理每个标记。parse_tag()函数先判断tag_header结构中的标记类型,然后调用相应的处理函数。例如,调用parse_tag_cmdline()处理ATAG_CMDLINE标记,调用parse_tag_initrd()处理ATAG_INITRD标记,等等。对应关系如下:
static const struct tagtable core_tagtable[] __init = {
{ ATAG_CORE, parse_tag_core},
{ ATAG_MEM, parse_tag_mem32},
{ ATAG_VIDEOTEXT, parse_tag_videotext},
{ ATAG_RAMDISK, parse_tag_ramdisk},
{ ATAG_INITRD, parse_tag_initrd},
{ ATAG_SERIAL, parse_tag_serialnr},
{ ATAG_REVISION, parse_tag_revision},
{ ATAG_CMDLINE, parse_tag_cmdline}
};
对于Kernel Command Line,parse_tag_cmdline()函数将用内核参数表中的命令字符串来覆盖default_command_line[]变量。如果Kernel不从Bootloader接收启动参数,也可以有两种方法来初始化Kernel Command Line。在linux/arch/ARMnommu/kernel/setup.c中有:
static char default_command_line[COMMAND_LINE_SIZE] __initdata = CONFIG_CMDLINE;
因此一种方法是在make menuconfig时通过修改“General Setup”子菜单中的“Default kernel command string”选项来定义linux/include/linux/autoconf.h头文件中的CONFIG_CMDLINE宏,另一种方法是在linux/arch/ARMnommu/kernel/setup.c中直接把default_command_line[]写死。
4.调用Kernel
Bootloader调用uClinux Kernel的方法是直接跳转到Kernel的第一条指令处。在跳转时要满足下列条件:
CPU寄存器r0=0;
CPU寄存器r1=Machine Type ID(S3C44B0X的Machine Type ID定义在include/asm-ARM/mach-types.h中:#define MACH_TYPE_S3C44B0 178。寄存器r1也可以在Kernel启动之初的head-ARMv.S中设置);
禁止中断(IRQs和FIQs);
CPU运行在SVC模式;
MMU必须关闭(S3C44B0X没有MMU);
指令Cache可以打开也可以关闭,数据Cache必须关闭(S3C44B0X的Cache是指令与数据合一的,因此只能选择关闭)。
C代码调用Kernel的示例如下,其中r0和r1的值通过参数传递:
void (*CallKernel)(int zero, int mach) = (void (*)(int, int))KERNEL_ADDR;
CallKernel(0, 178);
5.辅助功能
完整的Bootloader还应该支持从主机下载文件到目标板的RAM;用RAM中的数据烧写Flash Memory;以及上述功能所需的人机交互接口。文件下载途径视目标板所提供的物理通讯接口而定,比较简单的方法一般是通过串口,用Xmodem或Ymodem协议下载,但速度较慢。目标板上只需要实现协议的接收部份,主机上可以用HyperTerminal等工具来发送文件。如果目标板提供Ethernet等快速接口,也可以移植一个简单的TCP/IP栈,用TFTP等标准文件传输协议下载。目前Flash Memory一般都是用NOR Flash,烧写是非常简单的;需要注意的是对于多数Flash芯片,在Erase/Program之前需要先Unprotect。人机交互接口不难在串行终端上实现,这里就不赘述了。
二. uClinux Kernel的调试
在移植uClinux的过程中,较为困难的是如何发现并解决Kernel启动阶段的问题。本节的内容主要取自网上的一篇文档《用AXD + Multi-ICE调试uClinux内核》,说明如何使用ADS和Multi-ICE工具对Kernel进行源代码级的调试。
首先是设置ARM-elf-gcc编译器,使其能够输出dwarf-2格式的调试信息。修改linux/Makefile,将CFLAGS_KERNEL设置为-gdwarf-2,然后重新编译uClinux Kernel:
CFLAGS_KERNEL = -gdwarf-2
把以下文件复制到Windows下:image.ram,System.map,linux以及一份编译时所用的内核源码。image.ram是上面提到过的非压缩/非XIP模式的Kernel Image;System.map提供了各symbol所对应的地址;linux是一个ELF格式的文件,包含有AXD Debugger所需的调试信息。
开发板上电,启动Multi-ICE Server,启动AXD Debugger。使用FileLoad Memory From File读入image.ram,其中基地址应设为Kernel的TEXTADDR,这里是0x0C200000。然后使用FileLoad Debug Symbols读入linux文件。把PC寄存器设置为image.ram的起始地址0x0C200000,此时在AXD Debugger的Disassembly窗口中已能看到一些符号的名称。
在AXD Debugger中,Processor ViewsSource,选择要调试的源码文件。例如,选择linux/init/main.c,在Kernel的入口函数start_kernel()中设置断点。也可以在System.map中查找到start_kernel的地址,然后在Disassembly窗口中跳到该地址并设置断点。最后Go,就可以从断点处开始调试了。
三. uClinux Kernel的移植
1. 相关的目录结构
Machine-Specific源代码: linux/arch/ARMnommu/mach-s3c44b0
Machine-Specific头文件: linux/include/asm-ARMnommu/arch-s3c44b0
通用的设备驱动程序: linux/drivers/block
linux/drivers/char
linux/drivers/mtd
linux/drivers/net
文件系统: linux/fs
网络协议栈: linux/net
其它linux/arch/ARMnommu 子目录下的Architecture-Specific源代码:
kernel: 核心内核代码;
mm: 内存管理代码;
lib: ARM-Specific或经过优化的内部库函数代码;
nwfpe/fastfpe: 浮点库的两种实现;
boot: 用于生成压缩内核镜像的代码;
tools: 用于自动生成头文件的配置脚本;
def-configs: 各Machine-Specific的缺省配置文件。
其它linux/include/asm-ARMnommu子目录下的Architecture-Specific头文件:
hardware: ARM-Specific芯片或设备的头文件;
mach: 多数Machine共有的一般性接口定义(如DMA/IRQ/PCI);
proc-ARMo: 26-bit版本的ARM处理器相关头文件;
proc-ARMv: 32-bit版本的ARM处理器相关头文件。
2. 相关的源码文件和移植要点
现在结合源码文件,对一些移植过程中的要点进行讨论,目的是得到能够在Samsung S3C44B0X开发板上运行的非压缩,非XIP模式的uClinux Kernel Image。该Image由Bootloader加载并调用。
2.1 内核配置系统
Linux的内核配置系统由Makefile,配置脚本(config.in)和配置工具组成。当用户Make config,Make menuconfig或Make xconfig时,相应的配置工具按照配置脚本config.in的内容显示可用的配置选项。用户完成配置并存盘退出时,配置信息保存在配置文件.config中,原有的.config文件被更名为.config.old。Makefile根据.config中的配置信息,构造出需要编译的源文件列表,然后分别编译;并根据Makefile中指定的链接器脚本,把目标代码链接到一起,最终形成Linux Kernel Image。
在配置脚本linux/arch/ARMnommu/config.in中,应给出可供用户选择S3C44B0X开发板的选项,并定义基于该处理器的开发板的一些重要参数,如存储器空间等:
comment 'System Type'
choice 'ARM system type' …… ……
S3C44B0 CONFIG_ARCH_S3C44B0 …… ……
if [ "$CONFIG_ARCH_S3C44B0" = "y" ]; then
define_bool CONFIG_NO_PGT_CACHE y
define_bool CONFIG_CPU_32 y
define_bool CONFIG_CPU_26 n
define_bool CONFIG_CPU_ARM710 y
define_bool CONFIG_CPU_WITH_CACHE y
define_bool CONFIG_CPU_WITH_MCR_INSTRUCTION n
define_bool CONFIG_SERIAL_S3C44B0 y
define_hex DRAM_BASE 0x0C000000
define_hex DRAM_SIZE 0x02000000
define_hex FLASH_MEM_BASE 0x00000000
define_hex FLASH_SIZE 0x00200000
fi
在针对ARM Architecture (NO MMU)的linux/arch/ARMnommu/Makefile中,应为S3C44B0X开发板定义处理器类型(26-bit或32-bit),Machine名称和代码段基地址,并指定链接器脚本为linux/arch/ARMnommu/vmlinux-ARMv.lds.in:
LINKFLAGS := -p -X -T arch/ARMnommu/vmlinux.lds
……
ifeq ($(CONFIG_CPU_32),y)
PROCESSOR = ARMv
endif
……
ifeq ($(CONFIG_ARCH_S3C44B0), y)
TEXTADDR = 0x0C200000
MACHINE = s3c44b0
endif
……
arch/ARMnommu/vmlinux.lds: arch/ARMnommu/vmlinux-$(PROCESSOR).lds.in dummy
2.2 内核启动入口
在linux/Makefile中可以找到生成uClinux Kernel Image的规则为:
ifdef CONFIG_UCLINUX
LINUX=linux
endif
……
$(LINUX): $(CONFIGURATION) init/main.o init/version.o init/do_mounts.o linuxsubdirs
$(LD) $(LINKFLAGS) $(HEAD) init/main.o init/version.o init/do_mounts.o --start-group $(CORE_FILES) $(DRIVERS) $(NETWORKS) $(LIBS) --end-group -o $(LINUX)
可见linux是由HEAD、main.o、version.o、do_mounts.o、CORE_FILES、DRIVERS、NETWORKS和LIBS组成的。这些变量定义了用于链接生成linux的目标文件和库文件列表。其中HEAD在linux/arch/ARMnommu/Makefile中定义,用来确定最先被链接进linux的文件:
HEAD := arch/ARMnommu/kernel/head-$(PROCESSOR).o arch/ARMnommu/kernel/init_task.o
因此,可以确定入口代码是linux/arch/ARMnommu/kernel/head-ARMv.S。在前面介绍Bootloader时曾经提到,uClinux Kernel在启动时要求把Machine Type ID存放在寄存器r1中。如果Bootloader在调用Kernel时没有以参数传递的方式设置r1,则必须在head-ARMv.S中先对r1进行初始化:
……
#elif defined(CONFIG_ARCH_S3C44B0)
mov r1, #MACH_TYPE_S3C44B0
……
#endif
其中,宏MACH_TYPE_S3C44B0定义在头文件linux/include/asm-ARMnommu/mach-types.h中:
#define MACH_TYPE_S3C44B0 178
……
#ifdef CONFIG_ARCH_S3C44B0
#ifdef machine_arch_type
#undef machine_arch_type
#define machine_arch_type __machine_arch_type
#else
#define machine_arch_type MACH_TYPE_S3C44B0
#endif
#define machine_is_s3c44b0() (machine_arch_type==MACH_TYPE_S3C44B0)
#else
#define machine_is_s3c44b0() (0)
#endif
该头文件是由脚本linux/arch/ARMnommu/tools/gen-mach-types根据linux/arch/ARMnommu/tools/mach-types文件中的对应信息自动生成的:
#machine_is_xxx CONFIG_xxxx MACH_TYPE_xxx number
…… …… …… ……
s3c44b0 ARCH_S3C44B0 S3C44B0 178
在head-ARMv.S中,接下来就可以初始化Processor ID和Machine Type ID,清除BSS段,并跳转到C代码的入口函数start_kernel()了:
#if defined(CONFIG_ARCH_S3C44B0)
adr r5, LC0
ldmia r5, {r5, r6, r8, r9, sp}
/* clear BSS */
mov r4, #0
1: cmp r5, r8
strcc r4, [r5], #4
bcc 1b
ldr r2, S3C44B0_PROCESSOR_TYPE
str r2, [r6]
mov r2, #MACH_TYPE_S3C44B0
str r2, [r9]
/* call start_kernel() */
mov fp, #0
b start_kernel
LC0: .long __bss_start @ r5
.long processor_id @ r6
.long _end @ r8
.long __machine_arch_type @ r9
.long init_task_union+8192 @ sp
S3C44B0_PROCESSOR_TYPE:
.long 0x36366036
#endif
其中标号__bss_start和_end分别代表了BSS段的起始地址和结束地址,它们都定义在链接器脚本linux/arch/ARMnommu/vmlinux-ARMv.lds.in中。而全局变量processor_id和__machine_arch_type则是定义在linux/arch/ARMnommu/kernel/setup.c中。
2.3 异常处理和中断处理
uClinux Kernel的C代码入口函数start_kernel()定义在linux/init/main.c中。该函数中,有关调用setup_arch()处理内核启动参数等内容在前面已有提及。此外,较为重要的还有对异常和中断的处理:
asmlinkage void __init start_kernel(void)
{
……
trap_init();
init_IRQ();
……
}
linux/arch/ARMnommu/kernel/traps.c中的trap_init()函数调用linux/arch/ARMnommu/kernel/entry-ARMv.S中的__trap_init()函数,把二级Exception Vector Table设置到基地址vectors_base()处:
void __init trap_init(void)
{
……
extern void __trap_init(void *);
__trap_init((void *)vectors_base());
……
}
其中宏vectors_base()被设置为系统中SDRAM的起始地址0x0C000000,定义在linux/include/asm-ARMnommu/proc-ARMv/system.h中:
#ifdef CONFIG_ARCH_S3C44B0
#define vectors_base() (0x0C000000)
#endif
__trap_init()函数首先从标号.LCvectors处复制二级Exception Vector Table,其中参数vectors_base()通过寄存器r0传递:
.equ __real_stubs_start, .LCvectors + 0x200
.LCvectors:
swi SYS_ERROR0
b __real_stubs_start + (vector_undefinstr - __stubs_start)
ldr pc, __real_stubs_start + (.LCvswi - __stubs_start)
b __real_stubs_start + (vector_prefetch - __stubs_start)
b __real_stubs_start + (vector_data - __stubs_start)
b __real_stubs_start + (vector_addrexcptn - __stubs_start)
b __real_stubs_start + (vector_IRQ - __stubs_start)
b __real_stubs_start + (vector_FIQ - __stubs_start)
…… …… ……
adr r1, .LCvectors
ldmia r1, {r2, r3, r4, r5, r6, r7, r8, r9}
stmia r0, {r2, r3, r4, r5, r6, r7, r8, r9}
然后__trap_init()函数把各Exception Handler Routine的代码从__stubs_start复制到vectors_base()+0x200,也就是0x0C000200处,以便与上述二级Exception Vector Table中的跳转指令相匹配。原先存放该段代码的地址区间由__stubs_start和__stubs_end标记:
add r2, r0, #0x200
adr r0, __stubs_start
adr r1, __stubs_end
1: ldr r3, [r0], #4
str r3, [r2], #4
cmp r0, r1
blt 1b
中断的初始化函数init_IRQ()定义在linux/arch/ARMnommu/kernel/irq.c中:
struct irqdesc irq_desc[NR_IRQS];
void (*init_arch_irq)(void) __initdata = NULL;
……
void __init init_IRQ(void)
{
……
for (irq = 0; irq < NR_IRQS; irq++) {
irq_desc[irq].probe_ok = 0;
irq_desc[irq].valid = 0;
irq_desc[irq].noautoenable = 0;
irq_desc[irq].mask_ack = dummy_mask_unmask_irq;
irq_desc[irq].mask = dummy_mask_unmask_irq;
irq_desc[irq].unmask = dummy_mask_unmask_irq;
}
init_arch_irq();
……
}
irq_desc[]数组用于存放IRQ请求描述符。每个中断号对应一个irq_desc结构,NR_IRQS代表中断总数。S3C44B0X开发板共有26个中断,中断号和NR_IRQS都定义在linux/include/asm-ARMnommu/arch-s3c44b0/irqs.h中。函数指针init_arch_irq()初始时为空,在前面提到的linux/arch/ARMnommu/kernel/setup.c里的setup_arch()函数中初始化:
struct machine_desc *mdesc;
……
mdesc = setup_architecture(machine_arch_type);
……
init_arch_irq = mdesc->init_irq;
其中结构类型machine_desc定义在linux/include/asm-ARMnommu/mach/arch.h中,而对应的结构体定义在前面提到过的linux/arch/ARMnommu/mach-s3c44b0/arch.c中:
MACHINE_START(S3C44B0, "44B0EVAL")
MAINTAINER("XXX YYY")
BOOT_MEM(DRAM_BASE,0x00000000,0x00000000)
BOOT_PARAMS(0x0C000100)
INITIRQ(genarch_init_irq)
MACHINE_END
setup_architecture()函数根据Machine Type ID在.arch.info段中搜索与之匹配的machine_desc结构,故mdesc->init_irq实际上将指向genarch_init_irq()函数。该函数定义在linux/arch/ARMnommu/kernel/irq-arch.c中:
void __init genarch_init_irq(void)
{
irq_init_irq();
}
而真正对irq_desc[]数组进行初始化的irq_init_irq()函数定义在linux/include/asm-ARMnommu/arch-s3c44b0/irq.h中,该头文件被linux/arch/ARMnommu/mach-s3c44b0/irq.c所包含:#include <asm/mach/irq.h>
在s3c44b0x_int_init()函数中将初始化S3C44B0X中断控制器的各寄存器。s3c44b0x_int_init()、s3c4510b_mask_ack_irq()、s3c4510b_mask_irq()和s3c4510b_unmask_irq()等函数也都定义在linux/arch/ARMnommu/mach-s3c44b0/irq.c中。
static __inline__ void irq_init_irq(void)
{
……
s3c4510b_int_init();
……
for (irq = 0; irq < NR_IRQS; irq++) {
irq_desc[irq].valid = 1;
irq_desc[irq].probe_ok = 1;
irq_desc[irq].mask_ack = s3c4510b_mask_ack_irq;
irq_desc[irq].mask = s3c4510b_mask_irq;
irq_desc[irq].unmask = s3c4510b_unmask_irq;
}
}
2.4 系统时钟
在linux/init/main.c有对系统时钟的初始化:
asmlinkage void __init start_kernel(void)
{
……
time_init();
……
}
函数time_init()定义在linux/arch/ARMnommu/kernel/time.c中:
#include <asm/arch/time.h>
void __init time_init(void)
{
……
setup_timer();
……
}
函数setup_timer()属于architecture-specific代码,可以在头文件linux/include/asm-ARMnommu/arch-s3c44b0/time.h里实现:
extern struct irqaction timer_irq;
extern void samsung_timer_interrupt(int, void *, struct pt_regs *);
void __inline__ setup_timer (void)
{
rTCON &= 0xf0ffffff;
rTCFG0 &= 0xff00ffff;
rTCFG0 |= (16-1)<<16;
rTCFG1 &= 0xff0fffff;
rTCFG1 |= 0<<20;
rTCNTB5 = fMCLK_MHz/(100*16*2);
rTCON |= 0x02000000;
rTCON &= 0xf0ffffff;
rTCON |= 0x05000000;
CLEAR_PEND_INT(IRQ_TIMER);
INT_ENABLE(IRQ_TIMER);
timer_irq.handler = samsung_timer_interrupt;
setup_ARM_irq(IRQ_TIMER, &timer_irq);
}
该函数首先初始化S3C44B0X PWM Timer 5,作为系统时钟源。Timer 5的输入时钟频率由以下公式计算:
Timer Input Clock Frequency = fMCLK_MHz / (Prescaler Value + 1) / (Divider Value)
其中Prescaler Value和Divider Value在TCFG0和TCFG1寄存器中设置。fMCLK_MHz通常等于PLL的输出频率,定义在linux/include/asm-ARMnommu/arch-s3c44b0/hardware.h中。PLL是在Bootloader里初始化的,该频率值应与当初的设置相匹配,假设为50MHz:
#define MHz 1000000
#define fMCLK_MHz (50 * MHz)
对寄存器TCNTB5的设置决定了uClinux系统时钟的频率,这里为100Hz,即Timer 5的计时周期为(1/100)秒。然后通过设置TCON寄存器,更新TCNTB5的值,并启动Timer 5定时器,使其工作在Auto Reload模式下。
setup_timer()函数最后清除中断标记,开Timer 5中断,挂接中断服务例程samsung_timer_interrupt(),并通过linux/arch/ARMnommu/kernel/irq.c中的setup_ARM_irq()函数用已经赋过初值的timer_irq结构来初始化Timer 5的中断处理数据结构。samsung_timer_interrupt()函数定义在linux/arch/ARMnommu/mach-s3c44b0/time.c中,只是简单地调用linux/kernel/timer.c中的do_timer()函数:
void samsung_timer_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
do_timer(regs);
}
答 1: 顶!
共3条
1/1 1 跳转至页
回复
有奖活动 | |
---|---|
【有奖活动】分享技术经验,兑换京东卡 | |
话不多说,快进群! | |
请大声喊出:我要开发板! | |
【有奖活动】EEPW网站征稿正在进行时,欢迎踊跃投稿啦 | |
奖!发布技术笔记,技术评测贴换取您心仪的礼品 | |
打赏了!打赏了!打赏了! |
打赏帖 | |
---|---|
vscode+cmake搭建雅特力AT32L021开发环境被打赏30分 | |
【换取逻辑分析仪】自制底板并驱动ArduinoNanoRP2040ConnectLCD扩展板被打赏47分 | |
【分享评测,赢取加热台】RISC-V GCC 内嵌汇编使用被打赏38分 | |
【换取逻辑分析仪】-基于ADI单片机MAX78000的简易MP3音乐播放器被打赏48分 | |
我想要一部加热台+树莓派PICO驱动AHT10被打赏38分 | |
【换取逻辑分析仪】-硬件SPI驱动OLED屏幕被打赏36分 | |
换逻辑分析仪+上下拉与多路选择器被打赏29分 | |
Let'sdo第3期任务合集被打赏50分 | |
换逻辑分析仪+Verilog三态门被打赏27分 | |
换逻辑分析仪+Verilog多输出门被打赏24分 |