|
看本文请参考《都江堰操作系统与嵌入式系统设计》第15章,该书可在www.djyos.com下载。
写这一篇的时候,djyos在stm32上已经跑起来了,只是串口驱动还没有写,不能输出而已。从之一到之五,东扯西扯了很多东西,包括移植方法,中断系统实现方法,上下文设计、开发工具等,除了在之三列出了初始化代码外,其他几篇都没有代码。这是因为代码一直在调试中,变化很大,写出来也是白搭,这不,之三列出的代码,就是针对gcc环境的,在mdk下全改了。写移植日志,如果没有代码的话,原理讲得再多,也是纸上谈兵,现在系统跑起来了,底层代码基本不会变了,我们就结合代码来讲解一下。
移植djyos,需要修改的代码主要有:中断相关代码、cpu硬件初始化代码、上下文操作代码等。djyos代码在所有可能需要在移植时修改的代码中,均有“移植关键”字样的注释。
我们先对比arm7来讲解一下cm3中上下文切换。
1、设置初始上下文
线程的上下文指线程执行环境的当前状态,其实也就是影响该线程执行的cpu寄存器的状态,在cm3中,这些寄存器一共有17个:R0-R15(R15即PC)和xpsr,但从之四上的那张图上,我们并没有看到R13(即SP),那是因为上下文保存在当前线程的栈的当前位置,该位置是变化的,而恢复上下文又必须先得到SP才可以,所以SP只能保存在某一个固定的位置,而不能保存在栈中。djyos把SP保存在当前线程虚拟机的线程控制块中。
线程执行之前,我们必须为它准备好上下文,也就是初始上下文,djyos中,所有线程都是由函数__djy_vm_engine发动的,该函数有一个参数,即线程所属事件的处理函数指针,根据aapcs规定,该参数保存在R0中。因此,初始上下文中只有R0和PC是有效的,xpsr应该给一个合法的初始值外,其他寄存器可以用任意值,所以,初始上下文设置为:
R0:线程所属事件的处理函数指针。
PC:__djy_vm_engine函数的地址
xpsr:0x01000000,thumb状态,清掉其他所有标志。
其他寄存器:任意。
__asm_reset_thread函数完成线程初始上下文设置,意为复位线程到初始状态。函数本身注释已经很明白,就不再解释了。
;函数原型:void * __asm_reset_thread(void (*thread_routine)(struct event_script *),
; struct thread_vm *vm);
;-----------------------------------------------------------------------------
__asm_reset_thread PROC
EXPORT __asm_reset_thread
ldr r2,[r1,#4] ;取虚拟机栈顶指针
mov r4,#0x01000000 ;xpsr的初始值
ldr r3,=__djy_vm_engine ;取虚拟机引擎指针
stmfd r2!,{r3,r4} ;存pc,xpsr
sub r2,r2,#14*4 ;后退14个寄存器,初始状态r0-r12中,除r0外均无意义,
;__vm_engine函数不返回,lr也无意义
;存在r0中的thread_routine是__vm_engine的参数,切换上下文时,thread_routine将
;恢复到r0中,根据调用约定,r0的值就是__vm_engine函数的参数。
str r0,[r2,#8*4] ;保存 thread_routine指针到r0的位置.
str r2,[r1] ;保存vm的当前栈指针到vm->stack中
bx lr
ENDP
2、上下文切换
上下文切换即中止正在处理的事件(线程),转而处理另一个事件,在下列情形下,将引发上下文切换:
a、正在执行的线程因故被阻塞,比如请求使用设备,但该设备正忙。
b、有高优先级的线程就绪,比如释放了信号量,而使正在等待信号量的高优先级事件就绪。
上下文切换要完成两个操作:保存被中止的线程的上下文;恢复待切入的线程的上下文。在cm3中,上下文切换函数如下:
;函数原型: void __asm_switch_context(struct thread_vm *new_vm,struct thread_vm *old_vm);
;-----------------------------------------------------------------------------
__asm_switch_context PROC
EXPORT __asm_switch_context
mrs r4,xpsr
orr r4,#0x01000000 ;xpsr的T标志读不出来,得手工置位。
push {r4} ;保存xpsr
push {lr} ;保存PC,从其他线程切换回来时,相当于原线程调用
;本函数返回,故用lr替代pc
push {r0-r3,r12,lr} ;保存r0-r3,r12,lr
push {r4-r11}
str sp,[r1] ;保存旧上下文栈指针到old_vm->stack
ldr sp,[r0] ;取得新上下文指针
bl int_restore_asyn_signal ;对应done函数开头的 int_save_asyn_signal 调用
svc 0
ENDP
__asm_switch_context函数完成的只是保存上下文,而新线程上下文的切入是在0号svc调用中完成的,0号svc调用的代码如下:
add r0,#32 ;R0保存的是psp指针值
ldmfd r0!,{r4-r11} ;手工弹出r4-r11
msr psp,r0 ;psp指向待切入的上下文
bx lr ;返回,实际弹出的将是带切入上下文
在线程栈中,最底下的8个字是执行svc 0时自动压栈的,我们并不需要,因此第一句直接加32,跳过这8个字。此时,r0指向的是待切入的线程的上下文了。
随后是手工弹出r4-r11,而r0-r3、r12、lr、pc、xpsr则是从svc返回时走法律程序自动恢复,因为此时psp已经指向待切入的线程的上下文,故svc将直接返回到新线程,至此,上下文切换完成。
这跟arm7版本不一样,在arm7版本中,是直接手工恢复新线程的上下文,具体可参阅djyos for 44b0的代码。那为什么cm3版本需要这么严格的程序,动用svc才能实现切换呢?问题就出在ICI位上,cm3为了保证中断响应速度,对需要很长执行时间的多寄存器加载指令ldm和多寄存器存储指令stm以及IT指令,可在执行过程中中断,利用ICI为保存执行进度,中断恢复时从被中断处继续执行该指令。然而ICI位是只读的,因此,手工恢复寄存器是不可能恢复这些位的,只有从真正的异常返回才能根据ICI位的状态继续被中断的指令执行。
有人要问了,上下文切换不是通过调用__asm_switch_context函数实现的吗?调用该函数时,不可能正在执行ldm或stm或IT指令啊?是的,没错,但是,保存上下文除了主动调用上下文切换函数外,还可能是中断。如果在中断服务例程中使得高优先级的线程就绪,中断返回将直接返回到高优先级线程,被中断打断的低优先级线程的上下文就被保留了,直到该线程变成最高优先级线程才被切回。中断在任何时候都有可能发生,当然也可能在正在执行stm、ldm或IT指令时发生,所以,中断保存的上下文,必须模拟异常返回才能确保恢复执行被中断的半截子指令。
3、启动多事件调度
在初始化完成后,切入第一个线程,标志着多事件调度的开始。cm3版本中,初始化程序工作在handler状态,使用msp作为栈指针,而事件在特权状态的thread模式处理,使用psp为栈指针。因此,启动多事件调度要完成以下工作:
a、 切换到特权模式的thread状态。
b、 切入第一个需要处理的事件的线程的上下文。
c、 因初始化函数的任务已经终结,无需保存其上下文。
下面是
;函数原型: void __asm_start_thread(struct thread_vm *new_vm);
;-----------------------------------------------------------------------------
__asm_start_thread PROC
EXPORT __asm_start_thread
mov r1,#2
msr control,r1 ;切换到特权线程模式,栈指针用psp
ldr sp,[r0] ;取得新上下文指针
bl int_restore_asyn_signal ;对应done函数开头的 int_save_asyn_signal 调用
svc 0 ;切入新上下文
ENDP
4、切入线程
__asm_turnto_context函数不保存当前线程的上下文,直接把新线程的上下文恢复到cpu中。当一条事件处理完毕,该事件的线程可能被销毁,这种情况下,就无需保存当前线程的上下文,直接切入新线程的上下文即可。__asm_turnto_context函数只做一件事:调用svc 0恢复新线程上下文:
;函数原型: void __asm_turnto_context(struct thread_vm *new_vm);
;-----------------------------------------------------------------------------
__asm_turnto_context PROC
EXPORT __asm_turnto_context
ldr sp,[r0] ;取得新上下文指针
bl int_restore_asyn_signal ;对应done函数开头的 int_save_asyn_signal 调用
svc 0 ;切入新上下文
ENDP
4、复位旧线程,切入新线程
当一条事件处理完成,如果系统调度发现该事件的线程应该予以保留,就调用__asm_reset_switch复位该线程,然后切入新的最高优先级的就绪线程。__asm_reset_switch函数是__asm_reset_thread和__asm_turnto_context的结合体。代码如下:
;函数原型:void __asm_reset_switch(void (*thread_routine)(struct event_script *),
; struct thread_vm *new_vm,struct thread_vm *old_vm);
;-----------------------------------------------------------------------------
__asm_reset_switch PROC
EXPORT __asm_reset_switch
ldr sp,[r2,#4] ;取老虚拟机栈顶指针
mov r12,#0x01000000 ;xpsr的初始值
ldr r11,=__djy_vm_engine ;取虚拟机引擎指针
push {r11,r12} ;存pc,xpsr
sub sp,sp,#14*4 ;后退14个寄存器,初始状态r0-r12中,除r0外均无意义,
;__vm_engine函数不返回,lr也无意义
;至此,完成老虚拟机复位
str r0,[sp,#8*4] ;保存 thread_routine指针.至此,完成老线程重置
str sp,[r2] ;保存当前栈指针到old_vm->stack中
ldr sp,[r1] ;取得新上下文指针
bl int_restore_asyn_signal ;对应done函数开头的 int_save_asyn_signal 调用
svc 0 ;切入新上下文
ENDP
5、中断服务例程中的上下文切换
最后一级异步信号(包括systick,由于djyos for cm3把所有异步信号优先级设为相同,故不会嵌套)返回前,即将返回线程模式时,系统将检查在中断服务例程的执行过程中是否有更高优先级的事件(线程)就绪。如果有,则调用__asm_switch_context_int函数执行上下文切换,该函数完成2个操作:
a、把当前PSP保存在被ISR中断的线程虚拟机的线程控制块中。
b、从高优先级的事件的线程控制块中取出SP,放到PSP寄存器中。
函数代码如下:
;函数原型: void __asm_switch_context_int(struct thread_vm *new_vm,struct thread_vm *old_vm);
;-----------------------------------------------------------------------------
__asm_switch_context_int PROC
EXPORT __asm_switch_context_int
mrs r2,psp ;取被中断线程的psp
str r2,[r1] ;把psp写入虚拟机数据结构中
ldr r2,[r0] ;取待切入线程的psp
msr psp,r2 ;待切入线程的当前栈指针写入psp,中断返回恢复上下文将以此为准
bx lr
ENDP
6、一点小经验
cm3中从异常返回必须用bx lr,不能用mov pc,lr,否则会发生存储器管理fault(取址违例)
读函数名得到奇数地址。
中断压栈的PC是偶数地址。
中断返回时,无论栈中的PC值是奇数还是偶数,都能正确返回。 |
阿莫论坛20周年了!感谢大家的支持与爱护!!
曾经有一段真挚的爱情摆在我的面前,我没有珍惜,现在想起来,还好我没有珍惜……
|