這一章,我們將著重 STM32 開發的一些基礎知識,讓大家對 STM32 開發有一個初步的了解,為後面 STM32 的學習做一個鋪墊,方便後面的學習。這一章的內容大家第一次看的時候可以只了解一個大概,後面需要用到這方面的知識的時候再回過頭來仔細看看。這章我們分 7個小結,
·4.1 MDK 下 C 語言基礎複習
·4.2 STM32F1 系統架構
·4.3 STM32F103 時鐘系統
·4.4 IO 引腳復用器和映射
·4.5 STM32F1 NVIC 中斷優先級管理
·4.6 MDK 中寄存器地址名稱映射分析
·4.7 MDK 固件庫快速開發技巧
4.1 MDK 下 C 語言基礎複習
這一節我們主要講解一下 C 語言基礎知識。C 語言知識博大精深,也不是我們三言兩語能講解清楚,同時我們相信學 STM32F4 這種級別 MCU 的用戶,C 語言基礎應該都是沒問題的。我們這裡主要是簡單的複習一下幾個 C 語言基礎知識點,引導那些 C 語言基礎知識不是很紮實的用戶能夠快速開發 STM32 程序。同時希望這些用戶能夠多去複習一下 C 語言基礎知識,C 語言畢竟是單片機開發中的必備基礎知識。對於 C 語言基礎比較紮實的用戶,這部分知識可以忽略不看。
4.1.1 位操作
C 語言位操作相信學過 C 語言的人都不陌生了,簡而言之,就是對基本類型變量可以在位級別進行操作。這節的內容很多朋友都應該很熟練了,我這裡也就點到為止,不深入探討。下面我們先講解幾種位操作符,然後講解位操作使用技巧。C 語言支持如下 6 種位操作
這些與或非,取反,異或,右移,左移這些到底怎麼回事,這裡我們就不多做詳細,相信大家學 C 語言的時候都學習過了。如果不懂的話,可以百度一下,非常多的知識講解這些操作符。下面我們想著重講解位操作在單片機開發中的一些實用技巧。1) 不改變其他位的值的狀況下,對某幾個位進行設值。
這個場景單片機開發中經常使用,方法就是先對需要設置的位用&操作符進行清零操作,然後用|操作符設值。比如我要改變 GPIOA->ODR 的狀態,可以先對寄存器的值進行&清零操作
操作GPIOA->ODR &=0XFF0F; //將第 4-7 位清 0
然後再與需要設置的值進行|或運算
GPIOA->ODR |=0X0040; //設置相應位的值,不改變其他位的值2) 移位操作提高代碼的可讀性。
移位操作在單片機開發中也非常重要,我們來看看下面一行代碼
GPIOA->ODR| = 1 << 5;
這個操作就是將 ODR 寄存器的第 5 位設置為 1,為什麼要通過左移而不是直接設置一個固定的值呢?其實,這是為了提高代碼的可讀性以及可重用性。這行代碼可以很直觀明了的知道,是將第 5 位設置為 1,其他位的值不變。如果你寫成
GPIOA->ODR =0x0020;
這樣的代碼可讀性非常差同時也不好重用。
3) ~取反操作使用技巧
例如 GPIOA->ODR 寄存器的每一位都用來設置一個 IO 口的輸出狀態,某個時刻我們希望去設置某一位的值為 0,同時其他位都為 1,簡單的作法是直接給寄存器設置一個值:
GPIOA->ODR =0xFFF7;
這樣的作法設置第 3 位為 0,但是這樣的寫法可讀性很差。看看如果我們使用取反操作怎麼實現:
GPIOA->ODR= (uint16_t)~(1<<3);
看這行代碼應該很容易明白,我們設置的是 ODR 寄存器的第 3 位為 0,其他位為 1,可讀性非常強。
4.1.2 define 宏定義
define 是 C 語言中的預處理命令,它用於宏定義,可以提高原始碼的可讀性,為編程提供方便。常見的格式:
#define 標識符 字符串
「標識符」為所定義的宏名。「字符串」可以是常數、表達式、格式串等。例如:#define HSI_VALUE ((uint32_t)16000000)
定義標識符 HSI_VALUE 的值為 16000000。這樣我們就可以在代碼中直接使用標識符HSI_VALUE,而不用直接使用常量 16000000,同時也很方便我們修改 HSI_VALUE 的值。至於 define 宏定義的其他一些知識,比如宏定義帶參數這裡我們就不多講解。
4.1.3# ifdef 和 #if defined 條件編譯
單片機程序開發過程中,經常會遇到一種情況,當滿足某條件時對一組語句進行編譯,而當條件不滿足時則編譯另一組語句。條件編譯命令最常見的形式為:
#ifdef 標識符
程序段 1
#else
程序段 2
#endif
它的作用是:當標識符已經被定義過(一般是用#define 命令定義),則對程序段 1 進行編譯,否則編譯程序段 2。 其中#else 部分也可以沒有,即:
#ifdef
程序段 1
#endif
這個條件編譯在 MDK 裡面是用得很多的,在 stm32f4xx_hal_conf.h 這個頭文件中會看到這樣的語句:
#ifdef HAL_GPIO_MODULE_ENABLED
#include "stm32f1xx_hal_gpio.h"
#endif
這段代碼的作用是判斷宏定義標識符 HAL_GPIO_MODULE_ENABLED 是否被定義,如果被定義了,那麼就引入頭文件 stm32f1xx_hal_gpio.h。
對於條件編譯,還有個常用的格式,如下:
#if defined XXX1
程序段 1
#elif defined XXX2
程序段 2
…
#elif defined XXXn
程序段 n
…
#endif
這種寫法的作用實際跟 ifdef 很相似,不同的是 ifdef 只能在兩個選擇中判斷是否定義,而 if defined 可以在多個選擇中判斷是否定義。
條件編譯也是 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 G_TYPE {
uint32_t Pin;
uint32_t Mode;
uint32_t Speed;
}GPIOA,GPIOB;
在結構體申明的時候可以定義變量,也可以申明之後定義,方法是:
Struct 結構體名字 結構體變量列表 ;
例如:struct G_TYPE GPIOA,GPIOB;
結構體成員變量的引用方法是:
結構體變量名字.成員名
比如要引用 GPIOA 的成員 Mode,方法是:GPIOA. Mode;
結構體指針變量定義也是一樣的,跟其他變量沒有啥區別。
例如:struct G_TYPE *GPIOC;//定義結構體指針變量 GPIOC;
結構體指針成員變量引用方法是通過「->」符號實現,比如要訪問 GPIOC 結構體指針指向的結
構體的成員變量 Speed,方法是:
GPIOC-> Speed;
上面講解了結構體和結構體指針的一些知識,其他的什麼初始化這裡就不多講解了。講到這裡,
有人會問,結構體到底有什麼作用呢?為什麼要使用結構體呢?下面我們將簡單的通過一個實
例回答一下這個問題。
在我們單片機程序開發過程中,經常會遇到要初始化一個外設比如 IO 口。它的初始化狀態
是由幾個屬性來決定的,比如模式,速度等。對於這種情況,在我們沒有學習結構體的時候,
我們一般的方法是:
void HAL_GPIO_Init (uint32_t Pin, uint32_t Mode, uint32_t Speed);
這種方式是有效的同時在一定場合是可取的。但是試想,如果有一天,我們希望往這個函數裡
面再傳入一個參數,那麼勢必我們需要修改這個函數的定義,重新加入上下拉 Pull 這個入口參
數。於是我們的定義被修改為:
void HAL_GPIO_Init (uint32_t Pin, uint32_t Mode, uint32_t Speed,uint32_t Pull);
但是如果我們這個函數的入口參數是隨著開發不斷的增多,那麼是不是我們就要不斷的修改函
數的定義呢?這是不是給我們開發帶來很多的麻煩?那又怎樣解決這種情況呢?
這樣如果我們使用到結構體就能解決這個問題了。我們可以在不改變入口參數的情況下,
只需要改變結構體的成員變量,就可以達到上面改變入口參數的目的。
結構體就是將多個變量組合為一個有機的整體。上面的函數中 Pin, Mode,
Speed 和 Pull 這些參數,他們對於 GPIO 而言,是一個有機整體,都是來設置 IO 口參數的,所
以我們可以將他們通過定義一個結構體來組合在一個。MDK 中是這樣定義的:
typedef struct
{
uint32_t Pin;
uint32_t Mode;
uint32_t Pull;
uint32_t Speed;
}GPIO_InitTypeDef;
於是,我們在初始化 GPIO 口的時候入口參數就可以是 GPIO_InitTypeDef 類型的變量或者指針
變量了,MDK 中是這樣做的:
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init);
這樣,任何時候,我們只需要修改結構體成員變量,往結構體中間加入新的成員變量,而不需
要修改函數定義就可以達到修改入口參數同樣的目的了。這樣的好處是不用修改任何函數定義
就可以達到增加變量的目的。
理解了結構體在這個例子中間的作用嗎?在以後的開發過程中,如果你的變量定義過多,
如果某幾個變量是用來描述某一個對象,你可以考慮將這些變量定義在結構體中,這樣也許可
以提高你的代碼的可讀性。
使用結構體組合參數,可以提高代碼的可讀性,不會覺得變量定義混亂。當然結構體的作
用就遠遠不止這個了,同時,MDK 中用結構體來定義外設也不僅僅只是這個作用,這裡我們只
是舉一個例子,通過最常用的場景,讓大家理解結構體的一個作用而已。後面一節我們還會講
解結構體的一些其他知識。
4.2 STM32F1 系統架構
STM32 的系統架構比 51 單片機就要強大很多了。STM32 系統架構的知識可以在《STM32
中文參考手冊 V10》的 P25~28 有講解,這裡我們也把這一部分知識抽取出來講解,是為了大
家在學習 STM32 之前對系統架構有一個初步的了解。這裡的內容基本也是從中文參考手冊中
參考過來的,讓大家能通過我們手冊也了解到,免除了到處找資料的麻煩吧。如果需要詳細深
入的了解 STM32 的系統架構,還需要在網上搜索其他資料學習學習。
我們這裡所講的 STM32 系統架構主要針對的 STM32F103 這些非互聯型晶片。首先我們看
看 STM32 的系統架構圖:
STM32 主系統主要由四個驅動單元和四個被動單元構成。
四個驅動單元是:
內核 DCode 總線;
系統總線;
通用 DMA1;
通用 DMA2;
四被動單元是:
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 的時候只需要一個大概的了解,大致知道是個
什麼情況即可。對於尋址之類的知識,這裡就不做深入的講解,中文參考手冊都有很詳細的講
解。
4.3 STM32F103 時鐘系統
STM32F1 時鐘系統的知識在《STM32 中文參考手冊 V10》第六章復位和時鐘控制章節有非
常詳細的講解,網上關於時鐘系統的講解也基本都是參考的這裡。這些知識也不是什麼原創,
純粹根據官方提供的中文參考手冊和自己的應用心得來總結的,如有不合理之處望大家諒解。
這部分內容我們分 3 個小節來講解:
·4.3.1 STM32F103 時鐘樹概述
·4.3.2 STM32F103 時鐘初始化配置
·4.3.3 STM32F103 時鐘使能和配置
4.3.1 STM32F103 時鐘樹概述
眾所周知,時鐘系統是 CPU 的脈搏,就像人的心跳一樣。所以時鐘系統的重要性就不言而
喻了。STM32F103的時鐘系統比較複雜,不像簡單的51單片機一個系統時鐘就可以解決一切。
於是有人要問,採用一個系統時鐘不是很簡單嗎?為什麼 STM32 要有多個時鐘源呢? 因為首
先 STM32 本身非常複雜,外設非常的多,但是並不是所有外設都需要系統時鐘這麼高的頻率,
比如看門狗以及 RTC 只需要幾十 k 的時鐘即可。同一個電路,時鐘越快功耗越大,同時抗電磁
幹擾能力也會越弱,所以對於較為複雜的 MCU 一般都是採取多時鐘源的方法來解決這些問題。
首先讓我們來看看 STM32F103 的時鐘系統圖:
在 STM32 中,有五個時鐘源,為 HSI、HSE、LSI、LSE、PLL。從時鐘頻率來分可以分為
高速時鐘源和低速時鐘源,在這 5 個中 HIS,HSE 以及 PLL 是高速時鐘,LSI 和 LSE 是低速時
鍾。從來源可分為外部時鐘源和內部時鐘源,外部時鐘源就是從外部通過接晶振的方式獲取時
鍾源,其中 HSE 和 LSE 是外部時鐘源,其他的是內部時鐘源。下面我們看看 STM32 的 5 個時
鍾源,我們講解順序是按圖中紅圈標示的順序:
①、HSI 是高速內部時鐘,RC 振蕩器,頻率為 8MHz。
②、HSE 是高速外部時鐘,可接石英/陶瓷諧振器,或者接外部時鐘源,頻率範圍為 4MHz~16MHz。
我們的開發板接的是 8M 的晶振。
③、LSI 是低速內部時鐘,RC 振蕩器,頻率為 40kHz。獨立看門狗的時鐘源只能是 LSI,同
時 LSI 還可以作為 RTC 的時鐘源。
④、LSE 是低速外部時鐘,接頻率為 32.768kHz 的石英晶體。這個主要是 RTC 的時鐘源。
⑤、PLL 為鎖相環倍頻輸出,其時鐘輸入源可選擇為 HSI/2、HSE 或者 HSE/2。倍頻可選擇為
2~16 倍,但是其輸出頻率最大不得超過 72MHz。
上面我們簡要概括了 STM32 的時鐘源,那麼這 5 個時鐘源是怎麼給各個外設以及系統提
供時鐘的呢?這裡我們將一一講解。我們還是從圖的下方講解起吧,因為下方比較簡單。
圖中我們用 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 口等。居寧
老師的《稀裡糊塗玩 STM32》資料裡面教大家的記憶方法是 2>1, APB2 下面所掛的外設的時
鍾要比 APB1 的高。
在以上的時鐘輸出中,有很多是帶使能控制的,例如 AHB 總線時鐘、內核時鐘、各種 APB1
外設、APB2 外設等等。當需要使用某模塊時,記得一定要先使能對應的時鐘。後面我們講解
實例的時候回講解到時鐘使能的方法。
4.3.2 STM32F103 時鐘系統配置
上一小節我們對 STM32F103 時鐘樹進行了詳細講解,接下來我們來講解通過 STM32F1 的
HAL 庫進行 STM32F103 時鐘系統配置步驟。實際上,STM32F1 的時鐘系統配置也可以通過圖
形化配置工具 STM32CubeMX 來配置生成,這裡我們講解初始化代碼,是為了讓大家對 STM32
時鐘系統有更加清晰的理解。
前面我們講解過,在系統啟動之後,程序會先執行 HAL 庫定義的 SystemInit 函數,進行系
統一些初始化配置。那麼我們先來看看 SystemInit 程序:
void SystemInit (void)
{
/* 將 RCC 時鐘配置重置為默認重置狀態(用於調試)*/
RCC->CR |= (uint32_t)0x00000001; //打開 HSION 位
/* 設置 SW, HPRE, PPRE1, PPRE2, ADCPRE 和 MCO 位 */
#if !defined(STM32F105xC) && !defined(STM32F107xC)
RCC->CFGR &= (uint32_t)0xF8FF0000;
#else
RCC->CFGR &= (uint32_t)0xF0FF0000;
#endif /* STM32F105xC */
RCC->CR &= (uint32_t)0xFEF6FFFF; // 復位 HSEON, CSSON 和 PLLON 位
RCC->CR &= (uint32_t)0xFFFBFFFF; // 復位 HSEBYP 位
RCC->CFGR &= (uint32_t)0xFF80FFFF; //復位 CFGR 寄存器
#if defined(STM32F105xC) || defined(STM32F107xC)
RCC->CR &= (uint32_t)0xEBFFFFFF; // 復位 PLL2ON 和 PLL3ON 位
RCC->CIR = 0x00FF0000; // 禁用所有中斷並清除掛起位
RCC->CFGR2 = 0x00000000; // 重置 CFGR2 註冊
#elif defined(STM32F100xB) || defined(STM32F100xE)
RCC->CIR = 0x009F0000; // 禁用所有中斷並清除掛起位
RCC->CFGR2 = 0x00000000; // 重置 CFGR2 註冊
#else
RCC->CIR = 0x009F0000; // 禁用所有中斷並清除掛起位
#endif /* STM32F105xC */
#if defined(STM32F100xE) || defined(STM32F101xE) || defined(STM32F101xG) ||
defined(STM32F103xE) || defined(STM32F103xG)
#ifdef DATA_IN_ExtSRAM
SystemInit_ExtMemCtl();
#endif /* DATA_IN_ExtSRAM */
#endif
/* 配置中斷向量表地址=基地址+偏移地址 ------------------*
#ifdef VECT_TAB_SRAM
SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; //內部 SRAM 中的向量表重定位
#else
SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; //在內部 FLASH 中的向量表重定位
#endif
}從上面代碼可以看出,SystemInit 主要做了如下三個方面工作:
1) 復位 RCC 時鐘配置為默認復位值(默認開始了 HIS)
2) 外部存儲器配置
3) 中斷向量表地址配置
HAL 庫的 SystemInit 函數並沒有像標準庫的 SystemInit 函數一樣進行時鐘的初始化配置。HAL
庫的 SystemInit 函數除了打開 HSI 之外,沒有任何時鐘相關配置,所以使用 HAL 庫我們必須編
寫自己的時鐘配置函數。首先我們打開工程模板看看我們在工程 SYSTEM 分組下面定義的 sys.c
文件中的時鐘初始化函數 Stm32_Clock_Init 的內容:
//時鐘系統配置函數
//PLL:選擇的倍頻數,RCC_PLL_MUL2~RCC_PLL_MUL16
//返回值:0,成功;1,失敗
void Stm32_Clock_Init(u32 PLL)
{
HAL_StatusTypeDef ret = HAL_OK;
RCC_OscInitTypeDef RCC_OscInitStructure;
RCC_ClkInitTypeDef RCC_ClkInitStructure;
RCC_OscInitStructure.OscillatorType=RCC_OSCILLATORTYPE_HSE; //時鐘源為 HSE
RCC_OscInitStructure.HSEState=RCC_HSE_ON; //打開 HSE
RCC_OscInitStructure.HSEPredivValue=RCC_HSE_PREDIV_DIV1; //HSE 預分頻
RCC_OscInitStructure.PLL.PLLState=RCC_PLL_ON; //打開 PLL
RCC_OscInitStructure.PLL.PLLSource=RCC_PLLSOURCE_HSE;
//PLL 時鐘源選擇 HSE
RCC_OscInitStructure.PLL.PLLMUL=PLL; //主 PLL 倍頻因子
ret=HAL_RCC_OscConfig(&RCC_OscInitStructure);//初始化
if(ret!=HAL_OK) while(1);
//選中 PLL 作為系統時鐘源並且配置 HCLK,PCLK1 和 PCLK2
RCC_ClkInitStructure.ClockType=(RCC_CLOCKTYPE_SYSCLK|
RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_PCLK1|
RCC_CLOCKTYPE_PCLK2);
//設置系統時鐘時鐘源為 PLL
RCC_ClkInitStructure.SYSCLKSource=RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStructure.AHBCLKDivider=RCC_SYSCLK_DIV1; //AHB 分頻係數為 1
RCC_ClkInitStructure.APB1CLKDivider=RCC_HCLK_DIV2; //APB1 分頻係數為 2
RCC_ClkInitStructure.APB2CLKDivider=RCC_HCLK_DIV1; //APB2 分頻係數為 1
ret=HAL_RCC_ClockConfig(&RCC_ClkInitStructure,FLASH_LATENCY_2);
//同時設置 FLASH 延時周期為 2WS,也就是 3 個 CPU 周期。
if(ret!=HAL_OK) while(1);
}
從函數注釋可知,函數 Stm32_Clock_Init 的作用是進行時鐘系統配置,除了配置 PLL 相關
參數確定 SYSCLK 值之外,還配置了 AHB,APB1 和 APB2 的分頻係數,也就是確定了 HCLK,
PCLK1 和 PCLK2 的時鐘值。
接下來我們看看結構體 RCC_OscInitTypeDef 的定義:
typedef struct
{
uint32_t OscillatorType; //需要選擇配置的振蕩器類型
uint32_t HSEState; //HSE 狀態
uint32_t HSEPredivValue; // Prediv1 值
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 PLLMUL; //PLL VCO 輸入時鐘的乘法因子
}RCC_PLLInitTypeDef;
從 RCC_PLLInitTypeDef;結構體的定義很容易看出該結構體主要用來設置 PLL 時鐘源以及
相關分頻倍頻參數。
這個結構體的定義我們就不做過多講解,接下來我們看看我們的時鐘初始化函數
Stm32_Clock_Init 中的配置內容:
RCC_OscInitStructure.OscillatorType=RCC_OSCILLATORTYPE_HSE; //時鐘源為 HSE
RCC_OscInitStructure.HSEState=RCC_HSE_ON; //打開 HSE
RCC_OscInitStructure.HSEPredivValue=RCC_HSE_PREDIV_DIV1; //HSE 預分頻
RCC_OscInitStructure.PLL.PLLState=RCC_PLL_ON; //打開 PLL
RCC_OscInitStructure.PLL.PLLSource=RCC_PLLSOURCE_HSE;
//PLL 時鐘源選擇 HSE
RCC_OscInitStructure.PLL.PLLMUL=PLL; //主 PLL 倍頻因子
ret=HAL_RCC_OscConfig(&RCC_OscInitStructure);//初始化
通過該段函數,我們開啟了 HSE 時鐘源,同時選擇 PLL 時鐘源為 HSE,然後把
Stm32_Clock_Init 的唯一的入口參數直接設置作為 PLL 的倍頻因子。設置好 PLL 時鐘源參數之
後,也就是確定了 PLL 的時鐘頻率,接下來我們就需要設置系統時鐘,以及 AHB,APB1 和
APB2 相關參數。
接下來我們來看看步驟 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 延遲,這個參數我們放在後面講解。
RCC_ClkInitTypeDef 結構體類型定義非常簡單,這裡我們就不列出來,我們來看看
Stm32_Clock_Init 函數中的配置內容:
//選中 PLL 作為系統時鐘源並且配置 HCLK,PCLK1 和 PCLK2
RCC_ClkInitStructure.ClockType=(RCC_CLOCKTYPE_SYSCLK|
RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_PCLK1|
RCC_CLOCKTYPE_PCLK2);
//設置系統時鐘時鐘源為 PLL
RCC_ClkInitStructure.SYSCLKSource=RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStructure.AHBCLKDivider=RCC_SYSCLK_DIV1; //AHB 分頻係數為 1
RCC_ClkInitStructure.APB1CLKDivider=RCC_HCLK_DIV2; //APB1 分頻係數為 2
RCC_ClkInitStructure.APB2CLKDivider=RCC_HCLK_DIV1; //APB2 分頻係數為 1
ret=HAL_RCC_ClockConfig(&RCC_ClkInitStructure,FLASH_LATENCY_2);
//同時設置 FLASH 延時周期為 2WS,也就是 3 個 CPU 周期。
第一個參數 ClockType 配置說明我們要配置的是 SYSCLK,HCLK,PCLK1 和 PCLK2 四個時鐘。
第二個參數 SYSCLKSource 配置選擇系統時鐘源為 PLL。
第三個參數 AHBCLKDivider 配置 AHB 分頻係數為 1。
第四個參數 APB1CLKDivider 配置 APB1 分頻係數為 2。
第五個參數 APB2CLKDivider 配置 APB2 分頻係數為 1。
根據我們在主函數中調用 Stm32_Clock_Init(RCC_PLL_MUL9)時候設置的入口參數值,我
們可以計算出,PLL 時鐘為 PLLCLK=HSE*9 =8MHz*9=72MHz,同時我們選擇系統時鐘源為
PLL , 所 以 系 統 時 鍾 SYSCLK=72MHz 。 AHB 分頻系 數 為 1 ,故其頻率為
HCLK=SYSCLK/1=72MHz。APB1 分頻係數為 2,故其頻率為 PCLK1=HCLK/2=36MHz。APB2
分頻係數為 1,故其頻率為 PCLK2=HCLK/1=72/1=72MHz。最後我們總結一下通過調用函數
Stm32_Clock_Init(RCC_PLL_MUL9)之後的關鍵時鐘頻率值:
SYSCLK(系統時鐘) =72MHz
PLL 主時鐘 =72MHz
AHB 總線時鐘(HCLK=SYSCLK/1) =72MHz
APB1 總線時鐘(PCLK1=HCLK/2) =36MHz
APB2 總線時鐘(PCLK2=HCLK/1) =72MHz
4.3.3 STM32F1 時鐘使能和配置
上一節我們講解了時鐘系統配置步驟。在配置好時鐘系統之後,如果我們要使用某些外設,
例如 GPIO,ADC 等,我們還要使能這些外設時鐘。這裡大家必須注意,如果在使用外設之前
沒有使能外設時鐘,這個外設是不可能正常運行的。STM32 的外設時鐘使能是在 RCC 相關寄
存器中配置的。因為 RCC 相關寄存器非常多,有興趣的同學可以直接打開《STM32 中文參考手
冊 V10》6.3 小節查看所有 RCC 相關寄存器的配置。接下來我們來講解通過 STM32F1 的 HAL
庫使能外設時鐘的方法。
在 STM32F1 的 HAL 庫中,外設時鐘使能操作都是在 RCC 相關固件庫文件頭文件
stm32f1xx_hal_rcc.h 定義的。大家打開 stm32f1xx_hal_rcc.h 頭文件可以看到文件中除了少數幾
個函數聲明之外大部分都是宏定義標識符。外設時鐘使能在 HAL 庫中都是通過宏定義標識符
來實現的。首先,我們來看看 GPIOA 的外設時鐘使能宏定義標識符:
#define __HAL_RCC_GPIOA_CLK_ENABLE() do { \
__IO uint32_t tmpreg; \
SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN);\
tmpreg = READ_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN);\
UNUSED(tmpreg); \
} while(0U))
這幾行代碼非常簡單,主要是定義了一個宏定義標識符__HAL_RCC_GPIOA_CLK_ENABLE(),
它的核心操作是通過下面這行代碼實現的:
SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN);
這行代碼的作用是,設置寄存器 RCC_APB2ENR 的相關位為 1,至於是哪個位,是由宏定義標
識符 RCC_APB2ENR_IOPAEN 的值決定的,而它的值為:
#define RCC_APB2ENR_IOPAEN ((uint32_t)0x00000001)
所以,我們很容易理解上面代碼的作用是設置寄存器 RCC->APB2ENR 寄存器的位 2 為 1。我
們可以從 STM32F1 的中文參考手冊中搜索 APB2ENR 寄存器定義,位 2 的作用是用來使用
GPIOA 時鐘。APB2ENR 寄存器的位 2 描述如下:
位 2 IOPAEN:IO 埠 A 時鐘使能
由軟體置 1 和清零
0:禁止 IO 埠 A 時鐘
1:使能 IO 埠 A 時鐘
那麼我們只需要在我們的用戶程序中調用宏定義標識符__HAL_RCC_GPIOA_CLK_ENABLE()
就可以實現 GPIOA 時鐘使能。使用方法為:
__HAL_RCC_GPIOA_CLK_ENABLE();//使能 GPIOA 時鐘
對於其他外設,同樣都是在 stm32f1xx_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->APB2ENR &= ~(RCC_APB2ENR_IOPAEN))
同樣,宏定義標識符__HAL_RCC_GPIOA_CLK_DISABLE()的作用是設置 RCC->APB2ENR 寄
存器的位 2 為 0,也就是禁止 GPIOA 時鐘。具體使用方法我們這裡就不做過多講解,我們這裡
同樣列出幾個常用的禁止外設時鐘的宏定義標識符使用方法:
__HAL_RCC_DMA1_CLK_DISABLE();//禁止 DMA1 時鐘
__HAL_RCC_USART2_CLK_DISABLE();//禁止串口 2 時鐘
__HAL_RCC_TIM1_CLK_DISABLE();//禁止 TIM1 時鐘
關於 STM32F1 的外設時鐘使能和禁止方法我們就給大家講解到這裡。
4.4 埠復用和重映射
STM32F1 有很多的內置外設,這些外設的外部引腳都是與 GPIO 復用的。也就是說,一個 GPIO
如果可以復用為內置外設的功能引腳,那麼當這個 GPIO 作為內置外設使用的時候,就叫做復用。
這部分知識在《STM32 中文參考手冊 V10》的 P109,P116~P121 有詳細的講解哪些 GPIO 管腳是
可以復用為哪些內置外設的。這裡我們就不一一講解。
大家都知道,MCU 都有串口,STM32 有好幾個串口。比如說 STM32F103ZET6 有 5 個串口,我
們可以查手冊知道,串口 1 的引腳對應的 IO 為 PA9,PA10.PA9,PA10 默認功能是 GPIO,所以當
PA9,PA10 引腳作為串口 1 的 TX,RX 引腳使用的時候,那就是埠復用。
接下來我們以串口 1 為例來講解配置 GPOPA.9,GPIOA.10 口為串口 1 復用功能的一般步驟。
① 首先,我們要使用 IO 復用功能,必須先打開對應的 IO 時鐘和復用功能外設時鐘,這裡
我們使用了 GPIOA 以及 USART1,所以我們需要使能 GPIOA 和 USART1 時鐘。方法如下:
__HAL_RCC_GPIOA_CLK_ENABLE(); //使能 GPIOA 時鐘
__HAL_RCC_USART1_CLK_ENABLE(); //使能 USART1 時鐘
__HAL_RCC_AFIO_CLK_ENABLE(); //使能輔助功能 IO 時鐘
② 然後,我們在 GIPOx_MODER 寄存器中將所需 IO(對於串口 1 是 PA9,PA10)配置為復
用功能。
③ 最後,我們還需要對 IO 口的其他參數,例如上拉/下拉以及輸出速度等進行配置。
上面三步,在我們 HAL 庫中是通過 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_FREQ_HIGH;//高速
HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化 PA9
通過上面的配置,PA9 復用為串口 1 的發送引腳。這個時候,PA9 將不再作為普通的 IO 口
使用。對於 PA10,配置方法一樣,修改 Pin 成員變量值為 PIN_10 即可。
STM32F1 的埠復用和映射就給大家講解到這裡,希望大家課餘結合相關實驗工程和手冊
鞏固本小節知識。
4.5 STM32 NVIC 中斷優先級管理
CM3 內核支持 256 個中斷,其中包含了 16 個內核中斷和 240 個外部中斷,並且具有 256
級的可編程中斷設置。但 STM32 並沒有使用 CM3 內核的全部東西,而是只用了它的一部分。
STM32 有 84 個中斷,包括 16 個內核中斷和 68 個可屏蔽中斷,具有 16 級可編程的中斷優先級。
而我們常用的就是這 68 個可屏蔽中斷,但是 STM32 的 68 個可屏蔽中斷,在 STM32F103 系列
上面,又只有 60 個(在 107 系列才有 68 個)。因為我們開發板選擇的晶片是 STM32F103 系列
的所以我們就只針對 STM32F103 系列這 60 個可屏蔽中斷進行介紹。
在 MDK 內,與 NVIC 相關的寄存器,MDK 為其定義了如下的結構體:
typedef struct
{
__IOM uint32_t ISER[8U];
uint32_t RESERVED0[24U];
__IOM uint32_t ICER[8U];
uint32_t RSERVED1[24U];
__IOM uint32_t ISPR[8U];
uint32_t RESERVED2[24U];
__IOM uint32_t ICPR[8U];
uint32_t RESERVED3[24U];
__IOM uint32_t IABR[8U];
uint32_t RESERVED4[56U];
__IOM uint8_t IP[240U];
uint32_t RESERVED5[644U];
__OM uint32_t STIR;
} NVIC_Type;;
STM32 的中斷在這些寄存器的控制下有序的執行的。只有了解這些中斷寄存器,才能方便
的使用 STM32 的中斷。下面重點介紹這幾個寄存器:
ISER[8]:ISER 全稱是:Interrupt Set-Enable Registers,這是一個中斷使能寄存器組。上面
說了 CM3 內核支持 256 個中斷,這裡用 8 個 32 位寄存器來控制,每個位控制一個中斷。但是
STM32F103 的可屏蔽中斷只有 60 個,所以對我們來說,有用的就是兩個(ISER[0]和 ISER[1]),
總共可以表示 64 個中斷。而 STM32F103 只用了其中的前 60 位。ISER[0]的 bit0~bit31 分別對
應中斷 0~31。ISER[1]的 bit0~27 對應中斷 32~59;這樣總共 60 個中斷就分別對應上了。你要
使能某個中斷,必須設置相應的 ISER 位為 1,使該中斷被使能(這裡僅僅是使能,還要配合中
斷分組、屏蔽、IO 口映射等設置才算是一個完整的中斷設置)。具體每一位對應哪個中斷,請
參考 stm32f10x.h 裡面的第 140 行處(針對編譯器 MDK5 來說)。
ICER[8]:全稱是:Interrupt Clear-Enable Registers,是一個中斷除能寄存器組。該寄存器組
與 ISER 的作用恰好相反,是用來清除某個中斷的使能的。其對應位的功能,也和 ICER 一樣。
這裡要專門設置一個 ICER 來清除中斷位,而不是向 ISER 寫 0 來清除,是因為 NVIC 的這些寄
存器都是寫 1 有效的,寫 0 是無效的。具體為什麼這麼設計,請看《CM3 權威指南》第 125 頁,
NVIC 概覽一章。
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,是一個中斷優先級控制的寄存器組。這個寄
存器組相當重要!STM32 的中斷分組與這個寄存器組密切相關。IP 寄存器組由 240 個 8bit 的寄
存器組成,每個可屏蔽中斷佔用 8bit,這樣總共可以表示 240 個可屏蔽中斷。而 STM32 只用到
了其中的前 60 個。IP[59]~IP[0]分別對應中斷 59~0。而每個可屏蔽中斷佔用的 8bit 並沒有全部
使用,而是 只用了高 4 位。這 4 位,又分為搶佔優先級和子優先級。搶佔優先級在前,子優先
級在後。而這兩個優先級各佔幾個位又要根據 SCB->AIRCR 中的中斷分組設置來決定。
這裡簡單介紹一下 STM32 的中斷分組:STM32 將中斷分為 5 個組,組 0~4。該分組的設
置是由 SCB->AIRCR 寄存器的 bit10~8 來定義的。具體的分配關係如表 4.5.1 所示:
通過這個表,我們就可以清楚的看到組 0~4 對應的配置關係,例如組設置為 3,那麼此時
所有的 60 個中斷,每個中斷的中斷優先寄存器的高四位中的最高 3 位是搶佔優先級,低 1 位是
響應優先級。每個中斷,你可以設置搶佔優先級為 0~7,響應優先級為 1 或 0。搶佔優先級的
級別高於響應優先級。而數值越小所代表的優先級就越高。
這裡需要注意兩點:第一,如果兩個中斷的搶佔優先級和響應優先級都是一樣的話,則看
哪個中斷先發生就先執行;第二,高優先級的搶佔優先級是可以打斷正在進行的低搶佔優先級
中斷的。而搶佔優先級相同的中斷,高優先級的響應優先級不可以打斷低響應優先級的中斷。
結合實例說明一下:假定設置中斷優先級組為 2,然後設置中斷 3(RTC 中斷)的搶佔優先級
為 2,響應優先級為 1。中斷 6(外部中斷 0)的搶佔優先級為 3,響應優先級為 0。中斷 7(外
部中斷 1)的搶佔優先級為 2,響應優先級為 0。那麼這 3 個中斷的優先級順序為:中斷 7>中
斷 3>中斷 6。
上面例子中的中斷 3 和中斷 7 都可以打斷中斷 6 的中斷。而中斷 7 和中斷 3 卻不可以相互
打斷!
通過以上介紹,我們熟悉了 STM32F103 中斷設置的大致過程。接下來我們介紹如何使用
HAL 庫實現以上中斷分組設置以及中斷優先級管理,使中斷配置簡單化。NVIC 中斷管理相關
函數主要在 HAL 庫關鍵文件 stm32f1xx_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_cm3.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<< SCB_AIRCR_PRIGROUP_Pos) );
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 函數進行分組設置的。
打開 stm32f1xx_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 庫文件 stm32f1xx_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 庫,在文件 stm32f1xx_hal.c
內部定義函數 HAL_Init 中有調用 HAL_NVIC_PriorityGroupConfig 函數進行相關設置,所以我
們只需要修改 HAL_Init 內部對中斷優先級分組設置即可。
② 設置單個中斷的中斷優先級別和使能相應中斷通道,使用到的函數函數主要為函數
HAL_NVIC_SetPriority 和函數 HAL_NVIC_EnableIRQ。
4.6 HAL 庫中寄存器地址名稱映射分析
之所以要講解這部分知識,是因為經常會遇到客戶提到不明白 HAL 庫中那些結構體是怎麼
與寄存器地址對應起來的。這裡我們就做一個簡要的分析吧。
首先我們看看 51 中是怎麼做的。51 單片機開發中經常會引用一個 reg51.h 的頭文件,下
面我們看看他是怎麼把名字和寄存器聯繫起來的:
sfr P0 =0x80;
sfr 也是一種擴充數據類型,點用一個內存單元,值域為 0~255。利用它可以訪問 51 單片
機內部的所有特殊功能寄存器。如用 sfr P1 = 0x90 這一句定義 P1 為 P1 埠在片內的寄存
器。然後我們往地址為 0x80 的寄存器設值的方法是:P0=value;
那麼在 STM32 中,是否也可以這樣做呢??答案是肯定的。肯定也可以通過同樣的方
式來做,但是 STM32 因為寄存器太多太多,如果一一以這樣的方式列出來,那要好大的篇
幅,既不方便開發,也顯得太雜亂無序的感覺。所以 MDK 採用的方式是通過結構體來將
寄存器組織在一起。下面我們就講解 MDK 是怎麼把結構體和地址對應起來的,為什麼我
們修改結構體成員變量的值就可以達到操作對應寄存器的值。這些事情都是在 stm32f1xx.h
文件中完成的。我們通過 GPIOA 的幾個寄存器的地址來講解吧。
首先我們可以查看《STM32 中文參考手冊 V10》中的寄存器地址映射表(P129)。這裡
我們選用 GPIOA 為例來講解。GPIO 寄存器地址映射如下表 4.6.1:
從這個表我們可以看出,GPIOA 的 7 個寄存器都是 32 位的,所以每個寄存器佔有 4
個地址,一共佔用 28 個地址,地址偏移範圍為(000h~01Bh)。這個地址偏移是相對 GPIOA
的基地址而言的。GPIOA 的基地址是怎麼算出來的呢?因為 GPIO 都是掛載在 APB2 總線
之上,所以它的基地址是由 APB2 總線的基地址+GPIOA 在 APB2 總線上的偏移地址決定
的。同理依次類推,我們便可以算出 GPIOA 基地址了。下面我們打開 stm32f103.h 定位到
GPIO_TypeDef 定義處:
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;
然後定位到:
#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 (APB2PERIPH_BASE + 0x0800)
依次類推,可以找到最頂層:
#define APB2PERIPH_BASE (PERIPH_BASE + 0x10000)
#define PERIPH_BASE ((uint32_t)0x40000000)
所以我們便可以算出 GPIOA 的基地址位:
GPIOA_BASE= 0x40000000+0x10000+0x0800=0x40010800
下面我們再跟《STM32 中文參考手冊 V10》比較一下看看 GPIOA 的基地址是不是
0x40010800。截圖 P28 存儲器映射表我們可以看到,GPIOA 的起始地址也就是基
地址確實是 0x40010800:
同樣的道理,我們可以推算出其他外設的基地址。
上面我們已經知道 GPIOA 的基地址,那麼那些 GPIOA 的 7 個寄存器的地址又是怎麼
算出來的呢??在上面我們講過 GPIOA 的各個寄存器對於 GPIOA 基地址的偏移地址,所
以我們自然可以算出來每個寄存器的地址。
GPIOA 的寄存器的地址=GPIOA 基地址+寄存器相對 GPIOA 基地址的偏移值
這個偏移值在上面的寄存器地址映像表中可以查到。
那麼在結構體裡面這些寄存器又是怎麼與地址一一對應的呢?這裡就涉及到結構體的
一個特徵,那就是結構體存儲的成員他們的地址是連續的。上面講到 GPIOA 是指向
GPIO_TypeDef 類型的指針,又由於 GPIO_TypeDef 是結構體,所以自然而然我們就可以算
出 GPIOA 指向的結構體成員變量對應地址了。
我們可以把 GPIO_TypeDef 的定義中的成員變量的順序和 GPIOx 寄存器地址映像對比
可以發現,他們的順序是一致的,如果不一致,就會導致地址混亂了。
這就是為什麼固件庫裡面:GPIOA->BRR=value;就是設置地址為 0x40010800
+0x014(BRR 偏移量)=0x40010814 的寄存器 BRR 的值了。它和 51 裡面 P0=value 是設置地
址為 0x80 的 P0 寄存器的值是一樣的道理。
看到這裡你是否會學起來踏實一點呢??STM32 使用的方式雖然跟 51 單片機不一樣,
但是原理都是一致的。
4.7 MDK 中使用 HAL 庫快速組織代碼技巧
這一節主要講解在 MDK 中使用 HAL 庫開發的一些小技巧,僅供初學者參考。這節的知識
大家可以在學習第一個跑馬燈實驗的時候參考一下,對初學者應該很有幫助。我們就用最簡單
的 GPIO 初始化函數為例。
現 在 我 們 要 初 始 化 某 個 GPIO 端 口 , 我 們 要 怎 樣 快 速 操 作 呢 ? 在 頭 文 件
stm32f1xx_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…」,
如下圖 4.7.1:
於是定位到 stm32f1xx_hal_gpio.h 中 GPIO_InitTypeDef 的定義處:
typedef struct
{
uint32_t Pin;
uint32_t Mode;
uint32_t Pull;
uint32_t Speed;
}GPIO_InitTypeDef;
可以看到這個結構體有 4 個成員變量,這也告訴我們一個信息,一個 GPIO 口的狀態是由模式
(Mode),速度(Speed)以及上下拉(Pull)來決定的。我們首先要定義一個結構體變量,下面
我們定義:
GPIO_InitTypeDef GPIO_InitStructure;
接著我們要初始化結構體變量 GPIO_InitStructure。首先我們要初始化成員變量 Pin,這個時候我
們就有點迷糊了,這個變量到底可以設置哪些值呢?這些值的範圍有什麼規定嗎?
這裡我們就回到 HAL_GPIO_Init 聲明處,同樣雙擊 HAL_GPIO_Init,右鍵點擊「Go to
definition of …」,這樣光標定位到 stm32f1xx_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 是斷言語句,是對函數入口參數的有效性進行判斷,所以我們可以從
這個函數入手,確定入口參數範圍。第一行是對第一個參數 GPIOx 進行有效性判斷,雙擊
「IS_GPIO_ALL_INSTANCE」右鍵點擊「go to defition of…」 定位到了下面的定義:
#define IS_GPIO_ALL_INSTANCE(INSTANCE) (((INSTANCE) == GPIOA) || \
((INSTANCE) == GPIOB) || \
((INSTANCE) == GPIOC) || \
((INSTANCE) == GPIOD) || \
((INSTANCE) == GPIOE) || \
((INSTANCE) == GPIOF) || \
((INSTANCE) == GPIOG))
很明顯可以看出,GPIOx 的取值規定只允許是 GPIOA~GPIOG。
同樣的辦法,我們雙擊「IS_GPIO_PIN」 右鍵點擊「go to defition of…」,定位到下面的定義:
#define IS_GPIO_PIN(PIN) (((((uint32_t)PIN) & GPIO_PIN_MASK ) != 0x00u)
&& ((((uint32_t)PIN) & ~GPIO_PIN_MASK) == 0x00u))
同時,宏定義標識符 GPIO_PIN_MASK 的定義為:
#define GPIO_PIN_MASK 0x0000FFFFu
從上面可以看出,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,方法都是一樣的,這裡基於篇幅考慮我們就不重複講解。講到這裡,
我們基本對 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_FREQ_HIGH;//高速
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 參數值相同,因為這些參數並不能一次定義多種。所以初始
化多個具有相同配置的 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_FREQ_HIGH;//高速
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 */
}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 是設置的哪個寄存器的哪個位,然後去中文參
考手冊查看該寄存器相應位的定義以及前後文的描述。
這一節我們就講解到這裡,希望能對大家的開發有幫助。