STM32 启动代码分析详解

发布者:bobojrt最新更新时间:2019-04-01 来源: eefocus关键字:STM32  启动代码 手机看文章 扫描二维码
随时随地手机看文章

1、堆栈存储器


堆栈存储区是在片上存储器中的SRAM(或RAM)中由用户自行开辟的一片数据存储区域,并且堆栈区的大小可根据用户的需要任意指定(只要不超过SRAM或RAM的大小),而堆栈区的位置由编译器指定分配。

 

Cortex-M3/M4处理器的堆栈指针SP是“满递减,空递增”,呈现向下逆生长的特点。 


堆栈区数据的存储特点是“先进后出,后进先出”。


这种特点是由堆栈指针的移动方式决定的,先入栈的数据对应的指针值比较大,后入栈的数据对应的指针值比较小,而出栈时堆栈指针的值是递增的,所以指针值大的数据当然后出栈。


堆栈的作用:


局部变量的存储、函数调用时函数或子程序间数据的传递(形参的保存,其实函数调用一旦结束被调函数中定义的所有局部变量单元就会被释放)、函数调用时现场数据的保存、中断发生时现场数据的保存。


2、启动代码作用


C 程序的的执行是从主函数main()开始的。


可是微控制器上电后,是如何寻找main()函数的呢?


显然,我们不可能从硬件上来寻找到main()函数的入口地址。


实际上,main()函数的入口地址是编译器在编译过程中分配的。


并且,从微控制器上电到main()函数执行前,微控制器有个启动的过程,这个启动过程正是启动代码执行的过程,时间非常短暂。


启动代码的作用: 


1)初始化堆栈指针 SP == _initial_sp; 


2)初始PC指针 ==Reset_Handler(复位处理程序); 


3)初始化中断向量表; 


4)配置系统时钟; 


5)调用C库_main()函数初始化用户堆栈,从而最终调用main()主函数去到C世界。


3、编写启动代码


一般标准的CMSIS启动代码都是汇编语言的,用C来编写启动代码之前,我们需要先构思一下C能不能用来编写启动代码?


a)正确的在存储空间中摆放异常&中断向量表;


b)分散加载&C Library的初始化(调用 __main函数);


c)堆栈初始化。


其中 a)功能在标准的汇编启动代码中是通过声明一个数据段(就是汇编指令DCD xxx)来实现的,这个我们在C中可以用数组来做,关键是要摆放到指定的位置上,这点需要用分散加载来配合,所以 a)应该可以实现;


其中 b)最好实现,直接调用__main就好了;


c),虽然通过读标准的CMSIS汇编启动代码也可以知道堆栈是怎样初始化的。


以LPC54608为例,早期的芯片会有不同的处理方式,比如大家都很熟悉的STM32F103系列,是需要启动代码与分散加载配合完成的。


3.1、异常&中断向量表


之前我们说这个可以用数组来实现,普通数组肯定不行,因为这些向量的本质是中断服务函数的入口,也就是“函数指针”所以这个数组必须得是函数指针数组:


所以我们先声明一个函数指针类型:


typedef void ( *__vector )( void );


然后定义一个函数指针型数组取名为__vector_table:


__vector __vector_table[] = {};

    这个数组好做,但是最重要的是摆放问题,必须确保这个向量表被摆放到Flash的0x00000000地址上去(在LPC54608这颗芯片中默认的向量表地址),这个才是问题的关键。


C与汇编是无法解决这个问题的,管程序在存储空间内摆放的是编译器开发环境中的链接器,指导链接器工作的是分散加载的脚本文件,这里需要分散加载的介入。 


比如大家常用的开发MCU的各种开发环境(Keil、IAR、Eclipse……等)其中真正起到编译作用的就是编译内核,Keil的编译内核是ARMCC(有时写成CCARM)、IAR编译内核是ICCARM、Eclipse一般大多是GCC,编译内核实际上是分成4个功能模块:


预处理器(Preprocessor)、编译器(Compiler)、汇编器(Assembler)、链接器(Linker),预处理器负责把各种头文件与对应的C文件结合在一起,负责删除注释、展开宏定义等工作;


编译器负责把C语言转换为对应的汇编语言;


汇编器负责把汇编语言转换为可重定位的目标文件*.o,这个就已经是二进制文件了;


链接器负责把所有.o文件链接成一个整体并分配真实的物理地址(就是上文反反复复提到的分散加载)。


如何摆放异常&中断向量表呢?


这里以Keil MDK为例(IAR、GCC在这部分语法不同,不过原理相同),指导链接器如何工作有一个专门的描述脚本文件,在Keil中此文件以.sct/.scf为扩展名,在这里可以找到分散加载描述文件:


这里直接列出来我针对自己写的启动代码编写的分散加载文件:


load_rom 0x00000000 0x00080000

{

    vector_rom 0x00000000 0x400

    {

        *( vector_table, +first)        

    }

    

    execute_rom 0x400 FIXED 0x0007FC00

    {

        *( InRoot$$Sections )

        .any( +ro )

    }

    

    execute_data 0x20000000 0x00010000   

    {

        .any ( +rw +zi )

    }

    

    ARM_LIB_HEAP  +0 empty 0x400 {}

    ARM_LIB_STACK 0x20020000  empty -400 {}

}

其中用*( vector_table, +first)中的+first来确保项链表被放置到Flash地址的最前端,这里需要注意的是,分散加载文件中的语言是脚本语言,既不是C也并非汇编,这个文件无法编译更无法调试,如果写错了,只能通过经验来修改不能DEBUG。


我们要放置一个数组,那么我们需要声明一个数据段Symbol,然后用分散加载来指定这个数据段Symbol的放置方式也就是+first。所以我们要先让这个数组生成一个段:


const __vector __vector_table[] __attribute__( ( section ( "vector_table" ), used ) ) = {};


于是我们使用__attribute__来实现。


堆栈的指定我们在分散加载中完成,即:


ARM_LIB_HEAP  +0 empty 0x400 {}

ARM_LIB_STACK 0x20020000  empty -400 {}

这两句话的意思就是声明一个大小为0x400的堆和一个大小为0x400的栈,注意堆和栈是不同的,简单来说函数的调用会占用栈空间,而使用malloc这样的函数则会产生堆分配,具体细节大家自己去百度。


*( InRoot$$Sections )


这部分指定了C Library以及分散加载部分代码的放置;


总之,这个分散加载文件最终把代码在存储空间内放置成了类似下图的形态:

 

__attribute__关键字用于指定函数、变量等Symbol的属性,是编译器可以识别的C语言关键字,当然这个要看使用的是什么编译器,不同的C编译器语法上略有差别,有的C编译器甚至还不支持__attribute__关键字,以下是Keil的帮助文档里面的关于__attribute__关键字可实现的功能:


__attribute__( ( section ( "vector_table" ), used ) ),其中的“section("vector_table")”意思是在elf(可重定位的目标文件)文件中放置一个名字为vector_table的section。used关键字指定了编译器该变量要在OBJ文件中保持为static类型。


再配合分散加载的*( vector_table, +first)的+first属性我们就成功的定义了一个放置在Flash最前端地址0x00000000地址上并且不会被编译器优化掉,数值不会被改掉,名字为vector_table的向量数据段。


这个是编译器识别的特殊栈标号,可以在分散加载或者汇编或者C语言中使用,0x00000000地址(向量表首地址)放置主堆栈指针MSP,0x00000004地址放置PC指针,所以我们先导入这个标号:


0x00000004放置PC指针,我们需要把复位向量放到此处:


3.2、__reset_handler复位函数


这个比较简单,最重要的是调用__main,有一些初始化要放到__main之前的,LPC54608的FPU以及一些SRAM的开关要放到__main之前;


首先导入__main函数:


extern void __main( void );


然后是__reset_handle的函数体:


void __reset_handler ( void )

{

#if ( ( __FPU_PRESENT == 1 ) && ( __FPU_USED == 1 ) )

    SCB->CPACR |= ( ( 3UL << 10 * 2 ) | ( 3UL << 11 * 2 ) );       // set CP10, CP11 Full Access 

#endif  

    SCB->VTOR = ( uint32_t ) 0x00000000;

    SYSCON->ARMTRACECLKDIV = 0;

    SYSCON->AHBCLKCTRLSET[ 0 ] = SYSCON_AHBCLKCTRL_SRAM1_MASK | SYSCON_AHBCLKCTRL_SRAM2_MASK | SYSCON_AHBCLKCTRL_SRAM3_MASK;

    

    __main();

}

因为是C语言的,在__main之前加入自己需要的函数体部分会更容易。


3.3、默认的异常&中断处理函数


以其中一个向量为例:(USB0中断)


函数体部分:


void usb0_irqhandler              ( void )                 

{

    while( 1 );

}   

然后再前面声明为“弱函数”,依然使用__attribute__来指定其属性为“weak”:


extern void usb0_irqhandler          ( void ) __attribute__( ( weak ) );    

    如果你不写中断服务函数,但程序却进了这个中断,程序就会进入默认的中断服务函数,如果你自己写了一个,你自己的中断服务函数会覆盖被声明为“weak”的函数,当然前提是函数名一致。


关键字:STM32  启动代码 引用地址:STM32 启动代码分析详解

上一篇:STM32 字节对齐 #pragma pack
下一篇:STM32 启动代码汇编指令详解

推荐阅读最新更新时间:2024-03-16 16:26

stm32 id加密破解方法分享 STM32系列ID号加密解密方法
STM32系列ID号加密解密方法 STM32系列单片机通常会遇到ID号软加密,一般行外认识是不清楚如何去除软加密的,此次分享,是告诉您,如何判断是否有软加密及软加密去除方法。 具体方法如下: 1、读出完整程序段,搜索关键字节“E8F7FF1F”,因为STM32唯一的96位(12字节)ID基地址位于0x1FFF7E8,若程序段中涉及ID校验必然会读取芯片中ID索引该地址。。 2、用工具读取此芯片该地址的96位(12字节)ID码,找到空片区写入此地址,更改索引地址:例如此芯片ID码为“34FFDA 054E5038 31 19651843”选取起始地址区域0x80000020,在前四个字节输入 34 FF1843 ,后12字节输入
[单片机]
STM32 IO口的八种方式的解释
1、上拉输入:上拉就是把电位拉高,比如拉到Vcc。上拉就是将不确定的信号通过一个电阻嵌位在高电平!电阻同时起限流作用!弱强只是上拉电阻的阻值不同,没有什么严格区分。 2、下拉输入:就是把电压拉低,拉到GND。与上拉原理相似。 3、浮空输入:浮空(floating)就是逻辑器件的输入引脚即不接高电平,也不接低电平。由于逻辑器件的内部结构,当它输入引脚悬空时,相当于该引脚接了高电平。一般实际运用时,引脚不建议悬空,易受干扰。 通俗讲就是让管脚什么都不接,浮空着。 4、模拟输入:模拟输入是指传统方式的输入.数字输入是输入PCM数字信号,即0,1的二进制数字信号,通过数模转换,转换成模拟信号,经前级放大进入功率放大器,功率放大器还是模拟的
[单片机]
33. 外部SRAM实验
STM32F103ZET6 自带了 64K 字节的 SRAM,对一般应用来说,已经足够了,不过在一些对内存要求高的场合,STM32 自带的这些内存就不够用了。比如跑算法或者跑 GUI 等,就可能不太够用,所以战舰 STM32 开发板板载了一颗 1M 字节容量的 SRAM 芯片: IS62WV51216, 满足大内存使用的需求。 本实验采用IS62WV51216 芯片,访问速度为55ns。 一。IS62WV51216简介 兼容TTL电平就是兼容5V,支持高/低字节控制,可以访问8位的字节。 本次实验选择的封装没有CS2引脚,只有CS1,为低电平片选。 LB和UB为低电平有效,比如如果LB==0,说明I/O 0
[单片机]
33. 外部SRAM实验
STM32-快速上手ADC
1. ADC端口映射 ADC肯定要外部输入信号的,通过IO口进行采集,IO与ADC通道的对应关系如下: 2. 配置 2.1 配置步骤 开启需要用到的ADCx时钟和对应的IO时钟 ADC模块和GPIO模块都在APB2时钟下 void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState); 配置ADCx预分频器分频系数 void RCC_ADCCLKConfig(uint32_t RCC_PCLK2); 配置需要用到的对应IO设置为模拟输入 void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTyp
[单片机]
STM32-快速上手ADC
新手入门使用STM32函数库之外部中断
啥也不说,先看看我的外星人开发板上的按键原理图。 板子偷懒,或者叫充分利用IO口得上拉功能,这边没有加常见的上拉电阻。到时候编程的时候使能IO的上拉就行了~看下面的接口知道了KEY0接到了STM32的PA13上! 曾经CZZ在梦里和我说过,STM32的任何一个IO都能作为外部中断输入,哇塞,超级强大! 参考一般的程序步骤如下: 1、系统初始化,如系统时钟初始化,使之进入72MHZ 主频; 2、GPIO 配置,务必注意打开GPIO 时钟时,一定打开AFIO 时钟。 3、EXTI 配置,在这里配置需要选择哪个引脚作为中断引脚。 4、NVIC 配置,这也是比单
[单片机]
新手入门使用<font color='red'>STM32</font>函数库之外部中断
STM32重难点-IIC原理及应用详细步骤
IIC概述 IIC介绍 I2C(IIC,Inter-Integrated Circuit),两线式串行总线,由PHILIPS公司开发用于连接微控制器及其外围设备。它是由数据线SDA和时钟SCL构成的串行总线,可发送和接收数据。在CPU与被控IC之间、IC与IC之间进行双向传送,高速IIC总线一般可达400kbps以上。是半双工通信方式。 IIC特性 相对于UART,IIC的优点在于可以一对多,缺点在于无论主从器件均不对消息进行确认。 模拟IIC优点是可以任意选择SDA和SCL,不受管脚限制而比较灵活。它的缺点是不可用DMA。 硬件IIC优点是可用DMA减轻CPU负担,速度也比模拟IIC快,但是实际调试时可能会出现死
[单片机]
<font color='red'>STM32</font>重难点-IIC原理及应用详细步骤
STM32之timer2的精准延时
一、简介 本文介绍STM32系列如何使用timer2进行精确定时。 二、实验平台 库版本:STM32F10x_StdPeriph_Lib_V3.5.0 编译软件:MDK4.53 硬件平台:STM32开发板(主芯片stm32f103c8t6) 仿真器:JLINK 三、版权声明 四、实验前提 1、在进行本文步骤前,请先阅读以下博文: 暂无 2、在进行本文步骤前,请先实现以下博文: 暂无 五、基础知识 暂无 六、实验步骤 1、编写并添加定时器2驱动 1)编写驱动GUA_Timer2.c(存放在“……HARDWARE”) //***************************
[单片机]
<font color='red'>STM32</font>之timer2的精准延时
工程师STM32单片机学习基础手记(4):用PWM实现荧火虫灯(四)
补充一些硬件知识      SEGGER 给出的Jlink引脚图      开发板上的连接图      标准的JTAG连接图,供对照参考。   调试方式既可以用JTAG,也可以用SW。   以下是转载:   SWD 仿真模式概念简述   先所说 SWD 和传统的调试方式有什么不一样:   首先给大家介绍下经验之谈:   (一): SWD 模式比 JTAG 在高速模式下面更加可靠。 在大数据量的情况下面 JTAG 下载程序会失败, 但是 SWD 发生的几率会小很多。 基本使用 JTAG 仿真模式的情况下是可以直接使用 SWD 模式的, 只要你的仿真器支持。 所以推荐大家使用这个模式。   (二): 在大家
[模拟电子]
工程师<font color='red'>STM32</font>单片机学习基础手记(4):用PWM实现荧火虫灯(四)
小广播
添点儿料...
无论热点新闻、行业分析、技术干货……
设计资源 培训 开发板 精华推荐

最新单片机文章
何立民专栏 单片机及嵌入式宝典

北京航空航天大学教授,20余年来致力于单片机与嵌入式系统推广工作。

换一换 更多 相关热搜器件
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2024 EEWORLD.com.cn, Inc. All rights reserved