首页 > 其他分享 >FreeRTOS 实时操作系统应用入门

FreeRTOS 实时操作系统应用入门

时间:2022-10-27 15:04:39浏览次数:82  
标签:const 入门 FreeRTOS void list queue 任务 实时操作系统 define

一、FreeRT基本知识

1. FreeRT中的链表

  (1) 链表节点数据结构

struct xLIST_ITEM
{
configLIST_VOLATILE TickType_t xItemValue; /*< The value being listed. In most cases this is used to sort the list in descending order. */ struct xLIST_ITEM * configLIST_VOLATILE pxNext; /*< Pointer to the next ListItem_t in the list. */ struct xLIST_ITEM * configLIST_VOLATILE pxPrevious; /*< Pointer to the previous ListItem_t in the list. */ void * pvOwner; /*< Pointer to the object (normally a TCB) that contains the list item. There is therefore a two way link between the object containing the list item and the list item itself. */ struct xLIST * configLIST_VOLATILE pxContainer; /*< Pointer to the list in which this list item is placed (if any). */ };

  (2) 链表节点初始化

void vListInitialiseItem( ListItem_t * const pxItem )
{
    /* Make sure the list item is not recorded as being on a list. */
    pxItem->pxContainer = NULL;
}

  (3) 链表根节点数据结构

typedef struct xLIST
{
    volatile UBaseType_t uxNumberOfItems;
    ListItem_t * configLIST_VOLATILE pxIndex;            /*< Used to walk through the list.  Points to the last item returned by a call to listGET_OWNER_OF_NEXT_ENTRY (). */
    MiniListItem_t xListEnd;                            /*< List item that contains the maximum possible item value meaning it is always at the end of the list and is therefore used as a marker. */
} List_t;

  (4) 链表根节点初始化

void vListInitialise( List_t * const pxList )
{
    /* The list structure contains a list item which is used to mark the
    end of the list.  To initialise the list the list end is inserted
    as the only list entry. */
    pxList->pxIndex = ( ListItem_t * ) &( pxList->xListEnd );            /*lint !e826 !e740 !e9087 The mini list structure is used as the list end to save RAM.  This is checked and valid. */

    /* The list end value is the highest possible value in the list to
    ensure it remains at the end of the list. */
    pxList->xListEnd.xItemValue = portMAX_DELAY;

    /* The list end next and previous pointers point to itself so we know
    when the list is empty. */
    pxList->xListEnd.pxNext = ( ListItem_t * ) &( pxList->xListEnd );    /*lint !e826 !e740 !e9087 The mini list structure is used as the list end to save RAM.  This is checked and valid. */
    pxList->xListEnd.pxPrevious = ( ListItem_t * ) &( pxList->xListEnd );/*lint !e826 !e740 !e9087 The mini list structure is used as the list end to save RAM.  This is checked and valid. */

    pxList->uxNumberOfItems = ( UBaseType_t ) 0U;
}

  (5) 将节点插入链表尾部

void vListInsertEnd( List_t * const pxList, ListItem_t * const pxNewListItem )
{
  ListItem_t * const pxIndex = pxList->pxIndex;
/* Insert a new list item into pxList, but rather than sort the list,
    makes the new list item the last item to be removed by a call to
    listGET_OWNER_OF_NEXT_ENTRY(). */
    pxNewListItem->pxNext = pxIndex;
    pxNewListItem->pxPrevious = pxIndex->pxPrevious;

    /* Only used during decision coverage testing. */
    mtCOVERAGE_TEST_DELAY();

    pxIndex->pxPrevious->pxNext = pxNewListItem;
    pxIndex->pxPrevious = pxNewListItem;

    /* Remember which list the item is in. */
    pxNewListItem->pxContainer = pxList;

    ( pxList->uxNumberOfItems )++;
}

  (6) 将节点按升序插入链表

void vListInsert( List_t * const pxList, ListItem_t * const pxNewListItem ) PRIVILEGED_FUNCTION;

  (7) 将节点从链表中删除

UBaseType_t uxListRemove( ListItem_t * const pxItemToRemove ) PRIVILEGED_FUNCTION;

  (8) 节点带参宏函数

     ① 设置节点拥有者

#define listSET_LIST_ITEM_OWNER( pxListItem, pxOwner )        ( ( pxListItem )->pvOwner = ( void * ) ( pxOwner ) )

    ② 获取节点拥有者

#define listGET_LIST_ITEM_OWNER( pxListItem )    ( ( pxListItem )->pvOwner )

    ③ 设置节点排序辅助值

#define listSET_LIST_ITEM_VALUE( pxListItem, xValue )    ( ( pxListItem )->xItemValue = ( xValue ) )

    ④ 获取节点排序辅助值

#define listGET_LIST_ITEM_VALUE( pxListItem )    ( ( pxListItem )->xItemValue )

    ⑤ 获取链表中第一个普通节点的辅助值

#define listGET_ITEM_VALUE_OF_HEAD_ENTRY( pxList )    ( ( ( pxList )->xListEnd ).pxNext->xItemValue )

    ⑥ 获取链表的入口节点

#define listGET_HEAD_ENTRY( pxList )    ( ( ( pxList )->xListEnd ).pxNext )

    ⑦ 获取节点的下一个节点

#define listGET_NEXT( pxListItem )    ( ( pxListItem )->pxNext )

    ⑧ 获取链表的最后一个节点标志

#define listGET_END_MARKER( pxList )    ( ( ListItem_t const * ) ( &( ( pxList )->xListEnd ) ) )

    ⑨ 判断链表是否为空

#define listLIST_IS_EMPTY( pxList )    ( ( ( pxList )->uxNumberOfItems == ( UBaseType_t ) 0 ) ? pdTRUE : pdFALSE )

    ⑩ 获取链表节点数

#define listCURRENT_LIST_LENGTH( pxList )    ( ( pxList )->uxNumberOfItems )

    ⑪ 获取链表第一个节点的OWNER,即TCB(链表主要是为任务就绪表设计的)

#define listGET_OWNER_OF_HEAD_ENTRY( pxList )  ( (&( ( pxList )->xListEnd ))->pxNext->pvOwner )

    ⑫ 获取链表下一个节点的OWNER

#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList )                                        \
{                                                                                            \
    List_t * const pxConstList = ( pxList );                                                    \
    /* Increment the index to the next item and return the item, ensuring */                \
    /* we don't return the marker used at the end of the list.  */                            \
    ( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;                            \
    if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) )    \
    {                                                                                        \
        ( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;                        \
    }                                                                                        \
    ( pxTCB ) = ( pxConstList )->pxIndex->pvOwner;                                            \
}

      注:链表根节点的pxIndex主要作用是用来索引节点的

    ⑬ 检查某一个节点是否在链表中

#define listIS_CONTAINED_WITHIN( pxList, pxListItem ) ( ( ( pxListItem )->pxContainer == ( pxList ) ) ? ( pdTRUE ) : ( pdFALSE ) )

    ⑭ 返回某一节点所在的链表

#define listLIST_ITEM_CONTAINER( pxListItem ) ( ( pxListItem )->pxContainer )

    ⑮ 检查某一链表是否初始化()

#define listLIST_IS_INITIALISED( pxList ) ( ( pxList )->xListEnd.xItemValue == portMAX_DELAY ) 

  (9) 链表示意图

 

 

 2. FreeRT任务模型

(1)创建任务

  ① 定义任务栈

#define TASK1_STACK_SIZE                    128
StackType_t Task1Stack[TASK1_STACK_SIZE];

  ② 定义任务函数

void Task1Entry( void *p_arg )
{
  ...   
}

  ③ 定义任务控制块

typedef struct tskTaskControlBlock
{
    volatile StackType_t* pxTopOfStack;       //Stack top
    
    ListItem_t xStateListItem;                    //List
  
    StackType_t* pxStack;                         //Stack start
    
    char pcTaskName[ configMAX_TASK_NAME_LEN ];   //Task name
  
    TickType_t xTicksToDelay;               //
  
    UBaseType_t uxPriority;                  //Priority
} tskTCB;

  ⑤ 实现任务创建函数:xTaskCreateStatic()

TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode,           /* 任务入口 */
                                const char * const pcName,           /* 任务名称,字符串形式 */
                                const uint32_t ulStackDepth,         /* 任务栈大小,单位为字 */
                                void * const pvParameters,           /* 任务形参 */
                                UBaseType_t uxPriority,              /* 任务优先级,数值越大,优先级越高 */
                                StackType_t * const puxStackBuffer,  /* 任务栈起始地址 */
                                TCB_t * const pxTaskBuffer );        /* 任务控制块 */

    * xTaskCreateStatic()调用prvInitialiseNewTask()来创建任务

static void prvInitialiseNewTask(   TaskFunction_t pxTaskCode,              /* 任务入口 */
                                    const char * const pcName,              /* 任务名称,字符串形式 */
                                    const uint32_t ulStackDepth,            /* 任务栈大小,单位为字 */
                                    void * const pvParameters,              /* 任务形参 */
                                    UBaseType_t uxPriority,                 /* 任务优先级,数值越大,优先级越高 */
                                    TaskHandle_t * const pxCreatedTask,     /* 任务句柄 */
                                    TCB_t *pxNewTCB )                       /* 任务控制块 */

{
    StackType_t *pxTopOfStack;
    UBaseType_t x;    
    
    /* 获取栈顶地址 */
    pxTopOfStack = pxNewTCB->pxStack + ( ulStackDepth - ( uint32_t ) 1 );
    //pxTopOfStack = ( StackType_t * ) ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );
    /* 向下做8字节对齐 */
    pxTopOfStack = ( StackType_t * ) ( ( ( uint32_t ) pxTopOfStack ) & ( ~( ( uint32_t ) 0x0007 ) ) );    

    /* 将任务的名字存储在TCB中 */
    for( x = ( UBaseType_t ) 0; x < ( UBaseType_t ) configMAX_TASK_NAME_LEN; x++ )
    {
        pxNewTCB->pcTaskName[ x ] = pcName[ x ];

        if( pcName[ x ] == 0x00 )
        {
            break;
        }
    }
    /* 任务名字的长度不能超过configMAX_TASK_NAME_LEN */
    pxNewTCB->pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] = '\0';
    
    vListInitialiseItem( &( pxNewTCB->xStateListItem ) );
    listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB );
    
    /* 初始化优先级 */
    if( uxPriority >= ( UBaseType_t ) configMAX_PRIORITIES )
    {
        uxPriority = ( UBaseType_t ) configMAX_PRIORITIES - ( UBaseType_t ) 1U;
    }
    pxNewTCB->uxPriority = uxPriority;
    
    /* 初始化任务栈 */
    pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters ); 
    
    /* 让任务句柄指向任务控制块 */
    if( ( void * ) pxCreatedTask != NULL )
    {        
        *pxCreatedTask = ( TaskHandle_t ) pxNewTCB;
    }
}

     * pxPortInitialiseStack()函数用于初始化任务栈

StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters )
{
    pxTopOfStack--;
    *pxTopOfStack = portINITIAL_XPSR;                                        /* xPSR的bit24必须置1 */
    pxTopOfStack--;
    *pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK;    /* PC,即任务入口函数 */
    pxTopOfStack--;
    *pxTopOfStack = ( StackType_t ) prvTaskExitError;                        /* LR,函数返回地址 */

    pxTopOfStack -= 5;    /* R12, R3, R2 and R1 默认初始化为0 */
    *pxTopOfStack = ( StackType_t ) pvParameters;                            /* R0,任务形参 */
    pxTopOfStack -= 8;    /* R11, R10, R9, R8, R7, R6, R5 and R4默认初始化为0 */

    return pxTopOfStack;
}

    * 任务栈初始化完成后栈空间分布图

 

(2)实现任务就绪表

  ① 定义任务就绪表

List_t pxReadyTasksLists[ configMAX_PRIORITIES ];

  注: 任务就绪表就是一个List_t类型的数组,数组下表对应任务的优先级,同一优先级的任务插入同一链表中

   ② 任务就绪表初始化

void prvInitialiseTaskLists( void )
{
    UBaseType_t uxPriority;
    
    for( uxPriority = ( UBaseType_t ) 0U; uxPriority < ( UBaseType_t ) configMAX_PRIORITIES; uxPriority++ )
    {
        vListInitialise( &( pxReadyTasksLists[ uxPriority ] ) );
    }
    
    vListInitialise( &xDelayedTaskList1 );
    vListInitialise( &xDelayedTaskList2 );
    
    pxDelayedTaskList = &xDelayedTaskList1;
    pxOverflowDelayedTaskList = &xDelayedTaskList2;
}

  ③ 将任务插入任务就绪表

void prvAddNewTaskToReadyList( TCB_t *pxNewTCB );

 

(3)实现调度器

  ① 启动调度器

    * vTaskStartScheduler()函数

void vTaskStartScheduler( void );

    * vTaskStartScheduler()函数调用xPortStartScheduler()

BaseType_t xPortStartScheduler( void )
{
    /* 配置PendSV 和 SysTick 的中断优先级为最低 */
    portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
    portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
    
    /* 初始化SysTick */
    vPortSetupTimerInterrupt();

    /* 启动第一个任务,不再返回 */
    prvStartFirstTask();

    /* 不应该运行到这里 */
    return 0;
}

    * xPortStartScheduler()函数调用 prvStartFirstTask()

__asm void prvStartFirstTask( void )
{
    PRESERVE8

    /* 在Cortex-M中,0xE000ED08是SCB_VTOR这个寄存器的地址,
       里面存放的是向量表的起始地址,即MSP的地址 */
    ldr r0, =0xE000ED08
    ldr r0, [r0]
    ldr r0, [r0]

    /* 设置主堆栈指针msp的值 */
    msr msp, r0
    
    /* 使能全局中断 */
    cpsie i
    cpsie f
    dsb
    isb
    
    /* 调用SVC去启动第一个任务 */
    svc 0  
    nop
    nop
}

    * svc 0: 触发svc异常调用svc异常处理函数SVC_Handler(xPortPendSVHandler())

__asm void vPortSVCHandler( void )
{
    extern pxCurrentTCB;
    
    PRESERVE8

    ldr    r3, =pxCurrentTCB    /* 加载pxCurrentTCB的地址到r3 */
    ldr r1, [r3]            /* 加载pxCurrentTCB到r1 */
    ldr r0, [r1]            /* 加载pxCurrentTCB指向的值到r0,目前r0的值等于第一个任务堆栈的栈顶 */
    ldmia r0!, {r4-r11}        /* 以r0为基地址,将栈里面的内容加载到r4~r11寄存器,同时r0会递增 */
    msr psp, r0                /* 将r0的值,即任务的栈指针更新到psp */
    isb
    mov r0, #0              /* 设置r0的值为0 */
    msr    basepri, r0         /* 设置basepri寄存器的值为0,即所有的中断都没有被屏蔽 */
    orr r14, #0xd           /* 当从SVC中断服务退出前,通过向r14寄存器最后4位按位或上0x0D,
                              使得硬件在退出时使用进程堆栈指针PSP完成出栈操作并返回后进入线程模式、返回Thumb状态 */
    
    bx r14                  /* 异常返回,这个时候栈中的剩下内容将会自动加载到CPU寄存器:
                               xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)
                               同时PSP的值也将更新,即指向任务栈的栈顶 */
}

 

  ② 任务切换

    * taskYIELD():通过写SCB->ICSR寄存器,触发PendSV异常PendSV_Handler(xPortPendSVHandler)

__asm void xPortPendSVHandler( void )
{
//    extern uxCriticalNesting;
    extern pxCurrentTCB;
    extern vTaskSwitchContext;

    PRESERVE8

    /* 当进入PendSVC Handler时,上一个任务运行的环境即:
       xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)
       这些CPU寄存器的值会自动保存到任务的栈中,剩下的r4~r11需要手动保存 */
    /* 获取任务栈指针到r0 */
    mrs r0, psp
    isb

    ldr    r3, =pxCurrentTCB        /* 加载pxCurrentTCB的地址到r3 */
    ldr    r2, [r3]                /* 加载pxCurrentTCB到r2 */

    stmdb r0!, {r4-r11}            /* 将CPU寄存器r4~r11的值存储到r0指向的地址 */
    str r0, [r2]                /* 将任务栈的新的栈顶指针存储到当前任务TCB的第一个成员,即栈顶指针 */                
                               

    stmdb sp!, {r3, r14}        /* 将R3和R14临时压入堆栈,因为即将调用函数vTaskSwitchContext,
                                  调用函数时,返回地址自动保存到R14中,所以一旦调用发生,R14的值会被覆盖,因此需要入栈保护;
                                  R3保存的当前激活的任务TCB指针(pxCurrentTCB)地址,函数调用后会用到,因此也要入栈保护 */
    mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY    /* 进入临界段 */
    msr basepri, r0
    dsb
    isb
    bl vTaskSwitchContext       /* 调用函数vTaskSwitchContext,寻找新的任务运行,通过使变量pxCurrentTCB指向新的任务来实现任务切换 */ 
    mov r0, #0                  /* 退出临界段 */
    msr basepri, r0
    ldmia sp!, {r3, r14}        /* 恢复r3和r14 */

    ldr r1, [r3]
    ldr r0, [r1]                 /* 当前激活的任务TCB第一项保存了任务堆栈的栈顶,现在栈顶值存入R0*/
    ldmia r0!, {r4-r11}            /* 出栈 */
    msr psp, r0
    isb
    bx r14                      /* 异常发生时,R14中保存异常返回标志,包括返回后进入线程模式还是处理器模式、
                                   使用PSP堆栈指针还是MSP堆栈指针,当调用 bx r14指令后,硬件会知道要从异常返回,
                                   然后出栈,这个时候堆栈指针PSP已经指向了新任务堆栈的正确位置,
                                   当新任务的运行地址被出栈到PC寄存器后,新的任务也会被执行。*/
    nop
}

     注:xPortPendSVHandler()的作用

      * 储存上一个任务的运行环境到上一个任务的栈

      * 寻找优先级最高的TCB表

      * 将寻找到的TCB表的栈中所保存的运行环境加载到CPU的寄存器

  ③ Cortex-M3/M4内核异常处理基本知识

    * 处理器进入异常处理或中断服务程序(ISR)时,会自动把寄存器xPSR、PC(返回地址或者任务入口地址)、LR、R12、R0-R3寄存器压入栈,将LR寄存器(R14)的值赋值为EXC_RETURN

    * 当利用BX、POP或者存储加载指令(LDR、LDM)将LR寄存器(R14)加载到程序计数器(PC)中时会触发异常返回机制

    * EXE_RETURN定义:

    * EXE_RETURN合法值:

    * 异常返回:处理器首先根据EXE_RETURN的第2位判断使用MSP或PSP,然后将栈中的PC指针取出,然后再依次取其他寄存器

  ④ 任务切换的本质

    * 任务切换的本质是各个任务的栈帧切换,而各个任务的栈地址是保存在各个任务的TCB中的

    * 异常进入压栈和异常返回出栈,只会保存或者恢复CPU除了SP(MSP或PSP)以外的寄存器(手动或者自动),而SP的值是不变的,需要手动修改(如果需要任务切换)

    * 任务管理的本质就是管理各个任务的SP,而SP是保存在各个任务的TCB表中的

 

3. 临界段的保护

(1)临界段:一段在执行时不能被打断的代码

(2)代码段执行时被打断的情况:

  ① 系统调度

  ② 外部中断

(3)FreeRT中,系统调度是通过PendSv中断实现的,所以对临界段的保护可以通过关闭中断实现

(4)不带返回值的关中断函数

#define portDISABLE_INTERRUPTS()                vPortRaiseBASEPRI()

static portFORCE_INLINE void vPortRaiseBASEPRI( void ) { uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY; __asm { /* Set BASEPRI to the max syscall priority to effect a critical section. */ msr basepri, ulNewBASEPRI dsb isb } }

(4)带返回值的关中断函数:可以嵌套,主要在中断中使用。在修改BASEPRI之前,先将它的值保存起来,以便后面恢复

#define portSET_INTERRUPT_MASK_FROM_ISR()        ulPortRaiseBASEPRI()
static portFORCE_INLINE uint32_t ulPortRaiseBASEPRI( void )
{
uint32_t ulReturn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;

    __asm
    {
        /* Set BASEPRI to the max syscall priority to effect a critical
        section. */
        mrs ulReturn, basepri
        msr basepri, ulNewBASEPRI
        dsb
        isb
    }

    return ulReturn;
}

(5)开中断

#define portENABLE_INTERRUPTS()                    vPortSetBASEPRI( 0 )
static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
{
    __asm
    {
        /* Barrier instructions are not used as this function is only used to
        lower the BASEPRI value. */
        msr basepri, ulBASEPRI
    }
}

(6)进入退出临界段的宏

#define portENTER_CRITICAL()                    vPortEnterCritical()
#define portEXIT_CRITICAL()                        vPortExitCritical()
#define portSET_INTERRUPT_MASK_FROM_ISR()        ulPortRaiseBASEPRI()
#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x)    vPortSetBASEPRI(x)

 

 4. 空闲任务与阻塞延时

(1)FreeRT中空闲任务是系统在启动时创建的优先级最低的任务,主要做一些内存清理工作(当没有其它任务运行时,保证CPU有一条任务在运行)

(2)阻塞延时

  ① 阻塞延时函数:vTaskDelay()

void vTaskDelay( const TickType_t xTicksToDelay )
{
    TCB_t *pxTCB = NULL;
    
    /* 获取当前任务的TCB */
    pxTCB = pxCurrentTCB;
    
    /* 将任务插入到延时列表 */
    prvAddCurrentTaskToDelayedList( xTicksToDelay );
    
    /* 任务切换 */
    taskYIELD();
}

(3)SysTick中断服务函数

  ①  在任务上下文切换函数vTaskSwitchContext()中,会判断延时列表中任务控制块的成员xTicksToDelay是否为0,如果为0,则将该任务控制块从延时列表中删除,插入任务就绪表中

  ② 而延时列表中各个任务控制块的成员xTicksToDelay递减是发生在Systick的中断服务函数中

  ③ Systick的中断服务函数还会触发一次调度

void xPortSysTickHandler( void )
{
    /* 关中断 */
    vPortRaiseBASEPRI();
    
    {
        //xTaskIncrementTick();
        
        /* 更新系统时基 */
        if( xTaskIncrementTick() != pdFALSE )
        {
            /* 任务切换,即触发PendSV */
            //portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
            taskYIELD();
        }
    }

    /* 开中断 */
    vPortClearBASEPRIFromISR();
}

 

 5. 多优先级支持

(1)任务就绪列表pxReadyTasksLists是一个链表数组,数组下标对应优先级,数字越小,优先级越低

List_t pxReadyTasksLists[ configMAX_PRIORITIES ];

(2)pxCurrentTCB是一个全局TCB指针,用于指向优先级最高的任务的TCB,即当前正在运行的任务

(3)uxTopReadyPriority是一个tack.c中的静态变量,用于表示任务就绪表中的最高优先级

(4)查找任务就绪表中的最高优先级:本质是更新uxTopReadyPriority和pxCurrentTCB

 

6.任务延时列表

 

7. 时间片支持

 

 

 二、FreeRT应用开发

1. 任务

(1)FreeRTOS中,任务是竞争体统资源的最小单元,如果configUSE_TIME_SLICING定义为1,处于就绪态的多个相同优先级任务将会以时间片切换的方式共享处理器

(2)任务状态

  ① 就绪态(ready): 任务处于就绪列表中,等待调度运行

  ② 运行态(running): 任务处于执行中

  ③ 阻塞态(blocked): 任务正在等待某个时序或中断,包括任务挂起、任务延时、任务等待信号量等

  ④ 挂起态(suspended): 将任务从就绪列表中暂时删去(任务可以处于就绪态或运行态)

    注:任务挂起是针对就绪态任务和运行态任务,而阻塞一般只针对运行态任务

(3)任务状态迁移

(4)FreeRTOS任务管理的几个重要结构

  ① 创建任务时需要传入的参数:TaskParameters_t(给应用程序用的)

typedef struct xTASK_PARAMETERS
{
    TaskFunction_t pvTaskCode;
    const char * const pcName;    /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
    configSTACK_DEPTH_TYPE usStackDepth;
    void *pvParameters;
    UBaseType_t uxPriority;
    StackType_t *puxStackBuffer;
    MemoryRegion_t xRegions[ portNUM_CONFIGURABLE_REGIONS ];
    #if ( ( portUSING_MPU_WRAPPERS == 1 ) && ( configSUPPORT_STATIC_ALLOCATION == 1 ) )
        StaticTask_t * const pxTaskBuffer;
    #endif
} TaskParameters_t;

  ② 内核用于描述一个任务的结构 TaskStatus_t

typedef struct xTASK_STATUS
{
    TaskHandle_t xHandle;            /* The handle of the task to which the rest of the information in the structure relates. */
    const char *pcTaskName;            /* A pointer to the task's name.  This value will be invalid if the task was deleted since the structure was populated! */ /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
    UBaseType_t xTaskNumber;        /* A number unique to the task. */
    eTaskState eCurrentState;        /* The state in which the task existed when the structure was populated. */
    UBaseType_t uxCurrentPriority;    /* The priority at which the task was running (may be inherited) when the structure was populated. */
    UBaseType_t uxBasePriority;        /* The priority to which the task will return if the task's current priority has been inherited to avoid unbounded priority inversion when obtaining a mutex.  Only valid if configUSE_MUTEXES is defined as 1 in FreeRTOSConfig.h. */
    uint32_t ulRunTimeCounter;        /* The total run time allocated to the task so far, as defined by the run time stats clock.  See http://www.freertos.org/rtos-run-time-stats.html.  Only valid when configGENERATE_RUN_TIME_STATS is defined as 1 in FreeRTOSConfig.h. */
    StackType_t *pxStackBase;        /* Points to the lowest address of the task's stack area. */
    configSTACK_DEPTH_TYPE usStackHighWaterMark;    /* The minimum amount of stack space that has remained for the task since the task was created.  The closer this value is to zero the closer the task has come to overflowing its stack. */
} TaskStatus_t;

    * TaskHandle_t: 结构为tskTaskControlBlock(任务控制块)类型的指针

    * pcTaskName: 任务名称

    * xTaskNumber: 任务ID

    * eCurrentState: 任务当前状态

    * uxCurrentPriority:任务当前优先级

    * uxBasePriority:任务基本优先级(由应用程序开发者指定的优先级)

      注:任务当前优先级(因为优先级继承机制)可能会高于基本优先级,任务调度器是按当前优先级来调度的(任务当前优先级>=任务基本优先级)

    * ulRunTimeCounter:任务运行次数

    * pxStackBase:任务栈的起始地址

  ③ 任务控制块(TCB):tskTaskControlBlock

typedef struct tskTaskControlBlock             /* The old naming convention is used to prevent breaking kernel aware debuggers. */
{
    volatile StackType_t    *pxTopOfStack;    /*< Points to the location of the last item placed on the tasks stack.  THIS MUST BE THE FIRST MEMBER OF THE TCB STRUCT. */

    ListItem_t            xStateListItem;    /*< The list that the state list item of a task is reference from denotes the state of that task (Ready, Blocked, Suspended ). */
    ListItem_t            xEventListItem;        /*< Used to reference a task from an event list. */
    UBaseType_t            uxPriority;            /*< The priority of the task.  0 is the lowest priority. */
    StackType_t            *pxStack;            /*< Points to the start of the stack. */
    char                pcTaskName[ configMAX_TASK_NAME_LEN ];/*< Descriptive name given to the task when created.  Facilitates debugging only. */ /*lint !e971 Unqualified char types are allowed for strings and single characters only. */

} tskTCB;

 

(5)任务相关函数

  ① 任务静态创建函数

    TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode,
                                    const char * const pcName,        /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
                                    const uint32_t ulStackDepth,
                                    void * const pvParameters,
                                    UBaseType_t uxPriority,
                                    StackType_t * const puxStackBuffer,
                                    StaticTask_t * const pxTaskBuffer )

  * pxTaskCode: 任务入口函数

  * pcName:任务名称

  * ulStackDepth:任务栈大小

  * pvParameters: 任务入口参数

  * uxPriority:任务优先级

  * puxStackBuffer:任务栈起始地址

  * pxTaskBuffer:任务控制块指针

  * 返回值:返回任务控制块指针

    注1:任务控制块为FreeRT内核描述任务的结构体,每一个任务控制块对应一个任务

    注2:栈空间地址对齐:FreeRTOS中是以8字节对齐的(portBYTE_ALIGNMENT = 8)

    注3:要静态创建任务,configSUPPORT_STATIC_ALLOCATION必须配置为1

 

  ② 任务动态创建函数

    BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
                            const char * const pcName,        /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
                            const configSTACK_DEPTH_TYPE usStackDepth,
                            void * const pvParameters,
                            UBaseType_t uxPriority,
                            TaskHandle_t * const pxCreatedTask )

  * pxTaskCode: 任务入口函数

  * pcName: 任务名称

  * usStackDepth: 任务栈大小

  * pvParameters: 任务入口参数

  * uxPriority: 任务优先级

  * pxCreatedTask: 任务控制块

  * 返回值:状态码

    注1:动态内存的来源: FreeRTOS是在SRAM中定义一个大数组来供动态内存分配函数使用

static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];

 

  ③ 启动任务(就绪态->运行态)

void vTaskStartScheduler( void )

  注1:任务调度器只启动一次

  注2:任务调度器会先创建两个任务,空闲任务和定时器任务(configUSE_TIMERS)。然后再调用调度函数 xPortStartScheduler()

    * 空闲任务:

    * 定时器任务:

 

  ④ 任务挂起(就绪态->挂起态 || 运行态->挂起态)

void vTaskSuspend( TaskHandle_t xTaskToSuspend );

 

  ⑤ 任务恢复(挂起态->就绪态)

void vTaskResume( TaskHandle_t xTaskToResume );
BaseType_t xTaskResumeFromISR( TaskHandle_t xTaskToResume );

 

  ⑥ 任务延时

void vTaskDelay( const TickType_t xTicksToDelay );
void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement )

 

 

2. FreeRTOS启动流程

(1)

 

 

 

3. 消息队列

(1)消息队列运作过程

(2)消息队列控制块

typedef struct QueueDefinition
{
    int8_t *pcHead;                    /*< Points to the beginning of the queue storage area. */
    int8_t *pcTail;                    /*< Points to the byte at the end of the queue storage area.  Once more byte is allocated than necessary to store the queue items, this is used as a marker. */
    int8_t *pcWriteTo;                /*< Points to the free next place in the storage area. */

    union                            /* Use of a union is an exception to the coding standard to ensure two mutually exclusive structure members don't appear simultaneously (wasting RAM). */
    {
        int8_t *pcReadFrom;            /*< Points to the last place that a queued item was read from when the structure is used as a queue. */
        UBaseType_t uxRecursiveCallCount;/*< Maintains a count of the number of times a recursive mutex has been recursively 'taken' when the structure is used as a mutex. */
    } u;

    List_t xTasksWaitingToSend;        /*< List of tasks that are blocked waiting to post onto this queue.  Stored in priority order. */
    List_t xTasksWaitingToReceive;    /*< List of tasks that are blocked waiting to read from this queue.  Stored in priority order. */

    volatile UBaseType_t uxMessagesWaiting;/*< The number of items currently in the queue. */
    UBaseType_t uxLength;            /*< The length of the queue defined as the number of items it will hold, not the number of bytes. */
    UBaseType_t uxItemSize;            /*< The size of each items that the queue will hold. */

    volatile int8_t cRxLock;        /*< Stores the number of items received from the queue (removed from the queue) while the queue was locked.  Set to queueUNLOCKED when the queue is not locked. */
    volatile int8_t cTxLock;        /*< Stores the number of items transmitted to the queue (added to the queue) while the queue was locked.  Set to queueUNLOCKED when the queue is not locked. */

} xQUEUE;

(3)消息队列常用函数

  ① 创建消息队列

    * 动态创建函数:xQueueCreate()

#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
    #define xQueueCreate( uxQueueLength, uxItemSize ) xQueueGenericCreate( ( uxQueueLength ), ( uxItemSize ), ( queueQUEUE_TYPE_BASE ) )
#endif

    * 静态创建函数:xQueueCreateStatic()

#if( configSUPPORT_STATIC_ALLOCATION == 1 )
    #define xQueueCreateStatic( uxQueueLength, uxItemSize, pucQueueStorage, pxQueueBuffer ) xQueueGenericCreateStatic( ( uxQueueLength ), ( uxItemSize ), ( pucQueueStorage ), ( pxQueueBuffer ), ( queueQUEUE_TYPE_BASE ) )
#endif /* configSUPPORT_STATIC_ALLOCATION */

    * 消息队列创建完成示意图

 

  ② 写队列操作

    * 任务向队列尾部写入函数:xQueueSend()和xQueueSendToBack()

#define xQueueSend( xQueue, pvItemToQueue, xTicksToWait ) xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK )
#define xQueueSendToBack( xQueue, pvItemToQueue, xTicksToWait ) xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK )

    * 中断向队列尾部写入函数:xQueueSendFromISR()和xQueueSendToBackFromISR()

#define xQueueSendFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken ) xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), ( pxHigherPriorityTaskWoken ), queueSEND_TO_BACK )
#define xQueueSendToBackFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken ) xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), ( pxHigherPriorityTaskWoken ), queueSEND_TO_BACK )

    * 任务向队列头部写入函数

#define xQueueSendToFront( xQueue, pvItemToQueue, xTicksToWait ) xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_FRONT )

    * 中断向队列头部写入函数

#define xQueueSendToFrontFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken ) xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), ( pxHigherPriorityTaskWoken ), queueSEND_TO_FRONT )

    * 任务向队列写入通用函数

BaseType_t xQueueGenericSend( QueueHandle_t xQueue, const void * const pvItemToQueue, TickType_t xTicksToWait, const BaseType_t xCopyPosition )
{

    * 中断向队列写入通用函数

BaseType_t xQueueGenericSendFromISR( QueueHandle_t xQueue, const void * const pvItemToQueue, BaseType_t * const pxHigherPriorityTaskWoken, const BaseType_t xCopyPosition )

 

  ③ 读队列操作

     * 任务读消息函数:xQueueReceive()和xQueuePeek()

BaseType_t xQueueReceive( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait )
BaseType_t xQueuePeek( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait )

      注:xQueueReceive()和xQueuePeek()的区别是xQueueReceive读完会删除消息,而xQueuePeek读完不会删除消息

    * 中断读取消息函数:xQueueReceiveFromISR()和xQueuePeekFromISR()

BaseType_t xQueueReceiveFromISR( QueueHandle_t xQueue, void * const pvBuffer, BaseType_t * const pxHigherPriorityTaskWoken )
BaseType_t xQueuePeekFromISR( QueueHandle_t xQueue,  void * const pvBuffer )

    * 任务读取消息通用函数

BaseType_t xQueueGenericReceive( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait, const BaseType_t xJustPeeking )
{

 

  ④ 删除队列:vQueueDelete()

void vQueueDelete( QueueHandle_t xQueue )

 

 

4. 信号量

(1)信号量(semaphore)是一种实现任务间通信的机制可以实现任务之间同步或临界资源的互斥访问

(2)信号量是一个非负整数,所有获取它的任务都会将该整数减1,当该整数值为0时,所有试图获取它的任务都将处于阻塞状态

(3)信号量分类

  ① 二值信号量:信号量只有0和1两种值,是任务间同步的重要手段

  ② 计数信号量

  ③ 互斥信号量

  ④ 递归信号量

(4)信号量控制块

typedef struct QueueDefinition
{
    int8_t *pcHead;                    /*< Points to the beginning of the queue storage area. */
    int8_t *pcTail;                    /*< Points to the byte at the end of the queue storage area.  Once more byte is allocated than necessary to store the queue items, this is used as a marker. */
    int8_t *pcWriteTo;                /*< Points to the free next place in the storage area. */

    union                            /* Use of a union is an exception to the coding standard to ensure two mutually exclusive structure members don't appear simultaneously (wasting RAM). */
    {
        int8_t *pcReadFrom;            /*< Points to the last place that a queued item was read from when the structure is used as a queue. */
        UBaseType_t uxRecursiveCallCount;/*< Maintains a count of the number of times a recursive mutex has been recursively 'taken' when the structure is used as a mutex. */
    } u;

    List_t xTasksWaitingToSend;        /*< List of tasks that are blocked waiting to post onto this queue.  Stored in priority order. */
    List_t xTasksWaitingToReceive;    /*< List of tasks that are blocked waiting to read from this queue.  Stored in priority order. */

    volatile UBaseType_t uxMessagesWaiting;/*< The number of items currently in the queue. */
    UBaseType_t uxLength;            /*< The length of the queue defined as the number of items it will hold, not the number of bytes. */
    UBaseType_t uxItemSize;            /*< The size of each items that the queue will hold. */

    volatile int8_t cRxLock;        /*< Stores the number of items received from the queue (removed from the queue) while the queue was locked.  Set to queueUNLOCKED when the queue is not locked. */
    volatile int8_t cTxLock;        /*< Stores the number of items transmitted to the queue (added to the queue) while the queue was locked.  Set to queueUNLOCKED when the queue is not locked. */

} xQUEUE;

  ① uxMessagesWaiting:如果结构体用于消息队列,用来记录当前消息队列中消息的个数;如果用于信号量,表示有效信号量个数

    注:信号量和消息队列用的同一个结构体,因为他们机制类似

(5)常用的信号量函数

  ① 信号量创建函数

    * 二值信号量创建函数:xSemaphoreCreateBinary()

#define xSemaphoreCreateBinary() xQueueGenericCreate( ( UBaseType_t ) 1, semSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_BINARY_SEMAPHORE )

    * 计数信号量创建函数:xSemaphoreCreateCounting()

#define xSemaphoreCreateCounting( uxMaxCount, uxInitialCount ) xQueueCreateCountingSemaphore( ( uxMaxCount ), ( uxInitialCount ) )

  ②  信号量的删除函数

#define vSemaphoreDelete( xSemaphore ) vQueueDelete( ( QueueHandle_t ) ( xSemaphore ) )

  ③ 信号释放函数

    * 任务释放信号量:

#define xSemaphoreGive( xSemaphore )        xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), NULL, semGIVE_BLOCK_TIME, queueSEND_TO_BACK )

    * 中断释放信号量:

#define xSemaphoreGiveFromISR( xSemaphore, pxHigherPriorityTaskWoken )    xQueueGiveFromISR( ( QueueHandle_t ) ( xSemaphore ), ( pxHigherPriorityTaskWoken ) )

  ③ 信号量的获取函数

    * 任务获取信号量:

#define xSemaphoreTake( xSemaphore, xBlockTime )        xQueueSemaphoreTake( ( xSemaphore ), ( xBlockTime ) )

    * 中断获取信号量:

#define xSemaphoreTakeFromISR( xSemaphore, pxHigherPriorityTaskWoken )    xQueueReceiveFromISR( ( QueueHandle_t ) ( xSemaphore ), NULL, ( pxHigherPriorityTaskWoken ) )

 

5. 互斥量

(1)互斥量又称互斥信号量,是一种特殊的二值信号量,具有优先级继承机制,一般用于保护临界资源()

(2)互斥量控制块

typedef struct QueueDefinition         /* The old naming convention is used to prevent breaking kernel aware debuggers. */
{
    int8_t *pcHead;                    /*< Points to the beginning of the queue storage area. */
    int8_t *pcWriteTo;                /*< Points to the free next place in the storage area. */

    union
    {
        QueuePointers_t xQueue;        /*< Data required exclusively when this structure is used as a queue. */
        SemaphoreData_t xSemaphore; /*< Data required exclusively when this structure is used as a semaphore. */
    } u;

    List_t xTasksWaitingToSend;        /*< List of tasks that are blocked waiting to post onto this queue.  Stored in priority order. */
    List_t xTasksWaitingToReceive;    /*< List of tasks that are blocked waiting to read from this queue.  Stored in priority order. */

    volatile UBaseType_t uxMessagesWaiting;/*< The number of items currently in the queue. */
    UBaseType_t uxLength;            /*< The length of the queue defined as the number of items it will hold, not the number of bytes. */
    UBaseType_t uxItemSize;            /*< The size of each items that the queue will hold. */

    volatile int8_t cRxLock;        /*< Stores the number of items received from the queue (removed from the queue) while the queue was locked.  Set to queueUNLOCKED when the queue is not locked. */
    volatile int8_t cTxLock;        /*< Stores the number of items transmitted to the queue (added to the queue) while the queue was locked.  Set to queueUNLOCKED when the queue is not locked. */
} xQUEUE;

/* The old xQUEUE name is maintained above then typedefed to the new Queue_t
name below to enable the use of older kernel aware debuggers. */
typedef xQUEUE Queue_t;

 

(3)互斥量函数

  ① 互斥量创建函数

    * 一般互斥量创建函数

#define xSemaphoreCreateMutex() xQueueCreateMutex( queueQUEUE_TYPE_MUTEX )

    * 递归互斥量创建函数

#define xSemaphoreCreateRecursiveMutex() xQueueCreateMutex( queueQUEUE_TYPE_RECURSIVE_MUTEX )

  ② 互斥量删除函数

#define vSemaphoreDelete( xSemaphore ) vQueueDelete( ( QueueHandle_t ) ( xSemaphore ) )

  ③ 互斥量获取函数

    * 一般互斥量获取函数

#define xSemaphoreTake( xSemaphore, xBlockTime )        xQueueSemaphoreTake( ( xSemaphore ), ( xBlockTime ) )

    * 递归互斥量获取函数

#define xSemaphoreTakeRecursive( xMutex, xBlockTime )    xQueueTakeMutexRecursive( ( xMutex ), ( xBlockTime ) )

  ④ 互斥量释放函数

    * 一般互斥量释放函数

#define xSemaphoreGive( xSemaphore )        xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), NULL, semGIVE_BLOCK_TIME, queueSEND_TO_BACK )

    * 递归互斥量释放函数

#define xSemaphoreGiveRecursive( xMutex )    xQueueGiveMutexRecursive( ( xMutex ) )

 (4)互斥量与二值信号量最大的不同是互斥量具有优先级继承机制,而信号量没有

  ① 优先级翻转:当一个低优先级任务持有互斥量,而此时一个高优先级任务获取互斥量而进入阻塞,这种高优先级需要的等低优先级先运行的现象叫做优先级翻转

  ② 优先级继承机制:当一个低优先级任务持有互斥量,而此时一个高优先级任务获取互斥量而进入阻塞,此时系统会把持有互斥量的任务的优先级提升至那个等待获取互斥量任务的同一等级

 

 

6. 事件

(1)事件是一种任务间通信的制剂,主要用于实现多任务间的同步,但事件通信只能是事件类型的通信,无数据传输

(2)事件控制块

typedef struct EventGroupDef_t
{
    EventBits_t uxEventBits;
    List_t xTasksWaitingForBits;        /*< List of tasks waiting for a bit to be set. */
} EventGroup_t;

  ① uxEventBits:用于存储事件标志组,configUSE_16_BIT_TICKS定义为0,其为32位,用24位存储事件

  ② xTasksWaitingForBits:事件等待列表

(3)事件相关函数

  ① 事件创建函数:xEventGroupCreate()

  ② 事件删除函数:vEventGroupDelete()

  ③ 事件组置位函数 :xEventGroupSetBits ()和xEventGroupSetBitsFromISR()

  ④ 等待事件函数:xEventGroupWaitBits()

  ⑤ 清除事件组指定位函数:xEventGroupClearBits()和xEventGroupClearBitsFromISR()


7. 软件定时器

 (1)

 

8. 任务通知

(1)

 

9. 内存管理

 

10. 中断管理

 

标签:const,入门,FreeRTOS,void,list,queue,任务,实时操作系统,define
From: https://www.cnblogs.com/wulei0630/p/16388337.html

相关文章

  • vs code c++入门
    新建工程命令行创建vscode提供了命令行打开工作目录的功能mkdirplaygroundcdplaygroudcode.UI打开直接使用File->openFoldercommand模式使用F1快捷键可以打开......
  • hadoop入门-运行环境搭建
    Hadoop运行环境搭建(开发重点)2.1模板虚拟机环境准备0)安装模板虚拟机,IP地址192.168.10.100、主机名称hadoop100、内存4G、硬盘50G1)hadoop100虚拟机配置要求如下(本文Linux......
  • hadoop入门-概述
     第1章Hadoop概述1.1Hadoop是什么1.2Hadoop发展历史(了解) 1.3Hadoop三大发行版本(了解)Hadoop三大发行版本:Apache、Cloudera、Hortonworks。Apache版本最原始......
  • XAF新手入门 - 模块(Module)
    模块概述谈到模块大家应该都不会感到陌生,不管是前端还是后端都有模块的概念,XAF中的模块概念与大多数框架中的模块概念是相通的。XAF模块首先是一个.NET类库,同时它还包含一......
  • Java零基础入门---JDK的安装及环境变量配置教程
    ?在官方网站?(http://www.oracle.com)下载JDK安装包,针对不同的操作系统,下载不同的版本。?1、双击JDK安装包,进行安装?2、点击“下一步”?3、更改安......
  • JDBC概念和JDBC快速入门
     JDBC概念概念:javaDataBaseConnectivityjava数据连接java语法操作数据库 JDBC本质其实是官方(sun公司)定义的一套操作所有关系系数据库的规则即接口各个数据库......
  • 【一】ERNIE:飞桨开源开发套件,入门学习,看看行业顶尖持续学习语义理解框架,如何取得世界
    ​参考文章:深度剖析知识增强语义表示模型——ERNIE_财神Childe的博客-CSDN博客_ernie模型ERNIE_ERNIE开源开发套件_飞桨https://github.com/PaddlePaddle/ERNIE/blob/d......
  • java多线程编程详细入门教程
    ##1、概念?线程是jvm调度的最小单元,也叫做轻量级进程,进程是由线程组成,线程拥有私有的程序技术器以及栈,并且能够访问堆中的共享资源。这里提出一个问题,为什么要用多......
  • .MD语法入门,教你写好readme文档
    .md即markdown文件的基本常用编写语法,是一种快速标记、快速排版语言,现在很多前段项目中的说明文件readme等都是用.md文件编写的,而且很多企业也在在鼓励使用这种编辑方式,特......
  • C# RulesEngine 规则引擎:从入门到看懵
    说明RulesEngine是C#写的一个规则引擎类库,读者可以从这些地方了解它:仓库地址:https://github.com/microsoft/RulesEngine使用方法:https://microsoft.github.io/Rule......