1. 程式人生 > >[原創] 我也來講ModBus移植,基於飛思卡爾 K60,中斷接收,中斷髮送

[原創] 我也來講ModBus移植,基於飛思卡爾 K60,中斷接收,中斷髮送

題外話,用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中斷接收和中斷髮送存在誤解,也希望路過的小夥伴及時指出