首页 > 其他分享 >FreeRTOS启动任务调度器函数解释

FreeRTOS启动任务调度器函数解释

时间:2024-08-08 22:20:28浏览次数:10  
标签:函数 FreeRTOS 中断 DCD 任务 Handler 任务调度 指针

目录

FreeRTOS的任务开始运行的前提是调用了启动调度器函数 vTaskStartScheduler() ,只有调用了该函数任务才会被调度并运行。下面以FreeRTOS v9.0.0版本的源码进行分析FreeRTOS任务调度的启动流程。

vTaskStartScheduler() 函数

void vTaskStartScheduler(void)
{
    BaseType_t xReturn;

/* 静态方法创建空闲任务 */
#if (configSUPPORT_STATIC_ALLOCATION == 1)
    {
        StaticTask_t *pxIdleTaskTCBBuffer = NULL;
        StackType_t *pxIdleTaskStackBuffer = NULL;
        uint32_t ulIdleTaskStackSize;

        /* 以静态方式创建任务用户需自定义空闲任务的内存分配函数 */
        vApplicationGetIdleTaskMemory(&pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize);
        xIdleTaskHandle = xTaskCreateStatic(prvIdleTask,
                                            "IDLE",
                                            ulIdleTaskStackSize,
                                            (void *)NULL,
                                            (tskIDLE_PRIORITY | portPRIVILEGE_BIT),
                                            pxIdleTaskStackBuffer,
                                            pxIdleTaskTCBBuffer);

        if (xIdleTaskHandle != NULL)
        {
            xReturn = pdPASS;
        }
        else
        {
            xReturn = pdFAIL;
        }
    }
#else
    { /* 动态方法创建空闲任务 */
        xReturn = xTaskCreate(prvIdleTask,
                              "IDLE", configMINIMAL_STACK_SIZE,
                              (void *)NULL,
                              (tskIDLE_PRIORITY | portPRIVILEGE_BIT),
                              &xIdleTaskHandle);
    }
#endif /* configSUPPORT_STATIC_ALLOCATION */
/* 如果启用软件定时器 */
#if (configUSE_TIMERS == 1)
    {
        if (xReturn == pdPASS)
        {
            /* 创建空闲任务成功,且启用软件定时器,就创建定时器任务 */
            xReturn = xTimerCreateTimerTask();
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }
    }
#endif /* configUSE_TIMERS */
    /* 任务创建成功或定时器任务创建成功(如果使能定时器任务创建的话)*/
    if (xReturn == pdPASS)
    {
        /* 关中断,确保开启调度器之前或过程中,SysTick 不会产生中断
           在第一个任务开始运行时,会重新打开中断 */
        portDISABLE_INTERRUPTS();

#if (configUSE_NEWLIB_REENTRANT == 1)
        {
            _impure_ptr = &(pxCurrentTCB->xNewLib_reent);
        }
#endif /* configUSE_NEWLIB_REENTRANT */
        /* 设置下一个任务的解锁时间为最大,这样可以避免在启动调度器之前不会因为任务解锁而引起任务调度 */
        xNextTaskUnblockTime = portMAX_DELAY;
        /* 置 xSchedulerRunning 标志为真,这指示这调度器即将进行运行 */
        xSchedulerRunning = pdTRUE;
        /* 将计数值初始化为0,确保在启动调度器时开始计数,使所有任务的时钟节拍一致 */
        xTickCount = (TickType_t)0U;

        /* 如果定义了configGENERATE_RUN_TIME_STATS,
        则必须定义以下宏来配置用于生成运行时计数器时基的定时器/计数器。
        运行时间统计功能 */
        portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();

        /* 用于完成启动任务调度器中与硬件架构相关的配置部分,以及启动第一个任务
        调用这个函数以后,就不会再回来了*/
        if (xPortStartScheduler() != pdFALSE)
        {
            /* 调度器正在运行函数不会返回到这里执行 */
        }
        else
        {
            /* 只有当任务调用 xTaskEndScheduler() 时才会到达这里
            如果调度器没有成功启动,或者某个任务调用了 xTaskEndScheduler() 函数以结束调度器的运行,
            则程序将会执行到这里。因此,在这里放置的代码可能会处理一些特殊情况或进行清理工作 */
        }
    }
    /* 可能是因为没有足够的堆空间来创建空闲任务或定时器任务
    通过断言来检查内核是否能成功分配所需的内存 */
    else
    {
        configASSERT(xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY);
    }

    /* 防止编译器警告 */
    (void)xIdleTaskHandle;
}

可以看到该函数主要做了几件事。

1.创建空闲任务,根据配置以不同方式创建空闲任务,静态或者动态方式。

2.如果启动了软件定时器功能就创建软件定时器任务,根据配置以静态还是动态方式进行创建软件定时器任务。

3.关闭中断,使用 portDISABLE_INTERRUPTS() 关闭中断,这种方式只会关闭受FreeRTOS所管理的中断,主要是为了防止Systick中断在任务调度器开启之前或过程中产生中断,FreeRTOS会在开始运行第一个任务时重新打开中断。

4.初始化一些全局变量,并将调度器标记为正在运行。

5.初始化任务运行时间统计功能的时基定时器,任务运行时间统计功能需要一个硬件定时器提供高精度的计数,这个硬件定时器就在这里进行配置,如果配置不启用任务运行时间统计功能的,就无需进行这项硬件定时器的配置。

6.调用 xPortStartScheduler() 启动调度器。

xPortStartScheduler() 函数

/* 代码确定了可以在中断服务例程中调用的 FreeRTOS API 函数的最高优先级。
这样可以确保在中断上下文中仅调用安全的API函数,从而保持中断处理的效率和可靠性。*/
BaseType_t xPortStartScheduler(void)
{
#if (configASSERT_DEFINED == 1)
    {
        volatile uint32_t ulOriginalPriority; // 用于保存即将被覆盖的中断优先级值。
        /* 指向第一个用户中断的优先级寄存器地址。这个寄存器存储了中断的优先级值 */
        volatile uint8_t *const pucFirstUserPriorityRegister = (uint8_t *)(portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER);
        /* 用于存储计算得到的最大优先级值 */
        volatile uint8_t ucMaxPriorityValue;

        /* 确定FreeRTOS ISR安全的API函数可以调用的最大优先级。ISR安全函数是以“FromISR”结尾的。
        保存即将被覆盖的中断优先级值 */
        ulOriginalPriority = *pucFirstUserPriorityRegister;

        /* 确定可用的优先级位的数量。首先写入所有可能的位 */
        *pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;

        ucMaxPriorityValue = *pucFirstUserPriorityRegister;

        /* Use the same mask on the maximum system call priority.
        对最大系统调用优先级使用相同的掩码。*/
        ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;

        /* Calculate the maximum acceptable priority group value for the number
        of bits read back.*/
        ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;
      
        while ((ucMaxPriorityValue & portTOP_BIT_OF_BYTE) == portTOP_BIT_OF_BYTE)
        {
            ulMaxPRIGROUPValue--;
            ucMaxPriorityValue <<= (uint8_t)0x01;
        }

        /* Shift the priority group value back to its position within the AIRCR
        register.*/
        ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
        ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;

        /* Restore the clobbered interrupt priority register to its original
        value. */
        *pucFirstUserPriorityRegister = ulOriginalPriority;
    }
#endif /* conifgASSERT_DEFINED */

    /* Make PendSV and SysTick the lowest priority interrupts.
    使PendSV和SysTick为最低优先级中断 */

    /* 0xE000ED20 就是SHPR3寄存器的地址,用于配置 PendSV(可悬挂请求)、SysTick 的中断优先级*/
    portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;

    portNVIC_SYSPRI2_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;

    /* 启动第一个任务 */
    prvStartFirstTask();

    /* Should not get here! */
    /* 不应该到达这里 */
    return 0;
}

1.配置 PendSV 和 SysTick 的中断优先级为最低优先级。

2.调用函数 vPortSetupTimerInterrupt()配置 SysTick,该首先会将 SysTick 当前计数值清空,并根据 FreeRTOSConfig.h 文件中配置的configSYSTICK_CLOCK_HZ(SysTick 时钟源频率)和 configTICK_RATE_HZ(系统时钟节拍频率)计算并设置 SysTick 的重装载值,然后启动 SysTick 计数和中断。

3.初始化临界区嵌套计数器为 0。

4.调用函数 prvStartFirstTask() 启动第一个任务。

prvStartFirstTask() 函数

函数 prvStartFirstTask() 用于初始化启动第一个任务前的环境,主要是重新设置 MSP (主堆栈指针)指针,并使能全局中断。

/* 初始化启动第一个任务前的环境,主要是重新设置主堆栈MSP指针,并使能全局中断 */
__asm void prvStartFirstTask( void )
{
	/* 8 字节对齐 */
	PRESERVE8

	/* Use the NVIC offset register to locate the stack. */
	ldr r0, =0xE000ED08	/* 0xE000ED08 为 VTOR 地址 */
	ldr r0, [r0]		/* 获取 VTOR 的值 */
	ldr r0, [r0]		/* 获取 MSP 的初始值 */

	/* Set the msp back to the start of the stack. 
	 初始化 MSP */
	msr msp, r0
	/* Globally enable interrupts. */
	/* 使能全局中断 */
	cpsie i
	cpsie f
	dsb
	isb
	/* Call SVC to start the first task. */
	/* 调用 SVC 启动第一个任务 */
	svc 0
	nop
	nop
}

1.首先是使用了 PRESERVE8,进行 8 字节对齐,这是因为,栈在任何时候都是需要 4 字节对齐的,而在调用入口得 8 字节对齐,在进行 C 编程的时候,编译器会自动完成的对齐的操作,而对于汇编,就需要开发者手动进行对齐。

2.接下来的三行代码是为了获得 MSP 指针的初始值。

什么是 MSP 指针?

程序在运行过程中需要一定的栈空间来保存局部变量等一些信息。当有信息保存到栈中时,MCU 会自动更新 SP 指针,使 SP 指针指向最后一个入栈的元素,那么程序就可以根据 SP 指针来从栈中存取信息。ARM Cortex-M 提供了两个栈空间,这两个栈空间的堆栈指针分别是 MSP(主堆栈指针)和 PSP(进程堆栈指针)。

这两个栈指针在任一时刻只能使用其中一个。

主堆栈指针(MSP):复位后缺省使用的堆栈指针,用于操作系统内核及异常处理例程(中断服务例程使用的永远都是MSP)。

进程堆栈指针(PSP):由用户的应用程序代码使用。

在FreeRTOS 中 MSP 是给系统栈空间使用的,而 PSP 是给任务栈使用的,也就是说,FreeRTOS 任务的栈空间是通过 PSP 指向的,而在进入中断服务函数时,则是使用 MSP 指针。

为什么是 0xE000ED08?

0xE000ED08 是 VTOR(向量表偏移寄存器)的地址,VTOR 中保存了向量表的偏移地址。一般来说向量表是从起始地址 0x00000000 开始的,但是在有些情况下,可能需要修改或重定向向量表的首地址,因此 ARM Corten-M 提供了 VTOR 寄存器对向量表进行重定向。

向量表是用来保存中断异常的入口函数地址,即栈顶地址的,并且向量表中的第一个字保存的就是栈顶的地址,在 start_stm32xxxxxx.s 文件中有如下定义:

__Vectors       DCD     __initial_sp               ; Top of Stack
                DCD     Reset_Handler              ; Reset Handler
                DCD     NMI_Handler                ; NMI Handler
                DCD     HardFault_Handler          ; Hard Fault Handler
                DCD     MemManage_Handler          ; MPU Fault Handler
                DCD     BusFault_Handler           ; Bus Fault Handler
                DCD     UsageFault_Handler         ; Usage Fault Handler
                DCD     0                          ; Reserved
                DCD     0                          ; Reserved
                DCD     0                          ; Reserved
                DCD     0                          ; Reserved
                DCD     SVC_Handler                ; SVCall Handler
                DCD     DebugMon_Handler           ; Debug Monitor Handler
                DCD     0                          ; Reserved
                DCD     PendSV_Handler             ; PendSV Handler
                DCD     SysTick_Handler            ; SysTick Handler

以上就是向量表的部分内容,可以看到向量表的第一个元素就是栈指针的初始值,也就是栈顶指针。所以 prvStartFirstTask() 该函数首先是获取 VTOR 的地址,接着获取VTOR 的值,也就是获取向量表的首地址,最后获取向量表中第一个字的数据,也就是栈顶指针。

3.在获取了栈顶指针后,将 MSP 指针重新赋值为栈顶指针。这个操作相当于丢弃了程序之前保存在栈中的数据,因为FreeRTOS从开启任务调度器到启动第一个任务都是不会返回的,是一条不归路,因此将栈中的数据丢弃,也不会有影响。

4.重新赋值 MSP 后,接下来就重新使能全局中断,因为之前在函数 vTaskStartScheduler()中关闭了受 FreeRTOS 管理的中断。

5.最后使用 SVC 指令,并传入系统调用号 0,触发 SVC 中断。

vPortSVCHandler() 函数

当使能了全局中断,并且手动触发 SVC 中断后,就会进入到 SVC 的中断服务函数中。

__asm void vPortSVCHandler( void )
{
	PRESERVE8
	/* 获取任务栈地址 */
	ldr	r3, =pxCurrentTCB	/* 恢复上下文, r3 指向优先级最高的就绪态任务的任务控制块 */
	ldr r1, [r3]			/* 使用pxCurrentTCBConst获取pxCurrentTCB地址. r1 为任务控制块地址*/
	ldr r0, [r1]			/* pxCurrentTCB中的第一项是任务栈的顶部。r0 为任务控制块的第一个元素(栈顶) */
	
	/* 模拟出栈,并设置 PSP,任务栈弹出到 CPU 寄存器 */
	ldmia r0!, {r4-r11}		/* Pop the registers that are not automatically saved on exception entry and the critical nesting count. */
	/* 设置 PSP 为任务栈指针 */
	msr psp, r0				/* 恢复任务栈指针 */
	isb
	/* 使能所有中断 */
	mov r0, #0
	msr	basepri, r0
	/* 使用 PSP 指针,并跳转到任务函数 */
	orr r14, #0xd
	bx r14
}

该函数用于跳转到任务函数当中。

首先通过 pxCurrentTCB 获取优先级最高的就绪态任务的任务栈地址,优先级最高的就绪态任务就是系统将要运行的任务。pxCurrentTCB 是一个全局变量,用于指向系统中优先级最高的就绪态任务的任务控制块,在前面创建 start_task 任务、空闲任务、定时器处理任务时自动根据任务的优先级高低已经进行过赋值的。

标签:函数,FreeRTOS,中断,DCD,任务,Handler,任务调度,指针
From: https://www.cnblogs.com/RAM-YAO/p/18349835

相关文章

  • C语言--函数
    函数的概述:函数:实现一定功能的,独立的代码模块。函数一定是先定义,后使用使用函数的优势:·我们可以通过函数提供功能给别人使用,也可以使用别人提供的函数,减少代码量               ·借助函数可以减少重复的代码               ·实现结构化(......
  • freertos学习笔记(十)事件标志组
    事件标志组相当于用户平时定义的Flag,事件标志,不过freertos支持将该标志组作为启动task的条件概述分为8位和24位的模式(通过设置宏来配置)每一位有0和1两个状态用法用于平常程序的标记位用于task之间的同步任务a先到达同步点,进入阻塞态设置任务a的事件标记位检查其......
  • 轮换挑选图片,补充 es6的对象写法,uniapp使用,class和style,条件渲染,列表渲染,input
    Ⅰ轮换挑选图片【一】方式一<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><title>Title</title><scriptsrc="./js2/vue.js"></script></head><body>......
  • STM32CubleMX创建FreeRtos工程教程,图文教程
        前言:STM32CubeMX是一个开发工具,它已经将FreeRTOS这个实时操作系统(RTOS)集成到其工具中。换句话说,通过STM32CubeMX,可以非常方便地为STM32微控制器生成配置代码,其中包括对FreeRTOS的支持。    而本篇就是使用STM32CubleMX,生成支持FreeRtos的图文教程......
  • 目录函数以及链接文件
    一、stat补充1、getpwuid()structpasswd*getpwuid(uid_tuid);功能:根据用户id到/etc/passwd文件下解析获得结构体信息参数:uid:用户id返回值:成功返回id对应用户的信息失败返回NULL2、getgrgid()structgroup*getgrgid(gid_tgid);拿到组的结构体功能:根据gid......
  • freertos学习(九)软件定时器
    软件定时器软件定时器是freeRTOS通过一个硬件定时器,实现的定时器。可以实现不同时长的多个定时任务不从中断上下文中执行定时器回调函数(不消耗任何处理时间)实现流程设置软件定时器,推入定时器命令队列开始计时计时到,启用回调函数如不是循环模式,则该定时器停止运行注......
  • 函数不声明也可调用
    目录一、编译试试二、说明原由编译器隐式声明三、总结四、源码一、编译试试1、main.c2、add.c3、编译执行二、说明原由编译器隐式声明gcc编译器在编译源文件时,遇到未声明的函数调用时,会根据函数调用时传入的参数类型隐式的为此源文件生成一个函数声明:函数参数......
  • FFmpeg源码:av_realloc、av_reallocp、size_mult、av_realloc_f函数分析
    =================================================================FFmpeg内存管理相关的源码分析:FFmpeg中内存分配和释放相关的源码:av_malloc函数、av_mallocz函数、av_free函数和av_freep函数分析FFmpeg源码:av_realloc、av_reallocp、size_mult、av_realloc_f函数分析FF......
  • Pandas高级操作:多级索引、窗口函数、数据透视表等
            在数据处理和分析中,pandas库提供了强大的功能,支持从简单到复杂的数据操作。本文将介绍一些pandas的高级操作,包括多级索引(MultiIndex)、窗口函数(WindowFunctions)、数据透视表与复杂聚合、数据合并与连接、高级数据变换以及时间序列数据的高级处理。1.多级索......
  • 【Python】excel常用函数操作Python实现,办公入门首选
    常见的Excel函数,在Python中的如何实现:VLOOKUP:可以使用merge或map函数来实现类似的功能。IF:可以使用numpy库的where函数来实现类似的功能。SUMIF:可以使用pandas的query函数来筛选数据,然后使用sum函数来计算总和。COUNTIF:类似于SUMIF,可以使用query函数来筛选数据,然......