Freertos 小小想法
从开机记录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;
}
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐


所有评论(0)