說在前面的話
一位初學(xué)單片機的小伙伴讓我推薦 C 語言書籍,因為 C 語言基礎(chǔ)比較差,想把 C 語言重新學(xué)一遍,再去學(xué)單片機,我以前剛學(xué)單片機的時候也有這樣子的想法。
其實 C 語言是可以邊學(xué)單片機邊學(xué)的,學(xué)單片機的一些例程中,遇到不懂的 C 語言知識,再去查相關(guān)的知識點,這樣印象才會深刻些。
下面就列出了一些 STM32 中重要的 C 語言知識點,初學(xué)的小伙伴可以多讀幾遍,其中大多知識點之前都有寫過,這里重新整理一下,更詳細地分析解釋可以閱讀附帶的鏈接。
assert_param
斷言(assert
)就是用于在代碼中捕捉這些假設(shè),可以將斷言看作是異常處理的一種高級形式。
斷言表示為一些布爾表達式,程序員相信在程序中的某個特定點該表達式值為真。
可以在任何時候啟用和禁用斷言驗證,因此可以在測試時啟用斷言,而在部署時禁用斷言。同樣,程序投入運行后,最終用戶在遇到問題時可以重新啟用斷言。
注意 assert()是一個宏,而不是函數(shù)。
在 STM32 中,常常會看到類似代碼:
assert_param(IS_ADC_ALL_INSTANCE(hadc->Instance));
assert_param(IS_ADC_SINGLE_DIFFERENTIAL(SingleDiff));
這是用來檢查函數(shù)傳入的參數(shù)的有效性。STM32 中的 assert_param 默認是不使用的,即:
如果要使用,需要定義 USE_FULL_ASSERT 宏,并且需要自己實現(xiàn) assert_failed 函數(shù)。特別的,使用 STM32CubeMX 生成代碼的話,會在 main.c 生成:
我們在這進行填充就好。
下面分享一下 assert 的應(yīng)用例子:
// 公眾號:嵌入式大雜燴
#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
,結(jié)果是什么呢?結(jié)果是程序會蹦:
這個例子中只有幾行代碼,我們很快就可以找到程序蹦的原因就是變量 c 的值為 0。但是,如果代碼量很大,我們還能這么快的找到問題點嗎?
這時候,assert()
就派上用場了,以上代碼中,我們可以在a = b / c;
這句代碼之前加上assert(c);
這句代碼用來判斷變量 c 的有效性。此時,再編譯運行,得到的結(jié)果為:
可見,程序蹦的同時還會在標(biāo)準(zhǔn)錯誤流中打印一條錯誤信息:
Assertion failed:c, file hello.c, line 12
這條信息包含了一些對我們查找 bug 很有幫助的信息:問題出在變量 c,在hello.c
文件的第 12 行。這么一來,我們就可以迅速的定位到問題點了。
這時候細心的朋友會發(fā)現(xiàn),上邊我們對assert()的
介紹中,有這么一句說明:
如果表達式的值為假,assert()宏就會調(diào)用 _assert 函數(shù)在標(biāo)準(zhǔn)錯誤流中打印一條錯誤信息,并調(diào)用 abort()(abort()函數(shù)的原型在 stdlib.h 頭文件中)函數(shù)終止程序。
所以,針對我們這個例子,我們的assert()宏
我們也可以用以下代碼來代替:
if (0 == c)
{
puts("c 的值不能為 0,請重新輸入!");
abort();
}
這樣,也可以給我們起到提示的作用:
但是,使用assert()
至少有幾個好處:
1)能自動標(biāo)識文件和出問題的行號。
2)無需要更改代碼就能開啟或關(guān)閉 assert 機制(開不開啟關(guān)系到程序大小的問題)。如果認為已經(jīng)排除了程序的 bug,就可以把下面的宏定義寫在包含assert.h
的位置的前面:
#define NDEBUG
并重新編譯程序,這樣編輯器就會禁用工程文件中所有的 assert()語句。如果程序又出現(xiàn)問題,可以移除這條#define
指令(或把它注釋掉),然后重新編譯程序,這樣就可以重新啟用了assert()
語句。
相關(guān)文章:【C 語言筆記】assert()怎么用?
預(yù)處理指令
1、#error
#error "Please select first the target STM32L4xx device used in your application (in stm32l4xx.h file)"
#error 指令讓預(yù)處理器發(fā)出一條錯誤信息,并且會中斷編譯過程。
#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 worldn");
return 0;
}
這段示例代碼很簡單,當(dāng) RX_BUF_IDX 宏的值不為 0~3 時,在預(yù)處理階段就會通過#error 指令輸出一條錯誤提示信息:
"Invalid configuration for 8139_RXBUF_IDX"
下面編譯看一看結(jié)果:
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
執(zhí)行起來就是,如果整形常量表達式為真,則執(zhí)行程序段 1,以此類推,最后#endif 是#if 的結(jié)束標(biāo)志。
(2)#ifdef、#ifndef
#ifdef HAL_RTC_MODULE_ENABLED
#include "stm32l4xx_hal_rtc.h"
#endif /* HAL_RTC_MODULE_ENABLED */
#ifdef 的作用是判斷某個宏是否定義,如果該宏已經(jīng)定義則執(zhí)行后面的代碼,一般使用格式如下:
#ifdef 宏名
程序段 1
#else
程序段 2
#endif
它的意思是,如果該宏已被定義過,則對程序段 1 進行編譯,否則對程序段 2 進行編譯,通#if 一樣,#endif 也是#ifdef 的結(jié)束標(biāo)志。
#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,以此類推。
既然已經(jīng)有#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 */
更進一步,可以構(gòu)建一些更密切地因果處理,如:
#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 指令為我們提供了讓編譯器執(zhí)行某些特殊操作提供了一種方法。這條指令對非常大的程序或需要使用特定編譯器的特殊功能的程序非常有用。
#pragma 指令的一般形式為:#pragma para
,其中,para 為參數(shù)。如
#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 命令中出現(xiàn)的命令集在不同的編譯器上是不一樣的,使用時必須查閱所使用的編譯器的文檔來了解有哪些命令、以及這些命令的功能。
下面簡單看一下#pragma 命令的常見用法。
(1)、#pragma pack
我們可以利用#pragma pack 來改變編譯器的對齊方式:
#pragma pack(n) /* 指定按 n 字節(jié)對齊 */
#pragma pack() /* 取消自定義字節(jié)對齊 */
我們使用#pragma pack 指令來指定對齊的字節(jié)數(shù)。例子:
①指定按 1 字節(jié)對齊
運行結(jié)果為:
②指定 2 字節(jié)對齊
運行結(jié)果為:
可見,指定的對齊的字節(jié)數(shù)不一樣,得到的結(jié)果也不一樣。指定對齊有什么用呢,大概就是可以避免了移植過程中編譯器的差異帶來的代碼隱患吧。比如兩個編譯器的默認對齊方式不一樣,那可能會帶來一些 bug。
(2)#pragma message
該指令用于在預(yù)處理過程中輸出一些有用的提示信息,如:
運行結(jié)果為:
如上,我們平時可以在一些條件編譯塊中加上類似信息,因為在一些宏選擇較多的情況下,可能會導(dǎo)致代碼理解起來會混亂。不過現(xiàn)在一些編譯器、編輯器都會對這些情況進行一些很明顯的區(qū)分了,比如哪塊代碼沒有用到,那塊代碼的背景色就會是灰色的。
(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 號警告信息作為一個錯
這個指令暫且了解這么多,知道有這么一回事就可以。
關(guān)于#pragma 指令還有很多用法,但比較冷門,這里暫且不列舉,有興趣的朋友可以自行學(xué)習(xí)。
相關(guān)文章:認識認識#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++編譯器對函數(shù)的編譯處理是不完全相同的,尤其對于 C++來說,支持函數(shù)的重載,編譯后的函數(shù)一般是以函數(shù)名和形參類型來命名的。
例如函數(shù) void fun(int, int),編譯后的可能是_fun_int_int
(不同編譯器可能不同,但都采用了類似的機制,用函數(shù)名和參數(shù)類型來命名編譯后的函數(shù)名);而 C 語言沒有類似的重載機制,一般是利用函數(shù)名來指明編譯后的函數(shù)名的,對應(yīng)上面的函數(shù)可能會是 _fun 這樣的名字。
相關(guān)文章:干貨 | extern "C"的用法解析
#與##運算符
#define __STM32_PIN(index, gpio, gpio_index)
{
index, GPIO##gpio##_CLK_ENABLE, GPIO##gpio, GPIO_PIN_##gpio_index
}
1、#運算符
我們平時使用帶參宏時,字符串中的宏參數(shù)是沒有被替換的。例如:
輸出結(jié)果為:
然而,我們期望輸出的結(jié)果是:
5 + 20 = 25
13 + 14 = 27
這該怎么做呢?其實,C 語言允許在字符串中包含宏參數(shù)。在類函數(shù)宏(帶參宏)中,#號
作為一個預(yù)處理運算符
,可以把記號轉(zhuǎn)換成字符串
。
例如,如果 A 是一個宏形參,那么#A 就是轉(zhuǎn)換為字符串"A"的形參名。這個過程稱為字符串化(stringizing)
。以下程序演示這個過程:
輸出結(jié)果為:
這就達到我們想要的結(jié)果了。所以,#運算符
可以完成字符串化(stringizing)
的過程。
2、##運算符
與#運算符類似,##運算符
可用于類函數(shù)宏(帶參宏)的替換部分。##運算符
可以把兩個記號組合成一個記號。例如,可以這樣做:
#define XNAME(n) x##n
然后,宏 XNAME(4)將展開 x4。以下程序演示##運算符的用法:
輸出結(jié)果為:
注意:
PRINT_XN()
宏用#運算符
組合字符串,##運算符
把記號組合為一個新的標(biāo)識符。
其實,##運算符
在這里看來并沒有起到多大的便利,反而會讓我們感覺到不習(xí)慣。但是,使用##運算符
有時候是可以提高封裝性及程序的可讀性的。
相關(guān)文章:這兩個 C 運算符你可能沒用過,但卻很有用~
_IO、 _I、 _O、volatile
一些底層結(jié)構(gòu)體成員中,常常使用 _IO、 _O、 _I 這三個宏來修飾,如:
typedef struct
{
__IO uint32_t TIR; /*!< CAN TX mailbox identifier register */
__IO uint32_t TDTR; /*!< CAN mailbox data length control and time stamp register */
__IO uint32_t TDLR; /*!< CAN mailbox data low register */
__IO uint32_t TDHR; /*!< CAN mailbox data high register */
} CAN_TxMailBox_TypeDef;
而這三個宏其實是 volatile 的替換,即:
#define __I volatile /*!< Defines 'read only' permissions */
#define __O volatile /*!< Defines 'write only' permissions */
#define __IO volatile /*!< Defines 'read / write' permissions */
volatile 的作用就是不讓編譯器進行優(yōu)化,即每次讀取或者修改值的時候,都必須重新從內(nèi)存或者寄存器中讀取或者修改。 在我們嵌入式中, volatile 用在如下的幾個地方:
- 中斷服務(wù)程序中修改的供其它程序檢測的變量需要加 volatile;多任務(wù)環(huán)境下各任務(wù)間共享的標(biāo)志應(yīng)該加 volatile;存儲器映射的硬件寄存器通常也要加 volatile 說明,因為每次對它的讀寫都可能由不 同意義;
例如:
/* 假設(shè) REG 為寄存器的地址 */
uint32 *REG;
*REG = 0; /* 點燈 */
*REG = 1; /* 滅燈 */
此時若是 REG 不加volatile
進行修飾,則點燈操作將被優(yōu)化掉,只執(zhí)行滅燈操作。
位操作
STM32 中,使用外設(shè)都得先配置其相關(guān)寄存器,都是使用一些位操作。比如庫函數(shù)的內(nèi)部實現(xiàn)就是一些位操作:
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 ;
}
看似很復(fù)雜,其實就是按照規(guī)格書來配置就可以。雖然實際應(yīng)用中,很少會采用直接配置寄存器的方法來使用,但是也需要掌握,一些特殊的地方可以直接操控寄存器,比如中斷中。
位操作簡單例子:
首先,以下是按位運算符:
在嵌入式編程
中,常常需要對一些寄存器進行配置,有的情況下需要改變一個字節(jié)中的某一位或者幾位,但是又不想改變其它位原有的值,這時就可以使用按位運算符進行操作。下面進行舉例說明,假如有一個 8 位的 TEST 寄存器:
當(dāng)我們要設(shè)置第 0 位 bit0 的值為 1 時,可能會這樣進行設(shè)置:
TEST = 0x01;
但是,這樣設(shè)置是不夠準(zhǔn)確的,因為這時候已經(jīng)同時操作到了高 7 位:bit1~bit7
,如果這高 7 位沒有用到的話,這么設(shè)置沒有什么影響;但是,如果這 7 位正在被使用,結(jié)果就不是我們想要的了。
在這種情況下,我們就可以借用按位操作運算符進行配置。
對于二進制位操作來說,不管該位原來的值是 0 還是 1,它跟 0 進行&運算,得到的結(jié)果都是 0,而跟 1 進行&運算,將保持原來的值不變;不管該位原來的值是 0 還是 1,它跟 1 進行|運算,得到的結(jié)果都是 1,而跟 0 進行|運算,將保持原來的值不變。
所以,此時可以設(shè)置為:
TEST = TEST | 0x01;
其意義為:TEST 寄存器
的高 7 位均不變,最低位變成 1 了。在實際編程中,常改寫為:
TEST |= 0x01;
這種寫法可以一定程度上簡化代碼,是 C 語言常用的一種編程風(fēng)格。設(shè)置寄存器的某一位還有另一種操作方法,以上的等價方法如:
TEST |= (0x01 << 0);
第幾位要置 1 就左移幾位。
同樣的,要給TEST
的低 4 位清 0,高 4 位保持不變,可以進行如下配置:
TEST &= 0xF0;
相關(guān)文章:C 語言、嵌入式位操作精華技巧大匯總
do {}while(0)
這是在宏定義中用的,STM32 的標(biāo)準(zhǔn)庫中沒有使用這種用法,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 代碼預(yù)處理過后,相應(yīng)的宏代碼會被轉(zhuǎn)換為什么。如:
這里我們可以看到,我們的 if、else 結(jié)構(gòu)代碼被替換為如下形式:
if(c)
{ /* ....... */ };
else
{ /* ....... */ };
顯然,出現(xiàn)了語法錯誤。if 之后的大括號之后不能加分號,這里的分號其實可以看做一條空語句,這個空語句會把 if 與 else 給分隔開來,導(dǎo)致 else 不能正確匹配到 if,導(dǎo)致語法錯誤。
為了解決這個問題,有幾種方法。第一種方法是:把分號去掉。代碼變成:
第二種方法是:在 if 之后使用 DBG_PRINTF 打印調(diào)試時總是加{}。代碼變成:
以上兩種方法都可以正常編譯、運行了。
但是,我們 C 語言中,每條語句往往以分號結(jié)尾;并且,總有些人習(xí)慣在 if 判斷之后只有一條語句的情況下不加大括號;而且我們創(chuàng)建的 DBG_PRINTF 宏函數(shù)的目的就是為了對標(biāo) printf 函數(shù),printf 函數(shù)的使用加分號在任何地方的使用都是沒有問題的。
基于這幾個原因,我們有必要再對我們的 DBG_PRINTF 宏函數(shù)進行一個改造。
下面引入 do{}while(0)來對我們的 DBG_PRINTF 進行一個簡單的改造。改造后的 DBG_PRINTF 宏函數(shù)如下:
#define DBG_PRINTF(fmt, args...)
do
{
printf("<> ", __FILE__, __LINE__, __FUNCTION__);
printf(fmt, ##args);
}while(0)
這里的 do...while 循環(huán)的循環(huán)體只執(zhí)行一次,與不加循環(huán)是效果一樣。并且,可以避免了上面的問題。預(yù)處理文件:
我們的宏函數(shù)實體中,while(0)后面不加分號,在實際調(diào)用時補上分號,既符合了 C 語言語句分號結(jié)尾的習(xí)慣,也符合了 do...while 的語法規(guī)則。
使用 do{}while(0)來封裝宏函數(shù)可能會讓很多初學(xué)者看著不習(xí)慣,但必須承認的是,這確確實實是一種很常用的方法。
推薦文章:C 語言、嵌入式中幾個非常實用的宏技巧
static 與 extern
1、static
static 主要有三種用法:在函數(shù)內(nèi)用于修飾變量、用于修飾函數(shù)、用于修飾本 .c 文件全局變量。后兩個容易理解,用于修飾函數(shù)與全局變量表明變量與函數(shù)在本模塊內(nèi)使用。
下面看看 static 在函數(shù)內(nèi)用于修飾變量的例子:
// 公眾號:嵌入式大雜燴
#include
void test(void)
{
int normal_var = 0;
static int static_var = 0;
printf("normal_var:%d static_var:%dn", normal_var, static_var);
normal_var++;
static_var++;
}
int main(void)
{
int i;
for ( i = 0; i < 3; i++)
{
test();
}
return 0;
}
運行結(jié)果:
可以看出,函數(shù)每次被調(diào)用,普通局部變量都是重新分配,而 static 修飾的變量保持上次調(diào)用的值不變,即只被初始化一次。
2、extern
extern 的用法簡單,用于聲明多個模塊共享的全局變量、聲明外部函數(shù)。
最后
以上就是本次的分享,如果覺得文章不錯,轉(zhuǎn)發(fā)、在看,也是我們繼續(xù)更新的動力。