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 环境下。

相关推荐
EVERSPIN37 分钟前
高压风机专用32位单片机MM32SPIN080G
单片机·嵌入式硬件
0xCC说逆向2 小时前
Windows逆向工程入门之数据结构使用
数据结构·windows·单片机
Hetertopia3 小时前
STM32寄存器控制引脚高低电平
stm32·单片机·嵌入式硬件
因心,三人水3 小时前
【STM32F103ZET6——库函数】2.按键控制蜂鸣器
stm32·单片机·嵌入式硬件
小麦嵌入式4 小时前
Linux驱动开发实战(一):LED控制驱动详解
linux·c语言·驱动开发·stm32·单片机·嵌入式硬件·ubuntu
道一236 小时前
STM32 微控制器库RCC_ClkInitTypeDef结构参数介绍
stm32·单片机·嵌入式硬件
道一236 小时前
STM32 微控制器库RCC_OscInitTypeDef结构参数介绍
stm32·单片机·嵌入式硬件
厂太_STAB_丝针6 小时前
【自学嵌入式(11)闪存文件系统的应用】
c语言·单片机·嵌入式硬件
电子小子洋酱6 小时前
ESP32移植Openharmony外设篇(9)NB-IOT
单片机·物联网·华为·harmonyos·鸿蒙