Rt-thread针对rx62N的移植
作者:xuzhenglim
RT-Thread RTOS是一款来自中国的开源实时操作系统,由国内一些专业开发人员开发、维护。它不仅仅是一款高效、稳定的实时核心,也是一套面向嵌入式系统的软件平台,覆盖了全抢占的实时操作系统内核,小巧而与底层具体实现无关的文件系统,轻型的TCP/IP协议栈以及轻型的多窗口多线程图形用户界面(http://www.rt-thread.org/ )。
当然,这个只是它官网上的介绍,个人来说,对它还是有些感情的,并不是说我参与它的开发,我可没那个水平。而是它是我常玩用的两个os中的一个(另一个是日本的iton/topper asp),这个os最大的优点并不是高效、实时,而是它齐全的功能,文件系统、tcp/ip、图形界面(其实这个图形界面做得不够好,速度并不快,但是好在有不错的线程管理,与窗口,能自由绘画,我画出来的界面是很不错的,不过没有相机拍照, 不然也是要上传的)。
RX62N是瑞萨公司的一个32芯片,对于它嘛,我唯一的感觉就是狗血。非常的狗血,资料,有,例程,也有,全在它官网,全英文,只此一家,绝无分店。栈嘛,也有,还是两个,一个用户栈,一个中断栈,汇编嘛,也有,没有例程的,跟arm差别很大的。网上资料查询嘛,也有:价格,货源。。。。。OS嘛,还是有的,封装好库的UCOS,没封装但是找不到工程的free_rtos.(移植这个,我唯一能参考的就是free_rtos与rt_thread的cm3了).
废话就不说了,谁都有血泪史,不过像我这么悲催的找什么资料就没什么资料的人来说,只能说是习惯了。靠自己吧,好在我两年的自学经验不是盖的,虽然第一次移植OS(以前都是别人做好的T_T)。
现在就来说移植吧。
移植一个OS,最重要的是底层,上下文切换,这个不用说的。
一个任务能不能正常启动,其中很大的原因在于你的上下文切换有没有做好,要做好上下文切换,最重要的就是理清楚芯片在处理中断的时候到底做了什么工作,压了哪些寄存器,如何实现pc跳转等。这个是必须的。当然,一个星期想移植好一个完全陌生的芯片到一个没看过底层的os,是有些难度的,但是不能用不会当个理由。
好在瑞萨有份中文的硬件手册与英文软件手册。根据手册上介绍,RX62N的栈有两个,一个是中断栈,一个是用户栈,具体用哪个栈是靠PSW寄存器的U位(实际调试中发现用户栈USP在开机时启一直是0,中断栈ISP不为0,PSW的U位是0,也就是说这时侯不能用用户栈的,当然,你能用条setPSW U,随时置位U,让芯片转向用户栈,当你进入函数然后退出来或者干啥的时候,记得停下来看看是不是程序卡在brk中断里了。),在中断的时候,RX62N会先保存PSW寄存器,再保存PC,然后将PSW寄存器的PM位(管理模式位,这个位主要是设置程序管理权限的,当它为0时,为管理模式,这时候能使用特权指令而不发生错误,当它为1时,是户模式,这时候特权指令是不能运行的,不然直接卡brk中断,这里很重要,等下你就会知道了)置0(g管理模式),U位置0(使用中断栈ISP),I位也指令0(关中断),而中断结束后需要将pc与PSW弹出,弹出PSW时芯片的运行模式是有PSW的PM位控制的,也就是说这个位我们不能直接改,但是可以通过中断弹间接修改。
既然它的中断模式是只压两个寄存器,使用中断栈指针,禁止中断,进入管理模式,那么我们在上下文切换的时候就需要把其它的寄存器保存起来。需要保存的寄存器有R1-R15,ACC累加器,FPSW(浮点状态寄存器)。
嗯,这下明了,那么一个任务切换就大概包括这几个寄存器的保存与下一个任务的弹出。
任务保存简单,可以将原来弹的两个寄存器PSW各pc移动到要保存的任务栈的相对位置,然后切换成用户栈,让用户栈指向任务栈,然后执行push放寄存器到栈就好了,但是如何弹出栈呢。如何让弹出的任务运行呢。那就需要任务的栈的结构和中断的推栈相匹配。
直白点说,中断入栈是保存PSW和pc,用的是中断栈,而我们的任务栈是放在用户栈USP的,而rx62n的一个特点是用户栈与中断栈可以随时切换使用,只是一条指令而已,那么只要把用户栈的指针指向任务栈,弹出必要寄存器后,剩下的两内容是PC与PSW,那么任务就能正常启动了,当然,前提是PSW与PC里面的内容是有效的, 不然,咱们又得跟BRK见面了。
于是我们的任务栈结构如下:
PSW(程序状态字)
PC(指令地址)
R1-R15(15,个通用寄存器,R15是最先弹的)
FPSW(浮点状态字)
ACC(累加器)
SP(栈指针,这个不是保存在栈里面的,是保存在任务指针那的)
于是一个任务的栈初始化应该如下:
rt_uint8_t *rt_hw_stack_init(void *tentry, void *parameter,
rt_uint8_t *stack_addr, void *texit)
{
unsigned long *stk;
stk = (unsigned long *)stack_addr;
*(--stk) = (unsigned long)0x00030000; /* PSW */
*(--stk) = (unsigned long)tentry; /* 程序运行到的地方*/
*(--stk) = 0xffffffff;/*r15*/
*(--stk) = 0xeeeeeeee;/*r14*/
*(--stk) = 0xdddddddd;/*r13*/
*(--stk) = 0xcccccccc;/*r12*/
*(--stk) = 0xbbbbbbbb;/*r11*/
*(--stk) = 0xaaaaaaaa;/*r10*/
*(--stk) = 0x99999999;/*r9*/
*(--stk) = 0x88888888;/*r8*/
*(--stk) = 0x77777777;/*r7*/
*(--stk) = 0x66666666;/*r6*/
*(--stk) = 0x55555555;/*r5*/
*(--stk) = 0x44444444;/*r4*/
*(--stk) = 0x33333333;/*r3*/
*(--stk) = 0x22222222;/*r2*/
*(--stk) = (unsigned long )parameter; /* r1 : 参数 */
*(--stk) = 0x00000100; /* fPSW */
*(--stk) = 0x12345678; /* acc */
*(--stk) = 0x12345678; /* acc */
/* return task's current stack address */
return(rt_uint8_t *)stk;
}
知道为什么是--STK吗,因为一般栈都是默认递减栈,push一个,SP会减1,相反的,pop一个,SP会加1。
好吧,从刚才讨论的结构是这样,第一个保存的必须是PSW,接着是PC,然后是通用寄存器。但是你们有没有想过,一个任务是可以直接运行完退出来的。不一定是一个死循环,比如rt_thread的app是专门开了一个线程来初始化其它线程的,它跟普通函数没有两样,只能是运行到底退出,那样的话,执行到最后会是一条RTS,这条指令是函数返回指令,会弹出堆栈的PC,但是如果此时弹出的PC无效呢?恭喜,你又要见到BRK了。
所以显然,我们上面的结构是有问题的,不管动态任务还是静态任务,运行出函数范围,就应该通知内核去释放内存与终止该任务,从任务链表中剔除,该怎么解决这个问题了,我一个晚上睡不着就是想这个问题,这时候我注意到
rt_uint8_t *rt_hw_stack_init(void *tentry, void *parameter,
rt_uint8_t *stack_addr, void *texit)
这个函数有个texit的参数,这个参数起始跟tentry一样是个函数,不过,它是一个任务退出函数。给这个函数的目的很明显,就是当函数退出的时候告诉内核这个函数退出了。需要处理。于是我们需要想法子让任务退出时运行到这个函数,怎么实现?弹堆栈实现pc跳转,那么很明显了,它将是最后一个弹出的内容,因为当一个函数运行到退出的时候,它一般会弹光自己的堆栈,使得堆栈指向它刚进来的那会的状态,所以结合任务切换的过程,texit肯定是在psw的上面。于是将任务栈结构调整成了
rt_uint8_t *rt_hw_stack_init(void *tentry, void *parameter,
rt_uint8_t *stack_addr, void *texit)
{
unsigned long *stk;
stk = (unsigned long *)stack_addr;
/*关于任务的退出,rx62n是跟cm3不一样的,cm3具有LR返回寄存器,
但是在rx62n是没有的,所以当执行完一个任务后(比如说init任务
是一个直接退出的函数,那该怎么办,动态建立的任务应该释放内
存的,根据这个,发现在rts的后面有个弹回pc的动作,也就是说
一个函数返回,它最后弹的栈应该是pc,所以栈底明显第一个字节是
退出函数了*/
*(stk) = (unsigned long)texit; /* 退出任务 */
*(--stk) = (unsigned long)0x00030000; /* PSW */
*(--stk) = (unsigned long)tentry; /* 程序运行到的地方*/
*(--stk) = 0xffffffff;/*r15*/
*(--stk) = 0xeeeeeeee;/*r14*/
*(--stk) = 0xdddddddd;/*r13*/
*(--stk) = 0xcccccccc;/*r12*/
*(--stk) = 0xbbbbbbbb;/*r11*/
*(--stk) = 0xaaaaaaaa;/*r10*/
*(--stk) = 0x99999999;/*r9*/
*(--stk) = 0x88888888;/*r8*/
*(--stk) = 0x77777777;/*r7*/
*(--stk) = 0x66666666;/*r6*/
*(--stk) = 0x55555555;/*r5*/
*(--stk) = 0x44444444;/*r4*/
*(--stk) = 0x33333333;/*r3*/
*(--stk) = 0x22222222;/*r2*/
*(--stk) = (unsigned long )parameter; /* r1 : 参数 */
*(--stk) = 0x00000100; /* fPSW */
*(--stk) = 0x12345678; /* acc */
*(--stk) = 0x12345678; /* acc */
/* return task's current stack address */
return(rt_uint8_t *)stk;
}
接下来是上下文的编写了。
首先,rt-thread的上下文切换是都是发生在中断中的,而且,实际上,在中断中切换堆栈是最安全的。跟别的OS不同的是,rt-trhead有三个任务切换变量:
rt_uint32_t rt_interrupt_from_thread;//前一个任务栈
rt_uint32_t rt_interrupt_to_thread;//后一个任务栈
rt_uint32_t rt_thread_switch_interrupt_flag;//中断任务切换标志,1的时候说明进行任务切换。
void rt_hw_context_switch(rt_uint32_t from, rt_uint32_t to)
{
/* 判断中断标志是否置位,是的话需要保存切换前的线程*/
if (rt_thread_switch_interrupt_flag == 0)
{
rt_thread_switch_interrupt_flag = 1;
rt_interrupt_from_thread = from;
}
rt_interrupt_to_thread = to;
/* 启动软件中断*/
ICU.SWINTR.BIT.SWINT = 1;
}
void rt_hw_context_switch_interrupt(rt_uint32_t from, rt_uint32_t to)
{
/* 判断中断标志是否置位,是的话需要保存切换前的线程*/
if (rt_thread_switch_interrupt_flag == 0)
{
rt_thread_switch_interrupt_flag = 1;
rt_interrupt_from_thread = from;
}
rt_interrupt_to_thread = to;
/* 启动软件中断*/
ICU.SWINTR.BIT.SWINT = 1;
}
上面两个是rt-thread任务切换的预处理,其实就是置位rt_thread_switch_interrupt_flag,
同时传递要切换的任务指针。
void rt_hw_context_switch_to(rt_uint32_t to)
{
rt_interrupt_from_thread = 0;
rt_interrupt_to_thread = to;
rt_thread_switch_interrupt_flag = 1;
//
/* 启动中断*/
_IEN( _ICU_SWINT ) = 1;
/*确定中断已经清除(退出中断)*/
_IR( _ICU_SWINT ) = 0;
_IPR( _ICU_SWINT ) =15;
asm(" SETPSW U");
// while(1);
/*触发软件中断*/
ICU.SWINTR.BIT.SWINT = 1;
while (1);
}
这个是切换第一次任务用的,至于为什么要while(1),是因为启动软件中断要时间的,等待中断而己,呵呵。
接下来是重点了,上下文切换。上下文切换是用c写呢还是用汇编写?答案是可以c里面嵌套汇编,但是不推荐,因为用C编写函数的话,编译器会多弹栈与出栈寄存器,这样对于任务保存结构来说就不那么好掌握了。所以最好不要用C,用汇编比较好。
汇编的代码如下:
#include "cpuconfig.h"
EXTERN _rt_thread_switch_interrupt_flag
EXTERN _rt_interrupt_from_thread
EXTERN _rt_interrupt_to_thread
/*PUBLIC _Interrupt_SWINT*/
PUBLIC ___interrupt_27
RSEG CODE:CODE(4)
; r0 --> swith from thread stack
; r1 --> swith to thread stack
; psr, pc, lr, r12, r3, r2, r1, r0 are pushed into [from] stack
/* 重定义软件中断函数*/
___interrupt_27:
/* 禁止中断的方法其实就是将优先级提到最高*/
/* 重新使能中断*/
SETPSW I
MVTIPL #MAX_SYSCALL_INTERRUPT_PRIORITY
PUSH.L R15
/* 判断rt_thread_switch_interrupt_flag是否为0,如果为0就是说不是切换任务*/
MOV.L #_rt_thread_switch_interrupt_flag, R15
MOV.L [ R15 ], R15
CMP #0, R15
BEQ notask_exit
/* 将rt_thread_switch_interrupt_flag清零*/
MOV.L #_rt_thread_switch_interrupt_flag, R15
MOV.L #0, [ R15 ]
/* 如果原来线程为空就不用保存寄存器*/
MOV.L #_rt_interrupt_from_thread, R15
MOV.L [ R15 ], R15
CMP #0, R15
/*
MOV.L #_rt_interrupt_to_thread, R15
MOV.L [ R15 ], R15
CMP , R15
*/
BEQ need_modify_isp
/*保存几个CPU寄存器*/
/* 读取CPU栈指针*/
MVFC USP, R15
/* 将栈指针移动到保存数据开始的地方*/
SUB #12, R15
MVTC R15, USP
/* 拷贝数据*/
MOV.L [ R0 ], [ R15 ] ;PSW
MOV.L 4[ R0 ], 4[ R15 ];PC
MOV.L 8[ R0 ], 8[ R15 ] ;R15
/*移动栈指针到它的新位置*/
ADD #12, R0
/*切换到用户栈*/
SETPSW U
/* 将寄存器保存到当前的新位置*/
PUSHM R1-R14
MVFC FPSW, R15
PUSH.L R15
MVFACHI R15
PUSH.L R15
MVFACMI R15 ; Middle order word.
SHLL #16, R15 ; Shifted left as it is restored to the low orde r w
PUSH.L R15
/*将栈指针保存到rt_thread_from中*/
MOV.L #_rt_interrupt_from_thread, R15
MOV.L [ R15 ], R15
MOV.L R0, [ R15 ]
BRA swtich_to_thread
need_modify_isp:
/* 读取中断栈的指针*/
MVFC ISP, R15
ADD #12, R15
MVTC R15, ISP
swtich_to_thread:
/* 切换到新任务*/
/* 移动栈指针到新任务栈*/
SETPSW U
MOV.L #_rt_interrupt_to_thread, R15
MOV.L [ R15 ], R15
MOV.L [ R15 ], R0
/* 新任务的数据从栈中提出来*/
POP R15
MVTACLO R15
POP R15
MVTACHI R15
POP R15
MVTC R15, FPSW
POPM R1-R15
BRA pendsv_exit
/*这里是中断栈用的*/
notask_exit:
/* 一开始只弹了这个寄存器*/
POP R15
pendsv_exit:
/* 重新打开中断*/
MVTIPL #KERNEL_INTERRUPT_PRIORITY
RTE
NOP
NOP
END
由于有本人比较不靠谱的中文注释,上面代码我就不讲解了。但是需要提醒的是,中断进来的中断栈,出去的是用户栈,为了不使中断栈溢出,必须做修正,于是才有那么多的BRA.。
定时器的初始化与中断是比较简单的,给出例程了。Rt的定时器中断都是大同小异的。
#if 1
#pragma vector = VECT_CMT0_CMI0
__interrupt void rt_hw_timer_handler( void )
{
/* 使能中断*/
__enable_interrupt();
// CMT.CMSTR1.BIT.STR3 = 0;
/* Enable Interrupt */
//ICU.IR[IR_CMT0_CMI0].BIT.IR = 0;
/* enter interrupt */
rt_interrupt_enter();
/*必须,不然上下文会被破坏*/
rt_tick_increase();
/* leave interrupt */
rt_interrupt_leave();
}
#endif
void rt_hw_systick_init(void)
{
/* Enable compare match timer 0. */
MSTP( CMT0 ) = 0;
/* Interrupt on compare match. */
CMT0.CMCR.BIT.CMIE = 1;
/* Set the compare match value. */
CMT0.CMCOR = ( unsigned short ) (6912);
/* Divide the PCLK by 128. */
CMT0.CMCR.BIT.CKS = 2;
/* Enable the interrupt... */
_IEN( _CMT0_CMI0 ) = 1;
/* ...and set its priority to the application defined kernel priority. */
_IPR( _CMT0_CMI0 ) = 5;
/* Start the timer. */
CMT.CMSTR0.BIT.STR0 = 1;
}
接下来的,是一个中断的使能与禁止。大部分人都会以为使能与禁止中断只是关掉与重开全局中断而已(包括我),但是后来我发现我错了,一个中断不应该被进制,那样任务切换过程可能会造成很多意外。正确的中断禁止与使能应该是在提升中断的等级,让其他中断没法打断需要保存的任务或者运行过程。
但是大用户模式是无法修改中断优先级的,不知道你们注意没有。在上面栈初始化中,我的PSW寄存器内容是0x00030000,别的位先不说,主要是PM= 0,I = 0,U=0,知道这意味着什么吗?Rx62N的os必须运行在管理模式下,不能能是用户模式,不然是无法正常运行的。因为你不能用特权指令去禁止中断,用了,等待你的就是BRK了。。哈哈,下面是我编的使能禁止函数:
rt_base_t rt_hw_interrupt_disable(void)
{
rt_base_t level = get_ipl();
set_ipl(MAX_SYSCALL_INTERRUPT_PRIORITY);
return level;
}
void rt_hw_interrupt_enable(rt_base_t level)
{
set_ipl(level);
}
到这,已经完成了rt-thread的底层移植,可以正常跑了。不过。需要注意的是,中断栈不能太小,因为一开始的几个任务都是在中断栈里面分配空间的,这个可能是我的移植有问题。以后如果有解决方案会提的。
另外。这个属于个人原创(除非你能找到别人移植好的),虽然写的不是很好,也不是什么了不起成就。。请尊重个人成果,张冠李戴的把戏还是省下吧。请不要往ourdev论坛转发,原因很简单,我的问题从来没人解决,最近还删了我ID,既然不让上我也不上了,反正也不是什么好地方,对我没有什么用处的,还是上http://www.openedv.com/ ,看看有什么东西。。
例程会上传,放心,我不要分的,还有400分没用,我也不稀罕。