FreeRTOS

因为上传原因完整pdf版的链接
【有道云笔记】espidf启动流程.pdf
有道云笔记
或者在CSDN我的主页
分析到这里espidf启动流程已经分析完成

后面的是个人的一个好奇

在arm m3m4内核里 FreeRTOS

ARM 中FreeRTOS的实现

机制作用优先级配置
SVC启动第一个任务(特权模式切换)最高优先级
PendSV任务上下文切换最低优先级(可被抢占)
SysTick提供时间片调度中等优先级(低于 SVC)

那在ESP32S3(risc-v)里FreeRTOS的这些是如何实现的呢

先说结论ESP32-S3(RISC-V)中 FreeRTOS 的实现

机制作用实现方式与 ARM Cortex-M 的对比
ECALL(环境调用)启动第一个任务通过 ECALL 指令触发异常,切换到机器模式(M-Mode)初始化任务上下文。类似 ARM 的 SVC,但基于 RISC-V 特权模式。
上下文切换任务切换直接通过汇编保存/恢复寄存器(无 PendSV),依赖 portYIELD() 触发切换。无专用异常,手动保存上下文。
CLINT(定时器中断)时间片调度使用 RISC-V 的机器模式定时器中断(MTI)或核心本地中断器(CLINT)驱动调度。类似 ARM 的 SysTick,但基于 RISC-V 硬件定时器。

开始分析源码

先从 vTaskStartScheduler();// 启动FreeRTOS调度器开始分析

在这里插入图片描述

xPortStartScheduler主要做了

  1. 设置硬件定时器并使能定时器中断
  2. 配置中断阈值:
    • 根据硬件支持情况(SOC_INT_CLIC_SUPPORTED),设置全局中断屏蔽阈值(INTC 或 CLIC 模式)。
  3. 全局中断使能:
    • 调用 rv_utils_intr_global_enable 使能全局中断。
  4. 启动任务调度:
    • 通过 vPortYield 触发任务调度,开始执行最高优先级的就绪任务。

设置硬件定时器并使能定时器中断

在这里插入图片描述

可以看到 vSystimerSetup 配置了硬件定时器并使能了定时器中断

vSystimerSetup

  • 注册了硬件定时器中断回调函数 SysTickIsrHandler
    在这里插入图片描述

一套流程下来其实就是把定时器中断函数与参数保存在 s_intr_handlers 结构体数组中

vSystimerSetup后面再调systimer_hal_enable_alarm_int 来使能这个中断

后面发生中断时再根据mcase在这个s_intr_handlers 结构体数组中取出SysTickIsrHandler 来处理
在这里插入图片描述

中断使能就是把从系统定时器发出中断到cpu处理这条路配置好

  • 配置了系统定时器

    通过 systimer_hal_init 给 systimer_hal 赋值,让它指向系统定时器(寄存器地址)

systimer_hal 结构体

/**
 * @brief systimer HAL 上下文结构体
 * 
 * 该结构体用于存储 systimer 硬件抽象层(HAL)的上下文信息。
 */
typedef struct {
    systimer_soc_handle_t dev;      /*!< systimer 外设基地址(SoC 相关句柄)*/
    ticks_to_us_func_t ticks_to_us; /*!< 将 ticks 转换为微秒的函数指针 */
    us_to_ticks_func_t us_to_ticks; /*!< 将微秒转换为 ticks 的函数指针 */
} systimer_hal_context_t;

SYSTIMER的定义
在这里插入图片描述

vSystimerSetup 后面的函数就是通过给 systimer_hal.dev成员赋值,从而配置系统定时器

配置系统定时器为周期报警模式,周期为SYSTIMER_COUNTER_OS_TICK

配置系统定时器的时钟源
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

官方文档对时钟中断描述

在这里插入图片描述

配置好系统定时器就要开始调度第一个任务

vPortYield
在这里插入图片描述

软件触发一个中断,那是啥中断呢,它的回调函数是啥
在这里插入图片描述

在 esp_startup_start_app 注册了esp_crosscore_isr 回调函数,这个就是前面软件触发中断的回调函数

调度第一个任务

调度第一个任务是在中断中处理的

那就要分析esp32c3(RISC-V)的中断了

在前面已经注册中断向量表了
在这里插入图片描述

中断向量表的定义
在这里插入图片描述

通过中断向量表可以知道所以的中断都调用 _interrupt_handler

_interrupt_handler 的部分代码截图(完整的在 components\riscv\vectors.S)

在这里插入图片描述

保存寄存器,在ARM m3 m4中进入中断时硬件会保存一部分寄存器,所以在ARM中软件只需保存部分寄存器

那在risc-v中
在这里插入图片描述

cpu进入中断只会保存中断返回地址,更新mcause

所以在risc-v中,保存寄存器是保存所有寄存器(如果用到浮点寄存器也要保存)

接下来分析 _interrupt_handler 中关于rtos的两个关键函数rtos_int_enter,rtos_int_exit

rtos_int_enter

1. 多核调度器状态检查

  • 读取当前核心 ID(mhartid),计算调度器状态变量的偏移地址。
  • 检查调度器是否运行(port_xSchedulerRunning):
    • 若未运行(a0=0),直接跳过后续处理(避免中断嵌套管理或栈切换)。

2. 中断嵌套管理

  • 更新中断嵌套计数器(port_uxInterruptNesting):
    • 首次进入中断时,计数器从 0 → 1,触发后续处理(如栈切换)。
    • 嵌套中断(计数器 > 0)则跳过栈切换,仅递增计数器。

3. 协处理器(如 FPU)管理

  • 禁用 FPU(若存在 SOC_CPU_COPROC_NUM > 0):
    • 防止中断服务例程(ISR)意外使用 FPU 寄存器,避免任务上下文被破坏。

4. 硬件栈保护暂停

  • 暂停当前核心的栈监控(ESP_HW_STACK_GUARD_MONITOR_STOP_CUR_CORE):
    • 避免栈保护机制在中断处理期间误报溢出。

5. 任务栈与中断栈切换

  • 保存任务栈指针:
    • 将当前任务的栈指针(sp)保存到其 TCB(任务控制块)中(pxCurrentTCBs[coreID])。
  • 切换至中断栈:
    • 加载预定义的中断栈顶地址(xIsrStackTop[coreID])到 sp,确保 ISR 使用独立栈空间,避免任务栈溢出。

rtos_int_exit源码中文注释版

/**
 * @brief 恢复下一个要运行任务的栈指针
 * 
 * @param a0 原始的 mstatus 状态
 * @return 新的 mstatus 状态(可能禁用协处理器)
 *
 * 功能:中断退出处理,主要完成:
 * 1. 恢复任务栈指针
 * 2. 更新中断嵌套计数器
 * 3. 协处理器状态管理
 * 4. 触发任务调度(如果需要)
 */
    .global rtos_int_exit
    .type rtos_int_exit, @function
rtos_int_exit:
    /* 使用被调用者保存寄存器(s11)优化性能 */
    mv      s11, a0                         /* s11 = 原始mstatus */

#if ( configNUM_CORES > 1 )
    csrr    a1, mhartid                     /* 获取当前核心ID */
    slli    a1, a1, 2                       /* 计算数组偏移(核心ID*4) */
    la      a0, port_xSchedulerRunning      /* 加载调度器运行状态数组 */
    add     a0, a0, a1                      /* 计算当前核心状态地址 */
    lw      a0, (a0)                        /* 读取调度器状态 */
#else
    lw      a0, port_xSchedulerRunning      /* 单核直接读取状态 */
#endif
    beqz    a0, rtos_int_exit_end           /* 调度器未运行则直接退出 */

    /* 更新中断嵌套计数器 ------------------------------------------*/
    la      a2, port_uxInterruptNesting     /* 加载嵌套计数器数组 */
#if ( configNUM_CORES > 1 )
    add     a2, a2, a1                      /* 计算当前核心计数器地址 */
#endif
    lw      a0, 0(a2)                       /* 读取当前嵌套值 */
    beqz    a0, isr_skip_decrement          /* 防下溢(嵌套计数=0则跳过递减) */
    addi    a0, a0, -1                      /* 嵌套计数-1 */
    sw      a0, 0(a2)                       /* 更新嵌套计数器 */
    bnez    a0, rtos_int_exit_end           /* 仍有嵌套中断则直接退出 */

    /* 以下代码仅在最外层中断退出时执行 ----------------------------*/
isr_skip_decrement:
    /* 检查任务切换标志 */
#if ( configNUM_CORES > 1 )
    la      a0, xPortSwitchFlag             /* 加载切换标志数组 */
    add     a0, a0, a1                      /* 计算当前核心标志地址 */
    lw      a2, 0(a0)                       /* 读取切换标志 */
#else
    lw      a2, xPortSwitchFlag             /* 单核直接读取标志 */
#endif
    beqz    a2, no_switch                   /* 无切换请求则跳过 */

    /* 保存返回地址并准备上下文切换 ------------------------------*/
    mv      s10, ra                         /* 保存返回地址到s10 */

#if ( SOC_CPU_COPROC_NUM > 0 )
    /* 协处理器状态管理 */
    call    rtos_current_tcb                /* 获取当前TCB */
    mv      s9, a0                          /* 保存旧TCB */
    call    vTaskSwitchContext              /* 执行任务切换 */
    call    rtos_current_tcb                /* 获取新TCB */
    beq     a0, s9, rtos_int_exit_no_change /* 相同任务则跳过FPU设置 */

    /* 禁用协处理器(修改s11中的mstatus镜像) */
    li      a0, ~CSR_MSTATUS_FPU_DISABLE
    and     s11, s11, a0                    /* 清除FPU使能位 */

通过启动第一个任务来分析

在启动调度器时分析了,触发了一个中断来启动第一个任务

那就会调用 _interrupt_handler

进而调用 rtos_int_enter
在这里插入图片描述

此时 port_xSchedulerRunning==0,rtos_int_enter 啥也没做

调用中断处理函数

在这里插入图片描述

在 s_intr_handlers 找出 esp_crosscore_isr来调用
在这里插入图片描述

esp_crosscore_isr 其实就是设置 port_xSchedulerRunning[coreID] = 1; xPortSwitchFlag[coreID] = 1;

rtos_int_exit

isr_skip_decrement 实现切换任务,恢复下一个要运行任务的栈指针

在这里插入图片描述

rtos_int_exit_no_change 清0 xPortSwitchFlag[coreID]

后面从任务的栈里恢复寄存器,第一个任务就启动了

通过systick 中断触发切换任务来分析

假如 systick 中断了

那就会调用 _interrupt_handler

  • 进而调用 rtos_int_enter

​ rtos_int_enter就把port_uxInterruptNesting[coreID]加一,切换栈

  • 调用中断处理函数

    在 s_intr_handlers 找出 SysTickIsrHandler来调用

SysTickIsrHandler函数分析
在这里插入图片描述

就是设置 port_xSchedulerRunning[coreID] = 1; xPortSwitchFlag[coreID] = 1;

  • rtos_int_exit
    在这里插入图片描述

此时就有疑问了 isr_skip_decrement 是实现切换任务,恢复下一个要运行任务的栈指针的核心函数

在rtos_int_exit中既然没有调用

AI的回答
在这里插入图片描述

意思就是先减减,再判断

那就有几个疑问了

  • 第一个任务启动时,调rtos_int_exit时 port_uxInterruptNesting[coreID]=0;那还能减吗
  • 还有一个就是只要不中断嵌套,那不是所有中断都调用 isr_skip_decrement
    在这里插入图片描述

查看反汇编可以知道是先判断然后再减减,是按源码的逻辑

生成反汇编命令

riscv32-esp-elf-objdump -D ./build/hello_world.elf > hello_world.dis

是按源码的逻辑,那按上面的分析好像不太合理

那猜测一下port_uxInterruptNesting有人修改

比如在SysTickIsrHandler让 port_uxInterruptNesting 减减

那好像就符合逻辑了,其他中断那个比较分支都是不成立的

但是认真分析了 SysTickIsrHandler,发现没人改到 port_uxInterruptNesting

那就要换一种思路了

那就要认真分析rtos_int_exit了

在这里插入图片描述

a0=0;那就不调用rtos_int_exit_end了,rtos_int_exit函数到这没了,但是没有ret语句

那此时cpu就会取这个地址的下一条指令执行
在这里插入图片描述

那就是isr_skip_decrement函数
在这里插入图片描述

isr_skip_decrement 通过 xPortSwitchFlag 来判断是否要切换

在 SysTickIsrHandler 中设置了xPortSwitchFlag 所以在 isr_skip_decrement 就要进行切换逻辑

感觉分析下来espidf的FreeRTOS切换任务有点奇怪,所有中断共有一个_interrupt_handler ,这就导致所有中断都要做rtos的操作

可能这样编写是为了兼容不支持向量中断的芯片吧(直接中断)

FreeRTOS的系统时钟节拍
在这里插入图片描述

xTaskIncrementTick() 是 FreeRTOS 的 系统时钟节拍(tick)中断服务函数,通常由 硬件定时器中断 触发,用于:

  1. 更新系统时钟(xTickCount)。

  2. 检查是否有阻塞任务需要唤醒(pxDelayedTaskList)。

  3. 处理时间片调度(configUSE_TIME_SLICING)。

  4. 调用用户定义的 Tick Hook 函数(vApplicationTickHook())。

  5. 决定是否需要任务切换(返回 xSwitchRequired)。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐