Timers

El timer es un reloj de precisión que el PIC contiene, y que crea una interrupción cuando su registro se "desborda" (llega al máximo).

En ésta ocasión usaremos el PIC16f682A, porque contiene un TIMER0 (8 bits) y un TIMER1 (16 bits), que tendrán distintas duraciones.

En primer lugar cabe decir que el TIMER funciona de la siguiente forma: configuramos el registro, cargamos un número, el timer va sumando 1 a ése número según el oscilador y el preescaler que le asignemos, y cuando llega a su límite (256 o 65536), crea una interrupción y el PIC atiende a ésa ISR.

Concretamente, nos vamos a centrar por ahora en usar el TIMER como temporizador, porque también se puede usar como contador de eventos externos (suma 1 cada vez que cambia el estado de un pin). Aunque parezca un poco tonto, vamos a volver a hacer parpadear un LED cada segundo, pero ésta vez será con toda la precisión que pueda darnos un TIMER.

Empezaremos por el TIMER0. Éste se configura y controla desde el registro OPTION_REG (0x81h):

  • T0CS: selección de la fuente del timer (externa para contador, interna para temporizador). Se configura como externa con un 1 y como interna con 0.
  • T0SE: selecciona el flanco de conteo si la fuente es externa.
  • PSA: asigna el preescaler con un 1 al WDT y con 0 al modulo TIMER
  • PS (2 bits): asigna el valor del preescaler según unas tablas que se pueden ver en la datasheet.

También nos interesa el registro del módulo TIMER0, donde cargamos el número y se va sumando, TMR0 (0x01h). Y ya de paso el registro de interrupciones (INTCON, 0x0Bh) del cual usaremos el T0IE (5) y el T0IF(2).

Así pues, nos pondríamos a programar, pero ¿cuál es el valor que tengo que cargar al registro para que me dé el tiempo exacto que yo quiero? Para ello utilizamos una fórmula:

timer0

Pero no seamos optimistas, el tiempo que va a resultar no es en segundos, ya que el máximo que se puede conseguir con el TIMER0 es de 65.53 ms. Así que utilizaremos iteraciones para conseguir 1 segundo. Como habréis supuesto entonces, el valor de tiempo es de tiempo multiplicado por diez elevado a menos tres (tiempo(s)*10^-3 = tiempo(ms)), y el de frecuencia suele ser de 4Mhz por lo que será de 4*10^6. Lo que hacemos es despejar ValorTMR0.

Supongamos entonces que queremos una interrupción cada 50 ms, tendremos que sumar a un entero hasta que llegue a 20 veces, entonces tendremos 1 segundo y llevamos a cabo la acción.

Pero si calculamos el ValorTMR0 para 50ms no es un número entero y por lo tanto ya perdemos precisión. Dicho valor con un oscilador de 4MHz y un prescaler de 256 es 60'6875, así que podemos cargar el valor 61, que en hexadecimal es 0x3D.

Otra cosa que nos fastidia bastante es que no podemos usar el TIMER a la vez que una función delay, porque en el lapso de tiempo que ocurre un delay, el TIMER deja de contar y ése periodo se ha perdido. Ocurre lo mismo si el micro entra en sleep, por lo que hay que hacer un bucle infinito.

Vamos a empezar a programar.

#Include 
#Fuses NOPROTECT,NOWDT,INTRC_IO,PUT,NOLVP,BROWNOUT
//INTRC_IO indica que vamos a usar el oscilador interno del PIC, no un cristal
#use delay(clock=4000000)
#byte trisb=getenv("SFR:TRISB")
#byte portb=0x06
#byte TMR0=0x01
#Byte OPREG=0x81 //mucho ojo con no modificar otras configuraciones
#Bit GIE=0x0B.7
#Bit TMRI=0x0B.5
#Bit TMRF=0x0B.2
#bit led=0x06.0

int i=0; //usaremos ésto para la iteración

void TMR0INTSET(){
TMR0=0x3D; //Cargamos éste valor en el TIMER0 que empieza a contar desde ahí
GIE=1;
TMRI=1; //Pero ahora tiene una interrupción para cuando se desborde
TMRF=0;
}

#INT_TIMER0 //Ésta es la directiva para la interrupción
void tmr0isr(){
i++; //Suma 1 al contador cada 50ms
if(i>=20){ //si has llegado 20 o más veces hasta aquí
  led=!led //cambia el estado del led
  i=0; //y resetea la variable
}
TMR0INTSET(); //Una vez termine la ISR se vuelve a cargar y habilitar
}

void main(){
trisb=0x00; //Todo salidas
led=0; //que se inicie apagado
OPREG=0b00000111; //Reloj interno, prescaler modulo TIMER y con valor 256.
//También podemos usar la función setup_timer_0(RTCC_INTERNAL | RTCC_DIV_256);
TMR0INTSET(); //Cargamos el registro y habilitamos la interrupción
while(1); //Bucle infinito para que no entre en sleep
}

Otras funciones que se pueden usar son:

enable_interrupts(INT_TIMER0); //Para habilitar la interrupción
enable_interrupts(GLOBAL); //Es como poner en 1 GIE
set_TIMER0(Valor); //Carga el registro TIMER0 con ése valor

Con el TIMER1 funciona de la misma forma, se configura en el T1CON (0x10h), se activa su interrupción en el PIE(0x8C), TIMER1IE (0) y su bandera en el PIR(0x0C), TIMER1IF(0). Tiene un bit en T1CON capaz de activar y desactivar éste temporizador, lo cuál le da algo más de posibilidades. La fórmula cambia:

timer1

Pero dado que el TIMER1 tiene dos registros de 8 bits cada uno para cargar el valor, se hace necesario usar la función set_timer1(Valor). Con éste timer, oscilador de 4MHz y prescaler de 8 se puede llegar a un tiempo de 524.288ms, lo cual está muy bien. La otra buena noticia es que para valores pequeños de tiempo casi siempre se encontrarán valores enteros para cargar en el registro, por lo que no hay pérdida de precisión. La función para configurarlo sería por ejemplo:

setup_timer_1(T1_INTERNAL | T1_DIV_BY_8);

Ésta función tiene a bien ponerlo en marcha a la vez.

Interrupciones externas

Los PICs tienen la habilidad de crear interrupciones en el código cuando cambia el estado de una entrada y así se le ha indicado.

La interrupción se definiría como decirle al PIC "deja absolutamente todo lo que estés haciendo y atiende a la función que he escrito para cuando ésto ocurra". Dicha función es la ISR.

Las patas de interrupción externa de un PIC suelen ser la RB0/INT (Lo de INT ya da una pista) y las patas de RB4 a RB7. Normalmente las patas RB4-7 se deben activar todas a la vez y cuando se crea el cambio de estado la ISR no distingue cuál ha sido la patilla responsable, a no ser que lo hagas distinguir mediante código.

Por manías mías, yo no utilizo las funciones del CCS para crear interrupciones, sino que creo las mías propias para el mismo programa, pero podríais verlas en la librería del PIC correspondiente y usarlas al gusto.

El mundillo de las interrupciones se encuentra en el registro INTCON del PIC, que normalmente se encuentra en la dirección 0x0B. De dicho registro, para las interrupciones externas, nos interesan 5 bits:

  • GIE: Global Interrupt Enable. Habilita o deshabilita las interrupciones globalmente, es decir, afecta a todas
  • INTE: Habilita o deshabilita la interrupción por cambio de estado en el pin INT (Rb0)
  • INTF: Es la llamada "bandera", cuando ha ocurrido la interrupción en INT, se pone en 1, debe ser bajada a 0 mediante código.
  • RBIE: Habilita o deshabilita la interrupción por cambio de estado en los pines RB4-RB7
  • RBIF: Es la bandera de cambio en RBIE, también hay que bajarla mediante código.

La función de habilitar una interrupción, ocurre de la siguiente forma:

  • Ocurre la interrupción o se inicializa el PIC
  • Se habilita GIE
  • Se habilita INTE o RBIE
  • Se baja la bandera INTF o RBIF
  • Se espera a que ocurra la interrupción, se lleva a cabo la ISR, se vuelve a configurar la interrupción y se continúa desde el punto del programa en el que estaba el PIC cuando le dijimos que lo dejase todo.

Para ello definimos al principio del código los bits de la configuración

#Bit GIE=0x0B.7
#Bit INTE=0x0B.4
#Bit INTF=0x0B.1
#Bit RBIE=0x0B.3
#Bit RBIF=0x0B.0

Automáticamente después escribimos las funciones que habilitan la interrupción y bajan la bandera en cuestión:

void intenab(){
GIE=1;
INTE=1;
INTF=0;
}
void rbienab(){
GIE=1;
RBIE=1;
RBIF=0;
}

Lo siguiente antes del main es escribir la ISR. ¿Cómo indicamos qué ISR se debe llevar a cabo cuando ocurra la interrupción en INT o RB4-7? Mediante una directiva del preprocesador:

#INT_EXT
//Ésta ISR se llevará a cabo cuando la interrupción ocurra en INT/RB0
void isrint(){
...
intenab();
}
#INT_RB
//Y ésta cuando la interrupción ocurra en RB4-7
void rbisr(){
...
rbienab();
}

Como veis, ya he incluido la función de habilitar la interrupción justo al final de ésta, para que pueda volver a ocurrir otras veces.

Por último, ya escribimos la función main, en la que incluiremos también las de habilitar interrupción, sin olvidar que se debe configurar aquellos pines que queramos que produzcan interrupción como entradas:

void main(){
//Configuración y limpieza de puertos
//Otras configuraciones
...
intenab();
rbienab();
while(1){
   ...
}
}

De ésta forma, una vez esté todo configurado, al cambiar el estado de un pin se producirá la interrupción correspondiente.

También podemos configurar si queremos que la interrupción se produzca por flanco de bajada o de subida, es decir, si se debe producir cuando el pin pase de 0 a 1 lógico o al contrario, para el pin INT. Ésto se hace mediante el registro OPTION_REG (0x81h), concretamente el bit 6, INTEDG. Poniendole un 1 la interrupción se hará cuando el flanco suba (de 0 a 1), y con un 0 ocurrirá lo contrario.

Extra: el extraño caso del PIC12F675

Éste pequeño, pero matón, PIC trae un pin de INT y además podemos activar la interrupción por cambio de flanco entre los pines GP0 y GP5 (es decir, todos...), pero trae consigo la genialidad de poder seleccionar cuáles de ellos crean interrupción y cuáles no, mediante el registro IOC (0x96h), en el que escribiremos como si se tratase de un TRIS cuáles de los pines crearán interrupción con un 1.

En éste caso la directiva de preprocesador a usar para la ISR será #INT_RA

Lectura analógica

Algunos modelos de PICs (y Arduinos) cuentan con unas entradas llamadas analógicas. ¿Qué es una entrada analógica?

Hasta ahora hemos estado trabajando con entradas y salidas digitales, es decir, que pueden estar en estado alto o bajo, 5 voltios o 0 voltios. Sin embargo, el mundo real no está conformado de señales digitales ni mucho menos evoluciona como tal. Lo que nos permite un puerto analógico es leer y transformar un valor de tensión entre 0 y 5 voltios (en el modo más basico) en un valor numérico.

Dicho valor numérico viene determinado por el PIC y la programación, los hay que usan 8 bits.

Con 8 bits tenemos un recorrido de 2^8 = 256, si tenemos en cuenta que cuando lea 5 voltios tendremos un valor de 256, ésto nos da una resolución (paso) de 0.0195 voltios por cada 1 que se sume al valor.

También tenemos la posibilidad de usar 10 bits: 1024, lo cual nos da una resolución de 0.00488V por valor.

Para un sistema analógico bien acondicionado, es una precisión excelente ¿no creéis?

¿Cómo usamos dicho puerto analógico en la programación?

En primer lugar, se debe indicar al compilador que vamos a usar el "device ADC" y con qué resolución lo queremos (en bits), justo después de añadir la librería del PIC.

#Include <PICQUEVASAUSAR.h>
#Device ADC= Resolución //8 o 10 según os convenga
#Fuses
#Byte
Etc...

Y se debe poner justo ahí porque si no, ya he comprobado que no funciona. Así pues, necesitaremos una variable para almacenar el valor que nuestro ADC lea. Para intentar ahorrar espacio, si usas 8 bits te conviene un int, y si usas 10 bits mejor usar un int16. Como deduciréis, el primero ocupa 8 bits y el segundo 16.

También necesitaréis un float para almacenar el valor convertido a tensión (en caso de necesitarlo).

int16 lectura;
float valor;

El siguiente paso es configurar el puerto. En primer lugar, el pin del puerto que se vaya a usar para la lectura se debe declarar como entrada obligatoriamente, mientras el resto se usará como entradas o salidas digitales normalmente. Recordad que ésto se hace con el registro TRIS.

Para configurar el ADC podéis hacerlo accediendo directamente al registro ADCON0 y ADCON1 y según indique la datasheet los programáis al gusto, o bien podéis tomar el atajo y usar las funciones de CCS.

En primer lugar, decimos qué entradas queremos que sean analógicas:

setup_adc_ports(---);
// Donde he puesto --- hay una lista enorme de posibilidades, las más usadas serían:
// ALL_ANALOG -> Todas las patas del puerto analógico funcionan como tal
// NO_ANALOG -> Ninguna analógica, ésto viene por defecto
// AN0 -> Sólo la pata AN0 es usada como entrada analógica, la más usada.
// AN0_AN1_AN3 -> Ésas 3 serían analógicas
// Y más combinaciones que podéis ver en la librería .h de vuestro PIC
// También es posible seleccionarlos uno a uno escribiendo por ejemplo
// setup_adc_ports(sAN0 | sAN2 | sAN6);

Como ya he indicado, lo más normal es usar sólo una entrada analógica y el resto digitales, se pone AN0 o sAN0 según el PIC (ensayo y error).

El próximo paso es indicarle al ADC con qué velocidad de reloj va a funcionar:

setup_adc(---);
//ADC_CLOCK_DIV_2
//ADC_CLOCK_DIV_8
//ADC_CLOCK_INTERNAL
//Etc
//Lo más indicado es visitar la librería del pic para ver
//de qué dispone

Por último, hay que decir al pic, de entre todas las entradas analógicas que hemos configurado, cuál queremos que lea.

set_adc_channel(Canal);
//El canal vendría siendo 0 para AN0, 3 para AN3, ...

Cuando todo esté listo, podemos empezar a leer el ADC con la/s siguiente/s función/es:

Posibilidad 1:
lectura = Read_adc(); //que es lo mismo que usar
lectura = Read_adc(ADC_START_AND_READ);
delay_us(20); //Le damos un poco de tiempo

Posibilidad 2:
Read_adc(ADC_START_ONLY); //Inicia el ADC pero no lo lee
//Aquí haríamos lo que quisiéramos entre medias, más funciones
lectura = Read_adc(ADC_READ_ONLY); //Y cuando se lo pedimos lo lee
delay_us(20);

Lo más rápido y sencillo es usar la "posibilidad 1" con lect = read_adc() y punto.

Una vez hecho ésto habremos adquirido en la variable lect un valor correspondiente a la tensión que se encontrase en la pata analógica en ése momento. Si lo que queremos es saber el valor de la tensión que se ha leído, tenemos que hacer una regla de tres:

Si 1024 son 5 voltios ¿Cuántos voltios es lo que he leído?

1024 ---- 5
lect ---- valor

valor = lect*1024/5;

Y así se obtiene ya un valor decimal de dicha tensión.

Espero que os sea de utilidad.

Escritura en LCD

Continuamos con el "cursillo" de PIC en C con algo que he conseguido simular hoy. Tan simple como utilizar funciones predefinidas en una librería y tan importante como otra nueva salida al exterior de la información que maneja nuestro PIC. Se trata de escribir en una LCD un par de frases y un relojito. Algo tal que así aunque luego me ocuparé de mejorarlo:

lcd

 

Como no se ve muy bien, he conectado RB0 al pin Enable (6), RB1 a RS(4) y RB2 a RW(5).

La configuración de la alimentación de la LCD es la siguiente: alimentamos directamente VDD y ponemos VSS a tierra. VEE es el control del contraste de nuestra pantalla (que en simulación no funciona). Tomamos de VDD a un potenciómetro (normalmente de 10K Ohm) y conectamos el pin intermedio (el variable) a VEE. El otro extremo del potenciómetro va a tierra.

En programación: hay que declarar qué puerto usar antes de añadir la librería, en éste caso he utilizado la lcd.c que viene por defecto en CCS, aunque existen por internet versiones flex_lcd.c en las que defines los pines uno a uno. Allá vamos. También hay dos formas de controlar el LCD: por 8 o 4 bits. He utilizado la configuración de 8. La LCD es de 2x16. 16 caracteres y 2 líneas.


#include <16f84a.h>
#use delay(clock=4000000)
#fuses NOWDT,NOPROTECT
#define LCD_DATA_PORT getenv("SFR:PORTB") //Se define el puerto B como datos
#include <lcd.c> //incluimos la librería por defecto
int i; //variable que utilizaremos para contar segundos
void main(){
lcd_init(); //Éste comando inicializa la LCD automáticamente
delay_ms(20); //*1
i=0; //inicializamos variable a 0
while(1){
lcd_putc("\fHola mundo"); //limpia la pantalla y escribe Hola mundo
lcd_gotoxy(1,2); //baja a primer caracter segunda linea
printf(lcd_putc,"By jmth %ds",i); //imprime By jmth y la variable entera i
delay_ms(1000); //retardo de 1s
i++; //subimos 1 a la var
if(i>60) i=0; //si pasa de 1 min que vuelva a 0
}
}
//*1: En usos reales se deberá hacer un pequeño delay entre cada instrucción
// debido a lo que tarda el controlador de LCD en procesar cada una

También cabe añadir las otras 2 funciones de putc o printf, b, que retrocede un caracter, y n, que pasa a la siguiente línea. Si no tenéis la librería lcd.c aquí la dejo:

///////////////////////////////////////////////////////////////////////////////
//// LCD.C ////
//// Driver for common LCD modules ////
//// ////
//// lcd_init() Must be called before any other function. ////
//// ////
//// lcd_putc(c) Will display c on the next position of the LCD. ////
//// The following have special meaning: ////
//// f Clear display ////
//// n Go to start of second line ////
//// b Move back one position ////
//// ////
//// lcd_gotoxy(x,y) Set write position on LCD (upper left is 1,1) ////
//// ////
//// lcd_getc(x,y) Returns character at position x,y on LCD ////
//// ////
//// CONFIGURATION ////
//// The LCD can be configured in one of two ways: a.) port access or ////
//// b.) pin access. Port access requires the entire 7 bit interface ////
//// connected to one GPIO port, and the data bits (D4:D7 of the LCD) ////
//// connected to sequential pins on the GPIO port. Pin access ////
//// has no requirements, all 7 bits of the control interface can ////
//// can be connected to any GPIO using several ports. ////
//// ////
//// To use port access, #define LCD_DATA_PORT to the SFR location of ////
//// of the GPIO port that holds the interface, -AND- edit LCD_PIN_MAP ////
//// of this file to configure the pin order. If you are using a ////
//// baseline PIC (PCB), then LCD_OUTPUT_MAP and LCD_INPUT_MAP also must ////
//// be defined. ////
//// ////
//// Example of port access: ////
//// #define LCD_DATA_PORT getenv("SFR:PORTD") ////
//// ////
//// To use pin access, the following pins must be defined: ////
//// LCD_ENABLE_PIN ////
//// LCD_RS_PIN ////
//// LCD_RW_PIN ////
//// LCD_DATA0 ////
//// LCD_DATA1 ////
//// LCD_DATA2 ////
//// LCD_DATA3 ////
//// LCD_DATA4 ////
//// ////
//// Example of pin access: ////
//// #define LCD_ENABLE_PIN PIN_E0 ////
//// #define LCD_RS_PIN PIN_E1 ////
//// #define LCD_RW_PIN PIN_E2 ////
//// #define LCD_DATA0 PIN_D4 ////
//// #define LCD_DATA1 PIN_D5 ////
//// #define LCD_DATA2 PIN_D6 ////
//// #define LCD_DATA3 PIN_D7 ////
//// ////
///////////////////////////////////////////////////////////////////////////////
//// (C) Copyright 1996,2009 Custom Computer Services ////
//// This source code may only be used by licensed users of the CCS C ////
//// compiler. This source code may only be distributed to other ////
//// licensed users of the CCS C compiler. No other use, reproduction ////
//// or distribution is permitted without written permission. ////
//// Derivative programs created using this software in object code ////
//// form are not restricted in any way. ////
///////////////////////////////////////////////////////////////////////////
typedef struct 
{ // This structure is overlayed
 BOOLEAN enable; // on to an I/O port to gain
 BOOLEAN rs; // access to the LCD pins.
 BOOLEAN rw; // The bits are allocated from
 BOOLEAN unused; // low order up. ENABLE will
 int data : 4; // be LSB pin of that port.
 #if defined(__PCD__) // The port used will be LCD_DATA_PORT.
 int reserved: 8;
 #endif
} LCD_PIN_MAP;
#if defined(__PCB__)
 // these definitions only need to be modified for baseline PICs.
 // all other PICs use LCD_PIN_MAP or individual LCD_xxx pin definitions.
/* EN, RS, RW, UNUSED, DATA */
 const LCD_PIN_MAP LCD_OUTPUT_MAP = {0, 0, 0, 0, 0};
 const LCD_PIN_MAP LCD_INPUT_MAP = {0, 0, 0, 0, 0xF};
#endif
#ifndef LCD_ENABLE_PIN
 #define lcd_output_enable(x) lcdlat.enable=x
 #define lcd_enable_tris() lcdtris.enable=0
#else
 #define lcd_output_enable(x) output_bit(LCD_ENABLE_PIN, x)
 #define lcd_enable_tris() output_drive(LCD_ENABLE_PIN)
#endif
#ifndef LCD_RS_PIN
 #define lcd_output_rs(x) lcdlat.rs=x
 #define lcd_rs_tris() lcdtris.rs=0
#else
 #define lcd_output_rs(x) output_bit(LCD_RS_PIN, x)
 #define lcd_rs_tris() output_drive(LCD_RS_PIN)
#endif
#ifndef LCD_RW_PIN
 #define lcd_output_rw(x) lcdlat.rw=x
 #define lcd_rw_tris() lcdtris.rw=0
#else
 #define lcd_output_rw(x) output_bit(LCD_RW_PIN, x)
 #define lcd_rw_tris() output_drive(LCD_RW_PIN)
#endif
#ifndef LCD_DATA_PORT
 #if defined(__PCB__)
 #define LCD_DATA_PORT 0x06 //portb
 #define set_tris_lcd(x) set_tris_b(x)
 #elif defined(__PCM__)
 #define LCD_DATA_PORT getenv("SFR:PORTD") //portd
 #elif defined(__PCH__)
 #define LCD_DATA_PORT getenv("SFR:PORTD") //portd
 #elif defined(__PCD__)
 #define LCD_DATA_PORT getenv("SFR:PORTD") //portd
 #endif 
#endif
#if defined(__PCB__)
 LCD_PIN_MAP lcd, lcdlat;
 #byte lcd = LCD_DATA_PORT
 #byte lcdlat = LCD_DATA_PORT
#elif defined(__PCM__)
 LCD_PIN_MAP lcd, lcdlat, lcdtris;
 #byte lcd = LCD_DATA_PORT
 #byte lcdlat = LCD_DATA_PORT
 #byte lcdtris = LCD_DATA_PORT+0x80
#elif defined(__PCH__)
 LCD_PIN_MAP lcd, lcdlat, lcdtris;
 #byte lcd = LCD_DATA_PORT
 #byte lcdlat = LCD_DATA_PORT+9
 #byte lcdtris = LCD_DATA_PORT+0x12
#elif defined(__PCD__)
 LCD_PIN_MAP lcd, lcdlat, lcdtris;
 #word lcd = LCD_DATA_PORT
 #word lcdlat = LCD_DATA_PORT+2
 #word lcdtris = LCD_DATA_PORT-0x02
#endif
#ifndef LCD_TYPE
 #define LCD_TYPE 2 // 0=5x7, 1=5x10, 2=2 lines
#endif
#ifndef LCD_LINE_TWO
 #define LCD_LINE_TWO 0x40 // LCD RAM address for the second line
#endif
BYTE const LCD_INIT_STRING[4] = {0x20 | (lcd_type << 2), 0xc, 1, 6};
 // These bytes need to be sent to the LCD
 // to start it up.

BYTE lcd_read_nibble(void);
BYTE lcd_read_byte(void)
{
 BYTE low,high;
#if defined(__PCB__)
 set_tris_lcd(LCD_INPUT_MAP);
 #else
 #if (defined(LCD_DATA0) && defined(LCD_DATA1) && defined(LCD_DATA2) && defined(LCD_DATA3))
 output_float(LCD_DATA0);
 output_float(LCD_DATA1);
 output_float(LCD_DATA2);
 output_float(LCD_DATA3);
 #else
 lcdtris.data = 0xF;
 #endif
 #endif
 
 lcd_output_rw(1);
 delay_cycles(1);
 lcd_output_enable(1);
 delay_cycles(1);
 high = lcd_read_nibble();
 
 lcd_output_enable(0);
 delay_cycles(1);
 lcd_output_enable(1);
 delay_us(1);
 low = lcd_read_nibble();
 
 lcd_output_enable(0);
#if defined(__PCB__)
 set_tris_lcd(LCD_INPUT_MAP);
 #else
 #if (defined(LCD_DATA0) && defined(LCD_DATA1) && defined(LCD_DATA2) && defined(LCD_DATA3))
 output_drive(LCD_DATA0);
 output_drive(LCD_DATA1);
 output_drive(LCD_DATA2);
 output_drive(LCD_DATA3);
 #else
 lcdtris.data = 0x0;
 #endif
 #endif
return( (high<<4) | low);
}
BYTE lcd_read_nibble(void)
{
 #if (defined(LCD_DATA0) && defined(LCD_DATA1) && defined(LCD_DATA2) && defined(LCD_DATA3))
 BYTE n = 0x00;
/* Read the data port */
 n |= input(LCD_DATA0);
 n |= input(LCD_DATA1) << 1;
 n |= input(LCD_DATA2) << 2;
 n |= input(LCD_DATA3) << 3;
 
 return(n);
 #else
 return(lcd.data);
 #endif
}
void lcd_send_nibble(BYTE n)
{
 #if (defined(LCD_DATA0) && defined(LCD_DATA1) && defined(LCD_DATA2) && defined(LCD_DATA3))
 /* Write to the data port */
 output_bit(LCD_DATA0, BIT_TEST(n, 0));
 output_bit(LCD_DATA1, BIT_TEST(n, 1));
 output_bit(LCD_DATA2, BIT_TEST(n, 2));
 output_bit(LCD_DATA3, BIT_TEST(n, 3));
 #else 
 lcdlat.data = n;
 #endif
 
 delay_cycles(1);
 lcd_output_enable(1);
 delay_us(2);
 lcd_output_enable(0);
}
void lcd_send_byte(BYTE address, BYTE n)
{
 lcd_output_rs(0);
 while ( bit_test(lcd_read_byte(),7) ) ;
 lcd_output_rs(address);
 delay_cycles(1);
 lcd_output_rw(0);
 delay_cycles(1);
 lcd_output_enable(0);
 lcd_send_nibble(n >> 4);
 lcd_send_nibble(n & 0xf);
}
void lcd_init(void) 
{
 BYTE i;
#if defined(__PCB__)
 set_tris_lcd(LCD_OUTPUT_MAP);
 #else
 #if (defined(LCD_DATA0) && defined(LCD_DATA1) && defined(LCD_DATA2) && defined(LCD_DATA3))
 output_drive(LCD_DATA0);
 output_drive(LCD_DATA1);
 output_drive(LCD_DATA2);
 output_drive(LCD_DATA3);
 #else
 lcdtris.data = 0x0;
 #endif
 lcd_enable_tris();
 lcd_rs_tris();
 lcd_rw_tris();
 #endif
lcd_output_rs(0);
 lcd_output_rw(0);
 lcd_output_enable(0);
 
 delay_ms(15);
 for(i=1;i<=3;++i)
 {
 lcd_send_nibble(3);
 delay_ms(5);
 }
 
 lcd_send_nibble(2);
 for(i=0;i<=3;++i)
 lcd_send_byte(0,LCD_INIT_STRING[i]);
}
void lcd_gotoxy(BYTE x, BYTE y)
{
 BYTE address;
if(y!=1)
 address=LCD_LINE_TWO;
 else
 address=0;
 
 address+=x-1;
 lcd_send_byte(0,0x80|address);
}
void lcd_putc(char c)
{
 switch (c)
 {
 case 'f' : lcd_send_byte(0,1);
 delay_ms(2);
 break;
 
 case 'n' : lcd_gotoxy(1,2); break;
 
 case 'b' : lcd_send_byte(0,0x10); break;
 
 default : lcd_send_byte(1,c); break;
 }
}
 
char lcd_getc(BYTE x, BYTE y)
{
 char value;
lcd_gotoxy(x,y);
 while ( bit_test(lcd_read_byte(),7) ); // wait until busy flag is low
 lcd_output_rs(1);
 value = lcd_read_byte();
 lcd_output_rs(0);
 
 return(value);
}