实际需求
最近在做一个西门子 Step 200 系列的PLC通讯口扩展项目时,遇到了这样的问题:
224XP ,这个CPU的外部通讯端口只用两个,在物联网大火的当下,这样的扩展口数量,在加入联网模块后,显然无法满足更多的联网需求。当前实际需求如下:
编号 | 功能 |
---|
01 | PLC串口屏通讯 | 02 | EBM风扇通讯 | 03 | 4G/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_PORT0 与 STC8_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-->UART3 和 UART2-->UART4-->PLC_PORT0 ,存在相反的公共序列 PLC_PORT0-->UART4 ,UART4-->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];
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 **串口帧中断机制设计**
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)
SET_FRAME(COM_UART2);
if(COM_UART3.LNode[COM_UART3.Wptr].Timer_Flag)
SET_FRAME(COM_UART3);
if(COM_UART4.LNode[COM_UART4.Wptr].Timer_Flag)
SET_FRAME(COM_UART4);
}
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))
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();
}
}
}
void Uart_Handle(void)
{
Uart_DataForward(CHANNEL_PLC, CHANNEL_RS485);
Uart_DataForward(CHANNEL_WIFI, CHANNEL_PLC);
}
void Plc_To_Rs485(void)
{
if ((COM_UART4.LNode[COM_UART4.Rptr].Frame_Flag))
{
if (COM_UART4.LNode[COM_UART4.Rptr].Rx_Buffer[0] != MODBUS_SLAVEADDR)
{
COM_UART4.LNode[COM_UART4.Rptr].Frame_Flag = false;
USART3_EN = 1;
Uartx_SendStr(&Uart3, COM_UART4.LNode[COM_UART4.Rptr].Rx_Buffer, COM_UART4.LNode[COM_UART4.Rptr].Rx_Length + 1U);
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].Frame_Flag = false;
Uartx_SendStr(&Uart4, COM_UART3.LNode[COM_UART3.Rptr].Rx_Buffer, COM_UART3.LNode[COM_UART3.Rptr].Rx_Length);
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黑电子论坛主页下载。
|