乐于分享
好东西不私藏

嵌入式操作系统:FreeRTOS源码分析—准备启动第一个任务

嵌入式操作系统: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-RTOS2FreeRTOS的封装层(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 → osKernelReadyosKernelInitialize()之后)
    • • 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

voidvTaskStartSchedulervoid ){  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 xPortStartSchedulervoid ){  .../* 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优先级
    • • 把PendSVSysTick设成最低优先级(configKERNEL_INTERRUPT_PRIORITY
    • • 按configCPU_CLOCK_HZ / configTICK_RATE_HZ装载SysTick->LOAD并启动计数
  • • 使能FPU(若为CM4F/CM7configENABLE_FPU为 1)
  • • 调用prvStartFirstTask,开始第一个任务

prvStartFirstTask

__asm voidprvStartFirstTaskvoid ){    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之后PendSVSysTick都无法响应,调度器会死锁。

5. 触发第一个上下文切换

    svc 0           ; 触发 SVC 异常    nop    nop

SVC 0跳转到vPortSVCHandler(),后面两个 nop 只是占位,永远不会执行


我们暂时先分析到这里,接下来就是正式跳转到第一个任务开始运行了,下篇继续。接下来,当第一个任务开始运行后,我们会分析任务是如何创建的以及多个任务之间是如何进行切换的。

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » 嵌入式操作系统:FreeRTOS源码分析—准备启动第一个任务

猜你喜欢

  • 暂无文章