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