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

发布时间:2024年01月22日

前言

(1)FreeRTOS是我一天过完的,由此回忆并且记录一下。个人认为,如果只是入门,利用STM32CubeMX是一个非常好的选择。学习完本系列课程之后,再去学习网上的一些其他课程也许会简单很多。
(2)本系列课程是使用的keil软件仿真平台,所以对于没有开发板的同学也可也进行学习。
(3)叠甲,再次强调,本系列课程仅仅用于入门。学习完之后建议还要再去寻找其他课程加深理解。
(4)本系列博客对应代码仓库:gitee仓库
(5)学习本文之前,需要先学会上一篇博客:利用STM32CubeMX和keil模拟器,3天入门FreeRTOS(0) —— 创建工程

实战

(1)说实话,我个人建议先实战,再理论这样会很方便大家理解和掌握。所以我就先直接实战了。
(2)本文是在上一篇博客基础上进行的,所以建议各位先按照上文配置好再开启本文。
(3)创建任务的方法有两种,第一种是STM32CubeMX图像化自动创建,第二种就是keil调用函数创建。我先使用STM32CubeMX图像化自动创建方便各位快速上手,然后再利用keil创建一个任务方便各位对比学习。
强调:利用keil创建一个任务是一定要学会的,否则你永远只会使用STM32CubeMX,想要换一款非ST的MCU操作FreeRTOS,那么你铁定懵逼。

在这里插入图片描述

FreeRTOS任务创建

STM32CubeMX端自动创建任务

(1)按照下图进行配置,很简单对吧。肯定有新手会想知道里面的参数都有什么作用。不要着急,下图中的介绍可以先不看。咱们先配置,让他跑起来,后面再讲理论。

在这里插入图片描述

keil端手动创建任务

(1)打开freertos.c文件。我们的任务创建代码写在这里面。

在这里插入图片描述

(2)创建任务句柄。因为STM32CubeMX有一个很恶心的特性,要求你的代码一定要写在他注释的范围之内,否则下次重新使用STM32CubeMX生成代码就会把你写的代码删除,所以要写在如下代码块内。
(找不到注释部分,按Ctrl+F搜索BEGIN Variables

/* USER CODE BEGIN Variables */
osThreadId_t keilTaskHandle;
/* USER CODE END Variables */

(3)创建任务句柄。任务函数要求必须是无限循环的,或者任务执行完成之后“自杀”。否则会进入产生HardFault_Handler硬件错误,之后就卡死在HardFault_Handler中断里面了。
(找不到注释部分,按Ctrl+F搜索BEGIN FunctionPrototypes,之后是BEGIN Application

/* USER CODE BEGIN FunctionPrototypes */
void StartKeilTask(void *argument);
/* USER CODE END FunctionPrototypes */
/* Private application code --------------------------------------------------*/
/* USER CODE BEGIN Application */
// 写法1
void StartKeilTask(void *argument)
{
	while(1)
	{
	}
}
// 写法2
void StartKeilTask(void *argument)
{
	// 任务代码
	// 任务执行完后自杀
	 vTaskDelete(NULL); 
}
/* USER CODE END Application */

(4)添加任务,找到add threads部分注释,如果不知道怎么找,按Ctrl+F搜索即可找到这部分注释,然后添加如下代码。

  /* USER CODE BEGIN Init */
	BaseType_t xReturned;
  /* USER CODE END Init */
  
  // ...
  
  /* USER CODE BEGIN RTOS_THREADS */
  /* add threads, ... */
	xReturned = xTaskCreate(StartKeilTask,"KeilTask", 128, "StartKeilTask\r\n", osPriorityLow1, &keilTaskHandle);
	if(xReturned != pdPASS)
	{
		printf("KeilTask creation failed\r\n");
	}
  /* USER CODE END RTOS_THREADS */

具体任务补充

(1)要做多任务,因为我是打算用keil模拟器学习,懒得上板子。所以说,打算一个任务进行GPIO13电平反转,一个任务进行串口打印。

STM32CubeMX具体任务实现

(1)打开串口1的异步通知。

在这里插入图片描述

keil端操作补充任务具体实现

(1)头文件部分补充
(找不到注释部分,按Ctrl+F搜索BEGIN Includes

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include <stdio.h>
#include <string.h>
#include "usart.h"
/* USER CODE END Includes */

(2)增加fputc()函数重定向。在StartKeilTask()StartCubemxTask()函数中添加具体任务实现,同时删除上一篇博客中FreeRTOS默认产生的StartDefaultTask任务中的信息。
<1>按Ctrl+F搜索Header_StartDefaultTask (找不到注释部分,按Ctrl+F搜索Header_StartDefaultTaskHeader_StartCubemxTaskapplication code

/* USER CODE END Header_StartDefaultTask */
void StartDefaultTask(void *argument)
{
  /* USER CODE BEGIN StartDefaultTask */
  /* Infinite loop */
  for(;;)
  {
    osDelay(1);
  }
  /* USER CODE END StartDefaultTask */
}

<2>按Ctrl+F搜索Header_StartCubemxTask

/* USER CODE END Header_StartCubemxTask */
void StartCubemxTask(void *argument)
{
  /* USER CODE BEGIN StartCubemxTask */
  /* Infinite loop */
  for(;;)
  {
		HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
		HAL_Delay(100);
  }
  /* USER CODE END StartCubemxTask */
}

<3>按Ctrl+F搜索application code

/* 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)
{
	char *KeilTaskPrintf = (char *)argument;
	while(1)
	{
		printf(KeilTaskPrintf);
	}
}
/* USER CODE END Application */

测试结果

keil调试配置

(1)打开微库。

在这里插入图片描述

(2)配置模拟器
DARMSTM.DLL
pSTM32F103C8

在这里插入图片描述

配置虚拟示波器

(1)打开调试界面

在这里插入图片描述

(2)选择逻辑分析仪,检测PC13引脚。为什么下面输入的是PORTC.13,原因很简单,格式为PORTx.yx表示端口,y表示具有引脚数值,注意’.'必须是英文的!

在这里插入图片描述
在这里插入图片描述

配置虚拟串口

(1)如下图

在这里插入图片描述

实测

(1)我们能够发现波形虚拟示波器上出现方波,同时虚拟串口也有数据打印。

在这里插入图片描述

理论

xTaskCreate()函数讲解

(1)传入参数:
<1>pxTaskCode。这个是一个函数指针,指向了我们要创建的任务函数。例如上面实战中,我们创建的任务是StartKeilTask()。所以说第一个参数传入StartKeilTask。(为了防止阅读本博客的人C预压基础过分的差,我在此科普以下,函数名就是一个函数指针。具体细节请自行学习。)
<2>pcName。这个是任务名字,对实际的开发中,就只是起一个标识作用,只要任务名字长度小于configMAX_TASK_NAME_LEN,那么随便你起什么名字。
<3>usStackDepth。任务栈深度,这个涉及的内容比较深,如果没有汇编基础,听这个部分也是浪费时间。所以新手记住一般给128即可,后面我也许会写一篇专门的博客。不过这里需要强调一点,这个单位是world,也就是4字节。如果给128,那么栈深度为128*4=512字节。
<4>pvParameters。调用任务函数时候传入的参数,我们能够看到,上面的实例中,这个地方我们传入的是"StartKeilTask\r\n"。那么在StartKeilTask()函数中,进行一次强制类型转换,即可调用这个值。这很好的体现了void *作为万能指针的优越性。
<5>uxPriority。任务优先级,数值越大,优先级越高。CMSIS_V1CMSIS_V2的优先级数量不一样。CMSIS_V1能够分配的优先级少一些,因此他所占用的资源也会少很多。其实绝大多数CMSIS_V1的优先级数量就够了,但是为了满足新手小白变态的欲望,硬要最新版本才爽,我就选择了CMSIS_V2
<6>pxCreatedTask 。这个是RTOS的任务控制块,后续如果想要删除某个任务,就需要调用这个任务句柄。这里如果传入NULL,那么想要删除某个任务,就只能让那个任务调用vTaskDelete(NULL);进行自杀了。否则这个任务永远存在。
(2)返回值:如果返回pdPASS,表示任务创建成功,如果返回errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY表示任务创建失败。

BaseType_t xTaskCreate(
	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)。数值越大,优先级越大
	TaskHandle_t * const pxCreatedTask         // 任务句柄, 以后使用它来操作这个任务
); 

osThreadNew()函数返回值部分讲解

(1)根据上面的讲解,想必各位对下图基本是有了一个大概的理解。我们在STM32CubeMX中进行的如下操作,最终会产生一个osThreadNew()函数。因为这个函数STM32CubeMX已经帮我们自动创建生成了,那么就需要相信STM32CubeMX。但是为什么还要讲解呢?因为STM32CubeMX虽然可以帮你创建任务,但是无法检测出任务是否创建成功。
(2)正因为上述问题,我们需要进行判断。osThreadNew()函数最终会返回一个指针,这个指针如何是空指针,表示任务创建失败,否则任务创建成功

CubemxTaskHandle = osThreadNew(StartCubemxTask, NULL, &CubemxTask_attributes);
if(CubemxTaskHandle == NULL)
{
	printf("StartCubemxTask creation failed\r\n");
}

在这里插入图片描述

为什么任务都要求是死循环

(1)不知道是否有小白存在疑惑,为什么我们创建的任务都是需要死循环?详细信息可以韦东山:FreeRTOS入门与工程实践 --由浅入深带你学习FreeRTOS的[5-5-3]空闲任务 章节的第1分钟开始有讲解。
(2)如果不愿意看的同学,我这里也精简一下里面的内容。如果一个任务不是死循环并且没有经过特殊处理,那么他将会返回到prvTaskExitError()函数中,然后调用portDISABLE_INTERRUPTS()函数,关闭所有的中断,并且进入死循环。这样最终所有的任务将无法执行。
(3)进入prvTaskExitError()函数之后,会触发configASSERT()断言,之后我们就可以根据这个断言捕获错误。

在这里插入图片描述

堆栈部分(新手小白建议简单了解,后续再深入)

(1)强调!强调!强调!这部分需要一定的汇编基础,数据结构的链表知识。新手小白大概率听不懂,有个粗浅了解即可!这里是入门,别想着一口吃成胖子。
(2)本来打算写一篇关于任务栈确定的方法的,后面发现这玩意写的话太麻烦了。一般来说,RTOS都会有工具能够让你检测堆栈大小的。
<1>B站:[嵌入式]如何确定RTOS任务堆栈大小
<2>C站:【RTOS 进阶修炼】如何设定 RTOS 中的任务栈(线程栈)大小
(3)如果硬要学习这部分内容比较升入,可以看韦神的课程:FreeRTOS入门与工程实践 --由浅入深带你学习FreeRTOS如下框选部分,讲到很清晰,如果这个视频都看不懂,我并不认为我写的博客能够比这个更加清晰,那样浪费所有人时间。。。
(3)在讲解堆部分的时候,因为像韦神这种级别的大佬,思维都是非常跳跃的,很多人听的云里雾里。感兴趣的同学可以看看许佬Pooled Allocation(池式分配)实例——Keil 内存管理这篇博客来学习,讲解的也很清晰,只是排版有点点丑,哭笑。
(本来我打算根据许佬这篇博客写一篇自己的理解,然后拖欠了快半年了,哈哈哈哈。之后如果写了自己的,会在这篇博客中调整)

在这里插入图片描述

(4)关于韦神在讲解堆的申请释放过程中,这个108和58似乎没有讲解,我在此解释一下,这个和数据结构的链表有关。在这个链表头部有两个指针,有C语言基础的同学都知道,指针的大小都是4字节(事前说明,是32位机器)。而头有两个指针,所以头为8字节。因此申请100字节,最终消耗108字节数据。
(5)需要注意,韦神讲解的那种堆管理办法存在内存碎片问题各位新手小白,如果看不懂可直接跳过,不要折磨自己。

在这里插入图片描述

cmsis_os2.c文件作用

(1)有些同学,学习完上面的的内容之后,很可能存在一些疑惑。我们不都是学的FreeRTOS吗?为什么还有一个STM32CubeMX端操作和keil端操作?这个就涉及到 cmsis_os2.c文件了。

为什么要做RTOS抽象

(1)在嵌入式开发中存在很多RTOS,最常见的有FreeRTOSRT-Thread。不同的RTOS,创建任务的函数不一样,这样就容易面临多个问题。
<1>如果公司在开发一个长期项目的时候,开发几年之后发现当前的RTOS并不满足项目需求,需要更改其他的RTOS。难道我们整个开发团队利用新的RTOS重新编写已经写好的项目代码吗?很显然,这是非常耽误时间的。
<2>市面上所有的RTOS都大差不差。但是,希望各位注意我的措辞,是大差不差,而非一模一样。那么就有一个问题,如果我的应用程序空间中的RTOS出现问题,开发人员流失导致团队没有特定的RTOS开发经验的工程师。这样又需要团队重新学习特定的RTOS,然后定位问题,非常消耗人力成本和时间成本。
(2)为了预防上述问题,ST做了RTOS抽象层也就是 cmsis_os2.c文件。这样你开发STM32的时候,只需要调用ST官方提高的OS函数,就可以使用任意的RTOS了。很明显,STM32CubeMX下一目标应该是增加RTOS的支持,利用这些特性卷死其他芯片厂家。

RTOS抽象就绝对的好吗

(1)听了上面的那番言论,肯定有同学就说,好啊!有了RTOS抽象,那么我学会了一个RTOS,相当于学会了所有的RTOS!我真的牛逼啊!
(2)非常抱歉的是,RTOS抽象也对带来很多不利因素,关于RTOS层抽象在embedded论坛也引起的非常多的争论。以下是我找到的一点点资料,用于各位简单了解:
<1>大多数RTOS抽象层都是为了满足最低标准功能而编写的。而每个 RTOS 都具有独特的功能,旨在专门应对特定细分市场的挑战。如果做RTOS抽象层并且只想遵守它提供的功能,您可能会失去 RTOS 的一些功能。
<2>抽象层会导致系统性能下降,因为这样增加了函数调用。
<3>调BUG的难度提高。
<4>RTOS的抽象可能会存在潜在错误,这样会导致一些奇怪的问题,也给了黑客可乘之机。

参考

(1)利用STM32CubeMX和keil模拟器,3天入门FreeRTOS(0) —— 创建工程
(2)RTOS abstractions are wrong!
(3)Should you abstract your RTOS?
(4)FreeRTOS 任务函数里面的死循环
(5)韦东山:FreeRTOS入门与工程实践 --由浅入深带你学习FreeRTOS
(6)B站:[嵌入式]如何确定RTOS任务堆栈大小
(7) 【RTOS 进阶修炼】如何设定 RTOS 中的任务栈(线程栈)大小

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