實用型軟件架構
==============
多串口數據交互模型
-------------------
實際需求
~~~~~~~~
最近在做一個西門子 ``Step 200`` 系列的PLC通訊口擴展項目時,遇到了這樣的問題:
``224XP`` ,這個CPU的外部通訊端口只用兩個,在物聯網大火的當下,這樣的擴展口數量,在加入聯網模塊后,顯然無法滿足更多的
聯網需求。當前實際需求如下:
.. csv-table:: **通訊口對應功能**
:header: "編號", "功能"
:widths: 5, 10
:align: center
01, "PLC串口屏通訊"
02, "EBM風扇通訊"
03, "4G/WIFI模塊通訊"
04, "以太網通訊"
51hei.png (32.71 KB, 下載次數: 70)
下載附件
2021-10-10 23:22 上傳
在考慮到成本與技術可行性前提下,盡可能保留產品研發核心技術手段,選用STC8系列單片機對PLC原有的兩個通訊口
利用串口進行擴展。設計思路如下:
.. figure:: mode1.png
:align: center
:alt: NULL
:scale: 70%
圖 4.1 理論中多串口數據交互模型
從圖中可以看出,數據信息的主要請求目標主要是通過 ``PLC_PORT0`` 獲得PLC內部存儲區數據( ``PLC_PORT1`` 默認用于連接屏幕)。
因此,進行軟件拓展的目標物理鏈路就是 ``PLC_PORT0`` 。
矛盾的產生
~~~~~~~~~~
從上面的模型可以看出,當前工作模式應該是一個多主單從結構。那么按照常理應該是由STC8的4個串口通過輪詢的方式對共享設備PLC
目標地址發出數據請求的命令,隨后由PLC把響應數據返回給當前請求對象。如果嚴格遵循這樣的工作模式,不會存在任何問題。
但是,實際的架構設計需求如下:
51hei.png (30.71 KB, 下載次數: 96)
下載附件
2021-10-10 23:22 上傳
.. figure:: mode2.png
:align: center
:alt: NULL
:scale: 70%
圖 4.2 實際的多串口數據交互模型
.. attention::
其中每個通訊端口上端的標號都代表在實際的通訊過程中,STC8單片機作為擴展主機時輪詢框架下的調度關系(數字越小,優先級越高;數字相等,代表處于同一優先級)。
這里實際使用的時候是通過 ``PLC_PORT0`` 與 ``STC8_UART4`` 進行物理上的連接,在通過STC8內部軟件協議通過其他串口與拓展設備
進行數據交互。很顯然當前的架構無法滿足這樣的實際需求,矛盾就應運而生了。
.. note::
既然多主機,單從機的通訊模型無法在PLC作為主機時滿足需求,那么就可以重新考慮另外一種工作模式。為了適應更多可能的情況,
建立一種不分主從結構的工作模式,在多對象數據交互的基礎上建立一種相對是一對一的通訊機制。
.. figure:: mode3.png
:align: center
:alt: NULL
:scale: 70%
51hei.png (31.4 KB, 下載次數: 89)
下載附件
2021-10-10 23:23 上傳
圖 4.2 改進后的多串口數據交互模型
軟件設計思想
~~~~~~~~~~~~~~
51hei.png (21.84 KB, 下載次數: 85)
下載附件
2021-10-10 23:23 上傳
.. figure:: F0.png
:align: center
:alt: NULL
:scale: 70%
圖 4.3 基礎數據結構
.. note::
從圖中可以看出,最上層采用的是循環隊列,每個隊列的元素由一條鏈表進行連接,每條鏈表的一個節點代表一幀數據。
.. csv-table:: **單節點上成員描述**
:header: "標識符", "意義"
:widths: 8, 20
:align: center
"Frame_Flag", "幀標志:由定時器幀中斷機制置為true;輪詢轉發程序轉發當前幀后置為false"
"Timer_Flag", "幀中斷定時器開啟標志:當任意串口接收中斷收到一個字節數據時設置為true;超時后設置false"
"Rx_Buffer", "數據幀接收緩沖區"
"Rx_Length", "當前數據幀長度"
"OverTime", "幀判定時間:該變量在串口中斷有字節數據接收時會不斷刷新;在幀仲裁定時器中其值不斷減小至0"
**詳細工作原理:**
以PLC通過485總線發送數據為例,假設PLC當前要像EBM請求某一個狀態值,發出一幀數據 ``15 21 01 CA``,此時EBM響應數據為 ``35 01 01 00 CA`` ,則:
1、串口四接收中斷收到PLC發出的第一個字節,打開幀中斷定時器,判斷當前寫指針所對應的鏈表節點幀標志是否為false,條件成立后判斷當前節點幀長度是否溢出,
如果沒有就刷新當前幀鏈表塊中 ``OverTime`` , 最后把當前字節 ``15`` 存到當前幀緩沖區 ``Rx_Buffer`` 的位置上。
2、后續字符 ``21 01 CA`` 的接收操作與第一個字符一致,其中每個字節間間隔由通訊的波特率決定,*<<Timer(OverTime)* ,當接收完這一幀數據后,``OverTime``
值將不會在串口接收中斷中被刷新,而是由幀中斷定時器中不斷減小為0,最終標志該節點上這幀數據接收完成,并把對應的 ``Frame_Flag``
置為true。
3、在主程序輪詢機制中,一旦檢測到有 ``Frame_Flag`` 產生,則利用讀指針訪問當前節點幀緩沖區,對目標設備發出請求命令。
4、響應數據返回給目標對象的工作過程與前三個步驟完全一致。值得注意的是,入果存在對個數據交換序列(:menuselection:`PLC_PORT0-->UART4-->UART3` 和 :menuselection:`UART2-->UART4-->PLC_PORT0` ,
存在相反的公共序列 :menuselection:`PLC_PORT0-->UART4` , :menuselection:`UART4-->PLC_PORT0`),此時如果公用的是同一個緩沖區,且不對不同類型的數據進行分流,將會造成不同請求對象數據響應錯誤,
所以必須加以條件限制。
建立數據結構
~~~~~~~~~~~~~~
.. code-block:: c
:caption: 1.0.0 基礎數據結構
:linenos:
:emphasize-lines: 3,5
/*鏈隊數據結構*/
typedef struct
{
uint8_t Frame_Flag; /*幀標志*/
uint8_t Timer_Flag; /*打開定時器標志*/
uint8_t Rx_Buffer[MAX_SIZE]; /*數據接收緩沖區*/
uint16_t Rx_Length; /*數據接收長度*/
uint16_t OverTime; /*目標設備響應超時時間*/
}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::
頂層數據結構采用環形隊列,只不過隊列中的單個元素并不是一個單一的值,而是一個帶有記錄信息的數據塊 ``Uart_Queu`` 。
這樣做的目的在于,使用的單片機是C51,其本身的串口是不帶有空閑中斷或者DMA這些高級硬件的,那這就需要我們通過軟件算法模擬這一些硬件功能
來完成功能設計。
.. code-block:: c
:caption: 1.0.1 改進后基礎數據結構
:linenos:
:emphasize-lines: 3,5
/*鏈隊數據結構*/
typedef struct
{
uint8_t Frame_Flag; /*幀標志*/
uint8_t Timer_Flag; /*打開定時器標志*/
uint8_t Rx_Buffer[MAX_SIZE]; /*數據接收緩沖區*/
uint16_t Rx_Length; /*數據接收長度*/
uint16_t OverTime; /*目標設備響應超時時間*/
Uart_Queu *Next; /*指向下一個節點*/
}Uart_Queu;
.. note::
主要改進了隊列下數據塊元素的內存分配方式,由原來的靜態的分配,改為程序運行過程根據實際需求來分配。考慮 ``Malloc`` 函數在
51編譯器中安全性和適用性,實際使用過程建議非必要情況采用靜態內存分配方式。當然,采用動態內存分配方式,使用循環鏈表將會帶來更多的
可操作性、靈活性和內存節約。
.. code-block:: c
:caption: 1.0.2 串口幀中斷機制設計
:linenos:
:emphasize-lines: 3,5
/**
* @brief 定時器0的中斷服務函數
* @details
* @param None
* @retval None
*/
void Timer0_ISR() interrupt 1
{
if(COM_UART1.LNode[COM_UART1.Wptr].Timer_Flag)
/*以太網串口接收字符間隔超時處理*/
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中斷函數
* @details 使用的是定時器4作為波特率發生器,PLC口用
* @param None
* @retval None
*/
void Uart4_Isr() interrupt 18
{ /*發送中斷*/
if (S4CON & S4TI)
{
S4CON &= ~S4TI;
/*發送完成,清除占用*/
Uart4.Uartx_busy = false;
}
/*接收中斷*/
if (S4CON & S4RI)
{
S4CON &= ~S4RI;
/*當收到數據時打開幀中斷定時器*/
COM_UART4.LNode[COM_UART4.Wptr].Timer_Flag = true;
/*當前節點還沒有收到一幀數據*/
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)
{ /*把數據存到當前節點的緩沖區*/
COM_UART4.LNode[COM_UART4.Wptr].Rx_Buffer[COM_UART4.LNode[COM_UART4.Wptr].Rx_Length++] = S4BUF;
}
}
}
}
.. note::
因為硬件定時器數量有限,所以幾個串口的幀中斷機定時器均采用了 ``Timer0`` 進行仲裁,可能會存在中斷延時的問題,在硬件定時器資源充足情況下,盡可能選用硬件定時器較佳。
.. code-block:: c
:caption: 1.0.3 幀中斷宏
:linenos:
:emphasize-lines: 3,5
/*置位目標串口接收幀標志*/
#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)))
最后,有了這些軟件機制,僅僅只需要編寫對應的邏輯就可以了。
.. code-block:: c
:caption: 1.0.4 多串口數據輪詢處理機制
:linenos:
:emphasize-lines: 3,5
/*設置隊列讀指針*/
#define SET_RPTR(x) ((COM_UART##x).Rptr = (((COM_UART##x).Rptr + 1U) % MAX_NODE))
/*設置隊列寫指針*/
#define SET_WPTR(x) ((COM_UART##x).Wptr = (((COM_UART##x).Wptr + 1U) % MAX_NODE))
/*串口一對一數據轉發數據結構*/
typedef struct
{
SEL_CHANNEL Source_Channel; /*數據起源通道*/
SEL_CHANNEL Target_Channel; /*數據交付通道*/
void (*pHandle)(void);
} ComData_Handle;
/*定義當前串口交換序列*/
const ComData_Handle ComData_Array[] =
{
{CHANNEL_PLC, CHANNEL_RS485, Plc_To_Rs485},
{CHANNEL_WIFI, CHANNEL_PLC, Wifi_To_Plc},
};
/*增加映射關系時,計算出當前關系數*/
#define COMDATA_SIZE (sizeof(ComData_Array) / sizeof(ComData_Handle))
/**
* @brief 串口1對1數據轉發
* @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)
{
/*數據交換序列1:PLC與RS485進行數據交換*/
Uart_DataForward(CHANNEL_PLC, CHANNEL_RS485);
/*數據交換序列2:WIFI與PLC進行數據交換*/
Uart_DataForward(CHANNEL_WIFI, CHANNEL_PLC);
}
/**
* @brief PLC數據交付到RS485
* @details
* @param None
* @retval None
*/
void Plc_To_Rs485(void)
{
/*STC串口4收到PLC發出的數據*/
if ((COM_UART4.LNode[COM_UART4.Rptr].Frame_Flag)) //&& (COM_UART4.LNode[COM_UART4.Rptr].Rx_Length)
{
/*如果串口4接收到的數據幀不是EBM所需的,過濾掉*/
if (COM_UART4.LNode[COM_UART4.Rptr].Rx_Buffer[0] != MODBUS_SLAVEADDR)
{ /*標記該接收幀以進行處理*/
COM_UART4.LNode[COM_UART4.Rptr].Frame_Flag = false;
/*允許485發送*/
USART3_EN = 1;
/*數據轉發給RS485時,數據長度+1,可以保證MAX3485芯片能夠最后一位數據剛好不停止在串口的停止位上*/
Uartx_SendStr(&Uart3, COM_UART4.LNode[COM_UART4.Rptr].Rx_Buffer, COM_UART4.LNode[COM_UART4.Rptr].Rx_Length + 1U);
/*接收到數據長度置為0*/
COM_UART4.LNode[COM_UART4.Rptr].Rx_Length = 0;
/*發送中斷結束后,清空對應接收緩沖區*/
memset(&COM_UART4.LNode[COM_UART4.Rptr].Rx_Buffer[0], 0, MAX_SIZE);
/*發送完一幀數據后拉低*/
USART3_EN = 0;
/*讀指針指到下一個節點*/
SET_RPTR(4);
}
/*目標設備發出應答*/
if ((COM_UART3.LNode[COM_UART3.Rptr].Frame_Flag)) //&& (COM_UART3.LNode[COM_UART3.Rptr].Rx_Length)
{
/*標記該接收幀已經進行處理*/
COM_UART3.LNode[COM_UART3.Rptr].Frame_Flag = false;
/*數據返回給請求對象*/
Uartx_SendStr(&Uart4, COM_UART3.LNode[COM_UART3.Rptr].Rx_Buffer, COM_UART3.LNode[COM_UART3.Rptr].Rx_Length);
/*接收到數據長度置為0*/
COM_UART3.LNode[COM_UART3.Rptr].Rx_Length = 0;
/*發送中斷結束后,清空對應接收緩沖區*/
memset(&COM_UART3.LNode[COM_UART3.Rptr].Rx_Buffer[0], 0, MAX_SIZE);
/*讀指針指到下一個節點*/
SET_RPTR(3);
}
}
}
以上圖文的pdf格式文檔下載(內容和本網頁上的一模一樣,方便保存):
sphinx.pdf
(427.3 KB, 下載次數: 6)
2021-10-10 17:45 上傳
點擊文件名下載附件
LaTex生成的PDF文檔 下載積分: 黑幣 -5
|