目录
- 一、前言
- 二、多任务核心接口与开发准备
- 三、核心驱动实现(定时器 + LED + 按键)
- 3.1 高精度定时器驱动(延时基础)
- 3.2 LED 驱动(PC8-15 配置与控制)
- 3.3 按键驱动(PB0 输入与 LED2 控制)
- 四、FreeRTOS 多任务创建与配置
- 4.1 自定义任务创建(按键控制任务)
- 4.2 默认任务适配(LED 闪烁任务)
- 五、测试现象与多任务验证
- 六、下一篇预告
- 七、结尾
一、前言
大家好,我是 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 开发准备
- 在工程
Drivers文件夹下新建 “User_Driver” 目录,存放定时器、LED、按键的驱动文件; - 配置驱动文件的编译路径(确保编译器能识别头文件和源文件);
- 确认 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 开发板,观察到以下现象,验证多任务调度正常:
- LED1(PC8)持续 500ms 循环闪烁,无卡顿;
- 按下按键 1(PB0):LED2(PC9)立即点亮,LED1 仍正常闪烁;
- 松开按键 1:LED2 立即熄灭,LED1 闪烁不受任何影响。
现象截图:
-
按键按下时(LED2 亮,LED1 闪烁中):

-
按键松开时(LED2 灭,LED1 持续闪烁):

核心结论:FreeRTOS 调度器成功实现两个死循环任务的交替执行,即使单个任务处于无限循环,也不会阻塞其他任务,彻底解决了裸机开发中 “单任务阻塞” 的痛点。
六、下一篇预告
本次我们通过 LED 和按键验证了 FreeRTOS 多任务的核心功能,下一篇将深入底层,学习 ARM 架构相关知识,为理解 FreeRTOS 调度原理打下基础:
- ARM 硬件架构核心概念;
- 常用 ARM 汇编指令;
- 汇编实例分析(任务切换底层逻辑)。
七、结尾
本篇笔记的核心是 “FreeRTOS 多任务创建与驱动适配”,我们完成了三个核心驱动(定时器、LED、按键)的开发,通过xTaskCreate函数创建两个任务,验证了多任务并发执行的效果。
关键收获总结:
- 掌握 FreeRTOS 原生任务创建函数
xTaskCreate的参数配置; - 理解多任务的核心逻辑:死循环任务通过调度器交替执行,互不阻塞;
- 学会适配不同开发板的硬件驱动(定时器、GPIO),解决硬件差异带来的兼容性问题。
FreeRTOS 的多任务调度依赖底层硬件架构(如 ARM 的寄存器、异常机制),下一篇将深入 ARM 架构基础知识,帮助大家从 “会用” 过渡到 “理解原理”。我是 Hello_Embed,FreeRTOS 系列笔记将持续更新,欢迎关注,一起扎实掌握嵌入式实时操作系统的开发技能!
1058

被折叠的 条评论
为什么被折叠?



