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

时间:2021-12-27 21:18:54

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