精通C语言中的X宏:Arduino编程案例研究

预览:

  • X宏技术可用于自动化常规Arduino编程操作,如I/O端口配置和读取。

  • 列表是X宏技术的核心组成部分。预处理器会将列表展开为对象式宏和函数式宏。

  • 将信息整合到单一列表中能提升代码清晰度并减少错误。

  • Arduino的pinMode()和digitalInput()函数是绝佳案例,它们既常见又完美契合该技术消除重复代码的初衷。

  • 通过宏展开最能理解C语言中的X宏技术。

  • X宏是一种技术,而非命令或函数。事实上,在GNU GCC等文档中并未明确提及该技术。

介绍

你来到此页面绝非偶然。

很可能你是在论坛中读到X宏,或偶然听到程序员讨论该技术时表达了强烈支持或反对的态度。无论如何,你正渴望了解更多。

让我们开始吧。

宏是C语言编程的常用特性。你可能熟悉类似对象的#define关键字。例如,你可能用它来设置程序限制。

#define MAX_VAL 1000

在此示例中,预处理器会将所有MAX_VAL实例替换为文本1000。代码编译时就像程序员在所有MAX_VAL出现处都输入了1000。

更复杂的函数式宏可用于返回变量的绝对值:

#define ABS(X) ((X) < 0.0 ? -(X) : (X))

这里使用了三元运算符,它是if else语句的简写形式。本例中的测试条件是(X) < 0.0。若成立则返回–(X),否则返回(X),其中X可以是类似(A + B)的复杂表达式。多数程序员对宏和预处理器的理解就止步于此。让我们继续深入,探索名为X宏的更深层技术。

本工程简报将以Arduino Opta为参考设计,探讨X宏的实际应用。虽然该技术普遍适用于所有C语言编程,但Arduino凭借其广为人知的pinMode()和digitalWrite()函数,为在已知基础上添加宏复杂性提供了理想切入点。

作为引子,请看这段Arduino代码。使用X宏技术后,setup()和loop()函数变得极为简单,因为我们将I/O操作进行了封装和抽象。

void setup() {
  initializeInputs();
  initializeOutputs();
}

void loop() {
  readInputs();
  if (SS1 & PB_START & !PB_STOP) {
    FAN = ON;
    PL_GREEN = ON;
    PL_RED = OFF;
  }
  if (!SS1 | PB_STOP) {
    FAN = OFF;
    PL_GREEN = OFF;
    PL_RED = ON;
  }
  writeOutputs();
  delay(10);
}

技术提示 :本可以用任何微控制器来演示X宏。选择Arduino Opta(图1)是为了与更多基于PLC的文章探索保持同步。有关Opta的详细信息及训练器接线方式,请参阅《Arduino Opta训练器接线:DigiKey实验室》。

1 :本文使用的Arduino Opta设备图。

什么是 X 宏?

简而言之,X宏包含一个或多个类函数宏,这些宏以包含项目列表的另一个宏作为参数。请注意并不存在"X宏"指令。相反,X宏技术建立在预处理器的强大功能之上。
本文将展示如何将Arduino I/O放入宏表中。该表随后可传递给一个或多个类函数宏,经扩展后由编译器进行操作。

I/O 配置为输入

让我们从Arduino的输入开始。每个输入都需要将描述性名称与物理端口关联。本例中,选择开关(SS1)连接至OPTA的A0输入。常闭停止按钮连接A1输入,以此类推。

#define INPUT_TABLE \
  X(SS1, A0, 0) \
  X(PB_STOP, A1, 1) \
  X(PB_START, A2, 0) \
  X(PROX_SENSOR, A3, 0) \
  X(IN5, A4, 0) \
  X(IN6, A5, 0) \
  X(IN7, A6, 0) \
  X(IN8, A7, 0) \
  X(OPTA_BTN, BTN_USER, 1)

输入表的每行包含三个有序变量:(名称, 引脚, 反转)。描述性名称位于首字段,物理端口在第二字段,第三字段为反转/非反转指示符。

技术提示 :反斜杠\用于提升代码可读性。它允许INPUT_TABLE宏跨越多行显示。

注意Arduino使用pinMode(引脚, INPUT)函数将单个引脚配置为输入。通过以下宏实现:

#define X(name, pin, invert) pinMode(pin, INPUT);
void initializeInputs() {
  INPUT_TABLE
}
#undef X

可以看到initializeInputs()函数将遍历INPUT_TABLE。要理解下一部分,需重新审视最基本的宏(#define MAX_VAL 1000),并认识到它包含两个组成部分。第一部分是“当你看到此内容”部分,第二部分是“写入此内容”部分。对于我们的initializeInputs()宏,当你看到“X(name, pin, invert)”时,请写入“pinMode(pin, INPUT);”。了解这一点后,我们可以将宏展开为:

void initializeInputs() {
  pinMode(A0, INPUT);
  pinMode(A1, INPUT);
  pinMode(A2, INPUT);
  pinMode(A3, INPUT);
  pinMode(A4, INPUT);
  pinMode(A5, INPUT);
  pinMode(A6, INPUT);
  pinMode(A7, INPUT);
  pinMode(BTN_USER, INPUT);
}

构建一个结构体来保存输入值

接下来的几个部分共同协作,设置一个内存位置来保存输入的值。这将最终使我们能够使用前面提到的readInputs()函数。但在读取之前,我们需要先设置一个内存位置。为了实现这一点,我们将从一个结构体内部展开INPUT_TABLE宏。当预处理器遇到“X(name, pin, invert)”时,它会替换为“bool name;”。

struct stInput {
#define X(name, pin, invert) bool name;
  INPUT_TABLE
#undef X
};

stInput inputs;  // instantiate an instance called inputs

以下是展开后的结构体。在实例化一个名为inputs的实例后,我们可以访问其中的各个元素。例如 inputs.PB_STOP = true;。

struct stInput {
  bool SS1;
  bool PB_STOP;
  bool PB_START;
  bool PROX_SENSOR; 
  bool IN5;  
  bool IN6;
  bool IN7;
  bool IN8;
  bool OPTA_BTN;
};

stInput inputs;  // instantiate an instance called inputs

为点表示法创建别名

在开头部分,我们建议使用像PB_START这样的别名来简化代码。我们需要一个将PB_START替换为inputs.PB_START的过程。这对预处理器来说是一项简单的任务,因为我们会对输入表进行迭代。对于遇到的每一个“X(name, pin, invert)”,我们替换为“bool& name = inputs.name;”。这创建了一系列指向inputs结构体的引用。

#define X(name, pin, invert) bool& name = inputs.name;
  INPUT_TABLE
#undef X

展开为:

bool& SS1 = inputs.SS1;
bool& PB_STOP = inputs.PB_STOP;
bool& PB_START = inputs.PB_START;
bool& PROX_SENSOR = inputs.PROX_SENSOR;
bool& IN5 = inputs.IN5;
bool& IN6 = inputs.IN6;
bool& IN7 = inputs.IN7;
bool& IN8 = inputs.IN8;
bool& OPTA_BTN = inputs.OPTA_BTN;

技术提示 :在此示例中,PB_START是inputs.PB_START的别名。这里有一个细微但必要的需求,即包含&符号以允许引用更改值。如果没有&,我们将创建inputs.PB_START内容的副本——这并不实用。相反,我们需要完全控制,就像直接使用inputs.PB_START本身一样。

由于我们使用的是全局变量,因此不需要static修饰符。此外,由于我们(暂时)没有使用ISR,所以也不需要volatile限定符。

读取输入并将结果存储到结构体中

最后一步是对每个输入执行digitalRead()操作,并将结果存入inputs结构体。本例中我们看到了readInputs()函数。它将遍历输入表,把每个"X(name, pin, invert)“替换为"inputs.name = digitalRead(pin) ^ invert;”。

#define X(name, pin, invert) inputs.name = digitalRead(pin) ^ invert;
void readInputs() {
  INPUT_TABLE
}
#undef X

展开为:

void readInputs() {
  inputs.SS1 = digitalRead(A0) ^ 0; 
  inputs.PB_STOP = digitalRead(A1) ^ 1;   
  inputs.PB_START = digitalRead(A2) ^ 0;   
  inputs.PROX_SENSOR = digitalRead(A3) ^ 0;  
  inputs.IN5 = digitalRead(A4) ^ 0;        
  inputs.IN6 = digitalRead(A5) ^ 0;        
  inputs.IN7 = digitalRead(A6) ^ 0;        
  inputs.IN8 = digitalRead(A7) ^ 0;        
  inputs.OPTA_BTN = digitalRead(BTN_USER) ^ 1; 
}

反转常闭开关信号

为简化主代码,我们对特定数字输入执行反转操作。演示用的Opta PLC连接了一个常闭"停止"按钮。此外,Opta面板上的按钮似乎内置了上拉电阻。

回顾INPUT_TABLE定义可见,第三个字段表示是否应进行信号反转。使用XOR(^)运算符可轻松实现这种反转。

技术提示 :输入信号未经过消抖处理。这可能对程序造成影响,也可能不会。

Arduino 输出操作

类似的X宏操作也可用于Arduino输出。这里包含了一个可供参考的功能性程序。

/*  This INPUT table consolidates the input configuration data into a single location.

  Fields are described as(name, port, invert).

  * name: change the name variable to a descriptive self-documenting variable such as PB_START
  * port: DO NOT change the port assignments as these are fixed physical ports for the Arduino OPTA I/O.
  * invert: when invert is true, the input value is inverted upon reading. This is useful to retain positive logic for normally closed switches.

  For example, assume a normally closed stop pushbutton is connected to input A1.
  The command X(PB_STOP, A1, 1) identifies:
    * the alias for the pushbutton
    * the physical OPTA port ref: https://content.arduino.cc/assets/AFX00002-full-pinout.pdf
    * that the pushbutton input should be inverted when read
*/
#define INPUT_TABLE \
  X(SS1, A0, 0) \
  X(PB_STOP, A1, 1) \
  X(PB_START, A2, 0) \
  X(PROX_SENSOR, A3, 0) \
  X(IN5, A4, 0) \
  X(IN6, A5, 0) \
  X(IN7, A6, 0) \
  X(IN8, A7, 0) \
  X(OPTA_BTN, BTN_USER, 1)

/*
  Dynamically initialize each I/O to an input based on the input table data.
*/
#define X(name, pin, invert) pinMode(pin, INPUT);
void initializeInputs() {
  INPUT_TABLE
}
#undef X

/*
  Create a structure so that we can later use dotted notation to access the members e.g., inputs.PB_STOP.
*/
struct stInput {
#define X(name, pin, invert) bool name;
  INPUT_TABLE
#undef X
};

stInput inputs;  // Declare an instance of the structure

/*
  Create a descriptive reference for each input e.g., PB_STOP as opposed to inputs.PB_STOP.
  Mutability (the ability to change states) is maintained by using the reference bool&. This allows on-the-fly modifications e.g.,
    FAN = SS1;
    or even
    SS1 = false; // Not recommended, but possible
*/
#define X(name, pin, invert) bool& name = inputs.name;
  INPUT_TABLE
#undef X

/*
  Construct a function-like-macro to read each digital input and then store the results in the inputs structure.

  The input values are inverted as specified in the input table.
*/
#define X(name, pin, invert) inputs.name = digitalRead(pin) ^ invert;
void readInputs() {
  INPUT_TABLE
}
#undef X

/*
  Output table initialized as name and port
*/
#define OUTPUT_TABLE \
  X(PL_RED, D0) \
  X(PL_GREEN, D1) \
  X(CR1, D2) \
  X(FAN, D3) \
  X(LED_BLUE, LED_USER) \
  X(LED_S1, LED_D0) \
  X(LED_S2, LED_D1) \
  X(LED_S3, LED_D2) \
  X(LED_S4, LED_D3)
 // X(LED_RESET, LEDR) TODO: Determine why the red and green LEDs are not available using this X macros style.
// X(LED_GREEN, PH_12)

#define X(name, pin) pinMode(pin, OUTPUT);
void initializeOutputs() {
  OUTPUT_TABLE
}
#undef X

struct stOutput {

#define X(name, pin) bool name;
  OUTPUT_TABLE
#undef X
};

stOutput outputs;

#define X(name, pin) digitalWrite(pin, outputs.name);
void writeOutputs(){
  OUTPUT_TABLE
}
#undef X

#define X(name, pin) bool& name = outputs.name;
OUTPUT_TABLE
#undef X

#define ON HIGH
#define OFF LOW

void setup() {
  initializeInputs();
  initializeOutputs();
}

void loop() {

  readInputs();

  if (SS1 & PB_START & !PB_STOP) {
    FAN = ON;
    PL_GREEN = ON;
    PL_RED = OFF;
  }

  if (!SS1 | PB_STOP) {
    FAN = OFF;
    PL_GREEN = OFF;
    PL_RED = ON;
  }

  writeOutputs();

  delay(10);
}

下一步 :

如需深入了解Arduino、PLC和C语言编程,可考虑:

  • 将示例代码集成到基于类的结构中。

  • 在方案中加入模拟量处理。

  • 寻找在代码中应用宏的机会。

  • 研究PLC语言如何简化代码。

  • 学习工业自动化的经济学原理,以及减少昂贵停机时间的技术。

  • 关注并考虑参加未来的"混淆C代码大赛"

最后的思考

X宏技术能有效处理重复且易错的细节。当我们将命名替换视为预处理器遍历列表时,语法并不复杂。当我们考虑到将输入数据整合到单一位置的清晰性,以及initializeInputs()和readInputs()函数提供的自动化时,该技术的优势尤为突出。通过引用(&)技术的运用,loop()的清晰性得到了进一步增强。

你如何看待这种应用于Arduino的X宏方法?就个人而言,我仍持观望态度,因为它既可能被归类为天才之举,也可能被视为疯狂。

请在下方评论区分享你的想法。我们特别想了解你如何在项目中使用X宏。你遇到了哪些棘手的问题?

相关信息

请点击以下链接获取相关和有用的信息: