目录
- 任务的定义
- 栈和任务栈
- 任务控制块
- 任务初始化函数
- 初始化任务栈
- 任务创建函数pxTopOfStack
- 任务列表初始化
- 将任务插入到就绪列表中
- 调度器
- xPortStartScheduler() 函数
- prvStartFirstTask()函数 (该函数是偏硬件底层的函数,用汇编语言编写,在port.c中实现)
- vPortSVCHandler() 函数
- 任务切换
- 任务切换函数 taskYIELD()
- vTaskSwitchContext() 函数 (真正意义上实现任务切换的函数)
- 实验
- 仿真
任务的定义
在多任务系统中,根据功能的不同,把整个系统分割成一个个独立的且无法返回的函数,这个函数我们称为任务。
栈和任务栈
在裸机系统中,全局变量、局部变量、函数返回地址放在一个叫栈的地方,栈是单片机RAM里面一段连续的内存空间,栈的大小一般在启动文件或者链接脚本里面指定,最后由C库函数_main进行初始化。
而在多任务系统中:每个任务都是独立的,互不干扰的,所以要为每个任务都分配独立的栈空间,这个栈空间通常是一个预先定义好的全局数组,也可以是动态分配的一段内存空间,但它们都存在于RAM中。
在多任务系统中,有多少个任务就需要定义多少个任务栈。
任务栈定义如下:
#define TASK1_STACK_SIZE 128;
StackType_t Task1Stack[TASK1_STACK_SIZE];
#define TASK1_STACK_SIZE 128;
StackType_t Task2Stack[TASK2_STACK_SIZE];
任务栈本质上是预先定义好的全局数据。
注:在FreeRTOS中,凡是涉及数据类型的地方,FreeRTOS都会将标准的C数据类型用typedef重新定义一个类型名。具体可参考portmacro.h文件。
任务控制块
系统为了顺利地调度任务,为每个任务都额外定义了一个任务控制块,其中包含如:任务的栈指针、任务名、任务的形参等。
任务控制块的数据结构如下:
typedef struct tskTaskControlBlock
{
volatile StackType_t *pxTopOfStack; /* 栈顶 */
ListItem_t xStateListItem; /* 任务节点 */
StackType_t *pxStack; /* 任务栈起始地址 */
char pcTaskName[ configMAX_TASK_NAME_LEN ]; /* 任务名称,字符串形式 */
} tskTCB; typedef tskTCB TCB_t;
其中,xStateListItem是内置在TCB控制块中的链表节点(列表项),通过这个节点可以将任务控制块挂接到各种链表(列表)中。
任务控制块的定义:
TCB_t Task1tTCB;
TCB_t Task2tTCB;
任务初始化函数
static void prvInitialiseNewTask( TaskFunction_t pxTaskCode, /* 任务入口 */
const char * const pcName, /* 任务名称,字符串形式 */
const uint32_t ulStackDepth, /* 任务栈大小,单位为字 */
void * const pvParameters, /* 任务形参 */
TaskHandle_t * const pxCreatedTask, /* 任务句柄 */
TCB_t *pxNewTCB ) /* 任务控制块指针 */
{
StackType_t *pxTopOfStack;
UBaseType_t x; //定义任务名
/* 获取栈顶地址 */
pxTopOfStack = pxNewTCB->pxStack + ( ulStackDepth - ( uint32_t ) 1 );
//pxTopOfStack = ( StackType_t * ) ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );
/* 向下做8字节对齐 */
pxTopOfStack = ( StackType_t * ) ( ( ( uint32_t ) pxTopOfStack ) & ( ~( ( uint32_t ) 0x0007 ) ) );
/* 将任务的名字存储在TCB中 */
for( x = ( UBaseType_t ) 0; x < ( UBaseType_t ) configMAX_TASK_NAME_LEN; x++ )
{
pxNewTCB->pcTaskName[ x ] = pcName[ x ];
if( pcName[ x ] == 0x00 )
{
break;
}
}
/* 任务名字的长度不能超过configMAX_TASK_NAME_LEN */
pxNewTCB->pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] = '\0';
/* 初始化TCB中的xStateListItem节点 */
vListInitialiseItem( &( pxNewTCB->xStateListItem ) );
/* 设置xStateListItem节点的拥有者 */
listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB );
/* 初始化任务栈 */
pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters );
/* 让任务句柄指向任务控制块 */
if( ( void * ) pxCreatedTask != NULL )
{
*pxCreatedTask = ( TaskHandle_t ) pxNewTCB;
}
}
其中,任务句柄是用于指向任务控制块的指针,通过访问任务句柄就可以访问这个任务的相关内容了。
将栈顶指针向下做8字节对齐的原因:在Cortex-M3(Cortex-M4或Cortex-M7)内核的单片机中,因为总线的宽度是32位的,通常只需要栈保持4字节对齐即可。但对于64位的操作系统时可能会遇到浮点运算,所以需要8字节对齐。如果栈顶指针是8字节对齐的,在进行向下8字节对齐时,指针不会移动,如果不是8字节对齐的,在做向下8字节对齐时就会空出几个字节。这里,若StackType_t 是32位,pxTopOfStack = ( StackType_t * ) ( ( ( uint32_t ) pxTopOfStack ) & ( ~( ( uint32_t ) 0x0007 ) ) );
则改行代码执行的是4字节对齐;若是64位,即可能会遇到浮点数的情况,该行代码则执行的是8字节对齐。
比如当pxTopOfStack是33时,明显不能被8整除,进行向下8字节对齐就是32,那么就会空出一个字节不使用。
补充:为什么要做字节对齐,字节对齐的原理是什么?
- 做字节对齐的原因:字节对齐的目的是为了提高内存访问的效率,因为CPU一次读取内存的数据是有限制的,比如32位的CPU一次只能读取4个字节。如果一个变量跨越了两个内存块,那么就需要多次读取,这样就降低了效率。
- 字节对齐的原理:字节对齐的原理是根据变量的类型和大小来确定其对齐值,然后按照一定的规则来分配内存空间。一般来说,有以下几条规则:
基本类型的变量,其对齐值等于其类型大小,比如int类型的变量,其对齐值为4。
数组类型的变量,其对齐值等于数组元素的对齐值,比如int a[10],其对齐值为4。
结构体类型的变量,其对齐值等于结构体中最大成员的对齐值,比如struct {int a; char b;} s,其对齐值为4。
联合体类型的变量,其对齐值等于联合体中最大成员的对齐值,比如union {int a; char b;} u,其对齐值为4。
对于结构体或联合体中的每个成员,其起始地址必须是其对齐值的整数倍,如果不满足,则在前一个成员后面填充字节。
对于整个结构体或联合体,其总大小必须是其对齐值的整数倍,如果不满足,则在最后一个成员后面填充字节。
pxTopOfStack = ( StackType_t * ) ( ( ( uint32_t ) pxTopOfStack ) & ( ~( ( uint32_t ) 0x0007 ) ) );
例:当pxTopOfStack是33,StackType_t为32为时,则该行执行4字节对齐。
首先把pxTopOfStack转换为无符号32位整数,然后将无符号32位的16位进制数0x0007取反,取反为0xFFF8,都是32位的数,再进行与计算,最终得到的结果用10进制表示为20,可以被4整除,因此执行了4字节对齐。
初始化任务栈
这是一段跟硬件接口相关的代码,不同的板子顺序已经名称不一样。一般在FreeRTOS中port.c文件中
StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters )
{
/* 异常发生时,自动加载到CPU寄存器的内容 */
pxTopOfStack--;
*pxTopOfStack = portINITIAL_XPSR; /* xPSR的bit24必须置1 */
pxTopOfStack--;
*pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; /* PC,即任务入口函数 */
pxTopOfStack--;
*pxTopOfStack = ( StackType_t ) prvTaskExitError; /* LR,函数返回地址 */
pxTopOfStack -= 5; /* R12, R3, R2 and R1 默认初始化为0 */
*pxTopOfStack = ( StackType_t ) pvParameters; /* R0,任务形参 */
/* 异常发生时,手动加载到CPU寄存器的内容 */
pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4默认初始化为0 */
/* 返回栈顶指针,此时pxTopOfStack指向空闲栈 */
return pxTopOfStack;
}
如图所示:
任务创建函数pxTopOfStack
任务的栈、任务的函数实体以及任务的控制块最终需要联系起来才能由系统进行统一调度。 因此需要任务创建函数来完成。在FreeRTOS中,任务的创建分为动态创建和静态创建。动态创建时,任务控制块和栈的内存是创建任务时动态分配的,任务删除时,内存可以释放。任务静态创建时,任务控制块和栈的内存需要事先定义好,是静态从内存,任务删除时,内存不能释放。
静态创建任务函数如下:
如果configSUPPORT_STATIC_ALLOCATION 宏为1,则静态创建。
#if( configSUPPORT_STATIC_ALLOCATION == 1 )
TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode, /* 任务入口 */
const char * const pcName, /* 任务名称,字符串形式 */
const uint32_t ulStackDepth, /* 任务栈大小,单位为字 */
void * const pvParameters, /* 任务形参 */
StackType_t * const puxStackBuffer, /* 任务栈起始地址 */
TCB_t * const pxTaskBuffer ) /* 任务控制块指针 */
{
TCB_t *pxNewTCB;
TaskHandle_t xReturn;
//判断任务控制块指针和任务栈起始地址是否为空。如果为空,说明没有为任务分配静态内存,那么就不能创建任务。
if( ( pxTaskBuffer != NULL ) && ( puxStackBuffer != NULL ) )
{
pxNewTCB = ( TCB_t * ) pxTaskBuffer;
pxNewTCB->pxStack = ( StackType_t * ) puxStackBuffer;
/* 创建新的任务 */
prvInitialiseNewTask( pxTaskCode, /* 任务入口 */
pcName, /* 任务名称,字符串形式 */
ulStackDepth, /* 任务栈大小,单位为字 */
pvParameters, /* 任务形参 */
&xReturn, /* 任务句柄 */
pxNewTCB); /* 任务栈起始地址 */
}
else
{
xReturn = NULL;
}
/* 返回任务句柄,如果任务创建成功,此时xReturn应该指向任务控制块 */
return xReturn;
}
#endif /* configSUPPORT_STATIC_ALLOCATION */
任务入口 TaskFunction_t 即任务的函数名称,定义为:
#ifndef PROJDEFS_H
#define PROJDEFS_H
typedef void (*TaskFunction_t)( void * );
#define pdFALSE ( ( BaseType_t ) 0 )
#define pdTRUE ( ( BaseType_t ) 1 )
#define pdPASS ( pdTRUE )
#define pdFAIL ( pdFALSE )
#endif /* PROJDEFS_H */
任务创建好之后,需要把任务添加到就绪列表中,表示任务已经就绪,系统随时可以调度。
/*任务就绪列表*/
List_t pxReadyTasksLists [ configMax_PRIORITIES ];
就绪列表实际上就是一个List_t 类型的数组。换句话说,数组元素是链表。
就绪列表如图所示:
任务列表初始化
/* 初始化任务相关的列表 */
void prvInitialiseTaskLists( void )
{
UBaseType_t uxPriority;
for( uxPriority = ( UBaseType_t ) 0U; uxPriority < ( UBaseType_t ) configMAX_PRIORITIES; uxPriority++ )
{
vListInitialise( &( pxReadyTasksLists[ uxPriority ] ) );
}
}
初始化后的列表示意图为:
将任务插入到就绪列表中
由于任务控制块任务控制块 里面有一个xStateListItem成员,数据类型为ListItem_t,可以将任务插入到就绪列表里面。
例子:将任务插入到列表项中:
/*初始化与任务相关的列表,这里是就绪列表*/
prvInitialiseTaskLists();
/* 创建任务 */
Task1_Handle = xTaskCreateStatic( (TaskFunction_t)Task1_Entry, /* 任务入口 */
(char *)"Task1", /* 任务名称,字符串形式 */
(uint32_t)TASK1_STACK_SIZE , /* 任务栈大小,单位为字 */
(void *) NULL, /* 任务形参 */
(StackType_t *)Task1Stack, /* 任务栈起始地址 */
(TCB_t *)&Task1TCB ); /* 任务控制块 */
/* 将任务添加到就绪列表 */
vListInsertEnd( &( pxReadyTasksLists[1] ), &( ((TCB_t *)(&Task1TCB))->xStateListItem ) );
Task2_Handle = xTaskCreateStatic( (TaskFunction_t)Task2_Entry, /* 任务入口 */
(char *)"Task2", /* 任务名称,字符串形式 */
(uint32_t)TASK2_STACK_SIZE , /* 任务栈大小,单位为字 */
(void *) NULL, /* 任务形参 */
(StackType_t *)Task2Stack, /* 任务栈起始地址 */
(TCB_t *)&Task2TCB ); /* 任务控制块 */
/* 将任务添加到就绪列表 */
vListInsertEnd( &( pxReadyTasksLists[2] ), &( ((TCB_t *)(&Task2TCB))->xStateListItem ) );
}
如图所示:
调度器
调度器是操作系统的核心,其主要功能是实现任务的切换,即从就绪队列中找到优先级高的任务,然后执行该任务。
在FreeRTOS中,调度器是由几个全局变量和一些可实现任务切换的函数组成,在task.c中实现。
调度器的启动由 vTaskStartScheduler() 函数完成。
void vTaskStartScheduler( void )
{
/* 手动指定第一个运行的任务 */
pxCurrentTCB = &Task1TCB;
/* 启动调度器 */
if( xPortStartScheduler() != pdFALSE )
{
/* 调度器启动成功,则不会返回,即不会来到这里 */
}
}
其中,pxCurrentTCB 为task.c 中的全局指针,用于指向当前正在运行或将要运行的任务控制块。
调用函数xPortStartScheduler() 启动调度器,调度器启动成功则不会返回该函数。
xPortStartScheduler() 函数
该函数可以简单理解为配置调度器
BaseType_t xPortStartScheduler( void )
{
/* 配置PendSV 和 SysTick 的中断优先级为最低 */
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
/* 启动第一个任务,不再返回 */
prvStartFirstTask();
/* 不应该运行到这里 */
return 0;
}
PendSV 和 SysTick 是属于Cortex-M 内核,并非外设,而内核的设置基本上是一致的,PendSV 和 SysTicks 是内核中断 ,所以直接操作 portNVIC_SYSPRI2_REG 和 portNVIC_SYSPRI2_REG 寄存器就可以了。
寄存器地址定义如下:
/*
* 参考资料《STM32F10xxx Cortex-M3 programming manual》4.4.3,百度搜索“PM0056”即可找到这个文档
* 在Cortex-M中,内核外设SCB中SHPR3寄存器用于设置SysTick和PendSV的异常优先级
* System handler priority register 3 (SCB_SHPR3) SCB_SHPR3:0xE000 ED20
* Bits 31:24 PRI_15[7:0]: Priority of system handler 15, SysTick exception
* Bits 23:16 PRI_14[7:0]: Priority of system handler 14, PendSV
*/
#define portNVIC_SYSPRI2_REG ( * ( ( volatile uint32_t * ) 0xe000ed20 ) )
#define portNVIC_PENDSV_PRI ( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) << 16UL )
#define portNVIC_SYSTICK_PRI ( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) << 24UL )
1)如何确认portNVIC_SYSPRI2_REG 的地址是0xe000ed20?
下载 PM0056 ,在Cortex-M中,内核外设SCB中SHPR3寄存器用于设置SysTick和PendSV的异常优先级,找到系统中断寄存器SCB_SHPR3
- 这两个寄存器中只有高4位是可以使用的,(rw是可读写,r是只读),configKERNEL_INTERRUPT_PRIORITY 是255,即等于0xff,或者是15
因此需要分别偏移16和24位。
2)PendSV 和 SysTick 的中断优先级为最低的原因?
- SysTick 和 PendSV 都会涉及系统调度,而系统调度的优先级要低于其他的硬件中断优先级,即优先响应系统中的外部中断,所以 SysTick 和 PendSV的 中断优先级为最低。
prvStartFirstTask()函数 (该函数是偏硬件底层的函数,用汇编语言编写,在port.c中实现)
该函数用于开始第一个任务,其中做了两件事,一是更新MSP的值,二是产生SVC调用,然后到SVC的中断服务函数中真正切换到第一个任务。
__asm void prvStartFirstTask( void )
{
PRESERVE8
/* 在Cortex-M中,0xE000ED08是SCB_VTOR这个寄存器的地址,
里面存放的是向量表的起始地址,即MSP的地址 */
ldr r0, =0xE000ED08
ldr r0, [r0]
ldr r0, [r0]
/* 设置主堆栈指针msp的值 */
msr msp, r0
/* 使能全局中断 */
cpsie i
cpsie f
dsb
isb
/* 调用SVC去启动第一个任务 */
svc 0
nop
nop
}
-
__asm 关键字用于调用内联汇编程序,并且可在 C 或 C++ 语句合法时出现。__asm 关键字用于调用内联汇编程序,并且可在 C 或 C++ 语句合法时出现。
逐句解释汇编代码: -
PRESERVE8
当前栈按照8字节对齐,如果都是32操作,则是四字节对齐。前文的任务栈初始化时同一样用到,并且具体阐述了是字节对齐的原理。 -
图中给的是SCB_VTOR寄存器的偏移量,SCB的基地指为:0xE000 ED00,加上偏移量,SCB_VTOR的地址为:0xE000ED08。SCB_VTOR寄存器里面存放的是msp的地址,即向量表的起始地址。向量表通常是·从内部FLASH的起始地址开始存放,即,memory 0x00000000处存放的就是msp的值。这里的memory是指存储器,也就是单片机内部或者外部连接的可读写的存储空间,比如RAM、ROM、FLASH等。
-
ldr
是Load Register的缩写,它是一种加载数据到寄存器的指令。(因为没有学过汇编,所以这里记录比较详细)
ps:立即数是一种在指令中直接给出的常量值,而不是从寄存器或内存中读取的值。 -
ldr r0, =0xE000ED08
: 把立即数0xE000ED08加载到寄存器r0中。这里的=号表示这个数是一个常量,而不是一个地址。 -
ldr r0, [r0]
:把r0寄存器中的值作为一个地址,从内存中读取一个字(32位)的数据,然后把这个数据存入r0寄存器中。即,将0xE000ED08指向的内容加载到寄存器r0中。(r0此时等于寄存器SCB_VTOR的值,为0x00000000)
ps: 这里的 [] 表示间接寻址,也就是通过寄存器中的值来访问内存。 -
ldr r0, [r0]
:再将0x00000000作为地址,将0x00000000指向的值存入到r0中。(此时r0中存的值为0x20008DB) -
为什么要执行两遍
ldr r0, [r0]
?
a:执行两遍ldr r0, [r0]的目的可能是为了实现间接寻址,也就是通过两次从内存中读取数据来得到最终的数据。例如,假设内存中的0xE000ED08这个地址处的数据是0x20008DB,而内存中的0x20008DB这个地址处的数据是0x12345678。那么,执行两遍ldr r0, [r0]之后,r0寄存器中的值就是0x12345678。这样可以实现一种类似于指针的机制,把一个地址存储在另一个地址中。 -
msr msp, r0
: 它将r0寄存器中存储的数据(也就是复位处理函数的入口地址)写入到主栈指针(MSP)寄存器中。也就是说,这一行实现了设置主栈指针msp的值为复位处理函数的入口地址。此时msp等于0x200008DB。
ps: msr是一个缩写,它表示Move to State Register,也就是将数据移动到状态寄存器中。
msr和ldr的主要区别如下:
msr指令的操作对象是状态寄存器,而ldr指令的操作对象是通用寄存器。
msr指令可以选择要操作的状态寄存器的域,而ldr指令只能操作整个通用寄存器。
msr指令可以用来实现对处理器状态的修改,比如切换处理器模式、允许或禁止中断、设置条件标志等。而ldr指令只能用来实现对通用寄存器的赋值。
msr指令的源操作数可以是通用寄存器或者立即数,而ldr指令的源操作数必须是一个存储器地址 -
cpsie i
和cpsie f
:为启用CPS指令把全局中断打开。
ps: 为了快速开关中断,Cotrtex-M内核专门设置了一条CPS指令CPSID I ;PRIMASK=1 ;关中断 CPSIE I ;PRIMASK=0 ;开中断 CPSID F ;PRIMASK=1 ;关异常 CPSIE F ;PRIMASK=1 ;开异常
-
dsb
和isb
: 执行数据同步屏障(dsb),执行指令同步屏障(isb)
ps: dsb是一个指令,它的作用是数据同步屏障。dsb指令要求所有在它前面的内存访问指令都执行完毕后,才会执行在它后面的指令,即任何指令都要等待dsb指令前面的内存访问指令完成。
isb是一个指令,它的作用是指令同步屏障。isb指令要求所有在它后面的指令都从指令高速缓存或内存中重新预取。它刷新流水线和预取缓冲区后才会从指令高速缓存或者内存中预取isb指令之后的指令。isb指令通常用来保证上下文切换(如ASID更改、TLB维护操作等)的效果。
那么为什么要在cpsie i
和cpsie f
后加上dsb
和isb
?
a: 一般来说,在开启中断和异常之前,需要确保之前的内存访问和上下文切换已经完成,否则可能会导致数据不一致或者执行错误的代码。因此,在cpsie i cpsie f之前加上dsb isb就是为了保证这一点。
举个例子,假设你有如下两条指令:
str x0, [x1] // 把x0寄存器的数据存储到x1地址
cpsie i // 开启IRQ中断
如果不加dsb isb,那么可能会出现这样的情况:str指令还没有把数据写入到内存中,就被一个IRQ中断打断了,而这个IRQ中断可能会读取x1地址的数据,从而得到错误的结果。如果你加上dsb isb,那么就可以保证str指令一定在cpsie i之前完成,并且IRQ中断能够正确地预取和执行代码。 -
svc 0
:产生系统调用,服务号0表示SVC中断,接下来会执行SVC中断服务函数。
ps:
1)SVC是一个缩写,它表示SuperVisorCall,也就是系统服务调用。SVC指令可以用来在操作系统上请求特权操作或访问系统资源。SVC指令中嵌入了一个数字,这个数字通常称为SVC编号。在大多数ARM处理器上,此编号用于指示要请求的服务。
2)SVC指令可以在ARM状态或Thumb状态下执行,但是有一些区别。在ARM状态下,SVC编号占用指令的0-23位,可以有24位的范围。在Thumb状态下,SVC编号占用指令的0-7位,只有8位的范围。因此,在Thumb状态下,如果要使用超过255的SVC编号,需要使用动态调用的方法。
3)当执行SVC指令时,处理器会进入异常模式,并跳转到SVC异常处理程序。在SVC异常处理程序中,需要从LR寄存器中获取返回地址,并从SPSR寄存器中获取处理器状态。然后需要从返回地址中提取SVC编号,并根据编号选择不同的服务函数。服务函数执行完毕后,需要恢复处理器状态,并返回到SVC指令之后的位置继续执行。 -
nop
和nop
: 表示执行两个空操作。
ps: NOP指令的作用是无操作,它是一种不改变任何程序可访问的寄存器,处理器状态标志或主存的指令,而且可能需要特定的时钟周期来执行。
那么为什么要在SVC指令后面加上两个NOP指令呢?
a: 一般来说,这是为了保证SVC指令能够正确地触发SVC异常处理程序,并且不会被其他异常打断。因为在ARM处理器中,当执行一条异常触发指令时,处理器会先完成当前的指令周期,然后再进入异常模式。这就意味着,在SVC指令之后的一条或两条指令可能还会被执行,而这些指令可能会影响程序的逻辑或状态。因此,在SVC指令之后加上NOP指令就是为了占据这些潜在的异常延迟槽,防止其他有意义的指令被执行。
vPortSVCHandler() 函数
vPortSVCHandler() 函数本来是为了响应SVC中断的,但是要想相应SVC中断,函数名称必须与向量表注册的名称一致。在启动文件的向量表中,SVC中断服务函数注册的名称是SVC_Handler,所以SVC中断服务函数应该写成SVC_Handler,而vPortSVCHandler是freeRTOS的官方命名版本。因此,为了能够顺利响应SVC中断,有2种方式:1)修改向量表中的SVC的注册函数名称,2)修改FreeRTOS中SVC的中断服务函数名称。这里采用第二种,在FreeRTOSConfig.h文件中通过添加宏的方式来修改。同理,PendSV和SysTick的中断服务函数以相同的方式修改。
#define xPortPendSVHandler PendSV_Handler
#define xPortSysTickHandler SysTick_Handler
#define vPortSVCHandler SVC_Handler
从 vTaskStartScheduler- > xPortStartScheduler -> prvStartFirstTask -> vPortSVCHandler,到这里才是真正意义上从底层启动第一个任务,并不再返回。
__asm void vPortSVCHandler( void )
{
extern pxCurrentTCB; /*声明外部变量pxCurrentTCB,pxCurrentTCB是在task.c中定义的全局指针吗,用于指向正在运行或即将运行的任务控制块*/
PRESERVE8
ldr r3, =pxCurrentTCB /* 加载pxCurrentTCB的地址到r3 */
ldr r1, [r3] /* 加载pxCurrentTCB到r1 */
ldr r0, [r1] /* 加载pxCurrentTCB指向的值到r0,目前r0的值等于第一个任务堆栈的栈顶 */
ldmia r0!, {r4-r11} /* 以r0为基地址,将栈里面的内容加载到r4~r11寄存器,同时r0会递增 */
msr psp, r0 /* 将r0的值,即任务的栈指针更新到psp */
isb
mov r0, #0 /* 设置r0的值为0 */
msr basepri, r0 /* 设置basepri寄存器的值为0,即所有的中断都没有被屏蔽 */
orr r14, #0xd /* 当从SVC中断服务退出前,通过向r14寄存器最后4位按位或上0x0D,
使得硬件在退出时使用进程堆栈指针PSP完成出栈操作并返回后进入线程模式、返回Thumb状态 */
bx r14 /* 异常返回,这个时候栈中的剩下内容将会自动加载到CPU寄存器:
xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)
同时PSP的值也将更新,即指向任务栈的栈顶 */
}
ldmia r0! {r4-r11}
: r0指向的地址开始,将内存中的数据依次加载到r4-r11中,并将r0更新为最后一个加载的寄存器的地址.
ps: ldmia是多数据加载指令,表示从内存中加载多个寄存器的值;
!表示加载后自动调节r0的值,使其指向最后一个加载的寄存器的地址;
{r4-r11}表示要加载的寄存器列表,从左到右依次加载;msr psp, r0
: 将r0的值存储到psp中,即更新线程模式下的堆栈指针.
ps: msr是将通用寄存器的值存储到特殊寄存器的指令;
psp是进程堆栈指针,用于保存线程模式下的堆栈地址;
r0是一个通用寄存器,可以存储任意数据;mov r0, #0
: 将寄存r0清零
ps: mov是将一个操作数的值移动到另一个操作数的指令,并且mov对立即数的长度有限制,只能是8位;
#0是一个立即数,表示数值0;msr basepri, r0
: 将r0的值存储到basepri中,即将寄存器BASEPRI的值为0,即不屏蔽(打开)所有中断。
ps: basepri是基一个中断屏蔽寄存器,大于或等于此寄存器值对应优先级的中断的值都被屏蔽;
basepri寄存器是一个8位的寄存器,但是它只有高位有效,低位被忽略;
在中断优先级数值是0-255,其中0是表示中断优先级最高,因此优先级上没有大于此寄存器的,所以是不屏蔽任何中断。orr r14, #0xd
: 当从SVC中断服务退出前,通过r14寄存器最后4位按位或上0x0D。使得硬件在退出时使用进程栈指针psp完成出栈操作并返回后进入任务模式,即返回Thumb状态。
ps: 在SVC中断服务中,使用的是msp栈指针,处于ARM状态。bx r14
: 意思是跳转到r14寄存器保存的地址中,并根据r14寄存器的最低位切换指令集。r14寄存器是链接寄存器LR,在异常处理中,它记录了异常返回值EXC_RETURN。EXC_RETURN的最低位决定了返回时使用的指令集是ARM还是Thumb。这条指令通常用于从异常处理程序返回到正常程序.(即,从中断返回)
ps: bx是分支交换的意思,它是一种跳转指令,用于从一个地址跳转到另一个地址,并根据目标地址的最低位切换指令集.
当r14的bit0为1表示返回thumb状态,bit1和bit2分别表示返回后sp用msp还是psp以及返回到特权模式还是用户模式。
任务切换
任务切换是在就绪队列中寻找优先级最高的任务就绪任务,进行执行该任务。
目前还没有学到优先级,因此仅实现2个任务的轮流切换。
任务切换函数 taskYIELD()
通过宏定义,先把taskYIELD 定义为portYILED
/*在task.h 中定义*/
#define taskYIELD() portYIELD()
/*在portmacro.h中定义*/
/* 中断控制状态寄存器:0xe000ed04
* Bit 28 PENDSVSET: PendSV 悬起位
*/
#define portNVIC_INT_CTRL_REG ( * ( ( volatile uint32_t * ) 0xe000ed04 ) )
#define portNVIC_PENDSVSET_BIT ( 1UL << 28UL )
#define portSY_FULL_READ_WRITE ( 15 )
#define portYIELD() \
{ \
/* 触发PendSV,产生上下文切换 */ \
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
__dsb( portSY_FULL_READ_WRITE ); \
__isb( portSY_FULL_READ_WRITE ); \
}
portYILED()的实现为将PendSV的悬起位置1,当没有其他中断运行时,执行PendSV中断,然后去执行PendSV中断服务函数,在里面实现任务切换。
在FreeRTOS中,将 xPortPendSVHandler() 宏定义为 PendSV (在FreeRTOSConfig.h)
#define xPortPendSVHandler PendSV_Handler
__asm void xPortPendSVHandler( void )
{
extern pxCurrentTCB;
extern vTaskSwitchContext;
PRESERVE8
/* 当进入PendSVC Handler时,上一个任务运行的环境即:
xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)
这些CPU寄存器的值会自动保存到任务的栈中,剩下的r4~r11需要手动保存 */
/* 获取任务栈指针到r0 */
mrs r0, psp
isb
ldr r3, =pxCurrentTCB /* 加载pxCurrentTCB的地址到r3 */
ldr r2, [r3] /* 加载pxCurrentTCB到r2 */
stmdb r0!, {r4-r11} /* 将CPU寄存器r4~r11的值存储到r0指向的地址 */
str r0, [r2] /* 将任务栈的新的栈顶指针存储到当前任务TCB的第一个成员,即栈顶指针 */
stmdb sp!, {r3, r14} /* 将R3和R14临时压入堆栈,因为即将调用函数vTaskSwitchContext,
调用函数时,返回地址自动保存到R14中,所以一旦调用发生,R14的值会被覆盖,因此需要入栈保护;
R3保存的当前激活的任务TCB指针(pxCurrentTCB)地址,函数调用后会用到,因此也要入栈保护 */
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY /* 进入临界段 */
msr basepri, r0
dsb
isb
bl vTaskSwitchContext /* 调用函数vTaskSwitchContext,寻找新的任务运行,通过使变量pxCurrentTCB指向新的任务来实现任务切换 */
mov r0, #0 /* 退出临界段 */
msr basepri, r0
ldmia sp!, {r3, r14} /* 恢复r3和r14 */
ldr r1, [r3]
ldr r0, [r1] /* 当前激活的任务TCB第一项保存了任务堆栈的栈顶,现在栈顶值存入R0*/
ldmia r0!, {r4-r11} /* 出栈 */
msr psp, r0
isb
bx r14 /* 异常发生时,R14中保存异常返回标志,包括返回后进入线程模式还是处理器模式、
使用PSP堆栈指针还是MSP堆栈指针,当调用 bx r14指令后,硬件会知道要从异常返回,
然后出栈,这个时候堆栈指针PSP已经指向了新任务堆栈的正确位置,
当新任务的运行地址被出栈到PC寄存器后,新的任务也会被执行。*/
nop
}
str r0, [r2]
:将r0存储到r2指向的内容中,此时r2= pxCurrentTCB。具体为将r0的值存储到上一个任务的栈顶指针pxTopOfStack。
vTaskSwitchContext() 函数 (真正意义上实现任务切换的函数)
void vTaskSwitchContext( void )
{
/* 两个任务轮流切换 */
if( pxCurrentTCB == &Task1TCB )
{
pxCurrentTCB = &Task2TCB;
}
else
{
pxCurrentTCB = &Task1TCB;
}
}
实验
/**
************************************************************************
* @file main.c
* @author fire
* @version V1.0
* @date 2018-xx-xx
* @brief 《FreeRTOS内核实现与应用开发实战指南》书籍例程
* 新建FreeRTOS工程—软件仿真
************************************************************************
* @attention
*
* 实验平台:野火 STM32 系列 开发板
*
* 官网 :www.embedfire.com
* 论坛 :http://www.firebbs.cn
* 淘宝 :https://fire-stm32.taobao.com
*
************************************************************************
*/
/*
*************************************************************************
* 包含的头文件
*************************************************************************
*/
#include "FreeRTOS.h"
#include "task.h"
/*
*************************************************************************
* 全局变量
*************************************************************************
*/
portCHAR flag1;
portCHAR flag2;
extern List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
/*
*************************************************************************
* 任务控制块 & STACK
*************************************************************************
*/
TaskHandle_t Task1_Handle;
#define TASK1_STACK_SIZE 20
StackType_t Task1Stack[TASK1_STACK_SIZE];
TCB_t Task1TCB;
TaskHandle_t Task2_Handle;
#define TASK2_STACK_SIZE 20
StackType_t Task2Stack[TASK2_STACK_SIZE];
TCB_t Task2TCB;
/*
*************************************************************************
* 函数声明
*************************************************************************
*/
void delay (uint32_t count);
void Task1_Entry( void *p_arg );
void Task2_Entry( void *p_arg );
/*
************************************************************************
* main函数
************************************************************************
*/
/*
* 注意事项:1、该工程使用软件仿真,debug需选择 Ude Simulator
* 2、在Target选项卡里面把晶振Xtal(Mhz)的值改为25,默认是12,
* 改成25是为了跟system_ARMCM3.c中定义的__SYSTEM_CLOCK相同,确保仿真的时候时钟一致
*/
int main(void)
{
/* 硬件初始化 */
/* 将硬件相关的初始化放在这里,如果是软件仿真则没有相关初始化代码 */
/* 初始化与任务相关的列表,如就绪列表 */
prvInitialiseTaskLists();
/* 创建任务 */
Task1_Handle = xTaskCreateStatic( (TaskFunction_t)Task1_Entry, /* 任务入口 */
(char *)"Task1", /* 任务名称,字符串形式 */
(uint32_t)TASK1_STACK_SIZE , /* 任务栈大小,单位为字 */
(void *) NULL, /* 任务形参 */
(StackType_t *)Task1Stack, /* 任务栈起始地址 */
(TCB_t *)&Task1TCB ); /* 任务控制块 */
/* 将任务添加到就绪列表 */
vListInsertEnd( &( pxReadyTasksLists[1] ), &( ((TCB_t *)(&Task1TCB))->xStateListItem ) );
Task2_Handle = xTaskCreateStatic( (TaskFunction_t)Task2_Entry, /* 任务入口 */
(char *)"Task2", /* 任务名称,字符串形式 */
(uint32_t)TASK2_STACK_SIZE , /* 任务栈大小,单位为字 */
(void *) NULL, /* 任务形参 */
(StackType_t *)Task2Stack, /* 任务栈起始地址 */
(TCB_t *)&Task2TCB ); /* 任务控制块 */
/* 将任务添加到就绪列表 */
vListInsertEnd( &( pxReadyTasksLists[2] ), &( ((TCB_t *)(&Task2TCB))->xStateListItem ) );
/* 启动调度器,开始多任务调度,启动成功则不返回 */
vTaskStartScheduler();
for(;;)
{
/* 系统启动成功不会到达这里 */
}
}
/*
*************************************************************************
* 函数实现
*************************************************************************
*/
/* 软件延时 */
void delay (uint32_t count)
{
for(; count!=0; count--);
}
/* 任务1 */
void Task1_Entry( void *p_arg )
{
for( ;; )
{
flag1 = 1;
delay( 100 );
flag1 = 0;
delay( 100 );
/* 任务切换,这里是手动切换 */
taskYIELD();
}
}
/* 任务2 */
void Task2_Entry( void *p_arg )
{
for( ;; )
{
flag2 = 1;
delay( 100 );
flag2 = 0;
delay( 100 );
/* 任务切换,这里是手动切换 */
taskYIELD();
}
}
仿真
学习于《FreeRTOS内核实现与应用开发实战指南–基于stm32》
b站视频地址:https://www.bilibili.com/video/BV1Jx411X7NS/