Preview:
-
The X macros technique may be used to automate routine Arduino programming operations such as I/O port configurations and reading.
-
Lists are an essential component of the X macros technique. The preprocessor expands the list for object-like and function-like macros.
-
The consolidation of information into a single list adds clarity and reduces errors.
-
The Arduino pinMode( ) and digitalInput( ) functions provide a good case study as they are common and fit perfectly with the technique’s purpose of elimination of repetitive code.
-
The X macros in C technique is best understood by expanding the macro.
-
X macros is a technique, not a command or a function. In fact, it is not identified in documents such as the GNU GCC documentation.
Introduction
You didn’t land on this page by accident.
Chances are you read about the X macros in a forum or overheard a programmer describing the technique while expressing strong feelings either in support or against the technique. Either way, you are scratching your head trying to learn more.
Let’s get started.
The macro is a common feature used in C programming. You are likely familiar with the object-like #define keyword. For example, you may have used it to set limits in your program.
#define MAX_VAL 1000
In this example the preprocessor will replace all instances of MAX_VAL with the text 1000. The code is then compiled as if the programmer had entered 1000 for every occurrence of MAX_VAL.
A more complex function-like macro could be used to return the absolute value of a variable:
#define ABS(X) ((X) < 0.0 ? -(X) : (X))
Here we use the ternary operator which is a shorthand for an if else statement. In this example, the test is (X) < 0.0. If yes, we return –(X) else (X) where X can be a complex statement such as (A + B). This is about as far as many programmers go with macros and the preprocessor. Let’s keep going, as there is a deep layer that we have yet to explore, known as X macros.
In this engineering brief, we will explore an application of X macros using the Arduino Opta as a reference design. While the technique is generally applicable to all C programming, the Arduino with its well known pinMode( ) and digitalWrite( ) functions provide the sweet spot for adding macro complexity onto the known.
As a teaser, consider this Arduino code. With X macros, the setup( ) and loop( ) functions become trivial as we encapsulate and abstract I/O operations.
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);
}
Tech Tip: We could have used any microcontroller to demonstrate X macros. The Arduino Opta (Figure 1) was chosen to align with a greater exploration of PLC based articles. For more information about the Opta and how the trainer was wired, please see Arduino Opta Trainer Wiring: A DigiKey Lab.
Figure 1: Image of the Arduino Opta used in this article.
What are X macros?
The short definition is that X macros incorporate one or more function-like macros that take an argument in the form of another macro containing a list of items. Understand that there is no “X macro” instruction. Instead, the X macros technique is built upon the power of the preprocessor.
In this document, we will show that the Arduino I/O can be placed into a table (macro). This table can then be passed to one or more function-like macros, expanded, and then operated upon by the compiler.
Configure I/O as inputs
Let’s start with the Arduino inputs. For each input, we will need to associate a descriptive name with a physical port. In this example, the selector switch (SS1) is connected to the OPTA’s A0 input. The normally closed stop pushbutton is connected to the A1 inputs and so on.
#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)
Each line of the input table contains three variables ordered as (name, pin, invert). The descriptive names are contained in the first field, the physical port in the second, and an invert/invert-not indicator in the third.
Tech Tip: Recall that the \ is used for human readable code. It allows the INPUT_TABLE macro to be spread over multiple lines.
Recall that Arduino uses the pinMode(pin, INPUT) function to configure an individual pin as an input. This is done using this macro:
#define X(name, pin, invert) pinMode(pin, INPUT);
void initializeInputs() {
INPUT_TABLE
}
#undef X
We can see the initializeInputs() function which will iterate over the INPUT_TABLE. To understand the next part, we need to reconsider our most basic macro (#define MAX_VAL 1000 ) and recognize that it has two parts. The first is the “when you see this” portion and the second is the “write this” portion. For our initializeInputs() macro, when you see “X(name, pin, invert)”, write “pinMode(pin, INPUT);”. Knowing this, we can expand the macro as:
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);
}
Construct a structure to hold the input values
The next few sections work together to set up a memory location to hold the value of the inputs. This will ultimately allow us to use the previously mentioned readInputs() function. But, before we can read, we need to set up a memory location. To accomplish this, we will expand the INPUT_TABLE macro from within a structure. When the preprocessor encounters an “X(name, pin, invert)”, it substitutes “bool name;”.
struct stInput {
#define X(name, pin, invert) bool name;
INPUT_TABLE
#undef X
};
stInput inputs; // instantiate an instance called inputs
Here is the expanded structure. After instantiating an instance called inputs, we can access the individual elements. For example 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
Alias the dotted notation
In the opening section we suggested that an alias such as PB_START could be used to simplify the code. We need a process that will substitute PB_START for inputs.PB_START. This is an easy task for the preprocessor, as we iterate over the input table. For every “X(name, pin, invert)” we encounter we substitute a “bool& name = inputs.name;”. This creates a series of references into the inputs structure.
#define X(name, pin, invert) bool& name = inputs.name;
INPUT_TABLE
#undef X
Expands to:
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;
Tech Tip: In this example, PB_START is an alias for inputs.PB_START. There is a subtle necessity to include the & designation allowing the reference to change the value. Without the &, we would have created a copy of the inputs.PB_START contents — not useful. Instead, we want full control, as if we were using inputs.PB_START itself.
Since we are using global variables, there is no need for a static designation. Also, since we are not using the ISR (yet), there is no need for the volatile qualifier.
Read the inputs and store results in the structure
Our final step is to perform a digitalRead( ) operation on each input and store the results in the inputs structure. In this example, we see the readInputs() function. It will iterate over the input table, replacing each “X(name, pin, invert)” with a “inputs.name = digitalRead(pin) ^ invert;”.
#define X(name, pin, invert) inputs.name = digitalRead(pin) ^ invert;
void readInputs() {
INPUT_TABLE
}
#undef X
Expands to:
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;
}
Invert the normally closed switches
To simplify the main code, we perform an invert operation on select digital inputs. The featured Opta PLC is connected to a normally closed “stop” pushbutton. Also, the pushbutton on the Opta’s face appears to contain a pull up resistor.
Looking back to our INPUT_TABLE definition we see the third field indicates if an inversion should or should not take place. This inversion is easily accomplished using the XOR (^) operator.
Tech Tip: The inputs have not been debounced. This may or may not cause complications for your program.
Operation for the Arduino outputs
Similar X macros operation may be implemented for the Arduino outputs. A functional program is included here for your consideration.
/* 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);
}
Next Steps:
To learn more about the Arduino, PLC, and C coding, consider:
-
Integrating the sample code into a class-based structure.
-
Adding analog into the mix.
-
Look for opportunities to apply macros in your code.
-
Explore PLC languages to determine how they simplify code.
-
Learn the economics of industrial automation along with techniques to minimize costly down time.
-
Watch for and consider entering any future Obfuscated C Code Contest contests.
Parting thoughts
The X macros technique works well to handle the repetitious and error-prone details. The syntax isn’t too difficult when we consider the named substitutions as the preprocessor iterates over the list. The technique shines when we consider the clarity of consolidating the input data into a single location coupled with the automation provided by the initializeInputs( ) and readInputs( ) function. The clarity of the loop( ) is further enhanced using the reference (&) technique.
What do you think of this X macros method as applied to Arduino? Personally, I’m still on the fence as it could be classified as genius or crazy.
Please share your thoughts in the comments section below. We are especially interested to learn how you used X macros in your projects. What are the gotcha problems you encountered?
Best wishes,
APDahlen
Related Information
Please follow these links to related and useful information:
About this author
Aaron Dahlen, LCDR USCG (Ret.), serves as an application engineer at DigiKey. He has a unique electronics and automation foundation built over a 27-year military career as a technician and engineer which was further enhanced by 12 years of teaching (interwoven). With an MSEE degree from Minnesota State University, Mankato, Dahlen has taught in an ABET-accredited EE program, served as the program coordinator for an EET program, and taught component-level repair to military electronics technicians. Dahlen has returned to his Northern Minnesota home and thoroughly enjoys researching and writing articles such as this.
