miércoles, 26 de febrero de 2020

Menu LCD con Encoder Rotativo

Menú LCD utilizando Encoder Rotativo


Muchas gracias por tu visita a este blog que trata acerca de la programación de micro-controladores PIC, en esta ocasión veremos como elaborar un pequeño menú de selección para una pantalla LCD utilizando un encoder rotativo para la navegación.

Se dará por entendido que ya se cuenta con un dominio medio avanzado de programación en lenguaje C y electrónica en general por lo que la presente entrada se enfocara directamente en el código de ejemplo sin profundizar los conceptos teóricos de base. La programación se realizara utilizando el compilador XC8 y el entorno de desarrollo MPLABX, ambos disponibles de forma gratuita en la pagina de microchip.
 
El código principal del proyecto main.c referencia a funciones del LCD que se encuentran en el archivo local lcd.c, todos los archivos mencionados se encuentran dentro de la carpeta del proyecto MPLABX y puedes descargarlo en el enlace que dejare al final.

1- ESQUEMA DE CIRCUITO



Se utilizara los pines RD0-RD3 para enviar datos en modo 4-bit a la pantalla LCD, los pines RD4(RS)-RD5(EN) para señal de control, el pin RW estará conectado de forma permanente a nivel bajo esto porque utilizaremos la pantalla únicamente para mostrar información. Adicional a esto el pin RD6 permite activar la luz de fondo mediante un transistor Bipolar.
El Encoder rotativo requiere el uso de resistencias pull-ups para establecer los niveles lógicos validos, por este motivo se emplearan las resistencias internas que posee el PORTB, además también se agregaran los capacitores de desacople para minimizar el efecto de reboote. La siguiente imagen muestra el montaje final utilizando la placa de pruebas B8P40.


El siguiente diagrama nos dará una idea general de como funciona el programa del microcontrolador, básicamente se implementara dentro del bucle principal procedimientos separados para manejar el destello de un LED cada medio segundo, la lectura del Pulsador del encoder y el manejo del menú en pantalla en función del encoder. Para conseguir que esto trabaje en un mismo bucle se aplicara el concepto de una maquina de estado finito para cada procedimiento con la siguiente designación: TaskLED1, TaskBUT1, TaskMENU1.


Como referencia de tiempo para cada repetición del bucle utilizaremos un retardo de una milésima de segundo, esto nos permitirá un control de tiempo para el resto del programa. También es importante mencionar que la gestión de tiempo en este ejemplo no posee un alto grado de precisión debido que el uso de __delay_ms(1) no incluye los ciclos de maquina adicionales para el manejo del bucle y llamadas internas dentro de este. Se puede mejorar la precisión utilizando una interrupción del temporizador sincronizada con una referencia externa RTC o GPS pero es un tema que no se considera en esta sección.

Ahora trataremos de describir el programa en varias secciones:

2- INICIO Y CONFIGURACIÓN

Las siguientes lineas corresponden al encabezado que incluye las librerías y definiciones utilizadas en el resto del programa, los comentarios de cada linea nos ayudaran a comprender mejor el funcionamiento. 

#include <xc.h>
#include <stdio.h>
#include <stdlib.h>
#define _XTAL_FREQ 4000000     //Definición del reloj interno para Tcy = 1uS
#define LED1pin PORTEbits.RE2 //Pin del LED1
#define BUT1pin PORTBbits.RB0 //Pulsador del Encoder
#define ENCApin PORTBbits.RB1 //Señal A(DATO) del Encoder
#define ENCBpin PORTBbits.RB2 //Señal B(CLK) de Enconder
#define KEYUP 1
#define KEYDOWN 2
#define LCD_PORT PORTD //Puerto de datos para LCD
#define LCD_TRIS TRISD    //Registro de configuración LCD
#define LCD_RS PORTDbits.RD4 //Pin de control RS del LCD
#define LCD_EN PORTDbits.RD5 //Pin de control EN del LCD
#define LCD_BACKLIGHT PORTDbits.RD6 //Control de luz de fondo LCD
#include "lcd.h"

La lista de variables a utilizar se declara de la siguiente manera: 

enum led {LEDOFF, LEDON} led1st; //Lista de estados para el LED
enum but {RELEASE, PRESSED} but1st; //Lista de estados para el BUT
enum men {NORMAL, AJUSTE, RETARDO} men1st; //Lista de estados MENU
volatile char keysense = 0; //Variable de captura para el encoder
char menusel = 0;      //Variable para guardar el indice actual del menú

char but1OK = false; //Variable de captura para el pulso BUT en RB0
char lcdtext[20], hora = 12, min = 0, sec = 0; //Variables para control de hora
unsigned int seccnt = 0; //Contador de segundos


El inicio de los puertos y variables antes del bucle del programa principal contiene las siguientes lineas:

ANSEL = 0; //Deshabilita pines analogicos AN0-AN7
ANSELH = 0; //Deshabilita pines analogicos AN8-AN13
TRISEbits.TRISE2 = 0; //Configura como salida pin LED1
LED1pin = 0; //Apaga el LED1
TRISB = 0xFF; //Configura como entrada los pines del PORTB
EnablePU(); //Activa las resistencias pull-up del PORTB
TRISD = 0; //Configura todo el PORTC como salida
PORTD = 0; //Coloca en nivel bajo todos los pines del PORTD
led1st = LEDOFF; //Estado inical del LED1
but1st = RELEASE; //Estado inicial del pulsador BUT1
men1st = NORMAL; //Estado inicial del Menú LCD
__delay_ms(1000);
LCDSetup(LINES2); //Inicia el modulo LCD a dos lineas
LCDPuts(" L.C.D. PIC16F "); //Escribe el mensaje en la primera linea
LCDGotoln(1);//Cambia el cursos a la segunda linea LCD
IOCBbits.IOCB1 = 1; //Habilita la interrupción por cambio RB1(DATO)
INTCONbits.RBIE = 1;//Habilita la interrupción por cambio de estado PORTB
INTCONbits.GIE = 1; //Habilita la interrupción global.


Se hará uso de la interrupción por cambio de estado unicamente en el pin RB1 que corresponde a la linea A(DAT) del encoder, entonces cada vez que se gire la perilla en cualquier sentido habrá un cambio de nivel en este pin que se atenderá inmediatamente en la rutina de interrupción para determinar el sentido de giro, en nuestro caso vamos definir cada sentido como KEYUP(hacia arriba) y KEYDOWN(hacia abajo), esta definición es utilizada para la navegación en el menú de opciones.


3- DESTELLO DEL LED

Aquí explicaremos como trabaja el procedimiento TaskLED1 que es llamado en el bucle del programa principal cada milésima de segundo, para esto observe la siguiente imagen que nos muestra un diagrama de transiciones para los dos posibles estados que tiene este LED.
En este caso el estado del LED depende unicamente de un contador cnt que se incremente con cada llamada(1ms), entonces cuando el valor de este contador es de 500 que en tiempo suma unos 0.5 segundos se efectuá un cambio de estado, logrando con esto un destello continuo del LED. Abajo puede observar el código de este procedimiento y se dará cuenta que puede modificar fácilmente estos tiempos, incluso puede crear un destello con tiempos de encendido y apagado de led asimétricos.


void TaskLED1()
{
   static unsigned int cnt = 0;
   switch(led1st)
   {
       case LEDOFF:
       {
          cnt = cnt + 1;
          if(cnt == 500)
          {
             cnt = 0;
             LED1pin = 1; //Activa el LED
             led1st = LEDON;
          }
       } break;
       case LEDON:
      {
         cnt = cnt + 1;
         if(cnt == 500)
         {
            cnt = 0;
            LED1pin = 0; //Apaga el LED
             led1st = LEDOFF;
          }
       } break;
   }
}



4- LECTURA DEL PULSADOR
  
Ahora explicaremos como trabaja el procedimiento TaskBUT1, sobre el mismo principio de llamar al procedimiento cada milésima de segundo, en este caso hay dos estados posibles, que el pulsador sea presionado(PRESSED) y que el pulsador este liberado(RELEASED), en este caso el cambio de estado va depender del valor en la entrada del pulsador BUT1 y los tiempos mínimos para cada estado se controlan con el contador cnt, la idea de esto es minimizar el efecto de rebote que posee un pulsador mecánico, además para la confirmación se emplea una variable bandera but1OK que se activara únicamente si el pulsador se presiona y se libera respetando los tiempos mínimos de control.

Observe el código del procedimiento y notara que puede hacer ajustes de tiempo para la confirmación, además la bandera but1OK es una variable global que se utilizara en otra sección del programa principal.

void TaskBUT1(void)
{
    static unsigned int cnt = 0;
    switch(but1st)
    {
        case RELEASE:
       {
           if(BUT1pin == 0)
           {
              cnt = cnt + 1;
             if(cnt > 200)
             {
                but1st = PRESSED;
                cnt = 0;
             }
           }
            else cnt = 0;
       } break;
       case PRESSED:
       {
          if(BUT1pin)
          {
             cnt = cnt + 1;
             if(cnt > 200)
             {
                 but1st = RELEASE;
                 cnt = 0;
                 but1OK = 1; //Confirma que el botón fue presionado y liberado
             }
          }
          else cnt = 0;
       } break;
   }
}


5- CREACIÓN DEL MENÚ LCD

Muy bien, ahora es momento de empezar definir nuestro menú con las opciones de selección, considero como un ejemplo básico un menú de dos niveles sin encabezado puesto que utilizaremos una pantalla de solo dos lineas, la siguiente imagen ilustra con mayor claridad la navegación que el usuario puede efectuar en este menú.




El estado de la pantalla trabajara en dos modos manejados por el programa principal, se tiene el modo normal en la que se mostrar una mensaje con información de la hora y el modo ajuste que permitirá la navegación por el menú. El pulsador del encoder representara la confirmación para cambiar de modo en el programa principal.

Entonces para tratar de sistematizar este menú a través de la programación en lenguaje C declararemos cada mensaje del menú como una constante en el siguiente orden. 


const char menu_000[] = "  MENÚ A    ";
const char menu_001[] = "  MENÚ B    ";
const char menu_002[] = "  SALIR        ";
const char menu_010[] = "  Opción 1    ";
const char menu_011[] = "  Opción 2    ";
const char menu_012[] = "  Opción 3    ";
const char menu_013[] = "  Atrás         ";
const char menu_020[] = "  Opción 4    ";
const char menu_021[] = "  Opción 5    ";
const char menu_022[] = "  Opción 6    ";
const char menu_023[] = "  Opción 7    ";
const char menu_024[] = "  Atrás          ";


Ahora definiremos una estructura para la navegación dentro del menú considerando las posibles opciones para el usuario.

typedef const struct
{
    const char *text;   //Mensaje
    char mpoints;       //Cantidad de items en el submenu
    char btUP;            //Navegación hacia arriba

    char btDOWN;      //Navegación hacia abajo
    char btENTER;    //Aceptar o ingresar a opción
    void (*fp) (void);   //Puntero a Función
} menustruc;


La ultima linea en la estructura representa un puntero de función que se llamara cuando la selección implique realizar una tarea particular. Establecemos un arreglo de la estructura final con los mensajes y valores de navegación utilizados en nuestro menú.

menustruc menú[] =
{
    {menú_000, 3, 0, 1, 3, 0},           //0 MENÚ A
    {menú_001, 3, 0, 2, 7, 0},           //1 MENÚ B
    {menú_002, 3, 1, 2, 2, 0},           //2 SALIR
    {menú_010, 4, 3, 4, 4, func1},    //3
    {menú_011, 4, 3, 5, 5, func2},    //4
    {menú_012, 4, 4, 6, 6, func3},    //5
    {menú_013, 4, 5, 6, 0, 0},           //6
    {menú_020, 5, 7, 8, 8, func4},    //7
    {menú_021, 5, 7, 9, 9, func5},    //8
    {menú_022, 5, 8, 10, 10, func6},//9
    {menú_023, 5, 9, 11, 11, func7},//10
    {menú_024, 5, 10, 11, 1, 0},       //11
};


Si tienes dudas hasta aquí, déjame decirte que estas por buen camino, así que tomate el tiempo necesario porque tratare de explicar mejor esta estructura; en cada linea deje un comentario con un valor que representa el índice de cada opción, este valor es almacenado en la variable global menusel, por ejemplo analicemos el índice 2 que corresponde a:  {menu_002, 3, 1, 2, 0, 0}

Según la estructura menustruc, el primer elemento es el mensaje const char *text que se inicializa con menu_002, esto en otras palabras es el mensaje "SALIR", luego se tiene el elemento mpoints que cuenta con 3 posibles opciones que son MENU A, MENU B y SALIR. El siguiente elemento de la estructura es btUP =1 que determina cual es el índice superior y btDOWN = 2 el índice inferior estos valores limitan el desplazamiento del cursor dentro del menú de opciones. finalmente el elemento btENTER = 0 indica el índice seleccionado en caso de presionar el pulsador del encoder, la acción asociada a esta selección deberá permitir salir del modo Ajuste. 
La siguiente imagen le ayudara a entender mejor como se estructura este menú y también se dará cuenta que podría fácilmente agregar opciones y mas niveles.



El procedimiento ShowMenu(), se encarga de manejar la navegación del menú dentro del programa y la mista debe actualizarse cada vez que ocurra un cambio en el encoder. el código se elaboro en base a un ejemplo que encontré en la red hace mucho tiempo, por esta razón no pode ubicar la referencia para dejar el respectivo enlace.

void ShowMenu()
{
    uint8_t lcnt = 0, from = 0, till = 0, temp = 0;
    while(till <= menusel)
        till += menu[till].mpoints;      //Índice de posición máxima
    from = till - menu[menusel].mpoints; //Índice de posición mínima
    till --;
    temp = from;
    if(menusel >= (from + 1) && (menusel <= (till-1))) //Si requiere desplazar
    {  
        from = menusel - 1;
        till = from + 1;
        for(from; from <= till; from ++)
        {
            LCDGotoln(lcnt);
            LCDPuts(menu[from].text);
            lcnt++;
        }
        LCDGotoln(1); //Coloca el curso al principio de la segunda linea
        LCDPutc(0x7E); //Escribe el caracter ->
    }
    else
    {
        if(menusel < (from + 1))
        {
            till = from  + 1;
            for (from; from <= till; from ++)
            {
                LCDGotoln(lcnt);
                LCDPuts(menu[from].text);
                lcnt++;
            }
            LCDGotoln(menusel - temp);
            LCDPutc(0x7E);
        }
        if(menusel == till)
        {
            from = till - 1;
            for (from; from <= till; from ++)
            {
                LCDGotoln(lcnt);
                LCDPuts(menu[from].text);
                lcnt++;
            }
            LCDGotoln(1);
            LCDPutc(0x7E);
        }
    }
}


6- MENÚ Y LECTURA DE ENCODER

El procedimiento que controla la navegación del menú mediante el enconder se llama TaskMENU1, que cuenta con tres estados que son dependientes de la variable cnt y la acción sobre el encoder.

El estado NORMAL tiene como objetivo mostrar en la pantalla el tiempo en formato hora-minuto-segundo, el tiempo de actualización esta condicionado por cnt para medio segundo. La única condición que determina un cambio de estado es que la bandera but1OK se active, esto nos indica que se presiono y libero el pulsador del encoder para cambiar al estado AJUSTE.

El estado AJUSTE nos muestra el menú llamando al procedimiento ShowMenu, y su actualización va depender únicamente de un cambio en el encoder, este cambio puede ser: Aceptar la opción actual but1OK, desplazar arriba KeyUP o desplazar abajo KeyDOWN; El desplazamiento es detectado por el cambio de estado en el pin RB1, siendo la rutina de interrupción la responsable de determinar el sentido(variable keysense). Cada vez que se detecte un cambio en el encoder el estado cambiara a RETARDO.

El estado RETARDO cumple la tarea de mantener un tiempo mínimo de espera para un nuevo cambio en el encoder, esto permite en cierta manera ajustar la sensibilidad de cambio, otra tarea que se realiza es rehabilitar la interrupción por cambio de estado para un nuevo evento; Finalmente la activación de la bandera but1OK mientras el índice del menú es igual a 2 corresponde a seleccionar la opción SALIR, logrando con esto retornar al estado NORMAL para mostrar el tiempo nuevamente. Esta breve explicación se puede resumir en el siguiente diagrama de estados
 



void TaskMENU1()
{
   static unsigned int cnt = 0;
   switch(men1st)
   {
     case NORMAL:
    {
      cnt ++;
       if(cnt > 499)
       {
         LCDGotoln(1);
         sprintf(lcdtext, " [%02u:%02u:%02u]", hora, min, sec);
         LCDPuts(lcdtext);
         cnt = 0;
       }
       if(but1OK == 1)
      {
        but1OK = 0;
        men1st = AJUSTE;
        LCDSet(CLEAR);
        ShowMenu();
        cnt = 0;
     }
   } break;
   case AJUSTE:
  {
     if(keysense || but1OK)
    {
       if(keysense == KEYUP) menusel = menu[menusel].btUP;
       if(keysense == KEYDOWN) menusel = menu[menusel].btDOWN;
       if(but1OK)
       {
          if(menu[menusel].fp != 0) //Si hay un puntero valido en la seleccion
          menu[menusel].fp(); //Ejecuta la funcion asociada al puntero
          else
          menusel = menu[menusel].btENTER; //Mantiene la posición
       }
       cnt = 0;
       men1st = RETARDO;
       ShowMenu(); //Actualiza el menu en pantalla
     }
   } break;
   case RETARDO:
   {
      cnt ++;
      if(cnt > 100)
     {
        keysense = PORTB;
        INTCONbits.RBIF = 0;
       INTCONbits.RBIE = 1;
       keysense = 0;
       if(but1OK)
      {
         but1OK = 0;
         if(menusel == 2) //Opción de salir
        { 
           LCDSet(CLEAR);
           LCDPuts(" L.C.D. PIC16F ");
           LCDGotoln(1);
           men1st = NORMAL;
           cnt = 0;
           break;
        }
     }
     men1st = AJUSTE;
    }
   } break;
  }

} 

Las siguientes lineas corresponden a la rutina de interrupción del programa, note que la ejecución de RBIE = 0 deshabilita la re-entrada hasta que el evento actual sea atendido por el programa principal, es importante adicionar condensadores de desacople para una mejor respuesta del encoder.

void interrupt isr()
{
    if(INTCONbits.RBIF && INTCONbits.RBIE)
    {
       if(ENCApin) //if DAT = 1
      {
         if(ENCBpin) keysense = KEYUP;
         else keysense = KEYDOWN;
       }
       else //if(DAT = 0)
       {
          if(ENCBpin) keysense = KEYDOWN;
          else keysense = KEYUP;
       }
       INTCONbits.RBIE = 0; //Deshabilita la interrupción.
       INTCONbits.RBIF = 0;
    }
}


7- FUNCIONAMIENTO

Dentro del programa principal, la estructura del bucle indefinido es muy simple de comprender gracias a que el resto de los procedimiento son llamados a intervalos continuos como maquinas de estado finito, El único proceso efectuado en el bucle es el de actualizar las variables del tiempo que son la hora, minuto y segundo. Observe la siguiente imagen:
 

Una vez programado el PIC, como pantalla de inicio no mostrara el tiempo transcurrido. Observe la imagen
 

Aquí les dejo el enlace para la descarga del proyecto MPLABX, para lo cual utilice la version 2.1 del compilador XC8.




8- CONCLUSIONES


Como habrán visto la implementación de este menú no es difícil, pero el ejemplo que se describe en esta entrada no esta adaptado a ninguna situación real y en este punto hay que ser claros en decir que el diseño de un menú responde a una necesidad particular en cada aplicación, esto supone que el programador necesitara modificar la estructura y considerar condiciones adicionales dentro de la navegación. Un procedimiento que no se ha descrito a profundidad en este blog pero que es clave en la personalización del menú es ShowMenu, basicamente en sus lineas se pueden crear ventanas, encabezados, desplazamientos, etc. Después veré trabajar en otro ejemplo utilizando otra configuración para un nuevo menú.

Saludos.
Pablo Zarate Arancibia
Ingeniero Electrónico
pablinzte@gmail.com

No hay comentarios.:

Publicar un comentario