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