多任务处理 (TASK)
希望通过本章讲解,大家能够对以下问题有一个比较清晰的答案:
FreeRTOS如何将CPU处理时间分配到每个任务上?FreeRTOS如何选择当前该执行哪个任务?每个任务的优先级不同会有什么样的系统调用行为?每个任务有哪些可能的状态?如何创建任务?怎样设置任务的参数?如何删除一个任务?idle task是做什么的?
以下是一个最简单的多任务执行场景

本文所有代码基于瑞萨RZA系列产品的FreeRTOS示例,应用代码只是对标准FreeRTOS接口进行了二次封装,并不影响对于FreeRTOS的理解。
TEST01示例代码如下:
子任务1和子任务2里面是2个while死循环,它没有执行任何任务休眠等操作,死循环中会打印当前任务名称和系统时钟。这两个子任务是由主任务os_main_task()在运行时动态创建的。
R_OS_TaskCreate函数是FreeRTOS函数xTaskCreate函数的封装,它实际会调用如下函数
BaseType_t xTaskCreate( TaskFunction_t pvTaskCode,
const char * const pcName,
uint16_t usStackDepth,
void *pvParameters,
UBaseType_t uxPriority,
TaskHandle_t *pxCreatedTask );
关键参数为:
这个任务启动后的入口函数任务名称,唯一字符串任务的栈大小可以给每个任务传递一个参数指针,不传递参数时可以为NULL任务优先级返回的任务句柄
static int_t os_test_task_1(void)
{
const char *task_name = R_OS_TaskGetCurrentName();
uint32_t loopcnt;
while(1) {
printf("[%8d]: This is %s
",R_OS_SYSTICKS_TO_MS(R_OS_GetTickCount()), task_name);
loopcnt = 0x3FFFFFF;
while(loopcnt > 0) {
loopcnt--;
}
}
static int_t os_test_task_2(void)
{
const char *task_name = R_OS_TaskGetCurrentName();
uint32_t loopcnt;
while(1) {
printf("[%8d]: This is %s
",R_OS_SYSTICKS_TO_MS(R_OS_GetTickCount()), task_name);
loopcnt = 0x3FFFFFF;
while(loopcnt > 0) {
loopcnt--;
}
}
static int_t os_main_task(void)
{
ptask1 = R_OS_TaskCreate("task1", (os_task_code_t)os_test_task_1, NULL, R_OS_ABSTRACTION_DEFAULT_STACK_SIZE, R_OS_TASK_MAIN_TASK_PRI);
ptask2 = R_OS_TaskCreate("task2", (os_task_code_t)os_test_task_2, NULL, R_OS_ABSTRACTION_DEFAULT_STACK_SIZE, R_OS_TASK_MAIN_TASK_PRI);
while(1){
R_OS_TaskSleep(1000);
}
}
int_t main(void)
{
R_OS_RegistrMainTaskCb((void*)os_main_task);
R_OS_AbstractionLayerInit();
while(1);
}
任务的分时运行
任务的定义及不同状态
每一个任务都是一个单独的小程序,它总是包含一个while(1)的无限循环,任务一定不能包含return语句退出它的while循环。每个任务都有自己独立的栈空间,这个空间会由OS内部在HEAP上自动分配。每一个任务中都可以随时创建任意多个各自独立运行的其它任务。示例中mainTask创建了task1和task2,也可以由MainTask创建task1,task1创建task2,在执行效果上是一样的。新创建的任务状态为Ready,如果没有更高优先级的任务正在执行,它会转到Running状态。


任务的状态定义如下:
typedef enum
{
eRunning = 0,/* 任务正在查询自身状态,因此必须处于运行状态 */
eReady, /* 被查询的任务处于就绪或待命就绪列表中 */
eBlocked, /* 被查询的任务处于阻塞状态 */
eSuspended, /* 被查询的任务处于挂起状态,或处于带有无限超时的阻塞状态 */
eDeleted, /* 被查询的任务已被删除,但其任务控制块(TCB)尚未释放 */
eInvalid /* 用作“无效状态”值 */
} eTaskState;
任务不同状态的转换
由Kernel来维护各个task的状态转换,即调度器谁来驱动调度器?调度器的执行由硬件平台的定时器模块的中断驱动调度器的工作频率可配置,默认为1ms,即时间片
示例代码执行结果如下:

任务1或2在执行变量递减运算过程中已经完成过N多次任务切换,任务不同状态之间可以进行切换的可能性如下:

如果包含系统内核调度器,其时间片调度过程如下图所示,内核调度器负责不同任务的状态切换。

不同优先级任务如何执行
优先级从0开始分配,0为最低优先级,最高优先级为(configMAX_PRIORITIES – 1)。configMAX_PRIORITIES是一个用户自定义的常量允许多个任务使用相同的优先级,在没有更高优先级任务时,相同优先级的任务通过时间片轮转来获得执行时间(TEST01)OS调度器总是会确保最高优先级的任务先被运行,如果每个任务的优先级设定不同,则进入优先级抢占调度模式 prioritized preemptive scheduling,即下面TEST02示例的工作模式
TEST02示例中任务2的优先级比任务1小,因此任务2的优先级比任务1低。让我们看一下次数代码会如何运行。
static int_t os_test_task_1(void)
{
const char *task_name = R_OS_TaskGetCurrentName();
uint32_t loopcnt;
while(1) {
printf(“[%8d]: This is %s
”,
R_OS_SYSTICKS_TO_MS(R_OS_GetTickCount()), task_name);
loopcnt = 0x3FFFFFF;
while(loopcnt > 0) {
loopcnt--;
}
}
static int_t os_test_task_2(void)
{
const char *task_name = R_OS_TaskGetCurrentName();
uint32_t loopcnt;
while(1) {
printf("[%8d]: This is %s
",
R_OS_SYSTICKS_TO_MS(R_OS_GetTickCount()), task_name);
loopcnt = 0x3FFFFFF;
while(loopcnt > 0) {
loopcnt--;
}
}
static int_t os_main_task(void)
{
ptask1 = R_OS_TaskCreate("task1", (os_task_code_t)os_test_task_1, NULL, R_OS_ABSTRACTION_DEFAULT_STACK_SIZE, R_OS_TASK_MAIN_TASK_PRI);
ptask2 = R_OS_TaskCreate("task2", (os_task_code_t)os_test_task_2, NULL, R_OS_ABSTRACTION_DEFAULT_STACK_SIZE, R_OS_TASK_MAIN_TASK_PRI-1);
while(1){
R_OS_TaskSleep(1000);
}
}
运行结果如下:
可以看到,只有任务1在运行,任务2无法被运行。
因为任务1里面是一个while死循环,且没有主动释放时间片(后面章节会讨论到如何释放时间片),因此调度器始终在运行高优先级的任务1

Task sleep 和 task yield
| Task sleep 任务休眠 | Taskyield 任务让步 |
|---|---|
| 可以指定任意的delay时间(1ms的倍数)调用后会将当前任务转为blocked状态,并立即触发调度器实现任务轮转,执行ready任务列表中排在最前的任务如果没有其它ready的任务可以执行,则执行最低优先级的idle任务(后续说明)高优先级任务sleep时低优先级可以获得执行时间 | 不能指定时间只会释放当前时间片未使用的时间,会立即触发调度器切换到同优先级ready的任务,不必等待时间片结束如果没有同优先级任务会立即返回当前任务,高优先级任务执行yield,低优先级任务不会获得执行时间(后续示例TEST04中任务2不执行的原因) |
Task Sleep示例代码
TEST03示例与之前TEST02代码的区别是在任务1和任务2的while循环中添加了task sleep函数,每个循环开头先睡眠1000ms。
任务1 优先级:高
任务2 优先级:低
TEST02的结果是任务2得不到运行,但是TEST03中添加了Task Sleep,它是任务主动进入了睡眠状态,让出了时间片,可以让低优先级的任务也能够得到执行。
static int_t os_test_task_1(void)
{
……
while(1) {
printf("[%8d]: This is %s
",
R_OS_SYSTICKS_TO_MS(R_OS_GetTickCount()), task_name);
R_OS_TaskSleep(1000);
}
static int_t os_test_task_2(void)
{
……
while(1) {
printf("[%8d]: This is %s
",
R_OS_SYSTICKS_TO_MS(R_OS_GetTickCount()), task_name);
R_OS_TaskSleep(1000);
}
从执行结果可以看到,任务2得到了执行,且2个任务的执行间隔都是1000ms

让我们看一下此时2个任务是如何调度的

当绿色任务1切换到灰色,或蓝色任务2切换到灰色时,对应任务1,任务2的状态会被切换到blocked状态。即下图从Running状态切换到Blocked状态
除了task Sleep之外还有一些其它事件也会将当前任务切换到blocked状态
TaskSleepQueuesBinary semaphoresCounting semaphoresMutexRecursive mutexEvent group

如果我们将task sleep换成yield,那么代码会如何执行呢?
TEST04示例代码如下
static int_t os_test_task_1(void)
{
……
while(1) {
printf("[%8d]: This is %s
",
R_OS_SYSTICKS_TO_MS(R_OS_GetTickCount()), task_name);
R_OS_TaskYield();
}
static int_t os_test_task_2(void)
{
……
while(1) {
printf("[%8d]: This is %s
",
R_OS_SYSTICKS_TO_MS(R_OS_GetTickCount()), task_name);
R_OS_TaskYield();
}
其执行结果如下:

此时只有任务1得到了执行,低优先级的任务2无法运行,让我们看一下它的时间片调度情况
前面我们讲过,yield会释放当前时间片未使用完的时间,即如果1个时间片是1ms,while循环中的任务执行实际只需要500us,那么执行了yield之后,剩余的500us调度器会挂起任务1,让其他同优先级的任务能够尽快得到执行,而不必等待1ms时间片的结束,在某些特殊场景中此方法可以增强系统的实时性。
一般情况下yield由FreeRTOS的porting层实现,各平台移植代码可能略有不同,在RZA平台中,yield用“SWI 0”功能触发一个软中断,执行vector表的中断入口函,最终会调用FreeRTOS Kernel的调度器任务切换功能,类似于时间片运行终止的行为。
(R_OS_ABSTRACTION_CFG_PRV_SVC_HANDLER)

优先级抢占场景举例
我们用如下示例看一下高优先级任务是如何抢占低优先级任务的

TEST05示例代码如下:
// 任务1(高优先级)每隔1000ms唤醒一次
static int_t os_test_task_1(void)
{
……
while(1) {
printf("[%8d]: This is %s
",
(R_OS_GetTickCount()), task_name);
R_OS_TaskSleep(1000);
}
// 低优先级持续运行不释放时间片
static int_t os_test_task_2(void)
{
……
while(1) {
printf("[%8d]: This is %s
",
(R_OS_GetTickCount()), task_name);
loopcnt = 0x3FFFFFF;
while(loopcnt > 0) {
loopcnt--;
}
}
执行结果如下:

在高优先级任务睡眠期间,低优先级任务会不间断执行低优先级任务随时会被定时唤醒的高优先级打断,且被打断时低优先级任务自己并不知道被打断;高优先级任务从blocked状态恢复到ready状态后,会被优先转到running状态
系统调度示意如下图所示:

如何删除一个任务
任务可以通过 API 函数删除自身或其他任务。需要注意,
vTaskDelete() API 仅在
vTaskDelete() 中设置
FreeRTOSConfig.h 为 1 时可用。
INCLUDE_vTaskDelete
被删除的任务将不复存在,无法再次进入运行状态。空闲任务负责释放分配给已删除任务的内存。
内核会自动释放被任务删除在创建期间由内核自身分配的内存,但任务本身代码中由应用程序自己分配的任何内存或其他资源必须由应用程序自己显式释放。
让我们看一下TEST06示例中的任务删除代码, R_OS_TaskDelete函数是vTaskDelete()的封装。
// 任务2(高优先级),在执行结束后会删除自己
static int_t os_test_task_2(void)
{
…
while(1) {
printf("[%8d]: This is %s
",R_OS_GetTickCount(),task_name);
printf("[%8d]: %s will delete itself after 100ms
",R_OS_GetTickCount(),task_name);
R_OS_TaskSleep(100);
R_OS_TaskDelete(&ptask2);
}
}
// 任务1(低优先级),在执行过程中它创建了一个高优先级的任务2
static int_t os_test_task_1(void)
{
…
while(1) {
printf("[%8d]: This is %s
",R_OS_GetTickCount(),task_name);
printf("[%8d]: %s will create high priority task2
",R_OS_GetTickCount(),task_name);
ptask2 = R_OS_TaskCreate("task2", (os_task_code_t)os_test_task_2, NULL, R_OS_ABSTRACTION_DEFAULT_STACK_SIZE, R_OS_TASK_MAIN_TASK_PRI+1);
printf("[%8d]: %s will sleep 1000ms
",R_OS_GetTickCount(),task_name);
R_OS_TaskSleep(1000);
}
}
// 主任务只创建任务1,任务2由任务1来创建
static int_t os_main_task(void)
{
ptask1 = R_OS_TaskCreate("task1", (os_task_code_t)os_test_task_1, NULL, R_OS_ABSTRACTION_DEFAULT_STACK_SIZE, R_OS_TASK_MAIN_TASK_PRI);
while(1){
R_OS_TaskSleep(1000);
}
}
其执行结果如下:

由运行结果可以看出,低优先级任务task1创建了高优先级task2后,task2立刻被得到了运行,但是运行了一次后就执行了任务删除,因此任务2只运行了一次。直到下一次任务1再次创建task2
任务删除期间的系统调度行为如下:

关于任务删除的一些关键点:
一个任务可以删除自己或任何其它任务被删除的任务不会再进入到Running状态被删除的任务曾经用到的栈空间及任务控制块信息TCB存放的空间会由Kernel负责释放任务中应用自己通过malloc分配的内存,需要由应用自己先释放
IDLE任务
在所有任务都处于blocked状态时,处理器总是要执行一些程序,这个程序就是Idle任务Idle任务是在调度器初始化时被自动生成的最低优先级任务,以保证只有在空闲时运行而不影响其它任务Idle任务会清理已删除任务使用的资源,因此当有任务被删除后这个任务会短暂的处于eDeleted状态,等待idle任务有机会执行后,被删除的任务才会彻底销毁
typedef enum
{
eRunning = 0,/* 任务正在查询自身状态,因此必须处于运行状态 */
eReady, /* 被查询的任务处于就绪或待命就绪列表中 */
eBlocked, /* 被查询的任务处于阻塞状态 */
eSuspended, /* 被查询的任务处于挂起状态,或处于带有无限超时的阻塞状态 */
eDeleted, /* 被查询的任务已被删除,但其任务控制块(TCB)尚未释放 */
eInvalid /* 用作“无效状态”值 */
} eTaskState;
通过以上讲解,大家应该对以下问题有了一个比较清晰的答案:
FreeRTOS如何将CPU处理时间分配到每个任务上?FreeRTOS如何选择当前该执行哪个任务?每个任务的优先级不同会有什么样的系统调用行为?每个任务有哪些可能的状态?如何创建任务?怎样设置任务的参数?如何删除一个任务?idle task是做什么的?
堆内存的管理 (HEAP)
HEAP的基本概念
堆是什么?
堆是可以动态分配的内存池应用程序可以在任何时候分配一块内存和释放已分配的内存
为什么使用HEAP?
FreeRTOS的所有组件,例如任务,事件,信号量,消息队列等都需要一些内存来存储他们的控制块(CB)信息。应用程序可以在任意时间,使用任意多个组件,因此无法采用静态内存来提前预留这部分内存应用程序中存在2种不同的HEAP,FreeRTOS管理的heap,C标准库使用的heap,两种HEAP可同时使用
R_OS_Malloc 是pvPortMalloc接口的封装函数
R_OS_Free是vPortFree接口的封装函数

如果你的应用程序中使用了某些第三方的库,这些库有可能会调用stdlib里面的系统内侧分配和释放函数 malloc 和 free

FreeRTOS中的heap管理
FreeRTOS中有5中不同的heap管理文件
Heap的管理在FreeRTOS中属于移植层,瑞萨默认使用第五种管理方式,其他平台可能会有不同的移植选择。
heap_1.c
最简单的堆管理,只实现了pvPortMalloc(). 这种方式只能分配内存,不能释放内存
heap_2.c
简单实现了内存的分配 pvPortMalloc() 和释放 vPortFree() ,但是并不会将多个连续未使用块合并成一个大的内存块,长时间使用会产生较多的内存碎片
heap_3.c
只是简单将 pvPortMalloc() and vPortFree() 功能指向了编译器提供的标准malloc() 和 free() 接口
heap_4.c
实现了内存的分配pvPortMalloc() 和释放 vPortFree(),能够将多个连续未使用块合并成一个大的内存块,在一定程度上做了内存碎片整理的工作
heap_renesas.c
在heap_4.c的基础上增加了多个不连续的内存块的支持,
可以指定在哪个内存块上进行操作
例如:
HeapRegion_t xHeapRegions[] =
{
{( uint8_t * ) 0x80080000UL, 0x00060000UL}, /* R_MEMORY_REGION_DEFAULT */
{( uint8_t * ) 0x800E0000UL, 0x00020000UL}, /* R_MEMORY_REGION_NEW */
{( uint8_t * ) 0x00000000UL, 0x00000000UL}, /* Terminates the array */
};
内存的分配与释放示例
static int_t os_test_task_1(void)
{
…
while(1)
{
uint8_t *pBuf1 = R_OS_Malloc(1024, R_MEMORY_REGION_DEFAULT);
uint8_t *pBuf2 = malloc(1024);
if (pBuf1 != NULL) {
printf("%s allocated memory from FreeRTOS heap at [0x%x]
",task_name, (uint32_t)pBuf1);
}
else {
printf("%s failed to get memory from FreeRTOS heap
",task_name);
}
if (pBuf2 != NULL) {
printf("%s allocated memory from system heap at [0x%x]
",task_name, (uint32_t)pBuf2);
}
else {
printf("%s failed to get memory from system heap
",task_name);
}
if (pBuf1 != NULL) {
printf("%s release FreeRTOS memory [0x%x]
",task_name, (uint32_t)pBuf1);
R_OS_Free((void*)&pBuf1);
printf("FreeRTOS buf ptr = [0x%x]
",(uint32_t)pBuf1);
}
if (pBuf2 != NULL) {
printf("%s release system memory [0x%x]
",task_name, (uint32_t)pBuf2);
free(pBuf2);
printf("system buf ptr = [0x%x]
",(uint32_t)pBuf2);
}
R_OS_TaskSleep(1000);
}
*pBuf1 = R_OS_Malloc(1024, R_MEMORY_REGION_DEFAULT);
在FreeRTOS的HEAP上分配1024字节内存
*pBuf2 = malloc(1024);
在System的HEAP上分配1024字节内存
内存的释放和内存分配一定要成对使用,否则会造成内存泄漏,因此在程序结束应该分别调用
R_OS_Free((void*)&pBuf1); 和 free(pBuf2);
执行结果:

每个while循环新分配的内存地址都是相同的[0x800502f8] 或[0x8006c6e8],原因是每次释放内存后,没有其它应用再去分配FreeRTOS的释放函数需要传入指针的地址,释放完成后指针被清零,标准库的free函数只释放内存,不清零指针,应用中请确保不要在已释放的内存上继续操作
几种不同HEAP模式的比较

当多个应用任务频繁分配,释放内存时,FreeeRTOS是如何知道该从哪块内存分配的呢?
下图从上到下按照时间从先到后的顺序,举例说明了FreeRTOS内部使用单向链表记录管理内存空间的方法。

第一行中内存池都是可用的,还没有被分配过,PTR是一个数据结构指针,起始地址就是可用内存的地址。其中也包含了指向下一块可用内存块的指针,以及当前内存块的大小


第2行执行了一次malloc内存分配,将第一行的PTR指针返回给应用程序使用,将新的PTR信息放到了第一个使用内存块(BlockSize)的后面,同时标记下一个可用内存块地址,及当前块大小都有变化。
第3,4行也同样进行了内存分配动作,PTR指针依次会往后移动。
第5行将第3行分配的内存释放了,第3行分配的这块内存变为可用内存,也放置了一个PTR作为链表头,并记录当前可用块大小,这个PTR会指下一个可用快PTR的,图中红色箭头指示部分。
第6行有申请内存分配,但这个BlockSize比较大,当前链表第一块内存大小不足,FreeRTOS会检查下一个内存块是否足够大,发现内存块足够大,因此在下一块内存块中分配了红色BlockSize部分的内存。
第7行也是分配内存,但是其需要分配的内存比较小,第一块内存足够,因此在第一块内存中分配了红色BlockSize部分的内存。
第8行,释放了绿色背景的BlockSize部分的内存,这部分内存空间的大小被加到了第一个PTR位置处的表头信息中,因此可以看到这个PTR红色表示当前块大小信息的内容被变更了。
第9行释放了第6行分配的内存,释放后两个链表记录块的内存空间进行了合并,因此这一行PTR信息的链表指针(不指向下一个内存块了)及块大小均被更新了
HEAP使用时的注意事项
每次分配内存后一定要检测返回的buffer指针是否有效,由于内存碎片存在,即使剩余空间足够,也不一定能够分配出连续的一块内存小心内存泄漏,配对使用malloc和free,尤其在多线程操作中,一个任务负责分配,另一个任务负责释放的场景小心内存写越界,对分配的内存块进行写操作时,一定不能写出这块内存,否则会很难调试这类问题碎片问题,虽然已经有简单的碎片整理,但碎片问题还是存在,如果某个应用会频繁分配释放大小不一的内存,也可以考虑分配一块大的内存,由应用自己来管理这个大块内存的使用HEAP空间的设定,由于操作系统会在每个分配的内存块之前增加一个头(Meta info),因此可用的最大内存数量是略小于HEAP总空间的HEAP分配的内存初始值为随机,如需全零内存,应用需调用memset自己清零
消息队列(message QUEUE)
基本概念
Queue是存储消息内容的数据结构每一个Queue都是公共资源,可以被任何任务访问经常用于在任务和任务之间,任务和中断之间传输一段数据存储消息的内存来自HEAP,在调用R_OS_MessageQueueCreate 时由Kernel自动分配发送,读取Queue内容时采用FIFO模式Queue中的内容可以是一个word的真实数据,也可以是一个消息实体的内存地址指针-由应用自行决定发送消息时,消息内容(1个word)会被拷贝进Queue buffer,读取时会拷贝给读取者Queue队列满或空时,等待发送或接收的任务会进入blocked状态(不会消耗CPU资源)

之间讲过任务的Blocked状态,Queue是让任务进入Blocked状态的原因之一:
TaskSleepQueuesBinary semaphoresCounting semaphoresMutexRecursive mutexEvent group
Queue的常规操作场景:
一个任务发送,一个任务接收多个任务发送,单个任务接收
任务读取消息时的状态转换
任务如果读取到了message,则任务进入running状态处理该消息
如果任务尝试读取消息,但是消息队列为空,则该任务进入Blocked状态,不会被分配时间片运行
如果消息队列有新消息进入,则等待该消息的任务会被置于ready状态,等待系统调度器按照任务优先级高低将合适的任务唤醒运行。

发送消息示意如下:

Blocked to Ready:
如果有多个不同优先级的任务等待接收或发送,当条件允许时,只有最高优先级的任务会先发送或接收并进入Ready状态如果多个相同优先级的任务等待接收或发送,当条件允许时,等待时间最长的任务会先发送或接收并进入Ready状态
Queue示例1
直接发送U32数值作为消息内容
p_os_msg_queue_handle_t g_pmsg_handle;
static int_t os_test_task_1(void)
{
…
while (1)
{
printf("[%8d]: %s sent message [%d]
",
R_OS_GetTickCount(), task_name, msgData);
if (R_OS_MessageQueuePut(g_pmsg_handle, (p_os_msg_t)msgData) == TRUE) {
msgData++;
}
else {
printf(“[%8d]: %s sent message failed as queue is full
”,
R_OS_GetTickCount(), task_name, msgData);
}
R_OS_TaskSleep(1000);
}
}
static int_t os_test_task_2(void)
{
…
while (1)
{
p_os_msg_t p_msgData = 0;
R_OS_MessageQueueGet(g_pmsg_handle,
(p_os_msg_t)&p_msgData, 0, TRUE);
printf("[%8d]: %s received msg,
value=%d
",R_OS_GetTickCount(), task_name, p_msgData);
}
}
static int_t os_main_task(void)
{
…
ret = R_OS_MessageQueueCreate((p_os_msg_queue_handle_t)&g_pmsg_handle, 1);
ptask1 = R_OS_TaskCreate("task1", (os_task_code_t)os_test_task_1, NULL, R_OS_ABSTRACTION_DEFAULT_STACK_SIZE, R_OS_TASK_MAIN_TASK_PRI);
ptask2 = R_OS_TaskCreate("task2", (os_task_code_t)os_test_task_2, NULL, R_OS_ABSTRACTION_DEFAULT_STACK_SIZE, R_OS_TASK_MAIN_TASK_PRI);
…
}
主函数创建一个长度为1的消息队列,及2个任务
任务1每隔1秒发送1次消息,其它时间处于blocked状态,消息体中的数值就是变量msgData的值
任务2一直等待新消息的接收,处于blocked状态,接收到新消息后任务2唤醒,收到的消息不是指针而是一个立即数
打印结果如下:

Queue示例2
发送大于U32的消息体和Queue Peek功能
示例场景如下:
任务1每隔1秒随机发送3种不同的消息消息,消息中包含一个随机ID和一个32位随机数。ID为2或3或4任务2只接收ID为2的消息,任务3只接收ID为3的消息,任务4只接收ID为4的消息,忽略不属于自己的消息如果是自己的消息,接收后从对列中删除消息

typedef struct
{
uint32_t msgID;
uint32_t msgData;
}R_MSG_BODY;
static int_t os_test_task_1(void)
{
…
While(1){
R_MSG_BODY msg;
msg.msgID = (rand() % 3) + 2; //random ID between 2~4
msg.msgData = rand();
if (R_OS_MessageQueuePut(g_pmsg_handle, (p_os_msg_t)&msg))
{
printf("[%8d]: %s sent message ID=%d, value=%d
",
R_OS_GetTickCount(), task_name, msg.msgID, msg.msgData);
}
else
{
printf("[%8d]: %s sent message failed
",
R_OS_GetTickCount(), task_name);
}
R_OS_TaskSleep(1000);
}
…
}
static int_t os_test_task_2(void)
{
…
While(1){
R_MSG_BODY *p_msg;
while (pdPASS != xQueuePeek((xQueueHandle) g_pmsg_handle,
(void *) (&p_msg), R_OS_ABSTRACTION_EV_WAIT_INFINITE));
if (p_msg->msgID == 2)
{
R_OS_MessageQueueGet(g_pmsg_handle, (p_os_msg_t)&p_msg, 0, FALSE);
printf("[%8d]: %s received his msg, ID=%d, value=%d
",
R_OS_GetTickCount(), task_name, p_msg->msgID, p_msg->msgData);
}
}
}
// os_test_task_3() 略,和os_test_task_2类似
// os_test_task_4() 略,和os_test_task_2类似
消息体内容大于U32时,需要将消息放入结构体中
task2,3,4中定义接收消息的指针查询是否有新消息,无限循环等待(等待期间不消耗CPU)
如果是自己的消息,接收下来并从FIFO中删除
xQueuePeek() 可以读取消息,读取后并不会将消息从FIFO队列中删除,用于查询消息;R_OS_MessageQueueGet(), 即xQueueReceive()的封装层,会读取消息,并将消息从队列中删除。
输出结果如下:

信号量 (SEMAPHORE)
基本概念
信号量是一种特殊的Queue,Queue中每个消息的长度为0,不能传递数据
信号量的使用场景:
场景1:提供一种极低系统开销的通信机制
用于任务和任务之间通信:一个任务处理完后通知另外一个任务继续处理用于中断和任务之间进行同步操作:在中断中发送信号量,等待这个信号量的任务可以被唤醒
场景2:资源保护
Binary Semaphore 消息数量为1, Counting Semaphore消息数量可设定如果某个资源同时只能支持1个用户则使用Binary Semaphore 如果某个资源最大支持N个用户同时使用则需要使用 Counting Semaphore,并设定数量为N用户使用资源时计数器会加1,用户使用结束时计数器会减1,得不到资源的任务会处于blocked状态
中断和任务之间的同步操作
这种场景通常需要尽量避免在ISR中运行比较耗时的代码,原因是:
ISR会打断最高优先级的任务在执行ISR时其它中断也有可能发生中断嵌套会增加系统复杂性,减小ISR执行时间可以减少中断嵌套的机会
比较通用的方法是将一些比较耗时的中断处理延时到一个特殊任务中 ,这个特殊任务的优先级通常会高于其它任务,从执行效果上看,就像这个特殊任务在中断里执行(类似DSR或Linux中的底半部)
如下图所示:
R_OS_SemaphoreRelease() 是 xSemaphoreGive和xSemaphoreGiveFromISR的平台层封装函数R_OS_SemaphoreWait() 是xSemaphoreTakeFromISR和xSemaphoreTake的平台层封装函数

示例代码:
static void gpio_interrupt_isr(uint32_t int_sense)
{
// 中断里面执行信号量资源释放
R_OS_SemaphoreRelease((p_semaphore_t)&g_psemaphore_handle);
}
static int_t os_test_task_1(void)
{
…
//信号量初始化后默认第一次是可以拿到资源的(封装层特性),第一个wait可以顺利通过
R_OS_SemaphoreWait((p_semaphore_t)&g_psemaphore_handle,R_OS_ABSTRACTION_EV_WAIT_INFINITE);
while(1)
{
//后续信号量无法拿到资源,需要等待ISR将这个信号源释放 R_OS_SemaphoreWait((p_semaphore_t)&g_psemaphore_handle,R_OS_ABSTRACTION_EV_WAIT_INFINITE);
//已经等到ISR释放的信号量并占用这个资源,执行打印,执行之后并不释放资源,循环后等待下一个ISR来释放资源
printf("%s received semaphore from interrupt
",task_name);
}
}
static int_t os_test_task_2(void)
{
… … //周期性打印输出
}
static int_t os_main_task(void)
{
…
/* Use PJ1(SW3) as Interrupt Source */
//使用SW3键做为中断源,产生中断后执行注册的中断处理函数gpio_interrupt_isr()
R_GPIO_PinAssignmentSet(GPIO_PORT_J_PIN_1, GPIO_ASSIGN_TO_GPIO);
R_GPIO_PinDirectionSet(GPIO_PORT_J_PIN_1, GPIO_DIRECTION_INPUT);
R_GPIO_PinTintSet(GPIO_PORT_J_PIN_1, GPIO_TINT_ENABLE);
R_INTC_RegistIntFunc(INTC_ID_TINT_TINT3, &gpio_interrupt_isr);
R_TINT_SetSense(INTC_TINT3, TINT_SENSE_RISINGEDGE);
R_INTC_SetPriority(INTC_ID_TINT_TINT3, 28);
R_INTC_Enable(INTC_ID_TINT_TINT3);
//新建1个资源使用者数量=1的信号量
R_OS_SemaphoreCreate ((p_semaphore_t)&g_psemaphore_handle, 1);
//需要执行ISR后续任务的优先级一般会比较高
ptask1 = R_OS_TaskCreate("task1", (os_task_code_t)os_test_task_1, NULL, R_OS_ABSTRACTION_DEFAULT_STACK_SIZE, R_OS_TASK_MAIN_TASK_PRI+1); //DSR Task
ptask2 = R_OS_TaskCreate("task2", (os_task_code_t)os_test_task_2, NULL, R_OS_ABSTRACTION_DEFAULT_STACK_SIZE, R_OS_TASK_MAIN_TASK_PRI);//Normal Task
…
}
执行结果如下,每按一次按键,等待信号量的task1都会被运行一次,由于task1没有运行在中断上下文,因此它可以运行一些比较耗时的任务而不影响其它中断的实时响应。

如果接受信号量的任务优先级比主任务优先级高,如代码中实现时把task1的优先级升高了一级,系统调度大致如下:
task2优先级高,在没有等到信号量之前已知处于blocked状态,task1任务会被中断及task2打断。

假设task1和task2的任务优先级相同,那么系统调度会有何不同呢,通过如下示意图可以看出区别。

资源保护示例
假如有一个硬件资源可以同时处理最多2个任务,如果实现呢?

示例代码,模拟四个任务需要使用某个硬件模块,初始化信号量时使用的counter参数为2
每个人物都会去获取这个信号量,如果拿不到资源则该任务处于blocked状态,拿到资源后执行打印并在任务结束释放资源。
static int_t os_test_task_4(void)
{
…
while(1)
{
R_OS_SemaphoreWait((p_semaphore_t)&g_psemaphore_handle,R_OS_ABSTRACTION_EV_WAIT_INFINITE);
printf("%s got one semaphore resource, start processing ...
",task_name);
loopcnt = 0x3FFFFFF;
while(loopcnt > 0)
{
loopcnt--;
}
printf("%s finished processing, release one semaphore resource
",task_name);
R_OS_SemaphoreRelease((p_semaphore_t)&g_psemaphore_handle);
loopcnt = 0x3FFFFFF;
while(loopcnt > 0)
{
loopcnt--;
}
}
}
static int_t os_test_task_3(void) // 略
static int_t os_test_task_2(void) // 略
static int_t os_test_task_1(void) // 略
static int_t os_main_task(void)
{
…
R_OS_SemaphoreCreate ((p_semaphore_t)&g_psemaphore_handle, 2);
ptask1 = R_OS_TaskCreate("task1", (os_task_code_t)os_test_task_1, NULL, R_OS_ABSTRACTION_DEFAULT_STACK_SIZE, R_OS_TASK_MAIN_TASK_PRI);
ptask2 = R_OS_TaskCreate("task2", (os_task_code_t)os_test_task_2, NULL, R_OS_ABSTRACTION_DEFAULT_STACK_SIZE, R_OS_TASK_MAIN_TASK_PRI);
ptask3 = R_OS_TaskCreate("task3", (os_task_code_t)os_test_task_3, NULL, R_OS_ABSTRACTION_DEFAULT_STACK_SIZE, R_OS_TASK_MAIN_TASK_PRI);
ptask4 = R_OS_TaskCreate("task4", (os_task_code_t)os_test_task_4, NULL, R_OS_ABSTRACTION_DEFAULT_STACK_SIZE, R_OS_TASK_MAIN_TASK_PRI);…
}
执行结果如下:最多2个任务可以获取到资源同时运行,只有其中一个任务释放了资源后,其他任务才会得到该资源进入运行状态。

互斥锁(MUTEX)
基本概念
互斥锁是一种特殊的二值信号量(资源数量为1的信号量)使用场景:某个受保护的资源只能有1个任务访问它同样需要配对使用:“Release”,“Wait”或“Acquire”
优先级反转问题
Mutex和Semaphore的区别:
Mutex支持优先级继承机制,但Binary Semaphore不支持
为什么需要优先级继承机制呢?以下场景会有什么问题呢?
尽管任务3拥有最高优先级,但它不得不等中优先级任务执行结束,返回被抢占的低优先级任务,等低优先级任务执行结束并释放资源锁后才能继续执行任务3,因此中优先级任务会优先高优先级任务执行,应用程序执行的优先级被反转了,这与设计意图不符,即高优先级应该能抢占低优先级和中优先级的任务。

这个场景的系统调度行为如下:

这个场景中,如果持有锁的低优先级任务长时间被其它中优先级任务抢占,会导致高优先级任务长时间得不到执行。
拥有优先级继承功能的Mutex可用解决此类问题,但Semaphore不行
使用了Mutex后系统调度机制变更为:

对比之前的情况,最高优先级的执行时间被提前了,不再会被中优先级抢占
示例代码
示例中3个任务访问一个共享资源
static int_t os_test_task_1(void)
{
…
while(1)
{
R_OS_MutexAcquire(g_pmutex);
printf("%s got Mutex, Start to access share memory...
",task_name);
R_OS_TaskSleep(300);
printf("%s release Mutex, Stop to access share memory...
",task_name);
R_OS_MutexRelease(g_pmutex);
R_OS_TaskSleep(300);
}}
static int_t os_test_task_2(void)
{
…
while(1)
{
R_OS_MutexAcquire(g_pmutex);
printf("%s got Mutex, Start to access share memory...
",task_name);
R_OS_TaskSleep(200);
printf("%s release Mutex, Stop to access share memory...
",task_name);
R_OS_MutexRelease(g_pmutex);
R_OS_TaskSleep(200);
}}
static int_t os_test_task_3(void)
{
…
while(1)
{
R_OS_MutexAcquire(g_pmutex);
printf("%s got Mutex, Start to access share memory...
",task_name);
R_OS_TaskSleep(100);
printf("%s release Mutex, Stop to access share memory...
",task_name);
R_OS_MutexRelease(g_pmutex);
R_OS_TaskSleep(100);
}}
static int_t os_main_task(void)
{
…
g_pmutex = R_OS_MutexCreate();
ptask1 = R_OS_TaskCreate("task1", (os_task_code_t)os_test_task_1, NULL, R_OS_ABSTRACTION_DEFAULT_STACK_SIZE, R_OS_TASK_MAIN_TASK_PRI);
ptask2 = R_OS_TaskCreate("task2", (os_task_code_t)os_test_task_2, NULL, R_OS_ABSTRACTION_DEFAULT_STACK_SIZE, R_OS_TASK_MAIN_TASK_PRI);
ptask3 = R_OS_TaskCreate("task3", (os_task_code_t)os_test_task_3, NULL, R_OS_ABSTRACTION_DEFAULT_STACK_SIZE, R_OS_TASK_MAIN_TASK_PRI);
;…
}
执行结果:

互斥锁使用注意事项
避免互斥锁死锁是需要应用程序考虑的问题之一。
场景1:两个任务,两把锁,每个任务都等待对方持有的锁,谁也执行不了场景2:随着任务复杂性提高(加锁保护的代码保护很多子函数),一个子函数持有了一把锁,再后续操作中有另一个子函数想再次持有相同的锁。

最好的办法是在设计代码时审查
使用R_OS_MutexWait方法,这个获取锁的接口增加了等待超时
R_OS_MutexWait平台层移植代码如下
bool_t R_OS_MutexWait (pp_mutex_t pp_mutex, uint32_t time_out)
{
bool_t ret_val = false;
/* Check to see if it is pointing to NULL */
if (NULL == ( *pp_mutex))
{
/* Create the mutex */
*pp_mutex = xSemaphoreCreateMutex();
}
/* Check that the mutex was created */
if (NULL != ( *pp_mutex))
{
if (R_OS_ABSTRACTION_EV_WAIT_INFINITE == time_out)
{
/*Cast to uint32_t from tick type*/
time_out = portMAX_DELAY;
}
/* Try Get mutex */
if (xSemaphoreTake(( *pp_mutex), time_out))
{
ret_val = true;
}
}
return (ret_val);
}
使用recursive mutex 避免第二类场景的死锁问题. 在拿锁(take)时计数器会加1,解锁(give)时计数器会减1,可直接调用FreeRTOS的如下接口
xSemaphoreCreateRecursiveMutex()
xSemaphoreTakeRecursive()
xSemaphoreGiveRecursive()
软件定时器 (TIMER)
软件定时器和硬件定时器或计数器没有关系,他们由OS内核来实现,一般情况下对于Timer的数量多少是没有限制的软件定时器工作在一个最高优先级的任务中应用程序在任何时间都可以创建一个定时器,当定时器到时,会执行创建时指定的callback函数
在Timer回调函数中不能调用会使回调函数进入blocked状态的FreeRTOS接口,因为它会导致其它timer无法被触发在Timer回调函数中不能调用vTaskDelay()可用调用消息队列Queue的接口,但需要指定等待时间为0,即如果不成功则立即返回
有两种类型的定时器: one-short timer 和 auto-reload timer
One-short: 它只会调用1次回调函数,需要手动restart启动下一次定时Auto-reload: 周期性的执行回调函数
定时器Daemon任务
FreeRTOS调度器启动时会自动启动一个定时器的Daemon Task,Daemon task 主要处理2类事:
是否有一个定时器已经到时?如果到时就调用对应的回调函数如果接收到一个创建定时器/控制定时器的消息,则处理这个消息。(Timer的API接口会通过Queue和Daemon任务通讯)

定时器实现的方法
定时器维护了一个列表,列表里面是各个定时器属性(什么时间到时?什么类型?)这个列表从时间轴上看如下图,从左到右是定时器到时先后的顺序

Daemon task处理流程

定时器用于场景举例

static int_t os_main_task(void)
{
…
int32_t x;
uint8_t timer_active[5];
for( x = 0; x < 5; x++ )
{
g_xTimers[x] = xTimerCreate(“Timer”, (R_OS_MS_TO_SYSTICKS(100 *(x+1)), pdTRUE,( void * ) x, vTimerCallback);
if( g_xTimers[ x ] == NULL ) {
printf("Timer Create failed
");
}
else {
if( xTimerStart( g_xTimers[ x ], 0 ) != pdPASS ) {
printf("Timer start failed
");
}
else {
timer_active[x] = 1;
}
}
}
while(1)
{
for( x = 0; x < 5; x++ ){
if( xTimerIsTimerActive(g_xTimers[x]) == pdFALSE && timer_active[x] == 1) {
printf("[%8d]: Timer[%d] was stopped
",R_OS_SYSTICKS_TO_MS(R_OS_GetTickCount()),x);
timer_active[x] = 0;
}
}
R_OS_TaskSleep(50);
}
}
void vTimerCallback( TimerHandle_t pxTimer )
{
int32_t lArrayIndex;
const int32_t xMaxExpiryCountBeforeStopping = 10;
lArrayIndex = ( int32_t ) pvTimerGetTimerID( pxTimer );
g_lExpireCounters[ lArrayIndex ] += 1;
if( g_lExpireCounters[ lArrayIndex ] == xMaxExpiryCountBeforeStopping )
{
xTimerStop( pxTimer, 0 );
}
}
执行结果如下:

事件组 (EVENT group)
通过前面的章节,我们知道使用Semaphore可以在线程(任务)之间做消息同步。
也可以使用Queue在单任务与其它多个任务之间传递消息。
如果是更复杂的场景,例如如下情况该使用那种机制呢?


这些复杂场景就比较适合使用EVENT group事件组。
基本概念
Event groups 允许处于blocked状态的任务,等待一个或多个事件触发而变为ready状态Event groups 当等待组合的事件发生时:
事件的组合可以是“OR”,也可以是“AND”
虽然应用可以通过多个信号量协同工作来实现类似的功能,但往往会需要更多的内存,及外部控制逻辑。EventGroup用一个U32的变量来存储所有的事件,每个bit代表一个事件
bit=0 – 事件未发生bit=1 – 事件已发生
FreeRTOS有两种不同的配置:
configUSE_16_BIT_TICKS = 1 -> 8 usable event bitsconfigUSE_16_BIT_TICKS = 0 -> 24 usable event bits

事件组API

事件组示例

//任务1 每100ms设置事件19
static void os_test_task_1 (void)
{
…
R_OS_TaskSleep(100);
while(1)
{
EventBits_t uxBits;
printf("[%8d]: %s set Event 19
", R_OS_GetTickCount(), task_name);
uxBits = xEventGroupSetBits(g_eventgroup, BIT_19 );
R_OS_TaskSleep(100);
}
//任务2 每150ms设置事件16
static void os_test_task_2 (void)
{
…
R_OS_TaskSleep(100);
while(1)
{
EventBits_t uxBits;
printf("[%8d]: %s set Event 16
", R_OS_GetTickCount(), task_name);
uxBits = xEventGroupSetBits(g_eventgroup, BIT_16 );
R_OS_TaskSleep(150);
}
//任务3一直等待事件19,事件触发后不清除事件标记,接收事件后设置事件22
static void os_test_task_3 (void){
while(1) {
EventBits_t uxBits;
uxBits = xEventGroupWaitBits(g_eventgroup, BIT_19, pdFALSE, pdFALSE, portMAX_DELAY );
printf("[%8d]: %s process Event 19(keep it) and set Event 22
", R_OS_GetTickCount(), task_name);
uxBits = xEventGroupSetBits(g_eventgroup, BIT_22 );
}
}
//任务4一直等待事件19,事件触发后清除事件标记,接收事件后设置事件18
static void os_test_task_4 (void){
while(1) {
EventBits_t uxBits;
uxBits = xEventGroupWaitBits(g_eventgroup, BIT_19, pdFALSE, pdFALSE, portMAX_DELAY );
printf("[%8d]: %s process Event 19(clear it) and set Event 22
", R_OS_GetTickCount(), task_name);
uxBits = xEventGroupSetBits(g_eventgroup, BIT_18 );
}
}
//任务5一直等待事件16或18或22,事件触发后清除事件标记
static void os_test_task_5 (void){
const char *task_name = R_OS_TaskGetCurrentName();
while(1) {
EventBits_t uxBits;
uxBits = xEventGroupWaitBits(g_eventgroup, BIT_16 | BIT_18 | BIT_22, pdTRUE, pdFALSE, portMAX_DELAY );
if ((uxBits & BIT_16) == BIT_16)
printf("[%8d]: %s process Event 16(Clear it)
",R_OS_SYSTICKS_TO_MS(R_OS_GetTickCount()), task_name);
if ((uxBits & BIT_18) == BIT_18)
printf("[%8d]: %s process Event 18(Clear it)
",R_OS_SYSTICKS_TO_MS(R_OS_GetTickCount()), task_name);
if ((uxBits & BIT_22) == BIT_22)
printf("[%8d]: %s process Event 22(Clear it)
",R_OS_SYSTICKS_TO_MS(R_OS_GetTickCount()), task_name);
}}
//创建事件组和5个同优先级任务
static int_t os_main_task(void)
{
…
g_eventgroup = xEventGroupCreate();
if( g_eventgroup == NULL ) printf("Failed to Create EventGroup
");
ptask1 = R_OS_TaskCreate("task1", (os_task_code_t)os_test_task_1, NULL, R_OS_ABSTRACTION_DEFAULT_STACK_SIZE, R_OS_TASK_MAIN_TASK_PRI);
ptask2 = R_OS_TaskCreate("task2", (os_task_code_t)os_test_task_2, NULL, R_OS_ABSTRACTION_DEFAULT_STACK_SIZE, R_OS_TASK_MAIN_TASK_PRI);
ptask3 = R_OS_TaskCreate("task3", (os_task_code_t)os_test_task_3, NULL, R_OS_ABSTRACTION_DEFAULT_STACK_SIZE, R_OS_TASK_MAIN_TASK_PRI);
ptask4 = R_OS_TaskCreate("task4", (os_task_code_t)os_test_task_4, NULL, R_OS_ABSTRACTION_DEFAULT_STACK_SIZE, R_OS_TASK_MAIN_TASK_PRI);
ptask5 = R_OS_TaskCreate("task5", (os_task_code_t)os_test_task_5, NULL, R_OS_ABSTRACTION_DEFAULT_STACK_SIZE, R_OS_TASK_MAIN_TASK_PRI);
…
}
执行结果:

此文章主要讲述FreeRTOS主要功能的工作机制以及应用程序的使用方法,希望能够对使用FreeRTOS的朋友起到一些帮助和启发,文章中可能局部还有描述不太贴切的地方,近表达个人观点供大家参考。

