FreeRTOS入门学习日志(1)

本文作者在学习STM32标准库后,为深入学习Linux,选择从FreeRTOS入手。介绍了CubeMX的简单使用,对比HAL库与标准库,阐述FreeRTOS的多任务管理功能,还讲解了任务的创建与删除、汇编和ARM架构基础、优先级与tick概念以及状态机等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在学习STM32标准库的基本操作之后,为了后续Linux的深入学习,我选择先从FreeRTOS入手,学习简单的操作系统原理。

下面是我选择观看的视频教程(该文章大量引用了此视频的内容),该教程使用的是STM32cubeMX辅助开发FreeRTOS。我的计划是先学会FreeRTOS的应用,再去学习FreeRTOS的源码

FreeRTOS入门与工程实践 --由浅入深带你学习FreeRTOS(FreeRTOS教程 基于STM32,以实际项目为导向)_哔哩哔哩_bilibili

声明:本视频可能存在许多错误的地方,欢迎大家指正

一、学习CubeMX的简单使用

CubeMX为stm32的开发提供了很多的便利,但这个软件用的是HAL库开发,与我之前用的标准库开发有一些不同。在阅览一段时间的HAL代码之后,我们还是很容易发现其与标准库的许多相同之处。总的来说,只要学习过标准库,我们再花费一点时间,还是容易读懂HAL库的

我下面举个例子,大家简单的对比一下就会明白

1.HAL库与标准库


/*标准库初始化 PA1 PA2 */
void LED_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//开启RCC时钟
	
	GPIO_InitTypeDef GPIO_InitStructure;//结构体
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;//模式选择
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2;//引脚选择
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //速度选择
	GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化引脚
	
	GPIO_SetBits(GPIOA,GPIO_Pin_1 | GPIO_Pin_2);//引脚置低电平
}

/*HAL库初始化 */
int Led_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};//结构体
    
    /* GPIO Ports Clock Enable */
    __HAL_RCC_GPIOC_CLK_ENABLE();//RCC时钟配置

    /*Configure GPIO pin Output Level */
    HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);//引脚置低电平
		
    /*Configure GPIO pin : PC13 */
    GPIO_InitStruct.Pin = GPIO_PIN_13;//引脚选择
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;//引脚模式
    GPIO_InitStruct.Pull = GPIO_NOPULL;//是否上下拉
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;//速度选择
    HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);//初始化引脚
   
    return 0;
}

2.CubeMX配置

a.创建新工程

b.芯片选择

c.初始化系统时钟(该教程用的TIM4),RCC时钟(内部高速晶振),时钟树

d.FREERTOS配置

e.工程配置(名称,路径,编译软件)

f.简单认识一下GPIO初始化(熟悉软件的使用)

 

对于这部分配置的话我推荐大家回去复习一下STM32的时钟树

【STM32】超清晰STM32时钟树动画讲解_哔哩哔哩_bilibili

二、FreeRTOS可以干什么

接下来我们先了解一下FreeRTOS可以干什么(最简单的应用)

其实就是一件事多任务管理与运行(多线程)

我们知道MCU都是单核的,在我们之前学习的过程中,它一次只能运行一个程序,要想让他执行几个程序一般就是加个外部中断或者定时中断,但这并不是真正的多线程运行。这样子看起来FreeRTOS的牛逼之处就体现出来了,比如FreeRTOS能够同时运行两个while函数,让两个LED灯以不同的频率同时闪烁(虽然我们也可以直接修改代码,但是那样子也太麻烦了,且可读性不强)

这里再推荐大家观看一个视频

单片机也能跑多线程?5分钟带你入门FreeRTOS_哔哩哔哩_bilibili

三、任务的创建与删除

CubeMX生成出来的程序我们用Keil将其打开可以发现里面有个freertos.c的文件,这就是Freertos执行的代码,而main函数在这里只执行一些初始化操作

我们可以看一下main

int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_I2C1_Init();
  /* USER CODE BEGIN 2 */

  /* USER CODE END 2 */

  /* Init scheduler */
  osKernelInitialize();  /* Call init function for freertos objects (in freertos.c) */
  MX_FREERTOS_Init();
  /* Start scheduler */
  osKernelStart();

  /* We should never get here as control is now taken by the scheduler */
  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

那在FreeRTOS中如何让程序跑起来呢?这里我们要引入一个新概念“任务”,任务其实就是一个函数程序。

比如初始化时,软件就帮我们生成的一个默认任务程序,我们在里面写一些代码就能让Freertos跑起来

void StartDefaultTask(void *argument)
{
  /* USER CODE BEGIN StartDefaultTask */
  /* Infinite loop */

  for(;;)
  {

  }
  /* USER CODE END StartDefaultTask */
}

既然知道了任务就是一个函数,那么我们创建一个函数之后怎么才能使其运行呢

FreeRTOS提供了两个创建任务的函数(里面涉及到堆和栈,建议大家看视频学习一下,另外的还需要学习链表的知识)

1.基本方法

一个是动态内存分配:

BaseType_t xTaskCreate( 
TaskFunction_t pxTaskCode, // 函数指针, 任务函数
 const char * const pcName, // 任务的名字
 const configSTACK_DEPTH_TYPE usStackDepth, // 栈大小,单位为word,10表示40字节
 void * const pvParameters, // 调用任务函数时传入的参数
 UBaseType_t uxPriority, // 优先级
 TaskHandle_t * const pxCreatedTask ); // 任务句柄, 以后使用它来操作这个任务

另一个是静态内存分配:

TaskHandle_t xTaskCreateStatic ( 
 TaskFunction_t pxTaskCode, // 函数指针, 任务函数
 const char * const pcName, // 任务的名字
 const uint32_t ulStackDepth, // 栈大小,单位为word,10表示40字节
 void * const pvParameters, // 调用任务函数时传入的参数
 UBaseType_t uxPriority, // 优先级
 StackType_t * const puxStackBuffer, // 静态分配的栈,就是一个buffer
 StaticTask_t * const pxTaskBuffer // 静态分配的任务结构体的指针,用它来操作这个任务
);

例如:

TaskHandle_t xInLEDHandle;
BaseType_t ret;

static StackType_t g_pucStackExternLEDtask[128];
static StaticTask_t g_TCBExternLED;
static TaskHandle_t xExternLEDHandle;

void MX_FREERTOS_Init(void) {
    ret=xTaskCreate(Led_Test,"LEDtask",128,NULL,osPriorityNormal,&xInLEDHandle);/*函数名,任务名,栈大小,函数变量的参数,优先级,任务句柄*/
xExternLEDHandle=xTaskCreateStatic(Led_Extern_Test,"InLEDtask",128,NULL,osPriorityNormal,g_pucStackExternLEDtask,&g_TCBExternLED);
}

其中Led_Test和Led_Extern_Test是已经写好的函数

2.创建结构体,以快速配置(多个任务可调用同个函数)

static StackType_t g_pucStackOLEDtask[128];
static StaticTask_t g_TCBOLED;
static TaskHandle_t xOLEDHandle;

struct TaskPrintInfo
{
	uint8_t x;
	uint8_t y;
	char name[16];
};



static struct TaskPrintInfo g_Task1Info={0,0,"Task1"};
static struct TaskPrintInfo g_Task2Info={0,3,"Task2"};
static struct TaskPrintInfo g_Task3Info={0,6,"Task3"};
static int g_LCDCanUse = 1; //用于保护lcd的全局变量


void LcdPrintTask(void *params)
{
	struct TaskPrintInfo *pInfo = params;
	uint32_t count=0;
	int len;
	
	while(1)
	{
		if(g_LCDCanUse)
		{
			g_LCDCanUse = 0;
			len  = LCD_PrintString(pInfo->x,pInfo->y,pInfo->name);
			len += LCD_PrintString(len,pInfo->y,":");
			LCD_PrintSignedVal(len,pInfo->y,count++);
			g_LCDCanUse = 1; 
		}
		mdelay(500);
	}
}

void MX_FREERTOS_Init(void) {
	
	
	/*使用同个函数创建不同的任务*/
	xTaskCreate(LcdPrintTask,"task1",128,&g_Task1Info,osPriorityNormal,NULL);
	xTaskCreate(LcdPrintTask,"task2",128,&g_Task2Info,osPriorityNormal,NULL);
	xTaskCreate(LcdPrintTask,"task3",128,&g_Task3Info,osPriorityNormal,NULL);
	
	

}

     

FreeRTOS执行三个任务

但是多个任务同时调度一个函数是可能发生冲突的,这个冲突的解决方法等以后再说

3.删除任务

任务删除的函数只有一个

void vTaskDelete( TaskHandle_t xTaskToDelete )

任务删除的方式正常有两种,一种是自杀(本任务删除本任务,Handle写NULL),一种是他杀(一个任务被另一个任务删除Handle写要删除的任务的Handle)

他杀(其中xHandleOLED是另一个任务的Handle):

void LED()
{
    vTaskDelete(xHandleOLED);
}

自杀:

void LED()
{
    vTaskDelete(NULL);
}

四、汇编基础

因为FreeRTOS的内部机制会涉及到少数汇编知识,所以在学习FreeRTOS最好补一下汇编的几个基础代码,了解C语言的本质

五、ARM架构基础

主要是了解一下芯片是如何进行程序运算的,一句简单的C语言代码实际上是由几句汇编代码组成的,而汇编再转换为机器语言(二进制)即可实现数据运算

六、优先级与tick

1.优先级

我们可以在上面的创建任务里发现有一个优先级的参数,这个其实和NVIC的优先级配置差不多,就是当优先级高和优先级低的任务冲突的时候,高优先级的任务先执行,低优先级的任务后执行,同优先级的任务轮流执行(可以看一下上面的视频“FreeRTOS执行三个任务”)

但是在我们使用优先级时就会发现一个问题(使用代码如下)我们会发现执行仅停滞于优先级最高的任务

static StackType_t g_pucStackOLEDtask[128];
static StaticTask_t g_TCBOLED;
static TaskHandle_t xOLEDHandle;

struct TaskPrintInfo
{
	uint8_t x;
	uint8_t y;
	char name[16];
};



static struct TaskPrintInfo g_Task1Info={0,0,"Task1"};
static struct TaskPrintInfo g_Task2Info={0,3,"Task2"};
static struct TaskPrintInfo g_Task3Info={0,6,"Task3"};
static int g_LCDCanUse = 1; //用于保护lcd的全局变量


void LcdPrintTask(void *params)
{
	struct TaskPrintInfo *pInfo = params;
	uint32_t count=0;
	int len;
	
	while(1)
	{
		if(g_LCDCanUse)
		{
			g_LCDCanUse = 0;
			len  = LCD_PrintString(pInfo->x,pInfo->y,pInfo->name);
			len += LCD_PrintString(len,pInfo->y,":");
			LCD_PrintSignedVal(len,pInfo->y,count++);
			g_LCDCanUse = 1; 
		}
		mdelay(500);
	}
}

void MX_FREERTOS_Init(void) {
	
	
	/*使用同个函数创建不同的任务*/
	xTaskCreate(LcdPrintTask,"task1",128,&g_Task1Info,osPriorityNormal+1,NULL);
	xTaskCreate(LcdPrintTask,"task2",128,&g_Task2Info,osPriorityNormal,NULL);
	xTaskCreate(LcdPrintTask,"task3",128,&g_Task3Info,osPriorityNormal,NULL);
	
	

}

FreeRTOS任务停滞

那这个问题的本质就是CPU被占用导致任务无法被切换(最主要的就是Delay对CPU资源的占用),所以到这里我们就要引入FreeRTOS自带的Delay函数(Untial函数暂不赘述),该函数在执行时可以跳出本任务,去执行另外的任务

void vTaskDelay( const TickType_t xTicksToDelay );
void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement );

只要我们把之前代码的delay函数修改为vTaskDelay就可以多任务运行,只是“task1”会优先运行

		if(g_LCDCanUse)
		{
			g_LCDCanUse = 0;
			len  = LCD_PrintString(pInfo->x,pInfo->y,pInfo->name);
			len += LCD_PrintString(len,pInfo->y,":");
			LCD_PrintSignedVal(len,pInfo->y,count++);
			g_LCDCanUse = 1; 
		}
		vTaskDelay(500);

2.Tick

我们知道了任务会按优先级切换执行,那任务会在什么时候进行切换,运行多长时间就会切换呢?

那就需要引入FreeRTOS里面Tick的概念了,这其实和我们之前学习的Tick很像,也是用定时器产生中断,每次中断就会产生任务切换,该切换周期由configTICK_RATE_HZ 决定,如果configTICK_RATE_HZ为100,那么任务切换时间就是10ms(我们也可以在CubeMX发现这一配置)

上面提到的vTaskDelay其实就是等待多少个Tick,在等待过程中,可以执行其他任务

那vTaskDelayUntil其实就是把任务执行时间设置为一个更精确的时间(但是我们要注意,vTaskDelayUntil和vTaskDelay都会产生一点小误差,这是无法避免的)

七、状态机

FreeRTOS中的状态机和江科大教的串口通讯状态机有些类似

我们可以简单对比一下

FreeRTOS任务创建之后就会处于就绪态,调度的时候会处于运行态,在我们使用vTaskDelay之后任务就会处于阻塞态,此时其他任务可以运行,或者我们直接将任务设置在暂停(挂起)态(可以自己进入暂停态,但是需要别的任务将其唤醒,本任务无法唤醒本任务)。学会暂停时候我们就可以不再一味的删除、创建任务,并且可以保留任务的运行状态,也可给别的任务提供更充裕的运行时间

具体的函数可以参考图片或者韦东山老师的视频教程

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值