ARM-Cortex-M3任务创建与切换(简易板)
芯片:GD32F103C8T6
编译环境:DMK Keil5
参考资料:
《FreeRTOS 内核实现与应用开发实战—基于STM32》—野火
《Cortex-M3权威指南(中文)》
《STM32F3与F4系列Cortex M4内核编程手册》
一、任务创建
1、申请内存空间
栈内存:
#define TASK1_STACK_SIZE 128
StackType_t Task1Stack[TASK1_STACK_SIZE];
#define TASK2_STACK_SIZE 128
StackType_t Task2Stack[TASK2_STACK_SIZE];
任务句柄和任务控制块:
typedef void * TaskHandle_t;
typedef struct tskTaskControlBlock
{
volatile StackType_t
pxTopOfStack; /
栈顶
/
ListItem_t xStateListItem; /
任务节点 */
StackType_t
pxStack; /
任务栈起始地址
/
char pcTaskName[ configMAX_TASK_NAME_LEN ];/
任务名称,字符串形式 */
} tskTCB;
typedef tskTCB TCB_t;
TaskHandle_t Task1_Handle;
TCB_t Task1TCB;
TaskHandle_t Task2_Handle;
TCB_t Task2TCB;
2、创建任务函数:
static void LED1_Task(void *parameter)
{
while(1)
{
gpio_bit_reset(GPIOA,GPIO_PIN_4);//LED off
delay_1ms(1000);
gpio_bit_set(GPIOA,GPIO_PIN_4); //LED on
delay_1ms(1000);
taskYIELD();//任务切换,这里是手动切换
}
}
3、初始化任务1内存数据
将栈其实地址与任务控制块关联:
Task1TCB.pxStack = ( StackType_t * ) Task2Stack;
获取栈顶地址并8字节对齐:
StackType_t *pxTopOfStack;
//获取栈顶地址
pxTopOfStack = Task1TCB.pxStack + ( TASK1_STACK_SIZE – ( uint32_t ) 1 );
//向下做 8 字节对齐
pxTopOfStack = ( StackType_t * ) ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );
将任务的名字存储在 TCB 中
设置 xStateListItem 节点的拥有者
listSET_LIST_ITEM_OWNER( &( Task1TCB.xStateListItem ), Task1TCB );
模拟的异常响应序列,开始自动入栈,即把xPSR, PC, LR, R12, R3‐R0压入堆栈:
pxTopOfStack--; /* Offset added to account for the way the MCU uses the stack on entry/exit of interrupts. */
*pxTopOfStack = portINITIAL_XPSR; /* xPSR(状态字寄存器)的位24置1,其他位为0。Bit24(T: Thumb state bit.),在 CM3 中 T 位必须是 1,所*/
pxTopOfStack--;
*pxTopOfStack = ( ( StackType_t ) LED1_Task ) & portSTART_ADDRESS_MASK; /* PC */
pxTopOfStack--;
*pxTopOfStack = ( StackType_t ) prvTaskExitError; /* LR */
pxTopOfStack -= 5; /* R12, R3, R2 and R1. */
*pxTopOfStack = ( StackType_t ) pvParameters; /* R0 */
pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4. */
将任务控制块添加到就绪列表
vListInsertEnd( &( pxReadyTasksLists
1
),&( ((TCB_t *)(&Task1TCB))->xStateListItem ) );
4、初始化任务2内存数据(同上)
二、启动调度
1、配置 PendSV 和 SysTick 的中断优先级为最低
#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 )
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
2、开始第一个任务
__asm void prvStartFirstTask( void )
{
PRESERVE8//当前栈需按照 8 字节对齐
/* 向量表偏移寄存器0xE000ED08,里面存放的是向量表的起始地址,即 MSP 的地址
—-《STM32F3与F4系列Cortex M4内核编程手册》P212
/
ldr r0, =0xE000ED08
ldr r0, [r0]
ldr r0, [r0]
/
设置主堆栈指针 msp 的值
/
msr msp, r0
/
使能全局中断
/
cpsie i//等同于PRIMASK=0,快速开中断
cpsie f//等同于FAULTMASK=0,快速开异常
dsb//数据同步屏障指令。它的作用是等待所有前面的指令完成后再执行后面的指令。
isb//指令同步屏障。它的作用是等待流水线中所有指令执行完成后再执行后面的指令。
/
调用 SVC 去启动第一个任务 */
svc 0
nop
nop
}
3、执行SCV调用
SCV中断中完成了:将Task1TCB栈中的相应内容加载到r4-r11;将psp指向Task1TCB栈;打开所有中断;设置r14 为 0xFFFFFFFD,使得硬件在退出时使用进程堆栈指针 PSP 完成出栈操作并返回后进入任务模式、返回 Thumb 状态;退出中断到任务1中。
#define vPortSVCHandler SVC_Handler
pxCurrentTCB=Task1TCB;
__asm void vPortSVCHandler(void)
{
extern pxCurrentTCB;
PRESERVE8//8 字节对齐
ldr r3, =pxCurrentTCB /* 加载 pxCurrentTCB 的地址到 r3。
/
ldr r1, [r3] /
加载 pxCurrentTCB 到 r1。
/
ldr r0, [r1] /
任务控制块的第一个成员就是栈顶指针,所以此时 r0 等于栈顶指针。
/
ldmia r0!, {r4-r11} /
以 r0 为基地址,将栈中向上增长的 8 个字的内容加载到 CPU 寄存器 r4~r11,同时 r0 也会跟着自增。
/
msr psp, r0 /
将新的栈顶指针 r0 更新到 psp,任务执行的时候使用的堆栈指针是psp。 */
isb//指令同步屏障。它的作用是等待流水线中所有指令执行完成后再执行后面的指令。
mov r0, #0//将寄存器 r0清 0。
msr basepri, r0//即打开所有中断。
/*0x0d=1101b
当 r14 为 0xFFFFFFFX,执行是中断返回指令,cortext-m3 的做法,X 的 bit0 为 1 表示返回 thumb 状态,
bit1 和 bit2 分别表示返回后 sp 用 msp 还是 psp、以及返回到特权模式还是用户模式。
当从 SVC 中断服务退出前,通过向 r14 寄存器最后 4 位按位或上0x0D,使得硬件在退出时使用进程堆
栈指针 PSP 完成出栈操作并返回后进入任务模式、返回 Thumb 状态。
在 SVC 中断服务里面,使用的是 MSP 堆栈指针,是处在 ARM 状态。
/
orr r14, #0xd
/
异常返回,这个时候出栈使用的是 PSP 指针,自动将栈中的剩下
内容加载到 CPU 寄存器: xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0
(任务的形参)同时 PSP 的值也将更新,即指向任务栈的栈顶,
*/
bx r14
}
三、任务切换
taskYIELD()函数
任务切换通过调用taskYIELD()函数实现,taskYIELD()函数里就是通过触发 PendSV实现。
#define taskYIELD() portYIELD()
//0xE000ED04:中断控制状态寄存器 [28]PENDSVSET 设置挂起pendSV位:1= 设置挂起 pendSV 《CM3技术参考手册》P88
#define portNVIC_INT_CTRL_REG ( * ( ( volatile uint32_t * ) 0xe000ed04 ) )
#define portNVIC_PENDSVSET_BIT ( 1UL << 28UL )
#define portEND_SWITCHING_ISR( xSwitchRequired ) if( xSwitchRequired != pdFALSE ) portYIELD()
#define portYIELD_FROM_ISR( x ) portEND_SWITCHING_ISR( x )
#define portYIELD()
{
/* 触发 PendSV,产生上下文切换.
/
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
/
__dsb:数据同步屏障,确保在下一条指令开始执行前,所有的存储器访问已经完成。
__isb: 指令同步屏障,清除流水线并且确保在新指令执行时,之前的指令都已经执行完毕。*/
__dsb( portSY_FULL_READ_WRITE );
__isb( portSY_FULL_READ_WRITE );
}
PendSV中断
PendSV回调函数中实现了:硬件将CPU 寄存器(R0(任务形参)、R1、R2、R3、R12、R14(LR)、R15(PC)和 xPSR)压入原任务栈中;将r4~r11压入原任务栈中,调用vTaskSwitchContext函数切换pxCurrentTCB指向新任务的控制块;打开中断;将新任务栈中相应数数存入r4-r11;将新任务栈地址存入psp,此时的 r14 等于 0xfffffffd,表示异常返回后进入任务模式,SP 以 PSP 作为堆栈指针出栈,出栈完毕后 PSP 指向新任务栈的栈顶;中断结束,硬件将新任务栈中剩下的内容加载到 CPU 寄存器(R0(任务形参)、R1、R2、R3、R12、R14(LR)、R15(PC)和 xPSR),从而切换到新的任务。
#define xPortPendSVHandler PendSV_Handler
__asm void xPortPendSVHandler( void )
{
extern pxCurrentTCB;//声明外部变量 pxCurrentTCB,用于指向当前正在运行或者即将要运行的任务的任务控制块。
extern vTaskSwitchContext;//声明外部函数 vTaskSwitchContext
PRESERVE8
/*当进入 PendSVC Handler 时,上一个任务运行的环境即:
xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)
这些 CPU 寄存器的值会自动存储到任务的栈中,并且在返回时自动弹出它们.
剩下的 r4~r11 需要手动保存,同时PSP 会自动更新(在更新之前 PSP 指向任务栈的栈顶),
/
mrs r0, psp//将 PSP 的值存储到 r0。
isb//指令同步屏障。它的作用是等待流水线中所有指令执行完成后再执行后面的指令。
ldr r3, =pxCurrentTCB /
Get the location of the current TCB. */
ldr r2, [r3]//即 r2 等于 pxCurrentTCB。
/*由于前面“mrs r0, psp”将 PSP 的值存储到 r0。
以 r0 作为基址(指针先递减,再操作,STMDB 的 DB 表示Decrease Befor),
将 CPU 寄存器 r4~r11 的值存储到任务栈,同时更新 r0的值。
/
stmdb r0!, {r4-r11}
/
由于前面“r2, [r3]”将r2 等于 pxCurrentTCB。pxCurrentTCB首个元素即为栈顶指针 pxTopOfStack。
r0就存储了pxCurrentTCB的栈顶指针。到此,上下文切换中的上文保存就完成了。
/
str r0, [r2] /
Save the new top of stack into the first member of the TCB.
/
/
lr(r14)的作用问题,这个lr一般来说有两个作用:
(1)当使用bl或者blx跳转到子过程的时候,r14保存了返回地址,可以在调用过程结尾恢复。
(2)异常中断发生时,这个异常模式特定的物理R14被设置成该异常模式将要返回的地址。
因为接下来要调用函数 vTaskSwitchContext,调用函数时,返回
地址自动保存到 R14 中,所以一旦调用发生,R14 的值会被覆盖(PendSV 中断服务函数执
行完毕后,返回的时候需要根据 R14 的值来决定返回处理器模式还是任务模式,出栈时使
用的是 PSP 还是 MSP),因此需要入栈保护.
R3 保存的是TCB 指针(pxCurrentTCB)地址,函数调用后 pxCurrentTCB 的值会被更新
/
stmdb sp!, {r3, r14}//将 R3 和 R14 临时压入堆栈(在整个系统中,中断使用的是主堆栈,栈指针使用的是 MSP)
//关中断,进入临界段
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0
dsb
isb
bl vTaskSwitchContext//调用函数 vTaskSwitchContext选择优先级最高的任务,然后更新 pxCurrentTCB。
//退出临界段,开中断,直接往 BASEPRI 写 0。
mov r0, #0
msr basepri, r0
ldmia sp!, {r3, r14}//从主堆栈中恢复寄存器 r3 和 r14 的值,此时的 sp 使用的是 MSP。
ldr r1, [r3]
ldr r0, [r1] /
加载 r1 指向的内容到 r0,即下一个要运行的任务的栈顶指针。
/
ldmia r0!, {r4-r11} /
将下一个要运行的任务的任务栈的内容加载到 CPU 寄存器 r4~r11。
/
msr psp, r0//更新 psp 的值,等下异常退出时,会以 psp 作为基地址,将任务栈中剩下的内容自动加载到 CPU 寄存器。
isb
/
异常发生时,R14 中保存异常返回标志,包括返回后进入任务模
式还是处理器模式、使用 PSP 堆栈指针还是 MSP 堆栈指针。此时的 r14 等于 0xfffffffd,最
表示异常返回后进入任务模式,SP 以 PSP 作为堆栈指针出栈,出栈完毕后 PSP 指向任务栈
的栈顶。当调用 bx r14 指令后,系统以 PSP 作为 SP 指针出栈,把接下来要运行的新任务
的任务栈中剩下的内容加载到 CPU 寄存器:R0(任务形参)、R1、R2、R3、R12、R14
(LR)、R15(PC)和 xPSR,从而切换到新的任务。
*/
bx r14
nop
}
void vTaskSwitchContext( void )
{
if( pxCurrentTCB == &Task1TCB )
{
/* The scheduler is currently suspended – do not allow a context
switch. */
pxCurrentTCB = &Task2TCB;
}
else
{
pxCurrentTCB = &Task1TCB;
}
}
四、main()函数
TaskHandle_t Task1_Handle;
#define TASK1_STACK_SIZE 128
StackType_t Task1Stack[TASK1_STACK_SIZE];
TCB_t Task1TCB;
TaskHandle_t Task2_Handle;
#define TASK2_STACK_SIZE 128
StackType_t Task2Stack[TASK2_STACK_SIZE];
TCB_t Task2TCB;
int main(void)
{
BSP_init();
//初始化与任务相关的列表,如就绪列表
prvInitialiseTaskLists();
Task1_Handle=
xTaskCreateStatic((TaskFunction_t)LED1_Task,//任务入口函数
(const char *) “LED1_Task”,//任务名称
(uint16_t) TASK1_STACK_SIZE,//任务栈大小
(void *) NULL,//入口函数参数
(StackType_t
)Task1Stack, /
任务栈起始地址 */
(TCB_t
)&Task1TCB);//任务控制块
/
将任务添加到就绪列表 */
vListInsertEnd( &( pxReadyTasksLists
1
),&( ((TCB_t *)(&Task1TCB))->xStateListItem ) );
Task2_Handle=
xTaskCreateStatic((TaskFunction_t)LED2_Task,//任务入口函数
(const char *) “LED2_Task”,//任务名称
(uint16_t) TASK2_STACK_SIZE,//任务栈大小
(void *) NULL,//入口函数参数
(StackType_t
)Task2Stack, /
任务栈起始地址 */
(TCB_t *)&Task2TCB);//任务控制块
vListInsertEnd( &( pxReadyTasksLists
2
), &( ((TCB_t
)(&Task2TCB))->xStateListItem ) );
/
启动调度器,开始多任务调度,启动成功则不返回 */
vTaskStartScheduler();
while(1)
{
gpio_bit_reset(GPIOA,GPIO_PIN_5);//LED off
gpio_bit_reset(GPIOA,GPIO_PIN_4);//LED off
delayms(1000);
gpio_bit_set(GPIOA,GPIO_PIN_5); //LED on
gpio_bit_set(GPIOA,GPIO_PIN_4);//LED off
delayms(2000);
}
}