堆内存管理
- 有五种内存分配方式
- 常用的为heap_4方式
任务管理
-
任务不能以任何方式实现函数返回,可以在任务的死循环外加上xTaskDelete( )
-
创建任务:xTaskCreate( )
- 任务堆栈的大小,空闲任务的最小是configMINIMAL_STACK_SIZE,其他任务不能比他小
- 任务优先级,0~configMAX_PRIORITIES-1 ,数字越小,优先级越低 。
- configMAX_PRIORITIES尽量保证必要最小值 ,越大消耗RAM越多
-
滴答中断:一般调度器都是基于时间片的抢占系统
- 滴答中断频率:configTICK_RATE_HZ,要与滴答定时器的频率匹配
- 典型值为100HZ,即10ms的时间片,但是要注意vTaskDelay函数的延时
- pdMS_TO_TICKS,使得100HZ的滴答只能延时10ms的整数倍时间,所以我感觉1000HZ也还可以
-
任务状态
- 阻塞状态:
- 时间性事件:例如延时vTaskDelay
- 同步事件:进入阻塞等待数据到达,例如:队列、二进制和计数量、互斥量、事件组、任务通知等。如果设置10ms,10ms内数据到达,或者超过10ms数据没到达,都会离开阻塞状态
- 暂停状态:进入暂停唯一方法就是vTaskSuspend,退出方法就是vTaskResume
- 就绪状态:准备好运行,但是还没运行
- 阻塞状态:
-
阻塞延时:vTaskDelay和vTaskDelayUntil
-
空闲任务
- 调度器启动时会自动创建空闲任务,当其他任务都阻塞时也有空闲任务时刻运行
- 空闲任务钩子:vApplicationIdleHook
- ***功能:***执行低优先级、后台或连续处理函数;测量空闲处理能力;将处理器置于低功耗模式
- **注意:**钩子函数绝对不能阻塞和暂停;如果使用了vTaskDelete,空闲任务负责清理被删除任务的资源,还要确保空闲任务不会被饿死
-
任务优先级:设置任务优先级:vTaskPrioritySet,获取任务优先级:vTaskPriorityGet
-
**任务删除:**vTaskDelete
-
**线程本地存储:**vTaskSetThreadLocalStoragePointer和pvTaskGetThreadLocalStoragePointer,根据任务数组的索引设置和读取数据
-
调度算法*:***
- 时间片优先的抢占式调度:用的最多,但注意资源不能被多个任务同时访问,否则可能破坏资源
- 一个任务优先级高于运行状态任务会抢占运行状态任务
- 同等优先级任务使用时间片共享处理时间
- 自动会执行最高优先级就绪态的任务
- 不含时间片的抢占式调度
- 调度器转换新任务只有两种可能:高优先级任务就绪抢占,运行状态任务阻塞或者暂停
- 协同调度
- 只有运行态任务阻塞或者使用taskYIELD进行任务让步,才会发生切换
- 时间片优先的抢占式调度:用的最多,但注意资源不能被多个任务同时访问,否则可能破坏资源
队列管理
-
FreeRTOS通过复制实现队列的方式,优点是可以直接发送栈变量到队列,不需要先分配缓冲区存放数据,发送和接收任务完全脱钩,复制实现队列不妨碍引用实现队列。
-
**多任务访问:**队列本身就是对象,可以被任意个任务或者ISR访问
-
队列读取、写入阻塞:
- 当任务从队列读取或者写入数据时,可以选择性阻塞时间,当阻塞状态接收到数据或者阻塞时间超时,任务都会转移到就绪状态。
- 如果有多个任务等待队列数据,只会有一个任务会解除阻塞,最高优先级先解除,相同优先级则等待时间最长的先解除
-
**队列创建:**xQueueCreate
-
**队尾插入:**xQueueSendToBack
-
**队首插入:**xQueueSendToFront
-
**队列接收:**xQueueReceive
-
**数据量查询:**uxQueueMessagesWaiting
-
从多个来源接收数据:
- 简单:可以使用队列传输结构体,结构体包含来源和数据
-
处理大数据和可变大小数据:最好使用队列传输数据的指针
- 被指向的RAM的所有者必须明确定义
- 被指向的RAM保持有效
-
**队列集:**能够从多个来源接收数据,但比结构体方法更繁琐效率更低
- 创建队列集
- 队列添加队列集
- 队列集读取数据
-
**队列集创建:**xQueueCreateSet
-
**队列添加队列集:**xQueueAddToSet
-
**队列集读取队列:**xQueueSelectFromSet
-
**队列集移除队列:**xQueueRemoveFromSet
-
***队列创建邮箱:***邮箱用来指长度为1的队列
- 队列将数据从一个任务发送到另一个任务,发送者放置数据,接收者读取后移除数据
- 邮箱将数据从一个任务发送到另一个任务,发送者放置新数据并覆盖原数据,接收者读取数据并不移除
-
**发送并覆盖邮箱数据:**xQueueOverwrite
-
**接收并不移除数据:**xQueuePeek
软件定时器
- **软件定时器回调函数形式:**void ATimerCallback(TimerHandle_t xTimer)
- 回调函数应该短小精悍,不能阻塞
- 可以多个定时器使用同一回调函数,但是需要在函数里判断定时器句柄
- 也可以每个定时器使用不同回调函数
- **定时器周期:**指从启动软件定时器到执行软件定时器回调函数之间的时间
- 一次性定时器:只执行一次回调函数
- 自动重载:到期后自动重启,回调函数周期执行
- 到期时间是从发送“启动定时器”命令到定时器命令队列的时间开始计算,而不是守护任务从命令队列收到“启动定时器”命令的时间开始计算的
- 定时器状态:
- 休眠:休眠的定时器存在,可以通过句柄调用,回调函数不会执行
- xTimerCreate创建后就是休眠状态
- 运行转休眠,
- 运行:到规定的周期时间,自动执行回调函数
- 调用xTimerStart,xTimerReset,xTimerChangePeriod都可以转换为运行态
- 休眠:休眠的定时器存在,可以通过句柄调用,回调函数不会执行
- RTOS守护任务
- 在调度器启动时自动创建,优先级和栈大小由常量设置
- 软件定时器API将命令从调用函数发送到守护任务,在守护任务的“定时器命令队列”上
- 守护任务的调度:只有当守护任务为能够运行的最高优先级时,才会处理命令和执行定时器回调函数
- 发送到定时器命令队列的命令包含时间戳,确保启动的定时器是从发送“启动定时器”命令到定时器命令队列的时间开始计算
- **创建软件定时器:**xTimerCreate
- 启动软件定时器:xTimerStart
- **停止定时器:**xTimerStop
- **删除定时器:**xTimerDelete
- **定时器ID:**是一个标签值,可以随意使用,创建软件定时器时,会给ID分配初始值
- vTimerSetTimerID,创建定时器时,会为软件定时器分配一个标识符 (ID), 此函数更改此标识符。
- pvTimerGetTimerID,返回分配给软件计时器的 ID
- **更改软件定时器周期:**xTimerChangePeriod
- **重置软件定时器:**xTimerReset
中断管理
-
任务是软件功能,和运行的硬件无关。中断是硬件功能,最低优先级的中断可以抢断最高优先级任务,反之则不可以。
-
专门用于ISR的API函数,在名称后添加FromISR,不要在ISR中调用没有后缀的函数
-
**使用单独中断安全函数的缺点:**有需要在ISR中调用第三方函数,但第三方函数用了正常的FreeRTOS API函数
- 将中断处理推迟给任务,就可以在任务中调用
- 如果有的话,将API改为FromISR结尾的函数
-
xHigherPriorityTaskWoken参数
-
中断结束时,会回到打断处继续执行,但如果中断期间,有了更高优先级任务就绪,就应该执行更高优先级任务,而不是返回原点继续执行。如果不管的话,更高优先级任务将保持就绪状态,直到不在中断时的下一次调度器运行。
- 切换更高优先级任务不会在中断内自动发生,设置了xHigherPriorityTaskWoken变量通知应该上下文切换
- FromISR结尾的函数有xHigherPriorityTaskWoken变量,用于此目的,taskYIELD函数是在任务中请求任务切换的函数。
- portYIELD_FROM_ISR是taskYIELD函数的中断版本
-
示例代码如下:
void vTimerISR( void * pvParameters ) { BaseType_t xHigherPriorityTaskWoken = pdFALSE; xHigherPriorityTaskWoken = pdFALSE; xSemaphoreGiveFromISR( xSemaphore, &xHigherPriorityTaskWoken ); /* Yield if xHigherPriorityTaskWoken is true. The actual macro used here is port specific. */ portYIELD_FROM_ISR( xHigherPriorityTaskWoken ); }
-
-
推迟中断处理
- 中断所需的其他处理工作通常可以在任务中运行,所以可以将中断的工作推迟到任务
- 以下情况强烈建议推迟到任务
- 中断所需的处理并不简单
- 任务处理能方便执行ISR内部无法执行的操作
- 中断处理不确定,不知道处理工作需要多长时间
-
二进制信号量:能够有效使任务和中断同步,可以认为使长度为1的队列
-
设置推迟任务的优先级确保,该任务可以抢占系统其他任务
-
在ISR实现中调用portYIELD_FROM_ISR,确保ISR返回到推迟中断处理任务
-
创建二值信号量: xSemaphoreCreateBinary
-
**释放信号量:**xSemaphoreGive
-
**获取信号量:**xSemaphoreTake
-
-
**计数信号量:**可用于资源管理,计数事件
- **创建计数信号量:**xSemaphoreCreateCounting
- **释放信号量:**xSemaphoreGive
- **获取信号量:**xSemaphoreTake
-
**推迟工作到守护任务:**xTimerPendFunctionCallFromISR()
-
中断程序使用队列:
- 使用xQueueSendToFront和xQueueSendToBack的ISR版本
- 数据到达频率很高,队列效率不高
- 更高效且适合生产代码方法:
- 直接内存访问DMA
- 接收到的字符复制到线程安全的RAM缓冲区
- 直接在ISR内处理接收到的字符,队列只发送处理结果
-
中断嵌套
- 数字优先级和逻辑优先级:数字是分配给中断优先级的数字,逻辑是描述该中断相较于其他中断的优先级
- configMAX_SYSCALL_INTERRUPT_PRIORITY,低于此优先级中断可以被管理
- configKERNEL_INTERRUPT_PRIORITY,设置滴答中断的优先级,最低优先级
- 对时间精度要求非常严格的功能,可以考虑使用高于configMAX_SYSCALL_INTERRUPT_PRIORITY的优先级
- 必须始终configKERNEL_INTERRUPT_PRIORITY设置为尽可能最低的中断优先级
- configMAX_SYSCALL_INTERRUPT_PRIORITY的数值要注意,如cortex-m不允许设置为0
资源管理
-
资源访问导致数据损坏的例子:A任务打印hello,A打印到he,B任务抢占A,打印abort,最终结果就是heabortllo。这是不正确的。
-
**函数重入:**函数可以安全的在多个任务调用,或既可以从中断也可以在任务使用,那么函数就是重入,也称作线程安全。每个任务都维护自己的栈和硬件寄存器值,除了访问栈上的数据或保存至寄存器的数据外,不访问其他数据,就是重入函数。
-
**相互排斥:**任务之间共享的资源进行访问时,必须使用相互排斥进行管理。使资源不被共享,被单一程序访问。
-
**临界区:**taskENTER_CRITICAL和taskEXIT_CRITICAL成对使用。FROM_ISR版本的taskENTER_CRITICAL会有返回值,返回值要传递给taskEXIT_CRITICAL。
- 工作原理是禁用中断,即禁用所有可以管理的中断
- 临界区必须短小,否则对中断响应产生不利响应
- 临界区的嵌套是安全的,Freertos中两个宏定义是改变中断使能状态唯一合法方式
-
**暂停调度器:**如果临界区代码过长,可以使用暂停调度器,但恢复调度器操作较慢
- vTaskSuspendAll,暂停调度器
- xTaskrResumeAll,恢复调度器
-
**互斥量:**特殊的二进制信号量,获取后必须归还,归还后别的任务才能获取,否则可能死锁。
- 创建互斥量:xSemaphoreCreateMutex
- 获取互斥量:xSemaphoreTake
- 释放互斥量:xSemaphoreGive
-
互斥量和优先级密切相关的概念
- **优先级反转:**优先级高的任务等待优先级低的任务释放互斥量,仔细思考资源访问尽量避免。
- **优先级继承:**为了解决互斥量产生的优先级反转,rtos会将持有者的优先级提高至与等待互斥量的任务优先级相同,即继承优先级,归还后优先级恢复原状。不能在中断中使用互斥量。
- **死锁:**两个任务都在等待对方持有的资源而无法继续,充分考虑系统,识别并消除。
- **递归互斥量:**当任务获取了互斥量,但执行过程中有函数继续获取互斥量,导致自己锁死自己。
- 使用递归互斥量来避免,可以被同一任务多次获取,接着执行调用归还后,才能归还。类似成对使用。
- 创建递归互斥量:xSemaphoreCreateRecursiveMutex
- 获取递归互斥量:xSemaphoreTakeRecursive
- 释放递归互斥量:xSemaphoreGiveRecursive
-
互斥量和任务调度
-
不同优先级任务获取同一互斥量,优先级高的任务先进入运行状态。
-
如果A,B任务优先级相同,A先获取了互斥量,等到时间片结束切换B,接着回A,释放互斥量,再等到时间片结束,B才能运行
-
推荐的更平等处理时间的例子
-
// 记录时间 Xtime = xTaskGetTickCount(); // 释放互斥量 xSemaphoreGive // 滴答计数变化时,调用切换 if(xTaskGetTickCount() != Xtime) { taskYIELD(); }
-
-
-
守门人任务:指对任务拥有唯一所有权的任务,其他需要访问资源的任务只能通过守门人任务。
- 方法简单,基本没有反转和死锁风险
事件组
-
事件组的特性:
- 允许任务阻塞状态下等待一个或多个事件组合发生
- 事件组会解除所有在等待同一个事件或事件组合的任务的阻塞
- 通常可以用一个事件组代替多个二进制信号量
-
事件组标志是一个布尔值,每一位都代表一个事件是否发生
- configUSE_16_BIT_TICKS为1,则事件组为16位,但其中的高八位保留,即包含8个可用事件位
- configUSE_16_BIT_TICKS为0,则事件组为32位,但其中的高八位保留,即包含24个可用事件位
-
**事件组创建:**xEventGroupCreate
-
**事件组位设置:**xEventGroupSetBits
-
**读取事件组位:**xEventGroupWaitBits
-
**多任务事件组相互同步:**xEventGroupSync
任务通知
-
任务通知允许任务之间交互,不要单独的通信对象。通过任务通知,任务或ISR可以直接向接收任务发送事件。
-
通常用来替代二值信号量
-
优势与劣势:
- 任务通知比队列、信号量等操作快得多
- 所需RAM也要小的多
- 无法向ISR发送事件和数据
- 无法启用多个接收任务
- 无法缓冲多个数据项
- 无法向多个任务广播
- 无法在阻塞状态等待发送完成
-
**发送任务通知:**xTaskNotify
-
**发送任务通知的简单版:**xTaskNotifyGive
-
**接收任务通知:**xTaskNotifyWait
-
**接收任务通知的简单版:**ulTaskNotifyTake
低功耗支持
- **与节能有关的宏:**portSUPPRESS_TICKS_AND_SLEEP
开发者支持
- **断言:**cofigASSERT
- **任务状态信息快照:**uxTaskGetSystemState,比较重要的信息主要有
- 到目前为止分配给任务的总运行时间
- 任务剩余的最小堆栈空间量
- **提供可读的任务信息ASCII表格:**vTaskList
- 很耗费CPU,仅用于调试阶段
- **将运行时统计信息格式化为可读表格:**vTaskGetRunTimeStats
- 很耗费CPU,仅用于调试阶段
- 还需要实现一个计数器
故障排除
- 演示工程添加任务导致演示工程崩溃
- 延时工程的堆空间很精确,没有足够的堆空间
- 中断使用API导致应用崩溃
- 使用FROM_ISR结尾的API函数
- 有时应用程序在中断服务程序中崩溃
- 中断是否导致了栈溢出
- 调度器启动第一个任务时崩溃
- 有些处理器启动调度器哦i之前必须处于特权模式
- 确保移植没问题,中断处理程序没问题
- 应用程序在调度器启动前崩溃
- 启动前不允许上下文切换
- 调度器暂停或者临界区调用API函数,导致应用程序崩溃
- 调度器暂停不能调用API函数
- 临界区内不能调用API函数
Freertos移植
- 官网1:https://github.com/FreeRTOS/FreeRTOS-LTS,稳定版本的源码下载,主要使用其中的FreeRTOS-Kernel文件夹
- 官网2:https://github.com/FreeRTOS/FreeRTOS,移植的官方Demo,主要参考FreeRTOS下的Demo文件夹
第一步
- 复制FreeRTOS-LTS文件夹下的代码,portable只保留“MemMang”文件夹与“RVDS”文件夹,不同的芯片需要保留不同的文件夹,我用的keil+STM32F103,RVDS文件夹只保留ARM_CM3
- 将保留的文件添加到工程中,Src是上一张图中的所有C文件,keil添加源文件和头文件就不谈了
- FreeRTOSConfig.h文件获取有两种途径:
- 从 FreeRTOS-Kernel\examples\template_configuration获取,这个比较全面有各种数据,需要自己阅读与删减,例如configCPU_CLOCK_HZ等等都要自己修改
- 从官方Demo下的FreeRTOS\Demo\CORTEX_STM32F103_Keil下获取,这个比较精简,都按照你芯片类型改好了,推荐
- 增加文件后,添加,然后将it.c的PendSV_Handler与SVC_Handler注释
#define xPortPendSVHandler PendSV_Handler
#define vPortSVCHandler SVC_Handler
- 修改SysTick_Handler,移植完成即可测试,请保证滴答定时器与configTICK_RATE_HZ周期相同
extern void xPortSysTickHandler(void);
void SysTick_Handler(void)
{
// INCLUDE_xTaskGetSchedulerState在FreeRTOS.h中定义,默认为0
#if (INCLUDE_xTaskGetSchedulerState == 1 )
if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED)
{
#endif /* INCLUDE_xTaskGetSchedulerState */
xPortSysTickHandler();
#if (INCLUDE_xTaskGetSchedulerState == 1 )
}
#endif /* INCLUDE_xTaskGetSchedulerState */
}