為了順利過渡到庫開發,在STM32編程的開始,我們對照51點亮一個LED的方法,給大家演示一下STM32如何用操作寄存器的方法點亮一個LED,然后再慢慢講解到底什么是庫,讓大家知道庫跟寄存器的關系。
1. 用51點亮一個LED
在用STM32點亮一個LED之前,我們先來復習下用51如何點亮一個LED。
硬件上我們假設51單片機的P0口的第0位接了一個LED,負邏輯亮。如果我們要點亮這個LED,代碼上我們會這么寫:
P0 = 0XFE;//總線操作點亮 LED
這時候我們就把LED點亮了,如果要關掉LED ,則是:
P0 = 0XFF;//總線操作關閉 LED
這里面我們用的是總線操作的方法,即是對P0口的8個IO同時操作,但起作用的只是P0^0。
除了這種總線操作的方法,我們還學習過位操作,利用51編譯器的關鍵字sbit,我們可以定義一個位變量:
sbit LED = P0^0;
那么LED = 0,就點亮了LED;
LED = 1,就關閉了 LED。
為了讓程序看起來見名知義,我們定義兩個宏:
#define ON 0 #define OFF 1
點亮和關閉LED的代碼就變成了:
LED = ON;//位操作點亮LED LED = OFF;//位操作關閉LED
稍微整理一下代碼,整體效果就是:
//假設51單片機的PO~0口接LED,負邏輯點亮 #define ON 0 #define OFF 1 sbit LED = P0^0; void main(void) { P0 = 0XFE;//總線操作點亮LED P0 = 0XEF;//總線操作關閉LED LED = ON;//位操作點亮LED LED = OFF;//位操作關閉LED }
上面總線和位操作的的方法,學過51的朋友是非常熟悉的,也很容易理解。那么我們再說一下大家容易忽略的幾個知識點。
1. 什么是寄存器
在點亮 LED 的時候,我們都是用操作寄存器的方法來實現的,那大家是否想過,這個寄存器到底是什么?為什么我們可以直接操作P0口?
解答上面的問題之前,我們先簡單介紹下51單片機的主要組成部分,這對我們學習其他單片機也有好處。
我們以國內的STC89C51為例,該單片機主要由51內核、外設IP、和總線這三大部分組成。內核是由 Intel 公司生產的,外設 IP 就是 STC 公司在內核的基礎上添加的諸如定時器、串口、IO 口等這些東西,總線就是用來連接內核和外設的接口單元。Intel 在這里屬於IP核設計公司,STC 屬於 IC 設計公司。世界上能設計 IP 核的公司屈指可數。我們非常熟悉的ARM公司就屬於IP核設計公司,ARM 給其他公司授權,其他IC公司就在ARM內核上設計出各具特色的MCU,我們后面要學習的STM32就是屬於一中基於ARM內核的MCU。
寄存器則是內置於各個 IP 外設中,是一種用於配置外設功能的存儲器,就是一種內存,並且有想對應的地址。學過C語言我們就知道,要操作這些內存就可以使用C語言中的指針,通過尋址的方式來操作這些具有特殊功能的內存一寄存器。比如 P0 口對應的地址是0X80,那么我們要修改 0X80 這個地址對應的內存的內容的話,按照常理可以這樣操作:
*(*0X80) = 0XFE;//點亮LED
可當我們編譯的時候,編譯器會報錯,在51里面只能通過sfr和sbit這兩個關鍵字來實現寄存器映象,不能直接操作寄存器對應的地址,這是51相較於STM32不同的地方。
51單片機的這些寄存器位於地址80H~FFH中,對應着128個地址,但不是每個地址都是有效的,51系列的單片機有21個,52系列的則有26個,其他的都是保留區。

寄存器映射
2. 寄存器映射
實際上我們在編程的時候並不是通過指針來操作寄存器的,而是直接給P0、P1這些端口寄存器賦值。那么這些外設資源是如何與地址建立一一對應的關系(寄存器映射定義),這得益與51特有的兩個關鍵字:sfr和sbit,其他單片機沒有,只能用其他的方式來實現寄存器映射。這兩個關鍵字幫我們實現了所有寄存器的定義,所以我們才可以像操作普通變量一樣來操作寄存器。其實我們一開始提到的點亮LED的代碼,全貌應該是這樣的:
sfr P0 = 0x80;//寄存器定義 P0 = 0XFE;//總線操作點亮LED
為了方便起見,我們可以把寄存器映射全部寫好封裝在一個頭文件里面,不用每用一個寄存器就定義一次。其實這方面的工作不用我們做,我們在編程的時候都會在開始的地方添加一個頭文件:
#include <reg51.h>
這個頭文件已經實現了全部寄存器的定義,該文件是keil自帶,在安裝目錄:
Keil\C51\INC下可以找到。這個文件實現了字節寄存器和位寄存器的定義。
1 /*----------------------------------------------------------------- 2 3 REG51.H 4 5 Header file for generic 80C51 and 80C31 microcontroller. 6 7 Copyright(c)1988-2002 Keil Elektronik GmbH and Keil Software,Inc. 8 9 All rights reserved. 10 11 -------------------------------------------------------------------*/ 12 13 #ifndef __REG51_H_ 14 15 #define __REG51_H_ 16 17 /*BYTE Register */ 18 19 sfr P0 = 0x80; 20 21 sfr P1 = 0×90; 22 23 sfr P2 = 0xA0; 24 25 sfr P3 = 0xB0; 26 27 sfr PSW = 0xD0; 28 29 sfr ACC = 0xE0; 30 31 sfr B = 0XF0; 32 33 sfr SP= 0×81; 34 35 sfr DPL= 0×82; 36 37 sfr DPH = 0×83; 38 39 sfr PCON = 0×87; 40 41 sfr TCON = 0x88; 42 43 sfr TMOD = 0×89; 44 45 sfr TL0 = 0×8A; 46 47 sfr TL1 = 0×8B; 48 49 sfr TH0 = 0×8C; 50 51 sfr TH1 = 0×8D; 52 53 sfr IE = 0xA8; 54 55 sfr IP = 0×B8; 56 57 sfr SCON = 0x98; 58 59 sfr SBUF = 0x99; 60 61 /*BIT Register */ 62 63 /*PSW*/ 64 65 sbit CY = 0xD7; 66 67 sbit AC = 0xD6; 68 69 sbit F0 =0xD5 70 71 sbit RS1 = 0xD4; 72 73 sbit RS0 = 0xD3; 74 75 sbit OV = 0xD2; 76 77 sbit P = 0xD0; 78 79 80 81 /*TCON*/ 82 83 sbit TF1 = 0x8F; 84 85 sbit TR1 = 0x8E; 86 87 sbit TF0 = 0×8D; 88 89 sbit TR0 = 0×8C; 90 91 sbit IE1 = 0×8B; 92 93 sbit IT1 = 0×8A; 94 95 sbit IE0 = 0×89; 96 97 sbit IT0 = 0×88; 98 99 100 101 /*IE*/ 102 103 sbit EA = 0XAF; 104 105 sbit ES = 0xAC; 106 107 sbit ET1 = 0xAB; 108 109 sbit EX1 = 0xAA; 110 111 sbit ET0 = 0xA9; 112 113 sbit EX0 = 0xA8; 114 115 116 117 /*IP*/ 118 119 sbit PS = 0xBC; 120 121 sbit PT1 =0xBB; 122 123 sbit PX1 = 0xBA; 124 125 sbit PT0 = 0xB9; 126 127 sbit PX0 = 0xB8; 128 129 130 131 /*P3*/ 132 133 sbit RD = 0xB7; 134 135 sbit WR = 0xB6; 136 137 sbit T1 = 0xB5; 138 139 sbit T0 = 0xB4; 140 141 sbit INT1 = 0xB3; 142 143 sbit INT0 = 0xB2; 144 145 sbit TXD = 0xB1; 146 147 sbit RXD = 0xBO; 148 149 150 151 /*SCON*/ 152 153 sbit SM0 = 0x9F; 154 155 sbit SM1 = 0x9E; 156 157 sbit SM2 = 0x9D; 158 159 sbit REN = 0x9C; 160 161 sbit TB8 = 0x9B; 162 163 sbit RB8 = 0×9A; 164 165 sbit TI = 0x99; 166 167 sbit RI = 0x98; 168 169 #endif
3. 啟動文件一STARTUP.A51
還有一個就是啟動代碼,這個也是很多初學者容易忽略的地方,對於這部分我們主要總結下它的功能,不詳解講解里面的代碼。
單片機在上電復位后,首先執行的是啟動文件一STARTUP.A51,而不是我們通常看到的main函數。我們新建51工程的時候會有一個提示:是否拷貝啟動代碼到當前的工程,我們一般選擇是。

是否添加啟動代碼
啟動代碼用匯編語言編寫,主要實現了以下功能:
清除內部數據存儲器、清除外部數據存儲器、清除外部頁儲存器、初始化small模式下的可重入棧和指針、初始化large模式下可重入棧和指針、初始化compact模式下的可重入棧和指針、初始化8051硬件棧指針、傳遞初始化全局變量的控制命令或者在沒有初始化全局變量時給main函數傳遞命令。然后程序就跳轉到main函數,來到我們熟知的C世界。
2. 用STM32點亮一個LED
1. 新建工程
用KEIL5新建一個工程,把工程放在一個事先建好的文件夾內,工程命名為REG后保存。然后在工程目錄下添加啟動文件:startup_stm32fl0x_hd.s,該文件可以從KEIL5安裝目錄找到,也可以從ST庫里面找到,然后把啟動文件添加到工程里面。
2.啟動文件一startup_stm32f10x_hd.s
啟動文件由匯編語言編寫,具體功能跟51里面的啟動文件:STARTUP.A51差不多。
STM32的啟動文件主要實現了:
1、設置初始SP。
2、設置初始PC = Reset_Handler
3、設置向量表入口地址,並初始化向量表。
4、調用庫函數SystemInit,把系統時鍾配置成72M,SystemInit在庫文件system_stm32f10.c定義。
5、跳轉到標號_main,最終來到C的世界。
這里我們先去除繁枝細節,挑重點的講,主要理解第4和第5點,在啟動文件的147~155行,是復位處理函數,代碼如下:
1 ;Reset handler 2 Reset Handler PROC 3 EXPORT Reset_Handler [WEAK] 4 IMPORT __main 5 IMPORT SystemInit 6 LDR R0, =SystemInit 7 BLX R08LDRRO,=main 8 LDR R0, =__main 9 BX R0 10 ENDP
這里我們簡單介紹下這10行代碼。
第1行是程序注釋,在匯編里面注釋用的是“;”,跟C語言不一樣。
第2行是定義了一個子程序:Reset_Handler。PROC是子程序定義偽指令。一般用法為:
1 子程序名 PROC NEAR(或FAR) 2 3 ...... 4 5 ret 6 7 子程序名 ENDP
其中NEAR和FAR是屬性詞。
NEAR屬性(段內近調用):調用程序和子程序在同一代碼段中,只能被相同代碼段的其他程序調用。
FAR屬性(段間遠調用):調用程序和子程序不在同一代碼段中,可以被相同或不同代碼段的程序調用。
第3行EXPORT表示Reset_Handler這個子程序可供其他模塊調用。關鍵字[WEAK]表示弱定義,如果編譯器發現在別處定義了同名的函數,則在鏈接時用別處的地址進行鏈接,如果其它地方沒有定義,編譯器也不報錯,以此處地址進行鏈接。
第4行和第5行IMPORT說明Systemlnit和__main這兩個標號在其他文件,在鏈接的時候需要到其他文件去尋找。
Systemlnit在庫文件system_stm32f10x.c實現,用來初始化STM32的一系列時鍾,把系統時鍾設置為72MHZ。STM32的時鍾比51單片機復雜,需要經過一系列的配置才能達到穩定運行的狀態。
__main其實不是我們定義的,當編譯器編譯時,只要遇到這個標號就會定義這個函數,該函數的主要功能是:負責初始化棧、堆,配置系統環境,並在最后跳轉到用戶自定義的main函數,從此來到C的世界。
第6行把Systemlnit的地址加載到寄存器R0。
第7行程序跳轉到R0中的地址執行程序,之后系統的時鍾就被設置成72MHZ。
第8行把__main的地址加載到寄存器R0。
第9行程序跳轉到R0中的地址執行程序,執行完畢之后就去到我們熟知的C世界。
第10行表示子程序的結束。
總結下就是,Reset_Handler這個函數執行了兩個函數調用,一個是Systemlnit,把系統時鍾設置成72M,另一個是__main,初始化好系統環境,最終調用C的main,從此去到C的世界。
__main函數由編譯器生成,負責初始化棧、堆等,並在最后跳轉到用戶自定義的main()函數,來到C的世界。
