(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,那么你铁定懵逼。
(1)按照下图进行配置,很简单对吧。肯定有新手会想知道里面的参数都有什么作用。不要着急,下图中的介绍可以先不看。咱们先配置,让他跑起来,后面再讲理论。
(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
电平反转,一个任务进行串口打印。
(1)打开串口1的异步通知。
(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_StartDefaultTask
、Header_StartCubemxTask
、application 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 */
(1)打开微库。
(2)配置模拟器
DARMSTM.DLL
pSTM32F103C8
(1)打开调试界面
(2)选择逻辑分析仪,检测
PC13
引脚。为什么下面输入的是PORTC.13
,原因很简单,格式为PORTx.y
,x
表示端口,y
表示具有引脚数值,注意’.
'必须是英文的!
(1)如下图
(1)我们能够发现波形虚拟示波器上出现方波,同时虚拟串口也有数据打印。
(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_V1
和CMSIS_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 // 任务句柄, 以后使用它来操作这个任务
);
(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)需要注意,韦神讲解的那种堆管理办法存在内存碎片问题各位新手小白,如果看不懂可直接跳过,不要折磨自己。
(1)有些同学,学习完上面的的内容之后,很可能存在一些疑惑。我们不都是学的
FreeRTOS
吗?为什么还有一个STM32CubeMX
端操作和keil
端操作?这个就涉及到cmsis_os2.c
文件了。
(1)在嵌入式开发中存在很多
RTOS
,最常见的有FreeRTOS
,RT-Thread
。不同的RTOS
,创建任务的函数不一样,这样就容易面临多个问题。
<1>如果公司在开发一个长期项目的时候,开发几年之后发现当前的RTOS
并不满足项目需求,需要更改其他的RTOS
。难道我们整个开发团队利用新的RTOS重新编写已经写好的项目代码吗?很显然,这是非常耽误时间的。
<2>市面上所有的RTOS
都大差不差。但是,希望各位注意我的措辞,是大差不差,而非一模一样。那么就有一个问题,如果我的应用程序空间中的RTOS
出现问题,开发人员流失导致团队没有特定的RTOS
开发经验的工程师。这样又需要团队重新学习特定的RTOS,然后定位问题,非常消耗人力成本和时间成本。
(2)为了预防上述问题,ST
做了RTOS
抽象层也就是cmsis_os2.c
文件。这样你开发STM32
的时候,只需要调用ST
官方提高的OS
函数,就可以使用任意的RTOS
了。很明显,STM32CubeMX
下一目标应该是增加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 中的任务栈(线程栈)大小