從STM32的位帶操作重談嵌入式中尋址與對齊的理解
初接觸STM32的人一定花了不少時間用于理解其位帶操作(bit banding)的原理與步驟。位帶操作允許編程人員以字的單位讀/寫單一bit位?;叵胛覀兤綍r對于一個bit位的操作比如:↓
@-> PIN0 |= (1<<3);
@-> PIN0 &= ~(1<<5);
雖然這只是一行代碼,但是實際上這一行做了好幾步的工作。比如第一行,首先讀出當前PIN0的值放到緩存區(qū),將1左移三位放入緩存區(qū),將二者進行“或”操作,即將當前PIN0的第三位置位1,將結(jié)果存入到實際PIN0所在的地址,即更新了PIN0的值。當然實際寫成匯編后可能步驟不見得一定一樣,但是這幾步工作是一定得做的。
而對于位帶操作,STM32中將上述PIN0(假設它處于允許重新映射的區(qū)域,即位帶區(qū)->Bit Band Region)的每一個bit位重新映射到了一個單獨的地址,只需對這一個新的地址進行寫操作,則原PIN0值的對應位自動置位或清零。假設剛才我們PIN0的第3bit位重新映射的地址我們用變量PIN0BIT3表示,則剛才的操作可以寫作如下↓
@-> PIN0BIT3 = 1; //等同于PIN0 |= (1<<3), 這是由地址重映射保證的。
這一行的操作是,將1寫入到PIN0BIT3所在的地址,即更新了PIN0BIT3的值,結(jié)束。由于地址重映射,將保證PIN0的第三bit位被置一了??梢钥闯觯僮鞑襟E比之前簡單,因此同樣的操作處理的速度更快了。
好,以上就是位帶操作的原理,全部介紹完了,是不是很簡單。接下來我們自然就想問了,這個PIN0第三bit位重新映射的地址在哪?這樣地址重映射不是把內(nèi)存擴大了么,允許重映射的地址會不會有限制?原地址跟重映射的地址之間有沒有個換算公式將他們對應上?
我們自然而然會去尋找STM32的官方手冊的說明。在STM32F1系列的的編程參考以及官方手冊里均有提到位帶操作的感念,那份編程參考里更是提到了計算二者聯(lián)系的公式。
在編程參考P25頁可以找到,允許bit位重新映射的位帶區(qū)只有兩處,一處是SRAM區(qū),一處是片內(nèi)的外設區(qū)Peripheral,均有1M大小。熟悉的人一眼就看出來了,SRAM區(qū)里存放的是堆棧(heap, stack)、全局變量等,外設區(qū)Peripheral區(qū)就是我們操作這塊CPU經(jīng)常打交道的GPIO, TIMER, PWM, A/D等各個功能的寄存器的所在地址。重新映射的區(qū)域叫位帶別名區(qū)(Bit band alias),均有32MB大小。也就是說,我們最終操作的地址都僅僅是1MB,那擴充出來的32MB空間無外乎是為了操作方便快速而設定的,最終還是得影響到那1MB空間才能起作用。編程參考的P30頁以SRAM區(qū)介紹了這一對應關系↓
以0x20000000(1MB的開頭)這SRAM最低地址為例,其第一bit位重新映射到了0x22000000(32MB的開頭)地址上,第7bit位映射到了0x2200001C地址上,以此類推,到SRAM最高地址0x200FFFF(1MB的結(jié)尾)F的第7bit位映射到了0x23FFFFFC(32MB的結(jié)尾)。注意到上面跟下面的區(qū)域之間每個方格的地址增長區(qū)別,下面(bit-band region)每塊方格地址增長1,而上面(alias region)地址增長4,因此有了編程參考的第P30頁的關系轉(zhuǎn)換計算公式↓
好了,對于基礎扎實熟悉的人來說到這里已經(jīng)可以了,但是對于我,或者現(xiàn)在隱隱覺得有點疑問的人來說,可能對于這個換算的結(jié)果(1MB對應32MB)有點想進一步搞清楚這是為什么。為什么一會是字偏移(word_offset),一會是字節(jié)偏移(byte_offset),等等,字,bit,字節(jié),是怎么對應的?等等,不是說寄存器都是32位的,怎么上面的對應圖都是8bit(一字節(jié))一對應的?暈了。所以這里有必要鞏固一下這方面的基礎知識。
首先回顧最基本概念。
在二進制中,從單純數(shù)學上講我們知道有
@-> 2^10=1024=1K
@-> 2^20=1024*1024=1M
@-> 2^30=1024*1024*1024=1G
最小二進制單位為比特(bit),即單純的0,1,0,1,等等。對于音樂、圖像等模擬信號我們進行壓縮時通常采用的單位為比特率(bps),比如MP3最大比特率320Kbps,即每秒有320K個bit位,也就是每秒采樣后的數(shù)字0,1的個數(shù)有320K個。一般CD的采樣率為1411.2Kbps,因此音質(zhì)就好很多了。普通VCD為1.25Mbps,DVD視頻為5Mbps,標準藍光為40Mbps,所以采用藍光光盤的PS3游戲機的內(nèi)部通信帶寬比普通PC大很多也就是這個道理,因為每秒需要吞吐很大的數(shù)據(jù)量才能保證畫面的清晰。
一個字節(jié)(Byte)等于8個bit,按照慣例我手寫的B大寫了。字節(jié)是通常的計算機存儲的基本單位。我們通常所說的500GB硬盤、2GB內(nèi)存就是指500個G的字節(jié)(Byte)和2個G的字節(jié)(Byte)。通常我們所說的32位處理器(比如ARM)的內(nèi)存尋址范圍為4GB就很好理解了。從單純數(shù)學上講↓
@-> 2^32= 4 * 2^30=4*1G=4G
最后,4GB的后面加了個B,即字節(jié)(Byte),表示是4G個字節(jié)數(shù),因此32位處理器尋址范圍為4G個字節(jié)。
若覺得4GB內(nèi)存對于一些運算覺得不夠用,采用64位處理器就可以這一問題,我們看看64位的尋址范圍↓
@-> 2^64=2^34 * 2^30=16G*G
看到了吧,尋址范圍能有16G*G個字節(jié),遠遠大于32位處理器,連跳好幾個數(shù)量級,足夠滿足很多應用了。一般G*G就稱為E了,即64位處理器尋址范圍為16EB。不過這么大的數(shù)我是已經(jīng)沒什么概念了。
最早的紅白機,任天堂的FC,是一臺8位機(MOS 6502),小時候玩的紅白機覺得畫面簡單音樂粗糙,與其CPU性能不無關系。FC的接班人超任SFC采用了摩托羅拉的65836,3.58MHz的16位CPU,游戲畫面和音質(zhì)明顯上了一個檔次。掌機GameBoy(GB)和GameBoyColor(GBC)同為8位機。之后的GBA和NDS均采用了ARM系列芯片則直接是32位機了。這個網(wǎng)址可以很方便地查看GBA和NDS的硬件參數(shù)。32位主機時代PlayStation是王者可以說毫無疑問,而PS2你猜猜有多少位?64?不,人家直接跳到128位了。天文數(shù)字不是么,雖然PS2的CPU(Emotion Engion 簡稱EE)主頻只有295Mhz。所以說現(xiàn)在很多PC端的PS2模擬器并不能很好的模擬就是這個道理。而到了PS3時代又回到了64位。不過要理解,單純追求CPU的帶寬并不一定能帶來畫面和性能的提升,其中架構(gòu)的合理,緩存、外設時鐘等等都會影響性能。
之后,為什么所有這些數(shù)字,4GB,16EB后面都要加個B(字節(jié)),為什么存儲的單位是字節(jié)?這個問題我們先放一放,先來看看字(Word)的概念。
如果說比特(bit),字節(jié)(Byte)的概念比較好理解,那么字(Word)的概念就容易把人搞暈了,因為,字的長度并不統(tǒng)一,在不同CPU,不同時代,字的長度并不一致。從前的8位機上,比如前面提到的紅白機的MOS 6502,字長為8bit,即一個字節(jié)。在一些16位CPU上,比如著名的8086,字長是16位的,2個字節(jié)。而現(xiàn)在的32位CPU比如ARM和我們手中的PC,字長是32位,即4個字節(jié)。
可以參考這張wiki表對照歷史上CPU們對字長的規(guī)定。
如果說,字節(jié)(Byte)對應于存儲的單位大小,那么字(Word)則對應了CPU一次處理數(shù)據(jù)/指令的大小,因此才為了方便起了個字(Word)這個名字。對于ARM來說,字長是32位的,也就是4個字節(jié)?;叵肫餉RM里所有的寄存器,是不是每個寄存器都是32位的?所以,以這個32位為單位進行操作,因此這個32位即為一個字(Word)。那么為什么之前說字節(jié)(Byte)是存儲的基本單位呢?
對于ARM里面,數(shù)據(jù)的地址值跟數(shù)據(jù)自己本身都是32位的,這樣做的好處是操作起來方便,統(tǒng)一。當然,對于ARMv4架構(gòu)里的指令來說,有著32位的ARM指令集和16位的Thumb指令集,甚至對于Cortex M3來說都是32位或16位的Thumb指令集。這里先不討論這種指令集之前的區(qū)別,僅僅以允許的最大指令為32位來討論。另外,對于Cortex這一重回哈弗架構(gòu)的CPU來說,指令和數(shù)據(jù)是分開的,完全可以不用同樣的帶寬訪問(當然實際上STM32二者帶寬還是一樣的,方便操作,只是分開了而已)。有興趣的可以參考這篇文章對照指令集與架構(gòu)的區(qū)別。
現(xiàn)代主流CPU的存儲單元為字節(jié)(Byte),即物理地址的編碼是以字節(jié)為單位編碼的,一個地址對應于一個字節(jié)(Byte)或8個bit的空間,這一地址加上1,則對應于下一個字節(jié)或下一組8bit。這種物理地址的編碼方式是由CPU的架構(gòu)所保證的,并且為現(xiàn)在主流CPU所采用,因此說32位CPU的尋址范圍是4GB就是指可找到物理地址上總共4G范圍的區(qū)域,每一個區(qū)域上都有1個字節(jié)(Byte)的空間用于存放數(shù)據(jù)或指令。
那么很明顯,對于ARM的寄存器來說,一塊這樣的1個字節(jié)區(qū)域肯定是不夠的,每個32位的寄存器需要4個這樣的區(qū)域來存放才可以。我們經(jīng)常可以看到在定義寄存器時使用了下面的語句↓
/* General Purpose Input/Output (GPIO) */#define IOPIN0 (*((volatile unsigned long *) 0xE0028000))#define IOSET0 (*((volatile unsigned long *) 0xE0028004))#define IODIR0 (*((volatile unsigned long *) 0xE0028008))#define IOCLR0 (*((volatile unsigned long *) 0xE002800C))#define IOPIN1 (*((volatile unsigned long *) 0xE0028010))#define IOSET1 (*((volatile unsigned long *) 0xE0028014))#define IODIR1 (*((volatile unsigned long *) 0xE0028018))#define IOCLR1 (*((volatile unsigned long *) 0xE002801C))
以上寄存器在內(nèi)存里是相互連續(xù)的,我們可以很清楚的看到,他們之間的地址值的增量為4。這就很清楚了,相鄰寄存器地址值差4,實際上之間有4*1Byte的空間,即4*8bit=32bit的空間,這一空間剛好可以容下一個32bit的寄存器值存放。實際上,你可以看到幾乎所有訪問寄存器時的地址值的末尾均為0,4,8,C,即寄存器們一個挨著一個,32bit為一組,塞滿了他們所在的一片物理地址區(qū)域。因此對于32位CPU來說,出于效率一般均按字訪問,即訪問地址末尾為0,4,8,C的物理地址,一次訪問到4個字節(jié),不會單獨訪問其他地址,比如地址末尾為1的物理地址。當然,還有所謂的以半字(Half-Word)方式訪問,例如Thumb指令集,一次訪問2個字節(jié),訪問地址末尾為2的倍數(shù)的物理地址。
好了,那怎么保證訪問到這個地址時能讀取到32bit的數(shù)據(jù),且他們并不錯位、順序相反呢?這就涉及到字節(jié)的對齊問題。
我們先分析一下前面的一條預定義
@->#defineIOPIN0 (*((volatile unsigned long *) 0xE0028000))
這是一個指針的寫法。首先當訪問一個已知地址值的內(nèi)容時我們可以先定義一個指針,比如↓
@-> (uint32*)0xE0028000//當然也可以是unsigned int來代替uint32,都可以。
即將地址位于0xE0028000的數(shù)據(jù)用指針來表達。對于這一指針,uint32是一個32位的數(shù)據(jù)結(jié)構(gòu),限制了這一指針指向的內(nèi)容是以0xE0028000開始往地址增長方向,共計4個Byte,32bit的這么一塊區(qū)域,其數(shù)據(jù)結(jié)構(gòu)是uint32。之后我們需要得到這個指針的值,那么很簡單,用*運算取值即可↓
@-> ( *( (uint32*)0xE0028000 ) )//我故意多留了空格,目的是為了看得清楚。
這樣一整塊就得到了0xE0028000這一地址上的值,剩下想要讀取或?qū)懭攵伎梢粤?。原本的宏定義中用到的數(shù)據(jù)類型是unsigned long,也是32位無符號型整數(shù),加上volatile修飾,表示編譯器對這個數(shù)不做優(yōu)化處理。大小確定了之后,現(xiàn)在我們看著這4個字節(jié),假如其中的內(nèi)容如下(還記得每個地址上存放的是一個字節(jié)么),以十六進制表示↓
@-> 0xE0028000 :0xDD
@-> 0xE0028001 :0xCC
@-> 0xE0028002 :0xBB
@-> 0xE0028003 :0xAA
當讀取時,你認為我們最終得到的值是什么樣的?是0xDDCCBBAA(高位數(shù)存在地址低位),還是反過來的0xAABBCCDD(高位數(shù)存在地址高位)?想一想。
關于這一點,就是CPU在設計時最有爭議的地方,許多芯片廠商在設計時也并沒有很好的統(tǒng)一。習慣上將,規(guī)定第一種存儲方式,即高位數(shù)存放在地址低位,稱為大端(Big-endian),而第二種存儲方式,即高位數(shù)存放在地址高位,稱為小端(Small-endian)。對于我們來說,覺得小端對齊方式更符合常規(guī)思維,高位對應高地址,地位對應低地址??梢詮倪@個wiki網(wǎng)址參考有哪些硬件使用大端,哪些使用小端。注意ARM架構(gòu)是可以Bi-endian的,即可設置為大小端的一種,只不過我們常用的ARM芯片被制造商設置為小端,大小端設置的寄存器位往往設為只讀,只能通過REV指令零時調(diào)換存儲大小端而已。
回過頭看看我們訪問寄存器時,已知了地址值0xE0028000,并且我們需要讀取4Byte,即32bit因此需要設立變量為unsinged long,我們也知道了讀取后的字節(jié)順序為小端,因此對(*((volatile unsigned long *) 0xE0028000)) 這樣一句話的操作就恰好對應為我們需要的4個Byte的順序正確的寄存器值,我們在對嵌入式的寄存器進行操作時也都是這么做的而且運行的很好。
之前提到的兩個區(qū)域,SRAM區(qū)和Peripheral區(qū)都有位帶操作區(qū),這樣一來↓
IN A NUTSHELL:
@->位帶區(qū)(Bit band region)中的每一個bit均擴充到別名區(qū)(Bit band alias)上的一個字(Word),即4個字節(jié)(Byte),32個bit,因此總共1MB的位帶區(qū)被擴充為32MB的別名區(qū)。
@->為什么每一個bit位要擴充為一個字(Word)而不是字節(jié)(Byte)?因為CPU進行常規(guī)操作都是以字(Word)為單位訪問地址的。所以位帶區(qū)的相鄰一bit映射到別名區(qū)的地址增量是4,正好是4個字節(jié)(Byte),一個字(Word)。
之前提到的,編程手冊中給出的別名區(qū)和位帶區(qū)之間的計算公式,我想只要你有高中知識,用數(shù)學歸納法就可以推導出來了。選擇幾個實際地址試試看,你就明白了。↓
在實際操作中,根據(jù)Cortex-M3權威指南,可以根據(jù)如下宏定義進行位帶操作。以GPIOA口的控制輸出引腳寄存器ODR為例,有如下定義
#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+((addr &0xFFFFF)<<5)+(bitnum<<2)) #define MEM_ADDR(addr) *((volatile unsigned long *)(addr)) #define BIT_ADDR(addr, bitnum) MEM_ADDR(BITBAND(addr, bitnum))#define GPIOA_ODR_Addr (GPIOA_BASE+12) //0x4001080C #define PAout(n) BIT_ADDR(GPIOA_ODR_Addr,n) //OutPut
使用時只需要寫
@-> PAout(4)=1
就可以將GPIOA口的第四個bit位置為1了。
評論