[原创] 我也来讲ModBus移植,基于飞思卡尔 K60,中断接收,中断发送

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/ysgjiangsu/article/details/81512310

题外话,用K60,其实我是抵触的,哪有STM32用的舒服,客户就要汽车级MCU,那就上吧,就是多花点时间呗。移植下来,收获还很多,记录下来,或许将来有小伙伴用得上:

在移植MB之前,先理一理MB的实现机理:

首先是三个函数:
1. eMBInit() eMBEnable() 和 eMBPoll()

我倒过来讲:
首先 eMBPoll()是在while(1)里的,本质上是一个状态机
eEvent包含三种状态:
EV_READY(什么也没干)

EV_FRAME_RECEIVED(看看CRC对不对以及传过来的地址,跟我本身的地址是不是一致,一致就跳下一个状态,不一致就break)
EV_EXECUTE(已经确认就是我的消息,赶紧执行吧)

EV_FRAME_SENT(什么也没干)

着重看一下EV_EXECUTE

for( i = 0; i < MB_FUNC_HANDLERS_MAX; i++ )
{
    /* No more function handlers registered. Abort. */
    if( xFuncHandlers[i].ucFunctionCode == 0 )
    {
        break;
    }
    else if( xFuncHandlers[i].ucFunctionCode == ucFunctionCode )
    {
        eException = xFuncHandlers[i].pxHandler( ucMBFrame, &usLength );
        break;
    }
}

通过for循环来找功能码在不在本地执行表中,本地执行表是一个结构体数组,可以通俗理解成字典,一个码对应一个函数指针,比如:(mb.c)

static xMBFunctionHandler xFuncHandlers[MB_FUNC_HANDLERS_MAX] = {
  {MB_FUNC_READ_HOLDING_REGISTER,eMBVendorRead},        //0x03
  {MB_FUNC_WRITE_MULTIPLE_REGISTERS,eMBVendorWrite},    //0x06
  {MB_VENDOR_READ,eMBVendorRead},                       //0x21
  {MB_VENDOR_WRITE,eMBVendorFWrite},                    //0x22
  {MB_VENDOR_IAP,eMBVendorIap},                         //0x30
#if MB_FUNC_OTHER_REP_SLAVEID_ENABLED > 0
  {MB_FUNC_OTHER_REPORT_SLAVEID, eMBFuncReportSlaveID}, //0x17
#endif
};

结构体定义如下:(mbproto.h)

typedef struct
{
    UCHAR           ucFunctionCode;
    pxMBFunctionHandler pxHandler;
} xMBFunctionHandler;

typedef         eMBException( *pxMBFunctionHandler ) ( UCHAR * pucFrame, USHORT * pusLength );

由定义可知,pxMBFunctionHandler 是一个函数指针,参数1 为数据指针,参数2为数据长度指针,返回值为eMBException(这是一个枚举类型)
可见,本质上是通过功能码调用函数指针指向的函数
函数执行过程中,需要对传输的参数1,按MB帧格式要求进行改造,比如

uLen = pucFrame[4] << 1;
pucFrame[1] = uLen;
for(i = 0;i < uLen;i++)
{
    pucFrame[i + 2] = testData++;//测试使用,实际根据情况修改
}
*usLen = 2 + ulen;

数据也更新了,下面该把数据报回去了,执行下一条语句

 eStatus = peMBFrameSendCur( ucMBAddress, ucMBFrame, usLength );

peMBFrameSendCur是一个函数指针,在eMBInit()的时候被赋值了eMBRTUSend,

eMBErrorCode
eMBRTUSend( UCHAR ucSlaveAddress, const UCHAR * pucFrame, USHORT usLength )
{
    eMBErrorCode    eStatus = MB_ENOERR;
    USHORT          usCRC16;

    ENTER_CRITICAL_SECTION(  );

    /* Check if the receiver is still in idle state. If not we where to
     * 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 = ( UCHAR * ) 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 CRC16 checksum for Modbus-Serial-Line-PDU. */
        usCRC16 = usMBCRC16( ( UCHAR * ) pucSndBufferCur, usSndBufferCount );
        ucRTUBuf[usSndBufferCount++] = ( UCHAR )( usCRC16 & 0xFF );
        ucRTUBuf[usSndBufferCount++] = ( UCHAR )( usCRC16 >> 8 );

        /* Activate the transmitter. */
        eSndState = STATE_TX_XMIT;
        vMBPortSerialEnable( FALSE, TRUE );
    }
    else
    {
        eStatus = MB_EIO;
    }
    EXIT_CRITICAL_SECTION(  );
    return eStatus;
}

总结下来,就是把数据头也就是自身的地址加上,算上头 算上功能码 算上数据 计算一下CRC放在最后两个字节,然后开启发送中断,就完事了。发送中断自动地完成以上数据的发送,这里也体现了一个问题:发送缓冲区就是在接收缓冲区的基础上改动得到的,内存位置一样。好eMBPoll()结束

下面开始讲剩下的两个函数eMBInit() eMBEnable()
eMBInit()
初始化了一堆函数指针,这里不细表,然后调用了eMBRTUInit完成对硬件串口的初始化,包括本地地址,串口号,波特率,奇偶校验四个参数。
eMBEnable()
开启协议栈,->开启接收中断,关闭发送中断,开启定时器中断
以下是接收状态机函数

BOOL
xMBRTUReceiveFSM( void )
{
    BOOL            xTaskNeedSwitch = FALSE;
    UCHAR           ucByte;

    assert( eSndState == STATE_TX_IDLE );

    /* Always read the character. */
    ( void )xMBPortSerialGetByte( ( CHAR * ) & ucByte );

    switch ( eRcvState )
    {
        /* If we have received a character in the init state we have to
         * wait until the frame is finished.
         */
    case STATE_RX_INIT:
        vMBPortTimersEnable(  );
        break;

        /* In the error state we wait until all characters in the
         * damaged frame are transmitted.
         */
    case STATE_RX_ERROR:
        vMBPortTimersEnable(  );
        break;

        /* In the idle state we wait for a new character. If a character
         * is received the t1.5 and t3.5 timers are started and the
         * receiver is in the state STATE_RX_RECEIVCE.
         */
    case STATE_RX_IDLE:
        usRcvBufferPos = 0;
        ucRTUBuf[usRcvBufferPos++] = ucByte;
        eRcvState = STATE_RX_RCV;

        /* Enable t3.5 timers. */
        vMBPortTimersEnable(  );
        break;

        /* We are currently receiving a frame. Reset the timer after
         * every character received. If more than the maximum possible
         * number of bytes in a modbus frame is received the frame is
         * ignored.
         */
    case STATE_RX_RCV:
        if( usRcvBufferPos < MB_SER_PDU_SIZE_MAX )
        {
            ucRTUBuf[usRcvBufferPos++] = ucByte;
        }
        else
        {
            eRcvState = STATE_RX_ERROR;
        }
        vMBPortTimersEnable(  );
        break;
    }
    return xTaskNeedSwitch;
}

接收状态机,无论何种状态,只要离开状态机就开启定时检查,超时(超时时间有讲究,MB技术要求是3.5T,即3.5倍的帧时长)就算通信终止。
然后告知EV_FRAME_RECEIVED,当然这仅是在STATE_RX_RCV状态下其他状态,直接算废帧了,重新接收。
eMBPoll()收到EV_FRAME_RECEIVED就开始了前面各种操作,就不赘述了。
最后讲一下发送状态机:

BOOL
xMBRTUTransmitFSM( void )
{
    BOOL            xNeedPoll = FALSE;

    assert( eRcvState == STATE_RX_IDLE );

    switch ( eSndState )
    {
        /* 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;

    case STATE_TX_XMIT:
        /* check if we are finished. */
        if( usSndBufferCount != 0 )
        {
            xMBPortSerialPutByte( ( CHAR )*pucSndBufferCur );
            pucSndBufferCur++;  /* next byte in sendbuffer. */
            usSndBufferCount--;
        }
        else
        {
            xNeedPoll = xMBPortEventPost( EV_FRAME_SENT );
            /* Disable transmitter. This prevents another transmit buffer
             * empty interrupt. */
            vMBPortSerialEnable( TRUE, FALSE );
            eSndState = STATE_TX_IDLE;
        }
        break;
    }

    return xNeedPoll;
}

中断发送,直到发送完毕,自己把发送中断关了,待在STATE_TX_IDLE状态。
至此,MB源码讲解完毕。

/**********************************************************************************************/
针对K60系统移植过程:
1. 配置uart,我用的是UART4,核心代码如下:
奇校验、使能发送中断和接收中断,数据位设置为9位(是因为加入了奇偶校验导致),1位停止位,也就是说1bit start + 8bit data + 1bit parity + 1 bit stop,共计11bit(这是MB协议要求的,若无parity,停止位应该是2位)

//配置成9位奇校验模式
UART_C1_REG(UARTN[MB_RXTX_PORT]) |= (0
                                  | UART_C1_M_MASK                    
                                  | UART_C1_PE_MASK                  
                                  | UART_C1_PT_MASK                   
                                 );

2. 另建MBK60Set源文件(想通过这个文件将MB移植与平台无关)

void ABT_MBUartRXIRQEN(void)
{    
    gpio_set(MB_DIR_PORT, MB_DIR_RX);

    uart_rx_irq_en(MB_RXTX_PORT);//recv intr
}

void ABT_MBUartRXIRQDIS(void)
{
    gpio_set(MB_DIR_PORT, MB_DIR_TX);

    uart_rx_irq_dis(MB_RXTX_PORT);//禁止recv intr
}

void ABT_MBUartTXIRQEN(void)
{
    gpio_set(MB_DIR_PORT, MB_DIR_TX);

    uart_tx_irq_en(MB_RXTX_PORT);//tx intr
}

void ABT_MBUartTXIRQDIS(void)
{
    gpio_set(MB_DIR_PORT, MB_DIR_RX);

    uart_tx_irq_dis(MB_RXTX_PORT);//禁止tx intr
}

就是控制一下irq使能与SP3485传输方向
3. 配置Timer,也在MBK60Set

void ABT_MBPitExpired_handler(void)
{   
    prvvTIMERExpiredISR();//-> pxMBPortCBTimerExpired(); == xMBRTUTimerT35Expired();
    led_turn(LED0);

    PIT_Flag_Clear(PIT0);
}

void ABT_MBTimer35TInit(uint32 timeOut)
{
    //pit_init_ms(PIT0,timeOut);
    pit_init_ms(PIT0,2);

    set_vector_handler(PIT0_VECTORn,ABT_MBPitExpired_handler);    
    set_irq_priority(PIT0_IRQn,1);

    enable_irq(PIT0_IRQn);
    printf("timer is ok\n");
}

void ABT_MBTimerEnable(void)
{   
    PIT_Flag_Clear(PIT0);                           //清中断标志位

    PIT_TCTRL(PIT0) &= ~ PIT_TCTRL_TEN_MASK;        //禁止PITn定时器(用于清空计数值)
    PIT_TCTRL(PIT0)  = ( 0
                         | PIT_TCTRL_TEN_MASK        //使能 PITn定时器
                         | PIT_TCTRL_TIE_MASK        //开PITn中断
                       );

    enable_irq(PIT0_IRQn);  
}

void ABT_MBTimerDisable(void)
{
    PIT_Flag_Clear(PIT0);                           //清中断标志位

    PIT_TCTRL(PIT0) &= ~ PIT_TCTRL_TEN_MASK;        //禁止PITn定时器(用于清空计数值)
    PIT_TCTRL(PIT0)  = ( 0
                         | PIT_TCTRL_TEN_MASK        //使能 PITn定时器
                         | PIT_TCTRL_TIE_MASK        //开PITn中断
                       );

    disable_irq(PIT0_IRQn);    
}

有价值的问题

0: MB通过反复使能Timer来重启定时,在K60中,可不能只是简单地enable一下timer中断就完事,一定要清计数值
(MB的本意也是如此,只不过命名不是清计数值)

1: K60中断接收一串16进制数,总是漏掉第一个字节
比如 发送 23 03 00 60 00 02 C2 97
收到 03 00 60 00 02 C2 97
单步看可以完整接收,跑起来就丢,后来发现在STATE_RX_IDLE,会重复进入两次
(测试方法:放一个全局变量在STATE_RX_IDLE中由0自加,竟然发现是2,正常情况执行一次就跳转,应该是1啊)
因此,我对xMBRTUReceiveFSM做如下改动,可以正常接收

case STATE_RX_IDLE:
    if(idleFlag == 0)
    { 
        usRcvBufferPos = 0;
        ucRTUBuf[usRcvBufferPos++] = rxData;
        idleFlag = 1;
    }
    else
    {
        ucRTUBuf[usRcvBufferPos++] = rxData;
    }
    eRcvState = STATE_RX_RCV;
    /* Enable t3.5 timers. */
    vMBPortTimersEnable(  );
        break;

2: K60中断发送一串16进制数,总是漏掉最后一个字节
比如上报 23 03 04 11 12 13 14 7D C7
终端只收到 23 03 04 11 12 13 14 7D
断点看缓冲区,明明最后一个字节是C7,却没有发送
因此我对xMBRTUTransmitFSM做如下改动,发送正常

case STATE_TX_XMIT:
        /* check if we are finished. */
        if( usSndBufferCount != 0 )
        {
            xMBPortSerialPutByte( ( CHAR )*pucSndBufferCur );
            pucSndBufferCur++;  /* next byte in sendbuffer. */
            usSndBufferCount--;
            flag = 0;
        }
        else
        {
            if(flag == 0)
            {
                flag = 1;
                xMBPortSerialPutByte( ( CHAR )*pucSndBufferCur );
            }
            else
            {
                xNeedPoll = xMBPortEventPost( EV_FRAME_SENT );
                /* Disable transmitter. This prevents another transmit buffer
                 * empty interrupt. */

                vMBPortSerialEnable( TRUE, FALSE );
                eSndState = STATE_TX_IDLE;
            }
        }
        break;

下图是测试了30000多次的截图,
这里写图片描述

最后的话,修改MB官方源码实属无奈,以后用STM32移植MB,应该不会这么xxx了

当然,或许我对K60中断接收和中断发送存在误解,也希望路过的小伙伴及时指出

猜你喜欢

转载自blog.csdn.net/ysgjiangsu/article/details/81512310
今日推荐