【若需要嵌入式系統技術輔導課程 可來信洽談合作方式: iws6645@gmail.com,亦可先點擊參考這篇介紹文章】
歡迎透過合法的方式分享此文內容,若要轉載/轉貼,請明確貼出此原始連結並標示作者基本資訊,請勿抄襲及非法轉貼(例如擷取內文但並未註明出處)
< 簡介 >
這篇文章以市面上很流行的俗稱STM32F103C8T6 MCU最小系統開發板隨附的簡易GPIO驅動範例程式為例,因為這是一個淺顯的應用範例,所以適合寫成文章分享。相信許多初學者都曾以此範例來測試板子以及開發環境,但是許多人可能對於內容細節的方面沒有較深入的探討及了解,這邊會針對一些重點部分的細節做些說明
(註: ARM Cortex-M3 CPU從2004年就被推出了。而本文所講的ARM Cortex-M3 CPU based的STM32F103這款MCU是2007年推出,但至今現在市場上似乎還是供不應求)
STM32F103C8T6 MCU最小系統開發板 |
這個板子隨附的應用範例是個以MDK-ARM(KEILC)為開發環境的範例專案,我們這邊主要以編輯器將程式碼開起來並搭配官方硬體手冊資料來trace其流程和重點細節。專案裡面的主程式流程是屬於non-OS類的程式(沒有跑作業系統)
觀察main.c的main function不難看出其主要應用流程(目的/功能),該流程是要驅動/控制STM32 MCU的GPIO,以驅動開發板上面的LED,達到一般肉眼可見的亮/滅閃爍的功能。
程式碼的註解是我加上去方便讀者觀看
雖然它這個範例main function的回傳值型態寫int,然後其實沒有return,這方面不太標準
這篇文章主要針對PCout()的內容原理進行詳細的追蹤與分析探討
< 主要內容探討 >
PCout()從表面上看起來是一支函式(function),但其實是個macro(巨集),內容在GPIOLIKE51.h這支檔案裡面,其主要內容為BIT_ADDR(GPIOC_ODR_Addr,n)
(註: 在C語言之中,巨集和一般函式各有其特性,須看場合挑選合適的方式來應用。若以相同使用量的狀況下來比較兩者,則簡單來說,前者相對較省執行時間但是較耗費儲存空間;而後者相對較省儲存空間但較耗費執行時間。使用量越多則各自的特性所帶來的結果和差異也越顯著)
#define BIT_ADDR(addr, bitnum) MEM_ADDR(BITBAND(addr, bitnum))
#define MEM_ADDR(addr)
*((volatile unsigned long *)(addr))
#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+((addr &0xFFFFF)<<5)+(bitnum<<2))
(volatile unsigned long *)(addr)是將addr整數數值轉型為位址(address),以利存取(定址)記憶體空間或周邊裝置的暫存器(這屬於memory-map IO的方式去存取周邊,後面會說明)。而對PCout()的狀況來說,addr就是會被代入GPIOC的暫存器(register)的address(位址)數值,也就是0x4001100C
--------------------------
補充: 關於volatile關鍵字
C語言中的volatile關鍵字用來提醒編譯器,避免編譯器過度最佳化的狀況在所修飾的變數上發生
我們用GPIO input應用當例子來說明,如果GPIO外接一個按鈕,這個按鈕被按下的時機是由user按壓按鈕的狀況而定。所以,如果我們上面談到的這指標((volatile unsigned long *)(addr))去指向放置GPIO input digital signal的暫存器的位址,也就是將addr這個欄位代入正確對應的暫存器的位址,而該暫存器的內容(值)會隨user對按鈕的按壓狀況而改變(從單一bit來看就是1或0,下面再進一步解釋),所以這指標((volatile unsigned long *)(addr))須使用volatile關鍵字修飾
user對按鈕的按壓狀況而改變的狀況,以下面這個電路為例(截取自範例中的部分電路來說明),按鈕按下就是0(low level),沒按的就是1(high
level)。當然可能有些按鈕的機械彈跳狀況須要透過軟體或硬體的方式去避免(作debounce,因為不是這篇文章的重點,所以這部分就不在此贅述)
按鈕電路 |
再額外補充一個指標指向I/O周邊時可能會使用到volatile關鍵字的例子,特別是在嵌入式系統上面常見到的狀況。
ADC(Analog to Digital Converter)轉換由感測器輸出的類比訊號進而轉換為數位訊號(數位值)之後,依照現在許多ADC的設計,這個數位值通常會被硬體自動放置在某個暫存器(舉個別款MCU的例子,例如Arduino uno/nano/mini/fio等板子上面的Atmega328這顆同樣有內含ADC的MCU的狀況,這個數位值就會被放在ADC Result Registers,因為是轉成10bits的數位值,所以因為一個byte長度的暫存器放不下,所以就放在兩個暫存器內,ADCH的和ADCL),而這個暫存器內的數位值就會因為感測器感測的環境變化而跟著改變),現在為了方便說明,我們假設這個暫存器只有一個,而我們宣告一個指標變數指向這個暫存器,那這個指標變數在宣告時通常也須要加上volatile關鍵字以防止編譯器做了過度的最佳化導致非預期的後果(告知編譯器,在程式裡面指定要讀取的時候就真的是須要從ADC的register(暫存器)去讀值,不可因優化而簡化掉)。
所以addr被轉型為unsigned long的address(為了對硬體做存取),也須用volatile關鍵字修飾,以避免非預期的狀況發生
--------------------------
補充: 關於memory-map I/O 與port-mapped I/O
STM32F103 MCU內的ARM Cortex-M3 CPU是使用memory-map I/O的方式[4][5],簡單來說也就是CPU透過native的load/store指令去存取記憶體(如SRAM空間)與周邊裝置(如GPIO、UART、I2C、SPI、ADC…等等,更嚴格精確來說就是存取這些周邊裝置的控制或讀取相關功能的暫存器),如同將這些周邊裝置的暫存器當成記憶體空間來存取。至今ARM Cortex-M系列CPU應該都是屬於使用memory-map I/O的方式去存取周邊(有沒有特殊的例外不清楚)。
而另一種出現在其它平台(如x86)的方式為port-mapped I/O,可以讓I/O周邊和記憶體空間獨立區分開來,也就是表面上同一個address號碼,可以是存取記憶體空間的address,也可以是存取周邊裝置暫存器的address,雖然表面上是同一個address位址數字號碼,但是在兩邊是獨立不同的實體硬體空間。而既然要獨立區分兩種空間這種port-mapped I/O方式要使用特定的CPU指令去存取I/O周邊(而不是一般的load/store指令),而以CPU架構設計的角度,設計這些特定的CPU指令就要耗費相對的電晶體資源。
--------------------------
回到主題,多了一個'*'字號的 *((volatile unsigned long *)(addr)) 的意思就是這個指標所指向的位址的硬體(記憶體或暫存器)的內容值(以這邊的例子來說就是GPIO暫存器的內容值)。多嘮叨補充一下,這內容值以實際硬體的角度來看,其實就是數位訊號。
所以綜上所述,如果有仔細理解上述內容的讀者應該可以得知為何我們在main function之中去寫PCout(13)=1就得以直接驅動GPIO輸出high level(邏輯1)的數位訊號。因為在上述巨集的實作方面,就已經讓PCout(13)=1直接對應存取到GPIO之對應暫存器的內容值
而#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+((addr &0xFFFFF)<<5)+(bitnum<<2))這個BITBAND(addr, bitnum)這個macro的功能/目的就是要將addr和bitnum轉化對應到正確的暫存器位址並且可以atomic operation完成對暫存器的存取(後續會說明),可先參考這支GPIOLIKE51.h的:
#define GPIOC_ODR_Addr (GPIOC_BASE+12) //0x4001100C
我們可以從ST的STM32F103x8 Datasheet[1]的memory map圖以及RM0008 Reference manual[2]的GPIO registers的說明內容得知,0x4001100C是這顆STM32 MCU的GPIOC的ODR(Output Data Register)暫存器的位址,並且可在STM32F10x_StdPeriph_Driver之中的stm32f10x.h裡看到對應的GPIOC_BASE、APB2PERIPH_BASE的#define值。
Ref:[1] |
Ref:[2] |
而一大串的((addr & 0xF0000000)+0x2000000+((addr &0xFFFFF)<<5)+(bitnum<<2))主要是為了能正確存取Bit-banding功能方面的alias region的位址,這邊先針對Bit-banding功能機制作些說明。
Bit-banding是STM32F103 MCU內的ARM Cortex-M3 CPU的一種功能特性(在ARM Cortex-M系列的其他款式CPU應該也有,詳情請參照對應的ARM官方手冊確認),當CPU(執行我們寫的程式碼)對alias region之中的word做存取(STM32的word長度是32bits),就會對應存取到bit-band region的SRAM記憶體空間 或 周邊裝置暫存器的某個對應的bit,如下ARM Cortex-M3 Technical Reference Manual官方手冊[3]的圖所示(這個圖是以存取SRAM空間為例)。而在這個STM32F103C8小板子的範例程式之中就是要存取GPIOC_ODR暫存器的bit13。
Ref:[3] |
以STM32F103 MCU的規劃(搭配ARM Cortex-M3功能特性),在SRAM和Peripheral的方面都有這種Bit-banding機制/功能,各有1MB的bit-band region範圍,如下方的ARM Cortex-M3 Technical Reference Manual[3]的System address map圖。而因為alias region的一個word對應bit-band region的1個bit,一個word長度是32bits,所以對應的存取設定區域也就是的alias region的長度範圍為32MB(SRAM與Peripheral各有32MB的alias region對應各自的1MB的bit-band region)。
(提醒:前述已有提到ARM Cortex-M系列CPU based的STM32 MCU是以memory-map I/O式的方式去存取Peripheral周邊裝置)
Ref:[3] |
在寫入的方面,對alias region的word寫入0x01或者0xff(其實就是必須針對對應的word的bit0寫入1)都會讓bit-band region的對應bit被set為1,另外這個寫的過程其實會被轉換成atomic(操作過程不會被打斷)的read-modify-write operation;而在讀取的方面,將會從alias region所讀到的word的bit0讀回所要讀取的bit-band region的bit的status(1 or 0)。
Bit-Banding機制/功能的優點就是讓CPU能透過使用一般的Load/Store指令即可完成bit的讀/寫,這和過往只能先透過執行read(load相關指令)、modify(通常是邏輯運算相關指令)、write(store相關指令)流程的一連串相關獨立指令來完成bit讀/寫的傳統方式不同。所以Bit-Banding機制/功能也有助於節省code size
另外官方手冊[2]也有提到CPU在進行bit-band operations時,除了特殊狀況之外,基本上是不會停頓(stall)的
對於BITBAND這個macro,ARM官方的Cortex-M3 Technical Reference Manual[3]有直接給出公式如下,本文的後續內容也會針對這些公式內容的由來進行詳細解說
Ref:[3](紅色框起的是我認為命名不直覺的部分) |
回頭來看我們範例程式內的macro,前面內容提過addr為0x4001100C
關於#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+((addr &0xFFFFF)<<5)+(bitnum<<2))之中的(addr & 0xF0000000):
0x4001100C(addr)和0xF0000000作AND運算的目的是為了取出/取得結果為0x40000000也就是peripheral的起始位址或稱peripheral base address(由上面官方手冊內的Ref:[1]的memory map圖可得知)
接下來的+0x2000000(加上0x2000000)之目的就是為了得到我們peripheral的alias region的starting address,也就是公式中的bit_band_base(Cortex-M3 Technical Reference Manual官方手冊[3]裡面的內容: bit_band_base is the starting address of the alias region.),也就是0x42000000。
ARM Cortex-M3官方手冊[3]這個公式裡面的bit_band_base我認為在命名的方面有些不直覺的問題(因為會讓人導致誤解以為是bit-band region的starting address,但其實是alias region的starting address)。所以我認為應該要將公式內的此項目名稱改叫作bit_band_alias_base或者alias_base才對,畢竟是alias region的starting address嘛!才剛想完,我就上網google搜尋了一下,在加拿大有間大學Ryerson University的電機與電腦工程系在2019年開設的Embedded System Design課程[7]的Lab 2: Exploring Cortex-M3 Features for Performance Efficiency講義內容[6]就真的在他們的講義內將bit_band_base改以Bit Band Alias Base Address表示,如下圖:
Ref:[6] |
註:其實0x2000000就是十進制的33,554,432,也就是32M(32*1024*1024)。alias region的起始位址就是bit-band region起始位址加上32M之後的address
((addr &0xFFFFF)<<5)+(bitnum<<2)也就是bit_word_offset也就是(byte_offset x 32) + (bit_number × 4)。其中(addr &0xFFFFF)取出0x0001100C,0x1100C是GPIOC_ODR暫存器的offset位址偏移量,左移5位等於乘以32(公式中的*32),等於0x220180。
註(如果不想了解上面公式原理的讀者可直接參照公式,跳過這個註解): 為何要左移5位(等同乘以32)呢?細心的讀者如果搭配上面內容的Ref:[3]的官方例圖來觀察就應該可以發現,因為每一個word是32 bits也就是4個bytes也就是會占掉4個address號碼(每一個byte的資料會對應存放於一個address,這是電腦的基本rule,byte-addressable。暫存器和其位址的對應也是一樣),而alias region的一個word是對應到bit-band region的一個bit,換句話說,在bit-band region差了1個bit就等同在alias region差了4個address號碼的距離(1個word)的位址距離。而如果是在bit-band region差了1個byte(8bits)的位址距離,就等同在alias region差了32個address號碼(8個word)的位址距離(8*4=32),但這只能知道這個長度為32bits的GPIOC_ODR暫存器的位址(bit-band region)對應到alias region的一段空間的32個word的開頭的位址(對應暫存器的32個位元,一個位元對應一個word)。
但實際上我們所希望存取的暫存器的bit,是哪個word(其位於alias region的位址到底是哪個),就還要加上bitnum*4也就是bitnum<<2 (左移兩位),例如如果我們想存取暫存器的bit0,0*4=0,代表的就是該word就是從上述alias region的一段空間的32個word(包含128個位址)的開頭的位址(對應暫存器的32個位元)的開頭(+0)的這個word去存取;如果是想存取暫存器的bit1,1*4=4,代表的就是該word就是從上述alias region的一段空間的32個word(對應暫存器的32個位元)的開頭的位址去加上4(+4)的這個位址(這個word)去存取,所以到此也已經說明為何macro內容的最後要加上(bitnum<<2)。而在我們這個範例的例子中就是bitnum是13,故bitnum<<2(乘以4)的結果就是52(十六進制的0x34)
(2020/12/13更正,原先我誤將13<<2的結果看成24,其實是十進制的52,也就是十六進制的0x34)
所以最後我們將公式內的各項結果相加: 0x42000000 + 0x220180 + 0x34 = 0x422201B4
這個0x422201B4就是GPIOC_ODR暫存器的bit13在alias region所對應的word的位址
上面關於bit-banding的這部分比較複雜,初學者可能要多花點耐心才比較能看懂。如果想進一步閱讀關於Bit-banding功能機制(alias region和bit-band region)的官方說明資料,可參考ARM官方的Cortex-M3 Technical Reference Manual[3]或者ST的RM0008 Reference manual[2]的3.3.2章節。或者如果有對上述內容不清楚的朋友,我的課程內容(未來預計將會陸續開設STM32 MCU應用開發實務基礎的相關課程)會有相關的引導。
另外,我手上的範例程式中的stm32f10x.h中的某些巨集的註解將alias region和bit-band region給寫反了,看日期得知這應該是2010年的code
範例程式專案中的stm32f10x.h有錯誤的部分 |
< 結語 >
雖然這篇文章是用一個淺顯的應用範例(控制GPIO以驅動LED閃爍)來作為說明對象,寫這篇文章的用意有二:
(一) 為協助初學者與有興趣了解相關專業內容的人
(二) 希望讓一些習慣看技術表面的族群不要只看到表面,雖然這只是個LED閃爍的範例,但內容包含的學問可不少,光是看了上面的細節內容我想就可以讓更多人理解,工程實務不是簡單的事情,不要以為驅動GPIO沒什麼。如果覺得簡單,通常是有以下兩個原因
(1)只看到表面功能,只看到東西功能會動即滿足,但沒有去思考這背後要作多少事情及其原理,才會有眼下的這個應用功能
(2)因為用的是現成的東西,有人幫你做好太多事情,如果都要自己從頭來,恐怕沒幾個人有能力在短時間內做好,因為這絕對不是call現成的API就OK的事
何況這文章的內容這還只是這範例專案中一個部分而已,事實上如果要細說細節真的講不完,例如booting flow、進入main()之前還有一系列的初始流程(如將stack初始化)、clock init、GPIO init等等,只是太多東西都是廠家在事先所做好的(前人的累積)。
我想還是希望能藉由這類文章,讓一些比較少親自動手或長年來基本上沒從事過開發的族群,特別像是多數的資深專任大學老師或者已經很久沒動手的公司高層大咖們了解實際真實的工程實務的難度和重要性。
如果因為對於這些內容的本質不夠深入了解,可能會有些誤解,例如誤以為什麼這些範例程式的寫法過時了。而其實基本上根本就沒有這個問題,原因如下:
1. 除了本文所探討的bit-banding方式,以及傳統的直接對GPIO_ODR暫存器做讀(read)-改(modify)-寫(write)的存取之外,STM32F103 MCU存取GPIO_ODR暫存器確實也其它方式。例如去設定GPIOx_BSRR對應GPIOx_ODR做bit set/clear(而另一個暫存器GPIOx_BRR只能做clear),但是也是只能一次寫32bits也就是一個word(設定GPIO_BSRR或GPIO_BRR)去對應控制GPIOx_ODR的狀態(按照[2]針對GPIOx_BSRR的說明所述的:These bits are write-only and can be accessed in Word mode only)。
上述這使用GPIOx_BSRR的例子,和本文所探討的bit-banding是不同的方式,而且bit-banding機制不是只有針對周邊裝置,還可以針對SRAM空間做存取,SRAM有什麼BSRR可用嗎?何況bit-banding本來就也是一種ARM Cortex-M3 CPU所提供的方式/機制,讓開發者可以獨立去set/clear register或SRAM的單一bit。這機制有它合適的應用場合,只要不是被官方公開宣稱其功能必須被完全取代的狀況下,機制/方法的選擇沒有什麼絕對的優劣好壞之分,只有開發者自己會不會用的問題,而沒有什麼過時不過時的問題,畢竟這顆MCU的硬體特性/功能提供的選擇就是如此(就是有這些方式),既然這個功能特性仍存在,那就不會失去探討的意義。
如前面提到的,ARM
Cortex-M3 CPU從2004年就被推出了。而本文所講的ARM
Cortex-M3 CPU based的STM32F103這款MCU是在2007年被推出的,但至今現在市場上似乎還是供不應求。
另外,bit-banding功能機制在ARM Cortex-M4 CPU是個optional feature [8]。
2. 有用的知識就是有用(有核心重點知識/觀念上的通用性和可延伸性),例如C語言就是C語言,C語言已經被用了好幾十年了,C語言的指標也被用了好幾十年了,暫存器的觀念也好幾十年了,所以這和過時不過時有何相關? 如前所述,只有開發者自己會不會用的問題,而沒有什麼過時不過時的問題。
< 參考資料 >
Ref:
[1] STM32F103x8 STM32F103xB Datasheet - production data, https://www.st.com/resource/en/datasheet/cd00161566.pdf
[2] ST RM0008 Reference manual - STM32F101xx, STM32F102xx, STM32F103xx, STM32F105xx and STM32F107xx advanced Arm®-based 32-bit MCUs, https://www.st.com/resource/en/reference_manual/cd00171190-stm32f101xx-stm32f102xx-stm32f103xx-stm32f105xx-and-stm32f107xx-advanced-arm-based-32-bit-mcus-stmicroelectronics.pdf
[3] ARM Cortex-M3 Technical Reference Manual, https://developer.arm.com/documentation/ddi0337/h/programmers-model/bit-banding?fbclid=IwAR1OuCvOHza3oUnJK68PiIX9ytKBJhsTwatgI6Jg_zF6D9GfMZVvTmesI5o
[4] Memory-mapped I/O, https://en.wikipedia.org/wiki/Memory-mapped_I/O
[5] Lecture 5: Memory Mapped I/O, https://www.youtube.com/watch?v=aT5XMOrid7Y
[6] Ryerson University Dept. of Electrical and Computer Engineering - Embedded System Design course - Lab 2: Exploring Cortex-M3 Features for Performance Efficiency, https://www.ee.ryerson.ca/~courses/coe718/labs/Lab2.pdf
[7] Ryerson University Dept. of Electrical and Computer Engineering - Embedded System Design course, https://www.ee.ryerson.ca/~courses/coe718/
[8] ARM Cortex-M4 Processor Technical Reference Manual, https://developer.arm.com/documentation/100166/0001
--------------------------
孫文良 (阿良的嵌入式系統技術學習區)
【若需要嵌入式系統技術輔導課程
可來信洽談合作方式:
iws6645@gmail.com,亦可先點擊參考這篇介紹文章】
中國那邊這樣類似的寫法很多,但是台灣我只有看到這篇寫的最佳。
回覆刪除感謝
刪除