STM32FXX MODBUS移植心得

先吐槽一下:网络是个神奇的东西,可以找到许多学习资料,但是得花大量的时间去辨别真伪。
最近开发一个小玩意,需要用到MODBUS主从结构,一个兄弟强烈推荐freeModbus栈,说是十几分钟就能搞定,于是我开始查看各种网上的文章,很少有能说清楚怎么回事的,没办法,只能啃源码,前后共花了两天半才搞明白怎么回事。原以为很简单的事,硬生生搞得筋疲力尽,这本不是协议栈的有问题,而是网络上太多的文章误导,胡乱截一段代码,就说这个简单,那个容易。。。不知道这些人到底是抱着什么样心态在做技术。在此引以为戒,做技术还是得脚踏实地,实事求是。
好了,废话不多说了,记录下思路。

背景:STM32F429,USART1实现收发,RS485,从机RTU。MODBUS协议栈,1.6版本。

1、MODBUS从机响应过程分析:
移植过程就不细讲了,协议栈中自带的例子非常详细。
主从MODBUS,RS485通信,无非就是实现一个串口收发的功能。栈使用了中断收发。
首先,从接收中断开始分析,每接收到一个字符,触发接收中断,在接收中断中调用栈的prvvUARTRxISR()函数。
prvvUARTRxISR()函数中调用pxMBFrameCBByteReceived()函数实现多态。因为栈本身需要处理多平台、多模式的兼容性,此处使用了指针,实现多平台、多模式的处理函数调用接口,形式如下:

/* Functions pointer which are initialized in eMBInit( ). Depending on the
 * mode (RTU or ASCII) the are set to the correct implementations.
 */
static peMBFrameSend peMBFrameSendCur;
static pvMBFrameStart pvMBFrameStartCur;
static pvMBFrameStop pvMBFrameStopCur;
static peMBFrameReceive peMBFrameReceiveCur;
static pvMBFrameClose pvMBFrameCloseCur;

不同模式给函数指针赋相应的处理函数,形式如下:

#if MB_RTU_ENABLED > 0
        case MB_RTU:
            pvMBFrameStartCur = eMBRTUStart;
            pvMBFrameStopCur = eMBRTUStop;
            peMBFrameSendCur = eMBRTUSend;
            peMBFrameReceiveCur = eMBRTUReceive;
            pvMBFrameCloseCur = MB_PORT_HAS_CLOSE ? vMBPortClose : NULL;
            pxMBFrameCBByteReceived = xMBRTUReceiveFSM;
            pxMBFrameCBTransmitterEmpty = xMBRTUTransmitFSM;
            pxMBPortCBTimerExpired = xMBRTUTimerT35Expired;

            eStatus = eMBRTUInit( ucMBAddress, ucPort, ulBaudRate, eParity );
            break;
#endif

接下来,通过初始化时给定的指针进行函数调用处理,我这里使用RTU,实际调用的函数为xMBRTUReceiveFSM(),在这个函数中会处理接收到的字符,通过需要补全的函数xMBPortSerialGetByte()实现数据接收(根据自己平台实现的数据接收函数)。
接收到的数据会存放到ucRTUBuf[MB_SER_PDU_SIZE_MAX]这个数组中。接收过程中,每接收一个数据就会刷新一次超时定时器。到这里,接收数据的事就结束了。
那何时处理接收到的数据呢?在数据帧结束时处理。数据帧何时结束?接收字符长度间隔超过3.5个字符时间认为一帧结束。如何知道字符间隔?通过定时器来实现超时判定,两字符之间若超过3.5个字符时间,则触发定时中断,若未超过,前面讲过,新收到字符数据时,会刷新定时器。

OK,现在来看看定时器超时处理过程,通过初始化时的指针赋值,找到xMBRTUTimerT35Expired()函数为超时处理函数。这个函数负责状态切换:

BOOL
xMBRTUTimerT35Expired( void )
{
    BOOL            xNeedPoll = FALSE;

    switch ( eRcvState )
    {
        /* Timer t35 expired. Startup phase is finished. */
    case STATE_RX_INIT:
        xNeedPoll = xMBPortEventPost( EV_READY );
        break;

        /* A frame was received and t35 expired. Notify the listener that
         * a new frame was received. */
    case STATE_RX_RCV:
        xNeedPoll = xMBPortEventPost( EV_FRAME_RECEIVED );
        break;

        /* An error occured while receiving the frame. */
    case STATE_RX_ERROR:
        break;

        /* Function called in an illegal state. */
    default:
        assert( ( eRcvState == STATE_RX_INIT ) ||
                ( eRcvState == STATE_RX_RCV ) || ( eRcvState == STATE_RX_ERROR ) );
    }

    vMBPortTimersDisable(  );
    eRcvState = STATE_RX_IDLE;

    return xNeedPoll;
}

接收完成时,最终会切换到 EV_FRAME_RECEIVED 状态,此时,eMBPoll()轮循会通过peMBFrameReceiveCur指针调用帧接收处理函数eMBRTUReceive(),在eMBRTUReceive()中校验设备地址,CRC是否合法,若通过校验,则切换到执行阶段EV_EXECUTE,根据帧中指令代码,执行相应的动作。如读输入寄存器,则会通过指针调用eMBFuncReadInputRegister(),eMBFuncReadInputRegister()中又调用eMBRegInputCB(),eMBRegInputCB()为我们要实现的具体函数,其中需要实现输入寄存器返回的具体值。

到此时,只是解析了接收到的命令帧,并执行了相应的动作或需要返回的数据准备工作,还需要组帧,返回响应帧。
响应接收帧的组帧工作由peMBFrameSendCur指向的函数执行,实际指向eMBRTUSend(),在eMBRTUSend()中组帧完成后,由xMBRTUTransmitFSM()函数进行实际发送工作,实际发送数据时,会通过xMBPortSerialPutByte()依次发送字符,直到全部字符发送完成。
至此,从机RTU模式响应一条请求完成。
2、异常处理,因比较简单,此处不详细分析了。
3、注意事项:
a) 移植时,要注意发送中断的使用。
在这里插入图片描述

void USART1_IRQHandler(void)
{	
    if(USART_GetITStatus(USART1,USART_IT_RXNE)!=RESET)
    {
        prvvUARTRxISR();
	}

	if(USART_GetITStatus(USART1,USART_IT_TC)!=RESET)
    {
        prvvUARTTxReadyISR();
		USART1->SR &= ~(1<<6); 		//注意发送中断清零方式,有两种,一种是读SR写DR,一种是直接写0
    }
}

b) 此协议栈中由于是利用大循环处理单个字符,所以在速度上会有些慢(只是相对慢),这在低速MCU应用中要尤其注意通讯速率不宜太高。
c) 此协议栈由于使用中断处理实际收发数据过程,所以在中断使用上要理解中断清零的方式,接收中断只要读DR就会清零,发送中断有两种选择,一是TC,一是TXE,这两个都可以使用,TXE若是合理利用,可以简化发送过程。
d) 关于接收缓冲的问题:看到一些网友说需要接收缓冲,否则容易丢数据,我实测没有发现这个问题。个人分析,可能是通信速率与实际MCU主频配合不合理造成。
e) 关于临界状态的宏应用:不使用OS的临界函数,可以下列方式实现,并无本质区别。即:
一种宏:

#define ENTER_CRITICAL_SECTION() INTX_DISABLE(); //关闭中断
#define EXIT_CRITICAL_SECTION()  INTX_ENABLE(); //开启中断

另一种宏:

#define ENTER_CRITICAL_SECTION() __set_PRIMASK(1); //关闭中断
#define EXIT_CRITICAL_SECTION()  __set_PRIMASK(0); //开启中断

另一种为函数实现:

void ENTER_CRITICAL_SECTION(void);
void EXIT_CRITICAL_SECTION(void);

若使用OS,可以将宏分别设置为进入退出临界状态函数。
f) 关于MODBUS本身,这只是一种通信方式,约定好了一些操作码,有固定的组帧方式,而且也不算复杂,所以自行实现可能速度会更快些,若是对自己的技术没有信心,用这种栈的方式实现也并无问题,但需要注意,拿来主义固然方便,要想提升还需要消化吸收,理解整个实现思路。千万不能以讹传讹,盲目跟风。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值