首先,在使能modbus协议栈的时候,会调用pvMBFrameStartCur函数
/* 使能modbus */
eMBErrorCode eMBEnable(void)
{
eMBErrorCode eStatus = MB_ENOERR;
/* modbus还未使能 */
if(eMBState == STATE_DISABLED)
{
/* 启动modbus */
pvMBFrameStartCur();
/* 设置modbus状态为使能 */
eMBState = STATE_ENABLED;
}
else
{
/* 状态不合法 */
eStatus = MB_EILLSTATE;
}
return eStatus;
}
在rtu模式下pvMBFrameStartCur指针指向eMBASCIIStart函数
/* modbus ascii启动函数 */
void eMBASCIIStart(void)
{
ENTER_CRITICAL_SECTION();
/* 串口打开接收、关闭发送 */
vMBPortSerialEnable(TRUE, FALSE);
/* 接收状态设置为接收空闲 */
eRcvState = STATE_RX_IDLE;
EXIT_CRITICAL_SECTION();
/* 发送就绪事件 */
(void)xMBPortEventPost(EV_READY);
}
启动RTU时,接收状态eRcvState 设置为接收空闲态STATE_RX_IDLE,打开接收中断,向主程序发送就绪事件。
主程序接收到就绪事件后什么也没做
/* modbus轮询 */
eMBErrorCode eMBPoll(void)
{
......
/* 获取事件 */
if(xMBPortEventGet(&eEvent) == TRUE)
{
/* 判断事件类型 */
switch(eEvent)
{
/* 就绪事件 */
case EV_READY:
break;
......
}
}
return MB_ENOERR;
}
ASCII传输模式下,modbus报文包含明确的起始字符和结束字符
因此在接收空闲态下,如果接收到数据,先判断是否为起始字符。如果是起始字符,使能超时定时器,将接收状态eRcvState切换为接收态STATE_RX_RCV。由于ASCII模式下每个字节需要两个字符编码,所以需要来回切换高半字和低半字,一开始将字节位置初始化为高半字。
/* modbus ascii接收一个字节函数 */
BOOL xMBASCIIReceiveFSM(void)
{
BOOL xNeedPoll = FALSE;
UCHAR ucByte;
UCHAR ucResult;
assert_param(eSndState == STATE_TX_IDLE);
/* 串口接收一个字节 */
(void)xMBPortSerialGetByte((CHAR *) & ucByte);
/* 判断接收状态 */
switch(eRcvState)
{
......
/* 接收空闲状态 */
case STATE_RX_IDLE:
/* 起始字符 */
if(ucByte == ':')
{
/* 超时定时器使能 */
vMBPortTimersEnable();
/* 接收缓冲区偏移量初始化为0 */
usRcvBufferPos = 0;
/* 字节位置初始化高半字节 */
eBytePos = BYTE_HIGH_NIBBLE;
/* 将接收状态切换为接收态 */
eRcvState = STATE_RX_RCV;
}
break;
}
return xNeedPoll;
}
在接收态下,将一个一个字节接收数据。直到接收到结束字符CR,将接收状态eRcvState切换为等待结束态STATE_RX_WAIT_EOF。
/* modbus ascii接收一个字节函数 */
BOOL xMBASCIIReceiveFSM(void)
{
BOOL xNeedPoll = FALSE;
UCHAR ucByte;
UCHAR ucResult;
assert_param(eSndState == STATE_TX_IDLE);
/* 串口接收一个字节 */
(void)xMBPortSerialGetByte((CHAR *) & ucByte);
/* 判断接收状态 */
switch(eRcvState)
{
/* 接收态 */
case STATE_RX_RCV:
/* 超时定时器使能 */
vMBPortTimersEnable();
/* 起始字符 */
if(ucByte == ':')
{
/* 字节位置重新初始化高半字节 */
eBytePos = BYTE_HIGH_NIBBLE;
/* 接收缓冲区偏移量重新初始化为0 */
usRcvBufferPos = 0;
}
/* 结束字符,CR */
else if(ucByte == MB_ASCII_DEFAULT_CR)
{
/* 将接收状态设置为等待LF状态 */
eRcvState = STATE_RX_WAIT_EOF;
}
/* 普通数据 */
else
{
/* 将ascii码转换为数字 */
ucResult = prvucMBCHAR2BIN(ucByte);
/* 判断高半字节还是低半字节 */
switch(eBytePos)
{
/* 高半字节 */
case BYTE_HIGH_NIBBLE:
/* 数据帧最大256字节 */
if(usRcvBufferPos < MB_SER_PDU_SIZE_MAX)
{
/* 将数据填入高半字节 */
ucASCIIBuf[usRcvBufferPos] = (UCHAR)(ucResult << 4);
/* 字节位置设置为低半字节 */
eBytePos = BYTE_LOW_NIBBLE;
break;
}
/* 接收字节数超过256字节 */
else
{
/* 接收状态设置为空闲态 */
eRcvState = STATE_RX_IDLE;
/* 超时定时器关闭 */
vMBPortTimersDisable();
}
break;
/* 低半字节 */
case BYTE_LOW_NIBBLE:
/* 将数据填入低半字节 */
ucASCIIBuf[usRcvBufferPos] |= ucResult;
/* 字节数加一 */
usRcvBufferPos++;
/* 字节位置设置为高半字节 */
eBytePos = BYTE_HIGH_NIBBLE;
break;
}
}
break;
......
}
return xNeedPoll;
}
在等待结束态下,如果接收到LF字符,则表示一帧数据结束。关闭超时定时器,将接收状态eRcvState切换为接收空闲态STATE_RX_IDLE,向主程序发送接收完成事件。
/* modbus ascii接收一个字节函数 */
BOOL xMBASCIIReceiveFSM(void)
{
BOOL xNeedPoll = FALSE;
UCHAR ucByte;
UCHAR ucResult;
assert_param(eSndState == STATE_TX_IDLE);
/* 串口接收一个字节 */
(void)xMBPortSerialGetByte((CHAR *) & ucByte);
/* 判断接收状态 */
switch(eRcvState)
{
.......
/* 等待LF状态 */
case STATE_RX_WAIT_EOF:
/* 检查该字节是否为LF */
if(ucByte == ucMBLFCharacter)
{
/* 超时定时器关闭 */
vMBPortTimersDisable();
/* 将接收状态设置为接收空闲状态 */
eRcvState = STATE_RX_IDLE;
/* 发送接收完成事件 */
xNeedPoll = xMBPortEventPost(EV_FRAME_RECEIVED);
}
/* 该字节为起始字符 */
else if(ucByte == ':')
{
/* 字节位置重新初始化高半字节 */
eBytePos = BYTE_HIGH_NIBBLE;
/* 接收缓冲区偏移量重新初始化为0 */
usRcvBufferPos = 0;
/* 将接收状态重新切换为接收态 */
eRcvState = STATE_RX_RCV;
/* 超时定时器使能 */
vMBPortTimersEnable();
}
/* 该字节不是LF字符也不是LF字符 */
else
{
/* 将接收状态设置为接收空闲状态 */
eRcvState = STATE_RX_IDLE;
}
break;
......
}
return xNeedPoll;
}
主程序接收到接收完成事件之后,对数据帧进行校验和拆解,最后会得到PDU数据的指针和长度。并向主程序发送执行事件。
/* modbus轮询 */
eMBErrorCode eMBPoll(void)
{
......
/* 获取事件 */
if(xMBPortEventGet(&eEvent) == TRUE)
{
/* 判断事件类型 */
switch(eEvent)
{
......
/* 接收完成事件 */
case EV_FRAME_RECEIVED:
/* modbus接收函数,获取地址、PDU指针、PDU长度 */
eStatus = peMBFrameReceiveCur(&ucRcvAddress, &ucMBFrame, &usLength);
if(eStatus == MB_ENOERR)
{
/* 判断地址是否吻合 */
if((ucRcvAddress == ucMBAddress) || (ucRcvAddress == MB_ADDRESS_BROADCAST))
{
/* 发送执行事件 */
(void)xMBPortEventPost(EV_EXECUTE);
}
}
break;
......
}
}
return MB_ENOERR;
}
下面看一下peMBFrameReceiveCur调用的eMBASCIIReceive函数。主要工作是,对数据帧进行LRC校验,然后对数据帧进行拆分。
/* modbus ascii接收函数 */
eMBErrorCode eMBASCIIReceive(UCHAR *pucRcvAddress, UCHAR **pucFrame, USHORT *pusLength)
{
eMBErrorCode eStatus = MB_ENOERR;
ENTER_CRITICAL_SECTION();
assert_param(usRcvBufferPos < MB_SER_PDU_SIZE_MAX);
/* ASCII数据帧最小3字节,进行LRC校验 */
if((usRcvBufferPos >= MB_SER_PDU_SIZE_MIN) && (prvucMBLRC((UCHAR *)ucASCIIBuf, usRcvBufferPos) == 0))
{
/* 从机地址 */
*pucRcvAddress = ucASCIIBuf[MB_SER_PDU_ADDR_OFF];
/* PDU长度=ADU长度-1字节(地址)-1字节(LRC) */
*pusLength = (USHORT)(usRcvBufferPos - MB_SER_PDU_PDU_OFF - MB_SER_PDU_SIZE_LRC);
/* PDU数据指针 */
*pucFrame = (UCHAR *)&ucASCIIBuf[MB_SER_PDU_PDU_OFF];
}
/* 检验失败 */
else
{
/* IO错误 */
eStatus = MB_EIO;
}
EXIT_CRITICAL_SECTION();
return eStatus;
}
主程序接收到执行事件之后,判断功能码,调用相应功能函数。然后对主机进行响应。
/* modbus轮询 */
eMBErrorCode eMBPoll(void)
{
......
/* 获取事件 */
if(xMBPortEventGet(&eEvent) == TRUE)
{
/* 判断事件类型 */
switch(eEvent)
{
......
/* 执行事件 */
case EV_EXECUTE:
/* 功能码 */
ucFunctionCode = ucMBFrame[MB_PDU_FUNC_OFF];
eException = MB_EX_ILLEGAL_FUNCTION;
/* 遍历所有支持的功能码 */
for(i = 0; i < MB_FUNC_HANDLERS_MAX; i++)
{
/* 遍历完了 */
if(xFuncHandlers[i].ucFunctionCode == 0)
{
break;
}
/* 匹配到合适的功能码 */
else if(xFuncHandlers[i].ucFunctionCode == ucFunctionCode)
{
/* 调用相关功能 */
eException = xFuncHandlers[i].pxHandler(ucMBFrame, &usLength);
break;
}
}
/* 不是广播 */
if(ucRcvAddress != MB_ADDRESS_BROADCAST)
{
/* 出现异常 */
if(eException != MB_EX_NONE)
{
/* PDU长度初始化为0 */
usLength = 0;
/* 功能码+0x80则表示异常 */
ucMBFrame[usLength++] = (UCHAR)(ucFunctionCode | MB_FUNC_ERROR);
/* 异常码 */
ucMBFrame[usLength++] = eException;
}
if((eMBCurrentMode == MB_ASCII) && MB_ASCII_TIMEOUT_WAIT_BEFORE_SEND_MS)
{
vMBPortTimersDelay(MB_ASCII_TIMEOUT_WAIT_BEFORE_SEND_MS);
}
/* 发送响应帧 */
eStatus = peMBFrameSendCur(ucMBAddress, ucMBFrame, usLength);
}
break;
......
}
}
return MB_ENOERR;
}
peMBFrameSendCur指针调用eMBASCIISend对主机进行响应。主要工作包括:将PDU封装为ADU数据,将发送状态eSndState切换为发送开始态STATE_TX_START,并启动发送,发送一个字节。
/* modbus ascii发送函数 */
eMBErrorCode eMBASCIISend(UCHAR ucSlaveAddress, const UCHAR *pucFrame, USHORT usLength)
{
eMBErrorCode eStatus = MB_ENOERR;
UCHAR usLRC;
ENTER_CRITICAL_SECTION();
/* 接收空闲状态 */
if(eRcvState == STATE_RX_IDLE)
{
/* 将指针偏移到ADU */
pucSndBufferCur = (UCHAR *)pucFrame - 1;
/* 1字节(从机地址) */
usSndBufferCount = 1;
/* 从机地址 */
pucSndBufferCur[MB_SER_PDU_ADDR_OFF] = ucSlaveAddress;
/* PDU长度 */
usSndBufferCount += usLength;
/* LRC */
usLRC = prvucMBLRC((UCHAR *)pucSndBufferCur, usSndBufferCount);
ucASCIIBuf[usSndBufferCount++] = usLRC;
/* 发送开始状态 */
eSndState = STATE_TX_START;
/* 串口启动,使能发送 */
vMBPortSerialEnable(FALSE, TRUE);
}
/* 接收不在空闲状态不能发送 */
else
{
/* IO错误 */
eStatus = MB_EIO;
}
EXIT_CRITICAL_SECTION();
return eStatus;
}
在发送开始态下,发送起始字符。然后将发送状态eSndState切换为发送数据态STATE_TX_DATA,将字节位置初始化为高半字节。
/* modbus ascii发送一个字节函数 */
BOOL xMBASCIITransmitFSM(void)
{
BOOL xNeedPoll = FALSE;
UCHAR ucByte;
assert_param(eRcvState == STATE_RX_IDLE);
/* 判断发送状态 */
switch(eSndState)
{
/* 发送开始状态 */
case STATE_TX_START:
/* 发送起始字符 */
ucByte = ':';
xMBPortSerialPutByte((CHAR)ucByte);
/* 发送状态设置为发送态 */
eSndState = STATE_TX_DATA;
/* 字节位置初始化高半字节 */
eBytePos = BYTE_HIGH_NIBBLE;
break;
......
}
return xNeedPoll;
}
在发送数据态下,将数据一个字节一个字节发送出去。直到数据都发送完毕,发送结束字符CR,将发送状态eSndState设置为发送结束态STATE_TX_END。
/* modbus ascii发送一个字节函数 */
BOOL xMBASCIITransmitFSM(void)
{
BOOL xNeedPoll = FALSE;
UCHAR ucByte;
assert_param(eRcvState == STATE_RX_IDLE);
/* 判断发送状态 */
switch(eSndState)
{
......
/* 发送态 */
case STATE_TX_DATA:
/* 还有数据未发送 */
if(usSndBufferCount > 0)
{
/* 判断字节位置 */
switch(eBytePos)
{
/* 高半字节 */
case BYTE_HIGH_NIBBLE:
/* 将数字转换为ascii码 */
ucByte = prvucMBBIN2CHAR((UCHAR)(*pucSndBufferCur >> 4));
/* 将数据发送出去 */
xMBPortSerialPutByte((CHAR)ucByte);
/* 字节位置设置为低半字节 */
eBytePos = BYTE_LOW_NIBBLE;
break;
/* 低半字节 */
case BYTE_LOW_NIBBLE:
/* 将数字转换为ascii码 */
ucByte = prvucMBBIN2CHAR((UCHAR)(*pucSndBufferCur & 0x0F));
/* 将数据发送出去 */
xMBPortSerialPutByte((CHAR)ucByte);
/* 将指针向后偏移一个 */
pucSndBufferCur++;
/* 字节位置设置为高半字节 */
eBytePos = BYTE_HIGH_NIBBLE;
/* 剩余字节数减一 */
usSndBufferCount--;
break;
}
}
/* 数据发完 */
else
{
/* 发送结束字符,CR */
xMBPortSerialPutByte(MB_ASCII_DEFAULT_CR);
/* 将发送状态设置为发送结束态 */
eSndState = STATE_TX_END;
}
break;
......
}
return xNeedPoll;
}
在发送结束态下,发送结束字符LR,将发送状态切换为通知态。
/* modbus ascii发送一个字节函数 */
BOOL xMBASCIITransmitFSM(void)
{
BOOL xNeedPoll = FALSE;
UCHAR ucByte;
assert_param(eRcvState == STATE_RX_IDLE);
/* 判断发送状态 */
switch(eSndState)
{
......
/* 发送结束态 */
case STATE_TX_END:
/* 发送结束字符,LF */
xMBPortSerialPutByte((CHAR)ucMBLFCharacter);
/* 发送状态设置为通知态 */
eSndState = STATE_TX_NOTIFY;
break;
......
}
return xNeedPoll;
}
在通知态下,将发送状态eSndState切换为发送空闲态STATE_TX_IDLE,打开接收中断,通知主程序发送完毕事件
/* modbus ascii发送一个字节函数 */
BOOL xMBASCIITransmitFSM(void)
{
BOOL xNeedPoll = FALSE;
UCHAR ucByte;
assert_param(eRcvState == STATE_RX_IDLE);
/* 判断发送状态 */
switch(eSndState)
{
......
/* 通知态 */
case STATE_TX_NOTIFY:
/* 将发送状态设置为空闲态 */
eSndState = STATE_TX_IDLE;
/* 发送发送完成事件 */
xNeedPoll = xMBPortEventPost(EV_FRAME_SENT);
/* 串口接收启动、发送关闭 */
vMBPortSerialEnable(TRUE, FALSE);
/* 将发送状态设置为空闲态 */
eSndState = STATE_TX_IDLE;
break;
......
}
return xNeedPoll;
}
主程序接收到发送完毕事件后,什么也不做
/* modbus轮询 */
eMBErrorCode eMBPoll(void)
{
......
/* 获取事件 */
if(xMBPortEventGet(&eEvent) == TRUE)
{
/* 判断事件类型 */
switch(eEvent)
{
......
/* 发送完成 */
case EV_FRAME_SENT:
break;
}
}
return MB_ENOERR;
}