正点原子STM32(基于HAL库)

Posted 行稳方能走远

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了正点原子STM32(基于HAL库)相关的知识,希望对你有一定的参考价值。

正点原子B站视频地址:https://www.bilibili.com/video/BV1bv4y1R7dp?p=1&vd_source=cc0e43b449de7e8663ca1f89dd5fea7d

目录

单片机简介


Cortex-M介绍

初识STM32

STM32芯片分类
ST中文社区网:https://www.stmcu.org.cn/
ST官网:https://www.st.com/content/st_com/en.html

STM32命名规则

STM32 选型

了解了STM32 的系列和命名以后,我们再进行STM32 选型就会比较容易了,这里我们只要遵循:由高到低,由大到小的原则,就可以很方便的完成设计选型了,具体如下:

由高到低原则:在不能评估项目所需性能的时候,可以考虑先选择高性能的STM32 型号进行开发,比如选择F4/F7/H7 等,在高性能STM32 上面完成关键性能(即最需要性能的代码)开发验证,如果满足使用要求,则可以降档,如从H7→F7→F4→F1,如不满足要求,则可以升档,如从F4→F7→H7,通过此方法找到最佳性价比的STM32 系列。

由大到小原则:在不能评估项目所需FLASH 大小、SRAM 大小、GPIO 数量、定时器等资源需求的时候,可以考虑先选择容量较大的型号进行开发,比如选择512K/甚至1M FLASH 的型号进行开发,等到开发完成大部分功能之后,就对项目所需资源有了定论,从而可以根据实际情况进行降档选择(当然极少数情况可能还需要升档),通过此方法,找到最合适的STM32
型号。

整个选型工作大家可以在正点原子开发板上进行验证,一般我们开发板都是选择容量比较大/资源比较多的型号进行设计的,这样可以免去大家自己设计焊接验证板的麻烦,加快项目开发进度。一些资深工程师,对项目要求认识比较深入的话,甚至都不需要验证了,直接就可以选出最合适的型号,这个效率更高。当然这个需要长期积累和多实践,相信只要大家多学习,多实践,总有一天也能达到这个级别。

STM32 设计

这里我们简单给大家介绍一下STM32 的原理图设计,上一节我们通过选型原则可以确定项目所需的STM32 具体型号,但是在选择型号以后,需要先设计原理图,然后再画PCB、打样、焊接、调试等步骤。这里我们重点介绍如何设计STM32F103 的原理图。
任何MCU 部分的原理图设计,其实都遵循:最小系统+ IO 分配的设计原则。在开始设计原理图之前,我们要通读一遍对STM32F103 原理图设计非常有用的手册:STM32F103 的数据手册,可以说不看这个数据手册,我们就无法设计STM32F103 原理图。

数据手册

在设计STM32F103 原理图的时候,我们需要用到一个非常重要的文档:STM32F103 数据
手册,里面对STM32F103 的IO 定义和说明有非常详细的描述,是我们设计原理图的基础。战舰开发板所使用的STM32F103ZET6 芯片数据手册,存放在:A 盘→ 7,硬件资料→2,芯片资料→ STM32F103ZET6.pdf / STM32F103ZET6(中文版).pdf,接下来我们简单介绍一下如何使用该文档。

STM32F103ZET6.pdf 是最新的英文版(V13)STM32 数据手册

STM32F103ZET6(中文版).pdf 是中文版(V5)STM32 数据手册

大家可以根据自己的喜欢来选择合适的版本进行阅读,内容上基本大同小异,从准确性全面性的角度来说,看V13 英文版是最好的,从简单,易懂来说,看V5 中文版也是可以的。

STM32F103ZET6.pdf 数据手册是针对大容量系列(FLASH 容量在256KB~512KB 之间),主要包括8 个章节,如表2.3.4.1 所示:

章节概要说明
介绍简单说明数据手册作用:介绍大容量增强型F103xC/D/E 产品的订购信息和机械特性
规格说明简单介绍STM32F103 内部所有资源及外设特点
引脚定义介绍不同封装的引脚分布、引脚定义等,含引脚特性、复用功能、脚位等
存储器映像介绍STM32F103 整个4GB 存储空间和外设的地址映射关系
电气特性介绍STM32F103 的详细电气特性,包括工作电压、电流(自己设计电路的时候超过额定值就要额外加电阻限流)、温度、各外设资源的电气性能等
封装特性介绍了STM32F103 不同封装的封装机械数据(脚距、长短等)、热特性等
订货代码和2.3.2 节内容类似,介绍STM32 具体型号所代表的意义,方便选型订货
版本历史介绍数据手册不同版本之间的差异和修订内容

整个STM32F103 数据手册,对我们开发学习STM32 来说都比较重要,因此建议大家可以简单的通读一遍这个文档,以加深印象。对于原理图设计,最重要的莫过于引脚定义这一章节了,只有知道了STM32 的引脚定义,才能开始设计原理图。

STM32F103ZET6 引脚分布如图2.3.4.1 所示:

STM32F103ZET6 引脚定义如表2.3.4.2 所示:

引脚定义表的具体说明如表2.3.4.3 所示:

序号名称说明
脚位对应芯片的引脚,LQFP封装使用纯数字表示,BGA 使用字母+数字表示 这里列出了6 种封装的脚位描述,根据实际型号选择合适的封装查阅
管脚名称即对应引脚的名字,PD0~5 表示GPIO 引脚,VSS_10/VDD_10 表示第10 组电源引脚,其他类似
类型I/O :表示输入/输出引脚 I :表示输入引脚 S :表示电源引脚
IO 电平FT :表示5V 兼容的引脚(可以接5V/3.3V) 空:表示5V 不兼容引脚(仅可以接3.3V)
主功能(复位后)复位后,该引脚的默认功能
可选的复用功能默认复用功能:是指开启复用功能后,该引脚默认的复用功能;重定义功能:是指可以通过重映射的复用功能,需设置重映射寄存器

了解引脚分布和引脚定义以后,我们就可以开始设计STM32F103 的原理图了。

最小系统

最小系统就是保证MCU 正常运行的最低要求,一般是指MCU 的供电、复位、晶振、BOOT等部分。STM32F103 的最小系统需求如表2.3.4.4 所示:





完成以上引脚的设计以后,STM32F103 的最小系统就完成了,关于这些引脚的实际原理图,大家可以参考我们战舰开发板的原理图。接下来就可以开始进行IO 分配了。

IO 分配

IO 分配就是在完成最小系统设计以后,根据项目需要对MCU 的IO 口进行分配,连接不同的器件,从而实现整体功能。比如:GPIO、IIC、SPI、SDIO、FSMC、USB、中断等。遵循:先分配特定外设IO,再分配通用IO,最后微调的原则,见表2.3.4.5 所示:

分配外设说明
特定外设IICIIC 一般用到2 根线:IIC_SCL 和IIC_SDA(ST 叫I2C)数据手册有I2C_SCL、I2C_SDA 复用功能的GPIO 都可选用
特定外设SPISPI 用到4 根线:SPI_CS/MOSI/MISO/SCK 一般SPI_CS 我们使用通用GPIO 即可,方便挂多个SPI 器件;数据手册有SPI_MOSI/MISO/SCK 复用功能的GPIO 都可选用
特定外设TIM根据需要可选:TIM_CH1/2/3/4/ETR/1N/2N/3N/BKIN 等;数据手册有TIM_CH1/2/3/4/ETR/1N/2N/3N/BKIN 复用功能的GPIO 都可选用
特定外设USART UARTUSART 有USART_TX/RX/CTS/RTS/CK 信号 UART 仅有UART_TX/RX 两个信号 一般用到2 根线:U(S)ART_TX 和U(S)ART_RX ;数据手册有U(S)ART_TX/RX 复用功能的GPIO 都可选用
特定外设USBUSB 用到2 根线:USB_DP 和USB_DM;数据手册有USB_DP、USB_DM 复用功能的GPIO 都可选用
特定外设CANCAN 用到2 根线:CAN_RX 和CAN_TX;数据手册有USB_DP、USB_DM 复用功能的GPIO 都可选用
特定外设ADCADC 根据需要可选:ADC_IN0 ~ ADC_IN15;数据手册有ADC_IN0 ~ ADC_IN15 复用功能的GPIO 都可选用
特定外设DACDAC 根据需要可选:DAC_OUT1 / DAC_OUT2;DAC 固定为:DAC_OUT1 使用PA4、DAC_OUT2 使用PA5
特定外设SDIOSDIO 一般用到6 根线:SDIO_D0/1/2/3/SCK/CMD;数据手册有SDIO_D0/1/2/3/SCK/CMD 复用功能的GPIO 都可选用
特定外设FSMC根据需要可选:FSMC_D0 ~ 15 /A0 ~ 25/ NBL0 ~ 1/NE1~ 4/NCE2~ 3/ NOE/NWE/NWAIT/CLK 等;数据手册有FSMC_D0~ 15/A0~ 25/ NBL0~ 1/NE1~ 4/NCE2~3/NOE/;NWE/NWAIT/CLK 复用功能的GPIO 都可选用
通用GPIO在完成特定外设的IO 分配以后,就可以进行GPIO 分配了比如将按键、LED、蜂鸣器等仅需要高低电平读取/输出的外设连接到空闲的普通GPIO 即可
微调IO微调主要包括两部分:1,当IO不够用的时候,通用GPIO 和特定外设可能要共用IO 口;2,为了方便布线,可能要调整某些IO 口的位置。这两点,根据实际情况进行调整设置,做到:尽可能多的可以同时使用所有功能,尽可能方便布线。

经过以上几个步骤,我们就可以完成STM32F103 的原理图设计了。

STM32启动过程分析

本章给大家分析STM32F1 的启动过程,这里的启动过程是指从STM32 芯片上电复位执行的第一条指令开始,到执行用户编写的main 函数这之间的过程。我们编写程序,基本都是用C 语言编写,并且以main 函数作为程序的入口。但是事实上,main 函数并非最先执行的,在此之前需要做一些准备工作,准备工作通过启动文件的程序来完成。理解STM32 启动过程,对今后的学习和分析STM32 程序有很大的帮助。

注意:学习本章内容之前,请大家最好先阅读由正点原子团队编写的《STM32 启动文件浅析》和《STM32 MAP 文件浅析》这两份文档(路径:A 盘→ 1,入门资料)。

启动模式

我们知道的复位方式有三种:上电复位,硬件复位和软件复位。当产生复位,并且离开复
位状态后,CM3 内核做的第一件事就是读取下列两个32 位整数的值:

  • (1)从地址0x0000 0000 处取出堆栈指针MSP 的初始值,该值就是栈顶地址。
  • (2)从地址0x0000 0004 处取出程序计数器指针PC 的初始值,该值指向复位后执行的第一条指令(复位向量)。

为何地址加4?32位单片机每次取4个字节(32位)

下面用示意图表示,如图9.1.1 所示。

上述过程中,内核是从0x0000 0000 和0x0000 0004 两个的地址获取堆栈指针SP 和程序计
数器指针PC。事实上,0x0000 0000 和0x0000 0004 两个的地址可以被重映射到其他的地址空间。例如:我们将0x0800 0000 映射到0x0000 0000,即从内部FLASH 启动,那么内核会从地址0x0800 0000 处取出堆栈指针MSP 的初始值,从地址0x0800 0004 处取出程序计数器指针PC 的初始值。
CPU 会从PC 寄存器指向的地址空间取出的第1 条指令开始执行程序,就是开始执行复位中断服务程序Reset_Handler。
将0x0000 0000 和0x0000 0004 两个地址重映射到其他的地址空间,就是启动模式选择。

对于STM32F1 的启动模式(也称自举模式),我们看表9.1.1 进行分析。

注:启动引脚的电平:0:低电平;1:高电平;x:任意电平,即高低电平均可

由表9.1.1 可以看到,STM32F1 根据BOOT 引脚的电平选择启动模式,这两个BOOT 引脚
根据外部施加的电平来决定芯片的启动地址。(0 和1 的准确电平范围可以查看F103 系列数据手册I/O 特性表,但我们最好是设置成Gnd 和VDD 的电平值)
(1)内部FLASH 启动方式(主闪存存储器,用的最多)
当芯片上电后采样到BOOT0 引脚为低电平时(系统复位后,SYSCLK的第4个上升沿,BOOT引脚的值将被锁存),0x00000000 和0x00000004 地址被映射到
内部FLASH 的首地址0x08000000 和0x08000004。因此,内核离开复位状态后,读取内部FLASH的0x08000000 地址空间存储的内容,赋值给栈指针MSP,作为栈顶地址,再读取内部FL ASH的0x08000004 地址空间存储的内容,赋值给程序指针PC,作为将要执行的第一条指令所在的地址。完成这两个操作后,内核就可以开始从PC 指向的地址中读取指令执行了。

(2)内部SRAM 启动方式
类似于内部Flash,当芯片上电后采样到BOOT0 和BOOT1 引脚均为高电平时,地址
0x00000000 和0x00000004 被映射到内部SRAM 的首地址0x20000000 和0x20000004,内核从SRAM 空间获取内容进行自举。在实际应用中,由启动文件starttup_stm32f103xe.s 决定了0x00000000 和0x00000004 地址存储什么内容,链接时,由分散加载文件(sct)决定这些内容的绝对地址,即分配到内部FLASH 还是内部SRAM。
(3)系统存储器启动方式
当芯片上电后采样到BOOT0 =1,BOOT1=0 的组合时,内核将从系统存储器的0x1FFFF000及0x1FFFF004 获取MSP 及PC 值进行自举。系统存储器是一段特殊的空间,用户不能访问,ST 公司在芯片出厂前就在系统存储器中固化了一段代码(bootloader)。因而使用系统存储器启动方式时,内核会执行该代码,该代码运行时,会为ISP(In System Program)提供支持x,在STM32F1 上最常见的是检测USART1 传输过来的信息,并根据这些信息更新自己内部FLASH 的内容,达到升级产品应用程序的目的,因此这种启动方式也称为ISP 启动方式。

启动文件分析(startup_stm32xxx.s)

STM32 启动文件由ST 官方提供,在官方的STM32Cube 固件包里,对于STM32F103 系
列芯片的启动文件,我们选用的是startup_STM32F103xe.s 这个文件。启动文件用汇编编写,是系统上电复位后第一个执行的程序。

启动文件主要做了以下工作:
1、初始化堆栈指针MSP = _initial_sp,从0X0800 0000获取
2、初始化程序计数器指针PC = Reset_Handler,从0X0800 0004获取
3、设置堆和栈的大小
4、初始化中断向量表,_Vectors定义
5、配置外部SRAM 作为数据存储器(可选)
6、配置系统时钟,通过调用SystemInit 函数(可选)
7、调用C库中的_main 函数初始化用户堆栈,最终调用main 函数

启动文件中的一些指令

指令名称作用
EQU给数字常量取一个符号名,相当于C 语言中的define
AREA汇编一个新的代码段或者数据段
ALIGN编译器对指令或者数据的存放地址进行对齐,一般需要跟一个立即数,缺省表示4 字节对齐。要注意的是,这个不是ARM 的指令,是编译器的,这里放到一起为了方便。
SPACE分配内存空间
PRESERVE8当前文件堆栈需要按照8 字节对齐
THUMB表示后面指令兼容THUMB 指令。在ARM 以前的指令集中有16 位的THUMBM 指令,现在Cortex-M 系列使用的都是THUMB-2 指令集,THUMB-2 是32 位的,兼容16 位和32 位的指令,是THUMB 的超级版。
EXPORT声明一个标号具有全局属性,可被外部的文件使用
DCD以字节为单位分配内存,要求4 字节对齐,并要求初始化这些内存
PROC定义子程序,与ENDP 成对使用,表示子程序结束
WEAK弱定义,如果外部文件声明了一个标号,则优先使用外部文件定义的标号,如果外部文件没有定义也不会出错。要注意的是,这个不是ARM 的指令,是编译器的,这里放到一起为了方便。
IMPORT声明标号来自外部文件,跟C 语言中的extern 关键字类似
LDR从存储器中加载字到一个存储器中
BLX跳转到由寄存器给出的地址,并根据寄存器的LSE 确定处理器的状态,还要把跳转前的下条指令地址保存到LR
BX跳转到由寄存器/标号给出的地址,不用返回
B跳转到一个标号
IF,ELSE,EN-DIF汇编条件分支语句,跟C 语言的类似
END到达文件的末尾,文件结束

上表,列举了STM32 启动文件的一些汇编和编译器指令,关于其他更多的ARM 汇编指令,我们可以通过MDK 的索引搜索工具中搜索找到。打开索引搜索工具的方法:
MDK->Help->uVision Help,如图9.2.1.1 所示。


打开之后,我们以EQU 为例,演示一下怎么使用,如图9.2.1.2 所示。
图9.2.1.2 搜索EQU 汇编指令

搜索到的结果有很多,我们只需要看位置为Assembler User Guide 这部分即可。

启动文件代码讲解

(1)栈空间的开辟
栈空间的开辟,源码如图9.2.2.1 所示:


源码含义:开辟一段大小为0x0000 0400(1KB)的栈空间,段名为STACK,NOINIT 表示不初始化;READWRITE 表示可读可写;ALIGN=3,表示按照2^3 对齐,即8 字节对齐。
AREA 汇编一个新的代码段或者数据段。
SPACE 分配内存指令,分配大小为Stack_Size字节连续的存储单元给栈空间。

__initial_sp 紧挨着SPACE 放置,表示栈的结束地址,栈是从高往低生长,所以结束地址就是栈顶地址。

栈主要用于存放局部变量,函数形参等,属于编译器自动分配和释放的内存,栈的大小不能超过内部SRAM 的大小。如果工程的程序量比较大,定义的局部变量比较多,那么就需要在启动代码中修改栈的大小,即修改Stack_Size 的值。如果程序出现了莫名其妙的错误,并进入了HardFault 的时候,你就要考虑下是不是栈空间不够大,溢出了的问题。

(2)堆空间的开辟
堆空间的开辟,源码如图9.2.2.2 所示:

源码含义:开辟一段大小为0x0000 0200(512 字节)的堆空间,段名为HEAP,不初始化,可读可写,8 字节对齐。

__heap_base 表示堆的起始地址,__heap_limit 表示堆的结束地址。堆和栈的生长方向相反的,堆是由低向高生长,而栈是从高往低生长。

堆主要用于动态内存的分配,像malloc()、calloc()和realloc()等函数申请的内存就在堆上面,如果程序里面没有用到malloc等这些函数可以将堆空间设置成0,节省空间。堆中的内存一般由程序员分配和释放,若程序员不释放,程序结束时可能由操作系统回收。

接下来是PRESERVE8 和THUMB 指令两行代码。如图9.2.2.3 所示。

PRESERVE8:指示编译器按照8 字节对齐。
THUMB:指示编译器之后的指令为THUMB 指令。
注意:由于正点原子提供了独立的内存管理实现方式(mymalloc,myfree 等),并不需要使用C库的malloc 和free 等函数,也就用不到堆空间,因此我们可以设置Heap_Size 的大小为0,以节省内存空间。

(3)中断向量表定义(简称:向量表)
为中断向量表定义一个数据段,如图9.2.2.4 所示

源码含义:定义一个数据段,名字为RESET, READONLY 表示只读。EXPORT 表示声明一
个标号具有全局属性,可被外部的文件使用。这里是声明了__Vectors、__Vectors_End 和__Vectors_Size 三个标号具有全局性,可被外部的文件使用。

STM32F103 的中断向量表定义代码,如图9.2.2.5 所示。


__Vectors 为向量表起始地址,__Vectors_End 为向量表结束地址,__Vectors_Size 为向量表大小,__Vectors_Size = __Vectors_End - __Vectors。

DCD:分配一个或者多个以字为单位的内存,以四字节对齐,并要求初始化这些内存。
中断向量表被放置在代码段的最前面。例如:当我们的程序在FLASH 运行时,那么向量表的起始地址是:0x0800 0000。结合图9.2.2.5 可以知道,地址0x0800 0000 存放的是栈顶地址。
DCD 以四字节对齐分配内存,也就是下个地址是0x0800 0004,存放的是Reset_Handler 中断函数入口地址。

从代码上看,向量表中存放的都是中断服务函数的函数名,所以C 语言中的函数名对芯片来说实际上就是一个地址

STM32F103 的中断向量表可以在《STM32F10xxx 参考手册_V10(中文版).pdf》的第9 章的9.1.2 小节找到,与中断向量表定义代码是对应的。

(4)复位程序
接下来是定义只读代码段,如图9.2.2.6 所示:

定义一个段命为.text,只读的代码段,在CODE 区。
复位子程序代码,如图9.2.2.7 所示:

  • 利用PROC、ENDP 这一对伪指令把程序段分为若干个过程,使程序的结构加清晰。
  • 复位子程序是复位后第一个被执行的程序,主要是调用SystemInit 函数配置系统时钟、还有就是初始化FSMC 总线上外挂的SRAM(可选)。然后在调用C 库函数__main,最终调用main 函数去到C 的世界。

如果外部没有:SystemInit函数则会报错!可以把它屏蔽掉,或者在外面定义一个空的函数

  • EXPORT 声明复位中断向量Reset_Handler 为全局属性,这样外部文件就可以调用此复位中断服务。

  • WEAK:表示弱定义,如果外部文件优先定义了该标号则首先引用外部定义的标号,如果外部文件没有声明也不会出错。这里表示复位子程序可以由用户在其他文件重新实现,这里并不是唯一的。

  • IMPORT 表示该标号来自外部文件。这里表示SystemInit 和__main 这两个函数均来自外部的文件。

  • LDR、BLX、BX 是内核指令,可在《Cortex-M3 权威指南》第四章-指令集里面查询到。

  • LDR 表示从存储器中加载字到一个存储器中。

  • BLX 表示跳转到由寄存器给出的地址,并根据寄存器的LSE 确定处理器的状态,还要把跳转前的下条指令地址保存到LR。

  • BX 表示跳转到由寄存器/标号给出的地址,不用返回。这里表示切换到__main 地址,最终调用main 函数,不返回,进入C 的世界。

(5)中断服务程序


接下来就是中断服务程序了,如图9.2.2.8 所示。

可以看到这些中断服务函数都被[WEAK]声明为弱定义函数,如果外部文件声明了一个标号,则优先使用外部文件定义的标号,如果外部文件没有定义也不会出错。

这些中断函数分为系统异常中断和外部中断,外部中断根据不同芯片有所变化。B 指令是跳转到一个标号,这里跳转到一个‘.’,表示无限循环。

在启动文件代码中,已经把我们所有中断的中断服务函数写好了,但都是声明为弱定义,所以真正的中断服务函数需要我们在外部实现。

如果我们开启了某个中断,但是忘记写对应的中断服务程序函数又或者把中断服务函数名写错,那么中断发生时,程序就会跳转到启动文件预先写好的弱定义的中断服务程序中,并且在B 指令作用下跳转到一个‘.’中,无限循环。

这里的系统异常中断部分是内核的,外部中断部分是外设的。

(6)用户堆栈初始化
ALIGN 指令,如图9.2.2.9 所示:

ALIGN 表示对指令或者数据的存放地址进行对齐,一般需要跟一个立即数,缺省表示4 字节对齐。要注意的是,这个不是ARM 的指令,是编译器的。

接下就是启动文件最后一部分代码,用户堆栈初始化代码,如图9.2.2.10 所示:

IF, ELSE, ENDIF 是汇编的条件分支语句。
588 行判断是否定义了__MICROLIB。关于__MICROLIB 这个宏定义,我们是在KEIL 里面配置,具体方法如图9.2.2.11 所示。

勾选了Use MicroLIB 就代表定义了__MICROLIB 这个宏。
如果定义__MICROLIB,声明__initial_sp、__heap_base 和__heap_limit 这三个标号具有全局属性,可被外部的文件使用。__initial_sp 表示栈顶地址,__heap_base 表示堆起始地址,
__heap_limit 表示堆结束地址。
如果没有定义__MICROLIB,实际的情况就是我们没有定义__MICROLIB,所以使用默认的C 库运行。那么堆栈的初始化由C 库函数__main 来完成。

IMPORT 声明__use_two_region_memory 标号来自外部文件。
EXPORT 声明__user_initial_stackheap 具有全局属性,可被外部的文件使用。

340 行标号__user_initial_stackheap,表示用户堆栈初始化程序入口。
接下来进行堆栈空间初始化,堆是从低到高生长,栈是从高到低生长,是两个互相独立的数据段,并且不能交叉使用。

344 行保存堆起始地址。345 行保存栈大小。346 行保存堆大小。347 行保存栈顶指针。348行跳转到LR 标号给出的地址,不用返回。354 行END 表示到达文件的末尾,文件结束。

Use MicroLIB
MicroLIB 是MDK 自带的微库,是缺省C 库的备选库,MicroLIB 进行了高度优化使得其代码变得很小,功能比缺省C 库少。MicroLIB 是没有源码的,只有库。
关于MicroLIB 更多知识可以看官方介绍http://www.keil.com/arm/microlib.asp 。

系统启动流程

我们知道启动模式不同,启动的起始地址是不一样的,下面我们以代码下载到内部FLASH的情况举例,即代码从地址0x0800 0000 开始被执行。

当产生复位,并且离开复位状态后,CM3 内核做的第一件事就是读取下列两个32 位整数
的值:
(1)从地址0x0800 0000 处取出堆栈指针MSP 的初始值,该值就是栈顶地址。
(2)从地址0x0800 0004 处取出程序计数器指针PC 的初始值,该值指向中断服务程序Reset_Handler。下面用示意图表示,如图9.2.3.1 所示。

我们看看STM32F103 开发板HAL 库例程的实验1 跑马灯实验中,取出的MSP 和PC 的值是多少,方法如图9.2.3.2 所示。

由图9.2.3.2 可以知道地址0x0800 0000 的值是0x2000 0788,地址0x0800 0004 的值是0x0800 01CD,即堆栈指针SP =0x2000 0788,程序计数器指针PC = 0x0800 01CD(即复位中断服务程序Reset_Handler 的入口地址)。因为CM3 内核是小端模式,所以倒着读。

请注意,这与传统的ARM 架构不同——其实也和绝大多数的其它单片机不同。传统的ARM架构总是从0 地址开始执行第一条指令。它们的0 地址处总是一条跳转指令。而在CM3 内核中,0 地址处提供MSP 的初始值,然后就是向量表(向量表在以后还可以被移至其它位置)。
向量表中的数值是32 位的地址,而不是跳转指令。向量表的第一个条目指向复位后应执行的第一条指令,就是Reset_Handler 这个函数。下面继续以MINI 开发板HAL 库例程实验1 跑马灯实验为例,代码从地址0x0800 0000 开始被执行,讲解一下系统启动,初始化堆栈、MSP 和PC后的内存情况。

因为CM3 使用的是向下生长的满栈,所以MSP 的初始值必须是堆栈内存的末地址加1。

举例来说,如果你的栈区域在0x2000 0388‐0x2000 0787(1KB 大小)之间,那么MSP 的
初始值就必须是0x2000 0788。

向量表跟随在MSP 的初始值之后——也就是第2 个表目。

R15 是程序计数器,在汇编代码中,可以使用名字“PC”来访问它。ARM 规定:PC 最低两位并不表示真实地址,最低位LSB 用于表示是ARM 指令(0)还是Thumb 指令(1),因为CM3 主要执行Thumb 指令,所以这些指令的最低位都是1(都是奇数)。因为CM3 内部使用了指令流水线,读PC 时返回的值是当前指令的地址+4。比如说:

0x1000: MOV R0, PC ; R0 = 0x1004

如果向PC 写数据,就会引起一次程序的分支(但是不更新LR 寄存器)。CM3 中的指令至少是半字对齐的,所以PC 的LSB 总是读回0。然而,在分支时,无论是直接写PC 的值还是使用分支指令,都必须保证加载到PC 的数值是奇数(即LSB=1),表明是在Thumb 状态下执行。倘若写了0,则视为转入ARM 模式,CM3 将产生一个fault 异常。

正因为上述原因,图9.2.3.3 中使用0x0800 01CD 来表达地址0x0800 01CC。当0x0800 01CD处的指令得到执行后,就正式开始了程序的执行(即去到C 的世界)。所以在此之前初始化MSP是必需的,因为可能第1 条指令还没执行就会被NMI 或是其它fault 打断。MSP 初始化好后
就已经为它们的服务例程准备好了堆栈。

STM32 启动文件分析就给大家介绍到这里,更多内容请看《STM32 启动文件浅析》。

map 文件分析

MDK 编译生成文件简介

MDK 编译工程,会生成一些中间文件(如.o、.axf、.map 等),最终生成hex 文件,以便下载到MCU 上面执行,以STM32F103 开发板HAL 库例程的实验1 跑马灯实验为例(其他开发板类似),编译过程产生的所有文件,都存放在Output 文件夹下,如图9.3.1.1 所示:


这里总共生成了43 个文件,共11 个类型,分别是:.axf、.crf、.d、.dep、.hex、.lnp、.lst、.o、.htm、
bulild_log.htm 和.map。43 个文件(勾选Browse informatio-n 时为59 个)看着不是很多,但是随着工程的增大,这些文件也会越来越多,大项目编译一次,可以生成几百甚至上千个这种文件,不过文件类型基本就是上面这些。

对于MDK 工程来说,基本上任何工程在编译过程中都会有这11 类文件,常见的MDK编译过程生产文件类型如表9.3.1.1 所示:

文件类型说明
.o可重定向(注1)对象文件,每个源文件(.c/.s 等)编译都会生成一个.o 文件
.axf由ARMCC 编译生产的可执行对象文件,不可重定向(注2)(绝对地址);多个.o 文件链接生成.axf 文件,我们在仿真的时候,需要用到该文件
.hexIntel Hex 格式文件,可用于下载到MCU,.hex 文件由.axf 文件转换而来
.crf交叉引用文件,包含浏览信息(定义、标识符、引用)
.d由ARMCC/GCC 编译生产的依赖文件(.o 文件所对应的依赖文件)每个.o 文件,都有一个对应的.d 文件
.dep整个工程的依赖文件
.lnpMDK 生成的链接输入文件,用于命令输入
.lstC 语言或汇编编译器生成的列表文件
.htm链接生成的列表文件
.build_log.htm最近一次编译工程时的日志记录文件
.map连接器生成的列表文件,对分析程序存储占用情况非常有用

注1,可重定向是指该文件包含数据/代码,但是并没有指定地址,它的地址可由后续链接的时候进行指定。

注2,不可重定向是指该文件所包含的数据/代码都已经指定地址了,不能再改变。

map 文件分析

.map 文件是编译器链接时生成的一个文件,它主要包含了交叉链接信息。通过.map 文件,我们可以知道整个工程的函数调用关系、FLASH 和RAM 占用情况及其详细汇总信息,能具体到单个源文件(.c/.s)的占用情况,根据这些信息,我们可以对代码进行优化。.map文件可以分为以下5 个组成部分:

  • 1,程序段交叉引用关系(Section Cross References):描述各文件之间函数调用关系
  • 2,删除映像未使用的程序段(Removing Unused input sections from the image):描述工程中未用到而被删除的冗余程序段(函数、数据)
  • 3,映像符号表(Image Symbol Table):描述各符号(程序段、数据)在存储器中的地址、类型、大小等
  • 4,映像内存分布图(Memory Map of the image):描述各个程序段(函数)在存储器中的地址及占用大小
  • 5,映像组件大小(Image component sizes):给出整个映像代码(.o)占用空间汇总信息

map 文件的MDK 设置

要生成map 文件,我们需要在MDK 的魔术棒→Listing 选项卡里面,进行相关设置,如图9.3.2.1.1 所示:

图9.3.2.1.1 中红框框出的部分就是我们需要设置的,默认情况下,MDK 这部分设置就是全勾选的,如果我们想取消掉一些信息的输出,则取消相关勾选即可(一般不建议)。

如图9.3.2.1.1 设置好MDK 以后,我全编译当前工程,当编译完成后(无错误),就会生成.map 文件。在MDK 里面打开.map 文件的方法如图9.3.2.1.2 所示:

1,先确保工程编译成功(无错误)。
2,双击LED,打开.map 文件。
3,map 文件打开成功。

map 文件的基础概念

为了更好的分析map 文件,我们先对需要用到的一些基础概念进行一个简单介绍,相关概念如下:

⚫ Section:描述映像文件的代码或数据块,我们简称程序段
⚫ RO:Read Only 的缩写,包括只读数据(RO data)和代码(RO code)两部分内容,占用FLASH 空间
⚫ RW:Read Write 的缩写,包含可读写数据(RW data,有初值,且不为0),占用FLASH(存储初值)和RAM(读写操作)
⚫ ZI:Zero initialized 的缩写,包含初始化为0 的数据(ZI data),占用RAM 空间
⚫ .text:相当于RO code
⚫ .constdata:相当于RO data
⚫ .bss:相当于ZI data
⚫ .data:相当于RW data

map 文件的组成部分说明

我们前面说map 文件分为5 个部分组成,下面以STM32F103 开发板HAL 库例程的实验1 跑马灯实验为例,简要讲解一下。

1.程序段交叉引用关系(S S ection Cross References s )
这部分内容描述了各个文件(.c/.s 等)之间函数(程序段)的调用关系,举个例子如图9.3.2.3.1 所示:

上图中,框出部分:main.o(i.main) refers to sys.o(i.sys_stm32_clock_init) for sys_stm32_
clock_init 表示:main.c 文件中的main 函数,调用了sys.c 中的sys_stm32_clock_init 函数。其中:i.main 表示main 函数的入口地址,同理i.sys_stm32_clock_init 表示sys_stm32_clock_init 函数的入口地址。

2. 删除映像未使用的程序段(Removing Unused input sections from the image)

这部分内容描述了工程中由于未被调用而被删除的冗余程序段(函数/数据),如图9.3.2.3.2 所示:


上图中,列出了所有被移除的程序段,比如usart.c 里面的usart_init 函数就被移除了,因为该例程没用到usart_init 函数。

另外,在最后还有一个统计信息:216 unused section(s) (total 15556bytes) removed from the image. 表示总共移除了216 个程序段(函数/数据),大小为15556 字节。即给我们的MCU 节省了15556 字节的程序空间。

为了更好的节省空间,我们一般在MDK→魔术棒→C/C++选项卡里面勾选:One ELFSection per Function,如图9.3.2.3.3 所示:

3. 映像符号表(Image Symbol Table)
映像符号表(Image Symbol Table)描述了被引用的各个符号(程序段/数据)在存储器中的存储地址、类型、大小等信息。映像符号表分为两类:本地符号(Local Symbols)和全局符号(Global Symbols)。

本地符号(Local Symbols)记录了用static 声明的全局变量地址和大小,c 文件中函数的地址和用static 声明的函数代码大小,汇编文件中的标号地址(作用域:限本文件)。

全局符号(Global Symbols)记录了全局变量的地址和大小,C 文件中函数的地址及其代码大小,汇编文件中的标号地址(作用域:全工程)。

4. 映像内存分布图(Memory Map of the image)

映像文件分为加载域(Load Region)和运行域(Execution Region)。一个加载域必须有至少一个运行域(可以有多个运行域),而一个程序又可以有多个加载域。加载域为映像程序的实际存储区域,而运行域则是MCU 上电后的运行状态。加载域和运行域的简化关系(这里仅表示一个加载域的情况)图,如图9.3.2.3.4 所示:

由图可知,RW 区也是存放在ROM(FLASH)里面的,在执行main 函数之前,RW(有初值且不为0 的变量)数据会被拷贝到RAM 区,同时还会在RAM 里面创建ZI 区(初始化为0 的变量)。

5. 映像组件大小(Image component sizes)
映像组件大小(Image component sizes)给出了整个映像所有代码(.o)占用空间的汇总信息。这部分是程序实际功能可执行代码的存储空间。


由于篇幅较长,更多内容请大家查阅《STM32 MAP 文件浅析》文档的内容。

串口IAP实验

IAP,即在应用编程,通俗地说法就是“程序升级”。产品阶段设计完成后,在脱离实验室
的调试环境下,如果想对产品做功能升级或BUG 修复会十分麻烦,如果硬件支持,在出厂时预留一套升级固件的流程,就可以很好解决这个问题,IAP 技术就是为此而生的。在之前的FLASH模拟EEPROM 实验里面,我们学习了STM32F103 的FLASH 自编程,本章我们将结合FLASH自编程的知识,通过STM32F103 的串口实现一个简单的IAP 功能。

IAP 简介

IAP(In Application Programming)即在应用编程。在讲解STM32 的启动模式时我们已经知道STM32 可以通过设置MSP 的方式从不同的地址启动:包括Flash 地址、RAM 地址等,在默认方式下,我们的嵌入式程序是以连续二进制的方式烧录到STM32 的可寻址Flash 区域上的。
如果我们用的Flash 容量大到可以存储两个或多个的完整程序,在保证每个程序完整的情况下,上电后的程序通过修改MSP 的方式,就可以保证一个单片机上有多个有功能差异的嵌入式软件,这就是我们要讲解的IAP 的设计思路。

IAP 是用户自己的程序在运行过程中对User Flash 的部分区域进行烧写,目的是为了在产
品发布后可以方便地通过预留的通信口对产品中的固件程序进行更新升级,由于用户可以自定义通讯方式和自定义加密,使得IAP 在使用上非常灵活。通常实现IAP 功能时,即用户程序运行中作自身的更新操作,需要在设计固件程序时编写两个项目代码,第一个程序检查有无升级需求,并通过某种通信方式(如USB、USART)接收程序或数据,执行对第二部分代码的更新;
第二个项目代码才是真正的功能代码。这两部分项目代码都同时烧录在User Flash 中,当芯片上电后,首先是第一个项目代码开始运行,它做如下操作:
1)检查是否需要对第二部分代码进行更新
2)如果不需要更新则转到4)
3)执行更新操作
4)跳转到第二部分代码执行

第一部分代码必须通过其它手段,如JTAG、ISP 等方式烧录,常常是烧录后就不再进行更
改;第二部分代码可以使用第一部分代码IAP 功能烧入,也可以和第一部分代码一起烧入,以后需要程序更新时再通过第一部分IAP 代码更新。

我们将第一个项目代码称之为Bootloader 程序,第二个项目代码称之为APP 程序,他们存
放在STM32F103 内部FLASH 的不同地址范围,一般从最低地址区开始存放Bootloader,紧跟其后的就是APP 程序(注意,如果FLASH 容量足够,是可以设计很多APP 程序的,本章我们只讨论一个APP 程序的情况)。这样我们就是要实现2 个程序:Bootloader 和APP。

STM32F1 的APP 程序不仅可以放到FLASH 里面运行,也可以放到SRAM 里面运行,本
章,我们将制作两个APP,一个用于FLASH 运行,一个用于内部SRAM 运行。

我们先来看看STM32F1 正常的程序运行流程(为了方便说明IAP 过程,我们先仅考虑代
码全部存放在内部FLASH 的情况),如图59.1.1 所示:

STM32F1 的内部闪存(FLASH)地址起始于0X0800 0000,一般情况下,程序文件就从此
地址开始写入。此外STM32F103 是基于Cortex-M3 内核的微控制器,其内部通过一张“中断向量表”来响应中断,程序启动后,将首先从“中断向量表”取出复位中断向量执行复位中断程
序完成启动,而这张“中断向量表”的起始地址是0x08000004,当中断来临,STM32F103 的内部硬件机制亦会自动将PC 指针定位到“中断向量表”处,并根据中断源取出对应的中断向量执行中断服务程序。

在图59.1.1 中,STM32F103 在复位后,先从0X08000004 地址取出复位中断向量的地址,
并跳转到复位中断服务程序,如图标号①所示;在复位中断服务程序执行完之后,会跳转到我们的main 函数,如图标号②所示;而我们的main 函数一般都是一个死循环,在main 函数执行过程中,如果收到中断请求(发生了中断),此时STM32F103 强制将PC 指针指回中断向量表处,如图标号③所示;然后,根据中断源进入相应的中断服务程序,如图标号④所示;在执行完中断服务程序以后,程序再次返回main 函数执行,如图标号⑤所示。

当加入IAP 程序之后,程序运行流程如图59.1.2 所示:

在图59.1.2 所示流程中,STM32F103 复位后,还是从0X08000004 地址取出复位中断向量
的地址,并跳转到复位中断服务程序,在运行完复位中断服务程序之后跳转到IAP 的main 函数,如图标号①所示,此部分同图59.1.1 一样;在执行完IAP 以后(即将新的APP 代码写入STM32F103 的FLASH,灰底部分。新程序的复位中断向量起始地址为0X08000004+N+M),跳转至新写入程序的复位向量表,取出新程序的复位中断向量的地址,并跳转执行新程序的复位中断服务程序,随后跳转至新程序的main 函数,如图标号②和③所示,同样main 函数为一个死循环,并且注意到此时STM32F103 的FLASH,在不同位置上,共有两个中断向量表。

在main 函数执行过程中,如果CPU 得到一个中断请求,PC 指针仍然会强制跳转到地址
0X08000004 中断向量表处,而不是新程序的中断向量表,如图标号④所示;程序再根据我们设置的中断向量表偏移量,跳转到对应中断源新的中断服务程序中,如图标号⑤所示;在执行完中断服务程序后,程序返回main 函数继续运行,如图标号⑥所示。

通过以上两个过程的分析,我们知道IAP 程序必须满足两个要求:
1)新程序必须在IAP 程序之后的某个偏移量为x 的地址开始;
2)必须将新程序的中断向量表相应的移动,移动的偏移量为x;
对STM32F1 系列来说,闪存编程一次可以写入16 位(半字)。闪存擦除操作可以按页面擦
除或完全擦除(全擦除)。全擦除不影响信息块。根据类别的不同,Flash 有如下区别:
⚫ 小容量产品主存储块最大为4K×64 位,每个存储块划分为32 个1K 字节的页。
⚫ 中容量产品主存储块最大为16K×64 位,每个存储块划分为128 个1K 字节的页。
⚫ 大容量产品主存储块最大为64K×64 位,每个存储块划分为256 个2K 字节的页。
⚫ 互联型产品主存储块最大为32K×64 位,每个存储块划分为128 个2K 字节的页
使用时我们需要根据自己的芯片型号来选择,设计IAP 程序时需要严格避免不同的程序占
用相同Flash 扇区的情形。

本章,我们有2 个APP 程序:
1,FLASH APP 程序,即只运行在内部FLASH 的APP 程序。
2,SRAM APP 程序,即只运行在内部SRAM 的APP 程序,其运行过程和图59.1.2 相似,
不过需要设置向量表的地址为SRAM 的地址。

1.APP 程序起始地址设置方法
APP 我们使用以前的例程即可,不过需要对程序进行修改,
默认的条件下,图中IROM1 的起始地址(Start)一般为0x08000000,大小(Size)为0x80000,即从0x08000000 开始的512K 空间为我们的程序存储区。

图59.1.3 中,我们设置起始地址(Start)为0X08010000,即偏移量为0x10000(64K 字节,即留给BootLoader 的空间),因而,留给APP 用的FLASH 空间(Size)为0x80000-0x10000=0x70000(448K 字节)大小了。设置好Start 和Szie,就完成APP 程序的起始地址设置。IRAM 是内存的地址,APP 可以独占这些内存,我们不需要修改。

注意:需要确保APP 起始地址在Bootloader 程序结束位置之后,并且偏移量为0X200 的倍
数即可(相关知识,请参考:http://www.openedv.com/posts/list/392.htm)。

这是针对FLASH APP 的起始地址设置,如果是SRAM APP,那么起始地址设置如图59.1.4
所示:

这里我们将IROM1 的起始地址(Start)定义为:0X20001000,大小为0XD000(52K 字节),即从地址0X20000000 偏移0X1000 开始,存放SRAM APP 代码。这个分配关系大家可以根据自己的实际情况修改,由于STM32F103ZE 只有一个64K 的片内SRAM,存放程序的位置与变量的加载位置不能重复,所以我们需要设置IRAM1 中的地址到SRAM 程序空间之外。

关于APP 起始地址的设置方法,我们就介绍到这里,大家可以根据自己项目的实际需求进
行修改。

  1. 中断向量表的偏移量设置方法

VTOR 寄存器存放的是中断向量表的起始地址。默认的情况它由BOOT 的启动模式决定,
对于F103 来说就是指向0x0800 0000 这个位置,也就是从默认的启动位置加载中断向量等信息,不过ST 允许重定向这个位置,这样就可以从Flash 区域的任意位置启动我们的代码了。我们可以通过调用sys.c 里面的sys_nvic_set_vector_table 函数实现,该函数定义如下:

/**
 * @brief 设置中断向量表偏移地址
 * @param baseaddr: 基址
 * @param offset: 偏移量
 * @retval 无
 */
void sys_nvic_set_vector_table(uint32_t baseaddr, uint32_t offset)

        /* 设置NVIC的向量表偏移寄存器,VTOR低9位保留,即[8:0]保留*/
        SCB->VTOR = baseaddr | (offset & (uint32_t)0xFFFFFE00);

该函数用于设置中断向量偏移,baseaddr 为基地址(即APP 程序首地址),Offset 为偏移量,需要根据自己的实际情况进行设置。比如FLASH APP 设置中断向量表偏移量为0x10000,调用情况如下:

/* 设置中断向量表偏移量为0X10000 */
sys_nvic_set_vector_table(FLASH_BASE,0x10000正点原子STM32(基于HAL库)1

正点原子STM32(基于HAL库)3

正点原子STM32(基于HAL库)2

正点原子STM32(基于HAL库)2

正点原子STM32(基于HAL库)4

正点原子STM32(基于HAL库)3