endSV优先级寄存器的地址NVIC_PENDSV_PRI EQU 0xFF
endSV中断的优先级为255(最低) NVIC_PENDSVSET EQU 0x10000000 ;位28为1定义几个常量,类似C语言中的#define预处理指令。OS_CPU_SR_SaveMRS R0, PRIMASK ;读取PRIMASK到R0中,R0为返回值CPSID I
RIMASK=1,关中断(NMI和硬fault可以响应)BX LR ;返回OS_CPU_SR_RestoreMSR PRIMASK, R0 ;读取R0到PRIMASK中,R0为参数BX LR ;返回OSStartHighRdy()由OSStart()调用,用来启动最高优先级任务,当然任务必须在OSStart()前已被创建。OSStartHighRdy ;设置PendSV中断的优先级 #1LDR R0, =NVIC_SYSPRI14 ;R0 = NVIC_SYSPRI14LDR R1, =NVIC_PENDSV_PRI ;R1 = NVIC_PENDSV_PRISTRB R1, [R0] ;*(uint8_t *)NVIC_SYSPRI14 = NVIC_PENDSV_PRI;设置PSP为0 #2MOVS R0, #0 ;R0 = 0MSR PSP, R0
SP = R0;设置OSRunning为TRUE LDR R0, =OSRunning ;R0 = OSRunning MOVS R1, #1 ;R1 = 1STRB R1, [R0] ;OSRunning = 1;触发PendSV中断 #3 LDR R0, =NVIC_INT_CTRL ;R0 = NVIC_INT_CTRLLDR R1, =NVIC_PENDSVSET ;R1 = NVIC_PENDSVSETSTR R1, [R0] ;*(uint32_t *)NVIC_INT_CTRL = NVIC_PENDSVSETCPSIE I ;开中断OSStartHang ;死循环,应该不会到这里B OSStartHang#1.PendSV中断的优先级应该为最低优先级,原因在<<ARM Cortex-M3权威指南>>的7.6节已有说明。#2.PSP设置为0,是告诉具体的任务切换程序(OS_CPU_PendSVHandler()),这是第一次任务切换。做过切换后PSP就不会为0了,后面会看到。#3.往中断控制及状态寄存器ICSR(0xE000ED04)第28位写1即可产生PendSV中断。这个<<ARM Cortex-M3权威指南>>8.4.5 其它异常的配置寄存器有说明。当一个任务放弃cpu的使用权,就会调用OS_TASK_SW()宏,而OS_TASK_SW()就是OSCtxSw()。OSCtxSw()应该做任务切换。但是在CM3中,所有任务切换都被放到PendSV的中断处理函数中去做了,因此OSCtxSw()只需简单的触发PendSV中断即可。OS_TASK_SW()是由OS_Sched()调用。void OS_Sched (void){#if OS_CRITICAL_METHOD == 3 OS_CPU_SR cpu_sr = 0; #endifOS_ENTER_CRITICAL(); if (OSIntNesting == 0) { if (OSLockNesting == 0) { OS_SchedNew(); if (OSPrioHighRdy != OSPrioCur) { OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy]; #if OS_TASK_PROFILE_EN > 0 OSTCBHighRdy->OSTCBCtxSwCtr++; #endif OSCtxSwCtr++; OS_TASK_SW(); } } }OS_EXIT_CRITICAL();}OSCtxSw ;触发PendSV中断 LDR R0, =NVIC_INT_CTRL ;R0 = NVIC_INT_CTRL LDR R1, =NVIC_PENDSVSET ;R1 = NVIC_PENDSVSET STR R1, [R0] ;*(uint32_t *)NVIC_INT_CTRL = NVIC_PENDSVSET BX LR ;返回当一个中断处理函数退出时,OSIntExit()会被调用来决定是否有优先级更高的任务需要执行。如果有OSIntExit()对调用OSIntCtxSw()做任务切换。OSIntCtxSw ;触发PendSV中断 LDR R0, =NVIC_INT_CTRL LDR R1, =NVIC_PENDSVSET STR R1, [R0] BX LR看到这里有些同学可能奇怪怎么OSCtxSw()和OSIntCtxSw()完全一样,事实上,这两个函数的意义是不一样的,OSCtxSw()做的是任务之间的切换,如任务A因为等待某个资源或是做延时切换到任务B,而OSIntCtxSw()则是中断退出时,由中断状态切换到另一个任务。由中断切换到任务时,CPU寄存器入栈的工作已经做完了,所以无需做第二次了(参考邵老师书的3.10节)。这里只不过由于CM3的特殊机制导致了在这两个函数中只要做触发PendSV中断即可,具体切换由PendSV中断来处理。前面已经说过真正的任务切换是在PendSV中断处理函数里做的,由于CM3在中断时会有一半的寄存器自动保存到任务堆栈里,所以在PendSV中断处理函数中只需保存R4-R11并调节堆栈指针即可。PendSV中断处理函数伪代码如下:OS_CPU_PendSVHandler(){ if (PSP != NULL) {Save R4-R11 onto task stack;OSTCBCur->OSTCBStkPtr = SP; } OSTaskSwHook(); OSPrioCur = OSPrioHighRdy; OSTCBCur = OSTCBHighRdy;PSP = OSTCBHighRdy->OSTCBStkPtr; Restore R4-R11 from new task stack; Return from exception;}OS_CPU_PendSVHandler ;xPSR, PC, LR, R12, R0-R3已自动保存 CPSID I ;任务切换期间需要关中断MRS R0, PSP ;R0 = PSP ;如果PSP == 0,跳到OS_CPU_PendSVHandler_nosave执行 #1 CBZ R0, OS_CPU_PendSVHandler_nosave;保存R4-R11到任务堆栈 SUBS R0, R0, #0x20 ;R0 -= 0x20 STM R0, {R4-R11} ;保存R4-R11到任务堆栈;OSTCBCur->OSTCBStkPtr = SP; LDR R1, =OSTCBCur ;R1 = &OSTCBCur LDR R1, [R1] ;R1 = *R1 (R1 = OSTCBCur) STR R0, [R1] ;*R1 = R0 (*OSTCBCur = SP) #2OS_CPU_PendSVHandler_nosave;调用OSTaskSwHook()PUSH {R14} ;保存R14,因为后面要调用函数 LDR R0, =OSTaskSwHook ;R0 = &OSTaskSwHook BLX R0 ;调用OSTaskSwHook() POP {R14} ;恢复R14;OSPrioCur = OSPrioHighRdy; LDR R0, =OSPrioCur ;R0 = &OSPrioCur LDR R1, =OSPrioHighRdy ;R1 = &OSPrioHighRdy LDRB R2, [R1] ;R2 = *R1 (R2 = OSPrioHighRdy) STRB R2, [R0] ;*R0 = R2 (OSPrioCur = OSPrioHighRdy);OSTCBCur = OSTCBHighRdy; LDR R0, =OSTCBCur ;R0 = &OSTCBCur LDR R1, =OSTCBHighRdy ;R1 = &OSTCBHighRdy LDR R2, [R1] ;R2 = *R1 (R2 = OSTCBHighRdy) STR R2, [R0] ;*R0 = R2 (OSTCBCur = OSTCBHighRdy)LDR R0, [R2] ;R0 = *R2 (R0 = OSTCBHighRdy), 此时R0是新任务的SP;SP = OSTCBHighRdy->OSTCBStkPtr #3LDM R0, {R4-R11} ;从任务堆栈SP恢复R4-R11ADDS R0, R0, #0x20 ;R0 += 0x20MSR PSP, R0
SP = R0,用新任务的SP加载PSPORR LR, LR, #0x04 ;确保LR位2为1,返回后使用进程堆栈 #4CPSIE I ;开中断BX LR ;中断返回END#1 如果PSP == 0,说明OSStartHighRdy()启动后第一次做任务切换,而任务刚创建时R4-R11已经保存在堆栈中了,所以不需要再保存一次了。#2 OSTCBStkPtr是任务控制块结构体的第一个变量,所以*OSTCBCur = SP(不是很科学)就是OSTCBCur->OSTCBStkPtr = SP;#3 和#2类似。#4 因为在中断处理函数中使用的是MSP,所以在返回任务后必须使用PSP,所以LR位2必须为1。os_dbg.c用于系统调试,可以不管。需要修改的代码就介绍到这里,如果还有不明白之处,就再看看AN-1018.pdf,邵老师的书和<<ARM Cortex-M3权威指南>>。ucosii在stm32上的移植详解4详解3中有一个问题还没解释,就是stm32f10x_it.c中已经有SysTick中断函数的定义SysTick_Handler(),为什么官方版非要弄个OS_CPU_SysTickHandler()。答案就在启动文件上,一般我们自己开发基于stm32芯片的软件,都会使用标准外设库CMSIS中提供的启动文件,而官方移植的启动文件却是自己写的,在两个文件init.s,vectors.s中(Micrium\Software\EvalBoards\ST\STM3210B-EVAL\RVMDK)。init.s负责进入main(),vectors.s设置中断向量。OS_CPU_SysTickHandler和OS_CPU_PendSVHandler就是在vectors.s中被设置的。我的移植是使用标准外设库CMSIS中startup_stm32f10x_hd.s作为启动文件的,那该怎么在这个文件中设置OS_CPU_SysTickHandler呢,事实上在startup_stm32f10x_hd.s文件中,PendSV中断向量名为PendSV_Handler,所以只需用OS_CPU_PendSVHandler把所有出现PendSV_Handler的地方替换掉就可以了。那么为什么OS_CPU_SysTickHandler不用这种方式处理呢,这样也就不用注释os_cpu.c中的OS_CPU_SysTickHandler(),这主要是基于两个原因:1. startup_stm32f10x_hd.s尽量少该,能不改就不改。2. 如果保留OS_CPU_SysTickHandler(),在以后开发过程中,改动OS_CPU_SysTickHandler()中的内容可能性是非常大的,如果一不小把该文件其他部分改了造成了问题,这个bug就非常难查了,所以我一般移植好后就把ucosii的这些文件设置为只读。对于上面的原因1,一开始移植时,我曾做过在PendSV_Handler()中调用OS_CPU_PendSVHandler(),后来发现这样不行,这是为什么呢?问题出在LR寄存器上。PendSV_Handler(){OS_CPU_PendSVHandler();}汇编出来的代码会是这样: PendSV_Handler PROC PUSH {r4,lr} BL OS_CPU_PendSVHandler POP {r4,pc} ENDP这样在进入OS_CPU_PendSVHandler之后,LR寄存器中存放的是指令POP {r4,pc}的地址+1。在OS_CPU_PendSVHandler中的ORR LR, LR, #0x04就不会起作用,也就无法使用PSP,移植因此失败。其实在AN-1018.pdf的3.04.06中也有强调OS_CPU_PendSVHandler必须被放置在中断向量表中。一开始我也没注意。到这里移植的大部分工作都做完了,下面剩下的就是把工程配置好,SysTick中断处理好。在工程中建立ucosii组,把ucosii下的文件都加进该组。这里别忘了把os_cpu_a.asm加入。在工程的Options中,c/c++选项卡的Include Paths中添加.\src\ucosii\src;.\src\ucosii\port。编译工程,会发现缺少app_cfg.h和os_cfg.h文件,app_cfg.h是用来配置应用软件的,主要是任务的优先级和堆栈大小,中断优先级等信息。目前还没有基于ucosii开发应用软件,所以只需在include文件夹中创建一个空的app_cfg.h文件即可。os_cfg.h是用来配置ucosii系统的。拷贝Micrium\Software\EvalBoards\ST\STM3210B-EVAL\RVMDK\OS-Probe\os_cfg.h到template\include,对其做如下修改:#define OS_APP_HOOKS_EN 0 #define OS_DEBUG_EN 0 #define OS_EVENT_MULTI_EN 0 #define OS_SCHED_LOCK_EN 0 #define OS_TICK_STEP_EN 0 #define OS_TASK_CHANGE_PRIO_EN 0 #define OS_TASK_QUERY_EN 0 #define OS_TASK_STAT_EN 0 #define OS_TASK_STAT_STK_CHK_EN 0 #define OS_TASK_SUSPEND_EN 0 #define OS_FLAG_EN 0 #define OS_MBOX_EN 0 #define OS_TIME_DLY_HMSM_EN 0 #define OS_TIME_DLY_RESUME_EN 0 #define OS_TIME_GET_SET_EN 0 #define OS_TIME_TICK_HOOK_EN 0所做的修改主要是把一些功能给去掉,减少内核大小,也利于调试。等移植完成后,如果需要该功能,再做开启。接下来就剩下处理好SysTick中断和启动任务了。SysTick是系统的“心跳”,本质上来说就是一个定时器。先把原来main.c中的内容删除,添加如下代码:#include "ucos_ii.h" #include "stm32f10x.h"static OS_STK startup_task_stk[STARTUP_TASK_STK_SIZE];static void systick_init(void) { RCC_ClocksTypeDef rcc_clocks; RCC_GetClocksFreq(&rcc_clocks); SysTick_Config(rcc_clocks.HCLK_Frequency / OS_TICKS_PER_SEC); }static void startup_task(void *p_arg) { systick_init();#if (OS_TASK_STAT_EN > 0) OSStatInit(); #endifOSTaskDel(OS_PRIO_SELF); }int main(void) { OSInit(); OSTaskCreate(startup_task, (void *)0, &startup_task_stk[STARTUP_TASK_STK_SIZE - 1], STARTUP_TASK_PRIO); OSStart(); return 0; }systick_init()用来初始化并启动SysTick定时器。 RCC_GetClocksFreq()用来获取系统时钟。 SysTick_Config()初始化并使能SysTick定时器。这里要注意的是OS_TICKS_PER_SEC,它是每秒钟的ticks数,如果为1000,就是1s中1000个ticks,也就是说1ms就会产生一个SysTick中断。系统的时间片为1ms。在邵老师的书中3.11节已有明确说明,必须在调用OSStart()之后,才能开启时钟节拍器(SysTick)。一般会把它放在第一个任务(启动任务)中。startup_task()用来创建其他应用任务,创建完其他任务后,就会自己删除自己。文件中的STARTUP_TASK_STK_SIZE,STARTUP_TASK_PRIO需要在app_cfg.h中定义。代码如下: #define STARTUP_TASK_PRIO 4#define STARTUP_TASK_STK_SIZE 80在stm32f10x_it.c中,还需要添加SysTick中断的处理代码:void SysTick_Handler(void) { OSIntEnter(); OSTimeTick(); OSIntExit(); }这个代码是仿照OS_CPU_SysTickHandler()中代码的,在邵老师书的3.11节亦有说明。这里就不解释。至此ucosii在stm32上的移植已全部完成。ucosii在stm32上的移植详解5详解1-4把移植过程都已经介绍了。接下来的工作是验证移植是否ok以及如何基于移植好的ucosii开发应用程序。前一个问题可以说是后一个问题的特殊情况,一般我们会创建两个简单的任务,看看任务切换是否成功来验证移植是否ok,因为任务切换可以说是ucosii最核心的功能。任务代码(main.c):static void task1(void *p_arg) {for (;;){led_on(LED_0); OSTimeDly(500); led_off(LED_0); OSTimeDly(500); } }static void task2(void *p_arg) { for (;;) { led_on(LED_1); OSTimeDly(500); led_off(LED_1); OSTimeDly(500); } }在startup_task()创建任务: err = OSTaskCreate(task1, (void *)0, &task1_stk[TASK1_STK_SIZE-1], TASK1_PRIO);err = OSTaskCreate(task2, (void *)0, &task2_stk[TASK2_STK_SIZE-1], TASK2_PRIO);把任务的堆栈大小和优先级写入app_cfg.h,定义任务堆栈,编译调试。在任务中打断点,用模拟器调试可以发现已经可以做任务切换了。如果有板子,烧到板子中运行,可以看到两个灯会以1Hz的频率闪烁。可以认为移植初步成功,内核其他功能有待在应用中继续验证。如何基于移植好的ucosii开发应用程序呢? 开发应用程序大部分都是为了处理或控制一个真实的物理系统,而真实的物理系统往往都是模拟系统,为了方便计算机处理,首先需要对系统做离散化处理。针对ucosii,离散化过程是通过系统“心跳”(SysTick)来实现的。一般应用程序都有多个任务(不多任务谁用ucosii啊),任务可以分为周期任务和非周期任务。周期任务是周期性循环地处理事情的任务,而非周期任务一般是某个条件触发才执行的任务。这里有一个问题,SysTick的时间是多少合适。SysTick的时间一般取周期性任务中周期最短的时间值。譬如说,系统里有3个周期性任务:系统主任务(如处理pid等,任务周期4ms),键盘扫描任务(任务周期16ms),通信任务(任务周期128ms),SysTick时间就取4ms。当然在SysTick时间较小时,要注意系统负荷问题,这时最好测一下cpu使用率及各个任务的时间等。周期性任务的开发套路是怎么样的呢?看看定时器任务的做法就知道了,代码在os_tmr.c。首先在OSTmr_Init()中初始化OSTmrSemSignal,然后OSTmr_Task()任务会一直等待OSTmrSemSignal,等到OSTmrSemSignal后去处理各个定时器。那么谁在释放OSTmrSemSignal呢?OSTmrSignal(),这个函数要求放在一定频率的时钟中断里,默认是在SysTick中断中(如果使能OS_TIME_TICK_HOOK_EN)。好了,现在我们可以总结总结周期性任务的一般套路了。首先在任务初始化函数中初始化一个信号量(一般会用信号量),伪代码如下: void task_init(void) { task_sem = OSSemCreate(0); }在任务中等待信号量void task (void *p_arg) { for (;;) { OSSemPend(task_sem, 0, &err);} }周期性的释放信号量OSSemPost(task_sem);对于上面所说系统主任务,OSSemPost(task_sem)可以放在SysTick_Handler()中。所以一般来说OS_CPU_SysTickHandler()改动的可能性是非常大的。非周期任务的开发套路又是怎样的呢?其实和周期性任务是差不多的,只是信号量不是周期性地释放,而是按需释放。其他内核功能就不多介绍了,大家按需使用,不是很难。本文代码:http://download.csdn.net/source/3472653 该移植代码在我自己开发的一个小玩意上已得到一段时间的验证,未发现问题。但由于水平所限,并不敢保证该移植是没有任何问题的,殷切希望大家批评指正。| 欢迎光临 中科因仑“3+1”工程特种兵精英论坛 (http://bbs.enlern.com/) | Powered by Discuz! X3.4 |