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有关。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐