Programar como un electrónico

Programando hoy en día las llamadas librerías nos quitan un montón de trabajo, el mejor ejemplo de esto es programar en el Arduino IDE. En él ni siquiera te tienes que preocupar de importar las librerías más usadas, y puedes cambiar de placa sin tener que cambiar ni un poquito del código que has escrito, es casi magia.

Si no sabes de qué te estoy hablando, no pasa nada, se explicará según avance este hilo.

Pero hubo un tiempo, no muy lejano, en el que las cosas eran más complejas y se programaban con ensamblador. Este es el lenguaje de programación más puro (por describirlo de alguna forma) al que te vas a enfrentar cuando programes algo, aunque estoy seguro de que no lo tendrás que usar porque, como digo, es cosa del pasado. Sin embargo está bien, estuvo bien por mi parte aprender eso, porque da una visión mucho más cercana de lo que es en realidad un sistema de este tipo, un microcontrolador, un procesador, su memoria, por dónde van los datos... Que parece innecesario, pero luego hasta los ordenadores se manejan con esto en el fondo. Y eso es lo que quiero transmitir.

Como decía, el ensamblador es el lenguaje de programación más básico, lo que le transmites al programador con lo que escribes no es más que mover unos bits aquí y allá, hacer operaciones lógicas básicas, todo funciona leyendo y escribiendo registros.

¿Qué es un registro?

Bueno, no me voy a poner ahora a explicar qué tiene un ordenador por dentro (RAM, CPU, almacenamiento, buses de datos), pero sí extender esto a los microcontroladores.

En primer lugar, el programa se escribe en la memoria permanente del micro, ahora por lo general son memorias tipo FLASH, ahí se almacena en unos y ceros todo el programa y es el único lugar donde se mantienen los datos inalterados aunque se apague y se encienda. A no ser que el micro tenga otra memoria para datos del usuario, por ejemplo una EEPROM, que ahí también son permanentes, hasta que los borres.

En los micros la CPU es un concepto un poco abstracto, ya que está desperdigada por el resto de componentes. Se encarga de leer la memoria de programa, mover los datos y llevarlos a la unidad aritmético-lógica (ALU), el lugar donde se realizan operaciones matemáticas o lógicas muy simples, como sumar, multiplicar o hacer AND u OR.

Hay memoria RAM, está por ahí para guardar las variables mientras corre el programa, esas variables que se borran tras el apagado.

Hay vida más allá de estos tres componentes, hay lo que se llama registros. Un registro es como otra memoria que se borra al apagar el micro, y está hecha de la misma forma, de una serie de bits. Además tiene una dirección, a la que la CPU puede ir para leer o escribir los datos. Es igual que una memoria simple, pero los registros tienen funciones especiales. Vamos a ver un ejemplo.

El micro está leyendo el programa en la FLASH, y el programa le dice que escriba dos variables en la RAM, que son dos números en binario 110 (6) y 010 (2), y quiere que esas dos variables se sumen. El proceso no es nada simple.

En primer lugar va a coger uno de esos números y lo va a poner en un registro auxiliar (el registro W por ejemplo en el PIC), para ello tiene que pasar por la ALU sin realizar ninguna operación.

En el siguiente paso va a coger el otro número y lo va a poner en la ALU a la vez que el primer número, que estaba en el registro auxiliar, también entra en la ALU por el otro lado. La ALU tiene la instrucción de sumar y obtiene 1000 (8) que se guarda en, ojo cuidado, el registro auxiliar. Una vez terminada la operación hay un registro de estado (STATUS en el PIC), que tiene un bit que se pone a 1 cuando la ALU ha terminado. Se mira este bit y como ya lo hemos visto, hay que decirle que se ponga a 0 para que avise la próxima vez.

Ya sabemos que el resultado está preparado, ahora se puede mover a la RAM para guardar el resultado o incluso enviarlo a un registro de los periféricos, por ejemplo un pin de salida digital, si se envía al registro del puerto digital (p.ej. PORTB), esto haría que (con otras configuraciones previas) el pin correspondiente al bit número 4 de ese registro se pusiese en 1, y encendería... No sé, digamos que se enciende el LED, que siempre queda bonito.

Espero que con esto más o menos ya tengas una idea de qué es un registro. Es eso, un lugar en las direcciones de memoria del micro donde cambiamos bits para que ocurran ciertas cosas. Ya estamos un poquito más cerca de entender qué hace un código en C.

Direcciones

Ahora hablemos de las direcciones, hemos dicho que los registros son lugares en la memoria y que la CPU puede dirigirse a ellos mediante su dirección. Esta viene dada en las hojas de características del micro, es decir, te lo chiva el fabricante, y en numeración hexadecimal, lo que no lo hace muy amigable pero sí algo más compacto. Y tu se lo transmites al programa previamente, aunque aún no lo sepas.

Continuando con el ejemplo, en ensamblador tu no le dices al programa "manda lo que hay en el registro auxiliar al puerto b", lo que dices realmente es "mueve lo que hay en el registro auxiliar al registro con dirección 0x06". En este caso no tienes que decir la dirección del registro auxiliar porque ya está implícita en la función de ensamblador, MOVWF. Al fin y al cabo casi todo lo que se mueve pasa por ese registro, así se escribe menos. Vamos a escribir esta línea en ensamblador:

MOVWF 0x06

Nota: no todo lo que se mueve en todos los micros pasa por ese registro, pero sí la absoluta mayoría. Los más actuales tienen un módulo llamado DMA (direct memory access) que evita precisamente eso, pasando directamente de un registro a otro o a la memoria, con lo que la CPU puede usar el registro auxiliar para otra cosa mientras tanto.

Librerías

Pero menudo lío, ¿no? Pues llegan al rescate las librerías. ¿Qué hacen las librerías? Predefinir las cosas para que no lo tengas que hacer tu, y con esto me refiero a que, con la tecnología tan avanzada que tenemos, podemos hacer lo siguiente:

MOVWF portb

¿Qué acaba de hacer esta librería? Asignar un nombre bonito a una dirección de memoria, para que no tengamos que recordar los numeritos, ha hecho algo tan simple como esto:

portb equ 0x06

Hagamos otro ejemplo, esta vez vamos a poner a 1 un pin del puerto b directamente, pero sin alterar el estado del resto de los pines como hemos hecho (ups) accidentalmente antes. Para ello el ensamblador tiene que hacer:

  • Leer el puerto b y llevarlo al registro auxiliar W
  • Operar con un número binario cuyo resultado cambie el bit exacto que queremos sin alterar el resto
  • Limpiar el bit de STATUS que indica que ha terminado la operación
  • Llevar el resultado del registro W al registro del puerto b

¿Qué has hecho tu en un código C con librerías? Si estamos en Arduino IDE por ejemplo:

digitalWrite(pin4, HIGH)

Pero la ejecución es exactamente la misma, y esto ya viene hecho de antes por otra persona entonces no tienes que preocuparte.

"Bueno no es para tanto"

Pues no, no es para tanto si hablamos de un micro de 8 bits como es el PIC o los más comunes Atmel de Arduino, en estos la dirección de un registro se puede describir mediante un número hexadecimal de 2 dígitos, por ejemplo el 0x06 que hemos visto antes.

PERO. Luego vienen los micros de 32 bits. Los ordenadores son de entre 32 y 64 bits, recordemos. En estos casos la dirección del registro se tiene que escribir con 8 dígitos. 0x00000000. Ya va empeorando el asunto, pero esto no es nada.

En un micro de arquitectura ARM, la configuración de cada uno de los pines alcanza tal grado de complejidad (resistencias pullup/down, multiplexores, funciones analógicas, pwm, comunicación, velocidad, etc etc etc) que los registros empiezan a tener registros dentro. Y sin embargo, gracias a las librerías, logramos compactarlo todo a una serie de instrucciones muy simples y fáciles de recordar, por ejemplo lo que expliqué aquí.

¿Pero entonces se usa para algo el ensamblador? No, el ensamblador se ha sustituído completamente por las librerías, pero sí que usamos de vez en cuando un sucedáneo. Ya no nos gusta escribir la función movwf, sino que podemos hacer algo como:

portb = 0x08

Y en ocasiones, se da que no existen librerías para lo que quieres o simplemente lo vas a hacer más rápido tu directamente que ponerte a buscarlo, por lo que sí que puede pasar que escribas direcciones a mano...

0x06 = 0x08

Como esto cambiaba todo el puerto voy a hacer algo que sólo cambia el pin 4:

0x06.4 = 1

¿Entonces qué es eso de programar como un electrónico?

La programación que se hace en los ordenadores es muchísimas veces independiente del hardware, es decir, vas a hacerlo funcionar en cualquier ordenador, y no importan sus registros, direcciones, ni su configuración porque eso ya lo han implementado previamente los fabricantes de cada uno de los componentes (drivers) y se ha agrupado todo bajo el sistema operativo para hacerlo funcionar sin que te enteres.

En electrónica programamos microcontroladores muy dependientes del hardware y sus periféricos. Cuando hacemos una placa con un micro, le añadimos una señal de reloj, esta señal tiene que ser tratada previamente para que llegue a todos los periféricos correctamente, y esto se hace a nivel de registros (pero con librerías). Nosotros somos el fabricante. Cuando añadimos las entradas y salidas, configuramos cada uno de los pines para que haga exactamente su tarea. Se trata de saber dónde están y a dónde van los bits que quieres manejar, más que poner una función y dejar que el sistema operativo se preocupe por tí.

Estos métodos tienen ciertas ventajas, un mayor control sobre lo que estás haciendo y muchas veces, en vez de usar una librería que lo hace todo, puedes encender únicamente los periféricos que necesitas, con lo que ahorras energía. Otra ventaja es que durante el diseño tienes libertad de colocar las cosas como mejor te convenga, sin tener que seguir los esquemas del fabricante.

Espero que os haya parecido interesante

ARM: Introducción

En éste apartado voy a escribir lo que estoy aprendiendo en la asignatura de informática industrial, donde estamos viendo la programación de un microcontrolador basado en ARM Cortex. Además, se parece "sospechosamente" a lo que hay en éste blog sobre el curso de programación:

http://armpower.blogs.upv.es/materiales-arm-cortex-m-iniciacion-2/

Pero bueno...

Yo voy a pasar de "los microprocesadores y microcontroladores son blablabla y sirven para blablabla..." y me meto directamente en lo interesante del tema. Material del bueno.

¿Qué es ARM y por qué me interesa?

Es la empresa que diseña los núcleos de los microcontroladores, entre ellos las series Cortex-M, Cortex-R y Cortex-A.

La familia Cortex-A os puede sonar porque están integrándose en el mundo de la telefonía móvil. Unos pepinazos.

Pues bien, ésta empresa vende el diseño a empresas fabricantes como ST, Texas Instruments, Atmel, ... Y éstos le añaden unos periféricos (puertos de entrada/salida, relojes, todo aquello que el núcleo no lleva) y lo meten todo en uno de éstos:

Pongo una imagen porque, como que impresiona más

Además tienen la buena manía de añadirlos a placas de desarrollo ya construidas y fáciles de usar, tipo sistema embebido, y son baratas, muy buen precio, la verdad. Ésta es la que uso yo en clase y que, debido a mi diógenes electrónico, he adquirido:

Si es que mola sólo de verla, y con más razones.

STM32F4Discovery

Pues es éso, han cogido el integrado y te lo han puesto en una placa con sus pines, un par de botones y cosas bonitas, y a programar que se ha dicho. Ésta placa me costó unos 14 euros y tiene lo siguiente:

  • Un microcontrolador STM32F407VGT6, con un núcleo ARM Cortex-M de 32 bits,  1MB de Flash, 192KB de RAM y todo ésto a 168 MHz.
  • Un acelerómetro de 3 ejes
  • Un micrófono
  • Un conversor digital/analógico para audio
  • 4 LEDs integrados que puedes manejar a tu gusto
  • 2 botones: uno de reset y otro de usuario para que lo programes como quieras
  • Un USB para que conectes lo que puedas, aparte de otro para programación y debug
  • Montones y montones de pines para el resto de puertos y utilidades

Algo muy importante de éstos microcontroladores es que tienen un modo debug, en el que puedes ver a tiempo real cómo cambian los registros y posiciones en la memoria en la pantalla de tu ordenador.

Me ha gustado ¿cómo se programa eso?

Hay distintos programas y todos tienen sus tonterías. A mí el que me está funcionando es uno semi-gratuito llamado Keil: tienes una versión de prueba con todas las funcionalidades disponibles pero "solo" puedes programar 32KB de la memoria.

También hay otros de licencia gratuita, por ejemplo el CooCox.

En realidad la programación de un ARM es algo "especialita": todos los fabricantes tienen sus programas y librerías, todos los programas tienen unas funciones u otras, ...  Al final sale ARM un poco al medio y pone algo de orden con sus librería estándar. Es probable que no consigas que funcione a la primera, pero no te preocupes porque al final algo saldrá en claro.

Yo voy a usar el Keil versión 4, aunque puedes probar la versión 5. Descarga gratuita en Keil.com

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.