说在前面的话
一位初学单片机的小伙伴让我推荐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#includeintmain(void) { inta,b,c; printf("请输入b, c的值:"); scanf("%d%d",&b,&c); a=b/c; printf("a=%d",a); return0; }
此处,变量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的位置的前面:
#defineNDEBUG
并重新编译程序,这样编辑器就会禁用工程文件中所有的assert()语句。如果程序又出现问题,可以移除这条#define指令(或把它注释掉),然后重新编译程序,这样就可以重新启用了assert()语句。
相关文章:【C语言笔记】assert()怎么用?
预处理指令
1、#error
#error"PleaseselectfirstthetargetSTM32L4xxdeviceusedinyourapplication(instm32l4xx.hfile)"
#error 指令让预处理器发出一条错误信息,并且会中断编译过程。
#error的例子:
//公众号:嵌入式大杂烩 #include#defineRX_BUF_IDX100 #ifRX_BUF_IDX==0 staticconstunsignedintrtl8139_rx_config=0; #elifRX_BUF_IDX==1 staticconstunsignedintrtl8139_rx_config=1; #elifRX_BUF_IDX==2 staticconstunsignedintrtl8139_rx_config=2; #elifRX_BUF_IDX==3 staticconstunsignedintrtl8139_rx_config=3; #else #error"Invalidconfigurationfor8139_RXBUF_IDX" #endif intmain(void) { printf("helloworld "); return0; }
这段示例代码很简单,当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
#ifdefHAL_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一样的效果。如例子:
#ifdefined(STM32L412xx) #include"stm32l412xx.h" #elifdefined(STM32L422xx) #include"stm32l422xx.h" //........ #elifdefined(STM32L4S9xx) #include"stm32l4s9xx.h" #else #error"PleaseselectfirstthetargetSTM32L4xxdeviceusedinyourapplication(instm32l4xx.hfile)" #endif
如果STM32L412xx宏被定义,则包含头文件stm32l412xx.h,以此类推。
既然已经有#ifdef、#ifndef了,#if defined与#if !defined是否是多余的?
不是的,#ifdef和#ifndef仅能一次判断一个宏名,而defined能做到一次判断多个宏名,例如:
#ifdefined(STM32L4R5xx)||defined(STM32L4R7xx)||defined(STM32L4R9xx)||defined(STM32L4S5xx)||defined(STM32L4S7xx)||defined(STM32L4S9xx) //...... #endif/*STM32L4R5xx||STM32L4R7xx||STM32L4R9xx||STM32L4S5xx||STM32L4S7xx||STM32L4S9xx*/
更进一步,可以构建一些更密切地因果处理,如:
#ifdefined(__ARMCC_VERSION)&&(__ARMCC_VERSION#definePI(3.14) #defineR(6) #ifdefined(PI)&&defined(R) #defineAREA(PI*R*R) #endif
3、#pragma指令
#pragma指令为我们提供了让编译器执行某些特殊操作提供了一种方法。这条指令对非常大的程序或需要使用特定编译器的特殊功能的程序非常有用。
#pragma指令的一般形式为:#pragma para,其中,para为参数。如
#ifdefined(__GNUC__) #pragmaGCCdiagnosticpush #pragmaGCCdiagnosticignored"-Wsign-conversion" #pragmaGCCdiagnosticignored"-Wconversion" #pragmaGCCdiagnosticignored"-Wunused-parameter" #endif
这一段的作用是忽略一些gcc的警告。#pragma命令中出现的命令集在不同的编译器上是不一样的,使用时必须查阅所使用的编译器的文档来了解有哪些命令、以及这些命令的功能。
下面简单看一下#pragma命令的常见用法。
(1)、#pragma pack
我们可以利用#pragma pack来改变编译器的对齐方式:
#pragmapack(n)/*指定按n字节对齐*/ #pragmapack()/*取消自定义字节对齐*/
我们使用#pragma pack指令来指定对齐的字节数。例子:
①指定按1字节对齐
运行结果为:
②指定2字节对齐
运行结果为:
可见,指定的对齐的字节数不一样,得到的结果也不一样。指定对齐有什么用呢,大概就是可以避免了移植过程中编译器的差异带来的代码隐患吧。比如两个编译器的默认对齐方式不一样,那可能会带来一些bug。
(2)#pragma message
该指令用于在预处理过程中输出一些有用的提示信息,如:
运行结果为:
如上,我们平时可以在一些条件编译块中加上类似信息,因为在一些宏选择较多的情况下,可能会导致代码理解起来会混乱。不过现在一些编译器、编辑器都会对这些情况进行一些很明显的区分了,比如哪块代码没有用到,那块代码的背景色就会是灰色的。
(3)#pragma warning
该指令允许选择性地修改编译器警告信息。
例子:
#pragmawarning(disable:450734;once:4385;error:164)
等价于:
#pragmawarning(disable:450734)//不显示4507和34号警告信息 #pragmawarning(once:4385)//4385号警告信息仅报告一次 #pragmawarning(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、##运算符
与#运算符类似,##运算符可用于类函数宏(带参宏)的替换部分。##运算符可以把两个记号组合成一个记号。例如,可以这样做:
#defineXNAME(n)x##n
然后,宏XNAME(4)将展开x4。以下程序演示##运算符的用法:
输出结果为:
注意:PRINT_XN()宏用#运算符组合字符串,##运算符把记号组合为一个新的标识符。
其实,##运算符在这里看来并没有起到多大的便利,反而会让我们感觉到不习惯。但是,使用##运算符有时候是可以提高封装性及程序的可读性的。
相关文章:这两个C运算符你可能没用过,但却很有用~
_IO、 _I、 _O、volatile
一些底层结构体成员中,常常使用_IO、 _O、 _I这三个宏来修饰,如:
typedefstruct { __IOuint32_tTIR;/*!
而这三个宏其实是volatile的替换,即:
#define__Ivolatile/*!
volatile的作用就是不让编译器进行优化,即每次读取或者修改值的时候,都必须重新从内存或者寄存器中读取或者修改。在我们嵌入式中, volatile 用在如下的几个地方:
中断服务程序中修改的供其它程序检测的变量需要加 volatile;
多任务环境下各任务间共享的标志应该加 volatile;
存储器映射的硬件寄存器通常也要加 volatile 说明,因为每次对它的读写都可能由不 同意义;
例如:
/*假设REG为寄存器的地址*/ uint32*REG; *REG=0;/*点灯*/ *REG=1;/*灭灯*/
此时若是REG不加volatile进行修饰,则点灯操作将被优化掉,只执行灭灯操作。
位操作
STM32中,使用外设都得先配置其相关寄存器,都是使用一些位操作。比如库函数的内部实现就是一些位操作:
staticvoidTI4_Config(TIM_TypeDef*TIMx,uint16_tTIM_ICPolarity,uint16_tTIM_ICSelection, uint16_tTIM_ICFilter) { uint16_ttmpccmr2=0,tmpccer=0,tmp=0; /*DisabletheChannel4:ResettheCC4EBit*/ TIMx->CCER&=(uint16_t)~TIM_CCER_CC4E; tmpccmr2=TIMx->CCMR2; tmpccer=TIMx->CCER; tmp=(uint16_t)(TIM_ICPolarity<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<
第几位要置1就左移几位。
同样的,要给TEST的低4位清0,高4位保持不变,可以进行如下配置:
TEST&=0xF0;
相关文章:C语言、嵌入式位操作精华技巧大汇总