1.看门狗watch dog timer
现实中因为一些外部因素,电子设备经常会跑飞或者死机(譬如极端炎热、极端寒冷、工业复杂场合)。在这种情况下我们希望设备自动复位而不需要人工干预(无人值守)。看门狗用来完成这个工作。看门狗其实是我们SoC内部的一个定时器(类似于闹钟,类似于门口的狗),定好时间之后看门狗定时器会去计时,时间到之前(狗饿了之前)必须去重新置位看门狗定时器(喂狗),如果没有喂狗则系统会被强制复位。系统在正常工作时,系统软件会自己去喂狗,所以看门狗定时器不会复位。但是系统一旦故障跑飞啥的,看门狗就没人喂了,然后下一个周期就会自动复位,达到我们期望的效果。
物理特性上看门狗其实是个定时器,硬件上就是SoC内部的一个内部外设,且没有外部相关的原件与他有关,所以不需要原理图分析,原理图上根本找不到和看门狗有关的地方。
为什么要关看门狗?
一般CPU设计,在CPU启动后看门狗默认是工作的(为什么默认不关闭而要工作?我猜测是因为怕你的程序在启动代码前端就死机了或者跑飞了没人管),好处就是没有空当和漏洞,坏处就是在启动代码段我们不方便去喂狗(或者说懒得去喂狗)时看门狗会复位,所以为了偷懒我们就在启动代码前端先去关闭看门狗,然后在后面系统启动起来之后再根据需要决定是否要打开看门狗(一旦打开就必须同时提供喂狗)。
在S5PV210内部的iROM代码(BL0)中,其实已经关过看门狗了。所以我们的启动代码实际上是不用去关也没事的,也就是说这次写的关闭看门狗的代码运行后没有任何现象(没有现象就是正常现象)。很多CPU内部是没有BL0的,因此也没人给你关看门狗,都要在启动代码前段自己写代码关看门狗。
关看门狗的SFR(特殊功能寄存器),WTCON(0xE2700000),其中bit5是看门狗的开关:0代表关,1代表开。
2.汇编写启动代码之设置栈和调用c语言①
“C语言运行时(runtime)”需要一定的条件,这些条件由汇编来提供。C语言运行时主要是需要栈。C语言与栈的关系:C语言中的局部变量都是用栈来实现的。如果我们汇编部分没有给C部分预先设置合理合法的栈地址,那么C代码中定义的局部变量就会落空,整个程序就死掉了。
我们平时在编写单片机程序(譬如51单片机)或者编写应用程序时并没有去设置栈,但是C程序还是可以运行的。原因是:在单片机中由硬件初始化时提供了一个默认可用的栈,在应用程序中我们编写的C程序其实并不是全部,编译器(gcc)在链接的时候会帮我们自动添加一个头,这个头就是一段引导我们的C程序能够执行的一段汇编实现的代码,这个代码中就帮我们的C程序设置了栈及其他的运行时需要。
在ARM中37个寄存器中,每种模式下都有自己的独立的SP寄存器(r13),为什么这么设计?
如果各种模式都使用同一个SP,那么就意味着整个程序(操作系统内核程序、用户自己编写的应用程序)都是用一个栈的。你的应用程序如果一旦出错(譬如栈溢出),就会连累操作系统的栈也损坏,整个操作系统的程序就会崩溃。解决方案就是各种模式下用不同的栈。设置栈的时候只设置自己模式下的栈道合理合法的位置即可。
系统在复位后默认是进入SVC模式的。我们如何访问SVC模式下的SP呢?很简单,先把模式设置为SVC,再直接操作SP。但是因为我们复位后就已经是SVC模式了,所以直接设置SP即可。
栈必须是当前一段可用的内存(可用的意思是这个地方必须有被初始化过可以访问的内存,而且这个内存只会被我们用作栈,不会被其他程序征用)。当前CPU刚复位(刚启动),外部的DRRAM尚未初始化,目前可用的内存只有内部的SRAM(因为它不需初始化即可使用)。因此我们只能在SRAM中找一段内存来作为SVC的栈。
在ARM中,ATPCS(ARM关于程序应该怎么实现的一个规范)要求使用满减栈,结合iROM_application_note中的memory map(上图),可知SVC栈应该设置为0xd0037D80。
汇编程序和c语言互相调用:bl cfunction
3.汇编写启动代码之设置栈和调用c语言②
在工程中新建并且添加一个C语言源文件(led.c),注意添加时要修改Makefile。在汇编启动代码中设置好栈后,使用bl xxx的方式来调用C中的函数xxx。
寄存器的地址类似于内存地址(IO与内存统一编址的),所以这里的问题是用C语言读写寄存器,就是用C语言来读写内存地址。用C语言来访问内存,就要用到指针
unsigned int *p = (unsigned int *)0x0xE0200240;
p = 0x11111111;
这两句其实可以简化为1句:((unsigned int *)0x0xE0200240) = 0x11111111;(而前面的又可以用宏定义,即#define rGPJ0CON *((volatile unsigned int *)GPJ0CON))
函数delay写在后面需要在前面声明,写在前面就不需要了。
volatile的作用是让程序在编译时,编译器不对程序做优化(如把+1再-1省略)。优化有时候是ok的,但是有时候是自作聪明会造成程序不对。如果你的一个变量是易变的,不希望编译器帮我们做优化,就在这个变量定义时加volatile。加不加有没有差别,取决于编译器。如果编译器做了优化则有差异;如果编译器本身没做优化,那就没有差别。这里不加也可以。
编译报错(实际上是连接阶段报错):undefined reference to `__aeabi_unwind_cpp_pr1’。解决:在编译时添加-nostdlib这个编译选项即可解决。nostdlib就是不使用标准函数库。标准函数库就是编译器中自带的函数库,用-nostdlib可以让编译器链接器优先选择我程序内自己写的函数库(防止名字冲突)。
下列三张图为3.set_sp的start.S、led.c、makefile修改后的代码
4.汇编写启动代码之开iCache
cache是一种内存,叫高速缓存。从容量来说:CPU < 寄存器 < cache < DDR;从速度来说:CPU > 寄存器 > cache > DDR。cache的存在,是因为寄存器和ddr之间速度差异太大,ddr的速度远不能满足寄存器的需要(不能满足cpu的需要,所以没有cache会拉低整个系统的整体速度)。整个系统中CPU的供应链由:寄存器+cache+DDR+硬盘/flash四阶组成,这是综合考虑了性能、成本后得到的妥协的结果。
cache的意义:指令平时是放在硬盘/flash中的,运行时读取到DDR中,再从DDR中读给寄存器,再由寄存器送给cpu。但是DDR的速度和寄存器(代表的就是CPU)相差太大,如果CPU运行完一句再去DDR读取下一句,那么CPU的速度完全就被DDR给拖慢了。解决方案就是icache。
icache工作时,会把我们CPU正在运行的指令的旁边几句指令事先给读取到icache中(CPU设计有一个基本原理:代码执行时,下一句执行当前一句代码旁边代码的可能性要大很多)。当下一句CPU要指令时,cache首先检查自己事先准备的缓存指令中有没这句,如果有就直接拿给CPU,如果没有则需要从DDR中重新去读取拿给CPU,并同时做一系列的动作:清缓存、重新缓存。
210内部有32KB icache和32kb dcache。icache是用来缓存指令的;dcache是用来缓存数据的。首先,icache的一切动作都是自动的,不需人为干预。我们所需要做的就是打开/关闭icache。其次,在210的iROM中BL0已经打开了icache。所以之前看到的现象都是icache打开时的现象。
icache在协处理器cp15里。搜索资料知cp15的寄存器c1的bit12用来开关icache。
这里第1/2/3步在iROM中已经初始化做过,在这里做不做都一样。发现关闭icache比icache打开时led闪烁变慢,说明指令执行速度确实变慢。
5.重定位引入和链接脚本①
位置无关编码(PIC,position independent code):汇编源文件被编码成二进制可执行程序时编码方式与位置(内存地址)无关。位置有关编码:汇编源码编码成二进制可执行程序后和内存地址是有关的。大部分指令是位置有关编码。
链接地址:链接时指定的地址(指定方式为:Makefile中用-Ttext,或者链接脚本)
运行地址:程序实际运行时地址(指定方式:由实际运行时被加载到内存的哪个位置说了算)
我们在设计一个程序时,会给这个程序指定一个运行地址(链接地址)。就是决定将来程序运行时的地址(运行地址)的,而且必须给编译器链接器指定这个地址(链接地址)才行。最后得到的二进制程序理论上是和你指定的运行地址有关的,将来这个程序被执行时必须放在当时编译链接时给定的那个地址(链接地址)下才行,否则不能运行(就叫位置有关代码)。但是有个别特别的指令他可以跟指定的地址(链接地址)没有关系,也就是说这些代码实际运行时不管放在哪里都能正常运行。
位置无关代码要好一些,适应性强,放在哪里都能正常运行;位置有关代码就必须运行在链接时指定的地址上,适应性差。位置无关码有一些限制,不能完成所有功能,有时候不得不使用位置有关代码。
对于位置有关代码来说:最终执行时的运行地址和编译链接时给定的链接地址必须相同,否则一定出错。
我们之前的裸机程序中,Makefile中用 -Ttext 0x0 来指定链接地址是0x0。这意味着我们认为这个程序将来会放在0x0这个内存地址去运行。但是实际上我们运行时的地址是0xd0020010(我们用dnw下载时指定的下载地址)。这两个地址看似不同,但是实际相同。这是因为S5PV210内部做了映射,把SRAM映射到了0x0地址去。(IROM&IRAM,一个房间的两个门)
三星推荐的启动方式中:bootloader必须小于96KB并大于16KB,假定bootloader为80KB,启动过程是这样子:先开机上电后BL0运行,BL0会加载外部启动设备中的bootloader的前16KB(BL1)到SRAM中去运行,BL1运行时会加载BL2(bootloader中80-16=64KB)到SRAM中(从SRAM的16KB处开始用)去运行;BL2运行时会初始化DDR并且将OS搬运到DDR去执行OS,启动完成。
uboot实际使用的方式:uboot大小随意,假定为200KB。启动过程是这样子:先开机上电后BL0运行,BL0会加载外部启动设备中的uboot的前16KB(BL1)到SRAM中去运行,BL1运行时会初始化DDR,然后将整个uboot搬运到DDR中,然后用一句长跳转(从SRAM跳转到DDR)指令从SRAM中直接跳转到DDR中继续执行uboot直到uboot完全启动。uboot启动后在uboot命令行中去启动OS。
为什么要重定位?链接地址和运行地址有时候必须不相同,而且还不能全部用位置无关码,这时候只能重定位。
分散加载:把uboot分成2部分(BL1和整个uboot),两部分分别指定不同的链接地址。启动时将两部分加载到不同的地址(BL1加载到SRAM,整个uboot加载到DDR),这时候不用重定位也能启动。评价:分散加载其实相当于手工重定位。重定位是用代码来进行重定位,分散加载是手工操作重定位的。
6.重定位引入和链接脚本②
运行地址是由运行时决定的(编译链接时是无法绝对确定运行时地址的)。链接地址是由程序员在编译链接的过程中,通过Makefile中-Ttext xxx或者在链接脚本中指定的。程序员事先会预知自己的程序的执行要求,并且有一个期望的执行地址,并且会用这个地址来做链接地址。
举例:(1)linux中的应用程序。gcc hello.c -o hello,这时使用默认的链接地址就是0x0,所以应用程序都是链接在0地址的。因为应用程序运行在操作系统的一个进程中,在这个进程中这个应用程序独享4G的虚拟地址空间。所以应用程序都可以链接到0地址,因为每个进程都是从0地址开始的。(编译时可以不给定链接地址而都使用0)
(2)210中的裸机程序。运行地址由我们下载时确定,下载时下载到0xd0020010,所以就从这里开始运行。(这个下载地址也不是我们随意定的,是iROM中的BL0加载BL1时事先指定好的地址,这是由CPU的设计决定的)。所以理论上我们编译链接时应该将地址指定到0xd0020010,但是实际上我们在之前裸机程序中都是使用位置无关码PIC,所以链接地址可以是0。
从源代码到可执行程序的步骤:预编译:预编译器执行,如替换#define宏、注释由预编译器处理。编译:编译器执行,把.c/.S变成机器码.o文件。链接:链接器执行,把.o文件的各函数按照一定规则(链接脚本)累积在一起形成可执行文件。可选步骤①strip:把可执行程序中的符号信息拿掉以节省空间(如Debug和Release,拿掉前为可调试的Debug版本,拿掉后为Release版本)②objcopy:由可执行程序生成的可烧录的镜像bin文件
7.重定位引入和链接脚本②
段就是程序的一部分,我们把整个程序分成了一个一个的段,给每个段起个名字,然后在链接时就可以用这个名字来指示这些段。段名分为2种:一种是编译器链接器内部定好的名字;一种是程序员自己定义的段名。
先天性段名:
代码段:(.text),又叫文本段,代码段其实就是函数编译后生成的东西
数据段:(.data),数据段就是C语言中有显式初始化为非0的全局变量
bss段:(.bss),又叫ZI(zero initial)段,就是零初始化段,对应C语言中初始化为0的全局变量。C语言中全局变量如果未显式初始化,值是0。本质就是C语言把这类全局变量放在了bss段,从而保证了为0
后天性段名:
段名由程序员自己定义,段的属性和特征也由程序员自己定义。
C运行时环境如何保证显式初始化为非0的全局变量的值在main之前就被赋值了?就是因为它把这类变量放在了.data段中,而.data段会在main执行之前被处理(初始化)。
链接脚本其实是个规则文件,他是程序员用来指挥链接器工作的。链接器会参考链接脚本,并且使用其中规定的规则来处理.o文件中那些段,将其链接成一个可执行程序。链接脚本的关键内容有2部分:段名 + 地址(作为链接地址的内存地址)。
编译脚本:
SECTIONS {}这个是整个链接脚本。. 点号在链接脚本中代表当前位置。= 等号代表赋值,bss_start和bss_end在别的程序里可能会被引用。*表示匹配其他所有的同类文件
8.代码重定位实战①
任务:在SRAM中将代码从0xd0020010重定位到0xd0024000
任务解释:本来代码是运行在0xd0020010的(因为CPU在设计的时候确定了只有下载到这里代码才能被执行),但是因为一些原因我们又希望代码实际是在0xd0024000位置运行的,这时需要重定位。
注:本练习对代码本身无实际意义,但是某些情况重定位就是必须的,如在uboot中。
①通过链接脚本将代码链接到0xd0024000;②dnw下载时将bin文件下载到0xd0020010。以上两点需要通过重定位来完成。
当我们把代码链接地址设置为0xd0024000时,意思就是代码将来必须放在0xd0024000位置才能正确执行。如果实际运行地址不是这个地址就要出事(除非代码是PIC位置无关码)。所以重定位代码的作用就是:在PIC执行完之前(在代码中第一句位置有关码执行之前)必须将整个代码搬移到0xd0024000位置去执行(去执行同一个函数led_blink)。
③代码执行时通过代码前段的少量位置无关码将整个代码搬移到0xd0024000;④使用一个长跳转跳转到0xd0024000处的代码继续执行,重定位完成。
ARM中的跳转指令就是类似于分支指令B、BL等作用的指令。跳转指令通过给PC(r15)赋一个新值来完成代码段的跳转执行。长跳转指的是跳转的范围比较宽广。重定位后,实际上在SRAM中有2份代码的镜像(一份在0xd0020010处开头,另一份是重定位代码复制到了0xd0024000)。重定位之后使用ldr pc, =led_blink这句长跳转直接从0xd0020010处代码跳转到0xd0024000开头的那一份代码的led_blink函数处去执行。(如果短跳转bl led_blink则执行的就是0xd0020010开头的这一份)
9.代码重定位实战②
ldr和adr都是伪指令,区别是ldr是长加载、adr是短加载。adr加载的是运行时地址;ldr加载的是链接地址。只要知道adr和ldr分别用于加载运行地址和链接地址,从而可以判断是否需要重定位即可(具体原因可以从反汇编文件中找到对应程序,ldr最终会替换成一句间接寻址,而adr会替换成相对于pc的偏移量的加减)。
只要知道adr和ldr分别用于加载运行地址和链接地址,从而可以判断是否需要重定位即可只要知道adr和ldr分别用于加载运行地址和链接地址,从而可以判断是否需要重定位即可。
重定位就是copy_loop函数,作用是复制代码到链接地址。复制的源地址是SRAM的0xd0020010,复制目标地址是SRAM的0xd0024000,复制长度是bss_start减去_start,也就是整个程序中代码段+数据段的长度。bss段(bss段中就是0初始化的全局变量)不需要重定位,只需要清零即可。
清除bss段是为了满足C语言的运行时要求。在C语言中,对于显式初始化为0的全局变量和未显式初始化的全局变量的值为0,实际上是编译器通过清bss段来实现的。即编译器和链接器会帮程序自动添加一段头程序,在main函数运行前清除bss。但是编译器不负责清除重定位地址处开头的那一份代码的bss,所以需要自己去清除。
ldr的目标是pc就叫长跳转,是r1就叫长加载;adr加载叫短加载,bl跳转叫短跳转。
10.SDRAM引入
SDRAM同步动态随机存储器,平时说的DDR就是DDR SDRAM(双倍速率同步动态随机存储器),是SDRAM的升级版。DDR有DDR1/DDR2/DDR3/DDR4/LPDDR(低功耗)。
SDRAM的特性:容量大、价格低、掉电易失性、随机读写、总线式访问。SDRAM/DDR属于动态内存DRAM,需要代码进行初始化,不像SRAM上电就可以使用(外存NorFlash和NandFlash也是如此,前者是总线式访问所以可以直接读取,后者不是所以需要初始化)。
开发板原理图上使用的是K4T1G164QQ,但是实际开发板上贴的不是这个,不过两款完全兼容,可以参考其文档。K4T1G164QE:K表示三星产品,4表示是DRAM,T表示产品号码,1G表示容量(1Gb(bit),等于128MB(byte),X210上有4片,所以总容量是512MB)16表示单芯片是16位宽的,4表示是8bank。
11.SDRAM初始化
S5PV210核心板原理图中可见两个内存端口(port1/2),对应数据手册中内存映射部分分别为DRAM0/1。DRAM0:0x200000000x3FFFFFFF(512MB),对应引脚是Xm1xxxx;DRAM1:0x400000000x7FFFFFFF(1024MB),对应引脚是Xm2xxxx。所以210最多支持1.5GB内存,但是实际开发板不一定这么多,比如我们X210开发板只有512MB,DRAM0和DRAM1分别占256MB,即合法地址是0x200000000x2FFFFFFF+0x400000000x4FFFFFFF。每个DDR端口都由三类总线构成:地址总线14根(Xmn_ADDR0Xmn_ADDR13)、控制总线、数据总线32根(Xmn_DATA0Xmn_DATA31,可见用的是32位的(物理)内存)。
核心板原理图中画出4片内存芯片的一页,可以看出:X210用了4片内存(每片1Gb=128MB,共512MB),每片的数据总线都是16位的。可以使用并联将16位内存得到32位内存,横向的2颗内存芯片就是并联连接的。并联时地址总线相同,数据总线相加,从逻辑上把两颗16位芯片看作一颗32位芯片。
看数据手册《NT5TU64M16GG-DDR2-1G-G-R18-Consumer》第10页的block diagram。这个框图是128Mb×8结构的,8指的是8bank,每bank128Mbit。DDR芯片上的BA0~BA2就是用来选择bank的。每个bank通过row address(14位) + column address(10位)的方式来综合寻址。寻址范围是:214*210 = 2^24,即16MB(128Mbit)。
12.汇编初始化SDRAM详解①
修改重定位的地址为0x20000000,等初始化完后就可以重定位到SDRAM了。
start.S中增加第4步:初始化ddr,此处短跳转到函数sdram_asm_init进行初始化(在sdram_init.S中,是一个汇编函数)。.global是一个供外部使用的声明,汇编实现的函数在返回时需要明确使用返回指令(mov pc, lr)。
初始化DDR2需27步,步骤在S5PV210数据手册memory→DRAM CONTROLLER→1.2.1.3中。DDR初始化和SoC(SoC中的DDR2控制器)、开发板使用的DDR芯片、开发板设计时DDR的连接方式有关。
因为是在DRAM0上连接256MB,在DRAM1上连接256MB,所以在sdram_asm_init中初始化DRAM时分为两部分,第一部分初始化DRAM0(127-270行),第二部分初始化DRAM1(271-417行)。此代码来自于①九鼎官方uboot;②九鼎逻辑教程进行优化;③朱老师修改并优化部分参数。
(开头的三行代码无需理会)
ldr r0, =0xf1e00000
ldr r1, =0x0
str r1, [r0, #0x0]
接下来DMC0 Drive Strength (Setting 2X)(64-95行)设置IO端口驱动强度,因为DDR芯片和S5PV210之间通过很多总线连接,物理表现为引脚。DDR芯片工作时需要一定的驱动信号,驱动信号达到一定水平才能抗干扰,所以需要设置引脚的驱动能力,使其正常工作。
这里的基地址加上偏移量就是0xE02003CC,在数据手册找到如下设置方式,这里要求的是设置2X(参考的是原厂代码),即0b1010101010101010,对应为0xAAAA(0xA=0b1010),由于是32位所以r1应设为0x0000AAAA。DMC0/1分别对应DRAM0/1,DMC1设置同理。
上面就设置完了第1步,下面设置时钟为第2-4步(127-154行)。按照手册上第2步的要求,根据手册PHY Control该部分的提示,把DMC0的PHYCONTROL0设置为0x00101000。然后就可以开启DLL(dram的锁相环pll)然后find_lock_val等待锁存。
13.汇编初始化SDRAM详解②
(160-270行)设置DDR2,DMC_CONTROL部分的设置在1.4.1.1涉及时序,这里不做了解,照用的是原厂的代码。
DMC0_MEMCONTROL部分的设置在1.4.1.2,bl根据描述在DDR2情况下只支持4(即0x2),num_chip是指存储器芯片的数量(这里朱老师的理解是由于逻辑地址相连,2片是并联的,所以认做1片),burst length=4,1chip,……对应值是0x00202400。
DMC0_MEMCONFIG_0的设置在1.4.1.3,朱老师认为应该设为0xF01323(原来是0xE01323),chip_base由内存芯片的起始地址决定,所以为DMC0应为0x20,chip_mask是掩码,用来设置内存的长度(这里的长度是0x20000000-0x2FFFFFFF,为0x0FFFFFFF,由手册给的方法算出来掩码应为0xF0),chip_col、chip_row和chip_bank指的是一块DDR的有8bank,每个bank是通过row address(14位) + column address(10位)的方式来综合寻址的。
朱老师猜测(推论):三星设置DRAM0通道,允许我们接2片256MB的内存,分别叫memory chip0和memory chip1,分别用这两个寄存器来设置它的参数。按照三星的设计,chip0的地址应该是0x20000000到0x2FFFFFFF,然后chip1的地址应该是0x30000000~0x3FFFFFFF(可由1.4.1.4看出)各自256MB。但是我们X210开发板实际在DRAM0端口只接了256MB的内存,所以只用了chip0,没有使用chip1(我们虽然是2片芯片,然后这两片是并联形成32位内存的,逻辑上只能算1片)。按照这个推论,DMC0_MEMCONFIG_0有用,而DMC0_MEMCONFIG_1无用,所以我直接给他了默认值(原先的代码和默认值有差别,朱老师认为因为用不到所以随便设为什么都行,就设了默认值)。
DMC0_TIMINGA_REF,我们的时钟不是1.4.1.10中的133MHz,是200MHz,所以转换后为0x00000618而不是0x40E。DMC0_TIMINGA_ROW/DATA/PWR都是时序相关的东西,朱老师不建议看。
DMC_DIRECTCMD这个寄存器是个命令寄存器,我们210通过向这个寄存器写值来向DDR芯片发送命令(通过命令总线),这些命令应该都是用来配置DDR芯片工作参数(于1.4.1.5可见)。程序的配置顺序按照27步的顺序进行。
总结:DDR配置过程比较复杂,基本上是按照DDR控制器的时序要求来做的,其中很多参数要结合DDR芯片本身的参数来定,还有些参数是时序参数,要去详细计算。所以DDR配置非常繁琐、细致、专业。所以我们对DDR初始化的态度就是:学会这种思路和方法,结合文档和代码能看懂,会算一些常见的参数即可。