12.02 STM32 嵌入式C語言教程--第三課嵌入式C語言基礎

上一次課學習如何建立一個工程,還有用軟件仿真來調試程序,這節我們來學習一下在嵌入式c編程中一些基礎知識,大家也可以學習後自己用軟件仿真來測試一下。

1、#ifdef 和 #ifndef

#ifdef 標識符A// 如果標識符A定義了,就編譯程序段1,否則編譯程序段2

程序段1

#else

程序段2

#endif

#ifndef 的功能則與 #ifdef相反,是沒有定義標識符A的時候編譯程序段1。

2、全局define

定義一個常量,例如:

#define RAM_SIZE 1024

3、extern變量申明

C語言中extern可以置於變量或者函數前,以表示變量或者函數的定義在別的文件中,提示編譯器遇到此變量和函數時在其他模塊中尋找其定義。這裡面要注意,對於extern申明變量可以多次,但定義只有一次。

extern u16 USART_RX_STA;

這個語句是申明USART_RX_STA變量在其他文件中已經定義了,在這裡要使用到。

下面通過一個例子說明一下使用方法。

在Main.c定義的全局變量id,id的初始化都是在Main.c裡面進行的。

Main.c文件

Unsigned char cnt; //定義只允許一次

void main() {

id=1;

printf("d%",cnt); //cnt=1

test();

printf("d%"cnt); //cnt=2

}

但是我們希望在test.c的 changeId(void)函數中使用變量cnt,這個時候我們就需要在test.c裡面去申明變量cnt是外部定義的了,因為如果不申明,變量cnt的作用域是到不了test.c文件中。

看下面test.c中的代碼:

extern unsigned char cnt;//申明變量cnt是在外部定義的,申明可以在很多個文件中進行

void test(void){

cnt=2;

}

在test.c中申明變量cnt在外部定義,然後在test.c中就可以使用變量cnt了。

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<stdio.h>
#include<string>
struct Student{
char name[100];


int score;
};/<string>/<stdio.h>

void Print(Student one){
printf(“%s \\r\\n”, one.name);

printf(“%d \\r\\n”, ++one.score);

//在Print函數中,對score進行加一
}


int main(){
Student one;
strcpy( one.name,"千手");
one.score=99;
Print(one);
printf(“%s \\r\\n”, one.name);

printf(“%d \\r\\n”, one.score);


return 0;
}
這種方式值採取的“值傳遞”的方式,將結構體變量所佔的內存單元的內存全部順序傳遞給形參。在函數調用期間形參也要佔用內存單元。這種傳遞方式在空間和實踐上開銷較大,如果結構體的規模很大時,開銷是很客觀的。並且,由於採用值傳遞的方式,如果在函數被執行期間改變了形參的值,該值不能反映到主調函數中的對應的實參,這往往不能滿足使用要求。因此一般較少使用這種方法。

(2)用指向結構體變量的指針作為函數參數

#include<stdio.h>
#include<string.h>
struct Student{
string name;
int score;
};/<string.h>/<stdio.h>

void Print(Student *one){
printf(“%s \\r\\n”, one->name);

printf(“%d \\r\\n”, one->score++);

//在Print函數中,對score進行加一
}


int main(){
Student one;
one.name="千手";
one.score=99;
Student *p=&one;
Print(p);

printf(“%s \\r\\n”, one.name);

printf(“%d \\r\\n”, one.score);


return 0;
}
這種方式雖然也是值傳遞的方式,但是這次傳遞的值卻是指針。通過改變指針指向的結構體變量的值,可以間接改變實參的值。並且,在調用函數期間,僅僅建立了一個指針變量,大大的減小了系統的開銷。

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時,就會追溯到源地址中存儲的數據去取數據,程序就能正常執行。


未完待續,敬侯更新..............


STM32 嵌入式C語言教程--第三課嵌入式C語言基礎


分享到:


相關文章: