今天的学习内容是任务的挂起和恢复,我使用按键控制任务挂起和恢复,又使用了外部中断控制恢复任务,踩了好几个坑,终于把流程跑顺了。

按键控制任务挂起和恢复

一、实验功能说明

这次实验是在之前双任务点灯的基础上,加了两个按键来控制任务状态,整体逻辑很简单:

  1. 两个业务任务:任务 1 控制 LED0 每秒闪烁,任务 2 控制 LED1 每秒闪烁,同时串口打印各自的运行日志;
  2. 新增一个按键任务,专门处理按键扫描,不干扰业务逻辑;
  3. PA0 按键按下时,挂起任务 1,LED0 停止闪烁,串口不再输出任务 1 的日志;
  4. 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 的任务状态管理有了更直观的理解,整理一下通用的实现要点,以后写代码可以直接套用:

  1. 提前定义好要控制的任务句柄,挂起和恢复都需要用到句柄;
  2. 单独创建一个低优先级的按键任务,专门处理按键扫描,不干扰业务逻辑;
  3. 按键扫描必须加双层判断 + 延时消抖,同时等待按键松手,避免长按重复触发;
  4. 挂起任务用vTaskSuspend(句柄),恢复任务用vTaskResume(句柄),注意任务挂起计数的问题;
  5. 按键任务里加适当延时,避免频繁扫描占用 CPU 资源。

外部中断恢复任务

一、我这次想实现的功能

  1. PA0 按键 → 挂起任务 1(LED0 停止闪烁)
  2. PC13 外部中断 → 恢复任务 1(LED0 继续闪烁)
  3. 任务 2 正常运行不受影响
  4. 串口能看到挂起、恢复、中断触发信息

结果一开始: 按 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);
    }
}

四、现在运行效果(完全正常)

  1. 开机 → LED0、LED1 同时闪烁
  2. PA0 → 任务 1 挂起 → LED0 停止闪烁
  3. PC13 → 外部中断触发 → 任务 1 恢复 → LED0 继续闪烁
  4. 串口打印清晰,不重复、不乱码、不卡死

终于完美跑通了!

五、我总结的外部中断恢复任务黄金规则(新手必看)

  1. 中断初始化一定要在 main 里调用
  2. PC13 中断号是 EXTI15_10_IRQn
  3. 中断服务函数名必须是 EXTI15_10_IRQHandler
  4. 中断里不能有任何延时
  5. 必须清除中断标志位
  6. FreeRTOS 中断里必须用 FromISR 结尾的函数
  7. 中断优先级不能太高(一般设置为 6~8 最安全)

总结

从按键扫描到外部中断,我算是真正把 FreeRTOS 任务挂起 / 恢复吃透了。 最开始按键按一次触发两次,后来中断不响应,再到系统卡死… 一步一步踩坑,一步一步修正,最后终于跑通。

写这篇就是想告诉大家: FreeRTOS 不难,坑多而已,踩完就通透了!

如果你也在做按键 + 中断控制任务,希望这篇能帮你少走 90% 的弯路。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐