系统时钟和定时器

《嵌入式Linux应用完全开发手册》第2篇第10章总结归纳

本章目标

  1. 了解S3C2410/S3C2440的时钟体系结构
  2. 掌握通过设置MPLL改变系统时钟的方法
  3. 掌握在不同频率下设置存储控制器的方法
  4. 掌握PWM定时器的用法
  5. 了解WATCHDOG定时器的用法

时钟体系以及各类时钟部件

S3C2410/S3C2440时钟体系

S3C2410/S3C2440的时钟控制逻辑既可以外接晶振,然后通过内部电路产生时钟源;也可以直接使用外部提供的时钟源,它们通过引脚的设置来选择。时钟控制逻辑给整个芯片提供了3种时钟。FCLK用于CPU核,HCLK用于AHB总线上的设备,比如CPU核、存储控制器、中断控制器、LCD控制器、DMA和USB主机模块等,PCLK用于APB总线上的设备,比如WATCHDOG、IIS、IIC、PWM定时器、MMC接口、ADC、UART、GPIO、RTC、SPI。
AHB(Advanced High performance Bus)总线主要用于高性能模块(CPU、DMA和DSP等)之间的连接;APB(Advanced Peripheral Bus)总线主要用于低带宽的周边外设之间的连接,例如UART,IIC等。
S3C2410核的工作电压为1.8V时,主频可达200MHz;工作电压为2.0V,主频可达266MHz。S3C2440核的工作电压为1.2V时,主频可达300MHz;工作电压为1.3V时,主频可达300MHz。为了降低电磁干扰,降低板间布线的要求,S3C2410/S3C2440外接晶振的频率一般很低,本开发板上的为12MHz,需要通过时钟控制逻辑的PLL提高系统时钟。
S3C2410/S3C2440有两个PLL,MPLL和UPLL。UPLL专用于USB设备,MPLL用于设置FCLK、HCLK、PCLK。它们的设置方式类似。以MPLL为例。
上电时,PLL没启动,FCLK即等于外部输入的时钟,称为Fin。若要提高系统时钟,需要软件来启动PLL。结合下图来介绍PLL的设置过程。
img not found

  1. 上电几毫秒后,晶振(图中的OSC)输出稳定,FCLK=Fin(晶振频率),nRESET信号恢复高电平之后,CPU开始执行指令。
  2. 可以在程序开头启动MPLL,设置MPLL的几个寄存器之后,需要等待一段时间(Lock Time),MPLL的输出才稳定。这段时间(Lock Time)内,FCLK停振,CPU停止工作。Lock Time的长短由寄存器LOCKTIME设定。
  3. Lock Time之后,MPLL的输出才稳定,CPU工作在新的FCLK下。

FCLK、HCLK、PCLK的比例是可以改变的,设置它们三者的比例,启动MPLL只需要设置3个寄存器。

  1. LOCKTIME寄存器(LOCK TIME COUNT),用于设置“Lock Time”的长度。
    MPLL启动后需要等待一段时间(Lock Time),使得其输出稳定。S3C2410中,位[23:12]用于UPLL,位[11:0]用于MPLL。S3C2440中,位[31:16]用于UPLL,位[15:0]用于MPLL。一般而言。使用它的默认值即可,S3C2410中默认值为0x00FFFFFF,S3C2440中的默认值为0xFFFFFFFF。
  2. MPLLCON寄存器(Main PLL Control),用于设置FCLK与Fin的倍数,
    位[19:12]的值称为MDIV,位[9:4]的值称为PDIV,位[1:0]的值称为SDIV。FCLK与Fin的关系有如下计算公式:
    1
    2
    3
    4
    5
    6
    7
    //对于S3C2410
    MPLL(FCLK) = (m*Fin)/(p*2^s)
    其中:m = MDIV + 8,p = PDIV + 2,s = SDIV。

    //对于S3C2440
    MPLL(FCLK) = (2*m*Fin)/(p*2^s)
    其中:m = MDIV + 8,p = PDIV + 2,s = SDIV。
    设置MPLLCON就相当于之前图中说的“首先使用软件来设置PLL”,Lock Time就被自动插入。Lock Time之后,MPLL输出稳定,CPU工作在新的FCLK下。
  3. CLKDIVN寄存器(CLOCK DIVIDER CONTROL),用于设置FCLK、HCLK、PCLK三者的比例。
    对于S3C2410、S3C2440这个寄存器表现稍有不同
    CLKDIVN 说明
    HDIVN1 2 0表示保留,1表示FCLK:HCLK:PCLK=1:4:4,此时HDIVN、PDIVN必须设为0b00
    HDIVN 1 HCLK的分频系数,0-HCLK=FCLK,1-HCLK=FCLK/2
    PDIVN 0 PCLK的分频系数,0-PCLK=HCLK,1-PCLK=HCLK/2

对于S3C2440的一些时钟比例,还需要额外设置一个寄存器CAMDIVN。下图中,HDIVN为CLKDIVN寄存器的位[2:1],PDIVN为位[0];HCLK4_HALF、HCLK3_HALF分别为CAMDIVN寄存器的位[9]、[8]。各种时钟比例对应的寄存器设置如下图所示:
img not found
对于S3C2410,HDIVN是CLKCIVN寄存器的位[1],对于S3C2440,HDIVN是CLKDIVN寄存器的位[2:1]。如果HDIVN非0,CPU的总线模式应该从“fast bus mode”变为“asynchronous bus mode”,这可以通过如下指令来完成。

1
2
3
4
#MMU_SetAsyncBusMode
mrc p15,0,r0,c1,c0,0
orr r0,r0,#R1_nF:OR:R1_iA
mcr p15,0,r0,c1,c0,0

其中的“#R1_nF:OR:R1_iA”等于0xC0000000。如果HDIVN非0时,而CPU的总线模式仍是“fast bus mode”,则CPU的工作频率将自动变为HCLK,而不再是FCLK。

PWM定时器

S3C2410/S3C2440的定时器部件完全一样,共有5个16位的定时器。其中定时器0、1、2、3有PWM(Pulse Width Modulation)功能,即它们都只有一个输出引脚,可以通过定时器来控制引脚周期性的高低电平变化;定时器4没有输出引脚。
定时器部件的时钟源为PCLK,首先通过两个8位的预分频器降低频率:定时器0、1共用第一个预分频器,定时器2、3、4共用第一个预分频器。预分频器的输出将进入第二级分频器。它们输出5种频率的时钟,2、4、8、16分频或者外部时钟TCLK0/TCLK1。每个定时器的工作时钟可以从这5种频率中选择。
两个预分频都可以通过TCFG0寄存器来设置,每个定时器工作在哪个频率下也可以通过TCFG1寄存器来选择。下图形象的说明了定时器的结构:
img not found
上面只是确定了定时器的工作频率,至于定时器如何工作还得了解其内部结构。定时器内部控制逻辑的工作流程如下:
img not found

  1. 程序初始化,设定TCMPBn、TCNTBn这两个寄存器,它们表示定时器n的比较值,初始计数值。
  2. 随之设置TCON寄存器启动定时器n,这时,TCMPBn、TCNTBn的值将被装入其内部寄存器TCMPn、TCNTn中。在定时器n的工作频率下,TCNTn开始减1计数,其值可以通过读取TCNTOn寄存器得知。
  3. 当TCNTn的值等于TCMPn的值时,定时器n的输出管脚TOUTn反转;TCNTn继续减1计数。
  4. 当TCNTn的值到达0时,其输出管脚TOUTn再次反转,并触发定时器n的中断(如果使能了中断的话)。
  5. 当TCNTn的值到达0时,如果在TCON寄存器中将定时器n设为自动加载,则TCMPB0和TCNTB0寄存器的值被自动装入TCMP0和TCNT0寄存器中,下一个计数流程开始。
    定时器n的输出管脚TOUTn初始状态为高电平,以后在TCNTn的值等于TCMPn的值、TCNTn的值时反转。也可以通过TCON寄存器设置其初始电平,这样TOUTn的输出就完全反相了。通过设置TCMPBn、TCNTBn的值可以设置管脚TOUTn输出信号的占空比,这就是所谓的可调制脉冲(PWM),所以这些定时器就可以称为PWM定时器。

定时器的寄存器:

  1. TCFG0寄存器(TIMER CONTFIGURATION)
    位[7:0]、位[15:8]分别用来控制预分频器0、1。它们的值为0-255。经过预分频器出来的时钟频率为PCLK/{prescaler value + 1}。
  2. TCFG1寄存器(TIMER CONTFIGURATION)
    经过预分频器的得到的时钟将被2分频、4分频、8分频和16分频,除这4种频率外,额外的,定时器0、1还可以工作在外接的TCLK0时钟下,定时器2、3、4还可以工作在外接的TCLK1时钟下。
    通过TCFG1寄存器来设置这5个定时器,分别工作于这5个频率的哪一个之下,如下表所示:
    img not found
    这样,定时器n的工作频率或者是外接的TCLK0或TCLK1可通过这个公式进行计算:
    1
    2
    3
    定时器工作频率 = PCLK / {presacaler value + 1} / {divider value}
    {prescaler value} = 0-255
    {divider value} = 2,4,8,16
  3. TCNTBn/TCMPBn寄存器(COUNT BUFFER REGISTER & COMPARE BUFFER REGISTER)
    n为0-4,这四个寄存器都只用到位[15:0],TCNTBn中保存定时器的初始计数值,TCMPBn中保存比较值。它们的值在启动定时器时,被传到定时器内部寄存器TCNTn、TCMPn中。
    没有TCMPB4因为定时器4没有输出引脚。
  4. TCNTOn寄存器(COUNT OBSERVATION)。
    n为0-4,定时器n被启动后,内部寄存器TCNTn在其工作时钟下不断减1计数,可以通过读取TCNTOn寄存器得知其值。
  5. TCON寄存器(TIMER CONTROL)。
    它有以下4个作用:
    1. 第一次启动定时器时“手动”将TCNTBn/TCMPBn寄存器的值装入内部寄存器TCNTn、TCMPn中。
    2. 启动、停止定时器。
    3. 决定在定时器计数到达0时是否自动将TCNTBn/TCMPBn寄存器的值装入内部寄存器TCNTn、TCMPn中。
    4. 决定定时器的管脚TOUTn的输出电平是否反转。
      TCON寄存器位[3:0]、位[11:8]、位[15:12]、位[19:16]、位[22:20]分别用于定时器0-4。除定时器因为没有输出引脚在没有“输出反转”位外,其他位的功能相似,下表以定时器0为例说明这些寄存器:
      功能 位置
      开启/停止 0 0:停止定时器 1:开启定时器
      手动更新 1 0:无用 1:将TCNTBn/TCMPBn寄存器的值装入内部寄存器TCNTn、TCMPn中
      输出反转 2 0:TOUT0不反转 1:TOUT0反转
      自动加载 3 0:不自动加载 1:在定时器计数达到0时,TCNTBn/TCMPBn寄存器的值自动装入内部寄存器TCNTn、TCMPn中

第一次使用定时器时,需要设置“手动更新”位为1以使TCNTBn/TCMPBn寄存器的值装入TCNTn、TCMPn中。下一次如果还要设置这一位,需要先将它清0。

WATCHDOG 定时器

WATCHDOG定时器可以像一般16位定时器一样用于产生周期性的中断,也可以用于发出复位信号以重启失常的系统。它与PWM定时器结构类似。
img not found
同样的,WATCHDOG定时器的8位预分频器将PCLK分频后,将再次分频得到4种频率。16、32、64、128分频。WATCHDOG 定时器可以选择工作于哪种频率之下。WTCNT寄存器按照其工作频率减1计数,当达到0时,可以产生中断信号,可以输出复位信号。在第一次使用WATCHDOG 定时器时,需要往WTCNT寄存器中写入初始计数值,以后在计数值达到0时自动从WATDAT寄存器中装入,重新开始下一个计数周期。
使用WATCHDOG 定时器的“WATCHDOG 功能”时,在正常的程序中,必须不断重新设置WTCNT寄存器使得它不为0,这样可以保证系统不被重启,称为“喂狗”。当程序崩溃时不能正常喂狗,计数值达到0后系统将被重启,这样程序将程序运行。为了克服各种干扰、避免各类系统错误时系统彻底死机,经常使用WATCHDOG 功能。
WATCHDOG 定时器涉及到的寄存器如下:

  1. WTCON寄存器(WATCHDOG TIMER CONTROL)
    用于设置预分频系数,选择各种频率,决定是否使能中断,是否启用WATCHDOG 功能(即是否输出复位信号)。
    功能 说明
    WATCHDOG功能 0 当定时器达到0时,0:不输出复位信号 1:输出复位信号
    中断使能 1 0:禁止中断 1:使能中断
    时钟选择 [4:3] 选择分频系数 0b00:16 0b01:32 0b10:64 0b11:128
    定时器启动 5 0:停止 1:启动
    预分频系数 [15:8] 预分频系数:0-255

与PWM定时器相似,WATDOG定时器的工作频率可以通过这公式计算:

1
2
3
WATDOG定时器工作频率=PCLK/{presacler value + 1}/{divider value}
{prescaler value} = 0-255
{divider value} = 16,32,64,128
  1. WTDAT寄存器(WATCHDOG TIMER DATA)
    WTDAT寄存器被用来决定WATCHDOG定时器的超时周期,在定时器启动后,当计数达到0时,WTDAT寄存器的值会自动传入WTCNT寄存器。不过,第一次启动WATCHDOG定时器时,WTDAT寄存器的值会自动传入WTCNT寄存器。
  2. WTCNT寄存器(WATCHDOG TIMER COUNT)
    在启动WATCHDOG前,必须往这个寄存器写入初始计数值。启动定时器后,它减1计数,当计数值达到0时:如果中断被使能的话发出中断,如果WATCHDOG功能被使能的话就发出复位信号,装载WTDAT寄存器的值并重新计数。

MPLL和定时器操作实例

程序设计

本实例讲解MPLL、定时器的使用。首先启动MPLL提高系统时钟,初始化存储控制器使SDRAM工作在新的HCLK下,然后将定时器0设为0.5s产生一次中断,在中断程序里改变LED的状态。

代码详解

4个关键点:设置/启动MPLL、根据HCLK设置存储控制器、初始化定时器0、定时器中断。

设置/启动MPLL

clock_init函数用于设置MPLL,本开发板的输入时钟频率Fin为12MHz,将FCLK、HCLK、PCLK分别设为200MHz、100MHz和50MHz。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#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 == 0x3241000) || (GSTATUS1 == 0x32410002))
{
MPLLCON = S3C2410_MPLL_200MHZ; //FCLK = 200MHz,HCLK = 100MHz,PCLK = 50MHz
}
else
{
MPLLCON = S3C2440_MPLL_200MHZ; //FCLK = 200MHz,HCLK = 100MHz,PCLK = 50MHz
}
}

第17行设置FCLK、HCLK、PCLK三者分频比为1:2:4。
当HDIVN非0时,需要将CPU总线模式从“fast bus mode”设为“asynchronous bus mode”,第21-23行的汇编代码即完成此事。
第27-34行代码判断芯片是S3C2410还是S3C2440,它们的MPLL计算公式稍有不同,需要区分开来。如果处理器为S3C2410,使用第29行设置MPLL寄存器,令MDIV=0x5C,PDIV=0x04,SDIV=0,所以

1
2
3
MPLL(FCLK)=(m * Fin)/(p * 2 ^ s) = (0x5c + 8)*12MHz / ((0x04+2)*2^0) = 200MHz
HCLK = FCLK / 2 = 100MHz
PCLK = FCLK / 4 = 50MHz

类似的,S3C2440的FCLK = 200MHz,HCLK = 100MHz,PCLK = 50MHz。

设置存储控制器

memsetup函数被用来设置存储控制器,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/*
使用存储控制器以使用SDRAM
*/
void memsetup(void)
{
volatile unsigned long *p = (volatile unsigned long *)MEM_CTL_BASE;

/*
这个函数之所以这样赋值,而不是像前面的实验(MMU)那样将配置值写在数组中,是因为要生成位置无关的代码,使得这个函数可以被复制到SDRAM之前就可以在Steppingstone中运行。
*/
/*存储控制器13个寄存器的值*/
p[0] = 0x22011110; //BWSCON
p[1] = 0x00000070; //BANKCON0
p[2] = 0x00000070; //BANKCON1
p[3] = 0x00000070; //BANKCON2
p[4] = 0x00000070; //BANKCON3
p[5] = 0x00000070; //BANKCON4
p[6] = 0x00000070; //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] = 0x00018030; //MRSRB6
p[12] = 0x00018030; //MRSRB7
}

现在HCLK的值等于100MHz,REFRESH寄存器的值需要重新计算。
R_CNT = 2^11 + 1 - 100MHz * 7.8125μS = 0x04F4,所以REFRESH = 0x008C0000 + R_CNT = 0x008C0000 + 0x04F4 = 0x008C04F4。

1
2
REFRESH = 0x008c0000 + R_CNT
R_CNT = 2 ^ 11 + 1 - SDRAM时钟频率(MHz) * SDRAM刷新周期(μS)

对于第12到第20行为何用这么笨拙的方式设置存储控制器的13个寄存器,在连接脚本timer.lds中,全部代码的起始运行地址都被设为0x30000000,但是在执行memsetup函数时,代码仍在SRAM(Steppingstone)中,为了能够在Steppingstone中运行这个函数,它应该是位置无关的代码,而第12到第20行得手工赋值可以达到这个要求。

初始化定时器0

tiemr0_init函数用于初始化定时器0,根据相关寄存器的格式并参考代码中的注释就可以理解这个函数,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
Timer input clock Frequence = PCLK / {prescaler value + 1}/{divider value}
{prescaler value} = 0-255
{divider value} = 2,4,8,16
本实验的Timer0的时钟频率=100MHz/(99 + 1)/(16) = 62500Hz
设置Timer0 0.5s触发一次中断
*/

void timer0_init(void)
{
TCFG0 = 99; //预分频器0 = 99
TCFG1 = 0x03; //选择16分频
TCNB0 = 31250; //0.5s触发一次中断
TCON |= (1 << 1); //手动更新
TCON = 0x09; //自动加载,清除“手动更新位”,启动定时器0
}

定时器中断

head.S中调用timer0_init函数之后,定时器0即开始工作;调用init_irq函数使能定时器0中断,设置CPSR寄存器开启IRQ中断之后,每当定时器0达到0就会触发中断。init_irq函数很简单,在init.c中,代码如下:

1
2
3
4
5
6
7
8
9
/*
定时器0中断使能
*/
void init_irq(void)
{
//定时器0中断使能
INTMSK &= (~(1 << 10));

}

发生定时器中断时,CPU将调用其中断服务程序Timer0_Handle,它在interrupt.c中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void Timer0_Handle(void)
{
/*
每次中断令4个LED改变状态
*/
if(INTOFFSET == 10)
{
GPBDAT = ~(GPBDAT & (0xf << 5));
}

//清除中断
SRCPND = 1 << INTOFFSET;
INTPND = INTPND;
}

定时器0的中断使用SRCPND、INTPND寄存器中的位10来表示。中断服务程序Timer0_Handle先判断是否定时器0的中断,若是则反转4个LED的状态。

实例测试

编译生成的bin文件烧入NAND Flash后,上电运行,即可看到4个LED每1S闪烁一次。
将head.S中对clock_init函数的调用去掉,不启用MPLL,并随之将init.c中的memesetup函数的REFRESH寄存器改为12MHz对应的0x008C07A3。重新编译、烧写。可以看到差不多8S这4个LED才闪烁一次。