8 de febrero de 2010

Comunicación entre un PLC Beckhoff y Visual Basic

En una aplicación reciente he tenido que comunicar a través de Ethernet un PLC Beckhoff CX9010 con una aplicación programada en Visual Basic 2008 Express. Para realizar esta conexión tenemos a nuestra disposición de manera gratuita controles OCX y DLL de comunicación. Estas son las notas que resumen las pruebas que hice para obtener una comunicación óptima.

La comunicación entre el PLC y Visual Basic se realiza mediante el protocolo ADS (Automation Device Specification) y funciona sobre los protocolos TCP/IP o UDP/IP. En el PLC no es necesario realizar ninguna configuración especial. Simplemente se deben tener claras las variables que necesitamos comunicar para poder agruparlas y leerlas y/o escribirlas en una única operación. Mis primeras pruebas leían y escribían variables individualmente, pero al pasar de un pequeño número de operaciones de lectura/escritura el programa se ralentizaba. Entonces opté por agrupar todas las variables necesarias en dos estructuras, una con los datos que se van a leer desde la aplicación en Visual Basic (estructura de lectura) y otra con los datos que la aplicación podrá modificar (estructura de escritura). Como ejemplo pondré estas dos estructuras declaradas en 'Data types':
(**********************************************************)
(*** DATOS DE PLC A VISUAL BASIC **************************)
(**********************************************************)

TYPE DATOS_PLC_VB :
STRUCT
TABLA_BOOL: ARRAY [0..7] OF BOOL;
VALOR_INT: INT;
VALOR_LREAL: LREAL;
END_STRUCT
END_TYPE

(**********************************************************)
(*** DATOS DE VISUAL BASIC A PLC **************************)
(**********************************************************)

TYPE DATOS_VB_PLC :
STRUCT
TABLA_BOOL: ARRAY [0..7] OF BOOL;
VALOR_INT: INT;
VALOR_LREAL: LREAL;
END_STRUCT
END_TYPE
Para trabajar con las estructuras las declaro como variables globales:
VAR_GLOBAL
PLC_VB: DATOS_PLC_VB;
VB_PLC: DATOS_VB_PLC;
END_VAR
Ahora deberemos tomar nota del tamaño de las estructuras. Tengo una tabla de ocho booleanos, un entero y un real largo. Cada booleano ocupa un byte, un entero son 2 bytes y un real largo 8 bytes, con lo que tenemos un total de 18 bytes para cada una de las estructuras.

En la web de Beckhoff hay una explicación de transferencia de estructuras con programas de ejemplo.

Ahora vamos a Visual Basic y creamos un proyecto nuevo. Lo primero será incluir las referencias a bibliotecas necesarias. En la pestaña 'References', en la parte inferior 'Imported namespaces' marcamos 'System.IO'.


A continuación pulsamos el botón 'Add...' para añadir una nueva referencia y en la ventana que aparece vamos a la pestaña 'Browse'. Vamos a añadir la DLL en .NET para ADS de Beckhoff, que se incluye con la instalación de TwinCAT. Buscamos el directorio 'c:\TwinCAT\ADS Api\.NET\v2.0xxx' (puede variar en función de qué versión de TwinCAT hayamos instalado) y marcamos el fichero 'TwinCAT.Ads.dll'.


Nos aseguramos también de marcar en 'Imported namespaces' las casillas de 'TwinCAT', 'TwinCAT.Ads' y 'TwinCAT.Ads.Internal'.


Con esto ya tenemos en nuestro proyecto acceso a todas las funciones para realizar la comunicación, así que voy a continuación con la programación. Lo que he hecho es agrupar todo el código necesario en una clase que voy a ir comentando a continuación:

En la declaración de variables de la clase puede ser necesario cambiar la constante Num_Var, si aparte de las estructuras de lectura y escritura es necesario acceder a otras variables o estructuras.
'Conexión con un PLC Beckhoff a través de ADS
'GR - notasdeautomatización.blogspot.com

Public Class Conex_ADS

#Region "Variables"

'Nos dice si ya existe una conexión activa
Private Conectado As Boolean

'Variables para comunicación ADS (Beckhoff)
Private tcAds As TcAdsClient
Private DSRead As AdsStream
Private DSWrite As AdsStream
Private BR As BinaryReader
Private BW As BinaryWriter
Private VarHandle() As Integer

Public Mensaje As String 'Mensaje que indica el éxito de la operación
Public Detalle_Error As String 'Si hay algún error se indica en esta variable

Private Const Num_Var As Integer = 2 'Número máximo de variables que se controlaran con 'Handles'

#End Region
En el constructor de la clase es necesario adaptar el tamaño de las variables tipo 'AdsStream' al tamaño de las estructuras que se van a leer o escribir, en mi caso 18 bytes.
#Region "Funciones"

'Constructor
Public Sub New()
'Al crear el objeto, no estamos conectados
Conectado = False
DSRead = New AdsStream(18)
DSWrite = New AdsStream(18)
BR = New BinaryReader(DSRead)
BW = New BinaryWriter(DSWrite)
End Sub
La conexión con el PLC se hace con la función Conectar, a la que debemos darle la dirección ADS del PLC y el puerto a través del cual nos comunicaremos. Cada variable que queramos comunicar debe tener asignado un 'handle', si añadimos alguna debemos recordar asignárselo en esta función (ver comentarios en el código).
'Realizar una conexión con el PLC
'El primer parámetro es la dirección del ADS del PLC
'(si el PLC es el propio PC (local) debe ser una cadena vacía "")
'El segundo parámetro es el puerto a través del cual nos comunicaremos (por defecto 801)
Public Function Conectar(ByVal Dir As String, ByVal Puerto As Integer) As Boolean

' Si ya estamos conectados se devuelve error
If Conectado Then
Mensaje = "Error conexión PLC."
Detalle_Error = "Se ha intentado crear una conexión al PLC cuando ya hay una creada."
Conectar = False
Exit Function
End If

Try
Mensaje = "Conectando con el PLC..."
tcAds = New TcAdsClient
If Dir = "" Then
tcAds.Connect(Puerto)
Else
tcAds.Connect(Dir, Puerto)
End If

Catch ex As Exception

Detalle_Error = "Error: " & ex.ToString
Conectado = False
Conectar = False
Exit Function

End Try

tcAds.ReadState()

If tcAds.IsConnected Then
Mensaje = "Conexión con el PLC realizada correctamente."

'Si la conexión fue exitosa hay que indicar las variables que se podrán transmitir
ReDim VarHandle(Num_Var)

Try
'Incluir aquí las variables necesarias para la comunicación
'Se indica el nombre de la variable en el programa PLC precedido de un punto
'Pueden indicarse elementos de tablas y/o estructuras, p. ej. ".D_LECTURA.BYTES[3]"
VarHandle(0) = tcAds.CreateVariableHandle(".PLC_VB") 'Datos que voy a leer del PLC
VarHandle(1) = tcAds.CreateVariableHandle(".VB_PLC") 'Datos que voy a escribir en el PLC
'Fin de las variables de la conexión

Catch ex As Exception
Mensaje = "Fallo en la conexión con las variables."
Detalle_Error = "Error:" & ex.ToString
Conectar = False
End Try

Detalle_Error = ""
Conectado = True
Conectar = True

Else
Mensaje = "Fallo en la conexión con el PLC."
Detalle_Error = "tcAds.IsConnected = False"
Conectado = False
Conectar = False
End If

End Function
Esta función nos desconecta ordenadamente del PLC.
'Desconectar del PLC
Public Function Desconectar() As Boolean

Dim Índice As Integer

For Índice = 0 To Num_Var
tcAds.DeleteVariableHandle(VarHandle(Índice))
Next

Try
Mensaje = "Desconectando..."
tcAds.Dispose()

Catch ex As Exception

Detalle_Error = "Error: " & ex.ToString
Desconectar = False
Exit Function

End Try

Mensaje = "Desconexión realizada correctamente."
Detalle_Error = ""
Conectado = False
Desconectar = True

End Function
La función 'Actualizar_Lectura' lee en bruto la estructura del PLC, en este caso los 18 bytes y los coloca en el lector binario (BinaryReader). Si necesitamos leer datos continuamente podemos llamar esta función periódicamente con un temporizador.
'Actualizar los valores de la estructura de lectura desde el PLC
Public Function Actualizar_Lectura() As Boolean
If Not Conectado Then
Mensaje = "Error al leer DATOS"
Detalle_Error = "No hay conexión para leer datos."
Actualizar_Lectura = False
Exit Function
End If

DSRead.Position = 0

Try

tcAds.Read(VarHandle(0), DSRead)

Catch ex As Exception

Detalle_Error = ex.ToString
Mensaje = "Error al leer datos."
Actualizar_Lectura = False
Exit Function

End Try

Actualizar_Lectura = True
Mensaje = ""
Detalle_Error = ""

End Function
La función 'Actualizar_Escritura' escribe en bruto los 18 bytes que hayamos puesto en un escritor binario (BinaryWriter) en la estructura del PLC.
'Actualizar los valores de la estructura de escritura al PLC
Public Function Actualizar_Escritura() As Boolean
If Not Conectado Then
Mensaje = "Error al escribir DATOS"
Detalle_Error = "No hay conexión para escribir datos."
Actualizar_Escritura = False
Exit Function
End If

DSWrite.Position = 0

Try

tcAds.Write(VarHandle(1), DSWrite)

Catch ex As Exception

Detalle_Error = ex.ToString
Mensaje = "Error al escribir datos."
Actualizar_Escritura = False
Exit Function

End Try

Actualizar_Escritura = True

Mensaje = ""
Detalle_Error = ""

End Function
Para leer y escribir variables individuales debemos diseccionar la tabla de valores binarios. Las siguientes funciones son ejemplos de como leer y escribir booleanos, enteros y reales, siendo necesario adaptarlas a nuestras necesidades, ya que solo son válidas para las estructuras de ejemplo. Debemos tener claro en qué byte empieza cada dato y qué tamaño tiene para direccionarlo correctamente.

NOTA: las variables tendrán que estar dispuestas en el mismo orden en el que las hayamos declarado en el PLC, recordando que el tipo de datos BOOL ocupa inevitablemente 1 byte.
'Función para escribir un valor booleano
Public Function Escribir_Bool(ByVal Indice As Integer, ByVal Valor As Boolean) As Boolean

If Not Conectado Then
Mensaje = "Error."
Detalle_Error = "No hay conexión para escribir datos."
Escribir_Bool = False
Exit Function
End If

'Los booleanos ocupan un byte (1 - true; 0 - false)
Dim Bit As Byte

'En la estructura de datos de lectura los 8 primeros bytes son los booleanos
DSWrite.Position = Indice

If Valor Then
Bit = 1
Else
Bit = 0
End If

BW.Write(Bit)

Escribir_Bool = True

Mensaje = ""
Detalle_Error = ""

End Function

'Función para leer un valor booleano
Public Function Leer_Bool(ByVal Indice As Integer) As Boolean

If Not Conectado Then
Mensaje = "Error."
Detalle_Error = "No hay conexión para leer datos."
Leer_Bool = False
Exit Function
End If

'En la estructura de datos de lectura los 8 primeros bytes son los booleanos
DSRead.Position = Indice
Leer_Bool = BR.ReadBoolean()

Mensaje = ""
Detalle_Error = ""

End Function

'Función para escribir un valor entero (16 bits)
Public Function Escribir_Int(ByVal Valor As Int16) As Boolean

If Not Conectado Then
Mensaje = "Error."
Detalle_Error = "No hay conexión para escribir datos."
Escribir_Int = False
Exit Function
End If

'En la estructura de datos de lectura los 8 primeros bytes son los booleanos
'A continuación están el entero de 16 bits (2 bytes)
DSWrite.Position = 8
BW.Write(Valor)

Escribir_Int = True

Mensaje = ""
Detalle_Error = ""

End Function

'Función para leer un valor entero (16 bits)
Public Function Leer_Int() As Int16

If Not Conectado Then
Mensaje = "Error."
Detalle_Error = "No hay conexión para leer datos."
Leer_Int = False
Exit Function
End If

'En la estructura de datos de lectura los 8 primeros bytes son los booleanos
'A continuación están el entero de 16 bits (2 bytes)
DSRead.Position = 8
Leer_Int = BR.ReadInt16()

Mensaje = ""
Detalle_Error = ""

End Function

'Función para escribir un valor real largo (64 bits)
Public Function Escribir_Lreal(ByVal Valor As Double) As Boolean

If Not Conectado Then
Mensaje = "Error."
Detalle_Error = "No hay conexión para escribir datos."
Escribir_Lreal = False
Exit Function
End If

'En la estructura de datos de lectura los 8 primeros bytes son los booleanos
'A continuación están el entero de 16 bits (2 bytes)
'Finalmente está el valor LREAL (8 bytes)
DSWrite.Position = 8 + 2
BW.Write(Valor)

Escribir_Lreal = True

Mensaje = ""
Detalle_Error = ""

End Function

'Función para leer un valor real largo (64 bits)
Public Function Leer_Lreal() As Double

If Not Conectado Then
Mensaje = "Error."
Detalle_Error = "No hay conexión para leer datos."
Leer_Lreal = False
Exit Function
End If

'En la estructura de datos de lectura los 8 primeros bytes son los booleanos
'A continuación están el entero de 16 bits (2 bytes)
'Finalmente está el valor LREAL (8 bytes)
DSRead.Position = 8 + 2
Leer_Lreal = BR.ReadDouble

Mensaje = ""
Detalle_Error = ""

End Function

#End Region

End Class
La forma de trabajar con esta implementación es primero declarando una variable del tipo Conex_ADS:
Private ADS As Conex_ADS
ADS = New Conex_ADS
A continuación realizamos la conexión:
If Not ADS.Conectar("",801) Then
'Si hay un error en la conexión se visualiza
MsgBox(ADS.Mensaje)
MsgBox(ADS.Detalle_Error)
Exit Sub
End If
Ahora con las funciones Actualizar_Escritura y Actualizar_Lectura llamadas periodicamente podemos mantener la comunicación con el PLC.
ADS.Actualizar_Lectura()
ADS.Actualizar_Escritura()
Para leer y escribir las variables individuales usaré las funciones Leer_Bool, Escribir_Int y similares, teniendo en cuenta que la comunicación no se realizará realmente hasta que las funciones de actualización se ejecuten.
Dim Entero as Integer = ADS.Leer_Int()
ADS.Escribir_Bool(1,False)
Y hasta aquí estas notas. Esto solo es un ejemplo del que partir para implementar nuestra comunicación, que se puede hacer muy diversas maneras en función de nuestras necesidades. En la página de Beckhoff puedes encontrar más ejemplos de programación.

Como siempre, agradeceré cualquier comentario.

3 comentarios:

  1. soy nuevo en esto de la comunicasion entre el visual basic y el plc, me gustaria conocer mas acerca de este proyecto y si se aplica igual para cualquier plc como el allen bradley, ya que quiero realizar un proyecto con este plc micrologix 1500 lsp serie A, y la variable a manejar seria la del tiempo para monitorear maquinas continuas

    ResponderEliminar
  2. Supongo que Allen Bradley dispondrá de sus propias bibliotecas para acceder al PLC desde Visual Basic. Hace años que no toco esa marca de PLCs, siento no ser de mucha ayuda.

    ResponderEliminar
  3. Habra algun limite en el numero de variables
    que podemos Visualizar en VB.Net con ADS

    ResponderEliminar

Por favor, no pidas copias de programas comerciales, licencias o números de serie.