深度剖析STM32内存地址与启动过程

STM32介绍

STM32是意法半导体推出的低成本、高性能、低功耗的单片机,主流产品(STM32F0、STM32F1、STM32F3)、超低功耗产品(STM32L0、STM32L1、STM32L4、STM32L4+)、高性能产品(STM32F2、STM32F4、STM32F7、STM32H7)。
如下图所示,STM32代表此款单片机为32位单片机,F表示产品类型为基础增强型,051代表此款单片机的系列为入门级单片机,R表示单片机引脚数目为64引脚,8表示单片机的闪存容量也就是FLASH容量为64Kbytes,T代表封装为QFN封装,6表示温度范围。所有的STM32单片机均遵循此规则。
在这里插入图片描述

STM32寄存器映射

我们以STM32F103VET6为例
在这里插入图片描述
存储器映射就是用地址来表示对象的意思,因为STM32是32位的单片机,因此其PC指针可以指向2^32=4G的地址空间,也就是图中的 0x00000000到0xFFFFFFFF的区间,也就是将程序存储器、数据存储器、寄存器和输入输出端口被组织在同一个4GB的线性地址空间内,数据字节以小端格式存放在存储器中。
现在,我们从下到上依次梳理STM32寄存器映射关系图:
所有的Reserved均为保留未开放的。
0x0000 0000 - 0x0007 FFFF:此为闪存或系统内存,具体取决于BOOT的引脚设置;
0x0800 0000 - 0x0807 FFFF:此为512K的闪存,用于保存用户编写的程序code以及一些静态变量,这也就是程序为什么是从0x8000000开始执行的原因;
0x1FFF F000 - 0x1FFF F7FF:这2K的闪存是系统内存,是厂家出场时烧录的固件,此固件不可修改;
0x1FFF F800 - 0x1FFF F80F:此16个字节的内存是option选择字节;
以上就是第0块512Mbyte的内存分配。
0x2000 0000 - 0x2000 FFFF:此为64K的SRAM地址区域,单片机运行时静态RAM地址就全部包含在内;
以上就是第1块512Mbyte的内存分配。
第三块512Mbyte的内存是我们关心的重点,这里的地址映射了关于STM32所有的外设,我们所操作的所有外设寄存器地址均包含在内(除了FSMC),所有外设的基地址也都是从0x4000 0000开始偏移的原因,这在库函数中可以查看到。
这些地址就不一一介绍了,图中都可以清晰的看到每个外设的地址,通过查询寄存器手册,就可以查看到在这个地址范围内包含的所有的寄存器地址,通过修改寄存器地址的配置就可以去操作STM32的各种外设了。
在51单片机的头文件中会经常看到sfr P0=0x80之类的语句,sfr就是一种扩充的数据类型,它的意思是用一个0-255的8位的内存单元可以访问51单片机内部的寄存器,然后我们设置P0口的寄存器方法就变成了P0=value。那么在STM32中也可以用相同的办法去实现51单片机中的位操作,因为STM32的外设寄存器映射地址是0x40000000,所以所有的外设都是由此基地址进行偏移得到的。
下图是GPIO的结构体定义,定位到GPIOA之后就发现是一个宏定义#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE),可以看到这句话是操作GPIOA是将GPIOA BASE的地址强制转换为GPIO的结构体指针。
在这里插入图片描述
当跟进GPIOA BASE之后就发现GPIOA BASE的宏定义是(APB2PERIPH_BASE + 0x0800),接着我们继续跟进,直到最后看到#define APB2PERIPH_BASE (PERIPH_BASE + 0x10000)和#define PERIPH_BASE ((uint32_t)0x40000000),所以根据0x40000000为基地址加上所有外设的偏移地址就可以得到所有外设的地址。通过查询STM32中文参考手册得出,STM32的GPIOA地址为0x40010800 - 0x40010BFF,这正好验证了程序中的地址偏移。那么同样的道理,既然知道了GPIOA的地址,那么GPIOA上有7个寄存器也就是依靠GPIOA的基地址偏移而来的,这个偏移量可以寄存器映射表中进行查询。因为GPIOA的寄存器是以结构体的形式进行定义的,而且又因为结构体的地址是连续的这一规律就可以计算出来每一个寄存器对应的地址。当我们操作函数库中的GPIO->CRL时,其实也就是操作了地址在0x40010800位置的寄存器,所以这与直接操作51单片机中的P0原理是相同的。

STM32时钟

STM32总线

STM32总线分为AHB和APB,类似于PC系统里的南桥北桥,一条高速总线和一条低速总线。AHB是高级高性能总线,这是一种系统总线,APB是外围总线,主要用于一些外设之间的连接,比如串口、IIC,SPI之类的外设,在APB1主要负责DAC、CAN、2345串口和一些普通的定时器,APB2主要负责ADC、IO、高级定时器和串口1。AHB是高级高速总线,连接的是内部高速外设,比如FSMC、SDIO中断和FLASH以及SRAM。这两者都是总线并且符合AMBA规范。
在这里插入图片描述

STM32时钟系统

STM32时钟树

在STM32F1系列中,一共有五个是中原,分别是HSI、HSE、LSI、LSE以及PLL。
HSI:此为高速内部时钟,内部RC振荡器,默认频率为8Mhz;
HSE:高速外部时钟,接外部振荡器或者时钟源,频率范围为4~16Mhz;
LSI:低速内部时钟,默认频率为40Khz;
LSE:低速外部时钟,一般接32.768Khz的晶体振荡器;
PLL:锁相环倍频器,可配置其输入时钟并且倍频范围为2~16倍,在F103系列中,PLL最大输出频率不得超过72Mhz;
在用户配置中,用户可以根据需要对预分频器进行配置,可以设置AHB、APB总线的频率,其中AHB和APB2的最大频率不得超过72Mhz,APB1最大频率不得超过36Mhz。其中SDIO的接口不能改变,固定频率为HCLK/2.内部低速时钟供看门狗以及RTC使用,因为在单片机编程中牵扯到低功耗有时候需要关闭总线时钟。STM32只有一个全速的USB接口,该时钟源只能从PLL中获取,也就意味着如果要使用USB模块,必须使能倍频器,USB模块需要一个48Mhz的时钟源,所以PLL的时钟频率必须配置为48Mhz或者72Mhz,因为可选择1分频和1.5分频,当选择1分频时选择48Mhz,1.5分频时选择72Mhz。在STM32的PLL时钟也可以通过配置利用IO引脚输出用于时钟信号输出。
在这里插入图片描述

STM32时钟配置

在这里插入图片描述
如上图所示,一般情况下我们会优先选择外部时钟源,因为外部时钟源受温度影响较小并且更加稳定,晶振一般选择使用8Mhz或者12Mhz,在这里我们以8Mhz为例。
HSE为8Mhz晶振,通过PLL倍频器9倍频后变为72Mhz,此频率刚好为F103系列的最大值,此时,单片机的系统时钟就已确定为72Mhz,此时钟直接接入高级高速总线AHB与APB2中,APB1因为最大频率为36Mhz,所以进行2分频为36Mhz。这时,单片机系统时钟和两条总线的频率就已确定。

STM32内存

内存的分类

在STM32中有易失性存储器和非易失性存储器两种,易失性存储器的代表就是RAM。非易失性存储器的代表是FLASH。简单理解易失性存储器就是掉电数据全部丢失,不会保存数据,非易失性存储器掉电之后数据会进行保存。
RAM:RAM是随机存储器(Random Access Memory),属于易失性存储器。表示不但可以从中读取数据也可以向其中写入数据,当电源关闭时,所有的数据都会丢失。RAM分为两类,一类是SRAM称为静态存储器,另一种是DRAM成为动态存储器。静态存储器的优点是速度非常快,是目前内存中读写速度最快的,一般用做CPU的一级缓存以及二级缓存,缺点是非常昂贵。动态存储器的优点是价格比静态的存储器便宜,缺点就是速度稍微慢一点,一般计算机的内存就是DRAM。DRAM的改进型叫DDR RAM,这种DDR RAM在一个时钟周期可以读写两次数据,速度大步幅提升。那么既然RAM掉电数据就会丢失那么为什么还要使用它?是因为RAM相对于FLASH而言,速度快很多,所有的数据如果都在FLASH中进行读取操作会降低计算机的性能,所以在计算机运行时,会将一些暂存的数据和一些需要频繁读写的数据进行临时存放以便提高计算机运行速度。在单片机的RAM中使用的是SRAM。
FLASH:FLASH属于非易失性存储器,具备掉电数据保存的能力,我们常用的U盘就是这种内存。FLASH分两种NOR FLASH和NADA FLASH,NOR FLASH和RAM的读取方式类似并且采用了随机存储技术,STM32中的FLASH就是NOR FLASH。
NADA FLASH是一次读取需要读取一个扇区的方式,通常是512个字节,这种FLASH的优点是价格比较低廉,缺点是读取比较麻烦。

STM32的内存分配

在STM32中可供用户管理的就是0x8000000和0x20000000开始的FLASH和SRAM部分,在FLASH中主要用于存储我们的代码和一些静态变量,在SRAM中保存一些运行时的参数和堆栈。
主存储器:该部分用于存放用户编写的代码以及协议数据常量,比如用const修饰的数据,起始地址从0x8000000开始运行代码,只有当BOOT0和BOOT1都接地的时候系统上电会自动开始运行此处的代码。
信息块:此部分是ST官方出场时写入的,无法更改,此部分分为ISP串口程序下载的引导程序以及BOOT的选择程序两部分。
SRAM
因为SRAM是易失性存储器,所以在单片机运行前此部分是没有任何数据的,只有当单片机在运行时,此部分可以保存程序运行时的临时数据以及局部变量和寄存器数据。
在这里插入图片描述
在STM32中分为RAM和FALSH两部分存储区,RAM中有ZI和RW段,FLASH中有RM段,ZI段中bss为存储之后未初始化的数据或者初始化之后为0的全局变量和静态变量,heap是堆区,用于存放程序运行中的数据,这部分的是可供程序员操作的ram区域,但只有使用malloc和free函数时堆区才有用。stack是栈区,此部分时存放局部变量和函数的返回值以及参数和寄存器的地方,当函数递归过多并且函数中数据量过大就会造成栈溢出导致程序死机或者出现严重问题。data段时用于存放已经初始化不为0的全局变量和局部静态变量。text段存储用户程序。constdata存储用const修饰的常量,无论是局部的还是全局的都保存在这里。
在这里插入图片描述
code就是代码部分,存储用户代码,RO-DATA是只读区域,const修饰的数据以及常量保存在这里,RW-DATA是可读可写的数据,已经初始化的全局变量和静态变量(包括局部静态变量)都存放在这里,ZI-DATA保存的是初始化为0的全局变量以及静态变量和未初始化的变量。

STM32启动过程

启动文件分析

无论哪种单片机都必须有启动文件,启动文件顾名思义就是引导单片机如何运行的文件,因为我们程序存放的地址是0x8000000,那么单片机要运行到这个位置开始运行我们的程序就需要启动文件,STM32的启动文件是startup_xxxx.s,因为Cortex-M3 内核规定,0x00000000起始地址处必须存放栈顶指针,栈顶指针指向中断向量表入口,在中断向量表中进行复位以及跳转程序。在Cortex-M3 内核固定了中断向量表的位置,但是起始地址是可以根据用户需求修改的。

开辟堆栈空间

在程序开始运行时首先需要开辟堆栈的空间,用于局部变量和函数的调用工作。EQU为伪指令,类似于C语言中的宏定义。开辟堆是用于动态内存分配,如果程序员使用malloc和free函数时此区域才会有作用。
在这里插入图片描述

初始化中断向量表

标号_Vectors表示中断向量表的入口
标号 __Vectors_End,表示中断向量表的结束地址
标号__Vectors_Size,表示中断向量表的长度
DCD指令的意思是开辟一段空间,作用等价于C语言中的取址符,每个成员都是一个函数指针,指向所对应的终端服务函数;
在这里插入图片描述

系统启动

在STM32上电或者复位之后首先执行的就是在中断服务程序中的Reset_Handle,此函数为经过week修饰的弱函数,我们可以自己在外部进行编写,在复位之后会跳转到_main函数中,也就是跳转到我们编写的void main函数中开始执行我们的程序。
在这里插入图片描述

中断服务程序开启

中断服务程序都是死循环,也就意味着如果开启了哪个中断服务但是并没有编写中断服务程序那么程序就会一直陷入死循环状态,系统开启之后首先会开始系统中断服务程序,接下来会运行外设中断服务程序。无论哪种中断程序都是week生命的也就是弱函数,程序员都是可以重新编写的。
在这里插入图片描述

初始化堆栈

文件的最后就是初始化堆栈。
在这里插入图片描述

STM32 NVIC中断

Cortex-M3内核一共支持256个中断,包含了16个内核中断和240和外部中断,但是STM32并未使用Cortex-M3内核的全部功能,只是使用了一部分,所以STM32一共包含了84个中断,其中包括16个内核异常中断和68个可屏蔽中断。但是在F103系列只包含了60个可屏蔽中断,所以我们能使用的就是这16个内核异常中断和60个可屏蔽中断。
STM32可以将中断分成5个组,分别为组0到组4,设置分组的是由SCB->AIRCR寄存器来设置,关系表如下图。
在这里插入图片描述
通过这张表我们就可以根据自己的需求去进行设置,如果说我们设置为组2,那么就有两位抢占优先级和两位响应优先级,抢占优先级级别高于响应优先级,抢占优先级可以打断响应优先级,但是响应优先级无法打断抢占优先级,并且如果一个中断的抢占优先级和相应优先级都相同,那么就看在启动文件中谁在前那么谁先执行。优先级设置为0-15,0为最高优先级,15为最低优先级,此优先级和操作系统中的任务优先级不同。我们这里说的优先级指的是硬中断,而操作系统中的任务优先级是软中断,无论软中断的优先级再高也比硬件中断的优先级低。

内核异常

中断属于内核异常的一种,任何异常都可以打断程序的正常执行流。Cortex-M3内核一共支持256个中断,其中包含16个内核异常,但是STM32F103系列则只包含10个系统异常和60个外部中断,编号0-15为系统异常,优先级为-3到6,16以上均为外部中断。其中优先级最高的是复位,优先级为-3,当引脚检测到复位指令后无论什么情况都会直接执行复位功能,-2的是NMIN,此为不可屏蔽中断,-1是硬件故障。这三个内核异常属于最高级别的内核异常并且不允许用户修改。

外部中断

内核异常STM32中排到了6,所以7以上的都属于外部中断,STM32将外部中断进行了分类,比如编号7的是看门狗中断,后面的依次进行排序。不过外部中断所有的优先级都是用户可以进行修改的,根据NVIC寄存器修改有几个抢占优先级有几个响应优先级然后对外部中断进行优先级管理。

STM32程序架构

常见程序架构

在我们对单片机程序进行编程时,程序的架构直接影响到程序的效率和维护难度,好的程序架构在后期会有非常大的优势,无论从维护以及程序稳定性和程序执行效率上说都非常棒。我们常见的单片机程序架构基本分三种,分别为大循环程序,前后台程序和操作系统程序。嵌入式编程的灵魂就在于实时性,实时性这三个字是嵌入式编程的核心,所有的工作都是围绕这三个字展开的。

大循环程序

大循环程序没有运用中断,没有定时器中断也没有外部中断,在一些很小并且对实时性要求比较低的项目上可能会用到,不过目前已经很少见了。这类程序架构只有简单的循环,比如灯的闪烁或者很简单的程序,在此种程序架构中只有一个while无限循环。此种程序架构的优点是编写非常简单,程序出问题后容易排查,缺点是实时性较差。

前后台程序

前后台程序就是在大循环程序中加入了中断机制,中断可以在任意时间打断程序执行跳入中断服务程序中执行程序,中断服务程序执行结束后进入后台程序继续执行主程序,在前后台程序中,中断充当前台程序,主程序充当后台程序,在没有中断发生时,程序一直会执行主程序,当有中断发生时,程序立即进入中断程序中执行。此种程序架构也称为裸机程序,是目前使用最普遍的程序。
在中断程序中编写中断服务程序时要注意,程序不能有延时更不能有死等,必须短小精悍,比如可以在中断服务程序中标志位置位或者使能一些寄存器操作。此种程序架构的优点时实时性高,程序编写相对容易,缺点是随着程序的增大,虽然在中断发生后在中断服务程序中修改了标志位,但是轮询时间过长导致实时性变差,但是随着单片机主频越来越高速度越来越快这部分问题会越来越好。

操作系统

嵌入式常用的操作系统有FreeRTOS、μC/OS以及国产操作系统RT-Thread。操作系统常用的机制有抢占式调度、时间片轮转调度以及协同式三种,最常用的是抢占式调度。运用了操作系统后,程序中任务调度以及任务切换都由操作系统来完成,在最常用的FreeRTOS中使用systick以及pendsv和svc三个内核异常中断,systick用于定时进行任务优先级查询,pendsv是可挂起的中断服务,在pendsv中进行任务切换,svc用于任务的启动。systick一般设置切换频率为1ms,也就是程序执行1ms就会被打断进行任务切换。操作系统中的任务分为四种状态,分别为运行态,就绪态、阻塞态以及挂起态。
运行状态顾名思义就是程序正在运行的状态,阻塞态就是由于任务被信号量或者事件标志阻塞时的状态,当这些事件发生或者信号量被释放的时候任务就会由阻塞态变为就绪态,任务调度器再根据任务的优先级进行任务调度。就绪态指的是当任务由阻塞态或者挂起态由于信号量或者事件标志被释放后,或者任务被恢复后到达的状态,当进行任务调度器进行任务调度时,会根据任务的优先级对已就绪的任务进行执行,如果哪个任务处于就绪态并且任务优先级高那么就会得到执行。挂起态指的是由程序员将任务挂起,被挂起的任务不再参与任务的调度直到任务被恢复。
抢占式的操作系统程序在执行时每隔1ms任务调度器就会查询当前就绪态的任务,当发现有更高优先级的任务就绪时就会马上进行任务切换,这样保证了实时性,当有中断发生时,在中断中释放了某些信号量或者事件标志,在中断结束时进行任务切换,那么任务就不会等到被轮询到的时候才被执行,所以实时性比较高。
操作系统的优点就是实时性很强,任务划分清晰,缺点是对程序员的水平要求较高,并且会占用一部分单片机的FLASH和RAM。

常见驱动架构

这部分的内容就牵扯到了一些C语言的知识,写好一个驱动程序对C语言的要求比较高,要能灵活运用指针,数组以及结构体,并且对函数的重入性以及一些入参检查,除零检查和野指针检查需要有深入的理解。驱动架构有带入参和不带入参的形式,也有带返回值和不带返回值的形式,入参和出参可以是指针或者变量。写嵌入式软件驱动时需要注意,应当把驱动理解为一个通用的加工厂,我们在入参时规定的传入参数的类型,并且在参数传入后对其进行检查,后经过函数的处理进行输出,也需要注意可移植性和规范。
在本函数中,我们定义函数的返回值是一个浮点型,入参是从DS18B20温度传感器的序号(因为存在多个温度传感器),经过一系列的转换和运算之后根据温度是正值还是负值对数据进行输出,只要主函数对本驱动进行调用时入参填写要读取哪个温度传感器的值,那么就会得到这个传感器当前的温度数据,只需要利用一个局部变量来获取即可,本驱动程序仅仅实现温度获取的功能并且方便移植。
编写驱动函数要注意函数不能过长并且函数要实现的功能要单一,不能一个函数写了几百行或者在函数内实现了多个功能,这样会导致程序的通用性下降。
在这里插入图片描述
编写程序时以实时性为核心,遵循高内聚,低耦合的思想,这样写出来的程序不仅便于移植也方便后期维护与修改,如果在一个main函数中长篇大论,那么程序一旦出现问题,改一处而动全身,随着程序的功能越来越多维护与修改起来就会越来越麻烦。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值