Índice general 1. Introducción 1.1. Estructura de la memoria . . . . . . . . . . . . . . . . . . . . . . . . . 2. Los microcontroladores PIC 2.1. ¿Qué es un microcontrolador? . . . . . . 2.2. Aplicaciones . . . . . . . . . . . . . . . . 2.3. Historia de los microcontroladores PIC . 2.4. Las gamas de PIC . . . . . . . . . . . . . 2.5. Los PIC de gama alta . . . . . . . . . . . 2.6. El PIC18F4550 . . . . . . . . . . . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
3. Arquitectura PIC 3.1. Diseño del PIC . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2. CPU . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.3. ALU . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.4. Organización de la memoria . . . . . . . . . . . . . . . . . . . . 3.4.1. Memoria de programa . . . . . . . . . . . . . . . . . . . 3.4.2. Memoria de datos . . . . . . . . . . . . . . . . . . . . . 3.5. Juego de instrucciones . . . . . . . . . . . . . . . . . . . . . . . 3.6. Periféricos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.6.1. Puertos de Entrada/Salida . . . . . . . . . . . . . . . . 3.6.2. Universal Serial Bus . . . . . . . . . . . . . . . . . . . . 3.6.3. SPP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.6.4. MSSP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.6.5. EUSART . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.6.6. Memoria EEPROM . . . . . . . . . . . . . . . . . . . . . 3.6.7. Memoria Flash . . . . . . . . . . . . . . . . . . . . . . . 3.6.8. Timers . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.6.9. Capturador, comparador y modulador de ancho pulso 3.6.10. Conversor A/D . . . . . . . . . . . . . . . . . . . . . . . 1
. . . . . .
. . . . . . . . . . . . . . . . . .
. . . . . .
. . . . . . . . . . . . . . . . . .
. . . . . .
. . . . . . . . . . . . . . . . . .
5 7
. . . . . .
9 9 10 10 11 11 12
. . . . . . . . . . . . . . . . . .
13 13 14 14 15 15 17 21 30 30 32 32 33 33 35 39 44 46 47
Compilador de C para el microcontrolador Microchip PIC18F4550 3.6.11. Comparador Analógico . . . . . . 3.6.12. Módulo de Tensión de Referencia 3.6.13. Otros . . . . . . . . . . . . . . . . . 3.7. Características especiales de la CPU . . . 3.7.1. Interrupciones . . . . . . . . . . . . 3.7.2. Perro Guardián . . . . . . . . . . . 3.7.3. Modo de reposo . . . . . . . . . . . 3.7.4. ICSP . . . . . . . . . . . . . . . . . 3.8. Características eléctricas . . . . . . . . . . 4. GNU Compiler Collection 4.1. Historia de GCC . . . . 4.2. Características . . . . . 4.3. Estructura . . . . . . . 4.3.1. Front-end . . . 4.3.2. Middle-end . . 4.3.3. Back-end . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
5. Diseño de la solución 5.1. Registros . . . . . . . . . . . . . . . . . . 5.2. Memoria . . . . . . . . . . . . . . . . . . 5.3. Paso de argumentos a funciones . . . . 5.4. Retorno de valores de funciones . . . . . 5.5. Variables globales y estáticas . . . . . . 5.6. Flujo de ejecución (memoria de código) 5.7. Pila . . . . . . . . . . . . . . . . . . . . . 5.8. Biblioteca de funciones matemáticas . . 6. Implementación 6.1. Creación de patrones . . . . . . . . 6.1.1. define_insn . . . . . . . . . 6.1.2. Ejemplo de define_insn . . 6.1.3. Define_expand . . . . . . . 6.2. Asignación - los patrones mínimos. 6.3. Los patrones del back-end . . . . . 6.4. Especificación . . . . . . . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . .
. . . . . . . .
. . . . . . .
. . . . . . . . .
. . . . . .
. . . . . . . .
. . . . . . .
. . . . . . . . .
. . . . . .
. . . . . . . .
. . . . . . .
. . . . . . . . .
. . . . . .
. . . . . . . .
. . . . . . .
. . . . . . . . .
. . . . . .
. . . . . . . .
. . . . . . .
. . . . . . . . .
. . . . . .
. . . . . . . .
. . . . . . .
. . . . . . . . .
. . . . . .
. . . . . . . .
. . . . . . .
. . . . . . . . .
. . . . . .
. . . . . . . .
. . . . . . .
. . . . . . . . .
. . . . . .
. . . . . . . .
. . . . . . .
. . . . . . . . .
. . . . . .
. . . . . . . .
. . . . . . .
2 . . . . . . . . .
. . . . . .
. . . . . . . .
. . . . . . .
. . . . . . . . .
. . . . . .
. . . . . . . .
. . . . . . .
. . . . . . . . .
. . . . . .
. . . . . . . .
. . . . . . .
. . . . . . . . .
. . . . . .
. . . . . . . .
. . . . . . .
. . . . . . . . .
. . . . . .
. . . . . . . .
. . . . . . .
. . . . . . . . .
. . . . . .
. . . . . . . .
. . . . . . .
. . . . . . . . .
49 50 50 51 51 53 53 53 53
. . . . . .
56 56 57 58 58 59 59
. . . . . . . .
60 60 61 62 63 64 64 66 67
. . . . . . .
69 75 75 76 79 80 88 93
7. Análisis del código generado 105 7.1. Asignación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
Compilador de C para el microcontrolador Microchip PIC18F4550 7.2. Operaciones matemáticas simples . . . . . . 7.2.1. Suma . . . . . . . . . . . . . . . . . . . 7.2.2. Resta . . . . . . . . . . . . . . . . . . . 7.2.3. Negación . . . . . . . . . . . . . . . . . 7.2.4. Valor absoluto . . . . . . . . . . . . . . 7.3. Operaciones lógicas . . . . . . . . . . . . . . . 7.3.1. AND, OR y XOR . . . . . . . . . . . . 7.3.2. Negación Lógica o Complemento a 1 7.3.3. Desplazamientos . . . . . . . . . . . . 7.4. Conversión de tipos o casting . . . . . . . . . 7.4.1. Extensión de signo . . . . . . . . . . . 7.4.2. Extensión de ceros . . . . . . . . . . . 7.5. Operaciones matemáticas complejas . . . . . 7.5.1. Multiplicación . . . . . . . . . . . . . . 7.5.2. División . . . . . . . . . . . . . . . . . 7.6. Estructuras condicionales o de selección . . . 7.6.1. if . . . . . . . . . . . . . . . . . . . . . 7.6.2. switch . . . . . . . . . . . . . . . . . . 7.7. Estructuras iterativas o bucles . . . . . . . . . 7.7.1. while . . . . . . . . . . . . . . . . . . . 7.7.2. do-while . . . . . . . . . . . . . . . . . 7.7.3. for . . . . . . . . . . . . . . . . . . . . 7.8. Funciones . . . . . . . . . . . . . . . . . . . . 7.8.1. Genéricas . . . . . . . . . . . . . . . . 7.8.2. main . . . . . . . . . . . . . . . . . . . 7.9. Archivos . . . . . . . . . . . . . . . . . . . . . 7.9.1. Genéricos . . . . . . . . . . . . . . . . 7.9.2. Principal . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
3 . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
108 108 110 112 112 113 113 113 114 115 115 116 117 117 118 119 119 124 127 127 128 129 131 131 134 135 135 136
8. Optimizando código con GCC 141 8.1. Tipos de optimización . . . . . . . . . . . . . . . . . . . . . . . . . . . 141 9. Conclusiones
147
Bibliografía
149
A. PIC18-GCC 150 A.1. Compilando GCC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150 A.2. Utilidades . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151
Compilador de C para el microcontrolador Microchip PIC18F4550
4
A.3. Argumentos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152 A.4. Parámetros de GCC específicos para el PIC18 . . . . . . . . . . . . . . 153 A.5. Compilando con pic18-gcc . . . . . . . . . . . . . . . . . . . . . . . . . 153 B. GNU PIC Utilities B.1. Introducción . . . . . . . . . . . . . . . . . . B.2. Herramientas . . . . . . . . . . . . . . . . . B.2.1. gpasm . . . . . . . . . . . . . . . . . B.2.2. Código fuente admitido por gpasm B.2.3. gplink . . . . . . . . . . . . . . . . . B.2.4. gplib . . . . . . . . . . . . . . . . . . B.3. Compilando ensamblador . . . . . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
155 155 156 156 157 160 161 162
C. Programas de ejemplo
165
D. Manual de usuario D.1. GCC . . . . . . . . . . . . . . . D.1.1. Ensamblador en línea D.2. GNU Pic Utilities . . . . . . . D.2.1. gpasm . . . . . . . . . D.2.2. gplink . . . . . . . . . D.2.3. gplib . . . . . . . . . . D.2.4. Programador . . . . . D.2.5. Archivos auxiliares . .
170 170 171 172 173 173 173 174 175
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
Capítulo 1 Introducción La mayoría de los programadores de microcontroladores prefieren usar lenguaje ensamblador en lugar de un lenguaje de alto nivel. Sus argumentos son el desperdicio de memoria que introduce un compilador y la mayor optimización que llegan a conseguir escribiendo su propio código ensamblador personalizado, así como la elevada predecibilidad del tiempo de ejecución de los programas escritos en ensamblador. Pese a que puede demostrarse que es cierto en la mayoría de los casos, hay otros muchos factores que inclinan la balanza hacia un lenguaje más alejado de la arquitectura de la máquina, como es el lenguaje C. Para propósitos de experimentación o aprendizaje, la elección de un lenguaje u otro puede apoyarse únicamente en las preferencias personales del desarrollador. Sin embargo, en entornos de desarrollo de productos comerciales hay que considerar dos factores muy importantes: tiempo de desarrollo y coste del mismo. Reducir el coste es un objetivo común en todas las empresas y, en un principio, no tiene relación directa con el lenguaje de programación elegido. En cambio, el tiempo invertido en el desarrollo sí está ligado al lenguaje por el que se opte, especialmente cuando las opciones son lenguaje ensamblador o uno de alto nivel, en nuestro caso C. El desarrollo en lenguaje ensamblador siempre depende del dispositivo elegido para realizar el proyecto. En el caso de dispositivos integrados como los microcontroladores, cambiar de modelo puede implicar un comienzo desde cero, con la enorme pérdida de tiempo y dinero que esto supondría. En cambio, un código en lenguaje C es independiente de la arquitectura del procesador, por lo que sólo habría que preocuparse del control de los dispositivos periféricos. Desarrollando en un lenguaje de alto nivel, un cambio de modelo de microcontrolador a otro (de la misma familia y compatible en dispositivos) resultaría totalmente transparente, siempre suponiendo que el nuevo dispositivo disponga de suficiente memoria para el programa. Incluso sería posible cambiar de familia de dispositivos, por ejemplo 5
Compilador de C para el microcontrolador Microchip PIC18F4550
6
de PIC18 a PIC16, sin más cambios que adaptar el código específico que gestiona los dispositivos periféricos. Relacionado con el punto anterior está el hecho de que el código escrito en un lenguaje de alto nivel como C es fácilmente reutilizable. Por consiguiente, si disponemos de un algoritmo ya implementado, probado, depurado y, por supuesto, comentado, podremos utilizarlo en otros proyectos sin necesidad de invertir grandes cantidades de tiempo en reescribirlo. Si el mismo algoritmo estuviese escrito en código ensamblador para un dispositivo concreto, su reutilización sería mucho más cara, ya que portarlo a otra arquitectura supone, prácticamente, reescribirlo desde cero. Además de las ventajas de portabilidad y reutilización de código, los lenguajes de alto nivel facilitan la depuración de errores. Debido a la dificultad de analizar y verificar un programa en ensamblador, la fase depuración resulta larga y tediosa, lo que lleva a un tiempo de desarrollo en ocasiones excesivo y a que los clientes con menos paciencia opten por cancelar el proyecto. En cambio, los lenguajes de alto nivel permiten técnicas de desarrollo y depuración más eficientes, e incluso es posible emplear técnicas formales de verificación. Por ello el tiempo necesario para esta fase del desarrollo es menor que para el mismo proyecto escrito en ensamblador. Todos estos son motivos de peso que justifican el esfuerzo invertido en este proyecto, cuyo objetivo es disponer de un compilador de lenguaje C completo que permita mejorar la calidad de los programas escritos para el dispositivo escogido, el microcontrolador PIC18F4550 de Microchip, así como reducir el tiempo necesario para desarrollar proyectos de diversa índole que empleen este microcontrolador. Los motivos de escoger el compilador de GNU, GCC, como punto de partida han sido su buen diseño, que después de 25 años de historia sigue siendo una pieza de ingeniería del software digna de admiración; su desarrollo activo por parte de una gran comunidad de desarrolladores experimentados; su amplia difusión y aceptación por usuarios, profesionales y empresas de todo el mundo; su naturaleza de código abierto y, por supuesto, la licencia libre bajo la cual se distribuye, la General Public License (Licencia Pública General) de GNU, que permite en última instancia que este proyecto final de carrera sea posible. El objetivo del proyecto ha sido desarrollar el backend de GCC para el microcontrolador PIC18F4550. Aunque, por tratarse de un proyecto de la Ingeniería Técnica acotamos la dificultad del proyecto en un principio, excluyendo funciones, punteros y matrices de este proyecto, finalmente hemos implementado también funciones, punteros y matrices, dando soporte completo al frontend de C para el PIC18F4550. Para ello, y aunque en principio el plan era ampliar el backend para microcontroladores PIC16 desarrollado en [8], hemos terminado reescribiendo desde cero un
Compilador de C para el microcontrolador Microchip PIC18F4550
7
backend propio; ya que, aunque las diferencias en el código en ensamblador son pequeñas, las diferencias en el manejo de la memoria lo han justificado, y además nos ha permitido utilizar las instrucciones específicas del 18F4550, que suponen una mejora de rendimiento del código resultante muy importante. Hemos realizado el proyecto para la plataforma Linux, aunque por su naturaleza y por el grado de integración con la GCC toolchain alcanzado puede ser recompilado para cualquier plataforma que queramos emplear y que permita ejecutar la GCC toolchain. Finalmente, hemos hecho un sobreesfuerzo para cumplir las líneas de desarrollo de código de GCC, y no afectar código de GCC fuera del que se espera que sea modificado en el desarrollo de un porting limpio, con objeto de su futura integración en la línea principal de desarrollo de GCC.
1.1.
Estructura de la memoria
La estructura de la memoria es la siguiente: En el primer capítulo presentamos el trabajo desarrollado y sus partes. En el segundo capítulo introducimos los microcontroladores y la arquitectura PIC. En el tercer capítulo analizamos la arquitectura del compilador GCC. En el cuarto capítulo planteamos el diseño de nuestra solución, así como las opciones que hemos tomado y justificamos dichas opciones. En el quinto capítulo estudiamos con más detalle aspectos destacables de las decisiones que hemos tomado al implementar el backend, incluyendo análisis de las optimizaciones de código soportadas. En el sexto capítulo comentamos las conclusiones a las que hemos llegado en nuestro trabajo. Después planteamos cuatro apéndices, que incluyen: En el primer apéndice, la mecánica (que no es trivial) para incorporar nuestro código en GCC y compilarlo. En el segundo apéndice describimos las utilidades de GNU para microcontroladores PIC, imprescindibles para utilizar nuestro proyecto: GPUTILS.
Compilador de C para el microcontrolador Microchip PIC18F4550
8
En el tercer apéndice incluimos algunos programas de ejemplo. Finalmente, en el cuarto y último apéndice, incluimos el manual de usuario del programa desarrollado.
Capítulo 2 Los microcontroladores PIC 2.1.
¿Qué es un microcontrolador?
Un microcontrolador es un dispositivo integrado que proporciona las funcionalidades de un pequeño ordenador. Los microcontroladores están compuestos por un procesador, memoria y varios periféricos. Las grandes ventajas de los microcontroladores son su reducido tamaño, bajo coste y gran variedad. Los dispositivos que proporcionan datos de entrada al microcontrolador o reciben información de salida desde el mismo pueden ser muy diversos, desde líneas digitales sencillas que sólo trabajan con dos valores (cero o uno) hasta puertos complejos, como los que se emplean en ordenadores, que permiten la comunicación del microcontrolador con otros dispositivos externos, tales como otros microcontroladores o un ordenador personal. Existen numerosos fabricantes que ofrecen una gran variedad de modelos diferentes. Cada modelo tiene unas características: tamaño, cantidad de memoria, potencia de cálculo, periféricos, consumo de energía, resistencia al calor y resistencia a la humedad. El objetivo de esta diversidad es la reducción de costes, ya que no es razonable exigir un dispositivo potente y completo que se adecúe a cualquier proyecto y que además sea barato. Cuantas más características tenga un dispositivo, mayor será su coste de producción; y, por consiguiente, mayor precio de adquisición. Por ello, la clave reside en hacer diseños sencillos y con unas características limitadas. Dada la variedad de microcontroladores existente, no nos resultará difícil encontrar un modelo que se ajuste a los requisitos de nuestro proyecto y al mismo tiempo resulte económico.
9
Compilador de C para el microcontrolador Microchip PIC18F4550
2.2.
10
Aplicaciones
La gran diversidad de modelos disponibles en el mercado nos permite afrontar infinidad de diseños diferentes, por simples o complejos que estos sean. Algunos ejemplos de uso real de microcontroladores son teléfonos móviles, controles de acceso a edificios, mandos a distancia, sistemas de freno ABS, micro-robótica, redes de sensores o sistemas de control de riego. A la hora de realizar un proyecto hardware la dificultad no reside en el uso de un microcontrolador concreto, sino en la elección del fabricante y el modelo adecuado para nuestro proyecto. Aunque el uso de microcontroladores nos facilitará la tarea en muchos aspectos, debemos tener siempre presente el objetivo de minimizar los costes. En ocasiones encontraremos aplicaciones en las que no será factible emplear un único microcontrolador o en las que un diseño distribuído con varios microcontroladores resultará más eficiente y económico. Por este motivo, resulta conveniente realizar un análisis de costes antes de comenzar el desarrollo.
2.3.
Historia de los microcontroladores PIC
En el mercado de microcontroladores, tres empresas son las más conocidas: Atmel, Motorola y Microchip. Existen muchas otras, como Intel, Texas Instruments o Renesas Technology. Microchip es el fabricante de los microcontroladores PIC. Todos ellos son heredados del PIC1650, que fue desarrollado originalmente por General Instruments. El nombre PIC es la abreviatura de PICmicro, aunque por lo general se considera acrónimo de Peripheral Interface Controller (Controlador de Interfaz Periférico). En un principio, el PIC se diseñó para combinarse con el procesador de 16 bits CP16000 -de ahí que el nombre original fuese Programmable Interface Controller (Controlador de Interface Programable)-. El CP16000, a pesar de ser un buen microprocesador, carecía de una buena E/S y por ello, en 1975, surgió el PIC de 8 bits, pensado para mejorar el rendimiento del sistema conjunto mediante la descarga de operaciones de E/S del CP16000. El PIC original empleaba un microcódigo simple almacenado en ROM para ejecutar su tarea y, a pesar de que el término no existía en aquella época, poseía características típicas de los diseños RISC. Cuando la división de microelectrónica de General Instruments se separó del resto de la empresa, en 1985, el nuevo propietario canceló casi todos los desarrollos, que para esa época estaban obsoletos. Sin embargo, el PIC se mejoró con EPROM, convirtiéndose en un controlador programable de E/S. A día de hoy existen multitud de modelos de PIC que incorporan varios puertos
Compilador de C para el microcontrolador Microchip PIC18F4550
11
de comunicación con el exterior (puertos serie, controladores de motores o conversores analógico-a-digital) y con memorias de programa de hasta 32.000 palabras.
2.4.
Las gamas de PIC
Los modelos de PIC constituyen gamas distintas, en función del tamaño de instrucción que emplean. Actualmente, Microchip comercializa sus microcontroladores clasificados en cuatro gamas: La gama baja la componen la serie PIC10 y una parte de las series PIC12 y PIC16. Utilizan palabras de instrucción de 12 bits, su tamaño es reducido, así como sus características, y su coste es muy bajo. La gama media está compuesta por casi toda la serie PIC16 y una porción de los PIC12. Utilizan un ancho de palabra de instrucción de 14 bits. Ésta es la gama más popular por su buena relación calidad/precio. Además, programarlos en lenguaje ensamblador resulta bastante sencillo, dentro de la complejidad del lenguaje, y es por ello que son una buena opción de cara al aprendizaje. La gama alta o de alto rendimiento la forma la serie de microcontroladores PIC18. Emplean palabras de instrucción de 16 bits y están basados en los PIC de gama media pero con mejoras sustanciales: más puertos de E/S, más conversores A/D o interfaces USB. La gama de 24 bits la componen de las series dsPIC30 y PIC24. Utilizan 24 bits como palabra de instrucción, usan palabras de memoria de datos de 16 bits (y no 8 bits), y son los que ofrecen más memoria y mayor rendimiento.
2.5.
Los PIC de gama alta
La gama alta ofrece microcontroladores adecuados para proyectos relativamente complejos, tales como adquisición de datos, redes de sensores distribuidos e incluso algunas aplicaciones de tiempo real. Además, incorpora varias características que, combinadas con un buen compilador, hacen de esta gama una opción muy interesante para ejecutar programas estructurados y modulares, escritos en lenguajes de alto nivel y haciendo uso de técnicas más avanzadas de ingeniería del software, con el ahorro de tiempo y dinero que esto supone. En el capítulo 3 estudiaremos la arquitectura de los PIC de gama alta.
Compilador de C para el microcontrolador Microchip PIC18F4550
2.6.
12
El PIC18F4550
De entre todos los modelos que componen la gama alta de PIC, el elegido para realizar el port de GCC es el 18F4550. Implementa ocho bancos de memoria de datos, 2Kbytes en total, y 32Kbytes de memoria de programa. Además incorpora 256 bytes de EEPROM para uso general. Cuenta con cuatro temporizadores, dos unidades de comparación, captura y modulación de ancho de pulso, un conversor A/D de 10 bits con trece entradas. Además, cuenta con puertos SPI, I 2 C, USART, SPP, USB. Junto con estos dispositivos, tiene -multiplexados- un total de 5 puertos de E/S. Todas estas características hacen del PIC18F4550 un gran microcontrolador con el que se pueden llevar a cabo infinidad de proyectos de diversa índole.
Capítulo 3 Arquitectura PIC 3.1.
Diseño del PIC
Los microcontroladores PIC emplean la arquitectura Harvard, en la que la memoria de programa y la memoria de datos se encuentran separadas, con sus propios buses de direcciones y datos. Una ventaja de esta arquitectura frente a la Von Neumann es que permite el acceso simultáneo a ambas memorias, con lo que es posible solapar las etapas de búsqueda de la siguiente instrucción mientras la instrucción actual accede al espacio de memoria de datos. El principal inconveniente de esta arquitctura surge con el uso de memoria caché, dado que cada espacio de memoria necesitará su propia caché independiente, y el rendimiento máximo sólo se alcanzará con programas que hagan un número similar de accesos a memorias de programa y datos. En caso contrario, el espacio menos usado tendrá parte de su caché sin utilizar, mientras que la caché del otro se encontrará siempre llena, con las posibles penalizaciones ocasionadas por los fallos de caché que esto conlleva. En cualquier caso, los PIC no incorporan memoria caché, así que este problema no les afecta y sólo obtienen los beneficios de la arquitectura Harvard. Otra mejora de la arquitectura Harvard es que el tamaño de palabra de cada banco de memoria puede ser diferente. En los microcontroladores PIC el banco de memoria de datos emplea un tamaño de palabra típico, 8 bits, mientras que las palabras de la memoria de programa son de distinto tamaño en función de la gama: la gama alta emplea palabras de 16 bits para este espacio de memoria. De este modo, es posible incluir en una sola palabra todos los datos para la ejecución de la instrucción, mejorando la eficiencia ya que se necesita un único acceso a la memoria de programa para cada instrucción. Entre las decisiones de diseño que han permitido a los PIC lograr su alto rendi-
13
Compilador de C para el microcontrolador Microchip PIC18F4550
14
miento sin que esto penalice en su precio se encuentra el pipeline segmentado en dos etapas: búsqueda de instrucción y ejecución. Ambas etapas se ejecutan en un único ciclo de reloj. Gracias a la división de memoria, estas dos etapas se superponen en el tiempo sin ninguna restricción, con lo que se consigue completar la ejecución de una instrucción con cada ciclo de reloj en la mayoría de los casos. Las únicas instrucciones donde no es posible este solape son las que modifican el contador de programa (en inglés, program counter o PC), ya que la búsqueda de la próxima instrucción se efectúa en paralelo a la ejecución de la actual y, hasta que no termine la ejecución de ésta y se cargue el PC, no será posible hacer la búsqueda de la siguiente instrucción. Así, todas las instrucciones de salto emplearán dos ciclos de reloj para ejecutarse. El conjunto de instrucciones es reducido gracias a la ortogonalidad de las mismas, que permiten mover contenidos desde y hacia cualquier registro. La curva de aprendizaje del ensamblador del PIC es, por consiguiente, reducida. Esto, unido a que todos los registros con funciones especiales se encuentran mapeados en memoria (incluído el PC) nos proporciona la potencia y flexibilidad necesarias para manejar, de manera sencilla y elegante, todas las funciones del microcontrolador.
3.2.
CPU
La Unidad Central de Proceso -en adelante, CPU- es el cerebro del microcontrolador. Su cometido es ejecutar la instrucción apuntada por el PC en la memoria de programa mediante la generación de la señales de control apropiadas sobre los buses, registros internos y unidad aritmético-lógica (ALU).
3.3.
ALU
Los microcontroladores PIC incorporan una ALU de 8 bits. Además tienen un registro de trabajo, también de 8 bits, llamado WREG (Working Register). La ALU efectúa operaciones aritméticas y lógicas de propósito general entre el registro WREG y cualquier otro registro de la memoria de datos, o entre WREG y un valor inmediato, tomado de la palabra de la instrucción en curso. La salida de la ALU puede almacenarse en el registro WREG o en el registro de la memoria de datos empleado como parámetro. En función de la instrucción ejecutada, la ALU actualizará los bits apropiados del registro de estado (STATUS), indicando acarreo, acarreo de dígito, desbordamiento, resultado negativo o cero. Con las operaciones que soporta la ALU y la información que devuelve es posible implementar cualquier operación matemática, como se verá más adelante en este
Compilador de C para el microcontrolador Microchip PIC18F4550
15
documento. La gama alta de PIC incorpora, además de la ALU estándar, un multiplicador hardware de valores de 8 bits que permite efectuar multiplicaciones en un sólo ciclo de reloj y facilita el desarrollo de rutinas de multiplicación para datos más grandes, así como de otras operaciones matemáticas avanzadas.
3.4.
Organización de la memoria
Como ya hemos visto, la memoria está dividida en dos espacios, uno para datos y otro para programa. La memoria de datos, a su vez, se encuentra organizada en registros que podemos clasificar en registros de propósito general (GPR) y registros de función especial (SFR), que controlan las funciones del núcleo del microcontrolador. Más adelante estudiaremos los registros especiales para controlar los periféricos.
3.4.1.
Memoria de programa
La memoria de programa emplea palabras de 16 bits con una alineación a nivel de byte. Está conectada a un bus también de 16 bits. Dado que una instrucción ocupa una palabra de la memoria de programa, podemos almacenar tantas instrucciones como palabras de memoria tenga nuestro microcontrolador. Para direccionar este banco de memoria se usa el contador de programa (PC), que direcciona bytes, no palabras. Este registro tiene un ancho de 21 bits, lo que permite direccionar hasta 2Mbytes de memoria. Ante la posibilidad de que el PC quede desalineado respecto a las palabras, su bit menos significativo es siempre 0 y las instrucciones de ejecución secuencial incrementan el contador de programa en dos unidades. De este modo, el PC siempre apuntará a una palabra de 16 bits alineada a nivel de palabra. Pese a que siempre podemos direccionar el máximo de memoria, los microcontroladores incorporan una cantidad de memoria inferior a la máxima que soportan. Todos los accesos de lectura a posiciones de memoria por encima del límite de memoria incorporada devolverán el valor cero. PC El contador de programa apunta a la dirección de la memoria de programa que será accedida en el próximo acceso. Como todos los registros de función especial, está mapeado en la memoria de datos y ésta emplea palabras de 8 bits. Dado que el PC tiene un tamaño de 21 bits, está dividido en tres registros mapeados en la memoria de datos: PCL, PCH y PCU. PCL corresponde al byte bajo del PC (PC[7:0]),
Compilador de C para el microcontrolador Microchip PIC18F4550
16
PCH al byte alto (PC[15:8]) y PCU al byte superior, que almacena los 5 bits más significativos del PC (PC[20:16]). El registro PCL permite lectura y escritura de manera directa. En cambio los otros dos registros, PCH y PCU, no son directamente accesibles por el programador. Para modificar estos registros hacemos uso de otros dos registros especiales, que sí permiten lectura y escritura: PCLATH y PCLATU. Estos registros se comportan como registros temporales que almacenan los valores que cargarán PCH y PCU cuando se realice una escritura en PCL. Por tanto, una modificación del PC requiere tres operaciones, no necesariamente unidas, que tendrán efecto tras la carga de PCL. De manera análoga, una lectura de PCL produce que se carguen en PCLATH y PCLATU los valores de PCH y PCU respectivamente. Instrucciones que modifican el PC Además del procedimiento anterior, el ensamblador de PIC proporciona varias instrucciones que modifican el PC: GOTO, CALL, RCALL y RETURN. Todas ellas modifican el PC directamente, sin tocar los registros PCLATH y PCLATU. La instrucción GOTO efectúa un salto incondicional en el flujo de ejecución del programa a cualquier dirección del rango de 2Mbytes que permite el PIC18. Es fácil deducir que se trata de una instrucción codificada mediante dos palabras de memoria, ya que, de lo contrario, el rango de salto sería menor. El comportamiento de la instrucción CALL es similar al de GOTO. La diferencia entre ellas es que CALL almacena en la pila interna la dirección de la instrucción que se encuentra a continuación de ella (PC+4). Podemos considerar la instrucción RCALL como una versión recortada de la anterior. Produce saltos en el programa de hasta 1K hacia delante o hacia atrás, respecto al valor, incrementado en dos, que almacena el PC en el momento de ejecutarse. La cuarta instrucción en realidad es un conjunto de tres instrucciones (RETURN, RETLW y RETFIE) que, grosso modo, cumplen el mismo cometido. Cuando el microcontrolador ejecuta una de estas instrucciones, el PC carga el valor que se encuentra almacenado en la cima de la pila. Pila Los PIC18 implementan una pila de direcciones de 31 niveles en una memoria aparte de los espacios de programa y datos. Es posible leer y modificar el puntero a la cima de la pila (STKPTR), así como la dirección almacenada en la cima, mediante los registros de función especial de cima de pila. Hay que tener en cuenta que tiene un comportamiento circular, de modo que tras efectuar 31 acciones seguidas
Compilador de C para el microcontrolador Microchip PIC18F4550
17
de apilamiento el puntero a la cima habrá dado la vuelta y quedará apuntando a la primera posición, lo que podría no ser el comportamiento deseado por el programado. Por ello es necesario tener mucha precaución de no sobrepasar los 31 niveles de anidamiento de subrutinas. Vectores Para terminar con la descripción de la memoria de código, veremos dos posiciones importantes para la programación de los dispositivos. En el momento de inicializarse el microcontrolador, la ejecución del programa comienza en la posición de memoria de programa 0x000000h, llamada vector de reset. Es desde esta posición donde debemos insertar un salto al inicio real de nuestro código (el inicio deseado), a través de un salto con GOTO u otro de los métodos alternativos. Existen otros dos vectores ubicados en 0x000008h y 0x000018h, correspondientes a las interrupciones de alta y baja prioridad, respectivamente. Cada vez que se active una señal de interrupción, el microcontrolador deshabilitará las interrupciones, guardará el valor del PC en pila, cargará el nuevo valor de PC a partir del almacenado en uno de estos dos vectores, en función de la señal de interrupción que se haya activado, y continuará ejecutando instrucciones. Esta rutina de tratamiento de interrupción (ISR) deberá terminar con una instrucción RETFIE, con la que el PC recupera su valor a partir de la pila, el registro GIE vuelve a estar habilitado y con él, las interrupciones. Hay que tener en cuenta que disponemos de dos niveles de interrupciones: alta y baja prioridad. La distinción de prioridades permite que los acontecimientos de alta prioridad reciban atención inmediata aún cuando el micro esté tratando otra interrupción -siempre que ésta sea de prioridad baja- y que su rutina de servicio no se vea interrumpida por otros eventos de menor prioridad.
3.4.2.
Memoria de datos
La memoria de datos está compuesta por registros de propósito general (GPR) y registros de función especial (SFR). Los primeros sirven para almacenar cualquier valor sin un propósito establecido de antemano. Los segundos tienen tareas definidas y fijadas en el diseño del hardware: comunicar y controlar los diversos dispositivos integrados en el microcontrolador y controlar el funcionamiento de éste. Debido a la diversidad de microcontroladores, el mapa de registros difiere para cada modelo. A pesar de que existe una tendencia a igualar la ubicación en memoria de los SFR para microcontroladores de una misma familia y gama, es conveniente consultar la hoja de características del modelo, incluso de nuevas revisiones del mismo, para tener la certeza de que el dispositivo funcionará de la manera espe-
Compilador de C para el microcontrolador Microchip PIC18F4550
18
Banco Bits 3:0 accesible de BSR 0 0000 1 0001 2 0010 3 0011 4 0100 5 0101 6 0110 7 0111 Cuadro 3.1: Valores del BSR para acceso a los bancos del microcontrolador PIC18F4550.
rada. Los registros de los periféricos los describiremos más adelante, en la sección correspondiente a cada periférico. Los PIC18 pueden direccionar hasta 4Kbytes de memoria de datos que, como ocurre con el espacio de memoria de programa, pueden estar implementados en su totalidad o disponer de menos memoria. Esta memoria está dividida en varias partes, llamadas bancos, de 256 bytes cada una. El PIC18F4550 implementa 8 bancos completos, lo que resulta en un total de 2Kbytes de memoria de datos. Igual que ocurre con la memoria de programa, todo acceso de lectura a una posición de memoria fuera del rango implementado devuelve un valor 0. Los accesos de escritura a direcciones de memoria no implementadas no tienen ningún efecto más allá de actualizar el registro de estado STATUS como si la operación hubiese tenido éxito. La mayoría de instrucciones de los PIC18 que efectúan accesos a memoria construyen la dirección absoluta a partir de una dirección base (el número de banco) y un desplazamiento relativo. El número de banco al que accederá la próxima instrucción lo indica el registro de selección de banco (BSR), el cual habrá que cambiar antes de hacer un acceso a una posición de memoria ubicada en un banco diferente al que se encuentre seleccionado en el momento actual. El desplazamiento dentro del banco, en cambio, es una dirección de 8 bits incluída en la palabra de instrucción. Banco de acceso Aunque el uso del BSR junto a un desplazamiento de 8 bits permite direccionar todo el rango de memoria de datos, también obliga a tener especial cuidado con qué banco se encuentra seleccionado antes de cada acceso. Podría resultar desastroso que por error escribiésemos en un SFR en lugar de un GPR. Además, la tarea de
Compilador de C para el microcontrolador Microchip PIC18F4550
19
verificar y cambiar el BSR para cada acceso a memoria llega a resultar muy ineficiente. Para agilizar los accesos a las posiciones de memoria que se usan con mayor frecuencia, la memoria de datos está configurada con un banco de acceso, que permite acceder a un bloque de memoria sin hacer uso del BSR. Este banco está compuesto por los primeros 96 bytes del banco 0 y los últimos 160 bytes del banco 15. La primera mitad recibe el nombre de RAM de acceso y está compuesta de registros GPR. La mitad superior, en cambio, está compuesta por los SFR del microcontrolador. Ambas áreas se encuentran mapeadas de manera contigua en el banco de acceso y permiten accesos de modo lineal empleando una dirección de 8 bits. Las instrucciones que hacen uso del banco de acceso son aquellas que incluyen un parámetro a. Cuando este parámetro vale 1, la instrucción accede a memoria de la manera habitual, empleando el BSR y un desplazamiento de 8 bits. Cuando vale 0, el acceso a memoria lo efectúa mediante el banco de acceso, con lo que el valor que tenga el BSR no tiene efecto alguno. El uso de este direccionamiento forzado permite a la instrucción operar sobre una dirección de memoria en un único ciclo de instrucción sin tocar primero el BSR. Para direcciones a partir de la 0x0060h, esto significa un acceso más eficiente a los SFR. La RAM de acceso por debajo de la posición 0x0060h es un buen lugar para valores de datos que necesitemos acceder rápidamente, como resultados de operaciones recientes o variables globales del programa. Registros de propósito general Los registros de propósito general (GPR), como su nombre indica, no tienen un propósito prefijado de antemano: los podemos usar para lo que consideremos oportuno (datos temporales, variables y valores constantes, entre otros usos). La modificación de uno u otro no tiene más relevancia que el cambio de su valor, por lo que nos proporciona la libertad necesaria para ejecutar las acciones que deseemos en el microcontrolador. No todos los dispositivos implementan la totalidad de registros GPR. En estos modelos es posible acceder a todo el mapa de registros pero los accesos de lectura devolverán siempre un valor 0 y las escrituras no tendrán más efecto que actualizar los bits convenientes del registro STATUS en caso de que sean resultado de una operación de la ALU.
Compilador de C para el microcontrolador Microchip PIC18F4550 Registro INDFx POSTINCx POSTDECx PREINCx PLUSWx FSRxH FSRxL TMRxH TMRxL TxCON STATUS PRODH PRODL PCLATU PCLATH PCL INTCON INTCON2 INTCON3 PIR1 PIR2 PIE1 PIE2 IPR1 IPR2 RCON
20
Uso
Registros para acceso indirecto
Punteros a memoria para acceso indirecto Cuenta de los temporizadores Configuración de los temporizadores Registro de estado Byte alto del resultado de multiplicador Byte bajo del resultado de multiplicador Parte superior del PC Parte alta del PC Parte baja del PC Configuración de las interrupciones Estado de las interrupciones Habilitación de interrupciones Asignación de prioridad de las fuentes de interrupción Control de Reset y prioridad de interrupciones
Cuadro 3.2: Resumen de registros de función especial del núcleo del microcontrolador PIC18F4550.
Registros de función especial Los registros de función especial (SFR) son los registros que emplean la CPU y los módulos periféricos para controlar el funcionamiento del microcontrolador. Están mapeados en las últimas posiciones del banco 15 de memoria de datos, desde la posición 0x0F60h hasta la 0x0FFFh. Podemos clasificar los SFR en dos conjuntos: los registros correspondientes a funciones del núcleo (ALU, Reset e interrupciones) y los que corresponden a funciones de los periféricos. La tabla 3.2 presenta un resumen de los registros de función especial del núcleo. Los registros de funciones de periféricos los veremos más adelante.
Compilador de C para el microcontrolador Microchip PIC18F4550
21
Direccionamiento indirecto Existe otro modo de acceso a los registros: el acceso indirecto. Este modo permite acceder a posiciones de memoria sin indicar una dirección fija como parámetro de la instrucción. Para ello hacemos uso de los pares de registros de selección de archivo (FSR), los cuales actúan como punteros a las direcciones de memoria en las que deseamos leer o escribir. En total, es posible direccionar 4K posiciones, luego hacen falta 12 bits para este cometido. Los ocho bits menos significativos están almacenados en el registro FSRxL, mientras que los más significativos son los cuatro bits más bajos del registro FSRxH correspondiente. El PIC18F4550 ofrece 3 pares de FSR, de modo que podemos emplear hasta tres apuntadores independientes para acceso indirecto a memoria. Una vez indicada la dirección en un par FSR, podemos acceder al contenido de esta posición de memoria a través del registro INDF asociado al FSR. Los registros INDFx podemos considerarlos virtuales, ya que no son registros físicos implementados en memoria, a pesar de que están mapeados en ésta. Por ejemplo, un acceso a INDF0 resulta ser un acceso al registro apuntado por FSR0H:FSR0L. Cabe destacar que tanto los FSR como sus INDF están mapeados en el banco de acceso, por lo que no es necesario preocuparse por la selección de banco a la hora de utilizarlos. Además, la dirección de memoria empleada para direccionamiento indirecto se encuentra almacenada completamente en el par FSR, por lo que la gestión de bancos tampoco es necesaria cuando hagamos accesos indirectos a memoria.
3.5.
Juego de instrucciones
El conjunto de instrucciones de los PIC18 está dividido en cuatro categorías básicas: Orientadas a byte. Orientadas a bit. De literales. De control. Orientadas a tabla. La mayoría de instrucciones orientadas a byte tienen tres operandos: 1. Registro objetivo. f
Compilador de C para el microcontrolador Microchip PIC18F4550
22
2. Destino del resultado (WREG o un GPR). d 3. Banco de memoria accedido (datos o acceso). a El registro objetivo f indica sobre qué registro operará la instrucción. El destino d especifica dónde almacenar el resultado de la operación. Si d es cero, el resultado se guarda en WREG. Si d es uno, se almacena en el mismo registro objetivo. El bit de acceso a puesto a cero indica que el registro objetivo pertenece al banco de acceso. Si a vale uno, entonces el registro objetivo corresponde al banco de memoria indicado en BSR. Todas las intrucciones orientadas a bit tiene tres operandos: 1. Registro objetivo. f 2. Bit del registro. b 3. Banco de memoria accedido (datos o acceso). a El operando b indica el bit afectado por la instrucción. Los otros dos parámetros tienen el mismo significado que en las instrucciones orientadas a byte. Las instrucciones de literales pueden tener uno de los siguientes operandos o los dos: Valor literal para cargar en un registro. k Registro FSR donde cargar el valor literal. f Las instrucciones de control pueden tener uno o varios de los siguientes operandos: Dirección de memoria de programa. n Modo de las instrucciones CALL o RETURN. s Las instrucciones orientadas a tabla tan sólo tienen un operando de modo, que indica si se trata de un acceso con pre-incremento, post-incremento, post-decremento o un acceso simple que no modifica el apuntador de tabla. Las instrucciones ensamblador las veremos con una simple tabla-resumen. La mejor fuente para ampliar información, una vez más, es el datasheet del PIC18F4550, que proporciona ejemplos para cada instrucción de las distintas posibilidades de ejecución. La tabla 3.5 muestra todas las instrucciones separadas por formato de instrucción mostrando una pequeña descripción. La tabla 3.5 complementa a ésta, mostrando los ciclos que necesita cada instrucción, los bits del registro STATUS afectados y la palabra genérica de código máquina correspondiente a cada nemotécnico.
Compilador de C para el microcontrolador Microchip PIC18F4550
Nemotécnico
Descripción
Orientadas a byte ADDWF f,d,a ADDWFC f,d,a ANDWF f,d,a CLRF f,a COMF f,d,a CPFSEQ f,a CPFSGT f,a CPFSLT f,a DECF f,d,a DECFSZ f,d,a DCFSNZ f,d,a INCF f,d,a INCFSZ f,d,a INFSNZ f,d,a IORWF f,d,a MOVF f,d,a MOVFF fs,fd MOVWF f,a MULWF f,a NEGF f,a RLCF f,d,a RLNCF f,d,a RRCF f,d,a RRNCF f,d,a SETF f,a SUBFWB f,d,a SUBWF f,d,a SUBWFB f,d,a SWAPF f,d,a TSTFSZ f,a XORWF f,d,a
Suma de WREG y f Suma con acarreo de WREG y f And lógico de WREG y f Puesta a 0 de f Complemento a 1 de f Compara f y WREG, salta si igual Compara f y WREG, salta si mayor Compara f y WREG, salta si menor Decrementa f Decrementa f, salta si cero Decrementa f, salta si no cero Incrementa f Incrementa f, salta si cero Incrementa f, salta si no cero Or lógico de WREG y f Carga f en WREG Mueve de fs a fd Almacena WREG en f Multiplica WREG con f Complemento a 2 de f Rotación a la izquierda con acarreo Rotación a la izquierda sin acarreo Rotación a la derecha con acarreo Rotación a la derecha sin acarreo Puesta a 0xFFh de f Resta con acarreo WREG menos f Resta sin acarreo f menos WREG Resta con acarreo f menos WREG Intercambia los nibbles de f Comprueba f, salta si cero Xor lógico de WREG y f
Cuadro 3.3: Resumen del conjunto de instrucciones del microcontrolador PIC18F4550.
23
Compilador de C para el microcontrolador Microchip PIC18F4550
Orientadas a bit BCF f,b,a BSF f,b,a BTFSC f,b,a BTFSS f,b,a BTG f,d,a
Limpia un bit de f Activa un bit de f Comprueba un bit de f, salta si es 0 Comprueba un bit de f, salta si es 1 Conmuta un bit de f
De literales ADDLW k ANDLW k IORLW k LFSR f,k MOVLB k MOVLW k MULLW k SUBLW k XORLW k
Suma k a WREG And lógico de k y WREG Or lógico de k y WREG Carga k en el FSR indicado por F Carga k en el BSR Carga k en el WREG Multiplica k con WREG Resta k menos WREG Xor lógico de WREG y k
Cuadro 3.3: Resumen del conjunto de instrucciones del microcontrolador PIC18F4550.
24
Compilador de C para el microcontrolador Microchip PIC18F4550
De control BRA n BC n BZ n BOV n BN n BNC n BNN n BNOV n BNZ n CALL n,s CLRWDT DAW GOTO n NOP POP PUSH RCALL n RESET RETFIE s RETLW k RETURN s SLEEP
Salto relativo incondicional Salto relativo si acarreo Salto relativo si cero Salto relativo si desbordamiento Salto relativo si negativo Salto relativo si no acarreo Salto relativo si no negativo Salto relativo si no desbordamiento Salto relativo si no cero Llamada a subrutina Puesta a 0 del temporizador del WDT Ajuste decimal de WREG Salto absoluto No operation Extrae de la pila y carga en PC Introduce en la pila el valor de PC Llamada a subrutina, relativa a PC Reset por software del dispositivo Fin de ISR Fin de subrutina y carga k en WREG Fin de subrutina Pasa el dispositivo al modo de reposo
Cuadro 3.3: Resumen del conjunto de instrucciones del microcontrolador PIC18F4550.
25
Compilador de C para el microcontrolador Microchip PIC18F4550
Orientadas a tabla TBLRD* TBLRD*+ TBLRD*TBLRD+* TBLWT* TBLWT*+ TBLWT*TBLWT+*
Lectura de tabla Lectura con post-incremento Lectura con post-decremento Lectura pre-incremento Escritura de tabla Escritura con post-incremento Escritura con post-decremento Escritura con pre-incremento
Cuadro 3.3: Resumen del conjunto de instrucciones del microcontrolador PIC18F4550.
26
Compilador de C para el microcontrolador Microchip PIC18F4550
Nemotécnico
Ciclos Bis de STATUS afectados
Palabra de instrucción
Orientadas a byte ADDWF f,d,a ADDWFC f,d,a ANDWF f,d,a CLRF f,a COMF f,d,a CPFSEQ f,a CPFSGT f,a CPFSLT f,a DECF f,d,a DECFSZ f,d,a DCFSNZ f,d,a INCF f,d,a INCFSZ f,d,a INFSNZ f,d,a IORWF f,d,a MOVF f,d,a
1 1 1 1 1 1-3 1-3 1-3 1 1-3 1-3 1 1-3 1-3 1 1
MOVFF fs,fd
2
MOVWF f,a MULWF f,a NEGF f,a RLCF f,d,a RLNCF f,d,a RRCF f,d,a RRNCF f,d,a SETF f,a SUBFWB f,d,a SUBWF f,d,a SUBWFB f,d,a SWAPF f,d,a TSTFSZ f,a XORWF f,d,a
1 1 1 1 1 1 1 1 1 1 1 1 1-3 1
C,DC,Z,OV,N C,DC,Z,OV,N Z,N Z Z,N
C,DC,Z,OV,N
C,DC,Z,OV,N
Z,N Z,N
C,DC,Z,OV,N C,Z,N Z,N C,Z,N Z,N C,DC,Z,OV,N C,DC,Z,OV,N C,DC,Z,OV,N
Z,N
0010 01da ffff ffff 0010 00da ffff ffff 0001 01da ffff ffff 0110 101a ffff ffff 0001 11da ffff ffff 0110 001a ffff ffff 0110 010a ffff ffff 0110 000a ffff ffff 0000 01da ffff ffff 0010 11da ffff ffff 0100 11da ffff ffff 0010 10da ffff ffff 0011 11da ffff ffff 0100 10da ffff ffff 0001 00da ffff ffff 0101 00da ffff ffff 1100 ffff ffff ffff 1111 ffff ffff ffff 0110 111a ffff ffff 0000 001a ffff ffff 0110 110a ffff ffff 0011 01da ffff ffff 0100 01da ffff ffff 0011 00da ffff ffff 0100 00da ffff ffff 0110 100a ffff ffff 0101 01da ffff ffff 0101 11da ffff ffff 0101 10da ffff ffff 0011 10da ffff ffff 0110 011a ffff ffff 0001 10da ffff ffff
Cuadro 3.4: Instrucciones del microcontrolador PIC18F4550: ciclos, bits de STATUS afectados y palabra de instrucción.
27
Compilador de C para el microcontrolador Microchip PIC18F4550
Orientadas a bit BCF f,b,a BSF f,b,a BTFSC f,b,a BTFSS f,b,a BTG f,d,a
1 1 1-3 1-3 1
1001 bbba ffff ffff 1000 bbba ffff ffff 1011 bbba ffff ffff 1010 bbba ffff ffff 0111 bbba ffff ffff
De literales ADDLW k ANDLW k IORLW k
1 1 1
LFSR f,k
2
MOVLB k MOVLW k MULLW k SUBLW k XORLW k
1 1 1 1 1
C,DC,Z,OV,N Z,N Z,N
C,DC,Z,OV,N Z,N
0000 1111 kkkk kkkk 0000 1011 kkkk kkkk 0000 1001 kkkk kkkk 1110 1110 00ff kkkk 1111 0000 kkkk kkkk 0000 0001 0000 kkkk 0000 1110 kkkk kkkk 0000 1101 kkkk kkkk 0000 1000 kkkk kkkk 0000 1010 kkkk kkkk
Cuadro 3.4: Instrucciones del microcontrolador PIC18F4550: ciclos, bits de STATUS afectados y palabra de instrucción.
28
Compilador de C para el microcontrolador Microchip PIC18F4550
De control BRA n BC n BZ n BOV n BN n BNC n BNN n BNOV n BNZ n
2 1-2 1-2 1-2 1-2 1-2 1-2 1-2 1-2
CALL n,s
2
CLRWDT DAW
1 1
GOTO n
2
NOP NOP POP PUSH RCALL n RESET RETFIE s RETLW k RETURN s SLEEP
1 1 1 1 2 1 2 2 2 1
TO,PD C
Todos GIE/GIEH, PEIE/GIEL
TO,PD
1101 0nnn nnnn nnnn 1110 0010 nnnn nnnn 1110 0000 nnnn nnnn 1110 0100 nnnn nnnn 1110 0110 nnnn nnnn 1110 0011 nnnn nnnn 1110 0111 nnnn nnnn 1110 0101 nnnn nnnn 1110 0001 nnnn nnnn 1110 110s kkkk kkkk 1111 kkkk kkkk kkkk 0000 0000 0000 0100 0000 0000 0000 0111 1110 1111 kkkk kkkk 1111 kkkk kkkk kkkk 0000 0000 0000 0000 1111 xxxx xxxx xxxx 0000 0000 0000 0110 0000 0000 0000 0101 1101 1nnn nnnn nnnn 0000 0000 1111 1111 0000 0000 0001 000s 0000 1100 kkkk kkkk 0000 0000 0001 001s 0000 0000 0000 0011
Cuadro 3.4: Instrucciones del microcontrolador PIC18F4550: ciclos, bits de STATUS afectados y palabra de instrucción.
29
Compilador de C para el microcontrolador Microchip PIC18F4550
30
Orientadas a tabla TBLRD* TBLRD*+ TBLRD*TBLRD+* TBLWT* TBLWT*+ TBLWT*TBLWT+*
2 2 2 2 2 2 2 2
0000 0000 0000 1000 0000 0000 0000 1001 0000 0000 0000 1010 0000 0000 0000 1011 0000 0000 0000 1100 0000 0000 0000 1101 0000 0000 0000 1110 0000 0000 0000 1111
Cuadro 3.4: Instrucciones del microcontrolador PIC18F4550: ciclos, bits de STATUS afectados y palabra de instrucción.
3.6.
Periféricos
A continuación estudiaremos de forma resumida los distintos periféricos que podemos encontrar en un PIC. Debido a la gran variedad de dispositivos integrados que llegan a formar parte de un microcontrolador PIC y las diferencias existentes en un dispositivo entre los distintos modelos, nos centramos en los dispositivos contenidos en el microcontrolador 18F4550.
3.6.1.
Puertos de Entrada/Salida
El puerto de E/S es el dispositivo más sencillo de los existentes en un microcontrolador y también es el más utilizado. Los PIC ofrecen puertos de E/S de hasta 8 bits de tamaño. El 18F4550 dispone de 5 puertos de E/S, etiquetados con las letras de la A hasta la E, de los cuales B y D son puertos de 8 bits, A y C son de 7 bits, mientras que el puerto E tiene un ancho de 4 bits. Cada puerto dispone de tres registros que permiten su manejo. El registro TRISx (donde x es la letra que identifica a cada puerto) indica el sentido (entrada o salida) de cada pin, de modo que un bit puesto a 1 indica que el pin correspondiente es de entrada, mientras que un 0 indica que dicho pin es de salida. Por tanto, para emplear como pines de entrada los dos menos significativos del puerto B y como salida el resto de pines del mismo puerto basta con cargar en TRISB el valor binario
Compilador de C para el microcontrolador Microchip PIC18F4550
31
00000011. Una vez configurada la dirección, la lectura y escritura de un puerto de E/S resulta tan sencilla como leer o escribir en el registro PORTx correspondiente. El tercer registro asociado a un puerto, LATx, hace las funciones de latch de salida. Cuando efectuamos una operación de escritura sobre PORTx, en realidad el registro escrito es LATx. Sin embargo, una operación de lectura sobre PORTx no efectúa una lectura del registro LATx, sino que produce la carga del dato de entrada en otro latch (destinado únicamente a datos entrantes). Acto seguido, el microcontrolador almacena el valor de este latch de entrada en el registro destino de la instrucción. Gracias a esta combinación de latches para entrada y salida es posible obtener el valor que sale del puerto sin necesidad de circuitería extra ni de un puerto de entrada adicional. Un ejemplo que aprovecha esta configuración es el par de instrucciones BSF/BCF. Estas instrucciones cambian el valor de un bit almacenado en un registro cualquiera del microcontrolador. En el caso de un puerto de E/S, primero leen el valor del registro (LATx en este caso), activan o desactivan el bit indicado y, por último, almacenan el nuevo valor en el latch de salida del puerto en cuestión (LATx). Un detalle muy a tener en cuenta es que si un puerto de entrada, que haya leído un valor en algún momento, cambia de modo y pasa a funcionar en modo de salida, el valor que tendrá será el almacenado en LATx (latch de salida de PORTx), no el que había en latch de entrada de PORTx. Otro punto a tener en cuenta es que debido al coste que suponen las patillas de comunicación en los PIC y, en general, en cualquier dispositivo integrado, los puertos de E/S están multiplexados con otros dispositivos. Cuando uno de estos dispositivos esté activado, los pines compartidos con el puerto de E/S no podrán usarse con tal propósito, con lo que el puerto tendrá un tamaño efectivo menor. Un ejemplo de esto es el puerto A, cuyo pin 0 está multiplexado con el canal 0 del conversor analógico-digital. Cuando esté activo el módulo de conversión A/D, habrá que tener cuidado de no modificar los registros TRISA, TRISB y TRISE, ya que afectan a los pines que emplea el conversor y podría resultar en errores de conversión. Por último, hay que recordar que la escritura en los registros ocurre al final del ciclo de instrucción, mientras que la lectura se produce al principio. Debido a esto, puede darse el caso de que el valor a la salida de un puerto no esté estabilizado antes del próximo acceso. Por este motivo, Microchip recomienda intercalar entre dos operaciones consecutivas sobre un puerto concreto una instrucción que no haga uso del mismo, para tener la seguridad de que los valores del puerto se han estabilizado antes de un acceso de lectura.
Compilador de C para el microcontrolador Microchip PIC18F4550
3.6.2.
32
Universal Serial Bus
En la actualidad, uno de los interfaces de comunicación con periféricos más utilizados es el USB. El PIC18F4550 incorpora un motor de interface serie (SIE) que soporta el estándar USB 2.0 en modos de baja y alta velocidad. El SIE puede canalizarse directamente a dispositivos USB haciendo uso de su transceptor interno o conectarse a un transceptor externo, proporcionando una notable flexibilidad a la hora de diseñar del hardware. El subsistema USB del 18F4550 también incorpora un regulador de tensión de 3.3V con las resistencias de polarización exigidas por la especificación USB 2.0. Además, ofrece la posibilidad de usar polarización, e incluso alimentación externa, con el transceptor interno. El control del subsistema USB recae sobre un total de 22 registros: UCON; registro de control. UCFG; registro de configuración. USTAT; registro de estado de transferencia. UADDR; registro de dirección del dispositivo. UFRMH y UFRML; registros de número de marco. UEP0 a UEP15; registros de control de los terminadores. El medio de intercambio de datos entre el controlador y el SIE es una memoria de 1Kb de tamaño, denominada USB RAM. Se trata de una memoria de 1Kb de puerto doble, es decir, una memoria que permite accesos simultáneos desde dos dispositivos independientes, cada uno con sus propios buses de direcciones, datos y señales de control. El puerto correspondiente al controlador está mapeado en los bancos 4 a 7 de la memoria de datos.
3.6.3.
SPP
Este modelo de PIC incorpora un puerto paralelo orientado a flujos de datos, en inglés Streaming Parallel Port (SPP), pensado para utilizarse como interface de alta velocidad para intercambiar grandes cantidades de datos con un sistema externo. El SPP opera como puerto paralelo maestro completo, con señales de control y de reloj para controlar el dispositivo esclavo conectado a él. Es posible configurarlo de modo que el control del SPP recaiga sobre la CPU del PIC o sobre el controlador USB descrito en la sección anterior.
Compilador de C para el microcontrolador Microchip PIC18F4550
33
Los registros empleados para configurar el SPP son SPPCON, el cual controla el modo de operación y determina si el control recae sobre la CPU o sobre el SIE, y el SPPCFG, que se encarga de la configuración de temporización y de habilitar las señales de control hacia el exterior. A estos se le suman los registros TRISD, TRISE y TRISB, que indican en qué modo operarán los pines correspondientes a señales de datos del SPP. Cuando el SPP está configurado para que sea la CPU quien lo maneje, el registro SPPEPS es el que proporciona información de estado y control sobre el puerto, mientras que SPPDATA es el registro objetivo de la lectura/escritura de datos transmitidos a través del puerto paralelo.
3.6.4.
MSSP
El puerto serie síncrono maestro (MSSP) es un interface serie que resulta útil para comunicar el PIC con periféricos externos o con otros microcontroladores. Tiene dos modos de funcionamiento diferentes: SPI (interface de periférico serie) o como I2 C. Los registros empleados para controlar el MSSP son SSPSTAT, SSPCON y SSPCON2, el primero de los tres proporciona información sobre el estado del módulo y los dos restantes cumplen las funciones de control. Los registros encargados de albergar los datos a enviar o recibir son SSPSR y SSPBUF, mientras que el registro SSPADD es el responsable de operaciones de direccionamiento cuando el módulo está configurado en modo I2 C. Para conocer los detalles de su uso es conveniente acudir a la sección correspondiente del datasheet del microcontrolador. Además, la web de Microchip contiene diversos ejemplos de aplicación con cada uno de los modos de este módulo.
3.6.5.
EUSART
Además del MSSP, este PIC ofrece un segundo módulo para comunicaciones serie: el transmisor/receptor síncrono/asíncrono universal mejorado (EUSART). Puede funcionar en modos half-duplex y full-duplex, y además incorpora características extra sobre los USART convencionales, como detección y ajuste automáticos de la tasa de transferencia. El control del módulo EUSART recae sobre tres registros: TXSTA (control y estado de la transmisión), RCSTA (control y estado de la recepción) y BAUDCON (control de la tasa de transferencia). El par de registros SPBRGH y SPBRG controlan el período empleado por temporizador del generador de símbolos del EUSART. Una vez que el módulo está configurado para un modo de funcionamiento, el envío
Compilador de C para el microcontrolador Microchip PIC18F4550
34
comienza con la escritura del dato a enviar en el registro TXREG, mientras que una recepción con éxito termina con el dato recibido en RCREG. El proceso para transmitir un dato consta de los siguientes pasos: 1. Inicializar SPBRGH:SPBRG con el valor correspondiente a los baudios deseados para la frecuencia actual del oscilador. 2. Configurar el módulo EUSART como síncrono o asíncrono según la comunicación. 3. Habilitar la transmisión activando el bit TXEN de TXSTA. 4. Cargar TXREG con el valor que se desea transmitir. Esto hace que comience la transmisión. 5. Comprobar el bit TRMT de TXSTA. Cuando esté puesto a uno, la transmisión habrá finalizado. De manera análoga, los pasos a seguir para recepción son: 1. Inicializar SPBRGH:SPBRG con el valor correspondiente a los baudios deseados para la frecuencia actual del oscilador. 2. Configurar el módulo EUSART como síncrono o asíncrono según la comunicación. 3. Habilitar la recepción activando el bit SREN de RCSTA. 4. Comprobar el bit RCIF de PIR1. Cuando esté puesto a uno, la recepción habrá finalizado y el registro RCREG contendrá el dato recibido. El valor que deberá contener el par de registros SPBRGH:SPBRG establece el período del generador de símbolos, produciendo una velocidad en baudios diferente según la frecuencia de oscilador empleada. Cada extremo de la línea de comunicaciones serie debe emplear la misma configuración, de forma que transmisor y receptor funcionen a la misma velocidad y en el mismo modo para que sea posible establecer con éxito la comunicación entre ambos. Para determinar el valor de SPBRGH:SPBRG hace falta conocer la velocidad en baudios a la que debe funcionar el EUSART. Una vez conocido este detalle, el valor de los registros SPBFGH:SPBRG se calcula a partir de una de las fórmulas siguientes, según el modo de funcionamiento en que vaya a trabajar el módulo EUSART: Como se deduce de la tabla anterior, el valor de SPBRGH:SPBRG depende también del modo empleado para la comunicación. Cabe destacar que la configuración de la comunicación serie debe ser la misma en ambos extremos de la línea.
Compilador de C para el microcontrolador Microchip PIC18F4550
35
Bits de configuración Modo SPBRGH:SPBRG Baudios Sync BRG16 BRGH 0 0 0 8-bit asíncrono (Fosc/(64*Baud))-1 Fosc/(64*(n+1)) 0 0 1 8-bit asíncrono (Fosc/(16*Baud))-1 Fosc/(16*(n+1)) 0 1 0 16-bit asíncrono 0 1 1 16-bit asíncrono 1 0 x 8-bit síncrono (Fosc/(4*Baud))-1 Fosc/(4*(n+1)) 1 1 x 16-bit síncrono Cuadro 3.5: Tabla de configuración del EUSART del microcontrolador PIC18F4550.
Debido al redondeo al entero más próximo, aparece un porcentaje de error respecto a la tasa de transferencia teórica deseada. Una práctica muy recomendable es realizar la operación contraria, tomando el valor de SPBRGH:SPBRG como dato de partida y la tasa de baudios como incógnita, para calcular el error introducido por el redondeo. Un error demasiado grande producirá una desincronización entre emisor y receptor, e imposibilitará la comunicación entre ellos. El registro TXSTA es el encargado del control del modo de transmisión. El bit CSRC indica qué fuente de reloj empleará el EUSART para comunicación síncrona. TX9 permite seleccionar entre palabras de 8 o 9 bits. TXEN es el bit que habilita la transmisión. SYNC, como indica su nombre, es el bit de selección entre modo síncrono o asíncrono; este bit también afecta al modo de recepción. SENDB permite forzar el envío, en comunicaciones asíncronas, de un caracter de sincronización (Sync Break). El control de la tasa de baudios (alta o baja velocidad) recae en el bit BRGH. TRMT es un bit de estado, no de control; indica cuándo está vacío el buffer de salida. Por último, el bit TX9D del registro TXSTA alberga el noveno bit de la palabra a enviar cuando el EUSART está configurado para enviar palabras de 9 bits. Por otro lado, el registro RCSTA controla la recepción y da información sobre la misma. RX9 indica el tamaño de palabra, 8 o 9 bits. El bit SREN habilita la recepción simple en modo síncrono, mientras que CREN habilita la recepción múltiple, también llamada continua. El bit de FERR indica si hubo un error de marco (frame) en la recepción, mientras que OERR avisa de que ha habido un error de desbordamiento. RX9D es el bit análogo a TX9D: almacena el noveno bit de la palabra recibida, si el EUSART está trabajando con palabras de 9 bits.
3.6.6.
Memoria EEPROM
El 18F4550 dispone de una pequeña memoria EEPROM a la que es posible acceder, tanto para lectura como escritura, desde el programa en ejecución. Su tamaño
Compilador de C para el microcontrolador Microchip PIC18F4550
36
es reducido, sólo 256 bytes. Esta memoria no está mapeada directamente sobre la memoria de datos; la única manera de acceder a ella es mediante direccionamiento indirecto, a través de varios SFR. EEADR es el registro de direccionamiento para accesos a la EEPROM. Complementando a éste se encuentra el registro EEDATA; en él aparecerá el dato leído de la EEPROM tras una operación de lectura y es el registro que aloja el dato a cargar en EEPROM con la próxima operación de escritura. Aparte de estos dos registros, de direccionamiento y datos, respectivamente, hay dos registros más, que cumplen las funciones de control de acceso: EECON1 y EECON2. EECON1 es un registro de control convencional, con varios bits que cumplen distintos cometidos; es responsable del control de acceso a las memorias EEPROM, flash de programa y a los bits de configuración del PIC. El bit EEPGD selecciona a memoria accederán las operaciones de lectura/escritura: flash de programa (uno) o EEPROM de datos (cero). EECFGS determina si los accesos serán a los registros de configuración o a alguna de las dos memorias antes mencionadas. WRERR indica si la última operación de escritura terminó con error. WREN es el bit que habilita la operación de escritura. La operación de lectura comienza con la carga de un uno en el bit RD del registro EECON1; una vez completada la operación, el hardware del microcontrolador carga de manera automática un cero en este bit. La operación de escritura tiene un comportamiento análogo con el bit WR. El registro EECON2 no es un registro físico. Su función se reduce a hacer de seguro antes de una operación de escritura o borrado. La única manera de efectuar una escritura es escribir el valor 0x55h en EECON2, acto seguido escribir 0xAAh en este mismo registro, y, una vez hecho esto, poner a cero el bit WR de EECON1. En caso de no seguir exactamente esta secuencia, el PIC no ejecutará la operación de escritura. Por supuesto, el bit WREN debe estar puesto a uno antes de efectuar estos pasos. Como precaución adicional, es muy recomendable tener deshabilitadas las interrupciones durante la ejecución de este fragmento de código. A continuación, un ejemplo de cómo efectuar una lectura de la EEPROM: ; Cargar EEADR con la dirección deseada. MOVLW dirección MOVWF EEADR ; Seleccionar memoria EEPROM de datos. BCF EECON1, EEPGD ; Activar acceso a memoria especial.
Compilador de C para el microcontrolador Microchip PIC18F4550 BCF
37
EECON1, CFGS
; Leer de la EEPROM. BSF EECON1, RD ; Cargar en W el dato leído. MOVF EEDATA, W El código anterior resulta sumamente sencillo. Comienza con la carga en el registro EEADR de la dirección de memoria EEPROM a la que acceder en esta operación de lectura. Acto seguido, pone a cero el bit EECFGS del registro de control EECON1, con lo que indica al PIC que el próximo acceso a memoria especial será a EEPROM o flash. Lo siguiente es seleccionar la memoria EERPOM, que se consigue poniendo a cero el bit EEPGD. Por último, inicia la operación de lectura mediante la puesta a uno del bit RD. Este fragmento de código concluye con la carga en WREG del dato leído de la EEPROM, el cual aparece alojado en EEDATA tras una lectura exitosa. Un ejemplo práctico de código que efectúa una operación de escritura:
noWR
; Mientras no haya terminado la posible ; escritura en proceso, hacer espera activa. BTFSC EECON1, WR GOTO noWR
; Cargar EEADR con la dirección deseada. MOVLW dirección MOVWF EEADR ; Cargar EEDATA con el dato a almacenar. MOVLW valor MOVWF EEDATA ; Seleccionar EEPROM de datos. BCF EECON1, EEPGD ; Activar acceso a memoria especial. BCF EECON1, CFGS ; Habilitar las operaciones de escritura.
Compilador de C para el microcontrolador Microchip PIC18F4550 BSF
38
EECON1, WREN
; Deshabilitar las interrupciones. BCF INTCON, GIE
; Paso 1: EECON2 MOVLW 0x55 MOVWF EECON2 ; Paso 2: EECON2 MOVLW 0xAA MOVWF EECON2 ; Escribir en la BSF EECON1,
<- 0x55h
<- 0xAAh
EEPROM. WR
; Volver a habilitar las interrupciones. BSF INTCON, GIE ; Volver a deshabilitar la escritura. BCF EECON1, WREN Un primer vistazo de este otro fragmento de código permite ver que las operaciones de escritura resultan algo más complicadas de efectuar que las de lectura, sobre todo por las diversas precauciones que hay que tomar, tanto para poder iniciar la escritura en sí, como para asegurar que ésta se efectúa con éxito y sin afectar a una posible escritura en curso. En primer lugar, hay que comprobar que no haya una operación de escritura en proceso, ya que la lectura es inmediata, pero la escritura requiere un tiempo para completarse. Es el propósito del bucle que comienza en la etiqueta noWR, comprobando una y otra vez el bit WR de EECON1 hasta que éste vale cero. Una vez garantizado que no hay ninguna escritura en proceso, el programa carga en el registro EEADR la dirección de EEPROM en la que escribir. El siguiente paso es cargar en EEDATA el valor a almacenar en la EEPROM. Tras estos dos pasos viene la activación de acceso a memoria especial y la selección de memoria EEPROM, instrucciones que también aparecen en el código para hacer una lectura de EEPROM. Sólo queda habilitar la escritura antes de proceder a la misma. A continuación viene la secuencia de cuatro instrucciones obligatorias para quitar el seguro de la operación de lectura, y la puesta a uno del bit WR de EECON1, que es el que da comienzo la
Compilador de C para el microcontrolador Microchip PIC18F4550
39
escritura en memoria EEPROM del dato cargado en EEDATA. Antes de terminar, este código vuelve a poner a cero el bit WREN, evitando con esto posibles escrituras accidentales.
3.6.7.
Memoria Flash
El manejo de la memoria flash de programa comparte algunos registros con el de la EEPROM, así como los mecanismos de protección contra escritura y borrado. Los registros de control comunes con la EEPROM son EECON1 y EECON2. Aparte de estos dos, los registros de acceso a tablas TABLAT y TBLPTR también son un componente esencial del mecanismo de acceso a la flash. Los bits de EECON1 tienen exactamente el mismo propósito que en el caso de accesos a EEPROM, salvo el bit 4, que sólo sirve para habilitar la operación borrado de una fila de memoria flash, y los bits 0 y 1, que no cumplen ninguna función cuando se trata de accesos a la memoria flash de programa. De nuevo, el cometido de EECON2 se reduce a hacer de seguro antes de una operación de escritura o borrado, con la misma secuencia de escrituras (0x55h, 0xAAh) inmediatamente antes de activar el bit WR de EECON1. A pesar de que el espacio de memoria flash de programa tiene un ancho de palabra de 16 bits, los accesos a éste desde el programa del PIC sólo permiten palabras de 8 bits (ancho de palabra de la memoria datos). El latch que almacena el dato a intercambiar con la flash es TABLAT. Por otro lado, la función de direccionamiento recae sobre el registro TBLPTR, de 22 bits. Este registro está compuesto por tres SFR: TBLPTRU (byte superior), TBLPTRH (byte alto) y TBLPTRL (byte bajo). Un ejemplo de código para leer una palabra (16 bits) de la memoria flash es el siguiente: ; TBLPTRU[5:0] <- dir[21:16] MOVLW dirU MOVWF TBLPTRU ; TBLPTRH[7:0] <- dir[15:8] MOVLW dirH MOVWF TBLPTRH ; TBLPTRL[7:0] <- dir[7:0] MOVLW dirL MOVWF TBLPTRL ; Cargar en TABLAT el byte bajo
Compilador de C para el microcontrolador Microchip PIC18F4550
40
; e incrementar TBLPTR. TBLRD*+ ; Almacenar en memoria de datos. MOVFF TABLAT, varL ; Cargar en TABLAT el byte alto. TBLRD* ; Almacenar en memoria de datos. MOVFF TABLAT, varH El procedimiento para escribir en flash es mucho más complicado que el de lectura debido a algunas características de la memoria flash, como el tamaño de bloque para escritura (16 palabras, 32 bytes), el tamaño de fila para borrado (32 palabras, 64 bytes) o la particularidad de que una escritura realmente sólo escriba ceros y que el borrado de un bloque suponga escribir todos sus bytes con 0xFFh. Por este motivo no es extraño combinar una operación de escritura con un borrado previo y una copia en memoria de datos del bloque de 64 bytes de flash que resultará sobreescrito. La mejor manera de comprender esto es con un ejemplo completo, que incluya copia en memoria de datos de la fila a sobreescribir, modificación de los bytes deseados, borrado de la fila de flash y escritura, en dos iteraciones, de la misma con los nuevos valores. El siguiente código reemplaza una palabra de dos bytes en memoria flash, reemplazando toda una fila de 64 bytes: ; Bloque de borrado de 64 bytes. MOVLW D’64’ MOVWF Contador ; Cargar MOVLW MOVWF MOVLW MOVWF
puntero a buffer auxiliar. BufferDirH FSR0H BufferDirL FSR0L
; Cargar puntero a bloque de ; memoria Flash. MOVLW FlashDirU MOVWF TBLPTRU MOVLW FlashDirH MOVWF TBLPTRH
Compilador de C para el microcontrolador Microchip PIC18F4550 MOVLW MOVWF
FlashDirL TBLPTRL
LEER_BLOQUE: ; Cargar byte en TABLAT e incrementar TBLPTR. TBLRD*+ ; Almacenar en buffer y apuntar a la ; siguiente posición. MOVFF TABLAT, POSTINC0 ; Si terminado (contador == 0), salir del bucle. ; Si no, siguiente iteración. DECFSZ Contador BRA LEER_BLOQUE MODIFICAR_PALABRA ; Reemplazar byte bajo del dato en el buffer. MOVLW NuevoDatoL MOVWF BuffDatoDirH ; Reemplazar byte alto. MOVLW NuevoDatoH MOVWF BuffDatoDirL BORRAR_BLOQUE ; Cargar de nuevo puntero a bloque de ; memoria Flash. MOVLW FlashDirU MOVWF TBLPTRU MOVLW FlashDirH MOVWF TBLPTRH MOVLW FlashDirL MOVWF TBLPTRL ; Seleccionar Flash de programa. BSF EECON1, EEPGD ; Activar acceso a memoria especial. BCF EECON1, CFGS
41
Compilador de C para el microcontrolador Microchip PIC18F4550 ; Habilitar las operaciones de escritura. BSF EECON1, WREN ; Habilitar operación de borrado de fila. BSF EECON1, FREE ; Deshabilitar las interrupciones. BCF INTCON, GIE ; Paso 1: EECON2 <- 0x55h MOVLW 0x55 MOVWF EECON2 ; Paso 2: EECON2 <- 0xAAh MOVLW 0xAA MOVWF EECON2 ; Comenzar borrado (CPU en espera). BSF EECON1, WR ; Volver a habilitar las interrupciones. BSF INTCON, GIE
; Cargar de nuevo el puntero a buffer ; auxiliar. MOVLW BufferDirH MOVWF FSR0H MOVLW BufferDirL MOVWF FSR0L ; Número de bloques de escritura: 2 MOVLW D’2’ MOVWF Bloque ESCRIBIR_BUFFER ; Bloque de escritura de 32 bytes. MOVLW D’32’ MOVWF Contador ESCRIBIR_BYTE
42
Compilador de C para el microcontrolador Microchip PIC18F4550 ; Copiar byte bajo en latch de tabla. MOVFF POSTINC0, TABLAT ; Escritura "corta" en flash ; (registros de "hold") TBLWT*+ ; Si terminado, salir del bucle; ; Si no, siguiente iteración. DECFSZ Contador BRA ESCRIBIR_BYTE ESCRITURA_LARGA ; Seleccionar Flash de programa. BSF EECON1, EEPGD ; Activar acceso a memoria especial. BCF EECON1, CFGS ; Habilitar las operaciones de escritura. BSF EECON1, WREN ; Deshabilitar las interrupciones. BCF INTCON, GIE ; Paso 1: EECON2 <- 0x55h MOVLW 0x55 MOVWF EECON2 ; Paso 2: EECON2 <- 0xAAh MOVLW 0xAA MOVWF EECON2 ; Comenzar escritura (CPU en espera). BSF EECON1, WR ; Volver a habilitar las interrupciones. BSF INTCON, GIE ; Si último bloque, salir del bucle; ; Si no, siguiente bloque. DECFSZ Bloque BRA ESCRIBIR_BUFFER
43
Compilador de C para el microcontrolador Microchip PIC18F4550
44
; Volver a deshabilitar la escritura. BCF EECON1, WREN La operación de escritura en flash tiene otra diferencia más respecto a la misma sobre EEPROM: el microcontrolador suspende la ejecución de instrucciones hasta que finaliza la operación de escritura en memoria flash. Una vez terminada ésta, reanuda la ejecución de instrucciones y atiende las interrupciones en caso de que hubiese alguna activa. La posibilidad de escribir en la memoria flash, más allá de almacenar datos en el espacio de memoria de programa, permite modificar parte del programa durante la ejecución del mismo para poder ejecutar código nuevo no incluído en el momento de programar del microcontrolador.
3.6.8.
Timers
Los timers (temporizadores) son dispositivos cuya función se limita a contar. De forma general, tienen dos modos de funcionamiento: reloj (incrementa un valor con cada ciclo de instrucción) y contador (el incremento se produce con un flanco de subida o bajada de un cierto pin del microcontrolador). El 18F4550 incorpora cuatro timers, cada uno con caracterísiticas propias. Todos ellos ofrecen la posibilidad de generar interrupciones cuando llegan al desbordamiento, es decir, cuando incrementan el valor máximo y pasan al mínimo. De manera resumida, éstas son las características propias de cada temporizador: El Timer0 puede funcionar en dos modos: reloj o contador. Ambos modos de funcionamiento pueden emplear palabras de cuenta de 8 y 16 bits. Como reloj, incrementa el contenido del par de registros TMR0H:TMR0L (sólo TMR0L si está trabajando en modo de 8 bits) con cada ciclo de instrucción (4 ciclos de reloj). En modo contador, incrementa TMR0 con cada flanco de subida (o bajada, según esté configurado) del pin RA4/T0CKI. En ambos casos es posible usar un preescalado que divida la frecuencia de incremento. Como hace mención el párrafo anterior, son los registros TMR0H y TMR0L los que almacenan la palabra de cuenta. Estos registros permiten lectura y escritura, con la particularidad de que una operación de escritura no tiene efecto hasta dos ciclos de instrucción después, por motivos de sincronización de cuenta. La configuración de este timer recae sobre el registro T0CON. El bit TMR0ON activa o detiene la cuenta. T08BIT selecciona entre los modos de cuenta de
Compilador de C para el microcontrolador Microchip PIC18F4550
45
8 y 16 bits. T0CS selecciona qué fuente de reloj emplear, cambiando de esta forma entre reloj interno (reloj de ciclos de instrucción) y contador (fuente de reloj externa, pin RA4/T0CKI). El pin PSA activa el preescalado cuando está puesto a 0. El valor de la división de frecuencia viene establecido por los bits T0PS[2:0]. El Timer1 ofrece las mismas posibilidades que el anterior, salvo que sólo admite palabras de cuenta de 16 bits, permite utilizar como señal de reloj un oscilador de cristal (común a Timer3, soporta modo asíncrono cuando está seleccionada la fuente de reloj externa y, además, es posible configurarlo para que reinicie la cuenta cuando alguno de los módulos comparadores (CCP, tratados en la sección siguiente) active un disparador de evento especial. Asimismo, este temporizador permite emplear como señal de reloj su propio oscilador interno, compuesto por un cristal de cuarzo externo conectado a los pines RC0/T1OSO/T13CKI y RC1/T1OSI. T1CON es el registro de configuración del Timer1. TMR1ON activa el funcionamiento del temporizador. TMR1CS selecciona entre el reloj de ciclos de instrucción y una fuente externa. T1SYNC puesto a 0 indica que la cuenta debe ser síncrona con la entrada de reloj externa. T1OSCEN es el bit de activación del oscilador de cristal del Timer1. Los bits T1CKPS[1:0] seleccionan la preescala de reloj a emplear; si están puestos a 00, no efectúa preescalado. El Timer2 es un temporizador de 8 bits con preescala y postescala. Sólo permite utilizar el reloj de ciclos de instrucción como señal de reloj. Incrementa el valor de TMR2, con cada flanco de subida del reloj. Cuando el valor de TMR2 coincide con el del registro PR2, genera la señal de salida del temporizador e incrementa en uno el valor del contador de postescalado. El control de este tercer temporizador recae sobre el registro T2CON. TMR2ON pone en marcha el contador. Los bits T2CKPS[1:0] son los que controlan la preescala de la cuenta, mientras que T2OUTPS[3:0] hacen lo propio con el valor de postescalado. El Timer3 es idéntico al Timer1. Ambos son temporizadores/contadores de 16 bits que pueden emplear la señal del reloj de ciclos de instrucción, los flancos de subida de una señal externa o del oscilador de cristal común, y efectuar la cuenta de manera síncrona o asíncrona a la señal de reloj seleccionada. El registro de cuenta de este temporizador es TMR3 y, al igual que con los otros tres temporizadores, permite lectura y escritura.
Compilador de C para el microcontrolador Microchip PIC18F4550
46
El registro de control de Timer3 es T3CON. TMR3ON pone en marcha el contador. TMR3CS selecciona entre el reloj de ciclos de instrucción y una fuente externa. T3SYNC puesto a 0 indica que la cuenta debe ser síncrona con la entrada de reloj externa. Los bits T1CKPS[1:0] seleccionan la preescala de reloj a emplear; si están puestos a 00, no efectúa preescalado. Además, el bit T1OSCEN del registro de control del Timer1 también forma parte del control del Timer3, dado que es el que activa el oscilador de cristal, compartido entre ambos temporizadores.
3.6.9.
Capturador, comparador y modulador de ancho pulso
Los microcontroladores PIC18F4550 incorporan dos módulos CCP, los cuales tienen tres modos de funcionamiento, como su nombre indica: captura, comparación y modulación de ancho de pulso (PWM). Los tres modos trabajan con valores de 16 bits. Los CCP trabajan asociados a un temporizador según el modo de funcionamiento configurado; los modos de captura y comparación pueden depender del Timer1 o del Timer3, mientras que el modo PWM sólo permite usar el Timer2. El registro de control de los CCP es CCPxCON. Los bits CCPxM[3:0] seleccionan qué modo de funcionamiento tendrá el CCP. Además de estos cuatro, los bits 5 y 4 de CCPxCON, denominados DCxB[1:0], son los bits de menor peso que indican la duración del pulso activo cuando este CCP esté configurado como modulador de ancho de pulso. En modo capturador, el CCP captura el valor del temporizador asociado en el momento que en detecta un evento en su pin CCPx de entrada. Este evento puede ser un flanco de bajada, de subida, o el cuarto o décimosexto flanco de subida de la señal de este pin. Hay que tener en cuenta que el temporizador asignado a un CCP en modo captura debe estar configurado como temporizador o como contador síncrono; de lo contrario el CCP no hará su trabajo. Como comparador, el CCP compara continuamente el valor del temporizador asignado con el del par de registros CCPRxH:CCPRxL. Cuando los valores coinciden, produce un evento programado previamente, que puede ser poner a uno o a cero el pin de salida CCPx, conmutar el valor de este mismo pin, generar una interrupción software o disparar un evento especial que actúe sobre otros módulos, como por ejemplo el temporizador o el conversor A/D. Como modulador de ancho de pulso, el CCP produce una onda cuadrada cuyo periodo viene indicado por el registro PR2 y cuya duración del pulso activo (alto) es escrita en el registro CCPRxL y los bits CCPxCON[5:4], siendo estos últimos los bits menos significativos de este valor. Como se ve, es un valor de 10 bits, por lo que
Compilador de C para el microcontrolador Microchip PIC18F4550
47
la palabra total es CCPRxL:CCPxCON[5:4]. Las fórmulas para calcular el periodo y ancho activo son las siguientes: P eriodo = (P R2 + 1) ∗ 4 ∗ TOSC ∗ (P reescala)
P ulso = CCP RxL : CCP xCON [5 : 4] ∗ TOSC ∗ (P reescala)
3.6.10.
Conversor A/D
El módulo de conversión analógico-a-digital permite la conversión de un valor analógico a uno digital de 10 bits, tomando un valor analógico de referencia como máximo y otro como mínimo. Tiene trece canales de entrada en los pines AN[12:0], los cuales están multiplexados sobre la entrada del conversor A/D. El valor de referencia máximo puede ser la tensión de alimentación positiva VDD o la tensión aplicada al pin RA3/AN3/VREF + . De manera análoga, el valor de referencia mínimo puede ser la tensión de alimentación negativa VREF − o la aplicada al pin RA2/AN2/VREF − . Este módulo permite, asimismo, la posibilidad de operar mientras el 18F4550 se encuentra en modo de reposo, con la limitación de que, para hacerlo, el reloj de conversión A/D debe derivar del oscilador RC interno del conversor. La configuración del conversor A/D recae sobre tres registros: ADCON0, ADCON1 y ADCON2. El registro de control ADCON0 contiene los bits de selección de canal, CHS[3:0]. Además es estos, incorpora el bit de estado de la conversión, GO, que inicia el proceso de conversión y permanecerá puesto a uno mientras ésta no haya terminado; una vez completada la conversión, este bit pasa automáticamente a valer cero. El bit ADON es el que habilita o deshabilita el módulo conversor. El registro ADCON1 contiene los bits para configuración de la tensión de referencia, VCFG1 para la referencia negativa y VCFG0 para la positiva, así como los bits de configuración de puertos A/D, PCFG[3:0]. Por último, ADCON2 contiene los bits de selección de reloj para la conversión A/D, ADCS[2:0]; los bits de selección de tiempo de adquisición, ACQT[2:0]; y el bit de formato para el resultado, ADFM, que indica si el resultado estará justificado a izquierda o derecha. Los registros donde el conversor almacena el resultado de la conversión son ADRESH y ADRESL. El resultado puede estar en ADRESH[1:0]:ADRESL[7:0] o, por el contrario, ADRESH[7:0]:ADRESL[7:6], según indique el bit ADFM. El resto de bits valdrán cero. Los pasos necesarios para leer un valor analógico utilizando el módulo A/D
Compilador de C para el microcontrolador Microchip PIC18F4550
48
pueden resumirse en los siguientes: 1. Configurar pines A/D para la entrada deseada. 2. Seleccionar tensiones de referencia. 3. Seleccionar fuente de reloj. 4. Seleccionar canal de entrada. 5. Habilitar módulo A/D. 6. Iniciar conversión (GO <- 1). 7. Espera activa mientras GO=1. 8. Resultado almacenado en ADRESH:ADRESL. Hay que añadir una precaución muy importante a la hora de efectuar el procedimiento descrito. La conversión A/D no puede activarse justo después de habilitar el módulo conversor. Es necesario esperar un tiempo para dejar que el condensador de mantenimiento, CHOLD , alcance la tensión que hay aplicada en el canal de entrada analógica. Este tiempo recibe el nombre de tiempo de adquisición, TACQ , y es la suma del tiempo de establecimiento del amplificador de entrada, el tiempo de carga de CHOLD y un coeficiente que depende de la temperatura. En el caso de emplear una impedancia de la fuente RS de 2.5KΩ, valor máximo recomendado para fuentes analógicas, un CHOLD de 25pF, y que la temperatura máxima de funcionamiento sea de 85◦ C, el tiempo de adquisición es de 2.45µs. Por consiguiente, antes de activar el bit GO de ADCON0 es necesario garantizar que han transcurrido al menos 2.45µs desde que se habilitó el módulo conversor A/D, o de lo contrario la conversión resultará en un valor incorrecto. Otra restricción de tiempo es el tiempo de conversión por bit, denominado TAD . El fabricante Microchip garantiza una conversión correcta para tiempos de conversión entre 1.4µs y 25µs. Por otro lado, el conversor del 18F4550 necesita 11 TAD para completar una conversión de 10 bits, entre 15.4 y 275µs, por lo tanto es necesario seleccionar un TAD comprendido en este rango de valores. El módulo A/D ofrece siete opciones posibles: 2*TOSC , 4*TOSC , 8*TOSC , 16*TOSC , 32*TOSC , 64*TOSC y el oscilador interno RC, cuyo periodo típico es de 2.5µs. El cuadro 3.6 muestra los tiempos de las fuentes de reloj para distintas frecuencias de funcionamiento del microcontrolador. Dado que el tiempo mínimo de TAD es de 1.4µs, los valores menores (entre paréntesis en la tabla) no permiten una conversión correcta. La mejor opción es elegir
Compilador de C para el microcontrolador Microchip PIC18F4550 Fuente de reloj (valor de TAD ) 2*TOSC 4*TOSC 8*TOSC 16*TOSC 32*TOSC 64*TOSC RC
ADCS[2:0]
20 MHz
4 MHz
1 MHz
000 100 001 101 010 110 x11
(0.1µs) (0.2µs) (0.4µs) (0.8µs) 1.6µs 3.2µs 2-6µs
(0.5µs) (1µs) 2µs 4µs 8µs 16µs 2-6µs
2µs 4µs 8µs 16µs 32µs 64µs 2-6µs
49
Cuadro 3.6: Tiempos según fuente de reloj de TAD en función de la frecuencia del microcontrolador
la fuente que ofrezca el menor tiempo de conversión posible y que éste no sea inferior al mínimo impuesto por el hardware. Por ejemplo, para el caso de un sistema con un reloj de cristal de 4MHz es posible elegir cinco configuraciones posibles, 8*TOSC , 16*TOSC , 32*TOSC , 64*TOSC y RC, dado que las dos opciones restantes no aseguran un tiempo de conversión superior al mínimo de 1.4µs. De estas cinco opciones, lo más adecuado en la mayoría de los casos será escoger 8*TOSC , que es la fuente de reloj que produce el TAD válido más pequeño. No obstante, es posible escoger 8*TOSC , o cualquier otra fuente de periodo mayor, la cual también llevaría a una conversión A/D correcta pero con un mayor gasto de tiempo.
3.6.11.
Comparador Analógico
El módulo comparador analógico incluye dos comparadores cuyas entradas y salidas permiten ocho configuraciones diferentes, desde estar todas desconectadas, dejando desactivados ambos comparadores, hasta modos complejos, como el de cuatro entradas multiplexadas dos a dos. Por supuesto, también es posible la configuración más obvia de dos comparadores independientes. El registro CMCON es el que dicta qué configuración empleará este módulo. La selección entre los ocho modos de funcionamiento es indicada por los bits CM[2:0]. Los bits C1OUT, C2OUT, C1INV y C2INV permiten controlar la lógica de comparación y salida de cada comparador por separado. Este registro también incluye un bit para controlar el multiplexor de las entradas cuando el módulo está configurado para entradas multiplexadas.
Compilador de C para el microcontrolador Microchip PIC18F4550
3.6.12.
50
Módulo de Tensión de Referencia
El propósito de este módulo es generar una tensión exacta que pueda utilizarse como referencia por el propio microcontrolador u otros dispositivos que requieran una tensión de referencia exacta. La ventaja de disponer de un módulo como éste incorporado en el microcontrolador es, además de eliminar la necesidad de circuitería externa, la posibilidad de utilizarlo como referencia para varios dispositivos cuyo funcionamiento no se solape en el tiempo. La tensión generada por este dispositivo puede ser relativa a VDD , a VSS o a una tensión de referencia externa, con lo que la flexibilidad que otorga este módulo no deja lugar a dudas. Al igual que ocurre con el comparador analógico, un único registro de control es suficiente para albergar toda la configuración. El nombre de este registro es CVRCON.
3.6.13.
Otros
Además de los dispositivos ya vistos, los microcontroladores pueden incorporar periféricos de otro tipo. Debido a la gran diversidad de periféricos existentes no es posible disponer de todos ellos en un modelo único de microcontrolador. Algunos de los dispositivos que es posible encontrar en otros modelos de microcontrolador, aparte de los ya descritos para el PIC18F4550, son: Conversor A/D de rampa. Es un modelo de conversor analógico-a-digital diferente al que incorpora el 18F4550. En lugar de efectuar un bucle de comparación y aproximaciones sucesivas, este dispositivo mide el tiempo de carga de un condensador y, a partir de ahí, calcula el valor de la tensión aplicada a la entrada (bornas del condensador). Este tipo de conversores son más lentos que los basados en el método de aproximaciones sucesivas pero también son más precisos. Módulo LCD. Este tipo de módulos facilita el manejo de pantallas LCD desde un PIC. Generalmente, su uso se reduce a configurar el tiempo de refresco y definir los registros que servirán como memoria gráfica. Una vez hecha la configuración, sólo hay que escribir en la memoria gráfica los datos que deberán aparecer en la pantalla.
Compilador de C para el microcontrolador Microchip PIC18F4550
3.7.
Características especiales de la CPU
3.7.1.
Interrupciones
51
El PIC18F4550 tiene hasta 19 fuentes de interrupción, 16 generadas por los periféricos integrados y 3 señales de interrupción entrantes desde dispositivos externos. Por supuesto, no es necesario que todas ellas estén habilitadas. Además, es posible agrupar las interrupciones en dos niveles de prioridad, alta o baja, o conservar el modo de compatibilidad en el que todas las interrupciones tienen la misma prioridad, tal como sucede en los PIC de gama media. Diez registros son los responsables del control de interrupciones en este microcontrolador: RCON INTCON INTCON2 INTCON3 PIR1 y PIR2 PIE1 y PIE2 IPR1 y IPR2 Cada fuente de interrupción tiene tres bits que controlan su funcionamiento individual: Bit de activación o Flag bit. Indica que se produjo la interrupción. Bit de habilitación. Si está activado, la CPU ejecutará la rutina de tratamiento de interrupción (ISR) correspondiente cuando ésta interrupción se produzca. Bit de prioridad. Selecciona a qué nivel de prioridad pertenece esta interrupción. Existen dos vectores de interrupción cuando la prioridad de interrupciónes está habilitada. El vector que da servicio a las interrupciones de alta prioridad se encuentra en la posición 0x000008h de la memoria de programa. Por otro lado, la dirección donde se aloja el vector asociado a las interrupciones de prioridad baja es 0x000018h.
Compilador de C para el microcontrolador Microchip PIC18F4550
52
A pesar de disponer de dos vectores de interrupción, la gestión de interrupciones no es vectorizada: es necesario efectuar un polling por software para determinar qué dispositivo fue el que activó la señal de interrupción. Cada rutina de tratamiento de interrupción debe finalizar con la instrucción RETFIE, que recupera de la pila el valor de PC en el momento de producirse la interrupción. Es responsabilidad del programador incorporar el código adecuado para salvaguardar los valores de los registros modificados durante la rutina de atención, incluído el registro WREG, para restaurarlos antes de ejecutar la instrucción RETFIE. El registro de control INTCON alberga dos de los tres bits más importantes del control general de interrupciones: GIE/GIEH y PEIE/GIEL. El primero de ellos habilita las interrupciones de prioridad alta, o todas las interrupciones si el controlador de interrupciones está puesto en modo de compatibilidad. El segundo bit habilita las interrupciones de prioridad baja, salvo en modo de compatibilidad, cuya función es habilitar las interrupciones generadas por los periféricos integrados en el PIC. El tercer bit esencial para control general del controlador de interrupciones es el octavo bit del registro RCON, PIEN, que habilita los niveles de prioridad de las interrupciones. Aparte de los ya descritos, el registro INTCON, junto con INTCON2 e INTCON3, alberga los bits de habilitación, activación y prioridad de las entradas de interrupción externas, el Timer0 y el puerto B. Los registros PIE1 y PIE2 alojan los bits de habilitación de las interrupciones generadas por los periféricos del PIC. PIR1 y PIR2, por su parte, están compuestos por los bits de activación de interrupciones, mientras que IPR1 e IPR2 contienen los bits de prioridad de cada periférico. Hasta este momento, la única manera de controlar dispositivos era programarlos para que comenzasen a funcionar y efectuar una espera activa, comprobando una y otra vez los bits de estado, hasta que el trabajo hubiese terminado. Ahora, haciendo uso del controlador de interrupciones es posible configurar un periférico, activar la interrupción adecuada y ponerlo a funcionar mientras la CPU efectúa otras tareas, con la seguridad de que cuando el periférico haya completado su trabajo, la interrupción captará la atención del microcontrolador. De esta manera, el tiempo que antes era desperdiciado en la espera activa ahora pasa a ser tiempo invertido en otras tareas más provechosas. Por ejemplo, para la transmisión del EUSART, es posible preparar una ISR en la dirección 0x000008h que compruebe si el bit TRMT del registro TXSTA está puesto a uno y, en caso afirmativo, ejecute una acción, y activar la interrupción de transmisión del EUSART.
Compilador de C para el microcontrolador Microchip PIC18F4550
3.7.2.
53
Perro Guardián
El dispositivo perro guardián, más conocido por sus siglas en inglés WDT (WatchDog Timer), es un temporizador que utiliza el oscilador RC interno del microcontrolador. Como el oscilador RC es independiente del reloj utilizado por el micro, este temporizador sigue contando aunque el PIC esté en modo de reposo. La misión del WDT es producir, cuando llega al final de su cuenta, el reinicio del microcontrolador cuando éste se encuentra en modo normal, o reactivarlo si está en modo de reposo.
3.7.3.
Modo de reposo
El modo de reposo es un modo donde los osciladores y temporizadores del PIC están detenidos, con lo que la ejecución del programa también queda suspendida. Las únicas excepciones son el oscilador interno RC, en el caso de que el WDT esté activo, y el Timer1, si se encuentra funcionando en el momento de entrar en modo de reposo. Los eventos que despiertan al microcontrolador son el desbordamiento del contador del WDT, una señal de interrupción proveniente de alguno de los periféricos que no dependen de ningún oscilador distinto al RC, una interrupción que provenga del exterior, o un reset del sistema.
3.7.4.
ICSP
ICSP son las siglas de In-Circuit Serial Programming. El interface ICSP permite programar el PIC estando ya montado en el circuito de aplicación final, sin necesidad de desconectarlo del mismo. Está compuesto por cinco líneas, las cuales deben cumplir unas restricciones determinadas. Las tensiones de alimentación VDD y masa VSS serán las mismas que las utilizadas para el funcionamiento habitual del PIC. La tensión de programación VP P será de al menos 13V; esta tensión aplicada al pin MCLR del microcontrolador hace que entre en modo programación. Por último, las señales de reloj (PGC) y datos (PGD) deberán alcanzar los valores de VP P y VSS
3.8.
Características eléctricas
De manera general, las características del PIC18F4550 permiten un amplio rango de aplicaciones. Según el datasheet, este microcontrolador puede funcionar en un rango de temperaturas comprendido entre -40◦ C y 85◦ C. Estos valores son los límites que no deberán superarse durante la operación del PIC. No cumplir las limitaciones de
Compilador de C para el microcontrolador Microchip PIC18F4550
54
temperatura puede resultar en una ejecución incorrecta del programa o incluso la destrucción física del dispositivo. Es conveniente recordar que la temperatura no solo puede dañar el microcontrolador, sino que afecta cuantitativamente al comportamiento del mismo, pudiendo variar, por poner un ejemplo, la frecuencia de oscilación del oscilador empleado por el conversor A/D, dando lugar a un posible fallo en la conversión. El fabricante Microchip distribuye series especiales de microcontroladores para aplicaciones militares, los cuales soportan condiciones de funcionamiento más extremas que las series estándar. El rango de tensiones soportado por cualquier pin comprende aquellas tensiones entre 0.3V por debajo de VSS y VDD +0.3V por encima de VSS . Por ejemplo, si VSS está contectado a 0.2V, una entrada a nivel bajo no deberá ser inferior a -0.1V; suponiendo que VDD esté conectado a 5V, la tensión aplicada a una entrada para un nivel alto no deberá exceder de 5.5V. Sólo dos pines tienen un rango distinto al descrito en el párrafo anterior: VDD y MCLR. El pin de alimentación VDD admite un rango de tensiones que va desde 0.3V por debajo de VSS hasta 7.5V por encima. Por otro lado, el rango de tensiones soportado por MCLR va desde VSS hasta 13.25V por encima de éste. Nótese que estos rangos de tensiones son los valores que admite el microcontrolador sin llegar a dañarse físicamente, no los valores necesarios para su buen funcionamiento. Por ejemplo, VDD admite desde VSS -0.3V hasta VSS +7.5V, pero para funcionar es necesario que VDD esté comprendido entre 4.2V y 5.5V. La corriente que puede suministrar o consumir por los pines de entrada y salida es de 25 mA. Esta es suficiente para iluminar leds, y comunicarse con otros integrados. Para poder manejar corrientes mayores es necesario emplear transductores externos, dado que un exceso de corriente a través de los pines puede destruir el microcontrolador.
Compilador de C para el microcontrolador Microchip PIC18F4550
Símbolo
VIL
VIH
VOL VOH
Pin Pin E/S con buffer TTL Pin E/S con buffer Trigger Schmitt MCLR OSC1 y T1OSI (en modos XT, HS y HSPLL) OSC1 (en modo EC) Pin RB0 y RB1 (en modo I2 C) Pin E/S con buffer TTL Pin E/S con buffer Trigger Schmitt MCLR OSC1 y T1OSI (en modos XT, HS y HSPLL) OSC1 (en modo EC) Pin RB0 y RB1 (en modo I2 C) Pin E/S OSC2/CLKO (en modo EC e ECIO) Pin E/S OSC2/CLKO (en modo EC, ECIO e ECPIO)
Mínimo VSS VSS VSS VSS VSS VSS 2V 0.8 VDD 0.8 VDD 0.7 VDD 0.9 VDD 0.7 VDD VDD -0.7V VDD -0.7V
55
Máximo 0.15 VDD 0.2 VDD 0.2 VDD 0.3 VDD 0.2 VDD 0.3 VDD VDD VDD VDD VDD VDD VDD 0.6V 0.6V -
Cuadro 3.7: Niveles de tensión para el microcontrolador PIC18F4550
Capítulo 4 GNU Compiler Collection 4.1.
Historia de GCC
Año 1983, 27 de septiembre. Llega al grupo net.unix-wizards de Usenet un mensaje que comienza exclamando “¡Unix libre!”. El autor de este correo es un joven de Massachusetts llamado Richard M. Stallman. En él, anunciaba su proyecto de desarrollar un sistema operativo tipo Unix completamente libre. “Hará falta ayuda en forma de tiempo, dinero, programas y equipos”. De esta manera nacía el proyecto GNU, cuyo objetivo era proporcionar un sistema operativo libre para usuarios y desarrolladores. Los sistemas Unix están íntimamente relacionados con el lenguaje C, por lo que todos ellos disponen de al menos un compilador de este lenguaje. Dado que no existía ningún compilador libre, Stallman decidió crear uno desde cero que sirviese para su propósito. Todo este trabajo fue posible gracias al esfuerzo de muchas personas y la financiación tanto de personas individuales como de la Fundación del Software Libre, más conocida por sus siglas en inglés, FSF. Richard Stallman decidió crear la FSF en 1985 con el propósito de proporcionar soporte logístico, financiero y legal al proyecto GNU. En el año 1987 apareció la primera versión del GNU C Compiler. Esta versión marcó un antes y un después en la historia del software, ya que se trataba del primer compilador de lenguaje C completamente libre. Desde ese momento GCC es una de las herramientas de desarrollo de software más importantes. La versión 2.0 vió la luz en 1992. Fue la primera gran revisión de GCC, e incorporó cambios en su arquitectura que permitieron la inclusión de mejoras que eran imposibles en la rama 1.x. Durante el tiempo de vida de la rama 2.x la FSF mantuvo un estricto control so-
56
Compilador de C para el microcontrolador Microchip PIC18F4550
57
bre las mejoras de GCC. A muchos desarrolladores les resultaba frustrante lo difícil que resultaba que la FSF incluyese sus mejoras en la versión oficial del compilador. Por este motivo, en 1997, un grupo de desarrolladores insatisfechos con el lento desarrollo de GCC optó por iniciar un proyecto a partir de la versión de desarrollo del GCC oficial. Este proyecto bifurcado de GCC recibió el nombre de Sistema Compilador de GNU Experimental/Mejorado (Experimenta/Enhanced GNU Compiler System), EGCS), e incorporaba infinidad de mejoras ajenas a las de la FSF y que no habían sido incluidas en GCC. El desarrollo de EGCS fue mucho más rotundo que el de GCC hasta el punto de que, en 1999, la FSF optó por dejar de desarrollar GCC y bendecir EGCS como versión oficial de GCC. El primer nuevo GCC oficial fue GCC 2.95. A día de hoy, GCC ha sido ampliado para soportar muchos lenguajes adicionales, incluyendo C++, Objetive-C, Java y ADA entre muchos otros. Puesto que GCC ya no es sólo un compilador de C, su nombre ha pasado a ser GNU Compiler Collection (Colección de Compiladores de GNU). Las decisiones sobre el camino a seguir en el desarrollo de GCC, en la actualidad, las toma el GCC Steering Committee, un grupo formado por desarrolladores e ingenieros de todo el mundo.
4.2.
Características
GCC es un compilador portable. Se ejecuta en la mayoría de plataformas actuales y puede producir código para un gran número de arquitecturas. Además de los procesadores usados en PCs, soporta microcontroladores, DSPs y CPUs de 64 bits. GCC no es sólo un compilador nativo, puede realizar compilación cruzada de cualquier programa, produciendo ejecutables para diferentes sistemas desde cualquiera que use GCC. Esto permite compilar software para sistemas empotrados que no tengan la capacidad de ejecutar el compilador. GCC esta escrito principalmente en C, con el objetivo de ser portable, y puede compilarse a si mismo para adaptarse a nuevos sistemas sin dificultad. GCC tiene un diseño modular que le permite soportar nuevos lenguajes y arquitecturas. Para soportar un nuevo lenguaje es suficiente con incluir un front-end compatible que traduzca dicho lenguaje y añadir las bibliotecas necesarias en tiempo de ejecución para los programas escritos en el lenguaje en cuestión. De manera análoga, para soportar una nueva arquitectura basta con tener un back-end que produzca código ensamblador para ésta. En la actualidad, GCC dispone de múltiples front-ends, para traducir diferentes lenguajes, así como diversos back-ends para soportar múltiples arquitecturas computacionales diferentes.
Compilador de C para el microcontrolador Microchip PIC18F4550
58
Por último, GCC es software libre, distribuido bajo Licencia Pública General de GNU (General Public License, GPL). Esto significa que cualquier persona tiene la libertad de poder usarlo y modificarlo para cualquier propósito. Si se necesita soporte para una nueva arquitectura, lenguaje, o una característica aún no incluída, la GPL garantiza la libertad para incluir dichas modificaciones e incluso publicarlas, con la única condición de conservar la licencia en el software publicado. El resultado más destacable de esta libertad es que cualquier persona puede beneficiarse de las mejoras introducidas por terceras partes. GCC es un buen ejemplo de cómo el trabajo cooperativo beneficia a toda la humanidad.
4.3.
Estructura
La interface externa es la propia de cualquier programa de línea de comandos. El usuario invoca un programa controlador, llamado gcc, que interpreta los argumentos, decide qué compilador utilizar con cada archivo de entrada, ejecuta el ensamblador para la arquitectura objetivo y, posiblemente, termina ejecutando el programa enlazador. Cada uno de los compiladores es un programa independiente que recibe como entrada uno o varios archivos de código fuente y genera un código en lenguaje ensamblador. Cada compilador no es más que una combinación de módulos internos de GCC. El pipeline de GCC está compuesto, a grandes rasgos, por tres etapas: Front-end El front-end es la primera etapa del proceso de compilación. En esta etapa el código fuente de entrada se convierte en una estructura de árbol sintáctico optimizada según el lenguaje de origen. Middle-end Es la etapa intermedia, donde el árbol sintáctico recibe optimizaciones independientes de la arquitectura y el lenguaje, y se transforma en un código intermedio independiente del lenguaje inicial y de la arquitectura objetivo. Back-end Esta es la etapa final. En ella, el código intermedio generado por el middleend recibe optimizaciones específicas para la arquitectura objetivo y finalmente se genera el código ensamblador.
4.3.1.
Front-end
Los front-end son diferentes para cada lenguaje de entrada, pero todos generan árboles sintácticos que el middle-end puede manejar. Todos ellos implementan analizadores gramaticales LALR(1).
Compilador de C para el microcontrolador Microchip PIC18F4550
59
Internamente, el front-end ejecuta la fase de preprocesado, si la hubiese, y efectúa el análisis sintáctico, del que resulta una estructura de árbol sintáctico en lenguaje GENERIC. Tras diversos procesos de optimización dependientes del lenguaje de entrada, este código GENERIC pasa por un proceso de simplificación, denominado gimplificación. Producto de este proceso es la representación en lenguaje GIMPLE del código original de entrada. Esta estructura en GIMPLE es la que recibe el middle-end.
4.3.2.
Middle-end
El middle-end está compuesto por diversas fases de optimización independientes tanto del lenguaje de entrada como de la arquitectura objetivo. Estas optimizaciones incluyen desplegado de bucles, propagación de expresiones constantes, supresión de la recursión de cola, eliminación de redundancias, eliminación de código que no se ejecuta, optimización de bucles y descomposición de las estructuras correspondientes al manejo de excepciones de alto nivel en sus equivalentes en forma de sentencias de control de flujo. Tras todas las fases de optimización, el código GIMPLE ya optimizado se convierte al lenguaje de transferencias de registros (RTL) específico de GCC. Este lenguaje, guarda un gran parecido con el lenguaje Lisp. Es muy expresivo, y ha sido diseñado para que resulte sumamente fácil traducirlo a código ensamblador de cualquier procesador, con independencia de la arquitectura de éste.
4.3.3.
Back-end
Los back-end son específicos de cada arquitectura. Cada uno produce código ensamblador para una arquitectura objetivo diferente pero todos ellos toman como entrada un código RTL, que es independiente de la arquitectura y, por supuesto, del lenguaje de entrada. El comportamiento del back-end viene parcialmente especificado por las macros del preprocesador específicas de la arquitectura objetivo. Por ejemplo, existen macros para definir el tamaño de palabra, la convención de llamadas a sub-rutina o la endianness que utilice la máquina que ejecute el programa compilado. El back-end hace uso de estas macros para adaptar el código RTL inicial a la arquitectura objetivo y efectuar optimizaciones dependientes de la misma. Por último, el back-end empareja, una a una, las expresiones RTX del código RTL con los patrones de código ensamblador dados en la descripción de máquina correspondiente a la arquitectura objetivo y concluye el proceso de compilación con la sustitución de cadenas simbólicas por direcciones físicas de memoria o registros del procesador.
Capítulo 5 Diseño de la solución Registros, memoria, operaciones matemáticas y control de flujo son los cuatro pilares maestros sobre los que recae el peso de este proyecto. Una vez diseñados, permitirán trabajar de manera coherente con la especificación de la arquitectura. El primer paso es definir estos cuatro puntos. Una vez tomadas las decisiones de diseño fundamentales, la creación de patrones de instrucción resulta semejante a programar pequeños fragmentos de código ensamblador para la arquitectura final.
5.1.
Registros
Los PIC tienen un único registro como tal, conocido como registro de trabajo o WREG. Una primera solución es indicar a GCC en la descripción de la arquitectura que la máquina objetivo sólo dispone de un registro físico. Teniendo en cuenta que el compilador hace un uso intensivo de un lenguaje intermedio basado en registros (RTL) y que utiliza tantos registros1 como considera necesario, esta idea no parece una buena solución. Si se optara por este diseño, los códigos generados por GCC resultarían muy ineficientes, debido al exceso de operaciones de salvado y recuperación del valor del único registro disponible. La segunda idea considerada es diametralmente opuesta a la anterior. Partiendo de que cada posición de memoria RAM de un PIC recibe el nombre de registro, GCC podría considerar que tiene a su disposición tantos registros como posiciones de memoria RAM haya. Sin embargo, el código generado por este compilador, además de registros, necesita memoria para almacenar variables y salvaguardar valores intermedios, entre otras cosas, y esta solución implica que no quede RAM disponible para dedicar como memoria. 1 Estos registros no son registros físicos del procesador, sino contenedores abstractos de datos que GCC considera que tienen las características de registros físicos.
60
Compilador de C para el microcontrolador Microchip PIC18F4550
61
Antes de optar por una u otra solución, hagamos una breve estimación del uso de los registros. Para sumar dos valores enteros, de 16 bits, harán falta seis registros, de ocho bits: dos registros cada sumando y dos más para almacenar el resultado. En el caso de que la operación de suma sobreescriba uno de los dos operandos, sólo serán necesarios cuatro registros. La operación de suma de dos números reales de 32 bits cada uno emplea, en el caso más conservador que no conlleve sobreescritura, un total de doce registros; ocho si uno de los sumandos resulta sobreescrito por el resultado. Hasta ahora parece razonable considerar un mínimo de ocho registros, lo que descarta por completo la primera solución. Por otro lado, el grueso de operaciones, tal como las ve el back-end, operan con registros; lo que puede llevar a pensar que la segunda solución, que considera toda la memoria física del microcontrolador como registros, es la más adecuada. Sin embargo, esta solución también resulta inviable debido a la necesidad de memoria aparte de los registros, de lo que deriva la idea de establecer un límite superior en el número de registros físicos que GCC puede utilizar. Planteadas estas restricciones, la mejor solución es un compromiso entre el límite inferior de ocho regisros y el superior, que no deja RAM libre para que el compilador considere memoria. El registro físico WREG será transparente para GCC, de modo que, a pesar de que el código final en ensamblador lo utilizará de manera intensiva, el compilador no tendrá en cuenta su existencia. Así, la solución final al problema de los registros utilizará un máximo de 32 registros, número que deja suficientes posiciones de RAM disponibles para otros usos y es lo bastante alto como para permitir una optimización de operaciones consecutivas más que razonable.
5.2.
Memoria
GCC accede a memoria2 mediante direccionamiento indirecto, haciendo uso de punteros almacenados en registros. El principal puntero a memoria es el frame pointer. El propósito de este puntero es direccionar el marco de memoria donde se almacenan las variables locales de una función. La memoria, al igual que los registros, para GCC será una porción de la RAM del PIC. Además de registros y memoria, habrá un espacio en RAM asignado para la pila virtual y varios registros de propósito específico para el control del programa. Tras eliminar estas porciones de RAM asignadas de antemano, el espacio restante será asignado a memoria. 2
Lo que GCC considera memoria, en el caso de la arquitectura PIC y nuestro back-end, es el mismo espacio de memoria de datos que el utilizado para lo que GCC considera registros, aunque se trate de porciones disjuntas de la memoria de datos del PIC.
Compilador de C para el microcontrolador Microchip PIC18F4550
62
La arquitectura del PIC18 divide el espacio de memoria de datos en bancos de 256 bytes, a los que hay que restar el espacio designado para gestión de memoria, por lo que resulta un máximo de 252 bytes por cada banco, con la excepción del banco 0, cuyas posiciones inferiores corresponden también al banco de acceso. No es posible designar el espacio asignado a memoria por el compilador, ya que éste depende de la asignación de RAM a espacio de pila y control de programa. Sólo existe la seguridad de que el máximo es de 252 bytes por banco, un total de 1768 bytes en el PIC18F4550. Cuando GCC traduce una llamada a una función, conoce de antemano la cantidad de memoria necesaria para almacenar las variables empleadas por la misma. Esta memoria es la denominada marco (en inglés frame) de la función. Una vez conocido este detalle estamos en posición de afirmar que una buena estrategia de selección de banco haría más eficiente el uso de la RAM por parte de los programas compilados con GCC. Sin embargo, a pesar de escoger una estrategia óptima, siempre existe la posibilidad de que ninguno de los bancos disponga de suficientes posiciones libres para almacenar el marco de una función concreta. Llegado a este caso, la única solución es rediseñar el programa que se quiere compilar. La solución tomada para la asignación de RAM como memoria emplea dos palabras de memoria RAM por cada banco para almacenar el tamaño de éste y el espacio utilizado del mismo hasta el momento. Al invocar una función, busca cual de los bancos dispone de espacio suficiente para albergar el marco de la función, efectuando la diferencia entre espacio total y espacio ocupado. En caso de que alguno de los bancos satisfaga los requisitos de espacio libre, actualiza el espacio usado e inicializa el puntero a marco de función (frame pointer) para que apunte al inicio del espacio reservado en ese banco. Al finalizar la función actualiza la variable de espacio usado del banco. Teniendo en cuenta que con cada llamada a función se pierde el frame pointer correspondiente al marco de la función invocadora, es necesario salvar el mismo cada vez que se efectúe una llamada a función y restaurarlo al salir de cada función invocada. Para esto se emplea la pila virtual. GCC está diseñado de forma que minimice los accesos a memoria. Siempre que le sea posible, mantendrá los valores de variables locales en registros y no en memoria, ahorrando de este modo los costosos accesos a memoria y, en caso de conseguirlo con todas las variables, la reserva de espacio.
5.3.
Paso de argumentos a funciones
En el paso de argumentos a funciones es posible clasificar los datos en dos tipos: simples y compuestos. Los tipos de datos simples son los caracteres, y los valores
Compilador de C para el microcontrolador Microchip PIC18F4550
63
enteros y reales, mientras que los datos compuestos son los vectores, estructuras y uniones. Los argumentos que sean datos simples, por su tamaño reducido y su naturaleza sencilla se pasarán almacenados en registros. La asignación de registros para tal efecto comenzará por el último registro y el almacenamiento será en orden inverso, correspondiendo el primer dato al último registro; el segundo, al penúltimo y así sucesivamente. De esta forma la posibilidad de solapamiento entre registros para uso general y registros designados para paso de argumentos se ve reducida notablemente. A modo de ejemplo, supongamos que una función toma dos argumentos, uno de tipo char y otro de tipo int. Según esta solución, el registro número 32 albergará el carácter, mientras que el registro 31 almacenará el byte bajo del entero, y el 30 hará lo propio con el byte alto del mismo parámetro. El paso de argumentos de tipo compuesto será siempre por referencia. En lugar del valor almacenado en registros, la función recibirá un puntero a la dirección de memoria donde está alojado el primer byte del dato pasado como argumento. Así, un vector de n elementos pasado como parámetro sólo ocupará dos registros de la lista de argumentos de la función en cuestión.
5.4.
Retorno de valores de funciones
La solución ideada para el retorno de valores es semejante al paso de argumentos, con la diferencia de que la asignación de registros destinados a este propósito comienza en la mitad de la lista de registros y continúa en sentido ascendente. De este modo, el dato devuelto por una función estará almacenado a partir del registro 16. Igual que sucede con el paso de argumentos a una función, el retorno de valores devueltos distingue dos casos según la clase del dato devuelto. Para datos simples, el valor devuelto será alojado en registros a partir de la mitad de la tabla de registros posibles. En cambio, cuando una función devuelve un valor de tipo compuesto la solución no es tan sencilla. El retorno de valores de tipo compuesto será por referencia, en lugar de por valor. Esta decisión resulta en un código más eficiente, ya que ahorra el tener que duplicar un valor complejo en el momento de finalizar una función. No obstante, esta mejora no es gratuita: para que la referencia sea accesible por la función llamante es necesario pasar un argumento extra a la función llamada que será un apuntador a la posición de memoria donde esperamos encontrar el dato resultante una vez completada la ejecución de la función invocada. El valor devuelto por la función será un puntero a los datos del resultado.
Compilador de C para el microcontrolador Microchip PIC18F4550
64
Como ejemplo a las ideas presentadas, si una función recoge un carácter como argumento y devuelve un entero, el argumento se pasará a la función en el registro 32, y encontraremos el resultado en los registros 16 y 17 una vez completada su ejecución. Sin embargo, si la misma función con un carácter como argumento devuelve un tipo complejo, tal como una estructura, la función recogerá en los registros 31 y 32 un apuntador a una posición de memoria, junto con el carácter en el registro 30, y tras su ejecución devolverá el puntero en los registros 16 y 17.
5.5.
Variables globales y estáticas
La reserva del espacio para almacenar las variables globales y las estáticas, al contrario que en la gestión de memoria, no se realiza sólo en el fichero fuente principal sino que puede darse en cualquier lugar en el que puedan ser declaradas. Por tanto, las variables estáticas y globales definidas en los ficheros secundarios ocupan un tamaño de memoria desconocido al compilar el fichero fuente principal. Por este motivo, siempre que nuestra conversión del GCC trabaje con variables globales o estáticas en archivos que no contengan la función main, será necesario que el usuario indique al compilador, por linea de comandos, que reserve la cantidad de espacio necesario para estas variables cuando ejecute la compilación del archivo principal. Esta práctica es una mejora sobre los compiladores de microcontroladores, incluido el compilador privativo de este microcontrolador en particular, ya que en casi todos los compiladores para microcontroladores sólo está permitido declarar variables globales en el archivo principal, mientras que GCC permite declararlas en cualquier archivo de los que compongan el programa.
5.6.
Flujo de ejecución (memoria de código)
La ejecución de un programa (no trivial) no es totalmente lineal. GCC producirá saltos en la ejecución del código en muchas instrucciones diferentes. Podemos catalogar los saltos en tres clases: saltos incondicionales, saltos condicionales y llamadas a función. Tanto los saltos como las llamadas tienen varias peculiaridades que hemos de tener en cuenta. Comenzaremos con la memoria de programa de un PIC. Los microcontroladores PIC18 emplean un contador de programa (PC) de 21 bits, el cual permite direccionar 2M posiciones de memoria de programa. El PC es un registro que almacena la dirección de la próxima instrucción a ejecutar. Este puntero consta de tres registros de 8 bits de los cuales sólo se accede directamente a los ocho bits de menor significado
Compilador de C para el microcontrolador Microchip PIC18F4550
65
a través del registro conocido como PCL (PC low). Las partes alta y superior de la dirección pueden accederse mediante los registros PCLATH y PCLATU. Estos registros se comportan como registros convencionales que permiten lectura y escritura, sin embargo, los valores que almacenan no pasan al PC hasta que se efectúa una escritura en el registro PCL. Por tanto, los pasos para cargar un nuevo valor en el PC son escribir los bits más significativos en PCLATU y PCLATH, y una vez hecho esto, escribir los ocho bits menos significativos en PCL. La posibilidad de escribir valores arbitrarios en PC ofrece al desarrollador todo lo necesario para poder efectuar saltos computados (computed gotos en inglés). La técnica de saltos computados permite, por ejemplo, hacer uso de valores constantes para inicializar variables de nuestro programa (no olvidemos que estos microcontroladores se programan escribiendo únicamente la memoria de programa) y fué uno de los métodos considerados durante el diseño de este proyecto. Las instrucciones de salto incondicional modifican el registro PC a partir del valor inmediato especificado en su operando. Las palabras clave de la sentencia anterior son valor inmediato, porque es un valor especificado en el código de operación de la instrucción, y modifican. La explicación a esta última reside en que cada instrucción de salto incondicional tiene un comportamiento distinto. La instrucción GOTO tiene el comportamiento tradicional de salto a cualquier posición de la memoria y, debido al tamaño de palabra de la memoria de programa y el número de posiciones direccionables, esta instrucción se compone siempre de dos palabras (32 bits). En cambio, el comportamiento de la otra instrucción de salto incondicional, BRA, es análogo a los saltos cortos del modo real en la arquitectura x86: el operando inmediato indica un salto relativo al PC, ya sea en sentido positivo o negativo. Debido al diseño de la arquitectura PIC18, concretamente su manera de direccionar la memoria de programa a través del PC, los saltos resultantes de ejecutar la instrucción BRA cargan en el PC un valor PC+2+2n (siendo n el valor en complemento a 2 de los 11 bits menos significativos de la palabra de instrucción). Por esta limitación y por la dificultad de predecir la distancia de los saltos, nuestro back-end únicamente hace uso de BRA para saltos internos a los bloques de código resultantes de traducir instrucciones individuales del lenguaje RTL intermedio; las instrucciones RTL que corresponden saltos en el programa escrito en C siempre resultarán en instrucciones GOTO, con el consumo de memoria extra que estas instrucciones suponen y que el desarrollador deberá tener en cuenta cuando escriba su código C. El tipo de salto que efectúan las instrucciones de salto condicional, con la peculiaridad de que el PC se modifica en caso de satisfacerse una condición, es similar al de la instrucción BRA de salto incondicional: el valor que se carga en el PC es, de nuevo, PC+2+2n. En el caso de las instrucciones de salto condicional, el rango
Compilador de C para el microcontrolador Microchip PIC18F4550
66
de direcciones de memoria al que es posible saltar es mucho más pequeño que el admitido por la instrucción BRA, debido a que el tamaño del operando inmediato es de sólo 8 bits, frente a los 11 bits del mismo operando para BRA. La tercera clase de salto, denominada llamadas a función, está compuesta por la instrucción de llamada CALL y también por sus complementarias, las instrucciones de retorno de función RETURN, RETLW y RETFIE (esta última está orientada a finalizar una rutina de atención a interrupciones, por lo que el código generado por nuestro compilador no hará uso de ella). La instrucción CALL puede verse como una versión mejorada de GOTO: permite cargar en el PC cualquier posición absoluta de la memoria de programa y, además, salva en la pila el valor del PC en ese momento (teniendo en cuenta el post-incremento), por lo que es posible retomar la ejecución del bloque de código actual con sólo recuperar el valor de la cima de la pila y almacenarlo de nuevo en el PC. Recuperar el valor anterior del PC es la tarea que efectúan las instrucciones de retorno de función. Estos dos grupos de instrucciones facilitan notablemente el uso de subrutinas en el código ensamblador y, por ende, la implementación de llamadas a función tal como las especifica el estándar del lenguaje C. Llegado a este punto puede parecer que con las facilidades proporcionadas por las instrucciones de llamada y retorno, junto a la versatilidad de los saltos condicionales e incondicionales, quedan resueltos todos los problemas relacionados con los saltos en el flujo de ejecución de los programas. No es así. Concretamente existe un problema muy importante íntimamente relacionado con las llamadas a función: el tamaño de la pila.
5.7.
Pila
Los PIC18 implementan una pila hardware de 31 niveles de profundidad en la cual se almacenan los valores del PC. Las instrucciones de llamada y retorno de funciones hacen uso de esta pila. Además de esto, los PIC18 ofrecen al desarrollador la posibilidad de leer y escribir a su antojo el valor almacenado en la cima de la pila (top of stack o TOS), así como modificar el vector que apunta a la cima de la misma, es decir, modificar qué nivel se considera que es el TOS. No obstante, estas funcionalidades más avanzadas no son necesarias para el proyecto que nos atañe, por lo que de aquí en adelante consideraremos que las instrucciones de llamada y retorno son las únicas que operan sobre la pila de llamadas a función. El tamaño fijo de la pila supone un límite pero este problema se agrava con la naturaleza cíclica de la misma, es decir, que tras efectuar 31 operaciones de apilado, la número 32 escribirá en la posición más baja de la misma, destruyendo el valor alma-
Compilador de C para el microcontrolador Microchip PIC18F4550
67
cenado en ésta y, con toda seguridad, haciendo errónea la ejecución del programa. Esta limitación física debe estar siempre presente en la mente del programador que trabaje con estos microcontroladores, el cual debe evitar las funciones recursivas sin acotar y reduciendo en la medida de lo posible el número de llamadas a función en su programa. Se recomienda no exceder más allá de 28 niveles de anidamiento en las llamadas a función, limitación muy inferior a la impuesta por el hardware, pero aconsejada debido a que las llamadas a funciones matemáticas, transparentes para el programador, también hacen uso de la pila. Para la mayoría de aplicacones de estos dispositivos, 31 niveles de profundidad son más que suficientes, aún teniendo en cuenta que las operaciones matemáticas complejas añaden algunas llamadas extra a función.
5.8.
Biblioteca de funciones matemáticas
Los PIC18 incorporan una ALU básica con soporte para instrucciones lógicas y aritméticas de suma y resta. Además, incorpora un multiplicador hardware de 8 bits que permite implementar funciones complejas de manera más eficiente. Sin embargo no incorpora circuitería dedicada específicamente a las operaciones de división. Para efectuar las operaciones de división es necesario implementar algoritmos que hagan uso de las operaciones disponibles. Debido al tamaño de estos algoritmos resulta conveniente no reemplazar cada instrucción de división por la implementación del algoritmo, sino hacer una llamada a una rutina donde se ejecute el mismo y devolver el resultado mediante los registros o la memoria. De este modo el código resultante será más pequeño a costa de añadir una pequeña cantidad de tiempo de ejecución. La solución elegida para la operación de multiplicación es una combinación de todas las posibles opciones. Para las operaciones con valores pequeños, la mejor opción es hacer uso del multiplicador hardware que incorpora el microcontrolador; de este modo el código resultante es rápido de ejecutar y muy compacto, por lo que apenas desperdicia memoria de programa. En cambio, para las multiplicaciones de valores grandes, los algoritmos que hacen uso del multiplicador son bastante extensos, por lo que a pesar de ser rápidos, desperdician demasiada memoria de programa. Como alternativa a esto disponemos del algoritmo de Booth, basado en sumas y restas, que es un algoritmo bastante compacto y no mucho más lento que el anterior dependiente del multiplicador hardware. Además, para la multiplicación de números sin signo también existe el sencillo algoritmo de suma+desplazamiento, que es aún más corto y rápido de ejecutar que el algoritmo de Booth, y es el elegido para efectuar operaciones de multiplicación de números enteros sin signo de tamaño mediano y
Compilador de C para el microcontrolador Microchip PIC18F4550
68
grande. Para implementar la operación de división, la opción elegida, al contrario que para la multiplicación, es utilizar un único algoritmo: el algoritmo de división sin restauración. En el algoritmo estándar de división sin restauración se resta el divisor al acumulador (previamente inicializado con el dividendo) y en caso de dar un resultado negativo, se deshace la resta. En la implementación realizada en nuestro back-end simplemente no se almacena el resultado de la resta hasta tener la certeza de que éste es positivo. Para el caso de división de números con signo, el algoritmo implementado obtiene el signo de cada operando antes de efectuar la división y, una vez terminada la operación de división, adecúa el resultado en función de los signos de dividendo y divisor. Pese a ser operaciones costosas y de uso frecuente, la velocidad de ejecución obtenida con la biblioteca de funciones matemáticas es buena y, lo más importante, ahorra mucho espacio en la memoria de programa que, de haberlo implementado de la manera tradicional (duplicando código) el desperdicio de la misma resultaría excesivo hasta el punto de impedir la realización de muchos proyectos con este microcontrolador, que con nuestra solución son perfectamente posibles y evita tener que optar por dispositivos más avanzados, complejos y caros.
Capítulo 6 Implementación Al enfrentarnos al problema de portar GCC a una nueva arquitectura objetivo, el primer paso es modificar convenientemente el conjunto de archivos que definen el build system. El primer archivo a modificar es config.sub, situado en la raíz del árbol de directorios del código fuente. config.sub contiene los datos de arquitecturas y sistemas operativos soportados por GCC. La única modificación necesaria para nuestro propósito es añadir una entrada adicional con la nueva arquitectura objetivo pic18. El parche que hace este cambio es el siguiente: index f9fcdc8..bf1eb36 100755 --- a/config.sub +++ b/config.sub @@ -288,7 +288,7 @@ case $basic_machine in | ns16k | ns32k \ | open8 \ | or32 \ | pdp10 | pdp11 | pj | pjl \ + | pdp10 | pdp11 | pic18 | pj | pjl \ | powerpc | powerpc64 | powerpc64le | powerpcle \ | pyramid \ | rx \ Con esto le indicamos al build system de GCC que la arquitectura pic18 existe y es válida. Para conseguir que GCC la soporte, debemos modificar otro fichero más: el de configuración del build system. En realidad, la modificación la efectuaremos sobre el archivo de entrada del programa autoconf. autoconf forma parte de las herramientas de GNU denominadas autotools, y su cometido es generar un programa de configuración automática que ayude en la automatización del proceso de compilación del software. 69
Compilador de C para el microcontrolador Microchip PIC18F4550 A continuación se muestra el parche para configure.ac: diff --git a/configure.ac b/configure.ac index 337e11d..f11ac53 100644 --- a/configure.ac +++ b/configure.ac @@ -498,11 +498,62 @@ case "${target}" in # No hosted I/O support. noconfigdirs="$noconfigdirs target-libssp" ;; + pic18-*-*) + noconfigdirs="$noconfigdirs target-libssp" + ;; powerpc-*-aix* | rs6000-*-aix*) noconfigdirs="$noconfigdirs target-libssp" ;; esac +# Disable target libiberty for some systems. +case "${target}" in + *-*-kaos*) + # Remove unsupported stuff on all kaOS configurations. + skipdirs="target-libiberty" + ;; + *-*-netbsd*) + # Skip some stuff on all NetBSD configurations. + noconfigdirs="$noconfigdirs target-libiberty" + ;; + *-*-netware*) + noconfigdirs="$noconfigdirs target-libiberty" + ;; + *-*-rtems*) + skipdirs="${skipdirs} target-libiberty" + ;; + *-*-tpf*) + noconfigdirs="$noconfigdirs target-libiberty" + ;; + *-*-vxworks*) + noconfigdirs="$noconfigdirs target-libiberty"
70
Compilador de C para el microcontrolador Microchip PIC18F4550 + ;; + sh*-*-pe|mips*-*-pe|*arm-wince-pe) + noconfigdirs="$noconfigdirs target-libiberty" + ;; + arm*-*-symbianelf*|arm*-*-linux-androideabi) + noconfigdirs="$noconfigdirs target-libiberty" + ;; + avr-*-*) + noconfigdirs="$noconfigdirs target-libiberty" + ;; + m68hc11-*-*|m6811-*-*|m68hc12-*-*|m6812-*-*) + noconfigdirs="$noconfigdirs target-libiberty" + ;; + pic18-*-*) + noconfigdirs="$noconfigdirs target-libiberty" + ;; + picochip-*-*) + noconfigdirs="$noconfigdirs target-libiberty" + ;; + mips*-sde-elf*) + skipdirs="$skipdirs target-libiberty" + ;; + ip2k-*-*) + noconfigdirs="$noconfigdirs target-libiberty" + ;; +esac + # Disable libstdc++-v3 for some systems. case "${target}" in *-*-vxworks*) @@ -516,6 +567,8 @@ case "${target}" in avr-*-*) noconfigdirs="$noconfigdirs target-libstdc++-v3" ;; + pic18-*-*) + noconfigdirs="$noconfigdirs target-libstdc++-v3" esac
71
Compilador de C para el microcontrolador Microchip PIC18F4550
72
# Disable Fortran for some systems. @@ -524,6 +577,9 @@ case "${target}" in # See
. unsupported_languages="$unsupported_languages fortran" ;; + pic18-*-*) + unsupported_languages="$unsupported_languages fortran" + ;; esac # Disable Java if libffi is not supported. @@ -657,6 +713,9 @@ case "${target}" in mmix-*-*) noconfigdirs="$noconfigdirs target-libffi target-boehm-\ gc" ;; + pic18-*-*) + noconfigdirs="$noconfigdirs ${libgcj} target-libffi" + ;; powerpc-*-aix*) # copied from rs6000-*-* entry noconfigdirs="$noconfigdirs ${libgcj}" @@ -955,6 +1014,13 @@ case "${target}" in mt-*-*) noconfigdirs="$noconfigdirs sim" ;; + pic18-*-*) + noconfigdirs="$noconfigdirs target-libquadmath" + unsupported_languages="$unsupported_languages ada c++ j\ ava objc obj-c++" + ;; + picochip-*-*) + noconfigdirs="$noconfigdirs target-libiberty" + ;; powerpc-*-aix*) # copied from rs6000-*-* entry noconfigdirs="$noconfigdirs gprof"
Compilador de C para el microcontrolador Microchip PIC18F4550
73
@@ -2264,6 +2330,17 @@ case "${target}" in mips*-*-*linux* | mips*-*-gnu*) target_makefile_frag="config/mt-mips-gnu" ;; + pic18-*-*) + NCN_STRICT_CHECK_TOOLS(AS_FOR_TARGET, gpasm) + AC_PATH_PROG(AS_FOR_TARGET, gpasm) + NCN_STRICT_CHECK_TOOLS(LD_FOR_TARGET, gplink) + AC_PATH_PROG(LD_FOR_TARGET, gplink) + NCN_STRICT_CHECK_TOOLS(AR_FOR_TARGET, gplib) + extra_arflags_for_target=" -c" + NCN_STRICT_CHECK_TOOLS(NM_FOR_TARGET, gplib) + NCN_STRICT_CHECK_TOOLS(RANLIB_FOR_TARGET, true) + NCN_STRICT_CHECK_TOOLS(STRIP_FOR_TARGET, gpstrip) + ;; *-*-linux* | *-*-gnu* | *-*-k*bsd*-gnu | *-*-kopensolaris\ *-gnu) target_makefile_frag="config/mt-gnu" ;; Después de esta modificación es necesario ejecutar autoconf para que genere el nuevo programa de configuración del build system. Hecho esto, el sistema de compilación de GCC ya conoce la existencia de la arquitectura objetivo pic18 y puede compilar GCC con el back-end para esta arquitectura. Ahora queda definirla. Las definiciones de arquitectura residen en el subdirectorio gcc/config. En este directorio crearemos un subdirectorio con el mismo nombre que nuestra arquitectura destino, pic18, donde albergaremos los archivos de especificación y descripción de arquitectura. Los archivos principales que describen la arquitectura y que han sido el centro de trabajo fundamental durante el transcurso de este proyecto son: pic18.md pic18.h pic18.c El primero, pic18.md, contiene la descripción de la máquina. En él especificamos a GCC cómo tiene que traducir las instrucciones RTL generadas por el middle-end a código ensamblador del PIC18.
Compilador de C para el microcontrolador Microchip PIC18F4550
74
Recordemos el proceso de traducción que efectúa el compilador. Primero, el front-end lee el archivo de código fuente y crea, a partir del mismo, una estructura de árbol sintáctico. En segundo lugar, genera una lista de instrucciones en el lenguaje intermedio RTL llamadas insn. Por último, empajera las insn de esta lista, una por una, con los patrones de instrucción definidos y genera los bloques de código ensamblador correspondientes a cada insn. El archivo de descripción de máquina (Machine Description), pic18, es el que contiene los patrones de instrucción que protagonizan el tercer paso de la compilación. Según los patrones definidos en él, el middle-end generará unas insn u otras, con el objetivo de que todas las instrucciones RTL que compongan el programa justo antes de la fase de emparejamiento tengan asociado un patrón de instrucciones en ensamblador. El archivo pic18.h contiene la especificación de la máquina. Está compuesto por macros que indican a GCC cómo es la arquitectura objetivo. Entre otros datos, incluye información sobre el número de registros, el nombre de cada uno, para qué puede usarse y para qué no, el tamaño de palabra, tamaño de cada tipo de dato, la alineación en memoria y los nombres de las distintas secciones dentro del código ensamblador. Para algunas de estas macros no es posible utilizar sólamente el preprocesador, por lo que, en su lugar, hemos optado por emplear funciones en C externas, que hagan más sencillo el desarrollo y faciliten la comprensión y depuración del código. Debido a la flexibilidad que otorga el uso de funciones ejecutadas en tiempo de ejecución de nuestro compilador frente al uso de macros, los desarrolladores de GCC han optado por ir migrando la mayoría de macros a target hooks. El tercer archivo de la lista, pic18.c, contiene las funciones auxiliares mencionadas en el párrafo anterior, funciones auxiliares para la generación de código ensamblador y los target hooks que especifican la máquina objetivo. Los target hooks son datos que indican a GCC cómo es la arquitectura objetivo y apuntadores a funciones que deciden dinámicamente las características de la máquina objetivo, durante la ejecución del compilador (al contrario de lo que ocurre con las macros, que se evalúan una única vez al compilar GCC y apenas otorgan facilidades para utilizar código condicional). A parte de estos archivos, existen otros que sirven de apoyo a la descripción y especificación de la arquitectura. El archivo libgcc.S es el archivo principal de la biblioteca auxiliar libgcc. Esta biblioteca contiene todas las operaciones matemáticas que no están soportadas directamente por la arquitectura pero sí por el estándar ANSI C.
Compilador de C para el microcontrolador Microchip PIC18F4550
6.1.
75
Creación de patrones
Como primer paso para explicar la descripción tenemos que comprender como funcionan los patrones. Para ello examinaremos los dos tipos de patrones existentes. Por un lado tenemos los patrones definidos mediante define_insn y por otro los definidos con define_expand. Ambos están constituidos de forma similar, pero los segundos sirven para generar otros insn o agrupar una familia de los mismos. Los primeros se comportan, grosso modo, como generadores de código y son los que realmente producen el texto del código ensamblador final.
6.1.1.
define_insn
El lenguaje de definición de patrones es bastante similar al lenguaje funcional Lisp: cada definición es una lista con un número variable de elementos, los cuales pueden ser elementos finales u otras listas. Las sentencias define_insn toman los siguientes argumentos: 1. Nombre. Para GCC existen varias instrucciones insn que realizan tareas básicas y que el middle-end utiliza durante la fase de generación de RTL. Estas son fijas, dado que GCC tiene la posibilidad de adecuarse a los patrones definidos; y, de los que tenga, elegirá la mejor combinación para realizar cada operación. En cualquier caso, debemos definir suficientes insn para que GCC pueda componer el total de operaciones necesarias. Para esto podemos usar uno de los nombres conocidos e implementar la acción requerida o emitir, con define_expand las insn que se necesiten para conseguir el objetivo de otra manera. Si usamos un nombre no conocido por GCC, o simplemente no le damos nombre (usando la cadena vacía), el compilador no utilizará este patrón durante la fase de generación de RTL, pero con ellos permitimos que varias insn simples puedan ser combinadas posteriormente. A los patrones no estándar o sin nombre se les suele dar un nombre basado en su finalidad y comenzando con asterisco, para diferenciarlos de los patrones estándar. 2. Plantilla RTL. Vector de expresiones RTL incompletas que indican a GCC cómo debe usar el patrón. Con expresiones RTL incompletas nos referimos a que son plantillas de instrucción con elementos desconocidos o huecos, que GCC completará, de acuerdo a ciertas reglas, para crear el texto del código ensamblador que finalmente aparezca en el código ensamblador de salida. 3. Condición. Es una expresión en C que permite comprobar si el cuerpo de este patrón de instrucción es el adecuado para la insn.
Compilador de C para el microcontrolador Microchip PIC18F4550
76
4. Plantilla de salida. Cadena de caracteres que indica a GCC qué código ensamblador debe emitir para el insn actual. Es posible utilizar una cadena de caracteres, un vector compuesto por varias cadenas o un bloque de código C que devolverá el código ensamblador correspondiente. 5. Atributos. Vector de valores para los atributos de los insn que encajen con este patrón.
6.1.2.
Ejemplo de define_insn
Ilustramos lo comentado con el siguiente ejemplo: (define_insn "xorhi3" [(set (match_operand:HI 0 "register_operand" "=v,v") (xor:HI (match_operand:HI 1 "register_operand" " %0,0 ") (match_operand:HI 2 "reg_or_int_operand" "i, v"))))] "" "@ movlw low %2\n\txorwf %0,F\n\tmovlw high %2\n\txorwf %0+1,F movf %2,W\n\txorwf %0,F\n\tmovf %2+1,W\n\txorwf %0+1,F" ) El patrón xorhi3 es un patrón estándar. Realiza la disyunción exclusiva entre el operando 1 y el operando 2, guardando el resultado en el operando 0. La plantilla RTL muestra esta acción, la primera lista encabezada con set, tiene dos operandos: el primero es el registro que será modificado, y el segundo una lista xor que contiene a su vez dos registros. La forma de especificar el tipo de un operando y las restricciones que debe cumplir la vemos con este ejemplo: (match_operand:HI 0 "register_operand" "=v,v") Usamos match_operand para que unifique con un operando. HI especifica el modo. El modo de un operando indica de qué tipo y de qué tamaño son los datos que permite este operando. El número de operando, 0 para el ejemplo, debe ser parte de una lista de números consecutivos que comience por cero. El siguiente parámetro de match_operand es el predicado, que determina si existe la relación deseada sobre el operando, es decir, si el operando es válido o no. En este ejemplo, el predicado
Compilador de C para el microcontrolador Microchip PIC18F4550
77
es register_operand y especifica que este operando debe ser un registro1 . A continuación vemos =v,v. Esta cadena especifica una restricción de clase al operando. Esta restricción no se examina en la fase de emparejamiento de patrones, sino en la fase de recarga. En el ejemplo, register_operand exige que el operando sea un registro, pero con la restricción v exigimos que en la recarga el resultado sea uno de los registros de la clase registros virtuales de nuestra máquina. En un principio GCC usará infinitos registros para emparejar patrones, pero en la recarga asignará los registros virtuales disponibles y, en caso de no quedar ninguno libre, buscará liberar alguno para poder usarlo. En la restricción vemos también un signo =, que indica que el operando es de salida, es decir, que su valor será sobreescrito al terminar la instrucción. Vemos como existen dos v indicando que la instrucción en realidad corresponde a un vector de dos instrucciones, ambas con la misma forma, pero diferentes restricciones en sus operandos. Si se usan dos restricciones en un operando, todos los operandos con restricciones deben tener un vector con el mismo número de restricciones, en este caso dos. Para el segundo registro (el primero de la operación xor) las restricciones muestran el valor 0. El carácter 0 como restricción indica que el registro a elegir para este operando en la fase de recarga será el mismo que el numerado como 0, es decir, para el caso que estamos tratando, el resultado y un operando comparten registro. Otro valor que llama la atención en las restricciones es el % de las restricciones del segundo operando (numerado como 1). El % indica a GCC que los operandos son intercambiables, es decir, que la operación A = xor B C es equivalente a A = xor C B. Dejamos con esto libertad a GCC para escoger registro destino entre los dos que se establecerán como entrada, decisión que tomará en base al registro que no se vaya a usar tras la operación, dado que el elegido será sobreescrito con el resultado. En el tercer operando nos encontramos con un nuevo predicado y una nueva restricción. Como podemos ver, la restricción i se refiere a valores constantes enteros, y el predicado reg_or_int_operand sólo permitirá registros o valores constantes enteros. Vista la forma general en que se define una insn, nos queda ver la plantilla de salida. Lo vemos partiendo de este ejemplo: "@ movlw low %2\n\txorwf %0, F\n\tmovlw high %2\n\txorwf %0+1, F 1
Lo que GCC considera un registro físico.
Compilador de C para el microcontrolador Microchip PIC18F4550
78
movf %2, W\n\txorwf %0, F\n\tmovf %2+1, W\n\txorwf %0+1, F" Hay dos formas de especificar la plantilla de salida: como una cadena, que contiene los argumentos especificados con %n, donde n es un número de operando, y como un trozo de código C que devuelve una cadena de caracteres con el código ensamblador. En este ejemplo vemos el primer caso. Como comentamos, al usar dos restricciones en los operandos, en realidad estamos definiendo dos patrones de instrucción diferentes (con forma similar). Por ello es por lo que debemos usar @ al inicio de la cadena. Posteriormente cada linea escrita corresponderá a un juego de restricciones en el orden en que se especifican. La primera línea corresponde a la primera forma, con los operandos v, 0, y i, que hace la operación xor con dos operandos de entrada, un valor entero constante y un registro que es el mismo donde queda establecida la salida. La segunda línea corresponde a las restricciones v, 0 y v, donde se opera sobre dos registros y uno de ellos será el escogido para el resultado. Como vemos, el código ensamblador realiza tales operaciones pero deja los parámetros de entrada y salida sin especificar, hasta que, en la fase de recarga, se designan los valores definitivos para cada operando, sustituyendo los valores %n de la plantilla por el texto que identifica a los registros designados. En el caso de que queramos hacer este mismo código con un trozo de código C, se escribiría de esta forma: { if (which_alternative==0) { output_asm_insn("movlw low %2", operands); output_asm_insn("xorwf %0, F", operands); output_asm_insn("movlw high %2", operands); } else { output_asm_insn("movf %2, W", operands); output_asm_insn("xorwf %0, F", operands); output_asm_insn("movf %2+1, W", operands); } output_asm_insn("xorwf %0+1, F", operands); return ""; }
Compilador de C para el microcontrolador Microchip PIC18F4550
79
Evaluando el valor which_alternative sabemos qué alternativa en las restricciones se ha tomado. Con output_asm_insn damos salida a la cadena especificada en el primer operando con los valores del vector de rtx definidos en el segundo operando. Por defecto, podemos utilizar operands, que es el vector que contiene los operandos del insn actual, pero podemos crear o modificar nuevos vectores para operaciones más complejas. El trozo de código C debe terminar devolviendo una cadena de caracteres. Es posible devolver todo el código ensamblador en esta cadena o, como hacemos en este ejemplo, generarlo paso por paso y terminar el bloque de código C devolviendo una cadena vacía. En este ejemplo, usar código C no aporta ninguna diferencia, pero podemos modificar variables globales, crear etiquetas, efectuar ejecución condicional, llamar a procedimientos, incluir datos de depuración o emitir nuevas instrucciones según lo deseemos en insn más complejas. Gracias a esto tenemos total flexibilidad para generar código tras el emparejamiento. El código, a diferencia de la cadena, está encerrado entre llaves. Las cadenas cuyo primer carácter es una arroba @ indican salida múltiple. En caso contrario, solo hay un bloque de código de salida que puede ocupar varias lineas. Hay más opciones para especificar la plantilla de salida, pero en la definición de la máquina hemos usado tan solo la opción de bloques de código C para generar código ensamblador.
6.1.3.
Define_expand
Para terminar con la compresión de los tipos de patrones, tenemos que hablar del segundo tipo de patrón, los definidos con define_expand. El objetivo de estos patrones es crear nuevos insn en la fase de generación de RTL. Sirven para expandir o refinar un patrón estándar, no para emitir código ensamblador, y su sintaxis consta de cuatro parámetros: 1. Nombre. Dado que sólo se utilizan durante la fase de generación de RTL, deben tener un nombre estándar. 2. Plantilla RTL. Vector de expresiones RTL incompletas. Su forma es idéntica a la plantilla RTL de define_insn. 3. Condición. Al igual que la plantilla RTL, es idéntica a la condición en una declaración define_insn. 4. Sentencia de preparación. Bloque de código C que es ejecutado antes de generar el código RTL. El propósito de estas declaraciones es preparar registros tem-
Compilador de C para el microcontrolador Microchip PIC18F4550
80
porales para su uso como operandos internos o emitir otros insn de manera manual mediante las funciones de generación de insn. En el proyecto actual hemos usado los patrones de expansión con dos propósitos distintos. En los patrones de asignación los hemos aplicado con la intención de dividir dichos patrones en otros más sencillos que puedan tratarse de forma más simple con define_insn. El otro uso dado a los patrones de expansión ha sido con las operaciones de multiplicación y división, y la intención en estos casos no era subdividir los casos sino adaptar los argumentos de entrada a los requisitos estrictos impuestos por las funciones de la biblioteca de operaciones matemáticas.
6.2.
Asignación - los patrones mínimos.
GCC necesita conocer la definición de un conjunto mínimo de patrones a partir de los cuales pueda componer el resto de patrones de instrucción. De forma obligatoria, necesitamos describir las operaciones de asignación o movimiento de registros con todos los tipos que queramos considerar en la arquitectura. Como tipos permitidos existen los QI (un byte), HI (dos), SI (cuatro bytes) y SF (cuatro bytes de un valor en coma flotante). Debemos definir patrones de insn para todos los movimientos posibles entre todas las posibilidades de registros, constantes y memoria, es decir, desde un registro, memoria o una constante, hacia un registro o a una posición de memoria. Veremos las operaciones para el tipo HI, pero el formato es extrapolable a los demás tipos. Tenemos un define_expand que es necesario para que GCC tenga el patrón estándar. En él tratamos cuatro casos que suponen movimientos entre posiciones de memoria, gracias a lo cual evitamos sumar otros cuatro casos a los ocho existentes. El patrón estándar movhi unificará con: (define_expand "movhi" [(set (match_operand:HI 0 "target_mov_operand") (match_operand:HI 1 "source_mov_operand"))] "" { if ((GET_CODE(operands[0]) == MEM) && (GET_CODE(operands[1])==MEM)) { rtx tmp = gen_rtx_REG(HImode, PIC_RETURN_REGISTER); emit_insn( gen_movhi(tmp, operands[1]) );
Compilador de C para el microcontrolador Microchip PIC18F4550
81
emit_insn( gen_movhi(operands[0], tmp) ); DONE; } } ) Con esto GCC verá el patrón estándar que necesita para generar el RTL, y también verá que no tiene información para generar código ensamblador (se trata de de un patrón de expansión, no de instrucción). Para el caso de ser una operación cuyos parámetros sean ambos de acceso a memoria, GCC emite dos insn correspondientes a movimientos del mismo tipo de dato. El primer insn corresponde a un movimiento desde la posición de memoria origen hacia el registro dado por la función gen_rtx_REG, que devuelve, en este caso, el registro usado para retorno de las funciones y que podemos usar siempre sin tener que salvar su valor. El segundo, copiará el contenido de ese registro en la posición de memoria destino. Este bloque de código finaliza con la sentencia DONE, que indica a GCC que la expansión ha finalizado con éxito y puede dar por finalizado el tratamiento de este insn en la fase de generación de código RTL. Cuando no se trate de este caso, el RTL generado corresponderá al de la plantilla definida en la definición de expansión (segundo parámetro del define_expand). Llegada la fase de generación de código ensamblador, GCC buscará el emparejamiento de patrones comparando cada RTX (expresión en lenguaje RTL) con los patrones definidos. Para este proceso no tiene en cuenta el nombre (primer parámetro) de cada definición, sólo compara, uno a uno y por orden de aparición en el archivo de descripción de máquina, los patrones de cada definición de instrucción (los definidos con define_insn). Para el ejemplo que estamos tratando, la unificación tendrá éxito al llegar a esta definición: (define_insn "*movhi" [(set (match_operand:HI 0 "target_mov_operand" "=v,v,S") (match_operand:HI 1 "source_mov_operand" "U,S,U"))] "" { return pic_movhi(insn, operands, NULL); } ) Esta definición resulta muy clara gracias al uso de una función auxiliar, que es la responsable de analizar de qué tipo de movimiento de datos se trata y producir el código ensamblador correspondiente. A parte de esto, la mayor curiosidad reside
Compilador de C para el microcontrolador Microchip PIC18F4550
82
en el asterisco que precede al nombre: con esto logramos tener un nombre descriptivo para la persona que estudie el RTL generado durante las distintas etapas de compilación y, al mismo tiempo, no confundir al compilador con un posible patrón de instrucción extra a tener en cuenta durante la etapa de generación de RTL. Para ayudar a comprender la etapa de generación de código ensamblador, hemos optado por incluir el código de la función pic_movhi en este documento. const char * pic_movhi (rtx insn, rtx operands[], int * l ATTRIBUTE_UNUSED) { rtx op_source = ignore_CONST_SUBREG(operands[1]); rtx op_target = ignore_CONST_SUBREG(operands[0]); enum rtx_code code_source = GET_CODE(op_source); enum rtx_code code_target = GET_CODE(op_target); rtx xoperands[3]; /* Check that it is not a MEM-to-MEM transfer. If so, an error in movhi expansion happened. */ /* There are 3 cases: */ /* A) - R = {I, R, SR, SR+I} */ /* B) - R = M(x) */ /* C) - M(x) = {I, R, SR, SR+I} */ if (code_target == REG) { if (code_source != MEM) { /* Case A : R = {I, R, SR, SR+I} */ switch (code_source) { case CONST_INT: if ((INTVAL(op_source) & 0xFF) == 0) { ASM_OUT("clrf %0"); } else
Compilador de C para el microcontrolador Microchip PIC18F4550 { ASM_OUT("movlw low %1"); ASM_OUT("movwf %0"); } if ((INTVAL(op_source) & 0xFF00) == 0) return "clrf %0+1"; else return "movlw high %1\n" "\tmovwf %0+1"; break; case REG: /* If there is register overlapping between source and target, make the copying so target will end with the expected value. Final value in source is not our business at this point. */ if (true_regnum(op_target) > true_regnum(op_source)) { return "movff %1+1, %0+1\n" "\tmovff %1, %0"; } else { return "movff %1, %0\n" "\tmovff %1+1, %0+1"; } break; case SYMBOL_REF: return "movlw low %1\n" "\tmovwf %0\n" "\tmovlw high %1\n" "\tmovwf %0+1"; break;
83
Compilador de C para el microcontrolador Microchip PIC18F4550
case PLUS: xoperands[0] = op_target; xoperands[1] = ignore_CONST_SUBREG( XEXP(op_source, 0)); xoperands[2] = ignore_CONST_SUBREG( XEXP(op_source, 1)); if ((GET_CODE(xoperands[1]) == SYMBOL_REF) && (GET_CODE(xoperands[2]) == CONST_INT)) { output_asm_insn( "movlw (low %1) + (low %2)\n" "\tmovwf %0\n" "\tmovlw (high %1) + (high %2)\n" "\tmovwf %0+1", xoperands); return ""; } /* else ERROR */ break; default: /* ERROR */ break; } } else /* code_source == MEM */ { /* Case B : R = M(x) */ /* Write address to FSR... */ pic_direct_FSR(ignore_CONST_SUBREG( XEXP(op_source, 0))); /* ... and copy from INDF to registers. */ if (last_fsr != 0) last_fsr++; ASM_OUT("movff INDF0, %0"); ASM_OUT("infsnz FSR0L, F");
84
Compilador de C para el microcontrolador Microchip PIC18F4550 ASM_OUT("incf FSR0H, F"); ASM_OUT("movff INDF0, %0+1"); return ""; } } else /* code_target != REG */ { /* Case C : M(x) = {I, R, SR, SR+I} */ /* Write address to FSR... */ pic_direct_FSR(ignore_CONST_SUBREG( XEXP(op_target, 0))); /* ... switch { case if
and copy from source to INDF. */ (code_source)
CONST_INT: ((INTVAL(op_source) & 0xFF) == 0) { ASM_OUT("clrf INDF0"); } else { ASM_OUT("movlw low %1"); ASM_OUT("movwf INDF0"); } if (last_fsr != 0) last_fsr++; ASM_OUT("infsnz FSR0L, F"); ASM_OUT("incf FSR0H, F"); if ((INTVAL(op_source) & 0xFF00) == 0) return "clrf INDF0"; else return "movlw high %1\n" "\tmovwf INDF0"; break;
85
Compilador de C para el microcontrolador Microchip PIC18F4550
case REG: if (last_fsr != 0) last_fsr++; ASM_OUT("movff %1, INDF0"); ASM_OUT("infsnz FSR0L, F"); ASM_OUT("incf FSR0H, F"); ASM_OUT("movff %1+1, INDF0"); return ""; break; case SYMBOL_REF: if (last_fsr != 0) last_fsr++; ASM_OUT("movlw low %1"); ASM_OUT("movwf INDF0"); ASM_OUT("infsnz FSR0L, F"); ASM_OUT("incf FSR0H, F"); ASM_OUT("movlw high %1"); ASM_OUT("movwf INDF0"); return ""; break; case PLUS: xoperands[0] = op_target; xoperands[1] = ignore_CONST_SUBREG( XEXP(op_source,0)); xoperands[2] = ignore_CONST_SUBREG( XEXP(op_source,1)); if ((GET_CODE(xoperands[1]) == SYMBOL_REF) && (GET_CODE(xoperands[2]) == CONST_INT)) { if (last_fsr != 0)
86
Compilador de C para el microcontrolador Microchip PIC18F4550
87
last_fsr++; output_asm_insn("movlw (low %1) + (low %2)", xoperands); output_asm_insn("movwf INDF0", xoperands); output_asm_insn("infsnz FSR0L, F", xoperands); output_asm_insn("incf FSR0H, F", xoperands); output_asm_insn("movlw (high %1) + (high %2)", xoperands); output_asm_insn("movwf INDF0", xoperands); return ""; } /* else ERROR */ break; default: /* ERROR */ break; } } fprintf(stderr, "RTX not known by movhi.\n"); debug_rtx(insn); return "UNKNOWN_MOVHI"; } Vista esta operación, hemos alcanzado el formato de instrucción más complejo del fichero de descripción. Podemos observar operaciones más largas en el fichero C de apoyo, pero no más complejas que ésta. Por lo tanto, vemos cómo desaparece la complicación de la descripción de una máquina mediante la especificación de operaciones sencillas. Los otros tipos de movimiento con QI, SI y SF son análogos a los analizados para el tipo HI, en formato y en idea de funcionamiento. Podemos verlos en el listado del archivo de la descripción de máquina, pero no tienen distinción en su operativa a la que hemos presentado en esta sección.
Compilador de C para el microcontrolador Microchip PIC18F4550 Nombre absM2 addM3 andM3 ashlM3 ashrM3 cmpM divmodMN4 udivmodMN4 extendMN2 zero_extendMN2 iorM3 lshrM3 mulM3 mulhisi3 umulhisi3 mulqihi3 umulqihi3 negM2 one_cmplM2 rotlM3 rotrM3 subM3 xorM3
88
Descripción Valor absoluto. Suma. Conjunción lógica. Desplazamiento aritmético a la izquierda. Desplazamiento aritmético a la derecha. Comparación de valores. División y módulo. División y módulo sin signo. Extensión de signo de un dato de tipo M a otro de tipo N (QI=>HI, QI=>SI, HI=>SI). Extensión de ceros de un dato de tipo M a otro de tipo N (QI=>HI, QI=>SI, HI=>SI). Disyunción lógica. Desplazamiento lógico a la derecha. Multiplicación. Multiplicación de valores HI con resultado en tipo SI. Multiplicación de valores HI con resultado en tipo SI. Sin signo. Multiplicación de valores QI con resultado en tipo HI. Multiplicación de valores QI con resultado en tipo HI. Sin signo. Negación. Complemento a uno. Rotación izquierda. Rotación derecha. Resta. Disyunción exclusiva.
Cuadro 6.1: Resumen de patrones definidos I: Aritmético-lógicas
6.3.
Los patrones del back-end
Recordemos que GCC buscará, entre los patrones disponibles, aquellos patrones que le permitan efectuar la operación deseada. Por ejemplo, podemos definir el complemento a uno y el and, y GCC los usará para definir el or. Pero cuantos más patrones describamos, mejor funcionamiento tendrá GCC, al usar patrones directos y no mezclas de ellos, con el consiguiente almacenamiento temporal que esto conlleva. En las tablas 6.1 y 6.2 podemos ver los patrones definidos. Mostraremos algunos de diferente dificultad para tener más afianzada la idea general. Comenzando por lo fácil, tenemos el patrón zero_extendqihi2. (define_insn "zero_extendqihi2"
Compilador de C para el microcontrolador Microchip PIC18F4550
Nombre beq bge bgeu bgt bgtu ble bleu blt bltu bne call call_value cbranchM4 jump indirect_jump movsf movM_from_mem movM_mem_to_mem movM_reg movM_to_mem nop pushM popM return
89
Descripción Salto condicional si igual. Salto condicional si mayor o igual. Salto condicional si mayor o igual. Versión sin signo. Salto condicional si mayor. Salto condicional si mayor. Versión sin signo. Salto condicional si menor o igual. Salto condicional si menor o igual. Versión sin signo. Salto condicional si menor. Salto condicional si menor. Versión sin signo. Salto condicional si no igual. Llamada a subrutina. Llamada a subrutina con parámetro. Salto condicional. Salto incondicional. Salto incondicional indirecto. Movimiento en punto flotante. Movimiento de memoria a registro en tipo M (QI,HI,SI,SF). Movimiento de una posición de memoria a otra, de un dato de tipo M (QI,HI,SI,SF). Movimiento de registro y constante a registro en tipo M (QI, HI, SI, SF). Movimiento de registro y constante a memoria en tipo M (QI, HI, SI, SF). No operación. Introducción de dato en la pila. Recuperación de dato de la pila. Vuelta de subrutina.
Cuadro 6.2: Resumen de patrones definidos II: Control
Compilador de C para el microcontrolador Microchip PIC18F4550
90
[(set (match_operand:HI 0 "register_operand" "=v") (zero_extend:HI (match_operand:QI 1 " register_operand" "0")))] "" "clrf %0+1" ) Ante la plantilla de entrada, que admite un valor del tipo QI obtenemos un dato de tipo HI, dos bytes, que es la extensión sin signo del valor de entrada. La forma normal de hacer esto es simplemente rellenar con ceros la parte de valor final todavía no definida. Aumentando un poco el grado de complejidad de una instrucción, veamos el patrón neghi2. (define_insn "neghi2" [(set (match_operand:HI 0 "register_operand" "=v,v,v") (neg:HI (match_operand:HI 1 "reg_or_int_operand" "i ,0,v")))] "" { switch (which_alternative) { case 0: ASM_OUT("movlw low %1"); ASM_OUT("movwf %0"); ASM_OUT("movlw high %1"); ASM_OUT("movwf %0+1"); break; case 1: break; case 2: ASM_OUT("movff %1, %0"); ASM_OUT("movff %1+1, %0+1"); break; default: break;
Compilador de C para el microcontrolador Microchip PIC18F4550
91
} ASM_OUT("comf %0+1, F"); ASM_OUT("negf %0"); ASM_OUT("btfsc STATUS, C"); ASM_OUT("incf %0+1, F"); return ""; } ) Este patrón tiene tres restricciones y con ello tres posibles alternativas. Podríamos haber usado una cadena comenzando por arroba y una línea por cada posibilidad, pero la opción de utilizar un bloque de código C resulta más legible debido a la extensión de los códigos en ensamblador restultantes. Vemos que en el condicional sólo tratamos dos alternativas, la tercera no necesita paso de registros. No hemos usado la función output_asm_insn para producir código, sino la macro ASM_OUT, que no es sino un envoltorio (wrapper en inglés) de dicha función con el parámetro de operandos por defecto. #define ASM_OUT(a)
output_asm_insn(a, operands)
Con esta macro mejoramos la lectura y ayuda a distinguir con más facilidad las líneas de código ensamblador donde intervienen parámetros diferentes a los que toma el insn que está siendo procesado en este momento. Vistos un ejemplo sencillo y otro de complejidad media, queda por ver un ejemplo algo más complejo. No debería existir ningún impedimento en su comprensión si tenemos en cuenta lo explicado al inicio de este capítulo; sin embargo, veremos el uso de algunas funciones propias del back-end de GCC que necesitarán una explicación adicional. El ejemplo es el insn abshi2: (define_insn "abshi2" [(set (match_operand:HI 0 "register_operand" "=v,v,v") (abs:HI (match_operand:HI 1 "reg_or_int_operand" "i ,0,v")))] "" { rtx label = gen_label_rtx(); switch (which_alternative)
Compilador de C para el microcontrolador Microchip PIC18F4550 { case 0: ASM_OUT("movlw ASM_OUT("movwf ASM_OUT("movlw ASM_OUT("movwf break;
92
low %1"); %0"); high %1"); %0+1");
case 1: break; case 2: ASM_OUT("movff %1, %0"); ASM_OUT("movff %1+1, %0+1"); break; default: break; } ASM_OUT("btfss %0+1, 7"); fputs("\tbra ", asm_out_file); output_addr_const(asm_out_file, label); fputs("\n", asm_out_file); ASM_OUT("comf %0+1, F"); ASM_OUT("negf %0"); ASM_OUT("btfsc STATUS, C"); ASM_OUT("incf %0+1, F"); output_addr_const(asm_out_file, label); fputs("\n", asm_out_file); return ""; } ) Observamos tres nuevas funciones que todavía no han sido explicadas. Por un lado, vemos una llamada a fputs con dos argumentos, uno de ellos, un descriptor de
Compilador de C para el microcontrolador Microchip PIC18F4550
93
archivo llamado asm_out_file. Este descriptor corresponde al archivo ensamblador de salida. Todo lo que escribamos sobre él aparecerá en el archivo de salida, por ello tenemos la posibilidad de usar fputs o fprintf para formatear la salida como creamos conveniente. Por otro lado, tenemos otra función que devuelve un rtx con la que podemos crear etiquetas únicas en el código: gen_label_rtx. Con esta función obtenemos una etiqueta única que podremos usar allá donde nos haga falta. Para utilizar esta etiqueta en el código se utiliza la tercera función todavía no explicada: output_addr_const. Esta función escribe en el archivo cuyo descriptor asociado se pasa como primer argumento el nombre de la etiqueta que corresponde al rtx que se le pasa como segundo argumento. Este segundo parámetro debe ser una estructura rtx que represente una etiqueta de una dirección de memoria. En este ejemplo utilizamos output_add_const dos veces: la primera para generar el texto de la etiqueta como parámetro de la instrucción bra y la segunda para producir el mismo texto como etiqueta del código que estamos generando. El código generado por este patrón, para el caso de la segunda alternativa y antes pasar por la fase de recarga es el siguiente: btfss %0+1, 7 bra _L3 comf %0+1, F negf %0, F btfsc STATUS, C incf %0+1, F _L3 Con estos tres ejemplos queda vista la creación de patrones para la descripción de una nueva arquitectura. A partir de este punto, el archivo de descripción de máquina no debe representar ningún problema más allá del código ensamblador generado para cada insn.
6.4.
Especificación
Como vimos al principio del capítulo, el archivo pic18.h de especificación de la arquitectura no es más que la manera de describirle a GCC los detalles de la arquitectura objetivo, descritos en el capítulo 5, a través de las macros oportunas, junto a los target hooks que, inspirados en la especificación del back-end para i386, hemos utilizado en pic18.c. El archivo pic18.opt es el de especificación de opciones para el compilador. En
Compilador de C para el microcontrolador Microchip PIC18F4550
94
él especificamos los modificadores que admitirá nuestro back-end desde la línea de comandos. Para este trabajo sólo ha sido necesario utilizar un tipo de entrada, el registro de definición de opción, que tiene tres campos: nombre del modificador, propiedades de la opción y descripción que mostrará pic18-gcc cuando se ejecute con la opción –help. mstack-size= Target RejectNegative Joined Var(user_defined_stack_size) \ Init("16") Specify stack size mextra-alloc= Target RejectNegative Joined Var(user_defined_extra_memory) \ Init("0") Specify memory size to reserve mpic-model= Target RejectNegative Joined Var(user_defined_pic_model) \ Init("18f4550") Select PIC model La propiedad Target indica que esta opción es específica para este back-end. RejectNegative especifica que no habrá una opción negativa de esta opción, es decir, no existirá la opción no-opcion_de_ejemplo. La propiedad Joined exige que esta opción tome un argumento obligatorio, el cual debe indicarse a continuación de la opción, sin espacios en blanco entre medias. Var especifica a GCC que debe guardar el argumento de esta opción en la variable indicada entre paréntesis. Por último, la propiedad Init, como puede intuirse, especifica el valor que tomará la variable mencionada en caso de que esta opción no forme parte de la línea de comandos utilizada para ejecutar el compilador. Para el back-end de PIC18 hemos especificado tres opciones. La primera de ellas es –mstack-size, especifica el tamaño de la pila de datos, que, por defecto, será de 16 bytes. La segunda es –mextra-alloc, y su propósito es especificar el tamaño de memoria adicional que debe reservar GCC (útil a la hora de declarar variables globales en archivos de código distintos al principal). La tercera opción permitirá, en un futuro próximo, seleccionar para qué modelo de microcontrolador deberá GCC generar código. Las siguientes funciones son las encargadas de, una vez recogidos los valores
Compilador de C para el microcontrolador Microchip PIC18F4550
95
de la línea de comandos, adecuar las variables internas para conseguir el código deseado. Podemos ver en el archivo pic18.c la implementación, pero, en resumidas cuentas, revisamos que los parámetros sean los adecuados y modificamos los valores a considerar. La primera función revisa y aplica los valores indicados y la segunda adecúa el uso de los registros; por ejemplo, reduciendo el número de los mismos. #undef TARGET_OPTION_OVERRIDE #define TARGET_OPTION_OVERRIDE pic_override_options #undef TARGET_CONDITIONAL_REGISTER_USAGE #define TARGET_CONDITIONAL_REGISTER_USAGE \ pic_conditional_register_usage Vemos en las siguientes líneas la forma de especificar el ensamblador y enlazador por defecto, así como las opciones adecuadas. #ifndef DEFAULT_ASSEMBLER # define DEFAULT_ASSEMBLER "/usr/bin/gpasm" #endif #ifndef DEFAULT_LINKER # define DEFAULT_LINKER "/usr/bin/gplink" #endif #define CC1_SPEC "-P" #define ASM_SPEC "-c %{mpic-model=*:-p %*} " \ " %{!mpic-model=*:-p 18F4550}" #define LINK_SPEC " %{mpic-model=*:-s " \ " %:gplink_include_file()} " \ " %{!mpic-model=*:-s " \ "/usr/share/gputils/lkr/18f4550.lkr}" #define LINK_LIBGCC_SPEC "" #define LIB_SPEC "" #define LIBGCC_SPEC "" #define STARTFILE_SPEC "libgcc.a %s" Las siguientes macros indican a GCC los detalles sobre los tamaños de cada tipo de dato, la alineación y el endianess de las palabras en memoria.
Compilador de C para el microcontrolador Microchip PIC18F4550
96
#define BITS_PER_UNIT 8 #define BITS_BIG_ENDIAN 0 #define BYTES_BIG_ENDIAN 0 #define WORDS_BIG_ENDIAN 0 #ifdef IN_LIBGCC2 #define UNITS_PER_WORD 4 #else #define UNITS_PER_WORD 1 #endif #define PARM_BOUNDARY 8 #define STACK_BOUNDARY 8 #define FUNCTION_BOUNDARY 8 #define BIGGEST_ALIGNMENT 8 #define EMPTY_FIELD_BOUNDARY 8 #define STRICT_ALIGNMENT 0 #define MAX_FIXED_MODE_SIZE 16 #define #define #define #define #define #define #define #define #define #define #define
INT_TYPE_SIZE 16 SHORT_TYPE_SIZE 8 LONG_TYPE_SIZE 32 LONG_LONG_TYPE_SIZE 64 FLOAT_TYPE_SIZE 32 DOUBLE_TYPE_SIZE 32 LONG_DOUBLE_TYPE_SIZE 32 DEFAULT_SIGNED_CHAR 0 SIZE_TYPE "unsigned int" PTRDIFF_TYPE "int" WCHAR_TYPE_SIZE 16
El primer bloque establece que la unidad de memoria será un byte, 8 bits; el esquema de almacenamiento será little-endian tanto para bits como para palabras de memoria, y establece que la alineación de palabras en memoria será siempre por bytes. En el segundo bloque especificamos los tamaños, en bits, para los distintos tipos de datos. Hemos seguido el estándar ANSI C. Estas opciones permiten hacer los tipos de datos en C más próximos a los tipos que permite el hardware pero, en el caso particular de los microcontroladores PIC, todos son múltiplos de 8 bits. Especial mención merece la definición de UNITS_PER_WORD, donde distinguimos entre compilaciones genéricas y la compilación de la biblioteca libgcc, la biblio-
Compilador de C para el microcontrolador Microchip PIC18F4550
97
teca de funciones matemáticas de GCC. El motivo de tal distinción es conseguir que los modos SI y DI de libgcc2.c compilen de forma adecuada, dado que varias funciones utilizan modos superiores para trabajar en modos soportados, impidiendo la compilación si no hay espacio de palabra para los modos superiores, pese a que dichos modos nunca se vayan a utilizar. Continuamos con la especificación de los registros. PIC_FREG_NUM 32 PIC_RETURN_REGISTER (PIC_FREG_NUM / 2) FIRST_PSEUDO_REGISTER 35 FIXED_REGISTERS { \ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* F0 .. F15*/ \ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* F16 .. F31*/ \ 1,1,1} /*StackPointer, FramePointer, ArgPointer*/ #define CALL_USED_REGISTERS { \ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* F0 .. F15*/ \ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, /* F16 .. F31*/ \ 1,1,1} /*StackPointer, FramePointer, ArgPointer*/ #define REG_ALLOC_ORDER { \ 16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,\ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,15,\ 32,33,34} #define #define #define #define
Con FIRST_PSEUDO_REGISTER especificamos el primer registro que no es real. Al especificar 35, tenemos 35 registros que GCC considerará reales. De estos registros, 32 serán de propósito general y 8 bits de tamaño, y 3 de propósito específico (frame pointer, stack pointer y argument pointer). El tamaño de los tres registros de propósito específico será 16 bits, frente a los 8 bits de los registros de propósito general. Esto se deberá a que su propósito es actuar como punteros a memoria de datos. En FIXED_REGISTERS especificamos a GCC qué registros son de propósito específico y no debe utilizar como genéricos (aquellos marcados con un 1). Los registros marcados con 1 en CALL_USED_REGISTERS serán aquellos cuyo valor sobrevive a un cambio de marco de función; es decir, a una entrada o salida de una función. Para aquellos que estén marcados con 0, GCC se encargará de producir el código necesario para salvar estos registros antes de ejecutar una llamada a función y restaurarlos al finalizar la ejecución de la misma. Con REG_ALLOC_ORDER especificamos el orden en que GCC debe asignar los registros físicos. enum reg_class { NO_REGS,
Compilador de C para el microcontrolador Microchip PIC18F4550
98
VIRTUAL_REGS, GENERAL_REGS, ALL_REGS, LIM_REG_CLASSES }; #define N_REG_CLASSES (int) LIM_REG_CLASSES #define REG_CLASS_NAMES {"NO_REGS", \ "VIRTUAL_REGS", \ "GENERAL_REGS", \ "ALL_REGS"} #define REG_CLASS_CONTENTS {{0x0,0x0}, \ {0xFFFFFFFF,0x7}, \ {0xFFFFFFFF,0x7}, \ {0xFFFFFFFF,0x7}} #define REGNO_REG_CLASS(regno) (VIRTUAL_REGS) #define BASE_REG_CLASS VIRTUAL_REGS #define INDEX_REG_CLASS NO_REGS GCC usa clases de registros para utilizar los adecuados en los patrones de las insn. La macro enum_class especifica las clases de registros que utilice nuestro backend. A continuación, N_REG_CLASSES especifica el número de clases de registros existentes. REG_CLASS_NAMES es la macro donde especificamos los nombres simbólicos de estas clases de registros. REGNO_REG_CLASS es una macro que toma un registro como argumento y devuelve a qué clase pertenece. Dado que nuestro backend sólo utiliza la clase VIRTUAL_REGS, la macro REGNO_REG_CLASS siempre se evalúará como el valor de ésta. #define SMALL_REGISTER_CLASSES 1 #define REG_CLASS_FROM_LETTER(C) \ (C==’v’? VIRTUAL_REGS : NO_REGS) #define REGISTER_NAMES { "F_REG","F_REG+.01","F_REG+.02","F_REG+.03", "F_REG+.04","F_REG+.05","F_REG+.06","F_REG+.07", "F_REG+.08","F_REG+.09","F_REG+.10","F_REG+.11", "F_REG+.12","F_REG+.13","F_REG+.14","F_REG+.15", "F_REG+.16","F_REG+.17","F_REG+.18","F_REG+.19", "F_REG+.20","F_REG+.21","F_REG+.22","F_REG+.23",
\ \ \ \ \ \ \
Compilador de C para el microcontrolador Microchip PIC18F4550
99
"F_REG+.24","F_REG+.25","F_REG+.26","F_REG+.27", \ "F_REG+.28","F_REG+.29","F_REG+.30","F_REG+.31", \ "Stack_pointer", "Frame_pointer", "Arg_pointer"} La macro SMALL_REGISTER_CLASSES indica el número de subclases de registros disponible en esta arquitectura. Como el número es suficientemente bajo, GCC aplicará optimizaciones adecuadas para arquitecturas con pocas clases. La macro REG_CLASS_FROM_LETTER nos permite identificar la clase de registros virtuales mediante la letra v en las restricciones de los patrones de instrucción. La última macro del bloque anterior, REGISTER_NAMES, corresponde a un vector de cadenas de caracteres que almacena el nombre de cada registro físico. Seguimos con las macros que indican como los valores quedan introducidos en los registros. #define PREFERRED_RELOAD_CLASS(x,class) VIRTUAL_REGS #define HARD_REGNO_NREGS(regno, mode) \ ((GET_MODE_SIZE(mode) + UNITS_PER_WORD - 1) \ / UNITS_PER_WORD) #define HARD_REGNO_MODE_OK(regno, mode) 1 #define MODES_TIEABLE_P(mode1, mode2) 1 #define BASE_REG_CLASS VIRTUAL_REGS #define INDEX_REG_CLASS NO_REGS #define REGNO_OK_FOR_BASE_P(num) \ (num < FIRST_PSEUDO_REGISTER) #define REGNO_OK_FOR_INDEX_P(num) 0 #define CLASS_MAX_NREGS(class,mode) \ ((GET_MODE_SIZE (mode) + UNITS_PER_WORD - 1) \ / UNITS_PER_WORD) PREFERRED_RELOAD_CLASS especifica que durante la fase de recarga sólo deberá asignar registros pertenecientes a la clase de registros virtuales que definimos anteriormente. Con HARD_REGNO_NREGS especificamos el número de registros a partir de regno que hacen falta para almacenar un valor de tipo mode. HARD_REGNO_MODE_ OK devuelve 1 si un valor de tipo mode se puede almacenar en el registro regno: puesto que sólo tenemos una clase de registros, en los cuales se puede almacenar cualquier valor, esta macro siempre devuelve 1. La macro MODES_TIEABLE_P indica que nuestra máquina permite acceder a un valor de un registro sea cual sea el modo de acceso o el del registro. Las siguientes macros
Compilador de C para el microcontrolador Microchip PIC18F4550
100
del bloque anterior sólo especifican para qué registros está permitida, o no, la función indicada; pero dado que solo tenemos una clase de registros, no tienen ningún interés. Las tres macros siguientes determinan el comportamiento de la pila. #define STACK_PUSH_CODE POST_INC #define STACK_POINTER_OFFSET 0 #define PUSH_ROUNDING(npushed) npushed Con estas macros establecemos una pila con post-incremento, sin offset respecto a la cabeza de la pila y sin datos extra almacenados al apilar un valor: si apilamos un dato de dos bytes, la pila aumenta su tamaño exactamente en dos bytes. A continuación mostramos las macros y hooks que determinan cómo debe generar GCC el código para las llamadas a función. #define STARTING_FRAME_OFFSET 0 #define FIRST_PARM_OFFSET(fundec1) 0 #define RETURN_ADDR_RTX(count, frameaddr) \ gen_rtx_MEM(Pmode, \ memory_address(Pmode, \ plus_constant(tem,1)) #undef TARGET_RETURN_POPS_ARGS #define TARGET_RETURN_POPS_ARGS pic_return_pops_args #undef TARGET_FUNCTION_ARG #define TARGET_FUNCTION_ARG pic_function_arg #undef TARGET_FUNCTION_ARG_ADVANCE #define TARGET_FUNCTION_ARG_ADVANCE pic_function_arg_advance #define CUMULATIVE_ARGS int #define INIT_CUMULATIVE_ARGS(cum, fntype, libname, \ fndec1, n_named_args) \ (cum = PIC_FREG_NUM - 1) #define FUNCTION_ARG_REGNO_P(regno) \ (regno < FIRST_PSEUDO_REGISTER) #undef TARGET_FUNCTION_VALUE
Compilador de C para el microcontrolador Microchip PIC18F4550
101
#define TARGET_FUNCTION_VALUE pic_function_value #undef TARGET_FUNCTION_VALUE_REGNO_P #define TARGET_FUNCTION_VALUE_REGNO_P \ pic_function_value_regno_p #define LIBCALL_VALUE libcall_value Las dos primeras macros de este bloque indican que no hay offset ni para el marco de una función respecto al frame pointer, ni para los parámetros pasados a una función respecto al argument pointer. Con RETURN_ADDR_RTX indicamos el insn que se generará para salir de funciones con un tamaño de marco dado. Con el siguiente target hook, TARGET_RETURN_POPS_ARGS, que siempre devuelve 0, indicaremos que no se sacarán automáticamente valores de la pila al salir de una función. Las siguientes cinco macros y target hooks sirven para indicar en qué registros son pasados los parámetros a una función. En realidad, el peso de esta decisión recae sobre la función pic_function_arg, definida en pìc18.c, la cual devuelve el registro a partir del que debemos pasar el dato a una función, para un modo concreto. Las últimas tres macros y target hooks del bloque anterior indican dónde se devolverá el valor de retorno de una función. Las funciones, en pic18.c, determinan si el tipo de estos valores es simple o complejo; y, en función de esto, devolverán un registro si se trata de un dato simple, o un apuntador a la posición de memoria donde esté almacenado el dato si se trata de un tipo de dato complejo. Continuamos con más macros y target hooks de la especificación. #define STACK_POINTER_REGNUM (FIRST_PSEUDO_REGISTER - 3) #define FRAME_POINTER_REGNUM (FIRST_PSEUDO_REGISTER - 2) #define ARG_POINTER_REGNUM (FIRST_PSEUDO_REGISTER - 1) #undef TARGET_FRAME_POINTER_REQUIRED #define TARGET_FRAME_POINTER_REQUIRED \ pic_frame_pointer_required #define DEFAULT_PCC_STRUCT_RETURN 0 #define EPILOGUE_USES(regno) 0 Las tres primeras macros de esta lista indican los registros que utilizará GCC para cada puntero específico: pila, marco y argumentos de función. El target hook TARGET_FRAME_POINTER_REQUIRED almacenará un puntero a una función la
Compilador de C para el microcontrolador Microchip PIC18F4550
102
cual determinará si la función que se va a llamar necesita un frame pointer o no, que en nuestra especificación siempre devuelve true. El propósito de que la siguiente macro, DEFAULT_PCC_STRUCT_RETURN, valga 1 es que el retorno de tipos complejos se efectue mediante un puntero a memoria. La macro EPILOGUE_USES determina si el epílogo de una función utiliza el registro que se le pasa como argumento; como en nuestro diseño los epílogos nunca usan ningún registro, esta macro siempre vale 0. #define CONSTANT_ADDRESS_P(x) pic_constant_address_p(x) #define MAX_REGS_PER_ADDRESS 2 #define REGISTER_MOVE_COST(mode,from,to) 1 #define MEMORY_MOVE_COST(mode,class,in) 10 #define BRACH_COST 1 CONSTANT_ADDRESS_P determina si el rtx corresponde a una dirección de memoria constante. MAX_REGS_PER_ADDRESS especifica que una dirección de memoria estará compuesta, a lo sumo, por dos bytes. Este bloque de macros concluye con las macros de coste de diversas acciones, que permiten a GCC elegir la mejor opción cuando hay varias disponibles. Las macros siguientes indican a GCC qué tiene que escribir en el archivo de salida para los distintos valores y secciones internas. #define #define #define #define #define
TEXT_SECTION_ASM_OP pic_text_section() DATA_SECTION_ASM_OP pic_data_section() BBS_SECTION_ASM_OP pic_bbs_section() READONLY_DATA_SECTION_ASM_OP pic_rodata_section() TARGET_ASM_FUNCTION_RODATA_SECTION \ default_no_function_rodata_section
#define #define #define #define #define #define #define
TARGET_ASM_FILE_START_FILE_DIRECTIVE true TARGET_ASM_FILE_START pic_target_asm_file_start TARGET_ASM_FILE_END pic_target_asm_file_end ASM_COMMENT_START ";" ASM_APP_ON ";APP\n" ASM_APP_OFF ";NO_APP\n" TARGET_HAVE_NAMED_SECTIONS false
#define TARGET_ASM_UNALIGNED_HI_OP NULL
Compilador de C para el microcontrolador Microchip PIC18F4550
103
#define TARGET_ASM_UNALIGNED_SI_OP NULL #define ASM_OUTPUT_ASCII(stream, ptr, len) \ pic_output_ascii(stream, ptr, len) #define IS_ASM_LOGICAL_LINE_SEPARATOR(C, STR) \ ((C) == ’\n’ || (C) == ’$’) #define ASM_OUTPUT_LOCAL(x,y,z,k) \ pic_asm_output_local(x,y,z,k) #define ASM_OUTPUT_COMMON(x,y,z,w) \ pic_asm_output_common(x,y,z,w) #define ASM_OUTPUT_EXTERNAL(x,y,z) \ fprintf(x, "\t extern\t_ %s\n",z) #define SIZE_ASM_OP "\tres\t" #define ASM_DECLARE_FUNCTION_NAME(stream, name, decl) \ pic_asm_declare_function_name(stream, name,decl) #define ASM_DECLARE_OBJECT_NAME(stream, name, decl) \ pic_asm_declare_object_name(stream, name, decl) #define GLOBAL_ASM_OP "\tglobal\t_" #define ASM_GENERATE_INTERNAL_LABEL(STRING, PREFIX, NUM) \ do { \ snprintf(STRING, 100, "_ %s %lu", \ PREFIX, (unsigned long)(NUM)); \ last_fsr=0; \ } while (0) #define HAS_INIT_SECTION #define PRINT_OPERAND(stream, x, code) \ pic_print_operand(stream, x, code) #define PRINT_OPERAND_ADDRESS(stream, x) \ pic_print_operand_address(stream, x) #define ASM_OUTPUT_REG_PUSH(stream, regno) \ if (regno!=0) \ abort(); \ else \ fprintf(stream, "\tPUSH");
Compilador de C para el microcontrolador Microchip PIC18F4550
104
#define ASM_OUTPUT_REG_POP(stream, regno) \ if (regno != 0) \ abort(); \ else \ fprintf(stream, "\tPOP"); #define ASM_OUTPUT_SKIP(stream, nbytes) \ pic_asm_output_skip(stream, nbytes) #define ASM_OUTPUT_ALIGN(stream, nbytes) \ fprintf(stream, ";ALIGN") #define #define #define #define
DBX_DEBUGGING_INFO ASM_STABS_OP "\t;.stabs\t" ASM_STABD_OP "\t;.stabd\t" ASM_STABN_OP "\t;.stabn\t"
#define ASM_OUT(a) output_asm_insn(a, operands); Con todas las explicaciones de esta sección hemos querido mostrar los puntos más importantes de la especificación. Para ampliar la información de las macros y target hooks utilizados, recomendamos consultar la mejor fuente de información existente sobre GCC, el libro GCC Internals, que se distribuye junto con el código fuente de GCC y es posible encontrar una versión online en la web del proyecto: http://gcc.gnu.org/onlinedocs/gccint.
Capítulo 7 Análisis del código generado Durante este capítulo vamos a ver cómo las sentencias básicas en C pasan, gracias a nuestra especificación y los patrones dados, a código ensamblador para el PIC18F4550. A continuación, mostraremos bloques de código concretos para mostrar todos los datos que nuestro compilador escribe en el archivo ensamblador de salida resultante del proceso de compilación de un programa C. Los códigos mostrados son los que genera GCC de manera directa, sin ninguna optimización del middle-end. De esta forma resultará más sencillo comprender cómo se realiza la traducción de patrones. En el día a día de cualquier programador, lo habitual es añadir opciones de optimización que reducen el tamaño del código generado, lo hacen más rápido de ejecutar o incluso eliminan partes de él si GCC predice que no llegarán a ejecutarse.
7.1.
Asignación
Comenzamos con las sentencias de asignación. Algo tan sencillo como la siguiente sentencia: int x = 10; se transforma en la secuencia de instrucciones en ensamblador que realizan la operación equivalente. Para acceder a la memoria, GCC hace uso del puntero de marco de función (frame pointer). Primero carga el valor del frame pointer en una de las parejas de registros de acceso indirecto y, acto seguido, escribe el nuevo valor de la variable en la posición de memoria asignada. Debido a que se trata de una variable de tipo int, de tamaño 2 bytes, la asignación consta de dos escrituras de 1 byte: la primera de ellas tomará el valor bajo del valor constante (macro low) y
105
Compilador de C para el microcontrolador Microchip PIC18F4550
106
escribirá en el registro WREG; de ahí pasará a escribirse en la posición de memoria apuntada por FSR0, a través de INDF0; despues de esto, incrementará FSR0 para que apunte a la siguiente posición de memoria y escribirá la parte alta del valor constante (macro high). En este caso particular encontramos que la parte alta del valor 10 es 0, por lo que no hay necesidad de cargar el registro WREG y luego copiar a INDF0: es posible reemplazar esas dos instrucciones por una sola que ponga a cero el registro destino. movff Frame_pointer+1, FSR0H movff Frame_pointer, FSR0L movlw low D’10’ movwf INDF0 infsnz FSR0L, F incf FSR0H, F clrf INDF0 Si en la declaración de la variable no hubiese una asignación, GCC no emitiría código ensamblador correspondiente, tan sólo se limitaría a llevar la cuenta para sí mismo de cuanto espacio de memoria debe reservar para este marco de función, pero no produciría código ensamblador. El código anterior es exactamente el mismo que el que GCC producirá para las instrucciones siguientes: int x; x = 10; Aunque hubiese otras instrucciones entre la declaración de la variable y la posterior asignación a partir de un valor constante, el compilador sólo emitirá código ensamblador en el punto del programa correspondiente a la asignación. La declaración de más de una variable en un mismo marco de función resulta en un manejo más complejo, por parte de GCC, del frame pointer. int x = 10; int y = 25; El resultado de compilar las dos sentencias anteriores es un código como el siguiente: movff Frame_pointer+1, FSR0H movff Frame_pointer, FSR0L movlw low D’10’
Compilador de C para el microcontrolador Microchip PIC18F4550
107
movwf INDF0 infsnz FSR0L, F incf FSR0H, F clrf INDF0 movff Frame_pointer, F_REG+.16 movff Frame_pointer+1, F_REG+.16+1 movlw low D’2’ addwf F_REG+.16, F movlw high D’2’ addwfc F_REG+.16+1, F movff F_REG+.16+1, FSR0H movff F_REG+.16, FSR0L movlw low D’25’ movwf INDF0 infsnz FSR0L, F incf FSR0H, F clrf INDF0 Por claridad, este bloque de código está dividido en tres bloques más pequeños, cada uno de ellos con un propósito concreto. Las siete primeras instrucciones son análogas a las del ejemplo anterior: cargan en un registro de acceso indirecto la dirección base del marco de esta función y, a continuación, almacenan en dicha posición los dos bytes que componen el valor de la variable x, de tipo int. El segundo bloque de código carga la dirección base del marco de función en registros de propósito general para, acto seguido, incrementarla en dos unidades (los dos bytes que corresponden a la variable x, que ya se encuentran asignados). El último bloque es muy semejante al primero. Dejando a un lado la diferencia obvia de que almacena un valor distinto en la memoria direccionada mediante FSR0, lo más destacable de este bloque es que copia en la pareja de registros FSR0 el valor que se calculó en el bloque dos, valor que corresponde a la posición de memoria asignada a la variable y, contigua a x. De estos ejemplos se puede ver que el código producido por GCC es bastante redundante y simple. Una forma más eficiente de hacer las asignaciones anteriores pasa por almacenar las posiciones de memoria absolutas directamente en registros y evitar con esto el tener que calcular las relativas al frame pointer cada vez que haya que acceder de nuevo a esas posiciones. Debemos recordar que estos ejemplos son
Compilador de C para el microcontrolador Microchip PIC18F4550
108
el resultado de la traducción de C a ensamblador sin ninguna optimización. Con un nivel mínimo de optimizaciones, GCC probablemente ni siquiera cargará esos valores constantes (a menos que cambiase su valor en algún momento del programa), dado que sabe que los accesos a memoria son muy costosos y, desde el nivel más bajo de optimización, procura reducirlos al mínimo y emplear registros siempre que sea posible.
7.2.
Operaciones matemáticas simples
7.2.1.
Suma
Pasamos a estudiar las operaciones matemáticas. Todas tienen una forma parecida, a pesar de que las instrucciones en ensamblador empleadas varía de una a otra. Comenzamos con la operación de suma. Supongamos que x e y son variables de tipo entero, y que ya han sido declaradas e inicializadas con valores arbitrarios. x = x + y; El código generado para la sentencia C anterior es: movff Frame_pointer, F_REG+.16 movff Frame_pointer+1, F_REG+.16+1 movlw low D’2’ addwf F_REG+.16, F movlw high D’2’ addwfc F_REG+.16+1, F movff Frame_pointer+1, FSR0H movff Frame_pointer, FSR0L movff INDF0, F_REG+.18 infsnz FSR0L, F incf FSR0H, F movff INDF0, F_REG+.18+1 movff F_REG+.16+1, FSR0H movff F_REG+.16, FSR0L movff INDF0, F_REG+.16
Compilador de C para el microcontrolador Microchip PIC18F4550
109
infsnz FSR0L, F incf FSR0H, F movff INDF0, F_REG+.16+1 movf F_REG+.18, W addwf F_REG+.16, F movf F_REG+.18+1, W addwfc F_REG+.16+1, F movff Frame_pointer+1, FSR0H movff Frame_pointer, FSR0L movff F_REG+.16, INDF0 infsnz FSR0L, F incf FSR0H, F movff F_REG+.16+1, INDF0 Examinando por bloques, observamos cómo en los dos primeros bloques se preparan el apuntador a la variable cuya posición de memoria corresponde a dos bytes más allá de la base del frame pointer. Este puntero queda almacenado en los registros 16 y 17. El código del bloque tres carga en los registros 18 y 19 el contenido de la variable cuya memoria corresponde con la base del frame pointer. Una vez hecho esto, el bloque cuatro, haciendo uso del apuntador obtenido en los dos primeros bloques, carga en los registros 16 y 17 el valor de la otra variable que interviene en la suma, destruyendo el apuntador durante el proceso. El quinto bloque es el que efectúa la operación de suma. En él aparecen dos instrucciones de suma. Esto se debe a que los sumandos son de 2 bytes cada uno y el PIC18 trabaja con palabras de datos de 1 byte, por lo que hay que tener en cuenta el bit de acarreo después de hacer la suma de los bytes menos significativos de cada sumando y sumarlo al resultado de sumar los dos bytes más significativos. Los bytes resultantes de la suma quedan almacenados en los registros 16 y 17. El último bloque es el que almacena en memoria, en la primera variable del marco de la función actual, el resultado de la suma efectuada en el bloque anterior. Es conveniente destacar que la elección de registros la hace GCC, teniendo en cuenta unas reglas de preferencia que le hemos especificado y, por supuesto, los que se encuentran disponibles para utilizar. También merece la pena llamar la atención sobre que, a priori, no podríamos asegurar en qué posiciones del marco de función se encuentrará cada variable implicada. En cambio, una vez analizado el código vemos que la variable x es la que se encuentra en la dirección apuntada por el frame
Compilador de C para el microcontrolador Microchip PIC18F4550
110
pointer mientras que a y corresponden los bytes 3 y 4, tomando como referencia el mismo puntero.
7.2.2.
Resta
Veamos el caso de la operación de resta. x = x - y; El código generado para esta sentencia es: movff Frame_pointer, F_REG+.16 movff Frame_pointer+1, F_REG+.16+1 movlw low D’2’ addwf F_REG+.16, F movlw high D’2’ addwfc F_REG+.16+1, F movff Frame_pointer+1, FSR0H movff Frame_pointer, FSR0L movff INDF0, F_REG+.18 infsnz FSR0L, F incf FSR0H, F movff INDF0, F_REG+.18+1 movff F_REG+.16+1, FSR0H movff F_REG+.16, FSR0L movff INDF0, F_REG+.16 infsnz FSR0L, F incf FSR0H, F movff INDF0, F_REG+.16+1 movff F_REG+.18+1, F_REG+.20+1 movff F_REG+.18, F_REG+.20 movf F_REG+.16, W subwf F_REG+.20, F movf F_REG+.16+1, W
Compilador de C para el microcontrolador Microchip PIC18F4550
111
btfsc STATUS, N incfsz F_REG+.16+1, W subwf F_REG+.20+1, F movff F_REG+.20, F_REG+.16 movff F_REG+.20+1, F_REG+.16+1 movff Frame_pointer+1, FSR0H movff Frame_pointer, FSR0L movff F_REG+.16, INDF0 infsnz FSR0L, F incf FSR0H, F movff F_REG+.16+1, INDF0 El código que nos interesa es el del sexto bloque, que es el que efectúa la operación de resta de los dos valores. El acceso a los valores en memoria, carga de los registros (elegidos por GCC) y el posterior almacenamiento en memoria del resultado, siempre tiene la misma forma. Puesto que el código de accesos a memoria tanto para carga como almacenamiento de los registros se repite en todas las operaciones y ya ha sido descrito con suficientes detalles, a partir de aquí obviaremos dicho código y nos centraremos en el bloque de código que efectúa la operación a tratar. Volvamos a la resta, quedándonos con la parte interesante: movf F_REG+.16, W subwf F_REG+.20, F movf F_REG+.16+1, W btfsc STATUS, N incfsz F_REG+.16+1, W subwf F_REG+.20+1, F En las dos primeras líneas se efectúa la resta de la parte baja del sustraendo (contenido en el registro 16) al minuendo (almacenado en el registro 20). El resultado queda alojado en este último registro. A continuación se efectúa la resta de los bytes más significativos, estando el MSB del minuendo en el registro 21 y el MSB del sustraendo en el 17. Antes de ejecutar la instrucción de resta se comprueba si la resta de los bytes menos significativos produjo acarreo. En caso afirmativo, incrementará en uno el MSB del sustraendo antes de la instrucción de resta.
Compilador de C para el microcontrolador Microchip PIC18F4550
7.2.3.
112
Negación
El operador unario de negación puede implementarse de varias maneras. La implementación elegida para nuestro back-end hace uso de las instrucciones de complemento. La siguiente sentencia C: x = -y; resulta en el siguiente código ensamblador: comf F_REG+.16+1, F negf F_REG+.16 btfsc STATUS, C incf F_REG+.16+1, F El bloque de código que efectúa la operación de negación (complemento a 2) trabaja de arriba hacia abajo, es decir, comienza por los bytes más significativos, haciendo su complemento a 1, hasta llegar al LSB, el cual complementa a 2 y luego propaga el acarreo byte a byte hasta llegar al MSB. Este mismo algoritmo sirve para números enteros de cualquier tamaño.
7.2.4.
Valor absoluto
La operación de valor absoluto, como concepto, resulta muy sencilla pero por desgracia el PIC18 no ofrece ninguna instrucción que facilite su implementación. Sin embargo, es posible implementarla tal como la enseñan en una clase de matemáticas de nivel básico: “si el signo es negativo, se lo cambiamos; si no, se deja tal cual”. Así, haciendo uso de las instrucciones empleadas para la negación y una sencilla comprobación del bit de signo tenemos implementada esta operación. x = abs(y); Este ejemplo se traduce en el siguiente código ensamblador: btfss F_REG+.16+1, 7 bra _L2 comf F_REG+.16+1, F negf F_REG+.16 btfsc STATUS, C incf F_REG+.16+1, F _L2
Compilador de C para el microcontrolador Microchip PIC18F4550
113
La primera línea comprueba el bit más significativo del MSB que compone el valor. En caso de que éste valga uno, salta una única instrucción, pasando a ejecutar las líneas tres y sucesivas, que efectúan la negación del número. Si el bit de signo vale cero, ejecuta la instrucción de salto que carga PC con la dirección de la etiqueta ubicada al final del bloque, dando por finalizada la operación de valor absoluto.
7.3.
Operaciones lógicas
7.3.1.
AND, OR y XOR
El PIC18 puede realizar la mayoría de operaciones lógicas directamente. Una vez que tenga el dato en un registro basta con realizar la operación sobre el mismo. Con esta facilidad es posible realizar las operaciones de disyunción (AND), conjunción (OR) y exclusión (XOR) lógicas. Veamos un ejemplo ilustrativo de una operación AND sobre un valor de tipo entero. x = x & y; Obviando el código correspondiente a la carga y almacenamiento de los registros empleados por el compilador, el código resultante de traducir a ensamblador esta sentencia C es el siguiente: movf F_REG+.18, W andwf F_REG+.16, F movf F_REG+.18+1, W andwf F_REG+.16+1, F Las otras operaciones son idénticas en forma. Las instrucciones andwf, pasan a ser iorwf para la disyunción y xorwf si se trata de una operación de exclusión lógica.
7.3.2.
Negación Lógica o Complemento a 1
El caso del complemento a uno es sensiblemente distinto a las anteriores operaciones, ya que al tratarse de un operador unario puede suceder que GCC decida emplear el mismo registro como entrada y como salida, por lo que el código resultante suele ser algo más corto que para las operaciones de dos operandos. x = ~y; El código que nos interesa, resultante de compilar la negación de este ejemplo, es el siguiente:
Compilador de C para el microcontrolador Microchip PIC18F4550
114
comf F_REG+.16, F comf F_REG+.16+1, F
7.3.3.
Desplazamientos
El desplazamiento a nivel de bits es otra operación complicada en un microcontrolador PIC debido a lo reducido de su conjunto de instrucciones. A priori, uno puede pensar en dos tipos posibles: desplazamiento hacia la izquierda y desplazamiento hacia la derecha. No obstante, en el desplazamiento hacia la derecha hay dos variantes, dependiendo de si se trata de un valor con o sin signo, y su implementación es diferente. int x = 10; char y = 2; x = x >> y; El operador de desplazamiento, para este ejemplo, produce el código siguiente: movf F_REG+.16, W bra _L3 _L2 bcf STATUS, C btfsc F_REG+.20+1, 7 bsf STATUS, C rrcf F_REG+.20+1, F rrcf F_REG+.20, F addlw 0xFF _L3 btfss STATUS, Z bra _L2 El PIC18 sólo dispone de instrucciones para desplazar un único bit, por lo que hay que emplear un contador y un bucle que se ejecuta mientras el contador no haya llegado a cero. El código resultante emplea WREG como contador y en la instrucción ubicada en la segunda etiqueta es donde efectúa la comprobación: si WREG no es cero, salta a la primera etiqueta y carga en el bit de acarreo el valor del bit de mayor peso; una vez hecho esto, hace el desplazamiento y decrementa en una unidad el valor del contador. Para desplazamientos hacia la derecha de valores sin signo no es necesario comprobar el bit más significativo del dato, ya que siempre se introduce un cero. Así
Compilador de C para el microcontrolador Microchip PIC18F4550
115
que basta con asegurarse de poner a cero el bit de acarreo, que es el que entra por la izquierda cada vez que se ejecuta una instrucción de desplazamiento hacia la derecha. unsigned int x = 10; char y = 2 x = x >> y; Este ejemplo se traduce en el siguiente código: movf F_REG+.16, W bra _L3 _L2 bcf STATUS, C rrcf F_REG+.20+1, F rrcf F_REG+.20, F addlw 0xFF _L3 btfss STATUS, Z bra _L2 Como podemos ver, la única diferencia con el desplazamiento de valores con signo es que en el cuerpo del bucle no se efectúa la comprobación del bit 7 del MSB. En los desplazamientos hacia la izquierda no existe ninguna diferencia entre valores con o sin signo, ya que en ambos casos entra un cero por la derecha. El código resultante es idéntico al desplazamiento hacia la derecha de valores sin signo, tan sólo cambiando las instrucciones rrcf por rlcf.
7.4.
Conversión de tipos o casting
7.4.1.
Extensión de signo
Normalmente, cuando queremos usar datos de mayor rango necesitamos usar tipos nuevos. Por ejemplo, si queremos calcular un número por encima del valor máximo de los enteros con signo, 32767, necesitaremos utilizar un tipo que lo permita, como long (entero largo de 4 bytes), cuyo valor máximo es 2147483647. Para convertir de uno al otro, en C hacemos uso de lo que se denomina casting de tipos. int x = 10; long y = x;
Compilador de C para el microcontrolador Microchip PIC18F4550
116
Para esta sencilla operación, que básicamente consiste en cargar en la variable y el valor de x, necesitamos rellenar los bytes de la variable y, de cuatro bytes, que no quedan cubiertos por la variable x, ya que ésta es de sólo dos bytes. GCC utiliza la operación de extensión de signo, necesaria entre todos los tipos a los que se quiera tener la posibilidad de ampliación. El código generado para este ejemplo es: movff Frame_pointer+1, FSR0H movff Frame_pointer, FSR0L movff INDF0, F_REG+.18 infsnz FSR0L, F incf FSR0H, F movff INDF0, F_REG+.18+1 movlw btfsc movlw movwf movwf
0x00 F_REG+.18+1, 7 0xff F_REG+.18+2 F_REG+.18+3
Como vemos, el primer bloque del resultado se encarga de cargar en registros el valor de la variable de entrada, de sólo dos bytes. Sin embargo, la variable de salida, la asignada, es de cuatro bytes. El valor de los dos bytes más significativos dependerá del bit de signo de la variable de entrada. El cometido del segundo bloque es comprobar el valor de dicho bit y extenderlo por los dos bytes adicionales que conforman la variable asignada.
7.4.2.
Extensión de ceros
La extensión de valores sin signo se denomina, dentro del proyecto GCC, zeroextension que podemos traducirla como extensión de ceros. El algoritmo de la extensión de ceros es tan sencillo como poner a cero los bytes de la variable asignada que no corresponden con la variable de entrada. unsigned int x = 10; long y = x; El resultado de la zero-extension anterior es: movff Frame_pointer+1, FSR0H movff Frame_pointer, FSR0L
Compilador de C para el microcontrolador Microchip PIC18F4550
117
movff INDF0, F_REG+.18 infsnz FSR0L, F incf FSR0H, F movff INDF0, F_REG+.18+1 clrf F_REG+.18+2 clrf F_REG+.18+3 Igual que ocurre con la extensión de signo, para la zero-extension primero se carga en registros el valor de entrada. A continuación, el segundo bloque de código se limita a poner a cero los registros asignados a los bytes de más peso de la variable de salida. Dada la limitación de memoria de un PIC, lo más aconsejable es trabajar siempre con el tipo de dato más pequeño que permita el rango de valores necesario para cada propósito; es un desperdicio de memoria, por ejemplo, emplear una variable de tipo long para un dato leído del conversor analógico-digital configurado a 8 bits, cuando el tipo short permite almacenar todos los valores de ese rango. Lo aconsejable es mantener el dato en una variable del tipo más pequeño que pueda albergarlo hasta que sea necesario, por el motivo que sea, un mayor rango numérico.
7.5.
Operaciones matemáticas complejas
7.5.1.
Multiplicación
Durante la etapa de diseño tomamos la decisión de implementar una solución combinada para la multiplicación: para datos pequeños haremos uso del multiplicador hardware, por lo que el código resultante es análogo al producido para la suma y la resta. Para datos grandes optaremos por hacer una llamada a la biblioteca de funciones, de modo que el tamaño de nuestro programa no creciese más de lo estrictamente necesario. Veamos un ejemplo de multiplicación de datos grandes. long x; int y = 10; int z = 25; x = y * z; Esto se traduce en lo siguiente:
Compilador de C para el microcontrolador Microchip PIC18F4550
118
movff F_REG+.18+1, F_REG+.26+1 movff F_REG+.18, F_REG+.26 movff F_REG+.16+1, F_REG+.28+1 movff F_REG+.16, F_REG+.28 call _mulhisi movff movff movff movff
F_REG+.28, F_REG+.16 F_REG+.28+1, F_REG+.16+1 F_REG+.28+2, F_REG+.16+2 F_REG+.28+3, F_REG+.16+3
Las funciones matemáticas tienen un comportamiento particular respecto a las funciones definidas por el usuario en lo que se refiere al paso de argumentos, pero eso lo veremos más adelante. Ahora centrémonos en las funciones matemáticas de nuestra biblioteca. El paso de argumentos y el retorno del resultado es específico para cada función de la biblioteca, y lo mismo sucede con las variables que emplea cada una de estas funciones. GCC no decide sobre la marcha qué registros servirán para cada propósito dentro de estas funciones, esta decisión es fija en el código de la biblioteca y GCC sabe cómo comunicarse con estas funciones, sin interferir en el buen funcionamiento de éstas y evitando que dichas funciones hagan lo propio con el código que sí genera el compilador. En el código resultante del ejemplo que estamos tratando distinguimos cuatro bloques. Los dos primeros cargan en los registros 26 y 27 uno de los operandos y en los registros 28 y 29 el segundo operando. Esto es así porque la función mulhisi exige que los operandos estén almacenados en esos registros. Al finalizar su ejecución (la llamada a mulhisi) encontraremos el resultado de la multiplicación en los registros 28 a 31 (la elección de estos registros no la efectúa GCC de manera dinámica, sino que están predefinidos en el cuerpo de esta función). El cuarto bloque carga en los registros asignados a la variable x el resultado de multiplicar y por z.
7.5.2.
División
Veamos ahora un ejemplo de la operación de división. int x; int y = 10;
Compilador de C para el microcontrolador Microchip PIC18F4550
119
int z = 25; x = y / z; El resultado de traducir la división de dos números enteros de 2 bytes es un código como éste: movff F_REG+.18+1, F_REG+.30+1 movff F_REG+.18, F_REG+.30 movff F_REG+.16+1, F_REG+.28+1 movff F_REG+.16, F_REG+.28 call _divmodhi movff F_REG+.30, F_REG+.16 movff F_REG+.30+1, F_REG+.16+1 Encontramos el mismo esqueleto que para la operación de multiplicación: carga de un operando, carga del otro, llamada a la función de la biblioteca y recuperación del resultado deseado. Las funciones de división, además de las particularidades que tienen por tratarse de código ensamblador escrito a mano, son peculiares porque en realidad tienen un doble cometido: obtener cociente y resto de una operación de división. Una única llamada a una de estas funciones, como divmodhi, deja el valor del cociente resultante en unos registros y el resto en otros, por lo que las sentencias en C de división y módulo resultan en un código ensamblador prácticamente idéntico, con la diferencia de que, tras la llamada a función, cogerán el resultado de unos registros u otros.
7.6.
Estructuras condicionales o de selección
7.6.1.
if
Empezamos a examinar el código generado para las estructuras de control. La más sencilla es la estructura de ejecución condicional if. En este salto, la expresión del if es evaluada para decidir si se ejecutará el bloque then siguiente o no. La implementación de las estructuras if tiene dos partes: comparación y salto. Cuando la condición de la estructura if es una comparación entre dos datos de un
Compilador de C para el microcontrolador Microchip PIC18F4550
120
byte, el código ensamblador en el que se traduce tiene bien diferenciados los bloques de comparación y de salto. Sin embargo, cuando se trata de comparar, por ejemplo, dos valores de tipo int, ambas partes se mezclan dando lugar a un pequeño combinado de comparaciones, pequeños saltos y un gran salto, que es el que marca el final de la comparación de la estructura if. Lo vemos con un ejemplo de comparación entre dos valores de tipo int. if (x != y) { ... } Esto se traduce en el código siguiente:
_L3
movf F_REG+.16, W xorwf F_REG+.18, W btfss STATUS, Z goto _L3 movf F_REG+.16+1, W xorwf F_REG+.18+1, W btfsc STATUS, Z goto _L1 ...
_L1 Los puntos suspensivos representan el bloque then, cuyo contenido es irrelevante para nuestra explicación. No obstante, para mostrar dónde aparece el código correspondiente al bloque then, también los hemos incluído en el ensamblador generado por GCC. En este ejemplo sólo se ejecutará el bloque then si las dos variables evaluadas son diferentes. GCC genera una comparación de igualdad y un salto a la etiqueta L1 en caso afirmativo, es decir, salta si no se cumple la condición. Las dos primeras líneas comparan, mediante la instrucción de exclusión lógica, los bytes menos significativos que componen las variables implicadas y si el resultado es cero (bit Z del registro de estado puesto a uno), es que ambos son iguales y hay que comparar los bytes más significativos; en caso contrario los valores son distintos (no hay necesidad de comparar los otros dos bytes) y salta a la etiqueta L3, donde se efectúa la comparación final y evita, o no, la ejecución del bloque then. En
Compilador de C para el microcontrolador Microchip PIC18F4550
121
la comparación de los bytes altos, tras ejecutar la instrucción de exclusión lógica, comprueba otra vez si el bit Z está puesto a cero y, en caso de que fuese así (porque los valores sean iguales), ejecutaría el salto incondicional goto hacia la etiqueta L1, lo que significa no ejecutar el bloque then. Por analogía, para el caso de que la comparación fuese de igualdad, el código resultante sería exactamente igual en forma y tan sólo cambiaría la última comprobación, que pasaría a efectuar el salto incondicional si el bit Z no está puesto a cero. btfss STATUS, Z Existen otras ocho posibles comparaciones entre dos valores aparte de las dos que acabamos de analizar. En realidad son sólo cuatro, pero hay que distinguir entre comparaciones de valores con signo y sin él. Estas comparaciones son mayor-que, mayor-o-igual-que, menor-que y menor-o-igual-que. La implementación de estas comparaciones se apoyan en la instrucción de resta. El procedimiento es tan sencillo como restar los operandos y comprobar el signo del resultado. if (x <= y) { ... } Esta estructura se traduce en: movf F_REG+.18, W subwf F_REG+.16, W movf F_REG+.18+1, W btfss STATUS, C incfsz F_REG+.18+1, W subwf F_REG+.16+1, W andlw 0x80 btfss STATUS, Z goto _L1 ... ... ... _L1
Compilador de C para el microcontrolador Microchip PIC18F4550
122
El código resultante de esta compilación resta x a y y comprueba el bit de signo resultante. En caso de que valga uno (resultado negativo, lo que implicaa que x es mayor que y), ejecuta el cuerpo del bloque then. De lo contrario, ejecuta el goto hacia la etiqueta L1. El código para la comparación mayor-o-igual-que es idéntico a éste, tan sólo se intercambian los operandos. Cuando la comparación es una desigualdad estricta, el código que resulta es también muy parecido al de las desigualdades no-estrictas, teniendo en cuenta que unas son complementarias de las otras basta cambiar la condición de salto. Así, el código para una comparación A=B, pero cambiando la comprobación del bit Z, donde ahora hay que comprobar que valga cero. Para verlo con más claridad, el listado siguiente muestra el resultado de traducir la misma estructura if anterior para cada una de las cuatro comparaciones: ; (x < y) movf F_REG+.16, W subwf F_REG+.18, W movf F_REG+.16+1, W btfss STATUS, C incfsz F_REG+.16+1, W subwf F_REG+.18+1, W andlw 0x80 btfsc STATUS, Z goto _L1 ; (x > y) movf F_REG+.18, W subwf F_REG+.16, W movf F_REG+.18+1, W btfss STATUS, C incfsz F_REG+.18+1, W subwf F_REG+.16+1, W andlw 0x80 btfsc STATUS, Z goto _L1 ; (x >= y) movf F_REG+.16, W subwf F_REG+.18, W
Compilador de C para el microcontrolador Microchip PIC18F4550
123
movf F_REG+.16+1, W btfss STATUS, C incfsz F_REG+.16+1, W subwf F_REG+.18+1, W andlw 0x80 btfss STATUS, Z goto _L1 ; (x <= y) movf F_REG+.18, W subwf F_REG+.16, W movf F_REG+.18+1, W btfss STATUS, C incfsz F_REG+.18+1, W subwf F_REG+.16+1, W andlw 0x80 btfss STATUS,Z goto _L1 Para valores sin signo, la comparación funciona de forma idéntica a la mostrada, haciendo uso de la exclusión lógica para comparaciones de igualdad y la resta, con los oportunos intercambios de operandos en el resto de comparaciones. Varía, sin embargo, la comprobación final, ya que para valores con signo comprueba el bit más alto del resultado pero no es posible hacer lo mismo con operandos sin signo. Sin embargo, en los valores sin signo no es factible comprobar el signo del resultado: hay que emplear el acarreo. En la resta si el valor del minuendo es mayor o igual que el sustraendo obtenemos acarreo y en caso contrario no. Por lo tanto, en un menor-que sin signo, sólo tenemos que comprobar que no haya acarreo. Esto indica que el primer valor es menor que el segundo. Para el caso de mayor-o-igual-que miramos que sí se produzca acarreo en la resta. Además, como antes, no usaremos para un mayor-que la comparación directa, ya que nos obligaría a ver que existe acarreo pero además que el resultado no es cero. En su lugar intercambiamos los operandos y usamos el menor-que. Hemos analizado el principal salto condicional y su traducción, por parte de GCC, a comparación y salto. Las demás estructuras de control usan mecanismos similares a los que acabamos de ver, con distinta preparación pero idénticos procesos de comparación.
Compilador de C para el microcontrolador Microchip PIC18F4550
7.6.2.
124
switch
La estructura switch basa su funcionamiento en if condicionales de igualdad. La expresión a evaluar con switch se va comparando, por orden de aparición, con el valor de cada caso (case) y, en caso de cumplirse la igualdad se ejecuta el cuerpo correspondiente a dicho caso hasta llegar a una sentencia break o, en su defecto, llegar al final del cuerpo de la estructura switch. Si la expresión no se corresponde con ningún case, no se ejecutará código alguno, a menos que exista un caso por defecto (default). La estructura general es la siguiente: switch (x) { case ’a’: ... break;
/* Bloque A */
case ’b’: ... break;
/* Bloque B */
case ’c’: ... break;
/* Bloque C */
case ’d’: ... break;
/* Bloque D */
case ’e’: ... break;
/* Bloque E */
default: ... break; }
/* Bloque Default */
Su traducción a ensamblador es:
Compilador de C para el microcontrolador Microchip PIC18F4550 movlw low D’99’ xorwf F_REG+.16, W btfss STATUS, Z goto _L15 movlw high D’99’ xorwf F_REG+.16+1, W
; x == ’c’?
_L15 btfsc STATUS, Z goto _L10 movf F_REG+.16, W sublw low D’99’ movf F_REG+.16+1, W btfss STATUS, C incfsz F_REG+.16+1, W sublw high D’99’ andlw 0x80 btfss STATUS, Z goto _L8
; x < ’c’?
movlw low D’97’ xorwf F_REG+.16, W btfss STATUS, Z goto _L16 movlw high D’97’ xorwf F_REG+.16+1, W
; x == ’a’?
_L16 btfsc STATUS, Z goto _L11 movlw low D’98’ xorwf F_REG+.16, W btfss STATUS, Z goto _L17 movlw high D’98’ xorwf F_REG+.16+1, W _L17 btfsc STATUS, Z
; x == ’b’?
125
Compilador de C para el microcontrolador Microchip PIC18F4550 goto _L12 ; En otro caso -> default goto _L2 _L8: movlw low D’100’ xorwf F_REG+.16, W btfss STATUS, Z goto _L18 movlw high D’100’ xorwf F_REG+.16+1, W
; x == ’d’?
_L18 btfsc STATUS, Z goto _L13 movlw low D’101’ xorwf F_REG+.16, W btfss STATUS, Z goto _L19 movlw high D’101’ xorwf F_REG+.16+1, W
; x == ’e’?
_L19 btfsc STATUS, Z goto _L14 _L2: ... goto _L1
; Bloque Default
... goto _L1
; Bloque C
... goto _L1
; Bloque A
... goto _L1
; Bloque B
...
; Bloque D
_L10:
_L11:
_L12:
_L13:
126
Compilador de C para el microcontrolador Microchip PIC18F4550
127
goto _L1 _L14: ... nop
; Bloque E
_L1: El código resultante es notablemente más extenso que el de un if simple, debido a que no deja de ser una sucesión de estructuras if-else. A pesar de su tamaño, es sencillo de entender; más aún con los comentarios añadidos para el ejemplo. GCC no efectúa las comparaciones en el orden en que aparecen los case en el código C original, sino que opta por ordenar los valores especificados en tiempo de compilación, y genera el código ensamblador correspondiente a una búsqueda binaria. Comenzando con la primera comparación, si la expresión, cuyo valor está previamente alojado en los registros 16 y 17, es igual que el valor comparado (99 en nuestro ejemplo, correspondiente al carácter c en código ASCII), ejecuta el salto a la etiqueta L10, que marca el comienzo del código ensamblador correspondiente al bloque C. En caso de no satisfacerse la igualdad, comprueba si la expresión es menor o mayor; y, según sea, desciende por una rama del árbol de búsqueda o por la otra. Este proceso se repite sucesivamente hasta llegar a alguna hoja del árbol de búsqueda binaria y, en caso de llegar a una hoja y que ésta no corresponda al valor de la expresión evaluada, ejecuta el código correspondiente al bloque default y concluye con un salto incondicional a la etiqueta L1, la cual corresponde a la primera instrucción fuera de la estructura switch y da por finalizada la ejecución de esta estructura de selección.
7.7.
Estructuras iterativas o bucles
Los bucles, junto con las estructuras condicionales y la ejecución secuencial, son las bases de la programación estructurada. Con ellos es posible implementar cualquier algoritmo existente, otorgándonos la potencia necesaria para enfrentar todos los problemas resolubles.
7.7.1.
while
La estructura while ejecuta su cuerpo una y otra vez mientras la condición de su cabecera sea verdadera. Veamos un ejemplo de cómo se traduce a ensamblador un bucle while: while (x < 10)
Compilador de C para el microcontrolador Microchip PIC18F4550
128
{ ... } El código mostrado arriba, una vez más, no muestra el contenido del cuerpo del bucle ya que es irrelevante para nuestra explicación. En su lugar hemos optado por representarlo mediante puntos suspensivos cuya traducción al ensamblador, igual que con los ejemplos anteriores, no tiene traducción, por lo que aparecerán tal cual. goto _L2 _L3: ... _L2: movf F_REG+.16, W sublw low D’9’ movf F_REG+.16+1, W btfss STATUS, C incfsz F_REG+.16+1, W sublw high D’9’ andlw 0x80 btfsc STATUS, Z goto _L3
7.7.2.
do-while
En la variante do-while, al contrario que con el while, la comprobación se efectúa al final de la primera ejecución del cuerpo, por lo que éste siempre se ejecuta al menos una vez. No es difícil deducir que eliminando la primera instrucción goto de la compilación anterior obtendríamos el comportamiento de una estructura do-while y así lo considera también GCC. do { ... } while (x<10); Queda en su conversión a ensamblador como: _L2: ...
Compilador de C para el microcontrolador Microchip PIC18F4550
129
movf F_REG+.16, W sublw low D’9’ movf F_REG+.16+1, W btfss STATUS, C incfsz F_REG+.16+1, W sublw high D’9’ andlw 0x80 btfsc STATUS, Z goto _L2
7.7.3.
for
La estructura for, además de la condición de ejecución del cuerpo, incluye un bloque de inicialización, que se ejecuta una sola vez antes de comenzar el bucle y de hacer la primera comprobación de la condición, y un bloque de actualización, la cual se ejecuta al final de cada iteración, también justo antes de comprobar la condición de iteración. Vemos cómo traduce nuestro port de GCC un bucle for: for (i=0; i<10; i++) { ... } El código que genera GCC es el siguiente: ; Inicialización movff Frame_pointer+1, FSR0H movff Frame_pointer, FSR0L clrf INDF0 infsnz FSR0L, F incf FSR0H, F clrf INDF0 goto _L2 _L3: ...
; Cuerpo del bucle
; Actualización
Compilador de C para el microcontrolador Microchip PIC18F4550
130
movff Frame_pointer+1, FSR0H movff Frame_pointer, FSR0L movff INDF0, F_REG+.16 infsnz FSR0L, F incf FSR0H, F movff INDF0, F_REG+.16+1 movlw low D’1’ addwf F_REG+.16, F movlw high D’1’ addwfc F_REG+.16+1, F movff Frame_pointer+1, FSR0H movff Frame_pointer, FSR0L movff F_REG+.16, INDF0 infsnz FSR0L, F incf FSR0H, F movff F_REG+.16+1, INDF0 _L2: ; Condición movff Frame_pointer+1, FSR0H movff Frame_pointer, FSR0L movff INDF0, F_REG+.16 infsnz FSR0L, F incf FSR0H, F movff INDF0, F_REG+.16+1 movf F_REG+.16,W sublw low D’9’ movf F_REG+.16+1,W btfss STATUS,C incfsz F_REG+.16+1,W sublw high D’9’ andlw 0x80 btfsc STATUS,Z goto _L3 Como hicimos con la estructura switch, hemos incluído unos mínimos comentarios en el código ensamblador para que resulte más sencillo de comprender.
Compilador de C para el microcontrolador Microchip PIC18F4550
131
El primer bloque es el que realiza la sentencia de inicialización, que en este ejemplo se limita a poner a cero una variable de dos bytes. A continuación salta hasta la posición etiquetada como L2, donde comienza el bloque que efectúa la comprobación de la condición de iteración. En caso de resultar verdadera esta comprobación, ejecuta el cuerpo del bucle (etiqueta L3) y, acto seguido, los tres bloques correspondientes al apartado de actualización de la cabecera del for. Una vez hecho esto, entra de nuevo en el bloque etiquetado con L2 y el proceso se repite hasta que la condición de iteración es falsa y el bucle termina.
7.8.
Funciones
El empleo de funciones permite escribir programas más legibles, mejor estructurados y más fáciles de depurar si se emplean debidamente. Asimismo, el uso de llamadas a función permite reutilizar código sin necesidad de duplicarlo, ahorrando notablemente el consumo de memoria de programa. Por todo esto, resulta muy interesante disponer de esta característica a la hora de trabajar con microcontroladores, donde la memoria es escasa. En nuestra implementación distinguimos dos tipos de funciones, las denominadas genéricas y la función principal main, a la que tratamos de un modo diferente por ser la función que engloba a todo el programa. El motivo de tratar a la función main de manera diferente al resto de funciones se debe a que es la función inicial del programa: por encima de ella no hay ninguna otra función, así que las tareas previas y póstumas de ésta son diferentes.
7.8.1.
Genéricas
Veamos la siguiente función: int funcion (int x) { int y = 10; if (y != x) x++; return x; }
Compilador de C para el microcontrolador Microchip PIC18F4550
132
Toda función tiene un prólogo que podemos dividir en dos partes: salvar los registros que deban conservar su valor tras la ejecución de la función, y reservar un espacio en la memoria de datos donde albergar las variables que componen el marco de la función llamada. El código del prólogo correspondiente a la función de ejemplo es el siguiente: global _funcion _funcion_code CODE _funcion: ; Salvar en pila el frame pointer ; de la función superior. movff Stack_pointer+1, FSR0H movff Stack_pointer, FSR0L movff Frame_pointer, INDF0 incf FSR0L, F btfsc STATUS, C incf FSR0H, F movff Frame_pointer+1, INDF0 ; Reservar un nuevo marco para ; la función actual. movlw .4 movwf Frame_pointer+1 call _allocate_frame_8_banks ; Actualizar el puntero a la cima de la pila. movlw D’2’ addwf Stack_pointer, F btfsc STATUS, C incf Stack_pointer+1, F Las líneas de 1 a 3 indican el nombre de la función y la hacen accesible desde cualquier punto del programa. Una vez hecho esto, el siguiente paso será guardar el contenido de todos los registros que se utilizan en la función y que, por tanto, deberán permanecer con su valor tras la misma. Puesto que GCC ha determinado que no tendrá que utilizar ninguno de los registros físicos marcados con 0 en la macro CALL_USED_REGITERS, no es necesario incluir el código para guardar ni, posteriormente, restaurar los valores de estos registros.
Compilador de C para el microcontrolador Microchip PIC18F4550
133
En el segundo bloque de este código se efectúa la reserva de memoria de datos necesaria para la ejecución de esta función. Para ello, efectúa una llamada a la función _allocate_frame_8_banks, indicando en Frame_pointer+1 el número de bytes a reservar. Al volver de esta función, el frame pointer contendrá la dirección del marco de función reservado. El tercer bloque actualiza el apuntador de cima de pila según los registros que se hayan salvado en el primer bloque. En este ejemplo, el stack pointer sólo crece en dos unidades, ya que tan sólo se salvó el frame pointer, 2 bytes. Después de ejecutar el cuerpo de una función, es necesario restaurar los valores de los registros que se salvaron en la pila; así como restaurar el frame pointer para que apunte de nuevo al marco de la función a la que retornamos. El código del epílogo de la función que hemos puesto como ejemplo se muestra a continuación: ; Liberar memoria del marco de función. movlw .4 call _free_frame_8_banks ; Restaurar registros salvados en pila. dcfsnz Stack_pointer, F decf Stack_pointer+1, F movff Stack_pointer+1, FSR0H movff Stack_pointer, FSR0L movff INDF0, Frame_pointer+1 dcfsnz FSR0L, F decf FSR0H, F movff INDF0, Frame_pointer movlw low D’2’ subwf Stack_pointer, F bnc _L5 movlw high D’2’ subwfb Stack_pointer+1, F _L5 retlw 0 En el código anterior, el primer bloque se encarga de liberar los 4 bytes reservados para el marco de la función llamada. Posteriormente, en el segundo bloque se restauran los valores de los registros que se salvaron en la pila, en orden inverso a como se apilaron en el prólogo de la función.
Compilador de C para el microcontrolador Microchip PIC18F4550
134
No analizaremos ningún código para pasar parámetros o devolverlos, pero como vimos en la especificación, el código ensamblador que genere nuestro back-end hace el paso de parámetros copiando su valor en registros si se trata de datos de tipo simple; o copiando un puntero a memoria para el caso de tipos de dato complejos. Lo mismo ocurre con el retorno de valores, por lo que a nivel de código no existe diferencia entre una función con parámetros y/o valores de retorno y otra que no los tenga.
7.8.2.
main
Veamos una función main: void main (void) { int x = 10, y = 2; while (y > 0) { x = x * y; y--; } } La primera idea que nos puede venir a la cabeza es que sucede lo mismo que con las funciones genéricas; pero en este caso existen varias diferencias esenciales que descubrimos al analizar el código resultante de compilar esta función main de ejemplo.
_main main:
global CODE
main
; Inicializar puntero a la cima ; de la pila BANKSEL F_REG movlw low stackdata movwf Stack_pointer movlw high stackdata movwf Stack_pointer+1
Compilador de C para el microcontrolador Microchip PIC18F4550
135
; Reservar un nuevo marco para ; la función actual. movlw .4 movwf Frame_pointer+1 call _allocate_frame_8_banks Observamos que no hay ningún bloque de código que se encargue de guardar en pila ningún registro. Esto es correcto debido a que main es la primera función que ejecuta nuestro programa, por lo que no hay que salvar el marco de la función que llame a ésta. En su lugar encontramos, justo antes de la llamada a la función de reserva de marco, un bloque de código que inicializa el stack pointer para que apunte a la primera posición de memoria correspondiente al segmento designado para la pila de datos. La función main no devuelve ningún valor debido a que el programa no se ejecuta sobre un sistema operativo que reciba el resultado, así que el epílogo de esta función es un simple bucle infinito. _epimain: goto _epimain
7.9.
Archivos
El último punto de la estructura de un programa en C son los archivos. Un programa mediano C suele estar compuesto por diversos archivos de código fuente, los cuales se compilan por separado; y, finalmente, se enlazan los códigos objeto resultantes de compilar cada uno de ellos. Distinguiremos entre dos tipos de archivo. El archivo que denominamos principal es aquel que alberga la definición de la función main. Por otro lado, cualquier archivo de código que forme parte del programa y no se trate del principal, lo denominamos genérico.
7.9.1.
Genéricos
Dejando a un lado el código correspondiente al cuerpo de las funciones, nos queda por analizar de los archivos genéricos su cabecera y su terminación. En la cabecera, la línea fundamental es la que incluye el archivo de definiciones para el dispositivo que estamos programando, en nuestro caso, p18f4550.inc. ;START
Compilador de C para el microcontrolador Microchip PIC18F4550
136
#include "p18f4550.inc" La terminación de un archivo genérico está compuesto por las declaraciones de las variables y funciones externas utilizadas en el archivo actual. En el caso de este ejemplo, tan sólo declara las funciones de reserva y liberación de marco, y las variables internas que utiliza GCC para gestionar la memoria del PIC. extern extern extern extern extern extern extern extern
7.9.2.
_allocate_frame_8_banks _free_frame_8_banks stackdata Mem_entr F_REG Frame_pointer Stack_pointer Arg_pointer
Principal
El archivo principal es bastante más complejo que los archivos genéricos. Su cabecera es similar a la genérica, pero el final del archivo incluye el código de definición e inicialización de los distintos segmentos de memoria, incluidos el segmento asignado a registros y el asignado a pila de datos. Lo primero que encontramos en el final del archivo principal es la declaración de las funciones de biblioteca llamadas desde las funciones de este archivo. extern _mulhisi extern _allocate_frame_8_banks A continuación encontramos una etiqueta de código absoluto, ubicada en la posición 0x000000 de la memoria de programa, que corresponde con el vector de reset del microcontrolador. Como ya explicamos unos capítulos atrás, la primera instrucción que ejecuta el PIC al inicializarse es la que está alojada en la posición de memoria 0x000000. Por ello, esta etiqueta del código es fundamental para el funcionamiento del programa generado. Nuestro reset simplemente salta a la rutina _init, que efectúa la inicialización de variables, si las hubiera, y, acto seguido, salta a la primera instrucción de la función main. _reset
CODE
0x0
Compilador de C para el microcontrolador Microchip PIC18F4550
137
goto _init _initcode CODE _init clrf INTCON call _initialize goto _main Después de estas instrucciones en ensamblador encontramos más directivas que indican a gpasm y gplink cómo deben manejar la memoria. Por un lado, declaramos una región del banco de acceso del PIC que corresponde a las posiciones de memoria designadas para que GCC las consideres registros, tanto los 32 registros de propósito general como los tres registros de propósito específico y tamaño distinto al de los genéricos. Por el otro, declaramos otra región de memoria, no importa de qué banco, que será la pila de datos de nuestro programa compilado. v_reg UDATA_ACS F_REG res .32 Frame_pointer res 2 Stack_pointer res 2 Arg_pointer res 2 global F_REG global Frame_pointer global Stack_pointer global Arg_pointer
seg_stackDATA UDATA_ACS stackdata res .16 global stackdata El código del archivo principal termina con la reserva de toda la memoria de los bancos que existen en el PIC; reserva e inicializa los dos primeros bytes de cada banco, con el propósito de almacenar en ellos el tamaño total de cada banco y el espacio utilizado hasta el momento por nuestro programa. Además, reserva toda la memoria restante de cada banco para evitar que gpasm o gplink interfieran con la gestión de memoria. Nuestro programa, tras la compilación, incorpora toda la lógica necesaria para gestionar la memoria de datos y, de este modo, tenemos la certeza de que toda la memoria está disponible y bajo nuestro control. bank1DATA
IDATA
0x60
Compilador de C para el microcontrolador Microchip PIC18F4550 b1_used db b1_total
D’0’ db
D’158’
bank1UDATA UDATA 0x62 b1_data res .158 global b1_used global b1_total global b1_data bank2DATA b2_used db b2_total
IDATA D’0’ db
0x100 D’254’
bank2UDATA UDATA 0x102 b2_data res .254 global b2_used global b2_total global b2_data bank3DATA b3_used db b3_total
IDATA D’0’ db
0x200 D’254’
bank3UDATA UDATA 0x202 b3_data res .254 global b3_used global b3_total global b3_data bank4DATA b4_used db b4_total
IDATA D’0’ db
0x300 D’254’
bank4UDATA UDATA 0x302 b4_data res .254 global b4_used global b4_total global b4_data
138
Compilador de C para el microcontrolador Microchip PIC18F4550
bank5DATA b5_used db b5_total
IDATA D’0’ db
0x400 D’254’
bank5UDATA UDATA 0x402 b5_data res .254 global b5_used global b5_total global b5_data bank6DATA b6_used db b6_total
IDATA D’0’ db
0x500 D’254’
bank6UDATA UDATA 0x502 b6_data res .254 global b6_used global b6_total global b6_data bank7DATA b7_used db b7_total
IDATA D’0’ db
0x600 D’254’
bank7UDATA UDATA 0x602 b7_data res .254 global b7_used global b7_total global b7_data bank8DATA b8_used db b8_total
IDATA D’0’ db
bank8UDATA UDATA b8_data res .254 global b8_used
0x700 D’254’ 0x702
139
Compilador de C para el microcontrolador Microchip PIC18F4550
140
global b8_total global b8_data Como conclusión a este capítulo debemos recordar que, pese a que hemos visto de forma genérica todos los códigos ensamblador generados a partir de las instrucciones atómicas de C, GCC, desde el primer nivel de optimización, alterará la estructura del programa para reducir su coste de ejecución. Como veremos en el capítulo 8, la auténtica potencia de GCC se descubre al hacer uso de las diversas opciones de optimización.
Capítulo 8 Optimizando código con GCC 8.1.
Tipos de optimización
GCC ofrece múltiples opciones de optimización que modifican la manera de generar código del compilador. Además, ofrece cuatro niveles de optimización, los cuales no son más que agrupaciones de las diversas opciones de optimización individuales. Nivel 0 El primer nivel es no optimizar. En esta modalidad, el principal objetivo del compilador es reducir el coste de compilación y conseguir código sin modificar, tal cual esperábamos, para poder usarlo para depuración. Las sentencias son compiladas independientemente, con lo que podemos parar la ejecución en un depurador entre dos sentencias cualesquiera y acceder a las variables directamente. Es obvio que el código que obtenemos sin ninguna optimización es pesado e ineficiente. Las sentencias que nunca sean ejecutadas también se traducirán en código ensamblador y toda variable será leída de memoria, usada según la operación y devuelta a memoria. Aunque volvamos a usar una variable, ésta no será leída desde un registro, sino que se repite el proceso completo de acceso a memoria. Por tanto el nivel de optimización cero (nivel por defecto) no es aconsejable si no se va a depurar el código. Es posible indicar este nivel de manera explícita con la opción -O0. Con los otros tres niveles activamos, de manera incremental, distintas opciones de compilación que indican a GCC las estrategias que debe aplicar para generar código. Todas las optimizaciones se consiguen a costa de incrementar el tiempo de compilación. Además, un código optimizado se vuelve muy difícil de depurar.
141
Compilador de C para el microcontrolador Microchip PIC18F4550
142
Nivel 1 El nivel siguiente de optimización es el nivel 1, que lo especificamos a GCC con el parámetro -O1 o símplemente -O. En este nivel, el optimizador intenta llegar a un equilibrio entre la reducción del tamaño del código, el tiempo de ejecución del código ensamblador generado y el tiempo de compilación del programa. Con este nivel se evitan los modificadores que aumentan demasiado el tiempo de compilación y los que apenas suponen una reducción del tiempo de ejecución del programa compilado respecto a su versión sin optimizar. Los modificadores activados en este nivel de optimización son: -fauto-inc-dec: Combina incrementos o decrementos de direcciones con accesos a memoria. -fcompare-elim: Evita las operaciones de comparación cuando sea posible. fcprop-registers: Tras la pasada de localizar registros, se efectúa la pasada de propagación de copia, que intenta reducir dependencias y en el mejor caso evitar la copia de registros. -fdce: Realiza una eliminación de código muerto o no ejecutado sobre el RTL. -fdefer-pop: Hace que los argumentos de una función no sean sacados inmediatamente de la pila una vez acabada la misma, sino que queden en la pila para funciones posteriores y saque todos los argumentos de una sola vez. -fdelayed-branch: Intenta reorganizar las instrucciones para poder aprovechar los huecos introducidos al retrasar saltos. -fguess-branch-probability: Realiza una conjetura ante un salto para elegir la rama que tiene más probabilidad de ser ejecutada y darle más prioridad. Para elegir la rama, usa diversos métodos heurísticos basados en la información recogida de la función. -fif-conversion: Intenta transformar los saltos condicionales en saltos equivalentes pero sin ramas. -fif-conversion2: Usa ejecución condicional si está disponible a nivel de hardware para reducir o eliminar los saltos. -fipa-pure-const: Descubre qué funciones devuelven siempre un valor constante y reemplaza la llamada por dicho valor. -fipa-profile: Efectúa propagación de perfiles entre procedimientos.
Compilador de C para el microcontrolador Microchip PIC18F4550
143
-fipa-reference: Descubre qué variables globales son internas al archivo que se compila. -fmerge-constants: Intenta mezclar constantes idénticas a través de las unidades de compilación. -fomit-frame-pointer: No guarda el puntero a frame en registros para funciones que no lo necesiten. -fsplit-wide-types: Divide los tipos grandes entre varios registros independientes. -ftree-builtin-call-dce: Elimina código muerto de llamadas a funciones internas del compilador. -ftree-ccp: Realiza una propagación de las constantes condicionales en el árbol RTX. -ftree-ch: Copia la cabeza de los bucles a través del árbol de compilación. Incrementa la efectividad de las optimizaciones sobre el flujo de control pero incrementa el tamaño del código. -ftree-copyrename: Intenta renombrar las variables temporales del compilador a otras variables copiando las localizaciones. Resulta, de forma general, un código con los nombres de las variables más cercanos a los originales. -ftree-dce: Realiza una eliminación de código muerto o no ejecutado sobre el árbol de RTL. -ftree-dominator-opts: Efectúa una variedad de limpiezas simples sobre enteros, como la propagación de constantes, eliminación de redundancia o simplificación de expresiones, basadas en la rama dominante del árbol. -ftree-dse: Realiza eliminación de almacenamiento no útil. -ftree-forwprop: Efectúa propagación hacia delante en árboles. -ftree-fre: Realiza eliminación de redundancia de forma completa. Se diferencia de -ftree-pre en que sólo considera expresiones que están en todas las ramas. -ftree-sra: Reemplaza escalares por tipos agregados, reemplazando las referencias a estructuras con escalares para prevenir pasos de estructuras a memoria demasiado pronto. -ftree-ter: Reemplaza expresiones temporales durante la fase de paso de SSA a normal.
Compilador de C para el microcontrolador Microchip PIC18F4550
144
Nivel 2 El segundo nivel de optimización lo activamos con el parámetro -O2. En este nivel aumentamos la complejidad de compilación y el rendimiento del código obtenido. En este nivel, GCC utiliza todos los modificadores de optimizacióon salvo los que desenrollan bucles y empotran funciones en línea. Este nivel incluye todos los modificadores del nivel 1 y, además, los siguientes: -fthread-jumps: Optimiza los saltos en los que una de las ramas lleva a otra comprobación que es un subconjunto de la anterior. En este caso salta directamente al punto de destino del segundo salto o el punto justo después de ese bloque. -falign-functions: Alinea el comienzo de las funciones con direcciones de memoria que sean potencia de dos. -falign-jumps: Alinea los destinos de los saltos con direcciones de memoria que sean potencia de dos. -falign-loops: Alinea el comienzo de los bucles con direcciones de memoria que sean potencia de dos. -falign-labels: Alinea las etiquetas de los bloques de código con direcciones de memoria que sean potencia de dos. -fcaller-saves: Activa la localización de valores en registros que sean utilizados en una llamada a función, evitando con ello su almacenamiento y recuperación en el cuerpo de la función. -fcrossjumping: Realiza una transformación de saltos entrecruzados, unificando el código equivalente y ahorrando espacio. -fcse-follow-jumps: Optimiza los saltos que sea posible eliminando subexpresiones comunes. -fcse-skip-blocks: Hace que la fase de CSE optimice los saltos con bloques condicionales. -fdelete-null-pointer-checks: Hace un análisis global del flujo de datos para identificar y eliminar los chequeos de apuntadores nulos. -fdevirtualize: Intenta convertir las llamadas a funciones virtuales en llamadas directas a función.
Compilador de C para el microcontrolador Microchip PIC18F4550
145
-fexpensive-optimizations: Efectúa un gran número de pequeñas optimizaciones que son relativamente costosas en tiempo de compilación. -fgcse: Ejecuta una etapa de eliminación de subexpresiones globales. -fgcse-lm: En la fase de eliminación de subexpresiones globales, mueve las lecturas de memoria fuera de los bucles, reemplazándolas en su interior por copias. -finline-small-functions: Sustituye llamadas a función por el cuerpo de las funciones llamadas cuando éste es más pequeño que el código de llamada a dicha función. -findirect-inlining: Empotra también las llamadas indirectas que sean descubiertas en tiempo de compilación. -foptimize-sibling-calls: Optimiza las llamadas recursivas. -partial-inlining: Empotra parte de las funciones llamadas. -fpeephole2: Activa las optimizaciones peephole de la especificación de la arquitectura. -fregmove: Intenta reasignar los números de registros utilizados por las instrucciones de movimiento, para optimizar la cantidad de registros utilizados. -freorder-blocks: Reordena los bloques de código para reducir el número de saltos tomados. -freorder-functions: Reordena las funciones en el código para mejorar la localización del código en memoria de programa. -frerun-cse-after-loop: Repite la eliminación de subexpresiones comunes después de la optimización de bucles. -fsched-interblock: Planifica la secuencia de instrucciones entre bloques. -fsched-spec: Permite especular con la ejecución de instrucciones que no sean de carga de registros. -fschedule-insns: Intenta reordenar las instrucciones para eliminar los retrasos en la ejecución de algunas instrucciones debidos a que los datos que necesitan no están disponibles en ese momento.
Compilador de C para el microcontrolador Microchip PIC18F4550
146
-fschedule-insns2: Similar a -fschedule-insn, pero con una pasada adicional del planificador de instrucciones tras la asignación de registros. -fstrict-aliasing: Permite al compilador aplicar las reglas más restrictivas para localización en memoria. -fstrict-overflow: Permite al compilador aplicar las reglas más estrictas para desbordamiento de valores con signo. -ftree-switch-conversion: Convierte las inicializaciones simples en una estructura switch en inicializaciones a partir de un vector de valores. -ftree-pre: Realiza una eliminación de redundancias parcial sobre el árbol RTL. -ftree-vrp: Efectúa la propagación de rangos de valores sobre el árbol RTL. Nivel 3 El nivel máximo de optimización es el 3, y se activa con el parámetro -O3 en la línea de comandos. Activa todas las optimizaciones del nivel 2 y, además, -finlinefunctions, -funswitch-loops, -fpredictive-commoning, -fgcse-after-reload, -ftree-vectorize y -fipa-cp-clone. Estas están orientadas a linealizar los bucles y producir funciones en línea, con lo que acabamos con la sobrecarga de las llamadas a función. -finline-functions: Integra todas las funciones simples en las llamadas. -funswitch-loops: Mueve las ramas de un bucle con condiciones invariantes fuera del bucle. -fpredictive-commoning: Efectúa una optimización basada en la predicción de operaciones repetidas en cada iteración de un bucle. -fgcse-after-reload: Realiza una pasada de eliminación de cargas de memoria redundantes tras la pasada de relocalización de registros. -ftree-vectorize: Efectúa optimización de bucles en el árbol RTL. -fipa-cp-clone: Hace clones de algunas funciones para mejorar la propagación de constantes entre procedimientos. Nivel s Además de los niveles numerados, GCC ofrece un conjunto de optimizaciones orientadas a reducir el tamaño del código generado, en lugar del tiempo de ejecución del programa. Estas opmizaciones se activan con el parámetro -Os.
Capítulo 9 Conclusiones A lo largo de este proyecto hemos desarrollado un compilador de C para el PIC 18F4550, obteniendo como resultado un compilador de C plenamente funcional. Este compilador desarrollado tiene pleno soporte a todas las características del lenguaje, incluyendo matrices, punteros, y llamadas a funciones sin restricciones artificiales. Hemos optado por trabajar sobre la GCC toolchain, y nos hemos integrado de forma adecuada en ella según la especificación de arquitectura; es decir, solamente hemos modificado la parte que teníamos que soportar, el backend, ya que es la parte donde en dicha arquitectura se definen los portings de GCC a arquitecturas concretas. Esto ha supuesto un sobreesfuerzo; ya que nos ha obligado a soportar la GCC toolchain por completo, sin trampas ni atajos. Teniendo en cuenta que es común en los compiladores para microcontroladores el no soportar plenamente determinadas características del lenguaje, para facilitar el trabajo de desarrollo del compilador, el resultado de este proyecto queda a la altura de los mejores compiladores de C para microcontroladores. Gracias, sin embargo, a nuestro planteamiento, soportamos C por completo, soportamos las posibilidades de optimización de GCC, y abrimos la puerta a soportar los demás lenguajes que admite la GCC toolchain, haciendo apenas una serie de desarrollos acotados y definidos. Además, hemos sido muy cuidadosos en este proyecto de trabajar con la última versión de GCC toolchain, así como de seguir las guias de estilo y de modificar solo lo que en la GCC toolchain se espera que modifiques con objeto de una potencial inclusión futura en la línea principal de GCC; un asunto que actualmente estamos tratando con el equipo de desarrollo de GCC. Este proyecto abre nuevas líneas futuras de interés; entra las cuales hay dos especialmente destacadas: el desarrollo de bibliotecas específicas para dispositivos del PIC 18F4550, y ampliar el trabajo para soportar el resto de lenguajes de la GCC 147
Compilador de C para el microcontrolador Microchip PIC18F4550
148
toolchain. Las características específicas del software libre, el sobreesfuerzo para que el código cumpla las características necesarias para ser integrado en la línea principal de GCC y el hecho de que vamos a liberar el código fuente garantizan que el desarrollo de este proyecto será mantenido, y resultará de utilidad a otras personas.
Bibliografía [1] A. V. Aho y col. Compiladores: principios, técnicas y herramientas. Addison-Wesley, 1998. [2] F. J. Sanchís Llorca, C. Galán Pascual. Compiladores: teoría y construcción. Paraninfo, 1988. [3] Jan Hubicka. Porting GCC to the AMD64 architecture. w.cz/~hubicka/papers/amd64.pdf.
URL :
http://www.uc
[4] Microchip. Página principal de MicroChip. 2011. URL: http://www.microch ip.com. [5] Hans-Peter Nilsson. Porting GCC for dunces. URL: ftp://ftp.axis.se/pu b/users/hp/pgccfd/pgccfd.pdf. [6] David Santo Orcero. «Arquitectura interna de GCC toolchain». En: Mundo Linux 78 (2005), págs. 58-63. [7] David Santo Orcero. «Generación de código ensamblador con la GCC toolchain». En: Mundo Linux 79 (2005), págs. 56-61. [8] Pedro José Ramirez Gutierrez, dir. David Santo Orcero. Compilador de C para el microcontrolador Microchip PIC16F877. 2007. [9] GCC Team. GCC Internals. 2011. s/gccint.
URL :
http://gcc.gnu.org/onlinedoc
[10] GCC Team. Página principal de GCC. 2011. URL: http://gcc.gnu.org.
149
Apéndice A PIC18-GCC A.1.
Compilando GCC
Para poder compilar GCC con la arquitectura PIC18 como target es necesario disponer del código fuente de GCC incluído en el CD que acompaña esta memoria. La versión utilizada es la futura 4.7, aún no publicada, por lo que el código adjunto se trata de un snapshot (copia instantánea) del repositorio oficial de desarrollo, tomada el día 29 de septiembre de 2011. Debido a que se trata de una código en desarrollo activo, no se puede garantizar que el código específico del PIC18 vaya a funcionar con una instantánea posterior o anterior a la incluída en el CD. A parte del código fuente de GCC y, obviamente, el parche específico del port para PIC18, es necesario tener instaladas en el sistema las GPUTILS, ya que son las que utilizará GCC para ensamblar y enlazar el código ensamblador producido. La mayoría de distribuciones de GNU/Linux ofrecen la posibilidad de instalar las GPUTILS como paquete precompilado desde sus repositorios de software, por lo que no explicaremos los pasos correspondientes a la compilación e instalación de GPUTILS. La manera recomendada de aplicar el parche de este port es haciendo uso del programa patch. Tan sólo hay que cambiar al directorio principal del código fuente de GCC con el comando cd y, desde ahí, ejecutar patch de la siguiente forma: patch -p0 < ruta/hasta/gcc-pic18.patch Una vez aplicado el parche sobre el código oficial de GCC, lo siguiente es crear un directorio aparte donde efectuar la compilación del compilador sin que se mezclen los archivos compilados con los de código fuente. En este ejemplo, el directorio lo creamos dentro del directorio que alberga el código fuente, pero no supone ninguna diferencia el utilizar cualquier otro.
150
Compilador de C para el microcontrolador Microchip PIC18F4550
151
mkdir gcc-pic18 Desde este directorio invocaremos el script de configuración y, una vez haya terminado éste, ejecutaremos el comando make, responsable de compilar todo el proyecto de manera automatizada. cd gcc-pic18 ../configure --target=pic18 make Tras la ejecución (con éxito) de make, los ejecutables de nuestro compilador se hallarán en el subdirectorio gcc-pic18/gcc. No obstante, para evitar problemas de ruta es más que recomendable instalarlo en sus directorios de prefijo utilizando, de nuevo, el programa make. make install Una vez completados estos pasos, tendremos en nuestro sistema una instalación completamente integrada del compilador GCC para PIC18, que podremos invocar desde cualquier directorio.
A.2.
Utilidades
De los diversos programas que componen la GNU Compiler Collection, dos de ellos son especialmente interesantes para el usuario: pic18-cpp, pic18-gcc. pic18-cpp es el preprocesador de C. Su misión es procesar las macros incluidas en el código fuente antes de pasárselo al compilador. Aunque este programa es de uso transparente para el usuario de GCC, es posible ejecutarlo a mano y no necesariamente con un código escrito en C como entrada. La mayoría de las veces el usuario lo utilizará de manera transparente a través del driver de GCC. pic18-gcc es el driver (controlador) de los compiladores de GCC para PIC18. Este programa es el que utilizará directamente un usuario habitual de GCC. De manera genérica, toma como entrada uno o varios archivos de código fuente. En el caso que nos ocupa, el port para PIC18, sólo acepta código C y ensamblador del PIC18. Tras identificar en qué lenguaje está escrito cada archivo de entrada, invoca, si procede, al preprocesador; y, acto seguido, al compilador en sí, cc1. Nuestra versión cc1 da como resultado un archivo en ensamblador
Compilador de C para el microcontrolador Microchip PIC18F4550
152
del PIC18. Tras la traducción a ensamblador, el driver invoca al programa ensamblador gpasm con el archivo generado por cc1 como entrada. La salida de gpasm es un archivo de código objeto específico para el PIC18F4550. El último paso que efectúa el driver es tomar todos los archivos objeto generados y enlazarlos mediante el programa gplink. Tras todos estos pasos obtenemos un archivo de extensión .hex, listo para grabarlo en la memoria flash del microcontrolador.
A.3.
Argumentos
Los argumentos de pic18-gcc nos permiten modificar la operación normal del compilador. pic18-gcc requiere, como mínimo, un parámetro que sea un archivo C de entrada. En el caso de no pasar ningún modificador a pic18-gcc, éste compilará el archivo C con el mínimo de optimizaciones, y el resultado será ensamblado y enlazado con la biblioteca de funciones matemáticas, quedándonos el programa ejecutable final en a.hex. Los diversos parámetros que ofrece pic18-gcc permiten modificar este funcionamiento, incluyendo diversas optimizaciones, generando un archivo de salida con un nombre concreto o efectuando sólo una de las etapas que componen la compilación. A continuación encontramos una breve lista de los argumentos más comunes de gcc. La lista completa se puede encontrar en la página de man sobre gcc o en la documentación online que se encuentra en http://gcc.gnu.org. -E: sólo ejecuta el preprocesador. Equivale a invocar directamente a cpp. -S: sólo ejecuta el preprocesador y compilador de C. Equivale a invocar a cpp y, a continuación, cc1. -c: ejecuta todas las etapas salvo la final de enlazado. En este caso usa cpp, cc1 y gpasm, para obtener el código objeto. -v: muestra los programas invocados por el driver. De este modo podemos seguir la pista a los programas invocados y sus parámetros. -dP: incluye en el código ensamblador indicaciones de las insn que generan el código. -o nombre_de_archivo: especifica el nombre del archivo de salida. -On: nivel de optimización. GCC ofrece varios niveles generales de optimización: 0,1,2,3 y s. En el capítulo 8 veremos más detalles sobre las optimizaciones.
Compilador de C para el microcontrolador Microchip PIC18F4550
153
-M: muestra en la salida estándar los comandos para poder compilar el archivo de entrada desde un Makefile. La forma habitual en la que invocaremos al compilador GCC es con el parámetro ‘-c’, que nos entregará, ante un archivo C de entrada, un archivo ensamblador con el mismo nombre y cuya extensión será .s.
A.4.
Parámetros de GCC específicos para el PIC18
El back-end para PIC18 admite dos parámetros para modificar su funcionamiento. Estos argumentos permiten ajustar el funcionamiento del compilador según las necesidades del desarrollador y los requisitos del proyecto que esté desarrollando. -mextra-alloc=n_bytes: Expecifica cuantos bytes de memoria deberá reservar GCC de manera adicional. Este parámetro está pensado para programas de varios archivos de código fuente en los cuales la declaración de variables globales no sólo se hace en el archivo que alberga la función main. -mstack-size=: Especifica el tamaño en bytes de la pila software. Por defecto es de 16 bytes.
A.5.
Compilando con pic18-gcc
Supongamos que tenemos un archivo codpic.c con el código C del programa deseado. Para compilarlo basta con pasárselo como argumento a pic18-gcc, pero una manera más controlada de hacerlo es la siguiente: pic-gcc -O3 -mp=18f4550 -o codpic.hex codpic.c Con esto obtendremos el archivo codpic.hex listo para programarlo directamente en el microcontrolador. Además obtendremos el listado en ensamblador en codpic.lst con la información de enlazado incluída, y en codpic.cod podremos encontrar la información extendida lista para utilizar con un simulador o las herramientas auxiliares de GPUTILS. Con programas de mayor envergadura lo más probable es que necesitemos emplear varios archivos de código fuente. Para compilar este tipo de proyecto es recomendable generar los códigos objeto primero y después enlazarlos todos en un único archivo hex. Por ejemplo, supongamos un programa codpic complejo, cuyo código fuente se encuentra repartido en tres archivos diferentes, codpic1.c, codpic2.c y
Compilador de C para el microcontrolador Microchip PIC18F4550
154
codpicmain.c. La secuencia de comandos para compilar este programa sería similar a la siguiente: pic18-gcc pic18-gcc pic18-gcc pic18-gcc
-O3 -mp=18f4550 codpicuno.c -c -O3 -mp=18f4550 codpicdos.c -c -O3 -mp=18f4550 codpicmain.c -c -o codpic.hex codpicmain.o codpicuno.o codpicdos.o
Las tres primeras llamadas a pic18-gcc se encargan de compilar y ensamblar los archivos fuente individuales. Con la cuarta llamada al driver, éste identifica los archivos de entrada como código objeto y se limita a llamar al enlazador, el cual genera el archivo final codpic.hex. Todo esto es posible automatizarlo mediante un script de make como el mostrado a continuación: MC=18f4550 CC=pic18-gcc CFLAG=-mp=$(MC) codpic.hex: codpicmain.o codpicuno.o codpicdos.o $(CC) -o codpic.hex codpicmain.o codpicuno.o codpicdos.o codpicmain.o: codpicmain.c $(CC) $(CFLAG) -O3 -c codpicmain.c codpicuno.o: codpicuno.c $(CC) $(CFLAG) -O3 -c codpicuno.c codpicdos.o: codpicdos.c $(CC) $(CFLAG) -O3 -c codpicdos.c Con este makefile, bastará invocar al comando make para efectuar la compilación del hipotético programa complejo puesto como ejemplo. Además, make detecta qué archivos fuente han cambiado desde la última vez que se compiló el programa final y sólo compilará los archivos fuente más recientes, de manera cómoda, eficiente y sin la posibilidad de saltarse ningún paso.
Apéndice B GNU PIC Utilities B.1.
Introducción
Las GNU PIC Utilities, también conocidas como GPUTILS, son una colección de herramientas que nos permiten trabajar con los microcontroladores PIC de Microchip. Incluyen tres herramientas clave en todo proceso de trabajo con un microcontrolador: un ensamblador (gpasm), un enlazador (gplink) y una herramienta para creación y mantenimiento de bibliotecas (gplib). Todas estas herramientas fueron creadas con un claro objetivo: proporcionar un sustituto libre (licencia GPL) de las correspondientes herramientas propietarias de Microchip. Cuando se programa para un microprocesador, una de las primeras decisiones que hay que tomar es qué tipo de código utilizará el proyecto. Exsiten dos opciones: Modo absoluto. En este modo es el desarrollador quien decide qué posiciones de memoria ocupará cada bloque de código y cada variable de memoria. En este modo sólo se utiliza el lenguaje ensamblador y gpasm, ya que el código fuente incluye toda la información de almacenamiento necesaria. Este modo es idóneo para programar el microprocesador directamente en lenguaje ensamblador. Cuando se trabaja en este modo, es habitual organizar el código de manera que cumpla el principio de localidad espacial, es decir, que las variables comunes estén alojadas en posiciones próximas y, en el caso del PIC18, dentro del mismo banco de memoria. De este modo se evita el uso repetido del código para cambiar de banco, haciendo el programa más fácil de leer y ahorrando espacio en la memoria de programa. El principal inconveniente de este modo es que la utilización de bibliotecas externas resulta más tediosa, debido a la obligatoriedad de ubicar las funciones en memoria manualmente. 155
Compilador de C para el microcontrolador Microchip PIC18F4550
156
Modo relocalizable: Las posiciones de memoria utilizadas en este modo son relativas. Es posible dividir el código ensamblador en módulos separados, los cuales pueden ensamblarse en archivos objeto diferentes. gpasm es el encargado de crear símbolos para cada referencia a memoria (tanto para datos como para código). Estos módulos objeto pueden cargarse en cualquier posición de memoria, por lo que ahorra al desarrollador esta responsabilidad. Por otro lado, la responsabilidad del enlazador, gplink, es mucho mayor en este modo, puesto que es él quien se encarga de decidir la posición final de los objetos, resolviendo las referencias a los símbolos creados por en ensamblador. El archivo producido por gplink será un único archivo ejecutable en modo absoluto. Como primera ventaja de trabajar con el modo relocalizable, vemos que la escritura de ensamblador se vuelve más fácil, ya que no tenemos que preocuparnos por las direcciones del código. Podemos crear módulos objeto compuestos por funciones relacionadas y agruparlos en bibliotecas, permitiendo de este modo reutilizar un mismo código innumerables veces. Además, la compilación de un proyecto es más rápida, ya que los objetos de la biblioteca sólo se compilan una vez. Si modificamos un módulo concreto, solo será necesario recompilar ese módulo y enlazar todo. Por otro lado, en cada módulo podemos tener un espacio de nombres local, decidiendo que símbolos son globales y cuales no, cuales son accesibles por módulos ajenos y cuales son privados al módulo actual. La gran desventaja del modo relocalizable es la dificultad de controlar el banco de memoria de datos empleado en cada momento. La naturaleza reubicable de este modo, que otorga tantas ventajas de utilización, es un arma de doble filo que puede obligar al desarrollador a incluir comprobaciones del banco utilizado cuando quiera trabajar con un banco de memoria concreto. Por supuesto, dentro de un código relocalizable podemos incluir código en modo absoluto, fijando una o varias rutinas, o variables, en posiciones de memoria concretas y dejando el resto a elección del enlazador.
B.2.
Herramientas
B.2.1.
gpasm
gpasm es, como hemos presentado, el ensamblador de las GPUTILS que nos permite pasar de un archivo en código ensamblador a un objeto. Si el código no tiene símbolos sin instanciar, el objeto se puede usar directamente para programar el microcontrolador. La sintaxis es:
Compilador de C para el microcontrolador Microchip PIC18F4550
157
gpasm [opciones] programa.s Las opciones más importantes y comunes son: -c: genera código relocalizable. -o archivo_salida: establece un nombre al objeto de salida. Por defecto será el mismo que el dado como entrada, pero cambiando la extensión por .o si el código es relocalizable o por .hex si es absoluto. -p modelo_pic: especifica para qué microcontrolador se va a compilar el archivo de entrada. También se puede hacer desde el código ensamblador. En nuestro caso será “-p 18F4550”. Existen más opciones, pero para el trabajo cotidiano basta con conocer estas. De todas formas es recomendable leer el manual para conocer todas sus posibilidades.
B.2.2.
Código fuente admitido por gpasm
La forma que toma una línea de código ensamblador válido para gpasm es la siguiente: [etiqueta]
instrucción [argumentos]
[comentario]
Cada línea del archivo contendrá, como máximo, una instrucción en ensamblador, seguida de los argumentos que ésta requiera. El primer carácter del nemotécnico de la instrucción debe estar en una columna distinta a la primera de esa línea. Opcionalmente, cada instrucción puede tener una etiqueta. Los comentarios comienzan con el símbolo ’;’, a partir del cual nada de lo escrito será tratado como código hasta el final de la línea. Las etiquetas serán una combinación de letras, dígitos y el carácter ’_’, con la única restricción de que el primer carácter no puede ser un número. Las etiquetas, opcionalmente, irán seguidas por el carácter ’:’, con la finalidad de mejorar la lectura del programa. Como ejemplo de instrucciones válidas para gpasm mostramos el siguiente bloque de código:
loop
sleep incf 6,1 goto loop
; ; ; ;
Línea en blanco Etiqueta y operación Operación con dos parámetros Operación con un parámetro
gpasm establece varias formas de especificar números. Las más utilizadas son las binarias, decimales y hexadecimales. Todas se basan en una letra de prefijo que
Compilador de C para el microcontrolador Microchip PIC18F4550
158
establece el tipo y el número en el formato adecuado. El prefijo es B para binario, D decimal y H para hexadecimal. Para los números en hexadecimal existe otra representación alternativa que consiste en el prefijo 0x seguido del número. También se aceptan formatos alternativos heredados de MPASM. Lo vemos brevemente con ejemplos: Binario Decimal
Hexadecimal
B’1111011’ 1111011b D’123’ 123d .123 H’7B 7Bh 0x7b
Por defecto, si no se especifica un prefijo o sufijo para un número, gpasm lo interpretará como hexadecimal, aunque es posible cambiar este comportamiento desde la línea de comandos. gpasm admite expresiones basadas en el conjunto de operadores de C. Como ejemplo, y sin entrar en profundidad, mostramos la siguiente secuencia válida: movlw (0x56 >> 4) & 0x0F Esta instrucción carga en WREG el valor 56 hexadecimal (86 decimal), desplazándolo 4 posiciones a la derecha y operando conjuntamente con 0x0F, quedando sólo los 4 bits menos significativos, en este caso 1010 binario (10 decimal). Tenemos suma, resta, desplazamientos, operaciones lógicas y comparaciones, entre otros operandos, que nos proporcinan una gran flexibilidad a la hora de utilizar expresiones matemáticas constantes, teniendo en cuenta que estas son evaluadas antes de traducir código. Analizando un poco más el preprocesador, presentamos tres directivas más. La directiva include permite incluir archivos externos en el código actual; es útil, por ejemplo, para separar las definiciones propias de cada procesador en distintos archivos e incluir en el código sólo la necesaria para el PIC con el que estamos trabajando. Otra directiva conocida en casi todos los precompiladores de diversos lenguajes es define, y su correspondiente undefine, que permite asociar (o desasociar respectivamente) símbolos significativos con valores menos manejables y utilizarlos directamente en el código. Por ejemplo: #define altura D’25000’
Compilador de C para el microcontrolador Microchip PIC18F4550
159
movlw altura Un uso habitual de la directiva define es dar nombres simbólicos a las direcciones de memoria de los distintos dispositivos que ofrece el microcontrolador. De este modo, la legibilidad y portabilidad del código es mayor que si empleásemos los valores numéricos. Veremos a continuación más directivas de forma rápida, dado que el objetivo de este apéndice es tener sólo una idea clara de las directivas que se usan para la generación de código en el compilador de C. Para obtener más información y conocer el resto de las directivas recomendamos leer el manual de GPUTILS. ORG posición. En modo absoluto indica la posición exacta donde se cargará el código escrito a continuación de esta directiva. Si no se especifica una posición, gpasm usará 0x000000. $: esta etiqueta indica la dirección de la instrucción que la contiene. Es útil para escribir bucles en distancias relativas a la instrucción actual. Por ejemplo: goto $ - 2. BANKISEL etiqueta. Esta macro sirve para crear el código que selecciona el banco de memoria de datos que contiene la dirección de etiqueta para poder efectuar accesos indirectos. BANKSEL etiqueta. Al igual que la anterior, genera el código de selección del banco de etiqueta pero para accesos directos. Actuará de forma diferente según las características del PIC utilizado. etiqueta CODE expresión. En modo relocalizable, indica que el bloque de código desde la directiva hasta el final del archivo o hasta el siguiente bloque constituye una nueva sección de código máquina independiente del resto. Si no se especifica una etiqueta, se usará .code. La etiqueta expresión es opcional e indica la posición exacta donde está localizada. Se usa a menudo para especificar los vectores de inicio e interrupciones. END. Marca el final del código fuente. EXTERN símbolo. En modo relocalizable, declara símbolo como definido en otro archivo objeto y será gplink quien se encargue de resolverlo. Se usa para importar los parámetros y resultados de las bibliotecas. GLOBAL símbolo. En modo relocalizable, declara símbolo como global, lo que permite que sea utilizado en otros archivos objeto donde se necesite, usando en estos la directiva EXTERN.
Compilador de C para el microcontrolador Microchip PIC18F4550
160
etiqueta RES n_bytes. Hace que gpasm reserve n_bytes de memoria en la posición en la que aparezca esta directiva. Útil para reservar espacio y referenciar el mismo haciendo uso de etiqueta. etiqueta UDATA expresión. Usado en modo relocalizable, indica que el bloque de memoria desde la directiva hasta el siguiente bloque o el final del archivo constituyen una nueva sección de memoria de datos sin inicializar. Es útil, junto con RES, para reservar espacio de memoria para las variables del código. La etiqueta expresión se usa opcionalmente para fijar la dirección donde comienza el bloque. Posteriormente veremos un ejemplo de código completo. Para completar información podemos revisar el manual de GPUTILS o consultar el manual de MPASM, dado que todas las directivas del ensamblador propietario de Microchip están disponibles en gpasm.
B.2.3.
gplink
gplink es el enlazador de código reubicable. Se encarga de relocalizar y enlazar los archivos objeto que forman un programa, y crear un único archivo listo para transferir a la memoria de nuestro microcontrolador. Su sintaxis es: gplink [opciones] [objetos] [bibliotecas] Entre sus opciones, las más importantes y útiles para el trabajo cotidiano son: -I directorio. Especifica un directorio para incluir en la búsqueda de archivos, pudiendo existir varios parámetros -I en una misma llamada a gplink. -o programa.hex. Establece un nombre alternativo al programa compilado. Por defecto será a.hex. -s archivo. Especifica el archivo con las definiciones necesarias para el proceso de enlazado, correspondientes al microcontrolador especificado. Este archivo, llamado linker script, indica la memoria disponible, su tipo y localización, y demás parámetros necesarios para que el enlazador haga su trabajo. Si no se especifica ningún archivo, gplink usará el indicado en los códigos objeto pasados como argumentos.
Compilador de C para el microcontrolador Microchip PIC18F4550
B.2.4.
161
gplib
gplib crea, modifica y extrae archivos objeto de bibliotecas. Con esto podemos agrupar una colección de objetos en un único archivo y pasar este único archivo a gplink. gplink tomará solamente los objetos que necesite de la biblioteca, por lo que el tamaño final del programa no excede más allá de lo estrictamente necesario. Su sintaxis es: gplib [opciones] biblioteca [objetos] Entre las opciones, las más importantes y útiles para el trabajo cotidiano son: -c. Crea una nueva biblioteca. -d. Borra objetos de una biblioteca. -r. Añade o reemplaza un objeto de una biblioteca. -s. Muestra los símbolos globales de una biblioteca. -t. Muestra los objetos de una biblioteca. -x. Extrae los objetos de una biblioteca. Supongamos como ejemplo que tenemos los siguientes archivos: mult.o add.o sub.o div.o La forma de crear una biblioteca con los tres primeros archivos objeto utilizando gplib es ésta: gplib -c math.a mult.o add.o sub.o Si posteriormente deseamos añadir div.o, o actualizarlo si ya estuviese en la biblioteca, lo haríamos de esta manera: gplib -r math.a div.o
Compilador de C para el microcontrolador Microchip PIC18F4550
B.3.
Compilando ensamblador
Veamos dos ejemplos de archivos ensamblador válidos. El archivo funcion1.s: extern suma extern op1, op2 global result resultado resul
UDATA 1
res
_funcion_llamante funcion1_ll: BANKSEL op1 movlw 0x01 movwf op1 movlw 0x04 movwf op2 call suma goto $ _reset
CODE
CODE goto funcion1_ll nop nop retfie
END Y el archivo add.s: extern global global
result op1, op2 suma
operadores op1 res op2 res
UDATA 1 1
0x0
162
Compilador de C para el microcontrolador Microchip PIC18F4550 Opsuma suma:
163
CODE BANKSEL op1 movf op1, W addwf op2, W BANKSEL result movwf result return
END Examinando los ficheros vemos como en funcion1.s están declarados suma, op1 y op2 como símbolos externos, indicando con ello que se pueden usar pero que no están declarados en este archivo. Al declarar result como global le damos visibilidad desde otros archivos. Observamos cómo en add.s se utilizan los mismos símbolos pero de manera contraria, dando visibilidad a op1, op2 y suma, y permitiendo el uso de result pese a no estar declarado. Con los segmentos UDATA designamos secciones, sin inicializar, de memoria de datos: un byte en el primer archivo y dos bytes en el segundo. No especificamos dónde se establecerán sino el tamaño que ocuparán y un nombre con el que indentificarlos a ellos. Es habitual emplear una única etiqueta para referirse a la dirección base de una sección y sumarle un offset cuando direccionemos bytes más alla del correspondiente a la base. Con los segmentos CODE ocurre exactamente lo mismo: establecemos una sección de código pero no damos ninguna indicación de dónde situarlo. En este segmento podemos ver como en el primer archivo, antes de poder usar el símbolo op1, debemos usar BANKSEL para seleccionar el banco correcto en un acceso directo. Dado que los operadores, op1 y op2, están definidos dentro de la misma sección, estos están en el mismo banco de memoria de datos, así que no es necesario usar BANKSEL una vez direccionado uno de ellos. Este primer archivo termina con “GOTO $”, el cual resulta en un bucle infinito. De esta manera el microcontrolador queda activo pero sin realizar ninguna operación aparte de esta espera activa sin fin. Existe otro segmento de código, definido en el primer archivo, llamado “_reset”. Con éste sí indicamos una posición donde debe ubicarse, la posición 0x0. Esta dirección corresponde al vector de inicialización del microcontrolador y es donde se encontrará la primera instrucción que ejecutará el PIC cuando se encienda. Examinados nuestros ficheros de ejemplo pasamos a ensamblarlos. Esto lo hacemos con las siguientes llamadas a gpasm:
Compilador de C para el microcontrolador Microchip PIC18F4550
164
gpasm -c -p18F4550 funcion1.s gpasm -c -p18F4550 add.s Obtendremos dos archivos objeto, add.o y funcion1.o. Estos dos archivos no son ejecutables hasta haber efectuado la fase de enlazado, ya que contiene símbolos sin resolver. Además de los ficheros objeto, tendremos dos archivos (uno por cada objeto) con el mismo nombre y de extensión .lst, que contendrán datos importantes para la depuración, como una lista de símbolos generados, el código máquina de las instrucciones o la posición relativa de las instrucciones respecto al segmento. Después de la fase de ensamblado, llamamos al enlazador para que complete el proceso de compilación de nuestro programa en ensamblador: gplink add.o funcion1.o -o programa.hex El resultado del proceso de enlazado son tres archivos. El primero de estos archivos contiene el programa ejecutable que escribiremos en la flash del PIC; su nombre, para este ejemplo, es programa.hex. Los otros dos archivos son programa.cod, que nos sirve para depurar el programa con algún software de simulación, y programa.lst, que contiene el listado de memoria de nuestro programa, con direcciones de memoria absolutas.
Apéndice C Programas de ejemplo El Hola, mundo en el campo de la programación de microcontroladores es un programa como el siguiente: #include void main(void) { TRISD = 0; PORTD = 1; while(1) { PORTD = ~PORTD & 0x1; delayms(500); } } Su propósito es hacer que parpadee un diodo LED conectado al pin 1 del puerto D con un período de un segundo. Un montaje electrónico sencillo para usar este programa consiste, además de las tomas de alimentación y la señal de reloj, en un LED conectado, junto a su resistencia de polarización, al pin 1 del puerto D. Este programa comienza inicializando el puerto D como salida y, acto seguido, entra en un bucle infinito, donde cambia el valor de salida del pin 1 y ejecuta un retardo de 500ms mediante la función de biblioteca delayms, ante de la siguiente iteración. El archivo de cabecera p18f4550.h, incluido al comienzo del código, define una serie de macros y constantes de preprocesador que facilitan la programación de este microcontrolador, dando nombres autodescriptivos para hacer referencia a los registros especiales del microcontrolador. Por ejemplo, la constante TRISD es reem165
Compilador de C para el microcontrolador Microchip PIC18F4550
166
plazada por la cadena “(* (unsigned char *) 0x088)”. De este modo, el programador no necesita aprenderse los detalles de direccionamiento del microcontrolador y puede escribir un código más legible y menos propenso a errores. Además, el código escrito de esta manera es mucho más sencillo de portar a otros modelos de microcontrolador, limitándose a cambiar el archivo de cabecera en la mayoría de casos. En la fase de enlazado de código debemos combinar nuestro programa, ya compilado y ensamblado, con el código objeto de la función delayms. Con esto obtendremos un programa ejecutable completo que hará que nuestro microncontrolador genere una onda cuadrada, la cual que se traduce en un parpadeo periódico del led conectado al pin de salida. Veamos un ejemplo basado en el programa anterior pero más vistoso que el sencillo LED parpadeante: una luz que rebota en los extremos de un segmento (conocido popularmente como efecto del coche fantástico). Para este ejemplo hará falta conectar un LED a cada uno de los 8 pines del puerto D. #include void main(void) { TRISD=0; while (1) { PORTD = 1; while (PORTD) { delayms(50); PORTD = PORTD << 1; } PORTD = 1 << 7; while (PORTD) { delayms(50); PORTD = PORTD >> 1; } } }
Compilador de C para el microcontrolador Microchip PIC18F4550
167
Un ejemplo más complicado que es posible implementar con el mismo montaje hardware que el ejemplo anterior es mostrar una secuencia cíclica con los primeros valores de la sucesión de Fibonacci codificados en binario. void main(void) { unsigned char f1, f2; TRISD = 0; while (1) { f1 = 1; f2 = 1; while (1) { PORTD = f1; f1 += f2; delayms(1000); if (f1 == 233) break; PORTD = f2; f2 += f1; delayms(1000); if (f2 == 233) break; } } } Este código hace uso de dos variables para almacenar los dos números anteriores en la secuencia, que serían los necesarios para construir la sucesión de Fibonacci. Aunque puede resultar un código algo complejo para una implementación de la sucesión de números de Fibonacci, sirve para ilustrar la manera de emplear sin problemas varias variables en conjunción con los registros-fila del microcontrolador. Recordemos que los registros-fila son punteros desreferenciados, por lo que se manejan como variables normales.
Compilador de C para el microcontrolador Microchip PIC18F4550
168
Para terminar, veamos un ejemplo que hace uso de una entrada desde el exterior del microcontrolador. Este ejemplo usará el USART para comunicarse con un PC convencional. void main(void) { unsigned char i; unsigned char tmp; char datoadc[4]; datoadc[3] = ’\0’; /* El led parpadeará tres veces, indicando el comienzo del proceso. */ TRISD = 0; for (i = 1 ; i <= 3 ; i++) { PORTD = 0; delayms(500); PORTD = 1; delayms(500); } /* Informar al PC por el puerto serie. */ Inicializar_UART(9600); Send_String("Hola\n\r"); while (1) { /* Espera a leer del puerto serie un caracter ’p’ o ’P’. */ tmp = Get_Byte(); if (tmp == ’P’ || tmp == ’p’) { /* Envía por el puerto serie diez valores tomados con el conversor A/D con un segundo de tiempo entre muestras. */
Compilador de C para el microcontrolador Microchip PIC18F4550
169
for (i = 0 ; i < 10 ; i++) { Send_Byte(i + ’0’); Send_Byte(’.’); int2char(leerAD(), datoadc); Send_String(datoadc); Send_Byte(’\n’); Send_Byte(’\r’); delayms(1000); } } } } Al inicio del programa emitimos tres destellos en el primer LED del puerto D, dejando éste encendido, indicando que el dispositivo está encendido. Acto seguido, configuramos el USART a 9600 baudios 8N1 (8 bits, sin paridad y con un bit de parada) y enviamos la cadena “Hola” por el USART. Tras esto, esperamos la recepción de la letra P (mayúscula o minúscula), momento en el que enviamos diez muestras del conversor A/D, ya convertidas a cadenas de caracteres.
Apéndice D Manual de usuario Para programar los microcontroladores en lenguaje C con GCC necesitamos, lógicamente, el compilador GCC, pero además varias herramientas software y hardware que lo complementan. En este capítulo es una breve presentación de estas herramientas y una introducción a sus formas de uso.
D.1.
GCC
El compilador GCC, a estas alturas, no necesita presentación. Su cometido es traducir un código escrito en un lenguaje de alto nivel, C para el caso que nos afecta, y generar un código ensamblador equivalente. Además, GCC puede encargarse de manejar las otras utilidades software, por lo que el proceso de compilación termina reduciéndose a la mera invocación de GCC con los argumentos adecuados. La sintaxis básica del port de GCC para PIC18 es: pic18-gcc [opciones] programa.c Las opciones de GCC utilizadas con más frecuencia son las siguientes: -E: sólo ejecuta el preprocesador. Esta opción es útil para comprobar el funcionamiento de las macros del preprocesador que utilicemos en nuestro programa. -S: sólo ejecuta el preprocesador y compilador de C. Esta opción resulta de sumo interés cuando necesitamos verificar o ajustar a mano el código ensamblador generado por GCC. -c: Compila y ensambla pero no enlaza. Es habitual utilizar esta opción cuando compilamos programas compuestos por varios archivos de código y automatizamos el proceso de compilación con el programa make. 170
Compilador de C para el microcontrolador Microchip PIC18F4550
171
-o nombre_de_archivo: especifica el nombre del archivo de salida. -On: nivel de optimización. GCC ofrece varios niveles generales de optimización: 0,1,2,3 y s.
D.1.1.
Ensamblador en línea
GCC ofrece la posibilidad de introducir líneas de código ensamblador, escrito a mano, en el código C de un programa. Estas líneas de ensamblador no pasan por las etapas de compilación del código C, si no que pasan, salvando unos detalles que ahora explicaremos, directamente al archivo de código ensamblador resultante de la compilación. Las posibilidades que brinda esta funcionalidad son ilimitadas. Para insertar un bloque de código ensamblador entre nuestro código C emplearemos la sentencia especial __asm__. La sintaxis de esta sentencia especial es la siguiente: __asm__("bloque de instrucciones en ensamblador" : lista de datos de salida : lista de datos de entrada : lista de datos sobreescritos); El primer argumento es una cadena de caracteres con el código ensamblador que queremos obtener en el archivo final. Este bloque de código ensamblador no tiene que ser independiente del resto del programa escrito en C, si no que puede interactuar con las variables de éste que hayamos especificado en los otros tres argumentos. El segundo argumento es una lista de los registros y variables que escribe nuestro bloque de código ensamblador y cuyos valores pasarán a estar disponibles para el resto del programa. El tercer argumento es una lista de los registros, variables y valores inmediatos cuyo valor en ese momento deseamos utilizar en el bloque de código ensamblador. Por último, el cuarto argumento es una lista de los registros y variables que sobreescribe nuestro código ensamblador pero sin la necesidad de que los valores de estos estén disponibles para el programa en C una vez que haya terminado la ejecución de este bloque en ensamblador. Las instrucciones en ensamblador se refieren a los datos de salida, entrada y temporales mediante las incógnitas %n. Estas incógnitas corresponden a cada uno
Compilador de C para el microcontrolador Microchip PIC18F4550
172
de los parámetros de las tres listas por orden de aparición en las mismas. Asimismo, es posible especificar unas restricciones de almacenamiento para las variables y valores inmediatos utilizados en el bloque ensamblador que vamos a insertar. La mejor manera de comprender esto es con el siguiente ejemplo: char x = 1; char y = 2; __asm__ ("movf %1,W \n\t" "movwf %0 \n" : "=v" (x) : "v" (y) : ); Este bloque de código ensamblador está compuesto por dos instrucciones, separadas por los caracteres de salto de línea y tabulación. Cada una de las instrucciones hace uso de alguna de las variables de entrada o salida. La primera instrucción utiliza la incógnita %1, que se trata de una variable de entrada (tercer argumento de la función __asm__) y con la restricción de que debe estar almacenada en un registro virtual (restricción v). Esta variable de entrada es la variable y del programa en C. Por otro lado, la segunda instrucción del bloque en ensamblador opera con la incógnita %0, que corresponde a la variable x del código en C, con la restricción de que debe corresponder a un registro virtual y, además, este registro será sobreescrito (carácter =) por el código ensamblador dado. Dado que GCC optimizará todo el código que no resulte util, cuando se da el caso de un bloque de código ensamblador en línea cuyos valores de salida no se utilizan en el código C posterior, el compilador elimina el bloque de código ensamblador del programa final. Para evitar esto podemos aplicar el atributo __volatile__ a la llamada a __asm__ para que GCC no haga esa optimización con el bloque de ensamblador insertado a mano.
D.2.
GNU Pic Utilities
El conjunto de utilidades GPUTILS proporciona un ensamblador (gpasm), un enlazador (gplink) y un gestor de bibliotecas gplib, todos ellos para múltiples microcontroladores PIC.
Compilador de C para el microcontrolador Microchip PIC18F4550
D.2.1.
173
gpasm
gpasm es el ensamblador de las GPUTILS. Nos permite pasar de un archivo de código ensamblador a un objeto. La sintaxis de gpasm es: gpasm [opciones] programa.s Las opciones más importantes y comunes son: -c: genera código relocalizable. -o archivo_salida: establece un nombre al objeto de salida. Por defecto será el mismo que el dado como entrada, pero cambiando la extensión por “.o” si el código es relocalizable o por “.hex” si es absoluto. -p modelo_pic: especifica para qué microcontrolador se va a compilar el archivo de entrada. También se puede hacer desde el código ensamblador.
D.2.2.
gplink
gplink es el enlazador de código reubicable. Se encarga de relocalizar y enlazar los archivos objeto que forman un programa, y crear un único archivo listo para transferir a la memoria de nuestro microcontrolador. La sintaxis de gplink es: gplink [opciones] [objetos] [bibliotecas] Entre sus opciones, las más importantes y útiles para el trabajo cotidiano son: -I directorio. Especifica un directorio para incluir en la búsqueda de archivos, pudiendo existir varios parámetros -I en una misma llamada a gplink. -o programa.hex. Establece un nombre alternativo al programa compilado. Por defecto será “a.hex”. -s archivo. Especifica el archivo con las definiciones necesarias para el proceso de enlazado, correspondientes al microcontrolador especificado.
D.2.3.
gplib
gplib crea, modifica y extrae archivos objeto de bibliotecas. Con esto podemos agrupar una colección de objetos en un único archivo y pasar este único archivo a
Compilador de C para el microcontrolador Microchip PIC18F4550
174
gplink. gplink tomará solamente los objetos que necesite de la biblioteca, por lo que el tamaño final del programa no excede más allá de lo estrictamente necesario. La sintaxis de gplib es: gplib [opciones] biblioteca [objetos] Las opciones más importantes de gplib son: -c. Crea una nueva biblioteca. -d. Borra objetos de una biblioteca. -r. Añade o reemplaza un objeto de una biblioteca. -s. Muestra los símbolos globales de una biblioteca. -t. Muestra los objetos de una biblioteca. -x. Extrae los objetos de una biblioteca.
D.2.4.
Programador
Para programar un PIC es indispensable disponer de un sistema programador que soporte el modelo de microcontrolador deseado. Un programador consta de dos partes: el dispositivo hardware y el programa software que lo maneja. Existen numerosos dispositivos hardware que podemos fabricar o adquirirlos listos para utilizar. Las características destacables de un programador son la interfaz que utiliza para comunicación con el ordenador y los modelos de microcontrolador que soporta. Algunos ejemplos de dispositivos programadores son: TE-20X (puerto serie) GTP USB (puerto USB) Como ejemplos de software para programar PIC tenemos los siguientes: IC-Prog (Windows) Odyssey (Linux) WinPic800 (Windows)
Compilador de C para el microcontrolador Microchip PIC18F4550
D.2.5.
175
Archivos auxiliares
Como complemento a nuestro compilador de C necesitamos unos archivos extra que hacen posible la traducción a ensamblador de un programa escrito en C. Es indispensable disponer de la biblioteca libgcc.a, compilada junto con nuestro back-end de GCC. Esta biblioteca incluye las funciones de inicialización y reserva de marcos de memoria que utilizarán nuestros programas durante su ejecución. Por otro lado, también debemos tener el archivo de definiciones para el enlazador de nuestro modelo de microcontrolador, el PIC18F4550. Este archivo se llama p18f4550.lkr y, normalmente, viene incluido en el paquete GPUTILS del repositorio de nuestra distribución de Linux. La información que contiene este archivo es esencial para que gplink pueda efectuar el proceso de enlazado para nuestro modelo de microcontrolador.