IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 嵌入式 -> 2021-10-11 -> 正文阅读

[嵌入式]2021-10-11

51黑电子论坛地址:http://www.51hei.com/bbs/dpj-213065-1.html

实际需求

最近在做一个西门子 Step 200 系列的PLC通讯口扩展项目时,遇到了这样的问题:

  • 224XP ,这个CPU的外部通讯端口只用两个,在物联网大火的当下,这样的扩展口数量,在加入联网模块后,显然无法满足更多的联网需求。当前实际需求如下:
编号功能
01PLC串口屏通讯
02EBM风扇通讯
034G/WIFI模块通讯
04以太网通讯
  • 在考虑到成本与技术可行性前提下,尽可能保留产品研发核心技术手段,选用STC8系列单片机对PLC原有的两个通讯口利用串口进行扩展。设计思路如下:
    理论中多串口数据交互模型

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

矛盾的产生

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

其中每个通讯端口上端的标号都代表在实际的通讯过程中,STC8单片机作为扩展主机时轮询框架下的调度关系(数字越小,优先级越高;数字相等,代表处于同一优先级)。

  • 这里实际使用的时候是通过 PLC_PORT0STC8_UART4 进行物理上的连接,在通过STC8内部软件协议通过其他串口与拓展设备进行数据交互。很显然当前的架构无法满足这样的实际需求,矛盾就应运而生了。
  • 既然多主机,单从机的通讯模型无法在PLC作为主机时满足需求,那么就可以重新考虑另外一种工作模式。为了适应更多可能的情况,
    建立一种不分主从结构的工作模式,在多对象数据交互的基础上建立一种相对是一对一的通讯机制。
    改进后的多串口数据交互模型

软件设计思想

基础数据结构
从图中可以看出,最上层采用的是循环队列,每个队列的元素由一条链表进行连接,每条链表的一个节点代表一帧数据。

标识符意义
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 ,则:

  • 串口四接收中断收到PLC发出的第一个字节,打开帧中断定时器,判断当前写指针所对应的链表节点帧标志是否为false,条件成立后判断当前节点帧长度是否溢出,如果没有就刷新当前帧链表块中 OverTime , 最后把当前字节 15 存到当前帧缓冲区 Rx_Buffer 的位置上。
  • 后续字符 21 01 CA 的接收操作与第一个字符一致,其中每个字节间间隔由通讯的波特率决定,<<Timer(OverTime) ,当接收完这一帧数据后,OverTime值将不会在串口接收中断中被刷新,而是由帧中断定时器中不断减小为0,最终标志该节点上这帧数据接收完成,并把对应的 Frame_Flag置为true。
  • 在主程序轮询机制中,一旦检测到有 Frame_Flag 产生,则利用读指针访问当前节点帧缓冲区,对目标设备发出请求命令。
  • 响应数据返回给目标对象的工作过程与前三个步骤完全一致。值得注意的是,入果存在对个数据交换序列 PLC_PORT0-->UART4-->UART3UART2-->UART4-->PLC_PORT0,存在相反的公共序列 PLC_PORT0-->UART4UART4-->PLC_PORT0,此时如果公用的是同一个缓冲区,且不对不同类型的数据进行分流,将会造成不同请求对象数据响应错误,所以必须加以条件限制。

建立数据结构

@function:1.0.0 **基础数据结构**

/*链队数据结构*/
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];

顶层数据结构采用环形队列,只不过队列中的单个元素并不是一个单一的值,而是一个带有记录信息的数据块 Uart_Queu 。这样做的目的在于,使用的单片机是C51,其本身的串口是不带有空闲中断或者DMA这些高级硬件的,那这就需要我们通过软件算法模拟这一些硬件功能 来完成功能设计。

@function: 1.0.1 **改进后基础数据结构**
 /*链队数据结构*/
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;

主要改进了队列下数据块元素的内存分配方式,由原来的静态的分配,改为程序运行过程根据实际需求来分配。考虑 Malloc 函数在51编译器中安全性和适用性,实际使用过程建议非必要情况采用静态内存分配方式。当然,采用动态内存分配方式,使用循环链表将会带来更多的可操作性、灵活性和内存节约。

@function: 1.0.2 **串口帧中断机制设计**
   /**
    * @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;
                }
            }
        }
    }

因为硬件定时器数量有限,所以几个串口的帧中断机定时器均采用了 Timer0 进行仲裁,可能会存在中断延时的问题,在硬件定时器资源充足情况下,尽可能选用硬件定时器较佳。

@function: **1.0.3 帧中断宏**
/*置位目标串口接收帧标志*/
    #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)))

最后,有了这些软件机制,仅仅只需要编写对应的逻辑就可以了。

@function: **1.0.4 多串口数据轮询处理机制**
 /*设置队列读指针*/
    #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))
		    {
			ComData_Array[i].pHandle();
		    }
	    }
    }

    /**
    * @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文档,请到51黑电子论坛主页下载。

  嵌入式 最新文章
基于高精度单片机开发红外测温仪方案
89C51单片机与DAC0832
基于51单片机宠物自动投料喂食器控制系统仿
《痞子衡嵌入式半月刊》 第 68 期
多思计组实验实验七 简单模型机实验
CSC7720
启明智显分享| ESP32学习笔记参考--PWM(脉冲
STM32初探
STM32 总结
【STM32】CubeMX例程四---定时器中断(附工
上一篇文章      下一篇文章      查看所有文章
加:2021-10-12 23:37:07  更:2021-10-12 23:38:24 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 5:49:49-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码