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