系统时钟和定时器-学习笔记

时间:2022-05-27 21:03:08

鉴于上一篇文章的经历,本次分析先从源码做起。看看是不是有不一样的学习效果...仍然是按照韦东山老师的《嵌入式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的:

系统时钟和定时器-学习笔记

MPLLCON的配置是用来确定FCLK频率的。根据宏定义可知将S3C2440的FCLK设置为200MHZ,该频率是怎么计算得到的呢?

系统时钟和定时器-学习笔记

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 ) );
 	}

系统时钟和定时器-学习笔记

中断处理函数interrupt.c

#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