RTOS多任务切换实现

本文深入探讨了ARM Cortex-M3处理器的程序内部结构、汇编指令、AAPCS调用规范以及中断异常处理流程。重点在于多任务切换的实现,包括栈帧构建、任务切换的核心——栈、任务创建以及任务切换的汇编实现。通过具体代码示例展示了如何在Cortex-M3上伪造任务栈并进行任务切换,以实现多任务并发执行。
摘要由CSDN通过智能技术生成

实现任务需要的基础知识

1、程序内部细节

通过分析C语言程序的编码会发现程序都是一些指令和数据。
什么是程序?

  1. 指令
  2. 运行过程中的数据

2、常用汇编指令

汇编指令详解


3、ARM架构过程调用标准AAPCS

传参:

通过r0-r3传递,多于4个参数的部分用栈传递

返回值:

通过r0寄存器

C函数调用过程寄存器变化:

1、随意使用R0、R1、R2、R3、 R12 无需保护它们,硬件自动保存
2、r4-r11 可用,先保存,用完后要恢复原来的值

特殊的寄存器:

r13 - sp指针
r14 - LR链接寄存器,保存子程序返回地址
r15 - PC程序计数器,PC指向哪里,程序就运行到哪里

4、Cortex-M3中断异常处理流程

在这里插入图片描述

栈帧图中的返回地址保存中断执行结束后的返回地址,也就是中断结束后返回执行的第一条语句。

中断异常处理流程:

1、保存中断处理完成后的返回地址,这是由硬件自动保存的
2、中断处理,硬件自动调用中断服务函数,中断服务程序也是C函数,C语言函数调用过程会保证不破坏R4~R11
3、中断处理完成之后是恢复现场,硬件自动恢复R4-R11之外的寄存器,R4-R11在中断处理函数执行结束会恢复,所以保持不变

中断异常返回:

这里是引用8.1.4 EXC_RETURN
处理器进人异常处理或中断服务程序(ISR)时,链接寄存器(LR)的数值会被更新为EXC_RETURN数值。当利用BX、POP或存储器加载指令(LDR或LDM)被加中时,该数值用于触发异常返回机制。
EXC_RETURN中的一些位用于提高异常流程的其他信息。EXC_RETURN义如表8.1所示,EXC_RETURN的合法值则如表8.2所示。由于EXC_RETURN的编码格式,在地址区域0xF0000000一0xFFFFFFFF
行中断返回的。不过,由于系统空间中的地址区域已经被架构定义为不可执会带来什么问题。
在这里插入图片描述

—— 引用自《ARM Cortex-M3与Cortex-M4权威指南》
从这段话中可以知道,中断触发时硬件会自动会将LR寄存器设置为0xF0000000—0xFFFFFFFF这个范围的某个数值,这个数值用于中断返回
当使用BX之类的指令进行BX LR时,由于LR此时是个特殊值,不可执行,此时硬件就知道要触发硬件恢复R0-R3、R12等寄存器。


多任务切换实现

1、任务切换的核心 — 栈

任务切换的核心是切换任务的栈
创建任务实质就是伪造任务现场(栈帧)
任务能实现切换的核心也是栈。

Cortex-M3的栈帧的结构:
在这里插入图片描述
什么是现场? —— 当前执行程序被打断瞬间所有寄存器的值。
怎么保存现场? —— 存储到内存。
保存到内存什么地方? —— 栈

程序状态寄存器:
在这里插入图片描述
在这里插入图片描述
—— 引用自《ARM Cortex-M3与Cortex-M4权威指南》 4.2.3 特殊寄存器

PSR寄存器T位设置为1表示使用的是Thumb指令(16bit),0表示使用ARM指令(32bit),Cortex-M内核只支持Thumb指令。Thumb指令可以更好的节省空间。

2、伪造任务栈(现场)

伪造任务栈也就是所谓的创建任务(线程)。

void os_thread_create(thread_entry entry, void *arg, void *stack_addr, uint32_t stack_size)
{
    char *cstack = (char *)stack_addr;
    
    cstack += stack_size;    /*get stack top */
    
    uint32_t *stack =  (uint32_t *)cstack;
    
    stack -= 16;    /* 因为栈是向下生长;空出16x4的空间刚好能构造一个栈帧 */
    
    stack[0] = 0;   /* R4 */
    stack[1] = 0;   /* R5 */
    stack[2] = 0;   /* R6 */
    stack[3] = 0;   /* R7 */
    stack[4] = 0;   /* R8 */
    stack[5] = 0;   /* R9 */
    stack[6] = 0;   /* R10 */
    stack[7] = 0;   /* R11 */
    
    stack[8] = (uint32_t)arg;   /* R0 传递给线程函数的参数,只有一个参数,根据AAPCS规则是通过R0传递第一个参数 */
    stack[9] = 0;   /* R1 */
    stack[10] = 0;   /* R2 */
    stack[11] = 0;   /* R3 */
    
    stack[12] = 0;   /* R12 */
    stack[13] = 0;   /* LR */
    
    stack[14] = (uint32_t)entry;   /* 返回地址,中断产生执行结束后返回地址,从中断发挥执行任务第一条语句肯定是线程函数入口地址 */
    
    stack[15] = (1 << 24);   /* PSR, 设置24位为1表示使用Thumb指令 */
    
    thread_stacks_sp[thread_count] = (uint32_t)stack;    /* 记录栈恢复的位置 */
    thread_count++;                                   /* 线程计数+1 */
}
  • 对于寄存器位置的排布参考栈帧图才能更好理解。
  • R4-R11、R12、LR的值可以随便给,根据AAPCS规则,R0用来参数传递函数的第一个参数,任务函数也是函数,所以R0赋值的是给任务函数的参数。

2、任务切换实现

任务切换基本要靠汇编实现,也可以C内联汇编实现,但还是使用汇编方便。
任务调度器实现:

static uint8_t os_is_starting = 0;

static uint32_t thread_stacks_sp[OS_MAX_THREADS];
static uint32_t thread_count = 0;

static uint32_t cur_thread = -1;

void os_thread_scheduler(uint32_t lr, uint32_t new_sp)
{
    uint32_t prev_thread;
    uint32_t sp;
    
    if (os_is_starting == 0) return;   /* 没有启动 */
    
    if (cur_thread == -1)
    {
        cur_thread = 0;
        
        sp = thread_stacks_sp[cur_thread]; /* 得到当前任务的sp */
        Thread_Switch_Context(sp, lr);
    }
    else 
    {
        prev_thread = cur_thread;
        uint32_t next_thread = (cur_thread + 1) % thread_count;
        if (prev_thread != next_thread)  /* 当它们相等时只有一个任务,不用切换 */
        {
            thread_stacks_sp[prev_thread] = new_sp;  /* 更新上一个任务的栈位置 */
            sp = thread_stacks_sp[next_thread];  /* 获取当前任务SP位置 */
            cur_thread = next_thread;  /* 指向下一个要执行的任务 */
            Thread_Switch_Context(sp, lr);  /* 触发任务切换 */
        }
    }
}
  • thread_stacks_sp[OS_MAX_THREADS],保存创建的任务的sp。
  • thread_count ,任务计数
  • cur_thread ,指向当前任务,当其为-1时,表示第一次切换任务。
  • os_is_starting ,系统启动后再进行任务切换。
  • Thread_Switch_Context,切换到下一个任务,汇编函数实现。

任务切换汇编实现:

; 在SysTick中断中实现保存上一个任务的现场
SysTick_Handler PROC
				IMPORT SysTick_IRQ
					
				STMDB sp!, {r4 - r11}    ; 将当前任务的r4-r11寄存器内容到其栈中
				STMDB sp!, { lr }   ; 保存LR到栈中
				
				MOV R0, LR      ; 此时LR是个特殊值,保存此时的LR值,通过R0传递给函数SysTick_IRQ
				ADD R1, SP, #4  ; R1 =  sp + 4 得到栈的真正位置,因为多保存了LR,栈向下生长,所以需要+4才得到真正的栈点
				BL SysTick_IRQ
				
				LDMIA sp!, { r0 }     ; 当系统没有,没有任务的时候则执行到这里,所以要从栈中恢复原来的寄存器内容
				LDMIA sp!, {r4 - r11}
				
				ENDP
				
;切换到下一个任务				
Thread_Switch_Context PROC
				EXPORT Thread_Switch_Context
				LDMIA r0!, {r4 - r11}  ; 从栈中恢复r4-r11的内容
				
				MSR MSP, R0 ; 将MSP设置为任务的sp
				
				BX r1     ; 通过特殊的LR值跳转触发硬件自动恢复r0、r1、r2等寄存器的
				
				ENDP
  • ADD R1, SP, #4 才能得到任务的SP:
    在这里插入图片描述

  • r0、r2、r3、r12等寄存器的值中断触发会由自动保存。

  • STMDB sp!, { lr } , 在调用SysTick_IRQ函数前保存LR,LR此时是个特殊值,后续通过其触发硬件自动恢复自动恢复r0、r2、r3、r12等寄存器的值。为什么要保存LR?因为SysTick_IRQ是C函数,C函数调用会破坏原来的LR值。

  • Thread_Switch_Context 函数传入LR值,此时lr值是个特殊值,通过BX r1 触发硬件自动恢复r0、r2、r3、r12等寄存器的值。

  • Cortex-M内核有两个SP:MSP和PSP,使用哪一个实现任务切换都可以,在同一时刻只能使用其中一个,这里使用MSP。

  • 在中断处理函数,如果直接访问sp,sp指向的MSP。

只是为了实现多任务切换,为了方便直接在Systick中断中进行执行调度器:

void SysTick_IRQ(uint32_t lr, uint32_t prev_thread_sp)
{
	SCB_Type * SCB = (SCB_Type *)SCB_BASE_ADDR;
	
	os_thread_scheduler(lr, prev_thread_sp);
	
	/* clear exception status */
	SCB->ICSR |= SCB_ICSR_PENDSTCLR_Msk;
}
  • prev_thread_sp上一个任务的sp,作为参数传进来是为了保存起来。

MDK仿真测试

uint32_t thread_a_stack[1024/4];
uint32_t thread_b_stack[1024/4];
uint32_t thread_c_stack[1024/4];

void thread_a_entry(void *arg)
{
    char a = 'a';
    
    while (1)
    {
        putchar(a);
//        puts("\r\n");
    }
}

void thread_b_entry(void *arg)
{
    char b = 'b';
    
    while (1)
    {
        putchar(b);
//        puts("\r\n");
    }
}

void thread_c_entry(void *arg)
{
    int i;
    int sum = 0;
    
    for (i = 0; i < 10; i++)
    {
        sum += i;
    }
    
    while (1)
    {
        put_s_hex("sum = ", sum);
    }
}

int mymain()
{
    puts("os start\r\n");
    
    os_thread_create(thread_a_entry, "Thread a", thread_a_stack, 1024);
    os_thread_create(thread_b_entry, "Thread b", thread_b_stack, 1024);
    os_thread_create(thread_c_entry, "Thread c", thread_c_stack, 1024);
    
    os_start(); 
     
    while(1);

	return 0;
}

由于SysTick设置的1s定时加之仿真时间也不准,所以数据会疯狂打印,很久才切换到下一个任务,实现多个任务切换是没问题的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

欲盖弥彰1314

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值