你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

几个C语言和嵌入式中实用的宏技巧

[复制链接]
gaosmile 发布时间:2020-5-20 14:11
宏打印函数

在我们的嵌入式开发中,使用printf打印一些信息是一种常用的调试手段。但是,在打印的信息量比较多的时候,就比较难知道哪些信息在哪个函数里进行打印。

特别是对于异常情况的打印,我们需要快速定位到异常情况的位置。

这时候我们可以使用宏定义来封装一个宏打印函数,这个宏打印函数可以显示打印信息所在的文件、行数、函数名等信息。如:

#define DBG_PRINTF(fmt, args...)  \
+ l2 C/ d9 e7 @* A" n{\
& P" u- U9 v% I" i7 k# _ printf("<<File:%s  Line:%d  Function:%s>> ", __FILE__, __LINE__, __FUNCTION__);\# i+ m/ w, u2 y4 g6 F% j
printf(fmt, ##args);\7 w. u2 @( p) [9 {3 P0 I  _
}
5 X. n" N3 C5 d1 x4 B# |9 @+ W


9 `. u* }+ \( G5 A: Q6 H9 ]

使用范例:

微信图片_20200520140710.png
微信图片_20200520140713.png

8 q; s! J) W. A( {0 W( Q

可见,使用方法与printf的使用方法一样,而且每条打印语句开头都会打印调试信息所在的文件名、行号、函数名信息,方便我们查找一些调试信息。

其中,__FILE__、__LINE__、__FUNCTION__这三个宏是编译器内置宏定义,分别代表调试信息所在文件、行号、函数。

除此之外,常用的宏还有:__DATE__、__TIME__,分别代表当前的编译日期与时间。如:

DBG_PRINTF("Compile Time: %s  %s\n", __DATE__, __TIME__);$ J) s, j" A* k+ `' k6 M2 z' x6 `
微信图片_20200520140717.png


% ^8 C/ S1 h* ~

第二条printf中的##符号是为了处理args不代表任何参数的情况。如:

DBG_PRINTF("Hello world");- h4 U8 D2 L2 B( T

当不加##符号是,以上宏的第二条语句被拓展为:

printf("Hello world\n", );
7 d( a- r: z" E# S! ~- n& y

可见,多出了一个逗号,这个逗号是多余的。

加上##符号后,以上宏的第二条语句被拓展为:

printf("Hello world\n");: \' M' v( |' f* ^; t3 p

这才是我们想要的结果。其实这些结果我们通过查看预处理文件可以清晰的知道:

微信图片_20200520140720.png
微信图片_20200520140724.png
微信图片_20200520140727.png

- u" K8 B4 K7 ~( @

最后需要注意的是,这个DBG_PRINTF还是与printf不一样的。DBG_PRINTF宏是两条语句的组合,无返回值;而printf的原型是:

int printf (const char *__format, ...): \* J$ e) Z) @) ^# s2 }5 g

但是我们一般都很少使用printf的返回值,所以DBG_PRINTF的用法与printf函数基本一致。

打印调试宏开关

通常情况下,一些打印调试信息只是在我们调试阶段需要的,在程序发布阶段是不需要的。

所以,为了避免打印调试信息带来的资源开销,我们可以把这些打印调试语句给注释掉。

一种方法是逐句进行注释,这是一种比较低效的方法。比较高效的方法就是添加调试宏开关,利用条件编译来选择打印/不打印调试信息。

比如我们可以把上面的代码改造为:

#define  DEBUG   1  
" O8 {) A* V) C8 ]6 j: Y: A" d1 G; r5 a- s. P9 ?, R
#if DEBUG
5 e* w4 e9 `0 Z- w0 U5 E  #define DBG_PRINTF(fmt, args...)  \
/ S4 q& ?% e/ `: E# G  {\
% Y6 ^0 q& i( t1 M) L$ h    printf("<<File:%s  Line:%d  Function:%s>> ", __FILE__, __LINE__, __FUNCTION__);\
: z9 b) w( a& {    printf(fmt, ##args);\
# \5 E+ I0 ?8 }4 l; R7 e  }

7 }8 p6 g6 _" s5 \* u( D#else% _, m  w) v: o- ^0 Q0 b* n" W
  #define DBG_PRINTF(fmt, args...)   
2 v7 E" Q/ _$ d3 w, J#endif

9 N) K# S* e, C

根据DEBUG宏的值来选择对应的打印宏函数。当DEBUG的值为1时启动相关的打印调试语句,DEBUG的值为0时则关闭打印调试语句。

这样我们就可以很方便的通过设置DEBUG宏的值来启动与关闭我们整个工程的DBG_PRINTF打印调试信息。

do{}while(0)

其实,上面我们封装的打印宏DBG_PRINTF还有一点缺陷,比如我们与if、else使用的时候,会有这样的一种使用情况:

微信图片_20200520140730.png


7 s" g/ s" C6 I3 c% J' `* d6 g

此时会报语法错误。为什么呢?

同样的,我们可以先来看一下我们的demo代码预处理过后,相应的宏代码会被转换为什么。如:

微信图片_20200520140732.png


" {- k% E  Z( E' i  j# f4 P

这里我们可以看到,我们的if、else结构代码被替换为如下形式:

if(c)
  j5 b( f6 F) B$ d1 A9 L7 Q{ /* ....... */ };. A1 D* F  y% u0 M; q' M
else
/ l# b* O* w% A0 v2 Z; G{ /* ....... */ };
6 Y  x6 c9 ^; ^$ X

显然,出现了语法错误。if之后的大括号之后不能加分号,这里的分号其实可以看做一条空语句,这个空语句会把if与else给分隔开来,导致else不能正确匹配到if,导致语法错误。

为了解决这个问题,有几种方法。第一种方法是:把分号去掉。代码变成:

微信图片_20200520140735.png

/ i3 @1 \+ i( l7 z& ~

第二种方法是:在if之后使用DBG_PRINTF打印调试时总是加{}。代码变成:

微信图片_20200520140738.png

, m8 ?$ L$ O( H  y3 h  r  i& @* |

以上两种方法都可以正常编译、运行了。

但是,我们C语言中,每条语句往往以分号结尾;并且,总有些人习惯在if判断之后只有一条语句的情况下不加大括号;而且我们创建的DBG_PRINTF宏函数的目的就是为了对标printf函数,printf函数的使用加分号在任何地方的使用都是没有问题的。

基于这几个原因,我们有必要再对我们的DBG_PRINTF宏函数进行一个改造。

下面引入do{}while(0)来对我们的DBG_PRINTF进行一个简单的改造。改造后的DBG_PRINTF宏函数如下:

#define DBG_PRINTF(fmt, args...)  \
( k8 b; [* ^( ^, O. O- vdo\
; d, E2 c& T" T- s0 L{\
, R1 m' U  F2 \9 z    printf("<<File:%s  Line:%d  Function:%s>> ", __FILE__, __LINE__, __FUNCTION__);\
7 L! Z) E( J- Q+ z( q    printf(fmt, ##args);\
4 j4 l; d: y7 ?$ C% b; d}while(0)
3 n- q, v5 }0 ]0 X9 |

这里的do...while循环的循环体只执行一次,与不加循环是效果一样。并且,可以避免了上面的问题。预处理文件:

微信图片_20200520140741.png


# u# _5 B0 P+ X$ X( |& H6 r# \

我们的宏函数实体中,while(0)后面不加分号,在实际调用时补上分号,既符合了C语言语句分号结尾的习惯,也符合了do...while的语法规则。

使用do{}while(0)来封装宏函数可能会让很多初学者看着不习惯,但必须承认的是,这确确实实是一种很常用的方法。

在STM32的HAL库中搜索while(0):

微信图片_20200520140745.png
微信图片_20200520140747.png


  A6 R9 J. n8 }+ r6 E

在Linux源码中搜索while(0):

微信图片_20200520140751.png


5 D: X6 e- p4 E6 C& l2 [" G* t. I

可见,在实际应用中,do{}while(0)用的很多。

#运算符与##运算符

这两个运算符之前也有分享过,这里顺便也提一下。

#号作为一个预处理运算符,可以把记号转换成字符串。

例如,如果A是一个宏形参,那么#A就是转换为字符串"A"的形参名。这个过程称为字符串化(stringizing)。以下程序演示这个过程:

微信图片_20200520140754.jpg
微信图片_20200520140758.jpg


. _' o9 Z! J+ k

##运算符可以把两个记号组合成一个记号。以下程序演示这个过程:

微信图片_20200520140801.jpg
微信图片_20200520140804.jpg


, U3 Q5 y( L6 g, ]% P, D; Q8 F

这个运算符用得很多。如:

微信图片_20200520140807.png

3 ^& w, g5 E. M
赞 收藏 1 评论1 发布时间:2020-5-20 14:11

举报

1个回答
陌路夕颜 回答时间:2020-5-20 14:28:50
很全面
关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
人形机器人运动控制、感知与智能配电
半导体创新技术与应用方向
EE架构与软件定义汽车
12V/48V 汽车智能配电(SPD)
区域控制单元(ZCU)与分区架构
关注我们
st-img 微信公众号
st-img 手机版