RTOS精进(二):FreeRTOS 系统移植

FreeRTOS 系统移植

目录

移植步骤

  1. RTOS核心代码 , 比如task.c , queue.c 和list.c
  2. 一些与处理器架构相关的代码, FreeRTOS源码已经自带实现了很多不同的处理器和平台,它们位于FreeRTOS/Source/Portable/[相应编译器]/[相应处理器架构]文件夹下 ,即port.c 和 portmacro.h
  3. 堆栈分配, 在FreeRTOS/Source/portable/MemMang文件夹下具有各种类型的堆栈分配方案 , 即heap_x.c
  4. 编写FreeRTOSConfig.h文件, 在这其中做一些配置 ,比如时钟节拍(时钟频率是多少), 任务调度策略, 任务优先级划分成多少个, 具体可以参见FreeRTOS内核配置说明
  5. 编写一些钩子函数, 如果你在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 处理器 - 流水灯 - 博客园 Cortex-M3 系列处理器是基于 ARMv7-M 架构的处理器,应用非常广泛,为了能够深入的分析在此平台上跑 RTOS 的各种细节,所以有必要写一篇关于 CM3 处理器的结构相关的文章(CM4 类 https://www.cnblogs.com/god-of-death/p/14856578.html

  • Cortex M3是ARM针对微控制器设计的一种CPU Core , STM32某个型号就是Cortex M3的
  • 查看port.c 和 portmacro.h里涉及到哪些文件, 就知道该怎么移植了 (路径为FreeRTOS/Source/portable/RVDS/ARM_CM3), 参考移植步骤, 这里主要讲下port.c和portmacro.h的细节
移植细节
  1. 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
被用来存放函数的局部变量


  1. prvStartFirstTask
    //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,岂不是堆栈都没了么?是的,因为这是一条不归路,代码跑到这里,首先不会返回,之前压栈的内容再也不会用到,所以破坏之前的堆栈也没关系
    */
    
    FreeRTOS 启动第一个任务 prvStartFirstTask vPortSVCHandler_jltsun的博客-CSDN博客
  2. 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
    }
    
    
    1. SVC用于产生系统函数的调用请求。例如,操作系统通常不允许用户程序直接访问硬件,而是通过提供一些系统服务函数,让用户程序使用SVC发出对系统服务函数的调用请求,以这种方法调用它们来间接访问硬件。因此,当用户程序想要控制特定的硬件时,它就要产生一个SVC异常,然后操作系统提供的SVC异常服务程序得到执行,它再调用相关的操作系统函数,后者完成用户程序请求的服务。
    2. 结合prvStartFirstTaskpxPortInitialiseStack来看 , 就是将pxPortInitialiseStack里申请的那段空间里存放的R4-R11放到寄存器R4-R11中, 然后修改堆栈指针psp的值,使得取指PC指向最高优先级的 Ready 状态的任务指针(这其中还包含了xPSR,LR,R12,R3-R0, 退出函数时,栈的值会减少8(处理器自动操作), 并载入这8个值作为后续执行的参数,这其中就包括PC) 。 要完全理解可以参考这篇文章 https://blog.csdn.net/u012351051/article/details/125125796
  3. 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* */
    }
    
  4. 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();
    }
    
  5. 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;
    }
    
    
  6. vPortEndScheduler
    // 不需要实现, 直接放置一个断言
    void vPortEndScheduler( void )
    {
        /* Not implemented in ports where there is nothing to return to.
         * Artificially force an assert. */
        configASSERT( uxCriticalNesting == 1000UL );
    }
    
  7. 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 );
        }
    }
    
    
  8. vPortExitCritical
    // 本质就是使能中断
    
    void vPortExitCritical( void )
    {
        configASSERT( uxCriticalNesting );
        uxCriticalNesting--;
    
        if( uxCriticalNesting == 0 )
        {
            portENABLE_INTERRUPTS();
        }
    }
    
    
  9. 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 );                           \
    }
    
    
  10. vPortGetIPSR 获取当前的中断号 , 为啥这里没有返回值也行 ? 返回值是放在r0里的,实际上,在汇编语言中,函数调用的参数和返回值均可以通过寄存器来传送,只要函数内外相互配合,就可以精确地进行参数和返回值传递。 汇编认为返回的值就在r0里
    __asm uint32_t vPortGetIPSR( void )
    {
    /* *INDENT-OFF* */
        PRESERVE8 //代码段按8字节对齐
    
        mrs r0, ipsr   
        bx r14
    /* *INDENT-ON* */
    }
    
  11. 中断相关
    #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中断)
  • 另外宏根据需要设置, 时钟设置成硬件时钟的频率即可
参考资料

RTOS系列文章(8):深入分析中断处理过程_猪哥-嵌入式的博客-CSDN博客_rtos的中断和裸机中断 在上一篇文章中,我们深入分析了函数调用返回、中断调用返回使用的LR和EXC_TURN,也简单说明了函数调用流程、中断调用流程。在FreeRTOS中,任务的调度是通过SysTick + PendSV 中断来实现的,尤其在PenSV中断中进行任务上下文切换原理,是理解FreeRTOS任务调度原理的重中之重。而SysTick和PendSV都是中断,所以我们有必要深入分析一下在ARM CM3/4中中断的完 https://blog.csdn.net/u012351051/article/details/125125796

【FreeRTOS】FreeRTOS 源码学习笔记 (5) 任务调度器 + vTaskStartScheduler、xPortPendSVHandler、xPortSysTickHandler_taotao830的博客-CSDN博客_xportstartscheduler 1. 引言FreeRTOS的任务调度是个大头,也是一个操作系统的核心。其实个人理解,FreeRTOS调度规则很好理解,原则就是“优先级高抢占”,因为FreeRTOS是一个抢占式实时内核,一定会保证就绪态的高优先级任务可以先运行。所有的调度都是为了实现这个目的来做的。一些个人思考可以看4.1节。2. 原理分析2.1 什么情况会任务切换个人理解,只有当一个更高优先级的任务进入到就绪列表… https://blog.csdn.net/tao475824827/article/details/105622087

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, 下面详述细节
移植细节
  1. 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;
    }
    
    
  2. 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;
    }
    
  3. 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);
    }
    
  4. 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();
        }
    }
    
  5. vPortDisableInterrupts和vPortEnableInterrupts其实就是屏蔽和开启信号
    void vPortDisableInterrupts( void )
    {
        pthread_sigmask( SIG_BLOCK, &xAllSignals, NULL );
    }
    /*-----------------------------------------------------------*/
    
    void vPortEnableInterrupts( void )
    {
        pthread_sigmask( SIG_UNBLOCK, &xAllSignals, NULL );
    }
    
  6. 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 );
    }
    
    
  7. 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();
    }
    
  8. 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--;
    }
    
移植总结
  1. 堆内存分配选用heap3, 使用标准库进行堆内存分配
  2. 时钟模拟,采用linux的时间相关函数,每隔一个时钟周期发送SIG_ALRM信号
  3. 与移植到ARM硬件通过中断处理不同, 这里通过线程间的信号来处理, 信号来了跳转到信号处理函数里处理, 这个处理逻辑跟中断其实非常像 , 这里pendsv中断是没有的, 直接调用函数执行切换就好了,中断本质无非就是跳转某处去执行一个函数
  4. 另外栈空间直接保存在专用的堆里(一开始创建时会申请), 就不会有硬件移植过程中出现的寄存器出入栈保存的问题
  5. 通过线程间的同步进制进行任务优先级调度, 具体为任务开始后沉睡, 并由FreeRTOS的调度器决定唤醒哪个,沉睡和唤醒通过线程间的同步机制即条件变量(cond_wait,cond_signal)
  6. 条件变量和信号量没有任何关系 , 是两个独立的机制
  7. FreeRTOS的任务调度主要通过两种方式来触发 . 第一种是时间 , 即每隔固定时间触发中断 , 在中断里进行调度; 第二种是主动切换调度 ; 第三种是有新的高优先级任务创建出来了。
    1. 只有工作线程才会处理信号,当时间片信号来的时候,如果发现时间片已到,就会切换
    2. vTaskStartScheduler是在一个单独的线程里的,会唤醒优先级最高的任务,然后自身沉睡。后续任务的切换其实都是工作线程发起并切换的
    3. 新创建任务 ,如果任务优先级最高,则会将pxCurrentTCB指向当前新创建的任务TCB(确保pxCurrentTCB指向优先级最高的就绪任务) , 并且加入就绪列表, 最后按下面代码执行上下文切换
      if( xReturn == pdPASS )
      {
          if( xSchedulerRunning != pdFALSE ) // 如果调度器已经开启,否则等开启
          {
              /* 如果新创建的任务优先级大于当前任务优先级,则新创建的任务应该被立即执行。*/
              if(pxCurrentTCB->uxPriority < uxPriority )
              {
                  taskYIELD_IF_USING_PREEMPTION();
              }
          }
      }
      
成果物程序

gw_freertos_simu.zip

参考链接

FreeRTOS系列第5篇—FreeRTOS在Cortex-M3上的移植 - 极术社区 - 连接开发者与智能计算生态 下载包内的总文件数量多的令人生畏,但文件结构却很简洁。《FreeRTOS入门指南》一文的第3节详细描述了下载包文件结构,我们这里只是简单提一下。 https://aijishu.com/a/1060000000137854

FreeRTOS系列第6篇—FreeRTOS内核配置说明_研究是为了理解的博客-CSDN博客 FreeRTOS内核是高度可定制的,使用配置文件FreeRTOSConfig.h进行定制。每个FreeRTOS应用都必须包含这个头文件,用户根据实际应用来裁剪定制FreeRTOS内核。这个配置文件是针对用户程序的,而非内核,因此配置文件一般放在应用程序目录下,不要放在RTOS内核源码目录下。 在下载的FreeRTOS文件包中,每个演示例程都有一个FreeRTOSCon… https://blog.csdn.net/zhzht19861011/article/details/50134883

FreeRTOS simulator for Posix/Linux FreeRTOS is a portable, open source, mini Real Time kernel. A free RTOS for small embedded systems https://www.freertos.org/FreeRTOS-simulator-for-Linux.html

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值