TITLE=This code is property of Siemens Industry, Inc. Network 1 // If the CPU is not in freeport mode or if the modbus master has not been initialized then abort. // // NOTE: This error does not go through the standard error return because that will reset the mMasterState to idle (and we // do not want to do that because we could really screw up the the comm line with multiple messages all trying to // be sent at the same time). // LDB= VB687, 0 // if (freeport not enabled) || AN SM30.0 // (modbus is not active) LDB= VB687, 1 AN SM130.0 OLD OB<> VB655, 16#69 MOVB 5, LB14 // show Modbus not enabled error S L13.0, 1 // show done CRET // return Network 2 // If the first pass flag is set and the modbus is already busy (mModbusState <> 0) // // OR // // If the first flag is not set then this should be a subsequent call the the message routine waiting for the results. Check to see if this is the same instance (same message box) that initiated the message. If it is not the same instance... // // THEN // // Show a busy error and abort. // // NOTE: This error does not go through the standard error return because that will reset the mMasterState to idle (and we // do not want to do that because we could really screw up the the comm line with multiple messages all trying to // be sent at the same time). // LD L0.0 // if ( (new request) and AB<> VB654, 0 // (Modbus is already busy) ) or LDN L0.0 // ( (not a new request) and LDD<> LD1, VD674 // ((Signature 1 does not match) or OD<> LD5, VD678 // (Signature 2 does not match) or OD<> LD9, VD682 // (Signature 3 does not match)) ) ALD OLD MOVB 6, LB14 // show Modbus busy error S L13.0, 1 // show done CRET // return Network 3 // Store the accumulators so that we do not interfere with the user program use of accumulators. // LD SM0.0 MOVD AC0, LD15 // save accumulators MOVD AC1, LD19 // ... MOVD AC2, LD23 // ... Network 4 // If the First flag is not set then this should be a subsequent call the the message routine waiting for the results so go to the active handler. // LDN L0.0 // if (not a new request) and JMP 100 // go to active handler Network 5 // If we are here then the first flag is set and the Modbus function is not currently active. This means that we should kick off a new message (and initialize a new instance). // // LD SM0.0 MOVD LD1, VD674 // create Signature 1 MOVD LD5, VD678 // create Signature 2 MOVD LD9, VD682 // create Signature 3 MOVB 0, VB658 // clear retry count MOVB 0, VB656 // show no error R V652.0, 1 // show not done R V652.1, 1 // assume this is not a broadcast message Network 6 // Retry restart point... // LBL 1 Network 7 // Check the slave address. Legal modbus addresses are 1 through 247. The broadcast address is address 0 and we support that address for write operations. // // Make sure that this is either a mRead or a mWrite. Checking this here just simplifies subsequent checks. // // Check the bit/word count. The count cannot be zero or negative. The size is checked later we determine if it is really bits or words. // LDB> LB1, 247 // if (Slave address > 247) || LDB<> LB2, 0 // ((RW != mRead) && (RW != mWrite)) || AB<> LB2, 1 OLD OW<= LW7, 0 // (Count <= 0) MOVB 4, AC0 // show request error JMP 250 // goto error return Network 8 // // The Addr input is based on Modbus addresses. // // Modbus addresses are normally written as 5 or 6 character values. Sometimes the leading zero is not present. // // 000001 - 00xxxx are discrete outputs mapped to Q0.0 - Q15.7 // 010001 - 01xxxx are discrete inputs mapped to I0.0 - I15.7 // 030001 - 03xxxx are input registers // 040001 - 04xxxx are output (holding) registers // // The first two characters determine which function the master uses to access the data type. The last four characters of the address select the proper value within the data type. // // Addresses 00xxxx utilize functions 1, 5 and 15. // Addresses 01xxxx utilize function 2. // Addresses 03xxxx utilize function 4. // Addresses 04xxxx utilize functions 3, 6 and 16. // // Function 5 is a single bit write and function 15 is a multiple bit write. // // Function 6 is a single word write and function 16 is a multiple word write. // // All Modbus addresses are 1 based, that is, the first holding register is addressed as 040001, but Modbus is zero based on the wire (seems strange, doesn't it?). // // We need to check to be sure that the address is not zero (for any given data type). // // LD SM0.0 MOVD LD3, AC0 // get a copy of Addr Network 9 // If the Address input is greater than 0 and less than 10000 then the data type is for discrete outputs. // // If the request is to read then we will use modbus function 1. If this is a write and the count is 1 then we will use Modbus function 5. If this is a multibit write then we must use modbus function 15 for the write. // // NOTE: The boadcast address (0) cannot be used if this is a read request. // // NOTE: There is an override to force the use of function 15 for single bit writes in the case where the slave does not support // Modbus Function 5. // LDD> AC0, 0 AD< AC0, 10000 LPS AB= LB2, 0 MOVB 1, VB657 LRD AB= LB2, 1 LPS AW= LW7, 1 MOVB 5, VB657 LPP LDW> LW7, 1 O V653.0 ALD MOVB 15, VB657 LPP DECD AC0 JMP 10 Network 10 // If the Address input is greater than 10000 and less than 20000 then the data type is for discrete inputs. // // The only thing we can do with discrete inputs is to read them via modbus function 2. // LDD> AC0, 10000 // if (Address in discrete input range) && AD< AC0, 20000 // ... AB= LB2, 0 // (RW is read) MOVB 2, VB657 // FunctionNumber = 2 -D +10001, AC0 // StartAddr = Addr - 10001 JMP 10 // ... // continue Network 11 // If the Address input is greater than 30000 and less than 40000 then the data type is for analog inputs. // // The only thing we can do with analog inputs is to read them via modbus function 4. // LDD> AC0, 30000 // if (Address is in analog input range) && AD< AC0, 40000 // ... AB= LB2, 0 // (RW is read) MOVB 4, VB657 // FunctionNumber = 4 -D +30001, AC0 // StartAddr = Addr - 30001 JMP 10 // ... // continue Network 12 // If the Address input is greater than 40000 and less than 50000 then the data type is for holding registers. // // If the request is to read then we will use modbus function 3. If this is a write and the count is 1 then we will use Modbus function 6. If this is a multibit write then we must use modbus function 16 for the write. // // NOTE: There is an override to force the use of function 16 for single word writes in the case where the slave does not support // Modbus Function 6. // // NOTE: We have added a check for the address range for 400,001 to 465,536 so that users can address holding register // numbers greater than 9999. This allows use of the full address range for the "advanced" users but still keeps it // simple for the "normal" users. // LDD> AC0, 40000 AD< AC0, 50000 // if (Address is holding registers) -D +40001, AC0 AENO LDD> AC0, 400000 AD<= AC0, 465536 -D +400001, AC0 AENO OLD LPS AB= LB2, 0 MOVB 3, VB657 LRD AB= LB2, 1 LPS AW= LW7, 1 MOVB 6, VB657 LPP LDW> LW7, 1 O V653.0 ALD MOVB 16, VB657 LPP JMP 10 Network 13 // Address error... // // The address is outside of the expected ranges (we did not match any of the range checks above) so show an error and return. We will also reach here if there was, for example, a write to discrete inputs or analog inputs. In either case there is an error so abort the message. // LD SM0.0 MOVB 4, AC0 // show request error JMP 250 // goto error return Network 14 // The address was OK and now we have a FunctionNumber and StartAddr and everything else we need to build the request. // LBL 10 Network 15 // If this is a broadcast (address = 0) and the function is a read function (1, 2, 3 and 4) then show an error and abort. If this is a write request (functions 5, 6, 15 and 16) then set a flag so that we can terminate the message after it has transmitted. // LDB= LB1, 0 // if (slave address == 0) LPS AB<= VB657, 4 // if (function <= 4) MOVB 4, AC0 // error = request error JMP 250 // return LPP S V652.1, 1 // set the broadcast flag Network 16 // Place the slave address, function number, start address and count values in the modbus buffer. // // Almost all Modbus messages have the same start sequence: // // ll aa ff ssss nnnn // // where: ll = length of request including CRC for XMT (byte) // aa = address of slave (byte) // ff = function (byte) // ssss = start bit or word (word) // nnnn = number of bits or words (word) // LD SM0.0 MOVB LB1, VB403 // write slave address MOVB VB657, VB404 // write function MOVW AC0, VW405 // write start address MOVW LW7, VW407 // write count Network 17 // If the function is greater than 6 then check for functions 15 and 16 // LDB> VB657, 6 // if (FunctionNumber > 6) JMP 15 // continue Network 18 // // Functions 1 through 4 and functions 5 and 6 are very similiar. The first four input parameters (6 bytes) have already been copied into the transmit buffer. This is all we need for functions 1 through 4 so we can just jump to the common point. // // The count value in functions 1 through 4 is replaced with the data value in functions 5 and 6. Place the data in the buffer for functions 5 and 6. Common code is used for the first 6 functions except for the placement of data in the buffer. // // Functions 1, 2, 3 and 4 have the following request format: // // ll aa ff ssss nnnn cccc // // where: ll = length of request including CRC (byte) // aa = address of slave (byte) // ff = function (byte) // ssss = start bit or word (word) // nnnn = number of bits or words (word) // cccc = CRC // // Functions 5 and 6 have the following request format: // // ll aa ff ssss dddd cccc // // where: ll = length of request including CRC (byte) // aa = address of slave (byte) // ff = function (byte) // ssss = start bit or word (word) // dddd = data value to write to slave (word) // cccc = CRC // // // The data in functions 5 and 6 takes the place of the count field that we just loaded. The buffer pointer (AC1) is pointing to proper position to write the data. // LDB= VB657, 6 // if (FunctionNumber == 6) MOVW *LD9, VW407 // copy data word to modbus buffer Network 19 // The data for function 5 (write output bit) is supposed to be either 0xFF00 or 0x0000. Check the value, the LSBit of the data byte, and set the data word in the buffer to the appropriate value. // LDB= VB657, 5 // if (FunctionNumber == 5) LPS // AC0 = *DataPtr & 0x01 MOVB *LD9, AC0 // ... ANDB 1, AC0 AB= AC0, 0 // if (AC0 == 0) MOVW 0, VW407 // modbus data = 0 LPP AB<> AC0, 0 // else if (AC0 != 0) MOVW 16#FF00, VW407 // modbus data = 0xff00 Network 20 // Set the tx length then go calculate the CRC and transmit the request. // LD SM0.0 MOVB 6, VB402 // length = 6 bytes JMP 20 // transmit request Network 21 // // Functions 15 and 16 // // Determine how much data is to be sent with the request and copy the data to the request buffer. The DataPtr points to the first byte of the data. // // Function 15 uses the following format for the transmit buffer: // // ll aa ff ssss nnnn bb dddd...dddd cccc // // where: ll = length of request including CRC (byte) // aa = address of slave (byte) // ff = function number (byte) // ssss = start bit (word) // nnnn = number of bits (word) // bb = byte count of data // dddd = data // cccc = CRC // // The buffer has already be filled in up to the bytes count of data field. // // Function 15 allows the writing of multiple bits. The start bit must be a multiple of 8, that is, we must start on an even byte boundry. // LBL 15 Network 22 // If this is function 15 convert the bit count to a byte count by dividing by 8 (same as a shift right of 3 bits). If the bit count is not a multiple of 8 then we need to add one more byte to the byte count. // // // If this is function 16 convert the word count to a byte count by multiplying by 2 (same as a left shift by 1 bit). // LDB= VB657, 15 // if (FunctionNumber == 15) LPS MOVW LW7, AC2 // get the number of bits in AC2 ANDW 16#07, AC2 // keep only the LS 3 bits of count in AC2 SRW LW7, 3 // convert count to a byte count (divide by 8) AB<> AC2, 0 // if (bit count != 0) INCW LW7 // increment the byte count LPP NOT // else (FunctionNumber == 16) SLW LW7, 1 // count * 2 for byte count Network 23 // If the byte count is too large for the buffer then return an error. // // If the byte count is OK then move the data (from DataPtr) into the buffer and calculate the buffer length. // LDW> LW7, VW664 // if (byte count > data space in buffer) MOVB 4, AC0 // show address error JMP 250 // goto error return NOT // else MOVW LW7, AC0 // get the count in AC0 MOVB AC0, VB409 // write the byte count (in AC0) to the buffer BMB *LD9, VB410, AC0 // copy data to buffer +I +7, AC0 // transmit length = number of data bytes + 7 MOVB AC0, VB402 // put length in buffer Network 24 // If this is function 15 and if the number of bits was not a multiple of 8 then we need to clear the MSBits of the last data byte. // // This is done because some Modbus devices expect the bits to be cleared. // // First, calculate a pointer to the last data byte in the buffer. Then determine the number of bits we need to clear. This will be eight minus the bit count. We then shift the last data byte left for n bits, then shift that byte right n bits to fill with zeros. // // AC2 has the bit count (0 - 7) // LDB= VB657, 15 // if (FunctionNumber == 15) and AB<> AC2, 0 // (bit count is not a multiple of 8) MOVD &VB409, AC1 // point to the first byte of data... +I LW7, AC1 // point to the last byte of data... // ...in the comm buffer MOVW +8, AC0 // shift count = 8 - bit count -I AC2, AC0 // ... SLB *AC1, AC0 // shift left so we can then... SRB *AC1, AC0 // shift right to fill with zeros Network 25 // The request is complete... // // Calculate the CRC, place it in the buffer and add 2 bytes to the transmit count. // Transmit the request after waiting an idle line time. // LBL 20 Network 26 // Calculate the number of bytes we should receive in the Modbus response. // // If the function is 5, 6, 15 or 16 then the response should be 8 bytes. // // If the function is 3 or 4 then the response should be 5 plus the number of bytes requested. The number of bytes requested is the number of words requested (Count) times 2. // // If the function is 1 or 2 then the response should be 5 bytes plus the number of bytes requested. The number of bytes requested is the number of bits requested (Count) divided by 8. If the number of bits is not a mulitple of 8 then the byte count is incremented. // // We want to use a byte count to terminate the RCV box because this will be faster than waiting for a timeout. The timeout is not really a problem unless we are using modems. When using modems, the modems can add a variable amount of time to the message, sometimes there is 50 mSec or more of dead time on the lines. If we use a byte count to terminate the RCV box, we can set an intercharacter timer for a long timeout and still achieve a fast reaction when everything is correct. // LD SM0.0 LPS AB> VB657, 4 // if (Function > 4) MOVB 3, AC0 // expected length = 3 LRD LDB= VB657, 3 // else if (Function == 3) or OB= VB657, 4 // (Function == 4) ALD MOVW LW7, AC0 // expected length = word count * 2 SLW AC0, 1 // ... LRD AB<= VB657, 2 // else if (Function <= 2) MOVW LW7, AC2 // get bit count in AC2 ANDW 16#07, AC2 // keep only 3 LSBits in AC2 MOVW LW7, AC0 // expected count = bit count / 8 SRW AC0, 3 // ... AB<> AC2, 0 // if (3 LSBits != 0) INCB AC0 // expected count + 1 LRD +I 5, AC0 // expected count + 5 LRD // move expected count to RCV box setup AB= VB687, 0 // set the RCV box start character MOVB AC0, SMB94 MOVB LB1, SMB88 LPP AB= VB687, 1 MOVB AC0, SMB194 MOVB LB1, SMB188 Network 27 // Calculate the CRC for the message, place it in the buffer and add 2 to the buffer length for the CRC. // // Setup a timer to time the idle line time before transmitting the request. // LD SM0.0 CALL SBR37, AC0 // calculate CRC and put it in the buffer INCB VB402 // length + 2 for CRC INCB VB402 // ... BITIM VD670 // get the current time MOVB 1, VB654 // set state to 1 (waiting to transmit) JMP 255 // return Network 28 // Active handler.... // // The bulk of the active handler is in the MBUS_CTRL routine. That routine is (supposed to be) called every scan. All of the polled operations on the RCV box and the checking for timeouts is done in that routine. This protects the user from himself, if he should happen to disable the MBUS_MSG box after starting a message. If I were to have all of the polled operations in this routine, the state machine would stall if the user stops calling this routine. Since the MBUS_CTRL routine does all of the work, it might just as well manage the mModbusState state machine. This leaves very little to do in this routine. // // The only thing handled in this routine is the error checking of the response and moving any data read from the slave. The error checking includes any errors from the RCV box and from within the message. This is done here because I wanted to keep all of these checks with the code the moved the data (for Modbus read requests). I think that if the user stops calling the MBUS_MSG box, then the data should not move to his buffer (DataPtr). // // The values for mModbusState are: // // 0 = idle // 1 = waiting to transmit // 2 = waiting for tx complete // 3 = waiting for first rx char // 4 = waiting for rx complete // 5 = rx complete // // We have nothing to do here until the state is Rx complete. // LBL 100 Network 29 // If the Modbus master function is transmitting or waiting for the response, we have noting to do here. // LDB< VB654, 5 // if (state != Rx complete) JMP 255 // nothing to do so return Network 30 // Modbus State 5...Rx complete // // The receive is complete (there is a non-zero value in the RCV box status). // // If parity error then abort. // LDB= VB687, 0 // if (parity error) A SM86.0 // set parity error LDB= VB687, 1 // check for a retry A SM186.0 OLD MOVB 1, AC0 JMP 240 Network 31 // // If there is a timeout error, the cause may be that the modbus slave has returned an error. The error response is only 5 bytes will be fewer bytes than we were expecting, thus resulting in a timeout. (The mimimum response message will be 6 bytes.) // // The format for the error response is: // // ll aa ff ee cccc // // where: ll = RCV box length (5) of response including CRC (byte) // aa = address of slave (byte) // ff = function (byte) with MSBit set to 1 // ee = error code (byte) // cccc = CRC (word) // // We do not need to check the address field because we are using the slave address as the starting event for the RCV box, and we should not ever receive anything from any other address than the slave address. // // When we receive the error response we will not bother to calculate the CRC. We did not receive the response we expected so we will just confirm that this is an error response and then show the error to the user. The error we show to the user will be the error code returned from the modbus slave with the MSBit set to put the error in the range of 0x80 to 0x88. // // Typical error codes (from my old Modbus spec) returned by the modbus slave are: // // 01 Illegal function // 02 Illegal data address // 03 IIlegal data value // 04 Failure in associated device // 05 Acknowledge (should not happen for reads and writes) // 06 Busy, rejected message (device is busy with another message) // 07 NAK (request was rejected) // 08 Memory parity error // LDB= VB687, 0 // if (timeout == TRUE) A SM86.2 LDB= VB687, 1 // if (length == 5) A SM186.2 // if (function number > 0x80) OLD // get error code in AC0 LPS // offset error code by 100 AB= VB402, 5 // goto terminal error handler AB> VB404, 16#80 BTI VB405, AC0 // show a timeout error +I 100, AC0 // go to retry error handler JMP 250 LPP MOVB 3, AC0 JMP 240 Network 32 // Check to see whether we received anything at all. If the slave did not respond and we simply timed out, the code in MBUS_CTRL set the state to show the receive was complete and disabled the RCV box. In this case the number of bytes we received will be zero and SM86.7 (user disabled RCV box) will be set. // LDB= VB402, 0 // if (receive count == 0) MOVB 3, AC0 // show a timeout error JMP 240 // go to error handler Network 33 // Check the returned function number. If there is a Modbus error noted by the slave, the function will have the MSBit set. // // We skip over the address byte because we are using the slave address as the starting event for the RCV box. We will not receive any message if the address is not correct. // LDB<> VB404, VB657 // if (functionNumber != response function number) MOVB 7, AC0 // set response data error JMP 240 // goto retry error handler Network 34 // The function and address look good so check the CRC. The entire message is checked including the CRC itself. When this is done the resulting CRC is zero if the message is good. // // This check of the CRC confirms that we received the correct number of bytes, that is, we did not chop off the end of the message by terminating the RCV before all the bytes were received. If this were the case, the CRC would fail and we will reject the response. // LD SM0.0 CALL SBR37, AC0 // calculate CRC (count,pointer,crc) AW<> AC0, +0 // if (CRC result != 0) MOVB 8, AC0 // set CRC error code JMP 240 // goto error exit Network 35 // The CRC looks good so check the function to see if data was returned. // // If the function was 1, 2, 3 or 4 there was data so copy it to the response area. // // If the function is 5 or 6 the response is exactly the same as the request. If this is function 15 or 16 then the first six bytes of the response are the same as the request. For these functions we will say that the checks already done (the address, function code and CRC) are sufficient for checking for a valid frame. // // Functions 1, 2, 3 and 4 receive buffer format: // // ll aa ff nn dd dd .. cccc // // where: ll = length of response including CRC (byte) // aa = address of slave (byte) // ff = function (byte) // nn = byte count of data to follow (byte) // dd = data bytes // cccc = CRC // LDB<= VB657, 4 // if (function <= 4) BMB VB406, *LD9, VB405 // transfer data to response area Network 36 // Show we are done with no errors! // // NOTE: This jumps to the terminal error handler with a value of mNoError. This will store the error code and show it // to the user on the box outputs. // LD SM0.0 MOVB 0, AC0 // show no errors JMP 250 // terminate this message and return Network 37 // Retry handler... // // The error was such that if we retry the request, we might possibly get a good response. // LBL 240 Network 38 // If the current retry count is less than or equal to the max retries allowed, jump to the start and rebuild/re-transmit the message. // LD SM0.0 INCB VB658 // retry count + 1 AB<= VB658, VB659 // if (retry count <= max retry count) JMP 1 // retry the request Network 39 // Terminal error handler.... // // The error was such that a retry will not generate a good response. This handler is used when there is an error in the request or if the Modbus slave returns an error (address range error, command not supported, etc.). // LBL 250 Network 40 // Clear state machine (because we aborted this message), show the error and set the done flag. We load the error and done flags into instance memory so that on subsequent calls to this routine (after the response is complete) we can remember the status and show it to the user over and over again. This looks really nice with ladder execution status. // LD SM0.0 MOVB 0, VB654 // clear state machine MOVB AC0, VB656 // show error S V652.0, 1 // show done Network 41 // Exit... // LBL 255 Network 42 // Restore the accumulators and return. // LD SM0.0 MOVD LD15, AC0 // restore accumulators MOVD LD19, AC1 // ... MOVD LD23, AC2 // ... MOVB VB656, LB14 // show the error status A V652.0 // ...and the done status = L13.0 Network 43 Network 44 Network 45