百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

从0开始学FreeRTOS-(任务调度)-4(freertos调度机制)

csdh11 2025-04-06 15:05 19 浏览

大家晚上好,我是杰杰,最近挺忙的,好久没有更新了,今天周末就吐血更新一下吧!

前言

FreeRTOS是一个是实时内核,任务是程序执行的最小单位,也是调度器处理的基本单位,移植了FreeRTOS,则避免不了对任务的管理,在多个任务运行的时候,任务切换显得尤为重要。而任务切换的效率会决定了系统的稳定性与效率。

FreeRTOS的任务切换是干嘛的呢,rtos的实际是永远运行的是具有最高优先级的运行态任务,而那些之前在就绪态的任务怎么变成运行态使其得以运行呢,这就是我们FreeRTOS任务切换要做的事情,它要做的是找到最高优先级的就绪态任务,并且让它获得cpu的使用权,这样,它就能从就绪态变成运行态,这样子,整个系统的实时性就会很好,响应也会很好,而不会让程序阻塞卡死。

要知道怎么实现任务切换,那就要知道任务切换的机制,在不同的cpu(mcu)中,触发的方式可能会不一样,现在是以Cortex-M3为例来讲讲任务的切换。为了大家能看懂本文,我就抛转引玉一下,引用《Cortex-M3权威指南-中文版》的部分语句(如涉及侵权,请联系杰杰删除)

SVC 和 PendSV

SVC(系统服务调用,亦简称系统调用)和 PendSV(Pended System Call,可悬起系统调用),它们多用于在操作系统之上的软件开发中。SVC 用于产生系统函数的调用请求。例如,操作系统不让用户程序直接访问硬件,而是通过提供一些系统服务函数,用户程序使用 SVC 发出对系统服务函数的呼叫请求,以这种方法调用它们来间接访问硬件。因此,当用户程序想要控制特定的硬件时,它就会产生一个 SVC 异常,然后操作系统提供的 SVC 异常服务例程得到执行,它再调用相关的操作系统函数,后者完成用户程序请求的服务。

另一个相关的异常是 PendSV(可悬起的系统调用),它和 SVC 协同使用。一方面,SVC异常是必须立即得到响应的(若因优先级不比当前正处理的高,或是其它原因使之无法立即响应,将上访成硬 fault——译者注),应用程序执行 SVC 时都是希望所需的请求立即得到响应。另一方面,PendSV 则不同,它是可以像普通的中断一样被悬起的(不像 SVC 那样会上访)。OS 可以利用它“缓期执行”一个异常——直到其它重要的任务完成后才执行动作。悬起 PendSV 的方法是:手工往 NVIC 的 PendSV 悬起寄存器中写 1。悬起后,如果优先级不够高,则将缓期等待执行。

如果一个发生的异常不能被即刻响应,就称它被“悬起”(pending)。不过,少数 fault异常是不允许被悬起的。一个异常被悬起的原因,可能是系统当前正在执行一个更高优先级异常的服务例程,或者因相关掩蔽位的设置导致该异常被除能。对于每个异常源,在被悬起的情况下,都会有一个对应的“悬起状态寄存器”保存其异常请求,直到该异常能够执行为止,这与传统的 ARM 是完全不同的。在以前,是由产生中断的设备保持住请求信号。现在NVIC 的悬起状态寄存器的出现解决了这个问题,即使后来设备已经释放了请求信号,曾经的中断请求也不会错失。

系统任务切换的工程分析

在系统中正常执行的任务(假设没有外部中断IRQ),用Systick直接做上下文切换是完全没有问题的,如图:

但是问题是几乎很少嵌入式的设备会不用其丰富的中断响应,所以,直接用systick做系统的上下文切换那是不实际的,这存在很大的风险,因为假设systick打断了一个中断(IRQ),立即做出上下文切换的话,则触犯用法 fault 异常,除了重启你没有其他办法了,这样子做出来的产品就是垃圾!!用我老板的话说就是写的什么狗屎!!!如图所示:

那这么说这样不行那也不行,怎么办啊?请看看前面接介绍的PendSV,是不是有点豁然开朗了?PendSV 来完美解决这个问题。PendSV 异常会自动延迟上下文切换的请求,直到其它的 ISR 都完成了处理后才放行。为实现这个机制,需要把 PendSV 编程为最低优先级的异常。如果 OS 检测到某 IRQ 正在活动并且被 SysTick 抢占,它将悬起一个 PendSV 异常,以便缓期执行上下文切换。

懂了吗?就是说,只要将PendSV的优先级设为最低的,systick即使是打断了IRQ,它也不会马上进行上下文切换,而是等到IRQ执行完,PendSV 服务例程才开始执行,并且在里面执行上下文切换。过程如图所示:

任务切换的源码实现

过程差不多了解了,那看看FreeRTOS中怎么实现吧!!

FreeRTOS有两种方法触发任务切换:

  1. 一种就是systick触发PendSV异常,这是最经常使用的。
  2. 另一种是主动进行切换任务,执行系统调用,比如普通任务可以使用taskYIELD()强制任务切换,中断服务程序中使用portYIELD_FROM_ISR()强制任务切换。

第一种

先说说第一种吧,就在systick中断中调用xPortSysTickHandler();

下面是源码:

void xPortSysTickHandler( void )
{
 vPortRaiseBASEPRI();
 {
 /* Increment the RTOS tick. */
 if( xTaskIncrementTick() != pdFALSE )
 {
 /* A context switch is required. Context switching is performed in
 the PendSV interrupt. Pend the PendSV interrupt. */
 portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
 }
 }
 vPortClearBASEPRIFromISR();
}

它的执行过程是这样子的,屏蔽所有中断,因为SysTick以最低的中断优先级运行,所以当这个中断执行时所有中断必须被屏蔽。vPortRaiseBASEPRI();就是屏蔽所有中断的。而且并不需要保存本次中断的值,因为systick的中断优先级是已知的,执行完直接恢复所有中断即可。

在xTaskIncrementTick()中会对tick的计数值进行自加,然后检查有没有处于就绪态的最优先级任务,如果有,则返回非零值,然后表示需要进行任务切换,而并非马上进行任务切换,此处要注意,它只是向中断状态寄存器bit28位写入1,只是将PendSV挂起,假如没有比PendSV更高优先级的中断,它才会进入PendSV中断服务函数进行任务切换。

#define portNVIC_PENDSVSET_BIT ( 1UL << 28UL )

然后解除屏蔽所有中断。

vPortClearBASEPRIFromISR();

第二种

另一种方法是主动进行任务切换,不管是使用taskYIELD()还是portYIELDFROMISR(),最终都会执行下面的代码:

#define portYIELD() \
{ \
 /* Set a PendSV to request a context switch. */ \
 portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \ 
 __dsb( portSY_FULL_READ_WRITE ); \
 __isb( portSY_FULL_READ_WRITE ); \
}

这portYIELD()其实是一个宏定义来的。同样是向中断状态寄存器bit28位写入1,将PendSV挂起,然后等待任务的切换。

具体的任务切换源码

一直在说怎么进行任务切换的,好像还没看到任务切换的源码啊,哎,下面来看看任务切换的真面目!!

__asm void xPortPendSVHandler(void)
{
 extern uxCriticalNesting;
 extern pxCurrentTCB;
 extern vTaskSwitchContext;
 PRESERVE8
 mrs r0, psp
 isb
 ldr r3, =pxCurrentTCB /* Get the location of the current TCB. */
 ldr r2, [r3]
 stmdb r0!, {r4-r11} /* Save the remaining registers. */
 str r0, [r2] /* Save the new top of stack into the first member of the TCB. */
 stmdb sp!, {r3, r14}
 mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
 msr basepri, r0
 dsb
 isb
 bl vTaskSwitchContext
 mov r0, #0
 msr basepri, r0
 ldmia sp!, {r3, r14}
 ldr r1, [r3]
 ldr r0, [r1] /* The first item in pxCurrentTCB is the task top of stack. */
 ldmia r0!, {r4-r11} /* Pop the registers and the critical nesting count. */
 msr psp, r0
 isb
 bx r14
 nop
}

不是我不想看,是我看到汇编就头大啊,这几天我也在看源码,实在是头大。

找到核心的函数看看就好啦,不管那么多,有兴趣的可以研究一下中断代码,有不懂的也很欢迎你们来问我,一起研究研究,也是不错的选择。

下面是看重点的地方了:

mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0

这两句代码是关闭中断的。关中断就得干活了,嘿嘿嘿~

bl vTaskSwitchContext

BL是跳转指令嘛,这个我还是有点懂的。

调用函数vTaskSwitchContext(),寻找新的任务运行,通过使变量pxCurrentTCB指向新的任务来实现任务切换,然后就是打开中断,退出去了。

寻找下一个要运行任务

是不是感觉没什么大不了的样子,如果你是这样子觉得的,可能还没学到家,赶紧去看看FreeRTOS的源码,在config.h配置文件中是不是有一个叫做硬件查找下一个运行的任务呢?
configUSE_PORT_OPTIMISED_TASK_SELECTION,这个在FreeRTOS中叫做特殊方法,其实也是硬件查找啦,但是并不是每种单片机都支持的,如果是不支持的话,只能选择软件查找的方法了,就是所谓的通用方法。通用方法我就不多说了,因为我用的是STM32,他是支持硬件方法的,这样子效率更高,所以我也没必要去研究他的软件方法,假如有兴趣的小伙伴可以研读一下源码,有不懂的可以向我提问,源码如下:

#define taskSELECT_HIGHEST_PRIORITY_TASK() \
 { \
 UBaseType_t uxTopPriority = uxTopReadyPriority; \
 \
 /* Find the highest priority queue that contains ready tasks. */ \
 while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) ) \
 { \
 configASSERT( uxTopPriority ); \
 --uxTopPriority; \
 } \
 \
 /* listGET_OWNER_OF_NEXT_ENTRY indexes through the list, so the tasks of \
 the same priority get an equal share of the processor time. */ \
 listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) ); \
 uxTopReadyPriority = uxTopPriority; \
 } /* taskSELECT_HIGHEST_PRIORITY_TASK */

而硬件的方法源码则在下面:

 #define taskSELECT_HIGHEST_PRIORITY_TASK() \
 { \
 UBaseType_t uxTopPriority; \
 \
 /* Find the highest priority list that contains ready tasks. */ \
 portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority ); \
 configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0 ); \
 listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) ); \
 } /* taskSELECT_HIGHEST_PRIORITY_TASK() */

其方法是利用硬件提供的计算前导零指令CLZ,具体宏定义为:

#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )

静态变量uxTopReadyPriority包含了处于就绪态任务的最高优先级的信息,因为FreeRTOS运行的永远是处于最高优先级的运行态,而下个处于最高优先级的就绪态则必定会在下次任务切换的时候运行,uxTopReadyPriority使用每一位来表示任务是否处于就绪态,比如变量uxTopReadyPriority的bit0为1,则表示存在优先级为0的任务处于就绪态,bit6为1则表示存在优先级为6的任务处于就绪态。并且,由于bit0的优先级高于bit6,那么下个任务就是bit0的任务运行了(数组越低优先级越高)。由于32位整形数最多只有32位,因此使用这种特殊方法限定最大可用优先级数目为32,即优先级0~31。得到了下个处于最高优先级就绪态任务了,就调用
listGET_OWNER_OF_NEXT_ENTRY来获取下一个任务的列表项,然后将该列表项的任务控制块TCB赋值给pxCurrentTCB,那么我们就得到下一个要运行的任务了。

至此,任务切换已经完成。

END

关注我

更多资料欢迎关注“物联网IoT开发”公众号

相关推荐

NUS邵林团队发布DexSinGrasp基于强化学习实现物体分离与抓取统一

本文的作者均来自新加坡国立大学LinSLab。本文的共同第一作者为新加坡国立大学实习生许立昕和博士生刘子轩,主要研究方向为机器人学习和灵巧操纵,其余作者分别为硕士生桂哲玮、实习生郭京翔、江泽宇以及...

「PLC进阶」如何通过编写SCL语言程序实现物料分拣?

01、前言SCL作为IEC61131-3编程语言的一种,由于其高级语言的特性,特别适合复杂运算、复杂数学函数应用的场合。本文以FactoryIO软件中的物料分拣案例作为硬件基础,介绍如何通过SCL来实...

zk源码—5.请求的处理过程一(http1.1请求方法)

大纲1.服务器的请求处理链...

自己动手从0开始实现一个分布式 RPC 框架

前言为什么要自己写一个RPC框架,我觉得从个人成长上说,如果一个程序员能清楚的了解RPC框架所具备的要素,掌握RPC框架中涉及的服务注册发现、负载均衡、序列化协议、RPC通信协议、Socket通信、异...

MLSys’25 | 极低内存消耗:用SGD的内存成本实现AdamW的优化性能

AIxiv专栏是机器之心发布学术、技术内容的栏目。过去数年,机器之心AIxiv专栏接收报道了2000多篇内容,覆盖全球各大高校与企业的顶级实验室,有效促进了学术交流与传播。如果您有优秀的工作想要分享,...

线程池误用导致系统假死(线程池会自动销毁吗)

背景介绍在项目中,为了提高系统性能使用了RxJava实现异步方案,其中异步线程池是自建的。但是当QPS稍微增大之后却发现系统假死、无响应和返回,调用方出现大量超时现象。但是通过监控发现,系统线程数正常...

大型乘用车工厂布局规划(六大乘用车基地)

乘用车工厂的布局规划直接影响生产效率、物流成本、安全性和未来扩展能力。合理的布局应确保生产流程顺畅、物流高效、资源优化,并符合现代化智能制造和绿色工厂的要求。以下是详细的工厂布局规划要点:1.工厂布...

西门子 S7-200 SMART PLC 连接Factory IO的方法

有很多同学不清楚如何西门子200smart如何连接FactoryIO,本教程为您提供了如何使用西门子S7-200SMARTPLC连接FactoryIO的说明。设置PC和PLC之间的...

西门子博图高级仿真软件的应用(西门子博途软件仿真)

1.博图高级仿真软件(S7-PLCSIMAdvancedV2.0)S7-PLCSIMAdvancedV2.0包含大量仿真功能,通过创建虚拟控制器对S7-1500和ET200SP控制器进行仿真...

PLC编程必踩的6大坑——请对号入座,评论区见

一、缺乏整体规划:面条式代码问题实例:某快递分拣线项目初期未做流程图设计,工程师直接开始编写传送带控制程序。后期增加质检模块时发现I/O地址冲突,电机启停逻辑与传感器信号出现3处死循环,导致项目延期2...

统信UOS无需开发者模式安装软件包
统信UOS无需开发者模式安装软件包

原文链接:统信UOS无需开发者模式安装软件包...

2025-05-05 14:55 csdh11

100个Java工具类之76:数据指纹DigestUtils

为了提高数据安全性,保证数据的完整性和真实性,DigestUtils应运而生。正确恰当地使用DigestUtils的加密算法,可以实现数据的脱敏,防止数据泄露或篡改。...

麒麟KYLINIOS软件仓库搭建02-软件仓库添加新的软件包

#秋日生活打卡季#原文链接:...

Java常用工具类技术文档(java中工具类的作用)

一、概述Java工具类(UtilityClasses)是封装了通用功能的静态方法集合,能够简化代码、提高开发效率。本文整理Java原生及常用第三方库(如ApacheCommons、GoogleG...

软路由的用法(自动追剧配置)(软路由教学)

本内容来源于@什么值得买APP,观点仅代表作者本人|作者:值友98958248861环境和需求...