目录
1简介
FreeRTOS(Free Real-Time Operating System)是一个专门面向微控制器和小型微处理器的实时操作系统。作为一个轻量级的操作系统,它的内核仅需要几个C文件就能完成核心功能,这种精简的特性使其非常适合资源受限的嵌入式系统。
FreeRTOS主要是用于多任务管理,任务间通讯,中断控制,内存管理。
2多任务管理
使用操作系统就是为了将各个任务的调度交给操作系统,开发人员只需专注于任务功能的实现,不必过多关心任务之间的兼容问题。
任务的创建
xTaskCreate()使用动态内存的方式创建一个任务。
ret = xTaskCreate((TaskFunction_t) master_task_main, /* 任务入口函数 */
“MASTER”, /* 任务名字 */
64*1024, /* 任务栈大小 */
NULL, ,/* 任务入口函数参数 */
TASK_PRIORITY_NORMAL, /* 任务的优先级 */
&task_master_handler); /* 任务控制块指针 */
任务的删除
void vTaskDelete( TaskHandle_t xTaskToDelete )
vTaskDelete(任务句柄),传入要删除的任务句柄,传入NULL删除当前运行的任务。、
任务挂起
void vTaskSuspend( TaskHandle_t xTaskToSuspend )
vTaskSuspend(),挂起任务,类似暂停,可以恢复,传入NULL表示挂起当前任务。
需要将宏INCLUDE_vTaskSuspend配置为1。
任务恢复
void vTaskResume( TaskHandle_t xTaskToResume )
vTaskResume(),恢复挂起的任务,使其可以继续执行。无论任务被挂起多少次,只需要调用vTaskResume一次即可恢复。
3任务调度
3.1调度算法
FreeRTOS中调度算法:优先级抢占式调度算法,时间片轮转。
优先级抢占式调度算法
算法原理:任务优先级越高,被调度的机会就越大,任务优先级相同时,采用时间片轮转调度。
优点:可以满足实时系统的响应时间要求,可以实现任务的优先级管理。
缺点:低优先级任务可能会被长时间阻塞。
在FreeRTOS中,优先级数字越小。优先级越低。
时间片轮转调度算法
算法原理:每个任务被分配一个时间片,时间片用完后,任务被挂起,等待下一次调度。
优点:公平地分配CPU时间片,可以避免低优先级任务长时间阻塞。
缺点:无法满足实时系统的响应时间要求。
时间片调度
时间片调度是指将CPU的执行时间划分为固定长度的时间片,每个任务在一个时间片内执行一定的指令。当多个优先级相同的任务处于就绪状态时,它们将通过时间片调度方式轮流执行。
同优先级的情况下,我们一般是后创建的那个任务先执行。(首次执行的情况)
默认情况下采用优先级调度和时间片调度
当由多个优先级相同的任务处于就绪状态时,它们将通过时间片调度方式轮流执行。当由较高优先级的任务处于就绪状态时,高优先级任务会被优先执行而不受时间片调度的限制。
3.2开启任务调度
当任务创建成功后处于就绪状态(Ready),在就绪态的任务可以参与操作系统的调度。操作系统任务调度器只启动一次,之后就不会再次执行了,FreeRTOS 中启动任务调度器的函数是 vTaskStartScheduler(),并且启动任务调度器的时候就不会返回,从此任务管理都由FreeRTOS 管理,此时才是真正进入实时操作系统中的第一步。
vTaskStartScheduler开启调度时,顺便会创建空闲任务和定时器任务。
void vTaskStartScheduler( void )
{
BaseType_t xReturn;
/* Add the idle task at the lowest priority. */
#if ( INCLUDE_xTaskGetIdleTaskHandle == 1 )
{
/* Create the idle task, storing its handle in xIdleTaskHandle so it can
be returned by the xTaskGetIdleTaskHandle() function. */
//创建空闲任务
xReturn = xTaskCreate( prvIdleTask, "IDLE", tskIDLE_STACK_SIZE, ( void * ) NULL, ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), &xIdleTaskHandle ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */
}
#else
{
/* Create the idle task without storing its handle. */
xReturn = xTaskCreate( prvIdleTask, "IDLE", tskIDLE_STACK_SIZE, ( void * ) NULL, ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), NULL ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */
}
#endif /* INCLUDE_xTaskGetIdleTaskHandle */
#if ( configUSE_TIMERS == 1 )
{
if( xReturn == pdPASS )
{
//创建定时器task,接收开始、结束定时器等命令
xReturn = xTimerCreateTimerTask();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_TIMERS */
if( xReturn == pdPASS )
{
/* Interrupts are turned off here, to ensure a tick does not occur
before or during the call to xPortStartScheduler(). The stacks of
the created tasks contain a status word with interrupts switched on
so interrupts will automatically get re-enabled when the first task
starts to run. */
portDISABLE_INTERRUPTS();
#if ( configUSE_NEWLIB_REENTRANT == 1 )
{
/* Switch Newlib's _impure_ptr variable to point to the _reent
structure specific to the task that will run first. */
_impure_ptr = &( pxCurrentTCB->xNewLib_reent );
}
#endif /* configUSE_NEWLIB_REENTRANT */
xSchedulerRunning = pdTRUE;
xTickCount = ( TickType_t ) 0U;
/* If configGENERATE_RUN_TIME_STATS is defined then the following
macro must be defined to configure the timer/counter used to generate
the run time counter time base. */
portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();
/* Setting up the timer tick is hardware specific and thus in the
portable interface. */
if( xPortStartScheduler() != pdFALSE )
{
/* Should not reach here as if the scheduler is running the
function will not return. */
}
else
{
/* Should only reach here if a task calls xTaskEndScheduler(). */
}
}
else
{
/* This line will only be reached if the kernel could not be started,
because there was not enough FreeRTOS heap to create the idle task
or the timer task. */
configASSERT( xReturn );
}
}
创建完任务的时候,vTaskStartScheduler开启调度器,空闲任务、定时器任务也是在开启调度函数中实现的。
为什么要空闲任务?因为 FreeRTOS一旦启动,就必须要保证系统中每时每刻都有一个任务处于运行态(Runing),并且空闲任务不可以被挂起与删除,空闲任务的优先级是最低的,以便系统中其他任务能随时抢占空闲任务的 CPU 使用权。这些都是系统必要的东西,也无需自己实现。
最后调用xPortStartScheduler()开启调度。
3.3FreeRTOS启动方式
FreeRTOS有两种启动方式,效果一样,看个人喜好。
第一种:main 函数中将硬件初始化, RTOS 系统初始化,所有任务的创建完成,最后一步开启调度。目前看到的几个芯片SDK都是这种方式。
第二种:main 函数中将硬件和 RTOS 系统先初始化好,只创建一个任务后就启动调度器,然后在这个任务里面创建其它应用任务,当所有任务都创建成功后,启动任务再把自己删除。
4任务间的通信
4.1消息队列
队列(Queue)是最基础和使用最广泛的通信机制。它允许任务和中断将数据放入队列尾部,其他任务可以从队列头部获取数据。队列支持多个发送者和接收者,可以设置阻塞超时时间,使任务在等待数据时进入阻塞状态以节省CPU资源。队列还内置了优先级继承机制,可以有效防止优先级反转问题。
// 创建队列
QueueHandle_t xQueue = xQueueCreate(5, sizeof(uint32_t));
// 发送数据
uint32_t value = 100;
xQueueSend(xQueue, &value, portMAX_DELAY);
// 接收数据
uint32_t receivedValue;
if(xQueueReceive(xQueue, &receivedValue, portMAX_DELAY) == pdTRUE) {
// 处理接收到的数据
}
特点:
支持多个发送者和接收者
可以设置阻塞超时时间
支持优先级继承以避免优先级反转
4.2信号量
信号量(Semaphore)分为二值信号量和计数信号量两种。二值信号量类似于互斥量,主要用于简单的同步或互斥访问。它只有两种状态:可用和不可用。计数信号量则可以有多个计数值,常用于管理资源池或追踪事件发生的次数。比如,当管理一个有限的资源池时,可以创建一个初始值等于资源数量的计数信号量。
二值信号量
// 创建二值信号量
SemaphoreHandle_t xSemaphore = xSemaphoreCreateBinary();
// 获取信号量
if(xSemaphoreTake(xSemaphore, portMAX_DELAY) == pdTRUE) {
// 访问共享资源
xSemaphoreGive(xSemaphore); // 释放信号量
}
计数信号量
// 创建计数信号量,最大计数为5
SemaphoreHandle_t xSemaphore = xSemaphoreCreateCounting(5, 0);
// 示例:用于资源池管理
void vResourceUser(void *pvParameters) {
while(1) {
if(xSemaphoreTake(xSemaphore, portMAX_DELAY) == pdTRUE) {
// 使用资源
xSemaphoreGive(xSemaphore); // 归还资源
}
}
}
4.3事件组
事件组(Event Groups)提供了一种多事件同步机制。每个事件组包含多个事件位,任务可以等待一个或多个事件位被设置。这在需要等待多个条件都满足时特别有用。例如,一个数据处理任务可能需要等待传感器数据和配置数据都准备好才能开始处理。事件组支持设置、清除和等待多个事件位,还可以选择是等待所有事件都发生还是任意一个事件发生。
// 创建事件组
EventGroupHandle_t xEventGroup = xEventGroupCreate();
// 定义事件位
#define EVENT_TEMP_READY (1 << 0)
#define EVENT_HUMID_READY (1 << 1)
#define ALL_EVENTS (EVENT_TEMP_READY | EVENT_HUMID_READY)
// 设置事件
xEventGroupSetBits(xEventGroup, EVENT_TEMP_READY);
// 等待多个事件
EventBits_t uxBits = xEventGroupWaitBits(
xEventGroup, // 事件组句柄
ALL_EVENTS, // 等待的位
pdTRUE, // 清除这些位
pdTRUE, // 等待所有位
portMAX_DELAY // 无限等待
);
4.4消息缓冲区
消息缓冲区(Message Buffer)专门用于处理变长数据的传输。与队列不同,消息缓冲区不要求固定大小的数据项,而是提供了类似流式处理的接口。这对于处理串行数据流或网络数据包等场景特别有用。消息缓冲区在发送数据时会同时传输数据长度信息,接收方可以准确知道每条消息的大小。
// 创建消息缓冲区
MessageBufferHandle_t xMessageBuffer = xMessageBufferCreate(100);
// 发送数据
const char *message = "Hello World";
size_t xBytesSent = xMessageBufferSend(
xMessageBuffer,
(void *)message,
strlen(message),
portMAX_DELAY
);
// 接收数据
char receiveBuffer[20];
size_t xBytesReceived = xMessageBufferReceive(
xMessageBuffer,
receiveBuffer,
sizeof(receiveBuffer),
portMAX_DELAY
);
4.5流缓冲区
流缓冲区(Stream Buffer)与消息缓冲区类似,但更适合处理连续的字节流数据。它不会在数据中添加长度信息,而是直接传输原始数据。这使得流缓冲区特别适合处理需要连续传输的数据流,如串行通信数据。
// 创建流缓冲区
StreamBufferHandle_t xStreamBuffer = xStreamBufferCreate(100, 20);
// 发送数据
uint8_t data[] = {0x01, 0x02, 0x03};
size_t xBytesSent = xStreamBufferSend(
xStreamBuffer,
data,
sizeof(data),
portMAX_DELAY
);
// 接收数据
uint8_t receiveBuffer[10];
size_t xBytesReceived = xStreamBufferReceive(
xStreamBuffer,
receiveBuffer,
sizeof(receiveBuffer),
portMAX_DELAY
);
4.6任务通知
任务通知(Task Notification)是一种轻量级的任务间通信机制。每个任务都有一个32位的通知值,其他任务可以直接修改这个值来实现通信。任务通知的性能优于队列和信号量,因为它不需要创建额外的内核对象。它特别适合简单的一对一通信场景。
// 发送任务通知
xTaskNotify(xTargetTask, 0x01, eSetBits);
// 等待任务通知
uint32_t ulNotificationValue;
if(xTaskNotifyWait(0x00, 0xFFFFFFFF, &ulNotificationValue, portMAX_DELAY) == pdTRUE) {
// 处理通知
}
4.7通信机制的选择
在选择通信机制时,需要考虑几个关键因素:功能需求、性能要求和资源限制。对于简单的一对一通信,任务通知是最轻量级的选择。如果需要数据缓冲或多个发送者/接收者,队列是更好的选择。当需要管理共享资源时,信号量是合适的工具。对于需要多个条件同步的场景,事件组提供了便捷的解决方案。而在处理变长数据或连续数据流时,消息缓冲区和流缓冲区则是专门的解决方案。
不同通信机制的开销比较(从低到高):
- 任务通知(最轻量)
- 二值信号量
- 队列(单元素)
- 计数信号量
- 事件组
- 队列(多元素)
- 消息/流缓冲区(最重)
5内存管理

3866

被折叠的 条评论
为什么被折叠?



