先说明一下在加电之前的这个软硬件情况,这个三星公司根据ARM920T软核生产的这个s3c2410集成了64M的sdram和64M的nandflash存储器。Vivi和UCOS都存储在这个nandflash中,因为nandflash断电后不会丢失信息。这个VIVI是三星公公司为ARM系列芯片书写的bootloader,用于开发阶段,做系统的引导程序。
VIVI存放在flash 0x00000000地址开始的地方,UCOS存放在flash 0x03f30000地址开始的地方。ARM920T开机从flash启动,启动时把flash前4K (即vivi的前4K)COPY到SDRAM(这种启动方式是利用Nandflash启动,COPY前4K到sdram中是硬件自动实现的),vivi的前4K 代码中有用于COPY剩余VIVI的代码。执行完这些代码之后,VIVI就控制了FLASH的读取,串口的控制以及用户shell接口,当然它还有其他一些功能。当用户执行bootucos命令时,VIVI会把ucos相关代码从flash 0x03f30000 COPY到SDRAM 0x30008000的地方。当然也可以设置VIVI自动引导ucos执行。当代码copy完毕后,vivi会把PC值改成0x30008000去执行。
我们先说一下为什么我们非要说具体的那个地址那,咱们前面说了,编译好的程序有一个load地址,一个真正运行的地址,0x30008000这个地址就是咱们说的程序的装载地址,这个地址是我们用编译器指定的地址,也就是通过在ads工程里后缀名是scf的文件配置的。在这个文件里我们配置了程序的装载地址和程序运行的地址,我们为什么要指定这两个地址那?我们整个工程的程序是最后链接时一次性固定的绝对地址,也就是说最终链接出来的程序地址和真正运行的地址是一致的。只不过我们一般不会把这些代码直接放到相应的部位去罢了,其中一个原因就是,我们为了在不加电时保存程序会把程序放到非易失的存储设备里去,而我们运行时会把程序copy到运行速度比较快的sdram中去。也就是说,本来这些静态链接的程序的执行地址都是固定的了,我们要在这些程序运行之前要把这些程序放对位置。我们必须知道我们的程序装载到什么地址和真正在什么地址运行。这样我们才能知道那些装载地址和运行地址不一样的程序段应该怎么搬运。至于搬运的工作,你可以自己手工实现,也可以用ADS提供的库函数实现。
跳转到这个0x30008000去执行这个地址处的指令,我们这个工程编译出来后谁是第一条指令那?我们平时写的程序都是从main()函数开始执行,但我们这个嵌入式的开发可不是哦,在分析完启动代码后你就知道了,在执行的所谓的main()函数之前要做很多工作的。
arm映像文件的入口点有两种类型:一种是映像文件运行时的入口点,称为初始入口点(initial entry point),另一种是普通入口点(entry point).
初始入口点是映像文件运行时的入口点,每个映像文件只有一个唯一的初始入口点,它保存在ELF头文件中。假如映像文件是被操作系统加载的,操作系统是通过跳转到该初始入口点处来加载该映像文件。
普通的入口点是在汇编中用ENTRY伪操作定义。他通常用于标志该段代码是通过异常中断处理程序进入的。这样连接器删除无用的段时不会将该段代码删除。一个映像文件中可以定义多个普通入口点。
应该注重的是,初始入口点可以使普通入口点,但也可以不是普通入口点.
初始入口点必须满足下面两个条件:
1.初始入口点必须位于映像文件的运行时域内。
1.1饱含初始入口点的运行时域不能被覆盖,他的加载地址和运行地址必须是相同的。
可以使用连接选项-entry address来指定映像文件的初始入口点。这时,address指定了映像文件的初始入口点的地址值。对于地址0x0处为rom的嵌入式应用系统,可以使用-entry 0x0来指定映像文件的初始入口点。这样当系统复位后,自动跳转到该入口开始执行。假如映像文件是被一个加载器加载的,该映像文件该映像文件必须包含一个初始化入口点。这种映像文件通常还包含了其他普通入口点,这些普通入口点一般为异常中断处理程序的入口地址。
当用户没有指定-entry address时,连接器根据下面的规则决定映像文件的初始入口点。
假如输入的目标文件中只有一个普通入口点,该普通入口点被连接器当成映像文件的初始入口点。
假如输入的目标文件中没有一个普通入口点,或者其中的普通入口点多于一个,则连接器生成的映像文。
我们编译好的可执行文件时去除了头格式的映像文件,我们讲的本来就是操作系统,所以这个程序不是通过初始入口点执行的第一条指令,应该是通过普通入口点来执行的,通常是中断向量表。也就是程序中用伪指令entry指定的指令段的第一条指令。我们用ADS1.2打开ucos的学习资料的工程中的第十个实验(ucos系统移植实验)。在startup文件夹中有一个startup.s 的汇编程序,这个就是ucos的启动代码了。由ENTRY伪指令指定的第一条指令是b ColdReset,所以第一条指令就是它了。
咱先不管这个第一条指令的问题,我的目的是把我学习的UCOS讲述给你听,但这需要一定的讲述规范,希望我说的你能听懂,愿意看下去,我想这样做:
先从整体描述一下整个过程,然后在分阶段概括这一阶段整个硬件和软件系统干了什么?为什么会有这些顺序?为什么要这么干?在这个过程中可能思维随即发散到任何有关系的知识点。最后我将逐一分析源代码,在分析源代码时遇到的问题,都将解决,当然包括那些精华和美。还可能阐述一下我的理解和方法,以及我对学习的一些认识。我是想按照一定的规范去写这个东东,但是我又不想完全按照一种思路去写,毕竟我是随意书写的。我的整体思路就是针对硬件和软件在整个时间流里都干了什么?为什么要这么干为主要线路。在这个线路中涉及到的所有疑问和知识点都将一一展开阐述。我尽量做到自然,而不是强加给你一些生硬的概念,因为人不喜欢被。被学习,被干活,被记忆。
理解UCOS最好的方式是阅读其源代码,一本很好的参考书是嵌入式实时操作系统ucos-ii,邵贝贝译
声明:在写这个文档时,我还有很多地方没有真正弄明白,所以有些地方可能我也说不清楚,但我会把我的疑问写出来,我什么时候想明白了,我会把它写出来,如果你知道请你告诉我,我会很高兴的。
在说明一下现在的情况:现在ucos的所有代码(包括启动的bootloader)都被vivi copy到0x30008000的内存地址开始的地方了,然后PC值改为0x30008000,取出这个地址放的arm指令就开始执行这条指令了。前面已经分析完整个工程编译出来的可执行程序的第一条指令了。
好了,下面开始说整个班子以及UCOS的整体启动过程,只是大概的说明流程,至于会为什么这样的问题等到具体详解的时候在具体解释。
硬件初始化,主要是让硬件平台处于一个可知的状态,重要的一点就是初始化C语言运行环境。
UCOS初始化
UCOS运行并执行应用程序
哎 ,这样看的话,整个过程还真挺简单的,哈。
下面具体讲解硬件初始化阶段,这个就真的比较麻烦了,但没关系,咱们慢慢说。
从具体代码上看,它主要干了这些活:
关闭看门狗,(一个用于开发阶段的硬件,到代码讲解时具体说明)
屏蔽中断掩码寄存器(现在整个硬件平台的控制权都在UCOS,在初始化的时候,我们不希望被打扰,具体原因我们以后说)
初始化各个模式堆栈空间(堆栈空间很重要哦)
COPY中断向量表(关于为什么要copy,我们在后面说)
初始化c库环境
然后跳转到主应用程序(即我们平时说的main()函数)
下面这些代码是用汇编写的代码,其中分号后面的是注释。
下面就以具体代码为例,详细讲解启动代码。
每个代码块做一个说明,对于特别重要的代码,我在代码后面做详细注释。注释写在//后面,如果此处有很重要知识点的话,单独起一段进行说明。
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;
;;; Copyright (c) 2004-2007 threewater@up-tech.com, All rights reserved.
;;;
;;; Startup Code for
;;; S3C2410 : Startup.s
;;;; by threewater 2005.2.22
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
GET 2410addr.s //引入2410addr.s文件里的内容,作用像是c语言里的#include一样。
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Some ARM920 CPSR bit discriptions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;Pre-defined constants
USERMODE EQU 0x10
FIQMODE EQU 0x11
IRQMODE EQU 0x12
SVCMODE EQU 0x13
ABORTMODE EQU 0x17
UNDEFMODE EQU 0x1b
MODEMASK EQU 0x1f
NOINT EQU 0xc0
I_Bit * 0x80
F_Bit * 0x40
//以上代码是定义一些常量,都是关于arm的CPSR寄存器的,我们对这方面的知识是知道的,不知道的请参考datasheet的program model 章节相关内容。书写的代码是用于程序员之间交流用的,所以要尽量的可读,不要使用数字。使用数字的时候像上面一样,把他们定义成另一个可读的符号,这样就能读名知意了。
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Start here
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
AREA Init,CODE,READONLY // 定义一个段,并指明属性,这些都是给编译器说的。具体怎么回事请参考编译器相关知识部分。
IMPORT __use_no_semihosting_swi //禁止使用semihosting机制,现在说一下什么是semihosting机制,这是ADS提供的一种开发调试机制,就是说你的程序运行在目标平台上,你可以利用主机的硬件资源进行对程序的调试,比如你可以利用宿主机的屏幕和键盘。Semihosted环境(semihosting是针对ARM目标机的一种机制,它能够根据应用程序代码的输入/输出请求,与运行有调试功能的主机通讯。这种技术允许主机为通常没有输入和输出功能的目标硬件提供主机资源)在semlhosted环境下用来实现C库函数与目标相关的函数。可以在你的Application Code中使用printf等stand IO Function in C Library! 方便调试!更多的你可以参考ARM DUI 0058D(Debug Target Guide!)ARM公司对Semihosting的中文解释是半主机机制。为什么叫半主机呢?主要是指应用程序的代码运行在目标系统上,当需要类似PC平台下的控制台输入输出时,会调用Semihosting去利用PC上的控制台输入输出设备:如打开关闭文件,PC显示器输出,键盘输入等等。更详细的内容请参见Semihosting.pdf
而此处,我们做的系统是独自运行在硬件平台上的,即不需要使用Semihosting机制。IMPORT __use_no_semihosting_swi就是声明我们不使用这种机制,如果程序里有使用这种机制的地方编译器就会报错。在c语言中我们用#pragma import(__use_no_semihosting_swi)这句话禁用。
IMPORT Enter_UNDEF
IMPORT Enter_SWI
IMPORT Enter_PABORT
IMPORT Enter_DABORT
IMPORT Enter_FIQ //以上就是引用全局标号,就是说明这些标号在其他地方已经声明,而且是全局的。在汇编语言里声明全局变量用关键字EXPORT,在C语言中用extern ,当然c语言中的函数本身就是全局的标号。
;IMPORT main
ENTRY //指定程序普通入口点,以下代码都是一些跳转指令,ENTRY伪指令就是告诉编译器以下这些代码是有用的,不要优化掉。编译器可能认为这是一堆没有的代码。
b ColdReset //第一条是跳转指令,跳转到ColdReset处去执行
b Enter_UNDEF ;UndefinedInstruction
b Enter_SWI ;syscall_handler or SWI
b Enter_PABORT ;PrefetchAbort
b Enter_DABORT ;DataAbort
b . ;ReservedHandler
b IRQ_Handler ;IRQHandler
b Enter_FIQ ;FIQHandler
;deal with IRQ interrupt
EXPORT IRQ_Handler
IRQ_Handler
IMPORT ISR_IrqHandler
;SUBS LR, LR, #4 ;事先修正返回地址
STMFD sp!, {r0-r12, lr}
BL ISR_IrqHandler
;mrs r10, SPSR ; get the PSR
;MSR CPSR_cxsf,r10 ;恢复cpsr
; LDMFD sp!, {r0-r12,pc}^
;bic r10,r10,#0xc0 ;开中断
LDMFD sp!, {r0-r12,lr}
;mrs r10, spsr ; get the PSR
; MSR CPSR_cxsf,r10 ;恢复cpsr
;mov pc,lr
SUBS pc, lr, #4 ;s表示对cpsr有影响
;mrs r10, SPSR ; get the PSR
;MSR CPSR_cxsf,r10 ;恢复cpsr
;=======
; ENTRY
;=======
EXPORT ColdReset
ColdReset
ldr r0,=WTCON ;watch dog disable
ldr r1,=0x0
str r1,[r0] //关闭看门狗,有关看门狗的相关知识查看datasheet的第十八章watchdog
ldr r0,=INTMSK
ldr r1,=0xffffffff ;all interrupt disable
str r1,[r0]
ldr r0,=INTSUBMSK
ldr r1,=0x7ff ;all sub interrupt disable, 2002/04/10
str r1,[r0]
//以上代码用于屏蔽所有外部中断,用的方法是屏蔽外部中断掩码寄存器。具体原理查看datasheet的第十四章interrupt controllor或者查看前面写的基础知识篇之ARM芯片的相关知识。
;****************************************************
;* Initialize stacks *
;****************************************************
bl InitStacks ; Stack Setup for each MODE //跳转指令,跳转到InitStacks去执行
现在已经初始化好了各个模式下的系统堆栈空间了,总结一下现在处理器的情况:处理器现处于SVC模式,并且cpsr的irq和fiq位禁止中断。还有看门狗被屏蔽(整个系统不用看门狗),外部中断的中断控制器的屏蔽寄存器被屏蔽。也就是说现在外部中断的申请不能到cpu,就算外部的请求到了cpu,cpu也不会搭理,因为irq和fiq位都被屏蔽。所谓双重保障就是这么回事。
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;; copy excption table to sram at 0x0
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
IMPORT |Load$$EXCEPTION_EXEC$$Base| //装载时的地址
IMPORT |Image$$EXCEPTION_EXEC$$Base| //运行时的地址
IMPORT |Image$$EXCEPTION_EXEC$$Length|//exception段的长度
ldr r0, =|Load$$EXCEPTION_EXEC$$Base| ;source data
ldr r1, =|Image$$EXCEPTION_EXEC$$Base| ;place exception talbe at 0x0
ldr r2, =|Image$$EXCEPTION_EXEC$$Length|
exception_cploop
sub r2, r2, #4 //把r2(即exception的长度值减去4,因为arm的通用寄存器是32位的,即每次移动4个字节)
ldmia r0!, {r3} //借助把源地址处的前四个字节放到r3中去,然后r0的地址自动加4,这是!的作用。具体参考arm指令集。
stmia r1!, {r3} //利用r3装到目的地址
cmp r2, #0 //判断是否移动完毕,未移动完毕,则继续移动
bge exception_cploop
//以上代码是copy中断向量表,这里的中断向量表是工程中exception.s里定义的中断向量,不是startup.s中开始的那几个跳转指令。至于为什么要copy中断向量表呢,原因是这样的,咱们现在的ucos在sdram地址0x30008000的地方,我们还知道ucos要管理整个硬件,当然要管理中断,ARM的中断向量表规定要在内存地址的0开始的地方,而现在sdram 的0地址处是vivi的代码,我想一定也是一个中断向量表(不过他是vivi的),现在我们要把ucos自己的中断向量表copy到sdram的0地址处。一,这样就为ucos管理中断提供了前提条件,二,这也废除了vivi的中断机制,所谓一箭双雕。
对了,还有一个事要说,和那个叫分散加载文件有关的,具体分散加载文件的作用和使用请参见ARM开发工具ADS原理与应用.pdf第十章的相关章节。简单说一下,这个分散加载文件是用于告诉编译器怎么链接输入的文件,以及连接成什么样子,把链接的文件装载到什么地方,真正运行时在什么地方。而这个从装载地址到运行地址的一个转化可以自己用代码实现,也可以利用ADS提供的功能来实现,那就是下面的__main()函数可以帮助你做这个工作。__main()这个是ads提供的库函数,是根据你写的分散加载文件自动生成搬运代码的。
至于这个__main()函数的具体使命,我们下面再说。
下面我们就看一下这个分散加载文件的内容(我们用的是scat_ram.scf这个文件):
LOAD 0x30008000 ;load region
{
RAM_EXEC +0 ;PC
{
startup.o (init, +FIRST)
* (+RO)
}
STACKS +0x100000 UNINIT ;64KByte under L0 pagetable
{
stack.o (+ZI)
}
RAM +0
{
* (+RW,+ZI)
}
HEAP +0 UNINIT
{
heap.o (+ZI)
}
EXCEPTION_EXEC 0 OVERLAY ;exception region
{
exception.o (+RO)
}
}
关于这个分散加载文件的语法和具体含义,我就不一一解说了,这是一个格式文件,资料里有它的规范,看懂它很容易的。分析完我们发现这样一个问题,exception.o(即中断向量表这一段)其加载地址和运行地址是不同的,按理说应该由__main函数自动生成代码,因为我们下面用到了__main函数,但EXCEPTION_EXEC这个执行域的属性是OVERLAY,这个属性的代码段__main函数是不管的喽,只能自己去实现这些搬运代码的功能。我们现在就回过头去看搬运代码是怎么实现搬运的。
首先,引入了几个变量,这几个变量是编译器提供的,也就是你通过那个叫分散加载文件指定的装载地址和运行地址。本来就是,你想搬家,你得有原来的家的地址和目的地址吧。具体过程参考我在程序后写的注释。
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;; start main function in C language
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
IMPORT __main
BL __main ;Don't use main() because ......
//跳转到__main()函数执行,看好了,这里是__main()哦,并不是平常我们所说的main()函数,为什么呢,下面说明原因:
__main()函数是ads1.2开发环境提供的库函数,这个库函数主要的作用有以下几点:
1,根据你写的分散加载文件来生成自动搬运的代码
2,负责库函数的初始化
如果你用的存储结构比较复杂,那__main()函数提供的这个功能就很给力了,不用自己去实现搬运代码的工作了。
另外__main()函数还初始化库函数,这个也很重要,库函数初始化好了后,在你编程时就可以使用标准的c库函数进行编程了,比如printf()函数等,当然了,要在固定硬件平台上实现这些库函数,还要进行库函数的移植工作。关于库的移植你可以参考《使用ARM标准C库进行嵌入式应用程序开发.doc》。关于__main()函数的具体细节问题请参考《main __main.doc》。
关于__main()的问题,其中有一个库的初始化函数_rt_lib_init()到底做了什么初始化,不是很清楚,用AXD调试,都是汇编,又没看明白什么意思,有时间慢慢调试,再总结说明。
__main()函数最终要跳转到main()函数(即用户写的那个普通的main函数)
如果你写的程序里有main()函数的话,编译器就会默认的链接__main()函数和main()函数。
B . //跳转到本条指令,也即死循环,一般程序不会跑到这里的。
;****************************************************
;* The function for initializing stack *
;****************************************************
IMPORT UserStack
IMPORT SVCStack
IMPORT UndefStack
IMPORT IRQStack
IMPORT AbortStack
IMPORT FIQStack
//以下代码实现ARM芯片的各种模式下的堆栈空间的初始化,就是把分配的空间的地址分配给各个模式下的SP指针,至为什么要分配这个空间,我想你看了基础知识里的c语言和堆栈后应该很清楚了,另外当处理器模式更改时也要用到堆栈。记住了,现在分配的是系统堆栈空间哦。
InitStacks
;Don't use DRAM,such as stmfd,ldmfd......
;SVCstack is initialized before
;Under toolkit ver 2.50, 'msr cpsr,r1' can be used instead of 'msr cpsr_cxsf,r1'
mrs r0,cpsr //读取cpsr的值,在特权模式下才能修改cpsr的值,在特权模式下你可以用mrs和msr修改cpsr的后五位的值来实现模式的更改。
bic r0,r0,#MODEMASK
orr r1,r0,#UNDEFMODE|NOINT
msr cpsr_cxsf,r1 ;UndefMode
ldr sp,=UndefStack //更改为undefined模式,然后把分配空间的高地址付给这个模式的sp指针。定义这些空间是在stack.s这个文件中的,你可以看看哦。
orr r1,r0,#ABORTMODE|NOINT
msr cpsr_cxsf,r1 ;AbortMode
ldr sp,=AbortStack
orr r1,r0,#IRQMODE|NOINT
msr cpsr_cxsf,r1 ;IRQMode
ldr sp,=IRQStack
orr r1,r0,#FIQMODE|NOINT
msr cpsr_cxsf,r1 ;FIQMode
ldr sp,=FIQStack
;bic r0,r0,#MODEMASK|NOINT
orr r1,r0,#SVCMODE|NOINT
msr cpsr_cxsf,r1 ;SVCMode
ldr sp,=SVCStack
//其他模式的设置方法和第一个模式是一样的,最后ARM芯片运行在SVC模式下,芯片启动后默认的模式就是SVC,现在改来改去又回到了SVC模式下,而且cpsr的irq和fiq位是禁止中断的哦。
;USER mode is not initialized.
mov pc,lr ;The LR register may be not valid for the mode changes.//返回,当执行bl InitStacks 时把返回地址保存在了lr寄存器了。准确的说是保存到SVC模式下的lr寄存器了,所以最后切换的模式是SVC才保证了用mov pc,lr指令能正常返回。
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;; End of Startup.c
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
END
好了,现在总结一下现在的情况,硬件基本的初始化完成,c语言的执行环境已经初始化完成,现在跳转到main()函数去了,哎 终于见到了可爱的c语言。理解起来就简单喽。呵呵。我们进入下一阶段了。重要提醒:现在处于关中断状态。