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