FreeRTOS 系统移植
目录
移植步骤
- RTOS核心代码 , 比如task.c , queue.c 和list.c
- 一些与处理器架构相关的代码, FreeRTOS源码已经自带实现了很多不同的处理器和平台,它们位于FreeRTOS/Source/Portable/[相应编译器]/[相应处理器架构]文件夹下 ,即port.c 和 portmacro.h
- 堆栈分配, 在FreeRTOS/Source/portable/MemMang文件夹下具有各种类型的堆栈分配方案 , 即heap_x.c
- 编写FreeRTOSConfig.h文件, 在这其中做一些配置 ,比如时钟节拍(时钟频率是多少), 任务调度策略, 任务优先级划分成多少个, 具体可以参见FreeRTOS内核配置说明
- 编写一些钩子函数, 如果你在FreeRTOSConfig.h中设置了configUSE_TICK_HOOK=1,则必须编写voidvApplicationTickHook( void )函数。如果你在FreeRTOSConfig.h中设置了configCHECK_FOR_STACK_OVERFLOW=1或=2,则必须编写voidvApplicationStackOverflowHook( xTaskHandle pxTask, signed char *pcTaskName )函数
Cortex-M3移植
- Cortex M3是ARM针对微控制器设计的一种CPU Core , STM32某个型号就是Cortex M3的
- 查看port.c 和 portmacro.h里涉及到哪些文件, 就知道该怎么移植了 (路径为FreeRTOS/Source/portable/RVDS/ARM_CM3), 参考移植步骤, 这里主要讲下port.c和portmacro.h的细节
移植细节
pxPortInitialiseStack
这里面设置了当前任务的PC和LR等的地址 ,成为类似如下图所示的排列 。一个任务肯定需要存储PC等程序地址以及SP栈地址, 才能再调度切换后知道从哪里开始,或者从其它任务切换回自己的时候,知道从上次暂停的地方开始 . 这里跟TCB还是不同的, TCB是Freertos内核创建的, 本函数调用前就创建好了.StackType_t * pxPortInitialiseStack( StackType_t * pxTopOfStack, TaskFunction_t pxCode, void * pvParameters ) { /* Simulate the stack frame as it would be created by a context switch * interrupt. */ pxTopOfStack--; /* Offset added to account for the way the MCU uses the stack on entry/exit of interrupts. */ *pxTopOfStack = portINITIAL_XPSR; /* xPSR */ pxTopOfStack--; *pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; /* PC (Program Counter)为程序计数器, ,PC是当前取指的代码行,如果指针在动态库,则无法定位源文件及行数, 这里指的是自身的执行地址 */ pxTopOfStack--; *pxTopOfStack = ( StackType_t ) prvTaskExitError; /* LR , 函数的调用者地址 */ pxTopOfStack -= 5; /* R12, R3, R2 and R1. */ *pxTopOfStack = ( StackType_t ) pvParameters; /* R0 */ pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4. */ return pxTopOfStack; }
这里XPSR的意思如下, 就是存储状态的寄存器组
- xPSR(程序状态字寄存器组),32位,可分为三个寄存器分别进行访问,也可以PSR或xPSR的名字直接组合访问。
- 应用程序PSR(APSR)
- 中断号PSR(IPSR)
- 执行PSR (EPSR)
R0-R3
用作传入函数参数,传出函数返回值。在子程序调用之间,可以将 r0-r3 用于任何用途
R4-R11
被用来存放函数的局部变量
- prvStartFirstTask
FreeRTOS 启动第一个任务 prvStartFirstTask vPortSVCHandler_jltsun的博客-CSDN博客//ldr r0,r1 //表示把r1寄存器中的值放入r0 //ldr r0,[r1] // [r1]表示r1中值对应内存的地址,所以是把r1中的数当作一个地址,把这个地址中的值放入r0 //ldr r0, =0x12345678 这样,就把0x12345678这个地址写到r0中了 __asm void prvStartFirstTask( void ) { PRESERVE8 // 要求代码段按8字节对齐 /* Use the NVIC offset register to locate the stack. */ ldr r0, =0xE000ED08 // 0xE000ED08 地址处为VTOR(向量表偏移量)寄存器,存储向量表起始地址 ldr r0, [r0] // 启动文件中, 最初地址放置的__initial_sp ldr r0, [r0] // 根据向量表实际存储地址,取出向量表中的第一项,向量表第一项存储主堆栈指针 MSP 的初始值 /* Set the msp back to the start of the stack. */ msr msp, r0 // 将 __initial_sp的初始值写入 MSP 中 /* Globally enable interrupts. 接下来四个字令就是中断相关的*/ cpsie i cpsie f dsb isb /* Call SVC to start the first task. */ svc 0 // 调用SVC中断, 就是调用vPortSVCHandler nop nop } /* Cortex-M3 处理器,上电默认进入线程的特权模式,使用 MSP 作为堆栈指针,从上电跑到这里,经过一系列的函数调用,出栈,入栈,MSP 自然已经不是最开始的初始化的位置,这里通过 MSR 重新初始化 MSP,岂不是堆栈都没了么?是的,因为这是一条不归路,代码跑到这里,首先不会返回,之前压栈的内容再也不会用到,所以破坏之前的堆栈也没关系 */
- vPortSVCHandler 参考这个地址来理解 https://www.cnblogs.com/god-of-death/p/14855444.html
// 相当于把当前函数的地址以及 rx 放到PC里去执行。 __asm void vPortSVCHandler( void ) { PRESERVE8 /* Get the location of the current TCB. */ ldr r3, =pxCurrentTCB //pxCurrentTCB 指向的是最高优先级的 Ready 状态的任务指针 ldr r1, [r3] // 根据 tskTCB结构体定义, 首地址存的是 pxTopOfStack ldr r0, [r1] // r0得到TopOfStack的值 /* Pop the core registers. */ ldmia r0!, {r4-r11} /* ldmia用于将寄存器弹出栈, 将TopOfStack对应的地址中的内容放到将r4-r11和r14寄存器中 这里为什么要把TopOfStack对应的地址中对应的r4-r11放到r4-r11寄存器里呢 ? 因为r4-r11是给 程序使用的,可以任意使用,为此任务启用时,就应该把这些值放到r4-r11寄存器中, 当任务挂起时,则 需要把r4-r11保存起来,放到TopOfStack对应的地址中, 下次切换到当前任务,就又会弹出到r4-r11 寄存器中*/ msr psp, r0 /* 将此刻的 R0 赋值给 PSP, 因为出栈的时候,处理器会按照入栈的顺序去取 R4-R11、R14 , 而这些寄存器在我们创建任务的时候已经手动压栈*/ isb mov r0, #0 // r0赋值为0 msr basepri, r0 // 将 BASEPRI 寄存器赋值为 0,也就是允许任何中断 orr r14, r14, #13 /* 因为当前在 ISR 中还是使用的 MSP,启动任务后,我们期望在任务执行过程中,处于线程模式, 并使用 PSP(前面几行已经给 PSP 赋值了),所以我们需要将 LR 设计成为 0xFFFF_FFFD, 让处理器知道返回的时候呢,使用线程模式+PSP堆栈;*/ bx r14 /* BX 指令跳转到指令中所指定的目标地址, 跳转到r14. r14就是lr, lr (总是)包含着退出时要装载到 pc 中的值。 最后执行 bx R14,告诉处理器 ISR 完成,需要返回,此刻处理器便会使用 PSP 做为堆栈指针,进行出栈操作, 将xPSR、PC、LR、R12、R3~R0 出栈,初始化的时候,PC 被我们赋值成为了执行任务的函数的入口, 所以呢,就正常跳入到了优先级最高的 Ready 状态的第一个任务的入口函数了;*/ // https://blog.csdn.net/qwe5959798/article/details/122717894 }
- SVC用于产生系统函数的调用请求。例如,操作系统通常不允许用户程序直接访问硬件,而是通过提供一些系统服务函数,让用户程序使用SVC发出对系统服务函数的调用请求,以这种方法调用它们来间接访问硬件。因此,当用户程序想要控制特定的硬件时,它就要产生一个SVC异常,然后操作系统提供的SVC异常服务程序得到执行,它再调用相关的操作系统函数,后者完成用户程序请求的服务。
- 结合
prvStartFirstTask
和pxPortInitialiseStack
来看 , 就是将pxPortInitialiseStack
里申请的那段空间里存放的R4-R11放到寄存器R4-R11中, 然后修改堆栈指针psp的值,使得取指PC指向最高优先级的 Ready 状态的任务指针(这其中还包含了xPSR,LR,R12,R3-R0, 退出函数时,栈的值会减少8(处理器自动操作), 并载入这8个值作为后续执行的参数,这其中就包括PC) 。 要完全理解可以参考这篇文章 https://blog.csdn.net/u012351051/article/details/125125796
- xPortPendSVHandler 参考这个地址 https://blog.csdn.net/tao475824827/article/details/105622087
- PendSV是为系统级服务提供的中断驱动。在一个操作系统环境中,当没有其他异常正在执行时,可以使用PendSV来进行上下文的切换。
- 使用PendSV时,一般会将PendSV的优先级设置为最低,让外部IRQ先执行,等到没有外部中断后,在执行上下文切换
__asm void xPortPendSVHandler( void ) { extern uxCriticalNesting; extern pxCurrentTCB; extern vTaskSwitchContext; /* *INDENT-OFF* */ PRESERVE8 mrs r0, psp isb ldr r3, =pxCurrentTCB /* Get the location of the current TCB. */ ldr r2, [ r3 ] stmdb r0 !, { r4 - r11 } /* Save the remaining registers. 把r4-r11寄存器内容压入r0开始的地址。 保存寄存器到值, STMDB r0!, {r4-r11} r0的存储地址由高到低递减,将r4-r11里的内容存储到r0任务栈里面。*/ str r0, [ r2 ] /* Save the new top of stack into the first member of the TCB. */ stmdb sp !, { r3, r14 } /* SP是MSP和PSP的代言人,即SP是MSP和PSP的逻辑地址,SP始终指向各种模式、各种场景下使用的堆栈指针, 只不过在OS或Handler模式下,SP先指向MSP,或者说SP拷贝了MSP的值,可以直接访问主堆栈。而在线程模式下, SP拷贝了PSP的值,可以直接访问线程(任务)堆栈对于裸机程序,我们只需要知道SP即可,而对于OS系统, 尤其涉及中断、任务上下文切换时,就需要知道PSP和MSP了,OS底层也会直接针对PSP进行编程。这里后续分析FreeRTOS调度实现原理时再详细解释。*/ mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY msr basepri, r0 dsb isb bl vTaskSwitchContext。/* 调用vTaskSwitchContext, 这个函数在task.c里, 这里bl的作用跳转,但是跳转之前,会在寄存器R14 中保存PC 的当前内容。 其实就相当于调用vTaskSwitchContext后继续执行接下来的命令, 就相当于C语言里的一次函数调用 */ mov r0, #0 msr basepri, r0 ldmia sp !, { r3, r14 } ldr r1, [ r3 ] ldr r0, [ r1 ] /* The first item in pxCurrentTCB is the task top of stack. */ ldmia r0 !, { r4 - r11 } /* Pop the registers and the critical nesting count. 将r0存储地址里面的内容手动加载到 CPU 寄存器 r4-r11里 */ msr psp, r0 // r0 给 psp isb bx r14 nop /* *INDENT-ON* */ }
- xPortSysTickHandler , 其实在这里面是需要考虑是否需要切换任务的,这里通过PendSV中断处理去进行这个操作
- SysTick是一个中断, SysTick的优先级设置为最低, 因为SysTick的优先级为最低,并且将任务调度放到了PendSV,可以最大限度的减少SysTick中断服务程序消耗时间,IRQ不会被中断,并且能够最快的时间去响应。
- SysTick的优先级低,影响RTOS时钟精度怎么办? 不怎么办, RTOS的时钟节拍不需要很精准,只要有周期的心跳,就能保证操作系统周期的运行、周期的调度。 OTS的软件定时器只能用于【短时间】、对时间要求不严苛的应用场景
void xPortSysTickHandler( void ) { /* The SysTick runs at the lowest interrupt priority, so when this interrupt * executes all interrupts must be unmasked. There is therefore no need to * save and then restore the interrupt mask value as its value is already * known - therefore the slightly faster vPortRaiseBASEPRI() function is used * in place of portSET_INTERRUPT_MASK_FROM_ISR(). */ vPortRaiseBASEPRI(); { /* Increment the RTOS tick. , 增加tick计数*/ if( xTaskIncrementTick() != pdFALSE ) { /* A context switch is required. Context switching is performed in * the PendSV interrupt. Pend the PendSV interrupt. */ portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; } } vPortClearBASEPRIFromISR(); }
- xPortStartScheduler ,就是使能PendSV中断, 使能时钟节拍中断(就是xPortSysTickHandler), 开启第一个任务
{ /* Make PendSV and SysTick the lowest priority interrupts. */ portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI; portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI; /* Start the timer that generates the tick ISR. Interrupts are disabled * here already. */ vPortSetupTimerInterrupt(); /* Initialise the critical nesting count ready for the first task. */ uxCriticalNesting = 0; /* Start the first task. */ prvStartFirstTask(); /* Should not get here! */ return 0; }
- vPortEndScheduler
// 不需要实现, 直接放置一个断言 void vPortEndScheduler( void ) { /* Not implemented in ports where there is nothing to return to. * Artificially force an assert. */ configASSERT( uxCriticalNesting == 1000UL ); }
- vPortEnterCritical
// 本质上就是不使能中断(操作下寄存器), 因为只有通过中断才能切换任务 , 记住这点就可以了, 下面的注释无需细看。 void vPortEnterCritical( void ) { portDISABLE_INTERRUPTS(); uxCriticalNesting++; /* This is not the interrupt safe version of the enter critical function so * assert() if it is being called from an interrupt context. Only API * functions that end in "FromISR" can be used in an interrupt. Only assert if * the critical nesting count is 1 to protect against recursive calls if the * assert function also uses a critical section. */ if( uxCriticalNesting == 1 ) { configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 ); } }
- vPortExitCritical
// 本质就是使能中断 void vPortExitCritical( void ) { configASSERT( uxCriticalNesting ); uxCriticalNesting--; if( uxCriticalNesting == 0 ) { portENABLE_INTERRUPTS(); } }
- portYIELD // 触发一个pendSV中断
// portmacro.h中 #define portYIELD() { \ /* Set a PendSV to request a context switch. */ \ portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \ \ /* Barriers are normally not required but do ensure the code is completely \ * within the specified behaviour for the architecture. */ \ __dsb( portSY_FULL_READ_WRITE ); \ __isb( portSY_FULL_READ_WRITE ); \ }
- vPortGetIPSR 获取当前的中断号 , 为啥这里没有返回值也行 ? 返回值是放在r0里的,实际上,在汇编语言中,函数调用的参数和返回值均可以通过寄存器来传送,只要函数内外相互配合,就可以精确地进行参数和返回值传递。 汇编认为返回的值就在r0里
__asm uint32_t vPortGetIPSR( void ) { /* *INDENT-OFF* */ PRESERVE8 //代码段按8字节对齐 mrs r0, ipsr bx r14 /* *INDENT-ON* */ }
- 中断相关
#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI() #define portENABLE_INTERRUPTS() vPortSetBASEPRI( 0 )
移植总结
- 主要涉及到的其实就是中断, 任务切换通过出发PendSV中断,Enter和Exit Critial通过禁止和恢复中断实现。 另外为了开启或切换任务 , 为了切换到当前最高优先级的任务, 需要将PC等切换到当前任务。 为此根据中断的特性, 进入中断时CPU自动保存现场寄存器,包括R0~R3, R12, R14(LR), 返回地址(PC), xPSR ,将这8个寄存器值存放到堆栈。 为了切换到选定的任务, 则将psp指针修改为选定任务的r0位置(创建任务时会initialStack时有r0) ,后续中断退出时, CPU会恢复栈,这时PC就被修改为选定任务的PC了,也就跳转到选定任务执行 。 另外还有一个r4-r11寄存器, r4-r11用于人为用到的寄存器, 我不知道会被用到什么地方, 但我这就是需要假定被用到了. 所以在切换一个任务前, 我会把当前任务的这些值保存好, 等到切换任务后时候 ,我会手动将切换后到任务的这些值出栈到r4-r11
- 另外涉及三个中断, SVC中断(只在第一次启动任务调度的时候用), PendSV中断(用于任务切换), SysTick(系统滴答定时器的中断, 这个中断函数主要就是做了xTaskIncrementTick, 如果该函数返回值为真(不等于pdFALSE),说明处于就绪态任务的优先级比当前运行的任务优先级高, 就出发PendSV中断)
- 另外宏根据需要设置, 时钟设置成硬件时钟的频率即可
参考资料
FreeRTOS 启动第一个任务 prvStartFirstTask vPortSVCHandler_jltsun的博客-CSDN博客__ asm void prvStartFirstTask( void )_ _asm void prvStartFirstTask( void ){ PRESERVE8 /* Use the NVIC offset register to locate the stack.* / ldr r0, =0xE000ED08 //0xE000ED08 地址处为VTOR(向量表偏移量)寄存器,存储向量表起始 https://blog.csdn.net/jltsun/article/details/118383820
Linux Posix环境下的移植
- 参考网址FreeRTOS simulator for Posix/Linux实现这个功能
- 查看port.c 和 portmacro.h里涉及到哪些文件就知道如何移植, 文件路径为FreeRTOS/Source/portable/ThirdParty/GCC/Posix/port.c, 下面详述细节
移植细节
- pxPortInitialiseStack xTaskCreate会调用这个函数
/* 一些背景知识 pthread_sigmask要说明下 int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset); How: 三个参数都是设置屏蔽信号量的, 只是设置方法不同而已 SIG_BLOCK: 结果集是当前集合参数集的并集 , set和oldset的并集, 如果oldset为空,则就是set SIG_UNBLOCK: 结果集是当前集合参数集的差集, set和oldset的差集, 如果oldset为空,则就是set SIG_SETMASK: 结果集是由参数集指向的集, 即就是set, 另外会复制一份set到oldset里 sigaction信号处理, 有多个线程都不屏蔽的情况下, 是选定一个线程来处理的. 具体怎么选可以暂时不管 正确的处理方法, 主线程和工作线程都屏蔽信号, 用一个专门的线程来处理信号. 这里使用了这种方法 , 区别就是只有唤醒的任务线程才会处理, 其它的都屏蔽了. 沉睡唤醒是通过条件变量来的, 和信号相比是两个独立的机制 */ static void prvSetupSignalsAndSchedulerPolicy( void ) { struct sigaction sigresume, sigtick; int iRet; hMainThread = pthread_self(); /* Initialise common signal masks. */ sigemptyset( &xResumeSignals ); sigaddset( &xResumeSignals, SIG_RESUME ); sigfillset( &xAllSignals ); /* Don't block SIGINT so this can be used to break into GDB while * in a critical section. */ sigdelset( &xAllSignals, SIGINT ); /* * Block all signals in this thread so all new threads * inherits this mask. * * When a thread is resumed for the first time, all signals * will be unblocked. */ /* 会设置信号量, 屏蔽所有信号。 后续所有pthread_create创建的线程都会继承这个设置, 即创建后会屏蔽所有信号量。有一个信号SIG_INT, 不屏蔽这个信号, 这个只是为了gdb调试, 这里不细究,了解即可 */ (void)pthread_sigmask( SIG_SETMASK, &xAllSignals, &xSchedulerOriginalSignalMask ); /* SIG_RESUME is only used with sigwait() so doesn't need a handler. 这个只在一个特殊的地方vPortEndScheduler里用到,并且不需要处理函数。 */ sigresume.sa_flags = 0; sigresume.sa_handler = SIG_IGN; sigfillset( &sigresume.sa_mask ); sigtick.sa_flags = 0; sigtick.sa_handler = vPortSystemTickHandler; sigfillset( &sigtick.sa_mask ); iRet = sigaction( SIG_RESUME, &sigresume, NULL ); if ( iRet ) { prvFatalError( "sigaction", errno ); } iRet = sigaction( SIGALRM, &sigtick, NULL ); if ( iRet ) { prvFatalError( "sigaction", errno ); } } portSTACK_TYPE *pxPortInitialiseStack( portSTACK_TYPE *pxTopOfStack, portSTACK_TYPE *pxEndOfStack, pdTASK_CODE pxCode, void *pvParameters ) { Thread_t *thread; pthread_attr_t xThreadAttributes; size_t ulStackSize; int iRet; /* 只调用一次prvSetupSignalsAndSchedulerPolicy, 会设置信号量, 屏蔽所有信号。 后续所有 pthread_create创建的线程都会继承这个设置, 即创建后会屏蔽所有信号量。 同时会 设置收到SIGALRM后调用vPortSystemTickHandler处理(接触屏蔽情况下, sigaction用于定义所有子线程处理方法), 设置收到SIG_RESUME(其实就是SIGUSR1, 用户自定义信号处理)没有处理函数。只接收不需要处理 另外还有一个信号SIG_INT, 不屏蔽这个信号, 这个只是为了gdb调试, 这里不细究,了解即可 后续线程第一次被开启后, 会使能所有信号, 但只会处理SIG_ALRM, 只有这个信号定义了处理函数。其它都 为空或者没定义。 */ (void)pthread_once( &hSigSetupThread, prvSetupSignalsAndSchedulerPolicy ); /* * Store the additional thread data at the start of the stack. */ thread = (Thread_t *)(pxTopOfStack + 1) - 1; pxTopOfStack = (portSTACK_TYPE *)thread - 1; ulStackSize = (pxTopOfStack + 1 - pxEndOfStack) * sizeof(*pxTopOfStack); thread->pxCode = pxCode; //xTaskCreate创建的任务函数 thread->pvParams = pvParameters; thread->xDying = pdFALSE; pthread_attr_init( &xThreadAttributes ); pthread_attr_setstack( &xThreadAttributes, pxEndOfStack, ulStackSize ); thread->ev = event_create(); vPortEnterCritical(); // prvWaitForStart 一开始会阻塞,需要signal唤醒后才执行, iRet = pthread_create( &thread->pthread, &xThreadAttributes, prvWaitForStart, thread ); if ( iRet ) { prvFatalError( "pthread_create", iRet ); } vPortExitCritical(); return pxTopOfStack; } static void *prvWaitForStart( void * pvParams ) { Thread_t *pxThread = pvParams; prvSuspendSelf(pxThread); // cond_wait, 等价于任务线程沉睡 /* Resumed for the first time, unblocks all signals. */ uxCriticalNesting = 0; vPortEnableInterrupts(); /* 当前线程使能所有信号, 只有当前线程会处理SIG_ALRM ,其它都不能(因为屏蔽了) pthread_sigmask( SIG_UNBLOCK, &xAllSignals, NULL );*/ /* Call the task's entry point. */ pxThread->pxCode( pxThread->pvParams ); // 这个其实就是执行任务函数 /* A function that implements a task must not exit or attempt to return to * its caller as there is nothing to return to. If a task wants to exit it * should instead call vTaskDelete( NULL ). Artificially force an assert() * to be triggered if configASSERT() is defined, so application writers can * catch the error. */ configASSERT( pdFALSE ); return NULL; }
- xPortStartScheduler 见思维导图
portBASE_TYPE xPortStartScheduler( void ) { int iSignal; sigset_t xSignals; hMainThread = pthread_self(); /* Start the timer that generates the tick ISR(SIGALRM). Interrupts are disabled here already. */ prvSetupTimerInterrupt(); // 使能时间片信号, 时间片到了后会发送SIGALRM信号 /* Start the first task. */ vPortStartFirstTask(); // 发送cond_signal给最高优先级的线程, 唤醒它 /* Wait until signaled by vPortEndScheduler(). */ sigemptyset( &xSignals ); sigaddset( &xSignals, SIG_RESUME ); while ( !xSchedulerEnd ) { /*阻塞 , 直到vPortEndScheduler设置标志位xSchedulerEnd并发送信号SIG_RESUME 让主线程退出. 暂时可以不管, 非核心功能*/ sigwait( &xSignals, &iSignal ); } /* Cancel the Idle task and free its resources */ #if ( INCLUDE_xTaskGetIdleTaskHandle == 1 ) vPortCancelThread( xTaskGetIdleTaskHandle() ); #endif #if ( configUSE_TIMERS == 1 ) /* Cancel the Timer task and free its resources */ vPortCancelThread( xTimerGetTimerDaemonTaskHandle() ); #endif /* configUSE_TIMERS */ /* Restore original signal mask. */ (void)pthread_sigmask( SIG_SETMASK, &xSchedulerOriginalSignalMask, NULL ); return 0; }
- vPortEndScheduler
void vPortEndScheduler( void ) { struct itimerval itimer; struct sigaction sigtick; Thread_t *xCurrentThread; /* Stop the timer and ignore any pending SIGALRMs that would end * up running on the main thread when it is resumed. */ itimer.it_value.tv_sec = 0; itimer.it_value.tv_usec = 0; itimer.it_interval.tv_sec = 0; itimer.it_interval.tv_usec = 0; (void)setitimer( ITIMER_REAL, &itimer, NULL ); sigtick.sa_flags = 0; sigtick.sa_handler = SIG_IGN; sigemptyset( &sigtick.sa_mask ); sigaction( SIGALRM, &sigtick, NULL ); /* Signal the scheduler to exit its loop. */ xSchedulerEnd = pdTRUE; (void)pthread_kill( hMainThread, SIG_RESUME ); xCurrentThread = prvGetThreadFromTask( xTaskGetCurrentTaskHandle() ); prvSuspendSelf(xCurrentThread); }
- vPortEnterCritical 和 vPortExitCritical, 这里其实就是不使能中断, 由于这里时钟和线程都是通过线程信号来实现的, 其实就是屏蔽所有的信号pthread_sigmask( SIG_BLOCK, &xAllSignals, NULL );
void vPortEnterCritical( void ) { if ( uxCriticalNesting == 0 ) { vPortDisableInterrupts(); } uxCriticalNesting++; } /*-----------------------------------------------------------*/ void vPortExitCritical( void ) { uxCriticalNesting--; /* If we have reached 0 then re-enable the interrupts. */ if( uxCriticalNesting == 0 ) { vPortEnableInterrupts(); } }
- vPortDisableInterrupts和vPortEnableInterrupts其实就是屏蔽和开启信号
void vPortDisableInterrupts( void ) { pthread_sigmask( SIG_BLOCK, &xAllSignals, NULL ); } /*-----------------------------------------------------------*/ void vPortEnableInterrupts( void ) { pthread_sigmask( SIG_UNBLOCK, &xAllSignals, NULL ); }
- vPortYield 切换任务
void vPortYield( void ) { vPortEnterCritical(); vPortYieldFromISR(); vPortExitCritical(); } /*-----------------------------------------------------------*/ void vPortYieldFromISR( void ) { Thread_t *xThreadToSuspend; Thread_t *xThreadToResume; xThreadToSuspend = prvGetThreadFromTask( xTaskGetCurrentTaskHandle() ); vTaskSwitchContext(); xThreadToResume = prvGetThreadFromTask( xTaskGetCurrentTaskHandle() ); prvSwitchThread( xThreadToResume, xThreadToSuspend ); }
- prvSetupTimerInterrupt设置时钟周期 , 到点发送一个SIG_ALRM信号
void prvSetupTimerInterrupt( void ) { struct itimerval itimer; int iRet; /* Initialise the structure with the current timer information. */ iRet = getitimer( ITIMER_REAL, &itimer ); if ( iRet ) { prvFatalError( "getitimer", errno ); } /* Set the interval between timer events. */ itimer.it_interval.tv_sec = 0; itimer.it_interval.tv_usec = portTICK_RATE_MICROSECONDS; /* Set the current count-down. */ itimer.it_value.tv_sec = 0; itimer.it_value.tv_usec = portTICK_RATE_MICROSECONDS; /* Set-up the timer interrupt. */ iRet = setitimer( ITIMER_REAL, &itimer, NULL ); if ( iRet ) { prvFatalError( "setitimer", errno ); } prvStartTimeNs = prvGetTimeNs(); }
- vPortSystemTickHandler 增加时钟计数 ,获取最高优先级的,如果最高优先级跟当前的不是同一个的话, 切换到最高优先级
static void vPortSystemTickHandler( int sig ) { Thread_t *pxThreadToSuspend; Thread_t *pxThreadToResume; /* uint64_t xExpectedTicks; */ uxCriticalNesting++; /* Signals are blocked in this signal handler. */ #if ( configUSE_PREEMPTION == 1 ) pxThreadToSuspend = prvGetThreadFromTask( xTaskGetCurrentTaskHandle() ); #endif /* Tick Increment, accounting for any lost signals or drift in * the timer. */ /* * Comment code to adjust timing according to full demo requirements * xExpectedTicks = (prvGetTimeNs() - prvStartTimeNs) * / (portTICK_RATE_MICROSECONDS * 1000); * do { */ xTaskIncrementTick(); /* prvTickCount++; * } while (prvTickCount < xExpectedTicks); */ #if ( configUSE_PREEMPTION == 1 ) /* Select Next Task. */ vTaskSwitchContext(); pxThreadToResume = prvGetThreadFromTask( xTaskGetCurrentTaskHandle() ); // 如果两个不同才会切换 prvSwitchThread(pxThreadToResume, pxThreadToSuspend); #endif uxCriticalNesting--; }
移植总结
- 堆内存分配选用heap3, 使用标准库进行堆内存分配
- 时钟模拟,采用linux的时间相关函数,每隔一个时钟周期发送SIG_ALRM信号
- 与移植到ARM硬件通过中断处理不同, 这里通过线程间的信号来处理, 信号来了跳转到信号处理函数里处理, 这个处理逻辑跟中断其实非常像 , 这里pendsv中断是没有的, 直接调用函数执行切换就好了,中断本质无非就是跳转某处去执行一个函数
- 另外栈空间直接保存在专用的堆里(一开始创建时会申请), 就不会有硬件移植过程中出现的寄存器出入栈保存的问题
- 通过线程间的同步进制进行任务优先级调度, 具体为任务开始后沉睡, 并由FreeRTOS的调度器决定唤醒哪个,沉睡和唤醒通过线程间的同步机制即条件变量(cond_wait,cond_signal)
- 条件变量和信号量没有任何关系 , 是两个独立的机制
- FreeRTOS的任务调度主要通过两种方式来触发 . 第一种是时间 , 即每隔固定时间触发中断 , 在中断里进行调度; 第二种是主动切换调度 ; 第三种是有新的高优先级任务创建出来了。
- 只有工作线程才会处理信号,当时间片信号来的时候,如果发现时间片已到,就会切换
- vTaskStartScheduler是在一个单独的线程里的,会唤醒优先级最高的任务,然后自身沉睡。后续任务的切换其实都是工作线程发起并切换的
- 新创建任务 ,如果任务优先级最高,则会将pxCurrentTCB指向当前新创建的任务TCB(确保pxCurrentTCB指向优先级最高的就绪任务) , 并且加入就绪列表, 最后按下面代码执行上下文切换
if( xReturn == pdPASS ) { if( xSchedulerRunning != pdFALSE ) // 如果调度器已经开启,否则等开启 { /* 如果新创建的任务优先级大于当前任务优先级,则新创建的任务应该被立即执行。*/ if(pxCurrentTCB->uxPriority < uxPriority ) { taskYIELD_IF_USING_PREEMPTION(); } } }