(涉及專有名詞較多,難免解釋不到位,若有錯誤還請指出,謝謝!)
硬件連接圖如下:


一、掃描
思路是在main函數中通過死循環來掃描端口電平狀態檢測,以此判斷按鍵是否按下。實現較為簡單。
1.初始化(注意C語言中變量聲明需放在函數開頭)
以下是初始化PB5端口(LED燈)的代碼,每一條語句的含義在我另一篇博客里
GPIO_InitTypeDef GPIO_Init1;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_Init1.GPIO_Pin = GPIO_Pin_5;
GPIO_Init1.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init1.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_SetBits(GPIOB, GPIO_Pin_5); //先熄燈
GPIO_Init(GPIOB, &GPIO_Init1);
以下是初始化PE3端口(按鍵)的代碼
GPIO_InitTypeDef GPIO_Init2;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE, ENABLE);
GPIO_Init2.GPIO_Pin = GPIO_Pin_3; // 設置GPIO端口號為5
GPIO_Init2.GPIO_Mode = GPIO_Mode_IPU; // 設置端口模式為輸入上拉
// 設置為輸入端口時不需要指定GPIO_Speed參數
GPIO_Init(GPIOE, &GPIO_Init2);
輸入上拉與輸入下拉的區別:
輸入上拉(GPIO_Mode_IPU):端口與VCC通過一個電阻串連,因此沒有輸入或輸入高電平時端口為高電平,輸入低電平時端口為低電平
輸入下拉(GPIO_Mode_IPD):端口與GND通過一個電阻串連,因此沒有輸入或輸入低電平時端口為低電平,輸入高電平時端口為高電平
從硬件圖上得知按鍵與GND相連,如果端口設置為輸入上拉,那么松開按鍵時端口為高電平,按下按鍵時端口為低電平,可以區分兩種狀態
如果端口設置為輸入下拉,那么無論是按下還是松開按鍵時端口總為低電平,無法區分兩種狀態
類似地,如果按鍵與VCC相連,則端口需要設置為輸入下拉才能區分按下/松開兩種狀態
2.掃描
讀取PE3端口的狀態:
GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_3)
返回值為SET則端口為高電位,返回值為RESET則端口為低電位
在main函數中放入以下死循環代碼以實現掃描PE3端口並點燈的功能
while (1)
{
if(GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_3)==SET) // 如果按鍵對應端口為高電平
{
GPIO_SetBits(GPIOB, GPIO_Pin_5); // 熄燈(LED負極連接PB5,LED正極連接VCC,PB5高電平熄燈)
}
else // 否則
{
GPIO_ResetBits(GPIOB, GPIO_Pin_5); // 亮燈
}
delay_ms(10); // 一些開發板例程當中提供了delay函數,需要通過delay_init()初始化后才可使用
// 若無現成delay函數,可通過一定次數的for循環來代替
}
3.例程
代碼如下:
int main(void)
{
GPIO_InitTypeDef GPIO_Init1, GPIO_Init2;
delay_init();
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_Init1.GPIO_Pin = GPIO_Pin_5;
GPIO_Init1.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init1.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_Init1);
GPIO_SetBits(GPIOB, GPIO_Pin_5); //先熄燈
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE, ENABLE);
GPIO_Init2.GPIO_Pin = GPIO_Pin_3;
GPIO_Init2.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOE, &GPIO_Init2);
delay_ms(200);
while (1)
{
if(GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_3)==SET)
{
GPIO_SetBits(GPIOB, GPIO_Pin_5);
}
else
{
GPIO_ResetBits(GPIOB, GPIO_Pin_5);
}
delay_ms(10);
}
}
二、中斷
0.相關概念
中斷:程序運行過程中,系統外部、系統內部或者現行程序本身若出現緊急事件,處理機立即中止現行程序的運行,自動轉入相應的處理程序(中斷服務程序),待處理完后,再返回原來的程序運行
簡而言之就是觸發某一事件可以使得MCU跳轉執行該事件的處理程序,而按鍵按下或放開(GPIO口電平改變)則可作為一個外部中斷,通過編寫這一事件的處理程序從而達到改變燈亮滅狀態的目的
(這里提到的“事件”並不是STM32當中的專有名詞“事件”,而是泛指發生了某一件事)
使用掃描方式獲得按鍵輸入的思路如下:
主函數()
{
初始化()
死循環
{
如果(按鍵按下)
{……}
否則
{……}
}
}
而使用中斷獲得按鍵輸入的思路如下:
主函數()
{
初始化()
其它操作()
}
中斷處理函數()
{
如果(按鍵按下)
{……}
否則
{……}
}
對比可知使用掃描方式將使得芯片無法(難以)處理其它事務
NVIC:全名為“內嵌向量中斷控制器”,主要用來控制芯片中各個中斷的優先級,在很多地方都會使用(串口通信、SPI通信、定時器、I?C通信等涉及到實時處理的功能都會與中斷有關)
EXTI(不是EXIT):全名為“外部中斷/事件控制器”,可以實現輸入信號的上升沿檢測和下降沿的檢測
1.初始化(注意C語言中變量聲明需放在函數開頭)
1.1 NVIC
需要用到的初始化語句如下:
NVIC_InitTypeDef NVIC_I; //定義初始化結構體
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //設置整個系統的中斷優先級分組
NVIC_I.NVIC_IRQChannel=EXTI3_IRQn; //設置初始化哪個中斷
NVIC_I.NVIC_IRQChannelPreemptionPriority=0x02; //設置中斷搶占優先級
NVIC_I.NVIC_IRQChannelSubPriority=0x02; //設置中斷響應優先級(子優先級)
NVIC_I.NVIC_IRQChannelCmd=ENABLE; //中斷使能(啟動)
NVIC_Init(&NVIC_I); //初始化
中斷優先級分組、搶占優先級和子優先級的關系:
STM32系列的芯片當中一般會有很多的中斷,而當多個中斷同時發生時就需要一個調度機制來控制它們的執行順序,因此有了中斷的優先級的概念。優先級遵循以下幾點:
1.優先級的數字越小優先級越高
2.搶占優先級高的中斷會先執行,它也可以打斷搶占優先級低的中斷
3.當搶占優先級相同時,響應優先級高的中斷會先執行,但它不可以打斷響應優先級低的中斷
4.當兩個中斷的搶占優先級和響應優先級都相同時,先產生的中斷先執行(按照時間順序)
舉個例子,現在有三個中斷:
中斷1:搶占優先級為2,響應優先級為1
中斷2:搶占優先級為3,響應優先級為0
中斷3:搶占優先級為2,響應優先級為0
則3個中斷的優先級順序是中斷3>中斷1>中斷2,同時中斷1、3可以打斷中斷2,中斷3不能打斷中斷1
而兩類優先級可以設置成哪些值呢?這取決於整個系統的中斷優先級分組。通過
NVIC_PriorityGroupConfig();
可以設置整個系統的中斷優先級分組,其參數可以是NVIC_PriorityGroup_0、NVIC_PriorityGroup_1、NVIC_PriorityGroup_2、NVIC_PriorityGroup_3、NVIC_PriorityGroup_4之一。具體關系如下:
NVIC_PriorityGroup_0:0位搶占優先級(無效)+4位響應優先級(0~15)
NVIC_PriorityGroup_1:1位搶占優先級(01)+3位響應優先級(07)
NVIC_PriorityGroup_2:2位搶占優先級(03)+2位響應優先級(03)
NVIC_PriorityGroup_3:3位搶占優先級(07)+1位響應優先級(01)
NVIC_PriorityGroup_4:4位搶占優先級(0~15)+0位響應優先級(無效)
例如中斷分組設置為3,則所有中斷的搶占優先級可以被設置為0~7,響應優先級可以被設置為0、1
需要注意的是如果程序中用到了二次封裝的一些庫(比如開發板例程中廠商為初學者寫的串口庫等),則NVIC_PriorityGroupConfig()可能已經被調用過了,此時再次修改可能會帶來其它問題
1.2 EXTI
需要用到的初始化語句如下:
EXTI_InitTypeDef EXTI_I; //定義初始化結構體
EXTI_I.EXTI_Line=EXTI_Line3; //設置初始化哪條中斷/事件線
EXTI_I.EXTI_Mode = EXTI_Mode_Interrupt; //設置為產生中斷(EXTI_Mode_Event為產生事件)
EXTI_I.EXTI_Trigger = EXTI_Trigger_Falling; //設置為下降沿觸發
EXTI_I.EXTI_LineCmd = ENABLE; //使能
EXTI_Init(&EXTI_I); //初始化
中斷/事件的區別:
中斷產生后經由NVIC交給MCU進行處理(軟件層面)
事件最終作為一個脈沖信號直接觸發其它硬件(硬件層面)
附一張EXTI的框圖便於理解,藍色是中斷,紅色是事件

EXTI、NVIC與GPIO的對應關系:
如圖所示

上升/下降沿:
低電平跳到高電平為上升沿,高電平跳到低電平為下降沿
1.3 GPIO
與掃描方式的初始化代碼相同
GPIO_InitTypeDef GPIO_Init1, GPIO_Init2;
GPIO_EXTILineConfig(GPIO_PortSourceGPIOE,GPIO_PinSource3);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_Init1.GPIO_Pin = GPIO_Pin_5;
GPIO_Init1.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init1.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_Init1);
GPIO_SetBits(GPIOB, GPIO_Pin_5); //先熄燈
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE, ENABLE);
GPIO_Init2.GPIO_Pin = GPIO_Pin_3;
GPIO_Init2.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOE, &GPIO_Init2);
1.4 其它
目前不明確這兩條語句的作用
GPIO_EXTILineConfig(GPIO_PortSourceGPIOE,GPIO_PinSource3);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); //開啟端口復用,涉及到GPIO口做外部中斷時都需要這一條語句
2.中斷處理函數
函數名與中斷/事件線有着對應關系,可參照上一張表
void EXTI3_IRQHandler(void)
{
if(GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_3)==SET)
{
GPIO_SetBits(GPIOB, GPIO_Pin_5);
}
else
{
GPIO_ResetBits(GPIOB, GPIO_Pin_5);
}
EXTI_ClearITPendingBit(EXTI_Line3);
}
最后的EXTI_ClearITPendingBit()用於清除中斷標志位,避免對之后的中斷造成影響
3.例程
代碼如下:
int main(void)
{
GPIO_InitTypeDef GPIO_Init1, GPIO_Init2;
NVIC_InitTypeDef NVIC_I;
EXTI_InitTypeDef EXTI_I;
GPIO_EXTILineConfig(GPIO_PortSourceGPIOE,GPIO_PinSource3);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
NVIC_I.NVIC_IRQChannel=EXTI3_IRQn;
NVIC_I.NVIC_IRQChannelPreemptionPriority=0x02;
NVIC_I.NVIC_IRQChannelSubPriority=0x02;
NVIC_I.NVIC_IRQChannelCmd=ENABLE;
NVIC_Init(&NVIC_I);
EXTI_I.EXTI_Line=EXTI_Line3;
EXTI_I.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_I.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_I.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_I);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_Init1.GPIO_Pin = GPIO_Pin_5;
GPIO_Init1.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init1.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_Init1);
GPIO_SetBits(GPIOB, GPIO_Pin_5); //先熄燈
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE, ENABLE);
GPIO_Init2.GPIO_Pin = GPIO_Pin_3;
GPIO_Init2.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOE, &GPIO_Init2);
}
void EXTI3_IRQHandler(void)
{
if(GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_3)==SET)
{
GPIO_SetBits(GPIOB, GPIO_Pin_5);
}
else
{
GPIO_ResetBits(GPIOB, GPIO_Pin_5);
}
EXTI_ClearITPendingBit(EXTI_Line3);
}
2019.12.22
