上次基于u-boot写了一个跑马灯程序,见 《第一个跑马灯程序》 。但是这里有个问题,离了u-boot它就不能用了,因为缺少很多系统初始化工作,也没有人能把它加载到RAM中去运行。因此,为了学习(好强大的理由。。。),我们需要自己实现一个bootloader,来替代u-boot做这些事情。
显然让我在这么短的时间里写一个和u-boot同级别的bootloader很不现实,所以让我们把问题尽可能的简化,目的是让跑马灯程序在裸机上也能跑起来就行了。
OK,照着这个思路我奋斗了几个日夜(其实每天只有几小时的业余时间- -),经历无数的苦难(没办法,咱是无畏的菜鸟新手),终于达成了目标。
下面来介绍一下该bootloader的思路,其存储布局见下图:
首先将bootloader和内核程序(这里先用跑马灯程序替代)烧录进NOR Flash中物理地址0处,在系统加电后响应Reset异常,系统将从地址0处开始执行,这个位置就是bootloader的入口点。
要注意的是在这个模型里,boot.s程序上限是256B,整个内核(head.s + main.c)的大小不能超过4K - 256B。否则就要对这些参数作调整才行。
在bootloader执行其间除了初始化一些寄存器以外,还要把内核代码从ROM中搬到RAM去执行,这就要求我们需要在此之前先初始化好存储空间。
bootloader执行流程如下:
1. 异常向量初始化
2. 初始化CPSR,包括关闭中断及设定svc模式等
3. 关闭看门狗定时器
4. 初始化系统时钟
5. 初始化CPU Cache(这步可以不做,但是系统效率会明显下降许多)
6. 初始化存储空间,主要是配置SDRAM
7. 将内核程序复制到SDRAM中
8. 跳转到SDRAM中执行内核程序
内核的执行流程如下:
1. 内核异常向量初始化
2. 堆栈初始化
3. 跳转到C程序中执行
4. 执行内核任务(本例中使用万能的跑马灯程序替代- -b)
上面的这一切看起来非常的简单,相信很多人都见过类似的描述。我最初也这么觉得,但随着逐步的深入,我也越来越郁闷。好让我们一步步的看。
首先是boot.s,也即是该bootloader中真正属于bootloader的部分,也是本文的核心。
boot.s
几个.equ定义就不多说了,参照代码和前面的图就应该会很清楚了。
从zero开始是物理地址0,也是系统复位后的入口点。这里按照ARM的规定,放置异常向量表。首先复位异常就是我们bootloader的入口点,让它跳到下面的reset子程序去执行。其它的几个异常,除了保留异常,其它的都被定位到一个叫做KERNEL_VECTOR的地址附近去了。该地址就是内核异常表。在该系统里,内核可以不直接使用硬件的异常信号,而可以把它重定向到一个内核自己管理的异常向量表去。
这样做有什么好处?首先可以让它们作为内核的一部分,这样可以随内核程序一起复制到RAM中,不需另写复制代码。其次为了方便内核管理,也方便代码书写(对于一些现有的代码也更易于移植),可以把它们重新组成一个向量表,其向量号可以自己定义。其中一个可能的方案如上,将undef映射到0号内核异常,将swi映射到1号内核异常,pabort、dabort分别映射到2号和3号,irq和fiq被映射到内核异常4号和5号。
在后面的代码中还会看到这个内核异常向量表的定义,先让我们关注一下bootloader的主要工作,它们都在reset子程序中。
首先是初始化CPSR寄存器,S3C44B0X手册上解释得很清楚了,这里就不多说了。
关闭看门狗定时器,很简单,向WTCON寄存器写0就行了,详细看手册。
系统时钟初始化是个比较头痛的问题。S3C44B0X的时钟源可以由外部晶体来提供,也可以直接使用外部时钟信号。通过配置OM[3:2]管脚来确定采用哪种方式,总的来说OM[3:2]=00使用外部晶体,OM[3:2]=01使用外部时钟。详细请参阅S3C44B0X手册。
在我的板子上,OM3接地,OM2是通过跳线来设置的,原理图:
这里我把5-6跳线接通,OM2短路接地,所以OM[3:2]=00,使用外部晶振作为我们的时钟源。
接下来我们需要将外部晶振的频率转换为系统所需要的时钟频率。在S3C44B0X里这个操作是可编程的,需要程序员通过设置PLLCON寄存器来控制。说起这个PLLCON,自然就会引出这个著名的式子:
Fin是输入频率,也就是外部晶振的频率,我们的目的是通过PLL(锁相环电路)将它转化成Fpllo作为我们的系统时钟频率。我们需要确定的是上面三个分频值m、p、s,也即是在PLLCON寄存器设置MDIV、PDIV、SDIV。
除了那个公式,还有下面几条限制:
1. 20MHz < Fpllo < 66MHz
2. Fpllo * 2 ^ s < 170MHz
3. s频分值应该尽可能的大
4. 建议 1MHz < Fin / p < 2MHz
比较晕是吧,网上有人提供了计算器来算这个。常用的一些典型数值如下:
No. | Fin(MHz) | Fout(MHz) | MDIV | PDIV | SDIV |
1 | 10 | 40 | 0x48 | 0x3 | 0x2 |
2 | 10 | 50 | 0x2a | 0x3 | 0x1 |
3 | 10 | 60 | 0x34 | 0x3 | 0x1 |
4 | 4 | 60 | 0x34 | 0x0 | 0x1 |
5 | 3 | 60 | 0x48 | 0x0 | 0x1 |
6 | 10 | 75 | 0x3a | 0x3 | 0x1 |
那么首先要确定Fin,也就是外部晶振频率,这就要看板子的原理图了:
如图得知我的板子的晶振源是10MHz,在这里我选择第一个方案,也就是系统时钟为40MHz。于是有MDIV=0x48,PDIV=0x3,SDIV=0x2。
初始化CPU Cache就相对比较简单了。当然你可以跳过这一步,其结果是系统效率会变得很低(跑马灯程序的话,无Cache比有Cahche慢了10倍以上)。我选择最简单的方案,把全部内部8K的SRAM都作为Cache使用,同时使能写缓冲。
存储器初始化是最难的,下面的部分我也不能保证全部正确,请各位用批判的眼光来看待,如果发现有错误,强烈欢迎指正。
如前面所说,由于我们要把内核程序从NOR Flash复制到SDRAM,所以需要在其之前先进行存储器的初始化。代码末尾那堆魔法数字一样的数据就是相关寄存器的设定值了。寄存器名字在注释中有给出。
这里各位肯定要先翻S3C44B0X的手册了,上面的寄存器各位是什么意思我就不多说了,真正需要的数据我都在相应的注释中给出了,我们重点关注这些数据怎么求得。
BWSCON:
在本例中我们没有用到什么外部设备,于是只需要关心BANK0(NOR Flash)和BANK6(SDRAM)。由于BANK0的参数是确定的(寄存器相关位为只读),因此只需要关心SDRAM(BANK6)的设置。
我的板子使用的SDRAM是现代的HY57V641620HGT-H,根据手册的第一页标题“4 Banks x 1M x 16Bit Synchronous DRAM”,就知道它是16位的了。另外,从该芯片的引脚也能看得出,数据输出引脚为DQ0~DQ15,16个。所以这里,DW6设为16位。
BANKCON0:
NOR Flash的配置,在设置其以前让我们仔细看看S3C44B0X手册里这张时序图,理解一些概念:
很好,让我们对照两张时序图,可以很容易的得出:
Tacs = tACC - tCE
Tcos = tCE - tOE
Tacc = tRC - tACC + tOE
Toch = 0
Tcah = 0
于是再看Flash手册中的这个表:
前面说了,我的ROM是AM29LV160D -90 EI,故在取那些数值时使用-90列:
Tacs = tACC - tCE = 90 - 90 = 0ns
Tcos = tCE - tOE = 90 - 35 = 55ns
Tacc = tRC - tACC + tOE = 90 - 90 + 35 = 35ns
Toch = 0 = 0ns
Tcah = 0 = 0ns
手册上提供的单位都是纳秒(ns),而寄存器需要的是时钟周期数。S3C44B0X手册中在一个很不起眼的地方说了一段很重要的话:
All types of master clock in this memory controller correspond to the bus clock.
For example, MCLK in DRAM and SRAM is same as the bus clock, and SCLK in SDRAM is also the same as the bus clock. In this chapter (Memory Controller), one clock means one bus clock.
就是说存储器控制寄存器中涉及到时钟数的参数时,都是以总线时钟作为度量单位的。
Flash的总线时钟等于系统时钟,也就是前面配置的40MHz,因此可以得知:
T = 1 / 40MHz = 25ns
Tacs = 0/25 = 0clk
Tcos = 55/25 = 2.2clk
Tacc = 35/25 = 1.4clk
Toch = 0/25 = 0clk
Tcah = 0/25 = 0clk
在实际使用时的取值应该大于等于这些值,以免系统不稳定。因此最后我们取值:
Tacs = 0clk
Tcos = 4clk
Tacc = 2clk
Toch = 0clk
Tcah = 0clk
REFRESH:
参照S3C44B0X手册,发现其实只有CL(CAS潜伏期)需要我们来选择。而HY57V641620HGT-H只支持2或3,而通常认为2的效率会明显优于3,因此我选用了2。
然后HY57V641620HGT-H手册上下面这两张表(部分)非常重要,设置BANKCON6和REFRESH就靠它们了:
BANKCON6:
由于HY57V641620HGT -H ,我们只看-H这列的。显然Trcd=20ns。而当CL=2时,SDRAM的总线频率将是100MHz,即10ns,故Trcd=20/10=2clk。
然后是SCAN。HY57V641620HGT-H手册对其地址引脚有如下解释:
可知SCAN应填8位,即00。
REFRESH:
Trp和Trc在上面的表里都能查得出,Trp=2clk,Trc=7clk。而计算刷新频率的公式在S3C44B0X手册中有给出:
注意其中刷新周期(refresh period)单位为微秒us,MCLK单位为MHz。
这个刷新周期到哪里找呢?我的SDRAM手册第一页就有这么一句:4096 refresh cycles / 64ms。哈,这么一来显然刷新周期就是64ms/4096=15.625us。
由于MCLK就是PLL产生的系统时钟40MHz,因此很容易算出refresh_count=1424。
BANKSIZE:
HY57V641620HGT-H的手册上说了:4 Banks x 1M x 16Bit Synchronous DRAM,可以算出它是8M的,因此BK67MAP设为8M/8M就可以了。
呼,好累,至此存储器初始化就算是做完了。接下来就是照之前所说的,把内核程序从Flash搬进刚才初始化好的SDRAM里。代码很简单,注意好源地址和目标地址别弄错就行了。要说明的是由于这种方法一次复制4*8=32个字节,所以如果内核程序大小不能整除32的话会多复制几个字节:
实际复制大小 = 向上取整(内核大小 / 32) * 32
最后靠一个b指令跳进RAM中已放置好的内核代码入口点去执行就行了。
接下来看head.s,内核程序的最前面部分,包含了内核异常向量表和入口点。
head.s
start子程序的任务很简单,就是调用C函数entry进入C执行。调用C程序之前要初始化好堆栈,这是常识。由于我们本例中只使用svc模式,所以只要初始化好svc模式的堆栈就行了。栈址的选择需要注意,保证一不能溢出,二不能被代码覆盖。
GNU AS与ARMASM不同,使用外部符号不需要IMPORT声明,所以直接bl entry就可以了。
本文所使用的跑马灯和 《第一个跑马灯程序》 一文中的不同,没有死循环,完成一次显示就会返回到head.s,而在head.s中通过mov pc, #0软复位,这样系统重启后又会执行第二次LED显示,如此反复,表现上和前文中的跑马灯程序并无二异。
led.c
至此bootloader就写完了,剩下的任务自然是生成机器码。Makefile如下:
Makefile:
其中有个NASM程序image.asm,它是用来把boot.bin和kernel.bin拼在一起的。按照前面的设计,kernel.bin必须从KERNEL_DATA也就是0x00000100处开始,而这里boot.bin大小为228字节,因此需要填充256-228=28个字节。
image.asm:
make一下就能得到程序映像image.bin,但是怎么把它烧录进Flash?因为这次我们的目标机是完完全全的裸机,没有u-boot了,无法使用loadb,只好使用Flash烧录工具了。
我用的是板子配套光盘上的Flash Programmer(很想找个开源的工具,可惜没找到),满心欢喜的点开它之后傻眼了,该烧录工具不支持binary格式。。。只支持s19、elf和hex等格式。
一阵google,找到了这么一个开源的程序: bin2s19.cpp ,可以把binary转成s19格式(后缀是.cpp,其实是个纯c程序,最好把后缀名改成.c再用gcc编译)。
假设上面那个程序编出来为bin2s19放在image.bin的生成目录下,执行:
./bin2s19 image.bin 0 0x02000000 image.s19
就能生成s19文件image.s19。
使用Flash Programmer可以成功地烧录该文件。然后Reset,熟悉的跑马灯又成功的再现了!泪水之余再次让我感到了学海茫茫,前路遥远,我要挺住啊!
注:其实Flash Programmer自带了一个命令行程序BinToS19.exe,因为是win32的,配合我的编译环境使用起来不太方便。