|
@@ -0,0 +1,478 @@
|
|
|
+#include <stdio.h>
|
|
|
+#include <string.h>
|
|
|
+#include <gfambrtucom.h>
|
|
|
+#include "inc/hw_types.h"
|
|
|
+#include "inc/hw_uart.h"
|
|
|
+#include "driverlib/interrupt.h"
|
|
|
+#include "driverlib/sysctl.h"
|
|
|
+#include "driverlib/gpio.h"
|
|
|
+#include "driverlib/uart.h"
|
|
|
+#include <gfautils.h>
|
|
|
+
|
|
|
+/////////////////////////////////////////////////////////////////////////////
|
|
|
+/////////////////////////////////////////////////////////////////////////////
|
|
|
+
|
|
|
+static HFIFO g_hFifoTX = NULL, g_hFifoRX = NULL;
|
|
|
+static volatile uint32_t g_nTxEchoCount = 0;
|
|
|
+static volatile uint64_t g_nTimerTick = 0;
|
|
|
+static UART_STATUS_COUNTERS g_statCnt = {0};
|
|
|
+static UART_CONFIG g_uartCfg = {0};
|
|
|
+static uint64_t g_MODBUS_RTU_FRAME_TIMEOUT_US = 0;
|
|
|
+static uint64_t g_MODBUS_RTU_CHAR_TIMEOUT_US = 0;
|
|
|
+
|
|
|
+/////////////////////////////////////////////////////////////////////////////
|
|
|
+
|
|
|
+#define UART_BASE g_uartCfg.P_UART_BASE
|
|
|
+#define UART_BASE_SYSCTL g_uartCfg.P_UART_BASE_SYSCTL
|
|
|
+#define UART_PORT g_uartCfg.P_UART_PORT
|
|
|
+#define UART_PORT_SYSCTL g_uartCfg.P_UART_PORT_SYSCTL
|
|
|
+#define UART_RX_PIN g_uartCfg.P_UART_RX_PIN
|
|
|
+#define UART_RX_PIN_MUX g_uartCfg.P_UART_RX_PIN_MUX
|
|
|
+#define UART_TX_PIN g_uartCfg.P_UART_TX_PIN
|
|
|
+#define UART_TX_PIN_MUX g_uartCfg.P_UART_TX_PIN_MUX
|
|
|
+#define UART_INT g_uartCfg.P_UART_INT
|
|
|
+#define EN_485_PORT_SYSCTL g_uartCfg.P_EN_485_PORT_SYSCTL
|
|
|
+#define EN_485_PORT g_uartCfg.P_EN_485_PORT
|
|
|
+#define EN_485_PIN g_uartCfg.P_EN_485_PIN
|
|
|
+#define MODBUS_RTU_UART_INT_PRIORITY g_uartCfg.P_UART_INT_PRIORITY
|
|
|
+
|
|
|
+#define MODBUS_RTU_FRAME_TIMEOUT_US g_MODBUS_RTU_FRAME_TIMEOUT_US;
|
|
|
+#define MODBUS_RTU_CHAR_TIMEOUT_US g_MODBUS_RTU_CHAR_TIMEOUT_US;
|
|
|
+
|
|
|
+#define IS_INT_ENABLED(f) !!(HWREG(UART_BASE + UART_O_IM) & (f))
|
|
|
+#define INT_MASK_REGISTER HWREG(UART_BASE + UART_O_IM)
|
|
|
+
|
|
|
+/////////////////////////////////////////////////////////////////////////////
|
|
|
+
|
|
|
+static bool _TimerElapsed(void)
|
|
|
+{
|
|
|
+ struct timeval tv;
|
|
|
+ return g_nTimerTick < GfaSystickTimeval2Us(GfaSystickGetUsClock(&tv));
|
|
|
+}
|
|
|
+
|
|
|
+/////////////////////////////////////////////////////////////////////////////
|
|
|
+
|
|
|
+static void _FrameTimerStart(void)
|
|
|
+{
|
|
|
+ struct timeval tv;
|
|
|
+ g_nTimerTick = GfaSystickTimeval2Us(GfaSystickGetUsClock(&tv)) + MODBUS_RTU_FRAME_TIMEOUT_US;
|
|
|
+}
|
|
|
+
|
|
|
+/////////////////////////////////////////////////////////////////////////////
|
|
|
+
|
|
|
+static void _CharTimerStart(void)
|
|
|
+{
|
|
|
+ struct timeval tv;
|
|
|
+ g_nTimerTick = GfaSystickTimeval2Us(GfaSystickGetUsClock(&tv)) + MODBUS_RTU_CHAR_TIMEOUT_US;
|
|
|
+}
|
|
|
+
|
|
|
+/////////////////////////////////////////////////////////////////////////////
|
|
|
+
|
|
|
+static bool _OnRxFinalize(void)
|
|
|
+{
|
|
|
+ _FrameTimerStart();
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+/////////////////////////////////////////////////////////////////////////////
|
|
|
+
|
|
|
+static bool _OnTxPrepare(void)
|
|
|
+{
|
|
|
+ return _TimerElapsed();
|
|
|
+}
|
|
|
+
|
|
|
+/////////////////////////////////////////////////////////////////////////////
|
|
|
+
|
|
|
+static void _OnTxStart(void)
|
|
|
+{
|
|
|
+ uint8_t b;
|
|
|
+
|
|
|
+ if(!UARTBusy(UART_BASE) && FifoPop(g_hFifoTX, &b, false))
|
|
|
+ {
|
|
|
+ g_nTxEchoCount++;
|
|
|
+ GPIOPinWrite(EN_485_PORT, EN_485_PIN, EN_485_PIN); // enable the bus
|
|
|
+ UARTIntEnable(UART_BASE, UART_INT_TX);
|
|
|
+ ++g_statCnt.nCharsTransmitted;
|
|
|
+ UARTCharPutNonBlocking(UART_BASE, b);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/////////////////////////////////////////////////////////////////////////////
|
|
|
+
|
|
|
+static bool _OnTxFinalize(void)
|
|
|
+{
|
|
|
+ return !UARTBusy(UART_BASE) && _TimerElapsed();
|
|
|
+}
|
|
|
+
|
|
|
+/////////////////////////////////////////////////////////////////////////////
|
|
|
+
|
|
|
+static void UART_RX_ISR(void)
|
|
|
+{
|
|
|
+ uint8_t b;
|
|
|
+
|
|
|
+ if(g_nTxEchoCount)
|
|
|
+ {
|
|
|
+ UARTCharGetNonBlocking(UART_BASE); // discard transmit echo chars
|
|
|
+ _FrameTimerStart();
|
|
|
+ --g_nTxEchoCount;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ ++g_statCnt.nCharsReceived;
|
|
|
+
|
|
|
+ if(_TimerElapsed()) // check, if the time since the last character was received, is greater than 3.5/1.5 characters
|
|
|
+ {
|
|
|
+ FifoReset(g_hFifoRX, false);
|
|
|
+ FifoSetFlags(g_hFifoRX, MB_RTU_FLAG_FRAME_GAP_DETECT, false);
|
|
|
+ if(FifoMatchFlags(g_hFifoRX, MB_RTU_FLAG_IGNORE_FRAME, false))
|
|
|
+ {
|
|
|
+ FifoClearFlags(g_hFifoRX, MB_RTU_FLAG_IGNORE_FRAME, false);
|
|
|
+ }
|
|
|
+ FifoClearFlags(g_hFifoTX, MB_RTU_FLAG_TRANSMIT_END, false);
|
|
|
+ }
|
|
|
+ else if(FifoMatchFlags(g_hFifoRX, MB_RTU_FLAG_IGNORE_FRAME, false))
|
|
|
+ {
|
|
|
+ UARTCharGetNonBlocking(UART_BASE); // discard chars
|
|
|
+ _FrameTimerStart();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ else if(FifoMatchFlags(g_hFifoTX, MB_RTU_FLAG_TRANSMIT_END, false))
|
|
|
+ {
|
|
|
+ UARTCharGetNonBlocking(UART_BASE); // discard chars
|
|
|
+ FifoReset(g_hFifoRX, false);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ b = (uint8_t)UARTCharGetNonBlocking(UART_BASE);
|
|
|
+ FifoPush(g_hFifoRX, b, false);
|
|
|
+ _CharTimerStart();
|
|
|
+}
|
|
|
+
|
|
|
+/////////////////////////////////////////////////////////////////////////////
|
|
|
+
|
|
|
+static void UART_TX_ISR(void)
|
|
|
+{
|
|
|
+ uint8_t b;
|
|
|
+
|
|
|
+ if(FifoPop(g_hFifoTX, &b, false))
|
|
|
+ {
|
|
|
+ ++g_nTxEchoCount;
|
|
|
+ UARTCharPutNonBlocking(UART_BASE, b);
|
|
|
+ ++g_statCnt.nCharsTransmitted;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ UARTIntDisable(UART_BASE, UART_INT_TX);
|
|
|
+ GPIOPinWrite(EN_485_PORT, EN_485_PIN, 0); // disable the bus
|
|
|
+ FifoClearFlags(g_hFifoTX, MB_RTU_FLAG_TRANSMIT_IN_PROGRESS, false);
|
|
|
+ FifoSetFlags(g_hFifoTX, MB_RTU_FLAG_TRANSMIT_END, false);
|
|
|
+ _FrameTimerStart();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/////////////////////////////////////////////////////////////////////////////
|
|
|
+
|
|
|
+static void UART_OE_ISR(void)
|
|
|
+{
|
|
|
+ ++g_statCnt.nOverrunErrors;
|
|
|
+ FifoSetFlags(g_hFifoRX, MB_RTU_FLAG_OVERRUN_ERROR, false);
|
|
|
+}
|
|
|
+
|
|
|
+static void UART_BE_ISR(void)
|
|
|
+{
|
|
|
+ ++g_statCnt.nBreakErrors;
|
|
|
+ FifoSetFlags(g_hFifoRX, MB_RTU_FLAG_BREAK_ERROR, false);
|
|
|
+}
|
|
|
+
|
|
|
+static void UART_PE_ISR(void)
|
|
|
+{
|
|
|
+ ++g_statCnt.nParityErrors;
|
|
|
+ FifoSetFlags(g_hFifoRX, MB_RTU_FLAG_PARITY_ERROR, false);
|
|
|
+}
|
|
|
+
|
|
|
+static void UART_FE_ISR(void)
|
|
|
+{
|
|
|
+ ++g_statCnt.nFramingErrors;
|
|
|
+ FifoSetFlags(g_hFifoRX, MB_RTU_FLAG_FRAMING_ERROR, false);
|
|
|
+}
|
|
|
+
|
|
|
+/////////////////////////////////////////////////////////////////////////////
|
|
|
+
|
|
|
+static void UART_ISR(void)
|
|
|
+{
|
|
|
+ uint32_t nStatus = UARTIntStatus(UART_BASE, true); // Get the interrrupt status.
|
|
|
+ UARTIntClear(UART_BASE, nStatus); // Clear the asserted interrupts.
|
|
|
+
|
|
|
+ if(nStatus & UART_INT_TX)
|
|
|
+ UART_TX_ISR();
|
|
|
+ if(nStatus & UART_INT_RX)
|
|
|
+ UART_RX_ISR();
|
|
|
+ if(nStatus & UART_INT_OE) // UART Overrun Error
|
|
|
+ UART_OE_ISR();
|
|
|
+ if(nStatus & UART_INT_BE) // UART Break Error
|
|
|
+ UART_BE_ISR();
|
|
|
+ if(nStatus & UART_INT_PE) // UART Parity Error
|
|
|
+ UART_PE_ISR();
|
|
|
+ if(nStatus & UART_INT_FE) // UART Framing Error
|
|
|
+ UART_FE_ISR();
|
|
|
+}
|
|
|
+
|
|
|
+/////////////////////////////////////////////////////////////////////////////
|
|
|
+
|
|
|
+static bool EnableRxInt(bool bEnable)
|
|
|
+{
|
|
|
+ bool bIsEnabled = IS_INT_ENABLED(UART_INT_RX);
|
|
|
+ if(bEnable && !bIsEnabled)
|
|
|
+ UARTIntEnable(UART_BASE, UART_INT_RX);
|
|
|
+ else if(!bEnable && bIsEnabled)
|
|
|
+ UARTIntDisable(UART_BASE, UART_INT_RX);
|
|
|
+ return bIsEnabled;
|
|
|
+}
|
|
|
+
|
|
|
+/////////////////////////////////////////////////////////////////////////////
|
|
|
+
|
|
|
+static bool EnableTxInt(bool bEnable)
|
|
|
+{
|
|
|
+ bool bIsEnabled = IS_INT_ENABLED(UART_INT_TX);
|
|
|
+ if(bEnable && !bIsEnabled)
|
|
|
+ UARTIntEnable(UART_BASE, UART_INT_TX);
|
|
|
+ else if(!bEnable && bIsEnabled)
|
|
|
+ UARTIntDisable(UART_BASE, UART_INT_TX);
|
|
|
+ return bIsEnabled;
|
|
|
+}
|
|
|
+
|
|
|
+/////////////////////////////////////////////////////////////////////////////
|
|
|
+
|
|
|
+static uint32_t _GetDatabits(int nDatabits)
|
|
|
+{
|
|
|
+ switch(nDatabits)
|
|
|
+ {
|
|
|
+ case 5:
|
|
|
+ return UART_CONFIG_WLEN_5;
|
|
|
+ case 6:
|
|
|
+ return UART_CONFIG_WLEN_6;
|
|
|
+ case 7:
|
|
|
+ return UART_CONFIG_WLEN_7;
|
|
|
+ case 8:
|
|
|
+ return UART_CONFIG_WLEN_8;
|
|
|
+ default:
|
|
|
+ return (uint32_t)-1;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/////////////////////////////////////////////////////////////////////////////
|
|
|
+
|
|
|
+static uint32_t _GetStopbits(int nStopbits)
|
|
|
+{
|
|
|
+ switch(nStopbits)
|
|
|
+ {
|
|
|
+ case 1:
|
|
|
+ return UART_CONFIG_STOP_ONE;
|
|
|
+ case 2:
|
|
|
+ return UART_CONFIG_STOP_TWO;
|
|
|
+ default:
|
|
|
+ return (uint32_t)-1;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/////////////////////////////////////////////////////////////////////////////
|
|
|
+
|
|
|
+static uint32_t _GetParity(UART_Parity parity)
|
|
|
+{
|
|
|
+ switch(parity)
|
|
|
+ {
|
|
|
+ case P_None:
|
|
|
+ return UART_CONFIG_PAR_NONE;
|
|
|
+ case P_Even:
|
|
|
+ return UART_CONFIG_PAR_EVEN;
|
|
|
+ case P_Odd:
|
|
|
+ return UART_CONFIG_PAR_ODD;
|
|
|
+ case P_Zero:
|
|
|
+ return UART_CONFIG_PAR_ZERO;
|
|
|
+ case P_One:
|
|
|
+ return UART_CONFIG_PAR_ONE;
|
|
|
+ default:
|
|
|
+ return (uint32_t)-1;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/////////////////////////////////////////////////////////////////////////////
|
|
|
+/////////////////////////////////////////////////////////////////////////////
|
|
|
+
|
|
|
+bool MB_UART_Init(LPCUART_CONFIG pCfg)
|
|
|
+{
|
|
|
+ uint32_t nData, nStop, nPar;
|
|
|
+
|
|
|
+ if(!pCfg)
|
|
|
+ return false;
|
|
|
+ memcpy(&g_uartCfg, pCfg, sizeof(g_uartCfg));
|
|
|
+
|
|
|
+ /////////////////////////////////////////////////////////////////////////
|
|
|
+ // validate parameters
|
|
|
+
|
|
|
+ if((nData = _GetDatabits(pCfg->nDatabits)) == (uint32_t)-1)
|
|
|
+ return false;
|
|
|
+ if((nStop = _GetStopbits(pCfg->nStopbits)) == (uint32_t)-1)
|
|
|
+ return false;
|
|
|
+ if((nPar = _GetParity(pCfg->parity)) == (uint32_t)-1)
|
|
|
+ return false;
|
|
|
+
|
|
|
+ /////////////////////////////////////////////////////////////////////////
|
|
|
+ // Fifos
|
|
|
+
|
|
|
+ FIFO_BACKEND backend;
|
|
|
+
|
|
|
+ memset(&backend, 0, sizeof(backend));
|
|
|
+ backend.pfnLockBackend = EnableTxInt;
|
|
|
+ backend.pfnTxPrepare = _OnTxPrepare;
|
|
|
+ backend.pfnTxStart = _OnTxStart;
|
|
|
+ backend.pfnTxFinalize = _OnTxFinalize;
|
|
|
+
|
|
|
+ if(!(g_hFifoTX = FifoCreate(pCfg->nFifoIndexTx, &backend)))
|
|
|
+ return false;
|
|
|
+
|
|
|
+ memset(&backend, 0, sizeof(backend));
|
|
|
+ backend.pfnLockBackend = EnableRxInt;
|
|
|
+ backend.pfnRxFinalize = _OnRxFinalize;
|
|
|
+
|
|
|
+ if(!(g_hFifoRX = FifoCreate(pCfg->nFifoIndexRx, &backend)))
|
|
|
+ {
|
|
|
+ FifoRelease(g_hFifoTX);
|
|
|
+ g_hFifoTX = NULL;
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /////////////////////////////////////////////////////////////////////////
|
|
|
+ // Timer tick
|
|
|
+
|
|
|
+ g_nTimerTick = 0;
|
|
|
+
|
|
|
+ /////////////////////////////////////////////////////////////////////////
|
|
|
+ // UART
|
|
|
+
|
|
|
+ if(pCfg->nBaud)
|
|
|
+ {
|
|
|
+ g_MODBUS_RTU_FRAME_TIMEOUT_US = (350000000 / pCfg->nBaud + 5) / 10;
|
|
|
+ g_MODBUS_RTU_CHAR_TIMEOUT_US = (150000000 / pCfg->nBaud + 5) / 10;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ // For baud rates greater than 19200 the t1.5 and t3.5 timers are implemented
|
|
|
+ // using fixed values, for t1.5 that is 750μs, for t3.5 1.750ms
|
|
|
+ g_MODBUS_RTU_FRAME_TIMEOUT_US = 1750;
|
|
|
+ g_MODBUS_RTU_CHAR_TIMEOUT_US = 750;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Enable and setup the PORT that is used for the UART
|
|
|
+ SysCtlPeripheralEnable(UART_PORT_SYSCTL);
|
|
|
+ while(!SysCtlPeripheralReady(UART_PORT_SYSCTL))
|
|
|
+ ;
|
|
|
+
|
|
|
+ // Enable and setup the PORT that is used for EN_485
|
|
|
+ SysCtlPeripheralEnable(EN_485_PORT_SYSCTL);
|
|
|
+ while(!SysCtlPeripheralReady(EN_485_PORT_SYSCTL))
|
|
|
+ ;
|
|
|
+
|
|
|
+ GPIOPinTypeGPIOOutput(EN_485_PORT, EN_485_PIN);
|
|
|
+
|
|
|
+ // Enable the uart used for RS485 interface
|
|
|
+ SysCtlPeripheralEnable(UART_BASE_SYSCTL);
|
|
|
+ while(!SysCtlPeripheralReady(UART_BASE_SYSCTL))
|
|
|
+ ;
|
|
|
+
|
|
|
+ // Configure the UART pins
|
|
|
+ GPIOPinConfigure(UART_RX_PIN_MUX);
|
|
|
+ GPIOPinConfigure(UART_TX_PIN_MUX);
|
|
|
+ GPIOPinTypeUART(UART_PORT, UART_RX_PIN | UART_TX_PIN);
|
|
|
+
|
|
|
+ // Initialize the UART. Set the baud rate, number of data bits, turn off
|
|
|
+ // parity, number of stop bits, and stick mode.
|
|
|
+ UARTConfigSetExpClk(UART_BASE, SysCtlClockGet(), pCfg->nBaud, nData | nStop | nPar);
|
|
|
+
|
|
|
+ // transmit interrupt is asserted when the transmitter is completely idle
|
|
|
+ UARTTxIntModeSet(UART_BASE, UART_TXINT_MODE_EOT);
|
|
|
+
|
|
|
+ // Enable the UART interrupt.
|
|
|
+ IntPrioritySet(UART_INT, MODBUS_RTU_UART_INT_PRIORITY); // priority must be lower than system tick
|
|
|
+ UARTIntRegister(UART_BASE, UART_ISR);
|
|
|
+ UARTIntEnable(UART_BASE, (UART_INT_RX | UART_INT_OE | UART_INT_BE | UART_INT_PE | UART_INT_FE));
|
|
|
+
|
|
|
+ // Enable the UART.
|
|
|
+ UARTEnable(UART_BASE);
|
|
|
+ UARTFIFODisable(UART_BASE);
|
|
|
+
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+/////////////////////////////////////////////////////////////////////////////
|
|
|
+
|
|
|
+void MB_UART_Release(void)
|
|
|
+{
|
|
|
+ UARTIntDisable(UART_BASE, UART_INT_RX | UART_INT_TX | UART_INT_OE | UART_INT_BE | UART_INT_PE | UART_INT_FE);
|
|
|
+
|
|
|
+ UARTIntUnregister(UART_BASE);
|
|
|
+ UARTDisable(UART_BASE);
|
|
|
+
|
|
|
+ SysCtlPeripheralDisable(UART_BASE_SYSCTL);
|
|
|
+ SysCtlPeripheralDisable(EN_485_PORT_SYSCTL);
|
|
|
+ SysCtlPeripheralDisable(UART_PORT_SYSCTL);
|
|
|
+
|
|
|
+ if(g_hFifoRX)
|
|
|
+ {
|
|
|
+ FifoRelease(g_hFifoRX);
|
|
|
+ g_hFifoRX = NULL;
|
|
|
+ }
|
|
|
+
|
|
|
+ if(g_hFifoTX)
|
|
|
+ {
|
|
|
+ FifoRelease(g_hFifoTX);
|
|
|
+ g_hFifoTX = NULL;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/////////////////////////////////////////////////////////////////////////////
|
|
|
+
|
|
|
+HFIFO MB_UART_GetRxFifo(void)
|
|
|
+{
|
|
|
+ return g_hFifoRX;
|
|
|
+}
|
|
|
+
|
|
|
+/////////////////////////////////////////////////////////////////////////////
|
|
|
+
|
|
|
+HFIFO MB_UART_GetTxFifo(void)
|
|
|
+{
|
|
|
+ return g_hFifoTX;
|
|
|
+}
|
|
|
+
|
|
|
+/////////////////////////////////////////////////////////////////////////////
|
|
|
+
|
|
|
+void MB_UART_GetStatusCounters(LPUART_STATUS_COUNTERS pSc)
|
|
|
+{
|
|
|
+ if(pSc)
|
|
|
+ {
|
|
|
+ uint32_t nIrMask = INT_MASK_REGISTER;
|
|
|
+ UARTIntDisable(UART_BASE, nIrMask);
|
|
|
+ memcpy(pSc, &g_statCnt, sizeof(g_statCnt));
|
|
|
+ UARTIntEnable(UART_BASE, nIrMask);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/////////////////////////////////////////////////////////////////////////////
|
|
|
+
|
|
|
+void MB_UART_ResetStatusCounters(void)
|
|
|
+{
|
|
|
+ uint32_t nIrMask = INT_MASK_REGISTER;
|
|
|
+ UARTIntDisable(UART_BASE, nIrMask);
|
|
|
+ memset(&g_statCnt, 0, sizeof(g_statCnt));
|
|
|
+ UARTIntEnable(UART_BASE, nIrMask);
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|