本文使用的开发板是九鼎创展的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; }