nuttx-apps/modbus/ascii/mbascii.c
2022-10-31 06:28:17 +01:00

553 lines
15 KiB
C

/****************************************************************************
* apps/modbus/ascii/mbascii.c
*
* FreeModbus Library: A portable Modbus implementation for Modbus ASCII/RTU.
* Copyright (c) 2006 Christian Walter <wolti@sil.at>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. The name of the author may not be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
****************************************************************************/
/****************************************************************************
* Included Files
****************************************************************************/
#include <nuttx/config.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include "port.h"
#include "modbus/mb.h"
#include "modbus/mbframe.h"
#include "modbus/mbport.h"
#include "mbascii.h"
#include "mbcrc.h"
#ifdef CONFIG_MB_ASCII_ENABLED
/****************************************************************************
* Pre-processor Definitions
****************************************************************************/
#define MB_ASCII_DEFAULT_CR '\r' /* Default CR character for Modbus ASCII. */
#define MB_ASCII_DEFAULT_LF '\n' /* Default LF character for Modbus ASCII. */
#define MB_SER_PDU_SIZE_MIN 3 /* Minimum size of a Modbus ASCII frame. */
#define MB_SER_PDU_SIZE_MAX 256 /* Maximum size of a Modbus ASCII frame. */
#define MB_SER_PDU_SIZE_LRC 1 /* Size of LRC field in PDU. */
#define MB_SER_PDU_ADDR_OFF 0 /* Offset of slave address in Ser-PDU. */
#define MB_SER_PDU_PDU_OFF 1 /* Offset of Modbus-PDU in Ser-PDU. */
/****************************************************************************
* Private Type Definitions
****************************************************************************/
typedef enum
{
STATE_RX_IDLE, /* Receiver is in idle state. */
STATE_RX_RCV, /* Frame is being received. */
STATE_RX_WAIT_EOF /* Wait for End of Frame. */
} eMBRcvState;
typedef enum
{
STATE_TX_IDLE, /* Transmitter is in idle state. */
STATE_TX_START, /* Starting transmission (':' sent). */
STATE_TX_DATA, /* Sending of data (Address, Data, LRC). */
STATE_TX_END, /* End of transmission. */
STATE_TX_NOTIFY /* Notify sender that the frame has been sent. */
} eMBSndState;
typedef enum
{
BYTE_HIGH_NIBBLE, /* Character for high nibble of byte. */
BYTE_LOW_NIBBLE /* Character for low nibble of byte. */
} eMBBytePos;
/****************************************************************************
* Private Function Prototypes
****************************************************************************/
static uint8_t prvucMBint8_t2BIN(uint8_t ucCharacter);
static uint8_t prvucMBBIN2int8_t(uint8_t ucByte);
static uint8_t prvucMBLRC(uint8_t *pucFrame, uint16_t usLen);
/****************************************************************************
* Private Data
****************************************************************************/
static volatile eMBSndState eSndState;
static volatile eMBRcvState eRcvState;
/* We reuse the Modbus RTU buffer because only one buffer is needed and the
* RTU buffer is bigger.
*/
extern volatile uint8_t ucRTUBuf[];
static volatile uint8_t *ucASCIIBuf = ucRTUBuf;
static volatile uint16_t usRcvBufferPos;
static volatile eMBBytePos eBytePos;
static volatile uint8_t *pucSndBufferCur;
static volatile uint16_t usSndBufferCount;
static volatile uint8_t ucMBLFCharacter;
/****************************************************************************
* Private Functions
****************************************************************************/
static uint8_t prvucMBint8_t2BIN(uint8_t ucCharacter)
{
if ((ucCharacter >= '0') && (ucCharacter <= '9'))
{
return (uint8_t)(ucCharacter - '0');
}
else if ((ucCharacter >= 'A') && (ucCharacter <= 'F'))
{
return (uint8_t)(ucCharacter - 'A' + 0x0a);
}
else
{
return 0xff;
}
}
static uint8_t prvucMBBIN2int8_t(uint8_t ucByte)
{
if (ucByte <= 0x09)
{
return (uint8_t)('0' + ucByte);
}
else if ((ucByte >= 0x0a) && (ucByte <= 0x0f))
{
return (uint8_t)(ucByte - 0x0a + 'A');
}
else
{
/* Programming error. */
DEBUGASSERT(0);
}
return '0';
}
static uint8_t prvucMBLRC(uint8_t * pucFrame, uint16_t usLen)
{
uint8_t ucLocalLRC = 0; /* LRC char initialized */
while (usLen--)
{
ucLocalLRC += *pucFrame++; /* Add buffer byte without carry */
}
/* Return twos complement */
ucLocalLRC = (uint8_t) (-((int8_t) ucLocalLRC));
return ucLocalLRC;
}
/****************************************************************************
* Public Functions
****************************************************************************/
eMBErrorCode eMBASCIIInit(uint8_t ucSlaveAddress, uint8_t ucPort,
speed_t ulBaudRate, eMBParity eParity)
{
eMBErrorCode eStatus = MB_ENOERR;
ENTER_CRITICAL_SECTION();
ucMBLFCharacter = MB_ASCII_DEFAULT_LF;
if (xMBPortSerialInit(ucPort, ulBaudRate, 7, eParity) != true)
{
eStatus = MB_EPORTERR;
}
else if (xMBPortTimersInit(CONFIG_MB_ASCII_TIMEOUT_SEC * 20000UL) != true)
{
eStatus = MB_EPORTERR;
}
EXIT_CRITICAL_SECTION();
return eStatus;
}
void eMBASCIIStart(void)
{
ENTER_CRITICAL_SECTION();
vMBPortSerialEnable(true, false);
eRcvState = STATE_RX_IDLE;
EXIT_CRITICAL_SECTION();
/* No special startup required for ASCII. */
xMBPortEventPost(EV_READY);
}
void eMBASCIIStop(void)
{
ENTER_CRITICAL_SECTION();
vMBPortSerialEnable(false, false);
vMBPortTimersDisable();
EXIT_CRITICAL_SECTION();
}
eMBErrorCode eMBASCIIReceive(uint8_t *pucRcvAddress, uint8_t **pucFrame,
uint16_t *pusLength)
{
eMBErrorCode eStatus = MB_ENOERR;
ENTER_CRITICAL_SECTION();
DEBUGASSERT(usRcvBufferPos < MB_SER_PDU_SIZE_MAX);
/* Length and CRC check */
if ((usRcvBufferPos >= MB_SER_PDU_SIZE_MIN) &&
(prvucMBLRC((uint8_t *) ucASCIIBuf, usRcvBufferPos) == 0))
{
/* Save the address field. All frames are passed to the upper laid
* and the decision if a frame is used is done there.
*/
*pucRcvAddress = ucASCIIBuf[MB_SER_PDU_ADDR_OFF];
/* Total length of Modbus-PDU is Modbus-Serial-Line-PDU minus
* size of address field and CRC checksum.
*/
*pusLength = usRcvBufferPos - MB_SER_PDU_PDU_OFF - MB_SER_PDU_SIZE_LRC;
/* Return the start of the Modbus PDU to the caller. */
*pucFrame = (uint8_t *) & ucASCIIBuf[MB_SER_PDU_PDU_OFF];
}
else
{
eStatus = MB_EIO;
}
EXIT_CRITICAL_SECTION();
return eStatus;
}
eMBErrorCode eMBASCIISend(uint8_t ucSlaveAddress, const uint8_t *pucFrame,
uint16_t usLength)
{
eMBErrorCode eStatus = MB_ENOERR;
uint8_t usLRC;
ENTER_CRITICAL_SECTION();
/* Check if the receiver is still in idle state. If not we where too
* slow with processing the received frame and the master sent another
* frame on the network. We have to abort sending the frame.
*/
if (eRcvState == STATE_RX_IDLE)
{
/* First byte before the Modbus-PDU is the slave address. */
pucSndBufferCur = (uint8_t *) pucFrame - 1;
usSndBufferCount = 1;
/* Now copy the Modbus-PDU into the Modbus-Serial-Line-PDU. */
pucSndBufferCur[MB_SER_PDU_ADDR_OFF] = ucSlaveAddress;
usSndBufferCount += usLength;
/* Calculate LRC checksum for Modbus-Serial-Line-PDU. */
usLRC = prvucMBLRC((uint8_t *) pucSndBufferCur, usSndBufferCount);
ucASCIIBuf[usSndBufferCount++] = usLRC;
/* Activate the transmitter. */
eSndState = STATE_TX_START;
vMBPortSerialEnable(false, true);
}
else
{
eStatus = MB_EIO;
}
EXIT_CRITICAL_SECTION();
return eStatus;
}
bool xMBASCIIReceiveFSM(void)
{
bool xNeedPoll = false;
uint8_t ucByte;
uint8_t ucResult;
DEBUGASSERT(eSndState == STATE_TX_IDLE);
xMBPortSerialGetByte((int8_t *) & ucByte);
switch (eRcvState)
{
/* A new character is received. If the character is a ':' the input
* buffer is cleared. A CR-character signals the end of the data
* block. Other characters are part of the data block and their
* ASCII value is converted back to a binary representation.
*/
case STATE_RX_RCV:
/* Enable timer for character timeout. */
vMBPortTimersEnable();
if (ucByte == ':')
{
/* Empty receive buffer. */
eBytePos = BYTE_HIGH_NIBBLE;
usRcvBufferPos = 0;
}
else if (ucByte == MB_ASCII_DEFAULT_CR)
{
eRcvState = STATE_RX_WAIT_EOF;
}
else
{
ucResult = prvucMBint8_t2BIN(ucByte);
switch (eBytePos)
{
/* High nibble of the byte comes first. We check for
* a buffer overflow here.
*/
case BYTE_HIGH_NIBBLE:
if (usRcvBufferPos < MB_SER_PDU_SIZE_MAX)
{
ucASCIIBuf[usRcvBufferPos] = (uint8_t)(ucResult << 4);
eBytePos = BYTE_LOW_NIBBLE;
break;
}
else
{
/* not handled in Modbus specification but seems
* a reasonable implementation.
*/
eRcvState = STATE_RX_IDLE;
/* Disable previously activated timer due to error state. */
vMBPortTimersDisable();
}
break;
case BYTE_LOW_NIBBLE:
ucASCIIBuf[usRcvBufferPos] |= ucResult;
usRcvBufferPos++;
eBytePos = BYTE_HIGH_NIBBLE;
break;
}
}
break;
case STATE_RX_WAIT_EOF:
if (ucByte == ucMBLFCharacter)
{
/* Disable character timeout timer because all characters are
* received.
*/
vMBPortTimersDisable();
/* Receiver is again in idle state. */
eRcvState = STATE_RX_IDLE;
/* Notify the caller of eMBASCIIReceive that a new frame
* was received.
*/
xNeedPoll = xMBPortEventPost(EV_FRAME_RECEIVED);
}
else if (ucByte == ':')
{
/* Empty receive buffer and back to receive state. */
eBytePos = BYTE_HIGH_NIBBLE;
usRcvBufferPos = 0;
eRcvState = STATE_RX_RCV;
/* Enable timer for character timeout. */
vMBPortTimersEnable();
}
else
{
/* Frame is not okay. Delete entire frame. */
eRcvState = STATE_RX_IDLE;
}
break;
case STATE_RX_IDLE:
if (ucByte == ':')
{
/* Enable timer for character timeout. */
vMBPortTimersEnable();
/* Reset the input buffers to store the frame. */
usRcvBufferPos = 0;
eBytePos = BYTE_HIGH_NIBBLE;
eRcvState = STATE_RX_RCV;
}
break;
}
return xNeedPoll;
}
bool xMBASCIITransmitFSM(void)
{
bool xNeedPoll = false;
uint8_t ucByte;
DEBUGASSERT(eRcvState == STATE_RX_IDLE);
switch (eSndState)
{
/* Start of transmission. The start of a frame is defined by sending
* the character ':'.
*/
case STATE_TX_START:
ucByte = ':';
xMBPortSerialPutByte((int8_t)ucByte);
eSndState = STATE_TX_DATA;
eBytePos = BYTE_HIGH_NIBBLE;
break;
/* Send the data block. Each data byte is encoded as a character hex
* stream with the high nibble sent first and the low nibble sent
* last. If all data bytes are exhausted we send a '\r' character
* to end the transmission.
*/
case STATE_TX_DATA:
if (usSndBufferCount > 0)
{
switch (eBytePos)
{
case BYTE_HIGH_NIBBLE:
ucByte = prvucMBBIN2int8_t((uint8_t)(*pucSndBufferCur >> 4));
xMBPortSerialPutByte((int8_t) ucByte);
eBytePos = BYTE_LOW_NIBBLE;
break;
case BYTE_LOW_NIBBLE:
ucByte = prvucMBBIN2int8_t((uint8_t)(*pucSndBufferCur & 0x0f));
xMBPortSerialPutByte((int8_t)ucByte);
pucSndBufferCur++;
eBytePos = BYTE_HIGH_NIBBLE;
usSndBufferCount--;
break;
}
}
else
{
xMBPortSerialPutByte(MB_ASCII_DEFAULT_CR);
eSndState = STATE_TX_END;
}
break;
/* Finish the frame by sending a LF character. */
case STATE_TX_END:
xMBPortSerialPutByte((int8_t)ucMBLFCharacter);
/* We need another state to make sure that the CR character has
* been sent.
*/
eSndState = STATE_TX_NOTIFY;
break;
/* Notify the task which called eMBASCIISend that the frame has
* been sent.
*/
case STATE_TX_NOTIFY:
eSndState = STATE_TX_IDLE;
xNeedPoll = xMBPortEventPost(EV_FRAME_SENT);
/* Disable transmitter. This prevents another transmit buffer
* empty interrupt.
*/
vMBPortSerialEnable(true, false);
eSndState = STATE_TX_IDLE;
break;
/* We should not get a transmitter event if the transmitter is in
* idle state.
*/
case STATE_TX_IDLE:
/* enable receiver/disable transmitter. */
vMBPortSerialEnable(true, false);
break;
}
return xNeedPoll;
}
bool xMBASCIITimerT1SExpired(void)
{
switch (eRcvState)
{
/* If we have a timeout we go back to the idle state and wait for
* the next frame.
*/
case STATE_RX_RCV:
case STATE_RX_WAIT_EOF:
eRcvState = STATE_RX_IDLE;
break;
default:
DEBUGASSERT(eRcvState == STATE_RX_RCV || eRcvState == STATE_RX_WAIT_EOF);
break;
}
vMBPortTimersDisable();
/* no context switch required. */
return false;
}
#endif