Traducción Nota de Aplicación AVR4027: Ayudas y Trucos para optimizar tu código C para microcontroladores AVR de 8 bits
AVR04027: Ayudas y Trucos para optimizar tu código C para microcontroladores AVR de 8 bits Nota del Autor.- Este documento es una traducción no oficial del documento AVR4027 de ATMEL CORPORATION. Ha sido realizada por consentimiento y criterio del autor, los errores de traducción no tienen que ver con ATMEL CORPORATION. Sugerencias y comentarios a:
[email protected] Autor’s Note.- This document is a non-oficial translation from the documment AVR4027 from ATMEL CORPORATION It has been made by the autor’s consideration, translation mistakes have nothing to do with ATMEL CORPORATION. Feedback me at:
[email protected]
Características:
Núcleo Atmel® AVR® e introducción Atmel AVR GCC Ayuda y Trucos para reducir el tamaño del código Ayuda y Trucos para reducir el tiempo de ejecución Ejemplos de Aplicación
1. INTRODUCCION El núcleo AVR es una arquitectura RISC sintonizada para código en lenguaje C. Asegura el desarrollo de buenos productos con mas prestaciones a menor costo. Cuando se habla de optimización, usualmente nos referimos a dos aspectos: tamaño del código y velocidad de ejecución del código. En nuestros días, los compiladores de C tienen diferentes opciones de optimización para ayudar a los desarrolladores a obtener un código eficiente tanto en tamaño o velocidad. Sin embargo, una buena codificación en C brinda más oportunidades a los compiladores para optimizar el código como se desee. Y en algunos casos, la optimización para uno de los dos aspectos afecta o incluso causa degradación en el otro, entonces un desarrollador tiene que balancear los dos de acuerdo a sus necesidades específicas. Un entendimiento de algunos trucos y ayudas acerca de la codificación en C para un AVR de 8 bits ayuda a los desarrolladores a saber donde enfocarse en la mejora de eficiencia de código. En esta nota de aplicación, las ayudas están basadas en avr-gcc (compilador de lenguaje C). Sin embargo estas ayudas pueden ser implementadas en otros compiladores o con opciones similares de compilación y viceversa.
1
Hecho en Bolivia | SuaCh
Traducción Nota de Aplicación AVR4027: Ayudas y Trucos para optimizar tu código C para microcontroladores AVR de 8 bits
2. CONOCIENDO EL NUCLEO AVR Y ATMEL AVR GCC Antes de optimizar software de sistemas embebidos, es necesario tener un buen entendimiento de cómo está estructurado el núcleo AVR y que estrategias usa el AVR GCC para generar código eficiente para este procesador.
2.1
Arquitectura AVR 8-bit
AVR utiliza la arquitectura Harvard- con buses y memorias de programa y datos separados. Tiene un archivo de registro de rápido acceso de 32 x 8 registros de propósito general con un tiempo de acceso en un ciclo de reloj. Los 32 registros de trabajo son una de las llaves para la codificación eficiente en C. Estos registros tienen la misma función que la del tradicional acumulador, excepto que hay 32 de ellos. Las instrucciones aritméticas y lógicas del AVR trabajan en estos registros, de ahí toman menos espacio de instrucciones. En un ciclo de reloj, el AVR puede alimentar dos registros arbitrarios desde el archivo de registro a la ALU (autor> unidad aritmética lógica), realizando una operación, y respondiendo el resultado al archivo de registro. Las instrucciones en la memoria de programa son ejecutadas con un nivel simple de conducción a través de una tubería. Mientras una instrucción está siendo ejecutada, la siguiente instrucción es pre-cargada desde la memoria de programa. Este concepto permite a las instrucciones ser ejecutadas en cada ciclo de reloj. La mayoría de las instrucciones del AVR tienen una palabra de formato 16-bit. Cada dirección de la memoria de programa contiene una instrucción de 16 o 32 bits. Para más detalles, por favor remitirse a la sección "núcleo CPU AVR" en la hoja de datos del dispositivo.
2.2
AVR GCC
GCC representa a la colección de compiladores GNU. Cuando GCC es usado para AVR, este es comúnmente conocido con AVR GCC. El programa actual "gcc" es prefijado con "avr-" es decir, "avr-gcc". AVR GCC provee varios niveles de optimización. Estos son -O0, -O1, -O2, -O3 y -Os. En cada nivel, hay diferentes opciones de optimización disponibles, excepto -O0 que significa no optimización. Además de las opciones habilitadas en niveles de optimización, usted puede también habilitar separar las opciones de optimización para obtener una optimización específica. Por favor remítase al manual de colección de compiladores GNU como abajo para una completa lista de opciones y niveles de optimización. http://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html#Optimize-Options
2
Hecho en Bolivia | SuaCh
Traducción Nota de Aplicación AVR4027: Ayudas y Trucos para optimizar tu código C para microcontroladores AVR de 8 bits Además "avr-gcc", usa muchas otras herramientas trabajando juntas para producir la aplicación ejecutable final para el microcontrolador AVR. El grupo de herramientas es llamado toolchain (autor> conjunto de herramientas). En este toolchain de AVR, avr-libc sirve como una importante librería en C la cual provee muchas de las mismas funciones encontradas en una librería C regular Estándar y muchas funciones de librerías adicionales que son específicas para un AVR. El paquete AVR Libc provee un subconjunto de la librería estándar C para microcontroladores RISC AVR de 8 bits. Adicionalmente, la librería provee el código básico de inicio para la mayoría de las aplicaciones. Por favor compruebe el enlace de abajo para el manual de avr-libc, http://www.nongnu.org/avr-libc/user-manual/
2.3
Plataforma de Desarrollo
Los ejemplos de código y resultados de pruebas en este documento están basados en la siguiente plataforma y dispositivo, 1.
Entorno integrado de desarrollo (IDE): Atmel AVR Studio® 5 (Version: 5.0.1119).
2. AVR GCC 8-bit Toolchain Version: AVR_8_bit_GNU_Toolchain_3.2.1_292 (gcc version 4.5.1) 3. Dispositivo Objetivo: Atmel ATmega88PA.
3
Hecho en Bolivia | SuaCh
Traducción Nota de Aplicación AVR4027: Ayudas y Trucos para optimizar tu código C para microcontroladores AVR de 8 bits
3. AYUDAS Y TRUCOS PARA REDUCIR EL TAMAÑO DEL CODIGO En esta sección, listaremos algunas ayudas acerca de cómo reducir el tamaño del código. Para cada ayuda están dados la descripción y código de muestra.
3.1
Ayuda #1 - Tipo de datos y tamaño
Use el más pequeño tipo de dato aplicable como sea posible. Evalué su código y en particular los tipos de datos. Para leer un valor de 8 bits (1 byte) desde un registro se requiere una variable de 1-byte y no una variable de doble-byte, de este modo se ahorra espacio de código. El tamaño de tipos de datos sobre un AVR de 8 bits puede ser encontrado en la librería de cabecera y esta resumida en la Tabla 3-1.
Este consiente que ciertos cambios de compilador pueden cambiar esto (avr-gcc -mint8 cambia tipos de datos enteros a enteros de 8 bits). Los dos ejemplos en la Tabla 3-2 muestran el efecto de los diferentes tipos de datos y tamaños. La salida desde la utilidad avr-size muestra el espacio de código que hemos usado cuando esta aplicación es construida con -Os (optimización por tamaño).
En el ejemplo de la izquierda, utilizamos el tipo de dato int (2-bytes) como valor de retorno desde la función readADC() y en la variable temporal usada para almacenar el valor de retorno desde la función readADC(). 4
Hecho en Bolivia | SuaCh
Traducción Nota de Aplicación AVR4027: Ayudas y Trucos para optimizar tu código C para microcontroladores AVR de 8 bits En vez de eso en el ejemplo de la derecha estamos usando char (1-byte). La respuesta desde el registro ADCH es solo de ocho bits, y esto significa que un char es suficiente. Dos bytes grabados para retornar el valor de la función readADC() y la variable temporal en main son cambiados de int (2-bytes) a char (1-byte). NOTA
Hay un código de inicio antes de la ejecución desde main(). Esa es la razón porque un simple código en C consume cerca de 90 Bytes.
3.2
Ayuda #2 - Variables locales y globales
En la mayoría de los casos, El uso de variables globales no es recomendado. Use variables locales cuando sea posible. Si una variable es usada solo en una función, entonces deberá ser declarada dentro la función como una variable local. En teoría, la elección de si una variable debe ser declarada como global o local debe ser decidido por cómo es utilizada. Si una variable global es declarada, una dirección única en la SRAM será asignada para esta variable a tiempo de unir el programa. También para acceder a una variable global típicamente se necesitará bytes extras (usualmente dos bytes para una dirección de 16 bits de longitud) para obtener sus direcciones. Las variables locales son preferentemente asignadas a un registro o asignadas a la pila si son soportadas al declararse. Cuando una función se activa, las variables locales de la función también se activan. Una vez que la función sale (autor> termina), las variables locales de la función pueden ser removidas. En la tabla 3-3 hay dos ejemplos mostrando el efecto de las variables locales y globales.
En el ejemplo de la izquierda, hemos declarado una variable global de 1 byte. La salida desde la utilidad avr-size muestra que usamos 104 bytes de espacio de código y un byte de espacio de memoria de datos con nivel de optimizacion -Os (optimización por tamaño). 5
Hecho en Bolivia | SuaCh
Traducción Nota de Aplicación AVR4027: Ayudas y Trucos para optimizar tu código C para microcontroladores AVR de 8 bits En el ejemplo de la derecha, hemos declarado una variable dentro la función main() como local, el espacio de código se reduce a 84 bytes y no se utiliza la memoria SRAM.
3.3
Ayuda #3 - Indice del bucle
Los bucles son ampliamente utilizados en un código AVR 8 de bits. Están el bucle "while(){}", bucle "for()" y el bulce "do{}while()". Si la opción de optimización -Os está habilitada; El compilador optimizara los bucles automáticamente para tener el mismo tamaño de código. Sin embargo aun podemos reducir el tamaño del código levemente. Si utilizamos un bucle "do{}while()", un incremento o un decremento en el índice del bucle genera un tamaño de código diferente. Usualmente escribimos nuestros bucles contando desde cero al máximo valor (incremento), pero es más eficiente contar el bucle desde el máximo valor a cero (decremento). Esto es porque en un bucle de incremento, se necesita una instrucción de comparación para comparar el índice del bucle con el valor máximo en cada bucle para comprobar si el índice de bucle alcanza el valor máximo. Cuando usamos un bucle de decremento, esta comparación no es necesaria porque el resultado del decremento del índice del bucle pondrá a 1 la bandera Z (cero) en SREG si este llega a cero. En la Tabla 3-4 hay dos ejemplos mostrando el código generado por un bucle "do{}while()" con índices del bucle con incremento y decremento. El nivel de optimización utilizado aquí es -Os (optimización por tamaño).
6
Hecho en Bolivia | SuaCh
Traducción Nota de Aplicación AVR4027: Ayudas y Trucos para optimizar tu código C para microcontroladores AVR de 8 bits
Para tener una comparación clara en líneas de código C, este ejemplo está escrito como "do{conteo--;}while(conteo)" y no como "do{}while(--conteo);" usualmente utilizado en libros de lenguaje C. Los dos estilos generan el mismo código.
3.4 Ayuda #4 - Interferencia de bucle
La interferencia de bucle aquí se refiere a integrar declaraciones y operaciones de bucles diferentes a menos bucles o a un bucle, esto reduce el número de bucles en el código. En algunos casos, varios bucles son implementados uno por uno. Y esto puede llevar una larga lista de iteraciones. En este caso, interferir el bucle puede ayudar a incrementar la eficiencia de código por tener en realidad bucles combinados en uno. Interferir bucles reduce el tamaño de código y hace al código ejecutarse más rápido así como elimina la iteración encima de él. Del ejemplo en la Tabla 3-5, podemos ver cómo trabajan las interferencias de bucle.
7
Hecho en Bolivia | SuaCh
Traducción Nota de Aplicación AVR4027: Ayudas y Trucos para optimizar tu código C para microcontroladores AVR de 8 bits
3.5
Ayuda #5 - Constantes en el espacio de programa
Muchas aplicaciones exceden el tamaño de la SRAM, en las cuales se almacena datos, antes de que excedan el tamaño de la Flash. Variables constantes globales, tablas o arrays que nunca cambian, deberían ser colocadas en la sección de solo lectura (Flash o EEPROM en un AVR de 8 bits). Y de esta manera podemos ahorrar espacio precioso en la SRAM. En este ejemplo no utilizamos la palabra reservada de lenguaje C "const". Declarando un objeto "const" se entiende que este valor no será cambiado. "const" es utilizado para decirle al compilador que el dato es solo pare ser leído e incrementa las oportunidades de optimización. Esto no identifica donde debería ser almacenado el dato. Para almacenar datos dentro el espacio de la memoria de programa (solo-lectura) y recibirlos desde la memoria de programa, AVR-Libc provee un simple macro "PROGMEM" y un macro "pgm_read_byte". El macro PROGMEM y la función pgm_read_byte están definidas en el archivo sistema de cabecera . El siguiente ejemplo en la Tabla 3-6 muestra como ahorramos SRAM moviendo la cadena global dentro el espacio de memoria de programa.
Después que localizamos las constantes dentro la memoria de programa, vemos que el espacio de uso de la memoria de programa y de la memoria de datos se reducen. Sin embargo, Hay un pequeño costo cuando leemos el dato, porque la función de ejecución será más lenta que leer directamente de la SRAM.
8
Hecho en Bolivia | SuaCh
Traducción Nota de Aplicación AVR4027: Ayudas y Trucos para optimizar tu código C para microcontroladores AVR de 8 bits Si el dato almacenado en la memoria flash usa tiempos múltiples en el código, obtenemos un tamaño más pequeño usando una variable temporal en lugar de usar directamente el macro "pgm_read_byte" varias veces. Hay más macros y funciones en el archivo de sistema de cabecera para almacenar o recuperar diferentes tipos de datos desde/hacia la memoria de programa. Por favor vea el manual de usuario de avr-libc para más detalles.
3.6
Ayuda #6 - Tipos de Acceso: Static
Para datos globales, use la palabra reservada static cuando sea posible. Si hay variables globales declaradas con la palabra reservada static, puede accederse a ellas solo desde el archivo en el cual fueron definidas. Esto previene el uso no planeado de la variable (como variable externa) por otros archivos del código. Por otro lado, debería ser evitado declarar variables locales dentro una función como static. El valor de una variable local "static" necesita ser preservado entre llamadas a la función y la variable persiste por todo el programa. De este modo requiere un espacio de almacenamiento de datos (SRAM) permanente y código extra para acceder a la variable. Esto es similar a una variable global excepto que su ámbito está en la función donde fue definida. Una función static es más fácil de optimizar, porque su nombre es invisible fuera del archivo donde está declarada y no será llamada desde ningún otro archivo. Si una función static es llamada solo una vez en el archivo con optimización (-O1, -O2, -O3 y Os) activada, la función será optimizada automáticamente por el compilador como una función en-línea y ningún código assembler es producido para esta función. Por favor compruebe el ejemplo en la Tabla 3-7 para el efecto.
9
Hecho en Bolivia | SuaCh
Traducción Nota de Aplicación AVR4027: Ayudas y Trucos para optimizar tu código C para microcontroladores AVR de 8 bits
NOTA
Si la función es llamada múltiples veces, no será optimizada a una función en-línea, porque esto generará más código que la simple llamada a la función.
3.7
Ayuda #7 - Instrucciones assembler de bajo nivel
Las instrucciones en assembler bien codificadas siempre son el código mejor optimizado. Un inconveniente del código assembler es su sintaxis no-portable, así que no es recomendado para programadores en la mayoría de los casos. Sin embargo usando macros assembler se reduce la dificultad a menudo asociada al código assembler y mejora la portabilidad y entendimiento. Use macros assembler en vez de funciones para tareas que generan menos de 2 a 3 líneas de código assembler. El ejemplo un la Tabla 3-8 muestra el uso de código de un macro assembler comparado con el uso de una función.
10
Hecho en Bolivia | SuaCh
Traducción Nota de Aplicación AVR4027: Ayudas y Trucos para optimizar tu código C para microcontroladores AVR de 8 bits
Para más detalles de cómo usar lenguaje assembler (autor> ensamblador) con C en AVR de 8 bits, por favor diríjase a la sección "Inline Assembler Cookbook" en el manual de usario de avr-libc.
11
Hecho en Bolivia | SuaCh
Traducción Nota de Aplicación AVR4027: Ayudas y Trucos para optimizar tu código C para microcontroladores AVR de 8 bits
4. AYUDAS Y TRUCOS PARA RECUDIR EL TIEMPO DE EJECUCION En esta sección, listamos algunos trucos de cómo reducir el tiempo de ejecución. Para cada truco, están dadas algo de descripción y código de muestra.
4.1 Ayuda #8 - Tipos de datos y tamaño
Además de reducir el tamaño del código, seleccionando un tipo de dato y tamaño apropiado también reducirá el tiempo de ejecución. Para AVR de 8 bits, El acceso al valor de 8 bits (Byte) siempre es el camino más efectivo. Por favor compruebe el ejemplo en la Tabla 4-1 para ver la diferencia de una variable de 8 bits y una de 16 bits.
NOTA
El bucle será desenrollado automáticamente por el compilador con la opción -O3. Después el bucle será expandido en operaciones repetitivas indicadas por el índice del bucle, así que para este ejemplo no hay diferencia con la opción -O3 habilitada.
12
Hecho en Bolivia | SuaCh
Traducción Nota de Aplicación AVR4027: Ayudas y Trucos para optimizar tu código C para microcontroladores AVR de 8 bits 4.2 Ayuda#9 - Declaración Condicional Usualmente un pre-decremento o post-decremento (o pre-incrementos y post-decrementos) en líneas normales de código no hace diferencia. Por ejemplo, "i--" e "--i", simplemente generan el mismo código. Sin embargo, usando estos operadores como índices de bucles y en declaraciones condicionales hacen al código generado diferente. Como se explica en la Ayuda #3 - índice de bucle, usando un índice de bucle de decremento resulta en un código más pequeño. Esto es también algo útil para obtener un código más veloz en declaraciones condicionales. Es más, el pre-decremento y post-decremento tienen también resultados diferentes. Desde los ejemplos en la Tabla 4-2, podemos ver que se genera un código más veloz con una declaración condicional de pre-decremento. El contador de ciclos representa el tiempo de ejecución del bucle más largo.
La variable "loop_cnt" esta asignada con diferentes valores en los dos ejemplos en la Tabla 4-2 para asegurarse que el ejemplo trabaje igual: PORTC0 es cambiado nueve veces mientras PORTB0es cambiado nueve veces en cada ejecución del bucle.
13
Hecho en Bolivia | SuaCh
Traducción Nota de Aplicación AVR4027: Ayudas y Trucos para optimizar tu código C para microcontroladores AVR de 8 bits 4.3 Ayuda#10 - Bucles desenrollados
En algunos casos, podemos desenrollar bucles para acelerar la ejecución del código. Esto es especialmente efectivo para bucles cortos. Después que un bucle es desenrollado, no hay índices de bucle para ser comprobados y menos ramificaciones son ejecutadas en cada ronda en el bucle. El ejemplo en la Tabla 4-3 intercambiará un pin de un puerto diez veces.
Al desenrollar el bucle do{} while(), la velocidad del código se incrementa significativamente de 80 a 50 ciclos de reloj. Sea consciente que el tamaño del código se incrementa de 94 a 142 bytes después de desenrollar el bucle. Esto es también un ejemplo del sacrificio de optimización entre velocidad y tamaño de código. NOTA
Si la opción -O3 está habilitada en este ejemplo, el compilador desenrollará el bucle automáticamente y generará el mismo código como el bucle desenrollado manualmente.
14
Hecho en Bolivia | SuaCh
Traducción Nota de Aplicación AVR4027: Ayudas y Trucos para optimizar tu código C para microcontroladores AVR de 8 bits 4.4 Ayuda#11 - Control de flujo: if-else y switch-case
"if-else" y "switch-case" son ampliamente usados en código C; una organización apropiada de las ramificaciones puede reducir la ejecución del código. Para "if-else", siempre coloque las condiciones más probables en primer lugar. Después coloque las condiciones que son menos probables de ejecutarse. Así de esta manera se ahorra tiempo para la mayoría de los casos. Usando "switch-case" se puede eliminar los inconvenientes de "if-else", porque para un "switchcase", el compilador usualmente genera tablas de búsqueda con índice y salto directamente al lugar correcto. Si es difícil de usar "switch-case", se puede dividir la ramas "if-else" en sub-ramas más pequeñas. Este método reduce ejecuciones para el caso de la condición más improbable. En el ejemplo de abajo, se toma datos desde el ADC y luego se envía vía USART. "ad_result