(1)FreeRTOS是我一天过完的,由此回忆并且记录一下。个人认为,如果只是入门,利用STM32CubeMX是一个非常好的选择。学习完本系列课程之后,再去学习网上的一些其他课程也许会简单很多。
(2)本系列课程是使用的keil软件仿真平台,所以对于没有开发板的同学也可也进行学习。
(3)叠甲,再次强调,本系列课程仅仅用于入门。学习完之后建议还要再去寻找其他课程加深理解。
(4)本系列博客对应代码仓库:gitee仓库
(1)首先将上一篇博客的代码复制一遍下来。
(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)使用临界区,很好的保护了滴答定时器中断程序不会被干扰。但是这样就会导致一个问题,当滴答定时器中断正在执行的时候,优先级更高的中断触发,就会需要等待滴答定时器中断执行完成才能够执行。执行流程如下图:
(5)可能有朋友就会问了,如果我的中断,就是要立刻执行,怎么办呢?首先,我先需要先确认一下,这个中断立刻执行的这个立刻到底是多长时间?
<1>从FreeRTOS
的官网中我们可以知道,上下文切换时间为 84 个 CPU 周期。以STM32F103
为例,当我们设置系统时钟为72MHZ的时候,一次上下文切换时间大概是1.2us。
<2>也就是说,除非你的中断的实时性要求苛刻到比1.2us还要高,否则这个临界区是可接受的。
<3>但是,因为有朋友肯定要说了,我实时性要求就是极端的变态,怎么办呢?很简单,你将ISR
优先级设置比configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
高即可。(这个后面会细说)
(1)首先,我们需要知道为什么需要PendSV中断。我个人建议各位看看这篇博客,讲解的非常清晰,我在这里不进行赘述:PendSV功能,为什么需要PendSV
(2)如果不打算看上面这篇博客的,我就简单总结一下。因为我们知道,滴答定时器中断临界区会导致ISR的延迟处理,我们是否能够将任务切换分成两部分,一个是滴答定时器判断是否需要任务切换,一个是PendSV中断进行实际的任务切换。如果在判断过程中,来了ISR,那么我判断完成就可以马上进行ISR,然后再进行任务切换。那样就能够一定程度上提高ISR的响应速度。
(1)前面不是埋了一个坑麻,说如果
ISR
想要实时响应,能够打断滴答定时器的临界区如何处理。这个时候就需要请到一个宏了,在FreeRTOSConfig.h
文件中,有configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
宏,负责定义FreeRTOS
临界区可屏蔽的中断。例如现在这个宏设置为5,那么如果ISR
的优先级在0-4之间,即使现在现在滴答定时器在临界区中执行程序,ISR
也能够成功的打断滴答定时器,立即进入中断函数。执行流程如下图:
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5
(2)在上图我们看到,如果是
FreeRTOS
不可屏蔽中断不能使用以FromISR
结尾的API
函数。
(1)在
CM3
和CM4
内核中的NVIC控制器可以规定5中中断分组,在学习STM32
裸机教程的时候,我们会调用NVIC_PriorityGroupConfig()
函数设置中断分组。或者是在STM32CubeMX
中设置中断分组。
(2)但是,如果有细心的朋友会发现一个问题。在
STM32CubeMX
中无法设置成其他中断分组,只有中断分组4一个选项。这个是为什么呢?
因为FreeRTOS 的中断配置没有处理亚优先级这种情况,所以我们只能配置中断优先级分组为 4,直接就 16 个主优先级,使用起来也简单!
(1)中断函数要求快速执行,因此,中断中执行的函数都是立即返回的,没有等待时间。那么,我们能够发现,原来的等待时间的参数变成的
xHigherPriorityTaskWoken
。我们将如何理解xHigherPriorityTaskWoken
这个参数呢?
(2)还是回到上面关于中断和任务关系的介绍中,我们知道,滴答定时器是负责增加RTOS
时基和任务切换判断,PendSV
才是真正进行任务切换的中断函数。如果我们在ISR
中执行类似队列操作,成功唤醒了任务优先级更高的Task2
,ISR
结束之后,我们如果想要让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);
}
(5)但是,如果我们不利用
xHigherPriorityTaskWoken
这个参数,这个参数直接传入一个NULL
。ISR
中的队列操作成功唤醒了一个更高优先级的Task2
,程序的执行流程和代码如下:
xQueueSendToFrontFromISR(KeilQueueHandle, &Buf, NULL);
(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);
(1)和上面使用方法一致,只不过一个是尾插一个是头插。
/**
* @brief 中断函数队列数据头插
*
* @param xQueue 队列的句柄
* -pvItemToQueue 指向待入队数据项的指针,每次只能插入一个数据
* -pxHigherPriorityTaskWoken 是否成功唤醒了一个更高优先级的任务,成功唤醒该值为pdTRUE,否则为pdFALSE
*
* @return 如果数据头插成功,返回 pdTRUE,否则返回 errQUEUE_FULL。
*/
BaseType_t xQueueSendToFrontFromISR(QueueHandle_t xQueue,const void *pvItemToQueue,BaseType_t *pxHigherPriorityTaskWoken);
(1)覆盖队列中已经存在的数据,旨在用于长度为1的队列,队列要么为空,要么为满。
/**
* @brief 覆盖队列中已经存在的数据(旨在用于长度为1的队列,队列要么为空,要么为满)
*
* @param xQueue 队列的句柄
* -pvItemToQueue 指向待入队数据项的指针,每次只能插入一个数据
* -pxHigherPriorityTaskWoken 是否成功唤醒了一个更高优先级的任务,成功唤醒该值为pdTRUE,否则为pdFALSE
*
* @return pdPASS是唯一可以返回的值
*/
BaseType_t xQueueOverwriteFromISR(QueueHandle_t xQueue,const void *pvItemToQueue,BaseType_t *pxHigherPriorityTaskWoken);
(1)中断函数中,从队列头部读取消息,并删除消息。
/**
* @brief 中断函数中,从队列头部读取消息,并删除消息
*
* @param xQueue 队列的句柄
* -pvBuffer 指向缓冲区的指针,接收到的项目将被复制到这个缓冲区,每次只能读取一个数据
* -pxHigherPriorityTaskWoken 是否成功唤醒了一个更高优先级的任务,成功唤醒该值为pdTRUE,否则为pdFALSE
*
* @return 如果从队列成功接收到项目,返回 pdTRUE,否则返回 pdFALSE。
*/
BaseType_t xQueueReceiveFromISR(QueueHandle_t xQueue,void *pvBuffer,BaseType_t *pxHigherPriorityTaskWoken);
(1)中断函数中,从队列头部读取消息,但是不删除消息。
/**
* @brief 中断函数中,从队列头部读取消息,但是不删除消息
*
* @param xQueue 队列的句柄
* -pvBuffer 指向缓冲区的指针,接收到的项目将被复制到这个缓冲区,每次只能读取一个数据
*
* @return 如果从队列成功接收到项目,返回 pdTRUE,否则返回 pdFALSE。
*/
BaseType_t xQueuePeekFromISR(QueueHandle_t xQueue,void *pvBuffer);
(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