哈喽,大家好,这里是自律鸽。写这篇文章的起因是由于我经常水群,在聊天的时候发现很多小伙伴在学习FreeRTOS时不知道从何下手,对为什么学习FreeRTOS也没有很清晰的认知。因此,我今天想用一篇文章带大家快速入门FreeRTOS。
一、什么是FreeRTOS?
正如其名,FreeRTOS是一款“开源免费”的实时操作系统,由美国的Richard Barry于2003年发布,经历了数几十年的更新迭代,它已成为目前市场上占有率最高的RTOS。
二、为什么要学习FreeRTOS?
如果你一直在从事或者开发单片机,你会发现越来越多的单片机会用到譬如FreeRTOS这样的操作系统。在这里,有的小伙伴可能会问了。为什么我们会需要用到操作系统呢?为了解答大家的这个疑惑,在这里我给大家举个小栗子加以说明。
在刚入门学习单片机的时候,我想大家一定做过这种类似的跑马灯或者呼吸灯实验,就是让两个LED灯同时闪烁。(Tips:LED1间隔0.4s闪烁,LED2间隔0.6s闪烁。)
裸机代码实现:
while (1)
{
HAL_GPIO_WritePin(LED1_PORT, LED1_PIN, GPIO_PIN_SET);
HAL_GPIO_WritePin(LED2_PORT, LED2_PIN, GPIO_PIN_SET);
HAL_Delay(200);
HAL_GPIO_WritePin(LED1_PORT, LED1_PIN, GPIO_PIN_RESET);
HAL_Delay(100);
HAL_GPIO_WritePin(LED2_PORT, LED2_PIN, GPIO_PIN_RESET);
HAL_Delay(100);
HAL_GPIO_WritePin(LED1_PORT, LED1_PIN, GPIO_PIN_SET);
HAL_Delay(200);
HAL_GPIO_WritePin(LED1_PORT, LED1_PIN, GPIO_PIN_RESET);
HAL_GPIO_WritePin(LED2_PORT, LED2_PIN, GPIO_PIN_SET);
HAL_Delay(200);
HAL_GPIO_WritePin(LED1_PORT, LED1_PIN, GPIO_PIN_SET);
HAL_Delay(100);
HAL_GPIO_WritePin(LED2_PORT, LED2_PIN, GPIO_PIN_RESET);
HAL_Delay(100);
HAL_GPIO_WritePin(LED1_PORT, LED1_PIN, GPIO_PIN_RESET);
HAL_Delay(200);
}
}
从上述代码可以看出,由于单片机是单核系统,所以想要实现两个LED灯同时闪烁的功能就需要将两个LED闪烁的代码融合在一起。这样带来的后果就是代码的可读性非常差,不利于开发。众所周知,在实际的开发过程中,单片机所接的外设远不止两个LED灯这么简单。
FreeRTOS代码实现:
//创建LED1_Task任务
xTaskCreate(LED1_Task,"LED1",1000,NULL,1,&LED1_Task_Handle);
//创建LED2_Task任务
xTaskCreate(LED2_Task,"LED2",1000,NULL,2,&LED2_Task_Handle);
/*LED2_Task 任务入口函数*/
/*"LED2" 任务名字*/
/*1000 任务栈大小*/
/*NULL 任务入口函数参数*/
/*2 任务优先级*/
/*&LED2_Task_Handle 任务控制块指针*/
// LED1闪烁任务
void LED1_Task(void *pvParameters)
{
while(1)
{
digitalLo(LED1_GPIO_PORT,LED1_GPIO_PIN);
vTaskDelay(200); // 延时200毫秒
digitalHi(LED1_GPIO_PORT,LED1_GPIO_PIN);
vTaskDelay(200); // 延时200毫秒
}
}
// LED2闪烁任务
void LED2_Task(void *pvParameters)
{
while(1)
{
digitalLo(LED2_GPIO_PORT,LED2_GPIO_PIN);
vTaskDelay(300); // 延时300毫秒
digitalHi(LED2_GPIO_PORT,LED2_GPIO_PIN);
vTaskDelay(300); // 延时300毫秒
}
}
从上述代码可以看出,FreeRTOS引入了多线程的概念,两个LED灯闪烁分别对应着两个任务,且两个任务是同时进行的。相比于裸机系统,FreeRTOS的代码是否更简洁且易读呢?答案是肯定的。那么FreeRTOS是如何让单核的单片机系统同时运行两个任务的呢?
三、FreeRTOS的核心知识
(1)任务调度
由上文可知,在利用FreeRTOS实现两个LED闪烁的时候引入了多线程概念,那么这一小节将具体讲解FreeRTOS是如何同时运行不同任务的。
实际上FreeRTOS有自己的任务调度算法。它可以让cpu在不同的任务之间来回切换。即当cpu执行完一段代码后,它会在任务队列中检索是否还有其他任务可以做,如果有,则会去执行其他的任务,执行完后再回到原代码处,以此反复运行。因此,每个任务都可以轮流地享有相同的cpu时间。我们通常把该时间称为时间片。在RTOS中,最小的时间单位为一个tick,即SysTick中断周期。因为研读过RT-Thread源码,所以我知道RT-Thread中是可以指定时间片的大小为多少个tick的。而FreeRTOS的时间片只能为一个tick。
(2)线程交互
除了上述所说的多线程以及任务调度之外,FreeRTOS的另一个亮点在于其线程交互机制。在FreeRTOS中,为了支持线程间的灵活通信,它提供了四种用于线程之间交互的方式:分别是消息队列、信号量、事件、任务通知。今天我主要给大家简单介绍一下消息队列的使用。实验很简单,主要是通过按键发送不同指令实现对不同LED灯的控制。代码如下:
#define QUEUE_LEN 4 /* 队列的长度,最大可包含多少个消息 */
#define QUEUE_SIZE 4 /* 队列中每个消息大小(字节) */
/* 创建Test_Queue */
Test_Queue = xQueueCreate((UBaseType_t ) QUEUE_LEN,/* 消息队列的长度 */
(UBaseType_t ) QUEUE_SIZE);/* 消息的大小 */
/* 创建Receive_Task任务 */
xReturn = xTaskCreate(Receive_Task,"Receive_Task",512,NULL,1,&Receive_Task_Handle);
/* 创建Send_Task任务 */
xReturn = xTaskCreate(Send_Task,"Send_Task",512,NULL,2,&Send_Task_Handle);
/**********************************************************************
* @ 函数名 :Receive_Task
* @ 功能说明:Receive_Task任务主体
* @ 参数 :
* @ 返回值 :无
********************************************************************/
static void Receive_Task(void* parameter)
{
BaseType_t xReturn = pdTRUE;/* 定义一个创建信息返回值,默认为pdTRUE */
uint32_t r_queue; /* 定义一个接收消息的变量 */
while (1)
{
xReturn = xQueueReceive( Test_Queue, /* 消息队列的句柄 */
&r_queue, /* 发送的消息内容 */
portMAX_DELAY); /* 等待时间 一直等 */
if(pdTRUE == xReturn)
if(r_queue==1)
{
LED1_ON;
}
else if(r_queue==2)
{
LED2_ON;
}
else
printf("数据接收出错,错误代码0x%lx\n",xReturn);
}
}
/**********************************************************************
* @ 函数名 :Send_Task
* @ 功能说明:Send_Task任务主体
* @ 参数 :
* @ 返回值 :无
********************************************************************/
static void Send_Task(void* parameter)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
uint32_t send_cmd1 = 1;
uint32_t send_cmd2 = 2;
while (1)
{
if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON )
{/* K1 被按下 */
printf("发送指令1!\n");
xReturn = xQueueSend( Test_Queue, /* 消息队列的句柄 */
&send_cmd1,/* 发送的消息内容 */
0 ); /* 等待时间 0 */
if(pdPASS == xReturn)
printf("指令1发送成功!\n\n");
}
if( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON )
{/* K2 被按下 */
printf("发送指令2!\n");
xReturn = xQueueSend( Test_Queue, /* 消息队列的句柄 */
&send_cmd2,/* 发送的消息内容 */
0 ); /* 等待时间 0 */
if(pdPASS == xReturn)
printf("指令2发送成功!\n\n");
}
vTaskDelay(20);/* 延时20个tick */
}
}
从上述代码可以看出,消息的发送主要是通过函数xQueueSend()实现的。当单片机检测到按键K1按下的时,会通过函数xQueueSend()向消息队列Test_Queue发送消息,而消息的内容会存储在&send_cmd1中,第三个参数0则表示发送消息时阻塞或者等待的时间,即如果队列已满,调用xQueueSend()
的线程将不会等待,而是立即返回错误。如果需要等待队列空闲,可以设置一个非零的阻塞时间。而消息的接收主要是通过函数xQueueReceive()实现的。该函数的核心作用是从指定的消息队列中检索消息,其中这些消息的内容主要存储在&r_queue
所指向的内存位置。值得注意的是,与消息发送机制类似,消息接收也可能面临阻塞或等待的情况。具体而言,当消息队列为空,即当前没有可供接收的消息时,若你仍希望从该队列中接收到消息,那么调用xQueueReceive()
的线程或任务将会进入阻塞状态,等待直至有新的消息被发送到该队列中。一旦有消息到达,原先处于阻塞状态的接收者便能成功接收该消息。
四、总结
完结,撒花。本文主要通过几个常用的例子给大家梳理了一下如何学习FreeRTOS,目的是让大家对FreeRTOS有更加清晰的认知,以便深入了解FreeRTOS的精髓。FreeRTOS的核心在于多线程、任务调度、线程交互。对于初学者来说,只要掌握了这几点,就可以很自信的说自己已经迈入了FreeRTOS的门槛了。
Tips:文章中若有错误的内容,欢迎指出。笔者也一直在研读RTOS的道路中,希望能够共同进步。关注不迷路,谢谢大噶~