TwinCAT3 - 实现自己的Tc2_SerialCom

1,前言

在TwinCAT3中,典型的串口通信,硬件需要模块EL6022(类似的模块有EL6001、EL6002、EL6021),函数库需要Tc2_SerialCom,使用此函数库需要购买官方的TF6340授权。
如果实现了自己的Tc2_SerialCom,则不再需要购买官方授权。

2,原生Tc2_SerialCom简单使用

先简单介绍下原生函数库的使用,对此不熟悉的建议先看完倍福官方文档。
新建TwinCAT3项目,添加Tc2_SerialCom引用,在项目Device添加EL6022硬件。
在全局变量中定义两个硬件链接结构体,并链接到EL6022,再定义数据收发缓存(其实你想定义在哪都行,只要能访问到)。

GVL_IO
VAR_GLOBAL
  ComIn AT%I*: EL6inData22B;
  ComOut AT%Q*: EL6OutData22B;
  ComSendBuf: ComBuffer;  //数据发送缓存
  ComReceiveBuf: ComBuffer;  //数据接收缓存
END_VAR

然后新建一个Task,周期设置为2ms,命名为FastTask;新建一个PROGRAM挂载在FastTask下,用来跑硬件与缓存之间的数据交换。

PROGRAM PRG_SerialLine
VAR
  serialLine: SerialLineControl;  //硬件与缓存之间的数据交换
END_VAR

//代码部分
serialLine(Mode:= SERIALLINEMODE_EL6_22B, pComIn:= ADR(GVL_IO.ComIn), pComOut:= ADR(GVL_IO.ComOut),
SizeComIn:= SIZEOF(GVL_IO.ComIn), TxBuffer:= GVL_IO.ComSendBuf, RxBuffer:= GVL_IO.ComReceiveBuf);

接下来就在PROGRAM MAIN中进行数据的收发了,MAIN所在的Task一般周期在20ms左右。

VAR
  bSend: BOOL;
  SendS: SendString;  //发字符串
  StringToSend: STRING;

  bReceive: BOOL;
  ReceiveS: ReceiveString;  //收字符串
  StringToReceive: STRING;
END_VAR

//代码部分
IF bSend THEN
  SendS(SendString:= StringToSend, TXbuffer:= GVL_IO.ComSendBuf);
  bSend:= FALSE;
END_IF
IF bReceive THEN
  ReceiveS(Prefix:= 'A', Suffix:= 'B', TimeOut:= T#200MS, ReceivedString:= StringToReceive, RXbuffer:= GVL_IO.ComReceiveBuf);
  bReceive:= FALSE;
END_IF

SendS功能块将用户想发送的字符传给缓存GVL_IO.ComSendBuf,serialLine功能块将缓存传给GVL_IO.ComOut,硬件就将数据发送出去了

3,实现自己的Tc2_SerialCom

根据上面的简单使用,可知Tc2_SerialCom必需的结构体和功能块有:

  • 结构体:EL6inData22B,EL6outData22B,ComBuffer
  • 功能块:SerialLineControl,SendString,ReceiveString

下面依葫芦画瓢,逐个进行实现

3.1,EL6inData22B,EL6outData22B

先定义一个常数,表示EL6022收发区的大小

VAR_GLOBAL CONSTANT
  HardwareLength: USINT:= 22;
END_VAR

然后直接照抄官方的定义就好了

TYPE EL6inData22B :
STRUCT
  Status: WORD;
  DataIn: ARRAY[0..GVL_Constant.HardwareLength - 1] OF BYTE;
END_STRUCT
END_TYPE

TYPE EL6outData22B :
STRUCT
  Ctrl: WORD;
  DataOut: ARRAY[0..GVL_Constant.HardwareLength - 1] OF BYTE;
END_STRUCT
END_TYPE

其中状态字Status和控制字Ctrl是操作EL6022的硬件的关键,查阅官方文档可知

  • Status:
    • bit0:Transmit Done
    • bit1:Receive Request
    • bit2:Init Accepted
    • bit3:SndBuffer Full
    • bit8-bit15:Input Length
  • Ctrl:
    • bit0:Transmit Request
    • bit1:Receive Accecpted
    • bit2:Init Request
    • bit3:Send Continues
    • bit8-bit15:Output Length

DataIn和DataOut对应EL6022的接收区和发送区

3.2,ComBuffer

官方的ComBuffer用到了RingBuffer,我没太看明白,其实大部分情况下用不到RingBuffer,除非数据量很大,收发间隔很短。所以直接使用一个普通的Buffer得了

TYPE ComBuffer :
STRUCT
  Buffer: ARRAY [0..300] OF BYTE;
  Count: UDINT;
END_STRUCT
END_TYPE

其中Buffer就是供用户代码使用的缓存,Count表示缓存中有效数据长度

3.3,SerialLineControl

此功能块是整个函数库的核心

FUNCTION_BLOCK SerialLineControl
VAR_INPUT
  pComIn: POINTER TO EL6inData22B;
  pComOut: POINTER TO EL6outData22B;
END_VAR
VAR_OUTPUT
END_VAR
VAR_IN_OUT
  TxBuffer: ComBuffer;
  RxBuffer: ComBuffer;
END_VAR
VAR
  pInputLength: POINTER TO USINT;
  pOutputLength: POINTER TO USINT;
	
  A: INT;
  TimerWait: TON;
  StateCom: INT;
  CurrentInCycle: INT;
  CurrentOutCycle: INT; 
	
  bInited: BOOL;
END_VAR

//代码部分
//硬件发送区和接收区的长度指针
pInputLength:= pComIn + 1;
pOutputLength:= pComOut + 1;

//初始化硬件
IF NOT bInited THEN
  pComOut^.Ctrl:= 4;  //Init Request
  IF pComIn^.Status = 4 THEN  //Init Accepted 
    pComOut^.Ctrl:= 0;
    bInited:= TRUE;
  END_IF
  RETURN;
END_IF

CASE StateCom OF 
  0:
    IF TxBuffer.Count >= 1 THEN
      StateCom:= 100;
    ELSIF pComIn^.Status.1 <> pComOut^.Ctrl.1 THEN  //ReceiveRequest
      StateCom:= 200;
    END_IF
	
    //发送
    100:
      CurrentOutCycle:= 0;
      pComOut^.Ctrl.3:= TRUE;
      StateCom:= 110;
    110:
      pOutputLength^:= MIN(GVL_Constant.HardwareLength, UDINT_TO_USINT(TxBuffer.Count - CurrentOutCycle * GVL_Constant.HardwareLength));
      FOR A:= 0 TO pOutputLength^ - 1 DO
        pComOut^.DataOut[A]:= TxBuffer.Buffer[MIN(300, CurrentOutCycle * GVL_Constant.HardwareLength + A)];
      END_FOR
      pComOut^.Ctrl.0:= NOT pComOut^.Ctrl.0;
      CurrentOutCycle:= CurrentOutCycle + 1;
      StateCom:= 120;
    120:
      IF pComIn^.Status.0 = pComOut^.Ctrl.0 THEN
        IF CurrentOutCycle >= TxBuffer.Count / INT_TO_REAL(GVL_Constant.HardwareLength) THEN
          CurrentOutCycle:= 0;
          StateCom:= 130;
        ELSE
          StateCom:= 110;
        END_IF
      END_IF
    130:
      IF pComIn^.Status.0 = pComOut^.Ctrl.0 THEN
        pOutputLength^:= 0;
        TxBuffer.Count:= 0;
        StateCom:= 0;
      END_IF
	
    //接收
    200:
      CurrentInCycle:= 0;
      MEMSET(ADR(RxBuffer), 0, SIZEOF(RxBuffer));
      StateCom:= 210;
    210:
      FOR A:= 0 TO pInputLength^ - 1 DO
        IF CurrentInCycle * GVL_Constant.HardwareLength + A <= 300 THEN
          RxBuffer.Buffer[CurrentInCycle * GVL_Constant.HardwareLength + A]:= pComIn^.DataIn[A];
          RxBuffer.Count:= RxBuffer.Count + 1;
        END_IF
      END_FOR
      CurrentInCycle:= CurrentInCycle + 1;
      StateCom:= 220;
    220:
      pComOut^.Ctrl.1:= pComIn^.Status.1;
      StateCom:= 230;
    230:
      IF pInputLength^ >= 1 AND pComIn^.Status.1 <> pComOut^.Ctrl.1 THEN  //ReceiveRequest
        TimerWait(IN:= FALSE);
        StateCom:= 210;
      ELSE
        TimerWait(IN:= TRUE, PT:= T#20MS);  //这个时间不能太短,如果用RingBuffer就不需要这个了
        IF TimerWait.Q THEN
          TimerWait(IN:= FALSE);
          CurrentInCycle:= 0;
          StateCom:= 0;
        END_IF
      END_IF
END_CASE

逻辑不算复杂,发送就是将ComBuffer的数据往EL6OutData22B搬,接收就是将EL6inData22B的数据往ComBuffer搬,每次最多搬GVL_Constant.HardwareLength个字节,搬不完就硬件把这些字节处理完继续搬

3.4,SendString,ReceiveString

这俩就很简单了,实现用户代码和缓存之间的交互

FUNCTION_BLOCK SendString
VAR_INPUT
  SendString: STRING;
END_VAR
VAR_IN_OUT
  TXbuffer: ComBuffer;
END_VAR

//代码部分
TXbuffer.Count:= INT_TO_UINT(LEN(SendString));
MEMCPY(ADR(TXbuffer.Buffer), ADR(SendString), TXbuffer.Count);
FUNCTION_BLOCK ReceiveString
VAR_INPUT
  Prefix: STRING;
  Suffix: STRING;
  TimeOut: TIME;
END_VAR
VAR_OUTPUT
  bTimeOut: BOOL;
  StringReceived: BOOL;
  Error: BOOL;
END_VAR
VAR_IN_OUT
  ReceivedString: STRING;
  RXbuffer: ComBuffer;
END_VAR
VAR
  Timer: TON;
  PrefixIndex, SuffixIndex: INT;
  P: POINTER TO STRING(255);
  P1: POINTER TO STRING(255);
END_VAR

//代码部分
Timer(IN:= TRUE, PT:= TimeOut);
bTimeOut:= FALSE;
IF Timer.Q THEN
  Timer(IN:= FALSE);
  bTimeOut:= TRUE;
  RxBuffer.Count:= 0;
END_IF

PrefixIndex:= 0;
SuffixIndex:= 0;
P:= ADR(RXbuffer.Buffer);
PrefixIndex:= FIND(P^, Prefix);
P1:= P+PrefixIndex;
SuffixIndex:= PrefixIndex+FIND(P1^, Suffix);
StringReceived:= UDINT_TO_INT(RxBuffer.Count) >= SuffixIndex AND SuffixIndex > PrefixIndex > 0;

IF StringReceived THEN
  Timer(IN:= FALSE);
  MEMSET(ADR(ReceivedString), 0, SIZEOF(ReceivedString));
  MEMCPY(destAddr:= ADR(ReceivedString), srcAddr:= P1-1, SuffixIndex - PrefixIndex + 1);
  RxBuffer.Count:= 0;
END_IF

ReceiveString本身很简单,加了根据Prefix和Suffix截取数据才多了几行代码

4,自己的Tc2_SerialCom简单使用

先引用自己写的Tc2_SerialCom,PlcTask的PROGRAM MAIN

PROGRAM MAIN
VAR
  ComIn AT%I*: EL6inData22B;
  ComOut AT%Q*: EL6outData22B;
  TxBuffer: ComBuffer;
  RxBuffer: ComBuffer;

  sendString: SendString;
  receiveString: ReceiveString;
  StringToSend: STRING;
  StringToReceive: STRING;
END_VAR

//代码部分
IF LEN(StringToSend) > 0 THEN
  sendString(SendString:= StringToSend, TXbuffer:= TxBuffer);
  StringToSend:= '';
END_IF

receiveString(Prefix:= 'A', Suffix:= 'B', TimeOut:= T#200MS, ReceivedString:= StringToReceive, RXbuffer:= RxBuffer);
IF receiveString.bTimeOut THEN
  StringToReceive:= '';
ELSIF receiveString.StringReceived THEN
  //解析数据
END_IF

FastTask的PROGRAM

PROGRAM PRG_SerialLine
VAR
  serialLine: SerialLineControl;
END_VAR

//代码部分
serialLine(pComIn:= ADR(MAIN.ComIn), pComOut:= ADR(MAIN.ComOut), TxBuffer:= MAIN.TxBuffer, RxBuffer:= MAIN.RxBuffer);

使用起来和原生的几乎一毛一样。

5,总结

国内一些厂家也做了自己的串口通信模块,函数库设计上也是类似的思路;但是实际使用中接线方式,自动识别,函数接口等方面总是差点意思。也有一些厂家使用串口服务器来替代串口通信模块,由于官方授权的存在,成本上似乎还有降低。希望国产制造业能越做越好吧。

作者:tossorrow
出处:TwinCAT3 - 实现自己的Tc2_SerialCom
转载:欢迎转载,请保留此段声明,请在文章中给出原文链接;
posted @ 2023-06-20 16:38  tossorrow  阅读(533)  评论(0编辑  收藏  举报