STM32G473VET6 在 Keil MDK 下手动移植 FreeRTOS 指南

下面将详细介绍如何在 Keil MDK 环境下将 FreeRTOS 手动移植到 STM32G473VET6 微控制器上。内容涵盖工程创建、获取源码、文件组织、移植层适配、测试任务编写以及编译调试等步骤。

1. 工程搭建(Keil 项目创建)

  • 创建基础工程 :首先准备一个基础的 STM32G473VET6 工程(例如一个点灯工程)。可以使用 STM32CubeMX 生成 Keil uVision 工程,勾选需要的外设驱动和初始化代码(如 HAL 库)以简化后续开发。确保工程包含 STM32G4的启动文件(如startup_stm32g473xx.s)和系统时钟配置代码(SystemInit() 或 HAL 的 SystemClock_Config())。
  • 配置 Keil 工程选项:在 Keil 中选择正确的器件和编译器。对于 Cortex-M4F 内核,启用硬件 FPU 支持。使用 ARM Compiler 5 可以直接使用 FreeRTOS 提供的 RVDS 移植代码;如果使用 ARM Compiler 6,需要确保使用对应的移植代码或兼容设置。
  • 包含 HAL 库(可选):如果使用 STM32CubeMX 生成的工程并选择了 HAL 驱动,则工程已包含 HAL 库和CMSIS设备支持。HAL 库的使用是可选的,但如果使用HAL,需要注意其与 FreeRTOS 的时间基准(SysTick)的配合。

完成上述基础工程搭建后,确保一个简单的裸机程序(如LED闪烁)可以编译运行,以此作为移植 FreeRTOS 的起点。

2. 获取 FreeRTOS 源码

  • 下载最新 FreeRTOS 内核 :从 FreeRTOS 官方网站获取最新版本的内核源码压缩包。解压后,会看到包含 FreeRTOS/Source 源码目录以及各类 Demo 示例等。我们主要关注 FreeRTOS/Source 下的文件。
  • 整理所需源码文件 :FreeRTOS 提供了很多可选组件,我们在移植时只保留必要的内核源文件。将以下文件从 FreeRTOS/Source 复制到工程目录(建议放入 Middlewares/FreeRTOS/Source):
    • 内核核心tasks.c(任务调度与管理)、queue.c(队列和信号量实现)、list.c(内核链表)、timers.c(软件定时器,可选)、event_groups.c(事件标志组,可选)、croutine.c(协程,可选,根据需要)等。这些文件实现了任务管理、调度器、队列、软件定时器、事件组等核心功能。
    • 头文件 :复制 FreeRTOS/Source/include 目录下的所有头文件到工程的 FreeRTOS/Source/include(或指定一个包含路径)。这些头文件定义了FreeRTOS API和配置项。
  • 移植层与内存管理 :在 FreeRTOS/Source/portable 目录下,根据使用的内核架构和编译器选择正确的移植层代码:
    • 针对 STM32G473 (Cortex-M4F 内核) 和 Keil MDK 编译器,使用路径 portable/RVDS/ARM_CM4F 下的文件。这通常包含 port.cportmacro.h,实现与 Cortex-M4F 内核相关的上下文切换和中断处理代码。将 ARM_CM4F 文件夹复制到工程的 FreeRTOS/Source/portable 下。
    • 内存管理方面,FreeRTOS 提供多种堆管理实现。在 portable/MemMang 目录下选择一款堆实现源码文件,例如常用的 heap_4.c (最佳适应算法,支持分配和释放)或 heap_5.c (支持多内存区)。复制所选的 heap_x.c 文件到 FreeRTOS/Source/portable/MemMang 并加入工程。其余未用的 portable 子目录和堆实现文件可不添加,以减少干扰。
  • 添加到 Keil 工程 :在 Keil 中,为 FreeRTOS 创建分组并添加上述源文件。例如,新建 "FreeRTOS_CORE" 分组添加内核 .c 文件(tasks.c、queue.c 等),新建 "FreeRTOS_PORT" 分组添加移植层和堆文件(如 port.cheap_4.c 等)。同时,在工程的 C/C++ Include 路径中增加 FreeRTOS 的头文件目录(例如 Middlewares\FreeRTOS\Source\include 以及 Middlewares\FreeRTOS\Source\portable\RVDS\ARM_CM4F),以便编译器能够找到 FreeRTOS.hportmacro.h 等头文件。
  • 获取或创建 FreeRTOSConfig.hFreeRTOSConfig.h 是用于配置 FreeRTOS 内核的头文件,不包含在上述源码中。我们需要为 STM32G4 创建该文件并根据需求进行配置。可以参考 FreeRTOS 提供的示例配置(例如在 FreeRTOS/Demo 或 STM32 的示例中查找类似 STM32 的配置),复制并修改后加入工程的 Inc 或 FreeRTOS/include 目录。确保在编译选项的包含路径中能找到该文件。下面将在下一节详细说明关键配置项。

3. 文件结构整理与 FreeRTOSConfig 配置

  • 工程目录组织 :按照惯例,可将 FreeRTOS 源码放置在工程目录下的 Middlewares/FreeRTOS 文件夹中,并划分子目录:
    • Middlewares/FreeRTOS/Source:放置 FreeRTOS内核 .c 源文件和 include 头文件。
    • Middlewares/FreeRTOS/Source/portable:放置移植层相关文件。其中 portable/RVDS/ARM_CM4F 存放 Cortex-M4F + Keil 移植代码,portable/MemMang 存放所选择的堆管理实现。
    • (如果使用 CubeMX 自动生成中间件结构,可直接将文件对号入座到对应文件夹。)
  • FreeRTOSConfig.h 关键配置 :打开新建的 FreeRTOSConfig.h,根据 STM32G473VET6 的硬件参数和应用需求设置各项宏定义。以下是常用配置项:
    • 系统频率configCPU_CLOCK_HZ 定义CPU时钟频率(Hz)。可设置为系统时钟频率数值或使用 SystemCoreClock 变量。例如:

      cpp 复制代码
      #define configCPU_CLOCK_HZ (SystemCoreClock)

确保这个值与系统实际运行频率匹配。

  • Tick 定时频率configTICK_RATE_HZ 定义RTOS滴答时钟频率,即系统节拍中断频率。常用设为1000Hz(1ms周期)。需与 SysTick 配置匹配。
  • 最大优先级数configMAX_PRIORITIES 定义系统可用的任务优先级数量(优先级从0到configMAX_PRIORITIES-1)。根据应用需要设置一个合适值,比如5或以上。注意至少要大于等于使用的优先级数,Idle任务优先级为0。
  • 最小空闲任务栈configMINIMAL_STACK_SIZE 定义空闲任务的栈深度(以字为单位)。Cortex-M4上一般设置为128(即512字节)或根据需求调整。
  • 总堆大小configTOTAL_HEAP_SIZE 定义FreeRTOS可用的堆内存总字节数(仅对heap_1.c, heap_2.c, heap_4.c, heap_5.c等有效)。根据创建的任务、队列等数量估算所需内存并设置。例如设为(10*1024)表示10KB,用于容纳所有动态分配对象。
  • 内核特性开关 :根据需要启用或禁用内核功能宏:
    • configUSE_PREEMPTION 设置为1启用抢占式调度(常用),为0则为协作式调度。
    • configUSE_TIME_SLICING 为1则同优先级任务时间片轮转。
    • configUSE_IDLE_HOOK/configUSE_TICK_HOOK 设置是否使用空闲任务和Tick中断的钩子函数(如不需要可设0)。
    • configUSE_MUTEXESconfigUSE_COUNTING_SEMAPHORES 等设为1启用互斥信号量和计数信号量。
    • configUSE_TRACE_FACILITYconfigUSE_STATS_FORMATTING_FUNCTIONS 可用于启用运行时统计(如 uxTaskGetSystemState)。
    • configGENERATE_RUN_TIME_STATS 如需启用运行时间统计(需要提供时钟源)。
    • 其他比如 configCHECK_FOR_STACK_OVERFLOW (栈溢出检查),configUSE_MALLOC_FAILED_HOOK (内存分配失败钩子)可按需设置。
  • 软件定时器和事件组 :如果使用软件定时器和事件标志组:
    • 设置 configUSE_TIMERS 为1,并配置 configTIMER_TASK_PRIORITY(定时器服务任务优先级,一般高于普通任务),configTIMER_QUEUE_LENGTH(定时器命令队列长度),configTIMER_TASK_STACK_DEPTH(定时器任务栈深度)。
    • 设置 configUSE_EVENT_GROUPS 为1 以启用事件组机制。
  • 中断优先级配置 :针对 Cortex-M 内核,以下配置 极为重要 ,必须正确设置中断优先级相关宏,以确保 FreeRTOS 安全运行:
    • configPRIO_BITS:NVIC 可用优先级位数。STM32G4 系列有 4 位优先级(0-15级),因此 configPRIO_BITS 应定义为4(如果CMSIS的__NVIC_PRIO_BITS已定义则可用它)。
    • configLIBRARY_LOWEST_INTERRUPT_PRIORITY:应用可设置的最低中断优先级数值。STM32优先级数值越大优先级越低,一般设为15(表示最低优先级)。
    • configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY:使用FreeRTOS系统调用的最高中断优先级数值。建议选择一个较高的优先级等级,例如5(表示任何优先级数值<=5的中断服务例程 应调用FreeRTOS API)。
    • configKERNEL_INTERRUPT_PRIORITY:内核所使用的中断优先级(用于PendSVSysTick )。通常定义为 configLIBRARY_LOWEST_INTERRUPT_PRIORITY 左移适当位数,使其成为最低优先级。例如:
cpp 复制代码
#define configKERNEL_INTERRUPT_PRIORITY    ( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )

若优先级位4位,上式计算结果为 15 << 4 = 240 (0xF0),对应 NVIC 优先级15​。这确保RTOS内核的中断(PendSV/SysTick)设为最低优先级。

  • configMAX_SYSCALL_INTERRUPT_PRIORITY:可调用系统API的最高中断优先级(临界值)。定义为 configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 左移 (8 - configPRIO_BITS) 位。例如优先级5则计算为 5 << 4 = 80 (0x50),对应 NVIC 优先级5
  • 设置上述优先级配置后,务必 在应用中确保所有使用 FreeRTOS API的中断的NVIC优先级数值大于等于configMAX_SYSCALL_INTERRUPT_PRIORITY(例如设为5或更大,即优先级不高于5级),否则会触发优先级嵌套错误。
  • 。这意味着优先级数值小于5的中断(更高优先级的中断)不应调用FreeRTOS安全API。

配置完成后,保存 FreeRTOSConfig.h。经过以上设置,FreeRTOS内核行为就根据目标硬件和应用需求进行了调整。

4. 移植层适配(中断向量与启动文件调整)

在将 FreeRTOS 集成到 STM32G4 时,需要确保 Cortex-M 内核的几个特殊中断(SVC、PendSV、SysTick)正确地连接到 FreeRTOS 的调度机制。这涉及移植层代码和启动文件的适配:

  • 重定向内核中断处理函数 :FreeRTOS 的 Cortex-M 移植在 port.c 中实现了 SVC、PendSV 和 SysTick 中断处理逻辑。但我们需要把这些处理例程挂接到实际的中断向量。常用方法有两种:

    1. 宏定义映射法 :在 FreeRTOSConfig.h 中添加宏,将 FreeRTOS 移植层的中断处理函数名映射为标准中断名。例如:

      cpp 复制代码
      #define vPortSVCHandler    SVC_Handler  
      #define xPortPendSVHandler PendSV_Handler  
      #define xPortSysTickHandler SysTick_Handler  

      这样,编译器在编译移植层代码时会将实现函数名替换为对应的中断名,从而覆盖启动文件中的默认中断处理​。STM32CubeMX 生成的FreeRTOS配置常用此方法,其中将 SysTick 的映射宏默认注释是因为 HAL 自带 SysTick handler(如果我们希望RTOS接管SysTick,需要启用该宏)​。通过这种方式,FreeRTOS 的 vPortSVCHandler() 实际编译为 SVC_HandlerxPortPendSVHandler() 编译为 PendSV_HandlerxPortSysTickHandler() 编译为 SysTick_Handler,从而自动替换掉弱定义的默认 handlers。

    2. 启动文件重定向法 :直接修改启动文件 startup_stm32g473xx.s,将向量表中相应中断的入口指向 FreeRTOS 提供的函数。具体做法是在启动文件中声明移植层函数为外部符号,并将中断入口替换为跳转。例如,在启动文件的中断向量表区域,找到 PendSV_HandlerSysTick_HandlerSVC_Handler 的默认实现,替换为:

      cpp 复制代码
      EXTERN vPortSVCHandler  
      EXTERN xPortPendSVHandler  
      EXTERN xPortSysTickHandler  
      
      SVC_Handler    B vPortSVCHandler       ; 跳转到FreeRTOS的SVC处理  
      PendSV_Handler B xPortPendSVHandler    ; 跳转到FreeRTOS的PendSV处理  
      SysTick_Handler B xPortSysTickHandler  ; 跳转到FreeRTOS的SysTick处理  

      如此修改后,这三个中断会触发 FreeRTOS 对应的服务例程,实现上下文切换和心跳节拍。

注意 :采用上述两种方法之一 即可,通常推荐使用第一种映射宏的方法,修改少且清晰。如果使用HAL库且其SysTick作为时基,默认HAL会定义自己的SysTick_Handler,这时更需要使用映射或修改启动文件,确保RTOS的SysTick处理生效而HAL的时基不中断系统滴答(可选择在FreeRTOS的滴答钩子中调用HAL_IncTick()以保持HAL时基)。

  • SysTick 定时器设置 :FreeRTOS利用 SysTick 产生固定频率的节拍中断用于任务调度。需要保证 SysTick 定时中断按照 configTICK_RATE_HZ 配置的频率触发:

    • 如果基础工程使用了 HAL,HAL_Init() 默认将 SysTick 配置为1ms中断(1000Hz)。若 configTICK_RATE_HZ 也是1000,则默认配置可用,但要确保 SysTick 中断由 FreeRTOS接管。如上所述,可以在 FreeRTOS的 SysTick_Handler 中调用 xPortSysTickHandler() 实现RTOS心跳,同时也可调用 HAL_IncTick() 保持HAL的时基计数。

    • 如果未使用 HAL 或需手动配置:可以在系统初始化时配置 SysTick 寄存器。计算加载值:reload = SystemCoreClock / configTICK_RATE_HZ,然后设置 SysTick:

      cpp 复制代码
      SysTick->LOAD  = reload - 1;            // 装载值  
      SysTick->VAL   = 0;                    // 清零计数器  
      SysTick->CTRL  |= SysTick_CTRL_TICKINT_Msk; // 使能SysTick中断  
      SysTick->CTRL  |= SysTick_CTRL_ENABLE_Msk;  // 启动SysTick  

      同时选择合适的时钟源(如使用外部HCLK)。这样SysTick每1/configTICK_RATE_HZ秒产生一次中断​。在 SysTick 中断服务函数中,应调用 FreeRTOS 的心跳处理函数。例如:

      cpp 复制代码
      void SysTick_Handler(void) {  
          if(xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED) {  
              xPortSysTickHandler();  
          }  
      }  

      述实现确保只有在调度器启动后才调用 RTOS 的滴答处理​(在调度器启动前 SysTick 中断也许已经开启,此时不应调用RTOS API)。

  • PendSV 和 SVC 中断 :PendSV 用于触发上下文切换,应设置为最低优先级,以免打断高优先级中断。SVC(Supervisor Call)在 FreeRTOS 中用于启动第一个任务,之后不再使用。通常无需手动配置其优先级(默认即可),PendSV 和 SysTick 优先级会在 FreeRTOS启动时根据 configKERNEL_INTERRUPT_PRIORITY 设置。在移植过程中,只需确保上述映射正确,FreeRTOS 会将 PendSV/SysTick 设置为最低优先级。

  • NVIC 中断优先级分组 :STM32 默认优先级分组通常将全部4位用于抢占优先级(无子优先级)。确保保持这种配置(一般无需额外修改SCB->AIRCR)。这保证了configMAX_SYSCALL_INTERRUPT_PRIORITY规定的优先级阈值正确生效。

完成移植层适配后,FreeRTOS 核心应能正确接管 SVC、PendSV、SysTick 中断,实现其调度功能。可以编译工程,确保链接阶段没有未定义引用(尤其是 vPortSVCHandler 等应已映射或定义)。

5. 编写测试任务

移植完成后,在 main.c 中创建示例任务以验证 RTOS 调度运行是否正常:

  • 初始化 :在创建任务之前,执行必要的硬件初始化。例如调用 HAL_Init()SystemClock_Config()(若使用HAL);初始化用于测试的外设,如配置GPIO用于控制LED,初始化UART用于串口打印等。确认此时不要调用会引起延时阻塞的函数(如 HAL_Delay()),以免在RTOS启动前产生不确定延时。

  • 创建任务:使用 FreeRTOS 提供的 API 创建至少两个任务进行演示:

    • LED 闪烁任务 :例如创建一个周期性闪烁板上 LED 的任务。任务函数中反复切换 LED 引脚状态并调用 vTaskDelay() 延时一定 Tick 数(如500ms),以测试定时调度功能。
    • 串口打印任务 :创建另一个任务,周期性地通过串口打印消息(比如每1秒打印一行文本或计数值)。这可以测试多个任务并行运行,以及任务间的独立性。如使用HAL UART发送,可在任务中调用 HAL_UART_Transmit() 发送字符串(注意需确保串口初始化在RTOS启动前完成,或使用互斥确保线程安全)。

    使用 xTaskCreate() 创建任务时,需要提供任务入口函数、任务名、栈大小、任务参数、优先级和任务句柄等参数。例如:

    cpp 复制代码
    xTaskCreate(LED_Task, "LEDTask", 128, NULL, 2, NULL);
    xTaskCreate(UART_Task, "UARTTask", 256, NULL, 2, NULL);

    上述示例创建了LED_Task和UART_Task,分别指定了栈深度(128和256字,具体值视功能需求而定)和优先级(这里都为2,同优先级将时间片轮转执行)。可以根据需要调整优先级以验证优先级调度效果(优先级数值越大,优先级越高)。

  • 启动调度器 :所有任务创建完成后,调用 vTaskStartScheduler() 启动FreeRTOS调度。​该函数调用后,RTOS 接管CPU控制权,开始根据优先级和时间片调度任务。注意:vTaskStartScheduler() 若返回则意味着调度启动失败(通常是因为堆内存不足无法创建Idle任务),这种情况下可以在main函数中添加错误处理,例如进入死循环或触发断言,以便调试发现问题。正常情况下该调用不会返回。

  • 任务函数示例:下面给出简要的任务函数代码片段示例:

    cpp 复制代码
    void LED_Task(void *argument) {
        for(;;) {
            HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);  // 切换LED引脚状态
            vTaskDelay(pdMS_TO_TICKS(500));             // 延时500ms (pdMS_TO_TICKS宏将毫秒转换为节拍数)
        }
    }
    
    void UART_Task(void *argument) {
        const char *msg = "Hello from FreeRTOS!\r\n";
        for(;;) {
            HAL_UART_Transmit(&huart2, (uint8_t*)msg, strlen(msg), 100);
            vTaskDelay(1000);  // 延时1000ms
        }
    }
  • 在上述代码中,LED任务每0.5秒翻转一次LED状态,UART任务每1秒通过串口发送一条消息。两个任务的死循环中都使用了vTaskDelay或等效宏进行阻塞延时,让出CPU给其他任务。

  • 注意 HAL 与 RTOS 的配合 :如果使用了HAL库,在RTOS启动后不要使用 HAL_Delay()进行延时,因为HAL_Delay依赖于SysTick全局变量的中断更新。在FreeRTOS接管SysTick后,默认HAL的滴答不再增长(除非在SysTick_Handler中继续调用HAL_IncTick)。应改用vTaskDelay等RTOS延时机制来替代阻塞延时。此外,如果串口中断等外设中断需要调用FreeRTOS API(如xQueueSendFromISR),务必确保这些中断优先级符合前述configMAX_SYSCALL_INTERRUPT_PRIORITY限制。

完成上述移植和任务创建后,进行编译、下载并调试,重点关注以下方面:

  • 系统滴答验证 :调试时,可以在 SysTick_Handler 中打断点,或读取 xTaskGetTickCount() 返回值,确认滴答计数在持续增长,频率符合预期的 configTICK_RATE_HZ。例如,可在UART打印任务中定期打印 xTaskGetTickCount()值来观察。​

  • 任务调度检查:观察板上 LED 是否按设计频率闪烁,串口打印是否按周期输出。如果只有一个任务运行、另一个任务饿死,可能是优先级配置不当或某任务陷入死循环未调用阻塞API。确保每个任务在适当位置会阻塞或延时,以让出CPU。还可以使用Keil的调试器查看FreeRTOS线程列表(如果安装了FreeRTOS调试插件)以确认多个任务都处于就绪/阻塞状态并被调度。

  • 中断优先级问题 :如果程序跑一段时间后出现HardFault或异常,常见原因是中断优先级配置不正确导致违反了FreeRTOS的中断安全策略。检查 FreeRTOSConfig.h 中 configMAX_SYSCALL_INTERRUPT_PRIORITY 的设置以及有无中断使用了过高的优先级调用了RTOS API。如有需要,可在 FreeRTOSConfig.h 中定义 configASSERT() 钩子,以捕获运行时的优先级违规等错误。

  • 堆内存使用 :使用 xPortGetFreeHeapSize() 查询剩余堆内存​。该函数返回当前未被分配的堆空间大小,有助于判断 configTOTAL_HEAP_SIZE 设置是否合理。示例 :在所有任务创建后调用 xPortGetFreeHeapSize(),如果返回值很小(接近0),说明堆几乎耗尽,需要增大 configTOTAL_HEAP_SIZE​;如果返回值远大于实际需要,可以优化减小 configTOTAL_HEAP_SIZE 以节省RAM。FreeRTOS还提供 xPortGetMinimumEverFreeHeapSize() 可查询历史最低剩余堆空间,用于评估最糟情况内存占用。

  • 其他调试技巧 :可开启 configCHECK_FOR_STACK_OVERFLOW(并实现vApplicationStackOverflowHook)来捕获任务栈溢出;开启 configUSE_MALLOC_FAILED_HOOK(并实现vApplicationMallocFailedHook)来捕获内存分配失败。这些钩子在调试阶段很有帮助。一旦系统运行正常,可以选择关闭或保留这些检查。

经过以上步骤,如果 LED 按预期闪烁且串口输出正常,就表明 FreeRTOS 在 STM32G473VET6 上已成功移植并运行。由此可以进一步开发应用,比如创建更多任务,利用队列、信号量等进行任务间通信。在实际项目中,按照上述指南配置 FreeRTOS,可确保系统稳定运行于 Keil MDK 环境下。

相关推荐
森焱森2 小时前
无人机三轴稳定控制(2)____根据目标俯仰角,实现俯仰稳定化控制,计算出升降舵输出
c语言·单片机·算法·架构·无人机
白鱼不小白2 小时前
stm32 USART串口协议与外设(程序)——江协教程踩坑经验分享
stm32·单片机·嵌入式硬件
S,D2 小时前
MCU引脚的漏电流、灌电流、拉电流区别是什么
驱动开发·stm32·单片机·嵌入式硬件·mcu·物联网·硬件工程
芯岭技术5 小时前
PY32F002A单片机 低成本控制器解决方案,提供多种封装
单片机·嵌入式硬件
youmdt6 小时前
Arduino IDE ESP8266连接0.96寸SSD1306 IIC单色屏显示北京时间
单片机·嵌入式硬件
嘿·嘘6 小时前
第七章 STM32内部FLASH读写
stm32·单片机·嵌入式硬件
Meraki.Zhang6 小时前
【STM32实践篇】:I2C驱动编写
stm32·单片机·iic·驱动·i2c
几个几个n8 小时前
STM32-第二节-GPIO输入(按键,传感器)
单片机·嵌入式硬件
Despacito0o12 小时前
ESP32-s3摄像头驱动开发实战:从零搭建实时图像显示系统
人工智能·驱动开发·嵌入式硬件·音视频·嵌入式实时数据库
门思科技12 小时前
设计可靠 LoRaWAN 设备时需要考虑的关键能力
运维·服务器·网络·嵌入式硬件·物联网