基于 STM32CubeMX 实现 FreeRTOS 可视化移植的多任务 LED 控制实践(基于 STM32F103ZET6)
本实验基于STM32F103ZET6开发板,通过STM32CubeMX工具实现FreeRTOS可视化移植,完成多任务LED控制。实验重点包括:1)使用TIM3定时器配置运行时间统计;2)通过串口DMA输出任务状态(vTaskList)和CPU占用率(vTaskGetRunTimeStats);3)建立监控任务周期打印系统信息。
1. 概述
1.1 实验目的
本实验通过使用 STM32CubeMX 工具对 FreeRTOS 进行可视化移植,并基于 STM32F103ZET6 开发板实现多任务 LED 不同频率闪烁,以及通过重定向打印任务运行状态和运行时长。旨在掌握嵌入式实时操作系统的移植与任务调度机制。通过本实验,不仅加深了对 STM32 硬件架构与 HAL 库的理解,也提升了在多任务并行与系统资源管理方面的实践能力。该实验为今后在嵌入式系统开发、物联网设备控制及实时系统设计等工作中打下了坚实的基础。
1.2 硬件准备
本实验所使用的硬件平台为一块 STM32F103ZET6 最小系统开发板,该开发板基于 ARM Cortex-M3 内核,主频 72MHz,性能稳定、资源丰富,适合进行 FreeRTOS 的移植与多任务调度实验。板载集成了两个用户可控的 LED 灯,分别连接至 GPIO 引脚,可用于任务状态的可视化显示与实验验证。

程序下载与调试使用 J-Link V8 仿真器,其具备高速稳定的调试性能,能够实现对目标芯片的实时在线烧录与断点调试。同时,本实验也支持使用 ST-Link 仿真器 进行程序下载。若更换为 ST-Link,仅需在 Keil MDK 工程中修改 Debug 配置部分,STM32CubeMX 的外设及 FreeRTOS 配置保持完全一致,无需任何改动。


通过上述硬件环境的搭建,能够为后续的 FreeRTOS 任务创建、调度测试以及多任务 LED 控制实验提供良好的基础和可靠的运行平台。
2 实验原理
2.1 任务介绍
2.1.1 LED任务
本实验一共有两个LED任务,在任务中进行了引脚不同频率的反转,使用系统延时对任务进行一定时间的阻塞,从而降低闪烁频率达到肉眼可见
2.1.2 monitor任务
本任务分别调用了freeRTOS提供的两个函数,通过串口打印任务的信息。
2.2 任务状态表
2.2.1 状态表信息
| Task Name | State | Priority | Stack | Task Num |
|---|---|---|---|---|
| monitor_task | X | 8 | 48 | 3 |
| IDLE | R | 0 | 118 | 7 |
| led1_task | B | 8 | 102 | 2 |
| led0_task | B | 8 | 102 | 1 |
2.2.2 字段说明
| 列名 | 含义 |
|---|---|
| Task Name | 任务名称,对应在 osThreadNew() 时设置的名字 |
| State | 当前任务状态:R=Ready(就绪中),B=Blocked(阻塞/延时等待),S=Suspended(挂起/未运行)X = 运行中(Running) |
| Prio | 任务优先级(数值越大优先级越高) |
| Stack | 任务剩余栈空间(单位:字节),0 表示栈已满,需要注意 |
| Num | 任务编号,由 FreeRTOS 内核分配,用于内部识别 |
2.2.3 获取方法
状态表获取比较容易,只需要在STM32CUBEMX中配置开启监控,然后直接在任务中调用vTaskList 函数即可
2.3 任务运行时统计表
2.3.1 表信息
| Task Name | RunTime (ticks) | CPU Usage (%) |
|---|---|---|
| monitor_task | 6,243,617 | 5% |
| IDLE | 57,807,583 | 55% |
| led0_task | 1,887 | <1% |
| led1_task | 6,137 | <1% |
2.3.2 获取方法
任务运行时统计表获取稍显复杂,主要有如下几个步骤:
步骤1:配置溢出中断定时器作为统计时基,根据所需精度,推荐中断周期为10us
步骤2:在STM32CUBEMX中启动监控任务配置
步骤3:实现弱函数代码,并在定时中断中完成自定义变量的自加
步骤4:监控任务中调用vTaskGetRunTimeStats获取数据
3 STM32CubeMX配置
3.1 系统配置
与之前裸机开发不同,这里时基选择TIM2,选择其他通用定时器也可以。

3.2 配置时钟源


3.3 串口打印设置
3.3.1 设置串口参数

3.3.2 添加串口DMA通道

3.3.3 启动串口中断

3.4 LED引脚设置
通用推挽输出,默认高电平

3.5 freeRTOS设置
3.5.1 关闭NEWLIB
USE_NEWLIB_REENTRANT 选项 只适用于 GCC 编译器(如 STM32CubeIDE、ARM GCC 工具链)。
在 Keil MDK 环境下必须保持 Disable(关闭)。

3.5.2 调试信息配置
| 功能 | CubeMX 选项 | 作用 |
|---|---|---|
| 任务状态统计 | Use Trace Facility | 必须 Enable,支持 vTaskList() |
| 任务信息格式化 | Use Stats Formatting Functions | 必须 Enable,支持 vTaskList() 和 vTaskGetRunTimeStats() |
| 运行时间统计 | Generate Run Time Stats | Enable,支持 vTaskGetRunTimeStats() |

3.5.3 添加任务

在 FreeRTOS 中,每个任务在创建时都需要分配独立的堆栈空间,用于保存函数调用现场、局部变量以及中断返回信息等。
默认情况下,任务最小堆栈大小为 128 个字(word),由于 STM32 的字长为 4 字节,因此对应的最小堆栈空间为:
128 (word)×4 (bytes/word)=512 (bytes)
对于结构简单、逻辑较少的任务(如 LED 闪烁任务),512 字节的堆栈通常足够使用;
但对于 复杂任务(如包含 printf 调试输出、浮点运算或大量局部变量),该堆栈空间往往不足,可能导致任务运行异常或系统崩溃。
因此,在任务创建时应根据任务功能 适当调大堆栈大小,通常建议按照 2 的指数级(128、256、512、1024 等) 逐步调整和验证。在 FreeRTOS 中,vTaskList() 显示的 堆栈剩余大小 的单位是 “字”(Word),不是字节。以下是任务建议堆栈值:
| 任务类型 | 建议堆栈(Word) | 实际字节(Byte) | 说明 |
|---|---|---|---|
| 简单 LED 闪烁 | 128 | 512 | 几乎不使用局部变量和函数调用 |
| 按键扫描 / 状态检测 | 128~256 | 512~1024 | 少量逻辑判断 |
| 通信任务(UART/USART) | 256~512 | 1~2 KB | 含字符串、缓冲区等 |
| 监控任务(vTaskList + printf) | 512~1024 | 2~4 KB | printf 占栈大,注意溢出 |
| LCD 显示任务 | 512~1024 | 2~4 KB | 含字符串解析、绘制函数等 |
| 复杂逻辑(JSON、算法) | ≥1024 | ≥4 KB | 深层函数调用多 |
| 主任务(初始化/调度) | 512~2048 | 2~8 KB | 初始化内存、日志打印多 |
3.5.4 总堆内存设置
所有任务的堆栈空间最终都来自于系统总堆内存(configTOTAL_HEAP_SIZE),
因此应保证 所有任务的堆栈大小之和小于总堆内存大小。总堆内存设置建议如下表所示:
| 芯片型号 | SRAM 容量 | 推荐总堆内存比例 | 推荐堆内存大小(Bytes) | 适用场景与说明 |
|---|---|---|---|---|
| STM32F103C8T6 | 20 KB | 10% ~ 20% | 2 KB ~ 4 KB | 适合 2~3 个简单任务(如 LED 闪烁、按键扫描等) |
| STM32F103ZET6 | 64 KB | 20% ~ 40% | 8 KB ~ 24 KB | 适合中型多任务系统(如监控任务、通信任务、UI 任务等) |
| STM32F407VET6 | 192 KB | 30% ~ 50% | 60 KB ~ 96 KB | 适合复杂嵌入式系统(带网络协议栈或文件系统) |
| STM32H743ZI | 1 MB | 30% ~ 60% | 300 KB ~ 600 KB | 适合大型实时系统(多线程 + 网络通信 + 显示界面) |
否则,任务在创建时可能因内存不足而失败。配置和计算时要特别注意 单位换算(字与字节的区别),以避免因误差导致的内存越界问题。
设置页面如下所示:

3.5.5 运行时间计数器配置
FreeRTOS 运行时间统计只需要一个 单调递增计数器,通过读取计数器值即可计算各任务的 CPU 占用时间。
原则:统计定时器应独立于系统 Tick,否则计数器更新可能影响内核调度精度。
用户自定义一个32位的数值作为任务运行时长计数值,这样就能大大避免了定时器溢出的问题。定时器设置为中断溢出,精度按10us一次中断,每次中断然自定义的变量加一,然后用这个变量作为任务计数值。


3.6 project设置


4 keil MDK配置
4.1 下载调试配置


4.2 串口打印配置

5. VSCode
5.1 printf 重定向
把 printf 重定向到 USART 实现串口日志输出
5.1.1 在usart.c中添加代码
/* USER CODE BEGIN 1 */
int fputc(int ch, FILE * file){ //打印重定�?
HAL_UART_Transmit(&huart1,(uint8_t*)&ch,1,1000);
return ch;
}
/* USER CODE END 1 */
5.1.2 在对应usart.h中添加引用
/* USER CODE BEGIN Includes */
#include "stdio.h"
/* USER CODE END Includes */
5.2 任务日志打印
5.2.1 实现弱函数
在freertos.c中对宏定义的函数进行弱函数定义
extern uint32_t runtimeCounter;
/* Hook prototypes */
void configureTimerForRunTimeStats(void);
unsigned long getRunTimeCounterValue(void);
/* USER CODE BEGIN 1 */
/* Functions needed when configGENERATE_RUN_TIME_STATS is on */
__weak void configureTimerForRunTimeStats(void)
{
HAL_TIM_Base_Start_IT(&htim7);
}
__weak unsigned long getRunTimeCounterValue(void)
{
return runtimeCounter;
}
/* USER CODE END 1 */
5.2.2 计数器自加
其中获取计数值的用户自定义变量需要在定时器中断中进行自加
uint32_t runtimeCounter = 0;
void TIM7_IRQHandler(void)
{
/* USER CODE BEGIN TIM7_IRQn 0 */
/* USER CODE END TIM7_IRQn 0 */
HAL_TIM_IRQHandler(&htim7);
/* USER CODE BEGIN TIM7_IRQn 1 */
runtimeCounter++;
/* USER CODE END TIM7_IRQn 1 */
}
5.2.3 完善monitor任务
/* USER CODE BEGIN Header_monitor_task_handler */
/**
* @brief Function implementing the monitor_task thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_monitor_task_handler */
void monitor_task_handler(void *argument)
{
/* USER CODE BEGIN monitor_task_handler */
char taskListBuf[128];
char runTimeStatsBuf[128];
/* Infinite loop: 每秒打印一次系统状态 */
for(;;)
{
printf("\r\n========== FreeRTOS Monitor ==========\r\n");
// 1️⃣ 打印任务列表(任务名称、状态、优先级、剩余栈空间、任务编号)
vTaskList(taskListBuf);
printf("Task Name State Prio Stack Num\r\n%s\r\n", taskListBuf);
// 2️⃣ 打印 CPU 占用率和运行时间统计
vTaskGetRunTimeStats(runTimeStatsBuf);
printf("Task RunTime CPU Usage(%%)\r\n%s\r\n", runTimeStatsBuf);
// 3️⃣ 打印系统启动时间(毫秒 / 秒)
printf("System uptime: %lu ms (%lu s)\r\n", osKernelGetTickCount(), osKernelGetTickCount()/1000);
printf("======================================\r\n");
osDelay(1000); // 延时 1 秒,循环刷新状态
}
/* USER CODE END monitor_task_handler */
}
5.3 led任务完善
/* USER CODE BEGIN Header_led0_task_handler */
/**
* @brief Function implementing the led0_task thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_led0_task_handler */
void led0_task_handler(void *argument)
{
/* USER CODE BEGIN led0_task_handler */
/* Infinite loop */
for(;;)
{
HAL_GPIO_TogglePin(LED0_GPIO_Port,LED0_Pin);
osDelay(1000);
}
/* USER CODE END led0_task_handler */
}
/* USER CODE BEGIN Header_led1_task_handler */
/**
* @brief Function implementing the led1_task thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_led1_task_handler */
void led1_task_handler(void *argument)
{
/* USER CODE BEGIN led1_task_handler */
/* Infinite loop */
for(;;)
{
HAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin);
osDelay(300);
}
/* USER CODE END led1_task_handler */
}
6. 注意点
6.1 打印日志

6.2 监控时长
在当前实现中,使用的定时器为 16 位(最大计数 65535),预分频 7199,因此计数器在运行约 6.5 秒 后就会溢出,导致 CPU 占用率统计出现错误。
6.3 监控任务堆栈
任务越多,变量就应该越大,否则打印任务列表有截断,或者无法打印日志
char taskListBuf[128];
char runTimeStatsBuf[128];
这个变量调大后,同步的任务堆栈也要调大,否则程序运行异常,我试了下,最大可以调节到256,之后再大程序就无法正常运行了,可能和STM32F103ZET6的硬件最大SRAM有关。

更多推荐



所有评论(0)