来源:
https://rtos.100ask.net/zh/FreeRTOS/simulator/chapter3.html
1 基本概念
对于整个单片机程序,我们称之为application,应用程序。
使用FreeRTOS时,我们可以在application中创建多个任务(task),有些文档把任务也称为线程(thread)。
以日常生活为例,比如这个母亲要同时做两件事:
- 喂饭:这是一个任务
- 回信息:这是另一个任务
这可以引入很多概念:
- 任务状态(State):
- 当前正在喂饭,它是running状态;另一个"回信息"的任务就是"not running"状态
- "not running"状态还可以细分:
- ready:就绪,随时可以运行
- blocked:阻塞,卡住了,母亲在等待同事回信息
- suspended:挂起,同事废话太多,不管他了
- 优先级(Priority)
- 我工作生活兼顾:喂饭、回信息优先级一样,轮流做
- 我忙里偷闲:还有空闲任务,休息一下
- 厨房着火了,什么都别说了,先灭火:优先级更高
- 栈(Stack)
- 喂小孩时,我要记得上一口喂了米饭,这口要喂青菜了
- 回信息时,我要记得刚才聊的是啥
- 做不同的任务,这些细节不一样
- 对于人来说,当然是记在脑子里
- 对于程序,是记在栈里
- 每个任务有自己的栈
- 事件驱动
- 孩子吃饭太慢:先休息一会,等他咽下去了、等他提醒我了,再喂下一口
- 协助式调度(Co-operative Scheduling)
- 你在给同事回信息
- 同事说:好了,你先去给小孩喂一口饭吧,你才能离开
- 同事不放你走,即使孩子哭了你也不能走
- 你好不容易可以给孩子喂饭了
- 孩子说:好了,妈妈你去处理一下工作吧,你才能离开
- 孩子不放你走,即使同事连发信息你也不能走
- 你在给同事回信息
任务创建与删除
什么是任务
在FreeRTOS中,任务就是一个函数,原型如下:
cpp
void ATaskFunction( void *pvParameters );
要注意的是:
- 这个函数不能返回
- 同一个函数,可以用来创建多个任务;换句话说,多个任务可以运行同一个函数
- 函数内部,尽量使用局部变量:
- 每个任务都有自己的栈
- 每个任务运行这个函数时
- 任务A的局部变量放在任务A的栈里、任务B的局部变量放在任务B的栈里
- 不同任务的局部变量,有自己的副本
- 函数使用全局变量、静态变量的话
- 只有一个副本:多个任务使用的是同一个副本
- 要防止冲突(后续会讲)
下面是一个示例:
cpp
void ATaskFunction( void *pvParameters )
{
/* 对于不同的任务,局部变量放在任务的栈里,有各自的副本 */
int32_t lVariableExample = 0;
/* 任务函数通常实现为一个无限循环 */
for( ;; )
{
/* 任务的代码 */
}
/* 如果程序从循环中退出,一定要使用vTaskDelete删除自己
* NULL表示删除的是自己
*/
vTaskDelete( NULL );
/* 程序不会执行到这里, 如果执行到这里就出错了 */
}
创建任务
创建任务时使用的函数如下:
cpp
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 ); // 任务句柄, 以后使用它来操作这个任务
参数说明:
示例1: 创建任务
代码为:FreeRTOS_01_create_task
使用2个函数分别创建2个任务。
任务1的代码:
cpp
void vTask1( void *pvParameters )
{
const char *pcTaskName = "T1 run\r\n";
volatile uint32_t ul; /* volatile用来避免被优化掉 */
/* 任务函数的主体一般都是无限循环 */
for( ;; )
{
/* 打印任务1的信息 */
printf( pcTaskName );
/* 延迟一会(比较简单粗暴) */
for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
{
}
}
}
任务2的代码:
cpp
void vTask2( void *pvParameters )
{
const char *pcTaskName = "T2 run\r\n";
volatile uint32_t ul; /* volatile用来避免被优化掉 */
/* 任务函数的主体一般都是无限循环 */
for( ;; )
{
/* 打印任务1的信息 */
printf( pcTaskName );
/* 延迟一会(比较简单粗暴) */
for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
{
}
}
}
main函数:
cpp
int main( void )
{
prvSetupHardware();
xTaskCreate(vTask1, "Task 1", 1000, NULL, 1, NULL);
xTaskCreate(vTask2, "Task 2", 1000, NULL, 1, NULL);
/* 启动调度器 */
vTaskStartScheduler();
/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
return 0;
}
运行结果如下:

注意:
- task 2先运行!
- 要分析xTaskCreate的代码才能知道原因:更高优先级的、或者后面创建的任务先运行。
任务运行图:
- 在t1:Task2进入运行态,一直运行直到t2
- 在t2:Task1进入运行态,一直运行直到t3;在t3,Task2重新进入运行态

示例2: 使用任务参数
代码为:FreeRTOS_02_create_task_use_params
我们说过,多个任务可以使用同一个函数,怎么体现它们的差别?
- 栈不同
- 创建任务时可以传入不同的参数
我们创建2个任务,使用同一个函数,代码如下:
cpp
void vTaskFunction( void *pvParameters )
{
const char *pcTaskText = pvParameters;
volatile uint32_t ul; /* volatile用来避免被优化掉 */
/* 任务函数的主体一般都是无限循环 */
for( ;; )
{
/* 打印任务的信息 */
printf(pcTaskText);
/* 延迟一会(比较简单粗暴) */
for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
{
}
}
}
上述代码中的pcTaskText来自参数pvParameters,pvParameters来自哪里?创建任务时传入的。
代码如下:
- 使用xTaskCreate创建2个任务时,第4个参数就是pvParameters
- 不同的任务,pvParameters不一样
cpp
static const char *pcTextForTask1 = "T1 run\r\n";
static const char *pcTextForTask2 = "T2 run\r\n";
int main( void )
{
prvSetupHardware();
xTaskCreate(vTaskFunction, "Task 1", 1000, (void *)pcTextForTask1, 1, NULL);
xTaskCreate(vTaskFunction, "Task 2", 1000, (void *)pcTextForTask2, 1, NULL);
/* 启动调度器 */
vTaskStartScheduler();
/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
return 0;
}
任务的删除
删除任务时使用的函数如下:
cpp
void vTaskDelete( TaskHandle_t xTaskToDelete );
参数说明:

怎么删除任务?举个不好的例子:
- 自杀:
vTaskDelete(NULL) - 被杀:别的任务执行
vTaskDelete(pvTaskCode),pvTaskCode是自己的句柄 - 杀人:执行
vTaskDelete(pvTaskCode),pvTaskCode是别的任务的句柄
示例3: 删除任务
代码为:FreeRTOS_03_delete_task
本节代码会涉及优先级的知识,可以只看vTaskDelete的用法,忽略优先级的讲解。
我们要做这些事情:
- 创建任务1:任务1的大循环里,创建任务2,然后休眠一段时间
- 任务2:打印一句话,然后就删除自己
任务1的代码如下:
cpp
void vTask1( void *pvParameters )
{
const TickType_t xDelay100ms = pdMS_TO_TICKS( 100UL );
BaseType_t ret;
/* 任务函数的主体一般都是无限循环 */
for( ;; )
{
/* 打印任务的信息 */
printf("Task1 is running\r\n");
ret = xTaskCreate( vTask2, "Task 2", 1000, NULL, 2, &xTask2Handle );
if (ret != pdPASS)
printf("Create Task2 Failed\r\n");
// 如果不休眠的话, Idle任务无法得到执行
// Idel任务会清理任务2使用的内存
// 如果不休眠则Idle任务无法执行, 最后内存耗尽
vTaskDelay( xDelay100ms );
}
任务2的代码如下:
cpp
void vTask2( void *pvParameters )
{
/* 打印任务的信息 */
printf("Task2 is running and about to delete itself\r\n");
// 可以直接传入参数NULL, 这里只是为了演示函数用法
vTaskDelete(xTask2Handle);
}
main函数代码如下:
cpp
int main( void )
{
prvSetupHardware();
xTaskCreate(vTask1, "Task 1", 1000, NULL, 1, NULL);
/* 启动调度器 */
vTaskStartScheduler();
/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
return 0;
}
运行结果如下:

任务运行图:
- main函数中创建任务1,优先级为1。任务1运行时,它创建任务2,任务2的优先级是2。
- 任务2的优先级最高,它马上执行。
- 任务2打印一句话后,就删除了自己。
- 任务2被删除后,任务1的优先级最高,轮到任务1继续运行,它调用
vTaskDelay()进入Block状态 - 任务1 Block期间,轮到Idle任务执行:它释放任务2的内存(TCB、栈)
- 时间到后,任务1变为最高优先级的任务继续执行。
- 如此循环。

在任务1的函数中,如果不调用vTaskDelay,则Idle任务用于没有机会执行,它就无法释放创建任务2是分配的内存。
而任务1在不断地创建任务,不断地消耗内存,最终内存耗尽再也无法创建新的任务。
现象如下:

任务1的代码中,需要注意的是:xTaskCreate的返回值。
- 很多手册里说它失败时返回值是pdFAIL,这个宏是0
- 其实失败时返回值是errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY,这个宏是-1
- 为了避免混淆,我们使用返回值跟pdPASS来比较,这个宏是1
任务优先级和Tick
任务优先级
在上个示例中我们体验过优先级的使用:高优先级的任务先运行。
优先级的取值范围是:0~(configMAX_PRIORITIES -- 1),数值越大优先级越高。
FreeRTOS的调度器可以使用2种方法来快速找出优先级最高的、可以运行的任务。使用不同的方法时,configMAX_PRIORITIES 的取值有所不同。
- 通用方法 使用C函数实现,对所有的架构都是同样的代码。对configMAX_PRIORITIES的取值没有限制。但是configMAX_PRIORITIES的取值还是尽量小,因为取值越大越浪费内存,也浪费时间。 configUSE_PORT_OPTIMISED_TASK_SELECTION被定义为0、或者未定义时,使用此方法。
- 架构相关的优化的方法 架构相关的汇编指令,可以从一个32位的数里快速地找出为1的最高位。使用这些指令,可以快速找出优先级最高的、可以运行的任务。 使用这种方法时,configMAX_PRIORITIES的取值不能超过32。 configUSE_PORT_OPTIMISED_TASK_SELECTION被定义为1时,使用此方法。
在学习调度方法之前,你只要初略地知道:
- FreeRTOS会确保最高优先级的、可运行的任务,马上就能执行
- 对于相同优先级的、可运行的任务,轮流执行
这无需记忆,就像我们举的例子:
- 厨房着火了,当然优先灭火
- 喂饭、回复信息同样重要,轮流做
Tick
对于同优先级的任务,它们"轮流"执行。怎么轮流?你执行一会,我执行一会。
"一会"怎么定义?
人有心跳,心跳间隔基本恒定。
FreeRTOS中也有心跳,它使用定时器产生固定间隔的中断。这叫Tick、滴答,比如每10ms发生一次时钟中断。
如下图:
- 假设t1、t2、t3发生时钟中断
- 两次中断之间的时间被称为时间片(time slice、tick period)
- 时间片的长度由configTICK_RATE_HZ 决定,假设configTICK_RATE_HZ为100,那么时间片长度就是10ms

相同优先级的任务怎么切换呢?请看下图:
- 任务2从t1执行到t2
- 在t2发生tick中断,进入tick中断处理函数:
- 选择下一个要运行的任务
- 执行完中断处理函数后,切换到新的任务:任务1
- 任务1从t2执行到t3
- 从下图中可以看出,任务运行的时间并不是严格从t1,t2,t3哪里开始

有了Tick的概念后,我们就可以使用Tick来衡量时间了,比如:
cpp
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多一点就返回了。
如下图:

使用vTaskDelay函数时,建议以ms为单位,使用pdMS_TO_TICKS把时间转换为Tick。
这样的代码就与configTICK_RATE_HZ无关,即使配置项configTICK_RATE_HZ改变了,我们也不用去修改代码。
示例4: 优先级实验
代码为:FreeRTOS_04_task_priority
本程序会创建3个任务:
- 任务1、任务2:优先级相同,都是1
- 任务3:优先级最高,是2
任务1、2代码如下:
cpp
void vTask1( void *pvParameters )
{
/* 任务函数的主体一般都是无限循环 */
for( ;; )
{
/* 打印任务的信息 */
printf("T1\r\n");
}
}
void vTask2( void *pvParameters )
{
/* 任务函数的主体一般都是无限循环 */
for( ;; )
{
/* 打印任务的信息 */
printf("T2\r\n");
}
}
任务3代码如下:
cpp
void vTask3( void *pvParameters )
{
const TickType_t xDelay3000ms = pdMS_TO_TICKS( 3000UL );
/* 任务函数的主体一般都是无限循环 */
for( ;; )
{
/* 打印任务的信息 */
printf("T3\r\n");
// 如果不休眠的话, 其他任务无法得到执行
vTaskDelay( xDelay3000ms );
}
}
main函数代码如下:
cpp
{
prvSetupHardware();
xTaskCreate(vTask1, "Task 1", 1000, NULL, 1, NULL);
xTaskCreate(vTask2, "Task 2", 1000, NULL, 1, NULL);
xTaskCreate(vTask3, "Task 3", 1000, NULL, 2, NULL);
/* 启动调度器 */
vTaskStartScheduler();
/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
return 0;
}
运行情况如下图所示:
- 任务3优先执行,直到它调用vTaskDelay主动放弃运行
- 任务1、任务2:轮流执行

调度情况如下图所示:

示例5: 修改优先级
本节代码为:FreeRTOS_05_change_priority。
使用uxTaskPriorityGet来获得任务的优先级:
cpp
UBaseType_t uxTaskPriorityGet( const TaskHandle_t xTask );
使用参数xTask来指定任务,设置为NULL表示获取自己的优先级。
使用vTaskPrioritySet 来设置任务的优先级:
cpp
void vTaskPrioritySet( TaskHandle_t xTask,
UBaseType_t uxNewPriority );
使用参数xTask来指定任务,设置为NULL表示设置自己的优先级; 参数uxNewPriority表示新的优先级,取值范围是0~(configMAX_PRIORITIES -- 1)。
main函数的代码如下,它创建了2个任务:任务1的优先级更高,它先执行:
cpp
int main( void )
{
prvSetupHardware();
/* Task1的优先级更高, Task1先执行 */
xTaskCreate( vTask1, "Task 1", 1000, NULL, 2, NULL );
xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, &xTask2Handle );
/* 启动调度器 */
vTaskStartScheduler();
/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
return 0;
}
任务1的代码如下:
cpp
void vTask1( void *pvParameters )
{
UBaseType_t uxPriority;
/* Task1,Task2都不会进入阻塞或者暂停状态
* 根据优先级决定谁能运行
*/
/* 得到Task1自己的优先级 */
uxPriority = uxTaskPriorityGet( NULL );
for( ;; )
{
printf( "Task 1 is running\r\n" );
printf("About to raise the Task 2 priority\r\n" );
/* 提升Task2的优先级高于Task1
* Task2会即刻执行
*/
vTaskPrioritySet( xTask2Handle, ( uxPriority + 1 ) );
/* 如果Task1能运行到这里,表示它的优先级比Task2高
* 那就表示Task2肯定把自己的优先级降低了
*/
}
}
任务2的代码如下:
cpp
void vTask2( void *pvParameters )
{
UBaseType_t uxPriority;
/* Task1,Task2都不会进入阻塞或者暂停状态
* 根据优先级决定谁能运行
*/
/* 得到Task2自己的优先级 */
uxPriority = uxTaskPriorityGet( NULL );
for( ;; )
{
/* 能运行到这里表示Task2的优先级高于Task1
* Task1提高了Task2的优先级
*/
printf( "Task 2 is running\r\n" );
printf( "About to lower the Task 2 priority\r\n" );
/* 降低Task2自己的优先级,让它小于Task1
* Task1得以运行
*/
vTaskPrioritySet( NULL, ( uxPriority - 2 ) );
}
}
调度情况如下图所示:
- 1:一开始Task1优先级最高,它先执行。它提升了Task2的优先级。
- 2:Task2的优先级最高,它执行。它把自己的优先级降低了。
- 3:Task1的优先级最高,再次执行。它提升了Task2的优先级。
- 如此循环。
- 注意:Task1的优先级一直是2,Task2的优先级是3或1,都大于0。所以Idel任务没有机会执行。
