1. 环境准备与工程搭建

在开始FreeRTOS的实战之前,我们需要先准备好开发环境。STM32F103C8T6是一款基于Cortex-M3内核的微控制器,拥有72MHz的主频和64KB的Flash、20KB的RAM,非常适合运行轻量级的实时操作系统。我推荐使用Keil MDK作为开发工具,因为它对STM32的标准库支持得很好,调试功能也非常强大。

首先,确保你已经有一个可用的STM32F103C8T6标准库工程模板。如果没有,可以从ST官网下载标准外设库,或者使用常见的开源模板(如江科大的STM32工程模板)。工程模板应该包含基本的启动文件、标准库驱动文件和主函数框架。

接下来,我们需要下载FreeRTOS的源码。可以从FreeRTOS官网或者GitHub仓库获取最新版本。我实测过v202212.01版本,在STM32F103C8T6上运行非常稳定。下载完成后,解压源码包,你会看到FreeRTOS的目录结构主要包括Source文件夹(核心源码)和Demo文件夹(示例工程)。

在工程根目录下创建一个名为FreeRTOS的文件夹,并在其中新建三个子文件夹:inc(存放头文件)、src(存放核心源码文件)、port(存放平台相关代码)。这样的结构清晰明了,便于后续维护。记得将工程模板中的标准库文件(如stm32f10x_gpio.c、stm32f10x_rcc.c)也包含进来,因为FreeRTOS需要依赖这些底层驱动。

2. FreeRTOS源码移植详解

移植FreeRTOS的核心是将源码文件复制到工程中,并配置好编译选项。首先,从FreeRTOS源码包的Source/portable/RVDS/ARM_CM3文件夹中复制port.c和portmacro.h到工程的port文件夹。这两个文件是针对Cortex-M3架构的端口层代码,负责处理任务调度和上下文切换。

接下来,从Source/portable/MemMang文件夹中复制内存管理文件。这里我推荐使用heap_4.c,因为它支持内存碎片整理,适合长时间运行的系统。heap_4.c会动态分配任务栈和队列内存,避免静态分配的局限性。

然后,将Source文件夹下的核心文件复制到src文件夹中。包括tasks.c(任务管理)、queue.c(消息队列)、list.c(链表操作)、timers.c(软件定时器)、event_groups.c(事件组)和croutine.c(协程)。这些文件构成了FreeRTOS的内核功能,缺一不可。

头文件部分,将Source/include文件夹中的所有.h文件复制到inc文件夹中。此外,还需要从Demo/CORTEX_STM32F103_Keil文件夹中复制FreeRTOSConfig.h到FreeRTOS根目录。这个配置文件是FreeRTOS的“大脑”,允许你自定义内核参数,如任务优先级数量、堆大小和功能开关。

在Keil工程中,新建三个分组:FreeRTOS_Core(存放src文件夹的文件)、FreeRTOS_Port(存放port和内存管理文件)、FreeRTOS_Config(存放配置文件)。右键点击Target,选择Add Group,然后逐一添加文件。记得在Options for Target中设置头文件路径,将FreeRTOS/inc和FreeRTOS/port文件夹包含进来,否则编译时会报错找不到头文件。

3. 关键中断配置与优化

FreeRTOS依赖三个核心中断来实现任务调度:SVC_Handler、PendSV_Handler和SysTick_Handler。这些中断服务函数(ISR)负责处理系统调用、上下文切换和时间片管理。在STM32标准库中,这些函数默认存在于stm32f10x_it.c文件中,但我们需要让FreeRTOS接管它们。

首先,修改FreeRTOSConfig.h文件,添加以下宏定义:

#define vPortSVCHandler SVC_Handler
#define xPortPendSVHandler PendSV_Handler
#define xPortSysTickHandler SysTick_Handler

这些宏的作用是将FreeRTOS的内部函数名映射到标准库的中断向量表中。这样,当发生中断时,CPU会自动跳转到FreeRTOS的处理函数,而不是标准库的空实现。

接下来,打开stm32f10x_it.c文件,注释掉SVC_Handler、PendSV_Handler和SysTick_Handler的函数体。注意,不是删除整个函数,而是用#if 0和#endif条件编译屏蔽掉,这样可以保留函数框架以备后续调试。例如:

#if 0
void SVC_Handler(void) {
    // 原有代码
}
#endif

这样做的目的是避免函数重复定义。FreeRTOS会在port.c中实现这些函数的逻辑,包括任务栈的保存与恢复、调度器的触发等。

SysTick中断是FreeRTOS的心跳源,默认频率为1000Hz(1ms一次)。你可以在FreeRTOSConfig.h中通过configTICK_RATE_HZ参数调整频率。我建议保持默认值,因为更高的频率会增加系统开销,而更低的频率会影响任务响应速度。对于STM32F103C8T6,SysTick时钟源应设置为HCLK的1/8,即9MHz,这样1ms中断一次刚好合适。

4. 任务创建与调度实战

任务(Task)是FreeRTOS的基本执行单元,相当于裸机编程中的超级循环。每个任务都有自己的栈空间和优先级,由调度器统一管理。创建任务使用xTaskCreate函数,它接受任务函数、任务名、栈大小、优先级等参数。

下面是一个简单的LED闪烁任务示例。首先,在main.c中包含必要的头文件:

#include "FreeRTOS.h"
#include "task.h"
#include "stm32f10x_gpio.h"

然后定义任务函数。任务函数通常是一个无限循环,内部包含阻塞调用(如vTaskDelay)以释放CPU时间:

void vTaskLED(void *pvParameters) {
    GPIO_InitTypeDef GPIO_InitStruct;
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
    GPIO_Init(GPIOC, &GPIO_InitStruct);

    while (1) {
        GPIO_SetBits(GPIOC, GPIO_Pin_13);
        vTaskDelay(500 / portTICK_PERIOD_MS); // 延迟500ms
        GPIO_ResetBits(GPIOC, GPIO_Pin_13);
        vTaskDelay(500 / portTICK_PERIOD_MS);
    }
}

在main函数中,创建任务并启动调度器:

int main(void) {
    xTaskCreate(vTaskLED, "LED_Task", 128, NULL, 1, NULL);
    vTaskStartScheduler();
    while (1) {} // 调度器启动后不会执行到这里
}

这里的关键参数是栈大小(128字)和优先级(1)。栈大小需要根据任务内部变量和函数调用深度来调整,优先级数字越大越高,0为最低。启动调度器后,CPU的控制权就交给了FreeRTOS,它会根据优先级和时间片轮转算法自动切换任务。

5. 系统调试与性能分析

调试多任务系统比裸机复杂,因为多个任务可能同时访问共享资源。Keil的Logic Analyzer工具可以可视化任务状态和系统节拍。首先,在调试模式下打开View->Analysis Windows->Logic Analyzer,点击Setup按钮添加变量。

要观察任务执行情况,可以添加任务句柄到Logic Analyzer。例如,添加vTaskLED任务的栈指针或任务状态变量。你还需要在FreeRTOSConfig.h中启用调试宏:

#define configUSE_TRACE_FACILITY 1
#define configUSE_STATS_FORMATTING_FUNCTION 1

这些宏允许内核生成任务运行统计信息。你可以在任务中调用uxTaskGetSystemState函数获取当前所有任务的状态,包括运行时间、栈水位等。

另一个常见问题是栈溢出。FreeRTOS提供了栈溢出检测机制,在FreeRTOSConfig.h中设置configCHECK_FOR_STACK_OVERFLOW为2(最严格的检测级别)。当检测到溢出时,会触发configASSERT断言,帮助你定位问题。我建议在开发阶段始终开启此功能,因为栈溢出会导致难以调试的内存损坏。

性能优化方面,注意调整configTICK_RATE_HZ。更高的频率意味着更精细的时间片,但也会增加中断开销。对于STM32F103C8T6,1000Hz是一个平衡点。如果系统负载较重,可以降低到500Hz以节省CPU时间。此外,使用事件组(Event Groups)代替信号量(Semaphores)进行任务同步,可以减少上下文切换次数,提高效率。

6. 常见问题与解决方案

在移植和使用FreeRTOS过程中,我踩过不少坑。第一个常见问题是编译错误“undefined symbol PendSV_Handler”。这通常是因为没有正确映射中断函数名。检查FreeRTOSConfig.h中的宏定义是否与启动文件中的向量表名称一致。STM32F103的启动文件通常是startup_stm32f10x_md.s,其中定义了中断向量。

第二个问题是任务无法调度,卡在vTaskStartScheduler。这可能是堆大小不足导致的。在FreeRTOSConfig.h中,configTOTAL_HEAP_SIZE定义了系统堆的大小,默认是17KB左右。对于多个任务或大型队列,可能需要增加到20KB以上。你可以通过调用xPortGetFreeHeapSize函数实时监控剩余堆内存。

第三个坑是中断优先级配置。Cortex-M3内核允许设置中断优先级,FreeRTOS要求SysTick和PendSV的优先级为最低,以确保它们不会阻塞其他中断。在port.c中,函数vPortValidateInterruptPriority会自动检查优先级设置。如果你使用了其他中断(如USART、TIM),确保它们的优先级高于SysTick,否则会影响系统节拍的准确性。

最后,如果遇到系统运行不稳定,偶尔死机,可能是栈分配不足。每个任务的栈大小需要仔细计算,考虑函数调用深度和局部变量大小。我习惯给每个任务多分配20%的栈空间,并在运行时使用uxTaskGetStackHighWaterMark函数检查最大使用量,逐步优化。

Logo

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

更多推荐