開發環境:Keil 5.30
上一章通過控制GPIO的高低電平實現了流水燈,但只是告訴了大家怎么做,如何實現流水燈,本文將深入剖析的GPIO流水燈的前生今世,深入研究流水燈的調用邏輯和數據結構。
2.1 GPIO配置概述
前面一章一個大概講解GPIO的配置過程和核心的寄存器,當然啦,關于GPIO的寄存器遠不止我上一章列出來的,還有很多,具體請參看《STM32F10XXX參考手冊》中GPIO相關的內容吧。
根據前面實現的GPIO流水燈,本文將其歸納如下:
要想控制3個LED依次亮滅,就需要做以上三件事:使能時鐘,配置GPIO參數,最后循環控制GPIO的高低電平就能實現流水燈的效果,GPIO的寄存器這里就不說了,更多詳細的寄存器描述看官方手冊就行,下面先來看看STM32的時鐘。
2.2 Cortex-M的時鐘系統
2.2.1 Cortex-M的系統架構
基于Cortex-M的系統架構比51單片機強大很多了。首先我們看看Cortex-M3的系統架構圖:
對于對比,在看看GD32F407xx的系統架構圖。
GD32F407xx是基于Cortex-M4設計的。可以看到,Cortex-M3和Cortex-M4大體是相同的,系統主要由四個驅動單元和四個被動單元構成。
- 四個驅動單元
- 內核 DCode 總線;
- 系統總線;
- 通用 DMA1(DMA0);
- 通用 DMA2(DMA1);
- 四被動單元
- AHB 到 APB 的橋:連接所有的 APB 設備;
- 內部 FlASH 閃存;
- 內部 SRAM;
- FSMC;
下面我們具體講解一下圖中幾個總線的知識:
① ICode 總線:該總線將 M3 內核指令總線和閃存指令接口相連,指令的預取在該總線上面完成。
② DCode 總線:該總線將 M3 內核的 DCode 總線與閃存存儲器的數據接口相連接,常量加載和調試訪問在該總線上面完成。
③系統總線:該總線連接 M3 內核的系統總線到總線矩陣,總線矩陣協調內核和 DMA 間訪問。
④ DMA 總線:該總線將 DMA 的 AHB 主控接口與總線矩陣相連,總線矩陣協調 CPU 的DCode 和 DMA 到 SRAM,閃存和外設的訪問。
⑤總線矩陣:總線矩陣協調內核系統總線和 DMA 主控總線之間的訪問仲裁,仲裁利用輪換算法。
⑥ AHB/APB 橋:這兩個橋在 AHB 和 2 個 APB 總線間提供同步連接, APB1 操作速度限于36MHz,APB2 操作速度全速。
對于系統架構的知識,在剛開始學習 STM32 的時候只需要一個大概的了解,大致知道是個什么情況即可。
2.2.2 STM32時鐘架構
時鐘是整個處理器運行的基礎,時鐘信號推動處理器內各個部分執行相應的指令。時鐘系統就是CPU的脈搏,決定CPU速率,像人的心跳一樣只有有了心跳,人才能做其他的事情,而單片機有了時鐘,才能夠運行執行指令,才能夠做其他的處理 (點燈,串口,ADC),時鐘的重要性不言而喻。
我們在學習51單片機時,其最小系統必有晶振電路,這塊電路就是單片機的時鐘來源,晶振的振蕩頻率直接影響單片機的處理速度。STM32相比51單片機就復雜得多,不僅是外設非常多,就連時鐘來源就有四個。但我們實際使用的時候只會用到有限的幾個外設,使用任何外設都需要時鐘才能啟動,但并不是所有的外設都需要系統時鐘那么高的頻率,為了兼容不同速度的設備,有些高速,有些低速,如果都用高速時鐘,勢必造成浪費,而且,同一個電路,時鐘越快功耗越快,同時抗電磁干擾能力也就越弱,所以較為復雜的MCU都是采用多時鐘源的方法來解決這些問題,因此便有了STM32的時鐘系統和時鐘樹。
STM32三個不同的時鐘源可以用來驅動系統時鐘(SYSCLK):
● HSI晶振時鐘(高速內部時鐘信號)
● HSE晶振時鐘(高速外部時鐘信號)
● PLL時鐘
STM32有兩個二級時鐘源:
● 40kHz的低速內部RC,它可以驅動獨立看門狗,還可選擇地通過程序選擇驅動RTC。 RTC用于從停機/待機模式下自動喚醒系統。
● 32.768kHz的低速外部晶振,可選擇它用來驅動RTC(RTCCLK)。
每個時鐘源在不使用時都可以單獨被打開或關閉,這樣就可以優化系統功耗。
當使用HSI作為PLL時鐘的輸入時,所能達到的最大系統時鐘為64MHz。
2.2.3 STM32時鐘硬件電路
2.2.3.1 HSE時鐘
高速外部時鐘信號(HSE)由以下兩種時鐘源產生:
● HSE外部晶體 / 陶瓷諧振器(見下圖(a))
● HSE用戶外部時鐘(見下圖(b))
(a)外部時鐘 (b)晶振時鐘
1. 外部時鐘源(HSE旁路)
在這種模式下,必須提供一個外部時鐘源。它的頻率可高達25MHz。外部時鐘信號(占空比為50%的方波、正弦波或三角波)必須連到OSC_IN引腳,同時保證OSC_OUT引腳懸空,見上圖(a)。這個外部時鐘源是指從其他處理器等引入的時鐘源,STM32的demo板就是使用的這種方式,主控器MCU的外部時鐘源來自ST Link處理器提供的時鐘信號。
2. 外部晶體/陶瓷諧振器(HSE晶體)
這個4~16MHz的外部晶振的優點在于能產生非常精確的主時鐘。下圖顯示了它需要的相關硬件配置。諧振器和負載電容需要盡可能近地靠近振蕩器的引腳,以減小輸出失真和啟動穩定時間。負載電容值必須根據選定的晶振進行調節。這種方式也是我們常用的方式,具體電路如下所示。
2.2.3.2 LSE時鐘
低速外部時鐘源(LSE)可以由兩個可能的時鐘源來產生:
● LSE外部晶體 / 陶瓷諧振器(見下圖(a))
● LSE用戶外部時鐘(見下圖(b))
1. 外部源(LSE旁路)
在這種模式下,必須提供一個外部時鐘源。它的頻率必須為32.768kHz。外部信號(占空比為50%的方波、正弦波或三角波)必須連到OSC32_IN引腳,同時保證OSC_OUT引腳懸空。
2. 外部晶體/陶瓷諧振器(LSE晶體)
這個LSE晶體是一個32.768kHz的低速外部晶體或陶瓷諧振器。它的優點在于能為實時時鐘部件(RTC)提供一個低速的,但高精確的時鐘源。 RTC可以用于時鐘/日歷或其它需要計時的場合。諧振器和加載電容需要盡可能近地靠近晶振引腳,這樣能使輸出失真和啟動穩定時間減到最小。負載電容值必須根據選定的晶振進行調節。
(a)外部時鐘 (b)晶振時鐘
外部晶體時鐘如下圖所示。
HSE和LSE外部晶體兩時鐘電路的兩個電容式為了抗干擾。對抗自然界中的一些干擾,如雷擊。
2.2.4 STM32的時鐘系統
STM32 芯片為了實現低功耗,設計了一個功能完善但卻非常復雜的時鐘系統。普通的MCU 一般只要配置好 GPIO 的寄存器就可以使用了,但 STM32 還有一個步驟,就是開啟外設時鐘。
在 STM32 中,可分為五種時鐘源,為 HSI、 HSE、 LSI、 LSE、 PLL。從時鐘頻率來分可以分為高速時鐘源和低速時鐘源,其中 HIS, HSE 以及 PLL 是高速時鐘, LSI 和 LSE 是低速時鐘。從來源可分為外部時鐘源和內部時鐘源,外部時鐘源就是從外部通過接晶振的方式獲取時鐘源,其中 HSE 和 LSE 是外部時鐘源,其他的是內部時鐘源。下面我們看看 STM32 的 5 個時鐘源,我們講解順序是按圖中紅圈標示的順序:
1HSI 是 高速內部時鐘 , RC 振蕩器,頻率為 8MHz。
②HSE 是 高速外部時鐘 ,可接石英 /陶瓷諧振器,或者接外部時鐘源,頻率范圍為4MHz~16MHz。 我們的開發板接的是 8M 的晶振。 當使用有源晶振時,時鐘從 OSC_IN 引腳進入, OSC_OUT 引腳懸空,當選用無源晶振時,時鐘從 OSC_IN 和 OSC_OUT 進入,并且要配諧振電容。 HSE 最常使用的就是 8M 的無源晶振。 當確定 PLL 時鐘來源的時候, HSE 可以不分頻或者 2 分頻,這個由時鐘配置寄存器 CFGR 的位 17。
③LSI 是 低速內部時鐘 ,RC 振蕩器,頻率為 40kHz。 獨立看門狗的時鐘源只能是 LSI,同時 LSI 還可以作為 RTC 的時鐘源。
④LSE 是 低速外部時鐘 ,接頻率為 32.768kHz 的石英晶體。 這個主要是 RTC 的時鐘源。
⑤PLL 為鎖相環倍頻輸出,其時鐘輸入源可選擇為 HSI/2、HSE 或者 HSE/2。 倍頻可選擇為2~16 倍,但是其輸出頻率最大不得超過 72MHz。
圖中我們用 A~E 標示我們要講解的地方。
A. MCO 是 STM32 的一個時鐘輸出 IO(PA8),它可以選擇一個時鐘信號輸出,可以選擇為 PLL 輸出的 2 分頻、 HSI、 HSE、或者系統時鐘。 這個時鐘可以用來給外部其他系統提供時鐘源。
B. 這里是 RTC 時鐘源,從圖上可以看出, RTC 的時鐘源可以選擇 LSI, LSE,以及HSE 的 128 分頻。
C. 從圖中可以看出 C 處 USB 的時鐘是來自 PLL 時鐘源。 STM32 中有一個全速功能的 USB 模塊,其串行接口引擎需要一個頻率為 48MHz 的時鐘源。該時鐘源只能從 PLL 輸出端獲取,可以選擇為 1.5 分頻或者 1 分頻,也就是,當需要使用 USB模塊時, PLL 必須使能,并且時鐘頻率配置為 48MHz 或 72MHz。
D. D 處就是 STM32 的系統時鐘 SYSCLK,它是供 STM32 中絕大部分部件工作的時鐘源。系統時鐘可選擇為 PLL 輸出、 HSI 或者 HSE。系統時鐘最大頻率為 72MHz,當然你也可以超頻,不過一般情況為了系統穩定性是沒有必要冒風險去超頻的。
E. 這里的 E 處是指其他所有外設了。從時鐘圖上可以看出,其他所有外設的時鐘最終來源都是 SYSCLK。 SYSCLK 通過 AHB 分頻器分頻后送給各模塊使用。這些模塊包括:
①AHB 總線、內核、內存和 DMA 使用的 HCLK 時鐘。
②通過 8 分頻后送給 Cortex 的系統定時器時鐘,也就是 systick 了。
③直接送給 Cortex 的空閑運行時鐘 FCLK。
④送給 APB1 分頻器。 APB1 分頻器輸出一路供 APB1 外設使用(PCLK1,最大頻率 36MHz),另一路送給定時器(Timer)2、 3、 4 倍頻器使用。
⑤送給 APB2 分頻器。 APB2 分頻器分頻輸出一路供 APB2 外設使用(PCLK2,最大頻率 72MHz),另一路送給定時器(Timer)1 倍頻器使用。
其中需要理解的是 APB1 和 APB2 的區別, APB1 上面連接的是低速外設,包括電源接口、備份接口、 CAN、 USB、 I2C1、 I2C2、 UART2、 UART3 等等, APB2 上面連接的是高速外設包括 UART1、 SPI1、 Timer1、 ADC1、 ADC2、所有普通 IO 口(PA~PE)、第二功能 IO 口等。
不同的總線有不同的頻率,不同的外設掛在不同的總線下,為了更適合初學者查閱,筆者把常用的外設與總線的對應關系總結如下:
SystemInit()函數中設置的系統時鐘大小:
- SYSCLK(系統時鐘) =72MHz
- AHB 總線時鐘(使用 SYSCLK) =72MHz
- APB1 總線時鐘(PCLK1) =36MHz
- APB2 總線時鐘(PCLK2) =72MHz
- PLL 時鐘 =72MHz
具體代碼請讀者查看工程文件的system_stm32f10x.c文件。
舉個例子:Keil編寫程序是默認的時鐘為72Mhz,其實是這么來的:外部晶振(HSE)提供的8MHz(與電路板上的晶振的相關)通過PLLXTPRE分頻器后,進入PLLSRC選擇開關,進而通過PLLMUL鎖相環進行倍頻(x9)后,為系統提供72MHz的系統時鐘(SYSCLK)。 之后是AHB預分頻器對時鐘信號進行分頻,然后為低速外設提供時鐘。
或者內部RC振蕩器(HSI) 為8MHz /2 為4MHz 進入PLLSRC選擇開關,通過PLLMUL鎖相環進行倍頻(x18)后為72MHz。
PS: 網上有很多人說是5個時鐘源,這種說法有點問題,學習之后就會發現PLL并不是自己產生的時鐘源,而是通過其他三個時鐘源倍頻得到的時鐘,這點在前文已近講解得很清楚了。
2.2.5 STM32的時鐘配置剖析
既然時鐘搞清楚了,接下來回到上一章的配置時鐘的代碼:
/*開啟LED的外設時鐘*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOG, ENABLE);
RCC_APB2PeriphClockCmd就是配置時鐘的函數,函數原型如下:
/**
* @brief Enables or disables theHigh Speed APB (APB2) peripheral clock.
* @param RCC_APB2Periph:specifies the APB2 peripheral to gates its clock.
* This parameter can be anycombination of the following values:
* @arg RCC_APB2Periph_AFIO,RCC_APB2Periph_GPIOA, RCC_APB2Periph_GPIOB,
* RCC_APB2Periph_GPIOC,RCC_APB2Periph_GPIOD, RCC_APB2Periph_GPIOE,
* RCC_APB2Periph_GPIOF,RCC_APB2Periph_GPIOG, RCC_APB2Periph_ADC1,
* RCC_APB2Periph_ADC2,RCC_APB2Periph_TIM1, RCC_APB2Periph_SPI1,
* RCC_APB2Periph_TIM8,RCC_APB2Periph_USART1, RCC_APB2Periph_ADC3,
* RCC_APB2Periph_TIM15,RCC_APB2Periph_TIM16, RCC_APB2Periph_TIM17,
* RCC_APB2Periph_TIM9,RCC_APB2Periph_TIM10, RCC_APB2Periph_TIM11
* @param NewState: new state ofthe specified peripheral clock.
* This parameter can be: ENABLEor DISABLE.
* @retval None
*/
voidRCC_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;
}
}
第一個參數就是具體的外設時鐘,第二參數就是時鐘狀態開關,整個函數很簡單,就是配置RCC->APB2ENR,assert_param是檢查參數的,關于assert_param詳細講解看附錄的小貼士。
參數RCC_APB2Periph傳入值是通過宏來定義的,這樣的好處也是便于移植,如果換了MCU,架構一樣,只需要就該底層驅動就行,不需要更改上層應用,這樣就提高了開發效率。 言歸正傳,我們傳入的RCC_APB2Periph_GPIOB和RCC_APB2Periph_GPIOG定義如下。
其實就是一個個偏移而已。 我們還是看看RCC_APB2ENR寄存器,
如果要配置GPIOB的時鐘就是要將第3位置1,因此轉換成10進制就是8,同理GPIOG也是一樣的。
RCC_APB2ENR的地址是0x18,更準確的是說偏移地址是0x18,在代碼中也是有體現的,我們看看RCC結構體吧。
#define RCC ((RCC_TypeDef *) RCC_BASE)
這里是通過宏的方式定義的,結構體就是RCC_TypeDef。
/**
* @brief Reset and Clock Control
*/
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;
#ifdef STM32F10X_CL
__IO uint32_t AHBRSTR;
__IO uint32_t CFGR2;
#endif /* STM32F10X_CL */
#if defined (STM32F10X_LD_VL) ||defined (STM32F10X_MD_VL) || defined (STM32F10X_HD_VL)
uint32_t RESERVED0;
__IO uint32_t CFGR2;
#endif /* STM32F10X_LD_VL ||STM32F10X_MD_VL || STM32F10X_HD_VL */
} RCC_TypeDef;
這里可以看到有__IO修飾結構體成員,關于__IO詳細講解看附錄的小貼士。 RCC_TypeDef中的成員都是32位,因此偏移都是4,而APB2ENR的偏移量就是寄存器表的偏移一一對應了,其他的結構體都是這樣定義的。
在說說RCC_BASE,這就實際的RCC的物理地址,其中的依賴關系如下:
#define PERIPH_BASE ((uint32_t)0x40000000) /*!
這樣我們就知道最終的RCC的基地址是0x0x40021000。 這個地址也可以在參考手冊中找到。
是不就都對上了,MCU所有的外設都對應了一個地址,因此通過操作該地址就能控制相應的功能,關于STM32的存儲管理在后文會詳細描述,這里先了解個大概就好。
值得注意的是,既然有RCC_APB2PeriphClockCmd函數,肯定還有RCC_APB1PeriphClockCmd,它們各自配置的時鐘可以從系統架構中看到,當然啦,函數的說明中也列舉了具體的外設。
到這里基本上時鐘的配置就完成了。 但是我想還是有很多朋友沒有很明白,下面就GPIO的配置從地址映射和固件庫的封裝兩個方便再來詳細總結下固件庫是如何完成GPIO的具體操作的。
2.3 Cortex-M的地址映射
我們先看看51 單片機中是怎么做的,51 單片機開發中會引用一個 reg51.h 的頭文件,51單片機是通過以下方式將名字和寄存器聯系起來的:
sfr P0 =0x80;
sfr 也是一種擴充數據類型,占用一個內存單元,值域為 0~255。 利用它可以訪問 51 單片機內部的所有特殊功能寄存器。 如用 sfr P1 = 0x90 這一句定義 P1 為 P1 端口在片內的寄存器。 然后我們往地址為 0x80 的寄存器設值的方法是: P0=value; 通過改變value的值來控制單片機。
所謂地址映射,就是將芯片上的存儲器甚至 I/O 等資源與地址建立一一對應的關系。 如果某地址對應著某寄存器,我們就可以運用 C 語言的指針來尋址并修改這個地址上的內容,從而實現修改該寄存器的內容。 Cortex-M的地址映射也是類似的。 Cortex-M有 32 根地址線,所以它的尋址空間大小為 2 ^32^bit=4 GB。 ARM 公司設計時,預先把這 4 GB 的尋址空間大致地分配好了。 它把從 0x40000000 至 0x5FFFFFFF( 512 MB)的地址分配給片上外設。 通過把片上外設的寄存器映射到這個地址區,就可以簡單地以訪問內存的方式,訪問這些外設的寄存器,從而控制外設的工作。 這樣,片上外設可以使用 C 語言來操作。
stm32f10x.h 這個文件中重要的內容就是把 STM32 的所有寄存器進行地址映射。 如同51 單片機的
在這里我們以流水燈中的 GPIOB 為例進行剖析,如果是其他的 IO 端口,則改成相應的地址即可。 在這個文件中一系列宏實現了地址映射。
#define GPIOB_BASE(APB2PERIPH_BASE + 0xC00)
#define APB2PERIPH_BASE (PERIPH_BASE+ 0xC00
#define PERIPH_BASE((uint32_t)0x40000000)
這幾個宏定義是從文件中的幾個部分抽離出來的,具體的內容讀者可參考stm32f10x.h 源碼。
首先看到 PERIPH_BASE 這個宏,宏展開為 0x40000000,并把它強制轉換為 uint32_t的 32 位類型數據,這是因為 STM32 的地址是 32 位的,0x40000000 這個地址是 Cortex-M3 核分配給片上外設 512MB 尋址空間中的第一個地址,我們把0x40000000 稱為外設基地址。
接下來是宏 APB2PERIPH_BASE,宏展開為 PERIPH_BASE(外設基地址)加上偏移地址 0x10000,即指向的地址為 0x40010000。 這個 APB2PERIPH_BASE 宏是什么地址呢? STM32 不同的外設是掛載在不同的總線上的。 STM32 芯片有 AHB 總線、APB2總線和 APB1 總線,掛載在這些總線上的外設有特定的地址范圍。
其中像 GPIO、串口 1、ADC 及部分定時器是掛載在稱為 APB2 的總線上,掛載到APB2 總線上的外設地址空間是從0x40010000 至 0x40013FFF地址。 這里的第一個地址,也就是 0x40010000,稱為 APB2PERIPH_BASE (APB2 總線外設基地址)。
而 APB2 總線基地址相對于外設基地址的偏移量為 0x10000 個地址,即為 APB2 相對外設基地址的偏移地址,見下表。
由這個表我們可以知道,stm32f10x.h 這個文件中必然含有用于定義總線外設基地址的宏。
#define APB2PERIPH_BASE PERIPH_BASE
因為偏移量為零,所以 APB2的地址直接就等于外設基地址。
最后到了宏 GPIOB_BASE,宏展開為 APB2PERIPH_BASE (APB2 總線外設的基地址)加上相對 APB2 總線外設基地址的偏移量 0xC00 得到了 GPIOB端口的寄存器組的基地址。 這個所謂的寄存器組又是什么呢? 它包括什么寄存器?
細看 stm32f10x.h 文件,我們還可以發現有關各個 GPIO 基地址的宏。
#define GPIOA_BASE(APB2PERIPH_BASE + 0x0800)
#define GPIOB_BASE(APB2PERIPH_BASE + 0x0C00)
#define GPIOC_BASE(APB2PERIPH_BASE + 0x1000)
#define GPIOD_BASE(APB2PERIPH_BASE + 0x1400)
除了 GPIOB寄存器組的地址,還有 GPIOA、GPIOC和 GPIOD 的地址,并且這些地址是不一樣的。 前面提到,每組 GPIO 都對應著獨立的一組寄存器,查看 STM32 的數據手冊。
注意到這個說明中有一個偏移地址:0x04,這里的偏移地址是相對哪個地址的偏移呢? 下面進行舉例說明。
對于GPIOB組的寄存器,GPIOB含有的端口配置高寄存器(GPIOB_CRH地址為:GPIOB_BASE +0x04。 假如是 GPIOA 組的寄存器,則 GPIOA 含有的端口配置高寄存器(GPIOA_CRH)地址為:GPIOA_BASE+0x04。 也就是說,這個偏移地址,就是該寄存器相對所在寄存器組基地址的偏移量。
2.4固件庫對寄存器的封裝
ST的工程師用結構體的形式封裝了寄存器組,在 stm32f10x.h 文件定義的。
#define GPIOA ((GPIO_TypeDef *)GPIOA_BASE)
#define GPIOB ((GPIO_TypeDef *)GPIOB_BASE)
#define GPIOC ((GPIO_TypeDef *)GPIOC_BASE)
有了這些宏,我們就可以定位到具體的寄存器地址,結構體GPIO_TypeDef在 stm32f10x.h 文件中定義的。
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;
結構體內又定義了 7 個 __IO uint32_t 類型的變量。 這些變量都是 32 位,即每個變量占內存空間 4 個字節。 在 C 語言中,結構體內變量的存儲空間是連續的,也就是說假如我們定義了一個 GPIO_TypeDef ,這個結構體的首地址(變量 CRL 的地址)若為 0x40011000,那么結構體中第二個變量(CRH)的地址即為 0x40011000 +0x04, 加上的0x04 正是代表 4 個字節地址的偏移量。
0x04 偏移量正是 GPIOx_CRH 寄存器相對于所在寄存器組的偏移地址。 同理,GPIO_TypeDef 結構體內其他變量的偏移量,也與相應的寄存器偏移地址相符。 于是,只要我們匹配了結構體的首地址,就可以確定各寄存器的具體地址了。
GPIOA_BASE 在前文已解析,是一個代表 GPIOA組寄存器的基地址。“(GPIO_TypeDef *)”在這里的作用則是把 GPIOA_BASE 地址轉換為 GPIO_TypeDef結構體指針類型。有了這樣的宏,以后我們寫代碼的時候,如果要修改GPIO 的寄存器,就可用修改以下代碼的方式來實現。
GPIO_TypeDef * GPIOx; //定義一個 GPIO_TypeDef 型結構體指針 GPIOx
GPIOx = GPIOA; //把指針地址設置為宏 GPIOA 地址
GPIOx->CRL = 0xffffffff; //通過指針訪問并修改 GPIOA_CRL 寄存器
通過類似的方式,我們就可以給具體的寄存器寫上適當的參數以控制 STM32 了。這樣我們就可以通過庫函數實現了GPIO的初始化了。
/**
* @brief 初始化LED的GPIO
* @param None
* @retval None
*/
void LED_GPIO_Config(void)
{
/*定義一個GPIO_InitTypeDef類型的結構體*/
GPIO_InitTypeDefGPIO_InitStructure;
/*開啟LED的外設時鐘*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOG, ENABLE);
/*設置IO口*/
GPIO_InitStructure.GPIO_Mode= GPIO_Mode_Out_PP; //設置引腳模式為通用推挽輸出
GPIO_InitStructure.GPIO_Speed= GPIO_Speed_50MHz; //設置引腳速率為50MHz
/*調用庫函數,初始化GPIOB0*/
GPIO_InitStructure.GPIO_Pin= GPIO_Pin_0; //選擇要控制的GPIOB引腳
GPIO_Init(GPIOB,&GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin= GPIO_Pin_6|GPIO_Pin_7;/*選擇要控制的引腳*/
GPIO_Init(GPIOG,&GPIO_InitStructure);
/*開啟所有led燈 */
GPIO_SetBits(GPIOB,GPIO_Pin_0);
GPIO_SetBits(GPIOG,GPIO_Pin_6|GPIO_Pin_7);
}
當然啦,上述代碼包含了時鐘的使能。
通過對時鐘和GPIO的分析,我想大家已經對固件的邏輯有了一定的認識,從本質上講,都是在配置寄存器,只是地址和值不同罷了,而固件庫就是對寄存器配置的封裝,便于開發者調用。
當然啦,本文是基于標準庫分析,HAL庫的邏輯也是一樣的,只是HAL功能更完善,封裝更徹底,后面也會詳細分析HAL庫的調用邏輯。
小貼士
1.assert_param
在STM32的固件庫和提供的例程中,到處都可以見到assert_param()的使用。如果打開任何一個例程中的stm32f10x_conf.h文件,就可以看到實際上assert_param是一個宏定義;在固件庫中,它的作用就是檢測傳遞給函數的參數是否是有效的參數。
所謂有效的參數是指滿足規定范圍的參數,比如某個參數的取值范圍只能是小于3的正整數,如果給出的參數大于3,則這個assert_param()可以在運行的程序調用到這個函數時報告錯誤,使程序員可以及時發現錯誤,而不必等到程序運行結果的錯誤而大費周折。這是一種常見的軟件技術,可以在調試階段幫助程序員快速地排除那些明顯的錯誤。它確實在程序的運行上犧牲了效率(但只是在調試階段),但在項目的開發上卻幫助你提高了效率。
當你的項目開發成功,使用release模式編譯之后,或在stm32f10x_conf.h文件中注釋掉對USE_FULL_ASSERT的宏定義,所有的assert_param()檢驗都消失了,不會影響最終程序的運行效率。在執行assert_param()的檢驗時,如果發現參數出錯,它會調用函數assert_failed()向程序員報告錯誤,在任何一個例程中的main.c中都有這個函數的模板,如下:
void assert_failed(uint8_t*file, uint32_t line)
{
while (1)
{}
}
你可以按照自己使用的環境需求,添加適當的語句輸出錯誤的信息提示,或修改這個函數做出適當的錯誤處理。
1、STM32F10xD.LIB是DEBUG模式的庫文件。
2、STM32F10xR.LIB是Release模式的庫文件。
3、要選擇DEBUG和RELEASE模式,需要修改stm32f10x_conf.h的內容。#define DEBUG 表示DEBUG模式,把該語句注釋掉,則為RELEASE模式。
4、要選擇DEBUG和RELEASE模式,也可以在Options,C/C++,Define里填入DEBUG的預定義。這樣,就不需要修改stm32f10x_conf.h的內容。
5、如果把庫加入項目,則不需要將ST的庫源文件加入項目,比較方便。但是,庫的選擇要和DEBUG預定義對應。
2.__I、 __O 、__IO的含義
這是ST庫里面的宏定義,定義如下:
#define __I volatile const /*!< defines 'read only' permissions */
#define __O volatile /*!< defines 'write only'permissions */
#define __IO volatile /*!
顯然,這三個宏定義都是用來替換成 volatile 和 const 的,所以我們先要了解這兩個關鍵字的作用:
__I :輸入口。既然是輸入,那么寄存器的值就隨時會外部修改,那就不能進行優化,每次都要重新從寄存器中讀取。也不能寫,即只讀,不然就不是輸入而是輸出了。
__O :輸出口,也不能進行優化,不然你連續兩次輸出相同值,編譯器認為沒改變,就忽略了后面那一次輸出,假如外部在兩次輸出中間修改了值,那就影響輸出。
__IO:輸入輸出口,同上
為什么加下劃線?
原因是:避免命名沖突
一般宏定義都是大寫,但因為這里的字母比較少,所以再添加下劃線來區分。這樣一般都可以避免命名沖突問題,因為很少人這樣命名,這樣命名的人肯定知道這些是有什么用的。
經常寫大工程時,都會發現老是命名沖突,要不是全局變量沖突,要不就是宏定義沖突,所以我們要盡量避免這些問題,不然出問題了都不知道問題在哪里。
-
mcu
+關注
關注
146文章
17867瀏覽量
360971 -
ARM
+關注
關注
134文章
9316瀏覽量
375324 -
寄存器
+關注
關注
31文章
5424瀏覽量
123483 -
流水燈
+關注
關注
21文章
433瀏覽量
60325 -
GPIO
+關注
關注
16文章
1276瀏覽量
53622
發布評論請先 登錄
GD32開發實戰指南(基礎篇) 第3章 GPIO流水燈的前世今生
ARM Cortex-M堆棧機制介紹
ARM Cortex-M內核的相關資料推薦
ARM Cortex-M開發之初識GPIO流水燈
Arm Cortex-M處理器—Cortex-M85介紹
ARM Cortex-M 系列微控制器(ST)
米爾科技Cortex-M Prototyping System +介紹

評論