支持了位帶操作后,便可以使用普通的加載/儲存指令來對單一的比特進(jìn)行讀寫操作了。簡單而言,就是可以單獨(dú)的對一個比特位讀和寫。在F103中,有兩個地方實(shí)現(xiàn)了位帶操作,其中一個是SRAM區(qū)的最低1MB范圍,第二個則是片內(nèi)外設(shè)區(qū)的最低1MB范圍。這兩個區(qū)中的地址除了可以像普通的RAM一樣使用外,它們還都有自己的“位帶別名區(qū)”,位帶別名區(qū)把每個比特膨脹成一個32位的字。當(dāng)你通過位帶別名區(qū)訪問這些字時,就可以達(dá)到訪問原始比特的目的。
圖1.1位帶區(qū)與位帶別名區(qū)的膨脹對應(yīng)關(guān)系圖
支持位帶操作的兩個內(nèi)存區(qū)的范圍是:0x2000_0000‐0x200F_FFFF(SRAM區(qū)中的最低1MB);0x4000_0000‐0x400F_FFFF(片上外設(shè)區(qū)中的最低1MB)。外設(shè)位帶區(qū)經(jīng)過膨脹后的位帶別名區(qū)地址為:0X42000000~0X43FFFFFC,這部分地址空間為保留地址,沒有跟任何的外設(shè)地址重合。SRAM區(qū)經(jīng)過膨脹后的位帶別名區(qū)地址為:0X2200 0000~0X23FF FFFC,大小為32MB。
位帶區(qū)的一個比特位經(jīng)過膨脹之后,雖然變大到4個字節(jié),但是還是LSB才有效。有人會問這不是浪費(fèi)空間嗎,要知道F103的系統(tǒng)總線是32位的,按照4個字節(jié)訪問的時候是最快的,所以膨脹成4個字節(jié)來訪問是最高效的。我們可以通過指針的形式訪問位帶別名區(qū)地址從而達(dá)到操作位帶區(qū)比特位的效果。那這兩個地址直接如何轉(zhuǎn)換,下面簡單地介紹一下。
一、對于片上外設(shè)位帶區(qū)的某個比特,記它所在字節(jié)的地址為A,位序號為n(0<=n<=7),則該比特在別名區(qū)的地址為:
1.AliasAddr==0x42000000+ (A-0x40000000)*8*4 +n*4
0X42000000是外設(shè)位帶別名區(qū)的起始地址,0x40000000是外設(shè)位帶區(qū)的起始地址,(A-0x40000000)表示該比特位前面有多少個字節(jié),一個字節(jié)有8位,所以*8,一個位膨脹后是4個字節(jié),所以*4,n表示該比特在A地址的序號,因為一個位經(jīng)過膨脹后是四個字節(jié),所以也*4。
二、對于SRAM位帶區(qū)的某個比特,記它所在字節(jié)的地址為A,位序號為n(0<=n<=7),則該比特在別名區(qū)的地址為:
1.AliasAddr==0x22000000+ (A-0x20000000)*8*4 +n*4
公式的分析同上所示
三、上面公式的統(tǒng)一:
為了方便操作,我們可以把這兩個公式合并成一個公式,把"位帶地址+位序號"轉(zhuǎn)換成別名區(qū)地址統(tǒng)一成一個宏。
//把"位帶地址+位序號"轉(zhuǎn)換成別名地址的宏
2 #define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x02000000+((addr &
0x000FFFFF)<<5)+(bitnum<<2))
addr&0xF0000000是為了區(qū)別SRAM還是外設(shè),實(shí)際效果就是取出4或者2,如果是外設(shè),則取出的是4,+0X02000000之后就等于0X42000000,0X42000000是外設(shè)別名區(qū)的起始地址。如果是SRAM,則取出的是2,+0X02000000之后就等于0X22000000,0X22000000是SRAM別名區(qū)的起始地址。
addr & 0x00FFFFFF屏蔽了高三位,相當(dāng)于減去0X20000000或者0X40000000,但是為什么是屏蔽高三位?因為外設(shè)的最高地址是:0X2010 0000,跟起始地址0X20000000相減的時候,總是低5位才有效,所以干脆就把高三位屏蔽掉來達(dá)到減去起始地址的效果,具體屏蔽掉多少位跟最高地址有關(guān)。SRAM同理分析即可。<<5相當(dāng)于前面公式中的*8*4,<<2相當(dāng)于*4。
最后我們就可以通過指針的形式操作這些位帶別名區(qū)地址,最終實(shí)現(xiàn)位帶區(qū)的比特位操作。
//把一個地址轉(zhuǎn)換成一個指針
#defineMEM_ADDR(addr)*((volatileunsignedlong*)(addr))
//把位帶別名區(qū)地址轉(zhuǎn)換成指針
#defineBIT_ADDR(addr,bitnum)MEM_ADDR(BITBAND(addr,bitnum))
(后文有附上對*((volatileunsignedlong*)的理解)。
從手冊中我們可以知道ODR和IDR這兩個寄存器對應(yīng)GPIO基址的偏移是20和16,我們先實(shí)現(xiàn)這兩個寄存器的地址映射,其中GPIOx_BASE在庫函數(shù)里面有定義。接著根據(jù)下圖便可理解GPIO ODR和IDR寄存器映射代碼。
圖1.2寄存器起始地址
//IO口地址映射
#define GPIOA_ODR_Addr(GPIOA_BASE+12) //0x4001080C
#define GPIOB_ODR_Addr(GPIOB_BASE+12) //0x40010C0C
#define GPIOC_ODR_Addr(GPIOC_BASE+12) //0x4001100C
#define GPIOD_ODR_Addr(GPIOD_BASE+12) //0x4001140C
#define GPIOE_ODR_Addr(GPIOE_BASE+12) //0x4001180C
#define GPIOF_ODR_Addr(GPIOF_BASE+12) //0x40011A0C
#define GPIOG_ODR_Addr(GPIOG_BASE+12) //0x40011E0C
#define GPIOA_IDR_Addr(GPIOA_BASE+8) //0x40010808
#define GPIOB_IDR_Addr(GPIOB_BASE+8) //0x40010C08
#define GPIOC_IDR_Addr(GPIOC_BASE+8) //0x40011008
#define GPIOD_IDR_Addr(GPIOD_BASE+8) //0x40011408
#define GPIOE_IDR_Addr(GPIOE_BASE+8) //0x40011808
#define GPIOF_IDR_Addr(GPIOF_BASE+8) //0x40011A08
#define GPIOG_IDR_Addr(GPIOG_BASE+8) //0x40011E08
其中:
#define GPIOA_BASE(APB2PERIPH_BASE + 0x0800)
#define GPIOB_BASE(APB2PERIPH_BASE + 0x0C00)
#define GPIOC_BASE(APB2PERIPH_BASE + 0x1000)
#define GPIOD_BASE(APB2PERIPH_BASE + 0x1400)
#define GPIOE_BASE(APB2PERIPH_BASE + 0x1800)
#define GPIOF_BASE(APB2PERIPH_BASE + 0x1C00)
#define GPIOG_BASE(APB2PERIPH_BASE + 0x2000)
#define APB2PERIPH_BASE(PERIPH_BASE + 0x10000)
#define PERIPH_BASE((uint32_t)0x40000000)
//GPIO輸入輸出位操作
#define PAout(n)BIT_ADDR(GPIOA_ODR_Addr,n)//輸出
#define PAin(n)BIT_ADDR(GPIOA_IDR_Addr,n)//輸入
#define PBout(n)BIT_ADDR(GPIOB_ODR_Addr,n)//輸出
#define PBin(n)BIT_ADDR(GPIOB_IDR_Addr,n)//輸入
#define PCout(n)BIT_ADDR(GPIOC_ODR_Addr,n)//輸出
#define PCin(n)BIT_ADDR(GPIOC_IDR_Addr,n)//輸入
#define PDout(n)BIT_ADDR(GPIOD_ODR_Addr,n)//輸出
#define PDin(n)BIT_ADDR(GPIOD_IDR_Addr,n)//輸入
#define PEout(n)BIT_ADDR(GPIOE_ODR_Addr,n)//輸出
#define PEin(n)BIT_ADDR(GPIOE_IDR_Addr,n)//輸入
#define PFout(n)BIT_ADDR(GPIOF_ODR_Addr,n)//輸出
#define PFin(n)BIT_ADDR(GPIOF_IDR_Addr,n)//輸入
#define PGout(n)BIT_ADDR(GPIOG_ODR_Addr,n)//輸出
#define PGin(n)BIT_ADDR(GPIOG_IDR_Addr,n)//輸入
附:對*((volatile unsigned long*)的理解:
對于不同的計算機(jī)體系結(jié)構(gòu),設(shè)備可能是端口映射,也可能是內(nèi)存映射的。如果系統(tǒng)結(jié)構(gòu)支持獨(dú)立的IO地址空間,并且是端口映射,就必須使用匯編語言完成實(shí)際對設(shè)備的控制,因為C語言并沒有提供真正的“端口”的概念。如果是內(nèi)存映射,那就方便的多了。
以#define IOPIN(*((volatile unsigned long *) 0xE0028000))為例:作為一個宏定義語句,define是定義一個變量或常量的偽指令。首先(volatile unsigned long *)的意思是將后面的那個地址強(qiáng)制轉(zhuǎn)換成volatile unsigned long *,unsigned long *是無符號長整形,volatile是一個類型限定符,如const一樣,當(dāng)使用volatile限定時,表示這個變量是依賴系統(tǒng)實(shí)現(xiàn)的,以為著這個變量會被其他程序或者計算機(jī)硬件修改,由于地址依賴于硬件,volatile就表示他的值會依賴于硬件。
volatile類型是這樣的,其數(shù)據(jù)確實(shí)可能在未知的情況下發(fā)生變化。比如,硬件設(shè)備的終端更改了它,現(xiàn)在硬件設(shè)備往往也有自己的私有內(nèi)存地址,比如顯存,他們一般是通過映象的方式,反映到一段特定的內(nèi)存地址當(dāng)中,這樣,在某些條件下,程序就可以直接訪問這些私有內(nèi)存了。另外,比如共享的內(nèi)存地址,多個程序都對它操作的時候。你的程序并不知道,這個內(nèi)存何時被改變了。如果不加這個voliatile修飾,程序是利用catch當(dāng)中的數(shù)據(jù),那個可能是過時的了,加了voliatile,就在需要用的時候,程序重新去那個地址去提取,保證是最新的。歸納起來如下:
1. volatile變量可變允許除了程序之外的比如硬件來修改他的內(nèi)容2.訪問該數(shù)據(jù)任何時候都會直接訪問該地址處內(nèi)容,即通過cache提高訪問速度的優(yōu)化被取消。