在STM32F103上进行FreeRTOS移植和配置(STM32CubeIDE)

个人博客:blogs.wurp.top

一. 创建新工程 (以STM32CubeIDE为例)

  1. 核心组件:

    • 硬件: STM32F103开发板 (如常见的"Blue Pill"板)
    • 开发环境:
      • 选项1: Keil MDK-ARM (uVision) + STM32F1xx Device Family Pack (DFP)
      • 选项2: STM32CubeIDE (推荐,集成度更高)
    • 软件:
      • FreeRTOS源码 (从官网或STM32CubeF1库获取)
      • STM32标准外设库 (StdPeriph) 或 HAL库 (推荐使用CubeIDE自带的HAL)
  2. 打开STM32CubeIDE。

  3. File -> New -> STM32 Project

  4. Target Selection窗口:

    • Part Number搜索框输入STM32F103C8 (或你的具体型号,如F103RC)。
    • 选择正确的型号 (例如STM32F103C8Tx)。
    • 点击Next
  5. 输入项目名称 (如FreeRTOS_Demo),选择项目位置。

  6. 点击Finish。IDE会自动生成基于HAL库的初始化代码框架。

二. 启用并配置FreeRTOS

  1. Project Explorer中双击打开*.ioc文件 (CubeMX配置文件)。
  2. 转到Middleware选项卡。
  3. 在左侧列表中选择FREERTOS
  4. Mode下拉菜单中,选择InterfaceCMSIS_V2 (这是FreeRTOS的CMSIS-RTOS API v2封装层,标准化且易用)。
  5. 配置内核参数 (关键步骤!):
    • CONFIG 子选项卡:
      • Kernel settings:
        • TICK_RATE_HZ: 系统时钟节拍频率。通常设置为1000 (1ms)。注意: 这个值必须与SysTick中断频率匹配(在Clock Configuration中设置,通常HCLK / 1000)。
        • MAX_PRIORITIES: 最大任务优先级数。默认56通常足够,资源紧张时可减小(如7)。
        • MINIMAL_STACK_SIZE: 最小任务栈大小(字为单位,1字=4字节)。默认128字(512字节)。重要: 根据任务复杂度调整!简单任务可能够用,复杂任务(大量局部变量、函数调用深)需要增加(如256512字)。栈溢出是常见错误源!
        • TOTAL_HEAP_SIZE: FreeRTOS堆总大小(字节)。这是关键资源! STM32F103C8只有20KB RAM,需谨慎分配。初始值10240(10KB)是常用起点。观察后续应用使用情况调整(使用xPortGetFreeHeapSize()uxTaskGetStackHighWaterMark()监控)。
        • Memory management scheme: 内存分配方案。推荐heap_4 (碎片管理较好)。heap_1heap_2更简单但碎片严重。
      • Include parameters: 按需启用API(如vTaskDelayUntil(), Task notifications等),默认通常够用。
    • Tasks and Queues 子选项卡 (可选):可以在这里图形化创建初始任务(设置名称、优先级、栈大小、入口函数等)。对于学习,建议在代码中手动创建。
    • Timers and Semaphores 子选项卡 (可选):图形化创建软件定时器、信号量、互斥量、队列等。同样建议在代码中学习创建。
  6. 配置时钟 (SysTick):
    • 转到Clock Configuration选项卡。
    • 确保HCLK (AHB总线时钟) 是你期望的主频 (如72MHz)。
    • 确认SysTick的时钟源是HCLK (通常默认是)。
    • 关键: 确保SysTick中断频率等于你在FreeRTOS中设置的TICK_RATE_HZ。FreeRTOS依赖SysTick作为时基。例如,HCLK=72MHz,TICK_RATE_HZ=1000,则SysTick Reload Value应设置为72000 (72,000,000 / 1000 = 72,000)。
  7. 保存配置: 点击File -> Save或工具栏的保存图标。CubeIDE会自动生成FreeRTOS配置代码和初始化代码。

三. 理解自动生成的FreeRTOS相关代码

保存.ioc后,CubeIDE会自动更新工程:

  • Core/Src/freertos.c: 包含MX_FREERTOS_Init()函数。如果你在CubeMX中图形化创建了任务/信号量等,它们的创建代码会在这里生成。这是FreeRTOS对象的初始化入口。
  • Core/Inc/FreeRTOSConfig.h: 极其重要! 这是FreeRTOS的主要配置文件。它定义了:
    • 你在CubeMX中设置的所有参数 (configTICK_RATE_HZ, configTOTAL_HEAP_SIZE, configMINIMAL_STACK_SIZE, configMAX_PRIORITIES, configUSE_PREEMPTION等)。
    • 硬件相关设置 (如configCPU_CLOCK_HZ, configSYSTICK_CLOCK_HZ)。
    • 启用的API函数 (configUSE_MUTEXES, configUSE_RECURSIVE_MUTEXES, configUSE_COUNTING_SEMAPHORES, configUSE_QUEUES等)。
    • 内存对齐 (configTOTAL_HEAP_SIZE)。
    • 钩子函数 (configUSE_IDLE_HOOK, configUSE_TICK_HOOK等)。
    • 移植层定义: configKERNEL_INTERRUPT_PRIORITY (通常设置为15,即最低优先级,确保FreeRTOS API调用不会屏蔽其他中断), configMAX_SYSCALL_INTERRUPT_PRIORITY (通常设置为5,高于此优先级的中断不会被FreeRTOS管理,也不能调用FreeRTOS API)。STM32F103的优先级数值越小优先级越高 (0最高, 15最低)。
  • Middlewares/Third_Party/FreeRTOS/Source/: 存放FreeRTOS内核源码 (tasks.c, queue.c, list.c, timers.c等) 和内存管理方案 (heap_x.c)。
  • Middlewares/Third_Party/FreeRTOS/Source/portable/[Compiler]/ARM_CM3/: 包含针对Cortex-M3架构和特定编译器 (如GCC, Keil) 的移植层代码。最关键的文件是port.c (实现任务切换、SysTick中断处理、PendSV中断处理等) 和portmacro.h (定义数据类型、关键宏、内联汇编等)。

四. 编写应用程序代码 (创建任务)

通常在Core/Src/main.cmain()函数中或MX_FREERTOS_Init()之后创建任务。

示例:创建两个简单任务 (LED闪烁 & 串口打印)

c 复制代码
/* Private includes ----------------------------------------------------------*/
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "gpio.h" // 确保包含HAL GPIO头文件
#include "usart.h" // 如果使用串口

/* Private function prototypes -----------------------------------------------*/
void vTaskLED(void *pvParameters);
void vTaskPrint(void *pvParameters);

int main(void) {
  HAL_Init();
  SystemClock_Config(); // CubeIDE自动生成
  MX_GPIO_Init();       // CubeIDE自动生成
  MX_USART1_UART_Init(); // 如果使用串口,CubeIDE自动生成
  MX_FREERTOS_Init();   // **重要!** 初始化FreeRTOS对象(任务/队列等,如果有图形化创建)和内核。这个函数会调用vTaskStartScheduler()。

  /* 如果MX_FREERTOS_Init()里没有启动调度器,或者你想在它之后创建任务,需要手动调用vTaskStartScheduler() */
  // vTaskStartScheduler();

  /* 正常情况下,vTaskStartScheduler() 永远不会返回 */
  while (1);
}

/* 任务1:闪烁LED */
void vTaskLED(void *pvParameters) {
  (void)pvParameters; // 防止未使用参数警告
  const TickType_t xDelay500ms = pdMS_TO_TICKS(500); // 将毫秒转换为节拍数

  for (;;) {
    HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13); // 假设LED在PC13
    vTaskDelay(xDelay500ms); // 阻塞延时500ms,让出CPU
  }
}

/* 任务2:串口打印 */
void vTaskPrint(void *pvParameters) {
  (void)pvParameters;
  const TickType_t xDelay1000ms = pdMS_TO_TICKS(1000);
  char msg[] = "Hello from FreeRTOS!\r\n";

  for (;;) {
    HAL_UART_Transmit(&huart1, (uint8_t*)msg, strlen(msg), HAL_MAX_DELAY); // 假设使用USART1
    vTaskDelay(xDelay1000ms); // 阻塞延时1秒
  }
}

/* 在合适的地方创建任务 (例如在MX_FREERTOS_Init()之前或在MX_FREERTOS_Init()里面) */
void MX_FREERTOS_Init(void) {
  /* 创建LED任务 */
  if (xTaskCreate(
        vTaskLED,            /* 任务函数指针 */
        "LED Task",          /* 任务名称 (字符串) */
        128,                 /* 栈深度 (单位:字,4字节/字) - 根据需求调整! */
        NULL,                /* 传递给任务的参数 */
        2,                   /* 任务优先级 (0是最低,configMAX_PRIORITIES-1是最高) */
        NULL                 /* 任务句柄 (用于后续引用任务,可设为NULL) */
      ) != pdPASS) {
    /* 任务创建失败!处理错误 (例如循环或挂起) */
    Error_Handler();
  }

  /* 创建打印任务 */
  if (xTaskCreate(
        vTaskPrint,
        "Print Task",
        256,                 /* 串口任务可能需要稍大一点的栈 */
        NULL,
        1,                   /* 优先级低于LED任务 */
        NULL
      ) != pdPASS) {
    Error_Handler();
  }

  /* FreeRTOS内核启动 */
  vTaskStartScheduler(); // **核心!启动任务调度器,永不返回(除非出错)**
}

五. 编译与下载

  1. 点击STM32CubeIDE工具栏上的Build (锤子图标) 编译项目。
  2. 连接开发板。
  3. 点击Debug (虫子图标) 或 Run (播放图标) 将程序下载到目标板并启动调试/运行。

六. 调试与监控

  • 调试器 (ST-Link, J-Link): 单步执行、设置断点、查看变量、寄存器、内存、调用栈。FreeRTOS感知调试器能显示任务列表、队列、信号量状态。
  • 串口输出: 通过vTaskPrint任务输出状态信息。
  • LED指示: 观察vTaskLED任务的行为。
  • FreeRTOS API 监控:
    • uxTaskGetStackHighWaterMark(TaskHandle_t xTask): 获取任务栈的历史最小剩余空间(高水位线),强烈推荐用于检测栈是否设置过小。值接近0表示危险。
    • xPortGetFreeHeapSize(): 获取当前剩余堆大小,监控内存使用和碎片。
    • vTaskList(char *pcWriteBuffer): (需要启用configUSE_TRACE_FACILITYconfigUSE_STATS_FORMATTING_FUNCTIONS) 将任务状态信息格式化为字符串输出(任务名、状态、优先级、栈高水位线)。非常有用!

七. 关键注意事项与常见问题

  1. 栈大小 (Stack Size):

    • 这是最常见的问题!任务栈溢出会导致难以预测的崩溃(覆盖其他内存)。
    • 初始值MINIMAL_STACK_SIZE通常不够用于实际任务。
    • 使用uxTaskGetStackHighWaterMark()在任务运行时监控栈使用。目标是在任务生命周期内保持高水位线有合理的余量(例如>100字节)。
    • 复杂的函数调用、大的局部变量数组、递归都会消耗大量栈空间。
    • 如果使用printf等库函数,它们内部使用的栈可能很大。
  2. 堆大小 (TOTAL_HEAP_SIZE):

    • STM32F103 RAM有限 (20KB for C8, 64KB for RC)。
    • 分配的堆需要容纳:所有任务栈 + 任务控制块 (TCB) + 动态创建的对象 (队列、信号量、软件定时器、任务通知、动态分配内存)
    • 监控xPortGetFreeHeapSize()。如果堆耗尽,创建对象会失败。
    • 在资源紧张的F103上,尽量静态分配 (在CubeMX中图形化创建或使用xTaskCreateStatic())。
  3. 中断优先级 (configMAX_SYSCALL_INTERRUPT_PRIORITY):

    • 理解: FreeRTOS需要管理一些中断以进行任务切换(主要是PendSV)和提供API(如xQueueSendFromISR)。
    • configMAX_SYSCALL_INTERRUPT_PRIORITY定义了一个中断优先级阈值。
      • 高于 此阈值的中断:不能被FreeRTOS延迟,绝对禁止 调用任何可能导致上下文切换的FreeRTOS API (如xQueueSend, xSemaphoreGive, vTaskDelay等)。只能调用带FromISR后缀的API (如xQueueSendFromISR, xSemaphoreGiveFromISR)。
      • 低于或等于 此阈值的中断:可以被FreeRTOS延迟,可以调用FreeRTOS API(包括带FromISR和不带FromISR的,但在ISR中应始终使用FromISR版本)。
    • STM32F103设置: 通常设configMAX_SYSCALL_INTERRUPT_PRIORITY = 5 (数值优先级),configKERNEL_INTERRUPT_PRIORITY = 15 (最低)。确保你的关键硬件中断(如电机控制、高速ADC)优先级高于 configMAX_SYSCALL_INTERRUPT_PRIORITY (数值上更小,如0-4),并且这些中断的处理函数只使用FromISR API或不调用任何FreeRTOS API
  4. SysTick配置:

    • 必须保证在CubeMX的Clock Configuration中计算的SysTick重装载值 (Reload Value) 产生的实际中断频率精确等于 FreeRTOSConfig.h中的configTICK_RATE_HZ。不匹配会导致时间相关函数 (vTaskDelay, xQueueReceive with timeout) 时间基准错误。
  5. 启动调度器 (vTaskStartScheduler()):

    • 这个函数调用后,FreeRTOS接管控制权,开始调度任务。它通常不会返回
    • 如果它返回了,通常意味着:
      • 没有创建任何任务 (configMINIMAL_STACK_SIZE太小导致Idle任务创建失败?)。
      • 堆空间不足,无法创建Idle任务或Timer服务任务。
      • 移植层配置错误。
  6. 阻塞 (Blocking):

    • 任务函数中必须 包含能让出CPU的阻塞调用,如vTaskDelay(), xQueueReceive(), xSemaphoreTake()等。否则高优先级任务会独占CPU,导致低优先级任务永远无法运行。
    • Idle任务 (优先级0) 是系统自动创建的最低优先级任务,当所有其他任务阻塞时运行。
  7. HAL库与FreeRTOS:

    • HAL库本身不是 线程安全的。如果多个任务访问同一个外设 (如UART),必须 使用FreeRTOS同步原语 (互斥锁xSemaphoreCreateMutex(), 队列) 进行保护,防止并发访问导致数据损坏。
    • HAL延时 (HAL_Delay()) 基于SysTick,但SysTick已被FreeRTOS接管。在任务中禁止使用HAL_Delay() !必须用vTaskDelay()替代。在中断服务程序 (ISR) 中绝对禁止使用任何阻塞延时。

八. 总结关键步骤:

  1. 创建工程 & 配置时钟: 使用CubeIDE创建项目并正确配置系统时钟、外设时钟。
  2. 启用FreeRTOS: 在Middleware中选择FreeRTOS (CMSIS_V2)。
  3. 配置内核参数: 仔细设置TICK_RATE_HZ, TOTAL_HEAP_SIZE, MINIMAL_STACK_SIZE, MAX_PRIORITIES, 内存方案。
  4. 配置中断优先级: 设置configKERNEL_INTERRUPT_PRIORITYconfigMAX_SYSCALL_INTERRUPT_PRIORITY (STM32F103常用15和5)。
  5. 验证SysTick: 确保SysTick中断频率匹配TICK_RATE_HZ
  6. 编写任务函数: 实现任务逻辑,包含阻塞调用。
  7. 创建任务:main()MX_FREERTOS_Init()中调用xTaskCreate()
  8. 启动调度器: 调用vTaskStartScheduler()
  9. 编译下载调试: 监控栈和堆的使用。
  10. 添加同步机制: 根据需要使用队列、信号量、互斥量保护共享资源。
相关推荐
CC呢1 天前
基于单片机汽车防撞系统设计
stm32·单片机·嵌入式硬件·汽车
努力的小帅1 天前
CAN通信入门
网络·stm32·单片机·嵌入式硬件·stm32c8t6·can总线通信
学习噢学个屁1 天前
基于STM32的交通灯设计—紧急模式、可调时间
c语言·stm32·单片机·嵌入式硬件
潼心1412o1 天前
C语言(长期更新)第15讲 指针详解(五):习题实战
c语言·开发语言
gmmi1 天前
嵌入式学习 51单片机(3)
单片机·学习·51单片机
CC呢1 天前
基于单片机智能热水壶/养生壶设计
单片机·嵌入式硬件·单片机设计
JuneXcy1 天前
结构体简介
c语言·数据结构·算法
jiaway1 天前
【C语言】第四课 指针与内存管理
c语言·开发语言·算法
贾亚超1 天前
【STM32外设】DAC
stm32·单片机·嵌入式硬件
黑客思维者1 天前
《我是如何用C语言写工控系统的漏洞和Bug》连载(1)内容大纲
c语言·bug·工控漏洞