P80:μc/osⅡ时钟 硬件定时器中断(使用了STM32中的Systick中断)每产生一次,μc/osⅡ时钟就会进入一次系统中断服务程序(OSTickISR()),系统中断服务程序通过调用OSTimeTick()来完成系统每个时钟节拍所要完成的工作(包括遍历每个任务控制块将其延时参数减1等)。 P87:钩子函数 钩子函数的使用(以OSTimerTickHook为例): 需要到os_cfg.h中把这两个宏#define成>0。 P88:OSTimeDly在OSTimeDly中完成OSTCBCur->OSTCBDly(任务延时寄存器)的写入并进行一次任务切换: ·μc/osⅡ的时钟OSTimeTick()是怎么与STM32的SysTick关联起来的? 战舰开发板配套程序中在main()中有delay_init()(delay.c下)函数,其原代码如下: void delay_init() { #ifdef OS_CRITICAL_METHOD //如果OS_CRITICAL_METHOD定义了,说明使用ucosII了. u32 reload; #endif SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8); //选择外部时钟 HCLK/8 fac_us=SystemCoreClock/8000000; //为系统时钟的1/8 #ifdef OS_CRITICAL_METHOD //如果OS_CRITICAL_METHOD定义了,说明使用ucosII了. reload=SystemCoreClock/8000000; //每秒钟的计数次数 单位为K reload*=1000000/OS_TICKS_PER_SEC;//根据OS_TICKS_PER_SEC设定溢出时间 //reload为24位寄存器,最大值:16777216,在72M下,约合1.86s左右 fac_ms=1000/OS_TICKS_PER_SEC;//代表ucos可以延时的最少单位 SysTick->CTRL|=SysTick_CTRL_TICKINT_Msk; //开启SYSTICK中断 SysTick->LOAD=reload; //每1/OS_TICKS_PER_SEC秒中断一次 SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk; //开启SYSTICK #else fac_ms=(u16)fac_us*1000;//非ucos下,代表每个ms需要的systick时钟数 #endif } 可见delay_init()开启了STM32的SYSTICK中断,下面继续找SYSTICK的中断服务程序(同样也在delay.c),代码如下: void SysTick_Handler(void) { OSIntEnter(); //进入中断,其作仅仅是将判断中断层数是否达到255否则OSIntNesting++ OSTimeTick(); //调用ucos的时钟服务程序 OSIntExit(); //触发任务切换软中断 } 发现OSTimeTick(); 在 SYSTICK的中断服务程序被调用,现在μc/osⅡ的时钟OSTimeTick()就与STM32的SysTick关联了起来。 OSIntExit (void)的作用除了执行了OSIntNesting--之外 还进行了一次中断级任务调度OSIntCtxSw() 。 ·OSIntCtxSw()切换任务的原理: Step1:SIntCtxSw()触发了一次软件中断,代码如下 ;/************************************************************************************** ;* 函数名称: OSIntCtxSw ;* 功能描述: 中断级任务切换(其实是进行了一次软件中断) ;* 参 数: None ;* 返 回 值: None ;***************************************************************************************/ OSIntCtxSw PUSH {R4, R5} LDR R4, =NVIC_INT_CTRL ;触发PendSV异常 (causes context switch) ;NVIC_INT_CTRL就是软件中断控制寄存器 LDR R5, =NVIC_PENDSVSET ;NVIC_PENDSVSET是触发软件中断的值. STR R5, [R4] ;将R5中的字数据写入以R4为地址的存储器中就发生了PendSV中断 POP {R4, R5} BX LR NOP Step2:执行完了step后会进入软件中断服务函数,代码(在os_cpu_aasm中)如下 ;/************************************************************************************** ;* 函数名称: OSPendSV ;* ;* 功能描述: 该函数实际上完成了cpu各寄存器的压栈和新任务堆栈向cpu的进栈; ;* 参 数: None ;* ;* 返 回 值: None ;***************************************************************************************/ PendSV_Handler ;软件中断服务函数 CPSID I ; Prevent interruption during context switch MRS R0, PSP ; PSP is process stack pointer 如果在用PSP堆栈,则可以忽略保存寄存器,参考CM3权威中的双堆栈-白菜注 CBZ R0, PendSV_Handler_Nosave ; Skip register save the first time SUBS R0, R0, #0x20 ; Save remaining regs R4-11 on process stack STM R0, {R4-R11} LDR R1, =OSTCBCur ; OSTCBCur->OSTCBStkPtr = SP; =OSTCBCur就是取的OSTCBCur ;首地址,即任务控制块的堆栈。 LDR R1, [R1] STR R0, [R1] ; R0 is SP of process being switched out ·当临界区遇上单片机中断 其中OSIntExit对任务的调度是在临界区里进行的。那么进入了临界区后恰好单片机发生了中时会不会造成单片机中断的失效呢? ---此时当然是先完成临界区的程序然后退出临界区时单片机会自动执行中断服务函数。因为虽然在临界区系统给单片机关闭了总中断,但是中断还是会被单片机接收的,只是在临界区时中断没被触发。 所以在ucos下使用stm32的中断只要用裸奔的形式配置寄存器,而在进入和退出中断服务函数是只需要将中断层数+1或-1(以下程序以串口1为例)即可: /**********************USART1寄存器配置***********************/ void uart_init(u32 bound) { //GPIO端口设置 GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE); //使 能USART1,GPIOA时钟以及复用功能时钟 //USART1_TX PA.9 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出 GPIO_Init(GPIOA, &GPIO_InitStructure); //USART1_RX PA.10 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入 GPIO_Init(GPIOA, &GPIO_InitStructure); //Usart1 NVIC 配置 NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//抢占优先级3 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //子优先级3 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能 NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC寄存器 //USART 初始化设置 USART_InitStructure.USART_BaudRate = bound;//一般设置为9600; USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式 USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位 USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位 USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制 USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //收发模式 USART_Init(USART1, &USART_InitStructure); //初始化串口 USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启中断 USART_Cmd(USART1, ENABLE); //使能串口 } /***************/**********************断服务函数*******************************************/ ·任务的挂起与恢复 /***********************************任务的挂起与恢复*******************************************/ 挂起:OSTaskSuspend(INT8U prio) 如果是挂起自身则参数为 OS_PRIO_SELF 恢复:OSTaskResume(INT8U prio) 如若挂起和恢复成功则返回:OS_NO_ERROR,不成功则返回值见任哲教材P47。 ·微秒级延时和毫秒级延时的区别 微秒级延时和毫秒级延时都是使用了系统时钟,其中微秒级延时仅仅是用在延时场合,如DS18B20,而毫秒级延时在延时的同时还发生了任务调度。这两个延时都是使用了系统时钟是怎么做到的呢? ——SysTick是一个减计数器,delay_us是用实时查询定时器SysTick->VAL; 通过对比初始值和定时值之间的差来计算是否达到定时时长,delay_us只是单纯的延时,延时期间不会进行任务调度。而delay_ms是使用了SysTick中断,在中断中会执行一次任务调度。 ·OSTaskStkInit() 说之前还是得先说一下任务切换,因为初始化任务堆栈,是为任务切换服务的。代码在正常运行时,一行一行往下执行,怎么才能跑到另一个任务(即函数)执行呢?首先大家可以回想一下中断过程,当中断发生时,原来函数执行的地方(程序计数器PC、处理器状态寄存器及所有通用寄存器,即当前代码的现场)被保存到栈里面去了,然后开始取中断向量,跑到中断函数里面执行。执行完了呢,想回到原来函数执行的地方,该怎么办呢,只要把栈中保存的原来函数执行的信息恢复即可(把栈中保存的代码现场重新赋给cpu的各个寄存器),一切就都回去了,好像什么事都没发生一样。这个过程大家应该都比较熟悉,任务切换和这有什么关系,试想一下,如果有3个函数foo1(), foo2(), foo3()像是刚被中断,现场保存到栈里面去了,而中断返回时做点手脚(调度程序的作用),想回哪个回哪个,是不是就做了函数(任务)切换了。看到这里应该有点明白OSTaskStkInit()的作用了吧,它被任务创建函数调用,所以要在开始时,在栈中作出该任务好像刚被中断一样的假象。(关于任务切换的原理邵老师书中的3.06节有介绍)。
那么中断后栈中是个什么情形呢,中9.1.1有介绍,xPSR,PC,LR,R12,R3-R0被自动保存到栈中的,R11-R4如果需要保存,只能手工保存。因此OSTaskStkInit()的工作就是在任务自己的栈中保存cpu的所有寄存器。这些值里R1-R12都没什么意义,这里用相应的数字代号(如R1用0x01010101)主要是方便调试。
其他几个:
xPSR = 0x01000000L,xPSR T位(第24位)置1,否则第一次执行任务时Fault,
PC肯定得指向任务入口,
R14 = 0xFFFFFFFEL,最低4位为E,是一个非法值,主要目的是不让使用R14,即任务是不能返回的。
R0用于传递任务函数的参数,因此等于p_arg。
OS_STK *OSTaskStkInit (void (*task)(void *p_arg), void *p_arg, OS_STK *ptos, INT16U opt)
{
OS_STK *stk;
(void)opt; /* 'opt' is not used, prevent warning */
stk = ptos; /* Load stack pointer */
/* Registers stacked as if auto-saved on exception */
*(stk) = (INT32U)0x01000000L; /* xPSR */
*(--stk) = (INT32U)task; /* Entry Point */
/* R14 (LR) (init value will cause fault if ever used)*/
*(--stk) = (INT32U)0xFFFFFFFEL;
*(--stk) = (INT32U)0x12121212L; /* R12 */
*(--stk) = (INT32U)0x03030303L; /* R3 */
*(--stk) = (INT32U)0x02020202L; /* R2 */
*(--stk) = (INT32U)0x01010101L; /* R1 */
*(--stk) = (INT32U)p_arg; /* R0 : argument */
/* Remaining registers saved on process stack */
*(--stk) = (INT32U)0x11111111L; /* R11 */
*(--stk) = (INT32U)0x10101010L; /* R10 */
*(--stk) = (INT32U)0x09090909L; /* R9 */
*(--stk) = (INT32U)0x08080808L; /* R8 */
*(--stk) = (INT32U)0x07070707L; /* R7 */
*(--stk) = (INT32U)0x06060606L; /* R6 */
*(--stk) = (INT32U)0x05050505L; /* R5 */
*(--stk) = (INT32U)0x04040404L; /* R4 */
return (stk);
}
把OS_CPU_SysTickHandler(), OS_CPU_SysTickInit()注释掉。
#define OS_CPU_CM3_NVIC_ST_CTRL (*((volatile INT32U *)0xE000E010))
#define OS_CPU_CM3_NVIC_ST_RELOAD (*((volatile INT32U *)0xE000E014))
#define OS_CPU_CM3_NVIC_ST_CURRENT (*((volatile INT32U *)0xE000E018))
#define OS_CPU_CM3_NVIC_ST_CAL (*((volatile INT32U *)0xE000E01C))
#define OS_CPU_CM3_NVIC_ST_CTRL_COUNT 0x00010000
#define OS_CPU_CM3_NVIC_ST_CTRL_CLK_SRC 0x00000004
#define OS_CPU_CM3_NVIC_ST_CTRL_INTEN 0x00000002
#define OS_CPU_CM3_NVIC_ST_CTRL_ENABLE 0x00000001
把上面这些宏定义也注释掉,因为它们都用于OS_CPU_SysTickHandler(), OS_CPU_SysTickInit()。
os_cpu_a.asm
这个文件包含着必须用汇编写的代码。
EXTERN OSRunning ; External references
EXTERN OSPrioCur
EXTERN OSPrioHighRdy
EXTERN OSTCBCur
EXTERN OSTCBHighRdy
EXTERN OSIntNesting
EXTERN OSIntExit
EXTERN OSTaskSwHook
申明这些变量是在其他文件定义的,本文件只做引用(有几个好像并未引用,不过没有关系)。
EXPORT OS_CPU_SR_Save ; Functions declared in this file
EXPORT OS_CPU_SR_Restore
EXPORT OSStartHighRdy
EXPORT OSCtxSw
EXPORT OSIntCtxSw
EXPORT OS_CPU_PendSVHandler
申明这些函数是在本文件中定义的。
NVIC_INT_CTRL EQU 0xE000ED04 ;中断控制及状态寄存器ICSR的地址
NVIC_SYSPRI14 EQU 0xE000ED22 pendSV优先级寄存器的地址
NVIC_PENDSV_PRI EQU 0xFF pendSV中断的优先级为255(最低)
NVIC_PENDSVSET EQU 0x10000000 ;位28为1
定义几个常量,类似C语言中的#define预处理指令。
OS_CPU_SR_Save
MRS R0, PRIMASK ;读取PRIMASK到R0中,R0为返回值
CPSID I PRIMASK=1,关中断(NMI和硬fault可以响应)
BX LR ;返回
OS_CPU_SR_Restore
MSR PRIMASK, R0 ;读取R0到PRIMASK中,R0为参数
BX LR ;返回
OSStartHighRdy()由OSStart()调用,用来启动最高优先级任务,当然任务必须在OSStart()前已被创建。
OSStartHighRdy
;设置PendSV中断的优先级 #1
LDR R0, =NVIC_SYSPRI14 ;R0 = NVIC_SYSPRI14
LDR R1, =NVIC_PENDSV_PRI ;R1 = NVIC_PENDSV_PRI
STRB R1, [R0] ;*(uint8_t *)NVIC_SYSPRI14 = NVIC_PENDSV_PRI
;设置PSP为0 #2
MOVS R0, #0 ;R0 = 0
MSR PSP, R0 PSP = R0
;设置OSRunning为TRUE
LDR R0, =OSRunning ;R0 = OSRunning
MOVS R1, #1 ;R1 = 1
STRB R1, [R0] ;OSRunning = 1
;触发PendSV中断 #3
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
CPSIE I ;开中断
OSStartHang ;死循环,应该不会到这里
B OSStartHang
#1.PendSV中断的优先级应该为最低优先级,原因在的7.6节已有说明。
#2.PSP设置为0,是告诉具体的任务切换程序(OS_CPU_PendSVHandler()),这是第一次任务切换。做过切换后PSP就不会为0了,后面会看到。
#3.往中断控制及状态寄存器ICSR(0xE000ED04)第28位写1即可产生PendSV中断。这个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;
#endif
OS_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(); /* 触发PendSV中断 */
}
}
}
/* 一旦开中断,PendSV中断函数会执行(当然要等更高优先级中断处理完) */
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) #2
OS_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 #3
LDM R0, {R4-R11} ;从任务堆栈SP恢复R4-R11
ADDS R0, R0, #0x20 ;R0 += 0x20
MSR PSP, R0 ;PSP = R0,用新任务的SP加载PSP
ORR LR, LR, #0x04 ;确保LR位2为1,返回后使用进程堆栈 #4
CPSIE 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
用于系统调试,可以不管。
·关于任务切切换时的知识补充 上文可知,STM32任务调度是在软件中断服务函数中执行,首先要将CPU的内容保存到任务堆栈中,汇编代码如下:  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) #2 那么CPU的内容是被保存到那个堆栈中呢? ——当然是保存在将要切换出CPU的任务的堆栈中。 那么CPU是怎么做到的呢? ——因为CPU的R13中保存了当前正在运任务的堆栈指针。所以当进入中断时CPU就会按照R13指向的地址将CPU的状态保存到堆栈中。这就完成了旧任务的切出。
如右图9-1,一个任务堆栈的结构如右图所示。当一个任务放弃CPU的使用权时就将CPU的内容保存在该任务对应的任务堆栈中。
|