STM32 嵌入式學習入門(0)——C語言基礎複習
摘要
主要介紹了嵌入式程式設計中幾個常用,但軟體程式設計中用得不是很多的C語言知識。包括位操作、條件編譯、結構體和結構體指標、typedef宣告型別、以及extern變數宣告、static關鍵字等內容。
本文並沒有將相關C語言知識點介紹地很詳細,畢竟這麼多知識點要想掌握絕對不是看幾篇文件就能掌握的。因此博主建議,如果上述的C語言知識掌握得還不是很好的話,找一本C語言的書好好研究研究。尤其是結構體和結構體指標、還有函式的知識(本文沒提到),一定要很熟練。
本文除了簡要介紹C語言知識,也結合博主自己的感受簡單談了各個知識點用在了嵌入式程式設計的什麼地方,有不詳細和描述不準確的地方歡迎大家留言討論。
要想學習STM32,C語言的基礎是必須的。除了最基本的C語言的語法,如迴圈、判斷、陣列、結構體、函式、指標這些軟體程式設計常用的知識外,還包括位操作、條件編譯、結構體指標、typedef宣告型別、以及extern變數宣告、static關鍵字等常用內容。這裡結合實際程式碼分析一下這些知識點,如果想完整系統地瞭解這些C語言知識,大家可以翻翻C語言教材,比如《C Primer Plus》(第六版)這本書,尤其對於位操作的知識講得很詳細。
一、位操作:
位操作簡單說就是指對基本型別變數可以在位級別進行操作。下面先看幾種位操作符:
& | 按位與 | ~ | 取反 |
| | 按位或 | << | 左移 |
^ | 按位異或 | >> | 右移 |
掌握了這六種操作否的用法,C語言的位操作就差不多了。這六種操作符的解釋如下:
1. & 按位與: 如果兩個相應的二進位制位都為1,則該位的結果值為1,否則為0。// 1&1 = 1 1&0 = 0 0&1 = 0 0&0 = 0
2.|按位或:兩個相應的二進位制位中只要有一個為1,該位的結果值為1。// 1|1 = 1 0|1 = 1 1|0 = 1 0|0 = 0
3.^按位異或: 若參加運算的兩個二進位制位值相同則為0,否則為1。// 1^1 = 0 0^1 = 1 1^0 = 1 0^0 = 0
4.!取反: 對一個二進位制數按位取反,即將0變1,將1變0。// 1! = 0 0! = 1
5.<<左移:用來將一個數的各二進位制位全部左移N位,右補0。// 00001100 << 2 = 00110000
6.>>右移:將一個數的各二進位制位右移N位,移到右端的低位被捨棄,對於無符號數,高位補0。// 00001100 >> 2 = 00000011
下面介紹一些用暫存器開發STM32時候實用的位操作技巧:
1)不改變其他位的值的狀況下,對某幾個位進行設值。
這個場景微控制器開發中經常使用,方法就是先對需要設定的位用 & 操作符進行清零操作,然後用 | 操作符設值。比如我要改變GPIOA的狀態,可以先對暫存器的值進行 & 清零操作
然後再與需要設定的值進行 | (或運算)。
GPIOA->CRL&=0XFFFFFF0F; //將第 4-7 位清 0
GPIOA->CRL|=0X00000040; //設定相應位的值,不改變其他位的值 (將CRL暫存器第7位設定為1)
2)取反操作使用技巧
SR 暫存器的每一位都代表一個狀態,某個時刻我們希望去設定某一位的值為 0,同時其他位都保留為 1,簡單的作法是直接給暫存器設定一個值:
TIMx->SR=0xFFF7;
這樣的做法設定第3位為0,但是這樣的作法同樣不好看,並且可讀性很差。看看庫函式程式碼中怎樣使用的:
TIMx->SR = (uint16_t)~TIM_FLAG;
而 TIM_FLAG 是通過巨集定義定義的值:
#define TIM_FLAG_Update ((uint16_t)0x0001)
#define TIM_FLAG_CC1 ((uint16_t)0x0002)
看這個應該很容易明白,可以直接從巨集定義中看出 TIM_FLAG_Update 就是設定的第 0 位了,可讀性非常強。
注:在STM32的開發中,更多的時間可能會直接使用官方的庫函式,庫函式實際上是將複雜的暫存器封裝了一下。使用庫函式可以避免複雜的位操作,使程式碼更具有可讀性,但同樣的專案,使用庫函式其程式碼量可能會比直接通過操作暫存器寫出來的工程的程式碼量稍微多一點,執行效率可能會稍微低一點,當然這只是一點點…………
學習STM32的時候要從暫存器上去理解原理,理解實現過程,但是如果真的需要做一個嵌入式專案,可能用庫函式去開發比較方便,效率更好一點,這是博主自己的感受和觀點。
二、條件編譯:
微控制器程式開發過程中,經常會遇到一種情況, 當滿足某條件時對一組語句進行編譯,而當條件不滿足時則編譯另一組語句。 條件編譯命令最常見的形式為:
#ifdef 識別符號
程式段 1
#else
程式段 2
#endif
它的作用是:當識別符號已經被定義過(一般是用#define 命令定義),則對程式段 1 進行編譯,否則編譯程式段 2。 其中#else 部分也可以沒有,即:
#ifdef
程式段 1
#endif
這個條件編譯在 MDK 裡面是用得很多的,在 stm32f10x.h 這個標頭檔案中經常會看到這樣的語句:
#ifdef STM32F10X_HD
大容量晶片需要的一些變數定義
#end
而 STM32F10X_HD 則是我們通過#define 來定義的。
條件編譯理解起來也不是很困難,可以類比於C語言中的 if-else 語句去理解。條件編譯在STM32的開發中還是比較常用的。自己寫程式碼寫 .h 檔案的時候開頭會用到。此外就是要能看懂庫函式裡面的條件編譯了。
三、結構體和結構體指標:
結構體是C語言中的基礎知識,同時結構體和結構指標也是STM32開發中非常重要的東西,尤其在使用庫函式的時候,庫函式中很多函式的入口引數中都有結構體指標,所以如果我們要呼叫這種函式,就先在主調函式中宣告一個結構體變數,然後對這個結構體變數的各個成員賦值,最後再呼叫相關函式,呼叫的時候看清楚函式原型,入口引數是結構體型別還是結構體指標,不要搞錯了。這裡再多說兩句,這裡的結構體每個成員可以賦的值往往都是通過列舉或者巨集定義確定好的,不能自己亂寫,而應該去查詢巨集定義部分的程式碼,選定需要的那個列舉字面值作為結構體相關成員的值。
關於結構體和結構體指標的例子可以看GPIO的初始化,這裡就不再多說了:STM32 GPIO的介紹
四、typedef宣告型別:
如果學過資料結構,相信對typedef也不陌生。用typedef的一個好處就是使程式碼的可讀性更高,寫程式碼也更方便。typedef 在程式碼中用得最多的就是定義結構體的類型別名和列舉型別了。
struct _GPIO
{
__IO uint32_t CRL;
__IO uint32_t CRH;
…
};
定義了一個結構體 GPIO,這樣我們定義變數的方式為:
struct _GPIO GPIOA;//定義結構體變數 GPIOA
但是這樣很繁瑣, MDK 中有很多這樣的結構體變數需要定義。這裡我們可以為結體定義一個別名 GPIO_TypeDef,這樣我們就可以在其他地方通過別名 GPIO_TypeDef 來定義結構體變量了。方法如下:
typedef struct
{
__IO uint32_t CRL;
__IO uint32_t CRH;
…
} GPIO_TypeDef;
Typedef 為結構體定義一個別名 GPIO_TypeDef,這樣我們可以通過 GPIO_TypeDef 來定義結構體變數:
GPIO_TypeDef GPIOA,GPIOB;
這裡的 GPIO_TypeDef 就跟 struct _GPIO 是等同的作用了。 這樣是不是方便很多?
除了用在結構體上,typedef類型別名也大量用在int、short等這種變數上, 所以寫STM32程式碼的時候幾乎就不會出現類似於定義int型變數這樣的語句,全部用 u8、u16這樣的量代替了,比如u16代表的就是一個無符號的16位整型資料(這一個描述可能有一點偏差)。
五、extern關鍵字:
C 語言中 extern 可以置於變數或者函式前,以表示變數或者函式的定義在別的檔案中,提示編譯器遇到此變數和函式時在其他模組中尋找其定義。這裡面要注意,對於 extern 申明變數可以多次,但定義只有一次。在我們的程式碼中你會看到看到這樣的語句:
extern u16 USART_RX_STA;
這個語句是申明 USART_RX_STA 變數在其他檔案中已經定義了,在這裡要使用到。所以,你肯定可以找到在某個地方有變數定義的語句:
u16 USART_RX_STA;
嗯,extern關鍵字,說實話,博主自己寫程式碼確實沒用過。So……