目录
通过之前入门系列的学习,我们对FreeRTOS的移植已经有了一个大概的概念,那么我们下面就给其投入到实际的使用当中。
1. 程序设计
创建两个任务,一个LED任务实现电平的翻转,另一任务通过按键进行LED任务的挂起和恢复。
2. 工程创建
这里不在一一介绍每个文件夹的作用,详细可以了解:
根据上面的移植好的模版,我们进行后续任务相关函数的移植,首先对于LED.c的函数,我们增加一个电平翻转的功能,方便后续调用:
//红灯电平翻转
void Toggle_LED_R(void)
{
BitAction LED_R = (BitAction)(1 - GPIO_ReadOutputDataBit(LED1_GPIO_PORT, LED1_GPIO_PIN));
GPIO_WriteBit(LED1_GPIO_PORT, LED1_GPIO_PIN, LED_R);
}
//绿灯电平翻转
void Toggle_LED_G(void)
{
BitAction LED_G = (BitAction)(1 - GPIO_ReadOutputDataBit(LED2_GPIO_PORT, LED2_GPIO_PIN));
GPIO_WriteBit(LED2_GPIO_PORT, LED2_GPIO_PIN, LED_G);
}
//蓝灯电平翻转
void Toggle_LED_B(void)
{
BitAction LED_B = (BitAction)(1 - GPIO_ReadOutputDataBit(LED3_GPIO_PORT, LED3_GPIO_PIN));
GPIO_WriteBit(LED3_GPIO_PORT, LED3_GPIO_PIN, LED_B);
}
然后创建一个KEY.c函数,就是简单的按键触发函数,检测按键是否按下:
#include "Key.h"
//初始化配置
void Key_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/*开启按键端口的时钟*/
RCC_APB2PeriphClockCmd(KEY1_GPIO_CLK|KEY2_GPIO_CLK,ENABLE);
//选择按键的引脚
GPIO_InitStructure.GPIO_Pin = KEY1_GPIO_PIN;
// 设置按键的引脚为浮空输入
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
//使用结构体初始化按键
GPIO_Init(KEY1_GPIO_PORT, &GPIO_InitStructure);
//选择按键的引脚
GPIO_InitStructure.GPIO_Pin = KEY2_GPIO_PIN;
//设置按键的引脚为浮空输入
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
//使用结构体初始化按键
GPIO_Init(KEY2_GPIO_PORT, &GPIO_InitStructure);
}
//检测按键是否按下
uint8_t Key_Scan(GPIO_TypeDef* GPIOx,uint16_t GPIO_Pin)
{
/*检测是否有按键按下 */
if(GPIO_ReadInputDataBit(GPIOx,GPIO_Pin) == KEY_ON )
{
/*等待按键释放 */
while(GPIO_ReadInputDataBit(GPIOx,GPIO_Pin) == KEY_ON);
return KEY_ON;
}
else
return KEY_OFF;
}
然后其中宏定义相关存放在 KEY.h 文件当中:
#ifndef __KEY_H
#define __KEY_H
#include "stm32f10x.h"
// 引脚定义
#define KEY1_GPIO_CLK RCC_APB2Periph_GPIOA
#define KEY1_GPIO_PORT GPIOA
#define KEY1_GPIO_PIN GPIO_Pin_0
#define KEY2_GPIO_CLK RCC_APB2Periph_GPIOC
#define KEY2_GPIO_PORT GPIOC
#define KEY2_GPIO_PIN GPIO_Pin_13
/** 按键按下标置宏
* 按键按下为高电平,设置 KEY_ON=1, KEY_OFF=0
* 若按键按下为低电平,把宏设置成KEY_ON=0 ,KEY_OFF=1 即可
*/
#define KEY_ON 1
#define KEY_OFF 0
void Key_GPIO_Config(void);
uint8_t Key_Scan(GPIO_TypeDef* GPIOx,uint16_t GPIO_Pin);
#endif /* __KEY_H */
两个外设的代码创建完了,我们回到main.c文件,先完善头文件:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "LED.h"
#include "Usart.h"
#include "Key.h"
/* FreeRTOS头文件 */
#include "FreeRTOS.h"
#include "task.h"
然后为了方便管理初始化文件,我们创建一个函数,将初始化相关的函数都存放到这个函数中,这样主函数管理比较方便:
//初始化声明
static void All_Function_Init(void)
{
/*
* STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
* 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
* 都统一用这个优先级分组,千万不要再分组,切忌。
*/
NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
/* LED 初始化 */
LED_GPIO_Config();
/* 串口初始化 */
USART_Config();
//按键初始化
Key_GPIO_Config();
}
我们知道创建任务为了方便管理,我们需要创建一个任务句柄,我们这里先不考虑LED和KEY的任务创建,先创建一个用于创建任务的任务句柄:
static TaskHandle_t AppTaskCreate_Handle = NULL; /* 创建任务句柄 */
我们创建一个用于创建别的任务的任务,这个任务的作用就是通过临界区访问将我们需要的如LED,KEY等任务能够完整的创建,不被中断或者在任务创建时被任务调度打扰,创建完成后就可以将创建任务的这个任务通过调用刚刚创建的句柄删除了,这个任务存在的意义就是保护我们所需要的任务在创建时不被打扰:
//任务创建函数
static void AppTaskCreate(void)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
taskENTER_CRITICAL(); //进入临界区
/***这里是一些我们需要创建的任务***/
vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
taskEXIT_CRITICAL(); //退出临界区
}
对于临界区的概念,你可以把他理解为一个房子,保护内部正在做的事情,防止被外部打扰。
详细可以了解:
当然,虽然这个任务用一下就删除了,但是不妨碍他是一个任务,既然是任务,我们就需要给它分配,栈空间等参数,这里在主函数里进行分配:
int main(void)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
All_Function_Init();//硬件初始化
while (1)
{
/* 创建AppTaskCreate任务 */
xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate, /* 任务入口函数 */
(const char* )"AppTaskCreate",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL,/* 任务入口函数参数 */
(UBaseType_t )1, /* 任务的优先级 */
(TaskHandle_t* )&AppTaskCreate_Handle);/* 任务控制块指针 */
/* 启动任务调度 */
if(pdPASS == xReturn)
vTaskStartScheduler(); /* 启动任务,开启调度 */
else
return -1;
}
}
完整的main函数:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "LED.h"
#include "Usart.h"
#include "Key.h"
/* FreeRTOS头文件 */
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
/*
* 任务句柄是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄
* 以后我们要想操作这个任务都需要通过这个任务句柄,如果是自身的任务操作自己,那么
* 这个句柄可以为NULL。
*/
static TaskHandle_t AppTaskCreate_Handle = NULL; /* 创建任务句柄 */
/********************************** 内核对象句柄 *********************************/
/*
* 信号量,消息队列,事件标志组,软件定时器这些都属于内核的对象,要想使用这些内核
* 对象,必须先创建,创建成功之后会返回一个相应的句柄。实际上就是一个指针,后续我
* 们就可以通过这个句柄操作这些内核对象。
*
* 内核对象说白了就是一种全局的数据结构,通过这些数据结构我们可以实现任务间的通信,
* 任务间的事件同步等各种功能。至于这些功能的实现我们是通过调用这些内核对象的函数
* 来完成的
*
*/
/******************************* 宏定义 ************************************/
/*
* 当我们在写应用程序的时候,可能需要用到一些宏定义。
*/
//一些函数声明
static void AppTaskCreate(void);/* 用于创建任务 */
static void All_Function_Init(void);/* 用于初始化板载相关资源 */
int main(void)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
All_Function_Init();//硬件初始化
while (1)
{
/* 创建AppTaskCreate任务 */
xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate, /* 任务入口函数 */
(const char* )"AppTaskCreate",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL,/* 任务入口函数参数 */
(UBaseType_t )1, /* 任务的优先级 */
(TaskHandle_t* )&AppTaskCreate_Handle);/* 任务控制块指针 */
/* 启动任务调度 */
if(pdPASS == xReturn)
vTaskStartScheduler(); /* 启动任务,开启调度 */
else
return -1;
}
}
//任务创建函数
static void AppTaskCreate(void)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
taskENTER_CRITICAL(); //进入临界区
vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
taskEXIT_CRITICAL(); //退出临界区
}
//初始化声明
static void All_Function_Init(void)
{
/*
* STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
* 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
* 都统一用这个优先级分组,千万不要再分组,切忌。
*/
NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
/* LED 初始化 */
LED_GPIO_Config();
/* 串口初始化 */
USART_Config();
//按键初始化
Key_GPIO_Config();
}
运行一下没有报错,不过由于我们没有给它其他任务,所以现在也没有实验详细,对于这个工程我们先保存一下,因为后续实验都直接在这个工程上进行操作,省的每次都要创建一遍。
3. 功能实现
上面我们已经把工程的框架搭好了,下面开始对功能开始实现。
首先,我们上面也提到了,需要创建两个任务,那么就需要两个任务句柄:
static TaskHandle_t Test_Task_Handle = NULL;/* LED任务句柄 */
static TaskHandle_t KEY_Task_Handle = NULL;/* KEY任务句柄 */
既然句柄有了,就需要对任务主体进行设置,创建一个LED任务,每隔500ms亮灭一次:
static void Test_Task(void* parameter)
{
while (1)
{
LED1_ON;
printf("Test_Task Running,LED1_ON!\r\n");
vTaskDelay(500); /* 延时500个tick */
LED1_OFF;
printf("Test_Task Running,LED1_OFF!\r\n");
vTaskDelay(500); /* 延时500个tick */
}
}
然后是对按键任务的编写,我们设置两个按键一个调用vTaskSuspend()用来挂起任务,一个调用vTaskResume()用来恢复任务:
static void KEY_Task(void* parameter)
{
while (1)
{
if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON )
{/* K1 被按下 */
printf("挂起Test任务!\r\n");
vTaskSuspend(Test_Task_Handle);/* 挂起LED任务 */
printf("挂起Test任务成功!\r\n");
}
if( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON )
{/* K2 被按下 */
printf("恢复Test任务!\r\n");
vTaskResume(Test_Task_Handle);/* 恢复LED任务! */
printf("恢复Test任务成功!\r\n");
}
vTaskDelay(20);/* 延时20个tick */
}
}
对于挂起简单来说就是让任务调度器看不见他,这样被挂起的任务也就无法运行了,将其从挂起恢复,就是让调度器在次看见他,这样通过调度器还能继续运行。
详细的理解可以查看,任务管理的四种状态:
完整main函数代码:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "LED.h"
#include "Usart.h"
#include "Key.h"
/* FreeRTOS头文件 */
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
/*
* 任务句柄是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄
* 以后我们要想操作这个任务都需要通过这个任务句柄,如果是自身的任务操作自己,那么
* 这个句柄可以为NULL。
*/
static TaskHandle_t AppTaskCreate_Handle = NULL; /* 创建任务句柄 */
static TaskHandle_t Test_Task_Handle = NULL;/* LED任务句柄 */
static TaskHandle_t KEY_Task_Handle = NULL;/* KEY任务句柄 */
/********************************** 内核对象句柄 *********************************/
/*
* 信号量,消息队列,事件标志组,软件定时器这些都属于内核的对象,要想使用这些内核
* 对象,必须先创建,创建成功之后会返回一个相应的句柄。实际上就是一个指针,后续我
* 们就可以通过这个句柄操作这些内核对象。
*
* 内核对象说白了就是一种全局的数据结构,通过这些数据结构我们可以实现任务间的通信,
* 任务间的事件同步等各种功能。至于这些功能的实现我们是通过调用这些内核对象的函数
* 来完成的
*
*/
/******************************* 宏定义 ************************************/
/*
* 当我们在写应用程序的时候,可能需要用到一些宏定义。
*/
//一些函数声明
static void AppTaskCreate(void);/* 用于创建任务 */
static void Test_Task(void* pvParameters);/* Test_Task任务实现 */
static void KEY_Task(void* pvParameters);/* KEY_Task任务实现 */
static void All_Function_Init(void);/* 用于初始化板载相关资源 */
int main(void)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
All_Function_Init();//硬件初始化
while (1)
{
/* 创建AppTaskCreate任务 */
xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate, /* 任务入口函数 */
(const char* )"AppTaskCreate",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL,/* 任务入口函数参数 */
(UBaseType_t )1, /* 任务的优先级 */
(TaskHandle_t* )&AppTaskCreate_Handle);/* 任务控制块指针 */
/* 启动任务调度 */
if(pdPASS == xReturn)
vTaskStartScheduler(); /* 启动任务,开启调度 */
else
return -1;
}
}
//任务创建函数
static void AppTaskCreate(void)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
taskENTER_CRITICAL(); //进入临界区
/* 创建Test_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )Test_Task, /* 任务入口函数 */
(const char* )"Test_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL, /* 任务入口函数参数 */
(UBaseType_t )2, /* 任务的优先级 */
(TaskHandle_t* )&Test_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建Test_Task任务成功!\r\n");
/* 创建KEY_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )KEY_Task, /* 任务入口函数 */
(const char* )"KEY_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL,/* 任务入口函数参数 */
(UBaseType_t )3, /* 任务的优先级 */
(TaskHandle_t* )&KEY_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建KEY_Task任务成功!\r\n");
vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
taskEXIT_CRITICAL(); //退出临界区
}
//Test_Task任务主体
static void Test_Task(void* parameter)
{
while (1)
{
LED1_ON;
printf("Test_Task Running,LED1_ON!\r\n");
vTaskDelay(500); /* 延时500个tick */
LED1_OFF;
printf("Test_Task Running,LED1_OFF!\r\n");
vTaskDelay(500); /* 延时500个tick */
}
}
//Test_Task任务主体
static void KEY_Task(void* parameter)
{
while (1)
{
if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON )
{/* K1 被按下 */
printf("挂起Test任务!\r\n");
vTaskSuspend(Test_Task_Handle);/* 挂起LED任务 */
printf("挂起Test任务成功!\r\n");
}
if( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON )
{/* K2 被按下 */
printf("恢复Test任务!\r\n");
vTaskResume(Test_Task_Handle);/* 恢复LED任务! */
printf("恢复Test任务成功!\r\n");
}
vTaskDelay(20);/* 延时20个tick */
}
}
//初始化声明
static void All_Function_Init(void)
{
/*
* STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
* 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
* 都统一用这个优先级分组,千万不要再分组,切忌。
*/
NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
/* LED 初始化 */
LED_GPIO_Config();
/* 串口初始化 */
USART_Config();
//按键初始化
Key_GPIO_Config();
}
运行结果:
可以看到当我们将LED任务挂起后,其不在运行,只有当我们恢复后才会继续运行。
完整工程: