上一次課學習如何建立一個工程,還有用軟件仿真來調試程序,這節我們來學習一下在嵌入式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時,就會追溯到源地址中存儲的數據去取數據,程序就能正常執行。
未完待續,敬侯更新..............
閱讀更多 廈門蘇哥 的文章