FreeRTOS 入门(二):多任务创建与核心驱动实现

目录

一、前言

大家好,我是 Hello_Embed。上一篇我们完成了 FreeRTOS 工程的基础搭建,这一篇将聚焦核心功能 ——多任务创建与调度。FreeRTOS 的核心价值在于支持多个独立任务并行执行(逻辑上),本次将通过两个典型任务验证:LED1 循环闪烁任务、按键控制 LED2 亮灭任务,实现 “LED 闪烁不卡顿、按键响应不延迟” 的多任务效果。

需要说明的是:韦东山视频中使用 0.96 寸 OLED 屏演示,而我使用的 CT117E-M4 开发板搭载 2.4 寸 TFT-LCD 屏,底层驱动差异较大。为聚焦多任务核心逻辑,本次改用 “按键 + LED” 替代 LCD 打印,本质仍是验证多任务的并发执行能力 —— 即使一个任务处于死循环,另一个任务依然能正常响应。

开发过程中会用到高精度延时、HAL 库 GPIO 配置等基础技能,若有遗忘可回看之前的 HAL 库系列笔记。同时,由于开发板硬件与视频中的瑞士军刀开发板不同,部分代码已做适配修改,确保兼容 STM32G431RBT6 的外设资源。

二、多任务核心接口与开发准备

2.1 多任务创建接口说明

不同 RTOS 的任务创建函数存在差异:

  • FreeRTOS 原生接口:xTaskCreate(本次选用,更贴近底层逻辑);
  • RT-Thread 接口:rt_thread_create
  • CubeMX 统一接口:osThreadNew(兼容 FreeRTOS 和 RT-Thread,通用性强)。

2.2 FreeRTOS 原生任务创建函数格式

BaseType_t xTaskCreate(
    TaskFunction_t pxTaskCode,    // 任务函数(死循环逻辑)
    const char * const pcName,    // 任务名称(仅调试用)
    const configSTACK_DEPTH_TYPE usStackDepth,  // 任务栈大小(单位:字)
    void * const pvParameters,    // 任务参数(无则传NULL)
    UBaseType_t uxPriority,       // 任务优先级(数值越大优先级越高)
    TaskHandle_t * const pxCreatedTask  // 任务句柄(无需操作则传NULL)
);

2.3 开发准备

  1. 在工程Drivers文件夹下新建 “User_Driver” 目录,存放定时器、LED、按键的驱动文件;
  2. 配置驱动文件的编译路径(确保编译器能识别头文件和源文件);
  3. 确认 CubeMX 中已开启定时器 4(高精度延时依赖)、GPIO 端口时钟(LED 和按键使用)。

三、核心驱动实现(定时器 + LED + 按键)

3.1 高精度定时器驱动(延时基础)

多任务中需要稳定的高精度延时,本次基于定时器 4 实现微秒(us)和毫秒(ms)级延时,同时提供纳秒级时间戳获取函数。

3.1.1 头文件(timer_driver.h)
#ifndef _DRIVER_TIMER_H
#define _DRIVER_TIMER_H

#include <stdint.h>

// 微秒级延时
void udelay(int us);
// 毫秒级延时(基于udelay实现)
void mdelay(int ms);
// 获取系统纳秒级时间戳
uint64_t system_get_ns(void);

#endif
3.1.2 驱动文件(timer_driver.c)
#include "timer_driver.h"
#include "stm32g4xx_hal.h"

// 微秒级延时函数(基于定时器4实现)
void udelay(int us)
{
#if 0    // 注释:原SysTick延时方案(未使用)
    uint32_t ticks;
    uint32_t told, tnow, tcnt = 0;
    uint32_t reload = SysTick->LOAD;

    ticks = us * reload / (1000);  /* 假设reload对应1ms */
    told = SysTick->VAL;
    while (1)
    {
        tnow = SysTick->VAL;
        if (tnow != told)
        {
            if (tnow < told)
            {
                tcnt += told - tnow;
            }
            else
            {
                tcnt += reload - tnow + told;
            }
            told = tnow;
            if (tcnt >= ticks)
            {
                break;
            }
        }
    }
#else  // 注释:当前使用定时器4实现高精度延时
    extern TIM_HandleTypeDef        htim4;  // 外部声明定时器4句柄(CubeMX生成)
    TIM_HandleTypeDef *hHalTim = &htim4;

    uint32_t ticks;          // 需累计的定时器计数 ticks
    uint32_t told, tnow, tcnt = 0;  // 上次计数、当前计数、累计计数
    uint32_t reload = __HAL_TIM_GET_AUTORELOAD(hHalTim);  // 定时器自动重载值

    // 计算us对应的ticks数(假设reload对应1ms,即1000us)
    ticks = us * reload / (1000);
    told = __HAL_TIM_GET_COUNTER(hHalTim);  // 获取初始计数

    while (1)
    {
        tnow = __HAL_TIM_GET_COUNTER(hHalTim);  // 实时获取当前计数
        if (tnow != told)  // 计数发生变化时更新累计值
        {
            if (tnow > told)  // 定时器向上计数,无溢出
            {
                tcnt += tnow - told;
            }
            else  // 定时器溢出,累计溢出部分+当前计数
            {
                tcnt += reload - told + tnow;
            }
            told = tnow;
            if (tcnt >= ticks)  // 累计计数达到目标,退出延时
            {
                break;
            }
        }
    }
#endif
}

// 毫秒级延时函数(循环调用udelay(1000)实现)
void mdelay(int ms)
{
    for (int i = 0; i < ms; i++)
        udelay(1000);  // 1ms = 1000us
}

// 获取系统纳秒级时间戳(HAL_GetTick+定时器计数)
uint64_t system_get_ns(void)
{
    extern TIM_HandleTypeDef        htim4;  // 外部声明定时器4句柄
    TIM_HandleTypeDef *hHalTim = &htim4;
    
    uint64_t ns = HAL_GetTick();  // 获取毫秒级时间戳(HAL库原生函数)
    uint64_t cnt;                 // 定时器当前计数
    uint64_t reload;              // 定时器重载值

    cnt = __HAL_TIM_GET_COUNTER(hHalTim);    // 获取定时器当前计数
    reload = __HAL_TIM_GET_AUTORELOAD(hHalTim);  // 获取重载值

    ns *= 1000000;  // 毫秒转换为纳秒(1ms = 1e6 ns)
    // 累加定时器计数对应的纳秒数(按比例计算)
    ns += cnt * 1000000 / reload;
    return ns;
}

3.2 LED 驱动(PC8-15 配置与控制)

开发板搭载 8 个 LED(PC8~PC15),本次选用 LED1(PC8)实现 500ms 循环闪烁,LED2(PC9)由按键控制。CubeMX 中已配置 LED 引脚为 “初始高电平、上拉输出模式”。

3.2.1 头文件(led_driver.h)
#ifndef _LED_DRIVER
#define _LED_DRIVER

#define LED_GREEN 1  // LED1(PC8)定义为绿色LED
// LED初始化(GPIO配置)
int Led_Init(void);
// LED控制(which:LED编号;on:1=亮,0=灭)
int Led_Control(int which, int on);
// LED测试函数(死循环闪烁)
void Led_Test(void);

#endif
3.2.2 驱动文件(led_driver.c)
#include "led_driver.h"
#include "gpio.h"
#include "timer_driver.h"
#include "stm32g4xx_hal.h"
#include "cmsis_os.h"

// LED初始化:配置PC8为推挽输出模式
int Led_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    
    __HAL_RCC_GPIOC_CLK_ENABLE();  // 使能GPIOC时钟

    // 初始电平设置:PC8输出高电平(LED灭,因LED为低电平点亮)
    HAL_GPIO_WritePin(GPIOC, GPIO_PIN_8, GPIO_PIN_SET);

    // GPIO配置参数
    GPIO_InitStruct.Pin = GPIO_PIN_8;          // 引脚:PC8(LED1)
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出模式
    GPIO_InitStruct.Pull = GPIO_PULLDOWN;      // 下拉电阻
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; // 低速输出
    HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);    // 应用配置
    
    return 0;
}

// LED控制函数:控制指定LED的亮灭
int Led_Control(int which, int on)
{
    if (on)
        // 低电平点亮LED:PC8输出低电平
        HAL_GPIO_WritePin(GPIOC, GPIO_PIN_8, GPIO_PIN_RESET);
    else
        // 高电平熄灭LED:PC8输出高电平
        HAL_GPIO_WritePin(GPIOC, GPIO_PIN_8, GPIO_PIN_SET);
    return 0;
}

// LED测试任务函数:死循环实现500ms闪烁
void Led_Test(void)
{
    Led_Init();  // 初始化LED引脚

    while (1)
    {
        Led_Control(LED_GREEN, 1);  // LED1亮
        mdelay(500);                // 延时500ms

        Led_Control(LED_GREEN, 0);  // LED1灭
        mdelay(500);                // 延时500ms
    }
}

3.3 按键驱动(PB0 输入与 LED2 控制)

开发板有 4 个按键(PB0~PB2、PA0),本次选用按键 1(PB0),通过 “按下亮、松开灭” 控制 LED2(PC9)。CubeMX 中已配置按键引脚为 “上拉输入模式”(电路图中按键输入端接下拉电阻,按下时引脚为低电平)。

3.3.1 头文件(key_driver.h)
#ifndef _KEY_DRIVER
#define _KEY_DRIVER

// 按键1配置:GPIO组=GPIOB,引脚=PB0
#define KEY_GPIO_GROUP GPIOB
#define KEY_GPIO_PIN   GPIO_PIN_0

// 读取按键状态(1=按下,0=未按下)
int Key_Read(void);
// 按键测试任务函数(死循环检测按键,控制LED2)
void Key_Test(void);
// 按键初始化(GPIO配置)
int Key_Init(void);

#endif
3.3.2 驱动文件(key_driver.c)
#include "key_driver.h"
#include "cmsis_os.h"
#include "gpio.h"
#include "stm32g4xx_hal.h"

// 按键初始化:配置PB0为上拉输入模式
int Key_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    
    __HAL_RCC_GPIOB_CLK_ENABLE();  // 使能GPIOB时钟

    // 初始电平设置:PB0输出高电平(上拉模式)
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);

    // GPIO配置参数
    GPIO_InitStruct.Pin = GPIO_PIN_0;          // 引脚:PB0(按键1)
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;    // 输入模式
    GPIO_InitStruct.Pull = GPIO_PULLUP;        // 上拉电阻
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);    // 应用配置
    
    return 0;
}

// 读取按键状态:低电平表示按下(因硬件下拉电阻)
int Key_Read(void)
{
    if (GPIO_PIN_RESET == HAL_GPIO_ReadPin(KEY_GPIO_GROUP, KEY_GPIO_PIN))
        return 1;  // 按键按下,返回1
    else
        return 0;  // 按键未按下,返回0
}

// 按键测试任务函数:死循环检测按键,控制LED2(PC9)亮灭
void Key_Test(void)
{
    int val;         // 当前按键状态
    int last_val = 0;// 上一次按键状态
	
    Key_Init();      // 初始化按键引脚

    while (1)
    {
        val = Key_Read();  // 读取当前按键状态
        if(val != last_val)  // 按键状态发生变化(按下/松开)
        {
            if(val)  // 按键按下:LED2(PC9)亮(低电平点亮)
                HAL_GPIO_WritePin(GPIOC, GPIO_PIN_9, GPIO_PIN_RESET);
            else     // 按键释放:LED2(PC9)灭(高电平熄灭)
                HAL_GPIO_WritePin(GPIOC, GPIO_PIN_9, GPIO_PIN_SET);
            last_val = val;  // 更新上一次状态,避免重复触发
        }
        osDelay(10);  // FreeRTOS延时函数,降低CPU占用率(10ms检测一次)
    }
}

四、FreeRTOS 多任务创建与配置

多任务配置核心在app_freertos.c文件中,需创建两个任务:Key_Test(按键控制 LED2)和Led_Test(LED1 闪烁),通过xTaskCreate函数将任务加入 FreeRTOS 调度器。

4.1 自定义任务创建(按键控制任务)

app_freertos.c中添加自定义任务函数声明和创建代码:

/* USER CODE BEGIN FunctionPrototypes */
// 自定义任务函数:按键控制LED2(对应Key_Test死循环)
void MyTask(void *argument)
{
	while(1)
	{
		Key_Test();  // 调用按键测试函数(死循环)
	}
}
/* USER CODE END FunctionPrototypes */

// FreeRTOS初始化函数:创建任务并加入调度器
void MX_FREERTOS_Init(void) {
  /* add threads, ... */
  // 创建自定义任务:MyTask
  xTaskCreate(
      MyTask,                // 任务函数
      "myfirsttask",         // 任务名称(调试用)
      128,                   // 任务栈大小(128字,即512字节)
      NULL,                  // 任务参数(无)
      osPriorityAboveNormal, // 任务优先级(高于正常优先级)
      NULL                   // 任务句柄(无需操作,传NULL)
  );
}

4.2 默认任务适配(LED 闪烁任务)

CubeMX 生成工程时会默认创建StartDefaultTask任务,直接在该任务中调用Led_Test函数(LED1 闪烁):

// FreeRTOS默认任务:适配LED1闪烁任务
void StartDefaultTask(void *argument)
{
  /* USER CODE BEGIN StartDefaultTask */
  /* Infinite loop */
  for(;;)
  {
	Led_Test();  // 调用LED测试函数(死循环闪烁)
    osDelay(1);  // FreeRTOS延时(仅占位,实际延时由mdelay控制)
  }
  /* USER CODE END StartDefaultTask */
}

关键说明

  • 任务栈大小:128 字(STM32G4 为 32 位 MCU,1 字 = 4 字节,即 512 字节),足够驱动函数运行;
  • 任务优先级:osPriorityAboveNormal(高于正常优先级),确保按键响应及时;
  • 死循环必要性:FreeRTOS 任务必须是死循环(while(1)/for(;;)),若退出会导致任务删除,影响调度器稳定。

五、测试现象与多任务验证

将程序烧录到 CT117E-M4 开发板,观察到以下现象,验证多任务调度正常:

  1. LED1(PC8)持续 500ms 循环闪烁,无卡顿;
  2. 按下按键 1(PB0):LED2(PC9)立即点亮,LED1 仍正常闪烁;
  3. 松开按键 1:LED2 立即熄灭,LED1 闪烁不受任何影响。

现象截图:

  • 按键按下时(LED2 亮,LED1 闪烁中):

    请添加图片描述

  • 按键松开时(LED2 灭,LED1 持续闪烁):
    请添加图片描述

核心结论:FreeRTOS 调度器成功实现两个死循环任务的交替执行,即使单个任务处于无限循环,也不会阻塞其他任务,彻底解决了裸机开发中 “单任务阻塞” 的痛点。

六、下一篇预告

本次我们通过 LED 和按键验证了 FreeRTOS 多任务的核心功能,下一篇将深入底层,学习 ARM 架构相关知识,为理解 FreeRTOS 调度原理打下基础:

  1. ARM 硬件架构核心概念;
  2. 常用 ARM 汇编指令;
  3. 汇编实例分析(任务切换底层逻辑)。

七、结尾

本篇笔记的核心是 “FreeRTOS 多任务创建与驱动适配”,我们完成了三个核心驱动(定时器、LED、按键)的开发,通过xTaskCreate函数创建两个任务,验证了多任务并发执行的效果。

关键收获总结:

  1. 掌握 FreeRTOS 原生任务创建函数xTaskCreate的参数配置;
  2. 理解多任务的核心逻辑:死循环任务通过调度器交替执行,互不阻塞;
  3. 学会适配不同开发板的硬件驱动(定时器、GPIO),解决硬件差异带来的兼容性问题。

FreeRTOS 的多任务调度依赖底层硬件架构(如 ARM 的寄存器、异常机制),下一篇将深入 ARM 架构基础知识,帮助大家从 “会用” 过渡到 “理解原理”。我是 Hello_Embed,FreeRTOS 系列笔记将持续更新,欢迎关注,一起扎实掌握嵌入式实时操作系统的开发技能!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值