【为宏正名】本应写入教科书的“世界设定”

嵌入式资讯精选 2020-07-21 00:00

【说在前面的话】

市面上大部分C程序员对宏存在巨大的误解甚至是恐惧,并因此极力避免宏的适度使用,甚至将宏在封装中发挥正确作用的行为视作是对C语言的“背叛”——震惊之余,对于为什么大家会有这种想法的原因,我曾经一度是非常“傲慢的”,这种傲慢与某些人宣称“穷人都是因为懒所以才穷”时所表现出的那种态度并无任何本质不同——然而我错了,在闲暇之余认真看了不少经典的C语言教材后我才意识到:

不是读者普遍懒或者轻视教材中有关宏的内容,而是那些对宏来说如同“加法交换律、结合律”一样的基本规则和知识并没有认真且完整的出现在教科书中!


这是何等的“呵呵”。这下全都清楚了:
  • 为什么大家会那么惧怕宏的使用

  • 定义宏的时候,为什么遇到哪怕很基本的小问题也根本无从下手

  • 为什么那么多人声称系统提供的诸如 __LINE__ 之类的宏时好时坏

  • 为什么很多关于宏的正常使用被称为奇技淫巧……


真是哭笑不得。这些规则是如此简单,介绍一下根本无需多么复杂的篇幅。接下来,让我们简单的学习一下这些本应该写入教科书中的基本内容。注意,这与你们在其它公众号里学到的关于某些宏的基本使用方法是两回事。


【宏不属于C语言】


说“宏不属于C语言”是一种夸张的说法,但却非常反映问题的本质和基本事实:
  • C语言的编译分为三个阶段:预编译阶段、编译阶段和链接阶段。正如上图所示的那样,预编译阶段的产物是单个的“.c”文件;编译阶段将这些“.c”文件一个一个彼此独立的编译为对应的对象("*.obj")文件;这些对象文件就像乐高积木一样会在最终的链接阶段按照事先约定好的图纸(地址空间布局描述文件,又称linker script或者scatter script)被linker组装到一起,最终生成在目标机器上可以运行的镜像文件。


  • 宏仅在预编译阶段有效,它的本质只是文字替换。在完成预编译处理以后,进入编译阶段的.c实际上已经不存在任何“宏”、条件编译、“#include”以及"#pragma"之类的预编译内容——此时的C源文件是一个纯粹且独立的文本文件。很多编译器在命令行下都提供一个"-E"的选项,它其实就是告诉编译器,只进行预编译操作并停在这里。此时,编译的结果就是大家所说的“宏展开”后的内容。学会使用"-E"选项,是检测自己缩写的宏是否正确的最有效工具。


知道这一知识有什么用呢?首先,你会明白,宏本身是与C语言的其它语法毫无关联的。宏有自己的语法,且非常简单。在进行宏展开的时候,编译器并不会去进行任何宏以外的C语言语法检查、甚至根本不知道C语言语法。实际上,有大量C语言老鸟特别喜欢在其它C语言以外的文本文件里使用“宏”(其实还有条件编译之类的),最典型的例子就是在Arm Compiler 6的scatter-script中用宏来定义一些地址常数:
#! armclang --target=arm-arm-none-eabi -march=armv6-m -E -x c#define ADDRESS 0x20000000#include "include_file_1.h"LR1 ADDRESS{}

这里,第一行的命令行:

#! armclang --target=arm-arm-none-eabi -march=armv6-m -E -x c

就是告诉linker,在处理scatter-script之前要执行“#!” 后面的命令行,这里的"-E"就是告诉armclang:“我们只进行预编译”——也就是"#include"以及宏替换之类的工作——所以宏“ADDRESS” 会被替换会 0x20000000,而"include_file_1.h" 中的内容也会被加入到当前的scatter-script文件中来。



需要强调下,在这个例子中,放在第一行“#!”后面的命令行之所以为会被linker自动执行,是因为linker就是这么使用 “.sct” 文件的。对于其它想使用C语言宏对任意文本文件进行预处理的场合,需要自己动手编写命令行和脚本。比如,如果你想在 perl 里使用 C语言的预编译,那么就需要你在执行目标 .pl 文件前,先用C语言编译器对其进行一次预编译。



总的来说,“宏不属于C语言”并非空穴来风,事实上,只要你有兴趣去写脚本,包括宏在内的所有预编译语法可以在一切文本文件中使用

知道这一知识的另外一个作用就是回答每一个C语言初学者都绕不开的经典问题:“宏和枚举有啥区别”?有啥区别?这区别老大了:
  • 正如前面所说的,宏只存在于“预编译阶段”,而活不到“编译阶段”;宏是没有任何C语法意义的

  • 枚举与之相反,只存在于“编译阶段”,是具有严格的C语法意义的——它的每一个成员都明确代表一个整形常量值


其实,从宏和枚举服务的阶段看来,他们是老死不相往来的。那么具体在使用时,这里的区别表现在什么地方呢?我们来看一个例子:

#define USART_COUNT 4
#if USART_COUNT > 0extern uint8_t s_chUSARTBuffer[USART_COUNT];#endif

这里例子意图很简单,根据宏USART_COUNT的值来条件编译。如果我们把USART_COUNT换成枚举就不行了:

typedef enum {/* list all the available USART here */    USART0_idx = 0,    USART1_idx,    USART2_idx,    USART3_idx,    /* number of USARTs*/    USART_COUNT,}usart_idx_t;
#if USART_COUNT > 0extern uint8_t s_chUSARTBuffer[USART_COUNT];#endif

在这个例子里,USART_COUNT的值会随着前面列举的UARTx_idx的增加而自动增加——作为一个技巧——精确的表示当前实际有效的USART数量,从意义上说严格贴合了 USART_COUNT 这个名称的意义。这个代码看似没有问题,但实际上根据前面的知识我们知道:条件编译是在“预编译阶段”进行的、枚举是在“编译阶段”才有意义。换句话说,当下面代码判断枚举USART_COUNT的时候,预编译阶段根本不认识它是谁(预编译阶段没有任何C语言的语法知识)——这时候USART_COUNT作为枚举还没出生呢!

#if USART_COUNT > 0extern uint8_t s_chUSARTBuffer[USART_COUNT];#endif

同样道理,如果你想借助下面的宏来生成代码,得到的结果会出人意料:

typedef enum {/* list all the available USART here */ USART0_idx = 0, USART1_idx, USART2_idx, USART3_idx,/* number of USARTs*/ USART_COUNT,}usart_idx_t;
extern int usart0_init(void);extern int usart1_init(void);extern int usart2_init(void);extern int usart3_init(void);
#define USART_INIT(__USART_INDEX) \    usart##__USART_INDEX##_init()

应用中,我们期望配合UARTn_idx与宏USART_INIT一起使用:

...USART_INIT(USART1_idx);...

借助宏的胶水运算“##”,我们期望的结果是:

...usart1_init();...

由于同样的原因——在进行宏展开的时候,枚举还没有“出生”——实际展开的效果是这样的:

...usartUSART1_idx_init();...

由于函数  usartUSART1_idx_init() 并不存在,所以在链接阶段linker会报告类似“undefined symbol usartUSART1_idx_init()”——简单说就是找不到函数。要解决这一问题也很简单,直接把枚举用宏来定义就可以了:


#define USART_COUNT 4
#if USART_COUNT > 0extern int usart0_init(void);#   define USART0_idx 0#endif
#if USART_COUNT > 1extern int usart1_init(void);# define USART1_idx 1#endif
#if USART_COUNT > 2extern int usart2_init(void);# define USART2_idx 2#endif
#if USART_COUNT > 3extern int usart3_init(void);# define USART3_idx 3#endif


那么是不是说,宏就比枚举好呢?当然不是,准确的说法应该是:在谁的地盘谁的优点就突出。我们说枚举仅在编译阶段有效、它具有明确的语法意义(具体语法意义请参考相应的C语言教材)。相对宏来说,怎么理解枚举的好处呢?
  • 枚举可以被当作类型来使用,并定义枚举变量——宏做不到;

  • 当使用枚举作为函数的形参或者是switch检测的目标时,有些比较“智能”的C编译器会在编译阶段把枚举作为参考进行“强类型”检测——比如检查函数传递过程中你给的值是否是枚举中实际存在的;又比如在switch中是否所有的枚举条目都有对应的case(在省缺default的情况下)。

  • 除IAR以外,保存枚举所需的整型在一个编译环境中是相对来说较为确定的(不是short就是int)——在这种情况下,枚举的常量值就具有了类型信息,这是用宏表示常量时所不具备的。

  • 少数IDE只能对枚举进行语法提示而无法对宏进行语法提示。



【宏的本质和替换规则】

很多人都知道宏的本质是文字替换,也就是说,预编译过程中宏会被替换成对应的字符串;然而在这一过程中所遵守的关键规则,很多人就不清楚了。

首先,针对一个没有被定义过的宏:

  • 在#ifdef、#ifndef 以及 defined() 表达式中,它可以正确的返回boolean量——确切的表示它没有被定义过;

  • 在#if 中被直接使用(没有配合defined()),则很多编译器会报告warning,指出这是一个不存在的宏,同时默认它的值是boolean量的false——而并不保证是"0";

  • 在除以上情形外的其它地方使用,比如在代码中使用,则它会被作为代码的一部分原样保留到编译阶段——而不会进行任何操作;通常这会在链接阶段触发“undefined symbol”错误——这是很自然的,因为你以为你在用宏(只不过因为你忘记定义了,或者没有正确include所需的头文件),编译器却以为你在说函数或者变量——当然找不到了。


举个例子,宏 __STDC_VERSION__ 可以被用来检查当前ANSI-C的标准:

#if __STD_VERSION__ >= 199901L/* support C99 */#  define SAFE_ATOM_CODE(...)             \ {                            \ uint32_t wTemp = __disable_irq(); \      __VA_ARGS__;                       \      __set_PRIMASK(wTemp);            \ }#else/* doesn't support C99, assume C89/90 */#  define SAFE_ATOM_CODE(__CODE)         \ { \ uint32_t wTemp = __disable_irq(); \ __CODE; \ __set_PRIMASK(wTemp); \ }#endif

上述写法在支持C99的编译器中是不会有问题的,因为 __STDC_VERSION__ 一定会由编译器预先定义过;而同样的代码放到仅支持C89/90的环境中就有可能会出问题,因为 __STDC_VERSION__ 并不保证一定会被事先定义好(C89/90并没有规定要提供这个宏),因此 __STDC_VERSION__ 就有可能成为一个未定义的宏,从而触发编译器的warning。为了修正这一问题,我们需要对上述内容进行适当的修改:

#if defined(__STD_VERSION__) && __STD_VERSION__ >= 199901L/* support C99 */...#else/* doesn't support C99, assume C89/90 */...#endif


其次,定义宏的时候,如果只给了名字却没有提供内容:
  • 在#ifdef、#ifndef 以及 defined() 表达式中,它可以正确的返回boolean量——确切的表示它被定义了;

  • 在#if 中被直接使用(没有配合defined()),编译器会把它看作“空”;在一些数值表达式中,它会被默认当作“0”,没有任何警告信息会被产生

  • 在除以上情形外的其它地方使用,比如在代码中使用,编译器会把它看作“空字符串”(注意,这里不包含引号)——它不会存活到编译阶段;


最后,我们来说一个容易被人忽视的结论:
  • 第一条:任何使用到胶水运算“##”对形参进行粘合的参数宏,一定需要额外的再套一层

  • 第二条:其余情况下,如果要用到胶水运算,一定要在内部借助参数宏来完成粘合过程


为了理解这一“结论”,我们不妨举一个例子:在前面的代码中,我们定义过一个用于自动关闭中断并在完成指定操作后自动恢复原来状态的宏:
#define SAFE_ATOM_CODE(...)               \ { \ uint32_t wTemp = __disable_irq(); \ __VA_ARGS__; \ __set_PRIMASK(wTemp); \ }

由于这里定义了一个变量wTemp,而如果用户插入的代码中也使用了同名的变量,就会产生很多问题:轻则编译错误(重复定义);重则出现局部变量wTemp强行取代了用户自定义的静态变量的情况,从而直接导致系统运行出现随机性的故障(比如随机性的中断被关闭后不再恢复,或是原本应该被关闭的全局中断处于打开状态等等)。为了避免这一问题,我们往往会想自动给这个变量一个不会重复的名字,比如借助 __LINE__ 宏给这一变量加入一个后缀:

#define SAFE_ATOM_CODE(...)                \ { \      uint32_t wTemp##__LINE__ = __disable_irq();    \ __VA_ARGS__; \ __set_PRIMASK(wTemp); \ }
一个使用例子:
...SAFE_ATOM_CODE(    /* do something here */    ...)...
假设这里 SAFE_ATOM_CODE 所在行的行号是 123,那么我们期待的代码展开是这个样子的(我重新缩进过了):
...  {                                                         uint32_t wTemp123 = __disable_irq();           __VA_ARGS__;                                          __set_PRIMASK(wTemp);                            }...
然而,实际展开后的内容是这样的:
...  {                                                         uint32_t wTemp__LINE__ = __disable_irq();           __VA_ARGS__;                                          __set_PRIMASK(wTemp);                            }...
这里,__LINE__似乎并没有被正确替换为123,而是以原样的形式与wTemp粘贴到了一起——这就是很多人经常抱怨的 __LINE__ 宏不稳定的问题。实际上,这是因为上述宏的构建没有遵守前面所列举的两条结论导致的。

从内容上看,SAFE_ATOM_CODE() 要粘合的对象并不是形参,根据结论第二条,需要借助另外一个参数宏来帮忙完成这一过程。为此,我们需要引入一个专门的宏:

#define CONNECT2(__A, __B) __A##__B
注意到,这个参数宏要对形参进行胶水运算,根据结论第一条,需要在宏的外面再套一层,因此,修改代码得到:
#define __CONNECT2(__A, __B) __A##__B#define CONNECT2(__A, __B) __CONNECT2(__A, __B)
#define __CONNECT3(__A, __B, __C)    __A##__B##__C#define CONNECT2(__A, __B, __C) __CONNECT3(__A, __B, __C)
修改前面的定义得到:
#define SAFE_ATOM_CODE(...)                \ { \      uint32_t CONNECT2(wTemp,__LINE__) =              \       __disable_irq();      \ __VA_ARGS__; \ __set_PRIMASK(wTemp); \ }
有兴趣的朋友可以通过 "-E" 可以观察到 __LINE__ 被正确的展开了。

【宏是引用而非变量】

具体实践中,很多人在使用宏过程中会产生“宏是一种变量”的错觉,这是因为无论一个宏此前是否定义过,我们都可以借助 #undef 操作,强制注销它,从而有能力重新给这一宏赋予一个新的值,例如:
#include <stdbool.h>
#undef false#undef true
#define false     0#define true      (!false)

上述例子里,在stdbool.h中,true通常被定义为1,这会导致很多人在编写期望值是true的逻辑表达式时,一不小心落入圈套——因为true的真实含义是“非0”,这就包含了除了1以外的一切非0的整数,当用户写下:
if (true == xxxxx) {...}
表达式时,实际获得的是:
if (1 == xxxxx) {...}
这显然是过于狭隘的——会出现实际为true却判定为false(走else分支)的情况,为了避免这种情况,实践中,我们应该避免在逻辑表达式中使用true——无论true的值是什么。


实际上,宏的变量特性是不存在的,更确切地说法是,宏是一种“引用”。那么什么是引用呢?《六祖坛经》中有一个非常著名的公案,用于解释慧能关于“不立文字”的主张,他说,通过“文字”来了解真理,就好比用手指向月亮——正如手指可以指出明月的所在,文字也的确可以用来描述真理,但毕竟手指不是明月,文字也不是真理本身,因此如果有办法直击真理,又如何需要执着于文字(经文)本身呢?我们虽然不一定要修禅,但这里手指与明月的关系恰好可以非常生动的解释“引用”这一概念。



我们说宏的本质是一个引用,那么如何理解这种说法呢?我们来看一个例子:
#define EXAMPLE_A          123#define EXAMPLE EXAMPLE_A
#undef  EXAMPLE_A

对于下面的代码:

CONNECT2(uint32_t wVariable, EXAMPLE);

如果宏是一个变量,那么展开的结果应该是:

uint32_t wVariable123;

然而,我们实际获得的是:

uint32_t wVariableEXAMPLE_A;

如何理解这一结果呢?


如果宏是一个引用,那么当EXAMPLE_A与123之间的关系被销毁时,原本EXAMPLE > EXAMPLE_A > 123 的引用关系就只剩下 EXAMPLE > EXAMPLE_A。又由于EXAMPLE_A已经不复存在,因此EXAMPLE_A在展开时就被当作是最终的字符串,与"uint32_t wVariable"连接到了一起。



这一知识对我们有什么帮助呢?帮助实在太大了!甚至可以把预编译器直接变成一个脚本解释器。受到篇幅的限制,我们无法详细展开,就展示一个最常见的用法吧:

还记得前面定义的USART_INIT()宏么?
#define USART_INIT(__USART_INDEX) \    usart##__USART_INDEX##_init()
使用的时候,我们需要确保填写在括号中的任何内容都必须直接对应一个在效范围内的整数(比如0~3),比如:
USART_INIT(USART1_idx);
由于USART1_idx直接对应于字符串 “1”,因此,实际会被展开为:
usart1_init();

很多时候,我们可能会希望代码有更多的灵活性,因此,我们会再额外定义一个宏来将某些代码与具体的USART祛除不必要的耦合:
#include "app_cfg.h"
#ifndef DEBUG_USART# define DEBUG_USART    USART0_idx#endif
USART_INIT(DEBUG_USART);
这样,虽然代码默认使用USART0作为 DEBUG_USART,但用户完全可以通过配置文件 "app_cfg.h" 来修改这一配置。到目前为止,一切都好。但此时,app_cfg.h 中的内容已经和模块内的代码有了一定的“隔阂”——用户不一定知道 DEBUG_USART 必须是一个有效的数字字符串,而不能是一个表达式,哪怕这个表达式会“自动”计算出最终需要使用的值。比如,在 app_cfg.h 中,可能会出现以下的内容:
/* app_cfg.h */
#define USART_MASTER_CNT 1#define USART_SLAVE_CNT     2#define DEBUG_USART (USART_MASTER_CNT + USART_SLAVE_CNT)
这里,出于某种不可抗拒原因,用户希望永远使用最后一个USART作为 DEBUG_USART,并通过一个表达式计算出了这个USART的编号。遗憾的是,当用户自信满满的写下这一“智能算法”后,我们得到的实际上是:
usart(1+2)_init();
对编译器来说,这显然不是一个有效的C语法,因此报错是在所难免。那么如何解决这一问题呢?借助宏的引用特性,我们可以获得如下的内容:
#include "app_cfg.h"
#ifndef DEBUG_USART# define DEBUG_USART USART0_idx#else#   if DEBUG_USART == 0#    undef DEBUG_USART#    define DEBUG_USART    0#   elif   DEBUG_USART == 1# undef DEBUG_USART# define DEBUG_USART 1# elif DEBUG_USART == 2# undef DEBUG_USART# define DEBUG_USART 2# elif DEBUG_USART == 3# undef DEBUG_USART#       define DEBUG_USART    3#   else#   error "out of range for DEBUG_USART"#endif
进一步思考,假设一个宏的取值范围是 0~255,而我们想把这一宏的值切实的转化为对应的十进制数字字符串,按照上面的方法,那我们岂不是要累死?且慢,我们还有别的办法,假设输入数值的宏叫 MFUNC_IN_U8_DEC_VALUE 首先分别获得3位十进制的每一位上的数字内容:

#undef __MFUNC_OUT_DEC_DIGIT_TEMP0#undef __MFUNC_OUT_DEC_DIGIT_TEMP1#undef __MFUNC_OUT_DEC_DIGIT_TEMP2#undef __MFUNC_OUT_DEC_STR_TEMP
/* 获取个位 */#if (MFUNC_IN_U8_DEC_VALUE % 10) == 0# define __MFUNC_OUT_DEC_DIGIT_TEMP0 0#elif (MFUNC_IN_U8_DEC_VALUE % 10) == 1# define __MFUNC_OUT_DEC_DIGIT_TEMP0 1#elif (MFUNC_IN_U8_DEC_VALUE % 10) == 2# define __MFUNC_OUT_DEC_DIGIT_TEMP0 2#elif (MFUNC_IN_U8_DEC_VALUE % 10) == 3# define __MFUNC_OUT_DEC_DIGIT_TEMP0 3#elif (MFUNC_IN_U8_DEC_VALUE % 10) == 4# define __MFUNC_OUT_DEC_DIGIT_TEMP0 4#elif (MFUNC_IN_U8_DEC_VALUE % 10) == 5# define __MFUNC_OUT_DEC_DIGIT_TEMP0 5#elif (MFUNC_IN_U8_DEC_VALUE % 10) == 6# define __MFUNC_OUT_DEC_DIGIT_TEMP0 6#elif (MFUNC_IN_U8_DEC_VALUE % 10) == 7# define __MFUNC_OUT_DEC_DIGIT_TEMP0 7#elif (MFUNC_IN_U8_DEC_VALUE % 10) == 8# define __MFUNC_OUT_DEC_DIGIT_TEMP0 8#elif (MFUNC_IN_U8_DEC_VALUE % 10) == 9# define __MFUNC_OUT_DEC_DIGIT_TEMP0 9#endif
/* 获取十位数字 */#if ((MFUNC_IN_U8_DEC_VALUE/10) % 10) == 0# define __MFUNC_OUT_DEC_DIGIT_TEMP1 0#elif ((MFUNC_IN_U8_DEC_VALUE/10) % 10) == 1# define __MFUNC_OUT_DEC_DIGIT_TEMP1 1#elif ((MFUNC_IN_U8_DEC_VALUE/10) % 10) == 2# define __MFUNC_OUT_DEC_DIGIT_TEMP1 2#elif ((MFUNC_IN_U8_DEC_VALUE/10) % 10) == 3# define __MFUNC_OUT_DEC_DIGIT_TEMP1 3#elif ((MFUNC_IN_U8_DEC_VALUE/10) % 10) == 4# define __MFUNC_OUT_DEC_DIGIT_TEMP1 4#elif ((MFUNC_IN_U8_DEC_VALUE/10) % 10) == 5# define __MFUNC_OUT_DEC_DIGIT_TEMP1 5#elif ((MFUNC_IN_U8_DEC_VALUE/10) % 10) == 6# define __MFUNC_OUT_DEC_DIGIT_TEMP1 6#elif ((MFUNC_IN_U8_DEC_VALUE/10) % 10) == 7# define __MFUNC_OUT_DEC_DIGIT_TEMP1 7#elif ((MFUNC_IN_U8_DEC_VALUE/10) % 10) == 8# define __MFUNC_OUT_DEC_DIGIT_TEMP1 8#elif ((MFUNC_IN_U8_DEC_VALUE/10) % 10) == 9# define __MFUNC_OUT_DEC_DIGIT_TEMP1 9#endif
/* 获取百位数字 */#if ((MFUNC_IN_U8_DEC_VALUE/100) % 10) == 0# define __MFUNC_OUT_DEC_DIGIT_TEMP2 0#elif ((MFUNC_IN_U8_DEC_VALUE/100) % 10) == 1# define __MFUNC_OUT_DEC_DIGIT_TEMP2 1#elif ((MFUNC_IN_U8_DEC_VALUE/100) % 10) == 2# define __MFUNC_OUT_DEC_DIGIT_TEMP2 2#endif
接下来,我们将代表“个、十、百”的三个宏拼接起来:
#if __MFUNC_OUT_DEC_DIGIT_TEMP2 == 0 # if __MFUNC_OUT_DEC_DIGIT_TEMP1 == 0# define MFUNC_OUT_DEC_STR __MFUNC_OUT_DEC_DIGIT_TEMP0# else# define MFUNC_OUT_DEC_STR CONNECT2( __MFUNC_OUT_DEC_DIGIT_TEMP1,\ __MFUNC_OUT_DEC_DIGIT_TEMP0)# endif#else# define MFUNC_OUT_DEC_STR CONNECT3( __MFUNC_OUT_DEC_DIGIT_TEMP2,\ __MFUNC_OUT_DEC_DIGIT_TEMP1,\ __MFUNC_OUT_DEC_DIGIT_TEMP0)#endif

#undef MFUNC_IN_U8_DEC_VALUE
此时,保存在 MFUNC_OUT_U8_DEC_VALUE 中的值就是我们所需的十进制数字了。为了方便使用,我们将上述内容放置到一个专门的头文件中,就叫做mf_u8_dec2str.h (https://github.com/vsfteam/vsf/blob/master/source/vsf/utilities/preprocessor/mf_u8_dec2str.h),修改前面的例子:

#include "app_cfg.h"
#ifndef DEBUG_USART#   define DEBUG_USART    USART0_idx#endif
/* 建立脚本输入值与 DEBUG_USART 之间的引用关系*/#undef MFUNC_IN_U8_DEC_VALUE#define MFUNC_IN_U8_DEC_VALUE DEBUG_USART
/* "调用"转换脚本 */#include "mf_u8_dec2str.h"
/* 建立 DEBUG_USART 与脚本输出值之间的引用 */#undef DEBUG_USART#define DEBUG_USART MFUNC_OUT_U8_DEC_VALUE
USART_INIT(DEBUG_USART);

打完收工。



1.当前物联网应用中,边缘计算还存在这些问题!

2.内涵外延发生巨变的嵌入式技术,未来如何发展?

3.物联网时代,芯片企业靠什么赚钱?

4.2020年第7期《单片机与嵌入式系统应用》电子刊新鲜出炉!

5.DDR 5内存规范发布了~

6.据称Arm拟提高部分客户授权费,芯片成本或涨4倍

免责声明:本文系网络转载,版权归原作者所有。如涉及作品版权问题,请与我们联系,我们将根据您提供的版权证明材料确认版权并支付稿酬或者删除内容。

嵌入式资讯精选 掌握最鲜资讯,尽领行业新风
评论
  • 职场是人生的重要战场,既是谋生之地,也是实现个人价值的平台。然而,有些思维方式却会悄无声息地拖住你的后腿,让你原地踏步甚至退步。今天,我们就来聊聊职场中最忌讳的五种思维方式,看看自己有没有中招。1. 固步自封的思维在职场中,最可怕的事情莫过于自满于现状,拒绝学习和改变。世界在不断变化,行业的趋势、技术的革新都在要求我们与时俱进。如果你总觉得自己的方法最优,或者害怕尝试新事物,那就很容易被淘汰。与其等待机会找上门,不如主动出击,保持学习和探索的心态。加入优思学院,可以帮助你快速提升自己,与行业前沿
    优思学院 2025-01-09 15:48 68浏览
  • 村田是目前全球量产硅电容的领先企业,其在2016年收购了法国IPDiA头部硅电容器公司,并于2023年6月宣布投资约100亿日元将硅电容产能提升两倍。以下内容主要来自村田官网信息整理,村田高密度硅电容器采用半导体MOS工艺开发,并使用3D结构来大幅增加电极表面,因此在给定的占位面积内增加了静电容量。村田的硅技术以嵌入非结晶基板的单片结构为基础(单层MIM和多层MIM—MIM是指金属 / 绝缘体/ 金属) 村田硅电容采用先进3D拓扑结构在100um内,使开发的有效静电容量面积相当于80个
    知白 2025-01-07 15:02 158浏览
  • 在智能网联汽车中,各种通信技术如2G/3G/4G/5G、GNSS(全球导航卫星系统)、V2X(车联网通信)等在行业内被广泛使用。这些技术让汽车能够实现紧急呼叫、在线娱乐、导航等多种功能。EMC测试就是为了确保在复杂电磁环境下,汽车的通信系统仍然可以正常工作,保护驾乘者的安全。参考《QCT-基于LTE-V2X直连通信的车载信息交互系统技术要求及试验方法-1》标准10.5电磁兼容试验方法,下面将会从整车功能层面为大家解读V2X整车电磁兼容试验的过程。测试过程揭秘1. 设备准备为了进行电磁兼容试验,技
    北汇信息 2025-01-09 11:24 79浏览
  • 故障现象一辆2017款东风风神AX7车,搭载DFMA14T发动机,累计行驶里程约为13.7万km。该车冷起动后怠速运转正常,热机后怠速运转不稳,组合仪表上的发动机转速表指针上下轻微抖动。 故障诊断 用故障检测仪检测,发动机控制单元中无故障代码存储;读取发动机数据流,发现进气歧管绝对压力波动明显,有时能达到69 kPa,明显偏高,推断可能的原因有:进气系统漏气;进气歧管绝对压力传感器信号失真;发动机机械故障。首先从节气门处打烟雾,没有发现进气管周围有漏气的地方;接着拔下进气管上的两个真空
    虹科Pico汽车示波器 2025-01-08 16:51 111浏览
  • 1月7日-10日,2025年国际消费电子产品展览会(CES 2025)盛大举行,广和通发布Fibocom AI Stack,赋智千行百业端侧应用。Fibocom AI Stack提供集高性能模组、AI工具链、高性能推理引擎、海量模型、支持与服务一体化的端侧AI解决方案,帮助智能设备快速实现AI能力商用。为适应不同端侧场景的应用,AI Stack具备海量端侧AI模型及行业端侧模型,基于不同等级算力的芯片平台或模组,Fibocom AI Stack可将TensorFlow、PyTorch、ONNX、
    物吾悟小通 2025-01-08 18:17 69浏览
  • 在当前人工智能(AI)与物联网(IoT)的快速发展趋势下,各行各业的数字转型与自动化进程正以惊人的速度持续进行。如今企业在设计与营运技术系统时所面临的挑战不仅是技术本身,更包含硬件设施、第三方软件及配件等复杂的外部因素。然而这些系统往往讲究更精密的设计与高稳定性,哪怕是任何一个小小的问题,都可能对整体业务运作造成严重影响。 POS应用环境与客户需求以本次分享的客户个案为例,该客户是一家全球领先的信息技术服务与数字解决方案提供商,遭遇到一个由他们所开发的POS机(Point of Sal
    百佳泰测试实验室 2025-01-09 17:35 78浏览
  • 本文介绍编译Android13 ROOT权限固件的方法,触觉智能RK3562开发板演示,搭载4核A53处理器,主频高达2.0GHz;内置独立1Tops算力NPU,可应用于物联网网关、平板电脑、智能家居、教育电子、工业显示与控制等行业。关闭selinux修改此文件("+"号为修改内容)device/rockchip/common/BoardConfig.mkBOARD_BOOT_HEADER_VERSION ?= 2BOARD_MKBOOTIMG_ARGS :=BOARD_PREBUILT_DTB
    Industio_触觉智能 2025-01-08 00:06 111浏览
  •  在全球能源结构加速向清洁、可再生方向转型的今天,风力发电作为一种绿色能源,已成为各国新能源发展的重要组成部分。然而,风力发电系统在复杂的环境中长时间运行,对系统的安全性、稳定性和抗干扰能力提出了极高要求。光耦(光电耦合器)作为一种电气隔离与信号传输器件,凭借其优秀的隔离保护性能和信号传输能力,已成为风力发电系统中不可或缺的关键组件。 风力发电系统对隔离与控制的需求风力发电系统中,包括发电机、变流器、变压器和控制系统等多个部分,通常工作在高压、大功率的环境中。光耦在这里扮演了
    晶台光耦 2025-01-08 16:03 87浏览
  • 一个真正的质量工程师(QE)必须将一件产品设计的“意图”与系统的可制造性、可服务性以及资源在现实中实现设计和产品的能力结合起来。所以,可以说,这确实是一种工程学科。我们常开玩笑说,质量工程师是工程领域里的「侦探」、「警察」或「律师」,守护神是"墨菲”,信奉的哲学就是「墨菲定律」。(注:墨菲定律是一种启发性原则,常被表述为:任何可能出错的事情最终都会出错。)做质量工程师的,有时会不受欢迎,也会被忽视,甚至可能遭遇主动或被动的阻碍,而一旦出了问题,责任往往就落在质量工程师的头上。虽然质量工程师并不负
    优思学院 2025-01-09 11:48 94浏览
  • 在过去十年中,自动驾驶和高级驾驶辅助系统(AD/ADAS)软件与硬件的快速发展对多传感器数据采集的设计需求提出了更高的要求。然而,目前仍缺乏能够高质量集成多传感器数据采集的解决方案。康谋ADTF正是应运而生,它提供了一个广受认可和广泛引用的软件框架,包含模块化的标准化应用程序和工具,旨在为ADAS功能的开发提供一站式体验。一、ADTF的关键之处!无论是奥迪、大众、宝马还是梅赛德斯-奔驰:他们都依赖我们不断发展的ADTF来开发智能驾驶辅助解决方案,直至实现自动驾驶的目标。从新功能的最初构思到批量生
    康谋 2025-01-09 10:04 72浏览
  • 「他明明跟我同梯进来,为什么就是升得比我快?」许多人都有这样的疑问:明明就战绩也不比隔壁同事差,升迁之路却比别人苦。其实,之间的差异就在于「领导力」。並非必须当管理者才需要「领导力」,而是散发领导力特质的人,才更容易被晓明。许多领导力和特质,都可以通过努力和学习获得,因此就算不是天生的领导者,也能成为一个具备领导魅力的人,进而被老板看见,向你伸出升迁的橘子枝。领导力是什么?领导力是一种能力或特质,甚至可以说是一种「影响力」。好的领导者通常具备影响和鼓励他人的能力,并导引他们朝着共同的目标和愿景前
    优思学院 2025-01-08 14:54 93浏览
  • HDMI 2.2 规格将至,开启视听新境界2025年1月6日,HDMI Forum, Inc. 宣布即将发布HDMI规范2.2版本。新HDMI规范为规模庞大的 HDMI 生态系统带来更多选择,为创建、分发和体验理想的终端用户效果提供更先进的解决方案。新技术为电视、电影和游戏工作室等内容制作商在当前和未来提供更高质量的选择,同时实现多种分发平台。96Gbps的更高带宽和新一代 HDMI 固定比率速率传输(Fixed Rate Link)技术为各种设备应用提供更优质的音频和视频。终端用户显示器能以最
    百佳泰测试实验室 2025-01-09 17:33 84浏览
  • 根据环洋市场咨询(Global Info Research)项目团队最新调研,预计2030年全球中空长航时无人机产值达到9009百万美元,2024-2030年期间年复合增长率CAGR为8.0%。 环洋市场咨询机构出版了的【全球中空长航时无人机行业总体规模、主要厂商及IPO上市调研报告,2025-2031】研究全球中空长航时无人机总体规模,包括产量、产值、消费量、主要生产地区、主要生产商及市场份额,同时分析中空长航时无人机市场主要驱动因素、阻碍因素、市场机遇、挑战、新产品发布等。报告从中空长航时
    GIRtina 2025-01-09 10:35 71浏览
  • By Toradex 秦海1). 简介嵌入式平台设备基于Yocto Linux 在开发后期量产前期,为了安全以及提高启动速度等考虑,希望将 ARM 处理器平台的 Debug Console 输出关闭,本文就基于 NXP i.MX8MP ARM 处理器平台来演示相关流程。 本文所示例的平台来自于 Toradex Verdin i.MX8MP 嵌入式平台。  2. 准备a). Verdin i.MX8MP ARM核心版配合Dahlia载板并
    hai.qin_651820742 2025-01-07 14:52 121浏览
  • 光伏逆变器是一种高效的能量转换设备,它能够将光伏太阳能板(PV)产生的不稳定的直流电压转换成与市电频率同步的交流电。这种转换后的电能不仅可以回馈至商用输电网络,还能供独立电网系统使用。光伏逆变器在商业光伏储能电站和家庭独立储能系统等应用领域中得到了广泛的应用。光耦合器,以其高速信号传输、出色的共模抑制比以及单向信号传输和光电隔离的特性,在光伏逆变器中扮演着至关重要的角色。它确保了系统的安全隔离、干扰的有效隔离以及通信信号的精准传输。光耦合器的使用不仅提高了系统的稳定性和安全性,而且由于其低功耗的
    晶台光耦 2025-01-09 09:58 53浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦