移植OS汇编是绕不过去的,先从MDK默认提供的s3c2440.s开始,这里我们需要完成以下几个事情:
1.cpu状态和模式
2.设置clock,包括系统时钟,sdram时钟,外设时钟等等
3.设置sdram的时序参数,这个是非常重要的,否则我们的内存就不能使用
4.堆,栈
5.选择初始化外设,IO等
废话不多说先上图,下面是我花了几天时间在TQ2440+Freertos V8.2.3+MDK5.16上移植的成功的,一共创建了4个任务分别以不同的频率闪烁LED,下面记录下需要注意的细节。因为Freertos对三星的处理器支持不太好,有可能是三星是韩国人开的而Freertos主要是日本的一家公司在维护,日韩关系紧张所以才不维护的吧^_^。说正题下面一个一个来说。应为freertos没有对任何的一款三星的处理器的支持,同时也没有任何一款arm9的处理器在mdk工具下移植,所以我这个算是慢慢摸索。
1.首先是处理器的初始化,这里选择了处理器时钟400Mz,Fclk:Hclk:Pclk=1:4:8的设置,具体如下,这些都是s3c2440时钟相关的寄存器,在MDK当中startup.s设置很简单:
CLOCK_SETUP EQU 1
LOCKTIME_Val EQU 0x0FFF0FFF
MPLLCON_Val EQU 0x0007F021
UPLLCON_Val EQU 0x00038022
CLKCON_Val EQU 0x001FFFF0
CLKSLOW_Val EQU 0x00000004
CLKDIVN_Val EQU 0x00000005
CAMDIVN_Val EQU 0x00000000
2.其次是SDRAM设置,s3c2440虽然是有内在的sram但是只有4KB太小不适合跑操作系统,所以我们使用外部sdram,同样设置简单,使用mdk设置向导,主要关注bank6就行,其他的之后再细究:
;----------------------- Memory Controller Definitions -------------------------
MC_BASE EQU 0x48000000 ; Memory Controller Base Address
BWSCON_OFS EQU 0x00 ; Bus Width and Wait Status Ctrl Offset
BANKCON0_OFS EQU 0x04 ; Bank 0 Control Register Offset
BANKCON1_OFS EQU 0x08 ; Bank 1 Control Register Offset
BANKCON2_OFS EQU 0x0C ; Bank 2 Control Register Offset
BANKCON3_OFS EQU 0x10 ; Bank 3 Control Register Offset
BANKCON4_OFS EQU 0x14 ; Bank 4 Control Register Offset
BANKCON5_OFS EQU 0x18 ; Bank 5 Control Register Offset
BANKCON6_OFS EQU 0x1C ; Bank 6 Control Register Offset
BANKCON7_OFS EQU 0x20 ; Bank 7 Control Register Offset
REFRESH_OFS EQU 0x24 ; SDRAM Refresh Control Register Offset
BANKSIZE_OFS EQU 0x28 ; Flexible Bank Size Register Offset
MRSRB6_OFS EQU 0x2C ; Bank 6 Mode Register Offset
MRSRB7_OFS EQU 0x30 ; Bank 7 Mode Register Offset
3.设置timer,这里使用pwm timer0.这个里面一定要注意几个地方:
a.时钟设置一定要仔细检查,计算准确,不然timer就不准
b.中断向量部分,这里选择Preemptive模式(configUSE_PREEMPTION =1)建议使用一个存储在数字段的数组
IntVTAddress[32],有必要的话使用一些mdk编译选
项关键词__align(4)或者类似的关键词来对齐。我之前使用函数指针数组,
然后把中断函数向量放在
数组当中,结果发现当irq发生的时候,在中断分配函数里面从
数组里面取地址的时候,不止为何总是跟实际存储的地址有4个字节的偏差,比
如:我存储在指向函数指针的数组
里面的第0x0a个元素,也就
是
IntVTAddress【
0x0a】
的时候,在irq里面读取到数组内容的时候,逻辑来说应该是数组基地址+4*0xa的位置,可是不知道为何得到的总是会
比这个值要偏移4个字节,几番调
试未果,所以就放弃了,换了
一个纯数组的方法,也就是现在的办法,反正数组里面存储的都是每一个外设的中断向量的跳转地址,用
一个32位整形数组存储和用指向函数指
针的数组并没有本质的差别;至
于为什么用后者不行,暂时还
不知道。
//all 32 int entry address
unsigned int IntVTAddress[32];
static void prvSetupTimerInterrupt( void )
{
uint32_t ulCompareMatch;
IntVTAddress[0x0a]=(unsigned int)vPreemptiveTick; //设置系统tick中断向量
TCFG0=0x000000ff; //Prescaler 255 , timer0 1 tick=81.9188us //设置timer0的工作时钟,分频等
TCFG1=0x00000003; //mux =1/16 ;
ulCompareMatch =(uint32_t)(1000/81.9188);//1221 tick=1ms //设置系统tick最小单位1ms
TCNTB0 =ulCompareMatch; //预装初始值
//TCMPB0= ulCompareMatch;
INTMOD=0x0; //all irq ,not fiq
PRIORITY=0x0; //irq Priority rotate disable
INTSUBMSK=0xffffffff;
INTMSK &=(~(0x1<<10)); //bit10 enable timer0 intttupt
SUBSRCPND=0xffffffff; //clear all pending irq frist
SRCPND=0xffffffff;
INTPND=0xffffffff;
TCON |= (1<<1);
TCON =0x09; //start timer0,should not enable the cpu interrupt bit I or F bit,it will be enabled at portRESTORE_CONTEXT
}
4.中断向量表的查询函数,这个是跟体系结构相关,其他的处理器还没见过这种需要用户自己处理的情况。这个函数相对简单,无非是从
INTOFFSET寄存器读出当前的
中断是哪一个,然后使用中断号码,在上面建
立的中断向量表中查询,然后跳转到里面去执行,我们目前就一个irq也就是timer0.注意的是每个中断
向量是4个字节,一般来说是4字节对齐。从IRQ_Handler跳到中断服务函数之后,系统处理器的状态如下,
通用寄存器的值跟中断之前一样没有被破
坏,中断返回地址被保存在irq模式的LR寄存器当中,PC指向了中断服务函数。
IRQ_Handler PROC
IMPORT IntVTAddress
sub sp,sp,#4 ;reserved for PC
stmfd sp!,{r8-r9} // 函数需要使用r8 ,r9 先保存在irq栈
ldr r9,=INTOFFSET //读中断类型
ldr r9,[r9]
ldr r8,=IntVTAddress //取中断向量表的基地址
add r8,r8,r9,lsl #2 //每个中断向量是4字节
ldr r8,[r8] //读取中断服务函数的入口
str r8,[sp,#8] //存入堆栈
ldmfd sp!,{r8-r9,pc} //把PC弹出来,刚好跳转到中断服务函数里面去
ENDP
5.关于cpu模式,这里我们选择的是sys模式,注意必须是要在进入main之前设置好,否则可能有问题,在startup.s里面设置就好了。发现如果你在进入__main之前设置了usr
态的堆栈,系统在跑__main之后,main之前就会自动把系统设置为usr模式,这个可能是mdk里面的默认状态,所以你只需要把startup里面的设置usr模式栈的部分注释掉就
可以了 。
; Enter User Mode and set its Stack Pointer
; MSR CPSR_c, #Mode_USR
; MOV SP, R0
; SUB SL, SP, #USR_Stack_Size
; Enter User Mode and set its Stack Pointer
; MSR CPSR_c, #Mode_USR
; IF :DEF:__MICROLIB
; EXPORT __initial_sp
; ELSE
; MOV SP, R0
; SUB SL, SP, #USR_Stack_Size
; ENDIF
6.设置好sys和svc模式下的堆栈,在mdk startup向导设置就行了。0x300B貌似够用,当然我们板子上有64MB的内存,你随便设置一个也可以。
7.级的在timer0 isr
vPreemptiveTick 里面清中断标志。
MOV R0,#BIT_TIMER0 ;clear s3c2440 timer0 interrupt
LDR R1, =SRCPND
STR R0, [R1]
MOV R0,#BIT_TIMER0
LDR R1, =INTPND
STR R0, [R1]
8.portRESTORE_CONTEXT,portSAVE_CONTEXT这两个不需要修改。它两的最主要的功能是在timer0 irq到来的时候或者是swi到来的时候,我们通过把当前通用寄存器保存在pxCurrentTCB所指向的tcb里面的堆栈里面,同时根据不同的策略选择下一个需要执行的任务,也就是把pxCurrentTCB指向一个新的或者原来的tcb,然后把tcb里面堆栈里存的数据,回填到通用寄存器里面,最后都使用irq返回的方式,对pc值做一定的调整( SUBS PC, LR, #4)之后,跳到对应的任务,完成任务切换。至于是选哪一个任务,用什么策略来选择,我们都是通过c语言来实现,的真正需要汇编的地方就很少。需要注意的是,这里的这两个函数默认的算法里面,要求cpu初始的时候工作在sys模式,不然你就要修改这里面的汇编,也就是前面说的进入main之前,设置好cpu为sys模式。
9.关于最简单的测试代码,这里选择LED flash,刚好我板子上有4个LED。建立4个任务,同样的优先级,轮流闪烁,分别实现了1s,2s,3s,4s不同的周期的闪烁。代码很简单如下,这里有一个经验就是当你没有合适的调试工具,如jlink的时候,你 可以选择在代码流程当中适当的地方插入代码点亮或者熄灭led灯来做简单的诊断,虽然这个人肉debug方法很残忍,但是除非你很牛能够一次写出毫无错误的代码,不然这个是你唯一的选择。
#define ledNUMBER_OF_LEDS ( 4 )
#define ledFLASH_RATE_BASE ( ( TickType_t ) 1000 )
void vParTestToggleLED( unsigned portBASE_TYPE uxLED )
{
unsigned long ulLED = partstFIRST_IO, ulCurrentState;
if( uxLED < partstNUM_LEDS )
{
/* Rotate to the wanted bit of port 0. Only P10 to P13 have an LED
attached. */
ulLED <<= ( unsigned long ) uxLED;
/* If this bit is already set, clear it, and vice versa. */
ulCurrentState = GPBDAT;
if( ulCurrentState & ulLED )
{
GPBDAT &= (~ulLED)&0x000001e0;//only bit 5,6,7,8 ,other bit set to 0
}
else
{
GPBDAT |= ulLED&0x000001e0;//only bit 5,6,7,8 ,other bit set to 0
}
}
}
先到这里,下面再来看看细节部分,欢迎有对rtos有兴趣的人一起讨论。