FreeRTOS源码概述
入口函数
/* Init scheduler */osKernelInitialize(); /* Call init function for freertos objects (in freertos.c) */MX_FREERTOS_Init();/* Start scheduler */osKernelStart();
osKernelInitialize(); 初始化FreeRTOS运行环境
MX_FREERTOS_Init(); 创建任务osKernelStart(); 启动调度器这样就可以运行FreeRTOS内的代码任务
数据类型和编程规范
数据类型
TickType_t:
◼ FreeRTOS 配置了一个周期性的时钟中断: Tick Interrupt◼ 每发生一次中断,中断次数累加,这被称为 tick count◼ tick count 这个变量的类型就是 TickType_t◼ TickType_t 可以是 16 位的,也可以是 32 位的◼ FreeRTOSConfig.h 中定义 configUSE_16_BIT_TICKS 时, TickType_t 就是 uint16_t◼ 否则 TickType_t 就是 uint32_t◼ 对于 32 位架构,建议把 TickType_t 配置为 uint32_t
BaseType_t:
◼ 这是该架构最高效的数据类型◼ 32 位架构中,它就是 uint32_t◼ 16 位架构中,它就是 uint16_t◼ 8 位架构中,它就是 uint8_t◼ BaseType_t 通 常 用 作 简 单 的 返 回 值 的 类 型 , 还 有 逻 辑 值 , 比 如pdTRUE/pdFALSE
变量名
函数名
函数名的前缀有2部分:返回值类型、在哪个文件定义。
宏的名
内存管理
为什么要自己实现内存管理
在 C 语言的库函数中,有 mallc 、 free 等函数,但是在 FreeRTOS 中,它们不适用:⚫ 不适合用在资源紧缺的嵌入式系统中⚫ 这些函数的实现过于复杂、占据的代码空间太大⚫ 并非线程安全的 (thread-safe)⚫ 运行有不确定性:每次调用这些函数时花费的时间可能都不相同⚫ 内存碎片化⚫ 使用不同的编译器时,需要进行复杂的配置⚫ 有时候难以调试
FreeRTOS 的 5 中内存管理方法
heap_1
它只实现了 pvPortMalloc ,没有实现 vPortFree 。如果你的程序不需要删除内核对象,那么可以使用 heap_1 :⚫ 实现最简单⚫ 没有碎片问题⚫ 一些要求非常严格的系统里,不允许使用动态内存,就可以使用 heap_1
heap_2
Heap_2 也是在数组上分配内存,跟 Heap_1 不一样的地方在于:⚫ Heap_2 使用 最佳匹配算法 (best fit) 来分配内存⚫ 它支持 vPortFree最佳匹配算法:⚫ 假设 heap 有 3 块空闲内存: 5 字节、 25 字节、 100 字节⚫ pvPortMalloc 想申请 20 字节⚫ 找出最小的、能满足 pvPortMalloc 的内存: 25 字节⚫ 把它划分为 20 字节、 5 字节◼ 返回这 20 字节的地址◼ 剩下的 5 字节仍然是空闲状态,留给后续的 pvPortMalloc 使用与 Heap_4 相比, Heap_2 不会合并相邻的空闲内存,所以 Heap_2 会导致严重的 " 碎片化 " 问题。但是,如果申请、分配内存时大小总是相同的,这类场景下 Heap_2 没有碎片化的问题。所以它适合这种场景:频繁地创建、删除任务,但是任务的栈大小都是相同的 ( 创建任务时,需要分配 TCB 和栈, TCB 总是一样的 ) 。虽然不再推荐使用 heap_2 ,但是它的效率还是远高于 malloc 、 free 。
heap_3
Heap_3 使用标准 C 库里的 malloc、free 函数,所以堆大小由链接器的配置决定,配置项 configTOTAL_HEAP_SIZE 不再起作用。C 库里的 malloc 、 free 函数并非线程安全的, Heap_3 中先暂停 FreeRTOS 的调度器,再去调用这些函数,使用这种方法实现了线程安全。
heap_4
跟 Heap_1、Heap_2 一样,Heap_4 也是使用大数组来分配内存。Heap_4 使用 首次适应算法 (first fit) 来分配内存。它还会把相邻的空闲内存合并为一个更大的空闲内存,这有助于较少内存的碎片问题。首次适应算法:⚫ 假设堆中有 3 块空闲内存: 5 字节、 200 字节、 100 字节⚫ pvPortMalloc 想申请 20 字节⚫ 找出第 1 个能满足 pvPortMalloc 的内存: 200 字节⚫ 把它划分为 20 字节、 180 字节⚫ 返回这 20 字节的地址⚫ 剩下的 180 字节仍然是空闲状态,留给后续的 pvPortMalloc 使用Heap_4会把相邻空闲内存合并为一个大的空闲内存,可以较少内存的碎片化问题。适用于这种场景:频繁地分配、释放不同大小的内存
heap_5
Heap_5 分配内存、释放内存的算法跟 Heap_4 是一样的。相比于 Heap_4 , Heap_5 并不局限于管理一个大数组:它可以管理多块、分隔开的内存。在嵌入式系统中,内存的地址可能并不连续,这种场景下可以使用 Heap_5 。既然内存时分隔开的,那么就需要进行初始化:确定这些内存块在哪、多大:⚫ 在使用 pvPortMalloc 之前,必须先指定内存块的信息⚫ 使用 vPortDefineHeapRegions 来指定这些信息
指定一块内存
typedef struct HeapRegion
{uint8_t * pucStartAddress; // 起始地址size_t xSizeInBytes; // 大小
} HeapRegion_t;
怎么指定多块内存
HeapRegion_t xHeapRegions[] =
{{ ( uint8_t * ) 0x80000000UL, 0x10000 }, // 起始地址0x80000000,大小0x10000{ ( uint8_t * ) 0x90000000UL, 0xa0000 }, // 起始地址0x90000000,大小0xa0000{ NULL, 0 } // 表示数组结束
};
Heap 相关的函数
pvPortMalloc/vPortFree
void * pvPortMalloc( size_t xWantedSize );
void vPortFree( void * pv );
xPortGetFreeHeapSize
size_t xPortGetFreeHeapSize( void );
xPortGetMinimumEverFreeHeapSize
size_t xPortGetMinimumEverFreeHeapSize( void );
vApplicationMallocFailedHook(malloc 失败的钩子函数)
void * pvPortMalloc( size_t xWantedSize )vPortDefineHeapRegions
{......#if ( configUSE_MALLOC_FAILED_HOOK == 1 ){if( pvReturn == NULL ){extern void vApplicationMallocFailedHook( void );vApplicationMallocFailedHook();}}#endifreturn pvReturn;
}
如果分配失败就调用这个函数vApplicationMallocFailedHook
所以,如果想使用这个钩子函数:⚫ 在 FreeRTOSConfig.h 中,把 configUSE_MALLOC_FAILED_HOOK 定义为 1⚫ 提供 vApplicationMallocFailedHook 函数⚫ pvPortMalloc 失败时,才会调用此函数
创建任务
声光色影
什么是任务
在 FreeRTOS 中,任务就是一个函数,原型如下:void ATaskFunction( void *pvParameters );要注意的是:⚫ 这个函数不能返回⚫ 同一个函数,可以用来创建多个任务;换句话说,多个任务可以运行同一个函数⚫ 函数内部,尽量使用局部变量:◼ 每个任务都有自己的栈◼ 每个任务运行这个函数时◆ 任务 A 的局部变量放在任务 A 的栈里、任务 B 的局部变量放在任务 B 的栈里◆ 不同任务的局部变量,有自己的副本◼ 函数使用全局变量、静态变量的话◆ 只有一个副本:多个任务使用的是同一个副本◆ 要防止冲突(后续会讲)
创建任务
动态分配内存
任务创建1.函数
2.栈和TCB(任务控制块)
3.优先级
在 FreeRTOS 中,任务控制块(Task Control Block,TCB)是用来管理任务的一个重要数据结构。TCB 包含了与任务相关的所有信息,包括任务的状态、优先级、堆栈指针等。以下是 TCB 的一些主要组成部分和功能:
TCB 的组成部分
任务堆栈指针:
- TCB 中包含一个指向任务栈的指针。每个任务都有自己独立的堆栈,用于存储局部变量和函数调用信息。
任务优先级:
- TCB 记录了任务的优先级,FreeRTOS 会根据优先级调度任务。优先级越高,任务的执行频率越高。
任务状态:
- TCB 包含任务的当前状态,例如就绪、运行、阻塞等。这使得调度器能够管理任务的运行。
任务名字:
- TCB 可以包含任务的名称,方便调试和监控任务。
任务延迟计数:
- 用于跟踪任务的延迟状态,帮助调度器判断任务是否可以重新调度。
其他调度相关信息:
- 例如,任务的运行时间、等待的信号量或事件等信息。
TCB 的功能
- 任务调度:TCB 是 FreeRTOS 调度的核心,调度器通过 TCB 来管理各个任务的状态和切换。
- 资源管理:TCB 还帮助管理任务使用的系统资源,例如定时器、信号量和消息队列等。
- 上下文切换:在任务切换时,TCB 中的信息用于保存当前任务的状态,并恢复下一个任务的状态。
调度器的相关问题
在 FreeRTOS 中,调用
vTaskStartScheduler()
后,调度器会开始运行并根据任务的优先级和状态管理任务的调度。以下是为什么“如果一切正常,程序不会到达这里”的原因:1. 调度器的作用
- 任务调度:
vTaskStartScheduler()
函数启动了 FreeRTOS 的调度器,使其开始管理任务。调度器会选择适当的任务运行,并在需要时进行上下文切换。- 任务运行:一旦调度器开始运行,程序控制权转交给任务,而不是继续执行
main()
函数中调度器之后的代码。2. 无限循环
for (;;) {}
是一个无限循环,表示在正常情况下,程序应该一直停留在这个循环中,不再继续执行main()
函数中的其他代码。这个循环通常用于防止程序继续执行到意外的位置。3. 正常情况下的行为
- 正常运行:如果 FreeRTOS 正常运行并且至少有一个任务处于就绪状态,调度器会不断调度该任务。由于调度器在控制流中,它不会返回到
main()
函数的后续代码。- 错误情况:如果调度器无法正常启动(例如没有足够的堆栈空间、任务创建失败等),那么程序可能会到达这个无限循环。但在设计上,理想情况下调度器应该总是能够启动并运行。
总结
因此,
for (;;) {}
的存在是为了表明在正常情况下,程序不会执行到这一行。如果程序到达这一行,通常意味着有某种错误或异常情况发生,可能是需要进行调试和处理的地方。
TCB与任务句柄的关系
在操作系统中,TCB(Task Control Block)和任务句柄(Task Handle)是管理任务和进程的重要概念。它们之间的关系可以从以下几个方面进行说明:
1. 定义与概念
TCB(任务控制块): TCB 是一个数据结构,用于存储与任务或进程相关的信息。它通常包含以下内容:
- 任务的状态(如运行、就绪、阻塞等)
- 任务的优先级
- 任务的程序计数器(PC)
- 任务的堆栈指针
- 其他用于管理任务执行的上下文信息
任务句柄(Task Handle): 任务句柄是一种引用或指针,通常用于在程序中标识和操作特定的任务。句柄可以被视为对 TCB 的抽象,程序可以通过句柄来管理任务,而无需直接访问 TCB 结构。
2. TCB与任务句柄的关系
创建与维护: 当一个新任务被创建时,操作系统会为其分配一个 TCB,存储其状态和上下文信息。与此同时,操作系统会生成一个任务句柄,将其与 TCB 关联起来,以便在后续操作中引用。
任务管理: 通过任务句柄,程序可以执行对任务的操作,比如启动、暂停、终止或查询状态。这些操作通常会触发对 TCB 的访问,以获取任务的详细信息或更新其状态。
抽象层次: 任务句柄提供了对 TCB 的一种抽象,使得程序员可以更方便地管理任务,而不需要深入了解 TCB 的内部实现。这种设计使得任务管理更加灵活,能够简化程序的复杂性。
3. 示例
在一些操作系统或实时操作系统中,使用任务句柄来实现对任务的管理。例如,开发者可以调用
GetTaskStatus(task_handle)
函数来查询某个任务的状态,而这个函数内部实际上会通过任务句柄找到对应的 TCB,并返回其中的状态信息。4. 总结
TCB 和任务句柄在操作系统中共同发挥作用。TCB 是任务的详细描述,而任务句柄是对 TCB 的一种抽象引用,允许程序在不直接操作 TCB 的情况下管理和控制任务。这种设计提高了系统的可维护性和灵活性,简化了任务管理的过程。
BaseType_t xTaskCreate(
TaskFunction_t pxTaskCode, // 函数指针, 任务函数const char * const pcName, // 任务的名字const configSTACK_DEPTH_TYPE usStackDepth, // 栈大小,单位为word,10表示40字节void * const pvParameters, // 调用任务函数时传入的参数UBaseType_t uxPriority, // 优先级TaskHandle_t * const pxCreatedTask ); // 任务句柄, 以后使用它来操作这个任务
在动态分配中栈和TCB会自动分配
静态分配内存
TaskHandle_t xTaskCreateStatic ( TaskFunction_t pxTaskCode, // 函数指针, 任务函数const char * const pcName, // 任务的名字const uint32_t ulStackDepth, // 栈大小,单位为word,10表示40字节void * const pvParameters, // 调用任务函数时传入的参数UBaseType_t uxPriority, // 优先级StackType_t * const puxStackBuffer, // 静态分配的栈,就是一个bufferStaticTask_t * const pxTaskBuffer // 静态分配的任务结构体的指针,用它来操作这个任务
);
静态分配与动态分配的对比
-
静态分配 (
xTaskCreateStatic()
):- 内存分配:在编译时就分配好,使用一个预先定义的内存区域(例如数组)来存储 TCB 和任务堆栈。
- 没有句柄:由于内存是在编译时分配的,因此不需要句柄来管理这些资源。
- 内存管理:更简单,因为所有资源都是静态的,避免了动态内存分配带来的碎片和泄漏问题。
- 适合场景:适用于内存有限或要求高可靠性的嵌入式系统。
-
动态分配 (
xTaskCreate()
):- 内存分配:在运行时动态分配 TCB 和堆栈,通常会使用 FreeRTOS 的内存管理功能(如
pvPortMalloc()
)。 - 句柄:创建任务后,系统会返回一个任务句柄,允许后续对任务进行管理(例如删除任务、暂停任务等)。
- 内存管理:需要小心管理内存,以防止内存泄漏和碎片化。
- 适合场景:适用于资源较为充足且需要灵活创建和管理任务的应用。
- 内存分配:在运行时动态分配 TCB 和堆栈,通常会使用 FreeRTOS 的内存管理功能(如
定义
-
静态分配: 静态分配是在编译时或程序加载时分配内存。内存的大小和位置在程序运行之前就已经确定。这种分配方式通常适用于全局变量、静态变量和常量。
-
动态分配: 动态分配是在程序运行时根据需要分配内存。内存的大小和位置在程序运行过程中可以动态变化,通常使用内存管理函数(如
malloc()
、calloc()
、free()
在 C/C++ 中)来进行分配和释放。
2. 内存管理
-
静态分配:
- 内存大小固定,无法在运行时改变。
- 程序生命周期内分配的内存不释放,直到程序结束。
- 内存的分配和释放由编译器管理。
-
动态分配:
- 内存大小可以在运行时动态确定,适合不确定大小的数组或对象。
- 需要手动管理内存的分配和释放,以防止内存泄漏。
- 程序可以根据需求随时请求和释放内存。
3. 灵活性
-
静态分配:
- 灵活性较低,无法在运行时改变内存的大小。
- 适用于大小已知且不需要变化的场景。
-
动态分配:
- 灵活性高,可以根据实际需求随时调整内存大小。
- 适合处理不确定大小或变化的数据结构(如链表、树等)。
4. 效率
-
静态分配:
- 通常效率较高,因为内存分配和释放的过程较简单,编译器在编译时进行优化。
- 不存在内存碎片问题。
-
动态分配:
- 动态分配可能会引入一定的开销,尤其是在频繁分配和释放内存时。
- 可能会出现内存碎片,影响内存的使用效率。
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN Variables */
static StackType_t g_pucStackOfLightTask[128];
static StaticTask_t g_TCBofLightTask;
static TaskHandle_t xLightTaskHandle;static StackType_t g_pucStackOfColorTask[128];
static StaticTask_t g_TCBofColorTask;
static TaskHandle_t xColorTaskHandle;/* USER CODE END Variables */
/* Definitions for defaultTask *//* Create the thread(s) *//* creation of defaultTask */defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes);/* USER CODE BEGIN RTOS_THREADS *//* add threads, ... *//* ´´½¨ÈÎÎñ: Éù */extern void PlayMusic(void *params);ret = xTaskCreate(PlayMusic, "SoundTask", 128, NULL, osPriorityNormal, &xSoundTaskHandle);/* ´´½¨ÈÎÎñ: ¹â */xLightTaskHandle = xTaskCreateStatic(Led_Test, "LightTask", 128, NULL, osPriorityNormal, g_pucStackOfLightTask, &g_TCBofLightTask);/* ´´½¨ÈÎÎñ: É« */xColorTaskHandle = xTaskCreateStatic(ColorLED_Test, "ColorTask", 128, NULL, osPriorityNormal, g_pucStackOfColorTask, &g_TCBofColorTask);/* USER CODE END RTOS_THREADS *//* USER CODE BEGIN RTOS_EVENTS *//* add events, ... *//* USER CODE END RTOS_EVENTS */
使用任务参数
struct TaskPrintInfo {uint8_t x;uint8_t y;char name[16];
};static struct TaskPrintInfo g_Task1Info = {0, 0, "Task1"};
static struct TaskPrintInfo g_Task2Info = {0, 3, "Task2"};
static struct TaskPrintInfo g_Task3Info = {0, 6, "Task3"};
static int g_LCDCanUse = 1;void LcdPrintTask(void *params)
{struct TaskPrintInfo *pInfo = params;uint32_t cnt = 0;int len;while (1){/* ´òÓ¡ÐÅÏ¢ */if (g_LCDCanUse){g_LCDCanUse = 0;len = LCD_PrintString(pInfo->x, pInfo->y, pInfo->name);len += LCD_PrintString(len, pInfo->y, ":");LCD_PrintSignedVal(len, pInfo->y, cnt++);g_LCDCanUse = 1;}mdelay(500);}
}
/* USER CODE END FunctionPrototypes */void StartDefaultTask(void *argument);void MX_FREERTOS_Init(void); /* (MISRA C 2004 rule 8.1) *//*** @brief FreeRTOS initialization* @param None* @retval None*/
void MX_FREERTOS_Init(void) {/* USER CODE BEGIN Init */TaskHandle_t xSoundTaskHandle;BaseType_t ret;LCD_Init();LCD_Clear();/* USER CODE END Init *//* USER CODE BEGIN RTOS_MUTEX *//* add mutexes, ... *//* USER CODE END RTOS_MUTEX *//* USER CODE BEGIN RTOS_SEMAPHORES *//* add semaphores, ... *//* USER CODE END RTOS_SEMAPHORES *//* USER CODE BEGIN RTOS_TIMERS *//* start timers, add new ones, ... *//* USER CODE END RTOS_TIMERS *//* USER CODE BEGIN RTOS_QUEUES *//* add queues, ... *//* USER CODE END RTOS_QUEUES *//* Create the thread(s) *//* creation of defaultTask *///defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes);/* USER CODE BEGIN RTOS_THREADS *//* add threads, ... *//* ´´½¨ÈÎÎñ: Éù */extern void PlayMusic(void *params);//ret = xTaskCreate(PlayMusic, "SoundTask", 128, NULL, osPriorityNormal, &xSoundTaskHandle);/* ´´½¨ÈÎÎñ: ¹â *///xLightTaskHandle = xTaskCreateStatic(Led_Test, "LightTask", 128, NULL, osPriorityNormal, g_pucStackOfLightTask, &g_TCBofLightTask);/* ´´½¨ÈÎÎñ: É« *///xColorTaskHandle = xTaskCreateStatic(ColorLED_Test, "ColorTask", 128, NULL, osPriorityNormal, g_pucStackOfColorTask, &g_TCBofColorTask);/* ʹÓÃͬһ¸öº¯Êý´´½¨²»Í¬µÄÈÎÎñ */xTaskCreate(LcdPrintTask, "task1", 128, &g_Task1Info, osPriorityNormal, NULL);xTaskCreate(LcdPrintTask, "task2", 128, &g_Task2Info, osPriorityNormal, NULL);xTaskCreate(LcdPrintTask, "task3", 128, &g_Task3Info, osPriorityNormal, NULL);/* USER CODE END RTOS_THREADS *//* USER CODE BEGIN RTOS_EVENTS *//* add events, ... *//* USER CODE END RTOS_EVENTS */}
删除任务
使用遥控器控制音乐
void StartDefaultTask(void *argument)
{/* USER CODE BEGIN StartDefaultTask *//* Infinite loop */uint8_t dev, data;int len;TaskHandle_t xSoundTaskHandle = NULL;BaseType_t ret;LCD_Init();LCD_Clear();IRReceiver_Init();LCD_PrintString(0, 0, "Waiting control");while (1){/* ¶ÁÈ¡ºìÍâÒ£¿ØÆ÷ */if (0 == IRReceiver_Read(&dev, &data)){ if (data == 0xa8) /* play */{/* ´´½¨²¥·ÅÒôÀÖµÄÈÎÎñ */extern void PlayMusic(void *params);if (xSoundTaskHandle == NULL){LCD_ClearLine(0, 0);LCD_PrintString(0, 0, "Create Task");ret = xTaskCreate(PlayMusic, "SoundTask", 128, NULL, osPriorityNormal, &xSoundTaskHandle);}}else if (data == 0xa2) /* power */{/* ɾ³ý²¥·ÅÒôÀÖµÄÈÎÎñ */if (xSoundTaskHandle != NULL){LCD_ClearLine(0, 0);LCD_PrintString(0, 0, "Delete Task");vTaskDelete(xSoundTaskHandle);PassiveBuzzer_Control(0); /* Í£Ö¹·äÃùÆ÷ */xSoundTaskHandle = NULL;}}}}/* USER CODE END StartDefaultTask */
}/* Private application code --------------------------------------------------*/
/* USER CODE BEGIN Application *//* USER CODE END Application */
vTaskDelete
是 FreeRTOS 中的一个函数,用于删除一个任务。xSoundTaskHandle
是一个任务句柄,代表你希望删除的任务。使用这个函数可以释放与任务相关的资源,并将任务从调度器中移除。
使用 vTaskDelete
以下是使用 vTaskDelete
的基本信息和示例代码:
void vTaskDelete(TaskHandle_t xTaskToDelete);
xTaskToDelete
:要删除的任务的句柄。如果传递NULL
,将删除调用vTaskDelete
的任务本身。-
删除任务的注意事项:
- 一旦任务被删除,无法再恢复。
- 确保在删除任务之前,所有与任务相关的资源都已经释放或被处理。
- 调用
vTaskDelete
后,任何对该任务句柄的访问都是未定义行为。