有了前面的几篇文章 FreeRTOS 基本上已经可以在项目中使用上了:
《FreeRTOS入门(01):基础说明与使用演示》
《FreeRTOS入门(02):任务基础使用与说明》
《FreeRTOS入门(03):队列、信号量、互斥量》
这篇文章将介绍一些零散的,FreeRTOS使用过程中可能需要注意的,或者有助于开发调试的内容。
中断是嵌单片机开发中稍微复杂些但又不得不提的内容。
FreeRTOS中很多函数都有名称后面带 FromISR
的版本,这些函数都是提供在中断函数中使用的(虽然也可以在非中断函数中使用)。
这类函数中有些函数可能可以传入 BaseType_t *pxHigherPriorityTaskWoken
这样一个参数,这个参数的含义是:如果当前操作之后有更高优先级的任务可以运行了(比如获得了等待的资源),那么该参数会被设置为 pdTRUE
,最好在退出中断函数前切换任务。比如下面伪代码:
void xxxISR(void) {
BaseType_t woken = pdFALSE; // 用来保存pxHigherPriorityTaskWoken
...
xxxxxxFromISR( ..., ..., &woken); // 可能使用多次
...
taskYIELD_FROM_ISR(woken); // 如果woken为pdTRUE,则退出中断前切换任务
}
在不使用 taskYIELD_FROM_ISR
的时候,退出中断函数后会返回原来的任务,如果有高优先级的任务就绪的话需要在下一次调度时才会切换。使用 taskYIELD_FROM_ISR
相当于会立即进行一次调度。
上面是FreeRTOS中对于中断相关操作上最大的一点内容。剩下的就是常见的屏蔽和恢复中断的操作:
taskENTER_CRITICAL() // 屏蔽中断
taskEXIT_CRITICAL() // 恢复中断
// 需要注意的是这是可以递归调用的,多次屏蔽需要多次恢复
// 上面函数在中断中使用的版本
taskENTER_CRITICAL_FROM_ISR()
taskEXIT_CRITICAL_FROM_ISR()
除了这些操作外,中断相关操作最重要的是要快,中断服务程序或者禁用中断的时间越短越好。
在FreeRTOS内核代码的 portable/MemMang
目录下有 heap_x.c
文件,这些文件是FreeRTOS对堆内存的管理方式。
heap_1.c
方式只申请内存,不释放内存,适用于可靠性要求非常严格的场合(如果要这么用的话,所有的 xxxCreate函数建议都换成xxxCreateStatic函数)。
现在一般使用 heap_4.c
或者 heap_5.c
即可,比如使用CH32V307的FreeRTOS项目模板方式创建项目使用的就是 heap_4.c
。
下面是内存相关的一些公共函数(有些 heap_x.c
文件可能并没有实现所有的函数):
void * pvPortMalloc( size_t xWantedSize ) // 申请内存
void vPortFree( void * pv ) // 释放内存
size_t xPortGetFreeHeapSize( void ) // 返回当前空闲堆内存
size_t xPortGetMinimumEverFreeHeapSize( void ) // 返回在系统运行过程中堆空间的最小空闲空间
FreeRTOSConfig.h
文件中 configTOTAL_HEAP_SIZE
参数用于设置FreeRTOS可用的总的堆大小,你可以根据上面的函数实际测试来调整该参数的大小。
如果 FreeRTOSConfig.h
文件中 configUSE_MALLOC_FAILED_HOOK
设置为 1
,则你需要实现自己的 void vApplicationMallocFailedHook( void )
函数,这样在申请内存失败时会调用该函数。
在使用 xTaskCreate()
创建任务时,任务的栈会从FreeRTOS的堆中分配。在程序运行中栈溢出是一个比较危险的情况。所以FreeRTOS也提供了一些栈溢出检测的方案。
FreeRTOSConfig.h
文件中 configUSE_MALLOC_FAILED_HOOK
参数可以配置栈溢出检测的方案,值为 0
时不检查栈溢出。值为 1
时候使用检测方法一,值为 2
时候使用检测方法二,后者比前者可靠性稍高,但性能稍低。一般如果要使用的话使用方法二即可,要不就是测试各个任务栈足够用的话直接不检查栈溢出,因为毕竟检查本身比较耗性能。
如果使用的栈溢出检测,则需要实现栈溢出钩子函数:
void vApplicationStackOverflowHook( TaskHandle_t xTask, signed char *pcTaskName ) {
// TODO
}
如果使用了栈溢出检查,还可以在运行时获取任务最小空闲栈大小(注意这个不一定是准确的):
// 检测方法一使用
UBaseType_t uxTaskGetStackHighWaterMark(TaskHandle_t xTask)
// 检测方法二使用
UBaseType_t uxTaskGetStackHighWaterMark2(TaskHandle_t xTask)
在开发阶段可以在 FreeRTOSConfig.h
文件中定义 configASSERT
,比如使用CH32V307的FreeRTOS项目模板方式创建项目,该文件中就有如下定义:
#define configASSERT( x ) if( ( x ) == 0 ) { taskDISABLE_INTERRUPTS(); printf("err at line %d of file \"%s\". \r\n ",__LINE__,__FILE__); while(1); }
这样当发送严重错误时就会运行到这里定义错误位置。如果系统足够稳定的话可以注销改行提高性能。
下面是FreeRTOS提供的一些用于开发调试的方法,需要注意的是这些方法会消耗大量的性能:
// 下面两个方法需要设置configUSE_TRACE_FACILITY为1才能使用
// 为每个任务填充TaskStatus_t结构体,用于记录任务句柄、任务名称、任务优先级、任务状态和任务消耗的运行时间等信息
UBaseType_t uxTaskGetSystemState(TaskStatus_t * const pxTaskStatusArray, const UBaseType_t uxArraySize, unsigned long * const pulTotalRunTime);
// 为单个任务填充TaskStatus_t结构体,用于记录任务句柄、任务名称、任务优先级、任务状态和任务消耗的运行时间等信息
void vTaskGetInfo(TaskHandle_t xTask, TaskStatus_t *pxTaskStatus, BaseType_t xGetFreeStackSpace, eTaskState eState)
// 下面方法需要设置 configUSE_TRACE_FACILITY 和 configUSE_STATS_FORMATTING_FUNCTIONS 为1才能使用
// vTaskList() 调用 uxTaskGetSystemState(), 然后将 uxTaskGetSystemState() 生成的原始数据格式化为人类可读的 (ASCII) 表格
// 显示每个任务的状态,包括任务的堆栈高水位线(高水位线数字越小, 任务越接近于溢出其堆栈)。
void vTaskList(char *pcWriteBuffer);
下面是 vTaskList
函数的简单测试:
#include "debug.h"
#include "FreeRTOS.h" // 引入头文件
#include "task.h" // 引入头文件
void task(void *pvParameters) {
char buf[40*8]; // 每个任务约需要40字节
while(1) {
vTaskList(buf);
printf(buf);
vTaskDelay(1000);
}
}
int main(void) {
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
SystemCoreClockUpdate();
Delay_Init();
USART_Printf_Init(115200);
xTaskCreate(task, "task", 512,NULL, 5, NULL); // 创建一个任务
vTaskStartScheduler(); // 任务调度,任务将在这里根据情况开始运行,程序将在这里无序循环
while(1) {} // 程序不会运行到这里
}
B
已阻塞;R
准备就绪;D
已删除(等待清理);S
已挂起或已阻塞,没有超时;除了上面信息的统计,FreeRTOS中还带有统计各个任务占用时间的方法 :
// 使用前需要定义下面几项内容
// configGENERATE_RUN_TIME_STATS
// configUSE_STATS_FORMATTING_FUNCTIONS
// configSUPPORT_DYNAMIC_ALLOCATION
// portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()
// portGET_RUN_TIME_COUNTER_VALUE()
void vTaskGetRunTimeStats( char *pcWriteBuffer );
下面是使用演示:
// main.c文件
#include "debug.h"
#include "FreeRTOS.h" // 引入头文件
#include "task.h" // 引入头文件
volatile unsigned long ulHighFrequencyTimerTicks = 0; // 时间计数值
void TIM1_UP_IRQHandler(void) __attribute__((interrupt())); // 定时器中断回调函数声明
// 定时器中断回调处理
void TIM1_UP_IRQHandler(void){
ulHighFrequencyTimerTicks++; // 计数值累加
TIM_ClearITPendingBit(TIM1, TIM_IT_Update); // 清除中断标志
}
// 定时器初始化
void TIM1_Init(void) {
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure = { 0 };
RCC_APB2PeriphClockCmd( RCC_APB2Periph_TIM1, ENABLE);
TIM_TimeBaseInitStructure.TIM_Period = 100; // 根据下面分频设置,每次中断约为100us,这里默认FreeRTOS Tick为2ms,此处频率为FreeRTOS时钟的20倍(推荐为10~100倍之间)
TIM_TimeBaseInitStructure.TIM_Prescaler = 96 - 1; // 96MHz下此分频系数每次计数时间为1us
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit( TIM1, &TIM_TimeBaseInitStructure);
NVIC_InitTypeDef NVIC_InitStructure = {0};
NVIC_InitStructure.NVIC_IRQChannel = TIM1_UP_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
TIM_ITConfig(TIM1, TIM_IT_Update, ENABLE);
TIM_Cmd(TIM1, ENABLE);
}
// 时间统计并打印
void stat(void *pvParameters) {
char buf[40 * 8]; // 每个任务约需要40字节
while(1) {
vTaskGetRunTimeStats(buf);
printf(buf);
vTaskDelay(5000);
}
}
// 用于测试的任务
void task(void *pvParameters) {
uint32_t delay = (uint32_t)pvParameters;
while(1) {
for (int i = 0; i < 1000 * 1000; i++) {
__NOP();
}
vTaskDelay(delay);
}
}
// 主函数
int main(void) {
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
SystemCoreClockUpdate();
Delay_Init();
USART_Printf_Init(115200);
xTaskCreate(stat, "stat", 512, NULL, 7, NULL);
xTaskCreate(task, "task1", 256, (void *)50, 5, NULL); // 用于测试的任务
xTaskCreate(task, "task2", 256, (void *)100, 5, NULL); // 用于测试的任务
vTaskStartScheduler(); // 任务调度,任务将在这里根据情况开始运行,程序将在这里无序循环
while(1) {}
// 程序不会运行到这里
}
// FreeRTOSConfig.h文件(部分内容)
#define configGENERATE_RUN_TIME_STATS 1
#define configUSE_TRACE_FACILITY 1
// #define configUSE_STATS_FORMATTING_FUNCTIONS 1 // 默认为1
extern void TIM1_Init(void);
#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() TIM1_Init()
extern volatile unsigned long ulHighFrequencyTimerTicks;
#define portGET_RUN_TIME_COUNTER_VALUE() ulHighFrequencyTimerTicks
这篇文章中介绍的都是一些零散的,个人觉得相对重要或是有用的内容。可能没有办法一下子面面俱到,后面有需要补充或修改的内容会在这篇文章中更新。