在第一章我们讲解了 UCOSII 在 STM32H743 开发板上的移植过程,第二章讲解了一下Cortex-M3/M4/CM7 处理器的一些基础知识,本章我们就结合前两章内容讲解一下我们在UCOSII 移植过程中的一些重要文件和我们移植 UCOSII 的过程中都做了那些工作。
滴答定时器是一个 24 位的倒计数定时器,当计到 0 时,将从 RELOAD 寄存器中自动重装载定时器初值,只要不把它在 SysTick 控制以及状态寄存器中的使能位清零,就将永久不息。 SysTick 的最大使命,就是定期地产生异常请求作为系统的时基。OS 都需要这种“滴答”来推动任务和时间的管理。我们在移植 UCOSII 的过程中就要使用滴答定时器来作为系统时钟,首先就是对滴答定时器的设置,主要是设置它的定时周期,我们是在 delay_init()函数中完成滴答定时器设置的,delay_init()函数代码如下。
/初始化延迟函数 //当使用 ucos 的时候,此函数会初始化 ucos 的时钟节拍 //SYSTICK 的时钟固定为 AHB 时钟的 1/8 //SYSCLK:系统时钟频率 void delay_init(u8 SYSCLK) { #if SYSTEM_SUPPORT_OS //如果需要支持 OS. u32 reload; #endif HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);//SysTick 频 率 为 HCLK fac_us=SYSCLK; //不论是否使用 OS,fac_us 都需要使用 #if SYSTEM_SUPPORT_OS //如果需要支持 OS. reload=SYSCLK; //每秒钟的计数次数 单位为 K reload*=1000000/delay_ostickspersec; //根据 delay_ostickspersec 设定溢出时间 //reload 为 24 位寄存器,最大值:16777216,在 //216M 下,约合 77.7ms 左右 fac_ms=1000/delay_ostickspersec; //代表 OS 可以延时的最少单位 SysTick->CTRL|=SysTick_CTRL_TICKINT_Msk;//开启 SYSTICK 中断 SysTick->LOAD=reload; //每 1/OS_TICKS_PER_SEC 秒中断一次 SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk; //开启 SYSTICK #endif }其 中 红 色 代 码 部 分 就 是 在 使 用 UCOSII 时 配 置 SysTick 的 代 码 , 如 果SYSTEM_SUPPORT_OS 被定义了就说明使用了 UCOS,那么我们就需要配置 SysTick。首先要 根据 UCOSII 中的定义的 OS_TICKS_PER_SEC 来计算出 SysTick 的装载值 reload,开启 SysTick中断,将 reload 值写进 SysTick 的 LOAD 寄存器中,最后开启 SysTick。开启 SysTick 后还要编写 SysTick 的中断服务函数 SysTick_Handler(),函数代码如下,同样也采用了条件编译。
//systick 中断服务函数,使用 OS 时用到 void SysTick_Handler(void) { HAL_IncTick(); //HAL 库使用此函数还获取时间!一定要调用 if(delay_osrunning==1) //OS 开始跑了,才执行正常的调度处理 { OSIntEnter(); //进入中断 OSTimeTick(); //调用 ucos 的时钟服务程序 OSIntExit(); //触发任务切换软中断 } }注意!!!!滴答定时器中断服务函数SysTick_Handler()中一定要调用 HAL_IncTick(),因为HAL 库使用此函数来获得系统时间(非 UCOS 系统时间)供 HAL 库中延时函数使用!!!如果没有调用的话 HAL 库会工作不正常。
上面代码分为两部分,上半部分使用 IMPORT 来定义,下半部分使用 EXPORT 来定义。IMPORT 定义表示这是一个外部变量的标号,不是在本程序定义的;EXPORT 定义表示这些函数是在本文件中定义的,供其它文件调用。
NVIC_INT_CTRL EQU 0xE000ED04 ; 中断控制寄存器 NVIC_SYSPRI14 EQU 0xE000ED22 ; 系统优先级寄存器(14) NVIC_PENDSV_PRI EQU 0xFFFF ; PendSV 中断优先级和 ;Systick 中断优先级都为最低(0xFF) NVIC_PENDSVSET EQU 0x10000000 ; 触发软件中断的值。EQU 和 C 语言中的#define 一样,定义一个宏。NVIC_INT_CTRL 为中断控制寄存器,地址为 0xE000ED04;NVIC_SYSPRI14 为 PendSV 中断优先级寄存器,地址为 0xE000ED22;NVIC_PENDSV_PRI 为 PendSV 和 Systick 的中断优先级,这里为 0xFFFF,都为最低优先级; NVIC_PENDSVSET 可以触发软件中断,通过给中断控制寄存器(NVIC_INT_CTRL)的 bit28 写1 来触发软件中断,因此 NVIC_PENDSVSET 为0x10000000。
IF {FPU} != "SoftVFP" OS_CPU_FP_Reg_Push MRS R1, PSP ; PSP is process stack pointer CBZ R1, OS_CPU_FP_nosave ; Skip FP register save the first time VMRS R1, FPSCR STR R1, [R0, #-4]! VSTMDB R0!, {S0-S31} LDR R1, =OSTCBCur LDR R2, [R1] STR R0, [R2] OS_CPU_FP_nosave BX LR ENDIF IF {FPU} != "SoftVFP" OS_CPU_FP_Reg_Pop VLDMIA R0!, {S0-S31} LDMIA R0!, {R1} VMSR FPSCR, R1 LDR R1, =OSTCBHighRdy LDR R2, [R1] STR R0, [R2] BX LR ENDIFOS_CPU_FP_Reg_Push 和OS_CPU_FP_Reg_Pop 是对 FPU 寄存器进行入栈和出栈操作的,对于带有 FPU 的 MCU 来说非常重要!
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 ;返回OS_CPU_SR_Save 和 OS_CPU_SR_Restore 是开关中断的汇编代码,通过给 PRIMASK 写 1来关中断,写 0 来打开中断。这里也可是使用 CPS 指令来快速的开关中断,我们在OS_CPU_SR_Save 中就使用了 CPSID I 来关中断。
OSStartHighRdy LDR R0, =NVIC_SYSPRI14 LDR R1, =NVIC_PENDSV_PRI STRB R1, [R0] MOVS R0, #0 MSR PSP, R0 LDR R0, =OS_CPU_ExceptStkBase LDR R1, [R0] MSR MSP, R1 LDR R0, =OSRunning MOVS R1, #1 STRB R1, [R0] LDR R0, =NVIC_INT_CTRL LDR R1, =NVIC_PENDSVSET STR R1, [R0] CPSIE I OSStartHang B OSStartHang ;死循环,应该不会到这里的OSStartHighRdy 是由 OSStart()调用,用来开启多任务的,如果多任务开启失败的话就会进入 OSStartHang。
OSCtxSw LDR R0, =NVIC_INT_CTRL ; 触发 PendSV 中断 LDR R1, =NVIC_PENDSVSET STR R1, [R0] BX LR OSIntCtxSw LDR R0, =NVIC_INT_CTRL ; 触发 PendSV 中断 LDR R1, =NVIC_PENDSVSET STR R1, [R0] BX LROSCtxSw 和 OSIntCtxSw 这两个是用来做任务切换的,这两个看起来都是一样的,其实它们都只是触发一个 PendSV 中断,具体的切换过程在 PendSV 中断服务函数里面进行。这两个函数看起来是一样的,但是他们的意义是不同的,OSCtxSw 是任务级切换,比如从任务 A 切换到任务,OSIntCtxSw 是中断级切换,是从中断退出时切换到一个任务中,从中断切换到任务时,CPU 的寄存器入栈工作已经完成,无需做第二次。
PendSV_Handler CPSID I ;关中断,任务切换期间要关中断 MRS R0, PSP ;R0=PSP CBZ R0, PendSV_Handler_Nosave ; 如果 PSP 为 0 就转移 (1) ;到 PendSV_Handler_Nosave SUBS R0, R0, #0x20 ;R0-=0x20 STM R0, {R4-R11} ; 保存剩余的 R4-R11 寄存器 LDR R1, =OSTCBCur ; R1=&OSTCBCur LDR R1, [R1] ;R1=*R1 既 R1=OSTCBCur STR R0, [R1] ; *R1=R0 既 OSTCBCur=SP PendSV_Handler_Nosave PUSH {R14} ; 保存 R14 的值,后面要调用函数 LDR R0, =OSTaskSwHook ; R0=&OSTaskSwHook BLX R0 ;调用 OSTaskSwHook() POP {R14} ;恢复 R14 LDR R0, =OSPrioCur ;R0=&OSPrioCur LDR R1, =OSPrioHighRdy ;R1=&OSPrioHighRdy LDRB R2, [R1] ;R2=*R1 既 R2=OSPrioHighRdy STRB R2, [R0] ;*R0=R2 既 OSPrioCur=OSPrioHighRdy 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, (2) ;R0 是新任务的 SP LDM R0, {R4-R11} ;从堆栈中恢复 R4-R11 ADDS R0, R0, #0x20 ;R0+=20 MSR PSP, R0 ; PSP=R0,用新任务的 SP 加载 PSP ORR LR, LR, #0x04 ;确保 LR 的位 2 为 1,返回后使用进程堆栈(3) CPSIE I ;开中断 BX LR ;中断返回 NOP END上面的汇编代码才是真正的任务切换程序,在每行代码后都有详细的注释,为了更好的理解我们对代码中打标号的地方重点讲解一下。 (1)如果 PSP 为 0 的话说明是第一次做任务切换,而任务创建的时候会调用堆栈初始化函数 OSTaskStkInit()来初始化堆栈,在初始化的过程中已经做了入栈处理,所以这里就不需要在做入栈处理,直接跳转到 PendSV_Handler_Nosave。 (2)此时 SP 指向的就是要运行的最高优先级的任务。 (3)因为进入中断使用的是 MSP,而退出中断的时候使用的是 PSP,因此这里需要将 LR 的位 2 置 1。在第一章 UCOSII 移植的时候说过要屏蔽掉 stm32h7xx_it.c 文件中的 PendSV_Handler()中断服务函数,原因就是我们在 os_cpu_a.asm 中重新定义了 PendSV_Handler()函数,我们有时候在移植的时候可能会发现有 OS_CPU_PendSVHandler 这样的函数,其实这是官方移植使用到的,具体作用和 PendSV_Handler 一样都是 PendSV 的中断服务函数,不过 ST 官方启动文件startup_stm32h743xx.s 中定义的 PendSV 中断服务函数为 PendSV_Handler,所以说如果要使用 OS_CPU_PendSVHandler 作 为 PendSV 的 中 断 服 务 函 数 就 需 要 修 改 启 动 文 件startup_stm32h743xx.s 中的中断向量表。