02. FreeRTOS的基本使用
一、任务创建和删除
任务的创建和删除本质上就是调用 FreeRTOS 的 API 函数。
1.1、动态方式创建任务
xTaskCreate()
函数用于使用动态的方式创建任务,任务的任务控制块以及任务的栈空间所需的内存,均由 FreeRTOS 从 FreeRTOS 管理的堆中分配,若使用此函数,需要在 FreeRTOSConfig.h 文件中将宏 configSUPPORT_DYNAMIC_ALLOCATION
配置为 1。此函数创建的任务会立刻进入就绪态,由任务调度器调度运行。函数原型如下所示:
// 返回值: pdPASS: 任务创建成功; errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY: 内存不足,任务创建失败;
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, // 指向任务函数的指针
const char * const pcName, // 任务名,最大长度为 configMAX_TASK_NAME_LEN
const configSTACK_DEPTH_TYPE uxStackDepth, // 任务堆栈大小,单位:字 (注意,单位不是字节)
void * const pvParameters, // 传递给任务函数的参数
UBaseType_t uxPriority, // 任务优先级,最大值为(configMAX_PRIORITIES-1)
TaskHandle_t * const pxCreatedTask ); // 任务句柄,任务成功创建后,会返回任务句柄。任务句柄就是任务的任务控制块
此函数创建的任务会立刻进入 就绪态,有任务调度器调度运行。
FreeRTOS 中的每一个已创建任务都包含一个任务控制块,任务控制块是一个结构体变量,FreeRTOS 用任务控制块结构体存储任务的属性。任务控制块的定义如以下代码所示:
typedef struct tskTaskControlBlock
{
volatile StackType_t * pxTopOfStack; // 指向任务栈栈顶的指针
#if ( portUSING_MPU_WRAPPERS == 1 )
xMPU_SETTINGS xMPUSettings; // MPU 相关设置
#endif
ListItem_t xStateListItem; // 任务状态列表项
ListItem_t xEventListItem; // 任务等待事件列表项
UBaseType_t uxPriority; // 任务的任务优先级
StackType_t * pxStack; // 任务栈的起始地址
char pcTaskName[ configMAX_TASK_NAME_LEN ]; // 任务的任务名
#if ( ( portSTACK_GROWTH > 0 ) || ( configRECORD_STACK_HIGH_ADDRESS == 1 ) )
StackType_t * pxEndOfStack; // 指向任务栈栈底的指针
#endif
#if ( portCRITICAL_NESTING_IN_TCB == 1 )
UBaseType_t uxCriticalNesting; // 记录任务独自的临界区嵌套次数
#endif
#if ( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxTCBNumber; // 由系统分配(每创建一个任务,值增加一),分配任务的值都不同,用于调试
UBaseType_t uxTaskNumber; // 由函数 vTaskSetTaskNumber() 设置,用于调试
#endif
#if ( configUSE_MUTEXES == 1 )
UBaseType_t uxBasePriority; // 保存任务原始优先级,用于互斥信号量的优先级翻转
UBaseType_t uxMutexesHeld; // 记录任务获取的互斥信号量数量
#endif
#if ( configUSE_APPLICATION_TASK_TAG == 1 )
TaskHookFunction_t pxTaskTag; // 用户可自定义任务的钩子函数用于调试
#endif
#if ( configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0 )
void *pvThreadLocalStoragePointers[configNUM_THREAD_LOCAL_STORAGE_POINTERS];// 保存任务独有的数据
#endif
#if ( configGENERATE_RUN_TIME_STATS == 1 )
configRUN_TIME_COUNTER_TYPE ulRunTimeCounter; // 记录任务处于运行态的时间
#endif
#if ( configUSE_NEWLIB_REENTRANT == 1 )
struct _reent xNewLib_reent; // 用于 Newlib
#endif
#if ( configUSE_TASK_NOTIFICATIONS == 1 )
volatile uint32_t ulNotifiedValue[ configTASK_NOTIFICATION_ARRAY_ENTRIES ]; // 任务通知值
volatile uint8_t ucNotifyState[ configTASK_NOTIFICATION_ARRAY_ENTRIES ]; // 任务通知状态
#endif
#if ( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 )
uint8_t ucStaticallyAllocated; // 任务静态创建标志
#endif
#if ( INCLUDE_xTaskAbortDelay == 1 )
uint8_t ucDelayAborted; // 任务被中断延时标志
#endif
#if ( configUSE_POSIX_ERRNO == 1 )
int iTaskErrno; // 用于 POSIX
#endif
} tskTCB;
typedef struct tskTaskControlBlock * TaskHandle_t;
FreeRTOS 的任务控制块结构体中包含了很多成员变量,但是,大部分的成员变量都是可以通过 FreeRTOSConfig.h 配置文件中的配置项宏定义进行裁剪的。
xTaskCreateRestricted()
此函数用于使用动态的方式创建受 MPU 保护的任务,任务的任务控制块以及任务的栈空间所需的内存,均由 FreeRTOS 从 FreeRTOS 管理的堆中分配,若使用此函数,需要将宏 configSUPPORT_DYNAMIC_ALLOCATION
和宏 portUSING_MPU_WRAPPERS
同时配置为 1。此函数创建的任务会立刻进入就绪态,由任务调度器调度运行。函数原型如下所示:
// 返回值: pdPASS: 任务创建成功; errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY: 内存不足,任务创建失败;
BaseType_t xTaskCreateRestricted( const TaskParameters_t * const pxTaskDefinition, // 指向任务参数结构体的指针,建结构体中包含任务函数、任务名、任务优先级等任务参数
TaskHandle_t * pxCreatedTask); // 任务句柄,任务成功创建后,会返回任务句柄。任务句柄就是任务的任务控制块
1.2、静态方式创建任务
xTaskCreateStatic()
函数用于使用静态的方式创建任务,任务的任务控制块以及任务的栈空间所需的内存,需要由用户分配提供, 若使用此函数,需 要 在 FreeRTOSConfig.h 文件中将宏 configSUPPORT_STATIC_ALLOCATION
配置为 1。此函数创建的任务会立刻进入就绪态,由任务调度器调度运行。函数原型如下所示:
// 返回值: NULL: 用户没有提供相应的内存,任务创建失败; 其他值: 任务句柄,任务创建成功
TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode, // 指向任务函数的指针
const char * const pcName, // 任务名,最大长度为 configMAX_TASK_NAME_LEN
const configSTACK_DEPTH_TYPE uxStackDepth, // 任务堆栈大小,单位:字(注意,单位不是字节)
void * const pvParameters, // 传递给任务函数的参数
UBaseType_t uxPriority, // 任务优先级,最大值为(configMAX_PRIORITIES-1)
StackType_t * const puxStackBuffer, // 任务栈指针,内存由用户分配提供,一般为数组
StaticTask_t * const pxTaskBuffer ); // 任务控制块指针,内存由用户分配提供
此函数创建的任务会立刻进入就绪态,由任务调度器调度运行。
xTaskCreateRestrictedStatic()
函数用于使用静态的方式创建受 MPU 保护的任务,此函数创建的任务的任务控制块以及任务的栈空间所需的内存,需要由用户自行分配提供,若使用此函数,需要将宏 configSUPPORT_STATIC_ALLOCATION
和宏 portUSING_MPU_WRAPPERS
同时配置为 1。此函数创建的任务会立刻进入就绪态,由任务调度器调度运行。函数原型如下所示:
// 返回值: pdPASS: 任务创建成功; errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY: 没有提供相应的内存,任务创建失败;
BaseType_t xTaskCreateRestrictedStatic( onst TaskParameters_t * const pxTaskDefinition, // 指向任务参数结构体的指针,建结构体中包含任务函数、任务名、任务优先级等任务参数
TaskHandle_t * pxCreatedTask); // 任务句柄,任务成功创建后,会返回任务句柄。任务句柄就是任务的任务控制块
1.3、删除任务
vTaskDelete()
函数用于删除已被创建的任务,被删除的任务将被从就绪态任务列表、阻塞态任务列表、挂起态任务列表和事件列表中移除,要注意的是,空闲任务会负责释放被删除任务中由系统分配的内存,但是由用户在任务删除前申请的内存,则需要由用户在任务被删除前提前释放,否则将导致内存泄露。若使用此函数,需要在 FreeRTOSConfig.h 文件中将宏 INCLUDE_vTaskDelete
配置为 1。函数原型如下所示:
void vTaskDelete( TaskHandle_t xTaskToDelete ); // xTaskToDelete: 待删除任务的任务句柄
当传入的参数为 NULL,则代表删除任务自身(当前正在运行的任务)。
空闲任务会负责释放被删除任务中由系统分配的内存,但是由用户在任务删除前申请的内存, 则需要由用户在任务被删除前提前释放,否则将导致内存泄露。
1.4、任务创建和删除的流程
1.4.1、动态创建任务的流程
动态创建任务的流程的流程如下:
- 将宏
configSUPPORT_DYNAMIC_ALLOCATION
配置为 1。 - 定义 函数入口参数。
- 编写 任务函数。
使用动态方式创建任务时,系统则会自动从系统堆中分配一块内存,作为任务的栈空间。任务的任务控制块以及任务的栈空间所需的内存,均由 FreeRTOS 从 FreeRTOS 管理的堆中分配。动态创建任务函数内部实现如下:
- 申请堆栈内存和任务控制块内存。
- TCB 结构体(任务控制块)成员赋值。
- 添加新任务到就绪列表中。
1.4.2、静态创建任务的流程
静态创建任务的流程如下:
- 需将宏
configSUPPORT_STATIC_ALLOCATION
配置为 1。 - 定义 空闲任务 和 软件定时器(如果使能软件定时器的话)的 任务堆栈 和 任务控制块。
- 实现
vApplicationGetIdleTaskMemory()
(空闲任务内存赋值函数)和vApplicationGetTimerTaskMemory
(软件定时器内存分配函数,如果使能软件定时器的话,需要实现)接口函数。 - 定义 函数入口参数。
- 编写 任务函数。
对于 FreeRTOS,当使用静态方式创建任务时,需要用户自行分配一块内存,作为任务的栈空间。静态创建任务函数内部实现如下:
- TCB 结构体(任务控制块)成员赋值。
- 添加新任务到就绪列表中。
1.4.3、删除任务的流程
删除任务的流程如下:
- 使用删除任务函数,需将宏
INCLUDE_vTaskDelete
配置为 1。 - 入口参数输入需要删除的任务句柄(NULL 代表删除本身)。
空闲任务会负责释放被删除任务中由系统分配的内存,但是由用户在任务删除前申请的内存,则需要由用户在任务被删除前提前释放,否则将导致内存泄露。删除任务的内部实现流程如下:
- 获取所要删除任务的控制块:通过传入的任务句柄,判断所需要删除哪个任务,NULL 代表删除自身。
- 将被删除任务,移除所在列表:将该任务在所在列表中移除,包括:就绪、阻塞、挂起、事件等列表。
- 判断所需要删除的任务:
- 删除任务自身,需先添加到等待删除列表,内存释放将在空闲任务执行。
- 删除其他任务,释放内存,任务数量减 1。
- 更新下个任务的阻塞时间:更新下一个任务的阻塞超时时间,以防被删除的任务就是下一个阻塞超时的任务。
1.4.4、实验例程
1.4.4.1、main()函数
main() 函数内容如下:
int main(void)
{
HAL_Init();
System_Clock_Init(8, 336, 2, 7);
Delay_Init(168);
UART_Init(&g_usart1_handle, USART1, 115200);
LED_Init();
Key_Init();
freertos_demo();
return 0;
}
1.4.4.2、LED功能函数
LED 初始化函数:
/**
* @brief led初始化函数
*
*/
void LED_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOF_CLK_ENABLE(); // 使能GPIOF的时钟
GPIO_InitStruct.Pin = GPIO_PIN_9; // GPIO引脚
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出模式
GPIO_InitStruct.Pull = GPIO_NOPULL; // 不使用上下拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 高速模式
HAL_GPIO_Init(GPIOF, &GPIO_InitStruct); // GPIO初始化
GPIO_InitStruct.Pin = GPIO_PIN_10; // GPIO引脚
HAL_GPIO_Init(GPIOF, &GPIO_InitStruct); // GPIO初始化
LED_Status(GPIOF, GPIO_PIN_9, LED_OFF);
LED_Status(GPIOF, GPIO_PIN_10, LED_OFF);
}
LED 状态设置函数:
typedef enum LED_State
{
LED_ON,
LED_OFF,
} LED_State;
/**
* @brief 设置LED状态函数
*
* @param LED_Port LED的GPIO端口
* @param LED_Pin LED的GPIO引脚
* @param status LED的状态枚举值
*/
void LED_Status(GPIO_TypeDef *LED_Port, uint16_t LED_Pin, LED_State status)
{
if(status == LED_ON)
{
HAL_GPIO_WritePin(LED_Port, LED_Pin, GPIO_PIN_RESET);
}
else
{
HAL_GPIO_WritePin(LED_Port, LED_Pin, GPIO_PIN_SET);
}
}
1.4.4.3、按键功能函数
按键初始化函数:
#define WKUP_GPIO_PORT GPIOA
#define WKUP_GPIO_PIN GPIO_PIN_0
#define WKUP_GPIO_CLK_ENABLE() do{ \
__HAL_RCC_GPIOA_CLK_ENABLE(); \
} while(0);
#define KEY1_GPIO_PORT GPIOE
#define KEY1_GPIO_PIN GPIO_PIN_4
#define KEY1_GPIO_CLK_ENABLE() do{ \
__HAL_RCC_GPIOE_CLK_ENABLE(); \
} while(0);
#define KEY2_GPIO_PORT GPIOE
#define KEY2_GPIO_PIN GPIO_PIN_3
#define KEY2_GPIO_CLK_ENABLE() do{ \
__HAL_RCC_GPIOE_CLK_ENABLE(); \
} while(0);
#define KEY3_GPIO_PORT GPIOE
#define KEY3_GPIO_PIN GPIO_PIN_2
#define KEY3_GPIO_CLK_ENABLE() do{ \
__HAL_RCC_GPIOE_CLK_ENABLE(); \
} while(0);
/**
* @brief 按键初始化函数
*
*/
void Key_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
WKUP_GPIO_CLK_ENABLE();
KEY1_GPIO_CLK_ENABLE();
KEY2_GPIO_CLK_ENABLE();
KEY3_GPIO_CLK_ENABLE();
GPIO_InitStruct.Pin = WKUP_GPIO_PIN; // GPIO引脚
GPIO_InitStruct.Mode = GPIO_MODE_INPUT; // 输入模式
GPIO_InitStruct.Pull = GPIO_PULLDOWN; // 使用下拉
HAL_GPIO_Init(WKUP_GPIO_PORT, &GPIO_InitStruct); // GPIO初始化
GPIO_InitStruct.Pin = KEY1_GPIO_PIN; // GPIO引脚
GPIO_InitStruct.Pull = GPIO_PULLUP; // 使用上拉
HAL_GPIO_Init(KEY1_GPIO_PORT, &GPIO_InitStruct); // GPIO初始化
GPIO_InitStruct.Pin = KEY2_GPIO_PIN; // GPIO引脚
HAL_GPIO_Init(KEY2_GPIO_PORT, &GPIO_InitStruct); // GPIO初始化
GPIO_InitStruct.Pin = KEY3_GPIO_PIN; // GPIO引脚
HAL_GPIO_Init(KEY3_GPIO_PORT, &GPIO_InitStruct); // GPIO初始化
}
按键扫描函数:
/* 读取按键对应的GPIO引脚的电平状态 */
#define WK_UP HAL_GPIO_ReadPin(WKUP_GPIO_PORT, WKUP_GPIO_PIN)
#define KEY1 HAL_GPIO_ReadPin(KEY1_GPIO_PORT, KEY1_GPIO_PIN)
#define KEY2 HAL_GPIO_ReadPin(KEY2_GPIO_PORT, KEY2_GPIO_PIN)
#define KEY3 HAL_GPIO_ReadPin(KEY3_GPIO_PORT, KEY3_GPIO_PIN)
/* 对应按键按下时代表的数值 */
#define WKUP_PRESS 1
#define KEY1_PRESS 2
#define KEY2_PRESS 3
#define KEY3_PRESS 4
/**
* @brief 按键扫描函数
*
* @param mode 0:不支持连续按;1:支持连续按
* @return uint8_t WKUP_PRES,1,WKUP按下
* KEY1_PRES,2,KEY0按下
* KEY2_PRES,3,KEY1按下
* KEY3_PRES,4,KEY2按下
*
* @note 该函数有响应优先级,同时按下多个按键:WK_UP > KEY3 > KEY2 > KEY1
*/
uint8_t Key_Scan(uint8_t mode)
{
static uint8_t flag = 1; // 按键按松开标志
uint8_t keyValue = 0;
flag = (mode ? 1 : flag); // 支持连按
if (flag && ( WK_UP == 1 || KEY1 == 0 || KEY2 == 0 || KEY3 == 0)) // 按键松开标志为1, 且有任意一个按键按下了
{
HAL_Delay(10); // 按键消抖
flag = 0;
// 再次读取GPIO引脚的电平
keyValue = ((KEY1 == 0) ? KEY1_PRESS : keyValue);
keyValue = ((KEY2 == 0) ? KEY2_PRESS : keyValue);
keyValue = ((KEY3 == 0) ? KEY3_PRESS : keyValue);
keyValue = ((WK_UP == 1) ? WKUP_PRESS : keyValue);
}
else if (WK_UP == 0 && KEY1 == 1 && KEY2 == 1 && KEY3 == 1) // 没有任何按键按下, 标记按键松开
{
flag = 1;
}
return keyValue; // 按键没有按下返回0
}
1.4.4.4、串口功能函数
串口初始化函数:
UART_HandleTypeDef g_usart1_handle; // USART1句柄
uint8_t g_uart_rx_buffer[1]; // HAL库使用的串口接收数据缓冲区
uint8_t g_usart1_rx_buffer[UART_RECEIVE_LENGTH]; // 接收数据缓冲区
uint16_t g_usart1_rx_status = 0; // 接收状态标记
/**
* @brief 串口初始化函数
*
* @param huart 串口句柄
* @param UARTx 串口寄存器基地址
* @param band 波特率
*/
void UART_Init(UART_HandleTypeDef *huart, USART_TypeDef *UARTx, uint32_t band)
{
huart->Instance = UARTx; // 寄存器基地址
huart->Init.BaudRate = band; // 波特率
huart->Init.WordLength = UART_WORDLENGTH_8B; // 数据位
huart->Init.StopBits = UART_STOPBITS_1; // 停止位
huart->Init.Parity = UART_PARITY_NONE; // 奇偶校验位
huart->Init.Mode = UART_MODE_TX_RX; // 收发模式
huart->Init.HwFlowCtl = UART_HWCONTROL_NONE; // 硬件流控制
huart->Init.OverSampling = UART_OVERSAMPLING_16; // 过采样
HAL_UART_Init(huart);
HAL_UART_Receive_IT(huart, (uint8_t *)g_uart_rx_buffer, 1); // 开启接收中断
}
串口底层初始化函数:
/**
* @brief 串口底层初始化函数
*
* @param huart 串口句柄
*/
void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if (huart->Instance == USART1) // 初始化的串口是否是USART1
{
__HAL_RCC_USART1_CLK_ENABLE(); // 使能USART1时钟
__HAL_RCC_GPIOA_CLK_ENABLE(); // 使能对应GPIO的时钟
// PA9 -> USART TXD
GPIO_InitStruct.Pin = GPIO_PIN_9; // USART1 TXD的引脚
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 推挽式复用
GPIO_InitStruct.Pull = GPIO_NOPULL; // 不使用上下拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 输出速度
GPIO_InitStruct.Alternate = GPIO_AF7_USART1; // 复用功能
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// PA10 -> USART RXD
GPIO_InitStruct.Pin = GPIO_PIN_10; // USART1 RXD的引脚
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
HAL_NVIC_EnableIRQ(USART1_IRQn); // 使能USART1中断
HAL_NVIC_SetPriority(USART1_IRQn, 2, 0); // 设置中断优先级
}
}
串口 1 中断服务函数:
/**
* @brief USART1中断服务函数
*
*/
void USART1_IRQHandler(void)
{
HAL_UART_IRQHandler(&g_usart1_handle); // 调用HAL库公共处理函数
HAL_UART_Receive_IT(&g_usart1_handle, (uint8_t *)g_uart_rx_buffer, 1); // 再次开启接收中断
}
串口接收中断回调函数:
/**
* @brief USART接收中断回调函数
*
* @param huart 串口句柄
*/
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1)
{
if ((g_usart1_rx_status & 0x8000) == 0) // 接收未完成
{
if (g_usart1_rx_status & 0x4000) // 接收到了0x0D,即回车键
{
if (g_uart_rx_buffer[0] != 0x0A) // 接收到的不是0x0A,即不是换行符
{
g_usart1_rx_status = 0; // 接收错误,重新开始
}
else // 接收到的是0x0A,即换行符
{
g_usart1_rx_status |= 0x8000; // 接收完成
}
}
else // 还没接收到0x0D,即还没接收到回车键
{
if (g_uart_rx_buffer[0] == 0x0D) // 接收到的是0x0D,即回车键
{
g_usart1_rx_status |= 0x4000;
}
else // 如果没有接收到回车
{
// 接收到的数据存入接收缓冲区
g_usart1_rx_buffer[g_usart1_rx_status & 0x3FFF] = g_uart_rx_buffer[0];
g_usart1_rx_status++;
if (g_usart1_rx_status > (UART_RECEIVE_LENGTH - 1)) // 接收到数据大于接收缓冲区大小
{
g_usart1_rx_status = 0; // 接收数据错误,重新开始接收
}
}
}
}
}
}
重映像 printf() 函数:
/**
* @brief 重写_write使用printf()函数
*
* @param fd 一个非负整数,代表要写入数据的文件或设备的标识
* @param ptr 一个指向字符数据的指针,即要写入的数据的起始位置
* @param length 一个整数,表示要写入的数据的字节数
* @return int 数据的字节数
*/
int _write(int fd, char *ptr, int length)
{
HAL_UART_Transmit(&g_usart1_handle, (uint8_t *)ptr, length, 0xFFFF); // g_usart1_handle是对应串口
return length;
}
1.4.4.5、FreeRTOS任务函数
空闲任务配置:
/**
* 空闲任务配置
* 包括: 空闲任务的任务控制块 空闲任务任务栈 空闲任务的内存分配函数
*/
StaticTask_t idle_task_tcb;
StackType_t idle_task_stack[configMINIMAL_STACK_SIZE];
void vApplicationGetIdleTaskMemory(StaticTask_t **ppxIdleTaskTCBBuffer,
StackType_t **ppxIdleTaskStackBuffer,
configSTACK_DEPTH_TYPE *puxIdleTaskStackSize);
/**
* @brief 空闲任务的内存分配函数
*
* @param ppxIdleTaskTCBBuffer 空闲任务的任务控制块
* @param ppxIdleTaskStackBuffer 空闲任务的任务栈
* @param puxIdleTaskStackSize 空闲任务的任务栈大小
*/
void vApplicationGetIdleTaskMemory(StaticTask_t **ppxIdleTaskTCBBuffer,
StackType_t **ppxIdleTaskStackBuffer,
configSTACK_DEPTH_TYPE *puxIdleTaskStackSize)
{
*ppxIdleTaskTCBBuffer = &idle_task_tcb;
*ppxIdleTaskStackBuffer = idle_task_stack;
*puxIdleTaskStackSize = configMINIMAL_STACK_SIZE;
}
软件定时器任务配置:
/**
* 软件定时器任务配置
* 包括: 软件定时器任务控制块 软件定时器任务栈 软件定时器内存分配函数
*/
StaticTask_t timer_task_tcb;
StackType_t timer_task_stack[configMINIMAL_STACK_SIZE];
void vApplicationGetTimerTaskMemory(StaticTask_t **ppxTimerTaskTCBBuffer,
StackType_t **ppxTimerTaskStackBuffer,
configSTACK_DEPTH_TYPE *puxTimerTaskStackSize);
/**
* @brief 软件定时器内存分配函数
*
* @param ppxTimerTaskTCBBuffer 软件定时器任务控制块
* @param ppxTimerTaskStackBuffer 软件定时器任务栈
* @param puxTimerTaskStackSize 软件定时器任务栈大小
*/
void vApplicationGetTimerTaskMemory(StaticTask_t **ppxTimerTaskTCBBuffer,
StackType_t **ppxTimerTaskStackBuffer,
configSTACK_DEPTH_TYPE *puxTimerTaskStackSize)
{
*ppxTimerTaskTCBBuffer = &timer_task_tcb;
*ppxTimerTaskStackBuffer = timer_task_stack;
*puxTimerTaskStackSize = configMINIMAL_STACK_SIZE;
}
FreeRTOS 入口函数:
/**
* @brief FreeRTOS的入口函数
*
*/
void freertos_demo(void)
{
xTaskCreate((TaskFunction_t ) start_task, // 任务函数
(char * ) "start_task", // 任务名
(configSTACK_DEPTH_TYPE) START_TASK_STACK_SIZE, // 任务栈大小
(void * ) NULL, // 入口参数
(UBaseType_t ) START_TASK_PRIORITY, // 任务优先级
(TaskHandle_t * ) start_task_handle); // 任务句柄
vTaskStartScheduler(); // 开启任务调度器
}
START_TASK 任务配置:
/**
* START_TASK 任务配置
* 包括: 任务优先级 任务栈大小 任务句柄 任务函数
*/
#define START_TASK_PRIORITY 1
#define START_TASK_STACK_SIZE 128
TaskHandle_t start_task_handle;
void start_task(void *pvParameters );
/**
* @brief 开始任务的任务函数
*
* @param pvParameters 任务函数的入口参数
*/
void start_task(void *pvParameters)
{
taskENTER_CRITICAL(); // 进入临界区,关闭中断
// 动态创建任务
xTaskCreate((TaskFunction_t ) task1, // 任务函数
(char * ) "task1", // 任务名
(configSTACK_DEPTH_TYPE) TASK1_STACK_SIZE, // 任务栈大小
(void * ) NULL, // 入口参数
(UBaseType_t ) TASK1_PRIORITY, // 任务优先级
(TaskHandle_t * ) &task1_handle); // 任务句柄
// 静态创建任务,返回任务句柄
task2_handle = xTaskCreateStatic((TaskFunction_t ) task2, // 任务函数
(char * ) "task2", // 任务名
(configSTACK_DEPTH_TYPE) TASK2_STACK_SIZE, // 任务栈大小
(void * ) NULL, // 入口参数
(UBaseType_t ) TASK2_PRIORITY, // 任务优先级
(StackType_t * ) task2_stack, // 任务栈
(StaticTask_t * ) &task2_tcb); // 任务控制块
xTaskCreate((TaskFunction_t ) task3, // 任务函数
(char * ) "task3", // 任务名
(configSTACK_DEPTH_TYPE) TASK3_STACK_SIZE, // 任务栈大小
(void * ) NULL, // 入口参数
(UBaseType_t ) TASK3_PRIORITY, // 任务优先级
(TaskHandle_t * ) &task3_handle); // 任务句柄
vTaskDelete(NULL); // 删除任务自身
taskEXIT_CRITICAL(); // 退出临界区,重新开启中断
}
TASK1 任务配置:
/**
* TASK1 任务配置
* 包括: 任务优先级 任务栈大小 任务句柄 任务函数
*/
#define TASK1_PRIORITY 2
#define TASK1_STACK_SIZE 128
TaskHandle_t task1_handle;
void task1(void *pvParameters);
/**
* @brief 任务1的任务函数,实现LED1每500ms闪烁一次
*
* @param pvParameters 任务函数的入口参数
*/
void task1(void *pvParameters)
{
while (1)
{
printf("task1 正在运行\r\n");
HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_9);
vTaskDelay(500);
}
}
TASK2 任务配置:
/**
* TASK2 任务配置
* 包括: 任务优先级 任务栈大小 任务句柄 任务控制块 任务栈 任务函数
*/
#define TASK2_PRIORITY 3
#define TASK2_STACK_SIZE 128
TaskHandle_t task2_handle;
StaticTask_t task2_tcb;
StackType_t task2_stack[TASK2_STACK_SIZE];
void task2(void *pvParameters);
/**
* @brief 任务2的任务函数,实现LED2每500ms闪烁一次
*
* @param pvParameters 任务函数的入口参数
*/
void task2(void *pvParameters )
{
while (1)
{
printf("task2 正在运行\r\n");
HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_10);
vTaskDelay(500);
}
}
TASK3 任务配置:
/**
* TASK3 任务配置
* 包括: 任务优先级 任务栈大小 任务句柄 任务函数
*/
#define TASK3_PRIORITY 4
#define TASK3_STACK_SIZE 128
TaskHandle_t task3_handle;
void task3(void *pvParameters);
/**
* @brief 任务3的任务函数,K1按键按下删除任务1,K2按键按下删除任务2
*
* @param pvParameters 任务函数的入口参数
*/
void task3(void *pvParameters )
{
while (1)
{
printf("task3 正在运行\r\n");
switch (Key_Scan(0))
{
case KEY1_PRESS:
if (task1_handle)
{
printf("删除 task1 任务\r\n");
vTaskDelete(task1_handle);
task1_handle = NULL;
}
break;
case KEY2_PRESS:
{
if (task2_handle)
{
printf("删除 task2 任务\r\n");
vTaskDelete(task2_handle);
task2_handle = NULL;
}
break;
}
default:
break;
}
vTaskDelay(10);
}
}
二、任务的挂起和恢复
2.1、任务挂起
vTaskSuspend()
函数用于挂起任务,若使用此函数,需要在 FreeRTOSConfig.h 文件中将宏 INCLUDE_vTaskSuspend
配置为 1。无论优先级如何,被挂起的任务都将不再被执行,直到任务被恢复。此函数并不支持嵌套,不论使用此函数重复挂起任务多少次,只需调用一次恢复任务的函数,那么任务就不再被挂起。函数原型如下所示:
void vTaskSuspend(TaskHandle_t xTaskToSuspend); // xTaskToSuspend为待挂起任务的任务句柄
此函数用于挂起任务,使用时需将宏
INCLUDE_vTaskSuspend
配置为 1。无论优先级如何,被挂起的任务都将不再被执行,直到任务被恢复。
当传入的参数为 NULL,则代表挂起任务自身(当前正在运行的任务)。
2.2、任务恢复
vTaskResume()
函数用于在任务中恢复被挂起的任务,若使用此函数,需要在 FreeRTOSConfig.h 文件中将宏 INCLUDE_vTaskSuspend
配置为 1。不论一个任务被函数 vTaskSuspend() 挂起多少次,只需要使用函数 vTaskResume() 恢复一次,就可以继续运行。函数原型如下所示:
void vTaskResume(TaskHandle_t xTaskToResume); // xTaskToResume为待恢复任务的任务句柄
此函数用于在恢复任务,使用时需将宏
INCLUDE_vTaskSuspend
配置为 1。任务无论被 vTaskSuspend() 挂起多少次,只需在任务中调用 vTakResume() 恢复一次,就可以继续运行。且被恢复的任务会进入就绪态。
2.3、中断中恢复任务
xTaskResumeFromISR()
函数用于在中断中恢复被挂起的任务,若使用此函数,需要在 FreeRTOSConfig.h 文件中将宏 INCLUDE_vTaskSuspend
和 INCLUDE_xTaskResumeFromISR
配置为 1。不论一个任务被函数 vTaskSuspend() 挂起多少次,只需要使用函数 xTaskResumeFromISR() 恢复一次,就可以继续运行。函数原型如下所示:
// 返回值: pdTRUE: 任务恢复后需要进行任务切换; pdFALSE: 任务恢复后不需要进行任务切换;
BaseType_t xTaskResumeFromISR(TaskHandle_t xTaskToResume); // xTaskToResume为待恢复任务的任务句柄
此函数用于中断服务函数中,用于解挂被挂起任务,使用时需将宏
INCLUDE_vTaskSuspend
和INCLUDE_xTaskResumeFromISR
配置为 1。中断服务程序中要调用 FreeRTOS 的 API 函数则中断优先级不能高于FreeRTOS所管理的最高优先级。
2.4、实验例程
2.4.1、修改main()函数
修改 main() 函数:
int main(void)
{
HAL_Init();
System_Clock_Init(8, 336, 2, 7);
Delay_Init(168);
UART_Init(&g_usart1_handle, USART1, 115200);
LED_Init();
Key_Init();
EXTI2_Init();
freertos_demo();
return 0;
}
2.4.2、修改task3任务函数
修改 task3() 函数:
/**
* @brief 任务3的任务函数
*
* @param pvParameters 任务函数的入口参数
*/
void task3(void *pvParameters )
{
while (1)
{
switch (Key_Scan(0))
{
case KEY1_PRESS:
printf("挂起 task1 任务\r\n");
vTaskSuspend(task1_handle);
break;
case KEY2_PRESS:
{
printf("恢复 task1 任务\r\n");
vTaskResume(task1_handle);
break;
}
default:
break;
}
vTaskDelay(10);
}
}
2.4.3、EXTI功能函数
EXTI2 初始化函数:
/**
* @brief EXTI2初始化函数
*
*/
void EXTI2_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOE_CLK_ENABLE(); // 使能GPIOE的时钟
GPIO_InitStruct.Pin = GPIO_PIN_2; // GPIO引脚
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; // 下降沿触发
GPIO_InitStruct.Pull = GPIO_PULLUP; // 使用上拉
HAL_GPIO_Init(GPIOE, &GPIO_InitStruct); // GPIO初始化
HAL_NVIC_SetPriority(EXTI2_IRQn, 5, 0); // 设置中断优先级
HAL_NVIC_EnableIRQ(EXTI2_IRQn); // 使能中断
}
EXTI2 中断服务函数:
/**
* @brief EXTI2中断服务函数
*
*/
void EXTI2_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_2); // 调用HAL库的EXTI公共中断处理函数
}
重写 HAL 库的 EXTI 回调函数:
/**
* @brief 重写HAL库的EXTI回调函数
*
* @param GPIO_Pin EXTI的引脚
*/
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_2)
{
HAL_Delay(20); // 按键消抖
if (HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_2) == GPIO_PIN_RESET) // 读取GPIO引脚电平,再次判断按键是否按下
{
printf("在中断中恢复 task1 任务\r\n");
BaseType_t xYieldRequired = xTaskResumeFromISR(task1_handle); // 在中断中恢复函数
if (xYieldRequired == pdTRUE)
{
portYIELD_FROM_ISR(xYieldRequired); // 任务切换
}
}
}
}
如果在中断中恢复任务,串口输出:
Error: ./Middleware/FreeRTOS/FreeRTOS-Kernel/portable/GCC/ARM_CM4F/port.c, 906
,而 port.c 文件的 906 行对应的内容为:configASSERT((portAIRCR_REG & portPRIORITY_GROUP_MASK) <= ulMaxPRIGROUPValue);
,说明 FreeRTOS 中建议所有的中断优先级都作为抢占优先级使用,我们可以调用 HAL 库的HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
设置(HAL 库默认的分组就是分组 4)。如果在中断中恢复任务,串口输出:
Error: ./Middleware/FreeRTOS/FreeRTOSKernel/portable/GCC/ARM_CM4F/port.c, 890
,而 port.c 文件中的 890 行对应的内容为:configASSERT(ucCurrentPriority >= ucMaxSysCallPriority);
外部中断的优先级高于 FreeRTOS 所管理的中断优先级(FreeRTOS 的管理的中断优先级可以在 FreeRTOSConfig.h 配置文件中查看,对应的宏定义为configLIBRARY_LOWEST_INTERRUPT_PRIORITY
(FreeRTOS 所管理的最小中断优先级)和configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
FreeRTOS 所管理的最大中断优先级))。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
2023-03-01 09. 函数