基于 STM32CubeMX 实现 FreeRTOS 可视化移植的多任务 LED 控制实践(基于 STM32F103ZET6)
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 开启配置
在STM32CubeMx中完成如下配置开启
功能 | CubeMX 选项 | 作用 |
---|---|---|
任务状态统计 | Use Trace Facility | 必须 Enable,支持 vTaskList() |
任务信息格式化 | Use Stats Formatting Functions | 必须 Enable,支持 vTaskList() 和 vTaskGetRunTimeStats() |
运行时间统计 | Generate Run Time Stats | Enable,支持 vTaskGetRunTimeStats() |
2.2 任务状态信息
调用vTaskList(taskListBuf)获取相关信息,信息解析如下
列名 | 含义 |
---|---|
Task Name | 任务名称,对应在 osThreadNew() 时设置的名字 |
State | 当前任务状态:R=Running(运行中),B=Blocked(阻塞/延时等待),X=Suspended(挂起/未运行),其他还有 D=Deleted |
Prio | 任务优先级(数值越大优先级越高) |
Stack | 任务剩余栈空间(单位:字节),0 表示栈已满,需要注意 |
Num | 任务编号,由 FreeRTOS 内核分配,用于内部识别 |
2.3 任务时长及CUP占用
调用vTaskGetRunTimeStats(runTimeStatsBuf)获取相关信息。
为了统计 FreeRTOS 任务的运行时间,需要在硬件上准备一个高精度计数器并完成宏定义。通常步骤如下:
步骤1:定时器初始化
在 STM32 中,通过 CubeMX 配置一个独立定时器(如 TIM3)为递增计数模式,用作任务运行时间的计时源。
定时器应具有足够高的计数频率(如 1 MHz),以保证统计精度,同时避免与系统 Tick 或其他定时器冲突。
步骤2:宏定义绑定 FreeRTOS
在用户代码中定义两个宏,使 FreeRTOS 内核能够使用该定时器进行任务运行时间统计:
#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() HAL_TIM_Base_Start(&htim3)
#define portGET_RUN_TIME_COUNTER_VALUE() __HAL_TIM_GET_COUNTER(&htim3)
第一个宏负责启动计时器,第二个宏负责获取当前计数值。
步骤3:任务运行时间统计
FreeRTOS 提供 vTaskGetRunTimeStats() 等函数,可以调用上述计数器来计算各任务的累积运行时间及 CPU 占用率。
通过串口打印结果,可以实时观察各任务的运行状态和 CPU 利用率,从而方便调试和系统性能分析。
2.4 建立监控任务
在本实验中,为了实时监测 FreeRTOS 系统中各任务的运行状态与调度情况,需要建立一个独立的监控任务(Monitor Task)。
首先,在 STM32CubeMX 中完成 USART 外设的初始化,并开启 DMA 发送功能和中断功能,以确保系统在多任务环境下能够高效、稳定地进行串口日志输出。同时,通过 重定向 printf()
函数,实现将标准输出流映射到串口,从而便于在终端上查看任务运行信息。
随后,在 FreeRTOS 配置界面中新建一个名为 monitor_task
的任务,设置适当的任务优先级与堆栈空间。该任务将周期性调用 vTaskList()和
vTaskGetRunTimeStats()等系统函数,获取当前所有任务的状态信息,并通过串口打印输出。
该监控任务的建立,不仅为系统调试与性能分析提供了重要依据,也为后续实现 CPU 占用率统计与任务运行时间监控奠定了基础。
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 添加任务
3.5.4 运行时间计数器配置
FreeRTOS 运行时间统计只需要一个 单调递增计数器,通过读取计数器值即可计算各任务的 CPU 占用时间。
原则:统计定时器应独立于系统 Tick,否则计数器更新可能影响内核调度精度。
在当前实现中,使用的定时器为 16 位(最大计数 65535),预分频 7199,因此计数器在运行约 6.5 秒 后就会溢出,导致 CPU 占用率统计出现错误。
为避免溢出,可使用 32 位定时器或调低定时器分频,使溢出周期远大于统计周期,从而保证长期运行的准确性。在 STM32F103ZET6 单片机上,由于所用定时器仅为 16 位,其计数范围最大为 65535,即使调低分频,运行时间统计也只能支持约 6.5 秒 的最大统计周期。因此,在该平台上无法实现长期稳定的 CPU 占用率统计,这已接近硬件定时器的极限。
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 任务日志打印
以下内容均在freertos.c中完成:
5.2.1 添加inclues引用
/* USER CODE BEGIN Includes */
#include "usart.h"
#include "tim.h"
/* USER CODE END Includes */
5.2.2 添加宏定义
/* USER CODE BEGIN 1 */
/* Functions needed when configGENERATE_RUN_TIME_STATS is on */
__weak void configureTimerForRunTimeStats(void)
{HAL_TIM_Base_Start(&htim3); // 启动 TIM3
}__weak unsigned long getRunTimeCounterValue(void)
{return __HAL_TIM_GET_COUNTER(&htim3);
}
/* USER CODE END 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有关。