作者:ploor 提交日期:2007-4-8 12:03:00 | <script language="javascript" src="http://hot.tianyaclub.com/hot.js" type="text/javascript"></script> |
一.简介
本书面向由传统51单片机转向ARM嵌入式开发的硬件工程师、由硬件转嵌
入式软件开发的工程师、没有嵌入式开发经验的软件工程师。分9个部分:
1、开发环境建立
2、S3C2410功能部件介绍与实验(含实验代码)
3、bootloader vivi详细注释
4、linux移植
5、linux驱动
6、yaffs文件系统详解
7、调试工具
8、GUI开发简介
9、UC/OS移植
通过学习第二部分,即可了解基于ARM CPU的嵌入式开发所需要的外围器
件及其接口。对应的实验代码实现了对这些接口的操作,这可以让硬件工程师形
成一个嵌入式硬件开发的概念。这部分也可以当作S3C2410的数据手册来使用。
一个完整的嵌入式linux系统包含4部分内容:bootloader、parameters、kernel、
root file system。3、4、5、6部分详细介绍了这4部分的内容,这是linux底层软
件开发人员应该掌握的。通过学习这些章节,您可以详细了解到如何在一个裸板
上裁减、移植linux,如何构造自己的根文件系统,如何编写适合客户需求的驱
动程序——驱动程序这章将结合几个经典的驱动程序进行讲解。您还可以了解到
在用在nand flash上的非常流行的yaffs文件系统是如何工作的,本书将结合yaffs
代码详细介绍yaffs文件系统。
第7部分介绍了嵌入式linux开发中使用gdb进行调试的详细过程。
此文档目前完成了1、2、3部分,后面部分将陆续完成。希望能对各位在嵌
入式开发方面献上棉力。
欢迎来信指出文中的不足与错误,欢迎来信探讨技术问题。
Email :thisway.diy@163.com
MSN :thisway.diy@163.com
QQ :17653039
二.建立开发环境
(1)编译器arm-linux-gcc-3.4.1
下载地址:
ftp://ftp.handhelds.org/projects/toolchain/arm-linux-gcc-3.4.1.tar.bz2
执行如下命令安装:
bunzip2 arm-linux-gcc-3.4.1.tar.bz2
tar xvf arm-linux-gcc-3.4.1.tar -C /
生成的编译工具在目录/usr/local/arm/3.4.1/bin下,修改/etc/profile,
增加如下一行。这可以让我们直接运行arm-linux-gcc,而不必将其绝对路径都
写出来,不过这得重新启动后才生效:
pathmunge /usr/local/arm/3.4.1/bin
(2)Jflash-s3c2410:S3C2410芯片的JTAG工具
我们的第一个程序就是通过它下载到开发板上的nor flash或者nand flash
上去的。把它放到/usr/local/bin目录下。
下载地址:e
ftp://ftp.mizi.com/pub/linuette/SDK/1.5/target/box/Jflash/Jflash-
s3c2410
注意:步骤3您现在不必理会,可以等进行到“调试”部分时再回过头来看。
(3)安装gdb调试工具
下载地址:
http://www.gnu.org/software/gdb/download/
http://ftp.gnu.org/gnu/gdb/gdb-6.3.tar.gz
执行如下命令安装:
a.安装在主机上运行的arm-linux-gdb工具:
tar xvzf gdb-6.3.tar.gz
cd gdb6.3
./configure --target=arm-linux
make
make install
此时,在/usr/local/bin中生成arm-linux-gdb等工具
b.继续上面的步骤,安装gdbserver。需要将此工具下载到开发板上运
行,这在后面会详细描述:
cd gdbserver
export CC=/usr/local/arm/3.4.1/bin/arm-linux-gcc
./configure arm-linux
make
此时在当前目录中生成了gdbserver工具,当我们讲到如何调试时,
会把这个文件下载到开发板上去。
三.S3C2410基础实验
本章将逐一介绍S3C2410各功能模块,并结合简单的程序进行上机实验。您
不必将本章各节都看完,完全可以看了一、两节,得到一个大概的印象之后,就
开始下一章。本章可以当作手册来用。
注意:了解S3C2410各部件最好的参考资料是它的数据手册。本文不打算翻
译该手册,在进行必要的讲解后,进行实际实验——这才是本文的重点。
(1)实验一:LED_ON
led_on.s只有7条指令,它只是简单地点亮发光二极管LED1。本实验的目
的是让您对开发流程有个基本概念。
实验步骤:
a.把PC并口和开发板JTAG接口连起来、确保插上“BOOT SEL”跳线、上
电(呵呵,废话,如果以后实验步骤中未特别指出,则本步骤省略)
b.进入LED_ON目录后,执行如下命令生成可执行文件led_on:
make
c.执行如下命令将led_on写入nand flash:
i. Jflash-s3c2410 led_on /t=5
ii.当出现如下提示时,输入0并回车:
iii.当出现如下提示时,输入0并回车:
iv.当再次出现与步骤ii相同的提示时,输入2并回车
d.按开发板上reset键后可看见LED1被点亮了
实验步骤总地来说分3类:编写源程序、编译/连接程序、烧写代码。
先看看源程序led_on.s:
1 .text
2 .global _start
3 _start:
4 LDR R0,=0x56000010 @R0设为GPBCON寄存器。此寄存器
@用于选择端口B各引脚的功能:
@是输出、是输入、还是其他
5 MOV R1,#0x00004000
6 STR R1,[R0] @设置GPB7为输出口
7 LDR R0,=0x56000014 @R0设为GPBDAT寄存器。此寄存器
@用于读/写端口B各引脚的数据
8 MOV R1,#0x00000000 @此值改为0x00000080,
@可让LED1熄灭
9 STR R1,[R0] @GPB7输出0,LED1点亮
10 MAIN_LOOP:
11 B MAIN_LOOP
对于程序中用到的寄存器GPBCON、GPBDAT,我稍作描述,具体寄存器的操作
可看实验三:I/O PORTS。GPBCON用于选择PORT B的11根引脚的功能:输出、输
入还是其他特殊功能。每根引脚用2位来设置:00表示输入、01表示输出、10表
示特殊功能、11保留不用。LED1-3的引脚是GPB7-GPB10,使用GPBCON中位[12:13]、
[13:14]、[15:16]、[17:18]来进行功能设置。GPBDAT用来读/写引脚:GPB0对应
位0、GPB1对应位1,诸如此类。当引脚设为输出时,写入0或1可使相应引脚输出
低电平或高电平。
程序很简单,第4、5、6行3条指令用于将LED1对应的引脚设成输出引脚;
第7、8、9行3条指令让这条引脚输出0;第11行指令是个死循环。
实验步骤b中,指令“make”的作用就是编译、连接led_on.s源程序。Makefile
的内容如下:
1 led_on:led_on.s
2 arm-linux-gcc -g -c -o led_on.o led_on.s
3 arm-linux-ld -Ttext 0x0000000 -g led_on.o -o led_on_tmp.o
4 arm-linux-objcopy -O binary -S led_on_tmp.o led_on
5 clean:
6 rm -f led_on
7 rm -f led_on.o
8 rm -f led_on_tmp.o
make指令比较第1行中文件led_on和文件led_on.s的时间,如果led_on
的时间比led_on.s的时间旧(led_on未生成时,此条件默认成立),则执行第2、
3、4行的指令更新led_on。您也可以不用指令make,而直接一条一条地执行2、
3、4行的指令——但是这样多累啊。第2行的指令是预编译,第3行是连接,
第4行是把ELF格式的可执行文件led_on_tmp.o转换成二进制格式文件led_on。
执行“make clean”时强制执行6、7、8行的删除命令。
注意:Makefile文件中相应的命令行前一定有一个制表符(TAB)
汇编语言可读性太差,现在请开始实验二,我用C语言来实现了同样的功能,
而以后的实验,我也尽可能用C语言实现。
(2)实验二:LED_ON_C
C语言程序执行的第一条指令,并不在main函数中。当我们生成一个C程序
的可执行文件时,编译器总是在我们的代码前加一段固定的代码——crt0.o,它
是编译器自带的一个文件。此段代码设置C程序的堆栈等,然后调用main函数。
很可惜,在我们的裸板上,这段代码无法执行,所以我们得自己写一个。这段代
码很简单,只有3条指令。
crt0.s代码:
1 .text
2 .global _start
3 _start:
4 ldr sp, =1024*4 @设置堆栈,注意:不能大于4k
@nand flash中的代码在复位后会
@移到内部ram中,它只有4k
5 bl main @调用C程序中的main函数
6 halt_loop:
7 b halt_loop
现在,我们可以很容易写出控制LED的程序了,led_on_c.c代码如下:
1 #define GPBCON (*(volatile unsigned long *)0x56000010)
2 #define GPBDAT (*(volatile unsigned long *)0x56000014)
3 int main()
4 {
5 GPBCON = 0x00004000; //设置GPB7为输出口
6 GPBDAT = 0x00000000; //令GPB7输出0
7 return 0;
8 }
最后,我们来看看Makefile:
1 led_on_c : crt0.s led_on_c.c
2 arm-linux-gcc -g -c -o crt0.o crt0.s
3 arm-linux-gcc -g -c -o led_on_c.o led_on_c.c
4 arm-linux-ld -Ttext 0x0000000 -g crt0.o led_on_c.o -o
led_on_c_tmp.o
5 arm-linux-objcopy -O binary -S led_on_c_tmp.o led_on_c
6 clean:
7 rm -f led_on_c
8 rm -f led_on_c.o
9 rm -f led_on_c_tmp.o
10 rm -f crt0.o
第2、3行分别对源程序crt0.s、led_on_c.c进行预编译,第4行将预编译
得到的结果连接起来,第5行把连接得到的ELF格式可执行文件led_on_c_tmp.o
转换成二进制格式文件led_on_c。
好了,可以开始上机实验了:
实验步骤:
a.进入LED_ON_C目录后,执行如下命令生成可执行文件led_on_c:
make
b.执行如下命令将led_on_c写入nand flash:
i. Jflash-s3c2410 led_on_c /t=5
ii.当出现如下提示时,输入0并回车:
K9S1208 NAND Flash JTAG Programmer Ver 0.0
0:K9S1208 Program 1:K9S1208 Pr BlkPage 2: Exit
Select the function to test :
iii.当出现如下提示时,输入0并回车:
Input target block number:
iv.当出现与步骤ii相同的提示时,输入2并回车
c.按开发板上reset键后可看见LED1被点亮了
目录LEDS中的程序是使用4个LED从0到15轮流计数,您可以试试:
a.进入目录后make
b.Jflash-s3c2410 leds /t=5
c.reset运行
另外,如果您有兴趣,可以使用如下命令看看二进制可执行文件的反汇编码:
arm-linux-objdump -D -b binary -m arm xxxxx(二进制可执行文件名)
注意:本文的所有程序均在SOURCE目录中,各程序所在目录均为大写,其
可执行文件名为相应目录名的小写,比如LEDS目录下的可执行文件为leds。以
后不再赘述如何烧写程序:直接运行Jflash-s3c2410即可看到提示。
(3)实验三:I/O PORTS
请打开S3C2410数据手册第9章IO/ PORTS,I/O PORTS含GPA、GPB、..、
GPH八个端口。它们的寄存器是相似的:GPxCON用于选择引脚功能,GPxDAT用
于读/写引脚数据,GPxUP用于确定是否使用内部上拉电阻(x为A、B、..、H,
没有GPAUP寄存器)。
1、PORT A与PORT B-H在功能选择方面有所不同,GPACON中每一位对应一根引
脚(共23根引脚)。当某位设为0时,相应引脚为输出引脚,此时我们可以在
GPADAT中相应位写入0或1让此引脚输出低电平或高电平;当某位设为1时,
相应引脚为地址线或用于地址控制,此时GPADAT无用。一般而言GPACON通
常设为全1,以便访问外部存储器件。PORT A我们暂时不必理会。
2、PORT B-H在寄存器操作方面完全相同。GPxCON中每两位控制一根引脚:00
表示输入、01表示输出、10表示特殊功能、11保留不用。GPxDAT用于读/
写引脚:当引脚设为输入时,读此寄存器可知相应引脚的状态是高是低;当
引脚设为输出时,写此寄存器相应位可令此引脚输出低电平或高电平。GpxUP:
某位为0时,相应引脚无内部上拉;为1时,相应引脚使用内部上拉。
其他寄存器的操作在后续相关章节使用到时再描述;PORT A-H中引脚的特殊
功能比如串口引脚、中断引脚等,也在做相关实验时再描述。
目录KEY_LED中的程序功能为:当K1-K4中某个按键按下时,LED1-LED4中
相应LED点亮。
key_led.c代码:
1 #define GPBCON (*(volatile unsigned long *)0x56000010)
2 #define GPBDAT (*(volatile unsigned long *)0x56000014)
3 #define GPFCON (*(volatile unsigned long *)0x56000050)
4 #define GPFDAT (*(volatile unsigned long *)0x56000054)
/*
LED1-4对应GPB7-10
*/
5 #define GPB7_out (1<<(7*2))
6 #define GPB8_out (1<<(8*2))
7 #define GPB9_out (1<<(9*2))
8 #define GPB10_out (1<<(10*2))
/*
K1-K3对应GPF1-3
K4对应GPF7
*/
9 #define GPF1_in ~(3<<(1*2))
10 #define GPF2_in ~(3<<(2*2))
11 #define GPF3_in ~(3<<(3*2))
12 #define GPF7_in ~(3<<(7*2))
13 int main()
{
//LED1-LED4对应的4根引脚设为输出
14 GPBCON =GPB7_out | GPB8_out | GPB9_out | GPB10_out ;
//K1-K4对应的4根引脚设为输入
15 GPFCON &= GPF1_in & GPF2_in & GPF3_in & GPF7_in ;
16 while(1){
//若Kn为0(表示按下),则令LEDn为0(表示点亮)
17 GPBDAT = ((GPFDAT & 0x0e)<<6) | ((GPFDAT & 0x80)<<3);
}
18 return 0;
}
实验步骤:
a.进入目录KEY_LED,运行make命令生成key_led
b.烧写key_led
(4)实验四:arm-linux-ld
在开始后续实验之前,我们得了解一下arm-linux-ld连接命令的使用。在
上述实验中,我们一直使用类似如下的命令进行连接:
arm-linux-ld -Ttext 0x00000000 crt0.o led_on_c.o -o led_on_c_tmp.o
我们看看它是什么意思:-o选项设置输出文件的名字为led_on_c_tmp.o;
“--Ttext 0x00000000”设置代码段的起始地址为0x00000000;这条指令的作用就
是将crt0.o和led_on_c.o连接成led_on_c_mp.o可执行文件,此可执行文件的代
码段起始地址为0x00000000。
我们感兴趣的就是“—Ttext”选项!进入LINK目录,link.s代码如下:
1 .text
2 .global _start
3 _start:
4 b step1
5 step1:
6 ldr pc, =step2
7 step2:
8 b step2
Makefile如下:
1 link:link.s
2 arm-linux-gcc -c -o link.o link.s
3 arm-linux-ld -Ttext 0x00000000 link.o -o link_tmp.o
4 # arm-linux-ld -Ttext 0x30000000 link.o -o link_tmp.o
5 arm-linux-objcopy -O binary -S link_tmp.o link
6 arm-linux-objdump -D -b binary -m arm link >ttt.s
7 # arm-linux-objdump -D -b binary -m arm link >ttt2.s
8 clean:
9 rm -f link
10 rm -f link.o
11 rm -f link_tmp.o
实验步骤:
1.进入目录LINK,运行make生成arm-linux-ld选项为“-Ttext 0x00000000”
的反汇编码ttt.s
2.make clean
3.修改Makefile:将第4、7行的“#”去掉,在第3、6行前加上“#”
4.运行make生成arm-linux-ld选项为“-Ttext 0x30000000”的反汇编码ttt2.s
link.s程序中用到两种跳转方法:b跳转指令、直接向pc寄存器赋值。我们先
把在不同“—Ttext”选项下,生成的可执行文件的反汇编码列出来,再详细分析这
两种不同指令带来的差异。
ttt.s: ttt2.s
0: eaffffff b 0x4 0: eaffffff b 0x4
4: e59ff000 ldr pc, [pc, #0] ; 0xc 4: e59ff000 ldr pc, [pc, #0] ; 0xc
8: eafffffe b 0x8 8: eafffffe b 0x8
c: 00000008 andeq r0, r0, r8 c: 30000008 tsteq r0, #8 ; 0x8
先看看b跳转指令:它是个相对跳转指令,其机器码格式如下:
[31:28]位是条件码;[27:24]位为“1010”时,表示B跳转指令,为“1011”时,表示BL
跳转指令;[23:0]表示偏移地址。使用B或BL跳转时,下一条指令的地址是这样计算的:将指
令中24位带符号的补码立即数扩展为32(扩展其符号位);将此32位数左移两位;将得到的值
加到pc寄存器中,即得到跳转的目标地址。我们看看第一条指令“b step1”的机器码eaffffff:
1. 24位带符号的补码为0xffffff,将它扩展为32得到:0xffffffff
2.将此32位数左移两位得到:0xfffffffc,其值就是-4
3.pc的值是当前指令的下两条指令的地址,加上步骤2得到的-4,这恰好是第
二条指令step1的地址
各位不要被被反汇编代码中的“b 0x4”给迷惑了,它可不是说跳到绝对地址0x4
处执行,绝对地址得像上述3个步骤那样计算。您可以看到b跳转指令是依赖于当
前pc寄存器的值的,这个特性使得使用b指令的程序不依赖于代码存储的位置——
即不管我们连接命令中“--Ttext”为何,都可正确运行。
再看看第二条指令ldr pc, =step2:从反汇编码“ldr pc, [pc, #0]”可以看出,
这条指令从内存中某个位置读出数据,并赋给pc寄存器。这个位置的地址是当前
pc寄存器的值加上偏移值0,其中存放的值依赖于连接命令中的“--Ttext”选项。
执行这条指令后,对于ttt.s,pc=0x00000008;对于ttt2.s, pc=0x30000008。于
是执行第三条指令“b step2”时,它的绝对地址就不同了:对于ttt.s,绝对地址
为0x00000008;对于ttt.s,绝对地址为0x30000008。
ttt2.s上电后存放的位置也是0,但是它连接的地址是0x30000000。我们以后
会经常用到“存储地址和连接地址不同”(术语上称为加载时域和运行时域)的特性:
大多机器上电时是从地址0开始运行的,但是从地址0运行程序在性能方面总有很
多限制,所以一般在开始的时候,使用与位置无关的指令将程序本身复制到它的连
接地址处,然后使用向pc寄存器赋值的方法跳到连接地址开始的内存上去执行剩下
的代码。在实验5、6中,我们将会作进一步介绍。
arm-linux-ld命令中选项“-Ttext”也可以使用选项“-Tfilexxx”来代替,在
文件filexxx中,我们可以写出更复杂的参数来使用arm-linux-ld命令——在实验
6中,我们就是使用这种方法来指定连接参数的。
(5)实验五:MEMORY CONTROLLER
S3C2410提供了外接ROM、SRAM、SDRAM、NOR Flash、NAND Flash的接口。S3C2410
外接存储器的空间被分为8 BANKS,每BANK容量为128M:当访问BANKx(x从0到7)
所对应的地址范围(x*128M到(x+1)*128M-1,BANK6、7有稍微差别,请参考下面
第5点BANKSIZE寄存器的说明)时,片选信号nGCSx有效。本文所用的开发板,使
用了64M的NAND Flash和64M的SDRAM:NAND Flash不对应任何BANK,它是通过几
组寄存器来访问的,在上电后,NAND Flash开始的4k数据被自动地复制到芯片内
部一个被称为“Steppingstone”的RAM上。Steppingstone被映射为地址0,上面
的4k程序完成必要的初始化;SDRAM使用BANK6,它的物理起始地址为
6*128M=0x30000000。请您打开S3C2410数据手册,第5章的图“Figure 5-1.
S3C2410X Memory Map after Reset”可让您一目了然。
在开始下面内容前,如果您对SDRAM没什么概念,建议先看看这篇文章《高
手进阶,终极内存技术指南——完整/进阶版》。当然,不看也没关系,照着做就
行了。此文链接地址:
http://bbs.cpcw.com/viewthread.php?tid=196978&fpage=1&highlight=
本实验介绍如何使用SDRAM,这需要设置13个寄存器。呵呵,别担心,这些
寄存器很多是类似的,并且由于我们只使用了BANK6,大部分的寄存器我们不必
理会:
1.BWSCON:对应BANK0-BANK7,每BANK使用4位。这4位分别表示:
a.STx:启动/禁止SDRAM的数据掩码引脚,对于SDRAM,此位为0;对于
SRAM,此位为1。
b.WSx:是否使用存储器的WAIT信号,通常设为0
c.DWx:使用两位来设置存储器的位宽:00-8位,01-16位,10-32位,
11-保留。
d.比较特殊的是BANK0对应的4位,它们由硬件跳线决定,只读。
对于本开发板,使用两片容量为32Mbyte、位宽为16的SDRAM组成容量为
64Mbyte、位宽为32的存储器,所以其BWSCON相应位为:0010。对于本开发板,
BWSCON可设为0x22111110:其实我们只需要将BANK6对应的4位设为0010即可,其
它的是什么值没什么影响,这个值是参考手册上给出的。
2.BANKCON0-BANKCON5:我们没用到,使用默认值0x00000700即可
3.BANKCON6-BANKCON7:设为0x00018005
在8个BANK中,只有BANK6和BANK7可以使用SRAM或SDRAM,所以BANKCON6-7
与BANKCON0-5有点不同:
a.MT([16:15]):用于设置本BANK外接的是SRAM还是SDRAM:SRAM-0b00,
SDRAM-0b11
b.当MT=0b11时,还需要设置两个参数:
Trcd([3:2]):RAS to CAS delay,设为推荐值0b01
SCAN([1:0]):SDRAM的列地址位数,对于本开发板使用的SDRAM
HY57V561620CT-H,列地址位数为9,所以SCAN=0b01。如果使用其他
型号的SDRAM,您需要查看它的数据手册来决定SCAN的取值:00-8位,
01-9位,10-10位
4.REFRESH(SDRAM refresh control register):设为0x008e0000+ R_CNT
其中R_CNT用于控制SDRAM的刷新周期,占用REFRESH寄存器的[10:0]位,
它的取值可如下计算(SDRAM时钟频率就是HCLK):
R_CNT = 2^11 + 1 – SDRAM时钟频率(MHz) * SDRAM刷新周期(uS)
在未使用PLL时,SDRAM时钟频率等于晶振频率12MHz;SDRAM的刷新周期
在SDRAM的数据手册上有标明,在本开发板使用的SDRAM HY57V561620CT-H的
数据手册上,可看见这么一行“8192 refresh cycles / 64ms”:所以,刷
新周期=64ms/8192 = 7.8125 uS。
对于本实验,R_CNT = 2^11 + 1 – 12 * 7.8125 = 1955,
REFRESH=0x008e0000 + 1955 = 0x008e07a3
5.BANKSIZE:0x000000b2
位[7]=1:Enable burst operation
位[5]=1:SDRAM power down mode enable
位[4]=1:SCLK is active only during the access (recommended)
位[2:1]=010:BANK6、BANK7对应的地址空间与BANK0-5不同。BANK0-5
的地址空间都是固定的128M,地址范围是(x*128M)到(x+1)*128M-1,x
表示0到5。但是BANK7的起始地址是可变的,您可以从S3C2410数据手册
第5章“Table 5-1. Bank 6/7 Addresses”中了解到BANK6、7的地址范
围与地址空间的关系。本开发板仅使用BANK6的64M空间,我们可以令位
[2:1]=010(128M/128M)或001(64M/64M):这没关系,多出来的空间程序
会检测出来,不会发生使用不存在的内存的情况——后面介绍到的
bootloader和linux内核都会作内存检测。
位[6]、位[3]没有使用
6.MRSRB6、MRSRB7:0x00000030
能让我们修改的只有位[6:4](CL),SDRAM HY57V561620CT-H不支持CL=1
的情况,所以位[6:4]取值为010(CL=2)或011(CL=3)。
只要我们设置好了上述13个寄存器,往后SDRAM的使用就很简单了。本实验
先使用汇编语言设置好SDRAM,然后把程序本身从Steppingstone(还记得吗?本
节开始的时候提到过,复位之后NAND Flash开头的4k代码会被自动地复制到这里)
复制到SDRAM处,然后跳到SDRAM中执行。
本实验源代码在SDRAM目录中, head.s开头的代码如下:
1 bl disable_watch_dog
2 bl memsetup
3 bl copy_steppingstone_to_sdram
4 ldr pc, =set_sp @跳到SDRAM中继续执行
5 set_sp:
6 ldr sp, =0x34000000 @设置堆栈
7 bl main @跳转到C程序main函数
8 halt_loop:
9 b halt_loop
为了让程序结构简单一点,我都使用函数调用的方式。第一条指令是禁止
WATCH DOG,您如果细心的话,一定会发现程序LEDS运行得有些不正常,那是因
为WATCH DOG在不断地重启系统。以前为了程序简单,我没有把这段程序加上去。
往WTCON寄存器(地址0x53000000)写入0即可禁止WATCH DOG。第二条指令设置本
节开头所描述的13个寄存器,以便使用SDRAM。请您翻看实验四最后一段文字,
往下程序做的事情就是:将Steppingstone中的代码复制到SDRAM中(起始地址为
0x30000000),然后向pc寄存器直接赋值跳到SDRAM中执行下一条指令“ldr sp,
=0x34000000”。再往后的代码就和实验二、三一样了。
最后我们来看看SDRAM目录下的Makefile:
1 sdram : head.s sdram.c
2 arm-linux-gcc -c -o head.o head.s
3 arm-linux-gcc -c -o sdram.o sdram.c
4 arm-linux-ld -Ttext 0x30000000 head.o sdram.o -o sdram_tmp.o
5 arm-linux-objcopy -O binary -S sdram_tmp.o sdram
请看第4句,是否和实验四联系起来了呢?忘记的人请回头复习,不再罗嗦。
在目录SDRAM下执行make指令生成可执行文件sdram后,下载到板子上运行,
可以发现与LEDS程序相比,LED闪烁得更慢:这就对了,外部SDRAM的性能比起内
部SRAM来说性能是差些。
把程序从性能更好的内部SRAM移到外部SDRAM中去,是否多此一举呢?内部
SRAM只有4k大小,如果我们的程序大于4k,那么就不能指望利用内部SRAM来运行
了。所以得想办法把存储在NAND Flash中的代码,复制到SDRAM中去。对于NAND
Flash中的前4k,芯片自动把它复制到内部SRAM中,我们可以很轻松地再把它复
制到SDRAM中(实验五中函数copy_steppingstone_to_sdram就做这事)。但是对于
4k之后的代码,复制它就不那么轻松了,这正是实验六的内容:
(6)实验六:NAND FLASH CONTROLLER
当OM1、OM0都是低电平(请看数据手册198页)——即开发板插上BOOT SEL跳
线时,S3C2410从NAND Flash启动:NAND Flash的开始4k代码会被自动地复制到
内部SRAM中。我们需要使用这4k代码来把更多的代码从NAND Flash中读到SDRAM
中去。NAND Flash的操作通过NFCONF、NFCMD、NFADDR、NFDATA、NFSTAT和NFECC
六个寄存器来完成。在开始下面内容前,请打开S3C2410数据手册和NAND Flash
K9F1208U0M的数据手册。
在S3C2410数据手册218页,我们可以看到读写NAND Flash的操作次序:
1. Set NAND flash configuration by NFCONF register.
2. Write NAND flash command onto NFCMD register.
3. Write NAND flash address onto NFADDR register.
4. Read/Write data while checking NAND flash status by NFSTAT
register. R/nB signal should be checked before read operation or
after program operation.
下面依次介绍:
1、NFCONF:设为0xf830——使能NAND Flash控制器、初始化ECC、NAND Flash
片选信号nFCE=1(inactive,真正使用时再让它等于0)、设置TACLS、TWRPH0、
TWRPH1。需要指出的是TACLS、TWRPH0和TWRPH1,请打开S3C2410数据手册218页,
可以看到这三个参数控制的是NAND Flash信号线CLE/ALE与写控制信号nWE的时
序关系。我们设的值为TACLS=0,TWRPH0=3,TWRPH1=0,其含义为:TACLS=1个HCLK
时钟,TWRPH0=4个HCLK时钟,TWRPH1=1个HCLK时钟。请打开K9F1208U0M数据手册
第13页,在表“AC Timing Characteristics for Command / Address / Data
Input”中可以看到:
CLE setup Time = 0 ns,CLE Hold Time = 10 ns,
ALE setup Time = 0 ns,ALE Hold Time = 10 ns,
WE Pulse Width = 25 ns
可以计算,即使在HCLK=100MHz的情况下,TACLS+TWRPH0+TWRPH1=6/100 uS=60
ns,也是可以满足NAND Flash K9F1208U0M的时序要求的。
2、NFCMD:
对于不同型号的Flash,操作命令一般不一样。对于本板使用的K9F1208U0M,
请打开其数据手册第8页“Table 1. Command Sets”,上面列得一清二楚。
3、NFADDR:无话可说
4、NFDATA:只用到低8位
5、NFSTAT:只用到位0,0-busy,1-ready
6、NFECC:待补
现在来看一下如何从NAND Flash中读出数据,请打开K9F1208U0M数据手册第
29页“PAGE READ”,跟本节的第2段是遥相呼应啊,提炼出来罗列如下(设读
地址为addr):
1、NFCONF = 0xf830
2、在第一次操作NAND Flash前,通常复位一下:
NFCONF &= ~0x800 (使能NAND Flash)
NFCMD = 0xff (reset命令)
循环查询NFSTAT位0,直到它等于1
3、NFCMD = 0 (读命令)
4、这步得稍微注意一下,请打开K9F1208U0M数据手册第7页,那个表格列出
了在地址操作的4个步骤对应的地址线,A8没用到:
NFADDR = addr & 0xff
NFADDR = (addr>>9) & 0xff (注意了,左移9位,不是8位)
NFADDR = (addr>>17) & 0xff (左移17位,不是16位)
NFADDR = (addr>>25) & 0xff (左移25位,不是24位)
5、循环查询NFSTAT位0,直到它等于1
6、连续读NFDATA寄存器512次,得到一页数据(512字节)
7、NFCONF |= 0x800 (禁止NAND Flash)
本实验代码在NAND目录下,源代码为head.s、init.c和main.c。head.s调用
init.c中的函数来关WATCH DOG、初始化SDRAM、初始化NAND Flash,然后将main.c
中的代码从NAND Flash地址4096开始处复制到SDRAM中,最后,跳到main.c中的
main函数继续执行。代码本身没什么难度,与前面程序最大的差别就是“连接脚
本”的引入:实验4最后一段提到过在arm-linux-ld命令中,选项“-Ttext”可
以使用选项“-Tfilexxx”来代替。在本实验中,使用“-Tnand.lds”。nand.lds
内容如下:
1 SECTIONS {
2 firtst 0x00000000 : { head.o init.o }
3 second 0x30000000 : AT(4096) { main.o }
4 }
完整的连接脚本文件形式如下:
SECTIONS {
...
secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
{ contents } >region :phdr =fill
...
}
并非每个选项都是必须的,仅介绍nand.lds用到的:
1、secname:段名,对于nand.lds,段名为first和second
2、start:本段运行时的地址,如果没有使用AT(xxx),则本段存储的地址
也是start
3、AT( ldadr ):定义本段存储(加载)的地址
4、{ contents }:决定哪些内容放在本段,可以是整个目标文件,也可以
是目标文件中的某段(代码段、数据段等)
nand.lds的含义是:head.o放在0x00000000地址开始处,init.o放在hean.o
后面,它们的运行地址是0x00000000;main.o放在地址4096(0x1000)开始处,但
是它的运行地址在0x30000000,在运行前需要从4096处复制到0x30000000处。为
了更形象一点,您可以打开反汇编文件ttt.s,现摘取部分内容如下:
00000000 <.data>:
1 0: e3a0da01 mov sp, #4096 ; 0x1000
2 4: eb00000b bl 0x38
3 8: eb000011 bl 0x54
4 c: eb000042 bl 0x11c
...
5 1000: e1a0c00d mov ip, sp
6 1004: e92dd800 stmdb sp!, {fp, ip, lr, pc}
7 1008: e24cb004 sub fp, ip, #4 ; 0x4
8 100c: e59f1058 ldr r1, [pc, #88] ; 0x106c
...
上面的第1-4行与head.s中的前面4行代码对应,第2-4行调用init.c中的函
数disable_watch_dog、memsetup、init_nand;再看看第5行,“1000”的得来
正是由于设置了“AT(4096)”,这行开始的是main.c中的第一个函数Rand()。
如果您想进一步了解连接脚本如何编写,请参考《Using ld The GNU linker》
(在目录“参考资料”下)。
上面的几个程序都是在摆弄那几个LED,现在来玩点有意思的:
(7)实验七:UART
UART的寄存器有11X3个(3个UART)之多,我选最简单的方法来进行本实
验,用到的寄存器也有8个。不过初始化就用去了5个寄存器,剩下的3个用于接
收、发送数据。如此一来,操作UART倒也不复杂。本板使用UART0:
1、初始化:
把使用到的引脚GPH2、GPH3定义为TXD0、RXD0:
GPHCON |= 0xa0
GPHUP |= 0x0c (上拉)
b.ULCON0 ( UART channel 0 line control register ):设为0x03
此值含义为:8个数据位,1个停止位,无校验,正常操作模式(与之相对的
是Infra-Red Mode,此模式表示0、1的方式比较特殊)。
c.UCON0 (UART channel 0 control register ):设为0x05
除了位[3:0],其他位都使用默认值。位[3:0]=0b0101表示:发送、接收都
使用“中断或查询方式”——本实验使用查询查询方式。
d.UFCON0 (UART channel 0 FIFO control register ):设为0x00
每个UART内部都有一个16字节的发送FIFO和接收FIFO,但是本实验
不使用FIFO,设为默认值0
e.UMCON0 (UART channel 0 Modem control register ):设为0x00
本实验不使用流控,设为默认值0
f.UBRDIV0 ( R/W Baud rate divisior register 0 ):设为12
本实验未使用PLL, PCLK=12MHz,设置波特率为57600,则由公式
UBRDIVn = (int)(PCLK / (bps x 16) ) –1
可以计算得UBRDIV0 = 12,请使用S3C2410数据手册第314页的误差公式
验算一下此波特率是否在可容忍的误差范围之内,如果不在,则需要更换另
一个波特率(本实验使用的57600是符合的)。
2、发送数据:
a.UTRSTAT0 ( UART channel 0 Tx/Rx status register ):
位[2]:无数据发送时,自动设为1。当我们要使用串口发送数据时,
先读此位以判断是否有数据正在占用发送口。
位[1]:发送FIFO是否为空,本实验未用此位
位[0]:接收缓冲区是否有数据,若有,此位设为1。本实验中,需
要不断查询此位一判断是否有数据已经被接收。
b.UTXH0 (UART channel 0 transmit buffer register ):
把要发送的数据写入此寄存器。
3、接收数据:
a.UTRSTAT0:如同上述“2、发送数据”所列,我们用到位[0]
b.URXH0 (UART channel 0 receive buffer register ):
当查询到UTRSTAT0 位[0]=1时,读此寄存器获得串口接收到的数据。
串口代码在UART目录下的serial.c文件中,包含三个函数:init_uart,putc,
getc。代码如下:
1 void init_uart( )
2 {//初始化UART
3 GPHCON |= 0xa0; //GPH2,GPH3 used as TXD0,RXD0
4 GPHUP = 0x0c; //GPH2,GPH3内部上拉
5 ULCON0 = 0x03; //8N1(8个数据位,无校验位,1个停止位)
6 UCON0 = 0x05; //查询方式
7 UFCON0 = 0x00; //不使用FIFO
8 UMCON0 = 0x00; //不使用流控
9 UBRDIV0 = 12; //波特率为57600
10 }
11 void putc(unsigned char c)
12 {
13 while( ! (UTRSTAT0 & TXD0READY) ); //不断查询,直到可以发送数据
14 UTXH0 = c; //发送数据
15 }
16 unsigned char getc( )
17 {
18 while( ! (UTRSTAT0 & RXD0READY) ); //不断查询,直到接收到了数据
19 return URXH0; //返回接收到的数据
20 }
本实验将串口输入的数字、字母加1后再从串口输出,比如输入A就输出B。
实验步骤:
1、进入UART目录运行“make”命令生成可执行文件uart,下载到开发板上
2、在主机上运行串口工具minicom:
a.在终端上运行“minicom –s”启动minicom,出来如下界面:
b.进入“Serial port setup”:
键入相应字母设置各项,比如:我用的是串口1,所以在A项设置为
“/dev/ttyS0”;按“E”,设置波特率为57600,8N1;按F、G,设置无流控。
最后回车退出,回到步骤a所示的界面。
c.可以选择“Save setup as dfl”,这样下次启动minicom时可以不再进行
步骤b的设置。
d.选择“Exit”退出设置界面。
3、复位开发板后,您可以在minicom上体验一下本程序了。
(8)实验八:printf、scanf
本实验利用串口实现两个很常用的函数:printf和scanf。
试验代码存放在stdio目录下,其中lib目录中包含了实现printf和scanf函
数的主要文件。大部分文件摘自linux2.6内核,本试验主要在vsprintf.c文件的
基础上,封装了printf和scanf函数(print.c文件中)。在vsprintf.c文件中,需
要用到一些乘法和除法操作,文件lib1funcs.S实现除法、求模操作;div64.h
和div64.S实现64位的除法操作;muldi3.c实现乘法操作。另外,stdio目录下的
serial.c文件实现了putc和getc函数,这两个函数与具体板子的情况相关,所以
我没把它们放入lib目录下。
此试验的Makefile文件将lib目录里的文件生成静态库文件libc.a,现在,
你把stdio.h文件包含进你的代码后,就可以非常方便地实现输入、输出了——
请参考本试验代码。
实验:与试验7类似,以波特率57600 8N1打开串口,将make后生成的可执行
文件exe烧入开发板,可在minicom上观察到结果:程序从minicom中接收字串,
从这些字串中检出数字,分别以10进制和16进制方式打印出来。
另外,您可以参考stdio_test_lib目录下的代码,写出自己的测试程序。这
个目录里面的文件基本与本试验一样,只是在lib目录下仅仅保留了libc.a库文
件。
最后,sys/lib/stdio目录中存放的是标准输入、输出的代码,其中的
Makefile可以生成libc.a文件并存放在sys/lib目录下,以后本人编写的库函数
都也将存放在此目录下。
(9)实验九:INTERRUPT CONTROLLER
S3C2410数据手册354页“Figure 14-1. Interrupt Process Diagram”非常简洁地
概括了中断处理的流程,我把这个图搬过来,然后结合用到的寄存器用文字解释
一下。
图1 Interrupt Process Diagram
SUBSRCPND和SRCPND寄存器表明有哪些中断被触发了,正在等待处理
(pending);SUBMASK(INTSUBMSK寄存器)和MASK(INTMSK寄存器)用于屏蔽某些中
断。图中的“Request sources(with sub -register)”表示的是INT_RXD0、
INT_TXD0等11个中断源,它们不同于“Request sources(without sub
-register)”:
1、“Request sources(without sub -register)”中的中断源被触发之后,SRCPND
寄存器中相应位被置1,如果此中断没有被INTMSK寄存器屏蔽、或者是快中断(FIQ)
的话,它将被进一步处理
2、对于“Request sources(with sub -register)”中的中断源被触发之后,
SUBSRCPND寄存器中的相应位被置1,如果此中断没有被INTSUBMSK寄存器屏蔽的
话,它在SRCPND寄存器中的相应位也被置1,之后的处理过程就和“Request
sources(without sub -register)”一样了
继续沿着图1前进:在SRCPND寄存器中,被触发的中断的相应位被置1,等待
处理:
1、如果被触发的中断中有快中断(FIQ)——MODE(INTMOD寄存器)中为1的位对应
的中断是FIQ,则CPU的FIQ中断函数被调用。注意:FIQ只能分配一个,即INTMOD
中只能有一位设为1。
2、对于一般中断IRQ,可能同时有几个中断被触发,未被INTMSK寄存器屏蔽的中
断经过比较后,选出优先级最高的中断——此中断在INTPND寄存器中的相应位被
置1,然后CPU调用IRQ中断处理函数。中断处理函数可以通过读取INTPND寄存器
来确定中断源是哪个,也可以读INTOFFSET寄存器来确定中断源。
请打开S3C2410数据手册357页,“Figure 14-2. Priority Generating Block”显示
了各中断源先经过6个一级优先级仲裁器选出各自优先级最高的中断,然后再经
过二级优先级仲裁器选从中选出优先级最高的中断。IRQ的中断优先级由RIORITY
寄存器设定,请参考数据手册365页,RIORITY寄存器中ARB_SELn(n从0到6)用于
设定仲裁器n各输入信号的中断优先级,例如ARB_SEL6[20:19](0最高,其后各项
依次降低):
00 = REQ 0-1-2-3-4-5 01 = REQ 0-2-3-4-1-5
10 = REQ 0-3-4-1-2-5 11 = REQ 0-4-1-2-3-5
RIORITY寄存器还有一项比较特殊的功能,如果ARB_MODEn设为1,则仲裁器n
中输入的中断信号的优先级别将会轮换。例如ARB_MODE6设为1,则仲裁器6的6
个输入信号的优先级将如下轮换(见数据手册358页):
If REQ0 or REQ5 is serviced, ARB_SEL bits are not changed at all.
If REQ1 is serviced, ARB_SEL bits are changed to 01b.
If REQ2 is serviced, ARB_SEL bits are changed to 10b.
If REQ3 is serviced, ARB_SEL bits are changed to 11b.
If REQ4 is serviced, ARB_SEL bits are changed to 00b.
意思即是:
REQ0和REQ5的优先级不会改变
当REQ1中断被处理后,ARB_SEL6 = 0b01,即REQ1的优先级变成本仲裁器
中最低的(除去REQ5)
当REQ2中断被处理后,ARB_SEL6 = 0b10,即REQ2的优先级变成本仲裁器
中最低的(除去REQ5)
当REQ3中断被处理后,ARB_SEL6 = 0b11,即REQ3的优先级变成本仲裁器
中最低的(除去REQ5)
当REQ4中断被处理后,ARB_SEL6 = 0b00,即REQ4的优先级变成本仲裁器
中最低的(除去REQ5)
现在来总结一下使用中断的步骤:
1、当发生中断IRQ时,CPU进入“中断模式”,这时使用“中断模式”下的堆栈;
当发生快中断FIQ时,CPU进入“快中断模式”,这时使用“快中断模式”下的堆
栈。所以在使用中断前,先设置好相应模式下的堆栈。
2、对于“Request sources(without sub -register)”中的中断,将INTSUBMSK
寄存器中相应位设为0
3、将INTMSK寄存器中相应位设为0
4、确定使用此的方式:是FIQ还是IRQ。
a.如果是FIQ,则在INTMOD寄存器设置相应位为1
b.如果是IRQ,则在RIORITY寄存器中设置优先级
5、准备好中断处理函数,
a.中断向量:
在中断向量设置好当FIQ或IRQ被触发时的跳转函数, IRQ、FIQ的中断向量
地址分别为0x00000018、0x0000001c(数据手册79页“Table 2-3. Exception
Vectors”)
b.对于IRQ,在跳转函数中读取INTPND寄存器或INTOFFSET寄存器的值来确
定中断源,然后调用具体的处理函数
c.对于FIQ,因为只有一个中断可以设为FIQ,无须判断中断源
d.中断处理函数进入和返回时需要花点心思:
i.对于IRQ,进入和返回的代码如下:
sub lr, lr, #4 @计算返回地址
stmdb sp!, { r0-r12,lr } @保存使用到的寄存器
. .
ldmia sp!, { r0-r12,pc }^ @中断返回
@^表示将spsr的值赋给cpsr
ii.对于FIQ,进入和返回的代码如下:
sub lr, lr, #4 @计算返回地址
stmdb sp!, { r0-r7,lr } @保存使用到的寄存器
. .
ldmia sp!, { r0-r7,pc }^ @快中断返回,
@^表示将spsr的值赋给cpsr
iii. 中断返回之前需要清中断:往SUBSRCPND(用到的话)、SRCPND、
INTPND中相应位写1即可。对于INTPND,最简单的方法就是“INTPND=INTPND”。
6、设置CPSR寄存器中的F-bit(对于FIQ)或I-bit(对于IRQ)为0,开中断
本实验使用按键K1-K4作为4个外部中断——EINT1-3、EINT7,当Kn按下时,
通过串口输出“EINTn,Kn pressed!”,主程序让4个LED轮流从0到15计数。对于
外部中断,除了上面说的几个寄存器外,还有几个寄存器需要设置,请打开数据
手册276页“EXTERNAL INTERRUPT CONTROL REGISTER (EXTINTn)”:
1、EXTINT0-2:它们用于设置EINT0-24共25个外部中断分别是低电平触发、高电
平触发、上升沿触发、下降沿触发或者“上升/下降沿触发”(不知道确切的术语)。
对于本实验,使用默认值:低电平触发。
2、EINTFLT0-3:未用
3、EINTMASK、EINTPEND:呵呵,这对寄存器和上面的INTMSK、INTPND实在相似。
EINTMASK用于设置是否评比EINT4-23;EINTPEND表明有几个外部中断已经发生,
正在等待处理(pending),对某位写入1可以让此位清零。对于本实验,EINTMASK
位[7]设为0——使用EINT7。
4、GSTATUS0-4:未用
本实验代码在INT目录下,下面摘取与中断相关的代码:
(1)、head.s中:
. .
1 msr cpsr_c, #0xd2 @进入中断模式
2 ldr sp, =0x33000000 @设置中断模式堆栈
3 msr cpsr_c, #0xdf @进入系统模式
4 ldr sp, =0x34000000 @设置系统模式堆栈
5 bl init_irq @调用中断初始化函数,在init.c中
6 msr cpsr_c, #0x5f @设置I-bit=0,开IRQ中断
. .
7 HandleIRQ: @IRQ中断向量跳转函数
8 sub lr, lr, #4 @计算返回地址
9 stmdb sp!, { r0-r12,lr } @保存使用到的寄存器
10 ldr lr, =int_return @设置返回地址
11 ldr pc, =EINT_Handle @调用中断处理函数EINT_Handle,
@在interrupt.c中
12 int_return:
13 ldmia sp!, { r0-r12,pc }^ @中断返回,
@^表示将spsr的值复制到cpsr
(2)、init.c的中断初始化函数init_irq:
1 #define EINT1 (2<<(1*2))
2 #define EINT2 (2<<(2*2))
3 #define EINT3 (2<<(3*2))
4 #define EINT7 (2<<(7*2))
5 void init_irq( )
6 {
7 GPFCON |= EINT1 | EINT2 | EINT3 | EINT7; //K1-K4对应
//EINT1-3和EINT7
8 GPFUP |= (1<<1) | (1<<2) | (1<<3) | (1<<7); //上拉
9 EINTMASK &= (~0x80); //EINT7使能,对于外部中断EINT4-23,
//除下面的INTMSK外,还须设置EINTMASK
10 INTMSK &= (~0x1e); //EINT1-3、4-7使能(EINT4-7共用INTMSK[4])
11 PRIORITY &= (~0x03); //设定优先级
12 }
(3)、interrupt.c:
1 void EINT_Handle()
2 {
3 unsigned long oft = INTOFFSET;
4 switch( oft )
5 {
6 case 1: printk("EINT1, K1 pressed!/n/r"); break;
7 case 2: printk("EINT2, K2 pressed!/n/r"); break;
8 case 3: printk("EINT3, K3 pressed!/n/r"); break;
9 case 4: printk("EINT7, K4 pressed!/n/r"); break;
10 default: printk("Interrupt unknown!/n/r"); break;
11 }
//以下清中断
12 if( oft == 4 ) EINTPEND = 1<<7; //EINT4-7合用IRQ4,
//注意:EINTPEND[3:0]保留未用,
//向这些位写入1可能导致未知结果
14 SRCPND = 1<
15 INTPND = INTPND; //清0
16 }
实验步骤:
1、进入INT目录运行“make”命令生成可执行文件int,下载到板子上
2、打开minicom(拨特率:57600,8N1)
3、按下按钮即可看见串口输出
4、您可以同时按住几个按钮观察串口输出,可以发现K1优先级最高,K4最低
(10)实验十:TIMER
本实验利用TIMER0触发中断,使4个LED 1秒钟闪一次。先简述一下TIMER的
工作过程。请打开数据手册284页,我在“Figure 10-1. 16-bit PWM Timer Block
Diagram”上进行解说。摘录此图,见下面图3。
对于TIMER0,PCLK被“8-Bit Prescaler”(在TCFG0中定义)分频后,进入“Clock
Divider”,“Clock Divider”有5种频率输出:从“8-Bit Prescaler”进来的
时钟的2分频、4分频、8分频、16分频,或者外部时钟TCLK0。后面的“5:1 MUX”
(5选1的选择器)使用TCFG1来确定使用哪种频率。“Control Logic0”在“5:1 MUX”
的输出的频率下工作。对于“Control Logic0”,我得画图说明:
图2 Control Logic0内部结构
“Control Logic0”的工作流程为(请参考数据手册287页“TIMER
INITIALIZATION USING MANUAL UPDATE BIT AND INVERTER BIT”):
1、上电之后,设置TCMPB0和TCNTB0寄存器
2、设置TCON寄存器bit[1]为1,这样,TCMPB0和TCNTB0寄存器的值就被装入TCMP0
和TCNT0寄存器中;设置bit[2]确定是否使用“Timer 0 output inverter”
3、设置TCON寄存器bit[0]启动TIMER0,设置bit[3]确定是否使用“Timer 0 auto
reload”
4、当TIMER0启动后,TCNT0在“5:1 MUX”输出的时钟下,进行减1计数。当TCNT0
等于TCMP0时:TOUT0输出翻转;当TCNT0等于0时:TOUT0输出翻转,TIMER0中断
被触发(如果此中断使能了的话),TCMPB0和TCNTB0寄存器的值被自动装入TCMP0
和TCNT0寄存器中(如果TCON寄存器bit[3]等于1的话)——此时下个计数流程开
始
图3 16-bit PWM Timer Block Diagram
现在介绍一下上面说到的几个寄存器:
1、TCFG0和TCFG1:分别设为119和0x03
这连个寄存器用于设置“Control Logic”的时钟,计算公式如下:
Timer input clock Frequency = PCLK / {prescaler value+1} / {divider value}
对于TIMER0,prescaler value = TCFG0[7:0],divider value由TCFG[3:0]
确定(0b000:2,0b001:4,0b010:8,0b0011:16,0b01xx:使用外部TCLK0)。
对于本实验,TIMER0时钟 = 12MHz/(119+1)/(16) = 6250Hz
2、TCNTB0:设为3125
在6250Hz的频率下,此值对应的时间为0.5S
3、TCON:
TIMER0对应bit[3:0]:
bit[3]用于确定在TCNT0计数到0时,是否自动将TCMPB0和TCNTB0寄存器
的值装入TCMP0和TCNT0寄存器中
bit[2]用于确定TOUT0是否反转输出(本实验未用)
bit[1]用于手动更新TCMP0和TCNT0寄存器:在第一次使用定时器前,此
位需要设为1,此时TCMPB0和TCNTB0寄存器的值装入TCMP0和TCNT0寄存器中
bit[0]用于启动TIMER0
4、TCONO0:只读寄存器,用于读取当前TCON0寄存器的值,本实验未用
本实验的代码在TIMER目录下,init.c中的Timer0_init函数初始化并启动
TIMER0:
1 void Timer0_init()
2 {
3 TCFG0 = 119; //Prescaler0 = 119
4 TCFG1 = 0x03; //Select MUX input for PWM Timer0:divider=16
5 TCNTB0 = 3125; //0.5秒钟触发一次中断
6 TCON |= (1<<1); //Timer 0 manual update
7 TCON = 0x09; /*Timer 0 auto reload on
Timer 0 output inverter off
清"Timer 0 manual update"
Timer 0 start */
8 }
init.c中的init_irq函数使能TIEMR0中断:
1 void init_irq( )
2 {
3 INTMSK &= (~(1<<10)); //INT_TIMER0中断使能
4 }
interrupt.c中的Timer0_Handle函数用于处理TIMER0中断:每0.5s发生一次
中断,中断发生时将4个LED的状态反转,即1s闪一次:
1 void Timer0_Handle()
2 {
3 if(INTOFFSET == 10){
4 GPBDAT = ~(GPBDAT & (0xf << 7));
5 }
6 //清中断
7 SRCPND = 1 << INTOFFSET;
8 INTPND = INTPND;
9 }
(11)实验十一:MMU
在理论上概括或解释MMU,这不是我能胜任的。我仅基于为了理解本实验中
操作MMU的代码而对MMU做些说明,现在先简单地描述虚拟地址(VA)、变换后的虚
拟地址(MVA)、物理地址(PA)之间的关系:
启动MMU后,S3C2410的CPU核看到的、用到的只是虚拟地址VA,至于VA如何
最终落实到物理地址PA上,CPU是不理会的。而caches和MMU也是看不见VA的,它
们利用VA变换得来的MVA去进行后续操作——转换成PA去读/写实际内存芯片,
MVA是除CPU外的其他部分看见的虚拟地址。对于VA与MVA之间的变换关系,请打
开数据手册551页,我摘取了“Figure 2-8. Address Mapping Using CP15 Register 13”:
图4 VA与MVA的关系
如果VA<32M,需要使用进程标识号PID(通过读CP15的C13获得)来转换为MVA。
VA与MVA的转换方法如下(这是硬件自动完成的):
if(VA < 32M) then
MVA = VA | (PID << 25) //VA < 32M
else
MVA = VA //VA >= 32M
利用PID生成MVA的目的是为了减少切换进程时的代价:如果两个进程占用的
虚拟地址空间(VA)有重叠,不进行上述处理的话,当进行进程切换时必须进行虚
拟地址到物理地址的重新影射,这需要重建页表、使无效caches和TLBS等等,代
价非常大。但是如果像上述那样处理的话,进程切换就省事多了:假设两个进程
1、2运行时的VA都是0-32M,则它们的MVA分别是(0x02000000-0x03ffffff)、
(0x04000000-0x05ffffff)——前面说过MMU、Caches使用MVA而不使用VA,这样
就不必进行重建页表等工作了。
现在来讲讲MVA到PA的变换过程:请打开数据手册557页,“Figure 3-1.
Translating Page Tables”(见下述图5)非常精练地概括了对于不同类型的页表,MVA
是如何转换为PA的。图中的页表“Translation table”起始地址为“TTB base”,
在建立页表后,写入CP15的寄存器C2。
使用MVA[31:20]检索页表“Translation table”得到一个页表项(entry,4
字节),根据此entry的低2位,可分为以下4种:
1、0b00:无效
2、0b01:粗表(Coarse page)
entry[31:10]为粗表基址(Coarse page table base address),据此可以确
定一块1K大小的内存——称为粗页表(Coarse page table,见图5)。
粗页表含256个页表项,每个页表项对应一块4K大小的内存,每个页表项又
可以分为大页描述符、小页描述符。MVA[19:12]用来确定页表项。一个大页(64K)
对应16个大页描述符,这16个大页描述符相邻且完全相同,entry[31:16]为大页
基址(Large page base)。MVA[15:0]是大页内的偏移地址。一个小页(4K)对应1
个小页描述符,entry[31:12]为小页基址(Small page base)。MVA[11:0]是小页
内的偏移地址。
3、0b10:段(Section)
段的操作最为简单,entry[31:20]为段基址(Section base),据此可以确定
一块1M大小的内存(Section,见图5),而MVA[19:0]则是块内偏移地址
4、0b11:细表(Fine page)
entry[31:12]为细表基址(Fine page table base address),据此可以确定
一块4K大小的内存——称为细页表(Fine page table,见图5)。
细页表含1024个页表项,每个页表项对应一块1K大小的内存,每个页表项又
可以分为大页描述符、小页描述符、极小页描述符。MVA[19:10]用来确定页表项。
一个大页(64K)对应64个大页描述符,这64个大页描述符相邻且完全相同,
entry[31:16]为大页基址(Large page base)。MVA[15:0]是大页内的偏移地址。
一个小页(4K)对应4个小页描述符,entry[31:12]为小页基址(Small page base)。
MVA[11:0]是小页内的偏移地址。极小页(1K)对应1个极小页描述符,entry[31:10]
为极小页基址(Tiny page base)。MVA[9:0]是极小页内的偏移地址。
图5 Translating Page Tables
访问权限的检查是MMU主要功能之一,它由描述符的AP和domain、CP15寄存
器C1的R/S/A位、CP15寄存器C3(域访问控制)等联合作用。本实验不使用权限检
查(令C3为全1)。
下面简单介绍一下使用Cache和Write buffer:
1、“清空”(clean)的意思是把Cache或Write buffer中已经脏的(修改过,但未
写入主存)数据写入主存
2、“使无效”(invalidate):使之不能再使用,并不将脏的数据写入主存
3、对于I/O影射的地址空间,不使用Cache和Write buffer
4、在使用MMU前,使无效Cache和drain write buffer
与cache类似,在使用MMU前,使无效TLB。
上面有些部分讲得很简略,除了作者水平不足之外,还在于本书的侧重点—
—实验。理论部分就麻烦各位自己想办法了。不过这些内容也足以了解本实验的
代码了。本实验与实验9完成同样的功能:使用按键K1-K4作为4个外部中断——
EINT1-3、EINT7,当Kn按下时,通过串口输出“EINTn,Kn pressed!”,主程序
让4个LED轮流从0到15计数。代码在目录MMU下。下面摘取与MMU相关的代码详细
说明。
先看看head.s代码(将一些注释去掉了):
1 b Reset
2 HandleUndef:
3 b HandleUndef
4 HandleSWI:
5 b HandleSWI
6 HandlePrefetchAbort:
7 b HandlePrefetchAbort
8 HandleDataAbort:
9 b HandleDataAbort
10 HandleNotUsed:
11 b HandleNotUsed
12 ldr pc, HandleIRQAddr
13 HandleFIQ:
14 b HandleFIQ
15 HandleIRQAddr:
16 .long HandleIRQ
17 Reset: @函数disable_watch_dog, memsetup, init_nand,
@nand_read_ll在init.c中定义
18 ldr sp, =4096 @设置堆栈
19 bl disable_watch_dog @关WATCH DOG
20 bl memsetup_2 @初始化SDRAM
21 bl init_nand @初始化NAND Flash
22 bl copy_vectors_from_nand_to_sdram @在init.c中
23 bl copy_process_from_nand_to_sdram @在init.c中
24 ldr sp, =0x30100000 @重新设置堆栈
@(因为下面就要跳到SDRAM中执行了)
25 ldr pc, =run_on_sdram @跳到SDRAM中
26 run_on_sdram:
27 bl mmu_tlb_init @调用C函数mmu_tlb_init(mmu.c中),建立页表
28 bl mmu_init @调用C函数mmu_init(mmu.c中),使能MMU
29 msr cpsr_c, #0xd2 @进入中断模式
30 ldr sp, =0x33000000 @设置中断模式堆栈
31 msr cpsr_c, #0xdf @进入系统模式
32 ldr sp, =0x30100000 @设置系统模式堆栈
33 bl init_irq @调用中断初始化函数,在init.c中
34 msr cpsr_c, #0x5f @设置I-bit=0,开IRQ中断
35 ldr lr, =halt_loop @设置返回地址
36 ldr pc, =main @b指令和bl指令只能前后跳转32M的范围,
@所以这里使用向pc赋值的方法进行跳转
37 halt_loop:
38 b halt_loop
39 HandleIRQ:
40 sub lr, lr, #4 @计算返回地址
41 stmdb sp!, { r0-r12,lr } @保存使用到的寄存器
42 ldr lr, =int_return @设置返回地址
43 ldr pc,=EINT_Handle @调用中断处理函数,在interrupt.c中
44 int_return:
45 ldmia sp!, { r0-r12,pc }^ @中断返回,
@^表示将spsr的值复制到cpsr
请注意第12、15行,我们将IRQ中断向量由以前的“b HandleIRQ”换成了:
12 ldr pc, HandleIRQAddr
15 HandleIRQAddr:
16 .long HandleIRQ
这是因为b跳转指令只能前后跳转32M的范围,而本实验中中断向量将重新放
在VA=0xffff0000开始处(而不是通常的0x00000000),到HandleIRQAddr的距离远
远超过了32M。将中断向量重新定位在0xffff0000处,是因为MMU使能后,中断发
生时:
1、如果中断向量放在0x00000000处,则对于不同的进程(PID),中断向量的MVA
将不同
2、如果中断向量放在0xffff0000处,则对于不同的进程(PID),中断向量的MVA
也相同
显然,如果使用1,则带来的麻烦非常大——对于每个进程,都得设置自己
的中断向量。所以MMU使能后,处理中断的方法应该是2。
第22行copy_vectors_from_nand_to_sdram函数将中断向量复制到内存物理
地址0x33ff0000处,在mmu_tlb_init函数中会把0x33ff0000影射为虚拟地址
0xffff0000。
第23行copy_process_from_nand_to_sdram函数将存在nand flash开头的4K
代码全部复制到0x30004000处(本实验的连接地址为0x30004000)。请注意SDRAM
起始地址为0x30000000,前面的16K空间用来存放一级页表(在mmu_tlb_init中设
置)。
第27行mmu_tlb_init函数设置页表。本实验以段的方式使用内存,所以仅使
用一级页表,且页表中所有页表项均为段描述符。
mmu_tlb_init代码(在mmu.c中)如下:
1 void mmu_tlb_init()
2 {
3 unsigned long entry_index;
4 /*SDRAM*/
5 for(entry_index = 0x30000000; entry_index < 0x34000000;
entry_index+=0x100000){
6 /*section table's entry:AP=0b11,domain=0,Cached,write-through mode(WT)*/
7 *(mmu_tlb_base+(entry_index>>20)) =
entry_index |(0x03<<10)|(0<<5)|(1<<4)|(1<<3)|0x02;
8 }
9 /*SFR*/
10 for(entry_index = 0x48000000; entry_index < 0x60000000;
entry_index += 0x100000){
11 /*section table's entry:AP=0b11,domain=0,NCNB*/
12 *(mmu_tlb_base+(entry_index>>20)) =
entry_index |(0x03<<10)|(0<<5)|(1<<4)| 0x02;
13 }
14 /*exception vector*/
15 /*section table's entry:AP=0b11,domain=0,Cached,write-through mode(WT)*/
16 *(mmu_tlb_base+(0xffff0000>>20)) =
(VECTORS_PHY_BASE) |(0x03<<10)|(0<<5)|(1<<4)|(1<<3)|0x02;
17 }
第4-8行令64M SDRAM的虚拟地址和物理地址相等——从0x30000000到
0x33ffffff,这样可以使得在head.s中第28行调用mmu_init使能MMU前后的地址
一致。
第9-13行设置特殊功能寄存器的虚拟地址,也让它们的虚拟地址和物理地址
相等——从0x48000000到0x5fffffff。并且不使用cache和write buffer。
第14-17行设置中断向量的虚拟地址,虚拟地址0xfff00000对应物理地址
0x33f00000。
回到head.s中第28行,调用mmu.c中的mmu_init函数使能MMU,此函数代码入
下:
1 void mmu_init()
2 {
3 unsigned long ttb = MMU_TABLE_BASE;
4 __asm__(
5 "mov r0, #0/n"
6 /* invalidate I,D caches on v4 */
7 "mcr p15, 0, r0, c7, c7, 0/n"
8 /* drain write buffer on v4 */
9 "mcr p15, 0, r0, c7, c10, 4/n"
10 /* invalidate I,D TLBs on v4 */
11 "mcr p15, 0, r0, c8, c7, 0/n"
12 /* Load page table pointer */
13 "mov r4, %0/n"
14 "mcr p15, 0, r4, c2, c0, 0/n"
15 /* Write domain id (cp15_r3) */
16 "mvn r0, #0/n" /*0b11=Manager,不进行权限检查*/
17 "mcr p15, 0, r0, c3, c0, 0/n"
18 /* Set control register v4 */
19 mrc p15, 0, r0, c1, c0, 0/n"
20 /* Clear out 'unwanted' bits */
21 "ldr r1, =0x1384/n"
22 "bic r0, r0, r1/n"
23 /* Turn on what we want */
24 /*Base location of exceptions = 0xffff0000*/
25 "orr r0, r0, #0x2000/n"
26 /* Fault checking enabled */
27 "orr r0, r0, #0x0002/n"
28 #ifdef CONFIG_CPU_D_CACHE_ON /*is not set*/
29 "orr r0, r0, #0x0004/n"
30 #endif
31 #ifdef CONFIG_CPU_I_CACHE_ON /*is not set*/
32 "orr r0, r0, #0x1000/n"
33 #endif
34 /* MMU enabled */
35 "orr r0, r0, #0x0001/n"
36 /* write control register *//*write control register P545*/
37 "mcr p15, 0, r0, c1, c0, 0/n"
38 : /* no outputs */
39 : "r" (ttb) );
40 }
此函数使用嵌入汇编的方式,第29行的"r" (ttb)表示变量ttb的值赋给一个
寄存起作为输入参数,这个寄存器由编译器自动分配;第13行的“%0”表示这个
寄存器。MMU控制寄存器C1中各位的含义(第18-37行),可以参考539页“Table 2-10.
Control Register 1-bit Functions”。如果想详细了解本函数用到的操作协处理器的
指令,可以参考数据手册529页“Appendix 2 PROGRAMMER'S MODEL”。
本实验代码在CLOCK目录下,运行make命令后将可执行文件mmu下载、运行。
然后将mmu.h文件中如下两行的注释去掉,重新make后下载运行mmu,可以发现LED
闪烁的速度变快了很多——这是因为使用了cache(见上面代码28-33行):
#define CONFIG_CPU_D_CACHE_ON 1
#define CONFIG_CPU_I_CACHE_ON 1
(12)实验十二:CLOCK
S3C2410 CPU主频可以达到266MHz,前面的实验都没有使用PLL,CPU的频率
只有12MHz。本实验在实验10的基础上启动PLL,使得FCLK=200MHz,HCLK=100MHz,
PCLK=50MHz。
请打开数据手册219页第7章“CLOCK & POWER MANAGEMENT”。S3C2410有两
个PLL:MPLL和UPLL,UPLL专用与USB设备,本实验介绍的是MPLL——用于设置
FCLK、HCLK、PLCK。FCLK用于CPU核,HCLK用于AHB总线的设备(比如SDRAM),PCLK
用于APB总线的设备(比如UART)。请打开数据手册224页,“Figure 7-4. Power-On
Reset Sequence (when the external clock source is a crystal oscillator)”展示了上电
后, MPLL启动的过程,摘录此图如下:
图6 上电后MPLL的启动过程
请跟随FCLK的图像了解启动过程:
1、上电几毫秒后,晶振输出稳定,FCLK=晶振频率,nRESET信号恢复高电平后,
CPU开始执行指令。
2、我们可以在程序开头启动MPLL,在设置MPLL的几个寄存器后,需要等待一段
时间(Lock Time),MPLL的输出才稳定。在这段时间(Lock Time)内,FCLK停振,
CPU停止工作。Lock Time的长短由寄存器LOCKTIME设定。
3、Lock Time之后,MPLL输出正常,CPU工作在新的FCLK下。
本实验的工作就是设置MPLL的几个寄存器:
1、LOCKTIME:设为0x00ffffff
前面说过,MPLL启动后需要等待一段时间(Lock Time),使得其输出稳定。
位[23:12]用于UPLL,位[11:0]用于MPLL。本实验使用确省值0x00ffffff。
2、CLKDIVN:设为0x03
用于设置FCLK、HCLK、PCLK三者的比例:
bit[2]——HDIVN1,若为1,则bit[1:0]必须设为0b00,此时
FCLK:HCLK:PCLK=1:1/4:1/4;若为0,三者比例由bit[1:0]确定
bit[1]——HDIVN,0:HCLK=FCLK;1:HCLK=FCLK/2
bit[0]——PDIVN,0:PCLK=HCLK;1:PCLK=HCLK/2
本实验设为0x03,则FCLK:HCLK:PCLK=1:1/2:1/4
3、请翻到数据手册226页,有这么一段:
If HDIVN = 1, the CPU bus mode has to be changed from the fast bus
mode to the asynchronous bus mode using following instructions:
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 = 1时,
CPU bus mode需要从原来的“fast bus mode”改为“asynchronous bus mode”。
4、MPLLCON:设为(0x5c << 12)|(0x04 << 4)|(0x00),即0x5c0040
对于MPLLCON寄存器,[19:12]为MDIV,[9:4]为PDIV,[1:0]为SDIV。有如下
计算公式:
MPLL(FCLK) = (m * Fin)/(p * 2^s)
其中: m = MDIV + 8, p = PDIV + 2
对于本开发板,Fin = 12MHz,MPLLCON设为0x5c0040,可以计算出
FCLK=200MHz,再由CLKDIVN的设置可知:HCLK=100MHz,PCLK=50MHz。
当设置MPLLCON之后——相当于图6中的“PLL is configured by S/W first
time”,Lock Time就被自动插入,Lock Time之后,MPLL输出稳定,CPU工作在
200MHz频率下。
上面的4个步骤的工作在init.c中的clock_init函数中完成,代码如下:
1 void clock_init()
2 {
3 LOCKTIME = 0x00ffffff;
4 CLKDIVN = 0x03; /*FCLK:HCLK:PCLK=1:2:4,
5 HDIVN1=0,HDIVN=1,PDIVN=1 */
6 /*If HDIVN = 1,the CPU bus mode has to be changed from
the fast bus mode to the asynchronous bus mod using
following instructions.*/
7 __asm__(
8 "mrc p15, 0, r1, c1, c0, 0/n" /* read ctrl register */
9 "orr r1, r1, #0xc0000000/n" /* Asynchronous */
10 "mcr p15, 0, r1, c1, c0, 0/n" /* write ctrl register */
11 );
12 MPLLCON = MPLL_200MHz; /*现在,
FCLK=200MHz,HCLK=100MHz,PCLK=50MHz*/
13 }
在head.s中,在memsetup_2函数之前调用clock_init函数。
本实验是在实验10的基础上修改得来,当工作频率改变后,一些设备的初始
化参数需要调整,很幸运,只需要修改两个地方:
1、SDRAM控制器的REFRESH寄存器(在init.c中):
请翻看实验五:MEMORY CONTROLLER中关于REFRESH寄存器的介绍,本实验
HCLK=100MHz,REFRESH寄存器取值如下:
R_CNT = 2^11 + 1 – 100 * 7.8125 = 1268,
REFRESH=0x008e0000 + 1268 = 0x008e04f4
在init.c中的memsetup_2函数,将原来的0x008e07a3换成0x008e04f4即可。
2、UART0的UBRDIV0寄存器(在serial.c中):
请翻看实验七:UART中关于UBRDIV0的介绍,本实验PCLK=50MHz,设置波特
率为57600时,UART0寄存器取值可由下式计算得53:
UBRDIVn = (int)(PCLK / (bps x 16) ) –1
在serial.c中的init_uart函数,将UART0由原来的12换成53即可。
本实验代码在CLOCK目录下,运行make命令后将可执行文件clock下载、运行,
可以发现LED闪烁的速度变快了很多。然后将mmu.h文件中如下两行的注释去掉,
重新make后下载运行clock,可以发现LED闪烁得更快了,简直分辨不出来了——
这是因为使用了cache:
#define CONFIG_CPU_D_CACHE_ON 1
#define CONFIG_CPU_I_CACHE_ON 1
四.Bootloader vivi
为了将linux移植到ARM上,碰到的第一个程序就是bootloader,我选用韩国
mizi公司的vivi。您可以在以下地址下载:
http://www.mizi.com/developer/s3c2410x/download/vivi.html
如果您对bootloader没有什么概念,在学习VIVI的代码之前,建议您阅读一
篇文章《嵌入式系统 Boot Loader 技术内幕》(詹荣开著)。链接地址如下:
http://www-128.ibm.com/developerworks/cn/linux/l-btloader/
当您阅读了上述文章后,我再企图在理论上罗嗦什么就不合适了(这篇文章
实在太好了)。vivi也可以分为2个阶段,阶段1的代码在arch/s3c2410/head.S
中,阶段2的代码从init/main.c的main函数开始。您可以跳到实验部分先感受一
下vivi。
(1)阶段1:arch/s3c2410/head.S
沿着代码执行的顺序,head.S完成如下几件事情:
1、关WATCH DOG:上电后,WATCH DOG默认是开着的
2、禁止所有中断:vivi中没用到中断(不过这段代码实在多余,上电后中断默认
是关闭的)
3、初始化系统时钟:启动MPLL,FCLK=200MHz,HCLK=100MHz,PCLK=50MHz,“CPU
bus mode”改为“Asynchronous bus mode”。请参考实验十一:CLOCK
4、初始化内存控制寄存器:还记得那13个寄存器吗?请复习实验五:MEMORY
CONTROLLER
5、检查是否从掉电模式唤醒,若是,则调用WakeupStart函数进行处理——这是
一段没用上的代码,vivi不可能进入掉电模式
6、点亮所有LED
7、初始化UART0:
a.设置GPIO,选择UART0使用的引脚
b.初始化UART0,设置工作方式(不使用FIFO)、波特率115200 8N1、无流控
等,请参考实验七:UART
8、将vivi所有代码(包括阶段1和阶段2)从nand flash复制到SDRAM中:
a.设置nand flash控制寄存器
b.设置堆栈指针——调用C函数时必须先设置堆栈
c.设置即将调用的函数nand_read_ll的参数:r0=目的地址(SDRAM的地址),
r1=源地址(nand flash的地址),r2=复制的长度(以字节为单位)
d.调用nand_read_ll进行复制
e.进行一些检查工作:上电后nand flash最开始的4K代码被自动复制到一
个称为“Steppingstone”的内部RAM中(地址为0x00000000-0x00001000);
在执行nand_read_ll之后,这4K代码同样被复制到SDRAM中(地址为
0x33f00000-0x33f01000)。比较这两处的4K代码,如果不同则表示出错
9、跳到bootloader的阶段2运行——就是调用init/main.c中的main函数:
a.重新设置堆栈
b.设置main函数的参数
c.调用main函数
如果您做了第二章的各个实验,理解head.S就不会有任何困难——上面说到
的几个步骤,都有相应的实验。head.S有900多行,把它搬到这篇文章上来就太
不厚道了——白白浪费页数而已。在vivi/arch/s3c2410/head.S上,我做了比较
详细的注释(其中的乱码可能是韩文,不去管它)。
当执行完head.S的代码后,内存的使用情况如下:
图7 执行vivi stage1后内存的划分情况
(2)阶段2:init/main.c
本阶段从init/main.c中的main函数开始执行,它可以分为8个步骤。我先把
main函数的代码罗列如下(去掉了乱码——可能是韩文,现在没留下多少有用的
注释了),然后逐个分析:
1 int main(int argc, char *argv[])
2 {
3 int ret;
4 /*Step 1*/
5 putstr("/r/n");
6 putstr(vivi_banner);
7 reset_handler();
8 /*Step 2*/
9 ret = board_init();
10 if (ret) {
11 putstr("Failed a board_init() procedure/r/n");
12 error();
13 }
14 /*Step 3*/
15 mem_map_init();
16 mmu_init();
17 putstr("Succeed memory mapping./r/n");
18 /* Now, vivi is running on the ram. MMU is enabled.*/
19 /*Step 4*/
20 /* initialize the heap area */
21 ret = heap_init();
22 if (ret) {
23 putstr("Failed initailizing heap region/r/n");
24 error();
25 }
26 /*Step 5*/
27 ret = mtd_dev_init();
28 /*Step 6*/
29 init_priv_data();
30 /*Step 7*/
31 misc();
32 init_builtin_cmds();
33 /*Step 8*/
34 boot_or_vivi();
35 return 0;
36 }
1、Step 1:reset_handler()
reset_handler用于将内存清零,代码在lib/reset_handle.c中。
[main(int argc, char *argv[]) > reset_handler()]
1 void
2 reset_handler(void)
3 {
4 int pressed;
5 pressed = is_pressed_pw_btn(); /*判断是硬件复位还是软件复位*/
6 if (pressed == PWBT_PRESS_LEVEL) {
7 DPRINTK("HARD RESET/r/n");
8 hard_reset_handle(); /*调用clear_mem对SDRAM清0*/
9 } else {
10 DPRINTK("SOFT RESET/r/n");
11 soft_reset_handle(); /*此函数为空*/
12 }
13 }
在上电后,reset_handler调用第8行的hard_reset_handle(),此函数在
lib/reset_handle.c中:
[main(int argc, char *argv[]) > reset_handler() > hard_reset_handle()]
1 static void
2 hard_reset_handle(void)
3 {
4 #if 0
5 clear_mem((unsigned long)(DRAM_BASE + VIVI_RAM_ABS_POS), /
6 (unsigned long)(DRAM_SIZE - VIVI_RAM_ABS_POS));
7 #endif
8 /*lib/memory.c,将起始地址为USER_RAM_BASE,长度为USER_RAM_SIZE的内存清0*/
9 clear_mem((unsigned long)USER_RAM_BASE, (unsigned long)USER_RAM_SIZE);
10 }
2、Step 2:board_init()
board_init调用2个函数用于初始化定时器和设置各GPIO引脚功能,代码在
arch/s3c2410/smdk.c中:
[main(int argc, char *argv[]) > board_init()]
1 int board_init(void)
2 {
3 init_time(); /*arch/s3c2410/proc.c*/
4 set_gpios(); /*arch/s3c2410/smdk.c*/
5 return 0;
6 }
init_time()只是简单的令寄存器TCFG0 = 0xf00,vivi未使用定时器,这个
函数可以忽略。
set_gpios()用于选择GPA-GPH端口各引脚的功能及是否使用各引脚的内部
上拉电阻,并设置外部中断源寄存器EXTINT0-2(vivi中未使用外部中断)。
3、Step 3:建立页表和启动MMU
mem_map_init函数用于建立页表,vivi使用段式页表,只需要一级页表。它
调用3个函数,代码在arch/s3c2410/mmu.c中:
[main(int argc, char *argv[]) > mem_map_init(void)]
1 void mem_map_init(void)
2 {
3 #ifdef CONFIG_S3C2410_NAND_BOOT /* CONFIG_S3C2410_NAND_BOOT=y */
4 mem_map_nand_boot(); /* 最终调用mem_mepping_linear,建立页表 */
5 #else
6 mem_map_nor();
7 #endif
8 cache_clean_invalidate(); /* 清空cache,使无效cache */
9 tlb_invalidate(); /* 使无效快表TLB */
10 }
第9、10行的两个函数可以不用管它,他们做的事情在下面的mmu_init函数
里又重复了一遍。对于本开发板,在.config中定义了
CONFIG_S3C2410_NAND_BOOT。mem_map_nand_boot()函数调用
mem_mapping_linear()函数来最终完成建立页表的工作。页表存放在SDRAM物理
地址0x33dfc000开始处,共16K:一个页表项4字节,共有4096个页表项;每个页
表项对应1M地址空间,共4G。mem_map_init先将4G虚拟地址映射到相同的物理地
址上,NCNB(不使用cache,不使用write buffer)——这样,对寄存器的操作跟
未启动MMU时是一样的;再将SDRAM对应的64M空间的页表项修改为使用cache。
mem_mapping_linear函数的代码在arch/s3c2410/mmu.c中:
[main(int argc, char *argv[]) > mem_map_init(void) > mem_map_nand_boot( )
> mem_mapping_linear(void)]
1 static inline void mem_mapping_linear(void)
2 {
3 unsigned long pageoffset, sectionNumber;
4 putstr_hex("MMU table base address = 0x", (unsigned long)mmu_tlb_base);
5 /* 4G 虚拟地址映射到相同的物理地址. not cacacheable, not bufferable */
6 /* mmu_tlb_base = 0x33dfc000*/
7 for (sectionNumber = 0; sectionNumber < 4096; sectionNumber++) {
8 pageoffset = (sectionNumber << 20);
9 *(mmu_tlb_base + (pageoffset >> 20)) = pageoffset | MMU_SECDESC;
10 }
11 /* make dram cacheable */
12 /* SDRAM物理地址0x3000000-0x33ffffff,
13 DRAM_BASE=0x30000000,DRAM_SIZE=64M
14 */
15 for (pageoffset = DRAM_BASE; pageoffset < (DRAM_BASE+DRAM_SIZE); /
16 pageoffset += SZ_1M) {
17 //DPRINTK(3, "Make DRAM section cacheable: 0x%08lx/n", pageoffset);
18 *(mmu_tlb_base + (pageoffset >> 20)) = /
pageoffset | MMU_SECDESC | MMU_CACHEABLE;
19 }
20 }
mmu_init()函数用于启动MMU,它直接调用arm920_setup()函数。
arm920_setup()的代码在arch/s3c2410/mmu.c中:
[main(int argc, char *argv[]) > mmu_init( ) > arm920_setup( )]
1 static inline void arm920_setup(void)
2 {
3 unsigned long ttb = MMU_TABLE_BASE; /* MMU_TABLE_BASE = 0x 0x33dfc000 */
4 __asm__(
5 /* Invalidate caches */
6 "mov r0, #0/n"
7 "mcr p15, 0, r0, c7, c7, 0/n" /* invalidate I,D caches on v4 */
8 "mcr p15, 0, r0, c7, c10, 4/n" /* drain write buffer on v4 */
9 "mcr p15, 0, r0, c8, c7, 0/n" /* invalidate I,D TLBs on v4 */
10 /* Load page table pointer */
11 "mov r4, %0/n"
12 "mcr p15, 0, r4, c2, c0, 0/n" /* load page table pointer */
13 /* Write domain id (cp15_r3) */
14 "mvn r0, #0/n" /* Domains 0b01 = client, 0b11=Manager*/
15 "mcr p15, 0, r0, c3, c0, 0/n" /* load domain access register,
write domain 15:0, 数据手册P548(access permissions)*/
16 /* Set control register v4 */
17 "mrc p15, 0, r0, c1, c0, 0/n" /* get control register v4 */
/*数据手册P545:read control register */
18 /* Clear out 'unwanted' bits (then put them in if we need them) */
19 /* ..VI ..RS B... .CAM */ /*这些位的含义在数据手册P546*/
20 "bic r0, r0, #0x3000/n" /* ..11 .... .... .... */
/*I(bit[12])=0 = Instruction cache disabled*/
21 /*V[bit[13]](Base location of exception registers)=0 = Low addresses =
0x0000 0000*/
22 "bic r0, r0, #0x0300/n" /* .... ..11 .... .... */
23 /*R(ROM protection bit[9])=0*/
/*S(System protection bit[8])=0*/
/*由于TTB中AP=0b11(line141),所以RS位不使用(P579)*/
24 "bic r0, r0, #0x0087/n" /* .... .... 1... .111 */
/*M(bit[0])=0 = MMU disabled*/
/*A(bit[1])=0 =Data address
alignment fault checking disable*/
/*C(bit[2])=0 = Data cache disabled*/
/*B(bit[7])=0= Little-endian operation*/
25 /* Turn on what we want */
26 /* Fault checking enabled */
27 "orr r0, r0, #0x0002/n" /* .... .... .... ..1. */
/*A(bit[1])=1 = Data address
alignment fault checking enable*/
28 #ifdef CONFIG_CPU_D_CACHE_ON /*is not set*/
29 "orr r0, r0, #0x0004/n" /* .... .... .... .1.. */
/*C(bit[2])=1 = Data cache enabled*/
30 #endif
31 #ifdef CONFIG_CPU_I_CACHE_ON /*is not set*/
32 "orr r0, r0, #0x1000/n" /* ...1 .... .... .... */
/*I(bit[12])=1 = Instruction cache enabled*/
33 #endif
34 /* MMU enabled */
35 "orr r0, r0, #0x0001/n" /* .... .... .... ...1 */
/*M(bit[0])=1 = MMU enabled*/
36 "mcr p15, 0, r0, c1, c0, 0/n" /* write control register */
/*数据手册P545*/
37 : /* no outputs */
38 : "r" (ttb) );
39 }
至此,内存如下划分:
图8 创建页表后内存的划分情况
4、Step 4:heap_init()
heap——堆,内存动态分配函数mmalloc就是从heap中划出一块空闲内存的,
mfree则将动态分配的某块内存释放回heap中。
heap_init函数在SDRAM中指定了一块1M大小的内存作为heap(起始地址
HEAP_BASE = 0x33e00000),并在heap的开头定义了一个数据结构blockhead——
事实上,heap就是使用一系列的blockhead数据结构来描述和操作的。每个
blockhead数据结构对应着一块heap内存,假设一个blockhead数据结构的存放位
置为A,则它对应的可分配内存地址为“A + sizeof(blockhead)”到“A +
sizeof(blockhead) + size - 1”。blockhead数据结构在lib/heap.c中定义:
1 typedef struct blockhead_t {
2 Int32 signature; //固定为BLOCKHEAD_SIGNATURE
3 Bool allocated; //此区域是否已经分配出去:0-N,1-Y
4 unsigned long size; //此区域大小
5 struct blockhead_t *next; //链表指针
6 struct blockhead_t *prev; //链表指针
7 } blockhead;
现在来看看heap是如何运作的(如果您不关心heap实现的细节,这段可以跳
过)。vivi对heap的操作比较简单,vivi中有一个全局变量static blockhead
*gHeapBase,它是heap的链表头指针,通过它可以遍历所有blockhead数据结构。
假设需要动态申请一块sizeA大小的内存,则mmalloc函数从gHeapBase开始搜索
blockhead数据结构,如果发现某个blockhead满足:
a. allocated = 0 //表示未分配
b. size > sizeA,
则找到了合适的blockhead,于是进行如下操作:
a.allocated设为1
b.如果size – sizeA > sizeof(blockhead),则将剩下的内存组织成
一个新的blockhead,放入链表中
c.返回分配的内存的首地址
释放内存的操作更简单,直接将要释放的内存对应的blockhead数据结构的
allocated设为0即可。
下面用图来简单演示先分配1K内存,再分配2K内存的过程:
图9 在heap中连续分配1K和2K内存的示意图
heap_init函数直接调用mmalloc_init函数进行初始化,此函数代码在
lib/heap.c中,比较简单,初始化gHeapBase即可:
[main(int argc, char *argv[]) > heap_init(void) > mmalloc_init(unsigned
char *heap, unsigned long size)]
1 static inline int mmalloc_init(unsigned char *heap, unsigned long size)
2 {
3 if (gHeapBase != NULL) return -1;
4 DPRINTK("malloc_init(): initialize heap area at 0x%08lx, size =
0x%08lx/n", heap, size);
5 gHeapBase = (blockhead *)(heap);
6 gHeapBase->allocated=FALSE;
7 gHeapBase->signature=BLOCKHEAD_SIGNATURE;
8 gHeapBase->next=NULL;
9 gHeapBase->prev=NULL;
10 gHeapBase->size = size - sizeof(blockhead);
11 return 0;
12 }
分配heap区域后,内存划分情况如下:
图10 执行heap_init后内存的划分情况
5、Step 5:mtd_dev_init()
在分析代码前先介绍一下MTD(Memory Technology Device)相关的技术。在
linux系统中,我们通常会用到不同的存储设备,特别是FLASH设备。为了在使用
新的存储设备时,我们能更简便地提供它的驱动程序,在上层应用和硬件驱动的
中间,抽象出MTD设备层。驱动层不必关心存储的数据格式如何,比如是FAT32、
ETX2还是FFS2或其它。它仅仅提供一些简单的接口,比如读写、擦除及查询。如
何组织数据,则是上层应用的事情。MTD层将驱动层提供的函数封装起来,向上
层提供统一的接口。这样,上层即可专注于文件系统的实现,而不必关心存储设
备的具体操作。
在我们即将看到的代码中,使用mtd_info数据结构表示一个MTD设备,使用
nand_chip数据结构表示一个nand flash芯片。在mtd_info结构中,对nand_flash
结构作了封装,向上层提供统一的接口。比如,它根据nand_flash提供的
read_data(读一个字节)、read_addr(发送要读的扇区的地址)等函数,构造了一
个通用的读函数read,将此函数的指针作为自己的一个成员。而上层要读写flash
时,执行mtd_info中的read、write函数即可。
下面分析代码:
mtd_dev_init()用来扫描所使用的NAND Flash的型号,构造MTD设备,即构
造一个mtd_info的数据结构。对于本开发板,它直接调用mtd_init(),mtd_init
又调用smc_init(),此函数在drivers/mtd/maps/s3c2410_flash.c中:
[main(int argc, char *argv[]) > mtd_dev_init() > mtd_init() > smc_init()]
1 static int
2 smc_init(void)
3 {
/*struct mtd_info *mymtd,数据类型在include/mtd/mtd.h*/
/*strcut nand_chip在include/mtd/nand.h中定义*/
4 struct nand_chip *this;
5 u_int16_t nfconf;
/* Allocate memory for MTD device structure and private data */
6 mymtd = mmalloc(sizeof(struct mtd_info) + sizeof(struct
nand_chip));
7 if (!mymtd) {
8 printk("Unable to allocate S3C2410 NAND MTD device
structure./n");
9 return -ENOMEM;
10 }
/* Get pointer to private data */
11 this = (struct nand_chip *)(&mymtd[1]);
/* Initialize structures */
12 memset((char *)mymtd, 0, sizeof(struct mtd_info));
13 memset((char *)this, 0, sizeof(struct nand_chip));
/* Link the private data with the MTD structure */
14 mymtd->priv = this;
/* set NAND Flash controller */
15 nfconf = NFCONF;
/* NAND Flash controller enable */
16 nfconf |= NFCONF_FCTRL_EN;
/* Set flash memory timing */
17 nfconf &= ~NFCONF_TWRPH1; /* 0x0 */
18 nfconf |= NFCONF_TWRPH0_3; /* 0x3 */
19 nfconf &= ~NFCONF_TACLS; /* 0x0 */
20 NFCONF = nfconf;
/* Set address of NAND IO lines */
21 this->hwcontrol = smc_hwcontrol;
22 this->write_cmd = write_cmd;
23 this->write_addr = write_addr;
24 this->read_data = read_data;
25 this->write_data = write_data;
26 this->wait_for_ready = wait_for_ready;
/* Chip Enable -> RESET -> Wait for Ready -> Chip Disable */
27 this->hwcontrol(NAND_CTL_SETNCE);
28 this->write_cmd(NAND_CMD_RESET);
29 this->wait_for_ready();
30 this->hwcontrol(NAND_CTL_CLRNCE);
31 smc_insert(this);
32 return 0;
33 }
6-14行构造了一个mtd_info结构和nand_flash结构,前者对应MTD设备,后
者对应nand flash芯片(如果您用的是其他类型的存储器件,比如nor flash,这
里的nand_flash结构应该换为其他类型的数据结构)。MTD设备是具体存储器件的
抽象,那么在这些代码中这种关系如何体现呢——第14行的代码把两者连结在一
起了。事实上,mtd_info结构中各成员的实现(比如read、write函数),正是由
priv变量所指向的nand_flash的各类操作函数(比如read_addr、read_data等)
来实现的。
15-20行是初始化S3C2410上的NAND FLASH控制器,和我们在实验六“NAND
FLASH CONTROLLER”里面做的一样。
前面分配的nand_flash结构还是空的,现在当然就是填满它的各类成员了,
这正是21-26行做的事情。
27-30行对这块nand flash作了一下复位操作。
最后,也是最复杂的部分,根据刚才填充的nand_flash结构,构造mtd_info
结构,这由31行的smc_insert函数调用smc_scan完成。先看看smc_insert函数:
[main(int argc, char *argv[]) > mtd_dev_init() > mtd_init() > smc_init()
> smc_insert(struct nand_chip *this)]
1 inline int
2 smc_insert(struct nand_chip *this) {
2 /* Scan to find existance of the device */
/*smc_scan defined at drivers/mtd/nand/smc_core.c*/
4 if (smc_scan(mymtd)) {
5 return -ENXIO;
6 }
/* Allocate memory for internal data buffer */
7 this->data_buf = mmalloc(sizeof(u_char) *
8 (mymtd->oobblock + mymtd->oobsize));
9 if (!this->data_buf) {
10 printk("Unable to allocate NAND data buffer for S3C2410./n");
11 this->data_buf = NULL;
12 return -ENOMEM;
13 }
14 return 0;
15 }
后面的7-13行,我也没弄清楚,待查。
现在,终于到重中之重了,请看smc_scan函数的代码:
[main(int argc, char *argv[]) > mtd_dev_init() > mtd_init() > smc_init()
> smc_insert(struct nand_chip *this) > smc_scan(struct mtd_info *mtd)]
1 int smc_scan(struct mtd_info *mtd)
2 {
3 int i, nand_maf_id, nand_dev_id;
4 struct nand_chip *this = mtd->priv;
/* Select the device */
5 nand_select();
/* Send the command for reading device ID */
6 nand_command(mtd, NAND_CMD_READID, 0x00, -1);
7 this->wait_for_ready();
/* Read manufacturer and device IDs */
8 nand_maf_id = this->read_data();
9 nand_dev_id = this->read_data();
/* Print and sotre flash device information */
10 for (i = 0; nand_flash_ids[i].name != NULL; i++) {
11 if (nand_maf_id == nand_flash_ids[i].manufacture_id &&
12 nand_dev_id == nand_flash_ids[i].model_id) {
13 if (!(mtd->size) && !(nand_flash_ids[i].page256)) {
14 mtd->name = nand_flash_ids[i].name;
15 mtd->erasesize = nand_flash_ids[i].erasesize;
16 mtd->size = (1 << nand_flash_ids[i].chipshift);
17 mtd->eccsize = 256;
18 mtd->oobblock = 512;
19 mtd->oobsize = 16;
20 this->page_shift = 9;
21 this->dev =
&nand_smc_info[GET_DI_NUM(nand_flash_ids[i].chipshift)];
22 }
23 printk("NAND device: Manufacture ID:" /
24 " 0x%02x, Chip ID: 0x%02x (%s)/n",
25 nand_maf_id, nand_dev_id, mtd->name);
26 break;
27 }
28 }
/* De-select the device */
29 nand_deselect();
30 /* Print warning message for no device */
31 if (!mtd->size) {
32 printk("No NAND device found!!!/n");
33 return 1;
34 }
35 /* Fill in remaining MTD driver data */
36 mtd->type = MTD_NANDFLASH;
37 mtd->flags = MTD_CAP_NANDFLASH | MTD_ECC;
38 mtd->module = NULL;
39 mtd->ecctype = MTD_ECC_SW;
40 mtd->erase = nand_erase;
41 mtd->point = NULL;
42 mtd->unpoint = NULL;
43 mtd->read = nand_read;
44 mtd->write = nand_write;
45 mtd->read_ecc = nand_read_ecc;
46 mtd->write_ecc = nand_write_ecc;
47 mtd->read_oob = nand_read_oob;
48 mtd->write_oob = nand_write_oob;
49 mtd->lock = NULL;
50 mtd->unlock = NULL;
51 /* Return happy */
52 return 0;
53 }
5-9行,读取nand flash得厂家ID和设备ID,下面将用这两个ID号,在一个
预设的数组“nand_flash_ids”里查找到与之相符的项。nand_flash_ids是
nand_flash_dev结构的数组,里面存放的是世界上比较常用的nand flash型号的
一些特性(见下面的定义):
struct nand_flash_dev {
char * name;
int manufacture_id;
int model_id;
int chipshift;
char page256;
char pageadrlen;
unsigned long erasesize;
};
static struct nand_flash_dev nand_flash_ids[] = {
{"Toshiba TC5816BDC", NAND_MFR_TOSHIBA, 0x64, 21, 1, 2, 0x1000},
..
{NULL,}
};
10-28行根据找到的数组项,构造前面分配的mtd_info结构。
35行之后的代码,填充mtd_info结构的剩余部分,大多是一些函数指针,比
如read、write函数等。
执行完mtd_dev_init后,我们得到了一个mtd_info结构的全局变量(mymtd指
向它),以后对nand flash的操作,直接通过mymtd提供的接口进行。
6、Step 6:init_priv_data()
此函数将启动内核的命令参数取出,存放在内存特定的位置中。这些参数来
源有两个:vivi预设的默认参数,用户设置的参数(存放在nand flash上)。
init_priv_data先读出默认参数,存放在“VIVI_PRIV_RAM_BASE”开始的内存上;
然后读取用户参数,若成功则用用户参数覆盖默认参数,否则使用默认参数。
init_priv_data函数分别调用get_default_priv_data函数和
load_saved_priv_data函数来读取默认参数和用户参数。这些参数分为3类:
a.vivi自身使用的一些参数,比如传输文件时的使用的协议等
b.linux启动命令
c.nand flash的分区参数
get_default_priv_data函数比较简单,它将vivi中存储这些默认参数的变
量,复制到指定内存中。执行完后,此函数执行完毕后,内存使用情况如下:
图11 执行init_priv_data后内存的划分情况
load_saved_priv_data函数读取上述内存图中分区参数的数据,找到
“param”分区,然后从中读出上述3类参数,覆盖掉默认参数(如果能成功读出
的话)。
7、Step 7:misc()和init_builtin_cmds()
这两个函数都是简单地调用add_command函数,给一些命令增加相应的处理
函数。在vivi启动后,可以进去操作界面,这些命令,就是供用户使用的。它们
增加了如下命令:
a.add_command(&cpu_cmd)
b. add_command(&bon_cmd)
c. add_command(&reset_cmd)
d. add_command(¶m_cmd)
e. add_command(&part_cmd)
f. add_command(&mem_cmd)
g. add_command(&load_cmd)
h. add_command(&go_cmd)
i. add_command(&dump_cmd)
j. add_command(&call_cmd)
k. add_command(&boot_cmd)
l. add_command(&help_cmd)
现在来看看add_command函数是如何实现得:
1 void add_command(user_command_t *cmd)
2 {
3 if (head_cmd == NULL) {
4 head_cmd = tail_cmd = cmd;
5 } else {
6 tail_cmd->next_cmd = cmd;
7 tail_cmd = cmd;
8 }
9 /*printk("Registered '%s' command/n", cmd->name);*/
10 }
很简单!把user_command_t结构放入一个链表即可。user_command_t结
构定义如下,呵呵,name是命令名称,cmdfunc是这个命令的处理函数,helstr
是帮助信息。
typedef struct user_command {
const char *name;
void (*cmdfunc)(int argc, const char **);
struct user_command *next_cmd;
const char *helpstr;
} user_command_t;
8、Step 8:boot_or_vivi()
此函数根据情况,或者启动“vivi_shell”,进入与用户进行交互的界面,
或者直接启动linux内核。代码如下:
[main(int argc, char *argv[]) > void boot_or_vivi(void)]
1 void boot_or_vivi(void)
2 {
3 char c;
4 int ret;
5 ulong boot_delay;
5 boot_delay = get_param_value("boot_delay", &ret);
7 if (ret) boot_delay = DEFAULT_BOOT_DELAY;
/* If a value of boot_delay is zero,
* unconditionally call vivi shell
*/
8 if (boot_delay == 0) vivi_shell();
/*
* wait for a keystroke (or a button press if you want.)
*/
9 printk("Press Return to start the LINUX now, any other key for
vivi/n");
10 c = awaitkey(boot_delay, NULL);
11 if (((c != '/r') && (c != '/n') && (c != '/0'))) {
12 printk("type /"help/" for help./n");
13 vivi_shell();
14 }
15 run_autoboot();
16 return;
17 }
第10行等待键盘输入,如果在一段时间内键盘无输入,或者输入了回车键,
则调用run_autoboot启动内核;否则调用vivi_shell进入交互界面。
vivi_shell等待用户输入命令(等待串口数据),然后根据命令查找“Step 7:
misc()和init_builtin_cmds()”设置的命令链表,运行找到的命令函数。
vivi_shell函数是通过调用serial_term函数来实现的,serial_term代码如下:
[main(int argc, char *argv[]) > void boot_or_vivi(void) >
vivi_shell(void) > serial_term]
1 void serial_term(void)
2 {
3 char cmd_buf[MAX_CMDBUF_SIZE];
4 for (;;) {
5 printk("%s> ", prompt); /*prompt is defined upside*/
6 getcmd(cmd_buf, MAX_CMDBUF_SIZE);
/* execute a user command */
7 if (cmd_buf[0])
8 exec_string(cmd_buf);
9 }
10 }
第6行getcmd函数读取串口数据,将返回的字符串作为参数调用exec_string
函数。exec_string代码如下:
[main(int argc, char *argv[]) > void boot_or_vivi(void) >
vivi_shell(void) > serial_term > void exec_string(char *buf)]
1 void exec_string(char *buf)
2 {
3 int argc;
4 char *argv[128];
5 char *resid;
6 while (*buf) {
7 memset(argv, 0, sizeof(argv));
8 parseargs(buf, &argc, argv, &resid);
9 if (argc > 0)
10 execcmd(argc, (const char **)argv);
11 buf = resid;
12 }
13 }
它首先调用parseargs函数分析所传入的字符串,确定参数个数及将这些参
数分别保存。比如对于字符串“abcd efgh ijklm 123”,parseargs函数返回的
结果是:argc = 4,argv[0]指向“abcd”字符串,argv[1]指向“efgh”字符
串,argv[2]指向“ijklm”字符串,argv[3]指向“123”字符串。然后,调用execmd
函数:
[main(int argc, char *argv[]) > void boot_or_vivi(void) >
vivi_shell(void) > serial_term > void execcmd(int argc, const char
**argv)]
1 void execcmd(int argc, const char **argv)
2 {
3 user_command_t *cmd = find_cmd(argv[0]);
4 if (cmd == NULL) {
5 printk("Could not found '%s' command/n", argv[0]);
6 printk("If you want to konw available commands, type
'help'/n");
7 return;
8 }
9 /*printk("execcmd: cmd=%s, argc=%d/n", argv[0], argc);*/
10 cmd->cmdfunc(argc, argv);
11 }
首先,第3行的find_cmd函数根据命令(argv[0])找到对应的处理函数,然后
执行它(第10行)。命令名字和对应的处理函数指针,在上述“Step 7:misc()
和init_builtin_cmds()”中,使用一个命令链表存起来了。具体的命令处理函
数,这里就不细说了,各位有兴趣的可以自行阅读源代码。
run_autoboot用于启动内核,起始就是运行“boot”命令的处理函数
command_boot。我们看看它是如何启动内核的,下面我们只列出用到的代码:
[main(int argc, char *argv[]) > void boot_or_vivi(void) > void
run_autoboot(void) > exec_string("boot") > command_boot(1, 0)]
1 void command_boot(int argc, const char **argv)
2 {
..
3 media_type = get_param_value("media_type", &ret);
4 if (ret) {
5 printk("Can't get default 'media_type'/n");
6 return;
7 }
8 kernel_part = get_mtd_partition("kernel");
9 if (kernel_part == NULL) {
10 printk("Can't find default 'kernel' partition/n");
11 return;
12 }
13 from = kernel_part->offset;
14 size = kernel_part->size;
..
15 boot_kernel(from, size, media_type);
..
}
第3行,获得存储内核代码的器件类型;第8行获取“kernel”分区信息,第
13、14行由此分区信息获得内核存放的位置及大小;第15行的boot_kernel函数
将内核从其存储的器件上(flash)复制到内存,然后执行它。boot_kernel代码如
下:
[main(int argc, char *argv[]) > void boot_or_vivi(void) > void
run_autoboot(void) > exec_string("boot") > command_boot(1, 0) > int
boot_kernel(ulong from, size_t size, int media_type)]
1 int boot_kernel(ulong from, size_t size, int media_type)
2 {
3 int ret;
4 ulong boot_mem_base; /* base address of bootable memory */
5 ulong to;
6 ulong mach_type;
7 boot_mem_base = get_param_value("boot_mem_base", &ret);
8 if (ret) {
9 printk("Can't get base address of bootable memory/n");
10 printk("Get default DRAM address. (0x%08lx/n", DRAM_BASE);
11 boot_mem_base = DRAM_BASE;
12 }
/* copy kerne image
* LINUX_KERNEL_OFFSET = 0x8000
*/
13 to = boot_mem_base + LINUX_KERNEL_OFFSET;
14 printk("Copy linux kernel from 0x%08lx to 0x%08lx, size =
0x%08lx ... ", from, to, size);
15 ret = copy_kernel_img(to, (char *)from, size, media_type);
16 if (ret) {
17 printk("failed/n");
18 return -1;
19 } else {
20 printk("done/n");
21 }
22 if (*(ulong *)(to + 9*4) != LINUX_ZIMAGE_MAGIC) {
23 printk("Warning: this binary is not compressed linux kernel
image/n");
24 printk("zImage magic = 0x%08lx/n", *(ulong *)(to + 9*4));
25 } else {
26 printk("zImage magic = 0x%08lx/n", *(ulong *)(to + 9*4));
27 }
/* Setup linux parameters and linux command line
* LINUX_PARAM_OFFSET = 0x100
*/
28 setup_linux_param(boot_mem_base + LINUX_PARAM_OFFSET);
/* Get machine type */
29 mach_type = get_param_value("mach_type", &ret);
30 printk("MACH_TYPE = %d/n", mach_type);
/* Go Go Go */
31 printk("NOW, Booting Linux....../n");
32 call_linux(0, mach_type, to);
33 return 0;
34 }
第7-13行用来确定在内核将存放在内存中的位置;15行调用nand_read_ll
函数(详细介绍请看“实验六:NAND FLASH CONTROLLER”)从flash芯片上将内核
读出;28行设置内核启动的参数,比如命令行等;29行获取处理器型号;32行的
函数call_linux汇编代码,跳到内核存放的地址处执行。值得细看的有两个函数:
setup_linux_param和call_linux。
setup_linux_param函数用于设置linux启动时用到的各类参数:
[main( ) > boot_or_vivi( ) > run_autoboot( ) > exec_string("boot") >
command_boot(1, 0) > boot_kernel( ) > setup_linux_param( )]
1 static void setup_linux_param(ulong param_base)
2 {
3 struct param_struct *params = (struct param_struct *)param_base;
4 char *linux_cmd;
5 printk("Setup linux parameters at 0x%08lx/n", param_base);
6 memset(params, 0, sizeof(struct param_struct));
7 params->u1.s.page_size = LINUX_PAGE_SIZE;
8 params->u1.s.nr_pages = (DRAM_SIZE >> LINUX_PAGE_SHIFT);
/* set linux command line */
9 linux_cmd = get_linux_cmd_line(); /* lib/priv_data/param.c */
10 if (linux_cmd == NULL) {
11 printk("Wrong magic: could not found linux command line/n");
12 } else {
13 memcpy(params->commandline, linux_cmd, strlen(linux_cmd) +
1);
14 printk("linux command line is: /"%s/"/n", linux_cmd);
15 }
16 }
此函数在param_base地址存放linux启动时用到的参数。对于本开发板,此
地址为RAM_BASE + LINUX_PARAM_OFFSET = 0x3000_0000 + 0x100。第9行将命令
行参数从原来位置(上述Step 6:init_priv_data中将命令行等存储在内存中)
读出,复制到相应位置(第13行)。
call_linux函数先对cache和tlb进行一些设置,然后将内核存放的位置直接
赋给pc寄存器,从而执行内核。call_linux函数代码如下:
[main( ) > boot_or_vivi( ) > run_autoboot( ) > exec_string("boot") >
command_boot(1, 0) > boot_kernel( ) > call_linux( )]
1 void call_linux(long a0, long a1, long a2)
2 {
3 cache_clean_invalidate();
4 tlb_invalidate();
5 __asm__(
6 "mov r0, %0/n"
7 "mov r1, %1/n"
8 "mov r2, %2/n"
9 "mov ip, #0/n"
10 "mcr p15, 0, ip, c13, c0, 0/n" /* zero PID */
11 "mcr p15, 0, ip, c7, c7, 0/n" /* invalidate I,D caches */
12 "mcr p15, 0, ip, c7, c10, 4/n" /* drain write buffer */
13 "mcr p15, 0, ip, c8, c7, 0/n" /* invalidate I,D TLBs */
14 "mrc p15, 0, ip, c1, c0, 0/n" /* get control register */
15 "bic ip, ip, #0x0001/n" /* disable MMU */
16 "mcr p15, 0, ip, c1, c0, 0/n" /* write control register */
17 "mov pc, r2/n"
18 "nop/n"
19 "nop/n"
20 : /* no outpus */
21 : "r" (a0), "r" (a1), "r" (a2)
22 );
23 }
对cache、tlb等的操作,您可以参考“实验十一:MMU”。6、7、8行中的%0、
%1、%2对应3个寄存器,它们存放的值分别为参数a0、a1和a2。这样,执行完这3
条语句后,r0、r1、r2对应的值分别为:0, mach_type, 0x3000_8000(内核在内
存中的存放地址)。第17行将跳到去执行内核。至此,linux内核终于开始运行了!
vivi执行完毕后,内存使用情况如下:
图12 vivi启动内核后内存的划分情况
本书面向由传统51单片机转向ARM嵌入式开发的硬件工程师、由硬件转嵌
入式软件开发的工程师、没有嵌入式开发经验的软件工程师。分9个部分:
1、开发环境建立
2、S3C2410功能部件介绍与实验(含实验代码)
3、bootloader vivi详细注释
4、linux移植
5、linux驱动
6、yaffs文件系统详解
7、调试工具
8、GUI开发简介
9、UC/OS移植
通过学习第二部分,即可了解基于ARM CPU的嵌入式开发所需要的外围器
件及其接口。对应的实验代码实现了对这些接口的操作,这可以让硬件工程师形
成一个嵌入式硬件开发的概念。这部分也可以当作S3C2410的数据手册来使用。
一个完整的嵌入式linux系统包含4部分内容:bootloader、parameters、kernel、
root file system。3、4、5、6部分详细介绍了这4部分的内容,这是linux底层软
件开发人员应该掌握的。通过学习这些章节,您可以详细了解到如何在一个裸板
上裁减、移植linux,如何构造自己的根文件系统,如何编写适合客户需求的驱
动程序——驱动程序这章将结合几个经典的驱动程序进行讲解。您还可以了解到
在用在nand flash上的非常流行的yaffs文件系统是如何工作的,本书将结合yaffs
代码详细介绍yaffs文件系统。
第7部分介绍了嵌入式linux开发中使用gdb进行调试的详细过程。
此文档目前完成了1、2、3部分,后面部分将陆续完成。希望能对各位在嵌
入式开发方面献上棉力。
欢迎来信指出文中的不足与错误,欢迎来信探讨技术问题。
Email :thisway.diy@163.com
MSN :thisway.diy@163.com
QQ :17653039
二.建立开发环境
(1)编译器arm-linux-gcc-3.4.1
下载地址:
ftp://ftp.handhelds.org/projects/toolchain/arm-linux-gcc-3.4.1.tar.bz2
执行如下命令安装:
bunzip2 arm-linux-gcc-3.4.1.tar.bz2
tar xvf arm-linux-gcc-3.4.1.tar -C /
生成的编译工具在目录/usr/local/arm/3.4.1/bin下,修改/etc/profile,
增加如下一行。这可以让我们直接运行arm-linux-gcc,而不必将其绝对路径都
写出来,不过这得重新启动后才生效:
pathmunge /usr/local/arm/3.4.1/bin
(2)Jflash-s3c2410:S3C2410芯片的JTAG工具
我们的第一个程序就是通过它下载到开发板上的nor flash或者nand flash
上去的。把它放到/usr/local/bin目录下。
下载地址:e
ftp://ftp.mizi.com/pub/linuette/SDK/1.5/target/box/Jflash/Jflash-
s3c2410
注意:步骤3您现在不必理会,可以等进行到“调试”部分时再回过头来看。
(3)安装gdb调试工具
下载地址:
http://www.gnu.org/software/gdb/download/
http://ftp.gnu.org/gnu/gdb/gdb-6.3.tar.gz
执行如下命令安装:
a.安装在主机上运行的arm-linux-gdb工具:
tar xvzf gdb-6.3.tar.gz
cd gdb6.3
./configure --target=arm-linux
make
make install
此时,在/usr/local/bin中生成arm-linux-gdb等工具
b.继续上面的步骤,安装gdbserver。需要将此工具下载到开发板上运
行,这在后面会详细描述:
cd gdbserver
export CC=/usr/local/arm/3.4.1/bin/arm-linux-gcc
./configure arm-linux
make
此时在当前目录中生成了gdbserver工具,当我们讲到如何调试时,
会把这个文件下载到开发板上去。
三.S3C2410基础实验
本章将逐一介绍S3C2410各功能模块,并结合简单的程序进行上机实验。您
不必将本章各节都看完,完全可以看了一、两节,得到一个大概的印象之后,就
开始下一章。本章可以当作手册来用。
注意:了解S3C2410各部件最好的参考资料是它的数据手册。本文不打算翻
译该手册,在进行必要的讲解后,进行实际实验——这才是本文的重点。
(1)实验一:LED_ON
led_on.s只有7条指令,它只是简单地点亮发光二极管LED1。本实验的目
的是让您对开发流程有个基本概念。
实验步骤:
a.把PC并口和开发板JTAG接口连起来、确保插上“BOOT SEL”跳线、上
电(呵呵,废话,如果以后实验步骤中未特别指出,则本步骤省略)
b.进入LED_ON目录后,执行如下命令生成可执行文件led_on:
make
c.执行如下命令将led_on写入nand flash:
i. Jflash-s3c2410 led_on /t=5
ii.当出现如下提示时,输入0并回车:
iii.当出现如下提示时,输入0并回车:
iv.当再次出现与步骤ii相同的提示时,输入2并回车
d.按开发板上reset键后可看见LED1被点亮了
实验步骤总地来说分3类:编写源程序、编译/连接程序、烧写代码。
先看看源程序led_on.s:
1 .text
2 .global _start
3 _start:
4 LDR R0,=0x56000010 @R0设为GPBCON寄存器。此寄存器
@用于选择端口B各引脚的功能:
@是输出、是输入、还是其他
5 MOV R1,#0x00004000
6 STR R1,[R0] @设置GPB7为输出口
7 LDR R0,=0x56000014 @R0设为GPBDAT寄存器。此寄存器
@用于读/写端口B各引脚的数据
8 MOV R1,#0x00000000 @此值改为0x00000080,
@可让LED1熄灭
9 STR R1,[R0] @GPB7输出0,LED1点亮
10 MAIN_LOOP:
11 B MAIN_LOOP
对于程序中用到的寄存器GPBCON、GPBDAT,我稍作描述,具体寄存器的操作
可看实验三:I/O PORTS。GPBCON用于选择PORT B的11根引脚的功能:输出、输
入还是其他特殊功能。每根引脚用2位来设置:00表示输入、01表示输出、10表
示特殊功能、11保留不用。LED1-3的引脚是GPB7-GPB10,使用GPBCON中位[12:13]、
[13:14]、[15:16]、[17:18]来进行功能设置。GPBDAT用来读/写引脚:GPB0对应
位0、GPB1对应位1,诸如此类。当引脚设为输出时,写入0或1可使相应引脚输出
低电平或高电平。
程序很简单,第4、5、6行3条指令用于将LED1对应的引脚设成输出引脚;
第7、8、9行3条指令让这条引脚输出0;第11行指令是个死循环。
实验步骤b中,指令“make”的作用就是编译、连接led_on.s源程序。Makefile
的内容如下:
1 led_on:led_on.s
2 arm-linux-gcc -g -c -o led_on.o led_on.s
3 arm-linux-ld -Ttext 0x0000000 -g led_on.o -o led_on_tmp.o
4 arm-linux-objcopy -O binary -S led_on_tmp.o led_on
5 clean:
6 rm -f led_on
7 rm -f led_on.o
8 rm -f led_on_tmp.o
make指令比较第1行中文件led_on和文件led_on.s的时间,如果led_on
的时间比led_on.s的时间旧(led_on未生成时,此条件默认成立),则执行第2、
3、4行的指令更新led_on。您也可以不用指令make,而直接一条一条地执行2、
3、4行的指令——但是这样多累啊。第2行的指令是预编译,第3行是连接,
第4行是把ELF格式的可执行文件led_on_tmp.o转换成二进制格式文件led_on。
执行“make clean”时强制执行6、7、8行的删除命令。
注意:Makefile文件中相应的命令行前一定有一个制表符(TAB)
汇编语言可读性太差,现在请开始实验二,我用C语言来实现了同样的功能,
而以后的实验,我也尽可能用C语言实现。
(2)实验二:LED_ON_C
C语言程序执行的第一条指令,并不在main函数中。当我们生成一个C程序
的可执行文件时,编译器总是在我们的代码前加一段固定的代码——crt0.o,它
是编译器自带的一个文件。此段代码设置C程序的堆栈等,然后调用main函数。
很可惜,在我们的裸板上,这段代码无法执行,所以我们得自己写一个。这段代
码很简单,只有3条指令。
crt0.s代码:
1 .text
2 .global _start
3 _start:
4 ldr sp, =1024*4 @设置堆栈,注意:不能大于4k
@nand flash中的代码在复位后会
@移到内部ram中,它只有4k
5 bl main @调用C程序中的main函数
6 halt_loop:
7 b halt_loop
现在,我们可以很容易写出控制LED的程序了,led_on_c.c代码如下:
1 #define GPBCON (*(volatile unsigned long *)0x56000010)
2 #define GPBDAT (*(volatile unsigned long *)0x56000014)
3 int main()
4 {
5 GPBCON = 0x00004000; //设置GPB7为输出口
6 GPBDAT = 0x00000000; //令GPB7输出0
7 return 0;
8 }
最后,我们来看看Makefile:
1 led_on_c : crt0.s led_on_c.c
2 arm-linux-gcc -g -c -o crt0.o crt0.s
3 arm-linux-gcc -g -c -o led_on_c.o led_on_c.c
4 arm-linux-ld -Ttext 0x0000000 -g crt0.o led_on_c.o -o
led_on_c_tmp.o
5 arm-linux-objcopy -O binary -S led_on_c_tmp.o led_on_c
6 clean:
7 rm -f led_on_c
8 rm -f led_on_c.o
9 rm -f led_on_c_tmp.o
10 rm -f crt0.o
第2、3行分别对源程序crt0.s、led_on_c.c进行预编译,第4行将预编译
得到的结果连接起来,第5行把连接得到的ELF格式可执行文件led_on_c_tmp.o
转换成二进制格式文件led_on_c。
好了,可以开始上机实验了:
实验步骤:
a.进入LED_ON_C目录后,执行如下命令生成可执行文件led_on_c:
make
b.执行如下命令将led_on_c写入nand flash:
i. Jflash-s3c2410 led_on_c /t=5
ii.当出现如下提示时,输入0并回车:
K9S1208 NAND Flash JTAG Programmer Ver 0.0
0:K9S1208 Program 1:K9S1208 Pr BlkPage 2: Exit
Select the function to test :
iii.当出现如下提示时,输入0并回车:
Input target block number:
iv.当出现与步骤ii相同的提示时,输入2并回车
c.按开发板上reset键后可看见LED1被点亮了
目录LEDS中的程序是使用4个LED从0到15轮流计数,您可以试试:
a.进入目录后make
b.Jflash-s3c2410 leds /t=5
c.reset运行
另外,如果您有兴趣,可以使用如下命令看看二进制可执行文件的反汇编码:
arm-linux-objdump -D -b binary -m arm xxxxx(二进制可执行文件名)
注意:本文的所有程序均在SOURCE目录中,各程序所在目录均为大写,其
可执行文件名为相应目录名的小写,比如LEDS目录下的可执行文件为leds。以
后不再赘述如何烧写程序:直接运行Jflash-s3c2410即可看到提示。
(3)实验三:I/O PORTS
请打开S3C2410数据手册第9章IO/ PORTS,I/O PORTS含GPA、GPB、..、
GPH八个端口。它们的寄存器是相似的:GPxCON用于选择引脚功能,GPxDAT用
于读/写引脚数据,GPxUP用于确定是否使用内部上拉电阻(x为A、B、..、H,
没有GPAUP寄存器)。
1、PORT A与PORT B-H在功能选择方面有所不同,GPACON中每一位对应一根引
脚(共23根引脚)。当某位设为0时,相应引脚为输出引脚,此时我们可以在
GPADAT中相应位写入0或1让此引脚输出低电平或高电平;当某位设为1时,
相应引脚为地址线或用于地址控制,此时GPADAT无用。一般而言GPACON通
常设为全1,以便访问外部存储器件。PORT A我们暂时不必理会。
2、PORT B-H在寄存器操作方面完全相同。GPxCON中每两位控制一根引脚:00
表示输入、01表示输出、10表示特殊功能、11保留不用。GPxDAT用于读/
写引脚:当引脚设为输入时,读此寄存器可知相应引脚的状态是高是低;当
引脚设为输出时,写此寄存器相应位可令此引脚输出低电平或高电平。GpxUP:
某位为0时,相应引脚无内部上拉;为1时,相应引脚使用内部上拉。
其他寄存器的操作在后续相关章节使用到时再描述;PORT A-H中引脚的特殊
功能比如串口引脚、中断引脚等,也在做相关实验时再描述。
目录KEY_LED中的程序功能为:当K1-K4中某个按键按下时,LED1-LED4中
相应LED点亮。
key_led.c代码:
1 #define GPBCON (*(volatile unsigned long *)0x56000010)
2 #define GPBDAT (*(volatile unsigned long *)0x56000014)
3 #define GPFCON (*(volatile unsigned long *)0x56000050)
4 #define GPFDAT (*(volatile unsigned long *)0x56000054)
/*
LED1-4对应GPB7-10
*/
5 #define GPB7_out (1<<(7*2))
6 #define GPB8_out (1<<(8*2))
7 #define GPB9_out (1<<(9*2))
8 #define GPB10_out (1<<(10*2))
/*
K1-K3对应GPF1-3
K4对应GPF7
*/
9 #define GPF1_in ~(3<<(1*2))
10 #define GPF2_in ~(3<<(2*2))
11 #define GPF3_in ~(3<<(3*2))
12 #define GPF7_in ~(3<<(7*2))
13 int main()
{
//LED1-LED4对应的4根引脚设为输出
14 GPBCON =GPB7_out | GPB8_out | GPB9_out | GPB10_out ;
//K1-K4对应的4根引脚设为输入
15 GPFCON &= GPF1_in & GPF2_in & GPF3_in & GPF7_in ;
16 while(1){
//若Kn为0(表示按下),则令LEDn为0(表示点亮)
17 GPBDAT = ((GPFDAT & 0x0e)<<6) | ((GPFDAT & 0x80)<<3);
}
18 return 0;
}
实验步骤:
a.进入目录KEY_LED,运行make命令生成key_led
b.烧写key_led
(4)实验四:arm-linux-ld
在开始后续实验之前,我们得了解一下arm-linux-ld连接命令的使用。在
上述实验中,我们一直使用类似如下的命令进行连接:
arm-linux-ld -Ttext 0x00000000 crt0.o led_on_c.o -o led_on_c_tmp.o
我们看看它是什么意思:-o选项设置输出文件的名字为led_on_c_tmp.o;
“--Ttext 0x00000000”设置代码段的起始地址为0x00000000;这条指令的作用就
是将crt0.o和led_on_c.o连接成led_on_c_mp.o可执行文件,此可执行文件的代
码段起始地址为0x00000000。
我们感兴趣的就是“—Ttext”选项!进入LINK目录,link.s代码如下:
1 .text
2 .global _start
3 _start:
4 b step1
5 step1:
6 ldr pc, =step2
7 step2:
8 b step2
Makefile如下:
1 link:link.s
2 arm-linux-gcc -c -o link.o link.s
3 arm-linux-ld -Ttext 0x00000000 link.o -o link_tmp.o
4 # arm-linux-ld -Ttext 0x30000000 link.o -o link_tmp.o
5 arm-linux-objcopy -O binary -S link_tmp.o link
6 arm-linux-objdump -D -b binary -m arm link >ttt.s
7 # arm-linux-objdump -D -b binary -m arm link >ttt2.s
8 clean:
9 rm -f link
10 rm -f link.o
11 rm -f link_tmp.o
实验步骤:
1.进入目录LINK,运行make生成arm-linux-ld选项为“-Ttext 0x00000000”
的反汇编码ttt.s
2.make clean
3.修改Makefile:将第4、7行的“#”去掉,在第3、6行前加上“#”
4.运行make生成arm-linux-ld选项为“-Ttext 0x30000000”的反汇编码ttt2.s
link.s程序中用到两种跳转方法:b跳转指令、直接向pc寄存器赋值。我们先
把在不同“—Ttext”选项下,生成的可执行文件的反汇编码列出来,再详细分析这
两种不同指令带来的差异。
ttt.s: ttt2.s
0: eaffffff b 0x4 0: eaffffff b 0x4
4: e59ff000 ldr pc, [pc, #0] ; 0xc 4: e59ff000 ldr pc, [pc, #0] ; 0xc
8: eafffffe b 0x8 8: eafffffe b 0x8
c: 00000008 andeq r0, r0, r8 c: 30000008 tsteq r0, #8 ; 0x8
先看看b跳转指令:它是个相对跳转指令,其机器码格式如下:
[31:28]位是条件码;[27:24]位为“1010”时,表示B跳转指令,为“1011”时,表示BL
跳转指令;[23:0]表示偏移地址。使用B或BL跳转时,下一条指令的地址是这样计算的:将指
令中24位带符号的补码立即数扩展为32(扩展其符号位);将此32位数左移两位;将得到的值
加到pc寄存器中,即得到跳转的目标地址。我们看看第一条指令“b step1”的机器码eaffffff:
1. 24位带符号的补码为0xffffff,将它扩展为32得到:0xffffffff
2.将此32位数左移两位得到:0xfffffffc,其值就是-4
3.pc的值是当前指令的下两条指令的地址,加上步骤2得到的-4,这恰好是第
二条指令step1的地址
各位不要被被反汇编代码中的“b 0x4”给迷惑了,它可不是说跳到绝对地址0x4
处执行,绝对地址得像上述3个步骤那样计算。您可以看到b跳转指令是依赖于当
前pc寄存器的值的,这个特性使得使用b指令的程序不依赖于代码存储的位置——
即不管我们连接命令中“--Ttext”为何,都可正确运行。
再看看第二条指令ldr pc, =step2:从反汇编码“ldr pc, [pc, #0]”可以看出,
这条指令从内存中某个位置读出数据,并赋给pc寄存器。这个位置的地址是当前
pc寄存器的值加上偏移值0,其中存放的值依赖于连接命令中的“--Ttext”选项。
执行这条指令后,对于ttt.s,pc=0x00000008;对于ttt2.s, pc=0x30000008。于
是执行第三条指令“b step2”时,它的绝对地址就不同了:对于ttt.s,绝对地址
为0x00000008;对于ttt.s,绝对地址为0x30000008。
ttt2.s上电后存放的位置也是0,但是它连接的地址是0x30000000。我们以后
会经常用到“存储地址和连接地址不同”(术语上称为加载时域和运行时域)的特性:
大多机器上电时是从地址0开始运行的,但是从地址0运行程序在性能方面总有很
多限制,所以一般在开始的时候,使用与位置无关的指令将程序本身复制到它的连
接地址处,然后使用向pc寄存器赋值的方法跳到连接地址开始的内存上去执行剩下
的代码。在实验5、6中,我们将会作进一步介绍。
arm-linux-ld命令中选项“-Ttext”也可以使用选项“-Tfilexxx”来代替,在
文件filexxx中,我们可以写出更复杂的参数来使用arm-linux-ld命令——在实验
6中,我们就是使用这种方法来指定连接参数的。
(5)实验五:MEMORY CONTROLLER
S3C2410提供了外接ROM、SRAM、SDRAM、NOR Flash、NAND Flash的接口。S3C2410
外接存储器的空间被分为8 BANKS,每BANK容量为128M:当访问BANKx(x从0到7)
所对应的地址范围(x*128M到(x+1)*128M-1,BANK6、7有稍微差别,请参考下面
第5点BANKSIZE寄存器的说明)时,片选信号nGCSx有效。本文所用的开发板,使
用了64M的NAND Flash和64M的SDRAM:NAND Flash不对应任何BANK,它是通过几
组寄存器来访问的,在上电后,NAND Flash开始的4k数据被自动地复制到芯片内
部一个被称为“Steppingstone”的RAM上。Steppingstone被映射为地址0,上面
的4k程序完成必要的初始化;SDRAM使用BANK6,它的物理起始地址为
6*128M=0x30000000。请您打开S3C2410数据手册,第5章的图“Figure 5-1.
S3C2410X Memory Map after Reset”可让您一目了然。
在开始下面内容前,如果您对SDRAM没什么概念,建议先看看这篇文章《高
手进阶,终极内存技术指南——完整/进阶版》。当然,不看也没关系,照着做就
行了。此文链接地址:
http://bbs.cpcw.com/viewthread.php?tid=196978&fpage=1&highlight=
本实验介绍如何使用SDRAM,这需要设置13个寄存器。呵呵,别担心,这些
寄存器很多是类似的,并且由于我们只使用了BANK6,大部分的寄存器我们不必
理会:
1.BWSCON:对应BANK0-BANK7,每BANK使用4位。这4位分别表示:
a.STx:启动/禁止SDRAM的数据掩码引脚,对于SDRAM,此位为0;对于
SRAM,此位为1。
b.WSx:是否使用存储器的WAIT信号,通常设为0
c.DWx:使用两位来设置存储器的位宽:00-8位,01-16位,10-32位,
11-保留。
d.比较特殊的是BANK0对应的4位,它们由硬件跳线决定,只读。
对于本开发板,使用两片容量为32Mbyte、位宽为16的SDRAM组成容量为
64Mbyte、位宽为32的存储器,所以其BWSCON相应位为:0010。对于本开发板,
BWSCON可设为0x22111110:其实我们只需要将BANK6对应的4位设为0010即可,其
它的是什么值没什么影响,这个值是参考手册上给出的。
2.BANKCON0-BANKCON5:我们没用到,使用默认值0x00000700即可
3.BANKCON6-BANKCON7:设为0x00018005
在8个BANK中,只有BANK6和BANK7可以使用SRAM或SDRAM,所以BANKCON6-7
与BANKCON0-5有点不同:
a.MT([16:15]):用于设置本BANK外接的是SRAM还是SDRAM:SRAM-0b00,
SDRAM-0b11
b.当MT=0b11时,还需要设置两个参数:
Trcd([3:2]):RAS to CAS delay,设为推荐值0b01
SCAN([1:0]):SDRAM的列地址位数,对于本开发板使用的SDRAM
HY57V561620CT-H,列地址位数为9,所以SCAN=0b01。如果使用其他
型号的SDRAM,您需要查看它的数据手册来决定SCAN的取值:00-8位,
01-9位,10-10位
4.REFRESH(SDRAM refresh control register):设为0x008e0000+ R_CNT
其中R_CNT用于控制SDRAM的刷新周期,占用REFRESH寄存器的[10:0]位,
它的取值可如下计算(SDRAM时钟频率就是HCLK):
R_CNT = 2^11 + 1 – SDRAM时钟频率(MHz) * SDRAM刷新周期(uS)
在未使用PLL时,SDRAM时钟频率等于晶振频率12MHz;SDRAM的刷新周期
在SDRAM的数据手册上有标明,在本开发板使用的SDRAM HY57V561620CT-H的
数据手册上,可看见这么一行“8192 refresh cycles / 64ms”:所以,刷
新周期=64ms/8192 = 7.8125 uS。
对于本实验,R_CNT = 2^11 + 1 – 12 * 7.8125 = 1955,
REFRESH=0x008e0000 + 1955 = 0x008e07a3
5.BANKSIZE:0x000000b2
位[7]=1:Enable burst operation
位[5]=1:SDRAM power down mode enable
位[4]=1:SCLK is active only during the access (recommended)
位[2:1]=010:BANK6、BANK7对应的地址空间与BANK0-5不同。BANK0-5
的地址空间都是固定的128M,地址范围是(x*128M)到(x+1)*128M-1,x
表示0到5。但是BANK7的起始地址是可变的,您可以从S3C2410数据手册
第5章“Table 5-1. Bank 6/7 Addresses”中了解到BANK6、7的地址范
围与地址空间的关系。本开发板仅使用BANK6的64M空间,我们可以令位
[2:1]=010(128M/128M)或001(64M/64M):这没关系,多出来的空间程序
会检测出来,不会发生使用不存在的内存的情况——后面介绍到的
bootloader和linux内核都会作内存检测。
位[6]、位[3]没有使用
6.MRSRB6、MRSRB7:0x00000030
能让我们修改的只有位[6:4](CL),SDRAM HY57V561620CT-H不支持CL=1
的情况,所以位[6:4]取值为010(CL=2)或011(CL=3)。
只要我们设置好了上述13个寄存器,往后SDRAM的使用就很简单了。本实验
先使用汇编语言设置好SDRAM,然后把程序本身从Steppingstone(还记得吗?本
节开始的时候提到过,复位之后NAND Flash开头的4k代码会被自动地复制到这里)
复制到SDRAM处,然后跳到SDRAM中执行。
本实验源代码在SDRAM目录中, head.s开头的代码如下:
1 bl disable_watch_dog
2 bl memsetup
3 bl copy_steppingstone_to_sdram
4 ldr pc, =set_sp @跳到SDRAM中继续执行
5 set_sp:
6 ldr sp, =0x34000000 @设置堆栈
7 bl main @跳转到C程序main函数
8 halt_loop:
9 b halt_loop
为了让程序结构简单一点,我都使用函数调用的方式。第一条指令是禁止
WATCH DOG,您如果细心的话,一定会发现程序LEDS运行得有些不正常,那是因
为WATCH DOG在不断地重启系统。以前为了程序简单,我没有把这段程序加上去。
往WTCON寄存器(地址0x53000000)写入0即可禁止WATCH DOG。第二条指令设置本
节开头所描述的13个寄存器,以便使用SDRAM。请您翻看实验四最后一段文字,
往下程序做的事情就是:将Steppingstone中的代码复制到SDRAM中(起始地址为
0x30000000),然后向pc寄存器直接赋值跳到SDRAM中执行下一条指令“ldr sp,
=0x34000000”。再往后的代码就和实验二、三一样了。
最后我们来看看SDRAM目录下的Makefile:
1 sdram : head.s sdram.c
2 arm-linux-gcc -c -o head.o head.s
3 arm-linux-gcc -c -o sdram.o sdram.c
4 arm-linux-ld -Ttext 0x30000000 head.o sdram.o -o sdram_tmp.o
5 arm-linux-objcopy -O binary -S sdram_tmp.o sdram
请看第4句,是否和实验四联系起来了呢?忘记的人请回头复习,不再罗嗦。
在目录SDRAM下执行make指令生成可执行文件sdram后,下载到板子上运行,
可以发现与LEDS程序相比,LED闪烁得更慢:这就对了,外部SDRAM的性能比起内
部SRAM来说性能是差些。
把程序从性能更好的内部SRAM移到外部SDRAM中去,是否多此一举呢?内部
SRAM只有4k大小,如果我们的程序大于4k,那么就不能指望利用内部SRAM来运行
了。所以得想办法把存储在NAND Flash中的代码,复制到SDRAM中去。对于NAND
Flash中的前4k,芯片自动把它复制到内部SRAM中,我们可以很轻松地再把它复
制到SDRAM中(实验五中函数copy_steppingstone_to_sdram就做这事)。但是对于
4k之后的代码,复制它就不那么轻松了,这正是实验六的内容:
(6)实验六:NAND FLASH CONTROLLER
当OM1、OM0都是低电平(请看数据手册198页)——即开发板插上BOOT SEL跳
线时,S3C2410从NAND Flash启动:NAND Flash的开始4k代码会被自动地复制到
内部SRAM中。我们需要使用这4k代码来把更多的代码从NAND Flash中读到SDRAM
中去。NAND Flash的操作通过NFCONF、NFCMD、NFADDR、NFDATA、NFSTAT和NFECC
六个寄存器来完成。在开始下面内容前,请打开S3C2410数据手册和NAND Flash
K9F1208U0M的数据手册。
在S3C2410数据手册218页,我们可以看到读写NAND Flash的操作次序:
1. Set NAND flash configuration by NFCONF register.
2. Write NAND flash command onto NFCMD register.
3. Write NAND flash address onto NFADDR register.
4. Read/Write data while checking NAND flash status by NFSTAT
register. R/nB signal should be checked before read operation or
after program operation.
下面依次介绍:
1、NFCONF:设为0xf830——使能NAND Flash控制器、初始化ECC、NAND Flash
片选信号nFCE=1(inactive,真正使用时再让它等于0)、设置TACLS、TWRPH0、
TWRPH1。需要指出的是TACLS、TWRPH0和TWRPH1,请打开S3C2410数据手册218页,
可以看到这三个参数控制的是NAND Flash信号线CLE/ALE与写控制信号nWE的时
序关系。我们设的值为TACLS=0,TWRPH0=3,TWRPH1=0,其含义为:TACLS=1个HCLK
时钟,TWRPH0=4个HCLK时钟,TWRPH1=1个HCLK时钟。请打开K9F1208U0M数据手册
第13页,在表“AC Timing Characteristics for Command / Address / Data
Input”中可以看到:
CLE setup Time = 0 ns,CLE Hold Time = 10 ns,
ALE setup Time = 0 ns,ALE Hold Time = 10 ns,
WE Pulse Width = 25 ns
可以计算,即使在HCLK=100MHz的情况下,TACLS+TWRPH0+TWRPH1=6/100 uS=60
ns,也是可以满足NAND Flash K9F1208U0M的时序要求的。
2、NFCMD:
对于不同型号的Flash,操作命令一般不一样。对于本板使用的K9F1208U0M,
请打开其数据手册第8页“Table 1. Command Sets”,上面列得一清二楚。
3、NFADDR:无话可说
4、NFDATA:只用到低8位
5、NFSTAT:只用到位0,0-busy,1-ready
6、NFECC:待补
现在来看一下如何从NAND Flash中读出数据,请打开K9F1208U0M数据手册第
29页“PAGE READ”,跟本节的第2段是遥相呼应啊,提炼出来罗列如下(设读
地址为addr):
1、NFCONF = 0xf830
2、在第一次操作NAND Flash前,通常复位一下:
NFCONF &= ~0x800 (使能NAND Flash)
NFCMD = 0xff (reset命令)
循环查询NFSTAT位0,直到它等于1
3、NFCMD = 0 (读命令)
4、这步得稍微注意一下,请打开K9F1208U0M数据手册第7页,那个表格列出
了在地址操作的4个步骤对应的地址线,A8没用到:
NFADDR = addr & 0xff
NFADDR = (addr>>9) & 0xff (注意了,左移9位,不是8位)
NFADDR = (addr>>17) & 0xff (左移17位,不是16位)
NFADDR = (addr>>25) & 0xff (左移25位,不是24位)
5、循环查询NFSTAT位0,直到它等于1
6、连续读NFDATA寄存器512次,得到一页数据(512字节)
7、NFCONF |= 0x800 (禁止NAND Flash)
本实验代码在NAND目录下,源代码为head.s、init.c和main.c。head.s调用
init.c中的函数来关WATCH DOG、初始化SDRAM、初始化NAND Flash,然后将main.c
中的代码从NAND Flash地址4096开始处复制到SDRAM中,最后,跳到main.c中的
main函数继续执行。代码本身没什么难度,与前面程序最大的差别就是“连接脚
本”的引入:实验4最后一段提到过在arm-linux-ld命令中,选项“-Ttext”可
以使用选项“-Tfilexxx”来代替。在本实验中,使用“-Tnand.lds”。nand.lds
内容如下:
1 SECTIONS {
2 firtst 0x00000000 : { head.o init.o }
3 second 0x30000000 : AT(4096) { main.o }
4 }
完整的连接脚本文件形式如下:
SECTIONS {
...
secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
{ contents } >region :phdr =fill
...
}
并非每个选项都是必须的,仅介绍nand.lds用到的:
1、secname:段名,对于nand.lds,段名为first和second
2、start:本段运行时的地址,如果没有使用AT(xxx),则本段存储的地址
也是start
3、AT( ldadr ):定义本段存储(加载)的地址
4、{ contents }:决定哪些内容放在本段,可以是整个目标文件,也可以
是目标文件中的某段(代码段、数据段等)
nand.lds的含义是:head.o放在0x00000000地址开始处,init.o放在hean.o
后面,它们的运行地址是0x00000000;main.o放在地址4096(0x1000)开始处,但
是它的运行地址在0x30000000,在运行前需要从4096处复制到0x30000000处。为
了更形象一点,您可以打开反汇编文件ttt.s,现摘取部分内容如下:
00000000 <.data>:
1 0: e3a0da01 mov sp, #4096 ; 0x1000
2 4: eb00000b bl 0x38
3 8: eb000011 bl 0x54
4 c: eb000042 bl 0x11c
...
5 1000: e1a0c00d mov ip, sp
6 1004: e92dd800 stmdb sp!, {fp, ip, lr, pc}
7 1008: e24cb004 sub fp, ip, #4 ; 0x4
8 100c: e59f1058 ldr r1, [pc, #88] ; 0x106c
...
上面的第1-4行与head.s中的前面4行代码对应,第2-4行调用init.c中的函
数disable_watch_dog、memsetup、init_nand;再看看第5行,“1000”的得来
正是由于设置了“AT(4096)”,这行开始的是main.c中的第一个函数Rand()。
如果您想进一步了解连接脚本如何编写,请参考《Using ld The GNU linker》
(在目录“参考资料”下)。
上面的几个程序都是在摆弄那几个LED,现在来玩点有意思的:
(7)实验七:UART
UART的寄存器有11X3个(3个UART)之多,我选最简单的方法来进行本实
验,用到的寄存器也有8个。不过初始化就用去了5个寄存器,剩下的3个用于接
收、发送数据。如此一来,操作UART倒也不复杂。本板使用UART0:
1、初始化:
把使用到的引脚GPH2、GPH3定义为TXD0、RXD0:
GPHCON |= 0xa0
GPHUP |= 0x0c (上拉)
b.ULCON0 ( UART channel 0 line control register ):设为0x03
此值含义为:8个数据位,1个停止位,无校验,正常操作模式(与之相对的
是Infra-Red Mode,此模式表示0、1的方式比较特殊)。
c.UCON0 (UART channel 0 control register ):设为0x05
除了位[3:0],其他位都使用默认值。位[3:0]=0b0101表示:发送、接收都
使用“中断或查询方式”——本实验使用查询查询方式。
d.UFCON0 (UART channel 0 FIFO control register ):设为0x00
每个UART内部都有一个16字节的发送FIFO和接收FIFO,但是本实验
不使用FIFO,设为默认值0
e.UMCON0 (UART channel 0 Modem control register ):设为0x00
本实验不使用流控,设为默认值0
f.UBRDIV0 ( R/W Baud rate divisior register 0 ):设为12
本实验未使用PLL, PCLK=12MHz,设置波特率为57600,则由公式
UBRDIVn = (int)(PCLK / (bps x 16) ) –1
可以计算得UBRDIV0 = 12,请使用S3C2410数据手册第314页的误差公式
验算一下此波特率是否在可容忍的误差范围之内,如果不在,则需要更换另
一个波特率(本实验使用的57600是符合的)。
2、发送数据:
a.UTRSTAT0 ( UART channel 0 Tx/Rx status register ):
位[2]:无数据发送时,自动设为1。当我们要使用串口发送数据时,
先读此位以判断是否有数据正在占用发送口。
位[1]:发送FIFO是否为空,本实验未用此位
位[0]:接收缓冲区是否有数据,若有,此位设为1。本实验中,需
要不断查询此位一判断是否有数据已经被接收。
b.UTXH0 (UART channel 0 transmit buffer register ):
把要发送的数据写入此寄存器。
3、接收数据:
a.UTRSTAT0:如同上述“2、发送数据”所列,我们用到位[0]
b.URXH0 (UART channel 0 receive buffer register ):
当查询到UTRSTAT0 位[0]=1时,读此寄存器获得串口接收到的数据。
串口代码在UART目录下的serial.c文件中,包含三个函数:init_uart,putc,
getc。代码如下:
1 void init_uart( )
2 {//初始化UART
3 GPHCON |= 0xa0; //GPH2,GPH3 used as TXD0,RXD0
4 GPHUP = 0x0c; //GPH2,GPH3内部上拉
5 ULCON0 = 0x03; //8N1(8个数据位,无校验位,1个停止位)
6 UCON0 = 0x05; //查询方式
7 UFCON0 = 0x00; //不使用FIFO
8 UMCON0 = 0x00; //不使用流控
9 UBRDIV0 = 12; //波特率为57600
10 }
11 void putc(unsigned char c)
12 {
13 while( ! (UTRSTAT0 & TXD0READY) ); //不断查询,直到可以发送数据
14 UTXH0 = c; //发送数据
15 }
16 unsigned char getc( )
17 {
18 while( ! (UTRSTAT0 & RXD0READY) ); //不断查询,直到接收到了数据
19 return URXH0; //返回接收到的数据
20 }
本实验将串口输入的数字、字母加1后再从串口输出,比如输入A就输出B。
实验步骤:
1、进入UART目录运行“make”命令生成可执行文件uart,下载到开发板上
2、在主机上运行串口工具minicom:
a.在终端上运行“minicom –s”启动minicom,出来如下界面:
b.进入“Serial port setup”:
键入相应字母设置各项,比如:我用的是串口1,所以在A项设置为
“/dev/ttyS0”;按“E”,设置波特率为57600,8N1;按F、G,设置无流控。
最后回车退出,回到步骤a所示的界面。
c.可以选择“Save setup as dfl”,这样下次启动minicom时可以不再进行
步骤b的设置。
d.选择“Exit”退出设置界面。
3、复位开发板后,您可以在minicom上体验一下本程序了。
(8)实验八:printf、scanf
本实验利用串口实现两个很常用的函数:printf和scanf。
试验代码存放在stdio目录下,其中lib目录中包含了实现printf和scanf函
数的主要文件。大部分文件摘自linux2.6内核,本试验主要在vsprintf.c文件的
基础上,封装了printf和scanf函数(print.c文件中)。在vsprintf.c文件中,需
要用到一些乘法和除法操作,文件lib1funcs.S实现除法、求模操作;div64.h
和div64.S实现64位的除法操作;muldi3.c实现乘法操作。另外,stdio目录下的
serial.c文件实现了putc和getc函数,这两个函数与具体板子的情况相关,所以
我没把它们放入lib目录下。
此试验的Makefile文件将lib目录里的文件生成静态库文件libc.a,现在,
你把stdio.h文件包含进你的代码后,就可以非常方便地实现输入、输出了——
请参考本试验代码。
实验:与试验7类似,以波特率57600 8N1打开串口,将make后生成的可执行
文件exe烧入开发板,可在minicom上观察到结果:程序从minicom中接收字串,
从这些字串中检出数字,分别以10进制和16进制方式打印出来。
另外,您可以参考stdio_test_lib目录下的代码,写出自己的测试程序。这
个目录里面的文件基本与本试验一样,只是在lib目录下仅仅保留了libc.a库文
件。
最后,sys/lib/stdio目录中存放的是标准输入、输出的代码,其中的
Makefile可以生成libc.a文件并存放在sys/lib目录下,以后本人编写的库函数
都也将存放在此目录下。
(9)实验九:INTERRUPT CONTROLLER
S3C2410数据手册354页“Figure 14-1. Interrupt Process Diagram”非常简洁地
概括了中断处理的流程,我把这个图搬过来,然后结合用到的寄存器用文字解释
一下。
图1 Interrupt Process Diagram
SUBSRCPND和SRCPND寄存器表明有哪些中断被触发了,正在等待处理
(pending);SUBMASK(INTSUBMSK寄存器)和MASK(INTMSK寄存器)用于屏蔽某些中
断。图中的“Request sources(with sub -register)”表示的是INT_RXD0、
INT_TXD0等11个中断源,它们不同于“Request sources(without sub
-register)”:
1、“Request sources(without sub -register)”中的中断源被触发之后,SRCPND
寄存器中相应位被置1,如果此中断没有被INTMSK寄存器屏蔽、或者是快中断(FIQ)
的话,它将被进一步处理
2、对于“Request sources(with sub -register)”中的中断源被触发之后,
SUBSRCPND寄存器中的相应位被置1,如果此中断没有被INTSUBMSK寄存器屏蔽的
话,它在SRCPND寄存器中的相应位也被置1,之后的处理过程就和“Request
sources(without sub -register)”一样了
继续沿着图1前进:在SRCPND寄存器中,被触发的中断的相应位被置1,等待
处理:
1、如果被触发的中断中有快中断(FIQ)——MODE(INTMOD寄存器)中为1的位对应
的中断是FIQ,则CPU的FIQ中断函数被调用。注意:FIQ只能分配一个,即INTMOD
中只能有一位设为1。
2、对于一般中断IRQ,可能同时有几个中断被触发,未被INTMSK寄存器屏蔽的中
断经过比较后,选出优先级最高的中断——此中断在INTPND寄存器中的相应位被
置1,然后CPU调用IRQ中断处理函数。中断处理函数可以通过读取INTPND寄存器
来确定中断源是哪个,也可以读INTOFFSET寄存器来确定中断源。
请打开S3C2410数据手册357页,“Figure 14-2. Priority Generating Block”显示
了各中断源先经过6个一级优先级仲裁器选出各自优先级最高的中断,然后再经
过二级优先级仲裁器选从中选出优先级最高的中断。IRQ的中断优先级由RIORITY
寄存器设定,请参考数据手册365页,RIORITY寄存器中ARB_SELn(n从0到6)用于
设定仲裁器n各输入信号的中断优先级,例如ARB_SEL6[20:19](0最高,其后各项
依次降低):
00 = REQ 0-1-2-3-4-5 01 = REQ 0-2-3-4-1-5
10 = REQ 0-3-4-1-2-5 11 = REQ 0-4-1-2-3-5
RIORITY寄存器还有一项比较特殊的功能,如果ARB_MODEn设为1,则仲裁器n
中输入的中断信号的优先级别将会轮换。例如ARB_MODE6设为1,则仲裁器6的6
个输入信号的优先级将如下轮换(见数据手册358页):
If REQ0 or REQ5 is serviced, ARB_SEL bits are not changed at all.
If REQ1 is serviced, ARB_SEL bits are changed to 01b.
If REQ2 is serviced, ARB_SEL bits are changed to 10b.
If REQ3 is serviced, ARB_SEL bits are changed to 11b.
If REQ4 is serviced, ARB_SEL bits are changed to 00b.
意思即是:
REQ0和REQ5的优先级不会改变
当REQ1中断被处理后,ARB_SEL6 = 0b01,即REQ1的优先级变成本仲裁器
中最低的(除去REQ5)
当REQ2中断被处理后,ARB_SEL6 = 0b10,即REQ2的优先级变成本仲裁器
中最低的(除去REQ5)
当REQ3中断被处理后,ARB_SEL6 = 0b11,即REQ3的优先级变成本仲裁器
中最低的(除去REQ5)
当REQ4中断被处理后,ARB_SEL6 = 0b00,即REQ4的优先级变成本仲裁器
中最低的(除去REQ5)
现在来总结一下使用中断的步骤:
1、当发生中断IRQ时,CPU进入“中断模式”,这时使用“中断模式”下的堆栈;
当发生快中断FIQ时,CPU进入“快中断模式”,这时使用“快中断模式”下的堆
栈。所以在使用中断前,先设置好相应模式下的堆栈。
2、对于“Request sources(without sub -register)”中的中断,将INTSUBMSK
寄存器中相应位设为0
3、将INTMSK寄存器中相应位设为0
4、确定使用此的方式:是FIQ还是IRQ。
a.如果是FIQ,则在INTMOD寄存器设置相应位为1
b.如果是IRQ,则在RIORITY寄存器中设置优先级
5、准备好中断处理函数,
a.中断向量:
在中断向量设置好当FIQ或IRQ被触发时的跳转函数, IRQ、FIQ的中断向量
地址分别为0x00000018、0x0000001c(数据手册79页“Table 2-3. Exception
Vectors”)
b.对于IRQ,在跳转函数中读取INTPND寄存器或INTOFFSET寄存器的值来确
定中断源,然后调用具体的处理函数
c.对于FIQ,因为只有一个中断可以设为FIQ,无须判断中断源
d.中断处理函数进入和返回时需要花点心思:
i.对于IRQ,进入和返回的代码如下:
sub lr, lr, #4 @计算返回地址
stmdb sp!, { r0-r12,lr } @保存使用到的寄存器
. .
ldmia sp!, { r0-r12,pc }^ @中断返回
@^表示将spsr的值赋给cpsr
ii.对于FIQ,进入和返回的代码如下:
sub lr, lr, #4 @计算返回地址
stmdb sp!, { r0-r7,lr } @保存使用到的寄存器
. .
ldmia sp!, { r0-r7,pc }^ @快中断返回,
@^表示将spsr的值赋给cpsr
iii. 中断返回之前需要清中断:往SUBSRCPND(用到的话)、SRCPND、
INTPND中相应位写1即可。对于INTPND,最简单的方法就是“INTPND=INTPND”。
6、设置CPSR寄存器中的F-bit(对于FIQ)或I-bit(对于IRQ)为0,开中断
本实验使用按键K1-K4作为4个外部中断——EINT1-3、EINT7,当Kn按下时,
通过串口输出“EINTn,Kn pressed!”,主程序让4个LED轮流从0到15计数。对于
外部中断,除了上面说的几个寄存器外,还有几个寄存器需要设置,请打开数据
手册276页“EXTERNAL INTERRUPT CONTROL REGISTER (EXTINTn)”:
1、EXTINT0-2:它们用于设置EINT0-24共25个外部中断分别是低电平触发、高电
平触发、上升沿触发、下降沿触发或者“上升/下降沿触发”(不知道确切的术语)。
对于本实验,使用默认值:低电平触发。
2、EINTFLT0-3:未用
3、EINTMASK、EINTPEND:呵呵,这对寄存器和上面的INTMSK、INTPND实在相似。
EINTMASK用于设置是否评比EINT4-23;EINTPEND表明有几个外部中断已经发生,
正在等待处理(pending),对某位写入1可以让此位清零。对于本实验,EINTMASK
位[7]设为0——使用EINT7。
4、GSTATUS0-4:未用
本实验代码在INT目录下,下面摘取与中断相关的代码:
(1)、head.s中:
. .
1 msr cpsr_c, #0xd2 @进入中断模式
2 ldr sp, =0x33000000 @设置中断模式堆栈
3 msr cpsr_c, #0xdf @进入系统模式
4 ldr sp, =0x34000000 @设置系统模式堆栈
5 bl init_irq @调用中断初始化函数,在init.c中
6 msr cpsr_c, #0x5f @设置I-bit=0,开IRQ中断
. .
7 HandleIRQ: @IRQ中断向量跳转函数
8 sub lr, lr, #4 @计算返回地址
9 stmdb sp!, { r0-r12,lr } @保存使用到的寄存器
10 ldr lr, =int_return @设置返回地址
11 ldr pc, =EINT_Handle @调用中断处理函数EINT_Handle,
@在interrupt.c中
12 int_return:
13 ldmia sp!, { r0-r12,pc }^ @中断返回,
@^表示将spsr的值复制到cpsr
(2)、init.c的中断初始化函数init_irq:
1 #define EINT1 (2<<(1*2))
2 #define EINT2 (2<<(2*2))
3 #define EINT3 (2<<(3*2))
4 #define EINT7 (2<<(7*2))
5 void init_irq( )
6 {
7 GPFCON |= EINT1 | EINT2 | EINT3 | EINT7; //K1-K4对应
//EINT1-3和EINT7
8 GPFUP |= (1<<1) | (1<<2) | (1<<3) | (1<<7); //上拉
9 EINTMASK &= (~0x80); //EINT7使能,对于外部中断EINT4-23,
//除下面的INTMSK外,还须设置EINTMASK
10 INTMSK &= (~0x1e); //EINT1-3、4-7使能(EINT4-7共用INTMSK[4])
11 PRIORITY &= (~0x03); //设定优先级
12 }
(3)、interrupt.c:
1 void EINT_Handle()
2 {
3 unsigned long oft = INTOFFSET;
4 switch( oft )
5 {
6 case 1: printk("EINT1, K1 pressed!/n/r"); break;
7 case 2: printk("EINT2, K2 pressed!/n/r"); break;
8 case 3: printk("EINT3, K3 pressed!/n/r"); break;
9 case 4: printk("EINT7, K4 pressed!/n/r"); break;
10 default: printk("Interrupt unknown!/n/r"); break;
11 }
//以下清中断
12 if( oft == 4 ) EINTPEND = 1<<7; //EINT4-7合用IRQ4,
//注意:EINTPEND[3:0]保留未用,
//向这些位写入1可能导致未知结果
14 SRCPND = 1<
15 INTPND = INTPND; //清0
16 }
实验步骤:
1、进入INT目录运行“make”命令生成可执行文件int,下载到板子上
2、打开minicom(拨特率:57600,8N1)
3、按下按钮即可看见串口输出
4、您可以同时按住几个按钮观察串口输出,可以发现K1优先级最高,K4最低
(10)实验十:TIMER
本实验利用TIMER0触发中断,使4个LED 1秒钟闪一次。先简述一下TIMER的
工作过程。请打开数据手册284页,我在“Figure 10-1. 16-bit PWM Timer Block
Diagram”上进行解说。摘录此图,见下面图3。
对于TIMER0,PCLK被“8-Bit Prescaler”(在TCFG0中定义)分频后,进入“Clock
Divider”,“Clock Divider”有5种频率输出:从“8-Bit Prescaler”进来的
时钟的2分频、4分频、8分频、16分频,或者外部时钟TCLK0。后面的“5:1 MUX”
(5选1的选择器)使用TCFG1来确定使用哪种频率。“Control Logic0”在“5:1 MUX”
的输出的频率下工作。对于“Control Logic0”,我得画图说明:
图2 Control Logic0内部结构
“Control Logic0”的工作流程为(请参考数据手册287页“TIMER
INITIALIZATION USING MANUAL UPDATE BIT AND INVERTER BIT”):
1、上电之后,设置TCMPB0和TCNTB0寄存器
2、设置TCON寄存器bit[1]为1,这样,TCMPB0和TCNTB0寄存器的值就被装入TCMP0
和TCNT0寄存器中;设置bit[2]确定是否使用“Timer 0 output inverter”
3、设置TCON寄存器bit[0]启动TIMER0,设置bit[3]确定是否使用“Timer 0 auto
reload”
4、当TIMER0启动后,TCNT0在“5:1 MUX”输出的时钟下,进行减1计数。当TCNT0
等于TCMP0时:TOUT0输出翻转;当TCNT0等于0时:TOUT0输出翻转,TIMER0中断
被触发(如果此中断使能了的话),TCMPB0和TCNTB0寄存器的值被自动装入TCMP0
和TCNT0寄存器中(如果TCON寄存器bit[3]等于1的话)——此时下个计数流程开
始
图3 16-bit PWM Timer Block Diagram
现在介绍一下上面说到的几个寄存器:
1、TCFG0和TCFG1:分别设为119和0x03
这连个寄存器用于设置“Control Logic”的时钟,计算公式如下:
Timer input clock Frequency = PCLK / {prescaler value+1} / {divider value}
对于TIMER0,prescaler value = TCFG0[7:0],divider value由TCFG[3:0]
确定(0b000:2,0b001:4,0b010:8,0b0011:16,0b01xx:使用外部TCLK0)。
对于本实验,TIMER0时钟 = 12MHz/(119+1)/(16) = 6250Hz
2、TCNTB0:设为3125
在6250Hz的频率下,此值对应的时间为0.5S
3、TCON:
TIMER0对应bit[3:0]:
bit[3]用于确定在TCNT0计数到0时,是否自动将TCMPB0和TCNTB0寄存器
的值装入TCMP0和TCNT0寄存器中
bit[2]用于确定TOUT0是否反转输出(本实验未用)
bit[1]用于手动更新TCMP0和TCNT0寄存器:在第一次使用定时器前,此
位需要设为1,此时TCMPB0和TCNTB0寄存器的值装入TCMP0和TCNT0寄存器中
bit[0]用于启动TIMER0
4、TCONO0:只读寄存器,用于读取当前TCON0寄存器的值,本实验未用
本实验的代码在TIMER目录下,init.c中的Timer0_init函数初始化并启动
TIMER0:
1 void Timer0_init()
2 {
3 TCFG0 = 119; //Prescaler0 = 119
4 TCFG1 = 0x03; //Select MUX input for PWM Timer0:divider=16
5 TCNTB0 = 3125; //0.5秒钟触发一次中断
6 TCON |= (1<<1); //Timer 0 manual update
7 TCON = 0x09; /*Timer 0 auto reload on
Timer 0 output inverter off
清"Timer 0 manual update"
Timer 0 start */
8 }
init.c中的init_irq函数使能TIEMR0中断:
1 void init_irq( )
2 {
3 INTMSK &= (~(1<<10)); //INT_TIMER0中断使能
4 }
interrupt.c中的Timer0_Handle函数用于处理TIMER0中断:每0.5s发生一次
中断,中断发生时将4个LED的状态反转,即1s闪一次:
1 void Timer0_Handle()
2 {
3 if(INTOFFSET == 10){
4 GPBDAT = ~(GPBDAT & (0xf << 7));
5 }
6 //清中断
7 SRCPND = 1 << INTOFFSET;
8 INTPND = INTPND;
9 }
(11)实验十一:MMU
在理论上概括或解释MMU,这不是我能胜任的。我仅基于为了理解本实验中
操作MMU的代码而对MMU做些说明,现在先简单地描述虚拟地址(VA)、变换后的虚
拟地址(MVA)、物理地址(PA)之间的关系:
启动MMU后,S3C2410的CPU核看到的、用到的只是虚拟地址VA,至于VA如何
最终落实到物理地址PA上,CPU是不理会的。而caches和MMU也是看不见VA的,它
们利用VA变换得来的MVA去进行后续操作——转换成PA去读/写实际内存芯片,
MVA是除CPU外的其他部分看见的虚拟地址。对于VA与MVA之间的变换关系,请打
开数据手册551页,我摘取了“Figure 2-8. Address Mapping Using CP15 Register 13”:
图4 VA与MVA的关系
如果VA<32M,需要使用进程标识号PID(通过读CP15的C13获得)来转换为MVA。
VA与MVA的转换方法如下(这是硬件自动完成的):
if(VA < 32M) then
MVA = VA | (PID << 25) //VA < 32M
else
MVA = VA //VA >= 32M
利用PID生成MVA的目的是为了减少切换进程时的代价:如果两个进程占用的
虚拟地址空间(VA)有重叠,不进行上述处理的话,当进行进程切换时必须进行虚
拟地址到物理地址的重新影射,这需要重建页表、使无效caches和TLBS等等,代
价非常大。但是如果像上述那样处理的话,进程切换就省事多了:假设两个进程
1、2运行时的VA都是0-32M,则它们的MVA分别是(0x02000000-0x03ffffff)、
(0x04000000-0x05ffffff)——前面说过MMU、Caches使用MVA而不使用VA,这样
就不必进行重建页表等工作了。
现在来讲讲MVA到PA的变换过程:请打开数据手册557页,“Figure 3-1.
Translating Page Tables”(见下述图5)非常精练地概括了对于不同类型的页表,MVA
是如何转换为PA的。图中的页表“Translation table”起始地址为“TTB base”,
在建立页表后,写入CP15的寄存器C2。
使用MVA[31:20]检索页表“Translation table”得到一个页表项(entry,4
字节),根据此entry的低2位,可分为以下4种:
1、0b00:无效
2、0b01:粗表(Coarse page)
entry[31:10]为粗表基址(Coarse page table base address),据此可以确
定一块1K大小的内存——称为粗页表(Coarse page table,见图5)。
粗页表含256个页表项,每个页表项对应一块4K大小的内存,每个页表项又
可以分为大页描述符、小页描述符。MVA[19:12]用来确定页表项。一个大页(64K)
对应16个大页描述符,这16个大页描述符相邻且完全相同,entry[31:16]为大页
基址(Large page base)。MVA[15:0]是大页内的偏移地址。一个小页(4K)对应1
个小页描述符,entry[31:12]为小页基址(Small page base)。MVA[11:0]是小页
内的偏移地址。
3、0b10:段(Section)
段的操作最为简单,entry[31:20]为段基址(Section base),据此可以确定
一块1M大小的内存(Section,见图5),而MVA[19:0]则是块内偏移地址
4、0b11:细表(Fine page)
entry[31:12]为细表基址(Fine page table base address),据此可以确定
一块4K大小的内存——称为细页表(Fine page table,见图5)。
细页表含1024个页表项,每个页表项对应一块1K大小的内存,每个页表项又
可以分为大页描述符、小页描述符、极小页描述符。MVA[19:10]用来确定页表项。
一个大页(64K)对应64个大页描述符,这64个大页描述符相邻且完全相同,
entry[31:16]为大页基址(Large page base)。MVA[15:0]是大页内的偏移地址。
一个小页(4K)对应4个小页描述符,entry[31:12]为小页基址(Small page base)。
MVA[11:0]是小页内的偏移地址。极小页(1K)对应1个极小页描述符,entry[31:10]
为极小页基址(Tiny page base)。MVA[9:0]是极小页内的偏移地址。
图5 Translating Page Tables
访问权限的检查是MMU主要功能之一,它由描述符的AP和domain、CP15寄存
器C1的R/S/A位、CP15寄存器C3(域访问控制)等联合作用。本实验不使用权限检
查(令C3为全1)。
下面简单介绍一下使用Cache和Write buffer:
1、“清空”(clean)的意思是把Cache或Write buffer中已经脏的(修改过,但未
写入主存)数据写入主存
2、“使无效”(invalidate):使之不能再使用,并不将脏的数据写入主存
3、对于I/O影射的地址空间,不使用Cache和Write buffer
4、在使用MMU前,使无效Cache和drain write buffer
与cache类似,在使用MMU前,使无效TLB。
上面有些部分讲得很简略,除了作者水平不足之外,还在于本书的侧重点—
—实验。理论部分就麻烦各位自己想办法了。不过这些内容也足以了解本实验的
代码了。本实验与实验9完成同样的功能:使用按键K1-K4作为4个外部中断——
EINT1-3、EINT7,当Kn按下时,通过串口输出“EINTn,Kn pressed!”,主程序
让4个LED轮流从0到15计数。代码在目录MMU下。下面摘取与MMU相关的代码详细
说明。
先看看head.s代码(将一些注释去掉了):
1 b Reset
2 HandleUndef:
3 b HandleUndef
4 HandleSWI:
5 b HandleSWI
6 HandlePrefetchAbort:
7 b HandlePrefetchAbort
8 HandleDataAbort:
9 b HandleDataAbort
10 HandleNotUsed:
11 b HandleNotUsed
12 ldr pc, HandleIRQAddr
13 HandleFIQ:
14 b HandleFIQ
15 HandleIRQAddr:
16 .long HandleIRQ
17 Reset: @函数disable_watch_dog, memsetup, init_nand,
@nand_read_ll在init.c中定义
18 ldr sp, =4096 @设置堆栈
19 bl disable_watch_dog @关WATCH DOG
20 bl memsetup_2 @初始化SDRAM
21 bl init_nand @初始化NAND Flash
22 bl copy_vectors_from_nand_to_sdram @在init.c中
23 bl copy_process_from_nand_to_sdram @在init.c中
24 ldr sp, =0x30100000 @重新设置堆栈
@(因为下面就要跳到SDRAM中执行了)
25 ldr pc, =run_on_sdram @跳到SDRAM中
26 run_on_sdram:
27 bl mmu_tlb_init @调用C函数mmu_tlb_init(mmu.c中),建立页表
28 bl mmu_init @调用C函数mmu_init(mmu.c中),使能MMU
29 msr cpsr_c, #0xd2 @进入中断模式
30 ldr sp, =0x33000000 @设置中断模式堆栈
31 msr cpsr_c, #0xdf @进入系统模式
32 ldr sp, =0x30100000 @设置系统模式堆栈
33 bl init_irq @调用中断初始化函数,在init.c中
34 msr cpsr_c, #0x5f @设置I-bit=0,开IRQ中断
35 ldr lr, =halt_loop @设置返回地址
36 ldr pc, =main @b指令和bl指令只能前后跳转32M的范围,
@所以这里使用向pc赋值的方法进行跳转
37 halt_loop:
38 b halt_loop
39 HandleIRQ:
40 sub lr, lr, #4 @计算返回地址
41 stmdb sp!, { r0-r12,lr } @保存使用到的寄存器
42 ldr lr, =int_return @设置返回地址
43 ldr pc,=EINT_Handle @调用中断处理函数,在interrupt.c中
44 int_return:
45 ldmia sp!, { r0-r12,pc }^ @中断返回,
@^表示将spsr的值复制到cpsr
请注意第12、15行,我们将IRQ中断向量由以前的“b HandleIRQ”换成了:
12 ldr pc, HandleIRQAddr
15 HandleIRQAddr:
16 .long HandleIRQ
这是因为b跳转指令只能前后跳转32M的范围,而本实验中中断向量将重新放
在VA=0xffff0000开始处(而不是通常的0x00000000),到HandleIRQAddr的距离远
远超过了32M。将中断向量重新定位在0xffff0000处,是因为MMU使能后,中断发
生时:
1、如果中断向量放在0x00000000处,则对于不同的进程(PID),中断向量的MVA
将不同
2、如果中断向量放在0xffff0000处,则对于不同的进程(PID),中断向量的MVA
也相同
显然,如果使用1,则带来的麻烦非常大——对于每个进程,都得设置自己
的中断向量。所以MMU使能后,处理中断的方法应该是2。
第22行copy_vectors_from_nand_to_sdram函数将中断向量复制到内存物理
地址0x33ff0000处,在mmu_tlb_init函数中会把0x33ff0000影射为虚拟地址
0xffff0000。
第23行copy_process_from_nand_to_sdram函数将存在nand flash开头的4K
代码全部复制到0x30004000处(本实验的连接地址为0x30004000)。请注意SDRAM
起始地址为0x30000000,前面的16K空间用来存放一级页表(在mmu_tlb_init中设
置)。
第27行mmu_tlb_init函数设置页表。本实验以段的方式使用内存,所以仅使
用一级页表,且页表中所有页表项均为段描述符。
mmu_tlb_init代码(在mmu.c中)如下:
1 void mmu_tlb_init()
2 {
3 unsigned long entry_index;
4 /*SDRAM*/
5 for(entry_index = 0x30000000; entry_index < 0x34000000;
entry_index+=0x100000){
6 /*section table's entry:AP=0b11,domain=0,Cached,write-through mode(WT)*/
7 *(mmu_tlb_base+(entry_index>>20)) =
entry_index |(0x03<<10)|(0<<5)|(1<<4)|(1<<3)|0x02;
8 }
9 /*SFR*/
10 for(entry_index = 0x48000000; entry_index < 0x60000000;
entry_index += 0x100000){
11 /*section table's entry:AP=0b11,domain=0,NCNB*/
12 *(mmu_tlb_base+(entry_index>>20)) =
entry_index |(0x03<<10)|(0<<5)|(1<<4)| 0x02;
13 }
14 /*exception vector*/
15 /*section table's entry:AP=0b11,domain=0,Cached,write-through mode(WT)*/
16 *(mmu_tlb_base+(0xffff0000>>20)) =
(VECTORS_PHY_BASE) |(0x03<<10)|(0<<5)|(1<<4)|(1<<3)|0x02;
17 }
第4-8行令64M SDRAM的虚拟地址和物理地址相等——从0x30000000到
0x33ffffff,这样可以使得在head.s中第28行调用mmu_init使能MMU前后的地址
一致。
第9-13行设置特殊功能寄存器的虚拟地址,也让它们的虚拟地址和物理地址
相等——从0x48000000到0x5fffffff。并且不使用cache和write buffer。
第14-17行设置中断向量的虚拟地址,虚拟地址0xfff00000对应物理地址
0x33f00000。
回到head.s中第28行,调用mmu.c中的mmu_init函数使能MMU,此函数代码入
下:
1 void mmu_init()
2 {
3 unsigned long ttb = MMU_TABLE_BASE;
4 __asm__(
5 "mov r0, #0/n"
6 /* invalidate I,D caches on v4 */
7 "mcr p15, 0, r0, c7, c7, 0/n"
8 /* drain write buffer on v4 */
9 "mcr p15, 0, r0, c7, c10, 4/n"
10 /* invalidate I,D TLBs on v4 */
11 "mcr p15, 0, r0, c8, c7, 0/n"
12 /* Load page table pointer */
13 "mov r4, %0/n"
14 "mcr p15, 0, r4, c2, c0, 0/n"
15 /* Write domain id (cp15_r3) */
16 "mvn r0, #0/n" /*0b11=Manager,不进行权限检查*/
17 "mcr p15, 0, r0, c3, c0, 0/n"
18 /* Set control register v4 */
19 mrc p15, 0, r0, c1, c0, 0/n"
20 /* Clear out 'unwanted' bits */
21 "ldr r1, =0x1384/n"
22 "bic r0, r0, r1/n"
23 /* Turn on what we want */
24 /*Base location of exceptions = 0xffff0000*/
25 "orr r0, r0, #0x2000/n"
26 /* Fault checking enabled */
27 "orr r0, r0, #0x0002/n"
28 #ifdef CONFIG_CPU_D_CACHE_ON /*is not set*/
29 "orr r0, r0, #0x0004/n"
30 #endif
31 #ifdef CONFIG_CPU_I_CACHE_ON /*is not set*/
32 "orr r0, r0, #0x1000/n"
33 #endif
34 /* MMU enabled */
35 "orr r0, r0, #0x0001/n"
36 /* write control register *//*write control register P545*/
37 "mcr p15, 0, r0, c1, c0, 0/n"
38 : /* no outputs */
39 : "r" (ttb) );
40 }
此函数使用嵌入汇编的方式,第29行的"r" (ttb)表示变量ttb的值赋给一个
寄存起作为输入参数,这个寄存器由编译器自动分配;第13行的“%0”表示这个
寄存器。MMU控制寄存器C1中各位的含义(第18-37行),可以参考539页“Table 2-10.
Control Register 1-bit Functions”。如果想详细了解本函数用到的操作协处理器的
指令,可以参考数据手册529页“Appendix 2 PROGRAMMER'S MODEL”。
本实验代码在CLOCK目录下,运行make命令后将可执行文件mmu下载、运行。
然后将mmu.h文件中如下两行的注释去掉,重新make后下载运行mmu,可以发现LED
闪烁的速度变快了很多——这是因为使用了cache(见上面代码28-33行):
#define CONFIG_CPU_D_CACHE_ON 1
#define CONFIG_CPU_I_CACHE_ON 1
(12)实验十二:CLOCK
S3C2410 CPU主频可以达到266MHz,前面的实验都没有使用PLL,CPU的频率
只有12MHz。本实验在实验10的基础上启动PLL,使得FCLK=200MHz,HCLK=100MHz,
PCLK=50MHz。
请打开数据手册219页第7章“CLOCK & POWER MANAGEMENT”。S3C2410有两
个PLL:MPLL和UPLL,UPLL专用与USB设备,本实验介绍的是MPLL——用于设置
FCLK、HCLK、PLCK。FCLK用于CPU核,HCLK用于AHB总线的设备(比如SDRAM),PCLK
用于APB总线的设备(比如UART)。请打开数据手册224页,“Figure 7-4. Power-On
Reset Sequence (when the external clock source is a crystal oscillator)”展示了上电
后, MPLL启动的过程,摘录此图如下:
图6 上电后MPLL的启动过程
请跟随FCLK的图像了解启动过程:
1、上电几毫秒后,晶振输出稳定,FCLK=晶振频率,nRESET信号恢复高电平后,
CPU开始执行指令。
2、我们可以在程序开头启动MPLL,在设置MPLL的几个寄存器后,需要等待一段
时间(Lock Time),MPLL的输出才稳定。在这段时间(Lock Time)内,FCLK停振,
CPU停止工作。Lock Time的长短由寄存器LOCKTIME设定。
3、Lock Time之后,MPLL输出正常,CPU工作在新的FCLK下。
本实验的工作就是设置MPLL的几个寄存器:
1、LOCKTIME:设为0x00ffffff
前面说过,MPLL启动后需要等待一段时间(Lock Time),使得其输出稳定。
位[23:12]用于UPLL,位[11:0]用于MPLL。本实验使用确省值0x00ffffff。
2、CLKDIVN:设为0x03
用于设置FCLK、HCLK、PCLK三者的比例:
bit[2]——HDIVN1,若为1,则bit[1:0]必须设为0b00,此时
FCLK:HCLK:PCLK=1:1/4:1/4;若为0,三者比例由bit[1:0]确定
bit[1]——HDIVN,0:HCLK=FCLK;1:HCLK=FCLK/2
bit[0]——PDIVN,0:PCLK=HCLK;1:PCLK=HCLK/2
本实验设为0x03,则FCLK:HCLK:PCLK=1:1/2:1/4
3、请翻到数据手册226页,有这么一段:
If HDIVN = 1, the CPU bus mode has to be changed from the fast bus
mode to the asynchronous bus mode using following instructions:
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 = 1时,
CPU bus mode需要从原来的“fast bus mode”改为“asynchronous bus mode”。
4、MPLLCON:设为(0x5c << 12)|(0x04 << 4)|(0x00),即0x5c0040
对于MPLLCON寄存器,[19:12]为MDIV,[9:4]为PDIV,[1:0]为SDIV。有如下
计算公式:
MPLL(FCLK) = (m * Fin)/(p * 2^s)
其中: m = MDIV + 8, p = PDIV + 2
对于本开发板,Fin = 12MHz,MPLLCON设为0x5c0040,可以计算出
FCLK=200MHz,再由CLKDIVN的设置可知:HCLK=100MHz,PCLK=50MHz。
当设置MPLLCON之后——相当于图6中的“PLL is configured by S/W first
time”,Lock Time就被自动插入,Lock Time之后,MPLL输出稳定,CPU工作在
200MHz频率下。
上面的4个步骤的工作在init.c中的clock_init函数中完成,代码如下:
1 void clock_init()
2 {
3 LOCKTIME = 0x00ffffff;
4 CLKDIVN = 0x03; /*FCLK:HCLK:PCLK=1:2:4,
5 HDIVN1=0,HDIVN=1,PDIVN=1 */
6 /*If HDIVN = 1,the CPU bus mode has to be changed from
the fast bus mode to the asynchronous bus mod using
following instructions.*/
7 __asm__(
8 "mrc p15, 0, r1, c1, c0, 0/n" /* read ctrl register */
9 "orr r1, r1, #0xc0000000/n" /* Asynchronous */
10 "mcr p15, 0, r1, c1, c0, 0/n" /* write ctrl register */
11 );
12 MPLLCON = MPLL_200MHz; /*现在,
FCLK=200MHz,HCLK=100MHz,PCLK=50MHz*/
13 }
在head.s中,在memsetup_2函数之前调用clock_init函数。
本实验是在实验10的基础上修改得来,当工作频率改变后,一些设备的初始
化参数需要调整,很幸运,只需要修改两个地方:
1、SDRAM控制器的REFRESH寄存器(在init.c中):
请翻看实验五:MEMORY CONTROLLER中关于REFRESH寄存器的介绍,本实验
HCLK=100MHz,REFRESH寄存器取值如下:
R_CNT = 2^11 + 1 – 100 * 7.8125 = 1268,
REFRESH=0x008e0000 + 1268 = 0x008e04f4
在init.c中的memsetup_2函数,将原来的0x008e07a3换成0x008e04f4即可。
2、UART0的UBRDIV0寄存器(在serial.c中):
请翻看实验七:UART中关于UBRDIV0的介绍,本实验PCLK=50MHz,设置波特
率为57600时,UART0寄存器取值可由下式计算得53:
UBRDIVn = (int)(PCLK / (bps x 16) ) –1
在serial.c中的init_uart函数,将UART0由原来的12换成53即可。
本实验代码在CLOCK目录下,运行make命令后将可执行文件clock下载、运行,
可以发现LED闪烁的速度变快了很多。然后将mmu.h文件中如下两行的注释去掉,
重新make后下载运行clock,可以发现LED闪烁得更快了,简直分辨不出来了——
这是因为使用了cache:
#define CONFIG_CPU_D_CACHE_ON 1
#define CONFIG_CPU_I_CACHE_ON 1
四.Bootloader vivi
为了将linux移植到ARM上,碰到的第一个程序就是bootloader,我选用韩国
mizi公司的vivi。您可以在以下地址下载:
http://www.mizi.com/developer/s3c2410x/download/vivi.html
如果您对bootloader没有什么概念,在学习VIVI的代码之前,建议您阅读一
篇文章《嵌入式系统 Boot Loader 技术内幕》(詹荣开著)。链接地址如下:
http://www-128.ibm.com/developerworks/cn/linux/l-btloader/
当您阅读了上述文章后,我再企图在理论上罗嗦什么就不合适了(这篇文章
实在太好了)。vivi也可以分为2个阶段,阶段1的代码在arch/s3c2410/head.S
中,阶段2的代码从init/main.c的main函数开始。您可以跳到实验部分先感受一
下vivi。
(1)阶段1:arch/s3c2410/head.S
沿着代码执行的顺序,head.S完成如下几件事情:
1、关WATCH DOG:上电后,WATCH DOG默认是开着的
2、禁止所有中断:vivi中没用到中断(不过这段代码实在多余,上电后中断默认
是关闭的)
3、初始化系统时钟:启动MPLL,FCLK=200MHz,HCLK=100MHz,PCLK=50MHz,“CPU
bus mode”改为“Asynchronous bus mode”。请参考实验十一:CLOCK
4、初始化内存控制寄存器:还记得那13个寄存器吗?请复习实验五:MEMORY
CONTROLLER
5、检查是否从掉电模式唤醒,若是,则调用WakeupStart函数进行处理——这是
一段没用上的代码,vivi不可能进入掉电模式
6、点亮所有LED
7、初始化UART0:
a.设置GPIO,选择UART0使用的引脚
b.初始化UART0,设置工作方式(不使用FIFO)、波特率115200 8N1、无流控
等,请参考实验七:UART
8、将vivi所有代码(包括阶段1和阶段2)从nand flash复制到SDRAM中:
a.设置nand flash控制寄存器
b.设置堆栈指针——调用C函数时必须先设置堆栈
c.设置即将调用的函数nand_read_ll的参数:r0=目的地址(SDRAM的地址),
r1=源地址(nand flash的地址),r2=复制的长度(以字节为单位)
d.调用nand_read_ll进行复制
e.进行一些检查工作:上电后nand flash最开始的4K代码被自动复制到一
个称为“Steppingstone”的内部RAM中(地址为0x00000000-0x00001000);
在执行nand_read_ll之后,这4K代码同样被复制到SDRAM中(地址为
0x33f00000-0x33f01000)。比较这两处的4K代码,如果不同则表示出错
9、跳到bootloader的阶段2运行——就是调用init/main.c中的main函数:
a.重新设置堆栈
b.设置main函数的参数
c.调用main函数
如果您做了第二章的各个实验,理解head.S就不会有任何困难——上面说到
的几个步骤,都有相应的实验。head.S有900多行,把它搬到这篇文章上来就太
不厚道了——白白浪费页数而已。在vivi/arch/s3c2410/head.S上,我做了比较
详细的注释(其中的乱码可能是韩文,不去管它)。
当执行完head.S的代码后,内存的使用情况如下:
图7 执行vivi stage1后内存的划分情况
(2)阶段2:init/main.c
本阶段从init/main.c中的main函数开始执行,它可以分为8个步骤。我先把
main函数的代码罗列如下(去掉了乱码——可能是韩文,现在没留下多少有用的
注释了),然后逐个分析:
1 int main(int argc, char *argv[])
2 {
3 int ret;
4 /*Step 1*/
5 putstr("/r/n");
6 putstr(vivi_banner);
7 reset_handler();
8 /*Step 2*/
9 ret = board_init();
10 if (ret) {
11 putstr("Failed a board_init() procedure/r/n");
12 error();
13 }
14 /*Step 3*/
15 mem_map_init();
16 mmu_init();
17 putstr("Succeed memory mapping./r/n");
18 /* Now, vivi is running on the ram. MMU is enabled.*/
19 /*Step 4*/
20 /* initialize the heap area */
21 ret = heap_init();
22 if (ret) {
23 putstr("Failed initailizing heap region/r/n");
24 error();
25 }
26 /*Step 5*/
27 ret = mtd_dev_init();
28 /*Step 6*/
29 init_priv_data();
30 /*Step 7*/
31 misc();
32 init_builtin_cmds();
33 /*Step 8*/
34 boot_or_vivi();
35 return 0;
36 }
1、Step 1:reset_handler()
reset_handler用于将内存清零,代码在lib/reset_handle.c中。
[main(int argc, char *argv[]) > reset_handler()]
1 void
2 reset_handler(void)
3 {
4 int pressed;
5 pressed = is_pressed_pw_btn(); /*判断是硬件复位还是软件复位*/
6 if (pressed == PWBT_PRESS_LEVEL) {
7 DPRINTK("HARD RESET/r/n");
8 hard_reset_handle(); /*调用clear_mem对SDRAM清0*/
9 } else {
10 DPRINTK("SOFT RESET/r/n");
11 soft_reset_handle(); /*此函数为空*/
12 }
13 }
在上电后,reset_handler调用第8行的hard_reset_handle(),此函数在
lib/reset_handle.c中:
[main(int argc, char *argv[]) > reset_handler() > hard_reset_handle()]
1 static void
2 hard_reset_handle(void)
3 {
4 #if 0
5 clear_mem((unsigned long)(DRAM_BASE + VIVI_RAM_ABS_POS), /
6 (unsigned long)(DRAM_SIZE - VIVI_RAM_ABS_POS));
7 #endif
8 /*lib/memory.c,将起始地址为USER_RAM_BASE,长度为USER_RAM_SIZE的内存清0*/
9 clear_mem((unsigned long)USER_RAM_BASE, (unsigned long)USER_RAM_SIZE);
10 }
2、Step 2:board_init()
board_init调用2个函数用于初始化定时器和设置各GPIO引脚功能,代码在
arch/s3c2410/smdk.c中:
[main(int argc, char *argv[]) > board_init()]
1 int board_init(void)
2 {
3 init_time(); /*arch/s3c2410/proc.c*/
4 set_gpios(); /*arch/s3c2410/smdk.c*/
5 return 0;
6 }
init_time()只是简单的令寄存器TCFG0 = 0xf00,vivi未使用定时器,这个
函数可以忽略。
set_gpios()用于选择GPA-GPH端口各引脚的功能及是否使用各引脚的内部
上拉电阻,并设置外部中断源寄存器EXTINT0-2(vivi中未使用外部中断)。
3、Step 3:建立页表和启动MMU
mem_map_init函数用于建立页表,vivi使用段式页表,只需要一级页表。它
调用3个函数,代码在arch/s3c2410/mmu.c中:
[main(int argc, char *argv[]) > mem_map_init(void)]
1 void mem_map_init(void)
2 {
3 #ifdef CONFIG_S3C2410_NAND_BOOT /* CONFIG_S3C2410_NAND_BOOT=y */
4 mem_map_nand_boot(); /* 最终调用mem_mepping_linear,建立页表 */
5 #else
6 mem_map_nor();
7 #endif
8 cache_clean_invalidate(); /* 清空cache,使无效cache */
9 tlb_invalidate(); /* 使无效快表TLB */
10 }
第9、10行的两个函数可以不用管它,他们做的事情在下面的mmu_init函数
里又重复了一遍。对于本开发板,在.config中定义了
CONFIG_S3C2410_NAND_BOOT。mem_map_nand_boot()函数调用
mem_mapping_linear()函数来最终完成建立页表的工作。页表存放在SDRAM物理
地址0x33dfc000开始处,共16K:一个页表项4字节,共有4096个页表项;每个页
表项对应1M地址空间,共4G。mem_map_init先将4G虚拟地址映射到相同的物理地
址上,NCNB(不使用cache,不使用write buffer)——这样,对寄存器的操作跟
未启动MMU时是一样的;再将SDRAM对应的64M空间的页表项修改为使用cache。
mem_mapping_linear函数的代码在arch/s3c2410/mmu.c中:
[main(int argc, char *argv[]) > mem_map_init(void) > mem_map_nand_boot( )
> mem_mapping_linear(void)]
1 static inline void mem_mapping_linear(void)
2 {
3 unsigned long pageoffset, sectionNumber;
4 putstr_hex("MMU table base address = 0x", (unsigned long)mmu_tlb_base);
5 /* 4G 虚拟地址映射到相同的物理地址. not cacacheable, not bufferable */
6 /* mmu_tlb_base = 0x33dfc000*/
7 for (sectionNumber = 0; sectionNumber < 4096; sectionNumber++) {
8 pageoffset = (sectionNumber << 20);
9 *(mmu_tlb_base + (pageoffset >> 20)) = pageoffset | MMU_SECDESC;
10 }
11 /* make dram cacheable */
12 /* SDRAM物理地址0x3000000-0x33ffffff,
13 DRAM_BASE=0x30000000,DRAM_SIZE=64M
14 */
15 for (pageoffset = DRAM_BASE; pageoffset < (DRAM_BASE+DRAM_SIZE); /
16 pageoffset += SZ_1M) {
17 //DPRINTK(3, "Make DRAM section cacheable: 0x%08lx/n", pageoffset);
18 *(mmu_tlb_base + (pageoffset >> 20)) = /
pageoffset | MMU_SECDESC | MMU_CACHEABLE;
19 }
20 }
mmu_init()函数用于启动MMU,它直接调用arm920_setup()函数。
arm920_setup()的代码在arch/s3c2410/mmu.c中:
[main(int argc, char *argv[]) > mmu_init( ) > arm920_setup( )]
1 static inline void arm920_setup(void)
2 {
3 unsigned long ttb = MMU_TABLE_BASE; /* MMU_TABLE_BASE = 0x 0x33dfc000 */
4 __asm__(
5 /* Invalidate caches */
6 "mov r0, #0/n"
7 "mcr p15, 0, r0, c7, c7, 0/n" /* invalidate I,D caches on v4 */
8 "mcr p15, 0, r0, c7, c10, 4/n" /* drain write buffer on v4 */
9 "mcr p15, 0, r0, c8, c7, 0/n" /* invalidate I,D TLBs on v4 */
10 /* Load page table pointer */
11 "mov r4, %0/n"
12 "mcr p15, 0, r4, c2, c0, 0/n" /* load page table pointer */
13 /* Write domain id (cp15_r3) */
14 "mvn r0, #0/n" /* Domains 0b01 = client, 0b11=Manager*/
15 "mcr p15, 0, r0, c3, c0, 0/n" /* load domain access register,
write domain 15:0, 数据手册P548(access permissions)*/
16 /* Set control register v4 */
17 "mrc p15, 0, r0, c1, c0, 0/n" /* get control register v4 */
/*数据手册P545:read control register */
18 /* Clear out 'unwanted' bits (then put them in if we need them) */
19 /* ..VI ..RS B... .CAM */ /*这些位的含义在数据手册P546*/
20 "bic r0, r0, #0x3000/n" /* ..11 .... .... .... */
/*I(bit[12])=0 = Instruction cache disabled*/
21 /*V[bit[13]](Base location of exception registers)=0 = Low addresses =
0x0000 0000*/
22 "bic r0, r0, #0x0300/n" /* .... ..11 .... .... */
23 /*R(ROM protection bit[9])=0*/
/*S(System protection bit[8])=0*/
/*由于TTB中AP=0b11(line141),所以RS位不使用(P579)*/
24 "bic r0, r0, #0x0087/n" /* .... .... 1... .111 */
/*M(bit[0])=0 = MMU disabled*/
/*A(bit[1])=0 =Data address
alignment fault checking disable*/
/*C(bit[2])=0 = Data cache disabled*/
/*B(bit[7])=0= Little-endian operation*/
25 /* Turn on what we want */
26 /* Fault checking enabled */
27 "orr r0, r0, #0x0002/n" /* .... .... .... ..1. */
/*A(bit[1])=1 = Data address
alignment fault checking enable*/
28 #ifdef CONFIG_CPU_D_CACHE_ON /*is not set*/
29 "orr r0, r0, #0x0004/n" /* .... .... .... .1.. */
/*C(bit[2])=1 = Data cache enabled*/
30 #endif
31 #ifdef CONFIG_CPU_I_CACHE_ON /*is not set*/
32 "orr r0, r0, #0x1000/n" /* ...1 .... .... .... */
/*I(bit[12])=1 = Instruction cache enabled*/
33 #endif
34 /* MMU enabled */
35 "orr r0, r0, #0x0001/n" /* .... .... .... ...1 */
/*M(bit[0])=1 = MMU enabled*/
36 "mcr p15, 0, r0, c1, c0, 0/n" /* write control register */
/*数据手册P545*/
37 : /* no outputs */
38 : "r" (ttb) );
39 }
至此,内存如下划分:
图8 创建页表后内存的划分情况
4、Step 4:heap_init()
heap——堆,内存动态分配函数mmalloc就是从heap中划出一块空闲内存的,
mfree则将动态分配的某块内存释放回heap中。
heap_init函数在SDRAM中指定了一块1M大小的内存作为heap(起始地址
HEAP_BASE = 0x33e00000),并在heap的开头定义了一个数据结构blockhead——
事实上,heap就是使用一系列的blockhead数据结构来描述和操作的。每个
blockhead数据结构对应着一块heap内存,假设一个blockhead数据结构的存放位
置为A,则它对应的可分配内存地址为“A + sizeof(blockhead)”到“A +
sizeof(blockhead) + size - 1”。blockhead数据结构在lib/heap.c中定义:
1 typedef struct blockhead_t {
2 Int32 signature; //固定为BLOCKHEAD_SIGNATURE
3 Bool allocated; //此区域是否已经分配出去:0-N,1-Y
4 unsigned long size; //此区域大小
5 struct blockhead_t *next; //链表指针
6 struct blockhead_t *prev; //链表指针
7 } blockhead;
现在来看看heap是如何运作的(如果您不关心heap实现的细节,这段可以跳
过)。vivi对heap的操作比较简单,vivi中有一个全局变量static blockhead
*gHeapBase,它是heap的链表头指针,通过它可以遍历所有blockhead数据结构。
假设需要动态申请一块sizeA大小的内存,则mmalloc函数从gHeapBase开始搜索
blockhead数据结构,如果发现某个blockhead满足:
a. allocated = 0 //表示未分配
b. size > sizeA,
则找到了合适的blockhead,于是进行如下操作:
a.allocated设为1
b.如果size – sizeA > sizeof(blockhead),则将剩下的内存组织成
一个新的blockhead,放入链表中
c.返回分配的内存的首地址
释放内存的操作更简单,直接将要释放的内存对应的blockhead数据结构的
allocated设为0即可。
下面用图来简单演示先分配1K内存,再分配2K内存的过程:
图9 在heap中连续分配1K和2K内存的示意图
heap_init函数直接调用mmalloc_init函数进行初始化,此函数代码在
lib/heap.c中,比较简单,初始化gHeapBase即可:
[main(int argc, char *argv[]) > heap_init(void) > mmalloc_init(unsigned
char *heap, unsigned long size)]
1 static inline int mmalloc_init(unsigned char *heap, unsigned long size)
2 {
3 if (gHeapBase != NULL) return -1;
4 DPRINTK("malloc_init(): initialize heap area at 0x%08lx, size =
0x%08lx/n", heap, size);
5 gHeapBase = (blockhead *)(heap);
6 gHeapBase->allocated=FALSE;
7 gHeapBase->signature=BLOCKHEAD_SIGNATURE;
8 gHeapBase->next=NULL;
9 gHeapBase->prev=NULL;
10 gHeapBase->size = size - sizeof(blockhead);
11 return 0;
12 }
分配heap区域后,内存划分情况如下:
图10 执行heap_init后内存的划分情况
5、Step 5:mtd_dev_init()
在分析代码前先介绍一下MTD(Memory Technology Device)相关的技术。在
linux系统中,我们通常会用到不同的存储设备,特别是FLASH设备。为了在使用
新的存储设备时,我们能更简便地提供它的驱动程序,在上层应用和硬件驱动的
中间,抽象出MTD设备层。驱动层不必关心存储的数据格式如何,比如是FAT32、
ETX2还是FFS2或其它。它仅仅提供一些简单的接口,比如读写、擦除及查询。如
何组织数据,则是上层应用的事情。MTD层将驱动层提供的函数封装起来,向上
层提供统一的接口。这样,上层即可专注于文件系统的实现,而不必关心存储设
备的具体操作。
在我们即将看到的代码中,使用mtd_info数据结构表示一个MTD设备,使用
nand_chip数据结构表示一个nand flash芯片。在mtd_info结构中,对nand_flash
结构作了封装,向上层提供统一的接口。比如,它根据nand_flash提供的
read_data(读一个字节)、read_addr(发送要读的扇区的地址)等函数,构造了一
个通用的读函数read,将此函数的指针作为自己的一个成员。而上层要读写flash
时,执行mtd_info中的read、write函数即可。
下面分析代码:
mtd_dev_init()用来扫描所使用的NAND Flash的型号,构造MTD设备,即构
造一个mtd_info的数据结构。对于本开发板,它直接调用mtd_init(),mtd_init
又调用smc_init(),此函数在drivers/mtd/maps/s3c2410_flash.c中:
[main(int argc, char *argv[]) > mtd_dev_init() > mtd_init() > smc_init()]
1 static int
2 smc_init(void)
3 {
/*struct mtd_info *mymtd,数据类型在include/mtd/mtd.h*/
/*strcut nand_chip在include/mtd/nand.h中定义*/
4 struct nand_chip *this;
5 u_int16_t nfconf;
/* Allocate memory for MTD device structure and private data */
6 mymtd = mmalloc(sizeof(struct mtd_info) + sizeof(struct
nand_chip));
7 if (!mymtd) {
8 printk("Unable to allocate S3C2410 NAND MTD device
structure./n");
9 return -ENOMEM;
10 }
/* Get pointer to private data */
11 this = (struct nand_chip *)(&mymtd[1]);
/* Initialize structures */
12 memset((char *)mymtd, 0, sizeof(struct mtd_info));
13 memset((char *)this, 0, sizeof(struct nand_chip));
/* Link the private data with the MTD structure */
14 mymtd->priv = this;
/* set NAND Flash controller */
15 nfconf = NFCONF;
/* NAND Flash controller enable */
16 nfconf |= NFCONF_FCTRL_EN;
/* Set flash memory timing */
17 nfconf &= ~NFCONF_TWRPH1; /* 0x0 */
18 nfconf |= NFCONF_TWRPH0_3; /* 0x3 */
19 nfconf &= ~NFCONF_TACLS; /* 0x0 */
20 NFCONF = nfconf;
/* Set address of NAND IO lines */
21 this->hwcontrol = smc_hwcontrol;
22 this->write_cmd = write_cmd;
23 this->write_addr = write_addr;
24 this->read_data = read_data;
25 this->write_data = write_data;
26 this->wait_for_ready = wait_for_ready;
/* Chip Enable -> RESET -> Wait for Ready -> Chip Disable */
27 this->hwcontrol(NAND_CTL_SETNCE);
28 this->write_cmd(NAND_CMD_RESET);
29 this->wait_for_ready();
30 this->hwcontrol(NAND_CTL_CLRNCE);
31 smc_insert(this);
32 return 0;
33 }
6-14行构造了一个mtd_info结构和nand_flash结构,前者对应MTD设备,后
者对应nand flash芯片(如果您用的是其他类型的存储器件,比如nor flash,这
里的nand_flash结构应该换为其他类型的数据结构)。MTD设备是具体存储器件的
抽象,那么在这些代码中这种关系如何体现呢——第14行的代码把两者连结在一
起了。事实上,mtd_info结构中各成员的实现(比如read、write函数),正是由
priv变量所指向的nand_flash的各类操作函数(比如read_addr、read_data等)
来实现的。
15-20行是初始化S3C2410上的NAND FLASH控制器,和我们在实验六“NAND
FLASH CONTROLLER”里面做的一样。
前面分配的nand_flash结构还是空的,现在当然就是填满它的各类成员了,
这正是21-26行做的事情。
27-30行对这块nand flash作了一下复位操作。
最后,也是最复杂的部分,根据刚才填充的nand_flash结构,构造mtd_info
结构,这由31行的smc_insert函数调用smc_scan完成。先看看smc_insert函数:
[main(int argc, char *argv[]) > mtd_dev_init() > mtd_init() > smc_init()
> smc_insert(struct nand_chip *this)]
1 inline int
2 smc_insert(struct nand_chip *this) {
2 /* Scan to find existance of the device */
/*smc_scan defined at drivers/mtd/nand/smc_core.c*/
4 if (smc_scan(mymtd)) {
5 return -ENXIO;
6 }
/* Allocate memory for internal data buffer */
7 this->data_buf = mmalloc(sizeof(u_char) *
8 (mymtd->oobblock + mymtd->oobsize));
9 if (!this->data_buf) {
10 printk("Unable to allocate NAND data buffer for S3C2410./n");
11 this->data_buf = NULL;
12 return -ENOMEM;
13 }
14 return 0;
15 }
后面的7-13行,我也没弄清楚,待查。
现在,终于到重中之重了,请看smc_scan函数的代码:
[main(int argc, char *argv[]) > mtd_dev_init() > mtd_init() > smc_init()
> smc_insert(struct nand_chip *this) > smc_scan(struct mtd_info *mtd)]
1 int smc_scan(struct mtd_info *mtd)
2 {
3 int i, nand_maf_id, nand_dev_id;
4 struct nand_chip *this = mtd->priv;
/* Select the device */
5 nand_select();
/* Send the command for reading device ID */
6 nand_command(mtd, NAND_CMD_READID, 0x00, -1);
7 this->wait_for_ready();
/* Read manufacturer and device IDs */
8 nand_maf_id = this->read_data();
9 nand_dev_id = this->read_data();
/* Print and sotre flash device information */
10 for (i = 0; nand_flash_ids[i].name != NULL; i++) {
11 if (nand_maf_id == nand_flash_ids[i].manufacture_id &&
12 nand_dev_id == nand_flash_ids[i].model_id) {
13 if (!(mtd->size) && !(nand_flash_ids[i].page256)) {
14 mtd->name = nand_flash_ids[i].name;
15 mtd->erasesize = nand_flash_ids[i].erasesize;
16 mtd->size = (1 << nand_flash_ids[i].chipshift);
17 mtd->eccsize = 256;
18 mtd->oobblock = 512;
19 mtd->oobsize = 16;
20 this->page_shift = 9;
21 this->dev =
&nand_smc_info[GET_DI_NUM(nand_flash_ids[i].chipshift)];
22 }
23 printk("NAND device: Manufacture ID:" /
24 " 0x%02x, Chip ID: 0x%02x (%s)/n",
25 nand_maf_id, nand_dev_id, mtd->name);
26 break;
27 }
28 }
/* De-select the device */
29 nand_deselect();
30 /* Print warning message for no device */
31 if (!mtd->size) {
32 printk("No NAND device found!!!/n");
33 return 1;
34 }
35 /* Fill in remaining MTD driver data */
36 mtd->type = MTD_NANDFLASH;
37 mtd->flags = MTD_CAP_NANDFLASH | MTD_ECC;
38 mtd->module = NULL;
39 mtd->ecctype = MTD_ECC_SW;
40 mtd->erase = nand_erase;
41 mtd->point = NULL;
42 mtd->unpoint = NULL;
43 mtd->read = nand_read;
44 mtd->write = nand_write;
45 mtd->read_ecc = nand_read_ecc;
46 mtd->write_ecc = nand_write_ecc;
47 mtd->read_oob = nand_read_oob;
48 mtd->write_oob = nand_write_oob;
49 mtd->lock = NULL;
50 mtd->unlock = NULL;
51 /* Return happy */
52 return 0;
53 }
5-9行,读取nand flash得厂家ID和设备ID,下面将用这两个ID号,在一个
预设的数组“nand_flash_ids”里查找到与之相符的项。nand_flash_ids是
nand_flash_dev结构的数组,里面存放的是世界上比较常用的nand flash型号的
一些特性(见下面的定义):
struct nand_flash_dev {
char * name;
int manufacture_id;
int model_id;
int chipshift;
char page256;
char pageadrlen;
unsigned long erasesize;
};
static struct nand_flash_dev nand_flash_ids[] = {
{"Toshiba TC5816BDC", NAND_MFR_TOSHIBA, 0x64, 21, 1, 2, 0x1000},
..
{NULL,}
};
10-28行根据找到的数组项,构造前面分配的mtd_info结构。
35行之后的代码,填充mtd_info结构的剩余部分,大多是一些函数指针,比
如read、write函数等。
执行完mtd_dev_init后,我们得到了一个mtd_info结构的全局变量(mymtd指
向它),以后对nand flash的操作,直接通过mymtd提供的接口进行。
6、Step 6:init_priv_data()
此函数将启动内核的命令参数取出,存放在内存特定的位置中。这些参数来
源有两个:vivi预设的默认参数,用户设置的参数(存放在nand flash上)。
init_priv_data先读出默认参数,存放在“VIVI_PRIV_RAM_BASE”开始的内存上;
然后读取用户参数,若成功则用用户参数覆盖默认参数,否则使用默认参数。
init_priv_data函数分别调用get_default_priv_data函数和
load_saved_priv_data函数来读取默认参数和用户参数。这些参数分为3类:
a.vivi自身使用的一些参数,比如传输文件时的使用的协议等
b.linux启动命令
c.nand flash的分区参数
get_default_priv_data函数比较简单,它将vivi中存储这些默认参数的变
量,复制到指定内存中。执行完后,此函数执行完毕后,内存使用情况如下:
图11 执行init_priv_data后内存的划分情况
load_saved_priv_data函数读取上述内存图中分区参数的数据,找到
“param”分区,然后从中读出上述3类参数,覆盖掉默认参数(如果能成功读出
的话)。
7、Step 7:misc()和init_builtin_cmds()
这两个函数都是简单地调用add_command函数,给一些命令增加相应的处理
函数。在vivi启动后,可以进去操作界面,这些命令,就是供用户使用的。它们
增加了如下命令:
a.add_command(&cpu_cmd)
b. add_command(&bon_cmd)
c. add_command(&reset_cmd)
d. add_command(¶m_cmd)
e. add_command(&part_cmd)
f. add_command(&mem_cmd)
g. add_command(&load_cmd)
h. add_command(&go_cmd)
i. add_command(&dump_cmd)
j. add_command(&call_cmd)
k. add_command(&boot_cmd)
l. add_command(&help_cmd)
现在来看看add_command函数是如何实现得:
1 void add_command(user_command_t *cmd)
2 {
3 if (head_cmd == NULL) {
4 head_cmd = tail_cmd = cmd;
5 } else {
6 tail_cmd->next_cmd = cmd;
7 tail_cmd = cmd;
8 }
9 /*printk("Registered '%s' command/n", cmd->name);*/
10 }
很简单!把user_command_t结构放入一个链表即可。user_command_t结
构定义如下,呵呵,name是命令名称,cmdfunc是这个命令的处理函数,helstr
是帮助信息。
typedef struct user_command {
const char *name;
void (*cmdfunc)(int argc, const char **);
struct user_command *next_cmd;
const char *helpstr;
} user_command_t;
8、Step 8:boot_or_vivi()
此函数根据情况,或者启动“vivi_shell”,进入与用户进行交互的界面,
或者直接启动linux内核。代码如下:
[main(int argc, char *argv[]) > void boot_or_vivi(void)]
1 void boot_or_vivi(void)
2 {
3 char c;
4 int ret;
5 ulong boot_delay;
5 boot_delay = get_param_value("boot_delay", &ret);
7 if (ret) boot_delay = DEFAULT_BOOT_DELAY;
/* If a value of boot_delay is zero,
* unconditionally call vivi shell
*/
8 if (boot_delay == 0) vivi_shell();
/*
* wait for a keystroke (or a button press if you want.)
*/
9 printk("Press Return to start the LINUX now, any other key for
vivi/n");
10 c = awaitkey(boot_delay, NULL);
11 if (((c != '/r') && (c != '/n') && (c != '/0'))) {
12 printk("type /"help/" for help./n");
13 vivi_shell();
14 }
15 run_autoboot();
16 return;
17 }
第10行等待键盘输入,如果在一段时间内键盘无输入,或者输入了回车键,
则调用run_autoboot启动内核;否则调用vivi_shell进入交互界面。
vivi_shell等待用户输入命令(等待串口数据),然后根据命令查找“Step 7:
misc()和init_builtin_cmds()”设置的命令链表,运行找到的命令函数。
vivi_shell函数是通过调用serial_term函数来实现的,serial_term代码如下:
[main(int argc, char *argv[]) > void boot_or_vivi(void) >
vivi_shell(void) > serial_term]
1 void serial_term(void)
2 {
3 char cmd_buf[MAX_CMDBUF_SIZE];
4 for (;;) {
5 printk("%s> ", prompt); /*prompt is defined upside*/
6 getcmd(cmd_buf, MAX_CMDBUF_SIZE);
/* execute a user command */
7 if (cmd_buf[0])
8 exec_string(cmd_buf);
9 }
10 }
第6行getcmd函数读取串口数据,将返回的字符串作为参数调用exec_string
函数。exec_string代码如下:
[main(int argc, char *argv[]) > void boot_or_vivi(void) >
vivi_shell(void) > serial_term > void exec_string(char *buf)]
1 void exec_string(char *buf)
2 {
3 int argc;
4 char *argv[128];
5 char *resid;
6 while (*buf) {
7 memset(argv, 0, sizeof(argv));
8 parseargs(buf, &argc, argv, &resid);
9 if (argc > 0)
10 execcmd(argc, (const char **)argv);
11 buf = resid;
12 }
13 }
它首先调用parseargs函数分析所传入的字符串,确定参数个数及将这些参
数分别保存。比如对于字符串“abcd efgh ijklm 123”,parseargs函数返回的
结果是:argc = 4,argv[0]指向“abcd”字符串,argv[1]指向“efgh”字符
串,argv[2]指向“ijklm”字符串,argv[3]指向“123”字符串。然后,调用execmd
函数:
[main(int argc, char *argv[]) > void boot_or_vivi(void) >
vivi_shell(void) > serial_term > void execcmd(int argc, const char
**argv)]
1 void execcmd(int argc, const char **argv)
2 {
3 user_command_t *cmd = find_cmd(argv[0]);
4 if (cmd == NULL) {
5 printk("Could not found '%s' command/n", argv[0]);
6 printk("If you want to konw available commands, type
'help'/n");
7 return;
8 }
9 /*printk("execcmd: cmd=%s, argc=%d/n", argv[0], argc);*/
10 cmd->cmdfunc(argc, argv);
11 }
首先,第3行的find_cmd函数根据命令(argv[0])找到对应的处理函数,然后
执行它(第10行)。命令名字和对应的处理函数指针,在上述“Step 7:misc()
和init_builtin_cmds()”中,使用一个命令链表存起来了。具体的命令处理函
数,这里就不细说了,各位有兴趣的可以自行阅读源代码。
run_autoboot用于启动内核,起始就是运行“boot”命令的处理函数
command_boot。我们看看它是如何启动内核的,下面我们只列出用到的代码:
[main(int argc, char *argv[]) > void boot_or_vivi(void) > void
run_autoboot(void) > exec_string("boot") > command_boot(1, 0)]
1 void command_boot(int argc, const char **argv)
2 {
..
3 media_type = get_param_value("media_type", &ret);
4 if (ret) {
5 printk("Can't get default 'media_type'/n");
6 return;
7 }
8 kernel_part = get_mtd_partition("kernel");
9 if (kernel_part == NULL) {
10 printk("Can't find default 'kernel' partition/n");
11 return;
12 }
13 from = kernel_part->offset;
14 size = kernel_part->size;
..
15 boot_kernel(from, size, media_type);
..
}
第3行,获得存储内核代码的器件类型;第8行获取“kernel”分区信息,第
13、14行由此分区信息获得内核存放的位置及大小;第15行的boot_kernel函数
将内核从其存储的器件上(flash)复制到内存,然后执行它。boot_kernel代码如
下:
[main(int argc, char *argv[]) > void boot_or_vivi(void) > void
run_autoboot(void) > exec_string("boot") > command_boot(1, 0) > int
boot_kernel(ulong from, size_t size, int media_type)]
1 int boot_kernel(ulong from, size_t size, int media_type)
2 {
3 int ret;
4 ulong boot_mem_base; /* base address of bootable memory */
5 ulong to;
6 ulong mach_type;
7 boot_mem_base = get_param_value("boot_mem_base", &ret);
8 if (ret) {
9 printk("Can't get base address of bootable memory/n");
10 printk("Get default DRAM address. (0x%08lx/n", DRAM_BASE);
11 boot_mem_base = DRAM_BASE;
12 }
/* copy kerne image
* LINUX_KERNEL_OFFSET = 0x8000
*/
13 to = boot_mem_base + LINUX_KERNEL_OFFSET;
14 printk("Copy linux kernel from 0x%08lx to 0x%08lx, size =
0x%08lx ... ", from, to, size);
15 ret = copy_kernel_img(to, (char *)from, size, media_type);
16 if (ret) {
17 printk("failed/n");
18 return -1;
19 } else {
20 printk("done/n");
21 }
22 if (*(ulong *)(to + 9*4) != LINUX_ZIMAGE_MAGIC) {
23 printk("Warning: this binary is not compressed linux kernel
image/n");
24 printk("zImage magic = 0x%08lx/n", *(ulong *)(to + 9*4));
25 } else {
26 printk("zImage magic = 0x%08lx/n", *(ulong *)(to + 9*4));
27 }
/* Setup linux parameters and linux command line
* LINUX_PARAM_OFFSET = 0x100
*/
28 setup_linux_param(boot_mem_base + LINUX_PARAM_OFFSET);
/* Get machine type */
29 mach_type = get_param_value("mach_type", &ret);
30 printk("MACH_TYPE = %d/n", mach_type);
/* Go Go Go */
31 printk("NOW, Booting Linux....../n");
32 call_linux(0, mach_type, to);
33 return 0;
34 }
第7-13行用来确定在内核将存放在内存中的位置;15行调用nand_read_ll
函数(详细介绍请看“实验六:NAND FLASH CONTROLLER”)从flash芯片上将内核
读出;28行设置内核启动的参数,比如命令行等;29行获取处理器型号;32行的
函数call_linux汇编代码,跳到内核存放的地址处执行。值得细看的有两个函数:
setup_linux_param和call_linux。
setup_linux_param函数用于设置linux启动时用到的各类参数:
[main( ) > boot_or_vivi( ) > run_autoboot( ) > exec_string("boot") >
command_boot(1, 0) > boot_kernel( ) > setup_linux_param( )]
1 static void setup_linux_param(ulong param_base)
2 {
3 struct param_struct *params = (struct param_struct *)param_base;
4 char *linux_cmd;
5 printk("Setup linux parameters at 0x%08lx/n", param_base);
6 memset(params, 0, sizeof(struct param_struct));
7 params->u1.s.page_size = LINUX_PAGE_SIZE;
8 params->u1.s.nr_pages = (DRAM_SIZE >> LINUX_PAGE_SHIFT);
/* set linux command line */
9 linux_cmd = get_linux_cmd_line(); /* lib/priv_data/param.c */
10 if (linux_cmd == NULL) {
11 printk("Wrong magic: could not found linux command line/n");
12 } else {
13 memcpy(params->commandline, linux_cmd, strlen(linux_cmd) +
1);
14 printk("linux command line is: /"%s/"/n", linux_cmd);
15 }
16 }
此函数在param_base地址存放linux启动时用到的参数。对于本开发板,此
地址为RAM_BASE + LINUX_PARAM_OFFSET = 0x3000_0000 + 0x100。第9行将命令
行参数从原来位置(上述Step 6:init_priv_data中将命令行等存储在内存中)
读出,复制到相应位置(第13行)。
call_linux函数先对cache和tlb进行一些设置,然后将内核存放的位置直接
赋给pc寄存器,从而执行内核。call_linux函数代码如下:
[main( ) > boot_or_vivi( ) > run_autoboot( ) > exec_string("boot") >
command_boot(1, 0) > boot_kernel( ) > call_linux( )]
1 void call_linux(long a0, long a1, long a2)
2 {
3 cache_clean_invalidate();
4 tlb_invalidate();
5 __asm__(
6 "mov r0, %0/n"
7 "mov r1, %1/n"
8 "mov r2, %2/n"
9 "mov ip, #0/n"
10 "mcr p15, 0, ip, c13, c0, 0/n" /* zero PID */
11 "mcr p15, 0, ip, c7, c7, 0/n" /* invalidate I,D caches */
12 "mcr p15, 0, ip, c7, c10, 4/n" /* drain write buffer */
13 "mcr p15, 0, ip, c8, c7, 0/n" /* invalidate I,D TLBs */
14 "mrc p15, 0, ip, c1, c0, 0/n" /* get control register */
15 "bic ip, ip, #0x0001/n" /* disable MMU */
16 "mcr p15, 0, ip, c1, c0, 0/n" /* write control register */
17 "mov pc, r2/n"
18 "nop/n"
19 "nop/n"
20 : /* no outpus */
21 : "r" (a0), "r" (a1), "r" (a2)
22 );
23 }
对cache、tlb等的操作,您可以参考“实验十一:MMU”。6、7、8行中的%0、
%1、%2对应3个寄存器,它们存放的值分别为参数a0、a1和a2。这样,执行完这3
条语句后,r0、r1、r2对应的值分别为:0, mach_type, 0x3000_8000(内核在内
存中的存放地址)。第17行将跳到去执行内核。至此,linux内核终于开始运行了!
vivi执行完毕后,内存使用情况如下:
图12 vivi启动内核后内存的划分情况