鉴于上一篇文章的经历,本次分析先从源码做起。看看是不是有不一样的学习效果...仍然是按照韦东山老师的《嵌入式linux应用开发完全手册》来学习...
一、系统时钟和定时器
本次实验主要涉及四个方面:
1、设置启动MPLL
2、根据HCLK设置存储控制器
3、初始化定时器0
4、定时器中断
Makefile文件是不可缺少的一部分:
objs := head.o init.o interrupt.o main.o timer.bin: $(objs) arm-linux-ld -Ttimer.lds -o timer_elf $^ arm-linux-objcopy -O binary -S timer_elf $@ arm-linux-objdump -D -m arm timer_elf > timer.dis %.o:%.c arm-linux-gcc -Wall -O2 -c -o $@ $< %.o:%.S arm-linux-gcc -Wall -O2 -c -o $@ $< clean: rm -f timer.bin timer_elf timer.dis *.o
先来看head.S文件
@****************************************************************************** @ File:head.S @ 功能:初始化,设置中断模式、系统模式的栈,设置好中断处理函数 @****************************************************************************** .extern main .text .global _start _start: @****************************************************************************** @ 中断向量,本程序中,除Reset和HandleIRQ外,其它异常都没有使用 @****************************************************************************** b Reset @ 0x04: 未定义指令中止模式的向量地址 HandleUndef: b HandleUndef @ 0x08: 管理模式的向量地址,通过SWI指令进入此模式 HandleSWI: b HandleSWI @ 0x0c: 指令预取终止导致的异常的向量地址 HandlePrefetchAbort: b HandlePrefetchAbort @ 0x10: 数据访问终止导致的异常的向量地址 HandleDataAbort: b HandleDataAbort @ 0x14: 保留 HandleNotUsed: b HandleNotUsed @ 0x18: 中断模式的向量地址 b HandleIRQ @ 0x1c: 快中断模式的向量地址 HandleFIQ: b HandleFIQ Reset: ldr sp, =4096 @ 设置栈指针,以下都是C函数,调用前需要设好栈 bl disable_watch_dog @ 关闭WATCHDOG,否则CPU会不断重启 bl clock_init @ 设置MPLL,改变FCLK、HCLK、PCLK bl memsetup @ 设置存储控制器以使用SDRAM bl copy_steppingstone_to_sdram @ 复制代码到SDRAM中 ldr pc, =on_sdram @ 跳到SDRAM中继续执行 on_sdram: msr cpsr_c, #0xd2 @ 进入中断模式 ldr sp, =4096 @ 设置中断模式栈指针 msr cpsr_c, #0xdf @ 进入系统模式 ldr sp, =0x34000000 @ 设置系统模式栈指针, bl init_led @ 初始化LED的GPIO管脚 bl timer0_init @ 初始化定时器0 bl init_irq @ 调用中断初始化函数,在init.c中 msr cpsr_c, #0x5f @ 设置I-bit=0,开IRQ中断 ldr lr, =halt_loop @ 设置返回地址 ldr pc, =main @ 调用main函数 halt_loop: b halt_loop HandleIRQ: sub lr, lr, #4 @ 计算返回地址 stmdb sp!, { r0-r12,lr } @ 保存使用到的寄存器 @ 注意,此时的sp是中断模式的sp @ 初始值是上面设置的4096 ldr lr, =int_return @ 设置调用ISR即EINT_Handle函数后的返回地址 ldr pc, =Timer0_Handle @ 调用中断服务函数,在interrupt.c中 int_return: ldmia sp!, { r0-r12,pc }^ @ 中断返回, ^表示将spsr的值复制到cpsr
该部分上一篇文章已经介绍过了。再来看设置/启动MPLL
/* * init.c: 进行一些初始化 */ #include "s3c24xx.h" void disable_watch_dog(void); void clock_init(void); void memsetup(void); void copy_steppingstone_to_sdram(void); /* * 关闭WATCHDOG,否则CPU会不断重启 */ void disable_watch_dog(void) { WTCON = 0; // 关闭WATCHDOG很简单,往这个寄存器写0即可 } #define S3C2410_MPLL_200MHZ ((0x5c<<12)|(0x04<<4)|(0x00)) #define S3C2440_MPLL_200MHZ ((0x5c<<12)|(0x01<<4)|(0x02)) /* * 对于MPLLCON寄存器,[19:12]为MDIV,[9:4]为PDIV,[1:0]为SDIV * 有如下计算公式: * S3C2410: MPLL(FCLK) = (m * Fin)/(p * 2^s) * S3C2440: MPLL(FCLK) = (2 * m * Fin)/(p * 2^s) * 其中: m = MDIV + 8, p = PDIV + 2, s = SDIV * 对于本开发板,Fin = 12MHz * 设置CLKDIVN,令分频比为:FCLK:HCLK:PCLK=1:2:4, * FCLK=200MHz,HCLK=100MHz,PCLK=50MHz */ void clock_init(void) { // LOCKTIME = 0x00ffffff; // 使用默认值即可 CLKDIVN = 0x03; // FCLK:HCLK:PCLK=1:2:4, HDIVN=1,PDIVN=1 /* 如果HDIVN非0,CPU的总线模式应该从“fast bus mode”变为“asynchronous bus mode” */ __asm__( "mrc p15, 0, r1, c1, c0, 0\n" /* 读出控制寄存器 */ "orr r1, r1, #0xc0000000\n" /* 设置为“asynchronous bus mode” */ "mcr p15, 0, r1, c1, c0, 0\n" /* 写入控制寄存器 */ ); /* 判断是S3C2410还是S3C2440 */ if ((GSTATUS1 == 0x32410000) || (GSTATUS1 == 0x32410002)) { MPLLCON = S3C2410_MPLL_200MHZ; /* 现在,FCLK=200MHz,HCLK=100MHz,PCLK=50MHz */ } else { MPLLCON = S3C2440_MPLL_200MHZ; /* 现在,FCLK=200MHz,HCLK=100MHz,PCLK=50MHz */ } }
首先需要明白各时钟的定义:
1、FCLK
用于CPU核;
2、HCLK
用于AHB总线上的设备,如CPU核、存储器控制器、中断控制器、LCD控制器、DMA、USB主机模块等;
3、PCLK
用于APB总线上的设备,如WATCHDOG、IIS、IIC、PWM定时器、MMC接口、ADC、UART、GPIO、RTC、SPI等;
进入时钟初始化后出现:
// LOCKTIME = 0x00ffffff; // 使用默认值即可
为什么要使用LOCKTIME寄存器呢?
MPLL启动后需要等待一段时间(Lock Time),使得其输出稳定。位[31:16]用于UPLL,位[15:0]用于MPLL。使用确省值0x00ffffff即可。
由上图可知,MPLL的锁定时间必须大于300us,那么这个时间是如何计算的呢?首先需要明白,刚设置好PLL时,系统认为这时PLL还没稳定,所以这时不用PLL的时钟,而用外部晶振(12MHZ)做时钟,将PLL锁住,过了LOCKTIME后认为PLL已经稳定了,才使用PLL给系统提供时钟。那么锁定时间就是:LOCKTIME=(1 / 12 MHZ ) * N,N为U_TIME或者M_TIME,将N设置为4096(0xfff)>300us,即使用默认值即可。
既然谈到了MPLL的启动,那么我们就先来说一下MPLL的启动过程:
根据上图分析:
1、上电几毫秒后(power由低变高),晶振(OSC)输出稳定,此时FCLK=晶振频率,nRESET信号恢复高电平后,CPU开始执行指令。
2、我们可以在程序开头启动MPLL,在设置MPLL的几个寄存器后,需要等待一段时间(Lock Time),MPLL的输出才稳定。在这段时间(Lock Time)内,FCLK停振,CPU停止工作。Lock Time的长短由寄存器LOCKTIME设定。
3、Lock Time之后,MPLL输出正常,CPU工作在新的FCLK下。
总而言之,FCLK,在CPU上电后,晶振开始正常工作,此时FCLK=晶振频率,注意此时不存在MPLL,经过PLL电路后,得到MPLL,UPLL。此时FCLK=MPLL。
如何设置MPLL呢?看这句宏定义:
#define S3C2410_MPLL_200MHZ ((0x5c<<12)|(0x04<<4)|(0x00)) #define S3C2440_MPLL_200MHZ ((0x5c<<12)|(0x01<<4)|(0x02))
后面的((0x5c<<12)|(0x01<<4)|(0x02))有什么含义的?根据该语句 MPLLCON = S3C2440_MPLL_200MHZ; /* 现在,FCLK=200MHz,HCLK=100MHz,PCLK=50MHz */以及数据手册可知该语句是用来设定MPLLCON的:
Mpll=( 2 * ( 0x5c + 8 ) * 12MHZ) / ( ( 0X01 + 2 ) * 2 ^ 2 ) = ( 2 * ( 100 * 12 MHZ ) ) / 12 = 200MHZ
突然间想到,应该是先由要求的系统频率如何200MHZ再来设定((0x5c<<12)|(0x01<<4)|(0x02)),那么它是怎么设定的呢?
在网上找了很长时间的资料也没有找到满意的答案...下面是有关该问题涉及到的内容。
刚看到这张图的上面写着“It is not easy to find a proper PLL value, So,we recommend referring to the following value recommendation”
Oh!My GOD! 老婆又要批评我了。
mpll_val = (92<<12)|(1<<4)|(1);
ChangeMPllValue((mpll_val>>12)&0xff, (mpll_val>>4)&0x3f, mpll_val&3);
void ChangeMPllValue(int mdiv,int pdiv,int sdiv)
{
rMPLLCON = (mdiv<<12) | (pdiv<<4) | sdiv;
}
这个仍然是先由((0x5c<<12)|(0x01<<4)|(0x02))再算得的频率。
接下来看:CLKDIVN = 0x03; // FCLK:HCLK:PCLK=1:2:4, HDIVN=1,PDIVN=1
CLKDIVN为时钟分频控制寄存器,用于设置FCLK,HCLK,PCLK三者间的比例。由FCLK和其设定的比例来计算HCLK,PCLK。
通过设置CLKDIVN的相关位可以设定分频比例,不过其中也涉及到了CAMDIVN摄像头时钟分频控制寄存器,但在此程序中并没有涉及到。
而关于
/* 如果HDIVN非0,CPU的总线模式应该从“fast bus mode”变为“asynchronous bus mode” */ __asm__( "mrc p15, 0, r1, c1, c0, 0\n" /* 读出控制寄存器 */ "orr r1, r1, #0xc0000000\n" /* 设置为“asynchronous bus mode” */ "mcr p15, 0, r1, c1, c0, 0\n" /* 写入控制寄存器 */ );这句的设置可以从下面找到答案:
我们在CLKDIVN中将分频比例设置为FCLK:HCLK:PCLK为1:2:4,其中HDIVN为01并不为1。
由此可见NOTES是非常重要的,之前法拉第公司的张哥给我提到过的...怀念张哥的日子...
至此MPLL的设置和启动全部完成。下面是存储控制器的设置,此代码也是在init.c内容中的。
/* * 设置存储控制器以使用SDRAM */ void memsetup(void) { volatile unsigned long *p = (volatile unsigned long *)MEM_CTL_BASE; /* 这个函数之所以这样赋值,而不是像前面的实验(比如mmu实验)那样将配置值 * 写在数组中,是因为要生成”位置无关的代码”,使得这个函数可以在被复制到 * SDRAM之前就可以在steppingstone中运行 */ /* 存储控制器13个寄存器的值 */ p[0] = 0x22011110; //BWSCON p[1] = 0x00000700; //BANKCON0 p[2] = 0x00000700; //BANKCON1 p[3] = 0x00000700; //BANKCON2 p[4] = 0x00000700; //BANKCON3 p[5] = 0x00000700; //BANKCON4 p[6] = 0x00000700; //BANKCON5 p[7] = 0x00018005; //BANKCON6 p[8] = 0x00018005; //BANKCON7 /* REFRESH, * HCLK=12MHz: 0x008C07A3, * HCLK=100MHz: 0x008C04F4 */ p[9] = 0x008C04F4; p[10] = 0x000000B1; //BANKSIZE p[11] = 0x00000030; //MRSRB6 p[12] = 0x00000030; //MRSRB7 } void copy_steppingstone_to_sdram(void) { unsigned int *pdwSrc = (unsigned int *)0; unsigned int *pdwDest = (unsigned int *)0x30000000; while (pdwSrc < (unsigned int *)4096) { *pdwDest = *pdwSrc; pdwDest++; pdwSrc++; } }在书中韦老师提到:在连接脚本timer.lds中,全部代码的起始运行地址都被设为0x30000000,但是在执行memsetup函数时,代码还在内部SRAM中,为了能够在SRAM中运行这个函数,它应该是“位置无关”的,上面的“手工赋值”可以达到这个目的。
timer.lds源码:
SECTIONS { . = 0x30000000; .text : { *(.text) } .rodata ALIGN(4) : {*(.rodata)} .data ALIGN(4) : { *(.data) } .bss ALIGN(4) : { *(.bss) *(COMMON) } }上面提到的理解不动...在此做记录...下面对定时器进行初始化:
/* *Timer input clock Frequency = PCLK / ( PRESCALER VALUE + 1 ) / ( divider value ) *( prescaler value ) = 0 ~ 255; *( divider value ) = 2,4,8,16 *该试验的Timer0的时钟频率为100MHZ / ( 99 + 1 ) 16 = 62500 *设置TIMER0 0.5s触发一次 */ void timer0_init( void ) { TCFG0 = 99; //预分频器0的值 = 99 TCFG1 = 0X03; //选择16分频 TCNTB0 = 31250; //0.5S触发一次中断 TCON |= ( 1 << 1 ); //手动更新 TCON = 0X09; //自动加载,清除“手动更新”位,启动定时器0 }
关于定时器:
S3C2440有5个16位定时器,定时器0、1、2和3有PWM功能(因此这4个定时器也被称为PWM定时器),都有一个输出引脚,定时器4是一个内部定时器,无外部输出引脚。
定时器的时钟源是PCLK,然后通过内部的两级分频器分频得到定时器工作所需要的频率。其中,定时器0、1公用一个8位的第一级预分频器prescaler 0,定时器2、3、4公用另一个8位的第一级预分频器prescaler 1;每个定时器都有一个与之对应的第二级分频器clock divider如下图所示
虽然定时器较多,但工作原理都是相同的,只需要理解一个定时器的工作原理即可。对于某一个定时器,其内部结构原理图如图二所示。缓存寄存器TCMPBn和TCNTBn用于缓存定时器n的比较值和初始值;TCON用于控制定时器的开启与关闭;通过读取寄存器TCNTOn得到定时器当前计数值。
使用定时器就需要先了解定时器的工作原理:
定时器工作原理概述:
①首先,将定时器的比较值和初始值装入寄存器TCMPBn和TCNTBn中
②然后,设置定时器控制寄存器TCON,启动定时器。此时,TCMPBn和TCNTBn中的值会加载到寄存器TCMPn和TCNTn中。
③此时,定时器会减1计数,即TCNTn进行减1计数,当TCMPn=TCNTn时,TOUTn引脚输出相反。
定时器初始化
① 定时器时钟频率(比如定时器时钟频率为50,则1秒钟计数寄存器减去50;为100,则1秒钟计数寄存器减去100);定时器的时钟频率=PCLK/(prescaler+1)/(divider value)
prescaler value=0~255 (它的值由TCFG0寄存器设置,如下图)
divider value=2,4,8,16 (它的值由TCFG1寄存器设置,如下图)
② 设置定时器计数值(比如计数初值为100,而定时器时钟频率为50,则两秒后会产生中断,比如引脚输出相反电平);TCMPBn和TCNTBn寄存器存放的是设定的计数比较值,直接对其赋值即可。
例2:PCLK为50MHz,请设置适当的分频系数,使定时器0的输入时钟为62.5kHz。
答:已知PCLK为50MHz,则50MHz/62.5kHz=800,即需要对PCLK进行800分频。所以,使第一级分频器的分频系数为100,第2级的分频系数为8即可满足要求。最后,只需要将分频系数写入定时器控制寄存器中相应的位即可,代码如下:
1 rTCFG0&=~(0xFF);将TCFG0的低8位清零
2 rTCFG0|=99;因为分频系数=prescaler+1,即prescaler+1=100,所以,prescaler value=99
3 rTCFG1&=~(0xF);将TCFG1的低4位清零
4 rTCFG1|=0x02;将TCFG1的低4位赋值为0x02,即选择8分频输出
③ 设置中断处理函数
init_irq.c,使能中断
/* *定时器0中断使能 */ void init_irq( void ) { /*定时器0中断使能*/ INTMSK &= ( ~ ( 1 << 10 ) ); }
#include "s3c24xx.h" void Timer0_Handle(void) { /* * 每次中断令4个LED改变状态 */ if(INTOFFSET == 10) { GPBDAT = ~(GPBDAT & (0xf << 5)); } //清中断 SRCPND = 1 << INTOFFSET; INTPND = INTPND; }
在中断处理函数中用到了LED,因此还需要在init.文件中对LED进行初始化:
/* * LED1-4对应GPB5、GPB6、GPB7、GPB8 */ #define GPB5_out (1<<(5*2)) // LED1 #define GPB6_out (1<<(6*2)) // LED2 #define GPB7_out (1<<(7*2)) // LED3 #define GPB8_out (1<<(8*2)) // LED4 /* * K1-K4对应GPG11、GPG3、GPF2、GPF3 */ #define GPG11_eint (2<<(11*2)) // K1,EINT19 #define GPG3_eint (2<<(3*2)) // K2,EINT11 #define GPF3_eint (2<<(3*2)) // K3,EINT3 #define GPF2_eint (2<<(2*2)) // K4,EINT2 void init_led(void) { GPBCON = GPB5_out | GPB6_out | GPB7_out | GPB8_out ; }
main.c文件中,我们仍然是让其不断循环,待有中断发生时响应中断。
int main(void) { while(1); return 0; }参考:
http://www.liweifan.com/2011/12/16/embedded-system-s3c2440-clock/
http://bbs.21ic.com/icview-94316-1-1.html
http://blog.sina.com.cn/s/blog_b90c3cdf0101fr0n.html