本章參考資料:《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 #define MEM_ADDR(addr) *((volatile unsigned long *)(addr))
3 // 把位帶別名區地址轉換成指針
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 }
