一.概要

FreeRTOS是一个迷你的实时操作系统内核。作为一个轻量级的操作系统,功能包括:任务管理、时间管理、信号量、消息队列、内存管理、记录功能、软件定时器、协程等,可基本满足较小系统的需要。
由于RTOS需占用一定的系统资源(尤其是RAM资源),只有μC/OS-II、embOS、salvo、FreeRTOS等少数实时操作系统能在小RAM单片机上运行。相对μC/OS-II、embOS等商业操作系统,FreeRTOS操作系统是完全免费的操作系统,具有源码公开、可移植、可裁减、调度策略灵活的特点,可以方便地移植到各种单片机上运行。

二.什么是实时操作系统

操作系统是一个控制程序,作为硬件和应用程序之间的桥梁,主要是和硬件打交道,负责协调分配计算资源和内存资源给不同的应用程序使用,并防止系统出现故障。面对来自不同应用程序的大量且互相竞争的资源请求,操作系统通过一个调度算法和内存管理算法尽可能把资源公平且有效率地分配给不同的程序。应用程序则通过调用操作系统提供的API接口获得相应资源完成指定的任务。

实时操作系统(RTOS-Real Time Operating System)中实时(Real Time)指的是任务(Task)或者说实现一个功能的线程(Thread)必须在给定的时间(Deadline)内完成。

三.FreeRTOS的特性

具有抢占式或者合作式的实时操作系统内核
功能可裁剪,最小占用10kB左右rom空间,0.5kB ram空间
灵活的任务优先级分配
具有低功耗模式
有互斥锁、信号量、消息队列等功能
运行过程可追踪
支持中断嵌套

四.FreeRTOS的任务详解

FreeRTOS的核心是任务调度器(Task Scheduler),它负责按照一定的调度策略将任务分配给处理器执行。每个任务都是一个独立的函数,可以有不同的优先级和堆栈大小。任务调度器根据任务的优先级和调度策略决定哪个任务被执行。

下图就是任务调度简单介绍
在这里插入图片描述

下面代码就是个简单的示例代码,通过调用osThreadCreate函数创建了两个任务LED_Thread1(驱动LED灯灭)和LED_Thread2(驱动LED灯亮)。在main函数中,通过调用vTaskStartScheduler函数启动了实时系统,使得任务可以被调度执行,由于两个任务都能不断运行,所以能驱动LED灯闪烁。

//LED1任务函数 
void LED_Thread1(void *pvParameters)
{
    while(1)
    {
      gpio_bit_set(GPIOB, GPIO_PIN_4);//PB4输出高电平
      vTaskDelay(100);
    }
} 

//LED2任务函数 
void LED_Thread2(void *pvParameters)
{
    while(1)
    {
	  gpio_bit_reset(GPIOB, GPIO_PIN_4);//PB4输出低电平
      vTaskDelay(250);
    }
}  

1.任务函数定义

无论采用何种方法创建任务,均需要用到任务函数。FreeRTOS 规定任务函数的返回值必须为void,而且带有一个void型指针参数。

void LED_Thread1(void *pvParameters)
{
    while(1)
    {
      gpio_bit_set(GPIOB, GPIO_PIN_4);//PB4输出高电平
      vTaskDelay(100);
    }
} 

GD32F407VET6单片机只有一个内核,那怎么让多个人同时干活呢?其实每个子任务虽然都是死循环,但并不是每个子任务一直都在执行,每个子任务在执行期间,可能需要延时,这边就通过 vTaskDelay(100/ portTICK_RATE_MS),等待100ms,单片机就可以停止此任务,然后切换到其它任务执行,这样看起来就是多个人在同时干活了。

2.任务的创建

xTaskCreate((TaskFunction_t )LED_Thread1,
(const char* )“led1_task”,
(uint16_t )LED1_STK_SIZE,
(void* )NULL,
(UBaseType_t )LED1_TASK_PRIO,
(TaskHandle_t* )&LED1Task_Handler);//创建一个任务,任务名,任务函数,优先级,堆栈大小

xTaskCreate函数中的第1个参数(LED_Thread1)是任务函数:
任务函数是任务执行的代码,它应该是一个无返回值的函数,其参数是一个可选的指针,可以用来传递任何需要的数据,我们代码中的任务就是控制GPIO输出高或者低电平。

xTaskCreate函数中的第2个参数(“led1_task”)是任务名称:
这是一个字符串。

xTaskCreate函数中的第3个参数(LED1_STK_SIZE)是任务栈大小定义:
任务栈大小指定了任务可以使用多少RAM来存储局部变量和其他临时数据。这个大小应该足够大,能够容纳任务在执行期间可能出现的最大需求,一般代表多个字,如果是50,代表200字节,一般我们可以填100。

xTaskCreate函数中的第4个参数(NULL)是指向任务参数的指针:
这个参数可以是任何类型,它将传递给任务。

xTaskCreate函数中的第5个参数(LED1_TASK_PRIO)是任务优先级:
任务优先级决定了任务在操作系统调度下的执行顺序。数值越大,优先级越高。

xTaskCreate函数的第6个参数(LED1Task_Handler)是任务句柄:
任务句柄是一个指针,可以用来引用已经创建的任务,以便可以在任务创建后对其进行操作,例如删除、挂起、恢复等。

3.任务的调度原理

FreeRTOS默认使用抢占式调度策略,对同等优先级的任务使用时间片轮询调度,时间片轮询就是可轮流享有相同的单片机时间(可设置),一个时间片等于SysTick中断周期。
抢占式调度是指调度器始终运行优先级最高且处于可运行状态的任务,无论任务何时可以运行。如在中断服务函数中更改了优先级最高且可运行的任务,调度器会停止当前执行的低优先级任务,并启动高优先级任务。
刚才程序中的vTaskDelay是通过将当前任务加入到延时列表中,并设置一个定时器来在指定的延时时间,延时时间过去之后将任务从延时列表中移除并将其设置为就绪状态。这样当定时器触发时,任务重新被加入到就绪列表中,等待被调度器再次调度执行。

下面代码截图就是操作系统底层寻找就绪任务的最高优先级,获取优先级最高的就绪任务的 TCB(任务控制块),然后更新到 pxCurrentTCB ,当前运行的任务只可能有一个,因此pxCurrentTCB只是单个TCB_t指针,它始终指向当前运行的任务。通过xPortPendSVHandler(PendSV_Handler)函数实现调度。
PendSV_Handler 是 ARM Cortex-M 处理器中的一个特殊的中断处理函数,用于处理挂起 PendSV(Pending Supervisor Call)中断。PendSV 中断是 Cortex-M 架构中的一种特殊的软件中断,它可以用来实现任务切换或者其他与系统调度相关的操作。
PendSV_Handler 在中断向量表的位置
在这里插入图片描述

__asm void xPortPendSVHandler( void )
{
	extern uxCriticalNesting;
	extern pxCurrentTCB;
	extern vTaskSwitchContext;

	PRESERVE8

	mrs r0, psp
	isb
	/* Get the location of the current TCB. */
	ldr	r3, =pxCurrentTCB//获取当前任务
	ldr	r2, [r3]

	/* Is the task using the FPU context?  If so, push high vfp registers. */
	tst r14, #0x10
	it eq
	vstmdbeq r0!, {s16-s31}

	/* Save the core registers. */
	stmdb r0!, {r4-r11, r14}

	/* 保存当前任务上下文到堆栈 */
	str r0, [r2]

	stmdb sp!, {r0, r3}
	mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
	msr basepri, r0
	dsb
	isb
	bl vTaskSwitchContext//找到目前最高优先级的任务
	mov r0, #0
	msr basepri, r0
	ldmia sp!, {r0, r3}

	/* The first item in pxCurrentTCB is the task top of stack. */
	ldr r1, [r3]
	ldr r0, [r1]

	/* Pop the core registers. */
	ldmia r0!, {r4-r11, r14}

	/* Is the task using the FPU context?  If so, pop the high vfp registers
	too. */
	tst r14, #0x10
	it eq
	vldmiaeq r0!, {s16-s31}

	msr psp, r0
	isb
	#ifdef WORKAROUND_PMU_CM001 /* XMC4000 specific errata */
		#if WORKAROUND_PMU_CM001 == 1
			push { r14 }
			pop { pc }
			nop
		#endif
	#endif

	bx r14//跳转到最高优先级的任务
}

五.FreeRTOS系统移植到GD32F407VET6单片机

1.硬件准备

STLINK接GD32F407VET6开发板,STLINK接电脑USB口。

在这里插入图片描述

2.程序移植

FreeRTOS主要文件介绍:

port.c:这是一个示例端口文件,用于 Cortex-M4 微控制器。你需要根据你的处理器架构修改这个文件。
portmacro.h:这个文件包含硬件抽象层代码,也需要根据你的处理器架构修改。
FreeRTOSConfig.h:这个文件包含了 FreeRTOS 的配置选项,你可能需要修改这个文件来满足你的项目需求。
tasks.c, queue.c, list.c:这些是(kernel核心)文件,它们包含了FreeRTOS核心的任务管理和同步机制实现。
heap_2.c:这个是内存管理方法,它是一个小型的内存分配器,用于分配和释放固定大小的块内存。
timers.c:实现了定时器的所有功能,包括定时器的创建、删除、启动和停止等。
routine.c:这个文件包含了FreeRTOS的协程(或者称为微任务)的实现。

添加Freertos相关文件

添加FreeRTOS文件夹下面的所有文件到工程文件目录下
在这里插入图片描述

FreeRTOS文件夹下面的所有文件可以官网下载,也可以从我们例程上拷贝
在这里插入图片描述

打开Keil5工程,创建FreeRTOS文件夹,添加FreeRTOS文件到工程。
在这里插入图片描述

删除原先中断文件中的三个中断服务程序,操作系统中已经对这三个文件重新定义了
SysTick_Handler,SVC_Handler,PendSV_Handler。

#define vPortSVCHandler SVC_Handler
#define xPortPendSVHandler PendSV_Handler
void SysTick_Handler(void)
{
	if(xTaskGetSchedulerState()!=taskSCHEDULER_NOT_STARTED)//系统已经运行
	{
		xPortSysTickHandler();
	}
}

main.c中添加任务函数代码以及创建任务代码。

//开始任务任务函数
void start_task(void *pvParameters)
{
    taskENTER_CRITICAL();           //进入临界区
    //创建LED1任务
    xTaskCreate((TaskFunction_t )LED_Thread1,     	
                (const char*    )"led1_task",   	
                (uint16_t       )LED1_STK_SIZE, 
                (void*          )NULL,				
                (UBaseType_t    )LED1_TASK_PRIO,	
                (TaskHandle_t*  )&LED1Task_Handler);   


	    xTaskCreate((TaskFunction_t )LED_Thread2,     	
                (const char*    )"led2_task",   	
                (uint16_t       )LED2_STK_SIZE, 
                (void*          )NULL,				
                (UBaseType_t    )LED2_TASK_PRIO,	
                (TaskHandle_t*  )&LED2Task_Handler);   
													
							
    vTaskDelete(StartTask_Handler); //删除开始任务
    taskEXIT_CRITICAL();            //退出临界区

}

//LED1任务函数 
void LED_Thread1(void *pvParameters)
{
    while(1)
    {
      gpio_bit_set(GPIOB, GPIO_PIN_4);//PB4输出高电平
      vTaskDelay(100);
    }
} 

//LED2任务函数 
void LED_Thread2(void *pvParameters)
{
    while(1)
    {
			gpio_bit_reset(GPIOB, GPIO_PIN_4);//PB4输出低电平
      vTaskDelay(250);
    }
}  
int main(void)
{ 
	systick_config();//配置系统主频168M,外部8M晶振,配置在#define __SYSTEM_CLOCK_168M_PLL_8M_HXTAL        (uint32_t)(168000000)	
	rcu_periph_clock_enable(RCU_GPIOB);//使能GPIOB时钟
	gpio_mode_set(GPIOB, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO_PIN_4);//PB4配置成输出
	gpio_output_options_set(GPIOB, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_4);//PB4配置成推挽输出,50M速度	
	
	//创建开始任务
	xTaskCreate((TaskFunction_t )start_task,            //任务函数
							(const char*    )"start_task",          //任务名称
							(uint16_t       )START_STK_SIZE,        //任务堆栈大小
							(void*          )NULL,                  //传递给任务函数的参数
							(UBaseType_t    )START_TASK_PRIO,       //任务优先级
							(TaskHandle_t*  )&StartTask_Handler);   //任务句柄   
							
	vTaskStartScheduler();          //开启任务调度
}

3.调试FreeRTOS任务调度

调试代码,能更好理解任务调度。

任务1里设置断点,能跑进任务1,如下图所示。
在这里插入图片描述

任务2里设置断点,能跑进任务2,如下图所示。
在这里插入图片描述

pxCurrentTCB保存当前运行任务
在这里插入图片描述

任务切换后,pxCurrentTCB保存将要运行的任务。
在这里插入图片描述

六.工程源代码下载

源代码下载链接:
CSDN

七.小结

FreeRTOS实时操作系统能使在GD32单片机软件开发中,程序结构清晰,单片机执行效率提升许多,在多个任务模块的代码中,可以考虑使用FreeRTOS。

Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐