FreeRTOS启动任务调度器
这部分内容就要去深入了解源码以及熟悉汇编语言的操作。依旧正点原子的视频。下面首先看开启任务调度器这部分源码:
1开启任务调度器
任务调度器用于启动任务调度器,任务调度器启动后, FreeRTOS 便会开始进行任务调度。
下面进入工程查看RTOS的源码:
1 创建空闲任务:
#else /* if ( configSUPPORT_STATIC_ALLOCATION == 1 ) */
{
/* The Idle task is being created using dynamically allocated RAM. */
xReturn = xTaskCreate( prvIdleTask,
configIDLE_TASK_NAME,
configMINIMAL_STACK_SIZE,
( void * ) NULL,
portPRIVILEGE_BIT, /* In effect ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), but tskIDLE_PRIORITY is zero. */
&xIdleTaskHandle ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */
}
我们进入代码可以查看到空闲任务的优先级为0,所以空闲任务的优先级最低。在操作系统(尤其是实时操作系统,RTOS)中,空闲任务(Idle Task)扮演着一个特别的角色。它是系统中优先级最低的任务,当没有其他用户任务(或者说,高优先级任务)需要运行时,空闲任务就会执行。它的主要作用包括但不限于以下几点:
-
资源回收:空闲任务可以用来执行内存回收或者资源清理工作。在一些系统中,比如FreeRTOS,内存的释放操作会被推迟到空闲任务中执行,这样做可以避免在高优先级任务中执行可能耗时的内存释放操作。
-
系统监控:通过监测空闲任务的运行时间,可以评估系统的负载情况。例如,可以通过计算空闲任务占用的CPU时间比例来估算系统的空闲时间比例,这对于分析和优化系统性能非常有用。
-
省电和功耗管理:在空闲任务中,可以执行一些省电操作,比如将CPU置于低功耗模式。对于电池供电的设备,这是非常重要的功能,有助于延长设备的续航时间。
-
保活(Keep-alive)操作或心跳信号:在一些系统中,空闲任务也可以用来维持与外部系统的通信,比如定期发送心跳信号,确保系统仍然在线。
-
执行低优先级的后台任务:虽然空闲任务的主要目的不是执行实际的应用逻辑,但它可以被用来执行一些不紧急的后台任务,比如日志记录、状态更新等。
-
调试和测试:在开发和调试阶段,空闲任务可以用来执行一些诊断操作,例如内存检查、系统健康状况报告等。
2 如果使能软件定时器,则创建定时器任务
使能软件定时器的宏为1,则创建定时器任务,定时器任务在操作系统中,尤其是实时操作系统(RTOS)中,定时器任务(Timer Task)发挥着重要的作用。定时器任务允许系统或应用程序在指定的时间点或经过指定的时间间隔执行特定的操作。这些操作可以是一次性的,也可以是周期性的。定时器任务的主要作用包括:
-
任务调度:定时器可以用来触发或调度任务的执行。例如,一个任务可能需要每隔一定时间周期运行,定时器就可以用来实现这种周期性调度。
-
时间管理:在需要精确控制操作执行时间的场景中,定时器提供了一种有效的时间管理机制。例如,在通信协议中,定时器可以用来管理超时重传、心跳包发送等。
-
资源释放:定时器可以用来监控系统资源的使用情况,并在资源使用超时时自动释放资源。这对于防止资源泄漏非常有用。
-
性能监控:通过定时器,系统可以定期收集和记录性能数据,如CPU使用率、内存使用情况等,有助于系统性能的监控和优化。
-
用户界面更新:在图形用户界面(GUI)应用中,定时器常用于定期更新用户界面,如动画效果的实现、状态信息的刷新等。
-
省电和功耗管理:在嵌入式系统和移动设备中,定时器可以用来实现省电策略,比如在设备空闲时自动进入低功耗模式,或者定期唤醒设备执行必要的更新操作。
-
事件触发:定时器可以用来在特定时间点触发事件,这对于实现基于时间的事件处理逻辑非常有用。
3 关闭中断,防止调度器开启之前或过程中,受中断干扰,会在运行第一个任务时打开中断
if( xReturn == pdPASS )
{
/*如果定义了FREERTOS_TASKS_C_ADDITIONS_INIT,则调用freertos_tasks_c_additions_init()函数。
这个函数的目的是允许用户在任务创建时执行额外的初始化操作,
但它只会被调用如果用户通过定义FREERTOS_TASKS_C_ADDITIONS_INIT宏来启用它。*/
#ifdef FREERTOS_TASKS_C_ADDITIONS_INIT
{
freertos_tasks_c_additions_init();
}
#endif
//调用用于禁用中断。这是为了防止在调度器启动之前或在调度器启动过程中发生中断。这保证了系统启动的一致性和可预测性。
portDISABLE_INTERRUPTS();
#if ( ( configUSE_NEWLIB_REENTRANT == 1 ) || ( configUSE_C_RUNTIME_TLS_SUPPORT == 1 ) )
{
/* Switch C-Runtime's TLS Block to point to the TLS
* block specific to the task that will run first. */
configSET_TLS_BLOCK( pxCurrentTCB->xTLSBlock );
}
#endif
xNextTaskUnblockTime = portMAX_DELAY;
xSchedulerRunning = pdTRUE;//4:初始化全局变量,并将任务调度器的运行标志设置为已运行
xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;//初始化系统的滴答计数xTickCount为configINITIAL_TICK_COUNT。
portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();//5:初始化任务运行时间统计功能的时基定时器
traceTASK_SWITCHED_IN();
/* Setting up the timer tick is hardware specific and thus in the
* portable interface. */
xPortStartScheduler();/*xPortStartScheduler()函数负责启动
FreeRTOS调度器。这通常涉及到设置系统滴答定时器和使能中断。在大多数情况下,一旦调度器启动,这个函数不会返回。*/
}
xNextTaskUnblockTime = portMAX_DELAY;
在FreeRTOS中,xNextTaskUnblockTime
变量用于记录下一个需要从阻塞状态唤醒的任务的唤醒时间。这个时间是基于系统的滴答计数(tick count)来计算的。当任务因为某些原因(如等待时间延迟、等待信号量或消息队列)进入阻塞状态时,它们会被指定一个唤醒时间。调度器会检查xNextTaskUnblockTime
来确定是否有任务需要被唤醒。
初始化xNextTaskUnblockTime
为portMAX_DELAY
的目的是为了表示在当前时刻,没有任何任务是因为超时而需要被唤醒的。portMAX_DELAY
通常是一个非常大的值,表示一个“无限”延迟,这在系统刚启动时是一个合理的假设,因为此时还没有任何任务被放入阻塞状态,或者更具体地说,没有任务因为等待超时而需要在将来某个特定时刻被唤醒。
在调度器运行过程中,如果有任务因为超时(如vTaskDelayUntil()、xQueueReceive()等API调用)而进入阻塞状态,xNextTaskUnblockTime
会被更新为最近的未来某个时刻,这个时刻是所有阻塞任务中最早需要被唤醒的时刻。调度器会在每个滴答中断中检查当前时间是否已经达到xNextTaskUnblockTime
,如果是,相应的任务会被唤醒。
下面具体进入这个函数内部看这个函数xPortStartScheduler()
内部实现:
BaseType_t xPortStartScheduler( void )
{
#if ( configASSERT_DEFINED == 1 )
{
volatile uint32_t ulOriginalPriority;
volatile uint8_t * const pucFirstUserPriorityRegister = ( uint8_t * ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER );
volatile uint8_t ucMaxPriorityValue;
ulOriginalPriority = *pucFirstUserPriorityRegister;
/* Determine the number of priority bits available. First write to all
* possible bits. */
*pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;
/* Read the value back to see how many bits stuck. */
ucMaxPriorityValue = *pucFirstUserPriorityRegister;
/* The kernel interrupt priority should be set to the lowest
* priority. */
configASSERT( ucMaxPriorityValue == ( configKERNEL_INTERRUPT_PRIORITY & ucMaxPriorityValue ) );
/* Use the same mask on the maximum system call priority. */
ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;
/* Calculate the maximum acceptable priority group value for the number
* of bits read back. */
ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;
while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE )
{
ulMaxPRIGROUPValue--;
ucMaxPriorityValue <<= ( uint8_t ) 0x01;
}
#ifdef __NVIC_PRIO_BITS
{
/* Check the CMSIS configuration that defines the number of
* priority bits matches the number of priority bits actually queried
* from the hardware. */
configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == __NVIC_PRIO_BITS );
}
#endif
#ifdef configPRIO_BITS
{
/* Check the FreeRTOS configuration that defines the number of
* priority bits matches the number of priority bits actually queried
* from the hardware. */
configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == configPRIO_BITS );
}
#endif
/* Shift the priority group value back to its position within the AIRCR
* register. */
ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;
/* Restore the clobbered interrupt priority register to its original
* value. */
*pucFirstUserPriorityRegister = ulOriginalPriority;
}
#endif /* configASSERT_DEFINED */
/* Make PendSV and SysTick the lowest priority interrupts. */
portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;
portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;
/* Start the timer that generates the tick ISR. Interrupts are disabled
* here already. */
vPortSetupTimerInterrupt();/*调用vPortSetupTimerInterrupt
函数来配置和启动系统滴答定时器中断。这个定时器中断是FreeRTOS时间管理和任务调度的基础。*/
/* Initialise the critical nesting count ready for the first task. */
uxCriticalNesting = 0;//将临界区嵌套计数(uxCriticalNesting)初始化为0。这个计数用于管理临界区的嵌套调用,确保系统的稳定性
/* Start the first task. */
prvStartFirstTask();//调用prvStartFirstTask函数来启动第一个任务。这个函数实际上会设置堆栈,然后跳转到第一个任务的入口点,从而开始任务的执行。
/* Should not get here! */
return 0;
}
前面很大一部分都是断言检查,这部分代码可以忽略,我们直接看设置portNVIC_PENDSV_PRI、portNVIC_SYSTICK_PRI的设置,这部分其实我们之前学习中断管理的时候已经讲过如何设置标志位。详细设置过程可以看这个博客:FreeRTOS中断管理。这里其实就将它们俩个优先级设置为最低。以防止这俩个中断打断其他的中断。
启动第一个任务:
假设我们要启动的第一个任务是任务A,那么就需要将任务A的寄存器值恢复到CPU寄存器
任务A的寄存器值,在一开始创建任务时就保存在任务堆栈里边!这里也需要注意:
1、中断产生时,硬件自动将xPSR,PC(R15),LR(R14),R12,R3-R0出/入栈;而R4~R11需要手动出/入栈
2、进入中断后硬件会强制使用MSP指针 ,此时LR(R14)的值将会被自动被更新为特殊的EXC_RETURN
__asm void prvStartFirstTask( void )
{
/* *INDENT-OFF* */
PRESERVE8/* 8字节对齐:如果数组是按照8字节对齐的,每个64位的双精度浮点数都会精确地放在一个8字节对齐的内存块中。这样,处理器只需要执行一次内存访问操作就可以加载或存储整个64位数据。 */
/* 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
/* Globally enable interrupts. */
cpsie i
cpsie f
dsb
isb
/* Call SVC to start the first task. */
svc 0
nop
nop
/* *INDENT-ON* */
}
接下来来看下启动第一个任务的汇编语言。
ldr r0, =0xE000ED08
ldr r0, [ r0 ]
ldr r0, [ r0 ]
打开Cortex‐M3 权威指南,在里面搜索0xE000ED08可以搜到这个是向量表偏移量寄存器的地址。
将 0XE000ED08 保存在寄存器 R0 中。一般来说向量表应该是从起始地址(0X00000000)开始存储的,不过,有些应用可能需要在运行时修改或重定义向量表,Cortex-M 处理器为此提供了一个叫做向量表重定位的特性。向量表重定位特性提供了一个名为向量表偏移寄存器(VTOR)的可编程寄存器。VTOR 寄存器的地址就是 0XE000ED08,通过这个寄存器可以重新定义向量表,比如在 STM32F103 的 ST 官方库中会通过函数 SystemInit()来设置 VTOR 寄存器。
关于向量表,要具体去看手册,其实向量表中存储的就是各种对应的异常:
而向量表的出现就是为了当发生了异常并且要响应它时,CM3 需要定位其处理例程的入口地址。这些入口地址存储在所谓的“(异常)向量表”中。缺省情况下,CM3 认为该表位于零地址处,且各向量占用 4 字节,因此每个表项占用 4 字节,如表 7.6 所示。
因为地址 0 处应该存储引导代码,所以它通常是 Flash 或者是 ROM 器件,并且它们的值不得在运行时改变。然而,为了动态重分发中断,CM3 允许向量表重定位——从其它地址处开始定位各异常向量。这些地址对应的区域可以是代码区,但也可以是 RAM 区。在 RAM
区就可以修改向量的入口地址了。为了实现这个功能,NVIC 中有一个寄存器,称为“向量表偏移量寄存器”(在地址 0xE000_ED08 处),通过修改它的值就能定位向量表。但必须注意的是:向量表的起始地址是有要求的:必须先求出系统中共有多少个向量,再把这个数字
向上增大到是 2 的整次幂,而起始地址必须对齐到后者的边界上。例如,如果一共有 32 个中断,则共有 32+16(系统异常)=48 个向量,向上增大到 2 的整次幂后值为 64,因此地址地址必须能被 644=256 整除,对于包含64个向量的中断向量表,由于每个向量是4字节(32位),整个表的大小是644=256字节。要使整个表对齐,它的起始地址必须是256字节对齐的,即256的整数倍。这样,处理器在访问任何一个中断向量时,都能保证是在对齐的内存边界上进行,从而优化访问速度并确保系统的稳定运行。从而合法的起始地址可以是:0x0, 0x100, 0x200 等。具体原因为:
0x0:这是十六进制表示的0,显然是256的任何倍数(包括0倍)。
0x100:在十六进制中,0x100等同于十进制的256。因为十六进制的“100”意味着 1 × 1 6 2 + 0 × 1 6 1 + 0 × 1 6 0 = 256 1 \times 16^2 + 0 \times 16^1 + 0 \times 16^0 = 256 1×162+0×161+0×160=256。所以,0x100正好是256的1倍。
0x200:在十六进制中,0x200等同于十进制的512。计算方式是
2
×
1
6
2
=
512
2 \times 16^2 = 512
2×162=512,这是256的2倍。
程序在运行过程中需要一定的栈空间来保存局部变量等一些信息。当有信息保存到栈中时,MCU 会自动更新 SP 指针,ARM Cortex-M 内核提供了两个栈空间:
在FreeRTOS中,中断使用MSP(主堆栈),中断以外使用PSP(进程堆栈)。启动第一个任务函数前面关键的事情就是用于初始化启动第一个任务前的环境,主要是重新设置MSP 指针,并使能全局中断。
在操作系统(尤其是实时操作系统,RTOS)中,启动第一个任务函数之前进行的环境初始化是一个关键步骤,确保系统从一个已知且稳定的状态开始运行。这个过程中,重新设置主堆栈指针(MSP)和使能全局中断是两个非常重要的操作。下面详细解释这两个步骤的必要性:
重新设置MSP指针:
-
堆栈指针的重要性:堆栈指针是CPU用来追踪函数调用栈的当前位置的寄存器。在ARM架构中,有两种堆栈指针:主堆栈指针(MSP)和进程堆栈指针(PSP)。MSP通常用于中断服务例程(ISR)和系统初始化,而PSP用于用户任务或线程。
-
为什么需要重新设置MSP:在系统启动时,MSP通常被初始化为堆栈的顶部。但在系统运行期间,尤其是在启动RTOS之前,MSP可能已经被用于执行初始化代码、设置硬件等操作,从而改变了其原始值。为了确保第一个任务能够在一个干净且预定义的堆栈空间上运行,需要将MSP重置到一个确定的起始位置。这有助于避免潜在的堆栈溢出或数据损坏。
-
这段代码是针对ARM架构处理器编写的汇编语言,用于在实时操作系统(RTOS)中启动第一个任务。它的主要作用是定位并设置主堆栈指针(MSP)到其初始位置,然后全局使能中断,并通过一个软件中断(SVC指令)来启动第一个任务。这是RTOS初始化过程的一部分,确保系统从一个已知且安全的状态开始运行任务。下面是对这段代码中特定几行的详细解释:
ldr r0, =0xE000ED08
ldr r0, [ r0 ]
ldr r0, [ r0 ]
-
ldr r0, =0xE000ED08
:这行代码将地址0xE000ED08
加载到寄存器r0
中。0xE000ED08
是ARM Cortex-M处理器的NVIC(Nested Vectored Interrupt Controller)的VTOR(Vector Table Offset Register)的地址。VTOR寄存器用于指定中断向量表的起始地址,这是一个包含了所有中断处理函数入口点地址的数组。 -
ldr r0, [ r0 ]
:这行代码从r0
寄存器所指向的地址(即0xE000ED08
,VTOR的地址)加载数据到r0
寄存器中。此时,r0
将包含中断向量表的起始地址。在系统启动时,向量表通常位于内存的起始位置,其中第一个项目(索引0)是初始主堆栈指针(MSP)的值。 -
ldr r0, [ r0 ]
:这行代码再次从r0
寄存器所指向的地址(此时是中断向量表的起始地址)加载数据到r0
寄存器中。因为向量表的第一个项是初始MSP的值,所以此操作将MSP的初始值加载到r0
中。
MSR MSP, R0
这条指令的具体作用是将R0寄存器中的值(通常是一个地址)设置为新的主堆栈指针的值。
这些指令是ARM架构中与中断控制相关的汇编指令,常见于ARM Cortex-M系列处理器。它们用于控制中断的启用和确保指令执行的顺序。下面是对这些指令的解释:
cpsie i
:
cpsie
是Change Processor State, Enable interrupts
的缩写。i
代表使能(enable)IRQ中断,即允许中断请求(IRQ)。- 这条指令的作用是允许处理器响应IRQ中断请求。
cpsie f
:
- 同样,
cpsie
是Change Processor State, Enable interrupts
的缩写。 f
代表使能(enable)FIQ中断,即允许快速中断请求(FIQ)。- 这条指令的作用是允许处理器响应FIQ中断请求。FIQ是一种比IRQ优先级更高的中断,用于处理一些需要快速响应的场景。
dsb
:
dsb
是Data Synchronization Barrier
的缩写。- 这条指令用于确保在它之前的所有内存访问(读/写操作)指令完成后,才能执行之后的指令。
- 它是一种内存屏障指令,用于实现指令执行的同步,确保数据的一致性和内存操作的顺序性。
isb
:
isb
是Instruction Synchronization Barrier
的缩写。- 这条指令确保在它之前的所有指令完成执行后,才开始执行之后的指令。
isb
通常用于确保在进行了重要的系统配置更改(如更改中断状态)后,这些更改能够在执行后续指令前生效。
svc 0
调用 SVC 指令触发 SVC 中断,SVC 也叫做请求管理调用,SVC 和 PendSV 异常对于OS 的设计来说非常重要。SVC 异常由 SVC 指令触发。
当使能了全局中断,并且手动触发 SVC 中断后,就会进入到 SVC 的中断服务函数中
在函数 prvStartFirstTask()
中通过调用 SVC 指令触发了 SVC 中断,而第一个任务的启动就是在 SVC 中断服务函数中完成的,SVC 中断服务函数应该为 SVC_Handler()
。并且SVC中断会在启动第一次任务时候会调用一次,以后均不调用。
__asm void vPortSVCHandler( void )
{
/* *INDENT-OFF* */
PRESERVE8
ldr r3, = pxCurrentTCB /* Restore the context. */
ldr r1, [ r3 ] /* Use pxCurrentTCBConst to get the pxCurrentTCB address. */
ldr r0, [ r1 ] /* The first item in pxCurrentTCB is the task top of stack. */
ldmia r0 !, { r4 - r11 } /* Pop the registers that are not automatically saved on exception entry and the critical nesting count. */
msr psp, r0 /* Restore the task stack pointer. */
isb//isb为指令同步屏障
mov r0, # 0 //设置寄存器 R0 为 0
msr basepri, r0//设置寄存器 BASEPRI 为 R0,也就是 0,打开中断!
orr r14, # 0xd
bx r14
/* *INDENT-ON* */
}
里面具体也是汇编语言。
ldr r3, = pxCurrentTCB
/* 它的目的是获取当前正在运行的任务的TCB地址。
这是因为在任务切换或异常处理等情况下,操作系统
需要访问当前任务的上下文信息,而这些信息都存储在TCB中。
通过pxCurrentTCB,操作系统能够访问和管理这些关键信息。 */
ldr r1, [ r3 ] /* 因为r3中存储正在运行TCB的地址,地址中存储着TCB控制块,所以r1就为TCB控制块 */
ldr r0, [ r1 ] /* 这里就是之前为啥TCB控制块首个元素为栈顶指针了,因为这里要利用栈顶指针进行操作 */
接下来来看下一句代码:
ldmia r0 !, { r4 - r11 }
/* Pop the registers that are not automatically saved on exception entry and the critical nesting count. */
这里汇编代码这里写的原因是:
1、中断产生时,硬件自动将xPSR,PC(R15),LR(R14),R12,R3-R0出/入栈;而R4~R11需要手动出/入栈
然后这里结合前面章节学过的图会更加容易理解:
ldmia r0!, {r4-r11}
是ARM汇编语言中的一条指令,用于从内存中加载数据到寄存器组中。这条指令使用了LDMIA
(Load Multiple Increment After)模式,!操作符告诉指令执行后更新基地址(r0)到最后一个被访问的内存地址之后的地址。也就是指向R0寄存器。具体含义如下:
-
ldmia
:这是Load Multiple Increment After的缩写,表示这是一个多重加载指令,用于从内存中加载多个寄存器的数据。加载的地址从指定的基址(base address)开始,每加载一个寄存器,地址就增加一个寄存器的大小(在32位ARM架构中通常是4个字节)。 -
r0!
:这指定了基址寄存器(base register),在这个例子中是r0
。感叹号(!
)表示在加载操作完成后,将最终的地址写回到r0
中。这意味着r0
的值会被更新为加载操作之后的新地址,即原始r0
的值加上所加载数据的总大小。 -
{r4-r11}
:这指定了要从内存中加载数据的目标寄存器列表,从r4
到r11
。这意味着内存中从r0
指向的地址开始的连续区域的数据将被加载到寄存器r4
至r11
中。
msr psp, r0
在这段代码中,msr psp, r0
这条指令的作用是恢复任务的栈指针到程序栈指针(PSP)。这是在一个典型的RTOS环境中,特别是在使用Cortex-M架构的微控制器上实现任务切换的一部分。详细解释如下:
-
上下文保存与恢复:当RTOS决定切换当前运行的任务时,它需要保存当前任务的上下文(即寄存器的值和其他重要状态),然后恢复即将运行的任务的上下文。这样,每个任务都可以在被挂起时“冻结”其状态,并在重新调度时从上次停止的地方继续执行。
-
栈指针的角色:每个任务在RTOS中都有自己的栈空间,用于存储局部变量、函数参数、返回地址等。任务的当前状态(上下文)也会保存在其栈上。程序栈指针(PSP)指向任务的栈顶,是当前任务栈空间的指示器。
-
msr psp, r0
的具体作用:ldr r3, =pxCurrentTCB
:这条指令加载pxCurrentTCB
的地址到r3
寄存器。pxCurrentTCB
是一个指向当前任务控制块(TCB)的全局变量指针。ldr r1, [r3]
:通过r3
寄存器的值(即pxCurrentTCB
的地址),加载TCB的实际值到r1
寄存器。这里的TCB实际值是指向任务栈顶的指针。ldr r0, [r1]
:通过r1
寄存器的值(即指向栈顶的指针),加载栈顶的地址到r0
寄存器。ldmia r0!, {r4-r11}
:从r0
指向的地址(栈顶)开始,连续加载r4
到r11
寄存器的值,并更新r0
的值为新的栈顶地址。msr psp, r0
:最后,这条指令将r0
寄存器的值(更新后的栈顶地址,即任务的当前栈指针)设置为程序栈指针(PSP)。这样,当从异常处理程序返回到任务执行时,处理器将使用更新后的PSP作为栈指针,确保任务能够在正确的栈空间上继续执行。
orr r14, # 0xd
bx r14
这里涉及到了R14寄存器。由于STM32F103是M3,所以不存在浮点单元,所以只能是未使用浮点单元时候的值,由于我们上面是中断
返回后进入线程模式,并使用PSP指针。所以要将其置为0xFFFFFFFD。表示退出异常以后 CPU 进入线程模式并且使用进程栈!
bx r14执行此行代码以后硬件自动恢复寄存器 R0~R3、R12、LR、PC 和 xPSR 的值,堆栈使用进程栈 PSP,然后执行寄存器 PC 中保存的任务函数。至此,FreeRTOS 的任务调度器正式开始运行!
仿真
接下来让我们进行仿真来验证这节课所学的内容:
我们在该句地方打上断点,debug到这个地方,此时pxCurrentTCB这个地址存储的值为0x20000ED0,由于运行第一个任务选取的为优先级最高的任务。下面我们可以进程序看下空闲任务和定时器任务的优先级。
空闲任务的优先级为0,说明其优先级最低。
软件定时器的优先级为31最大,说明定时器任务的优先级最高。这时候pxCurrentTCB指针存储的应该是定时器任务控制块的地址。下面我们会到仿真去看一下。
在上图这个地方打上断点,复位,并且进行单步运行。会发现此时的值变为一样。说明我们的理解是正确的。pxCurrentTCB存储的应该是定时器任务控制块的地址
我们继续往底下单步运行。
获取 pxCurrentTCB 指针的存储地址,pxCurrentTCB 是一个指向 TCB_t 的指针,这个指针永远指向正在运行的任务。这里先获取这个指针存储的地址,比如我现在的代码测试出来这个指针是存放在0x2000002C,
继续往下单步运行,取 R3 所保存的地址处的值赋给 R1。通过这一步就获取到了当前任务的任务控制块的
存储地址。比如当前我的程序中这个地址就为 0X20000ED0,
继续单步运行。
取 R1所保存的地址处的值赋给 R0,我们知道任务控制块的第一个字段就是任务堆栈的栈顶指针 pxTopOfStack 所指向的位置,所以读取任务控制块所在的首地址(0X20000EE8)得到的就是栈顶指针所指向的地址,当前我的程序中这个栈顶指针(pxTopOfStack)所指向的地址为
0X20000E78,
然后接下来由于要把内存中加载数据到寄存器组。这里移动个8个寄存器。所以之后R0的值会变为:8*4=32转换为16进制。为20.
所以最后的结果为:0x20000E98.
程序仿真出来也是这个样子。说明我们理解是正确的。