云菲菲吕日通的邂逅:在PROTEUS中使用ARM处理器及uC/OS-II移植理解

来源:百度文库 编辑:中财网 时间:2024/04/30 13:48:35

PROTEUS中使用ARM处理器及uC/OS-II移植理解

                                     Rein Lee

一.嵌入式系统概述

通过本次嵌入式系统课程的学习,我了解了嵌入式系统的概念。所谓嵌入式系统,是指用于执行独立功能的专用计算机系统,它由包括微处理器、定时器、微控制器、存储器、传感器等一系列微电子芯片与器件,和嵌入在存储器中的微型操作系统、控制应用软件组成,共同实时诸如实时控制、监视、管理、移动计算、数据处理等各种自动化处理任务。嵌入式系统以应用为中心,以微电子技术、控制技术和通讯技术为基础,强调硬件软件的协同性与整合性,软件与硬件可裁减,以满足系统对功能、成本、体积和功耗等要求。

1嵌入式系统的硬件特征

嵌入式系统的硬件必须根据具体的应用任务,以功耗、成本、体积、可靠性、处理能力等为指标来选择。嵌入式系统的核心是系统软件和应用软件。由于存储空间有限,因而要求软件代码紧凑、可靠,大多对实时性有严格的要求。

早期的嵌入式系统设计方法,通常是采用“硬件优先”原则。在粗略估计软件任务需求的情况下,首先进行硬件设计与实现。然后在此硬件平台上,再进行软件设计。因为很难充分利用硬件软件资源,取得最佳性能的效果。同时,一旦在测试时发现问题,需求对设计进行修改时,整个设计流程将重新进行,对成本和设计周期的影响很大。这种传统的设计方法只能改善硬件/软件各自的性能,在有限的设计空间不可能对系统做出较好的性能综合优化,在很大程度上依赖于设计者的经验和反复实验。

随着电子系统功能的日益强大和微型化,系统设计涉及的问题越来越多,难度也越来越大。硬件和软件也不再是截然分开的两个概念。因而出现了软硬件协同的设计方法。在系统目标要求下,协同设计软硬件体系结构,以最大限度地挖掘系统软硬件能力,得到高性能低代价的优化设计方案。

1嵌入式操作系统

目前流行的嵌入式操作系统可以分为两类:一类是从运行在个人电脑上的操作系统向下移植到嵌入式系统中,形成的嵌入式系统,如微软公司的Windows CESUN公司的Java操作系统,嵌入式Linux等。

另一类是实时操作系统,如WindRiver公司的VxWorksISIpSOSATINucleus,和免费公开源代码的uC/OS-II等。

二.在Proteus中使用ARM处理器

由于Proteus中只支持LPC系列的ARM处理器,在这里只是简单的列举出LPC2124的一些特性:

LPC2124是基于一个支持实时仿真和跟踪的16/32ARM7TDMI-S CPU的微处理器,并带有256k的嵌入的高速Flash存储器和16k的片那静态RAM128位宽度的存储器接口和独特的加速结构使得32位代码能够在最大的时钟速率下运行。对代码规模有严格控制的应用可使用16Thumb模式,将使得代码规模降低超过30%,而性能的损失却很小。

LPC2124片那Boot装载程序实现在系统编程(ISP)和在应用编程(IAP)。1ms可以编程512字节。整片擦除只需要400ms。此外还有4A/D转换器,转换时间低于2.24us232位定时器,6PWM输出、RTC、看门狗和多个串行接口。LPC系列微处理器的抗干扰能力强,在很多应用中得到了使用。

三.软件分析

1LPCMemory MapRemapLPC2124Bootblock程序

Memory Map是把芯片中、芯片外的FlashRAM、外设、BootBlock等进行统一编址,用地址来表示对象。LPC系列ARM处理器的这个地址是出厂时,由厂家规定的,用户只能访问,而不能进行更改。

RemapBoot,个人理解如下:

Reset信号周期内,LPC2124运行芯片内部自带的Bootblock程序,复位信号过后才是运行用户的程序。

LPC系列ARM处理器的Bootblock被固化在最高的Flash块中,运行时是被映射到0x7FFFE0000x7FFFFFFF区域,这个程序是厂家写入的,它由任何复位硬件激活,在任何复位后都会先执行Boot装载程序。之所以要把BootBlock程序放在Flash块的顶端,是因为各芯片的Flash大小不一致,厂家为了BootBlock在芯片中的位置固定,在编址的2G靠前的位置虚拟划分一个区域作为BootBlock区域。这就是Remap

BootBlock的工作如下:

1. 判断P0.14是否为低,如果为低,进入ISP模式。

2. 若P0.14不为低,要判断Boot10)这两个脚,如果为11,要设置MEMAP=1,即运行内部Flash。否则设MEMAP3,运行外部Flash

3. 如果是运行外部Flash,那需要把外部Flash的起始地址重新映射到0x00000000,以便复位信号过后就开始运行用户程序。

个人理解BootBlock相当于PC中的BIOS,由厂家固化,上电后首先完成映射,即把它的地址映射到0x00000000处,当初始化完成后,就运行内部Flash或者外部Flash的程序,通常要以向量表开头,这个过程就是Remap,也就是把内部ROM或者外部Flash的地址映射到0x00000000处。

2.启动代码分析

运行完BootBlock程序之后就是要运行用户自己编写的启动代码了。启动代码中最重要的是异常向量表。这个表包括复位、未定义指令、软中断、预取指中止、数据中止、IRQ中断和FIQ中断。

Reset

        LDR     PC, ResetAddr

        LDR     PC, UndefinedAddr

        LDR     PC, SWI_Addr

        LDR     PC, PrefetchAddr

        LDR     PC, DataAbortAddr

        DCD     0xb9205f80 ;插入用户代码有效签名

        LDR     PC, [PC, #-0xff0] ;VIC处取得IRQ入口地址

        LDR     PC, FIQ_Addr

ResetAddr DCD     ResetInit

UndefinedAddr DCD     Undefined

SWI_Addr DCD     SoftwareInterrupt

PrefetchAddr DCD     PrefetchAbort

DataAbortAddr DCD     DataAbort

Nouse DCD     0

IRQ_Addr DCD     0

FIQ_Addr DCD     FIQ_Handler

Undefined

        B       Undefined ;未定义指令

PrefetchAbort

        B       PrefetchAbort ;取指令中止

;取数据中止

DataAbort

        B       DataAbort ;快速中断

未定义指令、取指令中止、快速中断使得PC不跳转,进入死循环,等待看门狗复位用户程序。对于ARM9,有MMU单元,应该在PrefetchAbort和DataAbort处进行处理,然后返回到发生异常的指令处重新执行一次。

FIQ_Handler

        STMFD   SP!, {R0-R3, LR} ;ADS编译器会自动保存R4-R11

        IMPORT  FIQ_Exception ;也可以在startup.s的开头引入

BL       FIQ_Exception ;调用C语言中的中断服务程序

        LDMFD   SP!, {R0-R3, LR} ;出栈

        SUBS    PC,  LR,  #4

如果在CPSR中没有禁止中断,那么每执行一条指令,处理器会检查是否有FIQ或者IRQ中断发生。若有中断发生,当且仅当PC指更新后,即处理器对下一条指令进行取指发生后,处理器才会进入到中断的服务程序中。因此保存在LR中的指是PC-4,此处的PC是更新后的PC,由于ARM处理器是三级流水线的结构,因此返回地址是LR-4,返回到的地址是将要执行,但是被中断了指令。

LDR只能实现当前PC 4KB范围内的跳转,而在LDR的不远处用DCD定义一个字,而在这个字里存放最终的异常服务程序的入口地址,可以实现在4GB范围内的全范围跳转。要求整个向量表的累加和为0

__user_initial_stackheap函数是对用户的堆栈进行初始化,对它的调用是在__main中完成的,对编程人员不可见。__mainADS编译系统提供的一个库函数,使用__main标号引导系统时,必须将应用程序的入口定义为main()__main完成代码和数据的复制,并且把ZI数据区清零。这一步当代码和数据区在存储和运行时处于不同的存储器位置时有意义。__main保证了系统在进入用户的应用程序前自动完成了系统调用。但是如果所有的初始化过程都已经被用户代码显式地被完成,如堆栈初始化、加载映象、执行映象、RWZI数据的复制等,那么用户应用程序的入口函数可以任意定义any_name(),完成初始化后,直接B any_name即可。个人建议使用B  __main的方式,防止疏忽出现不必要的错误,比如出现程序中的全局变量没有被正确的初始化等错误。

3.IRQ.S分析

通过定义宏:

$IRQ_Label  HANDLER  $IRQ_Exception_Function来实现LPC 2124的多个中断源共用一段异常处理代码的目的。

SUB     LR, LR, #4                      ; 计算返回地址

STMFD   SP!, {R0-R3, R12, LR}           ; 保存任务环境

进入IRQ中断,首先计算中断的返回地址,将LR4,然后将R0-R3,R12,LR压入堆栈。由于进入了IRQ中断,因此堆栈指针是SP_IRQ,和用户代码运行的SP_SYS不是同一个寄存器。至于只保存R0-R3,R12,是因为程序在正常运行过程中,R4-R11中装载的是局部变量,在中断跳转是ADS编译器会自动将R4-R11入栈保护。

进行了堆栈保护后,要保存进入中断之前的CPSR寄存器,以保证完成中断处理后,使处理器状态和没有发生中断一样。

MRS     R3, SPSR           ; 保存状态

STMFD   SP, {R3, SP, LR}^    ; 保存用户状态的R3,SP,LR,注意不能回写

STM的寄存器列表中不包含PC^的作用是保存用户模式下的寄存器。此操作的目的是用于中断嵌套。通过定义IRQ模式的堆栈深度,可以控制中断嵌套的深度。分析整段代码,一次中断响应需要占用832bit的堆栈,定义堆栈深度为n*8,可以允许n级中断嵌套。

此处^操作符不允许回写,经过AXD仿真验证,回写操作会写入sp_usr而不是sp_irq,此操作属于不安全的操作,并且编译之后,ADS编译器会给出响应的警告。

MSR     CPSR_c, #(NoInt | SYS32Mode)    ; 切换到系统模式

CMP     R1, #1

LDREQ   SP, =StackUsr

BL      $IRQ_Exception_Function         ; 调用c语言的中断处理程序

堆栈操作完成之后,切换到系统模式下运行C语言中定义的中断复位函数,才能达到中断嵌套的目的。

当中断服务函数执行完毕之后,退出中断。在uC/OS-II的环境下考虑,因为IRQ中断发生之后,会改变任务就绪列表,因此需要在中断退出时确定是否需要进行任务切换。这点在下面的OS_cpu_a.s中会涉及到。

4OS_cpu_a.s分析

移植uC/OS-II,需要改写os_cpu.hOS_cpu_a.sOS_cpu_a.cincludes.h几个文件。在LPC系列的平台,任务级的任务切换和中断级的任务切换是通过软中断(SWISoftware Interrupt)来完成的。

OS_CPU_A.S中包括SoftwareInterrupt、OSIntCtxSw、OSIntCtxSw_1__OSStartHighRdy四个函数。SoftwareInterrupt定义了软件中断处理方法,OSIntCtxSw对应于中断级任务切换和任务级任务切换。而__OSStartHighRdy是uC/OS-II启动时调用的,调用新建的优先级最高的任务。这四个函数的编写需要很好的了解ARM7的内核结构和完全了解uC/OS-II内核运行的原理。

在SoftwareInterrupt主要完成寄存器R0-R3,R12,LRSVC模式(Supervisor)的堆栈中的保存、取得软件中断号并调用相应的处理过程。在OS_CPU.H中可以找到关于几个函数的软件中断定义。

__swi(0x00) void OS_TASK_SW(void);   /*任务级任务切换函数*/

__swi(0x01) void _OSStartHighRdy(void);  /*运行优先级最高的任务*/

__swi(0x02) void OS_ENTER_CRITICAL(void) /*关中断*/

__swi(0x03) void OS_EXIT_CRITICAL(void);  /*开中断*/

由于SWI中断是由指令本身引起的,因此发生中断时,PC值并没有更新,所以计算中断返回地址,并不需要将LR4LR指向的就是发生中断指令的下一条指令。SWI中断下堆栈的操作如下指令所示:

LDR     SP, StackSvc            ; 重新设置堆栈指针

STMFD   SP!, {R0-R3, R12, LR} ; 寄存器入栈

…… ; 软件中断处理

LDMFD   SP!, {R0-R3, R12, PC}^ ; 寄存器出栈,返回

OS_TASK_SW是任务级任务切换函数,引起0x00号软中断,并最终会跳转到OSIntCtxSw执行,OSIntCtxSw也是中断级任务切换的函数。

对OSIntCtxSw的理解,实际上是要理解各个任务的堆栈设置。一个任务正常运行,是运行在user模式下,根据ARM7的体系结构,user模式的所有寄存器和system模式下的所有寄存器使用同一个物理地址。其他模式R13R14不同物理地址,FIQ模式R8-R14不同物理地址。其他模式相比user/sys模式,多一个SPSR寄存器,用于保存发生中断时的CPSR的备份。因此,当发生任务切换,需要保存当前任务的所有寄存器,即R0-R14

OSIntCtxSw 

LDR     R2, [SP, #20]                      ;获取PC (LR_SVC)

LDR     R12, [SP, #16]                     ;获取R12

MRS     R0, CPSR ;保存当前SVC模式到R0

MSR     CPSR_c, #(NoInt | SYS32Mode) ;切换到系统模式

MOV     R1, LR ;保存LR_sys

STMFD   SP!, {R1-R2}                     ;保存LR_sys, PC (LR_SVC)

STMFD   SP!, {R4-R12}                    ;保存R4-R12

MSR     CPSR_c, R0    ;R0中恢复到SVC模式

LDMFD   SP!, {R4-R7}                    ;获取R0-R3SVC堆栈

ADD     SP, SP, #8                        ;出栈R12,PC,计算SP_SVC

MSR     CPSR_c, #(NoInt | SYS32Mode)    ; 切换到系统模式

STMFD   SP!, {R4-R7}                     ;保存R0-R3

需要注意,执行MSR CPSR_c, #(NoInt | SYS32Mode)之前的SP是指SP_SVC,而模式切换之后,SP对应于SP_user

注意ADS采用的是满递减堆栈。

此段代码的作用是在系统模式下,依次入栈PC,(即LR_SVC),LRLR_SYSR12R11,……R4。然后切换回SVC模式下出栈R0-R3,在切换回SYSTEM模式下入栈R3R2R1R0

当以上入栈工作完成之后,再保存OSEnterSumCPSR即可完成当前任务的上下文环境的保存。

LDR     R1, =OsEnterSum                     ;获取OsEnterSum

LDR     R2, [R1]

STMFD   SP!, {R2, R3}                       ;保存CPSR,OsEnterSum

从上述程序分析可以看出,一个任务的上下文环境入栈如下图所示:

当任务的入栈工作完成之后,需要把当前的SP_SYS保存到当前任务TCB块的栈顶指针。

LDR     R1, =OSTCBCur

LDR     R1, [R1]

STR     SP, [R1]

至此,任务的上下文保存工作完成,下面需要进行任务切换。第一步是把就绪列表中最高优先级OSPrioHighRdy赋值给OSPrioCur,然后把最高优先级任务对应的TCB OSTCBHighRdy赋值给当前任务TCB OSTCBCur。之后运行到OSIntCtxSw_1,进行新任务的数据出栈,切换到新任务的运行。

这里值得一提的是以下两条语句:

ADD     SP, R4, #68 

LDR     LR, [SP, #-8]

之所以对于SP进行加68的操作,是因为此处的SP处理器在系统模式下的堆栈指针,也就是uC/OS-II的用户程序运行的任务堆栈指针。即将运行的任务的数据在这两条语句之后会通过设置SVC模式下的堆栈出栈,然后就将PC设置到新任务的入口处开始运行任务代码了。这样,我们没有机会再修改即将运行的任务的堆栈指针了。若在恢复任务之前,不修改SP_SYS,会导致有数据压栈,有数据出栈,但是堆栈指针却一直向栈底方向运动,没有往栈顶方向运动的情况出现,任务长期运行,被切换,那么任务的堆栈会溢出,出现了内存泄漏。那么之所以是68,是因为任务的上下文切换是17个寄存器的内容,每个寄存器是32bit,所以是68=17*4

那么LDR     LR, [SP, #-8]的原因又是什么呢?为什么在没有恢复待运行任务的数据之前,要先恢复系统模式下的LR寄存器呢?原因同上,当新任务运行后,程序再没有机会修改系统模式下的LR寄存器了,这样会导致任务跑飞。OSIntCtxSw_1的最后一句:LDMFD   SP!, {R0-R12, LR, PC }^,恢复了新任务。当时仔细调试程序会发现,MSR     SPSR_cxsf, R5,恢复处理器模式是在把堆栈的数据恢复到PC之后才会改变到系统模式下,因此可以得出结论,这里出栈的倒数第二个数据,就是寄存器列表中的LR,是SVC模式下的LR,并不是系统模式下的LR。所以,需要在任务数据出栈之前先恢复LR_SYS

至此,关于uC/OS-II的移植部分的分析全部完毕。至于OS_CPU_A.C中的一个函数是初始化任务堆栈。值得注意的地方是,ADS使用的是满递减堆栈。因此需要在OS_CPU.H中把OS_STK_GROWTH设置为1

四.uC/OS-II的使用

通过全部阅读uC/OS-II的代码,个人总结了一下uC/OS-II的核心算法。主要是通过对OSUnMapTbl这张表的读取,才能确定优先级最高的就绪任务。这张表的编码规则是:对应偏移量的数值中,出现第一个二进制”1”的位置。由于算出优先级最高的任务是通过查表完成的,因此任务个数的多少对于任务的调度没有影响。同样的道理,对于事件的管理,也是一样通过OSUnMapTbl来完成的。只要理解了这张表,也就理解了OSRdyGrp、OSRdyTbl[]OSEventGrp、OSEventTbl[]的使用。唯一的一点不同在于,新建任务的状态是:OS_STAT_RDY,任务处于就绪态。而新建事件之后,比如邮箱:新建一个邮箱调用OSMboxCreate完成,返回申请成功的邮箱地址,然后任务需要等待邮件的接受,调用OSMboxPend,通过从对应的OSRdyTbl中删除该任务,达到挂起任务的目的。

另外,关于如何新建任务,如何使用邮箱、信号量等,关于uC/OS-II的书籍上都会介绍,这里就不再赘述了。

唯一一点需要注意的是,在开始多任务之前,也就是调用OSStart之前,不允许打开时钟节拍中断,防止操作系统崩溃。时钟节拍中断的允许,是在TargetInit中完成的。

五.关于Proteus仿真、总结

此次仿真,以LPC2124为处理器,完成了对串口、SPI口、I2C接口和AD接口转换的编程。LCD采用LM4229T6963C控制器。个人感觉程序比较简单,在这里就不多加叙述了。

在实际仿真的时候,有一个问题无法解决。

由于LCM接口占用了P0.4P0.15,因此PWM的输出我选择了P0.21PWM5,我采用单边输出,匹配后复位TC的做法。若P0.21接示波器,uC/OS-II就无法正常启动了。我打开了串口,然后跟踪调试,发现,当程序调用GUI函数时,就是运行缓慢,P0.4P0.11缓慢读取数据,导致串口输出及其缓慢。开始我以为是LCD驱动程序关于PINSEL引脚的使用错误,但是,转换数据接口到P1口,P1.24-P1.31,占用JTAG口,使用GPIO功能,上述问题依然。当P0.21设置为PWM,但不接示波器,却一切正常。个人认为是PROTEUS本身仿真问题,并不是程序的问题。

通过通读、调试uC/OS-II代码和移植代码,可以更加深刻地理解嵌入式操作系统的工作原理和ARM处理器内核结构。不过,uC/OS-II作为比较简单的操作系统,还是存在一些不足。比如缺乏文件系统和GUI。相比于uclinux,还是显得不足更多。未来的路还很长,期待linux的更加深入的学习和使用!