Programación PIC en C

Este tutorial te guiará paso a paso en el aprendizaje de la programación en lenguaje C utilizando el compilador PCW de CCS. A lo largo del curso, aprenderás los fundamentos del lenguaje C, cómo utilizar las funciones y librerías del compilador PCW, y cómo programar microcontroladores PIC para realizar diferentes tareas y proyectos.

Biblioman

2/19/200965 min read

Curso para aprender a programar en lenguaje C utilizando un compilador para PIC, en concreto el PCW compiler de la casa CCS. Cursos sobre C en Internet a miles, pero todos los que yo he visto están realizados sobre compiladores de propósito general, como Vicual C++ de Microsoft ó Builder C++ de Borlan, sin duda son excelentes compiladores que nos permiten realizar aplicaciones para ordenadores de escritorio tanto en C como en C++ (la versión orientada a objetos de C), pero no sirven para programar PIC, es decir con el ejecutable que generan al compilar no se puede programar un Microcontrolador.

Lo habitual hasta ahora es que los usuarios que se inician en este apasionante mundo de la programación de Microcontroladores, sea de la marca que sea, primero lo hacían utilizando el lenguaje ensamblador, especifico no solo ya para cada marca de microcontrolador sino para cada modelo, ya que hay que conocer perfectamente los recursos de cada Microcontrolador (Número de puertos de Entrada/Salida Relojes internos, etc. ). Al principio de los tiempos de estos dispositivos esto era obligatorio ya que los recursos de memoria y velocidad de procesamiento no eran muy grandes y había que optimizar el código al máximo, esto implicaba que había que utilizar a la fuerza un lenguaje de programación de bajo nivel que bien utilizado explotara los recursos de estos dispositivos sin desperdiciar memoria y velocidad de procesamiento, pero al igual que ha ocurrido con los ordenadores personales las prestaciones de estos dispositivos ha ido creciendo exponencialmente con el tiempo, siendo ya perfectamente factible el utilizar un lenguaje de alto nivel para programar estos dispositivos y aprovecharnos de las ventajas de portabilidad que ofrecen este tipo de lenguajes, de esta manera por ejemplo podemos hacer un programa para un PIC en concreto y utilizarlo en otro de mayores prestaciones sin modificar apenas nada del código fuente.

¿Quien puede sacar provecho de este curso?. Este curso es para ti si:

  • Has programado PIC en Ensamblador y quieres hacerlo en un lenguaje de alto nivel como el C.

  • No has programado nunca Microcontroladores pero conoces el lenguaje de programación C de haberlo utilizado para otros propósitos.

  • No has programado nunca un PIC en Ensamblador, ni conoces ningún lenguaje de alto nivel como el C. Es decir, no tienes ni idea de Microcontroladores ni de programación (Esto es posible porque el curso va ha empezar desde cero es decir con el clásico Hola Mundo con el que empiezan todos los libros de iniciación a la programación.

Bueno alguno pensará que para aprender a programar en C vale cualquier compilador de uso general y lo que realmente interesa es saber las instrucciones de C que tengo que utilizar para configurar por ejemplo un puerto como entrada o salida, o que código tengo que utilizar para utilizar los convertidores A/D que incorporan ya casi todos los PIC, indudablemente ese es el propósito final de este curso y para ello paralelamente a él va haber otro donde se van a ver aplicaciones prácticas. Pero hay que tener en cuenta que los compiladores para Microcontroladores son específicos para estos dispositivos embebidos y no cumplen con el Estándar ANSI C al 100 %, por lo que cuando estés programando lo más seguro es que te vayas dando cuenta que una función que en el C estándar funciona perfectamente aquí te da un error al compilar. Además te irás quedando con mucho código que lo has probado y sabes que te funciona perfectamente, cuando tengas que hacer una aplicación práctica no tendrás la duda si puedes usar una determinada estructura en tu programa ó si es posible utilizar punteros o no y como hacerlo, porque ya lo sabrás a la vez que has ido aprendiendo el lenguaje de programación y no solo eso, te irás familiarizando con las instrucciones específicas del compilador: de que herramientas dispone, sus funciones precompiladas, su sistema de depuración de errores, etc.

¿Que herramientas voy a necesitar para realizar el curso?.

El compilador CCS seguro, vaya es de pago ya empezamos con problemas te puedes bajar una versión de Evaluación por 30 días desde aquí:

http://www.ccsinfo.com/ccsfreedemo.php

Después de rellenar el formulario te descargas el programa de instalación y lo instalas en tu ordenador como un programa más de Windows, aunque tienes que tener en cuenta que solo podrás programar un pequeño conjunto de PIC de cada familia, otro inconveniente es que tienes que estar conectado a Internet para que te funcione si no te aparecerá esta ventanita poco amigable:

Otra limitación es que el tamaño del programa no puede superar los 2K de memoria, aunque para los ejemplos que vamos a hacer aquí te sobra. Bien ya tenemos solucionado el tema del compilador, bien sea por que con la demo nos apañamos o porque tengo un amigo cojonudo que me va ha prestar uno con licencia para que pueda realizar el curso (je,je..).

Bien ya tengo el compilador y puedo empezar a programar y a crear mis .HEX (para el que no lo sepa es el archivo que tenemos que cargar en nuestro PIC para que funcione). Todo esto es muy elemental para el que lo sabe, pero como dije al principio este curso está pensado también para el que no tiene ni idea de programar microcontroladores. Así es que sigamos.

Una vez que tenemos nuestro .HEX tendremos que comprobar que funciona realmente para ello tenemos dos opciones:

  • Montar nuestro circuito con todos sus componentes, programar el PIC con un programador comercial como el PICSTART de Microchip o con uno de los muchos que hay en Internet que sirven perfectamente para empezar a programar estos dispositivos.

  • Utilizar un programa de simulación electrónica como Proteus que tiene la ventaja de disponer de una extensa biblioteca de microcontroladores PIC junto con los componentes auxiliares que normalmente utilizan estos dispositivos: leds, pantallas LCD, teclados, memorias, etc.

Bien nosotros utilizaremos la segunda opción, aunque en una práctica veremos como hacerlo de la primera forma.

La versión de evaluación de Proteus te la puedes descargar desde aquí:

http://www.labcenter.co.uk/download/prodemo_download.cfm

Claro que tiene limitaciones, sino no sería una demo, la principal es que no podemos guardar nuestros

trabajos y la segunda es que no lleva incorporado muchas de las librerías dinámicas necesarias

para realizar la simulación de algunos microcontrloladores. Mira a ver si el amigo que te dejo el compilador te puede dejar también una licencia para este magnífico Simulador.

Nota: en este sitio está prohibido el mostrar ó facilitar enlaces a lugares de descarga de dudosa reputación. Lo digo tambien para que se tenga en cuenta en el foro que próximamente estará a vuestra disposición y donde podremos exponer nuestras dudas ó experiencias sobre este tema y sobre otros que irán saliendo.

Otra cosa que quiero aclarar es que el curso va a ser todo lo práctico que pueda y la forma de proceder será

la siguiente: iré mostrando uno ó varios ejemplos de cada tema y posteriormente haré una Explicación de los mismos. Yo no se vosotros pero yo cada vez que he querido aprender algo nuevo lo primero que he hecho a sido mirar los ejemplos, después vosotros tendréis que comprobar que lo que he dicho es cierto y que el ejemplo compila y funciona sin errores.

Para facilitar la navegación el próximo día presentaré el índice de los temás que va a tratar el curso

Aunque puede que lo vaya modificando según vallamos avanzando.

Vamos a crear nuestro primer ejemplo paso a paso: abrimos el IDE de nuestro compilador y seleccionamos New->Source File según se muestra en la figura de abajo:

Nos saldrá un cuadro de dialogo de guardar de Windows, donde le pondremos un nombre a nuestro archivo y lo guardaremos

Después escribimos el código fuente que se muestra en la figura de abajo y guardamos el documento:

Comentario del programa:

En primer lugar nos encontramos con tres directivas del prepocesador, las identificaremos porque empiezan por el símbolo (#):

  • La primera de ellas es una directiva include su función es introducir un documento dentro de otro. En la posición del programa donde se encuentra esta directiva, se incluirá el archivo indicado. Se suele usar para incluir los archivos de cabecera (generalmente con extensión.h). En este caso concreto se incluye el archivo <16F877A.h>,en este archivo se incluyen las definiciones de los registros del PIC.

  • #use delay (clock=4000000); directiva para el uso de retardos, entre paréntesis tenemos que poner la frecuencia de reloj que vamos a utilizar.

  • #use rs232 (baud=9600,parity=N,xmit=PIN_C6,rcv=PIN_C7,bits=8) esta directiva es para la comunicación del PIC con otro dispositivo vía RS232, por ejemplo un ordenador, en ella se encuentran definidas los prototipos de las funciones de entrada y salida como printf().

En segundo y último lugar se encuentra la función main. Este es el núcleo del programa, el que va ha incluir todos los pasos a seguir durante su ejecución. En nuestro primer ejemplo solo contiene una sentencia que hace una llamada a la función printf(), esta función se encarga de mostrar un mensaje por el dispositivo de salida RS-232.

El mensaje que muestra la función printf es el que recibe como parámetro (el texto entre paréntesis). Dicho mensaje es delimitado por las comillas dobles, que indican el principio y el fin de una cadena de texto.

Bien una vez creado el archivo .c de nuestro programa tenemos que crear un proyecto y asociarle el archivo que acabamos de crear, tenemos dos opciones crearlo manualmente ó utilizar el wizard que tiene el IDE, en este primer ejemplo utilizaremos la opción manual.

Después seleccionamos New ->Project Manual

Y añadimos el Ejemplo1.c que hemos creado a nuestro proyecto:

Seleccionamos la pestaña Compile y pulsamos sobre Build All para construir todo.

Vemos que el archivo de salida no nos ha producido ningún error. Por tanto el proyecto se ha generado correctamente.

Y si vamos a la carpeta donde habíamos guardado nuestro primer ejemplo, tenemos todos los archivos que nos ha creado el IDE:

De todos estos archivos los que mas nos interesa son los que están marcados en la figura de arriba. El archivo Ejemplo1.hex es el que tenemos que utilizar para programar el PIC y el que termina con extensión .cof lo utilizaremos para cargarlo en el simulador Proteus y poder simular el programa paso a paso, entre otras posibilidades muy útiles a la hora de depurar nuestro código.

Bien ya tenemos nuestro primer ejemplo generado y listo para cargarlo en nuestro simulador Proteus. Vamos a ello: Arrancamos nuestro simulador Proteus y pasamos a colocar nuestros dispositivos en el área de trabajo. Empezaremos colocando el PIC, para ello hacemos clic en el botón que pone Pick Devices según se muestra en la figura de abajo:

En la ventana que nos aparece en el campo Keywords escribimos el nombre de nuestro PIC.

Una vez seleccionado hacemos doble clic sobre el para incorporarlo a nuestro proyecto.

Bien, vamos por el segundo y último elemento que necesitamos para simular nuestro programa. Hay que tener en cuenta que el simulador es capaz de hacer funcionar nuestro circuito sin algunos elementos que serían necesarios si decidimos montar nuestro circuito en una placa real (por ejemplo la alimentación del PIC y el cristal de cuarzo).

El segundo elemento que necesitamos es un Terminal Virtual que hará las veces de monitor, para poder ver las salidas en formato texto de nuestro PIC como si se tratará del símbolo del sistema en un ordenador de escritorio con el Windows instalado. En la figura de abajo se muestra donde podemos incorporar dicho instrumento.

Con esto ya tendremos los dos elementos necesarios para simular nuestros programas, recordemos que en este curso se va a ver las generalidades del lenguaje C aplicadas a este compilador, en el caso de las aplicaciones prácticas que empezaremos pronto en otro articulo tendremos que hacer un circuito independiente para cada ejemplo ya que cada uno de ellos incorporará elementos diferentes como: diodos Led, motores, teclados, displays, etc.

La interconexión de los dos dispositivos es muy sencilla según se muestra en la figura de abajo, solo hay que hacer clic con el puntero del ratón en forma de lápiz entre los terminales que queremos conexionar:

El pin del PIC que habíamos elegido como transmisión de datos en nuestro programa irá conectado al terminal RXD de recepción de datos en el Terminal Virtual y viceversa.

Bien ahora tenemos que cargar nuestro programa en el PIC para poder simularlo, para ello hacemos doble clic sobre el PIC y nos aparecerá la ventana de la figura de abajo:

Los valores que en un principio tenemos que introducir para que nuestra simulación funcione son los que están señalados en la figura de arriba. En Program File pincharemos sobre la carpeta y seleccionaremos el archivo con extensión .cof que se había creado al compilar nuestro programa, si en vez de este seleccionamos el que tiene extensión .Hex funcionará igual pero no podremos realizar la simulación paso a paso. El otro valor a tener en cuenta es que la frecuencia del reloj del PIC debe coincidir con el valor que le habíamos puesto en el programa en nuestro caso 4 MHz.
Una vez hecho esto guardamos nuestro proyecto.

Si ahora hacemos clic sobre el botón Play se nos abrirá una terminal al estilo MSDos donde nos mostrará la salida de nuestro programa:

Ahora si le damos al botón de simulación paso a paso podremos simular nuestro ejemplo paso a paso.

Si en vez de ello nos sale una ventana mostrando dos advertencias de que no se puede encontrar el código fuente de nuestro ejemplo, como se muestra en la figura de abajo.

Seguiremos los siguientes pasos:

En el menú seleccionamos Source y hacemos clic sobre Define Code Generation Tools

Nos aparecerá la ventana de abajo en la que pulsaremos sobre el botón New

Buscamos en nuestro directorio donde se ha instalado el compilador y seleccionamos CCsc.exe tal y como se muestra en la figura de abajo:

Después en el combo Tool seleccionamos el compilador y configuramos el resto de parámetros tal y como se muestra en la figura de abajo y pulsamos OK:

Ahora nos queda añadir nuestro código fuente para ello vamos al menú seleccionamos Source Add/Remove Source files…

Y añadimos nuestro código fuente ejemplo1.c
Si ahora volvemos a simular nuestro ejemplo paso a paso nos aparecerá la ventana siguiente:

Donde podemos ver la ejecución del programa línea a línea o poner puntos de interrupción en las partes del programa que nosotros queramos. Saber que existe un plugin que permite integrar un visor de proteus en el famoso simulador MPLAB. Incluiré un video en la sección de descargas donde explica como hacerlo, aunque nosotros seguiremos utilizando este método.

Bien a partir de ahora ya podemos empezar a estudiar el lenguaje de programación C en este compilador como si fuera un compilador cualquiera como Microsoft Visual C++ pero comprobando las particularidades de este compilador. Todos los ejemplos van a seguir el mismo procedimiento por lo que solo pondré el código y la explicación del mismo.

Variables

¿Qué son las variables? pues sencillamente el poder identificar con un nombre una o varias posiciones de memoria de la RAM de nuestro PIC y de esta manera el poder almacenar allí los datos que va a utilizar nuestro programa.
En C para poder utilizar una variable primeramente hay que declararla siguiendo la siguiente sintaxis:

tipo nombre_variable [=valor];

Lo que va entre corchetes es porque es opcional es decir, las variables se pueden inicializar ó no al declararlas.
Ejemplo de variable declarada:

int i;

Ejemplo de variable declarada e inicializada:


int i=5;

En una misma línea se puede declarar más de una variable siguiendo el siguiente formato:


tipo nombre_variable1,nombre_variable2,....;

Hay que tener en cuenta que la línea tiene que acabar en punto y coma.
El tipo de datos es obligatorio ponerlo y le dice al compilador cuantas celdillas de memoria tiene que reservar para almacenar el valor de la variable. Los tipos de datos pueden variar de un compilador a otro, vamos a ver los tipos de datos que podemos usar con nuestro compilador CCS.


Los tipos de datos básicos que utiliza nuestro compilador son los siguientes:

Sin embargo el compilador CCS también admite los siguientes tipos de datos definidos en el estándar C y que son los que normalmente se utilizan a la hora de programar:

Todos los tipos excepto float son por defecto sin signo, aunque pueden llevar el especificador unsigned ó signed y su rango de valores será el que corresponda a su tipo básico.

Estos son los tipos básicos, también están los tipos de datos compuestos como Enumeraciones, Estructuras y Uniones que están formados por una combinación de los básicos y que los veremos más adelante.


El nombre de la variable no puede ser una palabra clave (reservada por el compilador para realizar unas funciones determinadas y los caracteres que podemos utilizar son las letras: a-z y A-Z ( ¡ojo! la ñ o Ñ no está permitida), los números: 0-9 y el símbolo de subrayado _. Además hay que tener en cuenta que el primer carácter no puede ser un número.

¿Dónde se declaran las variables?

Las variables según el lugar en que las declaremos pueden ser de dos tipos: globales o locales.

  • La variables globales se declaran fuera de las funciones y pueden ser utilizadas en cualquier parte del programa y se destruyen al finalizar éste.

  • Las variables locales se declaran en la función en que van a ser utilizadas. Sólo existen dentro de la función en que se declara y se destruye al finalizar dicha función. Si una función va a usar argumentos (DATOS), entonces debe declarar las variables que van a aceptar los valores de esos argumentos. Estas variables son los parámetros formales de la función. Se comportan como cualquier otra variable local de la función, creándose al entrar en la función y destruyéndose al salir. Cuando veamos el tema de las funciones veremos ejemplos de estas variables.

Bueno ya está bien de teoría vamos hacer un ejemplo donde vamos a declarar y a usar varios tipos de variables:

Este programa generará la siguiente salida:

Comentario del programa:

  • El compilador utiliza 8 bits para representar los números enteros sin signo con lo cual podemos representar desde el 0 hasta el 255 que corresponde en binario al número: 11111111. Por lo que al asignarle a la variable el valor 256 el compilador no generará un error pero el dato guardado será erróneo, nos mostrará 0 que es el siguiente valor a 255 en binario.

  • Para los números enteros con signo también se utilizan 8 bits pero el último bit se reserva para el signo, con lo que se podrán representar los números desde: -127 al 127.

  • El tipo short se utilizará para las variables de un bit y tendrán como valor 0 ó 1.

  • Para los números tipo long int se reservan 16 bits sin signo con lo que su rango va de 0 a 65535

  • Para el tipo signed long se reservan también 16 bits pero se utiliza uno para el signo, por lo que se tiene un rango que va desde -32767 a 32767.

  • El tipo float define un número de 32 bits en punto flotante. y con el podremos representar los números reales.

  • El tipo char se utiliza para almacenar los caracteres, utiliza 8 bits sin signo suficientes para representar los 256 caracteres del código ASCII.

  • Los símbolos %D, %lu, %ld, %c le indica a la función printf en que formato tiene que representar el número. En la ayuda del compilador vienen los diferentes especificadores que hay para los diferentes tipos de datos. A lo largo de los siguientes ejemplos se irán mostrando algunos más.

CONSIDERACIONES: Hay que intentar siempre utilizar el tipo de dato que menos memoria ocupe dentro de los valores que pueda utilizar la variable. Si abusamos de los tipos grandes para almacenar valores pequeños nos quedaremos sin memoria y en los programas grandes es un dato que tenemos que tener en cuenta.

Nota: en los ejemplos que tengan poco código fuente como este y para que el formato de texto salga con los mismos colores que utiliza el compilador utilizaré imágenes para mostrar el código y en la sección de descargas iré incluyendo los ejemplos del curso para que todo el que no quiera teclearlos a mano se los pueda descargar. Otra cosa no incluiré el circuito en Proteus ya que es el mismo para todos los ejemplos a excepción de que en algunos ejemplos pueda ir cambiando el tipo de PIC.

Constantes

Antes de empezar con el tema de las constantes voy a comentar valga la redundancia la forma de poner comentarios a nuestros programas.

Hay dos formas de poner comentarios en C:

  • Poniendo doble barra (la que hay encima del 7), esta forma es práctica para comentar una línea.

Ejemplo:


//Este texto es un comentario.
//y este otro también.

  • la otra forma es meter el texto a comentar dentro de estos símbolos /* mi comentario*/. La ventaja de este sistema es que podemos comentar bloques de textos enteros.

Ejemplo:

/*Mi comentario empieza aquí.....
mas comentarios ..
y termina aquí */

El comentar nuestro código es una buena costumbre que no debemos pasar por alto, ya que si pasado un tiempo queremos volver a un programa y modificar alguna parte de él ayuda mucho el que su código esté comentado. Otra forma en la que se utilizan los comentarios es a la hora de depurar código, en vez de estar borrando y escribiendo trozos de código que no funcionan correctamente los comentamos, de está forma el compilador no los tratará como código fuente y podremos realizar ajustes y pruebas de una manera más fácil. Muchas veces también vemos que revisando código que han hecho otras personas hay partes del código que están comentadas esto es para hacerlo mas funcional, es decir, por poner un ejemplo, si utilizas el PIC 16F877 des comenta esta parte y si utilizas otro PIC lo dejas comentado, de esta manera comentando o descomentando unas cuantas líneas podemos utilizar el programa en varias situaciones.

Bueno, todo esto para el que tenga una idea de programación seguro que ya lo sabe, pero como dije al principio voy ha intentar que este curso le sirva también al que no tenga ni idea de programación aunque, en este caso, hay que decir también si se es honesto, que aprender un lenguaje de programación al igual que aprender un idioma nuevo supone un esfuerzo considerable y no vasta con leerse un libro de C y decir ¡ya soy un programador de C!, bajo mi modesta opinión lo que hay que hacer es practicar mucho, es decir teclear mucho código compilarlo y comprobar que funciona como nosotros queremos que lo haga, al principio cometeremos muchos errores pero el descubrir cual es la causa del error nos servirá para aprender mas todavía y sobre todo no desanimarse a la primera de cambio cuando algo no funcione. La constancia y la perseverancia son las claves del éxito para conseguir cualquier objetivo, no solo el aprender a programar PIC en C. Y ya está bien porque menudo rollo estoy soltando, así que vamos a empezar con lo que era el tema de este capitulo: las constantes.

Las constantes se refieren a valores fijos que no se pueden alterar por medio del programa.
Pueden definirse constantes de cualquiera de los tipos de datos simples que hemos visto.
Se declaran colocando el modificador const delante del tipo de datos.


Ejemplo:

const int MINIMO=10,INTERVALO=15;

Esto definirá dos constantes MINIMO con el valor de 10 e INTERVALO con el valor de 15.

Otra forma de definir constantes es usando la directiva de compilación #define.


Ejem.

#define MAXIMO 30

Esta orden se ejecuta de la siguiente forma: en la fase de compilación al ejecutar #define el compilador sustituye cada operación de la primera cadena de caracteres por la segunda, MAXIMO por el valor 30 además, no se permite asignar ningún valor a esa constante.
Es decir si pusiéramos:

#define MAXIMO = 30

Al compilar tendríamos un error.

Nota: La declaración #define no acaba en ";"

También podemos tener en nuestro programa Constantes de cadena: una cadena de texto es una secuencia de caracteres encerrados entre dobles comillas. Se usa para funciones de entrada y salida estándar, como función de entrada y salida de texto estamos utilizando la función printf que esta definida dentro de-> #use rs232, pero ya veremos que el compilador CCS proporciona un número considerable de funciones listas para usarse y que nos sirven para comunicarnos con el dispositivo de entrada y salida RS-232.

Hemos dicho que podemos definir constantes prácticamente de cualquier tipo de dato, pero CCS nos permite también representar esas constantes en diferentes sistemas de numeración como hexadecimal, binario, octal, decimal y además definir también constantes de caracteres especiales que el compilador utilizará para realizar acciones concretas. De los sistemas de numeración permitidos los que más se usan son los siguientes:


Decimal

Ejemplo: 222

Hexadecimal empiezan por 0x

Ejemplo: 0x2A

Binario empiezan por 0b

Ejemplo:

0b00001011


Este último formato es muy útil, por ejemplo el PIC dispone de unos registros que sirven para configurar los puertos del PIC como entradas de datos o salida de datos, por defecto vienen configurados como entradas y si quiero utilizar algún pin como salida porque quiero utilizarlo para encender un LED o lo que sea, tengo que poner a cero dicho registro. En el formato binario se ve fácilmente que valores se le va asignar al registro, teniendo en cuenta que los registros empiezan por 0.

Como siempre vamos hacer un ejemplo para ver si nuestro compilador se traga todo lo que he dicho:

Bien si todo va bien obtendremos la siguiente salida:

Comentario: Como dije en la introducción de este curso la finalidad es aprender a programar PIC en lenguaje C eso conlleva saber el lenguaje C, que seguiremos viendo en esta parte del curso, pero también el saber utilizar los recursos y funcionalidades que nos ofrecen los PIC como por ejemplo saber programar sus contadores, como enviar datos a un LCD, el utilizar los conversores A/D, etc. Para ello voy a iniciar próximamente un segundo artículo donde empezaremos a estudiar ejemplos prácticos de los PIC.

En la última práctica que hemos visto (el uso del TMR0 como contador) vimos que el entorno de Proteus nos proporciona una ventana de visualización del estado de los registro SFR de nuestro PIC , muy útil cuando estamos depurando nuestro programa, pero Proteus nos proporciona más ventanas para ver el estado de los registros de nuestro PIC que podemos acceder a ellas por medio del menú Debug --> PIC-CPU cuando estamos ejecutando nuestro programa en el modo de simulación paso a paso o cuando hemos pulsado el botón de pausa, una vista condensada de todas esas ventanas la tenemos en la figura de abajo:

Como vemos aparte de poder ver el estado de los registros SFR del PIC podemos ver el estado de la memoria EPROM del PIC, El contenido de la memoria de programa (donde se encuentra grabado de forma permanente nuestro programa ), el estado de PILA (útil cuando se trabaja con interrupciones y funciones), otra ventana nos muestra el estado de la memoria RAM reservada a los datos ó registros de propósito general (GPR) en formato hexadecimal y otra donde podemos ver el estado de las variables que tenemos activas en ese momento, recordar que si utilizamos variables locales por ejemplo dentro de una función, estás se destruirán al salir de la función. Pero todo esto como he dicho lo tenemos cuando estamos ejecutando nuestro programa en el modo paso a paso ó tenemos nuestro programa en pausa.

Si estamos en modo Run e intentamos acceder a estas ventanas vemos que están deshabilitadas:

¿Qué otro sistema tenemos para depurar nuestros programas? Pues bien una manera que siempre podemos utilizar es utilizar la función printf como herramienta de depuración, es decir, ponemos la función printf en determinadas partes del programa donde queramos saber el estado de una o varias variables y por medio de la terminal podemos saber el valor que van tomando, una vez comprobado que nuestro programa funciona como nosotros queremos borramos las funciones printf que hayamos introducido con propósitos de depuración.

Pero Proteus nos proporciona otro método para ver el estado de las variables cuando estamos ejecutando nuestro programa ya sea en modo Run ó en modo paso, es la ventana Watch Window y podemos acceder a ella por medio del menú Debug --> Watch Windows.

Vamos a ver cómo podemos utilizarla. Para ello compilaremos el siguiente ejemplo:

Es un programa que lo único que hace es incrementar la variable X de 0 a 10 y después hace lo mismo con la variable Y, pero es suficiente para ver cómo utilizar la ventana Watch Windows para ver el valor que van tomando las variables X e Y.

Primeramente compilamos el ejemplo y después dentro del IDE del compilador hacemos clic en el icono Symbol Map según se muestra en la figura de abajo:

Esto hará que nos aparezca el archivo Symbol Map en modo lectura, en este archivo podemos ver en qué posición de memoria se guardarán las diferentes variables que tengamos declaradas en nuestro programa, este archivo se actualizará en cada compilación que hagamos.

Como vemos en la figura de arriba las variable X e Y tienen asignadas las direcciones de memoria 0x011 y 0x012 en la memoria RAM de propósito general (GPR), que como ya sabemos es la que el programador dispone para almacenar los valores de sus variables.

Bien, una vez anotadas estas direcciones volvemos al entorno de Proteus y abrimos la ventana Watch Windows, dentro de ella hacemos clic con el botón derecho del ratón y seleccionamos Add Items (By Address)… , según se muestra en la figura de abajo:

Nos aparecerá una nueva ventana donde iremos añadiendo las variables con su dirección correspondiente:

Una vez añadidas las variables podemos ver el valor que van tomando mientras ejecutamos nuestro programa en la ventana Watch Windows, según se muestra en la figura de abajo:

Pero tenemos aún mas opcciones, por ejemplo podemos establecer condiciones para ello hacemos clic en la variable con el botón derecho y seleccionamos Watchpoint Condition…

Nos aparecerá la ventana que se muestra abajo:

Por ejemplo yo la he configurado para que cuando la variable X sea igual a cinco se pare la simulación, pero admite más condiciones solo hay que ponerse y experimentar con las diferentes opciones que tenemos, también decir que podemos hacer que la ventana Watch Windows nos muestre los registros SFR que nos interesan junto con las variables que nosotros hemos declarado, en fin muchas posibilidades de depuración. El conocer estas herramientas nos puede facilitar mucho el aprendizaje porque vemos la secuencia real que sigue nuestro programa, que algunas veces puede que no coincida con nuestra lógica de funcionamiento del programa.

Función printf()

Aunque no hemos visto el tema de las funciones todavía, pero ya que estamos utilizando esta función muy a menudo, vamos a ver alguna de las posibilidades que nos ofrece. El que tenga conocimientos del lenguaje C sabrá que para utilizar esta función que pertenece al estándar ANSI de C hay que incluir previamente el archivo de cabecera #include <stdio.h>, pero esto con el compilador PCW de CCS no funciona, en este compilador esta función está definida en la directiva:

#use RS232(BAUD=9600,BITS=8,PARITY=N,XMIT=PIN_B1,RCV=PIN_B2)

Esto quiere decir que cada vez que queramos utilizar la función printf tenemos que haber incluido previamente esta directiva, que posibilita la comunicación del PIC con otro dispositivo utilizando el protocolo de comunicación serie RS232, además de la función printf esta directiva permite el uso de otras funciones para la entrada y salida de datos serie como: getc, getchar, gets, puts y kbhit que iremos viendo poco a poco, pero la más importante para la salida de datos sin duda es printf, porque nos permite formatear la salida de esos datos de la forma que nosotros queramos.

Como vemos la directiva #use RS232 admite una serie de parámetros que son los que van entre paréntesis separados por comas, estos son los siguientes:

  • BAUD con este parámetro establecemos la velocidad en baudios a la que queremos que se transmitan los datos por el puerto serie, 9600 es lo normal.

  • BITS número de bits que utilizaremos en la transmisión, el estándar establece que pueden ser 8 ó 9, para la comunicación con microcontroladores con 8 son suficientes.

  • PARITY nos permite utilizar un bit de paridad para la comprobación de errores, está opción la dejamos a No.

  • XMIT está opción nos configura porque patilla del PIC saldrán los datos, está opción junto con la siguiente sí que la tendremos que cambiar a nuestras necesidades.

  • RCV nos configura porque patilla del PIC se recibirán los datos. En el ejemplo, los datos se transmiten por el PIN RB1 y se reciben por RB2.

La forma de hacer la llamada a la función printf es la siguiente:

printf(Nombre Función, Cadena de caracteres , valores);

Como vemos la función printf también admite parámetros que podremos utilizar para formatear el texto de salida. Vamos a ver cuáles son:

El primero es opcional y es el nombre de una función, si no lo ponemos los datos se transmitirán vía RS232 a través de los pines que hayamos configurado en la directiva #use RS232.

El segundo parámetro es una cadena de caracteres encerrada entre comillas dobles.

Y el tercero son datos o nombres de variables cuyo valor queremos que se muestren. Vamos a ver todo esto con ejemplos que es como mejor se ven las cosas:

1º Ejemplo:

Comentario:

En este primer ejemplo vamos a ver el uso de la función printf utilizando diferentes parámetros. Como vamos a utilizar la librería que incluye el compilador para el manejo de un LCD tenemos que incluir la directiva:

#include <LCD.C>

Declaramos una variable i1 de tipo entero que nos va a servir para mostrar su valor en la terminal y en un LCD.

Cuando utilicemos la librería LCD.C y antes de utilizar cualquier otra función incluida en la librería tenemos que llamar a la siguiente función que sirve para inicializar el LCD.

lcd_init();

En la primera llamada a la función printf como parámetros solo incluimos una cadena de caracteres constante que termina en (\r), esa barra invertida junto con la r se le llama secuencia de escape y le está diciendo al compilador que al final de la cadena introduzca un retorno de carro (tecla enter). Las secuencias de escape se utilizan para representar caracteres o acciones especiales.

printf("Esto es una cadena\r");

En la tabla de abajo se muestran las secuencias de escape que tenemos disponibles para utilizar con la función printf:

Vamos con la segunda llamada a la función:

printf("El valor de la variable i1 es: %d",i1);

En este caso tampoco está definido el primer parámetro, por tanto, al igual que en la primera llamada a la función, los datos se enviaran por el puerto serie al pin que hayamos definido en la directiva #use RS232, en esta llamada vemos que tenemos la cadena de caracteres limitada por las comillas dobles y separado por una coma, como tercer parámetro el nombre de la variable i1 que habíamos declarado previamente. En la cadena de caracteres vemos que aparece el carácter de % seguido de la letra d, ese es un carácter especial para la función y lo que le indica a la función es que en esa posición muestre el valor de la variable i1, la d le indica a la función que represente ese valor en formato de número entero. Podemos representar el valor de la variable en diferentes formatos según se muestra en la tabla de abajo:

Si quisiésemos mostrar el valor de más de una variable lo haríamos de la siguiente forma:

printf("El valor i1 es: %d el de i2: %d y el de i3: %d",i1,i2,i3);

Vamos con la última llamada a la función del 1º ejemplo:

printf (lcd_putc,"El valor de i1 es: %d",i1);

En esta llamada hemos incluido el primer parámetro y hemos puesto el nombre de la función lcd_putc, está función está definida en la librería LCD.C que trae el compilador para ayuda del manejo de los dispositivos LCD y que hemos incluido en nuestro programa por medio de la directiva #include <lcd.c>, vemos que la librería está encerrada entre los símbolos de <> esto le indica al compilador que busque la librería en el directorio en que se instalo el compilador, si copiáramos esa librería en otro directorio tendríamos que indicarle la ruta completa, pero esta vez encerrada entre comillas dobles.

Ejemplo:

#include “C:\Ejemplos de C\lcd.c”

Pues bien ahora la función printf no enviará los datos al puerto serie, sino a la función lcd_puct que será la encargada de enviárselos al LCD, esta función por defecto envía los datos al puerto D del PIC, pero accediendo a la librería se puede modificar el puerto fácilmente.

Aquí tenéis un video demostrativo del ejemplo:

2º Ejemplo:

Comentario del programa:

  • El especificador de formato %x indica al sistema que escriba en hexadecimal (base 16) el valor sustituido.

  • El ejemplo también escribe el carácter 'A', apoyándose en cuatro formas distintas de representaciones iníciales. En todos los casos se almacenará el mismo valor numérico, pero son diferentes las representaciones usadas.

  • El carácter (A) sale en la terminal en una línea diferente cada vez que se imprime, eso es debido a la secuencia de escape (\r) utilizada.

  • Observar que el ejemplo se ha hecho sobre el PIC 16f84 que no dispone de una USART hardware para la comunicación serie , pero sin embargo el programa se ha ejecutado correctamente, eso es debido a que la comunicación serie se ha establecido por software por medio de las librerías implementadas en el compilador PCW.

Salida del programa:

Vamos a continuar con las funciones disponibles en CCS para la entrada y salida de datos a través del puerto serie RS-232. Hasta ahora solo hemos visto que con la función printf(), podemos enviar datos formateados a través del pin que hayamos seleccionado en la directiva:

#use RS232(BAUD=9600,BITS=8,PARITY=N,XMIT=PIN_D1,RCV=PIN_D2)

En este caso los datos saldrán por el pin RD1 del PIC. Pero ¿de que funciones disponemos para recibir datos desde fuera hacia nuestro PIC?. El que haya programado en C echará de menos la función scanf() definida en la librería stdio.h y perteneciente al estándar ANSI C. Pero desgraciadamente esa función tampoco está disponible en CCS. Pero tampoco hay por qué preocuparse mucho, porque disponemos de otras. En este caso vamos a ver las funciones: getc(), getch() y getchar(). Las tres hacen lo mismo por lo que podemos usarlas indistintamente.


Estas funciones esperan un carácter por la patilla del PIC que hayamos definido en la directiva #use RS232 con el parámetro RCV. En el caso del ejemplo de arriba, los datos serán recibidos por el pin RD2 del PIC.


Pues vamos a ver nuestro primer ejemplo acerca del uso de estas funciones:

Comentario:

El ejemplo lo que hace es mostrar el valor de la tecla que pulsemos en el teclado y su equivalente en código ASCII


Vamos a explicar su funcionamiento paso a paso:

  • Primeramente, como siempre, incluimos por medio de la directiva #include el archivo de cabecera del PIC que vamos a utilizar, en este caso el PIC16F877.

  • Por medio de #use delay le decimos al compilador la frecuencia de reloj que vamos a utilizar en nuestro circuito.

  • Configuramos los parámetro de la directiva #use RS232, fijaros que XMIT=PIN_D0 y que RCV=PIN_D1. Con lo cual los datos saldrán del PIC por el pin RD0 y entrarán por el pin RD1.

  • Dentro de la función principal main(), escribimos lo que queremos que haga nuestro programa. Las instrucciones siempre empezarán a ejecutarse una a una a partir de esta función y de arriba hacia abajo.

  • Lo primero que hacemos es declarar una variable de tipo char donde almacenaremos el valor de la tecla que pulsemos en el teclado.

  • Después se nos mostrará un mensaje en la terminal invitándonos a que introduzcamos un carácter.

printf("Introduzca un caracter :\r");

  • Después se ejecutará la sentencia:

ch=getch()


Que esperará hasta que pulsemos una tecla y almacenará su valor en la variable ch.

  • La siguiente instrucción:

printf("El caracter %c tiene un valor ASCII decimal de %d.\r",ch,ch);


muestra el valor del carácter y su equivalente en código ASCII

  • Después se repite el proceso dos veces más, pero esta vez utilizando las funciones getc() y getchar()

Al utilizar solo la variable ch, el valor de la nueva tecla pulsada sobrescribirá el valor anterior de la variable.


La salida de nuestro programa será el siguiente:

Bien, hay que decir que el programa finalizará al llegar a la última sentencia incluida en la función main(). Para que el programa termine cuando nosotros queramos tenemos que incluir como mínimo un bucle y establecer una condición para que podamos salir de él, vamos a ver esto con otro ejemplo:

En este ejemplo se irán mostrando en la terminal las teclas que vayamos pulsando por el teclado hasta que pulsemos la tecla ‘n’ momento en el cual finalizará el programa.

Los códigos fuentes de los ejemplos los tenéis aquí.

Un par de funciones mas que se pueden utilizar en la entrada y salida de datos serie RS232 son las funciones gets() y puts().

gets(string): esta función lee los caracteres que se introducen por el teclado hasta que encuentra un retorno de carro (tecla Enter). El pin asignado para la lectura de los caracteres es el que hayamos configurado en RCV. En el ejemplo de abajo el pin RD5.

puts(string): esta función envía la cadena de texto contenida dentro de los paréntesis al pin que hayamos configurado en el parámetro XMIT de la directiva #use RS232, en el ejemplo de abajo el pin RD4. Una vez enviada la cadena añade un retorno de carro.

#use RS232(BAUD=9600, BITS=8, PARITY=N, XMIT=PIN_D4, RCV=PIN_D5)

Vamos a ver un ejemplo sencillo que utilice estas dos funciones:

Comentario

En este ejemplo se ha declarado un tipo de dato que todavía no hemos visto, un array de caracteres:

char nombre[9];

Aunque veremos los tipos de datos compuestos más adelante, podemos adelantar que un array es un conjunto de variable del mismo tipo de datos. Cada una de esas variables se coloca de forma consecutiva en la memoria RAM del PIC y se les llama elementos del array. Los elementos del array se enumeran empezando por el 0, (es una característica del lenguaje C). En el ejemplo de arriba se ha declarado un array de caracteres (tipo char) y el número máximo de elementos que podemos almacenar en el es de 9. Sus elementos estarán numerados del 0 al 8. Y podemos acceder a ellos de la siguiente forma:

valor = nombre[0];

…….. = ……..[.];

valor = nombre[8];

El ejemplo lo único que hace es enviar un mensaje a la terminal diciéndonos que introduzcamos nuestro nombre (puede ser también una password o lo que queramos). Cuando introduzcamos el nombre y pulsemos la tecla Enter, la cadena de caracteres será guardada en el array que hemos declarado previamente y luego con la primera función prinf() mostramos el valor de la cadena de texto guardada en el array, con la segunda función prinf() mostramos el tercer carácter del nombre introducido (nombre[2]). Este será la salida de nuestro programa:

Si intentamos introducir una cadena más larga, el valor se mostrará en la terminal pero truncado.

Por ejemplo si intentamos introducir la cadena:

“Microcontroladores PIC”

La terminal nos mostrará:

Microcont

Como he dicho antes los elementos del array se almacenan en posiciones consecutivas de la memoria RAM del PIC. Esto lo gestiona automáticamente el compilador, lo mismo que cuando haces un programa en C ó en otro lenguaje de alto nivel para un PC de escritorio el programador no está preocupado de en qué posición de la memoria RAM se almacenarán las variables que declara. Pero si a pesar de ello quieres saberlo haces lo siguiente:

Después de compilar el ejemplo, te vas al menú compile –> Symbol Map y nos aparecerá la ventana de abajo:

Donde vemos las posiciones de memoria donde se han mapeado nuestras variables. Como vemos nuestro array ha ocupado las posiciones de memoria de la 0x21 a la 0x29 de los registros GPR del PIC (en total 9 bytes), ya que los elementos que componen el Array son de tipo char que son de un byte (8 bits) cada uno.

Si queremos ver los valores que va tomando cada uno de los elementos del Array en tiempo de ejecución. Lo podemos hacer por medio de la ventana Watch Windows en Proteus. Si no te acuerdas de cómo se hace míralo aquí

Y obtendremos lo siguiente:

Consideraciones:

En C existe el concepto de memoria dinámica. La memoria dinámica es aquella que se puede reservar y liberar en tiempo de ejecución, es decir, durante la ejecución del programa se liberará y se asignará memoria para optimizar los recursos de la CPU, para ello se dispone de funciones como malloc() y free(). El compilador CCS también admite este tipo de funciones, para utilizarlas debemos de incluir el archivo de cabecera stdlibm.h, ya veremos un ejemplo sobre la asignación de memoria dinámica, si no utilizamos estas funciones la reserva de memoria es estática, es decir, si declaramos un array de nueve elementos el compilador le reservará memoria contigua a dicho array al compilar el programa en los registros de propósito general (GPR). Esa memoria se reserva cuando el PIC empieza a ejecutar su programa y permanece reservada hasta que desconectamos el PIC. Con todo esto quiero decir que tenemos que tener siempre claro de la memoria RAM de que disponemos, según el modelo de microcontrolador que utilicemos en nuestro proyecto, si no lo sabes consulta el data sheet del PIC que estés utilizando. Por ejemplo si en el PIC del ejemplo que hemos hecho (16f877) en vez de un array de 9 elementos declaramos uno de 100 elementos el compilador nos mostrará el siguiente error al compilar:

Y como ya he dicho en alguna ocasión, utiliza el tipo de datos más pequeño posible, en el ejemplo que he puesto he declarado un array de nueve elementos, para introducir el nombre “Antonio” que tiene seis caracteres, con lo cual estoy desperdiciando dos bytes de memoria RAM, eso en un ordenador de escritorio es insignificante pero en un microcontrolador si que es importante y puede que nos quedemos sin memoria suficiente para declarar todas las variables de nuestro programa.

El código del ejemplo lo tenéis aquí

Aunque existen algunas funciones más para la entrada y salida de datos serie, las que llevamos vistas hasta ahora son las más importantes, así que el próximo día empezaremos con los operadores, indispensables para hacer operaciones con los datos que introduzcamos en nuestro programa.

Operadores

El lenguaje C dispone de una gran cantidad de operadores que nos sirven para operar con los datos dentro de nuestros programas, se pueden clasificar en varios apartados: aritméticos, relacionales, de asignación, de manejo de un solo bit, etc. Pero lo importante no es saber a qué grupo pertenece cada operador, sino en conocer la operación que se puede realizar con cada uno de ellos. Vamos a ver los operadores que nos permite utilizar nuestro compilador CCS.

  • Operadores Aritméticos: permiten la realización de operaciones matemáticas en nuestros programas.

  • Operadores relacionales: compara dos operandos y devuelve 1 (verdadero) ó 0 (falso) según el resultado de la expresión. Se utilizan principalmente para elaborar condiciones en las sentencias condicionales e iterativas que se verán más adelante.

  • Operadores de asignación: permiten asignar valores a las variables. Tenemos las siguientes.

  • Operadores Lógicos: Al igual que los operadores relacionales, éstos devuelven 1 (verdadero), 0 (falso) tras la evaluación de sus operandos. La tabla siguiente ilustra estos operadores.

  • Operadores de manejo de bits: Estos operadores permiten actuar sobre los operandos para modificar un solo bit, los operandos sólo pueden ser de tipo entero (incluyendo el tipo char).

  • Operadores para manejar punteros: En el lenguaje C está muy difundido el uso de punteros, este compilador permite su uso y los operadores que utiliza para ello son los siguientes:

Los que se inician en el mundo de la programación suelen encontrar complicado el emplear punteros en sus programas pero, una vez que se entiende el concepto se simplifica y optimiza mucho nuestro código. Ya dijimos que el PIC dispone de unos registros de propósito general (GPR) que el programador utiliza para almacenar allí sus variables y poder utilizarlas a lo largo del programa, pues bien un puntero es otra variable a la cual se le asigna la dirección del registro ó memoria de otra variable.

La forma de utilizar los punteros lo veremos en profundidad más adelante, pero aquí tienes un pequeño ejemplo de cómo utilizarlos.

Ejemplo:

int y,z; //declaración de las variables x e y de tipo entero:

int *x; //declaración de la variable puntero x que guardará la dirección de memoria de una variable de tipo entero.

x=&y; // a través del operador de dirección (&) le asigno al puntero x la dirección de memoria donde está guardada la variable y.

z=*x; //a través del operador de inderección (*) le asignamos a z el valor de la variable cuya dirección está almacenada en la variable puntero x.

Nota: como vemos los símbolos de dirección (&) e inderección (*) son los mismos que el AND en el manejo de bits (&) y el operador aritmético de multiplicación, el compilador los diferencia según los operandos que le preceden.

  • Precedencia de los operadores:

Las operaciones con mayor precedencia se realizan antes que las de menor precedencia.
Si en una operación encontramos signos del mismo nivel de precedencia, dicha operación se realiza de izquierda a derecha.

Ejemplo:

a*b+c/d-e

Las operaciones se realizarán en el siguiente orden:

1. a*b resultado = x
2. c/d resultado = y
3. x+y resultado = z
4. z-e

Nota:
Es aconsejable el uso de paréntesis para evitar errores en la precedencia de operadores, además el código fuente queda más legible.

Ejemplo:

a*(b+c)+d

En este caso el orden en realizarse las operaciones será el siguiente:

1. b+c resultado = x

2. a*x resultado = y

3. y+d

sizeof(type)--> nos da el tamaño en bytes del tipo de dato ó variable que le pongamos entre los paréntesis.

Para conocer bien los resultados que se obtienen al utilizar cada uno de los operadores, lo mejor es practicar con ellos. Vamos a ver un ejemplo donde se muestra el uso de algunos de ellos:

Comentario del programa:

En este ejemplo introducimos unos valores por el teclado del ordenador y se los enviamos al pic via serie por el dispositivo RS-232, luego realizaremos diferentes operaciones con ellos y mostraremos el resultado en la Terminal, pero hay que tener en cuenta que que esos valores que introducimos por el teclado son caracteres y por tanto no se los podemos asignar directamente a una variable de tipo entero para operar con ellos, primero tenemos que convertirlos. CSS nos proporciona las siguientes funciones para ello:

- atoi(cadena) --> devuelve un valor entero de 8 bits de tamaño.

- atol(cadena) --> devuelve un valor entero de 16 bits

- atoi32(cadena) --> devuelve un valor entero de 32 bits

Para saber el rango de valores admisible por cada función repasa los tipos de datos

Estas funciones están definidas en el fichero de cabecera stdlib.h, por tanto no hay que olvidarse de incluirlo previamente por medio de la directiva: #include <stdlib.h>.

Fijaros en la instrucción de la línea 31 que nos da el tamaño de la variable y:

printf("El tama\xa4o de y es: %d bytes\r",sizeof(y));

\xa4 --> es la secuencia de escape para representar la letra ñ. Esto es porque el compilador no reconoce los caracteres en castellano.

Tabla equivalente de caracteres en castellano:

La salida del programa para los valores de x=50 e y=6 es la siguiente:

Una precaución que tenemos que tener en cuenta es que si utilizamos valores numéricos grandes y un PIC con poca memoria RAM, pronto la agotaremos. Esto solo es un ejemplo teórico del uso de operadores aritméticos, aplicaciones prácticas puede tener muchas, depende de lo que quieras hacer.

Sentencias repetitivas

Son aquellas que ejecutan un bloque de sentencias mientras se cumpla una expresión lógica. Este bloque de sentencias que se ejecuta repetidas veces, se denomina bucle, y cada ejecución se denomina iteración.

De las diferentes sentencias repetitivas que hay vamos a empezar con while.

La sentencia while permite la ejecución de un bloque de sentencias si se evalúa como verdadera una expresión lógica. La expresión lógica aparece al principio del bloque de sentencias.

En la figura de abajo se muestra el Pseudocódigo, el diagrama de flujo y la sintaxis de la sentencia while.

El Pseudocódigo es una forma informal de representar la secuencia del programa, sin tener en cuenta la sintaxis particular del lenguaje en que vayamos a programar y el diagrama de flujo es una representación gráfica del Pseudocódigo.

Cuando vayamos a crear un programa el dibujar previamente un diagrama de flujo ó el Pseudocódigo de la secuencia de nuestro programa puede ayudarnos en la tarea de programación, pero en ningún caso es un paso obligatorio.

El bloque delimitado por las llaves puede reducirse a una sentencia, y en este caso se suprimen las llaves.

La expresión lógica debe estar delimitada por paréntesis.

Cuando el programa llega a una sentencia while, sigue los siguientes pasos.

  • Evalúa la expresión.

  • Si es falsa, continua la ejecución tras el bloque de sentencias.

  • Si es verdadera entra en el bloque de sentencias asociado al while.

  • Ejecuta dicho bloque de sentencias, evaluando de nuevo la expresión y actuando en consecuencia.


Si la primera evaluación resulta falsa, el bloque de sentencias no se ejecuta nunca.

Si la expresión es siempre cierta el bucle es infinito.

Vamos con el primer ejemplo:

Comentario (1)

El ejemplo lo que hace es mostrar en la terminal la tabla de multiplicar del número cuatro, utilizando un bucle while. Para ello necesitamos declarar una variable auxiliar de tipo entero llamada i1, inicializada con el valor de 1, en cada iteración se comprueba el valor de la variable auxiliar, mientras el valor de i1 sea <=10 la evaluación será verdadera y se ejecutarán las instrucciones que hay dentro del bloque while, dentro de ese bloque tenemos que incrementar el valor de i1, de esta manera nos aseguramos en algún momento la salida del bucle, cuando i1 llegue a 11 la condición será falsa y la secuencia del programa saltará a la línea 19 finalizando el programa.

La salida del programa será el siguiente:

Si queremos que el programa este siempre ejecutándose (lo normal en un programa para Microcontroladores), hay que colocar un bucle infinito, mira el siguiente ejemplo:

Comentario (2)

true es una constante booleana que equivale a 1 ó verdadero. Por tanto la evaluación del bucle siempre será cierta y no habrá manera del salir de él. El programa estará siempre esperando a que pulses una tecla y mostrará el valor de la tecla pulsada en la terminal.

Bucle for()

En el ejemplo de la tabla de multiplicar utilizamos el bucle while para obtener los diez valores de la tabla, y si recordáis necesitábamos una variable de control que teníamos que inicializar antes de entrar en el bucle, comprobar el valor de la variable para la continuación en el bucle y la modificación posterior de la variable de control para poder salir del bucle en un momento determinado.

Pues bien, casi siempre que se hace algo, C proporciona frecuentemente un modo más compacto de hacer lo mismo.

El bucle for permite indicar estos tres elementos en un solo lugar, al principio del bucle, facilitando así la obtención de un código compacto, pero legible. Veamos cual es su sintaxis:

En un bucle for, el paréntesis que acompaña a la palabra reservada for generalmente contiene tres expresiones: Expresión 1; inicializa la variable ó variables de control del bucle. Expresión 2; representa la condición de continuación en el bucle. Expresión 3; modifica el valor de las variables de control en cada iteración del bucle.
Los puntos y comas que separan cada expresión son obligatorios.

Vamos a ver un ejemplo donde se muestran las diferentes posibilidades de que disponemos cuando utilicemos el bucle for:

Comentario

1º bucle for:

indice=1, inicializa a la variable de control del bucle. La segunda expresión, indice<=VAL_MAX, representa la condición de continuación. Por último, la tercera expresión, indice++, utiliza el operador de incremento para modificar a la variable de control en cada iteración del bucle. Los pasos que sigue la sentencia for son los siguientes:

1. indice es la variable de control. Se inicializa a 1

2. se testea la condición de expresión_2.

3. se ejecutan las sentencias

4. la variable de control indice se incrementa en uno

5. si se cumple que indice<=VAL_MAX va al paso 3. Si no va al paso 6.

6. Finalizará la ejecución cuando indice=20

2º bucle for

Bucle for con varias variables de control, las variables tienen que ir separadas por comas. En este caso tenemos las variables x e y, aunque podemos poner todas las que queramos, ambas variables son inicializadas dentro de la sentencia for.

3º bucle for

Cuando el bucle for se escribe sin cuerpo sirve por ejemplo para generar retardos, esta posibilidad la utilizaremos poco con este compilador ya que incluye funciones específicas de retardo.

4º bucle for

El bucle for permite no incluir las expresiones 1 y 3, aunque los puntos y comas son obligatorios ponerlos. En este caso se asemeja mucho a un bucle while.

5º bucle for

Se puede crear un bucle infinito por medio de la expresión for(;;).

Podemos salir de un bucle infinito por medio de la sentencia break, cuando se encuentra en cualquier lugar dentro del cuerpo de un bucle da lugar a la terminación inmediata de este, en el caso del ejemplo saldremos del bucle cuando pulsemos la letra ‘v’. Las sentencias de salto las veremos más adelante.

La salida del programa será la siguiente:

El código fuente lo tenéis aquí.

Como veis el lenguaje C posee mucha flexibilidad y se puede hacer una misma cosa de varias formas. Según el caso será más cómodo y legible utilizar una sentencia que otra.

Como ejercicio podéis hacer la tabla de multiplicar de un número utilizando el bucle for.

Bucle do-while()

A diferencia de los bucles for y while, que analizan la condición del bucle al principio del mismo, el bucle do-while analiza la condición al final del bucle. Esto significa que el bucle do-while siempre se ejecuta al menos una vez. La forma general del bucle do-while es la que se muestra en la figura de abajo:

Vamos a ver un ejemplo:

Comentario

Este ejemplo pide un valor entre 1 y 10, ejecutándose repetidas veces hasta que se introduce un valor entre ambos límites. Por último el programa visualiza el valor leído.

Este bucle se ejecutará como mínimo una vez porque el programa no sabe cuál es la condición de continuación hasta que se encuentra el while del final del cuerpo del bucle. Si la condición sigue siendo cierta (es decir, si el valor leído está fuera del intervalo deseado), el programa regresa al principio del bucle do-while y lo ejecuta de nuevo.

Fijaros en la orden if que aparece dentro del cuerpo del bucle. Esto se permite porque las estructuras de control se pueden anidar unas dentro de otras.

RECUERDA: En el bucle while la comprobación de la condición de control del bucle se encuentra al principio, por lo que dicho bucle puede no ejecutarse nunca; la comprobación en el bucle do-while está al final del bucle, por lo que al menos se ejecutará una vez.

El código fuente lo tenéis aquí.

If

Vamos a empezar las sentencias condicionales, con la más simple de todas, la sentencia if. Si se evalúa como cierta la expresión que hay entre paréntesis al principio de la sentencia if se ejecuta el bloque de sentencias contenido entre las llaves y si se evalúa como falsa la condición, el programa se salta ese bloque de instrucciones. En la figura de abajo tenéis la sintaxis de esta sentencia.

Si sólo hay una sentencia se pueden suprimir las llaves, ejemplo:

if (x=1)

printf(“Sin llaves solo una sentencia asociada al if”);

Como ejemplo de sentencias if tenéis el decodificador de binario a decimal.

Sentencia If…Else

Cuando el programa llega a una sentencia condicional del tipo If …Else, primero se evalúa una expresión; si se cumple (es cierta) se ejecuta un bloque de sentencias y si es falsa se ejecuta otro bloque.

En la figura de abajo se muestra la sintaxis de esta sentencia condicional.

Ejemplo:

Comentario

Este ejemplo visualiza en el display de cátodo común, conectado a la puerta B del PIC, el “0” si el interruptor conectado a RA0 está abierto y “1” si está cerrado, para ello utiliza la sentencia if-else, dentro de un bucle infinito para que el programa esté siempre chequeando el estado de la patilla RA0.

En este ejemplo hemos incluido una directiva nueva #use fast_io(puerto). Esta directiva se utiliza para optimizar el código generado por el compilador cuando se utilizan funciones de manejo de entrada y salida como “input(pin)” definidas ya en CCS.

Si no se incluye esta directiva el compilador tomará por defecto la directiva #use standard_io(A),que hará que cada vez que se utilicen estas funciones se reprograme el pin correspondiente como entrada ó salida, lo que hará que el código ASM generado tras la compilación sea mayor.


Podemos comprobar esto si después de compilar nuestro ejemplo, dentro del IDE de CCS seleccionamos Compile--> C/ASM List

Como se ve en la figura la memoria de programa (ROM) ocupa 21 palabras.

Ahora se pueden hacer las siguientes pruebas, la primera poner la directiva #use standard_io(A), y la segunda simplemente quitar la directiva #use fast_io(A) y no poner nada, según se muestra en la figura de abajo:

Volvemos a compilar y en ambos casos obtendremos lo siguiente:

En ambos casos la memoria ROM utilizada es de 24 palabras, tres más que cuando utilizábamos la directiva #use fast_io(A).

Otras funciones para el manejo de bits de los puertos de entrada y salida que vienen definidas en CCS y que dependen de la directiva #use*_io() son:

  • output_bit(Nombre_pin,valor) --> coloca el pin indicado a 0 ó 1.

  • output_high(Nombre_pin) --> coloca el pin indicado a 1.

  • output_low(Nombre_pin) --> coloca el pin indicado a 0

Fijaros que no he utilizado la directiva #use fast_io(B) para el puerto B, ya que no se utilizan funciones del compilador para el manejo de los bits de salida. En este caso el puerto B del PIC se controla mapeando la dirección de memoria del puerto B como una variable más en la RAM del PIC, por medio del identificador port_b.

Circuito del ejemplo:

El código del ejemplo y el circuito en Proteus lo tenéis aquí

Sentencia switch

La sentencia switch se compone de las siguientes palabras clave: switch, case, default y break.

Lo que hace está sentencia es comparar sucesivamente el valor de una expresión (dicha expresión tan solo puede ser de tipo entero o de tipo carácter) con una lista de constantes enteras o de caracteres. Cuando la expresión coincide con la constante, ejecuta las sentencias asociadas a ésta.

La estructura de la sentencia switch es la siguiente:

La sentencia break hace que el programa salte a la línea de código siguiente a la sentencia switch. Si se omite se ejecutará el resto de casos case hasta encontrar el próximo break.

La sentencia default se ejecuta cuando no ha habido ninguna coincidencia. La parte default es opcional y, si no aparece, no se lleva a cabo ninguna acción al fallar todas las pruebas y el programa seguirá a partir de la llave que cierra la sentencia switch

Consideraciones a la hora de usar esta sentencia:

  • En una sentencia switch No puede haber dos sentencias case con el mismo valor de constante.

  • Una constante char se convierte automáticamente a sus valores enteros.

  • Switch difiere del if en que switch solo puede comprobar la igualdad mientras que if puede evaluar expresiones relacionales o lógicas. Además cuando la comparación se basa en variables o se trabaja con expresiones que devuelven float deberemos usar el if-else.

  • Hay que decir que la secuencia de sentencias en un case no es un bloque (no tiene porque ir entre llaves). Por lo tanto no podríamos definir una variable local en él. Mientras que la estructura swith global sí que es un bloque.

Vamos a ver un ejemplo para ver todo esto:

Comentario

En el ejemplo introducimos un carácter numérico, lo almacenamos en el array llamado cadena1 y por medio de la función atoi() lo convertimos a un valor entero y guardamos su valor en la variable de tipo entero num, no hay que olvidarse de incluir el archivo de cabecera “stdlib.h” necesaria para la función atoi().

Ahora introducimos valores para ver que obtenemos a la salida.

Si introducimos un “1”, coincidirá con el valor de la constante asignada al primer case, por lo cual se ejecutan las dos primeras sentencias y el programa para de ejecutar sentencias porque se ha encontrado con un break, después ejecuta el último printf() por estar esta sentencia fuera de las llaves que delimitan a switch.

Fijaros en el segundo case, he omitido su break correspondiente a posta (el compilador no da error si se quita), para que veáis el resultado cuando se introduce un “2”:

Como veis en la figura de arriba se ejecutan las sentencias pertenecientes al segundo case, pero al no encontrar la sentencia break, ejecuta también la sentencia del tercer case. Esto hay que tenerlo en cuenta para tener claro que lo que hace salir de la sentencia switch es el break correspondiente a cada case.

Si introducimos por ejemplo un “9” al no a ver coincidencia con el valor de ningún case, se ejecutará la sentencia perteneciente a default.

El código fuente del ejemplo lo tenéis aquí.

Funciones

Las funciones son el pilar de los lenguajes estructurados como es el C, cualquier programa medianamente grande debe de estar formado por diferentes funciones, cada una de ellas hará una tarea determinada y serán llamadas desde la función principal (main) o desde otras funciones según vayan haciendo falta a lo largo de la ejecución del programa, por tanto es de vital importancia el conocer a fondo todas sus posibilidades, no solo para construir nuestras propias funciones, sino también para entender el código de programas y librerías hechos por otras personas.

Vamos ha empezar con algunas definiciones y ejemplos sencillos aplicados al compilador CCS.

1. INTRODUCCIÓN
  • Las funciones son los elementos principales de un programa en C.

  • Son bloques en los cuales se realiza una tarea específica.

  • Un programa en C está formado por la función main que es el bloque principal, por funciones propias del programador y por funciones de librerías propias del compilador.

  • Una característica importante de las funciones, es que pueden recibir parámetros y que pueden devolver un valor. La misma función nos puede servir para varios casos, con tan solo variar el valor de los parámetros. El compilador de CCS incluye muchas funciones "built-in" (listas para usarse) en sus librerías para el control directo de muchos de los recursos del PIC, para utilizarlas sólo necesitamos saber los parámetros que reciben y los valores que devuelven.

2. PARTES DE UNA FUNCIÓN

En una función hay que distinguir tres partes:

  • La declaración (también denominada prototipo).

  • La definición (o la propia función).

  • La llamada a la función.

El hecho de que nuestro programa defina una función, no quiere decir que esa función sea ejecutada. A menos que se produzca una llamada a la función, la función no será ejecutada, sino tan solo definida.

Por ejemplo, cuando nosotros incluimos la directiva #USE RS232 que es una directiva asociada a las bibliotecas precompiladas, incluimos los prototipos de muchas funciones, pero sólo se ejecutan aquellas funciones a las que llamamos, como printf. Vamos a ver con más detalle cada una de estas partes.

2.1 Declaración de una función
  • La declaración de una función se denomina prototipo de la función.

  • El prototipo aparece antes del bloque main, o normalmente en los archivos de cabecera (.h)

  • El prototipo de una función debe aparecer antes de su llamada, y le indica al compilador el número de parámetros que utiliza una función, y de que tipo son.

La sintaxis del prototipo es:

tipo nombre_función(parámetros);

Donde:

tipo-> es el tipo de dato que va a devolver la función. Si no se indica ningún tipo de dato, por defecto
se asume el tipo int. Si la función no va a devolver ningún dato hay que poner void.

nombre_funcion-> es el identificador de la función, con el que va a ser referenciada.


parámetros-> es la lista de parámetros (valores) que recibe la función; en la declaración también sería
correcto la siguiente expresión:

tipo nombre_función(tipo,tipo, tipo,....);

En el cual tenemos tantos tipos como parámetros vaya a aceptar la función.

Supongamos una función que define un parámetro de tipo float y que cuando es llamada en el programa, se le pasa un dato de tipo int. Como en el prototipo se ha indicado que el parámetro es de tipo float, el compilador convertirá primero el dato de tipo int a float, y después le pasará como parámetro a la función ese dato convertido.

La declaración de la función también controla que el número de argumentos usados en una llamada a una función coincida con el número de parámetros de la definición.

2.2 Definición de una función

La definición es la función en sí, el bloque de sentencias que va a componer esa función. La sintaxis
de la definición de una función es:

tipo nombre_función(parámetros)
{
declaración de datos de la función.
cuerpo de la función
}

La función va encabezada por el prototipo, pero esta vez sin finalizar en punto y coma. Después, se
incluye el bloque de sentencias de la función.

La lista de parámetros puede ser vacía, sin parámetros, si bien los paréntesis de la función deben
colocarse de igual forma.

Las definiciones de las funciones es aconsejable escribirlas a continuación de la función main()

Un ejemplo de la definición de una función sería:

float division (float x, float y)
{
float resultado;
resultado=x/y;
return(resultado)
}

Nota: Una función C, no puede contener en su interior otras funciones.

2.3 Llamada a una función

Para ejecutar una función hay que llamarla. La llamada a una función consta del nombre de la misma
y de una lista de argumentos o valores a pasar denominados parámetros actuales, separados por comas y encerrados entre paréntesis.

Cuando el programa llama a una función, la ejecución del programa se transfiere a dicha función. El
programa retorna a la sentencia posterior a la llamada cuando acaba esa función. La sintaxis de una
llamada a una función es:

nombre(parámetros);

donde:

nombre-> es el identificador con que es definida la función a la que queremos llamar.
parámetros-> es la lista de valores que se asignan a cada parámetro de la función (en caso de que
tenga), separados también por comas.

3. VALORES DEVUELTOS

Todas las funciones, excepto aquellas de tipo void, devuelven un valor. Este valor se especifica
explícitamente en la sentencia return y si no existe ésta, el valor es 0.

La forma general de return es:

return expresión;

Tres observaciones sobre la sentencia return:

A) La sentencia return tiene dos usos importantes. Primero, fuerza a una salida inmediata de la
función, esto es, no espera a que se llegue a la última sentencia de la función para acabar.
Segundo, se puede utilizar para devolver un valor.

B) return no es una función sino una palabra clave del C, por lo tanto no necesita paréntesis
como las funciones, aunque también es correcto: return(expresión); pero teniendo en cuenta
que los paréntesis forman parte de la expresión, no representa una llamada a una función.

C) En las funciones de tipo void se puede hacer:

return;

y de esta forma se provoca la salida inmediata de la función.

La función (como hemos dicho, en realidad es una sentencia de C) admite cualquier expresión
valida en C.

Ejemplo.

int multiplica (int x, int y)
{

return (x*y);

}

Si el tipo de datos de la expresión de la sentencia return no coincide con el tipo de datos que debe devolver la función automáticamente se convierte el tipo de datos para que haya coincidencia.

Una función puede contener varias sentencias return() en su código.

Ejemplo.

int compara (int x, int y)

{
if(x<y)
return (0);
else
return (1);
}

Todas las funciones pueden devolver variables de cualquiera de los tipos de datos válidos en C.

Si no se especifica el tipo, la función por defecto devuelve un entero.

Por ejemplo, las siguientes funciones son equivalentes:

int resta (int x, int y)

{
int z;
z = y-x;
return (z);

}

resta (int x, int y)
{
int z;
z = y-x;
return (z);
}


Una función que devuelva un tipo de datos válido en C se puede usar como operando en cualquier
expresión válida en C.

Ejemplo.

int resultado;
resultado = resta(4,6)*10;

Una función no puede usarse a la izquierda de una asignación.

Ejemplo.

resta(a,b)=12; //Esta asignación es incorrecta

El valor devuelto por una función en la sentencia return() puede no ser usado en una asignación,
ni en una expresión válida en C, con lo cual este valor devuelto se perderá.

En otras palabras, aunque todas las funciones, excepto las declaradas como void, devuelven valores,
no se tiene que usar necesariamente ese valor de vuelta para algo.
Una pregunta muy común acerca de los valores devueltos por una función es: ¿ya que se devuelve un
valor?, ¿no se tiene que asignar ese valor de vuelta a alguna variable? . La respuesta es no. Si no
hay una asignación especificada, el valor devuelto simplemente se ignora.

Vamos a ver algunos ejemplos sencillos de utilización de funciones:

Si simulamos el ejemplo con Proteus obtendremos obtendremos la siguiente salida en la terminal:

En el siguiente ejemplo se muestra diferentes formas de usar el valor devuelto por una función:

Continuamos con el tema de las funciones, en este caso vamos a ver a que nos referimos cuando hablamos sobre el ámbito de las funciones y también profundizaremos mas en el tema sobre el tipo de argumentos que pueden recibir las funciones, veremos que es eso de pasar un argumento por valor ó por referencia y que diferencia hay entre ambas formas, todo ello con ejemplos sencillos para entender el concepto y aplicados al compilador de CCS.

4. REGLAS DE ÁMBITO DE LAS FUNCIONES

Las reglas de ámbito de un lenguaje son las reglas que controlan si un fragmento de código conoce o tiene acceso a otro fragmento de código o de datos.

En C, cada función es un bloque de código discreto. El código de una función es privado a esa función, a menos que se haga a través de una llamada a esa función. (No es posible, por ejemplo, utilizar un goto para saltar en medio de otra función). El código que comprende el cuerpo de una función está oculto al resto del programa y, a no ser que se usen datos o variables globales, no puede ser afectado por otras partes del programa ni afectarlas. Dicho de otro modo, el código y los datos que están definidos dentro de una función no pueden interactuar con el código o los datos definidos dentro de otra función porque las dos funciones tienen un ámbito diferente.

Las variables que están definidas dentro de una función se llaman variables locales. Una variable local
comienza a existir cuando se entra en la función y se destruye al salir de ella. Así, las variables locales
no pueden conservar sus valores entre distintas llamadas a la función. La única excepción a esta regla
se da cuando la variable se declara con el especificador de clase de almacenamiento static. Esto hace que el compilador trate a la variable como si fuese una variable global en cuanto a almacenamiento se refiere, pero que siga limitando su ámbito al interior de la función.

En C todas las funciones están al mismo nivel de ámbito. Es decir no se puede definir una función dentro de otra función. Sin embargo si se puede llamar a funciones desde otras funciones.

Ahora, Vamos a profundizar un poco más sobre los valores (argumentos) que le puedo pasar a la función cuando la llamo.

5. ARGUMENTOS DE FUNCIONES

5.1 Funciones sin argumentos.

Cuando una función se declara con el tipo void como argumento, nos está indicando que esa función
no espera argumentos.

Ejemplo:

int multiplica(void); //esta función no espera ningún argumento y devuelve un número entero.

5.2 Funciones con argumentos.

  • Si una función va a usar argumentos, debe declarar variables que tomen los valores de esos argumentos. A estas variables que se declaran en la propia función se les suele llamar parámetros formales de la función. Se comportan como otras variables locales dentro de la función, creándose al entrar en la función y destruyéndose al salir.

  • Hay que asegurarse que el tipo de datos de los parámetros formales sea compatible con el tipo de datos usado para los argumentos de llamada a la función. Aunque el compilador no muestre ningún error por esto, los resultados obtenidos serían imprevisibles.

  • Se pueden pasar argumentos a las funciones de dos formas:

    • Por valor

    • Por referencia


Llamadas por valor.

Este método copia el valor de un argumento en el parámetro formal de la función. De esta forma, los cambios en los parámetros de la subrutina no afectan a las variables que se usan en la llamada.
Esto significa, en general que no se pueden alterar las variables usadas para llamar a la función.

Toda esta serie de nombres y conceptos, puede parecer un poco confuso al principio sobre todo para el que empieza por primera vez a estudiar un lenguaje de programación. Personalmente, he programado en varios lenguajes de programación y bajo mi punto de vista la única forma de entender y avanzar en el aprendizaje de un lenguaje de programación es: practicar, practicar,...., y practicar. Así que vamos ha empezar a ello, en el siguiente ejemplo vamos a ver como se le pasan los argumentos a una función por valor, los ejemplos de funciones que hemos visto anteriormente y que reciben parámetros utilizan este método, por lo que la sintaxis utilizada nos será familiar.

En el siguiente ejemplo se muestra lo que ocurre cuando no hay coincidencia en el número de parámetros declarados en el prototipo con el número de argumentos en la llamada a la función.

Si simulamos el ejemplo con proteus, obtendremos la siguiente salida:

Como vemos en la imagen lo que se pasa a la función es una copia del valor del argumento. Lo que ocurra dentro de la función no tiene efecto sobre la variable utilizada en la llamada.

En este ejemplo, se copia el valor del argumento de cuadrado(t), t=5 en el parámetro x. Cuando se realiza la asignación x=x*x, el único elemento que se modifica es la variable local x. La variable t, usada para llamar a cuadrado(t), todavía tiene el valor 5.

RECUERDA:
lo que se pasa a la función es una copia del valor del argumento. Lo que ocurra dentro de la función no tiene efecto sobre la variable utilizada en la llamada.

Bueno, ahora tocaría hacer un ejemplo del paso de argumentos a una función por referencia. Pero para comprender esto bien, antes debemos de tratar un tema muy importante en C que es el uso de punteros, por lo que el próximo tema tratará sobre ellos, aprenderemos a usarlos y las precauciones que tenemos que tener con ellos, veremos también algunas peculiaridades que tiene el compilador CCS en su uso.

Punteros

Empezamos hoy un tema muy importante en C como son los punteros, veremos que son, para que sirven y las precauciones que tenemos que tener en cuenta cuando los utilicemos.

¿Que es un puntero?

Un puntero es una variable más, pero que almacena la dirección de memoria de otra variable.

¿Para que sirven los punteros?

El conocer la dirección de memoria de una variable es muy útil en C, como funciones principales que tienen los punteros podemos citar las siguientes:

  • Pueden proporcionar una forma rápida de acceder o referenciar a tipos de dados compuestos como arrays, estructuras y enumeraciones.

  • Sirven para pasar variables por referencia a las funciones.

  • Según los casos pueden optimizar el código y ahorrar recursos de memoria.

¿De que operadores dispongo para manipular los punteros?

1. El primer operador de punteros es &, un operador monario que devuelve la dirección de memoria del operando. (Un operador monario es aquel que solo requiere un operando).

Por ejemplo:

m=&contador;

Coloca en m la dirección de memoria de la variable contador, o sea la dirección del registro del PIC donde se ha guardado la variable contador.
Para comprendedlo mejor, supongamos que la variable contador utiliza la posición de memoria 0x0C (primer registro de propósito general del banco 0 del PIC 16F84A) para guardar su valor, también supongamos que el valor de contador es 100. Después de la asignación, m tendrá el valor de 0x0C. Se puede pensar en & como "la dirección de". Por tanto, la sentencia anterior de asignación significa "m recibe la dirección de contador".

2. El segundo operador de punteros es * , que es el complementario de &. Es otro operador monario que devuelve el valor de la variable ubicada en la dirección que se especifica. Por ejemplo, si m contiene la dirección de memoria de la variable contador, entonces:

q=*m;

Colocará el valor de contador en q. Siguiendo con el ejemplo, q tendrá el valor 100, ya que 100 es lo guardado en el registro 0x0C, que es la dirección de memoria que indica m.
Piensa en * como "valor en la dirección". En este caso, la sentencia de asignación significa que recibe el valor en la dirección m.
Como vemos y eso puede despistar un poco, el símbolo de estos operadores coincide con los operadores (&) AND lógico y el operador matemático (*
) Multiplicación.
No hay ningún problema en esto porque el compilador se encarga de diferenciarlos según el contexto donde estén colocados

3. El tercer operador utilizado es: -> y se utiliza para acceder a tipo de datos compuestos en C como las estructuras, lo veremos más adelante cuando veamos este tipo de datos.

¿Como se declara una variable puntero?

Las variables que vayan a contener direcciones de memoria, o punteros, como se llaman en C, deben declararse colocando un * delante del nombre de la variable. Esto indica al compilador que va a contener un puntero a ese tipo de variable. Por ejemplo, para declarar c como puntero a carácter (char) escribiremos lo siguiente:

char *c;

Aquí, c no es un carácter, sino un puntero a un carácter, el tipo de dato al que apunta un puntero, en este caso char, se denomina tipo base del puntero. Bueno puede que alguien se pregunte lo siguiente ¿de que tipo es la propia variable puntero? Pues esta claro que tiene que ser de un tipo de datos cuyo tamaño sea suficiente para guardar una dirección de memoria tal y como esté definida por la arquitectura del microcontrolador que se utilice. Por ejemplo si utilizamos el PIC 16f84A, con un tipo de dato entero de 8 bits (int8) sería suficiente para direccionar todos los registros de este PIC, pero ese tipo de datos sería insuficiente para direccionar toda la memoria de otro PIC con mayor memoria RAM. Lo que hace CCS es establecer como tipo de dato por defecto para los punteros el entero de 16 bits (int16 en el tipo de dato nativo de CCS) para que sea compatible con todos los PICs que actualmente se pueden programar con CCS, pero da la opción de modificar ese parámetro a través de la directiva #device:

Esta directiva se encuentra incluida en el archivo de cabecera y en la siguiente instrucción se define para que el compilador le asigne un tipo de dato int8 (1 byte) al PIC 16f84A para las variables puntero.

#device PIC16f84A =8


Bueno, es una manera de ahorrar recursos cuando utilicemos punteros en PICs con poca memoria RAM. Pero en la mayoría de los casos la opción por defecto será la correcta y además de no tener que preocuparnos por ello, ganamos portabilidad en nuestro código, al saber que es compatible para todos los PICs.

Lo que si es responsabilidad del programador en cada momento es el tener en cuenta que un puntero sólo debe ser usado para apuntar a datos que sean del tipo base del puntero declarado (este tipo de cosas se verá mas adelante cuando veamos las precauciones que tenemos que tener en cuenta cuando usemos punteros).

Nota: se puede mezclar en una misma sentencia la declaración de variables puntero con variables normales, por ejemplo.

int x, *y, z; //declara x, z como variables de tipo entero, e y como puntero a un tipo entero.

Vamos a ver un ejemplo del uso de punteros:

La salida del programa así como el valor de los registros del PiC se pueden visualizar si simulamos nuestro ejemplo en Proteus.

Comentario

En este ejemplo se ha modificado el tipo de dato asignado por defecto por el compilador en la declaración de las variables puntero. Para ello como he dicho ya, hay que modificar la directiva #device en el archivo de cabecera según se muestra en la figura de abajo:

Podemos ver el tamaño que tiene la variable puntero por medio del operador sizeof(), si no modificáis el parámetro de la directiva #device veréis que el tamaño en ese caso es de 2 bytes.

Nota: cada vez que se modifique alguna librería perteneciente a CCS es conveniente hacer una copia de dicha librería en la carpeta de nuestro proyecto y modificarla allí, acordarse en este caso de incluir en el programa principal el archivo de cabecera entre comillas dobles y no entre los signos < >.

En la declaración de las variables el compilador reserva las posiciones de memoria RAM necesarias para poder contener los datos de las variables declaradas, como los registros de la RAM de propósito general del PIC 16FXXX son de un byte (8 bits) de tamaño, las posiciones de memoria reservadas por el compilador dependerán del tipo de dato con el que se ha declarado la variable, por ejemplo una variable declarada como tipo entero (int8) necesita un solo registro para almacenar su valor, una variable declarada como tipo float necesitará 4 registros (32 bits) para almacenar su valor.

En el siguiente paso se produce la asignación de datos a la variable fuente y a la variable puntero p, como veis en la figura de abajo, el dato almacenado en p es la dirección de memoria de la variable fuente, a partir de aquí se suele decir que p apunta a la variable fuente y que la variable puntero p queda vinculada a esa dirección de memoria.

Ahora hacemos una asignación indirecta de datos utilizando el operador de indirección (*). Como se ve en la figura de abajo asignamos a la variable destino el valor que tiene la variable fuente indirectamente a través del puntero p.

Sentencias de control break, continue y goto

Hoy continuamos con el curso hablando de las sentencias de control break, continue y goto.

La sentencia break: Esta sentencia tiene dos funciones, la primera es la que ya se ha mencionado en este curso que nos permite salir de un case en un bloque switch; la segunda, de la cual hablaremos hoy, es la de provocar la salida inmediata de cualquier ciclo que se esté ejecutando sin importar la condición de permanencia en el mismo.
Si tenemos dos o más ciclos iterativos anidados la sentencia break sólo provocará la salida del ciclo en el que se encuentre. Para ilustrar el uso de esta sentencia veamos el siguiente ejemplo:

Comentario:

Este programa hace que el PIC espere hasta que se pulse un botón conectado en la patilla RB0, cuando esto ocurre se enciende un led conectado a la patilla RB1 durante un segundo para luego apargarse y volver a esperar el botón sea pulsado nuevamente.

Nótese que, cuando ocurre el break se salta a la siguiente instrucción fuera del while más anidado o más "pequeño", es decir, salta a la instrucción: output_high(PIN_B1) . El break nos permite interrumpir un ciclo a la vez.

Aquí tienen una captura de la simulación en Proteus:

La sentencia continue: Esta otra sentencia provoca de forma forzada una nueva iteración del ciclo en ejecución saltando aquellas instrucciones que faltaban para el término normal de la iteración en curso. En el caso de un ciclo while o do-while se salta directamente a la verificación de la condición del ciclo mientras que en un ciclo for se salta a la parte de incremento del ciclo y luego a la verificación de la condición de ciclo.

Comentario:

Este programa se basa en un ciclo for de 5 iteraciones en la cuales se espera a que se presione una tecla numérica y la muestra en pantalla, si se presiona otra tecla que no sea numérica se muestra en el terminal el mensaje "No presionaste un numero" y se salta a la siguiente iteración. Luego de las cinco iteraciones, se termina el programa mostrando el mensaje "Fin de programa".

Lo ilustrativo de este ejemplo es que, aunque se presione otra tecla que no sea numérica (lo que provocará un salto a la siguiente iteración con la sentencia continue), la variable i se sigue incrementando.

Aquí tienen una captura de una corrida del programa:

Sentencia goto: Esta última sentencia de la que hablaremos hoy permite, en conjunto con una etiqueta, realizar un salto incondicional a cualquier parte del programa. La forma de hacerlo sería la siguiente:

Como verán, es muy parecido a cómo se hacen las cosas en assembler. En el ejemplo anterior simplemente se decrementa la variable i desde 5 hasta 0. Esto sería equivalente al siguiente ciclo for:

La sentencia goto es, generalmente, la opción menos recomendada a utilizar, esto se debe a que el lenguaje C es un lenguaje estructurado y el uso de esta sentencia puede, en algunas ocasiones, hacer que la estructura del programa sea "inentendible" pareciéndose así a un programa hecho en assembler. Además, muchos autores aseguran que con el uso de las sentencias break y continue se puede estruccturar cualquier programa para sin necesitar el uso de la función goto. No obstante, esta sentencia podría también hacernos la "vida" mucho más fácil a la hora de programar, un caso típico sería cuando queremos salir de varíos ciclos anidados de una vez cosa que, con la sentencia break, no sería tan sencillo. Para terminar de entender esto veamos el siguiente y último ejemplo del día de hoy:

Comentario

En este programa el PIC se encuentra "atrapado" en un cuarto while infinito anidado. Cuando la condición del if es cierta entonces se salta a la etiqueta "salida:" entrando así en un while que pondrá a parpadear a el led.

La simulación en Proteus de este ejemplo utiliza el mismo esquema que la simulación del ejemplo de la sentencia break. Los códigos fuentes y los esquemáticos de Proteus los puedes obtener aquí.

Nota

Lo referente a las sentencias de control break, continue y goto del tutorial fueron aportadas por el usuario Albert. Si quieres hacer tus propias aportaciones puedes hacerlo enviándome la solicitud a través del siguiente cuadro de texto.