摘要:從STM32新建工程、編譯下載程序出發,讓新手由淺入深,盡享STM32標準庫開發的樂趣。
自從CubeMX等圖像配置軟件的出現,同窗們每每點幾下鼠標就解決了單片機的配置問題。對於追求開發速度的業務場景下,使用快速配置軟件是合理的,高效的,但對於學生的學習場景下,更爲重要的是知其然並知其因此然。前端
如下是學習(包括但不限於)嵌入式的三個重要內容,編程
一、學會如何參考官方的手冊和官方的代碼來獨立寫本身的程序。函數
二、積累經常使用代碼段,知道哪裏的問題須要哪些代碼處理。學習
三、跟隨大佬步伐,一步一個腳印。優化
咱們將以STM32f10xxx爲例對標準庫開發進行概覽。ui
STM32f10xxx 系統結構spa
從結構框圖上看,Cortex-M3 內部有若 幹個總線接口,以使 CM3 能同時取址和訪內(訪問內存),它們是: 指令存儲區總線(兩條)、系統總線、私有外設總線。有兩條代碼存儲區總線負責對代 碼存儲區(即 FLASH 外設)的訪問,分別是 I-Code 總線和 D-Code 總線。操作系統
I-Code 用於取指,D-Code 用於查表等操做,它們按最佳執行速度進行優化。設計
系統總線(System)用於訪問內存和外設,覆蓋的區域包括 SRAM,片上外設,片外 RAM,片外擴展設備,以及系統級存儲區的部分空間。3d
私有外設總線負責一部分私有外設的訪問,主要就是訪問調試組件。它們也在系統級 存儲區。
還有一個 MDA 總線,從字面上看,DMA 是 data memory access 的意思,是一種鏈接內核和外設的橋樑,它能夠訪問外設、內存,傳輸不受 CPU 的控制,而且是雙向通訊。簡而言之,這個傢伙就是一個速度很快的且不受老大控制的數據搬運工。
從結構框圖上看,STM32 的外設有 串口、定時器、IO 口、FSMC、SDIO、SPI、I2C 等,這些外設按 照速度的不一樣,分別掛載到 AHB、APB二、APB1 這三條總線上。
什麼是寄存器?寄存器是內置於各個 IP 外設中,是一種用於配置外設功能的存儲器,而且有想對應的地址。一切庫的封裝始於映射。
是否是「又臭又長」,若是進行寄存器開發,就須要懟地址以及對寄存器進行字節賦值,不只效率低並且容易出錯。
來,開個玩笑。
你也許據說過「國際 C 語言亂碼大賽(IOCCC)」下面這個例子就是網上廣爲流傳的 一個經典做品:
#include <stdio.h> main(t,_,a)char *a;{return!0<t?t<3?main(-79,-13,a+main(-87,1-_, main(-86,0,a+1)+a)):1,t<_?main(t+1,_,a):3,main(-94,-27+t,a)&&t==2?_<13? main(2,_+1,"%s %d %d\n"):9:16:t<0?t<-72?main(_,t, "@n'+,#'/*{}w+/w#cdnr/+,{}r/*de}+,/*{*+,/w{%+,/w#q#n+,/#{l+,/n{n+,/+#n+,/#\ ;#q#n+,/+k#;*+,/'r :'d*'3,}{w+K w'K:'+}e#';dq#'l \ q#'+d'K#!/+k#;q#'r}eKK#}w'r}eKK{nl]'/#;#q#n'){)#}w'){){nl]'/+#n';d}rw' i;# \ ){nl]!/n{n#'; r{#w'r nc{nl]'/#{l,+'K {rw' iK{;[{nl]'/w#q#n'wk nw' \ iwk{KK{nl]!/w{%'l##w#' i; :{nl]'/*{q#'ld;r'}{nlwb!/*de}'c \ ;;{nl'-{}rw]'/+,}##'*}#nc,',#nw]'/+kd'+e}+;#'rdq#w! nr'/ ') }+}{rl#'{n' ')# \ }'+}##(!!/") :t<-50?_==*a?putchar(31[a]):main(-65,_,a+1):main((*a=='/')+t,_,a+1) :0<t?main(2,2,"%s"):*a=='/'||main(0,main(-61,*a, "!ek;dc i@bK'(q)-[w]*%n+r3#l,{}:\nuwloca-O;m.vpbks,fxntdCeghiry"),a+1);}
庫的存在就是爲了解決這類問題,將代碼語義化。語義化思想不只僅是嵌入式有的,前端代碼也在追求語義特性。
這個頭文件實現了:一、內核結構體寄存器定義 二、內核寄存器內存映射 三、內存寄存 器位定義。跟處理器相關的頭文件 stm32f10x.h 實現的功能同樣,一個是針對內核的寄存器,一個是針對內核以外,即處理器的寄存器。
內核應用函數庫頭文件,對應 stm32f10x_xxx.h
內核應用函數庫文件,對應 stm32f10x_xxx.c。在 CM3 這個內核裏面還有一些功能組 件,如 NVIC、SCB、ITM、MPU、CoreDebug,CM3 帶有很是豐富的功能組件,可是芯片 廠商在設計 MCU 的時候有一些並非非要不可的,是可裁剪的,好比 MPU、ITM 等在 STM32 裏面就沒有。其中 NVIC 在每個 CM3 內核的單片機中都會有,但都會被裁剪,只能是 CM3 NVIC 的一個子集。在 NVIC 裏面還有一個 SysTick,是一個系統定時器,能夠提 供時基,通常爲操做系統定時器所用。 misc.h 和 mics.c 這兩個文件提供了操做這些組件的函數,並能夠在 CM3 內核單片機 直接移植。
這個是由彙編編寫的啓動文件,是 STM32 上電啓動的第一個程序,啓動文件主要實現 了
這個文件的做用是裏面實現了各類經常使用的系統時鐘設置函數,有 72M,56M,48, 36,24,8M,咱們使用的是是把系統時鐘設置成 72M。
這個頭文件很是重要,這個頭文件實現了:一、處理器外設寄存器 的結構體定義 二、處理器外設的內存映射 三、處理器外設寄存器的位定義。
關於 1 和 2 咱們在用寄存器點亮 LED 的時候有講解。
其中 3:處理器外設寄存器的位定義,這個很是重要,具體是什麼意思?咱們知道一個寄存器有不少個位,每一個位寫 1 或 者寫 0 的功能都是不同的,處理器外設寄存器的位定義就是把外設的每一個寄存器的每一 個位寫 1 的 16 進制數定義成一個宏,宏名即用該位的名稱表示,若是咱們操做寄存器要開啓某一個功能的話,就不用本身親自去算這個值是多少,能夠直接到這個頭文件裏面找。
咱們以片上外設 ADC 爲例,假設咱們要啓動 ADC 開始轉換,根據手冊咱們知道是要控制 ADC_CR2 寄存器的位 0:ADON,即往位 0 寫 1,即:
ADC->CR2=0x00000001;
這是 通常的操做方法。如今這個頭文件裏面有關於 ADON 位的位定義:
#define ADC_CR2_ADON ((uint32_t)0x00000001)
有了這個位定義,咱們剛剛的 代碼就變成了:
ADC->CR2=ADC_CR2_ADON
外設 xxx 應用函數庫頭文件,這裏面主要定義了實現外設某一功能 的結構體,好比通用定時器有不少功能,有定時功能,有輸出比較功能,有輸入捕捉功 能,而通用定時器有很是多的寄存器要實現某一個功能,好比定時功能,咱們根本不知道 具體要操做哪些寄存器,這個頭文件就爲咱們打包好了要實現某一個功能的寄存器,是以機構體的形式定義的,好比通用定時器要實現一個定時的功能,咱們只須要初始化 TIM_TimeBaseInitTypeDef 這個結構體裏面的成員便可,裏面的成員就是定時所須要 操做的寄存器。 有了這個頭文件,咱們就知道要實現某個功能須要操做哪些寄存器,而後 再回手冊中精度這些寄存器的說明便可。
stm32f10x_xxx.c:外設 xxx 應用函數庫,這裏面寫好了操做 xxx 外設的全部經常使用的函 數,咱們使用庫編程的時候,使用的最多的就是這裏的函數。
工程中新建main.c 。
在此文件中編寫main函數後直接編譯會報錯:
Undefined symbol SystemInit (referred from startup_stm32f10x_hd.o).
錯誤提示說SystemInit 沒有定義。從分析啓動文件startup_stm32f10x_hd.s時咱們知道,
1 ;Reset handler 2 Reset_Handler PROC 3 EXPORT Reset_Handler [WEAK] 4 IMPORT __main 5 ;IMPORT SystemInit 6 ;LDR R0, =SystemInit 7 BLX R0 8 LDR R0, =__main 9 BX R0 10 ENDP
彙編中;分號是註釋的意思
第五行第六行代碼Reset_Handler 調用了SystemInit該函數用來初始化系統時鐘,而該函數是在庫文件system_stm32f10x.c 中實現的。咱們從新寫一個這樣的函數也能夠,把功能完整實現一遍,可是爲了簡單起見,咱們在main 文件裏面定義一個SystemInit 空函數,爲的是騙過編譯器,把這個錯誤去掉。關於配置系統時鐘以後會出文章RCC 時鐘樹詳細介紹,主要配置時鐘控制寄存器(RCC_CR)和時鐘配置寄存器(RCC_CFGR)這兩個寄存器,但最好是直接使用CubeMX直接生成,由於它的配置過程有些冗長。
若是咱們用的是庫,那麼有個庫函數SystemInit,會幫咱們把系統時鐘設置成72M。
如今咱們沒有使用庫,那如今時鐘是多少?答案是8M,當外部HSE 沒有開啓或者出現故障的時候,系統時鐘由內部低速時鐘LSI 提供,如今咱們是沒有開啓HSE,因此係統默認的時鐘是LSI=8M。
如圖,達到第四層級即是咱們所熟知的固件庫或HAL庫的效果。固然庫的編寫還須要考慮許多問題,不止於這些內容。咱們須要的是瞭解庫封裝的大概過程。
將庫封裝等級分爲四級來介紹是爲了有層次感,就像打怪升級同樣,進行認知理解的升級。
咱們都知道,操做GPIO輸出分三大步:
STM32 外設不少,爲了下降功耗,每一個外設都對應着一個時鐘,在系統復位的時候這些時鐘都是被關閉的,若是想要外設工做,必須把相應的時鐘打開。
STM32 的全部外設的時鐘由一個專門的外設來管理,叫RCC(reset and clockcontrol),RCC 在STM32 參考手冊的第六章。
STM32 的外設由於速率的不一樣,分別掛載到三條總繫上:AHB、APB二、APB1,AHB爲高速總線,APB2 次之,APB1 再次之。因此的IO 口都掛載到APB2 總線上,屬於高速外設。
這個由端口配置寄存器來控制。端口配置寄存器分爲高低兩個,每4bit 控制一個IO 口,因此端口配置低寄存器:CRL 控制這IO 口的低8 位,端口配置高寄存器:CRH控制這IO 口的高8bit。在4 位一組的控制位中,CNFy[1:0] 用來控制端口的輸入輸出,MODEy[1:0]用來控制輸出模式的速率,又稱驅動電路的響應速度,注意此處速率與程序無關,具體內容見文章:【嵌入式】GPIO引腳速度、翻轉速度、輸出速度區別輸入有4種模式,輸出有4種模式,咱們在控制LED 的時候選擇通用推輓輸出。
輸出速率有三種模式:2M、10M、50M,這裏咱們選擇2M。
STM32 的IO 口比較複雜,若是要輸出1 和0,則要經過控制:端口輸出數據寄存器ODR 來實現,ODR 是:Output data register 的簡寫,在STM32 裏面,其寄存器的命名名稱都是英文的簡寫,很容易記住。從手冊上咱們知道ODR 是一個32 位的寄存器,低16位有效,高16 位保留。低16 位對應着IO0~IO16,只要往相應的位置寫入0 或者1 就能夠輸出低或者高電平。
時鐘控制:
在STM32 中,每一個外設都有一個起始地址,叫作外設基地址,外設的寄存器就以這個基地址爲標準按照順序排列,且每一個寄存器32位,(後面做爲結構體裏面的成員正好內存對齊)。查表看到時鐘由APB2 外設時鐘使能寄存器(RCC_APB2ENR)來控制,其中PB 端口的時鐘由該寄存器的位3 寫1 使能。咱們能夠經過基地址+偏移量0x18,算出RCC_APB2ENR 的地址爲:0x40021018。那麼使能PB 口的時鐘代碼則以下所示:
#define RCC_APB2ENR *(volatile unsigned long *)0x40021018 // 開啓端口B 時鐘 RCC_APB2ENR |= 1<<3;
模式配置:
同RCC_APB2ENR 同樣,GPIOB 的起始地址是:0X4001 0C00,咱們也能夠算出GPIO_CRL 的地址爲:0x40010C00。那麼設置PB0 爲通用推輓輸出,輸出速率爲2M 的代碼則以下所示:
同上,從手冊中咱們看到ODR 寄存器的地址偏移是:0CH,能夠算出GPIOB_ODR 寄存器的地址是:0X4001 0C00 + 0X0C = 0X4001 0C0C。如今咱們就能夠定義GPIOB_ODR 這個寄存器了,代碼以下:
#define GPIOB_ODR *(volatile unsigned long *)0x40010C0C //PB0 輸出低電平 GPIOB_ODR = 0<<0;
第一層級:基地址宏定義完成用STM32 控制一個LED 的完整代碼:
1 #define RCC_APB2ENR *(volatile unsigned long *)0x40021018 2 #define GPIOB_CRL *(volatile unsigned long *)0x40010C00 3 #define GPIOB_ODR *(volatile unsigned long *)0x40010C0C 45 int main(void) 6 { 7 // 開啓端口B 的時鐘 8 RCC_APB2ENR |= 1<<3; 9 10 // 配置PB0 爲通用推輓輸出模式,速率爲2M 11 GPIOB_CRL = (2<<0) | (0<<2); 12 13 // PB0 輸出低電平,點亮LED 14 GPIOB_ODR = 0<<0; 15 } 16 17 void SystemInit(void) 18 { 19 }
外設寄存器結構體封裝
上面咱們在操做寄存器的時候,操做的是寄存器的絕對地址,若是每一個寄存器都這樣操做,那將很是麻煩。咱們考慮到外設寄存器的地址都是基於外設基地址的偏移地址,都是在外設基地址上逐個連續遞增的,每一個寄存器佔 32 個或者 16 個字節,這種方式跟結構體裏面的成員相似。因此咱們能夠定義一種外設結構體,結構體的地址等於外設的基地址,結構體的成員等於寄存器,成員的排列順序跟寄存器的順序同樣。這樣咱們操做寄存器的時候就不用每次都找到絕對地址,只要知道外設的基地址就能夠操做外設的所有寄存器,即操做結構體的成員便可。
下面咱們先定義一個 GPIO 寄存器結構體,結構體裏面的成員是 GPIO 的寄存器,成員的順序按照寄存器的偏移地址從低到高排列,成員類型跟寄存器類型同樣。(struct用法參考【C語言】(2):關鍵字的詳細介紹)
1 typedef struct { 2 volatile uint32_t CRL; 3 volatile uint32_t CRH; 4 volatile uint32_t IDR; 5 volatile uint32_t ODR; 6 volatile uint32_t BSRR; 7 volatile uint32_t BRR; 8 volatile uint32_t LCKR; 9 } GPIO_TypeDef;
在《STM32 中文參考手冊》8.2 寄存器描述章節,咱們能夠找到結構體裏面的7 個寄存器描述。在點亮LED 的時候咱們只用了CRL 和ODR 這兩個寄存器,至於其餘寄存器的功能你們能夠自行看手冊瞭解。
在GPIO 結構體裏面咱們用了兩個數據類型,一個是uint32_t,表示無符號的32 位整型,由於GPIO 的寄存器都是32 位的。這個類型聲明在標準頭文件stdint.h 裏面使用typedef對unsigned int重命名,咱們在程序上只要包含這個頭文件便可。
另一個是volatile(volatile用法參考【C語言】(2):關鍵字的詳細介紹),做用就是告訴編譯器這裏的變量會變化不因優化而省略此指令,必須每次都直接讀寫其值,這樣就能確保每次讀或者寫寄存器都真正執行到位。
外設封裝
STM32F1 系列的GPIO 端口分A~G,即GPIOA、GPIOB。。。。。。GPIOG。每一個端口都含有GPIO_TypeDef 結構體裏面的寄存器,咱們能夠根據手冊各個端口的基地址把GPIO 的各個端口定義成一個GPIO_TypeDef 類型指針,而後咱們就能夠根據端口名(實際上如今是結構體指針了)來操做各個端口的寄存器,代碼實現以下:
1 #define GPIOA ((GPIO_TypeDef *) 0X4001 0800) 2 #define GPIOB ((GPIO_TypeDef *) 0X4001 0C00) 3 #define GPIOC ((GPIO_TypeDef *) 0X4001 1000) 4 #define GPIOD ((GPIO_TypeDef *) 0X4001 1400) 5 #define GPIOE ((GPIO_TypeDef *) 0X4001 1800) 6 #define GPIOF ((GPIO_TypeDef *) 0X4001 1C00) 7 #define GPIOG ((GPIO_TypeDef *) 0X4001 2000)
外設內存映射
講到基地址的時候咱們再引人一個知識點:Cortex-M3 存儲器系統,這個知識點在《Cortex-M3 權威指南》第5 章裏面講到。CM3 的地址空間是4GB,以下圖所示:
咱們這裏要講的是片上外設,就是咱們所說的寄存器的根據地,其大小總共有512MB,512MB 是其極限空間,並非每一個單片機都用得完,實際上各個MCU 廠商都只是用了一部分而已。STM32F1 系列用到了:0x4000 0000 ~0x5003 FFFF。如今咱們說的STM32 的寄存器就是位於這個區域
如今咱們說的STM32 的寄存器就是位於這個區域,這裏面ST 設計了三條總線:AHB、APB2 和APB1,其中AHB 和APB2 是高速總線,APB1 是低速總線。不一樣的外設根據速度不一樣分別掛載到這三條總線上。從下往上依次是:APB一、APB二、AHB,每一個總線對應的地址分別是:APB1:0x40000000,APB2:0x4001 0000,AHB:0x4001 8000。
這三條總線的基地址咱們是從《STM32 中文參考手冊》2.3 小節—存儲器映像獲得的:APB1 的基地址是TIM2 定時器的起始地址,APB2 的基地址是AFIO 的起始地址,AHB 的基地址是SDIO 的起始地址。其中APB1 地址又叫作外設基地址,是全部外設的基地址,叫作PERIPH_BASE。
如今咱們把這三條總線地址用宏定義出來,之後咱們在定義其餘外設基地址的時候,只須要在這三條總線的基址上加上偏移地址便可,代碼以下:
1 #define PERIPH_BASE ((uint32_t)0x40000000) 2 #define APB1PERIPH_BASE PERIPH_BASE 3 #define APB2PERIPH_BASE (PERIPH_BASE + 0x10000) 4 #define AHBPERIPH_BASE (PERIPH_BASE + 0x20000)
由於GPIO 掛載到APB2 總線上,那麼如今咱們就能夠根據APB2 的基址算出各個GPIO 端口的基地址,用宏定義實現代碼以下:
1 #define GPIOA_BASE (APB2PERIPH_BASE + 0x0800)
2 #define GPIOB_BASE (APB2PERIPH_BASE + 0x0C00)
3 #define GPIOC_BASE (APB2PERIPH_BASE + 0x1000)
4 #define GPIOD_BASE (APB2PERIPH_BASE + 0x1400)
5 #define GPIOE_BASE (APB2PERIPH_BASE + 0x1800)
6 #define GPIOF_BASE (APB2PERIPH_BASE + 0x1C00)
7 #define GPIOG_BASE (APB2PERIPH_BASE + 0x2000)
1 #include <stdint.h> 2 #define __IO volatile 3 4typedef struct { 5 __IO uint32_t CRL; 6 __IO uint32_t CRH; 7 __IO uint32_t IDR; 8 __IO uint32_t ODR; 9 __IO uint32_t BSRR; 10 __IO uint32_t BRR; 11 __IO uint32_t LCKR; 12 } GPIO_TypeDef; 13 14 typedef struct { 15 __IO uint32_t CR; 16 __IO uint32_t CFGR; 17 __IO uint32_t CIR; 18 __IO uint32_t APB2RSTR; 19 __IO uint32_t APB1RSTR; 20 __IO uint32_t AHBENR; 21 __IO uint32_t APB2ENR; 22 __IO uint32_t APB1ENR; 23 __IO uint32_t BDCR; 24 __IO uint32_t CSR; 25 } RCC_TypeDef; 26 27 #define PERIPH_BASE ((uint32_t)0x40000000) 28 29 #define APB1PERIPH_BASE PERIPH_BASE 30 #define APB2PERIPH_BASE (PERIPH_BASE + 0x10000) 31 #define AHBPERIPH_BASE (PERIPH_BASE + 0x20000) 32 33 #define GPIOA_BASE (APB2PERIPH_BASE + 0x0800) 34 #define GPIOB_BASE (APB2PERIPH_BASE + 0x0C00) 35 #define GPIOC_BASE (APB2PERIPH_BASE + 0x1000) 36 #define GPIOD_BASE (APB2PERIPH_BASE + 0x1400) 37 #define GPIOE_BASE (APB2PERIPH_BASE + 0x1800) 38 #define GPIOF_BASE (APB2PERIPH_BASE + 0x1C00) 39 #define GPIOG_BASE (APB2PERIPH_BASE + 0x2000) 40 #define RCC_BASE (AHBPERIPH_BASE + 0x1000) 41 42 #define GPIOA ((GPIO_TypeDef *) GPIOA_BASE) 43 #define GPIOB ((GPIO_TypeDef *) GPIOB_BASE) 44 #define GPIOC ((GPIO_TypeDef *) GPIOC_BASE) 45 #define GPIOD ((GPIO_TypeDef *) GPIOD_BASE) 46 #define GPIOE ((GPIO_TypeDef *) GPIOE_BASE) 47 #define GPIOF ((GPIO_TypeDef *) GPIOF_BASE) 48 #define GPIOG ((GPIO_TypeDef *) GPIOG_BASE) 49 #define RCC ((RCC_TypeDef *) RCC_BASE) 50 51 52 #define RCC_APB2ENR *(volatile unsigned long *)0x40021018 53 #define GPIOB_CRL *(volatile unsigned long *)0x40010C00 54 #define GPIOB_ODR *(volatile unsigned long *)0x40010C0C 55 56 int main(void) 57 { 58 // 開啓端口B 的時鐘 59 RCC->APB2ENR |= 1<<3; 60 61 // 配置PB0 爲通用推輓輸出模式,速率爲2M 62 GPIOB->CRL = (2<<0) | (0<<2); 63 64 // PB0 輸出低電平,點亮LED 65 GPIOB->ODR = 0<<0; 66 67 } 68 69 void SystemInit(void) 70 { 71 }
第二層級變化:
①、定義一個外設(GPIO)寄存器結構體,結構體的成員包含該外設的全部寄存器,成員的排列順序跟寄存器偏移地址同樣,成員的數據類型跟寄存器的同樣。
②外設內存映射,即把地址跟外設創建起一一對應的關係。
③外設聲明,即把外設的名字定義成一個外設寄存器結構體類型的指針。
④經過結構體操做寄存器,實現點亮LED。
上面咱們在控制GPIO 輸出內容的時候控制的是ODR(Output data register)寄存器,ODR 是一個16 位的寄存器,必須以字的形式控制其實咱們還能夠控制BSRR 和BRR 這兩個寄存器來控制IO 的電平,下面咱們簡單介紹下BRR 寄存器的功能,BSRR 自行看手冊研究。
位清除寄存器BRR 只能實現位清0 操做,是一個32 位寄存器,低16 位有效,寫0 沒影響,寫1 清0。如今咱們要使PB0 輸出低電平,點亮LED,則只要往BRR 的BR0 位寫1 便可,其餘位爲0,代碼以下:
1 GPIOB->BRR = 0X0001;
這時PB0 就輸出了低電平,LED 就被點亮了。
若是要PB2 輸出低電平,則是:
1 GPIOB->BRR = 0X0004;
若是要PB3/4/5/6。。。。。。這些IO 輸出低電平呢?道理是同樣的,只要往BRR 的相應位置賦不一樣的值便可。由於BRR 是一個16 位的寄存器,位數比較多,賦值的時候容易出錯,並且從賦值的16 進制數字咱們很難清楚的知道控制的是哪一個IO。這時,咱們是否能夠把BRR 的每一個位置1 都用宏定義來實現,如GPIO_Pin_0 就表示0X0001,GPIO_Pin_2 就表示0X0004。只要咱們定義一次,之後均可以使用,並且還見名知意。「位封裝」(每一位的對應字節封裝) 代碼以下:
1 #define GPIO_Pin_0 ((uint16_t)0x0001) /*!< Pin 0 selected */ 2 #define GPIO_Pin_1 ((uint16_t)0x0002) /*!< Pin 1 selected */ 3 #define GPIO_Pin_2 ((uint16_t)0x0004) /*!< Pin 2 selected */ 4 #define GPIO_Pin_3 ((uint16_t)0x0008) /*!< Pin 3 selected */ 5 #define GPIO_Pin_4 ((uint16_t)0x0010) /*!< Pin 4 selected */ 6 #define GPIO_Pin_5 ((uint16_t)0x0020) /*!< Pin 5 selected */ 7 #define GPIO_Pin_6 ((uint16_t)0x0040) /*!< Pin 6 selected */ 8 #define GPIO_Pin_7 ((uint16_t)0x0080) /*!< Pin 7 selected */ 9 #define GPIO_Pin_8 ((uint16_t)0x0100) /*!< Pin 8 selected */ 10 #define GPIO_Pin_9 ((uint16_t)0x0200) /*!< Pin 9 selected */ 11 #define GPIO_Pin_10 ((uint16_t)0x0400) /*!< Pin 10 selected */ 12 #define GPIO_Pin_11 ((uint16_t)0x0800) /*!< Pin 11 selected */ 13 #define GPIO_Pin_12 ((uint16_t)0x1000) /*!< Pin 12 selected */ 14 #define GPIO_Pin_13 ((uint16_t)0x2000) /*!< Pin 13 selected */ 15 #define GPIO_Pin_14 ((uint16_t)0x4000) /*!< Pin 14 selected */ 16 #define GPIO_Pin_15 ((uint16_t)0x8000) /*!< Pin 15 selected */ 17 #define GPIO_Pin_All ((uint16_t)0xFFFF) /*!< All pins selected */
這時PB0 就輸出了低電平的代碼就變成了:
1 GPIOB->BRR = GPIO_Pin_0;
(若是同時讓PB0/PB15輸出低電平,用或運算,代碼:
1 GPIOB->BRR = GPIO_Pin_0|GPIO_Pin_15;
爲了避免使main 函數看起來冗餘,上述庫封裝 的代碼不該該放在main 裏面,由於其是跟GPIO 相關的,咱們能夠把這些宏放在一個單獨的頭文件裏面。
在工程目錄下新建stm32f10x_gpio.h,把封裝代碼放裏面,而後把這個文件添加到工程裏面。這時咱們只須要在main.c 裏面包含這個頭文件便可。
第四層級:基地址宏定義+結構體封裝+「位封裝」+函數封裝
咱們點亮LED 的時候,控制的是PB0 這個IO,若是LED 接到的是其餘IO,咱們就須要把GPIOB 修改爲其餘的端口,其實這樣修改起來也很快很方便。可是爲了提升程序的可讀性和可移植性,咱們是否能夠編寫一個專門的函數用來複位GPIO 的某個位,這個函數有兩個形參,一個是GPIOX(X=A...G),另一個是GPIO_Pin(0...15),函數的主體則是根據形參GPIOX 和GPIO_Pin 來控制BRR 寄存器,代碼以下:
1 void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin) 2 { 3 GPIOx->BRR = GPIO_Pin; 4 }
這時,PB0 輸出低電平,點亮LED 的代碼就變成了:
1 GPIO_ResetBits(GPIOB,GPIO_Pin_0);
同理, 咱們能夠控制BSRR 這個寄存器來實現關閉LED,代碼以下:
1 // GPIO 端口置位函數 2 void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin) 3 { 4 GPIOx->BSRR = GPIO_Pin; 5 }
這時,PB0 輸出高電平,關閉LED 的代碼就變成了:
1 GPIO_SetBits(GPIOB,GPIO_Pin_0);
一樣,由於這個函數是控制GPIO 的函數,咱們能夠新建一個專門的文件來放跟gpio有關的函數。
在工程目錄下新建stm32f10x_gpio.c,把GPIO 相關的函數放裏面。這時咱們是否發現剛剛新建了一個頭文件stm32f10x_gpio.h,這兩個文件存放的都是跟外設GPIO 相關的。C 文件裏面的函數會用到h 頭文件裏面的定義,這兩個文件是相輔相成的,故咱們在stm32f10x_gpio.c 文件中也包含stm32f10x_gpio.h 這個頭文件。別忘了把stm32f10x.h 這個頭文件也包含進去,由於有關寄存器的全部定義都在這個頭文件裏面。
若是咱們寫其餘外設的函數,咱們也應該跟GPIO 同樣,新建兩個文件專門來存函數,好比RCC 這個外設咱們能夠新建stm32f10x_rcc.c 和stm32f10x_rcc.h。其餘外依葫蘆畫瓢便可。
以上,是對庫封住過程的概述,下面咱們正在地使用庫函數編寫LED程序
當咱們開始調用庫函數寫代碼的時候,有些庫咱們不須要,在編譯的時候能夠不編譯,能夠經過一個總的頭文件stm32f10x_conf.h 來控制,該頭文件主要代碼以下:
1 //#include "stm32f10x_adc.h" 2 //#include "stm32f10x_bkp.h" 3 //#include "stm32f10x_can.h" 4 //#include "stm32f10x_cec.h" 5 //#include "stm32f10x_crc.h" 6 //#include "stm32f10x_dac.h" 7 //#include "stm32f10x_dbgmcu.h" 8 //#include "stm32f10x_dma.h" 9 //#include "stm32f10x_exti.h" 10 //#include "stm32f10x_flash.h" 11 //#include "stm32f10x_fsmc.h" 12 #include "stm32f10x_gpio.h" 13 //#include "stm32f10x_i2c.h" 14 //#include "stm32f10x_iwdg.h" 15 //#include "stm32f10x_pwr.h" 16 #include "stm32f10x_rcc.h" 17 //#include "stm32f10x_rtc.h" 18 //#include "stm32f10x_sdio.h" 19 //#include "stm32f10x_spi.h" 20 //#include "stm32f10x_tim.h" 21 //#include "stm32f10x_usart.h" 22 //#include "stm32f10x_wwdg.h" 23 //#include "misc.h"
這裏麪包含了所有外設的頭文件,點亮一個LED 咱們只須要RCC 和GPIO 這兩個外設的庫函數便可,其中RCC 控制的是時鐘,GPIO 控制的具體的IO 口。因此其餘外設庫函數的頭文件咱們註釋掉,當咱們須要的時候就把相應頭文件的註釋去掉便可。
stm32f10x_conf.h 這個頭文件在stm32f10x.h 這個頭文件的最後面被包含,在第8296行:
1 #ifdef USE_STDPERIPH_DRIVER 2 #include "stm32f10x_conf.h" 3 #endif
代碼的意思是,若是定義了USE_STDPERIPH_DRIVER 這個宏的話,就包含stm32f10x_conf.h 這個頭文件。咱們在新建工程的時候,在魔術棒選項卡C/C++中,咱們定義了USE_STDPERIPH_DRIVER 這個宏,因此stm32f10x_conf.h 這個頭文件就被stm32f10x.h 包含了,咱們在寫程序的時候只須要調用一個頭文件:stm32f10x.h 便可。(預處理指令詳細內容會在【C語言】的文章中提到)
通過寄存器點亮LED 的操做,咱們知道操做一個GPIO 輸出的編程要點大概以下:
一、開啓GPIO 的端口時鐘
二、選擇要具體控制的IO 口,即pin
三、選擇IO 口輸出的速率,即speed
四、選擇IO 口輸出的模式,即mode
五、輸出高/低電平
STM32 的時鐘功能很是豐富,配置靈活,爲了下降功耗,每一個外設的時鐘均可以獨自的關閉和開啓。STM32 中跟時鐘有關的功能都由RCC 這個外設控制,RCC 中有三個寄存器控制着因此外設時鐘的開啓和關閉:RCC_APHENR、RCC_APB2ENR 和RCC_APB1ENR,AHB、APB2 和APB1 表明着三條總線,全部的外設都是掛載到這三條總線上,GPIO 屬於高速的外設,掛載到APB2 總線上,因此其時鐘有RCC_APB2ENR 控制。
GPIO 時鐘控制
固件庫函數:RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE)函數的
原型爲:
1 void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState) 2 { 3 /* Check the parameters */ 4 assert_param(IS_RCC_APB2_PERIPH(RCC_APB2Periph)); 5 assert_param(IS_FUNCTIONAL_STATE(NewState)); 6 if (NewState != DISABLE) { 7 RCC->APB2ENR |= RCC_APB2Periph; 8 } else { 9 RCC->APB2ENR &= ~RCC_APB2Periph; 10 } 11 }
當程序編譯一次以後,把光標定位到函數/變量/宏定義處,按鍵盤的F12 或鼠標右鍵的Go to definition of,就能夠找到原型。固件庫的底層操做的就是RCC 外設的APB2ENR這個寄存器,宏RCC_APB2Periph_GPIOB 的原型是:0x00000008,即(1<<3),還原成存器操做就是:RCC->APB2ENR |= 1<<<3。相比固件庫操做,寄存器操做的代碼可讀性就不好,只有才查閱寄存器配置才知道具體代碼的功能,而固件庫操做剛好相反,見名知意。
GPIO 端口配置
GPIO 的pin,速度,模式,都由GPIO 的端口配置寄存器來控制,其中IO0~IO7 由端口配置低寄存器CRL 控制,IO8~IO15 由端口配置高寄存器CRH 配置。固件庫把端口配置的pin,速度和模式封裝成一個結構體:
1 typedef struct { 2 uint16_t GPIO_Pin; 3 GPIOSpeed_TypeDef GPIO_Speed; 4 GPIOMode_TypeDef GPIO_Mode; 5 } GPIO_InitTypeDef;
pin 能夠是GPIO_Pin_0~GPIO_Pin_15 或者是GPIO_Pin_All,這些都是庫預先定義好的宏。speed 也被封裝成一個結構體:
1 typedef enum { 2 GPIO_Speed_10MHz = 1, 3 GPIO_Speed_2MHz, 4 GPIO_Speed_50MHz 5 } GPIOSpeed_TypeDef;
速度能夠是10M,2M 或者50M,這個由端口配置寄存器的MODE 位控制,速度是針對IO 口輸出的時候而言,在輸入的時候能夠不用設置。mode 也被封裝成一個結構體:
1 typedef enum { 2 GPIO_Mode_AIN = 0x0, // 模擬輸入 3 GPIO_Mode_IN_FLOATING = 0x04, // 浮空輸入(復位後的狀態) 4 GPIO_Mode_IPD = 0x28, // 下拉輸入 5 GPIO_Mode_IPU = 0x48, // 上拉輸入 6 GPIO_Mode_Out_OD = 0x14, // 通用開漏輸出 7 GPIO_Mode_Out_PP = 0x10, // 通用推輓輸出 8 GPIO_Mode_AF_OD = 0x1C, // 複用開漏輸出 9 GPIO_Mode_AF_PP = 0x18 // 複用推輓輸出 10 } GPIOMode_TypeDef;
IO 口的模式有8 種,輸入輸出各4 種,由端口配置寄存器的CNF 配置。平時用的最多的就是通用推輓輸出,能夠輸出高低電平,驅動能力大,通常用於接數字器件。至於剩下的七種模式的用法和電路原理,咱們在後面的GPIO 章節再詳細講解。
最終用固件庫實現就變成這樣:
1 // 定義一個GPIO_InitTypeDef 類型的結構體 2 GPIO_InitTypeDef GPIO_InitStructure; 3 4// 選擇要控制的IO 口 5 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; 6 7// 設置引腳爲推輓輸出 8 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; 9 10 // 設置引腳速率爲50MHz 11 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz; 12 13 /*調用庫函數,初始化GPIOB0*/ 14 GPIO_Init(GPIOB, &GPIO_InitStructure);
假若同一端口下不一樣引腳有不一樣的模式配置,每次對每一個引腳配置完成後都要調用GPIO初始化函數,代碼以下:
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15 ; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉輸入 GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 ; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推輓輸出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz; GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO 輸出控制
GPIO 輸出控制,能夠經過端口數據輸出寄存器ODR、端口位設置/清除寄存器BSRR和端口位清除寄存器BRR 這三個來控制。端口輸出寄存器ODR 是一個32 位的寄存器,低16 位有效,對應着IO0~IO15,只能以字的形式操做,通常使用寄存器操做。
// PB0 輸出高電平,點亮LED GPIOB->ODR = 1<<0;
端口位清除寄存器BRR 是一個32 位的寄存器,低十六位有效,對應着IO0~IO15,只能以字的形式操做,能夠單獨對某一個位操做,寫1 清0。
// PB0 輸出低電平,點亮LED GPIO_ResetBits(GPIOB, GPIO_Pin_0);
BSRR 是一個32 位的寄存器,低16 位用於置位,寫1 有效,高16 位用於復位,寫1有效,至關於BRR 寄存器。高16 位咱們通常不用,而是操做BRR 這個寄存器,因此BSRR 這個寄存器通常用來置位操做。
// PB0 輸出高電平,熄滅LED GPIO_SetBits(GPIOB, GPIO_Pin_0);
綜上:固件庫LED GPIO 初始化函數
1 void LED_GPIO_Config(void) 2 { 3 // 定義一個GPIO_InitTypeDef 類型的結構體 4 GPIO_InitTypeDef GPIO_InitStructure; 5 6// 開啓GPIOB 的時鐘 7 RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE); 8 9// 選擇要控制的IO 口 10 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; 11 12 // 設置引腳爲推輓輸出 13 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; 14 15 // 設置引腳速率爲50MHz 16 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; 17 18 /*調用庫函數,初始化GPIOB0*/ 19 GPIO_Init(GPIOB, &GPIO_InitStructure); 20 21 // 關閉LED 22 GPIO_SetBits(GPIOB, GPIO_Pin_0); 23 }
主函數
1 #include "stm32f10x.h" 2 3 void SOFT_Delay(__IO uint32_t nCount); 4 void LED_GPIO_Config(void); 5 6int main(void) 7 { 8 // 程序來到main 函數以前,啓動文件:statup_stm32f10x_hd.s 已經調用 9 // SystemInit()函數把系統時鐘初始化成72MHZ 10 // SystemInit()在system_stm32f10x.c 中定義 11 // 若是用戶想修改系統時鐘,可自行編寫程序修改 12 13 LED_GPIO_Config(); 14 15 while ( 1 ) { 16 // 點亮LED 17 GPIO_ResetBits(GPIOB, GPIO_Pin_0); 18 Time_Delay(0x0FFFFF); 19 20 // 熄滅LED 21 GPIO_SetBits(GPIOB, GPIO_Pin_0); 22 Time_Delay(0x0FFFFF); 23 } 24 } 25// 簡陋的軟件延時函數 26 void Time_Delay(volatile uint32_t Count) 27 { 28 for (; Count != 0; Count--); 29 }
注意void Time_Delay(volatile uint32_t Count)只是一個簡陋的軟件延時函數,若是小夥伴們有興趣能夠看一看MultiTimer,它是一個軟件定時器擴展模塊,可無限擴展所需的定時器任務,取代傳統的標誌位判斷方式, 更優雅更便捷地管理程序的時間觸發時序。
本文分享自華爲雲社區《【嵌入式】層層遞進,瞭解庫開發》,原文做者:LongYorke。