STM32F0單片機快速入門三 MCU啟動過程

topsemic 發佈 2020-02-07T16:42:40+00:00

那麼從0x080000C8 + 0x4 + 0x10 = 0x080000DC 取出數據 0x0800092D 送至寄存器 R0。


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

關鍵字: