一、前情
電子信息專業,有嵌入式開發項目的經驗,但不算完全了解。上課沒認真聽,項目全靠吃老本,現在為了畢業設計和工作打算認真從頭學習嵌入式開發。
二、目前情況
看了一個星期正點原子關於STM32F429庫函數開發的書,主要看的部分是ucos、fatfs和音樂播放器這幾個部分,看的稀里糊塗的,應該是因為基礎太差。從頭開始看之后,明白了很多,但還有很多稀里糊塗。就去看了野火的資料,野火寫的很好,看完清晰里很多,包括寄存器概念,手冊的應用這種基礎知識都寫得很清晰明了,推薦大家看野火的教材。我目前的打算是看野火的教材,用正點原子的例程和開發板。正點原子的各種項目做的確實很好很有參考性。
三、筆記
(一)STM32芯片
M4內核是ARM公司做的,芯片集成設計由ST設計的。除內核以外的部分,是片上外設,內核與總線之間通過各種總線連接,其中其中主控總線有8條,被控總線有7條,具體見圖 STM32F429xx器件的總線接口
其中圓圈的部分表示可以通信,黃色部分是主控總線,粉色部分是被控總線。
(二)存儲器映射
存儲器本身不具有地址信息,它的地址是由芯片廠商或用戶分配,給存儲器分配地址的過程就稱為存儲器映射, 具體見圖 STM32F429存儲器映射 。如果給存儲器再分配一個地址就叫存儲器重映射。
這4GB的地址空間中,ARM已經粗線條的平均分成了8個塊,每塊512MB,每個塊也都規定了用途,具體分類見表格 存儲器功能分類 。
Boock0用來設計成內部FLASH,Block1用來設計成內部RAM,Block2用來設計成片上的外設。這是比較重要的三塊。
(三)寄存器映射
Block2片上外設,四個字節為一單元,共32bit,每一單元對應不同的功能,控制這些單元時就可以控制不同外設。除了找到每個單元的起始地址,然后通過C語言指針的操作方式來訪問這些單元外,還們可以根據每個單元功能的不同,以功能為名給這個內存單元取一個別名,這個別名就是我們經常說的寄存器,這個給已經分配好地址的有特定功能的內存單元取別名的過程就叫寄存器映射。
比如,我們找到GPIOH端口的輸出數據寄存器ODR的地址是0x40021C14(至於這個地址如何找到可以先跳過,后面我們會有詳細的講解), ODR寄存器是32bit,低16bit有效,對應着16個外部IO,寫0/1對應的的IO則輸出低/高電平。現在我們通過C語言指針的操作方式, 讓GPIOH的16個IO都輸出高電平,具體見 代碼清單:寄存器-1 。
0x4002 1C14在我們看來是GPIOH端口ODR的地址,但是在編譯器看來,這只是一個普通的變量,是一個立即數, 要想讓編譯器也認為是指針,我們得進行強制類型轉換,把它轉換成指針,即(unsigned int *)0x4002 1C14,然后再對這個指針進行 * 操作。
解釋:#define GPIOH_ODR *(unsigned int*)(0x4002 1C14) 表示地址為(0x4002 1C14)的存儲單元,其中 (unsigned int*)(0x4002 1C14) 表示0x4002 1C14是一個32位的地址指針,指針指向0x4002 1C14地址,*(unsigned int*)(0x4002 1C14)表示(0x4002 1C14)地址內存單元的內容。這種方式在單片機等底層文件普遍采用
用寄存器的方式來操作,具體見 代碼清單:寄存器-2 。
GPIOH_BASE是 GPIOH的基地址,0x14是 ODR寄存器的偏移地址,0xFF是1111 1111(這里有疑問,怎么變成8個1,不應該是16個1嗎)
(四)外設寄存器
以“GPIO端口置位/復位寄存器”為例,教大家如何理解寄存器的說明, 具體見圖 GPIO端口置位_復位寄存器說明 。
-
①名稱
寄存器說明中首先列出了該寄存器中的名稱,“(GPIOx_BSRR)(x=A…I)”這段的意思是該寄存器名為“GPIOx_BSRR”其中的“x”可以為A-I, 也就是說這個寄存器說明適用於GPIOA、GPIOB至GPIOI,這些GPIO端口都有這樣的一個寄存器。
-
②偏移地址
偏移地址是指本寄存器相對於這個外設的基地址的偏移。本寄存器的偏移地址是0x18,從參考手冊中我們可以查到GPIOA外設的基地址為0x4002 0000 , 我們就可以算出GPIOA的這個GPIOA_BSRR寄存器的地址為:0x4002 0000+0x18;同理,由於GPIOB的外設基地址為0x4002 0400, 可算出GPIOB_BSRR寄存器的地址為:0x4002 0400+0x18 。其他GPIO端口以此類推即可。
-
③寄存器位表
緊接着的是本寄存器的位表,表中列出它的0-31位的名稱及權限。表上方的數字為位編號,中間為位名稱,最下方為讀寫權限,其中w表示只寫,r表示只讀, rw表示可讀寫。本寄存器中的位權限都是w,所以只能寫,如果讀本寄存器,是無法保證讀取到它真正內容的。而有的寄存器位只讀, 一般是用於表示STM32外設的某種工作狀態的,由STM32硬件自動更改,程序通過讀取那些寄存器位來判斷外設的工作狀態。
-
④位功能說明
位功能是寄存器說明中最重要的部分,它詳細介紹了寄存器每一個位的功能。例如本寄存器中有兩種寄存器位,分別為BRy及BSy,其中的y數值可以是0-15, 這里的0-15表示端口的引腳號,如BR0、BS0用於控制GPIOx的第0個引腳,若x表示GPIOA,那就是控制GPIOA的第0引腳,而BR1、BS1就是控制GPIOA第1個引腳。
其中BRy引腳的說明是“0:不會對相應的ODRx位執行任何操作;1:對相應ODRx位進行復位”。這里的“復位”是將該位設置為0的意思, 而“置位”表示將該位設置為1;說明中的ODRx是另一個寄存器的寄存器位,我們只需要知道ODRx位為1的時候,對應的引腳x輸出高電平, 為0的時候對應的引腳輸出低電平即可。所以,如果對BR0寫入“1”的話, 那么GPIOx的第0個引腳就會輸出“低電平”,但是對BR0寫入“0”的話,卻不會影響ODR0位,所以引腳電平不會改變。要想該引腳輸出“高電平”, 就需要對“BS0”位寫入“1”,寄存器位BSy與BRy是相反的操作。
(五)C語言對寄存器的封裝
為了更方便地訪問寄存器,我們引入C語言中的結構體語法對寄存器進行封裝, 具體見 代碼清單:寄存器-6 。
這段代碼用typedef 關鍵字聲明了名為GPIO_TypeDef的結構體類型,結構體內有8個 成員變量,變量名正好對應寄存器的名字。 C語言的語法規定,結構體內變量的存儲空間是連續的,其中32位的變量占用4個字節,16位的變量占用2個字節, 具體見圖 GPIO_TypeDef結構體成員的地址偏移 。
也就是說,我們定義的這個GPIO_TypeDef ,假如這個結構體的首地址為0x4002 1C00(這也是第一個成員變量MODER的地址), 那么結構體中第二個成員變量OTYPER的地址即為0x4002 1C00 +0x04 ,正是代表MODER所占用的4個字節地址的偏移量, 其它成員變量相對於結構體首地址的偏移,在上述代碼右側注釋已給出,其中的BSRR寄存器分成了低16位BSRRL和高16位BSRRH,BSRRL置1引腳輸出高電平, BSRRH置1引腳輸出低電平,這里分開只是為了方便操作。
這樣的地址偏移與STM32 GPIO外設定義的寄存器地址偏移一一對應,只要給結構體設置好首地址,就能把結構體內成員的地址確定下來, 然后就能以結構體的形式訪問寄存器了,具體見 代碼清單:寄存器-7。
這段代碼先用GPIO_TypeDef類型定義一個結構體指針GPIOx,並讓指針指向地址GPIOH_BASE(0x4002 1C00),使用地址確定下來,然后根據C語言訪問結構體的語法,用GPIOx->BSRRL、GPIOx->MODER及GPIOx->IDR等方式讀寫寄存器。
最后,我們更進一步,直接使用宏定義好GPIO_TypeDef類型的指針,而且指針指向各個GPIO端口的首地址,使用時我們直接用該宏訪問寄存器即可, 具體 代碼清單:寄存器-8 。
(六)修改寄存器的位操作方法
使用C語言對寄存器賦值時,我們常常要求只修改該寄存器的某幾位的值,且其它的寄存器位不變,這個時候我們就需要用到C語言的位操作方法了。
5.5.3.1. 把變量的某位清零
此處我們以變量a代表寄存器,並假設寄存器中本來已有數值,此時我們需要把變量a的某一位清零, 且其它位不變,方法見 代碼清單:寄存器-9 。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//定義一個變量a = 1001 1111 b (二進制數)
unsigned char a = 0x9f; //對bit2 清零 a &= ~(1<<2); //括號中的1左移兩位,(1<<2)得二進制數:0000 0100 b //按位取反,~(1<<2)得1111 1011 b //假如a中原來的值為二進制數: a = 1001 1111 b //所得的數與a作”位與&”運算,a = (1001 1111 b)&(1111 1011 b), //經過運算后,a的值 a=1001 1011 b // a的bit2 位被被零,而其它位不變。
|
5.5.3.2. 把變量的某幾個連續位清零
由於寄存器中有時會有連續幾個寄存器位用於控制某個功能,現假設我們需要把寄存器的某幾個連續位清零, 且其它位不變,方法見 代碼清單:寄存器-10 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
//若把a中的二進制位分成2個一組
//即bit0、bit1為第0組,bit2、bit3為第1組, // bit4、bit5為第2組,bit6、bit7為第3組 //要對第1組的bit2、bit3清零 a &= ~(3<<2*1); //括號中的3左移兩位,(3<<2*1)得二進制數:0000 1100 b //按位取反,~(3<<2*1)得1111 0011 b //假如a中原來的值為二進制數: a = 1001 1111 b //所得的數與a作”位與&”運算,a = (1001 1111 b)&(1111 0011 b), //經過運算后,a的值 a=1001 0011 b // a的第1組的bit2、bit3被清零,而其它位不變。 //上述(~(3<<2*1))中的(1)即為組編號;如清零第3組bit6、bit7此處應為3 //括號中的(2)為每組的位數,每組有2個二進制位;若分成4個一組,此處即為4 //括號中的(3)是組內所有位都為1時的值;若分成4個一組,此處即為二進制數“1111 b” //例如對第2組bit4、bit5清零 a &= ~(3<<2*2);
|
5.5.3.3. 對變量的某幾位進行賦值。
寄存器位經過上面的清零操作后,接下來就可以方便地對某幾位寫入所需要的數值了,且其它位不變, 方法見 代碼清單:寄存器-11 ,這時候寫入的數值一般就是需要設置寄存器的位參數。
1 2 3 4 5 |
//a = 1000 0011 b
//此時對清零后的第2組bit4、bit5設置成二進制數“01 b ” a |= (1<<2*2); //a = 1001 0011 b,成功設置了第2組的值,其它組不變
|
5.5.3.4. 對變量的某位取反
某些情況下,我們需要對寄存器的某個位進行取反操作,即 1變0 ,0變1,這可以直接用如下操作, 其它位不變,見 代碼清單:寄存器-12 。
1 2 3 4 5 |
//a = 1001 0011 b
//把bit6取反,其它位不變 a ^=(1<<6); //a = 1101 0011 b
|
四、總結
火哥真是寫的非常清楚明了,對於嵌入式零基礎或者接近零基礎而言都非常友好,看完總算大徹大悟,以上部分來自F429挑戰者的第五章,建議看完這一章再去看火哥的一天stm32入門的PDF,對日后的理解有很大的幫助。(我的筆記比較潦草,還有大段摘抄,主要是針對我自己不太會的地方,勿噴)