espidf的FreeRTOS
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主要做了
- 设置硬件定时器并使能定时器中断
- 配置中断阈值:
- 根据硬件支持情况(
SOC_INT_CLIC_SUPPORTED),设置全局中断屏蔽阈值(INTC 或 CLIC 模式)。
- 根据硬件支持情况(
- 全局中断使能:
- 调用
rv_utils_intr_global_enable使能全局中断。
- 调用
- 启动任务调度:
- 通过
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)中断服务函数,通常由 硬件定时器中断 触发,用于:
-
更新系统时钟(
xTickCount)。 -
检查是否有阻塞任务需要唤醒(
pxDelayedTaskList)。 -
处理时间片调度(
configUSE_TIME_SLICING)。 -
调用用户定义的 Tick Hook 函数(
vApplicationTickHook())。 -
决定是否需要任务切换(返回
xSwitchRequired)。
更多推荐
所有评论(0)