从开机记录freertos 的启动过程——基于stm32f103c8t6

首先,从上电的时刻起,我们的mcu就会进入第一行代码,也就是厂家给的startup代码。主要干在这几行:

​
; Reset handler
Reset_Handler    PROC
                 EXPORT  Reset_Handler             [WEAK]
     IMPORT  __main
     IMPORT  SystemInit
                 LDR     R0, =SystemInit
                 BLX     R0
                 LDR     R0, =__main
                 BX      R0
                 ENDP

​

一般,startup文件会定义重启的文件函数句柄,也就是Reset handler.startup.s的后缀是.s,说明这是汇编文件,所以代码使用汇编代码写的,简单说明一下:

PROC:汇编里声明函数的写法,这里声明了Reset_Handler函数,也就是复位后执行的代码函数

EXPORT:大写,意味着这个变量可以导出并被其他文件识别使用(具体 是c文件里面用export 关键字导出,如果会汇编里定义了可以这样在c代码里面使用);

[WEAK]:弱定义,也就是说如果后面有代码重新定义了它,会采用后面的代码。给予了开发者重写自己逻辑的空间;

IMPORT :汇编里的引入,也就是从别的文件里面引入main,SystemInit

下面是关于寄存器的操作,结合这里的还可以扩展一点:

我么首先要明确,arm芯片架构用的是arm的指令集。对于单片机来说主要有两种指令集,一种是arm的,另一种是risc-v。这里的汇编代码都是采用arm的汇编。两种指令除了底层的ADD,MOV ,MUL类似在汇编里的命令略有不同。

MOV 移动           寄存器,立即数比如#5             也就是把立即数的值放到寄存器

(或者 寄存器,寄存器  就是把后面的寄存器的值赋值给前一个) 

但是处理的立即数比较小,对于Thumb2最多支持16位,对于早期的stm是8位

LDR :和MOV比较类似,但是LDR可以处理更大的立即数。上面的R0,=SystemInit也就是把Systeminit的值放到r0 寄存器

BLX :Branch with link exchange 也就是退出并且用LR 寄存器保存当前指令的下一条的地址,上面的BLX r0也就是说,我要去执行r0里面的命令了,由上面可知是SystemInit,而且我还要把下一条指令的位置记住,方便我执行完SystemInit之后,可以回来继续执行接下来的代码。LR寄存器就是干这个的,所以在平时debug的时候可以看看它的值,帮助判断函数的调用路线。

下面类推,就是在系统初始化好后,把用户的main函数放进r0,然后执行。这里不用  LR是因为BX不保存LR的结果。

ENDP:函数定义结束。

总结一下,这一段代码就是干了这么个事:上电的时候,sp指针会指向向量表的第0个位置读取stack顶部的指针(__initial_sp):

__Vectors       DCD     __initial_sp               ; Top of Stack
                DCD     Reset_Handler              ; Reset Handler
                DCD     NMI_Handler                ; NMI Handler
                DCD     HardFault_Handler          ; Hard Fault Handler
                DCD     MemManage_Handler          ; MPU Fault Handler
                DCD     BusFault_Handler           ; Bus Fault Handler
                DCD     UsageFault_Handler         ; Usage Fault Handler

__initial_sp的定义在前面可以找到:

Stack_Size      EQU     0x00000400

                AREA    STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem       SPACE   Stack_Size
__initial_sp

对于一个单片机,一定要先定义好stack,heap的大小,以及中断向量表,不然就是内存空间都没有规划好,自然没法存放变量,函数,代码。

这里__initial_sp是一个symbol的写法引出来,值的大小是stack顶部的位置,对于stm32f103c8t6一般默认是0x20000400。但是:这玩意是一个代号,意味它只是链接符号,有值但本身不带内存!!!

对比一下,如果是一个有内存的变量比如int x=8,我们知道会有一个指针指向x,假设是p,p=&x。如果我们要用p访问x的值可以用*p解引用。

但是对于__initial_sp,这个东西没有一个指针指向它,此时我们如果要在 c语言里面访问,就要用(uint32_t)&__initial_sp 来得到stack顶部的指针,这对于我们做内存优化来说很重要。

回到启动流程。现在我们的sp指针指向了 stack顶部,pc(程序指针,它的值代表了正在执行的程序的位置,比如看到pc的值是0x00008077,就说明正在执行这个位置的函数,debug好用判断当前代码位置)会指向中断向量表的下一个,__initial_sp的下一个就是Reset_Handle,所以就会执行Reset_Handle函数,到这里,全部是裸机的逻辑,没有FreeRTOS的参与。

Reset_Handle之后就会进入main函数了

进入到主函数,从调用vTaskCreate的时候,FreeRTOS就开始起作用了。FreeRTOS本质是向单片机接管了一部分堆,用来分配自己任务栈,调度算法由heap.c文件决定。我用的是heap4.c,这里不做赘述。

创建任务,也就是在FreeRTOS给你的内存里分配任务stack,同时创建任务的TCB。

你创建的任务的参数会保存在内存的TCB(Task Control Block)里面,接下来是重点,任务stack的结构,后面的内容会基于结构讲述()

----------------------------------------------  高位

 |            xPSR:0x01000001        |           xPSR:标记Thumb模式,任务运行核心状态

----------------------------------------------

|               0xFFFFFFFD               |                     EXC_RETURN:异常返回标识(核心)

----------------------------------------------

|                任务函数地址               |             当pc寄存器存到这个值,就回去找对应的任务执行

----------------------------------------------

|              返回地址 0xFFFFFFFD |        对于freertos的无限for循环,返回值无意义,填充和异常一样

-----------------------------------------------   

|                                                    |

|           通用寄存器      r0-r12         |

|                                                     |

--------------------------------------------        低位

到这里,任务创建的东西差不多就结束了。

接下来就会讲一下从进入main函数,触发任务调度是怎么样的。

在这之前还要介绍一个东西:sp  指针。如果你听过sp, 还听过msp,psp的话,你可以先把它们理解成是同一个指针,只不过用的情况不一样(实际上SP 是统称,MSP/PSP 是两个不同的寄存器,CPU 自动选择用哪个。)。当sp 指向主stack时就是msp,指向任务stack的时候就是psp。对于裸机,我们只会用到msp,而对于freertos来说psp就是任务切换的关键。

好了,接下来我们进入下一步,程序运行到任务调度函数:vTaskStartScheduler()

这个时候,我们的sp 指针会指向第一个任务的stack 顶部(一下用psp指代任务的sp,msp指代主的sp)。pc指向下一个也就是任务函数地址,开始执行任务。此时触发了systick 中断,唤醒PendSV异常。遇到中断,sp由硬件强制切换为msp。去中断向量表里找一找,发现PendSV [Weak],又一看,FreeRTOS 里面有自己的PendSV重写,于是用FreeRTOS的。此时设定好的硬件会帮我们把寄存器的值保存到任务的stack里面,但是只保存前八个sp,pc,lr,r0-r4;这时,任务的stack就变成了这样:

低位(栈从高→低生长,压栈后 PSP 向低地址移动顺序不影响对于整体过程的理解) 

---------------------------------------------------------- 

|               xPSR: 0x01000001                        |     xPSR  记录cpu运行状态             

--------------------------------------------------------------------

|                当前pc的值,表示了当前任务执行到哪里了   |             

--------------------------------------------------------------------------

|                      0xFFFFFFFD                                    |          

|                                                                               | 

|           通用寄存器      r0-r4    (硬件保存的)             |

--------------------------------------------------------------------

|          剩下的                                                           |

------------------------------------------------------------------------------高位

好了,保存现场的一半就是这么来的,是硬件保存,接下来是软件保存。我们刚刚进入主 stack,要处理中断了:

void vPortExitCritical( void )
{
	configASSERT( uxCriticalNesting );
	uxCriticalNesting--;
	if( uxCriticalNesting == 0 )
	{
		portENABLE_INTERRUPTS();
	}
}
/*-----------------------------------------------------------*/

__asm void xPortPendSVHandler( void )
{
	extern uxCriticalNesting;
	extern pxCurrentTCB;
	extern vTaskSwitchContext;

	PRESERVE8

	mrs r0, psp
	isb

	ldr	r3, =pxCurrentTCB		/* Get the location of the current TCB. */
	ldr	r2, [r3]

	stmdb r0!, {r4-r11}			/* Save the remaining registers. */
	str r0, [r2]				/* Save the new top of stack into the first member of the TCB. */

	stmdb sp!, {r3, r14}
	mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
	msr basepri, r0
	dsb
	isb
	bl vTaskSwitchContext
	mov r0, #0
	msr basepri, r0
	ldmia sp!, {r3, r14}

	ldr r1, [r3]
	ldr r0, [r1]				/* The first item in pxCurrentTCB is the task top of stack. */
	ldmia r0!, {r4-r11}			/* Pop the registers and the critical nesting count. */
	msr psp, r0
	isb
	bx r14
	nop
}

主要是这里:

ldr	r3, =pxCurrentTCB		/* Get the location of the current TCB. */
	ldr	r2, [r3]

	stmdb r0!, {r4-r11}			/* Save the remaining registers. */
	str r0, [r2]				/* Save the new top of stack into the first member of the TCB. */

看到我们把{r4-r11}的寄存器给保存到了任务的stack里面。到这里我们就保护了完整的现场,然后把新的任务比如任务2的stack顶部的地址给了r0,也就是sp寄存器。这些任务的TCB是在创建的时候就保存到内存里的,供FreeRTOS重写的PendSV异常使用。

好了,sp,pc的去想我们都已经比较清楚了,还有LR寄存器也不能忘记,它是决定程序接下去是走msp还是psp的决定者。LR 中的 EXC_RETURN 值是异常返回标识,决定返回后使用 MSP/PSP。当我们从psp退出触发中断的时候,硬件自动帮我们设置了LR的值0xFFFFFFFD,当我们处理完PendSV异常要出来的时候,一看LR的值就知道要回到psp去执行,从而切换到第二个任务。后面就是任务的不断切换了

LR寄存器的三重情况(EXC_RETURN 数值)

  • 0xFFFFFFF9 → 返回 MSP
  • 0xFFFFFFFD → 返回 PSP
  • 0xFFFFFFF1 → 返回 MSP(handler 模式)

附关于stack创建的源码佐证:

StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters )
{
    // 1. 填充xPSR(必须是0x01000001)
    pxTopOfStack--;
    *pxTopOfStack = portINITIAL_XPSR; // 宏定义:0x01000001
    
    // 2. 填充PC(任务函数地址)
    pxTopOfStack--;
    *pxTopOfStack = ( StackType_t ) pxCode;
    
    // 3. 填充LR(EXC_RETURN=0xFFFFFFFD)
    pxTopOfStack--;
    *pxTopOfStack = ( StackType_t ) portTASK_RETURN_ADDRESS; // 宏定义:0xFFFFFFFD
    
    // 4. 填充R12(清零)
    pxTopOfStack--;
    *pxTopOfStack = 0UL;
    
    // 5. 填充R3~R0(R0=任务参数,其余清零)
    pxTopOfStack--;
    *pxTopOfStack = ( StackType_t ) pvParameters; // R0
    pxTopOfStack--;
    *pxTopOfStack = 0UL; // R1
    pxTopOfStack--;
    *pxTopOfStack = 0UL; // R2
    pxTopOfStack--;
    *pxTopOfStack = 0UL; // R3
    
    // 6. 填充R11~R4(全部清零)
    for( uint8_t x = 0; x < 8; x++ )
    {
        pxTopOfStack--;
        *pxTopOfStack = 0UL;
    }
    return pxTopOfStack;
}

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐