miércoles, 24 de julio de 2024

Programacion por Estados con Temporizador

 Programación por Estados con Temporizador


Este sitio fue creado para compartir el mutuo interés en la electrónica y en especial la programación de microcontroladores PIC. Agradecido por visitar mi blog te quiero recordar que estoy atento a critica constructiva relacionada con esta publicación. 

Hoy quiero explicar como lograr de forma practica y sencilla la concurrencia de tareas dentro del bucle secuencial de un programa, para lo cual haremos uso del temporizador con interrupción, en microcontroladores PIC y AVR.

Para el caso de un PIC, la programación se realiza utilizando el software MPLABX junto con el compilador XC8 ambos disponibles en la pagina oficial de microchip y en el caso de microcontrolador AVR, utilizaremos la plataforma Arduino dada la facilidad y el amplio uso que tiene.

Mencionar también que en la presente publicación de da por entendido que el visitante o lector, posee medios de programación en lenguaje C y experiencia en el uso de microcontroladores PIC / AVR

Introducción

Dado que los microcontroladores disponen de un microprocesador para ejecutar las instrucciones del programa almacenadas en su memoria, el desarrollo de software se lleva a cabo principalmente con la programación estructurada secuencial, esto es básicamente ejecutar en secuencia múltiples tareas, una después de otra y repitiendo este ciclo de forma indefinida, como puede ver en la figura 1.

Fig1. Ejecución secuencial de cuatro Tareas

Si bien cada tarea puede incluir sentencias selectivas e iterativas, una vez iniciada la ejecución, la tarea debe finalizar para continuar con la siguiente, a simple vista podemos mencionar que la limitaciones que conlleva esta programación se dan debido a los siguientes aspectos:

  • La ejecución de cada tarea demora un tiempo, y no siempre es constante dado que en el proceso pueden existir retardos y condiciones sujetas a eventos que son asíncronos, como notara en la figura 1. se resaltan las magnitudes de tiempo utilizada por cada tarea, siendo en total un valor de 1000 por cada ciclo de ejecución, podemos imaginar que la unidad de medida son millonésimas(us) o milésimas(ms) de segundos.
  • Durante el ciclo secuencial, una tarea deberá esperar la ejecución del resto de tareas, si el tiempo es muy largo se verán afectados aquellos eventos que requieran atención inmediata, por ejemplo la tarea 1 debe esperar un tiempo de 750(50+500+200) para iniciar nuevamente, mientras que la tarea 3, debe esperar 500(200+250+50).

Observe la figura 2 que muestra el código de un par de tareas tomadas del ejemplo, la tarea 1 esta  encargada de obtener un valor de un canal analógico y guardarlo en memoria, y como sabemos este proceso debe primero iniciar la conversion y esperar a que finalice, lo que conlleva a tener tiempos de espera., en cuanto a la tarea 2, responsable de destellar una luz LED, posee tiempos de esperar suficientes para notar el cambio de encendido y apagado.

Fig2. Ejemplo de Código Tarea 1 y Tarea 3

Un enfoque de programación alternativo que vamos a revisar, es la ejecución de tareas concurrentes pero de forma simplificada ya que los PIC/AVR que estamos revisando poseen recursos limitados. No se tocaran detalles del concepto de concurrencia, ya que es un tema bastante amplio y complejo en algunos casos debido al uso que tienen dentro de los sistemas operativos. por lo que enfocare la explicación mas a la practica. 

Entonces sobre este tema mencionare que a diferencia de la estructura secuencial ya vista con antelación, la concurrencia permite que las tareas se lleven a cabo al mismo tiempo, y una técnica para conseguir esto tomando en cuenta un sistema con un solo microprocesador, es hacer que las tareas gestionen su propio estado y el tiempo total del ciclo de ejecución sea muy corto, para que todas las tareas que se ejecutan durante el ciclo secuencial lo mas rápido posible. Observe la figura 3, donde cada una de las cuatro tareas utilizan 0.25 de tiempo, para completar la unidad 1.0.  

Fig3. Concurrencia de cuatro tareas

Se preguntara entonces, ¿Como? es posible hacer esto, y la respuesta seria:

No esperar que las tareas finalicen su trabajo en cada ciclo, y por el contrario hacer que cada tarea tenga control de su estado en base al tiempo y el numero de veces que es invocado. Un modelo de programación aplicable a esta técnica, es la maquina de estados finitos, donde la tarea se divide en estados y las transiciones entre estados están sujetas a condiciones, que se validan únicamente cuando se ejecuta la tarea, observe como seria el código de las tareas 1 y 3 considerado esta técnica. 

Fig4. Código Tarea 1 y Tarea 3

Como notara en la figura 4, ahora la tarea 1, dispone de cinco estados, cada uno de ellos realiza una actividad, lo interesante es que ya no existen esperas, por ejemplo el estado 2, que debe esperar la conversion, ahora solo revisa si la bandera de ocupado (ADC_Busy) sigue activa, y dependiendo esta validación se determina si la tarea mantiene su estado o pasa el estado siguiente. En el caso de la tarea 2, se utiliza un contador de ciclos para determinar los momentos para encender y apagar la luz, pero la tarea nunca se queda esperando.

El concepto de las Maquinas de Estados Finitos al igual que otras técnicas como GRAFCET, implican el uso de diagramas de estado y simbologías propias que no se aplicaran a nuestro ejemplo practico, por esa razón de aqui en adelante haré referencia a esta técnica como Programación por Estados con Temporizador.

Ahora veamos la practica, para conseguir que el ciclo de ejecución posea un tiempo preciso, utilizare un Temporizador y su interrupción, entonces describiré la configuracion de este recurso considerando los módulos temporizadores del PIC16F886 y el ATmega328.

Si quieres conocer mas a profundidad los detalles de los módulos TMR de un PIC16, te recomiendo que revises las siguientes entradas: 

Configuración y uso del <TMR0>  <TMR1>  del PIC16F

Temporizadores en el PIC16F

El microcontrolador PIC16F887 dispone de los siguientes módulos:

  • TMR0 Modulo Contador/Temporizador de 8-bit
  • TMR1 Modulo Contador/Temporizador de 16-bit
  • TMR2 Modulo Temporizador de 8-bit

La operación de estos módulos es similar. Si hablamos de un temporizador significa que un registro contador incrementara su valor con cada pulso proveniente del oscilador utilizado por el PIC, en el caso del TMR1, este registro es de 16-bit, siendo su capacidad de contar desde 0 hasta 65535. Para incrementar la capacidad de contar, cada modulo cuenta con etapas divisoras de frecuencia que puede ajustarse a escalas determinadas.

Cuando el registro contador llega al valor limite, con el siguiente pulso ocurrirá un desbordamiento, donde el contador se reinicia a cero, y la bandera TxIF se activara para notificar este evento, esta bandera debe ser desactivada por programa cuando se complete la atención al evento.

Ahora veremos como configurar el temporizador con el modulo TMR0 y TMR1, para lo cual consideraremos los siguiente: 

La frecuencia del oscilador Fosc = 8000000 Hz, y el tiempo base requerido para ajustar el temporizador sera Tb = 0.001 segundo (1ms)

Configuración del  TMR0

La figura 5, describe los registros específicos asociados a la operación del modulo TMR0, donde se muestran los bits y sus valores por defecto.

Fig5. Registros SFR del TMR0
El código de configuracion considerando Fosc=8MHz y Tb=1ms sera.

 OPTION_REGbits.T0CS = 0;//Modo Termporizador
 OPTION_REGbits.PSA = 0; //Con pre-escala
 OPTION_REGbits.PS = 0b011; //Pre-escala 16:1
 TMR0 = 131; //Tb=1ms TMR0=256-(0.001*Fosc)/(pre*4)
 INTCONbits.T0IF = 0; //Limpia bandera
 INTCONbits.T0IE = 1; //Habilita la interrupción del TMR0
 INTCONbits.GIE = 1; //Habilitador Global ISR

La rutina de servicio a la interrupción tendrá lo siguiente:

void __interrupt() isr() //Rutina de interrupción
{
  if(INTCONbits.T0IF)
//Bandera desbordamiento TMR0
  {
    TMR0 = 131;
//Reinicia contador
    INTCONbits.T0IF = 0; 
//Limpia bandera
  }
}

Configuración del TMR1

La figura 6, describe los registros específicos asociados a la operación del modulo TMR0, donde se muestran los bits y sus valores por defecto.

Fig6. Registros SFR del TMR1
El código de configuracion considerando Fosc=8MHz y Tb=1ms sera.
      T1CONbits.TMR1CS = 0; //Modo temporizador
   T1CONbits.T1CKPS = 0b00;
//Ajuste pre-escala 1:1
   TMR1H = 0xF8;
//Tb=1ms TMR1=65536-[(0.001*8M)/(1:1*4)]
   TMR1L = 0x30;
// 63536 F830h
   PIR1bits.TMR1IF = 0;;
//Limpia la bandera
   T1CONbits.TMR1ON = 1;
//Arranca temporizador
   PIE1bits.TMR1IE = 1; 
//Activa interrupción del T1
  
INTCONbits.PEIE = 1;//Activa interrupción de periféricos
   INTCONbits.GIE = 1;
//Activador global ISR

La rutina de servicio a la interrupción tendrá lo siguiente:

void __interrupt() isr(void) //Rutina de servicio a interrupciones

    if(PIR1bits.TMR1IF)
//Evento de desbordamiento TMR1
    {
        PIR1bits.TMR1IF = 0;
//Limpia la bandera TMR1
        T1CONbits.TMR1ON = 0;
//Para al contador
        TMR1H = 0xF8;
//Actualiza valor del contador
        TMR1L = 0x30;

        T1CONbits.TMR1ON = 1;
//Arranca al contador
    }
}
 

Temporizadores ATmega

El microcontrolador ATmega328 que es utilizado por la tarjeta Arduino UNO dispone de los siguientes módulos:

  • TC0 Contador/Temporizador de 8-bit
  • TC1 Contador/Temporizador de 16-bit
  • TC2 Contador/Temporizador de 8-bit

Cada modulo cuenta con los  modos de operación: Normal, CTC, PWM y PWM con corrección de fase. Para nuestro caso trataremos únicamente el modo Normal que representa el modo de operación mas simple donde el registro contador se incrementa con cada pulso de reloj hasta su desbordamiento. En el caso de los AVR la bandera TOVxF se reinicia automáticamente cuando se atiende el evento.

Considerando que utilizaremos una tarjeta Arduino UNO, es importante mencionar que los modulo son utilizados de acuerdo a lo siguiente:

  • TC0: Funciones delay(), millis(), micros() y PWM pines 5,6.
  • TC1: Librería se servomotores Servo.h y PWM 9,10
  • TC2: Función Tone() y PWM 3,11

Por esta razón, para no afectar la funcionalidad de Arduino solo trataremos la configuracion de los temporizadores TC1 y TC2 considerando que la frecuencia del oscilador es Fosc = 16MHz y el tiempo base Tb = 1ms

Configuración del TC1

La figura 7, describe los registros específicos asociados a la operación del modulo TC1, donde se muestran los bits y sus valores por defecto.

Fig7. Registros SFR del TC1
El código de configuracion, considerando los valores defecto sera el siguiente:

  TCCR1A = B00000000;//Registro A Operación Modo Normal
 
TCCR1B = B00000011;//Registro B Ajusta la prescala CS=1:64
 
TCNT1H = 0xFF; //Tb=1ms TCNT1=65535-(0.001*8M/64)=FF05h
 TCNT1L = 0x05;
 TIMSK1 |= 0x01;
//Activa interrupcion TOIE del T1

La rutina de servicio a la interrupción tendrá lo siguiente:

ISR(TIMER1_OVF_vect) //Evento de desbordamiento T1
{
   TCNT1H = 0xFF;
//Reinicia el valor del contador
   TCNT1L = 0x05;
}

Configuración del TC2

La figura 8, describe los registros específicos asociados a la operación del modulo TC2, donde se muestran los bits y sus valores por defecto.

Fig8. Registros SFR del TC2

El código de configuracion, considerando los valores defecto sera el siguiente:

 TCCR2A = B00000000;//Registro A Operación Modo Normal
 
TCCR2B = B00000100;//Registro B Ajusta Prescala CS=1:64
 TCNT2 = 5;
//Tb=1ms TCNT2=255-(0.001*8M/64)=05h
 TIMSK2 |= 0x01;
//Activa la interrupción TOIE del T2

La rutina de servicio a la interrupción tendrá lo siguiente:

 ISR(TIMER2_OVF_vect) //Evento de desbordamiento T2
 {
   TCNT2 = 5;
//Reinicia el valor del contador
 }

Código de Ejemplo

Como practico vamos a crear un programa que permita ejecutar tres tareas de forma concurrente, las tareas que se llevaran a cabo son:

  • Tarea 1: Destello de LED indicador de actividad (taskLED)
  • Tarea 2: Lectura ADC del canal y almacenamiento de resultado (taskADC)
  • Tarea 3: Control de velocidad para motor de pasos (taskSTM)

Realizamos el programa considerando los esquemas de circuito que se observan en la figura 9 y 10. en ambos casos del PIC y AVR utilizaremos el modulo TMR1 y TC1 respectivamente.

Tome en cuenta que los esquemas que se observan en la figuras 9 y 10, son únicamente para fines de simulación,  obviando las conexiones de alimentación y amplificadores de corriente para el motor de pasos.

Circuito y Programa del PIC16F886

Fig9. Esquema de circuito PIC para simulación con Proteus

  Codigo Proyecto MPLABX 

#pragma config FOSC=INTRC_NOCLKOUT,WDTE=OFF,LVP=OFF
#include <xc.h>
void setup(void);
void taskLED(void);
//Prototipo de función
void taskADC(void);
void taskSTM(void);
uint16_t speed = 0;
//Variable para control de velocidad
volatile uint8_t tickms = 0;
void main()
{
    setup();
    while(1)
    {
        if(tickms)
//Bandera activada en ISR cada 1ms
        {
            tickms = 0;
//Limpia la bandera
            taskLED();
//Destella el led cada segundo
            taskADC();
//Lectura ADC canal 0
            taskSTM();
//Control de pasos en motor
        }
    }
}
void __interrupt() isr()
//Rutina de interrupción
{

  if(INTCONbits.T0IF)
//Bandera desbordamiento TMR0
  {
    TMR0 = 131;
//Reinicia contador TMR1
    INTCONbits.T0IF = 0;
//Limpia bandera del TMR1
    tickms = 1;
  }
}
void setup(void)
{
    OSCCONbits.IRCF = 0b111;
//Selecciona Fosc=8MHz
    while(OSCCONbits.HTS == 0);
//Espera Fosc estable
    TRISBbits.TRISB5 = 0;
//Modo Salida LED
    TRISB &= 0b11110000;
//
Salidas B0=L1 B1=L2 B2=L3 B4=L4
    ANSEL = 0; //Deshabilita pines analógicos AN0-7
    ANSELH = 0;
//
Deshabilita pines analógicos AN8-13
    /* CONFIGURACIÓN ADC-10 Canal0 para Fosc=8Mhz*/
    ANSELbits.ANS0 = 1;
//Habilita canal AN0
    ADCON0bits.ADCS = 0b10;
//TAD=4us > 1.6us (8MHz/32)
    ADCON0bits.CHS = 0;
//Selecciona Canal 0
   
/* CONFIGURACION TMR0 1ms para Fosc=8MHz*/
    OPTION_REGbits.T0CS = 0;
//Modo Temporizador
    OPTION_REGbits.PSA = 0;
//Con pre-escala
    OPTION_REGbits.PS = 0b011;
//Pre-escala 16:1
    TMR0 = 131;
//Tb=1ms TMR0=256-(0.001*Fosc)/(pre*4)
    INTCONbits.T0IF = 0;
//Limpia bandera
    INTCONbits.T0IE = 1;
//Habilita interrupción del TMR0
    INTCONbits.GIE = 1;
//Habilitador Global ISR
}
void taskLED(void)
//Tarea para destellar el led
{
  static uint16_t cnt = 0;
  if(cnt++ > 999)
//Cada segundo
  {
    cnt = 0;
    PORTBbits.RB5 = 1;
//Activa led a 0 seg
  }
  if(cnt == 200) PORTBbits.RB5 = 0;
//Apaga led a 0.2 seg
}
void taskADC(void)
//Tarea para leer el canal ADC0
{
  static uint8_t state = 0;
  static uint16_t cnt = 0;
  uint16_t adcval;
  switch(state)
  {
    case 0:
      ADCON0bits.ADON = 1;
//Activa el modulo AD
      state = 1;
      break;
    case 1:
      ADCON0bits.GO = 1;
//Inicia la conversion AD
      state = 2;
      break;
    case 2:
      if(ADCON0bits.GO == 0)
//Espera fin conversion
      {
        adcval = ADRESL;
        adcval |= (uint16_t) (ADRESH << 8);
        adcval >>= 6;
//Corrige alineación ADRESH:ADRESL
        speed = adcval;
        ADCON0bits.ADON = 0;
//Desactiva el modulo AD
        cnt = 0;
        state = 3;
      }
      break;
    case 3:
      if(cnt > 499)
//Espera 500 ms
        state = 0;
      else cnt = cnt + 1;
      break;
  }
}
void taskSTM(void)
{
  static uint8_t state = 0, res;
  static uint16_t cnt = 0;
  cnt = cnt + 1;
  switch(state)
  {
    case 0:
//Estado de Motor Paso 1
      if(cnt > speed)
      {
        res = PORTB & 0xF0;
        PORTB = res | 0b0011;
        cnt = 0;
        state = 1;
      } break;
    case 1:
//Estado de Motor Paso 2
      if(cnt > speed)
      {
        res = PORTB & 0xF0;
        PORTB = res | 0b0110;
        cnt = 0;
        state = 2;
      } break;
    case 2:
//Estado de Motor Paso 3
      if(cnt > speed)
      {
        res = PORTB & 0xF0;
        PORTB = res | 0b1100;
        cnt = 0;
        state = 3;
      } break;
    case 3:
//Estado de Motor Paso 4
      if(cnt > speed)
      {
        res = PORTB & 0xF0;
        PORTB = res | 0b1001;
        cnt = 0;
        state = 0;
      } break;
  }
}

Circuito y Programa del ATmega328
Fig10. Esquema de circuito AVR para simulación en Proteus

  Código sketch Arduino  

volatile uint8_t tickms = 0; //Bandera para indicar 1ms
uint16_t speed = 0;
//Variable para control de velocidad
void setup()
{
  DDRB |= _BV(DDB5);
//Salida pin RB5 UNO=D13
  DDRB |= B00001111;
//Salida pines B0=L1 B1=L2 B2=L3 B3=L4
  DDRC = 0xFF;
//Salidas pines del PORTC
  DDRC &= ~_BV(DDC0);
//Entrada pin PC0
 
/*CONFIGURACION TC1 como temporizador a 1ms Fosc=16MHz*/
  TCCR1A = 0x00;
//Modo Normal
  TCCR1B = 0x03;
//Registro B Prescala CS=1:64
  TCNT1H = 0xFF;
//t=1ms TCNT1=65535-(0.001*Fosc/64)=FF05h
  TCNT1L = 0x05;
  TIMSK1 |= 0x01;
//Activa interrupción TOIE del T1
 
/*CONFIGURACIÓN ADC Canal 0, Modo Free Running a 125Khz*/
  ADMUX = 0x40;
//AVcc referencia, MUX canal 0
  ADMUX |= _BV(ADLAR);
//ADC Alineado a la izquierda ADCH:ADCL
  ADCSRA = 0x07;
//Pre 1:128 16M/128 = 125Khz
  DIDR0 |= _BV(ADC0D);
//Habilita pin en modo analógico
}
void loop()
{
  if(tickms)
//Bandera activada en ISR cada 1ms
  {
    tickms = 0;
//Limpia la bandera
    taskLED();
//Destella el led cada segundo
    taskADC();
//Lectura ADC canal 0
    taskSTM();
//Control de pasos en motor
  }
}
ISR(TIMER1_OVF_vect)
//Interrupción Desbordamiento del T1
{
  TCNT1H = 0xFF;
//Reinicia el contador
  TCNT1L = 0x05;
  tickms = 1;
//Activa la bandera
}
void taskLED()
//Tarea para destellar el led
{
  static uint16_t cnt = 0;
  if(cnt++ > 999)
//Cada segundo
  {
    cnt = 0;
    PORTB |= _BV(PORTB5);
//Activa led a 0 seg
  }
  if(cnt == 200) PORTB &= ~_BV(PORTB5);
//Apaga led a 0.2 seg
}
void taskADC()
//Tarea para leer el canal ADC0
{
  static uint8_t state = 0;
  static uint16_t cnt = 0;
  uint16_t adcval;
  switch(state)
  {
    case 0:
      ADCSRA |= _BV(ADEN);
//Activa el modulo AD
      state = 1;
      break;
    case 1:
      ADCSRA |= _BV(ADSC);
//Inicia la conversion AD
      state = 2;
      break;
    case 2:
      if((ADCSRA & _BV(ADSC))==0)
//Espera fin conversion
      {
        adcval = ADCL;
        adcval |= (ADCH << 8);
        adcval >>= 6;
//Corrige alineación ADCH:ADCL
        speed = adcval;
        ADCSRA &= ~_BV(ADEN);
//Desactiva el modulo AD
        cnt = 0;
        state = 3;
      }
      break;
    case 3:
      if(cnt > 499)
//Espera 500 ms
        state = 0;
      else cnt = cnt + 1;
      break;
  }
}
void taskSTM()
{
  static uint8_t state = 0, res;
  static uint16_t cnt = 0;
  cnt = cnt + 1;
  switch(state)
  {
    case 0:
//Estado de Motor Paso 1
      if(cnt > speed)
      {
        res = PORTB & 0xF0;
        PORTB = res | B00000011;
        cnt = 0;
        state = 1;
      } break;
    case 1:
//Estado de Motor Paso 2
      if(cnt > speed)
      {
        res = PORTB & 0xF0;
        PORTB = res | B0110;
        cnt = 0;
        state = 2;
      } break;
    case 2:
//Estado de Motor Paso 3
      if(cnt > speed)
      {
        res = PORTB & 0xF0;
        PORTB = res | B1100;
        cnt = 0;
        state = 3;
      } break;
    case 3:
//Estado de Motor Paso 4
      if(cnt > speed)
      {
        res = PORTB & 0xF0;
        PORTB = res | B1001;
        cnt = 0;
        state = 0;
      } break;
  }
}

Conclusiones 

Como recomendación, considerar los siguientes aspectos:
  • Hay una relación entre el tiempo base (Tb) y el tiempo utilizado en cada ciclo, este ultimo va depender de la cantidad de tareas y las instrucciones que se ejecutan a nivel individual, donde la suma total de tiempo no debe ser mayor al tiempo base. 
  • Si utiliza un tiempo base menor a 1ms, el MCU debe poseer la capacidad de ejecutar las instrucciones de todas las tareas en un ciclo, este punto se puede garantizar incrementando la frecuencia del oscilador, pero hay limites que deben ser revisados.
  • Si utiliza un tiempo de base mayor a 1ms, no habrá inconvenientes con la ejecución, pero debe contar que incrementar el tiempo base, hace que el sistema sea menos sensible, y podrían ocasiones algunos inconvenientes en la tratamiento de los puertos E/S.
Abajo dejare un breve vídeo que muestra el funcionamiento del programa, en ambos casos utilice una placa de pruebas donde la conexión al motor de 5V se realizo con un driver basado en el amplificador ULN2003, el montaje del circuito se observa en las siguientes imágenes.
 
Fig11. Montaje del circuito con ATmega328

Fig12. Montaje del circuito con PIC16F



Repositorio git con ejemplo sencillos que utilizan la ejecución de tareas concurrentes con temporizador: https://github.com/pablinza/elt436
Sin mas que mencionar agradezco tu visita al blog y espero que el ejemplo visto pueda ser útil en tu formación y el proyecto que desarrollas.
Atte. Pablo Zárate Arancibia
email: pablinza@me.com / pablinzte@gmail.com, @pablinzar
Santa Cruz de la Sierra - Bolivia

No hay comentarios.:

Publicar un comentario