Introduction to Arduino with Allen-Bradley DF1
The purpose of this post is to document my progress on using Arduino with Allen-Bradley DF1 protocol. Particularly, for this experiment, I am using an Arduino MEGA with the MAX232 module. The mega has 3 UART ports. Also, I’m using the SLC 5/04 processor. In order to connect the MAX232 to the 5/04, I’m using a serial extension cable. Evidently, some of the MAX232 modules are inconsistent in their labeling. In this case, however, I connected TX on the MEGA to TX(In) on the MAX232 module. Likewise, I connect RX from the MEGA to RX on the MAX232. Without another good resource to have available when working with DF1 is the protocol and command set manual.
Arduino with Allen-Bradley DF1 Library
I attempted to use the DF1 Library by Cartiman from this link. Despite the code being written well, I could not get it to work well for this application. Basically, writing to the processor was no problem, but I could not get it to read a value. I’m not sure if this is because I’m using a 5/04 with a MEGA, or if I have something else set up differently. Furthermore, I might not be implementing the code properly.
DF1 Packet Layout
Firstly, let’s take a look at the packets of the DF1 Protocol for the MSG instruction. I figured if I can emulate the message instruction using the MEGA, we can get good data. At this point, I used a Serial.read(); to capture each byte the SLC sends out when trying to message another processor.
Before we begin, I’m going to take a look at the channel configuration of the 5/04 processor. Be sure Channel 0 is in system mode (on the general tab), and here is a snapshot of my Channel 0 system tab.
At this point, I’ll add some logic to the SLC to send out a message: If you use the example, be sure to create data table N11.
My Setup screen is as follows:
At last, I’ll download to the SLC. Notice I’m only sending the message once every 20 seconds to avoid getting overwhelmed with data. Obviously, we will get an error on the message instruction. The purpose of this message is to simply see how the packets are laid out as a read is requested by the message instruction.
Through Trial and error, I determined the total packet length was 24 bytes, so I wrote the following code for the MEGA to see what these packets are.
byte FromSLC[30];
int i = 0;
byte LastRead;
void setup() {
// Port for Serial Monitor in Arduino IDE
Serial.begin(9600);
// I'm using Serial1 to communicate to the 5/04 through
// the MAX232 module.
Serial1.begin(9600);
}
void loop() {
if (Serial1.available() > 0) {
FromSLC[i] = Serial1.read();
Serial.print(FromSLC[i],HEX);
Serial.print("-");
if (i==23){
Serial.println();
i=-1;
}
i++;
}
At this point, the serial monitor displayed the data as follows. Remember that each line is a separate execution of the MSG instruction in the SLC:
At last, let’s break down each byte and see what it is for. Some of these bytes are well documented in the protocol and command manual. Others, I had to guess at from other resources. If you see a byte that I have mis-interpreted, please comment below. I’ll use the last line above as the example.
0x10; //Data Link Escape
0x2; // STX Start of Text LOW
0x1; // Destination
0x0; // Source
0xF; // Command Code
0x0; // Status
0xC0; // TNS High (Transaction Counter)
0x8B; // TNS Low should increment each message
0xA2; // Function Code -- Read
0x2; // Size in bytes
0x7; // File Number
0x89; // File Type -- 0x89 is integer
0x0; // Element
0x0; // Sub Element
0x10; // Data Link Escape
0x3; // End of Text
0x25; // Checksum High
0xBB; // Checksum Low
Then at the end of the message, you see three ENQueries because nothing was available to send an ACK.
I’m sending out the bytes in this order: STX (10-2) – (The 12-Byte command) – ETX (10-3) – Checksum (2 bytes).
We get a 10-6 back from the processor, which is a DLE-ACKnowledge
Calculate the CRC
In the meantime, we will need to be able to calculate the CRC Checksum. This is a Cyclic Redundancy Check. The purpose of the CRC is to ensure that our packet did not contain any errors. This is simply a code that we calculate, and append to the end of the our message after the ETX command. This will eventually change for each message because the TNS counter will be incrementing.
I was able to check the result by monitoring the packets and checksum sent out through the 5/04’s Channel 0, format my message in the same way, and verify the checksum calculation matched what the 5/04 sent out.
Reading the Data
After we send out the command, we get a reply from the processor as follows:
10-2 is a DLE STX. 0 is our source, and 1 is the destination. 4F is the command that indicates this is a reply from the processor. 0 represents the status. C0 and 8B are the TNS counters. Then we have the Data… Which is 16 Hex=22 Decimal. This matches my data file for N7:0.
Final Sketch for reading from an SLC using Arduino with Allen-Bradley DF1
At any rate, I’ve put together a working Sketch. This should read N7:0, N7:1, and N7:2, and F8:0 right out of the box. Be sure your Serial port on the SLC is set for 9600/8/n/1/ with a source ID of 1. Your Serial Monitor should be set at 115,200 bps. Also, be sure you have the N7 data file expanded to at least 3 elements. I used Cartiman’s code for calculating the CRC, but put the rest together from scratch. Let me know if you have any problems, or find any errors! Use this sketch at your own risk. I’m just posting my sketch as an example.
// For Arduino Mega --- MAX232 connected to UART1
// ** John 3:16 ** //
// Checksum calculations are from the DF1 Library.
// https://program-plc.blogspot.com/2015/11/allen-bradley-plc-arduino-allen-bradley.html
// Other Logic written by Ricky Bryce
// https://bryceautomation.com
// Use at your own risk.
// **** Declare Variables for Tags ****//
// Optional
int N7_0;
float F8_0;
// **** Standard Variables **** //
const uint8_t STX = 0x2; // Start of Text
const uint8_t ENQ = 0x5; // Enquiry
const uint8_t ACK = 0x6; // Acknowledge
const uint8_t NAK = 0x15; // Not Acknowledged
const uint8_t EOT = 0x4; // End of Transmission
const uint8_t ETX = 0x3; // End of Text
const uint8_t DLE = 0x10; // Data Link Escape
// **** Command Variables **** //
const uint8_t DST = 0x0; // Destination Address
const uint8_t SRC = 0x1; // Source Address
const uint8_t CMD = 0xF; // Command Code
uint8_t STS = 0x0; // Status
uint8_t TNS[] = {0x0, 0x0}; // Transaction Counter
uint8_t FNS = 0xA1; // Function Code ******** Changed from A2
uint8_t SZE = 0x2; // Data Size
uint8_t FLN = 0x7; // File Number
uint8_t FTY = 0x89; // File Type
uint8_t ELE = 0x0; // Element Number
uint8_t SEL = 0x0; // Sub-Element
uint8_t CKSTX[2];
uint8_t CKSRX[2];
uint8_t ReadCommand[] = {DST, SRC, CMD, STS, TNS[0], TNS[1], FNS, SZE, FLN, FTY, ELE, SEL}; //Deleted SEL at end
uint8_t RXData[20];
uint8_t ChecksumRX[2];
uint8_t RXDataCounter = 0;
uint8_t RXCounter = 0;
uint8_t LastRXDataCounter = 0;
uint8_t ChecksumCounter = 0;
uint8_t LastByte = 0;
uint8_t CurrentByte = 0;
/* Table of CRC values for CRC16 */
const unsigned int CRC16table[] = {0x0, 0xc0c1, 0xc181, 0x140, 0xc301, 0x3c0, 0x280, 0xc241,
0xc601, 0x6c0, 0x780, 0xc741, 0x500, 0xc5c1, 0xc481, 0x440,
0xcc01, 0xcc0, 0xd80, 0xcd41, 0xf00, 0xcfc1, 0xce81, 0xe40,
0xa00, 0xcac1, 0xcb81, 0xb40, 0xc901, 0x9c0, 0x880,
0xc841, 0xd801, 0x18c0, 0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81,
0x1a40, 0x1e00, 0xdec1, 0xdf81, 0x1f40,
0xdd01, 0x1dc0, 0x1c80, 0xdc41, 0x1400, 0xd4c1, 0xd581, 0x1540,
0xd701, 0x17c0, 0x1680, 0xd641, 0xd201, 0x12c0, 0x1380, 0xd341,
0x1100, 0xd1c1, 0xd081, 0x1040, 0xf001, 0x30c0, 0x3180, 0xf141,
0x3300, 0xf3c1, 0xf281, 0x3240, 0x3600, 0xf6c1, 0xf781, 0x3740,
0xf501, 0x35c0, 0x3480, 0xf441, 0x3c00, 0xfcc1, 0xfd81,
0x3d40, 0xff01, 0x3fc0, 0x3e80, 0xfe41, 0xfa01, 0x3ac0, 0x3b80,
0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840, 0x2800,
0xe8c1, 0xe981, 0x2940, 0xeb01, 0x2bc0, 0x2a80, 0xea41, 0xee01,
0x2ec0, 0x2f80, 0xef41, 0x2d00, 0xedc1, 0xec81, 0x2c40, 0xe401,
0x24c0, 0x2580, 0xe541, 0x2700, 0xe7c1, 0xe681, 0x2640, 0x2200,
0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0, 0x2080, 0xe041, 0xa001,
0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281, 0x6240, 0x6600,
0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441, 0x6c00,
0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41, 0xaa01,
0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840, 0x7800,
0xb8c1, 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41, 0xbe01,
0x7ec0, 0x7f80, 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40, 0xb401,
0x74c0, 0x7580, 0xb541, 0x7700, 0xb7c1, 0xb681, 0x7640, 0x7200,
0xb2c1, 0xb381, 0x7340, 0xb101, 0x71c0, 0x7080, 0xb041, 0x5000,
0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0, 0x5280, 0x9241, 0x9601,
0x56c0, 0x5780, 0x9741, 0x5500, 0x95c1, 0x9481, 0x5440, 0x9c01,
0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40, 0x5a00,
0x9ac1, 0x9b81, 0x5b40, 0x9901, 0x59c0, 0x5880, 0x9841, 0x8801,
0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40, 0x4e00,
0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0, 0x4c80, 0x8c41, 0x4400,
0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641, 0x8201,
0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081, 0x4040
};
unsigned int calculated_checksumtx;
unsigned int calculated_checksumrx;
int OneShotFlag = 0;
int RecordFlag = 0;
int StartChecksumFlag = 0;
int ResendFlag = 0;
int RXExecutionFlag;
int MSGCounter = 0;
float MSGResult = 0; // was uint16_t
int CurrentMillis = 0;
int PreviousMillis = 0;
int CounterMillis = 0;
float Results[10][4];
int ResultsCounter = 0;
int MessageNumber = 0;
void setup() {
Serial.begin(115200);
//Serial.end();
Serial1.begin(9600);
// Initialize Variables
CKSTX[0] = 0x0; // Checksum High Must recalculate
CKSTX[1] = 0xF6; // Checksum Low Must recalculate
CKSRX[0] = 0x0; // Checksum High Must recalculate
CKSRX[1] = 0x0; // Checksum Low Must recalculate
TNS[0] = 0x0; // Transaction Counter High
TNS[1] = 0xFA; // Transaction Counter Low
MSGResult = 0;
for (int x = 0; x<10; x++){
Results[x][0]=0;
Results[x][1]=0;
Results[x][2]=0;
Results[x][3]=0;
}
}
void loop() {
CurrentMillis = millis();
//ReadCommand[] = {DST, SRC, CMD, STS, TNS[0], TNS[1], FNS, SZE, FLN, FTY, ELE, SEL};
MessageNumber = 0; // 0 to 9 based on which message you write here
if ((Results[MessageNumber][3] == 0) and (MSGCounter == MessageNumber)) { // Has not run
updateTNS(); // Increments the Transaction Counter
// Answer will be stored in Results[0][2] N7:0
Results[MessageNumber][0] = TNS[0]; //Transaction Counter High
Results[MessageNumber][1] = TNS[1]; // Transaction Counter Low
ReadCommand[7] = 2; // Size in Bytes
ReadCommand[8] = 7; // Data File #
ReadCommand[9] = 0x89; //Integer File Type
ReadCommand[10] = 0; // Element #
ReadCommand[11] = 0; // SubElement
calculateCKSTX();
MSG();
Results[MessageNumber][3] = 1; // Has Run
}
MessageNumber = 1; // 0 to 9 based on which message you write here
if ((Results[MessageNumber][3] == 0) and (MSGCounter == MessageNumber)) { // Has not run
updateTNS(); // Increments the Transaction Counter
// Answer will be stored in Results[1][2] N7:1
Results[MessageNumber][0] = TNS[0]; // Transaction Counter High
Results[MessageNumber][1] = TNS[1]; // Transaction Counter Low
ReadCommand[7] = 2; // Size in Bytes
ReadCommand[8] = 7; // Data File #
ReadCommand[9] = 0x89; //Integer File Type
ReadCommand[10] = 1; // Element #
ReadCommand[11] = 0; // SubElement
calculateCKSTX();
MSG();
Results[MessageNumber][3] = 1; // Has Run
}
MessageNumber = 2; // 0 to 9 based on which message you write here
if ((Results[MessageNumber][3] == 0) and (MSGCounter == MessageNumber)) { // Has not run
updateTNS(); // Increments the Transaction Counter
// Answer will be stored in Results[1][2] N7:1
Results[MessageNumber][0] = TNS[0]; // Transaction Counter High
Results[MessageNumber][1] = TNS[1]; // Transaction Counter Low
ReadCommand[7] = 2; // Size in Bytes
ReadCommand[8] = 7; // Data File #
ReadCommand[9] = 0x89; //Integer File Type
ReadCommand[10] = 2; // Element #
ReadCommand[11] = 0; // SubElement
calculateCKSTX();
MSG();
Results[MessageNumber][3] = 1; // Has Run
}
MessageNumber = 3; // 0 to 9 based on which message you write here
if ((Results[MessageNumber][3] == 0) and (MSGCounter == MessageNumber)) { // Has not run
updateTNS(); // Increments the Transaction Counter
// Answer will be stored in Results[1][2] N7:1
Results[MessageNumber][0] = TNS[0]; // Transaction Counter High
Results[MessageNumber][1] = TNS[1]; // Transaction Counter Low
ReadCommand[7] = 4; // Size in Bytes
ReadCommand[8] = 8; // Data File #
ReadCommand[9] = 0x8A; //Float File Type
ReadCommand[10] = 0; // Element #
ReadCommand[11] = 0; // SubElement
calculateCKSTX();
MSG();
Results[MessageNumber][3] = 1; // Has Run
}
if (CurrentMillis - CounterMillis > 100) {
MSGCounter++;
if (MSGCounter == 10) {
MSGCounter = 0;
}
CounterMillis = CurrentMillis;
}
// Clear all "Has Run" Bits
if (CurrentMillis - PreviousMillis > 1000) {
for (int k = 0; k < 10; k++) {
Results[k][3] = 0;
MSGCounter = 0;
}
PreviousMillis = CurrentMillis;
CounterMillis = CurrentMillis;
}
ProcessSerialRX();
// Results
N7_0 = Results[0][2];
}
void updateTNS() {
// Increment the Transaction Counter
if (TNS[1] != 0xFF) {
TNS[1]++;
}
if (TNS[1] == 0xFF) {
TNS[0]++;
TNS[1] = 0;
}
if (TNS[0] == 0xFF) {
TNS[1] = 0x1;
TNS[0] = 0x0;
}
ReadCommand[4] = TNS[0];
ReadCommand[5] = TNS[1];
}
int MSG() {
Serial.println("Message (HEX): ");
Serial1.write(DLE);
Serial.print(DLE, HEX);
Serial.print(" ");
Serial1.write(STX);
Serial.print(STX, HEX);
Serial.print(" ");
// Send ReadCommand to Serial Monitor
for (int i = 0; i <= 11; i++) {
Serial1.write(ReadCommand[i]);
Serial.print(ReadCommand[i], HEX);
Serial.print(" ");
}
Serial1.write(DLE);
Serial.print(DLE, HEX);
Serial.print(" ");
Serial1.write(ETX);
Serial.print(ETX, HEX);
Serial.print(" ");
Serial1.write(CKSTX[0]);
Serial.print(CKSTX[0], HEX);
Serial.print(" ");
Serial1.write(CKSTX[1]);
Serial.print(CKSTX[1], HEX);
Serial.print(" ");
Serial.println(" --> MSG Sent");
}
void ProcessSerialRX() {
RXExecutionFlag = 1;
if (Serial1.available() > 0) {
CurrentByte = Serial1.read(); // Current Data Byte
if (StartChecksumFlag == 1) {
ChecksumRX[ChecksumCounter] = CurrentByte;
ChecksumCounter++;
if (ChecksumCounter == 2) {
StartChecksumFlag = 0;
ChecksumCounter = 0;
Serial.println();
Serial.print("Checksum Received: ");
Serial.print(ChecksumRX[0], HEX);
Serial.print(ChecksumRX[1], HEX);
Serial.println();
Serial.print("Checksum Calculated: ");
Serial.print(CKSRX[0], HEX);
Serial.print(CKSRX[1], HEX);
Serial.println();
if ((ChecksumRX[0] == CKSRX[0]) and (ChecksumRX[1] == CKSRX[1l])) {
Serial1.write(DLE);
Serial1.write(ACK);
Serial.println("ACK Sent");
} else {
Serial1.write(DLE);
Serial1.write(NAK);
Serial.println("NAK Sent");
}
Serial.print("ETX at Position: ");
Serial.println(LastRXDataCounter);
if (LastRXDataCounter == 10) { // Integer Data
MSGResult = ((RXData[7] << 8) + RXData[6]);
} else if (LastRXDataCounter == 12) { // Float Data
union u_tag {
byte b[4];
float fval;
} u;
u.b[0] = RXData[6];
u.b[1] = RXData[7];
u.b[2] = RXData[8];
u.b[3] = RXData[9];
MSGResult = u.fval;
}
Serial.println();
RXExecutionFlag = 0;
ResultsCounter = 0;
placeResults();
Serial.println("-------------------------------------------");
}
}
if (RecordFlag == 1) {
RXData[RXDataCounter] = CurrentByte;
Serial.print(CurrentByte, HEX); Serial.print(" ");
RXDataCounter++;
}
if ((CurrentByte == 0x2) and (LastByte == 0x10)) { // STX
RecordFlag = 1;
RXDataCounter = 0;
Serial.println("STX Received");
}
if ((CurrentByte == 0x5) and (LastByte == 0x10)) { // ENQ
Serial.println("ENQ Received");
}
if ((CurrentByte == 0x3) and (LastByte == 0x10)) { //ETX
StartChecksumFlag = 1;
ChecksumCounter = 0;
LastRXDataCounter = RXDataCounter;
Serial.println();
Serial.println("ETX Received");
calculateCKSRX(); // Calculate Checksum
RecordFlag = 0;
}
if ((CurrentByte == 0x15) and (LastByte == 0x10)) { //NAK
ResendFlag = 1;
Serial.println("NAK Received");
}
if ((CurrentByte == 0x6) and (LastByte == 0x10)) { //ACK
Serial.println("ACK Received");
}
LastByte = CurrentByte;
CurrentByte = "";
}
}
void calculateCKSTX() {
calculated_checksumtx = CalculateCRC16TX(12); // Calculate the CRC
CKSTX[0] = (calculated_checksumtx & 0xFF);
CKSTX[1] = (calculated_checksumtx >> 8 & 0xFF);
}
unsigned int CalculateCRC16TX(unsigned char n) {
unsigned int iCRC = 0;
byte bytT = 0;
for (unsigned int i = 0; i < n; i++)
{
bytT = (byte)((iCRC & 0xff) ^ ReadCommand[i]);
iCRC = (unsigned int)((iCRC >> 8) ^ CRC16table[bytT]);
}
bytT = (byte)((iCRC & 0xff) ^ 3);
iCRC = (unsigned int)((iCRC >> 8) ^ CRC16table[bytT]);
return iCRC;
}
void calculateCKSRX() {
calculated_checksumrx = CalculateCRC16RX(8); // Calculate the CRC
CKSRX[0] = (calculated_checksumrx & 0xFF);
CKSRX[1] = (calculated_checksumrx >> 8 & 0xFF);
}
unsigned int CalculateCRC16RX(unsigned char n) {
unsigned int iCRC = 0;
byte bytT = 0;
for (unsigned int i = 0; i < n; i++)
{
bytT = (byte)((iCRC & 0xff) ^ RXData[i]);
iCRC = (unsigned int)((iCRC >> 8) ^ CRC16table[bytT]);
}
bytT = (byte)((iCRC & 0xff) ^ 3);
iCRC = (unsigned int)((iCRC >> 8) ^ CRC16table[bytT]);
return iCRC;
}
void placeResults() {
for (int i = 0; i < 10; i++) {
if ((RXData[4] == Results[i][0]) and (RXData[5] == Results[i][1])) {
Results[i][2] = MSGResult;
}
}
Serial.println();
Serial.println("Results Table");
Serial.println("Message#\tTNS 0\tTNS 1\tResult");
for (int j = 0; j < 10; j++) {
Serial.println();
Serial.print(j);
Serial.print("\t\t");
Serial.print(Results[j][0]);
Serial.print("\t");
Serial.print(Results[j][1]);
Serial.print("\t");
Serial.print(Results[j][2]);
Serial.print("\t");
Serial.print("Results["); Serial.print(j); Serial.print("][2]");
}
Serial.println();
}
This code should work for up to 10 data points without much modification. Just look in the void Loop(). You will see an example of four different messages. The first three are integers, and the last example is a floating point for F8:0. Download and run the sketch, and open your serial monitor. If you have trouble, you might double check the TX and RX are connected properly, and that you are using a STRAIGHT cable to connect to the SLC. I’m not using a NULL modem cable. Your first value from the SLC, for example, will be Results[0][2]; You can use that tag in your sketch as you like. Try to change the value of N7:0 in your processor. The value should update on your serial monitor.
For other information on other topics, visit my main page!
— Ricky Bryce