在FreeRTOS中,创建完任务后需要调用vTaskStartScheduler开启调度器,在这个函数主要就是创建空闲任务然后调用xPortStartScheduler函数开启任务的调度,本篇文章就以Cortex-M7为例来分析一下这个函数具体做了什么事,并深入理解其中的断言。

函数源码如下:

BaseType_t xPortStartScheduler( void )
{
    configASSERT( configMAX_SYSCALL_INTERRUPT_PRIORITY );
	/* 该port.c不支持CM7的这两个修订版本 */
    configASSERT( portCPUID != portCORTEX_M7_r0p1_ID );
    configASSERT( portCPUID != portCORTEX_M7_r0p0_ID );
	/* 一般打开了这个宏 */
    #if ( configASSERT_DEFINED == 1 )
        {
            volatile uint32_t ulOriginalPriority;
            volatile uint8_t * const pucFirstUserPriorityRegister = ( volatile uint8_t * const ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER );
            volatile uint8_t ucMaxPriorityValue;

            ulOriginalPriority = *pucFirstUserPriorityRegister;

            *pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;

            ucMaxPriorityValue = *pucFirstUserPriorityRegister;

            ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;

            ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;

            while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE )
            {
                ulMaxPRIGROUPValue--;
                ucMaxPriorityValue <<= ( uint8_t ) 0x01;
            }

            #ifdef __NVIC_PRIO_BITS
                {
                    configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == __NVIC_PRIO_BITS );
                }
            #endif

            #ifdef configPRIO_BITS
                {
                    configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == configPRIO_BITS );
                }
            #endif

            ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
            ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;

            *pucFirstUserPriorityRegister = ulOriginalPriority;
        }
    #endif /* configASSERT_DEFINED */

    portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;
    portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;

    vPortSetupTimerInterrupt();

    uxCriticalNesting = 0;

    vPortEnableVFP();

    *( portFPCCR ) |= portASPEN_AND_LSPEN_BITS;

    prvPortStartFirstTask();
	/* 后面三行理论上不会执行到 */
    vTaskSwitchContext();
    prvTaskExitError();
    return 0;
}

首先,宏定义configMAX_SYSCALL_INTERRUPT_PRIORITY 不能为0,它用于配置系统中允许的最高中断优先级,以便实现在中断服务程序中调用 FreeRTOS API 函数时,确保对系统的保护。下面是该宏定义的原型:

#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 2
#define configPRIO_BITS 4
#define configMAX_SYSCALL_INTERRUPT_PRIORITY (configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS))

其中configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY为FreeRTOS中允许的最高优先级;而configPRIO_BITS表示处理器所支持的中断优先级位数。以CM7为例,它的configPRIO_BITS为4,在CM7内核中使用二进制0bxxxx0000来表示优先级,其中xxxx为CM7中支持的4位优先级,所以需要下面的移位操作获得最终的configMAX_SYSCALL_INTERRUPT_PRIORITY

在FreeRTOS在PendSV异常中执行上下文切换函数vTaskSwitchContext时,将设置basepri寄存器为configMAX_SYSCALL_INTERRUPT_PRIORITY ,这可以使优先级比configMAX_SYSCALL_INTERRUPT_PRIORITY低的中断被屏蔽。

常见问题 :在系统外设中断中,我们需要调用FreeRTOS的函数。如在串口中断函数中释放一个信号量来通知任务处理数据,但是程序会卡死在中断中。
答: 外设中断的优先级如果没有设置的话,默认为0,它低于configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY而不受FreeRTOS的管理,如果在该优先级的中断中调用FreeRTOS函数,有可能会导致一些非原子操作的混乱。比如系统正在执行一个受FreeRTOS管理的中断且正在对一个信号量进行操作,此时产生了一个不受FreeRTOS管理的中断,在中断中也调用FreeRTOS信号量函数,那么有可能会使得之前中断保存的上下文并不是实际的上下文,从而产生错误。故需要执行NVIC_SetPriority设置外设中断的优先级,且优先级的数值需要大于等于configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY


接下来来分析一下这一段代码:

#define portFIRST_USER_INTERRUPT_NUMBER       ( 16 )
#define portNVIC_IP_REGISTERS_OFFSET_16       ( 0xE000E3F0 )
#define portMAX_8_BIT_VALUE                   ( ( uint8_t ) 0xff )
#define portMAX_PRIGROUP_BITS                 ( ( uint8_t ) 7 )
#define portTOP_BIT_OF_BYTE                   ( ( uint8_t ) 0x80 )

volatile uint32_t ulOriginalPriority;
volatile uint8_t * const pucFirstUserPriorityRegister = ( volatile uint8_t * const ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER );
volatile uint8_t ucMaxPriorityValue;

ulOriginalPriority = *pucFirstUserPriorityRegister;
*pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;
ucMaxPriorityValue = *pucFirstUserPriorityRegister;
ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;

ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;

while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE )
{
    ulMaxPRIGROUPValue--;
    ucMaxPriorityValue <<= ( uint8_t ) 0x01;
}

portNVIC_IP_REGISTERS_OFFSET_16+16,即0xE000E400,对应Cortex-M7处理器中的NVIC_IPR0寄存器,它用于配置中断优先级。其中每个IPRx寄存器代表4个中断的8位优先级,IPR0~IPR59总共240个中断的优先级。
在这里插入图片描述

还有一个问题,为什么要定义portNVIC_IP_REGISTERS_OFFSET_16呢?通过上表可以看到在NVIC手册中并没有标明这个地址存放的是什么寄存器,但我们来看一下Cortex-M7的异常中断表:
在这里插入图片描述
由此我们可以推断出,在IPR0寄存器之前,保存的是这16个异常的优先级,加起来正好256个中断。
所以,上面的代码就是取出中断优先级为0的,即第一个用户中断优先级配置寄存器,然后往里面写0xFF来测试8位优先级是几位有效的。比如低2位没有用到,往IPR寄存器写0xFF,读出来就是0xFC。然后用这个掩码和configMAX_SYSCALL_INTERRUPT_PRIORITY按位与得到ucMaxSysCallPriority

对于一个8位优先级来说,优先级有8种分组(0~7)。所以最后计算优先级分组的最高值ulMaxPRIGROUPValue,它的值就等于(7-ucMaxPriorityValue高位1的个数)


接下来进入两个断言中:

#define portMAX_PRIGROUP_BITS                 ( ( uint8_t ) 7 )

#ifdef __NVIC_PRIO_BITS
{
	configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == __NVIC_PRIO_BITS );
}
#endif

#ifdef configPRIO_BITS
{
	configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == configPRIO_BITS );
}
#endif

这两个断言都需要计算(系统的最大优先级组位数portMAX_PRIGROUP_BITS-最大可接受的优先级组值ulMaxPRIGROUPValue),它的结果表示优先级组的位数。比如优先级组为3,表示IPR寄存器的高4位用来配置优先级。
__NVIC_PRIO_BITS是一个由处理器提供的宏定义,在我的芯片中等于4,表示硬件中可用的优先级位数,而对于configPRIO_BITS来说,它就等于__NVIC_PRIO_BITS

#ifdef __NVIC_PRIO_BITS
	/* __BVIC_PRIO_BITS will be specified when CMSIS is being used. */
	#define configPRIO_BITS __NVIC_PRIO_BITS
#else
	#define configPRIO_BITS 4 /* 15 priority levels */
#endif

为了确保正确性,需要确保这两个值相等。因为configMAX_SYSCALL_INTERRUPT_PRIORITY是由configPRIO_BITS决定的,而configMAX_SYSCALL_INTERRUPT_PRIORITY将赋值给basepri寄存器。如果设置不对,可能导致本不能调用FreeRTOS函数的某优先级的中断可以调用FreeRTOS函数,从而导致某些错误,如信号量的不统一。


由于后续还需要判断特殊寄存器AIRCR的优先级分组字段[10:8]位的正确性,这里求出AIRCR寄存器中[10:8]位的掩码以备后续使用。

#define portPRIORITY_GROUP_MASK               ( 0x07UL << 8UL )
#define portPRIGROUP_SHIFT                    ( 8UL )

ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;

/* IPR0用来测试得到内核的最大优先级的掩码和组,最后需要将其初始值恢复给IPR0 */
*pucFirstUserPriorityRegister = ulOriginalPriority;

接着,将SysTick和PendSV设置为最低优先级

/*  将PendSV和SysTick设置为最低优先级 */
portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;
portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;

这样做的目的如下:

  • SysTick:SysTick为FreeRTOS的时钟,用于任务延时和时间管理。若设置为最高优先级,则会使IRQ响应速度变慢。
  • PendSV:PendSV中断用于任务切换,不会打断正在执行的高优先级的中断,在中断结束后,再进行任务切换

总得来说,就是要保证任务的实时性,还要保证它们不会抢占其他中断或任务,避免产生不可预期行为和竞争条件。


最后就执行以下代码:

/* 配置和使能SysTick中断 */
vPortSetupTimerInterrupt();

/* Initialise the critical nesting count ready for the first task. */
uxCriticalNesting = 0;

/* Ensure the VFP is enabled - it should be anyway. */
vPortEnableVFP();

/* Lazy save always. */
*( portFPCCR ) |= portASPEN_AND_LSPEN_BITS;

/* Start the first task. */
prvPortStartFirstTask();

(1)uxCriticalNesting是临界区嵌套层数,FreeRTOS允许临界区嵌套,这个变量是为了避免在较低级别的临界区内部发生不必要的任务切换或中断处理,从而提高系统的可靠性和响应性。
(2)VFP为浮点运算单元,表示可以在任务中使用浮点数运算,并且可以调用相关的浮点库函数。使能之后,在PendSV的开始处将保存VFP寄存器高16位的浮点上下文到栈中(AAPCS规定子例程调用必须保存s16-s31,而不需要保存s0-s15),并在结束处出栈。

开启FPU后,在调用函数时,硬件还将保存FPSCR字段,加上前面要压的s16-s31,总共要多压68个字节(假设不算硬件压的s0-s15),很影响性能。可以通过将FPCCR中的LSPEN位置1来使能Lazy Context Save。这意味着只要在中断不使用FPU,就可以不用将68字节的浮点状态压入堆栈。

最后,就是调用prvPortStartFirstTask开始第一个任务的调度,这个在我之前的文章有具体地进行分析:FreeRTOS第一个任务的创建和调度详解(SVC异常)

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐