2. RTOS内核移植到目标硬件

任务目标

将FreeRTOS内核移植到指定MCU(以STM32F103系列为例),完成系统时钟节拍(SysTick)配置与任务调度初始化,最终运行两个独立LED闪烁任务,验证内核移植正确性。

环境依赖

  • 硬件:STM32F103C8T6最小系统板(蓝丸),板载LED:PC13(低电平点亮)
  • 工具链:arm-none-eabi-gcc + CMake,或Keil MDK(二选一)
  • FreeRTOS源码:V10.4.6+(需包含 portable/GCC/ARM_CM3 目录)
  • STM32标准外设库或HAL库(仅用于时钟初始化与GPIO控制)

移植核心步骤

  1. 将FreeRTOS源码中的 tasks.c、queue.c、list.c、timers.c 及 portable/GCC/ARM_CM3/port.c 加入工程。
  2. 提供 FreeRTOSConfig.h 配置文件(见下文代码块)。
  3. 在启动文件(startup_stm32f103.s)中确保 PendSV_Handler、SysTick_Handler 分别映射到 xPortPendSVHandler、xPortSysTickHandler(若使用官方启动文件,需修改中断向量表名称)。
  4. 主函数中先初始化系统时钟(72MHz),再调用 vTaskStartScheduler() 启动调度器。

关键完整代码

// FreeRTOSConfig.h —— 最小配置
#ifndef FREERTOS_CONFIG_H
#define FREERTOS_CONFIG_H

#define configUSE_PREEMPTION            1
#define configUSE_IDLE_HOOK             0
#define configUSE_TICK_HOOK             0
#define configCPU_CLOCK_HZ              ( 72000000UL )
#define configTICK_RATE_HZ              ( 1000 )
#define configMAX_PRIORITIES            ( 5 )
#define configMINIMAL_STACK_SIZE        ( 128 )
#define configTOTAL_HEAP_SIZE           ( 8 * 1024 )
#define configMAX_TASK_NAME_LEN         ( 16 )
#define configUSE_16_BIT_TICKS          0
#define configIDLE_SHOULD_YIELD         1
#define configUSE_MUTEXES               0
#define configUSE_RECURSIVE_MUTEXES     0
#define configUSE_COUNTING_SEMAPHORES   0
#define configUSE_TIMERS                0
#define configUSE_QUEUE_SETS            0
#define configUSE_TASK_NOTIFICATIONS    1

#define configASSERT( x )               if( ( x ) == 0 ) { taskDISABLE_INTERRUPTS(); for( ;; ); }

/* 中断优先级配置(Cortex-M3 必须) */
#define configPRIO_BITS                 4
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY     0x0f
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5
#define configKERNEL_INTERRUPT_PRIORITY     ( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
#define configMAX_SYSCALL_INTERRUPT_PRIORITY ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )

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

#endif /* FREERTOS_CONFIG_H */
// main.c —— 完整移植验证代码
#include "FreeRTOS.h"
#include "task.h"
#include "stm32f10x.h"

/* 两个LED任务:PC13为板载LED,另用PA0外接LED(均低电平点亮) */
static void vLED1_Task(void *pvParameters) {
    (void)pvParameters;
    GPIO_ResetBits(GPIOC, GPIO_Pin_13);
    for (;;) {
        GPIO_SetBits(GPIOC, GPIO_Pin_13);   // 灭
        vTaskDelay(pdMS_TO_TICKS(200));
        GPIO_ResetBits(GPIOC, GPIO_Pin_13); // 亮
        vTaskDelay(pdMS_TO_TICKS(200));
    }
}

static void vLED2_Task(void *pvParameters) {
    (void)pvParameters;
    GPIO_ResetBits(GPIOA, GPIO_Pin_0);
    for (;;) {
        GPIO_SetBits(GPIOA, GPIO_Pin_0);    // 灭
        vTaskDelay(pdMS_TO_TICKS(300));
        GPIO_ResetBits(GPIOA, GPIO_Pin_0);  // 亮
        vTaskDelay(pdMS_TO_TICKS(300));
    }
}

static void vInitGPIO(void) {
    GPIO_InitTypeDef gpio;
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOA, ENABLE);

    gpio.GPIO_Mode = GPIO_Mode_Out_PP;
    gpio.GPIO_Speed = GPIO_Speed_2MHz;

    gpio.GPIO_Pin = GPIO_Pin_13;
    GPIO_Init(GPIOC, &gpio);

    gpio.GPIO_Pin = GPIO_Pin_0;
    GPIO_Init(GPIOA, &gpio);
}

int main(void) {
    /* 系统时钟初始化为72MHz(使用标准库SystemInit,已在启动文件中调用) */
    vInitGPIO();

    xTaskCreate(vLED1_Task, "LED1", configMINIMAL_STACK_SIZE, NULL, 1, NULL);
    xTaskCreate(vLED2_Task, "LED2", configMINIMAL_STACK_SIZE, NULL, 1, NULL);

    vTaskStartScheduler();  /* 启动调度器,不会返回 */

    for (;;);  /* 异常时到达此处 */
}

编译与运行

  • 若使用CMake + arm-none-eabi-gcc,需在链接脚本中确保堆栈足够(建议堆≥8KB),并链接FreeRTOS全部源文件。
  • 若使用Keil,将上述文件加入工程,并在Options→Target中勾选“Use MicroLIB”,将STM32启动文件中的中断向量名改为 PendSV_Handler、SysTick_Handler、SVC_Handler(与FreeRTOSConfig.h中的宏映射一致)。
  • 编译烧录后,板载LED(PC13)以200ms间隔闪烁,外接LED(PA0)以300ms间隔闪烁。

验证方法与判定标准

  1. 两个LED同时开始闪烁,且频率不同(PC13约2.5Hz,PA0约1.67Hz),说明两个任务均被调度执行。
  2. 若仅一个LED闪烁或系统死机,检查:
    - SysTick中断优先级是否低于 configMAX_SYSCALL_INTERRUPT_PRIORITY(在 NVIC_PriorityGroupConfig 中设置);
    - 启动文件中三个中断向量名是否与FreeRTOSConfig.h映射一致;
    - 堆大小 configTOTAL_HEAP_SIZE 是否足够两个任务栈(每个任务128字,共约1KB,8KB堆足够)。
  3. 用调试器暂停内核,查看 pxCurrentTCB 指向的任务是否在 vLED1_Task 与 vLED2_Task 间交替,可确认任务调度正常。

常见问题

  • 编译报错“Undefined symbol xPortPendSVHandler”:启动文件中的中断服务函数名必须与FreeRTOSConfig.h中宏定义一致,或直接修改启动文件为FreeRTOS提供的名称。
  • 任务不切换:确认 configUSE_PREEMPTION 为1,且SysTick中断已触发(检查NVIC配置)。
  • 进入HardFault:多为栈溢出或中断优先级配置错误,检查 configASSERT 是否触发,以及最小栈大小是否满足任务局部变量需求。