嵌入式開發中既有底層硬件的開發又涉及上層應用的開發,即涉及系統的硬件和軟件,C語言既具有匯編語言操作底層的優勢,又具有高級語言功能性強的特點,當之無愧地成為嵌入式開發的主流語言。在 STM32開發過程中,不論是基于寄存器開發還是基于庫開發,深入理解和掌握嵌入式C語言的函數、指針、結構體是學習STM32的關鍵。嵌入式C語言的結構特點如下。
(1)程序總是從main函數開始執行,語句以分號“;”結束,采用/ … /或//做注釋。
(2)函數是C語言的基本結構,每個C語言程序均由一個或多個功能函數組成。
(3) 函數由兩部分組成:說明部分和函數體。
函數名(參數)
{
[說明部分];
函數體;
}
(4)一個C語言程序包含若干個源程序文件(.c文件)和頭文件(.h文件),其中.h頭文件主要由預處理命令(包括文件、宏定義、條件編譯等)和數據聲明(全局變量、函數等聲明)組成;c源文件主要是功能函數的實現文件。
(5)采用外設功能模塊化設計方法,一個外設功能模塊包括一個源文件(.c文件)和一個頭文件(.h文件),.c文件用于具體外設功能模塊函數的實現,.h頭文件用于對該外設功能模塊參數及功能函數的聲明。嵌入式系統開發多采用模塊化、層次化的設計思想,系統層次架構清晰,便于協同開發。圖1為嵌入式系統的軟件基本結構框圖。
圖1 嵌入式系統的軟件基本結構框架圖
1 STM32的數據類型
數據是嵌入式C語言的基本操作對象,數據類型是指數據在計算機內存中的存儲方式,如基本數據類型中的整型(存放整數)、浮點型(存放實數)、字符型(存放字符)、指針(存放地址)以及派生出的復合數據類型(如數組、結構體、共用體、枚舉類型)。嵌入式C語言的數據類型如圖2所示。
圖二 嵌入式C語言的數據類型
由于不同CPU定義的數據類型的長度不同,因此ARM公司聯合其他半導體廠商制定了統一的CMSIS 軟件標準,這個標準中預先定義了相關的數據類型,ST公司也為開發人員提供了基于C語言的標準外設庫,其定義的數據類型如表1所示,相關源代碼請參考STM32標準外設庫v3.5.0的stdint.h頭文件。stm32f10x.h頭文件還對標準外設庫之前版本所使用的數據類型進行了說明,v3.5.0版本已不再使用這些舊的數據類型,為了兼容以前的版本,新版本對其進行了兼容說明,如圖3所示。
表1 STM32定義的數據類型
圖3 STM32標準外設庫數據類型兼容說明
圖3中的_I、_O以及_IO為IO類型限定詞,內核頭文件 core_cm3.h定義了標準外設庫所使用的IO類型限定詞,如表2所示。注意,IO類型限定詞加下畫線是為了避免命名沖突。表1的數據類型與表2中的IO類型限定詞相結合,在標準外設庫中常用來定義寄存器和結構體變量,圖4為stm32f10x.h頭文件中相關外設的寄存器定義。
表2 STM32的IO類型限定詞
圖4 stm32f10x.h頭文件中相關外設的寄存器定義
結合表2和圖3,可以看出同一數據類型有多種表示方式,如無符號8位整型數據有unsigned char、uint8_t、u8三種表示方式,在不同的ST標準外設庫版本中這三種表示方式都可以表示無符號8位整型數據,初學者應了解這三種表達方式,最新的v3.5.0版本采用 CMSIS軟件標準的C99標準,即 uint8_t方式。
2 CONST關鍵字
**const關鍵字用于定義只讀的變量,其值在編譯時不能被改變,注意,const關鍵字定義的是變量而不是常量。**使用 const關鍵字是為了在編譯時防止變量的值被誤修改,同時提高程序的安全性和可靠性,一般放在頭文件中或者文件的開始部分。在C99標準中,const關鍵字定義的變量是全局變量。const 關鍵字與#definc關鍵字存在區別,#define關鍵字只是簡單的文本替換,而const關鍵字定義的變量是存儲在靜態存儲器中的。使用#define關鍵字定義常量的形式為
#define PI3.14159
使用該方式定義后,無論在何處使用PI,都會被預處理器以3.14159替代,編譯器不對PI進行類型檢查,若使用不慎,則很可能由預處理引入錯誤,且這類錯誤很難發現。用const聲明變量的方式雖然增加了分配空間,但可以很好地消除預處理引入的錯誤,并提供了良好的類型檢查形式,保證安全性。利用 const關鍵字進行編程時需要注意以下三點。**(1)使用const關鍵字聲明的變量,只能讀取,不能被賦值。**如:
const uint8t sum = 3.14;
uint8_t abs=0;
...
sum= abs;//非法,將導致編譯錯誤,因為sum 只能被讀取,不能賦值
abs- sum: //合法
(2) const關鍵詞修飾的變量在聲明時必須初始化,上述語句表示 sum值是3.14,且sum值在編譯時不能修改,若在編譯過程中直接修改sum值,則編譯器會提示出錯。(3)函數的形參聲明為const,則意味著所傳遞的指針指向的內容只能讀,不能被修改。如C語言的標準函數庫中用于統計字符串長度的函數 int strlen(const char*str)。
3 static關鍵字
在嵌入式C語言中,static關鍵字可以用來修飾變量,使用static關鍵字修飾的變量,稱為靜態變量。靜態變量的存儲方式與全局變量一樣,都是靜態存儲方式。全局變量的作用范圍是整個源程序,當一個源程序由多個源文件組成時,全局變量在各個源文件中都是有效的,即一個全局變量定義在某個源文件中,若想在另一個源文件中使用該全局變量,則只需要在該源文件中通過 extern關鍵字聲明該全局變量就可以使用了。若在該全局變量前加上關鍵字static,則該全局變量被定義成一個靜態全局變量,其作用范圍只在定義該變量的源文件內有效,其他源文件不能引用該全局變量,這樣就避免了在其他源文件中因引用相同名字的變量而引發的錯誤,有利于模塊化程序設計。利用static關鍵字進行編程時需要注意以下要點。(1)static關鍵字不僅可以用來修飾變量,而且可以用來修飾函數。模塊化程序設計中,若用static聲明一個函數,則該函數只能被該模塊內的其他函數調用,例如:
#include "stm32f1xx_hal .h”
static void DMA_SetConfig (DMA_HandleTypeDef *hdma,uint32_t SrcAddress,uint32_t DstAddress, uint32_t DataLength);
...
HAL_statusTypeDef HAL_DMA_start_IT(DMA_HandleTypeDef *hdma, uint32_t SrcAddress, uint32_t DstAddress, uint32_t DataLength)
{
HAL_StatusTypeDef status- HAL_OK;”
.... ...
if(HAL_DMA_STATE_REA.DY m- hdma->state)
{
DMA_Setconfig(hdma, SrcAddress, DstAddress, DataLength);
... ...
}
... ...
}
上述代碼為DMA模塊的源文件stm32f1xx_hal_dma.c,若利用static將DMA_SetConfig()函數聲明為一個靜態函數,則 DMA_SetConfig)函數只能被stm32flxx_hal_dma.c中的其他函數調用,而不能被其他模塊的文件使用,即定義了一個本地函數,有效避免了因其他模塊的文件定義了同名函數而引發的錯誤,充分體現了程序的模塊化設計思想。(2) static除了用于定義靜態全局變量,還用于定義靜態局部變量,保證靜態局部變量在調用過程中不被重新初始化。典型應用案例有實現計數統計功能。
void fun_count()
{
static count_num=0;
//聲明一個靜態局部變量,count_num用作計數器,初值為0
count_num++;
printf("%dn",count_num) :
}
int main(void)
(
int i=0;
for( i=0;i<=5;i++)
{
fun_count();
}
return 0;
}
在main函數中每調用一次 fun_count()函數,靜態局部變量count_num加1,而不是每次都被初始化為初值0。
4 volatile關鍵字
嵌入式開發中,常用到volatile關鍵字,它是一個類型修飾符,含義為“易變的”。使用方式如下:
volatile char i;
這里使用volatile關鍵字定義了一個字符型的變量i,指出i是隨時可能發生變化的,每次使用該變量時都必須從i的地址中讀取。由于內存的讀/寫速度遠不及CPU中寄存器的讀/寫速度,為了提高數據信息的存取速度,一方面在硬件上引入高速緩存Cache,另一方面在軟件上使用編譯器對程序進行優化,將變量的值提前從內存讀取到CPU的寄存器中,以后用到該變量時,直接從速度較快的寄存器中讀取,這樣有利于提高運算速度,但同時也可能存在風險,如該變量在內存中的值有可能被程序的其他部分(如其他線程)修改或覆蓋,而寄存器中存放的仍是之前的值,這就導致應用程序讀取的值和實際變量值不一致;也有可能是寄存器中的值發生了改變,而內存中該變量的值沒有被修改,同樣也會導致不一致的情況發生。因此,為防止由于編譯器對程序進行優化導致讀取錯誤數據,使用 volatile關鍵詞進行定義。簡單地說,使用volatile關鍵字就是不讓編譯器進行優化,即每次讀取或者修改值時,都必須重新從內存中讀取或者修改,而不是使用保存在寄存器的備份。舉個簡單的例子:大學里的獎/助學金的發放一般都是直接轉給學校,學校再發給每名學生,學校財務處都登記了每名學生的銀行卡號,但不可避免地會有一些學生因各種原因丟失銀行卡或不再使用這張銀行卡,而沒來得及去財務處重新登記,從而影響獎/助學金的發放,這里,學生就是變量的原始地址,而財務處的銀行卡號就是變量在寄存器中的備份,使用 volatile關鍵字來定義學生這個變量,這樣每次發放獎/助學金時都去找學生這個變量的原始地址,而不是直接轉到財務處保存的銀行卡上,進而避免錯誤的發生。const關鍵字的含義為“只讀”,volatile關鍵字的含義為“易變的”,但volatile關鍵字解釋為“直接存取原始內存地址”更為合適,使用 volatile關鍵字定義變量后,該變量就不會因外因而發生變化了。一般來說,volatile 關鍵字常用在以下場合。
(1)中斷服務程序中修改的、供其他程序檢測的變量需要使用volatile關鍵字。
(2)多任務環境下各任務間共享的標志應添加 volatile關鍵字。
(3)外設寄存器地址映射的硬件寄存器通常要用volatile關鍵字進行聲明。
5 extern關鍵字
extern關鍵字用于指明此函數或變量定義在其他文件中,提示編譯器遇到此函數或變量時到其他模塊中尋找其定義。這樣,extern關鍵字聲明的函數或變量就可以在本模塊或其他模塊中使用,因此,使用extern關鍵字是一個聲明而不是重新定義。使用方法如下:
extern int a;
extern int funA():
解析:第一條語句僅僅是變量a的聲明,而不是定義變量a,并未為a分配內存空間,變量a作為全局變量只能被定義一次。第二條語句聲明函數funA(),此函數已在其他文件中定義。STM32中,extern關鍵字還有一個重要作用,即與"C一起連用,即 extern "c",進行鏈接指定。例如,stm32f10x.h頭文件中有如下代碼。
#ifndef _STM32F10× H
#define _STM32F10x_H
#ifdef .epluspius
extern "C"{
#endif
...
#ifdef _eplusplus
}
"endif
這段代碼的含義是,若沒有定義_STM32F10x_H,則定義_STM32F10x H,若已經定義_cplusplus,則執行 extern "C"中語句,extern "C"是告訴C++編譯器括號中的程序代碼是按照C語言的文件格式進行編譯的,_cplusplus是C++編譯器中自定義的宏,plus是“+”的意思。C+H+支持函數重載,即在編譯時會將函數名與參數聯合起來生成一個新的中間函數名稱,而C語言不支持函數重載,這就導致在C++環境下使用C函數會出現鏈接時找不到對應函數的情況,這時就需要使用extern "C"進行鏈接指定,告知編譯器此時采用的是C語言定義的函數,需要使用C語言 的命名規則來處理函數,不要生成用于鏈接的中間函數名。 一般將函數聲明存放在頭文件中,當函數有可能被C語言或C+使用時,將函數聲明存放在 extern "C"中以免出現編譯錯誤,完整的使用方法如下:
#ifdef__cplusplus
extern "C"{
#endif
//函數聲明
#ifdef_Cplusplus
}
#endif
STM32中很多頭文件都采用這樣的用法,如標準外設庫中的 stm32f1 0x_adc.h ,stm32f10x can.h、 stm32f1Ox_gpio.h 等。利用extern 關鍵字進行編程時需要注意以下要點。嵌入式開發一般采用模塊化設計思想,因此,為保證全局變量和功能函數的使用,extern關鍵字一般用在.h頭文件中對某個模塊提供給其他模塊調用的外部函數及變量進行聲明,實際編程中只需要將該.h頭文件包含進該模塊對應的.c文件中,即在該模塊的.c文件中加入代碼#include "xxx.h”。實例如下:
6 struct結構體
**struct用于定義結構體類型,其作用是將不同數據類型的數據組合在一起,構造出一個新的數據類型。**struct一般用法如下:
struct 結構體名
{
數據類型成員名1;
數據類型成員名2;
數據類型成員名n;
};
struct Student{ //聲明結構體
char name[20]; //姓名
int num; //學號
float score; //成績
};
7 enum
有時一個變量會有幾種可能的取值,如一個星期有7天、每學期開設的課程、12種不同的顏色(紅、橙、黃、綠、青、藍、紫、灰、粉、黑、白、棕)等,C語言提供了一種enum枚舉類型,用來將變量或對象的所有可能的值一一列出,變量取值只限于列舉出來的值。enum枚舉類型的用法如下:
enum枚舉名
{
枚舉成員1,
枚舉成員2,
...
枚舉成員n;
}枚舉變量;
enum枚舉類型是一個集合,將所有可能的取值用花括號括住,花括號中的各枚舉成員之間用逗號隔開,最后一個枚舉成員后省略逗號。enum枚舉類型以分號結束,這里的枚舉變量可以省略,在后面需要時再根據枚舉名進行定義。例如,利用enum枚舉類型列舉幾種常見的顏色。
enum Color
{
RED,
GREEN,
BLACK,
YELLOw
};
上述名為 Color的枚舉類型只有4個成員:RED、GREEN、BLACK、YELLOW,即意味著Color類型變量的取值只能取這4種顏色中的某一種顏色。例如,利用enum定義一個 Weekdays枚舉類型名,包括7個枚舉成員:從星期一到星期日,并定義枚舉變量 Mydays 與 Olddays.
enumweekdays
{
Monday=1,
Tuesday,
wednesday,
Thursday,
Friday,
Saturday,
sunday
}Mydays.olddays;
注意:enum枚舉類型具有自動編號功能,第一個枚舉成員的默認值為整型的0,后續枚舉成員的值在前一個成員值上自動加1,也可以自定義枚舉成員的值,若把第一個枚舉成員的值定義為1,則第二枚舉成員的值就為2,依此類推,如上述例子中 Friday 的值為5。因此,enum枚舉類型中的枚舉成員的值是常量而不是變量,不能在程序中用賦值語句再對它賦值,但可以將枚舉值賦給枚舉變量。例如,以下兩條語句是正確的。
Mydays=Thursday;
olddays=Friday;
但以下兩條語句是錯誤的。
Tuesday=o;
Mydays=1;
8 typedef
typedef用于為復雜的聲明定義一個簡單的別名,它不是一個真正意義上的新類型。在編程中使用 typedef的目的一般有兩個:
①為變量起一個容易記憶且意義明確的新名稱;
②簡化一些比較復雜的類型聲明。
其基本格式如下: typedef類型名自定義的別名; 例如:
typedef signed char int8_t;//為數據類型signed char起別名int8_t
typedef signed int int32_t;//為數據類型signed int起別名int32_t
STM32開發中,typedef主要有以下三種用法。
8.1 typedef的基本應用
為已知的數據類型起一個簡單的別名,如上例。
8.2 typedef 與結構體struct結合使用
該用法用于自定義數據類型。如 stm32f10x_gpio.h頭文件中的GPIO初始化結構體GPIO_InitTypeDef。
typedef struct
{
uint16_t GPIO_ Pin;
GPIOSpeed_TypeDef GPIO_Speed;
GPIOMode TypeDef GPIO_Mode;
}IGPIo_InitType
上述語句利用 struct創建了一個新的結構體,這個新結構體有三個成員GPIO_Pin、GPIO_Speed和 GPIO_Mode,同時又使用 typedef為這個新建的結構體定義一個新的名稱GPIO_InitTypeDef,在應用時就可以直接使用GPIO_InitTypeDef 來定義變量。例如:
GPIO_InitTypeDef GPIO_ InitStrueture;
上述語句利用 GPIO_InitTypeDef結構體定義了一個變GPIO_InitStructure,引用三個成員的方法如下:
GPIO InitStructure.GPIO_Pin;
GPIO_InitStructure.GPIO_Speed;
GPIO InitStructure.GPIO Mode;
8.3 typedef 與enum結合使用
利用typedef關鍵字將枚舉類型定義成別名,并利用該別名進行變量聲明,STM32標準外設庫v3.5.0版本中有很多enum和 typedef結合使用的應用。stm32f10x_gpio.h頭文件中的代碼如下。
Typedef enum
{
GPIO Speed_1OMHz=1,
GPIo_Speed_2MHz,
GPIOSpeed_50MHz;
}GPIOSpeed_TypeDef;
該例中enum枚舉類型共有三個成員:GPIO Speed_10MHz、GPIO Speed_2MHz和GPIO_Speed_50MHz,并將第一個枚舉成員GPIO_Speed_10MHz賦值為1,enum枚舉類型會將枚舉成員的賦值在第一個枚舉成員賦值的基礎上加1,因此GPIO_Speed_2MHz 默認值為2,GPIO_Speed_50MHz默認值為3。同時,利用typedef關鍵字將此枚舉類型定義一個別名GPIOSpeed TypeDef,這里省略了枚舉類型的枚舉名,只用 typedef為枚舉類型定義一個別名。
9 #define
#define是C語言的預處理命令,它用于宏定義,用來將一個標識符定義為一個字符串,該標識符稱為宏名,被定義的字符串稱為替換文本,采用宏定義的目的主要是方便程序編寫,一般放在源文件的前面,稱為預處理部分。所謂預處理是指在編譯前所做的工作。預處理是C語言的一個重要功能,由預處理程序負責完成,程序編譯時,系統將自動引用預處理程序對源程序中的預處理部分進行處理,處理完畢后自動進入對源程序的編譯。STM32標準外設庫中,#define的使用方式主要有以下兩種。
**9.1 **無參數宏定義
無參數宏定義的一般形式如下:
其中,字符串可以是常數、字符串和表達式等。例如:#define UINT8_MAX 255 該語句表示定義了宏名UINT8_MAX,它代表255,例如:#define_IO volatile; 該語句表示定義宏名_IO,代表 volatile,若以后程序中再需要用到 volatile,則可以使用IO。例如:#define RCC AHBPeriph_DMA1 ((uint32_t)0x00000001) 該語句表示定義RCC_AHBPeriph_DMA1宏名,代表32位的無符號數據0x00000001.
STM32中有很多此類用法,如標準外設庫 v3.5.0的 stm32f1 0x_rcc.h文件中APB2_peripheral外設基地址的定義,如圖5所示。
圖5 APB2_peripheral各外設基地址的定義
****9.2 帶參數的宏定義
宏定義格式如下:
#define<宏名>(參數1,參數2,…,參數n)<替換列表>
例如:
define SUM(x,y) (x+y)
…
a=SUM(2,2):
其中,a的結果是4,將 SUM(X,y)定義為x+y,預編譯時會將SUM(x,y)替換為xty。例如:
#define IsGPIO_SPEED(SPEED)(((SPEED) = GP1o_Speed_10MHz)||((SPEED)==GPIO_Speed_ 2MHz)||((SPEED)==GP10_Speed_50MHz))
使用宏定義#define 將 IS_GPIO_SPEED(SPEED)替換為 GPIO_Speed_10MHz、GPIO_Speed_2MHz或者GPIO_Speed_50MHz。注意:帶參數的宏定義同樣也只是進行簡單的字符替換,替換是在編譯前進行的,展開并不分配內存單元,不進行值的傳遞處理,因此替換不會占用運行時間,只占用編譯時間,因此該方式可以提高運行效率。#define與 typedef的區別為:typedef是在編譯階段處理的,具有類型檢查的功能,而#define是在預處理階段處理的,即在編譯前,只進行簡單的字符串替換,而不進行任何檢查。
10 回調函數
回調函數是一個通過函數指針調用的函數。操作系統中的某些函數常需要調用用戶定義的函數來實現其功能,由于與常用的用戶程序調用系統函數的調用方向相反,因此將這種調用稱為回調(Callback),而被系統函數調用的函數就稱為回調函數。STM32的HAL庫在stm32flxx_hal_xxx.c文件中定義了相應的回調函數,并由中斷觸發,其實質是中斷處理程序。如 stm32flxx_hal_gpio.c代碼中通過GPIO中斷處理函數voidHAL _GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)調用相應的回調函數HAL_GPIO_EXTICallback(GPIO_Pin),開發人員只需要在回調函數中編寫應用程序就能實現中斷服務功能。
**11 ** #ifdef 、#ifndef、#else 、#if
*#define 定義一個預處理宏 *
*#undef 取消宏的義 *
*#if 編譯預處理中的條件命令,相當于C語法中的if語句 *
*#ifdef 判斷某個宏是否被定義,若已定義,執行隨后的語句 *
*#ifndef 與#ifdef相反,判斷某個宏是否未被定義*
*#elif 若#if, #ifdef, #ifndef或前面的#elif條件不滿足,則執行#elif之后的語句,相當于C語法中的else-if *
*#else 與#if, #ifdef, #ifndef對應, 若這些條件不滿足,則執行#else之后的語句,相當于C語法中的else *
*#endif 與#if, #ifdef, #ifndef這些條件命令的結束標志. *
defined 與#if, #elif配合使用,判斷某個宏是否被定義
-
嵌入式
+關注
關注
5082文章
19123瀏覽量
305151 -
硬件
+關注
關注
11文章
3328瀏覽量
66218 -
軟件
+關注
關注
69文章
4943瀏覽量
87478 -
C語言
+關注
關注
180文章
7604瀏覽量
136813 -
程序
+關注
關注
117文章
3787瀏覽量
81038
發布評論請先 登錄
相關推薦
評論