目录
[为什么要使用 RTOS](#为什么要使用 RTOS)
RTOS(实时操作系统)
裸机开发模式
轮询方式
对于简单的应用程序,轮询(无限循环)的实现比较简单,在硬件完成初始化后,顺序的完成各种 任务。在外设的基础实验中,常采用这种方式。
int` `main()`
`{`
`while(1)`
`{`
` DHT11数据采集;`
` 读取WIFI数据;`
` 判断数据;`
`}`
`}
在实际的嵌入式系统中,存在周期性与触发型任务,每个任务的执行时间与实时响应要求不同,在采用轮询系统进行程序设计时,很难应对这些场景。
前后台(中断方式)
前后台系统是在轮询的基础上加入了中断。外部事件的记录在中断中操作,对事件的响应在轮询中 完成,中断处理过程称之为前台,main 函数中的轮询称为后台。
后台的程序顺序执行,如果产生中断,那么中断会打断后台程序的正常执行,转而去执行中断服务 程序。如果事件的处理过程比较简单,可以直接在中断服务程序中进行处理;如果事件的处理过程比较 复杂,可以在中断中对事件响应进行标记,进而返回后台程序进行处理。
int` `main()`
`{`
`while(1)`
`{`
`if(DHT11==1)`
`{`
` DHT11数据处理;`
` DHT11=0;`
`}`
`if(wifi==1)`
`{`
` 处理WIFI数据;`
` wifi=0;`
`}`
`}`
`}`
`void` `DHT11_irq()`
`{`
` DHT11=1;`
`}`
`void` `WIFI_irq()`
`{`
` wifi=1;`
`}`
`//中断处理的速度高了`
`//触发之后又成为了轮询操作`
`//假如DHT11优先级高,且处理时间过长 wifi的处理就不及时了
采用前后台系统进行程序时,对后台的任务需要进行设计,避免单个任务长时间占有处理器资源。 当任务的逻辑比较复杂,任务的拆分难度增加,同时,随着中断事件的增加,整个程序的设计与响应的 实时性将会降低。
改进(前后台(中断))定时器
设置3S定时器中断(DHT11采集)
设置5s定时器中断(Wifi数据处理)
裸机进一步优化
void` `wifi(void)`
`{`
`static` `int key=0;`
`switch(key)`
`{`
`case` `0:`
` 发送AT指令();`
` key=1;`
`return;`
`case` `1:`
` 接收数据();`
` key=2;`
`return;`
`case` `2:`
` 判断数据();`
` key=0;`
`return;`
`}`
`}`
`void` `DHT11(void)`
`{`
`static` `int DHT11=0;`
`switch(DHT11)`
`{`
`case` `0:`
` 握手();`
` DHT11=1;`
`return;`
`case` `1:`
` 接收数据();`
` DHT11=2;`
`return;`
`case` `2:`
` 判断数据完整性();`
` DHT11=3;`
`return;`
`case` `3:`
` 数据处理();`
` DHT11=0;`
`return;`
`}`
`}`
`//问题解决了,但是程序的复杂度上来了`
`//基于裸机架构无法完美解决复杂耗时的多个函数
裸机的其他问题
while(1)`
`{`
` DHT11;`
`if(key)`
`{`
`delay(100);//程序在这停止了,效率被影响了`
`if(key)`
`{`
`wifi();`
`}`
`}`
`}`
`
按键消抖优化:
在按键中断服务程序开启定时器
在定时器中断服务程序执行按键操作,并关掉定时器
RTOS的概念
什么是RTOS
为什么要使用 RTOS
主要是为了满足系统在时间和资源管理上的特殊需求
使用实时内核的理由可以从多个方面来描述,包括可维护性、可扩展性、模块化、团队开发、测试、代码复用、效率、空闲时间、电源管理、中断处理以及混合处理需求。以下是对这些方面的详细解释:
- 可维护性:实时内核通常具有抽象化的时间细节,这减少了模块间的依赖关系,使软件更易于维护和演化。由于内核负责计时,应用程序的执行受底层硬件变化的影响较小,进一步提高了可维护性。
- 模块化:实时内核的每个任务通常被设计为独立的模块,具有明确的设计目的。这种模块化设计有助于团队开发,因为每个团队成员可以独立地负责一个模块的开发和维护,提高开发效率和系统稳定性。
- 可扩展性:实时内核的设计通常允许添加新的功能或模块,而不需要对现有系统进行大规模修改。这使得系统能够随着需求的变化而扩展,保持长久的生命力。
- 代码复用:实时内核的模块化设计使得代码更容易被复用。通过低耦合设计,代码可以在不同的模块之间共享和重用,提高了开发效率并降低了维护成本。
- 提升效率:实时内核通常设计为事件驱动,这意味着它仅在事件发生时才进行处理。这种设计有助于提高系统的响应速度和运行效率。
RTOS的应用场景
物联网(IoT):RTOS是专门为物联网设备设计的操作系统,它提供了实时性、高效性和可靠性,以满足物联网应用的特殊需求。RTOS通过任务调度算法管理任务的执行顺序,确保高优先级任务能够及时响应,满足物联网设备对实时性的要求。
智能家居:RTOS在智能家居系统中用于控制各种智能设备,如智能灯泡、智能插座、智能门锁等。RTOS提供了设备之间的实时通信和协同工作,使用户能够方便地通过智能手机或其他设备控制家居环境。
医疗设备:RTOS在医疗设备中发挥着重要作用,如心脏起搏器、监护仪等设备需要实时处理生理参数,确保患者的生命安全。RTOS的高可靠性和实时性使得这些设备能够在关键时刻发挥关键作用。SAFERTOS等RTOS产品特别针对医疗设备的需求,提供响应迅速、稳健、确定性的嵌入式实时操作系统,降低项目风险、开发成本,并缩短上市时间。
汽车电子:在汽车领域,RTOS的应用越来越广泛。例如,高级驾驶辅助系统(ADAS)需要实时处理大量的传感器数据,以实现自动驾驶、车辆导航等功能。RTOS能够确保这些系统的实时性和稳定性,提高驾驶安全性。
工业自动化和机器人技术:RTOS在工业自动化系统中扮演关键角色,通过精确定时和控制能力,确保生产线的稳定运行,提高生产效率和质量。在机器人技术中,RTOS能够确保机器人实时响应指令,执行复杂任务,如无人机和机器人的飞行轨迹控制、任务执行等。
航空航天:在航空航天领域,RTOS的应用至关重要。由于航空电子系统的复杂性和对实时性的极高要求,RTOS能够提供高度可靠的中断处理和任务调度机制,确保飞行控制系统、导航系统和传感器数据处理的实时性和准确性。
RTOS的选择
安全性:RTOS 是否有助于设备的安全性或损害设备的安全性?容易出现用户错误吗?
性能:RTOS 能否促进应用程序代码的开发?代码是否在所需参数内执行?
可靠性:RTOS 是否会影响设备的可靠性?
功能:RTOS 是否具备完成这项工作所需的设施?
学习RTOS的3个步骤
学会使用API
了解API实现原理
可以优化改进API
RTOS的工作原理
① RTOS相当于实现了后台的主循环,并能够处理ISR与主循环的交互
② 使得用户可以只考虑任务的设计
③ RTOS还提供了各种组件用于实现任务间交互及其他控制管理功能(e.g. 存储管理)
提供多个执行流,虽然实际只有一颗CPU,但通过"虚拟化",每个Task好像独占CPU
提供资源管理和通信组件
提供一些组件用于简化任务对资源的访问,事件的处理,以及任务之间的通信,有效降低任务之间的代码耦合
FreeRTOS特点
创建第一个FreeRTOS程序
搭建方法
- 移植文件(FreeRTOS相关文件)+时钟配置
- 官网源码下的DEMO(需要精简 去掉无关文件)+标准库
- CUBEMX直接生成就可以了
1、官网源码下载
(1)进入FreeRTOS官网
(2)点击下载FreeRTOS
2、处理工程目录
(1)下载后解压FreeRTOS文件
(2)删除多余文件(红框里的)
(3)删除"FreeRTOSv202212.01\FreeRTOS\Demo"目录下用不到的示例工程,留下common这里放了一些公共文件
(4)"FreeRTOSv202212.01\FreeRTOS\Source\portable"目录下只保留如下两个文件夹,其他全部删掉。
(5)"FreeRTOSv202212.01\FreeRTOS\Source\portable\RVDS"目录下只保留如下一个文件夹,其他全部删掉
3、打开编译工程
(1)删除后文件后,进入如下图打开工程
(2)弹出如下对话框,说明该工程是用KeilMDK4创建的。点击"Migrate to Device Pack"更新为KeilMDK5。
(3)弹出对话框,点击"确定"。
- 更新后,关闭工程再重新打开,编译。
- 工程目录介绍(System里还有一个lcd也删掉)
4、去掉无关代码
(1)Demo Files文件下只保留"serial.c和main.c"文件,其他都删掉(删完之后main里去掉一些头文件)。
(2)编译
5、删除未定义报错内容
(1)在文件STM32F10x.s中,删除如下内容。
(2)删除其他未定义的相关内容,再次编译。报错的内容均删除或者注释,直到没错为止。
6、验证
在原有任务的基础上加个i++验证
串口打印实验
1、重定向
这个fputc在main中:
int fputc( int ch, FILE *f )//重定向 修改数据传输方向`
`{`
` while(!(USART1->SR & (1<<7))){}`
` USART1->DR =ch;`
` return ch;`
`}
2、配置串口
初始化删除多余的东西,自己写一个串口一的初始化。
` `void` `Uart_Init(void)`
`{`
` GPIO_InitTypeDef GPIO_InitStructure;`
` USART_InitTypeDef USART_InitStructure;`
`RCC_APB2PeriphClockCmd( RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE );`
`//tx`
` GPIO_InitStructure.GPIO_Pin=GPIO_Pin_9;`
` GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;`
` GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;`
`GPIO_Init(GPIOA,&GPIO_InitStructure);`
`//rx`
` GPIO_InitStructure.GPIO_Pin=GPIO_Pin_10;`
` GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN_FLOATING;`
`GPIO_Init(GPIOA,&GPIO_InitStructure);`
` USART_InitStructure.USART_BaudRate =` `115200;`
` USART_InitStructure.USART_WordLength = USART_WordLength_8b;`
` USART_InitStructure.USART_StopBits = USART_StopBits_1;`
` USART_InitStructure.USART_Parity = USART_Parity_No ;`
` USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;`
` USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;`
` USART_InitStructure.USART_Clock = USART_Clock_Disable;`
` USART_InitStructure.USART_CPOL = USART_CPOL_Low;`
` USART_InitStructure.USART_CPHA = USART_CPHA_2Edge;`
` USART_InitStructure.USART_LastBit = USART_LastBit_Disable;`
`USART_Init( USART1,` `&USART_InitStructure );`
`USART_Cmd( USART1, ENABLE );`
`}`
`
如何找串口仿真
命名规范
数据类型
命名规范
动态任务的创建
任务是什么
任务是由 C 语言函数实现的。唯一特别的地方是其函数原型,必须返回 void,并带有一个 void 指针参数。
任务的外观:一个永远不返回的函数
任务实验
实现
创建任务函数xTaskCreate:任务也不是很复杂的东西,任务也就是一个函数
xTaskCreate。简单得说,创建一个任务,你得提供它的执行函数,你得提供它的栈的大小,函数的执行空间,函数的优先级等重要的条件。因为任务在运行中,任务函数有调用关系,有局部变量,这些都保存在任务的栈里面;任务有可能被切换,有可能被暂停,这时候CPU寄存器中断现场数据都保存在栈里面。
函数原型
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,`
`const` `char` `*` `const pcName,` `const configSTACK_DEPTH_TYPE usStackDepth,`
`void` `*` `const pvParameters,`
` UBaseType_t uxPriority,`
` TaskHandle_t *` `const pxCreatedTask )`
`
参数说明
pvTaskCode:指向任务函数的指针。该函数表示任务要执行的代码。
pcName:任务名称字符串。用于调试和跟踪,不影响任务功能。也可用于获取任务句柄
usStackDepth:任务栈大小(以单词为单位)。根据任务需求设定,过小可能导致栈溢出。
pvParameters:传递给任务函数的参数。可以是任意类型的指针。
uxPriority:任务优先级。数值越大,优先级越高。
pxCreatedTask:任务句柄指针。用于存储创建任务后的任务句柄,可选参数。可为NULL
返回值
如果任务创建成功,返回pdPASS。如果任务创建失败(例如内存不足),返回错误码。
任务创建成功后,系统会自动将其加入到调度队列。调度器会根据任务优先级选择合适的任务执行。
实验
优先级
结论
FreeRTOS中,优先级数越大优先级越高,两个任务同优先级时通过时间片轮转执行,如果有高优先级时,高优先级执行,一直到高优先级停止执行低优先级才能执行。
延时函数
void` `vTaskDelay(` `const TickType_t xTicksToDelay );
查看延时时间验证
声明个变量a只有任务一执行时才为1,其他时候为0,然后进入仿真使用虚拟逻辑分析仪查看变量a状态
查看方法(进入仿真使用):
调度原理
调度器是内核中负责决定在任何特定时间应执行哪些任务的部分。内核可以在任务生命周期内多次挂起并且稍后恢复一个任务。调度策略是调度器用来决定在任何时间点执行哪个任务的算法。非实时多用户系统的策略极有可能使每个任务具有"公平"比例的处理时间。
FreeRTOS 共支持三种任务调度方式,分别为抢占式调度、时间片调度和协程式调度。需要注意的是,FreeRTOS 官方对协程式调度做了特殊说明,协程式调度用于一些资源非常少的设备上,但是现在已经很少用到。虽然协程式调度的相关代码还没有被删除,FreeRTOS 官方并未计划继续开发协程式调度。因此不推荐在开发中继续使用协程式调度,我们接着理解一下抢占式调度与时间片调度的概念。
抢占式调度
抢占式调度主要时针对优先级不同的任务,每个任务都有一个优先级,优先级高的任务可以抢占优先级低的任务,只有当优先级高的任务发生阻塞或者被挂起,低优先级的任务才可以运行。
时间片调度
时间片调度主要针对优先级相同的任务,当多个任务的优先级相同时, 任务调度器会在每一次系统时钟节拍到的时候切换任务,也就是说 CPU 轮流运行优先级相同的任务,每个任务运行的时间就是一个系统时钟节拍。
在 FreeRTOS 中的任务存在四种状态,分别为运行态、就绪态、阻塞态和挂起态。在 FreeRTOS 运行时,任务的状态一定是这四种状态中的一种,下面是四种任务状态的介绍。
任务状态
同优先级的任务正在运行,所以需要等待。
运行态(Running):如果一个任务得到 CPU 的使用权,即任务被实际执行时,那么这个任务处于运行态。如果运行 FreeRTOS 的 MCU 只有一个处理器核心,那么在任务时刻,都只能有一个任务处理运行态。
就绪态:如果一个任务已经能够被执行(不处于阻塞态或挂起态),但当前还未被执行(具有相同优先级或更高优先级的任务正持有 CPU 使用权),那么这个任务就处于就绪态。
阻塞态(Blocked):如果一个任务因延时一段时间或等待外部事件发生,那么这个任务就处理阻塞态。例如任务调用了函数 vTaskDelay(),进行一段时间的延时,那么在延时超时之前,这个任务就处理阻塞态。任务也可以处于阻塞态以等待队列、信号量、事件组、通知或信号量等外部事件。通常情况下,处于阻塞态的任务都 有一个阻塞的超时时间,在任务阻塞达到或超过这个超时时间后,即使任务等待的外部事件还没有发生, 任务的阻塞态也会被解除。要注意的是,处于阻塞态的任务是无法被运行的。
挂起态(Suspended):任务一般通过函数 vTaskSuspend()和函数 vTaskResums()进入和退出挂起态与阻塞态一样,处于挂起态的任务也无法被运行。
挂起任务、恢复任务
挂起任务函数原型
vTaskSuspend( TaskHandle_t xTaskToSuspend )
参数
xTaskToSuspend:需要挂起任务的句柄
创建任务时加上句柄
句柄声明
恢复任务函数原型
void vTaskResume( TaskHandle_t xTaskToResume )`
`
参数
TaskToResumex:需要恢复任务的句柄
任务删除
vTaskDelete()函数用于删除任务。在使用这个函数时,需要提供一个任务句柄作为参数,以便通知内核删除哪个任务。
函数原型
void` `vTaskDelete( TaskHandle_t xTaskToDelete )
参数:
xTaskToDelete:需要删除任务的句柄
如果写的是NULL,则自杀
空闲任务
当创建的任务大部分时间都处于阻塞状态。当任务处于阻塞状态时,它们无法运行,因此调度器无法选择它们。必须始终至少有一个任务可以进入运行状态(即使在使用 FreeRTOS 的特殊低功耗功能时也是如此,在这种情况下,如果应用程序创建的任务都无法执行,那么执行 FreeRTOS 的微控制器将被置于低功耗模式)。为了确保这种情况,当调用 vTaskStartScheduler()时,调度器会自动创建一个空闲任务。空闲任务除了在一个循环中等待之外几乎不执行其他任何操作,因此,就像第一个示例中的任务一样,它始终能够运行。
空闲任务具有尽可能低的优先级(优先级为 0),以确保它永远不会阻止更高优先级的应用程序任务进入运行状态。然而,这并不妨碍应用程序设计者根据需要创建与空闲任务共享优先级的任务。可以使用 FreeRTOSConfig.h 中的 configIDLE_SHOULD_YIELD 编译时配置常量阻止空闲任务消耗处理时间,可以更有效地分配给具有同样优先级的应用程序任务处理时间。运行在最低优先级确保了当更高优先级的任务进入就绪状态时,空闲任务会立即从运行状态转换出来。
注意:如果一个任务使用 vTaskDelete() API 函数来删除自己,那么必须确保空闲任务不会因处理时间不足而受到影响。这是因为空闲任务负责清理由已删除自身的任务所使用的内核资源。
空闲任务钩子函数用途
可以通过使用空闲钩子(或空闲回调)函数直接在空闲任务中添加应用程序的相关功能。处理一些不紧急的任务。空闲钩子函数是一种函数,它在空闲任务循环的每次迭代中自动由空闲任务调用。空闲任务钩子的常见用途包括:
- 执行低优先级、后台或连续处理功能,而无需为此目的创建应用程序任务,所带来额外的 RAM开销。
- 测量空闲处理能力(空闲任务仅在所有更高优先级的应用程序任务没有工作要执行时,空闲任务才会运行;因此,测量分配给空闲任务的处理时间,可以清楚地表明有多少处理时间是空闲的)。
- 将处理器置于低功耗模式,提供了一种简单且自动的方法,在没有应用程序处理要执行时,节省功耗。
空闲任务钩子函数的实现限制
- 空闲任务钩子函数绝对不能阻塞或挂起自己。(以任何方式阻塞空闲任务都可能导致没有任务能够进入运行态!)
- 如果一个应用程序任务使用 vTaskDelete() API 函数删除自己,那么必须在合理的时间段内将闲任务钩子返回给调用者。这是因为任务被删除后,空闲任务负责清理内核资源。如果空闲任务永久保持在空闲钩子函数中,则无法进行这种清理。
钩子函数的使用
- main函数中找到vTaskStartScheduler()并跳转
- vTaskStartScheduler()内可以找到如图的函数
- 跳转空闲任务函数找到了vApplicationIdleHook()函数就是钩子函数,但是需要configUSE_IDLE_HOOK==1
- 右键跳转configUSE_IDLE_HOOK并将configUSE_IDLE_HOOK等于1
- 编译发现报错,内容为vApplicationIdleHook未定义
- 接下来我们声明写一个vApplicationIdleHook函数并在里面写自己的任务程序就可以了
TaskHandle_t xTask1ToSuspend;`
`void vApplicationIdleHook( void )`
`{`
`while(1)`
`{`
`printf("我是钩子,我的优先级为0\n");`
`}`
`}`
`void task1( void *pvParameters )`
`{`
`for(;;)`
`{`
`printf("我是任务1,我的优先级高\n");`
`vTaskDelay(10);`
`}`
`}`
`void task2( void *pvParameters )`
`{`
`for(;;)`
`{`
`printf("我是任务2,我的优先级低\n");`
`vTaskDelete(xTask1ToSuspend);`
`}`
`}`
`
使用CUBE配置FreeRTOS编写程序
1、更新安装freertos插件
2.配置FreeRTOS
创建一个新的工程
3.配置基础硬件
4.最后生成工程MDK内编写程序
void` `StartTask02(void` `*argument)`
`{`
`/* USER CODE BEGIN myTask02 */`
`/* Infinite loop */`
`for(;;)`
`{`
`HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);`
`osDelay(100);`
`}`
`/* USER CODE END myTask02 */`
`}