IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 嵌入式 -> FreeRTOS的调度器源码分析及系统滴答SysTick -> 正文阅读

[嵌入式]FreeRTOS的调度器源码分析及系统滴答SysTick

1. PendSV系统调用

查遍了C站上所有关于FreeRTOS调度器的分析,发现大家分析完vTaskStartScheduler()之后就戛然而止了,我就会比较迷糊,这个仅开启了调度器的调度,而FreeRTOS是一个实时操作系统,并不能体现出他的实时性在哪里,虽然已经在FreeRTOSConfig.h中设置了configUSE_PREEMPTION等于1,那他是怎么其的作用呢?

project\demo\FreeRTOSConfig.h

?

刚开始对PendSV系统调用还比较陌生,读过很多的资料之后,下面是我的理解:
我们都知道,CPU的内部会有一个系统滴答,也就是SysTick,在我的系统里面设置的是1ms。系统滴答就相当于是CPU的心跳,注意,在每次的系统滴答时,都会产生一次中断,来判断是否有更高的优先级需要处理,如果有,就发生一次上下文切换。那么问题来了,如果每次都产生中断的话,那么如果CPU此时正好正在处理中断程序呢,处于系统的实时性问题,系统滴答同样会产生一次系统时钟的中断来判断是否需要发生一次调度。那么为了能使中断顺利且全部一次完成,就加入了PendSV系统调用。他的目的就是为了能够使中断产生时,避免被系统时钟中断打断产生调度而引入的。

而PendSV就是一次SWI系统调用。我们先看SWI系统调用的结果是什么,SWI系统中断定义在中断向量表中,

?当系统发生SWI中断的时候,就会触发调用FreeRTOS_SWI_Handler函数,而FreeRTOS_SWI_Handler的处理过程如下:

FreeRTOS_SWI_Handler:
	/* Save the context of the current task and select a new task to run. */
	portSAVE_CONTEXT
	LDR R0, vTaskSwitchContextConst
	BLX	R0
	portRESTORE_CONTEXT

vTaskSwitchContextConst: .word vTaskSwitchContext

根据上面的流程,可以知道,

首先它会先保存当前任务的上下文,他会继续调用到vTaskSwitchContext()函数,他是一个C函数,也是调度的关键函数,经过vTaskSwitchContext()后,当前任务已经不是上一个任务而是切换到了优先级最高的新任务,最后,将此时当前新任务的栈pop出来,继续执行,完成任务的切换工作。

void vTaskSwitchContext(void)
{
    ...
    taskCHECK_FOR_STACK_OVERFLOW();

    /* Select a new task to run using either the generic C or portoptimised asm code. */
	taskSELECT_HIGHEST_PRIORITY_TASK();
	traceTASK_SWITCHED_IN();
}

挑出了其中的重要函数,首先检查栈是否溢出,然后,找出系统中优先级最高的任务,FreeRTOS的实时性问题就体现在这个地方,将pxCurrentTCB赋值一个最高优先级的任务等待调度。注意此时并没有发生切换,只是在等待调度,这个就与上面为什么要引入PendSV对应了起来!

2. FreeRTOS任务切换的场合

  • 可以执行的一个系统调用
  • 系统能够滴答定时器(SysTick)中断

首先需要明确的是,FreeRTOS对于不同的架构,其实现方式是不同的。基于STM32的处理过程是?中断控制及状态寄存器ICSR(地址:0xE000_ED04),向ICSR的第28位写入1悬起PendSV(启动PendSV中断),而ARM_CA9的处理方式是,利用系统调用触发一次异常,执行SWI指令。

STM32的处理方式很多文章中都有提到,而这篇文章选择了ARM_CA9的处理方式。

2.1 系统调用

系统调用就是执行FreeRTOS系统提供的相关API函数,比如,任务切换的函数taskYIELD(),或是有些调用了taskYIELD()函数的API函数。

#define taskYIELD()					portYIELD()

#define portYIELD() __asm volatile ( "SWI 0" );

根据系统调用的过程可以看出来,触发了一次SWI中断,之后就会执行上面第一节的函数了。

2.2?系统滴答SysTick

在main函数中,任务创建完以后,就要开启调度器开始调度了,在开始调度器的函数中,有对系统滴答的注册,如下:

BaseType_t xPortStartScheduler(void)
{
    ...
    configSETUP_TICK_INTERRUPT();
    ...
}

它会调用到tick的注册函数,而它是一个宏定义,展开后

#define configSETUP_TICK_INTERRUPT()                                                                                  \
    do                                                                                                                \
    {                                                                                                                 \
        void SystemSetupSystick(uint32_t tickRateHz, void *tickHandler, uint32_t intPriority);                        \
        /* Setup systick with lowest priority */                                                                      \
        SystemSetupSystick(configTICK_RATE_HZ, (void *)FreeRTOS_Tick_Handler, configUNIQUE_INTERRUPT_PRIORITIES - 2); \
    } while (0)

SystemSetupSystick()是关键函数,它注册了系统滴答的中断函数,系统滴答的回调函数是FreeRTOS_Tick_Handler(),注册过程如下:

void SystemSetupSystick(uint32_t tickRateHz, void *tickHandler, uint32_t intPriority)
{
    system_register_irqhandler(GPT1_IRQn, (system_irq_handler_t)(uint32_t)tickHandler, NULL);
    ...
}

tickHandler(),就是我们的关键函数,也就是上面的FreeRTOS_Tick_Handler(),当系统每1ms tick一次,就会触发一次该函数,

void FreeRTOS_Tick_Handler(void)
{
	/* Set interrupt mask before altering scheduler structures.   The tick
	handler runs at the lowest priority, so interrupts cannot already be masked,
	so there is no need to save and restore the current mask value.  It is
	necessary to turn off interrupts in the CPU itself while the ICCPMR is being
	updated. */
	portCPU_IRQ_DISABLE();
	portICCPMR_PRIORITY_MASK_REGISTER = (uint32_t)(configMAX_API_CALL_INTERRUPT_PRIORITY << portPRIORITY_SHIFT);
	__asm volatile("dsb		\n"
								 "isb		\n");
	portCPU_IRQ_ENABLE();

	/* Increment the RTOS tick. */
	if (xTaskIncrementTick() != pdFALSE)
	{
		ulPortYieldRequired = pdTRUE;
	}

	/* Ensure all interrupt priorities are active again. */
	portCLEAR_INTERRUPT_MASK();
	configCLEAR_TICK_INTERRUPT();
}

这个函数很关键的函数就是xTaskIncrementTick(),更新系统的时间,并判断是否有任务到达了唤醒的时间,如果有就加入到就绪链表中,但最重要的任务是判断是否需要发生任务调度,它的返回值就是,若等于pdTRUE表示需要调度,否则不需要。这个很关键。

对STM32的处理方式是将中断控制及状态寄存器ICSR(地址:0xE000_ED04),向ICSR的第28位写入1悬起PendSV(启动PendSV中断),改变它的标志位,等待调度。而ARM_CA9则不大一样了,它只是附给一个全局变量ulPortYieldRequired的值为pdTRUE。但并没有任何调度的痕迹。

当时就很迷惑,搜索了很久才发现它的处理过程如下。

我们搜索ulPortYieldRequired,发现它会在portASM.S中调用到,

    LDR		r1, =ulPortYieldRequired
	LDR		r0, [r1]
	CMP		r0, #0
	BNE		switch_before_exit

switch_before_exit:
	/* A context swtich is to be performed.  Clear the context switch pending
	flag. */
	MOV		r0, #0
	STR		r0, [r1]

	/* Restore used registers, LR-irq and SPSR before saving the context
	to the task stack. */
	POP		{r0-r4, r12}
	CPS		#IRQ_MODE
	POP		{LR}
	MSR		SPSR_cxsf, LR
	POP		{LR}
	portSAVE_CONTEXT

    LDR		R0, vTaskSwitchContextConst
	BLX		R0

    portRESTORE_CONTEXT

首先判断ulPortYieldRequired是否等于0,如果不等于0就会执行下面的switch_before_exit,在switch_before_exit中,终于看到了第一节中分析的函数vTaskSwitchContextConst,找出优先级最高的任务等待调度。

而问题是,他是怎么执行的呢。

FreeRTOS_IRQ_Handler:
    ...
    LDR			r1, =system_irqhandler
	BLX		r1
	POP		{r0-r3, lr}
	ADD		sp, sp, r2
    ...
    LDR		r1, =ulPortYieldRequired
	LDR		r0, [r1]
	CMP		r0, #0
	BNE		switch_before_exit
    ...

看到这段代码应该就会明白了,我们的ulPortYieldRequired是在中断处理过程中得到的执行,也就是,当系统产生中断,中断执行完成之后,就会判断ulPortYieldRequired是否需要产生调度,如果需要就按第一节的过程进行处理。而这个中断恰好就是我们的系统时钟中断。

整体的过程就是,CPU每个tick就会产生一次中断,首先对系统时钟加一,并判断是否需要调度,如果需要产生调度,将ulPortYieldRequired赋值为pdTRUE。之后根据ulPortYieldRequired决定是否执行第一节中的代码。

最后的最后,此时的pxCurrentTCB已经指向了最新需要执行的任务,portSAVE_CONTEXT已经将上一个执行的任务的上下文保存了起来,portRESTORE_CONTEXT就是恢复任务的上下文,这个任务已经切换了,它就是需要执行的最新任务的上下文,当portRESTORE_CONTEXT执行完,就完成了上下文的切换了。

其实产生任务切换的函数有很多,因为FreeRTOS是一个实时操作系统,必须确保系统的实时性问题,调度是很关键的,当我们看到taskYIELD()的时候,就意味着需要调度,分析源代码时,可以关注一下这一点。

  嵌入式 最新文章
基于高精度单片机开发红外测温仪方案
89C51单片机与DAC0832
基于51单片机宠物自动投料喂食器控制系统仿
《痞子衡嵌入式半月刊》 第 68 期
多思计组实验实验七 简单模型机实验
CSC7720
启明智显分享| ESP32学习笔记参考--PWM(脉冲
STM32初探
STM32 总结
【STM32】CubeMX例程四---定时器中断(附工
上一篇文章      下一篇文章      查看所有文章
加:2021-11-25 08:17:11  更:2021-11-25 08:19:27 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 10:51:23-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码