0. 实验准备
正点原子 STM32407ZG 探索者开发板
FreeRTOS 例程模板(可以在这一篇文章找到:STM32F407 移植 FreeRTOS)
1. 任务挂起函数 API
1.1 函数简介
动态创建任务需要使用到void vTaskSuspend( TaskHandle_t xTaskToSuspend )
函数,我们可以在 FreeRTOS 官网中查看此函数详细的文档,点击此处跳转
根据上方的描述我们可以得知,必须将 INCLUDE_vTaskSuspend
定义为 1 才能使用此函数。暂停任意任务,无论任务优先级如何,任务被暂停后将无法获取任何微控制器处理时间。对 vTaskSuspend()
的调用不会累积次数,例如:若在同一任务上调用 vTaskSuspend()
两次,将仍然仅需调用一次 vTaskResume ()
,即可恢复暂停的任务
1.2 入参详解
同样是在官方文档的下面可以查看到下图的文字
- xTaskToSuspend:被挂起的任务句柄。传递空句柄将导致调用任务被暂停
2. 任务恢复函数 API
2.1 任务中恢复的 API
2.1.1 函数详解
在任务中恢复被挂起的任务需要使用到void vTaskResume( TaskHandle_t xTaskToResume )
函数,我们可以在 FreeRTOS 官网中查看此函数详细的文档,点击此处跳转
根据上方的描述我们可以得知,必须将 INCLUDE_vTaskSuspend
定义为 1 才能使用此函数。由一次或多次调用 vTaskSuspend ()
而挂起的任务可通过单次调用 vTaskResume ()
进行恢复。这和我们在上面看到的挂起函数的 API 描述一致,无论挂起多少次只需要恢复一次即可恢复。
2.1.2 入参详解
- xTaskToResume :要被恢复的任务句柄
2.2 中断中恢复的 API
2.2.1 函数详解
在中断中恢复被挂起的任务需要使用到 xTaskResumeFromISR
函数,我们可以在 FreeRTOS 官网中查看此函数详细的文档,点击此处跳转
根据上方的描述我们可以得知,必须将 include_vTaskSuspend
和 INCLUDE_xTaskResumeFromISR
定义为 1 才能使用此函数。可以在中断处理函数内调用的恢复挂起任务的函数。由多次调用 vTaskSuspend()
中的一次调用挂起的任务可通过单次调用xTaskResumeFromISR()
进行恢复。
xTaskResumeFromISR()
通常被视为危险函数,因为其操作未被锁定。 因此,如果中断可能在任务被挂起之前到达, 从而中断丢失, 则绝对不应使用该函数 来同步任务与中断。 可使用信号量, 或者最好是直达任务通知,来避免这种可能性。
还需要注意的是:中断服务程序中要调用 FreeRTOS 的 API 函数的中断优先级不能高于 FreeRTOS 所管理的最高优先级
2.1.2 入参详解
- xTaskToResume :要被恢复的任务句柄
2.1.3 返回值详解
如果恢复任务导致上下文切换,则返回 pdTRUE,否则返回 pdFALSE。 中断服务函数可以使用此信息来确定之后是否需要上下文切换。
3. 函数挂起以及恢复步骤
根据上面的描述,我们可以整理出任务挂起与恢复的步骤有三步:
- 将宏
INCLUDE_vTaskSuspend
和INCLUDE_xTaskResumeFromISR
配置为 1 - 在任务内调用
vTaskSuspend()
挂起一个任务 - 在任务内调用
vTaskResume()
或者在中断中调用xTaskResumeFromISR ()
进行解挂
4. 编程实战
4.1 实验设计
实现如下的功能:
- 设计四个任务:start_task、task1、task2、task3
- start_task:用来创建其他的三个任务
- task1:实现 LED0 每 500ms 闪烁一次
- task2:实现 LED1 每 500ms 闪烁一次
- task3:判断按键按下逻辑,KEY0 按下,挂起 task1,按下 KEY1 在任务中恢复 task1
- 外部中断:按下 KEY2,在中断中恢复 task1
这里主要讲解如何挂起任务和恢复任务,其他的代码将会一笔带过,创建任务的内容可以参考我的这两篇博客:FreeRTOS 动态创建任务与删除 和 FreeRTOS 静态创建任务与删除,这里采用动态创建任务的方式
4.2 编写代码
首先打开我们的 FreeRTOS 例程模板(在文章顶部的实验准备中可以找到),打开 FREERTOS_config.h
,将宏 INCLUDE_vTaskSuspend
和 INCLUDE_xTaskResumeFromISR
配置为 1 ,如下图所示
然后打开 freertos_demo.c
,如下图所示
此时的freertos_demo.c
是测试 FreeRTOS 是否移植成功的代码,这里全部进行删除,替换为如下的代码,下面的代码已经完成了 start_task、task1、task2 的任务创建以及逻辑实现
#include "freertos_demo.h"
#include "./SYSTEM/usart/usart.h"
#include "./BSP/LED/led.h"
#include "./BSP/LCD/lcd.h"
#include "./BSP/KEY/key.h"
/*FreeRTOS*********************************************************************************************/
#include "FreeRTOS.h"
#include "task.h"
/******************************************************************************************************/
/*FreeRTOS配置*/
/* START_TASK 任务 配置
* 包括: 任务句柄 任务优先级 堆栈大小 创建任务
*/
#define START_TASK_PRIO 1 /* 任务优先级 */
#define START_STK_SIZE 128 /* 任务堆栈大小 */
TaskHandle_t StartTask_Handler; /* 任务句柄 */
void start_task(void *pvParameters); /* 任务函数 */
/* TASK1 任务 配置
* 包括: 任务句柄 任务优先级 堆栈大小 创建任务
*/
#define TASK1_PRIO 2 /* 任务优先级 */
#define TASK1_STK_SIZE 128 /* 任务堆栈大小 */
TaskHandle_t Task1Task_Handler; /* 任务句柄 */
void task1(void *pvParameters); /* 任务函数 */
/* TASK2 任务 配置
* 包括: 任务句柄 任务优先级 堆栈大小 创建任务
*/
#define TASK2_PRIO 3 /* 任务优先级 */
#define TASK2_STK_SIZE 128 /* 任务堆栈大小 */
TaskHandle_t Task2Task_Handler; /* 任务句柄 */
void task2(void *pvParameters); /* 任务函数 */
/* TASK3 任务 配置
* 包括: 任务句柄 任务优先级 堆栈大小 创建任务
*/
#define TASK3_PRIO 4 /* 任务优先级 */
#define TASK3_STK_SIZE 128 /* 任务堆栈大小 */
TaskHandle_t Task3Task_Handler; /* 任务句柄 */
void task3(void *pvParameters); /* 任务函数 */
/******************************************************************************************************/
/**
* @brief FreeRTOS例程入口函数
* @param 无
* @retval 无
*/
void freertos_demo(void)
{
xTaskCreate((TaskFunction_t )start_task, /* 任务函数 */
(const char* )"start_task", /* 任务名称 */
(uint16_t )START_STK_SIZE, /* 任务堆栈大小 */
(void* )NULL, /* 传入给任务函数的参数 */
(UBaseType_t )START_TASK_PRIO, /* 任务优先级 */
(TaskHandle_t* )&StartTask_Handler); /* 任务句柄 */
vTaskStartScheduler(); /* 开启任务调度器 */
}
/**
* @brief start_task
* @param pvParameters : 传入参数(未用到)
* @retval 无
*/
void start_task(void *pvParameters)
{
taskENTER_CRITICAL(); /* 进入临界区 */
/* 创建任务1 */
xTaskCreate((TaskFunction_t )task1,
(const char* )"task1",
(uint16_t )TASK1_STK_SIZE,
(void* )NULL,
(UBaseType_t )TASK1_PRIO,
(TaskHandle_t* )&Task1Task_Handler);
/* 创建任务2 */
xTaskCreate((TaskFunction_t )task2,
(const char* )"task2",
(uint16_t )TASK2_STK_SIZE,
(void* )NULL,
(UBaseType_t )TASK2_PRIO,
(TaskHandle_t* )&Task2Task_Handler);
/* 创建任务3 */
xTaskCreate((TaskFunction_t )task3,
(const char* )"task3",
(uint16_t )TASK3_STK_SIZE,
(void* )NULL,
(UBaseType_t )TASK3_PRIO,
(TaskHandle_t* )&Task3Task_Handler);
vTaskDelete(StartTask_Handler); /* 删除开始任务 */
taskEXIT_CRITICAL(); /* 退出临界区 */
}
/* 任务一,实现LED0每500ms翻转一次 */
void task1( void * pvParameters )
{
uint8_t count = 0;
while(1)
{
count++;
printf("task1:%d\r\n",count);
LED0_TOGGLE();
vTaskDelay(500);
}
}
/* 任务二,实现LED1每500ms翻转一次 */
void task2( void * pvParameters )
{
uint8_t count = 0;
while(1)
{
count++;
printf("task2:%d\r\n",count);
LED1_TOGGLE();
vTaskDelay(500);
}
}
/* 任务三,KEY0 按下,挂起 task1,按下 KEY1 在任务中恢复 task1 */
void task3( void * pvParameters )
{
uint8_t key = 0;
while(1)
{
key = key_scan(0);
if(key == KEY0_PRES)
{
}
if(key == KEY1_PRES)
{
}
vTaskDelay(10);
}
}
4.2.1 task3 的任务编写
在上面的模板中,在 task3 中编写了 KEY0 和 KEY1 的按键检测,下面是具体的逻辑。
/* 任务三,判断按键KEY0,按下KEY0删除task1 */
void task3( void * pvParameters )
{
uint8_t key = 0;
while(1)
{
key = key_scan(0);
if(key == KEY0_PRES)
{
printf("挂起了任务1");
vTaskSuspend(Task1Task_Handler);
}
if(key == KEY1_PRES)
{
printf("在任务中恢复了任务1");
vTaskResume(Task1Task_Handler);
}
vTaskDelay(10);
}
}
4.2.2 外部中断的编写
由于需要使用到外部中断,我们可以直接从正点原子的外部中断例程中拷贝外部中断相关的驱动文件到项目文件夹中,如下图所示
然后增加外部中断的驱动文件到项目中,如下图所示
然后打开exit.c,修改为下面的代码,主要是删除了用不上的代码,保留框架结构
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
#include "./BSP/KEY/key.h"
#include "./BSP/EXTI/exti.h"
/**
* @brief KEY2 外部中断服务程序
* @param 无
* @retval 无
*/
void KEY2_INT_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(KEY2_INT_GPIO_PIN); /* 调用中断处理公用函数 清除KEY2所在中断线 的中断标志位,中断下半部在HAL_GPIO_EXTI_Callback执行 */
__HAL_GPIO_EXTI_CLEAR_IT(KEY2_INT_GPIO_PIN); /* HAL库默认先清中断再处理回调,退出时再清一次中断,避免按键抖动误触发 */
}
/**
* @brief 中断服务程序中需要做的事情
* 在HAL库中所有的外部中断服务函数都会调用此函数
* @param GPIO_Pin:中断引脚号
* @retval 无
*/
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
delay_ms(20); /* 消抖 */
switch(GPIO_Pin)
{
case KEY2_INT_GPIO_PIN:
if (KEY2 == 0)
{
}
break;
default : break;
}
}
/**
* @brief 外部中断初始化程序
* @param 无
* @retval 无
*/
void extix_init(void)
{
GPIO_InitTypeDef gpio_init_struct;
gpio_init_struct.Pin = KEY2_INT_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_IT_FALLING; /* 下降沿触发 */
gpio_init_struct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(KEY2_INT_GPIO_PORT, &gpio_init_struct); /* KEY2配置为下降沿触发中断 */
HAL_NVIC_SetPriority(KEY2_INT_IRQn, 2, 2); /* 抢占2,子优先级2 */
HAL_NVIC_EnableIRQ(KEY2_INT_IRQn); /* 使能中断线2 */
}
然后在主函数中添加外部中断初始化函数,如下图所示
#include "./BSP/EXTI/exti.h"
extix_init(); /* 外部中断初始化 */
进行编译,查看是否有错误。没有错误和警告的时候,开始编写在中断中恢复的任务的代码,有错误的先解决错误。
由于我们需要在中断中调用 FreeRTOS 的函数,并且要拿到 task1 的任务句柄,所以在 exti.c
的头部增加以下的头文件和定义
#include "FreeRTOS.h"
#include "task.h"
extern TaskHandle_t Task1Task_Handler;
参考官网的例子
void vAnExampleISR( void )
{
BaseType_t xYieldRequired;
// Resume the suspended task.
xYieldRequired = xTaskResumeFromISR( xHandle );
// We should switch context so the ISR returns to a different task.
// NOTE: How this is done depends on the port you are using. Check
// the documentation and examples for your port.
portYIELD_FROM_ISR( xYieldRequired );
}
我们在 exti.c
的中断服务函数中,编写下面的代码。根据官网的例子可以知道,在返回值为 pdTRUE
的时候,采用 portYIELD_FROM_ISR()
来进行任务切换
/**
* @brief 中断服务程序中需要做的事情
* 在HAL库中所有的外部中断服务函数都会调用此函数
* @param GPIO_Pin:中断引脚号
* @retval 无
*/
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
delay_ms(20); /* 消抖 */
switch(GPIO_Pin)
{
BaseType_t xYieldRequired;
case KEY2_INT_GPIO_PIN:
if (KEY2 == 0)
{
printf("在中断中恢复了任务1");
xYieldRequired = xTaskResumeFromISR(Task1Task_Handler);
}
if(xYieldRequired == pdTRUE)
{
portYIELD_FROM_ISR( xYieldRequired );
}
break;
default : break;
}
}
4.2.3 验证
下载程序到开发板,然后连接串口到电脑,查看输出现象
然后按下 KEY0 ,如下图所示,打印出挂起了任务,并且 task1 并没有继续输出了,此时 task1 输出的是 8
按下 KEY1 如下图所示,可以看到任务被恢复了,task1 输出了 9,说明是接着之前的断点进行运行的
再次按下 KEY0 挂起 task1 ,这次task1输出的是 6
然后按下 KEY2 发生了报错,如下图所示
4.2.4 修改代码
4.2.4.1 报错为 port.c, 807
报错信息如下,这代表中断的优先级位没有全部指定为抢占优先级位
Error: ..\..\Middlewares\FreeRTOS\portable\RVDS\ARM_CM4F\port.c, 807
打开 port.c
找到 807 行,如下图所示
在该文件的 789 行的注释中有一个网址解释的很清楚,点击打开
根据上方的描述可知,建议将所有优先级位都指定为抢占优先级位, 不保留任何优先级位作为子优先级位。使用 STM32 和 STM32 驱动器库, 可以调用 NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 )
来确保所有优先级位都被指定为抢占优先级位,这一步需要在启动 FreeRTOS 前完成。
在主函数中找到 HAL_Init()
,然后按照下图的方式找到此函数的定义
在下图红框中的代码处,修改为 HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4)
,此代码会将中断的优先级位全部指定为抢占优先级位
修改完成后,可以解决这个报错
4.2.4.2 报错为 port.c, 791
报错如下
Error: ..\..\Middlewares\FreeRTOS\portable\RVDS\ARM_CM4F\port.c, 791
打开 port.c
找到 791 行,如下图
这里的注释中有一个网址解释的很清楚,点击打开
根据上方的描述可知,调用这些函数的中断的逻辑优先级不能高于在 FreeRTOSConfig.h
头文件中定义的configMAX_SYSCALL_INTERRUPT_PRIORITY
的优先级。在中断中,0 的优先级是最高的
在此项目中,我们配置的 FreeRTOS 可管理的中断优先级为 5-15 ,如下图所示
而我们配置的外部中断的子优先级为 2,抢占优先级为 2,如下图所示
改为子优先级为 0,抢占优先级为 5,如下图所示
4.2.5 再次验证
下载编译完成的程序到开发板中,然后按下 KEY0 挂起 task1 ,被挂起的时候 task1 输出 8
按下 KEY2 ,可以发现task1被恢复了,而且是从 9 开始输出的,说明是接着之前的断点进行运行的
至此,FreeRTOS 任务挂起与恢复实验成功!