前言
(1)
FreeRTOS
是我一天过完的,由此回忆并且记录一下。个人认为,如果只是入门,利用STM32CubeMX
是一个非常好的选择。学习完本系列课程之后,再去学习网上的一些其他课程也许会简单很多。
(2)本系列课程是使用的keil
软件仿真平台,所以对于没有开发板的同学也可也进行学习。
(3)叠甲,再次强调,本系列课程仅仅用于入门。学习完之后建议还要再去寻找其他课程加深理解。
(4)本系列博客对应代码仓库:gitee仓库
实战
(1)将上一篇博客最终的代码复制一份。
开启静态创建任务宏定义
(1)使用
FreeRTOS
的静态创建任务的时候,需要在FreeRTOSConfig.h
打开configSUPPORT_STATIC_ALLOCATION
这个宏定义。一般是默认打开了的。如果你不需要使用静态创建任务,个人建议将这个宏关闭,这样生成的代码段会少一些。
任务创建
FreeRTOS静态任务创建
(1)对于
STM32CubeMX
而言,静态创建任务和动态创建任务只有如下部分不同,整体使用上都一样。反正ST
做了RTOS
层抽象,最终对外接口都是osThreadNew()
函数。
(2)因为上一篇博客是使用Keil端手动传入参数,咱们已经会了,那么我现在就教一下大家如何在STM32CubeMX
中传入参数。
(3)这里需要注意一点,你使用STM32CubeMX
让任务传入参数,这个参数需要在keil
端创建。(按Ctrl+F
搜索Private variables
即可找到如下部分)
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN Variables */
static char *CubemxTask_argument = "StartCubemxTask\r\n";
/* USER CODE END Variables */
(4)之后你就需要在任务里面调用这个参数,按照如下方法使用。(按
Ctrl+F
搜索StartCubemxTask
即可找到任务函数)
/* USER CODE END Header_StartCubemxTask */
void StartCubemxTask(void *argument)
{
/* USER CODE BEGIN StartCubemxTask */
char *CubemxTaskPrintf = (char *)argument;
/* Infinite loop */
for(;;)
{
printf(CubemxTaskPrintf);
}
/* USER CODE END StartCubemxTask */
}
keil端手动创建静态任务
(1)静态创建的任务,我们需要自己创建三个参数。一个是为静态任务准备的栈空间,一个是
TCB
控制块,一个是任务句柄。(按Ctrl+F
搜索Private variables
即可找到如下部分,并进行补充)
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN Variables */
static StackType_t g_pucStackKeilTaskBuff[128]; // 为静态任务准备的栈空间
static StaticTask_t g_TCBKeilTask; // 静态任务的TCB控制块
TaskHandle_t keilTaskHandle; // 静态任务的句柄
static char *CubemxTask_argument = "StartCubemxTask\r\n";
/* USER CODE END Variables */
(2)上一篇博客我们已经介绍了如何使用
keil
端手动传入参数的方法,于是当前这一篇就不传入参数了。(按Ctrl+F
搜索add threads
即可找到如下部分,并进行补充)
/* USER CODE BEGIN RTOS_THREADS */
/* add threads, ... */
keilTaskHandle = xTaskCreateStatic(StartKeilTask,"KeilTask", 128, NULL, osPriorityLow1, g_pucStackKeilTaskBuff,&g_TCBKeilTask);
if(keilTaskHandle == NULL)
{
printf("KeilTask creation failed\r\n");
}
(3)然后再补充任务函数内容。(按
Ctrl+F
搜索BEGIN Application
即可找到如下部分,并进行补充)
/* Private application code --------------------------------------------------*/
/* USER CODE BEGIN Application */
int fputc(int ch, FILE *f)
{
unsigned char temp[1]={ch};
HAL_UART_Transmit(&huart1,temp,1,0xffff);
return ch;
}
void StartKeilTask(void *argument)
{
while(1)
{
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
HAL_Delay(100);
}
}
/* USER CODE END Application */
测试结果
(1)和上文方法一致,不过因为我们修改了
STM32CubeMX
的配置信息重新生成keil
工程的时候,原来的配置信息都将会被清空,因此还需要重新配置。
keil调试配置
(1)打开微库。
(2)配置模拟器
DARMSTM.DLL
pSTM32F103C8
配置虚拟示波器
(1)打开调试界面
(2)选择逻辑分析仪,检测PC13引脚。为什么下面输入的是PORTC.13,原因很简单,格式为PORTx.y,x表示端口,y表示具有引脚数值,注意’.'必须是英文的!
配置虚拟串口
(1)如下图
实测
(1)我们会发现结果和动态创建任务的结果是一致的。这也很好的证明了,静态创建任务和动态创建任务,在使用上是没有本质上的区别的。只有在创建任务和删除任务的时候略微不同。
(2)那么我们又为什么需要弄一个动态创建任务和一个静态创建任务呢?这就需要大家对堆有一定的认识了。具体内容请看理论部分,如果不理解,我建议无脑使用动态创建任务。
理论
(1)再次强调,以下内容对堆栈的知识要有一定的认知!如果新手小白看不懂,建议无脑使用动态创建任务!
xTaskCreateStatic()函数介绍
(1)静态创建任务函数和动态创建任务函数就最后部分不一样。
<1>如果是动态创建任务函数,那么最后只需要传入一个任务句柄即可。
<2>如果是静态创建任务函数,那么最后传入的任务句柄修改为栈空间的首地址和TCB控制块。
(2)栈空间:可能有同学不太能理解,我也不想讲一大堆术语,说白了,就是一个数组。这个涉嫌到汇编的内容,下面部分能听懂就听,听不懂略过。
<1>当函数A调用函数B的时候,一些参数信息需要保存进入栈。(也就是你这里传入的数组)
<2>函数中的局部变量会存放在栈空间里面。(也就是你这里传入的数组)
<3>RTOS进行任务调度切换的时候,需要保护现场。所谓的保护现场,就算把当前的下面这16个寄存器里面的值都存入到栈空间。(也就是你这里传入的数组)
(3)TCB控制块:这个就是句柄最终指向的区域。具体细节请看:
句柄到底是什么?TCB又是什么?C代码实例讲解
BaseType_t xTaskCreateStatic(
TaskFunction_t pxTaskCode, // 指向任务函数的函数指针
const char * const pcName, // 任务的名字,最大长度configMAX_TASK_NAME_LEN
const configSTACK_DEPTH_TYPE usStackDepth, // 任务栈大小,单位为word,10表示40字节
void * const pvParameters, // 调用任务函数时传入的参数
UBaseType_t uxPriority, // 优先级,范围:0 ~ (configMAX_PRIORITIES-1)。数值越大,优先级越大
StackType_t * const puxStackBuffer, // 栈空间首地址指针
StaticTask_t * const pxTaskBuffer // 任务的TCB控制块
);
如何估计栈的大小
uxTaskGetStackHighWaterMark()函数介绍
(1)
<1>对于栈的大小估计,FreeRTOS
中提供了uxTaskGetStackHighWaterMark()
函数来查看任务使用的栈空间历史使用剩余值的最小值,单位是world(也就是4字节。很多C站博主都说是单位是1字节。我不知道他们从哪里得出的结论,我是直接看的FreeRTOS源码介绍)。
<2>当这个值越小说明任务堆栈溢出的可能性就越大。就要尝试适当的增大栈空间分配。
<3>如果这个值你发现非常的大。那么就可以适当的减小创建时候分配的栈空间。
/**
* @brief 查看任务使用的栈空间大小
*
* @param xTask 任务句柄
*
* @return 任务堆栈可用的最小值,单位word(4字节)
*/
UBaseType_t uxTaskGetStackHighWaterMark( TaskHandle_t xTask );
/* === 使用方法 === */
void StartCubemxTask(void *argument)
{
/* USER CODE BEGIN StartCubemxTask */
char *CubemxTaskPrintf = (char *)argument;
UBaseType_t Cubemx_Stack;
/* Infinite loop */
for(;;)
{
printf(CubemxTaskPrintf);
Cubemx_Stack = uxTaskGetStackHighWaterMark(keilTaskHandle);
printf("CubemxTask is %ld\r\n",Cubemx_Stack);
}
/* USER CODE END StartCubemxTask */
}
(2)使用这个函数之前,需要注意,要在
STM32CubeMX
中使能INCLUDE_uxTaskGetStackHighWaterMark
。
(3)或者在
FreeRTOSConfig.h
文件中让INCLUDE_uxTaskGetStackHighWaterMark
这个宏定义为1。
测试结果
(1)开启仿真调试,我们即可知道最终的还剩下的栈空间大小。
静态创建的任务和动态创建
两者区别
(1)在FreeRTOS中,任务可以通过静态创建和动态创建两种方式来实现。他们只有在任务创建的初期和能否释放栈有区别,最终使用是一模一样的。
- 静态创建的任务,栈是存放在数组里面的,也就是bss段。因此静态创建任务的栈无法释放,在编译初期就定好了。
- 动态创建的任务,栈是通过类似
malloc
的函数实现的,也就是堆区域。堆是可以通过类似free
函数释放的。(3)以下是它们之间的主要区别以及各自的优缺点:
<1>静态创建任务:
优点:
- 内存管理: 不需要动态分配内存,所有的资源在编译时就已经分配好了。
- 实时性: 由于任务的资源在编译时就已经分配,因此任务创建的实时性较好。注意,这里只有在任务创建的时候实时性不同,任务创建完成之后,使用是一模一样的!!!
- 可靠性: 如果你的应用程序在内存方面受到限制,静态创建任务可以帮助你在编译时就分配任务所需的内存,而不是在运行时动态分配。这样可以避免在运行时发生内存分配失败或碎片化问题。
- 容易调试: 静态创建任务在编译时就分配了任务的资源,这使得在调试阶段更容易检测和解决问题,因为你可以直接查看任务的内存布局和大小。
缺点:
- 灵活性: 静态创建任务需要在编译时确定任务的数量和占用的资源,因此不够灵活,无法动态地根据运行时的条件调整任务数量和任务栈大小,在程序烧录进入MCU之后就是定死的。
- 浪费资源: 如果分配的资源过大,可能会浪费内存。因为静态创建的任务栈无法被释放。但是如果你这个任务就是要一直运行,不需要删除,这个问题不需要考虑。
- 复杂性: 涉及多个任务和更复杂的系统结构时,静态创建任务可能会增加系统配置的复杂性。任务的数量、优先级和资源需求都需要在编译时确定,这可能使得系统的调整变得更加繁琐。
<2>动态创建任务:
优点:
- 灵活性: 可以根据应用程序的需求动态创建和删除任务。也就是说他的任务数量和任务栈大小,是可以在MCU运行过程中进行调整。
- 资源利用: 可以更灵活地分配内存,减少资源的浪费。因为他的任务栈是通过类似
malloc
函数申请的,也可也通过类似free
函数释放。缺点:
- 内存管理: 动态分配内存需要考虑内存的释放,否则可能导致内存泄漏。
- 实时性: 由于涉及到动态内存分配,任务的创建可能不如静态创建及时。
- 可靠性: 动态创建的任务可能会因为堆不足够,导致任务创建失败的问题。
两种创建方式如何抉择
(1)选择静态创建还是动态创建取决于应用的具体需求。如果你的任务具备以下特征,就推荐使用静态创建任务:
- 任务的数量和属性在编译时就确定,不需要动态创建或删除任务。
- 任务的栈空间需求可以预先估计,不需要动态调整。
- 堆空间有限,或者想要节省堆空间。
- 对任务创建的速度和可靠性有较高的要求。
(2)当你的任务具备以下特征,就推荐使用动态创建任务:
- 任务的数量和属性在运行时才确定,需要动态创建或删除任务。
- 任务的栈空间需求难以预先估计,需要动态调整。
- 堆空间充足,或者不在乎堆空间的占用。
- 对任务创建的速度和可靠性没有较高的要求。
(3)关于动态分配任务还是静态分配任务到底如何抉择,这个要具体问题具体分析。但是对于新手小白来说,哪个容易使用,就用哪个,因此我个人推荐新手小白无脑使用动态分配任务的方式。
FreeRTOS的堆管理机制
(1)上面我说动态创建任务的任务栈是通过类似
malloc
函数实现的,为什么用类似两字呢?因为标准C库存在如下问题,所以自己写了一个堆分配算法。
malloc()
和free()
函数在嵌入式系统上并不总是可用。- 占用了宝贵的代码空间
- 不是线程安全的
- 执行函数所需时间将因调用而异
(2)一个嵌入式/实时系统的
RAM
和定时要求可能与另一个非常不同,所以单一的RAM
分配算法 将永远只适用于一个应用程序子集。因此FreeRTOS
提供了几种堆管理方案, 其复杂性和功能各不相同。
<1>heap_1
不太有用,因为FreeRTOS
添加了静态分配支持。(也就是静态创建任务函数xTaskCreateStatic()
)
<2>heap_2
现在被视为旧版,因为较新的heap_4
实现是首选。
<3>heap_1
占用code段最小,heap_5
占用空间最多。
文件 | 优点 | 缺点 |
---|---|---|
heap_1.c | 分配简单,时间确定 | 只分配、不回收 |
heap_2.c | 动态分配、最佳匹配 | 碎片、时间不定 |
heap_3.c | 调用标准库函数 | 速度慢、时间不定 |
heap_4.c | 相邻空闲内存可合并 | 可解决碎片问题、时间不定 |
heap_5.c | 在 heap_4 基础上支持分隔的内存块 | 可解决碎片问题、时间不定 |
参考
(1)freeRTOS使用uxTaskGetStackHighWaterMark函数查看任务堆栈空间的使用情况
(2)CubeMX FreeRTOS uxTaskGetStackHighWaterMark()的使用
(3)FreeRTOS官方文档:静态内存分配 vs 动态内存分配
(4)FreeRTOS官方文档:内存管理
(5)FreeRTOS官方文档:任务堆栈应该多大?
(6)韦东山:FreeRTOS入门与工程实践课程——[4-2]内存管理部分