startup_stm32f10x_ld_vl.s : for STM32 Low density Value line devices
startup_stm32f10x_ld.s : for STM32 Low density devices
startup_stm32f10x_md_vl.s : for STM32 Medium density Value line devices
startup_stm32f10x_md.s : for STM32 Medium density devices
startup_stm32f10x_hd.s : for STM32 High density devices
startup_stm32f10x_xl.s : for STM32 XL density devices
startup_stm32f10x_cl.s : for STM32 Connectivity line devices
cl:互联型产品,stm32f105/107系列
vl:超值型产品,stm32f100系列
xl:超高密度产品,stm32f101/103系列
ld:低密度产品,FLASH小于64K
md:中等密度产品,FLASH=64 or 128
hd:高密度产品,FLASH大于128
1 startup_stm32f10x_hd.s解析:
1.1 启动文件简介
Set the initial SP //初始化堆栈指针 SP
Set the initial PC == Reset_Handler //初始化 PC 指针
Set the vector table entries with the exceptions ISR address //初始化中断向量表
Configure the system clock and the external SRAM mounted on STM324xG-EVAL board to be used as data memory (optional,to be enabled by user) //配置系统时钟
Branches to __main in the C library (which eventually calls main()). //调用 C 库函数_main 初始化用户堆栈,从而最终调用 main 函数去到 C 的世界
ARM汇编指令:
指令名称 | 作用 |
---|---|
EQU | 给数字常量取一个符号名,相当于 C 语言中的 define |
AREA | 汇编一个新的代码段或者数据段 |
SPACE | 分配内存地址 |
PRESERVE8 | 当前文件堆栈需按照 8 字节对齐 |
EXPORT | 声明一个标号具有全局属性,可被外部的文件使用 |
DCD | 以字为单位分配内存,要求 4 字节对齐,并要求初始化这些内存 |
PORC | 定义子程序,与 ENDP 成对使用,表示子程序结束 |
WEAK | 弱定义,如果外部文件声明了一个标号,则优先使用外部文件定义的标号,如果外部文件没有定义也不出错。要注意的是:这个不是 ARM的指令,是编译器的,这里放在一起只是为了方便。 |
IMPORT | 声明标号来自外部文件,跟 C 语言中的 EXTERN 关键字类似 |
B | 跳转到一个标号 |
ALIGN | 编译器对指令或者数据的存放地址进行对齐,一般需要跟一个立即数,缺省表示 4 字节对齐。要注意的是:这个不是 ARM 的指令,是编译器的,这里放在一起只是为了方便。 |
END | 到达文件的末尾,文件结束 |
IF,ELSE,ENDIF | 汇编条件分支语句,跟 C 语言的 if else 类似 |
1.2 开辟栈空间
/* Amount of memory (in bytes) allocated for Stack
Tailor this value to your application needs
<h> Stack Configuration ;栈定义
<o> Stack Size (in Bytes) <0x0-0xFFFFFFFF:8>
</h> */
Stack_Size EQU 0x00000400 ; /*EQU伪指令,作用是左边的符号名代表右边的表达式,定义栈大小1024B*/
AREA STACK, NOINIT, READWRITE, ALIGN=3 /*定义栈段:名称为STACK,未初始化,可读写,ELF 的栈段按2^3=8对齐*/
Stack_Mem SPACE Stack_Size /*分配一片连续的存储区域并初始化为 0,栈空间:0x400个字节*/
__initial_sp /*栈空间顶地址*/
- 开辟栈的大小为 0X00000400(1KB)(4*162),名字为 STACK,NOINIT 即不初始化,可读可写, 8(2^3)字节对齐。
- 栈的作用是用于局部变量,函数调用,函数形参等的开销,栈的大小不能超过内部SRAM 的大小。如果编写的程序比较大,定义的局部变量很多,那么就需要修改栈的大小。如果某一天,你写的程序出现了莫名奇怪的错误,并进入了硬 fault 的时候,这时你就要考虑下是不是栈不够大,溢出了。
- EQU:宏定义的伪指令,相当于等于,类似与 C 中的 define。
- AREA:告诉汇编器汇编一个新的代码段或者数据段。 STACK 表示段名,这个可以任意命名; NOINIT 表示不初始化; READWRITE 表示可读可写, ALIGN=3,表示按照 2^3对齐,即 8 字节对齐。
- SPACE:用于分配一定大小的内存空间,单位为字节。这里指定大小等于 Stack_Size。标号__initial_sp 紧挨着 SPACE 语句放置,表示栈的结束地址,即栈顶地址,栈是由高向低生长的,而地址是由低向高分配的。
1.3 开辟堆空间
/* <h> Heap Configuration 堆定义
<o> Heap Size (in Bytes) <0x0-0xFFFFFFFF:8>
</h> */
Heap_Size EQU 0x00000200 //定义堆的大小
AREA HEAP, NOINIT, READWRITE, ALIGN=3 //堆段,malloc用的地方,不一定连续空间,未初始化,允许读写,堆数据段8字节边界对齐
__heap_base /*堆空间起始地址*/
Heap_Mem SPACE Heap_Size /*堆空间:0x200个字节*/
__heap_limit /*堆空间结束地址*/
PRESERVE8 /*PRESERVE8 指令指定当前文件保持堆栈八字节对齐*/
THUMB /*告诉汇编器下面是32位的Thumb指令,如果需要汇编器将插入位以保证对齐*/
- 开辟堆的大小为 0X00000200(512 字节),名字为 HEAP, NOINIT 即不初始化,可读可写, 8(2^3)字节对齐。
- __heap_base表示堆的起始地址,__heap_limit表示堆的结束址。堆是由低向高生长的,跟栈的生长方向相反。
- 堆主要用来动态内存的分配,像 malloc()函数申请的内存就在堆上面。这个在 STM32里面用的比较少。、
- PRESERVE8表指定当前文件的堆栈按照8字节对齐,THUMB表示接下来的指令兼容THUMB指令。
- THUMB是ARM老的16bit的指令集,现在Cortex-M系列的ARM都使用32bit的THUMB-2指令集,它兼容16bit和32bit的指令。
1.4 定义向量表
/*EXPORT 命令声明一个符号,可由链接器用于解释各个目标和库文件中的符号引用,相当于声明了一个全局变量。 GLOBAL 于 EXPORT相同。
以下为向量表,在复位时被映射到FLASH的0地址
Vector Table Mapped to Address 0 at Reset;实际上是在CODE区(假设STM32从FLASH启动,则此中断向量表起始地址即为0x8000000)*/
AREA RESET, DATA, READONLY /*定义一块数据段<DATA>,只可读<READONLY,默认READWRITE>,段名字是RESET*/
EXPORT __Vectors /*标号输出,中断向量表开始;EXPORT在程序中声明一个全局的标号__Vectors,该标号可在其他的文件中引用*/
EXPORT __Vectors_End /*在程序中声明一个全局标号__Vectors_End*/
EXPORT __Vectors_Size /*在程序中声明一个全局号__Vectors_Size,中断向量表大小*/
- 定义一个数据段,名字为 RESET,可读。并声明 __Vectors、__Vectors_End和 __Vectors_Size这三个标号具有全局属性,可供外部的文件调用。
- EXPORT: 声明一个标号可被外部的文件使用,使标号具有全局属性。如果是 IAR 编译器,则使用的是 GLOBAL 这个指令。
- 当内核响应了一个发生的异常后,对应的异常服务例程(ESR)就会执行。为了决定 ESR的入口地址, 内核使用了“向量表查表机制”。这里使用一张向量表。向量表其实是一个WORD(32 位整数)数组,每个下标对应一种异常,该下标元素的值则是该 ESR 的入口地址。向量表在地址空间中的位置是可以设置的,通过 NVIC 中的一个重定位寄存器来指出向量表的地址。在复位后,该寄存器的值为 0。因此,在地址 0 (即 FLASH 地址 0) 处必须包含一张向量表,用于初始时的异常分配。要注意的是这里有个另类: 0 号类型并不是什么入口地址,而是给出了复位后 MSP 的初值。
由手册存储器映像图获知,STM32的0地址处是用来映射的。
STM32启动的方式如下:
(1)用户闪存存储器: 用户代码烧录在这里,STM32正常启动时就是从这里启动
(2)系统存储器: 实现ISP下载功能。ISP(in-system programming)意为在系统编程。烧录程序时不需要烧录器,PC机通过串口把BIN/HEX文件直接烧录到单片机内部FLASH中
(3)内嵌SRAM: 实现调试器调试功能用
当选择从用户闪存存储器(flash)启动时候,0x0地址就会把flash的起始地址映射于此,flash的起始地址是0x08000000,中断向量表就放在这个地址。当用户选择从内嵌SRAM启动时候,0x0地址就会把SRAM的起始地址映射于此,SRAM的起始地址是0x20000000。注意,一般我们都是选择从flash启动的。
中断向量表我的内容如下:
//DCD 命令分配一个或多个字的存储器,在四个字节的边界上对齐,并定义存储器的运行时初值。
__Vectors DCD __initial_sp // Top of Stack 栈顶指针,被放在向量表的开始,FLASH的0地址,复位后首先装载栈顶指针
DCD Reset_Handler // Reset Handler 复位异常,装载完栈顶后,第一个执行的,并且不返回。
DCD NMI_Handler // NMI Handler 不可屏蔽中断
DCD HardFault_Handler // Hard Fault Handler 硬件错误中断
DCD MemManage_Handler // MPU Fault Handler 内存管理错误中断
DCD BusFault_Handler // Bus Fault Handler 总线错误中断,一般发生在数据访问异常,比如fsmc访问不当
DCD UsageFault_Handler // Usage Fault Handler 用法错误中断,一般是预取值,或者位置指令,数据处理等错误
DCD 0 // Reserved
DCD 0 // Reserved
DCD 0 // Reserved
DCD 0 // Reserved
DCD SVC_Handler // SVCall Handler 系统调用异常,主要是为了调用操作系统内核服务
DCD DebugMon_Handler // Debug Monitor Handler 调试监视异常
DCD 0 // Reserved
DCD PendSV_Handler /*PendSV Handler 挂起异常,此处可以看见用作了uCOS-II的上下文切换异常,这是被推荐使用的,因为Cortex-M3会在异常发生时自动保存R0-R3,R12,R13(堆栈指针SP),R14(链接地址,也叫返回地址LR,在异常返回时使用),R15(程序计数器PC,为当前应用程序+4)和中断完成时自动回复我们只需保存R4-R11,大大减少了中断响应和上下文切换的时间。说明:此处涉及到一个中断保存寄存器问题:因为在所有的运行模式下,未分组寄存器都指向同一个物理寄存器,他们未被系统用作特殊的用途,因此,在中断或者异常处理进行模式转换时,由于不同模式(此处为"线程"和"特权")均使用相同的物理寄存器,可能会造成寄存器中数据的破坏,这也是常说的"关键代码段"和"l临界区"保护的原因。*/
DCD SysTick_Handler // SysTick Handler 滴答定时器,为操作系统内核时钟
// External Interrupts 以上都是Coretex M3内核自带的;以下为外部中断向量表
DCD WWDG_IRQHandler // Window Watchdog
DCD PVD_IRQHandler // PVD through EXTI Line detect
DCD TAMPER_IRQHandler // Tamper
DCD RTC_IRQHandler // RTC
DCD FLASH_IRQHandler // Flash
DCD RCC_IRQHandler // RCC
DCD EXTI0_IRQHandler // EXTI Line 0
DCD EXTI1_IRQHandler // EXTI Line 1
DCD EXTI2_IRQHandler // EXTI Line 2
DCD EXTI3_IRQHandler // EXTI Line 3
DCD EXTI4_IRQHandler // EXTI Line 4
DCD DMA1_Channel1_IRQHandler // DMA1 Channel 1
DCD DMA1_Channel2_IRQHandler // DMA1 Channel 2
DCD DMA1_Channel3_IRQHandler // DMA1 Channel 3
DCD DMA1_Channel4_IRQHandler // DMA1 Channel 4
DCD DMA1_Channel5_IRQHandler // DMA1 Channel 5
DCD DMA1_Channel6_IRQHandler // DMA1 Channel 6
DCD DMA1_Channel7_IRQHandler // DMA1 Channel 7
DCD ADC1_2_IRQHandler // ADC1 & ADC2
DCD USB_HP_CAN1_TX_IRQHandler // USB High Priority or CAN1 TX
DCD USB_LP_CAN1_RX0_IRQHandler // USB Low Priority or CAN1 RX0
DCD CAN1_RX1_IRQHandler // CAN1 RX1
DCD CAN1_SCE_IRQHandler // CAN1 SCE
DCD EXTI9_5_IRQHandler // EXTI Line 9..5
DCD TIM1_BRK_IRQHandler // TIM1 Break
DCD TIM1_UP_IRQHandler // TIM1 Update
DCD TIM1_TRG_COM_IRQHandler // TIM1 Trigger and Commutation
DCD TIM1_CC_IRQHandler // TIM1 Capture Compare
DCD TIM2_IRQHandler // TIM2
DCD TIM3_IRQHandler // TIM3
DCD TIM4_IRQHandler // TIM4
DCD I2C1_EV_IRQHandler // I2C1 Event
DCD I2C1_ER_IRQHandler // I2C1 Error
DCD I2C2_EV_IRQHandler // I2C2 Event
DCD I2C2_ER_IRQHandler // I2C2 Error
DCD SPI1_IRQHandler // SPI1
DCD SPI2_IRQHandler // SPI2
DCD USART1_IRQHandler // USART1
DCD USART2_IRQHandler // USART2
DCD USART3_IRQHandler // USART3
DCD EXTI15_10_IRQHandler // EXTI Line 15..10
DCD RTCAlarm_IRQHandler // RTC Alarm through EXTI Line
DCD USBWakeUp_IRQHandler // USB Wakeup from suspend
DCD TIM8_BRK_IRQHandler // TIM8 Break
DCD TIM8_UP_IRQHandler // TIM8 Update
DCD TIM8_TRG_COM_IRQHandler // TIM8 Trigger and Commutation
DCD TIM8_CC_IRQHandler // TIM8 Capture Compare
DCD ADC3_IRQHandler // ADC3
DCD FSMC_IRQHandler // FSMC
DCD SDIO_IRQHandler // SDIO
DCD TIM5_IRQHandler // TIM5
DCD SPI3_IRQHandler // SPI3
DCD UART4_IRQHandler // UART4
DCD UART5_IRQHandler // UART5
DCD TIM6_IRQHandler // TIM6
DCD TIM7_IRQHandler // TIM7
DCD DMA2_Channel1_IRQHandler // DMA2 Channel1
DCD DMA2_Channel2_IRQHandler // DMA2 Channel2
DCD DMA2_Channel3_IRQHandler // DMA2 Channel3
DCD DMA2_Channel4_5_IRQHandler // DMA2 Channel4 & Channel5
__Vectors_End //向量表结束标志
__Vectors_Size EQU __Vectors_End - __Vectors //计算向量表地址空间大小,得到向量表的大小,304个字节也就是0x130个字节
在《STM32中文参考手册_V10.pdf》的第9章,有对这些中断的描述的表格:(部分截图)
向量表(一般)从flash的0地址处开始放置,以4字节为一单位,地址0存放的是栈顶地址,0x04放的是复位处理函数的地址,以此类推。代码中的”*_Handler”是对应中断的中断服务函数,也就是中断处理程序的地址。
1.5 复位处理程序
复位处理程序是系统上电/复位后第一个执行的:
AREA |.text|, CODE, READONLY /*定义一个代码段,可读,段名字是.text
|.text| 用于表示由 C 编译程序产生的代码段,或用于以某种方式与 C 库关联的代码段。定义C编译器源代码的代码段,只读.定义一个代码段,可读,段名字是.text。 */
Reset_Handler PROC //利用PROC、ENDP这一对伪指令把程序段分为若干个过程,使程序的结构加清晰
EXPORT Reset_Handler [WEAK] /*[WEAK];WEAK声明其他的同名标号优先于该标号被引用,就是说如果外面声明了的话,调用外面的对应函数*/
IMPORT __main /*;IMPORT:伪指令用于通知编译器要使用的标号在其他的源文件中定义/但要在当前源文件中引用,而且无论当前源文件是否引用该标号/该标号均会被加入到当前源文件的符号表中*/
IMPORT SystemInit
LDR R0, =SystemInit /*系统初始化*/
BLX R0 /*带链接的跳转,切换指令集,跳到SystemInit*/
LDR R0, =__main /*__main为运行时库提供的函数;完成堆栈,堆的初始化等工作,会调用下面定义的__user_initial_stackheap*/
BX R0 /*切换指令集,main函数不返回跳到__main,进入C的世界*/
ENDP
- 定义一个名称为.text 的代码段,可读。
- 复位子程序是系统上电后第一个执行的程序,调用 SystemInit 函数初始化系统时钟,然后调用 C 库函数_mian,最终调用 main 函数去到 C 的世界。
- WEAK:表示弱定义,如果外部文件优先定义了该标号则首先引用该标号,如果外部文件没有声明也不会出错。这里表示复位子程序可以由用户在其他文件重新实现,这里并不是唯一的。特别注意[WEAK]声明,它表示弱定义:如果外部文件优先定义了该标号则首先引用外部文件定义的标号,反之就引用此处用[WEAK]声明的标号。
- IMPORT:表示该标号来自外部文件,跟 C 语言中的 EXTERN 关键字类似。这里表示SystemInit 和__main 这两个函数均来自外部的文件。
- SystemInit() 是一个标准的库函数,在 system_stm32f4xx.c 这个库文件总定义。主要作用是配置系统时钟,这里调用这个函数之后,F429 的系统时钟配被配置为 180M。
- __main 是一个标准的 C 库函数,主要作用是初始化用户堆栈,最终调用 main 函数去到 C 的世界。这就是为什么我们写的程序都有一个 main 函数的原因。如果我们在这里不调用__main,那么程序最终就不会调用我们 C 文件里面的 main,如果是调皮的用户就可以修改主函数的名称,然后在这里面 IMPORT 你写的主函数名称即可。
-
LDR:作为伪指令加载一个立即数或一个地址到一个寄存器
-
这个时候你在 C 文件里面写的主函数名称就不是 main 了,而是 user_main 了。
LDR、 BLX、 BX 是 CM4 内核的指令,可在《CM3 权威指南 CnR2》第四章-指令集里面查询到,具体作用见下表:
指令名称 | 做用 |
---|---|
LDR | 从存储器中加载字到一个寄存器中 |
BL | 跳转到由寄存器/标号给出的地址,并把跳转前的下条指令地址保存到 LR |
BLX | 跳转到由寄存器给出的地址,并根据寄存器的 LSE 确定处理器的状态,还要把跳转前的下条指令地址保存到 LR |
BX | 跳转到由寄存器/标号给出的地址,不用返回 |
1.6 中断服务函数
- 在启动文件里面已经帮我们写好所有中断的中断服务函数,跟我们平时写的中断服务函数不一样的就是这些函数都是空的,真正的中断复服务程序需要我们在外部的 C 文件里面重新实现,这里只是提前占了一个位置而已。
- 如果我们在使用某个外设的时候,开启了某个中断,但是又忘记编写配套的中断服务程序或者函数名写错,那当中断来临的时,程序就会跳转到启动文件预先写好的空的中断服务程序中,并且在这个空函数中无线循环,即程序就死在这里。
// Dummy Exception Handlers (infinite loops which can be modified)
// WEAK声明其他的同名标号优先于该标号被引用,就是说如果外面声明了的话,;会调用外面的
NMI_Handler PROC //系统异常
EXPORT NMI_Handler [WEAK] //不可屏蔽中断处理函数
B . //B表跳转到一个标号,"."表无限循环
ENDP
HardFault_Handler\ //意为换行
PROC
EXPORT HardFault_Handler [WEAK] //硬件错误处理函数
B .
ENDP
MemManage_Handler\
PROC
EXPORT MemManage_Handler [WEAK]
B .
ENDP
BusFault_Handler\
PROC
EXPORT BusFault_Handler [WEAK]
B .
ENDP
UsageFault_Handler\
PROC
EXPORT UsageFault_Handler [WEAK]
B .
ENDP
SVC_Handler PROC
EXPORT SVC_Handler [WEAK]
B .
ENDP
DebugMon_Handler\
PROC
EXPORT DebugMon_Handler [WEAK]
B .
ENDP
PendSV_Handler PROC
EXPORT PendSV_Handler [WEAK]
B .
ENDP
SysTick_Handler PROC
EXPORT SysTick_Handler [WEAK]
B .
ENDP
Default_Handler PROC
/*输出异常向量表标号,方便外部实现异常的具体功能,[WEAK]是弱定义的意思,如果外部定义了,优先执行外部定义,否则下面的函数定义*/
EXPORT WWDG_IRQHandler [WEAK]
EXPORT PVD_IRQHandler [WEAK]
EXPORT TAMPER_IRQHandler [WEAK]
EXPORT RTC_IRQHandler [WEAK]
EXPORT FLASH_IRQHandler [WEAK]
EXPORT RCC_IRQHandler [WEAK]
EXPORT EXTI0_IRQHandler [WEAK]
EXPORT EXTI1_IRQHandler [WEAK]
EXPORT EXTI2_IRQHandler [WEAK]
EXPORT EXTI3_IRQHandler [WEAK]
EXPORT EXTI4_IRQHandler [WEAK]
EXPORT DMA1_Channel1_IRQHandler [WEAK]
EXPORT DMA1_Channel2_IRQHandler [WEAK]
EXPORT DMA1_Channel3_IRQHandler [WEAK]
EXPORT DMA1_Channel4_IRQHandler [WEAK]
EXPORT DMA1_Channel5_IRQHandler [WEAK]
EXPORT DMA1_Channel6_IRQHandler [WEAK]
EXPORT DMA1_Channel7_IRQHandler [WEAK]
EXPORT ADC1_2_IRQHandler [WEAK]
EXPORT USB_HP_CAN1_TX_IRQHandler [WEAK]
EXPORT USB_LP_CAN1_RX0_IRQHandler [WEAK]
EXPORT CAN1_RX1_IRQHandler [WEAK]
EXPORT CAN1_SCE_IRQHandler [WEAK]
EXPORT EXTI9_5_IRQHandler [WEAK]
EXPORT TIM1_BRK_IRQHandler [WEAK]
EXPORT TIM1_UP_IRQHandler [WEAK]
EXPORT TIM1_TRG_COM_IRQHandler [WEAK]
EXPORT TIM1_CC_IRQHandler [WEAK]
EXPORT TIM2_IRQHandler [WEAK]
EXPORT TIM3_IRQHandler [WEAK]
EXPORT TIM4_IRQHandler [WEAK]
EXPORT I2C1_EV_IRQHandler [WEAK]
EXPORT I2C1_ER_IRQHandler [WEAK]
EXPORT I2C2_EV_IRQHandler [WEAK]
EXPORT I2C2_ER_IRQHandler [WEAK]
EXPORT SPI1_IRQHandler [WEAK]
EXPORT SPI2_IRQHandler [WEAK]
EXPORT USART1_IRQHandler [WEAK]
EXPORT USART2_IRQHandler [WEAK]
EXPORT USART3_IRQHandler [WEAK]
EXPORT EXTI15_10_IRQHandler [WEAK]
EXPORT RTCAlarm_IRQHandler [WEAK]
EXPORT USBWakeUp_IRQHandler [WEAK]
EXPORT TIM8_BRK_IRQHandler [WEAK]
EXPORT TIM8_UP_IRQHandler [WEAK]
EXPORT TIM8_TRG_COM_IRQHandler [WEAK]
EXPORT TIM8_CC_IRQHandler [WEAK]
EXPORT ADC3_IRQHandler [WEAK]
EXPORT FSMC_IRQHandler [WEAK]
EXPORT SDIO_IRQHandler [WEAK]
EXPORT TIM5_IRQHandler [WEAK]
EXPORT SPI3_IRQHandler [WEAK]
EXPORT UART4_IRQHandler [WEAK]
EXPORT UART5_IRQHandler [WEAK]
EXPORT TIM6_IRQHandler [WEAK]
EXPORT TIM7_IRQHandler [WEAK]
EXPORT DMA2_Channel1_IRQHandler [WEAK]
EXPORT DMA2_Channel2_IRQHandler [WEAK]
EXPORT DMA2_Channel3_IRQHandler [WEAK]
EXPORT DMA2_Channel4_5_IRQHandler [WEAK]
/*如下只是定义一些空函数 */
WWDG_IRQHandler
PVD_IRQHandler
TAMPER_IRQHandler
RTC_IRQHandler
FLASH_IRQHandler
RCC_IRQHandler
EXTI0_IRQHandler
EXTI1_IRQHandler
EXTI2_IRQHandler
EXTI3_IRQHandler
EXTI4_IRQHandler
DMA1_Channel1_IRQHandler
DMA1_Channel2_IRQHandler
DMA1_Channel3_IRQHandler
DMA1_Channel4_IRQHandler
DMA1_Channel5_IRQHandler
DMA1_Channel6_IRQHandler
DMA1_Channel7_IRQHandler
ADC1_2_IRQHandler
USB_HP_CAN1_TX_IRQHandler
USB_LP_CAN1_RX0_IRQHandler
CAN1_RX1_IRQHandler
CAN1_SCE_IRQHandler
EXTI9_5_IRQHandler
TIM1_BRK_IRQHandler
TIM1_UP_IRQHandler
TIM1_TRG_COM_IRQHandler
TIM1_CC_IRQHandler
TIM2_IRQHandler
TIM3_IRQHandler
TIM4_IRQHandler
I2C1_EV_IRQHandler
I2C1_ER_IRQHandler
I2C2_EV_IRQHandler
I2C2_ER_IRQHandler
SPI1_IRQHandler
SPI2_IRQHandler
USART1_IRQHandler
USART2_IRQHandler
USART3_IRQHandler
EXTI15_10_IRQHandler
RTCAlarm_IRQHandler
USBWakeUp_IRQHandler
TIM8_BRK_IRQHandler
TIM8_UP_IRQHandler
TIM8_TRG_COM_IRQHandler
TIM8_CC_IRQHandler
ADC3_IRQHandler
FSMC_IRQHandler
SDIO_IRQHandler
TIM5_IRQHandler
SPI3_IRQHandler
UART4_IRQHandler
UART5_IRQHandler
TIM6_IRQHandler
TIM7_IRQHandler
DMA2_Channel1_IRQHandler
DMA2_Channel2_IRQHandler
DMA2_Channel3_IRQHandler
DMA2_Channel4_5_IRQHandler
B .
ENDP
ALIGN //默认是字对齐方式,也说明了代码是4字节对齐的
- B:跳转到一个标号。这里跳转到一个‘.’,即表示无线循环。
- 真正的中断服务函数需要我们在外部的.c文件中实现,这里只是占位,是把中断和与之对应中断服务函数绑定了。当我们开启某个中断后,没有写对应的中断服务函数或者函数名有误,当中断来临时程序还是跳转到启动文件预先写好的中断服务函数中,在这个函数中无限循环。
1.7 堆栈初始化
/*******************************************************************************
User Stack and Heap initialization
*******************************************************************************/
IF :DEF:__MICROLIB //判断是否使用DEF:__MICROLIB(micro lib),如果勾选了micro lib
EXPORT __initial_sp //将栈顶地址、堆起始地址、堆结束地址赋予全局属性,使外部程序可用
EXPORT __heap_base
EXPORT __heap_limit
ELSE //如果没有勾选micro lib
IMPORT __use_two_region_memory //两区堆栈空间,堆和栈有各自的空间地址
EXPORT __user_initial_stackheap //这个函数由用户自己实现
__user_initial_stackheap //标号__user_initial_stackheap,表示用户堆栈初始化程序入口
/*此处是初始化两区的堆栈空间,堆是从由低到高的增长,栈是由高向低生长的,两个是互相独立的数据段,并不能交叉使用*/
LDR R0, = Heap_Mem //保存堆起始地址
LDR R1, =(Stack_Mem + Stack_Size) //保存栈结束地址
LDR R2, = (Heap_Mem + Heap_Size) //保存堆结束地址
LDR R3, = Stack_Mem //保存栈起始地址
BX LR
ALIGN
ENDIF
END //END命令指示汇编器,已到达一个源文件的末尾
ALIGH
- ALIGN:对指令或者数据存放的地址进行对齐,后面会跟一个立即数。缺省表示 4 字节对齐。
- 首先判断是否定义了__MICROLIB ,如果定义了这个宏则赋予标号__initial_sp(栈顶地址)、 __heap_base(堆起始地址)、 __heap_limit(堆结束地址)全局属性,可供外部文件调用。有关这个宏我们在 KEIL 里面配置,具体见下图。然后堆栈的初始化就由 C 库函数_main 来完成。
- 选中它表示使用c库的备选库,里面有一个__mian函数。若定义了则赋予标号__initial_sp(栈顶地址)、__heap_base(堆的起始地址)、__heap_limit(堆的结束地址)为外部文件可调用的变量,即可供外部c库中的__main调用,由__main初始化堆栈,否则需要用户自己实现__user_initial_stackheap函数初始化堆栈。
- 如果没有定义__MICROLIB,则插入标号__use_two_region_memory,这个函数需要用户自己实现,具体要实现成什么样,可在 KEIL 的帮助文档里面查询到,具体见下图。
- 然后声明标号__user_initial_stackheap 具有全局属性,可供外部文件调用,并实现这个标号的内容。
- IF,ELSE,ENDIF:汇编的条件分支语句,跟 C 语言的 if ,else 类似
- END:文件结束。
启动文件相当于嵌入式Linux中的BootLoader,只不过这里简单多了。感觉大概了解就可以,需要深究时再深究。
2 STM32 中断向量表的位置 、重定向
知道怎么跳到main函数了,那么,中断发生后,又是怎么跑到中断入口地址的呢?
从stm32f10x.s可以看到,已经定义好了一大堆的中断响应函数,这就是中断向量表,标号__Vectors,表示中断向量表入口地址,例如:AREA RESET, DATA, READONLY ; 定义只读数据段,实际上是在CODE区(假设STM32从FLASH启动,则此中断向量表起始地址即为0x8000000)
EXPORT __Vectors
OS_CPU_SysTickHandler
IMPORT OS_CPU_PendSVHandler
rs DCD __initial_sp // Top of Stack
DCD Reset_Handler // Reset Handler
DCD NMI_Handler // NMI Handler
DCD HardFault_Handler // Hard Fault Handler
DCD MemManage_Handler // MPU Fault Handler
DCD BusFault_Handler // Bus Fault Handler
DCD UsageFault_Handler // Usage Fault Handler
这个向量表的编写是有讲究的,跟硬件一一对应不能乱写的,CPU找入口地址就靠它了,bin文件开头就是他们的地址,参考手册RM0008的10.1.2节可以看到排列。
我们再结合CORTEX-M3的特性,他上电后根据boot引脚来决定PC位置,比如boot设置为flash启动,则启动后PC跳到0x08000000。此时CPU会先取2个地址,第一个是栈顶地址,第二个是复位异常地址,故有了上面的写法,这样就跳到reset_handler。那么这个reset_handler的实际地址是多少.?下面的一堆例如Nmi_handler地址又是多少呢?发生中断是怎么跑到这个地址的呢?下面挨个讲解。
1:我们可以通过反向来得知这些入口地址,查看工程下的map文件就可以看到了,这个地址跟keil里面设置的target->flash起始地址息息相关,实际上我们不太需要关心,让编译器分配,中断向量表放的就是他们的地址。
2:对比ARM7/ARM9内核,Cortex-M3内核则是固定了中断向量表的位置而起始地址是可变化的。
3:进到C语言后会先配置NVIC,NVIC_SetVectorTable()里面可以配置中断向量表的起始地址和偏移,主要是告诉CPU该向量表是位于Flash还是Ram,偏移是多少。例如设置为位于Flash内,偏移就是烧入的程序地址,可在Keil target中设置。这样CPU就知道入口地址了。
4:发生中断后,CPU找到中断向量表地址,然后根据偏移(对号入座)再找到中断地址,这样就跳过去了。
我们截一个图说明一下,map文件:
对应的bin文件,看是不是放的上面地址:
显然,200039c0就是栈顶地址,而08006F21就是reset_handler地址!
如何定位?以放到0x20000000为例
1:keil设置ram起始为0x20000100,我们在0x20000000~0x20000100放中断向量表,其他给程序用
2:设置NVIC_SetVectorTable(NVIC_VectTab_FLASH,0);
3:跳到C时把中断向量表拷贝到0x20000000
3 个人思考
3.1 在拿到ST公司官方的IAP 程序后 我们要思考几点:
1.ST 官方IAP是什么针对什么芯片型号的?我们要用的又是什么芯片型号?
2.我们要用官方IAP适合我们芯片的程序升级使用,要在原有的基础上做那些改变?
初略看了一下IAP源码后,现在我们可以回答一下上面的2个问题了:
1.官网刚下载的IAP针对的是stm32f103c8芯片的,所以他的启动代码文件选择的是 startup_stm32f10x_md.s,而我的芯片是stm32f100cb,所以我的启动代码文件选择的是 startup_stm32f10x_md_lv.s
2 .第二个问题就是今天我们要做详细分析才能回答的问题了。
(1).知道了IAP官方源码的芯片和我们要用芯片的差异,首先我们要在源码的基础上做芯片级的改动;
A.首先改变编译器keil的芯片型号上我们要改成我们的芯片类型---STM32F100CB;
B.在keil的options for targer 选项C/C++/PREPROMCESSOR symbols的Define栏里定义,把有关STM32F10X_MD的宏定义改成:STM32F10X_MD_VL
也可以在STM32F10X.H里用宏定义:
/* Uncomment the line below according to the target STM32 device used in your application */
#if !defined (STM32F10X_LD) && !defined (STM32F10X_LD_VL) && !defined (STM32F10X_MD) && !defined (STM32F10X_MD_VL) && !defined (STM32F10X_HD) && !defined (STM32F10X_HD_VL) && !defined (STM32F10X_XL) && !defined (STM32F10X_CL)
/* #define STM32F10X_LD */ /*!< STM32F10X_LD: STM32 Low density devices */
/* #define STM32F10X_LD_VL */ /*!< STM32F10X_LD_VL: STM32 Low density Value Line devices */
/* #define STM32F10X_MD */ /*!< STM32F10X_MD: STM32 Medium density devices */
#define STM32F10X_MD_VL /*!< STM32F10X_MD_VL: STM32 Medium density Value Line devices */
/* #define STM32F10X_HD */ /*!< STM32F10X_HD: STM32 High density devices */
/* #define STM32F10X_HD_VL */ /*!< STM32F10X_HD_VL: STM32 High density value line devices */
/* #define STM32F10X_XL */ /*!< STM32F10X_XL: STM32 XL-density devices */
/* #define STM32F10X_CL */ /*!< STM32F10X_CL: STM32 Connectivity line devices */
#endif
上面代码说的是如果没有定义 STM32F10X_MD_VL, 则宏定义 STM32F10X_MD_VL
C.外部时钟在stm32f10x.h 依据实际修改,原文是说如果没有宏定义外部时钟HES_VALUE的值,但是宏定义了stm32f10x_cl 则外部时钟设置为25MHZ, 否则外部时钟都设置为8MHZ; 我用的外部晶振是8MHZ的所以不必修改这部分代码;
#if !defined HSE_VALUE
#ifdef STM32F10X_CL
#define HSE_VALUE ((uint32_t)25000000) /*!< Value of the External oscillator in Hz */
#else
#define HSE_VALUE ((uint32_t)8000000) /*!< Value of the External oscillator in Hz */
#endif /* STM32F10X_CL */
#endif /* HSE_VALUE */
D.做系统主频时钟的更改
system_stm32f10x.c的系统主频率,依实际情况修改 ;我用的芯片主频时钟是24MHZ;
#if defined (STM32F10X_LD_VL) || (defined STM32F10X_MD_VL) || (defined STM32F10X_HD_VL)
/* #define SYSCLK_FREQ_HSE HSE_VALUE */
#define SYSCLK_FREQ_24MHz 24000000
#else
/* #define SYSCLK_FREQ_HSE HSE_VALUE */
#define SYSCLK_FREQ_24MHz 24000000
/* #define SYSCLK_FREQ_36MHz 36000000 */
/* #define SYSCLK_FREQ_48MHz 48000000 */
/* #define SYSCLK_FREQ_56MHz 56000000 */
/*#define SYSCLK_FREQ_72MHz 72000000*/
#endif
E.下面是关键部分操作了,在说这部分操作前我们先来说一下内存映射:
下图在stm32f103ZEXX芯片手册的38页,我们只截取关键部分
从上图我们看出几个关键部分:
1.内部flash 是从0x0800 0000开始 到0x0801 FFFF结束,0x0801FFFF-0x0800 0000= 0x20000 =128k,128k也就是flash的大小;
2.SRAM的开始地址是0x2000 0000 ;
我们要把我们的在线升级程序IAP放到FLASH里以0x0800 0000 开始的位置,应用程序放APP放到以0x08003000开始的位置,中断向量表也放在0x0800 3000开始的位置;如图
所以我们需要先查看一下misc.h 文件中的中断项量表的初始位置宏定义 为NVIC_VectTab_Flash 0x0800000
那么要就要设置编译器keil 中的 options for target 的target选项中的 IROM1地址 为0x0800 0000 大小为 0x20000即128K;IRAM1地址为0x2000 0000 大小为0x2000;(提示:这一项IROM1 地址 即为当前程序下载到flash的地址的起始位置)
下面我们来分析一下修改后的IAP代码:
/*******************************************************************************
* @函数名称 main
* @函数说明 主函数
* @输入参数
* @输出参数
* @返回参数
*******************************************************************************/
int main(void){ //Flash 解锁 FLASH_Unlock();
KEY_Configuration() ;
//配置串口1
IAP_Init();
//PA15管脚是否为低电平
if (GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_15) == 0x00) {
//执行IAP驱动程序更新Flash程序
SerialPutString("\r\n======================================================================");
SerialPutString("\r\n= (C) COPYRIGHT 2011 Lierda =");
SerialPutString("\r\n= =");
SerialPutString("\r\n= In-Application Programming Application (Version 1.0.0) =");
SerialPutString("\r\n= =");
SerialPutString("\r\n= By wuguoyan =");
SerialPutString("\r\n=========================================================");
SerialPutString("\r\n\r\n"); Main_Menu (); }
//执行用户程序
else {
//判断用户已经下载程序,因为正常情况下此地址是栈地址
//如没有这一句话,即使没有下载程序也会进入而导致跑飞
if (((*(__IO uint32_t*)ApplicationAddress) & 0x2FFE0000 ) == 0x20000000)
{
SerialPutString("Execute user Program\r\n\n");
//跳转至用户代码
JumpAddress = *(__IO uint32_t*) (ApplicationAddress + 4);
Jump_To_Application = (pFunction) JumpAddress;
//初始化用户程序指针的堆栈指针
__set_MSP(*(__IO uint32_t*) ApplicationAddress);
Jump_To_Application(); }
else
{ SerialPutString("no user Program\r\n\n"); } }
while (1)
{ }}
这里重点说一下几句经典且非常重要的代码:
第一句:if (((*(__IO uint32_t*)ApplicationAddress) & 0x2FFE0000 ) == 0x20000000) //判断栈定地址值是否在0x2000 0000 - 0x 2000 2000之间
怎么理解呢?在程序里#define ApplicationAddress 0x8003000 ,*(__IO uint32_t*)ApplicationAddress) 即取0x8003000开始到0x8003003 的4个字节的值, 因为我们的应用程序APP中设置把 中断向量表 放置在0x08003000 开始的位置;而中断向量表里第一个放的就是栈顶地址的值。也就是说,这句话即通过判断栈顶地址值是否正确(是否在0x2000 0000 - 0x 2000 2000之间) 来判断是否应用程序已经下载了。
第二句: JumpAddress = *(__IO uint32_t*) (ApplicationAddress + 4); [ common.c文件第18行定义了: pFunction Jump_To_Application;]
ApplicationAddress + 4 即为0x0800 3004 ,里面放的是中断向量表的第二项“复位地址” JumpAddress = *(__IO uint32_t*) (ApplicationAddress + 4); 之后此时JumpAddress
第三句: Jump_To_Application = (pFunction) JumpAddress;
startup_stm32f10x_md_lv. 文件中别名 typedef void (*pFunction)(void); 这个看上去有点奇怪;正常第一个整型变量 typedef int a; 就是给整型定义一个别名 a void (*pFunction)(void); 是声明一个函数指针,加上一个typedef 之后 pFunction只不过是类型 void (*)(void) 的一个别名;例如:pFunction a1,a2,a3;
void fun(void)
{
......
}
a1 = fun;
所以,Jump_To_Application = (pFunction) JumpAddress; 此时Jump_To_Application指向了复位函数所在的地址;
第四 、五句: __set_MSP(*(__IO uint32_t*) ApplicationAddress); \\设置主函数栈指针
Jump_To_Application(); \\执行复位函数
我们看一下启动文件startup_stm32f10x_md_vl。s 中的启动代码,更容易理解
3.2 解析STM32的启动过程
当前的嵌入式应用程序开发过程里,并且C语言成为了绝大部分场合的最佳选择。如此一来main函数似乎成为了理所当然的起点——因为C程序往往从main函数开始执行。但一个经常会被忽略的问题是:微控制器(单片机)上电后,是如何寻找到并执行main函数的呢?很显然微控制器无法从硬件上定位main函数的入口地址,因为使用C语言作为开发语言后,变量/函数的地址便由编译器在编译时自行分配,这样一来main函数的入口地址在微控制器的内部存储空间中不再是绝对不变的。相信读者都可以回答这个问题,答案也许大同小异,但肯定都有个关键词,叫“启动文件”,用英文单词来描述是“Bootloader”。
无论性能高下,结构简繁,价格贵贱,每一种微控制器(处理器)都必须有启动文件,启动文件的作用便是负责执行微控制器从“复位”到“开始执行main函数”中间这段时间(称为启动过程)所必须进行的工作。最为常见的51,AVR或MSP430等微控制器当然也有对应启动文件,但开发环境往往自动完整地提供了这个启动文件,不需要开发人员再行干预启动过程,只需要从 main函数开始进行应用程序的设计即可。
话题转到STM32微控制器,无论是keil uvision4还是IAR EWARM开发环境,ST公司都提供了现成的直接可用的启动文件,程序开发人员可以直接引用启动文件后直接进行C应用程序的开发。这样能大大减小开发人员从其它微控制器平台跳转至STM32平台,也降低了适应STM32微控制器的难度(对于上一代ARM的当家花旦ARM9,启动文件往往是第一道难啃却又无法逾越的坎)。
相对于ARM上一代的主流ARM7/ARM9内核架构,新一代Cortex内核架构的启动方式有了比较大的变化。ARM7/ARM9内核的控制器在复位后,CPU会从存储空间的绝对地址0x000000取出第一条指令执行复位中断服务程序的方式启动,即固定了复位后的起始地址为0x000000(PC = 0x000000)同时中断向量表的位置并不是固定的。而Cortex-M3内核则正好相反,有3种情况:
1:通过boot引脚设置可以将中断向量表定位于SRAM区,即起始地址为0x2000000,同时复位后PC指针位于0x2000000处;
2:通过boot引脚设置可以将中断向量表定位于FLASH区,即起始地址为0x8000000,同时复位后PC指针位于0x8000000处;
3:通过boot引脚设置可以将中断向量表定位于内置Bootloader区,本文不对这种情况做论述;
而Cortex-M3内核规定,起始地址必须存放堆顶指针,而第二个地址则必须存放复位中断入口向量地址,这样在Cortex-M3内核复位后,会自动从起始地址的下一个32位空间取出复位中断入口向量,跳转执行复位中断服务程序。对比ARM7/ARM9内核,Cortex-M3内核则是固定了中断向量表的位置而起始地址是可变化的。
stm32的启动过程
(1)没有IAP,只有APP时的正常启动流程:
STM32的FLASH地址起始于0x08000000,程序文件就从此地址开始写入。此外STM32内部通过“中断向量表”来响应中断,程序启动后,将首先从“中断向量表”取出复位中断向量执行复位中断程序完成启动,而“中断向量表”的起始地址是0x08000004,当中断来临,STM32的内部硬件机制亦会自动将PC指针定位到“中断向量表”处,并根据中断源取出对应的中断向量执行中断服务程序。
根据上图分析启动和运行过程,
① STM32在复位后,先从0X08000004地址取出复位中断向量的地址,并跳转到复位中断服务程序,
② 在复位中断服务程序执行完之后,会跳转到的main函数(如使用KEIL MDK调试时一下载进程序,会发现需要运行几次下一步才会跳转到main函数的位置)
③ main函数一般都是超循环体(while(1)死循环),在main函数执行过程中,如果收到中断请求(发生重中断),此时STM32强制将PC指针指回中断向量表处
④ 根据中断源进入相应的中断服务程序,
⑤ 在执行完中断服务程序以后,程序再次返回main函数执行。
(2)加入IAP后的启动流程
根据上图分析加入IAP后的起动和运行过程
① STM32复位后,还是从0X08000004地址取出复位中断向量的地址,并跳转到复位中断服务程序,在运行完复位中断服务程序之后跳转到IAP的main函数,如将IAP看作是一个APP的话,那么此部分和正常起动是一样的。(此步=执行复位中断服务程序+跳转main,即将正常运行的①和②合并了)。
② 在执行完IAP以后(固件升级或直接跳转),跳转至APP的复位向量表(APP的复位中断向量起始地址为0X08000004+N+M)。
③ 取出APP的复位中断向量的地址,并跳转执行新程序的复位中断服务程序,随后跳转至APP的main函数(此步=执行复位中断服务程序+跳转main)
④ 同样main函数为一个超循环,并且注意到此时STM32的FLASH,在不同位置上,共有两个中断向量表。在main函数执行过程中,如果CPU得到一个中断请求,PC指针仍强制跳转到地址0X08000004中断向量表处,而不是APP程序的中断向量表。
⑤ 程序再根据我们设置的中断向量表偏移量,跳转到对应中断源的APP的中断服务程序中,
⑥ 在执行完中断服务程序后,程序返回main函数继续运行。