This week I was able to get the DF1 protocol operational. It is not a compete implementation of the protocol, but it implements read and write operations for data transfer to/from a SLC 500 processor.
RS-232 communication
As mentioned in a previous post I am using a USB to RS232(Product LinkProduct Link) cable to be able to communicate to a SLC 500 PLC. The data sheet can be found here http://www.farnell.com/datasheets/814051.pdf. I had to make up a 9 pin (DB9) Female connector to connect to the PLC.
The cable pin out can be seen below:
The TXD on the cable is connected to the RXD (pin 2) on the SLC and the RXD on the cable is connected to the TXD (pin 3) on the SLC. The GND on the cable is connected to the GND (pin 5) on the SLC. Once connected it works very well. The USB side of the cable has 2 LED's - a red for transmit and a green for receive. So it is very easy to see when signal transmission is happening. I have been very pleased with the performance of the cable.
DF1 Protocol
I am communicating to an Allen Bradley SLC500 PLC using the DF1 protocol over a RS-232RS-232 serial connection The protocol can be found in the DF1 Protocol and Command Set Reference Manual http://literature.rockwellautomation.com/idc/groups/literature/documents/rm/1770-rm516_-en-p.pdf). I have found this to not be a complete description of all of the commands.
A typical command looks like this:
The protocol uses variable length packets that, with the exception of a couple of commands, start with standard header information of DEST, SRC, CMD, STS, and TNS. SRC and DEST are self explanatory, identifying the address of the device sending the command and the address of the device the message is being sent to. The DF1 protocol has many different CMD’s, but the ones I have programmed are in the ‘0F’ family – Protected Read and Write. The STS is an error code – really only intended for response packets, so set to 0 for commands. The TNS is a unique identifier for each message. The way I have implemented the protocol is to start with TNS of 1 and increment until it reaches 0xFFFF and then start over again.
Application specific data includes the protocol function. For reads and writes, the ADDR field is used to identify the data register to read/write to (e.g. N7:30) The SIZE byte specifies how many bytes to read/write. The DATA field is used when writing data to a register(s) and includes the data to be written.
The DATA field is delineated along word boundaries (16 bits/2 bytes). The Data must be encoded in ‘Little Endian’ format, where the low byte resides in bits 8-15 and the high byte resides in bits 0-7. The Raspberry Pi stores data in ‘Big Endian’ format where the high byte resides in bits 8-15 and the low byte resides in bits 0-7. This requires some manipulation of the DATA bytes before sending.
The struct module, which allows strings to be interpreted as packed binary data, offers a simple way to do this.
struct.pack('<h', self.Data)
where ‘<’ represents encoding in Little Endian format and the ‘h’ represents a signed integer (16 bits)
Response Packet
The handshaking that goes on requires the PLC to 'ACK'nowledge that it has received the command and a short while later it will send the response message. The response message looks very similar to the command message:
The SRC and DEST bytes are reversed in the response packet - because the PC is now the source and the Pi is the destination. The STS byte will be 0 unless there is an error and then it will hold the error byte. The TNS word will match to the TNS of the command packet. Read commands will have a response with the data requested. Again this data is encoded in Little Endian format.
After the message has been successfully received by the Pi, the Pi needs to 'ACK'nowledge the response. This completes the message transfer process.
Strings
ST file types took me a little while to figure out. When you read a ST file type, the data returned is 82 bytes, regardless of the length of the actual string. If the string is less than 82 bytes, the string is padded with \x00’s. What took me a while to figure out was that the string data is returned in Little Endian format (every 2 characters are swapped). So if you had a string ‘HELLO WORLD’ it would come across as ‘EHLL OOWLR D’. Once I figured this out, I was able to write code to account for this. It turns out that the first 2 bytes have the length of the string (in Little Endian format of course), so this can be used to eliminate the \x00’s.
I imagine that other non purely numeric files (BCD, MSG, PD) are similar, but I have not tested these yet.
Code
The code for this portion of the project will be posted at https://github.com/frellwan/SciFy-Pi.git in the Serial/df1 folder.
As with the RS422 communications, I have borrowed heavily from the pymodbus code. This package is well proven and seemed like a good starting point. The serialport instantiating is very simple:
options = utilities.Options() config = SafeConfigParser() config.read([options['config']]) reader = LoggingLineWriter(config.get('FTP', 'localLogDir')) # Create the FTP client FTPhost = config.get('FTP', 'host') FTPport = config.getint('FTP', 'port') ftpEndpoint = TCP4ClientEndpoint(reactor, FTPhost, FTPport) factory = DF1Factory(reader, ftpEndpoint) RS232port = config.get('RS-232', 'host') RS232baud = config.getint('RS-232', 'baudrate') SerialDF1Client(factory, RS232port, reactor, baudrate = RS232baud) serialLog.debug("Starting the client") reactor.run()
At this point the DF1Protocol is now responsible for encoding and sending the packets to the PLC. While I have coded both the encode and decode methods, I am only showing the encode method here. This is the only method I will be using. The decode would be for the case that the PLC was sending a command request to the Pi - I suppose if you needed the Pi to emulate a PLC or to act as a bridge between multiple PLC's.
class protectedReadRequest(PDU): ''' Base class for reading a PLC register ''' _frame_size = 18 cmd = 0x0F function = 0xA2 def __init__(self, dest, parameter, size=1, packet='', src=0, **kwargs): ''' Initializes a new instance :param address: The destination PLC address :param parameter: The PLC address to read :param size: The number of elements to read :param packet: Used when we already have an encoded packet ''' PDU.__init__(self, **kwargs) self.src = src self.dest = dest self.parameter = parameter self.sts = 0x00 self.size = size self.Address = utilities.calcAddress(parameter) self.packet = packet def encode(self): ''' Encodes the request packet :return: The encoded packet ''' if (not self.skip_encode): ############################ # Packet Start Sequence ############################ self.packet = struct.pack('>BB', 0x10, 0x02) #DLE STX ############################ # Packet Header Information ############################ data = struct.pack('>BBBBHB', self.dest, self.src, self.cmd, self.sts, self.transaction_id, self.function) if (self.Address.subElement > 0): elementSize = SUBELEMENT_SIZE[self.Address.fileType]*self.size else: elementSize = ELEMENT_SIZE[self.Address.fileType]*self.size data += struct.pack('>B', elementSize) ################################################### # Packet Address Information # Note: Use Little Endian format if using 2 bytes ################################################### if (self.Address.fileNumber > 254): data += struct.pack('>B', 0xFF) data += struct.pack('<H', self.Address.fileNumber) else: data += struct.pack('>B', self.Address.fileNumber) data += struct.pack('>B', self.Address.fileType) if (self.Address.eleNumber > 254): data += struct.pack('>B', 0xFF) data += struct.pack('<H', self.Address.eleNumber) else: data += struct.pack('>B', self.Address.eleNumber) if (self.Address.subElement > 254): data += struct.pack('>B', 0xFF) data += struct.pack('<H', self.Address.subElement) else: data += struct.pack('>B', self.Address.subElement) ####################################### # Calculate CRC before escaping DLE's ####################################### crc = utilities.computeCRC(data) #################################### # Escape any DLE's ('\x10') in data #################################### start = 0 while (data.find('\x10', start, len(data)) != -1): i = data.find('\x10', start, len(data)) data = data[:i] + '\x10' + data[i:] start = i+2 self.packet += data ################################### # Packet End ################################### self.packet += struct.pack('>BB', 0x10,0x03) #DLE ETX self.packet += struct.pack('>H', crc) else: self.packet = packet return self.packet def __str__(self): ''' Returns a string representation of the instance :returns: A string representation of the instance ''' return "DataInquiryRequest (%s,%s)" % (self.dest, self.parameter)
More to come ....