Programming a PLC to control your actuator can be one of the more difficult projects to undertake. It requires trial and error, testing and a heap of patience; though the results can be incredibly functional and rewarding.
The code in this tutorial is a complete program for a functional Serial Responsive Actuator. Utilizing the Serial Monitor with your PLC will allow you to interface your Arduino system with other computers, modules or automation systems.
We will review each function and section of the code as we go, so you may pull specific functions out of this tutorial. Please note, some functions call upon other functions that are not included in the same code block, so you will need to check over your code to ensure it will execute properly.
The full code will be posted at the end of this article. If you would like to use the full code, copy and paste it into your IDE software.
Program Overview
When initialized, this program will home and calibrate your actuators, finding the ends of motion for the unit. It then moves the actuator to the middle point of stroke to wait for further commands. You can then input a number into your Serial Monitor (from your PC) and the actuator will move to that position. This program does not get confused by end-of-stroke shutoff, and works with any Hall Effect Sensor or Optical Sensor actuator.
Parts List
Components with a (*) are included in the Arduino Starter Kit
- Arduino PLC (Uno* or Mega 2560)
- Motor Driver (High Current AD-MD6321) – 1 per actuator.
- 12v Power Supply for Actuators (Exceed cumulative amperage draw)
- 1 Breadboard, as needed*
- Jumper Wires for Arduino PLC – each high current MD will require 6 Female connections.
- 1-Pin Wire for PLCs* (M-F 10pc/pk)
- Jumper Wire Kit (M-M, 75pc/pk; various lengths);
- Actuators with Feedback Sensors and Actuator Mounting Brackets, as needed. (Hall Effect sensor or Optical Sensor actuators only, for this code)
Functions
Summary of Functions
-
void setup()
: Initializes pins, starts serial communication, and runs homing and calibration routines. -
void homingRoutine()
: Initiates homing, moves the actuator to full retraction, and sets the minimum stroke. -
void calibrateActuator()
: Initiates calibration, moves the actuator to full extension, and sets the maximum stroke. -
void loop()
: Checks for serial input, updates target position, and controls the actuator movement. -
void movement()
: Controls the direction of actuator movement based on the target position. -
void readSensor()
: Reads sensor values, updates position, and prints sensor readings. -
void stopMotor()
: Stops the actuator movement. -
bool isEndOfStroke()
: Checks for the end of stroke conditions and handles timeouts.
Variables
Variables in your code must be called before they are used. Best programming practice is to assign and set your variables at the top of your code, to make modifying or 'tuning' your variables easier.
These are the variables used for this code, with a comment explaining their function and possible range.
//Actuator Specifications
int maxStroke; // Leave undefined, set during calibration
int minStroke; // Leave undefined, set during homing
// Input Variables (Pins) - Change to match your pinout
const int Xpin=10;
const int Rpin=11;
// Sensor Read Pin 1
const int sensorPin=3;
// Hall Effect Sensor Input (Sensor Read pin 2) - Change to match your pinout
const int sensorPin2=4;
int sensorCount2;
// Position Target Function Variables
int targetNumber;
int currentPosition;
int lastPosition=0;
// Motor Control Variables
bool active = false; // Actuator On / Off toggle flag
bool EOSFlag=false; // End of Stroke Flag
// Sensor Readings
int sensorValue;
int lastSensorValue = LOW;
int sensorValue2;
int lastSensorValue2 = LOW;
// Variables for Debounce and End of Stroke Sensing
const unsigned long motionTimeout = 2000;
// Adjust this value based on your requirements (in milliseconds) 2000=2s
const unsigned long CALIBRATION_TIMEOUT=3000;
// Adjust this value based on your requirements (in milliseconds) 3000=3s
unsigned long lastMotionTime = millis(); //Timer function for EOS function
// Position Variables
unsigned long pulseCount = 0;
int direction = 0; // Used for incremental movement count
// 0: Stopped, 1: Moving Forward, -1: Moving Backward
Setup/Initialization
The void setup(); function is a base Arduino function that initializes the PLC, begins the Serial Monitor, and assigns inputs and outputs as programmed.
This setup function assigns 4 pins as Inputs or Outputs, then runs the homing and calibration routines.
void setup() {
pinMode(Xpin, OUTPUT);
pinMode(Rpin, OUTPUT);
pinMode(sensorPin, INPUT_PULLUP);
pinMode(sensorPin2, INPUT_PULLUP);
Serial.begin(115200);
homingRoutine();
calibrateActuator();
}
Homing Routine
This code snippet is the function for homing your feedback actuators. This code will retract your actuator until the end of stroke is detected, and will assign that position a value of "0" - then it initializes the calibration function.
In the full program, the Homing and Calibration functions are run once, during startup, and not called again.
This code snippet calls additional function(s) that are not included in this block.
void homingRoutine() {
active=true;
Serial.println("Homing Initiated");
digitalWrite(Xpin, LOW);
digitalWrite(Rpin, HIGH);
while (!EOSFlag) {
direction=-1;
readSensor();
isEndOfStroke();
// Move actuator to full retraction
}
direction=0;
minStroke=currentPosition;
Serial.println("Homing Completed");
}
Calibration Routine
Next, we have our Calibration routine. This function will fully extend your actuator after the Homing function has fully retracted the unit. The purpose of this routine is to count the sensor pulses from one end of the actuator's stroke to the other. This value is then saved as the maxStroke variable and used for positioning. This code can be modified to process Potentiometer Feedback by mapping voltage to position, rather than counting pulses.
This code snippet calls additional function(s) that are not included in this block.
void calibrateActuator() {
Serial.println("Calibration Initiated");
active = true;
// Reset variables
pulseCount = 0;
currentPosition = 0;
lastMotionTime=millis();
// Move actuator to full extension
digitalWrite(Xpin, HIGH);
digitalWrite(Rpin, LOW);
direction=1;
// Wait until the end of stroke is reached during calibration
while (!isEndOfStroke()) {
readSensor();
// Add a timeout condition to avoid infinite loop
if (millis() - lastMotionTime > motionTimeout) {
Serial.println("Calibration Timeout");
stopMotor();
maxStroke=currentPosition;
direction=0;
// Print the calibration results
Serial.print("Calibration Complete. Minimum Stroke: ");
Serial.print(minStroke);
Serial.print(" Maximum Stroke: ");
Serial.println(maxStroke);
targetNumber=((maxStroke+minStroke)/2);
break;
}
}
}
Loop
The void loop() function is the main body of your Arduino code. Anything written within the loop() function will be executed repeatedly during the program.
Programmers Note: I personally find it easiest to keep the loop function as simple as possible. This is the part of the code that will call most other functions in the code, and cannot be lifted without the rest of the code.
In this loop function, the program checks for serial monitor for input, updates target position with your command, and calls up the movement function. Most of the messages the PLC sends to the Serial Monitor come from this code snippet.
There is a line of code commented out below - when included, this line of code maps an input of 0-100 to the actuator. I.e. Your input is now a percentage of the actuator's stroke. This line can also be modified to utilize other inputs in a similar mapped format.
This code snippet calls additional function(s) that are not included in this block.
void loop() {
if (!active && Serial.available() > 0) {
String serialInput = Serial.readStringUntil('\n');
Serial.print("Received: ");
Serial.println(serialInput);
if (serialInput.length() > 0) {
targetNumber = serialInput.toInt();
//targetNumber = map(targetNumber, 0, 100, minStroke, maxStroke);
/*If the above line is active, you will input a value between 0-100,
The program will use this input as a % of stroke.*/
Serial.print("Target number: ");
Serial.println(targetNumber);
EOSFlag = false;
}
// Clear the serial buffer
while (Serial.available()) {
Serial.read();
}
}
if (targetNumber != currentPosition) {
active = true;
movement();
}
/*
if (!active) {
Serial.println("Waiting for Input");
return;
} */
if (active && targetNumber == currentPosition) {
stopMotor();
Serial.println("Target Met");
}
}
Movement
The Movement function controls the direction of actuator movement based on the target position. This is the only function in this program that tells the motor driver to move the actuator.
This code snippet calls additional function(s) that are not included in this block.
void movement() {
if (targetNumber > currentPosition) {
digitalWrite(Xpin,HIGH);
digitalWrite(Rpin,LOW);
//Serial.println(" Extending");
direction = 1;
} else if (targetNumber < currentPosition) {
digitalWrite(Rpin,HIGH);
digitalWrite(Xpin,LOW);
direction = -1;
//Serial.println("Retracting");
} else if (targetNumber == currentPosition) {
stopMotor();
delay(10);
}
if(active) {
readSensor();
}
if (isEndOfStroke()) {
return; // Skip further movement actions
}
}
Sensor Read
The Sensor processing function reads sensor values, updates position, and prints sensor readings to the Serial Monitor. The Serial commands may be used for debugging, or commented out for a less-busy readout.
This code snippet can be used in any program and will give an accurate reading of pulses from a feedback sensor.
void readSensor() {
sensorValue = digitalRead(sensorPin);
if(lastSensorValue != sensorValue) {
lastSensorValue = sensorValue;
pulseCount = pulseCount + direction;
Serial.print("Sensor 1: ");
Serial.println(pulseCount);
}
sensorValue2 = digitalRead(sensorPin2);
if(lastSensorValue2 != sensorValue2) {
lastSensorValue2 = sensorValue2;
sensorCount2=sensorCount2+direction;
pulseCount = pulseCount + direction;
Serial.print("Sensor 2: ");
Serial.println(sensorCount2);
Serial.print("Current Position: ");
Serial.println(currentPosition);
}
currentPosition = pulseCount;
}
Motor Stopping and Deactivation
Stops the actuator movement and re-enables the unit to read the Serial Monitor for further commands. This is the only code snippet in this program that will stop the actuator's motion.
This code snippet can be used in any program with any actuator, as it just turns off the motor and changes the 'active' state.
void stopMotor() {
if (active) {
active=false;
digitalWrite(Xpin,LOW);
digitalWrite(Rpin,LOW);
}
}
End of Stroke Check
Checks for the end of stroke conditions and handles timeouts. When the actuator hits end of stroke and stops moving, this function will trigger and reset the actuator for new command inputs, even if the target number has not yet been reached.
This code snippet calls additional function(s) that are not included in this block.
bool isEndOfStroke() {
// Check if there is motion (changes in the pulse count)
if (active && (currentPosition != lastPosition)) {
lastMotionTime = millis(); // Update the time of the last motion
lastPosition = currentPosition;
EOSFlag=false;
}
// Check if there is no motion for the specified timeout
if (active && ((millis() - lastMotionTime) > motionTimeout)) {
if(EOSFlag!=true) {
Serial.print("Timeout - ");
Serial.println("At limit");
EOSFlag=true;
}
direction=0;
stopMotor();
return true;
}
return false;
}
FULL CODE
//Actuator Specifications
int maxStroke;
int minStroke;
// Input Variables (Pins)
const int Xpin=10;
const int Rpin=11;
const int sensorPin=3;
// Hall Effect Sensor Input
const int sensorPin2=4;
int sensorCount2;
// Motor Function Variables
int targetNumber;
int currentPosition;
int lastPosition=0;
bool active = false;
bool EOSFlag=false;
// Sensor Readings
int sensorValue;
int lastSensorValue = LOW;
int sensorValue2;
int lastSensorValue2 = LOW;
// Variables for Debounce
const unsigned long motionTimeout = 2000; // Adjust this value based on your requirements (in milliseconds)
const unsigned long CALIBRATION_TIMEOUT=3000; // Adjust this value based on your requirements (in milliseconds)
unsigned long lastMotionTime = millis();
// Position Variables
unsigned long pulseCount = 0;
int direction = 0; // 0: Stopped, 1: Moving Forward, -1: Moving Backward
void setup() {
pinMode(Xpin, OUTPUT);
pinMode(Rpin, OUTPUT);
pinMode(sensorPin, INPUT_PULLUP);
pinMode(sensorPin2, INPUT_PULLUP);
Serial.begin(115200);
homingRoutine();
calibrateActuator();
}
void homingRoutine() {
active=true;
Serial.println("Homing Initiated");
digitalWrite(Xpin, LOW);
digitalWrite(Rpin, HIGH);
while (!EOSFlag) {
direction=-1;
readSensor();
isEndOfStroke();
// Move actuator to full retraction
}
direction=0;
minStroke=currentPosition;
Serial.println("Homing Completed");
}
void calibrateActuator() {
Serial.println("Calibration Initiated");
active = true;
// Reset variables
pulseCount = 0;
currentPosition = 0;
lastMotionTime=millis();
// Move actuator to full extension
digitalWrite(Xpin, HIGH);
digitalWrite(Rpin, LOW);
direction=1;
// Wait until the end of stroke is reached during calibration
while (!isEndOfStroke()) {
readSensor();
// Add a timeout condition to avoid infinite loop
if (millis() - lastMotionTime > motionTimeout) {
Serial.println("Calibration Timeout");
stopMotor();
maxStroke=currentPosition;
direction=0;
// Print the calibration results
Serial.print("Calibration Complete. Minimum Stroke: ");
Serial.print(minStroke);
Serial.print(" Maximum Stroke: ");
Serial.println(maxStroke);
targetNumber=((maxStroke+minStroke)/2);
break;
}
}
}
void loop() {
if (!active && Serial.available() > 0) {
String serialInput = Serial.readStringUntil('\n');
Serial.print("Received: ");
Serial.println(serialInput);
if (serialInput.length() > 0) {
targetNumber = serialInput.toInt();
Serial.print("Target number: ");
Serial.println(targetNumber);
EOSFlag = false;
}
// Clear the serial buffer
while (Serial.available()) {
Serial.read();
}
}
if (targetNumber != currentPosition) {
active = true;
movement();
}
/*
if (!active) {
Serial.println("Waiting for Input");
return;
} */
if (active && targetNumber == currentPosition) {
stopMotor();
Serial.println("Target Met");
}
}
void movement() {
if (targetNumber > currentPosition) {
digitalWrite(Xpin,HIGH);
digitalWrite(Rpin,LOW);
//Serial.println(" Extending");
direction = 1;
} else if (targetNumber < currentPosition) {
digitalWrite(Rpin,HIGH);
digitalWrite(Xpin,LOW);
direction = -1;
//Serial.println("Retracting");
} else if (targetNumber == currentPosition) {
stopMotor();
delay(10);
}
if(active) {
readSensor();
}
if (isEndOfStroke()) {
return; // Skip further movement actions
}
}
void readSensor() {
sensorValue = digitalRead(sensorPin);
if(lastSensorValue != sensorValue) {
lastSensorValue = sensorValue;
pulseCount = pulseCount + direction;
Serial.print("Sensor 1: ");
Serial.println(pulseCount);
}
sensorValue2 = digitalRead(sensorPin2);
if(lastSensorValue2 != sensorValue2) {
lastSensorValue2 = sensorValue2;
sensorCount2=sensorCount2+direction;
pulseCount = pulseCount + direction;
Serial.print("Sensor 2: ");
Serial.println(sensorCount2);
Serial.print("Current Position: ");
Serial.println(currentPosition);
}
currentPosition = pulseCount;
}
void stopMotor() {
if (active) {
active=false;
digitalWrite(Xpin,LOW);
digitalWrite(Rpin,LOW);
}
}
bool isEndOfStroke() {
// Check if there is motion (changes in the pulse count)
if (active && (currentPosition != lastPosition)) {
lastMotionTime = millis(); // Update the time of the last motion
lastPosition = currentPosition;
EOSFlag=false;
}
// Check if there is no motion for the specified timeout
if (active && ((millis() - lastMotionTime) > motionTimeout)) {
if(EOSFlag!=true) {
Serial.print("Timeout - ");
Serial.println("At limit");
EOSFlag=true;
}
direction=0;
stopMotor();
return true;
}
return false;
}