STM32 編程的C語言基礎


       
剛開始看STM32的庫函數,會有很多疑惑,例如指針怎么用,結構體跟指針怎么配合,例如函數的參數有什么要求,如何實時更新IO口的數據等。如果重新進行C語言的學習,那么要學很久才能夠系統地認識。本文則將比較容易想不起來的知識點進行簡單的整理。
 

1、#ifdef  和 #ifndef

#ifdef 標識符A// 如果標識符A定義了,就編譯程序段1,否則編譯程序段2  
  程序段1  
#else  
  程序段2  
#endif 
 
#ifndef  的功能則與 #ifdef相反,是沒有定義標識符A的時候編譯程序段1。
 
2、全局define
      在軟件的選項中,有如此一欄,在上面填寫的變量則表示在所有的文件中,上述的標識均被定義過。
 
       STM32 編程的C語言基礎 - lnleelove - 第一目標香港

 

#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用於為現有類型創建一個新的名字,或稱為類型別名,用來簡化變量的定義。typedefMDK用得最多的就是定義結構體的類型別名和枚舉類型了。

      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進行加一 
}
 
STM32 編程的C語言基礎 - lnleelove - 第一目標香港
 
這種方式值采取的“值傳遞”的方式,將結構體變量所占的內存單元的內存全部順序傳遞給形參。在函數調用期間形參也要占用內存單元。這種傳遞方式在空間和實踐上開銷較大,如果結構體的規模很大時,開銷是很客觀的。並且,由於采用值傳遞的方式,如果在函數被執行期間改變了形參的值,該值不能反映到主調函數中的對應的實參,這往往不能滿足使用要求。因此一般較少使用這種方法。
 
(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進行加一 
}
 
STM32 編程的C語言基礎 - lnleelove - 第一目標香港
 
 
這種方式雖然也是值傳遞的方式,但是這次傳遞的值卻是指針。通過改變指針指向的結構體變量的值,可以間接改變實參的值。並且,在調用函數期間,僅僅建立了一個指針變量,大大的減小了系統的開銷。
 
(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進行加一 
}
 
STM32 編程的C語言基礎 - lnleelove - 第一目標香港
 
 實參是結構體變量,形參是對應的結構體類型的引用,虛實結合時傳遞的是地址,因而執行效率比較高。而且,與指針作為函數參數相比較,它看起來更加直觀易懂。因而,引用變量作為函數參數,它可以提高效率,而且保持程序良好的可讀性。

 

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM