0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

Arduino预处理器指令教程

李欢 来源:醉狼工作室 作者:醉狼工作室 2023-02-24 09:51 次阅读

这篇文章来源于DevicePlus.com英语网站的翻译稿。

poYBAGPzFk-AMyEfAASJRZLy_C0605.jpg

适用于ROHM传感器评估套件的轻量级Arduino中,我介绍了RohmMultiSensor——帮您轻松连接ROHM传感器评估套件多个传感器的Arduino库。该库的核心特征之一就是通过仅编译与所需传感器相关的库部分,显著减小程序的大小。这意味着当您使用较少的传感器时,整体程序大小和内存使用量会减小。但是,这究竟是如何实现的呢?当您#include一个库然后按下“Upload”(上传)按钮之后,幕后究竟会发生什么?

硬件

Arduino UNO

软件

Arduino IDE

几乎所有用过Arduino的人都使用过库。这就是Arduino编程对初学者来说如此简单的原因之一——您无需深入了解传感器的工作原理;库会替您完成大部分工作。将代码分成单独的文件也是一种很好的编程习惯。组织、调试和维护单个文件要比处理一大堆代码容易得多。

想必Arduino初学者都已经熟悉了将库添加到主程序中的#include命令。要了解这是如何实现的,我们首先应快速了解C/C++源代码如何编译成程序。别担心,这听起来比较复杂,其实很简单。我们来看一下编译的工作原理。

按“上传”之后

我们先做一个快速实验:启动Arduino IDE,打开其中一个示例代码(比如“Blink”),然后按“Verify”按钮。假设程序中没有语法错误,底部的控制台应该会打印出有关程序大小和内存的一些信息。嗯,刚才我们成功地将C++源代码编译成了二进制文件。在编译过程中发生了以下几件事:

Arduino IDE执行了一种名为“语法检查”的操作,以确保您编写的程序是真正的C/C++源代码。此时,如果发生函数拼写错误或忘记分号,那么编译就会停止。

语法检查之后,Arduino IDE会启动另一个名为preprocessor(预处理器)的程序。这是一个非常简单的程序,如果文件是C/C++源代码,它不会怎么样。我们稍后会详细讨论这一步骤。那么现在我们假设结果是一个名为“扩展源代码”的文件——一个文本文件。

然后,该扩展源代码被移交给另一个名为compiler(编译器)的程序。该编译器(在Arduino IDE中是avr-gcc)接收文本源,并生成汇编文件。汇编一种人类可读的低级编程语言,但是更接近机器代码——适用于特定处理器的指令。这里就是您编写程序之前必须选择正确Arduino板的原因——不同的开发板具有不同的处理器,而处理器又具有不同的指令集。

处理您Arduino程序下一个的系统程序叫做assembler(汇编程序)。该程序会生成一个“目标文件”。该文件主要是机器代码,但也可以包含针对其他目标文件对象的“引用”。这允许Arduino IDE“预编译”一些编写Arduino程序时会始终用到的库,从而使整个过程更快。

最后一个阶段称为链接,由另一个名为linker(链接器,显而易见)的程序完成。链接器获取目标文件并添加缺少的内容——主要是来自其他目标文件的符号,以生产可执行文件。在此之后,程序完全转换为机器代码,并可以上传到电路板。

poYBAGPzFlCAP8kHAADv1ACnGVg466.jpg

现在,我们对Arduino程序编译有了一个基本的了解。但是在上述所有编译阶段中,我们将只关注第二个阶段:预处理器。

预处理器基本知识

在上本中,我提到预处理器本质上非常简单:接收文本输入,搜索关键字,根据找到的内容进行一些操作,然后输出不同的文本。它非常简单,同时也非常强大,因为它允许你用普通C/C++语言完成一些本来会非常复杂的事情(如果可能)。

预处理器会搜索以井号(#)开头且后面有文本的行。这种语句叫做预处理器指令,是预处理器的一种“命令”。预处理器指令的完整列表以及详细文档的地址如下所示:

https://gcc.gnu.org/onlinedocs/cpp/Index-of-Directives.html#Index-of-Directives。

接下来,我将主要关注#include、#define和条件指令,因为这是Arduino最有用的指令。如果您想了解一些更“奇异”的指令,比如#assert 或 #pragma, 请参阅上述地址,以获取官方信息。

添加额外代码:#include 指令

这可能是最著名的预处理器指令,不仅Arduino爱好者都知道,而且C/C++编程人员也都了解。原因很简单:该指令的作用是包含库。但是,这究竟是如何实现的呢?确切的语法如下所示:

#include 

#include "file"

两者的区别比较小,主要在于预处理器搜索file(文件)的确切位置。如果是第一句,预处理器仅搜索IDE指定的目录。如果是第二句,预处理器首先查看包含源文件的文件夹,且仅当没有在该目录下找到file(文件) 时, 它才会搜索第一句的搜索目录。由于包含库的文件夹是在Arduino IDE中指定的,因此在包含库时两者之间没有重大区别。

当预处理器找到文件时,它只是将其内容复制粘贴到源代码中,以替代程序中的#include指令。但是,如果在任何目录中都找不到此文件,就会引发致命错误,编译停止。

要记住,预处理器只处理文本——无法理解那些特殊字母和数字的含义。最重要的是,它对所包含的内容和包含次数绝对不会进行更高级别的检查。让我们来看一下使用编写不正确的库会发生什么。

#include 

void setup() {

}

#include 

void loop() {

}

这个Arduino程序中没有多少内容。请注意我们包含了一个名为“ExampleLibrary.h”的文件,而且我们包含了两次。

//This is an example library

int a = 0;

//End of example library

“ExampleLibrary.h”的内容如下所示。同样,除了一个整数变量之外,没有多少内容。那么当我们编译这个Arduino程序时会发生什么呢?

pYYBAGPzFlKABQHUAAJzvAUPScQ446.jpg

错误信息显示变量a声明了两次,这导致编译失败。这是预处理器完成后源代码的样子。

//This is an example  library

int a = 0;

//End of example library

void setup() {

}

//This is an example  library

int a = 0;

//End of example library

void loop() {

}

显而易见,不应该多次包含库,但是如何在不依赖用户的情况下实现这一目标?标准解决方案是将整个库包含在以下结构中:

#ifndef _EXAMPLE_LIBRARY_H
#define _EXAMPLE_LIBRARY_H

//This is an example  library

int a = 0;

//End of example library

#endif

现在,第一次包含库时,预处理器会检查是否存在用“_EXAMPLE_LIBRARY_H”定义的内容。由于没有类似的东西存在,预处理器继续下一行并定义一个名为“_EXAMPLE_LIBRARY_H”的常量。然后,库代码被复制到程序中。

当第二次包含库时,预处理器会再次检查是否存在名为“_EXAMPLE_LIBRARY_H”的常量。这次,由于上一个#include命令已经定义了该常量,所以预处理器不会向程序中添加任何内容。于是,编译成功完成。#ifdef 和 #endif是条件指令,我们稍后将对此进行讨论。

定义事物:#define 指令

在上一个例子中,我们用#define指令创建了一个常量,以决定是否包含一个库。在官方文档中,任何由#define指令定义的东西都被称为macro(宏), 因此本文中我会一直沿用这个术语。该指令的语法如下:

#define macro_name macro_body

大多数Arduino初学者可能会对宏感到困惑。如果我定义一个宏:

#define X 10

那么这与以下变量声明有什么区别呢?

int Y = 10;

同样,这一切都归结为预处理器仅处理文本。遇到#define指令时,预处理器会搜索其余的源代码并将所有出现的“X”替换为“10”。这意味着与变量不同,宏的值永远不会改变。此外,您必须牢记预处理器只搜索以#define开头的源代码。让我们看一下使用尚未定义的宏会发生什么情况。

int Y = X;
#define X 10
int Z = X;

void setup() {
 
}

void loop() {
 
}

编译上述代码会发生以下错误:

pYYBAGPzFlWAbpBYAAGCLzCzabE068.jpg

预处理后的代码如下所示:

int Y = X;
int Z = 10;

void setup() {

}

void loop() {
 
}

第一行包含X,它被看作一个变量。但是,该变量从未声明,因此编译停止。

尽管#define指令最常见的用途是创建带名称的常量,但是它可以做的远不止这些。例如,假设您想知道两个给定数字中哪一个较小。您可以编写一个实现此功能的函数。

int min(int a, int b) {
 if(a < b) {
   return(a);
 }
 return(b);
}

或者使用更简单的三元运算符:

int min(int a, int b) {
 return((a < b) ? a : b);
}

但是,这两个函数都将被编译并占用宝贵的程序存储空间。我们可以使用以下类似函数的宏来实现相同效果,但是占用的程序空间却会变少。

#ifndef MIN
 #define MIN(A, B)     (((A) < (B)) ? (A) : (B))
#endif

现在,每个“MIN(A, B)”都会被替换为“(((A) < (B)) ? (A) : (B))”,其“A”和“B”可以是数字,也可以是变量。请注意,#define包含在相同的保护性结构中,以防止用户重复定义宏。

创建宏时,您必须记住,系统将宏作为文本进行处理。这就是为什么在上面的定义中,几乎所有内容都包含在括号中。请猜测以下运算的结果。

#ifndef MULTIPLY
 #define MULTIPLY(A, B)     A * B
#endif

//some code...

int result = MULTIPLY(2 - 0, 3);

结果应该是6,因为2–0=2,然后2x3=6,对吧?如果我告诉你结果是2呢?实际编译的内容如下:

int result = 2 - 0 * 3;

由于乘法优先于减法,因此很明显结果肯定是2,因为3x0=0,然后2-0=2。正确的版本如下所示:

#ifndef MULTIPLY
 #define MULTIPLY(A, B)     ((A) * (B))
#endif

条件编译:#if指令

在前面的例子中,我使用了#ifndef指令,于是我可以检查是否已经包含了库。该指令可用于实现仅用C/C++语言不可能实现的内容:条件语句。这些指令的语法如下所示:

#if expression

  //compile this code

#elif different_expression
 
 //compile this different code

#else
 
 //compile this entirely different code

#endif

条件语句的常用功能是检查一个宏是否已定义。为此,您可以使用几个专门的指令:

#ifndef macro_name

  //compile this code if macro_name does not exist

#endif

我们已经熟悉了上述内容,因为我们之前使用此指令来检查是否已包含库。您也可以使用这个条件:

#ifdef macro_name

  //compile this code if macro_name exists

#endif

以上语句只是#if defined的简写,可根据单个条件测试多个宏。请注意,每个条件都必须用#endif 指令结束,从而指定代码的哪些部分受条件影响,哪些部分不受条件影响。

我们来看一个实际的例子。假设您编写了一个库,并且希望它在Arduino UNO和Arduino Mega上都能正常工作。这主意不错,对吧?便携代码总比为另一块电路板修改库更容易。但是,如果您的库使用了SPI总线呢?该总线在Arduino UNO上用的是11-13引脚,但是在Mega上却是50-52引脚。

那么您如何告诉编译器根据不同开发板使用相应的引脚呢?您猜对了——条件语法!根据您在Arduino IDE中选择(“Tools” > “Board”菜单)的开发板,IDE将定义不同的宏,从而仅编译与所选开发板相关的代码部分!这非常强大,因为您可以实现以下功能:

#if defined(__AVR_ATmega168__) || defined(__AVR_ATmega328P__)
 
 //this will compile for Arduino UNO, Pro and older boards
 int _sck = 13;
 int _miso = 12;
 int _mosi = 11;

#elif defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
 
 //this will compile for Arduino Mega
 int _sck = 52;
 int _miso = 50;
 int _mosi = 51;

#endif

怎么样,漂亮吧?仅用三行代码,我们就制作了一个多平台便携库!另外,这正是RohmMultiSensor库(适用于ROHM传感器评估套件的轻量级Arduino库)如何知道应该为所选传感器编译哪些代码。如果您看一下头文件RohmMultiSensor.h里面的内容,您只会看到几个#ifdef和几个#include指令。由于所有特定传感器代码都存储在单独的.cpp文件中,因此将新传感器添加到库中很容易——只需创建另一个文件,然后创建与其他传感器相同的#ifdef – #include – #endif结构即可。完成!

提供反馈:#warning 和 #error 指令

我们最后要介绍的指令是#warning#error。两者但是不言自明,语法如下:

#warning "message"

#error "message"

预处理器遇到这些指令时,它会将message打印到Arduino IDE控制台中。两者之间的区别在于,发生#warning之后,编译正常进行,而#error则会完全停止编译。

我们可以在前文的例子中使用这两个语句:

#if defined(__AVR_ATmega168__) || defined(__AVR_ATmega328P__)

  //this will compile for Arduino UNO, Pro and older boards
 int _sck = 13;
 int _miso = 12;
 int _mosi = 11;
 
#elif defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)

 //this will compile for Arduino Mega
 int _sck = 52;
 int _miso = 50;
 int _mosi = 51;
 
#else

 #error “Unsupported board selected!”

#endif

这样,当用户尝试为其他Arduino开发板(比如Yún、LilyPad等)编译该库时,编译会失败,与没有定义SPI引脚没有任何关系。

结论

在本文中,我们介绍了C/C++预处理器的相关知识。希望您看过本文之后,就不会再害怕编译预处理器、或指令等术语了。我总结一下本文描述的最重要的几点内容:

编写库时,请务必将其放在 #ifndef – #define – #endif结构中。这个结构我们已经见过多次了。这可能会为您省去一些麻烦。定义类似函数的宏时同样应该这样做。

编写代码时,应确保程序易于移植到其他Arduino板上。相信我,未雨绸缪要比出现不兼容问题之后再想法解决要容易得多。

分而治之!几个较小的文件总比一个1000多行的大文件要好得多。

审核编辑:汤梓红

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 指令
    +关注

    关注

    1

    文章

    606

    浏览量

    35649
  • Arduino
    +关注

    关注

    187

    文章

    6464

    浏览量

    186636
  • 预处理器
    +关注

    关注

    0

    文章

    13

    浏览量

    2221
收藏 人收藏

    评论

    相关推荐

    请问如何使用预处理指令#pragma禁止优化某段代码?哪里有c2000编译预处理指令的说明资料?

    本帖最后由 一只耳朵怪 于 2018-6-13 16:51 编辑 如何使用预处理指令#pragma禁止优化某段代码?哪里有c2000编译预处理
    发表于 06-13 04:57

    Intel 8051兼容预处理器接口

    Intel 8051兼容预处理器接口
    发表于 02-12 12:12

    预处理器在Build Settings中定义错误

    这个问题用PSoC Creator 3.3(3.3.0.410)进行。你好社区我问你关于一个问题的帮助(Bug?)在PSoC Creator。我想要的是:在编译环境中定义一个带有处理器值的预处理器
    发表于 02-22 06:25

    怎么使用预处理程序指令

    你好, 我想使用预处理器指令进行条件编译。我有一段代码,我想在定义预处理器指令时包含这些代码。在SPC5Studio中定义它的位置?这需要哪些设置? 在此先感谢您的回复。 麦克风。以
    发表于 06-21 07:21

    怎么使用#assert预处理程序指令

    大家好。在我的源代码中,我使用“assert”预处理器指令来检查常量值的一致性。如果我以简单的方式使用它,通过直接赋值,一切如预期:define ABC 0x7Fassert ABC
    发表于 04-15 09:41

    STM32CubeIDE暗模式预处理器突出显示错误怎么解决?

    我在 Ubuntu 18.04 系统上运行 STM32CubeIDE。我已将其切换为暗模式进行编程,但代码中的任何“ #if ”预处理器指令都有浅色背景。这使得无法阅读。我已经查看了语法着色的所有
    发表于 12-01 07:39

    处理器指令集设计

    处理器指令集设计垂直指令格式指令类型及其使用频度CISC指令集特点 RISC指令集特点
    发表于 10-29 17:13 64次下载
    微<b class='flag-5'>处理器</b><b class='flag-5'>指令</b>集设计

    预处理器的工作原理作用

    预处理器的工作原理作用,希望对学者们有帮助。
    发表于 10-29 11:40 0次下载

    基于FPGA的传像光纤束图像预处理器

    基于FPGA的传像光纤束图像预处理器,下来看看
    发表于 08-30 15:10 12次下载

    C语言预处理命令的分类和工作原理详细说明

    C 语言编程过程中,经常会用到如 #include、#define 等指令,这些标识开头的指令被称为预处理指令预处理
    发表于 11-25 10:34 18次下载
    C语言<b class='flag-5'>预处理</b>命令的分类和工作原理详细说明

    C语言预处理指令及分类

    C/C++ 程序中的源代码中包含以 # 开头的各种编译指令,这些指令称为预处理指令预处理指令
    的头像 发表于 11-29 10:14 2234次阅读

    嵌入式C预处理器的基本概念和常用指令

    在嵌入式系统开发中,C预处理器是非常重要的一部分,可以在编译之前对源代码进行宏替换、条件编译和包含等处理。在本文中,我们将介绍嵌入式C预处理器的基本概念和常用指令
    的头像 发表于 04-13 16:11 874次阅读

    C语言有哪些预处理操作?

    C语言的预处理是在编译之前对源代码进行处理的阶段,它主要由预处理器完成。预处理器是一个独立的程序,它负责对源代码进行一些文本替换和处理,生成
    的头像 发表于 12-08 15:40 583次阅读
    C语言有哪些<b class='flag-5'>预处理</b>操作?

    C语言中的预处理器

    所有的预处理器命令都是以井号(#)开头。它必须是第一个非空字符,为了增强可读性,预处理器指令应从第一列开始。
    发表于 03-01 12:16 820次阅读
    C语言中的<b class='flag-5'>预处理器</b>

    处理器指令的获取过程

    处理器指令的获取是计算机执行程序过程中的关键环节,它决定了微处理器如何对数据和指令进行处理。以下将详细阐述微
    的头像 发表于 10-05 15:16 207次阅读