本教程基于韦东山百问网出的 DShanMCU-F103开发板 进行编写,需要的同学可以在这里获取: https://item.taobao.com/item.htm?id=724601559592
配套资料获取:https://rtos.100ask.net/zh/freeRTOS/DShanMCU-F103
freeRTOS系列教程之freeRTOS入门与工程实践章节汇总: https://blog.csdn.net/qq_35181236/article/details/132842016
在本章中,会涉及如下内容:
对于整个单片机程序,我们称之为application,应用程序。
使用FreeRTOS时,我们可以在application中创建多个任务(task),有些文档把任务也称为线程(thread)。
以日常生活为例,比如这个母亲要同时做两件事:
在FreeRTOS中,任务就是一个函数,原型如下:
void ATaskFunction( void *pvParameters );
要注意的是:
void ATaskFunction( void *pvParameters )
{
/* 对于不同的任务,局部变量放在任务的栈里,有各自的副本 */
int32_t lVariableExample = 0;
/* 任务函数通常实现为一个无限循环 */
for( ;; )
{
/* 任务的代码 */
}
/* 如果程序从循环中退出,一定要使用vTaskDelete删除自己
* NULL表示删除的是自己
*/
vTaskDelete( NULL );
/* 程序不会执行到这里, 如果执行到这里就出错了 */
}
创建任务时使用的函数如下:
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 ); // 任务句柄, 以后使用它来操作这个任务
参数说明:
参数 | 描述 |
---|---|
pvTaskCode | 函数指针,任务对应的 C 函数。任务应该永远不退出,或者在退出时调用 “vTaskDelete(NULL)”。 |
pcName | 任务的名称,仅用于调试目的,FreeRTOS 内部不使用。pcName 的长度为 configMAX_TASK_NAME_LEN。 |
usStackDepth | 每个任务都有自己的栈,usStackDepth 指定了栈的大小,单位为 word。例如,如果传入 100,表示栈的大小为 100 word,即 400 字节。最大值为 uint16_t 的最大值。确定栈的大小并不容易,通常是根据估计来设定。精确的办法是查看反汇编代码。 |
pvParameters | 调用 pvTaskCode 函数指针时使用的参数:pvTaskCode(pvParameters)。 |
uxPriority | 任务的优先级范围为 0~(configMAX_PRIORITIES – 1)。数值越小,优先级越低。如果传入的值过大,xTaskCreate 会将其调整为 (configMAX_PRIORITIES – 1)。 |
pxCreatedTask | 用于保存 xTaskCreate 的输出结果,即任务的句柄(task handle)。如果以后需要对该任务进行操作,如修改优先级,则需要使用此句柄。如果不需要使用该句柄,可以传入 NULL。 |
返回值 | 成功时返回 pdPASS,失败时返回 errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY(失败原因是内存不足)。请注意,文档中提到的失败返回值是 pdFAIL 是不正确的。pdFAIL 的值为 0,而 errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY 的值为 -1。 |
使用静态分配内存的函数如下:
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 // 静态分配的任务结构体的指针,用它来操作这个任务
);
相比于使用动态分配内存创建任务的函数,最后2个参数不一样:
*参数* | *描述* |
---|---|
pvTaskCode | 函数指针,可以简单地认为任务就是一个C函数。 它稍微特殊一点:永远不退出,或者退出时要调用"vTaskDelete(NULL)" |
pcName | 任务的名字,FreeRTOS内部不使用它,仅仅起调试作用。 长度为:configMAX_TASK_NAME_LEN |
usStackDepth | 每个任务都有自己的栈,这里指定栈大小。 单位是word,比如传入100,表示栈大小为100 word,也就是400字节。 最大值为uint16_t的最大值。 怎么确定栈的大小,并不容易,很多时候是估计。 精确的办法是看反汇编码。 |
pvParameters | 调用pvTaskCode函数指针时用到:pvTaskCode(pvParameters) |
uxPriority | 优先级范围:0~(configMAX_PRIORITIES – 1) 数值越小优先级越低, 如果传入过大的值,xTaskCreate会把它调整为(configMAX_PRIORITIES – 1) |
puxStackBuffer | 静态分配的栈内存,比如可以传入一个数组, 它的大小是usStackDepth*4。 |
pxTaskBuffer | 静态分配的StaticTask_t结构体的指针 |
返回值 | 成功:返回任务句柄; 失败:NULL |
代码为: 05_create_task
使用动态、静态分配内存的方式,分别创建多个任务:监测遥控器并在LCD上显示、LED闪烁、全彩LED渐变颜色、使用无源蜂鸣器播放音乐。
代码为:FreeRTOS_02_create_task_use_params
我们说过,多个任务可以使用同一个函数,怎么体现它们的差别?
我们创建2个任务,使用同一个函数,但是在LCD上打印不一样的信息。
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,pvParameters来自哪里?创建任务时传入的。
代码如下:
// 录视频后再添加代码
删除任务时使用的函数如下:
void vTaskDelete( TaskHandle_t xTaskToDelete );
参数说明:
*参数* | *描述* |
---|---|
pvTaskCode | 任务句柄,使用xTaskCreate创建任务时可以得到一个句柄。 也可传入NULL,这表示删除自己。 |
怎么删除任务?举个不好的例子:
代码为: 07_delete_task
功能为:当监测到遥控器的Power按键被按下后,删除音乐播放任务。
代码如下:
任务运行图:
怎么让播放的音乐更动听?提高优先级。
优先级的取值范围是: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、滴答,比如每10ms发生一次时钟中断。
如下图:
相同优先级的任务怎么切换呢?请看下图:
有了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多一点就返回了。
如下图:
使用vTaskDelay函数时,建议以ms为单位,使用pdMS_TO_TICKS把时间转换为Tick。
这样的代码就与configTICK_RATE_HZ无关,即使配置项configTICK_RATE_HZ改变了,我们也不用去修改代码。
代码为:08_task_priority
本程序会:提高音乐播放任务的优先级,使用vTaskDelay进行延时。
代码如下:
调度情况如下图所示:
使用uxTaskPriorityGet来获得任务的优先级:
UBaseType_t uxTaskPriorityGet( const TaskHandle_t xTask );
使用参数xTask来指定任务,设置为NULL表示获取自己的优先级。
使用vTaskPrioritySet 来设置任务的优先级:
void vTaskPrioritySet( TaskHandle_t xTask,
UBaseType_t uxNewPriority );
使用参数xTask来指定任务,设置为NULL表示获取自己的优先级。
使用vTaskPrioritySet 来设置任务的优先级:
void vTaskPrioritySet( TaskHandle_t xTask,
UBaseType_t uxNewPriority );
使用参数xTask来指定任务,设置为NULL表示设置自己的优先级;
参数uxNewPriority表示新的优先级,取值范围是0~(configMAX_PRIORITIES – 1)。
以前我们很简单地把任务的状态分为2中:运行(Runing)、非运行(Not Running)。
对于非运行的状态,还可以继续细分,比如前面的FreeRTOS_04_task_priority中:
在日常生活的例子中,母亲在电脑前跟同事沟通时,如果同事一直没回复,那么母亲的工作就被卡住了、被堵住了、处于阻塞状态(Blocked)。重点在于:母亲在等待。
在FreeRTOS_04_task_priority实验中,如果把任务3中的vTaskDelay调用注释掉,那么任务1、任务2根本没有执行的机会,任务1、任务2被"饿死"了(starve)。
在实际产品中,我们不会让一个任务一直运行,而是使用"事件驱动"的方法让它运行:
在阻塞状态的任务,它可以等待两种类型的事件:
在等待一个同步事件时,可以加上超时时间。比如等待队里数据,超时时间设为10ms:
在日常生活的例子中,母亲正在电脑前跟同事沟通,母亲可以暂停:
FreeRTOS中的任务也可以进入暂停状态,唯一的方法是通过vTaskSuspend函数。函数原型如下:
void vTaskSuspend( TaskHandle_t xTaskToSuspend );
参数xTaskToSuspend表示要暂停的任务,如果为NULL,表示暂停自己。
要退出暂停状态,只能由别人来操作:
实际开发中,暂停状态用得不多。
这个任务完全准备好了,随时可以运行:只是还轮不到它。这时,它就处于就绪态(Ready)。
有两个Delay函数:
这2个函数原型如下:
void vTaskDelay( const TickType_t xTicksToDelay ); /* xTicksToDelay: 等待多少给Tick */
/* pxPreviousWakeTime: 上一次被唤醒的时间
* xTimeIncrement: 要阻塞到(pxPreviousWakeTime + xTimeIncrement)
* 单位都是Tick Count
*/
BaseType_t xTaskDelayUntil( TickType_t * const pxPreviousWakeTime,
const TickType_t xTimeIncrement );
下面画图说明:
本节代码为:09_taskdelay。
本程序会比较vTaskDelay和vTaskDelayUntil实际阻塞的时间,并在LCD上打印出来。
代码如下:
空闲任务(Idle任务)的作用之一:释放被删除的任务的内存。
除了上述目的之外,为什么必须要有空闲任务?一个良好的程序,它的任务都是事件驱动的:平时大部分时间处于阻塞状态。有可能我们自己创建的所有任务都无法执行,但是调度器必须能找到一个可以运行的任务:所以,我们要提供空闲任务。在使用vTaskStartScheduler()函数来创建、启动调度器时,这个函数内部会创建空闲任务:
空闲任务的优先级为0,这意味着一旦某个用户的任务变为就绪态,那么空闲任务马上被切换出去,让这个用户任务运行。在这种情况下,我们说用户任务"抢占"(pre-empt)了空闲任务,这是由调度器实现的。
要注意的是:如果使用vTaskDelete()来删除任务,那么你就要确保空闲任务有机会执行,否则就无法释放被删除任务的内存。
我们可以添加一个空闲任务的钩子函数(Idle Task Hook Functions),空闲任务的循环每执行一次,就会调用一次钩子函数。钩子函数的作用有这些:
在FreeRTOS\Source\tasks.c中,可以看到如下代码,所以前提就是:
这些知识在前面都提到过了,这里总结一下。
正在运行的任务,被称为"正在使用处理器",它处于运行状态。在单处理系统中,任何时间里只能有一个任务处于运行状态。
非运行状态的任务,它处于这3中状态之一:阻塞(Blocked)、暂停(Suspended)、就绪(Ready)。就绪态的任务,可以被调度器挑选出来切换为运行状态,调度器永远都是挑选最高优先级的就绪态任务并让它进入运行状态。
阻塞状态的任务,它在等待"事件",当事件发生时任务就会进入就绪状态。事件分为两类:时间相关的事件、同步事件。所谓时间相关的事件,就是设置超时时间:在指定时间内阻塞,时间到了就进入就绪状态。使用时间相关的事件,可以实现周期性的功能、可以实现超时功能。同步事件就是:某个任务在等待某些信息,别的任务或者中断服务程序会给它发送信息。怎么"发送信息"?方法很多,有:任务通知(task notification)、队列(queue)、事件组(event group)、信号量(semaphoe)、互斥量(mutex)等。这些方法用来发送同步信息,比如表示某个外设得到了数据。
所谓调度算法,就是怎么确定哪个就绪态的任务可以切换为运行状态。
通过配置文件FreeRTOSConfig.h的两个配置项来配置调度算法:configUSE_PREEMPTION、configUSE_TIME_SLICING。
还有第三个配置项:configUSE_TICKLESS_IDLE,它是一个高级选项,用于关闭Tick中断来实现省电,后续单独讲解。现在我们假设configUSE_TICKLESS_IDLE被设为0,先不使用这个功能。
调度算法的行为主要体现在两方面:高优先级的任务先运行、同优先级的就绪态任务如何被选中。调度算法要确保同优先级的就绪态任务,能"轮流"运行,策略是"轮转调度"(Round Robin Scheduling)。轮转调度并不保证任务的运行时间是公平分配的,我们还可以细化时间的分配方法。
从3个角度统一理解多种调度算法:
*配置项* | *A* | *B* | *C* | *D* | *E* |
---|---|---|---|---|---|
configUSE_PREEMPTION | 1 | 1 | 1 | 1 | 0 |
configUSE_TIME_SLICING | 1 | 1 | 0 | 0 | x |
configIDLE_SHOULD_YIELD | 1 | 0 | 1 | 0 | x |
说明 | 常用 | 很少用 | 很少用 | 很少用 | 几乎不用 |
注:
在 FreeRTOSConfig.h 中,定义这样的宏,对比逻辑分析仪的效果:
// 实验1:抢占
##define configUSE_PREEMPTION 1
##define configUSE_TIME_SLICING 1
##define configIDLE_SHOULD_YIELD 1
// 实验2:不抢占
##define configUSE_PREEMPTION 0
##define configUSE_TIME_SLICING 1
##define configIDLE_SHOULD_YIELD 1
对比结果为:
在 FreeRTOSConfig.h 中,定义这样的宏,对比逻辑分析仪的效果:
// 实验1:时间片轮转
##define configUSE_PREEMPTION 1
##define configUSE_TIME_SLICING 1
##define configIDLE_SHOULD_YIELD 1
// 实验2:时间片不轮转
##define configUSE_PREEMPTION 1
##define configUSE_TIME_SLICING 0
##define configIDLE_SHOULD_YIELD 1
从下面的对比图可以知道:
在 FreeRTOSConfig.h 中,定义这样的宏,对比逻辑分析仪的效果:
// 实验1:空闲任务让步
##define configUSE_PREEMPTION 1
##define configUSE_TIME_SLICING 1
##define configIDLE_SHOULD_YIELD 1
// 实验2:空闲任务不让步
##define configUSE_PREEMPTION 1
##define configUSE_TIME_SLICING 1
##define configIDLE_SHOULD_YIELD 0
从下面的对比图可以知道: