This is the first of several articles describing how to program a PLC in the language of C. It is applicable to intermediate programmers who wish to improve their programming skills with an emphasis on machine control and safety. The Arduino Opta as shown in Figure 1 will be used as the development platform. Please refer to this page for purchasing information and a list of related TechForum articles.
The primary objective is to simplify the program’s logic by reducing program clutter. An example is shown in this code snippet. The if statement evaluates the selector switch, red pushbutton, and green pushbutton. Based on the status, it will set the run state and the color of the red/green panel lamp. The code technique is best described as pointer injection enhanced by following the conventional PLC program scan where inputs and outputs are read at the top of the superloop.
Download the code: DemoIOUtil.zip (2.0 KB)
if (localSelectorSwitch.value && localStartPB.value && !localStopPB.value) {
runState = true;
localPLRed.value = false;
localPLGreen.value = true;
}
Tech Tip: There is an ongoing debate about whether to use C or C++ in embedded systems. The majority of the Arduino codebase is written in C++. However, this is not universally accepted as there is a balance between the speed and low overhead of C compared to the added protection, abstraction, and elegance associated with C++. All of which may or may not apply depending on how your embedded project is implemented.
This article is written for students in transition between the easy-to-use Arduino to the challenges of embedded programming. For better or worse, C-like constructs are used. You are encouraged to take the next step and convert the routines to C++ and decide for yourself which method is better for your embedded projects.
Figure 1: Image of the Arduino Opta installed on the DigiKey trainer. It may be programmed in the IEC 61131-3 languages C or C++.
Tech Tip: As a starting point, you are encouraged to learn PLC programming using the IEC 61131-3 ladder logic. You will find that PCL mastery is more than just programming, as a PLC is designed to interact in real-time with the physical world. Ladder logic is a good way to shape your thinking about the conditional on-off nature representing the bulk of PLC control and interfacing.
Transitioning beyond the comfort of Arduino
The Opta as pictured in Figure 1 is an underrated contender. While the Opta itself has eight inputs and four relay outputs, we recognize that it is expandable. For instance, a digital and an analog expansion module could be added. There is also a Modbus communication port and an Ethernet port which greatly expand the total number of inputs and outputs using external hardware.
Tech Tip: Don’t think of the Opta as just another little PLC or a “smart relay”. The power of multi-language capability with capable 32-bit dual ARM processors (Cortex-M7 and a Cortex-M4) is unlocked when we leverage the Modbus and Ethernet connections. When combined with external supporting hardware, the Arduino Opta provides a unique community-supported solution for modern edge computing. But first, we need a programming structure to simplify the code base.
This article is written with this expansion in mind. Programming the Opta for real-time responsiveness with potentially dozens of input and output connections is not a trivial task. At some point, we cross the line where abstraction and programming structure become a necessity. Without them the code becomes unmanageable and nearly impossible to troubleshoot.
With that said, strap yourself in and prepare for a deep dive into software development as we learn how to create structures and abstraction. This article serves as a bridge between the calm familiar waters of the Arduino sketch and the structured world of modular programming with a future vision of code safety, portability, and reliability for safety-critical systems.
The struct is a key component of program optimization
As implied in my earlier article, the struct is a key component to program organization. In that article we used the struct to consolidate the variables and constants associated with each servo in the RC robot arm. While the consolidation process can be uncomfortable learning process, the end results are justified. In the end, we were able to handle the servo motors as a single entity which made function like HomeAllServos() very easy to implement.
1) Struct description
We can now extend this encapsulation idea to the Opta’s Input and output pins using a LocalInputPin and LocalOutputPin struct.
typedef struct {
uint8_t pin; // Arduino physical pin number
const char *name; // Descriptive name (e.g., "Start Pushbutton")
bool inverted; // True for invert e.g., a normally closed pushbutton
bool value; // Current state (updated by LocalPinUtil.cpp)
} LocalInputPin;
typedef struct {
uint8_t pin; // Arduino pin number
const char *name; // Descriptive name (e.g., "Motor Enable")
bool value; // Current state
} LocalOutputPin;
2) Instantiate a struct for each input and each output
The magic occurs in the main .ino file when we instantiate structures for the input and output pins. In this code we see that all I/O pin related variables and constants are added to the struct. This includes the physical Arduino I/O pin and a human readable string describing the I/O. There is also a parameter to invert the logic of the input pin to naturally accommodate normally closed switches such as the stop button. The output pins struct are like input structs. You may or may not want to add additional code to invert the output to handle negative logic situations.
LocalInputPin localSelectorSwitch = { A0, "Local Switch", 0, 0 };
LocalInputPin localStopPB = { A1, "Stop Switch", 1, 0 };
LocalInputPin localStartPB = { A2, "Start Switch", 0, 0 };
LocalInputPin localProxSensor = { A3, "Prox Sensor", 0, 0 };
LocalOutputPin localPLGreen = { 0, "Green Panel Lamp", 0 };
LocalOutputPin localPLRed = { 1, "Red Panel Lamp", 1 };
LocalOutputPin localCR1 = { 2, "Control Relay", 0 };
LocalOutputPin localFan = { 3, "Cooling Fan", 0 };
Tech Tip: The normally closed stop pushbutton is a traditional way to guard against a broken wire. As a general statement, things that start a PLC should incorporate normally open switches. Things that stop a machine should be normally closed.
Be sure to evaluate the safety aspects of your machine. Add redundant circuitry as necessary to protect the machinery and more importantly, the operators and technicians.
3) Construct an array of pointers
Next, we construct an array of pointers, with one array for the input pins and one for the output pins.
LocalInputPin* inputPinList[] = {
&localSelectorSwitch,
&localStopPB,
&localStartPB,
&localProxSensor,
};
LocalOutputPin* outputPinList[] = {
&localPLGreen,
&localPLRed,
&localCR1,
&localFan,
};
4) Automatically determine the size of the array of pointers
Automatically calculate the number of input and output pins in each struct by dividing the total size of the struct (bytes) by the number of elements in the struct.
static const uint8_t numInputPins = sizeof(inputPinList) / sizeof(inputPinList[0]);
static const uint8_t numOutputPins = sizeof(outputPinList) / sizeof(outputPinList[0]);
Tech Tip: We could have manually hardcoded the number of input and output pointers. However, that could lead to errors when number of input and outputs are changed.
5) Register the array of structs and then initialize the I/O
The next step is to register the array of structs with LocalPinUtil.cpp. This “injection of pointers” is a critical step toward simplifying out code. It allows the primary .ino file to own the original input and output structs. However, it shifts responsibility to the LocalPinUtil.c to act upon the structs. For example, to perform the pinMode(), digitalWrite(), and digitalRead() operations.
The entire system relies on LocalPinUtil.c maintaining static pointers to the structs that reside in the primary .ino file. The hard part is taken care of by these pointers while the programmer in the main .ino file can happily access member of the input and output structs.
void setup() {
registerLocalInputPins(inputPinList, numInputPins);
registerLocalOutputPins(outputPinList, numOutputPins);
initLocalInOut();
}
6) Call the update function once each loop iteration
The last step is a simple request for the LocalPinUtil.cpp to update the I/O once each loop iteration. This approach is like the PLC’s program scan. Recall that the PLC program scan is composed of several stages:
-
Housekeeping activity
-
Read input
-
Perform the user’s program
-
Set the outputs
At no point does the user’s program operate directly on the I/O pins. Instead, it performs a memory-to-memory operation, directly modifying the value member of the associated struct.
void loop() {
LocalInOutUpdate();
// My program
}
Tech Tip: The traditional PLC program scan does introduce some latency as the input and outputs are not immediately updated. The resulting-real-time performance is widely adopted for the millions of PLCs in the field.
Designated hardware or interrupt capable PLCs may be used for applications requiring faster response times.
7) Use the variables
With the hard work done, we can now use these variables in our program. In this example, we control a cooling fan based on the runState and the value of the proximity sensor. Returning to our original struct definition, we see that each input and output struct has a member called value. We use dot notation to access the desired value member.
This is a near-ideal way to handle the logic for our PLC programs. The greatest benefit is clarity as the digitalRead() and digitalWrite() functions have been relegated to the utilities .c file. Also, note that the Arduino-specific functionality has been moved to the LocalPinUtil.c. This could be advantageous, should you decide to port the code to another environment.
if (runState && localProxSensor.value) { //Turn selector switch off and press the red button to clear fault state.
localFan.value = true;
} else {
localFan.value = false;
}
Parting thoughts
The code listing uses a C approach to programming as opposed to a C++ Object Oriented style. The pointer injection process is relatively simple. It is also fast, as the memory locations are directly accessed as opposed to using the stack of buffer to transfer values.
Where do we go from here?
That’s easy, the entire article was focused on the local I/O. We need to talk about local I/O such as a Modbus utility file. If we carefully construct the program, we should be able to seamlessly integrate the remote with the local.
Please share your thoughts on this topic. We would love to hear your programming experience with this pointer injection technique. Better yet, do you have a more effective method?
Comments—with clarification—about why C is better than C++ or vice versa are also welcome.
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.