armcm3权威指南笔记

CM3系列处理器优势:

功耗低。延长了电池的寿命——这简直就是便携式设备的命门(如无线网络应用)

实时性好。采用了很前卫甚至革命性的设计理念,使它能极速地响应中断,而且响应中断所需的周期数是确定的

代码密度得到很大改善(应用thumb指令?)。一方面力挺大型应用程序,另一方面为低成本设计而省吃俭用。

降低成本还有一招,就是使基础代码在所有系统中都可以重用,至少要方便移植。CM3的内核架构非常精工细作,使它与C语言成为了一个梦幻绝配。优质的C程序代码三下五除二就可以移植并重用,使升级和移植一下子从拦路虎变成了纸老虎。

,CM3还突破性地引入了很多时尚的甚至崭新的技术,专门满足单片机应用程序的需求。比如,服务于“使命-关键”应用的不可屏蔽中断,极度敏捷并且拥有确定性的嵌套向量中断系统.

P15和P16有关于cortex-m和cortex-a其他系列的比较,讲的比较好。

还有关键的対实时系统的定义:

通用处理器能否胜任实时系统的控制,常遭受质疑,并且在这方面的争论从没停止过。从定义的角度讲,“实时”就是指系统必须在给定的死线(deadline,亦称作“最后期限”)内做出响应。在一个以ARM处理器为核心的系统中,决定能否达到“实时”这个目标的,有很多因素,包括是否使用“实时操作系统”,中断延迟,存储器延时,以及当时处理器是否在运行更高优先级的中断服务例程。

 MMU,存储器管理单元,用于实现虚拟内存和内存的分区保护,这是应用处理器与嵌入式处理器的分水岭。电脑和数码产品所使用的处理器几乎清一色地都带MMU。但是MMU也引入了不确定性,这有时是嵌入式领域——尤其是实时系统不可接受的。然而对于安全关键(safety-critical)的嵌入式系统,还是不能没有内存的分区保护的。为解决矛盾,于是就有了MPU。可以把MPU认为是MMU的功能子集,它只支持分区保护,不支持具有“定位决定性”的虚拟内存机制。

1.5 Cortex-M3处理器的舞台也比较有意思

当呼叫一个子程序时,由R14存储返回地址

不像大多数其它处理器,ARM为了减少访问内存的次数(访问内存的操作往往要3个以上指令周期,带MMU和cache的就更加不确定了),把返回地址直接存储在寄存器中。这样足以使很多只有1级子程序调用的代码无需访问内存(堆栈内存),从而提高了子程序调用的效率。如果多于1级,则需要把前一级的R14值压到堆栈里。在ARM上编程时,应尽量只使用寄存器保存中间结果,迫不得以时才访问内存。

中断可屏蔽

既可以屏蔽优先级低于某个阈值的中断/异常[译注8](设置BASEPRI寄存器),也可以全体封杀(设置PRIMASK和FAULTMASK寄存器)。这是为了让时间关键(time-critical)的任务能在死线Cortex-M3权威指南 第 2 章 27

(deadline,或曰最后期限)到来前完成,而不被干扰。

Cortex-M3只使用Thumb-2指令集。这是个了不起的突破,因为它允许32位指令和16位指令水乳交融,代码密度与处理性能两手抓,两手都硬。而且虽然它很强大,却依然易于使用。

在过去,做ARM开发必须处理好两个状态。这两个状态是井水不犯河水的,它们是:32位的ARM状态和16位的Thumb状态。当处理器在ARM状态下时,所有的指令均是32位的(哪怕只是个”NOP”指令),此时性能相当高。而在Thumb状态下,所有的指令均是16位的,代码密度提高了一倍。不过,thumb状态下的指令功能只是ARM下的一个子集,结果可能需要更多条的指令去完成相同的工作,导致处理性能下降

事实上,Cortex-M3内核干脆都不支持ARM指令,中断也在Thumb态下处理(以前的ARM总是在ARM状态下处理所有的中断和异常)。这可不是小便宜,它使CM3在好几个方面都比传统的ARM处理器更先进

因为CM3专情于最新的Thumb-2,旧的应用程序需要移植和重建。对于大多数C源程序,只需简单地重新编译就能重建,汇编代码则可能需要大面积地修改和重写,才能使用CM3的新功能,并且融入CM3新引入的统一汇编器框架(unified assembler framework)中。

 

Cortex-M3的设计允许单片机高频运行(现代半导体制造技术能保证100MHz以上的速度)。即使在相同的速度下运行,CM3的每指令周期数(CPI)也更低,于是同样的MHz下可以做更多的工作;另一方面,也使同一个应用在CM3上需要更低的主频。P32

这句话是不是说芯片刚启动是4MHZ,然后经过PLL倍频变为60M或者144MHZ?

先进的中断处理功能

内建的嵌套向量中断控制器支持多达240条外部中断输入。向量化的中断功能剧烈地缩短了中断延迟,因为不再需要软件去判断中断源。中断的嵌套也是在硬件水平上实现的,不需要软件代码来实现。怎么去体现粗体字,是不是指下面的东西

Cortex-M3在进入异常服务例程时,自动压栈了R0-R3, R12, LR, PSR和PC,并且在返回时自动弹出它们,这多清爽!既加速了中断的响应,也再不需要汇编语言代码了(第8章有详述)。

优化中断响应还有两招,它们分别是“咬尾中断机制”和“晚到中断机制”。

有些需要较多周期才能执行完的指令,是可以被中断-继续的——就好比它们是一串指令一样。这些指令包括加载多个寄存器(LDM),存储多个寄存器(STM),多个寄存器参与的PUSH,以及多个寄存器参与的POP。 这些不懂???

尽管PC的LSB总是0(因为代码至少是字对齐的),LR的LSB却是可读可写的。这是历史遗留的产物。在以前,由位0来指示ARM/Thumb状态。因为其它有些ARM处理器支持ARM和Thumb状态并存,为了方便汇编程序移植,CM3需要允许LSB可读可写。P38

上面这段是不是就解释了同页的

寄存器的PUSH和POP操作永远都是4字节对齐的——也就是说他们的地址必须是0x4,0x8,0xc,……。事实上,R13的最低两位被硬线连接到0,并且总是读出0(Read As Zero)。

 

如果向PC中写数据,就会引起一次程序的分支(但是不更新LR寄存器)。CM3中的指令至少是半字对齐的,所以PC的LSB总是读回0然而,在分支时,无论是直接写PC的值还是使用分支指令,都必须保证加载到PC的数值是奇数(即LSB=1),用以表明这是在Thumb状态下执行。倘若写了0,则视为企图转入ARM模式,CM3将产生一个fault异常。

这句话又对上面的做了补充说明,粗体部分的原因是因为CM3还支持16位的指令

3.2.3 控制寄存器(CONTROL)的介绍

Ut-kernel里面只用了特权级和两种类型的堆栈,ut-kernel这方面设计得过于简单,使用户在编写应用程序时,竟然也可以访问特殊功能寄存器。给用户权限太大了。

代码依据:

Icrt0.s:

       ;;----------------------------------------------------------------------

       ;MSP setting

       ;;----------------------------------------------------------------------

       mov r0, #0                         ;MSP Effective ( privileged mode )

       msr  control, r0

       isb                     ;It is necessary after control register is operated.

      

       ldr   r0, =_kernel_MSP_stack_top

       msr  msp, r0

特权级和用户级主要是限定了不同的寄存器访问权限,有些特殊功能寄存器只能在用户级才能访问

Ut-kernel的usetrap功能好像就是针对这方面的。

当处理器处在线程状态下时,既可以使用特权级,也可以使用用户级;另一方面,handler模式总是特权级的。在复位后,处理器进入线程模式+特权级

在线程模式+用户级下,对系统控制空间(SCS)的访问将被阻止——该空间包含了配置寄存器组以及调试组件的寄存器组。除此之外,还禁止使用MRS/MSR访问刚才讲到的,除了APSR之外的特殊功能寄存器。如果以身试法,则对于访问特殊功能寄存器的,访问操作被忽略;而对于访问SCS空间的,将fault伺候。

特权级下的代码可以通过置位CONTROL[0]来进入用户级。而不管是任何原因产生了任何异常,处理器都将以特权级来运行其服务例程异常返回后,系统将回到产生异常时所处的级别用户级下的代码不能再试图修改CONTROL[0]来回到特权级。它必须通过一个异常handler,由那个异常handler来修改CONTROL[0],才能在返回到线程模式后拿到特权级(用户级的代码怎样拿到特权级)。P41

 

还有P42也是对上面的讲得做更进一步地补充说明,感觉这方面还得仔细看看,结合着本项目组之前做的FM3 reference代码移植中usetrap讨论,还得看看

 

Ut-kernel中判断是否在中断上下文还是任务上下文是通过knl_isTaskIndependent内联函数来判断的,它调用knl_get_stack_mode_impl,这个又是汇编函数的形式实现,通过写R0寄存器带回函数返回值为control寄存器的值,然后通过判断是MSP还是PSP模式来判断是否是中断或者任务上下文的。

下面这段貌似也跟我们组的usetrap问题讨论相关:

由CM3的中断优先级模型可知,我们不能在SVC服务例程中嵌套使用SVC指令(事实上这样做也没意义),因为同优先级的异常不能抢占自身。这种作法会产生一个用法fault。同理,在NMI服务例程中也不得使用SVC,否则将触发硬fault。

 

 

 

伪指令概念:

32位指令MOVW和MOVT可以支持16位立即数加载。

那要加载32位立即数怎么办呢?如果要直来直去,当前是要用两条指令来完成了。通过组合使用MOVW和MOVT就能产生32位立即数,但是要注意,必须先使用MOVW,再使用MOVT。这种顺序是不能颠倒的,因为MOVW会清零高16位。

不过,更流行的是另一种方法:使用汇编器提供的”LDR Rd, = imm32”伪指令。例如:

LDR, r0, =0x12345678

酷吧!它的名字也是LDR,而且能加载32位立即数!但可别忘了,它是伪指令,是“妖怪变的”,而且有若干种原形。所以不要因为名字相同就混淆。

大多数情况下,当汇编器遇到LDR伪指令时,都会把它转换成一条相对于PC的加载指令,来产生需要的数据。。大可依赖汇编器,它会明智地使用最合适的形式来实现该伪指令。

P62 LDR伪指令vs. ADR伪指令比较也比较有意思。

ISB指令的运用

ISB

指令同步隔离。最严格:它会清洗流水线,以保证所有它前面的指令都执行完毕之后,才执行它后面的指令。 P72

       ;MSP setting

       ;;----------------------------------------------------------------------

       mov r0, #0                         ;MSP Effective ( privileged mode )

       msr  control, r0

       isb                     ;It is necessary after control register is operated

 

位带区的作用:

。位带操作只适用于数据访问,不适用于取指。通过位带的功能,可以把多个布尔型数据打包在单一的字中,却依然可以从位带别名区中,像访问普通内存一样地使用它们。位带别名区中的访问操作是原子的,消灭了传统的“读-改-写”三步曲。P84

CM3中可支持非对齐数据传送,

CM3支持在单一的访问中使用非(地址)对齐的传送,数据存储器的访问无需对齐。在以前,ARM处理器只允许对齐的数据传送。这种对齐是说:以字为单位的传送,其地址的最低两位必须是0以半字为单位的传送,其地址的LSB必须是0;以字节为单位的传送则无所谓对不对齐。如果使用0x1001,0x10020x1003这样的地址做字传送在以前的ARM处理器中则会触发一个数据流产(Data abort)异常——CM3中总线fault异常的作用相同

CM3中,非对齐的数据传送只发生在常规的数据传送指令中,如LDR/LDRH/LDRSH(刚好这些指令也是伪指令吧)。其它指令则不支持

 

但是最好:

事实上,节省内存有很多方法,但没有一个是通过压缩数据的地址,不惜破坏对齐性的这种旁门左道。因此,应养成好习惯,总是保证地址对齐,这也是让程序可以移植到其它ARM芯片上的必要条件

为此,可以编程NVIC,使之监督地址对齐。当发现非对齐访问时触发一个fault。具体的办法是设置配置控制寄存器中的UNALIGN_TRP。这样,在整个调试期间就可以保证非对齐访问能当场被发现。

.

还有对Pendsv的理解,

另一个相关的异常是PendSV(可悬起的系统调用),它和SVC协同使用。一方面,SVC异常是必须在执行SVC指令后立即得到响应的(对于SVC异常来说,若因优先级不比当前正处理的高,或是其它原因使之无法立即响应,将上访成硬fault译者注),应用程序执行SVC时都是希望所需的请求立即得到响应。另一方面,PendSV则不同,它是可以像普通的中断一样被悬起的(不像SVC那样会上访)。OS可以利用它“缓期执行”一个异常直到其它重要的任务完成后才执行动作。悬起PendSV的方法是:手工往NVIC的PendSV悬起寄存器中写1。悬起后,如果优先级不够高,则将缓期等待执行。P125,至于代码具体对应knl_dispatch_request函数

然后下面讲述的pendsv的应用实例。

应用时,需要把PendSV编程为最低优先级的异常

我觉得ut-kernel中pendsv中断是做任务切换工作的。具体为什么ut-kernel中用pendsv,

1是因为确实推迟任务切换的执行,直到ut-kernel API执行完END_CRITICAL_SECTION;后才会有可能启动pendsv执行。

另外一方面也是由于在执行完END_CRITICAL_SECTION;如果此时有其他优先级更高的中断到来,则优先执行其他优先级更高的中断,执行完后再执行pendsv进行任务切换。

 

但若在产生SysTick异常时正在响应一个中断,则SysTick异常会抢占其ISR。在这种情况下,OS是不能执行上下文切换的,否则将使中断请求被延迟,而且在真实系统中延迟时间还往往不可预知任何有一丁点实时要求的系统都决不能容忍这种事。因此,在CM3中也是严禁没商量如果OS在某中断活跃时尝试切入线程模式,将触犯用法fault异常。

以下是我对上面文字的解释:

这段话中首先执行个用户实时中断,然后被SysTick异常打断,SysTick异常中会去进行任务切换,等切换回来发现之前被systick打断的那个中断竟然延迟这么长时间去执行(主要是有个任务切换的时间算在内了)。

这段话说明如果任务切花不交给pendsv去做的话,结果在真实系统中这种用户实时中断被延迟时间还往往不可预知。任何有一丁点实时要求的系统都决不能容忍这种事。所以最好交给

Pendsv去做。

另外上面这种特例在CM3中会自动触发fault异常。

原文这样说的:

在CM3中也是严禁没商量如果OS在某中断活跃时尝试切入线程模式,将触犯用法fault异常。

中断活跃怎么理解:

每个外部中断都有一个活动状态位。在处理器执行了其ISR的第一条指令后,它的活动位就被置1,并且直到ISR返回时才硬件清零。由于支持嵌套,允许高优先级异常抢占某个ISR。然而,哪怕中断被抢占,其活动状态也依然为1(请仔细琢磨前文讲到的“直到ISR返回时才清零)

 

下面文字:

如果中断发生时,正在处理同级或高优先级异常,或者被掩蔽,则中断不能立即得到响应。此时中断被悬起。P130

 让我想起代码中:

SYSCALL ID tk_cre_mpl_impl( T_CMPL *pk_cmpl )

{

BEGIN_CRITICAL_SECTION;

A;

END_CRITICAL_SECTION;

}

A在执行中,如果发生systick,则被悬起暂不执行,等到执行完END_CRITICAL_SECTION;

先执行systick,后执行Pendsv。

不过END_CRITICAL_SECTION后,不一定就打开中断了,它只是恢复BEGIN_CRITICAL_SECTION;之前的中断状态。

中断系统设置全过程的演示

P135还做了CM3上怎么建立中断,使中断发挥作用。

看过下面确认:

MSP和PSP是两个不同的寄存器。

在入栈和取向量操作完成之后,执行服务例程之前,还要更新一系列的寄存器:

l SP:在入栈后会把堆栈指针(PSP或MSP)更新到新的位置。在执行服务例程时,将由MSP负责对堆栈的访问。

嵌套的中断P144

然而,有一件事情却必须更加一丝不苟地处理了,否则有功能紊乱甚至死机的危险。这就是计算主堆栈容量的最小安全值。我们已经知道,所有服务例程都只使用主堆栈。所以当中断嵌套加深时,对主堆栈的压力会增大:每嵌套一级,就至少再需要8个字,即32字节的堆栈空间而且这还没算上ISR对堆栈的额外需求,并且何时嵌套多少级也是不可预料的。如果主堆栈的容量本来就已经所剩无几了,中断嵌套又突然加深,则主堆栈有被用穿的凶险

另一个要注意的,是相同的异常是不允许重入的。因为每个异常都有自己的优先级,并且在异常处理期间,同级或低优先级的异常是要阻塞的。因此对于同一个异常,只有在上次实例的服务例程执行完毕后,方可继续响应新的请求。由此可知,在SVC服务例程中,就不得再使用SVC指令,否则将fault伺候。

我们组usetrap那个讨论好像也是针对这个的。

Armcm3通过9.4咬尾中断和9.5晚到(的高优先级)异常来加快中断的响应。P145

Cm3主要是为了做实时系统来的。

在设计实时系统时,必须对中断延迟进行严肃和仔细地估算。在这里,中断延迟的定义是:从检测到某中断请求,到执行了其服务例程的第一条指令时,已经流逝了的时间。在CM3中,若存储器系统够快,且总线系统允许入栈与取指同时进行,同时该中断可以立即响应,则中断延迟是雷打不动的12周期(满足硬实时所要求的确定性)。在与时间赛跑的这12个周期里,处理器内部一直开足马力,进行了入栈、取向量、更新寄存器以及服务例程取指的一系列操作。但若存储器太慢以至于引入等待周期,或者还有其它因素,则会引入额外的延时反正如果有拖后腿的,那绝不可能是CM3内核。P148

为了加快中断响应,

有些指令需要较多的周期才能完成。它们是除法指令,双字传送指令LDRD/STRD以及多重数据传送指令(LDM/STM)。

对于除法指令,双字传送指令LDRD/STRDCM3,将为了保证中断及时响应而取消它们的执行,待返回后重新开始这牺牲了一点性能,以及某些子程序的一点个人利益,但换来了对意外事件的更快救援。

对于LDM/STM,则有另外的处理方式。因为它们不照前两者那么浑然一体它们其实是一串LDR/STR的速度优化版。于是,为了加速中断的响应,CM3支持LDM/STM指令的中止和继续,就好像它们只是普通的一串LDR/STR一样。为了实现“指令撕裂与粘合”的目的,需要记录中断时数据传送的进程。为此,CM3在xPSR中开出若干个“ICI位”,记录下一个即将传送的寄存器是哪一个(LDM/STM在汇编时,都把寄存器号升序排序)。在服务例程返回后,xPSR被弹出,CM3再从ICI bits中获取当时LDM/STM执行的进度,从而可以继续传送

LDR/STR为伪指令?

 

多个中断同时请求时,也会发生中断延迟,这表现在只有优先级最高的得到立即响应,所有其它的中断将被延迟。另外,在中断嵌套时,每个中断都会阻塞同级和低优先级的中断。最后,如果中断被掩蔽(也就是俗称的关中,在多任务系统下满地都有),则在掩蔽期间也会附加中断延迟。

最后一句话更加验证了本文档P6中的ut-kernel中对Pendsv中断的运用之我理解

 

ARM编程规范吧,为了更好地利用硬件的特性

不过,在大多数场合下的情况都比较简单:当主调函数需要传递参数(实参)时,它们使用R0-R3。其中R0传递第一个,R1传递第2个……在返回时,把返回值写到R0中。在子程序中,可以随心所欲Cortex-M3权威指南 第 10 章 153

地使用R0-R3,以及R12(回顾第9章,想想为什么会PUSH它们)。但若使用R4-R11,则必须在使用之前先PUSH它们,使用后POP回来。

10.4 第一步工作

而且实际上,开发工具几乎都会把启动工作做好,让我们根本不用去想还有启动代码的事(不过,这也妨碍了我们学习得更深入)。P153最下面

上文说的意思是一般各个开发工具自带的库和示例程序已经包含了启动代码及其他设置硬件的代码,框架有些都有了,不需要我们再写了。

 

10.4中这个小代码示例的编译链接选项配置使我想起了MDK环境下的CMSIS测试工作,不管MDK开发环境如何复杂,其实最后传到命令行里面的还是这些基本选项。只是--rw_base 0x20000000 --ro_base 0x0这些选项可以换成scatfile的形式进行link。

如果想要看看生成的映像是否确实是我们想要的,还可以像这样对它做反汇编:

$> fromelf -c --output test1.list test1.elf

一般各个开发工具(keil, IAR, DS-4)中都带相应的反汇编工具的。

使用位带实现互斥锁操作

潜在BUG

如果把向量表重定位到了RAM中,且这块RAM所在的存储器区域是写缓冲的,向量更新就可能被延迟

为了以防万一,必须在建立完所有向量后追加一条“数据同步隔离(DSB)”指令(见第4章),以等待缓冲写入后再继续,从而确保所有数据都已落实。

Cortex-M3 权威指南 第11168

 

中断服务程序的返回使用指令:

这里的服务例程都是使用BX LR返回的,但是真到了写程序时,往往利用POP {…,PC}的形式来使程序更精练(当然也可以使用LDMIA指令)。

P171d的指令DSB ;数据同步隔离运用。

我觉得像P174中那些TST,ITE,MRSEQ,MRSNE指令的灵活运用下来我还得开辟一个章节,好好研究下这些指令的变形,应用

下面让我对指令中的立即数提取有了一定认识:

LDRB R0, [R1,#-2] ; 从SVC指令中读取立即数放到R0

好像前面一些章节中说指令中存取16位立即数,指令必须为32位;存取8位立即数,指令可以为16位。

还有:

LDR R1, [R0,#24] ;从栈中读取PC的值

因为中断响应时,硬件压入栈中的寄存器多少长度都是固定的,所以可以算出PC的偏移量。

Ut-kernel中有关于svc_handler的代码实现,写的还不错。

在前面的例子中,我们写了若干个函数用于输出。但是有的时候,可能有一些障碍,使得我们不能用BL指令。例如,需要调用的函数是在另外的目标文件中,这就会导致有的时候我们无法定位子程序的入口地址;另外,如果跳转的目的地太远,也有诸多不便;或者,当使用OS时,这些输出函数已经被OS包装成系统调用了。在这些场合下,我们就需要使用SVC来作为传送门,如下面示例代码所示:

上面这一句话是不是告诉我BL指令的使用限制,跳转太远的就不能使用BL指令吧。这里面提出来用SVC传送门来处理这个问题。下来再看下这几个跳转指令的差异问题

 

P175那个代码中的

LDR R0, [R1,#0] ; 从堆栈中读取R0的值,此处R1应该改为R0

还有为什么这个地方还要从栈中取出R0值,为什么不直接读R0寄存器,因为晚到中断的缘故

可见同页最下面的那段叙述

PUSH {LR} ; 保护LR的值,因为后面将使用的BL指令

是因为下面的第二段的话:

 

最基本的无条件跳转指令有两条:

B Label ;跳转到Label处对应的地址

BX reg ;跳转到由寄存器reg给出的地址

BX中,reg的最低位指示出在转移后将进入的状态:是ARM(LSB=0)呢,还是Thumb(LSB=1)既然CM3只在Thumb中运行,就必须保证regLSB=1,否则一个fault打过来。

ARM的fault。P69

 

有心的读者可能已经发现,ARM的BL虽然省去了耗时的访内操作,却只能支持一级子程序调用。如果子程序再呼叫“孙程序”,则返回地址会被覆盖。因此当函数嵌套多于一级时,必须在调用“孙程序”之前先把LR压入堆栈也就是所谓的“溅出”。

BL常用在函数调用中,调用函数用BL,然后函数里面最后一条指令 BX LR直接进行调用返回。

编程技巧:善用LDR/STR中的多种寻址方式P176更加告诉我们熟悉指令使用,精简指令条数。这个回去有空可以再看看,想想ARM中那些灵活使用的指令。

P183,最下面那个程序告诉我们用PC值和正确的栈指针(栈上保存了一个可执行任务的上下文环境,设置正确的栈指针代表把栈指针挪到当时PC值对应的正确地当时上下文环境)就能把程序切换到正确的地方。

 

第二,如果没有必要,中断向量表也放到代码区中。只有这样,才能使取向量(I-Code总线)与入栈(System总线)同时进行。如果向量表在RAM中,就会出现取向量与入栈抢总线的情况,必然导致额外的中断延迟被引入(当然在极个别情况下,如果把SRAM放到Code区,则使用D-Code总线入栈。但如果就为了放向量表而专配一个SRAM,代价未免也太大了)。

上面说明了哈佛结构的性能优化。

 

 

 

第三、限制使用非对齐访问。前面讲到,CM3总线内部其实只接受对齐访问,而由总线接口来堵窟窿:把一个非对齐的访问拆成若干个对齐的访问,来实现这种透明性。可见,一次非对齐访问可能要数次对齐访问才能完成(最坏情况下3次)。而且节省内存的正道,在于优良的程序结构和算法设计,从来不在这种见缝插针地乱挤上。除非是客观上被定死的(常见于某些早期网络协议的报文头部),否则应在心里暗下决心:决不染指非对齐访问,在设计数据结构及定义变量时,都高度自觉。在ARM汇编器中,提供了ALIGN指示字(GNU AS中也有类似的汇率器指示字),可以保证产生所需的对齐方式。

回去查下关于arm 对齐访问方面的东西,这里面有很多料,甚至涉及到RISC和CISC设计思想的不同。

CM3中一些前卫的指令

IF-THEN指令P75

IT ;围起4条指令的IF-THEN块

其中, , 的取值可以是“T”或者“E”。

CMP R0, R1 ; 比较R0和R1

ITTEE EQ ;如果R0 == R1, Then-Then-Else-Else

ADDEQ R3, R4, R5 ; 相等时加法

ASREQ R3, R3, #1 ; 相等时算术右移

ADDNE R3, R6, R7 ; 不等时加法

ASRNE R3, R3, #1 ; 不等时算术右移

Then-Then-Else-Else意思为下面4条指令前两条为if then块中的语句,后两条为 else块中的语句。

P158,174也有它的使用方式

 

cpu如果被锁定,则比较危险了。P186

在锁定下,寄存器和存储器都被“冻结”,PC的值被强制为0xFFFF_FFFx,并且原地打转地定死在那里一直取指

如何避免被锁定:

简化硬fault和NMI的服务例程确实是个好主意:它们只做必需的,然后悬起PendSV,让诸如错误报告等其它工作在PendSV中处理,当然,软件中断兴许也能凑和着用。

除此之外,我们还必须杜绝在硬NMI/fault例程中使用SVC指令,这也是斩立决的因为SVC的优先级总是没有NMI和硬fault的高,而且它又不允许悬起(悬起时触发fault)

。而对于fault来说,有可能就是因为SP指针指飞了(干扰、堆栈溢出等),以致前面的堆栈操作触发了本次硬fault,再操作堆栈还不当场被秒杀?如下面代码所警示:

hard_fault_handler

PUSH {R4-R7,LR}; 除非确保堆栈是安全可用的(谁能确保?),否则不要这样做

...

值此危难关头,必须沉着冷静。在我们设计硬fault,总线fault以及存储管理fault的服务例程时,值得先花点工夫去查一查SP的值,看它是否在可接受的范围,然后再做后续工作。对于NMI服务例程来说,它做的通常是应急工作,设计系统时就应该让这种应急工作极简单(比如,只改变一个I/O脚的电平,最多也就是修改若干寄存器的值,就可以开启相关的应急硬件译者注),因此常常可以只使用R0-R3以及R12就完全够用,无需堆栈操作。

程序出错产生异常时的故障调查分析:

二、产生异常时:

1、有一个压栈的过程,产生异常时使用PSP,就压入到PSP中,产生异常时使用MSP,就压入到MSP中

2、会根据处理器的模式和使用的堆栈,设置LR的值(当然设置完的LR的值再压栈)

三、异常返回时:

根据LR的值,判读使用那个堆栈,然后再从相应的堆栈中POP数据到寄存器。

举例说明:

在利用OSStartHighRdy->OSPendSV->OSPendSV_nosave启动第一个线程时,在异常进入的时候,压栈到MSP(不会影响PSP的内容),在通过BX      LR指令从异常返回之前,有一句ORR     LR, LR, #0x04来设置LR的值,保证我们从异常返回时是从PSP中POP数据到寄存器里,而此时PSP的值是OSTCBHighRdy->OSTCBStkPtr,即任务创建时定义的堆栈数组,同时在OSTaskStkInit函数中对该任务数组初始化时,要保证其组织结构和异常产生时处理器自动压栈产生的栈的结构相同。

由于异常死机了之后的具体操作:

产生异常时,两个值我们需要,一个是pc,一个是LR,通过LR找到栈,再通过栈找到pc(调查ARM下程序故障的原因方法)

1、如果LR=0xFFFFFFF9说明产生异常的时候使用的是MSP,我们只需要读出当前sp的值,

 

sp  += 0x1c;读出的内容就是产生异常时压入栈的PC的值,这个值跟反汇编代码对比,就能得到具体哪句话产生的异常。

2、如果LR=0xFFFFFFFD明产生异常的时候使用的是PSP,我们需要读出PSP的值,不要直接来读sp的值,在keil集成开发环境中,调试时寄存器窗口有个Banked选项,会给出当前PSP的值,当然也可以在异常处理中加入两句话:

MRS        R0,PSP

PUSH             {R0}

我们就能在当前MSP中得到,我们PSP的值了,之后操作和上面一样,psp+=0x1c;读出的内容就是产生异常时压入栈的PC的值,这个值跟反汇编代码对比,就能得到具体哪句话产生的异常。

注意:如果启用了堆栈的双字对齐特性,但是SP却没能对齐到双字,则堆栈帧的顶部有可能从((OLD_SP-4) AND 0xFFFF_FFF8)处开始,且其余的内容被向下错位一个字 P301

  

CM3中有许多新特性,加以利用的话常常可以大大提高程序的性能,或者降低对存储器的使用。对于积极向上的我们,一定要挖掘这些特性:P251

l 使用32Thumb-2指令:对于下列的场合:先使用一条16thumb指令把数据从一个寄存器传送到另一个,再对该数据执行数据处理有时能使用一条Thumb-2指令来完成(这主要是因为16Thumb指令不能使用高寄存器——译者注),从而使所需的处理时间缩短。

 

立即数有些Thumb-2指令支持长达12位的立即数,因此可以把以前Thumb指令无法加载的立即数使用一条Thumb-2来加载

 

IT指令块:有些短距跳转可以使用IT指令取代,这样做消灭了因流水线清洗而引入的等待周期,从而提高了性能。

l ARM/Thumb状态切换:在大多情况下,可以把大部分代码以Thumb指令编码,一小部分以ARM指令编码。这主要是为了在平时提高代码密度,而在紧急关头下提高性能。在CM3下有Thumb-2代码,可以在同一模式下解决时间与空间的权衡。这就可以去掉这些状态转换及其所带来的额外负担(overhead),也简化了对工程的管理。

看看上面对ARM汇编指令有所理解。

 

读者可能还注意到了,在本例中我们使用了另一个称为-nostartfiles的编译器开关。使用它,就可以让编译器不再往可执行映像中插入启动代码(crt)这样做的目的之一就是减少程序映像的尺寸。不过,使用该选项的主要原因,其实是在于GNU工具链的启动代码是与发布包的提供者相关的,而有些人提供的启动代码不适合CM3它们往往是用于传统的ARM处理器的ARM7典型地这些启动代码使用了ARM代码,而没有使用Thumb代码)。

但是,在许多情况下,取决于应用程序和使用的库,都必须使用启动代码来执行初始化的过程,最主要的就是对数据的初始化例如,把bss区的存储单元全部清零)。P263

最后一句粗体话,使我想起了icrt0.asm文件中

__reset    FUNCTION

       EXPORT  start                     [WEAK]

       B       start

start

       cpsid       f             ;Interrupt disable

       ;;----------------------------------------------------------------------

       ;; RO/RW Area Copy

       ;; Initialize ZI Area (Use ARM Library)

       ;;----------------------------------------------------------------------

       IMPORT  __main

       B       __main

       EXPORT  __rt_entry

__rt_entry

。。。。自己的ut-kernel代码

 

粗体代码既是对上面文字的代码叙述。

关于程序进行fault历程后,怎么调试的总结:

首先看看本文档P12的程序出错产生异常时的故障调查分析:

 

为了调查MemManage fault的案发现场,NVIC中有一个“存储器管理fault状态寄存器(MFSR)”,它指出导致MemManage fault的原因。如果是因为一个数据访问违例(DACCVIOL位)或是一个取指访问违例(IACCVIOL位),则违例指令的地址已经被压入栈中。如果还有MMARVALID位被置位,则还能进一步查出引发此fault时访问的地址读取NVIC“存储器管理地址寄存器(MMAR)”的值。P122

附录rtex-M3疑难解答这一节貌似非常不错的,讲了出现fault时的调查思路。

E.2 设计Fault服务例程

这一节讲得视野很开阔。

用于开发阶段的fault服务例程,与用于实际系统中的服务例程,在绝大多数场合下是截然不同的。对于软件开发,fault服务例程应关注于准确及时地上报发生fault时上下文;而实际系统中的fault服务例程则要把这当作是危急关头来处理,它要尽可能地想办法来恢复系统,实在不可救要时可能只有重启。这里我们主要讨论前者,因为后者是比较有技术含量的,而且不同的应用需要不同的策略

对于比较专业复杂的软件,

首先:通常不直接在fault服务例程中报告与fault相关的状态,而是把它倾倒(专业术语:dump)到一块专用的内存中(主要包括fault状态寄存器,通用寄存器,自动入栈的内容等),

其次接着悬起PendSV。等到PendSV服务例程执行后,再上报问题。这是因为上报过程执行的工作可能比较多,夜长梦多有可能在上报过程中又不小心触发了其它的fault,使得处理器被锁死。因此先暂记下来,稍后转交PendSV处理。

当然如果软件很简单,则可以斟情简化fault的处理过程,甚至直接在服务例程中上报。

E.2.3 上报fault地址寄存器

这一节讲了为什么要:必须先读BFAR/MMAR,再读BFARVALID/MMARVALID。如果后者为零,则丢弃读出的地址值。

然后还有个注意事项:

在fault上报完毕后,一定不要忘记清除FSR中的fault状态位。否则下次再发生fault时,就分不清FSR中的状态位是反映新来的fault,还是反映以前的fault了。而且,如果fault地Cortex-M3权威指南附录 E 318

址有效位没有清除,下次发生fault时,BFAR/MMAR的值就无法更新。

Arm汇编和C语言的结合:

E.3 在C中上报入栈的寄存器和各fault状态寄存器

在这里有栈溢出错误检测还有C语言汇编,跟栈的关系

大多数的CM3项目还是以C语言为主的。然而,在C中不方便定位和直接访问堆栈帧(入栈的寄存器)。因为在标准C语言中是不能获取SP指针的。因此,如果使用C来写fault服务例程,最好配合一小段汇编码来获取SP的值,再把该值以一个参数传送给fault上报函数。P318

紧接着下面就有一个Arm汇编和C语言的结合的示例程序

请注意:如果发生了堆栈溢出或其它错误,使SP指向了无效的存储空对空区域,则上段代码会失能。在大多数情况下,这种错误都会影响C代码,因为所有C代码都需要堆栈

是否说汇编代码可以减少堆栈的使用啊?),建议多查查堆栈溢出错误方面的资料。

但是栈溢出BUG最近发生了一个新的现象,它的解决方法详见arm权威指南及我们组项目笔记中王SAN的栈溢出BUG解决方案。

P320可以根据MemManage fault状态寄存器MSTKERR 位或者总线fault状态寄存器STKERR 位检查是否栈溢出,详见armcm3项目相关资料资料《一种利用fault异常自检栈溢出的方法》

用法fault状态寄存器的INVSTATE位通常检查是否切入了ARM态,详见armcm3项目相关资料资料定位产生HARD FAULT之前的代码。

 

你可能感兴趣的:(FNST工作总结,嵌入式开发经历,嵌入式)