ARM-Cortex-M3任务创建与切换(简易板)

  • Post author:
  • Post category:其他


芯片: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);

}

}



版权声明:本文为m0_47457087原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。