FreeRTOS源码分析四:时钟中断处理响应流程
系列文章目录
FreeRTOS源码分析一:task创建(RISCV架构)
FreeRTOS源码分析二:task启动(RISCV架构)
FreeRTOS源码分析三:列表数据结构
文章目录
- 系列文章目录
- 前言
- 中断触发间隔:一个 tick
- 中断响应流程
- 1.中断向量设置
- 2.中断响应上下文保存
- 3.切换到 ISR 中断服务例程专用堆栈
- 4.更新定时器以触发下一次时钟中断
- 5. xTaskIncrementTick 更新 tick
- vTaskSwitchContext 调度任务
- 中断返回例程 portcontextRESTORE_CONTEXT
- 附
- 总结
前言
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_32estore_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 # 读取机器模式异常原因寄存器到a0csrr 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 = 1slli 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_CONTEXTload_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, pxCriticalNestingstore_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
,具体指令集描述如下所示:
总结
完结撒花!!!