前言
现在开始正式剖析FreeRTOS内核,原计划使用基于Windows仿真环境的基础代码,但进行分析时,发现找不到调用main函数的地方,有点摸不着头脑,没办法,职业病,总想知道个来龙去脉,也不知道仿真环境的运行机制,汗颜,看来得改变策略了,内核剖析使用基于真实平台的代码,如果需要纯软件的实操就用Windows仿真环境(应该是可以跑起来的)。基于本人目前的工作经验,对ARMv8架构再熟悉不过了,选择了类似的基于ARMv7-M架构Cortex-M4 核的如下平台(源自Amazon FreeRTOS控制台) :
这份代码就比较容易理清楚了,查找main函数的调用关系即可追踪到系统启动的第一条指令,权当是开胃小菜吧。
本文将介绍从CPU第一条指令到开始进行内核任务调度。
步入正轨
开始前,下面两个问题有必要先弄清楚,才方便后面的流程梳理:
A. 有些接口存在于多个文件,怎么知道本项目的接口存在于哪些文件里?
这一点很简单,查看相关单板平台的CMakeList.txt文件即可,比如我这套代码用到的部分代码路径如下:
比如"${AFR_KERNEL_DIR}/portable/"下是适配不同CPU的代码,很多是同一接口的在不同CPU上的实现,从上面的路径就可以知道本项目里的接口在"${AFR_KERNEL_DIR}/portable/GCC/ARM_CM4F" 下的文件里。
B. CPU起来后,第一条指令在哪里?
这就涉及到链接器使用的链接脚本,本平台的链接脚本为STM32L475VGTx_FLASH.ld(链接脚本后缀一般为*.ld,或*.lds),摘取部分示意如下:
..../* Specify the memory areas */MEMORY{ RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 96K RAM2 (xrw) : ORIGIN = 0x10000000, LENGTH = 32K FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 464K /* Use only the first bank */ FLASH_UC (r) : ORIGIN = 0x08074000, LENGTH = 9K /* Fixed-location area */}/* Define output sections */SECTIONS{.../* The startup code goes first into FLASH */ .isr_vector : /*存放CPU向量表的节区*/ { . = ALIGN(8); KEEP(*(.isr_vector)) /* Startup code */ . = ALIGN(8);} >FLAS/* The program code and other data goes into FLASH */.text :{. = ALIGN(8);*(.text) /* .text sections (code) */.... = ALIGN(8);_etext = .; /* define a global symbols at end of code */} >FLASH...}
该文件指定编译生成的各个段的在FLASH或RAM里的存放位置(关于链接脚本的语法,网上的资料很多了:)。CPU起来后直接跳转到复位向量处的指令,复位向量位于CPU的向量表里,该向量表一般定义在各个平台的汇编文件里,当前这个平台的代码在文件"startup_stm32l475xx.s":
.section .isr_vector,"a",%progbits.type g_pfnVectors, %object.size g_pfnVectors, .-g_pfnVectorsg_pfnVectors: .word _estack .word Reset_Handler //复位向量 .word NMI_Handler .word HardFault_Handler .word MemManage_Handler .word BusFault_Handler .word UsageFault_Handler ... .word SVC_Handler //SVC异常向量...
这里定义了复位向量表,段名命为".isr_vector",根据前面提供的链接脚本,该段将被放置到FLASH零偏移处。根据下面的Cortex-M4的手册里的向量表,复位向量位于向量表偏移4字节处,即对应这里的Reset_Handler接口。
这样,CPU起来后,直接从0x4偏移处取代码执行!下面开始正餐了,那么从第一条指令到第一个任务有多远?来看看下面这张流程图:
这么看也没有多少路,到少比我想像的少得多。这里创建了三个任务,分别是打印任务"Logging",空闲任务"IDLE"和定时服务任务"Tmr Svc"。
对于最后一步,是怎么启动第一个任务的呢?看下接口vPortStartFirstTask()的实现:
__asm volatile( " ldr r0, =0xE000ED08 " /* Use the NVIC offset register to locate the stack. */ " ldr r0, [r0] " " ldr r0, [r0] " " msr msp, r0 " /* Set the msp back to the start of the stack. */ " mov r0, #0 " /* Clear the bit that indicates the FPU is in use, see comment above. */ " msr control, r0 " " cpsie i " /* Globally enable interrupts. */ " cpsie f " " dsb " " isb " " svc 0 " /* System call to start first task. */ " nop ");
关键在于"svc"指令,要理解这个含义,就又需回到前面讲到的CPU的向量表了,通过分析CORTEX-M4资料,可以知道SVC指令将触发一个异常,该异常向量的地址为SVC_Handler函数的地址,该接口也很短,直接贴这里了:
.section .text.thumb.align 4SVC_Handler: .type func ;Get the location of the current TCB. ldr.w r3, =pxCurrentTCB ldr r1, [r3] ldr r0, [r1] ;Pop the core registers. ldmia r0!, {r4-r11, r14} msr psp, r0 isb mov r0, #0 msr basepri, r0 bx r14 .size SVC_Handler, $-SVC_Handler.endsec
可见,该接口主要是恢复pxCurrentTCB对应任务的上下文,并执行该任务,到此,第一个任务就开始跑了,关于pxCurrentTCB,会在后面的任务创建里讲到,目前只需要知道的就是它代表当前需要执行的任务。