前面将流程搞清楚后,下面就开始进行按照顺序来编写程序了。
第一步就是进行中断向量表的设置。在ARM11中,中断向量表叫做异常向量表。
ARM11共有10种异常,这个在ARM11的datasheet中有。
这里说明一下:
异常 |
说明 |
详细说明 |
Reset |
复位异常 |
当系统刚上电,或者按下复位键时候,触发这个异常,这个时候,程序跳转到这个地址处执行程序 |
undefined_instruction |
未定义指令异常 |
当程序执行发现有一条指令是未定义的指令,会触发这个异常,这个时候,程序跳转到这个地址处执行程序 |
software_interrupt |
软中断异常 |
当软件设置软中断时,会触发这个异常,这个时候,程序跳转到这个地址处执行程序 |
prefetch_abort |
取指异常 |
当CPU取指令发生问题时,会触发这个异常,这个时候,程序跳转到这个地址处执行程序 |
data_abort |
数据异常 |
这个就包括内部取数据和外部取数据,当取数据发生问题时,会触发这个异常,这个时候,程序跳转到这个地址处执行程序 |
irq |
中断异常 |
当有中断触发后,会触发这个异常,这个时候,程序会跳转到这个地址出执行程序 |
fiq |
快中断异常 |
当快中断触发后,会触发这个异常,这个时候,程序会跳转到这个地址出执行程序 |
最后一个目前不知道是什么意思。现在也用不上,先就不管了。
异常,也都写得比较清楚,都知道这些异常大致是干什么的。这里,要注意,异常发生的时候,是跳转到异常地址去执行程序的,但是每个异常地址的大小是4个字节,4个字节大小肯定是放不下程序的。所以,肯定会有第二级跳转。所以这个异常地址的指令,就是一个跳转指令,跳转到对应的程序去执行。
这些异常,现在不用清楚这些异常怎么用,用的时候再来学习就可以了。只要知道有这么些异常就可以了。
这些异常的地址是固定的,这个和STM32是不一样的。所以,我们设置中断向量表的时候,要将这些异常写在固定的地址上。这样,程序才能正常访问这些异常。
异常 |
地址 |
Reset |
0x0000_0000 |
undefined_instruction |
0x0000_0004 |
software_interrupt |
0x0000_0008 |
prefetch_abort |
0x0000_000c |
data_abort |
0x0000_0010 |
irq |
0x0000_0018 |
fiq |
0x0000_001c |
从表中,会发现,数据异常和中断异常之间怎么相隔了8个字节大小,其他都是相隔的4个字节大小。这里是保留了一个异常,但是目前没有定义这个异常是什么,所以把地址给空出来了。所以,我们写程序的时候,要注意把这个地址给空出来。
下面就是我们的程序:
.text.global _start_start: b reset ldr pc, _undefined_instructions ldr pc, _software_interrupt ldr pc, _prefetch_abort ldr pc, _data_abort ldr pc, _no_use ldr pc, _irq ldr pc, _fiq_undefined_instructions: .word undefined_instructions_software_interrupt: .word software_interrupt_prefetch_abort: .word prefetch_abort_data_abort: .word data_abort_no_use: .word no_use_irq: .word irq_fiq: .word fiq undefined_instructions: nopsoftware_interrupt: nopprefetch_abort: nop data_abort: nopno_use: nop irq: nop fiq: nopreset:
简单说明下
.text : 表示是代码段,说明下面的程序是代码
.global _start : 定义全局标号_start
_start的代码,就是设置中断向量表了。可以看出,其实都是跳转指令。不同的异常,跳转到不同的地方去执行程序,这样就实现了异常的处理。
这里
ldr pc, _undefined_instructions 1
_undefined_instructions:
.word undefined_instructions
undefined_instructions:
nop
1的指令,就是将标号_undefined_instructions地址处的值赋值给pc,这样pc的值就是undefined_instructions的值,所以就跳转到undefined_instructions程序地方去执行。这里,undefined_instructions程序就只有一个nop指令。因为目前没有用到这些异常,所以这里,除了Reset异常我们是编写代码外,其他的异常我们都是写的nop。
其他异常的分析,和以上的分析是一样的。只是要注意,我们中间定义了一个_no_use异常。可是这个异常是ARM异常里面没有的。这里定义这个异常,就是为了占一个字节大小,这样的话,irq的地址才会是0x0000_0018,否则的话就是0x0000_0014,这样就不对了。
这里有个问题,只有Reset的跳转指令是b指令,其他的指令都是ldr指令。这个是为什么了?
b指令是相对跳转指令,ldr是绝对跳转指令。在上电或者复位的时候,程序在内部的stepping stone中执行,地址从0x0000_0000开始。但是我们在编译代码的时候,用的链接脚本的链接地址是0x5000_0000,如果使用ldr决定跳转指令的话,就会跳到内存去执行程序了,这个时候,我们还没有把程序拷贝到内存中,所以执行就会出错了,所以这里使用b指令。复位结束后,我们已经把代码拷贝到内存中去了,所以这个时候,就要用ldr绝对跳转指令了。
以上,就将我们的异常向量表就设置好了,接下来,我们就对Reset函数编写代码就好了。因为这里写的代码,就是上电执行的代码。
对比一下STM32的中断向量表的建立:
这个应该很多学STM32的人,都很少去分析这个了,我以前也没有分析,现在是学习比较的时候,才去分析了一下这个东西,发现了很多好玩的东西。
STM32不像ARM一样,分为几个异常,而是将各个异常都分开成独立的中断(在STM32中将异常称为中断了)。在ARM11中,只有一个irq异常,这样的话,不管是什么中断发生,都会跳转到irq的代码去执行。但是STM32就不是了,他将各个中断给独立开,比如外部有外部中断,串口有串口中断。。。当外部中断产生时,就执行外部中断的代码,不会执行串口中断的代码。所以STM32的中断向量表就相对比较大。
下面是中断向量表的一部分截图
前面几个是系统的一些中断,后面是外设的一些中断。我们发现第一个Reset中断的地址竟然不是0x0000_0000,而是0x0000_0004。这里先记下来,后面分析。
上面就是外设的一些中断,可以看出,不同的外设对应不同的中断,而不同的中断有不同的地址。
这里,要说明一下,STM32的中断向量表和ARM11的中断向量表有什么区别,最大的区别就是
STM32的中断向量表的内容保存的是中断的入口地址,即当中断发生时,PC需要跳转的地址
ARM11的中断向量表的内容是异常发生时需执行的指令,即当异常发生时,PC应该执行什么指令。
所以,从上面两个区别可以得出:
ARM11的中断向量表的位置是绝对唯一的,即每个异常的地址是固定的,Reset就是0x0000_0000, irq就是0x0000_0018。
STM32的中断向量表的位置不是唯一的。即每个中断的地址是可以随意放置的。因为他存的是中断的入口地址,而不是存的指令。
还有一个区别,STM32的Reset的地址是0x0000_0004,而不是0x0000_0000。那是因为,STM32规定中断向量表的第一项内容,存的是栈的地址。这个和ARM11也不一样,ARM11的中断向量表的第一项内容就是Reset的指令。
从STM32的启动文件分析,在代码的前面,就定义了这样一个向量表,这个就是中断向量表,里面保存的每个中断的入口地址。第一项内容就是栈的地址。依次是定义各个中断的入口地址。使用DCD 0是定义一个数据,用来占位的。
中断向量表定义之后,就是定义各个中断函数了。
第一个定义的是复位中断,也是就系统上电或者复位有效的时候,执行的程序。后面的依次定义各个中断函数。每个函数的后面都带有[WEAK]属性,表示这里定义的函数是弱函数,外部程序是可以改写这个中断函数的。
到这里,我们就可以知道,因为这里定义了中断函数,所以,当你要使用某个外设的中断的时候,中断函数名字可是不能随便取的,而是要和这里取的名字要一样。不然的话,就跳转不到正确的中断地址去了。
在复位中断函数中,会跳转到SystemInit去执行,这个函数是对时钟和中断向量表进行设置。我们这里就看中断向量表的设置。
这里将设置中断向量表的代码和代码中宏定义定义的值给截图出来。
首先是判断是否定义了VECR_TAB_SRAM这个宏定义,
定义的话,那么中断向量表的基地址就为SRAM_BASE | VECT_TAB_OFFSET
否则的话,中断向量表的基地址就为FLASH_BASE | VECT_TAB_OFFSET
要理解这个代码,就得要说说STM32的启动方式。之前知道ARM11的启动方式有多种,从NANDFLASH启动,从SD卡启动。。。。不管怎么启动,ARM11都是从外部的存储器设备启动的。但是我们的STM32可不是这样的噢。
首先贴上STM32的内部存储器的图。
这个图,没有画出内部闪存FLASH的区域。可以看到内部是有SRAM的,地址从0x2000—_0000到0x3FFFF_FFFF。而且空间还挺大的,有0.5G空间,但是不是所有的STM32都使用了这0.5G空间,像我用的STM32F103ZET6,只有64KB的SRAM,不过已经很大了。不像ARM11,只有8K大小。所以一般STM32是不需要外加SRAM的。还有一个闪存区,图上没有画,从0x0800_0000开始,至于结束地址由芯片的FLASH大小决定的。我用的是STM32F103ZET6系列,FLASH大小是512K。看出来,这大小也不算特别大,但是一般的程序还是够了,毕竟,我们都是用STM32写裸机程序的。
所以说,程序是可以从内部FLASH启动,或者是从SRAM启动的。
这个就是由芯片的两个管脚来决定的。
我用过是主闪存和内置SRAM模式,这两个比较常见。内置SRAM模式启动一般是调试的时候用的。因为FLASH的擦除次数是有限的,调试的话会一直擦除FLASH。会影响FLASH寿命。
有了上面的知识后,理解设置中断向量表的程序就不难了。
定义了VECT_TAB_SRAM这个宏定义,就说明程序是从SRAM启动了,所以就要将中断向量表的基地址给映射到SRAM的起始地址中。SRAM的起始地址是0x2000_0000。
如果没有定义这个宏的话,就说明程序从FLASH启动,所以就要将中断向量表的基地址给映射到FLASH的起始地址中。FLASH的起始地址是0x0800_0000。
因为之前说过,STM32的中断向量表的位置是可以随意放的,可以就可以映射到内存或者是FLASH中。但是ARM11可就不行了,就必须得是0x0000_0000处。
以上,就分析了核心初始化的第一步,设置中断向量表,接着,就是进行设置处理器的模式了。