系列文章目录
FreeRTOS源码分析一:task创建(RISCV架构)
FreeRTOS源码分析二:task启动(RISCV架构)
FreeRTOS源码分析三:列表数据结构
文章目录
前言
task
启动篇中提到,启动第一个任务之前,xPortStartScheduler
函数调用 vPortSetupTimerInterrupt
设置定时器中断随后使能了 mie
寄存器的时钟中断位。
随后调用 xPortStartFirstTask
从任务堆栈中装载寄存器并通过 ret
返回到任务的第一条指令位置启动第一个任务。
本篇我们来分析一下时钟中断触发的时间间隔以及在 RISCV
架构下的响应流程。
中断触发间隔:一个 tick
void vPortSetupTimerInterrupt( void )
{
......
/* 计算下次定时器中断的时间点:当前时间 + 一个tick的时间增量 */
ullNextTime += ( uint64_t ) uxTimerIncrementsForOneTick;
/* 设置机器定时器比较寄存器,当MTIME达到这个值时触发中断 */
*pullMachineTimerCompareRegister = ullNextTime;
/* 预先计算下下次中断的时间,为下次中断处理做准备 */
ullNextTime += ( uint64_t ) uxTimerIncrementsForOneTick;
}
vPortSetupTimerInterrupt
会设置 mtimecmp
寄存器为当前 mtime + uxTimerIncrementsForOneTick
。当 mtime
大于 mtimecmp
时触发时钟中断。宏定义如下:
#define configCPU_CLOCK_HZ ( 10000000 )
#define configTICK_RATE_HZ ( ( TickType_t ) 1000 )
const size_t uxTimerIncrementsForOneTick = ( size_t ) ( ( configCPU_CLOCK_HZ ) / ( configTICK_RATE_HZ ) );
configCPU_CLOCK_HZ
定义了 CPU 的主频为10,000,000 Hz
(即 10 MHz)。configTICK_RATE_HZ
定义了FreeRTOS
的tick rate
,即每秒钟触发多少次系统时钟中断。这里设置为 1000,即每 1 毫秒触发一次 tick 中断。uxTimerIncrementsForOneTick
计算出 每个 tick 所需的定时器增量(计数器的步进),也就是说:要产生一次 tick 中断,硬件定时器需要计数多少个 CPU 时钟周期。
中断响应流程
1.中断向量设置
主函数调用 Demo 之前设置中断异常处理函数 freertos_risc_v_trap_handler
int main( void )
{
__asm__ volatile ( "csrw mtvec, %0" : : "r" ( freertos_risc_v_trap_handler ) );
return main_blinky();
}
2.中断响应上下文保存
宏 portcontextSAVE_CONTEXT_INTERNAL 在异常或中断发生时最先执行,依旧按照第一篇新建任务初始化堆栈时,堆栈的格式来设置堆栈内部的寄存器的内容
.macro portcontextSAVE_CONTEXT_INTERNAL
addi sp, sp, -portCONTEXT_SIZE
store_x x1, 2 * portWORD_SIZE( sp )
store_x x5, 3 * portWORD_SIZE( sp )
store_x x6, 4 * portWORD_SIZE( sp )
store_x x7, 5 * portWORD_SIZE( sp )
store_x x8, 6 * portWORD_SIZE( sp )
store_x x9, 7 * portWORD_SIZE( sp )
store_x x10, 8 * portWORD_SIZE( sp )
store_x x11, 9 * portWORD_SIZE( sp )
store_x x12, 10 * portWORD_SIZE( sp )
store_x x13, 11 * portWORD_SIZE( sp )
store_x x14, 12 * portWORD_SIZE( sp )
store_x x15, 13 * portWORD_SIZE( sp )
#ifndef __riscv_32e
store_x x16, 14 * portWORD_SIZE( sp )
store_x x17, 15 * portWORD_SIZE( sp )
store_x x18, 16 * portWORD_SIZE( sp )
store_x x19, 17 * portWORD_SIZE( sp )
store_x x20, 18 * portWORD_SIZE( sp )
store_x x21, 19 * portWORD_SIZE( sp )
store_x x22, 20 * portWORD_SIZE( sp )
store_x x23, 21 * portWORD_SIZE( sp )
store_x x24, 22 * portWORD_SIZE( sp )
store_x x25, 23 * portWORD_SIZE( sp )
store_x x26, 24 * portWORD_SIZE( sp )
store_x x27, 25 * portWORD_SIZE( sp )
store_x x28, 26 * portWORD_SIZE( sp )
store_x x29, 27 * portWORD_SIZE( sp )
store_x x30, 28 * portWORD_SIZE( sp )
store_x x31, 29 * portWORD_SIZE( sp )
#endif /* ifndef __riscv_32e */
load_x t0, xCriticalNesting /* Load the value of xCriticalNesting into t0. */
store_x t0, portCRITICAL_NESTING_OFFSET * portWORD_SIZE( sp ) /* Store the critical nesting value to the stack. */
portasmSAVE_ADDITIONAL_REGISTERS /* 空宏,无操作 */
csrr t0, mstatus
store_x t0, 1 * portWORD_SIZE( sp )
3.切换到 ISR 中断服务例程专用堆栈
中断服务例程代码保存寄存器之后根据 mcause 判断是异常还是中断进行进一步处理
.section .text.freertos_risc_v_trap_handler
.align 8 # 8字节对齐
freertos_risc_v_trap_handler:
portcontextSAVE_CONTEXT_INTERNAL # 保存当前任务的上下文(寄存器状态)
# 读取异常/中断原因和程序计数器
csrr a0, mcause # 读取机器模式异常原因寄存器到a0
csrr a1, mepc # 读取机器模式异常程序计数器到a1
# 判断是中断还是异常(mcause的MSB位)
bge a0, x0, synchronous_exception # 如果mcause >= 0,跳转到同步异常处理
# (中断的mcause MSB=1,为负数)
# 异步中断处理分支
asynchronous_interrupt:
store_x a1, 0( sp ) # 中断:保存未修改的异常返回地址
load_x sp, xISRStackTop # 切换到ISR专用栈
j handle_interrupt # 跳转到中断处理
# 同步异常处理分支
synchronous_exception:
addi a1, a1, 4 # 同步异常:返回地址+4,指向异常指令的下一条指令
store_x a1, 0( sp ) # 保存更新后的异常返回地址
load_x sp, xISRStackTop # 切换到ISR专用栈
j handle_exception # 跳转到异常处理
# 中断处理主体
handle_interrupt:
#if( portasmHAS_MTIME != 0 ) # 如果系统有MTIME定时器
test_if_mtimer: # 检查是否为机器定时器中断
addi t0, x0, 1 # t0 = 1
slli t0, t0, __riscv_xlen - 1 # 将1左移到MSB位置(32位系统移31位,64位系统移63位)
addi t1, t0, 7 # t1 = 0x8000...0007(机器定时器中断代码)
bne a0, t1, application_interrupt_handler # 如果不是定时器中断,跳转到应用中断处理
# 处理定时器中断
portUPDATE_MTIMER_COMPARE_REGISTER # 更新定时器比较寄存器(设置下次中断时间)
call xTaskIncrementTick # 调用FreeRTOS系统时钟增量函数
beqz a0, processed_source # 如果返回0(无任务需要切换),跳转到处理完成
call vTaskSwitchContext # 否则进行任务上下文切换
j processed_source # 跳转到处理完成
#endif /* portasmHAS_MTIME */
# 异常处理主体
handle_exception:
/* a0 包含 mcause 异常原因 */
li t0, 11 # t0 = 11(环境调用异常代码)
bne a0, t0, application_exception_handler # 如果不是环境调用,跳转到应用异常处理
# 处理系统调用(环境调用)
call vTaskSwitchContext # 进行任务上下文切换
j processed_source # 跳转到处理完成
# 中断/异常处理完成
processed_source:
portcontextRESTORE_CONTEXT # 恢复任务上下文(寄存器状态)
# 然后返回到被中断的代码继续执行
有个很重要的事情是:
- 经过时钟中断,进入中断处理例程,任务堆栈向下增长,值变小,保存寄存器切换中断服务例程的堆栈。这是第一步大流程。
- 此时堆栈中从栈顶开始到
portCONTEXT_SIZE
的大小空间从上到下存储的是SP、mstatus、x1、x5-x31、xCriticalNesting
- 这代表:此时任务可以切换,或者说,此时任务已经完成了所有的初始化,就像新建任务初始化完毕堆栈那样
- 同样的,对于最后的中断返回例程,我们也能猜到,它会按照约定取出堆栈中的内容,同时把堆栈增长的空间回收
总结来说,先保存现场到任务堆栈,确定是时钟中断之后,调用 portUPDATE_MTIMER_COMPARE_REGISTER 更新定时器,随后调用 xTaskIncrementTick 更新 tick,若返回非 0,则调用 vTaskSwitchContext 调度任务。随后恢复上下文并中断返回。
4.更新定时器以触发下一次时钟中断
portUPDATE_MTIMER_COMPARE_REGISTER 用于更新定时器
.macro portUPDATE_MTIMER_COMPARE_REGISTER
/* 加载定时器比较寄存器的地址到寄存器a0 */
load_x a0, pullMachineTimerCompareRegister
/* 加载下一次定时器触发时间变量的地址到寄存器a1 */
load_x a1, pullNextTime
#if( __riscv_xlen == 32 )
/*=== 32位RISC-V架构处理 ===*/
/* 在32位系统中,需要用两次32位写操作来更新64位的定时器比较值 */
li a4, -1 /* 将a4设置为0xFFFFFFFF(最大32位值) */
lw a2, 0(a1) /* 从ullNextTime加载低32位到a2 */
lw a3, 4(a1) /* 从ullNextTime加载高32位到a3 */
/*
* 关键的三步写入序列,防止意外的定时器中断:
* 1. 先写入最大值到低位,确保比较值不会小于当前值
* 2. 写入新的高位值
* 3. 最后写入新的低位值
*/
sw a4, 0(a0) /* 步骤1: 先将低位设为最大值,防止意外触发 */
sw a3, 4(a0) /* 步骤2: 写入ullNextTime的高32位到比较寄存器 */
sw a2, 0(a0) /* 步骤3: 写入ullNextTime的低32位到比较寄存器 */
/* 计算下一次定时器触发时间 = 当前时间 + 一个时间片的增量 */
lw t0, uxTimerIncrementsForOneTick /* 加载一个tick的定时器增量值 */
add a4, t0, a2 /* 低位相加:增量 + 当前时间低位 */
sltu t1, a4, a2 /* 检查低位相加是否溢出,如果a4 < a2则溢出 */
add t2, a3, t1 /* 高位 = 原高位 + 溢出标志 */
/* 将计算出的新的下次触发时间存回ullNextTime变量 */
sw a4, 0(a1) /* 存储新的低32位 */
sw t2, 4(a1) /* 存储新的高32位 */
#endif /* __riscv_xlen == 32 */
.endm
sltu
指令是 RISC-V
中的无符号比较指令,全称是 "Set Less Than Unsigned"
,如果 rs1 < rs2
(无符号比较),则将目标寄存器 rd
设置为 1
。
5. xTaskIncrementTick 更新 tick
xTaskIncrementTick 除了更新 tick 递增系统时钟计数,还会检查并唤醒到期的阻塞任务,根据优先级决定是否需要抢占式任务切换,调用应用程序时钟钩子函数
/**
*
* 当前配置参数:
* - configUSE_TICK_HOOK = 1 (启用时钟钩子)
* - configUSE_PREEMPTION = 1 (启用抢占调度)
* - configNUMBER_OF_CORES = 1 (单核处理器)
* - configUSE_TIME_SLICING = 0 (禁用时间片轮转)
*/
BaseType_t xTaskIncrementTick( void )
{
TCB_t * pxTCB; // 任务控制块指针
TickType_t xItemValue; // 延迟列表项的值(唤醒时间)
BaseType_t xSwitchRequired = pdFALSE; // 是否需要任务切换标志
/* 时钟递增应该在每个内核定时器事件上发生。
* 如果调度器被挂起,则递增待处理的时钟计数。 */
if( uxSchedulerSuspended == ( UBaseType_t ) 0U )
{
// === 调度器未被挂起的情况 ===
/* 小优化:在此代码块中时钟计数不会改变 */
const TickType_t xConstTickCount = xTickCount + ( TickType_t ) 1;
/* 递增RTOS时钟,如果溢出到0则切换延迟和溢出延迟列表 */
xTickCount = xConstTickCount;
// 处理时钟计数器溢出的情况(从最大值回绕到0)
if( xConstTickCount == ( TickType_t ) 0U )
{
taskSWITCH_DELAYED_LISTS(); // 切换延迟任务列表
}
/* 检查此时钟是否使某个超时到期。任务按其唤醒时间顺序存储在队列中
* 这意味着一旦找到一个阻塞时间未到期的任务,就无需继续查找列表中的其他任务 */
if( xConstTickCount >= xNextTaskUnblockTime )
{
// === 处理需要唤醒的阻塞任务 ===
for( ; ; )
{
// 检查延迟任务列表是否为空
if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
{
/* 延迟列表为空。将xNextTaskUnblockTime设置为最大可能值
* 这样下次通过时 if( xTickCount >= xNextTaskUnblockTime ) 测试
* 极不可能通过,避免不必要的列表检查 */
xNextTaskUnblockTime = portMAX_DELAY;
break;
}
else
{
/* 延迟列表不为空,获取延迟列表头部任务的唤醒时间
* 这是该任务必须从阻塞状态移除的时间点 */
pxTCB = listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );
xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );
// 检查是否到了唤醒时间
if( xConstTickCount < xItemValue )
{
/* 还没有到解除此任务阻塞的时间,记录下一个任务的唤醒时间
* 用于下次时钟中断时的快速判断 */
xNextTaskUnblockTime = xItemValue;
break;
}
/* 时间到了,从延迟列表中移除该任务 */
listREMOVE_ITEM( &( pxTCB->xStateListItem ) );
/* 检查任务是否同时在等待事件(如信号量、队列等)
* 如果是,也要从事件列表中移除 */
if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
{
listREMOVE_ITEM( &( pxTCB->xEventListItem ) );
}
/* 将唤醒的任务加入到对应优先级的就绪列表中 */
prvAddTaskToReadyList( pxTCB );
/* 抢占式调度:检查新唤醒的任务优先级是否高于当前运行任务
* 如果是,则需要进行任务切换以让高优先级任务立即运行 */
if( pxTCB->uxPriority > pxCurrentTCB->uxPriority )
{
xSwitchRequired = pdTRUE; // 标记需要任务切换
}
}
}
}
/* 调用应用程序定义的时钟钩子函数
* 只有在没有待处理时钟时才调用,避免调度器解锁时重复调用 */
if( xPendedTicks == ( TickType_t ) 0 )
{
vApplicationTickHook(); // 调用应用程序时钟钩子
}
/* 检查是否有待处理的让出请求需要处理
* 在单核系统中,xYieldPendings[0]用于标记是否需要任务切换 */
if( xYieldPendings[ 0 ] != pdFALSE )
{
xSwitchRequired = pdTRUE; // 需要任务切换
}
}
else
{
// === 调度器被挂起的情况 ===
/* 调度器被挂起时,不能立即处理任务调度
* 只累计待处理的时钟计数,等调度器恢复时统一处理 */
xPendedTicks += 1U;
/* 即使调度器被锁定,时钟钩子函数仍需要定期调用
* 这允许应用程序在调度器挂起期间执行时间相关的维护任务 */
vApplicationTickHook();
}
return xSwitchRequired; // 返回是否需要任务切换
}
这里我们先不关心任务相关的等待队列和事件等待队列。后续再细看。
简单来说,函数着重做几件事:
- 递增系统时钟计数,
- 检查并唤醒到期的阻塞任务,
- 根据优先级决定是否需要抢占式任务切换,若需要则设置返回值
xSwitchRequired
非 0。 - 另外,这里会检查条件
xYieldPendings[ 0 ] != pdFALSE
满足则需要调度。与下文函数vTaskSwitchContext
对应,它会在无法调度时设置该变量为pdTrue
。 - 调用应用程序时钟钩子函数
vTaskSwitchContext 调度任务
vTaskSwitchContext 非常简单,只设置全局变量 pxCurrentTCB 指向当前优先级最高的任务,待后续返回即执行该任务
void vTaskSwitchContext( void )
{
// 调度器是否挂起
if( uxSchedulerSuspended != ( UBaseType_t ) 0U )
{
/*
* xYieldPendings[0]: 挂起期间的切换请求标志
* - pdTRUE: 有挂起的切换请求,需要在调度器恢复时执行
* - pdFALSE: 没有挂起的切换请求
*/
xYieldPendings[ 0 ] = pdTRUE;
}
else
{
/*
* 调度器正常运行,可以执行任务切换
* 首先清除挂起标志,表示没有延迟的切换请求
*/
xYieldPendings[ 0 ] = pdFALSE;
/*
* 安全检查:栈溢出检测
* taskCHECK_FOR_STACK_OVERFLOW(): 宏定义,根据配置可能为空
*
* 检查内容:
* 1. 当前任务的栈指针是否超出栈空间边界
* 2. 栈末尾的"魔术数字"是否被破坏
*
* 如果检测到栈溢出:
* - 调用用户定义的钩子函数 vApplicationStackOverflowHook()
* - 通常会停止系统运行或重启
*/
taskCHECK_FOR_STACK_OVERFLOW();
/*
* 核心操作:选择下一个要运行的任务
* taskSELECT_HIGHEST_PRIORITY_TASK(): 关键宏定义
*/
taskSELECT_HIGHEST_PRIORITY_TASK();
/*
* 编译器优化抑制:
* (void)( pxCurrentTCB ): 告诉编译器pxCurrentTCB被"使用"了
*/
(void)( pxCurrentTCB );
}
}
注意:函数会在调度器挂起时在此返回,不执行实际的任务切换。那么什么时候切换?
- 1:当调度器恢复时
(xTaskResumeAll)
,会检查此标志如果为pdTRUE
,会执行延迟的任务切换。 - 2:当再次触发时钟中断,执行到函数
xTaskIncrementTick
更新tick
时它会检查xYieldPendings[0]
如果为pdTRUE
则返回非0
表示需要调度。
中断返回例程 portcontextRESTORE_CONTEXT
这里主要和中断执行例程对应上,在堆栈的哪个位置存的哪一个寄存器,就把哪一个寄存器写回去,另外回收堆栈空间
.macro portcontextRESTORE_CONTEXT
load_x t1, pxCurrentTCB /* 加载当前任务控制块指针 */
load_x sp, 0 ( t1 ) /* 从TCB第一个成员读取栈指针 */
/* 恢复程序计数器 */
load_x t0, 0 ( sp )
csrw mepc, t0 /* 设置下次要执行的指令地址 */
/* 恢复状态寄存器 */
load_x t0, 1 * portWORD_SIZE( sp )
csrw mstatus, t0
/* 空宏,什么都不执行 */
portasmRESTORE_ADDITIONAL_REGISTERS
/* 恢复临界嵌套计数 */
load_x t0, portCRITICAL_NESTING_OFFSET * portWORD_SIZE( sp )
load_x t1, pxCriticalNesting
store_x t0, 0 ( t1 )
/* 恢复通用寄存器 x1, x5-x15 */
load_x x1, 2 * portWORD_SIZE( sp )
load_x x5, 3 * portWORD_SIZE( sp )
load_x x6, 4 * portWORD_SIZE( sp )
load_x x7, 5 * portWORD_SIZE( sp )
load_x x8, 6 * portWORD_SIZE( sp )
load_x x9, 7 * portWORD_SIZE( sp )
load_x x10, 8 * portWORD_SIZE( sp )
load_x x11, 9 * portWORD_SIZE( sp )
load_x x12, 10 * portWORD_SIZE( sp )
load_x x13, 11 * portWORD_SIZE( sp )
load_x x14, 12 * portWORD_SIZE( sp )
load_x x15, 13 * portWORD_SIZE( sp )
#ifndef __riscv_32e
/* 标准RISC-V:恢复 x16-x31 */
load_x x16, 14 * portWORD_SIZE( sp )
load_x x17, 15 * portWORD_SIZE( sp )
load_x x18, 16 * portWORD_SIZE( sp )
load_x x19, 17 * portWORD_SIZE( sp )
load_x x20, 18 * portWORD_SIZE( sp )
load_x x21, 19 * portWORD_SIZE( sp )
load_x x22, 20 * portWORD_SIZE( sp )
load_x x23, 21 * portWORD_SIZE( sp )
load_x x24, 22 * portWORD_SIZE( sp )
load_x x25, 23 * portWORD_SIZE( sp )
load_x x26, 24 * portWORD_SIZE( sp )
load_x x27, 25 * portWORD_SIZE( sp )
load_x x28, 26 * portWORD_SIZE( sp )
load_x x29, 27 * portWORD_SIZE( sp )
load_x x30, 28 * portWORD_SIZE( sp )
load_x x31, 29 * portWORD_SIZE( sp )
#endif /* ifndef __riscv_32e */
addi sp, sp, portCONTEXT_SIZE /* 恢复栈指针位置 */
mret /* 机器模式返回,切换到任务 */
.endm
附
在选择最高优先级任务的时候用到了硬件加速。这里看一下:
这个宏用于记录就绪优先级,即设置该全局变量 uxReadyPriorities
的第 uxPriority bit
为 1
#define portRECORD_READY_PRIORITY( uxPriority, uxReadyPriorities ) \
( uxReadyPriorities ) |= ( 1UL << ( uxPriority ) )
这个宏用于清除就绪优先级,即设置该全局变量 uxReadyPriorities
的第 uxPriority bit
为 0
#define portRESET_READY_PRIORITY( uxPriority, uxReadyPriorities ) \
( uxReadyPriorities ) &= ~( 1UL << ( uxPriority ) )
这个宏用于获取最高就绪优先级,利用 __builtin_clz(x)
返回 x
的二进制表示中前导 0
的个数
#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) \
uxTopPriority = ( 31UL - __builtin_clz( uxReadyPriorities ) )
我们用一个简单程序验证一下它会被优化成 clz
指令,然后用一条指令完成任务:
// test_priority.c
#include <stdint.h>
#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) \
uxTopPriority = ( 31UL - __builtin_clz( uxReadyPriorities ) )
#define portRECORD_READY_PRIORITY( uxPriority, uxReadyPriorities ) \
( uxReadyPriorities ) |= ( 1UL << ( uxPriority ) )
volatile uint32_t uxReadyPriorities = 0;
volatile uint32_t uxTopPriority;
int main(void)
{
portRECORD_READY_PRIORITY(5, uxReadyPriorities);
portGET_HIGHEST_PRIORITY(uxTopPriority, uxReadyPriorities);
return 0;
}
使用如下命令编译:riscv32-unknown-elf-gcc -march=rv32im -mabi=ilp32 -O2 -S test_priority.c -o test_priority.s
,查看文件可以知道:
直观看,如果拥有 zbb
扩展,它被编译为指令 clz
,具体指令集描述如下所示:
总结
完结撒花!!!