PLC con Cortex-M0 (parte 1)

Desde hace muchos años he querido construir un PLC. Tomé un curso de 6 meses, pero nunca lo ejercí “profesionalmente”, por lo que mis conocimientos acerca de su utilización son más teóricos que prácticos.

La idea es retomar este proyecto y comenzarlo desde ya. Pero quiero aclarar que no pretendo armar una “tarjeta de relés”. Una tarjeta de relés no es más que un conjunto de relés y optoacopladores, y quizás algún microcontrolador de 8 bits fungiendo como cerebro.

En muchos sentidos un PLC es más que una tarjeta de relés . El PLC:

  • Tiene un hardware robusto
  • Tiene un firmware robusto
  • Se programa con varios lenguajes
  • Implementa protocolos de comunicación específicos del ramo

Y ninguno de estos aspectos se puede tomar a la ligera si se desea que el PLC por construir se tome en forma seria.

Hardware

El hardware del PLC debe ser diseñado de tal manera que soporte el ambiente hostil de las industrias, a lo que también se le conoce como “hardware de grado industrial”. Algunas características de este tipo de hardware son:

Inmunidad al ruido electromagnético producido por las salidas propias del PLC, así como el ruido producido por motores, líneas de transmisión, fuentes de radiofrecuencia, etc.

Watchdog. El PLC debe ser capaz de reiniciarse ante cualquier “colgado” del programa principal. Estos colgados (el programa entra a un ciclo infinito no deseado) se dan por dos situaciones: a) Errores de programación, b) El ruido electromagnético corrompe a los registros del CPU. En cualquier caso, el PLC debe poder detectar esta situación, y en consecuencia, tomar las medidas necesarias, entre las cuales figuran reiniciarse y/o llegar a un estado seguro y quedarse ahí.

Se menciona al watchdog aquí y en la sección de firmware ya que éste puede ser implementado tanto en hardware como en software. En el primer caso se agrega al sistema un circuito supervisor externo (WD), ya sea discreto o integrado. Un pin del microcontrolador se configura como “de salida” y se conecta a la entrada “feed” del WD. Y a su vez, la salida “Reset” del WD se conecta ya sea al pin de reset del MPU, o a algún pin que genere una interrupción por eventos externos (IRQ).

La MPU entonces  “alimenta” al WD en forma regular dentro de un tiempo establecido. En el supuesto de un colgado,  la MPU dejará de alimentar al WD y por lo tanto se generará un reset.

Variaciones de voltajes. Las variaciones de voltaje propias y comunes en los ambientes industriales pueden hacer, como se mencionó anteriormente, que los registros internos del CPU se corrompan y el programa “se pierda” o “se cuelgue”. Por ello se requiere que la fuente de alimentación del PLC sea robusta ante las variaciones de voltajes, y que entregue una tensión limpia y estable.

Temperaturas extremas. El PLC seguramente controlará refrigeradores y hornos. Las temperaturas alrededor de estos procesos están muy por arriba o por abajo del rango de una CPU convencional. Por esto, los componentes que  integren al hardware deberán ser escogidos cuidadosamente  para que en conjunto funcionen de manera correcta en temperaturas extremas.

Microcontrolador LPC1227

Este es un microcontrolador de 32 bits con núcleo Cortex-M0. ¿Qué lo hace tan especial cuando existen en el mercado cientos de otras MPUs con el mismo núcleo?

El fabricante, NXP, asegura que este MCU ha sido diseñado especialmente para aplicaciones industriales y electrodomésticos. Sin embargo, antes de mencionar sus principales características, creo que es conveniente aclarar porqué se debería utilizar un MCU de 32 bits en lugar de uno de 8. La respuesta es simple: el más humilde MCU de núcleo Cortex-M0 es mucho mejor que el más avanzado MCU de 8 bits. Son mejores en eficiencia (más operaciones por unidad de tiempo) y en densidad de código (más operaciones con menos código generado), además de otros puntos importantes (por ejemplo, periféricos avanzados).

Las principales características del microcontrolador LPC1227 son:

  • Núcleo de 32 bits
  • Memoria FLASH/RAM de 128K/8K
  • Velocidad máxima del núcleo: 45MHz
  • Watchdog de ventana y con clock independiente (cumple con el estándar IEC-60730)
  • 4 GPIOS de alta corriente
  • GPIOS con filtro programable
  • 2 UARTS (con capacidad de RS485 e IrDA)
  • IIC de alta corriente
  • DMA
  • CRC
  • División por hardware
  • ADC de 10 bits y 8 canales
  • 2 comparadores analógicos de 6 canales cada uno

¿Y el USB? Lo mismo me pregunté, pero ¿qué aplicación industrial o electrodoméstico requeriría USB? En conjunto se tienen 2 UARTs, 1 RS485 y 2 IrDA, todas multiplexadas en los canales UART0 y UART1. En este tipo de aplicaciones industriales se necesitan conectar PLCs en red; USB no lo permitiría, pero el RS485, IrDA y la IIC, sí.

Hablando de la IIC, este MCU incluye un módulo mejorado que permite conectar más dispositivos al bus, y alcanzar distancias más grandes, además de ser más rápido.

Firmware

El siguiente aspecto a considerar es el firmware del PLC. Debe existir una especie de sistema operativo (SO) que controle todo el funcionamiento interno del dispositivo, así como un procedimiento dedicado para ejecutar el ciclo de trabajo de cualquier PLC (verificación, lectura de las entradas, procesamiento, actualización de las salidas, verificación … etc).

El SO debe ser robusto en el sentido de no fallar él mismo, y en caso de que esto suceda, que se pueda recuperar sin ponerse en peligro a sí mismo, o al proceso que está controlando.

El SO puede fallar debido a varias circunstancias:

  • El SO tiene errores internos de programación
  • El programador cliente del SO introdujo fallas
  • El sistema se quedó sin memoria
  • Se disparan excepciones ante eventos no esperados como manejadores de interrupciones no definidos, o se pretendió escribir en zonas prohibidas, o la pila (stack) creció fuera de control y corrompió la zona de RAM del SO o del programa de usuario, etc.
  • El programá se colgó y no se había implementado un watchdog

También hay que tomar en cuenta el escenario en el que el reloj principal sistemadeja de funcionar. En ambientes ruidosos puede suceder que la señal que va del cristal hacia el MPU sea muy débil y se corrompa con facilidad, tanto así que el sistema se quede sin señal reloj. Sin importar la causa, sería un desastre que el PLC se detuviera en medio de una operación que pudiera poner en peligro tanto la infraestructura de una empresa, o el electrodoméstico, o vidas humanas. Sin embargo, para estos casos casi todas las MPU incluyen:

  • Un reloj interno
  • Un mecanismo que detecta si el reloj principal se salió de sincronía, tiene fluctuaciones, o si dejó de funcionar.

De esta manera, en el evento de falla del reloj, el PLC debería detectar la situación, comenzar a utilizar el reloj interno, avisar, e irse y quedarse en un estado seguro. Normalmente cuando falla el reloj principal el propio MPU, en forma automática, se cambia al reloj interno, y si así lo queremos, y deberíamos, el PLC debe ser programado para que genere  una interrupción que avise de ese evento para entonces tomar las medidas correspondientes.

Un siguiente escenario es cuando el programa del PLC “se cuelga” o “se pierde”. Ya mencioné esta situación anteriormente, entonces lo que voy a discutir son los mecanismos  para recuperarse de dicha situación.

El funcionamiento del WD fue mencionado brevemente en la sección de hardware; y aunque todos los MPU incluyen un WD intern0, no todos son iguales. Algunos requisitos para WDs que operarán en ambientes industriales o en electrodomésticos son:

  • Deben de funcionar con un reloj independiente al resto del sistema
  • Deben ser de “ventana” (windowed watchdogs)
  • Una vez activado no podrán desactivarse, sino hasta el siguiente reset del sistema
  • Si la secuencia de alimentación no es la correcta, entonces se disparará un reset

En muchos MPUs de gama baja (8 bits) el WD está conectado al reloj principal del sistema. Pero ¿qué sucedería en caso de que este reloj principal fallara? Pues el WD también fallaría. Por eso se requiere que el WD funcione con un reloj independiente. Este reloj es interno al MPU, está construído alrededor de un circuito RC (lo que implica que no requiere un cristal externo), es de baja velocidad, y seguiría funcionando en algunos modos de bajo consumo.

Un WD convencional requiere ser “alimentado” dentro de un intervalo de tiempo definido. Suponiendo que el WD esté programado a un tiempo T, entonces se le puede alimentar en todo el rango de tiempo que va desde 0 (cero) hasta T-1. Si por alguna razón el programa se queda colgado en un ciclo, y dentro de este ciclo se alimenta al WD muy pronto, el sistema no detectará ninguna falla, dado que el WD sigue aliméntandose dentro del intervalo requerido.

El WD de ventana evita esta situación, ya que no permite ser alimentado ni muy pronto, ni muy tarde. En cualquiera de estos dos casos se dispararía y resetaría al sistema (o generaría una interrupción). El WD de ventana va a tener un rango de tiempo para ser alimentado, pero ya no será de 0 a T-1, como en el ejemplo anterior, sino dentro de un rango de P a T, con P>>0 (P mucho mayor que cero). Dando números: supóngamos un WD con un tiempo máximo de T=1 segundo. En el caso del WD de ventana, se necesita programar el extremo izquierdo de la ventana, digamos P=750 ms. En este caso el WD deber ser alimentado dentro de un rango que va desde 750 ms hasta los 1000 ms. Si el WD es avisado, por ejemplo, a los 300 ms (muy pronto), o a los 1100 ms (muy tarde), entonces éste disparará un reset o una interrupción (depende cómo haya sido programado). En el caso del WD convencional, siguiendo con el ejemplo de T=1000 ms, éste podría ser alimentado cuando sea, inclusive cada 1 ms, sin reportar ningún error, aún cuando evidentemente los hay.

Es importante aclarar que existen WD robustos que no sólo generan un reset cuando se les alimenta fuera del rango permitido, sino que incluyen la opción de generar una interrupción en lugar del reset. Esto es útil cuando se requieren tomar medidas precautorias ante una falla, ya que nos permitiría llevar al sistema a un estado seguro, o guardar el último estado conocido para entonces comenzar desde donde se había quedado antes del reset.

Una última característica del WD es que por seguridad no pueda ser desactivado durante el transcurso del programa. De esta manera, no podría anularse solamente durante algunas partes del código “por conveniencia”; es decir, si en alguna parte de la programación el WD “nos diera problemas” en la lógica de la aplicación, no podríamos desactivarlo durante esa rutina, y luego reactivarlo para el resto del programa.

Debo mencionar que este tipo de comportamiento del WD es un requisito de la IEC-60730 para electrodomésticos que se pretendan vender en Europa (o mejor dicho, en la Unión europea). En Estados Unidos existe un equivalente de los UL, aunque no lo recuerdo por el momento. Y en México no existen normas de tal tipo.

Aunque falta platicar sobre los lenguajes de programación del PLC, en este punto ya debe ser claro que el diseño de un PLC serio no es una tarea trivial, y que cada vez se aleja más de “una tarjeta con relés”.

En la siguiente entrada describiré al software de programación y los protocolos que el PLC debería implementar. Mientras tanto sus comentarios son importantes y valiosos.

Anuncios

9 comentarios sobre “PLC con Cortex-M0 (parte 1)

  1. Hola fjrg76, tengo un problema con un proyecto en lpc2129, y es que debo hacer que trabaje con dos interrupciones y cuando le meto el programa en el chip solo me funciona una, en la otra directamente ni entra, he visto un articulo que tienes por ahi, pero he probado y ni con esas me funciona.Te estaria muy agradecido si me pudieras ayudar.

    1. Hola Miguel

      Claro que te puedo ayudar, sólo postea tu código, o la parte que tiene que ver con interrupciones, para darme una idea de lo que puede estar fallando. En ese, y en otros micros, regularmente utilizo dos o más interrupciones. En tu código quizás sea algo menor lo que se te está pasando.

      Un saludo

  2. #include
    #include

    //definiciones del PID

    #define epsilon 2 // epsilon es una cte que indica que entre el T0MR1 actual Y EL T0MR1 anterior debe ser el valor de esa cte
    #define dt 0.001 //10ms es el tiempo de ciclo que tiene para dar la respuesta el PID 1 ciclo=1khz=0.001
    #define MAX 1023 //valor limite superior del PID
    #define MIN 0 // valor limite inferior del PID
    #define Kp 2 // cte de proporcionalidad del PID
    #define Kd 0.0002 // cte de derivacion del PID
    #define Ki 0.005 // cte de integracion del PID

    //configuracion general

    int val;
    int canal;
    static int val_ADC;
    static int val_POT;
    static int AUX_T0MR1;

    //inicializo variables

    val=0;
    val_ADC=0;
    val_POT=0;
    AUX_T0MR1=0;

    //inicio variables PID

    static float pre_error = 0;
    static float integral = 0;
    float error;
    float derivative;
    static int output;

    //interrupciones

    void RAI_TIMER(void) __irq; // RAI para EINT0
    void RAI_ADC(void) __irq; // RAI para EINT1

    int main(void)
    {

    //configuracion de los registros del ARM7

    VPBDIV = 0x00000001; // igualo cclk=pclk
    PINSEL0 |= 0x00000800; // P0.12 como timer 0
    PINSEL1 |= 0x01400000; // habilito el AIN0 a P0.27 y AIN1 a P0.28
    IODIR0 |= 0x00001000; //configuro como salida el P0.12
    IOCLR0 = 0x00001000;
    IOPIN0 |= 0x18000000; //configuro como entrada el P0.27 y P0.28
    IOSET0 = 0x18000000; //configuro como entrada el P0.27 y P0.28
    T0PR = 0x0000003B; // preescalado a 59 tick (hecho con calculos) con 60 Mhz
    T0TCR = 0x00000002; // Reseteo counter y preescaler.Reseteo el contador del reloj
    T0MCR = 0x0000020B; // genero la interrupcion 0,1 y3 y pongo a cero el contador en MR0
    T0MR0 = 1023; // declaro el TMR0 a 1024 posiciones (tiempo de ciclo)
    T0MR1 = 600; // Duty cicle del PWM a 600 de un max de 1024
    T0MR3 = 64; // El ADC tomara valor cada 64 tick del ciclo

    T0TCR = 0x00000001; // habilitamos el timer

    //configuracion del ADC

    ADCR = 0x0D217703; // setup A/D: AIN0 Y AIN1=> ADCR(07.00) = 0000 0011
    // TIEMPO DE CICLO=> CLKDIV= (60MHZ/0.5MHZ)-1 = 119 => ADCR(15.8)= 0111 0111
    // MODO BURST (en modo repeticion) => ADCR(16) = 1
    // CONVERSION A 10 BITS => ADCR(19.17) = 000
    // MODO OPERACIONAL => ADCR(21) = 1
    // NORMAL OPERATION => ADCR(23.22) = 00
    // SELECCIONO A CANAL MAT3 => ADCR(26.24) = 101
    // SELECCION ACTIVAR A FLANCO DE BAJADA => ADCR(27) = 0

    //configuracion interrupcion del ciclo

    VICVectAddr0 = (unsigned) RAI_TIMER; // direccion de la RAI
    VICVectCntl0 = 0x00000024; // selecciona canal 4, slot 7
    VICIntEnable |= 0x00000010; // habilita INT

    //configuracion interrupcion del ADC

    VICVectAddr1 = (unsigned) RAI_ADC; // RAI_ADC en slot 0
    VICVectCntl1 = 0x00000032; // asigna slot 11 al canal 18
    VICIntEnable |= 0x00040000; // habilita canal 18 del VIC

    while(1);

    }

    void RAI_TIMER(void) __irq //interrupcion del ciclo

    {

    if(T0IR&0x01)//MR0
    {

    T0MR1 = AUX_T0MR1; //doy el nuevo dutycicle a la salida del transistor
    IOSET0 = 0x00001000; //pongo a 1 el pin de salida del transistor por el puerto P0.12
    T0MR1 = (val_ADC>>4); // dividimos por 16 el valor del adc en el ciclo entero
    AUX_T0MR1 = PIDcal(T0MR1); // hago PID del nuevo duty cicle
    val_ADC = 0; //pongo a cero el valor del ADC
    T0MR3 = 64; //actualizo de nuevo a otros 64 tick la toma de datos del ADC
    T0IR = 0x00000001; // borro flag INT

    }

    if(T0IR&0x02)//MR1
    {

    IOCLR0 = 0x00001000; //pongo a 0 el pin de salida del transistor por el puerto P0.12
    T0IR = 0x02; //borro flag INT

    }
    if(T0IR&0x08) //MR3
    {
    ADCR |= 0x01000000; //arranco ADC para una nueva conversion
    T0MR3 = T0MR3 + 64; // actualizo el ciclo del ADC en otros 64 tick mas
    T0IR = 0x08; //borro flag INT

    }
    VICVectAddr = 0x00000000; // borramos el contador de interrupciones

    }

    int PIDcal(int actual_position) // funcion del PID
    {

    //Calculamos el error entre el valor que marca el potenciometro y el del T0MR1

    error = val_POT – actual_position;

    //Si el error es muy pequeño la parte integral no actua ( epsilon)

    {
    integral = integral + error*dt;
    }

    // Calculamos el valor derivativo del PID

    derivative = (error – pre_error)/dt;

    // Calculamos el valor nuevo del T0MR1 con valores del PID

    output = Kp*error + Ki*integral + Kd*derivative;

    //Valores de saturacion del PID

    if(output > MAX)

    {
    output = MAX;
    }

    else if(output >24)&0x07);

    if(canal==0) //canal del pwm exterior AIN0

    {

    // Actualizacion del nuevo valor que muestra el PID

    val_POT= ((val>>6)& 0x03FF); // 0x3FC00000 enmascaro del bit 24 al 32

    }

    if(canal==1) //canal del potenciomentro AIN1

    {

    //valores para calculo del nuevo T0MR1

    val_ADC= val_ADC+((val>>6)& 0x03FF); // 0x3FC00000 enmascaro del bit 24 al 32

    }

    }

    }

  3. Al princio los includes pone:

    #include
    #include

    Te comento, uso una MCB2100, con el software keil uvision3, el programa me va perfecto en el modo simulacion, pero al volcarlo al micro y simular sobre el, no me hace la interrupcion del ADC, no se si sera tema de que tengo que configurar algo enel startup, o algun pequeño fallo, porque estoy harto de mirar el manual y las interrupciones creo que estan bien configuradas, vamos que no veo porque, y ando un poco desesperado.

    El programa lo que hace es con una interrupcion genera un ancho de pulso , con MR0 y MR1 genera los flancos de subida y bajadas del puerto , y luego por otro lado con MR3 genera los flags en los cuales mide el ADC, hasta hay bien. Pero la interrupcion del ADC en la cual tiene que saltar cuando el ADC tenga la conversion lista,no la hace nunca.

  4. #include
    #include

    //definiciones del PID

    #define epsilon 2
    #define dt 0.001
    #define MAX 1023
    #define MIN 0
    #define Kp 2
    #define Kd 0.0002
    #define Ki 0.005

    //configuracion general

    int val;
    int canal;
    static int val_ADC;
    static int val_POT;
    static int AUX_T0MR1;

    //inicializo variables

    val=0;
    val_ADC=0;
    val_POT=0;
    AUX_T0MR1=0;

    //inicio variables PID

    static float pre_error = 0;
    static float integral = 0;
    float error;
    float derivative;
    static int output;

    //interrupciones

    void RAI_TIMER(void) __irq;
    void RAI_ADC(void) __irq;

    int main(void)
    {

    //configuracion de los registros del ARM7

    VPBDIV = 0x00000001;
    PINSEL0 |= 0x00000800;
    PINSEL1 |= 0x01400000;
    IODIR0 |= 0x00001000;
    IOCLR0 = 0x00001000;
    T0PR = 0x0000003B;
    T0TCR = 0x00000002;
    T0MCR = 0x0000020B;
    T0MR0 = 1023;
    T0MR1 = 600;
    T0MR3 = 64;

    T0TCR = 0x00000001;

    //configuracion del ADC

    ADCR = 0x0D217703;

    //configuracion interrupcion del ciclo

    VICVectAddr0 = (unsigned) RAI_TIMER;
    VICVectCntl0 = 0x00000024;
    VICIntEnable |= 0x00000010;

    //configuracion interrupcion del ADC

    VICVectAddr1 = (unsigned) RAI_ADC;
    VICVectCntl1 = 0x00000032;
    VICIntEnable |= 0x00040000;

    while(1);

    }

    void RAI_TIMER(void) __irq

    {

    if(T0IR&0x01)//MR0
    {

    T0MR1 = AUX_T0MR1;
    IOSET0 = 0x00001000;
    T0MR1 = (val_ADC>>4);
    AUX_T0MR1 = PIDcal(T0MR1);
    val_ADC = 0;
    T0MR3 = 64;
    T0IR = 0x00000001;

    }

    if(T0IR&0x02)//MR1
    {

    IOCLR0 = 0x00001000;
    T0IR = 0x02;

    }
    if(T0IR&0x08) //MR3
    {
    ADCR |= 0x01000000;
    T0MR3 = T0MR3 + 64;
    T0IR = 0x08;

    }
    VICVectAddr = 0x00000000;

    }

    int PIDcal(int actual_position)
    {

    error = val_POT – actual_position;

    if(abs(error) > epsilon)

    {
    integral = integral + error*dt;
    }

    derivative = (error – pre_error)/dt;

    output = Kp*error + Ki*integral + Kd*derivative;

    if(output > MAX)

    {
    output = MAX;
    }

    else if(output >24)&0x07);

    if(canal==0)

    {

    val_POT= ((val>>6)& 0x03FF);

    }

    if(canal==1)

    {

    val_ADC= val_ADC+((val>>6)& 0x03FF);

    }

    }

    }

    Este es el codigo sin comentario, por si se ves mas claro el programa

  5. void RAI_ADC(void) __irq
    {

    if(ADDR&0x80000000)

    {
    val=ADDR;
    canal=((val>>24)&0x07);

    if(canal==0)

    {

    val_POT= ((val>>6)& 0x03FF);
    }

    if(canal==1)

    {
    val_ADC= val_ADC+((val>>6)& 0x03FF);

    }

    }

    }

    Esta es la funcion de interrupcion, que se ha cortado las dos veces

  6. Hola,

    Voy a probar un código similar al tuyo en una de mis tarjetas (Olimex-LPC2129). Mientras tanto permíteme hacerte algunas observaciones:

    Tu código:
    IODIR0 |= 0×00001000; //configuro como salida el P0.12
    IOCLR0 = 0×00001000;
    IOPIN0 |= 0×18000000; //configuro como entrada el P0.27 y P0.28
    IOSET0 = 0×18000000; //configuro como entrada el P0.27 y P0.28

    Mi recomendación:
    IODIR0 = 0×00001000;
    IOCLR0 = 0×00001000;

    Tu código:

    T0MR1 = 600; // Duty cicle del PWM a 600 de un max de 1024
    T0MR3 = 64; // El ADC tomara valor cada 64 tick del ciclo
    T0TCR = 0×00000001; // habilitamos el timer 0)
    tim_delay–;

    T1IR=(1<<3);

    }

    /*
    * El tick está implementado sobre T1MR3
    */
    void tick_init(void)
    {
    /*
    * detiene al timer
    */
    T1TCR=2;

    T1MCR|=(3<<9);

    /*
    * Base de tiempo de 1us=58982400Mhz/1.0Mhz
    */
    T1PR=59;

    /*
    * el tick está corriendo a 1ms=1us*1000
    */
    T1MR3=1000;

    VICIntSelect&=~(1<<5); /* IRQ */
    VICIntEnable|=(1<<5); /* En */
    VICVectCntl0=(5)|(1<<5);
    VICVectAddr0=(unsigned long) tick_isr;

    T1IR=(1<<3);

    T1TCR=1;
    }

    Por el momento eso es lo que he visto en tu código. En cuanto termine mi experiencia te envío mis resultados.

    1. Es cierto, esta cosa corta los mensajes. Te escribí mucho más pero ya se perdió =( Por favor dame tu correo para poder platicar largo y tendido, porque por aquí no se va a poder.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s