STM32F0单片机快速入门三 MCU启动过程
1.
MCU 代码如何启动首先我们需要澄清一个问题,什么是 Startup Code,什么是 Bootloader?因为总看到有同学混用这两个概念。
Bootloader 可以译为引导程序。早期的单片机是没有 Bootloader 这种概念的。如大家熟悉的 MCS51,最初芯片内是不能存储代码的,需要外挂EPROM,就是下面这种带个小玻璃窗的存储器。擦除 EPROM 中的代码需要用紫外线照射几分钟才行。
后来出现了 Flash 这种可电擦写的存储器,并集成在了单片机内部。但出厂的时候单片机的程序存储区仍然是空白的,没有任何代码。用户编译程序后,下载到单片机后才能运行。那么在产品发给用户后,如果发现有Bug怎么办呢?就得用编程器把新代码重新下载一次。这实在是有点儿麻烦,特别是如果客户距离很远的话。于是有聪明的程序猿想了一个办法,写一小段特殊的代码放在程序里,这段代码可以通过一定方式,比如用按键触发进入运行,它可以通过串口(早期的 PC 串口是标配)接收新的代码并写入Flash,从而在没有硬件编程器的情况下也能完成代码的更新。
程序猿们也是现代历史前进的重要推动力啊!
后来,有芯片厂商把这种代码在出厂时就固化在芯片里,极大的方便了代码下载和程序更新。STM32F030内部就固化了Bootloader。当我们把一个引脚 BOOT0 拉高的同时,重新给芯片上电或复位,就会触发Boootloader进入运行。此时我们通过单片机的串口就可以把新程序发送给单片机,发送完后把 BOOT0 拉低,再复位单片机,新程序就会运行起来。
Startup Code 可以译为启动代码。单片机上电或复位后最先执行的一段代码。一般主要会完成堆栈指针的设置,复位向量的获取和加载,然后初始化变量,最后跳转到用户代码。在详细看启动代码之前,我们先看一下 STM32F030 的内存映射。
2. STM32F030内存映射(Memory Map)
下面是 STM32F030 的内存映射,其它芯片会因为 Flash,SRAM 空间大小不同而略有不同。
因为是32位机,所以可寻址从 0x0000_0000 到 0xFFFF_FFFF 的总共 4G 空间。
这是采用32位机的好处,地址空间足够用。不像8位或16位机,很容易出现地址空间不够用,动不动就需要用 Page 来间接寻址。
我们从低地址到高地址逐段看一下:
0x0000 0000 Virtual memory
这段地址空间,会因为不同的 BOOT 模式而映射到不同的物理内存。
当芯片复位,或从 Standby 低功耗模式唤醒时:
如果引脚 BOOT0 是被拉低的,将映射到 Flash memory。这是最常用的代码运行模式;
如果引脚 BOOT0 是被拉高的,且nBOOT1为 1 ,将映射到 System memory。进入bootloader模式;
如果引脚 BOOT0 是被拉高的,且nBOOT1为 0 ,将映射到 SRAM。
注:nBOOT1 为Flash寄存器中的一位,用户何以设置。
0x0800 0000 Flash memory
存放用户代码
0x1FFF EC00 System memory
存放 bootloader, 片内集成温度传感器的校正数据,和片内集成电压参考的校正数据
这些代码和数据是在工厂固化好的。
0x2000 0000 SRAM
存放用户变量,堆(Heap)和栈(Stack)。也可以把代码加载到 SRAM 运行。
0x4000 0000 Pheriperals
芯片集成的外设,如 USART, SPI, GPIO等的寄存器地址在这一区域。
0xE000 0000 Cortex-M0 internal pheriperals
M0内核的外设映射到此区域。如 systick (System Tick),NVIC,Debug Registers。这些寄存器在芯片手册里是查不到的,需要到 ARM 的手册里查找。
3. 启动代码(Startup Code)
我们还是以下面这个最简单的GPIO翻转代码为例:
STM32Cube_FW_F0_V1.11.0\Projects\STM32F030R8-Nucleo\
Examples\GPIO\GPIO_IOToggle\MDK-ARM\Project.uvprojx
把此工程下载到单片机后,用调试器观察下面两个地址的内容:
我们会发现0x0000_0000开始的区域, 和0x0800_0000开始的区域,内容完全相同。这说明 Flash 区的内容映射到了 0x0000_0000起始的这一段地址区域。
注意STM32F030使用的是小端模式(Litlle Edian)。
不同于 MCS51 在 0x0000 放的是复位向量,STM32F030 还有其它 ARM 芯片在零地址存放的是初始堆栈指针地址。
0x0000 0000: (0x2000 0428) 初始堆栈指针
0x0000 0004: (0x0800 00C9) 复位向量,上电或复位后最先加载入PC
注:单片机上电或复位后,堆栈指针初始化和 PC 初始值的加载总是从地址 0x0000_0000,0x0000_0004获取。在上面这种用户模式下,实际是从 Flash 区的 0x0800_0000,0x0800_0004 获取的。
我们可以通过调试器观察一下芯片复位后 M0 内核的寄存器:
细心的同学这时可能发现了一个问题。
堆栈指针 SP 的内容和前面存储器中的内容是对的上的。但是 PC 里的内容好像对不上啊?PC 里的值是 0x0800_00C8,存储器里明明是 0x0800_00C9 啊!
这里牵涉到了 ARM 体系里的两种工作状态 ARM 和 Thumb。ARM 状态下执行32位指令,Thumb状态下执行16位指令。那么如何在这两者之间切换呢,一个方法就是靠跳转地址的最低位(Bit0), 当 Bit0 设为 1 时进入 Thumb 状态,当 Bit0 设为 0 时进入 ARM 状态。
对于单片机来说,16位的 Thumb 指令就足够了,而且16位指令比32位指令能节省存储器空间。所以 M0 内核只支持 Thumb 指令。
到这里我们就可以理解复位向量为什么是 0x0800_00C9 了。
接下来我们来看复位向量 0x0800_00C8 指向的第一条指令:
单片机将要执行的第一条指令 0x4804,这是什么意思呢?
先说结论:它就是下图中,单片机复位后光标指向的这条指令:
LDR R0, =SystemInit
在这里详细解释一下 0x4804 这条指令:
它对应的机器码是 0100100000000100
Bit15 to Bit11 (01001)为LDR(literal)指令,既从PC偏移地址取数据送至寄存器Rt。
Bit10 to Bit8 (000)表明目的寄存器Rt为 R0
Bit7 to Bit0 (00000100)表明相对于 PC 的偏移量为 0b10000,既0x10。
注意PC的值是当前地址+4。
那么从 0x080000C8 + 0x4 + 0x10 = 0x080000DC 取出数据 0x0800092D 送至寄存器 R0。此地址是 SystemInit( )函数的地址。下一条语句 BLX R0 就是调用此系统初始化函数。
SystemInit( ) 这个函数在 system_stm32f0xx.c 这个文件里,主要完成系统时钟的初始化。可以点进去看一下具体的内容。
函数 SystemInit( ) 执行完之后,程序跳转回来,取得 __main( ) 函数的地址,跳转到 __main() 函数执行。需要注意,这个函数不是我们用户代码里的 main( ) 函数。
__main() 函数是 Keil 的库提供的,我们看不到代码,它主要完成变量的初始化。这里不用太纠结,如果想进一步深究可以看一下 ARM Compiler User Guide 的 Reset and initialization 这一节。
__main() 函数执行完,基本工作就做完了,这才跳转到用户代码的 main( ) 函数。
参考资料:
STM32F030 Datasheet
STM32F030 Reference Manual
ARM Compiler User Guide
ARM®v6-M Architecture Reference Manual
STM32F0单片机快速入门四:不看手册也行吗?
关注 TopSemic 让我们一起成长!
1. 第一个工程 翻转引脚
上一篇文章我们详细介绍了 STM32F030 从复位时取得复位向量,系统初始化,然后跳转到 main( ) 函数的过程。下面我们结合一个最简单的例子,对 Cube 库的使用做一个简单的介绍。
我们用 Keil 打开下面这个工程:
STM32Cube_FW_F0_V1.11.0\Projects\STM32F030R8-Nucleo\Examples\GPIO\GPIO_IOToggle\MDK-ARM\Project.uvprojx
编译下载运行此代码,会看到一个 LED灯(连至MCU的 PA5引脚)不停地闪烁。为了完成这个简单的功能,我们看到这个工程里包含了不少文件:
如果是初次用这种库的方式做开发,乍一看还真感觉有点乱。不过让我们一个一个看一下这些文件,理清它们的关系后就会体会到这种方式的巨大优点。
2. 文件分类解释
工程里的文件分为五大类:启动代码,M0内核初始化,驱动,板级支持包(BSP),用户代码。一般来说我们开发应用程序,主要关注用户代码文件就行了。如果硬件电路板做了改动,则修改BSP里的内容。
在早期的单片机开发中,芯片内资源很少,通常的情况是一个工程师就从硬件到软件编程都做了,是没有 BSP(Board Support Package)这种概念的。BSP概念来源于较复杂的CPU系统的开发,一般是厂家设计主板,并提供 BSP(包含启动代码,驱动,Bootloader等)。我们这里的 BSP 概念稍有不同,它是指对某一块儿以 MCU 为核心的电路板的支持代码包。启动代码,内核初始化和驱动,没有包含在内。BSP会调用驱动层的代码。
对于 STM32 Nucleo 这块儿开发板来说,板上资源很少,所以BSP只提供了相应的按键(BUTTON)和指示灯(LED)支持代码。里面的ADC,SPI,LCD等代码是支持其它板子的,可以先忽略。
启动代码
为理解汇编代码,我们先熟悉一下这些伪指令:
ALIGN 变量或代码对齐。如:
ALIGN = 3 以8(2的3次方)字节对齐。
EQU 给标号赋值。如:
Stack_Size EQU 0x400;
DCD 分配1个或多个字(words)的内存空间。如:
Data DCD 1,5,8; 定义3个字并赋值为 1,5 和 8。
AREA 定义一个代码或数据段(section),命名并指定属性。如:
AREA Func01, CODE, READONLY;
定义了一个名字为 Func01 的只读代码段。
SPACE 保留一段空间并初始化为 0。如:
Data SPACE 100; 为 Data 保留 100个字节初始化为 0 的内存空间。
IMPORT 导入其它文件中的标号,以在当前文件中引用。如:
IMPORT SystemInit
LDR R0, =SystemInit
BLX R0
从文件 system_stm32f0xx.c 中导入 SystemInit 这个函数并调用。
EXPORT 导出能被连接器(Linker)识别的标号。从ASM文件导出的标号可以在C中引用。
[WEAK] 如果在其它地方定义了相同的标号,则此处定义被覆盖。
PROC 定义一个函数的起始地址。
ENDP 标志当前函数结束。
例子:
SysTick_Handler PROC
EXPORT SysTick_Handler [WEAK]
B.
ENDP
导出 SysTick_Handler 这个中断处理函数。如果在其它地方定义了一个新的 SysTick_Handler 函数,那么新函数将覆盖此处定义的这个陷阱函数。汇编语句 B.为在当前语句死循环。
下面我们看一下启动文件 startup_stm32f030x8.s
定义堆和栈:
中断向量表:
现在这个工程用到的只有绿线框中的几个向量:
__initial_sp
初始堆栈指针
Reset_Handler
复位向量,我们在上一篇文章已经讲到如何从复位向量一步一步执行到用户代码中的主程序main( )。
SysTick_Handler
系统时钟中断向量。此程序每 1ms 产生一次中断。
需要注意的是 SysTick_Handler 这个中断处理函数在用户代码文件stm32f0xx_it.c
中进行了重定义,所以当 SysTick 中断发生时,实际会跳转到用户代码的中断处理函数,而不是跳到下图所示的汇编代码中断处理函数进入死循环。
再往下可以看到,对所有芯片级中断定义了一个共享的陷阱函数。用户在实际使用到某一个中断的时候,要在中断处理文件 stm32f0xx_it.c 中用相同的函数名定义,从而在中断发生时跳转到实际的中断处理函数。
在此文件的最下面的代码,是用来传递堆栈信息给库的:
在芯片资源比较少时,可以通过选中 Options for Target->Target->Use MicroLIB 选项,使用简化版的库来实现 printf 等操作。若资源充足时使用标准库,库将调用下面的 __user_initial_stackheap 函数来获得堆栈信息。
M0 内核初始化
system_stm32f0xx.c
此文件只有两个函数:
SystemInit( ),在启动代码中调用,把系统时钟复位到初始默认状态(8MHz的高频内部时钟 HSI)。
SystemCoreClockUpdate( ), 在用户调用库函数更改时钟配置后,需要调用此函数以更新全局系统时钟变量 SystemCoreClock。其它模块基于此时钟的计算才会正确。一般来说更改时钟配置的 HAL函数已经包含此函数的调用,如 HAL_RCC_ClockConfig( ), 无需用户再次调用。
驱动
stm32f0xx_hal_cortex.c
包含 Cortex 内核中两个重要模块的驱动:
可嵌套中断向量控制器 NVIC(Nested Vectored Interrupt Controller),
系统滴答时钟 SYSTICK。
stm32f0xx_hal.c
此文件包含用户程序必须首先调用的 HAL_Init( ),它会使能数据和指令缓存,预取指令队列;配置系统滴答时钟产生 1ms 中断;调用 HAL_MspInit( )回调函数。
HAL_MspInit( )函数用来做系统级的初始化,配置某一模块相关的 时钟,引脚,DMA,中断等资源,但是在所有的例程中都没有实际用到此函数。可以先忽略。
stm32f0xx_hal_rcc.c
stm32f0xx_hal_rcc_ex.c
RCC(Reset and Clock Controller)模块的驱动。一个模块为什么要两个驱动文件呢?前一个文件提供了基本的通用的功能驱动,后一个文件是扩展功能驱动,通常针对某一特定型号的芯片。如同我们吃饭需要餐具,_rcc.c 提供碗筷等常用必备工具,_rcc_ex 可能提供的就是酒杯,烛台等这些东西。
stm32f0xx_hal_gpio.c
GPIO 模块的驱动。
BSP 板级支持包
stm32f0xx_nucleo.c
针对 STM32 Nucleo 开发板的类型,宏定义,支持代码。
用户代码
main.c 主程序
stm32f0xx_it.c 中断处理
前面介绍了一大堆文件,主要是为了清除系统的工作流程。在开发中使用库还是很简单的。在主程序中调用库,只需要通过 main.h 包含下面这个头文件:
stm32f0xx_hal.h
如果有 BSP 则包含 BSP 的头文件,在本工程是:
stm32f0xx_nucleo.h
使用到哪个模块就在配置文件中打开使能该模块的宏定义。
stm32f0xx_hal_conf.h
然后第一步必须调用 HAL_Init( )。
第二步,如果希望系统时钟工作在默认内部时钟(8MHz HIS)以外的频率,则需要调用 SystemClock_Config( )。此函数又调用 HAL_RCC_ClockConfig( ) 完成新配置。
下面是应用代码:
所有模块一般都是这三个步骤:使能模块的时钟,初始化模块,使用模块的功能。
stm32f0xx_it.c 中的中断处理函数 SysTick_Handler( ) 很简单,每次进入就对滴答计时变量 uwTick 加1,其它 HAL 函数可以基于此变量计时。
我们将在接下来的文章中进一步谈一下库的使用,和如何用按键触发中断去控制 GPIO 的翻转。
参考资料:
Description of STM32F0 HAL and low-layer drivers
ARM Compiler armasm User Guide
STM32F030 Datasheet
STM32F030 Reference Manual
相关问答
fo在 单片机 中是什么意思?答:51单片机f0的意思就是指是用户在进行编程使用的PSW中的D5位,作为表示状态改变的一个标志!当出现的中断或者子程序里面的某个状态改变时,就设置这个标志,然...
单片机 中0x08代表什么意思呢尤其是其中?简单点说吧+-*/加减乘除是常用的运算,在计算机中还有%取余,也就是计算余数Num就是计算Num/10后的余数,相当于计算个位数,除此之外还有按位的计算,比如&(与),|...
单片机 储存器00H—7FH,80H— F0 H是什么意思?00H-7FH包括了工作寄存器区,位寻址区和堆栈区,可以直接寻址或者间接寻址80H-FFH?是堆栈或数据缓冲区,只能间接寻址C51语言么??00H-7FH包括了工作寄存器区,...
简述程序状态寄存器PSW各位的含义, 单片机 如何确定和改变当前工作寄存器组?进位、借位CY=1;否则CY=0.AC(PSW.6):即PSW的D6位,辅助进位、借位标志。当D3向D4有借位或进位时,AC=1;否则AC=0.F0(PSW.5及PSW.1):即PSW的D5位,用户...
【 单片机 中高四位和低四位是什么意思.能用恰当的比喻表达出...[最佳回答]你把一个字节的数据假设成0~99高四位就是十位低四位就是个位,二进和十进制的数据没有多大差别,类比一下就行实在不理解12345678十进制数高四...
单片机 晶振电路原理电容大小没有固定值.一般二三十p.晶振是给单片机提供工作信号脉冲的.这个脉冲就是单片机的工作速度.比如12M晶振.单片机工作速度就是每秒12M.和电...
STM32是什么,是32位的 单片机 吗?STM32是一款普通的单片机,一款能够让普通工科学校的孩子吃上饭的单片机。1、嵌入式软件工程师的一个方向,就是对STM32进行编程。一般谈起嵌入式,首先想到的...
在 单片机 中,负跳变和高脉冲是什么意思? - 懂得其实负跳变就是下降沿的意思,有人故意这样说,也就是电平由高变低的那个过程.高脉冲也就是正脉冲.负跳变指平由高变成低的过程,也叫下降沿,高脉冲就...
在 单片机 中,负跳变和高脉冲是什么意思? - 废纸再生 的回答 -...其实负跳变就是下降沿的意思,有人故意这样说,也就是电平由高变低的那个过程.高脉冲也就是正脉冲.负跳变指平由高变成低的过程,也叫下降沿,高脉冲就...
【 单片机 程序TMOD& =0x0f;TMOD|=0x01;有什么作用】作业帮[最佳回答]TMOD&=0x0F;按位与,就是把TMOD的最右边的二进制4位留着不变,其它位全部清0.即TMOD=TMOD&0x0f;[清T1,保留T0]TM...