本文使用的开发板是九鼎创展的X210 iNand版本。
一、S5PV210的时钟系统简介
1、时钟域:MSYS、DSYS、PSYS
因为S5PV210的时钟体系比较复杂,内部外设模块太多,因此把整个内部的时钟划分为三大块,叫做三个时钟域,分别是MSYS、DSYS、PSYS。
因为S5PV210内部的这些模块彼此工作的时钟速率差异太大,因此把高速的放在一起,相对低速的放在一起。
(1)MSYS:CPU(Cortex-A8内核)、DRAM控制器(DMC0和DMC1)、IRAM & IROM等
(2)DSYS:和视频显示、编解码等有关的模块
(3)PSYS:和内部的各种外设时钟有关,例如:串口、SD接口、I2C、AC97、USB等
2、时钟来源:晶振 + 时钟发生器 + PLL + 分频电路
S5PV210外部有四个晶振接口,设计板子硬件时可以根据需要来决定在哪里接晶振。接了晶振之后,上电相应的模块就能产生振荡,产生原始时钟。原始时钟再经过一系列的筛选开关进入相应的PLL电路生成倍频后的高频时钟。高频时钟再经过分频到达芯片内部的各个模块上。
有些模块(串口)内部还有进一步的分频器进行再次分频使用。
3、PLL:APLL、MPLL、EPLL、VPLL
(1)APLL:MSYS域
(2)MPLL:DSYS域
(3)EPLL:PSYS域
(4)VPLL:视频相关模块
4、时钟域和时钟的关系
(1)MSYS域
ARMCLK:给CPU内核工作的时钟,也就是所谓的主频
HCLK_MSYS:MSYS域的高频时钟,给DMC0和DMC1使用
PCLK_MSYS:MSYS域的低频时钟
HCLK_IMEM:给IRAM和IROM(合称IMEM)使用
(2)DSYS域
HCLK_DSYS:DSYS域的高频时钟
PCLK_DSYS:DSYS域的低频时钟
(3)PSYS域
HCLK_PSYS:PSYS域的高频时钟
PCLK_PSYS:PSYS域的低频时钟
SCLK_ONENAND
S5PV210内部的各个外设都是接在内部AMBA总线上面的,AMBA总线有一条高频分支叫AHB,有一条低频分支叫APB。上面的各个域都有各自对应的HCLK_XXXX和PCLK_XXXX,其中HCLK_XXXX就是XXXX域中AHB总线的工作频率,PCLK_XXXX就是XXXX域中APB总线的工作频率。
SoC内部的各个外设其实是挂在总线上工作的,也就是说这个外设的时钟来自于它挂在的总线。例如串口UART挂在PSYS域下的APB总线上,因此串口的时钟来源是PCLK_PSYS。
我们可以通过记住和分析上面的这些时钟域和总线的数值,来确定各个外设的具体时钟频率。
5、各时钟的典型值(IROM中设置的值)
(1)当X210刚上电时,默认是外部晶振 + 内部时钟发生器产生的24MHz频率的时钟直接给ARMCLK的,这时系统的主频就是24MHz。
(2)IROM代码执行时,第六步中初始化了时钟系统,这时给了系统一个默认推荐的运行频率,这个时钟频率是三星推荐的S5PV210工作性能和稳定性最佳的频率。
二、S5PV210的时钟体系框图
(1)两张图之间是渐进的关系。第一张图从左到右依次完成了:原始时钟的生成-->PLL倍频得到高频时钟-->初次分频得到各总线时钟,第二张图是从各个中间时钟(第一张图中某个步骤生成的时钟)到各外设自己使用的时钟(实际就是个别外设自己再额外分频的设置)。
(2)第一张图是理解整个时钟体系的关键,第二张图是进一步分析各外设时钟来源的关键。
(3)要看懂时钟体系框图,两个符号很重要:一个是MUX开关,另一个是DIV分频器。
1)MUX开关就是一个或门,实际对应某个寄存器的某几个bit位的设置,设置值决定了哪条通道是通 的。分析这个可以知道右边的时钟是从左边哪条路过来的,从而知道右边的时钟是多少。
2)DIV分频器是一个硬件设备,可以对左边的频率进行分频,分频后的低频时钟输出到右边。分频器在 编程时实际对应某个寄存器中的某几个bit位。我们可以通过设置这个寄存器的对应bit位来设置分频 器的分频系数。
(4)寄存器中的Clock Source Control Register就是用来设置MUX开关的,Clock Divider Control Register就是用来设置DIV分频器的分频系数的。
三、时钟设置的关键性寄存器
(1)xPLL_LOCK
主要控制PLL的锁定周期
(2)xPLL_CON、xPLL_CON0、xPLL_CON1
主要用来打开 / 关闭PLL电路,设置PLL的倍频参数,查看PLL的锁定状态等
(3)CLK_SRCn(n取值为0~6)
主要用来设置时钟来源,对应时钟体系框图中的MUX开关
(4)CLK_SRC_MASKn
主要用来决定MUX开关n选1后是否能继续通过
(5)CLK_DIVn
主要用来设置各模块的分频器参数
(6)CLK_GATE_n
类似于CLK_SRC_MASKn,对时钟进行开关控制
(7)CLK_DIV_STATn、CLK_MUX_STATn
主要用来查看DIV和MUX的状态是否已经完成还是在进行中
其中最重要的寄存器有三类:CON、SRC、DIV。CON决定PLL倍频到多少,SRC决定MUX走哪一条路,DIV决定分频多少。
四、时钟设置的步骤
1、步骤分析
(1)先选择不使用PLL。让外部的24MHz原始时钟直接过去,绕过APLL那条路
(2)设置锁定时间。
(3)设置分频系数。决定由PLL倍频出来的最高时钟如何分频得到各个分时钟
(4)设置PLL。主要是设置PLL的倍频系数,决定由24MHz的原始时钟可以得到多大的输出频率
(5)打开PLL。前四步已经设置好了所有的开关和分频系数,本步骤打开PLL后,PLL开始工作,锁定频率后输出,然后经过分频得到各个频率。
2、CLK_SRC寄存器的设置分析
CLK_SRC寄存器主要是用来设置MUX开关的,可以先将该寄存器设置为全0,主要是bit0和bit4要设置为0,表示APLL和MPLL暂时都不启用。
3、xPLL_LOCK寄存器的设置分析
xPLL_LOCK寄存器主要是用来设置PLL的锁定周期的,官方的推荐值为0x0FFF。
4、CLK_DIV寄存器的设置分析
我们设置的值是0x14131440,含义分析如下:
PCLK_PSYS = HCLK_PSYS / 2
HCLK_PSYS = MOUT_PSYS / 5
PCLK_DSYS = HCLK_DSYS / 2
HCLK_DSYS = MOUT_DSYS / 4
PCLK_MSYS = HCLK_MSYS / 2
HCLK_MSYS = ARMCLK / 5
SCLKA2M = SCLKAPLL / 5
ARMCLK = MOUT_MSYS / 1
5、APLL和MPLL设置的关键
APLL和MPLL设置的关键都是P、M、S这三个值,我们设置的值都来自于官方数据手册的推荐值。
五、代码实现
1、Makefile
led.bin: start.o led.o clock.o
arm-linux-ld -Ttext 0x0 -o led.elf $^
arm-linux-objcopy -O binary led.elf led.bin
arm-linux-objdump -D led.elf > led_elf.dis
gcc mkv210_image.c -o mkx210
./mkx210 led.bin 210.bin
%.o : %.S
arm-linux-gcc -o $@ $< -c -nostdlib
%.o : %.c
arm-linux-gcc -o $@ $< -c -nostdlib
clean:
rm *.o *.elf *.bin *.dis mkx210 -f
2、start.S
#define SVC_STACK 0xD0037D80
.global _start
_start:
// 初始化时钟
bl clock_init
ldr sp, =SVC_STACK
bl led_blink
b .
3、led.c
#define GPJ0CON 0xE0200240
#define GPJ0DAT 0xE0200244
#define rGPJ0CON *((volatile unsigned int *)GPJ0CON)
#define rGPJ0DAT *((volatile unsigned int *)GPJ0DAT)
void delay(void);
void led_blink(void) {
rGPJ0CON = 0x11111111;
while(1) {
// led亮
rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
// 延时
delay();
// led灭
rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
// 延时
delay();
}
}
void delay(void) {
volatile unsigned int i = 900000;
while (i--);
}
4、clock.S
// 时钟控制器基地址
#define ELFIN_CLOCK_POWER_BASE 0xE0100000
// 时钟相关的寄存器相对时钟控制器基地址的偏移值
#define APLL_LOCK_OFFSET 0x00
#define MPLL_LOCK_OFFSET 0x08
#define APLL_CON0_OFFSET 0x100
#define APLL_CON1_OFFSET 0x104
#define MPLL_CON_OFFSET 0x108
#define CLK_SRC0_OFFSET 0x200
#define CLK_SRC1_OFFSET 0x204
#define CLK_SRC2_OFFSET 0x208
#define CLK_SRC3_OFFSET 0x20c
#define CLK_SRC4_OFFSET 0x210
#define CLK_SRC5_OFFSET 0x214
#define CLK_SRC6_OFFSET 0x218
#define CLK_SRC_MASK0_OFFSET 0x280
#define CLK_SRC_MASK1_OFFSET 0x284
#define CLK_DIV0_OFFSET 0x300
#define CLK_DIV1_OFFSET 0x304
#define CLK_DIV2_OFFSET 0x308
#define CLK_DIV3_OFFSET 0x30c
#define CLK_DIV4_OFFSET 0x310
#define CLK_DIV5_OFFSET 0x314
#define CLK_DIV6_OFFSET 0x318
#define CLK_DIV7_OFFSET 0x31c
#define CLK_DIV0_MASK 0x7fffffff
#define APLL_MDIV 0x7d // 125
#define APLL_PDIV 0x3 // 3
#define APLL_SDIV 0x1 // 1
#define MPLL_MDIV 0x29b // 667
#define MPLL_PDIV 0xc // 12
#define MPLL_SDIV 0x1 // 1
#define set_pll(mdiv, pdiv, sdiv) (1<<31 | mdiv<<16 | pdiv<<8 | sdiv)
#define APLL_VAL set_pll(APLL_MDIV,APLL_PDIV,APLL_SDIV)
#define MPLL_VAL set_pll(MPLL_MDIV,MPLL_PDIV,MPLL_SDIV)
.global clock_init
clock_init:
ldr r0, =ELFIN_CLOCK_POWER_BASE
// 设置各种时钟开关,暂时不使用PLL
ldr r1, =0x0
str r1, [r0, #CLK_SRC0_OFFSET]
// 设置锁定时间
ldr r1, =0x0000FFFF
str r1, [r0, #APLL_LOCK_OFFSET]
str r1, [r0, #MPLL_LOCK_OFFSET]
// 设置分频
// 清bit[0~31]
ldr r1, [r0, #CLK_DIV0_OFFSET]
ldr r2, =CLK_DIV0_MASK
bic r1, r1, r2
ldr r2, =0x14131440
orr r1, r1, r2
str r1, [r0, #CLK_DIV0_OFFSET]
// 设置PLL
// FOUT = MDIV * FIN / (PDIV * 2^(SDIV-1)) = 0x7d * 24 / (0x3 * 2^(1-1)) = 1000MHz
ldr r1, =APLL_VAL
str r1, [r0, #APLL_CON0_OFFSET]
// FOUT = MDIV * FIN / (PDIV * 2^SDIV) = 0x29b * 24 / (0xc * 2^1) = 667MHz
ldr r1, =MPLL_VAL
str r1, [r0, #MPLL_CON_OFFSET]
// 设置各种时钟开关,使用PLL
ldr r1, [r0, #CLK_SRC0_OFFSET]
ldr r2, =0x10001111
orr r1, r1, r2
str r1, [r0, #CLK_SRC0_OFFSET]
mov pc, lr
以下为时钟初始化的C语言实现,只需将时钟初始化的相关代码(clock.S)改为C语言实现(clock.c)即可。
// 时钟控制器基地址
#define ELFIN_CLOCK_POWER_BASE 0xE0100000
// 时钟相关的寄存器相对时钟控制器基地址的偏移值
#define APLL_LOCK_OFFSET 0x00
#define MPLL_LOCK_OFFSET 0x08
#define APLL_CON0_OFFSET 0x100
#define APLL_CON1_OFFSET 0x104
#define MPLL_CON_OFFSET 0x108
#define CLK_SRC0_OFFSET 0x200
#define CLK_SRC1_OFFSET 0x204
#define CLK_SRC2_OFFSET 0x208
#define CLK_SRC3_OFFSET 0x20c
#define CLK_SRC4_OFFSET 0x210
#define CLK_SRC5_OFFSET 0x214
#define CLK_SRC6_OFFSET 0x218
#define CLK_SRC_MASK0_OFFSET 0x280
#define CLK_SRC_MASK1_OFFSET 0x284
#define CLK_DIV0_OFFSET 0x300
#define CLK_DIV1_OFFSET 0x304
#define CLK_DIV2_OFFSET 0x308
#define CLK_DIV3_OFFSET 0x30c
#define CLK_DIV4_OFFSET 0x310
#define CLK_DIV5_OFFSET 0x314
#define CLK_DIV6_OFFSET 0x318
#define CLK_DIV7_OFFSET 0x31c
#define CLK_DIV0_MASK 0x7fffffff
#define APLL_MDIV 0x7d // 125
#define APLL_PDIV 0x3 // 3
#define APLL_SDIV 0x1 // 1
#define MPLL_MDIV 0x29b // 667
#define MPLL_PDIV 0xc // 12
#define MPLL_SDIV 0x1 // 1
#define set_pll(mdiv, pdiv, sdiv) (1<<31 | mdiv<<16 | pdiv<<8 | sdiv)
#define APLL_VAL set_pll(APLL_MDIV,APLL_PDIV,APLL_SDIV)
#define MPLL_VAL set_pll(MPLL_MDIV,MPLL_PDIV,MPLL_SDIV)
#define REG_CLK_SRC0 (ELFIN_CLOCK_POWER_BASE + CLK_SRC0_OFFSET)
#define REG_APLL_LOCK (ELFIN_CLOCK_POWER_BASE + APLL_LOCK_OFFSET)
#define REG_MPLL_LOCK (ELFIN_CLOCK_POWER_BASE + MPLL_LOCK_OFFSET)
#define REG_CLK_DIV0 (ELFIN_CLOCK_POWER_BASE + CLK_DIV0_OFFSET)
#define REG_APLL_CON0 (ELFIN_CLOCK_POWER_BASE + APLL_CON0_OFFSET)
#define REG_MPLL_CON (ELFIN_CLOCK_POWER_BASE + MPLL_CON_OFFSET)
#define rREG_CLK_SRC0 (*(volatile unsigned int *)REG_CLK_SRC0)
#define rREG_APLL_LOCK (*(volatile unsigned int *)REG_APLL_LOCK)
#define rREG_MPLL_LOCK (*(volatile unsigned int *)REG_MPLL_LOCK)
#define rREG_CLK_DIV0 (*(volatile unsigned int *)REG_CLK_DIV0)
#define rREG_APLL_CON0 (*(volatile unsigned int *)REG_APLL_CON0)
#define rREG_MPLL_CON (*(volatile unsigned int *)REG_MPLL_CON)
void clock_init(void) {
// 设置各种时钟开关,暂时不使用PLL
rREG_CLK_SRC0 = 0x0;
// 设置锁定时间
rREG_APLL_LOCK = 0x0000ffff;
rREG_MPLL_LOCK = 0x0000ffff;
// 设置分频
rREG_CLK_DIV0 = 0x14131440;
// 设置PLL
// FOUT = MDIV * FIN / (PDIV * 2^(SDIV-1)) = 0x7d * 24 / (0x3 * 2^(1-1)) = 1000MHz
rREG_APLL_CON0 = APLL_VAL;
// FOUT = MDIV * FIN / (PDIV * 2^SDIV) = 0x29b * 24 / (0xc * 2^1) = 667MHz
rREG_MPLL_CON = MPLL_VAL;
// 设置各种时钟开关,使用PLL
rREG_CLK_SRC0 = 0x10001111;
}