本文基础内容参考的是正点原子的FREERTOS课程。
这是基于HAL库的
正点原子手把手教你学FreeRTOS实时系统
这是基于标准库的
正点原子FreeRTOS手把手教学-基于STM32
基础知识,直接参考正点原子《FreeRTOS开发指南V1.1》基于标准库的,此处不再赘述。
本文主要对不理解的地方进行查缺补漏,并且先用起来,涉及到原理的部分,可以观看上述教学视频或者开发指南。
裸机和RTOS对比
我们知道,裸机系统的实时性比较差,各任务是顺序执行的,如果有紧急任务,就需要放在中断里,可是中断里又不能进行耗时较长的操作,通常是在中断里面置标志位,然后由主循环判断标志位来执行相关动作,可是这样一来,任务的执行又变成了顺序执行,实时性还是比较差。比如:
上面示例中,肚子疼这一任务属于紧急任务,中断里置标志位,然后主循环里判断标志位从而去医院,但是如果此时打游戏时间很长,就一直无法判断标志位,从而没法及时去医院。
因此,裸机的缺陷是很明显的。
如果是RTOS呢,是怎么处理的呢?
RTOS会创建三个任务,其中可以设置去医院的优先级更高,当肚子疼时,就可以立马执行去医院的操作,实时性就比较好了。
有个有意思的地方,就是延时函数,在裸机中,如果有延时函数,那就是CPU死等,但是在RTOS中,如果一个任务中有延时,那么CPU会看有没有其他任务,从而转去执行其他任务,而不会阻塞在延时这里。比如上面的去医院的例子,去医院的路上就类似于一个延时,这期间,我们照样可以玩游戏和发信息,但是裸机没法做到,去医院路上就只能等着去医院,其他什么事情也干不了。
裸机的特点
RTOS的特点
注意:
✔
这里的高中低优先级,都属于软件层面的优先级设定,中断是硬件层面的优先级,永远都是优先于软件优先级来执行的;
✔
每个任务都有自己的栈空间,这就可以用来保存调度之前的数据信息,是实现返回原来任务时继续执行的关键所在;
✔
如果高优先级任务一直运行,低优先级任务就得不到运行,除非高优先级任务进入阻塞态,这是合理的,因为优先级高,所以肯定很紧急,所以必定占用所有的时间片,比如去医院做手术,肯定是全心全意做手术,不可能做手术时还玩游戏回信息,这里的关键是,实际开发时需要合理安排各任务的优先级。
FREERTOS简介
官网首页:
FreeRTOS - Market leading RTOS (Real Time Operating System) for embedded systems with Internet of Things extensions
2024 年官网现在已经更新中文了。
官网参考手册:
官网参考手册:FreeRTOS 内核快速入门指南
注意事项:
官网进入比较慢,API估计都进不去。
这时如果需要查看API,可以查看正点原子给的几个文档
在这几个文档里搜索一下应该就能找到,可以优先看中文文档,没有的话再看英文文档。
freertos特点
由此可见,freertos的代码量并不高,主要是实现的任务调度。
源码简介
首先获取源码,我们可以从官网下载最新版本的源码
我们也可以直接拿正点原子的源码来使用
实际中我们可以直接拿对应MCU移植例程中的源码来用。
下载完成,我们再来看看里面有哪些内容
我们重点关注这里面的FreeRTOS文件夹,也就是我们要移植的源码。如果想了解其他文件夹中的内容,可以直接参考正点的开发手册。
打开FreeRTOS内核文件夹
Demo 文件夹里面就是 FreeRTOS 的相关例程,打开以后如下图所示:
可以看出 FreeRTOS 针对不同的 MCU 提供了非常多的 Demo,其中就有 ST 的 F1、F4 和F7 的相关例程。
打开Source文件夹
这里面包含了内核的头文件、移植文件、核心文件,前面说的FreeRTOS的9000多行代码,重点集中在list.c、queue.c、tasks.c这三个文件中。
再来重点来看一下 portable 这个文件夹,我们知道 FreeRTOS 是个系统,归根结底就是个纯软件的东西,它是怎么和硬件 联系在一起的呢?软件到硬件中间必须有一个桥梁,portable 文件夹里面的东西就是 FreeRTOS 系统和具体的硬件之间的连接桥梁!不同的编译环境,不同的 MCU,其桥梁应该是不同的,打 开 portable 文件夹,如下图所示:
从上图中可以看出 FreeRTOS 针对不同的编译环境和 MCU 都有不同的“桥梁”,我们这里就以 MDK 编译环境下的 STM32F103 为例。
MemMang 这个文件夹是跟内存管理相关的, 我们移植的时候是必须的,具体内容我们后面会专门有一章来讲解。里面的内容如下:
这里面有FreeRTOS提供的几种内存管理算法,我们一般使用heap_4.c这个文件。
Keil 文件夹里面的东西肯定也是必须的,但是我们打开Keil文件夹以后里面只有一个文件:See-also-the-RVDS-directory.txt。 这个 txt 文件是什么鬼?别急嘛!看文件名字“See-also-the-RVDS-directory”,意思就是参考 RVDS 文件夹里面的东西!哎,好吧,再打开 RVDS 文件夹,如下图所示:
RVDS 文件夹针对不同的架构的 MCU 做了详细的分类,STM32F103 就参考 ARM_CM3, 打开 ARM_CM3 文件夹,如下图所示:
ARM_CM3 有两个文件,这两个文件就是我们移植的时候所需要的!
开始移植
可以参考正点的移植过程,开发手册第二章 FreeRTOS 移植
我们这里移植过程略有不同。
1
获取源码
我们直接拿正点的移植例程中的源码来使用。
2
向工程分组中添加文件
打开基础工程,新建分组 FreeRTOS_CORE 和 FreeRTOS_PORTABLE,然后向这两个分组中添加文件,如下图所示:
我们把所有的c文件都加入进来,不用的就放着,没关系。
其中,port.c 是 RVDS 文件 夹下的 ARM_CM3 中的文件,因为 STM32F103 是 Cortex-M3 内核的,因此要选择 ARM_CM3 中的 port.c 文件。
heap_4.c 是 MemMang 文件夹中的,前面说了 MemMang 是跟内存管理相关 的,里面有 5 个 c 文件:heap_1.c、heap_2.c、heap_3.c、heap_4.c 和 heap_5.c。这 5 个 c 文件是 五种不同的内存管理方法,就像从北京到上海你可以坐火车、坐飞机,如果心情好的话也可以 走路,反正有很多种方法,只要能到上海就行。这里也一样的,这 5 个文件都可以用来作为 FreeRTOS 的内存管理文件,只是它们的实现原理不同,各有利弊。
这里我们选择 heap_4.c,至 于原因,后面会有一章节专门来讲解 FreeRTOS 的内存管理,到时候大家就知道原因了。这里 就先选择 heap_4.c。
3
添加相应的头文件路径
添加完 FreeRTOS 源码中的 C 文件以后还要添加 FreeRTOS 源码的头文件路径,头文件路 径如下图所示:
4
确认包含FreeRTOSConfig.h文件
我们这里使用的是正点例程中的源码,所以include目录里面已经包含了系统配置文件FreeRTOSConfig.h,此时编译后一般不会有缺文件的问题。
如果没有FreeRTOSConfig.h文件或者FreeRTOSConfig.h文件不适配MCU,我们需要自己去找。比如打开 FreeRTOS目录里的Demo文件夹里的CORTEX_STM32F103_Keil,打开以后如下图所示:
果然!官方的移植工程中有这个文件,我们可以使用这个文件,但是建议大家使用正点例程中的 FreeRTOSConf.h 文件,这个文件是 FreeRTOS 的系统配置文件,不同的平台其配置不同。看名字就知道,他是 FreeRTOS 的配置文件,一般的操作系统都有裁剪、配置功能,而这些裁剪及配置都是通过一个文件来完成的,基本都是通过宏定义来完成对系统的配置和裁剪的,关于FreeRTOS的配置文件FreeRTOSConfig.h后面也会有一 章节来详细讲解。
5
引入FreeRTOSConfig.h配置文件后,编译,会提示以下内容:
这是因为freertos中也定义了SVC 中断服务函数SVC_Handler()和 PendSV 中断服务函数PendSV_Handler(),而我们STM32的中断文件stm32f10x_it.c中也定义了。
所以,需要将STM32定义的这两个空中断处理函数给屏蔽掉。
之后再编译就没有错误了。
5
配置systick时钟
操作系统一般会使用systick时钟来作为时基,也就是freertos的工作心跳,所以需要配置。
这里说明下,正点原子教程里说要改system文件夹里的内容,他们是想让systick既能作为延时,又能用于操作系统时基,但是本来很简单的东西,这样一来就会让操作变得很复杂,完全没必要,如果使用操作系统,我们可以不用systick来延时,而是配置额外的定时器。让systick专心为rtos服务即可。
所以我们根据实际情况,配置systick为1ms的定时中断,此配置可参考这篇文章:
STM32实现延时(Systick+Tim)-CSDN博客
然后在stm32f10x_it.c文件里的systick中断服务函数里调用操作系统的时基心跳。
extern void xPortSysTickHandler(void); //systick 中断服务函数,使用 OS 时用到 void SysTick_Handler(void) { if(xTaskGetSchedulerState()!=taskSCHEDULER_NOT_STARTED)//系统已经运行 { xPortSysTickHandler(); } }
FreeRTOS 的心跳就是由滴答定时器产生的,根据 FreeRTOS 的系统时钟节拍设置好滴答定 时器的周期,这样就会周期触发滴答定时器中断了。在滴答定时器中断服务函数中调用 FreeRTOS 的 API 函数 xPortSysTickHandler()。
至此,移植完成,我们可以编写任务来验证是否移植成功,这一块后续再补充。
系统配置文件
FreeRTOSConf.h文件是 FreeRTOS 的系统配置文件,不同的平台其配置不同。看名字就知道,它是 FreeRTOS 的配置文件,一般的操作系统都有裁剪、配置功能,而这些裁剪及配置都是通过一个文件来完成的,基本都是通过宏定义来完成对系统的配置和裁剪的,这个文件可以在FreeRTOS目录里的Demo文件夹里的CORTEX_STM32F103_Keil路径下找到
我们将该文件拿来放到我们的移植目录中。
本文,重点介绍该文件中有哪些内容。
参考正点原子《FreeRTOS开发指南V1.1》“第三章 FreeRTOS 系统配置 ”
正点原子例程中将该文件放在了User目录下
把文件拿过来看看
/***************************************************************************************************************/ /* FreeRTOS基础配置配置选项 */ /***************************************************************************************************************/ //1使用抢占式内核,0使用协程 #define configUSE_PREEMPTION 1 //1使能时间片调度(默认式使能的) #define configUSE_TIME_SLICING 1 //1启用特殊方法来选择下一个要运行的任务 //一般是硬件计算前导零指令,如果所使用的 //MCU没有这些硬件指令的话此宏应该设置为0! #define configUSE_PORT_OPTIMISED_TASK_SELECTION 1 //1启用低功耗tickless模式 #define configUSE_TICKLESS_IDLE 0 //为1时启用队列 #define configUSE_QUEUE_SETS 1 //CPU频率 #define configCPU_CLOCK_HZ (SystemCoreClock) //时钟节拍频率,这里设置为1000,周期就是1ms #define configTICK_RATE_HZ (1000) //可使用的最大优先级 #define configMAX_PRIORITIES (32) //空闲任务使用的堆栈大小 #define configMINIMAL_STACK_SIZE ((unsigned short)130) //任务名字字符串长度 #define configMAX_TASK_NAME_LEN (16) //系统节拍计数器变量数据类型, //1表示为16位无符号整形,0表示为32位无符号整形 #define configUSE_16_BIT_TICKS 0 //为1时空闲任务放弃CPU使用权给其他同优先级的用户任务 #define configIDLE_SHOULD_YIELD 1 //为1时开启任务通知功能,默认开启 #define configUSE_TASK_NOTIFICATIONS 1 //为1时使用互斥信号量 #define configUSE_MUTEXES 1 //不为0时表示启用队列记录,具体的值是可以 //记录的队列和信号量最大数目。 #define configQUEUE_REGISTRY_SIZE 8 //大于0时启用堆栈溢出检测功能,如果使用此功能 //用户必须提供一个栈溢出钩子函数,如果使用的话 //此值可以为1或者2,因为有两种栈溢出检测方法。 #define configCHECK_FOR_STACK_OVERFLOW 0 //为1时使用递归互斥信号量 #define configUSE_RECURSIVE_MUTEXES 1 //1使用内存申请失败钩子函数 #define configUSE_MALLOC_FAILED_HOOK 0 #define configUSE_APPLICATION_TASK_TAG 0 //为1时使用计数信号量 #define configUSE_COUNTING_SEMAPHORES 1 /***************************************************************************************************************/ /* FreeRTOS与内存申请有关配置选项 */ /***************************************************************************************************************/ //支持动态内存申请 #define configSUPPORT_DYNAMIC_ALLOCATION 1 //系统所有总的堆大小 #define configTOTAL_HEAP_SIZE ((size_t)(20*1024)) /***************************************************************************************************************/ /* FreeRTOS与钩子函数有关的配置选项 */ /***************************************************************************************************************/ //1,使用空闲钩子;0,不使用 #define configUSE_IDLE_HOOK 0 //1,使用时间片钩子;0,不使用 #define configUSE_TICK_HOOK 0 /***************************************************************************************************************/ /* FreeRTOS与运行时间和任务状态收集有关的配置选项 */ /***************************************************************************************************************/ //为1时启用运行时间统计功能 #define configGENERATE_RUN_TIME_STATS 0 //为1启用可视化跟踪调试 #define configUSE_TRACE_FACILITY 1 //与宏configUSE_TRACE_FACILITY同时为1时会编译下面3个函数 #define configUSE_STATS_FORMATTING_FUNCTIONS 1 //prvWriteNameToBuffer(),vTaskList(), //vTaskGetRunTimeStats() /***************************************************************************************************************/ /* FreeRTOS与协程有关的配置选项 */ /***************************************************************************************************************/ //为1时启用协程,启用协程以后必须添加文件croutine.c #define configUSE_CO_ROUTINES 0 //协程的有效优先级数目 #define configMAX_CO_ROUTINE_PRIORITIES ( 2 ) /***************************************************************************************************************/ /* FreeRTOS与软件定时器有关的配置选项 */ /***************************************************************************************************************/ //为1时启用软件定时器 #define configUSE_TIMERS 1 //软件定时器优先级 #define configTIMER_TASK_PRIORITY (configMAX_PRIORITIES-1) //软件定时器队列长度 #define configTIMER_QUEUE_LENGTH 5 //软件定时器任务堆栈大小 #define configTIMER_TASK_STACK_DEPTH (configMINIMAL_STACK_SIZE*2) /***************************************************************************************************************/ /* FreeRTOS可选函数配置选项 */ /***************************************************************************************************************/ #define INCLUDE_xTaskGetSchedulerState 1 #define INCLUDE_vTaskPrioritySet 1 #define INCLUDE_uxTaskPriorityGet 1 #define INCLUDE_vTaskDelete 1 #define INCLUDE_vTaskCleanUpResources 1 #define INCLUDE_vTaskSuspend 1 #define INCLUDE_vTaskDelayUntil 1 #define INCLUDE_vTaskDelay 1 #define INCLUDE_eTaskGetState 1 #define INCLUDE_xTimerPendFunctionCall 1 /***************************************************************************************************************/ /* FreeRTOS与中断有关的配置选项 */ /***************************************************************************************************************/ #ifdef __NVIC_PRIO_BITS #define configPRIO_BITS __NVIC_PRIO_BITS #else #define configPRIO_BITS 4 #endif //中断最低优先级 #define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 15 //系统可管理的最高中断优先级 #define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5 #define configKERNEL_INTERRUPT_PRIORITY ( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) ) #define configMAX_SYSCALL_INTERRUPT_PRIORITY ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) ) /***************************************************************************************************************/ /* FreeRTOS与中断服务函数有关的配置选项 */ /***************************************************************************************************************/ #define xPortPendSVHandler PendSV_Handler #define vPortSVCHandler SVC_Handler /***************************************************************************************************************/ /* FreeRTOS断言等有关的配置选项 */ /***************************************************************************************************************/ /* 断言 */ #define vAssertCalled(char, int) printf("Error: %s, %d\r\n", char, int) #define configASSERT( x ) if( ( x ) == 0 ) vAssertCalled( __FILE__, __LINE__ ) /* FreeRTOS MPU 特殊定义 */ //#define configINCLUDE_APPLICATION_DEFINED_PRIVILEGED_FUNCTIONS 0 //#define configTOTAL_MPU_REGIONS 8 //#define configTEX_S_C_B_FLASH 0x07UL //#define configTEX_S_C_B_SRAM 0x07UL //#define configENFORCE_SYSTEM_CALLS_FROM_KERNEL_ONLY 1 //#define configALLOW_UNPRIVILEGED_CRITICAL_SECTIONS 1 /* ARMv8-M 安全侧端口相关定义。 */ //#define secureconfigMAX_SECURE_CONTEXTS 5 #endif /* FREERTOS_CONFIG_H */
大部分配置都有注释,可以先看看。
官方的在线文档中也有详细的说明:
FreeRTOS - The Free RTOS configuration constants and configuration options - FREE Open Source RTOS for small real time embedded systems
FreeRTOS 的配置基本是通过在 FreeRTOSConfig.h 中使用“#define”这样的语句来定义宏定义实现的。
“config”开始的宏是用来完成 FreeRTOS 的配置和裁剪的。
具体参考正点原子的开发手册。
使用“INCLUDE_”开头的宏用来表示使能或除能 FreeRTOS 中相应的 API 函数,作用就是用来配置 FreeRTOS 中的可选 API 函数的。比如当宏 INCLUDE_vTaskPrioritySet 设置为 0 的
时候表示不能使用函数 vTaskPrioritySet() , 当设置为1的时候就表示可 以使用函数vTaskPrioritySet()。这个功能其实就是条件编译,在文件 tasks.c 中有如下图所示的代码。
从图可以看出当满足条 件 : NCLUDE_vTaskPrioritySet == 1 的 时 候 , 函 数vTaskPrioritySet()才会被编译,注意,这里为了缩小篇幅将函数 vTaskPrioritySet()的内容进行了折叠。FreeRTOS 中的裁剪和配置就是这种用条件编译的方法来实现的,不止FreeRTOS这么干,很多的协议栈、RTOS 系统和 GUI 库等都是使用条件编译的方法来完成配置和裁剪的。条件编译的好处就是节省空间,不需要的功能就不用编译,这样就可以根据实际需求来减少系统占用的 ROM 和 RAM 大小,根据自己所使用的MCU来调整系统消耗。……
更多补充说明
✔
每个任务都有自己的堆栈。设置堆栈大小时,单位是字,也就是四个字节。
✔
任务优先级理论上是无限的,但因为和configUSE_PORT_OPTIMISED_TASK_SELECTION这个宏的功能相关,也就是和硬件相关,所以最大只能设置到32。不过一般也不会用到32个优先级。
✔
关于宏configUSE_16_BIT_TICKS
✔
宏configCHECK_FOR_STACK_OVERFLOW一般只在调试阶段打开。
✔
暂且认为钩子函数就是回调函数吧。
✔
heap4.c中实现了动态内存管理函数。
堆的内存池大小也在heap4.c中有使用。
这里总堆大小的单位是字节,定义的是20K,具体情况具体设置。
✔
这些都是跟调试相关的
补充:三个重要中断
在 FreeRTOS 的移植过程中会这遇到三个重要的中断,分别是 FreeRTOS 系统时基定时器的中断(SysTick 中断)、SVC 中断、PendSV 中断。
SysTick 中断是用来提供系统时基的,很好理解。
那么SVC和PendSV是用来干嘛的呢?
参考:怎样去理解异常SVC和PendSV_pendsv svc-CSDN博客
SVC(系统服务调用)和 PendSV( 可悬挂系统调用 )。
它们多用于在操作系统之上的软件开发中。
SVC用于产生系统函数的调用请求。
例如,操作系统不让用户程序直接访问硬件,而是通过提供一些系统服务函数,用户程序使用 SVC 发出对系统服务函数的呼叫请求,以这种方法调用它们来间接访问硬件。因此,当用户程序想要控制特定的硬件时,它就会产生一个 SVC 异常,然后操作系统提供的 SVC 异常服务例程得到执行,它再调用相关的操作系统函数,后者完成用户程序请求的服务。获取数据有两条路线:
通过SVC。
绕过SVC(但是不允许)。
由此实现软件硬件分离。
另一个相关的异常是 PendSV(可悬挂的系统调用),它和 SVC 协同使用。SVC异常是必须立即得到响应的(若因优先级不比当前正处理的高, 或是其它原因使之无法立即响应, 将上访成硬 fault),应用程序执行 SVC 时都是希望所需的请求立即得到响应。PendSV 则不同,它是可以像普通的中断一样被悬起的(不像 SVC 那样会上访)。 OS 可以利用它“缓期执行” 一个异常——直到其它重要的任务完成后才执行动作。
悬起 PendSV 的方法是:手工往 NVIC 的 PendSV 悬起寄存器中写 1。 悬起后, 如果优先级不够高,则将缓期等待执行。PendSV是可悬起异常,如果我们把它配置最低优先级,那么如果同时有多个异常被触发,它会在其他异常执行完毕后再执行,而且任何异常都可以中断它。
假设任务A、任务B。
任务A,2ms执行结束后延迟500ms再次执行,那么OS在这段500ms内寻找下一个要执行的任务B,任务B,2ms执行结束延迟600ms再次执行(如果还有任务C、E等依次类推)。但是现在任务A并没有延迟到500ms所以此时OS执行空闲任务,到达500ms后才会执行任务A。
当空闲任务切换到任务A或者任务B时,此时突然中断产生,PendSV执行。
一时半会儿还不太理解,后续可能需要看这本书:
《ARM Cortex-M3 与 Cortex-M4 权威指南(第三版)》
以后再补充。