预览:
-
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宏。你遇到了哪些棘手的问题?
相关信息
请点击以下链接获取相关和有用的信息: