FreeRTOS一些常识笔记之快速上手

简介: 用户无需关心时间信息内核负责计时,并由相关的API完成,从而使得用户的应用程序代码结构更简单。

一、为啥要用实时多任务操作系统

real-time Operate System 简称有:RTOS,有如下的好处:


用户无需关心时间信息

内核负责计时,并由相关的API完成,从而使得用户的应用程序代码结构更简单。

模块化、可拓展性强

也正是由于第一点的原因,程序性能不易受底层硬件更改的影响。姐,各个任务是独立的模块,每个模块都有明确的目

的,降低了代码的耦合性。

效率高

内核可以让软件完全由事件驱动,因次,轮询未发生的事件是不浪费时间的。相当于用中断来进行任务切换。

中断进程更短

通过把中断的处理推迟到用户创建的任务中,可以使得中断处理程序非常短。

二、核心的C文件和头文件

C文件

b3b748dad08e4ad1a92d55d23d7b3d72.png

头文件

86e79173927149909ed08295ad61d04f.png三、两个数据类型和变量的定义方法

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

变量名

每个变量的前缀表示的含义


ad85a00e18db4506bbb9f463b42306c3.png

函数名

函数名前缀有有2部分:返回值类型、在哪个文件定义。

017d1af2aee248c09a56cfbe57b9c164.png

宏的名

宏的名字是大小,可以添加小写的前缀。前缀是用来表示:宏在哪个文件中定义1aadc3cb4ee24b4392c5d93c7e08c036.png

通用的宏定义如下:

5d368cceccf5415a8506c87697b03b45.png

四、内存管理中的几个API

提及内存管理就必须说一下堆和栈

堆,heap,就是一块空闲的内存,需要提供管理函数

  • malloc:从堆里划出一块空间给程序使用
  • free:用完后,再把它标记为"空闲"的,可以再次使用

  • cccd1b494bd243b294ae74719d81bfb0.png
  • 栈,stack,函数调用时局部变量保存在栈中,当前程序的环境也是保存在栈中
  • 可以从堆中分配一块空间用作栈
  • 在FreeRTOS中内存管理的接口函数(API)为:

1、pvPortMalloc 、vPortFree,对应于C库的malloc、free。

void * pvPortMalloc( size_t xWantedSize ); //分配内存,如果分配内存不成功,则返回值为NULL。
void vPortFree( void * pv );//释放内存

2、当前还有多少空闲内存,这函数可以用来优化内存的使用情况。比如当所有内核对象都分配好后,执行此函数返回2000,那么configTOTAL_HEAP_SIZE就可减小2000。

size_t xPortGetFreeHeapSize( void );

3、空闲内存的最小值

size_t xPortGetMinimumEverFreeHeapSize( void );//程序运行过程中,空闲内存容量的最小值。

五、创建任务和删除任务

啥叫任务?

任务就一个函数,但要注意的是

d521244ebefc41c6a78f7ac46f11856f.png

示例

void ATaskFunction( void *pvParameters ) 
{ 
    /* 对于不同的任务,局部变量放在任务的栈里,有各自的副本 */
    int32_t lVariableExample = 0;
    /* 任务函数通常实现为一个无限循环 */
    for( ;; ) 
    { 
        /* 任务的代码 */ 
    }
    /* 如果程序从循环中退出,一定要使用vTaskDelete删除自己 * NULL表示删除的是自己 */
    vTaskDelete( NULL ); 
    /* 程序不会执行到这里, 如果执行到这里就出错了 */ 
}

创建任务API

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 ); // 任务句柄, 以后使用 它来操作这个任务

里面的参数说明如下:


b5b40e6cd5064a61b5b646e43f49ae08.png

示例:

任务1代码:

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代码:

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函数:

int main( void )
{ 
    prvSetupHardware(); 
    xTaskCreate(vTask1, "Task 1", 1000, NULL, 1, NULL); 
    xTaskCreate(vTask2, "Task 2", 1000, NULL, 1, NULL); 
    /* 启动调度器 */ 
    vTaskStartScheduler(); 
    /* 如果程序运行到了这里就表示出错了, 一般是内存不足 */ 
    return 0; 
}

运行结果:

d04177e8b1054512afae4d9be079d191.png解释:


main函数中创建任务1,优先级为1。任务1运行时,它创建任务2,任务2的优先级是2。


任务2的优先级最高,它马上执行。


任务2打印一句话后,就删除了自己。


任务2被删除后,任务1的优先级最高,轮到任务1继续运行,它调用

vTaskDelay() 进入Block状态


任务1 Block期间,轮到Idle任务执行:它释放任务2的内存(TCB、栈)


时间到后,任务1变为最高优先级的任务继续执行。


如此循环。


六、任务优先级和Tick

任务优先级


高优先级的任务先运行。

优先级的取值范围是:0~(configMAX_PRIORITIES – 1),数值越大优先级越高。

使用uxTaskPriorityGet来获得任务的优先级,

使用参数xTask来指定任务,设置为NULL表示获取自己的优先级。

UBaseType_t uxTaskPriorityGet( const TaskHandle_t xTask );

使用vTaskPrioritySet 来设置任务的优先级,

使用参数xTask来指定任务,设置为NULL表示设置自己的优先级;

参数uxNewPriority表示新的优先级,取值范围是0~(configMAX_PRIORITIES – 1)。

void vTaskPrioritySet( TaskHandle_t xTask, UBaseType_t uxNewPriority );

Tick

对于相同优先级的任务的话,它们“轮流”执行。怎么轮流?你执行一会,我执行一会。那么这个一会就是使用Tick定义的

vTaskDelay(2); // 等待2个Tick,假设configTICK_RATE_HZ=100, Tick周期时10ms, 等待20ms 
// 还可以使用pdMS_TO_TICKS宏把ms转换为tick 
vTaskDelay(pdMS_TO_TICKS(100)); // 等待100ms

七、任务的几种状态

  1. 阻塞状态(Blocked)
  2. 暂停状态(Suspended)
  3. 就绪状态(Ready)

任务转换图

1c6fb9c0ea1e4240b9acee582d2c9e8f.png

七、两个Delay函数

  • vTaskDelay:至少等待指定个数的Tick Interrupt才能变为就绪状态
  • vTaskDelayUntil:等待到指定的绝对时刻,才能变为就绪态
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 );

详细介绍:


使用vTaskDelay(n)时,进入、退出vTaskDelay的时间间隔至少是n个Tick中断

使用xTaskDelayUntil(&Pre, n)时,前后两次退出xTaskDelayUntil的时间至少是n个Tick中断

1、退出xTaskDelayUntil时任务就进入的就绪状态,一般都能得到执行机会

2、所以可以使用xTaskDelayUntil来让任务周期性地运行

0ea71941bbfc4ca38774a651c5b41e89.png

示例:

本程序会创建2个任务:


Task1:

1、高优先级

2、设置变量flag为1,然后调用 vTaskDelay(xDelay50ms); 或vTaskDelayUntil(&xLastWakeTime, xDelay50ms);


Task2:

1、低优先级

2、设置变量flag为0


main函数

int main( void )
{
    prvSetupHardware();
    /* Task1的优先级更高, Task1先执行 */ 
    xTaskCreate( vTask1, "Task 1", 1000, NULL, 2, NULL ); 
    xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL ); 
    /* 启动调度器 */ 
    vTaskStartScheduler(); 
    /* 如果程序运行到了这里就表示出错了, 一般是内存不足 */ 
    return 0; 
}

Task1的代码中使用条件开关来选择Delay函数,把 #if 1 改为 #if 0 就可以使用 vTaskDelayUntil

void vTask1( void *pvParameters ) 
{ 
    const TickType_t xDelay50ms = pdMS_TO_TICKS( 50UL ); 
    TickType_t xLastWakeTime; int i; 
    /* 获得当前的Tick Count */ 
    xLastWakeTime = xTaskGetTickCount(); 
    for( ;; ) 
    { 
        flag = 1;
        /* 故意加入多个循环,让程序运行时间长一点 */ 
        for (i = 0; i <5; i++)
            printf( "Task 1 is running\r\n" );
##if 1 
    vTaskDelay(xDelay50ms);
##else 
    vTaskDelayUntil(&xLastWakeTime, xDelay50ms); 
##endif 
    }
}

Task2的代码

void vTask2( void *pvParameters ) 
{ 
    for( ;; )
    { 
        flag = 0; 
        printf( "Task 2 is running\r\n" ); 
    } 
}

使用Keil的逻辑分析观察flag变量的bit波形,如下:

  • flag为1时表示Task1在运行,flag为0时表示Task2在运行,也就是Task1处于阻塞状态
  • vTaskDelay:指定的是阻塞的时间
  • vTaskDelayUntil:指定的是任务执行的间隔、周期

  • 0f676b8131024704b2a5e456c1f52111.png
  • 15686669f4954ee1875710eb2cbc4415.png
  • 八、空闲任务和钩子函数

空闲任务

为什么要有空闲函数呢?


因为一个良好的程序,它的任务都是事件驱动的:平时大部分时间处于阻塞状态。有可能我们自己创建的所有任务都无法执行,但是调度器必须能找到一个可以运行的任务:所以,我们要提供空闲任务。


在使用 vTaskStartScheduler() 函数来创建、启动调度器时,这个函数内部会创建空闲任务:


空闲任务优先级为0:它不能阻碍用户任务运行

空闲任务要么处于就绪态,要么处于运行态,永远不会阻塞

空闲任务的优先级为0,这意为着一旦某个用户的任务变为就绪态,那么空闲任务马上被切换出去,让这个用户任务运行。在这种情况下,我们说用户任务"抢占"(pre-empt)了空闲任务,这是由调度器实现的。

要注意的是:如果使用 vTaskDelete() 来删除任务,就要确保空闲任务有机会执行,否则就无法释放被删除任务的内存。


钩子函数


钩子函数在空闲任务中添加,空闲任务每执行一次,钩子函数就会被调用一次,那么钩子函数能干些什么事呢?


执行一些低优先级的、后台的、需要连续执行的函数

测量系统的空闲时间:空闲任务能被执行就意味着所有的高优先级任务都停止了,所以测量空闲任务占据的时间,就可以算出处理器占用率。

让系统进入省电模式:空闲任务能被执行就意味着没有重要的事情要做,当然可以进入省电模式 了。

钩子函数使用过程中应该注意:


不能导致空闲任务进入阻塞状态、暂停状态

如果你会使用 vTaskDelete()来删除任务,那么钩子函数要非常高效地执行。如果空闲任务移植 卡在钩子函数里的话,它就无法释放内存。

如果想使用钩子函数


在FreeRTOSConfig.h中,把configUSE_MALLOC_FAILED_HOOK定义为1

提供vApplicationMallocFailedHook函数

pvPortMalloc失败时,才会调用此函数

2ee29843c4814446b4c1b4ffa7106b5f.png因为只能一个任务处在运行状态中,所以需要调度来实现不同任务进入运行状态。

调度算法的行为主要体现为:

  • 高优先级的任务先运行
  • 同优先级的就绪态任务如何被选中

从3个角度统一理解多种调度算法:

cdaccc20307d486e9df5db2bd4006ec3.png

33138ee5cf6e4692aa081386093e0b60.png上表解释:


A:可抢占+时间片轮转+空闲任务让步

B:可抢占+时间片轮转+空闲任务不让步

C:可抢占+非时间片轮转+空闲任务让步

D:可抢占+非时间片轮转+空闲任务不让步

E:合作调度

示例:


注:


任务1优先级0

任务2优先级0

任务3优先级2(最高优先级)

是否抢占对比

在 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
————————————————
版权声明:本文为CSDN博主「小浩编程」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/m0_48216397/article/details/125024729
  • 抢占时:高优先级任务就绪时,就可以马上执行

  • 不抢占时:优先级失去意义了,既然不能抢占就只能协商了,图中任务1一直在运行(一点都没有协商精神),其他任务都无法执行。即使任务3的 vTaskDelay 已经超时、即使它的优先级更高,都没办法执行。
  • 97164e54adef4380a747476eb7da9d5d.png
  • 9fb4ac4af3a449e8b94bf6539f2f4858.png
  • 是否时间片轮转对比

在 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
  • 时间片轮转:在Tick中断中会引起任务切换
  • 时间片不轮转:高优先级任务就绪时会引起任务切换,高优先级任务不再运行时也会

  • 引起任务切换。可以看到任务3就绪后可以马上执行,它运行完毕后导致任务切换。其他时间没有任务切换, 可以看到任务1、任务2都运行了很长时间。


5e523069f76844b1b7bb4683a618dba5.png


f237bca59e11491a8498049196fff981.png

空闲任务是否让步对比

在 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
  • 让步时:在空闲任务的每个循环中,会主动让出处理器,从图中可以看到flagIdelTaskrun的波形很小

  • 不让步时:空闲任务跟任务1、任务2同等待遇,它们的波形宽度是差不多的
  • 32322c1f561c46899d88484ddd444671.png
  • 1d988a740f914871990302fe60968fdc.png

十、同步互斥与通信

关于RTOS的其他内容后续更新,已是凌晨1点,先睡会

相关文章
|
3月前
|
存储 算法 程序员
神秘代码世界惊现高效秘籍!究竟是什么让汇编语言编程如此强大?快来一探究竟!
【8月更文挑战第31天】《代码之美:探索高效汇编语言编程的最佳实践》介绍了汇编语言在系统内核、嵌入式系统及高性能应用中的不可替代作用。书中强调了深入理解处理器架构、提升代码可读性、优化算法与数据结构及有效利用寄存器等最佳实践的重要性。通过具体示例,如在 x86 架构下实现高效的加法函数,展示了如何运用这些技巧编写出既高效又可靠的汇编代码,充分展现了汇编语言的独特魅力及其在现代软件开发中的价值。
45 0
|
4月前
|
人工智能 搜索推荐 API
一键解锁:快速上手文心一言指令编程实践
【7月更文第18天】随着人工智能技术的飞速发展,对话式AI已经成为连接人与信息的新桥梁。百度的“文心一言”(ERNIE)作为国内领先的预训练语言模型,以其强大的语义理解和生成能力,正逐步改变我们获取信息和交互的方式。本文旨在为开发者提供一份快速上手指南,通过实际代码示例,深入浅出地介绍如何利用文心一言API进行指令编程,解锁AI对话新体验。
193 7
|
5月前
技术笔记:tcolorbox宏包简明教程
技术笔记:tcolorbox宏包简明教程
186 0
|
5月前
|
传感器 监控 机器人
技术经验解读:【雕爷学编程】Arduino动手做(83)
技术经验解读:【雕爷学编程】Arduino动手做(83)
30 0
|
6月前
|
存储 Linux 开发者
Linux驱动编程必备基础知识分享
Linux驱动编程必备基础知识分享
59 0
|
前端开发 Linux 定位技术
嵌入式Linux系列第21篇:应用程序之开篇闲聊
嵌入式Linux系列第21篇:应用程序之开篇闲聊
|
存储 算法 安全
[计算机操作系统(第四版 汤小丹 汤子瀛)]第一章 操作系统引论(学习复习笔记)
[计算机操作系统(第四版 汤小丹 汤子瀛)]第一章 操作系统引论(学习复习笔记)
游戏开发零基础入门教程(4):所有能够看到的听到的都是资源
在游戏中所有你能够看到的图片,听到的声音或者用到的脚本都被统称为“资源”。下面我们学习一下如何插入精灵以及声音资源。 点击资源管理器中的“添加素材”。这是游戏中所有素材的添加入口,游戏中的所有的素材都是通过“添加素材”按钮导入到游戏中的。
103 0
|
Linux
Linux驱动开发入门
Linux驱动开发入门
167 0
|
计算机视觉
LabVIEW感兴趣区域分析(实战篇—1)
LabVIEW感兴趣区域分析(实战篇—1)
LabVIEW感兴趣区域分析(实战篇—1)
下一篇
无影云桌面