剛開始看STM32的庫函數,會有很多疑惑,例如指針怎么用,結構體跟指針怎么配合,例如函數的參數有什么要求,如何實時更新IO口的數據等。如果重新進行C語言的學習,那么要學很久才能夠系統地認識。本文則將比較容易想不起來的知識點進行簡單的整理。
1、#ifdef 和 #ifndef
#ifdef 標識符A// 如果標識符A定義了,就編譯程序段1,否則編譯程序段2
程序段1
#else
程序段2
#endif
#ifndef 的功能則與 #ifdef相反,是沒有定義標識符A的時候編譯程序段1。
2、全局define
在軟件的選項中,有如此一欄,在上面填寫的變量則表示在所有的文件中,上述的標識均被定義過。
#ifdef STM32F10X_HD
大容量芯片需要的一些變量定義
#end
3、extern變量申明
C語言中extern可以置於變量或者函數前,以表示變量或者函數的定義在別的文件中,提示編譯器遇到此變量和函數時在其他模塊中尋找其定義。這里面要注意,對於extern申明變量可以多次,但定義只有一次。
extern u16 USART_RX_STA;
這個語句是申明USART_RX_STA變量在其他文件中已經定義了,在這里要使用到。
下面通過一個例子說明一下使用方法。
在Main.c定義的全局變量id,id的初始化都是在Main.c里面進行的。
Main.c文件
u8 id; //定義只允許一次
main() {
id=1; printf("d%",id); //id=1
test();
printf("d%",id);//id=2
}
但是我們希望在test.c的 changeId(void)函數中使用變量id,這個時候我們就需要在test.c里面去申明變量id是外部定義的了,因為如果不申明,變量id的作用域是到不了test.c
文件中。
看下面test.c中的代碼:
extern u8 id;//申明變量id是在外部定義的,申明可以在很多個文件中進行
void test(void){ id=2; }
在test.c中申明變量id在外部定義,然后在test.c中就可以使用變量id了。
4、
typedef
類型別名
typedef用於為現有類型創建一個新的名字,或稱為類型別名,用來簡化變量的定義。typedef在MDK用得最多的就是定義結構體的類型別名和枚舉類型了。
struct _GPIO { __IO uint32_t CRL; __IO uint32_t CRH; … };
定義了一個結構體GPIO,這樣我們定義變量的方式為:
struct _GPIO GPIOA;//定義結構體變量GPIOA
但是這樣很繁瑣。這里我們可以為結體定義一個別名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是等同的作用了。
5、結構體
聲明結構體類型: Struct 結構體名
{ 成員列表; }變量名列表; 例如:
Struct U_TYPE { Int BaudRate Int WordLength; }usart1,usart2;
在結構體申明的時候可以定義變量,也可以申明之后定義,方法是:
Struct 結構體名字 結構體變量列表 ; 例如:struct U_TYPE usart1,usart2;
結構體成員變量的引用方法是:
結構體變量名字.成員名
比如要引用usart1的成員BaudRate,方法是:
usart1.BaudRate;
結構體指針變量定義也是一樣的,跟其他變量沒有啥區別。
例如:
struct U_TYPE *usart3;//定義結構體指針變量usart1;
結構體指針成員變量引用方法是通過
“->”符號實現,
比如要訪問usart3結構體指針指向的結構體的成員變量
BaudRate,方法是:
Usart3->BaudRate;
在我們單片機程序開發過程中,經常會遇到要初始化一個外設比如串口,它的初始化狀態
是由幾個屬性來決定的,比如串口號,波特率,極性,以及模式。對於這種情況,在我們沒有學習結構體的時候,我們一般的方法是: void USART_Init(u8 usartx,u32 u32 BaudRate,u8 parity,u8 mode);
這種方式是有效的同時在一定場合是可取的。但是試想,如果有一天,我們希望往這個函數里
面再傳入一個參數,那么勢必我們需要修改這個函數的定義,重新加入字長這個入口參數。但是如果我們這個函數的入口參數是隨着開發不段的增多,那么是不是我們就要不斷的修改函數的定義呢?這是不是給我們開發帶來很多的麻煩呢?那又怎樣解決這種情況呢?
這樣如果我們使用到結構體就能解決這個問題了。我們可以在不改變入口參數的情況下,只需要改變結構體的成員變量,就可以達到上面改變入口參數的目的。
我們可以將他們通過定義一個結構體來組合在一個。MDK中是這樣定義的:
typedef struct { uint32_t USART_BaudRate;
uint16_t USART_WordLength;
uint16_t USART_StopBits;
uint16_t USART_Parity;
uint16_t USART_Mode;
uint16_t USART_HardwareFlowControl; } USART_InitTypeDef;
於是,我們在初始化串口的時候入口參數就可以是USART_InitTypeDef類型的變量或者指針變量了,MDK中是這樣做的: void USART_Init(USART_TypeDef* USARTx, USART_InitTypeDef* USART_InitStruct); 這樣,任何時候,我們只需要修改結構體成員變量,往結構體中間加入新的成員變量,而不需要修改函數定義就可以達到修改入口參數同樣的目的了。
6、關於函數中結構體的參數傳遞
在ST的庫函數中,有許多結構體的用法,就像第5點中講到的一樣,用結構體封裝有利於函數的傳遞。
下面是摘抄的一些解讀,具有一定的典型性。
在ST的結構體參數傳遞中,有指針式,也有結構體地址式。
(1)用結構體變量名作為參數。
#include<iostream>
#include<string>
using namespace std;
struct Student{
string name;
int score;
};
int main(){
Student one;
void Print(Student one);
one.name="千手";
one.score=99;
Print(one);
cout<<one.name<<endl;
cout<<one.score<<endl;//驗證 score的值是否加一了
return 0;
}
void Print(Student one){
cout<<one.name<<endl;
cout<<++one.score<<endl;//在Print函數中,對score進行加一
}
這種方式值采取的“值傳遞”的方式,將結構體變量所占的內存單元的內存全部順序傳遞給形參。在函數調用期間形參也要占用內存單元。這種傳遞方式在空間和實踐上開銷較大,如果結構體的規模很大時,開銷是很客觀的。並且,由於采用值傳遞的方式,如果在函數被執行期間改變了形參的值,該值不能反映到主調函數中的對應的實參,這往往不能滿足使用要求。因此一般較少使用這種方法。
(2)用指向結構體變量的指針作為函數參數
#include<iostream>
#include<string>
using namespace std;
struct Student{
string name;
int score;
};
int main(){
Student one;
void Print(Student *p);
one.name="千手";
one.score=99;
Student *p=&one;
Print(p);
cout<<one.name<<endl;
cout<<one.score<<endl;//驗證 score的值是否加一了
return 0;
}
void Print(Student *p){
cout<<p->name<<endl;
cout<<++p->score<<endl;//在Print函數中,對score進行加一
}
這種方式雖然也是值傳遞的方式,但是這次傳遞的值卻是指針。通過改變指針指向的結構體變量的值,可以間接改變實參的值。並且,在調用函數期間,僅僅建立了一個指針變量,大大的減小了系統的開銷。
(3)用結構體變量的引用變量作函數參數
#include<iostream>
#include<string>
using namespace std;
struct Student{
string name;
int score;
};
int main(){
Student one;
void Print(Student &one);
one.name="千手";
one.score=99;
Print(one);
cout<<one.name<<endl;
cout<<one.score<<endl;//驗證 score的值是否加一了
return 0;
}
void Print(Student &one){
cout<<one.name<<endl;
cout<<++one.score<<endl;//在Print函數中,對score進行加一
}
實參是結構體變量,形參是對應的結構體類型的引用,虛實結合時傳遞的是地址,因而執行效率比較高。而且,與指針作為函數參數相比較,它看起來更加直觀易懂。因而,引用變量作為函數參數,它可以提高效率,而且保持程序良好的可讀性。
7、IMPORT 偽指令
IMPORT偽指令用於通知編譯器要使用的標號在其他的源文件中定義,但要在當前源文件中引用,而且無論當前源文件是否引用該標號,該標號均會被加入到當前源文件的符號表中。
在ST的工程建立當中,會有兩種方式,一種是寄存器版本,一種是固件庫版本。
寄存器版本在新建的過程中就有一些功能和文件不需要添加到。
在寄存器版本新建工程后,添加啟動文件startup_stm32f10x_hd.s (堆棧、PC初始化,向量異常地址入口初始化、調用MAIN函數),其中,教程里要求注釋掉下面幾行(綠色部分):
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT __main
;寄存器版本代碼,因為沒有用到 SystemInit 函數,所以注釋掉
;庫函數版本代碼,建議加上這里(外部必須實現 SystemInit 函數),以初始化 stm32 時鍾等。
;IMPORT SystemInit
調用SystemInit這個函數
;LDR R0, =SystemInit
;BLX R0
LDR R0, =__main
BX R0
ENDP
當報找不到 SystemInit 函數時,解決的辦法有下面三個
①在外部(其他任何.c文件里面)定義SystemInit這個函數,空函數也可以。
②把
IMPORT SystemInit
LDR R0, =SystemInit
BLX R0
這兩句話注釋掉或者去掉。
③可以添加system_stm32f10x.c這個庫文件,到工程里面,也可以解決。
但是第三種方法比較麻煩,因為如果你自己定義了一些函數,也許和system_stm32f10x.c有沖突。
8、文件的包含問題
#include操作是,若后面帶的是<>,則文件在安裝路徑中找;
若后面帶的是“”,則文件在源目錄中找。
9、Volatile 語句
變量前若有加volatile 這個關鍵字,則每當系統用到這個變量時,則必須重新讀取這個變量的值。
這種語句被大量用來描述一個對應於內存映射的輸入輸出端口,或者寄存器,如IO口的寄存器等。
如下:
int flag = 0;
void car_action ()
{
while(1)
{
if (flag) car_go( );
}
}
void car_stop( )
{
flag = 1;
}
在上述例子中,car_action 沒有更改flag 的操作,所以可能只有第一次執行car_action 才會讀取flag的值。后續都直接采用第一次讀取的值。而實際上在car_stop中,flag的值已經變化。
在這種情況下,car_action函數的執行結果就可能出錯。
但若在定義中采用 volatile int flag的寫法,則每次要識別flag時,就會追溯到源地址中存儲的數據去取數據,程序就能正常執行。