說在前面的識點話
一位初學單片機的小伙伴讓我推薦C語言書籍,因為C語言基礎比較差,初學想把C語言重新學一遍,看老再去學單片機,鳥復我以前剛學單片機的習長時候也有這樣子的想法。
其實C語言是可以邊學單片機邊學的,學單片機的一些例程中,遇到不懂的C語言知識,再去查相關的知識點,這樣印象才會深刻些。
下面就列出了一些STM32中重要的C語言知識點,初學的小伙伴可以多讀幾遍,其中大多知識點之前都有寫過,這里重新整理一下,更詳細地分析解釋可以閱讀附帶的鏈接。
assert_param
斷言(assert
)就是用于在代碼中捕捉這些假設,可以將斷言看作是異常處理的一種高級形式。
斷言表示為一些布爾表達式,程序員相信在程序中的某個特定點該表達式值為真。
可以在任何時候啟用和禁用斷言驗證,因此可以在測試時啟用斷言,而在部署時禁用斷言。同樣,程序投入運行后,最終用戶在遇到問題時可以重新啟用斷言。
注意assert()是一個宏,而不是函數。
在STM32中,常常會看到類似代碼:
assert_param(IS_ADC_ALL_INSTANCE(hadc->Instance));
assert_param(IS_ADC_SINGLE_DIFFERENTIAL(SingleDiff));
這是用來檢查函數傳入的參數的有效性。STM32中的assert_param默認是不使用的,即:
如果要使用,需要定義USE_FULL_ASSERT宏,并且需要自己實現assert_failed函數。特別的,使用STM32CubeMX生成代碼的話,會在main.c生成:
我們在這進行填充就好。
下面分享一下assert的應用例子:
//?公眾號:嵌入式大雜燴
#include?
#include?
?
int?main(void)
{
?int?a,?b,?c;
?printf("請輸入b, c的值:");
?scanf("%d?%d",?&b,?&c);
?a?=?b?/?c;
?printf("a?=?%d",?a);
?return?0;
}
此處,變量c作為分母是不能等于0,如果我們輸入2 0
,結果是什么呢?結果是程序會蹦:
這個例子中只有幾行代碼,我們很快就可以找到程序蹦的原因就是變量c的值為0。但是,如果代碼量很大,我們還能這么快的找到問題點嗎?
這時候,assert()
就派上用場了,以上代碼中,我們可以在a = b / c;
這句代碼之前加上assert(c);
這句代碼用來判斷變量c的有效性。此時,再編譯運行,得到的結果為:
可見,程序蹦的同時還會在標準錯誤流中打印一條錯誤信息:
Assertion failed:c, file hello.c, line 12
這條信息包含了一些對我們查找bug很有幫助的信息:問題出在變量c,在hello.c
文件的第12行。這么一來,我們就可以迅速的定位到問題點了。
這時候細心的朋友會發現,上邊我們對assert()的
介紹中,有這么一句說明:
如果表達式的值為假,assert()宏就會調用_assert函數在標準錯誤流中打印一條錯誤信息,并調用abort()(abort()函數的原型在stdlib.h頭文件中)函數終止程序。
所以,針對我們這個例子,我們的assert()宏
我們也可以用以下代碼來代替:
if?(0?==?c)
{
?puts("c的值不能為0,請重新輸入!");
?abort();
}
這樣,也可以給我們起到提示的作用:
但是,使用assert()
至少有幾個好處:
1)能自動標識文件和出問題的行號。
2)無需要更改代碼就能開啟或關閉assert機制(開不開啟關系到程序大小的問題)。如果認為已經排除了程序的bug,就可以把下面的宏定義寫在包含assert.h
的位置的前面:
#define?NDEBUG
并重新編譯程序,這樣編輯器就會禁用工程文件中所有的assert()語句。如果程序又出現問題,可以移除這條#define
指令(或把它注釋掉),然后重新編譯程序,這樣就可以重新啟用了assert()
語句。
相關文章:【C語言筆記】assert()怎么用?
預處理指令
1、#error
#error?"Please?select?first?the?target?STM32L4xx?device?used?in?your?application?(in?stm32l4xx.h?file)"
#error 指令讓預處理器發出一條錯誤信息,并且會中斷編譯過程。
#error的例子:
//?公眾號:嵌入式大雜燴
#include?
#define??RX_BUF_IDX??100
#if?RX_BUF_IDX?==?0
static?const?unsigned?int?rtl8139_rx_config?=?0;
#elif?RX_BUF_IDX?==?1
static?const?unsigned?int?rtl8139_rx_config?=?1;
#elif?RX_BUF_IDX?==?2
static?const?unsigned?int?rtl8139_rx_config?=?2;
#elif?RX_BUF_IDX?==?3
static?const?unsigned?int?rtl8139_rx_config?=?3;
#else
#error?"Invalid?configuration?for?8139_RXBUF_IDX"
#endif
int?main(void)
{
?printf("hello?world\n");
?return?0;
}
這段示例代碼很簡單,當RX_BUF_IDX宏的值不為0~3時,在預處理階段就會通過#error 指令輸出一條錯誤提示信息:
"Invalid configuration for 8139_RXBUF_IDX"
下面編譯看一看結果:
2、#if、#elif、#else、#endif、#ifdef、#ifndef
(1)#if
#if?(USE_HAL_ADC_REGISTER_CALLBACKS?==?1)
??void?(*?ConvCpltCallback)(struct?__ADC_HandleTypeDef?*hadc);?????????????
??//?......
#endif?/*?USE_HAL_ADC_REGISTER_CALLBACKS?*/
#if的使用一般使用格式如下
#if?整型常量表達式1
??程序段1
#elif?整型常量表達式2
??程序段2
#else
??程序段3
#endif
執行起來就是,如果整形常量表達式為真,則執行程序段1,以此類推,最后#endif是#if的結束標志。
(2)#ifdef、#ifndef
#ifdef?HAL_RTC_MODULE_ENABLED
??#include?"stm32l4xx_hal_rtc.h"
#endif?/*?HAL_RTC_MODULE_ENABLED?*/
#ifdef的作用是判斷某個宏是否定義,如果該宏已經定義則執行后面的代碼,一般使用格式如下:
#ifdef??宏名
??程序段1
#else
??程序段2
#endif
它的意思是,如果該宏已被定義過,則對程序段1進行編譯,否則對程序段2進行編譯,通#if一樣,#endif也是#ifdef的結束標志。
#ifndef?__STM32L4xx_HAL_ADC_EX_H
#define?__STM32L4xx_HAL_ADC_EX_H
//?......
#endif
#ifndef的作用與#ifdef的作用相反,用于判斷某個宏是否沒被定義。
(3)#if defined、#if !defined
defined用于判斷某個宏是否被定義, !defined與defined的作用相反。這樣一來#if defined可以達到與#ifdef一樣的效果。如例子:
#if?defined(STM32L412xx)
??#include?"stm32l412xx.h"
#elif?defined(STM32L422xx)
??#include?"stm32l422xx.h"
//........
#elif?defined(STM32L4S9xx)
??#include?"stm32l4s9xx.h"
#else
?#error?"Please?select?first?the?target?STM32L4xx?device?used?in?your?application?(in?stm32l4xx.h?file)"
#endif
如果STM32L412xx宏被定義,則包含頭文件stm32l412xx.h,以此類推。
既然已經有#ifdef、#ifndef了,#if defined與#if !defined是否是多余的?
不是的,#ifdef和#ifndef僅能一次判斷一個宏名,而defined能做到一次判斷多個宏名,例如:
#if?defined(STM32L4R5xx)?||?defined(STM32L4R7xx)?||?defined(STM32L4R9xx)?||?defined(STM32L4S5xx)?||?defined(STM32L4S7xx)?||?defined(STM32L4S9xx)
//?......
#endif?/*?STM32L4R5xx?||?STM32L4R7xx?||?STM32L4R9xx?||?STM32L4S5xx?||?STM32L4S7xx?||?STM32L4S9xx?*/
更進一步,可以構建一些更密切地因果處理,如:
#if?defined(__ARMCC_VERSION)?&&?(__ARMCC_VERSION?400677)
??#error?"Please?use?ARM?Compiler?Toolchain?V4.0.677?or?later!"
#endif
#define?PI?(3.14)
#define?R??(6)
?
#if?defined(PI)?&&?defined(R)?
#define?AREA?(PI*R*R)?
#endif
3、#pragma指令
#pragma指令為我們提供了讓編譯器執行某些特殊操作提供了一種方法。這條指令對非常大的程序或需要使用特定編譯器的特殊功能的程序非常有用。
#pragma指令的一般形式為:#pragma para
?,其中,para為參數。如
#if?defined?(?__GNUC__?)
#pragma?GCC?diagnostic?push
#pragma?GCC?diagnostic?ignored?"-Wsign-conversion"
#pragma?GCC?diagnostic?ignored?"-Wconversion"
#pragma?GCC?diagnostic?ignored?"-Wunused-parameter"
#endif
這一段的作用是忽略一些gcc的警告。#pragma命令中出現的命令集在不同的編譯器上是不一樣的,使用時必須查閱所使用的編譯器的文檔來了解有哪些命令、以及這些命令的功能。
下面簡單看一下#pragma命令的常見用法。
(1)、#pragma pack
我們可以利用#pragma pack來改變編譯器的對齊方式:
#pragma?pack(n)??/*?指定按n字節對齊?*/
#pragma?pack()???/*?取消自定義字節對齊?*/
我們使用#pragma pack指令來指定對齊的字節數。例子:
①指定按1字節對齊
運行結果為:
②指定2字節對齊
運行結果為:
可見,指定的對齊的字節數不一樣,得到的結果也不一樣。指定對齊有什么用呢,大概就是可以避免了移植過程中編譯器的差異帶來的代碼隱患吧。比如兩個編譯器的默認對齊方式不一樣,那可能會帶來一些bug。
(2)#pragma message
該指令用于在預處理過程中輸出一些有用的提示信息,如:
運行結果為:
如上,我們平時可以在一些條件編譯塊中加上類似信息,因為在一些宏選擇較多的情況下,可能會導致代碼理解起來會混亂。不過現在一些編譯器、編輯器都會對這些情況進行一些很明顯的區分了,比如哪塊代碼沒有用到,那塊代碼的背景色就會是灰色的。
(3)#pragma warning
該指令允許選擇性地修改編譯器警告信息。
例子:
#pragma?warning(?disable?:?4507?34;?once?:?4385;?error?:?164?)
等價于:
#pragma?warning(disable:4507?34)?//?不顯示4507和34號警告信息
#pragma?warning(once:4385)???????//?4385號警告信息僅報告一次
#pragma?warning(error:164)???????//?把164號警告信息作為一個錯
這個指令暫且了解這么多,知道有這么一回事就可以。
關于#pragma指令還有很多用法,但比較冷門,這里暫且不列舉,有興趣的朋友可以自行學習。
相關文章:認識認識#pragma、#error指令
extern "C"
#ifndef?__STM32L4S7xx_H
#define?__STM32L4S7xx_H
#ifdef?__cplusplus
?extern?"C"?{
#endif?/*?__cplusplus?*/
?????
#ifdef?__cplusplus
}
#endif?/*?__cplusplus?*/
#endif?/*?__STM32L4S7xx_H?*/
加上extern "C"后,會指示編譯器這部分代碼按C語言(而不是C++)的方式進行編譯。因為C、C++編譯器對函數的編譯處理是不完全相同的,尤其對于C++來說,支持函數的重載,編譯后的函數一般是以函數名和形參類型來命名的。
例如函數void fun(int, int),編譯后的可能是_fun_int_int
(不同編譯器可能不同,但都采用了類似的機制,用函數名和參數類型來命名編譯后的函數名);而C語言沒有類似的重載機制,一般是利用函數名來指明編譯后的函數名的,對應上面的函數可能會是_fun這樣的名字。
相關文章:干貨 | extern "C"的用法解析
#與##運算符
#define?__STM32_PIN(index,?gpio,?gpio_index)?\
{ ?\
index,?GPIO##gpio##_CLK_ENABLE,?GPIO##gpio,?GPIO_PIN_##gpio_index?\
}
1、#運算符
我們平時使用帶參宏時,字符串中的宏參數是沒有被替換的。例如:
輸出結果為:
然而,我們期望輸出的結果是:
5?+?20?=?25
13?+?14?=?27
這該怎么做呢?其實,C語言允許在字符串中包含宏參數。在類函數宏(帶參宏)中,#號
作為一個預處理運算符
,可以把記號轉換成字符串
。
例如,如果A是一個宏形參,那么#A就是轉換為字符串"A"的形參名。這個過程稱為字符串化(stringizing)
。以下程序演示這個過程:
輸出結果為:
這就達到我們想要的結果了。所以,#運算符
可以完成字符串化(stringizing)
的過程。
2、##運算符
與#運算符類似,##運算符
可用于類函數宏(帶參宏)的替換部分。##運算符
可以把兩個記號組合成一個記號。例如,可以這樣做:
#define?XNAME(n)?x##n
然后,宏XNAME(4)將展開x4。以下程序演示##運算符的用法:
輸出結果為:
注意:
PRINT_XN()
宏用#運算符
組合字符串,##運算符
把記號組合為一個新的標識符。
其實,##運算符
在這里看來并沒有起到多大的便利,反而會讓我們感覺到不習慣。但是,使用##運算符
有時候是可以提高封裝性及程序的可讀性的。
相關文章:這兩個C運算符你可能沒用過,但卻很有用~
_IO、 _I、 _O、volatile
一些底層結構體成員中,常常使用_IO、 _O、 _I這三個宏來修飾,如:
typedef?struct
{
??__IO?uint32_t?TIR;??/*!??__IO?uint32_t?TDTR;?/*!??__IO?uint32_t?TDLR;?/*!??__IO?uint32_t?TDHR;?/*!}?CAN_TxMailBox_TypeDef;
而這三個宏其實是volatile的替換,即:
#define?????__I?????volatile?????????????/*!#define?????__O?????volatile?????????????/*!#define?????__IO????volatile?????????????/*!
volatile的作用就是不讓編譯器進行優化,即每次讀取或者修改值的時候,都必須重新從內存或者寄存器中讀取或者修改。?在我們嵌入式中, volatile 用在如下的幾個地方:
中斷服務程序中修改的供其它程序檢測的變量需要加 volatile; 多任務環境下各任務間共享的標志應該加 volatile; 存儲器映射的硬件寄存器通常也要加 volatile 說明,因為每次對它的讀寫都可能由不 同意義;
例如:
/*?假設REG為寄存器的地址?*/
uint32?*REG;
*REG?=?0;??/*?點燈?*/
*REG?=?1;??/*?滅燈?*/
此時若是REG不加volatile
進行修飾,則點燈操作將被優化掉,只執行滅燈操作。
位操作
STM32中,使用外設都得先配置其相關寄存器,都是使用一些位操作。比如庫函數的內部實現就是一些位操作:
static?void?TI4_Config(TIM_TypeDef*?TIMx,?uint16_t?TIM_ICPolarity,?uint16_t?TIM_ICSelection,
???????????????????????uint16_t?TIM_ICFilter)
{
??uint16_t?tmpccmr2?=?0,?tmpccer?=?0,?tmp?=?0;
??/*?Disable?the?Channel?4:?Reset?the?CC4E?Bit?*/
??TIMx->CCER?&=?(uint16_t)~TIM_CCER_CC4E;
??tmpccmr2?=?TIMx->CCMR2;
??tmpccer?=?TIMx->CCER;
??tmp?=?(uint16_t)(TIM_ICPolarity?<12);
??/*?Select?the?Input?and?set?the?filter?*/
??tmpccmr2?&=?((uint16_t)~TIM_CCMR1_CC2S)?&?((uint16_t)~TIM_CCMR1_IC2F);
??tmpccmr2?|=?(uint16_t)(TIM_ICSelection?<8);
??tmpccmr2?|=?(uint16_t)(TIM_ICFilter?<12);
??/*?Select?the?Polarity?and?set?the?CC4E?Bit?*/
??tmpccer?&=?(uint16_t)~(TIM_CCER_CC4P?|?TIM_CCER_CC4NP);
??tmpccer?|=?(uint16_t)(tmp?|?(uint16_t)TIM_CCER_CC4E);
??/*?Write?to?TIMx?CCMR2?and?CCER?registers?*/
??TIMx->CCMR2?=?tmpccmr2;
??TIMx->CCER?=?tmpccer?;
}
看似很復雜,其實就是按照規格書來配置就可以。雖然實際應用中,很少會采用直接配置寄存器的方法來使用,但是也需要掌握,一些特殊的地方可以直接操控寄存器,比如中斷中。
位操作簡單例子:
首先,以下是按位運算符:
在嵌入式編程
中,常常需要對一些寄存器進行配置,有的情況下需要改變一個字節中的某一位或者幾位,但是又不想改變其它位原有的值,這時就可以使用按位運算符進行操作。下面進行舉例說明,假如有一個8位的TEST寄存器:
當我們要設置第0位bit0的值為1時,可能會這樣進行設置:
TEST?=?0x01;
但是,這樣設置是不夠準確的,因為這時候已經同時操作到了高7位:bit1~bit7
,如果這高7位沒有用到的話,這么設置沒有什么影響;但是,如果這7位正在被使用,結果就不是我們想要的了。
在這種情況下,我們就可以借用按位操作運算符進行配置。
對于二進制位操作來說,不管該位原來的值是0還是1,它跟0進行&運算,得到的結果都是0,而跟1進行&運算,將保持原來的值不變;不管該位原來的值是0還是1,它跟1進行|運算,得到的結果都是1,而跟0進行|運算,將保持原來的值不變。
所以,此時可以設置為:
TEST?=?TEST?|?0x01;
其意義為:TEST寄存器
的高7位均不變,最低位變成1了。在實際編程中,常改寫為:
TEST?|=?0x01;
這種寫法可以一定程度上簡化代碼,是 C 語言常用的一種編程風格。設置寄存器的某一位還有另一種操作方法,以上的等價方法如:
TEST?|=?(0x01?<0);
第幾位要置1就左移幾位。
同樣的,要給TEST
的低4位清0,高4位保持不變,可以進行如下配置:
TEST?&=?0xF0;
相關文章:C語言、嵌入式位操作精華技巧大匯總
do { }while(0)
這是在宏定義中用的,STM32的標準庫中沒有使用這種用法,HAL庫中有大量的用法例子,如:
#define?__HAL_FLASH_INSTRUCTION_CACHE_RESET()???do?{ ?SET_BIT(FLASH->ACR,?FLASH_ACR_ICRST);???\
?????????????????????????????????????????????????????CLEAR_BIT(FLASH->ACR,?FLASH_ACR_ICRST);?\
???????????????????????????????????????????????????}?while?(0)
下面以一個例子來分析do { }while(0)的用法:
//?公眾號:嵌入式大雜燴
#define??DEBUG???1??
#if?DEBUG
??#define?DBG_PRINTF(fmt,?args...)??\
??{ \
????printf("<>?",?__FILE__,?__LINE__,?__FUNCTION__);\
????printf(fmt,?##args);\
??}
#else
??#define?DBG_PRINTF(fmt,?args...)???
#endif
這個宏打印有什么缺陷?
我們與if、else使用的時候,會有這樣的一種使用情況:
此時會報語法錯誤。為什么呢?
同樣的,我們可以先來看一下我們的demo代碼預處理過后,相應的宏代碼會被轉換為什么。如:
這里我們可以看到,我們的if、else結構代碼被替換為如下形式:
if(c)
{ ?/*?.......?*/?};
else
{ ?/*?.......?*/?};
顯然,出現了語法錯誤。if之后的大括號之后不能加分號,這里的分號其實可以看做一條空語句,這個空語句會把if與else給分隔開來,導致else不能正確匹配到if,導致語法錯誤。
為了解決這個問題,有幾種方法。第一種方法是:把分號去掉。代碼變成:
第二種方法是:在if之后使用DBG_PRINTF打印調試時總是加{ }。代碼變成:
以上兩種方法都可以正常編譯、運行了。
但是,我們C語言中,每條語句往往以分號結尾;并且,總有些人習慣在if判斷之后只有一條語句的情況下不加大括號;而且我們創建的DBG_PRINTF宏函數的目的就是為了對標printf函數,printf函數的使用加分號在任何地方的使用都是沒有問題的。
基于這幾個原因,我們有必要再對我們的DBG_PRINTF宏函數進行一個改造。
下面引入do{ }while(0)來對我們的DBG_PRINTF進行一個簡單的改造。改造后的DBG_PRINTF宏函數如下:
#define?DBG_PRINTF(fmt,?args...)??\
do\
{ \
????printf("<>?",?__FILE__,?__LINE__,?__FUNCTION__);\
????printf(fmt,?##args);\
}while(0)
這里的do...while循環的循環體只執行一次,與不加循環是效果一樣。并且,可以避免了上面的問題。預處理文件:
我們的宏函數實體中,while(0)后面不加分號,在實際調用時補上分號,既符合了C語言語句分號結尾的習慣,也符合了do...while的語法規則。
使用do{ }while(0)來封裝宏函數可能會讓很多初學者看著不習慣,但必須承認的是,這確確實實是一種很常用的方法。
推薦文章:C語言、嵌入式中幾個非常實用的宏技巧
static與extern
1、static
static主要有三種用法:在函數內用于修飾變量、用于修飾函數、用于修飾本.c文件全局變量。后兩個容易理解,用于修飾函數與全局變量表明變量與函數在本模塊內使用。
下面看看static在函數內用于修飾變量的例子:
//?公眾號:嵌入式大雜燴
#include?
void?test(void)
{
????int?normal_var?=?0;
????static?int?static_var?=?0;
????printf("normal_var:%d??static_var:%d\n",?normal_var,?static_var);
????normal_var++;
????static_var++;
}
int?main(void)
{
?????int?i;
?????for?(?i?=?0;?i?3;?i++)
?????{
???????test();
?????}
?????return?0;
}
運行結果:
可以看出,函數每次被調用,普通局部變量都是重新分配,而static修飾的變量保持上次調用的值不變,即只被初始化一次。
2、extern
extern的用法簡單,用于聲明多個模塊共享的全局變量、聲明外部函數。
免責聲明:本文內容由21ic獲得授權后發布,版權歸原作者所有,本平臺僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯系我們,謝謝!