开源PLC学习笔记16续(FREEMODBUS eMBPoll侦听)——2013_12_8
本帖最后由 oldbeginner 于 2013-12-8 20:25 编辑FreeModbus果然不是免费的,要想理解它就要花很多精力。看了这么多,基本上明白,如果应用中,只是用modbus控制几个线圈或寄存器,还是不要用freemodbus,用笔记15中的例子就非常简单和实用。举一反三,如果觉得论文写得一目了然,导师看不上眼,那么控制几个线圈时就使用freemodbus。
笔记16中使用的两个例子目前看来不适合入门,现在改为下面这个例子,avr mega128可以利用PROTEUS和modbus poll进行通信。
http://www.cnblogs.com/worldsing/category/502885.html
这个例子改得比较简练,和笔记16中我想做的差不多,这样就很省力气和时间,直接理解这个例子作为入门还是不错的。
理解FREEMODBUS的突破口就是eMBPoll函数了
http://www.cnblogs.com/worldsing/category/502885.html
直接摘录
eMbPoll()的作用是FreeMod协议通信过程中不断查询事件对列有无完速数据桢,并进行地址和CRC验证,最后运行和回复主机。
改进说明:
1、eMbPoll()调用一次即可运行功能码和回复主机;
2、省去独立的接收函数peMBFrameReceiveCur( &ucRcvAddress, &ucMBFrame, &usLength ); 而直接操作,(其实里面对算出数据桢的启始位置、和长度);
3、省去发送函数peMBFrameSendCur( ucMBAddress, ucMBFrame, usLength ); 而直接操作;
4、省去返回值,因为调用处没有使用;
5、对功能的遍历i改成unsigned char类型,省去ucRcvAddress和eMBErrorCode eStatus = MB_ENOERR; 变量,
6、功能兼容原版本。 本帖最后由 oldbeginner 于 2013-12-8 19:32 编辑
void eMBPoll( void )
{
变量定义();
桢事件判断();
最小桢判断();
CRC判断();
地址判断();
执行功能码();
回复主机();
错误码判断();
报文发送();
}
*******************************
变量定义();
static UCHAR *ucMBFrame;
static UCHAR ucFunctionCode;
static USHORT usLength;
static eMBException eException;
eMBEventType eEvent;
UCHAR i;
USHORT usCRC16;
变量名字前面加uc的意思应该是,uchar变量,MB表示modbus,所以ucMBFrame表示 uchar变量的modbus帧,前面加*,是一个指针。
ucFunctionCode应该是uchar变量的功能码,应该类似散转函数,只不过freemodbus使用了大量#ifdef .....#endif等,各种标志位来回绕几圈,现在还不清楚怎么调用。
usLength应该是ushort变量的帧长度。
static eMBException eException(ModBusRTU.h);
需要查看eMBException的定义,
typedef enum
{
MB_EX_NONE = 0x00,
MB_EX_ILLEGAL_FUNCTION = 0x01,
MB_EX_ILLEGAL_DATA_ADDRESS = 0x02,
MB_EX_ILLEGAL_DATA_VALUE = 0x03,
MB_EX_SLAVE_DEVICE_FAILURE = 0x04,
MB_EX_ACKNOWLEDGE = 0x05,
MB_EX_SLAVE_BUSY = 0x06,
MB_EX_MEMORY_PARITY_ERROR = 0x08,
MB_EX_GATEWAY_PATH_FAILED = 0x0A,
MB_EX_GATEWAY_TGT_FAILED = 0x0B
} eMBException;
e表示enum,MB同上,Exception表示异常状况,共有10种异常状况,暂不展开(原程序也没有解释,估计不太重要)。
上面四种变量都是static型,只能用在ModBusRTU.c中。
然后,
eMBEventType eEvent;
查看定义(ModBusRTU.h),
typedef enum
{
EV_READY, /*!< Startup finished. */
EV_FRAME_RECEIVED, /*!< Frame received. */
EV_EXECUTE, /*!< Execute function. */
EV_FRAME_SENT /*!< Frame sent. */
} eMBEventType;
这个变量好理解,可以用来表示状态,共有4种状态。
UCHAR i;
执行功能代码时,要用到的循环变量。
USHORT usCRC16;
ushort类型的检验码。
在avr中 USHORT (unsigned short) 是16位的。
*****************************************************
桢事件判断();
if(xMBPortEventGet( &eEvent) == TRUE ){
if(eEvent == EV_FRAME_RECEIVED){
调用了函数
xMBPortEventGet
查看一下定义(ModBusPort.c),
//出队列
BOOL xMBPortEventGet( eMBEventType * eEvent ){
BOOL xEventHappened = FALSE;
if( xEventInQueue ){
*eEvent = eQueuedEvent;
xEventInQueue = FALSE;
xEventHappened = TRUE;
}
return xEventHappened;
}
另外还要查看
static eMBEventType eQueuedEvent;
static BOOL xEventInQueue;
首先理解xEventInQueue,x表示返回类型布尔值,直译:队列中的事件。如果它为真,则函数返回值为真。
对
xEventInQueue赋值的函数有两个,
//对列初始化
BOOL xMBPortEventInit( void ) {
xEventInQueue = FALSE;
return TRUE;
}
//进入队列
BOOL xMBPortEventPost( eMBEventType eEvent ){
xEventInQueue = TRUE;
eQueuedEvent = eEvent;
return TRUE;
}
因为初始化只一次,所以表示进入队列的函数在某个地方应该被调用了,否则出队列返回值一直为false。
再看xMBPortEventGet中的
*eEvent = eQueuedEvent;
eQueuedEvent是eMBEventType类型,直译:队列中的事件。
//进入队列
BOOL xMBPortEventPost( eMBEventType eEvent ){
xEventInQueue = TRUE;
eQueuedEvent = eEvent;
return TRUE;
}
当从事件队列中提取的事件为EV_FRAME_RECEIVED时,表示接收帧状态,就会继续执行。
if(eEvent == EV_FRAME_RECEIVED),即
eQueuedEvent == EV_FRAME_RECEIVED
********************************************************************
oldbeginner 发表于 2013-12-8 14:18
void eMBPoll( void )
{
变量定义();
最小桢判断();
if(usRcvBufferPos < MB_SER_PDU_SIZE_MIN) //最小桢判断
return;
首先查看usRcvBufferPos 定义(ModBusRTU.c),
static volatile USHORT usRcvBufferPos;
变量定义中含有volatile,就应该马上想到该变量是多任务共享的(笔记12)或是中断修改的标志位。
us表示ushort变量,Receive Buffer Position,直译:接收缓存位置(接收数组下标),是用来定位接收的缓存。
另外
#define MB_SER_PDU_SIZE_MIN 4 /*!< Minimum size of a Modbus RTU frame. */
帧最小长度为何是4,地址,命令和CRC校验,正好4个字节。
所以最小帧判断, if ( usRcvBufferPos < 4 ) return;
********************************************************************
CRC判断();
if(usMBCRC16((UCHAR *)ucRTUBuf, usRcvBufferPos ) != 0) //CRC判断
return;
调用了函数usMBCRC16 (ModBusRTU.c),查看一下
USHORT usMBCRC16( UCHAR * pucFrame, USHORT usLen )
{
UCHAR ucCRCHi = 0xFF;
UCHAR ucCRCLo = 0xFF;
USHORT iIndex;
while( usLen-- ){
iIndex = ucCRCLo ^ *( pucFrame++ );
ucCRCLo = ( UCHAR )( ucCRCHi ^ aucCRCHi );
ucCRCHi = aucCRCLo;
}
return ( USHORT )( ucCRCHi << 8 | ucCRCLo );
}
和以前处理CRC函数一样,不再展开,只使用。
暂时只需要知道该函数判断是否CRC正确,用到了两个参数,一个是刚理解过的usRcvBufferPos ,另一个是ucRTUBuf,
找到定义,
volatile UCHARucRTUBuf;
其中,
#define MB_SER_PDU_SIZE_MAX 256 /*!< Maximum size of a Modbus RTU frame. */
即volatile UCHAR ucRTUBuf;
就是接收缓存(数组)
******************************************************************************
地址判断();
if(IS_VALID_ADD){ //地址
ucMBFrame = (UCHAR *) &ucRTUBuf;
usLength = (USHORT)( usRcvBufferPos - MB_SER_PDU_PDU_OFF - MB_SER_PDU_SIZE_CRC);
ucFunctionCode = ucMBFrame;
eException = MB_EX_ILLEGAL_FUNCTION;
有点长,主要是名称太长了,这样的长名字会让我想到安娜·阿尔卡迪耶夫娜·卡列尼娜;不太熟悉,作者是列夫·尼古拉耶维奇·托尔斯泰。
IS_VALID_ADD是一个宏,
查看定义,
#define IS_VALID_ADD ((ucRTUBuf == ucMBAddress) || \
(ucRTUBuf == MB_ADDRESS_BROADCAST))
#define MB_SER_PDU_ADDR_OFF 0 /*!< Offset of slave address in Ser-PDU. */
ucRTUBuf == ucMBAddress 或 ucRTUBuf == MB_ADDRESS_BROADCAST
现在大致可以看出,就是判断从机地址,或者广播地址
ucRTUBuf == 从机地址 或 ucRTUBuf == 广播地址
从机地址在ModBusRTU.c中定义,
static UCHAR ucMBAddress;
在主函数中被代入数值,
上图表示从机设备地址为0x01。
广播地址
#define MB_ADDRESS_BROADCAST ( 0 ) /*! Modbus broadcast address. */
即
ucRTUBuf == 1 或 ucRTUBuf == 0
就是有效地址
ucMBFrame = (UCHAR *) &ucRTUBuf;
其中
#define MB_SER_PDU_PDU_OFF 1 /*!< Offset of Modbus-PDU in Ser-PDU. */
ucMBFrame = (UCHAR *) &ucRTUBuf;
应该是
ucMBFrame 指向 命令码;
usLength = (USHORT)( usRcvBufferPos - MB_SER_PDU_PDU_OFF - MB_SER_PDU_SIZE_CRC);
其中,
#define MB_SER_PDU_SIZE_CRC 2 /*!< Size of CRC field in PDU. */
即,
usLength = (USHORT)( usRcvBufferPos - 1 -2 )
ucFunctionCode = ucMBFrame;
其中,
#define MB_PDU_FUNC_OFF 0 /*!< Offset of function code in PDU. */
即,
ucFunctionCode = ucMBFrame;
即,
ucFunctionCode = 命令码,
真绕
eException = MB_EX_ILLEGAL_FUNCTION;
赋值。
************************************************************************************
{:victory:}支持 本帖最后由 oldbeginner 于 2013-12-8 16:29 编辑
oldbeginner 发表于 2013-12-8 15:18
最小桢判断();
if(usRcvBufferPos < MB_SER_PDU_SIZE_MIN) //最小桢判 ...
执行功能码();
for(i = 0; i < MB_FUNC_HANDLERS_MAX; i++ ){ //执行功能码
if( xFuncHandlers.ucFunctionCode == 0 ){
return;
}
else if( xFuncHandlers.ucFunctionCode == ucFunctionCode ){
eException = xFuncHandlers.pxHandler( ucMBFrame, &usLength );
break;
}
}
其中ModBusConfig.h定义了
#define MB_FUNC_HANDLERS_MAX ( 16 )//使用的功能码数量
即,
for(i = 0; i < 16 ; i++ ) {
再看xFuncHandlers是什么?它的生成方法,我是第一次看到,为了简短,这里只列出2种#if情况
先看定义
typedef struct
{
UCHAR ucFunctionCode;
pxMBFunctionHandler pxHandler;
} xMBFunctionHandler;
其中,
typedefeMBException(*pxMBFunctionHandler) (UCHAR* pucFrame, USHORT * pusLength );
第二个成员变量是执行异常处理的指针。
再看该结构数组的生成,
/* 定义功能码与功能处理函数列表 */
static xMBFunctionHandler xFuncHandlers = {
#if MB_FUNC_READ_INPUT_ENABLED > 0
{MB_FUNC_READ_INPUT_REGISTER, eMBFuncReadInputRegister},
#endif
#if MB_FUNC_READ_HOLDING_ENABLED > 0
{MB_FUNC_READ_HOLDING_REGISTER, eMBFuncReadHoldingRegister},
#endif
};
先看第一个#if
#define MB_FUNC_READ_INPUT_ENABLED (1 ) //读输入寄存器功能
这样的话,
#if总是成立,
{MB_FUNC_READ_INPUT_REGISTER, eMBFuncReadInputRegister}就会成为数组成员,
其中,
#define MB_FUNC_READ_INPUT_REGISTER (4 )
在ModBusFun.c中,
eMBException eMBFuncReadInputRegister( UCHAR * pucFrame, USHORT * usLen )
eMBFuncReadInputRegister是一个函数,暂不展开。
功 能: 0x04(4) 读取输入寄存器。
xMBFunctionHandler 数组的成员变量是一个结构体,结构体的第一个变量是命令码,后一个变量是命令码对应的函数名。
这样看来,应该和开源PLC的函数散转同一功能,感觉开源PLC的写法更好理解。
****************************
if( xFuncHandlers.ucFunctionCode == 0 ){
return;
}
如果第一个变量对应的命令码是0,则不执行。
**********************************
else if( xFuncHandlers.ucFunctionCode == ucFunctionCode ){
eException = xFuncHandlers.pxHandler( ucMBFrame, &usLength );
break;
}
再回顾一下,
ucFunctionCode = ucMBFrame;
而 ucMBFrame = (UCHAR *) &ucRTUBuf;
即,
xFuncHandlers.ucFunctionCode == ucRTUBuf;
即,
接收缓存的命令码和功能码数组第一个变量名一样的话,就调用
xFuncHandlers.pxHandler( ucMBFrame, &usLength );
pxHandle是第二个变量名,代入时换成实际的函数名称,例如
eMBFuncReadInputRegister( ucMBFrame, &usLength );
这样就完成了函数调用。
(太绕了,看了几天才第一次理解到散转函数,可读性较差,真心怀疑FREEMODBUS是好程序还是坏程序) 本帖最后由 oldbeginner 于 2013-12-8 19:33 编辑
oldbeginner 发表于 2013-12-8 16:05
执行功能码();
for(i = 0; i < MB_FUNC_HANDLERS_MAX; i++ ){ //执行功能码 ...
回复主机();
if(IS_NOT_BROADCAST){
查看定义
#define IS_NOT_BROADCAST(ucRTUBuf != MB_ADDRESS_BROADCAST)
即
#define IS_NOT_BROADCAST(ucRTUBuf != 0)
只要地址不是0。
********************************************************
错误码处理();
if( eException != MB_EX_NONE ){ //错误码
usLength = 0;
ucMBFrame = ( UCHAR )( ucFunctionCode | MB_FUNC_ERROR );
ucMBFrame = eException;
}
首先,
eException是执行功能后的返回值,
eException = xFuncHandlers.pxHandler( ucMBFrame, &usLength );
如果返回值是MB_EX_NONE,表示正常。
否则修改ucMBFrame,
ucMBFrame = ( UCHAR )( ucFunctionCode | MB_FUNC_ERROR );
ucMBFrame = eException;
***********************************************
报文发送();
if(eRcvState == STATE_RX_IDLE){ //发送
pucSndBufferCur = ( UCHAR * ) ucMBFrame - 1;
pucSndBufferCur = ucMBAddress;
usSndBufferCount = usLength + 1;
usCRC16 = usMBCRC16( ( UCHAR * ) pucSndBufferCur, usSndBufferCount );
ucRTUBuf = ( UCHAR )( usCRC16 & 0xFF );
ucRTUBuf = ( UCHAR )( usCRC16 >> 8 );
eSndState = STATE_TX_XMIT;
vMBPortSerialEnable( FALSE, TRUE );
}
其中,
extern volatile eMBRcvState eRcvState;
接收状态标志位,
利用笔记16中的状态图来理解
只有当eRcvState处于 STATE_RX_IDLE 状态时,才能发送。
发送的函数比较好理解,就是生成报文并发送。
首先,
static volatile UCHAR *pucSndBufferCur;
p表示指针,uc表示uchar,Send Buffer Current ,直译就是发送数组目前下标。
pucSndBufferCur = ( UCHAR * ) ucMBFrame - 1;
因为ucMBFram指向功能码,-1就是指向地址,即 pucSndBufferCur指向ucBufRTU。
pucSndBufferCur = ucMBAddress;
usSndBufferCount = usLength + 1;
usCRC16 = usMBCRC16( ( UCHAR * ) pucSndBufferCur, usSndBufferCount );
ucRTUBuf = ( UCHAR )( usCRC16 & 0xFF );
ucRTUBuf = ( UCHAR )( usCRC16 >> 8 );
生成报文,和笔记15类似。
eSndState = STATE_TX_XMIT;
设定发送状态,
最后调用 vMBPortSerialEnable( FALSE, TRUE );
查看,
/* ------------------------------- 串口操作 -----------------------------------*/
//串口收发控制
void vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable ){
ENTER_CRITICAL_SECTION();
if( xRxEnable ){
USART_RX_ENABLE();
RS485SWITCH_TO_RECEIVE();
}
else{
USART_RX_DISABLE();
RS485SWITCH_TO_SEND();
}
if( xTxEnable ){
USART_TX_ENABLE();
}
else{
USART_TX_DISABLE();
}
EXIT_CRITICAL_SECTION();
}
在笔记16中专门理解过,复习一下
执行的是
if( xTxEnable ){
USART_TX_ENABLE();
再查看
#define USART_TX_ENABLE() SetBit(UCSR1B, UDRIE1)
其中,
#define SetBit(port, bitn) (port |=(1<<(bitn)))
************************************
发送应该是通过串口中断进行的,
//发送中断子程序
#pragma vector = USART1_UDRE_vect
__interrupt void TX1_isr( void ){
xMBRTUTransmitFSM();
}
IAR中定义中断函数的格式是
/////////////////////////////////
#pragma vector=中断向量
__interrupt void 中断服务程序(void)
{
//中断处理程序
}
********************************************
//发送状态机由发送中断调用
void xMBRTUTransmitFSM( void )
{
assert( eRcvState == STATE_RX_IDLE );
switch ( eSndState )
{
case STATE_TX_IDLE:
vMBPortSerialEnable( TRUE, FALSE );
break;
case STATE_TX_XMIT:
if( usSndBufferCount != 0 ){
xMBPortSerialPutByte( ( CHAR )*pucSndBufferCur );
pucSndBufferCur++;
usSndBufferCount--;
}
else{
xMBPortEventPost( EV_FRAME_SENT );
vMBPortSerialEnable( TRUE, FALSE );
eSndState = STATE_TX_IDLE;
}
break;
}
}
关注
case STATE_TX_XMIT:
if( usSndBufferCount != 0 ){
xMBPortSerialPutByte( ( CHAR )*pucSndBufferCur );
pucSndBufferCur++;
usSndBufferCount--;
}
因为设置了标志位STATE_TX_XMIT,调用了函数xMBPortSerialPutByte,直到待发送数为0 。
其中,
#define xMBPortSerialPutByte(ucByte)((UART_DR) = (ucByte))
这样就完成了eMBPoll的第一遍理解。
*************************** {:handshake:}{:victory:}{:lol:} 来膜拜楼主~{:loveliness:} 本帖最后由 oldbeginner 于 2013-12-8 22:56 编辑
oldbeginner 发表于 2013-12-8 17:23
回复主机();
if(IS_NOT_BROADCAST){
oldbeginner 发表于 2013-12-8 22:42
第二遍理解eMBPoll
有意思,顶一个 oldbeginner 发表于 2013-12-9 12:46
第二遍理解eMBPoll
复习发送报文的过程
oldbeginner 发表于 2013-12-8 16:05
执行功能码();
for(i = 0; i < MB_FUNC_HANDLERS_MAX; i++ ){ //执行功能码 ...
调度器,感觉好理解。
http://www.amobbs.com/thread-3757688-1-1.html
直接摘录
所有的C函数都可以看作是状态机,哪怕是一个最简单的函数,也可以看成是一个只有一个状态的
状态机。因此,最简单的状态机原型可以是大家常用的:
void FSMExample(void);
为了方便控制状态机的启动和关闭,我修改了最基本的原型:
BOOL FSMExample(void);
这就是最简单的双态状态机:
当函数返回TRUE,表明该状态机仍然“希望”处于运行状态;
当函数返回FALSE,表明该状态机已经完成,希望终止。
根据这一规定,如果通过函数指针把所有的状态机连接起来,就可以形成下面的简单调度器:
typedef struct Process
{
BOOL (*Proc)(void); //返回False,自动关闭任务
volatile BOOL IfProcAlive;
}PROCESS;
typedef BOOL (*PROC_FUNCTION)(void);
void Process_Task(void)
{
static uint8 n = 0;
if (ProcPCB.IfProcAlive) //处理进程
{
ProcPCB.IfProcAlive = (*ProcPCB.Proc)();
}
n ++;
if (n >= g_cCOSPROCCounter)
{
n = 0;
//g_cScheduleTest = MIN(g_cScheduleTest + 1,254);
}
}
这个调度器很简单,就是检测一个注册了的顶层状态机其Alive状态是否为TRUE,
如果为TRUE,就调用;同时在调用后将状态机的返回值重新赋给ALive属性,这样
状态机就可以通过在函数中返回TRUE或者FALSE来控制自己的运行状态。 oldbeginner 发表于 2013-12-9 15:55
复习发送报文的过程
进行了很多尝试,终于理解FREEMODBUS大概思路。
理解freemodbus后,虽然只是刚学习单片机几个月,但感觉freemodbus滥用状态机(比如最关键状态是完整的帧收到了,然后再开始处理数据并反馈主机,顺序执行好好的,非要假设主机在从机发送的时候又送来命令;这种处理应该放在主机上,而不是从机的职责),把整体结构搞得复杂难懂,明明不是操作系统,非得往上靠。
从机就应该把重点放在从机的职责上(接收命令执行,然后反馈),而不是老想着主机发命令超过我的处理能力怎么办,这种想一劳永逸的从机不仅让自己的结构复杂了好多倍,导致犯错的机会也增加好多倍,本来想节约时间的通用程序反而成了麻烦制造机。
对Freemodbus的反思:
1、有很多人移植,而且热烈讨论,并没有怀疑它本身定位的问题,让我一开始误认为FREEMODBUS是一贴灵药。
2、Freemodbus的定位问题,它适合谁使用?适合哪种情况下使用?目前根本找不到相关信息。我的理解是,Freemodbus定位不准确,完全是按照主机的方式在处理。
3、很多不必要的信息,增加了复杂度。比如Errorcode问题,因为modbus协议很成熟,从机不厌其烦地判断是地址错误还是什么类型错误的?都有MBPoll这类软件帮助调试,从机完全没有必要增加这类信息。
4、最关键的状态就是字符串接收完毕,这点在程序中并没有体现,反而搞出了很多状态和它并列,而其它状态根本不是从机应该关注的,这些状态要用主机来解决。
5、这种大而全,有点向操作系统靠拢,不适合具体的实际应用。
free modbus 划分的文件比较多 目的是为了支持各种传输方式 用了接口的概念
在51里面使用的确比较蛋疼
可以把不相关的功能都去掉 整合起来看 实时性很不错的 楼主威武{:biggrin:} 顶一下~谢谢 学习了! 好东西,学习了! 支持一下啊,感觉高大上啊
页:
[1]