Está en la página 1de 70

"La práctica sin teoría es ciega y la teoría sin práctica es estéril" (Kant, 1793)

Programación de los microcontroladores en Lenguaje C (ANSI C Standard)


El lenguaje C data del año 1972; fue creado por Dennis Ritchie en los laboratorios Bell como resultado de la
necesidad de reescribir los sistemas operativos UNIX con el fin de optimizar el conocido código ensamblador. El
lenguaje C fue la evolución de lenguajes previos llamados B creado por Ken Thompson, y BCPL. El nuevo lenguaje C,
rápidamente tomó fuerza por su funcionalidad y facilidad en la implementación en diversos sistemas
computacionales que requerían códigos de máquina. En 1983 el Instituto de Estándares Americanos estableció un
estándar que definiera al lenguaje C, conocido como ANSI C.
La forma del lenguaje C, se fundamenta en un complejo estructural que requiere un amplio estudio de las mismas,
sin embargo para la programación de los microcontroladores el estudiante requiere una porción fundamental que le
permita iniciar y crear los primeros proyectos en XC8.

1.- Lenguajes de programación


1.1 Lenguaje Máquina
El microcontrolador ejecuta el programa cargado
en la memoria Flash. Esto se denomina el código
ejecutable o código objeto y está compuesto por
una serie de ceros y unos, aparentemente sin
significado. Dependiendo de la arquitectura del
microcontrolador, el código binario está compuesto
por palabras de 12, 14 o 16 bits de anchura. Cada
palabra se interpreta por la CPU como una
instrucción a ser ejecutada durante el
funcionamiento del microcontrolador. Todas las
instrucciones que el microcontrolador puede
reconocer y ejecutar se les denominan
colectivamente Conjunto de instrucciones. Como es
más fácil trabajar con el sistema de numeración
hexadecimal, el código ejecutable se representa con frecuencia como una serie de los números hexadecimales
denominada código Hex. En los microcontroladores PIC con las palabras de programa de 14 bits de anchura, el
conjunto de instrucciones tiene 35 instrucciones diferentes.

1.2 Lenguaje Ensamblador

Como el proceso de escribir un


código ejecutable en lenguaje
máquina era considerablemente
arduo, en consecuencia fue creado
el primer lenguaje de programación
denominado ensamblador (ASM).
Siguiendo la sintaxis básica del
ensamblador, era más fácil escribir
y comprender el código. Las
instrucciones en ensamblador
consisten en las abreviaturas con
significado(mnemónicos) y a cada
instrucción corresponde una
localidad de memoria. Un programa
denominado Ensamblador compila (traduce) las instrucciones del lenguaje ensamblador a código máquina (código
binario).
Este programa compila instrucción a instrucción sin optimización. Como permite controlar en detalle todos los
procesos puestos en marcha dentro del chip, este lenguaje de programación todavía sigue siendo popular.
A pesar de todos los lados buenos, el lenguaje ensamblador tiene algunas desventajas:

 Muy complejo de usar y difícil de manejar.

 Cada tipo de microcontrolador tiene su propio conjunto de instrucciones que un programador tiene que
conocer para escribir un programa

 Un programador tiene que conocer el hardware del microcontrolador para escribir un programa

1.3 Lenguaje de Alto Nivel


Los lenguajes de programación de alto nivel (Basic, Pascal, C, C++, etc.) fueron creados con el propósito de superar
las desventajas del ensamblador. En lenguajes de programación de alto nivel varias instrucciones en ensamblador se
sustituyen por una sentencia. El programador ya no tiene que conocer el conjunto de instrucciones pero si las
características del hardware del microcontrolador utilizado. Ya no es posible conocer exactamente cómo se ejecuta
cada sentencia, de todas formas ya no importa. Aunque siempre se puede insertar en el programa una secuencia
escrita en ensamblador.

Si alguna vez ha escrito un programa para un microcontrolador PIC en lenguaje ensamblador, probablemente sepa
que la arquitectura RISC carece de algunas instrucciones. Por ejemplo, no hay instrucción apropiada para multiplicar
dos números. Por supuesto, para cada problema hay una solución y éste no es una excepción gracias a la aritmética
que permite realizar las operaciones complejas al descomponerlas en un gran número operaciones más simples. En
este caso, la multiplicación se puede sustituir con facilidad por adición sucesiva (a x b = a + a + a + ... + a). Ya estamos
en el comienzo de una historia muy larga... No hay que preocuparse al utilizar uno de estos lenguajes de
programación de alto nivel como es C, porque el compilador encontrará automáticamente la solución a éste
problema y otros similares. Para multiplicar los números a y b, basta con escribir a*b.

Lenguaje C
El lenguaje C dispone de todas las ventajas de un lenguaje de programación de alto nivel (anteriormente descritas) y
le permite realizar algunas
operaciones tanto sobre los
bytes como sobre los bits
(operaciones lógicas,
desplazamiento etc.). Las
características de C pueden
ser muy útiles al programar
los microcontroladores.
Además, C está estandarizado
(el estándar ANSI), es muy
portable, así que el mismo
código se puede utilizar
muchas veces en diferentes
proyectos. Lo que lo hace
accesible para cualquiera que
conozca este lenguaje sin
reparar en el propósito de uso
del microcontrolador. C es un
lenguaje compilado, lo que significa que los archivos fuentes que contienen el código C se traducen a lenguaje
máquina por el compilador. Todas estas características hicieron al C uno de los lenguajes de programación más
populares.
2.- CARACTERÍSTICAS PRINCIPALES DEL XC8
A continuación vamos a
presentar a los
elementos principales
del lenguaje XC8
desarrollado por
Microchip. Este lenguaje
es muy similar al C
estándar, no obstante
en determinados
aspectos difiere del ANSI
estándar en algunas
características. Algunas
de estas diferencias se
refieren a las mejoras,
destinadas a facilitar la programación de los microcontroladores PIC, mientras que las demás son la consecuencia de
la limitación de la arquitectura del hardware de los PIC. Aquí vamos a presentar características específicas del
lenguaje XC8 en la programación de los microcontroladores PIC. El término C se utilizará para referirse a las
características comunes de los lenguajes C y XC8.

2.1 FASES DE COMPILACIÓN


El proceso de compilación consiste en varios pasos y se ejecuta automáticamente por el compilador. Por un
conocimiento básico del funcionamiento puede ser útil para entender el concepto del lenguaje XC8.

El archivo fuente contiene el


código en XC8 que usted
escribe para programar el
microcontrolador. El
preprocesador se utiliza
automáticamente por el
compilador al iniciarse el
proceso de la compilación. El
compilador busca las
directivas del preprocesador
(que siempre empiezan por
‘#’) dentro del código y
modifica el código fuente de
acuerdo con las directivas.
En esta fase se llevan a cabo
inclusión de archivos,
definición de constantes y
macros etc, lo que facilita el
proceso. El analizador
sintáctico (parser) elimina
toda la información inútil del código (comentarios, espacios en blanco). Luego, el compilador traduce el código a un
archivo binario denominado archivo .mcl. El enlazador (linker) recupera toda la información requerida para ejecutar
el programa de los archivos externos y la agrupa en un solo archivo. Además, un proyecto puede contener más de un
archivo fuente y el programador puede utilizar funciones predefinidas y agrupadas dentro de los archivos
denominados librerías. Por último, el generador .hex produce un archivo .hex. Es el archivo que se va a cargar en el
microcontrolador. El proceso entero de la compilación que incluye todos los pasos anteriormente descritos se le
denomina “building”.

2.2 ESTRUCTURA DE PROGRAMA


La idea principal de escribir un programa en C es de “romper”
un problema mayor en varios trozos más pequeños.
Supongamos que es necesario escribir un programa para el
microcontrolador para medir la temperatura y visualizar los
resultados en un LCD. El proceso de medición se realiza por un
sensor que convierte temperatura en voltaje. El
microcontrolador utiliza el convertidor A/D para convertir este
voltaje (valor analógico) en un número (valor digital) que luego
se envía al LCD por medio de varios conductores. En
consecuencia, el programa se divide en cuatro partes, de las
que cada una corresponde a una acción específica:

1. Activar y configurar el convertidor A/D incorporado;

2. Medir el valor analógico;

3. Calcular temperatura; y

4. Enviar los datos en el formato apropiado al LCD;

Los lenguajes de programación de alto nivel como es C le permiten solucionar este problema con facilidad al escribir
cuatro funciones que se ejecutarán cíclicamente sin parar.

La idea general es de dividir el problema en varios trozos, de los que cada uno se puede escribir como una sola
función. Todos los programas escritos en XC8 contienen por lo menos una función llamada main(void) que encierra
entre llaves { } las sentencias a ser ejecutadas. Esto es la primera función a ser ejecutada al iniciarse la ejecución de
programa. Las otras funciones se pueden llamar dentro de la función main. En otras palabras, podemos decir que la
función main(void) es obligatoria, mientras que las demás son opcionales. Si todavía no ha escrito un programa en
C, es probable que todo le resulte confuso. No se preocupe, acéptelo tal como es por el momento y más tarde
entenderá la sintaxis.

Todos los programas (código fuente) en XC8 tienen una estructura básica, a partir de la cual se desarrolla el
programa, como se muestra a continuación:

/*
* File: xxxxxxxxxxxxxx.c
* Author: xxxxxxxxxx
* Información del programa
* Created on xxxxxxxxxxxxx, hora xx:xx
*/
#include <stdlib.h> // Declara funciones para conversión numérica, asignación de memoria y otros
#include <stdio.h> // Archivo de la biblioteca estándar de Entrada/Salida
#include <xc.h> // Definición de registros y de sus bit’s del SFR

void main(void) {
---------------sentencias a ejecutar------
return;
}
Comentarios son usados para documentar un programa funcionalmente y para explicar que hace un bloque
particular o línea de código. Los comentarios son ignorados por el compilador. Existen dos formas de usar los
comentarios:
Comentario de Bloque /* este es un comentario de varias líneas */
Comentario de Bloque simple //este también es un comentario para una línea

El preprocesador acepta el código fuente como entrada y es responsable de:


•quitar los comentarios
•interpretar las directivas del preprocesador las cuales inician con #.
Por ejemplo:
#include -- incluye el contenido del archivo nombrado. Estos son usualmente llamados archivos de cabecera
(header). Por ejemplo:
 #include <xc.h> -- Archivo con la definición de registros y bit’s del SFR.
 #include <stdio.h> -- Archivo de la biblioteca estándar de Entrada/Salida.
#define -- define un nombre simbólico o constante. Por ejemplo:
 #define TAM_MAX_ARREGLO 100
#include <nombre_fichero> o #include "nombre_fichero" como se mencionó es una directiva que hace que el
compilador incluya en el fichero fuente el texto que contiene el archivo especificado en <nombre_fichero>. Si el
nombre del fichero se incluye entre los símbolos < > el compilador busca el fichero en el directorio INCLUDE que
está en la carpeta de instalación XC8. Si se pone entre comillas dobles " " el compilador busca primero en el
directorio actual o directorio de trabajo donde hemos guardado nuestro proyecto y si no lo encuentra, entonces lo
busca en los directorios INCLUDE del compilador.

2.3 CREACIÓN DE UN PROGRAMA EN XC8 DEL MPLABX


Para configurar el MPLABX y nos permita compilar nuestros proyectos se debe seguir los siguientes pasos:

1) Instalar MPLABX (http://www.microchip.com/pagehandler/en-


us/family/mplabx/#downloads) siguiendo las instrucciones del instalador.

2) Instalar XC8 (http://www.microchip.com/pagehandler/en-


us/family/mplabx/#downloads) también siguiendo las instrucciones del instalador.

Haciendo la instalación en este orden, el compilador XC8 se debería agregar a MPLABX.


Si no se instaló o se instaló al revés no hay problema, el compilador se debe agregar manualmente.

Pasos para agregar XC8 a la lista de compiladores de MPLABX de forma manual:


Abrir MPLABX y hacer click en Tool/Options:

Figura 1

Hacer click en la pestaña Embedded:

Figura 2

Ahora tenemos 2 opciones:

Hacemos click en Build Tools y seleccionamos el


compilador si es que aparece.
Si no, hacemos click en Add luego con Browse.. se busca el directorio donde está instalado XC8 y se selecciona el
ejecutable principal.

Luego de esto hacemos click en Ok y ya estará todo configurado.

Con estos pasos es posible agregar cualquier otro compilador que se quiera probar.

Figura 3
Terminado lo anterior lo primero que tenemos que hacer es
crear un proyecto en MPLABX. Para esto, se debe pulsar el
ícono New Proyect como se observa en la figura 3.

Se nos abre un diálogo que nos permite seleccionar el


proyecto que queremos crear.

Figura 4
Para crear un proyecto desde cero, de las varias
opciones mostradas escogemos la Standalone
Project de la categoría Microchip Embedded .
Luego Next>

En la siguiente pantalla debemos seleccionar el


pic a programar, en este caso es el PIC16F876A.

Figura 5

En la ventana de Device digitamos o buscamos el


código del microcontrolador y hacemos click en
el pic 16F876A. Hacemos click en Next>.
Figura 6

Ahora se nos pide que elijamos la herramienta


que utilizaremos para hacer debug de nuestro
programa. Como no se dispone de ningún
módulo de las que aparecen en la lista,
seleccionar Simulator y luego Next>.

Figura 7

Ahora debemos indicarle a MPLABX que


compilador utilizar.
Como podemos observar, aparecen los
compiladores agregados. Vamos a seleccionar
XC8 y luego Next.

Figura 8

Por último, debemos indicarle el nombre y en qué


lugar se guardará el proyecto.

En Projet Name escribimos primero1xc8.

En Projet Location pulsar Browse.. para buscar


el disco de trabajo y en ella crear una carpeta de
trabajo del MPLABXC8 por ejemplo, para nuestro
caso seleccionamos el disco D:\ y creamos la
carpeta progs_mplab_xc8 presionamos enter y
pulsar Abrir.
Figura 9

Luego de finalizar la creación del proyecto, la izquierda de la


pantalla nos queda como se ve en la figura 9.

Lo que muestra la imagen es el proyecto con todos los lugares


en los que se agregará código. Los más importantes, son:
Header Files: Se agregan los archivos ".h" que contendrán la
definición de macros, configuraciones y la cabecera de las
funciones.
Source Files: Se agregan los archivos ".c" que contendrán las
funciones que se compilarán. La única función que no puede
faltar es la llamada "main" que es el punto de entrada de toda
aplicación (a menos que se esté creando una librería).

A todo proyecto se le puede agregar la siguiente estructura de archivos base:


Header Files:
system.h : Macros y funciones para configurar el sistema.
user.h : Macros y funciones específicas de la aplicación.
Source Files:
configuration_bits.c : Programación de los bits de configuración del sistema.
system.c : Funciones que configuran el sistema (Por ejemplo el Oscilador)
main.c : Programa principal
user.c : Algoritmos de la aplicación.

Ahora para editar el programa fuente se sigue los siguientes pasos.

Figura 10

Para agregar un archivo ".c" donde escribiremos nuestro


programa, hacemos click derecho en Source Files y
seleccionamos "New”, luego finalmente “main.c…”.

Figura 11

En esta pantalla en File Name ponemos el


nombre del programa fuente a trabajar por
ejemplo primero1xc8 y pulsamos Finish y
queda como se ve en la figura siguiente donde
se completará con el resto del programa que
se tenga pensado trabajar. (Ver figura 12)
Figura 12

2.4 VARIABLES, IDENTIFICADORES, CONSTANTES Y TIPOS DE DATOS


La programación de PIC en C se puede comprender mejor si se estudian sus elementos básicos; una vez que se
dominen estos elementos se podrá dar solución a la gran mayoría de problemas de programación. El propósito de la
mayoría de los programas es resolver un problema. Los programas resuelven los problemas por medio de la
manipulación de información o datos. Normalmente los programas se caracterizan por permitir el ingreso de
información, tener uno o varios lugares de almacenamiento de dicha información, contar con las instrucciones para
manipular estos datos y obtener algún resultado del programa que sea útil para el usuario. También, las
instrucciones se pueden organizar de tal forma que algunas de ellas se ejecuten sólo cuando una condición
específica (o conjunto de condiciones) sea verdadera, otras instrucciones se repitan un cierto número de veces y
otras pueden ser agrupadas en bloques que se ejecutan en diferentes partes de un programa.

Una variable es un nombre que representa una o más localizaciones de memoria usadas para tener los datos. Una
variable puede ser entendida como un contenedor que puede almacenar datos usados en un programa. A una
variable también se le conoce como un identificador.

Un identificador es un nombre con el que se hace referencia


a una función o al contenido de una zona de la memoria
(variable, constante). Cada lenguaje tiene sus propias reglas
respecto a las posibilidades de elección de nombres para las
funciones y variables. En ANSI C estas reglas son las
siguientes:

1. Un identificador se forma con una secuencia de letras (minúsculas de la a a la z; mayúsculas de la A a la


Z; y dígitos del 0 al 9).
2. El carácter subrayado o underscore (_) se considera como una letra más.
3. Un identificador no puede contener espacios en blanco, ni otros caracteres distintos de los citados,
como por ejemplo (* , ; . : - + etc.).
4. El primer carácter de un identificador debe ser siempre una letra o un (_), es decir, no puede ser un
dígito.
5. Se hace distinción entre letras mayúsculas y minúsculas. Así, Masa es considerado como un identificador
distinto de masa y de MASA.
6. ANSI C permite definir identificadores de hasta 31 caracteres de longitud.

Ejemplos de identificadores válidos son los siguientes: a, b, tiempo, distancia1, caso_A, PI, velocidad_de_la_luz

Por el contrario, los siguientes nombres no son válidos: 1_valor, tiempo-total, dolares$, %final
En general es muy aconsejable elegir los nombres de las funciones y las variables de forma que permitan conocer a
simple vista qué tipo de variable o función representan, utilizando para ello tantos caracteres como sean necesarios.
Esto simplifica enormemente la tarea de programación y –sobre todo– de corrección y mantenimiento de los
programas.

Las variables definidas en un programa pueden ser de tipo LOCAL o GLOBAL. Las variables locales solo se utilizan en
la función donde se encuentra declarada. Las variables globales se pueden utilizar en todas las funciones del
programa. Ambas deben ser declaradas antes de ser utilizadas y las globales deben declararse antes de cualquier
función y fuera de ellas. Las variables globales son puestas a cero cuando se inicia la función principal.

En C, como en cualquier otro lenguaje, existen una serie de palabras clave (keywords) que el usuario no puede
utilizar como identificadores (nombres de variables, constantes y/o de funciones). A continuación se presenta la lista
de las 32 palabras clave del ANSI C, es importante evitarlas como identificadores:

auto double int struct


break else long switch
case enum register typedef
char extern return union
const float short unsigned
continue for signed void
default goto sizeof volatile
do if static while

CONSTANTES. Las variables pueden cambiar de valor a lo largo de la ejecución de un programa, o bien en
ejecuciones distintas de un mismo programa. Además de variables, un programa utiliza también constantes, es decir,
valores que siempre son los mismos. Un ejemplo típico es el número pi(π), que vale 3.141592654. Este valor, con
más o menos cifras significativas, puede aparecer muchas veces en las sentencias de un programa. En C existen
distintos tipos de constantes:

1. Constantes numéricas. Son valores numéricos, enteros o de punto flotante. Se permiten también constantes
octales (números enteros en base 8) y hexadecimales (base 16).
2. Constantes carácter. Cualquier carácter individual encerrado entre apóstrofos (tal como 'a', 'Y', ')', '+', etc.)
es considerado por C como una constante carácter, o en realidad como un número entero pequeño (entre 0
y 255, o entre -128 y 127, según los sistemas). Existe un código, llamado código ASCII, que establece una
equivalencia entre cada carácter y un valor numérico correspondiente.
3. Cadenas de caracteres. Un conjunto de caracteres alfanuméricos encerrados entre comillas es también un
tipo de constante del lenguaje C, como por ejemplo: "espacio", "Esto es una cadena de caracteres", etc.
4. Constantes simbólicas. Las constantes simbólicas tienen un nombre (identificador) y en esto se parecen a las
variables. Sin embargo, no pueden cambiar de valor a lo largo de la ejecución del programa. En C se pueden
definir mediante el preprocesador o por medio de la palabra clave const.

Tipos de datos en CX8. En el lenguaje C, los datos tienen un tipo, o sea, cada dato utilizado en el programa debe
tener su tipo especificado. Esto permite al compilador conocer el tamaño de dato (número de bytes requerido en la
memoria) y su representación. Hay varios tipos de datos que se pueden utilizar en el lenguaje de programación CX8
dependiendo del tamaño de dato y del rango de valores. La tabla muestra el rango de valores que los datos pueden
tener cuando se utilizan en su forma básica.
TABLE: INTEGER DATA TYPES
Type Size (bits) Arithmetic Type Valores que soporta
bit 1 Unsigned integer 0 ó 1
signed char 8 Signed integer -128 a 127
unsigned char 8 Unsigned integer 0 a 255
signed short 16 Signed integer -32768 a 32767
unsigned short 16 Unsigned integer 0 a 65535
signed int 16 Signed integer -32768 a 32767
unsigned int 16 Unsigned integer 0 a 65535
signed short long 24 Signed integer -8388608 a 8388607
unsigned short long 24 Unsigned integer 0 a 16777215
signed long 32 Signed integer -2147483648 a 2147483647
unsigned long 32 Unsigned integer 0 a 4294967295
signed long long 32 Signed integer
unsigned long long 32 Unsigned integer

Debido a las limitaciones impuestas por el hardware del microcontrolador, es imposible alcanzar una mayor
precisión de datos que la del tipo float. Por eso, el tipo double long en CX8 equivale al tipo float.

TABLE: FLOATING-POINT DATA TYPES

Type Size (bits) Arithmetic Type


float 24 or 32 Real
double 24 or 32 Real
long double same as double Real

Al añadir un prefijo (calificador) a cualquier tipo de dato entero o carácter, el rango de sus posibles valores cambia
así como el número de los bytes de memoria necesarios. Por defecto, los datos de tipo int son con signo, mientras
que los de tipo char son sin signo. El calificador signed (con signo) indica que el dato puede ser positivo o negativo. El
prefijo unsigned indica que el dato puede ser sólo positivo.

En resumen, la declaración de variables se realiza indicando el tipo de variable seguido de un nombre que el
desarrollador asigna arbitrariamente a la variable. En el punto de la declaración de una variable es posible dar un
valor inicial a cada una de las variables sin embargo este último no es estrictamente necesario. Por último la
declaración debe culminar con el carácter punto y coma (;).

Ejemplos de declaración de variables válidos:


bit variable_bit; //Declaración de una variable tipo bit
char CARÁCTER; //Declaración de una variable tipo char
char CARACTER1=’K’; // Declaración de una variable tipo char inicializada con el valor ASCII de K
int a=123 // Declaración de una variable tipo entero inicializada con el valor 123
float b=-11.24 // Declaración de una variable con punto decimal inicializada con el valor -11.24
double decimal=86.56 // Declaración de una variable con punto decimal inicializada con el valor 86.56
long ENTERO=-162459 // Declaración de una variable de tipo entero largo inicializada con el valor -162459

Ejemplos de cómo declarar variables sin signo:


unsigned char CARÁCTER; // Declaración de una variable tipo char sin signo
unsigned int a; // Declaración de una variable tipo entero sin signo
unsigned long entero; // Declaración de una variable tipo entero largo sin signo

Las variables también pueden ser declaradas en un formato que asocia varias variables a un mismo nombre, este
formato se conoce como una cadena de variables, o un vector e incluso puede ser una matriz de variables, en
conclusión este tipo de declaraciones pueden ser de una o más dimensiones.
El siguiente ejemplo muestra un vector de caracteres, o también conocido como una cadena de caracteres:
char Texto[20]; //Cadena de caracteres con 20 posiciones de memoria.

De igual manera las cadenas de caracteres o de variables pueden ser declaradas con un valor inicial, este tipo
de declaración se puede ver en el siguiente ejemplo:
char Texto[20] = “Nuevo Texto”; //Declaración de una cadena de caracteres
//inicializada con el texto: Nuevo Texto.
int Enteros[5]={5,4,3,2,1}; //Declaración de una cadena de enteros con valores iniciales.
float Decimales[3]={0.8,1.5,5.8}; //Declaración de una cadena de números con
//punto decimal inicializadas.
Ejemplo 1: Para declarar una variable que pueda almacenar el nombre de una persona y que, inicialmente, contenga
el valor "Isabel", escribiremos: char nombre[7] = "Isabel";
Un dato de tipo cadena es un dato compuesto (estructurado), debido a que está formado por una agrupación de
caracteres.
Pues bien, dicha agrupación se define por medio de un array. Un array agrupa, bajo el mismo nombre de variable, a
una colección de elementos (datos) del mismo tipo.
Para declarar un array de caracteres, después del identificador de la variable, se tiene que escribir, entre corchetes
"[]", el número de caracteres que se van a almacenar en el array, más uno. Por tanto, en este caso, puesto que
"Isabel" tiene seis caracteres, hay que escribir un 7 entre los corchetes.
Se tiene que escribir un número más, porque en la memoria se va a reservar espacio
para los seis caracteres de la cadena "Isabel", más uno, conocido éste como el carácter
nulo, el cual se representa mediante una barra invertida y un cero (\0). El sistema se
encarga de "poner" dicho carácter, que indica el fin de la cadena.
Por consiguiente, en la memoria se almacenarán siete caracteres consecutivos, como se
observa en la figura.
Los caracteres del array pueden ser referenciados mediante el identificador del mismo,
seguido de un número entre corchetes. A dicho número, de manera formal, se le llama
"índice", y puede oscilar entre el valor 0 y n-1, siendo n el número de caracteres que
pueden ser almacenados en memoria en el array, en este caso 7.

<nombre_de_la_variable>[<índice>]

Por ejemplo, nombre[3] hace referencia al espacio de memoria donde está el carácter 'b'.

Ejemplo 2: Si se declara la variable de tipo cadena:


char nombre[7] = "June";
En memoria tendremos:

Fíjese que, en esta ocasión, el array de caracteres nombre ha sido inicializado con el valor
"June". De manera que, el fin de la cadena se encuentra en nombre[4], y no en el último
espacio de memoria reservado para el array, ya que, "June" tiene, solamente, cuatro
caracteres.
En este caso estamos desperdiciando espacios de memoria, lo que no es óptimo.

2.5 FORMATOS NUMÉRICOS USADOS EN EL LENGUAJE C


Los programas en lenguaje C usan números en diferentes bases numéricas, a pesar de que para el trabajo de
bajo nivel del microcontrolador todos sus números están procesados en base 2 es decir en números binarios. El ser
humano no está acostumbrado a pensar y procesar operaciones en esta base numérica. Desde las primeras etapas
de la formación académica las escuelas y colegios enseñan a pensar y procesar todos los cálculos en base 10, es decir
con números decimales. Es por esto que los compiladores en lenguaje C trabajan los números decimales facilitando
los diseños para el desarrollador. Además del sistema decimal el compilador en lenguaje C puede trabajar otras
bases tales como el binario, y hexadecimal haciendo más simple realizar cálculos y tareas que en decimal serían más
complejas. Los sistemas numéricos en base 2, 10, y 16, son los implementados en este compilador en lenguaje C. La
escritura de números decimales, es la forma de mayor simplicidad ya que se escriben en lenguaje C de la misma
manera convencional que se aprende desde los primeros cursos de matemáticas. Los números binarios se escriben
con el encabezado 0b seguidos del número en binario, un ejemplo de está escritura es: 0b10100001 que es
equivalente al número decimal 161. Los números en base 16 o hexadecimales se denotan con el encabezado 0x
precedidos de un número en hexadecimal, la expresión 0x2A, es un ejemplo de un número hexadecimal en lenguaje
C, y equivale a 42 en decimal. Los números binarios solo pueden tener dos dígitos que son el 0 y el 1. Los números
hexadecimales pueden tener 16 dígitos que son; 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E y F.
2.6 OPERADORES
Un operador es un carácter o grupo de caracteres que actúa sobre una, dos o más variables para realizar una
determinada operación con un determinado resultado. Ejemplos típicos de operadores son la suma (+), la diferencia
(-), el producto (*), etc. Los operadores pueden ser unarios, binarios y ternarios, según actúen sobre uno, dos o tres
operandos, respectivamente.

Los operadores aritméticos son los más sencillos de entender y de utilizar. Todos ellos son operadores binarios. En C
se utilizan los cinco operadores siguientes:
– Suma: +
– Resta: -
– Multiplicación: *
– División: /
– Resto: %
Todos estos operadores se pueden aplicar a constantes, variables y expresiones. El resultado es el que se obtiene de
aplicar la operación correspondiente entre los dos operandos. El único operador que requiere una explicación
adicional es el operador resto %. En realidad su nombre completo es resto de la división entera. Este operador se
aplica solamente a constantes, variables o expresiones de tipo int. Aclarado esto, su significado es evidente:
23%4 es 3, puesto que el resto de dividir 23 por 4 es 3. Si a%b es cero, a es múltiplo de b.
Ejemplos con las siguientes variables:
int A, B, C;
La suma aritmética entre dos o más números, se puede hacer como se ve en el siguiente ejemplo:
C = A+B; //Está expresión guarda la suma de A y B en la variable C.
C = A+B+C; //Está expresión guarda la suma de A, B y C en la variable C.
La resta aritmética entre dos o más números, se puede hacer como se ve en el siguiente ejemplo:
C = A-B; //Está expresión guarda la diferencia entre A y B en la variable C.
C = A-B-C; //Está expresión guarda la diferencia entre A, B y C en la variable C.
La operación matemática de multiplicación se puede realizar entre dos o más números, la operación se
relaciona con el carácter asterisco (*), está expresión se puede apreciar con mayor claridad en los siguientes
ejemplos:
C = A*B; //Está expresión guarda la multiplicación entre A y B en la variable C.
C = A*B*C; //Está expresión guarda la multiplicación entre A, B y C en la variable C.
La división aritmética en lenguaje C se especifica por medio de la barra inclinada (/), en el siguiente ejemplo
se puede observar su implementación:
C = A/B; //Está expresión guarda la división A entre B en la variable C.
La operación módulo calcula el módulo de una división aritmética es decir calcula el residuo de una división,
el cálculo del módulo se denota con el carácter de porcentaje, (%), la aplicación de está operación se puede
ver en el siguiente ejemplo:
C = A%B; //Está expresión guarda el residuo de la división de A entre B en la variable C.

Las operaciones aritméticas pueden ser usadas en forma combinada, es decir que se pueden mezclar varias
operaciones en una misma expresión, para ver esto con mayor claridad observe los siguientes ejemplos:
C = (A+B)/C; //Está expresión es equivalente a C = (A+B)÷C.
C = (A/B)*C; // Está expresión es equivalente a C = (A÷B) X C.

Como se verá más adelante, una expresión es un conjunto de variables y constantes –y también de otras expresiones
más sencillas– relacionadas mediante distintos operadores. Un ejemplo de expresión en la que intervienen
operadores aritméticos es el siguiente polinomio de grado 2 en la variable x:
5.0 + 3.0*x - x*x/2.0
Las expresiones pueden contener paréntesis (...) que agrupan a algunos de sus términos. Puede haber paréntesis
contenidos dentro de otros paréntesis. El significado de los paréntesis coincide con el habitual en las expresiones
matemáticas, con algunas características importantes que se verán más adelante. En ocasiones, la introducción de
espacios en blanco mejora la legibilidad de las expresiones.

Operadores de asignación. Los operadores de asignación atribuyen a una variable –es decir, depositan en la zona de
memoria correspondiente a dicha variable– el resultado de una expresión o el valor de otra variable (en realidad,
una variable es un caso particular de una expresión). El operador de asignación más utilizado es el operador de
igualdad (=), que no debe ser confundido con la igualdad matemática. Su forma general es:
nombre_de_variable = expresion;
Cuyo funcionamiento es como sigue: se evalúa expresion y el resultado se deposita en nombre_de_variable,
sustituyendo cualquier otro valor que hubiera en esa posición de memoria anteriormente. Una posible utilización de
este operador es como sigue:
variable = variable + 1;

Desde el punto de vista matemático este ejemplo no tiene sentido (¡Equivale a 0 = 1!), pero sí lo tiene considerando
que en realidad el operador de asignación (=) representa una sustitución; en efecto, se toma el valor de variable
contenido en la memoria, se le suma una unidad y el valor resultante vuelve a depositarse en memoria en la zona
correspondiente al identificador variable, sustituyendo al valor que había anteriormente. El resultado ha sido
incrementar el valor de variable en una unidad.

Así pues, una variable puede aparecer a la izquierda y a la derecha del operador (=). Sin embargo, a la izquierda del
operador de asignación (=) no puede haber nunca una expresión: tiene que ser necesariamente el nombre de una
variable. Es incorrecto, por tanto, escribir algo así como:
a + b = c; // incorrecto

Existen otros cuatro operadores de asignación (+=, -=, *= y /=) formados por los 4 operadores aritméticos seguidos
por el carácter de igualdad. Estos operadores simplifican algunas operaciones recurrentes sobre una misma variable.
Su forma general es:
variable op= expresion;

Donde op representa cualquiera de los operadores (+ - * /). La expresión anterior es equivalente a:


variable = variable op expresion;

A continuación se presentan algunos ejemplos con estos operadores de asignación: distancia += 1; equivale a:
distancia = distancia + 1;
rango /= 2.0 equivale a: rango = rango /2.0
x *= 3.0 equivale a: x = x * 3.0

Operadores incrementales. Los operadores incrementales (++) y (--) son operadores unarios que incrementan o
disminuyen en una unidad el valor de la variable a la que afectan. Estos operadores pueden ir inmediatamente
delante o detrás de la variable. Si preceden a la variable, ésta es incrementada antes de que el valor de dicha
variable sea utilizado en la expresión en la que aparece. Si es la variable la que precede al operador, la variable es
incrementada después de ser utilizada en la expresión. A continuación se presenta un ejemplo de estos operadores:
i = 2;
j = 2;
m = i++; // despues de ejecutarse esta sentencia m=2 e i=3
n = ++j; // despues de ejecutarse esta sentencia n=3 y j=3

Estos operadores son muy utilizados. Es importante entender muy bien por qué los resultados m y n del ejemplo
anterior son diferentes.
Ejemplos para aclarar lo mencionado con las variables siguientes:
int A=100, B=10;
A++; //Este operador incrementa en una unidad el valor de A.
A--; //Este operador decrementa en una unidad el valor de A.
A+=4; //Este operador incrementa en 4 el valor de A.
A-=5; //Este operador decrementa en 5 el valor de A.
A/=4; //Este operador divide el valor de A en 4.
A*=3; //Este operador multiplica el valor de A por 3.
A+=B; //Este operador incrementa el valor de A en el valor de B unidades.
A*=B; //Este operador multiplica el valor de A por B veces.

Completamos el programa anterior con lo que se muestra a continuación que viene a ser el ejemplo primero1xc8:
/*
* File: primero1xc8.c
* Author: xxxxxxxxxx
* Programa de uso de tres variables con los operadores de asignación
* Created on xxxxxxxxxxxxx, hora xx:xx
*/

#include <xc.h> // Definición de registros y de sus bit’s del SFR

void main(void) {

int a=32000,b=100, c=80; //


linea16:
a = b+c; //
c-=10; //
b = b * c; //
b = a / c; //
a = (a + c)-b; //
c = (a + b) / c; //
b = (a + b)/((c + b)* b); //
c = a % b; //
c=a++; //
a=b--; //
a=++c; //
c=--b; //
b=--c * a++; //
a=32000,b=100, c=80;
goto linea16;
return;
}

Operadores relacionales. Este es un apartado especialmente importante para todas aquellas personas sin
experiencia en programación. Una característica imprescindible de cualquier lenguaje de programación es la de
considerar alternativas, esto es, la de proceder de un modo u otro según se cumplan o no ciertas condiciones. Los
operadores relacionales permiten estudiar si se cumplen o no esas condiciones. Así pues, estos operadores
producen un resultado u otro según se cumplan o no algunas condiciones que se verán a continuación.

En el lenguaje natural, existen varias palabras o formas de indicar si se cumple o no una determinada condición. En
inglés estas formas son (yes, no), (on, off), (true, false), etc. En Informática se ha hecho bastante general el utilizar la
última de las formas citadas: (true, false). Si una condición se cumple, el resultado es true; en caso contrario, el
resultado es false.
En C un 0 representa la condición de false, y cualquier número distinto de 0 equivale a la condición true. Cuando el
resultado de una expresión es true y hay que asignar un valor concreto distinto de cero, por defecto se toma un
valor unidad. Los operadores relacionales de C son los siguientes:
>(alt+62) Mayor que a>b
>= Mayor o igual que a>=b
< (alt+60) Menor que a<b
<= Menor o igual que a<=b
== Igual que a==b
!= No igual que a!=b
Todos los operadores relacionales(op) son operadores binarios (tienen dos operandos), y su forma general es la
siguiente:
expresion1 op expresion2
Donde op es uno de los operadores (==, <, >, <=, >=, !=). El funcionamiento de estos operadores es el siguiente: se
evalúan expresion1 y expresion2, y se comparan los valores resultantes. Si la condición representada por el
operador relacional se cumple, el resultado es 1; si la condición no se cumple, el resultado es 0.
A continuación se incluyen algunos ejemplos de estos operadores aplicados a constantes:
(2==1); // resultado=0 porque la condición no se cumple
(3<=3); // resultado=1 porque la condición se cumple
(3<3) ; // resultado=0 porque la condición no se cumple
(1!=1); // resultado=0 porque la condición no se cumple

Operadores lógicos. Los operadores lógicos son operadores binarios que permiten combinar los resultados de los
operadores relacionales, comprobando que se cumplen simultáneamente varias condiciones, que se cumple una u
otra, etc. El lenguaje C tiene dos operadores lógicos: el operador Y (&&) y el operador O (||). En inglés son los
operadores and y or. Su forma general es la siguiente:
expresion1 || expresion2; // expresion1 OR expresion2
expresion1 && expresion2; // expresion1 AND expresion2

El operador && devuelve un 1 si tanto expresion1 como expresion2 son verdaderas (o distintas de 0), y 0 en caso
contrario, es decir si una de las dos expresiones o las dos son falsas (iguales a 0); por otra parte, el operador ||
devuelve 1 si al menos una de las expresiones es cierta. Es importante tener en cuenta que los compiladores de C
tratan de optimizar la ejecución de estas expresiones, lo cual puede tener a veces efectos no deseados. Por ejemplo:
para que el resultado del operador && sea verdadero, ambas expresiones tienen que ser verdaderas; si se evalúa
expresion1 y es falsa, ya no hace falta evaluar expresion2, y de hecho no se evalúa. Algo parecido pasa con el
operador ||: si expresion1 es verdadera, ya no hace falta evaluar expresion2. Los operadores && y || se pueden
combinar entre sí –quizás agrupados entre paréntesis–, dando a veces un código de más difícil interpretación. Por
ejemplo:
(2==1) || (-1==-1); // el resultado es 1
(2==2) && (3==-1); // el resultado es 0
((2==2) && (3==3)) || (4==0); // el resultado es 1
((6==6) || (8==0)) && ((5==5) && (3==2)); // el resultado es 0
Si A=10 y B=20:
(A==10)&&(B==20); // el resultado es ¿?
(A!=5)&&(B>2); // el resultado es ¿?
(A<=4)||(B>=6) ; // el resultado es ¿?
(A>3)&&(B!=4); // el resultado es ¿?

Operador negación lógica (!). Este operador devuelve un cero (false) si se aplica a un valor distinto de cero (true), y
devuelve un 1 (true) si se aplica a un valor cero (false). Su forma general es:
!expresion
p.e.: !(cont>max) || ((max==57) && (cont>=10))

Para entender los ejemplos observe la siguiente tabla de verdad:


Entrada O NOR NAND XNOR
Entrada 2 AND XOR NOT
1 R
0 0 0 0 0 1 1 1 1
0 1 1 0 1 1 0 1 0
1 0 1 0 1 0 0 1 0
1 1 1 1 0 0 0 0 1

OPERADORES DE MANEJO DE BITS


A diferencia de las operaciones lógicas que se realizan sobre los valores o expresiones, las operaciones de manejo de
bits se realizan sobre los bits de un operando. Se enumeran en la siguiente tabla:
OPERADOR DESCRIPCIÓN EJEMPLO RESULTADO

~ Complemento a uno a = ~b b=5 a = -5


<< Desplazamiento a la izquierda a = b << 2 b = 11110011 a = 11001100
>> Desplazamiento a la derecha a = b >> 3 b = 11110011 a = 00011110
a = 11100011
&(alt+38) Y lógico para manejo de bits c=a&b c = 11000000
b = 11001100

a = 11100011
|(alt+124) O lógico para manejo de bits c=a|b c = 11101111
b = 11001100

a = 11100011
^ EXOR lógico para manejo de bits c=a^b c = 00101111
b = 11001100

Note que el resultado de la operación de desplazamiento a la derecha depende del signo de la variable. En caso de
que el operando se aplique a una variable sin signo o positiva, se introducirán los ceros en el espacio vacío creado por
desplazamiento. Si se aplica a un entero con signo negativo, se introducirá un 1 para mantener el signo correcto de la
variable.
Otro Ejemplo: unsigned short a = 0b11010011,b= 0b01010001,c;
c=a&b; //c será 0b01010001
c=a|b; //c será 0b11010011
c=a^b; //c toma el valor 0b10000010
c= ~a; //c adopta el valor 0b00101100
c= a >> 3; //c: 0b00011010 entran 3 ceros por izq. y “pierde” los 3 salientes
c= b << 3; //c: 0b10001000 entran 3 ceros por dcha. y “pierde” los 3 salientes
La implementación de las operaciones lógicas NAND, NOR, XNOR, son similares a AND, OR, y XOR, agregando el
carácter de negación virgulilla, observe los siguieres ejemplos:
unsigned charVALOR1=0b01010000; //Variable inicializada en binario con el número 80.
unsigned char VALOR2=0b01011111; //Variable inicializada en binario con el número 95.
unsigned char RESULTADO;
RESULTADO = ~(VALOR1|VALOR2); //El valor de la operación lógica NOR , es guardado en RESULTADO.
RESULTADO = ~(VALOR1&VALOR2); //El valor de la operación lógica NAND, es guardado en RESULTADO.
RESULTADO = ~(VALOR1^VALOR2); //El valor de la operación lógica XNOR, es guardado en RESULTADO.

Ahora practicaremos con el siguiente programa ejemplo que corresponde a primero2xc8.


/*
* File: primero2xc8.c
* Author: xxxxxxxxxx
* Programa de uso de variables con los operadores de bit
* Created on xxxxxxxxxxxxx, hora xx:xx
*/
#include <xc.h> // Definición de registros y de sus bit’s del SFR

bit b1, b2;


void main(void) {
unsigned char m; //
unsigned int n; //
linea16:
m = 0x48; //
m = m & 0x0F; //
m = m | 0x24; //
m = m & 0b11110000; //
n = 0xFF00; //
n = ~n; //
m = m | 0b10000001; //
m = m & 0xF0; //
m = m ^ 0b00110000; //
m = 0b00011000; //
m = m >> 2; //
n = 0xFF1F; //
n = n << 12; //
m = m << 8; //
n++; //
m++; //
b1 = !(m == n); //
b2 = ((n > m)&&(m <= 2)); //
b1 = (m != 5)&&(n >0xff01); //
b2 = (n <= 4) || (m >= 6); //
return;
}

2.7 FUNCIONES EN LENGUAJE C


Una función es una fracción de código que realiza una tarea específica cada vez que ésta es invocada por el flujo del
programa principal. Las funciones cuentan con dos características fundamentales, uno o más parámetros de entrada,
y un parámetro de salida. Cualquiera de estas dos características puede ser una variable o un arreglo de ellas, de
igual manera estos parámetros de entrada y salida pueden ser vacíos.
Las funciones en C tienen el siguiente formato:

tipo_del_resultado NOMBRE(tipo_param1 param1, tipo_param2 param2, ... )


{
/* Cuerpo de la función */
}

Donde:
• Tipo_del_resultado: Tipo de dato que devuelve la función.
• NOMBRE: Nombre asignado a la función.
• lista de parámetros: Datos de entrada con los que trabaja la función

Ejemplos de prototipos de funciones:


int potencia (int base, int exponente);
float suma (float n1, float n2);
void mostrarDatos (int a, int b);
int leerDato(void);

Cuando se invoca una función se asignan valores a sus parámetros y comienza a ejecutar el cuerpo hasta que se llega
al final o se encuentra la instrucción return. Si la función devuelve un resultado, esta instrucción debe ir seguida del
dato a devolver.

Paso de parámetros a una función


Los parámetros son variables locales a los que se les asigna un valor antes de comenzar la ejecución del cuerpo de
una función. Su ámbito de validez, por tanto, es el propio cuerpo de la función. El mecanismo de paso de parámetros
a las funciones es fundamental para comprender el comportamiento de los programas en C.
Considera el siguiente programa:

1 int addition(int a, int b)


2 {
3 return (a + b);
4 }
5 int main()
6 {
7 int x = 10;
8 int y = 20;
9 int z;
10
11 z = addition(x, y);
12 }
Los parámetros a y b declarados en la línea 1 son válidos únicamente en la expresión de la línea 3. Las
variables x, y y z, por su lado, son válidas en el cuerpo de la función main (líneas 7 a 11).
El ámbito de las variables x, y y z (ámbito llamador), y el de las variables a y b (ámbito llamado) son totalmente
diferentes. El ámbito llamador desaparece temporalmente cuando se invoca la función desde la lińea 11. Durante
esa ejecución, el ámbito llamado es el visible. Al terminar la función, el ámbito llamado desaparece y se recupera el
ámbito llamador.
La comunicación entre estos dos ámbitos se realiza en la línea 11. Antes de comenzar la ejecución de la función, los
valores de las variables del ámbito llamador son copiados sobre las variables del ámbito llamado. Cuando termina
la ejecución de la función, la expresión de la llamada en la línea 11 se reemplaza por el valor devuelto.

El paso de parámetros y la devolución de resultado en las funciones C se realiza por valor, es decir, copiando los


valores entre los dos ámbitos.
Las variables locales de la función (incluidos los parámetros) desaparecen al término de la función, por lo que
cualquier valor que se quiera guardar, debe ser devuelto como resultado.

La estructura de las funciones se puede apreciar en los siguientes ejemplos:


void Funcion ( void ) //Función con parámetros de entrada y salida vacíos.
{ // Apertura de la función con corchete.
//Porción de código donde se ejecutan la rutina de la función.
} //Cierre de la función con corchete.

void Funcion ( int A ) //Función con un parámetro de entrada y salida vacía.


{ // Apertura de la función con corchete.
//Porción de código donde se ejecutan la rutina de la función.
} //Cierre de la función con corchete.

int Funcion ( void ) //Función con parámetro de entrada vacío y salida entera.
{ // Apertura de la función con corchete.
//Porción de código donde se ejecutan la rutina de la función.
} //Cierre de la función con corchete.

int Funcion ( int A ) //Función con un parámetro de entrada y salida enteras.


{ // Apertura de la función con corchete.
//Porción de código donde se ejecutan la rutina de la función.
} //Cierre de la función con corchete.
Funciones definidas por el usuario:
• La definición de una función se realiza escribiendo primero el tipo del valor de retorno de la función, después el
nombre de la función, posteriormente entre paréntesis las variables que utilizará dicha función (parámetros) y
finalmente las instrucciones de la función.
• Las funciones definidas por el usuario se invocan por su nombre y los parámetros opcionales que se puedan tener.
• Ejemplo1:
double promedio( int a, int b, int c)
{
return (a + b + c ) / 3.0;
}
Declara a la función promedio, la cual recibe tres valores enteros, calcula y regresa el promedio de ellos.
• Ejemplo2:
int suma( int a, int b)
{
return (a + b);
}
Declara a la función suma, la cual recibe dos valores enteros y calcula y regresa la suma de ellos.

Los nombres que se designan a las funciones cumplen con las mismas reglas para nombrar las variables. El siguiente
ejemplo muestra una función que es capaz de calcular el producto de dos números con punto decimal:

float Producto ( float A, flota B ) //Función para calcular el producto de A y B.


{ // Apertura de la función con corchete.
float RESULTADO;
RESULTADO = A*B; //Producto de A y B.
return RESULTADO; //La función retorna el resultado guardado en RESULTADO.
} //Cierre de la función con corchete.

Una función puede recurrir a otra función para realizar funciones más complejas, para demostrar está situación se
puede ver el siguiente ejemplo, que realiza el cálculo del área de una circunferencia en función de su radio:

Figura 2-1

El área de la circunferencia se calcula por medio de la ecuación A=πr², donde el valor de π es


aproximadamente 3,1416.

float Valor_PI ( void ) //Función que retorna el valor de π.


{ // Apertura de la función con corchete.
float PI=3.1416;
return PI;
} //Cierre de la función con corchete.

En el caso en que una función requiera variables internas ésta declaración se debe hacer al principio de la misma,
antes de realizar cualquier otra acción o instrucción.

float Area_Circunferencia ( float Radio ) //Función para calcular el área del círculo.
{ // Apertura de la función con corchete.
float Area;
Area = Valor_PI()*Radio*Radio; //Cálculo del área del círculo.
return Area;
} //Cierre de la función con corchete.

Las funciones son creadas por el desarrollador, sin embargo existen algunas que están predefinidas en el compilador
y pueden ser consultadas e implementadas respetando los parámetros de entrada y salida, la más importante de
ellas es la función main o principal. La importancia de esta función radica en el hecho de que es sobre ella que corre
todo el programa del microcontrolador y ésta se ejecuta automáticamente cuando el microcontrolador se energiza.
La función main en el caso particular de los microcontroladores no contiene parámetros de entrada ni de salida, su
declaración se puede apreciar en el siguiente ejemplo:

void main( void ) //Declaración de la función main.


{ // Apertura de la función main.
//Código del programa principal del microcontrolador.
} //Cierre de la función main.

Las declaraciones de funciones hechas por el desarrollador deben ser declaradas antes de la función main, y en el
caso de una función que invoque a otra función debe ser declarada antes de la función que hace el llamado.
Las funciones matemáticas trigonométricas y otras como logaritmos, exponenciales, y potenciación pueden ser
usadas con la librería predefinida por el compilador. Está librería se denomina C_Math.

2.8 CREACIÓN DE UN PROGRAMA EN LENGUAJE C


La estructura de un programa en lenguaje C es relativamente simple, como se ha visto anteriormente, primero es
indispensable declarar las variables globales que el desarrollador considere necesarias para el funcionamiento del
programa, estás variables globales son reconocidas por todos los puntos de código del programa incluidas las
funciones propias del desarrollador y la función main. El paso a seguir es hacer las declaraciones de funciones
diseñadas por el desarrollador para las tareas específicas en su programa. Posteriormente se declara la función main
y al comienzo de esta se deben declarar las variables que se requieran dentro de la misma. El código que sigue debe
configurar e inicializar los puertos y módulos del microcontrolador que sean indispensables en la aplicación. Por
último se edita el código que contiene el aplicativo concreto del programa. En el siguiente ejemplo se puede ver
cómo hacer una estructura para un programa:
//Declaración de variables globales.
int r;
float Radio;
//Declaración de funciones propias del desarrollador.
float Valor_PI ( void )
{
float PI=3.1416;
return PI;
}
float Calculo_Area_Circulo( float Radio )
{
float Area;
Area = Valor_PI()*Radio*Radio; //Cálculo del área.
return Area;
}
//Declaración de la función main o principal
void main( void )
{
float Valor_Area; //Declaración de variables de la función main.
{
r=1
Valor_Area=Calculo_Area_Circulo( Radio )*r;
}
return;
}

2.9 SENTENCIAS DE CONTROL DE PROGRAMA


Estas sentencias son las que el lenguaje C utiliza para controlar el flujo de ejecución del programa. Los operadores
lógicos y relacionales condicionan en muchos casos estas sentencias de control. También llamadas condicionales;
permiten que ciertas sentencias se ejecuten o no en función de una determinada condición. Llamadas también
sentencias de bifurcación, sirven para redirigir el flujo de un programa según la evaluación de alguna condición
lógica.
Las sentencias if e if–else son casi estándar en todos los lenguajes de programación. Además de ellas están las
sentencias if–else escalonadas y switch–case.
La sentencia if. Se ejecuta una sentencia o bloque de código si la expresión que acompaña a if tiene un valor
distinto de cero (verdadero), si es cero (falso) continúa sin ejecutar la sentencia o el bloque de sentencias.
Utilizando diagramas de flujo, tendríamos lo siguiente:

if (expresión) if (expresión)
sentencia; {
sentencias
}

p.e. if ((x==y)&&(t>=20)||(z==12))
cuenta=0;

La sentencia if-else. Se evalúa una expresión y si es cierta, se ejecuta el


primer bloque de código,
(o sentencia 1) si es falsa se ejecuta el segundo bloque (o sentencia 2):

if (expresión) if (expresión)
sentencia 1; {
else sentencias bloque 1;
En diagrama de flujo:

Se pueden combinar varios if-else-if escalonada para establecer muchos caminos de decisión:

p.e. if (numero==1)
if (expresión 1)
y=x+1;
sentencia 1;
else if (numero==2)
else if (expresión 2)
y=x+2;
sentencia 2;
else if (numero==3)
else
y=x+3;
sentencia 3;
else
y>3);

Ejemplo 3:

/* c = b-a;
* File: primero3xc8.c else
* Author: xxxxxx c =b+a;
* Uso de variables y estructura selectiva if y else // Sentencia if-else-if
* Created on xxxxxxxxxxxxxxxxxxxx if (r==0x0F)
*/ {
#include <xc.h> s=0x0F;
k=0x0F;
void main(void) r--;
{ }
signed int a; else if (r==0x0e)
int b=4,c=2; // Declarar variables a, b {
yc s =13;
unsigned char r=0x0d,s,k; k=13;
linea16: r--;
// Solo sentencia if }
a = b-c; // else
if ( a < 0) {
{ s++;
b = 1; k--;
c -= 1; r=r+2;
} }
// Sentencia if-else goto linea16;
a =5*b; return;
b =40/b; }
if ((a!=10)&&(b>20))

La sentencia switch. La sentencia switch brinda una forma más elegante de bifurcación múltiple. Podemos
considerarla como una forma más estructurada de la sentencia if – else – if escalonada, aunque tiene algunas
restricciones en las condiciones lógicas a evaluar, las cuales son comparaciones de valores enteros o caracteres.
Para elaborar el código en C se usan las palabras reservadas switch, case, break y default.
El diagrama de flujo de esta estructura sería el siguiente:
switch ( expresion )
{
case exprConst1 :
listaProp1
case exprConst2 :
listaProp2
case exprConstN :
listaPropN
default:
propDefault
}
El funcionamiento de esta estructura es como sigue:
1. Se evalúa la expresión que acompaña al switch. Esta expresión se
evalúa como expresión entera.
2. Se compara el valor obtenido, secuencialmente, con los valores que
acompañan los diferentes case, deteniéndose en el primero que coincida.

Estos valores deberán ser siempre expresiones constantes enteras.


Existe una etiqueta especial, default, que siempre es cierta, por lo que se
utiliza para contemplar el resto de casos que no se han
considerado en los case anteriores. Por eso es muy importante
que esta etiqueta se sitúe como la última del bloque, puesto
que las que se pongan detrás de ella nunca serán consideradas
(como se ha obtenido la coincidencia en una etiqueta, se deja
de comprobar el resto de etiquetas).
El problema de esta estructura es que si la expresión coincide
con un case, se ejecuta ese y todos los que hubiera por debajo
(los case hacen las funciones de etiquetas), que no es lo que
habitualmente se desea. La solución consiste en poner un
break como última proposición dentro de cada case, lo que
hace que se finalice la ejecución de la estructura switch:

switch ( expresion )
{
case exprConst1 :
listaProp1
break;
case exprConst2 :
listaProp2
break;
case exprConstN :
listaPropN
break;
default:
propDefault
break;
}
Observe cómo tras la última sentencia de cada bloque aparece una sentencia break para dar por finalizada la
ejecución de la estructura selectiva múltiple (incluyendo el caso por defecto, default).
El caso por defecto (default) suele utilizarse para detectar caso no válido o erróneo, evitando que se produzcan
errores en la ejecución. El siguiente ejemplo que corresponde a primero4xc8 determina si un número de mes entre
el 1 y el 12, nos indique su correspondiente número de días:
/*
* File: primero4xc8.c
* Author: xxxxxxxxxxxxxxxxx
* Programa que lea un numero de mes (en el rango de 1 a 12)
* e indique el numero de dias del mes usando la estructura selectiva SWITCH.
* Created on xxxxxxxxxxxxxxxx
*/
#include <xc.h>

void main(void)
{
int mes = 0;
int ndias = 0;
linea16:
switch (mes=mes+1)
{
case 2:
ndias = 28;
break;
case 4:
case 6:
case 9:
case 11:
ndias = 30;
break;
case 1:
case 3:
case 5:
case 7:
case 8:
case 10:
case 12:
ndias = 31;
break ;
default:
ndias = 111;
break ;
}
goto linea16;
return;
}
2.10 SENTENCIAS ITERATIVAS
Las sentencias de control iterativas sirven para que el programa ejecute una sentencia o un grupo de ellas un
número determinado o indeterminado de veces. Así es, esta sección no habla de otra cosa que de los bucles en C.
En este tipo de estructuras, se repite un conjunto de instrucciones en función de una condición. La principal
diferencia entre las diferentes estructuras repetitivas consiste en qué punto se realiza la comprobación de la
condición.
El lenguaje C soporta tres tipos de bucles, las cuales se construyen con las sentencias while, do – while y for. El
segundo es una variante del primero y el tercero es una versión más compacta e intuitiva del bucle while.
La sentencia while. Se emplea para repetir una sentencia o bloque de sentencias, se realiza la repetición mientras
sea cierta (no nula) una expresión (test). La expresión se evalúa antes de cualquier iteración, si es falsa ya no se
ejecuta la sentencia o bloque.
while (expresión)
{
sentencia (s);
}
Algunos aspectos a considerar:

1. Puede que el bloque que sigue al while no se ejecute ninguna


vez. Si la primera vez que se calcula la condición el resultado es
cero (falso), no se pasará a ejecutar el bloque, y se pasará
directamente a la sentencia que siga a la sentencia while.
2. Alguno de los valores que determinan la condición debe ser modificado dentro del bloque. Si no fuera así, y
la condición fuera cierta (distinta de cero) la primera vez que la comprobáramos, pasaríamos a ejecutar el
bloque, y ya no saldríamos nunca de él puesto que la condición seguiría siendo cierta de forma indefinida.
3. En la condición, es conveniente utilizar los operadores de rango (<, >, <= o >=) en lugar de los operadores de
igualdad y desigualdad (== o !=).
4. En las estructuras repetitivas en general, y como caso particular en la sentencia while, es muy importante
comprobar que los valores extremos de la condición (el primer y el último valor para los que se ejecuta el
bloque que acompaña a la cláusula while) son los adecuados.
5. Al igual que sucedía en el caso de las sentencias selectivas simples, es un error muy común querer utilizar
más de una sentencia dentro del bloque del while pero no encerrarlas entre llaves, como en el siguiente
ejemplo:

/*
* File: primero5axc8.c
* Author: xxxxxxxxxxxx
* Programa que suma los 10 primeros números del 1 al 10 con while
* Created on xxxxxxxxxxxxxxxxxxxxxxx
*/
#include <xc.h>

void main(void)
{
int num = 0;
int suma = 0;
while (10 > num)
num ++;
suma += num;
return ;
}
Si lo compilamos y ejecutamos paso a paso, obtenemos el siguiente resultado: La suma hasta el 10 vale 10.

Como puede observarse, y en contra de lo que pudiera parecer a simple vista, el fragmento de código anterior no
suma los diez primeros números (del 1 al 10), sino únicamente el último (10); a pesar de que el compilador no ha
detectado ningún error. Eso es porque el bloque del while está formado únicamente por la sentencia num++;, que
es la que se repite 10 veces. Una vez finalizado el bucle while es cuando se ejecuta la sentencia suma += num;, que
lógicamente sólo se ejecuta una vez. La forma correcta será la siguiente:

/*
* File: primero5bxc8.c
* Author: River
* Programa que suma los 10 primeros numeros del 1 al 10 con while
* Created on 26 de noviembre de 2016, 10:40 AM
*/
#include <xc.h>

void main(void) {

int num = 0;
int suma = 0;
while (10 > num)
{
num ++;
suma += num;
}
return;
}

Si lo compilamos y ejecutamos paso a paso, obtenemos el siguiente resultado: La suma hasta el 10 vale 55.

La sentencia do – while. Es una variación de la sentencia while simple. La principal diferencia es que la condición
lógica (expression) (test) de este bucle se presenta al final. Ahora las sentencias se ejecutan antes de que se evalúe
la expresión, por tanto el bucle se ejecuta siempre al menos una vez. Como se ve en la siguiente figura, esto implica
que el cuerpo o bloque (sentencia o grupo de sentencias) de este bucle se ejecutará al menos una vez. El diagrama
de flujo de esta sentencia sería:

do
{
sentencia (s);
}
while (expresión)

La sentencia for. Las dos sentencias


anteriores, while y do – while, se suelen
emplear cuando no se sabe de antemano
la cantidad de veces que se va a ejecutar
el bucle. En los casos donde el bucle
involucra alguna forma de conteo finito
es preferible emplear la sentencia for. El
diagrama de ujo de esta sentencia sería:
En iniciación se le asigna un valor inicial a una variable que se emplea para el control de la repetición del bucle (hay
que declararla como variable), esa inicialización se ejecuta una sola vez.

La condición se evalúa antes de ejecutar la sentencia o bloque del bucle, en la expresión entrará normalmente la
variable de control de repetición. Si la condición es cierta se ejecuta el bucle, si no se sale del mismo y se continúa
con el resto del programa.

El incremento se utiliza para establecer cómo cambia la variable de control cada vez que se repite el bucle.

En una sentencia for puede omitirse cualquiera de los tres campos, pero es imprescindible mantener los ( ; ). Si se
omite el segundo campo, la condición se da por cierta, y se obtiene un bucle infinito.

Pueden introducirse varias sentencias tanto en el bloque iniciación como en el bloque incremento. En este caso las
sentencias van separadas por comas, ejecutándose de izquierda a derecha. En la siguiente línea después de la
sentencia for se coloca el punto y coma (;) para que incluya el bloque for.

int i, suma;
for ( suma = 0, i = 2; i <= 10; suma += i, i += 2)
;

Obsérvese el ( ; ) aislado que aparece tras la cláusula for. Esto es necesario porque una sentencia for siempre
incluye un bloque for. Si no hubiéramos incluido ese (;) (que representa una sentencia vacía), el bloque del for lo
hubiera constituido la sentencia que pueda continuar.

Complete el programa anterior con lo que se muestra a continuación y verifique su funcionamiento paso a paso para
la sentencia while, do-while y for.

/*
* File: primero5cxc8.c
* Author:xxxxxxx
*1 Programa que suma los 10 primeros números del 1 al 10 con while
*2 Programa con operadores relacionales y lógicos con do-while
*3 Programa que obtiene la suma de los numeros pares
* comprendidos entre 2 y 10 con la sentencia for.
* Created on xxxxxxxxxxxxxxxxxxxxxxx
*/
#include <xc.h>
void main(void) {

int num = 0, suma = 0;


int a=2,b=12;
int i,suma1=0;
// 1 Con sentencia while
while (10 > num)
{
num ++;
suma += num;
}
// 2 Con sentencia do-while
do
{
a++;
b--;
}
while ((a<5)&&(b>=10));
// 3 Con sentencia for
for ( i = 2; i <= 10; i += 2 )
suma1 += i;

return ;
}

Estructuras repetitivas anidadas. Una estructura puede contener otra dentro, cumpliendo que estén totalmente
anidadas:

for (......)
{
for (..........)
{
while (...)
{
...........
}
}
}
En bucles anidados cada alteración del bucle externo provoca la ejecución completa del bucle interno. Verifique el
funcionamiento de sentencias for anidados con el siguiente ejemplo que corresponde a primero6xc8:

/*
* File: primero6xc8.c
* Author: xxxxxxxx
* Programa anidados de la sentencia for
* Created on xxxxxxxxxxxxxxxxx
*/

#include <xc.h>

void main(void)
{
short COL; //Declaración de variables.
short FIL;
int x=0;
short MATRIZ[3][3]; //Declamación de un vector bidimensional.
for( COL=0; COL<3; COL++ ) //Declaración de ciclo for externo.
{
//Fragmento del primer ciclo.
for( FIL=0; FIL<3; FIL++ ) //Declaración de ciclo for interno anidado.
{
//Fragmento del ciclo anidado.
MATRIZ[COL][FIL] = COL*FIL;
x=MATRIZ[COL][FIL];
}
}
//Otro programa anidado
int num=0, i;
long fac;
for(num = 1; num <= 5; num++)
{
fac = 1;
for(i = 1; i <= num; i++)
fac = fac * i;
}
return;
}

Sentencia break.
Esta sentencia tiene dos usos posibles:
a).- Permite salir de un bucle de repetición en cualquier punto del bloque. Si se encuentra esta sentencia, el
programa pasa a la siguiente sentencia tras el bucle.Ejm:
k=12;
for (i=0; i<=25; i++)
{
if (i==k)
break; //”imprime” desde 0 hasta k=12
}
b).- Se puede utilizar para finalizar un “case” en una sentencia switch. Ejm:

case ‘M’:
dia=2;
break;

Sentencia continue.
Funciona de manera parecida a la sentencia break dentro de un bucle. Sin embargo, en lugar de forzar la terminación
del mismo, continue fuerza una nueva iteración del bucle y salta el código que hay hasta que se evalúa la condición
del bucle. Ejm:
char *cadena; //definimos puntero a cadena
int espacios;

for(espacios=0; *cadena; cadena++)
{
if (*cadena !=‘ ‘) continue;
//si carácter no es blanco sigue a la condición e incremento
espacios++;
//solo llega a incrementar espacios si era blanco el carácter
}
//Este fragmento cuenta por tanto los espacios en una cadena de caracteres

Sentencia return
Esta sentencia se utiliza para finalizar y volver desde una función hacia el punto en que se le llamó. Se puede
devolver un valor a la función que hizo la llamada:
return (expresión); o bien return expresión
Si se omite la expresión, el valor devuelto estaría indefinido (no hay error). Si no se necesita devolver ningún valor, lo
más correcto sería declarar el tipo void en la declaración del valor devuelto por la función.
Si no se incluye una sentencia return en una función, ésta se finaliza tras la ejecución de la última línea del código.
Ejm:
void no_hace_nada(int c)
{ c++;
return;
}
Sentencia goto
El uso del goto tiene muy mala imagen entre los programadores de alto nivel ya que contribuye a hacer ilegibles los
programas. En C es posible escribir los códigos sin su uso, sin embargo vamos a indicar su existencia y si alguien lo
necesita, que lo use si no puede evitarlo (lo usamos en ensamblador).
La sentencia goto necesita una etiqueta para operar, una etiqueta es cualquier identificador válido en C seguido de
dos puntos (:). La etiqueta debe estar en la misma función que el goto (¡ no se puede saltar entre funciones !)
Sintaxis goto etiqueta;
...
etiqueta: sentencia;
Ejm: bucle:
x++;
if (x<=100) goto bucle;

3 .- MICROCONTROLADORES PIC16F87XA
Bajo el nombre de esta subfamilia de microcontroladores, actualmente encontramos cuatro modelos: EL PIC
16F873A/4A/6A y 7A. Estos microcontroladores disponen de una memoria de programa FLASH de 4 a 8 KBytes de 14
bits, considerablemente superior frente al PIC 16F84A en el que solo disponíamos de 1Kbyte de 14 bits.

3.1.- PRINCIPALES CARACTERÍSTICAS


✦ Procesador de arquitectura RISC avanzada
✦ Juego de solo 35 instrucciones de 14 bits de longitud. Todas ellas se ejecutan en un ciclo de instrucción, menos las
de salto que tardan dos.
✦ Hasta 8K palabras de 14 bits para la Memoria de Programa, tipo FLASH en los modelos 16F876 y 16F877 y 4KB de
memoria para los PIC 16F873 y 16F874.
✦ Hasta 368 Bytes de memoria de Datos RAM.
✦ Hasta 256 Bytes de memoria de Datos EEPROM.
✦ Pines de salida compatibles para el PIC 16C73/74/76/77.
✦ Hasta 14 fuentes de interrupción internas y externas.
✦ Pila de 8 niveles.
✦ Modos de direccionamiento directo e indirecto.
✦ Power-on Reset.
✦ Temporizador Power-on y Oscilador Temporizador Start-Up.
✦ Perro Guardián (WDT).
✦ Código de protección programable.
✦ Debugger In-Circuit
✦ Modo SLEEP de bajo consumo.
✦ Programación serie en circuito con dos pines.
✦ Solo necesita 5V para programarlo en este modo.
✦ Voltaje de alimentación comprendido entre 2 y 5,5 V.
✦ Bajo consumo: < 2 mA valor para 5 V y 4 Mhz; 20 μA para 3V y 32 Mhz; <1 μA en standby
3.2.- DISPOSITIVOS PERIFÉRICOS
✦ Tirner0: Temporizador-contador de 8 bits con preescaler de 8 bits
✦ Timerl: Temporizador-contador de 16 bits con preescaler que puede incrementarse en modo sleep de
forma
externa por un cristal/clock.
✦ Timer2: Temporizador-contador de 8 bits con preescaler y postescaler.
✦ Dos módulos de Captura, Comparación, PWM (Modulación de Anchura de Impulsos).
✦ Conversor A/D de 10 bits.
✦ Puerto Serie Síncrono Master (MSSP) con SPI e I2C (Master/Slave).
✦ USART/SCI (Universal Syncheronus Asynchronous Receiver Transmitter) con 9 bit.
✦ Puerta Paralela Esclava (PSP) solo en encapsulados con 40 pines

3.3.- DIFERENCIAS ENTRE LOS MODELOS DE 28 Y LOS DE 40 PATITAS


El PIC 1 6F873A y el 876A tienen 28 pines, mientras que el PIC 16F874A y 877A tienen 40.
Nos centraremos en el PIC 16F873A y las diferencias que tiene con sus hermanos son mínimas y se detallan a
continuación:
1. Los modelos de 40 pines disponen de 5 Puertos de E/S: A, B, C, D y E, mientras que los de 28 solo tienen 3
Puertos: A, B y C.
2. Los modelos de 40 pines tienen 8 canales de entrada al Conversor A/D, mientras que los de 28 solo tienen 5
canales.
3. Sólo poseen la Puerta Paralela Esclava los PIC 16F87XA de 40 pines.

En la Tabla 1-1 se muestran las características comparativas


más relevantes de esta familia de microcontroladores :

La familia consta de cuatro dispositivos (PIC16F873A, PIC16F874A, PIC16F876A y PIC16F877A). Los PIC
16F876A/873A entran en el bloque
de dispositivos encapsulados en 28
pines y los PIC 16F877A/874A
entran en el bloque de dispositivos
encapsulados en 40 pines. Los
dispositivos de 28 pines no tienen
implementado el puerto paralelo
esclavo.
Las siguientes figuras corresponden
a los diagramas de bloque de los
dispositivos. Los de 28 pines en la
Figura 3
y los de 40 pines en la Figura 4.

3.4.- ORGANIZACIÓN DE LA
MEMORIA
Hay tres bloques de memoria en
cada uno de estos
microcontroladores: la Memoria de
Programa, la Memoria de Datos,
tienen buses separados de manera
que el acceso concurrente puede
ocurrir y la Memoria de datos
EEPROM.
3.4.1 Organización de la Memoria
de Programa
Los dispositivos PIC16F87XA tienen
contador de programa de 13 bits
capaz de direccionar un espacio de
memoria de programa de 8K x 14.
Los dispositivos PIC16F877A/876A
tienen palabras de 8K x 14 de memoria de programa FLASH y los dispositivos PIC16F873A/874A tienen 4K x 14. El
vector de reset está en 0000h y el vector de interrupción está en 0004h, ver figura 5.
3.4.2 Organización de la Memoria de Datos
La memoria de datos está particionada en múltiples bancos los que contienen los Registros de Propósito General y
los Registros de Función Especial. Los bits RP1 (STATUS<6>) y RP0 (STATUS<5>) son los bits de selección de banco.
RP1:RP0 Banco
0 0 0
0 1 1
1 0 2
1 1 3
TABLA 04
Cada banco se extiende hasta 7Fh (128 bytes). Las posiciones más bajas de cada banco están reservadas para
Registros de Función Especial. Sobre los Registros de Función Especial están los Registros de Propósito General,
implementados como RAM estática. Todos los bancos implementados contienen Registros de Función Especial.
Algunos Registros de Función Especial de "alto uso" de un banco pueden ser reflejados en otro banco para reducción
de código y acceso más rápido. Ver figura 6 y figura 7.

Figura 3 Arquitectura Pic 16F873A y 16F876A de 28 pines


3.4.3 Memoria de datos EEPROM
En la memoria interna EEPROM del microcontrolador PIC se pueden guardar datos que no se quiere que se borren
en caso se corte la alimentación del PIC, en esta memoria se pueden almacenar hasta 256 bytes, esto es 256 datos
de 8 bits, la capacidad de esta memoria puede variar de un PIC a otro. Para la escritura y la lectura de datos en la
EEPROM PIC se tienen que seguir una serie de pasos, en el orden que viene indicado en la hoja de datos del PIC.
Figura 4 Arquitectura Pic 16F874A y 16F877A de 40 pines
Figura 5 Memoria de Programa para para los de 40 y 28 pines.
Figura 6 Memoria de Datos para los de 40 pines.

Figura 7 Memoria de Datos para los de 28 pines.

3.5.- PINES DEL PIC


Los pines del PIC pueden llegar a tener múltiples funciones, eso dependerá del microcontrolador PIC que se utilice,
para los ejemplos que se hagan se utilizarán distintos PIC, por lo que siempre será necesario tener a mano la hoja de
datos del PIC que se utilice.
Los pines del PIC normalmente vienen agrupados en grupos de 8 pines, a cada uno de estos grupos se les llama
puertos, algunos puertos pueden tener menos de 8 pines. por ejemplo para el PIC16F876A  se tiene que el puerto
B tiene 8 pines, mientras el puerto A solo tiene 6 pines. Cada uno de los pines del PIC a ser utilizados como entradas
salidas digitales tienen un nombre propio relacionado con el puerto al que pertenecen y al número de orden del bit
con el cual se programará, por ejemplo, para el puerto B se tienen los pines RB0, RB1, RB2, RB3, RB4, RB5, RB6, RB7.

En la Tabla 2 se muestra un listado de cada uno de los pines que corresponden a los dispositivos de 40 que tienen su
correspondiente patilla en los dispositivos de 28 pines.
Tabla 2 Breve descripción de pines para PIC de 28 y 40 pines

Tabla 2 Breve descripción de pines para PIC de 40 y 28 pines (continuación)


Tabla 2 Breve descripción de pines para PIC de 40 y 28 pines (continuación)

Tabla 2 Breve descripción de pines para PIC de 40 y 28 pines (continuación)


3.5.1 Configuración de puertos del PIC como ENTRADA/SALIDA digital.
Cada puerto: PORTA, PORTB, PORTC, etc. tiene su propio registro de control de flujo, o sea el registro TRIS
correspondiente: TRISA, TRISB, TRISC etc. lo que determina el comportamiento de bits del puerto, pero no
determina su contenido.
Mediante el registro TRISx se indica si se quiere configurar los pines como salidas o como entradas, este registro se
puede configurar bit por bit, dependiendo si los pines se quieren como entradas o como salidas, o si se quiere utilizar
solo algunos pines del puerto, o quizá solo uno.
Al poner a cero un bit del registro TRIS (pin=0), el pin correspondiente del puerto se configurará como una salida. De
manera similar, al poner a uno un bit del registro TRIS (bit=1), el pin correspondiente del puerto se configurará como
una entrada. Esta regla es fácil de recordar: 0 = Salida 1 = Entrada.

Pines del PIC: Registro TRISx


Se hará el ejemplo para el puerto B, pero
es similar para los demás puertos.
Si se quieren configurar todos los pines
del puerto B como salidas o como
entradas, en el XC8 esto se puede hacer
en binario, en decimal o hexadecimal de
la siguiente forma:

Pines como salidas


TRISB=0b00000000; //en binario
TRISB=0; //en decimal
TRISB=0x00 //en hexadecimal

Pines como entradas


TRISB=0b11111111; //en binario
TRISB=255; //en decimal
TRISB=0xff; //en hexadecimal

Muchas veces será necesario configurar los registros bit por bit, esto se hace en el XC8 mediante el corrimiento de
bits, lo que se comente aquí será aplicable a cualquier otro registro.
Por ejemplo, si se quiere utilizar solo el pin RB0 como salida, entonces el bit 0 de TRISB se tiene que poner a 0, esto
en C hay que hacerlo así:
TRISB &=~ (1<<0);
donde & es el operador de bits and y ~ es la negación, es lo mismo que hacer TRISB = TRISB & ~(0b00000001), aquí
se está diciendo que el bit 0 del registro TRISB se ponga a 0, con lo cual se logra que el pin RB0 sea una salida digital,
en el XC8 esto mismo se puede programar también así:
TRISBbits.TRISB0=0;

Si lo que se quiere por ejemplo es que sea el pin RB5 el que sea una salida se haría lo siguiente:
TRISB &=~ (1<<5);  //es lo mismo que TRISB = TRISB & ~(0b00100000), también se puede hacer TRISBbits.TRISB5=0.

Si se quiere por ejemplo que los pines RB1, RB4 y RB7 sean salidas, esto en una sola línea sería así:
TRISB &=~ ((1<<1) | (1<<4) | (1<<7));
también se puede hacer así
TRISBbits.TRISB1=0;
TRISBbits.TRISB4=0;
TRISBbits.TRISB7=0;

Por ejemplo, si se quiere utilizar solo el pin RB0 como entrada, entonces el bit 0 de TRISB se tiene que poner a 1, esto
C hay que hacerlo así:
TRISB |= (1<<0);
donde | es el operador de bits or, es lo mismo que hacer TRISB = TRISB | (0b00000001);
de esta forma se asegura que el bit 0 del registro TRISB sea un 1, con lo cual se logra que el pin RB0 sea una
entrada digital, en el XC8 esto mismo se puede programar también así TRISBbits.TRISB0=1.

Si lo que se quiere por ejemplo es que sea el pin RB5 el que sea una entrada se haría lo siguiente
TRISB |= (1<<5);  //es lo mismo que TRISB = TRISB | (0b00100000), también se puede hacer TRISBbits.TRISB5=1.

Si se quiere por ejemplo que los pines PB1, PB4 y PB7 sean entradas, esto en una sola linea sería así:
TRISB |= ((1<<1) | (1<<4) | (1<<7));
también se puede hacer así
TRISBbits.TRISB1=1;
TRISBbits.TRISB4=1;
TRISBbits.TRISB7=1;

Pines del PIC: Registro PORTx


Cuando algún pin o los pines de PIC se han configurado como salidas, mediante el registro PORTx se indica si la salida
será un alto o un bajo que es lo mismo que un 1 o un 0.
Por ejemplo, con la siguiente línea de código se pondría todos los pines del puerto B como altos si previamente
mediante el registro TRISB se han configurado como salidas:
PORTB=0b11111111; //en binario, también se puede hacer en decimal o hexadecimal.

Mediante la siguiente linea:


PORTB=0b01010101; //por los pines impares se obtendrán ceros, mientras que por los pares se obtendrán unos.

Si se quiere que solo por el pin RB3 salga un 1 será así:


PORTB |= (1<<3); //en el XC8 también puede ser así PORTBbits.RB3=1.

Si se quiere que por los pines RB5 y RB2 salgan unos sería así:
PORTB |= ((1<<2)|(1<<5));
o también
PORTBbits.RB2=1;
PORTBbits.RB5=1;

Cuando algún pin o pines del microcontrolador PIC se han configurado como entradas digitales, mediante los bits
del registro PORTx se pueden leer los estados de los pines del puerto, esto es, se puede leer si al pin del puerto x le
está llegando una alto o un bajo, se puede leer bit por bit o todo el puerto a la vez, para esto será necesario crear
variables donde se guardarán los estados leídos
.
Por ejemplo si todos los pines del puerto B son utilizados como entradas digitales se puede proceder así
char estados; //variable entera sin signo de 8 bits
estados=PORTB; //en la variable de 8 bits se guarda los estados de los pines del PUERTO B

3.6 TECLADOS Y SISTEMAS DE ENTRADA DE DATOS


La interacción con los microcontroladores requiere de sistemas de entrada para los datos con el usuario. Para este
propósito se pueden usar dispositivos como pulsadores, teclados matriciales, o paralelos, e incluso teclados PS2
como los que usan los ordenadores de escritorio. Por ahora comentaremos sobre los pulsadores y dip-sw.
3.6.1 Uso de pulsadores
La implementación de pulsadores es una de las alternativas de mayor popularidad en las interfaces de acción con
los usuarios. Los pulsadores son de uso simple y de costo económico en la implementación. Los pulsadores pueden
ser normalmente abiertos o normalmente cerrados al efecto eléctrico ante el flujo de corriente. La implementación
de estos dispositivos es propensa a los efectos de los rebotes o ruidos de tensión eléctrica cuando cambian de
estado. Dado el veloz procesamiento de los microcontroladores estos efectos de ruido, hacen que el PIC pueda
detectar cambios o estados lógicos no definidos. Para comprender este concepto observe la siguiente gráfica que
muestra el comportamiento de este ruido:
Figura 3.6.1
Cuando los cambios de tensión cruzan la zona indefinida, generan cambios de alto a bajo y viceversa que el
microcontrolador detecta como un pulso. Para evitar el efecto del ruido o rebotes se debe realizar una pausa en
espera de la estabilidad del estado lógico. La duración promedio de un rebote es de 1 a 5 milisegundos, lo que indica
que se debe hacer un retardo superior a este tiempo para esperar la estabilidad. Un retardo adecuado para este
efecto es un tiempo mayor o igual a 10 milisegundos. Este retardo se debe aplicar después de detectar el primer
cambio sobre el pulsador.

La instalación de los pulsadores se puede hacer de dos formas, activo en alto o activo en bajo, en la siguiente figura
se puede apreciar la forma de configurar estás dos posibilidades:

Figura 3.6.2
La decisión de usar la activación en alto o en bajo, depende del
desarrollador quien debe analizar la forma de mayor simplicidad
en el momento de hacer el diseño.

3.6.2 Uso de Dip-Switch


Los Dip-Switch, son dispositivos mecánicos que contienen múltiples interruptores en un solo encapsulado. Estos
dispositivos permiten configurar de forma simple las características binarias de los sistemas microcontrolados. Los
Dip-Switch, se consiguen comercialmente en tamaños, colores, y número de interruptores diferentes. La apariencia
física y la vista en el simulador ISIS, de estos dispositivos es la siguiente:

Figura 3.6.3

El uso de los Dip-Switch es similar a los pulsadores y se puede configurar de la misma forma que un pulsador con
activación en alto o en bajo. Las configuraciones antes citadas se pueden apreciar en el siguiente circuito:

Figura 3.6.4
El programa siguiente segundo1xc8 lee el puerto A al que se le suma 0x2a y el resultado se muestra en el puerto B:

/*
* File: segundo1xc8.c
* Author: xxxxxxxxxxx
* PB<-PA+2Ah
* Created on 13 de julio de 2017, 11:00 AM
*/
#include <xc.h>

// PIC16F876A Configuration Bit Settings


#pragma config FOSC = XT // Oscillator Selection bits (XT oscillator)
#pragma config WDTE = OFF // Watchdog Timer Enable bit (WDT disabled)
#pragma config PWRTE = ON // Power-up Timer Enable bit (PWRT enabled)
#pragma config BOREN = OFF // Brown-out Reset Enable bit (BOR disabled)
#pragma config LVP = OFF // Low-Voltage (Single-Supply) In-Circuit Serial Programming
#pragma config CPD = OFF // Data EEPROM Memory Code Protection bit of)
#pragma config WRT = OFF // Flash Program Memory Write Enable bits off
#pragma config CP = OFF // Flash Program Memory Code Protection bit of)

unsigned char x=0,y=0;

void main(void) {
// Configure the I/O ports
ADCON1 = 0x06;
TRISA = 0xFF;
TRISB = 0x00;
PORTA = 0;
while(1){
x = PORTA;
y=0x2a+x;
PORTB=y;
}

return;
}

 LEER UN PULSADOR
Utilizando una estructura condicional “if” podemos leer un pulsador conectado en RB0 para encender o apagar un
LED en el pin RB4.
/*
* File: segundo1axc8
* Author: xxxxxxxxxxxxxxxxxxxxx
* Created xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
* Microcontrolador: PIC16F876A
* Control de un pulsador
*/
 #include <xc.h> // Librería XC8
 #define _XTAL_FREQ 4000000 // Frecuencia de reloj para el micro
 
// PIC16F87xA Configuration Bit Settings
#include "bitsconfig87xAxc8.h"
#define led2 RB5
#define sw1 RB1
// FUNCION PRINCIPAL
void main() {
TRISB = 0b00000011; //
PORTB = 0b00000000;
while (1) {
//Con rebote
if (PORTBbits.RB0 == 0) {
PORTBbits.RB4 = 1;
__delay_ms(500);
PORTBbits.RB4 = 0;
}
//Sin rebote
if (sw1 == 0) {
__delay_ms(2);
while (sw1 == 0); //Mientras esté presionado
__delay_ms(2); //evita el efecto rebote
led2 = 1;
__delay_ms(500);
led2 = 0;
}
}
return;
}

Ejercicios:
1. Los datos que ingresan por el PA y PB se suman y el resultado se muestra en el PC.
2. Lo que ingresa por el PA se le resta a D5h y el resultado lo muestra por el PB.
3. Realizar un programa tal que compare un dato del PA con un número N de modo que:
a. Si (PA)=N -> El nible alto del PB en ON
b. Si (PA)>N -> Los bits pares del PB en ON
c. Si (PA)<N -> El nible bajo del PB en ON
4. Para cualquier dato del PA, en el PB los bits impares siempre en cero.
5. En el PB se vea siempre el inverso del PA.
6. En el PB siempre se tenga el intercambio de nibles del PA.
7. Con un pulsador en RA0 se active en ON a un LED conectado al RB0, y con otro pulsador en RA1 se lo
desactive a OFF el LED conectado al RB0.
8. Con un pulsador en RA0 se ponga en ON a un LED conectado al RB0, y con el mismo pulsador se ponga en
OFF al LED conectado al RB0.

3.7.- SUBRUTINAS y/o FUNCIONES


Básicamente una Subrutina es un segmento de código que se escribe sólo una vez pero puede invocarse o ejecutarse
muchas veces desde el programa principal o desde otros subprogramas.
Existen dos tipos: Procedimientos y Funciones.
1. Procedimientos
Son un tipo de subrutina que ejecuta un conjunto de acciones sin devolver valor alguno como resultado de dichas
operaciones. Estos se identifican por su declaración void().
P. ejem.
void Rutina(void);
void TAREA(void);
2. Funciones
A diferencia de los procedimientos, las funciones después de ejecutar un conjunto de acciones devuelven sólo un
valor del tipo usado en la declaración de ésta por medio de return().
P. ejem.
int SUMA(void); // Devuelve un valor de tipo entero
float CALCULA(void); // Devuelve un valor de tipo real

3. Usando argumentos para pasar datos a subrutinas


El mecanismo para enviar información a las subrutinas es llamado argumento (algunos autores lo conocen como
parámetro) y son los datos que se colocan entre paréntesis al invocarlas.
P. ejem.
int PROCESO(int x, float y)
Argumentos

RETARDOS EN XC8
Cuando necesitamos que transcurra un determinado tiempo de espera antes de que ocurra un evento como por
ejemplo el encendido de una luz, LED, activación de una bobina de un relé o lectura de una determinada entrada, se
suele recurrir a los retardos. Prácticamente casi todos los programas de microcontroladores PIC usan en algún
momento una rutina de retardo.

Los retardos en los PIC los podemos generar de dos formas diferentes:
Por Software.
Por Hardware mediante el Timer TMR0.

Los retardos por Software consisten en que el pic se quede ejecutando unos bucles que van decrementando unos
contadores cargados previamente con el tiempo de retardo, cuando los contadores llegan a 0 la rutina de retardo
queda terminada y el microcontrolador sigue ejecutando otros procesos.

Los retardos por Hardware se realizan mediante el temporizador/contador TMR0, TMR1, etc. que son registros de 8
bits, 16bits, es decir, son registros cuyo contenido es incrementado con una cadencia regular y programable
directamente por el hardware del PIC.

Los retardos en los microcontroladores son necesarios debido a que es necesario sincronizar a algunos periféricos
que tengan que ver con el actuar humano. Primero que todo hay que saber que los tiempos en programación para
los microcontroladores PIC dependen de un cristal para su funcionamiento, aunque en unos casos se pueda trabajar
con un oscilador interno y en otros con un cristal externo.

Sabiendo que los tiempos en los micros son muy reducidos lo que vamos a hacer es crear una rutina que nos
multiplique el tiempo; para programar un tiempo o retardo en un programa con XC8 se hace de la siguiente forma:

__delay_ms(X);         ========>  Donde X es el tiempo en milisegundos


__delay_us(X);          ========>  Donde X es el tiempo en microsegundos
__delaywdt_ms(X);         ========>  Donde X es el tiempo en milisegundos para el wdt
__delaywdt_us(X);          ========>  Donde X es el tiempo en microsegundos para el wdt

CICLO MÁQUINA
Un ciclo máquina es el tiempo que se tarda en ejecutar una instrucción. Este tiempo está estrechamente emparejado
con el tiempo de oscilación:

4 ciclos de reloj = 1 ciclo máquina.

Para calcular cuánto tiempo tarda en ejecutarse una instrucción


usaremos la siguiente ecuación:

Tiempo=4.(1/Fosc)
Por lo tanto, ya que hemos visto anteriormente que la frecuencia de oscilación va desde los 32Khz a los 20Mhz
podemos concluir que el tiempo en ejecutar una instrucción variará entre:

4(1/32000)= 0,000125 segundos-------> 125 microsegundos


4(1/20000000)=0,0000002 segundos---> 200 nanosegundos

NOTA: Todas las instrucciones tardan en ejecutarse 1 ciclo máquina excepto las instrucciones de que tardan 2 ciclos
máquina.

A continuación se tiene el programa tercero1xc8 para visualizar en el PB el parpadeo de LEDs a una frecuencia de
1Hz.

/*
* File: tercero1.c
* Author: xxxxxxxxxxx
* Parpadeo PB a 1Hz
* Created xxxxxxxxxxxxxxxxxx
*/
#include <xc.h>

// PIC16F876A Configuration Bit Settings


#include "bitsconfig876axc8.h"

#define _XTAL_FREQ 4000000 // se define la frecuencia del cristal para los retardos

/*==============================================================================
Inicializar puertos, esta función es llamada luego en el main().
==============================================================================*/
void initPorts(void)
{
TRISB = 0b00000000; // Todo el puerto B es salida
PORTB = 0; // Se apaga todo el puerto B
}
/*==============================================================================
Bucle principal
==============================================================================*/

void main(void)
{
// Inicializar puertos y periféricos
initPorts();

// Repetir indefinidamente
while(1)
{
// prender puerto B
PORTB = 0b11111111;
_delay(500000); //retardo de 1 millón de pulsos de reloj

// apagar puerto b
PORTB = 0b00000000;
_delay(500000);
}
return;
}

El siguiente programa tercero2xc8 es similar al anterior con la diferencia que tendrá un pulsador en RA1 como START
para iniciar el parpadeo y otro pulsador en RA2 como STOP para detener el parpadeo; pero al conectar la
alimentación en el PB se verá el número de meza.
/*
* File: StartStopPORTB.c
* Author: xxxxxxxxxxxxxxxxxxx
* StartStopPORTBxc8- pic 16f876a
* Created on xxxxxxxxxxxxxxxxxxxx
*/

#include <xc.h>

// PIC16F87xA Configuration Bit Settings


#include "bitsconfig87xAxc8.h"
#define _XTAL_FREQ 4000000 // Frecuencia de reloj para el micro

void initPorts(void)
{
ADCON1=0x06; //
TRISB = 0x00; //
TRISA=0xff; //
PORTB = 0; //
PORTA=0; //
}

void main(void)
{
// Inicializar puertos y periféricos
initPorts();

while(1){
PORTB=0x0a;
if(PORTAbits.RA1==0) //
{
while(1)
{
PORTB=0xaa;
__delay_ms(30);
PORTB=0x00;
__delay_ms(30);
if(PORTAbits.RA2==0) //
break;
}
}
else
PORTB=0x0a;
}
return;
}

Ejercicios
1. Realizar un programa para el parpadeo de nibles con control de start y stop
a. Al conectar la alimentación
- LEDs del PB off
- Display del PC, mostrar el número de meza
b. Al pulsar START (RA1)
- LEDs del PB parpadeo de nibles 0x0F (625ms) y 0xF0 (275ms)
- Display del PC off
c. Al pulsar STOP (RA2)
Regresar al punto a.
2. Realizar un programa de comparación de lo ingresa por PD con un número N, con control de start y stop
a. Al conectar la alimentación
- LEDs del PB off
- Display del PC, mostrar el número de meza
b. Al pulsar START (RA1)
- Si (PD) = N -> En PB parpadeo de bits pares a f=5Hz
- Si (PD) > N -> En PB parpadeo de nibles a f=10Hz
- Si (PD) < N -> En PB parpadeo de bits impares a f=20Hz
- Display del PC off
c. Al pulsar STOP (RA2)
Regresar al punto a.

3. Realizar un programa tal que produzca una rotación de un bit a la derecha cada 150ms con control de start y
stop.
4. Realizar un programa para el control de un semáforo inteligente con control de start stop.
5. Realizar un programa para el control del nivel de un depósito de líquido. El cual consta de tres sensores,
vacío, lleno y rebose; con dos bombas para el llenado.

LA PILA EN LOS PICS


La Pila en los PIC es una zona de memoria que se encuentra separada tanto de la memoria de programa como de
la de datos. Tiene una estructura LIFO (Last In First Out), por lo que el último valor que se guarda es el primero que
sale. Dispone de varios niveles de profundidad, cada uno de ellos con una longitud de n bits. Su funcionamiento es
como el de un buffer circular, de manera que el valor que se obtiene al realizar tantos desplazamientos como niveles
más uno, es igual al primer desplazamiento.

La única manera de cargar la Pila es a través de la instrucción CALL (llamada a subrutina) o por una interrupción
que hacen que con cada una de ellas, se cargue el contenido del PC en el valor superior de la Pila. Para recuperar el
contenido de la Pila en el PC hay que ejecutar una instrucción RETURN, RETLW o RETFIE (vuelta del programa de
atención a una subrutina o interrupción).
La pila es una zona aislada dentro de la arquitectura interna del microcontrolador y está asociada al Contador de
Programa. La instrucción call es bastante usada en la confección de programas, significa llamada a subrutina, y
provoca un desvío del flujo normal del programa.
Cuando se ejecuta una instrucción call ocurre dos eventos, se salta a la dirección que corresponde al call y se guarda
en la pila la dirección que le sigue al call, entonces cuando se retorna del call (return), este se va a la dirección
guradada por la pila. Si se tiene varios call anidados, es decir una dentro de otra, entonces se va almacenando en la
pila la dirección que le sigue a cada call, y en cada retorno se carga en el PC la dirección que le sigue a cada call;
siendo la última dirección que se guardó en la pila la primera en salir.

3.8.- MANEJO DE TABLAS


Toco el turno de ver algo sobre tablas, para esto mostraremos los números del cero al nueve en un display de 7
segmentos. En la tabla de verdad siguiente se tiene para los números del 0 al 9 y la diferente numeración que se
puede usar.

Las tablas en C son los ARREGLOS Y STRINGS: los arreglos pueden definirse como sucesiones de valores constantes ó
variables que son almacenados en memoria FLASH, o en RAM respectivamente. Los strings son arreglos formados de
caracteres ASCII, y terminados con un caracter "NULL".
const signed int web[5]={23,44,-81,92,-56}; es un arreglo de constantes y se almacenan en memoria FLASH. Su valor
no puede modificarse.
const char wob[30]="LAS CARACTERISTICAS MAS IMPORTANTES DEL DISPOSITIVO SON"; este arreglo se almacena en
la memoria FLASH. Las comillas indican al compilador que se trata de un string. Observe que no es necesario el uso
de corchetes.
signed int web[5]={23,44,-81,92,-56}; es un arreglo de variables inicializadas con un valor y se almacenan en RAM,
pero pueden modificarse a lo largo de un programa. Cada elemento del arreglo puede accesarse a través de su
subíndice, por ejemplo, en el arreglo "web": web[0]=23 , web[4]=-56
También puede declararse el arreglo sin inicializar sus valores. Por ejemplo: 
signed int web[5];     //reserva espacio en memoria RAM para 5 variables.

El siguiente programa cuarto1xc8 es un ejemplo de lo mencionado líneas arriba.


/*
* File: display-contador-xc8.c
* Author: xxxxxxxxxxxx
*
* Created on xxxxxxxxxxxxxxxxxxxxxxxxxx
*/
//display-contador-xc8- pic 16f873a
#include <xc.h>
// PIC16F87xA Configuration Bit Settings
#include "bitsconfig87xAxc8.h"
#define _XTAL_FREQ 4000000
const unsigned char digitos[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f};
void main(void) {
unsigned char cont=0;
TRISB = 0;
PORTB = 0;
while(1)
{
PORTB=digitos[cont++];
if(cont==10)
cont=0;
__delay_ms(500);
}
return;
}

El siguiente programa cuarto2xc8 muestra 2 displays multiplexados para un contador desde 00 hsta 99.

/*
* File: cuarto2xc8.c
* Author: xxxxxxxxxxxxxxxxx
* 2 display con pic 16f876a
* Created on xxxxxxxxxxxxxxxxxxxxxxx
*/
#include <xc.h>

// PIC16F87xA Configuration Bit Settings


#include "bitsconfig87xAxc8.h"
#define _XTAL_FREQ 4000000

const unsigned char digitos[] = {0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f};
char conteo = 0;
unsigned int numero = 0;
void MostrarDigitos(unsigned int numero) {
ADCON1 = 6;
TRISA = 0;
unsigned char u;
unsigned char d;

d = numero / 10;
u = numero % 10;

PORTB = digitos[u];
PORTAbits.RA0 = 1;
__delay_ms(6);
PORTAbits.RA0 = 0;

PORTB = digitos[d];
PORTAbits.RA1 = 1;
__delay_ms(6);
PORTAbits.RA1 = 0;
}

void main(void) {
unsigned char cont = 0;
ADCON1 = 6;
TRISA = 0;
TRISB = 0;
PORTB = 0;

while (1) {
MostrarDigitos(numero); //Función de aprox. 40ms
conteo++;
if (conteo == 24) //40ms*12 aprox 500ms
{
numero++;
conteo = 0;
if (numero == 100)
numero = 0;
}
}
return;
}

Ejercicio
1.- Modificación del programa cuarto2 con control de start y stop
a.- Al conectar la alimentación
- LEDs -> PC -> #Meza
- Display -> PB -> Off
b.- Al pulsar START (RA1)
- LEDs -> RC0 -> on de la cuenta 12 al 23 y el resto off
- Display -> muestra cuenta de 00 a 24
c.- Al pulsar STOP (RA2)
Regresar al punto a.
4.- PERIFÉRICOS EXTERNOS
4.1 MÓDULO LCD
Este apartado está destinado a una breve descripción del funcionamiento del módulo LCD. Las pantallas de cristal
líquido LCD o display LCD para mensajes (Liquid Cristal Display) tienen la capacidad de mostrar cualquier carácter
alfanumérico, permitiendo representar la información que genera cualquier equipo electrónico de una forma fácil y
económica.
La pantalla consta de una matriz de caracteres (normalmente de 5x7 o 5x8 puntos) distribuidos en una, dos, tres o
cuatro líneas de 16 hasta 40 caracteres cada línea. El proceso de visualización es gobernado por un microcontrolador
incorporado a la pantalla, siendo el Hitachi 44780 el modelo de controlador más utilizado.

Figura 1. LCD 2x16: está compuesto por 2 líneas de 16 caracteres

Las características generales de un módulo LCD 16x2 son las siguientes:


· Consumo muy reducido, del orden de 7.5mW
· Pantalla de caracteres ASCII, además de los caracteres japoneses Kanji, caracteres griegos y símbolos matemáticos.
· Desplazamiento de los caracteres hacia la izquierda o a la derecha
· Memoria de 40 caracteres por línea de pantalla, visualizándose 16 caracteres por línea
· Movimiento del cursor y cambio de su aspecto
· Permite que el usuario pueda programar 8 caracteres
· Pueden ser gobernados de 2 formas principales:
. Conexión con bus de 4 bits
. Conexión con bus de 8 bits

Las pantallas LCD tienen una serie de pines, las cuales tienen unos nombres especiales que por supuesto tienen un
orden de conexión, son 14 pines ordenados del  1 al 14, esto siempre viene indicado en la placa de las pantallas lcd,
estos pines son para  la alimentación, el control y la comunicación, el envió y recibo de datos, tienen ademas 2 pines
para el uso de un led interno que algunas pantallas lcd lo tienen y otras no, normalmente serán como se ve en la
siguiente imagen.

En la siguiente lista se tienen los pines de las pantallas LCD de 2×16 con su número de orden,  su nombre y su
función:

DDRAM
El módulo LCD posee una zona de memoria RAM llamada
DDRAM (Data Display RAM) donde se almacenan los
caracteres que se van a mostrar en la pantalla. Tiene una
capacidad de 80 bytes, 40 por cada línea, de los cuales sólo
32 se pueden visualizar a la vez (16 bytes por línea).
De las 80 posibles, las dos direcciones más importantes de
la DDRAM son:
· Dirección 00h, que es el comienzo de la primera línea
· Dirección 40h, que el comienzo de la segunda línea

CARACTERES DEFINIDOS EN LA CGROM


El LCD dispone de una zona de memoria interna no volátil llamada CGROM donde se almacena una tabla con los 192
caracteres que pueden ser visualizados. Cada uno de los caracteres tiene su representación binaria de 8 bits. Para
visualizar un carácter debe recibir por el bus de datos el código correspondiente ASCII.
También permite definir 8 nuevos caracteres de usuario que se guardan en una zona de RAM denominada CGRAM
(Character Generator RAM).

Modos de funcionamiento
El LCD tiene 3 modos de funcionamiento principales:
· Modo Comando
· Modo Carácter o Dato
· Modo de lectura del Busy Flag o LCD Ocupada

Inicialización
Hay dos algoritmos de inicialización. Cuál se utilizará depende de si la conexión al microcontrolador se realiza por el
bus de datos de 4 o 8 bits. En ambos casos, después de inicialización sólo queda especificar los comandos básicos y,
por supuesto, visualizar los mensajes.
Refiérase a la Figura que sigue para el procedimiento de inicialización por el bus de datos de 8 bits:

El procedimiento de inicialización por el bus de datos de 4 bits:


Para conocer un poco mejor los comandos soportados por la librería para la pantalla lcd 16×2, es necesario
estudiar el juego de instrucciones soportadas por el chip controlador de pantalla, el cual se resume en la
siguiente tabla que hemos traducido de la hoja de datos. La mayoría de los comandos listados pueden ser
ejecutados llamando a una función en C de nuestra librería.

Tiempo de
Código de Operación Descripción
Ejecución
Instrucción

RS R/W B7 B6 B5 B4 B3 B2 B1 B0

Borra el contenido de la
pantalla y retorna el
Clear display 0 0 0 0 0 0 0 0 0 1 1.52 ms
cursor a la posición
“home” (dirección 0).

Retorna el cursor a la
posición “Home”.
Retorna también el área
Cursor home 0 0 0 0 0 0 0 0 1 * de visión a la posición 1.52 ms
inicial. El contenido de la
DDRAM permanece
intacto.

Entry mode 0 0 0 0 0 0 0 1 I/D S Incrementar/Decrementa 37 μs


set r dirección (I/D); Habilitar
corrimiento de pantalla
(S). Estas operaciones se
realizan al leer o escribir
datos

Enciende o apaga el
Display display (D), Cursor
on/off 0 0 0 0 0 0 1 D C B encendido / apagado (C), 37 μs
control destello del cursor (blink)
(B).

Selecciona entre
desplazamiento de
pantalla o de cursor
Cursor/displa (S/C), selecciona la
0 0 0 0 0 1 S/C R/L * * 37 μs
y shift dirección de
desplazamiento (R/L). El
contenido de la DDRAM
permanece intacto.

Configurar en ancho de
bus (4 u 8 bits)
Function set 0 0 0 0 1 DL N F * * (DL),Numero de lineas de 37 μs
display (N), y tipo de
fuente (F).

Escribe la dirección de
CGRAM. Los datos a
Set CGRAM almacenar en CGRAM
0 0 0 1 CGRAM address 37 μs
address pueden ser enviados
después de esta
instrucción

Escribe la dirección de
DDRAM. Los datos a
Set DDRAM almacenar en DDRAM
0 0 1 DDRAM address 37 μs
address pueden ser enviados
después de esta
instrucción

Leer bandera “Ocupado”


(Bussy Flag) indicando si
el controlador está
realizando alguna
Read busy
operación interna o esta
flag &address 0 1 BF CGRAM/DDRAM address 0 μs
listo para aceptar
counter
datos/comandos. Lee la
dirección CGRAM o
DDRAM (dependiendo de
la instrucción previa).

Write Escribe datos a las


CGRAM 1 0 Write Data memorias CGRAM o 37 μs
orDDRAM DDRAM.
Lee datos desde las
Read from
1 1 Read Data memorias CGRAM o 37 μs
CG/DDRAM
DDRAM.

Nombres y significado de de bits en instrucciones


I/D – 0 = disminuir el contador de dirección, 1 = incrementar el contador de dirección;
S – 0 = Sin corrimiento de display, 1 = Con corrimiento de display;
D – 0 = Display Apagado, 1 = Display Encendido;
C – 0 = Cursor Apagado, 1 = Cursor Encendido;
B – 0 = Parpadeo cursor Apagado, 1 = Parpadeo Cursor Encendido;
S/C – 0 = Mover Cursor, 1 = Corrimiento de pantalla;
R/L – 0 = Mover/Correr a la izquierda, 1 = Mover/Correr a la derecha;
DL – 0 = Interfaz de 4 bits, 1 = Interfaz de 8 bits;
N – 0 = 1 linea, 1 = 2 lineas;
F – 0 = 5×8 puntos, 1 = 5×10 puntos;
BF – 0 = puede aceptar instrucción, 1 = operación interna en progreso.

La polarización del LED de fondo se logra conectando una


resistencia externa de 50 ohm-1/4 W con lo que se asegura el
correcto encendido sin una corriente excesiva. El control de
contraste se consigue con un potenciómetro de 10 k con el
cual se ajusta el nivel de voltaje en el pin 3 (Vee ó VLC).

La conexión más recomendable del display LCD 16x2 requiere 4 pines para los datos (D7:D4), 1 pin para
habilitar/deshabilitar el display (E) y 1 pin para los modos comando/carácter (RS).

Librería LCD
En xc8 hay que utilizar una librería para la comunicación con LCDs basados en el controlador HD44780 o
compatibles, a través de un interfaz de 4 bits para datos. Para el trabajo con el módulo LCD se debe añadir la
librería lcd_lib_riverXC8.h, que contiene las funciones listadas en la tabla siguiente.

FUNCION DESCRIPCION
lcd_Init() Inicializa el módulo LCD
lcd_Putch(char c) Visualiza un carácter en la posición actual del cursor
lcd_Putrs(char row, char column, const char * s) Visualiza texto en la posición especificada
lcd_Puts(char row, char column, char * s) Visualiza caracteres en la posición especificada
lcd_Clear(void) Limpia la pantalla y retorna a inicio
lcd_cmd(char c) Envía un comando al LCD
lcd_Data(char car) Escribe un caracter en el LCD

El siguiente programa lcd1xc8 nos mostrará en el lcd la palabra HOLA, letra por letra en la primera línea y dos
gráficos conocidos como el monigote y una llave y mensajes adicionales.
/*
* File: lcd0v2xc8_87xa.c
* Author: Electrotecnia
* lcd0 mejorado
* Created on 7 de noviembre de 2018, 08:57 AM
*/
#include <xc.h>
#include "bitconfig87xa.h"
#define _XTAL_FREQ 4000000
//Definición de comandos básicos
#define lines_5x7 0x28 //2lin-4bit-5x7
#define DonCoff 0x0C //display on cursor off
#define CursorIncr 0x06 //incrementa cursor
#define limpiar 0x01 //limpiar pantalla

//Definiciones de Pines para el conexionado del LCD


#define E_PIN PORTBbits.RB3
#define TRIS_E TRISBbits.TRISB3
#define RS_PIN PORTBbits.RB2
#define TRIS_RS TRISBbits.TRISB2
#define DATA_PORT PORTB
#define TRIS_DATA_PORT TRISB

//Definiciones de Funciones ---------------------------


void lcd_Init(); // initialise the LCD - put into 4 bit mode
void lcd_cmd(unsigned char cmd); // configurar comando
void lcd_Data(unsigned char car); // escribir un caracter
void lcd_Putrs(char row, char column, const char *s); //para cadena de caracteres
void lcd_Puts(char row, char column, char *s); //para cadena de caracteres
void lcd_cursor_xy(char row, char column); // escoger la fila del LCD

const char wob[11] = "ELECTRONICA";


unsigned char cmd, car, datlcd, comandLCD;
char row, column;

void main(void) {
PORTB = 0;
lcd_Init();
while (1) {
lcd_cmd(limpiar);
__delay_ms(300);
lcd_Data('H');
__delay_ms(300);
lcd_Data('o');
__delay_ms(300);
lcd_Data('l');

__delay_ms(300); lcd_Data(0x04);
lcd_Data('a'); lcd_Data(0x0C);
__delay_ms(300); lcd_Data(0x04);
//-- Definir el caracter 0: Monigote lcd_Data(0x00);
lcd_cmd(0x40); //-- Escribir los caracteres definidos
lcd_Data(0x0E); lcd_cmd(0x85);
lcd_Data(0x0E); lcd_Data(0);
lcd_Data(0x04); __delay_ms(500);
lcd_Data(0x1F); lcd_Data(1);
lcd_Data(0x04); __delay_ms(500);
lcd_Data(0x0A); lcd_cursor_xy(2, 5);
lcd_Data(0x11); lcd_Data('P');
lcd_Data(0x00); lcd_Data('E');
//-- Definir caracter 1: Llave lcd_Data('R');
lcd_Data(0x0E); lcd_Data('U');
lcd_Data(0x11); __delay_ms(1000);
lcd_Data(0x0E); lcd_cmd(limpiar);
lcd_Data(0x04); __delay_ms(200);
lcd_Putrs(1, 4, "SENATI"); __delay_ms(15);
__delay_ms(100); DATA_PORT = init_value;
lcd_Puts(2, 1, wob); RS_PIN = 0;
__delay_ms(1000); E_PIN = 1;
} __delay_ms(5);
return; E_PIN = 0;
} __delay_ms(5);
/**********************************/
/* Enviar un comando al LCD */ DATA_PORT = init_value;
/**********************************/ RS_PIN = 0;
void lcd_cmd(unsigned char cmd) { E_PIN = 1;
DATA_PORT &= 0x0F; __delay_ms(5);
DATA_PORT |= cmd & 0xF0; E_PIN = 0;
RS_PIN = 0; __delay_us(200);
E_PIN = 1; DATA_PORT = init_value;
__delay_ms(5); RS_PIN = 0;
E_PIN = 0; E_PIN = 1;
__delay_ms(5); __delay_ms(5);
DATA_PORT &= 0x0F; E_PIN = 0;
DATA_PORT |= (cmd << 4)&0xF0; __delay_us(200);
RS_PIN = 0; DATA_PORT = 0x20;
E_PIN = 1; RS_PIN = 0;
__delay_ms(5); E_PIN = 1;
E_PIN = 0; __delay_ms(5);
__delay_ms(5); E_PIN = 0;
return; __delay_us(40);
}
/*********************************/ lcd_cmd(lines_5x7);
/* Escribir un dato en el lcd */ lcd_cmd(DonCoff);
/*********************************/ lcd_cmd(CursorIncr);
void lcd_Data(unsigned char car) { lcd_cmd(limpiar);
DATA_PORT &= 0x0F; return;
DATA_PORT |= (car & 0xF0); }
RS_PIN = 1;
E_PIN = 1; void lcd_cursor_xy(char row, char column) {
__delay_ms(5); unsigned char direccion;
E_PIN = 0; if (row != 1)
__delay_ms(5); direccion = 0x40;
DATA_PORT &= 0x0F; else
DATA_PORT |= ((car << 4)&0xF0); direccion = 0x00;
RS_PIN = 1; direccion += column - 1;
E_PIN = 1; lcd_cmd(0x80 | direccion);
__delay_ms(5); }
E_PIN = 0;
__delay_ms(5); void lcd_Putrs(char row, char column, const char * s) {
return; char direccion;
} if (row != 1)
direccion = 0x40;
/* Inicialización del LCD */ else
void lcd_Init() { direccion = 0;
//-- Configurar el lcd direccion += column - 1;
char x = 0; lcd_cmd(0x80 | direccion);
char init_value; while (*s)
lcd_Data(*s++);
init_value = 0x30; }
TRIS_DATA_PORT = 0x03;
void lcd_Puts(char row, char column, char * s) {
unsigned char direccion; lcd_cmd(0x80 | direccion);
if (row != 1) while (*s)
direccion = 0x40; lcd_Data(*s++);
else }
direccion = 0;
direccion += column - 1;
El siguiente programa lcd2xc8 nos mostrará en el lcd la palabra HOLA, letra por letra en la primera línea pero
utilizando una libreria.
/*
* File: lcd2xc8.c
* Author: xxxxxxxxxxxxxxxxxxxxxx
* pic 16f877a
* Created on xxxxxxxxxxxxxxxxxxx
*/
#include <xc.h>
#include "lcd_lib_riverXC8.h"
#define _XTAL_FREQ 4000000
// PIC16F87xA Configuration Bit Settings
#include "bitsconfig87xAxc8.h"

void main(void) {
lcd_Init();
while (1) {
lcd_Clear();
lcd_Putrs(1, 1, "H");
__delay_ms(500);
lcd_Putrs(1, 2, "O");
__delay_ms(500);
lcd_Putrs(1, 3, "L");
__delay_ms(500);
lcd_Putrs(1, 4, "A");
__delay_ms(500);
lcd_Clear();
__delay_ms(500);

return;
}

El siguiente programa es un contador de 0 999 con mensajes en la dos líneas.


/*
* File: lcd3 contador-xc8.c
* Author: xxxxxxxxxxxxxxxxxx
*
* Created on xxxxxxxxxxxxxxxxxxxx
*/
//PORTB-lcd-contador-xc8- pic 16f873a
#include <xc.h>
#include "lcd_lib_riverXC8.h"
#define _XTAL_FREQ 4000000
// PIC16F87xA Configuration Bit Settings
#include "bitsconfig87xAxc8.h"
void main(void)
{
lcd_Init();
lcd_Putrs(1,2,"CONTADOR 0-999");
while(1)
{
for (char c = '0'; c <= '9'; c++)
{
lcd_cmd(0xc6);
lcd_Putch(c);
for(char d='0';d<='9';d++)
{
lcd_cmd(0xc7);
lcd_Putch(d);
for(char u='0';u<='9';u++)
{
lcd_cmd(0xc8);
lcd_Putch(u);
__delay_ms(500);
}
}

Ejercicio:
1. Modificar el programa LCD2 para funcionar con start y stop tal que al conectar la alimentación en la 1ra
fila tenga el mensaje NÚMERO DE MEZA X y en la 2da fila el apellido de uno del grupo. Al pulsar start, en
la 1ra fila, la palabra HOLA se muestra letra por letra y al completarse toda debe correr toda hacia la
derecha hasta que desaparezca y se repite nuevamente en un bucle infinito hasta que se presione stop;
en la 2da fila no se muestra ningún mensaje.
2. Modificar el programa para funcionar con start y stop, que cuente de 00 a 30 y que active un motor dc
de la cuenta 12 al 21.

4.2 TECLADOS Y SISTEMAS DE ENTRADA DE DATOS


4.2.1 Uso de teclados matriciales
Las aplicaciones con microcontroladores, requieren en algunos casos el uso de teclas de entrada de datos, para
datos numéricos, funciones e incluso caracteres de texto. La opción de mayor practicidad es el uso de teclados
matriciales, estos consisten en un arreglo de pulsadores alineados en filas y columnas, minimizando el número de
conexiones eléctricas. En las siguientes imágenes se puede ver la apariencia física de un teclado matricial de 4x4 y su
equivalente esquemático:
Figura 4.2.1
Los teclados matriciales pueden tener dimensiones mayores en función de la necesidad del desarrollador. Los
teclados especiales pueden ser fabricados con membranas plásticas con la cantidad de teclas y la distribución de las
mismas, de tal manera que suplan las necesidades del usuario. Sin embargo los teclados 4x4 permiten hacer una
interfaz lo suficientemente completa para muchas de las aplicaciones. Los teclados matriciales funcionan activando
una de 4 columnas y revisando cuál de las filas se activan, este proceso determina cual es la tecla pulsada, de igual
manera el análisis se puede hacer invirtiendo las columnas con las filas.
Digite, compile, simule y analice el siguiente programa:

/*
* File: teclado1xc8
* Author: xxxxxxxxxxxxx
*
* Created on xxxxxxxxxxxxxxxx
*/
#include <xc.h>
#define _XTAL_FREQ 4000000
// PIC16F87xA Configuration Bit Settings
#include "bitsconfig87xAxc8.h"
#include "keyriv.h"

char valor;
void main()
{
TRISA=0B11111111;
TRISC=0B11110000; //PUERTO C salida de la tecla presionada
PORTC=0;
key_init();
while (1)
{
valor=0;
while(valor==0)
{
valor=getkey(0);
}
//Funcion que te deja ver el canal analogo que hallamos elegido
switch(valor)
{
case '0': PORTC=0;
break;
case '1': PORTC=1;
break;
case '2': PORTC=2;
break;
case '3': PORTC=3;
break;
case '4': PORTC=4;
break;
case '5': PORTC=5;
break;
case '6': PORTC=6;
break;
case '7': PORTC=7;
break;
case '8': PORTC=8;
break;
case '9': PORTC=9;
break;
case 'A': PORTC=10;
break;
case 'B': PORTC=11;
break;
case 'C': PORTC=12;
break;
case 'D': PORTC=13;
break;
case '*': PORTC=14;
break;
case '#': PORTC=15;
break;
default: break;
}
}
return;
}

Digite, compile, simule y analice el siguiente programa que usa teclado y lcd:
/*
* File: teclado2lcd-xc8
* Author: xxxxxxxxxxxxxx
*
* Created on xxxxxxxxxxxxxxxxxxx
*/
#include <xc.h>
// PIC16F87xA Configuration Bit Settings
#include "bitsconfig87xAxc8.h"
#include "keyriv.h"
#include "lcd_lib_riverXC8.h"
#define _XTAL_FREQ 4000000
char valor,x;

void main()
{
lcd_Init();
key_init();

lcd_Clear();
lcd_Putrs(1, 1, "HOLA");
__delay_ms(500);
lcd_Putrs(2, 4, "TECLADO");
__delay_ms(500);

lcd_Clear();
lcd_Putrs(1, 1, "PULSA UNA TECLA");
__delay_ms(50);
while (1){
valor=0;
while(valor==0)
{
valor=getkey(0);
}
x=valor;
lcd_PutchDir(2,8,x);
__delay_ms(500);
}
return;

El siguiente programa teclado3XC8 incluye tambien un LCD


y teclado matricial de 4x4. Qué hace el programa?

/*
* File: accesoPortXC8.c
* Author: xxxxxxxxxxxxx
* ???????????
* Created on xxxxxxxxxxxxxxxxxxxx
*/
#include <xc.h>
// PIC16F87xA Configuration Bit Settings
#include "bitsconfig87xAxc8.h"
#include "lcd_lib_riverxc8.h"
#include "keyriv.h"
#include "eepromXC8riv.h"
#define _XTAL_FREQ 4000000
// Clave para almacenar en el PIC durante su grabación.
// La macro __EEPROM_DATA debe contener 8 bytes.
__EEPROM_DATA('7', '2', '3', 'A', '0', '0', '0', '0');

void main(void) {
ADCON1 = 6;
TRISA = 0x03;
TRISC=0xF7;
PORTC=0;
int item; //
int n; //
char k;
int i; //
char data[4], clave[4]; //
lcd_Init(); //
key_init(); //
while (1) //
{
if (PORTAbits.RA0 == 0) //
{
item++; //
__delay_ms(100); //
}
switch (item) {
case 0: //si item es igual a cero
lcd_Puts(1, 1, "Elija una opcion"); //
lcd_Puts(2, 1, "y presione enter"); //
break;

case 1: // si item igual a 1


lcd_Puts(1, 1, "Opcion 1 "); //
lcd_Puts(2, 1, "Ingresar Contras"); //
if (PORTAbits.RA1 == 0) //
{
lcd_Clear(); //Limpia la pantalla LCD
i = 0; //
lcd_Puts(1, 1, "Ingresar clave"); //
while (i <= 3) //
{
k = getkey(0); //
if (k != 0) //
{
data[i] = k; //
i++; //
lcd_cursor_xy(2, i);
lcd_Data('*'); //
}
}
for (i = 0; i <= 3; i++) //
{
clave[i] = leer_eeprom(i); //
}
if ((data[0] == clave[0])&&(data[1] == clave[1])&&(data[2] == clave[2])&&(data[3] == clave[3])) //
{
lcd_Puts(1, 1, "Puerta Abierta"); //
PORTCbits.RC3 = 1; //
__delay_ms(1000); //
PORTCbits.RC3 = 0; //
} else {
lcd_Puts(1, 1, "Puerta Cerrada"); //
__delay_ms(1000); //
item = 0; //
break;
}
}
break;

case 2: //Si item igua a2


lcd_Puts(1, 1, "Opcion 2 "); //
lcd_Puts(2, 1, "Cambiar Contrase"); //
if (PORTAbits.RA1 == 0) //
{
lcd_Clear(); //
i = 0; //
n=0;//
lcd_Puts(1, 1, "Ingrese clave"); //
while (i <= 3) //
{
k = getkey(0); //
if (k != 0) //
{
data[i] = k; //
i++; //
lcd_cursor_xy(2, i);
lcd_Data('*'); //
}
}
for (i = 0; i <= 3; i++) {
clave[i] = leer_eeprom(i); //
}
if ((data[0] == clave[0])&&(data[1] == clave[1])&&(data[2] == clave[2])&&(data[3] == clave[3]))//
{
lcd_Puts(1, 1, "Clave nueva "); //
lcd_Puts(2, 1, " ");
while (n <= 0) //
{
k = getkey(1); //
if (k != 0) //
{
data[n] = k; //
escribir_eeprom(0x00, k); //
n++; //
lcd_cursor_xy(2, n);
lcd_Data('*'); //
__delay_ms(20); //
}
}
while (n <= 1) //
{
k = getkey(1); //
if (k != 0) //
{
data[n] = k; //
escribir_eeprom(0x01, k); //
n++; //
lcd_cursor_xy(2, n);
lcd_Data('*'); //
__delay_ms(20); //
}
}
while (n <= 2) //
{
k = getkey(1); //
if (k != 0) //
{
data[n] = k; //
escribir_eeprom(0x02, k); //
n++; //
lcd_cursor_xy(2, n);
lcd_Data('*'); //
__delay_ms(20); //
}
}
while (n <= 3) //
{
k = getkey(1); //
if (k != 0) //
{
data[n] = k; //
escribir_eeprom(0x03, k); //
n++; //
lcd_cursor_xy(2, n);
lcd_Data('*'); //
__delay_ms(20); //
}
}
lcd_Puts(1, 1, "Clave cambiada"); //
__delay_ms(500);
} else {
lcd_Puts(1, 1, "Clave incorrecta"); //
__delay_ms(1000); //
item = 0; //
break;
}
}
break;

default: //
item = 0; //
break;
}
}
return;
}

El circuito es el de la siguiente figura:

ADC en el microcontrolador PIC PIC16F87xA


La mayoría de los proyectos de Microcontroladores incluirán un ADC (conversor de Analógico a Digital), porque es
una de las formas más utilizadas para leer datos del mundo real. Casi todos los sensores como sensor de
temperatura, sensor de flujo, sensor de presión, sensores de corriente, sensores de voltaje, giroscopios,
acelerómetros, sensor de distancia y casi todos los sensores o transductores conocidos producen una tensión
analógica de 0V a 5V según la lectura de los sensores. Un sensor de temperatura, por ejemplo, puede dar 2.1 V
cuando la temperatura es de 25 ° C y sube a 4.7 cuando la temperatura es de 60 ° C. Para conocer la temperatura del
mundo real, la MCU tiene que leer el voltaje de salida de este sensor de temperatura y relacionarlo con la
temperatura del mundo real. Por lo tanto, ADC es una importante herramienta de trabajo para proyectos de MCU y
permite aprender cómo podemos usarlo en nuestro PIC16F87xA.

El PIC16F87xA tiene un ADC de 10 bits, esto significa que el


valor de salida de nuestro ADC será de 0 a 1023 y que hay 8
pines (canales) en nuestra MCU que pueden leer voltaje
analógico. El valor 1023 se obtiene por (2 ^ 10)-1 ya que nuestro
ADC es de 10 bits. Los ocho pines que pueden leer el voltaje
analógico se mencionan en la hoja de datos. Ver figura.

Los canales analógicos AN0 a AN7 están resaltados, solo estos


pines podrán leer voltaje analógico. Entonces, antes de leer un
voltaje de entrada, debemos especificar en nuestro código qué
canal debe usarse para leer el voltaje de entrada.

El microcontrolador PIC 16F87xA convertirá las entradas analógicas en un número digital de 10 bits
correspondiente. En aras de la explicación, tome ADC Lower Reference como 0V y Higher Reference como 5V.
 Vref- = 0V
 Vref + = 5V
 n = 10 bits, algunos autores también le denominan Resolución del ADC
 Resolución = (Vref + - Vref -) / (2 n - 1) = 5/1023 = 0.004887V ≈ 5mV

Entonces la resolución del ADC es 0.00487V, que es el voltaje mínimo requerido para cambiar un bit. Vea los
ejemplos a continuación.
Entrada analógica Salida digital

Valor Hexadecim Decim


Valor Real Binario
Aproximado al al

0V 0b000000000
0V 0x000 0
0

0,004887 5mV 0b000000000


0x001 1
V 1

0,009774 10mV 0b000000001


0x002 2
V 0

0,014661 15mV 0b000000001


0x003 3
V 1

1,25mV

0b101010101
0

4,999401 5V 0b111111111
0x3FF 1023
V 1

El módulo ADC de PIC 16F87xA tiene 4 registros.


 ADRESH - A/D Result High Register
 ADRESL - Resultado bajo A/D Registro bajo
 ADCON0 - Registro de control A/D 0
 ADCON1 - Registro de control A/D 1
Además las líneas I/O involucrados al convertidor corresponden a los registros TRISA, PORTA, TRISE y PORTE
dependiendo del PIC a usar.
Si este convertidor trabajará con interrupciones, también habrá que considerar a los registros INTCON, PIE1 y PIR1.

Programación para ADC:


El programa para usar ADC con microcontrolador PIC es muy simple, solo tenemos que entender estos cuatro
registros y luego leer cualquier voltaje analógico será simple. Como de costumbre, inicialice los bits de configuración
y comencemos con el vacío principal ().

Dentro del vacío principal () tenemos que inicializar nuestro ADC usando el registro ADCON1 y el registro ADCON0 .
El registro ADCON0 tiene los siguientes bits:

ADCON0-register-of-PIC-microcontroller

En este registro tenemos que encender el módulo ADC haciendo ADON = 1 y activar el reloj de conversión A / D
utilizando los bits ADCS1 y ADCS0, el resto no se configurará por el momento. En nuestro programa, el reloj de
conversión A / D se selecciona como Fosc / 16, puede probar sus propias frecuencias y ver cómo cambia el resultado.
Los detalles completos están disponibles en la página de la hoja de datos 127 . Por lo tanto, ADCON0 se inicializará
de la siguiente manera.

ADCON0 = 0b01000001;

Ahora el registro ADCON1 tiene los siguientes bits:

ADCON1-register-of-PIC-microcontroller

En este registro tenemos que hacer que el bit de selección de formato de resultado A / D sea alto por ADFM = 1 y
hacer ADCS2 = 1 para seleccionar nuevamente el Fosc / 16. Los otros bits permanecen cero ya que hemos planeado
usar la tensión de referencia interna. Los detalles completos están disponibles en la página de datos 128. Por lo
tanto, ADCON1 estableceremos lo siguiente.

ADCON1 = 0x11000000;
Ahora, después de inicializar el módulo ADC dentro de nuestra función principal, ingresemos en el ciclo while y
comencemos a leer los valores ADC. Para leer un valor ADC, se deben seguir los siguientes pasos .

Inicializar el Módulo ADC


Seleccione el canal análogo
Inicie ADC haciendo que el bit Go / Done sea alto
Espere a que el bit Go / DONE sea bajo
Obtenga el resultado ADC de ADRESH y el registro ADRESL
1. Inicialice el Módulo ADC: ya hemos aprendido cómo inicializar un ADC, así que simplemente llamamos a esta
función para inicializar el ADC.

La función vacía ADC_Initialize () es la siguiente.

void ADC_Initialize ()
{
ADCON0 = 0b01000001; // ADC ON y se selecciona Fosc / 16
ADCON1 = 0b11000000; // Se ha seleccionado voltaje de referencia interno
}
2. Seleccione el canal analógico: ahora tenemos que seleccionar qué canal vamos a usar para leer el valor ADC.
Hagamos una función para esto, de modo que nos resulte fácil desplazarnos entre cada canal dentro del ciclo while.
unsigned int ADC_Read (canal de char sin firmar)
{
// **** Seleccionando el canal ** ///
ADCON0 & = 0x11000101; // Borrar los bits de selección de canal
ADCON0 | = canal << 3; // Configurando los Bits requeridos
// ** La selección del canal completa *** ///
}
Luego, el canal que se va a seleccionar se recibe dentro del canal variable. En la linea

ADCON0 & = 0x1100101;


La selección del canal anterior (si hay) se borra. Esto se hace usando el bit a bit y el operador "&". Los bits 3, 4 y 5 son
forzados a ser 0 mientras que los otros quedan en sus valores previos.

Luego, el canal deseado se selecciona desplazando a la izquierda tres veces el número del canal y configurando los
bits utilizando el operador bit a bit o el operador "|".

ADCON0 | = canal << 3; // Configurando los Bits requeridos


3. Inicie ADC haciendo que el bit Go / Done sea alto: una vez que se selecciona el canal, debemos comenzar la
conversión de ADC simplemente haciendo que el bit GO_nDONE sea alto:

GO_nDONE = 1; // Inicializa la conversión A / D


4. Espere a que el bit Go / DONE se ponga bajo: el bit GO / DONE permanecerá alto hasta que se complete la
conversión ADC, por lo tanto, tendremos que esperar hasta que este bit vuelva a bajar . Esto se puede hacer usando
un ciclo while.

while (GO_nDONE); // Espere a que se complete la conversión A / D


Nota: Colocar un punto y coma al lado de while hará que el programa se mantenga allí hasta que la condición del
ciclo while sea falsa.

5. Obtenga el resultado ADC de los registros ADRESH y ADRESL: cuando el bit Go / DONE vuelve a ser bajo significa
que la conversión de ADC está completa. El resultado del ADC será un valor de 10 bits. Como nuestra MCU es una
MCU de 8 bits, el resultado se divide en 8 bits superiores y 2 bits inferiores. El resultado superior de 8 bits se
almacena en el registro ADRESH y el inferior de 2 bits se almacena en el registro ADRESL. Por lo tanto, tenemos que
sumar estos a los registros para obtener nuestro valor ADC de 10 bits. Este resultado es devuelto por la función
como se muestra a continuación:

return ((ADRESH << 8) + ADRESL); // Devuelve el resultado


La función completa que se utiliza para seleccionar el canal ADC, activar el ADC y devolver el resultado se muestra
aquí.

unsigned int ADC_Read (canal de char sin firmar)


{
ADCON0 & = 0x11000101; // Borrar los bits de selección de canal
ADCON0 | = canal << 3; // Configurando los Bits requeridos
__delay_ms (2); // Tiempo de adquisición para cargar el condensador de espera
GO_nDONE = 1; // Inicializa la conversión A / D
while (GO_nDONE); // Espere a que se complete la conversión A / D
return ((ADRESH << 8) + ADRESL); // Devuelve el resultado
}
Ahora tenemos una función que tomará la selección del canal como entrada y nos devolverá el valor de ADC. Por lo
tanto, podemos llamar directamente a esta función dentro de nuestro ciclo while, ya que estamos leyendo la tensión
analógica del canal 4 en este tutorial, la llamada a la función será la siguiente.

i = (ADC_Read (4)); // almacena el resultado de adc en "i".


Para visualizar la salida de nuestro ADC necesitaremos algún tipo de módulos de visualización como el LCD o el 7-
segment. En este tutorial estamos usando una pantalla de 7 segmentos para verificar la salida.
El código completo se proporciona a continuación y el proceso también se explica en el Video al final.

Configuración y prueba de hardware:


Como es habitual simular el código utilizando Proteus antes de ir realmente con nuestro hardware, los esquemas del
proyecto se muestran a continuación :

Using-ADC-Module-of-PIC-Microcontroller-circuit-diagram

Las conexiones del módulo de pantalla de siete segmentos de cuatro dígitos con el microcontrolador PIC son las
mismas que en el proyecto anterior, hemos agregado un potenciómetro al pin 7 que es el canal analógico 4. Al variar
el potenciómetro, se enviará un voltaje variable a la MCU que será leído por el módulo ADC y se mostrará en el
Módulo de visualización de 7 segmentos. Consulte el tutorial anterior para obtener más información sobre la
pantalla de 7 dígitos de 4 dígitos y su interfaz con PIC MCU.

Aquí hemos utilizado la misma placa de microcontrolador PIC que hemos creado en el Tutorial LED parpadeante .
Después de asegurar la conexión cargue el programa en PIC y debería ver un resultado como este

output-of-ADC-Module-of-PIC-Microcontroller-PICF877A

output-of-ADC-Module-of-PIC-Microcontroller

Aquí hemos leído el valor ADC del pot y lo convertimos al voltaje real mapeando la salida 0-1024 como 0-5 voltios
(como se muestra en el programa). El valor se muestra luego en el segmento 7 y se verifica con el multímetro.

Eso es todo, ahora estamos listos para usar todos los Sensores Analógicos disponibles en el mercado, adelante,
intente esto y si tiene algún problema como de costumbre use la sección de comentarios, estaremos encantados de
ayudarlo.

Código:
// PIC16F87xA Configuration Bit Settings
#include "bitsconfig87xAxc8.h"

// Las sentencias de configuración de #pragma deben preceder al archivo de proyecto.


// Usa las enumeraciones del proyecto en lugar de #define para ENCENDIDO y APAGADO.

#include <xc.h>
#define _XTAL_FREQ 20000000

// *** Definir los pines de señal de las cuatro pantallas *** //


#define s1 RC0
#define s2 RC1
#define s3 RC2
#define s4 RC3
// *** Fin de la definición ** ////

void ADC_Initialize ()
{
ADCON0 = 0b01000001; // ADC ON y se selecciona Fosc / 16
ADCON1 = 0b11000000; // Se ha seleccionado voltaje de referencia interno
}

unsigned int ADC_Read (canal de char sin firmar)


{
ADCON0 & = 0x11000101; // Borrar los bits de selección de canal
ADCON0 | = canal << 3; // Configurando los Bits requeridos
__delay_ms (2); // Tiempo de adquisición para cargar el condensador de espera
GO_nDONE = 1; // Inicializa la conversión A / D
while (GO_nDONE); // Espere a que se complete la conversión A / D
return ((ADRESH << 8) + ADRESL); // Devuelve el resultado
}

vacío principal()
{int a, b, c, d, e, f, g, h, adc; // solo variables
int i = 0; // el valor de 4 dígitos que se va a mostrar
int flag = 0; // para crear demoras

unsigned int seg [] = {0X3F, // Valor hexadecimal para mostrar el número 0


0X06, // Valor hexadecimal para mostrar el número 1
0X5B, // Valor hexadecimal para mostrar el número 2
0X4F, // Valor hexadecimal para mostrar el número 3
0X66, // Valor hexadecimal para mostrar el número 4
0X6D, // Valor hexadecimal para mostrar el número 5
0X7C, // Valor hexadecimal para mostrar el número 6
0X07, // Valor hexadecimal para mostrar el número 7
0X7F, // Valor hexadecimal para mostrar el número 8
0X6F // Valor hexadecimal para mostrar el número 9
}; // Fin de la matriz para mostrar los números del 0 al 9

// ***** Configuración de E / S **** //


TRISC = 0X00;
PORTC = 0X00;
TRISD = 0x00;
PORTD = 0X00;
// *** Fin de la configuración de E / S ** ///

ADC_Initialize ();

#define _XTAL_FREQ 20000000

mientras (1)
{

if (flag> = 50) // espere a que la bandera llegue a 100


{
adc = (ADC_Read (4));
i = adc * 0.488281;
bandera = 0; // solo si el indicador es cien "i" obtendrá el valor de ADC
}
flag ++; // indicador de incremento para cada flash

// *** Dividir "i" en cuatro dígitos *** //


a = i% 10; // 4º dígito se guarda aquí
b = i / 10;
c = b% 10; // 3er dígito se guarda aquí
d = b / 10;
e = d% 10; // 2do dígito se guarda aquí
f = d / 10;
g = f% 10; // 1er dígito se guarda aquí
h = f / 10;
// *** Fin de la división *** //

PORTD = seg [g]; s1 = 1; // Encienda la pantalla 1 e imprima 4to dígito


__delay_ms (5); s1 = 0; // Apagar la pantalla 1 después de 5 ms de retraso
PORTD = seg [e]; RD7 = 1; s2 = 1; // ENCIENDA la pantalla 2 e imprima el 3er dígito
__delay_ms (5); s2 = 0; // Apagar la pantalla 2 después de 5 ms de retraso
PORTD = seg [c]; s3 = 1; // Enciende la pantalla 3 e imprime el 2do dígito
__delay_ms (5); s3 = 0; // Apagar la pantalla 3 después de 5 ms de retraso
PORTD = seg [a]; s4 = 1; // Encienda la pantalla 4 e imprima el 1er dígito
__delay_ms (5); s4 = 0; // Apagar la pantalla 4 después de 5 ms de retraso

}
}

También podría gustarte