利用STM32CubeMX和keil模拟器,3天入门FreeRTOS(1.1) —— 创建多个静态任务实操和简单讲解

发布时间:2024年01月22日

前言

(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]内存管理部分

文章来源:https://blog.csdn.net/qq_63922192/article/details/134222588
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。