您的位置:首页>创事记 >

热点!Cortex-M3精通之路-1(汇编启动文件)

来源:面包芯语  

源码分析写了一堆,但是每次都分析不完就鸽了,气死我了。也思考了很久究竟写一些怎么样的文章。我想通了,要写一个合集,精通Cortex-M3!精通的优点就不用说了:

懂?

基本上你加上几个实战的项目就稳了。

其次就是广泛性,M3会了,M0难吗?M0+呢?不言而喻了。我准备从一个工程入手,一行一行的代码,多方权威资料查询写出这个合集,首先这篇是汇编的启动文件,要求对读者的要求是较高的,建议细细阅读。


【资料图】

精通ARM Cortex-M意味着对ARM Cortex-M内核及相关微控制器有深入全面的理解和高超的开发技能。具体需要:

1. 深入理解ARM Cortex-M内核架构,包括核心模块如内存保护单元、中断控制器、定时器等,以及指令系统和编程模型。

2. 熟悉各系列ARM Cortex-M内核如M0/M3/M4/M7的具体特征与差异,尤其是外设配置与编程方法。

3.掌握主流MCU厂商基于ARMCortex-M内核的微控制器产品,如STM32、Kinetis、EFM32等的详细特性和使用方法。

4. 精通CMSIS标准,熟练使用其提供的寄存器访问函数、外设驱动、启动文件、调试工具等,快速开发MCU应用程序。

5. 具有丰富的开发经验,能够熟练完成MCU资源配置、时钟选择、中断服务程序编写、外设驱动开发等,并对各种复杂问题进行快速解决。

6. 具备一定的创新能力,能基于对内核和MCU的深入理解进行功能优化与改进,开发出技术水平更高的程序。

ARM官网找的图

这个就是M3内核的特性

一个和别的内核的对比图

Corstone-101,官网看见的

我真服了,全网找不到这个开发板啥样子??

Corstone-101是Arm推出的一款入门级的MCU开发板,目的是帮助工程师轻松学习 Arm Cortex-M系列MCU。

Corstone-101开发板的主要特性:-

- MCU:采用Arm Cortex-M3内核的STM32F103RB芯片

-集成CMSIS-DAP debug probe,支持无缝调试- 板载各类外设和接口:LCD显示屏、LED、按键、USB转串口、MicroSD卡槽等

- Arm Mbed OS预装,可直接进行MCU软件开发

-免费开源的课程教材,包括从零开始快速入门stm32的开发环境搭建,到深入学习stm32的定时器、串口、DMA等。

-低成本,入门友好,非常适合Cortex-M内核和MCU开发入门Corstone-101的主要特点是:

1)集成度高:MCU、Debug probe、丰富外设、Mbed OS软件平台均集成在一块开发板上,使用非常方便。

2)教学优势:提供极为详尽的中文视频教程和文档,可以零基础快速学习stm32和MCU开发。

3) 低成本:Corstone-101开发板定价非常低,大约250元人民币,非常适合入门体验。

4) Mbed OS支持:基于Mbed OS,可以使用其丰富的库函数包以及在线编译环境快速开发应用程序,降低学习门槛。

250这个价格,我还是觉得不够低成本哈。

倒是和我说的差不多

本来今天就是要说这个CMSIS的,但是这个已经是抽象的高层了,所以先写汇编。

CMSIS是ARM Cortex-M系列内核的设备抽象层,全称为Cortex Microcontroller Software Interface Standard。它定义了一套标准的软件接口,用于开发基于ARM Cortex-M内核的MCU设备的固件。主要包含:

- 设备寄存器及外设访问函数库:提供标准的API访问MCU内核寄存器和外设,屏蔽不同芯片的寄存器差异。

- 启动文件和链接脚本:提供标准的MCU启动和内存布局文件,用于引导程序运行和链接。

- 中断服务例程:提供标准的中断服务例程入口定义,用于编写设备特定的中断服务程序。

- 调试寄存器访问宏:标准宏定义访问用于MCU调试的寄存器。

- 标准外设驱动库:为常用外设提供跨MCU通用的驱动库,如GPIO、USART等。

CMSIS的主要目的是降低基于Cortex-M内核MCU的开发复杂度,屏蔽芯片差异,实现跨MCU的代码可重用性。

开发者可以专注于应用程序本身的开发,而不需过多关注具体MCU的配置细节。目前,主流的MCU厂商如ST、NXP、TI等都提供了基于CMSIS标准的固件库和例程,使用这些CMSIS标准的固件库可以轻松配置和访问MCU资源,开发出高质量的程序。

CMSIS本质上是一个标准的固件库和软件接口。它提供:

1. 标准的寄存器外设访问函数库,相当于一个硬件抽象层,屏蔽不同MCU的寄存器差异,实现统一的访问接口。

2. 标准的启动文件、链接脚本和外设驱动,支持MCU启动、内存配置和常用外设的使用。

3. 标准的中断服务例程入口定义和调试宏定义。

4. 其他工具和示例程序等。

所以,CMSIS为MCU开发提供了一系列标准化和统一的固件基础设施,开发者可以直接基于CMSIS开发应用程序,而不需过多关注MCU本身的配置细节。这大大降低了基于ARM Cortex-M内核MCU开发的难度,实现了跨MCU的代码可重用性。但是,CMSIS本身并不是一个完整的MCU固件库。它仅定义了一系列标准,提供基本的访问接口和工具,但具体的 periperal drivers(外设驱动)和board support package(板级支持包)还需要芯片厂商及开发板厂商基于CMSIS标准开发和提供。所以,CMSIS给MCU开发带来的主要价值在于:

1. 标准化:定义一套行业标准的软件接口和规范。

2. 抽象化:提供硬件抽象层,屏蔽MCU差异,实现统一编程模型。

3. 可重用性:基于标准接口开发的代码可以重用于不同的MCU,降低开发成本。

ARM Cortex-M3内核适合使用CMSIS版本3.0及以上版本。CMSIS最初发布于2008年,当时的版本为1.0,主要支持ARM Cortex-M0和M3内核。随着CMSIS标准的不断发展和Cortex-M内核系列的增加,CMSIS也在发布新版本以支持更多内核和增加更多功能。主要版本发布情况如下:

- CMSIS 1.0:2008年,支持Cortex-M0和M3内核。

- CMSIS 2.0:2010年,增加对Cortex-M4内核的支持。- CMSIS 3.0:2012年,统一设备抽象层,增加debug和RTOS功能。

- CMSIS 3.1:2013年,添加CMSIS-DAP接口和新增Cortex-M7内核支持。- CMSIS 3.2:2014年,新增配置 wizard 工具和ARM Compiler 6支持。

- CMSIS 5.0:2016年,重构代码结构,支持基于CMSIS-Core的Middleware组件。

- CMSIS 5.1:2017年,新增CMSIS-Driver概念,扩充外设驱动支持。所以,对于ARM Cortex-M3内核,CMSIS 3.0版本及以后版本均有很好的支持,提供设备抽象层、debug工具、标准外设驱动等,可以很好地满足基于Cortex-M3内核MCU开发的需求。

我们来学学32里面的CMSIS里面的启动文件

startup_stm32f10x_cl.s是STM32F10x系列MCU的启动文件,由ARM官方提供。启动文件主要完成以下工作:

1. 设置堆栈指针(MSP),为中断服务程序和中断向量表预留堆栈空间。

2.检查复位源,并清除相应的复位标志。

3.设置中断向量表偏移地址,指向__Vectors段。

4.如果需要,设置中断向量表在RAM中的拷贝。

5. 如果需要,设置FPU寄存器为默认值。

6. 调用SystemInit()函数初始化系统时钟和外设。

7.调用__main()函数进入C代码。启动文件一般由汇编语言编写,需要做的工作主要是CPU及外设的最低层初始化配置,为后续C代码的运行做好准备。

看这个代码

Stack_SizeEQU0x00000400

这行定义了堆栈的大小为0x400字节,即1024字节。

AREA STACK, NOINIT, READWRITE, ALIGN=3

这行指令定义了一个名为STACK的无初值段,属性为可读写,并且地址对齐到4字节。这个段用于定义堆栈空间。

Stack_Mem SPACE Stack_Size

这行在STACK段内分配了0x400字节的存储空间,作为MCU的堆栈空间。

__initial_sp

这行将当前位置的值定义为__initial_sp标号,该标号的值为堆栈底地址,即堆栈指针SP的初始值。

所以,这段汇编代码完成了以下工作:

1. 定义了1024字节的堆栈存储空间Stack_Mem。

2.使用__initial_sp标号来指向这个堆栈空间的底部,这就是SP寄存器的复位值。

3.启动文件会将MSP(主堆栈指针)初始化为__initial_sp的值,这样堆栈空间Stack_Mem就关联到主堆栈,为中断服务程序的执行做好准备。

4.该堆栈空间也会在复位后由C代码继续使用,以存储函数调用的返回信息等。

该段代码定义的堆栈空间Stack_Mem所在的区域是MCU的内存空间。

MCU的内存空间一般可以分为以下几个区域:

1.FLASH:程序存储区域,用于存储程序代码。

2.SRAM:静态RAM,用于数据存储和堆栈空间。

3.HEAP:动态内存分配区域,一般也在SRAM中,用于malloc/free管理。

4.STACK:函数调用堆栈空间,用于存储函数调用的返回地址、参数等信息,一般在SRAM顶部。

所以,对于Startup文件来说,在SRAM中预留一段空间作为堆栈区域,这个区域就是用来作为中断服务程序和系统调用等的堆栈空间,用于保存CPU运行现场等信息,这个区域就是我们这段代码定义的Stack_Mem。

具体来说:

AREA STACK, NOINIT, READWRITE, ALIGN=3

这行定义了一个名为STACK的段,该段位于SRAM中,用于定义堆栈空间。

Stack_Mem SPACE Stack_Size

这行在该STACK段内分配了Stack_Size字节的存储空间,作为MCU的堆栈空间。

__initial_sp 该标号的值为这个Stack_Mem的底部地址,即堆栈指针SP的初值。所以,总结来说:

1.该段代码定义的堆栈空间Stack_Mem位于MCU的内存空间SRAM中。

2.它为中断服务程序和函数调用预留了一片存储区域。

3.这个存储区域的底部地址被__initial_sp标号指向,用于初始化SP寄存器的值。

4.然后SP寄存器(堆栈指针)在程序运行过程中会根据推栈和出栈操作在这个区域中上移和下移。

堆栈底地址指的是堆栈空间的最低可用地址,它表示堆栈区域中最先分配的空间,用于存放最早推入堆栈的数据。

对于Startup文件来说,它使用__initial_sp标号来标记堆栈底地址,这就是SP寄存器(堆栈指针)的初始值。

具体代码如下:

AREA STACK, NOINIT, READWRITE, ALIGN=3

Stack_Mem SPACE Stack_Size __initial_sp

这段代码:

1. 定义了大小为Stack_Size的堆栈空间Stack_Mem。

2.__initial_sp标号的值指向Stack_Mem的最低地址,这就是堆栈底地址。3.启动文件会将SP寄存器初始化为__initial_sp的值,使其指向堆栈底,以便后续的推栈操作。

4.SP寄存器的值会随着推栈和出栈操作在Stack_Mem区域内增大和减小。所以,对这个堆栈空间来说:

1.堆栈底地址由__initial_sp标号的值决定,它指向Stack_Mem的最低可用地址。

2.SP寄存器初值为__initial_sp,会从堆栈底开始向高地址生长,来保存推入堆栈的数据。

3.出栈操作会使SP寄存器的值减小,释放堆栈顶的数据,直到恢复到堆栈底。

4.堆栈空间的使用是从底至顶,底部空间存放最早的数据。所以,总结来说,堆栈底地址就是堆栈空间中最先被使用的那部分地址空间,它标记了堆栈区域的起始,为堆栈的推栈和出栈操作提供基础。

堆!

这段代码定义了MCU的堆空间以及相关标号。

具体分析下

Heap_Size EQU 0x00000200

该行定义了堆大小为512字节(0x200)

AREA HEAP, NOINIT, READWRITE, ALIGN=3

该行指令定义一个名为HEAP的无初值段,具有读写属性,地址按4字节对齐。这个段用于定义堆空间。

__heap_base 该标号的值为HEAP段的起始地址,表示堆底地址。

Heap_Mem SPACE Heap_Size

该行在HEAP段中分配了0x200字节的存储空间作为MCU的堆空间。

__heap_limit

该标号的值为HEAP段结束地址,表示堆顶地址。

所以,这段汇编代码完成了以下工作:

1. 定义了512字节的堆存储空间Heap_Mem。

2. 使用__heap_base标号来标记这个堆空间的起始地址,表示堆底。

3. 使用__heap_limit标号来标记这个堆空间的结束地址,表示堆顶。

4. 该堆空间在程序运行过程中由malloc/free管理动态内存的分配与释放。5. C语言中可以通过extern声明__heap_base和__heap_limit,以获取堆信息。

6. PRESERVE8指令用于强制8字节对齐。

THUMB指令指定该代码区使用Thumb-2指令。

该段代码定义的堆空间为MCU动态内存管理提供了存储区域与边界标记。通过__heap_base和__heap_limit,C代码可以方便获取到这个堆空间的信息,为malloc/free调用服务。所以,这段汇编代码对MCU的动态内存分配起到很关键的作业。

这里说下如何访问栈空间。

对于启动文件定义的堆栈空间Stack_Mem来说,如果要在C代码中访问和使用这个区域,有以下几种方法:

1. 使用全局变量在Startup文件中定义:

Stack_Mem SPACE Stack_Size __initial_sp

然后在C代码中使用全局变量声明:

这样stack_ptr变量就指向了堆栈顶,可以通过stack_ptr访问和修改堆栈空间中的数据。

2. 基址偏移Startup文件中Label Stack_Mem的地址为base_addr,则在C代码中可以通过:

unsigned int stack_ptr = (unsigned int )(base_addr + Stack_Size -

4);

来得到堆栈顶指针,以访问数据。需要知道Stack_Mem的绝对地址base_addr。

3. 定义指针数组在C代码中声明指针数:

unsigned int Stack[Stack_Size];然后stack_ptr = &Stack[Sta

ck_Size - 1];

这种方法需要在链接过程中正确设置该指针数组与Startup文件中Stack_Mem区域的关联。

4. 借助寄存器因为Startup文件已经将SP寄存器初始化为__initial_sp,也就是Stack_Mem底部,所以我们可以通过SP寄存器读写这个区域:

使用inline汇编直接通过SP寄存器读写堆栈空间。

好了,再说堆空间的作用:

堆空间主要用于MCU的动态内存分配,它为malloc()和free()函数调用提供内存池。具体来说,堆空间在MCU中的作用有:

1. 为动态内存申请提供存储空间:在程序运行时,可以通过malloc()函数向堆空间申请任意大小的内存块,以保存数据或创建对象。

2. 释放不再使用的内存:通过free()函数可以将堆空间中的内存块释放,以便下次继续使用。

3. 管理内存碎片:堆空间可以由malloc()函数自动管理内存碎片,试图利用所有的内存空间。

4. 实现内存动态扩展:如果堆空间初始分配的空间不足,还可以在运行时通过sbrk()函数向操作系统申请更多内存空间。

5. C++程序的new和delete操作也依赖于堆空间,用于创建和销毁对象。

所以,简单来说,堆空间为动态内存分配提供内存池,它最大的作用在于:

1. 满足运行时变化的内存需求,程序不确定对象到底占用多少内存空间,只能在运行时进行内存分配。

2. 实现堆内存的重复分配与释放,节省内存空间,避免内存浪费。

3. C++程序可以在堆上创建对象,并使用new和delete进行内存管理。总之,堆空间为程序运行时的动态内存分配需求提供了支持,它的管理主要依赖malloc、free和new、delete函数。

堆栈的区别是什么?

堆空间和栈空间都是MCU内存空间的一部分,但二者有以下主要区别:

1. 生长方向不同:

堆空间:向高地址生长,使用malloc()函数申请内存,按需动态分配。

栈空间:向低地址生长,大小在编译时确定,自动分配。

2. 申请方式不同:堆空间:使用malloc()和free()函数显式申请和释放内存。栈空间: FUN调用时自动分配和释放,不需要显式控制。

3. 生命周期不同:

堆空间:使用完毕须手动释放,生命周期不确定,动态决定。

栈空间:函数调用结束自动释放,生命周期与函数调用一致,编译时决定。

4. 使用目的不同:

堆空间:用于动态内存分配,对象创建。

栈空间:用于存储函数参数、局部变量、返回地址等,实现函数调用机制。

5. 空间大小不同:

堆空间:大小动态变化,按需分配。

栈空间:大小在编译时确定,固定分配。

所以,总结来说:

堆空间为动态内存分配和对象创建提供能力,大小和生命周期不定,需要手工管理。

栈空间为函数调用机制提供自动分配和释放的临时存储空间,大小和生命周期在编译时决定,无需手工控制。

我直接回马枪,堆空间如何访问呢?

1. 使用全局变量在启动文件中定义:

然后在C代码中使用全局变量声明:

这样heap_ptr变量就指向了堆底,通过它可以访问堆空间中的数据。2. 基址偏移启动文件中Label __heap_base和__heap_limit的地址分别为base_addr和limit_addr,则在C代码中可以通过:

来访问堆空间数据,offset为任意偏移量。需要知道__heap_base和__heap_limit的绝对地址。3. 定义指针数组在C代码中声明指针数组:

然后heap_ptr = Heap;这种方法需要在链接过程中正确设置该指针数组与启动文件中Heap_Mem区域的关联。4. 借助malloc()函数因为启动文件已经定义了__heap_base和__heap_limit来标识堆空间的范围,所以我们可以直接使用malloc()函数向这个区域申请内存:

malloc()函数会在Heap_Mem区域分配size大小的内存,并返回其起始地址,通过ptr访问这片内存。

接下来看这一段

这段代码定义了STM32的矢量表。

矢量表包含了中断服务程序的入口地址,以及系统使用的其他处理程序的入口地址。它位于地址0x00000000,并在复位后被映射到这个位置。所以,这段代码完成的主要工作有:

1. 定义了名为RESET的只读数据段,该段位置是0x00000000。

2. EXPORT指令导出了__Vectors、__Vectors_End和__Vectors_Size标号,以方便其他文件使用。

3. __Vectors标号指向矢量表的起始地址。__Vectors_End和__Vectors_Size分别用于标识矢量表的结束地址和大小。

4. 表起始地址存储初始堆栈指针SP的值(__initial_sp标号)。

5. 接下来定义了各种异常和中断处理程序的入口地址:

Reset_Handler:复位中断服务程序

NMI_Handler:NMI中断服务程序

HardFault_Handler:硬件故障中断服务程序

......

SysTick_Handler:SysTick中断服务程序

6. 一些入口保留为0,用于未来扩展。

所以,这个矢量表完成了以下工作:

1. 定义了各种异常和中断处理程序的入口地址,这些入口地址指向对应中断服务程序。

2. 矢量表位于地址0x00000000,并在MCU复位后被映射到这个位置。

3. 初始SP寄存器的值由矢量表的第一个字存储。

4. 中断发生时,MCU会根据中断类型在矢量表中找到对应的入口地址,然后跳转到此地址执行中断服务程序。矢量表是MCU中断机制的基础,它为异常和中断提供入口和服务程序。所以,这个矢量表定义对MCU的中断配置和服务起到了基础作用。

矢量表在MCU的内存空间中保存。它以数组的形式保存,每个数组元素存储一个函数入口地址。

具体来说:

1. 这个矢量表定义在启动文件中的RESET数据段中,该数据段位于内存地址0x00000000处。

2. __Vectors标号指向这个矢量表的数组起始地址,__Vectors_End标号指向数组结束地址。

3. 数组每个元素大小为4字节(32位MCU),用于存储一个函数入口地址。

4. 数组第一个元素存储了初始SP值,接下来的元素存储各种异常和中断处理程序的入口地址。

5. 当某个中断发生时,MCU会计算出中断类型对应的数组下标,然后跳转到该下标元素所指向的入口地址执行中断服务程序。

6. 这个数组实际上定义在编译器产生的启动文件汇编代码中。然后通过链接器与其他文件连接,最终在MCU的内存空间布局中实现布局。

所以,总结来说:

1. 这个矢量表以数组的形式在MCU内存中实现,数组每个元素存储一个入口地址。

2. 数组第一个元素存储SP初值,其他元素存储异常和中断服务程序的入口地址。

3. 该数组位于地址0x00000000,在MCU上电复位后被映射到该位置。

4. 中断发生时,通过中断类型计算数组下标,获取中断服务程序入口地址。

5. 该数组最终通过链接过程存放在MCU内存的正确位置,并在程序运行时被使用。这种数组的形式便于通过索引快速查找对应的入口地址,实现MCU的异常和中断机制。所以,矢量表采用这种数组结构实际上加快了中断响应速度,提高了系统实时性。

下面还有一段,其实和上面一样,都是地址而已

接下来看这段

所有的单片机,电脑,都会说复位这个事情。那它为什么如此的重要?因为我们为了可控,所有的初始化我们知道,运行的规律知道,所以复位是为了可控而已。

复位中断服务程序(Reset Handler)之所以存在,主要是为了实现MCU的初始化配置和C语言程序的启动。

具体来说,Reset Handler的作用有:

1. 完成MCU的初始化配置:设置时钟,配置GPIO等,为程序正确运行做准备。这一般由启动文件中定义的SystemInit函数完成。

2. 跳转到C语言程序的入口函数__main:C语言程序的执行是从__main函数开始的,所以Reset Handler需要跳转到__main函数。

3. 设置初始堆栈指针SP的值:在MCU上电复位后,SP寄存器的值是未知的,需要设置为一个正确的值,以便进行后续的堆栈操作。这个值通常存储在矢量表的第一个元素中。

4. 作为MCU的最高优先级中断服务程序:当MCU上电或复位时,首先会执行Reset Handler来完成系统的初始化和C语言程序的启动。

5. 如果用户定义了自己的Reset Handler,它将覆盖启动文件中的定义,用户程序将从用户定义的Reset Handler开始执行。

所以,简单来说,Reset Handler之所以存在,主要是为了:

1. 完成MCU的初始化配置,为正确运行提供基础。

2. 跳转到C语言程序的入口,启动程序执行。

3. 设置SP寄存器的初始值,为使用堆栈做准备。

4. 作为上电复位中断服务程序,首先被执行。

5. 可以被用户定义的Reset Handler覆盖。如果没有Reset Handler,MCU将启动在未初始化的状态,C语言程序也无法得到执行,SP的值是未知的,这会导致程序运行错误或无法运行。所以,Reset Handler对MCU的启动起到了基础作用,它为C程序的执行提供了启动条件和基本环境,是MCU上电必不可少的初始化代码。

来,这次有了前置知识再看代码:

代码定义了复位中断服务程序Reset_Handler。具体分析如下:

__Vectors_Size EQU __Vectors_End - __Vectors

这行指令计算了矢量表的大小,用于其他文件使用。

AREA |.text|, CODE, READONLY

这行指令定义了一个只读的代码段,用于存放异常和中断服务程序。

Reset_Handler PROC

这行指令定义了Reset_Handler的过程,表示复位中断服务程序的起始。

EXPORT Reset_Handler [WEAK]

此行导出Reset_Handler过程,以便其他文件使用,并指定该过程为弱定义,意味着如果用户定义了自己的Reset_Handler过程,那么编译器会使用用户定义的过程,而忽略此处的定义。

看这个

IMPORTSystemInit

此行导入SystemInit函数,表示Reset_Handler过程调用该函数。

IMPORT __main

此行导入C语言程序的入口函数__main。

LDR R0, =SystemInit

此指令将SystemInit函数地址加载到R0寄存器。

BLX R0

此指令调用SystemInit函数。

LDR R0, =__main

此指令将__main函数地址加载到R0寄存器。

BX R0

此指令跳转到__main函数,执行C语言程序。

ENDP

此行指令表示Reset_Handler过程的结束。

所以,这个Reset_Handler过程完成了以下工作:

1. 调用SystemInit函数完成系统初始化。

2. 跳转到__main函数,执行C语言程序。

3. 作为复位中断的中断服务程序,在MCU上电复位后首先被执行。

4. 如果用户定义了自己的Reset_Handler,那么由于该过程被定义为弱定义,用户定义会覆盖此处定义,被执行。该过程为C语言程序的执行提供了启动入口,完成了MCU的初始化配置.

接下来的代码很多的一样,拿两个看

这段代码定义了NMI中断服务程序NMI_Handler和硬件故障中断服务程序HardFault_Handler。

NMI_Handler PROC

此行定义NMI_Handler过程,表示NMI中断服务程序的起始。

EXPORT NMI_Handler [WEAK]

此行导出NMI_Handler过程,指定为弱定义,意味着如果用户定义了自己的NMI_Handler过程,编译器会使用用户定义的过程,忽略此处定义。

B . 此指令是一个空操作,不执行任何动作。

ENDP

此行表示NMI_Handler过程的结束。

HardFault_Handler\PROC

此行定义HardFault_Handler过程,表示硬件故障中断服务程序的起始。EXPORT HardFault_Handler [WEAK]

此行导出HardFault_Handler过程,指定为弱定义,意味着如果用户定义了自己的HardFault_Handler过程,编译器会使用用户定义的过程,忽略此处定义。

B . 此指令是一个空操作,不执行任何动作。

ENDP 此行表示HardFault_Handler过程的结束。

所以,这两个中断服务程序定义完成了:

1. 定义了对应的中断服务程序入口,但程序体为空。

2. 将这两个过程定义为弱定义,意味着如果用户定义了自己的服务程序,编译器会使用用户定义的过程。

3. 以空操作结束了这两个过程。

之所以这两个过程定义为空,主要是考虑到:

1. 出于兼容性考虑,启动文件需要定义所有标准的异常与中断入口,但具体处理由用户决定是否定义。

2. 对于一些使用不太频繁或处理比较复杂的中断,用户可能按需选择是否定义自己的服务程序。

3. 定义为空的中断服务程序不会对程序产生任何影响,保证定义后的兼容性。

所以,这两个定义主要是为了实现异常与中断入口的完整定义,但具体处理交由用户根据需要选择是否定义,如果未定义则默认为空操作。

今日最后的代码,我觉得全网我是最全的启动代码解析

这段代码主要用于定义堆栈的初始化。具体分析如下:

IF :DEF:__MICROLIB

此行条件指令检查__MICROLIB宏是否被定义。如果定义,执行IF内语句,否则执行ELSE内语句。

EXPORT __initial_sp

此行导出__initial_sp符号,用于初始化SP寄存器。

EXPORT __heap_base

此行导出__heap_base符号,用于标识堆起始地址。

EXPORT __heap_limit

此行导出__heap_limit符号,用于标识堆结束地址。

IMPORT __use_two_region_memory

此行导入__use_two_region_memory符号,用于检查是否使用两片内存区域。

__user_initial_stackheap 此符号用于标识初始化堆栈的过程。

LDR R0, = Heap_Mem 此指令将堆起始地址加载到R0寄存器。

这几行指令完成堆起始地址,堆结束地址,栈起始地址以及栈结束地址的加载与设置。

BX LR此指令用于返回,结束初始化堆栈的过程。

ALIGN此行用于4字节对齐。

ENDIF 此行表示IF语句的结束。

END 此行表示文件的结束。

所以,这段代码的主要工作是:

1. 如果定义了__MICROLIB宏,则直接导出__initial_sp等符号,否则执行初始化堆栈的过程。

2. __user_initial_stackheap过程用于根据传入的Heap_Mem和Stack_Mem参数设置堆栈的参数。

3. 该过程判断是否使用两片区域设置堆栈参数,如果使用则设置两片区域的起始结束地址。

4. 这段代码的目的是在启动文件中设置初始化堆栈所需要的参数与地址,为C语言程序的执行提供堆栈环境。这段代码的定义为用户编写的C语言程序提供了基本的堆栈初始化与设置,完成了与编译器相关的参数定义,实现了C语言程序运行所需要的内存环境配置。

我再总结一下:

1. 定义了RESET只读数据段,用于存放矢量表,该段位于地址0x00000000,在上电复位后被映射到该地址。

2. 定义并导出了__Vectors、__Vectors_End和__Vectors_Size等符号,方便其他文件使用矢量表。

3. 矢量表首地址存储初始SP值,其他地址存储各种异常和中断服务程序入口。

4. 定义了复位中断服务程序Reset_Handler,它调用SystemInit完成系统初始化,然后跳转到__main函数执行C语言程序。

5. 定义了部分中断服务程序如NMI_Handler和HardFault_Handler,但仅定义了入口,程序体为空,这主要考虑程序的兼容性与扩展性。

6. 根据__MICROLIB的定义情况,选择是否执行__user_initial_stackheap过程来设置堆栈参数,为C语言程序执行提供堆栈环境。

7. 定义并导出了__initial_sp、__heap_base和__heap_limit等符号,用于标识SP、堆和栈的起始和结束地址。

8. 使用ALIGN指令实现了部分字节对齐,提高了程序的兼容性和效率。

9. 文件最后通过END指令实现了文件的结束。

所以,这个启动文件完成的主要工作是:

1. 完成MCU的复位向量表定义,为各种中断提供入口与服务。

2. 定义复位中断服务程序,完成系统初始化和C语言程序启动。

3. 定义部分中断服务程序入口,但程序体为空,由用户决定是否具体定义。

4. 根据情况设置堆栈参数,为C语言程序执行提供环境。

5. 定义各种导出符号,方便其他文件使用。

6. 使用ALIGN实现部分字节对齐,提高效率。7. 标识文件的结束。

频繁的说这个对齐,我补个对齐的作用:

字节对齐指的是存储单元的地址要遵循某种边界限制,即地址的低几位要为0。

它的主要作用有:

1. 提高访问效率:当数据的地址是某种边界的整数倍时,CPU可以以更大宽度的访问单元去访问数据,这样可以减少CPU读取数据的次数,提高访问效率。2. 减少存储空间浪费:如果不对齐,在访问更大宽度的数据类型时,CPU需要访问的数据可能超出实际需要,这会占用额外的存储空间并影响总线带宽,对齐可以避免这种浪费。

3. 方便数据的转换:在某些特定的地址处访问的数据可以直接转换为其他数据类型,这可以提高处理效率,如果地址未对齐,这种直接转换就无法实现。

总的来说,字节对齐主要带来三个方面的好处:

1. 提高访问和处理数据的效率。

2. 避免存储空间的浪费。

3. 方便数据之间的直接转换。

而对齐的基本实现方法是:

1. 指定对齐类型:如2字节对齐、4字节对齐等,这通常由编译器来指定,可以通过定义宏的方式实现。

2. 数据声明时通过对齐类型进行限定:如int __align(4) num;表示num为4字节对齐。

3. 在某些关键数据声明前通过专用指令进行手动对齐:如ALIGN 4对下一数据进行4字节对齐。

4. 编译器会自动选择合适的对齐方式来对数据进行对齐,通常为最大数据类型的长度,这样可以发挥对齐带来的所有好处。

这几个文件详细的作用我之后的文章来说

core_cm3.c是CMSIS标准定义的Cortex-M3内核芯片支持包,它提供了以下内容:

1. MCU寄存器结构体定义:SCB, SysTick, NVIC等内核寄存器的结构体定义。

2. 系统时钟配置函数:CMU_ClkInit()、CMU_ClockSelectConfig()等。

3. 系统滴答配置函数:SysTick_Config()用于配置SysTick定时器的溢出中断周期。

4. 中断配置函数:NVIC_EnableIRQ()、NVIC_DisableIRQ()、NVIC_SetPriority()等,用于配置中断优先级与使能状态。

5. 系统SoftReset和HardReset函数:用于软复位和硬复位MCU。

6. 内部Flash配置函数:用于配置Flash预取指缓存、等待信号以及访问权限等。

7. 函数调用栈配置函数:用于配置PSP堆栈指针和MSP主堆栈指针。

8. 系统滴答定时器SysTick的中断服务函数SysTick_Handler()。

9. 设备向量表__Vectors定义,里面包含SysTick和外设中断服务程序入口,以及各类异常入口。

10. 用户可根据需要实现的空操作清零函数:Default_Handler()。

11. MPU配置函数:用于配置内存保护单元,设定不同存储区域的访问权限。 所以,core_cm3.c文件为基于Cortex-M3内核的MCU提供了系统级配置与接口,包括时钟、中断、内存管理等方方面面。用户可以直接调用这些函数接口来配置MCU,也可以根据需要修改或扩充。这个文件遵循CMSIS标准,具有很高的通用性,主要目的是降低不同MCU的移植difficulty,加速嵌入式工程师的开发工作。所以,它是开发基于ARM Cortex-M3内核MCU的重要基石。

stm32f10x.h是ST公司为STM32F10x系列MCU提供的顶层头文件,它包含以下内容:

1. 对标准库的引用,如stdint.h、stdbool.h等。

2. 对CMSIS标准的引用,引入CMSIS定义的一些数据类型、寄存器定义和函数原型。

3. MCU型号选择,根据具体的芯片型号选择正确的定义。

4. 外设时钟使能宏定义,方便开启或关闭外设时钟。

5. 寄存器定义,定义MCU所有的内核寄存器和外设寄存器结构体。

6. 中断编号定义,定义所有的中断源所对应的值。

7. 位带操作宏定义,提供对寄存器位的设置、清除以及翻转等操作。

8. FLASH和OTP操作函数,提供FLASH读、写、擦除以及OTP读写的函数原型。

9. 复位和时钟控制寄存器地址的定义以及函数原型。

10. GPIO通用IO口操作函数原型定义。

11. 中断和事件管理函数原型定义,NVIC相关操作函数。

12. 其他外设(串口、SPI、I2C、ADC等)配置函数和外设特有的一些定义。所以,stm32f10x.h作为STM32F10x系列MCU的顶层头文件,它包含了影响MCU所有外设和内核的内容,包括但不限于:寄存器定义、中断定义、位带操作、复位和时钟配置、GPIO配置、串口配置等等。开发人员可以直接include这个头文件,就可以使用里面定义的东西,非常方便。

system_stm32f10x.c文件主要包含STM32F10x系列MCU的系统时钟配置函数。系统时钟配置主要完成以下工作:

1. 根据外部晶振的频率,配置PLL时钟进行倍频,得到MCU内核时钟和AHB/APB总线时钟。

2. 配置HSE(高速外部时钟)作为系统时钟,或者作为PLL的时钟源。

3. 配置LSE(低速外部时钟)作为RTC的时钟源。

4. 选择不同的预分频因子,以满足系统时钟等于内核时钟的要求。

5. 配置各总线时钟AHB, APB1和APB2的预分频因子。

6. 根据配置的总线时钟频率,配置外设的时钟。

7. 根据CPU工作频率,设置Flash访问时间 FlashLatency。

所以,system_stm32f10x.c文件主要通过调用stm32f10x_clk.c和stm32f10x_rcc.c等文件里的函数,来配置MCU的整个时钟系统,包括选择PLL时钟源、PLL倍频系数、AHB/APB总线分频系数以及外设时钟使能等。这个文件实现了ClockConfiguration()函数,该函数会在MCU启动时被startup_stm32f10x_xx.s的SystemInit()调用,从而完成MCU系统时钟的初始化配置。

关键词:

最新文章