1. 程式人生 > 其它 >(stm32學習總結)—GPIO位帶操作

(stm32學習總結)—GPIO位帶操作

本章參考資料:《STM32F10X-中文參考手冊》儲存器和匯流排構架章節、GPIO 章節,《CM3 權威指南 CnR2》儲存器系統章節。

位帶簡介

  位操作就是可以單獨的對一個位元位讀和寫,這個在 51 微控制器中非常常見。51 微控制器中通過關鍵字 sbit 來實現位定義,STM32 沒有這樣的關鍵字,而是通過訪問位帶別名區來實現。   在 STM32 中,有兩個地方實現了位帶,一個是 SRAM 區的最低 1MB 空間,令一個是外設區最低 1MB 空間。這兩個 1MB 的空間除了可以像正常的 RAM 一樣操作外,他們還有自己的位帶別名區,位帶別名區把這 1MB 的空間的每一個位膨脹成一個 32 位的字,當訪問位帶別名區的這些字時,就可以達到訪問位帶區某個位元位的目的。

外設位帶區

  外設外帶區的地址為:0X40000000~0X40100000,大小為 1MB,這 1MB 的大小在 103系列大/中/小容量型號的微控制器中包含了片上外設的全部暫存器,這些暫存器的地址為:0X40000000~0X40029FFF 。 外 設 位 帶 區 經 過 膨 脹 後 的 位 帶 別 名 區 地 址 為 :0X42000000~0X43FFFFFF,這個地址仍然在 CM3 片上外設的地址空間中。在 103 系列大/中小容量型號的微控制器裡面,0X40030000~0X4FFFFFFF 屬於保留地址,膨脹後的 32MB位帶別名區剛好就落到這個地址範圍內,不會跟片上外設的其他暫存器地址重合。    STM32 的全部暫存器都可以通過訪問位帶別名區的方式來達到訪問原始暫存器位元位的效果,這比 51 微控制器強大很多。因為 51 微控制器裡面並不是所有的暫存器都是可以位元位操作,有些暫存器還是得位元組操作,比如 SBUF。    雖然說全部暫存器都可以實現位元操作,但我們在實際專案中並不會這麼做,甚至不會這麼做。有時候為了特定的專案需要,比如需要頻繁的操作很多 IO 口,這個時候我們可以考慮把 IO 相關的暫存器實現位元操作。   

SRAM 位帶區

  SRAM 的位帶區的地址為:0X2000 0000~X2010 0000,大小為 1MB,經過膨脹後的位帶別名區地址為:0X2200 0000~0X23FF FFFF,大小為 32MB。操作 SRAM 的位元位這個用得很少。  

位帶區和位帶別名區地址轉換

  位帶區的一個位元位經過膨脹之後,雖然變大到 4 個位元組,但是還是 LSB (最低位)才有效。有人會問這不是浪費空間嗎,要知道 STM32 的系統匯流排是 32 位的,按照 4 個位元組訪問的時候是最快的,所以膨脹成 4 個位元組來訪問是最高效的。我們可以通過指標的形式訪問位帶別名區地址從而達到操作位帶區位元位的效果。那這兩個地址直接如何轉換,我們簡單介紹一下。  

外設位帶別名區地址

對於片上外設位帶區的某個位元,記它所在位元組的地址為 A,位序號為 n(0<=n<=7),則該位元在別名區的地址為:   1、 AliasAddr= =0x42000000+ (A-0x40000000)*8*4 +n*4 0X42000000 是外設位帶別名區的起始地址,0x40000000 是外設位帶區的起始地址,(A-0x40000000)表示該位元前面有多少個位元組,一個位元組有 8 位,所以*8,一個位膨脹後是 4 個位元組,所以*4,n 表示該位元在 A 地址的序號,因為一個位經過膨脹後是四個位元組,所以也*4。

SRAM 位帶別名區地址

對於 SRAM 位帶區的某個位元,記它所在位元組的地址為 A,位序號為 n(0<=n<=7),則 該位元在別名區的地址為: 1 AliasAddr= =0x22000000+ (A-0x20000000)*8*4 +n*4 公式分析同上。 

統一公式

為了方便操作,我們可以把這兩個公式合併成一個公式,把“位帶地址+位序號”轉換成別名區地址統一成一個巨集。   1 、// 把“位帶地址+位序號”轉換成別名地址的巨集   2、 #define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x02000000+((addr &0x00FFFFFF)<<5)+(bitnum<<2))    addr & 0xF0000000 是為了區別 SRAM 還是外設,實際效果就是取出 4 或者 2,如果是外設,則取出的是 4,+0X02000000 之後就等於 0X42000000,0X42000000 是外設別名區的起始地址。如果是 SRAM,則取出的是 2,+0X02000000 之後就等於 0X22000000,0X22000000 是 SRAM 別名區的起始地址。    addr & 0x00FFFFFF 遮蔽了高三位,相當於減去 0X20000000 或者 0X40000000,但是為什麼是遮蔽高三位?因為外設的最高地址是:0X2010 0000,跟起始地址 0X20000000 相減的時候,總是低 5 位才有效,所以乾脆就把高三位遮蔽掉來達到減去起始地址的效果,具體遮蔽掉多少位跟最高地址有關。SRAM 同理分析即可。<<5 相當於*8*4,<<2 相當於*4,這兩個我們在上面分析過。   最後我們就可以通過指標的形式操作這些位帶別名區地址,最終實現位帶區的位元位操作。   
1 2 3 4 // 把一個地址轉換成一個指標 2 #define MEM_ADDR(addr) *((volatile unsigned long *)(addr)) // 把位帶別名區地址轉換成指標 4 #define BIT_ADDR(addr, bitnum) MEM_ADDR(BITBAND(addr, bitnum))  
  這裡說明下 volatile 關鍵字,volatile 提醒編譯器它後面所定義的變數隨時都有可能改變,因此編譯後的程式每次需要儲存或讀取這個變數的時候,都會直接從變數地址中讀取資料。如果沒有 volatile 關鍵字,則編譯器可能優化讀取和儲存,可能暫時使用暫存器中的值,如果這個變數由別的程式更新了的話,將出現不一致的現象。

位帶操作的優點

在 STM32 應用程式開發中雖然可以使用庫函式操作外設,但如果加上位操作就如虎添翼。想想 51 微控制器內位操作的方便,就可以理解為什麼要對 STM32 使用位操作。STM32 位操作優點非常多,我們這裡就列舉幾個突出的: (1)對於控制 GPIO 的輸入和輸出非常簡單 (2)操作序列介面晶片非常方便(DS1302、74HC595 等),如果採用庫函式的話,那麼這個時序編寫就非常不方便。 (3)程式碼簡潔,閱讀方便

GPIO 位帶操作

  我們已經知道 STM32F1 支援的位帶操作區有兩個,其中應用最多的還是外設位帶區,在外設位帶區中包含了 APB1、APB2 還有 AHB 總線上的所有外設暫存器,使用位帶操作應用最多的外設還屬 GPIO,通過位帶操作控制 STM32 引腳輸入與輸出。   外設的位帶區,覆蓋了全部的片上外設的暫存器,我們可以通過巨集為每個暫存器的位都定義一個位帶別名地址,從而實現位操作。但這個在實際專案中不是很現實,也很少人會這麼做,我們在這裡僅僅演示下 GPIO 中 ODR 和 IDR 這兩個暫存器的位操作。   從手冊中我們可以知道 ODR 和 IDR 這兩個暫存器對應 GPIO 基址的偏移是 12 和 8,我們先實現這兩個暫存器的地址對映,其中 GPIOx_BASE 在庫函式裡面有定義。 GPIO 暫存器對映    
 1 //IO口地址對映
 2 #define GPIOA_ODR_Addr    (GPIOA_BASE+12) //0x4001080C 
 3 #define GPIOB_ODR_Addr    (GPIOB_BASE+12) //0x40010C0C 
 4 #define GPIOC_ODR_Addr    (GPIOC_BASE+12) //0x4001100C 
 5 #define GPIOD_ODR_Addr    (GPIOD_BASE+12) //0x4001140C 
 6 #define GPIOE_ODR_Addr    (GPIOE_BASE+12) //0x4001180C 
 7 #define GPIOF_ODR_Addr    (GPIOF_BASE+12) //0x40011A0C    
 8 #define GPIOG_ODR_Addr    (GPIOG_BASE+12) //0x40011E0C    
 9 
10 #define GPIOA_IDR_Addr    (GPIOA_BASE+8) //0x40010808 
11 #define GPIOB_IDR_Addr    (GPIOB_BASE+8) //0x40010C08 
12 #define GPIOC_IDR_Addr    (GPIOC_BASE+8) //0x40011008 
13 #define GPIOD_IDR_Addr    (GPIOD_BASE+8) //0x40011408 
14 #define GPIOE_IDR_Addr    (GPIOE_BASE+8) //0x40011808 
15 #define GPIOF_IDR_Addr    (GPIOF_BASE+8) //0x40011A08 
16 #define GPIOG_IDR_Addr    (GPIOG_BASE+8) //0x40011E08 
    現在我們就可以用位操作的方法來控制 GPIO 的輸入和輸出了,其中巨集引數 n 表示具體是哪一個 IO 口,n(0,1,2...16)。這裡麵包含了埠 A~G ,並不是每個微控制器型號都有這麼多埠,使用這部分程式碼時,要檢視你的微控制器型號,如果是 64pin 的則最多隻能使用到 C 埠。

GPIO 位操作

   
 1 //IO口操作,只對單一的IO口!
 2 //確保n的值小於16!
 3 #define PAout(n)   BIT_ADDR(GPIOA_ODR_Addr,n)  //輸出 
 4 #define PAin(n)    BIT_ADDR(GPIOA_IDR_Addr,n)  //輸入 
 5 
 6 #define PBout(n)   BIT_ADDR(GPIOB_ODR_Addr,n)  //輸出 
 7 #define PBin(n)    BIT_ADDR(GPIOB_IDR_Addr,n)  //輸入 
 8 
 9 #define PCout(n)   BIT_ADDR(GPIOC_ODR_Addr,n)  //輸出 
10 #define PCin(n)    BIT_ADDR(GPIOC_IDR_Addr,n)  //輸入 
11 
12 #define PDout(n)   BIT_ADDR(GPIOD_ODR_Addr,n)  //輸出 
13 #define PDin(n)    BIT_ADDR(GPIOD_IDR_Addr,n)  //輸入 
14 
15 #define PEout(n)   BIT_ADDR(GPIOE_ODR_Addr,n)  //輸出 
16 #define PEin(n)    BIT_ADDR(GPIOE_IDR_Addr,n)  //輸入
17 
18 #define PFout(n)   BIT_ADDR(GPIOF_ODR_Addr,n)  //輸出 
19 #define PFin(n)    BIT_ADDR(GPIOF_IDR_Addr,n)  //輸入
20 
21 #define PGout(n)   BIT_ADDR(GPIOG_ODR_Addr,n)  //輸出 
22 #define PGin(n)    BIT_ADDR(GPIOG_IDR_Addr,n)  //輸入
 

將上面的程式碼整理

   
 1 #ifndef _system_H
 2 #define _system_H
 3 
 4 
 5 #include "stm32f10x.h"
 6 
 7 
 8 //位帶操作,實現51類似的GPIO控制功能
 9 //具體實現思想,參考<<CM3權威指南>>第五章(87頁~92頁).
10 //IO口操作巨集定義
11 #define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+((addr &0xFFFFF)<<5)+(bitnum<<2)) 
12 #define MEM_ADDR(addr)  *((volatile unsigned long  *)(addr)) 
13 #define BIT_ADDR(addr, bitnum)   MEM_ADDR(BITBAND(addr, bitnum)) 
14 //IO口地址對映
15 #define GPIOA_ODR_Addr    (GPIOA_BASE+12) //0x4001080C 
16 #define GPIOB_ODR_Addr    (GPIOB_BASE+12) //0x40010C0C 
17 #define GPIOC_ODR_Addr    (GPIOC_BASE+12) //0x4001100C 
18 #define GPIOD_ODR_Addr    (GPIOD_BASE+12) //0x4001140C 
19 #define GPIOE_ODR_Addr    (GPIOE_BASE+12) //0x4001180C 
20 #define GPIOF_ODR_Addr    (GPIOF_BASE+12) //0x40011A0C    
21 #define GPIOG_ODR_Addr    (GPIOG_BASE+12) //0x40011E0C    
22 
23 #define GPIOA_IDR_Addr    (GPIOA_BASE+8) //0x40010808 
24 #define GPIOB_IDR_Addr    (GPIOB_BASE+8) //0x40010C08 
25 #define GPIOC_IDR_Addr    (GPIOC_BASE+8) //0x40011008 
26 #define GPIOD_IDR_Addr    (GPIOD_BASE+8) //0x40011408 
27 #define GPIOE_IDR_Addr    (GPIOE_BASE+8) //0x40011808 
28 #define GPIOF_IDR_Addr    (GPIOF_BASE+8) //0x40011A08 
29 #define GPIOG_IDR_Addr    (GPIOG_BASE+8) //0x40011E08 
30  
31 //IO口操作,只對單一的IO口!
32 //確保n的值小於16!
33 #define PAout(n)   BIT_ADDR(GPIOA_ODR_Addr,n)  //輸出 
34 #define PAin(n)    BIT_ADDR(GPIOA_IDR_Addr,n)  //輸入 
35 
36 #define PBout(n)   BIT_ADDR(GPIOB_ODR_Addr,n)  //輸出 
37 #define PBin(n)    BIT_ADDR(GPIOB_IDR_Addr,n)  //輸入 
38 
39 #define PCout(n)   BIT_ADDR(GPIOC_ODR_Addr,n)  //輸出 
40 #define PCin(n)    BIT_ADDR(GPIOC_IDR_Addr,n)  //輸入 
41 
42 #define PDout(n)   BIT_ADDR(GPIOD_ODR_Addr,n)  //輸出 
43 #define PDin(n)    BIT_ADDR(GPIOD_IDR_Addr,n)  //輸入 
44 
45 #define PEout(n)   BIT_ADDR(GPIOE_ODR_Addr,n)  //輸出 
46 #define PEin(n)    BIT_ADDR(GPIOE_IDR_Addr,n)  //輸入
47 
48 #define PFout(n)   BIT_ADDR(GPIOF_ODR_Addr,n)  //輸出 
49 #define PFin(n)    BIT_ADDR(GPIOF_IDR_Addr,n)  //輸入
50 
51 #define PGout(n)   BIT_ADDR(GPIOG_ODR_Addr,n)  //輸出 
52 #define PGin(n)    BIT_ADDR(GPIOG_IDR_Addr,n)  //輸入
53 
54 
55 #endif
 

 

主函式

該工程我們直接從 LED-庫函式 操作移植過來,有關 LED GPIO 初始化和軟體延時等函式我們直接用,修改的是控制 GPIO 輸出的部分改成了位操作。該實驗我們讓 IO 口輸出高低電平來控制 LED 的亮滅,負邏輯點亮。具體使用哪一個 IO 和點亮方式由硬體平臺決定。    
 1 #include "system.h"
 2 #include "led.h"
 3 
 4 
 5 /*******************************************************************************
 6 * 函 數 名         : delay
 7 * 函式功能           : 延時函式,通過while迴圈佔用CPU,達到延時功能
 8 * 輸    入         : i
 9 * 輸    出         : 無
10 *******************************************************************************/
11 void delay(u32 i)
12 {
13     while(i--);
14 }
15 
16 /*******************************************************************************
17 * 函 數 名         : main
18 * 函式功能           : 主函式
19 * 輸    入         : 無
20 * 輸    出         : 無
21 *******************************************************************************/
22 int main()
23 {
24     LED_Init();
25     while(1)
26     {
27         led1=!led1;
28         delay(6000000);
29     }
30 }