FreeRTOS——按键控制任务的挂起和恢复
今天的学习内容是任务的挂起和恢复,我使用按键控制任务挂起和恢复,又使用了外部中断控制恢复任务,踩了好几个坑,终于把流程跑顺了。
按键控制任务挂起和恢复
一、实验功能说明
这次实验是在之前双任务点灯的基础上,加了两个按键来控制任务状态,整体逻辑很简单:
- 两个业务任务:任务 1 控制 LED0 每秒闪烁,任务 2 控制 LED1 每秒闪烁,同时串口打印各自的运行日志;
- 新增一个按键任务,专门处理按键扫描,不干扰业务逻辑;
- PA0 按键按下时,挂起任务 1,LED0 停止闪烁,串口不再输出任务 1 的日志;
- PC13 按键按下时,恢复任务 1,LED0 继续闪烁,日志恢复输出。
本来以为照着教程写就能跑通,结果一开始按一次按键会触发两次打印,搞了了半天才搞明白问题出在哪,后面也会把踩坑的过程写出来。
二、完整实验代码
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "FreeRTOS.h"
#include "task.h"
#include "key.h"
//===================== 开始任务配置 =====================
#define START_TASK_SIZE 128
#define START_TASK_PRIO 1
TaskHandle_t StartTask_Hander;
void start_task( void * pvParameters );
//===================== 任务1配置 =====================
#define TASK1_TASK_SIZE 128
#define TASK1_TASK_PRIO 4
TaskHandle_t TASK1Task_Hander;
void task1_task( void * pvParameters );
//===================== 任务2配置 =====================
#define TASK2_TASK_SIZE 128
#define TASK2_TASK_PRIO 3
TaskHandle_t Task2Task_Hander;
void task2_task( void * pvParameters );
//===================== 任务key配置 =====================
#define KEY_TASK_SIZE 128
#define KEY_TASK_PRIO 2
TaskHandle_t KeyTask_Hander;
void key_task( void * pvParameters );
//主函数
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//中断分组4
delay_init();
uart_init(115200);
LED_Init();
Key_Init();
//创建开始任务
xTaskCreate( (TaskFunction_t) start_task,
(char *) "start_task",
(uint16_t) START_TASK_SIZE,
(void *) NULL,
(UBaseType_t) START_TASK_PRIO,
(TaskHandle_t *) &StartTask_Hander );
vTaskStartScheduler(); //开启任务调度
}
//开始任务:负责创建所有业务任务,随后自删除
void start_task( void * pvParameters )
{
//创建任务1
xTaskCreate( (TaskFunction_t) task1_task,
(char *) "task1_task",
(uint16_t) TASK1_TASK_SIZE,
(void *) NULL,
(UBaseType_t) TASK1_TASK_PRIO,
(TaskHandle_t *) &TASK1Task_Hander );
//创建任务2
xTaskCreate( (TaskFunction_t) task2_task,
(char *) "task2_task",
(uint16_t) TASK2_TASK_SIZE,
(void *) NULL,
(UBaseType_t) TASK2_TASK_PRIO,
(TaskHandle_t *) &Task2Task_Hander );
//创建任务key
xTaskCreate( (TaskFunction_t) key_task,
(char *) "key_task",
(uint16_t) KEY_TASK_SIZE,
(void *) NULL,
(UBaseType_t) KEY_TASK_PRIO,
(TaskHandle_t *) &KeyTask_Hander );
//开始任务完成使命,自我删除
vTaskDelete(StartTask_Hander);
}
//任务1:LED0每秒闪烁并打印日志
void task1_task( void * pvParameters )
{
char task1_num = 0;
while(1)
{
task1_num++;
LED0 = -LED0;
vTaskDelay(1000);
printf("Task1 is running %d\r\n",task1_num);
}
}
//任务2:LED1每秒闪烁并打印日志
void task2_task( void * pvParameters )
{
char task2_num =0;
while(1)
{
task2_num++;
LED1 = -LED1;
vTaskDelay(1000);
printf("Task2 is running %d\r\n",task2_num);
}
}
//按键任务
void key_task( void * pvParameters )
{
while(1)
{
// 处理 PA0 按键:挂起任务1
if( GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == 0 )
{
vTaskDelay(20); // 消抖延时
if( GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == 0 )
{
vTaskSuspend( TASK1Task_Hander );
printf("Task1 is Supend \r\n");
// 等待按键松手,避免长按重复触发
while( GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == 0 )
{
vTaskDelay(10);
}
}
}
// 处理 PC13 按键:恢复任务1
if(GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_13) == 0)
{
vTaskDelay(20); // 消抖延时
if(GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_13) == 0)
{
vTaskResume( TASK1Task_Hander );
printf("Task1 is Resume \r\n");
// 等待按键松手,避免长按重复触发
while( GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_13) == 0 )
{
vTaskDelay(10);
}
}
}
vTaskDelay(10);
}
}
注意:这里只附了主函数代码,按键配置和led的配置,前面的博客里面都有,去找找,添加进工程就好了。
三、代码说明
1. 任务配置部分
这次我还是用了动态创建任务的方式,四个任务分别定义了栈大小和优先级:任务 1 优先级最高,任务 2 次之,按键任务优先级最低,这样不会出现按键任务抢占业务任务的情况。每个任务都定义了句柄,后面挂起和恢复都要靠这个句柄来操作。
2. main 函数和开始任务
和之前的例子一样,main 函数只做硬件初始化和创建开始任务,调度器启动后就交给 FreeRTOS 管理。开始任务负责创建三个业务任务,创建完成后就自我删除,不占用系统资源,这也是工程里比较规范的写法。
3. 业务任务逻辑
任务 1 和任务 2 的逻辑没什么变化,都是循环翻转 LED,同时打印运行日志,去掉了之前删除任务的逻辑,改成了按键控制挂起恢复。
4. 按键任务(核心部分,也是之前踩坑的地方)
一开始写的按键任务逻辑很简单,就是判断按键按下,延时消抖后直接执行挂起 / 恢复操作,结果按一次按键会触发两次打印,而且长按的时候会反复执行,甚至出现任务多次挂起后恢复不了的情况。
后来查了资料才明白,原来的消抖逻辑不完善,而且没有等待按键松手,导致按键按下的过程中,任务循环会多次检测到低电平,重复执行操作。修改后的代码加了两层判断和松手等待,现在按一次按键只会触发一次操作,长按也不会重复执行了。
四、运行现象和踩坑修复记录
1. 预期现象
程序运行后,LED0 和 LED1 都会正常闪烁,串口同时输出两个任务的日志。按下 PA0,LED0 停止闪烁,串口不再输出任务 1 的日志;按下 PC13,LED0 恢复闪烁,日志也恢复输出。

2. 踩坑修复过程
-
坑 1:按一次按键触发两次打印 一开始的代码只加了简单的延时消抖,没有等待按键松手,导致按键按下的过程中,任务循环多次检测到低电平,重复执行
printf和挂起操作。解决方法是在按键触发后,加一个while循环等待按键松手,直到检测到高电平才退出,保证一次按下只触发一次逻辑。 -
坑 2:按键消抖延时太长,反应迟钝 一开始用了 100ms 的消抖延时,结果按键按下后要等好久才会响应,改成 20ms 就正常了,机械按键的抖动一般只有几毫秒,20ms 足够消抖了。
-
坑 3:任务多次挂起后恢复不了 一开始长按按键的时候,任务会被多次挂起,FreeRTOS 里任务的挂起是计数的,挂起几次就要恢复几次才能正常运行,所以单次按恢复键没反应。加了松手等待后,长按不会重复挂起,这个问题就解决了。
五、总结:按键控制任务挂起 / 恢复的关键要点
这次实验让我对 FreeRTOS 的任务状态管理有了更直观的理解,整理一下通用的实现要点,以后写代码可以直接套用:
- 提前定义好要控制的任务句柄,挂起和恢复都需要用到句柄;
- 单独创建一个低优先级的按键任务,专门处理按键扫描,不干扰业务逻辑;
- 按键扫描必须加双层判断 + 延时消抖,同时等待按键松手,避免长按重复触发;
- 挂起任务用
vTaskSuspend(句柄),恢复任务用vTaskResume(句柄),注意任务挂起计数的问题; - 按键任务里加适当延时,避免频繁扫描占用 CPU 资源。
外部中断恢复任务
一、我这次想实现的功能
- PA0 按键 → 挂起任务 1(LED0 停止闪烁)
- PC13 外部中断 → 恢复任务 1(LED0 继续闪烁)
- 任务 2 正常运行不受影响
- 串口能看到挂起、恢复、中断触发信息
结果一开始: 按 PA0 能挂起,但按 PC13 完全没反应! 串口不打印、灯不亮、中断像死了一样…
二、我踩过的真实大坑
1. 写了中断初始化,但 main 函数没调用!
我写了 Exti_Init(),结果在 main 里忘了写:
Exti_Init();
中断都没开启,你按烂按键都没用啊! 这是新手最容易犯的低级错误。
2. 中断通道写错(一开始写了 EXTI0_IRQn)
PC13 是 EXTI13,属于 EXTI10~EXTI15 中断号必须是:
EXTI15_10_IRQn
我之前写成了 EXTI0_IRQn,完全不匹配,中断根本进不去。
3. 中断服务函数名写错
PC13 必须用:
void EXTI15_10_IRQHandler(void)
名字错一个字母都进不去中断。
4. 中断里不能加延时!
我最开始在中断里写了:
delay_xms(20);
直接导致系统卡死、死机、不调度。 中断里严禁延时!快进快出!
5. 忘记清除中断标志位
没写这句:
EXTI_ClearITPendingBit(EXTI_Line13);
后果:一触发就死循环进中断,系统直接卡死。
6. FreeRTOS 中断里必须用 FromISR 函数
最开始我直接写:
vTaskResume(TASK1Task_Hander);
在中断里绝对不行! 必须用:
xTaskResumeFromISR(TASK1Task_Hander);
三、最终正确代码(我现在跑通的版本)
我直接把我最终能用的代码放出来,复制就能跑。
1. exti.c (中断初始化 + 中断服务函数)
#include "exti.h"
#include "stm32f10x.h"
#include "led.h"
#include "key.h"
#include "FreeRTOS.h"
#include "task.h"
void Exti_Init(void)
{
EXTI_InitTypeDef EXTIInitstruct;
NVIC_InitTypeDef NVICInitstruct;
Key_Init();
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOC, GPIO_PinSource13);
EXTIInitstruct.EXTI_Line = EXTI_Line13;
EXTIInitstruct.EXTI_Mode = EXTI_Mode_Interrupt;
EXTIInitstruct.EXTI_Trigger = EXTI_Trigger_Falling;
EXTIInitstruct.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTIInitstruct);
// 重点:PC13 必须用 EXTI15_10_IRQn
NVICInitstruct.NVIC_IRQChannel = EXTI15_10_IRQn;
NVICInitstruct.NVIC_IRQChannelPreemptionPriority = 6;
NVICInitstruct.NVIC_IRQChannelSubPriority = 0;
NVICInitstruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVICInitstruct);
}
// 外部中断句柄
extern TaskHandle_t TASK1Task_Hander;
void EXTI15_10_IRQHandler(void)
{
BaseType_t YieldRequired = pdFALSE;
if(EXTI_GetITStatus(EXTI_Line13) != RESET)
{
// 中断里恢复任务必须用 FromISR
YieldRequired = xTaskResumeFromISR(TASK1Task_Hander);
printf("外部中断触发 → 恢复任务1\r\n");
// 清除标志位,必须写!
EXTI_ClearITPendingBit(EXTI_Line13);
}
portYIELD_FROM_ISR(YieldRequired);
}
2. main.c (我最终完整版)
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "FreeRTOS.h"
#include "task.h"
#include "key.h"
#include "exti.h"
//===================== 开始任务 =====================
#define START_TASK_SIZE 128
#define START_TASK_PRIO 1
TaskHandle_t StartTask_Hander;
void start_task( void * pvParameters );
//===================== 任务1 =====================
#define TASK1_TASK_SIZE 128
#define TASK1_TASK_PRIO 4
TaskHandle_t TASK1Task_Hander;
void task1_task( void * pvParameters );
//===================== 任务2 =====================
#define TASK2_TASK_SIZE 128
#define TASK2_TASK_PRIO 3
TaskHandle_t Task2Task_Hander;
void task2_task( void * pvParameters );
//===================== 按键任务 =====================
#define KEY_TASK_SIZE 128
#define KEY_TASK_PRIO 2
TaskHandle_t KeyTask_Hander;
void key_task( void * pvParameters );
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
delay_init();
uart_init(115200);
LED_Init();
Key_Init();
Exti_Init(); // 必须开启外部中断!!!
xTaskCreate( start_task, "start_task", START_TASK_SIZE, NULL, START_TASK_PRIO, &StartTask_Hander );
vTaskStartScheduler();
}
void start_task( void * pvParameters )
{
xTaskCreate( task1_task, "task1_task", TASK1_TASK_SIZE, NULL, TASK1_TASK_PRIO, &TASK1Task_Hander );
xTaskCreate( task2_task, "task2_task", TASK2_TASK_SIZE, NULL, TASK2_TASK_PRIO, &Task2Task_Hander );
xTaskCreate( key_task, "key_task", KEY_TASK_SIZE, NULL, KEY_TASK_PRIO, &KeyTask_Hander );
vTaskDelete(StartTask_Hander);
}
void task1_task( void * pvParameters )
{
char task1_num = 0;
while(1)
{
task1_num++;
LED0 = ~LED0;
vTaskDelay(1000);
printf("Task1 is running %d\r\n",task1_num);
}
}
void task2_task( void * pvParameters )
{
char task2_num =0;
while(1)
{
task2_num++;
LED1 = ~LED1;
vTaskDelay(1000);
printf("Task2 is running %d\r\n",task2_num);
}
}
void key_task( void * pvParameters )
{
while(1)
{
if( GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == 0 )
{
vTaskDelay(20);
if( GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == 0 )
{
vTaskSuspend( TASK1Task_Hander );
printf("任务1已挂起\r\n");
while( GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == 0 )
{
vTaskDelay(10);
}
}
}
vTaskDelay(10);
}
}
四、现在运行效果(完全正常)
- 开机 → LED0、LED1 同时闪烁
- 按 PA0 → 任务 1 挂起 → LED0 停止闪烁
- 按 PC13 → 外部中断触发 → 任务 1 恢复 → LED0 继续闪烁
- 串口打印清晰,不重复、不乱码、不卡死
终于完美跑通了!

五、我总结的外部中断恢复任务黄金规则(新手必看)
- 中断初始化一定要在 main 里调用
- PC13 中断号是 EXTI15_10_IRQn
- 中断服务函数名必须是 EXTI15_10_IRQHandler
- 中断里不能有任何延时
- 必须清除中断标志位
- FreeRTOS 中断里必须用 FromISR 结尾的函数
- 中断优先级不能太高(一般设置为 6~8 最安全)
总结
从按键扫描到外部中断,我算是真正把 FreeRTOS 任务挂起 / 恢复吃透了。 最开始按键按一次触发两次,后来中断不响应,再到系统卡死… 一步一步踩坑,一步一步修正,最后终于跑通。
写这篇就是想告诉大家: FreeRTOS 不难,坑多而已,踩完就通透了!
如果你也在做按键 + 中断控制任务,希望这篇能帮你少走 90% 的弯路。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)