STM32F1+HAL库+FreeTOTS学习8——第一个任务,启动!

上一期我们学习了列表和列表项的相关内容和API函数实验,接下来我们来学习FreeRTOS是如何启动第一个任务开启任务调度的,以及期间发生了什么

开启任务调度器

1. 函数 vTaskStartScheduler()

void freertos_demo(void)
{
	taskENTER_CRITICAL();           /* 进入临界区,关闭中断,此时停止任务调度*/
    /* 创建任务1 */
    xTaskCreate((TaskFunction_t )task1,
                (const char*    )"task1",
                (uint16_t       )TASK1_STK_SIZE,
                (void*          )NULL,
                (UBaseType_t    )TASK1_PRIO,
                (TaskHandle_t*  )&Task1Task_Handler);
    /* 创建任务2 */
    xTaskCreate((TaskFunction_t )task2,
                (const char*    )"task2",
                (uint16_t       )TASK2_STK_SIZE,
                (void*          )NULL,
                (UBaseType_t    )TASK2_PRIO,
                (TaskHandle_t*  )&Task2Task_Handler);
    taskEXIT_CRITICAL();            /* 退出临界区,重新开启中断,开启任务调度 */

/*这里是开启任务调度*/
    vTaskStartScheduler();		//开启任务调度
}

在前面,我们已经使用过函数 vTaskStartScheduler(),作用就是开启FreeRTOS的任务调度,下面我们来具体的看一下内部实现:

/*开启任务调度器函数*/
void vTaskStartScheduler( void )
{
    BaseType_t xReturn;
	// 1. 创建空闲函数
    /* 如果使用的是静态内存管理,则使用静态的方式创建空闲函数 */
    #if ( configSUPPORT_STATIC_ALLOCATION == 1 )
        {
            StaticTask_t * pxIdleTaskTCBBuffer = NULL;
            StackType_t * pxIdleTaskStackBuffer = NULL;
            uint32_t ulIdleTaskStackSize;

            /* 空闲任务的创建是使用用户提供的RAM,获取到相应的地址后才会进行创建。*/
            vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize );
            xIdleTaskHandle = xTaskCreateStatic( prvIdleTask,
                                                 configIDLE_TASK_NAME,
                                                 ulIdleTaskStackSize,
                                                 ( void * ) NULL,       /*强制类型转换对于所有编译器都是多余的. */
                                                 portPRIVILEGE_BIT,     /* 实际上这里应该是是 ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), 但是 tskIDLE_PRIORITY 为0. */
                                                 pxIdleTaskStackBuffer,
                                                 pxIdleTaskTCBBuffer ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */
			/*创建成功*/
            if( xIdleTaskHandle != NULL )
            {
                xReturn = pdPASS;
            }
            /*创建不成功*/
            else
            {
                xReturn = pdFAIL;
            }
        }
        /* 否则使用动态的方式,创建空闲函数 */
    #else /* if ( configSUPPORT_STATIC_ALLOCATION == 1 ) */
        {
            /* 空闲函数动态分配RAM空间. */
            xReturn = xTaskCreate( prvIdleTask,
                                   configIDLE_TASK_NAME,
                                   configMINIMAL_STACK_SIZE,
                                   ( void * ) NULL,
                                   portPRIVILEGE_BIT,  /* In effect ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), but tskIDLE_PRIORITY is zero. */
                                   &xIdleTaskHandle ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */
        }
    #endif /* 结束空闲任务创建*/
   // 2. 创建软件定时器任务 
	/*如果使能软件定时器,则需要创建定时器服务任务*/
    #if ( configUSE_TIMERS == 1 )
        {
            if( xReturn == pdPASS )
            {	
            	/*函数内部会完成定时器服务任务的创建,创建方式参照空闲任务*/
                xReturn = xTimerCreateTimerTask();
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
    #endif /* 结束软件定时器配置*/

    if( xReturn == pdPASS )
    {
        /* 此函数用于添加一些附加初始化,不用理会*/
        #ifdef FREERTOS_TASKS_C_ADDITIONS_INIT
            {
                freertos_tasks_c_additions_init();
            }
        #endif
        
	// 3、关闭中断,防止调度器开启之前或过程中,受中断干扰,会在运行第一个任务时打开中断
        portDISABLE_INTERRUPTS();
        
		/* Newlib 相关的一些东西,这里我也看不懂 */
        #if ( configUSE_NEWLIB_REENTRANT == 1 )
            {
                /* Switch Newlib's _impure_ptr variable to point to the _reent
                 * structure specific to the task that will run first.
                 * See the third party link http://www.nadler.com/embedded/newlibAndFreeRTOS.html
                 * for additional information. */
                _impure_ptr = &( pxCurrentTCB->xNewLib_reent );
            }
        #endif /* configUSE_NEWLIB_REENTRANT */
	
	// 4. 初始化一些全局变量
        xNextTaskUnblockTime = portMAX_DELAY; 					// 下一个距离取消任务阻塞的时间,设置为最大,因为此时还没有运行任务
        														// 避免阻塞超时导致任务切换
        xSchedulerRunning = pdTRUE;				//设置任务调度标志为开启
        xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;	//系统节拍计数器,初始化为0

    // 5. 为任务运行时间统计功能初始化功能时基定时器,是否使用该功能可在 FreeRTOSConfig.h 文件中进行配置
        portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();
		/* 调试使用 */
        traceTASK_SWITCHED_IN();

        /* xPortStartScheduler() 设置用于系统时钟节拍的硬件定时器(SysTick) 会在这个函数中进入第一个任务,并开始任务调度
 		* 任务调度开启后,便不会再返回 */
        if( xPortStartScheduler() != pdFALSE )
        {
            /* 代码不会运行到这里 */
        }
        else
        {
            /* 当调用关闭任务调度器函数 xTaskEndScheduler()时会运行到这里. */
        }
    }
    else
    {
        /*动态方式创建空闲任务和定时器服务任务时,堆栈空间不足,会导致无法创建,进入这里 */
        configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
    }

    /* 防止编译器警告,不用管*/
    ( void ) xIdleTaskHandle;

    /* 调试使用,不用管*/
    ( void ) uxTopUsedPriority;
}

上述代码来自FreeRTOS官方提供源码,为了跟方便看懂,去除了大部分的英文注释,该为中文注释,更适合中国宝宝体质!!!

结合上述的注释,我们可以大概明白 vTaskStartScheduler() 完成了如下内容:

  1. 创建空闲任务
  2. 创建软件定时器任务
  3. 关闭中断(确切的说是关闭FreeRTOS能够控制的中断),防止调度器开启之前或过程中,受中断干扰,会在运行第一个任务时打开中断
  4. 初始化全局变量,并将任务调度器的运行标志设置为已运行
  5. 初始化任务运行时间统计功能的时基定时器,任务运行时间统计功能需要一个硬件定时器提供高精度的计数,这个硬件定时器就在这里进行配置,如果配置不启用任务运行时间统计功能的,就无需进行这项硬件定时器的配置。
  6. 最后就是调用函数 xPortStartScheduler(),由于里面的内容比较多,所以我们下面再起一部分讲解。

需要注意以下几点:

  1. 如果使用的是静态创建任务的方式,则空闲任务和定时器任务需要用户自行定义任务堆栈和TCB(任务控制块)
  2. 软件定时器人的优先级为31(最高),空闲任务的优先级为0(最低)

2. 函数xPortStartScheduler()

函数 xPortStartScheduler()完成启动任务调度器中与硬件架构相关的配置部分,以及启动第一个任务,具体的代码如下所示:

BaseType_t xPortStartScheduler( void )
{
 /*1. 检测用户在 FreeRTOSConfig.h 文件中对中断相关部分的配置是否有误*/
    #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;

            /* 确定可以从哪个最高优先级调用ISR安全的FreeRTOS API函数 
            ISR安全函数是以“FromISR”结尾的函数。FreeRTOS维护了单独的线程和ISR API函数,以确保中断进入尽可能快速和简单。
            保存即将被覆盖的中断优先级值。 */
            ulOriginalPriority = *pucFirstUserPriorityRegister;

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

            /* Read the value back to see how many bits stuck. */
            ucMaxPriorityValue = *pucFirstUserPriorityRegister;

            /* The kernel interrupt priority should be set to the lowest
             * priority. */
            configASSERT( ucMaxPriorityValue == ( configKERNEL_INTERRUPT_PRIORITY & ucMaxPriorityValue ) );

            /* 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;
            }

            #ifdef __NVIC_PRIO_BITS
                {
                    /* Check the CMSIS configuration that defines the number of
                     * priority bits matches the number of priority bits actually queried
                     * from the hardware. */
                    configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == __NVIC_PRIO_BITS );
                }
            #endif

            #ifdef configPRIO_BITS
                {
                    /* Check the FreeRTOS configuration that defines the number of
                     * priority bits matches the number of priority bits actually queried
                     * from the hardware. */
                    configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == configPRIO_BITS );
                }
            #endif

            /* 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 /* configASSERT_DEFINED */
    
/* 2. 设置 PendSV 和 SysTick 的中断优先级为最低优先级  */

    portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;

    portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;

 /* 3. 开启并配置SYSTick,设置系统节拍、时钟源、然后开启SysTick和中断 */
    vPortSetupTimerInterrupt();

 /* 4. 初始化临界区嵌套计数器为 0 */
    uxCriticalNesting = 0;

/* 5. 开启FPU,但是在Crotex_M3的内核里面,没有FPU,所以这里没有相关代码*/


/* 6. 开启第一个任务:第一个任务,启动! */
    prvStartFirstTask();

    /* Should not get here! */
    return 0;
}

上述代码来自FreeRTOS官方提供源码,为了跟方便看懂,去除了大部分的英文注释,该为中文注释,更适合中国宝宝体质!!!

结合上述的注释,我们可以大概明白 xPortStartScheduler() 完成了如下内容:

  1. 在启用断言的情况下,函数 xPortStartScheduler()会检测用户在 FreeRTOSConfig.h 文件中对中断的相关配置是否有误
  2. 配置 PendSV 和 SysTick 的中断优先级为最低优先级
  3. 调用函数 vPortSetupTimerInterrupt()配置 SysTick,函数 vPortSetupTimerInterrupt()首先会将 SysTick 当 前 计 数 值 清 空 , 并 根 据 FreeRTOSConfig.h 文件中配置的configSYSTICK_CLOCK_HZ(SysTick 时钟源频率)和 configTICK_RATE_HZ(系统时钟节拍频率)计算并设置 SysTick 的重装载值,然后启动 SysTick 计数和中断。
  4. 初始化临界区嵌套计数器,将其置为0
  5. 调用函数 prvEnableVFP()使能 FPU,因为 ARM Cortex-M3 内核 MCU 无 FPU,此函数仅在 ARM Cortex-M4/M7 内核 MCU 平台上被调用,执行改函数后 FPU 被开启。(这里因为我们使用的是STM32F1系列,使用的Crotex_M3内核,所以没有FPU)
  6. 调用prvStartFirstTask() 函数,第一个任务启动!

启动第一个任务

1. 函数 prvStartFirstTask()

prvStartFirstTask() 函数的调用,标志着我们正式迈步FreeRTOS的领域,该函数用于初始化启动第一个任务的环境:重新设置MSP指针和使能全局中断,最后使用SVC指令,触发SVC中断。

我们来看一下是如何实现的:

__asm void prvStartFirstTask( void )
{
 /* 8 字节对齐 */
 PRESERVE8
 
 ldr r0, =0xE000ED08 /* 0xE000ED08 为 VTOR 地址 */
 ldr r0, [ r0 ] /* 获取 VTOR 的值 */
 ldr r0, [ r0 ] /* 获取 MSP 的初始值 */
 
 /* 初始化 MSP */
 msr msp, r0
 
 /* 使能全局中断 */
 cpsie i
 cpsie f
 dsb
 isb
 
 /* 调用 SVC 启动第一个任务 */
 svc 0
 nop
 nop
}

咋一看,这一段代码有点让人看不懂,因为是汇编,但是请你放心,它一点都不简单,我们这里先来补充几个重要的知识:

  1. 0xE000ED08 是什么东西?

事实上,0xE000ED08 是向量表偏移量寄存器的地址,“ ldr r0, =0xE000ED08 ” 这一步的目的就是将向量表偏移量寄存器的地址读取到" r0 "寄存器,紧接着 “ ldr r0, [ r0 ] ” 则是获取向量表偏移量寄存器中所指向的内容,也就是我们的中断向量表,但是我们知道,中断向量表,里面存放着各个中断服务函数的地址,“ ldr r0, [ r0 ] ” 就是获取第一个中断服务函数的地址 ,查阅start_stm32xxxxxx.s文件,就可以知道,这里的目的就是获取MSP指针的初始值。

在这里插入图片描述
2. MSP指针是干嘛的?

在这里插入图片描述
Cortex‐M3 处理器拥有 R0‐R15 的寄存器组。其中 R13 作为堆栈指针 SP。SP 有两个,但在同一时刻只能有一个可以看到,这也就是所谓的“banked”寄存器。

程序在运行过程中需要一定的栈空间来保存局部变量等一些信息。对应任务的当有信息保存到栈中时,MCU 会自动更新 SP 指针,ARM Cortex-M 内核提供了两个栈空间,

  • 主堆栈指针(MSP):复位后默认使用的堆栈指针,用于操作系统内核以及异常处理例程(包
    括中断服务例程)
  • 进程堆栈指针(PSP):由用户的应用程序代码使用。

看到这里,相信你就一目了然了,复位后默认使用的是MSP指针,所以我们先需要查找中断向量表获取MSP指针初始值,将其赋值给msp寄存器,让MSP指针回到原点,紧接着,开启全局中断,进入SVC服务函数,由操作系统进行接管。
在这里插入图片描述

在这里插入图片描述

我们来总结一下prvStartFirstTask() 里面到底干了什么:

  1. 首先是使用了 PRESERVE8,进行 8 字节对齐,这是因为,栈在任何时候都是需要 4 字节对齐的,而在调用入口得 8 字节对齐,在进行 C 编程的时候,编译器会自动完成的对齐的操作,而对于汇编,就需要开发者手动进行对齐。
  2. 了获得 MSP 指针的初始值
  3. 对MSP指针进行初始化,这个操作相当于丢弃了程序之前保存在栈中的数据,因为FreeRTOS从开启任务调度器到启动第一个任务都是不会返回的,是一条不归路,因此将栈中的数据丢弃,也不会有影响。
  4. 使能全局中断,因为之前关闭了FreeRTOS管理的中断
  5. 最后使用 SVC 指令,并传入系统调用号 0,触发 SVC 中断。

2. 函数 vPortSVCHandler()

__asm void vPortSVCHandler( void )
{
 /* 8 字节对齐 */
 PRESERVE8
 
 /* 获取任务栈地址 */
 ldr r3, = pxCurrentTCB /* r3 指向优先级最高的就绪态任务的任务控制块 */
 ldr r1, [ r3 ] /* r1 为任务控制块地址 */
 ldr r0, [ r1 ] /* r0 为任务控制块的第一个元素(栈顶) */
 
 /* 模拟出栈,并设置 PSP */
 ldmia r0 !, { r4 - r11 } /* 任务栈r0地址从低到高,将r0存储地址里面的内容手动加载到 CPU寄存器r4到r11 */
 msr psp, r0 /* 设置 PSP 为任务栈指针 */
 isb
 
 /* 使能所有中断 */
 mov r0, # 0
 msr basepri,
 
 /* 使用 PSP 指针,并跳转到任务函数 */
 orr r14, # 0xd
 bx r14
}

同样的我们来分析一些,这个函数里做了什么:

  1. 同样进行了字节对齐,因为这里是汇编的世界!
  2. 获取任务栈的地址:pxCurrentTCB 是一个全局变量,用于指向系统中优先级最高的就绪态任务的任务控制块,之前我们创建了两个任务,一个空闲任务,优先级为0,一个软件定时器服务函数,优先级为31(这部没有包含用户自己创建的任务), 这里pxCurrentTCB 是软件定时器任务的任务控制块。那么对应的就是获取软件定时器任务的栈顶地址
  3. 将软件定时器任务栈的内容出栈到CPU寄存器组内,如何设置PSP指针。
  4. 使能所有中断
  5. 对CPU寄存器里面的r14(连接寄存器)或上0x0d ,使得r14的值为 0xFFFFFFFD

R14 是链接寄存器 LR,在 ISR 中(此刻我们在 SVC 的 ISR 中),它记录了异常返回值 EXC_RETURN,而EXC_RETURN 只有 6 个合法的值(M4、M7),如下表所示:

描述使用浮点单元(M4、M7的内核)未使用浮点单元(M3的内核)
中断返回后进入Hamdler模式,并使用MSP0xFFFFFFE10xFFFFFFF1
中断返回后进入线程模式,并使用 MSP0xFFFFFFE90xFFFFFFF9
中断返回后进入线程模式,并使用 PSP0xFFFFFFED0xFFFFFFFD

经过以上步骤,最终进入线程模式,使用PSP指针,开始运行第一个任务,软件定时器任务,至此第一个任务正式启动!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不想写代码的我

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值