STM32中的FreeRTOS-#1(入门)

写在前面:我一直觉得,如果我能把一点知识说给别人听,并且别人能听懂,大概率我自己真的学会了。记录的过程也是自己梳理的过程,本系列我把它称为“教程”,是想把它写得系统且有条理,本质上更多地是作为自己的学习心得和知识梳理。
本教程默认读者已有一定的STM32编程基础,并且已经熟悉CubeMX的使用,部分操作细节仅做文字提示或略过。


【欢迎关注我的个人公众号“早点儿毕业”,我会在公众号同步发布教程(笔记)内容】


本教程开发环境如下:

  • 软件:MDK Keil,CubeMX(V6.1.2),VSCode(仅作为代码编辑器)
  • 硬件:STM32F4VET6开发板(其他开发板也可以,原理相同)

RTOS(Real Time Operating System,实时操作系统),顾名思义,能够像操作系统(例如Windows)一样处理任务。操作系统的主要目的是“同时”处理多任务,这在传统的“main-while(1){…}”编程中是不能实现的。

本教程是一系列教程中的第一篇,主要包括以下内容:

(1)使用CubeMX配置Free RTOS;

(2)使用Free RTOS的优势;

(3)用“使用CubeMX”或“不使用CubeMX”两种方式,创建任务

(4)使用任务优先级解决一些常见问题。

现在开始配置CubeMX:

配置CubeMX

选择目标芯片型号后,CubeMX将为您打开默认页面。现在选择FREERTOS并按照下面的截图操作
STM32中的FreeRTOS-#1(入门)_第1张图片
这里我们选择CMSIS_V1,因为大多数的STM32芯片型号都支持这个版本。

接下来,转到任务和队列选项卡(tasks and queues),在这里您将看到软件已经自动创建了一个默认任务。双击默认任务,可以看到以下信息:
STM32中的FreeRTOS-#1(入门)_第2张图片
可以看到,一个任务包含很多设置项,但是不要被吓倒。这节教程,我们只关注TaskName(任务名称)、priority(优先级)和Entry Function(入口函数)。

现在,我们将在这里创建一个任务,下面是该任务的属性
STM32中的FreeRTOS-#1(入门)_第3张图片
任务名称默认即可,设置为普通优先级,入口函数也保持默认。一旦我们编写了程序,你就会对这些设置有更深刻的理解。

**注意:**一旦使用实时操作系统,我们不能使用systick作为HAL库代码的时间基(因为被操作系统占用了)。因此,转到SYS,并选择如下所示的TIM1作为时间基准
STM32中的FreeRTOS-#1(入门)_第4张图片
除此之外,我使用UART-1来传输数据,并将PA6和PA7作为推挽输出引脚(这两个引脚是教程演示开发板板载LED-0和LED-1所在引脚,请根据自己的开发板LED所在引脚设置)。代码生成后,打开main.c文件,现在是时候了解使用RTOS的重要性了。

使用RTOS的优势

一般学习单片机编程的第一个例子就是控制LED闪烁。现在假设有这样一个场景,LED-0闪烁周期是100ms,LED-1闪烁周期是300ms,这个编程可以在While循环里面做,并且比较容易,因为两个周期是整数倍关系。但是假如我们的需求更改为:LED-0闪烁周期是100ms,LED-1一般状态熄灭,当有按键按下时,LED1亮,延时5秒后LED1熄灭。在此期间,串口每隔1秒输出当前LED-1的亮/灭状态。这时候,情形就变得复杂起来。想完美完成需求,还是得花费点时间和精力安排代码。为了使CPU资源调度更简单,我们有必要学习使用实时操作系统。

在本节教程中,我们先不涉及上述“复杂”的需求。通过上面的步骤,我们已经创建了两个任务,下面我们以defaultTask为例,看一下CubeMX是如何定义一个任务的。

// 引用头文件
/* Includes ------------------------------------------------------------------*/
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "cmsis_os.h"

// 定义任务句柄
osThreadId defaultTaskHandle;

// 任务函数的原型
void StartdefaultTask(void const * argument);

// FREERTOS初始化函数
void MX_FREERTOS_Init(void)
{
  /*......其他代码......*/
    
  // 创建任务
  /* Create the thread(s) */
  /* definition and creation of defaultTask */
  osThreadDef(defaultTask, StartdefaultTask, osPriorityNormal, 0, 128);
  defaultTaskHandle = osThreadCreate(osThread(defaultTask), NULL);

  /*......其他代码......*/
}

/* USER CODE END Header_StartdefaultTask */
void StartdefaultTask(void const * argument)
{
  /* USER CODE BEGIN StartdefaultTask */
  /* Infinite loop */
  for(;;)
  {
    osDelay(1);
  }
  /* USER CODE END StartdefaultTask */
}

可见,定义一个任务需要完成以下几件事

  • 引入头文件
  • 定义任务句柄
  • 定义任务函数,与其它函数一样,使用前得在文件前面写上函数原型
  • 在FreeRTOS初始化函数中,把任务句柄和任务函数绑定起来,以后利用任务句柄即可操作任务

在任务函数里面添加控制LED闪烁的代码,为了测试任务调度性能,我们把闪烁周期定为1ms

void StartdefaultTask(void const * argument)
{
  /* USER CODE BEGIN 5 */
  /* Infinite loop */
  for(;;)
  {
      HAL_GPIO_TogglePin(BSP_LED0_GPIO_Port,BSP_LED0_Pin);
      osDelay(1);
  }
  /* USER CODE END 5 */ 
}

/* USER CODE BEGIN Header_Task2_init */
/**
* @brief Function implementing the Task2 thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_Task2_init */
void StartTask02(void const * argument)
{
  /* USER CODE BEGIN Task2_init */
  /* Infinite loop */
  for(;;)
  {
	  HAL_GPIO_TogglePin(BSP_LED1_GPIO_Port,BSP_LED1_Pin);
	  osDelay(1);
  }
  /* USER CODE END Task2_init */
}

我们将在defaultTask中翻转引脚LED-0(PA6)的电平,并在Task2中翻转引脚LED-1(PA7)的电平。这样,RTOS调度器将为这两个任务调度时间,以便它们有足够的时间执行。当上述代码被执行时,波器波形如下图所示(黄色-PA6,绿色PA7)
STM32中的FreeRTOS-#1(入门)_第5张图片
STM32中的FreeRTOS-#1(入门)_第6张图片
可以看出,两个LED的引脚几乎同时翻转,但是放大以后可以看出,上升沿还是有大约25微秒的时间差,这是因为在宏观上指令的执行是同时的,但在微观上,或者说在指令执行时间尺度上,单片机仍然是在一条一条地执行指令。系统任务调动也会消耗一定的时间,所以绝对的同时是不可能的,所以我在文章开头介绍RTOS的时候,给“同时”加了引号。

手动创建一个任务

**提示:**以下操作都在“freertos.c”文件中进行,代码的书写位置请参照CubeMX给出的代码,然后在相应代码附近的用户自定义代码区域书写,否则自己编写的代码可能会在软件再次生成代码时被清除。

CubeMX生成代码虽然很方便,但是不足之处在于缺乏灵活性。比如在CubeMX中更改已创建的任务的入口函数名称后,重新生成代码,自己在原任务函数中编写的代码会被全部清除。也就是说,任务一旦创建,入口函数名称便不便于更改(后续版本可能会优化这个问题)。

下面我们手动创建一个自定义的任务。创建一个新的Task,需要遵循一些步骤,如下所示:

(1)为任务定义一个ThreadID(线程ID,相当于人的身份证号)。一旦创建,这个变量将存储任务的唯一ID。稍后,所有操作都将需要这个ID。

/* USER CODE END Variables */
osThreadId myTask03Handle;	// 定义任务3句柄,仿照软件生成的代码去写

(2) 定义任务的入口函数。这是任务函数,以后这个任务的代码将被写在里面。注意,Free RTOS中的任务不是设计来处理任何返回值的。因此,入口函数应该里面应该包含一个无限循环(for或者while),整个程序都应该在这个循环中编写。

//  任务实现 不要忘了在前面写上本函数的声明
void StartTask03 (void const * argument)
 {
     while (1)
     {
         // do something
         osDelay (1000);  // 1 sec delay
     }
 }

(3) 在在**MX_FREERTOS_Init(void)**初始化函数中,我们需要创建任务(类似于PC编程里面的对象实例化)。

osThreadDef(myTask03, StartTask03, osPriorityBelowNormal, 0, 128);
myTask03Handle = osThreadCreate(osThread (myTask03), NULL);

在FreeRTOS原生函数中这个地方只需要一行代码,但是在CMSIS封装的库中,这里有两行代码,作用一样。细节可以查看osThreadDefosThreadCreate两个宏。

  • osThreadDef是一个宏,本质是定义一个结构体变量,并将括号内的参数,即任务的名称、入口函数、优先级、实例和堆栈大小,给这个新结构体变量赋值。
  • osThreadCreate也是一个宏,用于在定义任务后,顾名思义,给任务创建线程,并将任务的ID分配给myTask03Handle。

在FreeRTOS中处理优先级

  • 截止目前,我们知道了如何使用RTOS进行多任务处理。但随之而来的是一些问题。在上述示波器波形里面,可以看到,LED-0的引脚总是比LED-1优先翻转,那如果想让LED-1的引脚优先翻转该怎么办?
  • 再者,假设我们希望所有三个任务同时通过UART1发送一些数据该怎么做?
  • 当我们编写程序这样做时,结果将不完全相同。相反,串口发送将以这样一种方式进行: 一个任务将在1秒内发送数据,而另一个任务将在另一秒内发送数据,以此类推。
  • 当我们试图在具有相同优先级的任务之间使用共享资源时,就会发生这种情况。第二个任务必须等待第一个任务完成它的执行,然后只有控制进入它。同样地,第三个任务将等待第二个任务完成。即,任务不能相互打断,只能排队执行。

因为延时1ms对于串口输出来说时间太短,所以在开始下面的步骤前,请先把任务函数内的延时均改为1000ms,即 osDelay (1); -> osDelay (1000);

为了避免这些情况,我们对不同的任务使用不同的优先级。这意味着我们必须重新定义主要功能中的任务优先级(仅修改优先级即可)。

  /* definition and creation of defaultTask */
  osThreadDef(defaultTask, StartDefaultTask, osPriorityNormal, 0, 128);
  defaultTaskHandle = osThreadCreate(osThread(defaultTask), NULL);

  /* definition and creation of myTask02 */
  osThreadDef(myTask02, StartTask02, osPriorityAboveNormal, 0, 128);
  myTask02Handle = osThreadCreate(osThread(myTask02), NULL);

  /* USER CODE BEGIN RTOS_THREADS */
  /* add threads, ... */
  osThreadDef(myTask03, StartTask03, osPriorityBelowNormal, 0, 128);
  myTask03Handle = osThreadCreate(osThread (myTask03), NULL);
  /* USER CODE END RTOS_THREADS */

重定向printf函数,用于串口输出:

#include 	// 引用头文件
int fputc(int ch, FILE *f)
{
  HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
  return ch;
}

在各任务实现函数中,添加如下代码:

// 任务1
printf("Task1 is going");
// 任务2
printf("Task2 is going");
// 任务3
printf("Task3 is going");
  • 现在Task2的优先级最高,高于默认任务,Task3的优先级最低。
  • 当程序运行时,首先执行Task2,然后执行默认任务(任务1),最后执行Task3。
  • 这三个任务会同时发送数据。
    STM32中的FreeRTOS-#1(入门)_第7张图片
    STM32中的FreeRTOS-#1(入门)_第8张图片
    请注意串口数据的发送时间,几乎是1s发送一次数据,三个任务依次执行。
    【本节教程结束】
    欢迎关注我的个人公众号“早点儿毕业”,我会在公众号同步发布教程(笔记)内容。

你可能感兴趣的:(单片机,STM32,stm32)