arm开发经验(连载)
发表于 2007-8-9 9:59:22
前一段时间做了arm的一些开发,主要是编写了arm的启动软件和移植了uCOS-II到arm7。我做事情喜欢深入简出,及从最简单,最原理的方面先做一个框架,然后在这个框架里面进行补充。我还是一个很喜欢和别人讨论的人,希望有人可以给我提出意见和建议。我的这个心得很初级,都是一些基本的东西。现在拿出来和大家分享,希望在我毕业之前能给大家留一些纪念。^_^
由于这些东西发paper实在是没有价值,但是我感觉可以作为arm开发的入门。由于我的水平和经验有限,错误也是难免的。但是如果不拿出来和大家分享,就算有错误我也发现不了,是么,呵呵。我现试试发连载的第一篇,看看有多少价值,如果大家觉得有价值,我会继续连载的。
连载一:
前言
这个文档是我学习ARM编程的总结和心得。阅读这个文档的人应当首先阅读ADS1.2的帮助文档及相关内容。这个文档不会对编译器及连接器做出详细的说明,在需要的时候会指出具体内容在相关资料的章节。同时阅读这个文档的人需要了解ARM指令集和一些ARM汇编的基本内容以及C和C++的相关编程内容。同时还需要了解ARM的流水线结构及一些基本的编程知识。同时为了方便查阅英文文档,所有的相关术语都使用英文原文
第一章 STARTUP
1 ARM的启动
一般的嵌入式系统在主程序执行之前都需要执行一些初始化的过程以创造嵌入式程序运行的环境,尤其是一些高级的嵌入式系统,由于核心芯片使用内存映射、内存保护等机制以及编程使用高级语言C,C++甚至JAVA语言,都需要先创建一个适合程序运行的硬件环境,然后初始化或者配置或者剪裁run-time library,
这些工作都必须在主程序运行前完成,所以一个startup程序或者程序组对于一个嵌入式系统来说是非常重要的。要编写startup程序,需要对编译器、链接器和汇编器的细节有一定的了解,同时对ARM芯片硬件本身的地址分配以及memory mapping机制也需要有一些了解。
2 ARM 程序的工作过程
首先由各种source file经过编译产生object文件,然后object文件经过链接生成Image文件,然后通过ICE的方法,根据描述文件的指定下载到目标板上的固态存储器指定地址当中,比如flash,EEPROM, ROM等等。在程序执行之前,根据某些描述文件,将需要读写数据的部分读出放入动态存储器比如RAM当中,然后程序从ROM开始执行。或者有时为了提高程序的运行速度,也可以将所有的程序(有一些root的部分除外,以后会提及)通过一个描述文件放入指定的RAM当中,然后程序从RAM开始执行,但是这样会耗费大量的动态存储器,所以大部分程序会取折中的方法,将需要快速运行的部分和要读写的部分放入RAM中(一般读固态存储器的过程和动态存储器的过程是一样的,但是写就不同了,所以读写的部分一定要放到RAM中),而只读的部分和对速度要求不是那么高的部分放入固态存储器。同时ARM结构的异常向量表规定放在地址为0x00000000开始的地址空间上,而一般的CPU为了提高异常相应速度,会将这个向量段remap到其他的RAM当中,所以在描述文件当中必须精确指定异常向量跳转程序的地址到
remap的地方。在application程序执行前,还需要由一些文件描述application程序执行的环境。比如系统工作时钟,总线频率。现在一般嵌入式编程语言为C,C++等。如果在使用它们的时候使用的runtime-library,那么在程序执行前还需要为这些库
函数
excel方差函数excelsd函数已知函数 2 f x m x mx m 2 1 4 2拉格朗日函数pdf函数公式下载
初始化heap。然后ARM可能工作在不同的模式,还需要为不同的工作模式设置stack。这样,描述链接地址的文件,以及在application运行前所有的初始化程序就是startup程序组
3 STARTUP分类
这样,将startup程序所完成的功能分类。一类是链接地址描述,一类是各种初始化的程序。根据不同的应用,描述文件和初始化程序的内容以及结构和复杂程度都会不同。但是基本上,它们都必须实现以下功能。
3.1 描述文件实现功能
描述文件可以是链接命令行上简单的几个字符,也可以是一个非常复杂的文件,但是它必须完成如下功能:
; 指定程序下载的地址
; 指定程序执行的地址
3.2 初始化程序实现的功能
初始化程序根据不同的应用,其结构和复杂度也不同,但是它必须完成如下基本
功能:
; 异常向量初始化
; 内存环境初始化
; 其他硬件环境初始化
ARM开发经验(二)
发表于 2005-11-27 21:53:44
注:这个连载的版权属于自控所158所有。转载的时候请注明。转载需要通过作者本人同意。
/*
*********************************************************************
************************************ * Programming Arm
*
*
* (c) Copyright 1992-2008, 西安交通大学 * All Rights Reserved
*
* 自控研究所158 *
* 文件 : 连载二
* 版本 : V1.00
* 作者 : 潘自强
*
* 对象 : ARM7
* 模式 : ARM
* 工具 : ADS1.20
*********************************************************************
************************************ */
4 描述文件
要编写描述文件,必须知道ARM Image文件的组成及ARM Image文件执行的机理。 4.1 ARM Image的结构
一个ARM Image structure由linker在以下几个方面定义:
, 组成它的regions 和 output sections
当Image 下载的时候这些regions 和 sections 在内存中的位置,
当Image 执行时这些regions和sections在内存中的位置,
4.1.1 ARM Image的组成
一个ARM Image被保存在可执行文件当中,它的层次结构可以包括Image,regions,output sections和input sections。
, 一个Image由一个或多个regions组成,每个region包括一个或多个output sections
每个output, section由一个或多个input sections组成
Input sections是一个object file中的code和data信息。,
Image的结构如
下图:
1 附图: tu1.JPG (24684 字节)
NOTE Input section,output section和region的定义见ADS_LinkerGuide 3-3页。
同时Input section 有几种属性,分别为readonly,read-write,
zero-initialized。分别称为RO,RW和ZI。属性来源于AREA后的attr属性。 比如CODE是RO,DATA是RW,NOINT默认为ZI,即用0值初始化,但是可以选择不进行0值初始化。ZI属性仅仅来源于SPACE, DCB, DCD, DCDU, DCQ, DCQU,
DCW, 或者DCWU。由以上定义,ZI属性的包含于RW属性,它是有初始值的RW数据。又例如在C语言中,代码为RO,静态变量和全局变量是RW,ZI的。
ARM开发经验(三)
发表于 2005-11-28 9:14:09
注:这个连载的版权属于自控所158所有。转载的时候请注明。转载需要通过作
者本人同意。
/*
*********************************************************************
************************************
* Programming Arm
*
*
* (c) Copyright 1992-2008, 西安交通大学
* All Rights Reserved
*
* 自控研究所158
*
* 文件 : 连载三
* 版本 : V1.00
* 作者 : 潘自强
*
* 对象 : ARM7
* 模式 : ARM
* 工具 : ADS1.20
*********************************************************************
************************************ */
4.1.2 Image 的Load view 和 execution view 在下载的时候Image regions被放置在memory map当中,而在执行Image前,或许你需要将一些regions放置在它们执行时的地址上,并建立起ZI regions。例如,你初始化的RW数据需要从它在下载时的在ROM中的地址处移动到执行时RAM的地址处。
1 附图: tu2.jpg (640566 字节)
NOTE Load view 和execution view的详细定义见ADS_LinkerGuide 3-4
以上的描述包括二个内容,一是要指定各个section在load view和execution view时的地址即memory map,二是要在执行前根据这些地址进行section的初始化。
4.1.3 制定Memory map
制定memory map的方法基本上有二种,一是在link时使用命令行选项,并在程序执行前利用linker pre-define symbol使用汇编语言制定section的段初始化,二是使用scatter file。以上二种方法依应用程序的复杂度而定,一针对简单的情况,二针对复杂的情况。
ARM开发经验(四)
注:这个连载的版权属于自控所158所有。转载的时候请注明。转载需要通过作者本人同
意。
/*
*********************************************************************
*********
*************************** * Programming Arm
*
*
* (c) Copyright 1992-2008, 西安交通大学 * All Rights Reserved
*
* 自控研究所158 *
* 文件 : 连载四
* 版本 : V1.00
* 作者 : 潘自强
*
* 对象 : ARM7
* 模式 : ARM
: ADS1.20 * 工具
*********************************************************************
*********
*************************** */
4.1.1.1 利用linker pre-define symbol使用汇编程序
这是简单的方法,针对简单的memory map。在link时使用选项-ro, -rw, 等等指定memory map的地址。详细说明参看ADS_LinkerGuide中命令行选项说明。然后利用汇编使用pre-define symbol,来进行各种段的定位。Linker pre-define定义如下:
1 附图: tu1.jpg (22811 字节)
由前面对ZI的说明,Image$$RW$$Limit = Image$$ZI$$Limit。
: tu2.jpg (30577 字节) 2 附图
这些都是linker预先定义的外部变量,在使用的时候可以用IMPORT引入。下面给出一个例子。
假设linker 选项为:-ro-base 0x40000000 -rw-base 0x40003000。程序和只读变量(const 变量)大小为0x84,这样RO section的大小为0x84 bytes。Data
的大小为0x04 bytes,并且data被初始化,则RW section的大小为0x04,ZI section的大小为0x04。这样程序
在load view,地址是这样的:
0x40000000开始到地址0x40000080,是RO section部分(程序从0x40000000开始),Image$$RO$$Limit = 0x40000084. 0x40000084地址开始到地址0x40000084,是RW section部分。
在execution view,由linker的选项,各个section的地址是这样的: RO section的地址不变。
RW section的起始地址应当为0x40003000,则Image$$RW$$Base = 0x40003000。 因为全部的0x04 bytes data被初始化,所以Image$$RW$$Limit = Image$$ZI$$Limt = 0x40003004。
现在要做的就是将RW section移到以0x40003000开始的地方,并且创造一个ZI section。
一个更通用的做法是:
首先比较Image$$RO$$Limit和mage$$RW$$Base,如果相等,说明execution view下RW section的地址和load view 下RW section的地址相同,这样,不需要移动RW section;如果不等,说明需要移动RW section 到它在execution view中的地方。然后将Image$$ZI$$Base地址到Image$$ZI$$Limt地址的内容清零。 示例代码如下:
define symbols ;读入linker pre-
IMPORT |Image$$RO$$Limit|
IMPORT |Image$$RW$$Base|
IMPORT |Image$$ZI$$Base|
IMPORT |Image$$ZI$$Limit|
; .......一些其他的代码或伪指令
;R0读入section load address
LDR R0,=|Image$$RO$$Limit|
;R1读入section execution address LDR R1,=|Image$$RW$$Base|
;R2读入execution section 后的紧跟的word address LDR R2,=|Image$$ZI$$Base|
;检查RW section的地址在load view和execution view下
;是否相等,如果相等,就不移动RW section,直接建立
;ZI scetion
CMP R0,R1
BEQ do_zi_init
;否则就copy RW section到execution view下指定的地址
BL copy
; ......
; ......
;copy 是一个用于copy的子函数,它把从R0中的地址开始的 ;section copy到R1中的地址开始的section,这个section的 ;上限地址后紧跟的word address保存在R2中
copy
CMP R1,R2
LDRCC R3,[R0],#4
STRCC R3,[R1],#4
BCC copy
MOV PC,LR
; ......
; ......
;do_zi_int子函数是为创建ZI section做一些准备工作 do_zi_int
;将ZI section开始的地址装入R1
LDR R1,=|Image$$ZI$$Base| ;将ZI section结束后紧跟的word address装入R2 LDR R2,=|Image$$ZI$$Limit| ;将ZI section 需要的初始化量装入R3
MOV R3,#0
BL zi_int
; ......
; ......
;zi_int子函数用于建立并初始化ZI section,ZI section的 ;开始地址储存在R1,ZI section结束后紧跟的word address
;地址储存在R2
zi_int
CMP R1,R2
STRCC R3,[R1],#4
BCC zi_int
MOV PC,LR
; ......
; ......
这个方法针对比较简单的应用,如果需要进行一个比较复杂的memory map,如
下图,那么这个方法就不适用了。为了解决复杂memory map的问
题
快递公司问题件快递公司问题件货款处理关于圆的周长面积重点题型关于解方程组的题及答案关于南海问题
需要用到scatter load 机制。
3 附图: tu3.jpg (32473 字节)
系统分类: 汽车电子
用户分类: 无分类
标签: 无标签
来源: 无分类
发表评论 阅读全文(84) | 回复(0)
0
实现一个最简单的嵌入式操作系统
发表于 2007-8-9 9:54:21
实现一个什么都不能做的嵌入式操作系统
1.首先确定CPU,在这里为了简单,就选用嵌入式的CPU,比如ARM系列,之所以用RISC(简单指令集)
类型的CPU,其方便之处是没有实模式与保护模式之分,采用线性的统一寻址,也就是不需要进行段
页式内存管理,还有就是芯片内部集成了一些常用外设控制器,比如以太网卡,串口等等,不需要像
在PC机的主板上那么多外设芯片
2.确定要实现的模块和功能,为了简单,只实现多任务调度(但有限制,比如最
多不超过10),实
现中断处理(不支持中断优先级),不进行动态SHELL交互,不实现动态模块加载,不实现fork之类
的动态进程派生和加载(也就是说要想在你的操作系统上加入用户程序,只能静态编译进内核中;不
支持文件系统,不支持网络,不支持PCI,USB,磁盘等外设(除了支持串口,呵呵,串口最简单嘛),
不支持虚拟内存管理(也就是说多任务中的每个进程都可以访问到任何地址,这样做的话,一个程序
死了,那么这个操作系统也就玩完了)
3.确定要使用的编译器,这里采用GCC,文件采用ELF格式,当然,最终的文件就是BIN格式,GCC和
LINUX有着紧密的联系,自己的操作系统,需要C库支持和系统调用支持,所以需要自己去裁剪C库,
自己去实现系统调用
4.实现步骤:首先是CPU选型,交叉编译环境的建立,然后就是写BOOTLOADER,写操作系统
实现一个最简单的嵌入式操作系统(二) 如何实现BOOTLOADER
1.之所以要实现一个专用的BOOTLOADER,一是为了更好的移植和自身的升级,二是为了方便操作系统的调试,当然,你完全可以将这部分所要实现的与操作系统相关的功能集成到操作系统中去
2.确定一个简单的BOOTLOADER所要完成的功能:我们这里只需要完成两个主要功能,一是将操作系统加载到内存中去运行,二是将自己和操作系统内核固化到ROM存储区(这里的ROM可以是很多设备,比如嵌入式芯片中的FLASH,PC机上的软盘,U盘,硬盘等)
3.BOOTLOADER的编写:
第一步:要进行相关硬件的初使化,比如在at91rm9200这块嵌入式板子上(以后都使用这一款芯片,主要是我对这款芯片比较熟悉,嘿嘿),大概要做接下来的几方面的工作,其一:将CPU模式切换进系统模式,关闭系统中断,关闭看门狗,根据具体情况进行内存区域映射,初始化内存控制区,包括所使用的内存条的相关参数,刷新频率等,其二:设定系统运行频率,包括使用外部晶振,设置 CPU频率,设置总线频率,设置外部设备所采用的频率等。其三:设置系统中断相关,包括定时器中断,是否使用FIQ中断,外部中断等,还有就是中断优先级设置,这里只实现两个优先级,只有时钟中断高一级,其它都一样,而中断向量初始化时都将这些中断向量指向0x18处,并关闭这里的所有中断,如果板子还接有诸如FLASH设备的话,还需要设置诸如FLASH相关操制寄存器,其四:需要关闭CACHE,到此为止,芯片相关内容就完成初始化了
第二步:中断向量表,ARM的中断与PC机芯片的中断向量表有一点差异,嵌入式设备为了简单,当发生中断时,由CPU直接跳入由0x0开始的一部分区域(ARM芯片自身决定了它中断时就会跳入0x0开始的一片区域内,具体跳到哪个地址是由中断的模式决定的,一般用到的就是复位中断,FIQ,IRQ中断,SWI中断,指令异常中断,数据异常中断,预取指令异常中断),而当CPU进入相应的由0x0开始的向量表中时,这就需要用户自己编程接管中断处理程序了,这就是需要用户自己编写中断向量表,中断向量表里存放的就是一些跳转指令,比如当CPU发生一个IRQ中断时,就会自动跳入到0x18处,这里就是用户自己编写的一个跳转指令,假如用户在此编写了一条跳转到0x20010000处的指令,那么这个地址就是一个总的IRQ中断处理入口,一个CPU可能有多个IRQ中断,在这个总的入口处如何区分不同的中断呢,就由用户编程来决定了,具体实现请参见以后相关部分,中断向量表的一般用一个vector.S文件,当然,如何命名那是你自己的喜爱,但有一点需要声明,那就是在链接时一定要将它定位在0x0处
第三步:设置堆栈,一般使用三个栈,一个是IRQ栈,一个是系统模式下的栈(系统模式下和用户模式共享寄存器和内存空间,这主要是为了简单),设置栈的目的主要是为了进行函数调用和局部变量的存放,不可能全用汇编,也不可能不用局部变量
第四步:将自己以后的代码段和数据段全部拷贝至内存,并将BSS段清零
第五步:进行串口的初始化(主要是为了与用户交互,进行与PC机的文件传输),FLASH的初始化这里在FLASH中存放BOOT和内核),FLASH驱动的编写(这里的驱动有别于平常所说的驱动,由于FLASH不像SDRAM,只要设定了相关控制器之后就可以直接读写指定地址的数据,对FLASH的写操作是一块一块数据进行,而不是一个字节一个字节地写,具体请查阅相关资料)
第六步:等待一定的秒数,来接收用户进行输入,如果在指定的秒数内用户未输入任何字符,那么
BOOT就开始在FLASH中的指定位置(可以由自己指定,这么做主要是为了简单)读取内核的所有数据到内存中(具体是内存中的什么位置由自己指定,也可以采用LINUX之类的做法,就是在内存的起始位置加上一个0x8000处),将跳转到内核的第一条代码处);如果用户在指定的秒数内键入了字符(这主要是为了方便开发,如果开发定型之后完全可以不要这段代码),那么就在串口与用户进行交互,接受用户在串口输入的命令,比如用户要求下载文件在FLASH中指定的位置等,具体内容可参考U-BOOT之类的开源项目到这里为止,BOOT部分已完成,这个BOOT非常简单,仅仅只是将PC机上传下来的文件固化到FLASH中,然后再将FLASH中的操作系统内核部分加载进内存中,并将CPU的控制权交给操作系统,下一页开始讲解如何写一个最简单的操作系统,呵,到现在才开始切入正题呢~~~~
实现一个最简单的嵌入式操作系统(三)
如何实现一个最简单的操作系统
这里为了简单,就不考虑可移植性开求,不从BOOT部分来接收参数,也不对硬件进行检测,
也不需要进行DATA段,代码段的重定位。我只是读了LINUX内核相关部分,并未自己去实现
一个操作系统,所以我以下所说的只是概念性的东西:
1.接管系统的中断处理,由于BOOT部分的代码决定了那个中断向量表,从而决定了系统中断
之后进入的内存位置,但BOOT并不知道操作系统的中断处理函数位置所在啊,怎么办呢,
有几种方法,其一是:如果你的板子可以重映射地址,也就是可以将内存条所在的位置
重映射成0x0开始,那么在链接内核的时候,就将操作系统自己的中断向量表定位在0x0处
并且在BOOTLOADER引导结束时就完成映射操作,并让CPU跳转到0x0处执行;如果没有重映
射功能,我就不晓得怎么办了,不过我想到一个折衷的办法,就是在BOOTLOADER启动完成
时(也就是将CPU控制权交给操作系统内核时),重新改写FLASH的0x0区域,就是将操作
系统的内核的中断向量表写入FLASH区的0x0处,比如,当一个IRQ发生时,CPU决定了会
跳入0x18(假设这里FLASH占用地址总线0x0至0x0fffffff,内存占用0x20000000至0x2fffffff)
,而BOOTLOADER在最后将0x18处的代码修改成了0x20000000加上0x18的地址处的代码,而这个
地址就是内核的中断向量表中的相关跳转指令,就相当于跳转进了内核所关联的IRQ处理函数
的地址上去执行中断处理函数了,而这样的不好之处在于:当系统重新上电之后,BOOT的
中断向量表已经被修改,除非BOOT本身不使用中断,呵,在这样简单的系统中,BOOT是不
需要中断功能的
2.这里为了简单,所以没有使用分页内存管理,就不需要建立页表等操作,直接进行操作
系统的堆栈设置,同BOOT一样的设置过程一样,接着就进行BSS段清零操作,这里的BSS段
是指操作系统自身的BSS段,与BOOT的BSS段是同一个含义只是用在了不同的地方了,接着
就跳入了MAIN函数
3.为了最大可能的简单,采用静态建立任务结构数组,比如只建立十个任务,那么首先要
为这十个任务结构分配段内存,可以在堆上分配(这个分配的内存直到操作系统结束才会
被释放,当然也可以指定一片操作系统的其它地方都用不到的内存区域,不过这样写的话
就有点外行的味道了,而符务结构数组的指针却是全局变量,存放在BSS段或者
DATA段),
由于在上一步中已经分配了一个系统堆栈,那么我们这十个任务就分享这总体的堆栈区域
这里的重点就是如果定义每个任务结构数组里面的结构,可以参照LINUX的相关部分设计
4.中断处理:在第一步中已经确定了CPU进行相关的几类型的中断跳转地址,而相同类型
的中断却只有一个入口地址,这里的中断处理就会完成以几个动作: 其一:入栈操作,包括所有寄存器入栈,至于这个栈,就是在第二步中所设置的IRQ栈,
其二:屏掉所有中断,呵,这里为了简单起见,所以在处理中断时不允许再次发生中断
其三:读取中断相关的寄存器,判别是发生了什么中断,以至于跳进相关的中断处理函
数中去执行(在这里只包括两种中断,一是时钟中断,另一个是SWI中断,也就是所谓
的系统调用时需要用到的)
其四:等待中断处理完成,然后就开启中断并出栈,恢复现场,将CPU控制权交给被中断
的代码处
注意:
其一:在MIAN中必须首先确定整个系统有哪些需要处理的中断,也就是有哪些中断处理
函数,然后才编写这里的中断处理函数
其二:本操作系统不处理虚拟内存,其至连CPU异常都不处理(一切都为了简单),一旦
发生异常,系统就死机
5.对TIMER的实现,首先确定时间片,为了让系统更稳定,而且我们不需要实时功能,尽
可能让时间片设置长一点,比如我们让一个任务运行20个时钟滴答数,然后应根据系统
频率来确定每个系统滴答所占用的毫秒,这里使用5毫秒让系统定时器中断一次,那么就
需要写时钟寄存器,具体参阅芯片资料,计算下来,一个任务最大可能连续运行100毫秒
,注意:我们的操作系统不支持内核抢占,同时只支持两级中断优先级,就是只有时钟
中断的优先级高一点,其它的优先级都低一级,但是在中断处理一节中却屏掉了这个功能
因为一进入中断处理,就禁止中断,所以不管其它中断优先级有多高都没有用的,这样做
优点是简单了,但不好之处显而易见,特别在相关中断处理函数如果进入了死循环,那么
整个系统就死了,而且时间片也变得不准确了,反正都不用实时,也不需要实时钟支持嘛
至于中断优先级设置请参阅芯片资料
6.进程调度的实现,也就是do_timer函数(时钟中断处理函数),有一个全局变量指针,
指向的就是当前任务结构数组(或者链表),当时钟中断时,就进入此函数中,首先判断
任务结构体中的时间片是否用完,如未用完,就减一,然后退出中断,让CPU继续运行当
前的任结构,若用完了时间片,就重置时间片,并重新寻找任何结构数组中的下一个等待
运行的任务,若找到了,就切换至新的任务,至于如何切换,请见下一页描述,如果未找
到就切换到IDLE任务(类似于LINUX,呵呵,所有的处理就是模仿LINUX,由于本人水平太
差,所就不能自创一招),注意:为了简单,所以没有实现任务优先级,也未实现任务
休眠等,也就是说只要静态地决定了有十个任务,这十个任务就按先后顺序一个一个执行
而且每个任务都不允许结束,就是说在每个进程中的最后一句代码都必须用死循环,不然
的话系统就跑飞了),还有一点,进程不支持信号,没有休眠与唤醒操作,这个CPU就是
不停地在运行,呵呵,反正CPU又不是人,所以不需要人权的哈~~~这种调度是不是简
单得不能再简单了,,,,,~~~~
7.串口不使用中断,这就是最大可能的降低难度,串口使用论询的方式来实现读写(当
然是阻塞的方式了哦,而且只有写,不允许读,因为读的时候需要涉及到采用中断方式,
因为轮询方式有个不好的地方,那就是正在读的时候,这里有可能当前进程的时间片用
完了,系统切换到另一个进程,这里你在PC机的串口输入的数据就丢弃了,唉,
又是为
了简单嘛)
8,最后一步就是MIAN函数的最后一部分,将本进程当作IDLE进程(相当于修改任务结构
数组中的数据),开启中断,将当前进程加入一段死循环,以免它退出去。 9.编译你的BOOTLOADER,KERNEL,并烧写至FLASH,反复调试 10.至此将你的at91rm9200(或者是其它相类似的芯片)的串口接上PC机,打开超级终端,
打开板子电源,说不定你的操作系统就打印出了"hello,world"了~~~一个最简单的操作
系统就出来了
下一页是具体的功能模块实现
任务结构数组(或链表)的实现
我们的任务结构就采用链表形式吧,但其长度是限定了的,头指针是一个全局指针变量(
指针变量是一个无符号整型指针,其指针本身所在的地址是在BSS段,但其指向的内容是分
配在堆上的一片内存),分配内核内存的函数就用kmalloc吧,kmalloc函数需要自己编写
呵,为了简单,这个函数只接受一个参数,就是所需分配大小,这个函数做得很简单,首先
有一个全局针指,它在初始化时指向了整个堆的起始位置,并且固定大小,就是所谓的内核
堆栈,在内核堆栈之后就是用户堆栈,由于总共有十个任务,当然不包括内核本身的任务,
所以整个堆栈就平均分成十一部分,注意:在所有任务初始化完成之后,还有一个步骤就是
将内核这个任务移到用户态,相当于要将自己的任务结构的堆栈指针修改一下就行了),
判断大小是否超出了内核堆的可分配范围,还有一点,需要维护内核堆和其它任务的堆,
需要进行分块,并且有一个全局的内存使用标识,就用数组吧,简单,0表示相应的内存
部分未占用,1就表示占用,对应的kfree就相当于把标志置0),
对于内存的维护,比较复杂,为了简单,就定为4K,并且不能进行大于四K的内存申请,因为
大于4K之后,由于没有虚拟地址的概念,就不能实现堆上的连续分配地址,当然在栈上分配
是可以大于4K的,栈是由编译器和CPU所决定了的
任务结构包括:
1.所剩的时间片
2.本任务所指向的代码段内存地址,这里也就是函数入口地址
3.本任务所指向的数据段地址,这里的数据段被包含进了整个内核中,所以并没有用,作为保留
4.本任务的函数体是否存在,也就是否会被调度
5.本任务所使用的栈指针
6.本任务所使用的堆指针
7.本任务的标识,用0代表是IDLE,1代表是其它进程
8.所有寄存器的值
9.当前PC值,初始化时被置成了函数入口地址
首先讲解一下任务数组结构的初始化:
将先定义一个全局指针,然后将此指针强制转换为一个任务结构指针,并通过kmalloc函在内核所
占用的堆(前而讲过内核的堆的起始就是整个堆的起始)上去分配十个任务结
构所占的内存,这里
是绝不会超过4K的
并且为这十个任务结构赋值,将第一个任务置为IDLE,时间片为20,代码段
main函数的 内存地址为
的地址,数据段地址忽略,函数体存在,可以被调度,栈指针指向的位置根据以下来计算:
假定每个给每个任务可使用的堆栈设定为64K,而整个堆的起始位置是0x20030000,那么第一个堆指
针所指向的就是0x20030000,栈就是0x20030000+64K的位置,第二个以后就以此类推
注意:在初始化任务结构之前,不允许系统使用堆,但可以使用栈,那么内核任务栈部分就分成了
两个,在未进行调度之前,栈就是上一页中第二步中所设的栈,那么上一页设置堆栈的时候就得注
意必须将堆栈空间设成十个64K再加上在本步骤使用以前的最大可能所需的栈空间
再讲解一下任务切换时所要做的事情:
进入整个中断处理入口时,会将所有寄存器推入IRQ栈之中,并把值拷贝到当前任务结构相应的字段
当中,并取出被中断的进程的当前PC值存入当前任务结构中的相应字段中,接下就判别中断类型,
以进入相应的中断处理函数,这里就会进入do_timer函数中,以下就是进入此函数之后的流程:
内核中还有一个全局指针,就是当前任务指针,它本身也是在系统BSS段中,它的定义如上一步中的
那个全局指针一样,当由系统时钟中断之后,就取出这个全局指针,上一步初始化完成之后,还会
把这个指针指向第一个任务结构所在位置,也就是0x20030000处,那么就取出这个任务结构中的时
间片字段,判断其是否为0,若为0,就进行以下的操作:
保存用户态下的栈指针至当前任务结构,保存堆指针,并将搜索一下可以被调
度的任务结构,并将
此任务结构赋给当前任务指针,置需要进行任务切换标识,此标识同样是一个全局变量,但它是被
赋了初值,会放在整个系统的DATA段中,返回do_timer函数。
若不为0,就进行以下操作:
将时间片减一,返回do_timer函数
接下来判断任务切换标识,若为0,则进行以下操作:
不需要进行任务切换,所有寄存器出栈(这里的栈指的是IRQ栈),重新开启中断,切换到用户模式,
加载当前任务结构中的当前PC值字段,以退出中断处理程序
若此标识为1,则执行以下操作:
就需要进行任务切换,让所有寄存器出栈(这里的栈指的是IRQ栈),将当前任务结构中的所有寄
存器的值恢复到相应寄存器中,将用户态下的栈指针恢复至当前任务结构栈指针,将堆指针恢复至
当前任务结构堆指针,并把需要进行任务切换标识恢复为0,重新开启中断,切换到用户模式,任务
切换是通过加载PC值来实现的,也就是通过加载当前任务结构中的当前PC值字段,以退出中断处理程序
系统调用的实现
本系统是完全可以不实现系统调用的,因为没有实现内核态和用户态的保护,完全可以不实现
自己的C库,所有的函数都像kmalloc之类的实现一样,在内核中直接写函数原型,但为了以后
扩展,还是说一下系统调用,这里以malloc系统调用来实现
首先说明还有一个堆指针(前面在kmalloc时有一个堆指针,不过那个堆指针是为内核任务,中
断处理所提供),这里这个堆指针是用于用户态的,它在系统初始化完成之前会赋上初值,其初
值就是第一个任务结构所使用的堆的起始位置,也就是在内核所使用的堆加上
64K的位置
函数库中的malloc函数实现步骤如下:
1.首先检测申请大小是否超出了4K,若超出4K,就返回错误
2.进行系统调用(这里用_syscall1,并只传递一个参数(所需分配大小)
系统调用函数_syscall1的实现:
1.将寄存器压入堆栈(这里的栈指向就是当前任务的栈)
2.将系统调用号1放至R0,参数放入R1
3.发出SWI指令以产生SWI中断(就是所说的软中断,陷阱)
此时系统发生中断,会进入SWI中断处理入口,下面说一下SWI入口函数的实现
1.取出R0的值,判断其值,进入相应的分支处理代码段
2.在此进入_malloc处理代码段,取出R1的值,然后再得到前面所说的当前堆指针,并申请对应数
据块大小,置用于内存占用标识的相应字段,将当前堆指针放入R0,移动当前堆指针,改变当前任
务结构的堆指针,切换到用户态,返回SWI中断系统调用_syscall1的返回处理:
为了简单,在从内核态返回用户态时,不再进行任务的重新调度,所以上面的步骤就相对简单
1.当从SWI中断返回后,系统就运行在了用户态,此时取出R0的值,并赋值
给需要申请内存的指针
2.在用户态弹出寄存器,返回到上一层函数
malloc函数的返回,此时malloc函数直接返回指针就行了,整个malloc的流程就结束了,其它的系
统调用同这个过程类似
到此为止,这个操作系统初步实现了,但好像什么事情都不能做,如果让它支持串口中断的话,或许
可以做那么一点点事情,比如像单片机那样的功能,整个系统的难点就是中断处理和任务切换,在本
例中,由于ARM不支持像0x86那样的CPU级的保护模式,所以进行任务切换的时候,就得自己通过加载
PC值的方法来实现,呵,因为我想不到更好的办法,但这个办法有一个不好解决的地方,就是寄存器
入栈和出栈的保护,在进入中断时,必须保护寄存器,但如果需要进行重新调度,就得从中断上下文
切换到进程上下文中,如何从中断上下文切换到进程上下文呢,,我在这里所采用的方法很笨拙:
1.首先让寄存器入栈
2.让寄存器保存至当前任务结构数组,被中断掉的进程的PC值保存至任务结构
3.处理timer中断
4.如果进行任务切换,寻找下一个可调度的进程,然后把当前任务结构指下刚搜索到
的任务结构,让寄存器出栈,恢复当前任务结构里的值到寄存器,恢复堆栈指针,切换到用户态,通
过加载当前任务结构的PC值来恢复被挂起的进程
这里在中断上下文中使用了任务结构,这在LINUX上好像是不这样用的,中断
上下文和进程上下文是两
个不同的概念,中断上下文中不能访问进程上下文里的任务结构,我实在想不出有什么办法来实现进程
调度了,所以请看到我这则文章的人提出好一点的方法
欢迎对LINUX有兴趣的人加入群讨论,群号:10074203 系统分类: 嵌入式
用户分类: 无分类
标签: 无标签
来源: 无分类
发表评论 阅读全文(168) | 回复(0)
0
嵌入式linux快速入门
发表于 2007-8-9 9:52:51
一个典型的桌面Linux系统包括3个主要的软件层---linux内核、C库和应用程序代码。
内核是唯一可以完全控制硬件的层,内核驱动程序代表应用程序与硬件之间进行会话。内核之上是C库,负责把POSIX API转换为内核可以识别的形式,然后调用内核,从应用程序向内核传递参数。应用程序依靠驱动内核来完成特定的任务。
在设计嵌入式应用的时候,可以不按照这种层次,应用程序越过C库直接和内核会话,或者把应用和内核捆绑在一起,甚至可以把应用写为内核的一个线程,在内核中运行,虽然这样在移植上带来了困难,但考虑嵌入式系统对尺寸要求小的特点,是完全可行的。不过我们使用三层软件结构的模式来学习嵌入式linux将会是我们认识更清晰,简单可行并使应用具有弹性。
快速入门,最简单的建立嵌入式Linux应用的方法就是从我们使用的桌面Linux入手,安装一个喜爱的版本,把我们的某个应用作为初始化的一部分,框架就算完成了。当然,嵌入式linux应用远比我们的桌面版本功能简单专一,它也许就是一个用于足彩的终端机,或是一个数码音频播放器,这些系统除了使用嵌入式CPU外,仅仅再需要一个串口,网口等少量的输入输出接口就可以完成它们特定的应用了。在软件上,它可以按照三层的概念由内核装载器,定制的内核和较少的为特定任务设计的静态连接的应用程序组成。之所以使用静态连接的应用程序,是因为少量的静态连接程序所要的存储空间,比同样数量的动态连接的程序所占的空间小,这个平衡点需要我们在实际开发中去获取。也许你正在设计的是个PDA,它的应用程序较多,那么你很可能就要使用动态连接程序来减少存储空间。在你的/bin或者/sbin
目录
工贸企业有限空间作业目录特种设备作业人员作业种类与目录特种设备作业人员目录1类医疗器械目录高值医用耗材参考目录
下,用厂列表看看bash,ifconfig,vi...,也许只用几十K,当你运行 ldd /bin/bash 时,你会看到它们都和好几个库文件相连。好了,这样看来,我们得把PC想像成一个嵌入式硬件平台,再重新制作一个特定功能的嵌入式linux。
再进行实际操作之前,先来搞清楚几个基础知识。
内核装载器Loader,它的作用是把内核从外部存储器,移动到内存中。它只作这个事情,一旦完成了调入内核的工作,Loader就跳转到内核位置开始执行。不同架构有不同的Loader,在x86结构的PC上,通常使用的loader有LILO,GRUB,syslinux,syslinux在嵌入式 linux中也同样工作。其他非x86架构的应用中,你必须使用专门的loader,或者自己编写loader来装入内核。也有不使用loader的情况,系统加电以后,内核直接从烧录有映象的Flash上开始执行。
内核,一旦内核开始执行,它将通过驱动程序初始化所有硬件,这可以从我们的pc机监视器的输出看出来,每个驱动程序都打印一些有关它的信息。初始化完成后,计算机就准备运行嵌入式应用。也许一个,也许是多个应用程序组成了嵌入式应用,但通常首先调用的是init(通过loader 向核心传入init=/program 可以定制首先运行的程序)。桌面linux中,init会读取/etc/inittab文件,来决定执行级别和哪些脚本和命令。嵌入式应用中,可以根据实际的情况决定是否使用
标准
excel标准偏差excel标准偏差函数exl标准差函数国标检验抽样标准表免费下载红头文件格式标准下载
的init执行方式,也许这个init是个静态程序,它能够完成我们的嵌入
应用的特定任务,那完全不用考虑inittab了。
initrd文件系统,initrd以一种把内核从存储介质装入到内存的相同的机制来装入一个小型文件系统。这个文件系统最好是以压缩的方式存储在介质上的,解压缩到RAM盘上。通过使用initrd,包含有核心驱动和启动脚本的小文件系统,就可以直接从介质上和内核一起启动起来,内核届压缩这个文件系统,并执行这个文件系统上叫做/linuxrc的脚本文件,这个脚本通常会把启动过程中所需要的驱动程序装入。脚本退出以后,initrd文件系统也卸下了,启动过程进入真正初始化过程。对于嵌入式来讲,可以将需要的应用软件都运行在这个initrd文件系统上,只要/linxrc文件不结束,内核启动过程的其他部分就不会继续。 做个试验:
cp /boot/initrd-2.4.20.img /tmp cd /tmp
mv initrd-2.4.2-.img initrd.img.gz gunzip initrd.img.gz
mount -o loop initrd.img /mnt cd /mnt
ls
cat linuxrc 可以看到里面执行了加载了两个模块的操作,你在启动linxu的时候会看见屏幕打印信息。
入门试验,制作一个简单的应用:我们使用一张软盘启动一台假象的只有一个串口,键盘输入,显示输出的x86架构的linux系统,执行的特定应用就是运行 minicom,通过串口拨号。需要软件: minicom-xx.src.tar.gz 和 syslinux-xx.tar.gz,xx代表版本号,开始之前,在主目录建立一个目录,来释放这两个软件包:
cd
mkdir -p project/minilinux
cd project/minilinux
tar zxvf minicom-xx.src.tar.gz tar zxvf syslinux-xx.tar.gz
1、裁减linux内核(需要系统安装内核文件包)
配置内核的时候,我们需要选择这些:摸块编入内核,386处理器、物理内存o
(4096)、支持 initial RAM disk ff、支持ELF、标准PC软盘、支持RAM盘
(initrd)、虚你终端、虚拟终端控制台、标准串口、ext2文件系统、控制台驱动,VGA text console、DOS FAT、MSDOS文件系统,其他的都可以不要,这样内核编出来较小。
步骤:
cd /usr/src/linux
make mrproper
make xconfig
make dep && make bzImage
得到 /usr/src/linux/arch/i386/boot/目录的内核文件bzIamge。 2、编译一个静态的minicom ,把它作为将来的linuxrc cd minicom-xx/src
vi Makefile
修改下面这行
minicom: $(minicom_OBJECTS) $(minicom_DEPENDENCIES)
rm -f minicom 下面的行加上 -static,连接为静态程序
(LINK) -static $(minicom_LDFLAGS) $(minicom_OBJECTS) $(minic
om_LDADD) $(LIBS)
vi minicom.c
找到 if (real_uid==0 && dosetup==0 ) 删除这个判断条件语句,主要是用于权限判断的,因为这个嵌入应用不关注权限问题,否则会出错。 make
得到可执行程序,用ldd 检查一下是不是静态程序。
3、准备initrd压缩文件image.gz
dd if="/dev/zero" of="image" bs="1k" count="4096"
losetup /dev/loop0 image
mke2fs -m 0 /dev/loop0
mounmt -t ext2 /dev/loop0 /mnt/
mkdir -p /mnt/dev
mkdir -p /mnt/usr/share/terminfo/l/ cd /dev
cp -a consle null tty tty0 zero mem /mnt/dev cp -P /usr/share/terminfo/l/linux /mnt/usr/share/terminfo/l/linux
cp ~/project/minilinux/mincom/src/minicom /mnt/linuxrc
umount /mnt
losetup -d /dev/loop0
sync
gzip -9 image
4、制作软盘引导,并拷贝文件 bzimage image.gz 到软盘
A.使用grub
fdformat /dev/fd0
mke2fs /dev/fd0
mount /mnt/fd0 /mnt/floppy
mkdir -p /mnt/floppy/boot/grub
cp /boot/grub/stage1 /boot/grub/stage2 /mnt/floppy/boot/grub
执行 grub,在软盘上创建引导
grub > root (fd0)
grub > setup (fd0)
grub > quit
cp /usr/src/linux/arch/i386/boot/bzImge /mnt/floppy cp ~/porject/minilinux/image.gz /mnt/floppy
编辑 /mnt/floppy/boot/grub/grub.conf default =0
timeout-=10
title minilinux
root (fd0)
kernel /bzImage
initrd /image.gz
卸下软盘
umount /mnt/floppy
B. 使用syslinux
fdformat /dev/fd0
mkfs.msdos /dev/fd0
mount -t msdos /dev/fd0 /mnt/floppy cp /usr/src/linux/arch/i386/boot/bzImge /mnt/floppy
cp ~/porject/minilinux/image.gz /mnt/floppy
cp syslinux-xx/ldlinxu.sys /mnt/floppy cat > /mnt/floppy/syslinux.cfg LABEL linux
KERNEL bzimage
APPEND initrd="image".gz
umont /mnt/floppy
syslinux-xx/syslinux /dev/fd0 sync
5、用软盘启动计算机,如果幸运,minicom的运行画面出现在屏幕上。
到此,我们的单应用嵌入式linux做好了,但它还很简陋,没有什么实际用途,但通过这个实验,可以了解嵌入式系统的大致结构和开发过程。在进行实际的嵌入式开发时,通常要在PC机上借助嵌入式linux开发工具包,如:uclinux,bluecat等,对相应的硬件平台(目标机)进行软件编写编译,调试成功后,将内核及应用程序写入到目标机的存储器中,从而完成整个应用。
系统分类: 嵌入式
用户分类: 无分类
标签: 无标签
来源: 无分类
发表评论 阅读全文(156) | 回复(0)
0
使用ADS1.2进行嵌入式软件开发(来自ARM公司)
发表于 2007-8-9 9:48:19
使用ADS1.2进行嵌入式软件开发(上)
作者:ARM公司 来源:eepw.com.cn
概述
嵌入式应用程序通常都是在样机环境下调试与开发的,这种环境与最终产品之间并不完全相同。因此,在系统调试阶段就考虑应用程序在最终目标硬件中的运行情况是非常重要的。
本文旨在讨论如何将一个开发/调试环境下的嵌入式应用程序转移到最终独立运行的目标系统中去,并提到了ARM ADS1.2开发工具包的一些功能特性及其在这个过程中所起到的作用。
使用ADS开发嵌入式程序时,需要着重考虑以下几个问题: 1.与硬件相关的C语言库函数的使用;
2.某些C语言库函数使用了调试环境中的资源,要把这些使用的资源重定向到目标系统中的硬件上来;
3.可执行映象文件的存储器映射必须根据目标硬件的存储器分布进行裁剪; 4.在主程序执行前,嵌入式应用程序必须先完成系统的初始化。一个完整的初始化包括用户的启动执行代码和ADS中C库函数的初始化过程。
图1 Semihosting的实现举例
C语言库函数结构 图2
图3 缺省的存储器映射
图4 连接器布局规则
缺省的工程项目设置
刚开始一个嵌入式应用软件开发时,ADS用户可能并不完全清楚目标硬件的一些参数指标。比如有关外设、存储器地址分布,甚至处理器类型等一些细节,可能还没有最终确定。为了在所有这些细节全部就绪前就能进行软件开发,ADS工具有一套程序构建和调试的缺省设置。了解这套缺省的工程项目设置方法,对于掌握最终的移植步骤非常有好处。
ADS1.2C语言函数库
Semihosting
在ADS的C语言函数库中,某些ANSIC的功能是由主机的调试环境来提供的,这套机制有一个专门术语叫Semihosting。Semihosting通过一组软件中断(SWI)指令来实现。如图1所示,当一个Semihosting软中断被执行时,调试系统先识别这个SWI请求,然后挂起正在运行的程序,调用Semihosting的服务,完成后再恢复原来的程序执行。因此,主机执行的任务对于程序来说是透明的。
C语言库函数结构
从概念上来讲,C语言库函数可以被分成两部分,一是ANSIC语言
规范
编程规范下载gsp规范下载钢格栅规范下载警徽规范下载建设厅规范下载
本身的一部分,一是只受某一特定ANSIC层次支持的函数,如图2所示。 其中一些ANSIC的功能是由主机调试环境调用驱动程序级的函数完成的。例如,ADS的库函数printf()把输出信息输出到调试器的控制台窗口,这个功能通过调用__sys_write()实现,__sys_write()执行了一个把字符串输出到主机控制台的
Semihosting软中断服务程序。
缺省的存储器映射
如果用户在程序编译时没有指定映象的存储器映射分布,ADS将为生成的目标代码和数据分配一个缺省的存储器映射图,如图3所示。
目标印象被连接至地址0x8000,存储和执行区域都位于该地址开始的空间。
)部分放在前面,接着是RW(读写)部分,最后是ZI(零初始化)部分。 RO(只读
在ZI部分之上紧跟着HEAP,所以HEAP的确切地址要在连接时才能确定。 STACK的基地址是在应用程序启动时由一个Semihosting操作提供。这项Semihosting操作返回的地址值视不同调试环境而定:
ARMulator返回配置文件peripherals.ami中的设置值;缺省为0x08000000。 Multi-ICE返回的是调试器内部变量,top_of_memory的值;缺省为0x00080000。
连接器布局规则
连接器对代码和数据在存储器系统中的分配,遵循一套规则,如图4所示。 映象首先按照属性以RO-RW-ZI的次序进行排列,在同一种属性里面代码先于数据。然后连接器将输入段根据名字的字母顺序进行排列,输入段的名字与汇编代码里面的块名字指示一致(在汇编程序中用AREA关键字)。在输入段中,来自不同对象的代码和数据放置次序与在连接器命令行中指定的对象文件次序一致。 在需要灵活分配代码和数据放置位置的情况下,建议用户不要简单地依靠这些规则。后面会介绍一种如何控制代码和数据布局的机制Scatterloading。
图5 缺省的ADS初始化过程
图6 C库函数重定向
图7 scatter文件语法
图8 分散加载的简单样例
启动应用程序
大多数嵌入式系统在进入应用主程序之前有一个初始化的过程,该过程完成系统的启动和初始化功能。缺省的ADS初始化过程如图5所示。
总体上,初始化过程可以分成两部分来看:
_main负责设置运行映像存储器映射;
_rt_entry负责库函数的初始化。
_main完成代码和数据的复制,并把ZI数据区清零。这一步只有当代码和数据区在存储和运行时处于不同的存储器位置时才有意义。接着_main跳进_rt_entry,进行STACK和HEAP等的初始化。最后_rt_entry跳进应用程序的入口main()。当应用程序执行完时,_rt_entry又将控制权交还给调试器。
函数main()在ADS中有特殊的意义。当一个程序工程项目中存在main()时,连接器会把_main和_rt_entry中的初始化代码连接进来;如果没有main()函数,初始化过程就不会被连接,结果就会导致一些标准的C库函数无效。
根据目标环境裁减C库函数
缺省状态下C库函数利用Semihotsting机制来实现设备驱动的功能。但一个真正的嵌入式系统,要使用到具体的外设或硬件独立于主机环境运行。 C库函数重定向
用户可以定义自己的C语言库函数,连接器在连接时自动使用这些新的功能函数。这个过程叫做重定向C语言库函数,如图6所示。
举例来说,用户有一个I/O设备(如UART)。本来库函数fputc()是把字符输出到调试器控制窗口中去的,但用户把输出设备改成了UART端口,这样一来,所有基于fputc()函数的printf()系列函数输出都被重定向到UART端口上去了。 下面是实现fputc()重定向的一个例子:
externvoidsendchar(char*ch); intfputc(intch,FILE*f)
,/*e.g.writeacharactertoanUART*/ chartempch=ch;
sendchar(&tempch);
returnch;
,
这个例子简单地将输入字符重新定向到另一个函数sendchar(),sendchar()假定是一个另外定义的串口输出函数。在这里,fputc()就好像目标硬件和标准C库函数之间的一个抽象层。
在C语言库函数中禁用Semihosting
在一个独立的嵌入式应用程序中,应该不存在SemihostingSWI操作。因此,用户必须确定在所有调用到的库函数中没有使用Semihosting。为了保证这一点,在程序中可以引进一个符号关键字_use_no_semihosting:
在C代码中,使用#prgrama #pragmaimport〈_use_no_semihosting_swi〉 在汇编程序中,使用IMPORT
IMPORT_use_no_semihosting_swi 这样,当有使用SWI机制的库函数被连接时,连接器会进行报错: Error:Symbol_semihosting_swi_guardmultiplydefined
为了确定具体是哪一个函数,连接时打开-verbose选项。这样在结果信息输出时,该库函数上将有一个_I_use_semihosting_swi的标记。
Loadingmembersys_wxit.ofromc_a_un.1. Definition:_sys_exit
Reference:_I_use_semihosting_swi 用户必须要把这些函数定义成自己的执行内容。
有一点需要注意,连接器只能报告库函数中被调用的Semihosting,对用户自定义函数中使用的Semihosting则不会报错。
根据目标硬件定制存储器映射
分散装载(Scatlerloading)
在实际的嵌入式系统中,ADS提供的缺省存储器映射是不能满足要求的。用户的目标硬件通常有多个存储器设备位于不同的位置,并且这些存储器设备在程序装载和运行时可能还有不同的配置。
Scattertoading可以通过一个文本文件来指定一段代码或数据在加载和运行时在存储器中的不同位置。这个文本文件scatterfile在命令行中由-scatter开关指定,例如:
armlink_scatterscat.scffilel.ofile2.0 在scatterfile中可以为每一个代码或数据区在装载和执行时指定不同的存储区域地址,Scatlertoading的存储区块可以分成二种类型:
装载区:当系统启动或加载时应用程序的存放区。
执行区:系统启动后,应用程序进行执行和数据访问的存储器区域,系统在实时运行时可以有一个或多个执行块。
映像中所有的代码和数据都有一个装载地址和运行地址(二者可能相同也可能不同,视具体情况而定)。在系统启动时,C函数库中的__main初始化代码会执行必要的复制及清零操作,使应用程序的相应代码和数据段从装载状态转入执行状态。
1.scatter文件语法
scatter文件是一个简单的文本文件,包含一些简单的语法。
My_Region0x00000x1000
{
thecontextofregion
}
每个块由一个头标题开始定义,头中至少包含块的名字和起始地址,另外还有最大长度和其他一些属性选项。块定义的内容包括在紧接的一对花括号内,依赖于具体的系统情况。
一个加载块必须至少含有一个执行块;实践中通常有多个执行块。 一个执行块必须至少含有一个代码或数据段;这些通常来自源文件或库函数等的目标文件;通配符号*可以匹配指定属性项中所有没有在文件中定义的余下部分。
2.简单分散加载样例
图8所示样例中,只有一个加载块,包含了所有的代码和数据,起始地址为0。这个加载块一共对应两个执行块。一个包含所有的RO代码和数据,执行地址与装载地址相同;同时另一个起始地址为0x10000的执行块,包含所有的RW和ZI数据。这样当系统开始启动时,从第一个执行块开始运行(执行地址等于装载地址),在执行过程中,有一段初始化代码会把装载块中的一部分代码转移到另外的执行块中。
下面是这个scatter描述文件,该文件描述了上述存储器映射方式。LOAD_ROM0x4000
,
EXE_ROM0x00000x4000;Rootregion ,
*〈+RO〉;Allcodeandconstantdata ,
RAM0x100000x8000
,
*〈+RW,+ZI〉;Allnon-constantdata
,
,
3.在分散文件中放置对象
在大多数应用中,并不是像前例那样,简单地把所有属性都放在一起,用户需要控制特定代码和数据段的放置位置。这可以通过在scatter文件中对单个目标文件进行定义实现,而不是只简单地依靠通配符。
为了覆盖标准的连接器布局规则,我们可以使用+FIRST和+LAST分散加载指令。典型的例子是在执行块的开始处放置中断向量表格:
LOAD_ROM0x00000x4000
,
EXEC_ROM0x00000x4000
,
vectors.o〈Vect,+FIRST〉
*〈+RO〉
,
;moreexecregions...
,
在这个scatter文件中,保证了vextors.o中的Vect域被放置于地址0x0000。 4.RootRegion(根区)
根区是一个执行块,它的加载地址与执行地址是一致的。每个scatter文件至少有一个根区。分散加载有一个限制:创建执行块的代码和数据(即完成复制和清零的代码和数据)无法自行复制到另一个位置。因此,在根区中必须含有下面的部分:
_main.o,包含复制代码/数据的代码;
连接器输出变量,,Table和ZISection,,Table,包含被复制代码/数据的地址。 由于上面两个部分的属性是只读的,因此他们被*〈+RO〉通配符语法匹配。如果*〈+RO〉被用在了非根区中,则在根区中必须显式地指明另一个RO区域。
下面是一个例子:
LOAD_ROM0x00000x4000
,
EXE_ROM0x00000x4000;rootregion ,
+RO〉;copyingcode _main.o〈
*〈Region,,Tabl0e〉;RO/RWaddressestocopy *〈ZISection,,Table〉;ZIaddressestozero ,
RAM0x100000x8000
,
*〈+RO〉;allotherROsections
*〈+RW,+ZI〉;allRWandZIsections
,
,
(待续) 在下期中将更详细介绍利用scatter文件进行存储器配置的方法,以及系统启动和初始化的过程和存储器映射变化关系。
上期主要介绍了基于ARM的嵌入式系统软件开发中,怎样来对必要的C库函数进行移植和重定向,以及如何根据不同的目标存储器系统进行程序编译和连接设置。本期介绍程序中的存储器分配和如何根据设置正确初始化系统。
放置堆栈和heap
Scatterloading机制提供了一种指定代码和静态数据布局的方法。下面介绍如何放置应用程序的堆栈和heap。
* _user_initial_stackheap重定向
应用程序的堆栈和heap是在C库函数初始化过程中建立起来的。可以通过重定向对应的子程序来改变堆栈和heap的位置,在ADS的库函数中,即_user_initial_stackheap()函数。
_user_initial_stackheap()可以用C或汇编来实现,它必须返回如下参数: r0:heap基地址;
r1:堆栈基地址;
r2:heap长度限制值(需要的话);
r3:堆栈长度限制值。
当用户使用分散装载功能的时候,必须重调用_user_initial_stackheap(),否则连接器会报错:
Error: L6218E: Undefined symbol Image,,ZI
,,Limit (referred from sys_stackheap.o)
*存储器模型
ADS提供了两种实时存储器模型。缺省时为one-region,应用程序的堆栈和heap
位于同一个存储器区块,使用的时候相向生长,当在heap区分配一块存储器空间时需要检查堆栈指针。另一种情况是堆栈和heap使用两块独立的存储器区域。对于速度特别快的RAM,可选择只用来作堆栈使用。为了使用这种two-region模型,用户需要导入符号use_two_region_memory,heap使用需要检查heap的长度限制值。
对这两种模型来说,缺省情况下对堆栈的生长都不进行检查。用户可以在程序编译时使用 -apcs/swst 编译器选项来进行软件堆栈检查。如果使用two-region模型,必须得在执行_user_initial_stackheap时指定一个堆栈限制值。
图9 重定向_user_initial_stackheap()
图10 基本初始化过程
图11 ROM/RAM重定向和映射
表1
系统复位和初始化
目前情况,一般假设程序从C库函数的初始化入口_main开始执行。实际上,所有的嵌入式程序在启动时都要执行一些系统级的初始化操作。在此讨论这方面的内容。
初始化过程
图10中显示了一个基于ARM的嵌入式系统的基本初始化过程。可以看到,在_main之前加入了一个复位处理模块reset handler,它在系统上电复位时立即启动。标识为,sub,,main的新代码块在进入主程序之前执行。
复位处理模块reset handler通常是一小段汇编代码,在系统复位时执行。它至
少完成应用程序中使用到的所有处理器模式的堆栈初始化工作。对于含有本地存储器系统的内核(比如含cache的ARM内核),配置工作也必须在这一段初始化过程中完成。当完成系统初始化之后,通常程序会跳向_main,开始C库函数的初始化过程。
系统初始化过程一般还包括另外一些内容,中断使能等,这些大多安排在C库函数的初始化完成之后执行。,sub,,main()完成这部分功能。 向量表(vector table)
所有的ARM系统都有一张中断向量表当出现异常需要处理时,必须调用向量表。向量表一般要位于0地址处。
表2
表3
表4
表5
表6
表7
表8
表9
表10
存储器配置
*ROM/RAM重定向
当系统启动的时候,为了保证0地址处有正确的启动代码存在,需要非易失性的存储器。
一种简单的方法,就是把系统0x0000开始的一块地址分配给ROM。其缺点是,由于ROM的访问速度比RAM慢很多,当执行中断响应需要从中断向量表跳转时,会给系统性能带来损失;同时,在ROM中的向量表内容也不能被用户程序动态修改。
另外一种可行的方案如图11所示。ROM位于地址0x1000开始的地方,但是在系统复位时又被存储器控制器映射到0x0000地址处。这样当系统启动之后,在地址0x0000看到的是ROM,系统执行这块ROM中的启动代码,启动代码跳转到真正的ROM的地址,并让存储器控制器移除对ROM的地址映射。这时0x0000地址处的存储器又恢复回了RAM。__main中的代码把向量表copy到0x0000处的RAM中去,使得异常时能被正确响应。
表1为ARM汇编中执行ROM/RAM重定向和映射的一个例子。它以ARM公司的Integrator平台为基础的,该方法适用于类似ROM/RAM重定向方法的所有平台。第一条指令完成从ROM的映射地址(0x00000)到真实地址的跳转。地址标号instruct_2是ROM的真实地址(0x180004)。然后通过设置Integrator平台上的相应控制寄存器,移除ROM的地址映射。代码在系统一启动就被执行。所有关于地址重定向/映射的操作必须在C库函数初始化之前完成。
*本地存储器配置
许多ARM处理器都有片上存储器系统,如cache和紧密耦合存储器(TCM)、存储器管理单元(MMU)或存储器保护单元(MPU)。这些设备都要在系统初始化过程中正确配置,并且有一些特殊的要求需要考虑。
由前文可知,_main中的C库函数初始化代码负责程序运行时的存储器系统设置。因此,整个存储器系统本身必须得在__main之前完成初始化工作,如MMU或MPU必须在reset handler里面完成配置。
紧密耦合存储器(TCM)的初始化同样须在_main之前完成(通常在MMU/MPU之前),因为一般程序都需要把代码和数据分散装入TCM。需要注意的是当TCM被使能后,不再访问被TCM屏蔽的存储器。
关于cache的一致性问题,如果cache在_main之前使能的话,那么当_main里
面进行从装载区到执行区的代码和数据拷贝时(因为在拷贝过程中指令和数据在本质上都是被当作数据处理),指令会出现在数据缓冲区。避免此问题的方法是在C库函数初始化完成后再使能cache。
*Scatter loading与存储器配置
无论是通过ROM/RAM重定向还是MMU配置的方法,如果系统在启动和运行时存储器分布不一致,scatterloading文件中的定义就要按照系统重定向后的存储器分布情况进行。
以上文ROM/RAM重定向为例:
LOAD_ROM 0x10000 0x8000
{
EXE_ROM 0x10000 0x8000
{
reset_handler.o (+RO, +FIRST) ...
}
RAM 0x0000 0x4000
{
(+RO, +FIRST) vectors.o
...
}
}
装载区LOAD_ROM被放置在0x10000处,代表了重定向之后代码和数据的装载地址。
堆栈的初始化
程序中可能用到的处理器模式,都需要定义一个堆栈指针。
在表2中,堆栈位于stack_base标识的地址中。这个符号可以是存储器系统中的一个直接地址,也可以在另外的汇编文件中定义,由scatter文件来定义分配地址。表2代码为FIQ和IRQ模式各分配了一个256字节的堆栈,用户可以用同样的方法为其他模式也分配堆栈。最简单的方法就是进入相应的模式,然后为SP寄存器指定相应的值。如果想使用软件堆栈检查,还必须指定一个堆栈长度限制值。
堆栈指针和堆栈限制的数值会作为参数自动传递到C库函数的初始化代码__user_initial_stackheap中,在__user_initial_stackheap中不应该修改这些值。
硬件初始化 ,sub,,main()
一般来说,应该把所有的系统初始化代码与主应用程序分离开来,但是有几个例外,比如cache和中断的使能,需要在C库函数初始化之后执行。 表3代码显示了如何使用 ,sub和 ,supper 。连接器把呼叫main()的函数替换成呼叫,sub,,main(),完成cache和中断的使能,并最终跳向main()。
执行模式考虑
为主应用程序选择一个处理器执行模式非常重要,这取决于系统的初始化代码。
许多在启动过程中使用到的功能,如MMU/MPU的配置、中断的使能等,只能在特权级模式下进行。如果需要在特权极模式下运行自己的应用程序,只要在退出初始化过程之前改变到相应的模式就行了,没有其他任何问题。 如果使用user模式,必须保证所有只能在特权模式下执行的功能完成之后,才能进入user模式。因为system模式和user模式使用相同的寄存器组,
handler应该从system模式退出,_user_initial_stackheap在systemreset
模式下完成应用程序堆栈的初始化。这样在处理器进入user模式后,所有的堆栈空间都已经被正确设置好了。
对存储器布局的进一步考虑
在scatter文件中分配硬件地址
虽然可以在一个scatter文件中描述代码和数据的分散布局,但是目标硬件中的外设寄存器,堆栈和heap配置仍然直接采用硬件地址在程序源代码中进行设置。如果把所有存储器地址相关的信息都在scatter文件中进行定义,避免在源文件中引用绝对硬件地址,对程序的工程化管理是有大好处的。
*在scatter文件中定义目标外设地址
通常外设寄存器的地址在程序文件或头文件中定义,也可以声明一个结构类型指向外设寄存器,结构的地址定位在scatter文件中完成。
举例来说,目标定时器上有2个32位的寄存器,可以用表4来映射这些寄存器。为了把结构放置在指定的存储器地址上面,创建一个新的执行区(见表5)。
。 scatter文件便把timer_regs结构定位在了地址0x40000000注意,在启动过程当中这些寄存器的内容不需要清零,改变寄存器的内容可能影响系统状态。在执行区上加UNINIT属性可以防止ZI数据在初始化过程中被清零。 在scatter文件中分配堆栈和heap
在许多情况下,用scatter文件来定义堆栈和heap的地址会带来一些好处,主要有:所有的存储器分配信息集中在一个文件里;改变堆栈和heap的地址只要重新连接就行了,不需要重新编译。
*显式地放置符号
在ADS1.2环境下,这是最简单的方法。在前文中引用过2个符号stack_base和heap_base,这2个符号在汇编模块中创建,在scatter文件中各自的执行区里定位(见表6)。
表7文件中,heap基地址定位在0x20000上,堆栈基地址位于0x40000。现在heap和堆栈的位置就可以非常方便地进行编辑了。
*使用连接器产生的符号
这种方法需要在目标文件中指定好heap和堆栈的长度。这在一定程度上减弱了本节开头描述的两个优点。
首先在汇编源程序中定义heap和堆栈的长度。关键词SPACE用来保留一块存储器空间,NOINT则可以阻止清零操作(见表8)。注意在这里的源文件中并不需要地址标号。
然后这些部分就可以在scatter文件中对应的执行区里定位了(见表9)。连接器产生的符号指向每一个执行区的基地址和长度限制,这些符号可以被_user_initial_stackheap调用的重定向代码使用。在代码中使用DCD来给这些值定义更有意义的名字,可以增强代码的可读性(见表10)。
文件把heap基地址定位在0x15000,堆栈地址定位在0x4000。Heap和堆栈的位
置可以通过编辑对应执行区的地址方便地改变