嵌入式操作系统:FreeRTOS源码分析—准备启动第一个任务
FreeRTOS源码分析——从main函数开始观察OS是怎么开始运行的
FreeRTOS是一个轻量级的实时操作系统内核,适用于微控制器和嵌入式系统。因其开源、轻量级、可移植的特点使其成为一款受欢迎、广泛应用于嵌入式系统的RTOS。为进一步学习RTOS底层原理,本系列将基于STM32 HAL库从源码角度分析FreeRTOS的实现。
1. FreeRTOS源文件结构
FreeRTOS-Kernel├─ CMSIS_RTOS_V2/│ ├─ cmsis_os.h│ ├─ cmsis_os2.c│ ├─ cmsis_os2.h│ ├─ freertos_mpool.h│ └─ freertos_os2.h|├─ include/│ ├─ atomic.h│ ├─ croutine.h → 协程(已弃用,仍兼容)│ ├─ deprecated_definitions.h │ ├─ event_groups.h → 事件标志组│ ├─ FreeRTOS.h → 必须先包含的“总入口”│ ├─ FreeRTOSConfig_template.h │ ├─ list.h │ ├─ message_buffer.h → 消息缓冲区(V10.0+)│ ├─ mpu_prototypes.h │ ├─ mpu_wrappers.h │ ├─ portable.h│ ├─ projdefs.h│ ├─ queue.h → 队列 / 信号量 / 互斥量 API│ ├─ semphr.h → 信号量/互斥量宏包装│ ├─ stack_macros.h │ ├─ stackMacros.h → 栈溢出检测辅助宏│ ├─ stream_buffer.h → 流缓冲区(V10.0+)│ ├─ task.h → 任务管理 API│ └─ timers.h → 软件定时器│├─ portable/ ← “移植层”——与编译器/硬件相关的代码│ ├─ MemMang/ ← 堆管理移植点(仅 heap_1~5 的“入口”)│ │ ├─ heap_1.c│ │ ├─ heap_2.c│ │ ├─ heap_3.c│ │ ├─ heap_4.c│ │ └─ heap_5.c│ └─ RVDS/│ └─ ARM_CM4F/│ ├─ port.c│ └─ portmacro.h │├─ croutine.c ← 协程(遗留)├─ event_groups.c ← 事件标志组├─ list.c ← 双向链表(就绪表、延时表、挂起表的基础)├─ queue.c ← 队列 / 信号量 / 互斥量 统一实现├─ stream_buffer.c ← 流 / 消息缓冲区共用实现 ├─ tasks.c ← 核心调度器实现(任务创建、切换、就绪表、延时表)├─ timers.c ← 软件定时器 Daemon 任务|└─ LICENSE.md
CMSIS_RTOS_V2
CMSIS(Cortex Microcontroller Software Interface Standard – Real-Time Operating System),是ARM 给 Cortex-M 定的统一 RTOS 接口标准。关于FreeRTOS的常用API都在cmsis_os2.c中又封装了一层,用户可以不用关心底层使用的具体是哪个OS。
portable
移植适配,与硬件/编译器相关的文件
-
• MemMang不同的堆内存管理实现 -
• RVDS/ARM_Cm4F/port.c开关中断、启动调度、 PendSV 现场切换、SysTick 心跳、临界区嵌套、浮栈管理、杂项屏障。
内核
任务、定时器、队列、事件组等等
2. 从main函数开始向下分析
intmain(void){ .../* Init scheduler */ osKernelInitialize(); /* Call init function for freertos objects (in cmsis_os2.c) */ MX_FREERTOS_Init();/* Start scheduler */ osKernelStart();/* We should never get here as control is now taken by the scheduler */while (1) { }}
osKernelInitialize
osKernelInitialize的作用是设置内核状态为Ready
osStatus_t osKernelInitialize(void) { osStatus_t stat;if (IS_IRQ()) { stat = osErrorISR; }else {if (KernelState == osKernelInactive) { ... KernelState = osKernelReady; stat = osOK; } else { stat = osError; } }return (stat);}
MX_FREERTOS_Init
在MX_FREERTOS_Init中创建了一个默认的任务,后续会有专门的文章分析任务的创建,所以这里暂时先跳过。
voidMX_FREERTOS_Init(void) {/* Create the thread(s) *//* creation of defaultTask */ defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes);}
osKernelStart
osKernelStart是本文主要关注的函数,内核会从该函数开始调度
osStatus_t osKernelStart(void) { osStatus_t stat;if (IS_IRQ()) { stat = osErrorISR; }else {if (KernelState == osKernelReady) {/* Ensure SVC priority is at the reset value */ SVC_Setup();/* Change state to enable IRQ masking check */ KernelState = osKernelRunning;/* Start the kernel scheduler */ vTaskStartScheduler(); stat = osOK; } else { stat = osError; } }return (stat);}
osKernelStart()是CMSIS-RTOS2对FreeRTOS的封装层(cmsis_os2.c)里 “启动调度器” 的唯一入口。把CPU控制权交给FreeRTOS,从此任务开始竞争运行,main()线程变 idle上下文。
1. 检查当前是否为中断状态
if (IS_IRQ()) { stat = osErrorISR;
-
• CMSIS规定:不能在IRQ上下文启动调度器。 -
• IS_IRQ()通常读__get_IPSR(),非0表示正在异常/中断。
2. 状态机检查
if (KernelState == osKernelReady)
-
• 内核状态机: -
• osKernelInactive→osKernelReady(osKernelInitialize()之后) -
• osKernelReady→osKernelRunning(本函数设置) -
• 只允许就绪态进入运行态,重复调用返回 osError。
3. 保证 SVC 优先级为最高
__STATIC_INLINE voidSVC_Setup(void) {#if (__ARM_ARCH_7A__ == 0U)/* Service Call interrupt might be configured before kernel start *//* and when its priority is lower or equal to BASEPRI, svc intruction *//* causes a Hard Fault. */ NVIC_SetPriority (SVCall_IRQ_NBR, 0U);#endif}
-
• 把 NVIC_SVC_PRIORITY设为最高优先级(数值为0)。 -
• 防止用户误改导致 SVC 异常被抢占,上下文切换崩溃。 -
• 对 Cortex-M3/4/7/33 等带 SVC 的核有效;M0/M0+/M23 无 SVC 时为空函数。
4. 切换全局状态
KernelState = osKernelRunning;
此后IS_IRQ_MASKING()等宏会禁止在临界段外创建对象,保证API时序安全。
5. 启动 FreeRTOS 调度器
调用vTaskStartScheduler启动调度器
3. 调度器是如何启动的
vTaskStartScheduler
voidvTaskStartScheduler( void ){ StaticTask_t *pxIdleTaskTCBBuffer = NULL; StackType_t *pxIdleTaskStackBuffer = NULL;uint32_t ulIdleTaskStackSize;/* The Idle task is created using user provided RAM - obtain the address of the RAM then create the idle task. */ vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize ); xIdleTaskHandle = xTaskCreateStatic( prvIdleTask, configIDLE_TASK_NAME, ulIdleTaskStackSize, ( void * ) NULL, /*lint !e961. The cast is not redundant for all compilers. */ portPRIVILEGE_BIT, /* In effect ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), but tskIDLE_PRIORITY is zero. */ pxIdleTaskStackBuffer, pxIdleTaskTCBBuffer ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */ ... xTimerCreateTimerTask(); .../* Interrupts are turned off here, to ensure a tick does not occur before or during the call to xPortStartScheduler(). The stacks of the created tasks contain a status word with interrupts switched on so interrupts will automatically get re-enabled when the first task starts to run. */ portDISABLE_INTERRUPTS(); /* Setting up the timer tick is hardware specific and thus in the portable interface. */ xPortStartScheduler();}
vTaskStartScheduler主要完成以下工作:
-
• 创建 idle任务(静态或动态),#define configMINIMAL_STACK_SIZE ((uint16_t)128) -
• 创建 Timer守护任务(若configUSE_TIMERS开启),后续Timer章节分析。 -
• 关中断 -
• 调用 xPortStartScheduler()开启调度器
xPortStartScheduler
BaseType_t xPortStartScheduler( void ){ .../* Make PendSV and SysTick the lowest priority interrupts. */ portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI; portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;/* Start the timer that generates the tick ISR. Interrupts are disabled here already. */ vPortSetupTimerInterrupt(); .../* Ensure the VFP is enabled - it should be anyway. */ prvEnableVFP();/* Start the first task. */ prvStartFirstTask();}
xPortStartScheduler主要完成以下工作:
-
• 设置 PendSV/Systick优先级 -
• 把 PendSV和SysTick设成最低优先级(configKERNEL_INTERRUPT_PRIORITY) -
• 按 configCPU_CLOCK_HZ / configTICK_RATE_HZ装载SysTick->LOAD并启动计数 -
• 使能 FPU(若为CM4F/CM7且configENABLE_FPU为 1) -
• 调用 prvStartFirstTask,开始第一个任务
prvStartFirstTask
__asm voidprvStartFirstTask( void ){ PRESERVE8/* Use the NVIC offset register to locate the stack. */ ldr r0, =0xE000ED08 ldr r0, [r0] ldr r0, [r0]/* Set the msp back to the start of the stack. */ msr msp, r0/* Clear the bit that indicates the FPU is in use in case the FPU was used before the scheduler was started - which would otherwise result in the unnecessary leaving of space in the SVC stack for lazy saving of FPU registers. */ mov r0, #0 msr control, r0/* Globally enable interrupts. */ cpsie i cpsie f dsb isb/* Call SVC to start the first task. */ svc 0 nop nop}
prvStartFirstTask函数使用汇编语言编写,将会触发第一次上下文切换SVC 0——硬件跳转到第一个任务的现场:
1. 找到主堆栈MSP的“出生地址”
ldr r0, =0xE000ED08 ; r0 = &SCB->VTORldr r0, [r0] ; r0 = VTOR 值(向量表基址)ldr r0, [r0] ; r0 = 向量表第 0 项 = 主栈顶(_estack)
在Cortex-M中规定:向量表偏移寄存器VTOR里放的是“向量表起始地址”,表的第0个字就是“复位后MSP初值”。这一步把“Boot 阶段写进 Flash 的栈顶”重新捞回来,保证后面 msp指向合法且8字节对齐的RAM顶端。
2. 把 MSP 拉回“出厂设置”
msr msp, r0 ; 正式写回 MSP
在此之前FreeRTOS可能用MSP做过临时栈、甚至运行过C代码;现在彻底丢弃旧栈,防止“上层”残留数据污染第一任务。此后MSP只给异常帧用,PSP才跑任务代码。
3. 清 FPU 活跃标志
mov r0, #0msr control, r0 ; CONTROL.FPCA = 0
CONTROL[2](FPCA)=1时,硬件会在异常入口自动压浮点寄存器S0-S15&FPSCR,多占68字节。如果bootloader或早期C代码用过FPU,这里不清零,那么SVC一进来就会先推68字节垃圾,浪费RAM且破坏对齐。清零后,第一个任务若用FPU才在 vPortEnableVFP()里重新置位,实现“懒保存”。
4. 全局开中断
cpsie i ; 使能 IRQ (= __enable_irq())cpsie f ; 使能 FIQ (在 M 系列里 FIQ 就是总中断开关)dsbisb ; 流水线与总线屏障,保证前面的“使能”立即生效
vTaskStartScheduler()里调用portDISABLE_INTERRUPTS()关了全局中断;这里才是正式“放行”。不打开的话,SVC之后PendSV和SysTick都无法响应,调度器会死锁。
5. 触发第一个上下文切换
svc 0 ; 触发 SVC 异常 nop nop
SVC 0跳转到vPortSVCHandler(),后面两个 nop 只是占位,永远不会执行
我们暂时先分析到这里,接下来就是正式跳转到第一个任务开始运行了,下篇继续。接下来,当第一个任务开始运行后,我们会分析任务是如何创建的以及多个任务之间是如何进行切换的。
夜雨聆风