FreeRTOS源码分析二:task启动(RISCV架构)

系列文章目录

FreeRTOS源码分析一:task创建(RISCV架构)


文章目录


前言

本文继续看 task 的运行。主要解析函数 vTaskStartScheduler

主函数中调用函数 vTaskStartScheduler 开始调度。

c 复制代码
int main_blinky( void )
{
    vSendString( "Hello FreeRTOS!" );

    /* Create the queue. */
    xQueue = xQueueCreate( mainQUEUE_LENGTH, sizeof( uint32_t ) );

    if( xQueue != NULL )
    {
        /* Start the two tasks as described in the comments at the top of this
         * file. */
        xTaskCreate( prvQueueReceiveTask, "Rx", configMINIMAL_STACK_SIZE * 2U, NULL,
                     mainQUEUE_RECEIVE_TASK_PRIORITY, NULL );
        xTaskCreate( prvQueueSendTask, "Tx", configMINIMAL_STACK_SIZE * 2U, NULL,
                     mainQUEUE_SEND_TASK_PRIORITY, NULL );
    }

    vTaskStartScheduler();

    return 0;
}

vTaskStartScheduler 调度器启动函数

初始化核心调度数据结构、创建必要的后台任务(如 idle task 和 timer service task),并启动第一个任务的上下文切换,从而进入多任务运行模式。

c 复制代码
void vTaskStartScheduler( void )
{
    BaseType_t xReturn;
	
	//  创建 Idle Task(空闲任务)
    xReturn = prvCreateIdleTasks();

	// 启用了软件定时器(configUSE_TIMERS)
    #if ( configUSE_TIMERS == 1 )
    {
        if( xReturn == pdPASS )
        {
            // 创建 Timer Service Task。
            xReturn = xTimerCreateTimerTask();
        }
    }
    #endif /* configUSE_TIMERS */

    if( xReturn == pdPASS )
    {
        /* Interrupts are turned off here, to ensure a tick does not occur
         * before or during the call to xPortStartScheduler().  The stacks of
         * the created tasks contain a status word with interrupts switched on
         * so interrupts will automatically get re-enabled when the first task
         * starts to run. */
        // 关闭中断,防止 tick 提前进入,xPortStartScheduler中我们会开启中断
        portDISABLE_INTERRUPTS();

		// 设置 Tick 计数器、调度状态。
        xNextTaskUnblockTime = portMAX_DELAY;
        xSchedulerRunning = pdTRUE;
        xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;

        /* Setting up the timer tick is hardware specific and thus in the
         * portable interface. */

        // 开启调度
        ( void ) xPortStartScheduler();
        // 大部分情况下不会返回,除非内存不足
    }
    else
    {
        configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
    }
    ( void ) xIdleTaskHandles;

    ( void ) uxTopUsedPriority;
}

我们暂时先跳过空闲任务创建和时钟服务创建这两个地方,着重看 xPortStartScheduler 这个架构特定的调度器启动函数的实现。

xPortStartScheduler架构特定调度器启动函数

检查中断服务的堆栈对齐并填充字节,初始化必要的硬件设置并启动多任务调度

c 复制代码
BaseType_t xPortStartScheduler( void )
{
    /* 声明外部函数:启动第一个任务的汇编函数 */
    extern void xPortStartFirstTask( void );
    
    /* 如果启用了断言检查 */
    #if ( configASSERT_DEFINED == 1 )
    {
        /* 检查中断栈顶地址的字节对齐
         * 中断栈与调度器启动前main()函数使用的栈是同一个
         * 确保栈顶地址符合平台的字节对齐要求 */
        configASSERT( ( xISRStackTop & portBYTE_ALIGNMENT_MASK ) == 0 );
        
        /* 如果配置了中断栈大小(以字为单位) */
        #ifdef configISR_STACK_SIZE_WORDS
        {
            /* 使用特定的填充字节初始化整个中断栈内存区域
             * 这有助于调试时检测栈溢出和栈使用情况 */
            memset( ( void * ) xISRStack, portISR_STACK_FILL_BYTE, sizeof( xISRStack ) );
        }
        #endif /* configISR_STACK_SIZE_WORDS */
    }
    #endif /* configASSERT_DEFINED */
    
    /* 设置定时器中断 */
    vPortSetupTimerInterrupt();
    
    /* 如果配置了MTIME和MTIMECMP寄存器的基地址(RISC-V标准定时器) */
    #if ( ( configMTIME_BASE_ADDRESS != 0 ) && ( configMTIMECMP_BASE_ADDRESS != 0 ) )
    {
        /* 启用machine模式下的定时器中断和外部中断
         * 通过设置mie(Machine Interrupt Enable)寄存器:
         * - 位7 (0x80): 启用定时器中断
         * - 位11 (0x800): 启用外部中断
         * 0x880 = 0x80 | 0x800 */
        __asm volatile ( "csrs mie, %0" ::"r" ( 0x880 ) );
    }
    #endif /* ( configMTIME_BASE_ADDRESS != 0 ) && ( configMTIMECMP_BASE_ADDRESS != 0 ) */
    
    /* 启动第一个任务
     * 这个函数会切换到第一个就绪任务的上下文
     * 从这点开始,系统进入多任务调度模式 */
    xPortStartFirstTask();
    
    /* 正常情况下不应该执行到这里
     * 因为调用xPortStartFirstTask()后,只有任务应该在执行
     * 如果执行到这里,说明调度器启动失败 */
    return pdFAIL;
}

这里我们需要关注两个函数,一个是 vPortSetupTimerInterrupt 用于设置定时器中断。一个是 xPortStartFirstTask,启动第一个任务。

vPortSetupTimerInterrupt启动 RISCV 定时器中断

简单来说,只需要读取 mtime 得到当前系统运行时间,并加上余量,设置 mtimecmp 即可在 mtime > mtimecmp 时触发始终中断。

实际实现也是这样。

c 复制代码
void vPortSetupTimerInterrupt( void )
{
    uint32_t ulCurrentTimeHigh, ulCurrentTimeLow;
    /* 设置指向MTIME寄存器的指针,MTIME是64位寄存器,高32位在+4字节偏移处 */
    volatile uint32_t * const pulTimeHigh = ( volatile uint32_t * const ) ( ( configMTIME_BASE_ADDRESS ) + 4UL ); /* 8-byte type so high 32-bit word is 4 bytes up. */
    volatile uint32_t * const pulTimeLow = ( volatile uint32_t * const ) ( configMTIME_BASE_ADDRESS );
    volatile uint32_t ulHartId;
    
    /* 读取当前硬件线程ID(Hart ID),用于确定使用哪个定时器比较寄存器 */
    __asm volatile ( "csrr %0, mhartid" : "=r" ( ulHartId ) );
    
    /* 根据Hart ID计算对应的MTIMECMP寄存器地址,每个hart有独立的比较寄存器 */
    pullMachineTimerCompareRegister = ( volatile uint64_t * ) ( ullMachineTimerCompareRegisterBase + ( ulHartId * sizeof( uint64_t ) ) );
    
    /* 原子地读取64位MTIME寄存器值,防止在读取过程中高位发生变化 */
    do
    {
        ulCurrentTimeHigh = *pulTimeHigh;  /* 先读取高32位 */
        ulCurrentTimeLow = *pulTimeLow;    /* 再读取低32位 */
    } while( ulCurrentTimeHigh != *pulTimeHigh ); /* 确保读取期间高位没有变化 */
    
    /* 将32位的高低位组合成64位的当前时间值 */
    ullNextTime = ( uint64_t ) ulCurrentTimeHigh;
    ullNextTime <<= 32ULL; /* 高32位左移到正确位置 */
    ullNextTime |= ( uint64_t ) ulCurrentTimeLow; /* 或上低32位 */
    
    /* 计算下次定时器中断的时间点:当前时间 + 一个tick的时间增量 */
    ullNextTime += ( uint64_t ) uxTimerIncrementsForOneTick;
    
    /* 设置机器定时器比较寄存器,当MTIME达到这个值时触发中断 */
    *pullMachineTimerCompareRegister = ullNextTime;
    
    /* 预先计算下下次中断的时间,为下次中断处理做准备 */
    ullNextTime += ( uint64_t ) uxTimerIncrementsForOneTick;
}

xPortStartFirstTask启动第一个任务

xPortStartFirstTask 只需按照堆栈中放置数据的约定,把数据放入合适的寄存器,把PC放入RA寄存器,调用ret返回即可执行任务

c 复制代码
xPortStartFirstTask:
    /* 任务启动函数 - 启动第一个FreeRTOS任务 */
    
    load_x  sp, pxCurrentTCB            /* 将当前任务控制块(TCB)的地址加载到栈指针寄存器 */
    load_x  sp, 0( sp )                 /* 从TCB的第一个成员读取该任务的栈指针值,更新sp */
    
    /* 恢复任务的上下文 - 按照栈中保存的顺序恢复寄存器 */
    load_x  x1, 0( sp )                 /* 恢复x1寄存器(ra - 返回地址),用作任务函数的返回地址 */
    load_x  x5, 1 * portWORD_SIZE( sp ) /* 恢复初始mstatus寄存器值到x5(t0) */
    addi    x5, x5, 0x08                /* 设置MIE位(Machine Interrupt Enable),使任务启动时中断使能 */
    csrw    mstatus, x5                 /* 将修改后的mstatus写入控制状态寄存器,从此处开始中断使能! */
    
    portasmRESTORE_ADDITIONAL_REGISTERS /* 恢复RISC-V实现特有的额外寄存器(在freertos_risc_v_chip_specific_extensions.h中定义) */
    
    /* 恢复通用寄存器 - 临时寄存器和参数寄存器 */
    load_x  x7,  5  * portWORD_SIZE( sp )   /* 恢复t2寄存器 */
    load_x  x8,  6  * portWORD_SIZE( sp )   /* 恢复s0/fp寄存器(帧指针) */
    load_x  x9,  7  * portWORD_SIZE( sp )   /* 恢复s1寄存器 */
    load_x  x10, 8  * portWORD_SIZE( sp )   /* 恢复a0寄存器(第一个参数/返回值) */
    load_x  x11, 9  * portWORD_SIZE( sp )   /* 恢复a1寄存器(第二个参数) */
    load_x  x12, 10 * portWORD_SIZE( sp )   /* 恢复a2寄存器(第三个参数) */
    load_x  x13, 11 * portWORD_SIZE( sp )   /* 恢复a3寄存器(第四个参数) */
    load_x  x14, 12 * portWORD_SIZE( sp )   /* 恢复a4寄存器(第五个参数) */
    load_x  x15, 13 * portWORD_SIZE( sp )   /* 恢复a5寄存器(第六个参数) */

#ifndef __riscv_32e
    /* 非RV32E架构(完整寄存器集)才需要恢复以下寄存器 */
    load_x  x16, 14 * portWORD_SIZE( sp )   /* 恢复a6寄存器(第七个参数) */
    load_x  x17, 15 * portWORD_SIZE( sp )   /* 恢复a7寄存器(第八个参数) */
    load_x  x18, 16 * portWORD_SIZE( sp )   /* 恢复s2寄存器(保存寄存器) */
    load_x  x19, 17 * portWORD_SIZE( sp )   /* 恢复s3寄存器(保存寄存器) */
    load_x  x20, 18 * portWORD_SIZE( sp )   /* 恢复s4寄存器(保存寄存器) */
    load_x  x21, 19 * portWORD_SIZE( sp )   /* 恢复s5寄存器(保存寄存器) */
    load_x  x22, 20 * portWORD_SIZE( sp )   /* 恢复s6寄存器(保存寄存器) */
    load_x  x23, 21 * portWORD_SIZE( sp )   /* 恢复s7寄存器(保存寄存器) */
    load_x  x24, 22 * portWORD_SIZE( sp )   /* 恢复s8寄存器(保存寄存器) */
    load_x  x25, 23 * portWORD_SIZE( sp )   /* 恢复s9寄存器(保存寄存器) */
    load_x  x26, 24 * portWORD_SIZE( sp )   /* 恢复s10寄存器(保存寄存器) */
    load_x  x27, 25 * portWORD_SIZE( sp )   /* 恢复s11寄存器(保存寄存器) */
    load_x  x28, 26 * portWORD_SIZE( sp )   /* 恢复t3寄存器(临时寄存器) */
    load_x  x29, 27 * portWORD_SIZE( sp )   /* 恢复t4寄存器(临时寄存器) */
    load_x  x30, 28 * portWORD_SIZE( sp )   /* 恢复t5寄存器(临时寄存器) */
    load_x  x31, 29 * portWORD_SIZE( sp )   /* 恢复t6寄存器(临时寄存器) */
#endif

    /* 恢复任务的临界区嵌套计数器 */
    load_x  x5, portCRITICAL_NESTING_OFFSET * portWORD_SIZE( sp )    /* 从任务栈中获取该任务的临界嵌套计数值 */
    load_x  x6, pxCriticalNesting           /* 将全局临界嵌套变量的地址加载到x6 */
    store_x x5, 0( x6 )                     /* 恢复该任务的临界嵌套计数值到全局变量 */
    
    /* 恢复最后两个临时寄存器 */
    load_x  x5, 3 * portWORD_SIZE( sp )     /* 恢复x5(t0)寄存器的初始值 */
    load_x  x6, 4 * portWORD_SIZE( sp )     /* 恢复x6(t1)寄存器的初始值 */
    
    /* 调整栈指针,释放上下文保存空间 */
    addi    sp, sp, portCONTEXT_SIZE        /* 栈指针向上调整,跳过已恢复的上下文数据 */
    
    /* 跳转到任务函数开始执行 */
    ret                                     /* 返回到x1(ra)寄存器中保存的任务函数地址,开始执行任务 */

总结调用链如下所示:

复制代码
main_blinky()                           // 主函数:创建队列和任务,启动调度器
    └── vTaskStartScheduler()           // 调度器启动:初始化系统任务和调度状态
            ├── prvCreateIdleTasks()    // 创建空闲任务(系统必需的后台任务)
            ├── xTimerCreateTimerTask() // 创建定时器服务任务(软件定时器功能)
            └── xPortStartScheduler()   // 架构相关启动:硬件初始化和任务切换
                    ├── vPortSetupTimerInterrupt()  // 设置RISC-V定时器中断(任务切换时基)
                    └── xPortStartFirstTask()       // 启动第一个任务(上下文切换到用户任务)

这里我们简单看一下前面在开始第一次调度的时候,创建的空闲任务具体内容。

空闲任务

为每一个CPU创建空闲任务,当前仅一个CPU

c 复制代码
static BaseType_t prvCreateIdleTasks( void )
{
    BaseType_t xReturn = pdPASS;                    // 函数返回值,初始化为成功
    BaseType_t xCoreID;                             // 当前处理的CPU核心ID
    char cIdleName[ configMAX_TASK_NAME_LEN ] = { 0 };  // 空闲任务名称缓冲区
    TaskFunction_t pxIdleTaskFunction = NULL;       // 空闲任务函数指针
    UBaseType_t xIdleTaskNameIndex;                 // 任务名称字符索引
   
    // 第一步:构建空闲任务的基础名称
    // 从配置文件中的空闲任务名称复制字符,直到遇到空字符或达到最大长度
    for( xIdleTaskNameIndex = 0U; xIdleTaskNameIndex < ( configMAX_TASK_NAME_LEN - taskRESERVED_TASK_NAME_LENGTH ); xIdleTaskNameIndex++ )
    {
        // 逐字符复制配置的空闲任务名称
        cIdleName[ xIdleTaskNameIndex ] = configIDLE_TASK_NAME[ xIdleTaskNameIndex ];
        // 如果遇到字符串结束符,停止复制
        if( cIdleName[ xIdleTaskNameIndex ] == ( char ) 0x00 )
        {
            break;
        }
    }

    // 确保字符串以空字符结尾
    cIdleName[ xIdleTaskNameIndex ] = '\0';

    // 第二步:为每个CPU核心创建空闲任务
    // 以最低优先级为每个核心添加空闲任务
    for( xCoreID = ( BaseType_t ) 0; xCoreID < ( BaseType_t ) configNUMBER_OF_CORES; xCoreID++ )
    {
        // 根据系统配置选择合适的空闲任务函数
        #if ( configNUMBER_OF_CORES == 1 )
        {
            // 单核系统:使用标准空闲任务函数
            pxIdleTaskFunction = &prvIdleTask;
        }
        #else /* #if (  configNUMBER_OF_CORES == 1 ) */
        {
            /* 在FreeRTOS SMP中,除了主空闲任务外,还会创建 configNUMBER_OF_CORES - 1 个
             * 被动空闲任务,确保每个核心在没有其他任务可运行时都有空闲任务可执行 */
            if( xCoreID == 0 )
            {
                // 核心0:使用主空闲任务函数
                pxIdleTaskFunction = &prvIdleTask;
            }
            else
            {
                // 其他核心:使用被动空闲任务函数
                pxIdleTaskFunction = &prvPassiveIdleTask;
            }
        }
        #endif /* #if (  configNUMBER_OF_CORES == 1 ) */

        // 第三步:为多核系统更新空闲任务名称,添加核心ID后缀以区分不同核心的空闲任务
        /* 在单核FreeRTOS中不需要此功能,因为只有一个空闲任务 */
        #if ( configNUMBER_OF_CORES > 1 )
        {
			// 宏不成立
        }
        #else /* if ( configSUPPORT_STATIC_ALLOCATION == 1 ) */
        {
            // 动态内存分配方式创建空闲任务
            /* 空闲任务使用动态分配的RAM创建 */
            xReturn = xTaskCreate( pxIdleTaskFunction,           // 任务函数
                                   cIdleName,                    // 任务名称
                                   configMINIMAL_STACK_SIZE,     // 最小栈大小
                                   ( void * ) NULL,              // 任务参数
                                   portPRIVILEGE_BIT,           // 优先级(实际上是 tskIDLE_PRIORITY | portPRIVILEGE_BIT,但tskIDLE_PRIORITY为0)
                                   &xIdleTaskHandles[ xCoreID ] ); // 任务句柄存储位置
        }
        #endif /* configSUPPORT_STATIC_ALLOCATION */

    }

    return xReturn;  // 返回创建结果(pdPASS表示成功,pdFAIL表示失败)
}

pxIdleTaskFunction 是任务函数,具体内容非常简单:

c 复制代码
static portTASK_FUNCTION( prvIdleTask, pvParameters )
{
    /* Stop warnings. */
    ( void ) pvParameters;

    for( ; configCONTROL_INFINITE_LOOP(); )
    {
        /* See if any tasks have deleted themselves - if so then the idle task
         * is responsible for freeing the deleted task's TCB and stack. */
        prvCheckTasksWaitingTermination();
    }
}

简单来说就是循环检查是否需要回收任务空间。


总结

完结撒花!!!

相关推荐
文火冰糖的硅基工坊1 小时前
[硬件电路-115]:模拟电路 - 信号处理电路 - 功能放大器工作分类、工作原理、常见芯片
架构·信号处理·跨学科融合
你我约定有三2 小时前
分布式微服务--Nacos作为配置中心(二)
java·分布式·spring cloud·微服务·架构·wpf·负载均衡
俞凡6 小时前
[大厂实践] Netflix 分布式计数器抽象
架构
EnigmaGcl6 小时前
领域驱动设计,到底在讲什么?
后端·架构
元闰子7 小时前
分离还是统一,这是个问题
后端·面试·架构
文火冰糖的硅基工坊7 小时前
[硬件电路-124]:模拟电路 - 信号处理电路 - 测量系统的前端电路详解
前端·嵌入式硬件·fpga开发·架构·信号处理·电路
爷_8 小时前
手把手教程:用腾讯云新平台搞定专属开发环境,永久免费薅羊毛!
前端·后端·架构
Ashlee_code8 小时前
北极圈金融科技革命:奥斯陆证券交易所的绿色跃迁之路 ——从Visma千倍增长到碳信用衍生品,解码挪威资本市场的技术重构
科技·算法·金融·重构·架构·系统架构·区块链
前端搬砖仔噜啦噜啦嘞9 小时前
Cursor AI 编辑器入门教程和实战
前端·架构