找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
查看: 580|回复: 0
打印 上一主题 下一主题
收起左侧

单片机软件扩展的多串口数据转发模型

[复制链接]
跳转到指定楼层
楼主
实用型软件架构
==============
多串口数据交互模型
-------------------

实际需求
~~~~~~~~
最近在做一个西门子 ``Step 200`` 系列的PLC通讯口扩展项目时,遇到了这样的问题:
``224XP`` ,这个CPU的外部通讯端口只用两个,在物联网大火的当下,这样的扩展口数量,在加入联网模块后,显然无法满足更多的
联网需求。当前实际需求如下:

.. csv-table:: **通讯口对应功能**
   :header: "编号", "功能"
   :widths: 5, 10
   :align: center

   01, "PLC串口屏通讯"
   02, "EBM风扇通讯"
   03, "4G/WIFI模块通讯"
   04, "以太网通讯"

在考虑到成本与技术可行性前提下,尽可能保留产品研发核心技术手段,选用STC8系列单片机对PLC原有的两个通讯口
利用串口进行扩展。设计思路如下:

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

    图 4.1 理论中多串口数据交互模型

从图中可以看出,数据信息的主要请求目标主要是通过 ``PLC_PORT0`` 获得PLC内部存储区数据( ``PLC_PORT1`` 默认用于连接屏幕)。
因此,进行软件拓展的目标物理链路就是 ``PLC_PORT0`` 。

矛盾的产生
~~~~~~~~~~

从上面的模型可以看出,当前工作模式应该是一个多主单从结构。那么按照常理应该是由STC8的4个串口通过轮询的方式对共享设备PLC
目标地址发出数据请求的命令,随后由PLC把响应数据返回给当前请求对象。如果严格遵循这样的工作模式,不会存在任何问题。
但是,实际的架构设计需求如下:

.. 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%

    图 4.2 改进后的多串口数据交互模型

软件设计思想
~~~~~~~~~~~~~~

.. 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, 下载次数: 1)

评分

参与人数 1黑币 +50 收起 理由
admin + 50 共享资料的黑币奖励!

查看全部评分

分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
收藏收藏 分享淘帖 顶 踩
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|小黑屋|51黑电子论坛 |51黑电子论坛6群 QQ 管理员QQ:125739409;技术交流QQ群281945664

Powered by 单片机教程网

快速回复 返回顶部 返回列表