FreeRTOS 入门与实践 —— 实践(1)
整个单片机程序,称之为。用 FreeRTOS 时,可以在 应用程序 中创建多个,(有些地方称为线程 thread)。可以引入很多概念:任务状态 State,优先级 Priority,栈 Stack,事件驱动,协调式调度……
一,任务管理
1,创建与删除
整个单片机程序,称之为 应用程序(application)。
用 FreeRTOS 时,可以在 应用程序 中创建多个 任务 (task),(有些地方称为线程 thread)。可以引入很多概念:任务状态 State,优先级 Priority,栈 Stack,事件驱动,协调式调度……
(1)任务
在 FreeRTOS 中,任务就是一个函数,原型:
void ATaskFunction( void *pvParameters );
注意:
- 这个函数不能返回,函数内部尽量用局部变量
- 同一个函数,可以用来创建多个任务运行
- 每个运行任务都有自己的栈,不同任务运行函数时对于局部变量,都有自己的副本
- 函数中用到全局变量、静态变量时要防止冲突
// 示例如下:
void ATaskFunction( void *pvParameters )
{
/* 对于不同的任务,局部变量放在任务的栈里,有各自的副本 */
int32_t lVariableExample = 0;
/* 任务函数通常实现为一个无限循环 */
for( ;; )
{
/* 任务的代码 */
}
/* 如果程序从循环中退出,一定要使用vTaskDelete删除自己*/
vTaskDelete( NULL );
/* 程序不会执行到这里, 如果执行到这里就出错了 */
}
(2)创建任务
创建任务时可以使用 2 个函数:动态分配内存、静态分配内存。
1)动态分配内存函数如下:
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 ); // 任务句柄, 以后使用它来操作这个任务
2)静态分配内存函数如下:
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, // 静态分配的栈,就是一个buffer
StaticTask_t * const pxTaskBuffer // 静态分配的任务结构体的指针,用它来操作这个任务
);
区别在于最后两个参数:puxStackBuffer --- 静态分配的栈内存,可以传入一个数组 (大小为 usStackDepth*4;pxTaskBuffer --- 静态分配的 StaticTask_t 结构体的指针)
示例 1:创建多个任务
// 示例代码
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;
BaseType_t ret;
/* 动态创建任务: 声 */
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);
示例 2:用同一个函数创建 2 个任务,完成不一样的操作
struct DisplayInfo {
int x;
int y;
const char *str;
};
void vTaskFunction( void *pvParameters )
{
struct DisplayInfo *info = pvParameters;
uint32_t cnt = 0;
uint32_t len;
/* 任务函数的主体一般都是无限循环 */
for( ;; )
{
/* 打印任务的信息 */
len = LCD_PrintString(info->x, info->y, info->str);
LCD_PrintSignedVal(len+1, info->y, cnt++);
mdelay(500);
}
}
函数的 info 来自参数 pvParameters,用 xTaskCreate 创建任务时,第 4 个参数就是 pvParameters
/* 使用同一个函数创建不同的任务 */
xTaskCreate(LcdPrintTask, "task1", 128, &g_Task1Info, osPriorityNormal, NULL);
xTaskCreate(LcdPrintTask, "task2", 128, &g_Task2Info, osPriorityNormal, NULL);
xTaskCreate(LcdPrintTask, "task3", 128, &g_Task3Info, osPriorityNormal, NULL);
注意:
- 打印信息时 i2c 传输不能被多任务打断(需要用加锁保护)
- 用全局变量模拟互斥锁时不可靠,一定要用 mdelay 确保任务切换
- 后续会解答:为什么 Task3 最后创建,却最先运行
【估计栈的大小】:
简化粒度,宁大勿小(按占用内存最多的一层推算),结合硬件(ARM Cortex-M 的栈以 4 字节为单位)
多层调用时需关注局部变量用最多的一次,如 PlayMusic 占用估计是:36(4层调用)+4*28(局部结构体)+64(现场) = 250 【<128*4 预留的字节】
- 4层调用每层估算:4(返回地址)+5(寄存器备份)=9 字节,4 层 × 9 字节 = 36 字节
- 28由7个变量组成的结构体推算
- 现场保护开销:核心寄存器(R0-R11)48 字节,PSR+LR+PC寄存器12 字节,栈对齐补齐4 字节。总计 64 字节(嵌入式 RTOS 中线程现场保护的经典估值)。
(3)任务的删除
void vTaskDelete( TaskHandle_t xTaskToDelete );
xTaskToDelete传入的参数是创建任务得到的句柄 (类型是TaskHandle_t ),传入 NULL 时表示删除自己。
- 自杀:vTaskDelete(NULL)
- 被杀:别的任务执行 vTaskDelete(pvTaskCode),pvTaskCode 是自己的句柄
- 杀人:执行 vTaskDelete(pvTaskCode),pvTaskCode 是别的任务的句柄
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;
}
}
}
}
注意:
- 单纯的删除任务有缺陷,屏幕仍在显示,蜂鸣器保持最后声调。【需增加操作:清屏、停止蜂鸣器】
- 频繁创建、删除任务有缺陷,xTaskCreate 动态分配内存,多次删除后导致内存碎片化。【一般不用 vTaskDelete ,任务内做停止操作即可】
2,任务优先级和 Tick
(1)任务优先级
有些任务对响应速度要求高,这时候需提高优先级。
优先级的取值范围是:0~(configMAX_PRIORITIES – 1),数值越大优先级越高。
FreeRTOS的调度器可用如下两种方法快速找出优先级最高、可运行的任务:
| 通用方法(C 函数实现) | 架构优化方法(汇编指令实现) | |
|---|---|---|
| 实现方式 |
纯 C 代码循环遍历所有优先级,找出最高可运行任务 |
利用 MCU 架构专属汇编指令(如 CLZ/BSR),从 32 位掩码中快速找最高位 1 |
| configMAX_PRIORITIES 限制 | 无强制限制(理论可设任意值) | 最大只能设 32(因为汇编指令仅支持 32 位数据) |
| 内存占用 | 优先级数越多,内存占用越高(线性增长) | 固定占用少量内存(仅需 32 位掩码),不受优先级数影响 |
| 执行效率 | 低(遍历耗时,优先级数越多越慢) | 极高(单条汇编指令完成,耗时固定) |
| 跨平台性 | 好(所有 MCU 架构通用) | 差(仅支持有对应汇编指令的架构,如 ARM Cortex-M 系列) |
| 触发条件 |
configUSE_PORT_OPTIMISED _TASK_SELECTION = 0 或未定义 |
configUSE_PORT_OPTIMISED_TASK _SELECTION = 1 |
(2)Tick
FreeRTOS 用定时器产生固定间隔的中断。这叫 Tick 滴答,比如每10ms 发生一次时钟中断。两次中断之间的时间称为时间片 time slice,时间片的长度由configTICK_RATE_HZ 决定。
相同优先级的任务的切换方式:任务2 从 t2 发生 tick 中断(进入 tick 中断处理函数),切换到任务1,任务1执行到 t3 也发生 tick中断。【任务运行的时间并不是严格从 t1,t2,t3 处开始】

有了 Tick 概念后,就可以用 Tick 来衡量时间了,比如:
vTaskDelay(2); // 等待2个Tick,假设configTICK_RATE_HZ=100, Tick周期时10ms, 等待20ms
// 还可以使用pdMS_TO_TICKS宏把ms转换为tick
vTaskDelay(pdMS_TO_TICKS(100)); // 等待100ms
注意,基于Tick实现的延时并不精确,比如vTaskDelay(2)的本意是延迟2个Tick周期,有可能经过1个Tick多一点就返回了。
建议以 ms 为单位,使用 pdMS_TO_TICKS 把时间转换为 Tick,增强代码健壮性,让Delay 的时间与配置项 configTICK_RATE_HZ 无关。
// 修改优先级为“Normal+1”,让音乐播放任务在并行系统中更流畅
ret = xTaskCreate(PlayMusic, "SoundTask", 128, NULL,
osPriorityNormal+1, &xSoundTaskHandle);
// 引入了 bug : 控制任务优先级低于音乐任务,无法关闭了
// 解决:用 vTaskDelay 延时主动放弃 cpu 资源
(3)修改优先级
获得任务的优先级:
UBaseType_t uxTaskPriorityGet( const TaskHandle_t xTask );
设置任务的优先级:
void vTaskPrioritySet( TaskHandle_t xTask, UBaseType_t uxNewPriority );
// 使用参数 xTask 来指定任务,设置为 NULL 表示设置自己的优先级
3,任务状态

以上是完整的状态转换图,可以简单地把任务的状态分为 2 种:
- 运行 (Runing)
- 非运行 (Not Running)
对于非运行状态可细分:
- 阻塞状态 (Blocked)
- 暂停状态 (Suspended)
- 就绪状态 (Ready)
(1)阻塞状态
实际产品中不会让一个任务一直运行,而是用“事件驱动”的方法让它运行:
- 任务要等待某个事件,事件发生后它才能运行
- 在等待事件过程中,不消耗 CPU 资源
- 在等待事件的过程中,这个任务就处于阻塞状态
处于阻塞状态的任务,可以等待两种类型的事件
1)时间相关的时间(等待一段时间 或 等待到某个时间点)
2)同步事件,这个事件由别的任务 或 中断程序产生,同步事件来源有很多(队列、二进制信号量、计数信号量、互斥量、事件组、任务通知 等等)
等待同步事件时,可加上超时时间,超过等待时间后超时返回。
(2)暂停状态
进入暂停状态,唯一的方法是通过vTaskSuspend函数。函数原型如下:
void vTaskSuspend( TaskHandle_t xTaskToSuspend );
// 参数 xTaskToSuspend 表示要暂停的任务,如果为 NULL,表示暂停自己
要退出暂停状态,只能由别人来操作:
- 别的任务调用:vTaskResume
- 中断程序调用:vTaskResumeFromISR
【实际开发中,暂停状态用得不多】
(3)就绪状态
这个任务完全准备好了,随时可以运行,只是还轮不到它。这时它就处于就绪态。
示例代码:通过遥控器暂停、播放音乐任务。
bRunning = 1;
if (data == 0xa8) /* play */
{
/* 要么 suspend 要么 resume */
if (bRunning)
{
LCD_ClearLine(0, 0);
LCD_PrintString(0, 0, "Suspend Task");
vTaskSuspend(xSoundTaskHandle);
PassiveBuzzer_Control(0); /* 停止蜂鸣器 */
bRunning = 0;
}
else
{
LCD_ClearLine(0, 0);
LCD_PrintString(0, 0, "Resume Task");
vTaskResume(xSoundTaskHandle);
bRunning = 1;
}
}
4,Delay 函数
有两个 Delay 函数:
- vTaskDelay:至少等待指定个数的 Tick Interrupt 才变为就绪状态
- vTaskDelayUntil:等待到指定的绝对时刻才能变为就绪态(保证的是两次退出延迟的时间间隔至少为
n个 Tick,不管任务执行耗时多久都能稳定保持周期性)
void vTaskDelay ( const TickType_t xTicksToDelay );
// 等待多少个 Tick
BaseType_t xTaskDelayUntil( TickType_t* const pxPreviousWakeTime,
const TickType_t xTimeIncrement );
// pxPreviousWakeTime: 上一次被唤醒的时间
// xTimeIncrement: 要阻塞到(pxPreviousWakeTime + xTimeIncrement)
画图说明:
- 使用 vTaskDelay(n)时,进入、退出 vTaskDelay 的时间间隔至少是 n 个 Tick 中断
- 使用 xTaskDelayUntil(&Pre, n)时,前后两次退出 xTaskDelayUntil 的时间 至少是 n 个 Tick 中断(使用 xTaskDelayUntil 来让任务周期性地运行)
示例代码:
void LcdPrintTask(void *params)
{
struct TaskPrintInfo *pInfo = params;
uint32_t cnt = 0;
int len;
BaseType_t preTime;
uint64_t t1, t2;
preTime = xTaskGetTickCount();
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(cnt & 0x3);
}
t1 = system_get_ns();
//vTaskDelay(500); // 500000000
vTaskDelayUntil(&preTime, 500);
t2 = system_get_ns();
LCD_ClearLine(pInfo->x, pInfo->y+2);
LCD_PrintSignedVal(pInfo->x, pInfo->y+2, t2-t1);
//用 vTaskDelayUntil 代替 vTaskDelay:实现精准的周期性执行
//(固定 500ms 执行一次,不受任务执行耗时影响)
// 而 vTaskDelay 周期会随任务执行时间偏移。
}
}
5,空闲任务及其钩子函数
空闲任务(Idle 任务)的作用之一:释放被删除的任务内存。任务退出后需要空闲任务释放。
为什么必须要空闲任务? —— 良好的程序都是事件驱动的(大部分时间处于阻塞状态)【任务不是死循环,结束后要加 vTaskDelete(NULL); 创建任务时,会初始化栈,如果一个任务不经过其他处理,执行结束直接返回会进入ptvTaskExitError(关闭所有中断并进入死循环)】
当所以任务都不执行时调度器必须能找到一个可以运行的任务(即空闲任务)。
使用 vTaskStartScheduler() 函数来创建、启动调度器时,内部会创建空闲任务:
- 空闲任务优先级为0,不能阻碍用户任务的运行
- 空闲任务要么处于就绪态,要么处于运行态,永远不会阻塞
注意:用 vTaskDelete() 来删除任务,就要确保空闲任务有机会执行,否则无法释放被删除任务的内存。
可添加一个空闲任务的钩子函数,空闲任务循环执行一次就调用钩子函数,其作用为:
- 执行一些低优先级、后台的、需要连续执行的函数
- 测量系统的空闲时间(即空闲任务占据的时间)
- 让系统进入省电模式
其限制为:不能导致空闲任务阻塞或暂停;用vTaskDelete()删除任务时,钩子函数要高效执行(空闲任务一直在钩子函数时无法正常释放内存)
使用钩子函数的前提: (观察 FreeRTOS/Source/tasks.c 可知)
- 把宏 configUSE_IDLE_HOOK 定义为 1
- 实现 vApplicationIdleHook 函数
6,任务管理与调度
(1)引入优先级 —— 调度算法
【回顾】单处理系统中,任何时间里只能有一个任务处于运行状态。非运行的任务处于 3 种状态之一:阻塞(Blocked)、暂停(Suspended)、就绪 (Ready)。就绪态的任务可以被调度器挑选出来切换为运行状态,调度器永远都是挑选最高优先级的就绪态任务进入运行状态。阻塞状态的任务等待事件发生时就会进入就绪状态(时间相关事件 或 同步事件)。
【配置调度算法】
为了确定哪个就绪态的任务可以切换为运行状态,通过配置一些配置项确定调度算法。
- A:可抢占+时间片轮转+空闲任务让步
- B:可抢占+时间片轮转+空闲任务不让步
- C:可抢占+非时间片轮转+空闲任务让步
- D:可抢占+非时间片轮转+空闲任务不让步
- E:合作调度
(1)配置项:configUSE_PREEMPTION:高优先级的任务能否优先执行,即抢占调度。若为0 则更高优先级的任务需等待当前任务主动让出 CPU 资源。
(2)配置项:configUSE_TIME_SLICING:在可抢占的前提下,同优先级的任务是否轮流执行。若配置为 1 ,则同优先级的任务轮流执行;反之需要主动放弃或被高优先级任务抢占。
(3)配置项:configIDLE_SHOULD_YIELD:在"可抢占"+"时间片轮转"的前提下,细化空闲任务是否让步于用户任务。若为 1,空闲任务低人一等,每执行一次循环就看看是否主动让位给用户任务;反之空闲任务与用户任务轮流执行。
最常用的机制:① 相同优先级的任务轮流运行; ② 最高优先级的任务先运行
(2)任务管理 —— 链表操作
FreeRTOS设置中有优先级个数的设置(MAX_PRORITIRES),Normal对应24优先级,多个任务会放入一个链表里(pxReadyTasksLists[ ] ,xDelayedTaskList?[ ] 等等)
调用osKernelStart启动内核时,会启动调度器 vTaskStartScheduler,进而创建空闲任务prvIdleTask(优先级为0)
创建任务时,会把新任务添加进ReadyList (根据优先级找到),还有当前任务全局指针pxCurrentTCB的判断(解释了为何最后创建的任务最先运行)

主要关注的是tick中断和调度逻辑

详细的各种状态的切换说明看 FreeRTOS 笔记 p22,或对应视频。
更多推荐







所有评论(0)