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