Comunicación ModBus TCP con PLC utilizando Visual C# (librería)

  • 1 Respuestas
  • 300 Vistas

Soporte

  • Global Moderator
  • Experto
  • *****
  • Mensajes: 1922
  • Soporte Técnico
Librería ModBus TCP Master para Visual C#

A continuación subimos un ejemplo simple en Visual C# para comunicarse al PLC utilizando protocolo ModBus TCP.

En este caso, el PLC actúa como Esclavo y desde Visual C# (una computadora) nos comunicamos como Maestro para escribir/leer registros o bits.

Aplicación de ejemplo

Cargue en el PLC el proyecto MbTcpServerCounter.zip para StxLadder que adjuntamos.

Luego descargue el proyecto ModBusTcpResponseTime.zip para Visual C# y ejecute la aplicación "ModBusTcpResponseTime.exe" de la carpeta "bin\Release".

Especifique dirección del PLC (ejemplo 192.168.1.81) y haga click en botón "Conectar". El puerto por defecto utilizado es 502. A continuación, si no hay errores de comunicación verá la siguiente pantalla (similar):



El funcionamiento es muy simple.

En el PLC, como esclavo ModBus, constantemente se incrementa una variable contador cada 1 mS y se la carga al registro ModBus número 40001.

En el caso que el registro 40001 se haga 0 (por ejemplo un Maestro escribió valor 0), la variable contador se hace cero nuevamente.
Por lo tanto, el valor en el registro 40001 representa el tiempo que pasó desde que un Maestro escribió el valor 0 y volvió a leer el valor del registro.

La aplicación en Visual C#, simplemente envía cada 10 mS una transacción ModBus de escritura de registro (escribe valor 0 en 40001) y luego intenta leer el mismo registro a través de ModBus.

Luego imprime dicho valor en pantalla, que representa el tiempo entre escritura y lectura ModBus.

También calcula el valor promedio de los últimos X valores (definido en caja Cantidad de muestras) y el valor máximo alcanzado.

Adicionalmente en el directorio del ejecutable, se guarda un archivo de texto llamado "log.txt" que registra todas las lecturas de tiempo realizadas de la última conexión para realizar un mejor analisis.

Código en C#

Abra el proyecto en Visual C# (recomendado versión 2015 o superior).

La librería ModBus TCP Master, se encuentra en el archivo stx8xxx.dll incluido en el directorio "bin\release" del proyecto.
Es esta librería que incluimos como referencia en el proyecto (References) y debe ser empleada en su aplicación Visual C# para tener acceso a las funciones ModBus TCP.

Luego agregamos el espacio de nombre en el código fuente con:

Código: (C#) [Seleccionar]
using stx8xxx;

Creamos el objeto que accede a las funciones ModBus como maestro:

Código: (C#) [Seleccionar]
private ModBusTcpMaster MbMaster;
Para conectarnos, simplemente especificamos dirección IP y puerto (que se obtienen de los controles de la ventana):

Código: (C#) [Seleccionar]
MbMaster = new ModBusTcpMaster(textDeviceIP.Text, (ushort)numDevicePort.Value);
Luego especificamos dos eventos, que serán llamados cuando tengamos repuesta de datos y excepciones (un error):

Código: (C#) [Seleccionar]
MbMaster.OnResponseData += new ModBusTcpMaster.ResponseData(MbMaster_OnResponseData);
MbMaster.OnException += new ModBusTcpMaster.ExceptionData(MbMaster_OnException);

Posteriormente, en el código creamos una funcion para leer el registro ModBus 40001, asociado al contador del PLC:

Código: (C#) [Seleccionar]
        private bool ReadPlcCounterRegister()
        {
            bool ErrorsFound = false;

            // Check connection.
            if (!IsConnectedMsg())
            {
                ErrorsFound = true;
                return ErrorsFound;
            }

            // Unique transaction ID (only for identify this transaction on response event).
            ushort ID = 2;

            // Send "Read Holding Register" request.
            MbMaster.ReadHoldingRegister(ID, 40001, 1);

            // Return to caller.
            return ErrorsFound;
        }

Notar como usamos la función "MbMaster.ReadHoldingRegister()" para leer un Holding Register del PLC en dirección "40001". El ID, es un valor arbitrario que utilizamos para luego poder identificar en la repuesta que los datos recibidos corresponden a esta transacción y no a otra que podamos haber realizado.

También creamos una función para escribir el registro del PLC numero 40001:

Código: (C#) [Seleccionar]
        private bool WritePlcCounterRegister(ushort Value)
        {
            bool ErrorsFound = false;

            // Check connection.
            if (!IsConnectedMsg())
            {
                ErrorsFound = true;
                return ErrorsFound;
            }

            // Unique transaction ID (only for identify this transaction on response event).
            ushort ID = 1;

            // Get byte array from "Value".
            byte[] Data = BitConverter.GetBytes((ushort)IPAddress.HostToNetworkOrder((ushort)Value));
           
            // Send "Write Single Holding Register" request.
            MbMaster.WriteSingleRegister(ID, 40001, Data);           

            // Return to caller.
            return ErrorsFound;
        }

Nuevamente, notar como usamos la función "MbMaster.WriteSingleRegister()" para escribir un Holding Register del PLC en dirección "40001". El ID, es un valor arbitrario que utilizamos para luego poder identificar en la repuesta que los datos recibidos corresponden a esta transacción y no a otra que podamos haber realizado.

También, el objeto "MbMaster" de la librería contiene más funciones para leer bits, o escribir/leer varios registros al mismo tiempo. Ver documentación en código ofrecida por Visual C#

Luego, desde un timer, llamamos a estas funciones cada 10 mS, donde comprobamos variable ReadyForTransaction, que indica que estamos listos para comenzar una nueva transacción y luego comprobamos variable StartWriteTransaction, si es "1", enviamos una transacción de escritura, sino, enviamos una transacción de lectura:

Código: (C#) [Seleccionar]
        private void timerPolling_Tick(object sender, EventArgs e)
        {
            //
            // Check if previous response was received or is ready for new transaction.
            //

            if (ReadyForTransaction)
            {
                //
                // Check transaction to do: write or read counter register.
                //

                if (StartWriteTransaction)
                {
                    // Write counter register with 0 value.
                    WritePlcCounterRegister(0);
                }
                else
                {
                    // Read PLC counter register.
                    ReadPlcCounterRegister();
                }
            }
        }

La respuesta a estas transacciones, las obtenemos del evento:

Código: (C#) [Seleccionar]
        private void MbMaster_OnResponseData(ushort ID, byte function, byte[] values)
        {
            // ------------------------------------------------------------------------
            // Seperate calling threads
            // ------------------------------------------------------------------------

            if (this.InvokeRequired)
            {
                this.BeginInvoke(new ModBusTcpMaster.ResponseData(MbMaster_OnResponseData), new object[] { ID, function, values });
                return;
            }

            // ------------------------------------------------------------------------
            // Identify requested data
            // ------------------------------------------------------------------------

            StringBuilder ResponseStr = new StringBuilder("Respuesta recibida: ");

            //
            // Check ModBus function code received.
            //

            switch (function)
            {
                case 3:
                    ResponseStr.Append("Read holding register ");

                    //
                    // Check transaction ID (counter read).
                    //

                    if (ID == 2)
                    {
                        //
                        // Get register value.
                        //
                        // UInt16 RegisterValue = (UInt16)(((UInt16)(values[0] << 8)) | ((UInt16)(values[1])));
                        //

                        // Check architecture endianess.
                        if (BitConverter.IsLittleEndian)
                        {
                            // Reverse array values.
                            Array.Reverse(values);
                        }

                        // Get register value from received bytes.
                        UInt16 RegisterValue = BitConverter.ToUInt16(values, 0);

                        // Update text box values on windows form.
                        UpdateTextBox(RegisterValue);

                        // Read register is done, now, write register on next timer tick.
                        StartWriteTransaction = true;
                    }

                    break;
                case 6:
                    ResponseStr.Append("Write single register ");

                    //
                    // Check transaction ID (counter write).
                    //

                    if (ID == 1)
                    {
                        // Write register is done, now, read register on next timer tick.
                        StartWriteTransaction = false;
                    }

                    break;
                default:
                    ResponseStr.Append("Unknown ");
                    break;
            }

            // Show message in status bar.
            ResponseStr.AppendFormat("({0}) - data bytes/words ({1}/{2}).", function, values.Length, values.Length / 2);

            PrintStatusBarMessage(ResponseStr.ToString());

            // Response received, ready for next transaction.
            ReadyForTransaction = true;
        }

Cuando este evento es llamado, nos pasa 3 argumentos:

  • ID: Identificación de transacción que colocamos al realizar una operación.
  • function: Código de Función ModBus al que pertenece la repuesta.
  • values[]: Bytes recibidos de la repuesta ModBus.

Notar como el siguiente fragmento es llamado, cuando hay una repuesta de escritura:

Código: (C#) [Seleccionar]
                case 6:
                    ResponseStr.Append("Write single register ");

                    //
                    // Check transaction ID (counter write).
                    //

                    if (ID == 1)
                    {
                        // Write register is done, now, read register on next timer tick.
                        StartWriteTransaction = false;
                    }

Procesamos, si es "function=6" (repuesta de una transacción Write single register) y "ID=1" (lo establecimos en WritePlcCounterRegister(), hacemos la variable StartWriteTransaction=false para que en el próximo evento timer en timerPolling_Tick() realicemos una operación de lectura.

En la repuesta de lectura, luego de llamar a función "ReadPlcCounterRegister()" se entra al siguiente fragmento de código:

Código: (C#) [Seleccionar]
                case 3:
                    ResponseStr.Append("Read holding register ");

                    //
                    // Check transaction ID (counter read).
                    //

                    if (ID == 2)
                    {
                        //
                        // Get register value.
                        //
                        // UInt16 RegisterValue = (UInt16)(((UInt16)(values[0] << 8)) | ((UInt16)(values[1])));
                        //

                        // Check architecture endianess.
                        if (BitConverter.IsLittleEndian)
                        {
                            // Reverse array values.
                            Array.Reverse(values);
                        }

                        // Get register value from received bytes.
                        UInt16 RegisterValue = BitConverter.ToUInt16(values, 0);

                        // Update text box values on windows form.
                        UpdateTextBox(RegisterValue);

                        // Read register is done, now, write register on next timer tick.
                        StartWriteTransaction = true;
                    }

Aquí obtenemos el registro leído en RegisterValue (convertimos de valor de red a valor de computadora de 16-bits) y luego actualizamos las cajas de texto de la ventana con la función UpdateTextBox().

Al final del evento se hace la variable ReadyForTransaction igual a true indicando que la aplicación esta lista para una nueva transacción ya que recibimos el dato que esperábamos (esto es simplemente como lógica en nuestro ejemplo, para no enviar una transacción nueva sí no recibimos la enviada previamente).

Código: (C#) [Seleccionar]
            // Response received, ready for next transaction.
            ReadyForTransaction = true;
        }



Aún la librería no esta publicada en la página web oficialmente por estar en desarrollo, por ello cualquier duda, pueden utilizar nuestro foro.

Abrir los ejemplos para el PLC con StxLadder 1.8.9 o superior.

Saludos!
« Última Modificación: junio 05, 2018, 13:05:28 pm por Soporte »
SOPORTE TÉCNICO

Slicetex Electronics
www.slicetex.com

Atención: Desde el 1 de enero al 25 de enero de 2019, cerramos por vacaciones.
Tenga en cuenta para sus pedidos y/o consultas.

Soporte

  • Global Moderator
  • Experto
  • *****
  • Mensajes: 1922
  • Soporte Técnico
Re:Comunicación ModBus TCP con PLC utilizando Visual C# (librería)
« Respuesta #1 : junio 05, 2018, 13:03:51 pm »
A continuación adjuntamos el ejemplo "MbTcpServerCountOut.zip" para el PLC que a diferencia del ejemplo "MbTcpServerCounter.zip" conmuta una salida a transistor cada vez que el registro ModBus 40001 es puesto a 0.

Requiere un PLC con salida PWM o transistor rápida.

Esto permite medir tiempos mediante un osciloscopio o analizador lógico en la salida.

Capturas tomadas en salida digital (la duración de cada pulso  alto o bajo es el tiempo entre escritura lectura):





Log tomado desde aplicación Visual C# (sin relación a capturas previas):

Código: (log.txt) [Seleccionar]
Response: 915 ms
Response: 934 ms
Response: 20 ms
Response: 30 ms
Response: 76 ms
Response: 97 ms
Response: 139 ms
Response: 161 ms
Response: 21 ms
Response: 31 ms
Response: 84 ms
Response: 95 ms
Response: 140 ms
Response: 161 ms
Response: 205 ms
Response: 218 ms
Response: 271 ms
Response: 282 ms
Response: 327 ms
Response: 347 ms
Response: 389 ms
Response: 411 ms
Response: 458 ms
Response: 469 ms
Response: 522 ms
Response: 532 ms
Response: 10 ms
Response: 31 ms
Response: 9 ms
Response: 24 ms
Response: 76 ms
Response: 86 ms
Response: 133 ms
Response: 154 ms
Response: 10 ms
Response: 30 ms
Response: 20 ms
Response: 31 ms
Response: 79 ms
Response: 89 ms
Response: 141 ms
Response: 152 ms
Response: 205 ms
Response: 217 ms
Response: 21 ms
Response: 32 ms
Response: 13 ms
Response: 33 ms
Response: 76 ms
Response: 97 ms
Response: 142 ms

« Última Modificación: junio 05, 2018, 13:33:40 pm por Soporte »
SOPORTE TÉCNICO

Slicetex Electronics
www.slicetex.com

Atención: Desde el 1 de enero al 25 de enero de 2019, cerramos por vacaciones.
Tenga en cuenta para sus pedidos y/o consultas.