打印

ARM系统编程

[复制链接]
楼主: xinzha
手机看帖
扫描二维码
随时随地手机跟帖
21
顶起!

使用特权

评论回复
22
molongkuangshi| | 2012-9-14 15:39 | 只看该作者
顶起!

使用特权

评论回复
23
xinzha|  楼主 | 2012-9-14 17:18 | 只看该作者
本帖最后由 xinzha 于 2013-7-7 09:59 编辑

对齐问题
在ARM7,即arm v4中,规定如果访问int类型数据时给出的指针地址的低2位不为0的话,系统会自动将低两位的1抹平,即强制四字节对齐,这样的问题就是如果你的数据偏偏就是不对齐的,cpu拿到的数据就是不正确的,bug由此产生。
在ARM9(我接触的是arm926ejs)中,如果访问int型数据,低2位不为0,那么cpu直接挂住,一个data abort。
在ARM11之后的版本中支持了非对齐访问,是在总线上拆分然后拼接来实现的,也就是说如果你访问int时给出的地址不是4字节对齐,那么总线上会出现两个int访问,然后把数据拼接起来送给cpu,这些对于cpu是不可见的,但是会导致速度下降,总线占用率上升。此功能可以通过修改cp15中来关闭,实现跟以前版本一样的对齐方式。
需要说明的是这里的int是4字节而不是2字节。另外一点是对齐问题并不是专指四字节对齐,很多人都会犯这样的错误,认为只有在访问int时没有做到四字节对齐才会出问题。而实际上对齐指的是数据边界对齐,也就是说long long数据要8字节对齐,int要4字节对齐,short要2字节对齐,byte自然是怎么对都齐了。
用RVDS2.2版本如果用到了long long类型的数据就要注意了,全局变量没有问题,如果是用做临时变量,编译器只给做到了四字节对齐没有做到把字节对齐,遇到某些cp15的设置就会导致非对齐访问的错误。

使用特权

评论回复
24
木瓜498283984| | 2012-9-17 11:12 | 只看该作者
顶顶顶,好

使用特权

评论回复
25
xinzha|  楼主 | 2012-9-17 17:36 | 只看该作者
公司网速极其不给力,晚上回家更新。

使用特权

评论回复
26
xinzha|  楼主 | 2012-9-17 22:09 | 只看该作者
本帖最后由 xinzha 于 2013-7-7 10:05 编辑

工作模式
主流的ARM核心会包含很多种模式,除了定位为MCU的Cortex-M系列。这些模式包括如下几种:
1、中断模式(IRQ)
2、快速中断模式(FIQ),此模式在ARM7中并未实现
3、用户模式(USR)
4、系统模式(SYS)
5、软中端模式(SVC)
6、数据终止模式(ABT),包含了数据终止和预取中止
7、未定义指令模式(UNDEF)
8、监视模式(Monitor),这个模式是比较新的架构中才添加的,应用场景比较特殊,我还没有完全搞懂。
  系统复位后CPU的模式是SVC模式,一般情况下会在切到用户程序之后切换到USR模式,但是最近有个同事跟我讲三星2440的例程中CPU是始终工作在SVC模式下的,这种方式比较古怪,但是也无所谓,能安全地实现功能就行。
  保护模式的操作系统下,一般的原理是用户程序工作在USR模式下,当你要做系统调用的时候就激起软中断,软中断服务在判断你的请求合法之后调用内核函数,如果不合法就置上错误号,返回给用户程序。内核中的大部分函数工作在SYS模式下,USR模式和SYS模式公用相同的寄存器,只是特权等级不一样,所谓的不一样也主要是SYS模式下可以操作CPSR,而usr模式不允许。另外USR和SYS模式下是没有SPSR的,因为这两种模式不是异常模式,是常规的操作模式。(这段描述是我过去的理解,现在要在我读完内核的相应部分之后再确定)
  IRQ和FIQ模式就不用再多说了,做过单片机的人会觉得很熟悉。
  ABT模式就是当发生了地址或者数据异常时进入的模式,这种模式既可以做为数据的保护方式(当你有MMU,MPU时),也可以实现虚拟地址的扩展,还有可以帮助我们保存bug现场,方便调试。一般裸奔或者非保护式系统中,ABT模式的处理就是死循环,因为在这类系统中如果发生了ABT就是不可恢复的错误。
  UNDEF是指CPU遇到了无法解析的指令,发生这种状况一般来说有以下几种场景:
1、在ARM/THUMB混编情况下,ARM状态下执行了THUMB指令,或者THUMB状态下执行了ARM指令。
2、堆栈异常导致PC返回值异常,程序跑飞,执行了不存在的指令。
3、函数指针错误
4、有意的指令扩展,这种就需要在UNDEF异常处理中添加自己的指令扩展处理程序。
5、代码区被异常改写。

使用特权

评论回复
27
xinzha|  楼主 | 2012-9-18 10:17 | 只看该作者
本帖最后由 xinzha 于 2013-7-7 10:26 编辑

各种异常模式下的spsr是用来存储进入异常时前一种模式的cpsr,而且只有真正发生异常时才会把前一种模式的cpsr写入spsr,通过修改cpsr来切换模式并不会导致spsr的改写。
比如说如果在usr模式下产生了中断,就会在执行完当前指令之后进入到中断模式,后续流水线中的指令被抛弃,转而执行ISR,此时cpu会自动将用户模式的cpsr写入到中断模式的spsr中,以作为从中断模式退出时恢复cpsr的备份。
而在arm体系结构中,除了cortex-M系列,其他的堆栈操作都是由软件来做的,虽然你在写c代码的时候没有保存堆栈的操作,可你要记住,这是编译器帮你完成了。cortex-m系列在发生异常的时候是由硬件保存的8个寄存器,保存顺序也是体系结构预先定义好的,至于函数调用时什么样子我给忘了,cortex-m系列接触不多,只是帮人解了几个bug。

使用特权

评论回复
28
dfhf2007| | 2012-9-18 16:01 | 只看该作者
持续关注。。

使用特权

评论回复
29
xinzha|  楼主 | 2012-9-19 10:16 | 只看该作者
本帖最后由 xinzha 于 2013-7-7 10:31 编辑

Cache
Cache逐渐成为现代CPU中越来越重要的角色,不过到目前为止,依然有很多常用的ARM芯片是不带cache的,所以我们在这里只对cache做个简要的介绍。
Cache从其本质来说也是SRAM,只不过是离cpu最近的SRAM。Cache与外部(这里的内外是针对CPU的kernel而言,而不是针对SoC)SRAM最大的不同不在于速度,而在于它与cpu的交互方式,cpu访问cache的时候是直接通过其内部的cache控制器,而访问外部SRAM时首先要向总线发出请求,当总线仲裁器允许CPU访问总线后,CPU发出地址信息并等待SRAM返回数据。关于总线的详细信息可以到ARM官网上去下载AMBA的文档来学习。现在最新的版本可能是AMBA 3.0,加入了AXI总线的部分。
TCM和cache并列为ARM核心的一级memory,TCM的设计为ARM提供了另外一种快速访问的方式,并且TCM的速度是恒定的,不像cache会有invalidate和flush等操作。在访问cache前你并不知道你所要访问的数据是否在cache中,而在访问TCM的时候,你所需要的东西一定在TCM内,这是因为你在配置TCM的时候已经设置好了它的地址范围,只有在访问这一段地址的时候才会访问TCM。TCM的操作是系统启动之后将一段你认为对性能影响最大的数据或者代码拷贝到TCM中,并配置TCM控制器,使以后所有针对这段数据或者代码的访问都去直接访问TCM,而不用再费劲地请求总线并访问低速RAM。
Cache和TCM的操作都是通过cp15寄存器指令来实现的,指令格式略显繁琐,而且无法用c语言来实现,不过还好,照着arm手册依样画葫芦就行了。

使用特权

评论回复
30
xinzha|  楼主 | 2012-9-19 13:37 | 只看该作者
本帖最后由 xinzha 于 2013-7-7 10:47 编辑

向量表
预备知识的最后我们需要讲一下向量表,做过单片机的朋友对向量这个词应该不陌生,也就是发生了中断或者异常的情况下,cpu(我们认为MCU也是cpu)会把pc直接指向某个固定的地址,在那个地址有个跳转语句或者一小段处理(比如说mips为每种异常提供了128字节的处理空间)。而你的系统所有的异常处理的那个地址表也就是我们所说的向量表,ARM的向量表有两种选择,0x0起始或者0xffff0000起始,选择哪个地址作为可以通过修改CP15中的相应控制位来实现。一个例外就是Cortex-M3(M0和M4没接触过),它把很多传统ARM中CP15的system control寄存器拿到了直接可寻址的地址空间,可以像操作普通寄存器那样修改向量表起始地址。
至于为什么允许修改向量表地址,这个问题就跟操作系统的实现相关了,在linux和windows中,0地址位于用户空间,意味着整个向量表都在用户空间的起始地址,当有异常发生时,cpu自动就去用户空间拿指令,这样就会允许用户空间的代码在特权模式下执行(所有异常模式都是特权模式),这样就给了恶意代码机会,通过简单地手段就能获得整个系统的控制权,而0xffff0000处于内核空间就不会导致这样的问题。在保护式操作系统(linux/windows)的应用环境中,cpu启动时首先会默认0地址是向量表起始,而在系统启动前,启动代码会把向量表切换到高位,以保证操作系统的实现。

使用特权

评论回复
31
dogsun88| | 2012-9-19 20:17 | 只看该作者
勇哥,跟踪中,孙志!!!

使用特权

评论回复
32
dogsun88| | 2012-9-19 20:50 | 只看该作者
关于延时槽大家可以看一下这个地址:
http://blog.chinaunix.net/space. ... blog&id=3146408

做个广告,呵呵!!

使用特权

评论回复
33
xinzha|  楼主 | 2012-9-20 10:03 | 只看该作者
勇哥,跟踪中,孙志!!!
dogsun88 发表于 2012-9-19 20:17


:lol

使用特权

评论回复
34
xinzha|  楼主 | 2012-9-20 10:07 | 只看该作者
ARM的基础知识我们先讲这些,其他需要扩展的内容或者回头想到落下的东西再补充。
我们从复位开始讲起,复位又被分为很多种,什么上电复位,重启复位,冷复位热复位的,我们不考虑那么多,只从最根本的上电复位说,其他类型的当我们对cpu熟悉之后都可以自然地延伸。
复位之后pc指向一个固定的地址,也就是向量表的起始地址0x0,这个地址必须是有确定的内容-启动代码,为什么不用另外一种说法-这里必须是NV memory呢,因为有些公司确实实现了0地址是SRAM的做法,他们为了允许客户不使用昂贵的ROM或者NOR flash, SOC中用逻辑实现了NAND flash的驱动,上电之后立即从NAND中读出前面几K的内容拷贝到SRAM的0地址,这样cpu去0地址取到的实际就是预先写到NAND中的内容而不是随机的SRAM复位内容。个人猜测他们的实现机制是这样,上电之后,SOC的逻辑将cpu的复位拉住,使cpu始终处于复位状态,同时去拷贝代码,当代码拷贝完成后松开cpu复位线,这时就保证了cpu能够读到确定内容。

使用特权

评论回复
35
xinzha|  楼主 | 2012-9-20 10:13 | 只看该作者
下面是一段很典型的简单嵌入式系统的启动代码,没有MMU,没有cache,也没有外接RAM,所有代码数据都在SoC自带的SRAM中。我会将其中的重点一一讲解,而且启动的代码的讲解要配合上分散加载文件,才能显得更清晰。

这是scatter文件的内容,分散加载的具体语法和用法请参阅ARM链接器的官方文档:
ROM_LOAD 0x20000000  #说明加载域位于0x2000 0000
{
    ROM_EXEC 0x20000000  #说明根执行域也是从0x2000 0000开始
    {
        vectors.o (Vect, +First)  #vectors.o是生成文件的起始部分,保证向量表在0x0地址
        * (+RO)              #剩下的RO段放在vector的后面,包括代码和只读数据
    }

    RAM_1 +0
    {
        * (+RW,+ZI)           #非零全局数据和bss段放在RO段之后
    }
   
    HEAP +16
    {
        init.o(Heap)          #这段是HEAP段
    }
   
    STACK 0x2002FFFC
    {
        init.o(MyStacks)      #系统RAM的最高地址是0x2002FFFF,所以stack从此向下
    }
}
根执行域必须同加载域是重合的,并且是处于开始位置的可执行代码,因为要用根执行域的代码来完成分散加载部分的其他工作。

使用特权

评论回复
36
fkepynn| | 2012-9-20 11:08 | 只看该作者
关注,顶!

使用特权

评论回复
37
xinzha|  楼主 | 2012-9-21 10:51 | 只看该作者
这是Init.s的内容:        
    EXPORT  Reset_Handler ;这里要让vectors.s看到,所以要export
Reset_Handler
  ; 如果需要做地址空间转换的话,就会执行这段代码。为什么做地址空间转换呢,至少有两个理由的,首先 0x0地址一般是bootloader的地址,bootloader会用某种方式将runtime image加载进内存,一切处理好之后,bootloader会把控制权交给runtime image进行二次复位,两次复位的目的和执行的代码不一样,所以需要做一次地址空间转换。有的代码会做一些通用性处理让两次复位执行同样的函数,但是同样会有灵活性以及执行速度的需求要求向量表在SRAM中,所以在设计CPU时对于这里要仔细考虑好,加了没有任何害处,不加就可能日后造成困扰。
    IF :DEF: ROM_RAM_REMAP

        LDR     pc, =Instruct_2
        
Instruct_2        

; Remap by setting Remap bit of the CM_ctl register
        LDR     r1, =CM_ctl_reg
        LDR     r0, [r1]
        ORR     r0, r0, #Remap_bit
        STR     r0, [r1]
ENDIF
这里是个比较绕的过程,我尽量解释,如果还是有不懂的地方,可以继续提出来讨论。
比如说这段启动代码在一块ROM中,如果需要做地址空间转换,IC design的人会在系统复位后赋予这个ROM两个地址,一个0x0,另外一个是类似于0x4000 0000这样的其他非零地址,第二个地址在做地址空间转换前后都可以访问,0x0只能在地址空间转换之前访问。当链接的时候,我们告诉链接器,这段代码在0x4000 0000,然后我们把这段代码固化到ROM中,这样在复位时,pc指向0取到的是这段代码,而当执行完LDR  pc,=Instruct_2之后,pc已经变成了0x4000 0004,而这个地址也是可以正确访问ROM的,这之后再去写地址转换寄存器也不会导致读不到正确的ROM内容了。        
可以想象一下,如果不做前面所说的这个似乎多余的步骤,PC始终是按照0,4,8,c这个顺序执行下去,做完地址空间转换,拿的就是RAM的内容,必死无疑。

使用特权

评论回复
38
cc389518| | 2012-9-21 13:59 | 只看该作者
谢谢分享。。。。。。。。。。。。。。。。

使用特权

评论回复
39
xinzha|  楼主 | 2012-9-23 21:07 | 只看该作者
; --- Initialize stack pointer registers
        BL      InitStack        
        IMPORT  __main
; --- Now enter the C code
        B       __main   ;
InitStack就不用废话了,操作cpsr切换到各种模式,然后设置各种模式下的堆栈,就可以允许跑c代码了。设置完之后调用ARM提供的__main库函数,如果你使用了这个库函数,那么你必须保证你的c代码中有main函数,__main在做完分散加载,内存初始化,代码拷贝等工作之后默认跳到main函数。因为我只用过ARM官方的编译链接器,所以不知道其他人是怎么做的,不过我估计大概意思都差不多,以前用MIPS的时候用的GCC,也一样要自己写好LD(gcc的分散加载描述文件),不过当时分散加载,代码拷贝和内存初始化都是自己用汇编完成的,不清楚gcc是否也提供了类似于arm这样方便的工具。为什么用B __main呢,因为这个函数是不会返回的,直接把控制权交给main(),而不是交还给调用它的初始化代码。

InitStack   

        MOV     R0, LR
;Build the FIQ stack
        MSR     CPSR_c, #0xd1
        LDR     SP, StackFiq
;Build the IRQ stack
        MSR     CPSR_c, #0xd2
        LDR     SP, StackIrq
;Build the DATAABORT stack
        MSR     CPSR_c, #0xd7
        LDR     SP, StackAbt
;Build the UDF stack
        MSR     CPSR_c, #0xdb
        LDR     SP, StackUnd
;Build the SVC stack
        MSR     CPSR_c, #0xd3   ;/*uCOS starts with SVC mode */
        LDR     SP, StackSvc
;Build the SYS stack
;        MSR     CPSR_c, #0xd3 ;
;        LDR     SP, =StackUsr
;Return
        MOV     PC, R0


;must be 8 byte aligned
FIQ_STACK_LEGTH         EQU         128
IRQ_STACK_LEGTH         EQU         2048
ABT_STACK_LEGTH         EQU         128
UND_STACK_LEGTH        EQU         128
SVC_STACK_LEGTH         EQU         2048

StackAbt        DCD         top_of_stack -UND_STACK_LEGTH - IRQ_STACK_LEGTH-FIQ_STACK_LEGTH-SVC_STACK_LEGTH
StackSvc        DCD         top_of_stack -UND_STACK_LEGTH - IRQ_STACK_LEGTH-FIQ_STACK_LEGTH
StackFiq         DCD         top_of_stack -UND_STACK_LEGTH - IRQ_STACK_LEGTH
StackIrq         DCD          top_of_stack-UND_STACK_LEGTH
StackUnd        DCD         top_of_stack  

    AREA    MyStacks, DATA, NOINIT
top_of_stack SPACE 4   ;此处的space定义可以不用管,只是为了确定top_of_stack位置

    EXPORT bottom_of_heap
    AREA    Heap, DATA, NOINIT
bottom_of_heap    SPACE   1 ;道理同上

END

以上部分我给大家看的是runtime image部分的启动代码,下面再看一下bootloader部分的启动代码,略微有些差别。
因为绝大多数情况下,bootloader中我们不想启用中断 ,也绝不会去处理异常,所以在bootloader中就没有去写中断向量表。但是,没有中断向量表并不意味着cpu就不去响应异常,如果你的程序中有bug,比如说读写了不存在的地址或者函数指针错误,这样一定还是会激起异常,cpu照常去0xc(prefetch abort)或者0x10去拿指令执行,即使这里已经不是中断向量表了,但是cpu依然按照内置的逻辑去执行,所以bootloader要小心再小心,那种固化在SoC内部的bootloader,一旦出问题,你生产出来的芯片就是块石头。

使用特权

评论回复
40
dong_abc| | 2012-9-23 22:42 | 只看该作者

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则