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控制)
移植核心步骤
- 将FreeRTOS源码中的
tasks.c、queue.c、list.c、timers.c及portable/GCC/ARM_CM3/port.c加入工程。 - 提供
FreeRTOSConfig.h配置文件(见下文代码块)。 - 在启动文件(startup_stm32f103.s)中确保
PendSV_Handler、SysTick_Handler分别映射到xPortPendSVHandler、xPortSysTickHandler(若使用官方启动文件,需修改中断向量表名称)。 - 主函数中先初始化系统时钟(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间隔闪烁。
验证方法与判定标准
- 两个LED同时开始闪烁,且频率不同(PC13约2.5Hz,PA0约1.67Hz),说明两个任务均被调度执行。
- 若仅一个LED闪烁或系统死机,检查:
- SysTick中断优先级是否低于configMAX_SYSCALL_INTERRUPT_PRIORITY(在NVIC_PriorityGroupConfig中设置);
- 启动文件中三个中断向量名是否与FreeRTOSConfig.h映射一致;
- 堆大小configTOTAL_HEAP_SIZE是否足够两个任务栈(每个任务128字,共约1KB,8KB堆足够)。 - 用调试器暂停内核,查看
pxCurrentTCB指向的任务是否在vLED1_Task与vLED2_Task间交替,可确认任务调度正常。
常见问题
- 编译报错“Undefined symbol xPortPendSVHandler”:启动文件中的中断服务函数名必须与FreeRTOSConfig.h中宏定义一致,或直接修改启动文件为FreeRTOS提供的名称。
- 任务不切换:确认
configUSE_PREEMPTION为1,且SysTick中断已触发(检查NVIC配置)。 - 进入HardFault:多为栈溢出或中断优先级配置错误,检查
configASSERT是否触发,以及最小栈大小是否满足任务局部变量需求。