網上有很多關于pos機方案開發板,探索者 STM32F407 開發板資料連載第四章 F4 開發基礎知識入門的知識,也有很多人為大家解答關于pos機方案開發板的問題,今天pos機之家(www.tonybus.com)為大家整理了關于這方面的知識,讓我們一起來看下吧!
本文目錄一覽:
3、onu設備介紹?
pos機方案開發板
1)實驗平臺:探索者 STM32F407 開發板
2)摘自《STM32F4 開發指南(HAL 庫版)》關注官方微信號公眾號,獲取更多資料:正點原子
第四章 STM32F4 開發基礎知識入門
這一章,我們將著重 STM32 開發的一些基礎知識,讓大家對 STM32 開發有一個初步的了
解,為后面 STM32 的學習做一個鋪墊,方便后面的學習。這一章的內容大家第一次看的時候
可以只了解一個大概,后面需要用到這方面的知識的時候再回過頭來仔細看看。這章我們分 7
個小結,
·4.1 MDK 下 C 語言基礎復習
·4.2 STM32F4 系統架構
·4.3 STM32F4 時鐘系統
·4.4 IO 引腳復用器和映射
·4.5 STM32F4 NVIC 中斷優先級管理
·4.6 MDK 中寄存器地址名稱映射分析
·4.7 MDKHAL 庫快速開發技巧
4.1 MDK 下 C 語言基礎復習
這一節我們主要講解一下 C 語言基礎知識。C 語言知識博大精深,也不是我們三言兩語能
講解清楚,同時我們相信學 STM32F4 這種級別 MCU 的用戶,C 語言基礎應該都是沒問題的。我
們這里主要是簡單的復習一下幾個 C 語言基礎知識點,引導那些 C 語言基礎知識不是很扎實的
用戶能夠快速開發 STM32 程序。同時希望這些用戶能夠多去復習一下 C 語言基礎知識,C 語言
畢竟是單片機開發中的必備基礎知識。對于 C 語言基礎比較扎實的用戶,這部分知識可以忽略
不看。
4.1.1 位操作
C 語言位操作相信學過 C 語言的人都不陌生了,簡而言之,就是對基本類型變量可以在位級
別進行操作。這節的內容很多朋友都應該很熟練了,我這里也就點到為止,不深入探討。下面
我們先講解幾種位操作符,然后講解位操作使用技巧。
C 語言支持如下 6 種位操作
表 4.1.1 16 種位操作
這些與或非,取反,異或,右移,左移這些到底怎么回事,這里我們就不多做詳細,相信
大家學 C 語言的時候都學習過了。如果不懂的話,可以百度一下,非常多的知識講解這些操作
符。下面我們想著重講解位操作在單片機開發中的一些實用技巧。
1) 不改變其他位的值的狀況下,對某幾個位進行設值。
這個場景單片機開發中經常使用,方法就是先對需要設置的位用&操作符進行清零操作,
然后用|操作符設值。比如我要改變 GPIOA-> BSRRL 的狀態,可以先對寄存器的值進行&
清零操作
GPIOA-> BSRRL &=0XFF0F; //將第 4-7 位清 0
然后再與需要設置的值進行|或運算
GPIOA-> BSRRL |=0X0040;//設置相應位的值,不改變其他位的值2) 移位操作提高代碼的可讀性。
移位操作在單片機開發中也非常重要,我們來看看下面一行代碼
GPIOx->ODR = (((uint32_t)0x01) << pinpos);
這個操作就是將 ODR 寄存器的第 pinpos 位設置為 1,為什么要通過左移而不是直接設
置一個固定的值呢?其實,這是為了提高代碼的可讀性以及可重用性。這行代碼可以
很直觀明了的知道,是將第 pinpos 位設置為 1。如果你寫成
GPIOx->ODR =0x0030;
這樣的代碼就不好看也不好重用了。
3) ~取反操作使用技巧
SR 寄存器的每一位都代表一個狀態,某個時刻我們希望去設置某一位的值為 0,同時
其他位都保留為 1,簡單的作法是直接給寄存器設置一個值:
TIMx->SR=0xFFF7;
這樣的作法設置第 3 位為 0,但是這樣的作法同樣不好看,并且可讀性很差??纯磶旌瘮?/p>
代碼中怎樣使用的:
TIMx->SR = (uint16_t)~TIM_FLAG;
而 TIM_FLAG 是通過宏定義定義的值:
#define TIM_FLAG_Update ((uint16_t)0x0001)
#define TIM_FLAG_CC1 ((uint16_t)0x0002)
看這個應該很容易明白,可以直接從宏定義中看出 TIM_FLAG_Update 就是設置的第 0 位了,
可讀性非常強。
4.1.2 define 宏定義
define 是 C 語言中的預處理命令,它用于宏定義,可以提高源代碼的可讀性,為編程提供
方便。常見的格式:
#define 標識符 字符串
“標識符”為所定義的宏名?!白址笨梢允浅?、表達式、格式串等。例如:
#define PLL_M 8
定義標識符 PLL_M 的值為 8。
至于 define 宏定義的其他一些知識,比如宏定義帶參數這里我們就不多講解。
4.1.3 ifdef 條件編譯
單片機程序開發過程中,經常會遇到一種情況,當滿足某條件時對一組語句進行編譯,而
當條件不滿足時則編譯另一組語句。條件編譯命令最常見的形式為:
#ifdef 標識符
程序段 1
#else
程序段 2
#endif
它的作用是:當標識符已經被定義過(一般是用#define 命令定義),則對程序段 1 進行編譯,
否則編譯程序段 2。 其中#else 部分也可以沒有,即:
#ifdef
程序段 1
#endif
這個條件編譯在MDK里面是用得很多的,在stm32f4xx.h這個頭文件中經常會看到這樣的語句:
#if defined (STM32F40_41xxx)
STM32F40x 系列和 STM32F41x 系列芯片需要的一些變量定義
#end
而(STM32F40_41xxx 則是我們通過#define 來定義的。條件編譯也是 c 語言的基礎知識,這里
也就點到為止吧。
4.1.4 extern 變量申明
C 語言中 extern 可以置于變量或者函數前,以表示變量或者函數的定義在別的文件中,提示編
譯器遇到此變量和函數時在其他模塊中尋找其定義。這里面要注意,對于 extern 申明變量可以多
次,但定義只有一次。在我們的代碼中你會看到看到這樣的語句:
extern u16 USART_RX_STA;
這個語句是申明 USART_RX_STA 變量在其他文件中已經定義了,在這里要使用到。所以,你肯定
可以找到在某個地方有變量定義的語句:
u16 USART_RX_STA;
的出現。下面通過一個例子說明一下使用方法。
在 Main.c 定義的全局變量 id,id 的初始化都是在 Main.c 里面進行的。
Main.c 文件
u8 id;//定義只允許一次
main()
{
id=1;
printf("d%",id);//id=1
test();
printf("d%",id);//id=2
}
但是我們希望在main.c的 changeId(void)函數中使用變量id,這個時候我們就需要在main.c
里面去申明變量 id 是外部定義的了,因為如果不申明,變量 id 的作用域是到不了 main.c 文件
中。看下面 main.c 中的代碼:
extern u8 id;//申明變量 id 是在外部定義的,申明可以在很多個文件中進行
void test(void){
id=2;
}
在 main.c 中申明變量 id 在外部定義,然后在 main.c 中就可以使用變量 id 了。
對于 extern 申明函數在外部定義的應用,這里我們就不多講解了。
4.1.5 typedef 類型別名
typedef 用于為現有類型創建一個新的名字,或稱為類型別名,用來簡化變量的定義。
typedef 在 MDK 用得最多的就是定義結構體的類型別名和枚舉類型了。
struct _GPIO
{
__IO uint32_t MODER;
__IO uint32_t OTYPER;
…
};
定義了一個結構體 GPIO,這樣我們定義變量的方式為:
struct _GPIO GPIOA;//定義結構體變量 GPIOA
但是這樣很繁瑣,MDK 中有很多這樣的結構體變量需要定義。這里我們可以為結體定義一個別
名 GPIO_TypeDef,這樣我們就可以在其他地方通過別名 GPIO_TypeDef 來定義結構體變量了。
方法如下:
typedef struct
{
__IO uint32_t MODER;
__IO uint32_t OTYPER;
…
} GPIO_TypeDef;
Typedef 為結構體定義一個別名 GPIO_TypeDef,這樣我們可以通過 GPIO_TypeDef 來定義結構體
變量:
GPIO_TypeDef _GPIOA,_GPIOB;
這里的 GPIO_TypeDef 就跟 struct _GPIO 是等同的作用了。 這樣是不是方便很多?
4.1.6 結構體
經常很多用戶提到,他們對結構體使用不是很熟悉,但是 MDK 中太多地方使用結構體以及
結構體指針,這讓他們一下子摸不著頭腦,學習 STM32 的積極性大大降低,其實結構體并不是
那么復雜,這里我們稍微提一下結構體的一些知識,還有一些知識我們會在下一節的“寄存器
地址名稱映射分析”中講到一些。
聲明結構體類型:
Struct 結構體名{
成員列表;
}變量名列表;
例如:
Struct U_TYPE {
Int BaudRate
Int WordLength;
}usart1,usart2;
在結構體申明的時候可以定義變量,也可以申明之后定義,方法是:
Struct 結構體名字 結構體變量列表 ;
例如:struct U_TYPE usart1,usart2;
結構體成員變量的引用方法是:
結構體變量名字.成員名
比如要引用 usart1 的成員 BaudRate,方法是:usart1.BaudRate;
結構體指針變量定義也是一樣的,跟其他變量沒有啥區別。
例如:struct U_TYPE *usart3;//定義結構體指針變量 usart1;
結構體指針成員變量引用方法是通過“->”符號實現,比如要訪問 usart3 結構體指針指向的結
構體的成員變量 BaudRate,方法是:
Usart3->BaudRate;
上面講解了結構體和結構體指針的一些知識,其他的什么初始化這里就不多講解了。講到這里,
有人會問,結構體到底有什么作用呢?為什么要使用結構體呢?下面我們將簡單的通過一個實
例回答一下這個問題。
在我們單片機程序開發過程中,經常會遇到要初始化一個外設比如串口,它的初始化狀態
是由幾個屬性來決定的,比如串口號,波特率,極性,以及模式等。對于這種情況,在我們沒
有學習結構體的時候,我們一般的方法是:
void USART_Init(u8 usartx,u32 u32 BaudRate,u8 parity,u8 mode);
這種方式是有效的同時在一定場合是可取的。但是試想,如果有一天,我們希望往這個函數里
面再傳入一個參數,那么勢必我們需要修改這個函數的定義,重新加入字長這個入口參數。于
是我們的定義被修改為:
void USART_Init (u8 usartx,u32 BaudRate, u8 parity,u8 mode,u8 wordlength );
但是如果我們這個函數的入口參數是隨著開發不斷的增多,那么是不是我們就要不斷的修改函
數的定義呢?這是不是給我們開發帶來很多的麻煩?那又怎樣解決這種情況呢?
這樣如果我們使用到結構體就能解決這個問題了。我們可以在不改變入口參數的情況下,
只需要改變結構體的成員變量,就可以達到上面改變入口參數的目的。
結構體就是將多個變量組合為一個有機的整體。上面的函數,BaudRate,wordlength,
Parity,mode,wordlength 這些參數,他們對于串口而言,是一個有機整體,都是來設置串口參
數的,所以我們可以將他們通過定義一個結構體來組合在一個。MDK 中是這樣定義的:
typedef struct
{
uint32_t USART_BaudRate;
uint16_t USART_WordLength;
uint16_t USART_StopBits;
uint16_t USART_Parity;
uint16_t USART_Mode;
uint16_t USART_HardwareFlowControl;
} USART_InitTypeDef;
于是,我們在初始化串口的時候入口參數就可以是 USART_InitTypeDef 類型的變量或者指針變
量了,MDK 中是這樣做的:
void USART_Init(USART_TypeDef* USARTx, USART_InitTypeDef* USART_InitStruct);
這樣,任何時候,我們只需要修改結構體成員變量,往結構體中間加入新的成員變量,而不需
要修改函數定義就可以達到修改入口參數同樣的目的了。這樣的好處是不用修改任何函數定義
就可以達到增加變量的目的。
理解了結構體在這個例子中間的作用嗎?在以后的開發過程中,如果你的變量定義過多,
如果某幾個變量是用來描述某一個對象,你可以考慮將這些變量定義在結構體中,這樣也許可
以提高你的代碼的可讀性。
使用結構體組合參數,可以提高代碼的可讀性,不會覺得變量定義混亂。當然結構體的作
用就遠遠不止這個了,同時,MDK 中用結構體來定義外設也不僅僅只是這個作用,這里我們只
是舉一個例子,通過最常用的場景,讓大家理解結構體的一個作用而已。后面一節我們還會講
解結構體的一些其他知識。
4.2 STM32F4 總線架構
STM32F4 的總線架構比 51 單片機就要強大很多了。STM32F4 總線架構的知識可以在
《STM32F4XX 中文參考手冊》第二章有講解,這里我們也把這一部分知識抽取出來講解,是
為了大家在學習 STM32F4 之前對系統架構有一個初步的了解。這里的內容基本也是從中文參
考手冊中參考過來的,讓大家能通過我們手冊也了解到,免除了到處找資料的麻煩吧。如果需
要詳細深入的了解 STM32 的系統架構,還需要多看看《STM32F4XX 中文參考手冊》或者在網
上搜索其他相關資料學習。
我們這里所講的 STM32F4 系統架構主要針對的 STM32F407 系列芯片。首先我們看看
STM32 的總線架構圖:
圖 4.2.1 STM32F407 系統架構圖
主系統由 32 位多層 AHB 總線矩陣構成??偩€矩陣用于主控總線之間的訪問仲裁管理。仲裁采
取循環調度算法。總線矩陣可實現以下部分互聯:
八條主控總線是:
Cortex-M4 內核 I 總線, D 總線和 S 總線;
DMA1 存儲器總線, DMA2 存儲器總線;
DMA2 外設總線;
以太網 DMA 總線;
USB OTG HS DMA 總線;
七條被控總線:
內部 FLASH ICode 總線;
內部 FLASH DCode 總線;
主要內部 SRAM1(112KB)
輔助內部 SRAM2(16KB);
輔助內部 SRAM3(64KB) (僅適用 STM32F42xx 和 STM32F43xx 系列器件);
AHB1 外設 和 AHB2 外設;
FSMC
下面我們具體講解一下圖中幾個總線的知識。
① I 總線(S0):此總線用于將 Cortex-M4 內核的指令總線連接到總線矩陣。內核通過此總
線獲取指令。此總線訪問的對象是包括代碼的存儲器。
② D 總線(S1):此總線用于將 Cortex-M4 數據總線和 64KB CCM 數據 RAM 連接到總線矩
陣。內核通過此總線進行立即數加載和調試訪問。
③ S 總線(S2):此總線用于將 Cortex-M4 內核的系統總線連接到總線矩陣。此總線用于訪
問位于外設或 SRAM 中的數據。
④ DMA 存儲器總線(S3,S4):此總線用于將 DMA 存儲器總線主接口連接到總線矩陣。
DMA 通過此總線來執行存儲器數據的傳入和傳出。
⑤ DMA 外設總線:此總線用于將 DMA 外設主總線接口連接到總線矩陣。DMA 通過此
總線訪問 AHB 外設或執行存儲器之間的數據傳輸。
⑥ 以太網 DMA 總線:此總線用于將以太網 DMA 主接口連接到總線矩陣。以太網 DMA
通過此總線向存儲器存取數據。
⑦ USB OTG HS DMA 總線(S7):此總線用于將 USB OTG HS DMA 主接口連接到總線矩
陣。USB OTG HS DMA 通過此總線向存儲器加載/存儲數據。
對于系統架構的知識,在剛開始學習 STM32 的時候只需要一個大概的了解,大致知道是個
什么情況即可。對于尋址之類的知識,這里就不做深入的講解,中文參考手冊都有很詳細的講
解。
4.3 STM32F4 時鐘系統
STM32F4 時鐘系統的知識在《STM32F4 中文參考手冊》第六章復位和時鐘控制章節有非
常詳細的講解,網上關于時鐘系統的講解也基本都是參考的這里,講不出啥特色,不過作為一
個完整的參考手冊,我們必然要提到時鐘系統的知識。這些知識也不是什么原創,純粹根據官
方提供的中文參考手冊和自己的應用心得來總結的,如有不合理之處望大家諒解。
這部分內容我們分 3 個小節來講解:
·4.3.1 STM32F4 時鐘樹概述
·4.3.2 STM32F4 時鐘初始化配置
·4.3.3 STM32F4 時鐘使能和配置
4.3.1 STM32F4 時鐘樹概述
眾所周知,時鐘系統是 CPU 的脈搏,就像人的心跳一樣。所以時鐘系統的重要性就不言而
喻了。 STM32F4 的時鐘系統比較復雜,不像簡單的 51 單片機一個系統時鐘就可以解決一切。
于是有人要問,采用一個系統時鐘不是很簡單嗎?為什么 STM32 要有多個時鐘源呢? 因為首
先 STM32 本身非常復雜,外設非常的多,但是并不是所有外設都需要系統時鐘這么高的頻率,
比如看門狗以及 RTC 只需要幾十 k 的時鐘即可。同一個電路,時鐘越快功耗越大,同時抗電磁
干擾能力也會越弱,所以對于較為復雜的 MCU 一般都是采取多時鐘源的方法來解決這些問題。
首先讓我們來看看 STM32F4 的時鐘系統圖:
圖 4.3.1.1STM32 時鐘系統圖
在 STM32F4 中,有 5 個最重要的時鐘源,為 HSI、HSE、LSI、LSE、PLL。其中 PLL 實
際是分為兩個時鐘源,分別為主 PLL 和專用 PLL。從時鐘頻率來分可以分為高速時鐘源和低速
時鐘源,在這 5 個中 HSI,HSE 以及 PLL 是高速時鐘,LSI 和 LSE 是低速時鐘。從來源可分為
外部時鐘源和內部時鐘源,外部時鐘源就是從外部通過接晶振的方式獲取時鐘源,其中 HSE 和
LSE 是外部時鐘源,其他的是內部時鐘源。下面我們看看 STM32F4 的這 5 個時鐘源,我們講
解順序是按圖中紅圈標示的順序:①、LSI 是低速內部時鐘,RC 振蕩器,頻率為 32kHz 左右。供獨立看門狗和自動喚醒單元使用。
②、LSE 是低速外部時鐘,接頻率為 32.768kHz 的石英晶體。這個主要是 RTC 的時鐘源。
③、HSE 是高速外部時鐘,可接石英/陶瓷諧振器,或者接外部時鐘源,頻率范圍為 4MHz~26MHz。
我們的開發板接的是 8M 的晶振。HSE 也可以直接做為系統時鐘或者 PLL 輸入。
④、HSI 是高速內部時鐘,RC 振蕩器,頻率為 16MHz??梢灾苯幼鳛橄到y時鐘或者用作 PLL
輸入。
⑤、PLL 為鎖相環倍頻輸出。STM32F4 有兩個 PLL:
1) 主 PLL(PLL)由 HSE 或者 HSI 提供時鐘信號,并具有兩個不同的輸出時鐘。
第一個輸出 PLLP 用于生成高速的系統時鐘(最高 168MHz)
第二個輸出 PLLQ 用于生成 USB OTG FS 的時鐘(48MHz),隨機數發生器的時鐘和 SDIO
時鐘。
2)專用 PLL(PLLI2S)用于生成精確時鐘,從而在 I2S 接口實現高品質音頻性能。
這里我們著重看看主 PLL 時鐘第一個高速時鐘輸出 PLLP 的計算方法。圖 4.3.1.2 是主 PLL 的
時鐘圖。
圖 4.3.1.2 STM32F4 主 PLL 時鐘圖
從圖 4.3.1.2 可以看出。主 PLL 時鐘的時鐘源要先經過一個分頻系數為 M 的分頻器,然后經過
倍頻系數為 N 的倍頻器出來之后的時候還需要經過一個分頻系數為 P(第一個輸出 PLLP)或
者 Q(第二個輸出 PLLQ)的分頻器分頻之后,最后才生成最終的主 PLL 時鐘。
例如我們的外部晶振選擇 8MHz。同時我們設置相應的分頻器 M=8,倍頻器倍頻系數 N=336,
分頻器分頻系數 P=2,那么主 PLL 生成的第一個輸出高速時鐘 PLLP 為:
PLL=8MHz * N/ (M*P)=8MHz* 336 /(8*2) = 168MHz
如果我們選擇HSE為PLL時鐘源,同時SYSCLK時鐘源為PLL,那么SYSCLK時鐘為 168MHz。
這對于我們后面的實驗都是采用這樣的配置。
上面我們簡要概括了 STM32 的時鐘源,那么這 5 個時鐘源是怎么給各個外設以及系統提
供時鐘的呢?這里我們選擇一些比較常用的時鐘知識來講解。
圖 4.3.1.1 中我們用 A~G 標示我們要講解的地方。
A.
這里是看門狗時鐘輸入。從圖中可以看出,看門狗時鐘源只能是低速的 LSI 時鐘。
B.
這里是 RTC 時鐘源,從圖上可以看出,RTC 的時鐘源可以選擇 LSI,LSE,以及
HSE 分頻后的時鐘,HSE 分頻系數為 2~31。
C.
這里是 STM32F4 輸出時鐘 MCO1 和 MCO2。MCO1 是向芯片的 PA8 引腳輸出時
鐘。它有四個時鐘來源分別為:HSI,LSE,HSE 和 PLL 時鐘。MCO2 是向芯片的PC9 輸出時鐘,它同樣有四個時鐘來源分別為:HSE,PLL,SYSCLK 以及 PLLI2S
時鐘。MCO 輸出時鐘頻率最大不超過 100MHz。
D.
這里是系統時鐘。從圖 4.3.1 可以看出,SYSCLK 系統時鐘來源有三個方面:
HSI,HSE 和 PLL。在我們實際應用中,因為對時鐘速度要求都比較高我們才會選
用 STM32F4 這種級別的處理器,所以一般情況下,都是采用 PLL 作為 SYSCLK
時鐘源。根據前面的計算公式,大家就可以算出你的系統的 SYSCLK 是多少。
E.
這里我們指的是以太網 PTP 時鐘,AHB 時鐘,APB2 高速時鐘,APB1 低速時鐘。
這些時鐘都是來源于 SYSCLK 系統時鐘。其中以太網 PTP 時鐘是使用系統時鐘。
AHB,APB2 和 APB1 時鐘是經過 SYSCLK 時鐘分頻得來。這里大家記住,AHB
最大時鐘為168MHz, APB2高速時鐘最大頻率為84MHz,而APB1低速時鐘最大頻
率為 42MHz。
F.
這里是指 I2S 時鐘源。從圖 4.3.1 可以看出,I2S 的時鐘源來源于 PLLI2S 或者映
射到 I2S_CKIN 引腳的外部時鐘。I2S 出于音質的考慮,對時鐘精度要求很高。探
索者 STM32F4 開發板使用的是內部 PLLI2SCLK。
G.
這是 STM32F4 內部以太網 MAC 時鐘的來源。對于 MII 接口來說,必須向外部
PHY 芯片提供 25Mhz 的時鐘,這個時鐘,可以由 PHY 芯片外接晶振,或者使用
STM32F4 的 MCO 輸 出 來 提 供 。 然 后 , PHY 芯 片 再 給 STM32F4 提 供
ETH_MII_TX_CLK 和 ETH_MII_RX_CLK 時鐘。對于 RMII 接口來說,外部必須
提供 50Mhz 的時鐘驅動 PHY 和 STM32F4 的 ETH_RMII_REF_CLK,這個 50Mhz
時鐘可以來自 PHY、有源晶振或者 STM32F4 的 MCO。我們的開發板使用的是
RMII 接 口 , 使 用 PHY 芯 片 提 供 50Mhz 時 鐘 驅 動 STM32F4 的
ETH_RMII_REF_CLK。
H.
這里是指外部 PHY 提供的 USB OTG HS(60MHZ)時鐘。
這里還需要說明一下,Cortex 系統定時器 Systick 的時鐘源可以是 AHB 時鐘 HCLK 或
HCLK 的 8 分頻。具體配置請參考 Systick 定時器配置,我們后面會在 5.1 小節講解 delay 文件
夾代碼的時候講解。
在以上的時鐘輸出中,有很多是帶使能控制的,例如 AHB 總線時鐘、內核時鐘、各種 APB1
外設、APB2 外設等等。當需要使用某模塊時,記得一定要先使能對應的時鐘。后面我們講解
實例的時候會講解到時鐘使能的方法。
4.3.2 STM32F4 時鐘初始化配置
上一小節我們對 STM32F407 時鐘樹進行了詳細講解,接下來我們來講解通過 STM32F4 的
HAL 庫進行 STM32F407 時鐘系統配置步驟。實際上,STM32F4 的時鐘系統配置也可以通過圖
形化配置工具 STM32CubeMX 來配置生成,這里我們講解初始化代碼,是為了讓大家對 STM32
時鐘系統有更加清晰的理解。圖形化配置工具 STM32CubeMX 在另外的 Cube MX 教程,大家
可以對比參考學習。
前面我們講解過,在系統啟動之后,程序會先執行 HAL 庫定義的 SystemInit 函數,進行系
統一些初始化配置。那么我們先來看看 SystemInit 程序:
void SystemInit(void){ /* FPU settings ------------------------------------------------------------*/ #if (__FPU_PRESENT == 1) && (__FPU_USED == 1)、 SCB->CPACR |= ((3UL << 10*2)|(3UL << 11*2)); /* set CP10 and CP11 Full Access */ #endif /* Reset the RCC clock configuration to the default reset state ------------*/ /* Set HSION bit */ RCC->CR |= (uint32_t)0x00000001; /* Reset CFGR register */ RCC->CFGR = 0x00000000; /* Reset HSEON, CSSON and PLLON bits */ RCC->CR &= (uint32_t)0xFEF6FFFF; /* Reset PLLCFGR register */ RCC->PLLCFGR = 0x24003010; /* Reset HSEBYP bit */ RCC->CR &= (uint32_t)0xFFFBFFFF; /* Disable all interrupts */ RCC->CIR = 0x00000000;#if defined (DATA_IN_ExtSRAM) || defined (DATA_IN_ExtSDRAM) SystemInit_ExtMemCtl(); #endif /* DATA_IN_ExtSRAM || DATA_IN_ExtSDRAM */ /* Configure the Vector Table location add offset address ------------------*/#ifdef VECT_TAB_SRAM SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM */#else SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH */#endif}
從上面代碼可以看出,SystemInit 主要做了如下四個方面工作:
1) FPU 設置
2) 復位 RCC 時鐘配置為默認復位值(默認開始了 HIS)
3) 外部存儲器配置
4) 中斷向量表地址配置
HAL 庫的 SystemInit 函數并沒有像標準庫的 SystemInit 函數一樣進行時鐘的初始化配置。HAL
庫的 SystemInit 函數除了打開 HSI 之外,沒有任何時鐘相關配置,所以使用 HAL 庫我們必須編
寫自己的時鐘配置函數。首先我們打開工程模板看看我們在工程 SYSTEM 分組下面定義的 sys.c
文件中的時鐘初始化函數 Stm32_Clock_Init 的內容:
//時鐘系統配置函數//Fvco=Fs*(plln/pllm);//SYSCLK=Fvco/pllp=Fs*(plln/(pllm*pllp));//Fusb=Fvco/pllq=Fs*(plln/(pllm*pllq));//Fvco:VCO 頻率//SYSCLK:系統時鐘頻率//Fusb:USB,SDIO,RNG 等的時鐘頻率//Fs:PLL 輸入時鐘頻率,可以是 HSI,HSE 等. //plln:主 PLL 倍頻系數(PLL 倍頻),取值范圍:64~432.//pllm:主 PLL 和音頻 PLL 分頻系數(PLL 之前的分頻),取值范圍:2~63.//pllp:系統時鐘的主 PLL 分頻系數(PLL 之后的分頻),取值范圍:2,4,6,8.(僅限這 4 個值!)//pllq:USB/SDIO/隨機數產生器等的主 PLL 分頻系數(PLL 之后的分頻),取值范圍:2~15.//外部晶振為 8M 的時候,推薦值:plln=336,pllm=8,pllp=2,pllq=7.//得到:Fvco=8*(336/8)=336Mhz// SYSCLK=336/2=168Mhz// Fusb=336/7=48Mhz//返回值:0,成功;1,失敗void Stm32_Clock_Init(u32 plln,u32 pllm,u32 pllp,u32 pllq){ HAL_StatusTypeDef ret = HAL_OK; RCC_OscInitTypeDef RCC_OscInitStructure; RCC_ClkInitTypeDef RCC_ClkInitStructure; __HAL_RCC_PWR_CLK_ENABLE(); //使能 PWR 時鐘 //下面這個設置用來設置調壓器輸出電壓級別,以便在器件未以最大頻率工作 //時使性能與功耗實現平衡。 __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);//設置調壓器輸出電壓級別 1 RCC_OscInitStructure.OscillatorType=RCC_OSCILLATORTYPE_HSE; //時鐘源為 HSE RCC_OscInitStructure.HSEState=RCC_HSE_ON; //打開 HSE RCC_OscInitStructure.PLL.PLLState=RCC_PLL_ON;//打開 PLL RCC_OscInitStructure.PLL.PLLSource=RCC_PLLSOURCE_HSE;//PLL 時鐘源選擇 HSE RCC_OscInitStructure.PLL.PLLM=pllm; //主 PLL 和音頻 PLL 分頻系數(PLL 之前的分頻),取值范圍:2~63. RCC_OscInitStructure.PLL.PLLN=plln; //主 PLL 倍頻系數(PLL 倍頻),取值范圍:64~432. RCC_OscInitStructure.PLL.PLLP=pllp;//系統時鐘的主 PLL 分頻系數(PLL 之后的分頻),取值范圍:2,4,6,8.(僅限這 4 個值!) RCC_OscInitStructure.PLL.PLLQ=pllq; //USB/SDIO/隨機數產生器等的主 PLL 分頻系數(PLL 之后的分頻),取值范圍:2~15. ret=HAL_RCC_OscConfig(&RCC_OscInitStructure);//初始化 if(ret!=HAL_OK) while(1); //選中 PLL 作為系統時鐘源并且配置 HCLK,PCLK1 和 PCLK2RCC_ClkInitStructure.ClockType=(RCC_CLOCKTYPE_SYSCLK|RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2); RCC_ClkInitStructure.SYSCLKSource=RCC_SYSCLKSOURCE_PLLCLK;//設置系統時鐘時鐘源為 PLL RCC_ClkInitStructure.AHBCLKDivider=RCC_SYSCLK_DIV1;//AHB 分頻系數為 1 RCC_ClkInitStructure.APB1CLKDivider=RCC_HCLK_DIV4; //APB1 分頻系數為 4 RCC_ClkInitStructure.APB2CLKDivider=RCC_HCLK_DIV2; //APB2 分頻系數為 2 ret=HAL_RCC_ClockConfig(&RCC_ClkInitStructure,FLASH_LATENCY_5);//同時設置 FLASH 延時周期為 5WS,也就是 6 個 CPU 周期。if(ret!=HAL_OK) while(1);//STM32F405x/407x/415x/417x Z 版本的器件支持預取功能if (HAL_GetREVID() == 0x1001){__HAL_FLASH_PREFETCH_BUFFER_ENABLE(); //使能 flash 預取}}
從函數注釋可知,函數 Stm32_Clock_Init 的作用是進行時鐘系統配置,除了配置 PLL 相關
參數確定 SYSCLK 值之外,還配置了 AHB,APB1 和 APB2 的分頻系數,也就是確定了 HCLK,
PCLK1 和 PCLK2 的時鐘值。我們首先來看看使用 HAL 庫配置 STM32F407 時鐘系統的一般步
驟:
1) 使能 PWR 時鐘:調用函數__HAL_RCC_PWR_CLK_ENABLE()。
2) 設置調壓器輸出電壓級別:調用函數__HAL_PWR_VOLTAGESCALING_CONFIG()。
3) 選擇是否開啟 Over-Driver 功能:調用函數 HAL_PWREx_EnableOverDrive()。
4) 配置時鐘源相關參數:調用函數 HAL_RCC_OscConfig()。
5) 配置系統時鐘源以及 AHB,APB1 和 APB2 的分頻系數:調用函數 HAL_RCC_ClockConfig()。
步驟 2 和 3,具有一定的關聯性,我們放在后面講解。對于步驟 1 之所以要使能 PWR 時鐘,是
因為后面的步驟設置調節器輸出電壓級別以及開啟 Over-Driver 功能都是電源控制相關配置,所
以必須開啟 PWR 時鐘。接下來我們先著重講解步驟 4 和步驟 5 的內容,這也是時鐘系統配置
的關鍵步驟。
對于步驟 4,使用 HAL 來配置時鐘源相關參數,我們調用的函數為 HAL_RCC_OscConfig(),
該函數在 HAL 庫關鍵頭文件 stm32f4xx_hal_rcc.h 中聲明,在文件 stm32f4xx_hal_rcc.c 中定義。
首先我們來看看該函數聲明:
__weak HAL_StatusTypeDef HAL_RCC_OscConfig(RCC_OscInitTypeDef *RCC_OscInitStruct);
該函數只有一個入口參數,就是結構體 RCC_OscInitTypeDef 類型指針。接下來我們看看結構體
RCC_OscInitTypeDef 的定義:
typedef struct{ uint32_t OscillatorType; //需要選擇配置的振蕩器類型 uint32_t HSEState; //HSE 狀態 uint32_t LSEState; //LSE 狀態 uint32_t HSIState; //HIS 狀態 uint32_t HSICalibrationValue; //HIS 校準值 uint32_t LSIState; //LSI 狀態 RCC_PLLInitTypeDef PLL; //PLL 配置}RCC_OscInitTypeDef;
對于這個結構體,前面幾個參數主要是用來選擇配置的振蕩器類型。比如我們要開啟 HSE,
那么我們會設置 OscillatorType 的值為 RCC_OSCILLATORTYPE_HSE,然后設置 HSEState 的值
為 RCC_HSE_ON 開啟 HSE。對于其他時鐘源 HSI,LSI 和 LSE,配置方法類似。這個結構體還
有一個很重要的成員變量是 PLL,它是結構體 RCC_PLLInitTypeDef 類型。它的作用是配置 PLL
相關參數,我們來看看它的定義:
typedef struct{ uint32_t PLLState; //PLL 狀態 uint32_t PLLSource; //PLL 時鐘源 uint32_t PLLM; //PLL 分頻系數 M uint32_t PLLN; //PLL 倍頻系數 N uint32_t PLLP; //PLL 分頻系數 P uint32_t PLLQ; //PL
從 RCC_PLLInitTypeDef;結構體的定義很容易看出該結構體主要用來設置 PLL 時鐘源以及
相關分頻倍頻參數。
這個結構體的定義我們就不做過多講解,接下來我們看看我們的時鐘初始化函數
Stm32_Clock_Init 中的配置內容:
RCC_OscInitStructure.OscillatorType=RCC_OSCILLATORTYPE_HSE; //時鐘源為 HSERCC_OscInitStructure.HSEState=RCC_HSE_ON; //打開 HSERCC_OscInitStructure.PLL.PLLState=RCC_PLL_ON; //打開 PLLRCC_OscInitStructure.PLL.PLLSource=RCC_PLLSOURCE_HSE; //PLL 時鐘源為 HSERCC_OscInitStructure.PLL.PLLM=pllm;RCC_OscInitStructure.PLL.PLLN=plln;RCC_OscInitStructure.PLL.PLLP=pllp;RCC_OscInitStructure.PLL.PLLQ=pllq;ret=HAL_RCC_OscConfig(&RCC_OscInitStructure);
通過該段函數,我們開啟了 HSE 時鐘源,同時選擇 PLL 時鐘源為 HSE,然后把
Stm32_Clock_Init 的 4 個入口參數直接設置作為 PLL 的參數 M,N,P 和 Q 的值,這樣就達到了設
置 PLL 時鐘源相關參數的目的。設置好 PLL 時鐘源參數之后,也就是確定了 PLL 的時鐘頻率,
接下來我們就需要設置系統時鐘,以及 AHB,APB1 和 APB2 相關參數,也就是我們前面提到
的步驟 5。
接下來我們來看看步驟 5 中提到的 HAL_RCC_ClockConfig()函數,聲明如下:HAL_StatusTypeDef HAL_RCC_ClockConfig(RCC_ClkInitTypeDef *RCC_ClkInitStruct,
uint32_t FLatency);
該函數有兩個入口參數,第一個入口參數 RCC_ClkInitStruct 是結構體 RCC_ClkInitTypeDef
指針類型,用來設置 SYSCLK 時鐘源以及 AHB,APB1 和 APB2 的分頻系數。第二個入口參數
FLatency 用來設置 FLASH 延遲,這個參數我們放在后面跟步驟 2 和步驟 3 一起講解。
RCC_ClkInitTypeDef 結構體類型定義非常簡單,這里我們就不列出來,我們來看看
Stm32_Clock_Init 函數中的配置內容:
//選中 PLL 作為系統時鐘源并且配置 HCLK,PCLK1 和 PCLK2
RCC_ClkInitStructure.ClockType=(RCC_CLOCKTYPE_SYSCLK|\\
RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_PCLK1
|RCC_CLOCKTYPE_PCLK2);
RCC_ClkInitStructure.SYSCLKSource=RCC_SYSCLKSOURCE_PLLCLK;//系統時鐘源 PLL
RCC_ClkInitStructure.AHBCLKDivider=RCC_SYSCLK_DIV1;//AHB 分頻系數為 1
RCC_ClkInitStructure.APB1CLKDivider=RCC_HCLK_DIV4; //APB1 分頻系數為 4
RCC_ClkInitStructure.APB2CLKDivider=RCC_HCLK_DIV2; //APB2 分頻系數為 2
ret=HAL_RCC_ClockConfig(&RCC_ClkInitStructure,FLASH_LATENCY_5);
第一個參數 ClockType 配置說明我們要配置的是 SYSCLK,HCLK,PCLK1 和 PCLK2 四個時鐘。
第二個參數 SYSCLKSource 配置選擇系統時鐘源為 PLL。
第三個參數 AHBCLKDivider 配置 AHB 分頻系數為 1。
第四個參數 APB1CLKDivider 配置 APB1 分頻系數為 4。
第五個參數 APB2CLKDivider 配置 APB2 分頻系數為 2。
根據我們在主函數中調用 Stm32_Clock_Init(336,8,2,7)時候設置的入口參數值,我們可以計
算出,PLL 時鐘為 PLLCLK=HSE*N/M*P=8MHz*336/(8*2)=168MHz,同時我們選擇系統時鐘
源 為 PLL , 所 以 系 統 時 鐘 SYSCLK=168MHz 。 AHB 分 頻 系 數 為 1 , 故 其 頻 率 為
HCLK=SYSCLK/1=168MHz。APB1 分頻系數為 4,故其頻率為 PCLK1=HCLK/4=42MHz。APB2
分頻系數為 2,故其頻率為 PCLK2=HCLK/2=168/2=84MHz。最后我們總結一下通過調用函數
Stm32_Clock_Init(336,8,2,7)之后的關鍵時鐘頻率值:
SYSCLK(系統時鐘)
=168MHz
PLL 主時鐘
=168MHz
AHB 總線時鐘(HCLK=SYSCLK/1)
=168MHz
APB1 總線時鐘(PCLK1=HCLK/4)
=42MHz
APB2 總線時鐘(PCLK2=HCLK/2)
=84MHz
時鐘系統配置相關知識就給大家講解到這里。
4.4 IO 引腳復用器和映射
STM32F4 有很多的內置外設,這些外設的外部引腳都是與 GPIO 復用的。也就是說,一個 GPIO
如果可以復用為內置外設的功能引腳,那么當這個 GPIO 作為內置外設使用的時候,就叫做復用。
這部分知識在《STM32F4 中文參考手冊》第七章和芯片數據手冊有詳細的講解哪些 GPIO 管腳是
可以復用為哪些內置外設。
對于本小節知識,STM32F4 中文參考手冊講解比較詳細,我們同樣會從中抽取重要的知識點
羅列出來。同時,我們會以串口使用為例給大家講解具體的引腳復用的配置。
STM32F4 系列微控制器 IO 引腳通過一個復用器連接到內置外設或模塊。該復用器一次只允
許一個外設的復用功能(AF)連接到對應的 IO 口。這樣可以確保共用同一個 IO 引腳的外設之
間不會發生沖突。
每個 IO 引腳都有一個復用器,該復用器采用 16 路復用功能輸入(AF0 到 AF15),可通過
GPIOx_AFRL(針對引腳 0-7)和 GPIOx_AFRH(針對引腳 8-15)寄存器對這些輸入進行配置,每四
位控制一路復用:
1)完成復位后,所有 IO 都會連接到系統的復用功能 0(AF0)。
2)外設的復用功能映射到 AF1 到 AF13。
3)Cortex-M4 EVENTOUT 映射到 AF15。
復用器示意圖如下圖 4.4.1:
圖 4.4.1 復用器示意圖
接下來,我們簡單說明一下這個圖要如何看,舉個例子,探索者 STM32F407 開發板的原
理圖上 PC11 的原理圖如圖 4.4.2 所示:
圖 4.4.2 探索者 STM32F407 開發板 PC11 原理圖
如上圖所示,PC11 可以作為 SPI3_MISO/U3_RX/U4_RX/SDIO_D3/DCMI_D4/I2S3ext_SD
等復用功能輸出,這么多復用功能,如果這些外設都開啟了,那么對 STM32F1 來說,那就可
能亂套了,外設之間可互相干擾,但是 STM32F4,由于有復用功能選擇功能,可以讓 PC11 僅
連接到某個特定的外設,因此不存在互相干擾的情況。
上圖 4.4.1 是針對引腳 0-7,對于引腳 8-15,控制寄存器為 GPIOx_AFRH。從圖中可以看出。
當需要使用復用功能的時候,我們配置相應的寄存器 GPIOx_AFRL 或者 GPIOx_AFRH,讓對應引
腳通過復用器連接到對應的復用功能外設。這里我們列出 GPIOx_AFRL 寄存器的描述,
GPIOx_AFRH 的作用跟 GPIOx_AFRL 類似,只不過 GPIOx_AFRH 控制的是一組 IO 口的高八位,
GPIOx_AFRL 控制的是一組 IO 口的低八位。
圖 4.4.3 GPIOx_AFRL 寄存器位描述
從表中可以看出,32 位寄存器 GPIOx_AFRL 每四個位控制一個 IO 口,所以每個寄存器控制
32/4=8 個 IO 口。寄存器對應四位的值配置決定這個 IO 映射到哪個復用功能 AF。
在微控制器完成復位后,所有 IO 口都會連接到系統復用功能 0(AF0)。這里大家需要注意,
對于系統復用功能 AF0,我們將 IO 口連接到 AF0 之后,還要根據所用功能進行配置:
1) JTAG/SWD:在器件復位之后,會將這些功能引腳指定為專用引腳。也就是說,這些引腳
在復位后默認就是 JTAG/SWD 功能。如果我們要作為 GPIO 來使用,就需要對對應的 IO
口復用器進行配置。
2) RTC_REFIN:此引腳在系統復位之后要使用的話要配置為浮空輸入模式。
3) MCO1 和 MCO2:這些引腳在系統復位之后要使用的話要配置為復用功能模式。
對于外設復用功能的配置,除了 ADC 和 DAC 要將 IO 配置為模擬通道之外其他外設功能一律
要配置為復用功能模式,這個配置是在 IO 口對應的 GPIOx_MODER 寄存器中配置的。同時要配
置 GPIOx_AFRH 或者 GPIOx_AFRL 寄存器,將 IO 口通過復用器連接到所需要的復用功能對應的
AFx。
不是每個 IO 口都可以復用為任意復用功能外設。到底哪些 IO 可以復用為相關外設呢?這
在芯片對應的數據手冊(請參考光盤目錄:)上面會有詳細的表格列出來。對于 STM32F407,數
據手冊里面的 Table 9.Alternate function mapping 表格列出了所有的端口 AF 映射表,因為
表格比較大,所以這里只列出 PORTA 的幾個端口為例方便大家理解:
表 4.4.4 PORTA 部分端口 AF 映射表
上一節我們講解了時鐘系統配置步驟。在配置好時鐘系統之后,如果我們要使用某些外設,
例如 GPIO,ADC 等,我們還要使能這些外設時鐘。這里大家必須注意,如果在使用外設之前
沒有使能外設時鐘,這個外設是不可能正常運行的。STM32 的外設時鐘使能是在 RCC 相關寄
存器中配置的。因為 RCC 相關寄存器非常多,有興趣的同學可以直接打開《STM32F4 中文參
考手冊》6.3 小節查看所有 RCC 相關寄存器的配置。接下來我們來講解通過 STM32F4 的 HAL
庫使能外設時鐘的方法。
在 STM32F4 的 HAL 庫中,外設時鐘使能操作都是在 RCC 相關 HAL 庫文件頭文件
stm32f4xx_hal_rcc.h 定義的。大家打開 stm32f4xx_hal_rcc.h 頭文件可以看到文件中除了少數幾
個函數聲明之外大部分都是宏定義標識符。外設時鐘使能在 HAL 庫中都是通過宏定義標識符
來實現的。首先,我們來看看 GPIOA 的外設時鐘使能宏定義標識符:
#define __HAL_RCC_GPIOA_CLK_ENABLE()
do { \\
__IO uint32_t tmpreg = 0x00; \\
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN);\\
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN);\\
UNUSED(tmpreg); \\
} while(0)
這幾行代碼非常簡單,主要是定義了一個宏定義標識符__HAL_RCC_GPIOA_CLK_ENABLE(),
它的核心操作是通過下面這行代碼實現的:
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN);
這行代碼的作用是,設置寄存器 RCC->AHB1ENR 的相關位為 1,至于是哪個位,是由宏定義
標識符 RCC_AHB1ENR_GPIOAEN 的值決定的,而它的值為:
#define RCC_AHB1ENR_GPIOAEN ((uint32_t)0x00000001)
所以,我們很容易理解上面代碼的作用是設置寄存器 RCC->AHB1ENR 寄存器的最低位為 1。
我們可以從 STM32F4 的中文參考手冊中搜索 AHB1ENR 寄存器定義,最低位的作用是用來使
用 GPIOA 時鐘。AHB1ENR 寄存器的位 0 描述如下:
位 0
GPIOAEN:IO 端口 A 時鐘使能
由軟件置 1 和清零
0:禁止 IO 端口 A 時鐘
1:使能 IO 端口 A 時鐘
那么我們只需要在我們的用戶程序中調用宏定義標識符__HAL_RCC_GPIOA_CLK_ENABLE()
就可以實現 GPIOA 時鐘使能。使用方法為:
__HAL_RCC_GPIOA_CLK_ENABLE();//使能 GPIOA 時鐘
對于其他外設,同樣都是在 stm32f4xx_hal_rcc.h 頭文件中定義,大家只需要找到相關宏定義標
識符即可,這里我們列出幾個常用使能外設時鐘的宏定義標識符使用方法:
__HAL_RCC_DMA1_CLK_ENABLE();//使能 DMA1 時鐘
__HAL_RCC_USART2_CLK_ENABLE();//使能串口 2 時鐘
__HAL_RCC_TIM1_CLK_ENABLE();//使能 TIM1 時鐘
我們使用外設的時候需要使能外設時鐘,如果我們不需要使用某個外設,同樣我們可以禁
止某個外設時鐘。禁止外設時鐘使用方法和使能外設時鐘非常類似,同樣是頭文件中定義的宏
定義標識符。我們同樣以 GPIOA 為例,宏定義標識符為:
#define __HAL_RCC_GPIOA_CLK_DISABLE() \\
(RCC->AHB1ENR &= ~(RCC_AHB1ENR_GPIOAEN))
同樣,宏定義標識符__HAL_RCC_GPIOA_CLK_DISABLE()的作用是設置 RCC->AHB1ENR 寄
存器的最低位為 0,也就是禁止 GPIOA 時鐘。具體使用方法我們這里就不做過多講解,我們這
里同樣列出幾個常用的禁止外設時鐘的宏定義標識符使用方法:
__HAL_RCC_DMA1_CLK_DISABLE();//禁止 DMA1 時鐘
__HAL_RCC_USART2_CLK_DISABLE();//禁止串口 2 時鐘
__HAL_RCC_TIM1_CLK_DISABLE();//禁止 TIM1 時鐘
關于 STM32F4 的外設時鐘使能和禁止方法我們就給大家講解到這里。
這類函數跟前面講解的外設時鐘函數使用方法基本一致,不同的是一個是用來使能外設時
鐘,一個是用來復位對應的外設。這里大家在調用函數的時候一定不要混淆。
對于這些時鐘操作函數,我們就不一一列舉出來,大家可以打開 RCC 對應的文件仔細了解。
ST32F4 的端口復用和映射就給大家講解到這里,希望大家課余結合相關實驗工程和手冊鞏
固本小節知識。
4.5 STM32 NVIC 中斷優先級管理
CM4 內核支持 256 個中斷,其中包含了 16 個內核中斷和 240 個外部中斷,并且具有 256
級的可編程中斷設置。但 STM32F407 并沒有使用 CM4 內核的全部東西,而是只用了它的一部
分。STM32F4xx 則總共有 101 個中斷,以下僅以 STM32F407xx 為例講解。
STM32F407xx 的 96 個中斷里面,包括 10 個內核中斷和 91 個可屏蔽中斷,具有 16 級可編
程的中斷優先級,而我們常用的就是這 91 個可屏蔽中斷。在 MDK 內,與 NVIC 相關的寄存器,
MDK 為其定義了如下的結構體:
typedef struct
{
__IO uint32_t ISER[8];
uint32_t RESERVED0[24];
__IO uint32_t ICER[8];
uint32_t RSERVED1[24];
__IO uint32_t ISPR[8];
uint32_t RESERVED2[24];
__IO uint32_t ICPR[8];
uint32_t RESERVED3[24];
__IO uint32_t IABR[8];
uint32_t RESERVED4[56];
__IO uint8_t IP[240];
uint32_t RESERVED5[644];
__O uint32_t STIR;
} NVIC_Type;
STM32F407 的中斷在這些寄存器的控制下有序的執行的。只有了解這些中斷寄存器,才能
方便的使用 STM32F407 的中斷。下面重點介紹這幾個寄存器:
ISER[8]:ISER 全稱是:Interrupt Set-Enable Registers,這是一個中斷使能寄存器組。上面
說了 CM4 內核支持 256 個中斷,這里用 8 個 32 位寄存器來控制,每個位控制一個中斷。但是
STM32F407 的可屏蔽中斷最多只有 91 個,所以對我們來說,有用的就是三個(ISER[0~2]]),
總共可以表示 96 個中斷。而 STM32F407 只用了其中的前 91 個。ISER[0]的 bit0~31 分別對應
中斷 0~31;ISER[1]的 bit0~32 對應中斷 32~63;ISER[2]的 bit0~26 對應中斷 64~90;這樣總共
91 個中斷就分別對應上了。你要使能某個中斷,必須設置相應的 ISER 位為 1,使該中斷被使
能(這里僅僅是使能,還要配合中斷分組、屏蔽、IO 口映射等設置才算是一個完整的中斷設置)。
具體每一位對應哪個中斷,請參考 STM32F407xx.h 里面的第 84 行處。
ICER[8]:全稱是:Interrupt Clear-Enable Registers,是一個中斷除能寄存器組。該寄存器組
與 ISER 的作用恰好相反,是用來清除某個中斷的使能的。其對應位的功能,也和 ICER 一樣。
這里要專門設置一個 ICER 來清除中斷位,而不是向 ISER 寫 0 來清除,是因為 NVIC 的這些寄
存器都是寫 1 有效的,寫 0 是無效的。
ISPR[8]:全稱是:Interrupt Set-Pending Registers,是一個中斷掛起控制寄存器組。每個位
對應的中斷和 ISER 是一樣的。通過置 1,可以將正在進行的中斷掛起,而執行同級或更高級別
的中斷。寫 0 是無效的。
ICPR[8]:全稱是:Interrupt Clear-Pending Registers,是一個中斷解掛控制寄存器組。其作
用與 ISPR 相反,對應位也和 ISER 是一樣的。通過設置 1,可以將掛起的中斷接掛。寫 0 無效。
IABR[8]:全稱是:Interrupt Active Bit Registers,是一個中斷激活標志位寄存器組。對應位
所代表的中斷和 ISER 一樣,如果為 1,則表示該位所對應的中斷正在被執行。這是一個只讀寄
存器,通過它可以知道當前在執行的中斷是哪一個。在中斷執行完了由硬件自動清零。
IP[240]:全稱是:Interrupt Priority Registers,是一個中斷優先級控制的寄存器組。這個寄
存器組相當重要!STM32F407 的中斷分組與這個寄存器組密切相關。IP 寄存器組由 240 個 8bit
的寄存器組成,每個可屏蔽中斷占用 8bit,這樣總共可以表示 240 個可屏蔽中斷。而 STM32F407
只用到了其中的 91 個。IP[90]~IP[0]分別對應中斷 90~0。而每個可屏蔽中斷占用的 8bit 并沒有
全部使用,而是 只用了高 4 位。這 4 位,又分為搶占優先級和子優先級。搶占優先級在前,子
優先級在后。而這兩個優先級各占幾個位又要根據 SCB->AIRCR 中的中斷分組設置來決定。
這里簡單介紹一下 STM32F407 的中斷分組:STM32F407 將中斷分為 5 個組,組 0~4。該
分組的設置是由 SCB->AIRCR 寄存器的 bit10~8 來定義的。具體的分配關系如表 4.5.1 所示:
表 4.5.1AIRCR 中斷優先級分組設置表
通過這個表,我們就可以清楚的看到組 0~4 對應的配置關系,例如組設置為 3,那么此時
所有的 91 個中斷,每個中斷的中斷優先寄存器的高四位中的最高 3 位是搶占優先級,低 1 位是
響應優先級。每個中斷,你可以設置搶占優先級為 0~7,響應優先級為 1 或 0。搶占優先級的
級別高于響應優先級。而數值越小所代表的優先級就越高。
這里需要注意兩點:第一,如果兩個中斷的搶占優先級和響應優先級都是一樣的話,則看
哪個中斷先發生就先執行;第二,高優先級的搶占優先級是可以打斷正在進行的低搶占優先級
中斷的。而搶占優先級相同的中斷,高優先級的響應優先級不可以打斷低響應優先級的中斷。
結合實例說明一下:假定設置中斷優先級組為 2,然后設置中斷 3(RTC_WKUP 中斷)的搶
占優先級為 2,響應優先級為 1。中斷 6(外部中斷 0)的搶占優先級為 3,響應優先級為 0。中
斷 7(外部中斷 1)的搶占優先級為 2,響應優先級為 0。那么這 3 個中斷的優先級順序為:中
斷 7>中斷 3>中斷 6。
上面例子中的中斷 3 和中斷 7 都可以打斷中斷 6 的中斷。而中斷 7 和中斷 3 卻不可以相互
打斷!
通過以上介紹,我們熟悉了 STM32F407 中斷設置的大致過程。接下來我們介紹如何使用
HAL 庫實現以上中斷分組設置以及中斷優先級管理,使中斷配置簡單化。NVIC 中斷管理相關
函數主要在 HAL 庫關鍵文件 stm32f4xx_hal_cortex.c 中定義。
首先要講解的是中斷優先級分組函數 HAL_NVIC_SetPriorityGrouping,其函數申明如下:
void HAL_NVIC_SetPriorityGrouping(uint32_t PriorityGroup);
這個函數的作用是對中斷的優先級進行分組,這個函數在系統中只需要被調用一次,一旦
分組確定就最好不要更改,否則容易造成程序分組混亂。這個函數我們可以找到其函數體內容
如下:
void HAL_NVIC_SetPriorityGrouping(uint32_t PriorityGroup)
{
/* Check the parameters */
assert_param(IS_NVIC_PRIORITY_GROUP(PriorityGroup));
/* Set the PRIGROUP[10:8] bits according to the PriorityGroup parameter value */
NVIC_SetPriorityGrouping(PriorityGroup);
}
從函數體以及注釋可以看出,這個函數是通過調用函數 NVIC_SetPriorityGrouping 來進行中斷
優先級分組設置。通過查找(參考 3.5.3 小節 MDK 中“Go to definition of”的使用方法),我們可
以知道函數 NVIC_SetPriorityGrouping 是在文件 core_cm4.h 頭文件中定義的。接下來,我們來
分析一下函數 NVIC_SetPriorityGrouping 函數定義。定義如下:
__STATIC_INLINE void NVIC_SetPriorityGrouping(uint32_t PriorityGroup)
{
uint32_t reg_value;
uint32_t PriorityGroupTmp = (PriorityGroup & (uint32_t)0x07UL);
reg_value= SCB->AIRCR; /* read old register configuration */
reg_value&=~((uint32_t)(SCB_AIRCR_VECTKEY_Msk |SCB_AIRCR_PRIGROUP_Msk));
reg_value = (reg_value|((uint32_t)0x5FAUL << SCB_AIRCR_VECTKEY_Pos) |
(PriorityGroupTmp << 8U) );
SCB->AIRCR = reg_value;
}
從函數內容可以看出,這個函數主要作用是通過設置 SCB->AIRCR 寄存器的值來設置中斷優先
級分組,這在前面寄存器講解的過程中已經講到。
關于函數 HAL_NVIC_SetPriorityGrouping 的函數體內容解讀我就給大家介紹到這里。接下
來我們來看看這個函數的入口參數。大家繼續回到函數 HAL_NVIC_SetPriorityGrouping 的定義
可以看到,函數的最開頭有這樣一行函數:
assert_param(IS_NVIC_PRIORITY_GROUP(PriorityGroup));
其中函數 assert_param 是斷言函數,它的作用主要是對入口參數的有效性進行判斷。也就是說
我們可以通過這個函數知道入口參數在哪些范圍內是有效的。而其入口參數通過在 MDK 中雙
擊選中 “IS_NVIC_PRIORITY_GROUP”,然后右鍵“Go to defition of …”可以查看到為:
#define IS_NVIC_PRIORITY_GROUP(GROUP)
(((GROUP) == NVIC_PriorityGroup_0) ||\\
((GROUP) == NVIC_PriorityGroup_1) || \\
((GROUP) == NVIC_PriorityGroup_2) || \\
((GROUP) == NVIC_PriorityGroup_3) || \\
((GROUP) == NVIC_PriorityGroup_4))
從這個內容可以看出,當 GROUP 的值為 NVIC_PriorityGroup_0~ NVIC_PriorityGroup_4 的時候,
IS_NVIC_PRIORITY_GROUP 的值才為真。這也就是我們上面表 4.5.1 講解的,分組范圍為 0-4,
對應的入口參數為宏定義值 NVIC_PriorityGroup_0~ NVIC_PriorityGroup_4。比如我們設置整個
系統的中斷優先級分組值為 2,那么方法是:
HAL_NVIC_SetPriorityGrouping (NVIC_PriorityGroup_2);
這樣就確定了中斷優先級分組為 2,也就是 2 位搶占優先級,2 位響應優先級,搶占優先級和響
應優先級的值的范圍均為 0-3。
講到這里,大家對怎么進行系統的中斷優先級分組設置,以及具體的中斷優先級設置函數
HAL_NVIC_SetPriorityGrouping 的內部函數實現都有了一個詳細的理解。接下來我們來看看在
HAL 庫里面,是怎樣調用 HAL_NVIC_SetPriorityGrouping 函數進行分組設置的。
打開 stm32f4xx_hal.c 文件可以看到,文件內部定義了 HAL 庫初始化函數 HAL_Init,這個
函數非常重要,其作用主要是對中斷優先級分組,FLASH 以及硬件層進行初始化,我們在 3.1
小節對其進行了比較詳細的講解。這里我們只需要知道,在系統主函數 main 開頭部分,我們都
會首先調用 HAL_Init 函數進行一些初始化操作。在 HAL_Init 內部,有如下一行代碼:
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
這行代碼的作用是把系統中斷優先級分組設置為分組 4,這在我們前面已經詳細講解。也
就是說,在主函數中調用 HAL_Init 函數之后,在 HAL_Init 函數內部會通過調用我們前面講解
的 HAL_NVIC_SetPriorityGrouping 函數來進行系統中斷優先級分組設置。所以,我們要進行中
斷優先級分組設置,只需要修改 HAL_Init 函數內部的這行代碼即可。中斷優先級分組的內容我
們就給大家講解到這里。
設置好了系統中斷分組,也就是確定了那么對于每個中斷我們又怎么確定他的搶占優先級
和響應優先級呢?官方 HAL 庫文件 stm32f4xx_hal_cortex.c 中定義了三個單個中斷優先級設置
函數。函數如下:
void HAL_NVIC_SetPriority(IRQn_Type IRQn, uint32_t PreemptPriority, uint32_t SubPriority);
void HAL_NVIC_EnableIRQ(IRQn_Type IRQn);
void HAL_NVIC_DisableIRQ(IRQn_Type IRQn);
第一個函數 HAL_NVIC_SetPriority 是用來設置單個優先級的搶占優先級和響應優先級的值。
第二個函數 HAL_NVIC_EnableIRQ 是用來使能某個中斷通道。
第三個函數 HAL_NVIC_DisableIRQ 是用來清除某個中斷使能的,也就是中斷失能。
這三個函數的使用都非常簡單,對于具體的調用方法,大家可以參考我們后面第九章外部中斷
實驗講解。
這里大家還需要注意,中斷優先級分組和中斷優先級設置是兩個不同的概念。中斷優先級
分組是用來設置整個系統對于中斷分組設置為哪個分組,分組號為 0-4,設置函數為
HAL_NVIC_SetPriorityGrouping,確定了中斷優先級分組號,也就確定了系統對于單個中斷的
搶占優先級和響應優先級設置各占幾個位(對應表 4.5.1)。設置好中斷優先級分組,確定了分
組號之后,接下來我們就是要對單個優先級進行中斷優先級設置。也就是這個中斷的搶占優先
級和響應優先級的值,設置方法就是我們上面講解的三個函數。
最后我們總結一下中斷優先級設置的步驟:
①系統運行開始的時候設置中斷分組。確定組號,也就是確定搶占優先級和響應優先級的
分配位數。設置函數為 HAL_NVIC_PriorityGroupConfig。對于 HAL 庫,在文件 stm32f4xx_hal.c
內部定義函數 HAL_Init 中有調用 HAL_NVIC_PriorityGroupConfig 函數進行相關設置,所以我
們只需要修改 HAL_Init 內部對中斷優先級分組設置即可。
① 設置單個中斷的中斷優先級別和使能相應中斷通道,使用到的函數函數主要為函數
HAL_NVIC_SetPriority 和函數 HAL_NVIC_EnableIRQ。
4.6 MDK 中寄存器地址名稱映射分析
之所以要講解這部分知識,是因為經常會遇到客戶提到不明白 HAL 庫中那些結構體是怎么
與寄存器地址對應起來的。這里我們就做一個簡要的分析吧。
首先我們看看 51 中是怎么做的。51 單片機開發中經常會引用一個 reg51.h 的頭文件,下
面我們看看他是怎么把名字和寄存器聯系起來的:
sfr P0 =0x80;
sfr 也是一種擴充數據類型,點用一個內存單元,值域為 0~255。利用它可以訪問 51 單片
機內部的所有特殊功能寄存器。如用 sfr P1 = 0x90 這一句定義 P1 為 P1 端口在片內的寄存
器。然后我們往地址為 0x80 的寄存器設值的方法是:P0=value;
那么在 STM32 中,是否也可以這樣做呢??答案是肯定的??隙ㄒ部梢酝ㄟ^同樣的方
式來做,但是 STM32 因為寄存器太多太多,如果一一以這樣的方式列出來,那要好大的篇
幅,既不方便開發,也顯得太雜亂無序的感覺。所以 MDK 采用的方式是通過結構體來將
寄存器組織在一起。下面我們就講解 MDK 是怎么把結構體和地址對應起來的,為什么我
們修改結構體成員變量的值就可以達到操作對應寄存器的值。這些事情都是在 stm32f4xx.h
文件中完成的。我們通過 GPIOA 的幾個寄存器的地址來講解吧。
首先我們可以查看《STM32F4 中文參考手冊》中的寄存器地址映射表(P193)。這里我
們選用 GPIOA 為例來講解。GPIOA 寄存器地址映射如下表 4.6.1:
表 4.6.1 GIPOA 寄存器地址偏移表
從這個表我們可以看出,因為 GIPO 寄存器都是 32 位,所以每組 GPIO 的 10 個寄存器
中,每個寄存器占有 4 個地址,一共占用 40 個地址,地址偏移范圍為(0x00~0x24)。這個
地址偏移是相對 GPIOA 的基地址而言的。GPIOA 的基地址是怎么算出來的呢?因為 GPIO
都是掛載在 AHB1 總線之上,所以它的基地址是由 AHB1 總線的基地址加上 GPIOA 在
AHB1 總線上的偏移地址決定的。同理依次類推,我們便可以算出 GPIOA 基地址了。下面
我們打開 stm32f429.h 定位到 GPIO_TypeDef 定義處:
typedef struct
{
__IO uint32_t MODER;
__IO uint32_t OTYPER;
__IO uint32_t OSPEEDR;
__IO uint32_t PUPDR;
__IO uint32_t IDR;
__IO uint32_t ODR;
__IO uint32_t BSRR;
__IO uint32_t LCKR;
__IO uint32_t AFR[2];
} GPIO_TypeDef;
然后定位到:
#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
可以看出,GPIOA 是將 GPIOA_BASE 強制轉換為 GPIO_TypeDef 結構體指針,這句話的
意思是,GPIOA 指向地址 GPIOA_BASE,GPIOA_BASE 存放的數據類型為 GPIO_TypeDef。
然后在 MDK 中雙擊“GPIOA_BASE”選中之后右鍵選中“Go to definition of ”,便可以查
看 GPIOA_BASE 的宏定義:
#define GPIOA_BASE (AHB1PERIPH_BASE + 0x0000)
依次類推,可以找到最頂層:
#define AHB1PERIPH_BASE (PERIPH_BASE + 0x00020000)
#define PERIPH_BASE ((uint32_t)0x40000000)
所以我們便可以算出 GPIOA 的基地址位:
GPIOA_BASE= 0x40000000+0x00020000+0x0000=0x40020000
下面我們再跟《STM32F 中文參考手冊》比較一下看看 GPIOA 的基地址是不是 0x40020000 。
截圖 P53 存儲器映射表我們可以看到,GPIOA 的起始地址也就是基地址確實是 0x40020000:
圖 4.6.2 GPIO 存儲器地址映射表
同樣的道理,我們可以推算出其他外設的基地址。
上面我們已經知道 GPIOA 的基地址,那么那些 GPIOA 的 10 個寄存器的地址又是怎么
算出來的呢?在上面我們講過 GPIOA 的各個寄存器對于 GPIOA 基地址的偏移地址,所以
我們自然可以算出來每個寄存器的地址。
GPIOA 的寄存器的地址=GPIOA 基地址+寄存器相對 GPIOA 基地址的偏移值
這個偏移值在上面的寄存器地址映像表中可以查到。
那么在結構體里面這些寄存器又是怎么與地址一一對應的呢?這里涉及到結構體成員
變量地址對齊方式方面的知識,這方面的知識大家可以在網上查看相關資料復習一下,這
里我們不做詳細講解。在我們定義好地址對齊方式之后,每個成員變量對應的地址就可以
根據其基地址來計算。對于結構體類型 GPIO_TypeDef,他的所有成員變量都是 32 位,成
員變量地址具有連續性。所以自然而然我們就可以算出 GPIOA 指向的結構體成員變量對應
地址了。
表 4.6.3 GPIOA 各寄存器實際地址表
我們可以把 GPIO_TypeDef 的定義中的成員變量的順序和 GPIOx 寄存器地址映像對比
可以發現,他們的順序是一致的,如果不一致,就會導致地址混亂了。
這就是為什么 HAL 庫里面:GPIOA->BSRR=value;就是設置地址為 0x40020000
+0x18 (BSRR 偏移量)=0x40020018 的寄存器 BSRR 的值了。它和 51 里面 P0=value 是設置
地址為 0x80 的 P0 寄存器的值是一樣的道理。
看到這里你是否會學起來踏實一點呢?STM32 使用的方式雖然跟 51 單片機不一樣,
但是原理都是一致的。
4.7 MDK 代碼快速組織代碼技巧
這一節主要講解在 MDK 中使用 HAL 庫開發的一些小技巧,僅供初學者參考。這節的知識
大家可以在學習第一個跑馬燈實驗的時候參考一下,對初學者應該很有幫助。我們就用最簡單
的 GPIO 初始化函數為例。
現 在 我 們 要 初 始 化 某 個 GPIO 端 口 , 我 們 要 怎 樣 快 速 操 作 呢 ? 在 頭 文 件
stm32f4xx_hal_gpio.h 頭文件中,聲明 GPIO 初始化函數為:
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init);
現在我們想寫初始化函數,那么我們在不參考其他代碼的前提下,怎么快速組織代碼呢?
首先,我們可以看出,函數的入口參數是 GPIO_TypeDef 類型指針和 GPIO_InitTypeDef 類型指針,因為 GPIO_TypeDef 入口參數比較簡單,所以我們 就通過第二個入口參數
GPIO_InitTypeDef 類型指針來講解。雙擊 GPIO_InitTypeDef 后右鍵選擇“Go to definition of…”,
(前提是打開了“Browse Information”選項,可以參考前面 3.3.2 章節說明,勾選上打開),如
下圖 4.7.1:
圖 4.7.1 查看類型定義方法
于是定位到 stm32f4xx_hal_gpio.h 中 GPIO_InitTypeDef 的定義處:
typedef struct
{
uint32_t Pin;
uint32_t Mode;
uint32_t Pull;
uint32_t Speed;
uint32_t Alternate;
}GPIO_InitTypeDef;
可以看到這個結構體有 5 個成員變量,這也告訴我們一個信息,一個 GPIO 口的狀態是由模式
(Mode),速度(Speed)以及上下拉(Pull)來決定的。我們首先要定義一個結構體變量,下面
我們定義:
GPIO_InitTypeDef GPIO_InitStructure;
接著我們要初始化結構體變量 GPIO_InitStructure。首先我們要初始化成員變量 Pin,這個時候我
們就有點迷糊了,這個變量到底可以設置哪些值呢?這些值的范圍有什么規定嗎?
這里我們就回到 HAL_GPIO_Init 聲明處,同樣雙擊 HAL_GPIO_Init,右鍵點擊“Go to
definition of …”,這樣光標定位到 stm32f4xx_hal_gpio.c 文件中的 HAL_GPIO_Init 函數體開始處,
我們可以看到在函數中有如下幾行:
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init)
{
…//此處省略部分代碼
assert_param(IS_GPIO_ALL_INSTANCE(GPIOx));
assert_param(IS_GPIO_PIN(GPIO_Init->Pin));
assert_param(IS_GPIO_MODE(GPIO_Init->Mode));
assert_param(IS_GPIO_PULL(GPIO_Init->Pull));
…//此處省略部分代碼
assert_param(IS_GPIO_AF(GPIO_Init->Alternate));
…//此處省略部分代碼
}
顧名思義,assert_param 是斷言語句,是對函數入口參數的有效性進行判斷,所以我們可以從
這個函數入手,確定入口參數范圍。第一行是對第一個參數 GPIOx 進行有效性判斷,雙擊
“IS_GPIO_ALL_INSTANCE”右鍵點擊“go to defition of…” 定位到了下面的定義:
#define IS_GPIO_ALL_INSTANCE(INSTANCE) (((INSTANCE) == GPIOA) || \\
((INSTANCE) == GPIOB) || \\
((INSTANCE) == GPIOC) || \\
((INSTANCE) == GPIOD) || \\
…//此處省略部分代碼
((INSTANCE) == GPIOJ) || \\
((INSTANCE) == GPIOK))
很明顯可以看出,GPIOx 的取值規定只允許是 GPIOA~GPIOK。
同樣的辦法,我們雙擊“IS_GPIO_PIN” 右鍵點擊“go to defition of…”,定位到下面的定義:
#define IS_GPIO_PIN(PIN) (((PIN) & GPIO_PIN_MASK ) != (uint32_t)0x00)
同時,宏定義標識符 GPIO_PIN_MASK 的定義為:
#define GPIO_PIN_MASK ((uint32_t)0x0000FFFF)
從上面可以看出,PIN 取值只要低 16 位不為 0 即可。這里需要大家注意,因為一組 IO 口只有
16 個 IO,實際上 PIN 的值在這里只有低 16 位有效,所以 PIN 的取值范圍為 0x0001~0xFFFF。
那么是不是我們寫代碼初始化就是直接給一個 16 位的數字呢?這也是可以的,但是大多數情況
下,我們不會直接在入口參數處設置一個簡單的數字,因為這樣代碼的可讀性太差,HAL 庫會
將這些數字的含義 通過宏定義定義出來,這樣可讀性大大增強。我們可以看到在
GPIO_PIN_MASK 宏定義的上面還有數行宏定義:
#define GPIO_PIN_0 ((uint16_t)0x0001)
#define GPIO_PIN_1 ((uint16_t)0x0002)
#define GPIO_PIN_2 ((uint16_t)0x0004)
…//此處省略部分定義
#define GPIO_PIN_14 ((uint16_t)0x4000)
#define GPIO_PIN_15 ((uint16_t)0x8000)
#define GPIO_PIN_All ((uint16_t)0xFFFF)
這些宏定義 GPIO_PIN_0 ~ GPIO_PIN_All 就是 HAL 庫事先定義好的,我們寫代碼的時候初始
化結構體 成員變量 Pin 的時候入口參數可以是這些宏定義標識符。
同理,對于成員變量 Pull,我們用同樣的方法,可以找到其取值范圍定義為:
#define IS_GPIO_PULL(PULL) (((PULL) == GPIO_NOPULL)\\
|| ((PULL) == GPIO_PULLUP) || \\ ((PULL) == GPIO_PULLDOWN))
也就是 PULL 的 取 值 范 圍 只 能 是 標 識 符 GPIO_NOPULL , GPIO_PULLUP 以 及
GPIO_PULLDOWN。
對于其他成員變量 Mode 以及 Alternate,方法都是一樣的,這里基于篇幅考慮我們就不重
復講解。講到這里,我們基本對 HAL_GPIO_Init 的入口參數有比較詳細的了解了。于是我們可
以組織起來下面的代碼:
GPIO_InitTypeDef GPIO_Initure;
GPIO_Initure.Pin=GPIO_PIN_9;
//PA9
GPIO_Initure.Mode=GPIO_MODE_AF_PP; //復用推挽輸出
GPIO_Initure.Pull=GPIO_PULLUP;
//上拉
GPIO_Initure.Speed=GPIO_SPEED_FAST;
//高速
GPIO_Initure.Alternate=GPIO_AF7_USART1; //復用為 USART1
HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化 PA9
接著又有一個問題會被提出來,這個初始化函數一次只能初始化一個 IO 口嗎?我要同時
初始化很多個 IO 口,是不是要復制很多次這樣的初始化代碼呢?
這里又有一個小技巧了。從上面的 GPIO_PIN_X 的宏定義我們可以看出,這些值是 0,1,2,4
這樣的數字,所以每個 IO 口選定都是對應著一個位,16 位的數據一共對應 16 個 IO 口。這個
位為 0 那么這個對應的 IO 口不選定,這個位為 1 對應的 IO 口選定。如果多個 IO 口,他們都
是對應同一個 GPIOx,那么我們可以通過|(或)的方式同時初始化多個 IO 口。這樣操作的前
提是,他們的 Mode,Speed,Pull 和 Alternate 參數值相同,因為這些參數并不能一次定義多種。
所以初始化多個具有相同配置的 IO 口的方式可以是如下:
GPIO_InitTypeDef GPIO_Initure;
GPIO_Initure.Pin=GPIO_PIN_9| GPIO_PIN_10| GPIO_PIN_11; //PA9,PA10,PA11
GPIO_Initure.Mode=GPIO_MODE_AF_PP; //復用推挽輸出
GPIO_Initure.Pull=GPIO_PULLUP;
//上拉
GPIO_Initure.Speed=GPIO_SPEED_FAST;
//高速
GPIO_Initure.Alternate=GPIO_AF7_USART1; //復用為 USART1
HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化 PA9 ,PA10,PA11
對于那些參數可以通過|(或)的方式連接,這既有章可循,同時也靠大家在開發過程中不斷積累。
大家會覺得上面講解有點麻煩,每次要去查找 assert_param()這個函數去尋找,那么有沒有
更好的辦法呢?大家可以打開 GPIO_InitTypeDef 結構體定義:
typedef struct
{
uint32_t Pin; /*!< Specifies the GPIO pins to be configured.
This parameter can be any value of @ref GPIO_pins_define */
uint32_t Mode; /*!< Specifies the operating mode for the selected pins.
This parameter can be a value of @ref GPIO_mode_define */
uint32_t Pull; /*!< Specifies the Pull-up or Pull-Down activation for the selected pins.
This parameter can be a value of @ref GPIO_pull_define */
uint32_t Speed; /*!< Specifies the speed for the selected pins.
This parameter can be a value of @ref GPIO_speed_define */
uint32_t Alternate; /*!< Peripheral to be connected to the selected pins.
This parameter can be a value of @ref GPIO_Alternate_function_selection */
}GPIO_InitTypeDef;
從上圖的結構體成員后面的注釋我們可以看出 Pin 的意思是“Specifies the GPIO pins to be configured.
This parameter can be any value of @ref GPIO_pins_define”。
從這段注釋可以看出 Pin 的取值需要參考注釋 GPIO_pins_define,大家可以在 MDK 中搜索注釋
GPIO_pins_define,就可以找到上面我們提到的 Pin 的取值范圍宏定義。如果要確定詳細的信息
我們就得去查看手冊了。對于去查看手冊的哪個地方,你可以在函數 HAL_GPIO_Init ()的函數
體中搜索 Pin 關鍵字,然后查看庫函數設置 Pin 是設置的哪個寄存器的哪個位,然后去中文參
考手冊查看該寄存器相應位的定義以及前后文的描述。
這一節我們就講解到這里,希望能對大家的開發有幫助。
匯付天下pos機怎樣
匯付天下pos機比較安全。
匯付天下有央行頒橘冊發的支付牌照,所以是正規的機構,旗下的pos收單業務,只要是通過正常渠道購買的pos機,基本就是安全的,但是注意市面上一些代理,有些是二清機,存在一定風險。
匯付天下的業務主要覆蓋兩大板塊,即支付服務和金融科技服務。 支付服務 我們為數百萬小微商戶及垂直行業的公司譽賣提供各種支付服務。我們的解決方案能夠為客戶提供無縫、便捷及安全的支付方式。我們的服務包括 POS 、互聯網支付、移動支付、 移動POS 及跨境支付服務。
擴展資料:
機型慶伍逗分類:
1、手持POS機。體積較小,移動方便,能以單鍵快速操作,不必死記及輸入多位貨號。國內手持POS機品牌有:拉卡拉、微付通、快錢等。
2、臺式POS機。體積較手持POS機大,功能比手持POS機齊全。國內臺式POS機
3、移動手機POS機:按操作方式分類分為手機外置設備刷卡機和手機專用pos機。
onu設備介紹?
朋友雖然不知道160A是華為那款設備,但我是電信搞設備維護的,我們這邊的onu型號是5606t,一般onu上第一層是主控板,二層是業務板或語音板,支持混插,onu的主要作用是管理大樓內所有用戶的adsl和普話,他的上層是olt設備,onu上主要是對adsl帶寬的配置和限速和語音的分離,減少長距離對adsl線路的影響,一般onu上業務板豐富,可以支持pos機和窄帶撥號。
希望對你有幫助。以上就是關于pos機方案開發板,探索者 STM32F407 開發板資料連載第四章 F4 開發基礎知識入門的知識,后面我們會繼續為大家整理關于pos機方案開發板的知識,希望能夠幫助到大家!
