摘要:從STM32新建工程、編譯下載程序出發(fā),讓新手由淺入深,盡享STM32標(biāo)準(zhǔn)庫開發(fā)的樂趣。
自從CubeMX等圖像配置軟件的出現(xiàn),同學(xué)們往往點(diǎn)幾下鼠標(biāo)就解決了單片機(jī)的配置問題。對于追求開發(fā)速度的業(yè)務(wù)場景下,使用快速配置軟件是合理的,高效的,但對于學(xué)生的學(xué)習(xí)場景下,更為重要的是知其然并知其所以然。
以下是學(xué)習(xí)(包括但不限于)嵌入式的三個(gè)重要內(nèi)容,
1、學(xué)會如何參考官方的手冊和官方的代碼來獨(dú)立寫自己的程序。
2、積累常用代碼段,知道哪里的問題需要哪些代碼處理。
3、跟隨大佬步伐,一步一個(gè)腳印。
首先:我們都知道編程時(shí)一般查的是《參考手冊》,而進(jìn)行芯片選型或需要芯片數(shù)據(jù)時(shí),查閱的是《數(shù)據(jù)手冊》。此外市面上所有關(guān)于STM32的書籍都是立足于前二者(+Cortex內(nèi)核手冊)進(jìn)行編著。
其次:要分清什么是內(nèi)核外設(shè)與內(nèi)核之外的外設(shè),為了便于區(qū)分,按照網(wǎng)上的一種說法,將“內(nèi)核之外的外設(shè)”以“處理器外設(shè)”代替。再者:如今很少使用標(biāo)準(zhǔn)庫了,都是HAL庫,但作為高校目前教學(xué)方式,我們將以STM32f10xxx為例對標(biāo)準(zhǔn)庫開發(fā)進(jìn)行概覽。
一、STM32 系統(tǒng)結(jié)構(gòu)
STM32f10xxx系統(tǒng)結(jié)構(gòu)
內(nèi)核IP
從結(jié)構(gòu)框圖上看,Cortex-M3內(nèi)部有若干個(gè)總線接口,以使CM3能同時(shí)取址和訪內(nèi)(訪問內(nèi)存),它們是:指令存儲區(qū)總線(兩條)、系統(tǒng)總線、私有外設(shè)總線。有兩條代碼存儲區(qū)總線負(fù)責(zé)對代碼存儲區(qū)(即 FLASH 外設(shè))的訪問,分別是?I-Code 總線和?D-Code 總線。
I-Code用于取指,D-Code用于查表等操作,它們按最佳執(zhí)行速度進(jìn)行優(yōu)化。
系統(tǒng)總線(System)用于訪問內(nèi)存和外設(shè),覆蓋的區(qū)域包括SRAM,片上外設(shè),片外RAM,片外擴(kuò)展設(shè)備,以及系統(tǒng)級存儲區(qū)的部分空間。
私有外設(shè)總線負(fù)責(zé)一部分私有外設(shè)的訪問,主要就是訪問調(diào)試組件。它們也在系統(tǒng)級存儲區(qū)。
還有一個(gè)DMA總線,從字面上看,DMA是data memory access的意思,是一種連接內(nèi)核和外設(shè)的橋梁,它可以訪問外設(shè)、內(nèi)存,傳輸不受CPU的控制,并且是雙向通信。簡而言之,這個(gè)家伙就是一個(gè)速度很快的且不受老大控制的數(shù)據(jù)搬運(yùn)工。
處理器外設(shè)(內(nèi)核之外的外設(shè))
從結(jié)構(gòu)框圖上看,STM32的外設(shè)有串口、定時(shí)器、IO口、FSMC、SDIO、SPI、I2C等,這些外設(shè)按照速度的不同,分別掛載到AHB、APB2、APB1這三條總線上。
二、寄存器
什么是寄存器?寄存器是內(nèi)置于各個(gè)IP外設(shè)中,是一種用于配置外設(shè)功能的存儲器,并且有想對應(yīng)的地址。一切庫的封裝始于映射。
是不是“又臭又長”,如果進(jìn)行寄存器開發(fā),就需要懟地址以及對寄存器進(jìn)行字節(jié)賦值,不僅效率低而且容易出錯(cuò)。
來,開個(gè)玩笑。
你也許聽說過“國際C語言亂碼大賽(IOCCC)”下面這個(gè)例子就是網(wǎng)上廣為流傳的 一個(gè)經(jīng)典作品:
?
#include?? main(t,_,a)char?*a;{return!0 ?
庫的存在就是為了解決這類問題,將代碼語義化。語義化思想不僅僅是嵌入式有的,前端代碼也在追求語義特性。
三、萬物始于點(diǎn)燈
(1)內(nèi)核庫文件分析
cor_cm3.h
這個(gè)頭文件實(shí)現(xiàn)了:
1、內(nèi)核結(jié)構(gòu)體寄存器定義。?
2、內(nèi)核寄存器內(nèi)存映射。?
3、內(nèi)存寄存器位定義。跟處理器相關(guān)的頭文件stm32f10x.h實(shí)現(xiàn)的功能一樣,一個(gè)是針對內(nèi)核的寄存器,一個(gè)是針對內(nèi)核之外,即處理器的寄存器。
misc.h
內(nèi)核應(yīng)用函數(shù)庫頭文件,對應(yīng)stm32f10x_xxx.h。
misc.c
內(nèi)核應(yīng)用函數(shù)庫文件,對應(yīng)stm32f10x_xxx.c。在CM3這個(gè)內(nèi)核里面還有一些功能組件,如NVIC、SCB、ITM、MPU、CoreDebug,CM3帶有非常豐富的功能組件,但是芯片廠商在設(shè)計(jì)MCU的時(shí)候有一些并不是非要不可的,是可裁剪的,比如MPU、ITM等在STM32里面就沒有。
其中NVIC在每一個(gè)CM3內(nèi)核的單片機(jī)中都會有,但都會被裁剪,只能是CM3 NVIC的一個(gè)子集。在NVIC里面還有一個(gè)SysTick,是一個(gè)系統(tǒng)定時(shí)器,可以提供時(shí)基,一般為操作系統(tǒng)定時(shí)器所用。misc.h和mics.c這兩個(gè)文件提供了操作這些組件的函數(shù),并可以在CM3內(nèi)核單片機(jī)直接移植。
(2)處理器外設(shè)庫文件分析
startup_stm32f10x_hd.s
這個(gè)是由匯編編寫的啟動文件,是STM32上電啟動的第一個(gè)程序,啟動文件主要實(shí)現(xiàn)了
初始化堆棧指針 SP;
設(shè)置 PC 指針=Reset_Handler ;
設(shè)置向量表的地址,并 初始化向量表,向量表里面放的是 STM32 所有中斷函數(shù)的入口地址
調(diào)用庫函數(shù) SystemInit,把系統(tǒng)時(shí)鐘配置成 72M,SystemInit 在庫文件 stytem_stm32f10x.c 中定義;
跳轉(zhuǎn)到標(biāo)號_main,最終去到 C 的世界。
system_stm32f10x.c
這個(gè)文件的作用是里面實(shí)現(xiàn)了各種常用的系統(tǒng)時(shí)鐘設(shè)置函數(shù),有72M,56M,48, 36,24,8M,我們使用的是是把系統(tǒng)時(shí)鐘設(shè)置成72M。
Stm32f10x.h
這個(gè)頭文件非常重要,這個(gè)頭文件實(shí)現(xiàn)了:
1、處理器外設(shè)寄存器的結(jié)構(gòu)體定義。
2、處理器外設(shè)的內(nèi)存映射。
3、處理器外設(shè)寄存器的位定義。
關(guān)于 1 和 2 我們在用寄存器點(diǎn)亮 LED 的時(shí)候有講解。
其中 3:處理器外設(shè)寄存器的位定義,這個(gè)非常重要,具體是什么意思?
我們知道一個(gè)寄存器有很多個(gè)位,每個(gè)位寫 1 或者寫 0 的功能都是不一樣的,處理器外設(shè)寄存器的位定義就是把外設(shè)的每個(gè)寄存器的每一個(gè)位寫 1 的 16 進(jìn)制數(shù)定義成一個(gè)宏,宏名即用該位的名稱表示,如果我們操作寄存器要開啟某一個(gè)功能的話,就不用自己親自去算這個(gè)值是多少,可以直接到這個(gè)頭文件里面找。
我們以片上外設(shè) ADC 為例,假設(shè)我們要啟動 ADC 開始轉(zhuǎn)換,根據(jù)手冊我們知道是要控制 ADC_CR2 寄存器的位 0:ADON,即往位 0 寫 1,即:
?
ADC->CR2=0x00000001;?
這是一般的操作方法。現(xiàn)在這個(gè)頭文件里面有關(guān)于 ADON 位的位定義:
?
?#define?ADC_CR2_ADON?((uint32_t)0x00000001)?
有了這個(gè)位定義,我們剛剛的代碼就變成了:
?
ADC->CR2=ADC_CR2_ADON?
stm32f10x_xxx.h
外設(shè) xxx 應(yīng)用函數(shù)庫頭文件,這里面主要定義了實(shí)現(xiàn)外設(shè)某一功能的結(jié)構(gòu)體,比如通用定時(shí)器有很多功能,有定時(shí)功能,有輸出比較功能,有輸入捕捉功能,而通用定時(shí)器有非常多的寄存器要實(shí)現(xiàn)某一個(gè)功能。
比如定時(shí)功能,我們根本不知道具體要操作哪些寄存器,這個(gè)頭文件就為我們打包好了要實(shí)現(xiàn)某一個(gè)功能的寄存器,是以機(jī)構(gòu)體的形式定義的,比如通用定時(shí)器要實(shí)現(xiàn)一個(gè)定時(shí)的功能,我們只需要初始化 TIM_TimeBaseInitTypeDef 這個(gè)結(jié)構(gòu)體里面的成員即可,里面的成員就是定時(shí)所需要操作的寄存器。
有了這個(gè)頭文件,我們就知道要實(shí)現(xiàn)某個(gè)功能需要操作哪些寄存器,然后再回手冊中精度這些寄存器的說明即可。
stm32f10x_xxx.c
stm32f10x_xxx.c:外設(shè) xxx 應(yīng)用函數(shù)庫,這里面寫好了操作 xxx 外設(shè)的所有常用的函數(shù),我們使用庫編程的時(shí)候,使用的最多的就是這里的函數(shù)。
(3)SystemInit
工程中新建main.c 。
在此文件中編寫main函數(shù)后直接編譯會報(bào)錯(cuò):
Undefined symbol SystemInit (referred from startup_stm32f10x_hd.o).
錯(cuò)誤提示說SystemInit沒有定義。從分析啟動文件startup_stm32f10x_hd.s時(shí)我們知道,
?
;Reset?handler Reset_Handler?PROC EXPORT?Reset_Handler?[WEAK] IMPORT?__main ;IMPORT?SystemInit ;LDR?R0,?=SystemInit BLX?R0 LDR?R0,?=__main BX?R0 ENDP?
?
匯編中;分號是注釋的意思
?
第五行第六行代碼Reset_Handler調(diào)用了SystemInit該函數(shù)用來初始化系統(tǒng)時(shí)鐘,而該函數(shù)是在庫文件system_stm32f10x.c中實(shí)現(xiàn)的。我們重新寫一個(gè)這樣的函數(shù)也可以,把功能完整實(shí)現(xiàn)一遍,但是為了簡單起見,我們在main文件里面定義一個(gè)SystemInit空函數(shù),為的是騙過編譯器,把這個(gè)錯(cuò)誤去掉。
關(guān)于配置系統(tǒng)時(shí)鐘之后會出文章RCC時(shí)鐘樹詳細(xì)介紹,主要配置時(shí)鐘控制寄存器(RCC_CR)和時(shí)鐘配置寄存器(RCC_CFGR)這兩個(gè)寄存器,但最好是直接使用CubeMX直接生成,因?yàn)樗呐渲眠^程有些冗長。
如果我們用的是庫,那么有個(gè)庫函數(shù)SystemInit,會幫我們把系統(tǒng)時(shí)鐘設(shè)置成72M。
現(xiàn)在我們沒有使用庫,那現(xiàn)在時(shí)鐘是多少?答案是8M,當(dāng)外部HSE沒有開啟或者出現(xiàn)故障的時(shí)候,系統(tǒng)時(shí)鐘由內(nèi)部低速時(shí)鐘LSI提供,現(xiàn)在我們是沒有開啟HSE,所以系統(tǒng)默認(rèn)的時(shí)鐘是LSI=8M。
(4)庫封裝層級
如圖,達(dá)到第四層級便是我們所熟知的固件庫或HAL庫的效果。當(dāng)然庫的編寫還需要考慮許多問題,不止于這些內(nèi)容。我們需要的是了解庫封裝的大概過程。
將庫封裝等級分為四級來介紹是為了有層次感,就像打怪升級一樣,進(jìn)行認(rèn)知理解的升級。
我們都知道,操作GPIO輸出分三大步:
時(shí)鐘控制:
STM32 外設(shè)很多,為了降低功耗,每個(gè)外設(shè)都對應(yīng)著一個(gè)時(shí)鐘,在系統(tǒng)復(fù)位的時(shí)候這些時(shí)鐘都是被關(guān)閉的,如果想要外設(shè)工作,必須把相應(yīng)的時(shí)鐘打開。
STM32 的所有外設(shè)的時(shí)鐘由一個(gè)專門的外設(shè)來管理,叫RCC(reset and clockcontrol),RCC 在STM32 參考手冊的第六章。
STM32 的外設(shè)因?yàn)樗俾实牟煌謩e掛載到三條總系上:AHB、APB2、APB1,AHB為高速總線,APB2 次之,APB1 再次之。所以的IO 口都掛載到APB2 總線上,屬于高速外設(shè)。
模式配置:
這個(gè)由端口配置寄存器來控制。端口配置寄存器分為高低兩個(gè),每4bit 控制一個(gè)IO 口,所以端口配置低寄存器:CRL 控制這IO 口的低8 位,端口配置高寄存器:CRH控制這IO 口的高8bit。
在4 位一組的控制位中,CNFy[1:0] 用來控制端口的輸入輸出,MODEy[1:0]用來控制輸出模式的速率,又稱驅(qū)動電路的響應(yīng)速度,注意此處速率與程序無關(guān),GPIO引腳速度、翻轉(zhuǎn)速度、輸出速度區(qū)別輸入有4種模式,輸出有4種模式,我們在控制LED 的時(shí)候選擇通用推挽輸出。
輸出速率有三種模式:2M、10M、50M,這里我們選擇2M。
電平控制:
STM32的IO口比較復(fù)雜,如果要輸出1和0,則要通過控制:端口輸出數(shù)據(jù)寄存器ODR來實(shí)現(xiàn),ODR?是:Output data register的簡寫,在STM32里面,其寄存器的命名名稱都是英文的簡寫,很容易記住。
從手冊上我們知道ODR是一個(gè)32位的寄存器,低16位有效,高16位保留。低16位對應(yīng)著IO0~IO16,只要往相應(yīng)的位置寫入0或者1就可以輸出低或者高電平。
第一層級:基地址宏定義?
時(shí)鐘控制:
在STM32中,每個(gè)外設(shè)都有一個(gè)起始地址,叫做外設(shè)基地址,外設(shè)的寄存器就以這個(gè)基地址為標(biāo)準(zhǔn)按照順序排列,且每個(gè)寄存器32位,(后面作為結(jié)構(gòu)體里面的成員正好內(nèi)存對齊)。
查表看到時(shí)鐘由APB2外設(shè)時(shí)鐘使能寄存器(RCC_APB2ENR)來控制,其中PB端口的時(shí)鐘由該寄存器的位3寫1使能。我們可以通過基地址+偏移量0x18,算出RCC_APB2ENR的地址為:0x40021018。那么使能PB口的時(shí)鐘代碼則如下所示:
?
?#define?RCC_APB2ENR?*(volatile?unsigned?long?*)0x40021018 ?//?開啟端口B?時(shí)鐘 ?RCC_APB2ENR?|=?1<<3;?
模式配置:
同RCC_APB2ENR一樣,GPIOB的起始地址是:0X4001 0C00,我們也可以算出GPIO_CRL的地址為:0x40010C00。那么設(shè)置PB0為通用推挽輸出,輸出速率為2M的代碼則如下所示:
同上,從手冊中我們看到ODR寄存器的地址偏移是:0CH,可以算出GPIOB_ODR寄存器的地址是:0X4001 0C00 + 0X0C = 0X4001 0C0C。現(xiàn)在我們就可以定義GPIOB_ODR這個(gè)寄存器了,代碼如下:
?
#define?GPIOB_ODR?*(volatile?unsigned?long?*)0x40010C0C //PB0?輸出低電平 GPIOB_ODR?=?0<<0;?
第一層級:基地址宏定義完成用STM32控制一個(gè)LED的完整代碼:
?
#define?RCC_APB2ENR?*(volatile?unsigned?long?*)0x40021018 #define?GPIOB_CRL?*(volatile?unsigned?long?*)0x40010C00 #define?GPIOB_ODR?*(volatile?unsigned?long?*)0x40010C0C int?main(void) { ?//?開啟端口B?的時(shí)鐘 ?RCC_APB2ENR?|=?1<<3; ?//?配置PB0?為通用推挽輸出模式,速率為2M ?GPIOB_CRL?=?(2<<0)?|?(0<<2); ?//?PB0?輸出低電平,點(diǎn)亮LED ?GPIOB_ODR?=?0<<0; } void?SystemInit(void) { }?
第二層級:基地址宏定義+結(jié)構(gòu)體封裝
外設(shè)寄存器結(jié)構(gòu)體封裝
上面我們在操作寄存器的時(shí)候,操作的是寄存器的絕對地址,如果每個(gè)寄存器都這樣操作,那將非常麻煩。我們考慮到外設(shè)寄存器的地址都是基于外設(shè)基地址的偏移地址,都是在外設(shè)基地址上逐個(gè)連續(xù)遞增的,每個(gè)寄存器占32個(gè)或者16個(gè)字節(jié),這種方式跟結(jié)構(gòu)體里面的成員類似。
所以我們可以定義一種外設(shè)結(jié)構(gòu)體,結(jié)構(gòu)體的地址等于外設(shè)的基地址,結(jié)構(gòu)體的成員等于寄存器,成員的排列順序跟寄存器的順序一樣。這樣我們操作寄存器的時(shí)候就不用每次都找到絕對地址,只要知道外設(shè)的基地址就可以操作外設(shè)的全部寄存器,即操作結(jié)構(gòu)體的成員即可。
下面我們先定義一個(gè)GPIO寄存器結(jié)構(gòu)體,結(jié)構(gòu)體里面的成員是GPIO的寄存器,成員的順序按照寄存器的偏移地址從低到高排列,成員類型跟寄存器類型一樣。
typedef?struct?
{
?volatile?uint32_t?CRL;
?volatile?uint32_t?CRH;
?volatile?uint32_t?IDR;
?volatile?uint32_t?ODR;
?volatile?uint32_t?BSRR;
?volatile?uint32_t?BRR;
?volatile?uint32_t?LCKR;
}?GPIO_TypeDef;在《STM32 中文參考手冊》8.2 寄存器描述章節(jié),我們可以找到結(jié)構(gòu)體里面的7個(gè)寄存器描述。在點(diǎn)亮LED的時(shí)候我們只用了CRL和ODR這兩個(gè)寄存器,至于其他寄存器的功能大家可以自行看手冊了解。
在GPIO結(jié)構(gòu)體里面我們用了兩個(gè)數(shù)據(jù)類型,一個(gè)是uint32_t,表示無符號的32位整型,因?yàn)镚PIO的寄存器都是32位的。這個(gè)類型聲明在標(biāo)準(zhǔn)頭文件stdint.h 里面使用typedef對unsigned int重命名,我們在程序上只要包含這個(gè)頭文件即可。
另外一個(gè)是volatile作用就是告訴編譯器這里的變量會變化不因優(yōu)化而省略此指令,必須每次都直接讀寫其值,這樣就能確保每次讀或者寫寄存器都真正執(zhí)行到位。
外設(shè)封裝
STM32F1系列的GPIO端口分A~G,即GPIOA、GPIOB。。。。。。GPIOG。每個(gè)端口都含有GPIO_TypeDef結(jié)構(gòu)體里面的寄存器,我們可以根據(jù)手冊各個(gè)端口的基地址把GPIO的各個(gè)端口定義成一個(gè)GPIO_TypeDef類型指針,然后我們就可以根據(jù)端口名(實(shí)際上現(xiàn)在是結(jié)構(gòu)體指針了)來操作各個(gè)端口的寄存器,代碼實(shí)現(xiàn)如下:
?
#define?GPIOA?((GPIO_TypeDef?*)?0X4001?0800) #define?GPIOB?((GPIO_TypeDef?*)?0X4001?0C00) #define?GPIOC?((GPIO_TypeDef?*)?0X4001?1000) #define?GPIOD?((GPIO_TypeDef?*)?0X4001?1400) #define?GPIOE?((GPIO_TypeDef?*)?0X4001?1800) #define?GPIOF?((GPIO_TypeDef?*)?0X4001?1C00) #define?GPIOG?((GPIO_TypeDef?*)?0X4001?2000)?
外設(shè)內(nèi)存映射
講到基地址的時(shí)候我們再引人一個(gè)知識點(diǎn):Cortex-M3存儲器系統(tǒng),這個(gè)知識點(diǎn)在《Cortex-M3權(quán)威指南》第5章里面講到。CM3的地址空間是4GB,如下圖所示:
我們這里要講的是片上外設(shè),就是我們所說的寄存器的根據(jù)地,其大小總共有512MB,512MB是其極限空間,并不是每個(gè)單片機(jī)都用得完,實(shí)際上各個(gè)MCU廠商都只是用了一部分而已。STM32F1系列用到了:0x4000 0000 ~0x5003 FFFF。現(xiàn)在我們說的STM32的寄存器就是位于這個(gè)區(qū)域
APB1、APB2、AHB 總線基地址
現(xiàn)在我們說的STM32的寄存器就是位于這個(gè)區(qū)域,這里面ST設(shè)計(jì)了三條總線:AHB、APB2和APB1,其中AHB和APB2是高速總線,APB1是低速總線。不同的外設(shè)根據(jù)速度不同分別掛載到這三條總線上。
從下往上依次是:APB1、APB2、AHB,每個(gè)總線對應(yīng)的地址分別是:APB1:0x40000000,APB2:0x4001 0000,AHB:0x4001 8000。
這三條總線的基地址我們是從《STM32 中文參考手冊》2.3小節(jié)—存儲器映像得到的:APB1的基地址是TIM2定時(shí)器的起始地址,APB2的基地址是AFIO的起始地址,AHB的基地址是SDIO的起始地址。其中APB1地址又叫做外設(shè)基地址,是所有外設(shè)的基地址,叫做PERIPH_BASE。
現(xiàn)在我們把這三條總線地址用宏定義出來,以后我們在定義其他外設(shè)基地址的時(shí)候,只需要在這三條總線的基址上加上偏移地址即可,代碼如下:
?
#define?PERIPH_BASE?((uint32_t)0x40000000) #define?APB1PERIPH_BASE?PERIPH_BASE #define?APB2PERIPH_BASE?(PERIPH_BASE?+?0x10000) #define?AHBPERIPH_BASE?(PERIPH_BASE?+?0x20000)?
GPIO 端口基地址
因?yàn)镚PIO掛載到APB2總線上,那么現(xiàn)在我們就可以根據(jù)APB2的基址算出各個(gè)GPIO端口的基地址,用宏定義實(shí)現(xiàn)代碼如下:
?
#define?GPIOA_BASE?(APB2PERIPH_BASE?+?0x0800) #define?GPIOB_BASE?(APB2PERIPH_BASE?+?0x0C00) #define?GPIOC_BASE?(APB2PERIPH_BASE?+?0x1000) #define?GPIOD_BASE?(APB2PERIPH_BASE?+?0x1400) #define?GPIOE_BASE?(APB2PERIPH_BASE?+?0x1800) #define?GPIOF_BASE?(APB2PERIPH_BASE?+?0x1C00) #define?GPIOG_BASE?(APB2PERIPH_BASE?+?0x2000)?
第二層級:基地址宏定義+結(jié)構(gòu)體封裝完成用STM32控制一個(gè)LED的完整代碼:
?
#include?#define?__IO?volatile typedef?struct? { ?__IO?uint32_t?CRL; ?__IO?uint32_t?CRH; ?__IO?uint32_t?IDR; ?__IO?uint32_t?ODR; ?__IO?uint32_t?BSRR; ?__IO?uint32_t?BRR; ?__IO?uint32_t?LCKR; }?GPIO_TypeDef; typedef?struct? { ?__IO?uint32_t?CR; ?__IO?uint32_t?CFGR; ?__IO?uint32_t?CIR; ?__IO?uint32_t?APB2RSTR; ?__IO?uint32_t?APB1RSTR; ?__IO?uint32_t?AHBENR; ?__IO?uint32_t?APB2ENR; ?__IO?uint32_t?APB1ENR; ?__IO?uint32_t?BDCR; ?__IO?uint32_t?CSR; }?RCC_TypeDef; #define?PERIPH_BASE?((uint32_t)0x40000000) #define?APB1PERIPH_BASE?PERIPH_BASE #define?APB2PERIPH_BASE?(PERIPH_BASE?+?0x10000) #define?AHBPERIPH_BASE?(PERIPH_BASE?+?0x20000) #define?GPIOA_BASE?(APB2PERIPH_BASE?+?0x0800) #define?GPIOB_BASE?(APB2PERIPH_BASE?+?0x0C00) #define?GPIOC_BASE?(APB2PERIPH_BASE?+?0x1000) #define?GPIOD_BASE?(APB2PERIPH_BASE?+?0x1400) #define?GPIOE_BASE?(APB2PERIPH_BASE?+?0x1800) #define?GPIOF_BASE?(APB2PERIPH_BASE?+?0x1C00) #define?GPIOG_BASE?(APB2PERIPH_BASE?+?0x2000) #define?RCC_BASE?(AHBPERIPH_BASE?+?0x1000) #define?GPIOA?((GPIO_TypeDef?*)?GPIOA_BASE) #define?GPIOB?((GPIO_TypeDef?*)?GPIOB_BASE) #define?GPIOC?((GPIO_TypeDef?*)?GPIOC_BASE) #define?GPIOD?((GPIO_TypeDef?*)?GPIOD_BASE) #define?GPIOE?((GPIO_TypeDef?*)?GPIOE_BASE) #define?GPIOF?((GPIO_TypeDef?*)?GPIOF_BASE) #define?GPIOG?((GPIO_TypeDef?*)?GPIOG_BASE) #define?RCC?((RCC_TypeDef?*)?RCC_BASE) #define?RCC_APB2ENR?*(volatile?unsigned?long?*)0x40021018 #define?GPIOB_CRL?*(volatile?unsigned?long?*)0x40010C00 #define?GPIOB_ODR?*(volatile?unsigned?long?*)0x40010C0C int?main(void) { ?//?開啟端口B?的時(shí)鐘 ?RCC->APB2ENR?|=?1<<3; ?//?配置PB0?為通用推挽輸出模式,速率為2M ?GPIOB->CRL?=?(2<<0)?|?(0<<2); ?//?PB0?輸出低電平,點(diǎn)亮LED ?GPIOB->ODR?=?0<<0; } void?SystemInit(void) { } ?
第二層級變化:
①、定義一個(gè)外設(shè)(GPIO)寄存器結(jié)構(gòu)體,結(jié)構(gòu)體的成員包含該外設(shè)的所有寄存器,成員的排列順序跟寄存器偏移地址一樣,成員的數(shù)據(jù)類型跟寄存器的一樣。
②外設(shè)內(nèi)存映射,即把地址跟外設(shè)建立起一一對應(yīng)的關(guān)系。
③外設(shè)聲明,即把外設(shè)的名字定義成一個(gè)外設(shè)寄存器結(jié)構(gòu)體類型的指針。
④通過結(jié)構(gòu)體操作寄存器,實(shí)現(xiàn)點(diǎn)亮LED。
第三層級:基地址宏定義+結(jié)構(gòu)體封裝+“位封裝”(每一位的對應(yīng)字節(jié)封裝)
上面我們在控制GPIO輸出內(nèi)容的時(shí)候控制的是ODR(Output data register)寄存器,ODR是一個(gè)16位的寄存器,必須以字的形式控制其實(shí)我們還可以控制BSRR和BRR這兩個(gè)寄存器來控制IO的電平,下面我們簡單介紹下BRR寄存器的功能,BSRR自行看手冊研究。
位清除寄存器BRR只能實(shí)現(xiàn)位清0操作,是一個(gè)32位寄存器,低16位有效,寫0沒影響,寫1清0。現(xiàn)在我們要使PB0輸出低電平,點(diǎn)亮LED,則只要往BRR的BR0位寫1即可,其他位為0,代碼如下:
?
GPIOB->BRR?=?0X0001;?
這時(shí)PB0就輸出了低電平,LED就被點(diǎn)亮了。
如果要PB2輸出低電平,則是:
?
GPIOB->BRR?=?0X0004;?
如果要PB3/4/5/6。。。。。。這些IO輸出低電平呢?
道理是一樣的,只要往BRR的相應(yīng)位置賦不同的值即可。因?yàn)锽RR是一個(gè)16位的寄存器,位數(shù)比較多,賦值的時(shí)候容易出錯(cuò),而且從賦值的16進(jìn)制數(shù)字我們很難清楚的知道控制的是哪個(gè)IO。
這時(shí),我們是否可以把BRR的每個(gè)位置1都用宏定義來實(shí)現(xiàn),如GPIO_Pin_0就表示0X0001,GPIO_Pin_2就表示0X0004。只要我們定義一次,以后都可以使用,而且還見名知意。“位封裝”(每一位的對應(yīng)字節(jié)封裝) 代碼如下:
?
#define?GPIO_Pin_0?((uint16_t)0x0001)?/*!?
這時(shí)PB0就輸出了低電平的代碼就變成了:
?
GPIOB->BRR?=?GPIO_Pin_0;?
(如果同時(shí)讓PB0/PB15輸出低電平,用或運(yùn)算,代碼:
?
GPIOB->BRR?=?GPIO_Pin_0|GPIO_Pin_15;?
為了不使main函數(shù)看起來冗余,上述庫封裝 的代碼不應(yīng)該放在main里面,因?yàn)槠涫歉鶪PIO相關(guān)的,我們可以把這些宏放在一個(gè)單獨(dú)的頭文件里面。
在工程目錄下新建stm32f10x_gpio.h,把封裝代碼放里面,然后把這個(gè)文件添加到工程里面。這時(shí)我們只需要在main.c里面包含這個(gè)頭文件即可。
第四層級:基地址宏定義+結(jié)構(gòu)體封裝+“位封裝”+函數(shù)封裝
我們點(diǎn)亮LED的時(shí)候,控制的是PB0這個(gè)IO,如果LED接到的是其他IO,我們就需要把GPIOB修改成其他的端口,其實(shí)這樣修改起來也很快很方便。
但是為了提高程序的可讀性和可移植性,我們是否可以編寫一個(gè)專門的函數(shù)用來復(fù)位GPIO的某個(gè)位,這個(gè)函數(shù)有兩個(gè)形參,一個(gè)是GPIOX(X=A...G),另外一個(gè)是GPIO_Pin(0...15),函數(shù)的主體則是根據(jù)形參GPIOX 和GPIO_Pin來控制BRR寄存器,代碼如下:
?
void?GPIO_ResetBits(GPIO_TypeDef*?GPIOx,?uint16_t?GPIO_Pin) { ?GPIOx->BRR?=?GPIO_Pin; }?
這時(shí),PB0輸出低電平,點(diǎn)亮LED的代碼就變成了:
?
GPIO_ResetBits(GPIOB,GPIO_Pin_0);?
同理, 我們可以控制BSRR這個(gè)寄存器來實(shí)現(xiàn)關(guān)閉LED,代碼如下:
?
//?GPIO?端口置位函數(shù) void?GPIO_SetBits(GPIO_TypeDef*?GPIOx,?uint16_t?GPIO_Pin) { ?GPIOx->BSRR?=?GPIO_Pin; }?
這時(shí),PB0輸出高電平,關(guān)閉LED的代碼就變成了:
?
GPIO_SetBits(GPIOB,GPIO_Pin_0);?
同樣,因?yàn)檫@個(gè)函數(shù)是控制GPIO的函數(shù),我們可以新建一個(gè)專門的文件來放跟gpio有關(guān)的函數(shù)。
在工程目錄下新建stm32f10x_gpio.c,把GPIO相關(guān)的函數(shù)放里面。這時(shí)我們是否發(fā)現(xiàn)剛剛新建了一個(gè)頭文件stm32f10x_gpio.h,這兩個(gè)文件存放的都是跟外設(shè)GPIO相關(guān)的。
C文件里面的函數(shù)會用到h頭文件里面的定義,這兩個(gè)文件是相輔相成的,故我們在stm32f10x_gpio.c 文件中也包含stm32f10x_gpio.h這個(gè)頭文件。別忘了把stm32f10x.h這個(gè)頭文件也包含進(jìn)去,因?yàn)橛嘘P(guān)寄存器的所有定義都在這個(gè)頭文件里面。
如果我們寫其他外設(shè)的函數(shù),我們也應(yīng)該跟GPIO一樣,新建兩個(gè)文件專門來存函數(shù),比如RCC這個(gè)外設(shè)我們可以新建stm32f10x_rcc.c和stm32f10x_rcc.h。其他外依葫蘆畫瓢即可。
(5)實(shí)例編寫
以上,是對庫封住過程的概述,下面我們正在地使用庫函數(shù)編寫LED程序
①管理庫的頭文件
當(dāng)我們開始調(diào)用庫函數(shù)寫代碼的時(shí)候,有些庫我們不需要,在編譯的時(shí)候可以不編譯,可以通過一個(gè)總的頭文件stm32f10x_conf.h來控制,該頭文件主要代碼如下:
?
//#include?"stm32f10x_adc.h" //#include?"stm32f10x_bkp.h" //#include?"stm32f10x_can.h" //#include?"stm32f10x_cec.h" //#include?"stm32f10x_crc.h" //#include?"stm32f10x_dac.h" //#include?"stm32f10x_dbgmcu.h" //#include?"stm32f10x_dma.h" //#include?"stm32f10x_exti.h" //#include?"stm32f10x_flash.h" //#include?"stm32f10x_fsmc.h" #include?"stm32f10x_gpio.h" //#include?"stm32f10x_i2c.h" //#include?"stm32f10x_iwdg.h" //#include?"stm32f10x_pwr.h" #include?"stm32f10x_rcc.h" //#include?"stm32f10x_rtc.h" //#include?"stm32f10x_sdio.h" //#include?"stm32f10x_spi.h" //#include?"stm32f10x_tim.h" //#include?"stm32f10x_usart.h" //#include?"stm32f10x_wwdg.h" //#include?"misc.h"?
這里面包含了全部外設(shè)的頭文件,點(diǎn)亮一個(gè)LED我們只需要RCC和GPIO 這兩個(gè)外設(shè)的庫函數(shù)即可,其中RCC控制的是時(shí)鐘,GPIO控制的具體的IO口。所以其他外設(shè)庫函數(shù)的頭文件我們注釋掉,當(dāng)我們需要的時(shí)候就把相應(yīng)頭文件的注釋去掉即可。
stm32f10x_conf.h這個(gè)頭文件在stm32f10x.h這個(gè)頭文件的最后面被包含,在第8296行:
?
#ifdef?USE_STDPERIPH_DRIVER #include?"stm32f10x_conf.h" #endif?
代碼的意思是,如果定義了USE_STDPERIPH_DRIVER這個(gè)宏的話,就包含stm32f10x_conf.h這個(gè)頭文件。
我們在新建工程的時(shí)候,在魔術(shù)棒選項(xiàng)卡C/C++中,我們定義了USE_STDPERIPH_DRIVER 這個(gè)宏,所以stm32f10x_conf.h 這個(gè)頭文件就被stm32f10x.h包含了,我們在寫程序的時(shí)候只需要調(diào)用一個(gè)頭文件:stm32f10x.h即可。
②編寫LED初始化函數(shù)
經(jīng)過寄存器點(diǎn)亮LED的操作,我們知道操作一個(gè)GPIO輸出的編程要點(diǎn)大概如下:
1、開啟GPIO的端口時(shí)鐘
2、選擇要具體控制的IO口,即pin
3、選擇IO口輸出的速率,即speed
4、選擇IO口輸出的模式,即mode
5、輸出高/低電平
STM32的時(shí)鐘功能非常豐富,配置靈活,為了降低功耗,每個(gè)外設(shè)的時(shí)鐘都可以獨(dú)自的關(guān)閉和開啟。STM32中跟時(shí)鐘有關(guān)的功能都由RCC這個(gè)外設(shè)控制,RCC中有三個(gè)寄存器控制著所以外設(shè)時(shí)鐘的開啟和關(guān)閉:RCC_APHENR、RCC_APB2ENR和RCC_APB1ENR,AHB、APB2和APB1代表著三條總線,所有的外設(shè)都是掛載到這三條總線上,GPIO屬于高速的外設(shè),掛載到APB2總線上,所以其時(shí)鐘有RCC_APB2ENR控制。
GPIO 時(shí)鐘控制
固件庫函數(shù):RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE)函數(shù)的
原型為:
?
void?RCC_APB2PeriphClockCmd(uint32_t?RCC_APB2Periph, ??????????????????????????????FunctionalState?NewState) { ?/*?Check?the?parameters?*/ ?assert_param(IS_RCC_APB2_PERIPH(RCC_APB2Periph)); ?assert_param(IS_FUNCTIONAL_STATE(NewState)); ?if?(NewState?!=?DISABLE)? ?{ ??RCC->APB2ENR?|=?RCC_APB2Periph; ?}? ?else? ?{ ??RCC->APB2ENR?&=?~RCC_APB2Periph; ?} }?
當(dāng)程序編譯一次之后,把光標(biāo)定位到函數(shù)/變量/宏定義處,按鍵盤的F12或鼠標(biāo)右鍵的Go to definition of,就可以找到原型。固件庫的底層操作的就是RCC外設(shè)的APB2ENR這個(gè)寄存器,宏RCC_APB2Periph_GPIOB的原型是:0x00000008,即(1<<3),還原成存器操作就是:RCC->APB2ENR |= 1<<<3。相比固件庫操作,寄存器操作的代碼可讀性就很差,只有才查閱寄存器配置才知道具體代碼的功能,而固件庫操作恰好相反,見名知意。
GPIO 端口配置
GPIO的pin,速度,模式,都由GPIO的端口配置寄存器來控制,其中IO0~IO7由端口配置低寄存器CRL控制,IO8~IO15由端口配置高寄存器CRH配置。固件庫把端口配置的pin,速度和模式封裝成一個(gè)結(jié)構(gòu)體:
?
typedef?struct? { ?uint16_t?GPIO_Pin; ?GPIOSpeed_TypeDef?GPIO_Speed; ?GPIOMode_TypeDef?GPIO_Mode; }?GPIO_InitTypeDef;?
pin可以是GPIO_Pin_0~GPIO_Pin_15或者是GPIO_Pin_All,這些都是庫預(yù)先定義好的宏。speed也被封裝成一個(gè)結(jié)構(gòu)體:
?
typedef?enum? { ?GPIO_Speed_10MHz?=?1, ?GPIO_Speed_2MHz, ?GPIO_Speed_50MHz }?GPIOSpeed_TypeDef;?
速度可以是10M,2M或者50M,這個(gè)由端口配置寄存器的MODE位控制,速度是針對IO口輸出的時(shí)候而言,在輸入的時(shí)候可以不用設(shè)置。mode也被封裝成一個(gè)結(jié)構(gòu)體:
?
typedef?enum? { ?GPIO_Mode_AIN?=?0x0,?//?模擬輸入 ?GPIO_Mode_IN_FLOATING?=?0x04,?//?浮空輸入(復(fù)位后的狀態(tài)) ?GPIO_Mode_IPD?=?0x28,?//?下拉輸入 ?GPIO_Mode_IPU?=?0x48,?//?上拉輸入 ?GPIO_Mode_Out_OD?=?0x14,?//?通用開漏輸出 ?GPIO_Mode_Out_PP?=?0x10,?//?通用推挽輸出 ?GPIO_Mode_AF_OD?=?0x1C,?//?復(fù)用開漏輸出 ?GPIO_Mode_AF_PP?=?0x18?//?復(fù)用推挽輸出 }?GPIOMode_TypeDef;?
IO口的模式有8種,輸入輸出各4種,由端口配置寄存器的CNF配置。平時(shí)用的最多的就是通用推挽輸出,可以輸出高低電平,驅(qū)動能力大,一般用于接數(shù)字器件。
最終用固件庫實(shí)現(xiàn)就變成這樣:
?
//?定義一個(gè)GPIO_InitTypeDef?類型的結(jié)構(gòu)體 GPIO_InitTypeDef?GPIO_InitStructure; //?選擇要控制的IO?口 GPIO_InitStructure.GPIO_Pin?=?GPIO_Pin_0; //?設(shè)置引腳為推挽輸出 GPIO_InitStructure.GPIO_Mode?=?GPIO_Mode_Out_PP; //?設(shè)置引腳速率為50MHz GPIO_InitStructure.GPIO_Speed?=?GPIO_Speed_2MHz; /*調(diào)用庫函數(shù),初始化GPIOB0*/ GPIO_Init(GPIOB,?&GPIO_InitStructure);?
倘若同一端口下不同引腳有不同的模式配置,每次對每個(gè)引腳配置完成后都要調(diào)用GPIO初始化函數(shù),代碼如下:
?
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輸出控制,可以通過端口數(shù)據(jù)輸出寄存器ODR、端口位設(shè)置/清除寄存器BSRR和端口位清除寄存器BRR這三個(gè)來控制。端口輸出寄存器ODR是一個(gè)32位的寄存器,低16位有效,對應(yīng)著IO0~IO15,只能以字的形式操作,一般使用寄存器操作。
?
//?PB0?輸出高電平,點(diǎn)亮LED GPIOB->ODR?=?1<<0;?
端口位清除寄存器BRR是一個(gè)32位的寄存器,低十六位有效,對應(yīng)著IO0~IO15,只能以字的形式操作,可以單獨(dú)對某一個(gè)位操作,寫1清0。
?
//?PB0?輸出低電平,點(diǎn)亮LED GPIO_ResetBits(GPIOB,?GPIO_Pin_0);?
BSRR是一個(gè)32位的寄存器,低16位用于置位,寫1有效,高16位用于復(fù)位,寫1有效,相當(dāng)于BRR寄存器。高16位我們一般不用,而是操作BRR這個(gè)寄存器,所以BSRR這個(gè)寄存器一般用來置位操作。
?
//?PB0?輸出高電平,熄滅LED GPIO_SetBits(GPIOB,?GPIO_Pin_0);?
綜上:固件庫LED GPIO初始化函數(shù)
?
void?LED_GPIO_Config(void) { ?//?定義一個(gè)GPIO_InitTypeDef?類型的結(jié)構(gòu)體 ?GPIO_InitTypeDef?GPIO_InitStructure; ?//?開啟GPIOB?的時(shí)鐘 ?RCC_APB2PeriphClockCmd(?RCC_APB2Periph_GPIOB,?ENABLE); ?//?選擇要控制的IO?口 ?GPIO_InitStructure.GPIO_Pin?=?GPIO_Pin_0; ?//?設(shè)置引腳為推挽輸出 ?GPIO_InitStructure.GPIO_Mode?=?GPIO_Mode_Out_PP; ?//?設(shè)置引腳速率為50MHz ?GPIO_InitStructure.GPIO_Speed?=?GPIO_Speed_50MHz; ?/*調(diào)用庫函數(shù),初始化GPIOB0*/ ?GPIO_Init(GPIOB,?&GPIO_InitStructure); ?//?關(guān)閉LED ?GPIO_SetBits(GPIOB,?GPIO_Pin_0); }?
主函數(shù)
?
#include?"stm32f10x.h" void?SOFT_Delay(__IO?uint32_t?nCount); void?LED_GPIO_Config(void); int?main(void) { ?//?程序來到main 函數(shù)之前,啟動文件:statup_stm32f10x_hd.s 已經(jīng)調(diào)用 ?//?SystemInit()函數(shù)把系統(tǒng)時(shí)鐘初始化成72MHZ ?//?SystemInit()在system_stm32f10x.c?中定義 ?//?如果用戶想修改系統(tǒng)時(shí)鐘,可自行編寫程序修改 ?LED_GPIO_Config(); ?while?(?1?)? ?{ ??//?點(diǎn)亮LED ??GPIO_ResetBits(GPIOB,?GPIO_Pin_0); ??Time_Delay(0x0FFFFF); ??//?熄滅LED ??GPIO_SetBits(GPIOB,?GPIO_Pin_0); ??Time_Delay(0x0FFFFF); ?} } //?簡陋的軟件延時(shí)函數(shù) void?Time_Delay(volatile?uint32_t?Count) { ?for?(;?Count?!=?0;?Count--); }?
注意void Time_Delay(volatile uint32_t Count)只是一個(gè)簡陋的軟件延時(shí)函數(shù),如果小伙伴們有興趣可以看一看MultiTimer,它是一個(gè)軟件定時(shí)器擴(kuò)展模塊,可無限擴(kuò)展所需的定時(shí)器任務(wù),取代傳統(tǒng)的標(biāo)志位判斷方式, 更優(yōu)雅更便捷地管理程序的時(shí)間觸發(fā)時(shí)序。
?
審核編輯:湯梓紅
評論
查看更多