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、动态创建任务的流程

  动态创建任务的流程的流程如下:

  1. 将宏 configSUPPORT_DYNAMIC_ALLOCATION 配置为 1。
  2. 定义 函数入口参数
  3. 编写 任务函数

  使用动态方式创建任务时,系统则会自动从系统堆中分配一块内存,作为任务的栈空间。任务的任务控制块以及任务的栈空间所需的内存,均由 FreeRTOS 从 FreeRTOS 管理的堆中分配。动态创建任务函数内部实现如下:

  1. 申请堆栈内存和任务控制块内存。
  2. TCB 结构体(任务控制块)成员赋值。
  3. 添加新任务到就绪列表中。

1.4.2、静态创建任务的流程

  静态创建任务的流程如下:

  1. 需将宏 configSUPPORT_STATIC_ALLOCATION 配置为 1。
  2. 定义 空闲任务软件定时器(如果使能软件定时器的话)的 任务堆栈任务控制块
  3. 实现 vApplicationGetIdleTaskMemory()(空闲任务内存赋值函数)和 vApplicationGetTimerTaskMemory(软件定时器内存分配函数,如果使能软件定时器的话,需要实现)接口函数。
  4. 定义 函数入口参数
  5. 编写 任务函数

  对于 FreeRTOS,当使用静态方式创建任务时,需要用户自行分配一块内存,作为任务的栈空间。静态创建任务函数内部实现如下:

  1. TCB 结构体(任务控制块)成员赋值。
  2. 添加新任务到就绪列表中。

1.4.3、删除任务的流程

  删除任务的流程如下:

  1. 使用删除任务函数,需将宏 INCLUDE_vTaskDelete 配置为 1。
  2. 入口参数输入需要删除的任务句柄(NULL 代表删除本身)。

  空闲任务会负责释放被删除任务中由系统分配的内存,但是由用户在任务删除前申请的内存,则需要由用户在任务被删除前提前释放,否则将导致内存泄露。删除任务的内部实现流程如下:

  1. 获取所要删除任务的控制块:通过传入的任务句柄,判断所需要删除哪个任务,NULL 代表删除自身。
  2. 将被删除任务,移除所在列表:将该任务在所在列表中移除,包括:就绪、阻塞、挂起、事件等列表。
  3. 判断所需要删除的任务
    • 删除任务自身,需先添加到等待删除列表,内存释放将在空闲任务执行。
    • 删除其他任务,释放内存,任务数量减 1。
  4. 更新下个任务的阻塞时间:更新下一个任务的阻塞超时时间,以防被删除的任务就是下一个阻塞超时的任务。

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_vTaskSuspendINCLUDE_xTaskResumeFromISR 配置为 1。不论一个任务被函数 vTaskSuspend() 挂起多少次,只需要使用函数 xTaskResumeFromISR() 恢复一次,就可以继续运行。函数原型如下所示:

// 返回值: pdTRUE: 任务恢复后需要进行任务切换; pdFALSE: 任务恢复后不需要进行任务切换;
BaseType_t xTaskResumeFromISR(TaskHandle_t xTaskToResume);      // xTaskToResume为待恢复任务的任务句柄

此函数用于中断服务函数中,用于解挂被挂起任务,使用时需将宏 INCLUDE_vTaskSuspendINCLUDE_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_PRIORITYFreeRTOS 所管理的最大中断优先级))。

posted @   星光映梦  阅读(168)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
历史上的今天:
2023-03-01 09. 函数
点击右上角即可分享
微信分享提示