ARM系统编程

[复制链接]
楼主: xinzha
手机看帖
扫描二维码
随时随地手机跟帖
xinzha|  楼主 | 2012-9-25 11:40 | 显示全部楼层
有一个问题没有完全搞明白,先暂停更新,把问题搞明白再说。

使用特权

评论回复
jiangfuquan999| | 2012-9-26 16:14 | 显示全部楼层
期待跟新。

使用特权

评论回复
xinzha|  楼主 | 2012-9-26 20:33 | 显示全部楼层
下面是一款bootloader的scatter文件,scatter的解析可以参照上一个例子。

LOAD_ROM 0x10000000 0x002000
{
    EXEC_ROM 0x10000000 0x002000
    {
        init.o (init, +First) #可以看到,我们在这里没有vectors.o,因为不需要
        * (+RO)   
    }

    RAM 0x30080000 0x7000
    {
        * (+RW,+ZI)
    }

    STACKS 0x30088000 0x1000
    {
        stack.o (+ZI)
    }
}

这是相应的init.s的内容:
    ENTRY

; --- Perform ROM/RAM remapping, if required
    IF :DEF: ROM_RAM_REMAP

; On reset, an aliased copy of ROM is at 0x0.
; Continue execution from 'real' ROM rather than aliased copy
        LDR     pc, =Instruct_2
Instruct_2        
        LDR     r1, =Remap_ctl_reg
        LDR     r0, =Remap_value
        STR     r0, [r1]
    ENDIF

Reset_Handler

        IMPORT  top_of_stacks       ; defined in stack.s and located by scatter file
        LDR     r0, =top_of_stacks

        MSR     CPSR_c, #Mode_SVC:OR:I_Bit:OR:F_Bit ; No interrupts
        SUB     sp, r0, #Offset_SVC_Stack
               
        IMPORT  load_firmware
        B       load_firmware  ;
END

我们可以看出来,基本上runtime的启动文件基本相同,只是最后跳转的地方不一样。但是,这只是假象,由于scatter文件的不同,两个工程生成的可执行文件有着根本的不同,runtime那个在0地址储存的是向量表,供cpu在发生异常时取用,而bootloader的在0地址直接就是顺序执行的初始化语句,因为bootloader只关心一件事,把runtime image以某种方式加载进来,如果有需要的话还要解压,当这些做完之后再把控制权交给runtime image中的可执行域就行了,至于以后的异常处理等等问题不在它考虑范围内。
还有一个不同是我们用了B load_firmware,而不是B __main,这样做的一个好处是链接时就不会把ARM的库函数包进来,能节省1K左右的ROM空间,而坏处就是你要仔细对待你的代码,因为缺少了一个初始化函数,所以内存中的值就没有人给你赋值,也就是说你的代码中不允许有带初值的全局变量,这一点切记,很多人在这上面吃亏。

使用特权

评论回复
yanqiu3526| | 2012-9-27 22:38 | 显示全部楼层
值得学习借鉴

使用特权

评论回复
xinzha|  楼主 | 2012-9-28 09:12 | 显示全部楼层
放假旅游,回来更新。

使用特权

评论回复
1189594| | 2012-9-28 11:34 | 显示全部楼层
等待    关注!!

使用特权

评论回复
koovor| | 2012-9-29 17:25 | 显示全部楼层
:handshake
      谢谢楼主的分享。

使用特权

评论回复
Fedora| | 2012-10-15 00:39 | 显示全部楼层
马克

使用特权

评论回复
xinzha|  楼主 | 2012-10-15 21:34 | 显示全部楼层
从bootloader到main函数之间一般要经历如下的步骤:
1、        地址空间转换(可选)
2、        初始化供bootloader的c代码使用的栈空间
3、        等待从主机侧下载镜像文件或者直接从自己挂载的存储器拿。
4、        解析校验镜像,没问题的话将它放到预定的位置,有需要的话还得解压。
5、        将pc指向镜像文件的入口,即镜像文件的reset handler,二次复位。
6、        进行板级初始化的第一步骤,包括硬件,堆栈等等。
7、        控制权交给分散加载解释函数,完成软件系统的初始化。
8、        模式切换(从svc到sys,最好有这一步)
9、        控制权交给main函数。

需要注意的是,在一般情况下,整个这个过程中断是关闭的,除非你有特殊需要。记住,这个部分越简单越直观就越好。

现在开始进入大家都熟悉的部分,大多数人工程的开始点,main函数,在main函数中主要进行以下工作:
1、        完成硬件初始化
2、        软件数据结构的初始化
3、        任务初始化
4、        打开中断。

使用特权

评论回复
xinzha|  楼主 | 2012-10-17 20:44 | 显示全部楼层
关于打开中断要多说两句,ARM中断的使能必须至少有两级(Cortex-M除外,不属于传统ARM),中断控制器的使能和CPSR中I位的使能,而CPSR的使能才是最根本的使能,很多新手会卡在这儿,不停地问为啥我初始化了中断控制器却没有中断发生。在ARM架构中,中断控制器是cpu之外的东西,它只是总线上一个可读写的模块,中断状态寄存器一般情况下会用32个状态位表示最多32个中断源,不过这不是限制,可以做几级中断控制,这样就可以表达更多的信息。所有已经使能的中断源的状态位或之后,连接到CPU最根本的中断异常使能,如果此时cpu的中断异常也是使能的,就会激起cpu的异常响应,这段描述听着有点别扭,学过数电的人应该还可以理解。

当把任务,中断,优先级,以及消息队列,任务堆栈等都配置好以后,就可以跑一些简单的操作系统了,类似于ucos这类普通操作系统的任务切换只要依靠二个途径,
1)        任务主动让出cpu而导致低优先级任务得到执行
2)        中断导致高优先级任务就绪,退出中断时高优先级任务获得执行权
因为ucos不存在同级优先级的任务,所以也就不存在时间片到时切换这种模式(术语称为Round-Robin,或者简写为RR),这样做是为了使操作系统的实现简单化,但是在某些特定的场合确实让人很别扭,需要仔细设计以规避带来的问题。

Main函数在设置完任务并调用了os_start()之类的函数后就退出了舞台,正常情况下不会再返回到main函数,main函数不属于操作系统,操作系统运转起来之后就不需要它了,过河拆桥。

而完整的保护式操作系统则是一个完全不一样的概念,为什么叫保护式操作系统呢,个人理解至少有两个原因:
1)        每个进程只能看到自己的地址空间,其他进程的数据和代码都是不可见,每个进程都拥有自己的世界。
2)        用户进程不能直接调用内核函数或者读写内核数据,必须通过API调用这种间接方式向内核请求,在获得允许的情况下才能执行。
可能有朋友会问,既然用户空间不能执行内核函数,那如何进行API调用呢?这种情况我们就需要SWI即软中断的帮助,从cpu的角度来看,用户空间调用系统API的流程如下:
1)        按照寄存器参数传递规则,用户空间首先将需要的服务号,参数等填入各个寄存器或者数据结构,此时工作于usr模式。
2)        激起SWI异常
3)        CPU进入SVC模式,检查服务号,参数等是否合法,若不合法则置失败标识原因等,退出到用户模式,如若合法则会调用相应的系统服务函数。
4)        执行结束之后从SVC模式填好返回值,返回到用户模式。
这种执行过程,保证了内核在没有bug的情况下,不会被恶意用户程序轻易获得cpu特权从而为所欲为。当然没有bug的软件是不存在的。

使用特权

评论回复
xinzha|  楼主 | 2012-10-18 21:28 | 显示全部楼层
ARM的中断响应流程分为两个大类,一种是以Cortex-M系列的MCU方式,其余的ARM系列属于比较标准的RISC方式。
Cortex-M系列在响应中断时采用硬件自动压栈的方式,当有中断产生时,cpu自动依次把xPSR, PC, LR, R12以及R3‐R0等八个寄存器的值压入堆栈中,注意,如果你此时进行反堆栈跟踪,那么最重要的PC和LR就应该是当前SP + 20 和SP + 24的位置,。Cortex-M的中断返回也是与其他系列有很大的不同,在Cortex-M中的中断返回地址只能是几个特定的地址,每个地址都有其特殊的含义,代表着不同的跳转方式。
同时Cortex-M系列为了提高中断响应的速度以及效率,应用了几个比较新颖的中断响应模式。
1.        当系统响应一个比较低级的中断,并正处于压栈阶段,此时如果产生了一个较为高级的中断,系统会直接将cpu资源出让给较高级的中断,而且较高级的中断也不用再执行一遍压栈过程,这样保证了中断优先级的实现和效率。
2.        当系统正在响应一个中断,此时产生了一个低级或者同级的中断,前一个中断响应结束后,并不会将压栈内容弹出然后再重新响应第二个中断,而是直接保留第一个中断的堆栈内容,直接将之转交给第二个中断,并直接进入第二个中断的响应过程,以此来减少栈切换以及出栈入栈等的时间。
3.        当系统正在响应一个低优先级中断并且已经过了压栈的过程,此时如果来了一个高优先级的中断,就会按照正常的抢占流程来处理,高优先级中断会把当前低优先级中断的寄存器入栈然后执行自己的中断服务例程。

使用特权

评论回复
dong_abc| | 2012-10-19 00:45 | 显示全部楼层
学习

使用特权

评论回复
xinzha|  楼主 | 2012-10-21 21:22 | 显示全部楼层
写得我都有点乱了,还得得规划下,这几天正在整理一下,查缺补漏,再把描述不严密的部分修改下。

使用特权

评论回复
xinzha|  楼主 | 2012-10-25 23:13 | 显示全部楼层
ARM的状态切换是一个比较容易出问题的地方,ARM为了实现效率和成本的平衡,允许cpu在32位指令编码(ARM)和16位指令编码(THUMB)之间切换,因为确实有些简单指令完全没必要用32位来实现,而在某些对性能要求比较严格的场合,由于16位编码的限制,又导致了性能的下降,此时cpu就可以切换到32位去执行。公平地说ARM的这个设计是相当成功的,虽然我更喜欢MIPS。在以控制为主的系统中可以大量使用THUMB编码,只有在大量计算的模块使用ARM编码,实现起来非常方便,在ARMCC编译器下,只需要打开interworking选项,在需要ARM编码的模块标注#pragma ARM,在需要THUMB编码的模块标注#pragma THUMB就搞定了。C语言编码级别的ARM/THUMB态转换由编译器链接器插入一些称为veneer的小模块来解决,这些小模块在c层面上不可见,是armcc为了方便实现长跳转以及状态切换加入的。状态切换从cpu指令级别来看,常用的有两种方式:
1.        通过某些特殊指令将spsr恢复到cpsr来实现状态切换
2.        通过类似于blx或者bx这样的跳转指令,当目的地址的最低位为1时,切换到THUMB态,若其为0则应该切换到ARM态,这时你可能会问即使是THUMB态也是16位对齐,怎么会最低位为0?不用担心,这个由链接器来处理,它会把所有标记为THUMB态的函数地址的最低位置1,这样就不用担心了。
当你编写自己的汇编语言代码,并且系统中存在ARM/THUMB切换时,你就用非常小心,这个时候全靠你自己了,因为不论是在ARM态下执行THUMB代码,还是在THUMB态下执行ARM代码,都会遭遇一个问题,undefined instruction,也就是说,cpu不认识那些指令了。某著名的嵌入式os的2.89版就有这样的问题,在做中断处理的时候没有考虑到ARM/THUMB混编的情况,导致某些情况下,cpu在ARM态执行THUMB代码。等有空把他们本来的代码和我修改的部分贴出来比较下大家就能看明白了。当时联系了他们的FAE,也发给了他们的总部,不过一直没见修改,不知道现在怎么样了。

使用特权

评论回复
xinzha|  楼主 | 2012-10-27 22:00 | 显示全部楼层
昨天调了一整天的bootloader,脑袋都快木了,跳出了一个又一个的坑,并且目标板上没有提供Jtag接口,大大增加了调试难度,不过也由此想到了一些话题。
首先的一个话题是动态加载,以前的根bootloader和用于升级的bootloader使用两套代码,自然Makefile也是两套,可是既然都是bootloader,那么实际上大部分代码都是相同的,使用两套代码以及Makefile就等于是两个工程,这样会造成日后维护量的上升以及发生错误的可能性。按照多位软件大师的对于软件质量的看法,在确定的软件需求之下,当你尽可能地让自己的代码简单,你也就极大可能地减少了bug的数量。而且我也坚信一点,整个过程中如果需要人为参与的工作量更少,日后出现弱智性错误的几率就越小。
出于这些考虑,我接到这个需求之后就考虑把原有工程合并,但是首先需要解决的一个问题就是要解决动态加载的问题,因为根bootloader是直接可读写的,所以升级用的bootloader不能使用与它重叠的地址空间,而如果使用同样的Makefile和分散加载(ARM链接器里面是scatter,GNU的链接器里面叫做ld或者lds文件),这样的话就出现了一个问题,升级用的bootloader和根bootloader编译时是一样的,执行时却在不一样的地址。这就要求升级用bootloader必须有一种灵活的机制,使自己能够在不同的地址上都能执行,直观点说就是当pc从入口跳入执行之后,其内部的跳转都是相对跳转,并且不使用固定地址的全局变量等。实际上c语言级别的实现很简单,只需要在编译选项中加入一个开关,-fpic就行,开始时我想到了这点但是还是执行有问题,后来又查汇编部分,发现在跳转时使用了jal指令,由于这几年做arm做多了,已经遗忘了MIPS中实现了单指令的绝对跳转,多次放过这条指令,后来去查手册才知道应该改成b或者bal,于是第一个坑算是解决了。不懂汇编急死人啊。

使用特权

评论回复
现在努力吧| | 2012-10-30 15:12 | 显示全部楼层
好帖,顶起

使用特权

评论回复
W_Controller| | 2012-10-30 19:33 | 显示全部楼层
顶起,谢谢分享:)

使用特权

评论回复
inurl| | 2012-10-31 10:31 | 显示全部楼层
43# xinzha
楼主多大年纪了 ? 工作几年啦?

使用特权

评论回复
xinzha|  楼主 | 2012-10-31 13:58 | 显示全部楼层
35岁,工作时间加起来有快12年了吧。

使用特权

评论回复
zh5202| | 2012-11-1 11:43 | 显示全部楼层
强烈感谢楼主,**多写点,新人们期待中

使用特权

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

本版积分规则