標(biāo)題: 單片機(jī)軟件擴(kuò)展的多串口數(shù)據(jù)轉(zhuǎn)發(fā)模型 [打印本頁]

作者: 牛在飛    時間: 2021-10-10 17:47
標(biāo)題: 單片機(jī)軟件擴(kuò)展的多串口數(shù)據(jù)轉(zhuǎn)發(fā)模型
實用型軟件架構(gòu)
==============
多串口數(shù)據(jù)交互模型
-------------------

實際需求
~~~~~~~~
最近在做一個西門子 ``Step 200`` 系列的PLC通訊口擴(kuò)展項目時,遇到了這樣的問題:
``224XP`` ,這個CPU的外部通訊端口只用兩個,在物聯(lián)網(wǎng)大火的當(dāng)下,這樣的擴(kuò)展口數(shù)量,在加入聯(lián)網(wǎng)模塊后,顯然無法滿足更多的
聯(lián)網(wǎng)需求。當(dāng)前實際需求如下:

.. csv-table:: **通訊口對應(yīng)功能**
   :header: "編號", "功能"
   :widths: 5, 10
   :align: center

   01, "PLC串口屏通訊"
   02, "EBM風(fēng)扇通訊"
   03, "4G/WIFI模塊通訊"
   04, "以太網(wǎng)通訊"

在考慮到成本與技術(shù)可行性前提下,盡可能保留產(chǎn)品研發(fā)核心技術(shù)手段,選用STC8系列單片機(jī)對PLC原有的兩個通訊口
利用串口進(jìn)行擴(kuò)展。設(shè)計思路如下:

.. figure:: mode1.png
    :align: center
    :alt: NULL
    :scale: 70%

    圖 4.1 理論中多串口數(shù)據(jù)交互模型

從圖中可以看出,數(shù)據(jù)信息的主要請求目標(biāo)主要是通過 ``PLC_PORT0`` 獲得PLC內(nèi)部存儲區(qū)數(shù)據(jù)( ``PLC_PORT1`` 默認(rèn)用于連接屏幕)。
因此,進(jìn)行軟件拓展的目標(biāo)物理鏈路就是 ``PLC_PORT0`` 。

矛盾的產(chǎn)生
~~~~~~~~~~

從上面的模型可以看出,當(dāng)前工作模式應(yīng)該是一個多主單從結(jié)構(gòu)。那么按照常理應(yīng)該是由STC8的4個串口通過輪詢的方式對共享設(shè)備PLC
目標(biāo)地址發(fā)出數(shù)據(jù)請求的命令,隨后由PLC把響應(yīng)數(shù)據(jù)返回給當(dāng)前請求對象。如果嚴(yán)格遵循這樣的工作模式,不會存在任何問題。
但是,實際的架構(gòu)設(shè)計需求如下:

.. figure:: mode2.png
    :align: center
    :alt: NULL
    :scale: 70%

    圖 4.2 實際的多串口數(shù)據(jù)交互模型

.. attention::
    其中每個通訊端口上端的標(biāo)號都代表在實際的通訊過程中,STC8單片機(jī)作為擴(kuò)展主機(jī)時輪詢框架下的調(diào)度關(guān)系(數(shù)字越小,優(yōu)先級越高;數(shù)字相等,代表處于同一優(yōu)先級)。

這里實際使用的時候是通過 ``PLC_PORT0`` 與 ``STC8_UART4`` 進(jìn)行物理上的連接,在通過STC8內(nèi)部軟件協(xié)議通過其他串口與拓展設(shè)備
進(jìn)行數(shù)據(jù)交互。很顯然當(dāng)前的架構(gòu)無法滿足這樣的實際需求,矛盾就應(yīng)運而生了。

.. note::
    既然多主機(jī),單從機(jī)的通訊模型無法在PLC作為主機(jī)時滿足需求,那么就可以重新考慮另外一種工作模式。為了適應(yīng)更多可能的情況,
    建立一種不分主從結(jié)構(gòu)的工作模式,在多對象數(shù)據(jù)交互的基礎(chǔ)上建立一種相對是一對一的通訊機(jī)制。

.. figure:: mode3.png
    :align: center
    :alt: NULL
    :scale: 70%

    圖 4.2 改進(jìn)后的多串口數(shù)據(jù)交互模型

軟件設(shè)計思想
~~~~~~~~~~~~~~

.. figure:: F0.png
    :align: center
    :alt: NULL
    :scale: 70%

    圖 4.3 基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)

.. note::
    從圖中可以看出,最上層采用的是循環(huán)隊列,每個隊列的元素由一條鏈表進(jìn)行連接,每條鏈表的一個節(jié)點代表一幀數(shù)據(jù)。

.. csv-table:: **單節(jié)點上成員描述**
   :header: "標(biāo)識符", "意義"
   :widths: 8, 20
   :align: center

   "Frame_Flag", "幀標(biāo)志:由定時器幀中斷機(jī)制置為true;輪詢轉(zhuǎn)發(fā)程序轉(zhuǎn)發(fā)當(dāng)前幀后置為false"
   "Timer_Flag", "幀中斷定時器開啟標(biāo)志:當(dāng)任意串口接收中斷收到一個字節(jié)數(shù)據(jù)時設(shè)置為true;超時后設(shè)置false"
   "Rx_Buffer", "數(shù)據(jù)幀接收緩沖區(qū)"
   "Rx_Length", "當(dāng)前數(shù)據(jù)幀長度"
   "OverTime", "幀判定時間:該變量在串口中斷有字節(jié)數(shù)據(jù)接收時會不斷刷新;在幀仲裁定時器中其值不斷減小至0"

**詳細(xì)工作原理:**
以PLC通過485總線發(fā)送數(shù)據(jù)為例,假設(shè)PLC當(dāng)前要像EBM請求某一個狀態(tài)值,發(fā)出一幀數(shù)據(jù) ``15 21 01 CA``,此時EBM響應(yīng)數(shù)據(jù)為 ``35 01 01 00 CA`` ,則:

1、串口四接收中斷收到PLC發(fā)出的第一個字節(jié),打開幀中斷定時器,判斷當(dāng)前寫指針?biāo)鶎?yīng)的鏈表節(jié)點幀標(biāo)志是否為false,條件成立后判斷當(dāng)前節(jié)點幀長度是否溢出,
如果沒有就刷新當(dāng)前幀鏈表塊中 ``OverTime`` , 最后把當(dāng)前字節(jié) ``15`` 存到當(dāng)前幀緩沖區(qū) ``Rx_Buffer`` 的位置上。

2、后續(xù)字符 ``21 01 CA`` 的接收操作與第一個字符一致,其中每個字節(jié)間間隔由通訊的波特率決定,*<<Timer(OverTime)* ,當(dāng)接收完這一幀數(shù)據(jù)后,``OverTime``
值將不會在串口接收中斷中被刷新,而是由幀中斷定時器中不斷減小為0,最終標(biāo)志該節(jié)點上這幀數(shù)據(jù)接收完成,并把對應(yīng)的 ``Frame_Flag``
置為true。

3、在主程序輪詢機(jī)制中,一旦檢測到有 ``Frame_Flag`` 產(chǎn)生,則利用讀指針訪問當(dāng)前節(jié)點幀緩沖區(qū),對目標(biāo)設(shè)備發(fā)出請求命令。

4、響應(yīng)數(shù)據(jù)返回給目標(biāo)對象的工作過程與前三個步驟完全一致。值得注意的是,入果存在對個數(shù)據(jù)交換序列(:menuselection:`PLC_PORT0-->UART4-->UART3` 和 :menuselection:`UART2-->UART4-->PLC_PORT0` ,
存在相反的公共序列 :menuselection:`PLC_PORT0-->UART4` , :menuselection:`UART4-->PLC_PORT0`),此時如果公用的是同一個緩沖區(qū),且不對不同類型的數(shù)據(jù)進(jìn)行分流,將會造成不同請求對象數(shù)據(jù)響應(yīng)錯誤,
所以必須加以條件限制。

建立數(shù)據(jù)結(jié)構(gòu)
~~~~~~~~~~~~~~

.. code-block:: c
   :caption: 1.0.0 基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)
   :linenos:
   :emphasize-lines: 3,5

    /*鏈隊數(shù)據(jù)結(jié)構(gòu)*/
    typedef struct
    {
    uint8_t Frame_Flag;             /*幀標(biāo)志*/
        uint8_t Timer_Flag;             /*打開定時器標(biāo)志*/
        uint8_t Rx_Buffer[MAX_SIZE];    /*數(shù)據(jù)接收緩沖區(qū)*/
        uint16_t Rx_Length;             /*數(shù)據(jù)接收長度*/
        uint16_t OverTime;              /*目標(biāo)設(shè)備響應(yīng)超時時間*/
    }Uart_Queu;

    typedef struct
    {
    Uart_Queu LNode[MAX_NODE];
        /*存儲R ,W指針,表示一個隊列*/
        uint8_t Wptr;
        uint8_t Rptr;
    }Uart_List;

    /*聲明鏈隊*/
    extern Uart_List Uart_LinkList[MAX_LQUEUE];

.. note::
    頂層數(shù)據(jù)結(jié)構(gòu)采用環(huán)形隊列,只不過隊列中的單個元素并不是一個單一的值,而是一個帶有記錄信息的數(shù)據(jù)塊 ``Uart_Queu`` 。
    這樣做的目的在于,使用的單片機(jī)是C51,其本身的串口是不帶有空閑中斷或者DMA這些高級硬件的,那這就需要我們通過軟件算法模擬這一些硬件功能
    來完成功能設(shè)計。

.. code-block:: c
   :caption: 1.0.1 改進(jìn)后基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)
   :linenos:
   :emphasize-lines: 3,5
        
    /*鏈隊數(shù)據(jù)結(jié)構(gòu)*/
    typedef struct
    {
    uint8_t Frame_Flag;             /*幀標(biāo)志*/
        uint8_t Timer_Flag;             /*打開定時器標(biāo)志*/
        uint8_t Rx_Buffer[MAX_SIZE];    /*數(shù)據(jù)接收緩沖區(qū)*/
        uint16_t Rx_Length;             /*數(shù)據(jù)接收長度*/
        uint16_t OverTime;              /*目標(biāo)設(shè)備響應(yīng)超時時間*/
        Uart_Queu *Next;                /*指向下一個節(jié)點*/
    }Uart_Queu;

.. note::
    主要改進(jìn)了隊列下數(shù)據(jù)塊元素的內(nèi)存分配方式,由原來的靜態(tài)的分配,改為程序運行過程根據(jù)實際需求來分配?紤] ``Malloc`` 函數(shù)在
    51編譯器中安全性和適用性,實際使用過程建議非必要情況采用靜態(tài)內(nèi)存分配方式。當(dāng)然,采用動態(tài)內(nèi)存分配方式,使用循環(huán)鏈表將會帶來更多的
    可操作性、靈活性和內(nèi)存節(jié)約。

.. code-block:: c
   :caption: 1.0.2 串口幀中斷機(jī)制設(shè)計
   :linenos:
   :emphasize-lines: 3,5

   /**
    * @brief    定時器0的中斷服務(wù)函數(shù)
    * @details  
    * @param    None
    * @retval   None
    */
    void Timer0_ISR() interrupt 1
    {

        if(COM_UART1.LNode[COM_UART1.Wptr].Timer_Flag)
            /*以太網(wǎng)串口接收字符間隔超時處理*/
            SET_FRAME(COM_UART1);
        if(COM_UART2.LNode[COM_UART2.Wptr].Timer_Flag)
            /*4G/WiFi串口接收字符間隔超時處理*/
            SET_FRAME(COM_UART2);
        if(COM_UART3.LNode[COM_UART3.Wptr].Timer_Flag)
            /*RS485串口接收字符間隔超時處理*/
            SET_FRAME(COM_UART3);
        if(COM_UART4.LNode[COM_UART4.Wptr].Timer_Flag)
            /*PLC串口接收字符間隔超時處理*/
            SET_FRAME(COM_UART4);
    }

    /**
    * @brief    串口4中斷函數(shù)
    * @details  使用的是定時器4作為波特率發(fā)生器,PLC口用
    * @param    None
    * @retval   None
    */
    void Uart4_Isr() interrupt 18
    {   /*發(fā)送中斷*/
        if (S4CON & S4TI)
        {
            S4CON &= ~S4TI;
            /*發(fā)送完成,清除占用*/
            Uart4.Uartx_busy = false;
        }
        /*接收中斷*/
        if (S4CON & S4RI)
        {
            S4CON &= ~S4RI;

            /*當(dāng)收到數(shù)據(jù)時打開幀中斷定時器*/
            COM_UART4.LNode[COM_UART4.Wptr].Timer_Flag = true;
            /*當(dāng)前節(jié)點還沒有收到一幀數(shù)據(jù)*/
            if (!COM_UART4.LNode[COM_UART4.Wptr].Frame_Flag)
            {
                /*刷新幀超時時間*/
                COM_UART4.LNode[COM_UART4.Wptr].OverTime = MAX_SILENCE;
                if (COM_UART4.LNode[COM_UART4.Wptr].Rx_Length < MAX_SIZE)
                { /*把數(shù)據(jù)存到當(dāng)前節(jié)點的緩沖區(qū)*/
                    COM_UART4.LNode[COM_UART4.Wptr].Rx_Buffer[COM_UART4.LNode[COM_UART4.Wptr].Rx_Length++] = S4BUF;
                }
            }
        }
    }

.. note::
    因為硬件定時器數(shù)量有限,所以幾個串口的幀中斷機(jī)定時器均采用了 ``Timer0`` 進(jìn)行仲裁,可能會存在中斷延時的問題,在硬件定時器資源充足情況下,盡可能選用硬件定時器較佳。

.. code-block:: c
   :caption: 1.0.3 幀中斷宏
   :linenos:
   :emphasize-lines: 3,5

    /*置位目標(biāo)串口接收幀標(biāo)志*/
    #define SET_FRAME(COM_UARTx) (COM_UARTx.LNode[COM_UARTx.Wptr].OverTime ? \
    (COM_UARTx.LNode[COM_UARTx.Wptr].OverTime--): \
    ((COM_UARTx.LNode[COM_UARTx.Wptr].Frame_Flag = true), \
    (COM_UARTx.Wptr = ((COM_UARTx.Wptr + 1U) % MAX_NODE)), \
    (COM_UARTx.LNode[COM_UARTx.Wptr].Timer_Flag = false)))

最后,有了這些軟件機(jī)制,僅僅只需要編寫對應(yīng)的邏輯就可以了。

.. code-block:: c
   :caption: 1.0.4 多串口數(shù)據(jù)輪詢處理機(jī)制
   :linenos:
   :emphasize-lines: 3,5

    /*設(shè)置隊列讀指針*/
    #define SET_RPTR(x) ((COM_UART##x).Rptr = (((COM_UART##x).Rptr + 1U) % MAX_NODE))                  
    /*設(shè)置隊列寫指針*/
    #define SET_WPTR(x) ((COM_UART##x).Wptr = (((COM_UART##x).Wptr + 1U) % MAX_NODE))

    /*串口一對一數(shù)據(jù)轉(zhuǎn)發(fā)數(shù)據(jù)結(jié)構(gòu)*/
    typedef struct
    {
        SEL_CHANNEL Source_Channel; /*數(shù)據(jù)起源通道*/
        SEL_CHANNEL Target_Channel; /*數(shù)據(jù)交付通道*/
        void (*pHandle)(void);
    } ComData_Handle;

    /*定義當(dāng)前串口交換序列*/
    const ComData_Handle ComData_Array[] =
    {
        {CHANNEL_PLC, CHANNEL_RS485, Plc_To_Rs485},
        {CHANNEL_WIFI, CHANNEL_PLC, Wifi_To_Plc},
    };

    /*增加映射關(guān)系時,計算出當(dāng)前關(guān)系數(shù)*/
    #define COMDATA_SIZE (sizeof(ComData_Array) / sizeof(ComData_Handle))

    /**
    * @brief    串口1對1數(shù)據(jù)轉(zhuǎn)發(fā)
    * @details  
    * @param    None
    * @retval   None
    */
    void Uart_DataForward(SEL_CHANNEL Src, SEL_CHANNEL Dest)
    {
        uint8_t i = 0;

        for (i = 0; i < COMDATA_SIZE; i++)
        {
            if ((Src == ComData_Array[ i].Source_Channel) && (Dest == ComData_Array[ i].Target_Channel))[ i][ i]
            {
            ComData_Array[ i].pHandle();[ i]
            }
        }
    }

    /**
    * @brief    串口事件處理
    * @details  
    * @param    None
    * @retval   None
    */
    void Uart_Handle(void)
    {
        /*數(shù)據(jù)交換序列1:PLC與RS485進(jìn)行數(shù)據(jù)交換*/
        Uart_DataForward(CHANNEL_PLC, CHANNEL_RS485);
        /*數(shù)據(jù)交換序列2:WIFI與PLC進(jìn)行數(shù)據(jù)交換*/
        Uart_DataForward(CHANNEL_WIFI, CHANNEL_PLC);
    }

    /**
    * @brief    PLC數(shù)據(jù)交付到RS485
    * @details  
    * @param    None
    * @retval   None
    */
    void Plc_To_Rs485(void)
    {
        /*STC串口4收到PLC發(fā)出的數(shù)據(jù)*/
        if ((COM_UART4.LNode[COM_UART4.Rptr].Frame_Flag)) //&& (COM_UART4.LNode[COM_UART4.Rptr].Rx_Length)
        {                                                
            /*如果串口4接收到的數(shù)據(jù)幀不是EBM所需的,過濾掉*/
            if (COM_UART4.LNode[COM_UART4.Rptr].Rx_Buffer[0] != MODBUS_SLAVEADDR)
            {   /*標(biāo)記該接收幀以進(jìn)行處理*/
                COM_UART4.LNode[COM_UART4.Rptr].Frame_Flag = false;
                /*允許485發(fā)送*/
                USART3_EN = 1;
                /*數(shù)據(jù)轉(zhuǎn)發(fā)給RS485時,數(shù)據(jù)長度+1,可以保證MAX3485芯片能夠最后一位數(shù)據(jù)剛好不停止在串口的停止位上*/
                Uartx_SendStr(&Uart3, COM_UART4.LNode[COM_UART4.Rptr].Rx_Buffer, COM_UART4.LNode[COM_UART4.Rptr].Rx_Length + 1U);
                /*接收到數(shù)據(jù)長度置為0*/
                COM_UART4.LNode[COM_UART4.Rptr].Rx_Length = 0;
                /*發(fā)送中斷結(jié)束后,清空對應(yīng)接收緩沖區(qū)*/
                memset(&COM_UART4.LNode[COM_UART4.Rptr].Rx_Buffer[0], 0, MAX_SIZE);
                /*發(fā)送完一幀數(shù)據(jù)后拉低*/
                USART3_EN = 0;
                /*讀指針指到下一個節(jié)點*/
                SET_RPTR(4);
            }

            /*目標(biāo)設(shè)備發(fā)出應(yīng)答*/
            if ((COM_UART3.LNode[COM_UART3.Rptr].Frame_Flag)) //&& (COM_UART3.LNode[COM_UART3.Rptr].Rx_Length)
            {
                /*標(biāo)記該接收幀已經(jīng)進(jìn)行處理*/
                COM_UART3.LNode[COM_UART3.Rptr].Frame_Flag = false;
                /*數(shù)據(jù)返回給請求對象*/
                Uartx_SendStr(&Uart4, COM_UART3.LNode[COM_UART3.Rptr].Rx_Buffer, COM_UART3.LNode[COM_UART3.Rptr].Rx_Length);
                /*接收到數(shù)據(jù)長度置為0*/
                COM_UART3.LNode[COM_UART3.Rptr].Rx_Length = 0;
                /*發(fā)送中斷結(jié)束后,清空對應(yīng)接收緩沖區(qū)*/
                memset(&COM_UART3.LNode[COM_UART3.Rptr].Rx_Buffer[0], 0, MAX_SIZE);
                /*讀指針指到下一個節(jié)點*/
                SET_RPTR(3);
            }
        }
    }

以上圖文的pdf格式文檔下載(內(nèi)容和本網(wǎng)頁上的一模一樣,方便保存): sphinx.pdf (427.3 KB, 下載次數(shù): 6)





歡迎光臨 (http://www.torrancerestoration.com/bbs/) Powered by Discuz! X3.1