利用STM32CubeMX和Keil模拟器,3天入门FreeRTOS(4.2) —— 中断函数中使用队列

前言

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

前期准备

(1)首先将上一篇博客的代码复制一遍下来。

利用STM32CubeMX和Keil模拟器,3天入门FreeRTOS(4.2) —— 中断函数中使用队列_第1张图片

(2)将两个按键设置为双边沿触发,默认弱下拉。

利用STM32CubeMX和Keil模拟器,3天入门FreeRTOS(4.2) —— 中断函数中使用队列_第2张图片

(3)打开使能两个引脚中断

利用STM32CubeMX和Keil模拟器,3天入门FreeRTOS(4.2) —— 中断函数中使用队列_第3张图片

(4)因为我们使用的是外部中断0和外部中断1,所以这里需要使用HAL_GPIO_EXTI_Callback()进行重定义。

/* Private application code --------------------------------------------------*/
/* USER CODE BEGIN Application */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
	switch (GPIO_Pin)
	{
		case GPIO_PIN_0:
		{		
			break;
		}
		case GPIO_PIN_1:
		{
			break;
		}
	}
}
/* USER CODE END Application */

实战

补充细节

(1)我们按Ctrl+F搜索Header_StartCubemxTask ,然后直接进行如下函数补充即可。

/* USER CODE END Header_StartCubemxTask */
void StartCubemxTask(void *argument)
{
  /* USER CODE BEGIN StartCubemxTask */
	char *CubemxTaskPrintf = (char *)argument;
  /* Infinite loop */
  for(;;);
  /* USER CODE END StartCubemxTask */
}

/* 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)
{
	while(1);
}

/* --- 写实验 ---*/
#define Test_xQueueSendToFrontFromISR   1
#define Test_xQueueSendToBackFromISR    0
#define Test_xQueueOverwriteFromISR     0
/* --- 读实验 ---*/
#define Test_xQueueReceiveFromISR       0
#define Test_xQueuePeekFromISR          1
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
	static uint16_t Buf = 0;
	BaseType_t status;
	switch (GPIO_Pin)
	{
		case GPIO_PIN_0:
		{		
			if (HAL_GPIO_ReadPin(Key_0_GPIO_Port, Key_0_Pin) == GPIO_PIN_SET)
			{
				Buf++;
#if Test_xQueueSendToFrontFromISR
				// 写实验1:测试头插xQueueSendToFrontFromISR()函数
				status = xQueueSendToFrontFromISR(KeilQueueHandle, &Buf, 0);
				if (status == pdTRUE)
				{
					printf("xQueueSendToFrontFromISR writes data successfully : %d\r\n", Buf);
				}
				else
				{
					printf("xQueueSendToFrontFromISR failed to write data\r\n");
				}
#elif Test_xQueueSendToBackFromISR
				// 写实验2:测试尾插xQueueSendToBackFromISR()函数
				status = xQueueSendToBackFromISR(KeilQueueHandle, &Buf, 0);
				if (status == pdTRUE)
				{
					printf("xQueueSendToBackFromISR writes data successfully : %d\r\n", Buf);
				}
				else
				{
					printf("xQueueSendToBackFromISR failed to write data\r\n");
				}
#elif Test_xQueueOverwriteFromISR
				// 写实验3:测试xQueueOverwrite()函数,这个只能用于队列大小为1的情况。不为1的队列使用这个,程序会崩溃
				xQueueOverwriteFromISR(KeilQueueHandle, &Buf);
				printf("xQueueOverwriteFromISR writes data successfully : %d\r\n", Buf);
#endif
				//查询队列中存储的消息数
				status = uxQueueMessagesWaitingFromISR(KeilQueueHandle);
				printf("The number of data stored in the queue : %d\r\n",status);
			}
			break;
		}
		case GPIO_PIN_1:
		{
			// 按下K1读取数据
			if (HAL_GPIO_ReadPin(Key_1_GPIO_Port, Key_1_Pin) == GPIO_PIN_SET )
			{
#if Test_xQueueReceiveFromISR
				// 读实验1:从队列头部读取消息,并删除消息
				status = xQueueReceiveFromISR(KeilQueueHandle, &Buf, 0);
				if (status == pdTRUE)
				{
					printf("xQueueReceiveFromISR data read successfully :%d\r\n", Buf);
				}
				else
				{
					printf("xQueueReceiveFromISR data read failed\r\n");
				}
#elif Test_xQueuePeekFromISR
				// 读实验2:从队列头部读取消息,但是不删除消息
				status = xQueuePeekFromISR(KeilQueueHandle, &Buf);
				if (status == pdTRUE)
				{
					printf("xQueuePeekFromISR data read successfully :%d\r\n", Buf);
				}
				else
				{
					printf("xQueuePeekFromISR data read failed\r\n");
				}
#endif
				//查询队列中的可用空间数,这个没有提供中断适配函数
//				status = uxQueueSpacesAvailable(KeilQueueHandle);
//				printf("There is space left in the queue : %d\r\n",status);
			}
			break;
		}
	}
}
/* USER CODE END Application */


测试结果

(1)测试结果与前文一致。

理论

中断和任务关系

(1)不管中断的优先级是多少,中断的优先级永远高任何任务的优先级。即在执行的过程中,中断来了就开始执行中断服务程序,即使是拥有最小优先级的中断也会打断拥有最高优先级的任务
(2)中断是由硬件层面决定的,而任务只是一个软件的概念。RTOS本质上就是滴答定时器中断提供时间基准,之后判断是否有任务需要进行调度。如果需要任务调度,那么就会触发PendSV_Handler中断,进行任务调度。

滴答定时器中断简单介绍

(1)在FreeRTOS中,滴答定时器中断函数进行了一次宏定义。个人感觉应该是为了做其他内核的兼容,加上宏定义之后,可移植性增强。
(2)vPortRaiseBASEPRI();这个函数就是进行关中断,因为滴答定时器在增加时基的时候,可能会收到其他中断影响而导致增加时基出现错误。为了防止出现这样的情况,因此需要调用vPortRaiseBASEPRI();将这些中断进行屏蔽。程序执行完成之后,调用vPortClearBASEPRIFromISR();解除中断屏蔽。而这段代码区域,称为临界区
(3)在临界区中,我们会增加RTOS的时基,同时判断是否需要进行上下文切换。如果需要进行上下文切换,那么就触发PendSV中断。

#define xPortSysTickHandler SysTick_Handler
void xPortSysTickHandler( void )
{
	/* SysTick运行在最低的中断优先级上,因此当这个中断执行时,
	所有中断都必须处于未屏蔽状态。
	因此,无需保存和恢复中断屏蔽值,因为其值已经是已知的。
	因此,vPortRaiseBASEPRI() 函数被用作替代 
	portSET_INTERRUPT_MASK_FROM_ISR() 函数,因为它速度略快。*/
	vPortRaiseBASEPRI();
	{
		/* 增加RTOS时钟 */
		if( xTaskIncrementTick() != pdFALSE )
		{
			/* 如果需要进行上下文切换,就触发PendSV中断 */
			portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
		}
	}
	/* 解除中断屏蔽 */
	vPortClearBASEPRIFromISR();
}

(4)使用临界区,很好的保护了滴答定时器中断程序不会被干扰。但是这样就会导致一个问题,当滴答定时器中断正在执行的时候,优先级更高的中断触发,就会需要等待滴答定时器中断执行完成才能够执行。执行流程如下图:

利用STM32CubeMX和Keil模拟器,3天入门FreeRTOS(4.2) —— 中断函数中使用队列_第4张图片

(5)可能有朋友就会问了,如果我的中断,就是要立刻执行,怎么办呢?首先,我先需要先确认一下,这个中断立刻执行的这个立刻到底是多长时间?
<1>从FreeRTOS的官网中我们可以知道,上下文切换时间为 84 个 CPU 周期。以STM32F103为例,当我们设置系统时钟为72MHZ的时候,一次上下文切换时间大概是1.2us。
<2>也就是说,除非你的中断的实时性要求苛刻到比1.2us还要高,否则这个临界区是可接受的。
<3>但是,因为有朋友肯定要说了,我实时性要求就是极端的变态,怎么办呢?很简单,你将ISR优先级设置比configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY高即可。(这个后面会细说)

PendSV中断简单介绍

(1)首先,我们需要知道为什么需要PendSV中断。我个人建议各位看看这篇博客,讲解的非常清晰,我在这里不进行赘述:PendSV功能,为什么需要PendSV
(2)如果不打算看上面这篇博客的,我就简单总结一下。因为我们知道,滴答定时器中断临界区会导致ISR的延迟处理,我们是否能够将任务切换分成两部分,一个是滴答定时器判断是否需要任务切换,一个是PendSV中断进行实际的任务切换。如果在判断过程中,来了ISR,那么我判断完成就可以马上进行ISR,然后再进行任务切换。那样就能够一定程度上提高ISR的响应速度。

利用STM32CubeMX和Keil模拟器,3天入门FreeRTOS(4.2) —— 中断函数中使用队列_第5张图片

什么时候不能使用ISR后缀的函数

(1)前面不是埋了一个坑麻,说如果ISR想要实时响应,能够打断滴答定时器的临界区如何处理。这个时候就需要请到一个宏了,在FreeRTOSConfig.h文件中,有configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 宏,负责定义FreeRTOS临界区可屏蔽的中断。例如现在这个宏设置为5,那么如果ISR的优先级在0-4之间,即使现在现在滴答定时器在临界区中执行程序,ISR也能够成功的打断滴答定时器,立即进入中断函数。执行流程如下图:

#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5

利用STM32CubeMX和Keil模拟器,3天入门FreeRTOS(4.2) —— 中断函数中使用队列_第6张图片

利用STM32CubeMX和Keil模拟器,3天入门FreeRTOS(4.2) —— 中断函数中使用队列_第7张图片

利用STM32CubeMX和Keil模拟器,3天入门FreeRTOS(4.2) —— 中断函数中使用队列_第8张图片

(2)在上图我们看到,如果是FreeRTOS不可屏蔽中断不能使用以FromISR结尾的API函数。

利用STM32CubeMX和Keil模拟器,3天入门FreeRTOS(4.2) —— 中断函数中使用队列_第9张图片

为什么启动FreeRTOS后,必须是中断分组4

(1)在CM3CM4内核中的NVIC控制器可以规定5中中断分组,在学习STM32裸机教程的时候,我们会调用NVIC_PriorityGroupConfig()函数设置中断分组。或者是在STM32CubeMX中设置中断分组。

利用STM32CubeMX和Keil模拟器,3天入门FreeRTOS(4.2) —— 中断函数中使用队列_第10张图片
利用STM32CubeMX和Keil模拟器,3天入门FreeRTOS(4.2) —— 中断函数中使用队列_第11张图片

(2)但是,如果有细心的朋友会发现一个问题。在STM32CubeMX中无法设置成其他中断分组,只有中断分组4一个选项。这个是为什么呢?
因为FreeRTOS 的中断配置没有处理亚优先级这种情况,所以我们只能配置中断优先级分组为 4,直接就 16 个主优先级,使用起来也简单!

xHigherPriorityTaskWoken参数理解

(1)中断函数要求快速执行,因此,中断中执行的函数都是立即返回的,没有等待时间。那么,我们能够发现,原来的等待时间的参数变成的xHigherPriorityTaskWoken。我们将如何理解xHigherPriorityTaskWoken这个参数呢?
(2)还是回到上面关于中断和任务关系的介绍中,我们知道,滴答定时器是负责增加RTOS时基和任务切换判断,PendSV才是真正进行任务切换的中断函数。如果我们在ISR中执行类似队列操作,成功唤醒了任务优先级更高的Task2ISR结束之后,我们如果想要让Task2马上开始执行,就需要利用上xHigherPriorityTaskWoken这个参数。
(3)xHigherPriorityTaskWoken表示是否成功唤醒了一个更高优先级的任务Task2。如果成功唤醒了,那么xHigherPriorityTaskWoken会被标记为pdTRUE,那么我们就可以执行portYIELD_FROM_ISR()函数触发PendSV进行任务切换。
(4)ISR中的队列操作成功唤醒了一个更高优先级的Task2,利用xHigherPriorityTaskWoken参数的程序的执行流程和代码如下:

BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xQueueSendToFrontFromISR(KeilQueueHandle, &Buf, &xHigherPriorityTaskWoken);
if( xHigherPriorityTaskWoken )
{
	// 如果发送成功且唤醒了一个更高优先级的任务,则进行上下文切换
	portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

利用STM32CubeMX和Keil模拟器,3天入门FreeRTOS(4.2) —— 中断函数中使用队列_第12张图片

(5)但是,如果我们不利用xHigherPriorityTaskWoken这个参数,这个参数直接传入一个NULLISR中的队列操作成功唤醒了一个更高优先级的Task2,程序的执行流程和代码如下:

xQueueSendToFrontFromISR(KeilQueueHandle, &Buf, NULL);

利用STM32CubeMX和Keil模拟器,3天入门FreeRTOS(4.2) —— 中断函数中使用队列_第13张图片

API函数介绍

中断函数中向队列写数据

xQueueSendFromISR()和xQueueSendToBackFromISR()函数介绍

(1)xQueueSendFromISR()xQueueSendToFrontFromISR()作用一致。
(2)xQueueSendFromISR()xQueueSend()使用基本一样,除了第三个参数不同。具体细节看上面的xHigherPriorityTaskWoken参数理解
(3)使用场景不同,xQueueSend()是在任务中调用,而xQueueSendFromISR()是在FreeRTOS可屏蔽中断中调用。

/**
 * @brief  中断函数队列数据尾插
 *
 * @param  xQueue         队列的句柄
 *        -pvItemToQueue  指向待入队数据项的指针,每次只能插入一个数据
 *        -pxHigherPriorityTaskWoken 是否成功唤醒了一个更高优先级的任务,成功唤醒该值为pdTRUE,否则为pdFALSE
 *
 * @return  如果数据尾插成功,返回 pdTRUE,否则返回 errQUEUE_FULL。
 */
 BaseType_t xQueueSendToBackFromISR(QueueHandle_t xQueue,const void *pvItemToQueue,BaseType_t *pxHigherPriorityTaskWoken);

xQueueSendToFrontFromISR()函数介绍

(1)和上面使用方法一致,只不过一个是尾插一个是头插。

/**
 * @brief  中断函数队列数据头插
 *
 * @param  xQueue         队列的句柄
 *        -pvItemToQueue  指向待入队数据项的指针,每次只能插入一个数据
 *        -pxHigherPriorityTaskWoken 是否成功唤醒了一个更高优先级的任务,成功唤醒该值为pdTRUE,否则为pdFALSE
 *
 * @return  如果数据头插成功,返回 pdTRUE,否则返回 errQUEUE_FULL。
 */
 BaseType_t xQueueSendToFrontFromISR(QueueHandle_t xQueue,const void *pvItemToQueue,BaseType_t *pxHigherPriorityTaskWoken);

xQueueOverwriteFromISR()函数介绍

(1)覆盖队列中已经存在的数据,旨在用于长度为1的队列,队列要么为空,要么为满。

/**
 * @brief  覆盖队列中已经存在的数据(旨在用于长度为1的队列,队列要么为空,要么为满)
 *
 * @param  xQueue         队列的句柄
 *        -pvItemToQueue  指向待入队数据项的指针,每次只能插入一个数据
 *        -pxHigherPriorityTaskWoken 是否成功唤醒了一个更高优先级的任务,成功唤醒该值为pdTRUE,否则为pdFALSE
 *
 * @return  pdPASS是唯一可以返回的值
 */
 BaseType_t xQueueOverwriteFromISR(QueueHandle_t xQueue,const void *pvItemToQueue,BaseType_t *pxHigherPriorityTaskWoken);

中断函数中向队列读数据

xQueueReceiveFromISR()函数介绍

(1)中断函数中,从队列头部读取消息,并删除消息。

/**
 * @brief  中断函数中,从队列头部读取消息,并删除消息
 *
 * @param  xQueue         队列的句柄
 *        -pvBuffer       指向缓冲区的指针,接收到的项目将被复制到这个缓冲区,每次只能读取一个数据
 *        -pxHigherPriorityTaskWoken 是否成功唤醒了一个更高优先级的任务,成功唤醒该值为pdTRUE,否则为pdFALSE
 *
 * @return  如果从队列成功接收到项目,返回 pdTRUE,否则返回 pdFALSE。
 */
 BaseType_t xQueueReceiveFromISR(QueueHandle_t xQueue,void *pvBuffer,BaseType_t *pxHigherPriorityTaskWoken);

xQueuePeekFromISR()函数介绍

(1)中断函数中,从队列头部读取消息,但是不删除消息。

/**
 * @brief  中断函数中,从队列头部读取消息,但是不删除消息
 *
 * @param  xQueue         队列的句柄
 *        -pvBuffer       指向缓冲区的指针,接收到的项目将被复制到这个缓冲区,每次只能读取一个数据
 *
 * @return  如果从队列成功接收到项目,返回 pdTRUE,否则返回 pdFALSE。
 */
BaseType_t xQueuePeekFromISR(QueueHandle_t xQueue,void *pvBuffer);

中断函数中获取队列数据数量信息

uxQueueMessagesWaitingFromISR()函数介绍

(1)中断函数中,查询队列中存储的消息数。

/**
 * @brief  中断函数中,查询队列中存储的消息数
 *
 * @param  xQueue  队列的句柄
 *
 * @return  队列中可用的消息数
 */
UBaseType_t uxQueueMessagesWaitingFromISR( QueueHandle_t xQueue );

参考

(1)C站:freertos—中断管理(二)
(2)博客园:STM32 中断应用概览
(3)博客园:FreeRTOS 中断优先级配置(重要)
(4)知乎:FreeRTOS的中断管理
(5)韦东山freeRTOS快速入门:14_中断管理
(6)ST中文论坛:【经验分享】STM32之FreeRTOS:(一) 中断配置和临界段的使用
(7)Youtube:FreeRTOS on STM32 v2 - 11a Context switching
(8)C站:FreeRTOS记录(三、RTOS任务调度原理解析_Systick、PendSV、SVC)
(9)知乎:一文读懂->FreeRTOS任务调度(PendSV)
(10)微信公众号:RTOS内功修炼之FreeRTOS为何中断中只能用ISR结尾函数
(11)FreeRTOS官方文档:FreeRTOS 常见问题 -内存使用情况、启动时间&上下文切换时间
(12)C站:RTOS系列文章(2):PendSV功能,为什么需要PendSV

你可能感兴趣的:(FreeRTOS,stm32,嵌入式硬件,单片机)