打印
[STM32F4]

【安富莱STM32F407之uCOS-III教程】第5章 任务切换设计

[复制链接]
6317|13
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
Eric2013|  楼主 | 2014-12-18 19:24 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
本帖最后由 Eric2013 于 2014-12-18 19:28 编辑

特别说明:
1.  本教程是安富莱电子原创。
2.  安富莱STM32F407开发板资料已经全部开源,开源地址:地址链接
3.  当前共配套300多个实例,4套用户手册。


第5章  任务切换设计

    本期教程带领大家学习简易任务切换的设计,主要是学习SVC(Supervisor Call)和PendSV(Pendable Service Call)的使用,并在此基础上设计一个简单的时间片调度器。本期教程要涉及到一些汇编的知识,不过不用担心,用户只需了解一些简单的汇编命令就可以了,最重要的还是理解任务是如何实现切换的。
  5.1 中断的响应序列
  5.2 SVC异常
  5.3 PendSV异常
  5.4 实验例程说明
  5.5 实验总结
5.1  中断的响应序列
    由于SVC和PendSV中断需要用汇编来实现,用汇编来实现就得了解发生中断事件后,处理器内核是如何响应这个过程的。总的来说,当CM3/CM4开始响应一个中断时,会按照如下过程响应中断事件:
  l  入栈:不带浮点寄存器的情况下会有8个寄存器的值压入栈,带浮点寄存器的情况下会有26个寄存器压入栈。
  l  取向量:从向量表中找出对应的服务程序入口地址。
  l  选择堆栈指针MSP/PSP,更新堆栈指针SP,更新连接寄存器LR,更新程序计数器PC。
5.1.1      入栈
    响应异常的第一个行动,就是自动保存现场的必要部分:依次把xPSR,PC, LR, R12以及R3-R0由硬件自动压入适当的堆栈中(如果程序中做了浮点运算,还有18个浮点寄存器需要入栈):如果当响应异常时,当前的代码正在使用PSP,则压入PSP,也就是使用进程堆栈;否则就压入MSP,使用主堆栈。一旦进入了服务例程,就将一直使用主堆栈。
    下表是带有浮点寄存器和不带有浮点寄存器的异常堆栈,地址由上到下递减。
     注意左侧的浮点寄存器列表,在寄存器FPSCR上面是一个保留寄存器,在给任务堆栈分配大小时要考虑到这个寄存器,同时要注意这些寄存器的存储顺序。上面的寄存器是自动入栈的,还有寄存器R4-R11以及浮点寄存器S16-S31需要手动入栈的。这里有三个知识点,大家要注意:
  l  双字对齐的堆栈工作模式(大家可以查阅Cortex-M3权威指南中文版9.1.1小节进行了解,Cortex-M4现在最新的r0p1版,不知道此功能是否自动打开,后面查阅相关资料了,再做详细介绍)。
  l  为什么R0-R3以及R12可以自动的入栈,而R4-R11是手动入栈?原因就在于ARM上,有一套的C函数调用标准约定(《C/C++ Procedure Call Standard for the ARM Architecture》,AAPCS,Ref5)。各种原因就在它上面:它使得中断服务例程能用C语言编写,编译器优先使用入栈了的寄器来保存中间结果(当然,如果程序过大也可能要用到R4-R11,此时编译器负责生成代码来push它们)。
  l  为什么R0-R3, R12是最后被压进去的?这样可以更容易地使用SP基址来索引寻址,(这也方便了LDM等多重加载指令。因为LDM必须加载地址连续的一串数据,而现在R0-R3, R12的存储地址连续了)。这种顺序也舒展了参数的传递过程:使之可以方便地通过读取入栈了的R0-R3取出 (主要为系统软件所利用,多见于SVC与PendSV中的参数传递)。
5.1.2      取向量
    当数据总线(系统总线)正在进行入栈操作时,指令总线(I-Code总线)执行另一项重要的任务:从向量表中找出正确的异常向量,然后在服务程序的入口处预取指。由此可以看到各自都有专用总线的好处:入栈与取指这两个工作能同时进行。
5.1.3      更新寄存器
    在入栈和取向量操作完成之后,执行服务例程之前,还要更新一系列的寄存器:
  l  SP:在入栈后会把堆栈指针(PSP或MSP)更新到新的位置。在执行服务例程时,将由MSP负责对堆栈的访问。
  l  PSR:更新IPSR位段(地处PSR的最低部分)的值为新响应的异常编号。
  l  PC:在取向量完成后,PC将指向服务例程的入口地址。
  l  LR:在出入ISR的时候,LR的值将得到重新的诠释,这种特殊的值称为“EXC_RETURN”(这个在上期教程有讲解)在异常进入时由系统计算并赋给LR,并在异常返回时使用它。
    以上是在响应异常时通用寄存器的变化。另一方面,在NVIC中,也会更新若干个相关有寄存器。例如,新响应异常的悬起位将被清除,同时其活动位将被置位。

沙发
Eric2013|  楼主 | 2014-12-18 19:31 | 只看该作者
本帖最后由 Eric2013 于 2014-12-18 19:33 编辑

5.2  SVC异常
    SVC多用在上了操作系统的软件开发中,不过也不是OS设计所必须的,比如μCOS-III和μCOS-II就没有使用此中断,而RTX却充分的利用了这个中断,为了方便大家的学习,我们对SVC也做一个详细的介绍。
5.2.1      SVC功能介绍
    SVC用于产生系统函数的调用请求。例如,操作系统通常不让用户程序直接访问硬件,而是通过提供一些系统服务函数,让用户程序使用SVC发出对系统服务函数的呼叫请求,以这种方法调用它们来间接访问硬件。因此,当用户程序想要控制特定的硬件时,它就要产生一个SVC异常,然后操作系统提供的SVC异常服务例程得到执行,它再调用相关的操作系统函数,后者完成用户程序请求的服务。
SVC这种“提出要求——得到满足”的方式很好:
  l  它使用户程序从控制硬件的繁文缛节中解脱出来,而是由OS负责控制具体的硬件。
  l  OS的代码可以经过充分的测试,从而能使系统更加健壮和可靠。
  l  它使用户程序无需在特权级下执行,用户程序无需承担因误操作而瘫痪整个系统的风险。
  l  通过SVC的机制,还让用户程序变得与硬件无关,因此在开发应用程序时无需了解硬件的操作细节,从而简化了开发的难度和繁琐度,并且使应用程序跨硬件平台移植成为可能。开发应用程序唯一需要知道的就是操作系统提供的应用编程接口(API),并且在了解了各个请求代号和参数表后,就可以使用SVC来提出要求了(事实上,为使用方便,操作系统往往会提供一层封皮,以使系统调用的形式看起来和普通的函数调用一致。各封皮函数会正确使用SVC指令来执行系统调用)。其实,严格地讲,操作硬件的工作是由设备驱动程序完成的,只是对应用程序来说,它们也相当于操作系统的一部分。如下图所示:
                             
    SVC异常通过执行”SVC”指令来产生。该指令需要一个立即数,充当系统调用代号。SVC异常服务例程稍后会提取出此代号,从而获知本次调用的具体要求,再调用相应的服务函数。例如,
          SVC  0x3 ; 调用3号系统服务
    在SVC服务例程执行后,上次执行的SVC指令地址可以根据自动入栈的返回地址计算出。找到了SVC指令后,就可以读取该SVC指令的机器码,从机器码中萃取出立即数,就获知了请求执行的功能代号。如果用户程序使用的是PSP,服务例程还需要先执行MRS Rn, PSP指令来获取应用程序的堆栈指针。通过分析LR的值,可以获知在SVC指令执行时,正在使用哪个堆栈。
    注意,我们不能在SVC服务例程中嵌套使用SVC指令(事实上这样做也没意义),因为同优先级的异常不能抢占自身。这种作**产生一个用法fault。同理,在NMI服务例程中也不得使用SVC,否则将触发硬fault。

5.2.2      SVC触发方式
    SVC的异常号是11,支持可编程。SVC异常可以由SVC指令来触发,也可以通过NVIC来软件触发(通过寄存器NVIC->STIR触发软中断)。这两种方式触发SVC中断有一点不同:软件触发中断是不精确的,也就是说,抢占行为不一定会立即发生,即使当时它没有被掩蔽,也没有被其它ISR阻塞,也不能保证马上响应。这也是写缓冲造成的,会影响到与操作NVIC STIR相临的后一条指令:如果它需要根据中断服务的结果来决定如何工作(如条件跳转),则该指令可能会误动作——这也可以算是紊乱危象的一种表现形式。为解决这个问题,必须使用一条DSB指令,如下例所示:
MOV R0,#SOFTWARE_INTERRUPT_NUMBER
LDRR1,=0xE000EF00    ; 加载NVIC软件触发中断寄存器的地址
STR   R0,  [R1]        ; 触发软件中断
DSB                    ; 执行数据同步隔离指令
...
    但是这种方式还有另一种隐患:如果欲触发的软件中断被除能了,或者执行软件中断的程序自己也是个异常服务程序,软件中断就有可能无法响应。因此,必须在使用前检查这个中断已经在响应中了。为达到此目的,可以让软件中断服务程序在入口处设置一个标志。而SVC要精确很多,SVC指令后,只要此时没有其它高优先级的异常也发生了,SVC中断服务程序可以得到立即执行。

5.2.3      SVC的使用
    SVC是用于呼叫OS所提供的API(RTX是采用的这种方式)。用户程序只需知道传递给OS的参数,而不必知道各API函数的地址。SVC指令带一个8位的立即数,可以视为是它的参数,被封装在指令本身中,如:
SVC   3    ;呼叫3号系统服务
    则3被封装在这个SVC指令中。因此在SVC服务例程中,需要读取本次触发SVC异常的SVC指令,并提取出8位立即数所在的位段,来判断系统调用号,工作流程如下:

   
    上面的流程图用汇编来实现就是如下这样:
SVC_Handler
TST   LR,  #0x4        ; 测试EXC_RETURN的比特2
ITE   EQ               ; 如果为0,
MRSEQ  R0,  MSP     ; 则使用的是主堆栈,故把MSP的值取出
MRSNE  R0,  PSP      ; 否则, 使用的是进程堆栈,故把MSP的值取出
LDR   R1,  [R0,#24]    ; 从栈中读取PC的值
LDRB   R0,  [R1,#-2]   ; 从SVC指令中读取立即数放到R0
; 准备调用系统服务函数。这需要适当调整入栈的PC的值以及LR(EXC_RETURN),来进入OS内部
BXLR     ; 借异常返回的形式,进入OS内部,最终调用系统服务函数.
    上面的汇编代码结合着流程图就很好理解了,目的只有一个:得到调用号,用它来调用系统服务函数。接下来我们说一下如何在C中使用SVC。
    因为晚到中断的关系(为什么这么说,请看Cortex-M3权威指南中文版11.6小节),SVC中不能再使用寄存器来传递参数,而是必须使用堆栈。因此,需要使用一段汇编代码来给SVC函数传参数。如果SVC服务例程的主部由C来写,则必须在前面伴随一个汇编写的封皮,用于把堆栈中的参数提取到寄存器中。下面给出一段代码来演示这个工作。这些代码是要使用ARM的编译(armcc)和汇编(armasm)工具来处理的,RVDS和Keil RVMDK都使用这个工具链。
/*汇编封皮,用于提出堆栈帧的起始位置,并放到R0中,然后跳转至实际的SVC服务例程中 */
__asmvoid SVC_Handler(void)
{
TSTLR, #4           ; Test bit 2 of EXC_RETURN
ITEEQ
MRSEQR0, MSP     ; if 0, stackingused MSP, copy to R0
MRSNER0, PSP      ;if 1, stacking used PSP, copy to R0
B__cpp(SVC_Handler_C)
ALIGN4
}
/*不必写下BX LR来返回,而是由svc_handler来做决定 */
    接下来的SVC服务例程的主体就可以由C来写了,它使用R0作为输入参数(这也是堆栈帧的起始位置),用于进一步提取服务代号,并且传递参数(通过堆栈中的R0-R3)。
//堆栈内容:
//r0, r1, r2, r3, r12, r14, the return address and xPSR
//- Stacked R0 = svc_args[0]
//- Stacked R1 = svc_args[1]
//- Stacked R2 = svc_args[2]
//- Stacked R3 = svc_args[3]
//- Stacked R12 = svc_args[4]
//- Stacked LR = svc_args[5]
//- Stacked PC = svc_args[6]
//- Stacked xPSR= svc_args[7]
voidSVC_Handler_C(unsigned int * svc_args)
{
uint8_tsvc_number;
uint32_tstacked_r0, stacked_r1, stacked_r2, stacked_r3;

svc_number= ((char *) svc_args[6])[-2];  //Memory[(Stacked PC)-2]
stacked_r0= svc_args[0];
stacked_r1= svc_args[1];
stacked_r2= svc_args[2];
stacked_r3= svc_args[3];
//.other processing
.
// Return result (e.g. sum of first two arguments)
svc_args[0]= stacked_r0 + stacked_r1;
return;
}
    后面会有一个专门的例子跟大家讲解SVC的使用。

使用特权

评论回复
板凳
Eric2013|  楼主 | 2014-12-18 19:39 | 只看该作者
本帖最后由 Eric2013 于 2014-12-18 19:40 编辑

5.3  PendSV异常
    可以说PendSV (PendedService Call)是OS设计中最重要的中断,OS中可以没有SVC的支持,但是PendSV必须得有。PendSV的异常类型时14,优先级可编程。中断可以通过设置ICSR寄存器的挂起位进行触发。不像SVC,PendSV的中断是不准确的,所以它的挂起状态可以在其它高优先级中断服务程序中进行设置,等中断服务程序执行完以后再执行。
鉴于这种特性,我们可以设置PendSV为最低优先级,这样等其它高优先级的中断任务执行完以后再执行PendSV中断,这种特性在OS的上下文切换中非常重要。
5.3.1      OS中使用PendSV
    首先,我们来看一下上下文切换的基本概念。在一个典型的嵌入式OS中,处理时间被分成很多的时间片。例如,在一个系统中有两个任务,两个任务的执行是可选择的,如下图所示(两个任务间通过SysTick进行轮转调度的简单模式),OS内核可以通过下面的方式进行触发:
  l  系统滴答定时器(SYSTICK)中断。
  l  用户任务执行SVC指令。比如,由于应用任务等待事件或者数据,此任务将被挂起,然后调用系统服务切换到另一个任务中。
                 
    上图是两个任务轮转调度的示意图。但若在产生SysTick异常时正在响应一个中断,如下图所示,则SysTick异常会抢占其ISR。在这种情况下,OS是不能执行上下文切换的,否则将使中断请求被延迟,而且在真实系统中延迟时间还往往不可预知——任何有一丁点实时要求的系统都决不能容忍这种事。因此,在CM3/CM4中也是严禁没商量——如果OS在某中断活跃时尝试切入线程模式,将触犯用法fault异常。

    为解决此问题,早期的OS大多会检测当前是否有中断在活跃中,只有在无任何中断需要响应时,才执行上下文切换(切换期间无法响应中断)。然而,这种方法的弊端在于,它可以把任务切换动作拖延很久(因为如果抢占了IRQ,则本次SysTick在执行后不得作上下文切换,只能等待下一次SysTick异常),尤其是当某中断源的频率和SysTick异常的频率比较接近时,会发生“共振”,使上下文切换迟迟不能进行。
    现在好了,PendSV来完美解决这个问题了。PendSV异常会自动延迟上下文切换的请求,直到其它的ISR都完成了处理后才放行。为实现这个机制,需要把PendSV编程为最低优先级的异常。如果OS检测到某IRQ正在活动并且被SysTick抢占,它将悬起一个PendSV异常,以便缓期执行上下文切换。如下图所示。


    为方便大家理解,下面是上图的执行流程:
1.  任务A呼叫SVC来请求任务切换(例如,等待某些工作完成)
2.  OS接收到请求,做好上下文切换的准备,并且悬起一个PendSV异常。
3.  当CPU退出SVC后,它立即进入PendSV,从而执行上下文切换。
4.  当PendSV执行完毕后,将返回到任务B,同时进入线程模式。
5.  发生了一个中断,并且中断服务程序开始执行
6.  在ISR执行过程中,发生SysTick异常,并且抢占了该ISR。
7.  OS执行必要的操作,然后悬起PendSV异常以作好上下文切换的准备。
8.  当SysTick退出后,回到先前被抢占的ISR中,ISR继续执行
9.  ISR执行完毕并退出后,PendSV服务例程开始执行,并且在里面执行上下文切换
10.当PendSV执行完毕后,回到任务A,同时系统再次进入线程模式。

5.3.2      裸机中使用PendSV
    大家最常见的都是在OS中使用PendSV,在裸机中也用到PendSV。比如,一个中断服务程序需要大量的执行时间,这个程序中有一部分需要高优先级中断中执行,如果所有的中断服务程序都在这个高优先级任务中执行将阻塞低优先级中断的执行,为了解决这个问题,我们可以讲这个程序分为两个部分:
  l  前半部分是时间关键的部分,需要在高优先级任务中快速执行。我们将其放在高优先级的中断服务程序中,程序末尾将PendSV中断使能。
  l  后半部分放在PendSV中断中执行,此时PendSV被设置为最低优先级的中断。


使用特权

评论回复
地板
Eric2013|  楼主 | 2014-12-18 19:44 | 只看该作者
本帖最后由 Eric2013 于 2014-12-18 20:00 编辑

5.4  实验例程说明
    一共为本期教程制作了3个例子,下面就跟大家详细讲解这3个例子。这个三个工程都不需要添加额外的文件,将代码都放在了main.c文件中实现。


5.4.1      实验一:SVC的使用
实验目的:
    1.学习SVC异常的使用
实验内容:
    1.创建一个周期为500ms的自动重装定时器
    2.按键K1按下调用SVC 0
    3.按键K2按下调用SVC 1
    4.按键K3按下调用SVC 2
实验现象:
    请用USB转串口线连接PC机和开发板。PC机上运行SecureCRT软件,波特率设置为115200bps,无硬件流控。从PC机的软件界面观察程序执行结果:


程序设计:
本程序主要分为两个部分:
    Ø  SVC的实现
    Ø  主程序
1.    SVC的实现
/*
*********************************************************************************************************
*    函 数 名: SVC_Handler
*    功能说明: SVC异常,汇编的封皮用于获取使用的堆栈指针PSP或MSP
*    形   参:无
*    返 回 值: 无
*********************************************************************************************************
*/
__asm void SVC_Handler(void)
{
     TST LR, #4 ; Test bit 2 ofEXC_RETURN
     ITE EQ
     MRSEQ R0, MSP ; if 0, stackingused MSP, copy to R0
     MRSNE R0, PSP ; if 1, stackingused PSP, copy to R0
     B __cpp(SVC_Handler_C)
     ALIGN 4
}

/*
*********************************************************************************************************
*    函 数 名: SVC_Handler_C
*    功能说明: SVC异常C函数
*    形   参:svc_args 堆栈起始地址
*    返 回 值: 无
*********************************************************************************************************
*/
void SVC_Handler_C(unsigned int * svc_args)
{
     // 堆栈内容:
     // r0, r1, r2, r3, r12, r14, thereturn address and xPSR
     // - Stacked R0 = svc_args[0]
     // - Stacked R1 = svc_args[1]
     // - Stacked R2 = svc_args[2]
     // - Stacked R3 = svc_args[3]
     // - Stacked R12 = svc_args[4]
     // - Stacked LR = svc_args[5]
     // - Stacked PC = svc_args[6]
     // - Stacked xPSR= svc_args[7]
     unsigned int svc_number;
     svc_number = ((char*)svc_args[6])[-2];
     switch(svc_number)
     {
         case 0: svc_args[0] =svc_args[0] + svc_args[1];
         break;
         case 1: svc_args[0] =svc_args[0] - svc_args[1];
         break;
         case 2: svc_args[0] =svc_args[0] + 1;
         break;
         default: // 未知SVC请求
         break;
     }
}

2.     主程序
    这里特别的注意SVC在MDK中的调用方法。
/* 定义 SVC 函数 */
int __svc(0x00) svc_service_add(int x, int y); // 服务0 : 加法
int __svc(0x01) svc_service_sub(int x, int y); // 服务1 : 减法
int __svc(0x02) svc_service_incr(int x);       // 服务2 : 自加

/*
*********************************************************************************************************
*    函 数 名: main
*    功能说明: c程序入口
*    形   参:无
*    返 回 值: 错误代码(无需处理)
*********************************************************************************************************
*/
int main(void)
{   
     uint8_t x, y, z;
     uint8_t ucKeyCode;     /* 按键代码 */

     bsp_Init();           /* 硬件初始化 */

     Printf**();     /* 打印例程信息到串口1 */

     bsp_StartAutoTimer(0, 500); /* 启动1个500ms的自动重装的定时器 */

     /* 进入主程序循环体 */
     while (1)
     {
         bsp_Idle();        /* 这个函数在bsp.c文件。用户可以修改这个函数实现CPU休眠和喂狗 */

         if (bsp_CheckTimer(0)) /* 判断定时器超时时间 */
         {
              /* 每隔500ms 进来一次 */
              bsp_LedToggle(4);  /* 翻转LED4的状态 */
         }

         /* 按键滤波和检测由后台systick中断服务程序实现,我们只需要调用bsp_GetKey读取键值即可。 */
         ucKeyCode = bsp_GetKey();   /* 读取键值, 无键按下时返回KEY_NONE = 0 */
         if (ucKeyCode != KEY_NONE)
         {
              switch (ucKeyCode)
              {
                   case KEY_DOWN_K1:           /* K1键按下 */
                       x=3;
                       y=5;
                       z =svc_service_add(x, y);
                       printf("3+5 = %d \r\n", z);

                       break;

                   case KEY_DOWN_K2:           /* K2键按下 */
                       x=9;
                       y=2;
                       z =svc_service_sub(x, y);
                       printf("9-2 = %d \r\n", z);
                       break;

                   case KEY_DOWN_K3:           /* K3键按下 */
                       x=3;
                       z =svc_service_incr(x);
                       printf("3++ = %d \r\n", z);
                       break;

                   default:
                       /* 其它的键值不处理 */
                       break;
              }
         }
     }
}

使用特权

评论回复
5
Eric2013|  楼主 | 2014-12-18 19:49 | 只看该作者
本帖最后由 Eric2013 于 2014-12-18 20:01 编辑

5.4.2      实验二:任务切换设计(时间片调度不带浮点)
实验目的:
    1.  学习PendSV和简易时间片调度的设计,对于初学者,此程序理解起来比较的难些,有不懂的地方一定要多读几遍,或者查阅相关的资料进行学习,我也争取给大家解释清楚。
实验内容:
    1.工程中创建了四个LED闪烁的任务,并设置嘀嗒定时器中断频率是1ms。
    2. 实现一个简单的时间片调度,每1ms进入嘀嗒定时器后更改要执行的任务并使能PendSV中断,这样退出嘀嗒定时器中断后就能进入PendSV中断实现任务的切换。
实验现象:
    1.    四个LED按照不同的频率进行闪烁。
    2.    请用USB转串口线连接PC机和开发板。PC机上运行SecureCRT软件,波特率设置为115200bps,无硬件流控。从PC机的软件界面观察程序执行结果:


    如果程序进入硬件异常会打印如下的效果:

程序设计:
本程序主要分为五个部分:
    Ø  任务堆栈,PSP初始化
    Ø  嘀嗒定时器中断
    Ø  PendSV中断
    Ø  硬件异常
    Ø  四个任务和主程序
1.    任务堆栈,PSP初始化
    下表是第一次任务切换前,16个寄存器的存储顺序,由上向下地址递减。同时注意以下三点:
    Ø  执行第一个任务时PSP是指向任务的栈顶,后面三个任务第一次执行都是下面初始化时设置的位置。  
    Ø  堆栈空间是采用的双字的堆栈对齐方式,这个在前面5.1.1小节有说明,当然也可以将双字堆栈模式关闭。
    Ø  Task0任务要放在这个函数里面,要不出现硬件异常,造成这个问题的原因就在于从主堆栈指针MSP切换到进程堆栈指针PSP。在main函数中调用子函数OS_Start后部分寄存器会入栈,如果在这个子程序中做堆栈指针切换,退出的时候是用的PSP而不是MSP,用MSP入栈的寄存器数据将无法返回,从而造成失败。



/* 字访问 */
#define HW32_REG(ADDRESS) (*((volatile unsigned long *)(ADDRESS)))
/* 当检测到错误时,用Breakpoint来停止任务(MDK特有的)*/
/* 根据需要可以改成用while(1)来实现 */
#define stop_cpu   __breakpoint(0)

/* 4个任务 */
void task0(void);      
void task1(void);   
void task2(void);      
void task3(void);      
void HardFault_Handler_C(unsigned int * svc_args);

// 计数,用于切换任务
volatile uint32_t systick_count=0;
// 任务堆栈 (8Kbytes each - 1024 x 8 bytes) 这里也可以采用4字节,并将双字的堆栈对齐方式关闭
uint32_t task0_stack[1024], task1_stack[1024], task2_stack[1024],task3_stack[1024];

// 任务切换用到的数据
uint32_t  curr_task=0;     // 当前执行任务
uint32_t  next_task=1;     // 下一个任务
uint32_t  PSP_array[4];    // 用于在任务切换时记录PSP

/*
*********************************************************************************************************
*    函 数 名: OS_Start
*    功能说明: 任务启动
*    形   参:无
*    返 回 值: 无         
*********************************************************************************************************
*/
void OS_Start(void)
{
     /* 创建任务0的堆栈 */
     PSP_array[0] = ((unsigned int)task0_stack) + (sizeof task0_stack) - 16*4;
     HW32_REG((PSP_array[0] +(14<<2))) = (unsigned long) task0; /* PC */
     HW32_REG((PSP_array[0] +(15<<2))) = 0x01000000;           /* xPSR */

     /* 创建任务1的堆栈 */
     PSP_array[1] = ((unsigned int)task1_stack) + (sizeof task1_stack) - 16*4;
     HW32_REG((PSP_array[1] +(14<<2))) = (unsigned long) task1; /* PC */
     HW32_REG((PSP_array[1] +(15<<2))) = 0x01000000;           /* xPSR */

     /* 创建任务2的堆栈 */
     PSP_array[2] = ((unsigned int)task2_stack) + (sizeof task2_stack) - 16*4;
     HW32_REG((PSP_array[2] +(14<<2))) = (unsigned long) task2; /* PC */
     HW32_REG((PSP_array[2] +(15<<2))) = 0x01000000;           /* xPSR */

     /* 创建任务3的堆栈 */
     PSP_array[3] = ((unsigned int)task3_stack) + (sizeof task3_stack) - 16*4;
     HW32_REG((PSP_array[3] +(14<<2))) = (unsigned long) task3; /* PC */
     HW32_REG((PSP_array[3] +(15<<2))) = 0x01000000;           /* xPSR */

     /* 任务0先执行 */
     curr_task = 0;

     /* 设置PSP指向任务0堆栈的栈顶 */
     __set_PSP((PSP_array[curr_task]+ 16*4));

     /* 设置PendSV中断为最低优先级中断 */
     NVIC_SetPriority(PendSV_IRQn,0xFF);

     /* 设置嘀嗒定时器的中断时1ms */
     SysTick_Config(SystemCoreClock /1000);

     /* 使用堆栈指针,非特权级状态 */
     __set_CONTROL(0x3);

     /* 改变CONTROL后执行ISB (architectural recommendation) */
     __ISB();

     /* 启动任务0 */
     task0();  
}

2.    嘀嗒定时器中断
    嘀嗒定时器中断服务程序主要实现时间片的调度的时标,每进入一次实现一次任务切换(实际切换时通过PendSV中断实现)。并使能PendSV中断。
/*
*********************************************************************************************************
*    函 数 名: SysTick_Handler
*    功能说明: 嘀嗒定时器中断
*    形   参:无
*    返 回 值: 无
*********************************************************************************************************
*/
void SysTick_Handler(void)
{
     systick_count++;

     // 简单的 round robin 调度器,时间片是1ms
     // 每ms切换一次。
     switch(curr_task)
     {
         case 0:
              next_task=1;
             break;
         case 1:
              next_task=2;
             break;
         case 2:
              next_task=3;
             break;
         case 3:
              next_task=0;
             break;
         default:
              next_task=0;
              printf("ERROR:curr_task= %xundefined", curr_task);
              stop_cpu;
              break; // 程序不该运行到这里
     }
     if (curr_task!=next_task)
     {
         SCB->ICSR |=SCB_ICSR_PENDSVSET_Msk; // Set PendSV to pending
     }

}

3.     PendSV中断
    PendSV中断是实现任务切换的关键,主要作用是保存当前任务的寄存器内容到任务堆栈中,并从一个任务的堆栈中恢复这个任务的寄存器内容。这里大家要着重的学习以下两个汇编指令:
              STMDB.W  Rd!, {寄存器列表}  存储多个字到Rd处。每存一个字前Rd自减一次,32位宽度。
              LDMDB.W  Rd!, {寄存器列表}  从Rd处读取多个字,并依次送到寄存器列表中的寄存器。每读一个字前Rd自减一次,32位宽度
     如果Rd是R13(即SP),则与 POP/PUSH指令等效。(LDMIA->POP, STMDB ->PUSH)
             STMDB  SP!, {R0-R3, LR} 等效于 PUSH {R0-R3, LR}
             LDMIA  SP!, {R0-R3, PC} 等效于 PUSH {R0-R3, PC}
     Rd后面的“!”是什么意思?它表示要自增(Increment)或自减(Decrement)基址寄存器Rd的值,时机是在每次访问前(Before)或访问后(After)。增/减单位:字(4字节)。例如,记R8=0x8000,则下面两条指令:
             STMIA.WR8!,  {r0-R3} ;  R8值变为0x8010,每存一次增一次,先存储后自增
             STMDB.W R8,   {R0-R3} ; R8值的“一个内部复本”先自减后再存储数据,但R8的值不变
     感叹号还可以用于单一加载与存储指令——LDR/STR。这也就是所谓的 “带预索引”(Pre-indexing)的LDR和STR。例如:
              LDR.W  R0,  [R1,   #20]!    ;预索引
     该指令先把地址R1+offset处的值加载到R0,然后,R1 = R1+ 20(offset也可以是负数)。这里的“!”就是指在传送后更新基址寄存器R1的值。“!”是可选的。如果没有“!”,则该指令就是普通的带偏移量加载指令,不会自动调整R0的值。带预索引的数据传送可以用在多种数据类型上,并且既可用于加载,又可用于存储。
/*
*********************************************************************************************************
*    函 数 名: PendSV_Handler
*    功能说明: 任务切换的实现,这里特别的注意一点:
*             当前任务的16个寄存器全部保存后,PSP指向最后一个寄存器。
*    形   参:无
*    返 回 值: 无
*********************************************************************************************************
*/
__asm void PendSV_Handler(void)
{
     // 保存当前任务的内容
     MRS    R0, PSP    // 得到PSP  R0 = PSP
                        // xPSR, PC, LR, R12, R0-R3已自动保存
     STMDB  R0!,{R4-R11}// 保存R4-R11共8个寄存器得到当前任务堆栈
     LDR    R1,=__cpp(&curr_task)
     LDR    R2,[R1]    // 得到任务ID
     LDR    R3,=__cpp(&PSP_array)
     STR    R0,[R3, R2, LSL #2] // 保存PSP到相应的PSP_array中 R0 = [R3 + R2 << 2]
                               // 左移两位是因为PSP_array是4字节的数组。

     // 加载下一个任务的内容
     LDR    R4,=__cpp(&next_task)
     LDR    R4,[R4]    // 得到下一个任务的ID
     STR    R4,[R1]    // 设置 curr_task = next_task
     LDR    R0,[R3, R4, LSL #2] // 从PSP_array中获取PSP的值
     LDMIA  R0!,{R4-R11}// 将任务堆栈中的数值加载到R4-R11中
     MSR    PSP, R0    // 设置PSP指向此任务
     BX     LR         // 返回
                       // xPSR, PC,LR, R12, R0-R3会自动的恢复
     ALIGN  4
}

使用特权

评论回复
6
Eric2013|  楼主 | 2014-12-18 19:50 | 只看该作者
本帖最后由 Eric2013 于 2014-12-18 20:02 编辑

(续)实验二:任务切换设计(时间片调度不带浮点)
4.     硬件异常
/*
*********************************************************************************************************
*    函 数 名: HardFault_Handler
*    功能说明: 硬件故障中断服务程序
*    形    参:无
*    返 回 值: 无
*********************************************************************************************************
*/
__asm void HardFault_Handler(void)
{
     TST    LR, #4
     ITE    EQ
     MRSEQ  R0, MSP
     MRSNE  R0, PSP
     B      __cpp(HardFault_Handler_C)
}   
/*
*********************************************************************************************************
*    函 数 名: HardFault_Handler_C
*    功能说明: 进入硬件异常后,打印相关变量
*    形    参:无
*    返 回 值: 无
*********************************************************************************************************
*/
void HardFault_Handler_C(unsigned int * svc_args)
{
     puts("程序进入HardFault\r\n");
     printf ("curr_task = %d\r\n", curr_task);
     printf ("next_task = %d\r\n", next_task);
     printf ("PSP #0 = %x\r\n", PSP_array[0]);
     printf ("PSP #1 = %x\r\n", PSP_array[1]);
     printf ("PSP #2 = %x\r\n", PSP_array[2]);
     printf ("PSP #3 = %x\r\n", PSP_array[3]);
     printf ("Stacked PC = %x\r\n", svc_args[6]);
     stop_cpu;
}

5.     四个任务和主程序
/*
*********************************************************************************************************
*    函 数 名: main
*    功能说明: c程序入口
*    形    参:无
*    返 回 值: 错误代码(无需处理)
*********************************************************************************************************
*/
int main(void)
{
     /* (在Cortex-M3 r1p1中需要开启,Cortex-M3 r2px 和 Cortex-M4是默认开启的) */
     SCB->CCR |= SCB_CCR_STKALIGN_Msk; /* 使能双字的堆栈对齐方式 */
   
     bsp_Init();            /* 硬件初始化 */
   
     Printf**();     /* 打印例程信息到串口1 */
   
     OS_Start();  
     /* 进入主程序循环体, 进入这里的话说明程序出错 */
     while (1)
     {
         stop_cpu;
     }
}
/*
*********************************************************************************************************
*    函 数 名: task0
*    功能说明: 任务0实现LED1的闪烁
*    形    参:无
*    返 回 值: 无
*********************************************************************************************************
*/
void task0(void)
{
     while (1)
     {
         if (systick_count & 0x80)
         {
              bsp_LedOn(1);
         }
         else                     
         {
              bsp_LedOff(1);
         }
     }
}
/*
*********************************************************************************************************
*    函 数 名: task1
*    功能说明: 任务1实现LED2的闪烁
*    形    参:无
*    返 回 值: 无
*********************************************************************************************************
*/
void task1(void)
{
     while (1)
     {
         if (systick_count & 0x100)
         {
              bsp_LedOn(2);
         }
         else                     
         {
              bsp_LedOff(2);
         }
     }
}
/*
*********************************************************************************************************
*    函 数 名: task2
*    功能说明: 任务2实现LED2的闪烁
*    形    参:无
*    返 回 值: 无
*********************************************************************************************************
*/
void task2(void)
{
     while (1)
     {
         if (systick_count & 0x200)
         {
              bsp_LedOn(3);
         }
         else                     
         {
              bsp_LedOff(3);
         }
     }
}
/*
*********************************************************************************************************
*    函 数 名: task3
*    功能说明: 任务3实现LED4的闪烁
*    形    参:无
*    返 回 值: 无
*********************************************************************************************************
*/
void task3(void)
{
     while (1)
     {
         if (systick_count & 0x400)
         {
              bsp_LedOn(4);
         }
         else                     
         {
              bsp_LedOff(4);
         }
     }
}   

使用特权

评论回复
7
Eric2013|  楼主 | 2014-12-18 19:55 | 只看该作者
本帖最后由 Eric2013 于 2014-12-18 19:57 编辑

5.4.3      实验三:任务切换设计(时间片调度带浮点)
实验目的:
    1. 学习SVC,PendSV和简易时间片调度的设计
实验内容:
    1. 工程中创建了四个LED闪烁的任务,并设置嘀嗒定时器中断频率是1ms
    2.实现一个简单的时间片调度,每1ms进入嘀嗒定时器后更改要执行的任务并使能PendSV中断。这样退出嘀嗒定时器中断后就能进入PendSV中断实现任务的切换。
    3.任务0打印浮点运行的结果。
实验现象:
    1.    四个LED按照不同的频率进行闪烁。
    2.    请用USB转串口线连接PC机和开发板。PC机上运行SecureCRT软件,波特率设置为115200bps,无硬件流控。从PC机的软件界面观察程序执行结果:

程序设计:
本程序主要分为五个部分:
    Ø  主程序和四个创建的任务
    Ø  SVC中断
    Ø  PendSV中断
    Ø  滴答定时器中断
    Ø  硬件异常
1.     主程序和四个创建的任务
/* 字访问 */
#define HW32_REG(ADDRESS) (*((volatile unsigned long *)(ADDRESS)))
/* 当检测到错误时,用Breakpoint来停止任务(MDK特有的)*/
/* 根据需要可以改成用while(1)来实现 */
#define stop_cpu   __breakpoint(0)
/* 4个任务 */
void task0(void);      
void task1(void);   
void task2(void);      
void task3(void);      
void HardFault_Handler_C(unsigned int * svc_args);
void SVC_Handler_C(unsigned int * svc_args);
void __svc(0x00) os_start(void); // OS 初始化
// 计数,用于切换任务
volatile uint32_t systick_count=0;
// 任务堆栈 (8Kbytes each - 1024 x 8 bytes),8字节对齐
long long task0_stack[1024], task1_stack[1024], task2_stack[1024],task3_stack[1024];
// 任务切换用到的数据
uint32_t  curr_task=0;     // 当前执行任务
uint32_t  next_task=1;     // 下一个任务
uint32_t  PSP_array[4];    // 用于在任务切换时记录PSP
uint32_t  svc_exc_return;  // EXC_RETURN use by SVC
/* 仅允许本文件内调用的函数声明 */
static void Printf**(void);
/*
*********************************************************************************************************
*    函 数 名: main
*    功能说明: c程序入口
*    形   参:无
*    返 回 值: 错误代码(无需处理)
*********************************************************************************************************
*/
int main(void)
{
     bsp_Init();           /* 硬件初始化 */
   
     Printf**();     /* 打印例程信息到串口1 */
     
     os_start();     
     /* 进入主程序循环体 */
     while (1)
     {
         stop_cpu;
     }
}
/*
*********************************************************************************************************
*    函 数 名: task0
*    功能说明: 任务0实现LED1的闪烁
*    形   参:无
*    返 回 值: 无
*********************************************************************************************************
*/
void task0(void)
{
     float Temp = 1.111111f;
     
     while (1)
     {
         if (systick_count &0x80)
         {
              bsp_LedOn(1);
              Temp = Temp +0.000001f;
              printf("Temp =%f\r\n", Temp);
         }
         else                     
         {
              bsp_LedOff(1);
         }
     };
}
/*
*********************************************************************************************************
*    函 数 名: task1
*    功能说明: 任务1实现LED2的闪烁
*    形   参:无
*    返 回 值: 无
*********************************************************************************************************
*/
void task1(void)
{
     while (1)
     {
         if (systick_count &0x100)
         {
              bsp_LedOn(2);
         }
         else                     
         {
              bsp_LedOff(2);
         }
     };
}
/*
*********************************************************************************************************
*    函 数 名: task2
*    功能说明: 任务2实现LED2的闪烁
*    形   参:无
*    返 回 值: 无
*********************************************************************************************************
*/
void task2(void)
{
     while (1)
     {
         if (systick_count &0x200)
         {
              bsp_LedOn(3);
         }
         else                     
         {
              bsp_LedOff(3);
         }
     };
}
/*
*********************************************************************************************************
*    函 数 名: task3
*    功能说明: 任务3实现LED4的闪烁
*    形   参:无
*    返 回 值: 无
*********************************************************************************************************
*/
void task3(void)
{
     while (1)
     {
         if (systick_count &0x400)
         {
              bsp_LedOn(4);
         }
         else                     
         {
              bsp_LedOff(4);
         }
     };
}
2.     SVC中断
    任务堆栈的创建是在SVC中断中实现的。这个过程可以用下面的图来表示:


    寄存器在堆栈中的存储顺序和实验二是一样的,只是多了EXC_RETURN和CONTROL。
/*
*********************************************************************************************************
*    函 数 名: SVC_Handler
*    功能说明: SVC异常中断服务程序
*    形   参:无
*    返 回 值: 无
*********************************************************************************************************
*/
__asm void SVC_Handler(void)
{
     TST    LR, #4                      // 获取使用的MSP还是PSP
     ITE    EQ
     MRSEQ  R0, MSP
     MRSNE  R0, PSP
     LDR    R1, =__cpp(&svc_exc_return) // 保存当前 EXC_RETURN
     STR    LR, [R1]   
     BL     __cpp(SVC_Handler_C)        // 运行 SVC_Handler 的C程序
     LDR    R1, =__cpp(&svc_exc_return) // 加载新的 EXC_RETURN
     LDR    LR, [R1]
     BX     LR
     ALIGN  4
}
/*
*********************************************************************************************************
*    函 数 名: SVC_Handler_C
*    功能说明: 在SVC异常中运行的C代码
*    形   参:无
*    返 回 值: 无*********************************************************************************************************
*/
void SVC_Handler_C(unsigned int * svc_args)
{
     uint8_t svc_number;   
     svc_number = ((char *)svc_args[6])[-2]; // Memory[(Stacked PC)-2]
     switch(svc_number)
     {
         /* 开始任务调度 */
         case (0):
              
              /* 创建任务0的堆栈 */
              PSP_array[0] =((unsigned int) task0_stack) + (sizeof task0_stack) - 18*4;
              HW32_REG((PSP_array[0]+ (16<<2))) = (unsigned long) task0; // 初始化PC
              HW32_REG((PSP_array[0]+ (17<<2))) = 0x01000000;           // 初始化 xPSR
              HW32_REG((PSP_array[0]          )) = 0xFFFFFFFDUL;          // 初始化 EXC_RETURN
              HW32_REG((PSP_array[0]+ ( 1<<2))) = 0x3;// 初始化 CONTROL : unprivileged, PSP, no FP
              /* 创建任务1的堆栈 */
              PSP_array[1] =((unsigned int) task1_stack) + (sizeof task1_stack) - 18*4;
              HW32_REG((PSP_array[1]+ (16<<2))) = (unsigned long) task1; // 初始化 Program Counter
              HW32_REG((PSP_array[1]+ (17<<2))) = 0x01000000;           // 初始化 xPSR
              HW32_REG((PSP_array[1]          )) = 0xFFFFFFFDUL;          // 初始化 EXC_RETURN
              HW32_REG((PSP_array[1]+ ( 1<<2))) = 0x3;// 初始化 CONTROL : unprivileged, PSP, no FP
              /*创建任务2的堆栈 */
              PSP_array[2] =((unsigned int) task2_stack) + (sizeof task2_stack) - 18*4;
              HW32_REG((PSP_array[2]+ (16<<2))) = (unsigned long) task2; // 初始化 Program Counter
              HW32_REG((PSP_array[2]+ (17<<2))) = 0x01000000;           // 初始化 xPSR
              HW32_REG((PSP_array[2]          )) = 0xFFFFFFFDUL;          // 初始化 EXC_RETURN
              HW32_REG((PSP_array[2]+ ( 1<<2))) = 0x3;// 初始化 CONTROL : unprivileged, PSP, no FP
              /* 创建任务3的堆栈 */
              PSP_array[3] =((unsigned int) task3_stack) + (sizeof task3_stack) - 18*4;
              HW32_REG((PSP_array[3]+ (16<<2))) = (unsigned long) task3; // 初始化 Program Counter
              HW32_REG((PSP_array[3]+ (17<<2))) = 0x01000000;           // 初始化 xPSR
              HW32_REG((PSP_array[3]          )) = 0xFFFFFFFDUL;          // 初始化 EXC_RETURN
              HW32_REG((PSP_array[3]+ ( 1<<2))) = 0x3;// 初始化 CONTROL : unprivileged, PSP, no FP
              curr_task = 0; // 切换到 task #0
              svc_exc_return =HW32_REG((PSP_array[curr_task])); // 返回线程模式,PSP
              __set_PSP((PSP_array[curr_task]+ 10*4));  // PSP = R0
              NVIC_SetPriority(PendSV_IRQn,0xFF); // 设置PSP为最低优先级
              SysTick_Config(SystemCoreClock/1000);//中断频率1000Hz
              __set_CONTROL(0x3);                  // 切换到使用PSP,非特权级
              __ISB();      
              break;
         default:
              puts ("ERROR:Unknown SVC service number");
              printf("- SVCnumber 0x%x\n", svc_number);
              stop_cpu;
              break;
     }
}

3.     PendSV中断
这个中断比较重要,重点看浮点寄存器的入栈和出栈实现。
/*
*********************************************************************************************************
*    函 数 名: PendSV_Handler
*    功能说明: 任务切换的实现,根据需要加载或者存储浮点寄存器
*    形   参:无
*    返 回 值: 无
*********************************************************************************************************
*/
__asm void PendSV_Handler(void)
{
     
     // 保存当前任务的寄存器
     MRS      R0, PSP                 //得到PSP  R0 = PSP
     TST      LR, #0x10              // 检测bit 4. 如果是0的话,需要保存浮点寄存器
     IT       EQ
     VSTMDBEQ R0!, {S16-S31}          // 将浮点寄存器入栈
     MOV      R2, LR
     MRS      R3, CONTROL            // R3 = CONTROL
     STMDB    R0!,{R2-R11}            // 保存 LR,CONTROL and R4 to R11 in task stack (10 个寄存器)
     LDR      R1,=__cpp(&curr_task)
     LDR      R2,[R1]                 //得到当前任务ID
     LDR      R3,=__cpp(&PSP_array)
     STR      R0,[R3, R2, LSL #2]    // 保存PSP到相应的PSP_array中
     // 加载下一个任务堆栈中的数据到相应寄存器
     LDR      R4,=__cpp(&next_task)
     LDR      R4,[R4]                 //得到下一个任务ID
     STR      R4,[R1]                 //设置curr_task = next_task
     LDR      R0,[R3, R4, LSL #2]    // 从 PSP_array 中加载PSP
     LDMIA    R0!,{R2-R11}            // 加载 LR, CONTROL 和 R4 - R11 从任务堆栈中 (10 个寄存器)
     MOV      LR, R2
     MSR      CONTROL, R3            // CONTROL = R3
     TST      LR, #0x10               //检测bit 4. 如果是0的话,需要保存浮点寄存器
     IT       EQ
     VLDMIAEQ R0!, {S16-S31}          // 加载浮点寄存器
     MSR      PSP, R0                 //PSP =R0
     BX       LR                      //返回
     ALIGN  4
}



使用特权

评论回复
8
Eric2013|  楼主 | 2014-12-18 19:55 | 只看该作者
本帖最后由 Eric2013 于 2014-12-18 19:58 编辑

(续)  实验三:任务切换设计(时间片调度带浮点)
4.     滴答定时器中断
/*
*********************************************************************************************************
*    函 数 名: SysTick_Handler
*    功能说明: 嘀嗒定时器中断
*    形    参:无
*    返 回 值: 无
*********************************************************************************************************
*/
void SysTick_Handler(void)
{
     systick_count++;
   
     // 简单的 round robin 调度器,时间片是1ms
     // 每ms切换一次。
     switch(curr_task)
     {
         case 0:
              next_task=1;
             break;
         case 1:
              next_task=2;
             break;
         case 2:
              next_task=3;
             break;
         case 3:
              next_task=0;
             break;
         default:
              next_task=0;
              printf("ERROR:curr_task = %x\n", curr_task);
              stop_cpu;
              break; // 程序不该运行到这里
     }
     if (curr_task!=next_task)
     {
         SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk; // Set PendSV to pending
     }
}


5.     硬件异常
/*
*********************************************************************************************************
*    函 数 名: HardFault_Handler
*    功能说明: 硬件故障中断服务程序
*    形    参:无
*    返 回 值: 无
*********************************************************************************************************
*/
__asm void HardFault_Handler(void)
{
     TST    LR, #4
     ITE    EQ
     MRSEQ  R0, MSP
     MRSNE  R0, PSP
     B      __cpp(HardFault_Handler_C)
}   
/*
*********************************************************************************************************
*    函 数 名: HardFault_Handler_C
*    功能说明: 进入硬件异常后,打印相关变量
*    形    参:无
*    返 回 值: 无
*********************************************************************************************************
*/
void HardFault_Handler_C(unsigned int * svc_args)
{
     puts("[HardFault]");
     printf ("curr_task = %d\n", curr_task);
     printf ("next_task = %d\n", next_task);
     printf ("PSP #0 = %x\n", PSP_array[0]);
     printf ("PSP #1 = %x\n", PSP_array[1]);
     printf ("PSP #2 = %x\n", PSP_array[2]);
     printf ("PSP #3 = %x\n", PSP_array[3]);
     printf ("Stacked PC = %x\n", svc_args[6]);
     stop_cpu;
}

使用特权

评论回复
9
Eric2013|  楼主 | 2014-12-18 19:59 | 只看该作者
5.5  实验总结
    本期教程相对于前面几期要难一些,需要多花点时间去学习,掌握好了,后面学习μCOS-III事半功倍。



参考资料:
1.    Patterns fortime-triggered embedded systems英文版和中文版
2.    Cortex-M3权威指南中文版
3.    TheDefinitive Guide to Arm Cortex-M3 and Cortex-M4 Processors(M4权威指南)


使用特权

评论回复
10
小班儿| | 2014-12-19 09:07 | 只看该作者
支持下楼主……为什么没有人赞呢

使用特权

评论回复
11
Eric2013|  楼主 | 2014-12-19 09:49 | 只看该作者
小班儿 发表于 2014-12-19 09:07
支持下楼主……为什么没有人赞呢

没关系的,有兄弟就够啦:lol:handshake

使用特权

评论回复
12
小班儿| | 2014-12-19 13:19 | 只看该作者
Eric2013 发表于 2014-12-19 09:49
没关系的,有兄弟就够啦

:handshake

使用特权

评论回复
13
117师| | 2017-10-27 17:37 | 只看该作者
楼主威武

使用特权

评论回复
14
tkyl01| | 2017-12-19 09:12 | 只看该作者
这个很有用,收下了

使用特权

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

本版积分规则

个人签名:RTX->μCOS-II->FreeRTOS->embOS->μCOS-III μCGUI->emWin->FatFs->DSP 淘宝:armfly.taobao.com

115

主题

639

帖子

34

粉丝