目錄
- 條件編譯
- 指標用法
- 回呼函式
- 位運算
條件編譯
可以使用預處理指令創建條件編譯,即可以使用這些指令告訴編譯器根據編譯時的條件執行或忽略代碼塊,
- #ifdef、#else和#endif指令
我們用一個示例來看這幾個指令:
#ifdef HI /* 如果用#define 定義了符號 HI,則執行下面的陳述句 */
#include <stdio.h>
#define STR "Hello world"
#else /* 如果沒有用#define 定義符號 HI,則執行下面的陳述句 */
#include "mychar.h"
#define STR "Hello China"
#endif
#ifdef指令說明,如果前處理器已定義了后面的識別符號,則執行#else或#endif指令之前的所有指令并編譯所有C代碼,如果未定義且有#elif指令,則執行#else和#endif指令之間的代碼,
#ifdef、#else和C和if else很像,兩者的主要區別在于前處理器不識別用于標記塊的花括號{},因此它使用#else(如果需要的話)和#endif(必須存在)來標記指令塊,
- #ifndef指令
#ifndef指令與#ifdef指令的用法類似,也可以和#else、#endif一起使用,但是它的邏輯和#ifdef指令相反, - #if和#elif
#if指令很想C語言中的if,#if后面緊跟整型常量運算式,如果運算式為非零,則運算式為真,可以在指令中使用C的關系運算子和邏輯運算子:
#if MAX==1
printf("1");
#endif
可以按照 if else 的形式使用#if #elif:
#if MAX==1
printf("1");
#elif MAX==2
printf("2");
#endif
條件編譯還有一個用途是讓程式更容易移植,改變檔案開頭部分的幾個關鍵的定義即可根據不同的系統設定不同的值和包含不同的檔案,
指標用法
什么是指標?從根本上看,指標是一個值為記憶體地址的變數,正如char型別變數的值是字符,int型別變數的值是整數,指標變數的值是地址,
因為計算機或者嵌入式設備的硬體指令非常依賴地址,指標在某種程度上把程式員想要表達的指令以更接近機器的方式表達,因此,使用指標的程式更有效率,尤其是指標能夠有效的處理陣列,而陣串列示法其實是在變相的使用指標,比如:陣列名是陣列首元素的地址,
要創建指標變數,首先要宣告指標變數的型別,加入想把ptr宣告為儲存int型別變數地址的指標,就要使用間接運算子*來宣告,
假設已知ptr指向bah,如下表示:
ptr = &bah;
然后使用間接運算子*找出儲存在bah中的值:value = *ptr;此運算子有時也被稱為解參考運算子,陳述句ptr=&bah;value=*ptr;放在一起的效果等效于:value=bah;
那么該如何宣告一個指標變數呢?是這樣嗎:
pointer ptr; // 不能這樣宣告一個指標變數
為什么不能這樣宣告一個指標變數呢?因為宣告指標變數時必須指定指標所指向變數的型別,不同的變數型別所占據的儲存空間是不同的,一些指標操作需要知道操作物件的大小,另外程式必須知道儲存在指定地址的資料型別,例如:
int *pi; // pi 是指向 int 型別變數的指標
char *str; // str 是指向 char 型別變數的指標
float *pf, *pg; // pf, pg 都是只想 float 型別變數的指標
型別說明符表明了指標所指向物件的型別,解參考符號*表明宣告的變數是一個指標,int *pi宣告的意思是pi是一個指標,*pi是int型別,如圖 5.3.4 所示,

這僅僅是指標的簡單使用,實際指標的世界千變萬化,豐富多彩,縱使多年C語言開發的老手,有時在面對指標的使用也會出錯,后繼者更應謹慎求索,后面將會對指標常見的應用和注意事項進行介紹,
- 指標與陣列
前面提到可以使用地址運算子&獲取變數所在的地址,而在陣列中同樣可以使用取地址運算子獲取陣列成員中任意成員的地址,例如:
int week[7] = {1, 2, 3, 4, 5, 6, 7};
int *pw;
pw = &week[2];
printf("week is: %d", *pw);
輸出的結果是:week is 3,對這段代碼的釋義參照上圖 5.3.3,
- 指標與函式
指標在函式中的使用最簡單的是作為函式的形參,比如:
int sum(int *pdata)
{
int i = 0;
int temp = 0;
for(i=0;i<10;i++) {
temp = temp + (*pdata);
pdata++; }
return temp;
}
這個例子有幾點值得講解的地方,第1點指標pdata是作為函式的形參存在,指向一個儲存int型別變數的地址;第2點指標pdata++;陳述句執行后,pdata只想的地址自增的不是1,而是int型別所占的大小,加入pdata最初的值是0,int型別占2個位元組,那么pdata++;陳述句執行后,pdata的值就變成了2,而不是1,而*pdata的值是地址2所在的值不是地址1所在的值;第3點這個函式有個危險,即函式實作的是從pdata最初指向的地址開始往后的10個int型別變數的和,假如我們這樣使用:
int data[5] = {1, 2, 3, -1, -2};
int x = sum(data);
可以看到陣列data的陣列名即陣列的首地址作為引數輸入到函式sum里,而陣列的大小只有5個int,函式sum計算的卻是10個數的和,因而就會出現地址溢位,得不到正確的結果甚至于程式跑飛,為了避免這個問題,通常的解決方法是加一個數量形參:
int sum(int *pdata, int length)
{
int i = 0;
int temp = 0;
for(i=0;i<length;i++) {
temp = temp + (*pdata);
pdata++; }
return temp;
}x = sum(data, 5);
或者給出指標范圍:
int sum(int *pStart, int *pEnd)
{
int i = 0;
int temp = 0;
int length = (pEnd - pStart)/2; // 假設一個 int 占 2 個位元組
for(i=0;i<length;i++) {
temp = temp + (*pdata);
pdata++; }
return temp;
}x = sum(data, &data[4]);
指標與函式的關系除了指標作為函式形參外還有另一個重要的應用,那邊是函式指標,比如再typedef用法章節的那個例子:
typedef void (*pFunction)(void);
在這個例子中,首先*表明pFunction是一個指標變數,其次前面的void表示這個指標變數回傳一個void型別的值,最后括號里面的void表明這個函式指標的形參是void型別的,如何使用函式指標呼叫函式呢?
看下面這個例子:
int max(int a, int b)
{
return ((a>b)?a:b);
}
int main(void) {
int (*pfun)(int, int);
int a=-1, b=2, c=0;
pfun = max;
c=pfun(a, b);
printf("max: %d", c);
return 0; }
輸出的結果是:2,
- 指標與硬體地址
指標與硬體地址的聯系在volatile用法章節的例子中驚鴻一現,沒有詳細介紹,下面做詳細說明,比如在STM32F103ZET6中內部SRAM的基地址是0x20000000,我們想對這片空間的前256個位元組寫入資料,就可以使用指標指向這個基地址,然后開始寫:
volatile unsigned char *pData = (volatile unsigned char *)(0x20000000);
int main(void) {
int i = 0;
for(i=0; i<256; i++) {
pData[i] = i+10; }
return 0; }
除了記憶體地址,還可以指向硬體外設的暫存器地址,操作方式與上述例子類似,
指標應用的基本原則:
- 首先必須要指定指標的型別;
- 如果是普通指標變數,非函式形參或者函式指標,必須要給指標變數指定地址,避免成為一個“野指標”;
回呼函式
在C語言中回呼函式是函式指標的高級應用,所謂回呼函式,一個籠統簡單的介紹就是一個被作為引數傳遞的函式,從字面上看,回呼函式的意思是:一個回去呼叫的函式,如何理解這句話呢?從邏輯上分析,要“回去”,必然存在著一個已知的目的地,然后在某一個時刻去訪問;那么回呼函式就是存在一個已知的函式體A,將這個函式體A的地址即函式名“A”(函式名即是這個函式體的函式指標,指向這個函式的地址)告知給另外某個函式B,當那個函式B執行到某一步的時候就會去執行函式A,
回呼函式的應用有很多,因之后的程式都是在STM32的HAL庫下撰寫的,因而此處我們僅從HAL庫出發來看其中的回呼函式,
我們僅以GPIO的HAL庫函式來看,檔案名“stm32f1xx_hal_gpio.c”,我們用逆分析的方法來看這個回呼函式,
首先是GPIO的回呼函式宣告:
__weak void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
可以看到其函式名是:HAL_GPIO_EXTI_Callback,形參是GPIO_Pin表示引腳號(Px0~Px15, x=A,B,C,D,E,F,G),從這個函式的名稱出發,可以大致明確這是一個引腳的外部中斷(EXTI)的回呼函式,然后大家看到前面還有個“__weak”,這是虛函式的修飾符,告訴編譯器如果用戶在其它地方用void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)重新定義了此回呼函式那么優先呼叫用戶定義的,否則呼叫這個虛函式修飾的回呼函式,
緊接著我們來看此回呼函式是在哪里被呼叫的:
void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)
{
/* EXTI line interrupt detected */
if (__HAL_GPIO_EXTI_GET_IT(GPIO_Pin) != 0x00u) {
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_Pin);
HAL_GPIO_EXTI_Callback(GPIO_Pin); } }
可以看到是在GPIO的外部中斷服務函式中被呼叫的,與前面所說的這是一個外部引腳中斷回呼函式印證一致了,
GPIO的回呼函式到此就說完了,其實STM32的HAL庫中其它大多數的外設的回呼函式基本都是如此,用戶如果設計需求,就自己重定義需求的回呼函式,然后在中斷中被呼叫,
位運算
位運算是指二進制位之間的運算,在嵌入式系統設計中,常常要處理二進制的問題,例如將某個暫存器中的某一個位置1或者值0,將資料左移5位等,常用的位運算子如表 5.3.1 所示,

-
按位與運算子(&)
參與運算的兩個運算元,每個二進制位進行“與”運算,若兩個都為1,結果為1,否者為0,例如,1011&1001,第一位都為1,結果為1;第二位都為0,結果為0;第三位一個為1,一個為0,結果為0;第四位都為1,結果為1,最后結果為1001, -
按位或運算子(|)
參與運算的兩個運算元,每個二進制位進行“或”運算,若兩個都為0,結果為1,否者為1,例如,1011 | 1001,第一位都為1,結果為1;第二位都為0,結果為0;第三位一個為1,一個為0,結果為1;第四位都為1,結果為1,最后結果為1011, -
按位取反運算子(~)
按位取反運算子用于對一個二進制數按位取反,
例如,~1011,第一位為1,取反為0;第二位為0,取反為1;第三位為1,取反為0,結果為1;第四位為1,取反為0,最后結果為0100, -
左移(<<)和右移(>>)運算子
左移(<<)運算子用于將一個數左移若干位,右移(>>)運算子用于將一個數右移若干位,例如,假設val為unsigned char型資料,對應的二進制數為10111001,若val=va<<3,表示val左移3位,然后賦值給val,左移程序中,高位移出去后被丟棄,低位補0,最后val結果為1100100;若val=val>>3,表示val右移3位,然后賦值給val,右移程序中,低位移出去后被丟棄,高位補0,最后val結果為00010111, -
清零或置1
在嵌入式中,經常使用位預算符實作清零或置1,
例如,MCU的ODR暫存器控制引腳的輸出電平高低,暫存器為32位,每位控制一個引腳的電平,假設需要控制GPIOB的1號引腳輸出電平的高低,設定該暫存器第1位為1,輸出高電平,設定該暫存器第1位為0,輸出低電平,
#define GPIOB_ODR (*(volatile unsigned int *)(0x40010C0C))
GPIOB_ODR &= ~(1<<0);
GPIOB_ODR |= (1<<0);
第一行:使用#define定義了GPIOB_ODR 對應的記憶體地址為0x40010C0C,該地址為MCU的ODR暫存器地址,
第三行:GPIOB_ODR &= ~(1<<0)實際是GPIOB_ODR = GPIOB_ODR & (1<<0),先將GPIOB_ODR和(1<<0)的進行與運算,運算結果賦值給GPIOB_ODR,1<<0的值為00000000 00000000 00000000 00000010,
再取反為11111111 11111111 11111111 11111101,則GPIO_ODR的第1位和0與運算,結果必為0,其它位和1運算,由GPIO_ODR原來的值決定結果,這就實作了,只將GPIO_ODR的第1位清0,其它位保持不變的效果,
實作了單獨控制對應引腳電平輸出低,
第四行:GPIOB_ODR |= (1<<0)實際是GPIOB_ODR = GPIOB_ODR | (1<<0),先將GPIOB_ODR和(1<<0)的進行或運算,運算結果賦值給GPIOB_ODR,1<<0的值為00000000 00000000 00000000 00000010,則GPIO_ODR的第1位和0或運算,結果必為1,其它位和0運算,由GPIO_ODR原來的值決定結果,這就實作了,只將GPIO_ODR的第1位置1,其它位保持不變的效果,實作了單獨控制對應引腳電平輸出高,
百問網技術論壇:
http://bbs.100ask.net/
百問網嵌入式視頻官網:
https://www.100ask.net/index
百問網開發板:
淘寶:https://100ask.taobao.com/
天貓:https://weidongshan.tmall.com/
技術交流群(鴻蒙開發/Linux/嵌入式/驅動/資料下載)
QQ群:869222007
單片機-嵌入式Linux交流群:
QQ群:536785813
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/271414.html
標籤:其他
