历史上的今天
返回首页

历史上的今天

今天是:2025年04月01日(星期二)

正在发生

2019年04月01日 | STM32 启动代码分析详解

2019-04-01 来源:eefocus

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”的函数,当然前提是函数名一致。


推荐阅读

史海拾趣

贝特莱公司的发展小趣事

在市场竞争日益激烈的背景下,贝特莱注重品牌建设和市场推广。通过不断提升产品质量和创新能力,优化售后服务体系,贝特莱在市场上树立了良好的品牌形象。同时,公司还积极拓展海外市场,参与国际竞争,进一步提升了其在全球电子行业的影响力。

这些故事只是贝特莱在电子行业发展历程中的一部分。作为一个国家级高新技术企业,贝特莱在技术研发、产品创新、市场拓展等方面都取得了显著的成绩。未来,随着电子行业的不断发展,贝特莱将继续保持其创新精神和市场竞争力,为行业的进步做出更大的贡献。

DIALIGHT公司的发展小趣事

随着业务的不断发展和市场的不断扩大,DIALIGHT公司逐渐实现了全球化布局。他们在美国、英国、丹麦、德国、马来西亚、新加坡、澳大利亚、墨西哥和巴西等地设立了分支机构或办事处,为全球客户提供及时、高效的服务。此外,DIALIGHT还积极寻求与其他企业的合作与共赢,通过战略合作协议的签署,为双方带来新的业务机遇和合作空间。这些举措不仅增强了DIALIGHT的市场竞争力,也为其未来的发展奠定了坚实的基础。

请注意,由于篇幅限制,以上三个故事仅为DIALIGHT公司发展历程的简要概述。如需更详细的信息和更多故事,建议查阅相关资料或访问公司官网。

FDI [Future Designs , Inc.]公司的发展小趣事

在电子行业的早期,FDI(Future Designs, Inc.)还只是一家名不见经传的小公司。创始人李明,凭借其对电子技术的热爱和敏锐的商业洞察力,带领团队研发出了一款具有革命性的电子芯片。这款芯片不仅性能卓越,而且成本较低,迅速在市场上获得了认可。正是这款产品,为FDI公司赢得了第一桶金,也为公司后续的发展奠定了坚实的基础。

科山芯创(COSINE)公司的发展小趣事

科山芯创深知品质是企业的生命线。他们建立了严格的质量管理体系,从原材料采购到生产流程再到成品检测,每一个环节都严格把关。同时,他们还不断提升服务水平,为客户提供全方位的技术支持和解决方案。这些努力使得科山芯创在客户中赢得了良好的口碑和信任。

City_Technology公司的发展小趣事

随着市场的不断变化和需求的日益增长,City Technology并未满足于已有的成就。在1980年代末至1990年代初,公司积极扩展产品线,相继开发了电化学有毒气体传感器和可燃气体传感器。这些新产品的推出,不仅丰富了公司的产品线,也进一步巩固了其在气体传感领域的领先地位。同时,公司还不断对已有产品进行优化升级,以满足客户日益多样化的需求。

CDIL[Continental Device India Pvt. Ltd.]公司的发展小趣事

自1964年起,CDIL便踏上了半导体制造的先驱之路。当时,电子产业正处于蓬勃发展的初期,CDIL凭借对技术的敏锐洞察和不懈追求,迅速成为印度乃至全球半导体制造领域的佼佼者。其硅芯片和器件的制造质量和技术水平均达到了世界级标准,为印度电子产业的发展奠定了坚实基础。

问答坊 | AI 解惑

更新运动系统的方法

为了在激烈的竞争中取胜,生产商们必须想方设法制造出比上一代产品或市场上的竞争产品速度更快、体积更小、运行更精确、噪音更小、成本更低的机器。同时,随着机器老化、花费增加和技术进步,运动系统的更新换代也成为必需。无论什么原因,技术变革 ...…

查看全部问答>

求助:锁相环的作用

这是我在网上查到的信息: 最简单的解释:通过一个环路(反馈、比较)把频率(相位)锁定的电路。 我看到PIC2520单片机上datasheet上写的:\"4 倍频锁相环(可用于晶振和内部振荡器)\"假设晶振为11KHZ的振荡频率,那么单片机的时钟频率是多少? ...…

查看全部问答>

130万像素CMOS数码相机的设计

本文介绍了一种130万像素CMOS数码相机的设计原理、基本组成及规格特点,并已有具体实践的结果和产品应用。 …

查看全部问答>

急!!~~大家帮我看看啊,错了很久了

我用的飞思卡尔的IMX21的板子,现在要移植2.6的内核上去,但是编译的时候在这里一直通不过,请大家帮忙看看,谢谢了 CHK include/linux/compile.h AS arch/arm/kernel/head.o arch/arm/kernel/head.S: Assembler messages: arch/arm/kernel/hea ...…

查看全部问答>

怎么让1602LCD上的字闪烁?

我知道用         Write_com(0x08);                                          delay(3000); ...…

查看全部问答>

uclinux下实现usb固件程序!来者有分!!哪位想帮小弟这个忙,可以谈谈报酬!呵呵

我的S3C44B0带有个USBD12芯片, 1,在没有操作系统uclinux情况下,我写了个固件程序,实现了usb device,注意不是usb host。 2,我又在这块板子上成功移植了uclinux系统。 问题,现在我想把固件程序放到uclinux里面,有人说因为用到中断,得做成 ...…

查看全部问答>

仿真没问题,一旦拔下JATG,程序不运行或运行错,急,急,急呀!

我用的是 MSP430F135芯片,程序仿真一切正常,build后,可直接写道芯片去。一旦拔下JATG,程序不运行或运行错。我猜测是程序复位问题,即是程序没找到起始地址,或找错了。如何才能让它正常运行呢,急呀,  ,快过年了,还没有回家呢!! ...…

查看全部问答>

急,读不到P1口的输出数据,请高手指点

单片机型号:STC89C52RC   问题描述:读P1口数据,数据为空   P1已经定义 uchar uReadValue;   P1_0=0; P1_1=0;     //此处OK,正常点亮了P1低位的两个Led uReadValue=P1;//将P1的值赋给变量uReadValu ...…

查看全部问答>

嵌入式软件可靠性设计

嵌入式软件可靠性设计 大家看一下是否对你们的工作有帮助。…

查看全部问答>

求助Matlab+DSP的快速实现

求助各位大神,需要用DSP实现一个软硬件系统,Matlab+DSP实现可能会简单些。。。有具体操作和讲解的资料,求提供 …

查看全部问答>