FreeRtos快速入门

2023-05-16

堆和栈

  • 堆即为一块空闲的内存,从这块内存中来取出一部分用完之后再把它释放回去
//此时heap_buf就相当于一块空闲的内存
//我们只需要在它上面实现内存的分配和释放,那么它就是一个堆
char heap_buf[1024]; 

int pos = 0; //指针(指向空闲内存的位置)

//分配空间
void * my_malloc(int size)
{
	int old_pos = pos; //旧的下标
	pos += size; //下标更新
	
	return &heap_buf[old_pos]; //分配空间(返回首地址)
}

//释放分配出去的某一块内存(因为此时是没法释放的,所以就没写代码,意识一下即可)
void my_free(void * size)
{
	/*  err */
}

int main(void)
{
	int i;
	char * buf = my_malloc(100); //分配100个字节的空间
	unsigned char uch = 200;
	
	for(i=0;i<26;i++)
	{
		buf[i] = 'A' + i;
	}
}
image-20221113163859491

此时heap_buf就有数据了,并且下一次分配空间的时候,是从地址100开始分配的,因为pos指向100

  • 栈(stack)又名堆栈,它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。
void c_fun(void)
{
	;
}

void b_fun(void)
{
	;
}

int a_fun(int val)
{
	int a = 8;
	a += val;
	
	b_fun();
	c_fun();
	
	return a;
}
	
int main(void)
{
	a_fun(40);

	return 0;
}

基本知识:

  • 返回地址:可以理解为返回地址就是该函数执行结束后的下一条指令

在main函数中,a函数执行完之后将会返回到return 0中

a函数中调用函数b,当b函数执行之后将会返回到c函数调用前,c函数执行完之后返回到return a中

在c语言中,上面过程我们一目了然就知道了它返回的地址是谁,但这个返回地址保存在哪里呢?

1、返回地址保存在哪?

  • 返回地址保存在栈中
  • main在调用a_fun前会做两两件事情:
    1. 将a的返回地址(return 0的地址)保存到一个寄存器里面LR(link Register)
    2. 调用a_fun
  • 那么a_fun里面调用b_fun之前,就要先把b_fun的返回地址(c_fun)保存到LR里面,之后在调用b_fun

2.那么在a_fun里面保存的LR会不会覆盖之前LR的数据呢?如果不会,LR里面是如何处理的?

  • LR并不会被覆盖
  • 在a_fun内部会做一件事情:
    • 把LR的值(main里面的return 0的地址)存入栈中
  • 同样道理,b_fun的内部也会将LR的值(c_fun的地址)存入栈中,b_fun执行完就会开始执行c_fun
  • 当c_fun执行完,就会取出c_fun保存的LR值,并跳过去执行(return a),a函数也一样,最终跳到main函数的return 0

C函数开头:

  1. 划分栈(LR等寄存器、局部变量)
  2. 将LR等寄存器存入栈
  3. 执行代码
    • 如代码里面有a = 8的话,会先把a在栈里面划分空间,之后再把8这个值写到栈中

官方精简的第1个FreeRtos程序

  1. 下载

  2. 删减目录

    image-20221115181856738
  3. 编译、执行

  4. 添加串口打印功能

    • 去掉无关的代码:如LCD等
    • 增加串口打印功能
      • 初始化串口
      • 实现fputc

serial.c

#include "FreeRTOS.h"
#include "queue.h"
#include "semphr.h"
#include <stdio.h>

/* Library includes. */
#include "stm32f10x_lib.h"

/* Demo application includes. */
#include "serial.h"

/*-----------------------------------------------------------*/

/*-----------------------------------------------------------*/

/*
 * 串口初始化函数	
 */
void SerialPortInit(void)
{
	USART_InitTypeDef USART_InitStructure;
	GPIO_InitTypeDef GPIO_InitStructure;

	/* Enable USART1 clock */
	RCC_APB2PeriphClockCmd( RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE );	

	/* Configure USART1 Rx (PA10) as input floating */
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	GPIO_Init( GPIOA, &GPIO_InitStructure );
	
	/* Configure USART1 Tx (PA9) as alternate function push-pull */
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_Init( GPIOA, &GPIO_InitStructure );

	USART_InitStructure.USART_BaudRate = 115200;
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;	
	USART_InitStructure.USART_StopBits = USART_StopBits_1;
	USART_InitStructure.USART_Parity = USART_Parity_No ;
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
	USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
	USART_InitStructure.USART_Clock = USART_Clock_Disable;
	USART_InitStructure.USART_CPOL = USART_CPOL_Low;
	USART_InitStructure.USART_CPHA = USART_CPHA_2Edge;
	USART_InitStructure.USART_LastBit = USART_LastBit_Disable;
	
	USART_Init( USART1, &USART_InitStructure );
	
	USART_Cmd( USART1, ENABLE );		
}
/*
* 实现fputc
*/  
int fputc( int ch, FILE *f )
{
	USART_TypeDef * USARTx = USART1;
	
	//等待数据全部发送出去
	while ( (USARTx->SR & (1<<7)) == 0); //如果状态寄存器的第7位(TXE)不等于1,则继续等待
	
	USARTx->DR = ch; //往DR寄存器里写入数据
	
	return ch;
}

main.c

int main( void )
{
#ifdef DEBUG
  debug();
#endif

	prvSetupHardware();

	printf("Hello World\r\n");
	
	/* Start the scheduler. */
	vTaskStartScheduler();

	/* Will only get here if there was not enough heap space to create the
	idle task. */
	return 0;
}

static void prvSetupHardware( void )
{
	/* Start with the clocks in their expected state. */
	RCC_DeInit();

	/* Enable HSE (high speed external clock). */
	RCC_HSEConfig( RCC_HSE_ON );

	/* Wait till HSE is ready. */
	while( RCC_GetFlagStatus( RCC_FLAG_HSERDY ) == RESET )
	{
	}
	

	/* 2 wait states required on the flash. */
	*( ( unsigned long * ) 0x40022000 ) = 0x02;

	/* HCLK = SYSCLK */
	RCC_HCLKConfig( RCC_SYSCLK_Div1 );

	/* PCLK2 = HCLK */
	RCC_PCLK2Config( RCC_HCLK_Div1 );

	/* PCLK1 = HCLK/2 */
	RCC_PCLK1Config( RCC_HCLK_Div2 );

	/* PLLCLK = 8MHz * 9 = 72 MHz. */
	RCC_PLLConfig( RCC_PLLSource_HSE_Div1, RCC_PLLMul_9 );

	/* Enable PLL. */
	RCC_PLLCmd( ENABLE );

	/* Wait till PLL is ready. */
	while(RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET)
	{
	}

	/* Select PLL as system clock source. */
	RCC_SYSCLKConfig( RCC_SYSCLKSource_PLLCLK );

	/* Wait till PLL is used as system clock source. */
	while( RCC_GetSYSCLKSource() != 0x08 )
	{
	}

	/* Enable GPIOA, GPIOB, GPIOC, GPIOD, GPIOE and AFIO clocks */
	RCC_APB2PeriphClockCmd(	RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB |RCC_APB2Periph_GPIOC
							| RCC_APB2Periph_GPIOD | RCC_APB2Periph_GPIOE | RCC_APB2Periph_AFIO, ENABLE );

	/* SPI2 Periph clock enable */
	RCC_APB1PeriphClockCmd( RCC_APB1Periph_SPI2, ENABLE );


	/* Set the Vector Table base address at 0x08000000 */
	NVIC_SetVectorTable( NVIC_VectTab_FLASH, 0x0 );

	NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );

	/* Configure HCLK clock as SysTick clock source. */
	SysTick_CLKSourceConfig( SysTick_CLKSource_HCLK );
	
	SerialPortInit(); //初始化串口1 
}

第1个FreeRtos程序及引申

1.创建两个打印任务

  • FreeRtos是多任务系统,这个多任务在我们人类感觉上来说是同时执行的,但其实是交叉执行的
void task1Function(void * Parameters)
{
	while(1)	
	{
		printf("1");
	}
}	

void task2Function(void * Parameters)
{
	while(1)	
	{
		printf("2");
	}
}

/*-----------------------------------------------------------*/

int main( void )
{
	//句柄(可以理解为一个任务的唯一标识)
	TaskHandle_t xTaskHandle1;  //创建任务1的句柄
#ifdef DEBUG
  debug();
#endif

	prvSetupHardware();

	printf("Hello World\r\n");
	
	/*
	*	pxTaskCode:函数
	*	pcName:任务名字
	*	usStackDepth:栈深度
	* pvParameters:参数(给pxTaskCode函数用的)
	* uxPriority:优先级
	* pxCreatedTask:句柄
	*/
	//xTaskCreate(pxTaskCode,pcName,usStackDepth,pvParameters,uxPriority,pxCreatedTask);
	
	//倘若任务1和任务2是在同一个串口输出的,那么就会交叉使用串口
	//下面结果为:222222222222111111111112222222222221111111111122222222222211111111111
	xTaskCreate(task1Function,"Task1",100,NULL,1,&xTaskHandle1); //创建任务1
	xTaskCreate(task2Function,"Task2",100,NULL,1,NULL); //创建任务2
	
	//FreeRtos是多任务系统,这个多任务在我们人类感觉上来说,是同时执行的,但其实是交叉执行的
	//倘若任务1和任务2是不同的串口输出的,那么就会发现任务1和任务2“好像”是在同时执行的
	//再比如添加一个LED闪烁任务,那么串口输出和LED闪烁“好像是同时执行的”
	
	/* Start the scheduler. */
	vTaskStartScheduler();

	/* Will only get here if there was not enough heap space to create the
	idle task. */
	return 0;
}

2.FreeRtos源码结构

结构

以Keil工具下STM32F103芯片为例,它的FreeRTOS的目录如下:

image-20221115200626810

主要涉及2个目录:

  • Demo

    • Demo目录下是工程文件,以"芯片和编译器"组合成一个名字

    • 比如:CORTEX_STM32F103_Keil

  • Source

    • 根目录下是核心文件,这些文件是通用的
    • portable目录下是移植时需要实现的文件
      • 目录名为:[compiler]/[architecture]
      • 比如:RVDS/ARM_CM3,这表示cortexM3架构在RVDS工具上的移植文件

核心文件

FreeRTOS的最核心文件只有2个:

  • FreeRTOS/Source/tasks.c
  • FreeRTOS/Source/list.c

其他文件的作用也一起列表如下:

FreeRTOS/Source/下的文件作用
tasks.c必需,任务操作
list.c必须,列表
queue.c基本必需,提供队列操作、信号量(semaphore)操作
timer.c可选,software timer
event_groups.c可选,提供event group功能
croutine.c可选,过时了

3.编程规范

数据类型

每个移植的版本都含有自己的 portmacro.h 头文件,里面定义了2个数据类型:

  • TickType_t:
    • FreeRTOS配置了一个周期性的时钟中断:Tick Interrupt
    • 每发生一次中断,中断次数累加,这被称为tick count
    • tick count这个变量的类型就是TickType_t
    • TickType_t可以是16位的,也可以是32位的
    • FreeRTOSConfig.h中定义configUSE_16_BIT_TICKS时,TickType_t就是uint16_t
    • 否则TickType_t就是uint32_t
    • 对于32位架构,建议把TickType_t配置为uint32_t
  • BaseType_t:
    • 这是该架构最高效的数据类型
    • 32位架构中,它就是uint32_t
    • 16位架构中,它就是uint16_t
    • 8位架构中,它就是uint8_t
    • BaseType_t通常用作简单的返回值的类型,还有逻辑值,比如 pdTRUE/pdFALSE

变量名

变量名有前缀:

变量名前缀含义
cchar
sint16_t,short
lint32_t,long
xBaseType_t,其他非标准的类型:结构体、task handle、queue handle等
uunsigned
p指针
ucuint8_t,unsigned char
pcchar指针

函数名

函数名的前缀有2部分:返回值类型、在哪个文件定义

函数名前缀含义
vTaskPrioritySet返回值类型:void
在task.c中定义
xQueueReceive返回值类型:BaseType_t
在queue.c中定义
pvTimerGetTimerID返回值类型:pointer to void
在tmer.c中定义

宏的名

宏的名字是大小,可以添加小写的前缀。前缀是用来表示:宏在哪个文件中定义。

宏的前缀含义:在哪个文件里定义
port (比如portMAX_DELAY)portable.h或portmacro.h
task (比如taskENTER_CRITICAL())task.h
pd (比如pdTRUE)projdefs.h
config (比如configUSE_PREEMPTION)FreeRTOSConfig.h
err (比如errQUEUE_FULL)projdefs.h

通用的宏定义如下:

pdTRUE1
pdFALSE0
pdPASS1
pdFAIL0

创建任务详解

动态_静态创建任务

  • 对于每个任务都会有一个TCB_t结构体,对于TCB_t结构体我们可以选择动态分配或者静态分配

  • 使用xTaskCreate函数创建的任务和栈都是动态分配的

  • 一个任务可以简单的理解为是一个函数,在xTaskCreate函数中,我们还指定了的大小,因为在函数中有各种局部变量,以及各种调用,所以每一个任务的栈应该都不一样的,否则它们的栈会互相冲突,并且这个栈我们还可以静态分配

代码

void task1Function(void * Parameters)
{
	while(1)	
	{
		printf("1");
	}
}	

void task2Function(void * Parameters)
{
	while(1)	
	{
		printf("2");
	}
}

void task3Function(void * Parameters)
{
	while(1)
	{
		printf("3");
	}
}

/*-----------------------------------------------------------*/

//创建一个栈空间
StackType_t xTask3Stack[100]; //这里100的话深度(usStackDepth)也得写100

//创建TCB
StaticTask_t xTask3TCB;

//创建一个空闲任务
StackType_t xIdleTaskStack[100];
StaticTask_t xIdleTaskTCB; 

//想要调用xTaskCreateStatic的话得要实现vApplicationGetIdleTaskMemory函数
void vApplicationGetIdleTaskMemory( StaticTask_t ** ppxIdleTaskTCBBuffer,
                                               StackType_t ** ppxIdleTaskStackBuffer,
                                               uint32_t * pulIdleTaskStackSize )
{
	*ppxIdleTaskTCBBuffer = &xIdleTaskTCB;
	*ppxIdleTaskStackBuffer = xIdleTaskStack;
	*pulIdleTaskStackSize = 100;
}

int main( void )
{
	//句柄(可以理解为一个任务的唯一标识)
	TaskHandle_t xTaskHandle1;  //创建句柄
#ifdef DEBUG
  debug();
#endif

	prvSetupHardware();

	printf("Hello World\r\n");
	
	//动态创建任务
	xTaskCreate(task1Function,"Task1",100,NULL,1,&xTaskHandle1); //创建任务1
	xTaskCreate(task2Function,"Task2",100,NULL,1,NULL); //创建任务2
	
	/*
	 * 前面5个参数都跟xTaskCreate一样
	 * puxStackBuffer:传入一个栈,所谓栈就是一个空闲的内存,所以我们可以传一个数组
	*/
	
	//静态创建任务
	xTaskCreateStatic(task3Function,"Task3",100,NULL,1,xTask3Stack,&xTask3TCB);
	
	
	/* Start the scheduler. */
	vTaskStartScheduler();

	/* Will only get here if there was not enough heap space to create the
	idle task. */
	return 0;
}

运行结果

image-20221116001007984

总结

动态内存的使用

  • 怎么表示任务?
    • 以面向对象的思想:任务控制块
    • 可以动态分配
  • 多任务的核心:栈,可以动态分配

静态创建任务

  • 事先分配好任务控制块
  • 事先提供栈

进一步实验

优先级实验

高优先级的任务先执行,同优先级的任务交替执行

  • 定义任务标记用来表示任务是否运行
  • 将变量添加到逻辑分析仪中
    • 首先运行到main函数中
    • 变量名右键 ——> add ‘变量名’ to ——> logic Analyzer
    • 逻辑分析仪中右键变量名改为Bit

同优先级实验

在下面逻辑分析仪中可以看出同优先级的任务他们的交叉执行

代码
//用来标记任务是否运行
static int task1flagrun = 0;
static int task2flagrun = 0;
static int task3flagrun = 0;

/*-----------------------------------------------------------*/

void task1Function(void * Parameters)
{
	while(1)	
	{
		task1flagrun = 1;
		task2flagrun = 0;
		task3flagrun = 0;
		printf("1");
	}
}	

void task2Function(void * Parameters)
{
	while(1)	
	{
		task1flagrun = 0;
		task2flagrun = 1;
		task3flagrun = 0;
		printf("2");
	}
}

void task3Function(void * Parameters)
{
	while(1)
	{
		task1flagrun = 0;
		task2flagrun = 0;
		task3flagrun = 1;
		printf("3");
	}
}

/*-----------------------------------------------------------*/

//创建一个栈空间
StackType_t xTask3Stack[100]; //这里100的话深度(usStackDepth)也得写100

//创建TCB
StaticTask_t xTask3TCB;

//创建一个空闲任务
StackType_t xIdleTaskStack[100];
StaticTask_t xIdleTaskTCB; 

//想要调用xTaskCreateStatic的话得要实现vApplicationGetIdleTaskMemory函数
void vApplicationGetIdleTaskMemory( StaticTask_t ** ppxIdleTaskTCBBuffer,
                                               StackType_t ** ppxIdleTaskStackBuffer,
                                               uint32_t * pulIdleTaskStackSize )
{
	*ppxIdleTaskTCBBuffer = &xIdleTaskTCB;
	*ppxIdleTaskStackBuffer = xIdleTaskStack;
	*pulIdleTaskStackSize = 100;
}

int main( void )
{
	//句柄(可以理解为一个任务的唯一标识)
	TaskHandle_t xTaskHandle1;  //创建句柄
#ifdef DEBUG
  debug();
#endif

	prvSetupHardware();

	printf("Hello World\r\n");
	
	//动态创建任务
	xTaskCreate(task1Function,"Task1",100,NULL,1,&xTaskHandle1); //创建任务1
	xTaskCreate(task2Function,"Task2",100,NULL,1,NULL); //创建任务2
	
	/*
	 * 前面5个参数都跟xTaskCreate一样
	 * puxStackBuffer:传入一个栈,所谓栈就是一个空闲的内存,所以我们可以传一个数组
	*/
	
	//静态创建任务
	xTaskCreateStatic(task3Function,"Task3",100,NULL,1,xTask3Stack,&xTask3TCB);
	
	
	/* Start the scheduler. */
	vTaskStartScheduler();

	/* Will only get here if there was not enough heap space to create the
	idle task. */
	return 0;
}
运行结果
image-20221116122654915 image-20221116122707110

不同优先级实验

将上面代码中task3Function改为优先级2,其他两个任务的优先级均为1

可以看出只有taskFunction在运行

高优先级的任务先执行,如果高优先级的任务没有主动放弃执行的话,其他低优先级的任务将不能执行

image-20221116122144899 image-20221116122537750

删除任务实验

我们创建一个任务,并且传入了一个xTaskHandle1,让我们可以引用这个任务

想要删除任务,即必须要通过xTaskHandlex

vTaskDelete

  • 功能:删除xTaskCreate和xTaskCreateStatic创建的任务
  • 参数:传入一个TaskHandle_t(句柄)

代码

//句柄(可以理解为一个任务的唯一标识)
TaskHandle_t xTaskHandle1;  //创建句柄
TaskHandle_t xTaskHandle3;  //创建句柄

//用来标记任务是否运行
static int task1flagrun = 0;
static int task2flagrun = 0;
static int task3flagrun = 0;

/*-----------------------------------------------------------*/

void task1Function(void * Parameters)
{
	while(1)	
	{
		task1flagrun = 1;
		task2flagrun = 0;
		task3flagrun = 0;
		printf("1");
	}
}	

void task2Function(void * Parameters)
{
	int i;
	while(1)	
	{
		task1flagrun = 0;
		task2flagrun = 1;
		task3flagrun = 0;
		printf("2");
		
		if(i++ == 100)
		{
			vTaskDelete(xTaskHandle1); //将任务1删除
		}
		
		if(i == 200)
		{
			vTaskDelete(xTaskHandle3); //将任务3删除
		}
		
		if(i == 300)
		{
			vTaskDelete(NULL); //将任务2删除(自杀)
		}
	}
}

void task3Function(void * Parameters)
{
	while(1)
	{
		task1flagrun = 0;
		task2flagrun = 0;
		task3flagrun = 1;
		printf("3");
	}
}

/*-----------------------------------------------------------*/

//创建一个栈空间
StackType_t xTask3Stack[100]; //这里100的话深度(usStackDepth)也得写100

//创建TCB
StaticTask_t xTask3TCB;

//创建一个空闲任务
StackType_t xIdleTaskStack[100];
StaticTask_t xIdleTaskTCB; 

//想要调用xTaskCreateStatic的话得要实现vApplicationGetIdleTaskMemory函数
void vApplicationGetIdleTaskMemory( StaticTask_t ** ppxIdleTaskTCBBuffer,
                                               StackType_t ** ppxIdleTaskStackBuffer,
                                               uint32_t * pulIdleTaskStackSize )
{
	*ppxIdleTaskTCBBuffer = &xIdleTaskTCB;
	*ppxIdleTaskStackBuffer = xIdleTaskStack;
	*pulIdleTaskStackSize = 100;
}

int main( void )
{
#ifdef DEBUG
  debug();
#endif

	prvSetupHardware();

	printf("Hello World\r\n");
	
	xTaskCreate(task1Function,"Task1",100,NULL,1,&xTaskHandle1); //创建任务1
	xTaskCreate(task2Function,"Task2",100,NULL,1,NULL); //创建任务2
	
	//xTaskCreateStatic执行完之后会返回一个TaskHandle_t(句柄)类型的对象,我们可以通过这个来删除静态任务
	xTaskHandle3 = xTaskCreateStatic(task3Function,"Task3",100,NULL,1,xTask3Stack,&xTask3TCB);
	
	
	/* Start the scheduler. */
	vTaskStartScheduler();

	/* Will only get here if there was not enough heap space to create the
	idle task. */
	return 0;
}

运行结果

Hello World
3333333333331111111111122222222222233333333333111111111111222222222223333333333331111111111122222222222233333333333111111111111222222222223333333333331111111111122222222222233333333333111111111111222222222223333333333331111111111122222222222233333333333111111111111222222222223333333333331111111111122222222222233333333333222222222222333333333332222222222223333333333322222222222233333333333222222222222333333333332222222222223333333333322222222222233333333333222222222222333333333332222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222

image-20221116130748445

使用同一个任务函数创建多个任务

代码

//句柄(可以理解为一个任务的唯一标识)
TaskHandle_t xTaskHandle1;  //创建句柄
TaskHandle_t xTaskHandle3;  //创建句柄

//用来标记任务是否运行
static int task1flagrun = 0;
static int task2flagrun = 0;
static int task3flagrun = 0;

/*-----------------------------------------------------------*/

void task1Function(void * Parameters)
{
	while(1)	
	{
		task1flagrun = 1;
		task2flagrun = 0;
		task3flagrun = 0;
		printf("1");
	}
}	

void task2Function(void * Parameters)
{
	int i;
	while(1)	
	{
		task1flagrun = 0;
		task2flagrun = 1;
		task3flagrun = 0;
		printf("2");
		
		if(i++ == 100)
		{
			vTaskDelete(xTaskHandle1); //将任务1删除
		}
		
		if(i == 200)
		{
			vTaskDelete(xTaskHandle3); //将任务3删除
		}
		
		if(i == 300)
		{
			vTaskDelete(NULL); //将任务2删除(自杀)
		}
	}
}

void task3Function(void * Parameters)
{
	while(1)
	{
		task1flagrun = 0;
		task2flagrun = 0;
		task3flagrun = 1;
		printf("3");
	}
}
//创建通用函数
void taskGenericFunction(void * Para)
{
	int val = (int)Para;
	while(1)
	{
		printf("%d",val);
	}
}

/*-----------------------------------------------------------*/

//创建一个栈空间
StackType_t xTask3Stack[100]; //这里100的话深度(usStackDepth)也得写100

//创建TCB
StaticTask_t xTask3TCB;

//创建一个空闲任务
StackType_t xIdleTaskStack[100];
StaticTask_t xIdleTaskTCB; 

//想要调用xTaskCreateStatic的话得要实现vApplicationGetIdleTaskMemory函数
void vApplicationGetIdleTaskMemory( StaticTask_t ** ppxIdleTaskTCBBuffer,
                                               StackType_t ** ppxIdleTaskStackBuffer,
                                               uint32_t * pulIdleTaskStackSize )
{
	*ppxIdleTaskTCBBuffer = &xIdleTaskTCB;
	*ppxIdleTaskStackBuffer = xIdleTaskStack;
	*pulIdleTaskStackSize = 100;
}

int main( void )
{
#ifdef DEBUG
  debug();
#endif

	prvSetupHardware();

	printf("Hello World\r\n");
	
	xTaskCreate(task1Function,"Task1",100,NULL,1,&xTaskHandle1); //创建任务1
	xTaskCreate(task2Function,"Task2",100,NULL,1,NULL); //创建任务2
	
	//xTaskCreateStatic执行完之后会返回一个TaskHandle_t(句柄)类型的对象,我们可以通过这个来删除静态任务
	xTaskHandle3 = xTaskCreateStatic(task3Function,"Task3",100,NULL,1,xTask3Stack,&xTask3TCB);
	
	//使用同一个任务函数创建多个任务
	xTaskCreate(taskGenericFunction,"Task4",100,(void *)4,1,NULL);
	xTaskCreate(taskGenericFunction,"Task5",100,(void *)5,1,NULL);
	
	/* Start the scheduler. */
	vTaskStartScheduler();

	/* Will only get here if there was not enough heap space to create the
	idle task. */
	return 0;
}

运行结果

image-20221116173134749

为什么同一个函数能够创建不同的任务呢?

  • 因为它们的栈是不一样的
  • 执行任务时传入不同的参数,首先它们的参数就不一样,其次,每个任务有自己的栈,这些局部变量都保存到不同的栈中,使得它们在运行的时候互不影响

栈大小实验

栈的分配操作:(具体细节在内存管理章节)

  • 如下图,heap是一块空闲的内存
  • 假设分配了一块空间长度为len
  • 那么,在free的时候并没有传入长度,是怎么释放掉的呢?
    • free传入了buff,知道了buff的首地址
    • 在buff首地址前面有一个结构体,这个结构体至少存了buff的len信息
    • 当使用free函数来释放buff的时候,就可以反过来知道buff的长度是多少
  • 使用malloc(len)分配内存的时候,首先会分配一个头部,再去分配len字节的buff,最终返回头部后一个(Site)地址给buff
  • 当我们释放内存时,从buff地址往前推,找出头部,获得头部的长度信息,从而就可以释放掉buff
image-20221116180604948
image-20221116181324319
  • xTaskCreate(task1Function,“Task1”,100,NULL,1,&xTaskHandle1);
  • xTaskCreate创建任务时会分配一个TCB结构体,还会分配栈
  • 先分配TCB1的栈和头部
  • 之后分配Task1的栈(100 * 4)和头部
  • 同样道理,创建Tsak2时也是一样的

我们知道栈里面会保存各种寄存器和局部变量,我们知道栈是从高地址往下增长的(绿色线),当我们使用大量的局部变量之后就会破坏掉头和TCB1以及TCB1的头部,此时就会出现不可预计的后果

代码

void task1Function(void * Parameters)
{
	
	//volatile:确保本条指令不会因编译器的优化而省略,且要求每次直接读值
	 //因为创建任务1的时候传入的栈值是100( 100*4 = 400),这里故意写大一点,让buf把占空间耗尽
	volatile char buf[500];
	
	int i;
	while(1)	
	{
		task1flagrun = 1;
		task2flagrun = 0;
		task3flagrun = 0;
		printf("1");
		
		for (i=0; i<500; i++)
			buf[i] = 0; //把栈空间的值清零,此时会把TCB也破坏掉
	}
}	

void task2Function(void * Parameters)
{
	while(1)	
	{
		task1flagrun = 0;
		task2flagrun = 1;
		task3flagrun = 0;
		printf("2");
	}
}

void task3Function(void * Parameters)
{
	while(1)
	{
		task1flagrun = 0;
		task2flagrun = 0;
		task3flagrun = 1;
		printf("3");
	}
}
//创建通用函数
void taskGenericFunction(void * Para)
{
	int val = (int)Para;
	while(1)
	{
		printf("%d",val);
	}
}

/*-----------------------------------------------------------*/

//创建一个栈空间
StackType_t xTask3Stack[100]; //这里100的话深度(usStackDepth)也得写100

//创建TCB
StaticTask_t xTask3TCB;

//创建一个空闲任务
StackType_t xIdleTaskStack[100];
StaticTask_t xIdleTaskTCB; 

//想要调用xTaskCreateStatic的话得要实现vApplicationGetIdleTaskMemory函数
void vApplicationGetIdleTaskMemory( StaticTask_t ** ppxIdleTaskTCBBuffer,
                                               StackType_t ** ppxIdleTaskStackBuffer,
                                               uint32_t * pulIdleTaskStackSize )
{
	*ppxIdleTaskTCBBuffer = &xIdleTaskTCB;
	*ppxIdleTaskStackBuffer = xIdleTaskStack;
	*pulIdleTaskStackSize = 100;
}

int main( void )
{
#ifdef DEBUG
  debug();
#endif

	prvSetupHardware();

	printf("Hello World\r\n");
	
	xTaskCreate(task1Function,"Task1",100,NULL,1,&xTaskHandle1); //创建任务1
	xTaskCreate(task2Function,"Task2",100,NULL,1,NULL); //创建任务2
	
	//xTaskCreateStatic执行完之后会返回一个TaskHandle_t(句柄)类型的对象,我们可以通过这个来删除静态任务
	xTaskHandle3 = xTaskCreateStatic(task3Function,"Task3",100,NULL,1,xTask3Stack,&xTask3TCB);
	
	//使用同一个任务函数创建多个任务
	xTaskCreate(taskGenericFunction,"Task4",100,(void *)4,1,NULL);
	xTaskCreate(taskGenericFunction,"Task5",100,(void *)5,1,NULL);
	
	/* Start the scheduler. */
	vTaskStartScheduler();

	/* Will only get here if there was not enough heap space to create the
	idle task. */
	return 0;
}

运行结果

HardFault_Handler硬件错误

image-20221116182936050

image-20221116183627427

任务管理

任务状态理论

  • 任务切换的基础:tick中断
    • 定时器,每隔1毫秒产生中断
    • tick中断处理函数中会判断是否需要切换任务,若需要切换的话则会切换
    • 这个1毫秒的时间是在FreeRTOSConfig.h文件中定义的,若想要更改时间的话可以更改configTICK_RATE_HZ宏
    • image-20221116221543354
    • 在FreeRtos中我们可以指定任务的tick次数,如Task1执行10次tick,Task2执行5次Tick
  • 有哪些任务状态?状态切换图
    • Running:正在运行状态
    • Ready:准备状态(可随时运行)
    • Blocked:阻塞状态(等待某些事情发生,才可以继续运行)
    • suspended:暂停状态(主动休息 / 被命令去休息)
    • 状态转换图:
    • image-20221116224052586
  • 怎么管理不同状态的任务:放在不同链表里
  • 阻塞状态(Blocked)举例:vTaskDelay函数
  • 暂停状态(Suspended)举例:vTaskSuspend / vTaskResume

任务状态实验

代码

  • 创建三个任务,分别是:Task1、Task2、Task3
  • 让Task1调用vTaskSuspend( Task3 )函数,让Task3进入到暂停状态(Suspend)
  • 等待一段时间让Task1调用vTaskResume( Task3 )函数,让Task3从暂停状态进入准备状态(Ready)
    • 注意:Task3必须要让其他正在运行的任务来调用vTaskResume函数来解除暂停状态
  • 让Task2调用vTaskDelay函数(等待某个时间),主动进入阻塞状态(Blocked)

注意:

image-20221117220202327 image-20221117220422194
  • 这里的时钟是72MHz,并且PLLCLK9倍频,所以要把外部晶振设置为8MHz

  • 设置方法:

    • 点击魔术棒 --> Target --> 在STMicroelectronics STM32F13VB处改为8.0
    • image-20221117220955459
    //句柄(可以理解为一个任务的唯一标识)
    TaskHandle_t xTaskHandle1;  //创建句柄
    TaskHandle_t xTaskHandle3;  //创建句柄
    
    //用来标记任务是否运行
    static int task1flagrun = 0;
    static int task2flagrun = 0;
    static int task3flagrun = 0;
    
    /*-----------------------------------------------------------*/
    
    void task1Function(void * Parameters)
    {
    	//每次进入中断TickCount就会+1
    	TickType_t tStart = xTaskGetTickCount(); //记录开始时间
    	TickType_t t; 
    	int flag = 0; //标记是否进入暂停
    	
    	printf("打印了1次\r\n");
    	
    	while(1)	
    	{
    		t = xTaskGetTickCount(); //记录当前时间
    		
    		task1flagrun = 1;
    		task2flagrun = 0;
    		task3flagrun = 0;
    		printf("1");
    		
    		if( !flag && (t >= tStart + 10) ) 
    		{
    			//如果时间大于10的话,就让Task3进入暂停状态
    			vTaskSuspend(xTaskHandle3); 
    			flag = 1;
    		}
    		
    		if( t >= tStart + 20)
    		{
    			//让Task从暂停状态回到准备状态
    			vTaskResume( xTaskHandle3 ); 
    		}
    	}
    }	
    
    void task2Function(void * Parameters)
    {
    	while(1)	
    	{
    		task1flagrun = 0;
    		task2flagrun = 1;
    		task3flagrun = 0;
    		printf("2");
    	
    		//进入等待状态(Blocked) ,等待10个Tick的时间
    		vTaskDelay(10);
    		
    	}
    }
    
    void task3Function(void * Parameters)
    {
    	while(1)
    	{
    		task1flagrun = 0;
    		task2flagrun = 0;
    		task3flagrun = 1;
    		printf("3");
    	}
    }
    
    /*-----------------------------------------------------------*/
    
    //创建一个栈空间
    StackType_t xTask3Stack[100]; //这里100的话深度(usStackDepth)也得写100
    
    //创建TCB
    StaticTask_t xTask3TCB;
    
    //创建一个空闲任务
    StackType_t xIdleTaskStack[100];
    StaticTask_t xIdleTaskTCB; 
    
    //想要调用xTaskCreateStatic的话得要实现vApplicationGetIdleTaskMemory函数
    void vApplicationGetIdleTaskMemory( StaticTask_t ** ppxIdleTaskTCBBuffer,
                                                   StackType_t ** ppxIdleTaskStackBuffer,
                                                   uint32_t * pulIdleTaskStackSize )
    {
    	*ppxIdleTaskTCBBuffer = &xIdleTaskTCB;
    	*ppxIdleTaskStackBuffer = xIdleTaskStack;
    	*pulIdleTaskStackSize = 100;
    }
    
    int main( void )
    {
    #ifdef DEBUG
      debug();
    #endif
    
    	prvSetupHardware();
    
    	printf("Hello World\r\n");
    	
    	xTaskCreate(task1Function,"Task1",100,NULL,1,&xTaskHandle1); //创建任务1
    	xTaskCreate(task2Function,"Task2",100,NULL,1,NULL); //创建任务2
    	
    	//xTaskCreateStatic执行完之后会返回一个TaskHandle_t(句柄)类型的对象,我们可以通过这个来删除静态任务
    	xTaskHandle3 = xTaskCreateStatic(task3Function,"Task3",100,NULL,1,xTask3Stack,&xTask3TCB);
    	
    	/* Start the scheduler. */
    	vTaskStartScheduler(); 
    
    	/* Will only get here if there was not enough heap space to create the
    	idle task. */
    	return 0;
    }
    

运行结果

image-20221117223028403

  • 当Task1累加到10次tick的时候,下一次运行将会把Task3变为暂停状态(Suspend)

  • 对于Task2会在第一次运行的时候就进入阻塞状态(Blocked),当等待10个tick的时候重新返回准备状态(Ready)

VTaskDelay和vTaskDelayUntil

有两个Delay函数:

  • vTaskDelay:至少等待指定个数的Tick Interrupt才能变为就绪状态。
  • vTaskDelayUntil:等待到指定的绝对时刻,才能变为就绪状态。
image-20221117231124926

如图,vTaskDelay是固定等待时间 N * Tick,如前面有一个不固定时间的程序,那么t1 到 t2 的时间就不是固定的

image-20221117231758849

如图,从t1 到 t2 的时间称为△t ,当调用vTaskDelayUntil时传入t1 和 △t(终点),那么无论vTaskDelayUntil在t1的什么时候调用,t1 到 t2的时间都是不变的,变得只是阻塞时间(Blocked)

总结:

  • vTaskDelay:固定Blocked等待时间,动态Tick中断时间
  • vTaskDelayUntil:动态Blocked时间,固定Tick中断时间

代码

//句柄(可以理解为一个任务的唯一标识)
TaskHandle_t xTaskHandle1;  //创建句柄
TaskHandle_t xTaskHandle3;  //创建句柄

//用来标记任务是否运行
static int task1flagrun = 0;
static int task2flagrun = 0;
static int task3flagrun = 0;

/*-----------------------------------------------------------*/

//随机值,用来让Task1的执行时间不固定
static int radns[] = {3,53,45,110,12};

/*-----------------------------------------------------------*/

void task1Function(void * Parameters)
{
	//每次进入中断TickCount就会+1
	TickType_t tStart = xTaskGetTickCount(); //记录T1(此代码只会执行一次)
	int i,j=0;
	
	while(1)	
	{
		task1flagrun = 1;
		task2flagrun = 0;
		task3flagrun = 0;
		for(i=0; i<radns[j]; i++)
			printf("1");
		j++;
		
		if(j == 5)
			j = 0;
#if 0
		vTaskDelay(8); //阻塞(等待)8个Tick
#else
		/*
			vTaskDelayUntil( pxPreviousWakeTime, xTimeIncrement )
				1、直到*pxPr + △T时刻,该函数才会从Blocked状态退出
				2、更新*pxPr:*pxPr(之前的T) + △T
		*/
		vTaskDelayUntil(&tStart,8);
#endif
	}
}	

void task2Function(void * Parameters)
{
	while(1)	
	{
		task1flagrun = 0;
		task2flagrun = 1;
		task3flagrun = 0;
		printf("2");
	}
}

void task3Function(void * Parameters)
{
	while(1)
	{
		task1flagrun = 0;
		task2flagrun = 0;
		task3flagrun = 1;
		printf("3");
	}
}

/*-----------------------------------------------------------*/

//创建一个栈空间
StackType_t xTask3Stack[100]; //这里100的话深度(usStackDepth)也得写100

//创建TCB
StaticTask_t xTask3TCB;

//创建一个空闲任务
StackType_t xIdleTaskStack[100];
StaticTask_t xIdleTaskTCB; 

//想要调用xTaskCreateStatic的话得要实现vApplicationGetIdleTaskMemory函数
void vApplicationGetIdleTaskMemory( StaticTask_t ** ppxIdleTaskTCBBuffer,
                                               StackType_t ** ppxIdleTaskStackBuffer,
                                               uint32_t * pulIdleTaskStackSize )
{
	*ppxIdleTaskTCBBuffer = &xIdleTaskTCB;
	*ppxIdleTaskStackBuffer = xIdleTaskStack;
	*pulIdleTaskStackSize = 100;
}

int main( void )
{
#ifdef DEBUG
  debug();
#endif

	prvSetupHardware();

	printf("Hello World\r\n");
	
	//任务1的优先级最高
	xTaskCreate(task1Function,"Task1",100,NULL,2,&xTaskHandle1); //创建任务1
	xTaskCreate(task2Function,"Task2",100,NULL,1,NULL); //创建任务2
	
	xTaskHandle3 = xTaskCreateStatic(task3Function,"Task3",100,NULL,1,xTask3Stack,&xTask3TCB);
	
	/* Start the scheduler. */
	vTaskStartScheduler(); 

	/* Will only get here if there was not enough heap space to create the
	idle task. */
	return 0;
}

运行结果

当调用vTaskDelay函数时:

image-20221117233900444
  • 可以看到Task1的高电平时间(运行时间)是不一样的,但是休眠时间是一样的

当调用vTaskDelayUntil函数时:

image-20221117235709559
  • 此时t1在1.04ms时刻调用 --> t2:6.38 --> t3:11.71
  • 从t1到t2,t2到t3的的间隔时间都差不多为5.34

空闲任务

  • 删除任务后的清理工作,是在空闲任务(Idle,优先级为0)中完成的,比如释放任务的内存
  • 空闲任务何时才能执行?
  • 空闲任务只能处于这2个状态之一:Running、Ready

实验一

代码

  • 在main函数里面创建Task1
  • 在Task1里面创建Task2,并且删除Task2
void task2Function(void * Parameters);

void task1Function(void * Parameters)
{
	TaskHandle_t xTaskHandle2; //Task2的Handle,用来删除Task2
	BaseType_t xReturn;
	
	while(1)	
	{	
		printf("1");
		//当xTaskCreate创建成功后,会返回pdPASS
		xReturn = xTaskCreate(task2Function,"Task2",1024,NULL,2,&xTaskHandle2);
		
		if( xReturn != pdPASS ) //创建失败,唯一原因只有内存不够(堆不够)
			printf("xTaskCreate err");
		
		//因为Task2刚创建完,就会立马执行(优先级高),所以在这里调用删除函数,Task2也会执行至少一次的
		vTaskDelete(xTaskHandle2); //干掉Task2
	}
}	

void task2Function(void * Parameters)
{
	while(1)	
	{
		printf("2");
		//因为Task2的优先级比较高,所以想要在Task1中删除Task2就必须让Task2进入阻塞或者暂停状态
		vTaskDelay(2); //休息2个Tick
	}
}

/*-----------------------------------------------------------*/

int main( void )
{
	//句柄(可以理解为一个任务的唯一标识)
	TaskHandle_t xTaskHandle1;  //创建句柄
#ifdef DEBUG
  debug();
#endif

	prvSetupHardware();

	printf("Hello World\r\n");
	
	xTaskCreate(task1Function,"Task1",100,NULL,1,&xTaskHandle1);
	
	
	/* Start the scheduler. */
	vTaskStartScheduler();

	/* Will only get here if there was not enough heap space to create the
	idle task. */
	return 0;
}

运行结果

理论:

  • Task1会不断的创建和删除Task2,但是Task1在不断的运行,导致Idle(空闲任务,优先级为0)没办法执行
  • 也就是说没办法去清理Task2,意味着在Task1里面会不断地消耗内存,但是清理工作没办法执行,从而导致创建Task2失败

事实:

  • 但是,运行结果并没有打印 “xTaskCreate err”,也就是说在空闲任务里面执行清理工作,那么这个结论需要修正一下

image-20221127125436724

实验二 *

  • 在main函数里面创建Task1
  • 在Task1里面创建Task2
  • 在Task2中将Task2删除(自杀)

代码

void task2Function(void * Parameters);

void task1Function(void * Parameters)
{
	TaskHandle_t xTaskHandle2; //Task2的Handle,用来删除Task2
	BaseType_t xReturn;
	
	while(1)	
	{	
		printf("1");
		//当xTaskCreate创建成功后,会返回pdPASS
		xReturn = xTaskCreate(task2Function,"Task2",1024,NULL,2,&xTaskHandle2);
		
		if( xReturn != pdPASS ) //创建失败,唯一原因只有内存不够
			printf("xTaskCreate err");
		
		//因为Task2刚创建完,就会立马执行(优先级高),所以在这里调用删除函数,Task2也会执行至少一次的
		//vTaskDelete(xTaskHandle2); //干掉Task2 (不会出现内存不够)
	}
}

void task2Function(void * Parameters)
{
	while(1)	
	{
		printf("2");
		//因为Task2的优先级比较高,所以想要在Task1中删除Task2就必须让Task2进入阻塞或者暂停状态
		//vTaskDelay(2); //休息2个Tick  (不会出现内存不够)
		
		/*
			内存不够原因:
				如果一个任务删除自己,它就不能完成一些清理工作,必须由空闲任务来完成清理工作
		*/
		vTaskDelete(NULL); //自杀(会出现内存不够)
	}
}

/*-----------------------------------------------------------*/

int main( void )
{
	//句柄(可以理解为一个任务的唯一标识)
	TaskHandle_t xTaskHandle1;  //创建句柄
#ifdef DEBUG
  debug();
#endif

	prvSetupHardware();

	printf("Hello World\r\n");
	
	xTaskCreate(task1Function,"Task1",100,NULL,1,&xTaskHandle1);
	
	
	/* Start the scheduler. */
	vTaskStartScheduler();

	/* Will only get here if there was not enough heap space to create the
	idle task. */
	return 0;
}

运行结果

可以看到程序很快就出现了内存不够的情况

  • 使用这种自杀的方式,应由空闲任务帮你释放内存
image-20221127130403726

钩子函数

空闲任务钩子函数

  • 执行一些低优先级的、后台的、需要连续执行的函数
  • 测量系统的空闲时间:空闲任务能被执行就意味着所有的高优先级任务都停止了,所以测量空闲任务占据的时间,就可以算出处理器占用率
  • 让系统进入省电模式:空闲任务能被执行就意味着没有重要的事情要做,当然可以进入省电模式了
  • 绝对不能导致任务进入Blocked、Suspended状态
  • 如果你会使用vTaskDelete()来删除任务,那么钩子函数要非常高效地执行。如果空闲任务移植卡在钩子函数里的话,它就无法释放内存

使用钩子函数的前提:

  • 把这个宏定义为1:configUSE_IDLE_HOOK
  • 实现 vApplicationIdleHook 函数
image-20221127133743062

代码

static int task1Flagrun = 0;
static int task2Flagrun = 0;
static int taskidleFlagrun = 0;

void task2Function(void * Parameters);
void task1Function(void * Parameters)
{
	TaskHandle_t xTaskHandle2; //Task2的Handle,用来删除Task2
	BaseType_t xReturn;
	
	while(1)	
	{	
		task1Flagrun = 1;
		task2Flagrun = 0;
		taskidleFlagrun = 0;
		
		printf("1");
		
		xReturn = xTaskCreate(task2Function,"Task2",1024,NULL,2,&xTaskHandle2);
	
		if( xReturn != pdPASS ) //创建失败,唯一原因只有内存不够
			printf("xTaskCreate err\r\n");
	}
}

void task2Function(void * Parameters)
{
	while(1)	
	{
		task1Flagrun = 0;
		task2Flagrun = 1;
		taskidleFlagrun = 0;
		
		printf("2");
	
		vTaskDelete(NULL); //自杀(会出现内存不够)
	}
}

void vApplicationIdleHook( void )
{
	task1Flagrun = 0;
	task2Flagrun = 0;
	taskidleFlagrun = 1;
	
	printf("0");
}

/*-----------------------------------------------------------*/

int main( void )
{
	//句柄(可以理解为一个任务的唯一标识)
	TaskHandle_t xTaskHandle1;  //创建句柄
#ifdef DEBUG
  debug();
#endif

	prvSetupHardware();

	printf("Hello World\r\n");
	
	xTaskCreate(task1Function,"Task1",100,NULL,0,&xTaskHandle1);
	
	
	/* Start the scheduler. */
	vTaskStartScheduler(); //此处会创建空闲任务

	/* Will only get here if there was not enough heap space to create the
	idle task. */
	return 0;
}

运行结果

  • 此时空闲任务是有机会执行的,它一旦有机会执行就会去做清理工作,内存就不会被耗尽

image-20221127151835607

image-20221127151331824

任务调度算法

状态与事件

正在运行的任务,被称为"正在使用处理器",它处于运行状态。在单处理器系统中,任何时间里只能有一个任务处于运行状态。

非运行状态的任务,它处于这3种状态之一:

  • 阻塞(Blocked)
  • 暂停(Suspended)
  • 就绪(Ready)

就绪态的任务,可以被调度器挑选出来切换为运行状态,调度器永远都是挑选最高优先级的就绪态任务并让它进入运行状态。

阻塞状态的任务,它在等待"事件",当事件发生时任务就会进入就绪状态。

事件分为两类:

  • 时间相关的事件
    • 所谓时间相关的事件,就是设置超时时间:在指定时间内阻塞,时间到了就进入就绪状态。
    • 使用时间相关的事件,可以实现周期性的功能、可以实现超时功能。
  • 同步事件
    • 同步事件就是:某个任务在等待某些信息,别的任务或者中断服务程序会给它发送信息。
    • 怎么"发送信息"?方法很多
      • 任务通知(task notification)
      • 队列(queue)
      • 事件组(event group)
      • 信号量(semaphoe)
      • 互斥量(mutex)等
      • 这些方法用来发送同步信息,比如表示某个外设得到了数据。

调度算法

从3个角度统一理解多种调度算法:

  • 可否抢占?高优先级的任务能否优先执行(配置项: configUSE_PREEMPTION)
    • 可以:被称作"可抢占调度"(Pre-emptive),高优先级的就绪任务马上执行,下面再细化。
    • 不可以:不能抢就只能协商了,被称作"合作调度模式"(Co-operative Scheduling)
    • 当前任务执行时,更高优先级的任务就绪了也不能马上运行,只能等待当前任务主动让 出CPU资源。
    • 其他同优先级的任务也只能等待:更高优先级的任务都不能抢占,平级的更应该老实点
  • 可抢占的前提下,同优先级的任务是否轮流执行(配置项:configUSE_TIME_SLICING)
    • 轮流执行:被称为"时间片轮转"(Time Slicing),同优先级的任务轮流执行,你执行一个时间 片、我再执行一个时间片
    • 不轮流执行:英文为"without Time Slicing",当前任务会一直执行,直到主动放弃、或者被 高优先级任务抢占
  • 在"可抢占"+"时间片轮转"的前提下,进一步细化:空闲任务是否让步于用户任务(配置项: configIDLE_SHOULD_YIELD)
    • 空闲任务低人一等,每执行一次循环,就看看是否主动让位给用户任务
    • 空闲任务跟用户任务一样,大家轮流执行,没有谁更特殊

列表如下:

配置项ABCDE
configUSE_PREEMPTION11110
configUSE_TIME_SLICING1100x
configIDLE_SHOULD_YIELD1010x
说明常用很少用很少用很少用几乎不用

注:

  • A:可抢占+时间片轮转+空闲任务让步
  • B:可抢占+时间片轮转+空闲任务不让步
  • C:可抢占+非时间片轮转+空闲任务让步
  • D:可抢占+非时间片轮转+空闲任务不让步
  • E:合作调度

是否可抢占

image-20221127211535055

配置此配置项可决定是否可抢占

  • 若为0,即不可抢占
  • 若为1,即可抢占

代码

static volatile int flagIdleTaskrun = 0;  // 空闲任务运行时flagIdleTaskrun=1
static volatile int flagTask1run = 0;     // 任务1运行时flagTask1run=1
static volatile int flagTask2run = 0;     // 任务2运行时flagTask2run=1
static volatile int flagTask3run = 0;     // 任务3运行时flagTask3run=1

/*-----------------------------------------------------------*/

void vTask1( void *pvParameters )
{
	/* 任务函数的主体一般都是无限循环 */
	for( ;; )
	{
		flagIdleTaskrun = 0;
		flagTask1run = 1;
		flagTask2run = 0;
		flagTask3run = 0;
		
		/* 打印任务的信息 */
		printf("T1\r\n");				
	}
}

void vTask2( void *pvParameters )
{	
	/* 任务函数的主体一般都是无限循环 */
	for( ;; )
	{
		flagIdleTaskrun = 0;
		flagTask1run = 0;
		flagTask2run = 1;
		flagTask3run = 0;
		
		/* 打印任务的信息 */
		printf("T2\r\n");				
	}
}

void vTask3( void *pvParameters )
{	
	const TickType_t xDelay5ms = pdMS_TO_TICKS( 5UL );		
	
	/* 任务函数的主体一般都是无限循环 */
	for( ;; )
	{
		flagIdleTaskrun = 0;
		flagTask1run = 0;
		flagTask2run = 0;
		flagTask3run = 1;
		
		/* 打印任务的信息 */
		printf("T3\r\n");				

		// 如果不休眠的话, 其他任务无法得到执行
		vTaskDelay( xDelay5ms );
	}
}

void vApplicationIdleHook(void)
{
	flagIdleTaskrun = 1;
	flagTask1run = 0;
	flagTask2run = 0;
	flagTask3run = 0;	
	
	/* 故意加入打印让flagIdleTaskrun变为1的时间维持长一点 */
	//printf("Id\r\n");				
}

int main( void )
{
	prvSetupHardware();
	
	xTaskCreate(vTask1, "Task 1", 1000, NULL, 0, NULL);
	xTaskCreate(vTask2, "Task 2", 1000, NULL, 0, NULL);
	//此处Task3的优先级最高,所以当Task3从阻塞中出来之后,就会立马执行任务(此时为可抢占)
	xTaskCreate(vTask3, "Task 3", 1000, NULL, 2, NULL);
	

	/* 启动调度器 */
	vTaskStartScheduler();

	/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
	return 0;
}

运行结果

当configUSE_PREEMPTION为0时(不可抢占):

image-20221127212032849
  • 当任务3进入阻塞状态时,任务1就会一直执行,即使任务3从阻塞状态中退出,任务1仍然再运行
  • 若不允许抢占,可以在任务做完某件事之后主动放弃CPU资源

当configUSE_PREEMPTION为1时(允许抢占):

image-20221127212535484 image-20221127212552651
  • 此处Task3的优先级最高,所以当Task3从阻塞中出来之后,就会立马执行任
  • 其他任务优先级均为0,则交替执行

是否轮流执行

image-20221127212903308

配置此配置项可决定同优先级是否可轮流执行

  • 若为0,即不可轮流执行
  • 若为1,即可轮流执行

代码

static volatile int flagIdleTaskrun = 0;  // 空闲任务运行时flagIdleTaskrun=1
static volatile int flagTask1run = 0;     // 任务1运行时flagTask1run=1
static volatile int flagTask2run = 0;     // 任务2运行时flagTask2run=1
static volatile int flagTask3run = 0;     // 任务3运行时flagTask3run=1

/*-----------------------------------------------------------*/

void vTask1( void *pvParameters )
{
	/* 任务函数的主体一般都是无限循环 */
	for( ;; )
	{
		flagIdleTaskrun = 0;
		flagTask1run = 1;
		flagTask2run = 0;
		flagTask3run = 0;
		
		/* 打印任务的信息 */
		printf("T1\r\n");				
	}
}

void vTask2( void *pvParameters )
{	
	/* 任务函数的主体一般都是无限循环 */
	for( ;; )
	{
		flagIdleTaskrun = 0;
		flagTask1run = 0;
		flagTask2run = 1;
		flagTask3run = 0;
		
		/* 打印任务的信息 */
		printf("T2\r\n");				
	}
}

void vTask3( void *pvParameters )
{	
	const TickType_t xDelay5ms = pdMS_TO_TICKS( 5UL );		
	
	/* 任务函数的主体一般都是无限循环 */
	for( ;; )
	{
		flagIdleTaskrun = 0;
		flagTask1run = 0;
		flagTask2run = 0;
		flagTask3run = 1;
		
		/* 打印任务的信息 */
		printf("T3\r\n");				

		// 如果不休眠的话, 其他任务无法得到执行
		vTaskDelay( xDelay5ms );
	}
}

void vApplicationIdleHook(void)
{
	flagIdleTaskrun = 1;
	flagTask1run = 0;
	flagTask2run = 0;
	flagTask3run = 0;	
	
	/* 故意加入打印让flagIdleTaskrun变为1的时间维持长一点 */
	//printf("Id\r\n");				
}

int main( void )
{
	prvSetupHardware();
	
	xTaskCreate(vTask1, "Task 1", 1000, NULL, 0, NULL);
	xTaskCreate(vTask2, "Task 2", 1000, NULL, 0, NULL);
	//此处Task3的优先级最高,所以当Task3从阻塞中出来之后,就会立马执行任务(此时为可抢占)
	xTaskCreate(vTask3, "Task 3", 1000, NULL, 2, NULL);
	

	/* 启动调度器 */
	vTaskStartScheduler();

	/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
	return 0;
}

运行结果

此处配置可抢占,不可轮流执行

image-20221127213421314
  • 可以看出当任务3放弃CPU资源后,某个任务就一直在执行(除了空闲),直到任务3重新抢回CPU资源

空闲任务是否礼让其他任务

image-20221127220256303

此配置项可配置空闲任务是否礼让其他任务

  • 若为0,则不礼让
  • 若为1,则礼让

ldieTask任务内部流程

IdleTask(){
	while(1)
    {
    	xxxx //做某些事情
        钩子(); //调用钩子函数
        if: YIELD == 1
            触发一次调度
        endif
    }
}

代码

static volatile int flagIdleTaskrun = 0;  // 空闲任务运行时flagIdleTaskrun=1
static volatile int flagTask1run = 0;     // 任务1运行时flagTask1run=1
static volatile int flagTask2run = 0;     // 任务2运行时flagTask2run=1
static volatile int flagTask3run = 0;     // 任务3运行时flagTask3run=1

/*-----------------------------------------------------------*/

void vTask1( void *pvParameters )
{
	/* 任务函数的主体一般都是无限循环 */
	for( ;; )
	{
		flagIdleTaskrun = 0;
		flagTask1run = 1;
		flagTask2run = 0;
		flagTask3run = 0;
		
		/* 打印任务的信息 */
		printf("T1\r\n");				
	}
}

void vTask2( void *pvParameters )
{	
	/* 任务函数的主体一般都是无限循环 */
	for( ;; )
	{
		flagIdleTaskrun = 0;
		flagTask1run = 0;
		flagTask2run = 1;
		flagTask3run = 0;
		
		/* 打印任务的信息 */
		printf("T2\r\n");				
	}
}

void vTask3( void *pvParameters )
{	
	const TickType_t xDelay5ms = pdMS_TO_TICKS( 5UL );		
	
	/* 任务函数的主体一般都是无限循环 */
	for( ;; )
	{
		flagIdleTaskrun = 0;
		flagTask1run = 0;
		flagTask2run = 0;
		flagTask3run = 1;
		
		/* 打印任务的信息 */
		printf("T3\r\n");				

		// 如果不休眠的话, 其他任务无法得到执行
		vTaskDelay( xDelay5ms );
	}
}

void vApplicationIdleHook(void)
{
	flagIdleTaskrun = 1;
	flagTask1run = 0;
	flagTask2run = 0;
	flagTask3run = 0;	
	
	/* 故意加入打印让flagIdleTaskrun变为1的时间维持长一点 */
	//printf("Id\r\n");				
}

int main( void )
{
	prvSetupHardware();
	
	xTaskCreate(vTask1, "Task 1", 1000, NULL, 0, NULL);
	xTaskCreate(vTask2, "Task 2", 1000, NULL, 0, NULL);
	//此处Task3的优先级最高,所以当Task3从阻塞中出来之后,就会立马执行任务(此时为可抢占)
	xTaskCreate(vTask3, "Task 3", 1000, NULL, 2, NULL);
	

	/* 启动调度器 */
	vTaskStartScheduler();

	/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
	return 0;
}

运行结果

此处为configIDLE_SHOULD_YIELD = 0时,即空闲任务不礼让其他任务

image-20221127222341415
  • 可以看出空闲任务和其他任务的运行时间都差不多

此处为configIDLE_SHOULD_YIELD = 1时,即空闲任务不礼让其他任务

image-20221127222531345
  • 可以看出明显比上面的运行时间要短

同步互斥与通信概述

同步互斥的概念

一句话理解同步与互斥:我等你用完厕所,我再用厕所。

  • 什么叫同步?就是:哎哎哎,我正在用厕所,你等会。
  • 什么叫互斥?就是:哎哎哎,我正在用厕所,你不能进来。
  • 同步与互斥经常放在一起讲,是因为它们之的关系很大,“互斥”操作可以使用“同步”来实现。我“等”你用 完厕所,我再用厕所。这不就是用“同步”来实现“互斥”吗?

再举一个例子。在团队活动里,同事A先写完报表,经理B才能拿去向领导汇报。经理B必须等同事A完 成报表,AB之间有依赖,B必须放慢脚步,被称为同步。在团队活动中,同事A已经使用会议室了,经 理B也想使用,即使经理B是领导,他也得等着,这就叫互斥。经理B跟同事A说:你用完会议室就提醒 我。这就是使用"同步"来实现"互斥"。

有时候看代码更容易理解,伪代码如下:

void 抢厕所(void)
{
	if (有人在用) 
        我眯一会; //进入Blocked状态
	用厕所;
	喂,醒醒,有人要用厕所吗;
}

假设有A、B两人早起抢厕所,A先行一步占用了;B慢了一步,于是就眯一会;当A用完后叫醒B,B也 就愉快地上厕所了。 在这个过程中,A、B是互斥地访问“厕所”,“厕所”被称之为临界资源。我们使用了“休眠-唤醒”的同步机 制实现了“临界资源”的“互斥访问”。

同一时间只能有一个人使用的资源,被称为临界资源。比如任务A、B都要使用串口来打印,串口就是临 界资源。如果A、B同时使用串口,那么打印出来的信息就是A、B混杂,无法分辨。所以使用串口时, 应该是这样:A用完,B再用;B用完,A再用。

同步的例子(有缺陷)

  • 让Task1执行比较大的运算
  • 当Task1在执行时,Task2会不断的判断Task1是否执行完毕
  • 缺点:太浪费CPU资源,Task1在运行的过程中,Task2应该处于阻塞状态,不应该去抢占CPU资源

代码

static int sum = 0;
static volatile int flagCalcEnd = 0;

/*-----------------------------------------------------------*/

void vTask1( void *pvParameters )
{
	//volatile:让系统不去优化此代码
	volatile int i;
	while(1)
	{
		for(i=0; i<10000000; i++)
				sum++;
		
		//通知Task2
		flagCalcEnd = 1;
		vTaskDelete(NULL);
	}
}

void vTask2( void *pvParameters )
{	
	while(1)
	{
		if(flagCalcEnd == 1)
			printf("sum = %d\r\n",sum);
	}
}


int main( void )
{
	prvSetupHardware();
	
	xTaskCreate(vTask1, "Task 1", 1000, NULL, 1, NULL);
	xTaskCreate(vTask2, "Task 2", 1000, NULL, 1, NULL);
	

	/* 启动调度器 */
	vTaskStartScheduler();

	/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
	return 0;
}

运行结果

image-20221128150902985
  • 当程序运行到6s的时候,Task2才开始执行
  • Task1在不断累加,Task2也在跟Task2竞争CPU资源
  • 这种同步方式,虽然可以执行,但是太浪费CPU资源

不创建Task2,仅创建Task1的运行结果:

image-20221128151356711
  • 可以看到i从0执行到10000000只花了3s,比上面快了一半

互斥的例子(有缺陷)

  • 编写一个通用函数,向串口打印当前正在运行的任务
  • 用通用函数来创建两个任务

代码

void vTaskGenericFunction(void * para)
{
	while(1)
	{
		if(!flagUSERused)
		{
			flagUSERused = 1; //以被占用
			printf("%s\r\n",(char *)para);
			flagUSERused = 0; //未被占用
			vTaskDelay(1); //阻塞一段时间
		}
	}
}

int main( void )
{
	prvSetupHardware();
	
//	xTaskCreate(vTask1, "Task 1", 1000, NULL, 1, NULL);
//	xTaskCreate(vTask2, "Task 2", 1000, NULL, 1, NULL);
	
	xTaskCreate(vTaskGenericFunction,"Task3",1000,"This is Task3 run",1,NULL);
	xTaskCreate(vTaskGenericFunction,"Task4",1000,"This is Task4 run",1,NULL);
	

	/* 启动调度器 */
	vTaskStartScheduler();

	/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
	return 0;
}
/*-----------------------------------------------------------*/

运行结果

void vTaskGenericFunction(void * para)
{
	while(1)
	{
		printf("%s\r\n",(char *)para);
	}
}
image-20221128154512999
  • 可以看出Task3和Task4都很难打印完整的信息


void vTaskGenericFunction(void * para)
{
	while(1)
	{
		if(!flagUSERused)
		{
			flagUSERused = 1; //以被占用
			printf("%s\r\n",(char *)para);
			flagUSERused = 0; //未被占用
		}
	}
}
image-20221128154849064
  • 虽然能打赢完整的信息,但是永远都是Task4在执行

  • 因为if里虽然将flagUSERused设为未被占用,但是下一轮循环之后Task4又会立刻将flagUSERused设为已被占用,Task3永远都抢不过Task4


void vTaskGenericFunction(void * para)
{
	while(1)
	{
		if(!flagUSERused)
		{
			flagUSERused = 1; //以被占用
			printf("%s\r\n",(char *)para);
			flagUSERused = 0; //未被占用
			vTaskDelay(1); //阻塞一段时间
		}
	}
}
image-20221128155317030
  • 这种写法就解决了上面两个问题
  • 但是,这个代码仍然会有一个隐患
    • 当Task3和Task4都执行vTaskGenericFunction时,可能会出现同一时刻执行到if(!flagUSERused)语句,且都成功进入if语句里面,这时再去设置flagUSERused为1已经无济于事
    • 此代码的问题在于 判断设置 的间隔时间太长

FreeRTOS的解决方案

  • 正确性

  • 效率:等待者要进入阻塞状态

  • 多种解决方案

image-20221128161406471

队列的使用

队列的理论讲解

队列的读写操作

队列的简化操如入下图所示,从此图可知:

  • 队列可以包含若干个数据:
  • 队列中有若干项,这被称为"长度"(length)
  • 每个数据大小固定 创建队列时就要指定长度、数据大小
  • 数据的操作采用先进先出的方法(FIFO,First In First Out):写数据时放到尾部,读数据时从头部读
  • 也可以强制写队列头部:覆盖头部数据
image-20221130090718672

image-20221130090836479

队列的结构体

Queue{
	*head:指向一块真正用来存放数据的缓冲区
    *xTasksWaitingToReceive:等待读数据的任务;若队列中无数据,则进入阻塞状态,若队列有数据,则退出阻塞状态,并执行读操作
    *xTasksWaitingToSend:等待写数据的任务;若队列已满,则进入阻塞状态,若队列有空间,则退出阻塞状态,并执行写操作
}
typedef struct QueueDefinition /* The old naming convention is used to prevent breaking kernel aware debuggers. */
{
    //头指针(不会更改指向的地址)
    int8_t * pcHead;           /*< Points to the beginning of the queue storage area. */
    //尾指针
    int8_t * pcWriteTo;        /*< Points to the free next place in the storage area. */

    union
    {
        //xQueue结构体里有个指针pcReadFrom,它指向当前头部,用来读数据
        QueuePointers_t xQueue;     /*< Data required exclusively when this structure is used as a queue. */
        SemaphoreData_t xSemaphore; /*< Data required exclusively when this structure is used as a semaphore. */
    } u;
	//等待发送数据:list2
    List_t xTasksWaitingToSend;             /*< List of tasks that are blocked waiting to post onto this queue.  Stored in priority order. */
    //等待接收数据:list1
    List_t xTasksWaitingToReceive;          /*< List of tasks that are blocked waiting to read from this queue.  Stored in priority order. */
} xQUEUE;

队列创建

动态分配内存:xQueueCreate,队列的内存在函数内部动态分配

  • uxQueueLength:队列长度,最多能存放多少个数据(item)
  • uxItemSize:每个数据(item)的大小:以字节为单位
  • 返回值:非0:成功,返回句柄,以后使用句柄来操作队列;NULL:失败,因为内存不足
image-20221130095014091

写队列

写入尾部

往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait:

BaseType_t xQueueSendToBack( QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait );
image-20221130105919497
  • 当pcWirteTo指针写完n-1个字节之后,pcWirteTo会条会头部(0)

写入头部

往队列头部写入数据,如果没有空间,阻塞时间为xTicksToWait

BaseType_t xQueueSendToFront(QueueHandle_t xQueue,const void *pvItemToQueue,TickType_t xTicksToWait);

image-20221130114657973

参数

  • xQueue:队列句柄,要写哪个队列
  • pvItemToQueue:数据指针,这个数据的值会被复制进队列, 复制多大的数据?在创建队列时已经指定了数据大小
  • xTicksToWait:如果队列满则无法写入新数据,可以让任务进入阻塞状态, xTicksToWait表示阻塞的最大时间(Tick Count)。 如果被设为0,无法写入数据时函数会立刻返回; 如果被设为portMAX_DELAY,则会一直阻塞(调用此函数的任务)直到有空间可写
  • 返回值:
    • pdPASS:数据成功写入了队列
    • errQUEUE_FULL:写入失败,因为队列满了。

读队列

使用 xQueueReceive() 函数读队列,读到一个数据后,队列中该数据会被移除。

BaseType_t xQueueReceive( QueueHandle_t xQueue,void * const pvBuffer,TickType_t xTicksToWait );
  • xQueue:队列句柄,要读哪个队列
  • pvBuffer:bufer指针,队列的数据会被复制到这个buffer 复制多大的数据?在创建队列时已经指定了数据大小
  • xTicksToWait:如果队列空则无法读出数据,可以让任务进入阻塞状态, xTicksToWait表示阻塞的最大时间(Tick Count)。 如果被设为0,无法读出数据时函数会立刻返回; 如果被设为portMAX_DELAY,则会一直阻塞直到有数据可写
  • 返回值:
    • pdPASS:从队列读出数据入
    • errQUEUE_EMPTY:读取失败,因为队列空了。

image-20221130113917237

队列的阻塞访问

只要知道队列的句柄,谁都可以读、写该队列。任务、ISR都可读、写队列。

可以多个任务读写队列。 任务读写队列时,简单地说:如果读写不成功,则阻塞;可以指定超时时间。口语化地说,就是可以定 个闹钟:如果能读写了就马上进入就绪态,否则就阻塞直到超时。

某个任务读队列时,如果队列没有数据,则该任务可以进入阻塞状态:还可以指定阻塞的时间。如果队 列有数据了,则该阻塞的任务会变为就绪态。如果一直都没有数据,则时间到之后它也会进入就绪态。

既然读取队列的任务个数没有限制,那么当多个任务读取空队列时,这些任务都会进入阻塞状态:有多 个任务在等待同一个队列的数据。当队列中有数据时,哪个任务会进入就绪态?

  • 优先级最高的任务
  • 如果大家的优先级相同,那等待时间最久的任务会进入就绪态

跟读队列类似,一个任务要写队列时,如果队列满了,该任务也可以进入阻塞状态:还可以指定阻塞的 时间。如果队列有空间了,则该阻塞的任务会变为就绪态。如果一直都没有空间,则时间到之后它也会 进入就绪态。

既然写队列的任务个数没有限制,那么当多个任务写"满队列"时,这些任务都会进入阻塞状态:有多个 任务在等待同一个队列的空间。当队列中有空间时,哪个任务会进入就绪态?

  • 优先级最高的任务
  • 如果大家的优先级相同,那等待时间最久的任务会进入就绪态

队列的常规使用

用队列实现同步

此代码改进了之前同步的例子,让task1在运行的时候,task2不会去抢占task1的cpu资源

运行流程:

  • 先运行task2任务,当task2任务执行到xQueueReceive的时候,发现队列中没有数据,则进入blocked状态,直到有数据
  • 当task2进入blocked状态执行,task1运行,当task1执行完10000000次之后,将结果写入队列中,此时task2将会退出blocked状态,并将队列的数据取出,打印到串口中

代码

static int sum = 0; 
static int task2flagrun; //用来标记task2是否运行
static QueueHandle_t xQueueCalcHandle;

/*-----------------------------------------------------------*/

void vTask1( void *pvParameters )
{
	int i;
	while(1)
	{
		for(i = 0;i<10000000;i++)
			sum++;
		
		/*
			写入队列:
			参数:
				xQueue:哪个队列
				pvItemToQueue:保存数据的地址;注意:这里是将值拷贝到队列中,所以即使后面num改了也不影响队列的值
				xTicksToWait:若队列已满是否等待,等待多长时间;0则不等待,portMAX_DELAY则一直等待
			返回值:
				pdPASS:数据成功写入了队列
				errQUEUE_FULL:写入失败,因为队列满了
		*/
		//将sum的值放入xQueueCalcHandle队列中
		xQueueSend(xQueueCalcHandle,&sum,portMAX_DELAY);
	}
}

void vTask2( void *pvParameters )
{	
	int num;
	while(1)
	{
		task2flagrun = 0;
		
		/*
			读队列
			参数:
				xQueue:要读哪个队列
				pvBuffer:存放数值的指针
				xTicksToWait:若队列已满是否等待,等待多长时间;0则不等待,portMAX_DELAY则一直等待
			返回值:
				pdPASS:从队列读出数据
				errQUEUE_EMPTY:读取失败,因为队列空了
		*/
		//读取xQueueCalcHandle队列,并将读取的数值放入num中
		xQueueReceive(xQueueCalcHandle,&num,portMAX_DELAY);
		printf("num = %d\r\n",num);
		
		task2flagrun = 1;
	}
}


int main( void )
{
	prvSetupHardware();
	
	/*
		创建队列
		参数:
			uxQueueLength:队列长度,也可称为队列元素个数
			uxItemSize:队列大小
		返回值:
			QueueHandle_t类型
			若创建成功则返回队列,若创建失败则返回0
		
	*/
	//创建可放入两个元素的队列,每个元素大小为int
	xQueueCalcHandle = xQueueCreate(2,sizeof(int)); //因为要放入num数值,所以是4个字节
	if(xQueueCalcHandle == NULL)
	{
		printf("create queue error!!");
	}
	
	xTaskCreate(vTask1,"Task1",1000,NULL,1,NULL);
	xTaskCreate(vTask2,"Task2",1000,NULL,1,NULL);
	
	/* 启动调度器 */
	vTaskStartScheduler();
	
	/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
	return 0;
}

运行结果

image-20221201142922726
  • 可以看到这次运行速度相比之前要快上一倍

用队列实现互斥

代码

static QueueHandle_t xQueueLockHandle;
/*-----------------------------------------------------------*/

//创建锁
int InitUSARTLock(void)
{
	int val;
	xQueueLockHandle = xQueueCreate(1,sizeof(int));
	if(xQueueLockHandle == NULL)
	{
		printf("Init lock error");
		return 1;
	}
	//写入一个数据,用来表示当前无任务执行
	xQueueSend(xQueueLockHandle,&val,portMAX_DELAY);
	return 0;
}

//读取锁
void getUSARTLock(void)
{
	int val;
	//如果队列能读取到数据,则表示无其他任务在运行
	xQueueReceive(xQueueLockHandle,&val,portMAX_DELAY);
}


//释放锁
void putUSARTLock(void)
{
	int val;
	//写入一个数据,用来表示当前无任务执行
	xQueueSend(xQueueLockHandle,&val,portMAX_DELAY);
}

void vtaskGenericFunction(void * para)
{
	while(1)
	{
		getUSARTLock();
	
		printf("%s\r\n",(char *)para);
		
		//到这里task1还在等待
		
		putUSARTLock(); /*	task1 --> Ready 但是 task2 --> Running,所以task1不会打印	*/
		
		/*
			解决方法:
				1.使用taskYIELD()发起一次任务切换
				2.使用taskDelay()让当前任务进入Blocked状态(不推荐)
		*/
		taskYIELD();	
		
	}
}

int main( void )
{
	prvSetupHardware();
	
	InitUSARTLock();
	
	xTaskCreate(vtaskGenericFunction,"Task1",1000,"This is Task1 run",1,NULL);
	xTaskCreate(vtaskGenericFunction,"Task2",1000,"This is Task2 run",1,NULL);
	
	/* 启动调度器 */
	vTaskStartScheduler();
	
	/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
	return 0;
}

运行结果

image-20221201154303189

当优先级相同且没有调用taskYIELD()的运行结果

image-20221201153735985

邮箱

FreeRTOS的邮箱概念跟别的RTOS不一样,这里的邮箱称为"橱窗"也许更恰当:

  • 它是一个队列,队列长度只有1

  • 写邮箱:新数据覆盖旧数据,在任务中使用 xQueueOverwrite() ,在中断中使用 xQueueOverwriteFromISR() 。

    既然是覆盖,那么无论邮箱中是否有数据,这些函数总能成功写入数据。

  • 读邮箱:读数据时,数据不会被移除;在任务中使用 xQueuePeek() ,在中断中使用 xQueuePeekFromISR() 。

    这意味着,第一次调用时会因为无数据而阻塞,一旦曾经写入数据,以后读邮箱时总能成功。

main函数中创建了队列(队列长度为1)、创建了发送任务、接收任务:

  • 发送任务的优先级为2,它先执行
  • 接收任务的优先级为1
/* 队列句柄, 创建队列时会设置这个变量 */
QueueHandle_t xQueue;

int main( void )
{	
	prvSetupHardware();
    
    /* 创建队列: 长度为1,数据大小为4字节(存放一个char指针) */
    xQueue = xQueueCreate( 1, sizeof(uint32_t) );
    
    if( xQueue != NULL )
    {
        /* 创建1个任务用于写队列
        * 任务函数会连续执行,构造buffer数据,把buffer地址写入队列
        * 优先级为2
        */
        xTaskCreate( vSenderTask, "Sender", 1000, NULL, 2, NULL );
        
        /* 创建1个任务用于读队列
        * 优先级为1
        */
        xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 1, NULL );
        /* 启动调度器 */ 
        vTaskStartScheduler();
    }else
    {
   	 	/* 无法创建队列 */
    }
    
    /* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
    return 0;
}

队列集

skStartScheduler();

/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
return 0;

}




#### 运行结果

<img src="https://s2.loli.net/2022/12/01/IGkE9S8tafC6FhB.png" alt="image-20221201142922726" style="zoom: 80%;" />

- 可以看到这次运行速度相比之前要快上一倍



### 用队列实现互斥





#### 代码

```c
static QueueHandle_t xQueueLockHandle;
/*-----------------------------------------------------------*/

//创建锁
int InitUSARTLock(void)
{
	int val;
	xQueueLockHandle = xQueueCreate(1,sizeof(int));
	if(xQueueLockHandle == NULL)
	{
		printf("Init lock error");
		return 1;
	}
	//写入一个数据,用来表示当前无任务执行
	xQueueSend(xQueueLockHandle,&val,portMAX_DELAY);
	return 0;
}

//读取锁
void getUSARTLock(void)
{
	int val;
	//如果队列能读取到数据,则表示无其他任务在运行
	xQueueReceive(xQueueLockHandle,&val,portMAX_DELAY);
}


//释放锁
void putUSARTLock(void)
{
	int val;
	//写入一个数据,用来表示当前无任务执行
	xQueueSend(xQueueLockHandle,&val,portMAX_DELAY);
}

void vtaskGenericFunction(void * para)
{
	while(1)
	{
		getUSARTLock();
	
		printf("%s\r\n",(char *)para);
		
		//到这里task1还在等待
		
		putUSARTLock(); /*	task1 --> Ready 但是 task2 --> Running,所以task1不会打印	*/
		
		/*
			解决方法:
				1.使用taskYIELD()发起一次任务切换
				2.使用taskDelay()让当前任务进入Blocked状态(不推荐)
		*/
		taskYIELD();	
		
	}
}

int main( void )
{
	prvSetupHardware();
	
	InitUSARTLock();
	
	xTaskCreate(vtaskGenericFunction,"Task1",1000,"This is Task1 run",1,NULL);
	xTaskCreate(vtaskGenericFunction,"Task2",1000,"This is Task2 run",1,NULL);
	
	/* 启动调度器 */
	vTaskStartScheduler();
	
	/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
	return 0;
}

运行结果

[外链图片转存中…(img-D65rbcHE-1675569506983)]

当优先级相同且没有调用taskYIELD()的运行结果

[外链图片转存中…(img-z5DjFpLF-1675569506984)]

邮箱

FreeRTOS的邮箱概念跟别的RTOS不一样,这里的邮箱称为"橱窗"也许更恰当:

  • 它是一个队列,队列长度只有1

  • 写邮箱:新数据覆盖旧数据,在任务中使用 xQueueOverwrite() ,在中断中使用 xQueueOverwriteFromISR() 。

    既然是覆盖,那么无论邮箱中是否有数据,这些函数总能成功写入数据。

  • 读邮箱:读数据时,数据不会被移除;在任务中使用 xQueuePeek() ,在中断中使用 xQueuePeekFromISR() 。

    这意味着,第一次调用时会因为无数据而阻塞,一旦曾经写入数据,以后读邮箱时总能成功。

main函数中创建了队列(队列长度为1)、创建了发送任务、接收任务:

  • 发送任务的优先级为2,它先执行
  • 接收任务的优先级为1
/* 队列句柄, 创建队列时会设置这个变量 */
QueueHandle_t xQueue;

int main( void )
{	
	prvSetupHardware();
    
    /* 创建队列: 长度为1,数据大小为4字节(存放一个char指针) */
    xQueue = xQueueCreate( 1, sizeof(uint32_t) );
    
    if( xQueue != NULL )
    {
        /* 创建1个任务用于写队列
        * 任务函数会连续执行,构造buffer数据,把buffer地址写入队列
        * 优先级为2
        */
        xTaskCreate( vSenderTask, "Sender", 1000, NULL, 2, NULL );
        
        /* 创建1个任务用于读队列
        * 优先级为1
        */
        xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 1, NULL );
        /* 启动调度器 */ 
        vTaskStartScheduler();
    }else
    {
   	 	/* 无法创建队列 */
    }
    
    /* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
    return 0;
}

队列集

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

FreeRtos快速入门 的相关文章

  • ros-话题节点消息控制rviz中机械臂Publish Joint_State with Python to RVIZ-ros回炉再强学习(6)

    ros 话题节点消息Publish Joint State with Python to RVIZ ros回炉再强学习 xff08 6 xff09 笔者工作环境 xff1a ubuntu16 04 ros kinect 代码下载地址 xff
  • opencv边缘检测运用sobel算子源代码方法

    opencv边缘检测运用sobel算子源代码方法 span class token function import span cv2 span class token function import span numpy as np spa
  • v-rep仿真之键盘控制机械臂末端移动

    v rep仿真之键盘控制机械臂末端移动 键盘控制机械臂末端移动原理为 xff0c 设置机械臂逆运动学target xff0c 机械臂末端跟随target运动 xff0c 然后通过改变target的值 xff0c 从而达到控制机械臂末端移动的
  • urx驱动ur3和onrobot rg2

    urx驱动ur3和onrobot rg2 注意 xff1a 非常重要的一点 xff0c urx是可以在Python2和Python3都支持的 xff0c 随着时间改变 xff0c 如果有的读者发现Python2中不能使用 xff0c 报错m
  • ros-melodic安装解决sudo rosdep init问题

    ros melodic安装解决sudo rosdep init问题 解决办法1 去网站查看raw githubusercontent com的真实IP span class token function sudo span span cla
  • 上电浪涌电流

    上电浪涌电流 电机启动或者停转都会形成浪涌电流 xff0c 例如启动的浪涌最大 xff0c 毕竟电机启动静态电阻非常小 xff0c 上电等同短路 xff0c 其电机为感性负载 xff0c 由较大的无功电流 xff0c 对电网造成波动非常大
  • 电机功率计算公式

    电机功率计算公式 电动机输入功率 单相电机为P 61 UI xff0c 三相电机P 61 UIcos0 8 输出功率 xff08 驱动功率 xff09 P 61 FV F为力 牛顿 V xff1a 速度 m S xff09 换算到电机则有
  • C++ 中 map 字典与 set 集合的使用

    在 C 43 43 中 xff0c map 是关联容器 的一种 xff0c 关联容器将值 与键 关联到一起 xff0c 并使用键来查找值 这与 python 中的字典 类型类似 xff0c 也是用来存储键 值对 xff08 key valu
  • win11 安装 WSL2 在非 C 盘及配置(图形界面+代理)

    WSL 安装及配置 直接安装 WSL2 在非 C 盘启用 WSL 功能前提条件设置默认安装 WSL2安装在非 C 盘 图形界面先决条件更新 WSL 以支持 GUI 配置 WSL2 使用 Windows 网络代理 直接安装 WSL2 在非 C
  • CVTE嵌入式实习生与秋招

    目录 前言一 实习笔试二 实习面试三 实习工作内容四 公司看法 前言 今年暑假去CVTE实习了一个多月最后经过转正答辩 xff0c 获得了offer xff0c 现就我的实习经历和对公司的一些认知分享一下 xff08 仅代表个人观点 xff
  • 视频编解码行业及发展方向简述

    目录 一 视频行业1 视频是一个方兴未艾的大产业2 视频行业潜在商机大 人才缺口大3 了解华为海思的HI3518E方案 二 海思方案项目用到的硬件平台介绍1 本专栏文章使用的开发板配置2 处理器为什么选HI3518E 三 本专栏文章规划和核
  • 全面认识海思SDK及嵌入式层开发(1)

    目录 一 全面认识和检测配套开发套装1 套装配件介绍2 检测开发板3 注意 二 视频设备开发的技术流1 视频从产生到被消费的整个流程2 视频行业的商业角度分段3 几个疑问点 一 全面认识和检测配套开发套装 购买方式 xff1a 淘宝搜索 g
  • 嵌入式linux开发环境搭建(VMware16.0.0+Ubuntu16.04.3_X64)

    目录 一 安装VMware1 VMware介绍2 安装VMware16 0 0 二 安装ubuntu16 04 3 LTS1 Ubuntu介绍2 下载安装包iso3 安装 四 新安装Ubuntu的基本设置1 开机和关机等2 虚拟机基本设置3
  • 全面认识海思SDK及嵌入式层开发(2)

    目录 一 HI3518E方案系统整体架构介绍1 硬件上2 软件上 二 海思SDK的整体介绍三 海思SDK包的学习和实验1 2篇相关文档2 SDK包复制到linux原生目录中并解压3 SDK包操作的脚本程序研究4 SDK中源码包部分的配置编译
  • 计算机视觉之相机模型

    目录 一 相机模型1 相机与图像2 坐标系3 世界坐标系到摄像机坐标系4 摄像机坐标系到图像物理坐标系5 图像物理坐标系到图像像素坐标系6 摄像机坐标系到图像像素坐标系7 世界坐标系到图像像素坐标系 二 镜头畸变1 相机成像原理2 镜头畸变
  • vscode安装插件失败,完美解决

    vscode安装插件一直失败 xff0c 解决方案如下 访问vscode插件官网https marketplace visualstudio com vscode xff0c 搜索你要的插件点击插件详情 Version History 下载
  • ROS的topic通信机制

    1 通信步骤如图 xff1a 2 步骤介绍 第 xff08 0 xff09 步 xff1a talker gt master 发布者talker向mater注册 xff1a 包括节点的信息 需要发布的话题名等 xff0c 然后节点管理器RO
  • 关于快速幂和矩阵快速幂

    快速幂 xff1a 可参考该链接百科快速幂也可以参考这个博客快速幂博客 给出快速幂的题目和代码 xff1a 快速幂 取余计算 include lt iostream gt include lt string h gt using names
  • C/C++ 文件操作基础

    目录 1 文件分类 2 文件的打开与关闭 3 顺序读写文件 4 随机读写文件 5 其他与文件相关的操作 最近看 GNU Radio 源码看到了文件操作的部分 xff0c 因此记录下学习 C 43 43 C 操作文件的过程 本文的文件操作是
  • npm的装包原理

    原文 xff1a npm安装包原理 前言 xff1a 提起npm xff0c 大家第一个想到的应该就是 npm install 了 xff0c 但是 npm install 之后生成的 node modules大家有观察过吗 xff1f p

随机推荐

  • 02基于freertos实现串口通讯

    文章目录 一 操作步骤1 任务创建API2 步骤 二 代码 一 操作步骤 1 任务创建API 动态创建任务xTaskCreate 静态创建任务xTaskCreateStatic 任务句柄 xff1a 任务句柄就是一个指针 xff0c 指向任
  • windows中freeRTOS模拟器环境搭建

    windows中的freeRTOS使用 FreeRTOS参考手册中的模拟程序都是基于windows版本的模拟器 因此为了学习FreeRTOS xff0c 需要在Windows中搭建一下模拟器的运行环境 网络上的一般都是直接跑一下FreeRT
  • Xavier NX+4路Cameralink图像处理卡

    Xavier NX 43 4路Cameralink图像处理卡 M D CAP是天津雷航光电科技有限公司推出的一款复合加速计算平台 xff0c 由 NVidia GPU 和 Xilinx FPGA 通过PCIE 互联而成的高性能计算平台 其中
  • Xavier NX+KU040图像处理卡

    Xavier NX 43 KU040图像处理卡 CS NX21T是天津雷航光电科技有限公司推出的一款复合加速计算平台 xff0c 由 NVidia GPU 和 Xilinx FPGA 通过PCIE 互联而成的高性能计算平台 其中 FPGA
  • js使用input上传文件夹、拖拽上传文件夹并将文件夹结构展示为树形结构

    一 实现效果 左侧区域支持选择一个系统中的文件夹 xff0c 或者将文件夹拖拽到这个区域进行上传 xff0c 右侧区域可以将文件夹的结构展示为树形结构 二 代码实现 由于需要使用树形插件zTree xff0c 这个插件是依赖于jquery的
  • 傅里叶描述子、HOG特征描述子原理及matlab代码

    一 傅里叶描述子 傅里叶描述子的作用是用来描述图像的轮廓信息 xff0c 具有平移 旋转 尺度不变性特征 对于一幅图像 xff0c 通过傅里叶描述子获得其图像轮廓信息 xff0c 其本质就是空间 频域变换问题 通过将图像中的像素点进行傅里叶
  • raise ValueError(‘Expected input batch_size ({}) to match target batch_size ({}).‘

    raise ValueError Expected input batch size to match target batch size 记得在正向传播之前打印一下图片的尺寸 xff0c 这次没注意到图片其实都进来是 RGB 三通道的数据
  • RuntimeError mat1 dim 1 must match mat2 dim 0

    RuntimeError mat1 dim 1 must match mat2 dim 0 代码 self span class token punctuation span fc1 span class token operator 61
  • python 中 subprocess.Popen 总结

    python 中 subprocess Popen 总结 subprocess的目的就是启动一个新的进程并且与之通信 subprocess模块中只定义了一个类 Popen 可以使用Popen来创建进程 xff0c 并与进程进行复杂的交互 它
  • 关于 GNURadio-OFDM 运行时掉帧问题的解决

    最近在运行 GNURadio 中的 OFDM 例程时发现了丢帧的问题 2022 08 01更新 xff1a 1 突然又想起了这个问题 xff0c 今年年初的时候与西电的一个师兄又重新讨论了这个问题 xff0c 有了更深一步的理解 xff0c
  • 相机标定(Camera calibration)原理、步骤

    相机标定 xff08 Camera calibration xff09 原理 步骤 简介 在图像测量过程以及机器视觉应用中 xff0c 为确定空间物体表面某点的三维几何位置与其在图像中对应点之间的相互关系 xff0c 必须建立相机成像的几何
  • python3-rosdep2 : Depends: python3-catkin-pkg but it is not going to be installed

    python3 rosdep2 Depends python3 catkin pkg but it is not going to be installed 今天在 Ubuntu18 04 中安装ros莫名其妙的出现了上述问题 xff0c
  • Ubuntu 18.04 安装 T265 相机驱动

    Ubuntu 18 04 安装 T265 相机驱动 Make Ubuntu Up to date span class token function sudo span span class token function apt get s
  • VINS中旋转外参初始化

    VINS 中的旋转外参初始化 为了使这个两个传感器融合 xff0c 我们首先需要做的事情是将两个传感器的数据对齐 xff0c 除了时间上的对齐 xff0c 还有空间上的对齐 空间上的对齐通俗的讲就是将一个传感器获取的数据统一到另一个传感器的
  • 旋转矩阵求导

    旋转矩阵的导数 证明 quad 假设旋转矩阵为 R mathbf R theta
  • VINS中陀螺仪零偏的估计

    VINS中关于陀螺仪零偏的初始化估计 对于窗口中得连续两帧 b k b k b k 和 b
  • 非线性优化问题基本形式概述

    非线性优化问题以及在视觉SLAM中的应用 1 0 最小二乘基础概念 定义 quad 找到一个 n 维的变量 x
  • Ceres 自动求导解析-从原理到实践

    Ceres 自动求导解析 从原理到实践 文章目录 Ceres 自动求导解析 从原理到实践1 0 前言2 0 Ceres求导简介3 0 Ceres 自动求导原理3 1 官方解释3 2 自我理解 4 0 实践4 1 Jet 的实现4 2 多项式
  • 深度学习环境搭建之cuda、cudnn以及pytorch和torchvision的whl文件安装方法

    一 前言 假设已经装好了pycharm anaconda xff0c 并且新建了一个conda虚拟环境 xff08 我的虚拟环境名为pytorch xff09 接下来需要安装新版的显卡驱动 xff0c 安装cuda cudnn pytorc
  • FreeRtos快速入门

    堆和栈 堆 堆即为一块空闲的内存 xff0c 从这块内存中来取出一部分用完之后再把它释放回去 span class token comment 此时heap buf就相当于一块空闲的内存 span span class token comm