目錄
前言
輸出時的GPIO控制部分
標準庫是如何操作寄存器完成GPIO驅動的初始化的?
問題1:如何掌握GPIO的編程細節——跟寄存器如何打交道
問題2:哪些寄存器,去哪里找呢?
問題三,寄存器的含義,寫入的值的含義是如何的?我們想要完成功能需要寫什么東西才能達到我們的目的
輸入的IDR寄存器
最直接的輸出寄存器 ODR寄存器
BRR和BSRR寄存器
標準庫/HAL庫是如何操作寄存器初始化GPIO的?
標準庫GPIO_Init初始化函數
標準庫的習慣——檢查我們的參數合法性
設置我們的GPIO的Mode
GPIO Mode Binary Encoding Table
區分和判斷到底是設置哪一個CR寄存器
HAL庫如何初始化引腳的
對GPIO觸發寫操作,設置GPIO的電平
標準庫的GPIO_WriteBit函數
HAL庫的HAL_GPIO_WritePin函數
對GPIO觸發讀操作
GPIO_ReadInputDataBit
HAL_GPIO_ReadPin
前言
你玩夠了前面的實驗,現在就要準備豎起耳朵聽聽原理了。GPIO是單片機中很重要的基礎外設,是單片機跟外界溝通的橋梁。
查看原理,一個非常好的習慣就是學會閱讀我們的單片機手冊。
上面的兩張圖像是我們親愛的單片機GPIO機制全家福,是我們分析源代碼和原理的一個重要的輔助手段,哦,我想你應該再說——這我分析雞毛啊,對,這個圖是8種GPIO模式混合在了一起的圖像,而且你發現,實際上,第一張圖和第二章圖在物理上只有一個區別,那就是最右側的上拉部分的二極管從VDD換成了VDD_FT,還得的筆者的分析資源手冊的部分嗎?你會看到一些引腳被標記上了FT,這個意思,是說明我們的單片機是耐壓為5V的引腳,凡是沒有標記FT的,你需要小心,他們是耐壓3.3V的,一旦給大了電壓,您的開發板可能就要報廢了(芯片引腳燒壞)。
我們上面談到了——“8種GPIO模式”這個組合,一個很自然的想法在你的腦袋里Rise up了,哪八種啊?不是我回答,我沒那個權力,是手冊回答的——
Mode | 模式 | 說明 |
---|---|---|
Input floating | 浮空輸入 | 輸入模式的一個子集:浮空的,換而言之,當沒有明確的電平輸入的時候,你的GPIO引腳的電平輸入猜猜樂狀態,如果沒有非常明確的理由,不要使用這個模式!!! |
Input pull-up | 輸入上拉 | 輸入模式的一個子集:上拉的一個輸入,換而言之,沒有輸入的時候,我們的電平就是出于高電平,你讀取這個GPIO的電平就是高電平 |
Input-pull-down | 輸入下拉 | 輸入模式的一個子集:下拉的一個輸入,換而言之,沒有輸入的時候,我們的電平就是出于低電平,你讀取這個GPIO的電平就是低電平 |
Analog | 模擬輸入 | 輸入模式的一個子集:電路層上的模擬,換而言之,我們直接讀取了電平的模擬量,也就是能通過一定手段拿到這個GPIO引腳上的電壓!(筆者到ADC會好好說明這個事情) |
Output open-drain | 開漏輸出 | 輸出模式的一個子集:開漏是MOS管模電的術語,不太懂的朋友我打一個比方:就像是門縫里漏進來了高電平,這個高電平必須是外部別人給的,不是自己給的,這個時候的輸出更像是輸出一個控制,它本身不提供高電壓,他對提供者(比如說你自己接的電池)進行控制,讓他輸出高電壓。 |
Output push-pull | 推挽輸出 | 輸出模式的一個子集:還記得筆者說的——強勢的輸出能力嗎,這里,我們的GPIO自己就在使勁的驅動外設,這就是單片機自己強勢的控制外部外設而不是依賴于給定的電源,這種就適合驅動小外設,比如說LED,常見的最簡單傳感器等等 |
Alternate function push-pull | 復用的推挽輸出 | 輸出模式的一個子集:推挽輸出上面剛說過,在這個事情上沒有半毛錢變化.問題在于修飾詞AF上,AF就是Alternate Function,也就是復用功能.這個事情對于大部分萌新而言會一臉懵逼,復用?什么復用?答案是,單片機之所以不菜,就是因為他有強大的復用能力,很快你就知道,片上的IIC芯片,SPI芯片等等如何對外溝通的?利用引腳跟外面打交道的,這個時候,GPIO就被征用,用來完成更加詳細特定的外部功能了! |
Alternate function open-drain | 復用的開漏輸出 | 輸出模式的一個子集:開漏輸出還是一樣的,復用筆者上一個也說過了,不再重復。 |
現在的學習階段上,我們不去特別深究電路原理,等到我們真正涉及到電路設計的時候再去了解。目前為之,我們只是接觸了最簡單的輸入和輸出,為此,我們來看看手冊對這些地方的描述。
輸出時的GPIO控制部分
很多文檔說的太過于失去把控,將電路實現細節和編程細節全部混合在一起,筆者初學的時候也是非常的頭大,這里筆者嘗試說明白。
注意,在輸出的時候,只有一個地方我們是真正關心的。
筆者告訴你的是。當我們調用GPIO_WriteBits的時候,寫入的是Output data寄存器,Outputdata寄存器將我們的行為傳遞給Output driver輸出驅動器子系統,輸出驅動器,最關心的就是兩個MOS管,也就是P-MOS管和N-MOS管。作為初學,不要立馬鉆到P-MOS和N-MOS的實現細節里去了,你需要知道的是,當我們處于強勢的推挽輸出的時候,兩個MOS管都工作,共同根據我們的設置切換對應的工作狀態,順著線給出我們的高低電平,從而從I/O Pin上給出單片機自己的高低電平原則。
那開漏呢?答案是PMOS管就不工作了!只有NMOS管干活,也就是他只能輸出低電平,高電平呢?不是單片機管的!我上面就說過了,實際上是控制外部電源輸出他自己的大電壓。電路部分點到位置。
我們作為軟件開發,可以不用如此的關心這些細節(讓那些硬件佬們操心吧,我每一次看到硬件?都會開磕),我們關心的是,對于軟件編程,庫寄存器到底在做什么。
標準庫是如何操作寄存器完成GPIO驅動的初始化的?
按照邏輯順序,我們假設我們已經開啟了GPIO時鐘,現在,GPIO已經就緒,我們需要安排它的工作任務,我們這里的部分就是在探討——我們委派GPIO任務的全部編程細節。
回到我們如何使用標準庫完成這個事情的:
// 初始化GPIO PA0 完成設置PA0為八大模式下的推挽輸出狀態gpio_init.GPIO_Mode = GPIO_Mode_Out_PP;gpio_init.GPIO_Speed = GPIO_Speed_50MHz;gpio_init.GPIO_Pin = GPIO_Pin_0;GPIO_Init(GPIOA, &gpio_init);
問題1:如何掌握GPIO的編程細節——跟寄存器如何打交道
江科大的筆者聽過,大佬是如何分析寄存器的,說真的把我嚇跑了,因為真的不太理解。我們知道,單片機的核心實際上就是操作寄存器,甚至廣義的說——你的電腦現在的工作,就是我們無時不刻瘋狂的操作寄存器設置處理器的狀態,接受和傳遞處理器的結果。所以,我們在硬件和軟件上的銜接,就是寄存器,理解了寄存器要如何操作,我們實際上就可以完成對我們手頭器件的編程了,看過筆者手搓操作系統系列的朋友應該會深有體會。
問題2:哪些寄存器,去哪里找呢?
數據手冊的第一個重要含義,就是告訴你,當你面對一個外設的時候,他有哪些基本我們需要關心的寄存器,哪些我們需要關心的概念。比如說八大寄存器的模式啊,需要操作的寄存器啊,等等內容
約定俗稱的講,我們的單片機往往分為控制寄存器和數據寄存器,控制寄存器控制我們的片上外設/片外外設的行為,數據寄存器那就是真往里寫東西的。換而言之,數據寄存器是貨,控制寄存器是人,你指揮人,讓他們正確的處理貨物。所以,我們實際上就是看——如何編程這些寄存器呢?
我們的手冊上給出了我們想要的東西,所以任何問題,先找手冊,不是沒有道理的,請看STM32F1xx系列的手冊,在我們的第159頁上的9.1,也就是第一大點,存在這樣的一段話,筆者認為非常的關鍵,需要放到這里
Each of the general-purpose I/O ports has two 32-bit configuration registers (GPIOx_CRL, GPIOx_CRH), two 32-bit data registers (GPIOx_IDR, GPIOx_ODR), a 32-bit set/reset register (GPIOx_BSRR), a 16-bit reset register (GPIOx_BRR) and a 32-bit locking register (GPIOx_LCKR).
每個通用 I/O 端口都有兩個 32 位配置寄存器(GPIOx_CRL、GPIOx_CRH)、兩個 32 位數據寄存器(GPIOx_IDR、GPIOx_ODR)、一個 32 位置位/復位寄存器(GPIOx_BSRR)、一個 16 位復位寄存器(GPIOx_BRR)和一個 32 位鎖定寄存器(GPIOx_LCKR)。
啊哈,這個沒啥好說的,首先就是我們需要說的,關心的寄存器上,就是控制寄存器GPIOx_CRL和GPIOx_CRH,這就是GPIO為什么分組了,我們對每一個組的GPIO都配備了專門的控制寄存器和輸入輸出的緩存寄存器。我們對GPIO的編程,實際上就是圍繞著這七個寄存器來說話的。
問題三,寄存器的含義,寫入的值的含義是如何的?我們想要完成功能需要寫什么東西才能達到我們的目的
這個問題就是數據手冊存在的第二個重要的含義,舉個例子,我們進一步閱讀手冊
Each I/O port bit is freely programmable, however the I/O port registers have to be accessed as 32-bit words (half-word or byte accesses are not allowed). The purpose of the GPIOx_BSRR and GPIOx_BRR registers is to allow atomic read/modify accesses to any of the GPIO registers. This way, there is no risk that an IRQ occurs between the read and the modify access.
每個 I/O 端口位均可自由編程,但 I/O 端口寄存器必須以 32 位字的形式進行訪問(不允許半字或字節訪問)。GPIOx_BSRR 和 GPIOx_BRR 寄存器的目的是允許對任何 GPIO 寄存器進行原子讀取/修改訪問。這樣,就不會有在讀取和修改訪問之間發生 IRQ 的風險。
因此,我們知道,實際上我們不需要太關心ODR寄存器,他的作用實際上是作為BRR和BSRR寄存器的實際執行官,或者說,充當一個輸出的影子。畢竟中斷的存在的確會帶來打斷前后狀態不一致的風險,我們自己操作寄存器的時候,也要操作的是GPIOx_BSRR 寄存器和GPIOx_BRR!不要更改ODR寄存器!
先關心的是控制,控制寄存器分為兩個:低32位的GPIOx_CRL和高32位的GPIOx_CRH寄存器。我們在配置模式的時候,實際上也是針對我們的控制寄存器進行編程,請看171頁的手冊:
筆者下面教授我們如何思考和閱讀這樣的手冊,第一個問題,想清楚我們要干啥。“我們現在要點LED”,”我們現在要讓GPIO讀取外部的電平...”,想明白這些事情,我們才能以正確的姿勢閱讀手冊。
我們看,對于低32位的寄存器,我們實際上每一個Pin的信息占據4位。舉個例子,CNF0和MODE0位就是針對我們的PIN_0說的,因此,當我們想要配置PA0的時候,我們實際上就只需要操作GPIOA的最低四位,這樣就完事了。
那我們應該如何正確的寫入值呢?看到上面的表格,上面的表格中,我們就完成了寄存器的模式設置。舉個例子,在點燈部分的時候,我們就說過需要設置GPIO為推挽輸出,因此,我們就需要設置我們的比特位是:CNF00,然后Mode呢,我們選擇的是50MHZ的速度,因此就是0011.你可以使用自己寫的點亮小燈的例程中進入調試模式來確認額這個事情
非常好!你已經邁出了學習看手冊和驗證的第一步了。下一步,請你試一試求證一下按鈕輸入輸出的控制寄存器的值如何查找!
提示:我們學習按鈕的時候,GPIO是按照輸入上拉進行配置的。此外,你需要考證下我們的ODR寄存器的值,這個算額外的挑戰
下一個問題就是,我們編程的時候,GPIO的輸入和輸出寄存器又是怎么一回事呢?請看手冊
輸入的IDR寄存器
IDR寄存器負責處理我們的輸入,我們實際上ReadPin的時候,也是讀取的IDR寄存器,他會存儲我們的讀取的電平的狀態。
The Input Data register (GPIOx_IDR) captures the data present on the I/O pin at every APB2 clock cycle.
輸入數據寄存器(GPIOx_IDR)在每個 APB2 時鐘周期捕獲 I/O 引腳上的數據。
關于何為APB2,這個事情筆者放到時鐘樹的介紹的時候,我們好好聊聊.
所以,我們想要得到GPIO外部引腳的電平的狀態,只需要讀取我們的IDR寄存器即可.我們后面分析標準庫的ReadGPIOPinBits和HAL_GPIO_ReadPin函數的時候,還會回來好好看人家如何使用C語言代碼完成這個工作的
最直接的輸出寄存器 ODR寄存器
When configured as output, the value written to the Output Data register (GPIOx_ODR) is output on the I/O pin. It is possible to use the output driver in Push-Pull mode or Open-Drain mode (only the N-MOS is activated when outputting 0).
當配置為輸出時,寫入輸出數據寄存器 (GPIOx_ODR) 的值將在 I/O 引腳上輸出。輸出驅動器可以在推挽模式或開漏模式下使用(輸出 0 時僅激活 N-MOS)。
需要注意的是,我們的寫入是非常直接的,最需要操作的安全辦法是使用位操作來辦事情.
These bits can be read and written by software and can be accessed in Word mode only. Note: For atomic bit set/reset, the ODR bits can be individually set and cleared by writing to the GPIOx_BSRR register (x = A .. G).
這些位可以由軟件讀取和寫入,并且只能在字模式下訪問。 注意:對于原子位設置/重置,可以通過寫入 GPIOx_BSRR 寄存器(x = A .. G)單獨設置和清除 ODR 位。
一般而言,我們也只會操作一個pin一個pin的操作,而且往往不希望中斷打斷我們的設置導致非常不安全的寫入,因此,讓我們進一步學習BSRR寄存器的寫和BRR寄存器的寫.
BRR和BSRR寄存器
BSRR寄存器是只能寫入的,如手冊所說!這里我們看到,這個32位的寄存器分成了上下兩部分,高16位是Port Reset Bit,也就是說,往這里寫東西的時候,對應的Pin上的ODR的位就會被復位為1,反之,則是0.
BRR寄存器的功能是對我們的指定的位進行清零.嗯,好像跟BSRR重復了?是這樣的!
哦對了,新一點的STM32系列是沒有BRR寄存器了,下面是筆者的STM32F407ZGT6對GPIO操作的手冊,所以,ST半導體也認為這個寄存器實在是沒有存在的必要,也就被精簡掉了.
基本上我們的寄存器也就說完了.
標準庫/HAL庫是如何操作寄存器初始化GPIO的?
標準庫GPIO_Init
初始化函數
我們看看GPIO_Init函數針對設置GPIO為一個推挽輸出的時候的程序流。
-
模式設置
-
如果我們的Pin是低8個,則設置設置Control Register的低位寄存器. 反之,設置Control Register的高位寄存器
標準庫的習慣——檢查我們的參數合法性
我們編程的時候,也最好檢查一下參數合法性質的問題,不然程序出現了異常,就會導致整個系統崩潰重啟,這可就不好了。
/*** @brief Initializes the GPIOx peripheral according to the specified* ? ? ? ? parameters in the GPIO_InitStruct.* @param GPIOx: where x can be (A..G) to select the GPIO peripheral.* @param GPIO_InitStruct: pointer to a GPIO_InitTypeDef structure that* ? ? ? ? contains the configuration information for the specified GPIO peripheral.* @retval None*/
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct)
{uint32_t currentmode = 0x00, currentpin = 0x00, pinpos = 0x00, pos = 0x00;uint32_t tmpreg = 0x00, pinmask = 0x00;/* Check the parameters */assert_param(IS_GPIO_ALL_PERIPH(GPIOx));assert_param(IS_GPIO_MODE(GPIO_InitStruct->GPIO_Mode));assert_param(IS_GPIO_PIN(GPIO_InitStruct->GPIO_Pin));
?
之后的合法性檢查的代碼筆者將不會進行任何的說明,除非特殊!
設置我們的GPIO的Mode
?
/*---------------------------- GPIO Mode Configuration -----------------------*/currentmode = ((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x0F);if ((((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x10)) != 0x00){ /* Check the parameters */assert_param(IS_GPIO_SPEED(GPIO_InitStruct->GPIO_Speed));/* Output mode */currentmode |= (uint32_t)GPIO_InitStruct->GPIO_Speed;}
這是干啥呢?答案是,設置我們的GPIO速度!嗯,你想想看,設置速度的信息,必須是輸出,對吧。我們的(((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x10)) != 0x00
實際上就是在檢查我們的第5位。嗯,整個需要請出來我們的GPIO庫對標準庫的抽象
typedef enum
{ GPIO_Mode_AIN = 0x0,GPIO_Mode_IN_FLOATING = 0x04,GPIO_Mode_IPD = 0x28,GPIO_Mode_IPU = 0x48,GPIO_Mode_Out_OD = 0x14,GPIO_Mode_Out_PP = 0x10,GPIO_Mode_AF_OD = 0x1C,GPIO_Mode_AF_PP = 0x18
}GPIOMode_TypeDef;
GPIO Mode Binary Encoding Table
Enum Name | Hex Value | Binary (8-bit) | CNF[1:0] | MODE[1:0] | Description |
---|---|---|---|---|---|
GPIO_Mode_AIN | 0x0 | 0000 0000 | 00 | 00 | Analog Input (ADC/DAC) |
GPIO_Mode_IN_FLOATING | 0x04 | 0000 0100 | 01 | 00 | Floating Input (no pull-up/down) |
GPIO_Mode_IPD | 0x28 | 0010 1000 | 10 | 00 | Input with Pull-Down |
GPIO_Mode_IPU | 0x48 | 0100 1000 | 10 | 00 | Input with Pull-Up |
GPIO_Mode_Out_OD | 0x14 | 0001 0100 | 01 | 11 | Output Open-Drain (max 50MHz) |
GPIO_Mode_Out_PP | 0x10 | 0001 0000 | 00 | 11 | Output Push-Pull (max 50MHz) |
GPIO_Mode_AF_OD | 0x1C | 0001 1100 | 01 | 11 | Alternate Function Open-Drain |
GPIO_Mode_AF_PP | 0x18 | 0001 1000 | 00 | 11 | Alternate Function Push-Pull |
區分和判斷到底是設置哪一個CR寄存器
我們的標準庫的設置方法比較的,嗯,天才
#define GPIO_Pin_0 ? ? ? ? ? ? ? ? ((uint16_t)0x0001) /*!< Pin 0 selected */
#define GPIO_Pin_1 ? ? ? ? ? ? ? ? ((uint16_t)0x0002) /*!< Pin 1 selected */
#define GPIO_Pin_2 ? ? ? ? ? ? ? ? ((uint16_t)0x0004) /*!< Pin 2 selected */
#define GPIO_Pin_3 ? ? ? ? ? ? ? ? ((uint16_t)0x0008) /*!< Pin 3 selected */
#define GPIO_Pin_4 ? ? ? ? ? ? ? ? ((uint16_t)0x0010) /*!< Pin 4 selected */
#define GPIO_Pin_5 ? ? ? ? ? ? ? ? ((uint16_t)0x0020) /*!< Pin 5 selected */
#define GPIO_Pin_6 ? ? ? ? ? ? ? ? ((uint16_t)0x0040) /*!< Pin 6 selected */
#define GPIO_Pin_7 ? ? ? ? ? ? ? ? ((uint16_t)0x0080) /*!< Pin 7 selected */
#define GPIO_Pin_8 ? ? ? ? ? ? ? ? ((uint16_t)0x0100) /*!< Pin 8 selected */
#define GPIO_Pin_9 ? ? ? ? ? ? ? ? ((uint16_t)0x0200) /*!< Pin 9 selected */
#define GPIO_Pin_10 ? ? ? ? ? ? ? ((uint16_t)0x0400) /*!< Pin 10 selected */
#define GPIO_Pin_11 ? ? ? ? ? ? ? ((uint16_t)0x0800) /*!< Pin 11 selected */
#define GPIO_Pin_12 ? ? ? ? ? ? ? ((uint16_t)0x1000) /*!< Pin 12 selected */
#define GPIO_Pin_13 ? ? ? ? ? ? ? ((uint16_t)0x2000) /*!< Pin 13 selected */
#define GPIO_Pin_14 ? ? ? ? ? ? ? ((uint16_t)0x4000) /*!< Pin 14 selected */
#define GPIO_Pin_15 ? ? ? ? ? ? ? ((uint16_t)0x8000) /*!< Pin 15 selected */
#define GPIO_Pin_All ? ? ? ? ? ? ? ((uint16_t)0xFFFF) /*!< All pins selected */
發現規律了把,實際上就是GPIO_Pin_X就是左移了一個X位,所以你看:我們下面對位檢測的處理是這樣做的:
?
/*---------------------------- GPIO CRL Configuration ------------------------*//* Configure the eight low port pins */// 如果是低8pin,那么。。。if (((uint32_t)GPIO_InitStruct->GPIO_Pin & ((uint32_t)0x00FF)) != 0x00){// 我們需要處理的寄存器就是CRL寄存器tmpreg = GPIOx->CRL;for (pinpos = 0x00; pinpos < 0x08; pinpos++){// 我們遍歷一下,看看對應GPIO_PIN的位是不是被置1了pos = ((uint32_t)0x01) << pinpos;/* Get the port pins position */currentpin = (GPIO_InitStruct->GPIO_Pin) & pos;if (currentpin == pos){// 是的,我們放上了1,這個時候,我們需要做的就是萃取控制的4個位。// 對于Pin0,則是0~3位,對于Pin1,則是4~7位。。。一次類推,所以我們才需要左移2,相當于x4了pos = pinpos << 2;/* Clear the corresponding low control register bits */pinmask = ((uint32_t)0x0F) << pos;// 本來是1111,這一下翻轉了成了0000tmpreg &= ~pinmask;/* Write the mode configuration in the corresponding bits */// current mode是只取了低4位的valuetmpreg |= (currentmode << pos);/* Reset the corresponding ODR bit */// 如果是下拉的輸入,那么我們對對應的BRR寄存器置1,等價于清0了if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD){GPIOx->BRR = (((uint32_t)0x01) << pinpos);}else{/* Set the corresponding ODR bit */if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU){// 反之,那就是置高電平GPIOx->BSRR = (((uint32_t)0x01) << pinpos);}}}}// 寫回去值GPIOx->CRL = tmpreg;}
剩下的CRH寄存器的邏輯完全一致,請看官自行分析,筆者的代碼放到下面了
/*---------------------------- GPIO CRH Configuration ------------------------*//* Configure the eight high port pins */if (GPIO_InitStruct->GPIO_Pin > 0x00FF){tmpreg = GPIOx->CRH;for (pinpos = 0x00; pinpos < 0x08; pinpos++){pos = (((uint32_t)0x01) << (pinpos + 0x08));/* Get the port pins position */currentpin = ((GPIO_InitStruct->GPIO_Pin) & pos);if (currentpin == pos){pos = pinpos << 2;/* Clear the corresponding high control register bits */pinmask = ((uint32_t)0x0F) << pos;tmpreg &= ~pinmask;/* Write the mode configuration in the corresponding bits */tmpreg |= (currentmode << pos);/* Reset the corresponding ODR bit */if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD){GPIOx->BRR = (((uint32_t)0x01) << (pinpos + 0x08));}/* Set the corresponding ODR bit */if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU){GPIOx->BSRR = (((uint32_t)0x01) << (pinpos + 0x08));}}}GPIOx->CRH = tmpreg;}
}
HAL庫如何初始化引腳的
void HAL_GPIO_Init(GPIO_TypeDef ?*GPIOx, GPIO_InitTypeDef *GPIO_Init)
{// 一些無關的事情,筆者略去了
?/* 配置端口引腳 - 遍歷所有需要配置的引腳 */while (((GPIO_Init->Pin) >> position) != 0x00u){/* 獲取當前引腳位置掩碼 */ioposition = (0x01uL << position);
?/* 獲取當前需要配置的引腳 */iocurrent = (uint32_t)(GPIO_Init->Pin) & ioposition;
?/* 如果當前引腳需要配置 */if (iocurrent == ioposition){// 這里是我們終點分析的邏輯 // < --------}
?position++; ?// 檢查下一個引腳}
}
我們先看看大致的框架,這里我們的代碼寫的更加明白一些,那就是當我們當然的與結果為1,說明我們的確需要配置GPIO的引腳,這個時候,我們就會開始進行配置。跟標準庫的流程是完全一致的是,我們首先會設置好4位GPIO的configurations
/* 檢查復用功能參數 */assert_param(IS_GPIO_AF_INSTANCE(GPIOx));/* 根據所需模式,填充config變量中的MODE[1:0]和CNF[1:0]位 */// 在這里,我們實際上是做自己的config向寄存器位的映射switch (GPIO_Init->Mode){/* 配置為推挽輸出模式 */case GPIO_MODE_OUTPUT_PP:/* 檢查速度參數 */assert_param(IS_GPIO_SPEED(GPIO_Init->Speed));config = GPIO_Init->Speed + GPIO_CR_CNF_GP_OUTPUT_PP; // 速度模式 + 推挽輸出配置break;/* 配置為開漏輸出模式 */case GPIO_MODE_OUTPUT_OD:assert_param(IS_GPIO_SPEED(GPIO_Init->Speed));config = GPIO_Init->Speed + GPIO_CR_CNF_GP_OUTPUT_OD; // 速度模式 + 開漏輸出配置break;/* 配置為復用功能推挽模式 */case GPIO_MODE_AF_PP:assert_param(IS_GPIO_SPEED(GPIO_Init->Speed));config = GPIO_Init->Speed + GPIO_CR_CNF_AF_OUTPUT_PP; // 速度模式 + 復用推挽配置break;/* 配置為復用功能開漏模式 */case GPIO_MODE_AF_OD:assert_param(IS_GPIO_SPEED(GPIO_Init->Speed));config = GPIO_Init->Speed + GPIO_CR_CNF_AF_OUTPUT_OD; // 速度模式 + 復用開漏配置break;/* 配置為輸入模式(也適用于事件和中斷模式) */case GPIO_MODE_INPUT:case GPIO_MODE_IT_RISING:case GPIO_MODE_IT_FALLING:case GPIO_MODE_IT_RISING_FALLING:case GPIO_MODE_EVT_RISING:case GPIO_MODE_EVT_FALLING:case GPIO_MODE_EVT_RISING_FALLING:/* 檢查上拉/下拉參數 */assert_param(IS_GPIO_PULL(GPIO_Init->Pull));if (GPIO_Init->Pull == GPIO_NOPULL){config = GPIO_CR_MODE_INPUT + GPIO_CR_CNF_INPUT_FLOATING; // 浮空輸入}else if (GPIO_Init->Pull == GPIO_PULLUP){config = GPIO_CR_MODE_INPUT + GPIO_CR_CNF_INPUT_PU_PD; // 上拉輸入/* 設置對應的ODR位(上拉) */GPIOx->BSRR = ioposition;}else /* GPIO_PULLDOWN */{config = GPIO_CR_MODE_INPUT + GPIO_CR_CNF_INPUT_PU_PD; // 下拉輸入/* 清除對應的ODR位(下拉) */GPIOx->BRR = ioposition;}break;/* 配置為模擬輸入模式 */case GPIO_MODE_ANALOG:config = GPIO_CR_MODE_INPUT + GPIO_CR_CNF_ANALOG; // 模擬輸入break;/* 默認情況(參數已通過assert_param檢查) */default:break;}
組合好了4個位之后,就是做實際上的寄存器修改了
/* 判斷當前引腳屬于低8位(CRL)還是高8位(CRH) */configregister = (iocurrent < GPIO_PIN_8) ? &GPIOx->CRL : &GPIOx->CRH;registeroffset = (iocurrent < GPIO_PIN_8) ? (position << 2u) : ((position - 8u) << 2u);/* 將新的引腳配置應用到寄存器 */MODIFY_REG((*configregister), ((GPIO_CRL_MODE0 | GPIO_CRL_CNF0) << registeroffset), (config << registeroffset));// 后面,我們略去了對中斷部分的設置
MODIFY_REG實際上是HAL庫封裝的一個通用的宏,這個宏的原型如下:
#define SET_BIT(REG, BIT) ((REG) |= (BIT))#define CLEAR_BIT(REG, BIT) ((REG) &= ~(BIT))#define READ_BIT(REG, BIT) ((REG) & (BIT))#define CLEAR_REG(REG) ((REG) = (0x0))#define WRITE_REG(REG, VAL) ((REG) = (VAL))#define READ_REG(REG) ((REG))#define MODIFY_REG(REG, CLEARMASK, SETMASK) WRITE_REG((REG), (((READ_REG(REG)) & (~(CLEARMASK))) | (SETMASK)))
嗯,實際上就是合并了之前的操作,從語句直接編程了宏,編譯速度的確會得到一定的提升。
對GPIO觸發寫操作,設置GPIO的電平
標準庫的GPIO_WriteBit函數
void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal)
{/* Check the parameters */assert_param(IS_GPIO_ALL_PERIPH(GPIOx));assert_param(IS_GET_GPIO_PIN(GPIO_Pin));assert_param(IS_GPIO_BIT_ACTION(BitVal)); if (BitVal != Bit_RESET){GPIOx->BSRR = GPIO_Pin;}else{GPIOx->BRR = GPIO_Pin;}
}
很容易想到吧!注意了,因為BSRR和BRR寄存器可以做到直接原子化的設置我們的GPIO狀態,因此使用BSRR和BRR寄存器顯然更加的安全
HAL庫的HAL_GPIO_WritePin函數
void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState)
{/* Check the parameters */assert_param(IS_GPIO_PIN(GPIO_Pin));assert_param(IS_GPIO_PIN_ACTION(PinState));if (PinState != GPIO_PIN_RESET){GPIOx->BSRR = GPIO_Pin;}else{GPIOx->BSRR = (uint32_t)GPIO_Pin << 16u;}
}
看到HAL庫為什么新了吧,我們前面說過,BRR寄存器已經淘汰了,這就是一個體現,即使是舊單片機,我們也不去操作過時的外設,保證盡可能少的修改代碼。
對GPIO觸發讀操作
GPIO_ReadInputDataBit
我認為無需多言,讀取IDR寄存器即可
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{uint8_t bitstatus = 0x00;/* Check the parameters */assert_param(IS_GPIO_ALL_PERIPH(GPIOx));assert_param(IS_GET_GPIO_PIN(GPIO_Pin)); if ((GPIOx->IDR & GPIO_Pin) != (uint32_t)Bit_RESET){bitstatus = (uint8_t)Bit_SET;}else{bitstatus = (uint8_t)Bit_RESET;}return bitstatus;
}
HAL_GPIO_ReadPin
/*** @brief Reads the specified input port pin.* @param GPIOx: where x can be (A..G depending on device used) to select the GPIO peripheral* @param GPIO_Pin: specifies the port bit to read.* This parameter can be GPIO_PIN_x where x can be (0..15).* @retval The input port pin value.*/
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)
{GPIO_PinState bitstatus;/* Check the parameters */assert_param(IS_GPIO_PIN(GPIO_Pin));if ((GPIOx->IDR & GPIO_Pin) != (uint32_t)GPIO_PIN_RESET){bitstatus = GPIO_PIN_SET;}else{bitstatus = GPIO_PIN_RESET;}return bitstatus;
}
如你所見,一樣是直接讀取了我們的GPIO的IDR寄存器作為一個返回值