FreeRTOS开启任务调度

10 篇文章 3 订阅
9 篇文章 1 订阅

前面的系列介绍里List、TCB、创建任务流程及调度的重要变量等。现在从vTaskStartScheduler看看任务是怎么被调度的。

要启动调度之前先要初始化硬件信息,要先设置硬件时钟和中断优先级,和裸机差不多。

#include "InitOS.h"

//初始化硬件
void InitHardware(void)
{
	//从处于预期状态的时钟开始
	RCC_DeInit();
	//启动HSE高速时钟
	RCC_HSEConfig(RCC_HSE_ON);
	//等待时钟就绪
	while (RCC_GetFlagStatus(RCC_FLAG_HSERDY) == RESET)
	{
	}
	//闪存需要 2 个等待状态
	*((unsigned long*)0x40022000) = 0x02;
	//HCLK=SYSCLK
	RCC_HCLKConfig(RCC_SYSCLK_Div1);
	//PCLK2=HCLK
	RCC_PCLK2Config(RCC_HCLK_Div1);
	//PCLK1=HCLK/2
	RCC_PCLK1Config(RCC_HCLK_Div2);
	//PLLCLK=8MHz*9=72MHz
	RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9);
	//使能PLL
	RCC_PLLCmd(ENABLE);
	//等待PLL时钟准备就绪
	while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET)
	{
	}
	//设置PLL时钟为系统时钟源
	RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);
	//等待时钟使用系统时钟源成功
	while (RCC_GetSYSCLKSource() != 0x08)
	{
	}
	//使能GPIOA,GPIOB,GPIOC,GPIOD,GPIOE,AFIO时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC
		| RCC_APB2Periph_GPIOD | RCC_APB2Periph_GPIOE | RCC_APB2Periph_AFIO, ENABLE);
	//使能SPI2外设时钟
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE);
	//设置向量表的基础地址
	NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x0);
	//设置权限组
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
	//设置SysTick的时钟源
	SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK);
	//配置串口中断优先级
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQChannel;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = configLIBRARY_KERNEL_INTERRUPT_PRIORITY;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_InitStructure);

}

初始化硬件后就创建任务,让就绪状态列表有任务

//主函数
int main( void )
{
	//初始化硬件信息
	InitHardware();
	//初始化LED灯硬件环境
  InitLedEnv();
	//初始化蜂鸣器环境
	InitBeepEnv();
	//要注意堆栈大小
	//LED0任务
  xTaskCreate(LED0Task,"LED0",LedTaskStakSize,NULL,LedTaskPriority,NULL);
	//LED1任务
	xTaskCreate(LED1Task,"LED1",LedTaskStakSize,NULL,LedTaskPriority,NULL);
	//LED2任务
	xTaskCreate(LED2Task,"LED2",LedTaskStakSize,NULL,LedTaskPriority,NULL);
	//蜂鸣器任务
	xTaskCreate(BeepTask,"BEEP",BeepTaskStakSize,NULL,BeepTaskPriority,NULL);
	
	//按键任务
	xTaskCreate(KeyTask,"KEY",KeyTaskStakSize,NULL,KeyTaskPriority,NULL);
	//开始任务调度
	vTaskStartScheduler();
	return 0;
}

创建好任务之后就可以启动调度器vTaskStartScheduler()了。调度器里面干了些啥呢让任务能调度起来。

//开启调度器
void vTaskStartScheduler(void)
{
	//返回值
	BaseType_t xReturn;
	//把空闲任务加入最低优先级
	//动态创建空闲任务
	xReturn = xTaskCreate(prvIdleTask, "IDLE", 120, (void*)NULL, 0, &xIdleTaskHandle);

	//如果创建空闲任务成功了就启动调度
	if (xReturn == pdPASS)
	{
		//此处关闭比设置的系统所能管理最高优先级低的中断,以确保在调用xPortStartScheduler()之前或期间不会发生滴答中断
	   //创建的任务包含一个打开中断的状态字
	   //所以中断将在第一个任务时自动重新启用运行
		vPortRaiseBASEPRI();
		//下一个未阻塞的任务时间为0xFFFF_FFFF
		xNextTaskUnblockTime = portMAX_DELAY;
		//标识调度在运行
		xSchedulerRunning = pdTRUE;
		//Tick的计数器为 0
		xTickCount = (TickType_t)0;

		//启动调度
		if (xPortStartScheduler() != pdFALSE)
		{
			//代码逻辑不会走到这里,因为所有任务都是死循环,就算失败也会统一进入失败方法的死循环
		}
		else
		{
			//如果有任务调xTaskEndScheduler会走到这里
		}
	}
	else
	{
	}
}

调度器方法里面首先创建一个空闲任务,空闲任务优先级设置最低,让系统在没任务执行时候有个空闲任务可以执行。

	//把空闲任务加入最低优先级
	//动态创建空闲任务
	xReturn = xTaskCreate(prvIdleTask, "IDLE", 120, (void*)NULL, 0, &xIdleTaskHandle);

如果创建空闲任务成功之后就把比系统管理中断优先级低的中断全部屏蔽了。防止启动调度前发送滴答中断。

//如果创建空闲任务成功了就启动调度
	if (xReturn == pdPASS)
	{
		//此处关闭比设置的系统所能管理最高优先级低的中断,以确保在调用xPortStartScheduler()之前或期间不会发生滴答中断
	  //创建的任务包含一个打开中断的状态字
	  //所以中断将在第一个任务时自动重新启用运行
		vPortRaiseBASEPRI();

然后把下一个出块时间设置最大值,同时标志运行状态变量为运行,初始化系统节拍为0

		//下一个未阻塞的任务时间为0xFFFF_FFFF
		xNextTaskUnblockTime = portMAX_DELAY;
		//标识调度在运行
		xSchedulerRunning = pdTRUE;
		//Tick的计数器为 0
		xTickCount = (TickType_t)0;

然后调用(xPortStartScheduler()启动调度。这里调度器不会退出并不是调度里面是个无限死循环,调度只是引导执行第一个任务,因为所有任务是死循环,所以调度器开始执行任务后不会执行到后续逻辑。就算任务失败退出了死循环也会统一进入系统失败的死循环执行。

		//启动调度
		if (xPortStartScheduler() != pdFALSE)
		{
			//代码逻辑不会走到这里,因为所有任务都是死循环,就算失败也会统一进入失败方法的死循环
		}
		else
		{
			//如果有任务调xTaskEndScheduler会走到这里
		}

xPortStartScheduler()里的逻辑先把PendSV和SysTick中断优先级设置最低级别。OS中断使用最低优先级额。然后调用vPortSetupTimerInterrupt()设置SysTick时钟频率,让他按照我们要求的节拍中断。设置网滴答时钟后滴答这时候还没开启中断额,中断这时候还是禁用状态。然后调用prvStartFirstTask()引导执行第一个任务,在开启第一个任务起码启用中断,让SysTick中断启用,安装设置的频率触发中断函数,中断函数决定是否要切换任务执行。这时候不能开启中段,开启之后就开始滴答了,滴答时候要用MSP主栈执行中断函数,因为启动OS前还有把主栈指针恢复上电状态,避免主栈空间浪费,不恢复倒是没啥事,就是会永久浪费引导OS之前用的主栈空间。

//启动调度器
BaseType_t xPortStartScheduler(void)
{
	//使PendSV和SysTick成为最低优先级的中断。
	portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;
	portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;

	//启动滴答计时器,设置频率。中断在这里被禁用
	vPortSetupTimerInterrupt();

	//初始化为第一个任务准备的关键嵌套计数。
	uxCriticalNesting = 0;

	//开启第一个任务
	prvStartFirstTask();

	//正常执行不到这里
	return 0;
}

设置滴答时钟方法vPortSetupTimerInterrupt()里面就是设置定时器频率,和硬件相关,所以在移植层。按硬件的文档和要的节拍率设置即可,这时候中断还是禁用状态。

//设置systick中断
void vPortSetupTimerInterrupt(void)
{
	//停止并清空SysTick
	portNVIC_SYSTICK_CTRL_REG = 0UL;
	portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;

	//设置SysTick中断到需要取频率
	portNVIC_SYSTICK_LOAD_REG = (configCPU_CLOCK_HZ / configTICK_RATE_HZ) - 1UL;
	portNVIC_SYSTICK_CTRL_REG = (portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT);
}

prvStartFirstTask()方法开启第一个任务执行。这里启动第一个任务不是直接调用任务函数去启动的。从芯片上电就开始执行逻辑,一直跑到启动调度器都是在主栈执行逻辑。这时候主栈可能已经有很多数据,指针也不是在开始位置了。由于马上要执行第一个任务让OS进入多任务死循环了。这时候主栈的数据已经没用了,留着也是占用主栈空间(按我理解不恢复MSP到起始值也行,只是浪费内存)。所以按照向量表推断先把主栈指针恢复到上电的初始时候。后续Systick和PendSV执行的中断逻辑主栈是从干净状态开始。同时这也是一条不归路,前面说启动xPortStartScheduler()方法后面的逻辑不会执行到这也是一个原因。OS接管系统后,调度引导OS用到的主栈执行数据已经不需要了。重置MSP主栈指针之后就开启中断和开启异常,这时候由于屏蔽了中断,开启的SysTick不会被触发额。然后清空一下指令流水线和数据流水线开始触发SVC中断(SVC中断和PendSV中断的差别是SVC马上执行,PendSV会延迟执行,如果有中断运行,PendSV会押后延迟)。SVC中断后续也可以用来在运用程序和OS内存做系统调用使用,任务触发SVC中断调用内核API。然后就进入SVC中断处理函数了,SVC中断会引导执行第一个任务。(这时候SysTick还没执行,已经使能了,由于中断优先级最低,在任务调度前已经屏蔽中断了,所以处于被屏蔽状态)

//开启第一个任务
/*
取MSP的初始值的思路是先根据向量表的位置寄存器VTOR(0xE000ED08)来获取向量表存储的地址;
再根据向量表存储的地址,取出第一个元素__initial_sp,写入 MSP

Cortex-M3 处理器,上电默认进入线程的特权模式,使用 MSP 作为堆栈指针;
从上电跑到这里,经过一系列的函数调用,出栈,入栈,MSP已经不是最开始的初始化的位置;
这里通过 MSR 重新初始化 MSP,丢弃主堆栈中的数据;这是一条不归路,代码跑到这里,不会再返回之前的调用路径。
*/
__asm void prvStartFirstTask(void)
{
	PRESERVE8

		/*使用NVIC偏移寄存器来定位堆栈。*/
		  //地址处为VTOR(向量表偏移量)寄存器,存储向量表起始地址
		  //向量表寄存器地址加载到r0
		ldr r0, =0xE000ED08
		//启动文件中, 最初地址放置的__initial_sp
	  //从向量表寄存器地址把向量表地址读入r0
		ldr r0, [r0]
		//根据向量表实际存储地址,取出向量表中的第一项,向量表第一项存储主堆栈指针MSP的初始值
		ldr r0, [r0]

		/*将__initial_sp的初始值写入MSP中*/
		msr msp, r0
		//cpsie i=开启中断cpsi  enable interrupt
		  //cpsid i关闭中断cpsi  disable interrupt
		cpsie i
		//cpsie f=开启异常cpsi  enable faultmask
		//cpsid f关闭异常cpsi  disable faultmask
		cpsie f
		//数据同步令牌,清除数据流水线
		dsb
		//指令同步令牌,清除指令流水线
		isb
		/*调用SVC中断启动第一个任务。*/
		svc 0
		//等待两个时钟
		nop
		nop
}

SVC中断函数如下,他先把pxCurrentTCB变量的地址加载到r3寄存器。然后读取pxCurrentTCB地址存的值到r1(即pxCurrentTCB指向的TCB地址)。然后读TCB地址的值到r0,TCB地址等于第一个元素地址,也就是pxCurrentTCB任务的栈顶指针。执行到ldr r0, [r1]时候已经把pxCurrentTCB任务的栈顶地址读到r0了。然后基于r0的任务栈顶指针按入栈顺序相反释放r4-r11的值。这时候r0已经是出栈r4-r11后的栈顶位置了。然后把r0值设置到PSP上(用户栈执行,退出中断时候指定芯片按PSP指针执行逻辑)。然后执行isb清除指令流水线,防止指令缓存。r0的栈顶指针值已经放入PSP了,这时候r0可以用来搞别的了。通过mov r0, # 0把0读入r0里面。然后通过msr basepri, r0设置屏蔽优先级为0,放开中断屏蔽,这时候SysTick开始运行,系统节拍每中断一次增加1,达到int最大值溢出后又从0开始。然后执行orr r14, # 0xd使得硬件在退出时使用进程堆栈指针PSP完成出栈操作并返回后进入线程模式、返回Thumb状态。执行bx R14,告诉处理器ISR完成,需要返回,此刻处理器便会进行出栈操作,PC(程序计数器)被我们赋值成为了执行任务的函数的入口(在创建任务时候有个模拟入栈方法在R14入栈位置入栈的是任务的函数入口),也即正式跑起来。

//SVC中断处理函数
//启动调度器后触发SVC异常调用第一个任务
__asm void vPortSVCHandler(void)
{
	//当前运行的任务
	extern pxCurrentTCB;
	PRESERVE8
		//把pxCurrentTCB地址读入r3
		ldr r3, = pxCurrentTCB
		//从pxCurrentTCB读取值到r1
		ldr r1, [r3]
		//读出当前tcb第一个元素,即栈顶位置
		ldr r0, [r1]
		//从当前任务的栈里面弹出r4到r11寄存器的值
		ldmia r0 !, { r4 - r11 }
		//设置当前栈顶位置到psp
		msr psp, r0
		//指令同步令牌,清除指令流水线
		isb
		//读入0到r0
		mov r0, # 0
		//放开中断屏蔽
		msr basepri, r0
		/*在进入异常服务程序后,将自动更新LR的值为特殊的EXC_RETURN。
		这是一个高28位全为1的值,只有[3:0]的值有特殊含义。
		当异常服务例程把这个值送往PC时,就会启动处理器的中断返回序列。
		因为LR的值是由CM3自动设置的,所以只要没有特殊需求,就不要改动它。
		合法的EXC_RETURN值及其功能
		0xFFFF_FFF1 返回handler模式
		0xFFFF_FFF9 返回线程模式,并使用主堆栈(SP=MSP)
		0xFFFF_FFFD 返回线程模式,并使用线程堆栈(SP=PSP)
		*/
		//这个是第一次任务是MSP进来的所以进入异常服务程序后,LR的为 0xFFFF_FFF9
		/*当从SVC中断服务退出前,通过向r14寄存器最后4位按位或上0x0D,
	  使得硬件在退出时使用进程堆栈指针PSP完成出栈操作并返回后进入线程模式、返回Thumb状态 */
		orr r14, # 0xd
		//执行bx R14,告诉处理器ISR完成,需要返回,此刻处理器便会进行出栈操作,PC被我们赋值成为了执行任务的函数的入口,也即正式跑起来
		bx r14
}

在这个逻辑之后系统SysTick时钟按设定频率中断。SysTick函数判断是否要切换任务执行,要切换就触发PendSV,为啥不直接在SysTick里面做任务切换,而要SysTick触发PendSV中断切换。就是因为在SysTick切换可能导致时间延长,SysTick本身被影响,导致计时不准。PendSV是专门的延迟执行的中断,适合做上下文切换。

每个任务都是死循环,在系统滴答下交替使用CPU,这就是FreeRTOS任务调度。

总结就是OS调度离不开硬件中断。中断给了OS调度时机。只要能够保证任务内存数据不被破坏,记住每个任务的堆栈指针和停止时候寄存器值就能保证任务的连续性。说以任务的上下文不是C的某行代码。因为任务逻辑和OS逻辑基本都是C,他们是同等级别,OS的C也不知道任务编译后的机器指令,机器指令是硬件认可的东西。所以OS为了让多个任务共享使用CPU得从硬件角度触发。只要保存住任务堆栈指针和寄存器值即可。所以用到汇编的地方级别是切换任务逻辑,为啥切换逻辑要汇编而不直接C呢。因为C编译的逻辑本身编译的指令可能就用那些寄存器。切换逻辑执行时候就把寄存器值盖了,这样就保存不了完整的上下文了。

调度没有我以前想的那么悬,学习还是可以提高认知的。简单的OS并没达到现在windows和linux那种可怕的复杂度级别。如果哪天老美制裁我们通用OS和复杂芯片都用不了喽,就转行嵌入式,用国产嵌入式芯片,玩RTOS,还不至于因为老美回到蛮荒时代,哈哈

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小乌鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值